Bootstrap

【Effective C++】阅读笔记2

1. 复制对象时要保证复制内容完整性

错误场景复现(没有复制基类部分)

如果一个类中包含多个成员变量或者继承了基类,那么在拷贝构造函数或者赋值运算符中,必须保证所有成员的基类部分被复制。

基类没有被复制,这样就都导致了其函数返回的是默认数值0,可能会引起逻辑错误

 

#include <iostream>

class Base {
public:
    Base(int value = 0) : value_(value) {}
    int getValue() const { return value_; }

private:
    int value_;
};

class Derived : public Base {
public:
    Derived(int value, int extra) : Base(value), extra_(extra) {}

    // 错误:未复制基类部分
    Derived(const Derived& other) : extra_(other.extra_) {}

    void print() const {
        std::cout << "基类的值:" << getValue() << ",派生类的值:" << extra_ << std::endl;
    }

private:
    int extra_;
};

int main() {
    Derived d1(10, 20);
    Derived d2 = d1;  // 使用拷贝构造函数
    d2.print();  

    return 0;
}

 解决思路:显式的调用基类拷贝构造函数

 通过拷贝构造函数中,显式的调用基类的拷贝构造函数,这样就可以保证基类部分完整的复制

 

#include <iostream>

class Base {
public:
    Base(int value = 0) : value_(value) {}
    Base(const Base& other) : value_(other.value_) {}  // 拷贝构造函数
    int getValue() const { return value_; }

private:
    int value_;
};

class Derived : public Base {
public:
    Derived(int value, int extra) : Base(value), extra_(extra) {}

    // 显式调用基类的拷贝构造函数
    Derived(const Derived& other) : Base(other), extra_(other.extra_) {}

    void print() const {
        std::cout << "基类的值:" << getValue() << ",派生类的值:" << extra_ << std::endl;
    }

private:
    int extra_;
};

int main() {
    Derived d1(10, 20);
    Derived d2 = d1;  // 使用拷贝构造函数
    d2.print(); 

    return 0;
}

动态资源拷贝时,需要进行深拷贝,避免浅拷贝

一般出现在类中有指针成员的时候,一定要使用深拷贝,避免内存泄漏或者双重释放问题

#include <iostream>
#include <cstring>

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

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

    ~String() {
        delete[] data_;
    }

    void print() const {
        std::cout << "字符串:" << data_ << std::endl;
    }

private:
    char* data_;
};

int main() {
    String s1("hhahha");
    String s2 = s1;  // 使用拷贝构造函数
    s2.print();  

    return 0;
}

总结与反思

  • 类中成员复制的时候要保证其完整性,在有动态资源时,使用深拷贝,避免资源泄漏问题
  • 尽量使用智能指针管理资源,管理动态资源的时候,智能指针可以简化深拷贝逻辑,同时还能够避免资源泄漏

2. RAII

RAII原理理解

核心思想,将资源的管理和生命周期绑定在一起,当对象创建的时候获取资源,对象销毁的时候自动释放,从而保证资源正常管理避免资源泄漏。

实现原理

  • RAII中,将资源封装到一个对象的构造函数和析构函数中
  • 构造函数负责在对象创建的时候获取资源
  • 析构函数负责在对象销毁的时候释放资源
  • 借助构造函数与析构函数,当对象超出作用域的时候,析构函数会自动调用从而释放资源,避免资源泄漏

RAII的好处

  • 避免内存泄漏:动态内存分配的时候如果忘记释放资源,就会导致内存泄漏
  • RAII可以保证即使发生异常,对象的资源也可以被正确的释放
  • 将资源管理逻辑封装在类的内部,也让代码更好维护

RAII简单实现

#include <iostream>

class Array {
public:
    Array(size_t size) {
        data_ = new int[size];  // 在构造函数中分配资源
        std::cout << "分配内存" << std::endl;
    }

    ~Array() {  // 在析构函数中释放资源
        delete[] data_;
        std::cout << "释放内存" << std::endl;
    }

private:
    int* data_;
};

void withRAII() {
    Array arr(10);  // 自动管理内存
}

