C++面向对象程序设计[2]
虚函数
对虚函数的调用在运行时才被解析
动态绑定只会在通过指针或者引用调用虚函数时才会发生。
我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些函数的“多种形式”而无须在意他们的差异。指针和引用的动态类型和静态类型的不同正是C++语言支持多态的根本所在。
当我们使用基类的指针或者引用调用基类中定义的一个函数时,我们并不知道该函数真正作用的对象时什么类型,因为它可能是一个基类的对象也可能是一个派生类的对象。如果该函数是一个虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或者指针所绑定的对象的真实的类型。
当且仅当对通过指针或者引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下,对象的动态类型才有可能与静态类型不一致。
而对于非虚函数的调用在编译时进行绑定。类似的,通过对象调用的函数(虚函数或非虚函数)都是在编译时绑定。因为对象的类型是固定不变的,只有基类对象的指针或者引用才可能发生动态类型和静态类型不一样的情况。
class Quote {
public:
Quote() = default;
Quote(const std::string &book, double sales_price)
: bookNo(book), price(sales_price) {}
std::string isbn() const { return bookNo; }
void test() { std::cout << isbn() << " Quote\n"; }
virtual double net_price(std::size_t n) const { return n * price; }
virtual ~Quote() = default;
private:
std::string bookNo;
protected:
double price = 0.0;
};
class Bulk_quote : public Quote {
public:
Bulk_quote() = default;
Bulk_quote(const std::string &, double, std::size_t, double);
double net_price(std::size_t n) const override;
void test() const { std::cout << isbn() << " Bulk_quote\n"; };
private:
std::size_t min_qty = 0;
double discount = 0.0;
};
Bulk_quote::Bulk_quote(const std::string &book, double p, std::size_t qty,
double disc)
: Quote(book, p), min_qty(qty), discount(disc) {}
double Bulk_quote::net_price(std::size_t n) const {
double res;
if (n >= min_qty)
res = price * (1 - discount) * n;
else
res = n * price;
return res;
}
double print_total(std::ostream &os, const Quote &item, std::size_t n) {
double ret = item.net_price(n);
os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: " << ret
<< std::endl;
return ret;
}
int main() {
Quote *item1 = new Quote("C++ Primer", 45);
Bulk_quote item2 = Bulk_quote("C++ Primer PLUS", 40, 100, 0.3);
item1->test();
item2.test();
item1 = &item2;
item1->test();
}
注意上述代码中的test
函数。该函数是非虚函数,并且在派生类和基类中有有定义。
class Quote {
public:
...
void test() { std::cout << bookNo << " Quote\n"; }
...
};
class Bulk_quote : public Quote {
public:
...
void test() const { std::cout << isbn() << " Bulk_quote\n"; };
...
在主函数中,item1
是一个指向基类的指针,其静态类型是Quote
,动态类型在倒数第二行被转为Bulk_quote
。
int main() {
Quote *item1 = new Quote("C++ Primer", 45);
Bulk_quote item2 = Bulk_quote("C++ Primer PLUS", 40, 100, 0.3);
item1->test();
item2.test();
item1 = &item2;
item1->test();
}
这里test
函数虽然是被基类指针item1
调用的,但是它不是虚函数,所以它的实际类型在编译时即被绑定,两个item1->test()
都将调用基类中的test
函数,而由于item1
的动态类型(指向对象的类型)发生了改变,所以test
函数中打印的isbn()
字符串将由实际对象的值决定。即item1
的类型为动态绑定,所有test
函数的类型则是编译时绑定。
C++ Primer Quote
C++ Primer PLUS Bulk_quote
C++ Primer PLUS Quote
fianl 和 override 说明符
派生类如果定义一个与基类中虚函数名字相同但是形参列表不同的成员函数,任然是合法的行为。编译器将会认为新定义的函数与基类中原有的函数时相互独立的。
使用override
关键字来说明派生类的中的某个函数,但是该函数并没有覆盖基类已经有的虚函数,此时编译器将会报错。
当函数被override
修饰时,编译器会认为该函数是对基类中虚函数的覆盖,此时要求该派生类中的函数和基类中的虚函数类型严格一致。
如果某个函数被关键字fianl
修饰,则之后任何尝试覆盖该函数的操作都会引发错误。
虚函数与默认实参
虚函数也可以有默认实参,如果某次对虚函数的调用使用了默认实参,那么该实参的值由本地调用的静态类型决定。
如果我们通过基类的引用或者指针调用虚函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本。
如果虚函数使用了默认实参,则基类和派生类中定义的默认实参最好一致
还是之前的test
函数,将其修改为虚函数进行试验:
class Quote {
public:
...
virtual void test(std::string s = "Quote\n") const{ std::cout << s; }
...
};
class Bulk_quote : public Quote {
public:
...
void test(std::string s = "Bulk_quote") const override{ std::cout << s; };
...
int main() {
Quote *item1 = new Quote("C++ Primer", 45);
Bulk_quote item2 = Bulk_quote("C++ Primer PLUS", 40, 100, 0.3);
item1->test();
item1 = &item2;
item1->test();
}
这里item1
是指向基类的指针,通过基类指针调用虚函数test
,则test
的实际版本将会由item1
的实际对象类型决定(动态绑定)。但是这里基类虚函数test
的默认实参为"Quote\n"
,所以两次调用test
都会传递给它基类的默认实参。这与我们的"预期"不太一样。所以如果虚函数使用了默认实参,则基类和派生类中定义的默认实参最好一致。
Quote
Quote
回避虚函数的机制
使用作用域运算符可以强迫虚函数执行某个特定版本,而不进行动态绑定。
double undiscounted = baseP->Quote::net_price(42);
抽象基类
不希望用户实例化某个类,该类只表示一个通用的概念。
含有纯虚函数的类是抽象基类,抽象基类负责定义接口。
访问控制与继承
受保护的成员
受保护的成员对于类的用户来说不可访问,对于派生类的成员和友元来说可以访问。
派生类的成员或友元只能通过派生类对象来访问基类的受保护成员,而不能通过基类对象去访问基类的私有成员(使用基类对象相当于基类的用户)。派生类对于一个基类中的受保护的成员没有任何访问特权。
class Base{
protected:
int prot_mem;
};
class Sneaky : public Base{
//正确,clobber可以访问Sneaky对象的成员
friend void clobber(Sneaky& s) {s.j = s.prot_mem = 0;}
//错误,clobber不能访问Base对象的成员
friend void clobber(Base& b) {b.prot_mem = 0;}
};
公有、私有和受保护继承
派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。
派生类向基类转换的可访问性
对于代码中的某个给定节点来说,如果基类的共有成员是可以访问的,那么就可以使用派生类到基类的转换。
友元与继承
友元关系不能继承。当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来那个类来说,其友元的基类或者派生类不具有特殊的访问能力。
改变个别成员的可访问性
改变派生类继承的某个名字的访问级别。使用using
声明。
默认的继承保护级别
class Base{...};
struct D1 : Base {...};//默认public继承
class D2 : Base {...};//默认private继承
struct修饰的类和class修饰的类的唯一的不同在于默认成员访问说明符及默认派生类访问说明符。struct默认都是公有,class默认都是私有。
继承类中的类作用域
如果一个名字在派生类中无法解析,那么编译器会继续在外层的基类作用域中寻找该名字的定义。
派生类的作用域嵌套在基类之中。
在编译时进行名字查找
一个对象、引用或指针的静态类型决定了该对象名字查找时的起点。这就导致指向基类的指针无法使用派生类中特有的方法,但是指向基类的指针可以调用派生类中继承自基类的虚函数,通过动态绑定使用派生类中对虚函数的实现。
名字冲突与继承
派生类可以重新定义在其直接基类或者间接基类中的名字,此时定义在内层的作用域(即派生类)的名字将覆盖定义在外层作用域(即基类)的名字。
struct Base{
Base() : mem(0) {}
protected:
int mem;
};
struct Derived : Base{
Detived(int i) : mem(i) {}
protocted:
int mem;
};
定义在派生类中的 mem 将会隐藏基类中的 mem
通过作用域运算符来使用隐藏的成员
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
假设我们使用p->mem()
或者obj.mem()
则依次执行以下4个步骤:
- 首先确定 p 的静态类型
- 在 p 的静态类型对应的类中寻找
mem()
。如果找不到则依次在直接基类中不断查找直到到达继承链的顶端。如果都没找到,那么编译器报错 - 一旦找到了
mem()
,就进行常规的类型检查 - 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:
a. 如果 mem
是虚函数,且我们是通过引用或者指针进行的调用,那么编译器产生的代码将会在运行时确定到底运行虚函数的哪个版本,依据是对象的动态类型。
b. 反之,则产生一个常规的函数调用。
名字查找优先于类型检查
和其他作用域一样,如果派生类(即内层作用域)中的成员与基类中(外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。哪怕基类成员和派生类成员的形参列表不一致,基类成员也仍然会被隐藏掉。
struct Base{
int memfun();
};
struct Drived : Base{
int memfun(int);
};
Drived d; Base b;
b.memfun(); // 调用 Base::memfun
d.memfun(10); // 调用 Drived::memfun
d.memfun(); // 错误:派生类中的memfun没有无参数版本
d.Base::memfun(); // 正确: 调用Base::memfun()
虚函数与作用域
如果派生类中定义了一个与继承自基类的虚函数名字相同,但是形参不同的成员,那么该成员与继承得来的虚函数以一种类似重载的效果(但实际上不是重载)共存。也就是说,在派生类作用域中将会看到两个同名但是参数列表不同的函数,一个是派生类自己定义的函数,另一个是从基类继承来的虚函数,前者静态绑定,后者动态绑定。
#include <iostream>
using namespace std;
class Base {
public:
virtual int fcn(); // 虚函数
};
class D1 : Base {
public:
int fcn(int); // 非虚函数
virtual void f2(); // 虚函数
};
class D2 : D1 {
public:
int fcn(int); // 非虚函数,隐藏掉 D1::fcn(int)
int fcn(); // 虚函数,覆盖掉 Base::fcn()
void f2(); // 虚函数,覆盖掉 D1::f2()
};
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()
D1 *d1p = &d1obj;
D2 *d2p = &d2obj;
bp2->f2(); // 名字查找以静态类型的作用域为起点
// 错误:基类无法调用派生类的成员
d1p->f2(); // 虚函数,动态绑定至 D1::f2()
d2p->f2() // 虚函数,动态绑定至 D2::f2()
Base *p1 = &d2obj;
D1 *p2 = &d2obj;
D2 *p2 = &d2obj;
p1->fcn(42); // 错误,Base 中没有接受int的fcn
p2->fcn(42); // 正确,静态绑定,调用 D1::fcn(int)
p3->fcn(42); // 正确,静态绑定,调用 D2::fcn(int)