tags:
- Cpp
Copying and Copy Constructors in C++
拷贝是指将数据从一块内存复制到另一块内存。当我们在栈中进行拷贝时,栈中会出现两份相同的内存块。而当我们拷贝堆上的变量(对象)时,通常会将整个对象先搬到栈上,然后再复制一份到堆上。拷贝的过程涉及两次内存操作,可能会影响性能,如果堆上对象过大还可能引发栈溢出。为了避免拷贝带来的性能开销和不必要的对象,从而引入了移动语义。
所以当我们拷贝传参时,在函数内部对拷贝对象所作出的改变并不会影响我们原先内存中的数据。对象所占内存越大,拷贝所用的空间开销和时间开销就会越大。某些情况下,我们会尽量避免拷贝造成的开销。我们课堂上学习过两种方式:(1)指针 和 (2)引用。
我们下面先用一些简单的拷贝作为示例:
示例一:
int main(){
int a = 10;
int b = a; // b对a的拷贝
int* ptr1 = a;
int* ptr2 = ptr1; // ptr2对ptr1的拷贝
return 0;
}
示例二:
void increment(int x) {
x++;
}
int main() {
int a = 5;
increment(a);
// a 仍然是 5
}
我们将函数拆开放在主函数里面,像内联inline那样,就是如下这样:
// 内敛后的代码
int main() {
int a = 5;
{
int x = a;
x++;
}
// a 仍然是 5
}
当我们使用指针进行值传递和值改变的时候,实际上仍然使用了拷贝,即指针的拷贝。但相比类对象的拷贝而言,拷贝指针的代价要小很多。我们将上面的代码改成如下的指针传递。我们发现a
的值改变了,这是因为我们将a
的地址赋值给了指针x
。当我们改变指针x
所指向的值(也就是a
)时,a
当然会改变了。
void increment(int* x) {
(*x)++;
}
int main() {
int a = 5;
increment(&a);
// a 变为 6
}
// 相当于
int main() {
int a = 5;
{
int* x = &a;
(*x)++;
}
// a 变为 6
}
另一种避免拷贝的方式就是引用了,引用是对现有对象的别名,并不会创建新的对象。当我们用int& x = a;
时,a
的引用x
会直接指向对象的内存地址。这样子好像我们为x
分配了内存用于存储&a
即a
的地址。其实不然,引用和被引用对象共享同一个内存地址,因此对引用的任何修改都会直接反映在被引用对象上。
我们用链接视角可能更清楚一点。在C++中,引用(reference)和被引用对象在连接时的符号是一样的。引用只是被引用对象的别名,它们共享同一个内存地址。因此,对引用的任何操作实际上都是对原对象的操作。
void increment(int& x) {
x++;
}
int main() {
int a = 5;
increment(a);
// a 变为 6
}
//相当于
int main() {
int a = 5;
{
int& x = a;
x++;
}
// a 变为 6
}
// 相当于
int main() {
int a = 5;
{
a++;
}
// a 变为 6
}
上面的例子很好的展示了引用的作用。我们创建了a
的引用x
,但x
作为a
的别名(其实就是a
),当我们操作x
实际也就是对a
的操作。
This is why we need a copy constructor/copy assignment operator. That is - the rule of three.
在一个类中,由于我们没有定义拷贝构造函数,编译器会生成默认的拷贝构造函数进行浅拷贝。那究竟做了什么呢?我们创建了新的String对象string2
,随后编译器将string
对象中变量m_Buffer
和 m_Size;
赋予string2
中的变量。带来的后果就是string
和string2
的m_Buffer
指向同一片内存区域。在析构的时候就会导致内存的双重释放。
#include <iostream>
#include <string.h>
class String
{
private:
char* m_Buffer;
unsigned int m_Size;
public:
String(const char* str)
{
m_Size = strlen(str);
m_Buffer = new char[m_Size + 1];
memcpy(m_Buffer, str, m_Size + 1);
std::cout << "Constructor's been called." << std::endl;
}
~String()
{
delete[] m_Buffer;
std::cout << "Destructor's been called." << std::endl;
}
friend std::ostream& operator<<(std::ostream& stream, const String& str);
};
std::ostream& operator<<(std::ostream& stream, const String& str)
{
stream << str.m_Buffer;
return stream;
}
int main(){
String string = "CongZhi";
String string2 = string;
std::cout << string << std::endl;
std::cout << string2 << std::endl;
return 0;
}
运行结果如下:
Constructor's been called.
CongZhi
CongZhi
Destructor's been called.
free(): double free detected in tcache 2
Aborted
编译器所做的就是一个浅拷贝:
String(const String& other)
{
memcpy(this, &other, sizeof(String));
}
// 也就是
String(const String& other)
: m_Buffer(other.m_Buffer), m_Size(other.m_Size)
{}
如何避免上面的这种情况发生呢?我们可以用深拷贝。在上面浅拷贝的用例中,我们发现类对象的拷贝仅仅停留在指向string的指针上,但是我们期望对字符串本身的拷贝,而不是指向字符串的指针。我们定义一个拷贝构造函数来避免这种浅拷贝的情况发生。
String(const String& other)
: m_Size(other.m_Size)
{
m_Buffer = new char[m_Size + 1];
memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
std::cout << "Cpoy constructor's been called." << std::endl;
}
但是我们不想在main()
函数中打印,我们封装一个printStr()
函数,用printStr()
打印类对象会怎么样?
void printStr(String str)
{
std::cout << str << std::endl;
}
int main(){
String string = "CongZhi";
String string2 = string;
printStr(string);
printStr(string2);
return 0;
}
我们会调用非常多的构造和析构函数,这完全是不必要的。平白无故占用系统资源,这当然不是我们想看到的,所以我们应避免使用拷贝。我们可以用const引用,void printStr(String& str)
。
Constructor's been called.
Cpoy constructor's been called.
Cpoy constructor's been called.
CongZhi
Destructor's been called.
Cpoy constructor's been called.
CongZhi
Destructor's been called.
Destructor's been called.
Destructor's been called.
使用引用后:
Constructor's been called.
Cpoy constructor's been called.
CongZhi
CongZhi
Destructor's been called.
Destructor's been called.