C陷阱与缺陷读书心得

​ 作为C语言“三剑客”之一的《C陷阱与缺陷》被无数人奉为经典,以下是我第一次品读该经典的读书笔记,如有错误或者写得不好的地方可以告诉我哦,谢谢大家指教。

img

一.词法陷阱

1.1 ==与=的不同

=为赋值运算符,而 == 为比较运算符。 因此在 == 比较运算符书写的时候可以写成常量 == 变量(5==a),因为如果不小心用的赋值运算符会报错(5=a)不能将变量的值赋给常量。==不能用来比较字符串。

1.2 &和|不同于&&和||

&和|分别表示按位与以及按位或属于按位操作符,而&&和||表示逻辑与以及逻辑或属于逻辑运算符

​ &&逻辑与是要求左右两侧操作数结果均为真,||要求左右两侧操作数有一个为真就行。这个关乎到求值顺序,后面3.7会讲。

1.3 词法分析中的"贪心法"

C语言对每一个符号从左往右一个一个的读入,如果该字符能组成一个符号,那么就读入下一个字符,判断已经组成部分与下一个字符能否组合,就是能否每一个符号尽可能多的包含字符。

​ 例如如果想表达将x除以指针p内的值不能写成y=x/*p 应该写成 y=x/ (*p)第一种写法中,C语言规则会认为/*为一个注释符号。 还比如书中习题“为什么n–>0的含义是n-- >0,而不是n- ->0?” 答:在编译器读入>之前,编译器就尽可能的把前两个符号合并为一个符号,也就是自减符号,所以n–>0的含义也就是n自减小于0。但也不是所有都如此,又比如a+++++b的含义并不是a++ ++ +b即((a++)++)+b,因为a++并不能作为左值,所以其唯一有意义的解析只能是(a++) + (++b)。

1.4 整型常量

​ 我们在定义一个整型常量的时候尽量不要以0开头,如果整型常量的第一个数字为0,那么该常量会被视为八进制的数,也就是010与10所表示的含义不一样。

1.5 字符与字符串

​ C语言中我们常用单引号和双引号来分别初始化字符以及字符串,由单引号引起的字符实际上代表的是一个整数,比如在ASCII中‘a’对应的则是97(十进制)。用双引号引起的字符串实际上代表的是指向无名数组起始字符的指针,该数组被双引号内的字符以及“\0"初始化,也就是说字符串”hello“实际上是char hello[]={‘h’,‘e’,‘l’,‘l’,‘o’,0}。简单说就是字符代表的是整数,而字符串代表的是指针,如果混用则会报错,例如 char*p=‘/’。特别是不可以用printf('\n')来代替printf("\n")

二.语法陷阱

2.1 理解函数声明

任何C变量的声明都由两部分组成:类型以及一组类似表达式的声明符

(*(void (*)())0)()这有一段有趣的代码,什么意思呢?

void (*)()这是一个函数指针,(void (*)())0)这个是将0强制转换成函数指针类型,(*(void (*)())0)()这个就是把强制转换成函数指针的0进行解引用也即是调用函数。一句话就是,调用0地址上的函数,该函数没有参数,返回类型为void。

2.2 运算符优先级问题

在这里插入图片描述

​ 总体来说就是:数组下标引用符,函数调用操作符,结构成员选择符 >单目操作符>双目操作符。

​ 双目操作符中:算术运算符>移位运算符>逻辑运算符>赋值运算符>三目运算符。

​ 注意,同一类型的运算符之间,关系运算符比较特殊,==和!=优先级要比其他关系运算符低,也就是说我们比较可以用a<b == c<d来判断a相对b的大小与c相对d的大小是否相同。在实际写代码的时候,我们时常写出 if(a=5!=b)这样的代码,我们希望把5赋给变量a再与变量b比较是否相等,但实际上该代码的意思是“5是否不等于变量b的结果赋给变量a”。

2.3 注意语句结束标志分号

​ C语言中,分号是一个语句结束的标志,特别是结构体定义中末尾的分号容易遗忘。如果多写了一个分号,例如if(x>y);big=x;则相当于 if(x>y){};big=x;

2.4 switch语句

​ C语言中的switch语句的控制流程能够依次执行各个case部分,所以我们需要在每个case部分加上break语句。当然在某些特定情况下可以有意的去省去break语句。

2.5 函数调用

​ 我们在C语言中进行函数调用时,应该带上函数参数即使调用的函数没有参数,即函数名+函数参数。如果fun为一个函数,那fun(); 是一个调用语句,而fun则是一个什么也不做的语句。

2.6 “悬挂”else引发的问题

C语言中的else始终与同一对括号内最近的未匹配的if结合。

例如:

if (x==0)
     if (y==0) error();
else{
    z=x+y;
    f(&z);
}

​ 上述代码想要实现的是:x等于0以及x不等于0的两种情况,如果x等于0除非y也等于0(此时调用error函数),否则程序不执行任何处理;对于x不等于0的情形就将x与y求和后赋给z,然后用z的地址作为参数调用f函数。

但实际上:经过缩进后代码

if (x==0){
    if (y==0) error();
	else{
    	z=x+y;
    	f(&z);
	}
}

​ 与实际想实现的意图相差甚远,实际代码根本就没有x不等于0的处理。

​ 我们应该改成

if (x==0){
     if (y==0) error();
}
else{
    z=x+y;
    f(&z);
}
    

​ 小编认为这是一个很容易犯错并且容易出bug的点,所以我建议大家在写if语句的时候,尽量不要减少{}的书写,每一个if都配一个else,即使else不用执行任何语句。

三.语义“缺陷”

3.1 指针与数组

​ 书中提到C语言中只有一维数组,数组元素可以是任意一个类型的对象,也可以是另外一个数组(这有利于我们对二维数组的理解,例如int arr[3][4]也就是理解为arr[3]为一个一维数组,而每一个元素则是另一个一维数组,也就是arr[3]的每一个元素arr[0],arr[1],arr[2]都是数组,然后arr[0]数组有四个元素:arr[0][0],arr[0][1],arr[0][2],arr[0][3])。对于一个数组我们只能确定其的大小(我们在定义数组的时候也需要给定数组的大小,不可以是一个变量,但是在C99标准下允许变长数组),以及获得指向该数组第一个元素的指针,其余的所有操作都是通过指针进行,所以数组的运算与指针运算是可以互通的,例如上述数组int arr[3][4]我们访问在二行四列的元素可以用arr[1][3]也可以用*(arr[1]+4) 还可以用*(*(arr+2)+4)

​ 由以上例子,我们可能存在误解,因为指针通俗的说就是地址,那我们是否可以定义int *p=arr?答案是,不可以,arr是二维数组,也就是数组的数组,而p仅仅是一个一级指针,如果我们在访问数组arr内的各个元素的时候就会出错,因为简单说arr内有两重地址,而一级指针只能指向一重地址,所以我们不能正确访问出arr数组的每一个元素,正确的写法应该是使用数组指针,int (*p)[4]=arr p是一个指针指向一个数组,数组有四个元素。arr赋值给p也即是把第一行的地址给p。

假设有一个数组int arr [3],那么只有在计算arr数组大小的时候也就是sizeof(arr)时,还有&arr数组名表示整个数组,其他情况都是代表着数组首元素的地址。

​ 指针相减能够得到两个地址之间的内存空间,例如:int *q =p+i,那么我们通过q-p就能得到i的值,但是这两个指针必须指向同一个数组中的。但是,指针的加法是非法的。

3.2 非数组的指针

​ C语言中,字符串常量代表了一块包括字符串中所有字符以及空字符’\0’的内存地址

​ C语言为我们提供了一个库函数malloc,该函数可以实现接受一个整数,然后分配能够容纳同样数目的字符的一块内存。(我们在调用malloc函数申请了内存空间,使用完该空间**一定要记得释放!!!**同时malloc函数有可能无法提供申请的内存,这时malloc函数会返回一个空指针作为信号,所以我们在使用前,应当判断一下是否为空指针。)

​ 同时C语言也为我们提供了一个库函数strlen,该函数返回一个字符串中所有的字符数(但是!!!并未计算’\0’,因此假设字符串S用strlen(S)的值为n,那么实际需要n+1个字符的空间)。

3.3 作为参数的数组声明

​ **数组在传参的时候通过数组名把数组首元素地址传递,**但是值得注意的是数组名是转换成指向第一个元素的指针,也就是作为参数的数组声明转化为相应的指针声明,我们在函数传参的时候可以使用指针接受数组。例如:int strlen(char s[]){/*具体内容*/}int strlen(char *s){/*具体内容*/}两者是等价的。