int main() {
    withRAII();  // 函数结束后自动调用析构函数,释放内存
    return 0;
}

直接使用智能指针就可以实现RAII

智能指针的特性就是自动管理内存,当对象超出作用域的时候,内存就会自动释放

#include <iostream>
#include <memory>  // 包含智能指针头文件

void withSmartPointer() {
    std::unique_ptr<int[]> data = std::make_unique<int[]>(10);  // 自动管理内存
    std::cout << "分配内存" << std::endl;
    // 内存将在函数结束时自动释放
}

int main() {
    withSmartPointer();
    std::cout << "智能指针管理内存完成" << std::endl;
    return 0;
}

总结与反思

  • 智能指针管理内存最方便,不用自己手动实现RAII
  • RAII可以有效的避免资源泄漏,将资源的生命周期与对象的生命周期绑定在一起

 3. 资源管理类(RAII)中小心拷贝行为

拷贝时资源泄漏和双重释放问题分析

因为资源管理类负责在对象生命周期结束的时候释放资源,所以当资源管理类拷贝的时候,就可能会出现资源泄漏和双重释放问题

  • 例如当使用默认构造函数进行拷贝的时候,此时的浅拷贝就是让其共享内存,最终释放资源的时候也就导致了双重释放错误

#include <iostream>

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

    ~Resource() {
        delete[] data_;  // 释放资源
    }

    void print() const {
        std::cout << "数据:" << data_ << std::endl;
    }

private:
    char* data_;
};

int main() {
    Resource r1("Hello");  // 创建对象并分配资源
    Resource r2 = r1;       // 使用默认拷贝构造函数(浅拷贝)

    r2.print();  // 输出:数据:Hello
    // 当 r1 和 r2 被销毁时,都会尝试释放同一块内存

    return 0;
}

 解决方法1:直接禁用拷贝操作

简单粗暴的方法,直接禁止拷贝构造,这样就不会被多次释放情况

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

    ~Resource() {
        delete[] data_;
    }

    Resource(const Resource&) = delete;  // 禁用拷贝构造函数
    Resource& operator=(const Resource&) = delete;  // 禁用赋值运算符

    void print() const {
        std::cout << "数据:" << data_ << std::endl;
    }

private:
    char* data_;
};

int main() {
    Resource r1("Hello");
    // Resource r2 = r1;  // 编译错误:拷贝构造被禁用

    r1.print();
    return 0;
}

方法2:实现深拷贝

给构造函数和赋值运算符都实现一个的深拷贝;使用深拷贝确保每个对象都有独立的资源,避免共享资源导致双重释放问题

#include <iostream>
#include <cstring>

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

    // 深拷贝构造函数
    Resource(const Resource& other) {
        data_ = new char[strlen(other.data_) + 1];
        strcpy(data_, other.data_);
        std::cout << "拷贝构造函数被调用" << std::endl;
    }

    // 深拷贝赋值运算符
    Resource& operator=(const Resource& other) {
        if (this == &other) return *this;  // 检测自我赋值

        delete[] data_;  // 释放旧资源
        data_ = new char[strlen(other.data_) + 1];
        strcpy(data_, other.data_);

        std::cout << "赋值运算符被调用" << std::endl;
        return *this;
    }

    ~Resource() {
        delete[] data_;  // 释放资源
    }

    void print() const {
        std::cout << "数据:" << data_ << std::endl;
    }

private:
    char* data_;
};

int main() {
    Resource r1("Hello");
    Resource r2 = r1;  // 调用拷贝构造函数
    r2.print();

    Resource r3("World");
    r3 = r1;  // 调用赋值运算符
    r3.print();

    return 0;
}

 方法3:智能指针

拷贝的时候使用智能指针管理内存,然后通过移动语义转移资源。实现方法就是类中动态资源使用智能指针管理,拷贝以及赋值的时候使用move

  • 移动构造:将一个对象的资源移动到当前对象,避免资源的深拷贝
  • 移动赋值:对象直接移动资源,需要判断目标对象是否已经存在,如果存在则需要先释放资源

