当前位置: 首页 > news >正文

15.面向对象程序设计

文章目录

  • 面向对象程序设计
    • 15.1OOP:概述
      • 继承
      • 动态绑定
    • 15.2定义基类和派生类
      • 15.2.1定义基类
      • 成员函数与继承
      • 访问控制与继承
      • 15.2.2定义派生类
        • 派生类对象及派生类向基类的类型转换
        • 派生类构造函数
        • 派生类使用基类的成员
        • 继承与静态成员
        • 派生类的声明
        • 被用作基类的类
        • 防止继承的发生
      • 15.2.3类型转换与继承
        • 静态类型与动态类型
        • 不存在从基类向派生类的隐式类型转换
        • 在对象之间不存在类型转换
    • 15.3虚函数
      • 对虚函数的调用可能在运行时才被解析
      • 派生类中的虚函数(覆盖时,virtual和override是可选的)
      • final和override说明符
      • 虚函数与默认实参
      • 回避虚函数的机制
    • 15.4抽象基类
      • 纯虚函数
      • 含有纯虚函数的类是抽象基类
      • 派生类构造函数只初始化它的直接基类
    • 15.5访问控制与继承
      • 受保护的成员
      • 公有、私有和受保护继承
      • 派生类向基类转换的可访问性
      • 友元与继承
      • 改变个别成员的可访问性
      • 默认的继承保护级别
    • 15.6继承中的类作用域
      • 名字冲突与继承
      • 通过作用域运算符来使用隐藏的成员
      • 一如往常,名字查找先于类型检查
      • 虚函数与作用域
      • 通过基类调用隐藏的虚函数
      • 覆盖重载的函数
    • 15.7构造函数与拷贝控制
      • 15.7.1虚析构函数
        • 虚析构函数将阻止合成移动操作
      • 15.7.2合成拷贝控制与继承
        • 派生类中删除的拷贝控制与基类的关系
        • 移动操作与继承
      • 15.7.3派生类的拷贝控制成员
        • 定义派生类的拷贝或移动构造函数
        • 派生类赋值运算符
        • 派生类析构函数
        • 在构造函数和析构函数中调用虚函数
      • 15.7.4继承的构造函数
        • 继承的构造函数的特点
    • 15.8容器与继承
      • 在容器中放置(智能)指针而非对象

面向对象程序设计

面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定。

15.1OOP:概述

继承

通常在层次关系的根部有一个基类,继承得到的类称为派生类
对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数

// 不同定价策略的建模
class Quote {
public:// 返回书籍的ISBN编号,因为不涉及派生类的特殊性,因此只定义在Quote中。std::string isbn() const;virtual double net_price(std::size_t n) const;
};

派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的,其中,每个基类前面可以有:publicprotected或者private中的一个,具体含义后面会讲解。

class Bulk_quote : public Quote {
public:double net_price(std::size_t) const override;
};

派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字,但是并不是非得这么做。
C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即增加override关键字。

动态绑定

通过使用动态绑定,能用同一段代码分别处理基类和派生类的对象。

// 计算并打印销售给定数量的某种书籍所得的费用
double print_total(ostream &os, const Quote &item, size_t n) {// 根据传入的item形参的对象类型调用Quote::net_price// 或者Bulk_quote::net_price。double ret = item.net_price(n);os << "ISBN: " << item.isbn()<< " # sold: " << n << " total due: " << ret << endl;return ret;
}// basic的类型是Quote;bulk的类型是Bulk_quote。
print_total(cout, basic, 20);	// 调用Quote的net_price
print_total(cout, bulk, 20);	// 调用Bulk_quote的net_price

在c++中,当使用基类的引用(或指针)调用一个虚函数时将发生动态绑定

15.2定义基类和派生类

15.2.1定义基类

class Quote {
public:Quote() = default;Quote(const std::string &book, double sales_price): bookNo(book), price(sales_price) {}std::string isbn() const {return bookNo;}// 返回给定数量的书籍的销售总额// 派生类负责改写并使用不同的折扣计算算法virtual double net_price(std::size_t n) const {return n * price;}// 基类通常都应该定义一个虚析构函数,即使该函数// 不执行任何实际操作也是如此。virtual ~Quote() = default; // 对析构函数进行动态绑定private:std::string bookNo; // 书籍的ISBN编号protected:double price = 0.0; // 代表普通状态下不打折的价格
};

成员函数与继承

任何构造函数之外的非静态函数都可以是虚函数。virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时

访问控制与继承

基类可以使用受保护的访问运算符定义希望它的派生类有权访问而禁止其他用户访问的成员。

15.2.2定义派生类

派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明:

// 从Quote那里继承了isbn函数和bookNo、price等数据成员。
class Bulk_quote : public Quote {
public:Bulk_quote() = default;Bulk_quote(const std::string &, double, std::size_t, double);// 覆盖基类的函数版本以实现基于大量购买的折扣政策double net_price(std::size_t) const override;private:std::size_t min_qty = 0;    // 适用折扣政策的最低购买量double discount = 0.0;  // 以小数表示的折扣额
};