3.4 避免"举隅法"

​ 这里的举隅法是C语言中的:混淆指针和指针所指向数据。这是一个常见的错误。例如下面这个字符串的情形:

char *p,*q;
p="xyz";
q=p;

​ 我们可能认为p的值就是字符串"xyz",但是实际情况是p的值是指向’x’,‘y’,‘z’,'\0’四个字符组成的起始元素的指针,是地址。而q=p是把字符串的地址也给q,所以q也指向同一个地址,是两个指针同时指向同一个地址,并没有把字符串内的字符复制给q,也就是复制指针并不同时复制指针所指向的数据。所以我们通过q[1]='Y'就可以把原字符串改为”xYz“。 (但是这种修改字符串常量的行为在ANSI标准下是不允许的。)

3.5 空指针并非空字符串

​ 在C语言中将一个整数转换成指针,所得到的结果都是由编译器决定。但是有一个例外,那就是我们的0,我们定义:# define NULL 0当常数0被转换成指针使用时,那么这个指针绝对不能解引用来使用,也就是这个指针是一个空指针,什么都没有也不能指向别的地方,不想空字符串是一个空格或者其他的字符,空字符串里面是有东西的。

3.6 边界计算与不对称边界

​ C语言中下标是从0开始的,也就是说一个大小为10的数组,数组下标从0到9,也就是数组大小为n,而下标范围是0~n-1。最重要就是要记住下标计数从0开始

