本篇学习记录在于阅读《Effective C++中文版(第三版)》感悟所得,而且我发现使用chatGPT匹配阅读书效率非常高,因为书都是被翻译出来的,很多地方不是很专业,我只要使用chatGPT把书上某个标题上的内容进行扩展,就通俗易懂而且可以追问。

我的问题

待解决问题

已解决问题

(一)delete 和 delete[] 有什么区别?

在C++中,delete[]运算符用于释放通过new[]运算符分配的动态数组内存。[]表示要释放的内存是一个数组,而不是单个对象。

当您使用new[]运算符分配内存时,编译器会在内存中为数组元素分配连续的空间。为了正确释放这些数组元素,您需要使用delete[]运算符。如果您使用delete而不是delete[]来释放数组内存,可能会导致未定义行为,如内存泄漏或程序崩溃。

以下是一个使用new[]和delete[]运算符分配和释放动态数组内存的示例:

#include <iostream>

int main() {
    // 使用new[]分配动态数组内存
    int* data = new int[10];

    // 使用数组
    for (int i = 0; i < 10; ++i) {
        data[i] = i;
    }

    // 使用delete[]释放动态数组内存
    delete[] data;

    return 0;
}   

在这个示例中,我们使用new[]运算符分配了一个包含10个整数的动态数组。然后我们使用delete[]运算符释放数组内存。这确保了所有数组元素的内存都被正确释放,避免了内存泄漏和未定义行为。

总之,在C++中,delete[]运算符用于释放通过new[]运算符分配的动态数组内存。在释放数组内存时,您应该始终使用delete[]而不是delete,以确保所有数组元素的内存都被正确释放。

delete[] 和 new[] 搭配使用,操作数组内存。

(二)C++ 中的资源管理类指的是什么?

在C++中,资源管理类是指负责管理和封装资源(如内存、文件、网络连接等)的类。资源管理类通常遵循RAII(Resource Acquisition Is Initialization)原则,即将资源的获取与初始化绑定在一起。这意味着资源管理类在构造函数中获取和初始化资源,在析构函数中释放资源。这样,资源的管理自然与对象的生命周期绑定在一起。

资源管理类的主要目的是简化资源管理、避免资源泄漏和提高异常安全性。通过将资源封装在对象中,您可以确保在对象离开作用域或被销毁时自动释放资源。这消除了忘记释放资源的可能性,同时确保在异常情况下资源被正确释放。

以下是一个C++资源管理类的示例,用于管理动态分配的内存:

#include <iostream>
#include <stdexcept>

class MemoryManager {
public:
    MemoryManager(size_t size) {
        data_ = new char[size];
        if (data_ == nullptr) {
            throw std::runtime_error("Failed to allocate memory");
        }
    }

    ~MemoryManager() {
        delete[] data_;
    }

    // 使用和操作资源的其他成员函数...

private:
    char* data_;
};

int main() {
    try {
        MemoryManager memory(1024);
        // 使用和操作资源...
    }
    catch (const std::runtime_error& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }

    return 0;
}   

在这个示例中,我们定义了一个名为MemoryManager的资源管理类,它负责管理动态分配的内存。我们在构造函数中分配内存,在析构函数中释放内存。这确保了在MemoryManager对象的生命周期内正确管理内存资源。

总之,在C++中,资源管理类是指负责管理和封装资源的类。资源管理类遵循RAII原则,以简化资源管理、避免资源泄漏和提高异常安全性。在实际开发过程中,您应该尽可能使用资源管理类来管理内存、文件和其他资源。

一、让自己习惯C++

第1条: 视C++为一个语言联邦

C++ 是4种语言的合集:

  • C
  • Object-Oriented ++:构造函数、析构函数、封装、继承
  • Template C++
  • STL : template标准库

第2条:尽量以const,enum,inline替换 #define

在C++中,尽量使用const、enum和inline替换#define的建议源于这些替代方案提供了更好的类型安全、作用域控制和调试信息。以下是使用const、enum和inline替换#define的一些原因:

类型安全:const和enum都是类型安全的,因为它们与C++类型系统紧密集成。这意味着编译器可以检查它们的类型并捕获类型不匹配的错误。而#define是一种预处理器指令,不受C++类型系统的约束,这可能导致类型不匹配的错误。

作用域控制:const、enum和inline都遵循C++作用域规则,这有助于避免命名冲突和全局变量污染。而#define没有作用域,它在整个编译单元中都是可见的,这可能导致不必要的命名冲突。

调试信息:const、enum和inline在编译时会生成调试信息,这使得在调试过程中更容易找到问题。而#define是在预处理阶段展开的,因此在调试过程中很难追踪它们。

以下是使用const、enum和inline替换#define的一些示例:

  1. 使用const替换#define(常量):

    // 使用#define定义常量
    #define PI 3.14159

    // 使用const替换#define
    const double PI = 3.14159;

  2. 使用enum替换#define(枚举值):

    // 使用#define定义枚举值
    #define COLOR_RED 0
    #define COLOR_GREEN 1
    #define COLOR_BLUE 2

    // 使用enum替换#define
    enum Color {

     COLOR_RED,
     COLOR_GREEN,
     COLOR_BLUE
    

    };

  3. 使用inline替换#define(宏函数):

    // 使用#define定义宏函数
    #define SQUARE(x) ((x) * (x))

    // 使用inline替换#define
    inline int square(int x) {

     return x * x;
    

    }

    总之,在C++中,尽量使用const、enum和inline替换#define可以带来更好的类型安全、作用域控制和调试信息。这有助于提高代码的可读性、可维护性和健壮性。在实际开发过程中,您可以根据需要选择合适的替代方案来替换#define。

2.1 #define会在预处理阶段展开,这是什么意思?什么是预处理,和编译有什么区别?什么叫展开?

预处理

在C++(以及C)中,预处理(preprocessing)是编译过程的第一阶段,它在实际编译之前执行。

编译前执行的阶段,叫做预处理阶段,预处理阶段执行的操作在编译阶段不会报错。