派生类对象及派生类向基类的类型转换

一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。

在这里插入图片描述

因为在派生类对象中含有与其基类对应的组成部分,所以能把派生类的对象当成基类对象来使用,而且也能将基类的指针或引用绑定到派生类对象中的基类部分上。

派生类构造函数

尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。派生类必须使用基类的构造函数来初始化它的基类部分。
每个类控制它自己的成员初始化过程
派生类对象的基类部分与派生类对象自己的数据成员都是在构造函数的初始化阶段执行初始化操作的。

// 由Quote的构造函数负责初始化Bulk_quote的基类部分。当(空的)Quote构造函数体结束后,
// 构建的对象的基类部分也就完成初始化了。接下来初始化由派生类直接定义的数据成员。
// 最后运行Bulk_quote构造函数的(空的)函数体。
Bulk_quote(const std::string &book, double p, std::size_t qty, double disc): Quote(book, p), min_qty(qty), discount(disc) {}

派生类使用基类的成员

派生类可以访问基类的公有成员和受保护成员:

// 如果达到了购买书籍的某个最低限量值,就可以享受折扣价格了。
double Bulk_quote::net_price(std::size_t cnt) const {if (cnt >= min_qty) {return cnt * (1 - discount) * price;} else {return cnt * price;}
}

派生类的作用域嵌套在基类的作用域之内。

继承与静态成员

如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。

class Base {
public:static void statmem();
};class Derived : public Base {void f(const Derived &);
};

静态成员遵循通用的访问控制规则,如果基类中的成员是private的,则派生类无权访问它。假设某静态成员是可访问的,则既能通过基类使用它也能通过派生类使用它:

void Derived::f(const Derived &derived_obj) {Base::statmem();Derived::statmem();derived_obj.statmem();statmem();	// 通过this对象访问
}

派生类的声明

派生类的声明中包含类名但是不包含它的派生列表。派生列表以及与定义有关的其他细节必须与类的主体一起出现。

被用作基类的类

如果想将某个类用作基类,则该类必须已经定义而非仅仅声明。
原因在于,派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类当然要知道它们是什么。因此,一个类不能派生它本身。

防止继承的发生

在类后跟一个关键字final

15.2.3类型转换与继承

智能指针类也支持派生类向基类的类型转换。

静态类型与动态类型

表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。

不存在从基类向派生类的隐式类型转换

如果在基类中包含一个或多个虚函数,可以使用dynamic_cast请求一个类型转换,该转换的安全检查将在运行时执行。同样,如果已知某个基类向派生类的转换是安全的,则可以使用static_cast来强制覆盖掉编译器的检查工作。

在对象之间不存在类型转换

派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。
当初始化或赋值一个类类型的对象时,实际上是在调用某个函数。这些成员通常都包含一个类类型的const版本的引用的参数。
因为这些成员接受引用作为参数,所以派生类向基类的转换允许给基类的拷贝/移动操作传递一个派生类的对象。这些操作不是虚函数。当给基类的构造函数传递一个派生类对象时,实际运行的构造函数是基类中定义的那个,显然该构造函数只能处理基类自己的成员。类似的,如果将一个派生类对象赋值给一个基类对象,则实际运行的赋值运算符也是基类中定义的那个,该运算符同样只能处理基类自己的成员。

Bulk_quote bulk;	// 派生类对象
// 只能处理bookNo和price两个成员,它负责拷贝bulk中Quote部分的成员,
// 同时忽略掉bulk中Bulk_quote部分的成员。
Quote item(bulk);	// 使用Quote::Quote(const Quote &)构造函数
item = bulk;	// 调用Quote::operator=(const Quote &)

当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。

15.3虚函数

因为直到运行时才能知道到底调用了哪个版本的虚函数,所以所有的虚函数都必须有定义

对虚函数的调用可能在运行时才被解析

动态绑定只有当通过指针或引用调用虚函数时才会发生。
当通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。

派生类中的虚函数(覆盖时,virtual和override是可选的)

一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
同样,派生类中虚函数的返回类型也必须与基类函数匹配。但是存在一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。也就是说,如果DB派生得到,则基类的虚函数可以返回B*而派生类的对应函数可以返回D*,只不过这样的返回类型要求从DB的类型转换时可访问的。

final和override说明符

派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的行为。编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。这时,派生类的函数并没有覆盖掉基类的版本。
就实际的编程习惯而言,这种声明往往意味着发生了错误,因为可能原本希望派生类能覆盖掉基类中的虚函数,但是一不小心把形参列表弄错了。因此,在新标准中,可以使用override关键字来说明派生类中的虚函数。

虚函数与默认实参

虚函数也可以拥有默认实参,如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。因此,基类和派生类中定义的默认实参最好一致。

回避虚函数的机制

使用作用域运算符可以实现:

