“C++ Primer”
继承
protected
基类希望它的派生类有权访问该成员,同时禁止其他用户访问
- 与
private
一样,protected
成员对类的用户是不可见的; - 与
public
一样,protected
成员对派生类的成员和友元是可见的; - 派生类的成员和友元只能通过派生对象访问基类的
protected
成员。派生类不能访问独立的基类对象的protected
成员,如:
1
2
3
4
5
6
7
8
9
10
11
12
class Base {
protected:
int prot_mem;
};
class Sneaky : public Base {
friend void clobber(Sneaky &); //可以访问 Sneaky::prot_mem
friend void clobber(Base &); //不能访问 Base::prot_mem
int j;
};
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
//错误:clobber 不能访问 Base 中的 protected 成员
void clobber(Base &b) { s.prot_mem = 0; }
public, private 和 protected 继承
派生访问说明符并不影响派生类的成员和友元对其直接基类的成员访问权限。访问直接基类的成员是由基类自身的访问说明符决定的。public
继承和private
继承的派生类都可以访问基类的protected
成员,而都不能访问基类的private
成员。
派生访问说明符的作用在于控制派生类的用户对从基类继承来的成员的访问权限.
友元和继承
正如友元不具有传递性质(一个类是另一个类的友元并不意味着这个类自己的友元可以访问那个类),友元关系不会被继承。基类的友元对于派生类成员没有特殊的访问权限,派生类的友元对于基类成员没有特殊访问权限。如:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
friend class Pal; //Pal 对 Base 的派生类没有特殊访问权限
};
class Pal {
public:
// 正确,Pal 是 Base 的友元
int f(Base b) { return b.prot_mem; }
//错误:Pal 不是 Sneaky 的友元,不能访问私有成员
int f2(Sneaky s) { return s.j; }
//对基类的访问有基类自己控制,即便基类内嵌在派生对象中
//即便要访问的是 private 成员
int f3(Sneaky s) { return s.pri_mem; }
};
派生类构造函数
对象的基类部分与派生类的数据成员一起在构造函数的初始化阶段进行初始化。与初始化成员一样,派生类构造函数使用构造初始值列表来传递参数给基类构造函数。如:
1
2
Bulk_quote(const std::string &book, double p, std::size_t qty, double disc):
Quote(book, p), min_qty(qty), discount(disc) { }
默认的继承保护机制
用class
定义的派生类默认是private
继承;用sturct
定义的派生类默认是public
继承
1
2
3
class Base {};
struct D1 : Base {}; //默认共有继承
class D2 : Base {}; //默认私有继承
class
和sturct
之间唯一的区别就是成员的默认访问说明符和默认的派生访问说明符之间的不同
静态成员
如果在基类中定义了静态成员,那么整个继承层级中只有此成员的唯一定义。不管从一个基类中派生了多少类,每个静态成员只存在一份实例。
final
C++11
提供的一种防止继承发生的方式,即在类名后加一个关键字final
动态绑定
静态类型与动态类型
表达式的静态类型在编译时就是已知的,它是变量声明时的类型或者表达式的结果类型。
动态类型是变量或表达式所表示的在内存中的真正对象的类型,这个类型必须到运行时才能知道
虚函数(virtual function)
基类将希望派生类定义自己的版本的函数为virtual
的,派生类必须在其内部对所有重新定义的虚函数进行声明
基类通常应该定义虚析构函数,即便不做任何工作也是如此
virtual
关键只出现在类体内的函数声明处,而不会被用于类体外的函数定义处。在基类中被定义为virtual
的函数,其在派生类中隐式也是virtual
的。
由于调用哪个版本是由实参的类型决定的,而实参类型只有在调用时才能知道。因而,动态绑定有时也被称为运行时绑定(run-time binding)。在C++
中,动态绑定发生在虚函数通过基类的引用或指针调用时。
抽象基类
纯虚函数
通过在函数体的位置(即在声明语句的分号之前)写上=0
就可以将一个函数说明为纯虚函数,其中=0
只能出现在类内部的虚函数声明语句处。
抽象基类
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类(abstract base class),抽象基类负责定义接口,而后续的其他类可以覆盖该接口,我们不能直接创建一个抽象基类的对象。
隐藏
如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏掉该成员,即使派生类成员与基类成员的形参列表不一致,基类成员也仍然会被隐藏掉
1
2
3
4
5
6
7
8
9
10
11
12
struct Base {
int memfcn();
};
struct Derived : Base {
int memfcn(int);
};
Derived d;
Base b;
b.memfcn();
d.memfcn(10);
d.memfcn(); //错误:Base::memfcn() 被隐藏
d.Base::memfcn(); //调用 Base::memfcn()
虚函数与作用域
如果基类和派生类的虚函数接受的实参不同,那么将无法通过基类的指针或引用调用派生类的版本。如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Base {
public:
virtual int fcn();
};
class D1 : public Base {
public:
// 形参列表与 Base 中的 fcn 不一致,隐藏基类的 fcn,同时又继承了 Base 中的虚函数 fcn,因此此时拥有两 // 个名为 fcn 的函数
int fcn(int);
virtual void f2();
};
class D2 : public D1 {
public:
int fcn(int);
int fcn();
void f2();
};
Base bobj;
D1 d1obj;
D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); // virtual call, will call Base::fcn at run time
bp2->fcn(); // virtual call, will call Base::fcn at run time
bp3->fcn(); // virtual call, will call D2::fcn at run time
D1 *d1p = &d1obj; D2 *d2p = &d2obj;
bp2->f2(); // error: Base has no member named f2
d1p->f2(); // virtual call, will call D1::f2() at run time
d2p->f2(); // virtual call, will call D2::f2() at run time
Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj;
p1->fcn(42); // error: Base has no version of fcn that takes an int
p2->fcn(42); // statically bound, calls D1::fcn(int)
p3->fcn(42); // statically bound, calls D2::fcn(int)
虚析构函数将阻止合成移动操作
基类需要虚析构函数对基类和派生类的定义有一个重大的间接影响:如果一个类定义了析构函数,即便使用的是 = default
来使用合成版本的,编译器也不会为这个合成任何移动操作。
派生类中删除的拷贝控制与基类的关系
- 如果基类的默认构造函数、拷贝构造函数或拷贝赋值操作符或析构函数是被删除的或者不可访问的,那么派生类的对应成员也被定义为被删除的函数;
- 如果基类有一个被删除的或不可访问的析构函数,那么派生类合成的默认和拷贝构造函数将是被删除的函数;
- 与往常一样,编译器不会合成被删除的移动操作。当使用
= default
来请求移动操作时,如果基类的对应操作是被删除的或者不可访问的,或者基类的析构函数是被删除的或不可访问的;
定义派生类的拷贝或移动构造函数
当定义派生类的拷贝、移动构造函数,通常需要调用基类对应的构造函数来初始化对象的基类部分。如果不调用基类的构造函数,那么编译器将隐式调用基类的默认构造函数,但这肯定是不正确的。如果想要拷贝、移动基类部分,需要在构造函数初始值列表中显式调用基类对象的拷贝、移动构造函数。
1
2
3
4
5
6
7
8
9
10
class Base {/***/};
class D : public Base {
public:
// 默认情况下,基类的默认构造函数初始化对象的基类部分
// 要想使用拷贝或移动构造函数,我们必须在构造函数初始值列表中显式的调用该构造函数
D(const D& d):Base(d) // 拷贝基类成员
/* D 的成员的初始值 */ {/***/}
D(D&& d):Base(std::move(d)) // 移动基类成员
/* D 的成员的初始值 */ {/***/}
}
定义派生类的赋值运算符
派生类的赋值操作符必须显式对基类部分进行赋值。如:
1
2
3
4
5
D &D::operator=(const D &rhs)
{
Base::operator=(rhs);
return *this;
}
继承的构造函数
通过 using 声明可以让派生类继承基类的构造函数。如:
1
2
3
4
5
class Bulk_quote : public Disc_quote {
public:
using Disc_quote::Disc_quote;
double net_price(std::size_t) const;
};
常规的using
声明只是让名字可见而已。当其运用到构造函数时,using
声明将导致编译器生成代码。编译器将生成与基类一一对应的构造函数,这些编译器生成的构造函数有如下形式:derived(params) : base(params) {}
,如果派生类有自己的成员,要么执行类内初始化,要么就是默认初始化的。
using
声明的构造函数不会随着using
所在的位置改变继承来的构造函数的访问级别。不管using
身处何处,基类中的private
构造函数依然是private
的;protected
和public
构造函数也是一样。
容器与继承
当使用容器存储来自继承层次的对象时,通常得使用间接的方式存储对象。原因,不能在容器中持有不同类型的元素。由于对象被赋值给基类对象时是裁剪(sliced down)的,容器与有继承关系的类型不能很好的混合使用。
在容器中放入指针或智指针。当使用容器来存储有继承关系的类型时,通常将容器定义为存储基类的指针或智能指针。而且,存储智能指针是一种更加推崇的方案。