对比了下Java, C++, rust三种编程语言在对象复制过程中的区别。

Java

1. equals==

java的8种数据类型:float, double, byte, short, int, long, char, boolean对于这8种数据类型,==直接比较的是其存储的值,并且它们没有equals方法

public class Apple {
    private int category;
    private double price;

    public static void main(String args[]){

        int a = 5;
        int b = 5;

        System.out.println(a == b);
    }
}
true

Process finished with exit code 0

对于引用类型的变量,==比较的是所指向的对象的地址。如果没有重写equals方法,则equals==一样,都是比较引用类型的变量所指向的对象的地址。

public class Apple {
    private int category;
    private double price;

    public static void main(String args[]){

        Apple a1 = new Apple();
        Apple a2 = new Apple();

        System.out.println("a1 == a2? " + (a1 == a2));
        System.out.println("a1 equals a2? " + a1.equals(a2));
        System.out.println("The address of a is " + a1.hashCode());
        System.out.println("The address of b is " + a2.hashCode());
    }
}
a1 == a2? false
a1 equals a2? false
a1指向的地址为:999966131
a2指向的地址为:1989780873

诸如String, Date等类对equals方法进行了重写的话,equals比较的是所指向对象的内容(注意,重写equals的时候连hashCode方法也重写了,因此不能用hashCode来判断对象地址)。

public class Apple {
    private int category;
    private double price;

    public static void main(String args[]){

        String s1 = "hello world";
        String s2 = "hello world";

        String s3 = new String("hello");
        String s4 = new String("hello");

        System.out.println("s1 == s2? " + (s1 == s2));
        System.out.println("s1 equals s2? " + s1.equals(s2));

        System.out.println("s3 == s4? " + (s3 == s4));
        System.out.println("s3 equals s4? " + s3.equals(s4));
    }
}
s1 == s2? true
s1 equals s2? true
s3 == s4? false
s3 equals s4? true

2. 赋值

public class Apple {
    private int category;
    private float price;

    public static void main(String args[]){
        Apple a1 = new Apple();
        Apple a2 = new Apple();

        System.out.println("a1 == a2? " + (a1 == a2));
        System.out.println("a1指向的地址为:" + a1.hashCode());
        System.out.println("a2指向的地址为:" + a2.hashCode());

        a1 = a2;

        System.out.println("\na1 == a2? " + (a1 == a2));
        System.out.println("a1指向的地址为:" + a1.hashCode());
        System.out.println("a2指向的地址为:" + a2.hashCode());
    }
}

输出为:

a1 == a2? false
a1指向的地址为:999966131
a2指向的地址为:1989780873

a1 == a2? true
a1指向的地址为:1989780873
a2指向的地址为:1989780873

这里,我们创造了两个对象,a1和a2,它们分别指向内存的两块不同区域。但是当我执行a1 = a2后,a1和a2现在指向了同一个地方(原来a1指向的部分现在由java的GC接手)。而C++可不是如此:

C++

案例1:

#include <iostream>
using namespace std;

int main(){
    int a1 = 1;
    int a2 = 2;

    cout << "a1指向的地址为: " << &a1 << endl;
    cout << "a2指向的地址为: " << &a2 << endl;
    cout << "a1 == a2? " << (a1 == a2) << endl;

    a1 = a2;

    cout << "\na1指向的地址为: " << &a1 << endl;
    cout << "a2指向的地址为: " << &a2 << endl;
    cout << "a1 == a2? " << (a1 == a2) << endl;

    return 0;
}
a1指向的地址为: 0x61fe1c
a2指向的地址为: 0x61fe18
a1 == a2? 0

a1指向的地址为: 0x61fe1c
a2指向的地址为: 0x61fe18
a1 == a2? 1

对于没有重写拷贝构造函数赋值运算符的c++类来说

案例2

#include <iostream>
#include <cstring>

using namespace std;

class Apple{
public:
    char *name_;
    double price_;

    Apple(const char * n, double price){
        int len = strlen(n);
        name_ = new char[len + 1];
        strcpy(name_, n);
        price_ = price;
    }

    ~Apple(){
        delete [] name_;
    }
};

int main(){
    Apple a1("FuShi", 3.25);
    Apple a2("HongXiangjiao", 5.38);

    cout << "a1指向的地址为: " << &a1 << endl;
    cout << "a2指向的地址为: " << &a2 << endl;
    cout << "a1.name_指向的地址为: " << (void*)a1.name_ << endl;
    cout << "a2.name_指向的地址为: " << (void*)a2.name_ << endl;
    cout << "a1.name_为: " << a1.name_ << endl;
    cout << "a2.name_为: " << a2.name_ << endl;

    return 0;
}
a1指向的地址为: 0x61fd30
a2指向的地址为: 0x61fd20
a1.name_指向的地址为: 0xf71760
a2.name_指向的地址为: 0xf71780
a1.name_为: FuShi
a2.name_为: HongXiangjiao

