目录
1. 尽量以const、inline、constexpr替代预处理器
1. 尽量以const、inline、constexpr替代预处理器
问题分析
C++中使用#define进行预处理定义常量和宏会导致很多潜在的问题,例如无法被调试器识别、容易出现命名冲突等。
- 调试器无法识别分析
#define
在预处理阶段被替换成文本,编译器不会将这些宏或常量作为符号存储在符号表中。- 这会导致在调试时,无法通过调试器追踪或查看这些宏的值和来源,给调试过程带来困难。
- 作用域命名冲突
#define
的宏是全局的,没有作用域限制,这可能导致不同文件中定义的宏产生命名冲突。- 如果两个文件中使用相同名字的宏,它们的值会互相覆盖,导致代码行为出错且难以发现问题
- 类型安全检查
- 宏在替换的时候不会进行类型安全检查,这样就可能导致未知错误的出现
方案一:const替代
- 因为define在编译的时候是没有类型检查等缺点,但是使用const可以克服这些缺点
- const优点总结
- 类型安全:
const
常量有明确的类型,编译器可以检查类型错误。 - 易于调试:
PI
会作为符号表的一部分,可以在调试器中查看。
- 类型安全:
#define PI 3.14159
const double PI = 3.14159;
方案二:使用inline替代宏定义函数
- 因为宏函数是没有类型检查的,所以容易出现错误
- 使用inline的优点
- 类型安全:编译器会进行类型检查。
- 避免副作用:不会重复计算参数表达式。
- 优化性能:
inline
函数在调用时会被内联展开,性能接近于宏。
// 使用宏
#define SQUARE(x) ((x) * (x))
int a = 5;
int result = SQUARE(a++); // 结果可能不是预期的 36
// 使用Inline函数
inline int square(int x) {
return x * x;
}
方法三:使用constexpr替代常量表达式
- constexpr主要用于定义变量和表达式在编译期间就会求值,从而减少运行时候的开销。
- 优点总结
- 编译期计算:提升程序性能。
- 类型安全:如
const
,会进行类型检查。 - 更灵活:
constexpr
函数可以接受参数并在编译期计算。
constexpr double PI = 3.14159;
constexpr int square(int x) {
return x * x;
}
补充contexpr函数的作用
- C++14后,该函数的限制放宽,可以使用循环或者条件语句
constexpr int factorial(int n) {
int result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}
- C++17还增加了支持复杂语句、Lambda表达式以及类内部支持静态成员变量初始化
- C++20增加了动态内存分配、虚函数调用、以及部分STL容器
代码实践
#include <iostream>
// 不推荐的宏常量和宏函数
#define PI 3.14159
#define SQUARE(x) ((x) * (x))
// 推荐的替代方案
constexpr double ConstPI = 3.14159;
inline int square(int x) {
return x * x;
}
int main() {
std::cout << "宏定义的 PI: " << PI << std::endl;
std::cout << "Constexpr 定义的 PI: " << ConstPI << std::endl;
int a = 5;
std::cout << "宏函数 SQUARE(a++): " << SQUARE(a++) << std::endl; // 结果意外
a = 5;
std::cout << "内联函数 square(a++): " << square(a++) << std::endl; // 正确结果
return 0;
}
总结
- 少用预处理器:C++ 提供了更安全的替代方案。
- 提高代码质量:通过类型检查和调试器支持,减少错误的发生
- 性能优化:使用
inline
和constexpr
可以获得与宏类似的性能,但没有宏的缺点。
2. 尽量使用const
使用const原因
- 防止数据被意外修改:
const
修饰的数据不可更改,避免程序逻辑错误。 - 提高代码的可读性:
const
表明某个变量或函数不会修改数据,读代码时更易理解。 - 允许编译器进行更多优化:
const
数据是不可变的,编译器能更高效地优化代码。
const修饰局部变量
- 局部变量使用const修饰,可以避免该变量在后续代码中被意外修改
#include <iostream>
void printMessage(const std::string& message) {
std::cout << "消息内容:" << message << std::endl;
}
int main() {
const int number = 42;
std::cout << "常量 number 的值:" << number << std::endl;
std::string greeting = "你好,大古!";
printMessage(greeting);
// 错误:尝试修改 `number` 会导致编译错误
// number = 43;
return 0;
}
const修饰指针
- 主要注意两件事情,一个是指向常量的指针(此时指向的数据不可以修改),一个是常量指针(指针的指向不可以更改)
- 也可以是指向常量的常量指针,此时指向和数值都不可以更改
#include <iostream>
int main() {
int value = 10;
int anotherValue = 20;
// 1. 指向常量的指针,指向的数据不可修改
const int* p1 = &value;
std::cout << "p1 指向的值:" << *p1 << std::endl;
// *p1 = 11; // 错误:尝试修改常量数据
// 2. 常量指针,本身不可修改,但指向的数据可以修改
int* const p2 = &value;
*p2 = 15; // 正确:修改数据
std::cout << "修改后的值:" << *p2 << std::endl;
// p2 = &anotherValue; // 错误:常量指针不可重新赋值
// 3. 指向常量的常量指针,指针和数据都不可修改
const int* const p3 = &value;
std::cout << "p3 指向的值:" << *p3 << std::endl;
return 0;
}
const修饰成员函数
- 使用const修饰的成员,可以保证该成员函不会修改成员变量
#include <iostream>
class Person {
public:
Person(const std::string& name) : name_(name) {}
// const 成员函数,保证不会修改成员变量
void printName() const {
std::cout << "名字:" << name_ << std::endl;
}
private:
std::string name_;
};
int main() {
Person person("张三");
person.printName();
return 0;
}
总结
- 局部变量使用const,可以避免该局部变量被无意修改
- const修饰的指针,有效控制指针的指向和指向数据
- 将不修改对象状态的成员函数声明为
const
,增强代码的可读性和可维护性
3. 确保对象被正常初始化
C++并不会自动初始化所有的变量和对象,这也就会导致了未初始化的变量会带来很多未定义的行为,也就会导致很多Bug。所以在对象创建的时候要正确初始化。
核心方法总结
- 初始化变量,不要使用未初始化的变量
- 使用构造函数的初始化列表:当类成员中有默认构造函数的时候,初始化列表更为高效
- 成员变量要设定默认值:类内成员的初始化好处多
- 避免两阶段的初始化:优先使用带参数的构造函数,确保对象在创建时就被正确初始化
使用构造函数的初始化列表
使用初始化列表,可以保证类成员在对象创建的时候就初始化,效率高(因为避免了先调用默认构造函数再赋值的开销)
#include <iostream>
class Person {
public:
// 使用构造函数初始化列表初始化成员变量
Person(const std::string& name, int age)
: name_(name), age_(age) {}
void printInfo() const {
std::cout << "名字:" << name_ << ",年龄:" << age_ << std::endl;
}
private:
std::string name_;
int age_;
};
int main() {
Person person("李四", 25);
person.printInfo();
return 0;
}
避免未初始化的变量
- 实验,不初始化变量打印其数值
- 需要在C++11以前才可以测出
#include <iostream>
int main() {
int value; // 未初始化变量
std::cout << "未初始化变量的值:" << value << std::endl; // 结果不可预测
return 0;
}
C++11以后,会直接在类内给成员变量提供默认值
#include <iostream>
class Person {
public:
Person() {} // 默认构造函数
void printInfo() const {
std::cout << "名字:" << name_ << ",年龄:" << age_ << std::endl;
}
private:
std::string name_ = "未知"; // 提供默认值
int age_ = 0; // 提供默认值
};
int main() {
Person person;
person.printInfo();
return 0;
}
不要两段初始化
- 简单来说就是不要创建一个的函数对变量进行初始化,这样容易导致错误
class Person {
public:
Person() {} // 空的默认构造函数
void initialize(const std::string& name, int age) {
name_ = name;
age_ = age;
}
void printInfo() const {
std::cout << "名字:" << name_ << ",年龄:" << age_ << std::endl;
}
private:
std::string name_;
int age_;
};
int main() {
Person person; // 创建对象
// 如果忘记调用 initialize() 会导致错误
// person.initialize("王五", 30);
person.printInfo(); // 使用未初始化对象
return 0;
}
启发
- 未初始化变量是 bug 的来源,所以确保每个变量都被初始化。
- 构造函数初始化列表既高效又简洁,是处理类成员变量初始化的最佳实践。
- 使用类内成员默认值可以让代码更清晰,并减少构造函数的负担。
4. 编译器默认生成的函数
编译器自动生成的成员函数
- 默认构造函数(
ClassName()
):初始化对象时调用。 - 拷贝构造函数(
ClassName(const ClassName&)
):用已有对象创建新对象时调用。- 此处是浅拷贝,可能会导致内存泄漏和双重释放问题
- 拷贝赋值运算符(
ClassName& operator=(const ClassName&)
):将一个对象的值赋给另一个已有对象时调用。 - 析构函数(
~ClassName()
):销毁对象时调用
默认生成函数的证明
#include <iostream>
class Person {
public:
std::string name;
int age;
};
int main() {
// 默认构造函数
Person p1;
// 拷贝构造函数
Person p2 = p1;
// 拷贝赋值运算符
p2 = p1;
std::cout << "默认行为:" << p2.name << "," << p2.age << std::endl;
return 0;
}
控制编译器生成默认函数
- 使用深拷贝可以避免指针共享导致的错误
- 自定义析构函数确保资源可以正确的释放
#include <iostream>
#include <cstring>
class Person {
public:
// 自定义构造函数
Person(const char* name, int age) {
this->name = new char[strlen(name) + 1];
strcpy(this->name, name);
this->age = age;
}
// 自定义拷贝构造函数(深拷贝)
Person(const Person& other) {
name = new char[strlen(other.name) + 1];
strcpy(name, other.name);
age = other.age;
}
// 自定义析构函数,释放资源
~Person() {
delete[] name;
}
void printInfo() const {
std::cout << "名字:" << name << ",年龄:" << age << std::endl;
}
private:
char* name;
int age;
};
int main() {
Person p1("张三", 25);
Person p2 = p1; // 调用拷贝构造函数
p2.printInfo(); // 输出 p2 的信息
return 0;
}
使用C++11中的=delete和=default
class Person {
public:
Person() = delete; // 禁止使用默认构造函数
};
class Person {
public:
Person() = default; // 使用默认构造函数
};
拓展:智能指针简化深拷贝
#include <iostream>
#include <memory> // 包含智能指针头文件
class Person {
public:
// 构造函数
Person(const std::string& name, int age)
: name_(std::make_unique<std::string>(name)), age_(age) {}
// 拷贝构造函数(深拷贝)
Person(const Person& other)
: name_(std::make_unique<std::string>(*other.name_)), age_(other.age_) {
std::cout << "拷贝构造函数被调用" << std::endl;
}
// 移动构造函数
Person(Person&& other) noexcept = default; // 使用默认移动构造函数
// 打印信息
void printInfo() const {
std::cout << "名字:" << *name_ << ",年龄:" << age_ << std::endl;
}
private:
std::unique_ptr<std::string> name_;
int age_;
};
int main() {
Person p1("李四", 30);
Person p2 = p1; // 拷贝构造
Person p3 = std::move(p1); // 移动构造
p2.printInfo();
p3.printInfo();
return 0;
}
5. 如果不需要拷贝对象,禁用拷贝操作
原因分析
当拷贝构造函数和拷贝赋值运算符不需要的时候,有可能会导致意外拷贝,所以当程序代码中不需要拷贝的情况下,禁止编译器自动生成拷贝构造和拷贝赋值。
- 防止意外拷贝:如果类中有动态内容或者系统资源的时候,默认的浅拷贝可能会导致多个对象共享资源,从而带来内存泄漏或者双重释放问题的出现
- 拷贝构造可能会导致相应的性能开销
总结
- 优点使用C++11的=delete禁用不需要的拷贝操作
- 防止资源共享,类中管理资源的时候,考虑禁用拷贝,避免拷贝引发的错误
- 单例模式等场景下一定要禁用拷贝
C++11以后使用-delete禁用
#include <iostream>
#include <memory>
class NonCopyable {
public:
NonCopyable() {
std::cout << "构造函数被调用" << std::endl;
}
// 禁用拷贝构造函数
NonCopyable(const NonCopyable&) = delete;
// 禁用拷贝赋值运算符
NonCopyable& operator=(const NonCopyable&) = delete;
void display() const {
std::cout << "这是一个不可拷贝的对象" << std::endl;
}
};
int main() {
NonCopyable obj1;
obj1.display();
// NonCopyable obj2 = obj1; // 编译错误:拷贝构造被禁用
// NonCopyable obj3;
// obj3 = obj1; // 编译错误:拷贝赋值被禁用
return 0;
}
- 补充,C++98中可以通过将类放入私有域中实现防止拷贝操作,但是不建议使用
典型事例(单例模式)
#include <iostream>
class Singleton {
public:
// 提供全局访问点
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
void display() const {
std::cout << "这是单例对象" << std::endl;
}
// 禁用拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
// 私有构造函数,防止外部创建实例
Singleton() {
std::cout << "单例对象被创建" << std::endl;
}
};
int main() {
Singleton& s1 = Singleton::getInstance();
s1.display();
// Singleton s2 = s1; // 编译错误:拷贝构造被禁用
return 0;
}
6. 为多态基类声明虚析构函数
总结
- 如果一个类是被用作基类,并且有可能通过基类指针或者引用指向派生类对象,那么就必须将基类的析构函数声明为虚函数
- 因为没有虚析构函数会导致派生类的资源泄漏
实验证明内存泄漏的发生
当使用基类指针指向派生类对象的时候,如果基类的析构函数不是虚函数,那么基类指针删除对象的时候只会调用基类的析构函数,而派生类的析构函数不会被调用,这样就会导致资源泄漏。、
#include <iostream>
class Base {
public:
Base() { std::cout << "Base 构造函数" << std::endl; }
~Base() { std::cout << "Base 析构函数" << std::endl; } // 非虚析构函数
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived 构造函数" << std::endl; }
~Derived() { std::cout << "Derived 析构函数" << std::endl; }
};
int main() {
Base* p = new Derived(); // 基类指针指向派生类对象
delete p; // 只会调用 Base 的析构函数
return 0;
}
更改后:将析构的函数声明为虚函数就可以解决该问题
#include <iostream>
class Base {
public:
Base() { std::cout << "Base 构造函数" << std::endl; }
virtual ~Base() { std::cout << "Base 虚析构函数" << std::endl; } // 虚析构函数
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived 构造函数" << std::endl; }
~Derived() { std::cout << "Derived 析构函数" << std::endl; }
};
int main() {
Base* p = new Derived(); // 基类指针指向派生类对象
delete p; // 调用虚析构函数,确保正确析构
return 0;
}
使用虚函数可以保证内存安全原因分析
C++中,如果使用基类指针指向派生类对象的时候,默认行为就是调用基类的析构函数。如果基类的析构函数不是虚函数,那么销毁对象的时候就只会调用基类的析构函数,而派生类的析构函数不会被调用。这样也就导致了派生类资源无法正常释放。
从析构过程分析:如果通过一个指向基类的指针删除对象的时候,编译器会根据指针的类型决定调用哪个析构函数;如果析构函数不是虚函数,那么编译器就会静态绑定,只会调用基类的析构函数,而忽略派生类的析构函数。
如果将基类的析构函数声明为虚函数,就会动态绑定,通过基类指针删除派生类对象的时候,就会先调用派生类的析构函数,然后调用析构函数。
静态绑定就是在编译的时候决定调用的函数,也就是提前就知道这个函数的作用;动态绑定则是根据运行时候的实际类型决定调用的函数。
使用=default和override修饰虚析构函数
- C++11后,就可以用=default让编译器生成默认的虚构函数
class Base {
public:
virtual ~Base() = default; // 使用默认虚析构函数
};
- 派生类中可以使用override明确表明析构函数是重写基类的虚析构函数
class Derived : public Base {
public:
~Derived() override { std::cout << "Derived 析构函数" << std::endl; }
};
7. 不让异常逃离析构函数
析构函数中抛出异常的时候没有被捕捉到,此时有可能导致程序异常停止。如果在处理一个异常的时候,析构函数此时又抛出一个异常,此时就会导致程序崩溃。
析构函数抛出异常导致程序崩溃分析
如果一个异常出现,程序此时会依次调用栈中的析构函数来清理资源。如果析构函数在清理资源的时候,又出现了一个异常,这样就导致了两个异常,这明显了违反C++的规则,所以最后会导致程序的崩溃。
测试程序
使用runtime_error函数在main中抛出一个异常,同时创建一个临时类对象,模仿一个双异常的场景。也就是说main中抛出异常后,Test的析构函数会在栈中展开,在析构函数中设置了第二个异常,最终也就导致了两个异常的产生,C++此时是无法处理的,所以可能会导致程序崩溃。
#include <iostream>
#include <stdexcept>
class Test {
public:
~Test() {
std::cout << "析构函数被调用" << std::endl;
throw std::runtime_error("析构函数中的异常"); //第二个异常
}
};
int main() {
try {
Test t; // 临时对象,离开作用域时调用析构函数
throw std::runtime_error("主逻辑中的异常");//第一个异常
}
catch (const std::exception& e) {
std::cout << "捕获到异常:" << e.what() << std::endl;
}
return 0;
}
解决方法:在析构函数捕获异常并对错误进行修正
析构函数中及时捕捉异常,也可以使用日志替代处理异常
#include <iostream>
#include <stdexcept>
class Test {
public:
~Test() {
try {
std::cout << "析构函数被调用" << std::endl;
throw std::runtime_error("析构函数中的异常");
}
catch (const std::exception& e) {
std::cout << "捕获到析构函数中的异常:" << e.what() << std::endl;
// 可以在此处记录日志,而非向外抛出
}
}
};
int main() {
try {
Test t;
throw std::runtime_error("主逻辑中的异常");
}
catch (const std::exception& e) {
std::cout << "捕获到异常:" << e.what() << std::endl;
}
return 0;
}
使用智能指针可以确保在异常情况下资源也可以被正确的释放
#include <iostream>
#include <memory>
class Resource {
public:
Resource() { std::cout << "资源被分配" << std::endl; }
~Resource() { std::cout << "资源被释放" << std::endl; }
};
int main() {
try {
std::unique_ptr<Resource> res = std::make_unique<Resource>();
throw std::runtime_error("主逻辑中的异常");
}
catch (const std::exception& e) {
std::cout << "捕获到异常:" << e.what() << std::endl;
}
return 0;
}
反思
- 设计析构函数的时候,需要考虑到双重异常问题
- 智能指针可以简化资源管理,减少异常处理的复杂性
8. 不在构造和析构过程中调用虚函数
构造函数与析构函数中调用虚函数不安全的原因
首先每个对象都有一个虚表指针指向该类的虚表。构造函数和析构函数执行的时候,虚表指针会指向当前正在构造或者析构的类的虚表。
其次,关键问题在于,构造或者析构的时候,虚表还没有更新完整。
- 构造期间:基类构造函数执行的时候,虚表指针只会指向基类的虚表,因此,即使此时调用虚函数,也只会调用到基类版本的虚函数
- 析构期间:基类在析构的时候,也同样只会指向基类的虚表,派生类版本的虚函数也不会被使用
构造函数中调用虚函数事例分析
- Base构造函数执行的时候,虚函数指针只会指向Base的虚表,不会更新Derived虚表,从下面的调试可以看到
class Base {
public:
Base() {
std::cout << "Base 构造函数" << std::endl;
print(); // 调用虚函数
}
virtual void print() const {
std::cout << "Base 的 print 函数" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived 构造函数" << std::endl;
}
void print() const override {
std::cout << "Derived 的 print 函数" << std::endl;
}
};
int main() {
Derived d;
return 0;
}
析构函数中调用虚函数
同上,执行Base析构函数的时候,虚表指针也是指向Base
#include <iostream>
class Base {
public:
virtual ~Base() {
std::cout << "Base 析构函数" << std::endl;
print(); // 调用虚函数
}
virtual void print() const {
std::cout << "Base 的 print 函数" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived 析构函数" << std::endl;
}
void print() const override {
std::cout << "Derived 的 print 函数" << std::endl;
}
};
int main() {
Base* p = new Derived();
delete p; // 调用析构函数
return 0;
}
解决方法总结
首先不要在构造和析构函数中使用虚函数。可以将需要调用的函数放在专门的初始化函数或销毁函数中,然后外部代码进行调用。
也就是基类中设计一个纯虚函数,并且设置一个对该纯虚函数使用的函数
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base 构造函数" << std::endl;
}
virtual ~Base() {
std::cout << "Base 析构函数" << std::endl;
}
virtual void print() const = 0; // 纯虚函数
void init() { print(); } // 初始化时调用虚函数
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived 构造函数" << std::endl;
}
~Derived() override {
std::cout << "Derived 析构函数" << std::endl;
}
void print() const override {
std::cout << "Derived 的 print 函数" << std::endl;
}
};
int main() {
Derived d;
d.init(); // 在构造完成后调用虚函数
return 0;
}
9. 令operator=返回一个引用指向当前对象
operator=运算符用于将一个对象赋给另一个对象,为了符合其行为,赋值运算符应该返回一个指向当前对象的引用(*this)
分析返回*this的原因
首先C++支持a=b=c这样的链式赋值语句,如果想要实现链式赋值,那么operator=就必须要返回一个指向当前对象的引用
其次部分内置运算符返回的是左值引用,为了符合其类型,自定义的类型赋值运算符就要返回*this
#include <iostream>
class Test {
public:
Test(int value = 0) : value_(value) {} // 构造函数
// 拷贝赋值运算符
Test& operator=(const Test& other) {
if (this == &other) return *this; // 自赋值检测
value_ = other.value_; // 执行赋值操作
std::cout << "赋值运算符被调用" << std::endl;
return *this; // 返回当前对象的引用
}
void print() const {
std::cout << "值:" << value_ << std::endl;
}
private:
int value_;
};
int main() {
Test a(5), b(10), c;
c = b = a; // 链式赋值
c.print(); // 输出:值:5
return 0;
}
处理自赋值
如果要实现operator=的时候,需要检查a=a,否则就可能导致资源被重复释放或者没有定义
Test& operator=(const Test& other) {
if (this == &other) return *this; // 自赋值检测
value_ = other.value_;
return *this;
}
动态资源拷贝赋值的时候需要考虑内存释放问题
赋值运算符中应该使用深拷贝,避免多个对象共享同一块内存;在赋值之前要先释放旧资源,防止内存泄漏。
#include <iostream>
#include <cstring>
class String {
public:
String(const char* str = "") {
data_ = new char[strlen(str) + 1];
strcpy(data_, str);
}
// 拷贝赋值运算符
String& operator=(const String& 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;
}
~String() {
delete[] data_;
}
void print() const {
std::cout << "字符串:" << data_ << std::endl;
}
private:
char* data_;
};
int main() {
String s1("11111");
String s2("22222");
String s3;
s3 = s2 = s1; // 链式赋值
s3.print();
return 0;
}
总结
- 赋值运算符要确保支持链式赋值和自赋值检测
- 动态资源管理的时候,要小心处理赋值运算符,避免内存泄漏和资源重复释放
10 赋值运算符中处理“自我赋值”
问题分析
自我赋值就是一个对象给自己赋值,如果没有恰当处理好自我赋值,那么可能会导致资源泄漏、野指针、逻辑错误等情况。
例如自我赋值的时候导致了内存区域被释放,最终导致程序崩溃。下述代码就是自我赋值后,字符指针成为了野指针
class String {
public:
String(const char* str = "") {
data_ = new char[strlen(str) + 1];
strcpy(data_, str);
}
// 错误:未处理自我赋值
String& operator=(const String& other) {
delete[] data_; // 释放旧资源
data_ = new char[strlen(other.data_) + 1];
strcpy(data_, other.data_);
std::cout << "赋值运算符被调用" << std::endl;
return *this;
}
~String() {
delete[] data_;
}
void print() const {
std::cout << "字符串:" << data_ << std::endl;
}
private:
char* data_;
};
int main() {
String s("你好");
s = s; // 自我赋值
s.print();
return 0;
}
解决方法:自我赋值的时候,先进行一次自我赋值判断
String& operator=(const String& 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;
}
使用临时对象实现异常安全的赋值
也就是创建一个临时对象保存新数据,然后与当前对象进行交换,从而避免程序异常的出现
#include <iostream>
#include <cstring>
#include <utility> // std::swap
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& operator=(String other) {
std::swap(data_, other.data_); // 交换数据
std::cout << "赋值运算符被调用" << std::endl;
return *this;
}
~String() {
delete[] data_;
}
void print() const {
std::cout << "字符串:" << data_ << std::endl;
}
private:
char* data_;
};
int main() {
String s1("小米15");
String s2("小米SU7");
s1 = s2; // 赋值操作
s1.print();
s1 = s1; // 自我赋值
s1.print();
return 0;
}
总结
- 始终检测自我赋值:避免资源释放后重新分配,也防止野指针
- 使用临时对象:避免内存泄漏
- 实现拷贝构造、赋值运算符和析构函数的时候,要考虑到移动构造和移动赋值运算符