1 拷贝构造和赋值运算符
1.1 拷贝构造
拷贝构造在如下场景会被调用:
(1)函数调用时,函数参数是对象的值传递
(2)声明对象同时初始化的时候(而不是声明和初始化分开,因为声明的时候就创建了对象)
(3)函数返回的时候,返回对象的值。
第 3 种情况,默认情况下有返回值优化,不会调用拷贝构造函数。
通过编译参数 -fno-elide-constructors 可以禁用返回值优化。
注:
拷贝构造函数的形参必须是引用传递,如果是值传递的话,那么在传递过程中也会调用拷贝构造函数,这样就造成递归调用。
如果是值传递,会有编译错误。
#include <iostream>
#include <string>
class Test {
public:
Test(int x) {
a_ = x;
std::cout << "Test(), this = " << this << std::endl;
};
Test(Test &test) {
std::cout << "Test() copy constructor, this = " << this << ", &test = " << &test << std::endl;
}
~Test() {
std::cout << "~Test(), this = " << this << std::endl;
}
int a_;
};
void func1(Test test) {
std::cout << "func1, &test = " << &test << std::endl;
}
Test func2() {
Test t(2);
std::cout << "func2, &t = " << &t << std::endl;
return t;
}
int main() {
std::cout << "before t1" << std::endl;
Test t1(1);
std::cout << "&t1 = " << &t1 << std::endl;
std::cout << std::endl;
std::cout << "before func1" << std::endl;
// 函数调用,传参是对象的值传递,拷贝构造
func1(t1);
std::cout << std::endl;
std::cout << "before t2" << std::endl;
// 声明对象的时候使用另一个对象初始化,拷贝构造
Test t2 = t1;
std::cout << "&t1 = " << &t1 << ", &t2 = " << &t2 << std::endl;
std::cout << std::endl;
std::cout << "before func2" << std::endl;
// 函数返回值是返回对象的值,拷贝构造
Test t4 = func2();
std::cout << "&t4 = " << &t4 << std::endl;
std::cout << std::endl;
std::cout << "before return" << std::endl;
return 0;
}
编译命令:
g++ copy1.cpp -fno-elide-constructors
运行结果:
1.2 赋值运算符
赋值运算符,也是通过 = 赋值的时候调用。前提是 = 左边的对象已经构建了。
赋值运算符返回的是当前对象的引用,因为赋值运算符并没有创建新的对象,所以 = 左边的对象仍然指向原来的位置。
#include <iostream>
#include <string>
class Test {
public:
Test(int x) {
a_ = x;
std::cout << "Test(), this = " << this << std::endl;
};
Test(Test &test) {
std::cout << "Test() copy constructor, this = " << this << ", &test = " << &test << std::endl;
}
Test& operator=(const Test &test) {
std::cout << "Test() operator=, this = " << this << ", &test = " << &test << std::endl;
return *this;
}
int a_;
};
void func1(Test test) {
std::cout << "func1" << std::endl;
}
Test func2() {
Test t(2);
std::cout << "func2" << std::endl;
return t;
}
int main() {
std::cout << "before t1" << std::endl;
Test t1(1);
std::cout << std::endl;
std::cout << "before t2" << std::endl;
Test t2(10);
std::cout << "before t2 = t1" << std::endl;
// 赋值运算符
t2 = t1;
std::cout << "&t2 = " << &t2 << std::endl;
std::cout << std::endl;
std::cout << "before func2" << std::endl;
Test t3(3);
// 赋值运算符
// 默认情况下开启了返回值优化
// 在 func2 中构造的对象会直接通过赋值运算符赋值给 t3
// 如果禁用返回值优化,那么下边这句代码调用了拷贝构造函数,也调用了赋值运算符
t3 = func2();
std::cout << "&t3 = " << &t3 << std::endl;
std::cout << std::endl;
std::cout << "before return" << std::endl;
return 0;
}
1.3 拷贝构造和赋值运算符的区别
是否有新对象产生,有新对象产生则是拷贝构造,否则是赋值运算符。
Test t1(10); Test t2(20); t1 = t2; | 赋值运算符,t1 已经存在了,已经构造过了,只不过重新赋值。 没有产生新对象,所以调用的是赋值运算符。 |
Test t1(10); Test t2 = t1; | 拷贝构造,t2 之前还没有存在,在执行 Test t2 = t1 的时候才构造出来一个新的。 产生了新的对象,所以调用了拷贝构造。 |
2 深拷贝和浅拷贝
浅拷贝和深拷贝在有指针成员的时候需要注意。
如果拷贝的时候只把指针进行了拷贝,两个对象内的指针指向同一个内存块,这就是浅拷贝,浅拷贝,两个对象会相互影响。如果拷贝的时候,重新申请了内存,然后把指针指向的数据进行了拷贝,那就是深度拷贝。
直观理解的话,实际开发中,绝大多数情况下都应该使用深拷贝,避免使用浅拷贝,因为浅拷贝使用不当很容易导致问题。
2.1 浅拷贝
如下代码, Test 类中有一个指针 int *pi_,可以认为是一个 int 数组。在构造函数中如果没有指定数组长度那就申请 8 个长度,如果指定了,那申请指定的长度。
类中没有自己写拷贝构造函数,也没有自己实现赋值运算符,当自己没有指定的时候,编译器会自动生成拷贝构造函数和赋值构运算符。
#include <iostream>
#include <string>
class Test {
public:
Test(unsigned int length = 0) {
std::cout << "Test(), this = " << this << ", pi_ = " << pi_ << std::endl;
if (length == 0) {
pi_ = (int *)malloc(8 * sizeof(int));
length_ = 8;
} else {
pi_ = (int *)malloc(length * sizeof(int));
length_ = length;
}
std::cout << "pi_ = " << pi_ << std::endl;
};
/*
Test(Test &test) {
std::cout << "Test() copy constructor, this = " << this << ", &test = " << &test << std::endl;
std::cout << "pi_ = " << pi_ << ", test length = " << test.length_ << std::endl;
pi_ = (int *)malloc(test.length_ * sizeof(int));
length_ = test.length_;
std::cout << "after copy, pi_ = " << pi_ << ", test length = " << test.length_ << std::endl;
}
~Test() {
std::cout << "~Test(), this = " << this << ", pi_ = " << pi_ << ", length_ = " << length_ << std::endl;
if (pi_) {
free(pi_);
}
}
*/
int *pi_ = nullptr;
unsigned int length_ = 0;
};
void func1(Test test) {
std::cout << "func1, &test = " << &test << std::endl;
}
Test func2() {
Test t(2);
std::cout << "func2, &t = " << &t << std::endl;
return t;
}
int main() {
std::cout << "before t1" << std::endl;
Test t1(1);
*t1.pi_ = 100;
std::cout << "&t1 = " << &t1 << ", t1 data p = " << t1.pi_ << std::endl;
std::cout << std::endl;
Test t2 = t1;
std::cout << "t2 data p = " << t2.pi_ << std::endl;
Test t3(1);
*t3.pi_ = 200;
std::cout << "before assign, t3 data p = " << t3.pi_ << ", data = " << *t3.pi_ << ", t1 data = " << *t1.pi_ << std::endl;
t3 = t1;
std::cout << "after assign, t3 data p = " << t3.pi_ << ", data = " << *t3.pi_ << std::endl;
std::cout << "before return" << std::endl;
return 0;
}
代码编译之后,运行结果如下。
(1)拷贝构造
t2 是使用拷贝构造构造出来的对象,打印了 t2 中 pi_,可以看到 t2 中的 pi_ 和 t1 中的 pi_ 是相同的。也就是说默认情况下的拷贝构造是浅拷贝。
(2)赋值运算符
t3 被 t1 赋值前后,打印 t3 的 pi_,赋值前后, t3 中的 pi_ 是不相同的,赋值之后 t3 中的 pi_ 与 t1 中的 pi_ 是相同的。所以赋值运算符也是只把指针进行了赋值,没有对内存中的值进行赋值。
2.2 深拷贝
如下代码,自己实现了拷贝构造函数和赋值运算符,申请了空间,并把内存区域中的数据进行了复制。
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
class Test {
public:
Test(unsigned int length = 0) {
std::cout << "Test(), this = " << this << ", pi_ = " << pi_ << std::endl;
if (length == 0) {
pi_ = (int *)malloc(8 * sizeof(int));
length_ = 8;
} else {
pi_ = (int *)malloc(length * sizeof(int));
length_ = length;
}
std::cout << "pi_ = " << pi_ << std::endl;
};
Test(Test &test) {
std::cout << "Test() copy constructor, this = " << this << ", &test = " << &test << std::endl;
std::cout << "pi_ = " << pi_ << ", test length = " << test.length_ << std::endl;
pi_ = (int *)malloc(test.length_ * sizeof(int));
length_ = test.length_;
memcpy(pi_, test.pi_, test.length_ * sizeof(int));
std::cout << "after copy, pi_ = " << pi_ << ", test length = " << test.length_ << std::endl;
}
Test& operator=(const Test &test) {
std::cout << "Test() operator=, this = " << this << ", &test = " << &test << std::endl;
if (pi_) {
free(pi_);
}
pi_ = (int *)malloc(test.length_ * sizeof(int));
if (pi_) {
length_ = test.length_;
memcpy(pi_, test.pi_, test.length_ * sizeof(int));
}
return *this;
}
~Test() {
std::cout << "~Test(), this = " << this << ", pi_ = " << pi_ << ", length_ = " << length_ << std::endl;
if (pi_) {
free(pi_);
}
}
int *pi_ = nullptr;
unsigned int length_ = 0;
};
void func1(Test test) {
std::cout << "func1, &test = " << &test << std::endl;
}
Test func2() {
Test t(2);
std::cout << "func2, &t = " << &t << std::endl;
return t;
}
int main() {
std::cout << "before t1" << std::endl;
Test t1(1);
*t1.pi_ = 100;
std::cout << "&t1 = " << &t1 << ", t1 data p = " << t1.pi_ << ", data = " << *t1.pi_ << std::endl;
std::cout << std::endl;
Test t2 = t1;
std::cout << "t2 data p = " << t2.pi_ << ", data = " << *t2.pi_ << std::endl;
std::cout << std::endl;
Test t3(1);
*t3.pi_ = 200;
std::cout << "before assign, t3 data p = " << t3.pi_ << ", data = " << *t3.pi_ << ", t1 data = " << *t1.pi_ << std::endl;
t3 = t1;
std::cout << "after assign, t3 data p = " << t3.pi_ << ", data = " << *t3.pi_ << std::endl;
std::cout << "before return" << std::endl;
return 0;
}
t2 是调用拷贝构造函数,通过 t1 拷贝构造而来。可以看到拷贝构造之后,t2 中成员指向的内存和 t1 是不同的,内存中的值是相同的,所以是深度拷贝。
t3 被 t1 通过赋值运算符赋值,赋值之后,t3 指针指向的内存的值 与 t1 保持一致,所以是深度赋值。
2.3 派生类拷贝构造调用父类拷贝构造
#include <iostream>
class Base {
public:
Base() : i(0) {
std::cout << "Base()" << std::endl;
}
Base(int n) : i(n) {
std::cout << "Base(int), i = " << i << std::endl;
}
Base(Base &b) : i(b.i) {
std::cout << "Base(Base &), i = " << i << std::endl;
}
private:
int i;
};
class Derived : public Base {
public:
Derived() : Base(0), j(0) {
std::cout << "Derived()" << std::endl;
}
Derived(int m, int n) : Base(m), j(n) {
std::cout << "Derived(int)" << std::endl;
}
Derived(Derived &d) : Base(d), j(d.j) {
std::cout << "Derived(Derived &)" << std::endl;
}
private:
int j;
};
int main() {
Base b(1);
Derived d(2, 3);
std::cout << "----------------" << std::endl;
Derived d1(d);
std::cout << "----------------" << std::endl;
return 0;
}