预处理器(preprocessor)是一个单独的程序,用于处理源代码中的预处理指令(如#define、#include、#ifdef等)。预处理器会修改源代码,例如替换宏定义、包含头文件等,然后将修改后的代码传递给编译器进行实际编译。

预处理与编译的区别在于它们处理的内容和目标:

预处理:预处理主要处理源代码中的预处理指令。它不会分析C++语法,也不会生成目标代码。预处理的目标是准备源代码以供编译器使用,例如替换宏定义、包含头文件、条件编译等。

编译:编译是在预处理之后执行的过程。编译器会分析源代码的C++语法、语义和类型,生成目标代码(通常是汇编代码或机器代码)。编译的目标是将源代码转换为可执行的程序或库。

宏展开

术语“展开”(expanding)通常用于描述预处理器如何处理宏定义。当预处理器遇到宏定义时,它会将宏的所有实例替换为宏定义中的代码。这个过程称为宏展开。以下是一个使用#define的宏展开示例:

#include <iostream>

// 使用#define定义宏函数
#define SQUARE(x) ((x) * (x))

int main() {
    int a = 3;
    int b = SQUARE(a); // 预处理器将SQUARE(a)展开为((a) * (a))

    std::cout << "The square of " << a << " is " << b << std::endl;

    return 0;
}   

在这个示例中,我们定义了一个宏函数SQUARE(x)。当预处理器遇到SQUARE(a)时,它会将其展开为((a) * (a))。这意味着在实际编译过程中,源代码中的SQUARE(a)已经被替换为((a) * (a))。

总之,在C++中,预处理是编译过程的第一阶段,用于处理源代码中的预处理指令。预处理与编译的区别在于它们处理的内容和目标。术语“展开”通常用于描述预处理器如何处理宏定义。在实际开发过程中,了解预处理和编译的区别以及宏展开的概念可以帮助您更好地理解C++编译过程。

第3条:尽可能使用const

使用const可以实现iOS中readonly的效果。

在C++中,使用const关键字有许多优点,它可以帮助您编写更安全、更易于维护的代码。以下是尽可能使用const的一些原因:

  1. 提高代码可读性:使用const可以明确表示某个变量或对象在其生命周期内不会被修改。这使得代码更容易理解,因为读者无需跟踪变量的状态。

  2. 防止意外修改:const可以防止意外地修改变量或对象。当您声明一个变量或对象为const时,编译器会在尝试修改它时报错。这有助于捕获潜在的错误和bug。

  3. 编译时优化:使用const可能有助于编译器进行优化。因为编译器知道const变量或对象不会被修改,它可以对代码进行更积极的优化,例如将常量表达式折叠(constant folding)和将常量值存储在只读内存中。

  4. 支持逻辑常量性:const关键字可以用于声明具有逻辑常量性的成员函数。逻辑常量性表示成员函数在逻辑上不会修改对象的状态,即使它可能会修改某些底层数据。这允许您在不破坏封装的情况下向用户传达对象的不变性。

以下是一个使用const关键字的示例:

#include <iostream>
#include <vector>

class MyClass {
public:
    MyClass(int value) : value_(value) {}

    // 使用const声明逻辑常量性成员函数
    int value() const {
        return value_;
    }

private:
    int value_;
};

int main() {
    const MyClass obj(42);

    // 使用const对象调用逻辑常量性成员函数
    std::cout << "The value is: " << obj.value() << std::endl;

    return 0;
}   

在这个示例中,我们在MyClass类中使用const关键字声明了一个逻辑常量性成员函数value()。这表示value()函数在逻辑上不会修改对象的状态。我们还创建了一个const对象obj,并使用它调用value()函数。

总之,在C++中,尽可能使用const关键字可以帮助您编写更安全、更易于维护的代码。const关键字可以提高代码可读性,防止意外修改,支持编译时优化,以及支持逻辑常量性。在实际开发过程中,您应该根据需要使用const关键字来声明变量、对象和成员函数。

第4条: 确定对象被使用前已先被初始化

int x; 这句声明某些语境下x保证被初始化为0,但在另外一些语境下却不保证。

原因

在C++中,确保对象在使用前已经被初始化是非常重要的,因为未初始化的对象可能导致未定义行为、内存泄漏和安全漏洞。以下是在C++中确保对象被初始化的一些原因:

未定义行为:如果您在初始化对象之前访问或使用它,程序的行为将是未定义的。这可能导致程序崩溃、数据损坏或其他难以预料的错误。

资源泄漏:如果您在初始化对象之前分配了资源(如内存、文件或网络连接),但未将这些资源与对象关联,可能会导致资源泄漏。资源泄漏可能导致性能下降、内存耗尽或其他问题。

安全漏洞:未初始化的对象可能导致安全漏洞,例如信息泄漏(如果对象包含敏感数据)或远程代码执行(如果对象包含函数指针)。

处理方法

默认建议使用 初始化器,其次是初始化列表,然后是初始化列表和构造函数

在C++中,确保对象在使用前已经被初始化是非常重要的,因为未初始化的对象可能导致未定义行为、内存泄漏和安全漏洞。以下是在C++中确保对象被初始化的一些原因:

未定义行为:如果您在初始化对象之前访问或使用它,程序的行为将是未定义的。这可能导致程序崩溃、数据损坏或其他难以预料的错误。

资源泄漏:如果您在初始化对象之前分配了资源(如内存、文件或网络连接),但未将这些资源与对象关联,可能会导致资源泄漏。资源泄漏可能导致性能下降、内存耗尽或其他问题。

安全漏洞:未初始化的对象可能导致安全漏洞,例如信息泄漏(如果对象包含敏感数据)或远程代码执行(如果对象包含函数指针)。

#include <iostream>
#include <vector>

class MyClass {
public:
    // 使用初始化列表初始化成员变量
    MyClass(int value): value_(value),numbers_(1,2,3,4,5) {
         // 使用构造函数初始化对象
    }

private:
    int value_;
    int default_value_ = 0; // 使用默认初始化对象初始化成员变量
    std::vector<int> numbers_;
}

二、构造/析构/赋值运算

第5条:了解C++默默编写并调用哪些函数

系统为我们自动生成了哪些方法?

在学习C++时,了解C++编译器默默编写和调用的函数(隐式生成的函数)是重要的,因为这有助于您理解对象的生命周期、资源管理和类的行为。C++编译器会为类自动生成以下特殊成员函数(如果您没有显式地提供它们):

默认构造函数:如果您没有为类定义任何构造函数,编译器将为您生成一个默认构造函数。默认构造函数不会对成员变量执行任何初始化操作。

拷贝构造函数:如果您没有为类定义拷贝构造函数,编译器将为您生成一个。默认的拷贝构造函数会逐个复制类的所有成员变量。对于基本类型,这意味着直接复制值;对于类类型,这意味着调用其拷贝构造函数。

拷贝赋值运算符:如果您没有为类定义拷贝赋值运算符,编译器将为您生成一个。默认的拷贝赋值运算符与拷贝构造函数类似,会逐个复制类的所有成员变量。

析构函数:如果您没有为类定义析构函数,编译器将为您生成一个。默认析构函数会自动调用类的所有成员变量和基类的析构函数。然而,它不会释放动态分配的内存或其他资源。

移动构造函数和移动赋值运算符(C++11及更高版本):如果您没有为类定义移动构造函数和移动赋值运算符,且类满足某些条件(如没有用户定义的拷贝构造函数、拷贝赋值运算符和析构函数),编译器将为您生成它们。移动构造函数和移动赋值运算符用于实现高效的资源转移。

了解这些隐式生成的函数可以帮助您编写更安全、更高效的代码。例如,您可以根据需要显式地定义拷贝构造函数、拷贝赋值运算符和析构函数以实现深拷贝和正确的资源管理。此外,您还可以利用移动语义来优化性能,特别是在涉及大型对象或资源的情况下。

总之,在学习C++时,了解编译器默默编写和调用的函数(隐式生成的函数)有助于您理解对象的生命周期、资源管理和类的行为。这将使您能够编写更安全、更高效的代码,并充分利用C++的特性和优势。

了解拷贝构造函数、拷贝赋值运算符和移动构造函数

下面几种中:拷贝构造函数 和 拷贝赋值函数 用的比较多。

在C++中,拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符是特殊的成员函数,用于控制对象在复制和移动时如何处理其资源和状态。

  1. 拷贝构造函数:拷贝构造函数用于从现有对象创建一个新对象的副本。拷贝构造函数接受一个现有对象的常量引用作为参数。默认情况下,拷贝构造函数会按位复制对象的所有成员变量。然而,在某些情况下(如需要深拷贝或自定义资源管理),您可能需要显式地定义拷贝构造函数。

    class MyClass {
    public:

     // 拷贝构造函数
     MyClass(const MyClass& other) {
         // 自定义拷贝逻辑
     }
    

    };

  2. 拷贝赋值运算符:拷贝赋值运算符用于将现有对象的状态和资源赋值给另一个对象。拷贝赋值运算符接受一个现有对象的常量引用作为参数,并返回一个非常量引用。默认情况下,拷贝赋值运算符会按位复制对象的所有成员变量。然而,在某些情况下(如需要深拷贝或自定义资源管理),您可能需要显式地定义拷贝赋值运算符。

    class MyClass {
    public:

     // 拷贝赋值运算符
     MyClass& operator=(const MyClass& other) {
         if (this != &other) {
             // 自定义拷贝逻辑
         }
         return *this;
     }
    

    };

  3. 移动构造函数:移动构造函数用于从现有对象中移动资源和状态,而不是复制它们。移动构造函数接受一个现有对象的右值引用作为参数。默认情况下,移动构造函数会按位移动对象的所有成员变量。然而,在某些情况下(如自定义资源管理),您可能需要显式地定义移动构造函数。

    class MyClass {
    public:

     // 移动构造函数
     MyClass(MyClass&& other) {
         // 自定义移动逻辑
     }
    

    };

  4. 移动赋值运算符:移动赋值运算符用于将现有对象的资源和状态移动到另一个对象。移动赋值运算符接受一个现有对象的右值引用作为参数,并返回一个非常量引用。默认情况下,移动赋值运算符会按位移动对象的所有成员变量。然而,在某些情况下(如自定义资源管理),您可能需要显式地定义移动赋值运算符。

    class MyClass {
    public:

     // 移动赋值运算符
     MyClass& operator=(MyClass&& other) {
         if (this != &other) {
             // 自定义移动逻辑
         }
         return *this;
     }
    

    };

    总之,拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符在C++中用于控制对象在复制和移动时如何处理其资源和状态。默认情况下,这些特殊成员函数会按位复制或移动对象的所有成员变量。然而,在需要深拷贝或自定义资源管理的情况下,您可能需要显式地定义这些特殊成员函数。

第6条:若不想使用编译器自动生成的函数,就该明确拒绝

为驳回编译器自动提供的方法,可将相应的成员函数声明为private并且不予实现;也可以通过将函数声明成 delete 表示你不希望编译器自动生成这些函数的实现。

在C++中,编译器会为类自动生成一些特殊成员函数(如默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数等),如果您没有显式地提供它们。这些自动生成的函数通常适用于许多场景,但在某些情况下,您可能不希望使用它们。例如,当类包含指针成员时,自动生成的拷贝构造函数和拷贝赋值运算符可能会导致浅拷贝,从而引发悬挂指针和内存泄漏等问题。

为了避免这些问题,您可以明确拒绝编译器自动生成的函数。这可以通过将这些函数声明为delete来实现。将函数声明为delete表示您不希望编译器自动生成这些函数的实现。如果尝试使用已删除的函数,编译器将报错。

以下是一个明确拒绝编译器自动生成拷贝构造函数和拷贝赋值运算符的示例:

#include <iostream>

class MyClass {
public:
    MyClass(int value) : value_(value) {}

    // 明确拒绝编译器自动生成的拷贝构造函数
    MyClass(const MyClass& other) = delete;

    // 明确拒绝编译器自动生成的拷贝赋值运算符
    MyClass& operator=(const MyClass& other) = delete;

private:
    int value_;
};

int main() {
    MyClass obj1(42);

    // 下面的代码将导致编译错误,因为拷贝构造函数已被删除
    // MyClass obj2(obj1);

    // 下面的代码将导致编译错误,因为拷贝赋值运算符已被删除
    // MyClass obj3;
    // obj3 = obj1;

    return 0;
}   

在这个示例中,我们为MyClass类显式地删除了拷贝构造函数和拷贝赋值运算符。这意味着我们不能使用这些函数来复制MyClass对象。如果尝试使用已删除的函数,编译器将报错。

通过明确拒绝编译器自动生成的函数,您可以确保对象在复制和赋值时遵循正确的行为。这有助于避免悬挂指针、内存泄漏和其他问题。在实际开发过程中,您应该根据需要使用delete关键字来明确拒绝编译器自动生成的函数。

第7条:为多态基类声明virtual析构函数

在C++中,为多态基类声明virtual析构函数是非常重要的,因为它确保在删除基类指针时正确地调用派生类的析构函数。如果基类的析构函数不是虚函数,那么在通过基类指针删除派生类对象时,只有基类的析构函数会被调用,而不是派生类的析构函数。这可能导致资源泄漏和未定义行为。

#include <iostream>

class Base {
public:
    Base() {
        std::cout << "Base constructor" << std::endl;
    }

    // 非虚析构函数
    vitual ~Base() {
        std::cout << "Base destructor" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructor" < std:endl;
        data_ = new int[10];
    }

    ~Derived() {
        std::cout << "Derived destructor" << std::endl;
        delete[] data_;
    }

private:
    int* data_;
};  

当您将基类Base的析构函数声明为虚函数后,派生类Derived被回收时,会先调用派生类Derived的析构函数,然后再调用基类Base的析构函数。这确保了在派生类析构函数中分配的资源被正确释放,同时基类析构函数也能正确执行。

在这个示例中,当我们通过基类指针obj删除派生类对象时,会先调用派生类Derived的析构函数,然后再调用基类Base的析构函数。这确保了在派生类析构函数中分配的资源被正确释放,同时基类析构函数也能正确执行。

总之,在C++中,当基类的析构函数为虚函数时,派生类被回收时会先调用派生类的析构函数,然后再调用基类的析构函数。这有助于确保资源被正确释放,并避免内存泄漏和未定义行为。

第8条:析构函数不要加异常

第9条:绝不在构造和析构过程中调用 virtual 函数

在C++中,在构造和析构过程中调用虚函数(virtual function)是不安全的,因为这可能导致意外的行为。以下是在构造和析构过程中不应调用虚函数的原因:

  1. 构造过程:当基类的构造函数执行时,派生类的成员变量和虚函数表(vtable)尚未初始化。如果在基类构造函数中调用虚函数,将调用基类的虚函数实现,而不是派生类的实现。这可能导致错误的行为,因为派生类的虚函数可能依赖于尚未初始化的成员变量。

  2. 析构过程:与构造过程类似,当基类的析构函数执行时,派生类的成员变量和虚函数表(vtable)已经被销毁。如果在基类析构函数中调用虚函数,将调用基类的虚函数实现,而不是派生类的实现。这可能导致错误的行为,因为派生类的虚函数可能依赖于已经销毁的成员变量。

第10条:令operator= 返回一个 reference to *this

在C++中,赋值运算符operator=通常返回一个指向当前对象(*this)的引用,这样做有几个好处:

链式赋值:返回对当前对象的引用允许您执行链式赋值操作。这意味着您可以将多个对象设置为具有相同的值,如下所示:

a = b = c = d;

如果operator=返回void或其他类型,您将无法执行此类链式赋值。

一致性:大多数C++运算符(如算术运算符、比较运算符等)都返回一个值。让赋值运算符operator=返回一个引用可以使其行为与其他运算符保持一致。

可读性:返回对当前对象的引用允许您在赋值后继续操作该对象。这可以提高代码的可读性和灵活性。

以下是一个实现赋值运算符operator=的示例,它返回对当前对象的引用:

class MyClass {
public:
    // 赋值运算符
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            // 自定义赋值逻辑
        }
        return *this; // 返回对当前对象的引用
    }
};  