#include <iostream>
#include <memory>
#include <cstring> 

class Resource {
public:
    // 构造函数
    Resource(const char* data) {
        data_ = std::make_unique<char[]>(strlen(data) + 1);
        strcpy(data_.get(), data);
        std::cout << "构造函数被调用" << std::endl;
    }

    // 移动构造函数
    Resource(Resource&& other) noexcept {
        data_ = std::move(other.data_);
        std::cout << "移动构造函数被调用" << std::endl;
    }

    // 移动赋值运算符
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {  // 避免自我赋值
            data_ = std::move(other.data_);
            std::cout << "移动赋值运算符被调用" << std::endl;
        }
        return *this;
    }

    // 删除拷贝构造函数和拷贝赋值运算符,防止拷贝
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;

    // 打印数据
    void print() const {
        std::cout << "数据:" << data_.get() << std::endl;
    }

private:
    std::unique_ptr<char[]> data_;
};

int main() {
    // 创建资源对象 r1
    Resource r1("Hello");
    r1.print();

    // 使用移动构造函数
    Resource r2 = std::move(r1);  // r1 的资源转移到 r2
    r2.print();
    // r1 现在为空,不能再使用其资源。

    // 使用移动赋值运算符
    Resource r3("World");
    r3 = std::move(r2);  // r2 的资源转移到 r3
    r3.print();
    // r2 也变为空

    return 0;
}

总结反思

  • 资源管理类中,要避免多个对象共享同一资源,防止双重释放
  • 如果类需要拷贝就一定要使用深拷贝,否则就要禁止拷贝操作
  • 优先使用移动语义和智能指针

4. 资源管理类(RAII)中提供对原始资源的访问

场景分析

 资源管理类将资源封装起来,让对象生命周期结束的时候自动释放资源,但是某些函数或者其他情况还是需要访问原始资源,例如函数需要调用原始资源中智能指针管理的原始指针。

  • 获取 std::unique_ptrstd::shared_ptr 管理的原始指针。
  • 获取文件句柄或网络连接句柄,供第三方库函数使用。

方法1:使用智能指针管理资源并提供原始指针访问

智能指针负责对动态资源管理,同时类中通过get()方法提供对原始指针的访问

  • 注意return resource_.get()中的get方法,是智能指针中提供的一个获取内部管理的原始指针

#include <iostream>
#include <memory>  // 包含智能指针头文件

class ResourceManager {
public:
    ResourceManager(const char* data) {
        resource_ = std::make_unique<char[]>(strlen(data) + 1);
        strcpy(resource_.get(), data);
    }

    // 提供对原始指针的访问
    char* get() const {
        return resource_.get();
    }

    void print() const {
        std::cout << "资源内容:" << resource_.get() << std::endl;
    }

private:
    std::unique_ptr<char[]> resource_;  // 使用智能指针管理资源
};

int main() {
    ResourceManager manager("Hello, World!");
    manager.print();

    // 使用原始指针与第三方库交互
    char* rawPointer = manager.get();
    std::cout << "通过原始指针访问资源:" << rawPointer << std::endl;

    return 0;
}

 方法2:std::shared_ptr和weak_ptr

也是借助这两种智能指针,实现对原始资源的访问,此处回顾线程安全中智能指针的使用

#include <iostream>
#include <memory>  // 包含智能指针头文件

class ResourceManager {
public:
    ResourceManager(const char* data) {
        resource_ = std::make_shared<std::string>(data);
    }

    // 提供对原始资源的访问
    std::string* get() const {
        return resource_.get();
    }

    void print() const {
        std::cout << "资源内容:" << *resource_ << std::endl;
    }

private:
    std::shared_ptr<std::string> resource_;  // 使用 shared_ptr 管理资源
};

int main() {
    ResourceManager manager("共享资源");
    manager.print();

    // 获取原始指针并使用
    std::string* rawPointer = manager.get();
    std::cout << "通过原始指针访问资源:" << *rawPointer << std::endl;

    return 0;
}