// 强行调用基类中定义的函数版本而不管baseP的动态类型到底是什么
double undiscounted = baseP->Quote::net_price(42);

通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。
当一个派生类的虚函数调用它覆盖的基类的虚函数版本时,基类的版本通常完成继承层次中所有类型都要做的共同任务,而派生类中定义的版本需要执行一些与派生类本身密切相关的操作。

15.4抽象基类

纯虚函数

一个纯虚函数无须定义,通过在函数体的位置书写=0就可以进行说明。其中,=0只能出现在类内部的虚函数声明语句处

// 用于保存折扣值和购买量的类,其他的表示某种特定策略的类将分别继承自Disc_quote,派生类使用
// 这些数据可以实现不同的价格策略。由于Disc_quote类与任何特定的折扣策略都无关,因此,net_price
// 函数是没有实际含义的。如果不定义新的net_price,此时,Disc_quote将继承Quote中的net_price
// 函数。然而,这样的设计可能导致用户编写出一些无意义的代码。用户可能会创建一个Disc_quote对象并
// 为其提供购买量和折扣值,如果将该对象传给一个像print_total这样的函数,则程序将调用Quote版本的
// net_price。显然,最终计算出的销售价格并没有考虑在创建对象时提供的折扣值,因此上述操作毫无意义。
class Disc_quote : public Quote {
public:Disc_quote() = default;Disc_quote(const std::string &book, double price, std::size_t qty, double disc): Quote(book, price), quantity(qty), discount(disc) {}double net_price(std::size_t) const = 0;protected:std::size_t quantity = 0;   // 折扣适用的购买量double discount = 0.0;  // 表示折扣的小数值
};

也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。

含有纯虚函数的类是抽象基类

抽象基类负责定义接口,而后续的其他类可以覆盖该接口。不能(直接)创建一个抽象基类的对象

派生类构造函数只初始化它的直接基类

// 当同一书籍的销售量超过某个值时启用折扣。折扣值是一个小于1的正的小数值,
// 以此来降低正常销售价格。每个Bulk_quote对象包含三个子对象:一个(空的)
// Bulk_quote部分、一个Disc_quote子对象和一个Quote子对象。
class Bulk_quote : Disc_quote {
public:Bulk_quote() = default;// 每个类各自控制其对象的初始化过程。因此,即使Bulk_quote没有自己的数据成员,它也// 仍然需要像原来一样提供一个接受四个参数的构造函数。该构造函数将它的实参传递给// Disc_quote的构造函数,随后Disc_quote的构造函数继续调用Quote的构造函数,从而// 分别初始化。Bulk_quote(const std::string &book, double p, std::size_t qty, double disc): Disc_quote(book, p, qty, disc) {}// 覆盖基类的函数版本以实现一种新的折扣策略double net_price(std::size_t) const override;
};

15.5访问控制与继承

每个类还分别控制着其成员对于派生类来说是否可访问

受保护的成员

一个类使用protected关键字来声明那些它希望与派生类纷享但是不想被其他公共访问使用的成员:

  • 受保护的成员对于类的用户来说是不可访问的。
  • 受保护的成员对于派生类的成员和友元来说是可访问的。
  • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
class Base {
protected:int prot_mem;	// protected成员
};class Sneaky : public Base {friend void clobber(Sneaky &);	// 能访问Sneaky::prot_memfriend void clobber(Base &);	// 不能访问Base::prot_memint j;	// j默认是private
};// 正确:clobber能访问Sneaky对象的private和protected成员。
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
// 错误:clobber不能访问Base的protected成员。
void clobber(Base &b) { b.prot_mem = 0; }

公有、私有和受保护继承

某个类对其继承而来的成员的访问权限受到两个因素影响:一个是在基类中该成员的访问说明符,二是派生类的派生列表中的访问说明符。
派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关。

class Base {
public:void pub_mem();	// public成员
protected:int prot_mem;	// protected成员
private:char priv_mem;	// private成员
};// 如果继承是公有的,则成员将遵循其原有的访问说明符。
struct Pub_Derv : public Base {// 正确:派生类能访问protected成员。int f() { return prot_mem; }// 错误:private成员对于派生类来说是不可访问的。char g() { return priv_mem; }
};struct Priv_Derv : private Base {// private不影响派生类的访问权限int f1() const { return prot_mem; }
};

派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限:

Pub_Derv d1;	// 继承自Base的成员是public的
Priv_Derv d2;	// 继承自Base的成员是private的
d1.pub_mem();	// 正确:pub_mem在派生类中是public的。
d2.pub_mem();	// 错误:pub_mme在派生类中是private的。

派生访问说明符还可以控制继承自派生类的新类的访问权限:

struct Derived_from_Public : public Pub_Derv {// 正确:Base::prot_mem在Pub_Derv中仍然是protected的。int use_base() { return prot_mem; }
};// Priv_Derv继承自Base的所有成员都是私有的
struct Derived_from_Private : public Priv_Derv {// 错误:Base::prot_mem在Priv_Derv中是private的。int use_base() { return prot_mem; }
};

