导读
explicit
将构造函数声明为explicit
,禁止编译器执行非预期(不是我们想要的)的类型转换。
除非我们有一个很好的理由,允许构造函数被用来进行隐式类型转换,否则我们应该把它声明为explicit
的。
拷贝构造函数&&拷贝赋值操作符
拷贝构造函数被用来以同类型的对象初始化自我对象。
拷贝赋值操作符被用来从另一个同类型的对象中,拷贝对方的值到自我对象。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Widget{ public: Widget(); Widget(const Widget & rhs); Widget & operator=(const Widget & rhs); }; Widget w1;//默认构造 Widget w2(w1);//拷贝构造 w1 = w2;//拷贝赋值操作符 Widget w3 = w2;//调用了拷贝构造 |
如上述代码,我们有可能会不确定到底调用了拷贝赋值操作符还是拷贝构造函数。
其实,是很好区分的:
如果一个新对象被定义,那么一定会有一个构造函数被调用。如果没有新对象被定义,那么就不会有构造函数被调用(也就是赋值操作符会被调用)
从上面这段话里面,很明显能看出,确定是不是调用了构造的决定性因素是新对象被定义,我们再回过去看上面的代码,这个时候,应该就能一眼看出只有倒数第二行没有新对象被定义。
C++组成
C
C
部分
Object-Oriented C++
- 面向对象部分(涵盖了构造析构、封装继承、虚函数多态等特性)
Template C++
- 泛型编程部分(包括模板元编程)
STL
- 模板程序库部分(包括容器、迭代器、算法、函数对象等特征)
2 尽量以const,enum,inline代替#define
概述
- 按标题的叙述,可以换句话为:宁以编译器替换预编译器
- 为什么要替换,因为,类似于
#define
这类的东西,是不被视作语言的一部分的。
const常量
- 问题:
- 使用
#define
定义的常量没有类型信息 #define
常量不受作用域限制,可能导致名字冲突和难以调试
- 使用
1 2 3 4 5 |
// #define #define PI 3.14159 // const const double PI = 3.14159; |
1 2 |
#define ASPECT_RATIO 1.56 const double AspectRatio = 1.65; |
-
解析:
ASPECT_RATIO
这个符号也许从未被编译器看见过,因为很有可能在编译器处理源码之前,它就被预处理器移走了- 宏这种东西,我们应该都不陌生,我们把一个东西定义为宏,或许是为了便于我们区分或记忆,更甚于为了代码的可读性
- 但是,它的本质,是符号替换,这个是宏的核心。就如上面所说的,很可能在我们定义的这个宏被预处理器替换为对应的内容时候,都没到编译器出手的时候,也就是编译器很可能都没有见过它
- 如果,在代码某个地方出错了,我们看到的将是
1.56
,而不是ASPECT_RATIO
,那么在这种情况下,这个1.56
就会显得很突兀了,我们很可能一时半会儿定位不到出问题的地方。 - 但是,常量就不一样了,作为一个常量,编译器肯定会把这个常量记住的。
-
常量替换
#define
的情况:- 定义常量指针
- 类专属常量
enum枚举
-
问题:
#define
常量没有类型检查,可能引发难以发现的错误
-
一个属于枚举类型的数值可权充ints被使用。
123456class GamePlaer {private:enum {NumTurns = 1};int scores[NumTurns];...}; -
枚举的行为某方面来讲比较像
#define
,而不是const
- (我们可以对
const
取地址,但不能对#define
的东西和枚举取地址)
- (我们可以对
1 2 3 4 5 6 7 |
// #define #define RED 0 #define GREEN 1 #define BLUE 2 // enum enum Color { RED, GREEN, BLUE }; |
inline函数
-
问题:
- 使用
#define
定义的宏函数仅仅是文本替换,可能导致意想不到的副作用 - 宏函数没有类型检查
- 使用
-
为了避免一些麻烦或为了代码的可读性,以及其他的理由,我们有可能会用
#define
来实现一个看起来像函数的宏。- (一般这样的函数可能都是比较简洁,调用频繁)
- 一个比定义宏更好的方法就是把此类函数内联
12345template<typename T>inline void callWithMax(const T& lhs,const T& rhs){f(a>b?a:b);}
1 2 3 4 5 6 7 |
// #define #define SQUARE(x) ((x) * (x)) // inline inline int square(int x) { return x * x; } |
3 尽可能使用const
概述
- const功能挺强大的,我们可以用它在类的外部修饰全局变量或命名空间作用域内的变量
- 也可以修饰文件、函数、区块作用域中被声明为static的对象
- 也可以修饰类内部的静态或非静态的成员变量
- 对于指针,经常会有const出现在星号左边或右边的情况:
- const星左,底被指
(const 在*左边,被指物是常量,并且这个const是一个底层const) - const星右,顶指针
(const 在*右边,指针本身是个常量指针,并且这个const是一个顶层const) - 星号两边都有const,被指物是常量,执行被指物的指针也是常量
- const星左,底被指
1 2 3 |
//这两种写法是等价的,把const写在类型之后星号之前,这个是个人习惯 void f1(const Widget * pw); void f2(Widget const * pw); |
const指针
- 问题:
- 指针和指针所指向的数据在代码中经常会被修改,使用
const
可以限制这种修改
- 指针和指针所指向的数据在代码中经常会被修改,使用
- 指向常量数据的指针:
1 |
const char* p = "Hello"; // p指向的字符串不能被修改 |
- 常量指针:
1 2 |
int x = 10; int* const p = &x; // p是一个常量指针,不能指向别的地址 |
- 指向常量数据的常量指针:
1 |
const int* const p = &x; // p指向的内容和p本身都不能修改 |
const 成员函数
- 问题:
- 类的成员函数如果不修改对象的状态,应该声明为
const
,以便清楚地表明该函数不会修改对象
- 类的成员函数如果不修改对象的状态,应该声明为
1 2 3 4 5 6 |
class MyClass { public: int getValue() const { return value; } private: int value; }; |
const函数参数和返回类型
- 函数参数如果在函数体内不被修改,应该声明为
const
,以提高代码的可读性和安全性
1 2 3 |
void print(const std::string& str) { std::cout << str << std::endl; } |
- 如果函数返回一个指针或引用,且不希望返回值被修改,也应使用
const
:
1 |
const std::string& getName() const { return name; } |
const迭代器和容器
- 在使用标准库容器和迭代器时,使用
const
可以防止无意间修改容器内容
1 2 3 4 |
std::vector<int> vec = {1, 2, 3, 4}; for (std::vector<int>::const_iterator it = vec.cbegin(); it != vec.cend(); ++it) { std::cout << *it << std::endl; // 迭代器it指向的内容不能修改 } |
4 确保对象在使用之前已经初始化
概述
- 未初始化的对象可能包含垃圾值,导致未定义行为
内置类型的初始化
- 内置类型如
int
、double
、char
等在定义时不会自动初始化,必须显式地进行初始化
1 2 |
int x = 0; // 明确初始化 double y = 0.0; // 明确初始化 |
类对象的初始化
- 在
C++
中,类对象可以通过构造函数进行初始化
1 2 3 4 5 6 7 |
class MyClass { public: MyClass(int a, int b) : x(a), y(b) {} // 使用初始化列表 private: int x; int y; }; |
- 提供默认构造函数以确保对象可以在不带参数时也能被正确初始化:
1 2 3 4 5 6 7 |
class MyClass { public: MyClass() : x(0), y(0) {} // 默认构造函数 private: int x; int y; }; |
使用成员初始化列表
- 使用成员初始化列表初始化类成员,而不是在构造函数体内赋值
- 这不仅能提高效率,还能确保常量成员和引用成员被正确初始化
1 2 3 4 5 6 7 8 |
class MyClass { public: MyClass(int a, int b) : x(a), y(b), z(a + b) {} // 使用初始化列表 private: int x; int y; const int z; }; |
避免局部变量未初始化
- 局部变量在声明时不自动初始化,需要显式赋初值
1 2 3 4 |
void func() { int x = 0; // 显式初始化 // 使用x } |
使用智能指针
- 避免使用未初始化的指针
- 可以使用智能指针(如
std::unique_ptr
或std::shared_ptr
)来确保指针在使用前已经初始化
- 可以使用智能指针(如
1 2 3 |
#include <memory> std::unique_ptr<int> ptr = std::make_unique<int>(10); // 使用智能指针 |
5 了解C++编译器自动生成和调用的函数
概述
- 在C++中编译器会自动生成一些特殊成员函数,程序员应该了解这些函数,以便更好地控制对象的行为和避免潜在的问题
默认构造函数
- 如果没有定义任何构造函数,编译器会自动生成一个默认构造函数
- 用于创建对象,但不进行任何初始化
1 2 3 4 |
class MyClass { public: MyClass() = default; // 显式声明默认构造函数 }; |
拷贝构造函数
- 当需要复制一个对象时,编译器会自动生成拷贝构造函数
- 逐位复制对象中的非静态成员
1 2 3 4 |
class MyClass { public: MyClass(const MyClass& other) = default; // 显式声明拷贝构造函数 }; |
拷贝赋值运算符
- 当使用赋值运算符将一个对象的值赋给另一个对象时,编译器会自动生成拷贝赋值运算符
- 逐位复制对象中的非静态成员
1 2 3 4 |
class MyClass { public: MyClass& operator=(const MyClass& other) = default; // 显式声明拷贝赋值运算符 }; |
析构函数
- 当对象的生命周期结束时,编译器会自动调用析构函数
- 释放对象占用的资源
1 2 3 4 |
class MyClass { public: ~MyClass() = default; // 显式声明析构函数 }; |
移动构造函数和移动赋值运算符
- 当需要移动一个对象时,编译器会自动生成移动构造函数和移动赋值运算符
- (如果没有显式定义拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数)
- 通过移动对象的资源而不是复制,从而提高性能
1 2 3 4 5 |
class MyClass { public: MyClass(MyClass&& other) = default; // 显式声明移动构造函数 MyClass& operator=(MyClass&& other) = default; // 显式声明移动赋值运算符 }; |
6 显示禁止编译器生成你不想要的函数
概述
- 为什么需要显式禁止默认函数?
- 控制对象行为:
- 某些默认生成的函数(如拷贝构造函数、拷贝赋值运算符)可能不适用于你的类,使用这些函数会导致未定义行为或资源泄漏
- 提高代码安全性:
- 显式禁止这些函数可以防止不小心使用它们,从而避免潜在的错误
- 意图明确:
- 通过显式声明,可以清楚地表达程序员的意图,使代码更易读、易维护
禁止编译器生成函数的方法
- 使用
= delete
- 用来显式删除编译器生成的函数。这样在使用这些函数时,编译器会产生错误
1 2 3 4 5 6 7 |
class MyClass { public: MyClass() = default; // 默认构造函数 MyClass(const MyClass&) = delete; // 禁止拷贝构造函数 MyClass& operator=(const MyClass&) = delete; // 禁止拷贝赋值运算符 ~MyClass() = default; // 默认析构函数 }; |
- 将函数声明为私有(适用于
C++11
之前的版本)- 在
C++11
之前,可以通过将不需要的函数声明为私有并且不提供定义来禁止它们的使用
- 在
1 2 3 4 5 6 7 8 9 |
class MyClass { public: MyClass() = default; // 默认构造函数 ~MyClass() = default; // 默认析构函数 private: MyClass(const MyClass&); // 私有拷贝构造函数,不提供定义 MyClass& operator=(const MyClass&); // 私有拷贝赋值运算符,不提供定义 }; |
适用场景
- 禁止拷贝:
- 如果类管理资源(如文件句柄、网络连接、动态内存),拷贝可能会导致资源冲突或双重释放
- 禁止赋值:
- 类似地,如果赋值操作不安全或无意义,应该禁止
- 单例模式:
- 单例类通常禁止拷贝和赋值,以确保类的唯一实例
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Singleton { public: static Singleton& getInstance() { static Singleton instance; return instance; } Singleton(const Singleton&) = delete; // 禁止拷贝构造函数 Singleton& operator=(const Singleton&) = delete; // 禁止拷贝赋值运算符 private: Singleton() {} // 私有构造函数 ~Singleton() {} }; |
7 在多态基类中声明虚析构函数
概述
- 虚析构函数确保在删除指向派生类对象的基类指针时,能够正确调用派生类的析构函数,从而避免资源泄漏和未定义行为
为什么需要虚析构函数
- 在
C++
中,如果你删除一个指向派生类对象的基类指针,而基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数 - 这会导致派生类的资源没有被正确释放,进而引发资源泄漏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Base { public: Base() { std::cout << "Base constructor" << std::endl; } ~Base() { std::cout << "Base destructor" << std::endl; } }; class Derived : public Base { public: Derived() { std::cout << "Derived constructor" << std::endl; } ~Derived() { std::cout << "Derived destructor" << std::endl; } }; void example() { Base* b = new Derived(); delete b; // 只会调用Base的析构函数,不会调用Derived的析构函数 } |
1 2 3 |
Base constructor Derived constructor Base destructor |
使用虚析构函数
- 为了确保正确调用派生类的析构函数,应该在基类中将析构函数声明为虚函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Base { public: Base() { std::cout << "Base constructor" << std::endl; } virtual ~Base() { // 声明虚析构函数 std::cout << "Base destructor" << std::endl; } }; class Derived : public Base { public: Derived() { std::cout << "Derived constructor" << std::endl; } ~Derived() { std::cout << "Derived destructor" << std::endl; } }; void example() { Base* b = new Derived(); delete b; // 现在会调用Derived的析构函数 } |
1 2 3 4 |
Base constructor Derived constructor Derived destructor Base destructor |
建议
- 总是为多态基类添加虚析构函数:
1 2 3 4 |
class Base { public: virtual ~Base() {} }; |
- 非多态基类不需要虚析构函数:
- 如果类不会被继承,或者不会通过基类指针删除对象,则不需要虚析构函数
8 防止异常逃离析构函数
概述
- 如果异常在析构函数中被抛出,可能会导致程序崩溃或资源泄漏
- 因此,需要确保异常在析构函数内部被捕获和处理
为什么不应在析构函数中抛出异常
- 程序终止:
- 如果在析构函数中抛出异常,且该异常未被捕获,程序将会调用
std::terminate
并终止运行
- 如果在析构函数中抛出异常,且该异常未被捕获,程序将会调用
- 双重异常问题:
- 如果在栈展开过程中有另一个异常抛出(例如,在处理一个异常的过程中遇到另一个异常),程序也会终止
示例
- 以下代码,其中析构函数可能抛出异常:
- 在
example
函数中,构造一个Base
对象并随后抛出一个异常- 在栈展开过程中,当对象
b
的析构函数被调用时,又抛出了另一个异常,这将导致std::terminate
被调用,程序异常终止
- 在栈展开过程中,当对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Base { public: ~Base() { throw std::runtime_error("Exception in destructor"); } }; void example() { try { Base b; // Do something that may throw throw std::runtime_error("Exception in example"); } catch (const std::exception& e) { std::cout << "Caught exception: " << e.what() << std::endl; } } |
- 修改:
1 2 3 4 5 6 7 8 9 10 11 |
class Base { public: ~Base() { try { // 可能抛出异常的代码 } catch (...) { // 处理异常(记录错误、释放资源等) std::cerr << "Exception caught in destructor" << std::endl; } } }; |
建议
- 捕获所有异常:
- 在析构函数中捕获所有可能的异常,确保不让它们传播出去
- 记录错误:
- 在捕获异常后,可以记录错误信息以便于调试和分析
- 释放资源:
- 在处理异常时,确保所有资源(如内存、文件句柄)都被正确释放
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class ResourceHandler { public: ResourceHandler() { // 资源分配 } ~ResourceHandler() { try { // 可能抛出异常的代码 } catch (const std::exception& e) { // 记录错误信息 std::cerr << "Exception in destructor: " << e.what() << std::endl; // 确保资源被释放 } catch (...) { std::cerr << "Unknown exception in destructor" << std::endl; // 确保资源被释放 } } void doSomething() { // 执行一些操作,可能抛出异常 } }; void example() { try { ResourceHandler handler; handler.doSomething(); // 其他可能抛出异常的操作 } catch (const std::exception& e) { std::cerr << "Caught exception: " << e.what() << std::endl; } } |
9 在构造和析构过程中不要调用虚函数
概述
- 在构造和析构过程中,虚函数的动态绑定机制无法正常工作
为什么不应该在构造和析构过程中调用虚函数
- 对象未完全构造:
- 在构造函数中,对象的基类部分可能已经构造,但派生类部分可能尚未构造完全
- 如果在此时调用虚函数,调用的将是基类版本的函数,而不是派生类版本的函数
- 对象未完全析构:
- 在析构函数中,派生类部分已经被析构,但基类部分可能尚未析构完全
- 如果在此时调用虚函数,调用的将是基类版本的函数,而不是派生类版本的函数
- 未定义行为:
- 在构造或析构过程中调用虚函数,会导致不可预测的行为和潜在的错误
错误示例
- 以下代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class Base { public: Base() { init(); // 调用虚函数 } virtual void init() { std::cout << "Base::init" << std::endl; } virtual ~Base() { cleanup(); // 调用虚函数 } virtual void cleanup() { std::cout << "Base::cleanup" << std::endl; } }; class Derived : public Base { public: Derived() {} void init() override { std::cout << "Derived::init" << std::endl; } void cleanup() override { std::cout << "Derived::cleanup" << std::endl; } }; void example() { Derived d; } |
- 首先会调用基类的构造函数,然后再调用派生类的构造函数
- 然而,在
Base
的构造函数中调用init
,输出结果是Base::init
,而不是Derived::init
,因为此时Derived
部分尚未构造
- 然而,在
- 首先调用派生类的析构函数,然后调用基类的析构函数
- 上述示例派生类没有析构函数,编译器生成默认的析构函数,该析构函数实现中会插入调用基类析构函数的代码
第一次在Base
的析构函数中调用cleanup
,输出结果是Derived::cleanup
,因为此时的派生类对象是构造完成的 - 再调用基类析构函数
第二次在Base
的析构函数中调用cleanup
,输出结果是Base::cleanup
,因为Derived
部分已经被析构了
- 上述示例派生类没有析构函数,编译器生成默认的析构函数,该析构函数实现中会插入调用基类析构函数的代码
1 2 3 |
Base::init Derived::cleanup Base::cleanup |
解决方案
- 使用非虚函数:
- 有问题,还是间接地在构造或析构函数中调用了虚函数
- 在构造函数和析构函数中调用非虚函数,这些非虚函数可以调用所需的虚函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class Base { public: Base() { baseInit(); // 调用非虚函数 } void baseInit() { init(); // 调用虚函数 } virtual void init() { std::cout << "Base::init" << std::endl; } virtual ~Base() { baseCleanup(); // 调用非虚函数 } void baseCleanup() { cleanup(); // 调用虚函数 } virtual void cleanup() { std::cout << "Base::cleanup" << std::endl; } }; class Derived : public Base { public: Derived() {} void init() override { std::cout << "Derived::init" << std::endl; } void cleanup() override { std::cout << "Derived::cleanup" << std::endl; } }; |
- 后期初始化:
- 无问题
- 将需要调用的虚函数放在构造函数之外,由外部代码在对象构造完成后显式调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Base { public: Base() {} virtual void init() { std::cout << "Base::init" << std::endl; } virtual ~Base() {} virtual void cleanup() { std::cout << "Base::cleanup" << std::endl; } }; class Derived : public Base { public: Derived() {} void init() override { std::cout << "Derived::init" << std::endl; } void cleanup() override { std::cout << "Derived::cleanup" << std::endl; } }; void example() { Derived d; d.init(); // 显式调用初始化函数 // ... d.cleanup(); // 显式调用清理函数 } |
10 赋值运算符应该返回一个指向自身的引用
概述
- 在
C++
中定义赋值运算符时,应该返回一个对当前对象的引用 - 这样做不仅遵循了
C++
的惯例,还可以支持链式赋值操作
如何实现赋值运算符返回*this
1 2 3 4 5 6 7 8 9 10 |
class MyClass { public: MyClass& operator=(const MyClass& rhs) { if (this != &rhs) { // 检查自我赋值 // 执行赋值操作 // 复制rhs的成员到当前对象 } return *this; // 返回当前对象的引用 } }; |
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
#include <iostream> #include <cstring> class MyString { private: char* data; public: // 构造函数 MyString(const char* str = "") { data = new char[strlen(str) + 1]; strcpy(data, str); } // 拷贝构造函数 MyString(const MyString& other) { data = new char[strlen(other.data) + 1]; strcpy(data, other.data); } // 赋值运算符 MyString& operator=(const MyString& rhs) { if (this != &rhs) { // 检查自我赋值 delete[] data; // 释放旧内存 data = new char[strlen(rhs.data) + 1]; strcpy(data, rhs.data); } return *this; // 返回当前对象的引用 } // 移动赋值运算符 MyString& operator=(MyString&& rhs) noexcept { if (this != &rhs) { delete[] data; // 释放旧内存 data = rhs.data; rhs.data = nullptr; // 防止rhs析构时删除资源 } return *this; } // 析构函数 ~MyString() { delete[] data; } // 打印字符串 void print() const { std::cout << data << std::endl; } }; int main() { MyString str1("Hello"); MyString str2("World"); str1 = str2; // 使用赋值运算符 str1.print(); // 输出 "World" MyString str3("C++"); str1 = str2 = str3; // 链式赋值 str1.print(); // 输出 "C++" str2.print(); // 输出 "C++" return 0; } |
11 在赋值运算符中处理自我赋值
概述
- 自我赋值是指一个对象赋值给自己
- 例如
a = a
。如果不正确处理自我赋值,可能会导致资源管理问题或数据损坏
- 例如
为什么需要处理自我赋值
- 避免资源泄漏:
- 在没有处理自我赋值的情况下,如果先释放了旧资源再进行赋值操作,可能会导致释放了自己正在使用的资源
- 确保数据完整性:
- 如果不正确处理自我赋值,可能会导致数据被部分或完全破坏
解决方法
- 在赋值运算符中,首先需要检查自我赋值,然后再执行赋值操作
1 2 3 |
if (this != &rhs) { // 执行赋值操作 } |
- 在执行赋值操作之前,首先检查
this
指针是否与右值rhs
的地址相同- 如果相同,则是自我赋值,直接返回当前对象的引用即可
总结
- 避免资源泄漏和数据损坏:
- 在赋值运算符中正确处理自我赋值,可以避免资源泄漏和数据损坏
- 检查自我赋值:
- 通过比较
this
指针和右值的地址来检查自我赋值
- 通过比较
- 正确管理资源:
- 在赋值运算符中,首先分配新资源,然后释放旧资源,最后更新数据指针并返回当前对象的引用
示例代码
- 以下是一个包含动态内存分配的类的完整示例,展示如何正确处理自我赋值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
#include <iostream> #include <cstring> class MyString { private: char* data; public: // 构造函数 MyString(const char* str = "") { data = new char[strlen(str) + 1]; strcpy(data, str); } // 拷贝构造函数 MyString(const MyString& other) { data = new char[strlen(other.data) + 1]; strcpy(data, other.data); } // 赋值运算符 MyString& operator=(const MyString& rhs) { if (this != &rhs) { // 检查自我赋值 char* newData = new char[strlen(rhs.data) + 1]; // 分配新内存 strcpy(newData, rhs.data); // 复制数据 delete[] data; // 释放旧内存 data = newData; // 指向新内存 } return *this; // 返回当前对象的引用 } // 移动赋值运算符 MyString& operator=(MyString&& rhs) noexcept { if (this != &rhs) { delete[] data; // 释放旧内存 data = rhs.data; // 移动数据指针 rhs.data = nullptr; // 防止rhs析构时删除资源 } return *this; } // 析构函数 ~MyString() { delete[] data; } // 打印字符串 void print() const { std::cout << data << std::endl; } }; int main() { MyString str1("Hello"); MyString str2("World"); str1 = str2; // 使用赋值运算符 str1.print(); // 输出 "World" str1 = str1; // 自我赋值测试 str1.print(); // 确保输出依然是 "World" MyString str3("C++"); str1 = str2 = str3; // 链式赋值 str1.print(); // 输出 "C++" str2.print(); // 输出 "C++" return 0; } |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Soui二05/18
- ♥ C++_关于对象的具体初始化顺序11/30
- ♥ STL_stack05/19
- ♥ Effective C++_第二篇07/01
- ♥ STL_list05/04
- ♥ C++14_第一篇12/14