C/C++知识点清单02-上

第二章  预处理、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

时间: 2024-10-15 10:54:26

C/C++知识点清单02-上的相关文章

VPN相关知识点及ASA上VPN的配置整理

VPN只是IPSec的一种应用方式,IPSec其实是IPSecurity的简称,它的目的是为IP提供高安全性特性,VPN则是在实现这种安全特性的方式下产生的解决方案 IPSEC VPN 预先协商加密协议.散列函数.封装协议.封装模式和秘钥有效期等内容.具体执行协商任务的协议叫做互联网秘钥交换协议IKE.协商完成后的结果就叫做安全关联SA(IKE SA和IPSEC SA) IKE建立了安全关联(SA) IPSec 协议不是一个单独的协议,它给出了应用于IP层上网络数据安全的一整套体系结构,包括网络

ACM知识点清单

本文直接来源http://blog.csdn.net/xuanandting/article/details/52160859,如有侵权,请联系删除. 训练过ACM等程序设计竞赛的人在算法上有较大的优势,这就说明当你编程能力提高之后,主要时间是花在思考算法上,不是花在写程序与debug上. 下面给个计划你练练: 第一阶段:练经典常用算法,下面的每个算法给我打上十到二十遍,同时自己精简代码,因为太常用,所以要练到写时不用想,10-15分钟内打完,甚至关掉显示器都可以把程序打出来. 1.最短路(Fl

前端知识点整理02

53.new操作符具体干了什么呢? 1.创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型. 2.属性和方法被加入到 this 引用的对象中. 3.新创建的对象由 this 所引用,并且最后隐式的返回 this . var obj  = {}; obj.__proto__ = Base.prototype; Base.call(obj); 54.js延迟加载的方式有哪些? defer和async.动态创建DOM方式(创建script,插入到DOM中,加载完毕后callBac

面试前的准备---C#知识点回顾----02

经过昨天大量的简历投递,今天陆续收到面试邀约,明日准备大战一场,是死是活一试便知 1.数据库的范式 这算入门问题了吧,但凡是个数据库类的,都得问吧, 但我们在回答的时候开始背书啦 第一范式(1NF)无重复的列 第二范式(2NF)属性完全依赖于主键 [ 消除部分子函数依赖 ] 第三范式(3NF)属性不依赖于其它非主属性 [ 消除传递依赖 ] 还有什么BCNF,是不是看了好像十分清楚,但不看又好像懵懵懂懂的. 说个我的记忆方式吧 第一范式,1NF,无重复列,字段不冗余且必不可少.这必须得记住 第二范

资源的下载与上传——02上传

学习笔记适合新手,如有错误请指正.?号处也请各位指点下,谢谢. 上传 从服务器下载资源时,我们通常需要告诉服务器设备信息.用户信息等以便下载对应资源 参数或者文件可以通过UnityEngine.WWWForm类作为WWW的参数上传 设备类型可以通过APPlication.platform得到,例如:RuntimePlatform.IphonePlayer或者RuntimePlatform.Android 设备内存大小尅通过SystemInfo.systemMemorySize得到,例如:服务器通

【数据结构第三周】树知识点整理(上)

1.树的定义 树(Tree):n(n>=0)个结点构成的有限集合 子树是不相交的. 除了根节点外,每个结点有且仅有一个父结点. 一棵N个结点的树有N-1条边. 2.树的一些基本术语 (1)结点的度(Degree):结点的子树个数 (2)树的度:树的所有结点中最大的度数 (3)叶结点(Leaf):度为0的结点 (4)父结点(Parent):有子树的结点是其子树的根结点的父结点 (5)子节点(Child):若A结点是B结点的父结点,则称B结点是A结点的子结点 (6)兄弟结点(Sibling):具有同

python知识点总结02(不定时更新)

请用至少两种方式实现m与n值交换m=10,n=5 # 方式一 temp = 0 m = 10 n = 5 print(f'方式一交换前,m:{},n:{}') temp = m m = n n = temp print(f'方式一交换后,m:{},n:{}') # 方式二 m = 10 n = 5 m, n = n, m print(f'方式二交换前,m:{},n:{}') print(f'方式二交换后,m:{},n:{}') 你所知道的能够实现单例模式的方式有哪些,尝试着手写几个 ''' 单例

Android项目实战手机安全卫士(02)

目录 项目结构图 源代码 运行结果 项目源代码 项目结构图 源代码 清单 01.  SplashActivity.java package com.coderdream.mobilesafe.activity; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.ne

form表单上传文件使用multipart请求处理

在开发Web应用程序时比较常见的功能之一,就是允许用户利用multipart请求将本地文件上传到服务器,而这正是Grails的坚固基石——spring MVC其中的一个优势.Spring通过对Servlet API的HttpServletRequest接口进行扩展,使其能够很好地处理文件上传.扩展后的接口名为org.springframework.web.multipart.MultipartHttpServletRequest,其内容如清单7-31所示. 清单7-31  org.springf