目录
- 拷贝构造函数
-
- 概念
- 特性
- 拷贝构造函数为什么传引用的原因
-
- 浅拷贝
- 深拷贝
感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接
🐒🐒🐒 个人主页
🥸🥸🥸 C语言
🐿️🐿️🐿️ C语言例题
🐣🐣🐣 python
🐓🐓🐓 数据结构C语言
🐔🐔🐔 C++
🐿️🐿️🐿️ 文章链接目录
拷贝构造函数
概念
拷贝构造我们经常在使用,需要拷贝时我们Ctrl C ,构造时就Ctrl V
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用。
特性
拷贝构造函数也是特殊的成员函数,其特征如下:
拷贝构造函数是构造函数的一个重载形式。
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内置类型成员内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝(类似memcpy按一个字节一个字节的拷贝),对于自定义类型成员调用他的拷贝构造
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
义类型是调用其拷贝构造函数完成拷贝的。
- 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
- 拷贝构造函数典型调用场景:
使用已存在对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用。
拷贝构造函数为什么传引用的原因
因为拷贝构造是一个特殊的构造函数,所以在类中我们需要自己写一个构造函数,而这个构造函数中的传参比较特殊,需要传一个对象的引用,具体如下
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)//拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 5, 9);
Date d2(d1);
return 0;
}
这段代码是将d1的_year _month _day通过拷贝构造函数,构造出一个d2
构造函数是可以重载的,所以有两个Date,其中一个Date(Date &d)表示将d1中的_year _month _day都拷贝给d2,需要注意的是Date括号里必须有类名,并且要传引用
为什么一定要传引用呢?我们看看下面的代码
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void func1(Date d)
{
}
void func2(Date& rd)
{
}
int main()
{
Date d1(2024, 5, 10);
func1(d1);
func2(d1);
return 0;
}
这段代码func1是传值传参,也就是将d1的值传给d,d是d1的拷贝,func2是传引用传参,rd是d1的别名
通过调试我们发现,在调用func1函数的时候,调试的箭头首先会直接来到了拷贝构造函数中,这是因为C++规定自定义的类型都会调用拷贝构造函数, 箭头在走完拷贝构造函数后才会跳到func1函数中
当箭头走到func2后,是没有再去调用拷贝构造的,这是因为rd是d1的别名,所以不需要拷贝构造
我们梳理一下调用func1函数的过程
回到之前的问题,为什么一定要传引用呢?并且拷贝构造函数的参数不是引用的话会产生无限递归呢?
Date d2(d1)是拷贝构造d1,因为拷贝构造是一个特殊的构造(拷贝构造的参数是一个类的对象,普通构造的参数只是int…类型),所以在拷贝构造的时候会自动调用拷贝构造函数,但是由于拷贝构造函数没有用引用符号,所以会出现无限递归的情况,具体过程如图
而为什么func1函数的例子可以值调用一次就结束了呢?
这是因为func1调用的拷贝构造函数有引用符号,所以只需要调用一次就结束了,如果没有引用符号就会因为调用的拷贝构造需要传参,而传参又会自动调用一个新的拷贝构造函数,而新的拷贝构造函数又需要传参,又会自动调用一个新的拷贝构造函数…
通常拷贝构造函数都会在传引用的类名的签名加一个const,因为如果不加const可能会发生下面的情况
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
d._year=_year ;
d._month = _month;
d._day = _day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 5, 10);
Date d2(d1);
return 0;
}
这个代码原本是想让d2拷贝d1的成员变量,但是由于函数中的赋值写反了,导致变成了d2给d1赋值,因为拷贝构造函数是传的引用,所以d1在拷贝构造函数中会被修改,因为d2没有被初始化,所以在赋值给d1后,d1的成员变量全被变成了随机值
所以正确写法这样的
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 5, 10);
Date d2(d1);
return 0;
}
来看看下面这个代码
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year <<"/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 5, 10);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
这个代码中没有拷贝构造,但是最后d1和d2调用Print函数后却发现,d2的输出结果和d1的输出结果一样,这是因为拷贝构造也是默认的成员函数,我们不写拷贝构造时,编译器会自动生成,但是需要注意的是从输出的结果可以看出,这里的拷贝构造对内置类型处理了,让d2的成员变量和d1的相同
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
Time(const Time& t)
{
cout << "Time(cout Time&t)" << endl;
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year <<"/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
Time t;
};
int main()
{
Date d1(2024, 5, 10);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
这个代码想调用Time的构造函数,却报错说没有合适的默认构造函数可以用,明明代码中并没有写构造函数,理论上系统会自动生成一个构造函数,但是这里并没有生成,说明我们自己写了一个构造函数,只是我们并不知道,而我们写的构造函数其实是拷贝构造函数,因为拷贝构造函数是一个特殊的构造函数,所以当编译器调用拷贝构造函数时,发现并没有传对象去拷贝构造,所以报错
为了解决这个问题,我们提供一个方法,在类中加入 Time() = default 表示没有合适的构造函数时强制生成一个构造函数
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
Time() = default;
Time(const Time& t)
{
cout << "Time(cout Time&t)" << endl;
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year <<"/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
Time t;
};
int main()
{
Date d1(2024, 5, 10);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
浅拷贝
有时默认生成的拷贝构造会发生崩溃
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 st1;
Stack st2(st1);
通过调试我们发现这个拷贝构造是成功了的,但是需要注意的一个点是指针array也被拷贝了过去,两个指针指向的地址都是一样的
因为是调用的函数,所以内存存储在栈上,满足后进先出的原则,当Stack st2(st1)结束后,会调用析构函数,而析构函数中会将_array变成空,并释放_array所指向的空间,但是这里的_array只是str2中的_array,str1中的_array没有被释放,所以最后就导致str1中的_array变成了一个野指针
而当str1调用习惯函数时,又会对_array所指向的空间进行释放,但是这块空间之前已经被释放掉了,也就是说这里已经对这块空间进行了两次释放,同一块空间是不能释放两次的,所以就崩溃了
解决这种情况就需要用到深拷贝了
深拷贝
深拷贝的原理就是创造出一块同样大小的空间,改变指针的指向,如下图
在st2调用析构函数之前,先开一块和_array指向空间大小一样的空间,再将str1中的_array指向新开的空间,然后再让str2进行销毁
代码如下
Stack(const Stack& s)
{
DataType* tmp = (DataType*)malloc(sizeof(s._capacity * (sizeof(DataType))));
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(tmp, s._array, sizeof(DataType) * s._size);
_array = tmp;
_size = s._size;
_capacity = s._capacity;
}
完整代码
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;
}
Stack(const Stack& s)
{
DataType* tmp = (DataType*)malloc(sizeof(s._capacity * (sizeof(DataType))));
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(tmp, s._array, sizeof(DataType) * s._size);
_array = tmp;
_size = s._size;
_capacity = s._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 st1;
Stack st2(st1);
return 0;
}
最后拷贝构造告诉我们对于函数传参一般选择用传引用传参