深拷贝与浅拷贝+返回值优化
概念
在C++中,深拷贝(deep copy)和浅拷贝(shallow copy)是两种不同的对象拷贝方式,它们在处理动态分配内存时表现出不同的行为。
-
浅拷贝(Shallow Copy):
-
浅拷贝是指将一个对象的数据成员的值复制到另一个对象,但如果对象中包含指向动态分配内存的指针,它们将指向同一块内存空间。
-
这样,当一个对象被销毁时,它指向的内存空间会被释放。如果另一个对象仍然引用这片内存,那么这个内存将变成悬空指针,访问它可能导致未定义行为。
-
使用默认的拷贝构造函数和赋值运算符进行拷贝时,通常会发生浅拷贝。
-
-
深拷贝(Deep Copy):
-
深拷贝是指创建一个新的对象,然后将原对象中的数据成员复制到新对象中,包括指向动态分配内存的指针,而不是简单地复制指针本身。
-
这样,原对象和新对象各自拥有一份独立的内存空间,对其中一个对象的操作不会影响另一个对象。
-
为了实现深拷贝,通常需要自定义拷贝构造函数和赋值运算符,确保所有资源都被正确复制。
这样说大家可能看不懂,这样咱在代码中进行理解
-
浅拷贝
/*************************************************************************
> File Name: test.cpp
> Author:Xiao Yuheng
> Mail:3312638794@qq.com
> Created Time: Sat Oct 28 17:03:20 2023
************************************************************************/
#include <iostream>
using namespace std;
#define BEGINS(x) namespace x {
#define ENDS(x) }
BEGINS(xyh)
class A{
public:
int x,y;
A(int x = 100, int y = 100) : x(x), y(y) {}
~A() {}
};
ENDS(xyh)
int main() {
xyh::A a;
xyh::A b = a;
cout << a.x << " " << a.y << endl;
cout << b.x << " " << b.y << endl;
return 0;
}
我们先来看看这段代码的结果
由构造函数的知识可以知道xyh::A b = a;
这行代码会调用拷贝构造函数,由于我们没有写,这时会调用系统自默认的拷贝构造,也就是说把a对象的值全部拷贝给b对象,所以没有问题,好我们接下来在来看一段代码:
/*************************************************************************
> File Name: test.cpp
> Author:Xiao Yuheng
> Mail:3312638794@qq.com
> Created Time: Sat Oct 28 17:03:20 2023
************************************************************************/
#include <iostream>
using namespace std;
#define BEGINS(x) namespace x {
#define ENDS(x) }
BEGINS(xyh)
class A{
public:
int n;
int *data;
A(int n = 100) : n(n), data(new int[n]) {}
~A() {
delete[] data;
}
};
ENDS(xyh)
int main() {
xyh::A a;
xyh::A b = a;
return 0;
}
我们直接看运行结果:
我们可以看见报错了,可是为什么呢,我们由错误可以知道我们free()了两次,可是为什么会这样呢?这我们就得提到浅拷贝的缺陷了,在第二个样例中我们xyh::A b = a;
可以知道他会调用系统自带的默认拷贝构造函数,这时呢他会将a对象的值完全复制给b对象,包括指针指向的地址也是一样的,可当我们结束这段程序是,系统会调用析构函数来释放这段空间,可是由于a对象和b对象指向同一块地址,所以就导致我们释放了同一块内存两次,所以导致报错。我们可以在代码中加上这么一段代码:
int main() {
xyh::A a;
xyh::A b = a;
cout << a.data << endl;
cout << b.data << endl;
return 0;
}
把a.data和b.data这两个指针指向的地址打印出来,我们看运行结果:
我们可以发现地址是一样的。现在我们知道了,为什么报错了,可该怎么改呢?我们接着看深拷贝。
深拷贝
我们在之前的代码上手写一个拷贝构造
/*************************************************************************
> File Name: test.cpp
> Author:Xiao Yuheng
> Mail:3312638794@qq.com
> Created Time: Sat Oct 28 17:03:20 2023
************************************************************************/
#include <iostream>
using namespace std;
#define BEGINS(x) namespace x {
#define ENDS(x) }
BEGINS(xyh)
class A{
public:
int n;
int *data;
A(int n = 100) : n(n), data(new int[n]) {}
A(const A & a) : n(a.n), data(new int[n]){
for (int i = 0; i < n; i++) {
data[i] = a.data[i];
}
}
~A() {
delete[] data;
}
};
ENDS(xyh)
int main() {
xyh::A a;
xyh::A b = a;
cout << a.data << endl;
cout << b.data << endl;
return 0;
}
我们在重新开辟一块空间,然后用b对象的data指向这片空间,然后我们在进行赋值操作,这样我们就不会出现错误信息了,我们来运行一下:
我们可以发现这a对象的data指针指向的地址和b对象的data指针指向的地址不一样了,好,在这样样例中我们深拷贝完成了,可是有没有一种很好的方法,可以适应各种情况呢?好我们接着往下看。
/*************************************************************************
> File Name: test1.cpp
> Author:Xiao Yuheng
> Mail:3312638794@qq.com
> Created Time: Fri Oct 27 16:16:22 2023
************************************************************************/
#include <iostream>
using namespace std;
template<typename T>
class Vector {
public:
Vector(int n = 100) : n(n), data(new T[n]) {}
Vector(const Vector &a) : n(a.n), data(new T[n]) {
for (int i = 0; i < n; i++) {
data[i] = a.data[i];
}
return ;
}
~Vector() {
delete[] data;
}
int n;
T *data;
};
int main() {
Vector<Vector<int>> arr1;
Vector<Vector<int>> arr2(arr1);
cout << arr1.data << endl;
cout << arr2.data << endl;
return 0;
}
大家请看这段代码,还是相同的拷贝构造函数还能否继续执行正确呢?我们运行一下看看:
可以看见这里报错了,可是原因是什么呢?我们可以看见data[i]是一个数组指针,这时直接运行data[i] = a.data[i];是不是又相当于一次浅拷贝呢?这时我们应该怎么解决这个问题呢?
我们在这里引入一个新的概念,原地拷贝,什么是原地拷贝呢?大家可以这样理解:就是可以递归的进行拷贝构造。具体代码如下:
/*************************************************************************
> File Name: test1.cpp
> Author:Xiao Yuheng
> Mail:3312638794@qq.com
> Created Time: Fri Oct 27 16:16:22 2023
************************************************************************/
#include <iostream>
using namespace std;
template<typename T>
class Vector {
public:
Vector(int n = 100) : n(n), data(new T[n]) {}
Vector(const Vector &a) : n(a.n), data(new T[n]) {
for (int i = 0; i < n; i++) {
new(data + i) T(a.data[i]);
}
return ;
}
~Vector() {
delete[] data;
}
int n;
T *data;
};
int main() {
Vector<Vector<int>> arr1;
Vector<Vector<int>> arr2(arr1);
cout << arr1.data << endl;
cout << arr2.data << endl;
return 0;
}
这样就可以很好的解决这个问题。
返回值优化
返回值优化(Return Value Optimization,简称RVO)是一种C++编译器的优化技术,用于避免在函数返回一个对象时发生不必要的拷贝操作。
在C++中,当一个函数返回一个对象时,通常会创建一个临时对象,然后将函数中的局部对象拷贝到这个临时对象中,最后返回这个临时对象的副本。这个过程涉及到了拷贝构造函数的调用。
RVO的实现原理是,编译器会在返回对象时直接在函数调用的地方构造该对象,而不是在函数内部构造一个临时对象然后再进行拷贝操作。这样可以节省内存和运行时开销。
这样说可能不太明确,我来通过一段代码告诉大家:
/*************************************************************************
> File Name: test3.cpp
> Author:Xiao Yuheng
> Mail:3312638794@qq.com
> Created Time: Sat Oct 28 20:16:17 2023
************************************************************************/
#include <iostream>
using namespace std;
class A{
public:
int x, y;
A() {
cout << "构造函数" << endl;
}
A(const A &a) {
cout << "拷贝构造" << endl;
}
};
A fun() {
A temp;
return temp;
}
int main() {
A b = fun();
return 0;
}
大家猜一下这段代码会输出什么?我们来运行一下:
我们可以看见最后就输出了一个“构造函数”,可是按照原理来说我们应该先输出的是
-
构造函数:A temp;这个会调用构造函数
-
拷贝构造:return temp; 在拷贝给一个临时变量
-
拷贝构造:A b = fun();最后临时变量在拷贝给b对象
这是因为我们现在的编译器基本都带有返回值优化,这里我们把返回值优化关了大家看一下:
这时我们可以看见他输出了我们上面输出的那三个运行结果;
这是我们原本应该经过的的过程,可当开启返回值优化之后会发生什么呢?
/*************************************************************************
> File Name: test3.cpp
> Author:Xiao Yuheng
> Mail:3312638794@qq.com
> Created Time: Sat Oct 28 20:16:17 2023
************************************************************************/
#include <iostream>
using namespace std;
class A{
public:
int x, y;
A() {
cout << "构造函数" << endl;
}
A(const A &a) {
cout << "拷贝构造" << endl;
}
};
void fun(A &temp) {
return ;
}
int main() {
A b;
fun(b);
return 0;
}
我们看看这段代码,是的,开启返回值优化之后,差不多和这段代码一样,我们来看一下运行结果
是不是和开启返回值优化差不多呢!我们来分析一下这个代码,这时先定义一个对象b,然后在把b对象的引用传递到函数fun();然后在运行这个fun函数里面的内容。而返回值优化差不多也是同理。