C++(继承/虚函数)

一、类继承

1、什么是类继承

1、在解决一个问题之前,先考虑现有的类是否能解决部分问题,如果有则继承该类,在此基础上进行扩展,以缩短解决问题的目的,代码复用。

2、在解决一个复杂庞大的问题时,把问题拆分成若干个小问题,每个小问题实现一个类去解决,最后把这若干个类通过继承进行汇总,达到解决问题的目的,这个方式可以降低问题的规模、难度,也方便团队分工、协作。

2、类继承的语法格式
class 子类 : <继承方式 父类1>, <继承方式 父类2> // 继承表
{
    成员变量;
public:
    成员函数;
};
3、继承方式对访问权限的影响
成员的访问权限:
访问限定符类内子类类外
private可以不可以不可以
public可以可以可以
protected可以可以不可以

注意:类成员变量和成员函数一旦设置的访问属性,永远不会改变。

继承方式对成员的影响:

当成员变量在父类中是private、protected、public的访问属性,通过不同的方式继承到子类后,会变成什么权限?

父类中成员访问权限private继承后在子中类public继承到子类protected继承到子类
privateprivateprivateprivate
publicprivatepublicprotected
protectedprivateprotectedprotected

1、父类的成员被子类继承后,通过子类对象如果能在类外访问,则说明继承到子类后是public访问属性。

2、父类A的成员被子类B继承后,然后子类B再被孙子类C继承,在孙子类C的内部,如果能访问子类B中的成员,则说明该成员至少是protected,不能访问则说明是private。

3、继承方式只能影响父类成员继承到子类后变成什么访问方式(也就是成员被子类继承后,成员在外界、孙子类的访问权限),并不能影响父类成员在子类中的访问权限(是由父类中设置的访问权限决定的)。

4、C++语言的继承特点

1、C++的继承语法可以继承多个父类,这种语法叫多重继承。

2、子类继承父类后,会获取到父类中的所有内容,包括私有成员,即使不能直接访问,也会继承。

3、子类中的成员会隐藏父中的同名成员,在子类内部或通过子类对象访问的同名成员时,访问到的是子类的成员,但通过类名限定符在权限允许的情况下可以访问父类的同名成员。

4、子类对象(指针、引用)可以转换成父类对象(指针、引用),这是一种缩小转换,父类对象不能转换成子类对象,因为这是一种放大转换。子类对象转换成父类对象后,访问同名成员时,访问的是父类的成员变量和成员函数(因为子类会继承父类中的所有成员)。

5、子类对象可以直接调用父类的成员函数,子类对象计算出的地址,可以赋值给父类成员函数的this。

#include <iostream>
using namespace std;
​
class Base
{
public:
    int num;
    void func(void)
    {
        cout << "Base" << num << " " << &num << endl;
    }
};
​
// 通过不同的方式继承后,在子类中会变成什么权限
class Test:public Base
{
    int num;
public:
    void func(void)
    {
​
        cout << "Test" << num << " " << &num << endl;
    }
};
​
int main(int argc,const char* argv[])
{
    Test t;
    t.func();
    Base* bp = &t;
    bp->func();
    Base& b = t;
    b.func();
}   

6、当子继承多个父类时,子类会按照继承表的顺序挨个排列父类对象中的成员,当子类对象向父类对象转换时,会根据继承表计算出偏移值

#include <iostream>
using namespace std;
​
class BaseA
{
    int num;
};
class BaseB
{
    int num;
};
class BaseC
{
    int num;
};
​
class Test:public BaseA,public BaseB,public BaseC
{
​
};
​
int main(int argc,const char* argv[])
{
    Test t;
    BaseA* ap = &t; 
    BaseB* bp = &t; 
    BaseC* cp = &t;
    cout << ap << " " << bp << " " << cp << " " << &t << endl;  
    return 0;
}

7、子类的成员函数与父类的成员函数 重名构成不是重载关系,因为它们的作用域不同,构成的是隐藏关系(子类会隐藏父类的同名成员)。

在同一个作用域下,函数名相同,参数列表不同,构成重载关系。

在不同作用域下,函数名相同,构成隐藏关系。

二、对象的创建与释放过程

1、创建对象的过程
1、先为对象分配内存
Test t; // 可能在stack、data、bss分配内存
Test* p = new Test; // 在heap分配内存
2、按照继承表的顺序,调用父类无参构造函数,可以在初始化列表显式调用父类的有参构造,但要注意父类构造的执行顺序与初始化列表的调用顺序无关。

父类有参数构造调用方法:构造函数(...):类名(参数)

3、按照成员的定义顺序执行成员的无参构造函数,可以在初始化列表显式调用成员的有参构造。但要注意成员的构造执行顺序与初始化列表的调用顺序无关。

成员有参数构造调用方法:构造函数(...):成员名(参数)

