复制构造函数被自动调用的时机
对象参数传参时
示例代码
#include <stdio.h>
#include <string.h>
class Person {
public:
Person() {
name = NULL;//无参构造函数,初始化指针
}
Person(const Person& obj) {
// 注:如果在拷贝构造函数中直接复制指针值,那么对象内的两个成员指针会指向同一个资源,这属于浅拷贝
// this->name = obj.name;
// 为实参对象中的指针所指向的堆空间制作一份副本,这就是深拷贝了
int len = strlen(obj.name);
this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
strcpy(this->name, obj.name);
}
~Person(){ // 析构函数,释放资源
if (name != NULL) {
//如果使用浅拷贝,执行到这里会产生错误,因为源对象和复制的对象在作用域结束时会调用到此处,所以会产生同一个资源释放两次的错误。
delete[] name;
name = NULL;
}
}
void setName(const char* name) {
int len = strlen(name);
if (this->name != NULL) {
delete [] this->name;
}
this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
strcpy(this->name, name);
}
public:
char * name;
};
void show(Person person){ // 参数是对象类型,会触发拷贝构造函数
printf("name:%s\n", person.name);
}
int main(int argc, char* argv[]) {
Person person;
person.setName("Hello");
show(person);
return 0;
}
汇编
mov [rsp+arg_8], rdx
mov [rsp+arg_0], ecx
sub rsp, 48h
lea rcx, [rsp+48h+var_20]
call String_constructor ; Microsoft VisualC v14 64bit runtime
; Microsoft VisualC 64bit universal runtime
lea rdx, aHello ; "Hello"
lea rcx, [rsp+48h+var_20]
call String_set
lea rax, [rsp+48h+var_10]
mov [rsp+48h+var_18], rax
lea rdx, [rsp+48h+var_20]
mov rcx, [rsp+48h+var_18]
call String_constructor_copy
mov rcx, rax
call show
mov [rsp+48h+var_28], 0
lea rcx, [rsp+48h+var_20]
call String_dectructor
mov eax, [rsp+48h+var_28]
add rsp, 48h
retn
上面对象var_20的地址被作为参数,被var_18的构造函数调用,然后var_18的地址作为show的参数,但是实际这里用c++代码只有下面这个一句
show(person);
对象值传递会先调用复制构造函数初始化一个临时对象(参数对象),然后将这个临时对象的地址传入目标函数(show)使用,注意这个参数对象的生存周期只存在与目标函数(show),虽然对象的位置是在上一层(main)的栈帧中。
看show的汇编代码
mov [rsp+arg_0], rcx
sub rsp, 28h
mov rax, [rsp+28h+arg_0]
mov rdx, [rax]
lea rcx, Format ; "name:%s\n"
call printf
mov rcx, [rsp+28h+arg_0]
call String_dectructor
add rsp, 28h
retn
在show函数中直接把参数对象析构了
如果没有定义复制构造函数,就会直接内存浅拷贝。
综上所述,对象在传参之前会调用内存拷贝函数复制一份对象,然后将该对象的地址赋值给函数,并在函数内析构。
返回对象时
示例代码
#include <stdio.h>
#include <string.h>
class Person {
public:
Person() {
name = NULL;//无参构造函数,初始化指针
printf("ctor\n");
}
Person(const Person& obj) {
// // // 注:如果在拷贝构造函数中直接复制指针值,那么对象内的两个成员指针会指向同一个资源,这属于浅拷贝
this->name = obj.name;
// // // 为实参对象中的指针所指向的堆空间制作一份副本,这就是深拷贝了
int len = strlen(obj.name);
this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
strcpy(this->name, obj.name);
printf("copy ctor\n");
}
~Person(){ // 析构函数,释放资源
if (name != NULL) {
//如果使用浅拷贝,执行到这里会产生错误,因为源对象和复制的对象在作用域结束时会调用到此处,所以会产生同一个资源释放两次的错误。
delete[] name;
name = NULL;
}
}
void setName(const char* name) {
int len = strlen(name);
if (this->name != NULL) {
delete [] this->name;
}
this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
strcpy(this->name, name);
}
public:
char * name;
};
Person getObject() {
Person person; //定义局部对象
person.setName("Hello");
return person;
}
int main(int argc, char* argv[]) {
// Person person;
// person = getObject();
getObject();
return 0;
}
main
mov [rsp+arg_8], rdx
mov [rsp+arg_0], ecx
sub rsp, 38h
lea rcx, [rsp+38h+var_18]
call getObject
lea rcx, [rsp+38h+var_18]
call Person_dtor
xor eax, eax
add rsp, 38h
retn
getObject
mov [rsp+arg_0], rcx
sub rsp, 38h
lea rcx, [rsp+38h+var_18]
call Person_ctor
lea rdx, aHello ; "Hello"
lea rcx, [rsp+38h+var_18]
call Person_setName
lea rdx, [rsp+38h+var_18]
mov rcx, [rsp+38h+arg_0]
call Person_ctor_copy
lea rcx, [rsp+38h+var_18]
call Person_dtor
mov rax, [rsp+38h+arg_0]
add rsp, 38h
retn
可以看到在main函数中,编译器分配了一个临时对象,将它的地址传入getObject,并在getObject调用了赋值构造函数,并返回了该临时对象的地址,该临时对象在main函数中执行完getObject后立即析构
有一种特殊情况,不会产生临时对象,或者说将临时对象的生命周期延长
mian函数c++代码
int main(int argc, char* argv[]) {
Person person = getObject();
person.setName("你好");
return 0;
}
main函数汇编代码
mov [rsp+arg_8], rdx
mov [rsp+arg_0], ecx
sub rsp, 38h
lea rcx, [rsp+38h+var_10]
call getObject
lea rdx, asc_1400132F8 ; "你好"
lea rcx, [rsp+38h+var_10]
call Person_setName
mov [rsp+38h+var_18], 0
lea rcx, [rsp+38h+var_10]
call Person_dtor
mov eax, [rsp+38h+var_18]
add rsp, 38h
retn
可以看到临时对象在getObject结束以后并没有立即析构,而是在main函数结束以后才析构
综上所述,若编译器检测到返回对象时,函数内部会自动生成对象指针参数,并使用对象指针参数调用复制构造函数,并将该指针返回,而外部则分配一个临时对象,并在调用完后,立即析构,若有变量在声明时承接函数返回结果,则将该变量当作临时变量,生命周期和正常的局部对象一样
对象赋值时
代码示例
#include <stdio.h>
#include <string.h>
class Person {
public:
Person() {
name = NULL;//无参构造函数,初始化指针
printf("ctor\n");
}
Person(const Person& obj) {
// // // 注:如果在拷贝构造函数中直接复制指针值,那么对象内的两个成员指针会指向同一个资源,这属于浅拷贝
this->name = obj.name;
// // // 为实参对象中的指针所指向的堆空间制作一份副本,这就是深拷贝了
int len = strlen(obj.name);
this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
strcpy(this->name, obj.name);
}
~Person(){ // 析构函数,释放资源
if (name != NULL) {
//如果使用浅拷贝,执行到这里会产生错误,因为源对象和复制的对象在作用域结束时会调用到此处,所以会产生同一个资源释放两次的错误。
delete[] name;
name = NULL;
}
}
void setName(const char* name) {
int len = strlen(name);
if (this->name != NULL) {
delete [] this->name;
}
this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
strcpy(this->name, name);
}
//void operator = (Person & obj){};
public:
char * name;
};
Person getObject() {
Person person; //定义局部对象
person.setName("Hello");
return person;
}
int main(int argc, char* argv[]) {
Person person;
person.setName("你好");
Person person2;
person2 =person;
printf("%x\n",person.name);
printf("%x\n",person2.name);
return 0;
}
对象赋值时有两种情况,一种是会自动调用赋值构造函数,一种会调用operator =函数,由于这两个成员函数的默认代码是一样的浅复制内存,所以非常容易搞混
自动调用赋值构造函数
main c++
int main(int argc, char* argv[]) {
Person person;
person.setName("你好");
Person person2 = person;
printf("%x\n",person.name);
printf("%x\n",person2.name);
return 0;
}
汇编
push ebp
mov ebp, esp
sub esp, 0Ch
lea ecx, [ebp+person]
call Person_ctor
push offset asc_412168 ; "你好"
lea ecx, [ebp+person]
call Person_setName
lea eax, [ebp+person]
push eax
lea ecx, [ebp+person2]
call Person_ctor_copy
mov ecx, [ebp+person]
push ecx
push offset Format ; "%x\n"
call _printf
add esp, 8
mov edx, [ebp+person2]
push edx
push offset asc_412174 ; "%x\n"
call _printf
add esp, 8
mov [ebp+var_C], 0
lea ecx, [ebp+person2]
call Person_dtor
lea ecx, [ebp+person]
call Person_dtor
mov eax, [ebp+var_C]
mov esp, ebp
pop ebp
retn
可以看到,当对象声明的时候进行对象赋值时,此时会自动调用复制构造函数
自动调用operator =
main c++
int main(int argc, char* argv[]) {
Person person;
person.setName("你好");
Person person2;
person2 =person;
printf("%x\n",person.name);
printf("%x\n",person2.name);
return 0;
}
汇编
push ebp
mov ebp, esp
sub esp, 0Ch
lea ecx, [ebp+person]
call Person_ctor
push offset asc_412168 ; "你好"
lea ecx, [ebp+person]
call Person_setName
lea ecx, [ebp+person2]
call Person_ctor
lea eax, [ebp+person]
push eax
lea ecx, [ebp+person2]
call operator等于
mov ecx, [ebp+person]
push ecx
push offset Format ; "%x\n"
call _printf
add esp, 8
mov edx, [ebp+person2]
push edx
push offset asc_412174 ; "%x\n"
call _printf
add esp, 8
mov [ebp+var_C], 0
lea ecx, [ebp+person2]
call Person_dtor
lea ecx, [ebp+person]
call Person_dtor
mov eax, [ebp+var_C]
mov esp, ebp
pop ebp
retn
可以看到,person和person2构造之后,person2就调用了operator = 函数
综上所述,单纯的对象赋值并不会触发复制构造函数,而是触发了operator = 函数,只有对象声明时发生复制才会触发复制构造函数