今天上课学到了C++的浅层拷贝与深层拷贝, 于是在这里将C++和Java关于浅层拷贝与深层拷贝做一个对比.
一.C++的浅层拷贝与深层拷贝
- 先来了解一下C++中的复制构造函数:
//假设有这样一个TEST类:
class TEST
{
private:
int *num;
public:
TEST(int n)
{
num = new int;
*num = n;
}
void change(int anothernum)
{
*num = anothernum;
}
void print()
{
cout << *num << endl;
}
};
在上述TEST类中并没有显式定义TEST类的复制构造函数, 那么其默认的复制构造函数应为:
TEST(const TEST & t)
{
num = t.num;
}
1.C++的浅层拷贝
以上述TEST类为例, 假设有如下代码:
#include<iostream>
using namespace std;
class TEST
{
private:
int *num;
public:
TEST(int n)
{
num = new int;
*num = n;
}
void change(int anothernum)
{
*num = anothernum;
}
void print()
{
cout << *num << endl;
}
};
int main(void)
{
TEST a(10);
TEST b(a);
a.print();
b.print();
b.change(100);
a.print();
b.print();
}
上述代码中并没有显式定义复制构造函数, 运行结果如下:
10
10
100
100
//即更改了b对象的值, a对象的值也随之改变
上述结果可以证明, C++的默认构造函数为浅层复制, 也就是说a对象和b对象指向同一段内存空间.
在小伙伴的提示下, 上述代码存在一些问题:在TEST类中并没有定义析构函数.析构函数的定义应该如下:
~TEST() { delete[] num; }
那么定义析构函数和不定义析构函数都会引发什么样的问题呢?
- 定义析构函数:
首先a对象执行析构函数, 释放掉a对象所指向的空间之后, b对象再执行析构函数, 会释放掉b对象所指向的空间. 前面提过了, 使用默认的拷贝构造函数拷贝的b对象和a对象应该是指向同样一段内存空间的. 这样等于对一段空间delete了两次, 会造成内存非法访问, 是极为不妥当的!!!- 不定义析构函数:
如果没有构造析构函数, 那么a和b所指向的空间无法得到释放, 这会造成内存泄露, 这样也是极为不妥当的!!!综上所述, 在TEST类中, 不管是否定义析构函数都是不妥当的, 那这该怎么办呢?
- 拷贝构造函数中涉及到需要动态分配内存的情况下, 应该自定义拷贝构造函数.
- 换句话来说, 如果需要自定义析构函数, 那么也应该自定义拷贝构造函数.
2.C++的深层拷贝
依旧来看一段代码:
#include<iostream>
using namespace std;
class TEST
{
private:
int *num;
public:
TEST(int n)
{
num = new int;
*num = n;
}
//此处显示定义了TEST类的复制构造函数
TEST(const TEST & t)
{
num = new int;
*num = *t.num;
}
void change(int anothernum)
{
*num = anothernum;
}
void print()
{
cout << *num << endl;
}
};
int main(void)
{
TEST a(10);
TEST b(a);
a.print();
b.print();
b.change(100);
a.print();
b.print();
}
上述代码中显式定义了复制构造函数, 自定义的复制构造函数中, 对a对象在堆上动态申请了空间, 然后再将b对象的值赋值给a对象新申请的这段内存空间.
运行结果如下:
10
10
10
100
//更改b对象的值并没有改变a对象的值, 充分说明a对象和b对象所占的是不同的两段内存空间
二.Java的浅层拷贝和深层拷贝
1.以数组为例, 关于Java浅层拷贝和深层拷贝的几个小知识点
- 数组复制的两个方法:
System.arraycopy(array1, 0, array2, array1.length);五个参数分别为:源数组, 源数组起始索引, 目的数组, 源数组长度.
Arrays.copyOf(array1, array1.length);参数分别为:源数组, 源数组的长度. - 在Java中, 只要看到new, 就是建立对象, 即申请一段新的空间. 也就是说, 只要new了, 二者就不是同一个对象,这一点千万要注意!!!
- 关于Integer类的一点知识:若使用Integer a = xxx;这样的形式打包一个值, 要打包的值在Integercache.low~Integercache.high(-128~127)之间, 若在缓存中没有打包过, 则返回一个new的新对象; 若打包过, 则直接返回打包过的对象; 若不在此范围内, 则直接返回一个new的新对象. 而使用Integer a = new Integer(xxx);这样打包出来的值, 一定是一个新的对象.
- 关于字符串池:Java为了效率考虑, 凡是以”“写下的字符串都为字符串常量, 以”“包括的字符串, 只要内容相同, 无论在代码中出现多少次, JVM都只会建立一个String对象. 也就是说, “”写下的字符串都会被放入字符串池中, 往后再出现与”“写下字符串相同的字符串, 都将直接参考(意大致等同于C中的指向)至字符串池中已有对象, 不再建立新的对象.
2.Java的深层拷贝
为什么要先谈Java的深层拷贝呢?
因为在Java中, 只要是基本数据类型的数组, 使用上面介绍到的两个数组拷贝函数进行数组拷贝, 都是深层拷贝!
来看如下代码:
public class ArrayCopy {
public static void main(String args[]){
int[] array1 = {0, 1, 2, 3, 4, 5};
int[] array2 = Arrays.copyOf(array1, array1.length);
for(int num : array1){
System.out.printf("%3d", num);
}
System.out.println();
for(int num : array2) {
System.out.printf("%3d", num);
}
System.out.println();
array2[0] = 10;
for(int num : array1){
System.out.printf("%3d", num);
}
System.out.println();
for(int num : array2){
System.out.printf("%3d", num);
}
}
}
因为基本类型的数组的拷贝皆为深层拷贝额, 所以更改array2数组第一个元素的值, 并不会影响array1数组第一个元素的值. 运行结果如下:
0 1 2 3 4 5
0 1 2 3 4 5
0 1 2 3 4 5
10 1 2 3 4 5
3.Java的浅层拷贝
无论是使用System.arraycopy()还是Arrays.copyOf(), 只要用作类类型声明的数组时, 都执行浅层拷贝, 即源数组与拷贝数组指向同一段内存空间.
需要特别说明的是, 数组在Java中也是类的对象, 所以二维数组和三维数组在使用System.arraycopy()和Arrays.copyOf()的时候, 执行的也是浅层拷贝.
关于浅层拷贝就不在这里举例子了, 下面来看一看, 如何让类类型的数组执行深层拷贝.
4.使类类型的数组执行深层拷贝
看如下代码:
public class DeepCopy {
public static class cloths{
String color;
char size;
cloths(String col, char si){
color = col;
size = si;
}
}
public static void main(String[] args){
cloths[] c1 = {new cloths("red", 'l'), new cloths("blue", 'm')};
cloths[] c2 = new cloths[c1.length];
for(int i = 0; i < c1.length; i++){
cloths c = new cloths(c1[i].color, c1[i].size);
c2[i] = c;
}
c1[0].color = "yellow";
System.out.println(c2[0].color);
}
}
上述代码, 在复制每一个类类型的数组元素时, 都给其new一段新的空间, 使之与源数组元素完全隔离开. 所以运行结果如下:
red
//源数组的第一个元素的color并没有被改变