1.拷贝构造函数
当使用一个对象来初始化另一个同类对象时,编译器将自动生成一个构造函数,这个构造函数称为拷贝构造函数,其格式如下:
CustomClass(const CustomClass & obj);
在之前说过,创建类时,如果没有提供构造函数,那么编译器将会自动提供一个默认的构造函数。
拷贝构造函数也是一样,如果没有提供拷贝构造函数,那么编译器将会提供一个默认的拷贝构造函数。
1.1.调用时机
当程序中使用一个对象初始化一个新对象时,都会调用拷贝构造函数,这种情况包括以下两类:
- 1.显式地使用旧对象初始化新对象。
这种情况非常明显,如:
MyString str1("Hello world.");
MyString str2 = str1;
MyString str3 = MyString(str1);
MyString str4(str1);
MyString *p_str = new MyString(str1);//p_str指向一个匿名MyString对象
- 2.当函数按值传递对象或函数返回对象时.
因此,函数中经常使用引用传递,避免空间和时间的浪费。
2.由拷贝构造函数引发的浅拷贝问题
默认的拷贝构造函数逐个复制非静态成员,复制的是成员的值,称为浅拷贝。因此,如果成员为指针变量时,则容易引发问题,通过如下示例来说明。
这里我们定义一个Student类,如下:
student.h:
#include <iostream>
class Student
{
private:
char *name;
int age;
static int number;
public:
Student();
Student(int age, const char * arr);
~Student();
friend std::ostream & operator<<(std::ostream & os, const Student & stu);
};
student.cpp:
#include <cstring>
#include "student.h"
int Student::number = 0;
Student::Student() {
age = 10;
int len = 5;
name = new char[len + 1];
std::strcpy(name,"None");
number ++;
std::cout << name << " created by Stduent()" << std::endl;
}
Student::Student(int age, const char * arr) {
this->age = age;
int len = std::strlen(arr);
name = new char[len + 1];
std::strcpy(name,arr);
number ++;
std::cout << name << " created by Student(int,char *)" << std::endl;
}
Student::~Student() {
number --;
std::cout << name << " deleted by ~Student()" << std::endl;
std::cout << number << " left." << std::endl;
delete [] name;
}
std::ostream & operator<<(std::ostream & os, const Student & stu) {
os << stu.name << std::endl;
return os;
}
mainstu.cpp:
#include <iostream>
#include "student.h"
int main()
{
Student stu1;
Student stu2(12,"XiaoMing");
Student stu3 = stu2; //调用拷贝构造函数
return 0;
}
这里利用stu2对象来初始化stu3,在运行时,发生错误:
@ubuntu:$ g++ student.h student.cpp mainstu.cpp -o mainstu
@ubuntu:$ ./mainstu
None created by Stduent()
XiaoMing created by Student(int,char *)
XiaoMing deleted by ~Student()
1 left.
deleted by ~Student()
0 left.
*** Error in `./mainstu': double free or corruption (fasttop): 0x00000000010f9050 ***
为何会出现double free or corruption
呢?
由于拷贝构造只是复制成员的值,因此该例中stu2.name和stu3.name就指向同一块内存,当程序结束调用后,析构stu2和stu3时,由于指向同一块内存,导致该区域内存被析构两次。
除以上这种情况外,如下的方法调用也会导致同样问题:
void showStu(const Student stu){
std::cout << stu << std::endl;
}
由于函数按值传递时,也会调用拷贝构造函数,仅仅将实参的值拷贝给了形参,从而导致同一个内存被析构两次。
要解决由拷贝构造引起的浅拷贝问题,就必须显式实现拷贝构造函数,而不是使用由编译器提供的默认拷贝构造了。
Student::Student(const Student & stu) {
if (this == &stu) return;
this->age = stu.age;
this->name = new char[std::strlen(stu.name) + 1];
std::strcpy(this->name,stu.name);
number ++;
}
在拷贝构造函数中,根据实参中数组的大小,重新动态申请了一块内存,从而避免两个对象共用同一个内存。
再次编译并运行程序,程序完整输出:
@ubuntu:$ g++ student.h student.cpp mainstu.cpp -o mainstu
@ubuntu:$ ./mainstu
None created by Stduent()
XiaoMing created by Student(int,char *)
XiaoMing
XiaoMing deleted by ~Student()
2 left.
XiaoMing deleted by ~Student()
1 left.
None deleted by ~Student()
0 left.
3.赋值运算符=
引起的浅拷贝问题
除了拷贝构造函数外,赋值运算符=
也会引起浅拷贝问题,如:
int main()
{
//...
Student stu4;
stu4 = stu3;//调用赋值运算符
return 0;
}
默认情况下,复制运算符也是对成员的值进行复制,因此也将导致指针类型的成员变量指向同一个地址,从而引起错误。
所以,通过重载=
运算符来解决这个问题,重载方式和拷贝构造函数类似,但有点差别:
- 1.要避免自己给自己复制;
- 2.由于目标对象可能存在以前分配的数据,故在动态申请内存前,需要先使用
delete []
释放以前的数据。
重载逻辑如下:
Student & Student::operator=(const Student & stu) {
if (this == &stu)
return *this;//避免释放掉自身内存
delete[] name;
this->age = stu.age;
this->name = new char[std::strlen(stu.name) + 1];
std::strcpy(this->name,stu.name);
number++;
return *this;
}
4.总结
凡是使用到new的类通常都需要提供拷贝构造函数和重载赋值运算符,以防止浅拷贝引起的问题。
如果希望这个类不使用拷贝构造和赋值运算符,可以将它们在类声明时定义为private
形式,如:
class Student
{
private:
Student(const Student & stu) {};
Student & operator=(const Student & stu) { return *this;};
};