C++学习笔记(九)

目录九

  • 二十一.继承(inheritance)
    • 9.多重继承
      • 9.1 定义
      • 9.2 向上造型,偏移计算
      • 9.3 名字冲突问题
    • 10.钻石继承和虚继承
      • 10.1 定义
      • 10.2 性质
      • 10.3 虚继承语法
  • 二十二.多态(Polymorphic)
    • 案例
    • 1.虚函数覆盖(函数重写),多态的概念
      • 1.1 函数重写的定义
      • 1.2 多态的定义
    • 2.虚函数覆盖(函数重写)的条件
    • 3.多态的条件
    • 4.多态原理(了解)
      • 4.1 定义
      • 4.2 结论
    • 5.纯虚函数、抽象类和纯抽象类
      • 5.1 纯虚函数
      • 5.2 抽象类
      • 5.3 纯抽象类
    • 多态案例:
    • 6.虚析构函数
      • 6.1 问题
      • 6.2 解决方法

二十一.继承(inheritance)

9.多重继承

9.1 定义

一个子类可以同时继承多个基类,这样继承方式称为多重继承。
eg.
在这里插入图片描述
eg.

#include 
using namespace std;

class Phone 
{//电话基类
public:
    Phone(const string& num) :m_num(num) {}
    void call(const string& num) {
        cout << m_num << "打给" << num << endl;
    }
private:
    string m_num;
};

class Player 
{//播放器基类
public:
    Player(const string& media) :m_media(media) {}
    void play(const string& music) {
        cout << m_media << "播放器播放" << music << endl;
    }
private:
    string m_media;
};

class Computer 
{//计算器基类
public:
    Computer(const string& os) :m_os(os) {}
    void run(const string& app) {
        cout << "在" << m_os << "系统上运行" << app << endl;
    }
private:
    string m_os;
};

//智能手机子类
class SmartPhone :public Phone, public Player, public Computer 
{
public:
    SmartPhone(const string& num,
        const string& media, const string& os)
        :Phone(num), Player(media), Computer(os) {}
};

int main(void) 
{
    SmartPhone iphone11("13866668888", "MP4", "IOS");
    iphone11.call("12580");
    iphone11.play("一路向北");
    iphone11.run("Angry Bird");
    return 0;
}

//输出:
//13866668888打给12580
//MP4播放器播放一路向北
//在IOS系统上运行Angry Bird

9.2 向上造型,偏移计算

eg.

#include 
using namespace std;

class Phone 
{//电话基类
public:
    Phone(const string& num) :m_num(num) {}
    void call(const string& num) {
        cout << m_num << "打给" << num << endl;
    }
private:
    string m_num;
};

class Player 
{//播放器基类
public:
    Player(const string& media) :m_media(media) {}
    void play(const string& music) {
        cout << m_media << "播放器播放" << music << endl;
    }
private:
    string m_media;
};

class Computer 
{//计算器基类
public:
    Computer(const string& os) :m_os(os) {}
    void run(const string& app) {
        cout << "在" << m_os << "系统上运行" << app << endl;
    }
private:
    string m_os;
};

//智能手机子类
class SmartPhone :public Phone, public Player, public Computer 
{
public:
    SmartPhone(const string& num,
        const string& media, const string& os)
        :Phone(num), Player(media), Computer(os) {}
};

int main(void) 
{
    SmartPhone iphone11("13866668888", "MP4", "IOS");

    SmartPhone* p1 = &iphone11;
    Phone* p2 = p1; //由子类到基类,向上造型
    Player* p3 = p1; //由子类到基类,向上造型
    Computer* p4 = p1; //由子类到基类,向上造型
    cout << "p1=" << p1 << endl;
    cout << "p2=" << p2 << endl;
    cout << "p3=" << p3 << endl;
    cout << "p4=" << p4 << endl;
    //p1和p2地址相同,p3地址比p1多24,p4地址比p1多48

    cout << "sizeof(string):" << sizeof(string) << endl;//在我电脑上运行等于40
    SmartPhone* p5 = static_cast<SmartPhone*>(p4); //由基类到子类,向下造型 p5 = p2 = p1
    cout << "p5=" << p5 << endl;

    return 0;
}

