六一的部落格


关关难过关关过,前路漫漫亦灿灿。



派生类作用域嵌套在基类作用域之内: 派生类可以像使用自己的成员一样使用基类成员

存在继承关系时, 如果一个名字在派生类作用域内无法正确解析, 编译器将继续在外层的基类作用域中寻找该名字的定义


不能通过基类的引用/指针访问派生类非基类成员

 1class Disc_quote : public Quote
 2{
 3public:
 4    pair<size_t, double> discount_policy() const { return {quantity, discount}; }
 5};
 6
 7Bulk_quote bulk;
 8Bulk_quote *bulkP = &bulk;
 9Quote *itemP = &bulk;
10bulkP->discount_policy();             // 正确: 派生类指针
11itemP->discount_policy();             // 错误: 指针的静态类型决定可以使用的名字

定义在内层作用域的名字将隐藏定义在外层作用域的名字

派生类的成员将隐藏同名的基类成员

 1struct Base
 2{
 3    Base() : mem(0) { }
 4protected:
 5    int mem;
 6};
 7
 8struct Derived : Base
 9{
10    Derived(int i) : mem(i) { }
11
12    int get_mem() { return mem; }
13protected:
14    int mem;                  // 隐藏基类中的mem
15};
16
17Derived d(42);
18cout << d.get_mem() << endl;  // 42

可以通过作用域运算符来使用隐藏的成员: 作用域运算符将覆盖掉原有的查找规则, 指示编译器从给定类作用域开始查找名字

1struct Derived : Base
2{
3    // ...
4    int get_base_mem() { return Base::mem; }
5};
6
7Derived d(42);
8cout << d.get_base_mem() << endl;  // 0

除了覆盖继承而来的虚函数,派生类最好不要重用其他定义在基类中的名字


名字查找与继承

就通过指针/引用调用函数而言

  1. 确定指针/引用的静态类型, 于静态类型中查找名字

    如果查找不到, 依次在直接基类中不断查找直至到达继承链的顶端

    如果仍未找到, 编译器报错

  2. 找到名字后, 进行类型检查, 判断调用是否合法

    如果不合法, 编译器报错

  3. 如果调用合法, 编译器根据调用的是否为虚函数产生不同的代码:

    • 如果是虚函数, 根据动态类型确定虚函数版本
    • 如果是非虚函数, 编译器产生一个常规函数调用

名字查找先于类型检查

 1struct Base
 2{
 3    int memfcn();
 4};
 5
 6struct Derived : Base
 7{
 8    int memfcn(int);    // 隐藏基类的memfcn, 而不是作为重载函数
 9};
10
11Derived d;
12Base b;
13b.memfcn();             // 正确
14d.memfcn(10);           // 正确: 派生类作用域中有名字memfcn
15d.memfcn();             // 错误: 名字查找先得到int memfcn(int), 未通过类型检查
16d.Base::memfcn();       // 正确: 通过作用域运算符指示开始查找的位置为外层的基类作用域

虚函数与作用域

建议覆写虚函数时, 同时给出override和virtual关键字 :

  • override用于供编译器判断是否为覆写虚函数, 避免隐藏基类名字
  • virtual用于提高代码可读性
 1class Base
 2{
 3public:
 4    virtual int fcn();
 5};
 6
 7class D1 : public Base
 8{
 9public:
10    int fcn(int);            // 隐藏基类fcn
11    virtual void f2();
12};
13
14class D2 : public D1
15{
16public:
17    int fcn(int);            // 隐藏D1::fcn
18    int fcn();               // 覆写虚函数Base::fcn
19    void f2();               // 覆写虚函数D1::f2
20};
21
22Base bobj;
23D1 d1obj;
24D2 d2obj;
25
26Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
27
28bp1->fcn();     // 调用Base::fcn, 由指针的静态类型Base决定名字查找的作用域
29bp2->fcn();     // 调用Base::fcn, 由指针的静态类型Base决定名字查找的作用域, 而D1中fcn不是虚函数
30bp3->fcn();     // 调用D2::fcn, 由指针的静态类型Base决定名字查找的作用域, 使用D2中的虚函数版本
31
32D1 *d1p = &d1obj;
33D2 *d2p = &d2obj;
34
35bp2->f2();      // 错误: 在Base作用域查找名字f2失败
36d1p->f2();      // 调用D1::f2, 虚函数动态绑定
37d2p->f2();      // 调用D1::f2, 虚函数动态绑定
38
39Base *p1 = &d2obj;
40D1 *p2 = &d2obj;
41D2 *p3 = &d2obj;
42
43p1->fcn(42);    // 错误: 在Base作用域查找fcn得到int fcn(), 类型检查失败   
44p2->fcn(42);    // 正确: 调用D1::fcn, 静态绑定
45p3->fcn(42);    // 正确: 调用D2::fcn, 静态绑定

在派生类中重载基类成员函数

  1. 将基类成员函数的所有重载实例添加到派生类作用域中

    使用using声明语句
  2. 在派生类中定义重载函数

using声明会改变成员函数的访问属性 : 无法改变基类private成员的访问属性


