一、拷贝构造函数
1.1 拷贝构造函数的概念
在现实生活中我们对于两个一模一样的人我们将他们称之为双胞胎,那么我们在创建对象的时候,能不能创建一个和已经存在的对象一模一样的新对象呢?这种做法是可以的,通过拷贝构造函数我们就能完成这样的操作。
拷贝构造函数:只有单个形参,该形参是对本类型对象的引用(一般使用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
注意:1.使用拷贝构造函数进行初始化时,要用同类型的对象进行拷贝初始化。
2.自定义类型的传值传参要调用拷贝构造。
1.2 拷贝构造函数的特征
拷贝构造函数也是特殊的成员函数,它的特征如下所示:
1.拷贝构造函数是构造函数的一个重载形式
2.函数名与类名相同
3.参数只有一个并且必须是同类型对象的引用,采用传值方式编译器会直接报错,因为会无限引发无穷递归调用
我们来看下面的拷贝构造实例:
class Date
{
public:
//全缺省带参构造
Date(int year = 2004, int month = 11, int day = 30)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
//两种调用拷贝构造的方式
//传参
Date d2(d1);
d2.Print();
//使用赋值符号
Date d3 = d1;
d3.Print();
return 0;
}
我们通过结果可以看出我们写的拷贝构造是可行的,对于拷贝构造的调用方法有两种,一种是在创建的对象后面加括号,另一中是直接用赋值符号,将已存在的对象“赋值”给创建的新对象。
4. 若为显式定义,编译器会生成默认的拷贝构造函数,默认的拷贝构造函数对对象按内存存储按字节序完成拷贝,这种拷贝的方法被称为浅拷贝,或者值拷贝。
我们来看一下编译器的默认拷贝构造函数是怎么样的:
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(const Time& t) " << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
//内置类型
int _year = 2004;
int _month = 12;
int _day = 1;
//自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
从这里我们看到这个系统默认的拷贝构造函数对内置类型进行浅拷贝,对自定义类型去调用它的拷贝构造函数。
注意:在编译器生成的默认拷贝构造中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造完成拷贝的。
5.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,对于日期类这样的类我们就没有必要再去显式实现拷贝构造了,但是如果是下面这样的类呢?
typedef int DataType;
class Stack
{
public:
//全缺省的构造函数
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (_array == nullptr)
{
perror("malloc申请空间失败!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack" << endl;
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
void Print()
{
cout << _capacity << endl;
cout << _size << endl;
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
Stack s2(s1);
return 0;
}
我们来运行这段代码验证一下:
可以看到程序直接崩溃掉了。
其实崩溃的原因就是:
1.s1对象调用构造函数创建,在构造函数中默认申请了10个元素的空间,然后里面存了三个元素1, 2, 3,
2.s2对象使用了s1拷贝构造,而Stack类没有显式定义拷贝构造函数,则按这着值拷贝进行拷贝,将s1中的内容原封不动的拷贝到s2中,因此s1和s2指向了同一块内存空间
3.程序退出时,s2 和 s1 都要销毁,s2先销毁,s2销毁时调用析构函数,已经将空间释放,到s1销毁时,会将这块空间再次释放一次,一块内存空间被多次释放,必然会造成程序崩溃。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一但涉及到资源申请的情况时就必须得写拷贝构造函数,否则就是浅拷贝,就会造成程序的崩溃。
6.拷贝构造函数典型调用场景
1.使用已经存在的对象创建新对象
2.函数参数类型为类类型对象
3.函数返回值类型为类类型对象
我们来看下下面的实例代码段:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
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)
{
//使用d来拷贝构造创建新对象
Date temp(d);
return temp;
}
int main()
{
Date d1(2004, 11, 13);
Test(d1);
return 0;
}
通过这里我们看到这里调用了一次构造函数,和三次的拷贝构造函数,这里是因为:
1.调用构造函数创建d1
2.Test函数是以值传递的方式进行的,因此传参的时候调用拷贝构造函数
3.调用拷贝构造函数创建temp对象
4.函数以值返回的方式进行,返回时使用temp拷贝构造临时对象用来返回
后面调用了四次析构函数,分别是销毁了test函数中的temp,test中的参数d,test函数返回时创建的临时变量,最后是main函数中的d1对象。
因此,为了提高程序的效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景, 能用引用的尽量使用引用。
二、赋值运算符重载
2.1 运算符重载
C++为了增加代码的可读性引入了运算符重载这一概念,运算符重载是具有特殊函数名函数,也具有返回值类型,函数名字以及参数列表,它的返回值类型与参数列表与普通的函数类似。
运算符重载的函数名为:关键字operator后面加上需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:例如operator¥
2.重载 操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置类型int的+,不能改变它原本的含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数是隐藏的this指针
5. .* :: sizeof ?: . 这五个符号不能进行重载
对于类还有一个点需要注意:普通函数的函数名就是其地址,但是类里的成员函数的地址要加上去取地址符。
下面我们来看下一个全局的operator== :
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
void test()
{
Date d1(2008, 11, 12);
Date d2(2009, 11, 13);
cout << (d1 == d2) << endl;
Date d3(2004, 11, 13);
Date d4(2004, 11, 13);
cout << (d3 == d4) << endl;
}
int main()
{
test();
return 0;
}
这是一个全局的重载==,我们将类的成员变量的权限给放开了,因此可以访问类中的成员,但是这样就破坏了封装性,我们以后可以使用友元函数来解决这个问题 ,虽然使用友元也有一些破坏封装,但是比放开权限要好一点。
下面我们在来实现以下在类里面的operator==:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//类内的operator==
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
这样写的话我们的代码封装就没有被破坏,因此推荐这种写法。
2.2 赋值运算符重载
1.赋值运算符重载格式
(1)参数类型:const T&, 传递引用可以提高传参效率
(2)返回值类型:T&,返回引用可以提高返回的效率,有返回值的目的是为了可以支持连续赋值
(3)检测是否是自己给自己赋值
(4)返回*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;
}
//类内的operator==
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._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.赋值运算符只能重载成类的成员函数不能重载成全局函数
赋值运算符如果重载成全局的话,就没有this指针了,需要给两个参数,但是这就不符合赋值的意义了,编译也会失败。
编译失败的原因是:赋值运算符如果不显示实现的话编译器会默认生成一个,此时我们如果再在类外自己实现一个全局的赋值运算符重载的话,就和编译器在类中生成的默认赋值运算符重载冲突了, 因此赋值运算符重载只能是类的成员函数。
3.用户没有显式实现,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用其对应类的赋值运算符重载完成赋值。
下面就是调用默认赋值重载的实例:
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
//内置类型
int _year;
int _month;
int _day;
//自定义类型
Time _t;
};
int main()
{
Date d1(2000, 11, 12);
Date d2(2000, 11, 13);
d1 = d2;
return 0;
}
这里我们可以看到对于默认生成的赋值重载函数对于内置类型进行了值拷贝,对于自定义类型则是调用了自定义类型自己的赋值重载函数。
那么既然编译器默认生成的赋值运算重载,就已经可以完成字节序的值拷贝了,那么我们还需要自己去实现赋值重载函数吗?
对于Date类这种是没有必要的但是如果类中涉及内存资源管理,就必须要我们自己实现赋值运算重载,否则程序可能会崩溃,如果没有涉及资源管理的话,是否实现都可以。
2.3 前置++和后置++重载
我们先将前置++,后后置++的代码实现出来然后再来说明它们之间的区别:
class Date
{
public:
Date(int year = 2001, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//前置++,返回+1之后的结果
//注意这里的this指向的对象在函数结束后不会被销毁,因此用引用来提高效率
Date& operator++()
{
_day += 1;
return *this;
}
//由于前置++和后置++都是一元运算符,为了让它们能够正确重载
//c++规定:后置++重载时多增加一个int类型的参数,调用函数时该参数不用传递,编译器自动传递
//后置++是先使用后++,返回临时对象,使用传值返回的方式
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
//内置类型
int _year;
int _month;
int _day;
//自定义类型
Time _t;
};
int main()
{
Date d;
Date d1(2022, 11, 13);
d = d1++;
d = ++d1;
return 0;
}
前置++,返回+1之后的结果:
注意这里的this指向的对象在函数结束后不会被销毁,因此用引用来提高效率,由于前置++和后置++都是一元运算符,为了让它们能够正确重载
后置++,先使用再++:
c++规定:后置++重载时多增加一个int类型的参数,调用函数时该参数不用传递,编译器自动传递
后置++是先使用后++,返回临时对象,使用传值返回的方式
结语:
这篇博客到这里就结束了,介绍了C++中的拷贝构造函数以及运算符重载,希望大家能够通过这篇博客获得对C++学习的一点帮助。