C++面试常见问题
针对《程序员面试笔记——C/C++、算法、数据结构篇》一书的读书记录整理
文章目录
- C++面试常见问题
-
- 1.C++程序设计基础
- 2.指针和引用
- 3.内存管理
- 4.字符串
- 5.面向对象
-
-
- 简述面向过程和面向对象的区别
- 简述面向对象的基本特征
- 简述类和结构体的关系
- 类中的静态数据成员与静态成员函数
- 简述const修饰符在类中的用法
- 简述友元函数和友元类的概念。
- 构造函数和析构函数的执行顺序
- C语言不支持函数重载的原因
- extern C作用
- 简述函数重载和函数覆盖的区别
- 名字隐藏的问题
- 继承和组合的区别
- 简述公有继承,私有继承和保护继承的区别
- 父类构造函数和子类构造函数的关系
- 虚继承中构造函数的调用
- 空类的大小
- 简述虚函数表的概念
- 函数调用的匹配规则
- 简述vector容器空间增长的原理
- 简述vector容器中size和capacity函数的用途
- 手工调整vector容器空间的方式
- 简述deque容器的插入删除原理
- 迭代器失效原因
- 简述环状引用问题以及其解决方案
- unique_ptr优于auto_ptr的原因
-
1.C++程序设计基础
简述#include<>与#include""区别
答:#include<>从编译器指定路径处搜索,#include""首先在程序当前目录中进行搜索,然后再从编译器指定的路径进行搜索。
‘#’与‘##’在define中作用
答:宏定义中#运算符将其后面的参数转换成字符串,宏定义中的##运算符前后的参数进行字符串拼接
简述assert断言的概念
答:assert用于在程序的DEBUG版本中检测条件表达式,如果为假,则输出诊断信息并终止程序运行。
简述++i与i++的区别
前者先加1后赋值,后者先赋值后加1
简述C++类型转换操作符
- static_cast:基本类型转换(父类指针指向父类对象转换为子类指针的时候可以成功转换,但是不会检查对象是不是真的子类对象,所以运行结果不安全)
- dynamic_cast:用于对象指针之间的类型转换(将父类指针转换为子类指针的过程中,需要对其背后的真实对象进行类型检查,不匹配会转换失败返回空指针或者抛出异常)
- const_cast:在转换过程中增加或者删除const属性volatile属性
- reinterpret_cast:可以将一种类型的指针直接转换为另一种类型的指针,不论两个类型是否有继承关系。还可以把整数与指针互相转换。不安全。
静态全局变量的概念
静态全局变量的作用域为仅限于定义位置的文件内部。如果放在头文件里,其他文件包含以后,会对此静态全局变量进行拷贝,每个拷贝相互独立。
简述宏定义与内联函数的区别
对比 | 宏定义 | 内联 |
---|---|---|
替换阶段 | 预处理阶段 | 编译阶段 |
替换方式 | 简单字符串替换 | 函数内嵌 |
调试模式 | 不可调试 | 可调式 |
参数检查 | 无参数检查 | 有参数检查 |
作为类内成员函数 | 无法访问类内所有成员 | 可以访问类内所有成员 |
缺点 | 没有参数检查 | 代码膨胀 |
注意:在定义内联函数的时一定要在函数定义时使用inline关键字,在函数声明中使用inline时没有效果的。
sizeof计算结构体时内存对齐问题
数据对齐指在处理结构体中的成员时,成员在内存中的起始地址编码必须时成员类型所占字节数的整数倍。
结构体sizeof的计算结果必须是结构体中所占空间最多的成员所占空间的整数倍。
在数据对齐时,要以结构体最深层的基本数据类型为准。
简述malloc/free与new/delete的区别
对比 | malloc/free | new/delete |
---|---|---|
定义 | C语言的库函数 | C++的运算符 |
应用 | 只能用于基本类型 | 可以用于基本类型以及用户自定义类型 |
返回值 | 返回void*,需要显式转换成所需要的类型 | 直接指明对象类型 |
操作 | 只负责在堆上申请空间,返回首地址 | 申请空间后会调用对象的构造函数/析构函数 |
作为类内成员函数 | 无法访问类内所有成员 | 可以访问类内所有成员 |
简述delete与delete[]的区别
当new[]中的数组元素是基本类型时,通过delete和delete[]都可以释放数组空间
当new[]中的数组元素时自定义类型时,只能通过delete[]释放数组空间
不使用临时变量交换两个数
一个异或操作的交换律公式,a ^ b ^ a == a ^ a ^ b == b
一个数与其自身异或操作结果为0,一个数与0进行异或操作结果还是这个数本身。
- 方法1:
a=a-b;
b=b+a;
a=b-a; - 方法2:
a=a^b;
b=a^b;
a=a^b;
计算二进制中1的个数
将原数字每次右移1位,与1按位与操作,返回的是1,计数+1
或者把1按位左移进行按位与。
找出数组中唯一不成对出现的数字
所有数字异或一次,最后与0异或,返回值就是不成对的数字
简述main函数执行前后发生什么
main函数第一行代码执行前会调用全局对象和静态对象的构造函数,初始化全局变量和静态变量,main函数最后一行代码执行之后会调用atexit注册的函数,并且调用顺序与注册顺序相反。
2.指针和引用
区分数组和指针
数组名等价于数组首元素的地址,当数组名作为参数时,相当于传递了数组首元素的地址,而且只要实参是地址,那么形参一定是指针。
数组名是不能进行自增操作的,而指针可以。
简述指针和句柄的区别
句柄是一个32位的无符号证数,表示一个内存地址列表的整数索引,是分配给资源的唯一标识。句柄是间接指向资源对象的。有句柄的原因是因为如果资源对象在系统中一直处于空闲状态,那么操作系统的内存模块会将其内存回收,如果重新访问这个资源,系统会再重新分配内存,这个操作可能导致资源对象的物理地址放生改变。
实际上句柄中记录着资源对象列表中某个成员对象的缩影。
指针就是内存地址。
指针常量和常量指针的常见问题
常量地址不能初始化普通指针
指针常量定义时必须初始化
常量指针不能修改指向的内容
指针常量不能修改指针的值
指针常量与字符串常量的冲突
字符串常量是存在常量区中,如果定义一个const指针获取常量的指针,是不可以通过该定义的指针去修改常量的,此时这个指针相当于指向常量的常量指针。
简述数组指针与二维数组的区别
数组名始终等价于数组首元素的地址
指针作为参数的常见错误
如果想通过指针在被调函数中修改主调函数的变量,必须将主调函数变量的地址作为参数,在被调函数中修改指针指向的内容。
函数指针的概念
指针变量可以指向任意类型的数据,也可以指向一个函数,每个函数在内存中都占用一段存储单元,这段存储单元的首地址称为函数的入口地址,指向这个函数入口地址的指针称为函数指针。函数名等价于函数的入口地址。
空指针和野指针的概念
空指针是一种特殊的指针:处于空闲状态,没有指向任何变量。
野指针不是空指针,而是指向不明或不当的内存地址。出现野指针的原因
- 指针未初始化
- 指针指向的变量被free或者delete后没有置空
- 指针操作超过所指向变量的生存期
简述指针跟引用的区别
- 指针是变量的地址,引用是变量的别名
- sizeof指针为指针本身所占空间,32位返回4字节,引用sizeof返回原变量占有的空间
- 自增:指针自增指向下一地址空间,引用自增对原变量自增
- 初始化:指针可以不初始化或者初始化为空,引用必须初始化
- 修改:指针可以本身可以修改,引用本身不能修改
- 指针可以定义为二重,引用不可以
- 指针需要先解引用,引用直接使用
常量引用的初始化操作实际上分为两步:1将常量存放在一个临时变量里面,然后使用临时变量初始化常量引用
3.内存管理
简述栈空间跟堆空间的关系
栈空间存储函数参数局部变量,空间由操作系统自动分配回收。
堆空间用于动态分配内存块,由程序员分配跟释放
堆空间的频繁分配与释放会造成空间碎片化
栈空间默认在windows下都是1MB,堆空间理论有几G
生长方向:栈是自上而下,内存地址逐渐减小。堆是自下而上,内存地址逐渐增大
简述递归程序潜在的风险
在递归调用过程中,每次递归都会保留现场,把当前的上下文压入函数栈,调用过多,压栈过多会造成栈溢出,可以考虑使用循环代替递归调用。
预防内存泄露的方法
使用智能指针代替普通指针
保证malloc和free,new和delete成对出现
检查释放内存时有没有提前return的情况
访问vector元素时的越界问题
通过下标访问vector 的元素时不会进行边界检查,程序不会报错。
如果想在访问vector中的元素时首先进行边界检查,可以使用at函数
4.字符串
简述memcpy和strcpy的区别
两者都是C标准库函数
memcpy时C语言的内存拷贝函数,提供的是内存拷贝功能,不是只针对字符串
strcpy专为字符串定义的拷贝函数,用于标准字符串拷贝(有字符串结束标志\0)
5.面向对象
简述面向过程和面向对象的区别
面向过程是分析问题的解决步骤,明确步骤的输入输出跟流程,结构化自上而下的程序设计方法。
面向过程是把构成问题的事务分解成对象,从局部着手,通过迭代的方式逐步构建整个程序,以数据为核心,类设计为主要工作的程序设计方法。
面向过程使程序性能更高,系统开销更小。
面向过程使程序有更好的可扩展可复用可维护性。
简述面向对象的基本特征
抽象:对象抽象成类。
继承:继承一个类使得继承的类有被继承类的特性。
封装:把属性方法隐藏起来,只暴露有限的信息。
多态:不用对象对于同一消息的不同响应。
简述类和结构体的关系
类的默认访问控制是private,struct的默认访问控制是public
C++在C语言上对struct进行了拓展,使得struct也可以包含方法成员。
类中的静态数据成员与静态成员函数
静态数据成员属于整个类,不属于某个对象,必须在类内声明,类外初始化
静态成员函数也是属于整个类,没有this指针,所以不能访问非静态成员变量。
访问静态成员时可以通过类访问,也可以通过对象访问。
简述const修饰符在类中的用法
修饰成员变量代表成员变量初始化之后不可变,需要在初始化列表中进行初始化
修饰成员函数代表该成员函数不能改变成员变量的值
常量对象只能调用类的常量成员函数
简述友元函数和友元类的概念。
友元函数不是类的成员函数,而是类外部的函数,能够访问类的非公有成员。
友元类的所有成员函数都是另一个类的友元函数。
友元关系是单方向的,且不能被继承。
构造函数和析构函数的执行顺序
自上而下构造,自下而上析构
C语言不支持函数重载的原因
C++在编译过程中对函数重命名,而C语言保留原始函数名
extern C作用
告诉编译器使用C函数编译,不进行重命名
简述函数重载和函数覆盖的区别
函数覆盖发生在子类和父类之间,父类定义虚函数,子类重新实现这个函数,函数原型相同,根据对象的选择调用的函数。
函数重载是同一类不同方法,参数列表不同,根据参数类型选择调用的函数。
名字隐藏的问题
父类中有一组重载函数,子类在继承时如果覆盖了这组重载函数的任意一个,则其他没有被覆盖的同名函数在子类中是不可见的。
继承和组合的区别
继承是is-a关系,组合是has-a的关系
简述公有继承,私有继承和保护继承的区别
公有继承可以访问父类的公有成员保护成员
保护继承可以访问共有成员保护成员,继承过来的共有成员都变为保护成员
私有继承可以可以访问公有成员保护成员,继承过来的共有成员保护成员都变成私有成员
父类构造函数和子类构造函数的关系
创建一个子类对象时,系统在执行子类构造函数的函数体前,首先调用父类的构造函数。
虚继承中构造函数的调用
在菱形继承中存在访问二义性的问题,使用虚继承保证了子类只有被菱形继承的爷爷类只有一份拷贝,虚继承保证继承关系中的虚基类只被初始化一次
空类的大小
1字节,占位符
简述虚函数表的概念
如果一个类中有虚函数,那么这个类就会对应一个虚函数表,虚函数表中的元素是一组指向函数的指针,每个指针指向一个虚函数的入口地址,在访问虚函数的时候通过虚函数表进行函数调用。
在含有虚函数的类对象中,除了对象的数据成员之外,还有一个指向虚函数表的指针,位于顶部。
函数调用的匹配规则
先找参数完全匹配的普通函数
寻找模板参数完全匹配的函数模板,并实例化一个模板函数
通过隐式转换匹配普通函数
都失败编译失败
简述vector容器空间增长的原理
如果向一个已满的vector插入元素,会重新分配一块内存空间,并将原有元素和新插入的元素拷贝到新空间中。内存增长大小一般为1.5-2倍
简述vector容器中size和capacity函数的用途
size返回容器中已经保存的元素个数
capacity返回容器当前容量大小
手工调整vector容器空间的方式
reserve函数可以让容器重新分配指定大小的空间
shrink_to_fit函数可以回收所有尚未使用的剩余空间
resize函数可以强制调整容器中已保存的元素个数
简述deque容器的插入删除原理
双端队列deque是一种双向开口的存储空间分段连续的数据结构,每段数据空间内部是连续的,而每段数据空间不一定连续。在向deque删除和插入元素的过程中,会根据数据空间的状态,动态分配和释放空间,数据空间段的数量会发生变化。
迭代器失效原因
对容器进行删除操作时,容器中元素的数量发生变化,这种变化可能会导致某些元素的物理地址发生变化,使指向这些元素的迭代器失效。
简述环状引用问题以及其解决方案
有可能出现环状引用的地方使用weak_ptr弱指针代替shared_ptr共享指针可以有效地避免环状引用的问题
unique_ptr优于auto_ptr的原因
由于unique_ptr在内存安全性,充当容器元素和支持动态数组方面优于auto_ptr,因此C++11使用unique_ptr代替auto_ptr