跳到主要内容
C++ 核心概念与常见面试题解析 | 极客日志
C++ 算法
C++ 核心概念与常见面试题解析 C++ 面向对象编程包含封装、继承和多态三大特性,其中多态通过虚函数实现动态绑定。内存管理中,堆栈区分明确,new/delete 与 malloc/free 在对象生命周期处理上存在差异。C++11 引入的智能指针如 unique_ptr 和 shared_ptr 有效解决了资源管理问题,lambda 表达式则简化了匿名函数定义。重点涵盖了构造函数、析构函数、引用与指针的区别以及模板基础。
AiEngineer 发布于 2026/3/23 更新于 2026/4/25 1 浏览C++ 核心知识体系
面向对象和面向过程
面向过程编程 更关注的是过程,也就是一系列的步骤,把程序设计成一步一步解决问题的方式。这种编程方式把程序看作是由一组函数或过程组成的,每个函数完成一个具体的任务,数据则在这些函数之间传递。
面向对象编程 则更多地关注'对象',即如何将现实中的事物抽象成程序中的对象。对象既包含数据(称为属性),也包含操作这些数据的方法(称为方法)。面向对象的思想是将数据和操作封装在一起,通过对象之间的交互来实现程序的功能。
三大特性
封装 :把数据和操作封装在对象内部,外部只能通过定义好的接口(方法)来访问数据。
继承 :可以从已有的类派生出新的类,新的类可以继承和扩展原有类的属性和方法。
多态 :不同对象可以对同一个方法做出不同的响应,这增强了程序的灵活性。
简单来说,面向过程 是把程序看作是由函数组成的一步步过程,而面向对象 则是将程序中的元素视为'对象'。
C 语言与 C++ 的区别
头文件 :C 语言常用 .h,C++ 标准库通常不带后缀或使用特定命名空间。
布尔类型 :C 语言标准(C99)之前没有为布尔值单独设置一个类型,判断真假时使用整数 0 表示假,非 0 表示真。C99 标准新增 _Bool 表示布尔值。C++ 直接支持 bool 类型。
编译器效率 :C++ 引入了更多特性,编译开销相对较大,但这并不意味着它不是更好的工具,而是基于 C 的另一种编程语言。
语言定位 :C++ 不是一个完全面向对象的语言,它是基于面向对象的语言,因为其中还包含 C 语言的东西。C 语言是一个面向过程的语言,没有面向对象易于维护、易复用、易扩展的特性。
重载 :-> 是静态多态。C 语言不能重载,而 C++ 可以。重载的定义是在同一个作用域下,函数名相同,参数类型或者顺序或者个数不同则为函数重载。因为编译器对两种语言的处理方式不同,C++ 编译器编译后会在原函数名的基础上加上参数类型来识别重载函数,而 C 语言编译后还是原函数名就会出现重定义错误。
C++ 编译过程
主要分为四个阶段:预处理、编译、汇编、链接。
预处理 :主要处理 # 开头的指令,将头文件插入到程序中,将全部的 #define 宏展开进行宏替换。处理全部的预处理指令,如 #if、#ifdef、#else 等。这个过程是递归的,即被包括的文件可能还包括其它文件。删除全部注释 //、/* */。加入行号和文件标识。经过预处理后的 .i 文件不包括任何宏定义。
编译 :将源代码由文本形式转换成机器语言,生成汇编代码文件 .s。
汇编 :将汇编代码 .s 翻译成机器指令的 .o 或 .obj 目标文件,.o 文件是纯二进制文件。
链接 :产生的 .out 或 .exe 可运行文件。
链接是将所有的 .o 文件和库(动态库、静态库)链接在一起,得到可以运行的可执行文件(Windows 的 .exe 文件或 Linux 的 .out 文件)等。
多态
是什么?
面向对象的三大特征之一,简单来说就是不同对象调用同一行为表现出的不同形式和结果。多态可以理解成一个接口多种实现。
分类?
静态多态 :像函数重载、运算符重载、函数模板这种在编译期间确定的就是静态多态,就是编译期间确定绑定的是哪一个函数。
动态多态 :是通过虚函数重写实现的,是在运行期间确定的多态,是一种晚绑定机制,在运行期间才能确定调用哪一个函数。
虚函数
是什么? 在基类函数名前面加上 virtual 关键字的就是被声明为虚函数。虚函数是动态多态,在运行期间父类指针指向实际对象的类型,在运行期间根据对象类型确定调用的是哪个虚函数的,运行时在虚函数表中寻找调用的虚函数地址。
底层? 从底层实现来看,C++ 中的虚函数是通过虚函数表和虚函数指针来实现的。假如这个类中有虚函数,那么会给这个类提供一个虚函数表,这个表中存储了该类中所有虚函数的地址,每个对象都有一个指向虚表的虚表指针,然后通过虚表指针找到虚表,进而通过找到函数在表中的位置实现函数的正确调用。
当我们子类继承父类的话,它会也会把这个父类这个虚函数表给继承 下来。如果我们子类重写父类的虚函数的话,它会把父类那个虚函数表中的虚函数地址进行替换 ,从而达到运行时实现多态。然后确定调用子类的函数。(虚表指针是在创建对象时候初始化指向对应的虚函数表)
解决的问题? 把父类的析构函数设置为虚析构,解决了能够正确释放子类的资源的问题。
构造函数不能设置为虚函数? 因为在创建对象时,首先要调用构造函数来初始化对象,而虚函数机制依赖于虚函数表指针,在构造函数执行之前对象还未完全构建,虚函数表指针还不存在,所以构造函数不能是虚函数。
重载、重写、隐藏
函数重载 :是指在同一个类里,定义多个名字相同但参数不同的函数。通常用来处理类似的问题或任务,但输入的参数类型、个数或者顺序三者有一个不同。
重写 :是指在派生类中重新定义基类中的虚函数,相当于提供基类虚函数的新版本,名字和参数都一样。重写是为了在派生类中提供基类函数的新实现,以便通过基类指针或引用调用时,执行派生类中的实现。
隐藏 :是指在派生类中定义了一个与基类中同名的函数,但参数列表不同,或者基类中的函数不是虚函数。这样,基类的函数在派生类中被隐藏了。
引用
是什么? 引用是给变量起一个别名,在创建引用时就要初始化绑定一个变量。
好处
引用不占内存,使得引用在处理大对象时提高性能,避免了复制整个对象的内存和时间开销。
通过引用传递参数,可以避免使用指针的复杂性和潜在错误,还能保持代码的高可读性。
函数返回值本身是右值,想返回左值时需要引用。
还可以避免拷贝构造(引用与原始对象共享相同的内存地址,因此对引用的操作实际上是在操作原始对象本身,而不是创建对象的副本)。
为什么不能初始化为空? 因为它的定义就是这样,而且引用不同于指针,引用一旦创建就不能更改其指向,所以初始化引用的时候必须指定一个有效的对象。
引用与指针的区别?
指针是一个变量,只不过它存储的是另一个变量的地址,可以改变指向。而引用是已经存在的变量起一个别名,在创建引用时就要初始化绑定一个变量,不能改变引用关系绑定其他对象了。
想要访问一块内存,指针需要进行解引用操作,而引用直接可以进行访问。当指针作为函数参数 时,函数内部通过解引用指针来访问和修改所指向的对象。引用作为函数参数时,函数内部直接对引用进行操作就是对实参本身进行操作。
如果函数返回引用 ,这个引用可以被用作左值。当函数返回引用时,实际上返回的是对象本身的别名,而不是对象的副本,避免了不必要的拷贝。
补充:如果函数返回一个局部变量的引用 (非静态局部变量),当函数结束时,这个局部变量就被销毁了,再使用这个引用就会出错。如果返回一个局部变量的指针(非静态局部变量),当函数结束后,该局部变量的内存被释放,返回的指针就会成为野指针,使用野指针会导致未定义行为。
指针可以有多级指针,而引用没有多级的概念。当需要在函数内部修改指针本身时,可以使用多级指针作为函数参数。
引用不可以为空,指针可以。
引用 ++ 是值 ++,而指针 ++ 是地址偏移。
内存分区
堆和栈的区别? 堆是由程序员手动申请和释放的,可以使用 new 和 malloc 申请,释放使用 delete 和 free。
指针常量和常量指针
指向常量的指针 (const 在类型前):指针可以指向不同的地址,但不能通过指针修改所指向的对象。
常量指针 (const 在 * 号后):指针一旦初始化后,不能指向其他地址,但可以通过指针修改所指向的对象(前提是对象本身不是常量)。
指向常量的常量指针 (const 在两侧):指针不能改变指向,且不能通过指针修改所指向的对象。
NULL 在 C 语言中是 (void *)0 在 C++ 中是 0? 在 C 语言中,NULL 通常定义为 (void *)0,因为 C 允许将 void* 类型的指针隐式转换为任何其他类型的指针。这样,NULL 可以赋值给任何指针类型,编译器不会报错。
在 C++ 中,NULL 被定义为 0。C++ 比 C 对类型转换的检查更严格,不允许 void* 自动转换为其他类型的指针。用 0 来表示 NULL 是为了避免这些类型转换问题,因为 0 可以被看作是任何指针类型的空指针。
C++ 用 nullptr 代指空指针?
类型安全 :nullptr 有一个专门的类型 std::nullptr_t,不会被误用为整数或其他类型的指针。
清晰明确 :使用 nullptr 可以明确表示这是一个空指针,而不是整数 0,提高代码的可读性和维护性。
构造函数
是什么? 函数名与类名相同,无返回值也不写 void。创建对象时自动调用构造函数。没有实现构造函数时编译器会提供一个默认的无参构造函数。构造函数是给成员变量赋值的,不是初始化;初始化参数列表是给成员变量初始化的。
拷贝构造
调用时机
用一个已经存在的对象初始化一个新对象时。
对象以值的形式作为函数的参数和返回值。
拷贝构造参数不是引用行吗? 拷贝构造的参数不是引用会导致无限递归。加 const 是为了避免通过形参修改实参。
深浅拷贝的区别? 浅拷贝是简单的赋值操作。深拷贝是拷贝相同大小相同内容到堆区。
析构函数
是什么? 析构函数名与类名相同前面加 ~,无参数,无返回值,无 void。销毁对象时自动调用。未实现编译器会提供默认的析构。析构函数是释放对象里面成员变量所指向的堆区内存的 。
内存分配和销毁用什么? new 运算符申请内存会先调用 malloc 再调用构造函数。释放对象申请的内存空间用 delete,delete 会先调用析构函数再调用 free。delete[] 用于释放数组对象占用的内存。它会依次调用数组中每个对象的析构函数来清理资源。
new 和 malloc
区别?
new 不需要传入具体的申请字节数,会自动机选要分配的内存空间,返回类型正确的指针,malloc 需要手动计算需要分配的内存大小,返回 void* 类型的指针。
new 是运算符可以重载,malloc 是库函数不能重载。(库函数不能重载的原因主要是因为库函数的实现已经被固定在编译器或者库文件中,这些函数通常是用来执行特定的任务和操作的,比如内存分配、文件操作等。如果允许对这些函数进行重载,可能会导致程序运行时出现不可预测的行为,因为编译器无法确定到底使用哪个版本的函数实现。)
new 的返回值不需要强转,malloc 需要。
malloc 申请失败,例如申请负数会返回空,而 new 会抛出异常。
给一个类分配堆区内存时,会先调用 malloc 再调用构造函数给成员变量赋值。
new delete malloc free? 对于 new 和 delete:在 C++ 中,new 和 delete 是用于动态内存分配和释放的运算符。new 用于创建对象时,会自动调用对象的构造函数进行初始化,而 delete 则用于释放 new 分配的内存,并调用对象的析构函数。这样可以确保对象的生命周期管理是安全和正确的。
对于 malloc 和 free:在 C 语言中,我们使用 malloc 和 free 来进行动态内存管理。malloc 分配一块指定大小的内存空间,并返回一个指向该内存的 void* 指针,而 free 则用于释放 malloc 分配的内存。需要注意的是,使用 malloc 分配的内存需要手动管理对象的构造和析构,因为它不会自动调用构造函数或析构函数。
new 会先调用 malloc 再调用构造函数 delete 先析构再 free? 构造函数需要有一块内存来初始化对象的状态 ,所以先调用 malloc 在堆区中找到一个足够大的未初始化的内存,一旦 malloc 成功分配了内存,new 运算符会再这块内存上构造对象,也就是调用对象的构造函数来初始化成员。
delete 先析构再 free? 析构函数负责清理对象的资源和状态的。对象的状态存储在内存中,如果先调用 free 释放了这块内存,析构函数无法找到对象的数据和成员(访问已经被释放的内存),因此无法正确释放内存。所以必须在释放之前调用析构函数清理对象 。
new 可以重载吗? new 运算符可以被重载。这种重载允许程序员自定义 new 运算符的行为,以满足特定的需求或添加额外的功能。通过重载 new 运算符,可以指定特定的内存分配策略,如使用自定义的内存池、分配器或者记录内存分配信息等。
C++11
lambda 表达式
是什么? 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 转移到另一个 unique_ptr 的时候,通过 std::move 转移内存的所有权,而原来的 unique_ptr 就不再持有这个资源了。
shared_ptr shared_ptr 则是用来共享所有权的,多个 shared_ptr 可以同时管理同一块有效内存,它们通过引用计数来管理对象的生命周期,只有当最后一个 shared_ptr 被销毁时引用计数变为 0 时,内存才会被释放。这个很适合在需要多个地方共享同一个对象的场景。
不过要注意的是,不要使用一个原始指针初始化多个 shared_ptr。因为释放时,会释放多次内存导致内存泄漏。(一个原始指针初始化两个智能指针会导致内存重复释放的问题)
正确初始化 shared_ptr 的方法是直接通过将原始指针赋值给一个 shared_ptr,然后通过拷贝构造或移动构造来共享内存管理,而不是用同一个原始指针初始化多个 shared_ptr。
weak_ptr 如果两个 shared_ptr 互相引用,会导致循环引用的问题,这时候就需要用到 weak_ptr。(如果两个对象互相持有对方的 shared_ptr,就会出现循环引用的问题。具体来说,两个 shared_ptr 相互引用彼此时,它们的引用计数都不会归零,这意味着无论这两个对象是否超出了它们的作用域,引用计数都无法减到零,导致这两个对象永远无法被销毁,进而导致内存泄漏。)
这时候就需要用到 weak_ptr 了。它是一个弱引用。weak_ptr 是一种不增加引用计数的智能指针,它提供了一种观察而不拥有对象的方法,主要是监视 shared_ptr 中管理的资源是否存在。通过将其中一个对象的 shared_ptr 改为 weak_ptr,我们可以打破这个循环引用。当对象超出作用域时,引用计数能够正常减少到零,从而释放对象,避免内存泄漏。
智能指针让我们不再需要手动管理内存,避免了很多常见的错误。不过在使用时也要注意,比如 shared_ptr 的引用计数会有一些性能开销,所以要根据实际需求选择合适的智能指针。
函数模板
是什么? 函数模板允许我们编写一个通用的代码,避免了为不同类型编写相似的函数。拿比大小来说,通常情况下要将 int、float 等数据类型都编写一个函数很麻烦,而使用函数模板会节省时间和灵活很多,只需要写一次,然后通过传入的参数类型,编译器会自动帮你生成适当的函数版本。
C++ 中还有模板特化 的概念,这允许我们为某些特定类型提供特殊的实现。有时候,你可能需要对某种类型做一些特殊处理,而不想影响其他类型的模板实现,这时候就可以用模板特化。
虽然函数模板提供了很大的灵活性,但它也有一定的局限性。例如,模板代码在编译时会生成具体类型的代码,这可能导致代码膨胀,特别是在处理大量不同类型的情况下。此外,模板的语法和错误信息有时比较复杂,调试可能不太直观。
函数模板广泛应用于 C++ 标准库中,例如 STL(标准模板库)中的各种容器和算法都依赖模板。通过函数模板,STL 能够提供通用的接口来处理不同的数据类型,使得 C++ 程序更具通用性和灵活性。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online