最近抓住一些空闲时间,做一做C++的面试题目。买了一本人民邮电出版社的《C和C++程序员面试秘笈》,作者是董山海。
现在学完了前三章的内容,分别是【C/C++程序基础】、【预处理、const、static和sizeof】、【引用和指针】三部分。
由于是零零散散学习的,学过的东西总是忘记,因此决定每隔一段时间,复习一下学过的东西,做一个总结。
总的来说,这本书在目前我学到95页为止,已经发现了一屁股的错误。有的是印刷问题,有的是作者自己也没讲对。放一个网友总结的勘误表吧。我的文章中也会有提及。
不过这本书确实让我学习和巩固了一些C++知识,感觉比直接捧着C++ primer进步快一些。C++ primer我感觉像字典,查询可以,我读了几次,大概也读完一半了,结果发现记不住,脑子里只留了个大概,实在有点难受。
不过总的感觉是,学这本书,(虽然名字叫面试秘笈,听起来好像是临时抱佛脚糊弄面试官的)但说实话,对自己的C++水平还是有非常大的帮助的。不像《剑指offer》,个人感觉好像除了能拿来面试以外,似乎也没有其他什么太大的用处……
1.C++的域操作符“::”可以操作全局变量。比如main函数中的value是0,main函数以外的value是1。那么在main函数里可以用
::value=1;
来给全局变量的那个value赋值。(C++可以但是C不行)
2.printf("%d",-i++)表示的是打印-i,然后i再++。
3.前缀式“++i”返回对象的引用,后缀式“i++”返回对象的值。后者在大对象的时候产生较大的复制开销,效率降低。如果纯粹是内建数据则二者没有什么区别。
4.True的值究竟是什么并没有统一的标准。因此最好不要拿bool类型的变量flag直接与true比较,编程风格良好的代码应该是
if(flag)
而不是if(flag==true)
5.无符号数转成符号数,如果是负数的话,会变成很大的数。(负数用补码表示,是原码按位取反+1,第一位是符号位是1,变成无符号数的话会很大。这个具体还是要现场推算)
6.不用中间变量交换两数字,两种方式:
第一是用加减法:a=a+b;b=a-b;a=a-b;(有可能会发生数据溢出,但是交换仍然成功)
第二是用位运算:a^=b;b^=a;a^=b;
7.在标准头文件结构中:
#ifndef #define #endif作用是防止头文件被重复引用;
#ifdef __cplusplus //表示当前使用的是C++编译器
#ifdef __STDC__ //表示当前使用的是C编译器
extern "C"有两重含义:1.声明的函数和变量可以在其他模块中使用,之后的cpp文件包含这个头文件就行了。编译阶段没找到声明也不会报错而是在链接阶段从头文件中找到。2.修饰的目标是C的,意味着变量和函数是按照C语言方式编译的:C++支持函数重载(通过改变函数名把形参的类型加进去),C不支持函数重载。
8.可以通过声明atexit函数,让C++中main函数执行完以后还执行其他语句。(不过只有在正常终止后才执行,在linux系统下ctrl+c终止程序不会)
函数原型是:
int atexit(void(*)(void));
形参是一个函数指针。因此在main函数中,可以atexit(fn);这个过程叫注册。先注册的后调用,后注册的先调用。(fn是你希望在main函数结束后执行的函数的函数名,需要在main函数以外声明void fn (void);之后再定义函数内容,在main函数中仅注册即可)
1.使用宏定义的时候,注意要把每个地方的括号都带起来,以免直接替换的时候出现问题。
2.在#define的时候,之后跟着一个#,意味把宏参数变成字符串;两个#,意为把参数拼起来:
#define STR(s) #s
#define cons(a,b) (int)(a##e##b)
第一行的定义,在程序中使用,可以把s转换为字符串;第二行则是拼成一个aeb的数字(例如2e3)转化为int型。
3.关于const和*号关系还有const的引用,这个单独总结到了一个文章中:C++中const完全详解
4.#define只是用来做文本替换的,生命周期止于编译期。const在程序中确确实实存在并可以被调用和传递。
5.const的作用:1)定义常量。2)修饰函数形参,const&传递形参,提高效率的同时保证原始数据不被改变。3)修饰函数返回值,使其只能被赋值给常量类型。4)修饰类的成员函数,使该函数不能调用非const函数并且不能修改数据成员。(另外,一个const对象可以调用const成员函数而不能调用非const函数)。修饰的形式为:int getcount() const;(const放在后面,因为放在前面的话成了返回值是const int)
6.static作用:1)在函数内部,被声明为静态的变量在这一函数被调用的过程中维持其值不变。2)在函数外,一个变量或者函数被声明为static,那么同一项目的其他cpp文件不能通过extern声明来调用这个变量或者函数。(即只在模块内有效)
7.关于全局变量与静态全局变量,我自己的补充:
1)首先,不在函数内的变量就是全局变量。
2)建立一个工程,其中有两个cpp文件,因为加入了同一个工程,因此二者不需要互相#include进来。例如一个a.cpp,一个main.cpp。a.cpp中定义int a=9,则在main.cpp中声明extern int a;即可使用在a.cpp中定义的a=9。不过如果a.cpp中定义的是static int a=9; 则main.cpp中不可以声明extern int a,因为static限制了作用域,所以会报错。
3)有.h头文件的情况下:头文件中声明extern int value 不用初始化值,那么任何一个包含该头文件的cpp中对value初始化一次即可:int value=0; 然后所有包含该头文件的cpp文件都可以用value访问同一个变量。这样的extern声明的才是全局性的。但是。如果头文件中是static int value=1;那么其他的cpp文件中可以各自在函数中修改这个value值,互不影响。
8.在32位操作系统下,char一个字节,short是两个字节,int和指针都是4个字节,double是8个字节。对数组变量做sizeof得到的是数组占用内存的总大小,但如果数组变量是通过函数形参从外部传入的,在函数内部对数组名做sizeof得到的是4个字节,因为是对指针做了操作。
9.对结构体或者类做sizeof的时候,有六个陷阱点:
1)为了存取效率,采用字节对齐,因此sizeof得到的内存是其中最大的基本数据元素的整数倍(注意是“基本”,char[9]还是由char构成),而不是数据元素的内存直接相加。
2)普通函数不占内存,而只要有虚函数,不管有几个虚函数总共只占用一个指针大小的内存,因此在继承问题上,子类和父类都有虚成员函数,子类的算sizeof也是只多算一个指针大小。
3)对于空类,编译器会安排一个char类型填充,因此占用1个字节。不过空类A继承空类FatherOfA,空类A的大小还是一个字节不是两个字节。
4)虚继承时(虚继承的作用是确保多重继承时,顶端父类的副本只有一个),会多安排一个指向父类的指针。有几个虚继承就会多安排几个指针。
5)静态static变量存储在静态存储区,sizeof不计算静态变量的大小。
6)函数不占空间。就算函数内部定义了变量也不占空间。
10.sizeof是操作符(因此sizeof之后如果是变量的名字可以不加括号),strlen是函数。对于字符串来说,前者sizeof计算字符串分配的内存大小或者字符串指针的大小(函数参数的形式传入或者是char*的名字不是char[ ]的名字),而后者strlen只能计算字符串的长度(不包括结束符\0!)
11.sizeof用途:查看某个类型的对象在内存中占用的单元字节;动态分配对象时让系统知道要分配多少内存;在涉及操作数字节大小的时候用sizeof代替常量计算以避免由于系统位数不同造成的影响。
12.联合体union的大小:联合体的特点是共用相同的存储空间,因此大小必须被最大的基本数据类型整除。char[13]的大小为14,int为4,那么二者构成的联合体的大小是16,因为最大的是4,(char[13]是由char组成,char的大小是1)
13. 可以通过#pragma pack(k)来修改对齐方式为k,以k为基本单位来实现对齐。
14.内联函数inline,直接替换代码,节省调用开销,提高执行效率,但是会使总代码量变大消耗内存空间,因此函数体内代码较长或者有循环的时候不适合使用内联函数。
15.inline在编译时展开进行语法分析,宏在预编译阶段展开进行代码文本替换。inline像宏定义,但可以额外检测参数是否合法并且兼容C++对象中的私有成员(宏定义做不到)。
15. 类中成员函数的定义体放在类声明之中会自动转成inline函数。
1. 引用在声明的同时必须进行初始化,并且之后不可以修改成别的变量的引用。
2.一个指针声明以后,之后要么置空,要么指向一个变量。绝对不允许直接赋值写入数据,对野指针赋值会使系统崩溃因为不知道刚开始声明的时候指向哪里。
3. 函数形参是char*的时候,如果想在函数内部交换两个字符串,实际在函数内部修改不会影响到实际传入函数的char *实参。
应对方式有两种,第一是形参改为char *&,传参时正常传入字符串名字。第二种是改为char **,传参时取&传入字符串名字的地址。(只针对内部对两个形参char*交换的情况。如果内部直接通过指针修改字符串,是可以修改并且影响到外面的)
4. 函数在声明的时候,可以不写形参的名字,只写形参的类型。
5.函数内部对全局变量进行修改,然后函数返回这个全局变量。函数的返回值如果本身设置为引用&,那么可以返回给引用。例如int & func()的返回值是int引用,那么在main函数中定义了int &a=func()可以。但是如果函数的返回值不是&类型的,比如就是int func()类型的,那么int &a=func()不行。虽然看起来返回的是全局变量,实际上是建立了一个临时变量,把全局变量的值赋给临时变量以后才返回。
6.传引用比传指针更安全是因为不存在空引用,不会产生像野指针那样的问题。
7.复杂指针的声明:这部分看起来很吓人很难,但是实际不难。方法是先找到未定义的标识符,然后右左原则,右一下再左一下,见到半个括号就反向从右变成左或者从左变成右。基本单位按照括号来。例如:
int(*a)[10];//指针a,指向10个数的数组,数组中每个元素是int
int (*a)(int);//指针a,指向函数,函数形参是int,返回值是int
int(*a[10])(int);//找到a,右左原则,a是个数组,数组中每个元素是指针,指向函数,函数形参int返回int
右左原则非常好用:
int (*func)(int *p);
//定义func是一个指针,指向函数,函数的形参是int*,返回值是int
int (*func)(int *p,int(*f)(int*));
//func是指针,指向函数A,函数A的形参有两个,第一个是int*,第二个是函数指针(指向的那个函数B的形参是int*返回值是int),然后A的返回值是int
int (*func[5])(int *p);
//func是数组,数组中5个元素,每个元素是指针,指向函数,函数形参int*,返回值int
int (*(*func)[5])(int *p);
//func是指针,指向数组,数组中有5个元素,每个元素是指针,每个指针指向的是函数,函数形参是int*,返回值是int
int (*(*func)(int *p))[5];
//func是指针,指向函数,函数的形参是int*,函数的返回值是一个指针,指针指向了一个数组,数组中有5个元素,每个元素是int类型
8.数组名字可以看做是数组首地址,对数组名字取&得到的是数组对象的首地址。因此,数组a有5个元素,即a[0]到a[4],如果是a+1,得到a[1];如果是&a再加1,得到的是a[5]。
9.对于char str[ ]和const char str[ ]来说,赋初值是保存在栈中,因此变量名不同,地址就不同。而对于char str*或者const char str*来说,赋初值是放在数据区,不管变量名字是啥,有没有const,统统指向同一块数据区的内存。因此也不能对其内容以数组的形式修改。例如const char str*="abcdef",str[2]='a'会报错。(注意:不能修改不代表不能用中括号取值)
10.一个指针在初始化时指向0或者NULL,然后不可用*来解引用。(为NULL指针分配的分区的地址空间禁止进入)另外动态申请的指针p在free或者delete以后,应当指向NULL。因为free只是释放了原本申请的空间,指针还在,并且指向垃圾内存。
11.类的非静态成员函数才有this指针。友元函数和static成员函数没有this指针。
12. 定义一个数组指针 int*b=new int [10],那么释放空间的时候一定要delete [ ] b;来释放。如果只delete b,则只释放头一个。
13. 数组指针与指针数组的区别:单独总结了一章:数组指针与指针数组。
14.字符串的数组指针与指针数组:见经典奇葩面试题:C++中字符串的数组指针与指针数组 (对应面试题21)。这道题对于初学者很难,偏偏答案还是错的,误人子弟。
15.函数指针:
int max(int x,int y)
{
return (x>y?x:y);
}
int main()
{
int (*p)(int,int); //函数指针
p=max;
p=&max; // 二者都行!
int a;
a=(*p)(1,2);
cout<
16.用typedef定义函数指针:
typedef int (*pfun)(int x,int y);
int fun(int x,int y);
pfun p=fun;
int a=p(1,2)
17.malloc/free相比new/free来说,不足在于malloc/free是库函数而不是运算符,不能执行构造函数和析构函数,因此无法满足动态对象的需求。
此外,有一点书中没有提到,这里补充一下:
int *p1=new int[10];//p1申请的空间中的值是随机值
int *p2=new int[10]();//p2申请的空间中的值已经被初始化为0。
另外,如果用new申请一个二维动态数组:
int **data=new int*[size];
for(int i=0;i
malloc返回值是void*,因此需要类型转化:
Bitree *root=(Bitree *)malloc(sizeof(Bitree))
18.malloc,calloc,realloc的区别:
malloc调用形式为 (*类型)malloc(size)
calloc调用形式为 (*类型)calloc(n,size) //相当于 (*类型)malloc(n*size)
realloc调用形式为 (*类型)realloc(*ptr,size) //把ptr内存增大到size
19.动态内存的传递:
如同本节3所述,char*作为形参时不会影响实参,因此将空字符串指针传入在函数,在函数内部申请的内存不能返回到函数以外。应对方式:
1)形参用char *&,调用时传入指针名字。
2)形参用char **,调用时传入指针的地址即&指针名字。
3)函数返回值设置成char *,然后将新申请的指针以函数返回值的形式返回。(这种如果内部定义的不是new或者malloc出来的字符串而是普通的字符串,不能这样返回)
20.局部变量是栈地址,动态变量(new出来的变量)是堆地址,全局变量在静态存储区,常量(const)在常量存储区,还有malloc出来的变量在自由存储区。
21.句柄所指向的是复杂的结构可能与系统有关,windows编程中要用句柄来对应用程序进行操作,句柄可以算作一种特殊的指向指针的指针。而指针对应数据内存中的地址。
1.静态链接库与动态链接库的区别:
优点:代码装载速度快,执行速度快。只需要保证开发者的计算机中有正确的静态链接库文件,以二进制发布文件的时候不需要考虑用户的计算机上的lib文件是否存在的问题。
缺点:生成的可执行文件较大,包含相同的公共代码,资源浪费。
优点:节省内存,dll文件与exe文件独立,只要输出接口不变(名称、参数、返回值类型和调用约定不变),更换dll文件不会对exe文件造成影响,提高可维护性与可扩展性,适用于大规模的软件开发。此外,不同的语言编写的程序可以按照约定调用同一个dll文件。
缺点:可能会出现版本冲突问题。需要载入dll文件才能运行程序。
之后还有两节总结内容:
C和C++重难点复习笔记(二)【面向对象】
C和C++重难点复习笔记(三)【泛型编程与STL】
欢迎大家查阅参考。