C++
面向对象和面向过程
面向过程
面向过程编程更关注的是过程,即一系列步骤,把程序设计成一步一步解决问题的方式。这种编程方式把程序看作是由一组函数或过程组成的,每个函数完成一个具体的任务,数据则在这些函数之间传递。
C++ 语言涵盖面向对象三大特性:封装、继承、多态。多态通过虚函数实现动态绑定。内存管理中,堆栈区分明确,new/delete 与 malloc/free 机制不同。智能指针如 unique_ptr、shared_ptr、weak_ptr 解决内存泄漏问题。C++11 新增 lambda 表达式、内联函数及函数模板提升开发效率与安全性。掌握这些核心知识点有助于深入理解 C++ 底层机制及应对技术面试。

面向过程编程更关注的是过程,即一系列步骤,把程序设计成一步一步解决问题的方式。这种编程方式把程序看作是由一组函数或过程组成的,每个函数完成一个具体的任务,数据则在这些函数之间传递。
面向对象编程则更多地关注'对象',即如何将现实中的事物抽象成程序中的对象。对象既包含数据(称为属性),也包含操作这些数据的方法(称为方法)。面向对象的思想是将数据和操作封装在一起,通过对象之间的交互来实现程序的功能。
简单来说,面向过程是把程序看作是由函数组成的一步步过程,而面向对象则是将程序中的元素视为'对象'。
预处理 -> 编译 -> 汇编 -> 链接
汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个.cpp 文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序.exe 文件。
链接是将所有的.o 文件和库(动态库、静态库)链接在一起,得到可以运行的可执行文件(Windows 的.exe 文件或 Linux 的.out 文件)等。
面向对象的三大特征之一,简单来说就是不同对象调用同一行为表现出的不同形式和结果。多态可以理解成是一个接口多种实现。
多态分为静态多态和动态多态。
在基类函数名前面加上 virtual 关键字的就是被声明为虚函数。虚函数是动态多态,在运行期间父类指针指向实际对象的类型,在运行期间根据对象类型确定调用的是哪个虚函数的,运行时在虚函数表中寻找调用的虚函数地址。
从底层实现来看,C++ 中的虚函数是通过虚函数表和虚函数指针来实现的。假如这个类中有虚函数,那么会给这个类提供一个虚函数表,这个表中存储了该类中所有虚函数的地址,每个对象都有一个指向虚表的虚表指针,然后通过虚表指针找到虚表,进而通过找到函数在表中的位置实现函数的正确调用。然后当我们子类继承父类的话,它也会把这个父类这个虚函数表给继承下来。如果我们子类重写父类的虚函数的话,它会把父类那个虚函数表中的虚函数地址进行替换,从而达到运行时实现多态。然后确定调用子类的函数。
(虚表指针是在创建对象时候初始化指向对应的虚函数表)
把父类的析构函数设置为虚析构,解决了能够正确释放子类的资源。
因为在创建对象时,首先要调用构造函数来初始化对象,而虚函数机制依赖于虚函数表指针,在构造函数执行之前对象还未完全构建,虚函数表指针还不存在,所以构造函数不能是虚函数。
引用是给变量起一个别名,在创建引用时就要初始化绑定一个变量。
因为它的定义就是这样,然后引用还不同于指针,引用一旦创建就不能更改其指向,所以初始化引用的时候必须指定一个有效的对象。
堆是由程序员手动申请和释放的,可以使用 new 和 malloc 申请,释放使用 delete 和 free.
const 在类型前):指针可以指向不同的地址,但不能通过指针修改所指向的对象。const 在 * 号后):指针一旦初始化后,不能指向其他地址,但可以通过指针修改所指向的对象(前提是对象本身不是常量)。const 在两侧):指针不能改变指向,且不能通过指针修改所指向的对象。在 C 语言中,NULL 通常定义为 (void *)0,因为 C 允许将 void* 类型的指针隐式转换为任何其他类型的指针。这样,NULL 可以赋值给任何指针类型,编译器不会报错。
在 C++ 中,NULL 被定义为 0。C++ 比 C 对类型转换的检查更严格,不允许 void* 自动转换为其他类型的指针。用 0 来表示 NULL 是为了避免这些类型转换问题,因为 0 可以被看作是任何指针类型的空指针。
nullptr 有一个专门的类型 std::nullptr_t,不会被误用为整数或其他类型的指针。nullptr 可以明确表示这是一个空指针,而不是整数 0,提高代码的可读性和维护性。函数名与类名相同,无返回值也不写 void。创建对象时自动调用构造函数。没有实现构造函数时编译器会提供一个默认的无参构造函数。构造函数是给成员变量赋值的,不是初始化;初始化参数列表是给成员变量初始化的。
拷贝构造的参数不是引用会导致无限递归。加 const 是为了避免通过形参修改实参。
浅拷贝是简单的赋值操作。深拷贝是拷贝相同大小相同内容到堆区。
析构函数名与类名相同前面加~,无参数,无返回值,无 void。销毁对象时自动调用。未实现编译器会提供默认的析构。析构函数是释放对象里面成员变量所指向的堆区内存的。
new 运算符申请内存会先调用 malloc 再调用构造函数。释放对象申请的内存空间用 delete,delete 会先调用析构函数再调用 free。delete[] 用于释放数组对象占用的内存。它会依次调用数组中每个对象的析构函数来清理资源。
new 和 delete 是用于动态内存分配和释放的运算符。new 用于创建对象时,会自动调用对象的构造函数进行初始化,而 delete 则用于释放 new 分配的内存,并调用对象的析构函数。这样可以确保对象的生命周期管理是安全和正确的。malloc 和 free 来进行动态内存管理。malloc 分配一块指定大小的内存空间,并返回一个指向该内存的 void* 指针,而 free 则用于释放 malloc 分配的内存。需要注意的是,使用 malloc 分配的内存需要手动管理对象的构造和析构,因为它不会自动调用构造函数或析构函数。new 运算符可以被重载。这种重载允许程序员自定义 new 运算符的行为,以满足特定的需求或添加额外的功能。通过重载 new 运算符,可以指定特定的内存分配策略,如使用自定义的内存池、分配器或者记录内存分配信息等。
Lambda 表达式是一种在代码中直接定义匿名函数的方法,也是一种匿名的内联函数,与传统的函数定义方式不同,lambda 表达式可以在一行代码中定义函数,而无需显式地为其命名。这使得 lambda 表达式特别适合在需要临时函数或者回调函数的场景下使用。
通常我们在定义一个函数后,需要在其他地方调用它,这个过程可能会涉及到函数声明、调用等步骤。而使用 lambda 表达式,我们可以直接在需要调用的地方定义这个函数,这样不仅节省了代码量,也让代码更加直观易读。Lambda 表达式一般只作用于局部作用域,用完即释放,不会占用额外的资源。
Lambda 表达式的另一个优点是,它能够捕捉外部变量并使用它们,使得在局部范围内使用外部数据更加方便。
然而,lambda 表达式也有其局限性。由于其简洁性,lambda 表达式的功能相对有限,只适用于定义简单的、单行的逻辑。如果逻辑过于复杂,使用 lambda 表达式可能会导致代码难以理解和维护。在这种情况下,传统的函数定义方式可能更为合适。
内联函数是一种让编译器在调用时把函数的代码直接插入到调用处的方法,而不是像普通函数那样在运行时去跳转到函数的地址执行。这么做的好处是可以减少一些函数调用的开销,比如省掉了压栈、出栈这些步骤。对一些简单、频繁调用的函数来说,用内联能提高程序的执行效率。
在 C++ 中,可以通过在函数定义前加上 inline 关键字来建议编译器将其内联。需要注意的是,inline 只是一个建议,编译器可以根据实际情况选择是否进行内联。通常情况下,编译器会对那些函数体较小、逻辑简单的函数进行内联处理,而对于那些复杂的、包含循环或递归调用的函数,编译器可能会选择忽略 inline 建议。
虽然内联函数在某些情况下可以提高性能,但它也有一定的局限性。首先,内联函数会增加代码体积,因为每次调用内联函数时,都会将函数体复制到调用点。如果一个内联函数被多次调用,这将导致代码的冗余,可能增加可执行文件的大小。其次,对于一些复杂的函数,内联可能并不能带来性能提升,甚至会适得其反,因为编译器可能无法有效地优化这些代码。
此外,需要注意的是,递归函数通常不适合作为内联函数,因为递归函数本质上需要多次调用自身,内联化可能会导致代码膨胀和不必要的复杂性。
通常情况下,内联函数不应该是虚函数。 这是因为内联函数是在编译时直接将函数代码插入到调用点,而虚函数的特性是在运行时通过虚函数表进行动态绑定来确定调用哪个函数。这两个特性在本质上是冲突的:内联函数希望在编译时确定代码,而虚函数则依赖于运行时的动态决策。
虽然技术上可以声明虚函数为内联函数,但是这样做通常是无意义的。因为当你通过基类指针调用一个虚函数时,编译器无法内联它,因为调用的具体函数要到运行时才能确定。因此,这样的虚函数通常不会被内联,而是通过常规的虚函数调用机制来执行。
C++ 中不像 Java 自带垃圾回收机制,它必须要释放掉分配到内存。因此引入了智能指针。智能指针是 C++ 中用于自动管理内存的工具,可以有效防止内存泄漏和一些手动管理内存时容易出错的问题。传统的指针在动态分配内存后,我们必须记得手动释放,这很容易出错,比如忘记释放就会导致内存泄漏。而智能指针通过在对象生命周期结束时自动释放资源。智能指针的核心是引用计数,每使用它一次内部的引用计数会 +1,每析构一次引用计数会 -1,引用计数减为 0 时会删除原始指针指向的堆区内存。使用智能指针需要引入头文件 <memory>。
我了解的智能指针有三种:
unique_ptr 代表独占所有权的指针,意味着内存在某一时刻只能被一个 unique_ptr 拥有,不允许其他智能指针共享其内部的指针。通过构造函数初始化,不允许一个 unique_ptr 复制给另一个 unique_ptr,因为那会违反 unique_ptr 的独占所有权特性。这样可以明确地知道谁在管理这块内存,不会有多个指针混乱地操作同一块内存。如果你想把资源从一个 unique_ptr 转移到另一个 unique_ptr 时,通过 std::move 转移内存的所有权,而原来的 unique_ptr 就不再持有这个资源了。
shared_ptr 则是用来共享所有权的,多个 shared_ptr 可以同时管理同一块有效内存,它们通过引用计数来管理对象的生命周期,只有当最后一个 shared_ptr 被销毁时引用计数变为 0 时,内存才会被释放。这个很适合在需要多个地方共享同一个对象的场景。
不过要注意的是,不要使用一个原始指针初始化多个 shared_ptr。因为释放时,会释放多次内存导致内存泄漏。(一个原始指针初始化两个智能指针会导致内存重复释放的问题)
正确初始化 shared_ptr 的方法是直接通过将原始指针赋值给一个 shared_ptr,然后通过拷贝构造或移动构造来共享内存管理,而不是用同一个原始指针初始化多个 shared_ptr。
如果两个 shared_ptr 互相引用,会导致循环引用的问题,这时候就需要用到 weak_ptr。(如果两个对象互相持有对方的 shared_ptr,就会出现循环引用的问题。具体来说,两个 shared_ptr 相互引用彼此时,它们的引用计数都不会归零,这意味着无论这两个对象是否超出了它们的作用域,引用计数都无法减到零,导致这两个对象永远无法被销毁,进而导致内存泄漏。)
这时候就需要用到 weak_ptr 了。是一个弱引用。weak_ptr 是一种不增加引用计数的智能指针,它提供了一种观察而不拥有对象的方法,主要是监视 shared_ptr 中管理的资源是否存在。通过将其中一个对象的 shared_ptr 改为 weak_ptr,我们可以打破这个循环引用。当对象超出作用域时,引用计数能够正常减少到零,从而释放对象,避免内存泄漏。
智能指针让我们不再需要手动管理内存,避免了很多常见的错误。不过在使用时也要注意,比如 shared_ptr 的引用计数会有一些性能开销,所以要根据实际需求选择合适的智能指针。
函数模板允许我们编写一个通用的代码,避免了为不同类型编写相似的函数。拿比大小来说,通常情况下要将 int、float 等数据类型都编写一个函数很麻烦,而使用函数模板会节省时间和灵活很多,只需要写一次,然后通过传入的参数类型,编译器会自动帮你生成适当的函数版本。
关键字:template
C++ 中还有模板特化的概念,这允许我们为某些特定类型提供特殊的实现。有时候,你可能需要对某种类型做一些特殊处理,而不想影响其他类型的模板实现,这时候就可以用模板特化。
虽然函数模板提供了很大的灵活性,但它也有一定的局限性。例如,模板代码在编译时会生成具体类型的代码,这可能导致代码膨胀,特别是在处理大量不同类型的情况下。此外,模板的语法和错误信息有时比较复杂,调试可能不太直观。
函数模板广泛应用于 C++ 标准库中,例如 STL(标准模板库)中的各种容器和算法都依赖模板。通过函数模板,STL 能够提供通用的接口来处理不同的数据类型,使得 C++ 程序更具通用性和灵活性。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online