向上造型时,编译器会根据各个基类子对象的内存布局,自动进行偏移计算,以保证指针的类型和所指向的目标基类子对象类型一致。
(PS:向下造型时和向上造型也一样,但向下造型很少使用,参考下图理解)
sizeof(string)大小根据各个电脑和编译器版本)
C++学习笔记(九)_第1张图片

9.3 名字冲突问题

①如果一个子类的多个基类存在相同的名字,当通过子类访问这些名字时,编译器会报歧义错误——名字冲突。
②解决名字冲突的通用方法就是在前面加上“类名::”显式指明所访问的成员属于哪个基类。 推荐

③如果产生冲突是成员函数,并满足参数不同的重载要求,也可以通过using声明,让它们在子类中形成重载,通过函数的重载匹配来解决。
eg.

#include 
using namespace std;
class Base1{
public:
    void func(void){
        cout << "Base1的func(void)" << endl;
    }
    int m_i;
};
class Base2{
public:
    void func(int i){
        cout << "Base2的func(void)" << endl;
    }
    typedef int m_i;
};
class Derived:public Base1,public Base2{
public:
    //将两个基类中的func都声明到当前子类作用域,让它们在子类作用域中形成重载
    //using Base1::func;
    //using Base2::func;
};
int main(void){
    Derived d;
    d.Base1::func();
    d.Base2::func(123);

    d.Base1::m_i = 100; 
    Derived::Base2::m_i num = 200;
    cout << d.Base1::m_i << "," << num << endl;

    return 0;
}

10.钻石继承和虚继承

10.1 定义

一个子类的多个基类源自共同的基类祖先,这样继承结构被称为钻石继承。
eg.以下等等结构都属于钻石继承
C++学习笔记(九)_第2张图片
在这里插入图片描述
C++学习笔记(九)_第3张图片

10.2 性质

C++学习笔记(九)_第4张图片
问题:在创建末端子类(D)对象时,会存在多个公共基类(A)子对象,在通过末端子类访问公共基类的成员,会因为继承不同导致结果不一致。
eg.

#include 
using namespace std;

class A {
public:
    A(int data) :m_data(data) 
    {
        cout << "A:" << this << "," << sizeof(A) << endl; 
    }
protected:
    int m_data;
};
class B :public A {
public:
    B(int data) :A(data) 
    {
        cout << "B:" << this << "," << sizeof(B) << endl;
    }
    void set(int data) //修改值
    {
        m_data = data;
    }
};
class C :public A {
public:
    C(int data) :A(data) 
    {
        cout << "C:" << this << "," << sizeof(C) << endl;
    }
    int get(void) //获取值
    {
        return m_data;
    }
};
class D :public B, public C {
public:
    D(int data) :B(data), C(data) 
    {
        cout << "D:" << this << "," << sizeof(D) << endl;
    }
};
int main(void) {
    D d(100);
    cout << d.get() << endl;//100
    d.set(200);
    cout << d.get() << endl;//得到的结果是100而不是200

    cout << "sizeof(d):" << sizeof(d) << endl;//8

    return 0;
}

对于上面程序中,对d.get(200)设置为200后,再获取d的值时,发现还是100,配合下图的理解:
C++学习笔记(九)_第5张图片
解决办法:通过虚继承的特殊语法,可以让公共基类(A)子对象实例唯一,并为所有的中间(B、C)类共享,这样即使沿着不同的继承路径,所访问到公共基类的成员也是一致的。
C++学习笔记(九)_第6张图片
C++学习笔记(九)_第7张图片

10.3 虚继承语法

①在继承表中使用virtual关键字修饰
②位于继承链最末端的子类负责构造公共基类子对象

	 A(int m_data)
	/ \
   B   C //class B/C:virtual public A{...};
    \ /
 	 D //负责构造公共基类子对象

C++学习笔记(九)_第8张图片
eg.

#include 
using namespace std;
/*      A   //公共基类
 *     / \
 *    B   C //中间类
 *     \ /
 *      D   //末端子类
 * */