派生类向基类转换的可访问性

派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响(假设D继承自B):

  • 只有当D公有地继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换。
  • 不论D以什么方式继承BD的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
  • 如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和友元可以使用DB的类型转换;反之,如果D继承B的方式是私有的,则不能使用。

对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行。

友元与继承

友元关系也不能继承:

class Base {friend class Pal;	// Pal在访问Base的派生类时不具有特殊性// 其他成员保持一致
};class Pal {
public:int f(Base b) { return b.prot_mem; }	// 正确:Pal是Base的友元。int f2(Sneaky s) { return s.j; }	// 错误:Pal不是Sneaky的友元。// 对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此。int f3(Sneaky s) { return s.prot_mem; }	// 正确:Pal是Base的友元。
};

当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来那个类来说,其友元的基类或者派生类不具有特殊的访问能力:

// D2对Base的protected和private成员不具有特殊的访问能力
class D2 : public Pal {
public:int mem(Base b) { return b.prot_mem; }	// 错误:友元关系不能继承。
};

改变个别成员的可访问性

有时需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的:

class Base {
public:std::size_t size() const { return n; }protected:std::size_t n;
};class Derived : private Base {	// 注意:private继承。
public:// 保持对象尺寸相关的成员的访问级别using Base::size;protected:using Base::n;
};

通过在类的内部使用using声明语句,可以将该类的直接或间接基类中的任何可访问成员(例如,非私有成员)标记出来。using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。

  • 如果一条using声明语句出现在类的private部分,则该名字只能被类的成员和友元访问。
  • 如果位于public部分,则类的所有用户都能访问它。
  • 如果位于protected部分,则该名字对于成员、友元和派生类时可访问的。

默认的继承保护级别

默认情况下,使用class关键字定义的派生类是私有继承的;而使用struct关键字定义的派生类是公有继承的。

15.6继承中的类作用域

每个类定义自己的作用域,当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。

名字冲突与继承

派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字:

struct Base {Base() : mem(0) {}protected:int mem;
};struct Derived : Base {// 用i初始化Derived::mem,Base::mem进行默认初始化。Derived(int i) : mem(i) {}int get_mem() { return mem; }	// 返回Derived::memprotected:int mem;	// 隐藏基类中的mem
};

通过作用域运算符来使用隐藏的成员

struct Derived : Base {// 作用域运算符将覆盖掉原有的查找规则,并指示编译器从Base类的// 作用域开始查找mem。int get_base_mem() { return Base::mem; }// ...
};

除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。

一如往常,名字查找先于类型检查

定义派生类中的函数也不会重载其基类中的成员。如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉:

struct Base {int memfcn();
};struct Derived : Base {int memfcn(int);	// 隐藏基类的memfcn
};Derived d;
Base b;b.memfcn();	// 调用Base::memfcn
d.memfcn(10);	// 调用Derived::memfcn
// 为了解析这条调用语句,编译器首先在Derived中查找名字memfcn;因为Derived
// 确实定义了一个名为memfcn的成员,所以查找过程终止。
d.memfcn();	// 错误:参数列表为空的memfcn被隐藏了。
d.Base::memfcn();	// 正确:调用Base::memfcn。

虚函数与作用域

因此,基类与派生类中的虚函数必须有相同的形参列表,否则,无法通过基类的引用或指针调用派生类的虚函数:

class Base {
public:virtual int fcn();
};class D1 : public Base {
public:// 隐藏基类的fcn,这个fcn不是虚函数。// D1继承了Base::fcn()的定义。int fcn(int);	// 形参列表与Base中的fcn不一致virtual void f2();	// 是一个新的虚函数,在Base中不存在。
};class D2 : public D1 {
public:int fcn(int);	// 是一个非虚函数,隐藏了D1::fcn(int)。int fcn();	// 覆盖了Base的虚函数fcnvoid f2();	// 覆盖了D1的虚函数f2
};

通过基类调用隐藏的虚函数

Base bobj; D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1 -> fcn();	// 虚调用,将在运行时调用Base::fcn
bp2 -> fcn();	// 虚调用,将在运行时调用Base::fcn
bp3 -> fcn();	// 虚调用,将在运行时调用D2::fcnD1 *d1p = &d1obj; D2 *d2p = &d2obj;
bp2 -> f2();	// 错误:Base没有名为f2的成员。
d1p -> f2();	// 虚调用,将在运行时调用D1::f2()。
d2p -> f2();	// 虚调用,将在运行时调用D2::f2()。

再观察一些对于非虚函数的调用:

Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj;
p1 -> fcn(42);	// 错误:Base中没有接受一个int的fcn。
p2 -> fcn(42);	// 静态绑定,调用D1::fcn(int)。
p3 -> fcn(42);	// 静态绑定,调用D2::fcn(int)。

覆盖重载的函数

成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的0个或多个实例。如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。

