目录
前言
上篇我们讲解了前两个默认成员函数,构造函数和析构函数,再看本篇文章之前小编建议大家如果没有看过上一篇的或者还没怎么理解的就再看一遍,因为本篇文章讲的拷贝构造也会有一点点复杂,那我们就话不多说,开始正文
一、拷贝构造函数
1、拷贝构造函数的作用
拷贝构造函数是用一个已经存在的对象初始化另一个对象的,C++规定,自定义类型的拷贝行为都要去调用拷贝构造,所以像刚才说的自定义类型一个对象拷贝另一个对象以及传值传参,传值返回,都要去调用拷贝构造。
2、拷贝构造函数的特点
(1) 拷贝构造函数是构造函数的一个重载,所以也是函数名与类名相同,没有返回值
(2) 拷贝构造函数可以有多个参数,但第一个参数必须是类类型对象的引用,如果使用传值编译器会直接报错,因为会引发无穷递归,后面的参数必须给上缺省值,只要不满足这条规定,那这个函数就不是拷贝构造函数
用日期类来举一个例子
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
//拷贝构造函数
//d2(d1)
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;
Date d2(d1);
Date d3 = d1;
d1.Print();
d2.Print();
d3.Print();
return 0;
}
拷贝构造函数的写法如上,在写的时候一定不要写反,是用d1来初始化d2,那d1传参上去也就是d,把d当中的成员赋值给d2,d2的成员是用this指针访问的,还有就是Date d3 = d1也是拷贝构造,虽然有赋值符号,但它并不是赋值,而是拷贝构造,等我们讲完赋值重载会回来再去区分他们,所以大家一定要看到最后哦!
用指针也是可以的
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
Date(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;
Date d2(&d1);
d1.Print();
d2.Print();
return 0;
}
但是用指针这个函数就不满足了之前说过的规定,那它也就不是一个拷贝构造函数,而是一个普通的构造函数
我们通过打印的方式来看一下是否传值传参之前调用了拷贝构造
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void func(Date d)
{
cout << "void func(Date d)" << endl;
}
int main()
{
Date d;
func(d);
return 0;
}
在这里增加了一个func函数,我们可以很清晰地看到传参之前先去调用了拷贝构造,传值返回也是一样的道理,要去拷贝构造,如果把func函数改一下,改成这样
Date func(Date d)
{
cout << "void func(Date d)" << endl;
return d;
}
那调用情况就是这样
而只要传值了就被编译器直接禁掉
本质上是因为,每次调用拷贝构造之前是传值传参,传值传参是一种拷贝,就得形成一个新的拷贝构造,会形成死递归
3、编译器生成的拷贝构造函数
拷贝构造函数是一个默认成员函数,我们不写编译器会默认生成一个,编译器默认生成的拷贝构造函数对于内置类型会进行值拷贝(浅拷贝),对于自定义类型会去调用它的拷贝构造
像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完 成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但 是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,会出现两个指针指向同一块空间的情况,那就会出现同一块空间析构两次,而且修改其中一个另一个也会改,浅拷贝程序会直接崩溃,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
下面用Stack类举深拷贝的例子
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
_capacity = n;
_top = 0;
}
// st1(st)
Stack(const Stack& st)
{
_a = ((STDataType*)malloc(sizeof(STDataType) * st._capacity));
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._top;
}
void push(STDataType val)
{
_a[_top++] = val;
}
STDataType& Top()
{
return _a[_top - 1];
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
Stack st1(st);
cout << st.Top() << endl; // 4
cout << st1.Top() << endl; // 4
return 0;
}
st1开一块跟st一样大的空间,也就是sizeof(STDataType)*st的容量大小cacacity,,然后把数据拷贝下来,从st._a拷贝到_a中,用memcpy拷贝sizeof(STDataType)*st的有效数据个数top,再让_top和_capacity和st的变成一样的,这里面的st1中的成员变量都是用this指针访问的,只是没有显示写出来,现在就是st和st1指向不同的空间,那析构两次以及改一个会影响另一个的问题也就迎刃而解了。
4、什么情况下可以不自己显示定义拷贝构造函数
来看MyQueue这个类
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
_capacity = n;
_top = 0;
}
Stack(const Stack& st)
{
cout << "Stack(const Stack& st)" << endl;
_a = ((STDataType*)malloc(sizeof(STDataType) * st._capacity));
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._top;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue
{
public:
MyQueue(int capacity = 4)
:_st1(capacity)
,_st2(capacity)
{}
~MyQueue()
{}
private:
Stack _st1;
Stack _st2;
};
int main()
{
MyQueue mq1;
MyQueue mq2(mq1);
return 0;
}
我们没有显示定义MyQueue的拷贝构造,mq2中有两个栈所以就去调用了两次Stack的拷贝构造,所以就打印了两句。
所以大家再来感受下再讲引用的时候提到的,引用传参和传引用返回可以提高效率,就是因为如果传值传参和传值返回需要去调用拷贝构造,如果是像Stack这样的深拷贝的类代价会很大,所以在保证对象出了作用域不销毁的情况下就用引用做返回值,但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。虽然传引用返回可以减少拷贝,但是一定要确保返回对象,在出了作用域后还在,才能用引用返回,否则即使代价再大也要传值返回。
二、赋值运算符重载
1、运算符重载
有五个运算符不能重载:. .* :: ?: sizeof,要特殊急一下,是点,点星,域作用限定符,三目,sizeof
下面用一个日期类的+=实现一个运算符重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
Date& operator+=(int day)
{
_day += day;
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2025, 1, 10);
d += 5; // d.operator+=(5)
d.Print(); // 2025-1-15
return 0;
}
这里我们只是举个运算符重载的例子,没有考虑天和月超出的情况,这里让天加等了5,再返回自己,因为Date出了作用域不销毁,所以可以传引用返回,而我们写d += 5,其实也就是转换成了d.operator+=(5),一次函数调用,也可以显示的写成这种函数调用的形式,不过为了可读性一般不会这么写,我们可以从汇编角度看得更清楚
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
Date& operator+=(int day)
{
_day += day;
return *this;
}
Date& operator++()
{
_day += 1;
return *this;
}
Date operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 1, 10);
Date d2 = ++d1;
Date d3(2025, 1, 10);
Date d4 = d3++;
d1.Print(); //2025-1-11
d2.Print(); //2025-1-11
d3.Print(); //2025-1-11
d4.Print(); //2025-1-10
return 0;
}
上面我们重载了前置++和后置++,前置++是返回++之后的值,所以可以返回引用,后置++是返回++之前的值,所以需要用一个临时变量tmp,这里是一个拷贝构造,*this+=1也是去复用的刚刚发重载的+=,变自己,但返回的是++之前的值也就是tmp,返回局部变量不能用引用返回,要传值返回,d1就是自己++,变成11号,d2是d1++之后的值,也是11号,d3自己++了,是11号,d4返回的是d3++之前的值,也就是还没自增的时候,就是10号,这就要运算符重载的使用方式方法,而具体的实现日期类的功能,我们写一篇文章再来实现,到时候会把功能实现的更全,而且会考虑月和天是异常的情况,这篇文章只是举例说明运算符重载的方法,以及它的本质是函数调用
2、赋值运算符重载
(1)赋值运算符重载的作用
(2)赋值运算符重载的特点
赋值运算符重载是一个运算符重载,规定必须重载为成员函数,如果写成全局的,那默认成员函数没写就会生成一个,就会有两个赋值重载,调用会有歧义,赋值运算重载的参数建议写成const当前类类型引用,否则会传值传参会有拷贝。有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景,且需要判断是否自己给自己赋值,有些场景自己给自己赋值会出现问题。
下面用日期类举一个例子
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
//传引用返回 传值传参
Date& operator=(const Date& d)
{
//用地址判断是否自己给自己赋值
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 1, 10);
Date d2(2025, 1, 11);
d1 = d2;
d1.Print(); // 2025-1-11
d2.Print(); // 2025-1-11
return 0;
}
3、编译器生成的赋值运算符重载
下面用Stack类举深拷贝的例子
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
_capacity = n;
_top = 0;
}
Stack(const Stack& st)
{
cout << "Stack(const Stack& st)" << endl;
_a = ((STDataType*)malloc(sizeof(STDataType) * st._capacity));
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._top;
}
//赋值重载
Stack& operator=(const Stack& st)
{
if (this != &st)
{
free(_a);
_a = ((STDataType*)malloc(sizeof(STDataType) * st._capacity));
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
return *this;
}
void push(STDataType val)
{
_a[_top++] = val;
}
STDataType& Top()
{
return _a[_top - 1];
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack st;
st.push(1);
st.push(2);
st.push(3);
st.push(5);
Stack st1;
st1.push(1);
st1.push(2);
st1.push(3);
st1.push(4);
st1 = st;
cout << st.Top() << endl; //5
cout << st1.Top() << endl; //5
return 0;
}
赋值重载需要先把原空间释放掉,然后给_a重新开一块跟st一样大的空间,再拷贝数据,再改其他成员,st和st1的栈顶元素就都变成了5。
4、什么情况下可以不自己显示定义赋值运算符重载
Stack& operator=(const Stack& st)
{
if (this != &st)
{
cout << "Stack& operator=(const Stack& st)" << endl;
free(_a);
_a = ((STDataType*)malloc(sizeof(STDataType) * st._capacity));
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
return *this;
}
class MyQueue
{
public:
MyQueue(int capacity = 4)
:_st1(capacity)
,_st2(capacity)
{}
~MyQueue()
{}
private:
Stack _st1;
Stack _st2;
};
int main()
{
MyQueue mq1;
MyQueue mq2;
mq1 = mq2;
return 0;
}
我们在Stack类的赋值重载中加上一句打印
可以看到自定义类型去调用了它的赋值重载
三、取地址运算符重载
1、const成员函数
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 1, 10);
const Date d2(2025, 1, 11);
d1.Print();
d2.Print();
return 0;
}
const对象可以调用,普通对象也可以调用,是权限减小,所以在不改变成员变量的函数内,都可以加const来修饰this指针指向的内容不能改变。
2、取地址运算符重载
class Date
{
public:
Date* operator&()
{
//return this;
return nullptr;
}
const Date* operator&()const
{
//return this;
return nullptr;
}
private:
int _year; // 年
int _month; // ⽉
int _day; // ⽇
};
最后两个默认成员函数不重要,平时99%的场景都用不到,大家只需要掌握前四个默认成员函数就可以。
总结
有了运算符重载很多自定义类型的行为就都是可控的了,是一个C++很伟大的发明,下一篇文章将会来讲解日期类的全部实现,大家不要走开哟!