目录

第一章:词法的陷阱

1.1.“=”与“==”不同:

1.3词法分析的“贪心法”:

1.4整型常量:

1.5字符与字符串:

第二章:语法陷阱

2.1函数声明:

2.2 运算符优先级问题:

2.3 注意作为语句结束标记的分号:

2.4 switch语句:

2.5 函数调用:

2.6 else悬挂问题:

第三章:语义的陷阱

3.1指针与数组:

3.2 非数组的指针:

3.3 作为参数的数组声明:

3.4 避免“举隅法”:

3.5 空指针与空字符串:

3.6边界计算与不对称边界:

3.7 求值顺序:

3.8 运算符&&、||和!:

3.10 main函数的返回值:

第四章:链接

4.1 链接器:

4.2 声明、定义:

4.3 命名冲突和static修饰符:

4.4 形参、实参与返回值:

4.5 检查外部类型:

4.6 头文件:

第五章:库函数

5.1 getchar函数:

5.2 fseek函数:

5.3 setbuf函数:

5.4 errno变量:

第六章:预处理器

6.1  宏定义的空格不能掉:

6.2 宏不是函数:

6.3 宏不是语句:

6.4 宏不是类型定义:

第七章:可移植性缺陷

7.2 标识符的名称限制:

7.3 整数的大小:

7.4 字符是有符号还是无符号问题:

7.5 移位运算符的一些注意事项:

7.6 内存位置0:


第一章:词法的陷阱

C语言的编写的程序由一个个字符组成,组成的字符不同,字符处于的环境不同,编译器可能会对这些字符产生不同的理解。编译器在翻译时会将程序分为一个个字符。在编译时,符号之间的空白(空白包括空格符、制表符和换行符)将会被忽略。

所以下面的两组代码的执行结果是相同的:

 //代码1if(a > 10){b =a;}//代码2if (a > 10){b = a;}

1.1.“=”与“==”不同:

在我们的数学中,如果变量x的值与变量y的值相等,那么我们通常写作“x=y”;而在C语言中,两个变量要是相等,我们需要写作:x==y

为什么要这么麻烦呢?因为我们在C语言中,等号有另外的用途---赋值,我们用等号来表示赋值的操作,例如:变量a的值为2,我们就写作:a=2 

这类问题出现最多于if语句的判断条件中,例如我们要写如果变量a等于2,就进入if语句中,我们很容易就写成:if(a=2) 。最麻烦的是赋值后程序依然照常进入判断,这样会给我们排查问题增加麻烦。所以下面有一种更直观避免错误的写法:if(2==a)

1.3词法分析的“贪心法”:

因为编译器是将一个个符号组成字符,然后翻译字符。就像我们将一个个英文字母组成单词,然后再由单词组成句子一样。所以我们要知道编译器如何去分析这些符号是否能组成字符,这样就能避免写出错的程序;

在C语言中,编译器对待这个问题有一个简单的规则:每一个符号应该包含尽可能多的字符(贪心法描述:如果编译器的输入流截至某个字符之前都已经被分解为一个个符号,那么下一个符号将包括从该字符之后可能组成一个符号的最长字符串)

