第二章 预处理、const、static与sizeof (上)
在这章编写时,发现了一个不错的帖子。其中对程序预处理语句的基本概念写得还是十分清晰的。
(http://www.runoob.com/cprogramming/c-preprocessors.html)
一、预处理的使用:
考察#ifdef、#else、#endif在程序中的使用
1.程序:
1 #include<stdio.h> 2 #include<stdlib.h> 3 4 #define DEBUG 5 6 int main() 7 { 8 int i=0; 9 char c; 10 11 while(1) 12 { 13 i++; 14 c=getchar(); 15 if(c!=‘\n‘) 16 { 17 getchar(); 18 } 19 if(c==‘q‘||c==‘Q‘) 20 { 21 #ifdef DEBUG 22 printf("we got:%c,about to exit.\n",c); 23 #endif 24 break; 25 } 26 else 27 { 28 printf("i=%d",i); 29 #ifdef DEBUG 30 printf(",we got:%c",c); 31 #endif 32 printf("\n"); 33 } 34 } 35 36 return 0; 37 }
2.答案:
输入:A 输出:i=1,we got:A 输入:a 输出:i=2,we got:a 输入:q 输出:we got:q,about to exit.
3.分析:
代码行.4中定义了一个名为DEBUG的预处理器常量。
代码行.21-23、29-31中通过使用#ifdef/#endif语句来判断#ifdef后的DEBUG预处理器变量是否被定义过了,如果定义了,就执行#ifdef与#endif中间的执行语句(printf语句)。
由于我们已经在代码行.4中定义了DEBUG这个预处理器常量,故两个语句都是运行其中的执行语句的。
那么在这个程序中,实际传给main的代码是:
1 int main() 2 { 3 int i=0; 4 char c; 5 6 while(1) 7 { 8 i++; 9 c=getchar(); 10 if(c!=‘\n‘) 11 { 12 getchar(); 13 } 14 if(c==‘q‘||c==‘Q‘) 15 { 16 printf("we got:%c,about to exit.\n",c); 17 break; 18 } 19 else 20 { 21 printf("i=%d",i); 22 printf(",we got:%c",c); 23 printf("\n"); 24 } 25 } 26 27 return 0; 28 }
当然,如果在程序开头并没有定义DEBUG或者注释了DEBUG,那么那两个#ifdef/#endif中的执行语句就不会执行了。
4.小结:
上述代码中并没有提到#else,其实这和正常条件语句一样的。如果#ifdef判断为假,即执行#else后的语句。
简单地翻阅了一下资料,这个可以很好的用来调试程序。想想,确实是有一定作用的。
二、用#define实现宏并求最大值和最小值:
1.程序:
1 #define MAX(X,Y) (((X)>(Y))?(X):(Y)) 2 #define MIN(X,Y) (((X)<(Y))?(X):(Y))
2.答案:
(实现最大值、最小值的输出)
3.分析:
1)#define在宏上应用的基本知识,说白了就是宏替换。
2)三目运算符(?:)的知识点。这个运算符可以产生比if-else更加优化的代码,而且书写也更为简洁。当然如果不熟练,还是练练吧。
3)重点:在宏中需要将参数用小括号括起来(因为小括号的运算优先级最高)。因为宏说白了就是宏替换,也就是简单的文本替换,如果不注意,很容易出错。例如:
1 #define SQR(X) (X*X)
当执行SQR(b+2),就会无法出现我们想要的结果。所以应该改为:
1 #define SQR(X) ((X)*(X))
4.小结:
宏定义展开实在预处理时期,也即是编译之前,在那时编译器并不知道main等函数内的应用,所以它只能简单的文本替换。
三、宏参数的连接:
1.程序:
1 #include<stdio.h> 2 3 #define STR(s) #s 4 #define CONS(a,b) (int)a##e##b) 5 6 int main() 7 { 8 printf(STR(vck)); 9 printf("\n"); 10 printf("%d\n",CONS(2,3)); 11 12 return 0; 13 }
2.答案:
vck 2000
3.分析:
程序中,利用#吧宏参数变为一个字符串,通过##把两个宏参数贴合在一起。
代码行.3中通过s导入宏参数,再通过#s返回结果。
代码行.4中通过a##e##b显式转化为int类型的结果。
代码行.8中通过STR(vck)将vck导入的代码行.3中的STR(s)中的s。然后将宏参数s传入到#s,(不过#只是一个连接符的存在)。所以返回s(=‘vck’)的值。故代码行.8打印输出vck。
代码行.10操作类似上述,不过应当注意2e3表示的是2乘上10的三次方。故结果为2000。
四、用宏定义得到一个字的高位和低位字节:
1.程序:
1 #define WORD_LO ((byte) ((word)(xxx)&255)) 2 #define WORD_HI ((byte) ((word)(xxx)>>8))
2.答案:
(前者可以获取一个字的低位字节,后者可以获取一个字的高位字节)
3.分析:
首先这个程序要从两个角度来说。
首先是其中的数据类型转换。其中的byte和word只有在MFC/SDK中才能使用。另外在VC下这两个数据类型时BYTE和WORD,并且有已经定义的宏LOWORD()和HIWORD()。那么如果编译器无法识别的话,就可以加上一下代码:
#define byte unsigned char #define word unsigned short
其次是要理解两者实现高位、低位的位运算方式。前者通过位与运算(255其实就是2的八次方减1,即255在二进制下为八个1),位与运算结果位数(二进制)和位数最少的运算数相等(即八位)。八个1和字的后八位做位与计算还是字的后八位(即所需要的低八位)。后者通过位运算左移八位实现。一个字为16位(二进制),左移8位后,就只剩下前八位了(即所需要的字的高八位)。
4.小结:
其实在对C/C++的一步步学习,发现许多运算都可以通过位运算来简化、优化。
在查询资料时,发现了一份不错的资料,有着许多优美代码,值得学习一番。
(http://www.360doc.com/content/16/0322/12/478627_544288615.shtml)
五、用宏定义得到一个数组所含的元素个数:
考察宏定义和sizeof的使用
1.程序:
1 #define ARR_SIZE(a) (sizeof((a))/sizeof((a[0])))
2.答案:
#define ARR_SIZE(a) (sizeof((a))/sizeof((a[0])))
3.分析:
假定一个数组定义如下:
int array[100];
这个数组含有100个int类型的元素。一个int为4个字节,那么这个数组总共有400个字节。那么sizeof(array)为数组总大小,即400个字节;sizeof(array[0])为其中一个元素的大小小(int类型数据大小),即4个字节大小。两者大小相除得到的就是数组的元素个数,即100。另外为了确保宏定义不出现“二义性”,在a与a[0]上都加上了括号。
4.小结:
sizeof是C/C++的一个操作符,其作用就是返回一个对象或者数据类型所占用的内存字节数。
过去在C语言中,为了获取数组元素个数,我们是通过strlen来获取。
在查询资料时,我还查询到了两者的区别:
(参考资料:https://zhidao.baidu.com/question/12033577.html)
(后面有一节谈到这个问题)
六、const的使用:
1.程序:
1 #include<stdio.h> 2 3 int main() 4 { 5 const int x=1; 6 int b=10; 7 int c=20; 8 9 const int* a1=&b; 10 int* const a2=&b; 11 const int* const a3=&b; 12 13 x=2; 14 15 a1=&c; 16 *a1=1; 17 18 a2=&c; 19 a2=1; 20 21 a3=&c; 22 *a3=1; 23 24 return 0; 25 }
2.答案:
代码行.13、16、18、21和22会出现变异错误。
3.分析:
代码行.13中,由于在代码行.5中已经通过CONST将变量x定义为只读变量,所以无法通过赋值语句再次修改x的值,故报错。报错时,编译器会提示“l-value specifies const object”,即编译器认为等号左边是常量对象(常量当然是只读的)。有资料表示“如果在代码行.5中没有给x初始化,那么x就是一个随机数(),并且之后也无法赋值”。然后也有资料表示会编译错误。我试了一下,结果是编译错误,提示“const object must be initialized if not extern”(VC++6.0版本)
代码行.15、16中,由于在代码行.9中已经将a1定义为const int*类型,即常量指针类型。常量指针类型中的CONST在*左侧,CONST用来修饰指针所指向的变量,即指针指向为常量。在代码行.15中把a1指向变量c是可以的,因为这个操作修改的是指针a1本身。但是代码行.16中改变a1指向的内容是不可以的。编译器会报错,并提示“l-value specifies const object”。
代码行.18、19中,由于在代码行.10中已经将a2定义为int* const类型,即指针常量类型。指针常量类型中的CONST在*右侧,CONST用来修饰指针本身,即指针本身为常量。在代码行.18中修改指针a2本身是不允许的。而代码行.19修改a2指向的内容是可以的。
代码行.21、22中,由于在代码行.11中已经将a3定义为const int* const类型,(这个。。。姑且成为常量指针常量吧)。指针常量中的CONST在*左右两侧都有,CONST分别修饰指针本身与其所指向的变量,所以代码行.21、22中的两个修改都不可以(实际可以参考前面两个CONST类型)。故代码行.21、22报错。
PS:变量x、a2、a3在声明的同时就必须初始化,因为它们在之后都不可以被赋值了。而变量a1可以在声明的时候不初始化。
4.小结:
在查询资料时,发现一个知识点。其实CONST所修饰的并不是常量,而是只读变量。而常量是由#define与enum类型所定义的。其中的区别可以通过下列代码看出:
const int n = 5; int a[n];
这样的代码是会报错的。因为ANSI C规定数组定义长度时必须采用常量。虽然CONST定义的只读变量n和常量使用上没多少区别,但依旧不是常量,所以会报错。
七、const与#define的特点与区别:
1.程序:
1 #define PI 3.1415926 2 float angel; 3 angel=30*PI/180;
2.分析与答案:
当程序进行编译时,编译器会先将“#define PI 3.1415926”之后的所有代码中的“PI”全部替换成“3.1415926”(不考虑endif),然后进行编译。因此#define常量是一个Compile-Time概念,它的生命周期止于编译器,它存在于程序的代码段,在实际程序中它只是一个常数、一个命令中的参数,并没有实际的存在。
const常量存在于程序的数据段,并在堆栈分配了空间。const变量是一个Run-Time概念,它在程序中切实地存在并可以被调用、传递。const常量有数据类型,而宏常量(#define)没有数据类型。编译器可以对const常量进行类型安全检查。
八、C++中const的作用(至少三点):
1.分析与答案:
1)const用于定义常量:const定义的常量编译器可以对其进行数据静态类型安全检查。
2)const修饰函数形式参数:当输入参数为用户自定义类型和抽象数据类型时,应该将“值传递”改为“const&传递”,可以提高效率。比较下列两行代码:
1 void fun(A,a); 2 void fun(A,const &a);
前者效率低下。函数体内产生A类型的临时对象用于复制参数a,临时对象的构造、复制、析构过程都将消耗时间。而后者提高了效率。用“引用传递”不需要产生临时对象,节省了临时对象的构造、复制、析构过程消耗的时间。不过只用引用有可能改变a,所以添加const。
3)const修饰函数的返回值:如给“指针传递”的函数返回值加const,则返回值不能被直接修改,且该返回值只能被赋值给加const修饰的同类型指针。如:
1 const char *GetChar(void){}; 2 char *ch=GetChar(); //error 3 const char *ch=GetChar(); //correct
4)const修饰类的成员函数(函数定义体):任何不会修改数据成员的函数都应用const修饰,这样在无意修改了数据成员或调用了非const成员函数时,编译器都会报错。const修饰类的成员函数形式为:
1 int GetCount(void) const;
2.小结:
const相对#define而言,一方面,它可以保护被修饰的东西,防止意外修改,增强程序的健壮性;另一方面,编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
(参考资料:http://blog.csdn.net/xingjiarong/article/details/47282255)
九、static的作用(至少两点):
1.分析与答案:
1)在函数体中,一个被声明为静态的变量在这一函数被调用的过程中会维持其值不变。
为了清晰了解这个概念,我查询了不少资料。通常在一个函数体内定义了一个变量,当程序运行到这个语句时都会给该局部变量分配栈内存。不过当程序退出函数体时,系统会收回栈内存,从而使得这个局部变量失效。不过有时候,我们需要在两次调用间确保该变量的值不变。如果建立一个全局变量的话,这个变量便不只属于这个函数体了,容易被其他函数体控制、修改。而恰好静态局部变量可以很好地解决这个问题,一方面静态局部变量内存归于内存的全局区(静态区),可以长久保存其值(声明周期与程序相同);另一方面,静态局部变量作为一个局部变量不会被其他函数体所控制、修改。
(参考数据:http://bbs.csdn.net/topics/360149683
http://baike.baidu.com/link?url=TR4uCVICn2uaoMcj1x0eOb4jgHKTN6ODKMbYgre4d9D-Tr6X2IPhS7ptTkAem019XQz1ShBJfOR9_lCCqiwKya)
2)在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所有函数访问,但不能被模块外其他函数访问。它是一个本地的全局变量。
在设计一个程序时,往往将一个程序分成若干个程序模块,一个模块包含一个或多个函数。而一个模块常常被放在一个.c文件中。
3)在模块内,一个被声明为静态的函数只可被这一模块内的其他函数调用。那就是这个函数被限制在声明它的模块的本地范围内使用。
静态函数的好处有两个。一方面,静态静态函数的内存地址会被分配在一个固定使用的存储区,直到程序退出,这就避免了调用函数时压栈出栈,速度会快很多。另一方面,修饰符static将函数的作用域限制在本文件内。使用内部函数的好处就是不用担心自己定义的函数是否与其他文件内的函数同名,因为同名了也不会出错。
2.小结:
为了更好的解释上面的分析,查询了一些资料,来便于说明。
全局变量、静态局部变量保存在全局数据区,初始化的和未初始化的分别保存在一起;普通局部变量保存在堆栈中(通常局部变量是存储在栈中的)。
一个由c/C++编译的程序占用的内存分为以下几个部分 :
1)栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2)堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3)全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域(RW), 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(ZI)。 - 程序结束后有系统释放
4)文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放 (RO)
5)程序代码区—存放函数体的二进制代码。 (RO)
(参考资料:http://blog.csdn.net/jiucongtian/article/details/7940874)
在查询过程中,我还发现了一个小总结也不错,贡献一下:
1)把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。
把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围。
2)默认值 0(static修饰的变量如果没有在定义时初始化,那么默认值为0)。
(其它后面有详细说明。)
(参考资料:http://blog.csdn.net/junboyboy/article/details/17921763)
十、static全局变量与普通全局变量的区别极其延伸:
1,分析:
全局变量的说明前再添加static就构成了静态的全局变量。全局变量本身就是静态存储方式,而静态全局变量当然也是静态存储方式(前一个题目的小结中就说了是内存中的全局区)。所以这两者在存储方式上没有区别。两者的区别在于,非静态全局变量(未添加static)的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的;而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其他源文件不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件引起错误。
故得出了前一个题目小结中的一个结论:
1)把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。
2)把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围。
(static函数的分析还需要查询资料。
参考资料:http://bbs.csdn.net/topics/200036237)
2.答案:
1)static全局变量与普通全局变量的区别:static全局变量只被初始化一次,防止在其他文件单元中被引用。
2)static局部变量与普通局部变量的区别:static局不变量只被初始化一次,下一次依据上一次的结果值。
3)static函数与普通函数的区别:static函数在内存中只有一份,普通函数在每个被调用中维持一份复制品。
(资料参考地址:
http://blog.csdn.net/jiucongtian/article/details/7940874
http://blog.csdn.net/xingjiarong/article/details/47282255
http://blog.csdn.net/lynnlycs/article/details/8688692
)