在这个示例中,我们为MyClass类定义了一个赋值运算符operator=,它返回对当前对象的引用。这允许我们执行链式赋值操作,并在赋值后继续操作对象。

总之,在C++中,赋值运算符operator=通常返回一个指向当前对象(*this)的引用,以支持链式赋值、一致性和可读性。在实际开发过程中,您应该根据需要为类实现赋值运算符,并确保它返回对当前对象的引用。

第11条:令operator= 中处理 “自我赋值”

具体什么意思

意思就是检查准备要复制的对象是不是自己,如果是自己就不用复制了。

自我赋值(self-assignment)是指在赋值运算符operator=中检查传入的对象是否与当前对象相同。换句话说,您需要检查传入的对象的地址是否与当前对象的地址相同。如果它们的地址相同,这意味着它们是同一个对象,因此不需要执行任何赋值操作。

您可以使用if (this != &other)语句来检查自我赋值。this是一个指向当前对象的指针,&other是传入对象的地址。如果它们不相同,则执行正常的赋值逻辑;如果它们相同,则不执行任何操作。

以下是一个检查自我赋值的示例:

class MyClass {
public:
    MyClass & operator=(const MyClass& other) {
        // 检查是不是自己
        if(this != &other) {
            // 自定义赋值逻辑
        }
        
        return *this;
    }
}   