class A{
public:
    A(int data):m_data(data){
        cout << "A:" << this << "," << sizeof(A) << endl;
    }
protected:
    int m_data;
};
class B:virtual public A{//虚继承
public:
    B(int data):A(data){ 
        cout << "B:" << this << "," << sizeof(B) << endl;
    }
    void set(int data){
        m_data = data;
    }
};
class C:virtual public A{//虚继承
public:
    C(int data):A(data){
        cout << "C:" << this << "," << sizeof(C) << endl;
    }
    int get(void){
        return m_data;
    }
};
class D:public B,public C{
public:
    //虚继承时由末端子类负责构造公共基类子对象
    D(int data):B(data),C(data),A(data){
        cout << "D:" << this << "," << sizeof(D) << endl;
    }
};
int main(void){
    D d(100);
    cout << d.get() << endl;//100
    d.set(200);
    cout << d.get() << endl;//200

    cout << "sizeof(d):" << sizeof(d) << endl;//8

    return 0;
}

二十二.多态(Polymorphic)

案例

先举个案例,理解下多态:
C++学习笔记(九)_第9张图片

程序思路:定义图形基类,矩形子类继承图形基类(参数:中心点坐标,矩形长,矩形宽),圆形子类继承图形基类(参数:中心点坐标,半径)。
显示图形时,可以一个一个图形显示,但比较耗时,所以创建一个显示缓冲区,将要显示的图形先放到缓存区中,最后一起显示。

#include 
using namespace std;

class Shape {//图形基类
public:
    Shape(int x = 0, int y = 0) :m_x(x), m_y(y) {}
    void draw(void) {
        cout << "绘制图形:" << m_x << "," << m_y << endl;
    }
protected:
    int m_x;//位置坐标
    int m_y;
};

class Rect :public Shape {//矩形子类
public:
    Rect(int x, int y, int w, int h)
        :Shape(x, y), m_w(w), m_h(h) {}
    void draw(void) {
        cout << "绘制矩形:" << m_x << "," << m_y << "," << m_w << "," << m_h << endl;
    }
private:
    int m_w;//宽
    int m_h;//高
};

class Circle :public Shape {//圆形子类
public:
    Circle(int x, int y, int r) :Shape(x, y), m_r(r) {}
    void draw(void) {
        cout << "绘制圆形:" << m_x << "," << m_y << "," << m_r << endl;
    }
private:
    int m_r;//半径
};

void render(Shape* buf[]) {
    for (int i = 0; buf[i] != NULL; i++)
        buf[i]->draw();
}

int main(void) {
    //定义一个显示缓冲区,把要显示的图形先放到缓存区中
    Shape* buf[1024] = { NULL };
    buf[0] = new Rect(1, 2, 3, 4);
    buf[1] = new Circle(5, 6, 7);
    buf[2] = new Circle(8, 61, 71);
    buf[3] = new Rect(15, 26, 17, 81);
    buf[4] = new Circle(8, 9, 10);
    render(buf);
    return 0;
}

问题:然而上述程序的输出是:

//绘制图形:1,2
//绘制图形:5,6
//绘制图形:8,61
//绘制图形:15,26
//绘制图形:8,9

因为在三个类Shape、Rect、Circle中,都有draw()函数,定义缓存区buf[ ]时,用的Shape指针定义的,所以在遍历缓存区中调用draw()函数时,调用的是Shape基类中的draw()函数。
解决办法
在基类Shape中,在draw()函数前加virtual变成虚函数,则子类Rect、Circle中的draw()函数自动变成虚函数

#include 
using namespace std;

class Shape {//图形基类
public:
    Shape(int x = 0, int y = 0) :m_x(x), m_y(y) {}
    virtual void draw(void) {//虚函数
        cout << "绘制图形:" << m_x << "," << m_y << endl;
    }
protected:
    int m_x;//位置坐标
    int m_y;
};

