第九章 拷贝构造函数、深拷贝、浅拷贝、匿名对象、隐式构造、默认构造函数
拷贝构造函数(Copy Constructor)
- 拷贝构造函数是构造函数的一种
- 当利用已存在的对象创建一个新对象时(类似于拷贝),就会调用新对象的拷贝构造函数进行初始化
- 拷贝构造函数的格式是固定的,接收一个const引用作为参数
-
默认拷贝的本质
class Car { public: int m_price; int m_length; Car( int price = 0,int length =0) :m_price(price),m_length(length) { cout << "Car( int price = 0,int length =0)" << endl; } void display() { cout << "void display()"<<"-m_price-"<<m_price<<"-m_length-"<<m_length << endl; } } int main() { Car car1; Car car2(100); Car car3(100, 5); //利用已经存在的car3对象创建了一个car4新对象 //car4初始化时会调用拷贝构造函数 //没有手动实现拷贝函数的话,默认也会拷贝操作 Car car4(car3); //打印拷贝成功void display() - m_price - 100 - m_length - 5 car4.display(); getchar(); return 0; }
-
将
Car car4(car3);
反汇编本质如下:mov eax,dword ptr [ebp-30h] mov dword ptr [ebp-40h],eax mov ecx,dword ptr [ebp-2Ch] mov dword ptr [ebp-3Ch],ecx
- ebp-30h是car3的首地址,也是car3第一个成员变量的地址;ebp-2Ch是car3的第二个成员变量的地址
- ebp-40h是car4的首地址,也是car4第一个成员变量的地址;ebp-3Ch是car4的第二个成员变量的地址
-
上面默认的本质是
car4.m_length = car3.m_length; car4.m_price = car3.m_price;
-
-
手动实现拷贝函数
class Car { public: int m_price; int m_length; Car( int price = 0,int length =0) :m_price(price),m_length(length) { cout << "Car( int price = 0,int length =0)" << endl; } //手动实现拷贝构造函数 //需要手动实现拷贝操作 Car(const Car &car) :m_price(car.m_price),m_length(car.m_length){ /*m_price = car.m_price; m_length = car.m_length;*/ cout << "Car(const Car &car) " << endl; } void display() { cout << "void display()"<<"-m_price-"<<m_price<<"-m_length-"<<m_length << endl; } }; int main() { Car car3(100, 5); //利用已经存在的car3对象创建了一个car4新对象 //car4初始化时会调用拷贝构造函数 //手动实现拷贝构造函数 Car car4(car3); //打印的值是乱码:void display() - m_price--858993460 - m_length--858993460 //因此,一旦手动实现拷贝函数,那么就需要自己手动完成拷贝操作 car4.display(); getchar(); return 0; }
- 总结:
- C++默认就有拷贝函数,而且默认拷贝就是将旧对象的成员变量赋值给新的成员变量
- 一旦手动实现拷贝构造函数,那么默认就无效,需要手动实现拷贝
- 但是我们发现上述的手动实现拷贝构造好像多此一举,因此,当对象的成员变量是基本数据类型时,不需要手动实现构造函数。
调用父类的拷贝构造函数
class Person {
public:
int m_age;
Person(int age = 0):m_age(age){}
//拷贝构造
Person(const Person &person):m_age(person.m_age){}
};
class Student : public Person {
public:
int m_score;
Student(int age = 0,int score = 0):Person(age),m_score(score){}
//拷贝构造
//初始化列表中直接调用父类的拷贝构造函数Person(student),来拷贝父类的成员变量
Student(const Student &student) :Person(student), m_score(student.m_score) {}
};
//使用
Student student(18, 100);
Student student2(student);
cout << student.m_age << endl;
cout << student2.m_age << endl;
- 如何拷贝父类的成员?如何调用父类的拷贝构造函数?
- 在子类拷贝构造的初始化列表中直接调用父类的拷贝构造函数。
深拷贝、浅拷贝
- 成员变量为字符串的写法
- strcpy过期问题解决
- 方法一: 在写代码的前面加上如下宏定义:
#define _CRT_SECURE_NO_WARNINGS
- 注意:一定要写在最前面!!!
- 方法二: 操作 vs 中,在项目 -> 属性 -> C/C++ -> 预处理器 -> 预处理器定义中添加 _CRT_SECURE_NO_WARNINGS 这个预定义。
- 方法一: 在写代码的前面加上如下宏定义:
- C++中字符串常量必须用const修饰
- 函数的形参接收字符串时,必须也用const修饰,否则实参为字符串常量传入时会报错
-
代码示例:
class Car { int m_price; char *m_name; public: //这里形参必须用const修饰,否则外部创建直接传入字符串常量,会报错不匹配,而且const形参也可以接收字符串变量 Car(int price = 0, const char *name = NULL) :m_price(price) { //但是name是const,而成员m_name是非const,很显然不能直接赋值,怎么办呢? //m_name = name; if (name == NULL)return; //申请堆空间存储字符串的内容 this->m_name = new char[strlen(name) + 1]{}; //拷贝字符串内容到堆空间 strcpy(this->m_name, name); cout << "Car(int price = 0, char *name = NULL)" << endl; } ~Car(){ if (this->m_name == NULL) return; delete[] this->m_name; this->m_name = NULL; } void display() { cout << "price-" << m_price << "name-" << m_name << endl; } }; int main() { //定义字符串 //数组方法定义 char name[] = { 'b','m','w','\0' }; //因为“bmw”是一个字符串常量,因此在C++中必须用const修饰 const char *name2 = "bmw"; cout << name << endl; //strlen函数不包含\0字符 cout << strlen(name) << endl; Car *car = new Car(100, "bwm"); car->display(); }
- strcpy过期问题解决
- 浅拷贝
-
示例:
//car类跟上面一模一样 //使用 int main() { Car car1(100,"bwm"); //拷贝,将car1的内存空间复制给car2的内存空间,8个字节 //即car1、car2的m_price/m_name完全一样 //这么做的话,car1的m_name跟car2的m_name指向同一的堆空间 //导致结果:1. 一旦任何一个car的m_name被修改,另外一个一定也被修改 2. m_name指向的堆空间会被多次释放 Car car2 = car1; getchar(); return 0; }
-
Car car2 = car1;
意义:- 拷贝,将car1的内存空间复制给car2的内存空间,8个字节
- 即car1、car2的m_price/m_name完全一样
- 这么做的话,car1的m_name跟car2的m_name指向同一的堆空间
- 导致结果:
- 一旦任何一个car的m_name被修改,另外一个一定也被修改
- m_name指向的堆空间会被多次释放
- 这个就是浅拷贝
-
- 深拷贝
-
示例:
class Car { int m_price; char *m_name; public: //这里形参必须用const修饰,否则外部创建直接传入字符串常量,会报错不匹配,而且const形参也可以接收字符串变量 Car(int price = 0, const char *name = NULL) :m_price(price) { //但是name是const,而成员m_name是非const,很显然不能直接赋值,怎么办呢? //m_name = name; if (name == NULL)return; //申请堆空间存储字符串的内容 this->m_name = new char[strlen(name) + 1]{}; //拷贝字符串内容到堆空间 strcpy(this->m_name, name); cout << "Car(int price = 0, char *name = NULL)" << endl; } //深拷贝:因为涉及到内存分配,因此要实现拷贝构造函数 Car(const Car &car) :m_price(car.m_price) { if (car.m_name == NULL)return; //申请堆空间存储字符串的内容 this->m_name = new char[strlen(car.m_name) + 1]{}; //拷贝字符串内容到堆空间 strcpy(this->m_name, car.m_name); cout << "Car(const Car &car) :m_price(car.m_price) " << endl; } ~Car(){ if (this->m_name == NULL) return; delete[] this->m_name; this->m_name = NULL; cout << "~Car() " << endl; } }; //使用: int main() { { Car car1(100, "bwm"); //对象初始化 Car car2 = car1; Car car3(car2); //本质是调用默认参数构造函数:car1(0, NULL); Car car4; //变量赋值 car4 = car3; } getchar(); return 0; }
-
总结:
- 实现拷贝构造后:
- car1/car2的m_price一样,但是m_name不一样,他们分别指向不同的堆空间。
- car1/car2释放时分别释放自己的堆内存
- car2、car3都是通过拷贝构造函数初始化的,car、car4是通过非拷贝构造函数初始化
- car4 = car3是一个赋值操作(默认是浅复制),并不会调用拷贝构造函数
- 注意:与
Car car2 = car1;
的区别,前者是对象赋值,后者是对象初始化。 - 构造函数:顾名思义只跟创建(初始化)对象有关
- 注意:与
- 实现拷贝构造后:
-
- 深拷贝、浅拷贝总结
- 编译器默认的提供的拷贝是浅拷贝(shallow copy)
- 将一个对象中所有成员变量的值拷贝到另一个对象
- 如果某个成员变量是个指针,只会拷贝指针中存储的地址值,并不会拷贝指针指向的内存空间
- 可能会导致堆空间多次free的问题
- 如果需要实现深拷贝(deep copy),就需要自定义拷贝构造函数
- 将指针类型的成员变量所指向的内存空间,拷贝到新的内存空间
- 编译器默认的提供的拷贝是浅拷贝(shallow copy)
对象型参数和返回值
- 使用对象类型作为函数的参数或者返回值,可能会产生一些不必要的中间对象
-
示例代码:
class Car { int m_price; public: Car(int price = 0) :m_price(price) { cout << "Car(int)-" <<this<<"-"<<this-m_price<< endl; } Car(const Car &car) :m_price(car.m_price) { cout << "Car(const Car)-" << this << "-" << this - m_price << endl; } }; //对象为参数 void test1(Car car) {} //对象为返回值 Car test2() { Car car(10); return car; }
-
对象作为参数:
//使用 Car car1(10); test1(car1);
-
打印:
Car(int)-003AF9C0-003AF998 //Car car1(10);调用的构造函数 Car(const Car) - 003AF8E0 - 003AF8B8 //test1(car1);调用的?
test1(car1);
为何会调用拷贝构造呢?-
test1(car1);
就相当于void test1(Car car = car1) {}
- 那么
Car car = car1
参数就相当于调用拷贝构造函数,此时的car就是一个新的对象,而不是原来的旧对象,我们通常是为了改变旧对象的值,那么怎么办呢? -
解决办法:形参传引用
void test1(Car &car) {}
- 那么
-
-
对象作为返回值
-
使用1:
Car car = test2();
-
打印:
Car(int)-001EFA9C-001EFA74 //test2()内部创建对象 Car(const Car) - 001EFB84 - 001EFB5C //Car car = test2创建的对象,会调用拷贝
-
test2创建一个临时对象然后拷贝给外部对象
-
-
使用2:
Car car2 ; car2 = test2();
-
打印:
Car(int) - 0041FD50 - 0041FD50 //Car car2 ;调用 Car(int) - 0041FC5C - 0041FC34 // test2(); 调用 Car(const Car) - 0041FC84 - 0041FC5C //这个呢???
car2 = test2();
这个是赋值,不是拷贝呀?为何会调用拷贝构造呢?- 因为test2()内部创建的是一个临时对象,当外部赋值时,C++为了安全起见,默认会自动调用拷贝构造,成一个新的对象赋值给外部,旧的对象释放掉
- 说白了就是:car2 = xxx,等号右边是对象的时候是赋值,等号右边是函数的时候是拷贝
-
-
-
匿名对象(临时对象)
-
匿名对象:没有变量名、没有被指针指向的对象,用完后马上调用析构
class Person { public: Person() { cout << "Person()-" << this<< endl; } Person(const Person &person) { cout << "Person(const Person &)-" << this << endl; } ~Person() { cout << "~Person()-" << this << endl; } void display() { cout << "display()-" << this << endl; } }; void test1(Person peroson) {} Person test2() { //直接返回匿名对象 return Person(); } int main() { //匿名对象 //运行打印可知:改对象创建完毕之后立即销毁 //Person(); //Person().display(); //会调用拷贝构造 /*Person person; test1(person);*/ //参数为匿名对象时就不会调用拷贝构造函数了 //test1(Person()); //直接返回匿名对象,也不会调用拷贝构造函数 Person person1; person1 = test2(); getchar(); return 0; }
隐式构造
-
C++中存在隐式构造的现象:某些情况下,会隐式调用单参数(不一定是单参数,也可以是多参数)的构造函数
class Person { int m_age; public: Person() :Person(0){ cout << "Person()-" << this << endl; } Person(int age) : m_age(age) { cout << "Person(int)-" << this << endl; } Person(const Person &person) { cout << "Person(const Person &)-" << this << endl; } void display() { cout << "display()-age is-" << this->m_age << endl; } }; void test1(Person peroson) {} Person test2() { //直接返回一个值 return 70; } int main() { Person person(10); person = 20; person.display(); /* //打印 Person(int)-002BF978 Person(int)-002BF8AC display()-age is-20 会发现调用了2次构造函数Person(int),为什么呢? 第一次,肯定是Person person(10);这个调用的 那么第二次呢? person = 20;这么赋值本来肯定是有错误的,但是为何运行没错呢?-隐式构造,等价于 person = Person(20);//先通过匿名对象创建出来,然后在赋值给person对象 */ //一定要注意区别!!!!!!!!! //呈现方式1(定义一个对象时赋值) Person person2 = 10; //Person(int) //等价于(通过单参数的构造函数创建一个对象) //Person person2(10); //呈现方式2(给一个已经创建的对象赋值) Person person3(10); person3 = 20; //等价于(已存在对象 = (浅拷贝)匿名对象) //person3 = Person(20);//匿名对象,注意哦,类(20) //呈现方式3: //隐式构造针对函数参数为对象 test1(50); //呈现方式4: //隐式构造真的函数返回值为对象 Person person4 = test2(); getchar(); return 0; }
-
可以通过关键字explicit禁止掉隐式构造
explicit Person(int age) : m_age(age) { cout << "Person(int)-" << this << endl; }
- 只要添加了explicit 上面的直接赋值为数字的都会报错
- explicit作用:防止让别人误解-数字直接赋值给对象
编译器自动生成的构造函数
-
很多教材上说:编译器会为每一个类生成一个默认的无参的构造函数,这个结论是不正确的
class Person { public: int m_age; }; int main( /* //这两句话反汇编 Person person; person.m_age = 10; //结果 mov dword ptr [ebp-8],0Ah */ //通过反汇编发现,这一句没有调用任何构造函数 Person person; person.m_age = 10; getchar(); return 0; }
- 但是某些情况下,确实会生成
- C++的编译器在某些特定的情况下,会给类自动生成无参的构造函数,比如
- 成员变量在声明的同时进行了初始化
- 有定义虚函数
- 虚继承了其他类
- 包含了对象类型的成员,且这个成员有构造函数(编译器生成或自定义)
- 父类有构造函数(编译器生成或自定义)
- 总结一下
- 对象创建后,需要做一些额外操作时(比如内存操作、函数调用),编译器一般都会为其自动生成无参的构造函数
-
代码示例:
class Car { public: //有构造函数 Car() { } }; class Person { public: //1.成员变量在声明的同时进行了初始化 //int m_age =0 ; //2. 有定义虚函数 //virtual void test() {} //4. 包含了对象类型的成员,且这个成员有构造函数(编译器生成或自定义) Car car; }; //3. 虚继承了其他类 class Student : virtual public Person { }; int main() { /* //Person person; lea ecx,[ebp-0Ch] call 00A6137F //调用构造函数 //person.m_age = 10; mov dword ptr [ebp-0Ch],0Ah */ //通过反汇编发现,这一句有调用构造函数 Person person; //person.m_age = 10; Student student; getchar(); return 0; }