原始资源安全访问的注意点

  • 分清访问类型 
    • 如果是只读访问,那么提供只读访问接口
    • 避免外部代码直接修改资源,这样就可以保证资源管理类的封装性
  • 避免悬空指针
    • 确保资源管理类的对象原始指针在使用期间都不会被销毁
    • 要尽可能的缩短原始指针的生命周期,从而避免悬空指针问题
  • 配合shared_ptr一同使用
    • 如果资源需要在多个地方共享,可以使用 std::shared_ptr,并提供访问方法,如 get()weak_ptr

总结反思

  • 提供原始资源的安全访问接口:在资源管理类中,可以通过 get() 或类似的方法提供对原始资源的访问
  • 尽量缩短原始指针的生命周期:减少悬空指针的风险
  • 优先使用智能指针:如 unique_ptrshared_ptr,自动管理资源的生命周期,避免内存泄漏

5. 使用正确的形式释放内存

错误分析

核心就是必须是使用正确配对的方式来释放内存,否则就会导致未定义行为或者内存泄露

  • 单对象:new/delete
  • 数组:new[ ] / delete [ ] 

问题出现原因分析

  • new 和 new[ ] 实现不同,new[ ]可能会为数组元素额外的分配一些内存存储数组大小,所以其内存布局和单个对象是不同的
  • 如果使用delete释放new [ ] 分配的内存,就有可能调用所有对象的析构函数,从而导致资源泄露

错误用例分析

// 错误1

int main() {
    int* p = new int(10);  // 分配单个 int 对象
    delete[] p;  // 错误:使用 delete[] 释放单对象

    return 0;
}

// 错误2

int main() {
    int* p = new int[5];  // 分配一个 int 数组
    delete p;  // 错误:使用 delete 释放数组

    return 0;
}

正确配对方法

#include <iostream>

int main() {
    // 分配单个对象
    int* p1 = new int(10);
    delete p1;  // 正确:使用 delete 释放单个对象

    // 分配数组
    int* p2 = new int[5];
    delete[] p2;  // 正确:使用 delete[] 释放数组

    return 0;
}

总结反思

  • 手动管理内存的时候,一定要严格按照new 和 delete的匹配原则,避免形式错误导致的内存泄露或者没定义的行为
  • 优先选择智能指针去管理内存

6. 使用独立语句将new对象置入智能指针

问题分析

例如使用一个函数的同时使用std::shared_ptr管理对象的生命周期,当这个函数初始化的时候出现异常。那么在这种new和std::shared_ptr构造都放在一个语句中,就会导致内存泄漏问题

  • 下面代码问题关键在于,process()抛出异常之前的时候,已经new出来了内存,所以在智能指针构造的时候出现异常,内存也就会无法释放
  • 内存泄漏的发生,就是因为New创建的对象无法被智能指针管理

#include <iostream>
#include <memory>

void process(std::shared_ptr<int> ptr) {
    throw std::runtime_error("处理时发生异常");
}

int main() {
    try {
        // 错误:如果 process 抛出异常,new 分配的对象无法释放
        process(std::shared_ptr<int>(new int(42)));
    }
    catch (const std::exception& e) {
        std::cout << "捕获异常:" << e.what() << std::endl;
    }

    return 0;
}

解决方法1:使用独立的语句初始化智能指针

也就是将new对象和智能指针构造分离成两个语句,从而确保即使发生异常,内存也可以被正确的管理。

#include <iostream>
#include <memory>

void process(std::shared_ptr<int> ptr) {
    throw std::runtime_error("处理时发生异常");
}

int main() {
    try {
        // 使用独立语句分配对象并置入智能指针
        std::shared_ptr<int> ptr(new int(42));
        process(ptr);  // 如果抛出异常,ptr 会自动释放内存
    }
    catch (const std::exception& e) {
        std::cout << "捕获异常:" << e.what() << std::endl;
    }

    return 0;
}

解决方法2:使用make_shared或者make_unique

make_shared将对象构造和控制块创建合并成了一个操作,从而避免内存泄漏风险 

void process(std::shared_ptr<int> ptr) {
    throw std::runtime_error("处理时发生异常");
}