继承中的类作用域


派生类作用域嵌套在基类作用域之内: 派生类可以像使用自己的成员一样使用基类成员

存在继承关系时, 如果一个名字在派生类作用域内无法正确解析, 编译器将继续在外层的基类作用域中寻找该名字的定义


不能通过基类的引用/指针访问派生类非基类成员

 1class Disc_quote : public Quote
 2{
 3public:
 4    pair<size_t, double> discount_policy() const { return {quantity, discount}; }
 5};
 6
 7Bulk_quote bulk;
 8Bulk_quote *bulkP = &bulk;
 9Quote *itemP = &bulk;
10bulkP->discount_policy();             // 正确: 派生类指针
11itemP->discount_policy();             // 错误: 指针的静态类型决定可以使用的名字

定义在内层作用域的名字将隐藏定义在外层作用域的名字

派生类的成员将隐藏同名的基类成员

 1struct Base
 2{
 3    Base() : mem(0) { }
 4protected:
 5    int mem;
 6};
 7
 8struct Derived : Base
 9{
10    Derived(int i) : mem(i) { }
11
12    int get_mem() { return mem; }
13protected:
14    int mem;                  // 隐藏基类中的mem
15};
16
17Derived d(42);
18cout << d.get_mem() << endl;  // 42

可以通过作用域运算符来使用隐藏的成员: 作用域运算符将覆盖掉原有的查找规则, 指示编译器从给定类作用域开始查找名字

1struct Derived : Base
2{
3    // ...
4    int get_base_mem() { return Base::mem; }
5};
6
7Derived d(42);
8cout << d.get_base_mem() << endl;  // 0

除了覆盖继承而来的虚函数,派生类最好不要重用其他定义在基类中的名字


名字查找与继承

就通过指针/引用调用函数而言

  1. 确定指针/引用的静态类型, 于静态类型中查找名字

    如果查找不到, 依次在直接基类中不断查找直至到达继承链的顶端

    如果仍未找到, 编译器报错

  2. 找到名字后, 进行类型检查, 判断调用是否合法

    如果不合法, 编译器报错

  3. 如果调用合法, 编译器根据调用的是否为虚函数产生不同的代码:

    • 如果是虚函数, 根据动态类型确定虚函数版本
    • 如果是非虚函数, 编译器产生一个常规函数调用

名字查找先于类型检查

 1struct Base
 2{
 3    int memfcn();
 4};
 5
 6struct Derived : Base
 7{
 8    int memfcn(int);    // 隐藏基类的memfcn, 而不是作为重载函数
 9};
10
11Derived d;
12Base b;
13b.memfcn();             // 正确
14d.memfcn(10);           // 正确: 派生类作用域中有名字memfcn
15d.memfcn();             // 错误: 名字查找先得到int memfcn(int), 未通过类型检查
16d.Base::memfcn();       // 正确: 通过作用域运算符指示开始查找的位置为外层的基类作用域

虚函数与作用域

建议覆写虚函数时, 同时给出override和virtual关键字 :

  • override用于供编译器判断是否为覆写虚函数, 避免隐藏基类名字
  • virtual用于提高代码可读性
 1class Base
 2{
 3public:
 4    virtual int fcn();
 5};
 6
 7class D1 : public Base
 8{
 9public:
10    int fcn(int);            // 隐藏基类fcn
11    virtual void f2();
12};
13
14class D2 : public D1
15{
16public:
17    int fcn(int);            // 隐藏D1::fcn
18    int fcn();               // 覆写虚函数Base::fcn
19    void f2();               // 覆写虚函数D1::f2
20};
21
22Base bobj;
23D1 d1obj;
24D2 d2obj;
25
26Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
27
28bp1->fcn();     // 调用Base::fcn, 由指针的静态类型Base决定名字查找的作用域
29bp2->fcn();     // 调用Base::fcn, 由指针的静态类型Base决定名字查找的作用域, 而D1中fcn不是虚函数
30bp3->fcn();     // 调用D2::fcn, 由指针的静态类型Base决定名字查找的作用域, 使用D2中的虚函数版本
31
32D1 *d1p = &d1obj;
33D2 *d2p = &d2obj;
34
35bp2->f2();      // 错误: 在Base作用域查找名字f2失败
36d1p->f2();      // 调用D1::f2, 虚函数动态绑定
37d2p->f2();      // 调用D1::f2, 虚函数动态绑定
38
39Base *p1 = &d2obj;
40D1 *p2 = &d2obj;
41D2 *p3 = &d2obj;
42
43p1->fcn(42);    // 错误: 在Base作用域查找fcn得到int fcn(), 类型检查失败   
44p2->fcn(42);    // 正确: 调用D1::fcn, 静态绑定
45p3->fcn(42);    // 正确: 调用D2::fcn, 静态绑定

在派生类中重载基类成员函数

  1. 将基类成员函数的所有重载实例添加到派生类作用域中

    使用using声明语句
  2. 在派生类中定义重载函数

using声明会改变成员函数的访问属性 : 无法改变基类private成员的访问属性