class Rect :public Shape {//矩形子类
public:
    Rect(int x, int y, int w, int h)
        :Shape(x, y), m_w(w), m_h(h) {}
    void draw(void) {//自动变成虚函数
        cout << "绘制矩形:" << m_x << "," << m_y << "," << m_w << "," << m_h << endl;
    }
private:
    int m_w;//宽
    int m_h;//高
};

class Circle :public Shape {//圆形子类
public:
    Circle(int x, int y, int r) :Shape(x, y), m_r(r) {}
    void draw(void) {//自动变成虚函数
        cout << "绘制圆形:" << m_x << "," << m_y << "," << m_r << endl;
    }
private:
    int m_r;//半径
};
void render(Shape* buf[]) {
    /* 正常通过指针调用成员函数,根据指针的类型去
     * 调用;但是如果调用的是虚函数,不再根据指针的
     * 类型,而会根据指针所指向的实际目标对象类型
     * 取调用.
     * 如果是一个基类指针,实际指向目标对象可以是
     * 任何子类对象,就可以产生不同结果,这个语法现
     * 象就是多态.
     * */
    for (int i = 0; buf[i] != NULL; i++)
        buf[i]->draw();
}

int main(void) 
{
    //定义一个显示缓冲区,把要显示的图形先放到缓存区中
    Shape* buf[1024] = { NULL };
    buf[0] = new Rect(1, 2, 3, 4);
    buf[1] = new Circle(5, 6, 7);
    buf[2] = new Circle(8, 61, 71);
    buf[3] = new Rect(15, 26, 17, 81);
    buf[4] = new Circle(8, 9, 10);
    render(buf);
    return 0;
}

输出:

//绘制矩形:1,2,3,4
//绘制圆形:5,6,7
//绘制圆形:8,61,71
//绘制矩形:15,26,17,81
//绘制圆形:8,9,10

1.虚函数覆盖(函数重写),多态的概念

1.1 函数重写的定义

如果将基类中某个成员函数声明为虚函数,那么其子类中与该函数具有相同原型的成员函数就也是虚函数,并且对基类中的版型形成覆盖,即函数重写(override)。

1.2 多态的定义

满足虚函数覆盖要求后,通过指向子类对象的基类指针或者通过引用子类对象的基类引用,调用虚函数,实际被执行的将会子类中重写的覆盖版本,而不是基类中原始版本,这种语法现象就是多态

(多态:必须要产生虚函数覆盖;没有覆盖就没有多态;同样的函数,表示出不同的结果,这种语法现象就叫多态)

	class Base{
	public:
		virtual void func(void){}//声明为虚函数
	};
	class Derived:public Base{
		void func(void){}//自动变成虚函数
	};
	int main(){
		Derived d;
		Base* pb = &d;//pb:指向子类对象的基类指针
		Base& rb = d;//rb:引用子类对象的基类引用
		pb->func();//Derived::func()
		rb.func();//Derived::func()
	}

2.虚函数覆盖(函数重写)的条件

①只有类中的成员函数才能被声明为虚函数,而全局函数、静态成员函数、构造函数都不能被声明为虚函数。
(注:析构函数可以是虚函数)
②只有基类中以virtual关键字修饰的成员函数才能做为虚函数被子类覆盖,而与子类中的virtual关键字无关。
③虚函数在子类中覆盖版本和基类中的原始版本必须具有相同的函数签名,即函数名、参数表、常属性一致。
④如果基类中的虚函数版本返回基本类型数据,那么该函数在子类中覆盖版本必须返回相同类型的数据。
⑤如果基类中的虚函数版本返回类类型的指针(A*)或引用(A&),那么允许子类中的覆盖版本返回其子类类型的指针(B*)或引用(B&)。

class A{};
class B:public A{};

eg.

#include 
using namespace std;
class A{};
class B:public A{};
class Base{
public:
    virtual void func(void) const {
        cout << "Base的func" << endl;
    }
    virtual /*A**/A& foo(void){
        cout << "Base的foo" << endl;
    }
};
class Derived:public Base{
public:
    /*virtual*/void func(void) const {
        cout << "Derived的func" << endl;
    }
    /*B**/B& foo(void){
        cout << "Derived的foo" << endl;
    }
};
int main(void)
{
    Derived d;
    Base* pb = &d;
    pb->func();
    pb->foo();
    return 0;
} 

