拷贝构造函数
在实际代码运用中,我们经常会拷贝一个对象用来做其他事情,在C语言中,这个过程十分简单,把值直接全部从一个内容中拷贝到另外一个地方即可,但在C++中却不那么容易
拷贝构造函数的引入
首先要清楚堆创建后,除非通过free否则是不会被还原的,因此如果有这样的C语言代码:
void push(Stack ps)
{
ps._a[0] = 10;
ps._top++;
}
void test2()
{
Stack s={
0,0,0};
StackInit(&s);
push(s);
printf("%d", s._a[0]);
}
这里的StackInit函数只是单纯的初始化,给栈开辟空间,而最后运行结果是10,原因就在于在push函数中,虽然是值传递,但是ps结构体中的成员_a依旧拥有改变堆内存的能力,具体可以用下面的图来表示
那么现在换到C++,引入类的概念后,整个就变得比C语言要复杂一点,原因如下:
首先,定义一个栈的类,并且完成一系列栈的操作
typedef int STDataType;
class Stack
{
public:
Stack()
{
capacity = 4;
a = (STDataType*)malloc(sizeof(STDataType) * capacity);
if (a == nullptr)
{
perror("malloc fail");
exit(-1);
}
top = 0;
}
~Stack()
{
top = capacity = 0;
free(a);
a = nullptr;
}
void Push(STDataType x)
{
if (capacity == top)
{
capacity *= 2;
STDataType* tmp = nullptr;
tmp = (STDataType*)realloc(a,sizeof(STDataType) * capacity);
if (tmp == nullptr)
{
perror("realloc fail");
exit(-1);
}
a = tmp;
}
a[top] = x;
top++;
}
void Pop()
{
top--;
}
STDataType Top()
{
return a[top];
}
private:
STDataType* a;
int top;
int capacity;
};
和C语言的实现相同,假如我们直接进行传值拷贝,具体做法如下:
void func1(Stack s)
{
s.Push(1);
}
int main()
{
Stack s1;
func1(s1);
return 0;
}
再画出和上面相仿的图
看似和C语言基本相同,但实际相差很大,C++会执行构造函数和析构函数,那么在进入func1的栈帧后,销毁栈帧的时候就会执行析构函数,_a所指向的空间就被销毁掉了,那么回到main函数的栈帧后,结束程序依旧要进行析构函数,此时_a已经被销毁过一次了,程序就会崩溃,无法正常运行
这其实也就说明,C++中想要直接进行对象拷贝似乎不是一件容易的事,两个对象指向同一片空间就必然会出问题,C++语法就定义了拷贝函数来解决这个问题
拷贝构造函数的特征
拷贝构造函数也是特殊的成员函数,具体表现在:
- 拷贝函数是构造函数的一个重载
- 拷贝函数的参数只有一个并且必须是类类型对象的引用,使用传值方式编译器会报错,因为涉及到了无穷递归调用
- 若未显式定义,编译器会生成默认的拷贝构造函数,默认拷贝构造函数会按对象按内存中的存储字节序完成拷贝,也叫做浅拷贝或值拷贝
- 深拷贝就涉及到上面栈在堆上空间的问题
- 拷贝构造函数的典型应用场景
使用已存在的对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
==下面根据拷贝构造函数的特征进行一一分析==
1. 拷贝构造函数是构造函数的重载
这个很好解释,拷贝构造函数函数名和构造函数相同,只是函数参数不同
2. 拷贝函数的参数只有一个并且必须是类类型对象的引用,使用传值方式编译器会报错,因为涉及到了无穷递归调用
假设我们这里是这样实现拷贝构造函数:
//函数定义
Date(const Date d1)
{
_day = d1._day;
_month = d1._month;
_year = d1._year;
}
//函数调用
Date d1(d2);
那么标准写法是如何写的呢
//函数定义
Date(const Date& d1)
{
_day = d1._day;
_month = d1._month;
_year = d1._year;
}
//函数调用
void func2(Date d2)
{
d2.Print();
}
int main()
{
Date d1(2002, 10, 12);
d1.Print();
func2(d1);
return 0;
}
**3. 若未显式定义,编译器会生成默认的拷贝构造函数,默认拷贝构造函数会按对象按内存中的存储字节序完成拷贝,也叫做浅拷贝或值拷贝
- 深拷贝就涉及到上面栈在堆上空间的问题**
这里需要注意的是:
不写拷贝构造函数时,编译默认生成的拷贝构造,和之前的构造函数特性是不一样的
- 内置类型是值拷贝
- 自定义的类型是调用它的拷贝
简单来说,像Date类型的就不需要我们进行拷贝构造,但是Stack类型的就需要进行深拷贝
下面是深拷贝的拷贝构造函数
Stack(const Stack& s1)
{
top = s1.top;
capacity = s1.capacity;
STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * capacity);
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
a = tmp;
}
所谓深拷贝,就是重新在堆上开辟一个空间供构造出的栈使用,这样就避免了函数栈帧中的栈在结束时free掉了堆上的空间使得main函数崩溃的情况出现
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
运算符重载
C++在C的基础上的提升在运算符重载上也可以体现出
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
假设我们现在要比较日期谁大,那么这个场景就可以应用运算符重载
// 类体内定义运算符重载
bool operator <(const Date& d1)
{
if (_year > d1._year)
{
return false;
}
else if (_year == d1._year && _month > d1._month)
{
return false;
}
else if (_year == d1._year && _month == d1._month && _day > d1._day)
{
return false;
}
else
{
return true;
}
}
// 调用main函数
int main()
{
int i = 10;
int j = 20;
int tmp1 = i < j;
Date d1(2000, 10, 20);
Date d2(2001, 8, 10);
int tmp2 = d1 < d2;
cout << tmp1 << endl;
cout << tmp2 << endl;
}
我们转到汇编观察
从中也不难发现,运算符重载后调用小于实际上是调用了运算符重载函数
这样写代码的可读性大大提高