​ 我们来看这样一段代码

int i,a[10];
for	(i=1,i<=10,i++)
    a[i]=0;

​ 我们如果不注意下标10,我们可能认为这个代码就没问题,如果我们注意到i<=10那么就觉得这个代码会报错,执行不了,但这都是错的,实际上这段代码可能会陷入一个死循环。我们知道每一个变量都对应这一块内存空间来存放,而局部变量都存放在同一片区域。如果编译器按照内存地址递减的方式给变量分配内存,那么数组a后的内存就被分配给了i,所以当数组a全部设置为0后,访问arr[10]是不存在的,实际上就是数组a的下一块内存也就是i的内存,把i的值改为了0,导致了死循环。

​ 画了个简图,有点丑,哈哈。

在这里插入图片描述

​ 在C语言计数中,我们可能经常犯一个错误,那就是,“栏杆错误“,就像我上面画的图,一共12个格子,不是12条横线而是13条横线。又例如x>=16且x<=37中一共有几个数,我们可能会以为是37-16=21个,实际上是22个,因为范围的两边上界和下界都包含在内了,这种错误经常犯,尤其是在数组当中。实际上,我们推荐的是不包含上界包含下界的写法,也就是数学上的左闭右开的半开半闭区间像[0,10),也就是x>=16且x<38的不对称写法,因为直接算38-16=22就是个数。对应数组中的遍历

int arr[10],i;
for(i=0;i<10;i++)//不推荐写成(i=0;i<=9;i++)
    	arr[i]=0;

3.7 求值顺序

​ 求值顺序不等同于运算符优先级。

​ 我们来看下面一段代码a<b&&c<d,C语言规定我们先求a<b,如果a确实小于b,此时进行c<d求值,以确定表达式的结果。但是如果a大于或者等于b的话,那么就不再需要进行c<d的求值,表达式肯定为假。

