C++学习笔记----4.4 继承情况下的类作用域嵌套
引言:
在继承情况下,派生类的作用域嵌套在基类作用域中:如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义。
正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好像这些成员是派生类成员一样:
1
2
3
|
Bulk_item bulk; cout << bulk.book() << endl; |
名字book的使用将这样确定[先派生->后基类]:
1)bulk是Bulk_item类对象,在Bulk_item类中查找,找不到名字book。
2)因为从Item_base派生Bulk_item,所以接着在Item_base类中查找,找到名字book,则引用成功的确定了。
一、名字查找在编译时发生
对象、引用或指针的静态类型决定了对象能够完成的行为。甚至当静态类型和动态类型可能不同的时候,就像使用基类类型的引用或指针时可能会发生的,静态类型仍然决定着可以使用什么成员:
1
2
3
4
5
6
7
8
9
10
11
|
class Disc_item : public Item_base { public : std::pair<size_t, double > discount_policy() const { return std::make_pair(quantity,discount); } //other member as before... }; </size_t, double > |
只能通过Disc_item类型或Disc_item派生类型的对象、指针或引用访问discount_policy():
1
2
3
4
5
6
|
Bulk_item bulk; Bulk_item *bulkP = &bulk; Item_base *itemP = &bulk; bulkP -> discount_policy(); //OK itemP -> discount_policy(); //Error |
通过itemP访问是错误的,因为基类类型的指针(引用或对象)只能访问对象的基类部分,而不能访问派生类部分,而在基类中又没有定义discount_policy()成员。
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
//P498 习题15.21/22 class Item_base { public : Item_base( const std::string &book = "" , double sales_price = 0.0 ): isbn(book),price(sales_price) {} std::string book() const { return isbn; } //只是返回总价格,不进行打折 virtual double net_price(std::size_t n) const { return n * price; } virtual ~Item_base() {} private : std::string isbn; protected : double price; }; class Disc_item : public Item_base { public : Disc_item( const std::string &book = "" , double sales_price = 0.0 , std::size_t qty = 0 , double disc_rate = 0.0 ): Item_base(book,sales_price),quantity(qty),discount(disc_rate) {} //将函数设置为纯虚函数,以防止用户创建Disc_item对象 double net_price(size_t) const = 0 ; std::pair<size_t, double > discount_policy() const { return std::make_pair(quantity,discount); } protected : std::size_t quantity; //可实行折扣的数量 double discount; //折扣率 }; //批量购买折扣类 class Bulk_item : public Disc_item { public : Bulk_item( const std::string &book = "" , double sales_price = 0.0 , std::size_t qty = 0 , double disc_rate = 0.0 ): Disc_item(book,sales_price,qty,disc_rate) {} double net_price(std::size_t cnt) const { if (cnt >= quantity) { return cnt * ( 1 - discount) * price; } else { return cnt * price; } } }; //有限折扣类 class Lds_item : public Disc_item { public : Lds_item( const std::string &book = "" , double sales_price = 0.0 , std::size_t qty = 0 , double disc_rate = 0.0 ): Disc_item(book,sales_price,qty,disc_rate) {} double net_price(std::size_t cnt) const { if (cnt <= quantity) { return cnt * ( 1 - discount) * price; } else { return price * (cnt - quantity * discount); } } }; </size_t, double > |
二、名字冲突与继承
与基类成员同名的派生类成员将屏蔽对基类成员的直接访问:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class Base { public : Base():mem( 0 ){} protected : int mem; }; class Derived : public Base { public : Derived( int i):mem(i){} int get_mem() const { return mem; //Derived::mem } private : int mem; //将会屏蔽Base::mem }; |
get_mem中对mem的引用被确定为Derive中的名字:
1
2
|
Derived d( 43 ); cout << d.get_mem() << endl; //output 43 |
可以使用作用域操作符访问被屏蔽的成员:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Derived : public Base { public : int get_mem() const { return Base::mem; //Derived::mem } //As before }; //测试 Derived d( 43 ); cout << d.get_mem() << endl; //output 0 |
作用域操作符指示编译器在Base中查找mem成员。
【最佳实践】
设计派生类时,只要可能,最好避免与基类成员的名字冲突!
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
35
36
37
38
39
40
41
|
//P499 习题15.23 class Base { public : void foo( int ); protected : int bar; double foo_bar; }; class Derived : public Base { public : void foo(string); bool bar(Base *pb); void foobar(); protected : string bar; }; void Derived::foobar() { bar = "1024" ; } bool Derived::bar(Derived *pb) { return foo_bar == pb -> foo_bar; } int main() { Derived d; d.foo( "1024" ); } /*说明:可能是g++编译器对类型检查比较严格,这个程序在g++编译器上死活编译不过, *因为在Derivd中的string bar处编译器提示说:与前面的声明冲突了! *的确,在Derivd中,bar既有数据成员又有成员函数!!! */ |
三、作用域与成员函数
在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
struct Base { int memfuc(); }; struct Derived : Base { int memfuc( int ); }; int main() { Derived d; Base b; b.memfuc(); //调用Base::memfuc() d.memfuc( 10 ); //调用Derived::memfuc() d.memfuc(); //Error d.Base::memfuc(); //调用Base::memfuc() } |
Derived中的memfuc声明隐藏了Base中的声明。在确定下面一条语句时:
1
|
d.memfuc(); |
编译器查找名字memfuc,并在Derived类中找到。一旦找到了名字,编译器要就不再继续查找了。
【小心地雷】
局部作用域中声明的函数不会重载全局作用域中定义的函数,同样,派生类中定义的函数也不会重载基类中定义的成员。通过派生类对象调用函数时,实参必须与派生类中定义的版本相匹配,只有在派生类中根本没有定义该函数时,才考虑基类函数。如:
1
2
3
4
5
6
7
8
9
10
11
12
|
struct Base { int memfuc(); }; struct Derived : Base { int memfuc( int ); }; Derived d; d.memfuc(); //Error |
如果将Derived中的intmemfuc(int)注释掉,则:
1
|
d.memfuc(); //OK |
重载函数
像其他任意函数一样,成员函数(无论虚还是非虚)也可以重载。派生类可以重定义所继承的0个或多个版本。
[注意] 如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员!
1
2
3
4
5
6
7
8
9
10
11
12
13
|
struct Derived : Base { int memfuc(); int memfuc( int ); double memfuc( double ); }; int main() { Derived d; d.memfuc(); //Derived::memfuc() d.memfuc( 10 ); //Derived::memfuc(int) } |
如果派生类想通过自身类型使用所有的重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义。
有时类需要仅仅重定义一个重载版本,并且想要继承其他版本的含义,在这种情况下,派生类不用重定义所继承的每一个基类版本,它可以为重载成员提供using声明。一个using声明只能指定一个名字,不能指定形参表,因此:using声明会将该函数的所有重载实例加到派生类的作用域。将所有名字加入作用域之后,派生类只需要重定义本类型确实必须定义的那些函数,对其他版本可以使用继承的定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
struct Base { int memfuc(); int memfuc( int ); int memfuc( double ); }; struct Derived : Base { using Base::memfuc; int memfuc(); //重定义 }; int main() { Derived d; d.memfuc(); //Derived::memfuc() d.memfuc( 10 ); //Base::memfuc(int) } |
四、虚函数与作用域
虚函数:如果基类成员与派生类成员接受的实参不同,就没有办法通过基类类型的引用或指针调用派生类函数:
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
|
class Base { public : virtual int fcn(); }; class D1 : public Base { public : //该fcn屏蔽了Base类中的虚函数fun int fcn( int ); /**此时有两个名为 fcn 的函数: *类从 Base 继承的一个名为 fcn 的虚函数 *类定义的名为 fcn 的非虚成员函数,该函数接受一个 int 形参 */ }; class D2 : public D1 { public : /**重定义了它继承的两个函数: *1.重定义了 Base 中定义的 fcn 的原始版本 *2.重定义了 D1 中定义的非虚版本。 */ int fcn(); int fcn( int ); }; |
通过基类调用被屏蔽的虚函数
通过基类类型的引用或指针调用函数时,编译器将在基类中查找该函数而忽略派生类:
1
2
3
4
5
6
7
8
9
|
Base bobj; D1 d1obj; D2 d2obj; Base *bp1 = &bobj,*bp2 = &d1obj,*bp3 = &d2obj; bp1 -> fcn(); //调用Base::fcn() bp2 -> fcn(); //调用Base::fcn() bp3 -> fcn(); //调用D2::fcn() |
【关键概念:名字查找与继承】
理解 C++中继承层次的关键在于理解如何确定函数调用。确定函数调用遵循以下四个步骤:
1)首先确定进行函数调用的对象、引用或指针的静态类型
2)在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
3)一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
4)假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
1
2
3
4
5
6
7
8
9
10
11
|
//P502 习题15.24 Bulk_item bulk; Item_base item(bulk); Item_base *p = &bulk; /**由于net_price为虚函数 *对虚函数而言,只能通过指针或引用进行动态绑定 *而通过对象调用虚函数,所调用到的总是该对象所属类型中定义的函数 */ p -> net_price( 10 ); //调用Bulk_item版本的net_price item.net_price( 10 ); //调用Item_base版本的net_price
|
C++学习笔记----4.4 继承情况下的类作用域嵌套的更多相关文章
- C++ Primer 学习笔记_69_面向对象编程 --继承情况下的类作用域
面向对象编程 --继承情况下的类作用域 引言: 在继承情况下,派生类的作用域嵌套在基类作用域中:假设不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义. 正是这样的类作用域的层次嵌套使 ...
- 学习笔记——python(继承)
学习笔记(Python继承) 有几种叫法(父类-子类.基类-派生类)先拿代码演示一下: class father: def work(self): print("work>>&g ...
- C++学习笔记44:继承与派生
类的组合,类的继承 类的组合(汽车类,轮子类,此时可以把轮子类组合到汽车类:) 类的继承(交通工具类,汽车类,此时汽车类可以派生自交通工具类:) 组合:常用描述has a.. 继承:常用描述is a ...
- JavaSE学习笔记(5)---内部类和String类
JavaSE学习笔记(5)---内部类和String类 一.内部类基础 转自菜鸟教程 在 Java 中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类.广泛意义上的内部类一般来 ...
- Typescript 学习笔记四:回忆ES5 中的类
中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...
- es6学习笔记-class之继承
继承 上一篇学习了class的概念,在es5时,对象的继承有好几种,原型链继承,借用构造函数继承,组合继承,原型式继承,寄生式继承以及寄生组合式继承,都是按照函数的形式去集成的,现在class也有它的 ...
- (C/C++学习笔记) 十八. 继承和多态
十八. 继承和多态 ● 继承的概念 继承(inheritance): 以旧类为基础创建新类, 新类包含了旧类的数据成员和成员函数(除了构造函数和析构函数), 并且可以派生类中定义新成员. 形式: cl ...
- java学习笔记6--类的继承、Object类
接着前面的学习: java学习笔记5--类的方法 java学习笔记4--类与对象的基本概念(2) java学习笔记3--类与对象的基本概念(1) java学习笔记2--数据类型.数组 java学习笔记 ...
- C++对象在继承情况下的内存布局
1,C++ 中继承是非常重要的一个特性,本节课研究在继承的情形下,C++ 的对象模 型又有什么不同: 2,继承对象模型(最简单的情况下): 1,在 C++ 编译器的内部类可以理解为结构体: 2,子类是 ...
随机推荐
- python编码知识初始_ASCII码,Unicode,Utf-8,GBK
谍战片,电报,摩斯密码,相应规则(暗号),编码解码: 电脑底层是高低电平来传输信息(OSI七层模型,最底层):文件存储的本质,也是二进制,01010101 美国:ASCII码(8位表示一个字节 000 ...
- NYOJ--311(完全背包)
题目:http://acm.nyist.net/JudgeOnline/problem.php?pid=311 分析:这里就是一个完全背包,需要注意的就是初始化和最后的输出情况 dp[j ...
- Teigha的BlockTableRecord获取方法
Teigha的db(即database)可以有很多BlockTableRecord,可以用 OdDbBlockTablePtr blkTbl = db->getBlockTableId().op ...
- winDbg + VMware + window 双机联调环境搭建
这里简单的介绍一下内核开发双机联调的搭建环境,尽管网上有很多类似的文章,但看了很多总是不太舒服,觉得不太明白,所以自己实践一下总结一篇.下面就拿我的环境简单介绍,希望别人可以看懂. 准备工具:装虚拟机 ...
- Windows API GetShortPathName GetLongPathName
函数原型: The GetShortPathName function retrieves the short path form of a specified input path. DWORD G ...
- UITableViewHeaderFooterView can't change custom background when loading from nib
down voteforite I've created a custom UITableViewHeaderFooterView and successfully load from nib int ...
- python中的*args与**kwargs的含义与作用
在定义函数的时候参数通常会使用 *args与**kwgs,形参与实参的区别不再赘述,我们来解释一下这两个的作用. *args是非关键字参数,用于元组,**kwargs是关键字参数 (字典)例如下面的代 ...
- 初探.NET CORE WEB API(RESTful风格)
前面有4篇系列博客 (一)Asp.net web api中的坑-[找不到与请求 URI匹配的 HTTP 资源] (二)Asp.net web api中的坑-[http get请求中的参数] (三)As ...
- python 中文乱码问
在本文中,以'哈'来解释作示例解释所有的问题,“哈”的各种编码如下: 1. UNICODE (UTF8-16),C854: 2. UTF-8,E59388: 3. GBK,B9FE. 一.python ...
- 探索云网络技术前沿,Sigcomm 2019 阿里云参会分享
Sigcomm 2019简介 一年一度的网络顶级学术峰会Sigcomm于8月20日至22日在北京举行.作为ACM Special Interest Group on Data Communicatio ...