🔥个人主页:Forcible Bug Maker
🔥专栏:C++
目录
前言
拷贝构造函数
概念
拷贝构造函数的特性及用法
赋值运算符重载
运算符重载
赋值运算符重载
结语
前言
本篇主要内容:类的6个默认成员函数中的拷贝构造函数和赋值运算符重载
在上篇文章中我们讲到了类的默认成员函数的构造函数和析构函数,这两个默认成员函数在对象的生命周期中起着至关重要的作用。而今天我们要讲的拷贝构造函数和赋值运算符重载,作为类默认成员函数的其中之二,则是在对象间的初始化和拷贝当中起着重要作用。再次强六个默认成员函数的共性,这些函数会在你不提供的情况下由编译器自动生成。接下来开始我们今天的内容。
拷贝构造函数
概念
在创建对象时,你可能需要创建一个与已经存在的对象一模一样的新对象。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数的特性及用法
拷贝构造函数是一种特殊的构造函数,用于创建一个新对象作为现有对象的副本。
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器报错,因为会引发无穷调用。
拿一个Date类来举例:
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// Date(const Date& d) // 正确写法Date(const Date& d) // 错误写法:编译报错,会引发无穷递归{_year = d._year;_month = d._month;_day = d._day;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;Date d2(d1);return 0;
}
在 Date d2(d1); 这句,将调用拷贝构造函数,其中d1通过传引用传参传递给函数中的d,d2以this指针的形式隐式传递。在运行完拷贝构造函数之后,d2创建好就是和d1相同的一个对象了。接下来详细讲解为什么构造中不加应用会导致无穷调用。
void Fun1(Date d)
{cout << "Fun1" << endl;
}int main()
{Date d1(2024, 4, 16);Fun1(d1);return 0;
}
运行以上代码,会发现,在进入函数Fun1之前,会先调用一次拷贝构造,将d1的值赋给d,下面是调试观察,可以看到调试进入Fun1前先进入了拷贝构造。
说明类的传值传递在调用时会先调用类内部的拷贝构造,如果不写拷贝构造中的引用,拷贝构造的传应用传递就会变成类的传值传递,而类的传值传递又需要先调用拷贝构造,最终逻辑形成了一个闭环,导致无穷调用。
3. 若未显式定义,编译器会生成默认的构造函数。默认构造函数对象按内存存储字节完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d1;// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数Date d2(d1);return 0;
}
上述代码中Date的拷贝构造函数我们并没有提供,由编译器默认生成。
注:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造完成拷贝的。
4. 编译器生成的默认拷贝构造函数既然已经可以完成字节序的拷贝,那么是否还有自己实现的必要?像日期类这样的类是没什么必要的,但是当类涉及资源的打开和关闭,开辟和释放时,默认生成的浅拷贝构造就无法解决问题了。
看如下的Stack类:
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}
在Stack类中,我们在创建s2时使用了编译器默认生成的拷贝构造函数,在main函数代码运行的过程中,似乎并没有什么问题,但是当整个程序运行结束时,程序崩溃了。这是因为Stack类涉及到了堆中空间资源的开辟,由于编译器默认生成的拷贝构造是浅拷贝,s1和s2中的_array指针指向相同的堆空间,在程序运行到结尾会调用析构函数,s1和s2对象各调用一次析构时,会导致_array指向的堆空间被释放两次,最终程序崩溃。
注:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请,则拷贝构造函数是一定要写的,否则就是浅拷贝,导致上述问题。
5. 拷贝构造函数典型应用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
代码示例:
class Date
{
public:Date(int year, int minute, int day){cout << "Date(int,int,int):" << this << endl;}Date(const Date& d){cout << "Date(const Date& d):" << this << endl;}~Date(){cout << "~Date():" << this << endl;}
private:int _year;int _month;int _day;
};
Date Test(Date d) // 第二种
{Date temp(d); // 第一种return temp; // 第三种
}
int main()
{Date d1(2022, 1, 13);Test(d1);return 0;
}
为了提高效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能引用尽量使用引用。
6. 拷贝构造函数其实还有一种调用方式
类名 对象名 = 已有对象名;
赋值运算符重载
运算符重载
运算符重载是C++中的一个重要特性,它允许我们为自定义的数据类型(如类)重新定义或重载已有的运算符,以便它们能像内置类型(如int、float等)的运算符那样工作。通过运算符重载,我们可以让自定义类型的对象像内置类型一样进行运算和操作,提高代码的可读性和易用性。
函数名字为:关键字operator后面接重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
简单来说,运算符重载就是给运算符“赋予新的意义”,让它在不同的数据类型上有不同的作用。
重载的基本规则:
- 不能通过连接其他符号来创建新的操作符,如:operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,如,内置类型的+,不能改变含义
- 最为类成员函数重载时,其形参看起来比操作数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 这五个运算符不能重载。
上面代码中重载了一个全局的operator==,但是全局要求成员变量是公有的,这就牵扯到一个问题,如何保证封装性?
解决方式有三:
- 提供可以获取到成员变量的成员函数
- 使用友元(后面将会学习)
- 重载成成员函数
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// bool operator==(Date* this, const Date& d2)// 这里需要注意的是,左操作数是this,指向调用函数的对象bool operator==(const Date& d2){return _year == d2._year&& _month == d2._month&& _day == d2._day;}
private:int _year;int _month;int _day;
};
上面的代码就成功将==操作符重载成了成员函数,在使用时与全局的==并无区别。
赋值运算符重载
经过上面对操作符重载的简单介绍,赋值重载简单说就是字面意思,重载了=这一符号,使其可以用于对象之间的相互赋值。
1. 赋值运算符重载格式
- 参数类型:const T&,传引用提高效率
- 返回值类型:T& 返回引用可以提高返回效率,有返回值的目的是为了支持连续赋值
- 检测是否给自己赋值
- 返回*this:要符合连续赋值的含义
代码示例(完整的赋值运算符重载):
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}Date& operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year;int _month;int _day;
};
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。虽然编译器生成的默认赋值运算符可以完成字节序的值拷贝,但一旦涉及到资源的管理时,编译器生成的依然是不够用的,原因和默认生成的拷贝构造函数类型。
故:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
结语
本篇博客主要讲了拷贝构造函数和赋值运算符重载,它们在类中扮演着至关重要的角色,是对象复制和赋值操作的基础,确保对象在复制和赋值过程中保持正确的状态和行为。如果没有正确地实现这两个函数,可能会导致数据不一致、内存泄漏或其他严重问题。因此,在编写自定义类时,通常需要仔细考虑是否需要显式定义拷贝构造函数和赋值运算符重载,并根据类的具体需求来实现它们。对于某些类(如包含动态分配内存的类),显式定义这两个函数是必不可少的。下篇博客将会讲到最后两个类的默认成员函数,以及操作符重载更多的使用情境。
本篇博客到此结束,感谢大家的支持!♥