文章目录
- 概要:
- Q1 空类实例化对象的sizeof
- Q2 指针操作优先级 *p++
- Q3 c-string作为map key时会有什么问题
- Q4 虚函数表的Sizeof大小
- Q5 new/new[]/malloc 对应 delete/delete[]/free
- Q6 统计对象的sizeof
- Q7 类的默认构造函数
- Q8 类的构造函数
- Q9 构造函数、析构函数是否可抛出异常
- Q10 虚函数表
- Q11 子类实例化对象的虚函数调用
- Q12 多线程环境下实现单例模式
- Q13 实现智能指针
- Q14 dynamic_cast进行动态转换
- Q15 CRTP奇异递归模板,实现静态多态分发
- Q16 placement new的用法及用途
- Q17 左值右值
- Q18 move语义
- Q19 复制省略 copy elision
- Q20 std::shared_ptr是线程安全的吗?
- Q21 策略模式
- Q22 std::list 与 std::vector遍历元素时的性能对比
- Q23 stl容器是线程安全吗?
- Q24 类成员变量的初始化顺序
- Q25 嵌套类
- Q26 智能指针循环引用
- Q27 虚函数的调用过程
- Q28 lambda
- Q29 使用make_shared创建智能指针
- Boost库
概要:
C++当前在交易系统开发技能中还占主要核心位置,其核心特性在于保障低时延,以及提供系统处理能力。当然其也有缺点:写好c++真的很难,经常容易因为内存泄漏,操作越界等原因导致系统崩溃。程序的稳健性也是系统的考量目标之一,所以在掌握语言特性的本身,也可以学习其他新的语言,比如Rust。回到正题,本章内容主要列了关于c++面试的常见问题,除此之外,还希望大家掌握 STL、Modern C++、Boost库等。
Q1 空类实例化对象的sizeof
class Test {
};
Test t;
Test t2;
一个对象的大小大于等于所有非静态成员大小的总和,这里没有任何成员,那么是不是应该为0呢?
其实不然,如果值为0的话,那么t 与 t2 就应该是同个实例吧。其实每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器会往隐藏一个字节。
Q2 指针操作优先级 *p++
const char* p = "Test";
c++中, 运算符优先级高,*p++
等同于 *(p++), p++
会返回旧值,*(p)
输出T后,p指向e字符
Q3 c-string作为map key时会有什么问题
以上代码定义了key为const char*
的map,其key的默认比较函数参数为两个 char 指针,而两个不同的指针可以指向同样的字符串内容,那么违背了key的唯一性,我们需要自定义比较函数,来处理C-字符串。
struct StrCmp {
bool operator()(char const *a, char const *b) const {
//是遍历c字符串比对
return std::strcmp(a, b) < 0;
}
}
//
map<const char *, int , StrCmp> strMap;
Q4 虚函数表的Sizeof大小
class Test {
public:
virtual ~Test() {
}
};
Test t;
可以看到Test类包含了虚函数,为了实现多态,编译器采用了虚函数表的方式,即t对象包含了一个指针,这个指针指向了虚函数表,即sizeof(t)统计了指针的大小(32位系统指针大小为 4, 64位系统指针大小为8)
Q5 new/new[]/malloc 对应 delete/delete[]/free
int *p = new int[10];
p++;
delete[] p;
以上代码是不安全的,p++
将指针移动到指向p+1
的地址,该地址并不是new[]
分配的,所以使用delete[]
不是合法行为。
再看以下代码:
int main() {
int x = 5;
delete &x;
}
x是一个栈变量,并不是通过new
创建的,所以使用delete
也是不合法行为。
又比如以下代码:
class Test {
};
Test *p = new Test();
free(p);
因为p是通过new创建的,所以不能通过free释放,而应该通过delete,只有malloc
分配的内存由free
关键字释放
Q6 统计对象的sizeof
class Test {
public:
static int x;
char c;
};
int Test::x = 3;
Test t;
在前文讲到过,sizeof统计的是对象非静态成员的大小,那么sizeof(t) 只计算了char c
,即 sizeof(t) 输出 1个字节,因为Test已经不是一个空类了, 所以编译器没必要隐含一个字节。
Q7 类的默认构造函数
class Test {
public:
Test(const Test& obj) {
}
};
Test t;
以上代码不能正常编译,因为我们自定义了Test的构造函数,所以编译器不会创建默认构造函数,这个时候 t的实例化因为没有合适的构造函数而失败。
通常我们会自定义构造函数来组织编译器这种默认行为,默认构造函数会初始化成员变量,如果某成员无法初始化(没有默认构造函数时),会编译失败。
当需要自定义构造函数又需要默认构造函数时,可以这样定义:
Test() = default;
Q8 类的构造函数
class Test{
private:
int *p;
public:
Test() {
p = new int(10);
}
~Test(){
delete p;
}
//拷贝构造函数 Test t; Test t1(t);
Test(const Test& obj) {
this->p = obj.p;
}
//赋值构造函数 Test t; Test t1 = t;
Test& operator=(const Test& obj) {
this->p = obj.p;
return *this;
}
//modern c++ 移动构造函数
Test(Test &&obj) noexcept {
this->p = obj.p;
obj.p = nullptr;
}
};
因为Test类包含了指针的成员变量,如果使用编译器默认的构造函数,会引发c++中的深拷贝、浅拷贝问题,所以需要我们自定义赋值、拷贝构造函数来屏蔽编译器默认的构造函数。
深拷贝和浅拷贝主要是针对类中的指针和动态分配的空间来说的,浅拷贝只是简单的复制指针的地址,并不是实例化对象,在没有自定义赋值构造函数情况下:
Test t; Test t1(t); 构造两个实例化对象 t跟t1时, t1的p指针与t的指针地址一致(浅拷贝复制指针值),当t析构时会释放t的指针p,而t1进行析构时,因为t1.p指向的地址内存已经被t释放了,造成了double free的情况。
Modern c++ 引入了左值、右值的概念,但存在有所有权要转移情况下,优化临时变量的创建,我们可以定义移动构造函数。
拷贝构造函数里 使用了&符号,其表示的是一个左值引用,可使用&&来表示右值,右值表示所引用的对象将要被销毁,该对象没有其他的用户,可以被当前使用者窃取数据,类似于rust所讲的所有权转移。这里的函数使用了noexcept关键字,表示当前函数不会抛出异常,即通知标准库不需要对异常处理做额外的工作,可以提升效率。
因为是窃取obj的数据,而obj表示即将销毁的对象,那么它会调用析构函数,同样会释放obj的p指针,所以这里将其值设置为nullptr,避免窃取过来的内存被obj给释放。
如果不需要自定义构造函数又不想让编译器默认生成构造函数时,我们可以使用delete关键字
class Test{
private:
int *p;
public:
Test() {
p = new int(10);
}
~Test(){
delete p;
}
Test(const Test& obj) delete;
Test& operator=(const Test& obj) = delete;
};
Q9 构造函数、析构函数是否可抛出异常
因为构造函数不像其他的函数有返回值(比如返回错误码),而当需要在构造函数做资源准备等一些逻辑操作时,可以抛出异常来处理实例化的错误情况。当然了,也可以像windows api一样,通过getlasterror来查看是否有错误,可使用threalocal的存储技术。
析构函数在编程规范上不建议抛出异常,而是在析构函数里将所有的错误情况处理完再正常退出实例,保证实例的生命周期是完整的。
Q10 虚函数表
class