引言:
在继承情况下,派生类的作用域嵌套在基类作用域中:如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义。
正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好像这些成员是派生类成员一样:
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
|