本专栏文章持续更新,新增内容使用蓝色表示。
C++面向对象的三大特性:封装,继承,多态
(1)封装是将数据和函数组合到一个类里。主要目的是隐藏内部的实现细节,仅暴露必要的接口给外部。通过封装,可以控制类成员的访问级别(例如:public、protected 和 private)。
(2)继承是派生类获得基类的属性和方法。提高代码复用性。public、protected、private。
(3)多态是同一个函数在不同的对象上表现形式不同。主要通过重载和重写实现的,重载在编译阶段完成,属于静态的多态,重写是利用虚函数和动态绑定实现,属于动态多态。
【补充】重载是利用名称修饰技术来改函数名,区分参数列表不同的参数。
解释虚函数与虚函数表
(1)虚函数是C++中实现运行时多态的机制。重写是利用虚函数和动态绑定实现的,在基类函数前加上virtual,派生类中重写,运行时就会根据对象的实际类型去调用。
(2)虚函数表:每个有虚函数的类都会维护一个虚函数表(vtable),存储了该类所有虚函数的指针。调用虚函数时,程序通过vptr找到虚函数表,再通过表中的函数指针调用正确的函数实现。
【补充】对象创建时,编译器会在对象内存布局的最前面加一个vptr指针,指向该类的虚函数表。调用虚函数时,程序通过vptr找到虚函数表,再通过表中的函数指针调用正确的函数实现。
【补充】虚函数表在内存中包含:
指向类型信息的指针(RTTI),该类所有虚函数的实际地址,可能包含继承链中父类的虚函数地址,派生类的虚函数表会继承基类的虚函数表内容,并用派生类重写的函数地址覆盖对应的表项。纯虚函数在虚函数表中的位置会被保留,但指向一个特殊的"未实现"处理函数。
你对重载运算符了解多少?
首先只能重载已有的运算符,并且运算符的优先级、操作数个数、结合律和原来的一致。
其次是重载方式分为成员运算符和非成员运算符,区别是成员运算符少一个参数。
【补充】下标运算符、箭头运算符必须是成员运算符
重载、重写与隐藏
(1)重载是指相同作用域(命名空间、类)内有相同的函数名,但是参数列表不同。它根据参数不同来调用不同的函数。
【补充】返回值不能作为重载的区分条件
(2)重写是指在派生类中重新定义基类中的方法。在需要改变或扩展基类方法时使用。
【补充】基类中被重写的函数有virtual修饰,派生类在重写时要有相同的名称、参数列表、返回类型,只有函数体内不同。
(3)隐藏是指派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
【面试】在隐藏或者重写的情况下,如何调用基类的函数?
答:使用作用域解析符::,类名::函数名(),或者通过基类指针或引用调用。
malloc、new,free、delete的异同
malloc和free是C语言中的库函数,而new和delete是C++ 运算符,会调用它的构造和析构函数,而且运算符可以被重载。不过new和delete的底层会调用malloc和free。
【补充】malloc和free需要指定大小,返回的是void*类型,需要进行强制类型转换。
【补充】new的底层会调用operator new,而它又会使用malloc分配内存,而malloc的底层是系统调用。delete 先调用析构函数释放对象资源,再通过 operator delete释放内存,调用free 直接归还内存给系统。
malloc 1KB和1MB 有什么区别?
malloc() 并不是系统调用,而是 C 库里的函数,用于动态分配内存。它的源码里默认定义了一个阈值:
如果用户分配的内存小于阈值,则通过 brk() 系统调用从堆分配内存。
如果用户分配的内存大于阈值,则通过 mmap() 系统调用在文件映射区域分配内存;
字节序问题
大端字节序:高位字节存储在低地址处,低位字节存储在高地址处
低位字节序:低位字节存储在低地址处,高位字节存储在高地址处
【补充】网络字节序使用的是大端字节序
Explicit关键字
用于防止隐式转换。防止编译器自动执行预期外的类型转换,提高代码的安全性。
define、const、inline、typedef的区别
define,发生在预处理阶段,用于定义宏、常量,但是定义的常量没有类型检查,作用域是全局作用域,不受命名空间限制。
Const发生在编译阶段,定义的常量是带类型的,会存储在内存中,有利于类型转换。
Inline内联函数,在函数声明前加上 inline 关键字。
typedef 发生在编译阶段处理的,有更严格的类型检查,用于为现有类型创建别名。
【补充】定义为内联函数不一定会进行内联,就像mysql的索引优化一样,编译器内部会进行判断,选择最优的结果。优点是类型安全、可调试、可优化,缺点是函数体被复制多次,占用更多的代码段空间,某些情况下会导致代码膨胀,所以只适用于简单代码。
C++类对象的初始化
(1)基类初始化
如果当前类继承自一个或多个基类,它们将按照声明顺序进行初始化,有虚继承和一般继承的情况下,优先虚继承。
(2)成员变量初始化
按照在类定义中的声明顺序进行初始化(只与声明顺序有关)。
(3)执行构造函数
深copy与浅copy
浅拷贝是复制指针值,也就是地址,这会导致新旧对象共享同一块内存。而深拷贝不仅复制指针,还会创建新的内存空间去拷贝数据,每个对象都有自己独立的副本。
【补充】默认情况下,C++使用浅拷贝,效率高,但是有安全隐患:比如一个对象释放了内存,另一个对象的指针就会变成悬空的,访问会导致未定义行为;其次,当多个对象析构时,会多次释放同一块内存,造成程序崩溃。
【补充】深拷贝,需要自定义拷贝构造函数和赋值运算符重载。拷贝构造函数中,要为指针成员分配新的内存,并拷贝原始对象的数据;赋值运算符重载中,除了拷贝数据外,还需要先释放对象原有的内存,处理自赋值的情况。
使用智能指针,shared_ptr共享所有权,unique_ptr独占所有权。
Static类成员函数和类静态变量
static 函数是静态成员函数,它与类本身相关,而不是与类的对象。可以将其视为在类作用域下的全局函数。
【补充】静态函数没有 this 指针,不能访问任何非静态成员变量。
C++11新特性
(1)auto关键字编译器自动推断变量类型。
(2)基于范围的for循环,简化容器遍历语法
(3)智能指针:引入内存管理类,减少内存泄漏:
std::unique_ptr:独占所有权的智能指针,
std::shared_ptr:共享所有权的智能指针,
std::weak_ptr:不增加引用计数的智能指针。
(4)移动语义和右值引用,提高资源利用效率。
(5)Lambda表达式,支持匿名函数。
(6)nullptr更安全的空指针常量。
(7)委托构造函数,构造函数可以调用同类其他构造函数。
右值引用
右值引用简单来说,就是可以延长某块内存的存活时间。一般情况下,变量生命周期结束,它就被销毁了,比如,函数中的临时变量,在函数退出之后销毁。如果想要延长使用时间,就可以使用右值引用,此时生命周期变为右值引用的生命周期。
【补充】右值引用构造函数(移动构造函数),复用另外一个对象中的资源(堆内存)。移动构造对资源进行了转移,而普通的拷贝构造函数会使得多个对象的指针指向同一块内存。
【补充】左右值不是等号的左右,关于左值有个关键字是lvalue(locator value),字面意思是可通过地址定位到这个数据,所以左值是可以进行取地址操作。右值是rvalue(read value),只读的数据。所以一般能进行取地址操作的是左值,不能的是右值。
【补充】不管是左值引用还是右值引用都是别名,不占用内存空间。实现方面右值引用比左值引用多写一个取地址符。语法上我们只能通过右值去初始化右值引用。同类型的左值、右值引用,右值常量引用等都可以用来初始化常量左值引用(const T&)。
智能指针
(1)std::unique_ptr (独占指针)
特点:独占所有权,同一时间只能有一个unique_ptr指向特定对象
不能复制,但可以通过std::move转移所有权,离开作用域时自动释放所管理的内存,零开销(与原始指针相比几乎没有额外成本)
std::unique_ptr<MyClass> ptr(new MyClass());
// 或者更好的方式(C++14起):
auto ptr = std::make_unique<MyClass>();
(2)std::shared_ptr (共享指针) 可以使用std::make_shared
特点:共享所有权,多个shared_ptr可以指向同一个对象
使用引用计数机制跟踪资源,当最后一个shared_ptr被销毁时释放资源,需要额外内存,有少量额外开销
(3)std::weak_ptr (弱指针)
特点:不增加引用计数,不会阻止对象被销毁
用于解决shared_ptr的循环引用问题,必须转换为shared_ptr才能访问对象
static_cast、dynamic_cast、 const_cast、 reinterpret_cast类型转换
(1)static_cast在编译时执行类型转换,在进行指针或引用类型转换时,需要自己保证合法性。如果想要运行时类型检查,可以使用dynamic_cast进行安全的向下类型转换
(2)dynamic_cast在C++中主要应用于父子类层次结构中的安全类型转换。
它在运行时执行类型检查,因此相比于static_cast,它更加安全
dynamic_cast <new_type> (expression)
(3)const_cast
new_type 必须是一个指针、引用或者指向对象类型成员的指针。当需要使用const对象调用非const成员函数时,可以使用const_cast删除对象的const属性
const_cast <new_type> (expression)
(4) reinterpret_cast
reinterpret_cast用于在不同类型之间进行低级别的转换。
reinterpret_cast <new_type> (expression)
Lamada表达式
Lambda 表达式是一种匿名函数,可以在需要的地方内联定义函数,不用单独声明命名函数。作为回调函数时,常用。
[capture](parameters) -> return_type {
// 函数体
}
捕获列表 (capture):定义哪些外部变量可以在 lambda 体内使用
参数列表 (parameters):与普通函数参数类似
返回类型 (return_type):可选的,编译器通常可以自动推导
函数体:包含 lambda 要执行的代码
map、set是怎么样实现的,红黑树是怎么样能够同时实现这两种容器?为什么使用红黑树?
(1)底层都是以红黑树的结构实现,插入删除等操作都在对数级别O(log n)完成的,比较高效;
(2)通过定义了模版特化和仿函数,使用同一棵红黑树:基于模板的RBTree<Key, Value, KeyOfValue, Compare>。但是不同节点数据:set:Value = Key,直接存值,map:Value = pair<const Key, T>,存键值对。通过仿函数KeyOfValue从Value中提取比较键,set用Identity:直接返回值,map用SelectFirst。
(3)因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低。
了解RAII吗?
RAII全称是“Resource Acquisition is Initialization”,“资源获取即初始化”, 资源生命周期与对象生命周期绑定,简单来说就是对象构造时获取资源,析构时自动释放资源。智能指针(unique_ptr、shared_ptr、weak_ptr)是RAII的典型实现。
如有问题或建议,欢迎在评论区中留言~