C++类和对象之拷贝构造函数

目录:


拷贝构造函数

例:浅拷贝 / 深拷贝实例 拷贝构造函数的调用时机: 例:拷贝构造函数的3种调用时机实例

常见问题

1. 左值和右值 2. 拷贝构造函数的引用 3. 拷贝构造函数的const





拷贝构造函数

C++ 中经常会使用一个变量初始化另一个变量,如

int x = 1; 
int y = x;

我们希望这样的操作也能作用于自定义类类型,如

Point pt1(1, 2); 
Point pt2 = pt1;

这两组操作是不是一致的呢?第一组好说,而第二组只是将类型换成了 Point 类型,执行 Point pt2 = pt1;语句时, pt1 对象已经存在,而 pt2 对象还不存在,所以也是这句创建了 pt2 对象,既然涉及到对象的创建,就必然需要调用构造函数,而这里会调用的就是 复制构造函数,又称为 拷贝构造函数

当我们进行测试时,会发现我们不需要显式给出拷贝构造函数,就可以执行第二组测试。这是因为如果类中没有显式定义拷贝构造函数时,编译器会自动提供一个缺省的拷贝构造函数。其原型是:

类名::类名(const 类名&);

那缺省(默认)的拷贝构造函数是如何实现的呢?很简单,我们来实现一下 Point 类的拷贝构造函数:

Point::Point(const Point & rhs) 
: _ix(rhs._ix) 
, _iy(rhs._iy) 
{}

由于 Point 的成员比较简单,缺省的拷贝构造函数已经可以满足需求了,所以可以不显式定义。接下 来,我们把目光转向 Computer 类,如果 Computer 类使用缺省拷贝构造函数,会发生什么问题呢?我们先来看看 缺省的拷贝构造函数的实现

Computer::Computer(const Computer & rhs) 
: _brand(rhs._brand) 
, _price(rhs._price) 
{}

//执行构造初始化 
Computer pc1("Huawei Matebook14", 5699); 
Computer pc2 = pc1;

从上面的定义来看, pc1 与 pc2 对象的数据成员 _brand 都会指向同一个堆空间的字符串,这种只拷贝指针的地址的方式,我们称为浅拷贝。当两个对象被销毁时,就会造成 double free 的问题。显然, 缺省拷贝构造函数不再满足需求,此时需要显式定义拷贝构造函数:

Computer::Computer(const Computer & rhs) 
: _brand(new char[strlen(rhs._brand) + 1]()) 
, _price(rhs._price)
{
	strcpy(_brand, rhs._brand);
}

这种拷贝指针所指空间内容的方式,我们称为深拷贝。因为两个对象都拥有各自的独立堆空间字符串, 一个对象销毁时就不会影响另一个对象。



例:浅拷贝 / 深拷贝实例

#include 
#include 
#include 
#include 

using std::cout;
using std::endl;

class Student
{
public:

    //构造函数
    Student(const char * name, int score)
    : _name(new char[strlen(name) + 1]())
    , _score(score)
    {
        strcpy(_name, name);
        cout << "Student" << endl;
    }

    //拷贝构造函数-浅拷贝
    //Student(const Student & rhs)
    //: _name(rhs._name)
    //, _score(rhs._score)
    //{
    //    cout << "Student copy" << endl;
    //}
    
    //拷贝构造函数-深拷贝
    Student(const Student & rhs)
    : _name(new char[strlen(rhs._name) + 1]())
    , _score(rhs._score)
    {
        strcpy(_name, rhs._name);
        cout << "Student copy" << endl;
    }
    
    //参数score修改
    void setScore(int score)
    {
        _score = score;
    }

    //参数name修改
    void setName(const char * name)
    {
        strcpy(_name, name);
    }

    //打印函数
    void print() const
    {
        cout << "name:" << _name << endl
             << "score:" << _score << endl;
    }

    //析构函数
    ~Student()
    {
        delete [] _name;
        cout << "~Student" << endl;
    }

private:
    char * _name;
    int _score;
};

void func()
{
    Student stu1("Lones", 30);
    cout << "stu1" << endl;
    stu1.print();
    cout << endl << "stu2" << endl;
    Student stu2 = stu1;
    stu2.print();
    cout << endl;

    stu2.setScore(20);
    stu2.setName("pp");
    cout << "stu1" << endl;
    stu1.print();
    cout << endl << "stu2" << endl;
    stu2.print();
    cout << endl;
}

int main()
{
    func();
    return 0;
}

运行结果(浅拷贝和深拷贝):

C++类和对象之拷贝构造函数_第1张图片 C++类和对象之拷贝构造函数_第2张图片