3.多态的条件

①多态语法特性除了要满足虚函数的覆盖条件,还必须是通过指针或引用调用虚函数,才能表现出来。

eg.

#include 
using namespace std;
class Base {
public:
    virtual int cal(int x, int y) {
        return x + y;
    }
};

class Derived :public Base {
public:
    int cal(int x, int y) {
        return x * y;
    }
};
int main(void) {
    Derived d;
    //Base b = d;//不能体现多态特性 b.cal(10,20)=30
    Base& b = d;//可以体现多态特性 b.cal(10,20)=200
    cout << b.cal(10,20) << endl;
    return 0;
}

②调用虚函数的指针也可以是this指针,当通过子类对象调用基类中的成员函数时,其this将是指向子类对象的基类指针,再通过它调用虚函数,同样可以表现多态的语法特性。(重点掌握)

eg.

#include 
using namespace std;
class Base {
public:
    virtual int cal(int x, int y) {
        return x + y;
    }

    //程序会翻译为:void func(Base* this=&d)
    //可以看到,是通过基类指针调用的虚函数,即满足了多态的条件,实际执行的是子类中的cal()成员函数
    void func(void) {
        cout << cal(10, 20) << endl;
        //上句代码程序会翻译为:cout << this->cal(10,20) << endl;
    }
};

class Derived :public Base {
public:
    int cal(int x, int y) {
        return x * y;
    }
};
int main(void) {
    Derived d;
    d.func(); // 输出:200
    return 0;
}

4.多态原理(了解)

4.1 定义

通过"虚函数表"和"动态绑定"来实现。
C++学习笔记(九)_第10张图片

#include 
using namespace std;
class A{
public:
    virtual void func(){
        cout << "testA" << endl;
    }
};
class B:public A{
private:
    void func(){
        cout << "testB" << endl;
    }
    virtual void func2(){
        cout << "testB2" << endl;
    }
};
int main(void)
{
    B b;
    (*(*(void(***)(void))&b))();
    (*(*(void(***)(void))&b+1))();
    return 0;
} 

4.2 结论

①虚函数表有内存开销
②动态绑定有时间开销
③虚函数不能内联优化
(总结:实际开发中如果没有多态语法要求,不要滥用虚函数)

5.纯虚函数、抽象类和纯抽象类

5.1 纯虚函数

语法:

virtual 返回类型 函数名(形参表) = 0; //相当于只声明了该函数,但未进行定义

eg.在二十二.多态(Polymorphic)中的案例,图形基类的draw()函数是虚函数,且构成了多态语法,则图形基类中的draw()函数并无起到作用,为简化代码,直接将图形基类改为如下,此时的draw()函数称为纯虚函数

class Shape{//图形基类
public:
    Shape(int x=0,int y=0):m_x(x),m_y(y){}
    virtual void draw(void)=0;//纯虚函数
protected:
    int m_x;//位置坐标
    int m_y;
};

5.2 抽象类

如果类中包含了纯虚函数,那么这个类就是抽象类。

(注:抽象类不能创建对象, 因为类中的成员函数是纯虚函数,只进行了声明,却没有定义,如若创建对象调用成员函数会报错)

//Shape s; error

5.3 纯抽象类

如果类中所有的成员函数都是纯虚函数,那么它就是纯抽象类。
eg.

