Skip to Content

MAP

C++可以看作是C的超集,并引入了面向对象编程(OOP)、泛型编程等强大功能,同时它也像Java一样支持OOP,但内存管理上更接近C/Go(Go有GC但仍有指针概念)。

1. C++ 的核心定位与特点

  • C的超集:几乎所有合法的C代码也是合法的C++代码。C++在C的基础上增加了许多特性。
    • 类比C:你可以直接使用C的头文件(如<stdio.h>,C++推荐用<cstdio>),C的函数库,以及指针、结构体等概念。
  • 面向对象 (OOP):这是C++相对于C最大的增强之一。
    • 类比Java/Python:有类(class)、对象、封装、继承、多态。Java的方法默认是虚方法(virtual),C++需要显式声明virtual关键字。Python的方法也是“虚”的。
  • 泛型编程 (Templates):允许编写不依赖于具体数据类型的代码。
    • 类比Java的泛型 (Generics):如 List<String>。C++的模板更为强大,是编译期特性,可以进行更复杂的元编程。
    • 类比Go的泛型 (Generics, 1.18+):Go的泛型相对C++模板更简单一些。
  • 手动内存管理与RAII:你需要手动管理动态分配的内存(new/delete)。但C++推崇RAII(Resource Acquisition Is Initialization)思想,通过类(特别是智能指针)来自动化资源管理。
    • 类比Cmalloc/free 对应 C++ 的 new/delete
    • 对比Java/Python/Go:这些语言有自动垃圾回收(GC)。C++没有内置GC,RAII是主要的资源管理模式。
  • 性能与底层控制:C++提供了接近硬件的控制能力,性能是其核心优势之一。
    • 类比C/Go:都强调性能和对底层的访问能力。

2. 基本语法结构

  • Hello World 程序
    #include <iostream> // 类似 Java 的 import, Python 的 import, Go 的 import int main() { // 类似 C 的 main, Java 的 public static void main(String[] args), Go 的 func main() std::cout << "Hello, World!" << std::endl; // std::cout 类似 System.out, print(), fmt.Print // << 是输出运算符 // std::endl 是换行并刷新缓冲区 return 0; // 程序正常结束的返回值 }
  • 注释
    • 单行注释:// comment (同Java, Go, C99+)
    • 多行注释:/* comment */ (同C, Java, Python docstrings有点像但用途不同)
  • 分号:语句结束必须使用分号 ; (同C, Java, Go在很多情况下会自动插入)
  • 头文件与命名空间
    • #include <filename>:引入标准库头文件。
    • #include "filename.h":引入用户自定义头文件。
      • 类比:Java的import,Python的import,Go的import。C++的头文件通常包含声明,而实现放在.cpp文件。
    • namespace:避免命名冲突。
      namespace MyStuff { int value = 10; } // 使用: MyStuff::value // 或者 using namespace MyStuff; (不推荐在头文件或全局作用域)
      • 类比:Java的package,Python的模块名,Go的packagestd:: 就是标准库所在的命名空间。

3. 数据类型与变量

  • 基本数据类型
    • int, short, long, long long:整型 (同C, Java, Go有int32, int64等)
    • float, double, long double:浮点型 (同C, Java, Go有float32, float64)
    • char:字符型 (通常1字节,同C, Java的char是2字节Unicode)
    • bool:布尔型 (true, false) (同Java, Python, Go)
    • void:无类型 (同C, Java, Go)
  • 类型修饰符signed, unsigned (主要用于整型,C特性)
  • 变量声明与初始化
    int age; // 声明,类似 C, Java, Go (var age int) age = 30; // 赋值 double salary = 1000.50; // 声明并初始化 char initial = 'J'; bool isEmployed = true; // C++11 引入 auto 类型推断 (类似 Go 的 :=, Python 的动态类型) auto count = 10; // count 被推断为 int auto pi = 3.14; // pi 被推断为 double auto name = "Alice"; // name 被推断为 const char* (C风格字符串)
  • 常量
    • const:定义常量,一旦初始化后不可修改。
      const double PI = 3.14159; // PI = 3.0; // 编译错误
      • 类比Java的final,Go的const,Python没有严格意义的常量(约定俗成大写)。
    • #define:预处理器宏,C风格,C++中通常推荐用constenum
      #define MAX_USERS 100 // C风格

4. 运算符

  • C++特有或需注意的
    • :: (作用域解析运算符):用于访问命名空间成员、类静态成员、或在继承中指定基类成员。
    • -> (成员指针访问):pointer->member 等价于 (*pointer).member
      • 类比C的结构体指针,Go的指针也会自动解引用访问成员。
    • new, delete:动态内存分配和释放运算符。
    • typeid:运行时类型信息。
    • sizeof:计算类型或对象占用的字节数 (同C)。

