【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() {// 创建资源对象 r1Resource r1("Hello");r1.print();// 使用移动构造函数Resource r2 = std::move(r1);  // r1 的资源转移到 r2r2.print();// r1 现在为空,不能再使用其资源。// 使用移动赋值运算符Resource r3("World");r3 = std::move(r2);  // r2 的资源转移到 r3r3.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 [ ] 分配的内存,就有可能调用所有对象的析构函数,从而导致资源泄露

错误用例分析

// 错误1int main() {int* p = new int(10);  // 分配单个 int 对象delete[] p;  // 错误:使用 delete[] 释放单对象return 0;
}// 错误2int 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 转换为 doubleoperator 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;  // 隐式转换为 doublereturn 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) {  // 按值传递,拷贝了 namestd::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;  // 输出:10return 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();  // 按值获取 namestd::cout << "名字:" << name << std::endl;  // 输出:名字:Alicereturn 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;  // 输出:名字:Bobreturn 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;
}

总结反思

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/58667.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数组移除元素

目录 题目方法一&#xff1a;直接(暴力)求解法思路分析代码如下 方法二&#xff1a;双指针&#xff08;快慢指针&#xff09;思路分析代码如下 题目 这道题是来自于leetcode的一道算法题&#xff1a; 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于…

【Flask】二、Flask 路由机制

目录 什么是路由&#xff1f; Flask中的路由 基本路由 动态路由 路由中的HTTP方法 路由函数返回 在Web开发中&#xff0c;路由是将URL映射到相应的处理函数的过程。Flask是一个轻量级的Web应用框架&#xff0c;提供了简单而强大的路由机制&#xff0c;使得开发者能够轻松…

【IC验证_systemverilog】信号类型

IC验证_systemverilog 1.信号声明2.变量类型3.数据类型4.符号 1.信号声明 语法&#xff1a; 变量类型 信号类型 符号转换 位宽 信号名 深度&#xff1b;2.变量类型 &#xff08;1&#xff09;说明&#xff1a; systemverilog中的信号类型主要分为线网类型&#xff08;wire&a…

《模拟电子技术基础》第六版PDF课后题答案详解

《模拟电子技术基础》第六版是在获首届全国优秀教材建设奖一等奖的第五版的基础上&#xff0c;总结6年来的教学实践经验修订而成的新形态教材。为满足国家人才培养的需求&#xff0c;适应新型教学模式&#xff0c;并考虑到大多数院校逐渐减少课程学时的现状&#xff0c;在不降低…

DOM---鼠标事件类型(移入移出)

移入 移出mouseenter mouseleave 在父盒子上触发&#xff0c;在孩子盒子上不触发 移入 移出mouseover mouseout全触发 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Comp…

网络编程 UDP编程 Linux环境 C语言实现

UDP编程 1. 一般UDP编程 UDP传输特点&#xff1a;非面向连接、不可靠的、无序的 报式传输 支持组播和广播 UDP应用数据最大长度建议&#xff1a;MTU(以太网分组数据的最大长度)1500 - 20(IP头) - 8(UDP头) 1472Bytes 客户端&#xff1a;支持两种形式的代码编写: 1. 不定向…

在manjaro 2024里使用yay命令安装ROS2

不建议这么安装&#xff0c;研究了两天以失败告终。要不就手动编译吧。。。&#xff08;在系统环境良好的情况下&#xff0c;最好是刚装完系统就装ROS&#xff09;真的太多不适配了&#xff0c;旧有的很多yay包都会遇到一些奇怪的问题&#xff1a; 0.一开始就会遇到网络卡住的…

人工智能原理实验一:知识的表示与推理实验

一、实验目的 本实验课程是计算机、智能、物联网等专业学生的一门专业课程&#xff0c;通过实验&#xff0c;帮助学生更好地掌握人工智能相关概念、技术、原理、应用等&#xff1b;通过实验提高学生编写实验报告、总结实验结果的能力&#xff1b;使学生对智能程序、智能算法等有…

自研小程序-心情追忆

在近期从繁忙的工作中暂时抽身之后&#xff0c;我决定利用这段宝贵的时间来保持我的Java技能不致生疏&#xff0c;并通过一个个人项目来探索人工智能的魅力。 我在Hugging Face&#xff08;国内镜像站点&#xff1a;HF-Mirror&#xff09;上发现了一个关于情感分析的练习项目&…

贪心算法入门(一)

