C++中的默认构造函数、拷贝构造函数、移动构造函数

类的默认构造函数

当类定义中没有提供任何构造函数,C++会为提供一个默认构造函数,但只能有一个默认构造参数,下面行为会导致二义性;

class A
{
private:
	int m_a;
public:
	A();
	A(int x = 10 ) { m_a = x; } // 带参构造函数,只要所有参数有默认值也可以作为默认构造函数
	int get_a() { return this->m_a; };
};

int main()
{
    A t; // 此时编译器会报错,提示class A有两个默认构造参数
    cout << t.get_a() << endl;
    return 0;
}

如果定义了构造函数则C++不会提供默认构造函数,如果希望创建对象时不显示的进行初始化,则必须显示的定义默认构造参函数,不带任何参数,并在函数内部进行成员的赋值操作;

class A
{
private:
	int m_a;
public:
	A() {m_a = 20};
	//A(int x = 10 ) { m_a = x; } // 带参构造函数,只要所有参数有默认值也可以作为默认构造函数
	int get_a() { return this->m_a; };
};

上面说到定义了带参数的构造函数,C++则不会提供默认构造函数,出于某种原因,又希望创建一个默认对象,为了让编译器生成默认构造函数这时候就需要default属性

class A
{
private:
	int m_a;
public:
	A() = default;
	A(int x) { m_a = x; }
	int get_a() { return this->m_a; };
};

int main()
{
    A t; // 此时调用默认构造参数
    cout << t.get_a() << endl; // a 并不会初始化
    return 0;
}

复制构造函数

复制构造函数的形参为该类型的对象的引用,作用是用一个已存在的对象初始化同类型的新的对象;
如果类没有声明复制构造函数编译器将生成一个默认复制构造函数,同样,如果不声明复制赋值运算符,编译器将为你生成成员的复制赋值运算符。 声明复制构造函数不会取消编译器生成的复制赋值运算符,反之亦然。 如果实现其中任一方法,建议也实现另一个。 实现这两者时,代码的含义是明确的。


class Window
{
public:
    Window( const Window& );            // 复制构造函数
    Window& operator=(const Window& x); // 赋值运算符
    // ...
};

int main()
{

    return 0;
}

常见的三种会调用复制构造函数的情况:

  • 定义类对象时,以同类型的另一个对象来初始化该本对象;

  • 类对象作为函数参数进行值传递时,会使用实参来初始化形参对象,此时会进行拷贝构造;

  • 函数返回值为类对象,此时在函数return返回值时会发生拷贝构造,而函数内部对象会被销毁;

    由于C++移动语义的特性,在情况三中往往不会发生拷贝构造,而是通过移动构造避免不必要的复制。

浅拷贝与深拷贝

如果一个类拥有资源,在拷贝过程中没有发生资源重新分配就是浅拷贝,反之就是深拷贝
如果在类中没有显式地声明一个拷贝构造函数,则编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝
举个简单例子

// 深拷贝
char* pDst = new char[128];
memcpy(pDst, pSrc, sizeof(pSrc));

//浅拷贝(位拷贝)
pDst = pSrc;

当类成员函数有指针时,对象A执行拷贝构造函数 拷贝B的数据内容,当执行浅拷贝时,仅仅是将B对象的指针成员所指向的值(地址)赋值给了A中的指针成员,此时A与B中的指针成员指向同块内存,当B销毁时,A中的指针成员就变成了野指针,会导致意想不到的错误。
自己定义拷贝构造函数是一个良好的编程习惯,特别是当类成员中含有指针类型时。

移动构造函数

讲移动构造函数之前,先说说左值右值,能出现在赋值号左边的表达式称为左值,不能出现在赋值号左边的表达式称为右值。一般来说,左值是可以取地址的,右值则不可以

引入右值引用的主要目的是提高程序运行的效率。当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制对象的所有数据。深拷贝往往非常耗时,合理使用右值引用可以避免没有必要的深拷贝操作。

这里引入了移动语义,所谓移动语义(Move语义),指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。

移动语义的具体实现就是C++移动构造函数。

// 移动构造函数
MemoryBlock(MemoryBlock&& other)
   : _data(nullptr)
   , _length(0)
{
    _data = other._data;
    _length = other._length;
    other._data = nullptr;
    other._length = 0;
}

// 移动赋值运算符
MemoryBlock& operator=(MemoryBlock&& other)
{
    if (this != &other)
    {
        // 释放当前现有资源
        delete[] _data;
        
        _data = other._data;
        _length = other._length;

        // 防止内存多次释放
        other._data = nullptr;
        other._length = 0;

        return *this;
    }
}

通过std::move可以简化代码,通过移动构造函数调用移动赋值运算符,std::move可以把左值转换成右值。

MemoryBlock(MemoryBlock&& other) noexcept
   : _data(nullptr)
   , _length(0)
{
   *this = std::move(other);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值