面向对象编程
--定义基类和派生类[续]
四、virtual与其他成员函数
C++中的函数调用默认不使用动态绑定。要触发动态绑定,必须满足两个条件:
1)只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定。
2)必须通过基类类型的引用或指针进行函数调用。
1、从派生类到基类的转换
因为每个派生类对象都包含基类部分,所以可以将基类类型的引用绑定到派生类对象的基类部分可以用指向基类的指针指向派生类对象:
void print_total(const Item_base &item,size_t n);
Item_base item;
print_total(item,10);
Item_base *p = &item;
Bulk_item bulk;
print_total(bulk,10);
p = &bulk;
无论实际对象具有哪种类型,编译器都将它当做基类类型对象。将派生类对象当做基类对象是安全的,因为每个派生类对象都拥有基类子对象。而且,派生类继承基类的操作,即:任何在基类对象上执行的操作也可以通过派生类对象使用。
【释疑】
基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。
2、可以在运行时确定virtual函数的调用
当通过指针或引用调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型向对应的函数:
void print_total(const Item_base &item,size_t n)
{
cout << "ISBN: " << item.book()
<< "\t number sold: " << n << "\ttotal price: "
<< item.net_price(n) << endl;
}
因为 item形参是一个引用且net_price是虚函数,item.net_price(n)所调用的net_price版本取决于在运行时绑定到item形参的实参类型:
Item_base base;
Bulk_item derived;
print_total(base,10); //Item_base::net_price
print_total(derived,10); //Bulk_item::net_price
【关键概念:C++中的多态性】
引用和指针的静态类型与动态类型可以不同,这是C++用以支持多态的基石!
通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生类型的。
如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。
【理解:】
只有通过引用或指针调用,虚函数才在运行时确定。
3、在编译时确定非virtual调用
非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。尽管item的类型是 constItem_base 的引用,但是,无论在运行时item引用的实际对象是什么类型,调用该对象的非虚函数都将会调用Item_base中定义的版 本。
4、覆盖虚函数机制
如果希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,可以使用作用域操作符:
Item_base *baseP = &derived;
//显式调用Item_base中的版本,重载时确定
double d = baseP -> Item_base::net_price(42);
【最佳实践】
只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。
覆盖虚函数机制常用在:派生类虚函数调用基类中的版本,在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作:
【小心地雷】
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了作用域操作符,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归!
5、虚函数与默认实参
像其他任何函数一样,虚函数也可以有默认实参。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。
在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的。
//P482 习题15.8
struct base
{
base(string name = ""):baseName(name) {}
string name()
{
return baseName;
}
virtual void print(ostream &os)
{
os << baseName;
}
private:
string baseName;
};
struct derived : public base
{
derived(string name = "",int intMem = 0):base(name),mem(intMem) {}
void print(ostream &os)
{
base::print(os); //原来此处形成了无穷递归!
os << "" << mem;
}
private:
int mem;
};
//习题15.9 理解下面这段程序
int main()
{
base ba("xiaofang");
base *p = &ba;
p -> print(cout);
cout << endl;
derived de("xiaofang");
p = &de;
p -> print(cout); //调用派生类print函数
}
五、公用、私有和受保护的继承
对类所继承的成员的访问由基类中的成员访问级别和派生类列表中使用的访问标号共同控制。每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对继承的成员的访问!
基类本身指定对自身成员的最小访问控制。基类中的private,只有基类和基类的友元可以访问该成员。派生类也不能访问其基类的private成员,当然也不能使自己的用户访问!
如果基类成员为public或protected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别:
1)如果是公用继承public:基类成员保持自己的访问级别:基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。
2)如果是受保护继承protected:基类的public和protected成员在派生类中为protected成员。
3)如果是私有继承private:基类的所有成员在派生类中为private成员。
class Base
{
public:
void baseMem();
protected:
int i;
};
class Public_derived : public Base
{
int use_base()
{
return i; //OK
}
};
class Private_derived : private Base
{
int use_base()
{
return i; //OK
}
};
上例说明:无论派生列表中是什么访问标号,所有继承Base的类对Base的成员具有相同的访问权限;派生类访问标号将控制派生类的用户对从Base继承而来的成员的访问:
Base b;
Public_derived d1;
Private_derived d2;
b.baseMem();
d1.baseMem(); //OK
d2.baseMem(); //Error
派生类访问标号还控制来自非直接派生类的访问:
class Derived_from_Private : public Privat