文章目录
- 1. this指针干什么用的?
- 2. new和delete,什么时候用new[]申请,可以用delete释放?
- 3. static关键字的作用(从elf结构、链接过程回答)?
- 4. C++的继承有什么好处?
- 5. 讲一下C++的多态
- 6. 空间配置器allocator?
- 7. vector和list的区别?
- 8. map和multimap的区别
- 9. C++如何防止内存泄漏?智能指针详述
- 10. C++如何调用C语言函数接口?
- 11. 那些情况下可能出现访问越界
- 12. C++中类的初始化列表
- 13. C和C++的区别以及内存分布
- 14. A* const p和const A* p的区别
- 15. 构造函数{}之间的赋值和初始化列表之间的区别
- 16. malloc和new的区别
- 17. map和set的实现原理
- 18. shared_ptr的引用计数在哪存放
- 19. STL各个容器底层数据结构?
- 20. vector里empty()和size()的区别?
- 21. 迭代器的失效问题
- 22. C++中struct和class的区别
- 23. 初始化的全局变量和未初始化的全局变量的区别
- 24. 堆和栈的区别
- 25. 构造函数和析构函数可不可以是虚函数
- 26. 构造函数和析构函数是否可以抛异常
- 27. 宏和内联函数的区别
- 28. 局部变量存放在哪
- 29. 拷贝构造函数为什么传引用&,不传值
- 30. 内联函数和普通函数的区别(反汇编角度)
- 31. 如何实现一个不可继承的类
- 32. 什么是纯虚函数?为什么要有纯虚函数?虚函数表放在哪?
- 33. 说一下C++中的const
- 34. const与static的区别
- 35. 四种强制类型转换
- 36. deque底层数据结构
- 37. 虚函数
- 38. 多态
- 39. C++异常处理原理
- 40. 早绑定(静态)和晚绑定(动态)
- 41. 指针和引用的区别(汇编角度)
- 42. 交叉引用的问题如何解决
- 43. C++函数重载的原理
- 44. 编写一个C++程序需要注意什么
- 45. 设计模式知道哪些?具体讲一下
- 46. 构造函数抛出异常可能会导致内存泄漏,如何解决?
1. this指针干什么用的?
this的原型:A* const this,是不可改变指向的指针,是类方法的第一个参数,static方法没有this指针参数
一个class可以定义很多对象,每个对象都有自己的成员变量以及成员方法,this指针在编译后作为成员方法的第一个参数
在成员方法里访问成员变量的时候,this指针用于区分访问哪个对象的成员变量。成员方法第一个参数都是当前类的一个指针,我们通过对象调用成员方法时,会把对象的地址传入这个方法,该方法通过this指针就能访问该对象的数据成员
obj.fun() ==> fun(&obj)
2. new和delete,什么时候用new[]申请,可以用delete释放?
new和delete是重载的运算符,实际上调用的是operator new
和operator delete
如果是自定义类型,而且提供了析构函数,用new[]
申请的内存空间就一定要用delete[]
释放,因为new[]
除了开辟用户需要使用的内存空间,还多开辟了4字节存放对象的个数。因为编译器知道自己总共分配了多少空间,记录对象的个数后,可以在内存中准确划分出每个对象的首地址。
如果是内置类型,或没有提供析构函数的自定义类型, new[]
开辟空间的起始地址和返回给用户的地址相同,也就是没有存放对象个数的4字节空间。
胡乱猜测:没有提供析构函数,delete不执行析构,分配多少空间就回收多少空间,不用4字节记录对象个数。
可参考:C++运算符重载
3. static关键字的作用(从elf结构、链接过程回答)?
从面向过程角度来说:
符号表中被static修饰的全局变量、函数就从global变成了local,也即从整个项目可见变成当前文件可见,链接的时候不会被覆盖。static全局变量编译完成后就有初始值了
static修饰的局部变量放在数据段(.data或.bss),由于是在数据段,程序一开始就要开辟空间,在执行到相应语句的时候初始化变量。局部变量属于指令,本身不产生符号,通过ebp-偏移量
进行访问,但是被static修饰后由于放在数据段,这时候就需要产生符号。
从面向对象角度来说:
static可以修饰成员变量,修饰成员方法,不再产生this指针,变成类所有,通过类作用域调用
4. C++的继承有什么好处?
继承处于类和类之间的关系,好处如下:
- 代码复用
- 通过继承,在基类里面给所有派生类保留统一的纯虚函数接口,派生类进行重写。通过基类指针或者引用指向派生类对象实现多态(指针指向谁就访问谁的方法),也更符合软件开发的 “开闭原则”(只要是派生类,基类指针都可指向,不用修改)
可参考:C++的继承和多态
5. 讲一下C++的多态
分为静多态(编译时期)和动多态(运行时期)。
静多态的表现形式包括函数重载和类模板,在编译时期就要确定下来。
继承结构中,Base类指针(引用)指向Derive类对象,通过该指针(引用)调用同名覆盖方法(虚函数),该指针指向哪个Derive对象的覆盖对象,就调用哪个Derive类方法
可参考:C++的继承和多态
6. 空间配置器allocator?
allocator主要是给容器使用,作用是将开辟空间allocate和构造对象construct分开,以及将回收空间deallocate和析构对象destroy分开
对于容器而言,如果使用new进行内存管理,当我们申请容器空间时,就会生成很多对象,而这些对象是我们用不上的,这个时候需要用到空间配置器。容器删除元素的时候,只是析构对象,而不是像delete一样析构对象且回收空间
7. vector和list的区别?
vector底层是数组,内存连续。插入删除为O(n),随机访问O(1),尾部的插入删除都是O(1)
list底层是双向链表,内存不连续。中间增加删除O(n),首尾增加删除O(1),访问O(n)
可参考:C++STL总结
8. map和multimap的区别
map是一个映射表[key-value],不允许key重复,底层实现是红黑树(一种二叉排序树),主要是对key进行查找比较,快速找到key,时间复杂度为O(log2n)
multimap允许key重复,底层实现是红黑树
红黑树的5个性质:
- 每个节点要么是黑色,要么是红色
- 根节点是黑色
- 叶子节点是黑色
- 每个红色节点的两个子节点一定都是黑色, 不能有两个红色节点相连
- 任意一节点到每个叶子节点的路径都包含数量相同的黑色结点
性质5又可以推出: 如果一个节点存在黑色子节点,那么该结点肯定有两个子节点,不然走另一条路就会少一层黑色结点。
红黑树插入有3种情况(最多旋转2次),删除有4种情况(最多旋转3次)
9. C++如何防止内存泄漏?智能指针详述
内存泄漏:分配的堆内存(没有名字,只能用指针指向)没有释放
智能指针体现在把裸指针进行了面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源
利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源。所以,智能指针一般都是定义在栈上的
可参考:C++智能指针
10. C++如何调用C语言函数接口?
由于C和C++生成符号的方式不同,C和C++直接的API接口无法直接调用,C语言的函数声明必须在extern "C"
里
#ifdef __cplusplus
extern "C"{
#endif
int fun(int,int);
#ifdef __cplusplus
}
#endif
如果是C编译器编译这段代码,宏不会展开,只能看到函数声明部分的代码
如果是C++编译器编译这段代码,宏会展开,告诉C++编译器,编译和调用这个函数都得按照C语言的符号规则
// 使用C语言的方式生成函数符号
extern "C" {
// 函数符号function_C,而不是function_C_int
int function_C();
}
可参考:C和C++的区别
11. 那些情况下可能出现访问越界
- 数组、STL容器访问越界
- 字符串没有添加
\0
,导致访问越界 - 使用类型强转或static_cast,使得大类型(派生类)的指针指向小类型(基类)的对象,指针解引用直接越界
12. C++中类的初始化列表
可以指定数据成员的初始化方式,尤其是指定成员对象的初始化(成员变量的初始化和定义的顺序有关,和初始化列表的顺序无关)
放在构造函数体内的代码是进行赋值,而不是初始化,执行构造函数体时,所有的成员变量就已经初始化好了
13. C和C++的区别以及内存分布
引用、函数重载、运算符重载、new/delete/malloc/free、const、inline、模板、OOP语言可使用设计模式、STL、异常处理、智能指针
可参考:C和C++的区别
14. A* const p和const A* p的区别
我们在传递对象时,如果不想发生拷贝,可以通过传地址,用指针接收的形式。如果我们同时也不想改变实参的值,我们就使用const A* p
接收,这样我们可以读*p
,但是不能写*p
,有效的保护了数据
A* const p
表示不能改变指针的指向,可以改变指向的数据。而this指针原理就是这样的,this指针就是一个只能指向当前对象的指针,不能修改指向。我们不能对this赋值,但是能修改this指向的数据成员
非const形参不能接收const实参,这也就是为什么const对象无法调用普通方法
const只能控制在语言级别,而不是在指令级别控制读写性质,也就是说编译器检测到对const量赋值就会报错,但如果我们不显式赋值,编译器就检测不到,我们其实也可以修改const修饰的变量
const int a = 1;
int* p = (int*)&a;
*p = 2;
如果用const修饰成员方法时,这个成员方法第一个参数会变成const A* const this
,当我们用常对象调用普通方法时,就会报错,因为常指针无法指向普通对象
class A {
public:
void fun() {
cout << "A::fun()" << endl;
};
};
int main() {
const A a;
a.fun(); // 报错
return 0;
}
但如果我们希望在常方法中修改成员变量的值,可以用mutable
修饰这个成员,被mutable修饰的变量,将永远处于可变的状态,即使在常方法中
15. 构造函数{}之间的赋值和初始化列表之间的区别
执行构造函数初始化列表的时候,是正在构造对象,初始化对象的成员。而执行{}之间的赋值时对象已经构造完成,仅仅就是赋值,不是初始化。正因为const变量在语言级别不能被赋值,所以必须放在初始化列表
可参考:C和C++的区别
16. malloc和new的区别
-
malloc是按字节开辟空间的,new开辟内存时需要指定类型(new int()),malloc开辟内存返回的都是void*,而new返回的是对应类型的指针
-
malloc负责开辟空间,new不仅有malloc的功能,还可以进行数据初始化,比如:new int(10)。new有开辟空间和构造的功能。
-
malloc开辟内存失败返回nullptr,而new则会抛出bad_alloc异常
-
我们调用new实际上是调用的operator new
可参考:C++运算符重载
17. map和set的实现原理
set是集合,里面只存放key。map是映射表,存储[key,value]键值对。
有序的关联容器,底层数据结构都是红黑树,然后需要答一些红黑树的性质,相关算法
18. shared_ptr的引用计数在哪存放
因为引用计数对象也是new出来的,所以存放在堆区
可参考:C++智能指针的enable_shared_from_this和shared_from_this机制
19. STL各个容器底层数据结构?
顺序容器: vector(数组)、deque(二维数组)、list(双向循环链表)
容器适配器: stack(deque)、queue(deque)、priority_queue(vector)
关联容器: set系列(红黑树)、map系列(红黑树)
可参考:C++ STL总结
20. vector里empty()和size()的区别?
_NODISCARD bool empty() const noexcept {
auto& _My_data = _Mypair._Myval2;
return _My_data._Myfirst == _My_data._Mylast;
}
_NODISCARD size_type size() const noexcept {
auto& _My_data = _Mypair._Myval2;
return static_cast<size_type>(_My_data._Mylast - _My_data._Myfirst);
}
21. 迭代器的失效问题
迭代器不允许一边读一边修改
当容器调用erase方法后,当前位置到容器末尾元素的所有迭代器全部失效(首元素到插入点的迭代器有效)
当容器调用insert方法后,当前位置到容器末尾元素的所有迭代器全部失效(首元素到插入点的迭代器有效)
insert来说,如果引起容器内存扩容,原来容器的所有的迭代器就全部失效
当通过迭代器更新容器元素以后,要及时对迭代器进行更新,insert/erase都会返回新位置的迭代器
可参考:C++运算符重载
22. C++中struct和class的区别
- 定义类的时候,struct的访问限定默认是public,class的访问限定默认是private
- 继承时,
class Derive:Base
表示私有继承,struct Derive:Base
表示公有继承 - Linux下,C语言里struct定义的空结构体是0字节,C++里struct定义的空类是1字节(空结构体有多大?)
- 为了兼容C语言里的struct,C++里struct定义的类对象初始化也可以用C语言中结构体的初始化方式,如:
Data data = {10,20}
,但是C++的class不支持这种C风格的结构体初始化方式 - C++class支持定义模板类参数,
template<class T>
,不支持template<struct T>
23. 初始化的全局变量和未初始化的全局变量的区别
.data
:存放初始化且初始化值不为0的数据
.bss
:存放未初始化和初始化为0的数据
24. 堆和栈的区别
- 堆内存的大小 >> 栈内存的大小
- 堆内存需要手动开辟、释放,栈区开辟函数栈帧,自动清退
- 堆内存从低地址到高地址分配,栈从高地址(栈底)到低地址(栈顶)
25. 构造函数和析构函数可不可以是虚函数
构造函数不能是虚函数,析构函数可以是虚函数
构造函数不能实现成virtual,通常来说,vfptr存放在对象空间,任何虚函数被执行的流程,都是在对象空间种找到vfptr,找到vftable,才能执行。根据这个逻辑,虚函数执行前对象必须存在,也就是任何一个虚函数执行前构造函数必须执行完
如果构造函数没执行,对象不存在,也就不存在vfptr,也就无法执行虚函数
如果构造一个派生类对象,派生类构造需要先构造基类,基类的构造是一个虚函数,此时发生动态绑定,需要访问派生类的前4个字节vfptr,然后这个时候派生类对象还没产生,访问出错
基类的指针指向堆上的派生类对象时,delete ptr_base
调用析构函数的时候,由于必须要调用到派生类对象的析构函数,所以必须是动态绑定,此时需要把Base的析构函数实现成virtual
若是静态绑定,则直接根据指针的类型,调用析构函数,无法调用派生类的析构函数
可参考:C++中的继承和多态总结
26. 构造函数和析构函数是否可以抛异常
主要考虑内存泄漏、资源释放的问题
构造函数不能抛出异常,若抛出异常则表示对象创建失败,不能调用析构函数,资源无法释放,我们捕获构造函数中可能抛出的异常,保证已经分配的堆内存被释放
析构函数只能在释放完所有资源后抛出异常
27. 宏和内联函数的区别
#define
在预编译时期处理,inline
在编译时期处理#define
就是字符串替换,release版本中inline
在函数调用点把函数代码展开,节省函数的调用开销#define
可以定义很多,比如常量、代码段、函数等,inline
只能用于修饰函数#define
不能调试,inline
在Debug版本下可以调试,这时产生函数调用开销
28. 局部变量存放在哪
局部变量通过 栈底指针ebp
偏移访问,存放在栈区
局部变量不产生符号,不属于数据,属于指令的一部分(若是在VS Debug版本下查看反汇编会发现依然有符号,这是VS优化后展示给用户的结果,实际上应该是ebp-偏移量
)
29. 拷贝构造函数为什么传引用&,不传值
class Test{
// 会产生编译错误
Test(const Test t){
...
}
};
int main(){
Test t1;
Test t2(t1);
return 0;
}
这时用t1拷贝构造t2,如果传值,则需要用t1初始化形参t,这个时候也需要调用Test的拷贝构造函数(t1.Test(t)
),而调用拷贝构造函数的时候,仍然需要实参初始化形参,再次调用拷贝构造函数,陷入了死循环。
而实际上,编译器也会检查,直接发生编译错误,无法运行
30. 内联函数和普通函数的区别(反汇编角度)
inline函数
- 编译期间在代码调用的地方展开(release版本),有逻辑性的进行文本替换,因此不产生函数符号
- 能够调试,在debug版本(需要调试)inline函数和普通的函数表现一致,只有在release版本才会真正在调用点展开
- 因为需要在编译期间展开,而编译期间针对的是单文件。所以inline函数的作用域只在本文件,debug版本生成local符号
- 有类型检测,安全
- 类体内实现的成员方法直接是inline
核心: 函数调用开销
开辟栈帧
栈帧清退
普通函数
- 不展开
- 能调试
- 作用域在全局,生成global符号
- 有类型检测,安全
可参考:C和C++的区别
31. 如何实现一个不可继承的类
派生类构造的过程:先是执行基类构造函数,然后执行派生类构造函数。
我们将基类的构造函数私有化,此时基类的构造函数对派生类是不可见的,所以无法继承。
32. 什么是纯虚函数?为什么要有纯虚函数?虚函数表放在哪?
virtual void fun() = 0;
用纯虚函数定义的类是抽象类,无法实例化对象,可以定义指针和引用
纯虚函数定义在基类里面,基类不代表实体,它的主要任务之一就是 给所有的派生类保留统一的纯虚函数接口,让派生类进行重写。重写以后就 可以使用多态,基类指针指向派生类对象,指向哪个对象就调用哪个对象的方法
虚函数表是在编译阶段产生,运行时加载到.rodata
段
可参考:C++继承和多态总结
33. 说一下C++中的const
编译时期值替换
const定义的叫常量,它的编译方式是把出现常量名字的地方,用常量的值进行替换
const int a = 10;
int* p = (int*)&a;
*p = 100;
cout<<a<<" "<<*p<<endl; // 10 100
// 立即数初始化,a的内存已经改了,但是编译时期替换,指令就是输出10
int b = 10;
const int a = b;
int* p = (int*)&a;
*p = 100;
cout<<a<<" "<<*p<<endl; // 100 100 变量初始化,编译时期不会进行替换
常变量
若用立即数初始化,则编译阶段可以直接用立即数进行替换。
若用变量初始化,则退化为和C语言中一模一样的常变量(只是不能作为左值,编译方式同普通变量,不会在使用的地方直接替换为立即数)
定义常成员方法
this指针由Test* this
变成const Test* this
,普通对象和常对象都可以调用
可参考:C和C++的区别
34. const与static的区别
面向过程
const修饰:全局变量、局部变量、形参变量
static修饰:全局变量、局部变量、函数(本文件可见,把符号的作用域从global改成local,链接的时候看不见)
面向对象
const
常方法(普通对象和常对象都可以调用,只能对成员进行读不能写)、常成员变量(不能被修改,必须在构造函数的初始化列表初始化)
static
静态方法:该方法没有this指针,不依赖于对象,通过类作用域访问
静态成员变量:静态成员变量属于类,不属于某个具体的对象,实现多个对象共享数据的目的。static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。即没有在类外初始化的 static 成员变量不能使用
35. 四种强制类型转换
const_cast
:去掉常量属性
static_cast
:类型安全的转换
reinterpret_cast
:C风格的类型转换,没有安全可言,随意转换
dynamic_cast
:支持RTTI识别的类型转换。基类指针转成相应的派生类对象指针的时候,dynamic_cast会识别该指针是否能够进行转换
36. deque底层数据结构
deque是双端队列容器,底层数据是动态扩容的二维数组。一维数组从2开始,以2倍的方式进行扩容,每次扩容后原来第二维的数组从新的第一维数组的下标oldsize/2
开始存放,上下留出相同的空间,方便deque首尾入队
由于是双端队列,所以最开始的时候,first
和 last
其实是指向相同地方的
扩容后:
37. 虚函数
一个类如果有虚函数表,那在编译阶段就会产生vfptr,指向虚函数表,该表运行时加载到.rodata
用指针或引用调用虚函数的时候,首先访问对象前4个字节vfptr,然后找到虚函数表取出函数入口地址,进行动态绑定
38. 多态
使用基类的指针或引用指向不同的派生类对象,指向哪个对象就调用哪个对象的方法。
增加新功能或删除已有功能的时候,函数接口不需要改变。不管基类有多少个派生类,接口一套就够了,不需要对每一个对象都设计一套接口。
OOP里面的设计模式离不开多态,达到高内聚、低耦合的目标
39. C++异常处理原理
try{
可能抛出异常的代码
}catch(const string& error){
捕获相应异常类型的对象,进行处理,处理完成后继续向下执行
}
C++中如果在当前函数栈帧上没有找到相应的catch块处理,就会把异常抛给调用方函数。处理了就继续运行,没处理就继续向调用方抛出,直到main函数还没有处理,就向系统抛出,系统发现进程有异常未处理,就直接终止进程。
40. 早绑定(静态)和晚绑定(动态)
早绑定:编译时期完成(汇编代码是call 函数名
),用对象调用虚函数。
晚绑定:运行时确定(汇编代码是call 寄存器
),用指针或引用调用虚函数。确定指针类中被调用函数的类型,是虚函数则找到对象前4个字节的vfptr,从vftable拿到函数入口地址
可参考:C++继承和多态总结
41. 指针和引用的区别(汇编角度)
lea是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数,例如:lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。而mov指令则恰恰相反,例如:mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。
可参考:C和C++的区别
42. 交叉引用的问题如何解决
交叉引用问题:无论是定义对象还是使用对象都用shared_ptr,导致交叉引用问题
定义对象时候使用强智能指针shared_ptr,引用的时候使用弱智能指针weak_ptr。当通过weak_ptr访问对象成员时,需要调用weak_ptr的lock提升方法,把weak_ptr提升成shared_ptr,然后再进行对象成员调用。
可参考:C++智能指针
43. C++函数重载的原理
C++生成函数符号是依赖函数名+参数列表
44. 编写一个C++程序需要注意什么
- 首先要分析需求,然后进行概要设计、详细设计
- 设计合适的类,接口,是否使用多态,是否运行在多线程环境、考虑线程安全
- 考虑代码的可移植性,使用语言级别的接口,还是使用系统提供的接口
- 参考现有的代码框架、设计模式等
45. 设计模式知道哪些?具体讲一下
可参考:C++设计模式总结
46. 构造函数抛出异常可能会导致内存泄漏,如何解决?
class Test{
public:
Test(){
p1 = new int();
p2 = new int();
throw "xxxx"
}
~Test(){
delete p1;
delete p2;
}
private:
int* p1;
int* p2;
}
改写成:
class Test{
public:
Test(){
p1 = new int();
p2 = new int();
throw "xxxx"
}
~Test(){ }
private:
unique_ptr<int> p1;
unique_ptr<int> p2;
}