​ C语言中只有四个运算符(&&, ||, ?: 和 ,)存在规定的求值顺序。&&和||首先对左侧操作数求值,只有当需要时(也就是&&左侧为真,||左侧为假)才对右侧操作数求值,否则(也就是&&左侧为假,||左侧为真)右边就不执行。三目操作符(?:):a?b:c中,操作数a首先被求值,然后根据a的值再去求操作数b或者c的值。例如:a>b?a=5:a=1这段代码的意思就是,先判断a是否大于b,根据a是否大于b(a>b)的结果,如果是就把5赋给a,如果不是就把1赋给a。最后就是逗号运算符(,)首先对左侧操作数求值,然后该值被”丢弃“,继续对右侧操作数进行求值。值得注意的是函数参数内的逗号并非逗号运算符。

​ 其他的运算符对其操作数求值的书顺序都是未定义的,特别地,赋值运算符并不保证任何求值顺序。

3.8 运算符&&,||和!

​ 逻辑运算符&&,||,!对操作数的处理方式是将其视为要么是”真“,要么是“假”。约定0视作“假”,非0视作“真”。结果为”真“返回1,结果为“假”返回0。当左边操作数的值能决定最终结果后,不会对右侧操作数求值。

3.9 整数溢出

​ C语言中存在两种整数算术运算,有符号运算与无符号运算。在无符号算术运算中,没有所谓“溢出”一说。如果算术运算符中一个操作数是有符号整数,另一个是无符号整数,那么有符号整数会被转换成无符号整数。

​ 只有当两个操作数都为有符号整数时,才有可能发生未定义结果的”溢出“,而且做出的任何关于溢出结果的假设都是不安全的。

3.10 为函数main提供返回值

​ main函数以及其他函数一样,如果没有显式声明返回类型,那么函数返回类型就默认为整型。一个返回值为整型的函数如果返回失败,实际上是隐含地返回了某个“垃圾”整数。大多数C语言实现都通过函数main的返回值来告知操作系统该函数的执行是成功还是失败,经典的处理方案就是,返回0表示程序执行成功,返回非0表示失败

四.连接

4.1 什么是连接器

4.2 声明与定义

int a如果定义在所有函数体外,则称为对外部对象a的定义,分配储存空间,初始默认值为0

int a=7定义a的同时指定了初始值为7。

extern int a并不是对a进行定义,而是对a进行声明,说明a是一个外部整型变量a是一个外部整型变量,extern说明a的存储空间是在程序的其他地方分配的,只是一个外部对象的显式引用。如果一个程序包含了 extern int a ,那么这个程序就必须在别的某个地方包括语句: int a; 那么这两个语句既可以在同一个源文件中,也可以位于程序的不同源文件之中。注意:每个外部变量只能够定义一次。

4.3 命名冲突与static修饰符

​ 我们上述提到,每个外部变量只能定义一次,两个具有相同名称的外部对象实际上代表同一个对象。

static修饰符可以用于减少命名冲突。例如static int a;其含义与int a;相同,但是a的作用域被限定在一个源文件内,在其他源文件a是不可见的也就是与其他源文件内的a不冲突。static修饰符不仅可以修饰变量也可以修饰函数。如果一个函数或者一个变量仅仅被同一个源文件内的被调用或者使用,那么我们应该声明为static。

4.4 形参,实参与返回值

任何C函数都有形参列表,每一个形参列表都是一个变量,变量在调用的过程中被初始化,也有形参为空的函数。(敲黑板!!!记住,调用函数的过程中要传实参,即使没有任何参数也需要把括号带上。)

每个函数在被调用前,一定要被定义或者声明(在main函数前,函数是不能嵌套定义的。),如果一个函数在定义或者声明之前就被调用,那么它的返回值类型就默认为整型

​ 如果我们调用函数时使用的函数参数类型与定义时不一致,那么我们需要在函数声明的时候显示声明参数类型,否则float类型参数会自动转换成double类型,short或者char类型会转换成int类型。

4.5 检查外部类型

保证一个特定名称的所有外部定义在每个目标模块中都有相同的类型,也就是一个名称对应只有一种类型,必须是严格意义的相同,就算是数组和指针之间非常类似,但也是不同的。