在这个示例中,我们在赋值运算符operator=中检查自我赋值。这有助于避免潜在的错误和资源泄漏,特别是在涉及动态内存分配或其他资源管理的类中。

不这样做的后果

看下面代码即可理解,相当于在赋值过程中把自己的数据给清了,this和other实际上是同一个对象。

#include <iostream>
#include <cstring>

class MyClass {
public:
    MyClass(const char* str) {
        data_ = new char[strlen(str) + 1];
        strcpy(data_, str);
    }

    ~MyClass() {
        delete[] data_;
    }

    MyClass& operator=(const MyClass& other) {
        // 如果不检查自我赋值
        delete[] data_; // 释放当前对象的内存
        data_ = new char[strlen(other.data_) + 1]; // 为新数据分配内存
        strcpy(data_, other.data_); // 复制数据

        return *this;
    }

private:
    char* data_;
};

int main() {
    MyClass obj("Hello, World!");

    // 自我赋值(可能导致问题)
    obj = obj;

    return 0;
}   

第12条:复制对象时勿忘其每一个成分

在C++中,复制对象时确保复制其每一个成分是非常重要的,因为这有助于确保对象之间的数据一致性和资源管理。如果您在复制对象时遗漏了某些成分,可能会导致错误的行为、资源泄漏和未定义行为。

为了确保在复制对象时复制其每一个成分,您需要在实现拷贝构造函数和拷贝赋值运算符时注意以下几点:

