赣州网站建设wordpress主题 有分页

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

赣州网站建设,wordpress主题 有分页,番禺营销型网站建设,青岛市城市建设管理局网站文章目录面向对象程序设计15.1OOP#xff1a;概述继承动态绑定15.2定义基类和派生类15.2.1定义基类成员函数与继承访问控制与继承15.2.2定义派生类派生类对象及派生类向基类的类型转换派生类构造函数派生类使用基类的成员继承与静态成员派生类的声明被用作基类的类防止继承的发… 文章目录面向对象程序设计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; };派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的其中每个基类前面可以有public、protected或者private中的一个具体含义后面会讲解。 class Bulk_quote : public Quote { public:double net_price(std::size_t) const override; };派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字但是并不是非得这么做。 C11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数即增加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的类型是Quotebulk的类型是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是可选的) 一个派生类的函数如果覆盖了某个继承而来的虚函数则它的形参类型必须与被它覆盖的基类函数完全一致。 同样派生类中虚函数的返回类型也必须与基类函数匹配。但是存在一个例外当类的虚函数返回类型是类本身的指针或引用时上述规则无效。也就是说如果D由B派生得到则基类的虚函数可以返回B*而派生类的对应函数可以返回D*只不过这样的返回类型要求从D到B的类型转换时可访问的。 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的派生类的成员和友元可以使用D向B的类型转换反之如果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::memBase::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继承的构造函数 在c11新标准中派生类能够重用其直接基类定义的构造函数。一个类只继承其直接基类的构造函数。类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数则编译器将为派生类合成它们。 class Bulk_quote : public Disc_quote { public:using Disc_quote::Disc_quote; // 继承Disc_quote的构造函数 };通常情况下using声明语句只是令某个名字在当前作用域内可见。而当作用于构造函数时对于基类的每个构造函数编译器都在派生类中生成一个形参列表完全相同的构造函数。 继承的构造函数的特点 一个构造函数的using声明不会改变该构造函数的访问级别而且不能指定explicit或constexpr继承的构造函数也拥有相同的属性。 当一个基类构造函数含有默认实参时这些实参并不会被继承。相反派生类将获得多个继承的构造函数其中每个构造函数分别省略掉一个含有默认实参的形参。 如果基类含有几个构造函数大多数时候派生类会继承所有这些构造函数除了以下两种情况 派生类可以继承一部分构造函数而为其他构造函数定义自己的版本。如果派生类定义的构造函数与基类的构造函数具有相同的参数列表则该构造函数将不会被继承。定义在派生类中的构造函数将替换继承而来的构造函数。默认、拷贝和移动构造函数不会被继承。 15.8容器与继承 vectorQuote 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;当派生类对象被赋值给基类对象时其中的派生类部分将被切掉因此容器和存在继承关系的类型无法兼容。 在容器中放置(智能)指针而非对象 当希望在容器中存放具有继承关系的对象时实际上存放的通常是基类的指针(更好的选择是智能指针) vectorshared_ptrQuote basket; basket.push_back(make_sharedQuote(0-201-82470-1, 50)); basket.push_back(make_sharedBulk_quote(0-201-54848-8, 50, 10, 0.25)); // 调用Quote定义的版本打印562.5即在15 * 50中扣除掉折扣金额。 cout basket.back()-net_price(15) endl;