举个书中的例子:下面的语句原意思是想用x除以指针p解引用出来的数字,但是编译器却可能将/*理解为一段注释的开始!

 int a = 10;int b = 20;int* p = &b;int c = a/*p;

为了避免这种错误,我们可以自己用空格来将字符隔开或者添加括号,这也是一种很好的编程习惯,起码降低了造BUG的几率。

1.4整型常量:

在使用整型常量时要注意,如果一个数字前面是0,那么这个数字代表八进制的数字,同理0x代表16进制。0x还比较好认,我们要千万要注意数字0对整型常量的影响!!!

(这里插一个小知识,如果我们想输出八进制前的0,可以添加#)

int main()
{int a = 16;printf("%#o", a);//八进制020printf("%#x",a);//十六进制0x10return 0;
}

1.5字符与字符串: 

在C程序中,我们经常会看到下面的代码:

 char a = 'a';//字符char b[2] = "a";//字符串

那么这串代码里的'a'和"a"分别代表什么呢?实际上,用单引号引起的字符a它表示一个整数,他的二整数数值对于这个字符的ASCII码值,而双引号引起的就是一个指针,代表的是一个指向无名数组初始字符的指针。该数组被双引号之间的字符以及一个额外的二进制值为0的字符'\0'初始化。

我们可以用下面的代码来验证:

const char* p = "a";printf("%d", a);

这串代码在VS2019上能跑过去,并且输出97。

这里有一个有趣的事情,如果我们给一个字符变量输入的字符多于一个,那么最终存进去的是哪个呢?例如下面的示例,存进去的是a还是d?我们打印一下就知道了;

 char a = 'abcd';printf("%c", a);

我们也可以去看看内存:

我们可以看到是十六进制64呢,也就是十进制的100,我们可以通过查ASCII表,发现就是d,当然这只是在VS2019下测到的,这不代表所有编译器都是取最后一个字符的!!

第二章:语法陷阱

2.1函数声明:

函数声明,就是如果函数的实现在后面,我们需要提前写个函数声明,目的是告诉编译器我定义了这个函数,函数的具体实现就在后面。

函数的具体声明格式:类型+一组类似表达式的声明符号

void func1();//声明一个函数,返回值为空,形参为空
int func2(int q[]);//声明一个函数,返回值为整型,形参为数组
char* func3(int a, int b);//声明一个函数,返回值为字符指针类型,两个形参

提到函数声明,就有必要提一下函数指针,函数指针就是指向这个函数的一个指针,C语言在编译时会给每个函数一个入口地址,函数指针就是指向这个入口地址,我们可以利用函数指针找到这个函数。而函数指针的类型就是这个函数的对应的指针类型。注意,函数的类型也是可以用来强制类型转换的!

函数指针的作用:

1.调用函数;

2.做函数参数;

上面三个函数的函数指针是:

void (*pfunc1)()=&func1;//&符号有无皆可
int (*pfunc2)(int q[])=&func2;
char* (*pfunc3)(int a, int b)=&func3;(void(*)())pfunc2;//将pfunc2的函数指针类型进行强制转换

注意:void (*pfunc1)()与void *pfunc1()不同,前者是一个函数指针,后者是一个函数,返回值为void*,因为“()”在,所以 * 先与变量结合成为指针!

书上有个很有意思的声明:

(*((void*)())0())()

2.2 运算符优先级问题:

对于C语言中这么多运算符,如何准确规定它们的顺序就很重要了,例如下面这个例子是怎么执行的呢?

*p++

是先解引用再对解引用出来的数据进行加加还是先对地址进行加加再解引用呢?这类问题,都可以归于运算符的优先级的问题,下面是C语言的运算符优先级表:

运算符 结合性
()        []        ->    . 自左向右            
!  ~  ++  --   -   (type)   *   &  sizeof 自右向左
*      /     % 自左向右 
+      - 自左向右       

<<      >>

自左向右       
<     <=     >     >= 自左向右       
==         != 自左向右       
& 自左向右       
^ 自左向右       
| 自左向右       
&& 自左向右       
|| 自左向右       
? : 自右向左
assignments 自右向左
, 自左向右       

对于这些运算符,书上也总结了几条规律:

1.任何一个逻辑运算符优先级低于任何一个关系运算符;

2.移位运算符的优先级比算术运算符低,比关系运算符高

2.3 注意作为语句结束标记的分号:

我们要注意在C语言中注意有些时候不能多写一个分号或者少写一个分号!!

例如下面的语句就是while后多了一个分号,这样程序会将后面分号的空语句视为while的内容,这样会陷入无限死循环!!!

 while (a > 1);{b = 3;}

例如书中列举的少了分号的场景,这种场景下结构体后面少了个分号,而main又少了个返回值,这样系统会默认main返回值为这个结构体!!

struct Info
{int a;
}main()
{;
}

2.4 switch语句:

这节我感觉有一种思考方法很新颖,当我们调用switch里面某个case情况时,同时需要调两个case,这时候就可以利用这个性质,一次调用两个!

下面举个例子:

我们的意图是想在调用Mul函数同时也要调用Add函数,而调用Add函数是不调用Mul函数,所有我们选择Mul()后不加break,这样就可以顺势调用Add函数了。

 switch (Way){case 1:Mul();case 2:Add();break;default:break;}

2.5 函数调用:

一句话,函数调用一定要加参数列表!!!

2.6 else悬挂问题:

这个问题很常见了,引起这个问题的主要原因是,我们忘记了else会和最近的未匹配的if匹配

记得我之前写程序到单片机的时候,就写出过这样的代码:

int main()
{//...if (CONDITION1){//...}if (CONDITION2){//...}if (CONDITION3){//...}else{//...}//...return 0;
}

记得当时我的思路是想这三个if都不满足就走else,结果调试半天都不成功。后面时隔多日后,再次看到这个else悬挂问题,一言惊醒梦中人,立刻就意识到这个问题。这些问题看书时感觉好低级,自己应该也不会犯,但有时真的会无意间犯错也不知道!上面解决方法就是中间改为else if就行了。

第三章:语义的陷阱

3.1指针与数组:

C语言中的数组我们要记住两个特点:

1.C语言只有一维数组,我们看到的多维数组都是由一维数组变来的

2.对于一个数组,我们只能确定数组的大小和获得一个指向数组下标为0的指针,其它运算都是通过这个指针进行的

而对于多维数组,例如二维数组,我们也可以举个例子:

int arr[10][12];

这个数组可以看成是由多个一维数组组成:首先我们先创建一个具有十个元素的数组,并且里面每个元素都是一个拥有12个整型元素的数组指针,再具体创建每个具有12个整型元素的数组。

这里我们还要注意,数组名通常都是代表下标为0的元素的地址,只有两种例外:

1.对数组名进行取地址操作,那么此时取出来的是整个数组的地址

2.对数组名作为sizeof关键字的操作数

下面我们来看看数组的指针运算:

1.如果一个指针指向数组的某个元素,那么对这个指针进行增减操作就能使其指向相应的元素。

2.指针加上整数与指针转换为二进制表示后加上同样的整数意义不同!!

3.两个指向同一数组的指针相减得到的是相差指针类型的单元。

举个例子:

我们可以通过测试下面的代码,来判断指针相减的结果:

int main()
{int arr[3] = { 0,2,3 };int* p1 = &arr[2];int* p2 = &arr[0];printf("%d", p1 - p2);return 0;
}

结果显示输出是2,而我们也可以反过来通过&arr[0]+2得到arr[2]

然后我们看看指针和数组的关系:

首先我们先来看看下面的程序:


int main()
{int arr[5][5] = { 0 };arr[3][2] = 5;printf("第一次:%d\n", arr[3][2]);*(*(arr + 3) + 2) = 6;printf("第二次:%d", arr[3][2]);return 0;
}

运行这个程序输出结果第一次是5,第二次是6。第一次是5没什么好说的,就是通过下标给二维数组数据赋值,但第二次操作也能将位于arr[3][2]的值给改掉。所以我们可以得出以下结论:

arr[ i ][ j ]           =          * ( * ( arr + i )  +  j )      =        * ( arr [ i ]  +  j )

这里最后还要提及一个知识点:

数组的指针:数组的指针就是一个指向数组的指针,我们可以通过这个指针去访问这个数组,数组指针的类型就是这个数组的首元素对应的指针类型:

 int arr[10] = { 0 };int arr2[10][10] = { 0 };int *parr = arr;//一维数组指针int(*parr2)[10] = arr2;//二维数组指针

3.2 非数组的指针:

非数组的指针这部分的内容归结起来就一句话:字符串常量代表了一块包括字符串所有字符及一个空字符\0在内的所有内存区域。

我们可以通过下面的示例测试一下:

    char a[] = "abcde";printf("长度是:%d ",strlen(a));printf("内存空间是:%d ", sizeof(a));

结果输出一个5,一个6。这就证明了含有n个字符的字符串常量往往占用的是n+1个字符的内存。所以以后在分配内存时要千万注意这个多出来的\0!!

3.3 作为参数的数组声明:

这部分表达数组作为参数传递给函数时发生的情况:

在C语言中,我们无法将一个数组作为函数的参数传递,如果我们用函数名传递过去,那么编译器会将数组名转换为第一个元素的指针,即将数组声明转换为一个函数声明。当然,如果我们本意就是为了得到一个指向数组起始元素的指针,那么传递数组名也没有什么问题。

void func(int arr[10])
{;
}
//上下两个函数等价
void func(int* arr)
{;
}int main()
{int arr[10] = { 0 };func(arr);return 0;
}

3.4 避免“举隅法”:

这通俗来说就是:用整体代表了部分或者用部分代表了整体。在C语言中具体可以描述为混淆了指针和指针指向的内容。

3.5 空指针与空字符串:

这节提到了有关空指针的概念,在C语言中,我们可以将一些整数转换为一个指针,但这些程序实现结果取决于编译器。但是有一个通用的,就是0,编译器保证0不能转换得到有效指针,换句话说,空指针就是0。

这里有一个保证,空指针不能被解引用,我们可以运算,但不能访问。但我们经常可能无意间调用了某些函数,传递了空的指针,这些函数或者在内部的实现中会对传进来的指针进行解引用,所以在我们或许没有注意到的时候就产生BUG了。

所以我们要格外注意空指针的问题,或许我们也可以增加一个判断非空的条件,这样或许可以避免一些情况的出现。

3.6边界计算与不对称边界:

这节占了很多篇幅,从45到57页,我读第一遍也云里雾里,即使读完也感觉不能完全理解。先记录前几次阅读的理解,后面多读几次得到的启发再补写吧~

这主要介绍了数组下标从0开始取的不对称设计的一些好处和意义,特别提及了“栏杆问题”:100英尺长的围栏每隔10英尺要一个栏杆支撑,共需要多少个栏杆。答案是11个!我们特别需要注意到首和尾两个栏杆。这个问题提醒我们在数组范围计算要避免“栏杆错误”

为此作者给出两个原则:

1.先计算最简单情况下的特例,然后往外推

2.只能自己仔细计算啦

例如计算x>=17,x<=39区间有几个元素:

第一种方法:计算如果x>=17,x<=19情况下,扳扳手指头就能算出是3个,即19-17+1。同理,就能算出为39-17+1=23个。

第二种方法:自己仔细,认真,小心去算:也能得到23

在C语言设计时,将数组的下标的取值设为从0开始,即下界为0,而上界设计不包括于数组取值范围中。这样数组的下标取值就是0=<x,x<上界。这种边界不对称的设计能带来很多好处:

1.数组的上界就是数组的元素个数。

2.如果没有元素,那么上界等于下界,元素就为0。

3.即使取值范围为空,上界也永远不可能小于下界

这里提到了数组边界,就不得不提到位于数组的下一个地址,它不属于数组,但原则上这个地址可以用来赋值和比较,但不能对这个地址进行访问!

3.7 求值顺序:

求值顺序与我们的运算符优先级里的求值顺序是不同的,这里的求值顺序指的是在一个含有多个运算符的表达式中,如果其中某个项的真假对整个表达式有影响,那么可能影响后续的表达式是否继续执行。

举个例子:

 if(CONDITION1 && CONDITION2 && CONDITION3)if(CONDITION1 || CONDITION2 || CONDITION3)

我们先来看第一个语句,在这个语句中一般先对CONDITION1进行判断,如果它为假,那么后面的判断语句不用执行了,整个判断都是假;如果它为真,那么继续判断下一个CONTIDION2,以此类推。

对于第二个语句同理,如果条件为真,则后面的判断都可以不用执行,因为整个语句已经为真了;但是如果为假,那么仍需要继续执行下一个CONDITION2。

而C语言中一般只有四个运算符存在求值顺序(&&  、||?:  、,  对于其他任何运算符对其操作数的求值顺序都是未定义的!

这几个运算符的求值顺序是:

&&和||:这两个运算符首先对左侧操作数求值,只有在需要时才对右操作数求值。

a?b:c  :这个运算符符首先对a求值,根据a来选择走b还是c。

,:逗号运算符则取得是右边的数据。例如下面代码输出d为30

int main()
{int a = 10, b = 20,c=30;int d;d = (a, b,c);printf("%d", d);return 0;
}

3.8 运算符&&、||和!:

这节主要跟我们介绍了这些运算符使用时要仔细,因为有时候即使它们用错了,程序也能照常运行。这时候就会增加我们排查问题的时间!例如下面两行代码都每语法错误,但是它们的意义却大不相同!

 if (a & b)if (a && b)

3.10 main函数的返回值:

在了解这节之前,首先我们先要有一个预备知识:如果一个函数不声明返回类型,那么这个函数默认返回整型。如果函数没有给出任何返回值,实际上会隐式返回一个无用的整数

那么下面我们来看看这个C程序:

main()
{}

很遗憾,这个程序在VS2019上跑不过去,不支持默认返回int,即没有写返回值。其实如果不写return 0 在一般情况下问题也不大。但是万一遇到一些操作系统是以main函数的返回值来判断main函数的执行状态---0为成功,非0为失败,那这个程序就是错误的。所以一般我们为了保险起见还是写return 0吧!

第四章:链接

之前学习C语言就接触过一个程序编译的几个阶段,包括预编译,编译,汇编到后面的链接。链接器对于将一个个C的编译部分连接成一个整体尤为重要!这里因为链接是独立于C语言实现的,并且如果出现的问题是需要结合多个源文件才能查出来(编译器只能一次处理一个文件),那么如果出现与C语言相关的问题链接器也没有办法。(这里作者特别强调使用lint程序,但我还没学习过这个程序,所以也不展开说了)

4.1 链接器:

C语言中有个重要思想就是分别编译。而链接器就是在恰当时候将这些程序整合到一起。注意!链接器一般与编译器分离的,下面来说说链接器的工作过程:

首先链接器将汇编阶段后产生的目标模块整合到一个被称为载入模块可执行文件(exe)的实体其中目标模块是直接作为输入给连接器的,另一些则需要编译器去库文件种读取,例如printf文件就去对于库文件中获取相关模块。

在工作中,连接器会把目标模块都看作是由外部对象组成的,这些外部对象都有一个独特的外部名称,并且对于内存的某个部分,所以C程序中每个变量和函数都要有一个独特的外部名称不能重复,不然就产生命名冲突,一些链接器会干脆完全禁止掉!除非用static关键字修饰,这个后面再说。

在C程序中,我们通常会在一个C文件中引用另一个源文件中的某些函数,这时候链接器生成载入模块时会记录这些外部的对象的引用,做出判断这个对象是在另一个目标模块/定义的,这样我们就可以引用外部对象了!

最后我们总结一下链接器的工作原理:

4.2 声明、定义:

我们看看下面三个声明语句:

int a;
int a = 10;
extern int a;

注意,这些语句位置出现在所以函数体外,才叫做外部对象的定义!

第一个语句说明a是个外部对象,并且没有初始值(有些系统中会默认为0)

第二个语句说明a是个外部对象,并且有初始值10

第三个语句说明a这个变量是个外部变量,并且它的内存在别的程序中分配的。对连接器来说,这是一个对外部变量的引用,所以即使它是出现在一个函数内部,也不会有问题。

如果一个程序对一个外部变量的定义不止一次,例如对外部变量a进行多次定义,并且每次都赋予不同的初始值,那么大多数系统会拒绝接受这个程序。

4.3 命名冲突和static修饰符:

两个相同名称的外部对象实际上是同一个对象,即如果两个源文件中包含相同的定义,那么要么是程序错误,要么是共享同一个a。所以如果我们写的函数或者变量名称与标准库中的函数、变量名称冲突,那么可能库函数也会调用我们写出来的函数或者变量!

为了解决这类问题,引入了static修饰符

通过static修饰符修饰的变量,它的作用域会限制在一个源文件内,对于其他源文件它是不可兼得。因此,如果很多个源文件需要共享外部变量,那么可以将这些函数放置在同一个源文件中,并且引入static关键字修饰的外部变量!

利用static修饰符的特点,我们可以在多个源文件中定义同名的函数,只需要在其中一个函数前加上static即可。

4.4 形参、实参与返回值:

对于一个函数来说,传递给它的就是实参,它参数列表用来接收的就是形参,函数返回的就是返回值。下面来看看这些参数对函数的影响和特点:

返回值的一些特点:

1.任何一个函数都有返回类型,它要么是空void,要么是某些特定的数据类型(如int、char、int*、char*)。

2.函数的最好在它第一次调用前进行声明或定义,只要我们声明了,定义也可以放在调用后面写。

3.如果一个函数被声明或定义前被调用,那么它的返回类型默认为整型。

形参与实参的匹配:

1.函数声明最好写上形参类型,那么就不会有后面的一些问题。

2.如果函数没有short、char、float类型的参数,那么函数声明可以不加上形参类型说明:

int func();

这就要依赖于实参能传递数目与类型都正确的参数。但这也不是保证传递等同的参数,因为float类型会自动变成double、short或char类型会自动变成int类型。

3.特别注意printf和scanf函数在不同情况下可能接受不同类型的参数!!

4.5 检查外部类型:

如果我们声明的外部变量与这个外部变量定义的数据类型不同,那么往往会引发错误。而对于这种错误,不同的编译器会有不同的处理。这里要特别注意的是字符指针与字符数组,这两个是不能等同的,所以我们不能定义使用字符数组,而外部声明使用字符指针,它们的内存形式是不同的!

char str[];
extern char* str;

4.6 头文件:

这里作者推荐我们为了避免上面所说的一大堆声明问题,我们可以利用一个头文件来容纳我们所有的函数声明。而且只要声明是一致的,那么多次的声明也不会带来什么问题。

第五章:库函数

这章作者列了几个常用的库函数,并且提供它们的一些注意事项,提醒我们可以的话尽量去使用系统的头文件。

5.1 getchar函数:

这个函数需要注意的是,它一般会返回标准输入文件的下一个字符,如果没有则返回EOF(数值为-1)。

5.2 fseek函数:

这个函数用于更新文件的顺序,那它有什么用呢?我们在对文件进行读和写操作时,要特别注意一个输入操作后面不能紧跟着一个输出操作,相反也是,除非两者中间加入一个更新文件的函数fseek。

5.3 setbuf函数:

首先我们考虑一种情景,如果我们不着急着输出,而要将要输出的内容储存起来,等到它达到某个值或者程序员调用fflush函数(清空缓冲区)再一次性输出,那么我们要怎么做呢?这里引入一个缓冲区函数setbuf。它有两个参数stdout和buf,前者是输出流,后者是用于临时存储数据的字符数组,它的大小由系统头文件<stdio.h>中的BUFSIZ定义。

这里要注意一种使用情况:这是书中举的错误的例子

int main()
{int c;char buf[BUFSIZE];setbuf(stdout, buf);while ((c = getchar()) != EOF){putchar(c);}return 0;
}

因为最后一次清空缓冲区是在main函数结束之后,这时候buf数组已经被释放了!所以会带来一些意想不到的问题,为此我们有两种办法解决:

1.是将buf数组声明为静态数组;

2.在堆区开辟空间存储buf数组;

5.4 errno变量:

这个外部变量作用体现在有错误时,没有错误时它为0,在发生不同的错误时它对应着不同的数值,我们可以在通过strerror(errno)函数来输出错误信息。这里要注意的是errno的数值问题:

1.库函数调用没有失败,errno不一定为0。

2.库函数调用成功,errno不一定清零,也不会禁止errno设置。

所以,一般我们利用errno是先检测它的返回值,确定程序失败了,才检查输出原因!

第六章:预处理器

预处理器这章重点介绍了#define的作用,以及一些它与函数的区别!

6.1  宏定义的空格不能掉:

宏定义中,我们不能忽略它中间的空格,如下面的两个宏是不同的!

#define F (x)(x-1)
#define F(x)  (x-1)

不同的是,我们在宏的调用时,空格就显得不那么重要了。比如下面两个宏调用都是同一个意义。

F(x)
F  (x)

6.2 宏不是函数:

宏与函数不同,宏是在预编译阶段就对所有定义进行替换,而且宏有几个特点:

1.宏不会进行运算,所以我们尽量对宏里定义的每个表达式、变量括起来;

我们可以用下面的案例测试一下,结果输出是24,那就表明,宏的替换是直接替换的!

#define Add  x+xint main()
{int x = 8;printf("%d ", 2 * Add);return 0;
}

所以上面的程序等价于:

int main()
{int x = 8;printf("%d ", 2 * x+x);return 0;
}

那这个程序结果自然就是24了。所以我们以后在对宏的使用时,最好对每个参数都添加括号,并且对整个表达式外面都加上括号!

2.注意宏的副作用;

宏的副作用多体现于++的问题上,要么是宏内无意间进行的多次加加,要么就是传递进来参数无意间的多次加加;

#define M ((arr[i++])>5?(arr[i++]):arr[i])int main()
{int i = 0;int arr[5] = { 9,2,8,5,1 };i = 0;M;printf("%d ", i);return 0;
}

让我们看看上面的程序,首先i是0,那么宏替换后得到的arr[0]与5比较,得到9比5大,此时i++得到i是1,而因为又经历了一次arr[i++],所以我们最后的i会变成2!即我们无意间对i进行了两次++!所以在宏里面要进行++或者--运算的要格外小心再小心!同理,传递的参数要是有此类运算也要斟酌再三!

6.3 宏不是语句:

这里主要就是要注意宏定义后面的分号问题,宏定义后面有无分号要根据具体实现场景而定!

6.4 宏不是类型定义:

类似于typedef,我们可以通过宏来对一些类型名进行重新定义

#define CHAR char
int main()
{typedef int INT;INT a = 10;CHAR ap = 'a';return 0;
}

但最好还是用typedef,这样可以防止很多问题的出现!例如下面的代码中类型定义的一些变量会被解析成不同的数据类型:这里a是指针,b是字符变量。

#define CHAR char*
int main()
{CHAR a, b;return 0;
}//等价于#define CHAR char*
int main()
{char *a, b;return 0;
}

第七章:可移植性缺陷

7.2 标识符的名称限制:

我们要写出标识符能够不引起重复,所以这部分有一个注意点:C语言实现必须能够区别前6个字符不同的外部名称,这个定义并无区分字母大小写。

7.3 整数的大小:

这部分是关于C语言中几个数据类型的范围,具体我们可以到头文件#include<limits.h>中查看。

7.4 字符是有符号还是无符号问题:

这部分原书中有写一段注意事项:如果c是个字符变量,使用(unsigned)c就可以得到与c等价的无符号整数。但这是会失败的,因为在将c转换为无符号整数时,c首先将发生整型提升变成int型的整数。所以我们最好还是用(unsigned char)来进行转换!

7.5 移位运算符的一些注意事项:

这里主要注意的是移位运算符的补位和范围问题:

1.右移位时,多出来的空位将会被0填充,如果移动的是有符号数,那么补充符号位,否则补充0。

2.如果被移位的变量长度为n个位,那么被移动的位数只能大于0而严格小于n。

这里有一个技巧:

就是对于除以2的求法:我们可以利用移位进行操作,这样可以让执行速度提高!

 int low = 10, high = 20;int averge = (low + high) / 2;int averge = (low + high) >> 1;

7.6 内存位置0:

这里主要说明不能使用null指针进行访问,null指向在内存为0的位置,这里可能储存着操作系统的一些内容,如果我们访问修改,那麻烦就大了!

END


结尾的一些废话:《C陷阱和缺陷》是我第一本读的C语言的书,读之前也学过一下C语言,也算了解过C语言了,哈哈!书不算厚,但是里面的内容的确让我学到很多。并且这是我第一次阅读,对于一些知识点和细节并不能完全理解和感悟,这可能要等我学习到更多知识后,再回头来细细品味这本书才会有更深的感悟吧!为了记录自己第一次读书收获的知识,我决定写个读书记录(为了记下来免得以后忘记,回头再来看看),没想到竟然写了1万多字!

2022年5月11日

【知识点总结】-《C陷阱与缺陷》相关推荐

  1. 动图图解!既然IP层会分片,为什么TCP层也还要分段?

    文章持续更新,可以微信搜一搜「golang小白成长记」第一时间阅读,回复[教程]获golang免费视频教程.本文已经收录在GitHub https://github.com/xiaobaiTech/g ...

  2. TCP粘包:我只是犯了每个数据包都会犯的错 |硬核图解

    事情从一个健身教练说起吧. 李东,自称亚健康终结者,尝试使用互联网+的模式拓展自己的业务.在某款新开发的聊天软件琛琛上发布广告. 键盘说来就来.疯狂发送"李东",回车发送!,&qu ...

  3. 爷青回!最近很火的朋友圈怀旧小电视源码来啦!看到最后一个视频我大呼好家伙!

    原文首发于公众号:[golang小白成长记] 爷青回!最近很火的朋友圈怀旧小电视源码来啦!看到最后一个视频我大呼好家伙! 体验一把怀旧小电视 最近朋友圈被怀旧小电视刷爆啦! 点开来,是一台老式电视机! ...

  4. 在linux下安装windows系统--仅仅支持efi主板+gtp+U盘安装

    本人已经安装成功: 材料: 1. U盘 2.电脑,bios支持efi shell 3.win8-x86-64位安装镜像cn_windows_8_1_x64_dvd_2707237.iso 4.EFI_ ...

  5. TCP粘包为什么会粘包? 背后的原因让人暖心

    事情从一个健身教练说起吧. 李东,自称亚健康终结者,尝试使用互联网+的模式拓展自己的业务.在某款新开发的聊天软件琛琛上发布广告. 键盘说来就来.疯狂发送"李东",回车发送!,&qu ...

  6. 活久见!TCP两次挥手,你见过吗?那四次握手呢?

    文章持续更新,可以微信搜一搜「小白debug」第一时间阅读,回复[教程]获golang免费视频教程.本文已经收录在GitHub https://github.com/xiaobaiTech/golan ...

  7. 【软件工程】McCabe方法,输入三角形三边,判断三角形性状,画出流程图和环图,计算环形复杂度,要求有判断是否能构成三角形的条件。

    话不多说,直接上图解,知识点在最后. 流程图 环图 环形复杂度 整合 知识点 环型复杂度的三种计算方法 V(G) = 流图中的区域数 V(G) = 流图中的判定数 + 1 V(G) = E - N + ...

  8. 【机器学习基础】8个知识点,图解K-Means算法

    来源:Python数据之道 作者:Peter 整理:Lemon 8个知识点,图解K-Means算法 之前,公众号分享了关于 KNN算法 的介绍,今天,我们来学习下另一个经典的算法:K-means算法. ...

  9. 30 张图解 | 高频面试知识点总结:面试官问我高并发服务模型哪家强?

       面试中经常会被问到高性能服务模型选择对比,以及如何提高服务性能和处理能力,这其中涉及操作系统软件和计算机硬件知识,其实都是在考察候选人的基础知识掌握程度,但如果没准备的话容易一头雾水,这次带大家 ...

  10. 万字整理,图解Linux内存管理所有知识点

    Linux的内存管理可谓是学好Linux的必经之路,也是Linux的关键知识点,有人说打通了内存管理的知识,也就打通了Linux的任督二脉,这一点不夸张.有人问网上有很多Linux内存管理的内容,为什 ...

最新文章

  1. 线上分享 | 浅谈中台对产品经理的价值
  2. android助手专业版,开发助手专业版 v5.6.1-cs for Android 直装付费专业版
  3. FileItem API详解及演示
  4. 学习android操作系统,学习Android手机操作系统笔记总结
  5. kafka 思维导图
  6. Adobe CTO:Android将超预期获50%份额
  7. 【CVPR2021】论文汇总列表--Part2
  8. Java延时队列DelayedQueue
  9. redmine cannot load such file – rbpdf-font
  10. linux系统移植和根文件系统制作
  11. Java复习第二弹!
  12. 3D美术14——max——fbx导入max后直接生成bip人形骨骼插件
  13. matlab人口数据,matlab中国人口
  14. mfc treectrl设置背景透明_微信透明头像怎么弄 专用透明头像图片更换设置教程闽南网...
  15. silabs green power
  16. 机械键盘 单个按键不灵 修理
  17. 校招进腾讯,二本没戏了?
  18. 计算机科学与技术在生物方面的应用,浅谈信息技术在生物工程中的应用意义
  19. ESMM全空间多任务模型解读与试验
  20. 直方图均衡化算法原理及bins的理解

热门文章

  1. python构建智能机器人系列博文---借助于python实现QQ,微信消息的自动发送,音乐的自动播放
  2. 【STC单片机学习】第十四课:SPI通信-实时时钟DS1302
  3. python读取txt文件特定内容,并绘制折线图
  4. 将excel中的数据导入数据库
  5. 如何安全地嵌入第三方js – FBML/caja/sandbox/ADsafe简介
  6. UCINET入门案例
  7. js遍历json的key和value
  8. 面试中有关UI自动化的那些事 ~
  9. 01|读研这三年,你亏么?(研一篇)
  10. 预览版win11系统下载方法详解