复制所有成员变量:确保复制对象的所有成员变量,包括基本类型、类类型和指针类型。对于基本类型和类类型,您可以直接复制它们的值;对于指针类型,您可能需要执行深拷贝以避免浅拷贝带来的问题。

调用基类和成员对象的拷贝构造函数:如果您的类从其他类派生或包含其他类的对象,确保在拷贝构造函数和拷贝赋值运算符中调用基类和成员对象的拷贝构造函数。这可以确保基类和成员对象的状态和资源被正确复制。

确保资源管理:如果您的类负责管理资源(如内存、文件或网络连接),确保在拷贝构造函数和拷贝赋值运算符中正确处理这些资源。例如,您可能需要分配新的内存并复制数据,而不是简单地复制指针。

以下是一个C++示例,展示了如何在拷贝包含指针成员的对象时正确地执行深拷贝:

#include <iostream>
#include <cstring>

class MyClass {
public:
    MyClass(const char* str) {
        data_ = new char[strlen(str) + 1];
        strcpy(data_,str);
    }

    // 拷贝构造函数
    MyClass(const MyClass& other) {
        data_ = new char[strlen(other.data_) + 1];
        strcpy(data_,other.data_);
    }

    // 拷贝赋值运算符
    MyClass& operator=(const MyClass& other) {
        if (this != &other)
        {
            /* code */
            delete[] data_;
            data_ = new char(strlen(other.data_) + 1);
            strcpy(data_,other.data_);
        }
        return *this;
    }

    ~MyClass() {
        delete[] data_;
    }

    void print() {
        std::cout << "Data: " << data_ << std:endl;
    }

private:
    char* data_;
}

int main() {
    MyClass obj1("Hello,World!");

    // 使用拷贝构造函数复制对象
    MyClass obj2(obj1);
    obj1.print(); // 输出:Data: Hello,World!
    obj2.print(); // 输出: Data: Hello,World!

    // 使用赋值运算符复制对象
    MyClass obj3;
    obj3 = obj1;
    obj1.print();// 输出: Data: Hello,World!
    obj3.print(); // 输出: Data: Hello,World!
    
    return 0;
}   

三、资源管理

资源的定义:一旦用了它,将来必须还给系统。如果不这样,糟糕的事情就会发生。如包含动态内存分配、文件操作或网络连接的类

C++ 中常用的资源就是动态分配内存(如果只分配而不归还,会导致内存泄露)。

浅拷贝 <=> 悬垂指针。

第13条:以对象管理资源

在C++中,以对象管理资源(Resource Acquisition Is Initialization,简称RAII)是一种编程技术。

RAII的核心思想是将资源(如内存、文件、网络连接等)封装在对象中,并在对象的构造函数中获取资源、初始化状态,在对象的析构函数中释放资源。

一个常用的 RAII classes 就是 std::shared_ptr 智能指针。

RAII有以下优点:

  1. 简化资源管理:RAII可以简化资源管理,因为您不需要在每次使用资源时显式地分配和释放它。对象的构造函数和析构函数会自动处理资源的获取和释放。

  2. 避免资源泄漏:RAII有助于避免资源泄漏,因为资源在对象离开作用域或被销毁时自动释放。这消除了忘记释放资源的可能性。

  3. 异常安全:RAII可以提高代码的异常安全性,因为资源在对象的析构函数中自动释放。即使在异常情况下,资源也会被正确释放。

以下是一个使用RAII技术管理资源的C++示例(当然也是智能指针的内部实现方法):

#include <iostream>
#include <fstream>
#include <string>

class File {
public:
    File(const std::string& filename) {
        file_.open(filename);
        if (!file_.is_open())
        {
            /* code */
            throw std::runtime_error("Failed to open the file");
        }
    }

    ~File() {
        file_.close();
    }

    // 使用文件
    void write(const std:string& content) {
        file_ << content;
    }

private:
    std::ofstream file_;
}   

第14条:在资源管理类中小心coping行为

在C++中,对资源管理类(如包含动态内存分配、文件操作或网络连接的类)小心处理复制行为是非常重要的,因为不正确的复制可能导致资源泄漏、悬挂指针和未定义行为。以下是在资源管理类中小心处理复制行为的一些原因:

避免浅拷贝:默认情况下,C++编译器为类生成的拷贝构造函数和拷贝赋值运算符执行浅拷贝。这意味着它们仅复制对象的成员变量,而不是它们所指向的资源。对于资源管理类,浅拷贝可能导致多个对象共享相同的资源,从而引发悬挂指针和内存泄漏等问题。

确保深拷贝:为了正确地复制资源管理类,您需要确保执行深拷贝。这意味着您需要为新对象分配新的资源,并复制现有对象的状态。通过执行深拷贝,您可以确保每个对象拥有其自己的资源,从而避免资源共享和潜在的问题。

遵循“三五法则”:在C++中,有一个称为“三五法则”的指导原则,用于处理资源管理类的复制行为。根据这个原则,如果您需要为类定义以下任何一个特殊成员函数:拷贝构造函数、拷贝赋值运算符或析构函数,则您可能需要为类定义所有这三个函数。在C++11及更高版本中,这个原则扩展为“五法则”,还包括移动构造函数和移动赋值运算符。

以下是一个在资源管理类中正确处理复制行为的示例:

#include <iostream>
#include <cstring>

class MyClass {
public:
    MyClass(const char* str) {
        data_ = new char[strlen(str) + 1];
        strcpy(data_, str);
    }

    // 拷贝构造函数(执行深拷贝)
    MyClass(const MyClass& other) {
        data_ = new char[strlen(other.data_) + 1];
        strcpy(data_, other.data_);
    }

    // 拷贝赋值运算符(执行深拷贝)
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            delete[] data_;
            data_ = new char[strlen(other.data_) + 1];
            strcpy(data_, other.data_);
        }
        return *this;
    }

    ~MyClass() {
        delete[] data_;
    }

private:
    char* data_;
};

int main() {
    MyClass obj1("Hello, World!");

    // 使用拷贝构造函数复制对象(执行深拷贝)
    MyClass obj2(obj1);

    // 使用拷贝赋值运算符复制对象(执行深拷贝)
    MyClass obj3;
    obj3 = obj1;

    return 0;
}   

在这个示例中,我们为资源管理类MyClass实现了拷贝构造函数和拷贝赋值运算符,以确保执行深拷贝。这有助于避免资源泄漏、悬挂指针和未定义行为。

