面向对象

特性

  • 封装:隐藏内部实现细节,提供对外接口
  • 继承:子类继承父类,实现代码复用与扩展
  • 多态:同名方法不同实现
    • 静态多态(编译时多态):通过函数重载、运算符重载、模板实现
    • 动态多态(运行时多态):通过虚函数指针/引用实现

EBO 机制

  • 一个空类 A,它的 sizeof(A) 是多少?

    结果为 1,C++ 规定每个对象必须具有唯一的地址,因此即使为空类,也至少分配 1 个字节

  • 如果还有一个空类 B 继承 A 呢?sizeof(B) 是多少?

    sizeof(B) = 1,当一个空类作为基类被继承时,编译器会将它的空间优化掉,这就是 EBO 机制

  • 菱形继承(未解决二义性)四中个都为空类,派生类 D 的 sizeof 是多少?

    sizeof(D) = 1,派生类有两个 A 子对象,但它们占的空间都被优化掉了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class A {};
    class B : public A {};
    class C : public A {};
    class D : public B, public C {};

    int main(void)
    {
    A objA;
    B objB;
    C objC;
    D objD;

    int a = sizeof(objA);
    int b = sizeof(objB);
    int c = sizeof(objC);
    int d = sizeof(objD);

    // 输出为 1 1 1 1
    cout << a << ' ' << b << ' ' << c << ' ' << d << endl;

    return 0;
    }
  • 菱形继承(解决二义性)四中个都为空类,派生类 D 的 sizeof 是多少?

    由于虚表指针的存在,64位系统下 sizeof(D) = 16

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class A {};
    class B : virtual public A {};
    class C : virtual public A {};
    class D : public B, public C {};

    int main(void)
    {
    A objA;
    B objB;
    C objC;
    D objD;

    int a = sizeof(objA);
    int b = sizeof(objB);
    int c = sizeof(objC);
    int d = sizeof(objD);

    // 输出为 1 8 8 16
    cout << a << ' ' << b << ' ' << c << ' ' << d << endl;

    return 0;
    }



基础

栈溢出

拷贝

  • 深拷贝:不仅拷贝对象的成员变量,还会申请新的内存空间,并复制指针所指向的内容

    ​ 两个对象互相独立,互不影响

  • 浅拷贝:拷贝对象的成员变量,若其为指针,只拷贝指针地址,而非指针指向内容

    共享同一份资源,容易产生悬垂指针或者 Double Free 这种未定义行为

  • 移动拷贝:转移资源所有权,规避深浅拷贝的问题

拷贝后修改原对象,拷贝对象是否会改变?

深拷贝:不会改变,因为二者独立

浅拷贝:若为值对象,不会改变;若为指针,会跟着改变,因为二者共用一块内存



引用

是否占空间?

  • 类成员引用:占 1 个指针大小空间,因为需要持久绑定在某个对象上
  • 函数参数引用:占 1 个指针大小空间,
  • 普通局部引用:不占,底层的指针由汇编实现

底层实现原理

类成员引用:汇编层面在对象内存中存储一个指针,通过解引用访问被引用对象

函数参数引用:汇编层面会将原对象的地址放到寄存器/栈上,将其传给函数

普通局部引用:编译器直接展开引用为原对象的访问

引用与指针的区别

  • 引用声明时必须初始化,指针不用
  • 引用不可初始化为空,指针可以
  • 引用初始化后不可修改,指针可以
  • 引用是原对象的别名,指针是独立的变量
  • sizeof:引用的结果与原对象相同,指针为 4 或 8 字节


extern




STL

容器



sort



迭代器

遍历删除元素,需要注意什么?




新特性

智能指针

auto_ptr

C++98 引入,11 弃用

早期的独占指针,但其在拷贝后会交换所有权,导致原指针变为悬垂指针,故被 unique_ptr 替代


scoped_ptr

Boost 库中支持,更严格的 unique_ptr,不支持资源所有权转移、不可移动、也不支持 STL 容器


unique_ptr

通过将拷贝构造拷贝赋值绑定为 delete 实现独占;通过将对象生命周期作用域绑定实现 RAII

支持移动构造移动赋值转移所有权


shared_ptr

通过控制块实现引用计数,当最后一个 shared_ptr 被销毁时,它所管理的资源才会被释放

