Skip to content

Latest commit

 

History

History
325 lines (239 loc) · 8.49 KB

10.md

File metadata and controls

325 lines (239 loc) · 8.49 KB

C 编程,第 3 部分:常见问题

原文:Processes, Part 1: Introduction

校验:_stund

自豪地采用谷歌翻译

C 程序员常犯的错误是什么?

内存错误

字符串常量是常量

char array[] = "Hi!"; // 数组是一个可变的复制
strcpy(array, "OK");

char *ptr = "Can't change me"; // ptr 指向不可变的内存
strcpy(ptr, "Will not work");

字符串文字是存储在程序代码段中的字符数组,它是不可变的。两个字符串文字可以在内存中共享相同的空间。一个例子如下:

char * str1 = "Brandon Chong is the best TA";
char * str2 = "Brandon Chong is the best TA";
str1 == str2;// true

str1str2指向的字符串实际上可能位于内存中的相同位置。

但是,字符数组包含已从代码段复制到栈或静态内存中的文字值。以下 char 数组不驻留在内存中的相同位置。

char arr1[] = "Brandon Chong didn't write this";
char arr2[] = "Brandon Chong didn't write this";
arr1 == arr2;// false
&arr1[0] == &arr2[0];// false

缓冲区溢出/下溢

#define N (10)
int i = N, array[N];
for( ; i >= 0; i--) array[i] = i;

C 不检查指针是否有效。上面的例子写入array[10],它位于数组边界之外。这可能会导致内存损坏,因为该内存位置可能正在用于其他内容。在实践中,这可能更难以发现,因为溢出/下溢可能发生在使用不安全的库调用中或者设置错误的大小限制在安全库的调用中,例如

gets(array); // 我们希望输入比我的array短!(永远不要使用gets)
fgets(array, 4096, stdin); //Whoops

返回指向自动变量的指针

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)

struct User {
   char name[100];
};
typedef struct User user_t;

user_t *user = (user_t *) malloc(sizeof(user));

在上面的例子中,我们需要为 struct 分配足够的字节。相反,我们分配了足够的字节来保存指针。一旦我们开始使用用户指针,我们将破坏内存。正确的代码如下所示。

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]

  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;
}

使用未初始化的变量

int myfunction() {
  int x;
  int y = x + 2;
...

自动变量保存垃圾(无论位模式发生在内存中)。假设它将始终初始化为零是错误的。

初始化内存错误

void myfunct() {
   char array[10];
   char *p = malloc(10);
   printf("%s %s\n", array, p);

自动(栈内)变量和用malloc分配的堆内存不会自动初始化为零。这个函数的结果是未定义行为。

双重释放

  char *p = malloc(10);
  free(p);
//  .. later ...
  free(p); 

将同一块内存释放两次是错误的。

游荡的指针

  char *p = malloc(10);
  strcpy(p, "Hello");
  free(p);
//  .. later ...
  strcpy(p,"World"); 

访问被释放的内存是未定义行为。防御性编程实践是在释放内存后立即将指针设置为 null, 下面的宏可以完成这种操作。

#define safer_free(p) {free(p); (p) = NULL;}

忘记了复制getline缓存

#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]进行深复制来修复它。

strings[i] = getline(&line, &linecap, stdin) >= 0 ? strdup(line) : "";

逻辑和程序进程错误

忘了break在使用case之后

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 语句的代码。正确的代码如下所示。最后一个语句的中断是不必要的,因为在最后一个语句之后不再需要执行。但是,如果添加更多,则可能会导致一些错误。

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
}

赋值和相等

int answer = 3; // Will print out the answer.
if (answer = 42) { printf("I've solved the answer! It's %d", answer);}

编译器会警告你这个错误,如果你想要执行一个赋值,添加一个额外的括号去消除这个警告。

ssize_t x;
if ( (x = read(somefd, somebuf, somenum)) ){
  // do something
}

未声明或不正确的原型功能

#include <stdio.h>
int main(void){
  int start = time();
  printf("%d\n", start);
}

系统函数'time'实际上是一个参数(指向一些可以接收 time_t 结构的内存的指针)。编译器没有捕获此错误,因为程序员没有通过包含time.h来提供有效的函数原型

额外的分号

for(int i = 0; i < 5; i++) ; printf("I'm printed once");
while(x < 10); x++ ; // X 永远不会增加

但是,以下代码完全可以。

for(int i = 0; i < 5; i++){
    printf("%d\n", i);;;;;;;;;;;;;
}

拥有这种代码是可以的,因为 C 语言使用分号(;)来分隔语句。如果分号之间没有语句,则无需执行任何操作,编译器将继续执行下一个语句

其他陷阱

预处理器

什么是预处理器?这是在实际编译程序之前编译器执的操作。它是一个复制和粘贴命令。这意味着如果我执行以下操作。

#define MAX_LENGTH 10
char buffer[MAX_LENGTH]

预处理后,它看起来像这样。

char buffer[10]

C 预处理器宏和副作用

#define min(a,b) ((a)<(b) ? (a) : (b))
int x = 4;
if(min(x++, 100)) printf("%d is six", x);

宏是简单的文本替换,因此上面的示例扩展为x++ &lt; 100 ? x++ : 100(为清楚起见省略了括号)

C 预处理器宏和优先级

#define min(a,b) a<b ? a : b
int x = 99;
int r = 10 + min(99, 100); // r is 100!

宏是简单的文本替换,因此上面的示例扩展为10 + 99 &lt; 100 ? 99 : 100

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的副作用

int a = 0;
size_t size = sizeof(a++);
printf("size: %lu, a: %d", size, a);

代码打印出来的是什么?

size: 4, a: 0 

因为 sizeof 实际上并未在运行时进行评估。编译器分配所有表达式的类型并丢弃表达式的额外结果。