C++回炉之_C++PrimerPlus_第十三章 类继承

类的成员初始化表

类的成员变量总是在构造函数执行前创建完毕 但有此成员变量只能在初始化时赋值 -- 如const型常量 和 引用 使用初始化表可以使指定构造函数中的参数或常量作为成员的初始值

1
Point::Point(int i, int j): x(i), y(j) {}
初始化表只能用于构造函数 必须使用初始化表来初始化const型常量和__引用__ 成员初始化的顺序与它们出现在类声明中的位置有关,与初始化表中的顺序无关 C++11允许类内初始化 - 但初始化表会覆盖类内初始化
1
2
3
4
5
6
class Point {
private:
int x = 10;
int y = 20;
static const int num = 0;
};

公有继承

Example

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
32
33
34
class Base {
private:
int m_x;
protected:
int m_y;
public:
Base(int i = 0, int j = 0): m_x(i), m_y(j) {}
virtual ~Base() { cout << "Base Class deleted\n"; }

void set_x(int i) { m_x = i; }
int get_x() const { return m_x; }
void set_y(int i) { m_y = i; }
int get_y() const { return m_y; }

virtual void show() {
cout << m_x << " " << m_y << " ";
}
};

class Point : public Base {
private:
int m_z;

public:
Point(int i = 0, int j = 0, int k = 0): Base(i, j), m_z(k) {}
Point(const Base& b, int k): Base(b), m_z(k) {}

virtual ~Point() { cout << "Point Class deleted\n"; }

virtual void show() {
Base::show();
cout << m_z;
}
};

公有基类的 priavateprotected 变量对外部不可见, public 变量对外部可见 公有派生类 - 基类的变量和方法会继承下来,但是访问权限变了 - 私有变量和私有方法仍为 private 的, 但无法直接访问 -- 只能通过 protectedpublic 方法访问 - 保护变量和保护方法仍为 protected 的 -- 外界不可访问 - 公有变量和公有方法仍为 public 的 - 尽量不要使用protected 的变量,但 protected 的成员函数很有用 - 它让派生类能够访问公众不能使用的内部函数 - 派生类需要添加自己的__构造函数__ - 创建派生类对象时,程序首先创建基类对象 - 要通过初始化表用__基类的构造函数__给继承过来的成员变量提供初始值 - 如果省略了基类的初始化表,程序会使用基类的默认构造函数 - 也应初始化派生类新增的数据成员 - 派生类对象过期时,首先调用派生类的析构,再调用基类的析构 - 派生类应根据需要添加自己的成员变量和方法 基类与派生类的关系 -- 指针或引用

1
2
3
Point p(1, 2);
Base* bp = &p;
Base& br = p;
  • 基类指针或引用可以在不进行显式类型转换的情况下指向或引用派生类对象
    • 但是基类指针或引用只能调用基类的方法,无法调用派生类的方法
    • 而派生类指针或引用是不能指向或引用基类对象的
  • 参数为基类引用或指针的函数,也可以将派生类对象作为参数
    • 这可以让基类对象使用派生类来初始化
    • 这也可以将派生类赋值给基类 -- 此时派生类的基类部分被复制给基类
  • 可以将派生类对象赋值给基类对象,但这只涉及到基类的部分,派生类中的派生部分将被忽略掉 - 其实质是调用基类的赋值运算符 -- 此函数参数为基类的引用 公有继承建立一种 is-a 关系 -- is-a-kind-of
    • 即派生类是基类的一种 -- 只是比基类更特殊了一下而已
    • 并非 is-like-a -- 即没有比喻关系
    • 派生类只能在基类的基础上添加属性,但是不能删除属性
    • 并非 is-implenmented-as-a -- 即没有实现关系
      • 如数组可以用来实现栈,但是不能从Array派生出Stack类,而是将数组对象封装在栈里
    • 并非use-a -- 即没有使用关系 -- 可以通过友元或类来实现类之间的通信(使用)

多态 -- 动态多态

