构造函数与拷贝控制

Posted xiaojianliu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了构造函数与拷贝控制相关的知识,希望对你有一定的参考价值。

虚析构函数

class Quote{
public:
    virtual ~Quote() = default;
    //...
};

Quote *itemP = new Quote;
delete itemP;       //@ 调用Quote的析构函数
itemP = new Bulk_quote;
delete itemP;       //@ 调用Bulk_quote的析构函数

上面的程序,如果基类的析构函数不是虚函数,则 delete 一个指向派生类对象的基类指针将产生未定义的行为。

之前的经验是如果一个类需要析构函数,那么它也同样需要拷贝和赋值操作,但是基类的析构函数并不遵循上述原则,它是一个重要的例外,一个基类总是需要析构函数,而且它能将析构函数设定为虚函数,此时,该析构函数为了成为虚函数而令内容为空,显然无法推断这个基类是否还需要赋值运算符和拷贝构造函数。

注意:

虚析构函数将阻止合成移动操作:

如果基类中定义了虚析构函数,即使它是 = default 的形式,编译器也不会为这个类合成移动操作。

合成拷贝控制与继承

派生类中删除的拷贝控制与基类的关系

  • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值、或销毁操作。
  • 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁掉派生类对象的基类部分。
  • 编译器无法合成一个删除掉的移动操作,当使用 =default 请求一个移动操作时,如果基类中的对应操作时删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是删除的。
class B
{
public:
    B();
    B(const B&) = delete;
};

class D:public{
D d;            //@ 正确
D d2(d);        //@ 错误,D的合成拷贝构造函数是删除的
D d3(std::move(d));        //@ 错误,隐式地使用D的被删除的移动构造函数
};

因为 B 定义定义了拷贝构造函数,虽然是删除的,编译器不会为 B 合成一个移动构造函数,所以,既不能拷贝 B 的对象,也不能移动 B 的对象。如果B的派生类希望它自己的对象能被移动和拷贝,则派生类需要定义相应版本的构造函数,但是还需要考虑如何拷贝或移动基类部分的成员,在实际编程中,如果基类中没有默认、拷贝或移动构造函数,则一般情况下,派生类也不会定义相应的操作。

移动操作与继承

大多数基类都会定义虚析构函数,因此,在默认情况下,基类通常不含有合成的移动操作,而且他的派生类中也没有移动操作。

如果确实需要执行移动操作,应该首先在基类中进行定义,如果定义了移动操作,则应该也显示的定义拷贝操作:

class Quote
{
public:
    Quote() = default;
    Quote(const Quote&) = default;
    Quote(Quote&&) = default;
    Quote& operator=(const Quote&) = default;
    Quote& operator(Quote&&) = default;
    virtual ~Quote() = default;
}

派生类的拷贝控制成员

派生类构造函数在其初始化阶段不但要初始化派生类自己的成员,还负责初始化派生类对象的基类部分,派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员,类似的,派生类赋值运算符也必须为其基类部分的成员赋值。

和构造函数和赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源,派生类对象的基类部分是自动销毁的。

定义派生类的拷贝或移动构造函数

当为派生类定义拷贝或移动构造函数时,通常使用对应的基类构造函数初始化对象的基类部分:

class Base{/******/};
class D : public Base{
public:
    D(const D&d) : Base(d){...}
    D(D&& d) : Base(std::move(d)){...}
};

初始值 Base(d) 将一个 D 对象传递给基类构造函数,Base(d) 一般会匹配 Base 的拷贝构造函数,D 类型的对象 d 将被绑定到该构造函数的 Base& 形参上,Base 的拷贝构造函数负责将d的基类部分拷贝给要创建的对象。

假如没有提供基类的初始化值的话:

D(const D& d){}

D的这个拷贝构造函数很可能是不正确的,基类部分被默认初始化,而非拷贝初始化。

注意:

在默认情况下,基类默认构造函数初始化派生类对象的基类部分,如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显示地使用基类的拷贝、移动构造函数。

派生类赋值运算符

派生类的赋值运算符也必须显示地为其基类部分赋值:

//Base::operator=(const Base&)   //@ 不会被自动调用
D& D::operator=(const D &rhs)
{
    Base::operator = (rhs);     //为基类部分赋值
    //为派生类成员赋值
    return *this;
}

需要注意的是无论基类的构造函数或赋值运算符是自定义的版本还是合成的版本,派生类的对应操作都能使用它们。

派生类析构函数

析构函数体执行完成后,对象的成员会被隐式销毁,类似的,对象的基类部分也是隐式销毁的。因此,和构造函数以及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源:

class D : public Base
{
public:
    //Base::~Base  //@ 被自动调用执行
    ~D(){/*该处由用户定义清楚派生类成员的操作*/}
};

对象销毁的顺序正好与其创建的顺序相反,派生类的析构函数首先执行,然后是基类的析构函数,依次类推,沿着继承体系的反方向直至最后。

在构造函数和析构函数中调用虚函数

如果构造函数或者析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。

继承的构造函数

一个类只初始化它的直接基类,一个类也只继承其直接基类的构造函数。

类不能继承默认、拷贝和移动构造函数,如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。

派生类继承基类构造函数的方式是提供了一条注明了直接基类名的 using 声明语句:

class Bulk_quote : public Disc_quote{
public:
    using Disc_quote::Disc_quote;   //@ 继承Disc_quote的构造函数
    double net_price(std::size_t) const;
};

通常情况下,using 声明语句只是令某个名字在当前作用域中可见,而当作用于构造函数时,using 声明语句将令编译器产生代码。对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数,也就是说,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数:

derived(parms) : base(args) {}

例如:

Nulk_quote(const std::string& book,double price,std::size_t qty,double disc):Disc_quote(book,price,qty,disc){}

如果派生类含有自己的数据成员,则这些成员将被默认初始化。

继承的构造函数的特点

和普通成员的 using 声明不一样,一个构造函数的 using 声明不会改变该构造函数的访问级别,例如,不管 using 出现在哪,基类的私有构造函数在派生中还是一个私有构造函数:受保护的构造函数和公有构造函数也是同样的规则。

一个 using 声明语句不能指明 explicitconstexpr。如果基类的构造函数是 explicitconstexpr,则继承的构造函数拥有相同的属性。

当一个基类构造函数含有默认实参,这些实参不会被继承,相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参:

如果基类有一个接受两个形参的构造函数,其中第二个形参含有默认实参,则派生类将获得两个构造函数,一个构造函数接受两个形参(没有默认实参),另一个只接受一个形参,它对应于基类中最左侧的没有默认值的那个形参。

如果基类含有几个构造函数吗,则除了两个例外,大多数情况下派生类会继承所有这些构造函数:

  • 派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本,如果派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数将不会被继承,定义在派生类中的构造函数将替换继承而来的构造函数。
  • 默认、拷贝、移动构造函数不会被继承,这些构造函数按照正常规则被合成。如果一个类只含有继承的构造函数,则它将拥有一个合成的默认构造函数。

以上是关于构造函数与拷贝控制的主要内容,如果未能解决你的问题,请参考以下文章

拷贝控制——拷贝赋值与销毁

C++类拷贝控制 深拷贝 浅拷贝

C++C++的拷贝控制

C++ Primer笔记14---chapter13 拷贝控制1

OOP3(继承中的类作用域/构造函数与拷贝控制/继承与容器)

Chapter13:拷贝控制