《C++ Primer》读书笔记 ch15 面向对象编程
一、基类与派生类
- 定义为virtual的函数(即虚函数)是基类期望派生类重新定义的,基类希望派生类继承的函数是不能定义为虚函数的。
- 通过基类的引用(或指针)调用虚函数会引发动态绑定。
- 引用(或指针)既可以绑定基类对象也可以绑定派生类对象。
引用(或指针)调用虚函数在运行时确定被调用的函数是引用(或指针)所绑定的对象实际类型所定义的。 - 非虚函数的调用在编译时确定。
- 除了构造函数外,任意非static成员函数都可以是虚函数,包括析构函数。
- 关键字virtual只能在类内部的成员函数声明中出想,外部定义体不能出现。
- 派生类只能通过派生类对象访问基类的protected成员,派生类对其基类类型对象的protected成员无特殊访问权限。
可以理解为:派生类的成员函数对于其基类类型(对象)而言,只是普通用户,因而不能访问protected成员。 - 简单理解protected成员:积累提供给派生类的接口是:protected和public。
- 若虚函数在派生类中要重新你定义,则要重新声明,但不一定要加virtual关键字(且默认此函数就是虚函数)。
派生类中虚函数的声明必须与基类中的定义方式完全匹配。
但是有一个例外:返回类型是基类(B)的引用(或指针),派生类中虚函数可以返回基类B的派生类的引用(或指针)。此处用基类(B)明确表示是为了强调此基类不一定是虚函数所属的类的基类,可以是任何类型的。 - 派生类对象的组成:派生类本身定义的非static成员;基类定义的非static成员组成的子对象。
- 声明包含类名,但不包括派生列表。
- 触发动态绑定的条件:
1)调用的函数是虚函数;
2)必须通过基类类型的引用(或指针)调用函数。 - 因为可以使用基类类型的引用(或指针)来引用派生类对象,所以用基类类型的引用(或指针)时,编译器并不知道引用(或指针)所绑定的对象类型。无论实际对象为何种类型,编译器都将它当作基类类型处理。
任何可以在基类类型上执行的操作也可以通过派生类对象使用,但是派生类自己新定义的成员,是不可以通过基类引用(或指针)访问。 - 非虚函数总是在编译时根据调用函数的对象、引用或指针类型而确定。
- 可以使用作用域操作符(::)覆盖虚函数调用机制并强制调用虚函数的特定版本。
*只用在成员函数中的代码才应该使用这种方法。
**派生类虚函数调用基类中的版本。即基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只要添加自己的特殊工作 - 虚函数与默认实参:
如果调用省略了具有默认值的实参,默认实参为调用该函数的引用(或指针)类型决定,与其绑定的对象无关。
1 #include <iostream> 2 3 using namespace std; 4 5 class A{ 6 public: 7 int aoo; 8 }; 9 10 class B:public A{ 11 public: 12 int boo; 13 }; 14 15 static A aDerive; // for Derive actual parameter 16 17 class Basic{ 18 public: 19 virtual A *foo(A *a = NULL){ 20 cout << "call Basic foo" << endl; 21 if(a == NULL) 22 cout << "a == NULL" << endl; 23 else 24 cout << "a == aDerive" << endl; 25 return a; 26 } 27 }; 28 class Derive:public Basic{ 29 public: 30 B *foo(A *a = &aDerive){ 31 cout << "call Derive foo" << endl; 32 if(a == NULL) 33 cout << "a == NULL" << endl; 34 else 35 cout << "a == aDerive" << endl; 36 37 return new B(); 37 } 38 }; 39 int main(void) 40 { 41 Derive d; 42 Basic *pb = &d; /* pb point to d */ 43 Derive *pd = &d; /* pd point to d too */ 44 45 pd->foo(); 46 pb->foo(); 47 return 0; 48 }
运行结果:
$ ./a.out call Derive foo a == aDerive call Derive foo a == NULL
可以看出,指针pd和pb调用了相同的函数,但是默认实参是不一样的,pb->foo()的实参是基类Basic中foo(A *a = NULL)定义的,而pd->foo()的实参是由派生类Derive中foo(A *a = aDerive)定义的。
- 公有、私有和受保护继承:
派生类可以进一步限制但不能放松对所继承的成员访问权限。
*访问标号的权限控制并不影响派生类对基类的访问权限。
**派生类访问标号还控制着来自非直接派生类的访问权限。
***派生类的访问标号只是影响派生类自身的接口(成员)访问权限,但不会影响自身对基类的访问。
附加说明:
1)通过public标号继承的派生类继承基类的接口,而使用private或pretected标号继承的派生类不继承基类的接口。
后者称为实现继承:派生类在仅实现中使用被继承的基类,但继承的基类部分没有成为派生类接口的一部分。
2)使用using关键字可以恢复继承成员的访问级别。
3)struct默认是public继承,class中默认是private继承。在编码中一般是要明确指出的。 - 友元关系是不能继承的。(分为两部分)
1)基类的友元对派生类的成员没有特殊的访问权限。
2)类型A有一个友元类B,D是B的派生类,则D对A没有特殊的访问权限。 - 如果基类定义了static成员,则整个继承层次中只有一个这样的成员。static成员遵循常规的访问控制(private等)。
二、转换与继承
- 基类类型的对象既可以作为独立对象存在,也可以作为派生类对象的一部分而存在。
- 没有从基类引用(指针)到派生类引用(指针)的自动转换。
- 对象初始化和赋值:
一般可以用派生类型的对象对基类对象进行初始化或赋值。(分别调用构造函数和赋值操作符)
复制构造函数的原型:Basic::Basic(const Basic& b),赋值操作符重载原型:Basic& Basic::operater=(const Basic &b),这两个函数的形参都是基类的引用,而引用是可以与其派生类绑定的。
*但是没有从派生类对象到基类对象的直接转换。
**思考,猜想?赋重载操作符等既可以定义在类外也可以作为类的成员函数,只是前者要遵循类的接口访问控制,而成员函数则可以直接访问类(左操作数)的所有成员。二者的访问权限不同是否可以作为决定的标准? - 派生类到基类转换的可访问性:转换是否访问取决于派生类的派生列表中指定的访问标号。
1)如果是public继承,则用户代码和后代类都可以使用派生类到基类的转换;
2)如果类是使用private或protected继承派生的,则用户代码不能将派生类型对象转换为基类对象;
3)private继承,则从private继承类派生的类不能转换为基类;
4)protected继承,则后续派生类的成员可以转换为基类类型。 - 基类到派生类的自动转换:
A)从基类到派生类的自动转换不存在。(包括对象、指针和引用)
B)可使用static_cast和dynamic_cast强制转换。
三、构造函数和复制控制
- 构造函数和复制控制成员不能继承,如果类中没有定义则使用合成版。
- 派生类的合成构造函数运行情况:
A)调用基类的默认构造函数;
B)用常规变量初始化派生类部分的成员。 - 用户定义的默认构造函数是隐式调用其基类的默认构造函数来初始化对象的基类部分。
因此作为基类最好定义一个默认构造函数。
(还需注意,用户定义了一个构造函数后编译器就不会合成默认构造函数了,有可能会导致派生类的默认构造函数调用基类默认构造函数失败) - 派生类构造函数的初始化列表只能初始化派生类成员,不能直接初始化基类部分的成员。
但是派生类构造函数可以将 基类构造函数包含在其构造函数初始化列表中以间接初始化基类部分成员。 - 构造函数初始化顺序:先初始化基类部分,再根据声明的次序初始化派生类的成员。
- 一个类只能初始化自己的直接基类。
*尊重基类的接口,尽量通过基类的接口修改基类。 - 如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式调用基类复制构造函数来初始化对象的基类部分。
同理,赋值操作符也适用上条规则。
四、继承情况下类作用域
- 继承情况下,派生类的作用域嵌套在基类作用域中。
如果不能在派生类作用域中确定的(变量/函数)名字,就在外围基类作用域中查找名字的定义,逐级查找。 - 与基类成员同名的派生类成员将屏蔽基类成员的直接访问,
但是可以使用作用域操作副(::)访问被屏蔽的基类成员。
注意:
A)设计派生类时最好避免与基类成员重名;
B)对于成员函数,即使函数原型不同,基类的同名成员也会被屏蔽。 - 派生类中定义的函数不重载基类定义的成员,
对于重载函数,派生类对象只能访问在派生类中定义的成员(即定义的重载函数)。 - 对重载函数提供using声明(只能指定一个名字)
可以将基类成员函数名称用using声明,使所有重载实例都加入到派生类的作用域中,而后派生类只需要重定义确实需要重定义的那些函数,其他版本人然使用基类的定义。 - 要获得动态绑定,必须通过基类的引用(指针)调用虚成员。编译器在基类中查找函数,如果找到就检查实参是否与形参匹配。这个也在一定程度上解释了,为什么默认实参和引用(指针)类型中定义的函数一致了。
五、纯虚函数
- 在虚函数的形参列表后面加上“=0”即可指定为纯虚函数。虚函数可以不被定义。
- 将函数定义为纯虚函数能说明,该类型为的派生类提供了可以覆盖的接口,在这个类中的版本绝不会被调用,而且用户不可以创建该类型的对象。
- 含有或继承虚函数的类是抽象类。(没有实现该虚函数的定义)
- 抽象类只能作为其派生类对象的组成部分,不可以创建抽象类的对象。
- 待续。。。。