- 教程
【免费下载链接】InterviewGuide
🔥🔥「InterviewGuide」是阿秀从校园->职场多年计算机自学过程的记录以及学弟学妹们计算机校招&秋招经验总结文章的汇总,包括但不限于C/C++ 、Golang、JavaScript、Vue、操作系统、数据结构、计算机网络、MySQL、Redis等学习总结,坚持学习,持续成长!
本篇以 InterviewGuide 校招八股文系列的开篇文档 01-01-01-basic.md 为骨架,系统梳理 C++ 基础语法中最常被面试官问到的 20 个问题:从 main 函数执行前后的运行时行为、结构体内存对齐,到指针与引用的本质差异、堆与栈的管理机制、new/delete 与 malloc/free 的底层实现区别,再到宏、typedef、const 指针与数组名等易混概念。读完你既能背得住结论,也能结合仓库内的源码级笔记把每个考点理解到"能讲清原理、能写码验证"的程度,直接服务于 C++ 校招/社招面试。
1、在 main 执行之前和之后执行的代码可能是什么?
这是面试中一个"由表及里"的经典问题,考察你对程序运行时启动与收尾机制的理解。
main 函数执行之前,主要就是初始化系统相关资源:
- 设置栈指针;
- 初始化静态
static变量和global全局变量,即.data段的内容; - 将未初始化部分的全局变量赋初值:数值型
short、int、long等为0,bool为FALSE,指针为NULL等等,即.bss段的内容; - 全局对象初始化,在
main之前调用构造函数,这是可能在 main 之前执行的一些代码; - 将 main 函数的参数
argc、argv等传递给main函数,然后才真正运行main函数; - 还可以通过
__attribute__((constructor))在 main 之前执行自定义函数。
main 函数执行之后:
- 全局对象的析构函数会在 main 函数之后执行;
- 可以用
atexit注册一个函数,它会在 main 之后执行; - 也可以通过
__attribute__((destructor))在 main 之后执行收尾逻辑。
值得注意的是,同系列文档 01-01-03-basic.md 进一步指出:C++ 引入对象后,初始化必须执行相应的构造函数和析构函数,而构造函数或析构函数中经常需要进行特定操作,所以 C++ 标准规定全局或静态对象在首次用到时才会构造,并通过atexit()来管理析构——程序结束时按构造顺序的反方向逐个析构。这一点可以当作 main 前后执行顺序问题的纵深答案。
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 struct alignas(4) Info2 { uint8_t a; uint16_t b; uint8_t c; }; std::cout << sizeof(Info2) << std::endl; // 8 2 + 2 + 2 + 2 std::cout << alignof(Info2) << std::endl; // 4alignas将内存对齐调整为 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; // 1确定结构体中每个元素大小还可以通过位域的方式实现:
#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处理不了的。
纵向补充:对齐规则与#pragma pack(n)。同系列 01-01-05-basic.md 对内存对齐给出了更完整的规则表述:
- 分配内存的顺序按照声明的顺序;
- 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍则空出内存,直到偏移量是整数倍为止;
- 最后整个结构体的大小必须是里面变量类型最大值的整数倍。
添加#pragma pack(n)后规则变为:
- 偏移量要是 n 和当前变量大小中较小值的整数倍;
- 整体大小要是 n 和最大变量大小中较小值的整数倍;
- n 值必须为 1、2、4、8……,为其他值时就按照默认的分配规则。
01-01-04-basic.md 中也有对应验证:加上#pragma pack(4)指定 4 字节对齐方式即可改变结构体默认布局。
3、指针和引用的区别
- 指针是一个变量,存储的是一个地址;引用跟原来的变量实质上是同一个东西,是原变量的别名;
- 指针可以有多级,引用只有一级;
- 指针可以为空,引用不能为 NULL 且在定义时必须初始化;
- 指针在初始化后可以改变指向,而引用在初始化之后不可再改变;
sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小;- 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以;
- 在汇编层面,一些编译器将引用当成指针操作,因此引用会占用空间。是否占用空间,应该结合编译器分析;
- 引用在声明时必须初始化为另一变量,一旦出现必须为
typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量; - 引用一旦初始化之后就不可以再改变(变量可以被引用多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量;
- 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。
参考代码:
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++ 类对象传递的标准方式。
5、堆和栈的区别
- 申请方式不同:栈由系统自动分配;堆是自己申请和释放的。
- 申请大小限制不同:栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过
ulimit -a查看,由ulimit -s修改;堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。 - 申请效率不同:栈由系统分配,速度快,不会有碎片;堆由程序员分配,速度慢,且会有碎片。
一般说来,栈空间默认是 4M,堆区一般是 1G - 4G(具体数值与平台及编译器相关)。
| 堆 | 栈 | |
|---|---|---|
| 管理方式 | 堆中资源由程序员控制(容易产生 memory leak) | 栈资源由编译器自动管理,无需手工控制 |
| 内存管理机制 | 系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样 delete 才能正确释放本内存空间,另外系统会将多余的部分重新放入空闲链表中) | 只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出。(这一块理解一下链表和队列的区别、不连续空间和连续空间的区别,就比较好理解这两种机制的区别了) |
| 空间大小 | 堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit 系统理论上是 4G),所以堆的空间比较灵活,比较大 | 栈是一块连续的内存区域,大小是操作系统预定好的,windows 下栈大小是 2M(也有是 1M,在编译时确定,VC 中可设置) |
| 碎片问题 | 对于堆,频繁的 new/delete 会造成大量碎片,使程序效率降低 | 对于栈,它有点类似于数据结构上的一个先进后出的栈,进出一一对应,不会产生碎片。这也解释了为什么面试官在问堆和栈的区别之前先问栈和队列的区别 |
| 生长方向 | 堆向上,向高地址方向增长。 | 栈向下,向低地址方向增长。 |
| 分配方式 | 堆都是动态分配(没有静态分配的堆) | 栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由 alloca 函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现 |
| 分配效率 | 堆由 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 类型的。
补充说明:这四行声明的辨析与 01-01-01-basic.md 第 18 问中的int (*p)[10] = &a相互呼应——&a的类型正是int (*)[10],即指向整个数组的指针。
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的标准库函数释放该对象所用内存;后者(malloc/free)均没有相关调用; - 后者需要库文件支持,前者不用;
- new 是封装了 malloc,直接 free 不会报错,但是这只是释放内存,而不会析构对象。
9、new 和 delete 是如何实现的?
- new 的实现过程:首先调用名为
operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的对象的指针; - delete 的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为
operator delete的标准库函数释放该对象所用内存。
纵深补充(源码级印证):01-02-01-memory.md 指出,目前所有编译器在内部实现上都是直接调用::operator new()和::operator delete(),与直接使用 new 运算符效果一致;而operator new()内部有调用malloc的操作,operator delete()内部有调用free的操作。同系列 01-01-03-basic.md 也确认:"new 会先调用 operator new 函数,申请足够的内存(通常底层使用 malloc 实现),然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针;delete 先调用析构函数,然后调用 operator delete 函数释放内存(通常底层使用 free 实现)"。
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 也会尝试对小块内存进行合并,避免过多的内存碎片。
结合 01-02-01-memory.md 的内存管理链条(new -> operator new -> malloc、delete -> operator delete -> free)可以推断:面试官把这个问题和"堆碎片""频繁 new/delete 造成效率降低"连起来问,考察的正是你对底层内存分配器(glibc 的 ptmalloc)缓存机制的完整认识。
13、宏定义和函数有何区别?
- 宏在预处理阶段完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数;
- 宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值;
- 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型;
- 宏定义不要在最后加分号。
14、宏定义和 typedef 区别?
- 宏主要用于定义常量及书写复杂的内容;typedef 主要用于定义类型别名;
- 宏替换发生在编译阶段之前,属于文本插入替换;typedef 是编译的一部分;
- 宏不检查类型;typedef 会检查数据类型;
- 宏不是语句,不要在最后加分号;typedef 是语句,要加分号标识结束;
- 注意对指针的操作,
typedef char * p_char和#define p_char char *区别巨大。
15、变量声明和定义区别?
- 声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间;
- 相同变量可以在多处声明(外部变量 extern),但只能在一处定义。
16、strlen 和 sizeof 区别?
- sizeof 是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen 是字符处理的库函数;
- sizeof 参数可以是任何数据的类型或者数据(sizeof 参数不退化);strlen 的参数只能是字符指针且结尾是
'\0'的字符串; - 因为 sizeof 值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
int main(int argc, char const *argv[]){ const char* str = "name"; sizeof(str); // 取的是指针str的长度,是8 strlen(str); // 取的是这个字符串的长度,不包含结尾的 \0。大小是4 return 0; }16.2、(补充题)一个指针占多少字节?
上面 16 题中提到的sizeof(str)值为 8,是在64 位的编译环境下,指针的占用大小为 8 字节;而在32 位环境下,指针占用大小为 4 字节。
一个指针占内存的大小跟编译环境有关,而与机器的位数无关。还有疑问的话,可以自行打开 Visual Studio 编译器实验一番。
(该问由微信好友"轻与重"提出补充,已采纳。)
17、常量指针和指针常量区别?
- 指针常量是一个指针,读成"常量的指针",指向一个只读变量,也就是后面所指明的
int const和const int,都是一个常量,可以写作int const *p或const int *p; - 常量指针是一个不能改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的地址)就不能再改变了,即不能中途改变指向,如
int *const p。
关于命名,原文档特别强调过一段考证:《C++ Primer 5th》P56 页有明确说明"常量指针"和"指针常量",阿秀特意去确认过。网上关于指针常量和常量指针的说法很多跟书本上不一致,甚至百度百科上跟《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](就是第 7 问提到的数组指针),其加 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++ 的多,调用起来很方便。
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等等。
(原文档勘误记录:感谢微信好友"铁锤哥哥"勘误"C++ 允许重复定义变量 -> 不允许",已修正。)
延伸阅读:继续深入本系列
本篇是 InterviewGuide"校招八股文·编程语言"模块的第一篇(基础语法 1-20)。如果想继续巩固同一主题,建议按顺序阅读同目录下的后续文档:
- 基础语法 21-40:C++ 与 Java 的区别、struct 与 class 的区别等;
- 基础语法 41-60:new/malloc 深入、delete 如何得知释放大小、allocator 等;
- 基础语法 61-80:类与对象、
#pragma pack应用; - 基础语法 81-100:内存对齐规则、函数调用栈、结构体比较等;
- 内存管理专题:
operator new/delete与malloc/free的底层调用链; - C++11 专题 与 STL 专题:进阶考点;
- 如果还在打基础阶段,可先阅读仓库中的 C++ 求职版学习路线,再回到八股文系列逐题消化。
- 教程
【免费下载链接】InterviewGuide
🔥🔥「InterviewGuide」是阿秀从校园->职场多年计算机自学过程的记录以及学弟学妹们计算机校招&秋招经验总结文章的汇总,包括但不限于C/C++ 、Golang、JavaScript、Vue、操作系统、数据结构、计算机网络、MySQL、Redis等学习总结,坚持学习,持续成长!
相关推荐
校招八股文精选:C++ 基础语法高频面试题 21-40(InterviewGuide 实战解析)
校招八股文精选:C++ 基础语法高频面试题 21 40(InterviewGuide 实战解析) 本文是 InterviewGuide「阿秀的学习笔记」校招八股
文档教程知识库InterviewGuide C++ 基础语法高频面试题精讲(101-120):main 内存结构、volatile、引用传参与字符串函数安全性
InterviewGuide C++ 基础语法高频面试题精讲(101 120):main 内存结构、volatile、引用传参与字符串函数安全性 本文基于 In
文档教程知识库InterviewGuide C++基础语法面试题精讲(81-100):组合与继承、函数指针、内存对齐、模板头文件、static 与异常处理
InterviewGuide C++基础语法面试题精讲(81 100):组合与继承、函数指针、内存对齐、模板头文件、static 与异常处理 本篇是 Inter
文档教程知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考