#include 
using namespace std;
class Shape{//图形基类,抽象类/纯抽象类
public:
    Shape(int x=0,int y=0):m_x(x),m_y(y){}
    virtual void draw(void)=0;//纯虚函数
protected:
    int m_x;//位置坐标
    int m_y;
};
class Rect:public Shape{//矩形子类
public:
    Rect(int x,int y,int w,int h)
        :Shape(x,y),m_w(w),m_h(h){}
    void draw(void){//自动变成虚函数
        cout << "绘制矩形:" << m_x << "," << m_y
            << m_w << "," << m_h << endl;
    }
private:
    int m_w;//宽
    int m_h;//高
};
class Circle:public Shape{//圆形子类
public:
    Circle(int x,int y,int r):Shape(x,y),m_r(r){}
    void draw(void){//自动变成虚函数
        cout << "绘制圆形:" << m_x << "," << m_y
            << "," << m_r << endl;
    }
private:
    int m_r;//半径
};
void render(Shape* buf[]){
    /* 正常通过指针调用成员函数,根据指针的类型去
     * 调用;但是如果调用的是虚函数,不再根据指针的
     * 类型,而会根据指针所指向的实际目标对象类型
     * 取调用.
     * 如果是一个基类指针,实际指向目标对象可以是
     * 任何子类对象,就可以产生不同结果,这个语法现
     * 象就是多态.
     * */
    for(int i=0;buf[i]!=NULL;i++)
        buf[i]->draw();
}
int main(void){
    Shape* buf[1024] = {NULL};
    buf[0] = new Rect(1,2,3,4);
    buf[1] = new Circle(5,6,7);
    buf[2] = new Circle(8,61,71);
    buf[3] = new Rect(15,26,17,81);
    buf[4] = new Circle(8,9,10);
    render(buf);

    //Shape s;//error

    return 0;
}

多态案例:

#include 
using namespace std;

class PDFParser {
public:
	void parse(const char* pdffile)
	{
		cout << "解析出一些图形" << endl;
		onImage();
		cout << "解析出一些文本" << endl;
		onText();
	}
private:
	virtual void onImage(void) = 0;
	virtual void onText(void) = 0;
};

class PDFRender :public PDFParser {
private:
	void onImage(void)
	{
		cout << "显示图形" << endl;
	}

	void onText(void)
	{
		cout << "显示文本" << endl;
	}
};

int main(void)
{
	PDFRender pdf;
	pdf.parse("xx.pdf");
	return 0;
}

6.虚析构函数

6.1 问题

基类的析构函数不能调用子类的析构函数,所以delete一个指向子类对象的基类指针,实际被执行的仅是基类的析构函数,子类的析构函数执行不到,有内存泄漏风险。

eg.

#include 
using namespace std;
class Base {
public:
    Base(void) {
        cout << "基类动态内存分配" << endl;
    }
    ~Base(void) {
        cout << "基类动态内存释放" << endl;
    }
};
class Derived :public Base {
public:
    Derived(void) {
        cout << "子类动态内存分配" << endl;
    }
    ~Derived(void) {
        cout << "子类动态内存释放" << endl;
    }
};
int main(void) {
    Base* pb = new Derived;
    //pb->析构函数
    //如果不加虚析构函数,直接delete的话,调用的是基类Base的析构函数,释放的是基类的动态内存,
    //而不会释放子类Derived的内存,会造成内存泄漏
    delete pb;
    pb = NULL;
    return 0;
}

//输出:
//基类动态内存分配
//子类动态内存分配
//基类动态内存释放

6.2 解决方法

可以将基类的析构函数声明为虚函数,那么子类中的析构函数就也是虚函数,并且可以对基类中版本形成覆盖,也可以表现多态的语法特性,这时再delete一个指向子类对象的基类指针,实际被执行的将是子类中的虚析构函数,子类的析构函数在执行结束后又会自动调用基类的析构函数,从而避免内存泄漏。

eg.

#include 
using namespace std;
class Base {
public:
    Base(void) {
        cout << "基类动态内存分配" << endl;
    }
    virtual ~Base(void) {//虚析构函数
        cout << "基类动态内存释放" << endl;
    }
};
class Derived :public Base {
public:
    Derived(void) {
        cout << "子类动态内存分配" << endl;
    }
    ~Derived(void) {//自动变成虚析构函数
        cout << "子类动态内存释放" << endl;
    }
};
int main(void) {
    Base* pb = new Derived;
    //pb->析构函数
    delete pb;
    pb = NULL;
    return 0;
}

//输出:
//基类动态内存分配
//子类动态内存分配
//子类动态内存释放
//基类动态内存释放

你可能感兴趣的