int main() {
    try {
        auto ptr = std::make_shared<int>(42);  // 更优雅的方式
        process(ptr);  // 如果抛出异常,ptr 会自动释放内存
    }
    catch (const std::exception& e) {
        std::cout << "捕获异常:" << e.what() << std::endl;
    }

    return 0;
}

总结反思  

  • 使用智能指针应该尽可能的简洁安全,多使用make_shared()等函数,让代码可读性更高
  • new操作与智能指针进行分离,保证在异常的时候也可以释放资源

7. 设计易于使用的接口

不好用接口分析

设计类中接口的时候,不仅要满足基本功能需求,同时还需要防止接口误用

  • 下述代码如果忘记调用f.close(),那么文件就不会正确保存
#include <iostream>
#include <fstream>

class File {
public:
    File(const std::string& filename) {
        file_.open(filename);
    }

    void write(const std::string& data) {
        file_ << data;
    }

    void close() {
        file_.close();
    }

private:
    std::ofstream file_;
};

int main() {
    File f("example.txt");
    f.write("Hello, World!");
    f.write("Another line.");
    // 忘记调用 f.close(),可能导致文件未正确保存
    return 0;
}

方法1:通过RAII思想,在析构函数的时候自动关闭文件 

#include <iostream>
#include <fstream>

class File {
public:
    File(const std::string& filename) {
        file_.open(filename);
    }

    ~File() {  // 在析构函数中关闭文件
        if (file_.is_open()) {
            file_.close();
            std::cout << "文件已关闭" << std::endl;
        }
    }

    void write(const std::string& data) {
        if (file_.is_open()) {
            file_ << data;
        }
    }

private:
    std::ofstream file_;
};

int main() {
    {
        File f("example.txt");
        f.write("Hello, World!");
    }  // 离开作用域时自动关闭文件

    return 0;
}

方法2:使用智能指针简化

通过智能指针,实现对资源的管理,避免内存泄漏

#include <iostream>
#include <memory>

class Resource {
public:
    Resource() { std::cout << "资源被分配" << std::endl; }
    ~Resource() { std::cout << "资源被释放" << std::endl; }

    void doSomething() { std::cout << "执行操作" << std::endl; }
};

void useResource() {
    auto res = std::make_unique<Resource>();  // 使用智能指针管理资源
    res->doSomething();
}  // 离开作用域时,资源自动释放

int main() {
    useResource();
    return 0;
}

方法3:通过编译期检查规避错误

使用delete关键字,直接禁用自己不希望存在的操作,这样就可以在编译期间直接捕获错误

class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;  // 禁用拷贝构造
    NonCopyable& operator=(const NonCopyable&) = delete;  // 禁用赋值操作
};

int main() {
    NonCopyable obj1;
    // NonCopyable obj2 = obj1;  // 编译错误:拷贝构造被禁用
    return 0;
}

方法4:使用强类型枚举,避免枚举混用的错误

#include <iostream>

enum class Color { Red, Green, Blue };
enum class TrafficLight { Red, Yellow, Green };

void printColor(Color color) {
    switch (color) {
    case Color::Red: std::cout << "红色" << std::endl; break;
    case Color::Green: std::cout << "绿色" << std::endl; break;
    case Color::Blue: std::cout << "蓝色" << std::endl; break;
    }
}

int main() {
    printColor(Color::Red);
    printColor(TrafficLight::Red);  // 编译错误:类型不匹配
    return 0;
}

总结反思

  • 编程前多思考,使用多种方法设计安全好用的接口

8. 设计类的时候需要考虑类型转换的安全性

问题分析

C++中允许隐式类型转换可能会引起错误,例如当类中有支持允许隐式类型转换的构造函数,编译的时候就会自动调用,但是这不一定是我们想要看到的,因为部分情况下是不需要进行类型转换的

class Fraction {
public:
    Fraction(int numerator, int denominator)
        : numerator_(numerator), denominator_(denominator) {}

    // 单参数构造函数允许隐式类型转换
    Fraction(int wholeNumber) : numerator_(wholeNumber), denominator_(1) {}

