# C 編程,第 3 部分:常見問題
> 原文:[Processes, Part 1: Introduction](https://github.com/angrave/SystemProgramming/wiki/Processes%2C-Part-1%3A-Introduction)
> 校驗:[_stund](https://github.com/hqiwen)
> 自豪地采用[谷歌翻譯](https://translate.google.cn/)
C 程序員常犯的錯誤是什么?
## 內存錯誤
### 字符串常量是常量
```c
char array[] = "Hi!"; // 數組是一個可變的復制
strcpy(array, "OK");
char *ptr = "Can't change me"; // ptr 指向不可變的內存
strcpy(ptr, "Will not work");
```
字符串文字是存儲在程序代碼段中的字符數組,它是不可變的。兩個字符串文字可以在內存中共享相同的空間。一個例子如下:
```c
char * str1 = "Brandon Chong is the best TA";
char * str2 = "Brandon Chong is the best TA";
str1 == str2;// true
```
`str1`和`str2`指向的字符串實際上可能位于內存中的相同位置。
但是,字符數組包含已從代碼段復制到棧或靜態內存中的文字值。以下 char 數組不駐留在內存中的相同位置。
```c
char arr1[] = "Brandon Chong didn't write this";
char arr2[] = "Brandon Chong didn't write this";
arr1 == arr2;// false
&arr1[0] == &arr2[0];// false
```
### 緩沖區溢出/下溢
```c
#define N (10)
int i = N, array[N];
for( ; i >= 0; i--) array[i] = i;
```
C 不檢查指針是否有效。上面的例子寫入`array[10]`,它位于數組邊界之外。這可能會導致內存損壞,因為該內存位置可能正在用于其他內容。在實踐中,這可能更難以發現,因為溢出/下溢可能發生在使用不安全的庫調用中或者設置錯誤的大小限制在安全庫的調用中,例如
```c
gets(array); // 我們希望輸入比我的array短!(永遠不要使用gets)
fgets(array, 4096, stdin); //Whoops
```
### 返回指向自動變量的指針
```c
int *f() {
int result = 42;
static int imok;
int *p;
{
int x = result;
p = &x;
}
//imok = *p; // Not OK: x不在定義域內
//return &result; // Not OK: result將不在定義域內在函數返回之后
return &imok; // OK - 靜態成員不在棧區
}
```
自動變量僅在函數的生命周期內綁定到棧內存。函數返回后,儲存在棧區的數據將變為`undefined`,靜態變量儲存在數據區,當函數返回后依然可以被使用。
### sizeof(type *) 對比 sizeof(type)
```c
struct User {
char name[100];
};
typedef struct User user_t;
user_t *user = (user_t *) malloc(sizeof(user));
```
在上面的例子中,我們需要為 struct 分配足夠的字節。相反,我們分配了足夠的字節來保存指針。一旦我們開始使用用戶指針,我們將破壞內存。正確的代碼如下所示。
```c
struct User {
char name[100];
};
typedef struct User user_t;
user_t * user = (user_t *) malloc(sizeof(user_t));
```
### 字符串需要`strlen(s)+1`個字節
每個字符串在最后一個字符后必須有一個空字節。要存儲字符串`"Hi"`,需要 3 個字節:`[H] [i] [\0]`。
```c
char *strdup(const char *input) { /* return a copy of 'input' */
char *copy;
copy = malloc(sizeof(char*)); /* nope! 分配了一個指針大小的空間,不是一個字符串 */
copy = malloc(strlen(input)); /* Almost...null 終止符被遺忘了 */
copy = malloc(strlen(input) + 1); /* That's right. */
strcpy(copy, input); /* strcpy 將會提供null終止符 */
return copy;
}
```
### 使用未初始化的變量
```c
int myfunction() {
int x;
int y = x + 2;
...
```
自動變量保存垃圾(無論位模式發生在內存中)。假設它將始終初始化為零是錯誤的。
### 初始化內存錯誤
```c
void myfunct() {
char array[10];
char *p = malloc(10);
printf("%s %s\n", array, p);
```
自動(棧內)變量和用`malloc`分配的堆內存不會自動初始化為零。這個函數的結果是未定義行為。
### 雙重釋放
```c
char *p = malloc(10);
free(p);
// .. later ...
free(p);
```
將同一塊內存釋放兩次是錯誤的。
### 游蕩的指針
```c
char *p = malloc(10);
strcpy(p, "Hello");
free(p);
// .. later ...
strcpy(p,"World");
```
訪問被釋放的內存是未定義行為。防御性編程實踐是在釋放內存后立即將指針設置為 null,
下面的宏可以完成這種操作。
```c
#define safer_free(p) {free(p); (p) = NULL;}
```
### 忘記了復制`getline`緩存
```c
#include <stdio.h>
int main(void){
char *line = NULL;
size_t linecap = 0;
char *strings[3];
// assume stdin contains "1\n2\n\3\n"
for (size_t i = 0; i < 3; ++i)
strings[i] = getline(&line, &linecap, stdin) >= 0 ? line : "";
// this prints out "3\n3\n\3" instead of "3\n\2\n1\n"
for (size_t i = 3; i--;) // i=2,1,0
printf("%s", strings[i]);
}
```
getline重復是一個緩存,所有的指針在strings指向同一塊內存。我們可以通過對strings[i]進行深復制來修復它。
```c
strings[i] = getline(&line, &linecap, stdin) >= 0 ? strdup(line) : "";
```
## 邏輯和程序進程錯誤
### 忘了break在使用case之后
```c
int flag = 1; // 會打印出所有行
switch(flag) {
case 1: printf("I'm printed\n");
case 2: printf("Me too\n");
case 3: printf("Me three\n");
}
```
不間斷的 Case 語句將繼續執行下一個 case 語句的代碼。正確的代碼如下所示。最后一個語句的中斷是不必要的,因為在最后一個語句之后不再需要執行。但是,如果添加更多,則可能會導致一些錯誤。
```c
int flag = 1; // Will print only "I'm printed\n"
switch(flag) {
case 1:
printf("I'm printed\n");
break;
case 2:
printf("Me too\n");
break;
case 3:
printf("Me three\n");
break; //unnecessary
}
```
### 賦值和相等
```c
int answer = 3; // Will print out the answer.
if (answer = 42) { printf("I've solved the answer! It's %d", answer);}
```
編譯器會警告你這個錯誤,如果你想要執行一個賦值,添加一個額外的括號去消除這個警告。
```c
ssize_t x;
if ( (x = read(somefd, somebuf, somenum)) ){
// do something
}
```
### 未聲明或不正確的原型功能
```c
#include <stdio.h>
int main(void){
int start = time();
printf("%d\n", start);
}
```
系統函數'time'實際上是一個參數(指向一些可以接收 time_t 結構的內存的指針)。編譯器沒有捕獲此錯誤,因為程序員沒有通過包含`time.h`來提供有效的函數原型
### 額外的分號
```c
for(int i = 0; i < 5; i++) ; printf("I'm printed once");
while(x < 10); x++ ; // X 永遠不會增加
```
但是,以下代碼完全可以。
```c
for(int i = 0; i < 5; i++){
printf("%d\n", i);;;;;;;;;;;;;
}
```
擁有這種代碼是可以的,因為 C 語言使用分號(;)來分隔語句。如果分號之間沒有語句,則無需執行任何操作,編譯器將繼續執行下一個語句
## 其他陷阱
### 預處理器
什么是預處理器?這是在實際編譯程序之前編譯器執的操作。它是一個**復制和粘貼**命令。這意味著如果我執行以下操作。
```c
#define MAX_LENGTH 10
char buffer[MAX_LENGTH]
```
預處理后,它看起來像這樣。
```c
char buffer[10]
```
### C 預處理器宏和副作用
```c
#define min(a,b) ((a)<(b) ? (a) : (b))
int x = 4;
if(min(x++, 100)) printf("%d is six", x);
```
宏是簡單的文本替換,因此上面的示例擴展為`x++ < 100 ? x++ : 100`(為清楚起見省略了括號)
### C 預處理器宏和優先級
```c
#define min(a,b) a<b ? a : b
int x = 99;
int r = 10 + min(99, 100); // r is 100!
```
宏是簡單的文本替換,因此上面的示例擴展為`10 + 99 < 100 ? 99 : 100`
### C 預處理器邏輯問題
```c
#define ARRAY_LENGTH(A) (sizeof((A)) / sizeof((A)[0]))
int static_array[10]; // ARRAY_LENGTH(static_array) = 10
int* dynamic_array = malloc(10); // ARRAY_LENGTH(dynamic_array) = 2 or 1
```
宏有什么問題?好吧,如果我們有一個像第一個數組那樣的靜態數組,那么它是有效的,因為靜態數組的 sizeof 返回數組占用的字節數,并將它除以 sizeof(an_element)將得到條目數。但是如果我們使用指向一塊內存的指針,那么獲取指針的大小并將其除以第一個條目的大小并不總能給出數組的大小。
### `sizeof`的副作用
```c
int a = 0;
size_t size = sizeof(a++);
printf("size: %lu, a: %d", size, a);
```
代碼打印出來的是什么?
```
size: 4, a: 0
```
因為 sizeof 實際上并未在運行時進行評估。編譯器分配所有表達式的類型并丟棄表達式的額外結果。
- UIUC CS241 系統編程中文講義
- 0. 簡介
- #Informal 詞匯表
- #Piazza:何時以及如何尋求幫助
- 編程技巧,第 1 部分
- 系統編程短篇小說和歌曲
- 1.學習 C
- C 編程,第 1 部分:簡介
- C 編程,第 2 部分:文本輸入和輸出
- C 編程,第 3 部分:常見問題
- C 編程,第 4 部分:字符串和結構
- C 編程,第 5 部分:調試
- C 編程,復習題
- 2.進程
- 進程,第 1 部分:簡介
- 分叉,第 1 部分:簡介
- 分叉,第 2 部分:Fork,Exec,等等
- 進程控制,第 1 部分:使用信號等待宏
- 進程復習題
- 3.內存和分配器
- 內存,第 1 部分:堆內存簡介
- 內存,第 2 部分:實現內存分配器
- 內存,第 3 部分:粉碎堆棧示例
- 內存復習題
- 4.介紹 Pthreads
- Pthreads,第 1 部分:簡介
- Pthreads,第 2 部分:實踐中的用法
- Pthreads,第 3 部分:并行問題(獎金)
- Pthread 復習題
- 5.同步
- 同步,第 1 部分:互斥鎖
- 同步,第 2 部分:計算信號量
- 同步,第 3 部分:使用互斥鎖和信號量
- 同步,第 4 部分:臨界區問題
- 同步,第 5 部分:條件變量
- 同步,第 6 部分:實現障礙
- 同步,第 7 部分:讀者編寫器問題
- 同步,第 8 部分:環形緩沖區示例
- 同步復習題
- 6.死鎖
- 死鎖,第 1 部分:資源分配圖
- 死鎖,第 2 部分:死鎖條件
- 死鎖,第 3 部分:餐飲哲學家
- 死鎖復習題
- 7.進程間通信&amp;調度
- 虛擬內存,第 1 部分:虛擬內存簡介
- 管道,第 1 部分:管道介紹
- 管道,第 2 部分:管道編程秘密
- 文件,第 1 部分:使用文件
- 調度,第 1 部分:調度過程
- 調度,第 2 部分:調度過程:算法
- IPC 復習題
- 8.網絡
- POSIX,第 1 部分:錯誤處理
- 網絡,第 1 部分:簡介
- 網絡,第 2 部分:使用 getaddrinfo
- 網絡,第 3 部分:構建一個簡單的 TCP 客戶端
- 網絡,第 4 部分:構建一個簡單的 TCP 服務器
- 網絡,第 5 部分:關閉端口,重用端口和其他技巧
- 網絡,第 6 部分:創建 UDP 服務器
- 網絡,第 7 部分:非阻塞 I O,select()和 epoll
- RPC,第 1 部分:遠程過程調用簡介
- 網絡復習題
- 9.文件系統
- 文件系統,第 1 部分:簡介
- 文件系統,第 2 部分:文件是 inode(其他一切只是數據...)
- 文件系統,第 3 部分:權限
- 文件系統,第 4 部分:使用目錄
- 文件系統,第 5 部分:虛擬文件系統
- 文件系統,第 6 部分:內存映射文件和共享內存
- 文件系統,第 7 部分:可擴展且可靠的文件系統
- 文件系統,第 8 部分:從 Android 設備中刪除預裝的惡意軟件
- 文件系統,第 9 部分:磁盤塊示例
- 文件系統復習題
- 10.信號
- 過程控制,第 1 部分:使用信號等待宏
- 信號,第 2 部分:待處理的信號和信號掩碼
- 信號,第 3 部分:提高信號
- 信號,第 4 部分:信號
- 信號復習題
- 考試練習題
- 考試主題
- C 編程:復習題
- 多線程編程:復習題
- 同步概念:復習題
- 記憶:復習題
- 管道:復習題
- 文件系統:復習題
- 網絡:復習題
- 信號:復習題
- 系統編程笑話