总之,在C++中,对资源管理类小心处理复制行为是非常重要的。为此,您应该避免浅拷贝、确保深拷贝,并遵循“三五法则”来定义特殊成员函数。这将帮助您确保正确管理资源,并避免潜在的问题。

第15条:在资源管理类中提供对原始资源的访问

在C++中,为资源管理类提供对原始资源的访问有助于提高代码的灵活性和兼容性。这是因为这样可以让您在需要时直接访问和操作底层资源,同时仍然保持资源管理类的封装和抽象。

为资源管理类提供对原始资源的访问通常是通过添加一个访问器(getter)方法来实现的。该方法返回底层资源的引用或指针,以便在需要时直接访问和操作资源。

以下是一个为资源管理类提供对原始资源访问的示例:

#include <iostream>
#include <fstream>
#include <string>

class File {
public:
    File(const std::string& filename) {
        file_.open(filename);
        if (!file_.is_open()) {
            throw std::runtime_error("Failed to open the file");
        }
    }

    ~File() {
        file_.close();
    }

    // 提供对原始资源(std::ofstream)的访问
    std::ofstream& get() {
        return file_;
    }

private:
    std::ofstream file_;
};

int main() {
    try {
        File file("example.txt");

        // 通过访问器直接访问原始资源(std::ofstream)
        file.get() << "Hello, World!";
    }
    catch (const std::runtime_error& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }

    return 0;
}   

在这个示例中,我们创建了一个名为File的资源管理类,它封装了一个文件资源。我们为该类添加了一个访问器方法get(),用于返回底层资源(std::ofstream)的引用。这使得我们可以在需要时直接访问和操作文件资源,同时仍然保持File类的封装和抽象。

总之,在C++中,为资源管理类提供对原始资源的访问可以提高代码的灵活性和兼容性。这使得您可以在需要时直接访问和操作底层资源,同时仍然保持资源管理类的封装和抽象。在实际开发过程中,您可以根据需要为资源管理类添加访问器方法来实现对原始资源的访问。

第16条:成对使用new和delete时要采取相同形式

在C++中,”成对使用new和delete时要采取相同形式”是指在分配和释放内存时,您应该使用相同类型的new和delete运算符。C++提供了两种类型的new和delete运算符:单个对象的new和delete以及对象数组的new[]和delete[]。为了正确地分配和释放内存,您需要确保使用相应的new和delete运算符。

new <-> delete

new[] <=> delete[]

以下是一些关于如何正确地使用new和delete运算符的示例:

  1. 为单个对象分配和释放内存:

    int* ptr = new int; // 使用new为单个对象分配内存
    // 使用和操作ptr…
    delete ptr; // 使用delete释放单个对象的内存

  2. 为对象数组分配和释放内存:

    int* arr = new int[10]; // 使用new[]为对象数组分配内存
    // 使用和操作arr…
    delete[] arr; // 使用delete[]释放对象数组的内存

    请注意,在这些示例中,我们使用相同类型的new和delete运算符来分配和释放内存。如果您尝试使用不匹配的new和delete运算符(如使用new[]分配内存,然后使用delete释放内存),可能会导致未定义行为,如内存泄漏、程序崩溃或数据损坏。

总之,在C++中,成对使用new和delete时要采取相同形式意味着在分配和释放内存时,您应该使用相同类型的new和delete运算符。这有助于确保正确地管理内存资源,避免内存泄漏和未定义行为。在实际开发过程中,您应该始终确保使用相应的new和delete运算符来分配和释放内存。

第17条:以独立语句将 new 对象放入智能指针

什么叫独立语句放入智能指针?

在C++中,将new分配的对象独立地放入智能指针(如std::shared_ptr或std::unique_ptr)是一种推荐的做法,因为这有助于避免异常安全问题和资源泄漏。

当您在表达式中直接将new分配的对象放入智能指针时,编译器可能会以不同的顺序计算new表达式和智能指针的构造函数。这可能导致在智能指针构造函数之前抛出异常,从而导致资源泄漏。

为了避免这个问题,您应该首先将new分配的对象独立地放入智能指针,然后再执行其他操作。这样可以确保智能指针立即接管对象的所有权,并在异常情况下正确地释放资源。

以下是一个将new分配的对象独立地放入智能指针的示例:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor" << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destructor" << std::endl;
    }

    // ...其他成员函数...
};

int main() {
    // 以独立语句将new分配的对象置入智能指针
    std::unique_ptr<MyClass> ptr(new MyClass());

    // 使用和操作ptr...

    return 0;
}   

在这个示例中,我们将new分配的MyClass对象独立地放入std::unique_ptr智能指针。这确保了智能指针立即接管对象的所有权,并在对象离开作用域时正确地释放资源。

总之,在C++中,将new分配的对象独立地放入智能指针是一种推荐的做法,因为这有助于避免异常安全问题和资源泄漏。在实际开发过程中,您应该始终确保将new分配的对象独立地放入智能指针。

不使用独立语句放入智能指针会怎么样?

如果您不采用独立语句将new对象放入智能指针,可能会在一个表达式中直接创建智能指针并将new分配的对象传递给它。这可能导致异常安全问题和资源泄漏,因为编译器可能以不同的顺序计算new表达式和智能指针的构造函数。

以下是一个在表达式中直接将new对象放入智能指针的示例:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor" << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destructor" << std::endl;
    }

    // ...其他成员函数...
};

void some_function(std::unique_ptr<MyClass> ptr1, std::unique_ptr<MyClass> ptr2) {
    // 使用和操作ptr1和ptr2...
}

int main() {
    // 在表达式中直接将new分配的对象置入智能指针(可能导致问题)
    some_function(std::unique_ptr<MyClass>(new MyClass()), std::unique_ptr<MyClass>(new MyClass()));

    return 0;
}   