4、在初始化列表中给const成员变量赋值,也可以给普通成员赋值,但没必要。
5、执行对象自己构造函数,在构造函数中为对象的使用做一些准备工作,例如:从文件、数据库加载数据、给指针成员分配堆内存等,由于构造函数没有返回值,尽量不要执行可能失败的事情,因为没有办法反馈给调用者(但可以抛异常)。
6、等构造函数执行完毕后,对象才完成的创建成功。

2、释放对象的过程

当对象离开做用域或显式执行了delete/delete[]语句,就开始了释放对象的过程。

1、执行对象自己的析构函数,完成一些对象消失后的收尾工作,例如:保存数据到文件、数据库,释放指针成员所指向的堆内存。
2、按照成员的定义顺序,逆序调用成员的析构函数。
3、按照继承表的继承顺序,逆序调用父类的析构函数。
4、释放对象自己所占用的内存。
#include <iostream>
using namespace std;
​
class A
{
    int num;
public:
    A(void)
    {
        cout << "A的无参构造" << endl;
    }
    A(int num):num(num)
    {
        cout << "A的有参构造" << endl;
    }
    ~A(void)
    {
        cout << "A的析构函数" << endl;
    }
};
class B
{
    int num;
public:
    B(void)
    {
        cout << "B的无参构造" << endl;
    }
    B(int num):num(num)
    {
        cout << "B的有参构造" << endl;
    }
    ~B(void)
    {
        cout << "B的析构函数" << endl;
    }
};
class C
{
    int num;
public:
    C(void)
    {
        cout << "C的无参构造" << endl;
    }
    C(int num):num(num)
    {
        cout << "C的有参构造" << endl;
    }
    ~C(void)
    {
        cout << "C的析构函数" << endl;
    }
};
class D
{
    int num;
public:
    D(void)
    {
        cout << "D的无参构造" << endl;
    }
    D(int num):num(num)
    {
        cout << "D的有参构造" << endl;
    }
    ~D(void)
    {
        cout << "D的析构函数" << endl;
    }
};
class E
{
    int num;
public:
    E(void)
    {
        cout << "E的无参构造" << endl;
    }
    E(int num):num(num)
    {
        cout << "E的有参构造" << endl;
    }
    ~E(void)
    {
        cout << "E的析构函数" << endl;
    }
};
class F
{
    int num;
public:
    F(void)
    {
        cout << "F的无参构造" << endl;
    }
    F(int num):num(num)
    {
        cout << "F的有参构造" << endl;
    }
    ~F(void)
    {
        cout << "F的析构函数" << endl;
    }
};
​
class Test:public C,public B,public A
{
    F f;
    E e;
    D d;
public:
    Test(void)
    {
        cout << "Test的无参构造" << endl;
    }
    Test(int num):f(num),e(num),d(num),C(num),B(num),A(num)
    {
        cout << "Test的有参构造" << endl;
    }
    ~Test(void)
    {
        cout << "Test的析构函数" << endl;
    }
};
​
int main(int argc,const char* argv[])
{
    Test* t = new Test(1234);   
    cout << "------------" << endl;
    delete t;
}

三、钻石继承与虚继承

1、什么是钻石继承

假定有一个A类,B类继承A类,C类也继承的A类,如果D类同时继承了B类和C类,那么这种继承结构就叫钻石继承,也就是多重继承时,多个父类有一个共同的父类。

2、钻石继承危害

1、每个父类都会继承一份爷爷类中的成员,子类会继承每个父类中的成员,那么多个父类中的爷爷类成员就会在子类汇聚,存在多份,也就是同样的内容存在多份,就造成了冗余,就会形成内存的浪费。

2、如果通过子类对象或者在子类的内部访问爷爷类中的成员,编译器无法分辨使用从B类中继承的还是从C类中继承的,编译就会报错。

3、要想解决这类问题就需要使用一种特殊的继承方式,虚继承。

3、什么是虚继承

父类在继承爷爷类时,在继承方式的前面增加 virtual 关键字就能解决,冗余和调用冲突的问题。

4、虚继承的原理

1、父类继承爷爷类时使用了虚继承,父类中就多4个字节,这4个字节就个指针(虚指针),该指针指向一个表格,表格中记录了从爷爷类对象中继承的内容。

2、孙子类会继承所有父类中的虚指针,然后对多个表格中的内容进行对比,如果从父类继承的成员是爷爷类中的,那么这种成员在孙子类中只保留一份。

3、虚继承没有太大的实用价值,因为其它一些面向对象的编程语言,如:Java只允许继承一个父类,就说明单继承可以解决所有问题,从而规避钻石继承带来的问题,因此我们在编写C++代码时,尽量只继承一个父类。

4、但要小心笔试题中的虚继承。

四、虚函数与函数覆盖(重写)

1、什么虚函数

使用 virtual 关键字修饰的成员函数,被称为虚函数。

2、什么是函数覆盖

如果父类中有虚函数,且子类中有与父类的虚函数函数签名相同成员函数,此时子类就会把从父类继承的虚函数覆盖掉。

