C++重点知识回顾

C++的一些概念复杂而且存在一些容易使用错误的地方,这里进行部分总结.

面向对象

多态

C++中的多态是指同一个函数或者操作在不同的对象上有不同的表现形式。

C++实现多态的方法主要包括虚函数、纯虚函数和模板函数

其中虚函数、纯虚函数实现的多态叫动态多态,模板函数、重载等实现的叫静态多态。

区分静态多态和动态多态的一个方法就是看决定所调用的具体方法是在编译期还是运行时,运行时就叫动态多态。

虚函数、纯虚函数实现多态

在 C++ 中,可以使用虚函数来实现多态性。

虚函数是指在基类中声明的函数,它在派生类中可以被重写。

当我们使用基类指针或引用指向派生类对象时,通过虚函数的机制,可以调用到派生类中重写的函数,从而实现多态。

C++ 的多态必须满足两个条件:

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数是虚函数,且必须完成对基类虚函数的重写

虚函数表

虚函数是通过一张虚函数表(Virtual Table)来实现的,简称为V-Table。在这个表中,存放的是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。

这个类的实例内存中都有一个虚函数表的指针,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

在底层,当一个类声明一个虚函数时,编译器会为该类创建一个虚函数表(Virtual Table)。
这个表存储着该类的虚函数指针,这些指针指向实际实现该虚函数的代码地址。

每个对象都包含一个指向该类的虚函数表的指针,这个指针在对象创建时被初始化,通常是作为对象的第一个成员变量。

当调用一个虚函数时,编译器会通过对象的虚函数指针查找到该对象所属的类的虚函数表,并根据函数的索引值(通常是函数在表中的位置,编译时就能确定)来找到对应的虚函数地址。

然后将控制转移到该地址,实际执行该函数的代码。对于派生类,其虚函数表通常是在基类的虚函数表的基础上扩展而来的。在派生类中,如果重写了基类的虚函数,那么该函数在派生类的虚函数表中的地址会被更新为指向派生类中实际实现该函数的代码地址。

C++的动态多态必须满足两个条件:

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数是虚函数,且必须完成对基类虚函数的重写

其中第一条很重要,当我们使用派生类的指针去访问/调用虚函数时,实际上并未发生动态多态,因为编译时就能确定对象类型为派生类型,然后直接生成调用派生类虚函数的代码即可,这种叫做静态绑定。通过基类的指针或引用调用虚函数才能构成多态,因为这种情况下运行时才能确定对象的实际类型,这种称为动态绑定

纯虚函数

纯虚函数是一种在基类中声明但没有实现的虚函数。

它的作用是定义了一种接口,这个接口需要由派生类来实现。

包含纯虚函数的类称为抽象类(Abstract Class)。抽象类仅仅提供了一些接口,但是没有实现具体的功能。作用就是制定各种接口,通过派生类来实现不同的功能,从而实现代码的复用和可扩展性。

另外,抽象类无法实例化,也就是无法创建对象。原因很简单,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

虚函数使用了一种称为虚函数表(vtable)的机制。然而,在调用构造函数时,对象还没有完全创建和初始化,所以虚函数表可能尚未设置。

这意味着在构造函数中使用虚函数表会导致未定义的行为。只有执行完了对象的构造,虚函数表才会被正确的初始化。

内存管理

RAII

资源获取即初始化(Resource Acquisition Is Initialization,简称 RAII)是一种 C++ 编程技术,它将在使用前获取(分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥量、磁盘空间、数据库连接等有限资源)的资源的生命周期与某个对象的生命周期绑定在一起。

确保在控制对象的生命周期结束时,按照资源获取的相反顺序释放所有资源。

同样,如果资源获取失败(构造函数退出并带有异常),则按照初始化的相反顺序释放所有已完全构造的成员和基类子对象所获取的资源。

这利用了核心语言特性(对象生命周期、作用域退出、初始化顺序和堆栈展开),以消除资源泄漏并确保异常安全。

在实际的 C/C++ 开发中,我们经常会遇到诸如 coredump、segmentfault 之类的内存问题,使用指针也会出现各种问题,比如:

  • 野指针:未初始化或已经被释放的指针被称为野指针
  • 空指针:指向空地址的指针被称为空指针
  • 内存泄漏:如果在使用完动态分配的内存后忘记释放,就会造成内存泄漏,长时间运行的程序可能会消耗大量内存。
  • 悬空指针:指向已经释放的内存的指针被称为悬空指针
  • 内存泄漏和悬空指针的混合:在一些情况下,由于内存泄漏和悬空指针共同存在,程序可能会出现异常行为。

智能指针是一种可以自动管理内存的指针,它可以在不需要手动释放内存的情况下,确保对象被正确地销毁。

这种指针可以显著降低程序中的内存泄漏和悬空指针的风险。

在C++中,智能指针常用的主要是两个类实现:

  • std::unique_ptr
  • std::shared_ptr

智能指针是一种可以自动管理内存的指针,它可以在不需要手动释放内存的情况下,确保对象被正确地销毁。这种指针可以显著降低程序中的内存泄漏和悬空指针的风险。

std::unique_ptr是一个独占所有权的智能指针,它保证指向的内存只能由一个unique_ptr拥有,不能共享所有权。

当unique_ptr超出作用域时,它所指向的内存会自动释放。

1
2
3
4
5
6
7
8
9
#include <memory>
#include <iostream>

int main() {
std::unique_ptr<int> ptr(new int(10));
std::cout << *ptr << std::endl; // 输出10
// unique_ptr在超出作用域时自动释放所拥有的内存
return 0;
}

std::shared_ptr是一个共享所有权的智能指针,它允许多个shared_ptr指向同一个对象,当最后一个shared_ptr超出作用域时,所指向的内存才会被自动释放。

举个栗子:

1
2
3
4
5
6
7
8
9
10
#include <memory>
#include <iostream>

int main() {
std::shared_ptr<int> ptr1(new int(10));
std::shared_ptr<int> ptr2 = ptr1; // 通过拷贝构造函数创建一个新的shared_ptr,此时引用计数为2
std::cout << *ptr1 << " " << *ptr2 << std::endl; // 输出10 10
// ptr2超出作用域时,所指向的内存不会被释放,因为此时ptr1仍然持有对该内存的引用
return 0;
}

总的来说,智能指针可以提高程序的安全性和可靠性,避免内存泄漏和悬空指针等问题。

但需要注意的是,智能指针不是万能的,也并不是一定要使用的,有些场景下手动管理内存可能更为合适

参考资料

  1. 一文搞懂 C/C++ 面试重点知识和常见面试题(2025 年更新) | 编程指北-计算机学习指南
-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道