示例代码
#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
可以看到,当对象声明的时候进行对象赋值时,此时会自动调用复制构造函数
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 = 函数,只有对象声明时发生复制才会触发复制构造函数