​ 忽略了声明函数的返回类型,或者声明了错误的返回类型,也会带来麻烦。

4.6 头文件

​ 我们可以将所需要外部对象(一般是函数)都放在一个地方声明,也就是在头文件中。头文件包括需要用到该对象的所有模块以及定义该外部对象的所有模块。

五.库函数

C语言中没有定义任何输入/输出语句,基本的输入输出操作时通过调用库函数实现的。

5.1 返回整数的getchar函数

getchar函数在一般情况下返回的是标准输入文件的下一个字符,当没有输入时返回EOF(一个在头文件stdio.h中被定义的值,不同于任何一个字符)。

# include <stdio.h>
int main()
{
    char c;
    while ((c=getchar())!=EOF)
        putchar (c);
	return 0;
}

​ 这个程序乍一看好像是把标准输入复制到标准输出,实则不然。我们前面提到,字符在内存中存放的实际上是ASCII码,是数,而我们变量c定义为char(一个字节)类型不是int(通常为四个字节)类型并不能完全容下所有可能的字符,特别地,可能无法容下EOF。所以上述代码要么无法取到EOF的值,陷入死循环,要么就是某些字符输入后被“截断”使得c的取值与EOF相同然后停止。(当然了也会有因为完完全全的巧合导致似乎在“正常运行”的情况)

5.2 更新顺序文件

​ 许多系统中的标准输入/输出库都允许程序打开一个文件,同时进行写入和读出的操作,编程者也许认为,可以自由地交错进行读出和写入操作。遗憾的是,事实总是难随人愿,为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作不能随后直接紧跟着一个输出操作,反之亦然。如果要同时进行输入和输出操作,必须在其中插入 fseek 函数的调用(只适用于windows平台)

5.3 缓冲输出与内存分配

​ 程序输出有两种方式:一种是即时处理方式,另一种则是先暂存起来,然后再大块写入的方式。C语言实现通常都允许程序员进行实际的写操作之前控制产生的输入输出数据量。这种控制能力通过库函数setbuf实现。

setbuf(stdout, buf);

​ buf是一个大小适当的字符数组,以上语句,通知输入/输出库,所有写入到stdout的输出都应该使用buf 作为输出缓冲区, 直到buf 缓冲区被填满或者程序员直接调用fflush(调用fflush 将导致输出缓冲区的 内容被实际写入该文件 ),buf缓冲区中的内容才实际上写入stdout,缓冲区大小由系统头文件<stdio.h>中的BUFSIZ定义。

​ 书中提到一个题目“当一个程序异常终止时,程序输出的最后几行常常会丢失,原因是什么?我们能够采取怎样的措施来解决这个问题?”

​ 答:一个异常终止的程序可能没有机会来清空其输出缓冲区,因此,该程序生成的输出可能位于内存的某个位置,但却永远不会被写出。

对于试图调试这类程序的编程者来说,这种丢失输出的情况经常会误导他们,因为它会造成这样一种印象,程序发生失败的时刻比实际上运行失败的真正时刻要早得多。解决方案就是在调试时强制不允许对输出进行缓冲。要做到这一点,不同的系统有不同的做法,这些做法虽然存在细微差别,但大致如下 :setbuf(stdout, (char*)0);这个语句必须在任何输出被写入到stdout之前执行。该语句最恰当的位置就是作为main函数的第一个语句。

5.4 使用errno检测错误

​ error是一个包含在<errno.h>中的预定义的外部int变量是一个全局变量,用于表示最近一个函数调用是否产生了错误。若为0,则无错误,其它值均表示一类错误。

​ 很多库函数,特别是那些与操作系统有关的,当执行失败时会通过一个名称为errno的外部变量,通知程序该函数调用失败。库函数在调用没有失败的情况下并没有强制要求库函数一定要设置errno为0,但同时,库函数在调用成功时,既没有强制要求对errno清零也没有禁止设置errno。因此,在调用库函数时,我们应当首先检测作为错误指示的返回值,确定程序执行已经失败。然后,再检测errno

