知识杂货铺

不卖切糕

View on GitHub
17 April 2019 12:10

《王道程序员求职宝典》笔记 - 第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;

类型转换函数

转换构造函数将某一类型的数据转换成特定类型的对象,类型转换函数将某一类的对象转换成另一类的数据,他们刚好是相对的。

例如:

class Integral {
        public Integral(int);
        operator int();         //类型转换函数
private:
        int real;
};

Ingegral integ = 1;
int i = integ;                  //会调用类型转换函数将对象转换成相应类型的数据

非内建类型A和B,在一下几种情况下B可隐式转换成A

虚函数多态

多态性可总结为”一个接口,多个响应“,就是”对不同的对象来说一个接口可出现多种不同的行为“。

可分为静态多态性和动态多态性:

静态联编和动态联编

编译器在编译期根据调用方法参数的个数和类型决定使用哪一个函数,属于静态联编或早期联编,而有些情况下编译器无法在编译期确定而要在以后动态决定使用哪个函数的,叫做动态联编。

派生类中对虚函数重定义

使用指针访问非虚函数时,编译根据指针本身的类型决定要使用哪个函数,而不是指针类型,但是使用指针访问虚函数时,编译器根据指针所指向的对象的类型决定要调用哪个函数,而与指针本身的类型无关,这个过程就是动态联编。

另外,在使用引用调用虚函数时,调用函数也是访问引用对象类型决定的,差别是引用一经声明后,他所调用的方法就不会再改变,始终是最开始绑定的那个方法,所以引用相比来说比指针在使用虚函数的时候受限制,但有时候也可以理解为更安全。

所以C++中动态绑定只发生在使用指针或者引用调用虚函数的时候,。

构造函数不能时虚函数 虚函数使用的是什么是使用存储在对象内部的vptr指向的函数来实现的,如果构造函数是虚函数的话就需要在对象构造之前寻找vptr,不现实。

不能为虚函数的函数:

内涵成员函数、赋值操作服重载函数可声明为virtual,但没有实际意义。

$P_{166}例7$

构造函数与析构函数中的虚函数

派生类对象在构造过程中,先执行的基类构造函数之后才执行的派生类的构造函数,所以编译器将其看作从基类类型到派生类类型的转换,同理可应用于析构函数,并且构造函数和析构函数中可能会调用虚函数,调用的虚函数会是类自身定义的版本,不受上面的虚函数多态影响。也就是说,如果在构造函数或者析构函数中调用虚函数,无论以何种方式调用,调用的永远是这个构造函数所属的类的虚函数定义,但是正因为这种特殊性,所以这种使用应尽量避免。

虚函数标指针(vptr)和虚基类表指针(bptr)

c++中有两种数据成员:static和nonstatic,但是有三种成员函数:static、nonstatic和virtual。

static的数据和成员被放在全局数据区,nonstatic成员都放在类自身的内存中,比较特别的是virtual,virtual是通过如下流程工作的:

  1. 每一个类生成一些指向virtual functions的指针,放入一个表格中,称为virtual table(vtbl)。
  2. 每一个对象被添加了一个指针(virtual pointer, vptr),指向相关的virtual table,每个类的vptr设定和重置都是由构造函数、析构函数和复制构造函数自动完成,通常童语RTTI的类型信息也是在virtual table中的,而且通常在第一位。

$P_{168}$例1, 例2.

含静态变量与虚函数的类的空间大小计算

静态变量存储在全局区,不占用类大小空间。

虚函数无论有多少个虚函数,类中只添加一个vptr的内存空间。

虚函数表的内容

$P_{171}例2$

虚基类指针

在虚拟继承中,即继承类型增加一个virtual,基类不管被派生多少次,在派生类中也只存在一个实体,虚拟继承基类的子类中会增加某种指针,指向自身中的基类子对象,或者指向一个相关的表格,存储着基类子对象的地址或者其偏移量,这个指针就叫做虚基类指针,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;
        ...
};

动态运行时的类型识别和显式转换(RunTime Type Identification, RTTI)

typeid

c++提供typeid函数来识别一个对象的类型,可以动态的询问他,通过对两个对象的typeid进行比较就可以知道未知类型对象的类型了。

例如:

Base *bp
Derived *dp;

if (typeid (*bp) == typeid(*dp)) {}

当测试的对象有虚函数的时候,vptr中有动态id,通过这个知道是哪个对象,但是如果对象没有虚函数的时候,会直接通过静态时编译得到的类型。

显式转换

c++中的显式转换包括以下几种:

转换语句形式为:

cast-name<cast-type>(expression);

dynamic_cast的类型检查

习题

4

11 void *传递类型转换不改变内存值,容易出现错误。

13 析构函数为虚函数的意义。

14 dynamic_cast遍历继承树?意味着只要是实际类型的基类都可以?

tags: c++ - 笔试