    void print() const {
        std::cout << numerator_ << "/" << denominator_ << std::endl;
    }

private:
    int numerator_;
    int denominator_;
};

void printFraction(const Fraction& frac) {
    frac.print();
}

int main() {
    printFraction(5); 
    return 0;
}

方法1:将构造函数直接标记成explicit

  • explicit关键字可以防止构造函数被用作隐式类型转换的路径,也就是用户必须明确调用构造函数
#include <iostream>

class Fraction {
public:
    explicit Fraction(int numerator, int denominator)
        : numerator_(numerator), denominator_(denominator) {}

    explicit Fraction(int wholeNumber)
        : numerator_(wholeNumber), denominator_(1) {}

    void print() const {
        std::cout << numerator_ << "/" << denominator_ << std::endl;
    }

private:
    int numerator_;
    int denominator_;
};

void printFraction(const Fraction& frac) {
    frac.print();
}

int main() {
    //printFraction(5);  // 编译错误:不能进行隐式转换
    printFraction(Fraction(5));  // 必须显式构造对象
    return 0;
}

问题2:不加限制的使用重载运算符使用,可能会导致类型转换混乱

  • 类型转换成运算符会将Fraction对象隐式的转换为double
    • operator double()定义了一个类型转换运算符,其允许Fraction对象被转换成double类型
    • 具体来说将numerator_转换成double类型,然后除以denominator_
  • 隐式类型转换流程分析
    • 首先frac是一个Fraction类型,而1.5是double类型,所以编译器需要将frac转换成double,这样在可以与1.5进行相加
    • 因为类中定义了类型转换运算符operator double(),编译器会隐式调用这个运算符,将frac转换为double
    • 返回值也通过强制类型转换为了double类型
#include <iostream>

class Fraction {
public:
    Fraction(int numerator, int denominator) 
        : numerator_(numerator), denominator_(denominator) {}

    // 类型转换运算符,将 Fraction 转换为 double
    operator double() const {
        return static_cast<double>(numerator_) / denominator_;
    }

private:
    int numerator_;
    int denominator_;
};

int main() {
    Fraction frac(3, 2);
    std::cout << frac + 1.5 << std::endl;  // 隐式转换为 double
    return 0;
}

解决上述问题思路,仍然是将类型转换运算符标记为explicit

利用explicit来修饰类型转换运算符,这样当用户需要类型转换时显式调用类型转换运算符,从而避免隐式转换的弊端

#include <iostream>

class Fraction {
public:
    Fraction(int numerator, int denominator) 
        : numerator_(numerator), denominator_(denominator) {}

    // 使用 explicit 修饰类型转换运算符
    explicit operator double() const {
        return static_cast<double>(numerator_) / denominator_;
    }

private:
    int numerator_;
    int denominator_;
};

int main() {
    Fraction frac(3, 2);
    // std::cout << frac + 1.5 << std::endl;  // 编译错误:隐式转换被禁止
    std::cout << static_cast<double>(frac) + 1.5 << std::endl;  // 显式转换
    return 0;
}

总结反思

  • 设计接口的时候,要避免不必要的隐式类型转换,让接口变的安全可靠
  • 通过explicit防止编译器进行意外的类型转换

9. 常量引用转递代替值传递

避免按值传递原因分析

按值传递的时候,C++会创建一个临时对象,从而会增加性能开销。

#include <iostream>
#include <string>

void printName(std::string name) {  // 按值传递,拷贝了 name
    std::cout << "名字:" << name << std::endl;
}

int main() {
    std::string myName = "Alice";
    printName(myName);  // 会调用拷贝构造函数
    return 0;
}

解决方法:通过引用传递,不会创建对象副本,从而减少内存消耗

#include <iostream>
#include <string>

void printName(const std::string& name) {  // 常量引用传递
    std::cout << "名字:" << name << std::endl;
}

int main() {
    std::string myName = "Alice";
    printName(myName);  // 不会调用拷贝构造函数
    return 0;
}