可以使用strerror(char * strerror(int errnum))返回错误码所对应的错误信息,不会自动打印,使用时需要引用<errno.h>和<string.h>两个头文件,或者perror(void perror(const char *s))参数 s 所指的字符串会先打印出,后面再加上错误原因,使用时需要引用<stdio.h>。

#include <stdio.h>
#include <string.h>
#include <errno.h>
int main ()
{
  FILE * pFile;
  pFile = fopen ("unexist.ent","r");
  if (pFile == NULL)
  {
    printf ("Error opening file unexist.ent: %s\n",strerror(errno));
    perror("fopen")//先打印fopen再打印错误信息
  }
  return 0;
}
/*打印结果
Error opening file unexist.ent: No such file or directory
fopen: No such file or directory
*/

5.5 库函数signal

​ 库函数signal是捕获异步事件的一种方式,使用时需要引用头文件<signal.h>。可以这样调用 signal 函数, signal (signal type, handler function)

六.预处理器

​ 严格意义上,在编程开始之前,C语言预处理器首先对程序代码做了必要的转换处理。虽然宏很有用,但宏只是对程序的文本起作用,也就是宏提供了一种对组成C程序的字符进行变换的方式,而不是作用与程序中的对象。

6.1 不能忽视宏定义中的空格

​ 前面我们提到,如果函数不带参数,我们在调用时只需要在函数名后加上一对括号,但是宏如果不带参数,只需要使用宏名即可调用。

​ 宏定义中的空格”暗藏玄机“:

# define f (x) ((x)-1)

​ 上述代码是什么意思的,我们可能会以为f(x)代表((x)-1)但实际上是代表了(x)((x)-1),因为f与(x)之间多了一个空格!如果,我们希望定义f(x)为((x)-1),必须写成 # define f(x) ((x)-1)

6.2 宏并不是函数

我们最好在宏定义中把每个参数都用括号括起来,同样整个结果表达式也应该用括号括起来,以防止当宏用于一个更大一些的表达式中可能出现的问题。括号的作用就是预防引起与优先级有关的问题

​ 例如,假设宏abs被定义成这个样子:

# define abs(x) x>0?x:-x

​ 那我们来看看abs(a-b)的值是怎样的,表达式abs(a-b)会被展开成a-b>0?a-b:-a-b,我们可以看到与我们的预期有差别,这里的子表达式-a-b相当于(-a)-b而不是-(a-b)。而表达式abs(a)+1展开的结果是a>0?a:-a+1而不是((a)>0?(a):-(a))+1这就是没加括号的结果。

​ 正确的写法是:

# define abs(x) (((x)>=0?(x):(-x)

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

#define M ((arr[i++])>6?(arr[i++]):arr[i])

int main()
{
	int i = 0;
	int arr[5] = { 8,3,7,6,5 };
	i = 0;
	M;
	printf("%d ", i);
	return 0;
}

​ 上述程序,首先i是0,那么宏替换后得到的arr[0]与6比较,得到8比6大,此时i++得到i是1,但是此后又经历了一次arr[i++],所以我们的i就会变成2!也就是我们无意间就对i进行了两次++!所以在宏里面要进行++或者–运算的一定要小心,小心,再小心!!!同理,传递的参数要是有此类运算也要三思而后行!

6.3 宏并不是语句

宏不是语句,定义时不能加上分号

assert宏用于断言某个表达式的值为真。如果断言失败,即表达式的值为假,那么程序将会终止执行,并输出一条错误信息。

assert宏的语法如下:

#include <assert.h>

void assert(expression);

​ 其中,expression是一个需要断言的表达式。

​ 当程序执行到assert宏时,会首先计算expression的值。如果值为真,即非零,那么程序继续执行。如果值为假,即零,那么程序会终止执行,并输出一条错误信息,指示断言失败的位置和具体的表达式。

​ assert宏在调试过程中非常有用,可以用来验证程序中的假设是否成立。当程序中的某个假设不成立时,assert宏可以帮助我们快速定位问题所在,并输出错误信息。需要注意的是,assert宏只在调试模式下起作用。在发布版本中,assert宏会被宏定义为一个空操作,不会有任何影响。因此,assert宏只应该用于调试和测试阶段,在发布版本中应该移除或禁用。