struct Person {void walk() const {cout << "Person.walk()" << endl;}void walk(int step) const {cout << "Person.walk(int)" << endl;}
};struct Student : public Person {void test() const {// 此时只能调用Student::walk()walk();cout << "Student.test()" << endl;}// 如果不覆盖的话,可以调用Person::walk()以及Person::walk(int)。void walk() const {cout << "Student.walk()" << endl;}
};

有时,一个类仅需覆盖重载集合中的一些而非全部函数,此时,如果不得不覆盖基类中的每一个版本的话,显然操作将极其繁琐。
一种好的解决方案是为重载的成员提供一条using声明语句,指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类的作用域中。
类内using声明的一般规则同样适用于重载函数的名字;基类函数的每个实例在派生类中都必须是可访问的。对派生类没有重新定义的重载版本的访问实际上是对using声明点的访问。

15.7构造函数与拷贝控制

位于继承体系中的类也需要控制当其对象执行一系列操作时发生什么样的行为,这些操作包括创建、拷贝、移动、赋值和销毁。

15.7.1虚析构函数

基类通常应该定义一个虚析构函数,这样就能动态分配继承体系中的对象了。通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本:

class Quote {
public:// 如果删除的是一个指向派生类对象的基类指针,则需要虚析构函数。// 和其他虚函数一样,析构函数的虚属性也会被继承。virtual ~Quote() = default;	// 动态绑定析构函数
};Quote *itemP = new Quote;	// 静态类型与动态类型一致
delete itemP;	// 调用Quote的析构函数
itemP = new Bulk_quote;	// 静态类型与动态类型不一致
delete itemP;	// 调用Bulk_quote的析构函数

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

基类需要一个虚析构函数这一事实还会对基类和派生类的定义产生另外一个间接的影响:如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。

15.7.2合成拷贝控制与继承

基类或派生类的合成拷贝控制成员还负责使用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作。
值得注意的是,无论基类成员是合成的版本还是自定义的版本都没有太大影响。唯一的要求是相应的成员应该可访问并且不是一个被删除的函数。

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

基类或派生类也能出于同样的原因将其合成的默认构造函数或者任何一个拷贝控制成员定义成被删除的函数。此外,某些定义基类的方式也可能导致有的派生类成员成为被删除的函数:

  • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
  • 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分。
  • 编译器将不会合成一个删除掉的移动操作。当使用=default请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的。
class B {
public:B();// 因为定义了拷贝构造函数,所以编译器不会合成一个移动构造函数。B(const B &) = delete;// 其他成员,不含有移动构造函数。
};class D : public B {// 没有声明任何构造函数
}D d;	// 正确:D的合成默认构造函数使用B的默认构造函数。
D d2(d);	// 错误:D的合成拷贝构造函数是被删除的。
D d3(std::move(d));	// 错误:隐式地使用D的被删除的拷贝构造函数。

在实际编程过程中,如果在基类中没有默认、拷贝或移动构造函数,则一般情况下派生类也不会定义相应的操作。

移动操作与继承

因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当确实需要执行移动操作时应该首先在基类中进行定义。

// Quote可以使用合成的版本,不过前提是Quote必须显式地定义这些成员。一旦
// Quote定义了自己的移动操作,那么它必须同时显式地定义拷贝操作。除非派生类
// 中含有排斥移动的成员,否则它将自动获得合成的移动操作。
class Quote {
public:Quote() = default;	// 对成员依次进行默认初始化Quote(const Quote &) = default;	// 对成员依次拷贝Quote(Quote &&) = default;	// 对成员依次拷贝Quote &operator=(const Quote &) = default;	// 拷贝赋值Quote &operator=(Quote &&) = default;	// 移动赋值virtual ~Quote() = default;// 其他成员保持一致
};

15.7.3派生类的拷贝控制成员

派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。类似的,派生类赋值运算符也必须为其基类部分的成员赋值。

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

class Base { /* ... */ };
class D : public Base {
public:// 默认情况下,基类的默认构造函数初始化对象的基类部分,要想使用拷贝// 或移动构造函数,必须在构造函数的初始值列表中显式地调用该构造函数。D(const D &d) : Base(d) {	// 拷贝基类成员/* ... */}D(D &&d) : Base(std::move(d)) {	// 移动基类成员/* ... */}
};

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

// D的这个拷贝构造函数很可能是不正确的定义,基类
// 部分被默认初始化,而非拷贝。
D(const 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() {// 该处由用户定义清除派生类成员的操作}
};

对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数。

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

如果构造函数或析构函数调用了某个虚函数,则应该执行与构造函数或析构函数所属类型相对应的虚函数版本,因为此时对象处于未完成的状态。

15.7.4继承的构造函数

在c++11新标准中,派生类能够重用其直接基类定义的构造函数。一个类只继承其直接基类的构造函数。类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。

class Bulk_quote : public Disc_quote {
public:using Disc_quote::Disc_quote;	// 继承Disc_quote的构造函数
};

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

继承的构造函数的特点

一个构造函数的using声明不会改变该构造函数的访问级别,而且,不能指定explicitconstexpr,继承的构造函数也拥有相同的属性。
当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。
如果基类含有几个构造函数,大多数时候派生类会继承所有这些构造函数,除了以下两种情况:

  • 派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本。如果派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数将不会被继承。定义在派生类中的构造函数将替换继承而来的构造函数。
  • 默认、拷贝和移动构造函数不会被继承。

15.8容器与继承

vector<Quote> basket;
basket.push_back(Quote("0-201-82470-1", 50));
// 正确:但是只能把对象的Quote部分拷贝给basket。
basket.push_back(Bulk_quote("0-201-54848-8", 50, 10, 0.25));
// 调用Quote定义的版本,打印750,即15 * 50。
cout << basket.back().net_price(15) << endl;

当派生类对象被赋值给基类对象时,其中的派生类部分将被切掉,因此容器和存在继承关系的类型无法兼容。

在容器中放置(智能)指针而非对象

当希望在容器中存放具有继承关系的对象时,实际上存放的通常是基类的指针(更好的选择是智能指针):

vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-82470-1", 50));
basket.push_back(make_shared<Bulk_quote>("0-201-54848-8", 50, 10, 0.25));
// 调用Quote定义的版本;打印562.5,即在15 * 50中扣除掉折扣金额。
cout << basket.back()->net_price(15) << endl;

相关文章:

15.面向对象程序设计

文章目录面向对象程序设计15.1OOP&#xff1a;概述继承动态绑定15.2定义基类和派生类15.2.1定义基类成员函数与继承访问控制与继承15.2.2定义派生类派生类对象及派生类向基类的类型转换派生类构造函数派生类使用基类的成员继承与静态成员派生类的声明被用作基类的类防止继承的发…...

Element UI框架学习篇(一)

Element UI框架学习篇(一) 1.准备工作 1.1 下载好ElementUI所需要的文件 ElementUI官网 1.2 插件的安装 1.2.1 更改标签的时实现自动修改 1.2.2 element UI提示插件 1.3 使用ElementUI需要引入的文件 <link rel"stylesheet" href"../elementUI/element…...

【算法】【C语言】

差分算法力扣1094题目描述学习代码思考力扣1094 题目描述 车上最初有 capacity 个空座位。车 只能 向一个方向行驶&#xff08;也就是说&#xff0c;不允许掉头或改变方向&#xff09; 给定整数 capacity 和一个数组 trips , trip[i] [numPassengersi, fromi, toi] 表示第 …...

【✨十五天搞定电工基础】基本放大电路

本章要求1. 理解放大电路的放大作用和共发射极放大电路的性能特点&#xff1b; 2. 掌握静态工作点的估算方法和放大电路的微变等效电路分析法&#xff1b; 3. 了解放大电路输入、输出电阻和电压放大倍数的计算方法&#xff0c;了解放大电路的频率特性、 互补功率放大…...

MyBatis 入门教程详解

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…...

shiro、springboot、vue、elementUI CDN模式前后端分离的权限管理demo 附源码

shiro、springboot、vue、elementUI CDN模式前后端分离的权限管理demo 附源码 源码下载地址 https://github.com/Aizhuxueliang/springboot_shiro.git 前提你电脑的安装好这些工具&#xff1a;jdk8、idea、maven、git、mysql&#xff1b; shiro的主要概念 Shiro是一个强大…...

智能优化算法——粒子群优化算法(PSO)(小白也能看懂)

前言&#xff1a; 暑假期间&#xff0c;因科研需要&#xff0c;经常在论文中看到各种优化算法&#xff0c;所以自己学习了一些智能优化的算法&#xff0c;做了一些相关的纸质性笔记&#xff0c;寒假一看感觉又有点遗忘了&#xff0c;并且笔记不方便随时查看&#xff0c;所以希…...

Lesson 6.4 逻辑回归手动调参实验

文章目录一、数据准备与评估器构造1. 数据准备2. 构建机器学习流二、评估器训练与过拟合实验三、评估器的手动调参在补充了一系列关于正则化的基础理论以及 sklearn 中逻辑回归评估器的参数解释之后&#xff0c;接下来&#xff0c;我们尝试借助 sklearn 中的逻辑回归评估器&…...

Oracle数据库入门大全

oracle数据库 Oracle 数据库、实例、用户、表空间、表之间的关系 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pSv0SArH-1675906973035)(vx_images/573695710268888.png 676x)] 数据库 数据库是数据集合。Oracle是一种数据库管理系统&#xff…...