按值传递的特殊场景

  • 内置类型,一些较小的类型,直接使用按值传递,反而方便
  • 需要修改副本内容的时候,也就是传递给函数使用的时候,希望修改副本内容但是不影响原内容的时候

移动语义实现传递

  • std::move会将对象标记为右值,允许函数内部直接管理资源,避免拷贝
  • 使用移动语义减少内存分配和拷贝开销

#include <iostream>
#include <string>

void modifyCopy(std::string name) {
    name += " Smith";  // 修改副本
    std::cout << "修改后的名字:" << name << std::endl;
}

int main() {
    std::string myName = "Alice";
    modifyCopy(std::move(myName));  // 使用移动语义,避免拷贝
    std::cout << "原名字:" << myName << std::endl;  // 变为空字符串
    return 0;
}

总结反思

  • 常量引用传递可以减少不必要的拷贝
  • 使用move优化按值传递,从而进一步提高性能

 10. 必须返回对象的时候,不要试图返回器引用

问题分析

强调如果函数必须返回一个对象的时候,不要返回该对象的引用或者指针,这是因为返回对象的引用或者指针可能会导致悬空引用或者未定义行为。

返回局部对象引用的时候就会导致悬空引用

  • 首先局部变量在函数返回后就会被销毁
  • 然后如果尝试返回一个局部对象的引用或者指针,那么就会得到一个指向无效内存的引用或者指针,也就导致了未定义行为
  • 注意:在visual studio中测试可能最终打印结果是10,出现这个的原因怀疑是内存没有及时被覆盖的情况
#include <iostream>

int& getLocalVariable() {
    int x = 10;  // 局部变量
    return x;  // 返回局部变量的引用(错误)
}

int main() {
    int& ref = getLocalVariable();  // ref 指向已销毁的内存
    std::cout << "ref = " << ref << std::endl;  // 未定义行为
    return 0;
}

解决方法:通过返回对象值

  • 通过返回对象值,x的值就被拷贝并返回给调用者,避免了悬空引用的问题
  • 而且现在C++编译器也会通过特定的优化,减少拷贝的开销

#include <iostream>

int getLocalVariable() {
    int x = 10;  // 局部变量
    return x;  // 返回对象值
}

int main() {
    int value = getLocalVariable();  // 正常赋值,不会导致悬空引用
    std::cout << "value = " << value << std::endl;  // 输出:10
    return 0;
}

返回类对象的值

自己定义的类,返回对象的值是一种较为安全的选择,即使对象很大,因为编译器有优化机制,也可以避免不必要的拷贝

#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name) : name_(name) {}

    std::string getName() const {
        return name_;  // 按值返回字符串
    }

private:
    std::string name_;
};

int main() {
    Person p("Alice");
    std::string name = p.getName();  // 按值获取 name
    std::cout << "名字:" << name << std::endl;  // 输出:名字:Alice
    return 0;
}

返回引用场景1:返回类成员的引用 

如果想要修改类的成员变量,那么可以返回该成员的引用

#include <iostream>

class Person {
public:
    Person(const std::string& name) : name_(name) {}

    std::string& getName() {  // 返回成员变量的引用
        return name_;
    }

private:
    std::string name_;
};

int main() {
    Person p("Alice");
    p.getName() = "Bob";  // 修改成员变量
    std::cout << "名字:" << p.getName() << std::endl;  // 输出:名字:Bob
    return 0;
}

返回引用场景2:返回静态变量的引用

因为静态变量的生命周期是贯穿整个程序的,所以返回它的引用是十分安全

#include <iostream>

int& getStaticVariable() {
    static int x = 42; 
    return x;
}

int main() {
    int& ref = getStaticVariable();
    ref = 10;  // 修改静态变量的值
    std::cout << "静态变量的值:" << getStaticVariable() << std::endl; 
    return 0;
}

总结反思

  • 避免返回局部对象的引用或者指针,局部对象会在函数返回后销毁,返回后的引用会导致悬空引用问题
  • 优先按值返回对象,因为现代编译器有优化,可以避免不必要的拷贝
  • 返回引用的场景,返回类成员的引用和静态成员变量引用 
;