在这个示例中,我们在some_function的调用表达式中直接创建了两个std::unique_ptr智能指针,并将new分配的对象传递给它们。这可能导致以下问题:

  1. 编译器可能会以不同的顺序计算new MyClass()表达式和std::unique_ptr构造函数。例如,编译器可能首先计算两个new MyClass()表达式,然后再计算智能指针的构造函数。如果在计算第二个new MyClass()表达式时抛出异常,第一个new MyClass()分配的对象将无法释放,从而导致资源泄漏。
    为了避免这个问题,您应该采用独立语句将new分配的对象放入智能指针,如下所示:

    int main() {

     std::unique_ptr<MyClass> ptr1(new MyClass());
     std::unique_ptr<MyClass> ptr2(new MyClass());
     some_function(std::move(ptr1), std::move(ptr2));
    
     return 0;
    

    }

    通过将new分配的对象独立地放入智能指针,您可以确保智能指针立即接管对象的所有权,并在异常情况下正确地释放资源。这有助于避免异常安全问题和资源泄漏。在实际开发过程中,您应该始终确保将new分配的对象独立地放入智能指针。

四、设计与声明

第18条:让接口容易被正确使用,不易被误用

第19条:设计class犹如设计type

第20条:宁以 pass-by-reference-to-const(引用传递) 替换 pass-by-value (值传递)

在C++中,”宁以 pass-by-reference-to-const 替换 pass-by-value”是一种建议,意味着在函数参数传递时,优先考虑使用引用传递(特别是常量引用)而不是值传递。这样做有几个好处,如提高性能、减少内存使用和支持大型对象。

当您使用值传递(pass-by-value)时,编译器会为函数参数创建一个新的副本。这可能导致以下问题:

性能下降:创建参数副本可能需要额外的时间和内存,特别是对于大型对象或结构。这可能导致性能下降,特别是在频繁调用的函数中。

不适用于不可复制的对象:对于不可复制的对象(如具有删除的拷贝构造函数的对象),值传递不适用。在这种情况下,您需要使用引用传递。

为了避免这些问题,您可以使用引用传递(特别是常量引用)替换值传递。引用传递不会创建参数副本,而是直接使用原始对象。这可以提高性能,减少内存使用,并支持大型和不可复制的对象。

以下是一个使用常量引用传递替换值传递的示例:

#include <iostream>
#include <string>

// 使用值传递(可能导致性能下降)
void print_by_value(std::string str) {
    std::cout << "By value: " << str << std::endl;
}

// 使用常量引用传递(更高效)
void print_by_const_reference(const std::string& str) {
    std::cout << "By const reference: " << str << std::endl;
}

int main() {
    std::string message = "Hello, World!";

    // 调用使用值传递的函数(可能导致性能下降)
    print_by_value(message);

    // 调用使用常量引用传递的函数(更高效)
    print_by_const_reference(message);

    return 0;
}

第21条:必须返回对象时,别妄想返回其reference

在C++中,”必须返回对象时,别妄想返回其reference”是一种建议,意味着当您需要从函数返回一个对象时,不应该返回该对象的引用。这是因为在某些情况下,返回对象的引用可能导致悬挂引用(dangling reference)和未定义行为。

特别是当您从函数返回局部对象(在栈上分配的对象)的引用时,可能会导致悬挂引用。这是因为局部对象在函数返回后会从栈中弹出,它们的内存将变得无效。如果您尝试访问这些无效引用,可能会导致内存访问错误、数据损坏和程序崩溃。

以下是一个尝试返回局部对象引用的示例,可能导致悬挂引用:

#include <iostream>

int& get_value() {
    int value = 42;
    return value; // 返回局部对象的引用(悬挂引用)
}

int main() {
    int& value_ref = get_value(); // 获取悬挂引用
    std::cout << "Value: " << value_ref << std::endl; // 访问悬挂引用(未定义行为)

    return 0;
}   

在这个示例中,我们尝试从get_value函数返回局部对象value的引用。这会导致悬挂引用,因为在函数返回后,局部对象的内存将变得无效。在main函数中,我们尝试访问这个悬挂引用,这可能导致未定义行为。

为了避免这个问题,您应该直接返回对象,而不是其引用。这会创建一个新的对象副本,以便在函数返回后仍然有效。在C++11及更高版本中,编译器通常会使用返回值优化(RVO)或移动语义来避免额外的副本开销。

以下是一个正确返回对象的示例:

#include <iostream>

int get_value() {
    int value = 42;
    return value; // 直接返回对象(安全)
}

int main() {
    int value = get_value(); // 获取对象副本
    std::cout << "Value: " << value << std::endl; // 访问对象副本(安全)

    return 0;
}   

传参应该用引用,返回值应该对象。

在这个示例中,我们直接从get_value函数返回对象,而不是其引用。这确保了在函数返回后,我们可以安全地访问对象的副本,而不会导致悬挂引用和未定义行为。

总之,在C++中,必须返回对象时,别妄想返回其reference 是一种建议,用于避免悬挂引用和未定义行为。在实际开发过程中,您应该直接返回对象,而不是其引用,以确保正确地管理对象的生命周期。

第22条:将成员变量声明为private

将所有成员对象封装成private,外界真的需要操作时候提供public接口就好。

第23条:宁以 non-member、non-friend 替换 member函数

什么是 non-member、non-friend 和 member函数?

non-member、non-friend 函数没啥区别,就是不在类里面声明的函数,简单理解就是C语言面向过程阶段时的函数。

具体含义是什么?

在C++中,”宁以non-member、non-friend替换member函数”是一种建议,意味着在可能的情况下,优先使用非成员(non-member)和非友元(non-friend)函数,而不是成员(member)函数。这样做可以提高代码的封装性、灵活性和可重用性。

以下是使用非成员和非友元函数的一些原因:

封装性:非成员和非友元函数不需要访问类的私有成员,这有助于确保类的封装性。这使得您可以更容易地修改类的实现,而无需更改与之交互的函数。

灵活性:非成员和非友元函数可以与多个类一起使用,这使得您可以更容易地在不同类之间共享和重用代码。

可重用性:非成员和非友元函数可以在不依赖类的情况下单独使用。这使得您可以更容易地将这些函数用于其他项目和库。

以下是一个使用非成员和非友元函数的示例:

#include <iostream>

class MyClass {
public:
    MyClass(int value) : value_(value) {}

    int value() const {
        return value_;
    }

private:
    int value_;
};

// 非成员、非友元函数
bool compare_values(const MyClass& obj1, const MyClass& obj2) {
    return obj1.value() < obj2.value();
}

int main() {
    MyClass obj1(42);
    MyClass obj2(100);

    // 使用非成员、非友元函数比较对象
    bool result = compare_values(obj1, obj2);
    std::cout << "Result: " << result << std::endl;

    return 0;
}   

