面向对象程序设计概述
面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承和动态绑定。
1.使用数据抽象,可以将类的接口与实现分离;
2.使用继承,可以定义相似的类型并对其相似关系建模;
3.使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
继承
通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类 ,其他类则直接或间接地从基类继承而来,这些继承得到的类称为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
C++ 中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。
派生类 必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。
派生类必须在其内部对所有重新定义的虚函数进行声明,可以在这样的函数前加上 virtual
关键字,但不是必须的。允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个override
关键字
cpp
//基类
class Quote{
public:
std::string isbn() const;
virtual double net_price (std: :size_t n) const;
};
//派生类
class Bulk_quote : public Quote{ //Bulk_quote继承了Quote
public:
double net_price(std: :size_t) const override;
};
动态绑定
动态绑定根据传入的参数类型来选择函数版本(可能是基类中的该函数或派生类中的该函数),它发生在运行时,有时也叫运行时绑定。
在C++中,使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。
定义基类与派生类
定义基类
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也应该这样。
成员函数与继承
派生类可以继承其基类的成员,但需要时,需要提供自己新定义的操作以覆盖(override)从基类继承而来的旧定义。
基类需要将两种成员函数区分开:
1.基类希望其派生类进行覆盖的函数,通常将其定义为虚函数;
2.基类希望派生类直接继承而不需要改变的函数。
使用指针或引用调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。
基类通过在其成员函数的声明语句之前加上关键字virtual
使得该函数执行动态绑定。
任何构造函数之外的非静态函数都可以是虚函数 。关键字virtual
只能出现在类内部的声明语句之前而不能用于类外部的函数定义。
如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
虚函数的解析过程发生在运行时,普通函数的解析过程发生在编译时。
访问控制与继承
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。
派生类能访问公有成员和受保护的成员,而不能访问私有成员。
基类希望派生类有权访问该成员,同时禁止其他用户访问。可以用受保护的protected
访问运算符说明这样的成员。
定义派生类
派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。
类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有以下三种访问说明符中的一个: public
、 protected
或者 private
。
cpp
class Bulk_quote : public Quote {
};
如果一个派生是公有的,则基类的公有成员也是派生类接口的组成部分。并且能够将公有派生类型的对象绑定到基类的引用或指针上。
派生类中的虚函数
派生类经常(但不总是)覆盖它继承的虚函数。
如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于普通成员,派生类会直接继承其在基类中的版本。
C++11 允许使用 override
关键字显式地指明重新定义的虚函数,把 override
放到形参列表后面、或 const
成员函数的 const
关键字后面、或引用成员函数的引用限定符后面。
派生类对象及派生类向基类的类型转换
一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。
因为派生类对象中含有与基类对应的组成部分,所以可以把派生类的对象当成基类对象来使用,也能把基类的指针或引用绑定到派生类对象中的基类部分上。
cpp
Quote item; //基类对象
Bulk_quote bulk; //派生类对象
Quote *p = &item; //p 指向 Quote 对象
p=&bulk; //p 指向 bulk 的 Quote 部分
Quote &r = bulk; //r 绑定到 bulk 的 Quote部分
派生类构造函数
派生类并不能直接初始化从基类继承而来的成员,派生类必须使用基类的构造函数来初始化它的基类部分。
每个类控制它自己的成员初始化过程。
派生类对象的基类部分与派生类对象自己的数据成员都是在构造函数的初始化阶段执行初始化操作的。类似于初始化成员的过程,派生类构造函数同样是通过构造函数初始化列表来将实参传递给基类构造函数的。
cpp
Bulk_quote(const std::string& book, double p,
std::size_t qty, double disc):
Quote(book,p), min_qty(qty), discount (disc) {}
//与之前一致
};
除非特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化。
如果想使用其他的基类构造函数,需要以类名加圆括号内的实参列表的形式为构造函数提供初始值。这些实参将帮助编译器决定到底应该选用哪个构造函数来初始化派生类对象的基类部分。
派生类使用基类的成员
每个类负责定义各自的接口。要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此。因此,派生类对象不能直接初始化基类的成员。
继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。
静态成员遵循通用的访问控制规则,如果某静态成员是可访问的,则既能通过基类使用它也能通过派生类使用它。
cpp
class Base{
public:
static void statmem();
};
class Derived : public Base{
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj){
Base::statmem(); //正确:Base定义了statmem
Derived::statmem(); //正确:Derived继承了statmem
//正确:派生类的对象能访问基类的静态成员
derived_obj.statmem();//通过Derived对象访问
statmem();//通过this对象访问
};
派生类的声明
派生类的生命中包含类名但是不包含它的派生列表。
cpp
class Bulk_quote : public Quote; //错误:派生列表不能出现在这里
class Bulk_quote; //正确:声明派生类的正确方式
被用作基类的类
必须完整定义某个类后,该类才能作为基类被其他类继承。只声明是不够的。
继承可以多重继承,最终的派生类将包含它的直接基类的子对象和每一个间接基类的子对象。
阻止继承的发生
如果定义了一个类并不希望它被其他类继承,可以在类名后跟一个关键字 final。
cpp
class NoDerived final {/**/};//类 NoDerived 是不能被继承的
类型转换与继承
理解基类和派生类之间的类型转换是理解 C++ 面向对象编程的关键所在。
可以将基类的指针或引用绑定到派生类对象上有一层极为重要的含义:当使用基类的引用(或指针)时,实际上我们并不清楚该引用(或指针)所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。
和内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内。
静态类型和动态类型
使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型(static type)与该表达式表示对象的动态类型(dynamic type)区分开来。
表达式的静态类型在编译时总是已知的 ,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。基类的指针或引用的静态类型可能与其动态类型不一致。
不存在从基类向派生类的隐式类型转换
因为每个派生类对象都包含一个基类部分,所以存在派生类向基类的类型转换。而基类的引用或指针可以绑定到该基类部分上。
一个基类的对象既可以以独立的形式存在,也可以作为派生类对象的一部分存在。如果基类对象不是派生类对象的一部分,则它只含有基类定义的成员,而不含有派生类定义的成员。
因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。
cpp
Quote base;
Bulk_quote* bulkP =& base; //错误:不能将基类转换成派生类
Bulk_quote& bulkRef = base; //错误:不能将基类转换成派生类
编译器在编译时无法确定某个特定的转换在运行时是否安全,这是因为编译器只能通过检查指针或引用的静态类型来推断该转换是否合法。
在对象之间不存在类型转换
派生类向基类的自动类型转换只对指针或引用类型有效。
初始化或赋值一个类类型的对象时,实际上是在调用某个函数。实际是调用构造函数或赋值运算符。它们通常包含一个参数:该参数类型是类类型的 const
引用。此时是可以将派生类对象赋值给基类对象的,实际运行的是以引用作为参数的赋值运算符。
当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,其他部分会被忽略掉。
继承关系的类型之间的转换规则 :
1.从派生类向基类的类型转换只对指针或引用类型有效。
2.基类向派生类不存在隐式类型转换。
3.和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行。
虚函数
使用基类的引用或指针调用一个虚函数时会执行动态绑定。因为直到运行时才知道到底调用了哪些版本的虚函数,所以所有虚函数都必须有定义。
通常情况下,如果不使用某个函数,则无需为该函数提供定义。但必须为每一个虚函数都提供定义,而不管它是否被用到(因为编译器也无法确定到底会使用哪个虚函数)。
对虚函数的调用可能在运行时才被解析
当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。
被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。
派生类中的虚函数
当在派生类中覆盖了某个函数时,可以不适用 virtual
关键字指出该函数的性质。因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数 。
一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。同样,派生类中虚函数的返回类型也必须与基类函数匹配。
final 和 override 说明符
派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同是合法的行为。编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。
C++ 中可以使用 override
关键字来说明派生类中的虚函数。这时如果该函数没有覆盖基类中的虚函数,编译器就会报错。
可以把某个函数指定为 final
,任何尝试覆盖该函数的操作都会引发错误。
final
和 override
都写到形参列表和尾置返回类型之后。
虚函数与默认实参
虚函数也可以有默认实参,实参值由调用的静态类型决定。即如果通过基类的指针或引用调用虚函数,则使用基类中定义的默认实参。
如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
回避虚函数的机制
有时希望对虚函数的调用不进行动态绑定,而是强迫执行虚函数的某个特定版本,可以通过作用域运算符来实现。
cpp
//强行调用基类中定义的函数版本而不管baseP的动态类型到底是什么
double undiscounted = baseP->Quote::net price(42);
通常情况下,只有在成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。
通常当一个派生类的虚函数需要调用它的基类版本时,需要回避虚函数的默认机制。
抽象基类
纯虚函数
可以将一个没有实际意义的虚函数定义为纯虚函数,只需当在类内对它进行声明时最后加一个 =0
即可,无需额外定义。
cpp
class Disc guote : public Quote{
public:
Disc_quote()=defalut;
double net_price(std::size_t) const = 0;
也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,不能在类的内部为一个=0
的函数提供函数体。
含有纯虚函数的类是抽象基类
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。
不能创建抽象基类的对象。
派生类构造函数只初始化它的直接基类。
访问控制与继承
每个类分别控制自己的成员初始化过程,与之类似,每个类还分别控制着其成员对于派生类来说是否可访问。
受保护的成员
一个类使用protected
关键字来声明那些它希望与派生类分享但是不想被其他公共访问使用的成员。
protected
说明符可以看做是 public
和private
的中和:
1.和私有成员类似,受保护的成员对于类的用户来说是不可访问的 ;
2.和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的 。
3.派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
cpp
class Base {
protected:
int prot mem;// protected成员
};
class Sneaky : public Base{
friend void clobber(Sneaky&);//能访问Sneaky::prot_mem
friend void clobber(Base&);//不能访问 Base::prot_mem
int 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;}
公有、私有和受保护继承
某个类对其继承而来的成员的访问权限受到两个因素影:
一是在基类中该成员的访问说明符;二是在派生类的派生列表中的访问说明符。
cpp
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;}
}
派生列表中的访问说明符不会影响派生类自身的成员和友元对基类的访问权限,对直接基类的访问权限只与基类中的访问说明符有关。它影响的是派生类的用户(包括派生类的对象、派生类的派生类)对基类成员的访问权限。
- 如果继承是公有的,则在派生类中,基类的成员将遵循原有的访问说明符。
- 如果继承是受保护的,则基类的所有公有成员在派生类中都是受保护的。
- 如果继承是私有的,则基类的所有公有和受保护成员在派生类中都是私有的。
派生类向基类转换的可访问性
派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。
假定 D 继承自 B :
1.只有当 D 公有地继承 B 时,用户代码才能使用派生类向基类的转换:如果 D 继承 B 的方式是受保护的或者私有的,则用户代码不能使用该转换。
2.不论 D 以什么方式继承 B,D 的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
3.如果 D 继承 B 的方式是公有的或者受保护的,则 D 的派生类的成员和友元可以使用 D 向 B 的类型转换;反之,如果D继承B的方式是私有的,则不能使用。
友元与继承
友元关系不能传递,也不能继承。
基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员。每个类负责控制各自成员的访问权限。
改变个别成员的可访问性
可以通过 using
声明来改变派生类继承的某个名字的访问级别。
派生类只能为那些它可以访问的名字提供using
声明:
using
声明位于 public
部分就是公有成员,位于 private
部分就是私有成员,位于 protected
部分就是受保护成员。
cpp
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;
};
默认的继承保护机制
和直接定义相似,当继承时不使用访问说明符,
默认情况下,使用 class 关键字定义的派生类默认是私有继承的,使用 struct 关键字定义的派生类默认是公有继承的。
cpp
class Base {/ * ...*/};
struct D1 : Base { /*...*/}; //默认 public 继承
class D2 : Base(/*...*/}; //默认 private 继承
继承中的类作用域
每个类定义自己的作用域,在这个作用域内定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域内。
如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
派生类的作用域嵌套在基类的作用域内。
cpp
Bulk_quote bulk;
cout<< bulk.isbn();
//因为是通过 Bulk_quote 的对象调用 isbn 的,所以首先在 Bulk_quote 中查找,这一步没有找到名字 isbn。
//因为 Bulk quote 是 Disc_quote的派生类,所以接下来在 Disc_quote 中查找,仍然找不到。
//因为 Disc_quote 是 Quote 的派生类,所以接着查找 Quote;此时找到了名字 isbn,所以使用的 isbn 最终被解析为 Quote 中的 isbn。
在编译时进行名字查找
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况)。能使用哪些成员仍然是由静态类型决定的。
名字冲突与继承
和其他作用域一样,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字。
派生类的成员将隐藏同名的基类成员。
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
可以通过作用域运算符来使用隐藏的成员:
cpp
struct Derived:Base{
int get_base_mem(){return Base::mem;}
//...
};
//作用域运算符将覆盖掉原有的查找规则,并指示编译器从Base类的作用域开始查找mem。
虚函数与作用域
假如基类与派生类的虚函数接受的实参不同,则无法通过基类的引用或指针调用派生类的虚函数。
成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的 0 个或多个示例。
构造函数与拷贝控制
位于继承体系中的类需要控制当其对象执行一系列操作时发生什么样的行为,这些操作包括创建、拷贝、移动、赋值和销毁。
如果一个类(基类或派生类)没有定义拷贝控制操作,则编译器将为它合成一个版本。这个合成的版本也可以定义成被删除的函数。
虚析构函数
当delete
一个动态分配的对象的指针时将执行析构函数。
如果该指针指向继承体系中的某个类型,则有可能出现指针的静态类型与被删除对象的动态类型不符的情况。
析构函数的虚属性也会被继承。
如果基类的析构函数不是虚函数,则delete
一个指向派生类对象的基类指针将产生未定义的行为。
虚析构函数将阻止合成移动操作
基类需要一个虚析构函数会对基类和派生类的定义产生另外一个间接的影响:如果一个类定义了析构函数,即使它通过 =default
的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
合成拷贝控制与继承
基类或派生类的合成拷贝控制成员的行为与其他合成的构造函数、赋值运算符或析构函数类似:它们对类本身的成员依次进行初始化、赋值或销毁的操作。此外,这些合成的成员还负责使用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作。
无论基类成员是合成的版本还是自定义的版本都没有太大影响。唯一的要求是相应的成员应该可访问并且不是一个被删除的函数。
派生类中删除的铂贝控制与基类的关系:
基类或派生类能将其合成的默认构造函数或者任何一个拷贝控制成员定义成被删除的函数。此外,某些定义基类的方式也可能导致有的派生类成员成为被删除的函数:
1.如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作 。
2.如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分 。
3.和过去一样,编译器将不会合成一个删除掉的移动操作。当我们使用=default
请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的。
移动操作与继承
因为大多数基类都会定义一个虚析构函数,所以默认情况下基类通常没有合成的移动操作,在它的派生类中也没有。
如果需要执行移动操作,首先要在基类中定义。之后派生类会自动合成移动操作。
cpp
class Quote {
public:
Quote()= default; //对成员依次进行默认初始化
Quote (const Quote&) = default; //对成员依次移动拷贝
Quote (Quote&&) = default; //对成员依次kao贝
Quote& operator= (const Quote&) = default; //拷贝赋值
Quote& operator= (Quote&&) = default;//移动赋值
virtual ~Quote() = default;
//其他成员与之前的版本一致
};
派生类的拷贝控制成员
派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员,还负责初始化派生类对象的基类部分。因此,派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。
派生类赋值运算符也必须为其基类部分的成员赋值。
析构函数不同,因为析构部分是隐式销毁的,基类部分也是自动销毁的,不需要派生类来负责。
在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。
无论基类的构造函数或赋值运算符是自定义的版本还是合成的版本,派生类的对应操作都能使用它们。
在构造函数和析构函数中调用虚函数
派生类对象的基类部分将首先被构建。当执行基类的构造函数时,该对象的派生类部分是未被初始化的状态。而销毁派生类对象的次序正好相反,因此当执行基类的析构函数时,派生类已经销毁掉了。
因此,执行上述基类成员的时候,该对象处于未完成的状态。
如果构造函数或析构函数调用了某个虚函数,则应该执行与构造函数或析构函数所属类型相对应的虚函数版本。
因为,当执行基类构造函数时,要用到的派生类对象尚未初始化,如果允许访问,程序会奔溃。
继承的构造函数
派生类能够重用其直接基类定义的构造函数。这些构造函数并非以常规的方式继承而来,但仍然称之为"继承"。
派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的using
声明语句:
cpp
class Bulk_quote : public Disc_guote{
public:
using Disc_quote::Disc_quote;//继承Disc_quote的构造函数
double net price(std::size_t) const;
};
//通常,using 声明语句只是令某个名字在当前作用域内可见。而当作用于构造函数时,using 声明语句将令编译器产生代码。
对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数:
cpp
derived(parms) : base(args) { }
//其中,derived是派生类的名字,base是基类的名字,parms是构造函数的形参列表,args将派生类构造函数的形参传递给基类的构造函数.
如果派生类有自己的数据成员,这些成员将被默认初始化。
理解:继承的构造函数相当于派生类采用基类的构造函数来初始化自己的基类部分,而派生类中定义的成员采用默认初始化。也可以直接定义具有相同功能的构造函数。
继承的构造函数的特点
和普通成员的using
声明不一样,一个构造函数的using
声明不会改变该构造函数的访问级别。而且,一个 using
声明语句不能指定explicit
或constexpr
。
如果基类的构造函数是explicit
或constexpr
,则继承的构造函数也有相同的属性。
当一个基类构造函数含有默认实参时,这些实参并不会被继承。派生类会获得多个继承的构造函数,每个构造函数分别省略掉一个含有默认实参的形参。
如果基类含有几个构造函数,大多数时候派生类会继承所有这些构造函数。除了两个例外情况:
1.如果基类的某个构造函数和派生类自己定义的构造函数具有相同的参数列表,则该构造函数不会被继承。
2.默认、拷贝、移动构造函数不会被继承。
容器与继承
使用容器存放继承体系中的对象时,通常必须采取间接存储的方式。因为不允许在容器中保存不同类型的元素,所以不能把具有继承关系的多种类型的对象直接存放在容器当中。不能把基类和派生类同时放到一个容器中。
在容器中放(智能)指针而非对象
当要在容器中存放继承体系中的对象时,使用基类指针作为容器的元素类型。
cpp
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,.25));
//调用Quote定义的版本;打印562.5,即在15*&50中扣除掉折扣金额
cout << basket.back()->net price (15)<<endl;
重要术语
抽象基类: 含有一个或多个纯虚函数的类,我们无法创建抽象基类的对象。
可访问的: 能被派生类对象访问的基类成员。可访问性由派生类的派生列表中所用的访问说明符和基类中成员的访问级别共同决定。
类派生列表: 罗列了所有基类,每个基类包含一个可选的访问级别,它定义了派生类继承该基类的方式。如果没有提供访问说明符,则当派生类通过关键字struct
定义时继承是公有的:而当派生类通过关键字class
定义时继承是私有的。
动态绑定: 直到运行时才确定到底执行函数的哪个版本。在C++中,动态绑定的意思是在运行时根据引用或指针所绑定对象的实际类型来选择执行虚函数的某一个版本。
虚函数: 用于定义类型特定行为的成员函数。通过引用或指针对虚函数的调用直到运行时才被解析,依据是引用或指针所绑定对象的类型。
纯虚函数: 在类的内部声明虚函数时,在分号之前使用了=0
。----个纯虚函数不需要(但是可以)被定义。含有纯虚函数的类是抽象基类。如果派生类没有对继承而来的纯虚函数定义自己的版本,则该派生类也是抽象的。
覆盖: 派生类中定义的虚函数如果与基类中定义的同名虚函数有相同的形参列表,则派生类版本将覆盖基类的版本。
多态性: 当用于面向对象编程的范畴时,多态性的含义是指程序能通过引用或指针的动态类型获取类型特定行为的能力。