《C语言常见问题集.doc》由会员分享,可在线阅读,更多相关《C语言常见问题集.doc(177页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、第1章 C语言 本章主要描述C语言一些基本要素。当你开始编写C程序时,你可能对C语言的一些基本问题感到困惑,如C语言所使用的约定、关键字和术语等。本章将回答这方面你经常会遇到的一些问题。 例如,switch语句是最常用的一种C语言构件,本章将回答与它有关的三个常见问题。本章还涉及其它几个问题,如循环、分支、运算符的优先级和程序块技术。在阅读本章时,请注意有关switch语句和运算符优先级的一些问题,这些问题常常会使C语言的初学者感到迷惑。11 什么是局部程序块(local block)? 局部程序块是指一对大括号()之间的一段C语言程序。一个C函数包含一对大括号,这对大括号之间的所有内容都包含
2、在一个局部程序块中。if语句和swich语句也可以包含一对大括号,每对大括号之间的代码也属于一个局部程序块。此外,你完全可以创建你自己的局部程序块,而不使用C函数或基本的C语句。你可以在局部程序块中说明一些变量,这种变量被称为局部变量,它们只能在局部程序块的开始部分说明,并且只在说明它的局部程序块中有效。如果局部变量与局部程序块以外的变量重名,则前者优先于后者。下面是一个使用局部程序块的例子:#include void main(void);void main() / * Begin local block for function main() * / int test_ var = 10;
3、 printf(Test variable before the if statement: %dn, test_var); if (test_var5) / * Begin local block for if statement * / int test_ var = 5; printf(Test variable within the if statement: %dn, test_var); / * Begin independent local block (not tied to any function or keyword) * / int test_var = 0; prin
4、tf ( Test variable within the independent local block: %dn, test_var) / * End independent local block * / printf (Test variable after the if statement: %dn, test_var);/*End local block for function main () * /上例产生如下输出结果:Test variable before the if statement: 10Test variable within the if statement:
5、5Test variable within the independent local block:0Test variable after the if statement: 10 注意,在这个例子中,每次test_var被定义时,它都要优先于前面所定义的test_var变量。此外还要注意,当if语句的局部程序块结束时,程序重新进入最初定义的test_var变量的作用范围,此时test_var的值为10。 请参见: 12可以把变量保存在局部程序块中吗?12 可以把变量保存在局部程序块中吗? 用局部程序块来保存变量是不常见的,你应该尽量避免这样做,但也有极少数的例外。例如,为了调试程序,你可能
6、要说明一个全局变量的局部实例,以便在相应的函数体内部进行测试。为了使程序的某一部分变得更易读,你也可能要使用局部程序块,例如,在接近变量被使用的地方说明一个变量有时就会使程序变得更易读。然而,编写得较好的程序通常不采用这种方式来说明变量,你应该尽量避免使用局部程序块来保存变量。 请参见: 11 什么是局部程序块?13 什么时候用一条switch语句比用多条if语句更好? 如果你有两个以上基于同一个数字(numeric)型变量的条件表达式,那么最好使用一条switch语句。例如,与其使用下述代码:if (x =l) printf (x is equal to one. n);else if (x
7、 =2) printf (x is equal to two. n);else if (x = =3) printf (x is equal to three. n);else printf (x is not equal to one, two, or three. n);不如使用下述代码,它更易于阅读和维护:switch (x) case 1: printf (x is equal to one. n); break; case 2: printf (x is equal to two. n); break case 3: printf (x is equal to three. n);
8、break; default: printf (x is not equal to one, two, or three. n); break; 注意,使用switch语句的前提是条件表达式必须基于同一个数字型变量。例如,尽管下述if语句包含两个以上的条件,但该例不能使用switch语句,因为该例基于字符串比较,而不是数字比较: char *name=Lupto; if(!stricmp(name,Isaac) printf(Your name meansLaughtern); else if(!stricmp(name,Amy) printf(Your name meansBelovedn)
9、; else if(!stricmp(name,Lloyd) printf(Your name meansMysteriousn); else printf(I havent a clue as to what your name meansn); 请参见: 14 switch语句必须包含default分支吗7 15 switch语句的最后一个分支可以不要break语句吗?1. 4 switch语句必须包含default分支吗? 不,但是为了进行错误检查或逻辑检查,还是应该在switch语句中加入default分支。例如,下述switch语句完全合法:switch (char_code) ca
10、se tyt: case y: printf ( You answered YES ! n ) break case N: case n: printf (You answered NO!n); break但是,如果一个未知字符被传递给这条switch语句,会出现什么情况呢?这时,程序将没有任何输出。因此,最好还是加入一个default分支,以处理这种情况:.default: printf (Unknown response : %dn, char_code); break. 此外,default分支能给逻辑检查带来很多方便。例如,如果用switch语句来处理数目固定的条件,而且认为这些条件之
11、外的值都属于逻辑错误,那么可以加入一个default分支来辨识逻辑错误。请看下列:void move_cursor (int direction) switch (direction) case UP: cursor_up() break case DOWN: cursor_down() break case LEFT: cursor_left () break case RIGHT: cursor_ right ( ) break default: printf (Logic error on line number %ld! n, _ LINE_ ) break 请参见: 13 什么时候用
12、一条switch语句比用多条if语句更好? 15 Switch语句的最后一个分支可以不要break语句吗?15 switch语句的最后一个分支可以不要break语句吗? 尽管switch语句的最后一个分支不一定需要break语句,但最好还是在switch语句的每个分支后面加上break语句,包括最后一个分支。这样做的主要原因是:你的程序很可能要让另一个人来维护,他可能要增加一些新的分支,但没有注意到最后一个分支没有break语句,结果使原来的最后一个分支受到其后新增分支的干扰而失效。在每个分支后面加上break语句将防止发生这种错误并增强程序的安全性。此外,目前大多数优化编译程序都会忽略最后一
13、条break语句,所以加入这条语句不会影响程序的性能。 请参见: 1. 3 什么时候用一条switch语句比用多条if语句更好? 1. 4 switch语句必须包含default分支吗?1. 6 除了在for语句中之外,在哪些情况下还要使用逗号运算符? 逗号运算符通常用来分隔变量说明、函数参数、表达式以及for语句中的元素。下例给出了使用逗号的多种方式:#include #include void main(void);void main () / * Here, the comma operator is used to separate three variable declaration
14、s. * / int i, j, k; / * Notice how you can use the comma operator to perform multiple initializations on the same line. * / i=0, j=1, k=2;printf(i= %d, j=%d, k= %dn, i, j, k); / * Here, the comma operator is used to execute three expressions in one line: assign k to i, increment j, and increment k.
15、The value that i receives is always the rigbtmost expression. * / i= ( j+, k+ ); printf(i=%d, j=%d, k=%dn, i, j, k); / * Here, the while statement uses the comma operator to assign the value of i as well as test it. * / while (i=(rand() % 100), i !=50) printf(i is %d, trying again. n, i) printf (nGu
16、ess what? i is 50!n )请注意下述语句: i:(j+,k+) 这条语句一次完成了三个动作,依次为: (1)把k值赋给i。这是因为左值(lvaule)总是等于最右边的参数,本例的左值等于k。注意,本例的左值不等于k+,因为k+是一个后缀自增表达式,在把k值赋给j之后k才会自增。如果所用的表达式是+k,则+k的值会被赋给i,因为+k是一个前缀自增表达式,k的自增发生在赋值操作之前。 (2)j自增。 (3)k自增。 此外,还要注意看上去有点奇怪的while语句:while (i=(rand() % 100), i !=50) printf(i is %d, trying again
17、. n); 这里,逗号运算符将两个表达式隔开,while语句的每次循环都将计算这两个表达式的值。逗号左边是第一个表达式,它把0至99之间的一个随机数赋给i;第二个表达式在while语句中更常见,它是一个条件表达式,用来判断i是否不等于50。while语句每一次循环都要赋予i一个新的随机数,并且检查其值是否不等于50。最后,i将被随机地赋值为50,而while语句也将结束循环。 请参见: 112 运算符的优先级总能保证是“自左至右”或“自右至左”的顺序吗? 113 +var和var+有什么区别?17 怎样才能知道循环是否提前结束了? 循环通常依赖于一个或多个变量,你可以在循环外检查这些变量,以确
18、保循环被正确执行。请看下例:int xchar * cpREQUESTED_BLOCKS/ * Attempt (in vain, I must add. )to allocate 512 10KB blocks in memory. * /for (x = 0; xREQUESTED_ BLOCKS ; x+ ) cpix= (char * ) malloc (10000,1) if (cpx= = (char * ) NULL) break/ * If x is less than REQUESTED-BLOCKS, the loop has ended prematurely. * /i
19、f (xREQUESTED_BLOCKS) printf (Bummer ! My loop ended prematurely ! n );注意,如果上述循环执行成功,它一定会循环512次。紧接着循环的if语句用来测试循环次数,从而判断循环是否提前结束。如果变量x的值小于512,就说明循环出错了。18 goto,longjmp()和setjmp()之间有什么区别? goto语句实现程序执行中的近程跳转(local jump),longjmp()和setjmp()函数实现程序执行中的远程跳转(nonlocaljump,也叫farjump)。通常你应该避免任何形式的执行中跳转,因为在程序中使用g
20、oto语句或longjmp()函数不是一种好的编程习惯。 goto语句会跳过程序中的一段代码并转到一个预先指定的位置。为了使用goto语句,你要预先指定一个有标号的位置作为跳转位置,这个位置必须与goto语句在同一个函数内。在不同的函数之间是无法实现goto跳转的。下面是一个使用goto语句的例子:void bad_programmers_function(void) int x printf(Excuse me while I count to 5000. n) ; x-l while (1) printf( %dn, x) if (x =5000) goto all_done else x
21、=x+1; all_done: prinft(Whew! That wasnt so bad, was it?n);如果不使用goto语句,是例可以编写得更好。下面就是一个改进了实现的例子:void better_function (void) int x printf(Excuse me while I count to 5000. n); for (x=1; x=5000, x+) printf( %dn, x) printf(Whew! That wasnt so bad, was it?n) ; 前面已经提到,longjmp()和setjmp()函数实现程序执行中的远程跳转。当你在程序
22、中调用setjmp()时,程序当前状态将被保存到一个jmp_buf类型的结构中。此后,你可以通过调用longjmp()函数恢复到调用setjmp()时的程序状态。与goto语句不同,longjmp()和setjmp()函数实现的跳转不一定在同一个函数内。然而,使用这两个函数有一个很大的缺陷,当程序恢复到它原来所保存的状态时,它将失去对所有在longjmp()和setjmp()之间动态分配的内存的控制,也就是说这将浪费所有在longjmp()和setjmp()之间用malloc()和calloc()分配所得的内存,从而使程序的效率大大降低。因此,你应该尽量避免使用longjmp()和setjmp
23、()函数,它们和goto语句一样,都是不良编程习惯的表现。 下面是使用longjmp()函数和setjmp()函数的一个例子:#include #include jmp_buf saved_state;void main(void);void call_ longjmp (void);void main(void) int ret_code; printf(The current state of the program is being saved. n); ret_code = setjmp (saved_state) if (ret_code =1) printf(The longjmp
24、 function has been called. n ) printf(The programs previous state has been restored. n); exit(0) printf(I am about to call longjmp andn); printf(return to the previous program state. n ) call_ longjmp ( )void call_longjmp (void) longjmp (saved_state, 1 )19 什么是左值(lvaule)? 左值是指可以被赋值的表达式。左值位于赋值语句的左侧,与其
25、相对的右值(rvaule,见 111)则位于赋值语句的右侧。每条赋值语句都必须有一个左值和一个右值。左值必须是内存中一个可存储的变量,而不能是一个常量。下面给出了一些左值的例子:int x;int *p_int;x=1; p_int=5; 变量x是一个整数,它对应于内存中的一个可存储位置,因此,在语句“x1”中,x就是一个左值。注意,在第二个赋值语句“*p_int5中,通过“*”修饰符访问p_int所指向的内存区域;因此,p_int是一个左值。相反,下面的几个例子就不是左值:#define CONST_VAL 10int x/* example 1 * /l=x;/ * example 2 *
26、 /CONST_VAL = 5;在上述两条语句中,语句的左侧都是一个常量,其值不能改变,因为常量不表示内存中可存储的位置。因此,这两条赋值语句中没有左值,编译程序会指出它们是错误的。请参见: 1. 10 数组(array)可以是左值吗? 1. 11 什么是右值(rvaule)?110 数组(array)可以是左值吗? 在19中,左值被定义为可被赋值的表达式。那么,数组是可被赋值的表达式吗?不是,因为数组是由若干独立的数组元素组成的,这些元素不能作为一个整体被赋值。下述语句是非法的: int x5,y5; x=y; 不过,你可以通过for循环来遍历数组中的每个元素,并分别对它们赋值,例如: in
27、t i; int x5; int y5; . for(i=0; i5,i+) xiyi; . 此外,你可能想一次拷贝整个数组,这可以通过象memcpy()这样的函数来实现,例如: memcpy(x,y,sizeof(y); 与数组不同,结构(structure)可以作为左值。你可以把一个结构变量赋给另一个同类型的结构变量,例如: typedef struct t_name charlast_name25; char first_name15; char middle-init 2; NAME.NAME my_name, your_name;.your_name = my_name;.在上例中,
28、结构变量my_name的全部内容被拷贝到结构变量your_name中,其作用和下述语句是相同的: memcpy(your_name,my_name,sizeof(your_name); 请参见: 19 什么是左值(lvaule)? 111 什么是右值(rvaule)?111 什么是右值(rvaule)? 在19中,左值被定义为可被赋值的表达式,你也可以认为左值是出现在赋值语句左边的表达式。这样,右值就可以被定义为能赋值的表达式,它出现在赋值语句的右边。与左值不同,右值可以是常量或表达式:例如: int X,y; x = 1; /* 1 iS an rvalue, x is an lvalue
29、*/ y(x+1); /* (x+1)is an rvalue;y is an lvalue */ 在19中已经介绍过,一条赋值语句必须有一个左值和一个右值,因此,下述语句无法通过编译,因为它缺少一个右值: int x; x=void_function_call(); /* theunction voidfunctioncall() returns nothing */ 如果上例中的函数返回一个整数,那么它可以被看作一个右值,因为它的返回值可以存储 到左值x中。 请参见: 19 什么是左值(lvaule)? 110 数组可以是左值吗? 112 运算符的优先级总能保证是“自左至右”或“自右至左”
30、的顺序吗? 对这个问题的简单回答是:这两种顺序都无法保证。C语言并不总是自左至右或自右至左求值,一般说来,它首先求函数值,其次求复杂表达式的值,最后求简单表达式的值。此外,为了进一步优化代码,目前流行的大多数C编译程序常常会改变表达式的求值顺序。因此,你应该用括号明确地指定运算符的优先级。例如,请看下述表达式: a=b+c/d/functioncall() * 5 上述表达式的求值顺序非常模糊,你很可能得不到所要的结果,因此,你最好明确地指定运算符的优先级: ab+(c/d)/functioncall()* 5) 这样,就能确保表达式被正确求值,而且编译程序不会为了优化代码而重新安排运算符的优
31、先级了。113 +var和var+有什么区别? “+”运算符被称为自增运算符。如果“+”运算符出现在变量的前面(+var),那么在表达式使用变量之前,变量的值将增加1。如果“+”运算符出现在变量之后(var+),那么先对表达式求值,然后变量的值才增加1。对自减运算符(-)来说,情况完全相同。如果运算符出现在变量的前面,则相应的运算被称为前缀运算;反之,则称为后缀运算。 例如,请看一个使用后缀自增运算符的例子: int x, y; x=1; y=(x+* 5); 上例使用了后缀自增运算符,在求得表达式的值之后,x的值才增加1,因此,y的值为1乘以5,等于5。在求得表达式的值之后,x自增为2。 现
32、在看一个使用前缀自增运算符的例子: int x, y; x=1; y=(+x*5); 这个例子和前一个相同,只不过使用了前缀自增运算符,而不是后缀自增运算符,因此,x的值先增加1,变为2,然后才求得表达式的值。这样,y的值为2乘以5,等于10。114 取模运算符(modulus operator)“”的作用是什么? 取模运算符“”的作用是求两个数相除的余数。例如,请看下面这段代码: x=15/7; 如果x是一个整数,x的值将为2。然而,如果用取模运算符代替除法运算符/,得到的结果就不同了: X=157; 这个表达式的结果为15除以7的余数,等于1。这就是说,15除以7得2余1。 取模运算符通常
33、用来判断一个数是否被另一个数整除。例如,如果你要打印字母表中序号为3的倍数的字母,你可以使用下面这段代码: int x; for(x1; x=26; x+) if(x%3)0) printf(%c; x+64); 上例将输出字符串cfilorux,即字母表中序号为3的倍数的所有字母。第2章 变量和数据存储 C语言的强大功能之一是可以灵活地定义数据的存储方式。C语言从两个方面控制变量的性质:作用域(scope)和生存期(lifetime)。作用域是指可以存取变量的代码范围,生存期是指可以存取变量的时间范围。 作用域有三种: 1. extern(外部的) 这是在函数外部定义的变量的缺省存储方式。e
34、xtern变量的作用域是整个程序。 2static(静态的) 在函数外部说明为static的变量的作用域为从定义点到该文件尾部;在函数内部说明为static的变量的作用域为从定义点到该局部程序块尾部。 3auto(自动的) 这是在函数内部说明的变量的缺省存储方式。auto变量的作用域为从定义点到该局部程序块尾部。 变量的生存期也有三种,但它们不象作用域那样有预定义的关键字名称。第一种是extern和static变量的生存期,它从main()函数被调用之前开始,到程序退出时为止。第二种是函数参数和auto变量的生存期,它从函数调用时开始,到函数返回时为止。第三种是动态分配的数据的生存期,它从程序
35、调用malloc()或calloc()为数据分配存储空间时开始,到程序调用free()或程序退出时为止。21 变量存储在内存(memory)中的什么地方? 变量可以存储在内存中的不同地方,这依赖于它们的生存期。在函数外部定义的变量(全局变量或静态外部变量)和在函数内部定义的static变量,其生存期就是程序运行的全过程,这些变量被存储在数据段(datasegment)中。数据段是在内存中为这些变量留出的一段大小固定的空间,它分为两部分,一部分用来存放初始化变量,另一部分用来存放未初始化变量。 在函数内部定义的auto变量(没有用关键字static定义的变量)的生存期从程序开始执行其所在的程序块
36、代码时开始,到程序离开该程序块时为止。作为函数参数的变量只在调用该函数期间存在。这些变量被存储在栈(stack)中。栈是内存中的一段空间,开始很小,以后逐渐自动增大,直到达到某个预定义的界限。在象DOS这样的没有虚拟内存(virtual memory)的系统中,这个界限由系统决定,并且通常非常大,因此程序员不必担心用尽栈空间。关于虚拟内存 的讨论,请参见23。 第三种(也是最后一种)内存空间实际上并不存储变量,但是可以用来存储变量所指向的数据。如果把调用malloc()函数的结果赋给一个指针变量,那么这个指针变量将包含一块动态分配的内存的地址,这块内存位于一段名为“堆(heap)”的内存空间中
37、。堆开始时也很小,但当程序员调用malloc()或calloc()等内存分配函数时它就会增大。堆可以和数据段或栈共用一个内存段(memorysegment),也可以有它自己的内存段,这完全取决于编译选项和操作系统。 与栈相似,堆也有一个增长界限,并且决定这个界限的规则与栈相同。 请参见: 11 什么是局部程序块(10calblock)? 22 变量必须初始化吗? 23 什么是页抖动(pagethrashing)? 720 什么是栈(stack)? 721 什么是堆(heap)7 22 变量必须初始化吗? 不。使用变量之前应该给变量一个值,一个好的编译程序将帮助你发现那些还没有被给定一个值就被使
38、用的变量。不过,变量不一定需要初始化。在函数外部定义的变量或者在函数内部用static关键字定义的变量(被定义在数据段中的那些变量,见21)在没有明确地被程序初始化之前都已被系统初始化为0了。在函数内部或程序块内部定义的不带static关键字的变量都是自动变量,如果你没有明确地初始化这些变量,它们就会具有未定义值。如果你没有初始化一个自动变量,在使用它之前你就必须保证先给它赋值。 调用malloc()函数从堆中分配到的空间也包含未定义的数据,因此在使用它之前必须先进行初始化,但调用calloc()函数分配到的空间在分配时就已经被初始化为0了。 请参见: 11 什么是局部程序块(10calblo
39、ck)? 720 什么是栈(stack)? 721 什么是堆(heap)?23 什么是页抖动(pagethrashing)? 有些操作系统(如UNIX和增强模式下的Windows)使用虚拟内存,这是一种使机器的作业地址空间大于实际内存的技术,它是通过用磁盘空间模拟RAM(randomaccess memory)来实现的。 在80386和更高级的Intel CPU芯片中,在现有的大多数其它微处理器(如Motorola 68030,sparc和Power PC)中,都有一个被称为内存管理单元(Memory Management Unit,缩写为MMU)的器件。MMU把内存看作是由一系列“页(pag
40、e)”组成的来处理。一页内存是指一个具有一定大小的连续的内存块,通常为4096或8192字节。操作系统为每个正在运行的程序建立并维护一张被称为进程内存映射(Process Memory Map,缩与为PMM)的表,表中记录了程序可以存取的所有内存页以及它们的实际位置。 每当程序存取一块内存时,它会把相应的地址(虚拟地址,virtualaddress)传送给MMU,MMU会在PMM中查找这块内存的实际位置(物理地址,physical address),物理地址可以是由操作系统指定的在内存中或磁盘上的任何位置。如果程序要存取的位置在磁盘上,就必须把包含该地址的页从磁盘上读到内存中,并且必须更新PM
41、M以反映这个变化(这被称为pagefault,即页错)。 希望你继续读下去,因为下面就要介绍其中的难点了。存取磁盘比存取RAM要慢得多,所以操作系统会试图在RAM中保持尽量多的虚拟内存。如果你在运行一个非常大的程序(或者同时运行几个小程序),那么可能没有足够的RAM来承担程序要使用的全部内存,因此必须把一些页从RAM中移到磁盘上(这被为pagingout,即页出)。 操作系统会试图去判断哪些页可能暂时不会被使用(通常基于过去使用内存的情况),如果它判断错了,或者程序正在很多地方存取很多内存,那么为了读入已调出的页,就会产生大量页错动作。因为RAM已被全部使用,所以为了调入要存取的一页,必须调出另一页,而这将导致更多的页错动作,因为此时不同的一页已被移到磁盘上。在短时间内出现大量页错动作的情形被称为页抖动,它将大大降低系统的执行效率。 频繁存取内存中大量散布的位置的程序更容易在系统中