在这个示例中,我们定义了一个名为MyClass的类,并为其添加了一个非成员、非友元函数compare_values。该函数用于比较两个MyClass对象的值。请注意,compare_values函数不需要访问类的私有成员,因为它可以使用公共成员函数value()来获取对象的值。

总之,在C++中,宁以non-member、non-friend替换member函数是一种建议,用于提高代码的封装性、灵活性和可重用性。在实际开发过程中,您应该优先使用非成员和非友元函数,而不是成员函数,以确保正确地管理类的接口和实现。

第24条:若所有参数皆需类型转换,请为此采用 non-member 函数

先补充一下,理解清楚什么是 隐式类型转换

隐式类型转换是一种在C++(以及其他编程语言)中自动发生的类型转换,无需显式地指定类型转换。编译器会在需要时自动执行隐式类型转换,以使表达式、函数调用或赋值操作的类型匹配。

隐式类型转换可能发生在以下情况:

  1. 混合类型的算术运算:当两个不同类型的操作数参与算术运算时,编译器会自动将其中一个操作数的类型转换为另一个操作数的类型。例如,当一个int值与一个double值相加时,编译器会将int值隐式地转换为double值,然后执行加法运算。

    int a = 42;
    double b = 3.14;
    double c = a + b; // 隐式类型转换:将int值a转换为double

  2. 函数参数:当调用函数并传递参数时,编译器会自动将实际参数的类型转换为形式参数的类型(如果可以的话)。例如,当您将一个int值传递给接受double参数的函数时,编译器会将int值隐式地转换为double值。

    void print_double(double x) {

     std::cout << "Value: " << x << std::endl;
    

    }

    int main() {

     int a = 42;
     print_double(a); // 隐式类型转换:将int值a转换为double
    
     return 0;
    

    }

  3. 类型转换构造函数和转换运算符:C++允许为类定义类型转换构造函数和转换运算符,以支持隐式地将其他类型的值转换为类的对象,或将类的对象转换为其他类型的值。

    class MyClass {
    public:

     // 类型转换构造函数:允许从int到MyClass的隐式类型转换
     MyClass(int value) : value_(value) {}
    
     // 转换运算符:允许从MyClass到int的隐式类型转换
     operator int() const {
         return value_;
     }
    

    private:

     int value_;
    

    };

    int main() {

     MyClass obj = 42; // 隐式类型转换:使用类型转换构造函数将int值转换为MyClass对象
     int value = obj;  // 隐式类型转换:使用转换运算符将MyClass对象转换为int值
    
     return 0;
    

    }

    请注意,虽然隐式类型转换可以简化代码并提高编程效率,但在某些情况下,它可能导致意外的行为和错误。因此,在实际开发过程中,您应该谨慎使用隐式类型转换,并在需要时使用显式类型转换(如static_cast、dynamic_cast等)来指定类型转换。

要知道有隐式类型转换,但平时开发过程中不建议使用。尤其是第3种。。太丑了。

具体含义

在C++中,”若所有参数皆需类型转换,请为此采用non-member函数”是一种建议,意味着当一个操作需要为所有参数执行类型转换时,您应该使用非成员(non-member)函数而不是成员(member)函数。这样做可以提高代码的灵活性,因为非成员函数允许对所有参数执行隐式类型转换,而成员函数只允许对非this参数执行隐式类型转换。

以下是一个使用非成员函数以支持所有参数的类型转换的示例:

#include <iostream>

class MyClass {
public:
    MyClass(int value) : value_(value) {} // 允许从int到MyClass的隐式类型转换

    int value() const {
        return value_;
    }

private:
    int value_;
};

// 非成员函数
bool operator<(const MyClass& obj1, const MyClass& obj2) {
    return obj1.value() < obj2.value();
}

int main() {
    MyClass obj1(42);
    int obj2 = 100;

    // 使用非成员函数比较对象(允许对所有参数执行隐式类型转换)
    bool result = obj1 < obj2; // 编译器自动调用operator<函数
    std::cout << "Result: " << result << std::endl;

    return 0;
}   

说相当于写非成员函数 bool operator< 是覆写了系统原先的operator吗? 以及operator后面的<是什么意思?

是的,您理解正确。在C++中,您可以通过定义operator函数来覆盖系统原始的运算符操作。这使得您可以为自定义类型实现特定的运算符行为。这种方法称为运算符重载(operator overloading)。

在示例中,我们定义了一个名为operator<的非成员函数,它覆盖了系统原始的<运算符。这使得我们可以直接将MyClass对象与int值进行比较,而无需显式地转换类型。

operator后面的<是要重载的运算符。在这个示例中,我们重载了<运算符,以便在MyClass对象之间执行比较操作。您还可以重载其他运算符,如+、-、*、/等,以支持自定义类型的算术、比较和逻辑操作。

以下是一个重载+运算符的示例:

#include <iostream>

class MyClass {
public:
    MyClass(int value) : value_(value) {}

    int value() const {
        return value_;
    }

private:
    int value_;
};

// 非成员函数:重载+运算符
MyClass operator+(const MyClass& obj1, const MyClass& obj2) {
    return MyClass(obj1.value() + obj2.value());
}

int main() {
    MyClass obj1(42);
    MyClass obj2(100);

    // 使用重载的+运算符
    MyClass obj3 = obj1 + obj2;
    std::cout << "Result: " << obj3.value() << std::endl;

    return 0;
}   

第25条:考虑写出一个不抛异常的 swap 函数

在C++中,编写一个不抛异常的swap函数可能很困难,因为在交换对象的过程中,您可能需要执行一些可能抛出异常的操作,如分配内存、调用构造函数或赋值运算符等。

然而,在某些情况下,您可以通过优化代码和使用特定的技术来实现不抛异常的swap函数。例如,您可以使用std::swap函数或为自定义类型实现特殊的swap函数,以便在交换对象时避免执行可能抛出异常的操作。

以下是一个使用std::swap函数的示例,该函数不会抛出异常:

#include <iostream>
#include <vector>
#include <algorithm> // for std::swap

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用std::swap函数交换元素(不抛异常)
    std::swap(numbers[0], numbers[1]);

    for (int number : numbers) {
        std::cout << number << std::endl;
    }

    return 0;
}