C语言操作符详解(下)

提示&#xff1a;本篇内容是C语言操作符详解下篇 文章目录前言八、条件表达式九、逗号表达式十、 下标引用、函数调用和结构成员1. [ ] 下标引用操作符2. ( ) 函数调用操作符3.结构成员访问操作符十一、表达式求值1. 隐式类型转换举例说明1举例说明2举例说明32.算数转换3.操作…...

【五六七人口普查】我国省市两级家庭户住房状况

人口数据是我们在各项研究中最常使用的数据&#xff01;之前我们分享过第七次人口普查&#xff08;简称七普&#xff09;的数据&#xff01;很多小伙伴拿到数据后都反馈数据非常好用&#xff0c;同时很多小伙伴咨询有没有前面几次人口普查的数据&#xff0c;这样方便做人口变化…...

大数据框架之Hadoop:入门(二)从Hadoop框架讨论大数据生态

第2章 从Hadoop框架讨论大数据生态 2.1 Hadoop是什么 Hadoop是一个由Apache基金会所开发的分布式系统基础架构。主要解决&#xff0c;海量数据的存储和海量数据的分析计算问题。广义上来说&#xff0c;Hadoop通常是指一个更广泛的概念-Hadoop生态圈。 2.2 Hadoop发展历史 1&…...

