《C陷阱与缺陷》读书笔记与总结

《C陷阱与缺陷》读书笔记

目录

  • 《C陷阱与缺陷》读书笔记
  • 一,函数指针和指针函数
  • 二,C语言数组
  • 三,空指针并非空字符串
  • 四,整数溢出
  • 五,为main函数提供返回值
  • 六,声明和定义(extern)
  • 七,命名冲突与 static修饰符
  • 八,形参、实参和返回值
  • 九, 检查外部变量类型
  • 十,预处理器-不能忽视宏定义中的空格
  • 十一,预处理器-宏不是函数
  • 十二,宏不是类型定义
  • 十三,比较时把常量写在左边

一,函数指针和指针函数

float *g() , (*h)();

()的优先级大于 * , 因此 *g() 为 *( g() ) ,该函数 g() 的返回类型为指向浮点数的指针 float * 。

而h是一个函数指针,返回值类型为 float。

二,C语言数组

  1. C语言中只有一维数组,数组的大小需在编译时期确定(常数)数组的元素可以是任意类型的对象(包括数组)
  2. 对于一个数组,只能做两件事:1,确定数组的大小。2,获得下标为0的元素的指针。

以数组下标进行运算,实际上都是通过指针进行的。

三,空指针并非空字符串

#define NULL 0

当常数0被转换为指针使用时,这个指针绝对不能被解除引用(dereference),换言之,当我们将0赋值给一个指针变量时,绝对不能使用该指针所指向的内存中的存储内容。

四,整数溢出

无符号数无溢出

if((unsigned)a) + (unsigned)b > INT_MAX)

五,为main函数提供返回值

main函数与其他任何函数一样,若未显式声明返回类型,则默认为整型。

六,声明和定义(extern)

int a;

如果出现在所有函数体之外,则称为外部对象a的定义。a是一个外部整型变量,同时为a分配了存储空间,默认为0;

int a = 7;

在定义a的同时也为a明确指定了初始值。

extern int a; //这是一种引用,而非定义

a是一个外部整型变量,extern说明a的存储空间是在程序的其他地方分配的,只是一个外部对象的显式引用。

如果一个程序包含了 extern int a ,那么这个程序就必须在别的某个地方包括: int a; 那么这两个语句既可以在同一个源文件中,也可以位于程序的不同源文件之中。

七,命名冲突与 static修饰符

两个具有相同名称的外部对象,实际上代表同一个对象,如在两个不同的源文件都包括了定义:int a;
则或表示程序错误(禁止外部变量重复定义),或在两个源文件中共享a的同一个实例。

static 修饰符能解决命名冲突问题:

static int a;

static 具有文件作用域,将a的作用域限制在当前源文件中,对其他源文件,a并不可见。
static同样可以修饰函数,建议:若一个函数仅被同一个源文件中的其他函数调用,我们应当声明该函数为static。

八,形参、实参和返回值

任何C函数都有一个形参列表,列表中的每个参数都是一个变量,改变量在函数的调用过程中被初始化。

任何C函数都有自己的返回值:void/其他

如果一个函数在定义域之前被调用,那么它的返回值默认为 整型

  1. 如果一个函数在被定义或声明之前被调用,那么他的返回值类型默认为整型
  2. 在多文件中,若函数的定义和调用位于不同的文件中,则需要在调用前声明。

九, 检查外部变量类型

外部变量的定义和声明必须保持一致。
比如有两个文件,其中f2.c中定义了一个 long类型的变量n,但是在f1.c文件中却声明为int类型:

  • f1.c: extern int n;
  • f2.c:long n;

十,预处理器-不能忽视宏定义中的空格

如下所示为ref.h文件中定义的宏:

#ifndef _REF_H
#define _REF_H

#define F(x) (x+1)
#define G (x) (x+1) 

#endif

在main函数中使用:

    int a = F(2);
    int b = G(3);

在使用 G(x)宏定义时出现如下错误:
《C陷阱与缺陷》读书笔记与总结_第1张图片

错误原因在于G和 (x)之间多了一个空格,导致编译器将G作为一个宏,而不是G(x).

十一,预处理器-宏不是函数

注意:

  • 要把每个参数都用括号括起来
  • 整个表达式也要括起来
#define ABS1(a) ((a) > 0 ? (a) : -(a))
#define ABS2(a) (a > 0 ? a : -a)
#define MAX(a,b) ((a) > (b) ? (a) : (b))

如上代码片段中的两个宏定义所示,在宏定义ABS2(a)中,每个参数不用括号保护,当a=x-y的情况下,直接使用字符替换就会发生错误:

    int x = 4;
    int y = 6;
    int z1 = ABS1(x-y);
    int z2 = ABS2(x-y);

根据字符串简单替换的规则:

z1 = ABS1(x-y) = ((x-y) > 0 ? (x-y) : -(x-y)) = -2 > 0 ? -2 : 2 = 2
z2 = ABS2(x-y) = (x-y > 0 ? x-y : -x-y ) = -2 > 0 ? -2 : -4-6 = -10

所以使用括号来保护表达式,是很有必要的。

此外,还要确保宏中的参数不会出现二次变化(没有副作用),如以下代码片段:

int i = 0, arr = [2,4,6,8];
int a = MAX(arr[i++],1); 

按照一般思维:i是先使用后自加,所以arr[i++]为2,MAX(2,1)结果等于2,即a等于2。
但是按照宏的简单字符串替换原则:

int a = MAX(arr[i++],1) = arr[i++] > 1 ? arr[i++] : 1 ;

当执行完 arr[i++] > 1 操作后, 还会执行 ? 后面的 arr[i++],此时i等于1,导致a的结果等于4;

最后还需注意的是 宏的嵌套,可能会产生非常庞大的表达式,始终要记住宏是简单的字符替换,而不是函数,C语言中函数可以使用递归(利用了栈的原理)来达到精简代码的目的,如以下代码:

MAX(a,MAX(b,MAX(c,d)));

将其展开后,程序将非常臃肿,不利于后期调试。

总而言之,宏是一种 以空间换时间 的一中策略,在这一点上,类似内联函数。

十二,宏不是类型定义

类型定义可以使用 typedef:

// 宏定义和typedef 一致
#define FOOTYPE struct foo
typedef struct foo FOOTYPE

// T1和T2所表示的类型有可能会不一致
#define T2 struct foo *
typedef strcut foo * T1

//比如
T1 a , b; // struct foo * a,b; a是指针类型,b是结构体类型
T2 a , b; // strcut foo * a, strcut foo *b; a和b都是指针类型

十三,比较时把常量写在左边

比如:

if( 'c' == a)
{
    // 推荐这种写法,当少写一个等于号=时,('c' = a)编译器会立即报错
}

if( a == 'c')
{
    // 当少写一个等于号=时,(a = 'c')编译器不会报错,增加了调试难度
}

如上述程序所示,一个鲁棒性高,稳定性高的程序,一定要注意一个原则:

编译时报错 > 运行时报错

也就是说,程序中的bug要早发现早解决,程序运行过程中出现的bug其查找难度倍增。

你可能感兴趣的:(Linux_C,C语言,C陷阱与缺陷)