线程安全问题

  • 引用计数是原子操作,保证线程安全

    1
    2
    3
    4
    std::atomic<int> count;

    count.fetch_add(1, std::memory_order_relaxed); // 增加引用计数
    count.fetch_sub(1, std::memory_order_acq_rel); // 减少引用计数
  • shared_ptr 和所管理的对象并不保证线程安全,多线程下要加锁进行同步

与 make_shared 的区别

  • make_shared

    在一连续的堆内存中,同时分配对象控制块。只 new 一次,更节省内存且更高效

    对象控制块绑定在一起,需要一起释放

    不支持自定义删除器

  • shared_ptr

    先给对象分配内存,再给控制块分配内存,需要 new 两次

    对象控制块可以分开释放

    支持自定义删除器

enable_shared_from_this

用于从对象内部拿到一个 shared_ptr

  • 使用 shared_ptr 会开一个新的引用计数,最终对象会被 delete 两次(导致系统崩溃)
  • shared_from_this 从内部隐藏的 weak_ptr 提升出一个 shared_ptr,加入当前对象的控制块,保证 引用计数正确

简化代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename T>
class enable_shared_from_this
{
private:
mutable std::weak_ptr<T> weak_this; // 保存一个弱引用指向自己

public:
std::shared_ptr<T> shared_from_this()
{
return std::shared_ptr<T>(weak_this); // 提升成 shared_ptr
}

friend class std::shared_ptr<T>; // 允许 shared_ptr 内部设置 weak_this
};

weak_ptr

观察但不拥有资源,用于打破 shared_ptr 的循环引用,必须与 shared_ptr 配套出现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <memory>

struct B; // 前向声明

struct A
{
std::shared_ptr<B> ptr_to_b;
~A() { std::cout << "A destroyed\n"; }
};

struct B
{
// 如果这里是 shared_ptr,会导致循环引用
std::weak_ptr<A> ptr_to_a; // 改成 weak_ptr 解决循环引用
~B() { std::cout << "B destroyed\n"; }
};

int main()
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();

a->ptr_to_b = b;
b->ptr_to_a = a; // weak_ptr 不增加引用计数

return 0;
}

对比裸指针

智能指针 裸指针
内存管理 自动释放资源,避免泄漏和重复释放 手动管理,容易泄漏或双重释放
所有权表达 明确表达“拥有”、“共享”或“观察”的语义(如 unique_ptr / shared_ptr / weak_ptr 无所有权语义,容易误用
线程安全 shared_ptr 是线程安全的(引用计数),但会牺牲性能 非线程安全,需手动加锁
容器兼容性 unique_ptr / shared_ptr 可与 STL 容器无缝配合 可配合使用,但要手动管理释放
性能开销 引入一定开销:构造、析构、移动、引用计数(特别是 shared_ptr 性能极轻量,无额外开销
循环引用风险 shared_ptr 容易产生循环引用,需配合 weak_ptr 没有引用计数,不会循环引用
内存布局/碎片 存在控制块(特别是 shared_ptr),可能导致额外内存开销或碎片 紧凑、内存结构清晰
使用场景 推荐用于拥有动态资源的类和结构体,RAII 场景 推荐用于轻量访问、非拥有资源场景


Lambda 与捕获列表



move 语义

将左值强转为右值,调用移动构造函数,转移资源所有权

1
2
3
4
5
template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}



内存管理

内存模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
高地址

├── 栈区(Stack)
│ - 函数调用相关:局部变量、返回地址、参数等
│ - 自动分配和释放,后进先出(LIFO)

├── 堆区(Heap)
│ - 动态分配内存(malloc/new 等)
│ - 需要程序员手动释放(free/delete)

├── BSS段(未初始化的全局变量)

├── 数据段(已初始化的全局变量/静态变量)

├── 只读数据段(常量区)
│ - 字符串常量、const 修饰的全局常量等

├── 代码段(Text Segment)
│ - 存放程序的机器指令

└──
低地址



内存对齐

规则:结构体整体大小是最大对齐值的倍数,可用#pragam pack(n)限制最大对齐值为 n

好处

  • 提高访问效率:CPU 按字长来读取内存,对齐后 CPU 可以一次性读取完成
  • 跨平台支持:保证不同平台和编译器之间二进制兼容
  • 硬件限制:某些平台不支持非对齐访问,否则系统会崩溃