6.4 宏并不是类型定义

宏的一个常见用途是,使多个不同变量的类型可在一个地方说明。这种用法的一个优点就是可移植性。但是其与类型定义相比,类型定义更通用一些。

​ 例如:

# define T1 struct foo FOOTYPE
typedef struct foo *T2

​ 上述代码从定义上看,T1和T2从概念完全符同,都是指向结构foo的指针。但是如果我们用它们来定义多个变量时,就会出问题:

​ 第一个声明扩展为:struct foo *a,b;这个语句a定义为指向结构体的指针,而b却定义为一个结构。第二个声明则不同,它同时定义a和b为指向结构的指针。

​ 书中有一道练习题“请使用宏来实现max的一个版本,其中max的参数都是整数,要求在宏max的定义中这些整型参数只被求值一次“

​ 答案:

static int max_tmp1,max_tmp2;
#define max(a,b) (max_tmp1=(a),max_tmp2(b),max_tmp1>max_tmp2?max_tmp1:max_tmp2)

​ 注意上述代码不能嵌套调用。

七.可移植性缺陷

7.1 应对C语言标准变更

7.2 标识符名称的限制

​ 不同标准下的限制也不同,一般建议不能以数字开头,不能是关键字,严格区分大小写,不引起重复

7.3 整数的大小

​ C语言中为编程者提供了三种不同长度的整数:short型,int型,long型(C99下也还有longlong型)。三者之间长度关系是非递减的,做了一些规定:1.short型不大于int型,int型不大于long型。2.一个普通(int类型)整数足够大以容纳任何数组下标。3.字符长度由硬件特性决定。

类型大小(单位字节)
short2
int2 or 4
long4

7.4 字符是有符号整数还是无符号整数

​ 我们知道整数有有符号以及无符号两种类型。字符大多是八位,当我们把char字符转换成一个int整数时应该把字符作为有符号还是无符号呢?**如果要转换为有符号数,那么编译器将char类型扩展为int 型时需要复制符号位,如果是转换为无符号数,那么编译器只需要在多余的位上直接填充0即可。**一个字符最高位是1是代表有符号还是无符号,决定着取值范围是-128127还是0255。如果最高位是1,且声明为无符号字符,无论什么编译器,在该字符转换为整型时都只需在多余的位填充0即可。

​ 一个常见错误认识:如果c是一个字符变量,使用(unsigned)c 可以得到与c等价的无符号整数。其实这里会出现错误。因为转换为无符号时,会先转换为int型,这里会得到非预期的结果,正确的方法是(unsigned char)c,这个方式无需先转换为int就可以直接转换。

7.5 移位运算符

​ 在向右移位时,空出的位是由0填充还是由符号位的副本填充?如果被移位的对象是无符号数,那么空出的位是由0填充,如果是有符号数,那么二者都可以。需要注意,即使C实现将符号位复制到空出的位中,有符号整数的向右移位运算符并不等同于除以2的某次幂。
​ 移位计数(即移位操作的位数)允许的取值范围是什么?如果被移位的对象长度是n位,那么移位计数应该大于或者等于0且严格小于n,为什么要加这个限制,因为加上这个限制后,我们就能够在硬件上高效地实现移位运算。

7.6 内存位置0

正如前面所说空指针不同于空字符串,null指针并不指向任何对象,除非是用于赋值或比较运算,出于其他任何目的使用null指针都是非法的。

​ 不同的编译器不同机器对内存位置0的处理也是不同的,我们如何检查出机器对0位置的处理呢?我们再机器上运行这样的代码:

# include <stdio.h>
int main ()
{
    char *p;
    p=NULL;
    printf("location 0 contains %d\n",*p);
}

​ 如果程序执行失败,则该机器禁止读取内存地址0;如果以十进制格式打印出内存位置0中存储的字符内容(如果打印则通常是一些“垃圾信息”),则该机器可能允许读取内存地址0但不允许写。

