面向对象编程
--继承情况下的类作用域
引言:
在继承情况下,派生类的作用域嵌套在基类作用域中:假设不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义。
正是这样的类作用域的层次嵌套使我们能够直接訪问基类的成员,就好像这些成员是派生类成员一样:
Bulk_item bulk; cout << bulk.book() << endl;
名字book的使用将这样确定[先派生->后基类]:
1)bulk是Bulk_item类对象,在Bulk_item类中查找,找不到名字book。
2)由于从Item_base派生Bulk_item,所以接着在Item_base类中查找,找到名字book,则引用成功的确定了。
一、名字查找在编译时发生
对象、引用或指针的静态类型决定了对象能够完毕的行为。甚至当静态类型和动态类型可能不同的时候,就像使用基类类型的引用或指针时可能会发生的,静态类型仍然决定着能够使用什么成员:
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... };
仅仅能通过Disc_item类型或Disc_item派生类型的对象、指针或引用訪问discount_policy():
Bulk_item bulk; Bulk_item *bulkP = &bulk; Item_base *itemP = &bulk; bulkP -> discount_policy(); //OK itemP -> discount_policy(); //Error
通过itemP訪问是错误的,由于基类类型的指针(引用或对象)仅仅能訪问对象的基类部分,而不能訪问派生类部分,而在基类中又未定义discount_policy()成员。
//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); } } };
二、名字冲突与继承
与基类成员同名的派生类成员将屏蔽对基类成员的直接訪问:
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中的名字:
Derived d(43); cout << d.get_mem() << endl; //output 43
能够使用作用域操作符訪问被屏蔽的成员:
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成员。
【最佳实践】
设计派生类时,仅仅要可能,最好避免与基类成员的名字冲突!
//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既有数据成员又有成员函数!!! */
三、作用域与成员函数
在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽:
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中的声明。在确定以下一条语句时:
d.memfuc();
编译器查找名字memfuc,并在Derived类中找到。一旦找到了名字,编译器要就不再继续查找了。
【小心地雷】
局部作用域中声明的函数不会重载全局作用域中定义的函数,相同,派生类中定义的函数也不会重载基类中定义的成员。通过派生类对象调用函数时,实參必须与派生类中定义的版本号相匹配,仅仅有在派生类中根本未定义该函数时,才考虑基类函数。如:
struct Base { int memfuc(); }; struct Derived : Base { int memfuc(int); }; Derived d; d.memfuc(); //Error
假设将Derived中的intmemfuc(int)凝视掉,则:
d.memfuc(); //OK
重载函数
像其它随意函数一样,成员函数(不管虚还是非虚)也能够重载。派生类能够重定义所继承的0个或多个版本号。
[注意] 假设派生类重定义了重载成员,则通过派生类型仅仅能訪问派生类中重定义的那些成员!
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声明会将该函数的全部重载实例加到派生类的作用域。将全部名字增加作用域之后,派生类仅仅须要重定义本类型确实必须定义的那些函数,对其它版本号能够使用继承的定义。
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) }
四、虚函数与作用域
虚函数:假设基类成员与派生类成员接受的实參不同,就没有办法通过基类类型的引用或指针调用派生类函数:
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); };
通过基类调用被屏蔽的虚函数
通过基类类型的引用或指针调用函数时,编译器将在基类中查找该函数而忽略派生类:
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)假定函数调用合法,编译器就生成代码。假设函数是虚函数且通过引用或指针调用,则编译器生成代码以确定依据对象的动态类型执行哪个函数版本号,否则,编译器生成代码直接调用函数。
//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