负载均衡反向代理下的webshell上传+apache漏洞

目录一、负载均衡反向代理下的webshell上传1、nginx 负载均衡2、搭建环境3、负载均衡下的 WebShell连接的难点总结难点一、需要在每一台节点的相同位置都上传相同内容的 WebShell难点二、无法预测下次的请求交给哪台机器去执行。难点三、下载文件时&#xff0c;可能会出现飘逸&…...

打造安全可信的通信服务,阿里云云通信发布《短信服务安全白皮书》

随着数字化经济的发展&#xff0c;信息保护和数据安全成为企业、个人关注的焦点。近日&#xff0c;阿里云云通信发布《短信服务安全白皮书》&#xff0c;该白皮书包含安全责任共担、安全合规、安全架构三大板块&#xff0c;呈现了阿里云云通信在信息安全保护方面的技术能力、安…...

Python项目实战——外汇牌价(附源码)

前言 几乎每个人都在使用银行卡&#xff0c;今天我们就来爬取某行外汇牌价&#xff0c;获取我们想要的数据。 环境使用 python 3.9pycharm 模块使用 requests 模块介绍 requestsrequests是一个很实用的Python HTTP客户端库&#xff0c;爬虫和测试服务器响应数据时经常会用到&…...

String、StringBuffer、StringBuilder有什么区别?

第5讲 | String、StringBuffer、StringBuilder有什么区别&#xff1f; 今天我会聊聊日常使用的字符串&#xff0c;别看它似乎很简单&#xff0c;但其实字符串几乎在所有编程语言里都是个特殊的存在&#xff0c;因为不管是数量还是体积&#xff0c;字符串都是大多数应用中的重要…...

python基于django+vue的高铁地铁火车订票管理系统

目录 1 绪论 1 1.1课题背景 1 1.2课题研究现状 1 1.3初步设计方法与实施方案 2 1.4本文研究内容 2 2 系统开发环境 4 2.1 使用工具简介 4 2.2 环境配置 4 2.4 MySQL数据库 5 2.5 框架介绍 5 3 系统分析 6 3.1系统可行性分析 6 3.1.1经济可行性 6 3.1.2技术可行性 6 3.1.3运行可…...

全栈自动化测试技术笔记(一):前期调研怎么做

昨天下午在家整理书架&#xff0c;把很多看完的书清理打包好&#xff0c;预约了公益捐赠机构上门回收。 整理的过程中无意翻出了几年前的工作记事本&#xff0c;里面记录了很多我刚开始做自动化和性能测试时的笔记。 虽然站在现在的角度来看&#xff0c;那个时候无论是技术细…...

专家培养计划

1、先知道一百个关键词 进入一个行业&#xff0c;如果能快速掌握其行业关键词&#xff0c;你会发现&#xff0c;你和专家的距离在迅速缩短。 若不然&#xff0c;可能同事间的日常交流&#xff0c;你都会听得云里雾里&#xff0c;不知所云。 比如做零售&#xff0c;就要了解零售…...

583. 两个字符串的删除操作 72. 编辑距离

583. 两个字符串的删除操作 dp[i][j]:以i-1结尾的word1和j-1结尾的word2 变成相同字符串最少的步骤为dp[i][j] 初始化dp[i][0],dp[0][j]为空字符串和第一个字符匹配的最少步骤&#xff0c;即i/j&#xff0c;删除对应的字符个数。dp[i][0]i,dp[0][j]j; 遍历两个字符串。 若word1…...

[多线程进阶] 常见锁策略

专栏简介: JavaEE从入门到进阶 题目来源: leetcode,牛客,剑指offer. 创作目标: 记录学习JavaEE学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 目录: 1. 常见的锁策略 1.1 乐观锁 vs 悲观锁 1.2 读写…...

Scala - Idea 项目报错 Cannot resolve symbol XXX

一.引言 Idea 编译 Scala 项目大面积报错 Cannot resolve symbol xxx。 二.Cannot resolve symbol xxx 1.问题描述 Idea 内的 Scala 工程打开后显示下述异常&#xff1a; 即 Scala 常规语法全部失效&#xff0c;代码出现大面积红色报错。 2.尝试解决方法 A.设置 Main Sourc…...