7.7 除法运算时发生的截断

q=a/b;
    
r=a%b;

​ 性质1:根据数学知识,我们最希望q*b+r==a;

​ 性质2:如果改变a的正负号,我们希望只改变q的符号,但不改变q的绝对值;

​ 性质3:当b>0时,我们希望保证r>=0且r<b也就是r与除数同号;
​ 实际上,这三条性质不可能同时在计算机中成立,所以在实现整数除法截断运算时,会放弃三条原则中的至少一条,一般是第三条,改为要求余数与被除数的正负号相同(r与a)。但是C语言定义中之保证了性质1,以及a>=0且b>0时保证|r|<|b|以及r>=0。

最好是避免a为负数这样的情况,并且声明a为无符号数。

7.8 随机数的大小

rand函数会产生一个伪随机非负数,为什么是伪呢?因为每次运行都是同一个,随机数大小与机器的整数长度有关,如果是32位,那么最大为2^31-1。在ANSI C标准中定义RAND_MAX为随机数的最大取值。

​ 我看到这的时候,很疑惑,到底怎么才能产生真正的随机数,书中并没有说明,因此我去查阅了资料。我们可以使用另外一个函数srand函数,srand函数用于给rand函数设定种子。srand的用法为:

void srand (unsigned int seed);

​ 它需要一个 unsigned int 类型的参数。一般,我们通常用时间作为参数,只要每次播种的时间不同,那么生成的种子就不同,最终的随机数也就不同
​ 那我们如何获得时间呢?使用 <time.h> 头文件中的 time() 函数即可得到当前的时间(精确到秒),就像下面这样:

srand((unsigned)time(NULL));

生成随机数之前先进行播种。但是,这些随机数会有逐渐增大或者逐渐减小的趋势,这是因为我们以时间为种子,时间是逐渐增大的。

​ 到这可能又有问题了,在实际开发中,我们往往需要一定范围内的随机数,那么,如何产生一定范围的随机数呢?我们可以利用取余的方法:

int a = rand() % 10; //产生0~9的随机数,注意10会被整除

int a=rand() % 20; //则产生0~19的随机数

如果要规定上下限:

int a = rand() % 10 + 24; //产生24~33的随机数

​ rand()%10+24我们可以看成两部分:rand()%10是产生 0~9 的随机数,后面+24保证 a 最小只能是 24,最大就是 9+24=33。
最后给出产生 24~33 范围内随机数的完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
    int a;
    srand((unsigned)time(NULL));//播种
    a = rand() % 10 + 24;//生成随机数并且限定随机数范围24~33
    printf("%d\n",a);
    return 0;
}

7.9 大小写转换

库函数toupper(转大写 “to upper”嘛,肯定就是转大写啊,这个我觉得大家都看得出来)和tolower(转小写)。

​ 定义为宏(有一个缺点就是如果输入的大小写信息本来就是错误的,那就会返回无用的垃圾信息):

#define _toupper(c) ((c)+’A’-’a’)
#define _tolower(c) ((c)+’a’-’A’)

​ 定义为函数(使用时需要引用头文件<ctype.h>):

int toupper(int c)
{
	if ((c >= 'a') && (c <= 'z'))
		return c + ('A' - 'a');
	return c;
}

int tolower(int c)
{
	if ((c >= 'A') && (c <= 'Z'))//
		return c + ('a' - 'A');
	return c;
}
/*  
	这里另外简单介绍判断字符是否为大写的字符函数isupper()
	int isupper(int ch)
	以及判断字符是否为小写的字符函数islower()
	int islower(int ch)
	使用也需要引用头文件<ctype.h>
*/

7.10 首先释放,然后重新分配

​ C语言为大家提供了三个内存分配函数:malloc,realloc和free。

malloc:申请分配一块新内存
realloc:指向已分配的内存,重新调整大小,可能涉及到内存的拷贝
free :释放内存


img

​ 初看这本书真是收益匪浅,不愧为经典。值得细细去品尝,去嚼出味来,这本书看完吸收后,可以减少很多bug。
img

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值