当使用父类指针或引用,指向子类对象时,再通过父类对象的指针或引用调用虚函数时,访问到的是子类的同名函数。

也就是父类指针或引用,实际指向的是哪种对象,调用的就那个对象有成员函数,这种现象建立在函数覆盖的前提下。

3、构成函数覆盖的条件

1、一定发生在父子类之间。

2、父类中的成员函数是虚函数。

3、子类中的成员函数与父类虚函数的函数签名相同。

函数签名相同:

1、函数名必须相同

2、参数列表完全相同(参数的个数,类型完全相同,如果有指针、引用类型的参数,const属性也要相同,函数的常属性也要相同)。

3、返回值类型完全相同或是具有继承关系的父子类(如果参数列表已经相同,但返回值类型不同,编译会报错)。

4、函数覆盖的原理

1、一旦类中定义的虚函数,该类就会多一个隐藏指针成员(虚函数表指针),该指针指向一个表格,该表格中记录了类中所有虚函数的地址(函数指针数组)。

2、子类会继承父类中的虚函数表指针,编译器会检查子类中有没有与虚函数表中函数签名相同的成员函数,如果有则会把子类成员函数地址替换到虚函数表中。

3、当父类的指针或引用指向一个子类对象时,然后再调用虚函数,编译器就会按照虚函数表中的函数地址进行调用,如果实际对象是子类对象,调用的就是覆盖后的子类成员函数。

#include <iostream>
using namespace std;
​
class Base
{
public:
    virtual void func(void)
    {
        cout << "Base func" << endl;
    }
    virtual void func1(void)
    {
        cout << "func1" << endl;
    }
    virtual void func2(void)
    {
        cout << "func2" << endl;
    }
};
​
class Test:virtual public Base
{
public:
    void func(void)
    {
        cout << "Test func" << endl;
    }
};
int main(int argc,const char* argv[])
{
}
什么是函数隐藏、函数重载、函数覆盖?

隐藏:具有嵌套关系的作用域,有同名的函数(名字空间、全局与局部、父类与子类),内部作用域的函数会隐藏(屏蔽)外部作用域的同名函数。

重载:同一个作用域下,函数名相同,但参数列表不同。

覆盖(重写):父子类之间,父类中的函数是虚函数(虚函数表指针),子类中函数与父类中的函数签名相同,子类会覆盖父类的虚函数。

练习:设计一个类,在类内定义若干个虚函数,定义类对象后,通过对象内的虚函数表指针调用若干个虚函数。

五、多态

1、什么是多态

指令或语句的多种形态,也就指令或语句会根据环境的不同、参数不同会产生多种执行结果,这就叫多态。

2、编译时多态(静态多态)

指令或语句在编译时,编译器能根据环境或参数确定执行结果,这就叫作编译时多态,也叫静态多态,例如:调用重载过的函数,函数模板、类模板。

3、运行时多态(动态多态、类多态)

当定义一个父类的引用或指针时,使用它调用虚函数,它可能调用的是父类中的虚函数,也可能调用子类中的成员函数,但具体调用哪个版本的函数,要在程序运行时根据实际类对象才能决定。

4、构成类多态的原理

当使用父类的指针或引用指向一个对象时,然后通过父类指针或引用去调用虚函数,此时它会从虚函数指针所指向的虚函数表中寻找函数签名相同的函数进行调用。

如果指针或引用实际指向的是父类对象,那么虚函数表中记录就是父类中的虚函数地址,如果指针或引用实际指向的是子类对象,那么虚函数表中记录的就是覆盖后子类成员函数。

函数覆盖就是类多态的基础。

5、类多态的优点

1、使用类多态可以让调用者只关注被调用者的函数名,不需要关注背后是如何实现的。

2、作为多态的实现者,多态提高的代码的安全性、封装性,并且当代码需要更新和升级时,不需要通知调用者。

六、纯虚函数和抽象类

1、什么是纯虚函数

在虚函数(类成员函数)声明的末尾添加 =0 这种函数就叫纯虚函数,这种函数不需要实现,这种函数生来就被覆盖的。

2、什么是抽象类

类中有纯虚函数,这种类就叫抽象类,这种类不能创建类对象,必须被继承,不然就没有意义。

抽象类可以定义指针可引用,去指向子类对象,然后再根据实际指向的对象调用对象内的成员函数。

子类一旦继承了抽象类,必须把它的所有纯虚函数全部覆盖,否则子类也就变成了抽象类,不能创建对象。

3、什么是纯抽象类(接口类)

所以有成员函数都是纯虚函数,这种类就叫纯抽象类,是在C++语言中一种特殊的接口类,不负责实现具体的功能代码,它的意义在于告诉使用者过如何调用相关的接口,并且也限制继承者被调用时能得到哪些参数。

设计模式:

生产者与消费模式

单例模式

回调模式

命令模式

练习:了解一下工厂模式,并实现一个简单的工厂模式。

练习:了解一下策略模式,并实现一个简单的策略模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值