这里注意一点,我如果想查看指针name_所指向的地址,直接使用cout << a1.name_是不行的,因为cout流中重定义导致cout << a1.name_实际上输出的是a1.name_的字符串,而不是a1.name_的内容。不过通过强制类型转换就可以看到a1.name_的内容了。cout << (void*)a1.name_即可。这里的void*是空类型指针,它不指向任何类型,即void仅仅是一个地址,不能进行指针运算,也不能进行间接引用。允许其他类型指针赋值给它,不准它赋值给其他类型指针。’

案例3

int main(){
    Apple a1("FuShi", 3.25);
    Apple a2(a1);

    cout << "a1指向的地址为: " << &a1 << endl;
    cout << "a2指向的地址为: " << &a2 << endl;
    cout << "a1.name_指向的地址为: " << (void*)a1.name_ << endl;
    cout << "a2.name_指向的地址为: " << (void*)a2.name_ << endl;
    cout << "a1.name_为: " << a1.name_ << endl;
    cout << "a2.name_为: " << a2.name_ << endl;

    return 0;
}
a1指向的地址为: 0x61fd30
a2指向的地址为: 0x61fd20
a1.name_指向的地址为: 0x1e1760
a2.name_指向的地址为: 0x1e1760
a1.name_为: FuShi
a2.name_为: FuShi

Process finished with exit code -1073740940 (0xC0000374)

浅拷贝深拷贝

浅拷贝:如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,就是浅拷贝。

深拷贝:如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝。

这里使用了拷贝构造函数,其拷贝方式是浅拷贝。下图说明了该构造函数执行的操作。

go java rust高并发性能对比_go java rust高并发性能对比

但是,代码这么写会出现经典的"double free"错误,原因在于两个对象的name_指针现在指向了同一块内存区域,当两个变量离开作用域时,分别调用了析构函数。

案例4

int main(){
    Apple a1("FuShi", 3.25);
    Apple a2("HuangYuanshuai", 5.38);

    cout << "a1指向的地址为: " << &a1 << endl;
    cout << "a2指向的地址为: " << &a2 << endl;
    cout << "a1.name_指向的地址为: " << (void*)a1.name_ << endl;
    cout << "a2.name_指向的地址为: " << (void*)a2.name_ << endl;
    cout << "a1.name_为: " << a1.name_ << endl;
    cout << "a2.name_为: " << a2.name_ << endl;

    a2 = a1;

    cout << "a1指向的地址为: " << &a1 << endl;
    cout << "a2指向的地址为: " << &a2 << endl;
    cout << "a1.name_指向的地址为: " << (void*)a1.name_ << endl;
    cout << "a2.name_指向的地址为: " << (void*)a2.name_ << endl;
    cout << "a1.name_为: " << a1.name_ << endl;
    cout << "a2.name_为: " << a2.name_ << endl;

    return 0;
}
a1指向的地址为: 0x61fd30
a2指向的地址为: 0x61fd20
a1.name_指向的地址为: 0x1e1760
a2.name_指向的地址为: 0x1e1780
a1.name_为: FuShi
a2.name_为: HuangYuanshuai
a1指向的地址为: 0x61fd30
a2指向的地址为: 0x61fd20
a1.name_指向的地址为: 0x1e1760
a2.name_指向的地址为: 0x1e1760
a1.name_为: FuShi
a2.name_为: FuShi

Process finished with exit code -1073740940 (0xC0000374)

在执行a2 = a1之后,由于我没有重载赋值函数,因此这里调用了c++默认的赋值函数。二者虽然打印出的值相同,但所指向的并不是同一个地方。根据《C++ Primer Plus》的说法,在默认情况下,将一个对象赋给同类型的另一个对象时,C++将源对象的每个数据成员的内容复制到目标对象中相应的数据成员中。

下图说明了赋值函数执行的操作。

go java rust高并发性能对比_Apple_02

c++默认的拷贝构造函数和赋值函数都是浅拷贝,除非显式的重写这两个函数。

Rust

对于在stack上存储的数据,这么写是没有问题的:

fn main() {
    let a1 = 5;
    let a2 = a1;

    println!("{}", a1);
}

对于在heap上存储的数据

fn main() {
    let s1 = String::from("hello world!");
    let s2 = s1;

    println!("{}", s1);
}

如果这么写。。编译器直接就报错了

--> src\main.rs:5:20
  |
2 |     let s1 = String::from("hello world!");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 | 
5 |     println!("{}", s1);
  |                    ^^ value borrowed here after move

这是因为,在堆上的数据rust对其进行的是类似浅拷贝的东西,rust叫做move(移动)。它不仅像浅拷贝一样只拷贝指针不拷贝数据,而且同时使第一个变量(s1)无效了。如若不然,当变量离开它的作用域后将被drop函数释放,这里将会被释放两次造成double free错误。

go java rust高并发性能对比_c++_03