5. 控制流

  • 条件语句if, else if, else (同C, Java, Go, Python)
    if (condition1) { // ... } else if (condition2) { // ... } else { // ... }
  • Switch 语句:(同C, Java, Go)
    switch (expression) { case constant1: // statements break; // 重要!否则会 "fall-through" case constant2: // statements break; default: // statements }
  • 循环语句
    • for 循环 (C风格,同Java, Go)
      for (int i = 0; i < 10; ++i) { std::cout << i << std::endl; }
    • Range-based for loop (C++11) (类似Python的for item in iterable, Java的增强for, Go的for ... range)
      #include <vector> std::vector<int> numbers = {1, 2, 3, 4, 5}; for (int num : numbers) { // 拷贝每个元素 std::cout << num << " "; } for (auto& num : numbers) { // 引用每个元素,可修改 num *= 2; }
    • while 循环 (同C, Java, Go, Python)
      while (condition) { // ... }
    • do-while 循环 (同C, Java)
      do { // ... } while (condition);
  • break, continue, goto (同C, Java (无goto但有标签break/continue), Go (有goto))

6. 函数 (Functions)

  • 定义与声明 (原型)
    // 函数声明 (原型) - 通常放在头文件中或使用前 return_type function_name(parameter_list); // 函数定义 - 实现 return_type function_name(parameter_list) { // body return value; // 如果 return_type 不是 void } // 示例 int add(int a, int b); // 声明 int main() { int sum = add(5, 3); std::cout << "Sum: " << sum << std::endl; return 0; } int add(int a, int b) { // 定义 return a + b; }
    • 类比C:C也强调声明和定义的分离。
    • 类比Java:Java中方法声明和定义通常在一起,在类内部。
    • 类比Go/Python:函数定义即声明。
  • 参数传递
    • 传值 (Pass by Value):默认方式,函数内修改不影响外部。 (同C基本类型, Java基本类型, Go)
    • 传指针 (Pass by Pointer):传递内存地址,函数内可修改外部变量。 (同C, Go)
      void increment_ptr(int* p) { (*p)++; } int x = 5; increment_ptr(&x); // x 变为 6
    • 传引用 (Pass by Reference - C++特有):传递变量的别名,更安全简洁的指针用法。
      void increment_ref(int& r) { r++; } int y = 5; increment_ref(y); // y 变为 6
      • 类比Java的对象传递:Java中对象参数传递的是引用的副本,行为上类似C++的传引用(都能修改对象状态),但底层机制不同。
  • 函数重载 (Function Overloading):允许定义多个同名函数,但参数列表(类型、数量、顺序)必须不同。
    int compute(int a); double compute(double a); int compute(int a, int b);
    • 类比Java的方法重载。
    • **Go不支持函数重载,Python通过默认参数和*args/kwargs实现类似效果。C不支持。
  • 默认参数
    void print_msg(const std::string& msg, int times = 1) { for (int i = 0; i < times; ++i) { std::cout << msg << std::endl; } } print_msg("Hello"); // times = 1 print_msg("Hi", 3); // times = 3
    • 类比Python的默认参数。Java不支持。

7. 指针 (Pointers) 与引用 (References)

  • 指针:存储变量的内存地址。C++的指针非常强大但也容易出错(悬垂指针、野指针等)。
    int var = 10; int* ptr = &var; // ptr 存储 var 的地址 std::cout << "Value of var: " << var << std::endl; // 10 std::cout << "Address of var: " << &var << std::endl; // 内存地址 std::cout << "Value of ptr: " << ptr << std::endl; // 内存地址 (同上) std::cout << "Value pointed to by ptr: " << *ptr << std::endl; // 10 (解引用) *ptr = 20; // 修改 var 的值通过指针 std::cout << "New value of var: " << var << std::endl; // 20
    • 类比C的指针。Go也有指针,但不能进行指针算术。Java/Python没有直接暴露的指针。
  • 引用:变量的别名,一旦初始化后不能改变引用的对象。比指针更安全,使用更简洁。
    int val = 100; int& ref = val; // ref 是 val 的别名,必须在声明时初始化 std::cout << "Value of val: " << val << std::endl; // 100 std::cout << "Value of ref: " << ref << std::endl; // 100 ref = 200; // 修改 ref 实际上是修改 val std::cout << "New value of val: " << val << std::endl; // 200 // int& ref2; // 错误,引用必须初始化
    • C++特有。Java中的对象变量本质上是引用,但和C++引用机制不同。

8. 面向对象编程 (OOP)

  • 类 (Class):用户自定义的数据类型,包含数据成员(属性)和成员函数(方法)。
    class Dog { public: // 访问修饰符 // 构造函数 (Constructor) Dog(std::string n, int a) : name(n), age(a) { //成员初始化列表 std::cout << "Dog " << name << " created." << std::endl; } // 析构函数 (Destructor) - C++ 特有,用于资源释放 ~Dog() { std::cout << "Dog " << name << " destroyed." << std::endl; } // 成员函数 void bark() { std::cout << name << " says Woof!" << std::endl; } std::string getName() const { // const 成员函数,不修改对象状态 return name; } private: // 访问修饰符 std::string name; int age; }; int main() { Dog myDog("Buddy", 3); // 创建对象,调用构造函数 myDog.bark(); // 调用成员函数 // 对象超出作用域或被 delete 时,析构函数自动调用 }
    • 类比Java/Python的class
    • 构造函数:类比Java构造器,Python __init__。C++构造函数可以重载。
    • 析构函数 (~ClassName):C++特有,在对象销毁时自动调用,常用于释放new分配的资源。Java有finalize()(不推荐),Python有__del__(不推荐),Go用defer语句管理资源。
    • 访问修饰符public, private, protected (类似Java)。Python用命名约定 (___)。
    • this 指针:指向当前对象的指针 (类似Java的this,Python的self)。
  • 对象 (Object):类的实例。
    • 栈上创建:Dog myDog("Buddy", 3); (自动管理生命周期)
    • 堆上创建:Dog* pDog = new Dog("Rex", 5); (需要手动 delete pDog; 来释放内存并调用析构函数)
  • 继承 (Inheritance)
    class GoldenRetriever : public Dog { // public 继承 public: GoldenRetriever(std::string n, int a) : Dog(n, a) {} // 调用基类构造函数 void fetch() { std::cout << getName() << " is fetching!" << std::endl; } };
    • 类比Java的extends,Python的class Child(Parent): C++支持多重继承(Java通过接口实现多重继承)。
  • 多态 (Polymorphism):通过基类指针或引用调用派生类的重写方法。
    • 虚函数 (Virtual Functions):在基类中用virtual关键字声明,派生类可以override(C++11推荐)它。
      class Animal { public: virtual void makeSound() { // 虚函数 std::cout << "Generic animal sound" << std::endl; } virtual ~Animal() {} // 基类析构函数通常应为虚函数,确保正确析构 }; class Cat : public Animal { public: void makeSound() override { // C++11 override 关键字 std::cout << "Meow" << std::endl; } }; Animal* pAnimal = new Cat(); pAnimal->makeSound(); // 调用 Cat 的 makeSound() delete pAnimal; // 正确调用 Cat 的析构函数,然后 Animal 的析构函数
      • Java中所有非static、非final、非private方法默认都是虚方法。Python方法天然是“虚”的。Go通过接口实现多态。C需要手动用函数指针实现。
  • 抽象类与纯虚函数
    class Shape { public: virtual double getArea() = 0; // 纯虚函数,使 Shape 成为抽象类 virtual ~Shape() {} }; // Shape s; // 错误,不能实例化抽象类
    • 类比Java的抽象类和抽象方法,Python的abc模块。

9. 内存管理

  • 栈 (Stack):局部变量、函数参数。自动分配和释放。
  • 堆 (Heap):动态分配的内存。
    • new:分配内存并调用构造函数。
      int* p_int = new int; int* p_arr = new int[10]; Dog* p_dog = new Dog("Spot", 2);
    • delete:调用析构函数并释放单个对象的内存。
    • delete[]:调用数组中每个对象的析构函数并释放数组内存。
      delete p_int; delete[] p_arr; delete p_dog;
    • 忘记delete会导致内存泄漏。使用已delete的指针是未定义行为(悬垂指针)。
    • 对比:Java/Python/Go有垃圾回收器自动管理堆内存。C使用malloc/free
  • RAII (Resource Acquisition Is Initialization):C++核心思想。通过对象的生命周期管理资源。
    • 智能指针 (Smart Pointers - C++11及以后)std::unique_ptr, std::shared_ptr, std::weak_ptr。它们在对象超出作用域时自动释放所管理的内存。
      #include <memory> std::unique_ptr<Dog> uDog = std::make_unique<Dog>("Unique", 1); std::shared_ptr<Dog> sDog1 = std::make_shared<Dog>("Shared", 2); std::shared_ptr<Dog> sDog2 = sDog1; // 共享所有权,引用计数增加 // 当 uDog, sDog1, sDog2 超出作用域时,Dog 对象会自动被 delete
      • 强烈推荐使用智能指针来避免手动new/delete带来的问题。

10. 标准模板库 (STL - Standard Template Library)

  • 非常强大和高效的库,包含:
    • 容器 (Containers):存储数据的类模板。
      • std::vector:动态数组 (类似Java ArrayList, Python list, Go slice)
      • std::list:双向链表
      • std::deque:双端队列
      • std::map:键值对集合,有序 (类似Java TreeMap, Python dict但Python dict在3.7+后保持插入顺序)
      • std::unordered_map:哈希表 (类似Java HashMap, Python dict, Go map)
      • std::set:唯一元素集合,有序
      • std::unordered_set:哈希集合
      • std::string:字符串类 (比C风格字符串char*更安全易用)
    • 算法 (Algorithms):对容器数据进行操作的函数模板。
      • std::sort, std::find, std::copy, std::remove, std::accumulate 等。
      • 类比Java的Collections类,Python内置的列表方法和itertools等。
    • 迭代器 (Iterators):提供统一访问容器元素的方式,像指针一样。
      #include <vector> #include <algorithm> // For std::sort #include <iostream> std::vector<int> vec = {3, 1, 4, 1, 5, 9}; std::sort(vec.begin(), vec.end()); // vec.begin() 和 vec.end() 返回迭代器 for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) { std::cout << *it << " "; } // 或者使用 range-based for: // for (int val : vec) { std::cout << val << " "; }
      • 类比Java的Iterator,Python的迭代器协议。

11. 异常处理 (Exception Handling)

  • try, catch, throw 机制。
    #include <stdexcept> // For standard exceptions like std::runtime_error double divide(int a, int b) { if (b == 0) { throw std::runtime_error("Division by zero!"); } return static_cast<double>(a) / b; } int main() { try { double result = divide(10, 0); std::cout << "Result: " << result << std::endl; } catch (const std::runtime_error& e) { std::cerr << "Exception caught: " << e.what() << std::endl; } catch (...) { // 捕获所有类型的异常 (不推荐,除非你知道你在做什么) std::cerr << "Unknown exception caught!" << std::endl; } return 0; }
    • 类比Java和Python的异常处理。Go使用显式的错误返回值。C通常使用错误码。

12. 预处理器 (Preprocessor)

  • 在编译之前处理代码。
    • #include:包含文件内容。
    • #define:定义宏 (类似C)。
    • #ifdef, #ifndef, #if, #else, #elif, #endif:条件编译。
    • Java/Python/Go没有这种文本替换式的预处理器。Go有构建标签实现条件编译。

13. 模板 (Templates - 泛型编程)

  • 允许编写不依赖于特定类型的代码(函数模板、类模板)。
    // 函数模板 template <typename T> T maximum(T a, T b) { return (a > b) ? a : b; } // int max_i = maximum(10, 20); // double max_d = maximum(3.14, 2.71); // 类模板 template <typename T, int SIZE> class Array { private: T m_data[SIZE]; public: T& operator[](int index) { return m_data[index]; } // ... }; // Array<int, 10> intArray; // Array<double, 5> doubleArray;
    • 类比Java泛型,但C++模板是编译期多态,更强大(例如,可以有非类型模板参数如SIZE),会为每个使用的类型特化生成代码(可能导致代码膨胀)。Go 1.18+引入了泛型。

构造方法

构造方法(或构造函数)是类中一种特殊的成员函数,它的主要职责是在创建类的新对象时初始化该对象的数据成员。当你创建一个类的实例时,构造函数会自动被调用。

核心特点:

  1. 名称与类名相同:构造函数的名称必须与它所属的类的名称完全一致。
  2. 没有返回类型:构造函数不返回任何值,甚至连 void 也不写。
  3. 自动调用:在创建对象时(例如,声明一个栈对象,或使用 new 在堆上分配对象),构造函数会自动执行。
  4. 可以重载 (Overloaded):一个类可以有多个构造函数,只要它们的参数列表(参数的数量、类型或顺序)不同即可。
  5. 可以有参数:允许在创建对象时传递初始值。
  6. 不能被显式调用 (通常):你通常不会像调用普通成员函数那样直接调用构造函数(例如 object.ConstructorName())。例外是使用 placement new 或在构造函数中显式调用(C++11的委托构造函数)。

1. 默认构造函数 (Default Constructor)

  • 定义:不带任何参数的构造函数,或者所有参数都有默认值的构造函数。
  • 编译器生成:如果你没有为类定义任何构造函数,C++编译器会自动为你生成一个公共的、内联的默认构造函数。这个编译器生成的默认构造函数通常什么也不做(对于基本类型成员),或者会调用成员对象的默认构造函数(如果它们是类类型)。
  • 何时需要显式定义
    • 当你需要自定义对象的默认初始化行为时。
    • 如果类中包含需要特定初始化的成员(如指针、引用、const 成员,或没有默认构造函数的成员对象)。
    • 重要:一旦你为类定义了任何构造函数(即使是带参数的),编译器就不会再自动生成默认构造函数了。如果此时你还需要一个无参构造函数,就必须显式定义它。
  • C++11 = default;:你可以显式地告诉编译器使用它自动生成的版本:
    class MyClass { public: MyClass() = default; // 显式要求编译器生成默认构造函数 // ... };
  • 语法示例
    class Box { public: double length; double width; double height; // 1. 用户定义的默认构造函数 Box() { length = 1.0; width = 1.0; height = 1.0; std::cout << "Default Box constructor called." << std::endl; } }; Box b1; // 调用 Box()
  • 类比
    • Java: 类似Java中编译器自动生成的无参构造函数。如果你定义了其他构造函数,Java编译器也不会自动生成无参构造函数。
    • Python: __init__(self) 如果没有其他参数,可以看作是默认初始化行为。
    • Go: Go的struct可以直接用 MyStruct{} 初始化,所有字段都会被零值初始化,这有点像默认构造的行为。

2. 参数化构造函数 (Parameterized Constructor)

  • 定义:接受一个或多个参数的构造函数,用于根据传入的参数初始化对象。
  • 语法示例
    class Box { public: double length; double width; double height; // 参数化构造函数 Box(double l, double w, double h) { length = l; width = w; height = h; std::cout << "Parameterized Box constructor called." << std::endl; } }; Box b2(10.0, 5.0, 2.0); // 调用 Box(double, double, double)
  • 类比
    • Java: public Box(double l, double w, double h) { this.length = l; ... }
    • Python: def __init__(self, l, w, h): self.length = l ...
    • Go: 通常通过工厂函数或直接字面量初始化 func NewBox(l, w, h float64) *Box { return &Box{length:l, ...} }b := Box{length:10, ...}

3. 成员初始化列表 (Member Initializer List)

  • 定义:一种在构造函数体执行之前初始化数据成员的特殊语法。它位于构造函数参数列表之后,函数体 {} 之前,以冒号 : 开始。
  • 为什么使用?
    1. 效率:对于类类型的成员,使用初始化列表是直接调用该成员的相应构造函数进行初始化。如果在构造函数体内部赋值,则是先调用成员的默认构造函数,然后再进行赋值操作,可能效率较低。
    2. 必要性
      • 初始化 const 成员(const 成员必须在定义时初始化)。
      • 初始化引用成员(引用成员必须在定义时初始化)。
      • 初始化没有默认构造函数的成员对象。
      • 调用基类的构造函数(在继承中)。
  • 初始化顺序:成员初始化的顺序与它们在类中声明的顺序一致,而不是它们在初始化列表中出现的顺序。这是一个常见的陷阱!
  • 语法示例
    class Point { int x_coord; int y_coord; public: Point(int x, int y) : x_coord(x), y_coord(y) { // 初始化列表 // 构造函数体可以为空,或进行其他设置 std::cout << "Point (" << x_coord << ", " << y_coord << ") created." << std::endl; } }; class Window { const int id; Point topLeft; std::string& titleRef; // 引用成员 public: Window(int winId, int x, int y, std::string& title) : id(winId), // 初始化 const 成员 topLeft(x, y), // 初始化 Point 成员对象 (调用 Point 的构造函数) titleRef(title) // 初始化引用成员 { std::cout << "Window " << id << " with title '" << titleRef << "' created." << std::endl; } }; std::string myTitle = "My App"; Window w(1, 10, 20, myTitle);
  • 类比
    • Java: this.member = value; 在构造函数体中完成。对于 final 成员,必须在构造函数结束前赋值。调用父类构造函数使用 super(...),必须是第一条语句。
    • Python: self.member = value__init__ 方法体中。
    • Go: 结构体字面量 MyStruct{Field1: val1, Field2: val2} 类似初始化列表的精神,直接初始化。

4. 拷贝构造函数 (Copy Constructor)

  • 定义:一个特殊的构造函数,它接受同一类型的另一个对象作为参数(通常是 const 引用),并创建一个新对象作为参数对象的副本。
  • 签名ClassName(const ClassName& other)
  • 编译器生成:如果你不提供拷贝构造函数,编译器会生成一个。这个默认的拷贝构造函数执行成员逐一拷贝 (member-wise copy),也称为浅拷贝 (shallow copy)
  • 何时需要显式定义 (深拷贝 - Deep Copy)
    • 当类中包含指针成员,并且该指针指向动态分配的内存时。默认的浅拷贝只会复制指针的值(地址),导致两个对象共享同一块内存。当一个对象被销毁并释放内存后,另一个对象的指针就变成了悬垂指针。深拷贝会为新对象分配新的内存,并复制原对象所指向内存的内容。
    • 当类管理其他需要特殊复制逻辑的资源(如文件句柄、网络连接等)。
  • 何时被调用
    1. 当用一个对象去初始化另一个同类型的对象时:MyClass obj2 = obj1;MyClass obj2(obj1);
    2. 当对象按值传递给函数时:void func(MyClass obj);
    3. 当函数按值返回对象时:MyClass createObject() { MyClass temp; return temp; } (编译器通常有返回值优化RVO/NRVO来避免实际拷贝)
  • 语法示例 (深拷贝)
    class MyString { private: char* str; int len; public: MyString(const char* s = "") { // 普通构造函数 (也充当默认构造函数) len = std::strlen(s); str = new char[len + 1]; std::strcpy(str, s); std::cout << "MyString created: " << str << std::endl; } // 拷贝构造函数 (深拷贝) MyString(const MyString& other) : len(other.len) { str = new char[len + 1]; // 分配新内存 std::strcpy(str, other.str); // 复制内容 std::cout << "MyString copied (deep): " << str << std::endl; } ~MyString() { std::cout << "MyString destroyed: " << (str ? str : "nullptr") << std::endl; delete[] str; str = nullptr; // 好习惯 } void print() const { std::cout << (str ? str : "") << std::endl; } }; MyString s1("Hello"); // 普通构造 MyString s2 = s1; // 调用拷贝构造函数 MyString s3(s1); // 调用拷贝构造函数 s1.print(); s2.print();
  • 类比
    • Java: Java 对象变量是引用。Object obj2 = obj1; 只是复制引用。要实现深拷贝,需要实现 Cloneable 接口并重写 clone() 方法,或者手动创建新对象并复制字段。
    • Python: obj2 = obj1 是引用赋值。obj2 = copy.copy(obj1) (浅拷贝),obj2 = copy.deepcopy(obj1) (深拷贝)。
    • Go: 结构体赋值 s2 := s1 是值拷贝(浅拷贝,如果结构体包含指针或切片,则它们指向的数据是共享的)。深拷贝需要手动实现。

5. 移动构造函数 (Move Constructor - C++11)

  • 定义:一个特殊的构造函数,它接受一个右值引用 (rvalue reference, &&)作为参数。它的目的是“窃取”源对象(通常是临时对象或即将被销毁的对象)的资源,而不是复制它们。这可以显著提高性能,尤其对于管理大型动态分配资源的类。
  • 签名ClassName(ClassName&& other) noexcept (noexcept 表示该函数不会抛出异常,对标准库优化很重要)
  • 何时被调用
    • 当用一个右值(如临时对象、std::move() 的结果)初始化对象时。
    • 函数按值返回对象时(如果RVO/NRVO不适用,会优先尝试移动)。
  • 编译器生成:条件性生成。如果用户没有声明拷贝构造函数、拷贝赋值运算符、移动赋值运算符或析构函数,编译器可能会生成一个。
  • 核心思想:源对象的资源(如指向动态内存的指针)被转移给新对象,源对象通常被置于一个有效的、但“空的”或可析构的状态(例如,指针设为 nullptr)。
  • 语法示例
    // 续 MyString 类 class MyString { // ... (之前的构造函数、析构函数等) ... public: // 移动构造函数 MyString(MyString&& other) noexcept : str(other.str), len(other.len) { other.str = nullptr; // 窃取资源,并将源对象置于有效但空的状态 other.len = 0; std::cout << "MyString moved: " << (str ? str : "nullptr") << std::endl; } // ... (拷贝赋值、移动赋值运算符等通常也需要) }; MyString createTempString() { return MyString("Temporary"); // 返回临时对象,可能触发移动构造 } MyString s4("Original"); MyString s5 = std::move(s4); // 显式移动 s4 的资源给 s5 // s4 现在处于有效但未指定的状态 (其 str 为 nullptr) MyString s6 = createTempString(); // 返回的临时对象被移动到 s6
  • 类比
    • Java/Python/Go: 这些语言主要依赖垃圾回收和引用计数,没有C++中这种显式的、底层的资源所有权转移概念。Go中的某些操作(如切片传递)可以看作是高效的“共享”或“轻量级拷贝”,但与C++的移动语义不完全相同。

6. 委托构造函数 (Delegating Constructors - C++11)

  • 定义:允许一个构造函数调用同一个类中的另一个构造函数来完成部分或全部初始化工作。这有助于减少代码冗余。
  • 语法:使用成员初始化列表的语法。
    class Data { int a; double b; std::string c; public: // 目标构造函数 Data(int val_a, double val_b, const std::string& val_c) : a(val_a), b(val_b), c(val_c) { std::cout << "Target constructor called." << std::endl; } // 委托构造函数 1 Data(int val_a, double val_b) : Data(val_a, val_b, "default_string") { std::cout << "Delegating constructor 1 called." << std::endl; // 可以添加额外的初始化逻辑 } // 委托构造函数 2 Data() : Data(0, 0.0, "empty") { std::cout << "Delegating constructor 2 (default) called." << std::endl; } }; Data d1; // Calls Data() -> Data(0, 0.0, "empty") Data d2(10, 3.14); // Calls Data(10, 3.14) -> Data(10, 3.14, "default_string")
  • 类比
    • Java: this(...) 语法,必须是构造函数的第一条语句。
    • Python: 可以在 __init__ 中调用辅助方法,或者使用默认参数和 *args, **kwargs 来达到类似代码复用的效果。

7. explicit 关键字

  • 目的:用于修饰单参数(或除第一个参数外其他参数都有默认值的)构造函数,以防止不期望的隐式类型转换。
  • 示例
    class Foo { public: // explicit Foo(int x) { /* ... */ } // 如果加上 explicit Foo(int x) { std::cout << "Foo(int) called with " << x << std::endl; } }; void processFoo(Foo f) { /* ... */ } // Foo myFoo = 10; // 如果 Foo(int) 是 explicit,这行会编译错误 // 如果不是 explicit,这行OK,10 会隐式转换为 Foo(10) Foo myFoo(10); // 显式构造,总是OK // processFoo(20); // 如果 Foo(int) 是 explicit,这行会编译错误 // 如果不是 explicit,这行OK,20 会隐式转换为 Foo(20) 然后传参 processFoo(Foo(20)); // 显式构造,总是OK
  • 建议:通常,除非你有充分的理由允许隐式转换,否则将单参数构造函数声明为 explicit 是一个好习惯。

纯虚函数

理解纯虚函数的关键在于理解它与 抽象类 (Abstract Class)接口 (Interface) 概念的联系。

1. 什么是纯虚函数?

纯虚函数是在基类中声明的虚函数,但它在基类中没有具体的实现。它只是定义了一个接口,并强制派生类提供该函数的具体实现。

语法: 在虚函数的声明末尾加上 = 0 即可将其声明为纯虚函数。

class Base { public: // 这是一个纯虚函数 virtual void show() = 0; // 虚析构函数,对于多态基类是个好习惯 virtual ~Base() {} // 或者也可以是 ~Base() = default; };

2. 纯虚函数的特点和作用:

  • 没有函数体(通常情况下): 纯虚函数在基类中通常不提供实现。它的目的就是告诉派生类:“你必须实现这个名为 show 并且签名与我相同的函数。”

    • 注意一个特例:纯虚函数可以有定义(实现),但这很少见。如果定义了,它必须在类的外部定义。即使有定义,该类仍然是抽象的,派生类仍然必须override它才能成为具体类。派生类在覆盖时,可以通过 BaseClassName::pureVirtualFunctionName() 来调用基类的这个(罕见的)实现。
  • 使类成为抽象类 (Abstract Class): 任何包含至少一个纯虚函数的类都会自动成为抽象类。

    class Shape { public: virtual double getArea() = 0; // Shape 现在是抽象类 virtual void draw() = 0; // Shape 仍然是抽象类 virtual ~Shape() {} };
  • 抽象类不能被实例化: 你不能创建抽象类的对象。

    // Shape myShape; // 编译错误!Shape 是抽象类,不能实例化。

    这是合理的,因为抽象类中至少有一个函数没有实现,创建它的对象没有意义(调用那个未实现的函数会怎么样呢?)。

  • 强制派生类实现: 如果一个派生类继承自一个抽象基类,那么这个派生类必须为基类中所有的纯虚函数提供具体的实现(即 override 它们)。如果派生类没有实现所有继承来的纯虚函数,那么这个派生类自身也会成为抽象类。

    class Circle : public Shape { private: double radius; public: Circle(double r) : radius(r) {} // 必须实现 Shape::getArea() double getArea() override { return 3.14159 * radius * radius; } // 必须实现 Shape::draw() void draw() override { std::cout << "Drawing a circle with radius " << radius << std::endl; } }; // Circle c(5.0); // 正确!Circle 实现了所有纯虚函数,是具体类。 class Line : public Shape { public: // 假设 Line 只实现了 getArea,没有实现 draw double getArea() override { return 0; /* 线的面积为0 */ } // 因为没有实现 draw(),Line 仍然是抽象类 }; // Line myLine; // 编译错误!Line 也是抽象类。
  • 用于定义接口: 纯虚函数是C++中实现“接口”概念的主要方式。一个只包含纯虚函数(和一个虚析构函数)的抽象类,其行为非常类似于Java或C#中的interface。它规定了一组派生类必须遵守的契约或行为规范。

    • 类比 Java:

      // Java 接口 interface Drawable { void draw(); double getArea(); } // C++ 抽象类作为接口 class IDrawable { // "I" 前缀常用于表示接口 public: virtual void draw() = 0; virtual double getArea() = 0; virtual ~IDrawable() {} // 虚析构函数 };

      Java的interface中的方法默认就是public abstract(在Java 8之前)。C++中需要显式使用virtual= 0

    • 类比 Go: Go的interface是隐式实现的。任何类型只要实现了接口中定义的所有方法,就被认为实现了该接口。C++的抽象类和纯虚函数则需要显式的继承关系。但目标都是定义一组行为。

    • 类比 Python: Python中,通常使用abc模块 (Abstract Base Classes) 和@abstractmethod装饰器来实现类似的功能,以强制子类实现某些方法。

      from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def get_area(self): pass @abstractmethod def draw(self): pass
  • 实现多态: 纯虚函数是实现运行时多态的基础。你可以使用基类指针或引用来指向派生类对象,并调用这些(在派生类中实现的)虚函数。

    Shape* ptrShape1 = new Circle(5.0); Shape* ptrShape2 = new Rectangle(4.0, 6.0); // 假设 Rectangle 也实现了 Shape 接口 ptrShape1->draw(); // 调用 Circle::draw() std::cout << "Area: " << ptrShape1->getArea() << std::endl; // 调用 Circle::getArea() ptrShape2->draw(); // 调用 Rectangle::draw() std::cout << "Area: " << ptrShape2->getArea() << std::endl; // 调用 Rectangle::getArea() delete ptrShape1; delete ptrShape2;

3. 纯虚析构函数 (Pure Virtual Destructor) - 一个特殊情况

一个析构函数也可以被声明为纯虚的:

class AbstractBase { public: virtual ~AbstractBase() = 0; // 纯虚析构函数 };

特殊之处在于:即使析构函数是纯虚的,你也必须为它提供一个定义(实现)

// 实现必须提供,即使是空的 AbstractBase::~AbstractBase() { // 通常是空的,或者做一些基类特定的清理 std::cout << "AbstractBase destructor called." << std::endl; }

为什么必须定义? 当删除一个派生类对象(通过基类指针)时,析构函数的调用链是从派生类析构函数到基类析构函数。如果基类析构函数没有定义,链接器会在试图链接对它的调用时出错。

为什么要用纯虚析构函数? 主要目的是使类成为抽象类,而不需要任何其他成员函数成为纯虚的。有时你可能希望一个类是抽象的(不能被实例化),但它的所有其他方法都有合理的默认实现。将析构函数设为纯虚是实现这一目标的一种方法。但通常,如果一个类应该是抽象的,它自然会有一个或多个操作是依赖于具体子类去定义的,这些操作就适合做成纯虚函数。

总结一下纯虚函数的要点:

  1. 声明virtual ReturnType functionName(params) = 0;
  2. 目的:定义一个接口,强制派生类实现该函数。
  3. 结果:包含纯虚函数的类成为抽象类。
  4. 实例化:抽象类不能被实例化。
  5. 派生类责任:派生类必须实现所有继承的纯虚函数才能成为具体类(可实例化)。
  6. 多态基石:与虚函数一起,是C++运行时多态的核心。
  7. 类比:类似于Java中的abstract方法或interface中的方法。

纯虚函数是面向对象设计中一个非常强大的工具,它允许你定义清晰的类层次结构和接口,从而构建出更灵活、可扩展和可维护的系统。当你发现一个基类中的某个操作没有通用的实现,而是完全依赖于派生类的具体类型时,这个操作就应该被设计成纯虚函数。

析构函数

析构函数是类中一种特殊的成员函数,它在对象的生命周期结束时自动被调用。它的主要职责是执行清理工作,例如释放对象在生命周期内获取的资源(特别是动态分配的内存)、关闭文件、断开网络连接等。

核心特点:

  1. 名称:析构函数的名称是在类名前加上一个波浪号 ~。例如,如果类名是 MyClass,则析构函数名为 ~MyClass
  2. 没有返回类型:和构造函数一样,析构函数不返回任何值,甚至连 void 也不写。
  3. 没有参数:析构函数不能接受任何参数。因此,一个类只能有一个析构函数(不能重载)。
  4. 自动调用:析构函数在以下情况下会自动被调用:
    • 栈对象 (Stack Object):当对象的生命周期结束(例如,离开其定义的作用域)时。
      void func() { MyClass obj; // 构造函数调用 // ... obj is used ... } // obj 离开作用域,~MyClass() 自动调用
    • 堆对象 (Heap Object):当通过 delete 运算符删除一个指向动态分配对象的指针时。
      MyClass* ptr = new MyClass(); // 构造函数调用 // ... ptr is used ... delete ptr; // ~MyClass() 自动调用,然后内存被释放
      对于动态分配的数组,使用 delete[]:
      MyClass* arr = new MyClass[5]; // 5个对象的构造函数被调用 // ... arr is used ... delete[] arr; // 5个对象的析构函数被调用(逆序),然后数组内存被释放
    • 临时对象:当一个临时对象不再需要时(例如,在表达式求值完毕后)。
    • 容器中的对象:当包含对象的容器(如 std::vector)被销毁或从中移除元素时,元素的析构函数会被调用。
  5. 不能被显式调用 (通常):一般情况下,你不需要也不应该显式调用析构函数。让编译器在适当的时候自动调用它。显式调用析构函数(如 obj.~MyClass())是合法的,但除非你完全清楚自己在做什么(例如在placement new的场景下手动管理内存),否则应避免这样做,因为它可能导致对象被多次析构,引发未定义行为。
  6. 编译器生成:如果你没有为类定义析构函数,C++编译器会自动为你生成一个公共的、内联的默认析构函数。
    • 这个编译器生成的析构函数通常是空的。
    • 如果类的数据成员是其他类的对象,并且这些成员类有自己的析构函数,那么编译器生成的析构函数会自动调用这些成员对象的析构函数。
    • 如果基类有析构函数,编译器生成的派生类析构函数也会自动调用基类的析构函数。

为什么需要析构函数?

C++没有像Java或Python那样的自动垃圾回收机制来回收所有类型的资源。虽然对于基本数据类型和栈上对象,内存管理是自动的,但对于动态分配的内存(通过 new)或其他系统资源(文件句柄、网络连接、互斥锁等),开发者需要负责显式释放。

析构函数提供了一个在对象销毁前执行必要清理操作的标准化位置。

何时需要显式定义析构函数?

你需要显式定义析构函数的主要情况是:

  1. 类管理动态分配的资源: 如果你的类在构造函数中或在其生命周期内通过 new (或 new[]) 分配了内存,那么你必须在析构函数中使用 delete (或 delete[]) 来释放这些内存,以防止内存泄漏。
    class MyBuffer { private: int* data; size_t size; public: MyBuffer(size_t s) : size(s) { data = new int[size]; // 分配资源 std::cout << "MyBuffer created, allocated " << size << " ints." << std::endl; } ~MyBuffer() { // 显式定义的析构函数 delete[] data; // 释放资源 data = nullptr; // 好习惯,防止悬垂指针 size = 0; std::cout << "MyBuffer destroyed, memory released." << std::endl; } // ... 其他成员函数 ... };
  2. 类管理其他类型的资源: 例如,如果类打开了一个文件,需要在析构函数中关闭它;如果获取了一个锁,需要在析构函数中释放它。
    #include <fstream> class FileWriter { private: std::ofstream file_stream; std::string filename; public: FileWriter(const std::string& fname) : filename(fname) { file_stream.open(filename); if (file_stream.is_open()) { std::cout << "File " << filename << " opened." << std::endl; } } ~FileWriter() { if (file_stream.is_open()) { file_stream.close(); std::cout << "File " << filename << " closed." << std::endl; } } // ... 其他成员函数 ... };
  3. 作为多态基类(Polymorphic Base Class): 如果一个类被设计为基类,并且你打算通过基类指针删除派生类对象,那么基类的析构函数必须声明为 virtual
    class Base { public: Base() { std::cout << "Base constructor" << std::endl; } // 关键:虚析构函数 virtual ~Base() { std::cout << "Base destructor" << std::endl; } }; class Derived : public Base { private: int* data; public: Derived() { data = new int(10); std::cout << "Derived constructor, allocated data" << std::endl; } ~Derived() override { // 'override' 是好习惯 delete data; std::cout << "Derived destructor, released data" << std::endl; } }; int main() { Base* b_ptr = new Derived(); // 创建 Derived 对象,用 Base 指针指向它 // ... delete b_ptr; // <--- 这里是关键 return 0; }
    • 如果 Base 的析构函数不是 virtual:当执行 delete b_ptr; 时,只会调用 Base 的析构函数。Derived 的析构函数不会被调用,导致 Derived 中分配的 data 内存泄漏。这是一个非常常见的错误。
    • 如果 Base 的析构函数是 virtual:当执行 delete b_ptr; 时,会首先调用 Derived 的析构函数(因为 b_ptr 实际指向一个 Derived 对象,虚函数机制确保了这一点),然后再自动调用 Base 的析构函数。这样,所有资源都能被正确释放。
    • 经验法则:如果一个类有任何虚函数,它通常应该有一个虚析构函数。如果你不确定,但该类可能被继承,将其析构函数设为虚的是一个安全的选择。
    • 纯虚析构函数:如前所述,析构函数也可以是纯虚的 (virtual ~MyClass() = 0;)。这会使类成为抽象类。即使是纯虚析构函数,也必须提供一个定义(实现),因为派生类的析构函数在执行完毕后会隐式调用基类的析构函数。

析构函数的调用顺序

  1. 单个对象:先执行析构函数的函数体,然后按照成员在类中声明的相反顺序,依次调用非静态数据成员的析构函数(如果它们是类类型并且有析构函数)。
  2. 继承层次结构
    • 当派生类对象被销毁时,首先调用派生类的析构函数。
    • 然后,自动地、

智能指针

好的,我们来深入探讨C++中的 智能指针 (Smart Pointers)。它们是C++11及以后版本中现代C++编程的核心特性之一,极大地帮助开发者管理动态分配的内存,从而减少内存泄漏和悬垂指针等常见问题。

智能指针本质上是行为类似指针的类模板对象,但它们能自动管理所指向对象的生命周期。它们利用了C++的RAII(Resource Acquisition Is Initialization)原则:资源(这里是动态分配的内存)的生命周期与一个栈上对象的生命周期绑定。当智能指针对象本身被销毁时(例如,离开作用域),它会自动释放其管理的内存。

C++标准库主要提供了以下几种智能指针,都定义在 <memory> 头文件中:

  1. std::unique_ptr (唯一所有权指针)
  2. std::shared_ptr (共享所有权指针)
  3. std::weak_ptr (弱引用指针,配合 std::shared_ptr 使用)

我们来逐个详细了解它们。


1. std::unique_ptr<T> (唯一所有权)

  • 核心思想:在任何时候,只有一个 std::unique_ptr 可以拥有(并最终负责删除)它所指向的动态分配的对象。它实现了独占所有权 (exclusive ownership) 模型。

  • 特点

    • 轻量级:通常与裸指针(raw pointer)具有相同的性能开销,因为它内部只需要存储一个指向对象的指针。
    • 不可复制 (Non-copyable):你不能通过拷贝构造函数或拷贝赋值运算符来复制一个 std::unique_ptr。这保证了所有权的唯一性。
      std::unique_ptr<MyClass> ptr1(new MyClass()); // std::unique_ptr<MyClass> ptr2 = ptr1; // 编译错误! // std::unique_ptr<MyClass> ptr3; // ptr3 = ptr1; // 编译错误!
    • 可移动 (Movable):所有权可以通过移动构造函数或移动赋值运算符进行转移。一旦所有权转移,原来的 std::unique_ptr 就不再拥有该对象(通常其内部指针变为 nullptr)。
      std::unique_ptr<MyClass> ptr1(new MyClass()); std::unique_ptr<MyClass> ptr2 = std::move(ptr1); // ptr1 现在为 nullptr,ptr2 拥有对象 // assert(ptr1 == nullptr); // assert(ptr2 != nullptr);
    • 自动释放:当 std::unique_ptr 对象本身被销毁时(例如离开作用域),它会自动调用 delete (对于单个对象) 或 delete[] (对于动态分配的数组) 来释放其管理的内存。
    • 自定义删除器 (Deleter):可以提供一个自定义的删除器函数或函数对象,用于在 std::unique_ptr 销毁时执行特定的清理操作,而不仅仅是 delete。这对于管理非内存资源(如文件句柄、网络套接字)或使用特定分配/释放函数的内存非常有用。
      struct FileCloser { void operator()(FILE* fp) const { if (fp) fclose(fp); std::cout << "File closed by custom deleter." << std::endl; } }; // std::unique_ptr<FILE, FileCloser> file_ptr(fopen("test.txt", "w"), FileCloser()); // 使用 lambda 作为删除器 std::unique_ptr<int, void(*)(int*)> custom_deleter_ptr(new int(10), [](int* p){ std::cout << "Custom deleter for int: " << *p << std::endl; delete p; });
  • 创建方式

    • 直接构造 (不推荐,除非需要自定义删除器或管理已存在的裸指针):
      std::unique_ptr<int> p1(new int(10));
    • std::make_unique<T>(args...) (C++14及以后,推荐方式):更安全(避免了在复杂表达式中由于异常可能导致的内存泄漏)且更简洁。
      auto p2 = std::make_unique<int>(20); auto p_obj = std::make_unique<MyClass>(arg1, arg2);
      对于数组:
      auto p_arr = std::make_unique<int[]>(5); // 创建一个包含5个默认初始化int的数组 // p_arr[0] = 1;
  • 常用操作

    • get(): 返回一个指向被管理对象的裸指针。不释放所有权。
    • release(): 放弃对被管理对象的所有权,并返回指向该对象的裸指针。调用者现在负责管理该裸指针的生命周期。std::unique_ptr 自身变为空 (nullptr)。
    • reset(ptr = nullptr): 销毁当前管理的对象(如果存在),并接管新的裸指针 ptr(如果提供)的所有权。如果 ptrnullptr 或未提供,则 std::unique_ptr 变为空。
    • operator*()operator->(): 解引用和成员访问,与裸指针行为类似。
    • operator bool(): 检查是否拥有一个对象(即内部指针是否为 nullptr)。
  • 使用场景

    • 当一个对象只需要一个所有者时。
    • 作为工厂函数的返回值,将新创建对象的所有权安全地转移给调用者。
    • 在类中作为指针成员,管理类拥有的独占资源。
    • 管理动态分配的数组(std::unique_ptr<T[]>)。

2. std::shared_ptr<T> (共享所有权)

  • 核心思想:允许多个 std::shared_ptr 实例共同拥有同一个动态分配的对象。它内部维护一个引用计数 (reference count),记录有多少个 std::shared_ptr 指向该对象。当最后一个 std::shared_ptr 被销毁或重置时,引用计数变为0,此时对象才会被删除。
  • 特点
    • 可复制 (Copyable)std::shared_ptr 可以被自由地复制和赋值。每次复制或赋值都会增加引用计数。
      std::shared_ptr<MyClass> sptr1 = std::make_shared<MyClass>(); std::shared_ptr<MyClass> sptr2 = sptr1; // 引用计数增加 // assert(sptr1.use_count() == 2); // assert(sptr2.use_count() == 2);
    • 自动释放:当最后一个指向对象的 std::shared_ptr 析构时,对象被删除。
    • 线程安全 (Reference Counting):引用计数的操作(增加和减少)是原子性的,因此在多线程环境中,不同线程对同一组 std::shared_ptr 进行复制、赋值和销毁是安全的。但是,对被管理对象本身的访问不是自动线程安全的,需要用户自己同步。
    • 开销:比 std::unique_ptr 和裸指针有更大的开销。除了指向对象的指针外,它还需要管理一个控制块 (control block)。控制块通常动态分配,并包含:
      • 指向被管理对象的指针。
      • 强引用计数 (strong reference count)。
      • 弱引用计数 (weak reference count, 用于 std::weak_ptr)。
      • 可选的自定义删除器。
      • 可选的自定义分配器 (用于控制块本身)。
    • 自定义删除器:与 std::unique_ptr 类似,可以提供自定义删除器。
  • 创建方式
    • 直接构造 (不推荐,可能导致控制块的多次分配,且不安全):
      // MyClass* raw_ptr = new MyClass(); // std::shared_ptr<MyClass> sptr1(raw_ptr); // std::shared_ptr<MyClass> sptr2(raw_ptr); // 严重错误!两个独立的控制块,会导致双重释放!
    • std::make_shared<T>(args...) (推荐方式):更安全、更高效。它通常能在一个单独的内存分配中同时为对象和控制块分配内存,减少了内存分配次数和潜在的碎片。
      auto sptr1 = std::make_shared<int>(100); auto sptr_obj = std::make_shared<MyClass>(arg1, arg2);
      std::make_shared 不支持直接创建数组的共享指针(如 std::make_shared<int[]>(5) 是不行的)。如果需要 std::shared_ptr<int[]>,你需要这样:
      std::shared_ptr<int[]> sptr_arr(new int[5]); // 需要自定义删除器或使用默认的 delete[] // 或者更安全的: std::shared_ptr<int> sptr_arr_custom(new int[5], std::default_delete<int[]>());
  • 常用操作
    • get(): 返回裸指针。
    • reset(): 减少引用计数,如果计数为0则删除对象。然后可以接管新的裸指针。
    • use_count(): 返回当前的强引用计数(主要用于调试,不应依赖其值做逻辑判断)。
    • unique(): 检查引用计数是否为1(C++20 中已废弃,因为 use_count() == 1 并不保证线程安全地进行某些操作)。
    • operator*(), operator->(), operator bool(): 与 std::unique_ptr 类似。
  • 使用场景
    • 当一个资源需要被多个所有者共享,并且其生命周期由最后一个所有者决定时。
    • 在数据结构中(如图的节点,其中多个边可以指向同一个节点)。
    • 作为回调函数的参数,确保在回调执行期间对象仍然有效。
    • 注意循环引用:如果两个或多个对象通过 std::shared_ptr 相互引用,形成一个循环,它们的引用计数永远不会降为0,导致内存泄漏。这时需要 std::weak_ptr 来打破循环。

3. std::weak_ptr<T> (弱引用)

  • 核心思想std::weak_ptr 是对 std::shared_ptr 所管理对象的一种非拥有(non-owning)的“弱”引用。它不控制对象的生命周期,即它的存在与否不影响对象的引用计数。
  • 特点
    • 不增加强引用计数:创建 std::weak_ptr 或将其赋值给另一个 std::weak_ptr 不会改变对象的强引用计数。它会增加控制块中的弱引用计数。
    • 用于打破循环引用:这是 std::weak_ptr 最主要的应用场景。例如,在观察者模式中,Subject 可能持有观察者列表,而观察者也可能需要反向引用Subject。如果都用 std::shared_ptr,容易形成循环。可以将反向引用设为 std::weak_ptr
    • 检查对象是否存在std::weak_ptr 允许你检查它所指向的对象是否仍然存在(即其对应的 std::shared_ptr 还没有释放它)。
    • 不能直接访问对象:你不能直接通过 std::weak_ptr 来解引用或访问被管理对象的成员。
  • 创建方式
    • 通常从一个 std::shared_ptr 构造:
      std::shared_ptr<MyClass> sptr = std::make_shared<MyClass>(); std::weak_ptr<MyClass> wptr = sptr; // wptr 指向 sptr 管理的对象,但不增加强引用计数
  • 常用操作
    • expired(): 检查被引用的对象是否已经被销毁。如果 true,则对象不再存在。
    • lock(): 关键操作。尝试获取一个指向被管理对象的 std::shared_ptr
      • 如果对象仍然存在,lock() 返回一个有效的 std::shared_ptr,并增加对象的强引用计数(保证在返回的 std::shared_ptr 有效期间,对象不会被析构)。
      • 如果对象已经被销毁,lock() 返回一个空的(nullptrstd::shared_ptr
      if (std::shared_ptr<MyClass> locked_sptr = wptr.lock()) { // 对象仍然存在,locked_sptr 现在是一个有效的共享指针 locked_sptr->doSomething(); } else { // 对象已经被销毁 std::cout << "Object has been destroyed." << std::endl; }
    • reset(): 将 std::weak_ptr 置为空。
    • use_count(): 返回共享该对象的 std::shared_ptr 的数量(即强引用计数)。
  • 使用场景
    • 打破 std::shared_ptr 之间的循环引用
    • 实现缓存,其中缓存项可以被外部删除,而缓存只需要知道项是否存在。
    • 在需要临时访问一个可能被其他地方销毁的对象,并且不希望延长该对象生命周期的情况。

总结与选择智能指针:

  1. 默认使用 std::unique_ptr

    • 它是最轻量级、最高效的智能指针。
    • 明确了独占所有权,使得代码逻辑更清晰。
    • 如果需要转移所有权,可以使用 std::move
  2. 当需要共享所有权时,使用 std::shared_ptr

    • 当多个实体需要共同管理一个对象的生命周期时。
    • 要小心循环引用问题。
  3. 当需要一个非拥有的引用来观察 std::shared_ptr 管理的对象,并且需要打破循环引用时,使用 std::weak_ptr

    • 它允许你安全地检查对象是否存在,并在存在时临时获取一个 std::shared_ptr

避免的做法:

  • 不要混用裸指针和智能指针来管理同一个对象的所有权。例如,不要用裸指针初始化一个 std::unique_ptr,然后又用同一个裸指针初始化另一个 std::unique_ptrstd::shared_ptr,或者手动 delete 这个裸指针。
  • 尽量使用 std::make_unique (C++14+) 和 std::make_shared 来创建智能指针,而不是 new 后再传入构造函数。这更安全(异常安全)且对于 std::make_shared 更高效。
  • 不要从 this 指针创建 std::shared_ptr,除非类本身继承自 std::enable_shared_from_this<T> 并使用其 shared_from_this() 方法。直接 std::shared_ptr<MyClass>(this) 会导致独立的控制块和潜在的双重释放。
    class MyClass : public std::enable_shared_from_this<MyClass> { public: std::shared_ptr<MyClass> getSharedPtr() { return shared_from_this(); } static std::shared_ptr<MyClass> create() { return std::make_shared<MyClass>(); // 正确创建方式 } private: MyClass() = default; // 防止外部直接 new };

智能指针是现代C++中资源管理的重要工具,深刻理解它们的工作原理和使用场景,能够显著提高代码的安全性、可读性和可维护性。它们是学习C++时必须掌握的关键概念之一。

Last updated on