因为stu2是stu1的拷贝,可看见对于stu2的name的修改,浅拷贝共用一块内存,所以stu1也发生改变;深拷贝是各自的内存,所以stu2对name的修改,并没有修改stu1。

浅拷贝

调用一次构造函数,调用两次析构函数,两个对象的指针成员所指同一块内存,这会导致指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致崩溃!由于编译系统在我们没有自己定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝!即对指针name拷贝后会出现两个指针指向同一个内存空间

深拷贝

在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。

浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间;
深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。



拷贝构造函数的调用时机:

  1. 当把一个 已经存在的对象赋值给另一个新对象 时,会调用拷贝构造函数。
  2. 当实参和形参(函数参数)都是对象,进行 实参与形参的结合 时,会调用拷贝构造函数。
  3. 函数的返回值是对象,函数调用完成返回时(执行return),会调用拷贝构造函数。



例:拷贝构造函数的3种调用时机实例

#include 
#include 
#include 
#include 

using std::cout;
using std::endl;

class Student
{
	// ...
};

//时机1:已经存在的对象赋值给另一个新对象
void func()
{
    Student stu1("Lones", 30);
    cout << "stu1" << endl;
    stu1.print();
    cout << endl << "stu2" << endl;
    Student stu2 = stu1;
    stu2.print();
    cout << endl;

    stu2.setScore(20);
    stu2.setName("pp");
    cout << "stu1" << endl;
    stu1.print();
    cout << endl << "stu2" << endl;
    stu2.print();
    cout << endl;
}

//时机2:当实参和形参(函数参数)都是对象,进行实参与形参的结合
void func2_ret(Student stu)
{
    stu.print();
}

void func2()
{
    Student stu1("Jack", 66);
    func2_ret(stu1);
}


//时机3:当函数的返回值是对象,函数调用完成返回时(执行return)
Student func3_ret()
{
    Student stu1("Jason", 44);
    stu1.print();
    return stu1;
}

void func3()
{
    Student stu2 = func3_ret();
}

void func4()
{
    const Student &rhs = func3_ret();
}

int main()
{
    func();
    cout << endl << endl;
    func2();
    cout << endl << endl;
    func3();
    cout << endl << endl;
    func4();
    cout << endl << endl;
    return 0;
}

时机1,时机2都会调用拷贝构造函数,注意的是时机3,在直接使用g++编译时,不会显示调用拷贝构造函数,这是因为测试右值,临时对象的构造,编译器会自动优化导致没有打印。编译时在末尾添加 -fno-elide-constructors 关闭自动优化,就会正常打印该显示的拷贝构造函数

我们详细看一下func3,func4

  • func3调用 两次 拷贝构造函数return stu1; 返回的是一个值,编译器首先会创建一个临时变量拷贝下这个stu1(匿名对象), 这时调用了拷贝构造函数(变量或者对象,若结果是一个值就直接返回,若结果是一个对象则会产生一个匿名对象,将会调用拷贝构造函数将需要返回的对象赋值给匿名对象,然后把匿名对象返回出去,func3_ret() 函数执行完毕原来需要返回的对象就会被析构掉,匿名对象才是真的返回值),这是第一次拷贝调用;再是函数中stu2接收func3_ret();的返回值(复制),这是第二次拷贝调用

  • func4调用 一次 拷贝构造函数return stu1; 进行了一次拷贝调用,然后在接收返回值时,使用了左值引用来接收func3_ret();的返回值,因为是引用所以没有调用拷贝。



常见问题


1. 左值和右值

左值: 可以取地址
右值: 一般是 临时对象字面值常量 ,不可以取地址,生命周期只有本行,在内存没有存储

通常以下三种情况会产生临时对象:

1,以值的方式给函数传参;
2,类型转换;
3,函数需要返回一个对象时;


2. 拷贝构造函数的引用

调用拷贝构造函数去掉引用符号,形参是对象,无穷递归调用拷贝构造函数,没有出口,最后栈溢出。

拷贝构造函数
Student(const Student rhs)

函数里
Student p2 = p; //调用拷贝构造函数
传参(值传递,复制)时 const Student rhs = p; //又调用拷贝构造函数
传参(值传递,复制)时 const Student rhs1 = rhs; //又调用拷贝构造函数
传参(值传递,复制)时 const Student rhs2 = rhs1; //又调用拷贝构造函数

无限递归调用


3. 拷贝构造函数的const

const是不可以去掉的,如果const去掉,当传递一个临时对象(右值),无法调用拷贝构造函数,而且会报错。

你可能感兴趣的:(C++)