C++构造函数的一些注意事项

前言

  • C++11开始对象的拷贝由五个函数来控制。拷贝构造函数,赋值运算符,析构函数,移动构造函数,移动赋值运算符。对象的拷贝控制是一个类的基础,需要很好的辨别每个函数的用法与调用时机。

例子

  • 下面是一些对象构造,能否辨别每个构造调用的函数?
class Foo
{
public:
    //默认构造
    Foo() = default;
    //析构
    ~Foo() = default;
    //带参构造
    explict Foo(int x) : x(x)
    {
        std::cout << "Constructor" << std::ends;
    }
    //复值构造
    Foo(const Foo &rhs) : x(rhs.x)
    {
        std::cout << "Copy Constructor" << std::ends;
    }
    //赋值
    Foo &operator=(const Foo &rhs)
    {
        if (this != &rhs)
        {
            x = rhs.x;
            std::cout << "Assignment Operator" << std::ends;
        }

        return *this;
    }
    //移动构造
    Foo(Foo &&rhs) noexcept : x(rhs.x)
    {
        std::cout << "Move Constructor" << std::ends;
    }
    //移动赋值
    Foo &operator=(Foo &&rhs) noexcept
    {
        if (this != &rhs)
        {
            x = rhs.x;
            std::cout << "Move Assignment Operator" << std::ends;
        }

        return *this;
    }

public:
    int x;
};
//返回左值的函数
Foo CreateFoo()
{
    return Foo(1);
}
//返回右值的函数
Foo &&MoveFoo()
{
    return std::move(Foo(1));
}

int main()
{
    Foo a(1);  //Constructor
    std::cout << std::endl;

    Foo b = a;  //Copy Constructor
    std::cout << std::endl;

    Foo c(a);  //Copy Constructor
    std::cout << std::endl;

    b = a;  //Assignment Operator
    std::cout << std::endl;

    Foo d = std::move(Foo(1));  //Constructor / Move Constructor
    std::cout << std::endl;

    Foo e = std::move(a);  //Move Constructor
    std::cout << std::endl;

    Foo f = CreateFoo();  //Constructor / Move Constructor(注意这里编译器Copy elision会优化掉移动构造函数)
    std::cout << std::endl;

    Foo g = MoveFoo();  //Constructor / Move Constructor
    std::cout << std::endl;

    d = Foo(1);  //Constructor / Move Assignment Operator

    std::getchar();  

    return 0;
}

一些重点

  • 构造与赋值运算符的调用区别
    • 涉及到新对象的创建就是调用构造函数,已有对象之间的相互赋值是调用赋值运算符
  • 显式声明default构造和析构函数
    • 如果有带参数的构造函数,你是需要提供一个无参默认构造函数的,如果没有特别的参数,用default显式声明的默认构造函数会比自己声明的内联版本更优。同时使用=delete来隐藏不需要编译器自动生成的构造函数。
  • 浅拷贝与深拷贝问题
    • 深浅拷贝问题与移动构造函数的出现也有关系。简单来说浅拷贝只是按位进行了数据的拷贝。如果数据中有指针,也只是拷贝了一份指针,而不是指针指向的资源。问题在于这时就有两个指针指向了同一份资源,若其中一个对象析构了释放了资源,另一个对象指向此资源的指针便无效了。
    • 深拷贝便是拷贝了指针指向的资源,在新对象中这个资源指针指向的是新的拷贝的资源。
    • 大家可以看出问题的本质是拷贝之后多个指针指向同一块内存而造成的内存泄露可能性,所以如果可以保证拷贝之后只有一个指针指向这块内存就是安全的。深拷贝是一种方法,但是可以看出是消耗多余资源的。
    • 另一种方法便是移动构造。移动构造是浅拷贝,但是拷贝之后会把前一个对象指向内存的指针置空,也就是保证了只有新对象的指针指向了之前的那块内存,行为上相当于对资源进行了移动,但是实质是没有资源消耗的。
    • 除了默认会调用拷贝构造的三种情况以及确认拷贝时需要资源拷贝的类,其余情况下用移动构造都是更优的选择,所以声明移动构造函数和移动赋值运算符是很必要的,因为编译器不会为你生成默认版本。
  • explict关键字
    • 对于可以使用一个实参进行调用的构造函数,默认会进行隐式类型转换。隐式类型转换非常容易引起类型错误,如果不是有明确的设计,将其声明为explicit防止隐式类型转换总是更好的选择。
发布于 03-04