《王道程序员求职宝典》笔记 - 第9章 面向对象编程
by 宋强
继承
单继承
class <child-class-name>:<inherit-type> <base-class-name>{
<new-members>
};
inherit-type是指public, private或者protected中的一种,具体影响见下节三种派生方式下的访问权限.
##多继承
class <child-class-name>:<inherit-type1> <base-class-name1>, <inherit-type2> <bas-class-name2>, ...{
<new-members>
};
多继承中的多份同名成员 如果一个类有多个直接基类,而这些基类有有一个共同的基类的话,这个子类中会存在多分这个共同基类的同名成员.要解决这个问题需要使用虚继承,虚继承的话公共基类成员在子类中仅会有一份拷贝.
### 三种派生方式下的访问权限
| 基类成员 | private | protected | public | private | protected | public | private | protected | public |
|---|---|---|---|---|---|---|---|---|---|
| 派生方式 | private | protected | public | ||||||
| 派生类中 | 不可见 | private | private | 不可见 | protected | protected | 不可见 | protected | public |
| 外部 | 不可见 | 不可见 | 不可见 | 不可见 | 不可见 | 不可见 | 不可见 | 不可见 | 不可见 |
派生方式类似一个水口,比本身小会让本身变小,否则就用本身的。也可以理解为两个中用更小的。
类间的转换(多态)
- 公有继承方式下,子类对象、子类指针和子类引用可以直接赋值给父类对象、父类指针和父类引用,会发生隐式转换,不需要写明类型转换。
- 基类指针与基类引用可以通过强制转换的方式赋值给子类指针或子类引用,但是基类对象不可以通过强制转换赋值给子类对象。
- 一个指向基类的指针可以指向他的公有派生类的任何对象。
多重继承中的多个this指针问题
多重继承中一个类可能会存在多个基类,多个基类就会有多个this指针,这会引出一个特殊情况:将子类指针赋值给基类时,基类指向的是子类指针中不同的位置。
比如存在以下三个类:
class A {
char data[16];
};
class B {
char data[16];
};
class C: public A, public B {
char data[16];
};
那么class C实际上的内存结构为:
| A | 16字节 |
|---|---|
| B | 16字节 |
| C | 16字节 |
如果有如下代码:
C c;
A *ra = &c;
B *rb = &c;
C *rc = &c;
假设c的地址值为0xffff1000,那么ra的值就是0xffff1000,而rb的值实际上是0xffff1010,也就是
| ra与rc -> | A | 16字节 |
|---|---|---|
| rb -> | B | 16字节 |
| C | 16字节 |
所以尽管他们的地址值不一样,但是当出现比较的时候,(ra == rc)与(rb == rc)都会返回真,这个主要是因为在作比较的时候,子类会被隐式转换成基类类型的对象,而当rc转换成B *的时候自然就会地址增加16个字节变成和rb一样了。
参考$P_{157}例1$
多基继承问题
如果多重继承中继承的多个类中具有同名成员,则编译器无法判定子类要使用的是哪一个基类中的成员,这个叫做对基类成员访问的二义性问题。
使用成员名限定来消除二义性。
class A {
public:
void print() {
cout << "Hello, this is A" << endl;
}
};
class B {
public:
void print() {
cout << "Hello, this is B" << endl;
}
};
方式一
class C : public A, public B {
public:
void disp() {
A::print();
}
}
方式二
class C : public A, public B {
public:
void print() { //会发生隐藏
A::print(); //根据需要选择调用A类的print还是B类的print,以实现更好的封装。
}
}
菱形继承问题
假如存在以下几个类:
class A {
public:
void print() {
cout << "This is A" << endl;
}
};
class B : public A {};
class C : public A {};
class D : public B, public C {};
void main () {
D d;
A *pa = (A*)&d;
}
这个时候D中的内存结构为:
| A |
|---|
| A |
将D的指针转换成A的时候出现了菱形多重继承带来的二义性,编译器会报错。
解决方法是先将D*类型转换为B*类型或者C*类型再转换成A*类型指定上行转换路径:
void main () {
D d;
A *pa = (A*)(B*)&d;
A *pa = (A*)(C*)&d;
}
将A定义为virtual也可以解决这个问题。
转换构造函数
如果一个类的构造函数只有一个参数,其他的都没有或者有缺省值,那么可以直接用这个参数赋值给先前的类进行构造,本质上是一种隐式转换。 例如:
class Integral {
public Integral(int);
private:
int real;
};
Ingegral integ = 1;
- 如果赋值的对象可以隐式转换成参数类型(如int可以转换成float类型),则也可以。
- 通过将构造函数声明explicit,可以禁止这种隐式转换。
类型转换函数
转换构造函数将某一类型的数据转换成特定类型的对象,类型转换函数将某一类的对象转换成另一类的数据,他们刚好是相对的。
例如:
class Integral {
public Integral(int);
operator int(); //类型转换函数
private:
int real;
};
Ingegral integ = 1;
int i = integ; //会调用类型转换函数将对象转换成相应类型的数据
- 转换函数必须是成员函数。
- 转换函数不能制定类型,用return返回制定目标类型的变量。
- 转换函数不能有参数。
非内建类型A和B,在一下几种情况下B可隐式转换成A
- B公有继承于A。
- B中有类型转换函数转换成A。
- A实现了非explicit的参数为B的引用或B的转换构造函数。
虚函数多态
多态性可总结为”一个接口,多个响应“,就是”对不同的对象来说一个接口可出现多种不同的行为“。
可分为静态多态性和动态多态性:
- 静态多态性:函数重载和运算符重载。
- 动态多态性:虚函数。
静态联编和动态联编
编译器在编译期根据调用方法参数的个数和类型决定使用哪一个函数,属于静态联编或早期联编,而有些情况下编译器无法在编译期确定而要在以后动态决定使用哪个函数的,叫做动态联编。
派生类中对虚函数重定义
- 与基类的虚函数有相同的参数个数。
- 与基类的虚函数有相同的参数类型。
- 与基类的虚函数有相同的返回类型,或者是返回指针或者引用时返回的是基类虚函数返回的指针或者引用的子类型。
使用指针访问非虚函数时,编译根据指针本身的类型决定要使用哪个函数,而不是指针类型,但是使用指针访问虚函数时,编译器根据指针所指向的对象的类型决定要调用哪个函数,而与指针本身的类型无关,这个过程就是动态联编。
另外,在使用引用调用虚函数时,调用函数也是访问引用对象类型决定的,差别是引用一经声明后,他所调用的方法就不会再改变,始终是最开始绑定的那个方法,所以引用相比来说比指针在使用虚函数的时候受限制,但有时候也可以理解为更安全。
所以C++中动态绑定只发生在使用指针或者引用调用虚函数的时候,。
构造函数不能时虚函数 虚函数使用的是什么是使用存储在对象内部的vptr指向的函数来实现的,如果构造函数是虚函数的话就需要在对象构造之前寻找vptr,不现实。
不能为虚函数的函数:
- 普通外部函数。
- 静态成员函数。
- 构造函数。
- 友元函数。
内涵成员函数、赋值操作服重载函数可声明为virtual,但没有实际意义。
$P_{166}例7$
构造函数与析构函数中的虚函数
派生类对象在构造过程中,先执行的基类构造函数之后才执行的派生类的构造函数,所以编译器将其看作从基类类型到派生类类型的转换,同理可应用于析构函数,并且构造函数和析构函数中可能会调用虚函数,调用的虚函数会是类自身定义的版本,不受上面的虚函数多态影响。也就是说,如果在构造函数或者析构函数中调用虚函数,无论以何种方式调用,调用的永远是这个构造函数所属的类的虚函数定义,但是正因为这种特殊性,所以这种使用应尽量避免。
虚函数标指针(vptr)和虚基类表指针(bptr)
c++中有两种数据成员:static和nonstatic,但是有三种成员函数:static、nonstatic和virtual。
static的数据和成员被放在全局数据区,nonstatic成员都放在类自身的内存中,比较特别的是virtual,virtual是通过如下流程工作的:
- 每一个类生成一些指向virtual functions的指针,放入一个表格中,称为virtual table(vtbl)。
- 每一个对象被添加了一个指针(virtual pointer, vptr),指向相关的virtual table,每个类的vptr设定和重置都是由构造函数、析构函数和复制构造函数自动完成,通常童语RTTI的类型信息也是在virtual table中的,而且通常在第一位。
$P_{168}$例1, 例2.
含静态变量与虚函数的类的空间大小计算
静态变量存储在全局区,不占用类大小空间。
虚函数无论有多少个虚函数,类中只添加一个vptr的内存空间。
虚函数表的内容
- 虚函数表通常有一个结束符,不同编译器不同。
- 虚函数表一次存储了类的虚函数的地址。
- 派生类中的函数如果覆盖了虚函数,虚函数表中相应的虚函数地址会被替换。
- 如果出现多重继承, 会有多个虚函数表指针。
$P_{171}例2$
虚基类指针
在虚拟继承中,即继承类型增加一个virtual,基类不管被派生多少次,在派生类中也只存在一个实体,虚拟继承基类的子类中会增加某种指针,指向自身中的基类子对象,或者指向一个相关的表格,存储着基类子对象的地址或者其偏移量,这个指针就叫做虚基类指针,bptr。
- 同时存在vptr与bptr时,某些编译器会将其优化成一个。
虚拟继承时构造函数的书写
正常菱形多重继承下构造函数应该书写为:
B(params_b) : A(params_a)
C(params_b) : A(params_a)
D(params_d) : B(params_b), C(params_c)
但是因为虚继承在整个内存中只有一份虚基类的拷贝,而这样的话B和C都会自己调用一次A的构造函数,本来只用调用一次就可以了。
实际上c++对其的处理为这样才是正常的:
B(params_b) : A(params_a)
C(params_b) : A(params_a)
D(params_d) : B(params_b), C(params_c), A(params_a)
D类那里如果不加编译器也会自己加上,始终只调用一次。
纯虚函数
纯虚函数就是基类中不定义函数体的虚函数,通常形式如下:
class <class-name> {
virtual <func-type> <func-name> (<param-list>) = 0;
...
};
- 含有纯虚函数的类称为抽象类,不可有实例,只可以派生其他类。
- 只定义了protected类型的构造函数的类也是抽象类,主要是这样的话他的构造函数外部没人可以执行。
动态运行时的类型识别和显式转换(RunTime Type Identification, RTTI)
typeid
c++提供typeid函数来识别一个对象的类型,可以动态的询问他,通过对两个对象的typeid进行比较就可以知道未知类型对象的类型了。
例如:
Base *bp
Derived *dp;
if (typeid (*bp) == typeid(*dp)) {}
当测试的对象有虚函数的时候,vptr中有动态id,通过这个知道是哪个对象,但是如果对象没有虚函数的时候,会直接通过静态时编译得到的类型。
显式转换
c++中的显式转换包括以下几种:
- static_cast:编译器隐式完成的任何类型转换都可以显式的用这个写出来,但是有一种情况除外,就是类间的下行转换。
- dynamic_cast:转换的目标类型必须是类得指针、引用或者void *,并且如果目标是指针或者引用,要转换的表达式的结果也必须是指针或者引用。
- const_cast:添加或删除const修饰时使用。
- reinterpret_cast:效果和C语言中强制转换相同。
转换语句形式为:
cast-name<cast-type>(expression);
- 第4章有介绍,c++的基本类型的指针之间不含有隐式转换,除void *外。
dynamic_cast的类型检查
- dynamic_cast会进行类型检查,但是他只会通过查询虚函数表中的type_id来查询,所以没有虚函数标的类是不能使用dynamic_cast的,但如果两个对象是一个类型,不需要转换则没问题。
- 如果被赋值的指针类型不是要转换到的指针类型,则会失败,结果为0,如果是被赋值的引用不是我们要的引用,那么会抛出一个bad_cast异常。
习题
4
11 void *传递类型转换不改变内存值,容易出现错误。
13 析构函数为虚函数的意义。
14 dynamic_cast遍历继承树?意味着只要是实际类型的基类都可以?
tags: c++ - 笔试