C++
基础语法
C和C++区别
C:过程式(Procedural)以函数为中心、数据与函数分离、通过结构体 + 函数模拟数据封装
C++是C的超集,具备C的功能,并且引入了面向对象思想,引入了类这个概念。具备一些特性,例如虚函数、模版、C++ 在保持底层能力的同时,通过 模板和面向对象机制提升抽象能力
static与const关键字、指针常量和常量指针
static: 限制作用域与生命周期
修饰全局变量(只能在文件内不使用,不能其他地方调用)、
修饰局部变量(函数内部,不会重新初始化,复用数值,在第一次调用函数后初始化、一致保留)
修饰函数(只能在文件内使用该函数)
类内:修饰变量(类共有)、修饰函数(类共有,并且不能使用非静态的成员)
const: 限制是否可变
修饰变量:变量不能修改值
函数参数:函数内部不能修改
返回值:返回的值不能够修改内容
成员函数:放在成员函数后,成员变量值不能修改
指针常量和常量指针
1 | int a = 4; |
结构体和类的区别
C语言结构体只能使用POD类型,没有继承虚函数等机制。
C++中结构体和类都具备继承机制、依赖于
访问权限: struct内部变量默认为public,class为private
继承: struct继承默认使用共有继承,class默认使用私有继承
字节对齐
https://blog.csdn.net/2501_93249729/article/details/159347139
- 首地址规则: 结构体第一个成员的地址与结构体变量的起始地址相同(偏移量为0)。
- 对齐规则: 从第二个成员开始,每个成员变量都要对齐到其“对齐数”的整数倍地址处。
对齐数 = 编译器默认对齐数与该成员自身大小的较小值。
如果前一个成员结束后的地址不满足当前成员的对齐要求,编译器会在中间插入填充字节。
- 总大小规则: 结构体的总大小必须是该结构体中“最大对齐数”的整数倍。如果不满足,编译器会在结构体末尾填充字节。
1 | struct __tt__ { |
字节序: 大端和小端
高位字节存储在低地址处,低位字节存储在高地址处。例如,一个4字节的整数0x12345678,在大端字节序的系统中,内存布局如下(从左侧的低地址到右侧的高地址)注意,一般个人PC使用小端
1 | 一个4字节的整数0x12345678,在大端字节序的系统中,内存布局如下(从左侧的低地址到右侧的高地址): |
指针和引用
指针是变量、用于存储地址位置。所有类型的指针变量的大小相同,4或8根据系统是32还是64有关。
引用是别名,定义时必须初始化
指针可以为nullptr,表示不指向任何变量。且可以修改其值,并且可以解应用修改指向变量的值
define、typedef、inline
define宏定义,字符串直接替换,不涉及类型检查,需要注意操作符大小
typedef遵循 C++ 的作用域规则,可以受到命名空间、类等结构的作用域限制。typedef 通常用于定义复杂类型的别名,使代码更易读和易于维护,typedef 可以与模板结合使用,但在 C++11 之后,推荐使用 using 关键字定义模板类型别名。
inline修饰函数,会检查类型。由编译器进行函数优化,是否内联取决于编译器的实现和优化策略。
extern C
使用 extern "C" 声明的函数或变量会采用 C 语言的链接规则,即符号的名称和调用约定与 C 语言相同。
对于 C++ 语言,由于需要支持重载,所以一个函数的链接名(Linkage Name)是由函数的名称、参数类型和返回值类型等信息组成的,用于在编译和链接时唯一标识该函数。
volatile、explicit关键字
volatile是 C 语言中的一个关键字,用于修饰变量,表示该变量的值可能在任何时候被外部因素更改,例如硬件设备、操作系统或其他线程。
explicit 通常用于构造函数的声明中,用于防止隐式转换。这种机制可以防止编译器自动执行预期外的类型转换,提高代码的安全性。(当将一个参数传递给构造函数时,如果构造函数声明中使用了 explicit 关键字,则只能使用显式转换进行转换,而不能进行隐式转换。)
面向对象
面向对象的三大特性
封装、继承、多态
封装:将客观事物封装成为抽象的类, 类把自己数据和方法进行隐藏,仅对外公开接口来和对象进行交互,防止外界干扰或不确定性访问。
继承:指一个类(称为子类或派生类)可以从另一个类(称为父类或基类)中继承属性和行为的能力。通过继承,子类可以重用父类的代码,并且可以在不修改父类的情况下添加新的功能或修改已有的功能。继承使得代码具有层次性和可扩展性,能够建立起类之间的层次关系。
多态:多态是指同一个操作作用于不同的对象时,可以有不同的解释和行为。多态性允许以统一的方式处理不同类型的对象,从而提高了代码的可扩展性和可维护性。在C++中,多态性通常通过虚函数来实现。通过基类中定义虚函数,并在派生类中重新定义该函数,可以实现运行时的动态绑定。
重写、重载、多态
重载:在同一个类或命名空间中,声明多个同名函数, 但是参数列表不同。编译器根据参数的类型、数量或顺序来区分不同的函数。
重写:重写发生在继承体系中,在子类中,声明一个与父类中虚函数具有相同名称、相同参数列表和相同返回类型的函数,并在子类函数前加上 override 关键字。
重写与重载区别:
- 作用域:重载发生在同一个作用域内,而重写发生在继承体系中。
- 参数列表:对于重载的函数,参数列表必须不同;对于重写的函数,参数列表必须与被重写的函数完全相同。
- 返回类型:重载函数的返回类型可以不同,但重写函数的返回类型必须与被重写的函数相同(或与之兼容,C++中称为协变返回类型)。
- 虚函数:重写通常与虚函数一起使用,以实现运行时多态性;而重载是编译时多态性,由编译器在编译期间确定调用哪个函数。
- 关键字:重写函数需要使用 override 关键字,明确指出该函数是对父类虚函数的重写。
多态
静态多态(编译时多态):重载、模版,编译期确定
动态多态(运行时多态):虚函数(重写),运行时确定
虚函数实现(虚函数表与虚指针)、纯虚函数
虚函数实现依赖虚函数表和虚函数指针
虚函数表的创建: 当一个类包含虚函数时,编译器会自动为这个类创建一个虚函数表。这个表是一个函数指针数组,每个指针指向一个虚函数的实现。
虚函数表指针: 编译器会在对象的内存布局中添加一个隐式的虚函数表指针(通常是一个指向 vtable 的指针),这样每个对象都可以通过这个指针访问到类的虚函数表。
虚函数的声明: 在类中声明虚函数时,可以使用 virtual 关键字。如果一个函数被声明为虚函数,编译器会在类的 vtable 中为这个函数分配一个入口。
重写虚函数: 当从基类继承并创建派生类时,可以在派生类中重写基类的虚函数。重写的函数会替换掉 vtable 中对应的基类实现。
动态绑定: 当通过基类指针或引用调用虚函数时,程序会使用对象的虚函数表指针来查找正确的函数实现。这个过程称为动态绑定或晚期绑定。
调用虚函数: 程序运行时,当调用一个虚函数时,会先通过对象的虚函数表指针找到 vtable,然后在 vtable 中查找对应的函数指针,并调用该函数。
简短来说,每个类都有一个虚表,里面有这个类的虚函数地址;每个对象都有指向它的类的虚表的指针,这个指针称为虚指针。 当调用虚函数时,编译器会调用对象的虚指针查找虚表,通过虚函数的地址来执行相应的虚函数。
构造和析构函数,是否可以为虚函数
构造函数不能是虚函数。
构造函数在对象创建时被调用,此时不涉及多态性。
虚函数对应一个虚表,这个表存在对象的内存空间,如果此时构造函数是虚函数,对象还没实例化没有分配内存空间,也就无法调用;
虚函数是用在信息不全的情况下,能使重载的函数使用。但构造函数本身就是要初始化对象,因此没必要是虚函数。
析构函数可以是虚函数。
虚析构函数可以在运行时实现多态性;
如果基类的析构函数不是虚函数,当通过基类指针去删除派生类对象时,不会调用派生类的析构函数。可能会导致派生类的资源未被正确释放,从而造成资源泄漏
菱形继承、虚继承
菱形继承是面向对象编程中多继承的一种特殊情况,主要出现在一个类同时继承两个或多个子类,而这些子类又继承自同一个基类的场景。这种继承关系在类图中形成一个菱形结构,因此得名。
菱形继承会引发以下两个主要问题:
- 数据冗余:派生类会从不同路径继承基类的多个副本,导致内存浪费和数据不一致。例如,基类的成员变量可能在派生类中存在多份,修改其中一个副本不会影响其他副本。
- 成员访问二义性:当派生类访问基类的成员时,编译器无法确定应该调用哪一个路径继承的成员,导致编译错误或警告。
为了解决上述问题,C++ 提供了虚拟继承机制。通过虚拟继承,基类的多个副本被合并为一个共享实例,从而消除了数据冗余和二义性。
内存管理
虚拟内存、进程的内存布局
.text代码段,.data段,.bss, 堆区、栈区、内核
new和malloc、delete和free
总结来说,new和malloc都是动态内存分配的手段,但new提供了类型安全和构造/析构的自动化,而malloc则提供了更底层的内存分配方式,需要手动管理构造和析构。在C++中,推荐使用new来分配对象,以保持类型安全和自动化的资源管理。
总结来说,delete和free都是用来释放动态内存的,但它们分别用于C++和C语言中的内存管理。delete适用于C++对象,会自动调用析构函数;而free适用于C语言分配的内存,不涉及对象的析构。
深拷贝和浅拷贝
浅拷贝:只复制对象的成员变量值。对于指针成员,复制的是地址本身,两个对象的指针指向同一块堆内存。
深拷贝:为指针成员重新分配内存并复制内容,两个对象各自拥有独立的堆内存,互不影响。
核心问题:浅拷贝导致两个对象共享同一块堆内存,任何一个对象析构时 delete 这块内存,另一个对象的指针就变成悬垂指针——再次 delete 就是双重释放,程序崩溃。
内存泄漏、检测和预防
1如果程序的某一段代码在内存池中动态申请了一块内存而没有及时将其释放,就会导致那块内存一直处于被占用的状态而无法使用,造成了资源的浪费。内存泄漏并不是说物理上的消失掉了,是因为无法使用该区域,在外界看来这块内存就好像被泄漏了一样。 2什么操作会导致内存泄漏 ○忘记释放内存:使用 new 或 malloc 等分配内存后,没有使用 delete 或 free 释放内存。 ○子类继承父类时,没有将基类的析构函数定义为虚函数。 ○野指针:指针被赋值为 nullptr 或重新赋值后,丢失了对先前分配内存的引用,导致无法释放。 ○循环引用:在使用引用计数的智能指针(如 std::shared_ptr)时,循环引用会导致引用计数永远不会归零,从而无法释放内存。 ○使用不匹配的内存释放函数: 使用 delete 释放由 new[] 分配的内存,或使用 delete[] 释放由 new 分配的内存,这可能导致未定义行为。 ○资源未关闭:对于文件、网络连接等资源,如果没有正确关闭,虽然不直接导致内存泄漏,但会占用系统资源,可能导致资源耗尽。 3如何检测:使用工具如Valgrind、AddressSanitizer或Visual Studio的诊断工具来检测内存泄漏。 4如何避免 ○使用智能指针:优先使用 std::unique_ptr、std::shared_ptr 等智能指针来自动管理内存。 ○确保资源释放: 对于手动分配的内存,确保在不再需要时使用 delete 或 free 释放。 ○内存泄漏检测工具: 在开发和测试阶段,定期使用内存泄漏检测工具检查程序。
野指针和空悬指针、内存错误(coredump和segmentation fault)
野指针是指“指向已经被释放的或无效的内存地址的指针”。在 C 和 C++ 这类允许直接操作内存地址的语言中,如果指针没有被正确初始化,或者指针所指向的内存已经被释放,那么这个指针就成为了野指针。使用野指针可能会导致程序崩溃、数据损坏或者其他一些不可预测的行为。
垂悬指针是指指针指向的内容已被释放,指针指向的对象的生命周期已结束。
现代C++
三个智能指针,各自原理实现,是否线程安全
智能指针是C++中用来自动管理动态分配内存的一种模板类,维护着一个指向动态分配对象的指针,并在智能指针对象被销毁时,自动释放该内存,从而避免内存泄漏。 C++有以下几种智能指针:
●std::unique_ptr:独占式拥有指针,保证同一时间只有一个unique_ptr指向特定内存,适用于不需要共享所有权的场景,如栈上对象的管理。 ●std::shared_ptr:多个shared_ptr可以共享同一内存,使用引用计数机制来管理内存的生命周期,适用于多个对象需要共享同一个资源的场景。 ●std::weak_ptr:弱引用,用于解决shared_ptr可能导致的循环引用问题,它不拥有所指向的对象。 智能指针的实现原理是什么 1std::unique_ptr ○unique_ptr代表独占所有权的智能指针,同一时间只能有一个unique_ptr实例指向特定资源。 ○它通过析构函数来管理资源的释放。当unique_ptr超出作用域时,会自动调用删除操作符来释放其指向的内存。 ○std::unique_ptr 通过删除复制构造函数和复制赋值运算符来确保所有权的唯一性,但提供移动构造函数和移动赋值运算符,允许所有权的转移。 2std::shared_ptr ○shared_ptr允许多个指针实例共享对同一资源的所有权,使用引用计数机制来跟踪有多少个shared_ptr指向同一资源。 ○内部维护一个控制块,通常包含引用计数和资源的原始指针。每当创建一个新的shared_ptr或将一个shared_ptr赋值给另一个时,引用计数增加。 ○当shared_ptr被销毁或通过标准库提供的reset成员函数重置时,引用计数减少。当引用计数降到零时,控制块会释放资源并自我销毁。 3std::weak_ptr ○std::weak_ptr 是一种不拥有对象的智能指针,它观察 std::shared_ptr 管理的对象,但不增加引用计数。 ○它用于解决 std::shared_ptr 之间可能产生的循环引用问题,因为循环引用会导致引用计数永远不会达到零,从而造成内存泄漏。
左值和右值、右值引用和移动语义、完美转发
左值是指在内存中有确定地址的对象,可以通过引用或取地址符 & 操作。左值通常是变量或持久存在的对象。
右值是临时存在的值,通常是表达式的结果或字面量。右值不能取地址,生命周期短暂。
1 | int x = 10; // x 是左值 |
lambda函数,捕获机制
在 C++ 中,lambda 函数是一种轻量级、匿名的函数对象,可以在定义时直接捕获外部作用域的变量,常用于需要回调、算法传参或临时函数对象的场景。
1 | auto lambda = [](int x, int y) -> int { |
Lambda 可以捕获外部作用域的变量,让 lambda 内部访问这些变量:
值捕获 [=]**\引用捕获** [&]**\指定变量捕获\隐式与显式混合**
可变 lambda (**mutable**)
类型萃取
在C++中,类型萃取(type_traits)是一种编译时技术,用于在编译期间获取和操作类型的信息。
nullptr与NULL
STL
STL基本概念、六大组件
STL是指标准模板库(Standard Template Library),是C++区别于C语言的特征之一。
六大部分分别是容器(container)、迭代器(iterator)、适配器(adaptor)、分配器(allocator)、仿函数(functor)和算法(algorithm)。
vector
扩容机制,如何防止频繁扩容(reserve、allocator)
当向vector中插入元素时,如果元素有效个数size与空间容量capacity相等时,vector内部会触发扩容机制:开辟新空间,拷贝,释放旧空间。1.5倍或者2倍扩容。
防止扩容方法
reserve, 预先分配大小,保证后续不会触发扩容。(不改变size大小,不构造元素,只分配底层缓冲区)
与resize区别:(直接改变size大小,如果
n > size():新增元素会默认构造(或使用提供的默认值);如果n < size():末尾元素被销毁)修改allocator默认内存分配方式
size、resize、reserve、capacity的区别
vector可能导致其迭代器失效的操作有哪些
push_back和emplace_back
push_back:将一个 已经存在的对象 拷贝或移动到容器末尾。
1 | v.push_back(obj); // 拷贝 obj |
emplace_back: 在容器末尾 原地构造元素,直接调用构造函数,避免额外拷贝/移动。
1 | v.emplace_back(args...); // 直接传递构造函数参数 |
map和unordered_map
map有序O(logN),空间占用率高、unordered_map无序O(1),建表费时
map底层: 红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树)
unordered_map底层:Hash
hash解决冲突方法
- 链地址法
- 开放寻址法 (线性探查、二次探查、双重哈希)
- 再哈希法(容量增加时,重新计算所有元素的哈希值并放入新的桶中)
set
- set是一种关联式容器,其特性如下:
- set以RBTree作为底层容器
- 所得元素的只有key没有value,value就是key
- 不允许出现键值重复
- 所有的元素都会被自动排序
- 不能通过迭代器来改变set的值,因为set的值就是键,set的迭代器是const的
- map和set一样是关联式容器,其特性如下:
- map以RBTree作为底层容器
- 所有元素都是键+值存在
- 不允许键重复
- 所有元素是通过键进行自动排序的
- map的键是不能修改的,但是其键对应的值是可以修改的
综上所述,map和set底层实现都是红黑树;map和set的区别在于map的值不作为键,键和值是分开的。