定义 - 同一个方法在派生类和基类中的行为不同 - 即方法的行为应取决于调用该方法的对象 -- 随上下文而异 - 前面所学的重载和函数模板是在编译时间便确定的联编,称为静态多态 重写基类方法 - 基类的方法可以在派生类中重写 -- 使用class_name:: 来说明是基类还是派生类的方法 - 可在派生类中使用基类名作为限定符调用同名的基类函数

1
2
3
4
    Point::void show() {
Base::show();
cout << m_z;
}
  • 程序将根据对象类型来确定使用哪个版本 虚函数 -- 前加 virtual 关键字 -- 只在声明中加
    • 如果方法是通过引用或指针调用的,使用virtual关键字将成员函数声明为虚方法,程序将根据__引用或指针指向的对象__的类型来决定用哪个版本
    • 如果没有加virtual,则程序会根据__引用或指针__的类型来决定版本
    • 只要在基类中加virtual 即可,这样以后派生类及派生类的派生类都是虚函数
    • 可以创建指向基类的指针数组,那么此数组既可以指向基类,又可以指向派生类
1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
Base b(10, 20);
Point x(1, 2, 3);
Base* bs[3];
bs[0] = new Base(1, 2);
bs[1] = &x;
bs[2] = &b;

rep(i, 0, 3) bs[i]->show(), cout << endl;

return 0;
}

输出

1 2 1 2 3 10 20

  • 即使用一个数组来表示多个类型的对象 -- 此之谓多态性 虚析构函数
    • 如果析构函数不是虚的,则将只调用对应于引用或指针类型的析构函数
      • 那么如果引用或指针指向的是派生类对象
      • 那么派生类的析构函数将永远不会调用
    • 如果析构函数是虚函数
      • 则如果引用或指针指向的是派生类对象
      • 则将调用派生类的析构函数,然后再自动调用基类的析构函数

虚函数的实现原理

给每个对象添加一个隐藏成员 该隐藏成员保存了一个指向函数地址数组的指针 此地址数组称为__虚函数表__ 虚函数表中存储了类对象的虚函数的地址 对于基类来说,基类对象包含了一个指针,该指针指向基类中所有虚函数的地址表 对于派生类来说,派生类对象也包含了一个指针,该指针指向派生类中所有虚函数的地址表 - 如果派生类没有重新定义某虚函数,则此表保留此虚函数基类版本的地址 - 如果派生类重新定义了某虚函数,则此表将更新此虚函数的新地址 - 如果派生类增加了新的虚函数,则此表也增加该函数的地址 不管虚函数有多少个,都只需要在对象里添加一个指针成员 调用虚函数时 - 程序将查看存储在对象中的虚函数表头地址 - 然后转身相应的函数地址表 - 然后根据该虚函数在类中声明的位置找到其在表中的位置 - 然后跳到该地址指向的函数地址,执行函数 摘自C++PrimerPlus_522 使用虚函数的成本 - 每个对象都将增大 -- 增大一个指针的空间 -- 指向虚函数表的指针 - 对于每个类,编译器都要创建一个虚函数地址表 - 对于每个函数调用,都要执行额外的操作 -- 到表中查找地址 - 这也是默认使用静态联编的原因 -- 为了效率 及其他 - 构造函数不能是虚函数 -- 构造函数不继承基类的构造函数 -- 而是会调用基类的构造函数 - 基类的析构函数应该是虚函数 -- 即使它不执行任何操作 - 友元函数不能是虚函数 -- 友元不是类成员 -- 友元函数可以使用虚成员函数 - 重新定义继承的函数但参数列表不同,派生类将隐藏同名的基类方法 - 所以如果要重新定义继承的函数,应确保与原来的原型完全相同 - 但如果该函数返回类型是基类的引用或指针,则可改为派生类的 -- 返回类型协变 - 如果基类中的函数有多个重载,则继承过来的时候不能只重新定义一个版本的 -- 另外的会被隐藏

