C陷阱与缺陷读书笔记
绝对值得一看的好东西啊!!!!!!编译器中负责将程序分解为一个个符号的部分,一般称为"词法分析器".编译器在进行词法分析时会忽略符号之间的空白(包括空格符,制表符,换行符).
1.1 =不同于==
while(c=' '||c=='\t'||c=='\n');(本意是跳过空格符,制表符,换行符)
当时由于把比较运算符误写成了赋值运算符.又因为赋值运算符=的优先级要低于逻辑运算符,逻辑运算符的优先级要低于比较运算符.
因此实际上是将表达式 (' '||c=='\t'||c=='\n')的值赋给了c;因为' '不等于0(' '的ACSII码值为32),则不论此前c为何值,上述表达式的结果
都是1,因而陷入一个死循环.(正确的写法应该是:while(c==' '||c=='\t'||c=='\n');).
当要进行比较的时候,应该进行显示的比较,也就是说,下例:
if(x=y)
foo();
应该写成:
if((x=y)!=0) (比较运算符的优先级高于赋值运算符)
foo();
这种写法也使得代码的意图一目了然.
(比较运算符的结果只能是0或者1,永远不可能小于0;)
---------------------------------------------------------------------------------------------------------------------------
1.2 词法分析中的"贪心法"
C语言有一个很简单的规则:每一个符号应该包含尽可能多的字符.也就是说,编译器将程序分解成符号的方法时,从左到右一个字符一个字符的
读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,
继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号.这个策略被称为"贪心法"或者"大嘴法".
(如果(编译器的)输入流截止至某个字符之前都已经被分解为一个个符号,那么下一个符号将包含从该字符之后可能组成一个符号的最长字符串)
需要注意的是:符号中间是不能嵌有空白的(空格符,制表符,换行符).例如,==是一个符号,= =是两个符号.下面的表达式:
a---b,与表达式a -- - b的含义相同.
---------------------------------------------------------------------------------------------------------------------------
1.3 如果整形常量的第一个字符是数字0,那么该常量被视作八进制数.因此,10与010的含义截然不同.
int i=010;
printf("i=%d\n",i);
得到的输出结果为i=8;
需要注意的是,有时候在上下文中为了格式的对齐的需要,可能无意中将十进制数写成了八进制数,例如:
structst
{
int part;
char *des;
}parttab[]={
046, "left_handed" ,//无意将十进制数写成了八进制数(需要特别的注意)
047, "right_handed",
125, "frammis" ,//C语言允许初始化列表中出现多余的逗号(这里最后的逗号是可以省略的)
};
初始化列表中的每一行都是以逗号结尾的,正因为每一行在语法上的这种相似性,自动化的程序设计工具(代码编辑器)才能更方便的处理很大
的初始化列表.(sizeof(parttab)/sizeof(struct st)==3)
---------------------------------------------------------------------------------------------------------------------------
1.4 字符与字符串
用单引号引起来的一个字符实际上代表的是一个整数,整数值对应于该字符在编译器采用的字符集中的序列值.因此,对于采用ASCII字符集的
编译器来说,'a'的含义与97(十进制),0x61(十六进制)是严格一致的.(用单引号引起来的一个字符代表一个整数)
用双引号引起的字符串,代表的却是一个指向无名数组起始字符的指针,(用双引号引起来的一个字符代表一个指针)
该数组被双引号之间的字符以及一个额外的二进制为0的字符'\0'初始化
下面的这个语句:
printf("hello\n");
与
char hello[]={'h','e','l','l','o','\n','\0'};
printf(hello);
是等效的.
---------------------------------------------------------------------------------------------------------------------------
1.5 练习1-1
某些C编译器允许嵌套注释.请写一个测试程序,要求:无论是允许嵌套的还是不允许嵌套的编译器,该程序都能正常运行,并且两种情况下得到的
结果不相同.
提示:被双引号引起来的字符串中,注释/**/属于字符串的一部份.而在注释中出现的双引号""又属于注释的一部分.
解答:(C语言定义并不允许嵌套注释)
我们需要找到一组符号序列,且这组符号序列不可避免的要涉及嵌套注释,让我们从这里开始讨论:
/*/**/
对于一个允许嵌套注释的C编译器,无论上面的符号序列后面跟什么,都属于注释的一部分,而对于不允许嵌套注释的C编译器,后面跟的就是
实实在在的代码内容了.因此我们可以在后面跟上一个用一对双引号引起来的注释结束符:
/*/**/"*/"
如果允许嵌套注释,上面的符号序列就等效于一个引号,如果不允许嵌套注释,那么就等效于一个字符串"*/"
则对于/*/**/"*/"/*"/**/
如果允许嵌套注释,上面的字符序列就等效于"/*",如果不允许,那么就等效于"*/"
下面的解法更为巧妙:
/*/*/0*/**/1
这个解法主要是利用了编译器作词法分析时用的“大嘴法”规则,如果编译器允许嵌套,则上式将被解释为:
/* /* / 0 */ * */ 1
两个/*符号和两个*/符号正好匹配,所以上式的值就是1.
如果不允许嵌套注释,注释中的/*将被忽略.因此即使是/*出现在注释也没有特殊的含义,则上式将被解释为:
/* / */ 0 * /* */ 1
它的值就是0*1,也就是0.
因而测试得到程序为:
int test(void)
{
return (/*/*/0*/**/1);
}
返回值为1,表示允许嵌套注释,返回值为0,表示不允许嵌套注释.
**** Hidden Message *****
自己顶下哈 还会有第二章的读书笔记 第二章读书笔记------语法缺陷
2.1 理解函数声明
一旦我们知道了如何声明一个给定类型的变量时,那么该类型的类型转换符就很容易得到了:只需要把声明中的变量名和声明末尾的分号去掉,再将剩下的部分加一个括号"封装"起来就可以了.
例如,因为下面的声明:
float (*h)();
表示h是一个指向返回值类型为float的函数的指针(函数指正),因此
(float (*)())
表示一个"指向一个返回值类型为浮点型的函数的指针"的类型转换符.
有了上面的知识,我们就能对一个常数进行类型转换了,同时也可以把一个常数地址赋给一个指针.
如将0转换为"指向返回值为void的函数的指针"类型,可以这样写:
(void (*)())0
问题1:那么如何将数值存储到一个指定的内存地址:
假设我们现在要向内存地址0x12ff7c上存入一个整型数0x100,那么怎么样才能做到呢?可以用下面的方法实现:
int *p=(int *)0x12ff7c;
*p=0x100;
问题2:那么这里我们是怎么知道一个内存地址是否可以被合法的访问呢?(例如这了的0x12ff7c).
(如果一个地址不能被合法的访问,则在访问该内存地址的时候,编译器会发出内存访问错误的信息.使得程序无法正常运行.)
其实我们可以事先定义一个变量i,比如: int i=0;则变量i所在的地址肯定是可以被访问的,然后通过编译器的Watch窗口上观察&i的值
就知道了变量i的内存地址了.
问题3:假定变量fp是一个函数指针,那么如何调用fp所指向的函数呢? 调用方法如下:
(*fp)(); (函数运算符()的优先级高于单目运算符*,所以(*fp)两侧要加括号)
因为fp是一个函数指针,那么*fp就是该函数指针所指向的函数,所以(*fp)()就是调用该函数的方式.ANSIC标准允许程序员将上式简写为fp()
但是一定要记住这种写法只是一种简写形式.
最终的问题:那么假设我们现在要处理一种情况:当计算机启动的时候,硬件将调用首地址为0位置的子例程,那么我们该如何编写一个C语句来实现呢?
解析:
如果C编译器能够理解我们大脑中的对于类型的认识,那么我们可以这样写:(*0)();
但是上式并不能生效,因为运算法*必须要一个指针来做操作符.而且在这里这个指针还必须是一个函数指针.这样经运算符*作用后的结果才能
作为函数被调用.因此必须对上式的0做类型转换,转换后的描述大致为:“指向返回值为void类型的函数的指针”.则可写成下式:
(*(void (*)())0)(); (末尾的分号使得表达式成为一个语句---函数调用语句)
当然我们可以用typedef来更加清晰的表述这个表达式:
typedef void (*pfun)(); //typedef为类型void (*)() 取别名为pfun.
(*(pfun)0)();
有了上面的知识,我们就可以理解一些更加复杂的表达式了:例如:
考虑signal库函数,在包括该函数的C编译器实现中,signal函数接受两个参数:一个是代表需要"被捕获"的特定的signal的整数值,另一个是
指向用户提供的函数的指针.其中用户提供的函数是用于处理"被捕获"到的特定的signal,返回值为void.
一般情况下,程序员并不主动声明signal函数,而是直接用系统的头文件<signal.h>中的声明,
首先,让我们从用户定义的信号处理的函数开始考虑,这也无疑是最好解决的:该函数的定义如下:
void sigfun(int signal)
{
//特定的信号处理部分
}
函数sigfun的参数是一个代表特定信号的整数值.
上面假设的函数体定义了sigfun函数.我们现在要声明一个指向该函数sigfun的指针变量,设该变量名为sfp,则我们可以如下声明sfp:
void (*sfp)(int sig); sfp=(&sigfun);
又因为signal函数的返回值是一个指向调用前用户定义的信号处理函数的指针,则我们可以如下声明signal函数:
void (*signal(something))(int sig);
其中something代表了signal函数的参数类型,上面的声明可以这样理解:传递适当的参数调用signal函数,对signal函数返回值(为函数指针类型)
解除引用(dereference),然后传递一个整型参数调用解除引用后所得的函数,最后返回值为void类型.
而signal函数接受的两个参数为:一个整型的信号编号,以及一个指向用户定义的信号处理函数的指针,我们此前已经定义了指向用户定义的
信号处理函数的指针sfp: void (*sfp)(int);
则sfp的类型可以通过将上面的声明中的sfp去掉而得到: void (*)(int).
则完整的signal函数的声明为:void (*signal(int signum,void (*handler)(int)))(int);
同样的可以用typedef简化上面的函数声明:
typedef void (*handler)(int);
handler signal(int,handler);
(signal内部时,signal把信号做为参数传递给handler信号处理函数,接着signal函数返回指针,并且又指向信号处理函数,就开始执行它)
---------------------------------------------------------------------------------------------------------------------------
2.2 运算符的优先级问题
假设存在一个已定义的常量FLAG,FLAG是一个整数,且该整数值得二进制表示中只有某一位是1,其余各位的是0,亦即该数是2的某次幂.如果
对于整型常量flags,我们需要判断它在常量FLAG为1的那一位上是否也同样为1,通常可以这么写:
if( FLAG & flags )....
含义是显而易见的,if语句判断括号内表达式的值是否为0.考虑到可读性,如果对表达式的值是否为0(或者NULL)的判断能够显示的加以说明,
无疑使得代码自身就起到了注释该段代码意图的作用.
if( flags & FLAG != 0)...
这个语句虽然更好懂,当却是一个错误的语句,因为!=运算符的优先级高于&运算符.所以上式实际上是被解释为了:
if( flags & (FLAG != 0) )...
因此上式除了FLAG恰好为1的情况下式正确的,FLAG为其他值时都是错误的.
而我们真正想得到的表达式是:if( (lags & FLAG) != 0)...
如果表达式在复杂点的话,这类错误就更加难以发现了.如下面的一个错误:
if((t=BYTPE(pt1->aty)==STRTY) || t==UNIONTY)....
这行代码的本意是:首先赋值给t,然后判断t是否等于STRTY或者UNIONTY.但是实际的结果却是大相径庭:根据BYTPE(pt1->aty)的值是否等于
STRTY,t的取值或者为1或者为0(关系运算符的结果只有0或1),如果t取值为0,还将进一步与UNIONTY比较.
正确的写法为: if( ( (t=BYTPE(pt1->aty)) == STRTY) || t == UNIONTY )....
-------------------------------------------------------------------------------------------------------
C语言的优先级表格:
-------------------------------------------------------------------------------------------------------
运算符 | 结合性
() [] -> . | 自左向右
!~ ++ -- - (type) * & sizeof | 自右向左(记住几个特别的自右向左的运算符)
* / %(取余) | 自左向右
+ - | 自左向右
<<(左移) >>(右移)(移位运算符) | 自左向右
> >= < <= | 自左向右
==!= | 自左向右
& | 自左向右
^(按位异或) | 自左向右
| | 自左向右
&& | 自左向右
|| | 自左向右
?: (条件运算符) | 自右向左(△)
赋值运算符(复合复制运算符) | 自右向左(△)
逗号运算符 | 自左向右
--------------------------------------------------------------------------------------------------------
单目运算符是自右向左结合的,因此*p++会被编译器解释为*(p++),即取指针p所指向的对象,然后指针p递增1;而不是(*p)++,即取指针p指向的对象,然后将该对象加1.
我们需要记住的最重要的有两点:
1. 任何一个关系运算符的优先级都要高于任何一个逻辑运算符.
2. 移位运算符的优先级比算术运算符的低,但是比关系运算符的优先级高.
任何两个逻辑运算符都具有不同的优先级.
由于条件运算符的优先级只高于赋值和逗号运算符,这就允许我们在条件运算符的条件表达式中包括关系运算符的逻辑组合.例如:
int x=5000,z=6,y;
printf("%d\n",y = x>4000 && z>5?3:4);( y = (x>4000&&z>5)? 3 : 4 )
输出的结果为3.
本例还揭示了,赋值运算符的优先级低于条件运算符的优先级是有意义的.
此外,所有的赋值运算符的优先级都是一样的,且结合方向是自右向左,因此:x=y=0;与y=0;x=y;这两句是等价的.
--------------------------------------------------------------------------------------------------------------------------
2.3 注意作为语句结束的标志的分号(;)
如果不是多写了一个分号,而是遗漏了一个分号也会招致麻烦.例如:
if(n<3)
return
L.data=x;
L.time=x;
L.code=x;
此处return后面遗漏了一个分号,然而这段代码还是会顺利的通过编译而不会报错.
上面这段代码实际上相当于:
if(n<3)
return L.data=x;
L.time=x;
L.code=x;
产生的问题有:1. 如果这段代码所在的函数声明的返回值为void,编译器会因为实际的返回值的类型与声明 的返回值类型不一致而报错.
2. 当n>=3时,第一个赋值语句会被直接跳过,由此造成的错误可能会是一个隐藏很深、极难发现的程序Bug.
还有一种情况,也是有分号和没有分号的实际效果相差极为不同.
->那就是当一个声明的结尾紧跟一个函数的定义时,如果声明结尾的分号被省略,编译器可能会把声明的类型视作函数的返回值类型.
考虑下面的例子:
struct st
{
int day;
int month;
int year;
}
main()
{
//code
return 0;
}
在第一个}与紧跟其后的函数main定义之间,遗漏了一个分号.因此上面的代码实际的效果是声明main的返回值是结构st类型.则在编译时,
编译器会报出如下的错误:
incompatible types (不兼容类型).
如果分号没有被省略,函数main的返回值类型会缺省定义为int类型.
------------------------------------------------------------------------------------------------------------------------
2.4 switch语句
switch(variable)
{
case value1:
//program code
break;
case value2:
//program code
break;
case value2:
//program code
/*此处没有break;语句*/
case value2:
//program code
break;
......
default :
//program code
break;
}
当他人或者自己阅读到上面的代码时,能够清楚的知道此处是有意的略去了一个break语句.
case后面只能是整型常量或者字符型常量,或者常量表达式.
->关于case语句的排列顺序
1. 按字母或者数字顺序排列各条case语句.
2. 把正常的情况的case放在前面,异常的情况的case放在后面.
3. 按照执行的频率排列case语句.
(把最常执行的情况放在前面,因为最常执行的代码可能也是调试的时候要单步执行最多的代码) 顶一顶哈 陆续还会有剩下的各个章节的读书笔记大家凑合着看哈 加油!!! RE: C陷阱与缺陷读书笔记 [修改] 学习了啊! 绝对值得一看的好东西啊!!!!!! 感谢楼主分享 看看,,,,,,,,,, 学无止境,GOGO 顶楼主啦..希望楼主多发精品好帖啦..... :ton:好东西 谢谢分享 还会有第二章 楼主讲的很深刻,小宝前来学习了 秦晓彬 发表于 2014-5-3 01:28 static/image/common/back.gif
楼主讲的很深刻,小宝前来学习了
加油哈 gogo 七分醉意 发表于 2014-5-2 22:41 static/image/common/back.gif
好东西 谢谢分享
书还没有看完 不过看了一部分 感觉真的很多以前似懂非懂的东西都豁然开朗了 当然有些东西还是迷迷糊糊的 还有好多需要去搞懂的 剩下来的几章的笔记会陆续的写出来的 当然不是只有书中的内容,还会加上自己的理解,有些地方也会加些例子的 希望对大家有帮助 来自于鱼C回馈于鱼C. 某些地方打字有些错误希望大家理解 能理解懂意思就行了 希望对大家有帮助 我先看看哈 第三章----语义"陷阱"
3.1 指针和数组
问题1:什么是指针,指针的内存布局是怎么样的?
解析:我们可以简单的这样理解:一个基本的数据类型(包括结构体等自定义的类型),加上‘*’号就构成了一个指针类型的模子,这个模子的大小是一定的,与‘*’号前面的数据类型无关,‘*’号前面的数据类型只是说明指针所指向的内存里存储的数据的数据类型,在32位系统下,不管是什么类型的指针,它的大小都是4字节.如下面定义了一个指针:
int *p;(我们称p为指针变量.要注意:p也是变量,也有自己的地址&p)
一个“int *”类型的模子在内存中就“咔”出了4字节的空间,然后把这个4字节大小的空间命名为p,同时限定这4字节的空间里面只能存储某个内存地址,即使你存入的是别的任何数据,计算机都将它们当做地址来处理,而且这个内存地址开始的连续4字节的空间上只能存储int型的数据.
C预言中的数组值得注意的地方有两点:
1. C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来(注意,const修饰的只读变量不能作为数组的维数).并且,C语言中数组的元素可以是任何类型的对象(注意,不能是函数),当然也可以是另外一个数组,这样,要构造一个多维数组就不是一件难事了.
2. 对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组首元素首地址的指针(即数组名),其他的有关数组的操作,哪怕它们看上去是以数组的下标进行运算的,实际上都是通过指针进行的.
指针和数组的区别:
1. 存储的内容:
指针:保存数据的地址,任何存入指针变量p的数据都会被当作地址来处理。p本身的地址由编译器另外存储,存储在哪里,我们并不知道。
数组:保存数据,数组名代表的是数组的首元素的首地址,&a才是代表整个数组的首地址。a本身的地址由编译器另外存储,存储在哪里,我们并不知道。
2. 访问数据的方式:
指针:间接访问数据,首先取得指针变量p的内容,把它作为地址,然后对这个地址内存进行读写数据操作。指针可以以指针的形式访问*(p+i),也可以以下标的形式访问p,但其本质都是先取p的内容(-内存地址-)然后加上i*sizeof(类型)字节作为数据的真正的地址。
数组:直接访问数据,数组名a是整个数组的名字,数组内的每个元素并没有名字,只能通过"具名+匿名"的方式来访问某个数组元素,不能把数组当做一个整体进行读写操作。数组可以以指针的形式访问*(a+i),也可以以下标的形式访问a,但其本质都是a所代表的数组的首元素的首地址加上i*sizeof(类型)字节作为数据的真正的地址。
编译器总是把以下标的形式的操作解析为以指针的形式的操作.
指针就是指针,指针变量在32位系统下,永远占据4字节空间,其值为某一个内存的地址。
数组就是数组,其大小与元素的类型和个数有关;定义数组的时候必须指定数组其维数。数组和指针没有任何的关系!
有了上面的一些概念,明白了数组和指针到底是什么,我们来看下面的一个例子:
int a;
这个语句声明了a是一个数组,该数组拥有12个数组类型的元素,其中每个数组元素都是一个拥有31个整形元素的数组.因此:
sizeof(a)的值就是12*31*sizeof(int)
如果a不是用于sizeof的操作数,而是用于其它的场合,那么a总是被转换为一个指向a数组首元素首地址的指针(即a=&a).因此a是一个二维数组,即“数组的数组”,在此处的上下文中数组名称a会被转换为一个指向数组的指针(即 int (*) )
那么定义一个指向数组的指针就很好理解了:int (*ap);
这个语句的实际效果就是,声明了*ap是一个拥有31个整形元素的数组,因此,ap就是一个指向这类数组的指针.
则当执行 ap=a; 这条语句时,指针ap就指向了数组a的第一个数组元素(a),也就是数组a的12个有着31个元素的数组类型元素之一.
----------------------------------------------------------------------------------------------------------------
关于数组和指针就讨论到这里了,下面我们讨论:一个指针变量加上或者减去一个整数产生的效果.
int a={0};
int *p=a;
如果数组名的地址为2000,也就是数组a在内存的中的首元素的首地址为2000.若执行了p=a;则p的值也为2000.那么p+2的值为多少呢?
--->我们需要明白的是:给指针加上一个整数,与给该指针的二进制表示加上一个整数,两者的含义截然不同.如果p指向一个整数,那么p+1 指向的是计算机内存中的下一个整数,在大多数的计算机中,它都不同于p所指向的地址的下一个内存位置.
好了,那么我们就知道了p+2 的值为2000+2*sizeof(int).同时我么也明白了指针加1,不是简单的将指针的值加1,而是指向下一个数,系统会根据指针所指向的数据的数据类型自动地计算地址.
注意1:当指针指向数组时(并不是真的指向了整个数组,而是指向了数组的首元素),可通过指针形式和下标形式来访问数组元素,因为指针是变量而数组名是常量,所以指针的值可以改变,这就意味着,我们需要特别的注意指针的当前的指向,它是指向数组的哪个元素?还是已经指向了 数组所占内存空间以外的地方?如果已经指向了数组所占的内存空间以外的地方,则一般会出错,这个是指针常出错和最危险的地方.例子:
int a,i;
int *p=NULL;//当指针不用时,最好把它"栓"在NULL处
p=a;
for(i=0;i<10;i++)
*p++=i;
// p=a;(有没有这条语句的差别太大了)
for(i=0;i<10;i++)
printf("%-4d",*p++);
p=NULL; //当用完指针时,也把它"栓"在NULL处
上面的例子就是一个最好的说明.
注意2:指针之间的相减是受到类型影响的,不同类型的指针之间的运算时无法进行的.
如果两个指针指向的是同一个数组,我们可以把这两个指针相减,这样做是有意义的.如p指向s,q指向s,则(p-q)==(i-j).
值得注意的是,如果p和q指向的不是同一个数组的元素,即使它们做指向的地址在内存中的位置正好间隔一个数组元素的整数倍,所得的结果仍然是无法保证其正确性的.
----------------------------------------------------------------------------------------------------------------
3.2 非数组的指针
在C语言中,字符串常量代表了一块包括字符串所有字符以及一个空字符('\0')的内存区域的地址.(因为C语言要求字符串必须以'\0'结尾)
现在我们假定有这样两个字符串s和t,我们希望将这两个字符串连成单个字符串r.当然我们可以通过头文件<string.h>的strcat函数实现,前提要求是存储字符串s的数组要有足够的空间存放字符串s和t连接而成的字符串,那么我们就可以通过 strcat(s,t);的调用方式实现.
如果现在我们定义字符串的方式是通过字符指针的方式的话(即char *s="asd",*t="fgh";),我们就不能通过上面的strcat函数的调用方式了.
那么要做到这一点,我们就只有通过strcpy函数和strcat函数结合的方式实现了,让我们看下实现的代码:
char *r=NULL;
strcpy(r,s);
strcat(r,t);
上面的代码初看觉得好像可行,实际上是不行的.原因在于,不能确定指针r的指向,以及不能确定r指向的地址处是否有足够的空间容纳连接后的字符串.(你可能会问,这里的指针r不是指向了NULL处了吗?那为什么还不能使用呢?这个问题我们接下来会讲)
那我们这次为r分配一定的内存空间:
char r;
strcpy(r,s);
strcat(r,t);
只要s和t指向的字符串不是很大,上面的做法应该就大功告成了.不幸的是,C语言要求我们为数组声明的大小必须要是一个常量,因此我们不能确保r足够大.也许你应该想到了动态分配内存的方法.C语言为我们提供了malloc函数来分配一块连续的内存空间,以及为我们提供了strlen函数来求得一个字符串所包含的字符个数.(记住strlen函数计算字符串中字符的个数,并没有包括作为结束符的空字符('\0').)
那么这次我们就采用动态分配内存的处理这个问题:
char *r=NULL;
r=(char *)malloc(strlen(s)+strlen(t));
strcpy(r,s);
strcat(r,t);
也许你觉的这次应该大功告成了,其实不然,这个程序还是错误的.原因归纳起来有3个:
1. malloc函数有可能无法提供请求的内存,这种情况下malloc函数会返回一个空指针NULL来作为"内存分配失败"事件的信号.
2. 给r分配的内存在使用完之后应该及时的释放,这一点务必要记住,否者会照成内存泄露.
3. 第三个原因,也是最重要的一个原因,那就是在调用mallloc函数的时候并没有分配足够的内存空间.
我们再回忆一下上面刚刚讲的关于strlen函数需要注意的地方:strlen函数计算字符串中字符的个数,并没有包括作为结束符的空字符('\0')
如果strlen(s)的值是n,那么实际的字符串就需要 n+1 个字符的空间.所以,我们需要为r多分配一个字符的空间.
做到这些,并且检查了函数malloc是否调用成功,我们就得到了正确的结果:
char *r=NULL;
r=(char *)malloc(strlen(s)+strlen(t)+1);
if(NULL==r)
{
exit(0);
}
strcpy(r,s);
strcat(r,t);
free(r);
r=NULL;
----------------------------------------------------------------------------------------------------------------
3.3 空指针(NULL)并非空字符串
除了一个特殊的情况,在C语言中将一个整数转换成一个指针,最后得到的结果都取决于具体的C编译器实现.这个特例就是常数0,编译器保证由0转换而来的指针不等于任何一个有效的指针(这也是指针在不使用或者使用完毕的情况,将其“栓”在NULL的原因).出于文档化的考虑,常数0这个值经常用一个符号来代替: #define NULL 0
当然,无论是使用0还是使用NULL,效果都是相同的,需要记住的重要一点是:当常数0被转换为指针后,这个指针绝对不能被解除引用(dereference).换句话说,当我们将0赋值给一个指针变量的时候,绝对不能企图使用该指针所指向的内存中存储的内容.
下面的写法是合法的:
if(p==(char *)0) ......
或者
char *p=NULL;
//program code
if(NULL==p)......
但是如果写成这样: if(strcmp(p,(char *)0)==0)......
就是非法的了.原因在于,库函数strcmp()的实现会包括查看它的指针参数所指向的内存中的内容的操作.
如果p是一个空指针,即使printf(p);(运行的时候会提示assertion failure(断言失败))printf("%s\n",p);的行为也是未定义的.
----------------------------------------------------------------------------------------------------------------
3.4 作为参数的数组声明
C语言中,当一维数组作为函数参数时,编译器总是把它解析成一个指向其首元素首地址的指针。
这么做是有原因的,在C语言中,所有的非数组形式的数据实参均以传值的形式调用(指针作为实参时也不例外)(对实参做一份备份并传递给被调用函数,被调用函数不能够修改作为实参的实际变量的值,而只能修改传递给它的那份备份)然而,如果要复制整个数组,无论在空间上还是时间上,其开销都是非常大的。更重要的是,在大多数的情况下,你其实并不需要整个数组的备份,你只想告诉函数在哪一时刻对哪个特定的数组感兴趣,这样的话,为了节省空间和时间,提高程序的运行效率,就有了上述的规则。
同样的,函数的返回值也不能是一个数组,而只能是指针,这里要明确一个概念,函数本身是没有类型的,只有函数的返回值才有类型.
而这也解释了下面的程序的结果:
void fun(char a[])//char *a实际传递的数组大小与函数形参指定的数组大小没有关系,因而可以不指定形参的数组的大小.
{
int i=sizeof(a);
printf("i=%d\n",i);
}
int main()
{
char a={'0'};
fun(a);
return 0;
}
输出的结果为 4
如果一个指针参数并不实际代表一个数组,即使从技术上而言是正确的,但是,采用数组形式的记法经常会起到误导的作用.如果一个指针参数实际代表的就是一个数组,情况又会是怎么样呢?一个常见的例子就是函数main的第二个参数.
int main(int argc,char *argv[])
{
//program code
}
这种写法与下面的写法完全等价
int main(int argc,char **argv)
{
//program code
}
需要注意的是,前一种写法强调的重点在于argv是一个指向某个数组的起始元素的指针,该数组的元素为字符指针类型.因为这两种写法等价的,所以可以选择最能清楚的反应自己意图的写法.
---------------------------------------------------------------------------------------------------------------- 第三章的一部分读书笔记已经炮制出来了 等等会把第三章剩下的一部分也发上来. 你读或不读,它就在哪里.哈哈!!
页:
[1]
2