信息化发展与应用的新特点

一、信息化发展与应用二、国家信息化发展战略三、电子政务※四、电子商务五、两化融合&#xff08;工业和信息化&#xff09;六、智慧城市 一、信息化发展与应用 我国在“十三五”规划纲要中&#xff0c;将培育人工智能、移动智能终端、第五代移动通信(5G)先进传感器等作为新…...

软件测试】测试时间不够了,我很慌?项目马上发布了......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 常见的几种情况&…...

MapReduce编程规范

MapReduce编程规范 MapReduce的开发一共有八个步骤,其中Map阶段分为2个步骤&#xff0c;Shuffle阶段4个步骤&#xff0c;Reduce阶段分为2个步骤。 Map阶段2个步骤 设置InputFormat类,将数据切分为Key-Value(K1和V1)对,输入到第二步。 自定义Map逻辑,将第一步的结果转换成另外的…...

Unity 如何实现游戏Avatar角色头部跟随视角转动

文章目录功能简介实现步骤获取看向的位置获取头部的位置修改头部的朝向限制旋转角度超出限制范围时自动回正如何让指定动画不受影响功能简介 如图所示&#xff0c;当相机的视角转动时&#xff0c;Avatar角色的头部会同步转动&#xff0c;看向视角的方向。 实现步骤 获取看向的…...

深度学习优化算法总结

深度学习的优化算法 优化的目标 优化提供了一种最大程度减少深度学习损失函数的方法&#xff0c;但本质上&#xff0c;优化和深度学习的目标不同。 优化关注的是最小化目标&#xff1b;深度学习是在给定有限数据量的情况下寻找合适的模型。 优化算法 gradient descent&#xf…...

CMake详细使用

1、CMake简介CMake是一个用于管理源代码的跨平台构建工具可以方便地根据目标平台和编译工具产生对应的编译文件主要用于C/C语言的构建&#xff0c;但是也可以用于其它编程语言的源代码。如同使用make命令工具解析Makefile文件一样cmake命令工具依赖于一个CMakeLists.txt的文件该…...

【数据结构与算法】前缀树的实现

&#x1f320;作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《数据结构与算法要啸着学》 &#x1f387;座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录&#x1f449;…...

canvas 制作2048

效果展示 对UI不满意可以自行调整&#xff0c;这里只是说一下游戏的逻辑&#xff0c;具体的API调用不做过多展示。 玩法分析 2048 的玩法非常简单&#xff0c;通过键盘的按下&#xff0c;所有的数字都向着同一个方向移动&#xff0c;如果出现两个相同的数字&#xff0c;就将…...

想学Wordpress建站/互联网整合营销推广

面向对象设计在网上一直说不清到底是几种&#xff0c;5种&#xff1f;6种&#xff1f;7种&#xff1f; 这里我截取了《Java设计模式》一书的说法。7种设计原则6原则1法则。 7种原则主要从高内聚、低耦合的考量得出的。主要焦距在抽象、接口、实体的关系间。面向对象编程的三大…...

苏州网站建设与网络营销/网站关键词搜索排名优化

常用模块 一、random模块 import random# print(random.random()) #打印0-1之间的小数 # print(random.randint(1,3)) #大于等于1小于等于3之间的整数 # print(random.randrange(1,5)) #大于等于1小于5之间的整数 # print(random.choice([1,23,[4,5]])) #1或者23&#xff…...

湘潭本地的网站建设公司/投放广告

oracle 11g 增加了新的分区类型&#xff0c;总结一下目前之前的分区表区间分区散列分区列表分区区间分区&#xff1a;create table gh_range_example(id varchar2(100),range_date date not null)partition by range(range_date)(partition range_15 values less than (to_date…...

wordpress内容页主题修改/百度站长平台链接

一、RabbitMQ简述与其docker安装 这里主要讲解实战整合rabbitMQ&#xff0c;了解RabbitMQ简述与其docker安装请点击&#xff1a;传送门 二、springboot整合rabbitMQ 1.新建springboot项目 2.pom:主要添加以下两个依赖 <dependency><groupId>org.springframework.bo…...

网站建设源程序/软文推广渠道主要有

2019独角兽企业重金招聘Python工程师标准>>> mac上使用py import MySQLDb, 报出一下错误&#xff1a; ImproperlyConfigured: Error loading MySQLdb module: dlopen(/Library/Python/2.7/site-packages/_mysql.so, 2): Library not loaded: libmysqlclient.18.dyli…...

关于网站建设需要了解什么东西/搜狗站长工具综合查询

Android横竖屏切换小结 &#xff08;老样子&#xff0c;图片啥的详细文档&#xff0c;可以下载后观看 http://files.cnblogs.com/franksunny/635350788930000000.pdf&#xff09; Android手机或平板都会存在横竖屏切换的功能&#xff0c;通常是由物理重力感应触发的&#xff0c…...