校招笔记




2.3、C++岗位
C++能投哪些岗位?
嵌入式研发岗位
嵌入式方向可能比较偏向于硬件一些,比如国内的华为、中兴、小米、紫光展锐这些公司都是招嵌入式
开发的。
嵌入式开发由于涉及硬件比较多,所以对于通信、电信、自动化这些偏硬件的专业会友好一些,嵌入式
开发一般会涉及到一些网络编程、Socket通信之类的,还有一些会涉及到并发编程等。
主要的业务方向是物联网以及芯片等方向,国家也是大力发展芯片方向,所以嵌入式也是一个不错的职
业方向。
后端/服务器研发
这是C++方向的最大缺口之一了,同样也是竞争最为激烈的岗位之一,后端研发要求掌握了解的知识技
能也是非常多的。除了要有比较扎实的C++语言基础,还要求你具有多线程编程、跨平台编码等知识,
也需要你有一定的算法能力和了解常见的数据结构等。还要一些常用数据库,比如关系型数据库MySQL
和内存型数据库Redis、Memcached你要了解一些的。
对于社招选手来说,还会要求他们具有一些中间件的使用,包括微服务等,但是对于校招更看中扎实的
基础和潜力,微服务和中间件,有则加分,没有也无伤大雅。
还有就是计算机四大基础:操作系统,计算机网络,计算机组成原理以及编译原理了,这些基础才能保
障你在前进的路上走的更远更稳,在实际的后端开发过程中涉及到的东西很多,从网络通信到性能优化
再到系统总体架构,都需要你具有很扎实的计算机基础。
游戏研发岗位
不少游戏的引擎和服务器都是基于C++研发的,要是论业务范围广度,C++远远比不上Java,可你要是
比性能高、速度快,Java就得给C++让道了。
C++游戏研发比较吃经验,但是国内游戏大中厂就那么几家:网易游戏、腾讯游戏、米哈游、巨人网络
等,所以游戏研发岗位竞争也比较激烈,坑位没有后端那么多,你在跳槽时的可选择面就要窄一些。
最重要的是游戏研发是需要有一定兴趣的,因为就业面比较窄想转行就不是那么容易了。从事游戏研发
除了要求你具有C++基础之外,像一些基本的图形学理论也是必须了解的,Unity3D等引擎也是必问的。
多媒体研发岗位
近年来短视频和直播行业的崛起有一部分原因要归功于多媒体研发比如音视频等,一些播放器直播平台
和一些特效的实现都离不开C++开发。
但多媒体研发所需的不仅仅只是语言这一门,你可能还需要了解一些图像视频的采集、音视频的加工、
编码等,常见的可能需要你了解OpenCV、ffmpeg、x264等协议,还有就是一些基于音视频传输协议
等,具体还要看岗位的详细要求。
29
客户端研发岗位
这里主要说的是ios客户端,ios研发主要使用Objective-C/Swift开发,但是现在鉴于客户端研发岗位比较
小程序把很多客户端的市场份额抢占了。
客户端相较于后端,就业选择要窄一些,特别是对于一些二三线的互联网公司来说,可能根本就没有客
户端开发的岗位,这是一个很大的弊端,但是好处就是竞争要比后端小上不少,而且需求也不小,建议
大家自己好好思考,结合个人兴趣以及未来规划等加以把握。
30
3、C++
3.1、基础语法
为了不影响同学们的阅读,非原创部分均于文末给出出处,例如C++部分于3.5 小节文末 给出参考
文献,其余知识点类似,不再赘述。
1、 在main执行之前和之后执行的代码可能是什么?
main函数执行之前,主要就是初始化系统相关资源:
设置栈指针
初始化静态static变量和global全局变量,即.data段的内容
将未初始化部分的全局变量赋初值:数值型 short,int , long等为0 , bool 为 FALSE ,指针为 NULL 等
等,即.bss段的内容
全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码
将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数
__attribute__((constructor))
main函数执行之后:
全局对象的析构函数会在main函数之后执行;
可以用 atexit 注册一个函数,它会在main 之后执行;
__attribute__((destructor))
update1:https://github.com/forthespada/InterviewGuide/issues/2 ,由stanleyguo0207提出 - 2021.03.22
2、结构体内存对齐问题?
结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。
未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)
c++11以后引入两个关键字 alignas与 alignof。其中alignof可以计算出类型的对齐方式,alignas可以指
定结构体的对齐方式。
但是alignas在某些情况下是不能使用的,具体见下面的例子:
// alignas 生效的情况
struct Info {
uint8_t a;
uint16_t b;
uint8_t c;
};
std::cout << sizeof(Info) << std::endl; // 6 2 + 2 + 2
std::cout << alignof(Info) << std::endl; // 2
31
struct alignas(4) Info2 {
uint8_t a;
uint16_t b;
uint8_t c;
};
std::cout << sizeof(Info2) << std::endl; // 8 4 + 4
std::cout << alignof(Info2) << std::endl; // 4
alignas将内存对齐调整为4个字节。所以sizeof(Info2)的值变为了8。
// alignas 失效的情况
struct Info {
uint8_t a;
uint32_t b;
uint8_t c;
};
std::cout << sizeof(Info) << std::endl; // 12 4 + 4 + 4
std::cout << alignof(Info) << std::endl; // 4
struct alignas(2) Info2 {
uint8_t a;
uint32_t b;
uint8_t c;
};
std::cout << sizeof(Info2) << std::endl; // 12 4 + 4 + 4
std::cout << alignof(Info2) << std::endl; // 4
若alignas小于自然对齐的最小单位,则被忽略。
如果想使用单字节对齐的方式,使用 alignas 是无效的。应该使用 #pragma pack(push,1) 或者使用
__attribute__((packed))。
#if defined(__GNUC__) || defined(__GNUG__)
#define ONEBYTE_ALIGN __attribute__((packed))
#elif defined(_MSC_VER)
#define ONEBYTE_ALIGN
#pragma pack(push,1)
#endif
struct Info {
uint8_t a;
uint32_t b;
uint8_t c;
} ONEBYTE_ALIGN;
#if defined(__GNUC__) || defined(__GNUG__)
#undef ONEBYTE_ALIGN
#elif defined(_MSC_VER)
#pragma pack(pop)
#undef ONEBYTE_ALIGN
#endif
std::cout << sizeof(Info) << std::endl; // 6 1 + 4 + 1
std::cout << alignof(Info) << std::endl; // 6
32
确定结构体中每个元素大小可以通过下面这种方法:
#if defined(__GNUC__) || defined(__GNUG__)
#define ONEBYTE_ALIGN __attribute__((packed))
#elif defined(_MSC_VER)
#define ONEBYTE_ALIGN
#pragma pack(push,1)
#endif
/**
* 0 1 3 6 8 9 15
* +-+---+-----+---+-+-------------+
* | | | | | | |
* |a| b | c | d |e| pad |
* | | | | | | |
* +-+---+-----+---+-+-------------+
*/
struct Info {
uint16_t a : 1;
uint16_t b : 2;
uint16_t c : 3;
uint16_t d : 2;
uint16_t e : 1;
uint16_t pad : 7;
} ONEBYTE_ALIGN;
#if defined(__GNUC__) || defined(__GNUG__)
#undef ONEBYTE_ALIGN
#elif defined(_MSC_VER)
#pragma pack(pop)
#undef ONEBYTE_ALIGN
#endif
std::cout << sizeof(Info) << std::endl; // 2
std::cout << alignof(Info) << std::endl; // 1
这种处理方式是alignas处理不了的。
update1:https://github.com/forthespada/InterviewGuide/issues/2 ,由stanleyguo0207提出 - 2021.03.22
3、指针和引用的区别
指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
指针可以有多级,引用只有一级
指针可以为空,引用不能为NULL且在定义时必须初始化
指针在初始化后可以改变指向,而引用在初始化之后不可再改变
sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同
一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
引用本质是一个指针,同样会占4字节内存;指针是具体变量,需要占用存储空间(,具体情况还要
具体分析)。
引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和
定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指
针变量可以重新指向别的变量。
不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。
33
参考代码:
void test(int *p)
{
int a=1;
p=&a;
cout<<p<<" "<<*p<<endl;
}
int main(void)
{
int *p=NULL;
test(p);
if(p==NULL)
cout<<"指针p为NULL"<<endl;
return 0;
}
//运行结果为:
//0x22ff44 1
//指针p为NULL
void testPTR(int* p) {
int a = 12;
p = &a;
}
void testREFF(int& p) {
int a = 12;
p = a;
}
void main()
{
int a = 10;
int* b = &a;
testPTR(b);//改变指针指向,但是没改变指针的所指的内容
cout << a << endl;// 10
cout << *b << endl;// 10
a = 10;
testREFF(a);
cout << a << endl;//12
}
在编译器看来, int a = 10; int &b = a; 等价于 int * const b = &a; 而 b = 20; 等价于 *b = 20; 自动转换为指针
和自动解引用.
4、在传递函数参数时,什么时候该使用指针,什么时候该使用引用呢?
需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,
不然会内存泄漏。而返回局部变量的引用是没有意义的
对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更
小
类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式
34
5、堆和栈的区别
申请方式不同。
栈由系统自动分配。
堆是自己申请和释放的。
申请大小限制不同。
栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a查看,由ulimit -s修
改。
堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。
申请效率不同。
栈由系统分配,速度快,不会有碎片。
堆由程序员分配,速度慢,且会有碎片。
栈空间默认是4M, 堆区一般是 1G - 4G
堆 栈
管 堆中资源由程序员控制(容易产生memory leak) 栈资源由编译器自动管理,无需手
理 工控制
方
式
内 系统有一个记录空闲内存地址的链表,当系统收到程序申 只要栈的剩余空间大于所申请空
存 请时,遍历该链表,寻找第一个空间大于申请空间的堆结 间,系统为程序提供内存,否则报
管 点,删 除空闲结点链表中的该结点,并将该结点空间分配 异常提示栈溢出。(这一块理解一
理 给程序(大多数系统会在这块内存空间首地址记录本次分 下链表和队列的区别,不连续空间
机 配的大小,这样delete才能正确释放本内存空间,另外系统 和连续空间的区别,应该就比较好
制 会将多余的部分重新放入空闲链表中) 理解这两种机制的区别了)
空 堆是不连续的内存区域(因为系统是用链表来存储空闲内 栈是一块连续的内存区域,大小是
间 存地址,自然不是连续的),堆大小受限于计算机系统中 操作系统预定好的,windows下栈
大 有效的虚拟内存(32bit 系统理论上是4G),所以堆的空间 大小是2M(也有是1M,在 编译时
小 比较灵活,比较大 确定,VC中可设置)
碎 对于堆,频繁的new/delete会造成大量碎片,使程序效率降 对于栈,它是有点类似于数据结构
片 低 上的一个先进后出的栈,进出一一
问 对应,不会产生碎片。(看到这里
题 我突然明白了为什么面试官在问我
堆和栈的区别之前先问了我栈和队
列的区别)
生 堆向上,向高地址方向增长。 栈向下,向低地址方向增长。
长
方
向
分 堆都是动态分配(没有静态分配的堆) 栈有静态分配和动态分配,静态分
配 配由编译器完成(如局部变量分
方 配),动态分配由alloca函数分
式 配,但栈的动态分配的资源由编译
器进行释放,无需程序员实现。
35
堆 栈
分 堆由C/C++函数库提供,机制很复杂。所以堆的效率比栈 栈是其系统提供的数据结构,计算
配 低很多。 机在底层对栈提供支持,分配专门
效 寄存器存放栈地址,栈操作有专门
率 指令。
形象的比喻
栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切
菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
6、你觉得堆快一点还是栈快一点?
毫无疑问是栈快一点。
因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简
单,并且有专门的指令执行,所以栈的效率比较高也比较快。
而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且
获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
7、区别以下指针类型?
int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)
int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向
int类型的指针变量。
int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数
组,这个数组大小是10。
int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。
int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。
8、new / delete 与 malloc / free的异同
相同点
都可用于内存的动态申请和释放
不同点
前者是C++运算符,后者是C/C++语言标准库函数
new自动计算要分配的空间大小,malloc需要手工计算
new是类型安全的,malloc不是。例如:
int *p = new float[2]; //编译错误
int *p = (int*)malloc(2 * sizeof(double));//编译无错误
new调用名为operator new的标准库函数分配足够空间并调用相关对象的构造函数,delete对指针所指
对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存。后
36
者均没有相关调用
后者需要库文件支持,前者不用
new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象
9、new和delete是如何实现的?
new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,
以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回
指向新分配并构造后的的对象的指针
delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准
库函数释放该对象所用内存
10、malloc和new的区别?
malloc和free是标准库函数,支持覆盖;new和delete是运算符,不重载。
malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空
间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。
malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。
11 、 既 然 有 了 malloc/free , C++ 中 为 什 么 还 需 要 new/delete 呢 ? 直 接 用
malloc/free不好吗?
malloc/free和new/delete都是用来申请内存和回收内存的。
在对非基本数据类型的对象使用的时候,对象创建的时候还需要执行构造函数,销毁的时候要执行析
构函数。而malloc/free是库函数,是已经编译的代码,所以不能把构造函数和析构函数的功能强加给
malloc/free,所以new/delete是必不可少的。
12、被free回收的内存是立即返还给操作系统吗?
不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝
试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc
也会尝试对小块内存进行合并,避免过多的内存碎片。
13、宏定义和函数有何区别?
宏在编译时完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调
用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。
宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值。
宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
宏定义不要在最后加分号。
14、宏定义和typedef区别?
宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。
宏不检查类型;typedef会检查数据类型。
宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。
注意对指针的操作,typedef char * p_char和#define p_char char *区别巨大。
15、变量声明和定义区别?
声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其
分配存储空间。
相同变量可以在多处声明(外部变量extern),但只能在一处定义。
37
16、strlen和sizeof区别?
sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。
sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾
是' '的字符串。
因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
int main(int argc, char const *argv[]){
const char* str = "name";
sizeof(str); // 取的是指针str的长度,是8
strlen(str); // 取的是这个字符串的长度,不包含结尾的 。大小是4
return 0;
}
17、常量指针和指针常量区别?
指针常量是一个指针,读成常量的指针,指向一个只读变量,也就是后面所指明的int const 和 const
int,都是一个常量,可以写作int const *p或const int *p。
常量指针是一个不能给改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也
就是存放在指针中的地址)就不能在改变了,即不能中途改变指向,如int *const p。
update1:https://www.nowcoder.com/discuss/597948 ,网友“ 牛客191489444号 ”指出笔误,感谢!
update2: 《 C++ Primer 5th 》 P56 页 有 明 确 说 明 常 量 指 针 和 指 针 常 量 , 阿 秀 特 意 去 确 认
了-20210521。
多说一句,网上关于指针常量和常量指针的说法很多跟书本上都不一致,甚至百度百科上跟
《C++ Primer 5th》书上在指针常量和常量指针的说法刚好相反,鉴于百度百科是人人都可以去
编辑,因此我信书。
也希望各位遇到问题时要多去查阅资料,多去确认,不要因为某些博客或者文章说了就确认无
疑。
18、a和&a有什么区别?
假设数组int a[10]; int (*p)[10] = &a;其中:
a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加
1操作后变为0x00000005。*(a + 1) = a[1]。
&a是数组的指针,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首
地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。
若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小来读
取。
19、C++和Python的区别
包括但不限于:
Python是一种脚本语言,是解释执行的,而C++是编译语言,是需要编译后在特定平台运行的。
python可以很方便的跨平台,但是效率没有C++高。
Python使用缩进来区分不同的代码块,C++使用花括号来区分
C++中需要事先定义变量的类型,而Python不需要,Python的基本数据类型只有数字,布尔值,字符
串,列表,元组等等
Python的库函数比C++的多,调用起来很方便
38
20、C++和C语言的区别
C++中new和delete是对内存分配的运算符,取代了C中的malloc和free。
标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数(C中没有字符串类型)。
C++中用来做控制态输入输出的iostream类库替代了标准C中的stdio函数库。
C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和longjmp()函数。
在C++中,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别
开来。而这在C语言中是不允许的。也就是C++可以重载,C语言不允许。
C++语言中,允许变量定义语句在程序中的任何地方,只要在是使用它之前就可以;而C语言中,必
须要在函数开头部分。而且C++允许重复定义变量,C语言也是做不到这一点的
在C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们
只是名字不相同,其他都是相同的。
C++相对与C增加了一些关键字,如:bool、using、dynamic_cast、namespace等等
21、C++与Java的区别
语言特性
Java语言给开发人员提供了更为简洁的语法;完全面向对象,由于JVM可以安装到任何的操作系统
上,所以说它的可移植性强
Java语言中没有指针的概念,引入了真正的数组。不同于C++中利用指针实现的“伪数组”,Java引入
了真正的数组,同时将容易造成麻烦的指针从语言中去掉,这将有利于防止在C++程序中常见的因为
数组操作越界等指针操作而对系统数据进行非法读写带来的不安全问题
C++也可以在其他系统运行,但是需要不同的编码(这一点不如Java,只编写一次代码,到处运
行),例如对一个数字,在windows下是大端存储,在unix中则为小端存储。Java程序一般都是生成
字节码,在JVM里面运行得到结果
Java用接口(Interface)技术取代C++程序中的抽象类。接口与抽象类有同样的功能,但是省却了在实现
和维护上的复杂性
垃圾回收
C++用析构函数回收垃圾,写C和C++程序时一定要注意内存的申请和释放
Java语言不使用指针,内存的分配和回收都是自动进行的,程序员无须考虑内存碎片的问题
应用场景
Java在桌面程序上不如C++实用,C++可以直接编译成exe文件,指针是c++的优势,可以直接对内存
的操作,但同时具有危险性 。(操作内存的确是一项非常危险的事情,一旦指针指向的位置发生错
误,或者误删除了内存中某个地址单元存放的重要数据,后果是可想而知的)
Java在Web 应用上具有C++ 无可比拟的优势,具有丰富多样的框架
对于底层程序的编程以及控制方面的编程,C++很灵活,因为有句柄的存在
22、C++中struct和class的区别
相同点
两者都拥有成员函数、公有和私有部分
任何可以使用class完成的工作,同样可以使用struct完成
不同点
两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的
class默认是private继承,而struct模式是public继承
39
引申:C++和C的struct区别
C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函
数的定义,(C++中的struct能继承,能实现多态)
C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数
据,而且成员不可以是函数
C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public(为了与
C兼容)
struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加
上struct,才能做结构类型名(除:typedef struct class{};);C++中结构体标记(结构体名)可以直接
作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例
23、define宏定义和const的区别
编译阶段
define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用
安全性
define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住
全部的内容,要不然很容易出错
const常量有数据类型,编译器可以对其进行类型安全检查
内存占用
define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份,
且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表
宏替换发生在编译阶段之前,属于文本插入替换;const作用发生于编译过程中。
宏不检查类型;const会检查数据类型。
宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内
存空间。
24、C++中const和static的作用
static
不考虑类的情况
隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能
在该文件所在的编译模块中使用
默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区
静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量
相同,函数退出后仍然存在,但不能使用
考虑类的情况
static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,
必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声
明为const、虚函数和volatile;可以被非static成员函数任意访问
const
不考虑类的情况
const常量在定义时必须初始化,之后无法更改
const形参可以接收const和非const类型的实参,例如// i 可以是 int 型或者 const int 型void fun(const
int& i){ //...}
40
考虑类的情况
const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须
有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非
mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值
25、C++的顶层const和底层const
概念区分
顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是 * 号的右边
底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边
举个例子
int a = 10;int* const b1 = &a; //顶层const,b1本身是一个常量
const int* b2 = &a; //底层const,b2本身可变,所指的对象是常量
const int b3 = 20; //顶层const,b3是常量不可变
const int* const b4 = &a; //前一个const为底层,后一个为顶层,b4不可变
const int& b5 = a; //用于声明引用变量,都是底层const
区分作用
执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const
使用命名的强制类型转换函数const_cast时,只能改变运算对象的底层const
const int a;int const a;const int *a;int *const a;
int const a和const int a均表示定义常量类型a。
const int *a,其中a为指向int型变量的指针,const在 * 左侧,表示a指向不可变常量。(看成const (*a),
对引用加const)
int *const a,依旧是指针类型,表示a为指向整型数据的常指针。(看成const(a),对指针const)
26、数组名和指针(这里为指向数组首元素的指针)区别?
二者均可通过增减偏移量来访问数组中的元素。
数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。
当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,
但sizeof运算符不能再得到原数组的大小了。
27、final和override关键字
override
当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写,以下方法都可以:
41
class A
{
virtual void foo();
}
class B : public A
{
void foo(); //OK
virtual void foo(); // OK
void foo() override; //OK
}
如果不使用override,当你手一抖,将foo()写成了f00()会怎么样呢?结果是编译器并不会报错,因为它
并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整
个程序不利。所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名
字不小心打错了的话,编译器是不会编译通过的:
class A
{
virtual void foo();
};
class B : public A
{
virtual void f00(); //OK,这个函数是B新增的,不是继承的
virtual void f0o() override; //Error, 加了override之后,这个函数一定是继承自A的,A找
不到就报错
};
final
当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加
final关键字后被继承或重写,编译器会报错。例子如下:
class Base
{
virtual void foo();
};
class A : public Base
{
void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};
class B final : A // 指明B是不可以被继承的
{
void foo() override; // Error: 在A中已经被final了
};
class C : B // Error: B is final
{
};
28、拷贝初始化和直接初始化
当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用与实参匹配的构
造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,
42
然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。举例如下
string str1("I am a string");//语句1 直接初始化
string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调用构造函数对str2进行初始
化
string str3 = "I am a string";//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,
再把临时对象作为参数,使用拷贝构造函数构造str3
string str4 = str1;//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符
函数
为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就
完全等价于直接初始化了(语句1和语句3等价),但是需要辨别两种情况。
当拷贝构造函数为private时:语句3和语句4在编译时会报错
使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错
29、初始化和赋值的区别
对于简单类型来说,初始化和赋值没什么区别
对于类和复杂数据类型来说,这两者的区别就大了,举例如下:
class A{
public:
int num1;
int num2;
public:
A(int a=0, int b=0):num1(a),num2(b){};
A(const A& a){};
//重载 = 号操作符函数
A& operator=(const A& a){
num1 = a.num1 + 1;
num2 = a.num2 + 1;
return *this;
};
};
int main(){
A a(1,1);
A a1 = a; //拷贝初始化操作,调用拷贝构造函数
A b;
b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
return 0;
}
30、extern"C"的用法
为了能够正确的在C++代码中调用C语言的代码:在程序中加上extern "C"后,相当于告诉编译器这部分
代码是C语言写的,因此要按照C语言进行编译,而不是C++;
哪些情况下使用extern "C":
(1)C++代码中调用C语言代码;
(2)在C++中的头文件中使用;
(3)在多个人协同开发时,可能有人擅长C语言,而有人擅长C++;
举个例子,C++中调用C代码:
43
#ifndef __MY_HANDLE_H__
#define __MY_HANDLE_H__
extern "C"{
typedef unsigned int result_t;
typedef void* my_handle_t;
my_handle_t create_handle(const char* name);
result_t operate_on_handle(my_handle_t handle);
void close_handle(my_handle_t handle);
}
综上,总结出使用方法,在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持
extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。所以使用extern "C"全部都放在
于cpp程序相关文件或其头文件中。
总结出如下形式:
(1)C++调用C函数:
//xx.h
extern int add(...)
//xx.c
int add(){
}
//xx.cpp
extern "C" {
#include "xx.h"
}
(2)C调用C++函数
//xx.h
extern "C"{
int add();
}
//xx.cpp
int add(){
}
//xx.c
extern int add();
31、野指针和悬空指针
都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致