# C++ 高级特性 ## C++11的新特性有哪些 1. 语法的改进 (1)统一的初始化方法 (2)成员变量默认初始化 **(3)auto关键字: 允许编译器自动推断变量的类型,减少类型声明的冗余。** (4)decltype 求表达式的类型 **(5)智能指针 std::shared_ptr 和 std::unique_ptr** (6)**空指针 nullptr: 提供了明确表示空指针的关键字,替代了传统的 `NULL`。** (7)**基于范围的for循环: 简化遍历容器元素的语法** (8)**右值引用和move语义 引入右值引用和移动构造函数,允许高效地将资源从一个对象移动到另一个对象,提高性能。** 2. 标准库扩充(往STL里新加进一些模板类) (9)无序容器(哈希表) 用法和功能同map一模一样,区别在于哈希表的效率更高 (10)正则表达式 可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串 (1**1)Lambda表达式: 允许在代码中定义匿名函数** ## 智能指针 #### 1. shared_ptr 1、**shared_ptr的实现机制是在拷贝构造时使用同一份引用计数** (1)一个模板指针T* ptr 指向实际的对象 (2)一个引用次数 必须new出来的,不然会多个shared_ptr里面会有不同的引用次数而导致多次delete (3)重载operator*和operator-> 使得能像指针一样使用shared_ptr (4)重载copy constructor 使其引用次数加一(拷贝构造函数) (5)重载operator=(赋值运算符) 如果原来的shared_ptr已经有对象,则让其引用次数减一并判断引用是否为零(是否调用delete),然后将新的对象引用次数加一 (6)重载析构函数 使引用次数减一并判断引用是否为零; (是否调用delete) **2、线程安全问题** (1)同一个shared_ptr被多个线程“读”是安全的; (2)同一个shared_ptr被多个线程“写”是不安全的; 证明:在多个线程中同时对一个shared_ptr循环执行两遍swap。 shared_ptr的swap函数的作用就是和另外一个shared_ptr交换引用对象和引用计数,是写操作。执行两遍swap之后, shared_ptr引用的对象的值应该不变) (3)共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的。 #### 2. unique_ptr **1、unique_ptr”唯一”拥有其所指对象** 同一时刻只能有一个unique_ptr指向给定对象,离开作用域时,若其指向对象,则将其所指对象销毁(默认delete)。 **2、定义unique_ptr时** 需要将其绑定到一个new返回的指针上。 **3、unique_ptr不支持普通的拷贝和赋值**(因为拥有指向的对象) 但是可以拷贝和赋值一个将要被销毁的unique_ptr;可以通过release或者reset将指针所有权从一个(非const)unique_ptr转移到另一个unique。 #### 3. weak_ptr **1、weak_ptr是为了配合shared_ptr而引入的一种智能指针** 它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况,但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。 **2、和shared_ptr指向相同内存** shared_ptr析构之后内存释放,在使用之前使用函数lock()检查weak_ptr是否为空指针。 ## 类型推导 **1、auto**: **auto可以让编译器在编译期就推导出变量的类型** (1)auto的使用必须马上初始化,否则无法推导出类型 (2)auto在一行定义多个变量时,各个变量的推导不能产生二义性,否则编译失败 (3)auto不能用作函数参数 (4)在类中auto不能用作非静态成员变量 (5)auto不能定义数组,可以定义指针 (6)auto无法推导出模板参数 (7)在不声明为引用或指针时,auto会忽略等号右边的引用类型和cv限定 (8)在声明为引用或者指针时,auto会保留等号右边的引用和cv属性 **2、decltype**: **decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算** decltype不会像auto一样**忽略引用和cv属性**,decltype会保留表达式的引用和cv属性 对于decltype(exp)有: 1. exp是表达式,decltype(exp)和exp类型相同 2. exp是函数调用,decltype(exp)和函数返回值类型相同 3. 其它情况,若exp是左值,decltype(exp)是exp类型的左值引用 **auto和decltype的配合使用**: ``` template auto add(T t, U u) -> decltype(t + u) { return t + u; } ``` ## 右值引用 左值右值: **左值:** 可以放在等号左边,可以取地址并有名字 **右值:** 不可以放在等号左边,不能取地址,没有名字 字符串字面值"abcd"也是左值,不是右值 **++i、--i是左值,i++、i--是右值** **1、将亡值** 将亡值是指C++11新增的和右值引用相关的表达式 将亡值可以理解为**即将要销毁的值**,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务 **2、左值引用** 左值引用就是对左值进行引用的类型,**是对象的一个别名** 并不拥有所绑定对象的堆存,所以必须立即初始化。 对于左值引用,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使用const引用形式 **3、右值引用** 表达式等号右边的值需要是右值,可以使用std::move函数强制把左值转换为右值。 **4、移动语义** 可以理解为**转移所有权**,对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用。 通过移动构造函数使用移动语义,也就是std::move;移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型int、float等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数 浅拷贝: > a和b的指针指向了同一块内存,就是浅拷贝,只是数据的简单赋值; 深拷贝: > 深拷贝就是再拷贝对象时,如果被拷贝对象内部还有指针引用指向其它资源,自己需要重新开辟一块新内存存储资源 **5、完美转发** 写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,通过std::forward()实现 ## 范围for循环 基于范围的迭代写法,for(变量:对象)表达式 对string对象的每个字符做一些操作: ``` string str ("some thing"); for (char c : str) cout << c << endl; // 输出字符串str中的每个字符 ``` 对vector中的元素进行遍历: ``` std::vector arr(5, 100); for (std::vector::iterator i = arr.begin(); i != arr.end(); i ++) { std::cout << *i << std::endl; } // 范围for循环 for (auto &i : arr) { std::cout << i << std::endl; } ``` ## 列表初始化 C++定义了几种初始化方式,例如对一个int变量 x初始化为0: ``` int x = 0; // method1 int x = {0}; // method2 int x{0}; // method3 int x(0); // method4 ``` 采用花括号来进行初始化称为列表初始化,无论是初始化对象还是为对象赋新值。 用于对内置类型变量时,如果使用列表初始化,且初始值存在丢失信息风险时,编译器会报错。 ``` long double d = 3.1415926536; int a = {d}; //存在丢失信息风险,转换未执行。 int a = d; //确实丢失信息,转换执行。 ``` ## lambda表达式 lambda表达式**表示一个可调用的代码单元**,没有命名的内联函数,不需要函数名因为我们直接(一次性的)用它,不需要其他地方调用它。 **1、lambda 表达式的语法:** ``` [capture list] (parameter list) -> return type {function body } // [捕获列表] (参数列表) -> 返回类型 {函数体 } // 只有 [capture list] 捕获列表和 {function body } 函数体是必选的 auto lam =[]() -> int { cout << "Hello, World!"; return 88; }; auto ret = lam(); cout< int :代表此匿名函数返回int,大多数情况下lambda表达式的返回值可由编译器猜测得出,因此不需要我们指定返回值类型。 **2、lambda 表达式的特点:** (1)变量捕获才是成就lambda卓越的秘方 1. [] 不捕获任何变量,这种情况下lambda表达式内部不能访问外部的变量 2. [&] 以引用方式捕获所有变量(保证lambda执行时变量存在) 3. [=] 用值的方式捕获所有变量(创建时拷贝,修改对lambda内对象无影响) 4. [=, &foo] 以引用捕获变量foo, 但其余变量都靠值捕获 5. [&, foo] 以值捕获foo, 但其余变量都靠引用捕获 6. [bar] 以值方式捕获bar; 不捕获其它变量 7. [this] 捕获所在类的this指针 ``` int a = 1, b = 2, c = 3; auto lam2 = [&, a](){ //b,c以引用捕获, a以值捕获 b = 5; c = 6; //a = 1,a不能赋值 cout << a << b << c << endl; //输出 1 5 6 }; lam2(); void fcn() { //值捕获 size_t v1 = 42; auto f = [v1] {return v1;}; v1 = 0; auto j = f(); //j = 42,创建时拷贝,修改对lambda内对象无影响 } void fcn() { //可变lambda size_t v1 = 42; auto f = [v1] () mutable {return ++v1;}; //修改值捕获可加mutable v1 = 0; auto j = f(); //j = 43 } void fcn() { //引用捕获 size_t v1 = 42; //非const auto f = [&v1] () {return ++v1;}; v1 = 0; auto j = f(); //注意此时 j = 1 } ``` (2)lambda最大的一个优势是在使用STL中的算法(algorithms)库 例如:数组排序 ``` int arr[] = {6, 4, 3, 2, 1, 5}; bool compare(int& a, int& b) { //谓词函数 return a > b; } std::sort(arr, arr + 6, compare); //lambda形式 std::sort(arr, arr + 6, [](const int& a, const int& b){return a > b;}); //降序 std::for_each(begin(arr), end(arr), [](const int& e){cout << "After:" << e << endl;}); //6, 5, 4, 3, 2, 1 ``` ## 请解释C++中的异常处理机制,并且给出一个例子。(考点:异常处理)【中等】 #### 传统的错误处理机制: - 终止程序:如assert,缺陷:用户难以接受。如发生内存错误,**除0错误时就会终止程序**; - 返回错误码:缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误,实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误. #### C++中的异常处理机制 C++的异常情况无非两种,一种是**语法错误**,即程序中出现了错误的语句、函数结构和类,致使编译程序无法进行;另一种是**运行时发生的错误**,一般与算法有关. 关于语法错误, C++编译器的报错机制可以让我们轻松地解决这些错误; 第二种是运行时的错误,常见的有文件打开失败、数组下标溢出、系统内存不足等等,而一旦出现这些问题,引发算法失效、程序运行时无故停止等故障也是常有的,这就要求我们在设计算法时要全面,比如针对文件打开失败的情况,保护的方法有很多种,最简单的就是使用**“return”命令**,告诉上层调用者函数执行失败;另外一种处理策略就是利用c++的异常机制,抛出异常. - C++的异常处理机制 C++异常处理机制是一个用来有效地处理运行错误的非常强大且灵活的工具,它提供了更多的弹性、安全性和稳固性,克服了传统方法所带来的问题,异常的抛出和处理主要使用了以下三个关键字: try、 throw 、 catch ; - throw:用于抛出一个异常 - try:定义一个可能发生异常的代码块 - catch:捕获并处理抛出的异常 - 异常的抛出和匹配原则 (1)异常对象的类型决定匹配:在抛出异常时,**抛出的对象的类型决定了应该激活哪个catch块的处理代码**,只有与抛出的异常对象类型匹配的catch块会被执行. (2)最近匹配原则:如果在调用链中存在多个catch块与抛出的异常对象类型匹配,那么**离抛出异常位置最近的catch块会被选中执行**,这意味着程序会从当前位置开始,依次向上搜索调用链,直到找到最近匹配的catch块. (3)异常对象的拷贝:**抛出异常对象后,会生成该异常对象的拷**贝,这是因为抛出的异常对象可能是一个临时对象,为了在异常处理期间保持异常对象的有效性,会创建一个拷贝对象,这个拷贝对象在catch块处理完异常后会被销毁. (4)catch(...)的使用:**catch(...)是一种特殊的catch块,可以捕获任意类型的异常**,但是,它无法知道具体的异常类型,因此只能用于处理未知的异常错误,应该尽量避免在正常情况下使用catch(...),而是针对特定的异常类型提供明确的处理代码. (5)异常的派生类匹配:**实际中,可以抛出派生类对象,并在catch块中使用基类类型进行捕获**,这种情况下,派生类对象会与基类类型匹配,因为派生类对象也是基类的一种形式,这种技术称为基类捕获,它在实际编程中非常实用,可以处理一组相关的异常类型. 需要注意的是,异常处理的匹配原则并不总是依赖于类型完全匹配,还涉及到派生类和基类之间的关系,这允许开发人员在catch块中使用更通用的异常类型来捕获一组相关的异常,并提供相应的处理代码. 在函数调用链中,**异常栈展开**的匹配原则是指异常在调用链中传播时如何查找匹配的`catch`块进行处理. 重新捕获是指在异常处理代码中重新抛出当前捕获的异常,使得该异常可以在调用栈的更高层次继续传播,重新捕获的目的是将异常传递给更高层的异常处理代码或者使得异常在程序的其他部分进行处理 在C++中,**通过使用 throw; 语句可以重新抛出当前捕获的异常,这个语句不带参数,它会重新抛出当前已捕获的异常对象,使得异常在调用栈中向上传播**. #### 异常安全 在C++中,建议在构造函数和析构函数中尽量避免抛出异常,以避免对象不完整或资源泄漏等问题. - 构造函数中的异常 在构造函数中抛出异常可能导致对象不完整或没有完全初始化,**当抛出异常时,已经执行的构造函数代码将被撤销,对象将被销毁,且析构函数不会被调用**,这可能导致资源的泄漏或对象状态不一致,为了避免这种情况,应该在构造函数中尽可能避免抛出异常,或者使用初始化列表和异常安全的编程技术来处理异常. - 析构函数中的异常 在析构函数中抛出异常同样会导致问题,当析构函数中抛出异常时,可能会导致其他代码无法正常处理资源的释放或清理操作,从而导致资源泄漏,为了避免这种情况,应该在析构函数中尽量避免抛出异常,或者使用异常安全的编程技术确保资源的正确释放 - 资源泄露问题 异常在C++中确实可能导致资源泄漏的问题。例如,在使用new进行动态内存分配时,如果在分配后抛出异常而没有正确释放内存,就会导致内存泄漏。类似地,在使用锁进行资源保护时,如果在锁定和解锁之间抛出异常,可能导致死锁或资源未正确释放,为了解决这些问题,可以使用**RAII技术**,通过对象的构造函数获取资源,并在析构函数中自动释放资源,以确保资源在异常发生时也能正确释放. - C++标准库的异常体系 C++标准库提供了一个异常体系,其中定义了一些常见的异常类型,这些异常类型是继承自`std::exception`类的. 以下是一些C++标准库中常见的异常类型: - std::exception:这是所有标准库异常类的基类,它提供了一个基本的接口,包括what()方法用于获取异常的错误消息. - std::bad_alloc:当内存分配失败时,new操作符会抛出该异常. - std::bad_cast:在类型转换失败时,比如使用dynamic_cast进行类的动态类型转换时,如果转换不成功,就会抛出该异常. - std::logic_error:这是一组逻辑错误的基类异常,派生自std::exception,它的派生类包括: - std::invalid_argument:当函数参数无效或不合法时抛出。 - std::domain_error:当参数在函数的定义域之外时抛出。 - std::length_error:当容器长度超出其允许的最大限制时抛出。 - std::runtime_error:这是一组运行时错误的基类异常,派生自std::exception,它的派生类包括: - std::overflow_error:当进行数值溢出运算时抛出. - std::underflow_error:当进行数值下溢运算时抛出. - std::range_error:当数值超出有效范围时抛出. - std::out_of_range:当访问容器或数组中索引超出范围时抛出 #### C++异常处理机制的使用例子 处理零异常: ``` #include #include // 用于 std::runtime_error // 一个简单的除法函数 double divide(double numerator, double denominator) { if (denominator == 0) { // 抛出除零异常 throw std::runtime_error("Error: Division by zero"); } return numerator / denominator; } int main() { double num1 = 10.0; double num2 = 0.0; // 除数为零,触发异常 try { // 尝试进行除法运算 double result = divide(num1, num2); std::cout << "Result: " << result << std::endl; } catch (const std::runtime_error& e) { // 捕获并处理 std::runtime_error 异常 std::cout << "Caught exception: " << e.what() << std::endl; } std::cout << "Program continues running after handling the exception." << std::endl; return 0; } 函数的输出: Caught exception: Error: Division by zero Program continues running after handling the exception. ``` ## 如何使用try、catch、throw和finally关键字?(考点:异常处理语法)【简单】 #### try、catch、throw关键字 C++ 异常处理机制:`try`、`catch`、`throw` 1. **`throw`**:用于抛出异常,**一旦执行 `throw`,程序的控制流会跳出当前函数,进入到异常处理机制**,因此 `throw` 后面的代码不会执行; 2. **`try`**:用于包裹可能抛出异常的代码块; 3. **`catch`**:用于捕获并处理异常. 例如: ``` #include #include // std::runtime_error void mightThrowException(bool shouldThrow) { if (shouldThrow) { throw std::runtime_error("Something went wrong!"); } std::cout << "No exception thrown." << std::endl; } int main() { try { mightThrowException(true); // 这里会抛出异常 } catch (const std::runtime_error& e) { std::cout << "Caught exception: " << e.what() << std::endl; } try { mightThrowException(false); // 这里不会抛出异常 } catch (const std::runtime_error& e) { std::cout << "Caught exception: " << e.what() << std::endl; } return 0; } ``` #### finally关键字 finally关键字是许多编程语言(如 Java、Python、C# 等)中的一个关键字,**用于在异常处理机制中确保无论是否发生异常,某些代码块都会执行**,通常用于进行资源清理、释放锁、关闭文件、数据库连接等操作,但是C++并不支持finally关键字,C++通常通过**RAII**来代替类似finally的功能. #### RAII技术 **RAII**(资源获取即初始化)是C++ 中的一种编程技术,主要用于管理资源(如内存、文件句柄、数据库连接等)的生命周期,RAII 通过对象的生命周期来管理资源的获取和释放,确保资源在适当的时机自动释放,从而避免了资源泄漏问题. RAII 技术广泛应用于管理各种资源,最常见的应用场景是: 1. **内存管理**:通过智能指针(如 `std::unique_ptr`、`std::shared_ptr`)自动管理动态分配的内存; 2. **文件操作**:通过文件流类(如 `std::ofstream`、`std::ifstream`)自动管理文件的打开与关闭; 3. **锁管理**:通过锁类(如 `std::lock_guard`、`std::unique_lock`)自动管理线程锁的加锁与解锁; 4. **网络资源**:自动管理网络连接、套接字等资源. ## 什么是异常安全的代码,如何编写异常安全的程序?(考点:异常安全)【困难】 #### 异常安全 异常安全的目标: - **避免资源泄漏**:确保即使在发生异常时,所有的资源(内存、文件句柄、锁等)都能被正确释放. - **保持数据一致性**:确保程序中的数据在发生异常时不会处于不一致的状态. - **不影响程序的稳定性**:即使异常发生,程序仍然能够正确处理异常,并在可能的情况下继续执行. - 异常安全级别 基本保证:即使发生异常,已分配的资源(例如内存)会被正确释放,程序不会崩溃. 强保证:如果发生异常,程序将保持不变(即操作完全回滚,在发生异常的情况下,不会有任何中间状态被保留下来),这意味着要么操作成功,要么根本没有执行任何操作. 无异常保证:函数绝对不会抛出异常,甚至不会抛出由 `new`、`delete`、容器操作等带来的任何异常. #### 异常安全的代码 异常安全的代码指的是在 C++ 中,确保代码在发生异常时仍能保持程序的正确性,避免资源泄漏、数据不一致或程序崩溃,特别是在涉及动态内存分配、文件操作、数据库连接等资源管理时. 例子:假设我们有一个函数,涉及动态内存分配和修改操作,我们希望在发生异常时,内存能够被正确释放 ``` #include #include #include // 模拟操作:分配内存并进行修改 void modifyData() { // 基本保证:资源获取即初始化(RAII) std::unique_ptr data(new int[10]); // 假设在某些情况下抛出异常 if (true) { // 模拟某种条件 throw std::runtime_error("Something went wrong!"); } // 进一步的操作(不会执行,因为发生异常) data[0] = 42; std::cout << "Data modified!" << std::endl; } int main() { try { modifyData(); } catch (const std::exception& e) { std::cout << "Caught exception: " << e.what() << std::endl; } return 0; } ``` #### 编写异常安全的代码 异常安全的编程原则: - **资源管理**:确保程序中的所有资源(如内存、文件句柄、数据库连接等)在发生异常时都能正确释放,不会导致资源泄漏. - **数据一致性**:在操作过程中,如果发生异常,程序应该确保数据的状态保持一致,避免部分操作未完成导致的数据损坏. - **程序稳定性**:即使发生异常,程序也应该能够保持稳定,不会崩溃或进入不可预测的状态 ## 命名空间(namespace)的作用是什么?(考点:命名空间)【简单】 在C/C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染。 #### 定义 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。 #### 命名空间的使用 命名空间里的成员相当于有一个域的封装,所以在访问的时候需要加上域作用限定符(::)。 ``` namespace N { int a = 10; int b = 20; int Add(int left, int right) { return left + right; } int Sub(int left, int right) { return left - right; } } int main() { printf("%d\n", N::a); // 该语句编译出错,无法识别a return 0; } ``` ## 请解释inline函数的作用,并给出一个例子。(考点:内联函数)【简单】 ### inline函数的作用 inline关键字用于提示编译器将函数的代码直接插入到调用点,而不是正常的使用函数调用机制,这通常用于小型、频繁调用的函数,目的是减少函数调用的开销(如栈操作)并提高程序的执行效率. **使用方法:** ``` inline int add(int a,int b) { return a+b; } //在使用时,与普通函数的调用方式相同 int result=add(3,4); ``` ### inline函数的工作原理 当你声明一个函数为 inline 时,编译器会尝试将函数的代码展开到调用点,而不是进行常规的函数调用,然而,**inline只是一个建议,编译器并不一定会遵循**,编译器会根据函数的复杂性和大小决定是否内联. - 工作原理: 编译时展开:编译器在编译时用函数体替换函数调用; 减少调用开销:减少函数调用时的栈操作,消除了参数压栈和返回地址等操作; 增大代码体积:因为函数被多次插入到调用点,可能导致二进制代码变大,影响缓存性能. ## 说一下lambda函数,它有什么用途?(考点:lambda表达式)【中等】 lambda表达式(也称为匿名函数)是在 C++11标准中引入的一种方便的函数编写方式。Lambda允许你在需要一个函数对象的地方快速定义函数的行为,而不需要按照传统方式定义一个完整的函数。 我们主要出于这些情况用 lambda函数: - **API**:Lambda函数可以简化回调函数的编写,特别是在使用 STL时。 - **简洁**:在需要临时使用一个函数但不想定义一个完整函数的情况下。 - **闭包**:Lambda 可以捕获外部变量,形成闭包,使得函数可以访问和操作外部作用域的变量。 ## 请解释C++中的智能指针(如std::unique_ptr和std::shared_ptr)及其作用。(考点:智能指针)【中等】 ### C++中的智能指针 C++中的智能指针是用于管理动态分配的内存资源,避免内存泄漏和指针悬空问题的一种工具,它利用了RAII技术,在对象构造时获取资源,在对象析构时自动释放资源. C++中提供了以下一种类型的智能指针: - std::unique_ptr - std::shared_ptr - std::weak_ptr C++14引入了`std::make_unique`,C++11引入了`std::make_shared`,这些函数用于创建智能指针,它们更加安全高效,因为它们避免了直接使用`new`,智能指针允许自定义删除器,这可以用于管理需要特殊销毁逻辑的资源. ### std::unique_ptr `std::unique_ptr`是一个**独占所有权的智能指针**,意味着同一时间只能有一个`unique_ptr`指向一个给定的对象,-当 `std::unique_ptr` 被销毁时,它所指向的对象也会被自动删除. std::unique_ptr不允许复制,但允许通过std::move转移所有权. 例如: ``` #include #include class MyClass { public: MyClass() { std::cout << "MyClass constructor" << std::endl; } ~MyClass() { std::cout << "MyClass destructor" << std::endl; } void doSomething() { std::cout << "Doing something..." << std::endl; } }; int main() { // 创建一个 std::unique_ptr 并指向一个新的 MyClass对象 std::unique_ptr ptr1 = std::make_unique(); ptr1->doSomething(); // 转移所有权 std::unique_ptr ptr2 = std::move(ptr1); if (ptr1 == nullptr) { std::cout << "ptr1 is null after move" << std::endl; } ptr2->doSomething(); //当ptr2离开作用域时,它所指向的对象会被自动销毁 return 0; } ``` ### std::shared_ptr `std::shared_ptr`是一个共享所有权的智能指针,多个`shared_ptr`可以指向同一个对象,它使用**引用计数**机制来跟踪有多少个`shared_ptr`共享同一个对象,当最后一个`shared_ptr`被销毁或重置时,所拥有的对象也会被自动销毁. `shared_ptr`可以安全地复制和共享,适用于多个所有者需要访问同一资源的情况. 例如: ``` #include #include class MyClass { public: MyClass() { std::cout << "MyClass constructor" << std::endl; } ~MyClass() { std::cout << "MyClass destructor" << std::endl; } void doSomething() { std::cout << "Doing something..." << std::endl; } }; int main() { // 创建一个 std::shared_ptr 并指向一个新的 MyClass 对象 std::shared_ptr ptr1 = std::make_shared(); std::cout << "Reference count: " << ptr1.use_count() << std::endl; // 复制 ptr1 到 ptr2,引用计数加 1 std::shared_ptr ptr2 = ptr1; std::cout << "Reference count: " << ptr1.use_count() << std::endl; ptr1->doSomething(); ptr2->doSomething(); return 0; } ``` ### std::weak_ptr 弱引用,不拥有对象的所有权,不会增加引用计数,主要用于解决 `std::shared_ptr` 可能存在的**循环引用**问题,可以从 `std::shared_ptr` 或另一个 `std::weak_ptr` 构造,可以通过 `lock()` 方法获取一个 `std::shared_ptr` 来访问对象,如果对象已销毁,则返回一个空的std::shared_ptr `std::weak_ptr` 可以通过 `expired()` 方法检查它所指向的对象是否已经被销毁,如果对象已经被销毁,`expired()` 会返回 `true`;否则返回 `false`. ### 循环引用 由于对象之间相互持有对方的 `std::shared_ptr`,使得它们的引用计数永远不会降为 0,因此形成了循环引用,循环引用最主要的危害就是导致内存泄漏. 例如: ``` #include #include class B; class A { public: std::shared_ptr b_ptr; ~A() { std::cout << "A destructor" << std::endl; } }; class B { public: std::shared_ptr a_ptr; ~B() { std::cout << "B destructor" << std::endl; } }; int main() { std::shared_ptr a = std::make_shared(); std::shared_ptr b = std::make_shared(); a->b_ptr = b; b->a_ptr = a; return 0; } ``` 1. **对象创建**:在 `main` 函数中,使用 `std::make_shared` 创建了 `A` 和 `B` 类型的对象,并分别用 `std::shared_ptr` 进行管理。此时,`A` 和 `B` 对象的初始引用计数都为 1. 2. **循环引用形成**:通过 `a->b_ptr = b;` 和 `b->a_ptr = a;` 这两条语句,`A` 对象持有了对 `B` 对象的引用,`B` 对象也持有了对 `A` 对象的引用。这样,`A` 和 `B` 对象的引用计数都变为 2. 3. **引用计数无法归零**:当 `main` 函数结束时,`a` 和 `b` 这两个 `std::shared_ptr` 会离开作用域并被销毁,它们所指向对象的引用计数会减 1,但是由于循环引用的存在,`A` 和 `B` 对象的引用计数仍然为 1,不会降为 0,因此,这两个对象不会被销毁,**析构函数也不会被调用**,造成了内存泄漏. ## 左值引用和右值引用的区别?(考点: 左值引用和右值引用)【中等】 #### 【简要回答】 左值引用(`&`)绑定具名对象(可寻址),用于避免拷贝;右值引用(`&&`)绑定临时对象(不可寻址),用于移动语义和资源转移。本质区别:左值引用操作原有数据,右值引用允许“窃取”数据。 ------ #### 【详细回答】 1. **底层绑定逻辑** - 左值引用:编译器生成指针,指向已存在的具名对象(内存有明确位置)。 - 右值引用:编译器生成指针,指向临时值(通常在寄存器或即将销毁的内存)。 2. **典型场景** ``` void process(int& lval); // 左值引用:修改传入的变量 void process(int&& rval); // 右值引用:可安全“掏空”临时值 int a = 10; process(a); // 调用左值版本 process(10); // 调用右值版本 process(std::move(a)); // 强制转为右值引用(危险操作!a可能被掏空) ``` 3. **汇编视角** - 左值引用:类似指针操作(`mov eax, [addr]`)。 - 右值引用:直接操作寄存器或优化后的临时内存(避免`memcpy`)。 ------ #### 【知识拓展】 1. **移动语义的底层实现** 移动构造函数通过右值引用直接“窃取”资源指针(如`vector`内部数组指针),将源对象指针置`nullptr`,避免双重释放。 2. **引用折叠规则** 模板中`T&&`可能是通用引用(Universal Reference),根据传入参数推导类型: - `T = int&` → `T&& = int&` - `T = int` → `T&& = int&&` 3. **完美转发原理** `std::forward()` 基于引用折叠,保留原始值类别(左值/右值),常用于泛型代码中参数传递。 4. **生命周期延长** const左值引用可绑定右值(`const string& s = "hello";`),但右值引用更高效(避免隐式拷贝)。 ## 如何使用std::move和std::forward?(考点:右值引用的使用)【中等】 - `std::forward` 是一个实用函数,用于在传递参数时保持其原始的值类别。 - 如果参数是左值,`std::forward` 返回左值引用;如果参数是右值,`std::forward` 返回右值引用。 而 std::move用于将左值转化为右值从而避免资源复制 ## 如何在C++中实现一个不可变的字符串类?(考点:不可变字符串类的实现)【中等】 将内部的字符串指针声明为常量指针加指针常量 不可变动 对外只提供读取函数 或者将内部字符串指针声明为私有成员 对外只提供const的访问函数 将字符串数据设为私有成员,使用构造函数初始化 对方提供const访问函数 禁用拷贝构造函数可以拷贝赋值运算符