抽象基类(ABC)

abstract base class 抽象出一些类的共性 -- 构造一个抽象的类 使用纯虚函数提供未实现的函数 -- 在声明的结尾处加 =0 -

1
2
3
4
5
6
class Base {
private:
...;
public:
virtual double get_area() = 0;
};
- 包含纯虚函数的类只用作基类 -- 不能实例化 -- 但是能声明(但不初始化)指针 - 即ABC必须至少包含一个纯虚函数 - 如果在基类中声明了纯虚函数,而派生类中并没有对其定义,则该函数仍为纯虚函数发,派生类也为抽象类 - 基类中也可以定义(实现)纯虚函数,但在派生类中必需显示地调用(使用类名限定符) - 这样可以将不同子类中公共的事务放在父类中完成 - 只有声明而没有定义的纯虚函数派生类是无法调用的 - 如果要把基类的析构函数声明为纯虚函数(有时候这么做只是为了说明此类为抽象类),则必须定义这个纯虚析构函数的实现 -- 因为派生类析构时会自动调用它 纯虚函数作为一种“接口约定”, 在基于组件的编程模式中很常见 - 派生组件至少会实现基类组件的所有接口(纯虚函数)

继承与动态内存分配*

如果基类使用了动态内存分配 -- 即在构造中使用new分配空间 - 该基类需要声明其构造函数, 析构函数,复制构造,赋值运算符 如果此时子类中没有使用new分配的内存 - 则此子类并不需要定义显式的析构函数,复制构造,赋值运算符 - 此子类默认的复制构造会显式地调用基类的复制构造, 同时根据成员变量类型进行复制 - 此子类默认的赋值运算符会显式地调用基类的赋值运算符 - 如果此子类中含有其他类对象,如string类,则 - 默认的复制构造将使用string的复制构造来复制string类对象成员 - 默认的赋值运算符将使用string的赋值运算符来给string类对象成员赋值 - 默认的析构函数将自动调用string的析构函数 - 综上,只要子类中没有使用new动态分配的内存,则不需要自定义相关的函数 如果此时子类中有使用new分配的内存 - 必须为子类定义显式析构函数 - 此析构函数会自动调用基类的析构函数 - 故此析构函数的职责只是对子类执行清理工作 - 必须为子类定义复制构造函数 - 基类的复制构造函数的参数为基类的引用 -- 可以传递进来派生类对象 - 因此在派生类的复制构造函数里,将使用参数为派生类对象的基类复制构造函数 -

1
2
3
Point::Point(const Point& p) : Base(p) {
...;
}
- 此参数使用其基类部分来构造新对象的基类部分 - 而此参数的派生部分则刚好在这个复制构造函数里用来构造新对象的派生部分 - 必须为子类定义赋值运算符 - 显示调用基类的赋值运算符,以完成基类部分的赋值

1
2
3
4
5
6
7
8
9
10
String& String::operator=(const String& s) {
if(this == &s) return *this;
Base::operator=(s); // 注意此句 -- 必须显示调用基类的赋值运算符

delete[] str;
len = s.len;
str = new char[len+1];
strcpy(str, s.str);
return *this;
}
  • 不能隐式调用基类的赋值运算符 -- 编译器识别不出来是基类的 -- 造成无限递归
1
this* = s;    // 不能这样用 -- 会调用派生类的赋值运算符 --使其变为递归函数

继承与友元函数

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Base.h
class Base {
...;
friend ostream& operator<< (ostream& out, const Base& p) {
...;
} // 基类的友元函数
};

// Point.cpp
ostream& oeprator<< (ostream& out, const Point& p) {
out << (const Base&) p; // 输出基类部分
...; // 输出派生部分
} // 派生类的友元函数

在派生类的友元函数中, 只能访问派生类的派生部分,而不能访问基类的私有成员 可使用基类的友元函数来负责对派生类的基类部分的访问 - 但是需要强制类型转换 -- 友元函数无法使用类名限定符