《C语言编程精髓总结.doc》由会员分享,可在线阅读,更多相关《C语言编程精髓总结.doc(90页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、C和指针C专家编程C陷阱与缺陷C语言编程要点编程精粹-Microsoft编写优质无错C程序秘诀总 结说明:总结的知识点主要源于上面的4本书,编程精粹-Microsoft编写优质无错C程序秘诀这本书未做总结,该书有清晰版的pdf格式的电子版。-wuliming -2007-04-25wuliming_sc指针和数组相关概念*字符与字符串的区别指针与数组1指针与数组2指针和数组的相同与不同用malloc为字符串分配存储空间时的注意事项作为常数的数组声明(c缺陷与陷阱3.3节.在其它部分有包含该节的知识点,了解or略过)字符串常量用字符串常量初始化指针和数组二维数组下标操作的相关概念指向一维、二维数
2、组的指针array_name和&array_name的异同数组作为函数的参数时,不能通过sizeof运算符得到该数组的大小用strlen()求字符串的长度char * 和 const char *的兼容性问题空指针相关的问题NULL和NUL的区别未初始化的指针和NULL指针的区别理解函数的声明函数参数的传值调用函数指针作为函数参数的多维数组强制类型转换相关概念可变参数相关问题malloc()、calloc()、realloc()在程序退出main()函数之后,还有可能执行一部分代码吗?总线错误和段错误相关概念数字和字符串之间转换相关的函数*怎样判断一个字符是数字、字母或其它类别的符号?怎样将数
3、字转换为字符串?怎样将字符串转换为数字?字符串以及内存操作相关函数*字符串拷贝和内存拷贝函数:strcpystrncpymemcpymemmovememccpybcopy字符串和内存数据比较函数:strcmpstrcasecmpstrncasecmpmemcmpstrcollbcmp连接字符串的函数:strcatstrncat查找字符/字符串的函数:strstrstrchrstrrchrmemchr其它相关的函数:indexrindexstrlenstrdupmemsetbzerostrspnstrcspnstrpbrkstrtok数据结构及算法相关函数qsort()bsearch()lsea
4、rch(线性搜索)lfind(线性搜索)srand(设置随机数种子)rand(产生随机数)OTHER*什么是标准预定义宏?断言 assert(表达式) 相关概念连接运算符“#”和字符串化运算符#有什么作用?注释掉一段代码的方法Typedef相关概念= 不同于 =词法分析中的“贪心法”运算符的优先级问题变量的存储类型及初始化相关概念左值和右值相关的概念变量的值和类型相关的概念怎样删去字符串尾部的空格?怎样删去字符串头部的空格?怎样打印字符串的一部分?结构的自引用结构的存储分配边界计算与不对称边界整数溢出返回整数的getchar函数更新顺序文件随机数的相关概念用递归和迭代两种办法解fibonacc
5、i字符与字符串的区别(c缺陷与陷阱1.5节)#include int main() char ch = abcdefghijklmnopqrstuvwxyz; char str = abcdefghijklmnopqrstuvwxyz; printf(-%c-n%sn,ch, str );return 0;编译该程序可以通过,但是会产生警告;输出结过为:-z-Abcdefghijklmnopqrstuvwxyz / 在Dev-C+ 4.9.9.2编译环境中可以通过,但是在VC.0中通不过指针与数组1(c缺陷与陷阱3.1节)c语言中的数组值得注意的地方有以下两点:1、c语言中只有一维数组,而且数
6、组的大小必须在编译期间就作为一个常数确定下来(C99标准允许变长数组,GCC编译器中实现了变长数组)。然而,c语言中数组的元素可以是任何类型的对象,当然也可以是另外一个数组。这样,要仿真出一个多维数组就不是一件难事。2、对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针。其他有关数组的操作,哪怕它们乍看上去是以数组下标进行运算的,实际上都是通过指针进行的。换句话说,任何一个数组下标运算都等同于一个对应的指针运算,因此我们完全可以依据指针行为定义数组下标的行为。现在考虑下面的例子:int i;int *p;int calendar1231;上面声明的cal
7、endar是一个数组,该数组拥有12个数组类型的元素,其中的每个元素都是一个拥有31个整型元素的数组。因此,sizeof(calendar)的值是:3112sizeof(int)。考虑一下,calendar4的含义是什么?因为calender是一个有着12个数组类型元素的数组,它的每个数组类型元素又是一个有着31个整型元素的数组,所以calendar4是calendar数组的第5个元素,是calendar数组中12个有着31个整型元素的数组之一。因此,calendar4的行为也表现为一个有着31个整型元素的数组的行为。例如,sizeof(calendar4)的结果是:31sizeof(int)
8、。又如,p = calendar4;这个语句使指针p指向了数组calendar4中下标为0的元素。因为calendar4是一个数组,我们可以通过下标的形式来指定这个数组中的元素:i = calendar47,这个语句也可以写成下面这样而表达的意思保持不变:i = *( calendar4 + 7 ),还可以进一步写成:i = *( *( calendar + 4 ) + 7 )。下面我们再看:p = calendar; 这个语句是非法的,因为calendar是一个二维数组,即“数组的数组”,在此处的上下文中使用calendar名称会将其转换为一个指向数组的指针。而p是一个指向整型变量的指针,两
9、个指针的类型不一样,所以是非法的。显然,我们需要一种声明指向数组的指针的方法。int calendar1231;int (*monthp)31;monthp = calendar;int (*monthp)31 语句声明的 *monthp 是一个拥有31个整型元素的数组,因此,monthp就是一个指向这样的数组的指针。monthp指向数组calendar的第一个元素。HERE指针与数组2(c和指针.P141.)1、数组的名的值是一个指针常量,不能试图将一个地址赋值给数组名;2、当数组名作为sizeof操作符的操作数时,sizeof(arrayname)返回的是整个数组的长度,而不是指向数组的指
10、针的长度;3、当数组名作为单目操作符&的操作数,取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量值的指针。4、指针和数组并不总是相等的。为了说明这个概念,请考虑下面这两个声明:int a5;int *b;a和b能够互换吗?它们都具有指针值,它们都可以进行间接访问和下标操作。但是,它们还是有很大的区别的:声明一个数组时,编译器将根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置。声明一个指针变量时,编译器只为指针本身保留内存空间,它并不为任何整型值分配内存空间。而且,指针变量并未被初始化为指向任何现有的内存空间,如果它
11、是一个自动变量,它甚至根本不会被初始化。把这两个声明用图的方法表示,可以发现它们之间存在显著的不同: a b? 因此,上述声明后,表达式*a是完全合法的,但表达式*b却是非法的。*b将访问内存中某个不确定的位置,或者导致程序终止。另一方面,表达式b+可以通过编译,但是a+却不能,因为a的值是一个常量。#includeint main() /注意sizeof(num)的长度应该为10*4=40 int num = 0,1,2,3,4,5,6,7,8,9;printf( sizeof(num) = %dn, sizeof(num) );/注意sizeof(str)的长度应该为11,包括字符串后面的
12、0 char str = 0123456789;printf( sizeof(str) = %dn, sizeof(str) );/注意sizeof(str1)的长度应该为10,不包括字符串后面的0,但是,最好将字符串的最后一个字符设定为空char str1 = 0,1,2,3,4,5,6,7,8,9;printf( sizeof(str1) = %dn, sizeof(str1) );/&num的类型为int (*)10,表示的是一个指向长度为10的整形数组的指针int (*ptoint)10 = #printf( sizeof(ptoint) = %d, (*ptoint)9 =
13、%dn, sizeof(ptoint), (*ptoint)9 );/&str的类型为char (*)11,表示的是一个指向长度为11的字符数组的指针,注意str数组的长度是11,而不是10char (*ptostr)11 = &str;printf( sizeof(ptostr) = %d, (*ptostr)9 = %cn, sizeof(ptostr), (*ptostr)9 );/由于p指向的是数组num5,所以对下标取负值后,不会超出数组的正常取值范围/该例子也说明了为什么下标检查在c语言中是一项困难的任务:下标引用可以作用于任意的指针,而不仅仅是数组名/作用于指针的下标引用的有效性
14、即依赖于该指针当时恰好指向什么内容,也依赖于下标的值int *p = num + 5;printf( p-1 = %d, p0 = %d, p1 = %d n, p-1,p0,p1 );/下面的表达式中,num5和5num的值是一样的,把它们转换成对等的间接访问表达式,它们都等同于*(num + 2)/5num这个古怪的表达式之所以可行,缘于C实现下标的方法。对编译器来说,这两种形式并无差别/但是,决不应该编写形如5num的表达式,因为它会大大的影响程序的可读性printf( num5 = %d, 5num = %d n, num5, 5num ); getchar();return 0;输出
15、结果为:指针和数组的相同与不同(c专家编程.P199.)在实际应用中,数组和指针可以互换的情形要比两者不可互换的情形更为常见。让我们分别考虑“声明”和“使用”这两种情况。声明本身还可以进一步分为3种情况:外部数组的声明;数组的定义(定义是声明的一种特殊情况,它分配内存空间,并可能提供一个初始值);函数参数的声明; extern,如extern char a; 不能改写为指针的形式 声明 定义,如char a10; 不能改写为指针的形式 数组 函数的参数,可以随意选择数组 在表达式中使用 的形式或者指针的形式 如c=ai,可以随意选择数组 形式或者是指针形式也既是:作为函数参数时、在语句或表达式
16、中使用数组时,我们可以采用数组或者指针的任何一种形式,除此之外的其他情况下,指针和数组不要互换。下面就数组和指针相同的情况做详细的说明:规则1、表达式中的数组名被编译器当作一个指向该数组第一个元素的指针。假如我们声明:int a10; int *p = a;就可以通过一下任何一种方式来访问ai:pi *( p + i ) *( a + i ) 事实上,可以采用的方法很多。对数组的引用如ai 在编译时总是被编译器改写成*(a+i)的形式,C语言标准要求编译器必须具备这个概念性的行为。编译器自动把下标值的步长调整到数组元素的大小。如果整型数的长度是4个字节,那么ai+1和ai在内存中的距离就是4。
17、对起始地址执行加法操作之前,编译器会负责计算每次增加的步长。这就是为什么指针总是有类型限制,每个指针只能指向一种类型的原因所在,因为编译器需要知道对指针进行解除引用操作时应该取几个字节,以及每个下标的步长应取几个字节。规则2、下标总是和指针的偏移量相同。把数组下标作为指针加偏移量是c语言从BCPL(C语言的祖先)继承过来的技巧。在人们的常规思维中,在运行时增加对c语言下标的范围检查是不切实际的。因为取下标操作只是表示将要访问该数组,但并不保证一定要访问。而且程序员完全可以使用指针来访问数组,从而绕过下标操作符。在这种情况下,数组下标范围检测并不能检测所有对数组的访问的情况。事实上,下标范围检测
18、被认为不值得加入到c语言当中。还有一个说法是,在编写数组算法时,使用指针比使用数组更有效率。这个颇为人们所接收的说法在通常情况下是错误的。使用现代的产品质量优化的编译器,一维数组和指针引用所产生的代码并不具有显著的差别。不管怎样,数组下标是定义在指针的基础上,所以优化器常常可以把它转化为更有效率的指针表达式,并生成相同的机器指令。规则3、在函数参数的声明中,数组名被编译器当作指向该数组第一个元素的指针。 在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向数组第一个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的拷贝。这种转换意味着在声明函数的时候,以下三种形式都是合法的
19、(同时无论实参是数组还是真的指针也都是合法的):my_function( int *turnip ) my_function( int turnip ) my_function( int turnip200 ) 用malloc为字符串分配存储空间时的注意事项(c缺陷与陷阱3.2节)作为常数的数组声明(c缺陷与陷阱3.3节.在其它部分有包含该节的知识点,了解or略过)字符串常量(c和指针.P269.)当一个字符串常量出现在表达式中时,它的值是指针常量。编译器把该字符串的一份拷贝存储在内存的某个位置,并存储一个指向第一个字符的指针。我们可以对字符串常量进行下标引用、间接访问以及指针运算。“xyz”
20、+1字符串常量实际上是个指针,这个表达式计算“指针值加上1”的值。它的结果也是个指针,指向字符串中的第二个字符y* “xyz”对一个指针执行间接访问操作时,其结果就是指针所指向的内容。字符串常量的类型是“指向字符的指针”,所以这个间接访问的结果就是它所指向的字符:x。注意表达式的结果并不是整个字符串,而只是它的第一个字符。“xyz”2同样可以推断出上面这个表达式的值就是字符z。#include/接受一个无符号整型值,把它转换成字符,并打印出来/如果是打印16进值的数,可以用这种方法:putchar( 0123456789ABCDEF value % 16 )void binary_to_asc
21、ii( unsigned long value ) unsigned long quotient;quotient = value / 10;if( quotient != 0 )binary_to_ascii( quotient );putchar( 0123456789 value % 10 );int main() /字符串常量实际上是个指针,这个表达式计算指针值加上1的值。它的结果也是个指针,/指向字符串中的第二个字符:y printf( %sn, xyz+1 );/对一个指针执行间接访问操作时,其结果就是指针所指向的内容。/字符串常量的类型是指向字符的指针,所以这个间接访问的结果就是
22、它所指向的字符:xprintf( %cn, *abcdefg );/同样可以推断出上面这个表达式的值就是字符zprintf( %cn, abcdefg3 );binary_to_ascii( 1234567 ); getchar();return 0;用字符串常量初始化指针和数组 (c专家编程.P87.)定义指针时,编译器并不为指针所指的对象分配空间,它只是分配指针本身的空间,除非在定义时同时赋给指针一个字符串常量进行初始化。例如,下面的定义创建一个字符串常量(为其分配内存):char *p = “breadfruit”;注意只有对字符串常量才是如此。不能指望为浮点数之类的变量分配空间,如:f
23、loat *pip = 3.14; /*错误,无法通过编译*/在ANSI C中,初始化指针时所创建的字符串常量被定义为只读。如果试图通过指针修改这个字符串值,程序会出现未定义的行为。在有些编译器中,字符串常量被存放在只允许读取的文本段中,以防止它被修改。数组也可以用字符串常量进行初始化:char a = “gooseberry”;与指针相反,由字符串常量初始化的数组是可以修改的。比如下面的语句:strncpy( a, “black”, 5 );将数组的值修改为“blackberry”。#include#includeint main(void) char *p = this is a exam
24、ple;/char *pi = 3.14; /这样定义是错误的,无法通过编译/p0 = T; /修改该字符串常量时,编译是没问题,但是运行时会出现异常char a = gooseberry; strncpy( a, black, 5 );printf(%sn, p );printf(%sn, a );return 0;二维数组下标操作的相关概念(c和指针.P156.)指向一维、二维数组的指针(c和指针.P158.)array_name和&array_name的异同前者是指向数组中第一个元素的指针,后者是指向整个数组的指针。char aMAX; /*array of MAX characters
25、*/char *p = a; /*p为指向数组的指针*/ char *pa = &a; /*该语句是不正确的,pa的类型为char *,而&a的类型为char (*)MAX*/char (*pb)MAX = &a; /*该语句是正确的,pb的类型为char (*)MAX*/#includevoid main()char a5 = a,b,c,d,0; char *p = a; /运行下面这句后, vc6.0 提示的错误为:cannot convert from char (*)5 to char *,&a的类型应该是指向一个数组的指针/char *pa = &a; /所以,应该定义一个指向相同
26、类型和大小的数组的指针来获得“&a”的值char (*point_to_str)5; point_to_str = &a; printf(%dn%dn,&p, &point_to_str); printf(%sn%sn, p, point_to_str);运行结果为:12450441245040abcdabcd数组作为函数的参数时,不能通过sizeof运算符得到该数组的大小不可以。当把数组作为函数的参数时,你无法在程序运行时通过数组参数本身告诉函数该数组的大小,因为函数的数组参数相当于指向该数组第一个元素的指针。这意味着把数组传递给函数的效率非常高,也意味着程序员必须通过某种机制告诉函数数组参
27、数的大小。为了告诉函数数组参数的大小,人们通常采用以下两种方法:第一种方法是将数组和表示数组大小的值一起传递给函数,例如memcpy()函数就是这样做的: memcpy( dest,source,length );第二种方法是引入某种规则来结束一个数组,例如在C语言中字符串总是以ASCII字符NUL(0)结束,而一个指针数组总是以空指针结束。请看下述函数,它的参数是一个以空指针结束的字符指针数组,这个空指针告诉该函数什么时候停止工作: void printMany( char *strings ) inti = 0; while( stringsi != NULL ) puts(strings
28、i+); C程序员经常用指针来代替数组下标,因此大多数C程序员通常会将上述函数编写得更隐蔽一些: void printMany( char *strings ) while( *strings ) puts(*strings+); 尽管你不能改变一个数组名的值,但是strings是一个数组参数,相当于一个指针,因此可以对它进行自增运算,并且可以在调用puts()函数时对strings进行自增运算。用strlen()求字符串的长度(c和指针.P159.)库函数strlen的原型为:size_t strlen( char const *string );strlen返回一个类型为size_t的值。
29、这个类型是在头文件stddef.h中定义的,它是一个无符号整型类型。在表达式中使用无符号数可能导致不可预期的结果。例如,下面两个表达式看起来是相等的:if( strlen(str1) = strlen(str2) )if( strlen(str1) - strlen(str2) = 0 )但事实上它们是不相等的,第1条语句会按照预想的那样工作,但第2条语句的结果将永远是真的。strlen的结果是无符号数,所以操作符 = 左边的表达式也将是无符号数,而无符号数决不可能是负的。表达式中如果同时包含了无符号数和有符号数,可能会产生奇怪的结果。和上面的一对语句一样,下面两条语句并不相等,原因相同。if
30、( strlen(str1) = 10 )if( strlen(str1) - 10 = 0 )如果将strlen的结果值强制转换成int,就可以消除这个问题。类似的,sizeof()的返回类型也是size_t,和strlen()一样,也存在类似的现象(sizeof()是一个运算符,不是函数)。对无符号类型的建议:尽量不要在代码里面使用无符号类型,以免增加不必要的复杂性。尤其是,不要仅仅因为无符号数不存在负值而用它来表示数量。尽量使用像int这样的有符号类型,这样在涉及升级混合类型的复杂细节时,不必担心边界情况。对于返回类型为无符号的函数(strlen()、sizeof(),最好将结果转换成整
31、型( (int)strlen()、(int)sizeof() ),这样可以避免出现比较微妙的bug(在java里面,就没有无符号数)。#include#include#includeint main() char str1 = 0123456789;char str2 = abcdefghijk; / sizeof()的返回类型也是无符号类型,无符号类型的运算结果也被转换成无符号类型,不可能为负 / (int)sizeof( str1 ) (int) sizeof( str2 ) 0 这个表达式将得到预期结果if( sizeof( str1 ) - sizeof( str2 ) 0 )prin
32、tf( sizeof( str1 ) - sizeof( str2 )的计算结果是无符号型的,不可能为负n);if( strlen(str1) = strlen(str2) ) printf( strlen的返回值为无符号整型类型,把两个无符号整型类型做比较,会得到预期的结果n);if( strlen(str1) - strlen(str2) = 0 ) printf( strlen(str1) = %dn,strlen(str1) ); printf( strlen(str2) = %dn,strlen(str2) ); printf( ( strlen(str1) - strlen(str
33、2) = 0 ) 表达式的值为: %dn, strlen(str1) - strlen(str2) = 0); printf( strlen(str1) - strlen(str2)的结果是无符号类型,无符号数不可能是负值,所以该条件永远成立n);/注意:sizeof()和strlen()两个函数取得的值是不相等的/sizeof()求得的长度包括字符串末尾的那个空字符0/strlen()求得的长度不包括字符串末尾的空字符printf( sizeof(str1) = %dn strlen(str1) = %dn, sizeof(str1), strlen(str1) ); getchar();r
34、eturn 0;char * 和 const char *的兼容性问题(c专家编程.P19.)有时候必须非常专注的阅读ANSI C 标准才能找到某个问题的答案。一位销售工程师把下面的代码作为测试例子发给SUN的编译器小组。#includevoid foo( const char *P )int main( int argc, char *argv ) foo( argv );return 0;在VC6.0下编译这段代码,编译器会发出警告:cannot convert parameter 1 from char * to const char * 提交代码的工程师想知道为什么会产生类似的警告,他
35、认为,实参char *s 与形参 const char *p 应该是相容的,标准库中所有的字符串处理函数都是这样的。那么,为什么实参 char *argv 与形参 const char *P实际上不能相容呢?答案是肯定的,它们并不相容。现在我们回顾一下标准中有关简单赋值的部分,它位于ANSI C 第6.3.16.1节,描述了下列约束条件:要使上述赋值形式合法,必须满足下列条件之一:两个操作数都是指向有限定符或无限定符的相容类型的指针,左边指针所指向的类型必须具有右边指针所指向类型的全部限定符。正是这个条件,使得函数调用中实参char*能够与形参const char*匹配。它之所以合法,是因为在
36、下面的代码中:char *cp;const char *cpp;cpp = cp; 左操作数是一个指向有const限定符的char的指针; 右操作数是一个指向没有限定符的char的指针; char类型和char类型是相容的,左操作数所指向的类型具有右操作数所指向类型的限定符(无),再加上自身的限定符 const (注意反过来不能赋值)。标准第6.3.16.1节没有说明char *实参与const char *形参是否相容。标准6.1.2.5节中讲述实例的部分声称:const float * 类型并不是一个有限定符的类型,它的类型是“指向一个具有const限定符的float类型的指针”,也就是说
37、const限定符是修饰指针所指向的类型,而不是指针。类似地,const char *也是一个没有限定符的指针类型,它的类型是“指向有const限定符的char类型的指针的指针”。 由于char * 和const char * 都是没有限定符的指针类型,但它们所指向的类型不一样(前者指向char *,后者指向 const char *),因此它们是不相容的。因此类型为char *的实参和类型为 const char *的形参是不相容的,编译器会产生一条诊断信息。备注:解释的有些牵强,目前记住结果就可以了空指针相关的问题(c缺陷与陷阱3.5节)#include#includeint main()c
38、har *p = NULL;if( p = (char *)0 ) printf( p is a null pointn );else printf( p is not a null pointn ); /该语句不会引起编译错误,但是运行时会出现异常if( strcmp( p, (char *)0 ) = 0 ) printf(cant dereference pn); /该语句不会引起编译错误,但是运行时会出现异常printf(%d, *p ); getchar();return 0;NULL和NUL的区别NULL是在头文件中专门为空指针定义的一个宏。NUL是ASCII字符集中第一个字符的名
39、称,它对应于一个零值。C语言中没有NUL这样的预定义宏。注意:在ASCII字符集中,数字0对应于十进制值48,不要把数字0和0(NUL)的值混同起来。NULL可以被定义为(void *)0,而NUL可以被定义为0。NULL和NUL都可以被简单地定义为0,这时它们是等价的,可以互换使用,但这是一种不可取的方式。为了使程序读起来更清晰,维护起来更容易,你在程序中应该明确地将NULL定义为指针类型,而将NUL定义为字符类型。对指针进行解引用操作可以获得它的值。从定义来看,NULL指针并未指向任何东西。因此,对一个NULL指针进行解引用操作是非法的。在对指针进行解引用操作之前,必须确保它并非NULL指针。未初始化的指针和NULL指针的区别(c和指针.P95.)未初始化的指针NULL指针理解函数的声明(c缺陷与陷阱2.1节)函数参数的传值调用(c和指针.P122.)#includechar ga = abcdefghijklm;void my_ar