1.什么是贪心算法&#xff1f; 贪心算法是一种解决问题的策略&#xff0c;它将复杂的问题分解为若干个步骤&#xff0c;并在每一步都选择当前最优的解决方案&#xff0c;最终希望能得到全局最优解。这种策略的核心在于“最优”二字&#xff0c;意味着我们追求的是以最少的时间和…

FreeSWITCH 简单图形化界面30 - 使用MYODBC时可能遇到的错误

FreeSWITCH 简单图形化界面30 - 使用MYODBC时可能遇到的错误 测试环境1、 MYODBC 3.51.18 or higher2、分析和解决2.1 解决1&#xff0c;降级MySQL ODBC2.2 解决2&#xff0c;修改FreeSWITCH代码 测试环境 http://myfs.f3322.net:8020/ 用户名&#xff1a;admin&#xff0c;密…

四、k8s快速入门之Kubernetes资源清单

kubernetes中的资源 ⭐️ k8s中所有的内容都抽象为资源&#xff0c;资源实列化之后&#xff0c;叫做对象 1️⃣名称空间级别 ⭐️ kubeadm在执行k8s的pod的时候会在kube-system这个名称空间下执行&#xff0c;所以说当你kubectl get pod 的时候是查看不到的查看的是默认的po…

Java面试经典 150 题.P88. 合并两个有序数组(001)

本题来自&#xff1a;力扣-面试经典 150 题 面试经典 150 题 - 学习计划 - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台https://leetcode.cn/studyplan/top-interview-150/ 题解 class Solution {public void merge(int[] nums1, int m, int[] nums2, …

[LeetCode-55]基于贪心算法的跳跃游戏的求解(C语言版)

/* 题目出处&#xff1a;LeetCode 题目序号&#xff1a;55. 跳跃游戏 题目叙述&#xff1a;给你一个非负整数数组 nums &#xff0c;你最初位于数组的第一个下标位置 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标&#xff0c;如果可…

FPGA时序分析和约束学习笔记(3、Timequest时序路径详解和优化)

FPGA时序分析和约束学习笔记&#xff08;3、Timequest时序路径详解和优化&#xff09; Timequest中Data Path分析 Data Arrival Path clock path&#xff1a;时钟信号到达源寄存器时钟端口的时间 data path&#xff1a;数据从源寄存器Q端口出发到达目标寄存器D端口的时间 D…

四、鸿蒙开发-常用布局(线性布局、层叠布局、弹性布局、网格布局、列表布局)

提示&#xff1a;本文根据b站尚硅谷2024最新鸿蒙开发HarmonyOS4.0鸿蒙NEXT星河版零基础教程课整理 链接指引 > 尚硅谷2024最新鸿蒙开发HarmonyOS4.0鸿蒙NEXT星河版零基础教程 文章目录 一、布局基础1.1 盒子模型 二、线性布局&#xff08;Column/Row&#xff09;2.1 概述2.2…

单个相机矫正畸变

1、通过标定助手获取到内参外参&#xff0c;外参在此无效&#xff0c;只用到了内参 2、然后通过halcon算子进行矫正 参考&#xff1a;超人视觉

unity 中使用zeroMq和Mqtt 进行通讯

最近我在做一个车上的HMI项目&#xff0c;也就是车机应用&#xff0c;需要与云端和域控进行通信。HMI的功能已经外包了&#xff0c;但消息的统一层留给我自己来做。因为项目组其他人都没有经验&#xff0c;所以这个任务就落到了我头上&#xff0c;尽管我自己也没有太多经验&…

掌握ElasticSearch(六):分析过程

文章目录 一、什么是分析1. 字符过滤 (Character Filtering)2. 分词 (Breaking into Tokens)3. 词条过滤 (Token Filtering)4. 词条索引 (Token Indexing) 二、内置分析器分类1. 标准分析器 (Standard Analyzer)2. 简单分析器 (Simple Analyzer)3. 语言分析器 (Language Analyz…

LabVIEW Modbus通讯稳定性提升

在LabVIEW开发Modbus通讯程序时&#xff0c;通讯不稳定是一个常见问题&#xff0c;可能导致数据丢失、延迟或错误。为了确保通讯的可靠性&#xff0c;可以从多个角度进行优化&#xff0c;以下是一些有效的解决方案&#xff0c;结合实际案例进行分析。 1. 优化通讯参数设置 通讯…