面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。 这样做,也达到了重用代码功能和提高执行时间的效果。
- 被继承者称为基类
- 继承者成为派生类
继承代表了 is a 关系
继承的语法如下
class derived-class: access-specifier base-class
访问修饰符 access-specifier 是 public、protected 或 private 其中的一个
base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。
派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private
。
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
当一个类派生自基类,该基类可以被继承为 public
、protected
或 private
几种类型。
继承类型是通过访问修饰符来指定的。
- 公有继承(
public
):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员, 基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。 - 保护继承(
protected
): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。 - 私有继承(
private
):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
接口继承:public 派生类继承基类的接口,它具有与基类相同的接口。设计良好的类层次中,public 派生类的对象可以用在任何需要基类对象的地方。
实现继承:使用 private 或 protected 派生的类不继承基类的接口,相反,这些派生通常被称为实现继承。派生类在实现中使用被继承但继承基类的部分并未成为其接口的一部分。
多继承即一个子类可以有多个父类,它继承了多个父类的特性。
class <derived-class>:<inheritance-mode 1><base-class1>,:<inheritance-mode 2><base-class2>,…
{
};
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
如果基类中的函数不是虚函数,那么即使派生类重写了该函数,当以基类形态去调用实际类型为派生类的函数时,调用的是基类中的函数。 即函数调用在程序执行前就准备好了。有时候这也被称为早绑定
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。 在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
动态绑定的条件,C++ 中的函数调用默认不使用动态绑定。要触发动态绑定,满足两个条件:
- 只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;
- 必须通过基类类型的引用或指针进行函数调用。
在基类中不对虚函数给出有意义的实现,这个时候就是纯虚函数。
虚函数声明:virtual ReturnType FunctionName(Parameter)
,虚函数必须实现,如果不实现,编译器将报错
- 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。
- 包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
- 对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
- 虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
- 在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
- 友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
- 析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。
- 如果基类的虚函数不是纯虚函数,则派生类可以在实现中调用基类的虚函数
在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这里可以使用作用域操作符
Item_base *baseP = &derived;
// 这里从基类Item_base调用版本,而不考虑动态类型的baseP。
double d = baseP->Item_base::net_price(42);
如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。
static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类不能访问它。假定可以访问成员,则既可以通过基类访问 static 成员,也可以通过派生类访问 static 成员。 一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系, 则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。
派生类的合成默认构造函数与非派生的构造函数只有一点不同,即除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。
class A{
public:
std:string name;
int a;
}
class B: A{
public:
int b;
}
对于 B 类,合成的默认构造函数会这样执行:
- 调用A的默认构造函数,将 name 成员初始化空串,将 a 成员初始化为 0。
- B的成员初始化取决于B类所在的存储类型(局部或者全局)
使用定义的构造函数可以向基类构造函数传递实参来初始化基类成员
一个类只能初始化自己的直接基类。直接就是在派生列表中指定的类。如果类 C 从类 B 派生,类 B 从类 A 派生,则 B 是 C 的直接基类。 虽然每个 C 类对象包含一个 A 类部分,但 C 的构造函数不能直接初始化 A 部分。相反,需要类 C 初始化类 B,而类 B 的构造函数再初始化类 A。 这一限制的原因是,类 B 的作者已经指定了怎样构造和初始化 B 类型的对象。像类 B 的任何用户一样,类 C 的作者无权改变这个规约。
如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。 被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值。
如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分:
class Base { /* ... */ };
class Derived: public Base {
public:
Derived(const Derived& d):Base(d)
}
赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。
Base::operator=(const Base&) //不自动调用
Derived &Derived::operator=(const Derived &rhs){
if (this != &rhs) {
Base::operator=(rhs); //调用基类
// 然后在做其他复制操作
}
return *this;
}
析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是自动调用派生类对象基类部分的析构函数。 每个析构函数只负责清除自己的成员,对象的撤销顺序与构造顺序相反:首先运行派生析构函数,然后按继承层次依次向上调用各基类析构函数。
- 即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
- 在复制控制成员中,只有析构函数应定义为虚函数,构造函数不能定义为虚函数。
- 将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。
如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基 类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个 派生类对象。 撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序 撤销它的基类部分。在这两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。为了适应这种不完整, 编译器将对象的类型视为在构造或析构期间发生了变化。在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。
在继承情况下,派生类的作用域嵌套在基类作用域中。如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义。
对象、引用或指针的静态类型决定了对象能够完成的行为。甚至当静态类型和动态类型可能不同的时候, 就像使用基类类型的引用或指针时可能会发生的,静态类型仍然决定着可以使用什么成员。
- 与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。可以使用作用域操作符访问被屏蔽的基类成员。
- 在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽。
struct Base {
int memfcn();
};
struct Derived : Base {
int memfcn(int); //隐藏Base的memfcn
};
Derived d; Base b;
b.memfcn(); // 调用Base的memfcn
d.memfcn(10); // 调用Drived的memfcn
d.memfcn(); // 错误
d.Base::memfcn(); // 调用Base的memfcn
局部作用域中声明的函数不会重载全局作用域中定义的函数,同样,派生类中定义的函数也不重载基类中定义的成员。 通过派生类对象调用函数时,实参必须与派生类中定义的版本相匹配,只有在派生类根本没有定义该函数时,才考虑基类函数。
像其他任意函数一样,成员函数(无论虚还是非虚)也可以重载。派生类可以重定义所继承的0个或多个版本。 如果派生类想通过自身类型使用的重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义(否则就变成了屏蔽)。
派生类不用重定义所继承的每一个基类版本,它可以为重载成员提供 using
声明。一个 using
声明只能指定一个名字,
不能指定形参表,因此,为基类成员函数名称而作的 using
声明将该函数的所有重载实例加到派生类的作用域。
将所有名字加入作用域之后,派生类只需要重定义本类型确实必须定义的那些函数,对其他版本可以使用继承的定义。
接口描述了类的行为和功能,而不需要完成类的特定实现。如果类中至少有一个函数被声明为纯虚函数(包括定义的和继承的),则这个类就是抽象类。 设计抽象类的目的是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。