一、面向对象概述
1. 面向对象的三个基本特性
封装、继承和多态。
2. 封装
指把隐藏对象的实现细节,仅对外提供接口,从而达到接口与实现分离的效果。封装的好处:一是提高数据的安全性,用户只能使用对象提供的接口,而不能随意修改对象的数据。试想如果用户能够获取权限访问对象的所有实现细节并进行修改,那对象的安全性将无法保证。这和用外壳把电路板封装起来,以免用户随便拆卸电子器件的道理是类似的。二是方便使用。用户只需知道接口而不需了解内部细节便可使用。
3. 继承
指新对象可以直接使用现有对象的功能,并且可以在此基础上进行拓展,从而避免“重复造轮子”。
4. 多态
指某个接口的使用者可能是原有对象也可能是新对象,这个要在实际运行时才能确定,而在编译时无法确定。比如:定义一个函数void func(const Base&),其中Base是基类对象,假设类Derived继承类Base,则函数func的实际输入参数可能是Base对象也可能是Derived对象,视实际运行情况而定。
5. 代码重用和接口重用
封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是——代码重用。而多态则是为了实现另一个目的——接口重用!
二、基类与派生类
1. 派生类的声明与定义
(1) 派生类的声明与普通类的声明一样,“class” + 类名即可。注意不要在声明派生类时列出其继承的基类,因为声明的目的是让编译器知道某个名字存在以及它代表何种数据类型(类、函数或变量等)即可,不需要知道细节。
(2) 在声明类时可以在类名后面加上“final”,表明此类不可被继承。比如:class NoDerived final : Base {/* */};
(3) 定义派生类时,应该在紧跟类名的“:”后面、“{”前面写出继承列表,声明所继承的每一个基类的名字以及继承方式(public, protected或private)。比如:
1 class Derived : public Base1, private Base2 { 2 /* */ 3 };
2. 派生类构造函数
(1) 每个类负责自己定义的成员的初始化,因此派生类应该使用基类构造函数来初始化派生类中的基类成员。虽然派生类构造函数可以对public或protected的基类成员直接赋值,但不应该这样做,我们应该通过类提供的接口来访问类对象。
(2) 派生类构造函数初始化数据成员的顺序:首先初始化基类成员,然后初始化派生类自身定义的成员。另外,在初始化一个类的各成员时,按照各成员在该类中被声明的顺序进行初始化。
3. 派生类对基类的访问权限
(1) 派生类继承基类的方式(public, protected或者private)不影响派生类对基类的访问权限,派生类的成员或友元函数总是可以访问基类的public和protected成员,而不能访问基类的private成员。派生类继承基类的方式影响的是继承类的用户对基类的访问权限,其影响方式可总结为3条公式:
1 public * T = T; // T may be public, private or protected. 2 private * T = private; 3 protected * protected = protected.
(2) 派生类的成员或友元函数只能通过派生类对象访问基类的protected成员,而不能通过基类对象访问基类的protected成员。这样可以防止派生类或友元函数绕过保护机制去修改基类的protected成员。比如:
1 class Base { 2 protected: 3 int prot_mem; 4 }; 5 6 class Derived { 7 friend void func(Derived&); // can access Derived::prot_mem 8 friend void func(Base&); // can't access Base::prot_mem 9 };
(3) “派-基”转换的使用权限:
只有当派生类以public方式继承基类时,派生类的用户才能使用“派-基”转换。
派生类的成员或友元函数总是可以使用“派-基”转换。
只有当派生类以public或protected方式继承基类时,派生类的派生类的成员或友元函数才能使用“派-基”转换。
(4) 友元函数的“友谊”不可传递,亦不可继承。
(5) 可以在派生类中使用using语句改变其基类成员的访问权限。比如:
1 class Base { 2 public: 3 std::size_t size() const { return n;} 4 protected: 5 std::size_t n; 6 }; 7 8 class Derived: private Base { // private inheritance. 9 public: 10 using Base::size; // now users of Derived can use size(). 11 protected: 12 using Base::n; // now class derived from Derived can use n. 13 };
(6) 派生类对基类的继承方式默认为private,这和类的成员的访问权限默认为private类似。但即使派生类要以private方式继承基类,也应该加上private关键字,而非依赖缺省值。这样可以清晰表明自己的意图。
(7) struct和class的区别只是两者的成员的默认访问方式不同,除此之外没其他区别。
4. "派-基"转换(派生类到基类的转换)
(1) 派生类对象由两部分组成:一部分是派生类自身定义的成员,另一部分是派生类所继承的各个基类的成员。
(2) 一个变量或表达式的类型可分为静态类型和动态类型两种。静态类型是变量声明的类型或表达式计算结果所代表的类型,在编译时便可确定;动态类型是变量或表达式实际使用时的类型,在运行时才能确定。对于非指针且非引用的变量或表达式,其静态类型与动态类型相同;对于基类指针或基类引用,其动态类型与静态类型可能不同。
(3) 由于派生类包含基类成员,我们将一个派生类对象看作是基类对象来使用,特别是,可以把基类指针或基类引用绑定到派生类对象上,这叫做“派-基”转换(derived-to-base conversion)。
(4) 当我们直接使用派生类对象来初始化或赋值给基类对象时,只有派生类的基类成员被复制或赋值,派生类的自身成员会被忽略。
(5) 基类对象不能自动转换成派生类成员,因为编译器无法确定这样的转换是否安全。即使一个基类指针或基类引用已经与一个派生类对象绑定,仍然不能将其用于“基-派转换”:
1 Derived derived; 2 Base *basePtr = &derived; // ok: dynamic type is Derived 3 Derived *derivedPtr = basePtr; // error: can't convert base to derived
(6) 如果我们明确某个”基-派“转换是安全的,可以使用static_cast来让编译器允许这样的转换。
三、虚函数
1. 虚函数的定义
(1) 除构造函数外的所有非satic函数均可定义为虚函数。
(2) 如果基类定义了一个static成员,则无论有多少个继承此基类的派生类,都只存在于一个这样的static成员。
(3) 基类必须定义一个虚析构函数,即使它不会被使用。
(4) 所有的虚函数都必须被定义,即使它不会被使用。因为编译器无法知道某个虚函数是否被用到,为确保安全,要求定义所有的虚函数。
2. 虚函数的覆盖
(1) 派生类可以只对基类的部分而非全部虚函数进行覆盖(override)。
(2) 派生类可以使用“override”显式覆盖基类的虚函数,“override”应写在虚函数的参数列表后面。如果虚函数是const或reference函数,则把override写在const或&的后面。显式覆盖的一个好处是可以避免写错虚函数的形参,因为编译器一旦发现派生类中显式覆盖的虚函数的形参与基类的虚函数不一致时会报错。
(3) 继承类中要进行override的虚函数的形参必须与基类的同名虚函数一致,若不一致则不会覆盖,而会重载。除了一个例外,继承类中要进行override的虚函数的返回类型也必须与基类的一致。这个例外是如果待覆盖的虚函数的返回类型是基类对象的指针或引用时,继承类的虚函数可以返回继承类对象的指针或引用。
(4) 当继承类要覆盖基类的虚函数时,可以但不必在函数名前面重复virtual关键字。一旦某函数被声明为virtual,则在所有的继承类中它仍然为虚函数。(一日为虚,终身为虚。)
(5) 如果虚函数含有缺省参数,则其缺省值永远是基类中该虚函数定义的缺省值。因此,如果派生类中的虚函数含有缺省参数,则其缺省值应该和基类的该虚函数的缺省值相同。
3. 动态绑定
(1) 当使用基类指针或基类引用来调用某个虚函数时,会导致动态绑定;当使用基类对象来调用某个虚函数时,不会发生动态绑定。比如:
1 base = derived; // copies the Base part of derived to base 2 base.virtual_func(); // calls Base::virtual_func()
(2) 有时我们需要禁止动态绑定,最常见的一个例子是派生类的虚函数希望调用基类的同名虚函数。这时,可以使用::操作符禁止动态绑定。比如:
int data = baseP->Base::func(); // calls the version from the base class regardless of the dynamic type of baseP
四、抽象类与纯虚函数
1. 纯虚函数的定义
在某些场合,基类本身生成对象是不合情理的。例如动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。这时便引入纯虚函数的概念。当在基类中无法给出某个虚函数的具体实现时,我们可以将其声明为纯虚函数,而不进行定义,其定义留给派生类来实现。
2. 纯虚函数的声明
声明纯虚函数的方法是在函数名后面写上“= 0”,比如:virtual int pure_virtual_func() = 0;
3. 抽象类的定义
含有纯虚函数的类叫抽象类。因此,如果抽象类的派生类仍然含有纯虚函数,则此派生类也是抽象类。抽象类不能被实例化。
五、继承关系下的类作用域
1. 作用域的嵌套
派生类的作用域嵌套在基类的作用域里面。这意味着编译器在查找派生类中出现的所有名字时,总是先在派生类里查找,找不到的话再到基类中找。
2. 名字查找发生在编译时刻
因此,一个对象的成员能否被编译器“看见”(查找),取决于这个对象的静态类型(亦即此对象被声明时的类型),而非动态类型。因此,尽管基类指针或基类引用可以绑定到派生类对象,它们还是不能访问只在派生类定义的函数。比如:
1 class Base { 2 public: 3 virtual void func(); 4 // other members 5 }; 6 7 class Derived : public Base { 8 public: 9 void Derived_func(); 10 // other members 11 }; 12 13 Derived derived; 14 Base *pBase = &derived; // static and dynamic types differ 15 pBase->Derived_func(); // error: pBase has type Base*
3. 编译器查找名字的步骤
假如给定函数调用语句p->mem()或obj.mem(),则编译器查找mem的步骤如下:
(1) 确定p或obj的静态类型。
(2) 在其静态类型对应的类中查找mem,如果找不到,再到其直接基类中继续找,如此沿着继承链一直找,直到找到为止。
(3) 找到mem便进行类型检查,确定此调用是否合法。
(4) 如果此调用合法,编译器将生成代码。
4. 派生类对基类的覆盖
(1) 派生类的作用域嵌套在基类的作用域里面。内部作用域会覆盖外部作用域的同名对象。这是因为,编译器在查找名字时,是按照“由内到外”的顺序找的,即先在内层作用域里找,只有内层作用域找不到该名字时才到外层作用域找。
(2) 可以通过作用域操作符(::)来使用被隐藏的基类成员。
class Base{ public: Base() : data(0) {}; int func(); int data; }; class Derived{ public: Derived(int i) : data(i) {}; int GetData() { return data}; int func(int); // hide func() in the Base int data; // hide data in the Base }; Derived d(100); d.func(10); // calls Derived::func d.func(); // error: func with no arguments is hidden d.Base::func(); // ok: calls Base::func() cout << d.GetData() << endl; // print 100
(3) 派生类继承基类的虚函数时要保持函数参数一致,否则与派生类绑定的基类指针或引用无法调用派生类版本的该函数。
1 class Base { 2 public: 3 virtual int fcn(); 4 }; 5 6 class Derived { 7 public: 8 int fcn(int); // hides fcn in the Base; this fcn is not virtual. 9 }; 10 11 Base bobj; 12 Derived dobj; 13 Base *pBase1 = &bobj; 14 Base *pBase2 = &dobj; 15 pBase1->fcn(); // calls Base::fcn at run time 16 pBase2->fcn(); // calls Base::fcn at run time 17 pBase2->fcn(10); // error: Base has no version of fcn that takes an int.
(4) 若派生类希望能够使用其继承自基类的所有重载虚函数,则要么全部覆盖,要么全部不覆盖。如果部分覆盖,则基类中未被覆盖的函数会被隐藏。为简便起见,派生类可以使用using语句来表明使用基类的重载虚函数。使用using时,只需写函数名字而不必写函数参数列表。
六、构造函数与复制控制
FAQ:
1. 动态绑定是如何实现的?