C++中的封装继承多态

1.C++中的封装

对比与C语言面向过程不一样的是,C++是一门面向对象的语言,即一切皆对象。我们要从需要编程的事物中抽象出数据成员和代码成员并将其结合一个整体,这就是封装思想。

cpp 复制代码
class Clock
{
public://公共
    int setTime(int h, int m, int s);
    void showTime();//成员函数


private://私有
    int hour, min, sec;//成员变量
};

上面就是一个封装的实例,用class关键字进行类的封装,class后面的就是类名也就是你想要称这个类的一个名称,大括号是封装的界限,里面包括了公共部分和私有部分,公共部分是可以再类外进行访问的,私有部分在类内进行访问。在类内声明的函数我们叫做成员函数,声明的变量我们叫做成员变量,成员变量表示的是该类的属性所以一般是放在私有部分,防止类外随意访问和更改。成员函数一般表示一些对外的接口,用于给类外使用,所以一般放在公共部分。对于类的解释,类主要就是相同属性和行为的一组对象的集合,和C语言中的结构体有着异曲同工之妙。

我们知道了类中有成员,那么我们怎么对这些成员进行访问呢,看下面的程序

cpp 复制代码
int main(int argc, char const *argv[])
{
    Clock clk; //类实例化一个对象
    clk.setTime(10,20,30);
    clk.showTime();
    return 0;
}

首先我们使用类实例化了一个对象,然后我们就可以通过对象.成员的方式来对类中的成员进行访问了,我们也可以使用类::的方式对成员进行访问。

cpp 复制代码
int Clock::setTime(int h, int m, int s)
{
    hour = h;    //在成员函数内可以直接访问成员函数和成员变量
    min = m;
    sec = s;
    return 0;
}

void Clock::showTime()
{
    cout << hour << ":" << min << ":" << sec << endl;
}

然后我们要知道的是在上面定义的类中我们只是对类中的成员函数进行了声明,我们需要在类外进行定义,定义时要用类名加上作用域限定符来定函数进行域的说明,说明该函数时哪个类的成员函数,如上所示。我们也可以在类内进行函数的声明和定义后面我们会说到。

接下来我们说一下类重要的两个函数构造和析构函数,相关程序如下:

cpp 复制代码
class Clock
{
public://公共
    Clock(int h=0, int m=0, int s=0);//构造函数,函数名与类名相同,没有返回值,用于初始化对象,在类实例化对象时自动调用
    ~Clock();//析构函数,函数名与类名相同,没有返回值,用于释放对象占用的内存,在对象生命周期结束时自动调用
    int setTime(int h, int m, int s);
    void showTime();//成员函数


private://私有
    int hour, min, sec;//成员变量
};

构造函数的函数名和类名相同没有返回值,构造函数分为无参构造和有参构造。这里我们看到在程序中的构造函数中我们给了构造函数默认参数,默认参数即在声明函数时就给对了对应得参数一个初始值,前面得C++基础语法总结中讲到过。这里需要注意的是当使用了默认参数构造函数是,再使用无参构造时会造成编译错误,因为编译器不知道你要调用的时无参构造还是默认参数的构造,在具体的应用中构造函数主要是在对象创建时被调用,用来初始化一些参数。析构函数就是与构造函数一样函数名与类名相同,没有返回值,在具体的应用中主要用于在对象销毁时调用来清理对应的内存空间等。注意析构函数和构造函数的调用顺序是相反的,构造函数的调用顺序是入栈顺序,析构函数的调用顺序是出栈顺序。

cpp 复制代码
Clock::Clock(int h, int m, int s)
{
    cout << "构造函数被调用" << endl;
    hour = h;
    min = m;
    sec = s;
}

//析构函数
Clock::~Clock()
{
    cout << "析构函数被调用" << endl;
}

我们在类外对构造函数和析构函数进行定义。

在构造函数中有一种特别的构造函数叫拷贝构造函数。

cpp 复制代码
Clock(Clock &clk);                  //拷贝构造函数

该段程序就是定义了一个拷贝构造函数,可见拷贝构造的形参就是本类对象的引用。

cpp 复制代码
Clock clk4;
    Clock clk5 = clk4;
    Clock clk6(clk4);
    Clock clk7 = clk4.setup(1,1,1);

拷贝构造被调用的三种情况分别对应为该程序中的三种:当用类对象去初始化该类的另一个对象时系统自动调用拷贝构造,若函数的形参为类对象时调用函数时实参赋值给形参,系统调用拷贝构造,当函数的返回值是类对象的引用时,系统调用拷贝构造。该函数如下:

cpp 复制代码
Clock & Clock::setup(int h, int m, int s)
{
    this->hour = h;
    this->min = m;
    this->sec = s;
    return *this;  //this是当前类的对象指针,解引用后就是当前类的对象
}

在该函数中我们看到了一个this指针,在类中的成员函数的第一个参数就是this指针,我们可以通过this指针来访问类中成员,注意的是this指针不能用来访问静态成员。

cpp 复制代码
static string factory;               //声明一个静态成员变量,所有对象共享同一个变量,共同维护该静态变量

以上程序就是对一个静态成员的声明,对于静态成员我们还需要知道静态成员是所用该类对象的共同成员,需要所有对象一起来维护,当有一个对象更改了对应的值之后其他对象中该值也会更改。

cpp 复制代码
clk1.setFactory("shanghai");    //一个对象更改了静态成员变量,所有对象的静态成员变量都被更改了
    clk1.showFactory();
    clk2.showFactory();
    clk3.showFactory();

如上当其中一个对象调用成员函数来对该静态变量进行更改后,其他对象的该变量也会更改,上面的两个函数如下所示:

cpp 复制代码
{
public://公共
    Clock(int h=0, int m=0, int s=0);   //构造函数,函数名与类名相同,没有返回值,用于初始化对象,在类实例化对象时自动调用
    ~Clock();                           //析构函数,函数名与类名相同,没有返回值,用于释放对象占用的内存,在对象生命周期结束时自动调用
    Clock(Clock &clk);                  //拷贝构造函数
    Clock & setup(int h, int m, int s); //返回值是类对象的引用函数
    int setTime(int h, int m, int s);
    void showTime();                    //成员函数声明
    void setFactory(string f)           //成员函数可以直接在类内声明和定义也可以在类外定义
    {
        factory = f;
    }
    void showFactory()
    {
        cout << "factory = " << factory << endl;
    }


private://私有
    int hour, min, sec;                  //成员变量
    static string factory;               //声明一个静态成员变量,所有对象共享同一个变量,共同维护该静态变量
};

该两个函数就是我们在上面提到的我们可以在类内对成员函数进行声明和定义。当然除了静态成员变量我们还有静态成员函数如下。

cpp 复制代码
static void setFactory1(string f)  //静态成员函数,静态成员函数只能访问静态成员函数和静态成员变量
    {
        factory = f;
    }

在静态成员函数中我们只能访问静态成员变量。

我们紧接着来介绍一下在封装中的另一个概念友元函数。如果我们想要使用类外的函数来访问类内的成员时,我们就要使用到友元函数。如下:

cpp 复制代码
float distance(point p1, point p2)
{
    int x = p1.x - p2.x;
    int y = p1.y - p2.y;
    return sqrt(x*x + y*y);
}

当我们想要使用该函数来访问类内的成员时,我们要进行声明如下:

cpp 复制代码
friend float distance(point p1, point p2);//友元函数,用friend关键字声明后可以访问类的私有成员

除了友元函数外我们还有友元类的概念。若该类为另一个类的友元类时则该类所有的成员都能访问对方类中私有成员,但是对方类并不能访问此类中的私有成员。即友元类的关系时单向的。

cpp 复制代码
friend class line;                        //友元类,用friend关键字声明该类后可以访问类的私有成员

以上就是声明了line类是我的友元类。

在类中若要使用一个还没有被声明的类就要使用前向引用声明,即在该类前先声明要时使用的类,如下。

cpp 复制代码
class B;    //前向引用申明,若A要使用B,必须先声明B,否则编译错误

class A
{
private:    //使用前向引用声明虽然可以解决在类B还没有定义时先使用B的问题      
    //B b;  //但是直接在类A中实例化B的对象,用为B还没有创建,会导致编译错误
    B *pb;  //正确的用法时在类A中使用指向类B的指针
public:
};

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

可见我们想要在类A中使用类B我们就需要进行前向引用声明,但是就算我们进行了前向引用声明但是要是想在类A中使用B实例化对象也是不行的编译会报错,因为B还没有被创建,所以正确的使用方法应该是在A中使用B类型的指针。

2.C++中的继承

继承就是指的在保持自己类的特性不变的情况下去构建一个新的类,该过程就叫做继承。其中新的类叫做子类或者派生类,原来的类叫做父类或者基类。派生类具有父类本来的特性且具有自己新的特性。继承方式有三种:公有继承,私有继承,保护继承。具体写法如下:

cpp 复制代码
class rect : public point // 公有继承 父类point
{
private:
    float width, height; // 声明私有成员变量
public:
    rect();                                                    // 无参构造
    rect(float x,float y,float width, float height);                                    // 有参构造
    ~rect();                                                   // 析构函数
    void setrect(float x, float y, float width, float height); // 设置矩形
    void showrect();
};

class rect : private point // 私有继承 父类point
{
private:
    float width, height; // 声明私有成员变量
public:
    rect();                                                    // 无参构造
    rect(float x,float y,float width, float height);                                    // 有参构造
    ~rect();                                                   // 析构函数
    void setrect(float x, float y, float width, float height); // 设置矩形
    void showrect();
};

class rect : protected point // 保护继承 父类point
{
private:
    float width, height; // 声明私有成员变量
public:
    rect();                                                    // 无参构造
    rect(float x,float y,float width, float height);                                    // 有参构造
    ~rect();                                                   // 析构函数
    void setrect(float x, float y, float width, float height); // 设置矩形
    void showrect();
};

在私有继承和保护继承只需要将public改成private和protected即可,主要在类名和这些关键字之间有一个:符号,关键字后加上要继承的基类的类名。不同的继承方式的主要区别就是访问权限的不同,公有继承中基类的public和protected成员的访问属性在派⽣类中保持不变,派生类中的成员函数可以直接访问基类中public和protected成员,但是不能直接访问private成员,且派生类的对象只能访问基类中的public成员。私有继承中基类的public和protected成员都以private⾝份出现在派⽣类,派生类的成员函数可以访问基类中的public和protected成员但是不能访问private成员,派生类的对象不能访问基类的任何成员。保护继承中基类的public和protected成员都以protected⾝份出现在派⽣类中,派生类中的成员函数可以访问基类的public和protected成员但是不能访问private成员,派生类的对象不能访问基类中的任何成员。

公有继承: 没有实现对数据的隐藏,能在继承时延续向下传播 。私有继承: 实现了对数据的隐藏 , 不能在继承时延续向下传播 。保护继承: 实现了对数据的隐藏,能在继承时延续向下传播。

在继承中因为基类和派生类都有构造函数,所以在派生类创建对象时,会先调用基类的构造函数创建一个基类对象在调用派生类的构造器函数创建派生类对象。

cpp 复制代码
int main(int argc, char const *argv[])
{
    rect r1;              //不传递参数,调用无参构造
    r1.showrect();
    //r1.showpoint();     //保护继承时子类对象不能访问父类的任何成员,但是在子类的成员函数中可以访问父类的公有和保护的成员
    rect r2( 0, 0, 3, 4); //传递参数,调用有参构造
    return 0;
}

在派生类创建对象时不传递参数时调用的是基类的无参构造,然后调用派生类无参构造,当个基类没有无参构造时会出现编译错误所以需要注意。当派生类创建对象时传递参数则调用基类的有参构造和派生类的有参构造。对于继承的内存关系是派生类的内存相当于在基类的内存上再扩充了一部分内存用来存储派生类中自己的特性。

cpp 复制代码
void display() // 显示坐标
    {
        cout << "point:display()" << endl;
    }
};

class rect : public point // 公有继承 父类point
{
private:
    float width, height; // 声明私有成员变量
public:
    rect();                                                    // 无参构造
    rect(float x,float y,float width, float height);                                    // 有参构造
    ~rect();                                                   // 析构函数
    void setrect(float x, float y, float width, float height); // 设置矩形
    void showrect();
    void display() // 显示坐标
    {
        cout << "rect:display()" << endl;
    }
};

当派生类和基类的成员函数相同通过派生类对象调用该函数时,派生类成员函数会屏蔽基类成员函数。

这里我们所讲的继承都是单继承,在继承中还有一个重要概念就是多继承,即一个派生类继承于多个基类,所以也就拥有多个基类的特性。

cpp 复制代码
class roundtable : public circle, public table  //多继承,可以访问多个父类的成员
{
private:
    
public:
    roundtable() {}                        //无参构造
    roundtable(float rr, float hh, string cc);  //有参构造
    void show_roundtable();// 显示圆台信息
};

以上程序就是一个多继承,多继承的写法就是在原来单继承的写法后面加上一个,然后写上要采用什么方式继承基类是谁即可,该类可以访问多个基类中的成员。在多继承的构造函数调用中因为派生类继承于多个基类,所以在派生类创建对象时会调用多个基类的构造函数,同样不传递参数调用无参构造传递参数调用有参构造,调用的顺序取决于继承时书写基类的顺序。该继承关系的完整程序如下:

cpp 复制代码
#define _USE_MATH_DEFINES  // 添加这一行,必须在包含cmath之前
#include <iostream>
#include <cmath>
using namespace std;   

class circle
{
private:
    float r;
public:
    circle() {}                        //无参构造
    circle(float rr){r = rr;}            //有参构造
    void setradius(float rr){r = rr;}    // 设置半径
    float area(){return r * r * M_PI;} // 计算面积
    void show()
    {
        cout << "圆桌的面积是:" << area() << endl;
    }
};

class table
{
private:
    float h;
    string color;
public:
    table() {}                        //无参构造
    table(float hh, string cc){h = hh; color = cc;}
    void setheight(float hh){h = hh;}    // 设置高度
    void setcolor(string cc){color = cc;}    // 设置颜色
    float getheight(){return h;}    // 获取高度
    string getcolor(){return color;}    // 获取颜色
    void show()
    {
        cout << "圆桌的高度是:" << getheight() << endl;
        cout << "圆桌的颜色是:" << getcolor() << endl;
    }
};

class roundtable : public circle, public table  //多继承,可以访问多个父类的成员
{
private:
    
public:
    roundtable() {}                        //无参构造
    roundtable(float rr, float hh, string cc);  //有参构造
    void show_roundtable();// 显示圆台信息
};

roundtable::roundtable(float rr, float hh, string cc) : circle(rr), table(hh, cc) {}  //有参构造

void roundtable::show_roundtable()
{
    cout << "圆桌的面积为:" << area() << endl;
    cout << "圆桌的高度为:" << getheight() << endl;
    cout << "圆桌的颜色为:" << getcolor() << endl;   //调用父类的函数
}

int main(int argc, char const *argv[])
{
    roundtable rt(1, 0.75, "red");                //调用无参构造
    //rt.show();       //当父类中有同名函数时,子类调用该函数会出现错误,这就是多继承中的二义性问题
                       //不知道要调用哪个父类的show函数
                       //解决方法:使用类名加作用域运算符::来指定调用哪个父类的show函数
    rt.circle::show();
    rt.table::show();
  
    return 0;
}

在多继承中,当基类中有同名函数时派生类对象调用该同名函数就会出现编译错误,因为编译器不知道你要调用的是哪个基类中的该函数产生了歧义,我们可以通过用类名加作用域限定符的方式来对不同基类中的同名函数进行访问。

在类中也还有一个重要的概念就是组合类,即该类声明另一个类的对象时就是组合类,其中声明的对象可以叫做内嵌对象,如下:

cpp 复制代码
class point
{
private:
    float x, y;

public:
    point()
    {
        x = 0;
        y = 0;
    } // 无参构造
    point(float xx, float yy)
    {
        x = xx;
        y = yy;
    } // 有参构造                       // 设置点的坐标
    float getx() { return x; } // 获取点的横坐标
    float gety() { return y; } // 获取点的纵坐标
};

class line
{
private:
    point p1, p2; // 组合类,line类包含了两个point类的对象
public:
    line() {}                                                                                                                                 // 无参构造
    line(float xx1, float yy1, float xx2, float yy2) : p1(xx1, yy1), p2(xx2, yy2) {}                                                          // 在组合类的有参构造写法
    float getlength() { return sqrt((p2.getx() - p1.getx()) * (p2.getx() - p1.getx()) + (p2.gety() - p1.gety()) * (p2.gety() - p1.gety())); } // 获取线的长度
    void show()
    {
        cout << "线的长度为:" << getlength() << endl;
    }
};

对于组合类若要在创建对象时传递参数,不仅要对自己的基本类类型进行初始化还要对要被组合的类的对象成员初始化,所以编写有参构造时就要进行如下写法:

cpp 复制代码
roundtable::roundtable(float rr, float hh, string cc,string kk,int nn) 
: circle(rr), table(hh, cc), f1(kk,nn) //初始化列表,先初始化父类,再初始化内嵌对象,在调用构造函数时会先调用父类的构造函数
 {                                     //再调用内嵌对象的构造函数,最后调用自己的构造函数,父类构造函数的调用顺序取决于继承列表的顺序
                                       //内嵌对象的构造函数调用顺序取决于声明的顺序
 }  //有参构造

这里的话我们的该类即进行多继承又进行了组合,即在初始化列表中我们要先初始化基类,然后再初始化内嵌对象。对于内嵌对象构造函数调用取决于声明的顺序。

在继承中我们可以讲派生类当作基类使用,因为派生类继承了基类的成员。当我们将创建的对象赋值基类类类型的指针来访问同名函数时,我们可以访问到基类中的同名函数,但是当我们用对象直接来访问基类同名函数时就会发生我们上面说到的派生类同名函数屏蔽基类同名函数。程序如下可验证:

cpp 复制代码
#include <iostream>
using namespace std;

class B0
{
private:
public:
    void display()
    {
        cout << "B0:display" << endl;
    }
};

class B1 : public B0
{
private:
public:
void display()
    {
        cout << "B1:display" << endl;
    }
};

class D1 : public B1
{
private:
public:
void display()
    {
        cout << "D1:display" << endl;
    }
};

int main(int argc, char const *argv[])
{

    B0 b0;
    B1 b1;
    D1 d1;
    B0 *p;   //定义一个B0类类型的指针
    p = &b0;
    p->display();  //通过该指针来调用display函数
    p = &b1;
    p->display();  //通过该指针来调用display函数
    p = &d1;
    p->display();  //通过该指针来调用display函数
    //结果显示的都是B0的display函数,这是因为指针p是B0类类型的,定义是只指向该内存空间
    //虽然我们子类继承了父类可以当父类使用因为其可以使用父类的所有成员,
    //但是指针只会指向父类的内存空间,所有调用display函数是父类的display函数

    b0.display();
    b1.display();
    d1.display();
    //通过这种方式调用display函数,就会使用子类的display函数,因为其屏蔽了父类的display函数

    return 0;
}

这是为什么呢,想要理解好这是怎么回事我们结合下面的内存图即可:

因为我们定义的指针是指向基类说存储的那块内存空间的所以我们将由派生类创建的对象赋值给该指针时指针还是指向的原来那块空间,所以用该指针调用该函数时就会调用基类的同名函数。而直接用派生类的对象进行调用就会出现派生类同名函数屏蔽基类同名函数。

3.C++中的多态

在C++中多态的具体表现为函数重载,运算符重载,虚函数等。函数重载在前面C++基础语法中已经说过,下面我们首先来看看运算符重载,程序如下:

cpp 复制代码
#include <iostream>
using namespace std;

class complex
{
private:
    float real;
    float imag;
public:
    complex(float r, float i){real = r; imag = i;}  //有参构造
    void show()
    {
        cout << "(" << real << "," << imag << ")" << endl;
    }
    friend complex operator+(complex &c1, complex &c2);//友元函数
    friend complex operator-(complex &c1, complex &c2);//友元函数,双目运算符重载
};

complex operator+(complex &c1, complex &c2)
{
    complex c(c1.real + c2.real, c1.imag + c2.imag);
    return c;
}

complex operator-(complex &c1, complex &c2)
{
    complex c(c1.real - c2.real, c1.imag - c2.imag);
    return c;
}



int
main(int argc, char const *argv[])
{
    complex c1(1, -2);
    c1.show();
    complex c2(3, 9);
    c2.show();
    complex c3 = c1 + c2;
    c3.show();
    complex c4 = c1 - c2;
    c4.show();
    return 0;
}

对于运算符重载实际上就是函数重载,运算符的重载是非常必要的,C++库已经给我们定义了对基本类型的数据使用对应的运算符后数据应该怎么处理,但是对于我们的自定义类型库中并没有给出对应的处理方法所以就需要我们来实现了。上面程序中我们实现的就是两个类相加我们应该怎么处理,根据我们想要的结果就是对两个类中成员变量相加即可,所以我们对+运算符的重载就是对成员变量相应相加即可。

cpp 复制代码
class complex
{
private:
    float real;
    float imag;
public:
    complex(float r, float i){real = r; imag = i;}  //有参构造
    void show()
    {
        cout << "(" << real << "," << imag << ")" << endl;
    }

    //运算符重载重载为成员函数,参数-1
    //这里的c1 + c2 等价于c1.operator+(c2)
    complex operator+(complex &c2)
    {
        complex c(real + c2.real,imag + c2.imag);
        return c;
    }
    complex operator-(complex &c2)
    {
        complex c(real - c2.real,imag - c2.imag);
        return c;
    }
};

在前面中我们在类外实现了运算符的重载,对比以上两段程序中运算符重载我们可以看到在类内进行运算符重载比在类外少了一个参数,为什么呢跟本原因就在于我们前面说到的this指针,在类内的成员函数的第一个参数其实就是被隐藏的this指针所以我们就不需要再指定出一个当前类的类一引用参数了,所以再类内的运算符重载就成了c1 + c2 等价于c1.operator+(c2)。然后还需要注意的是再类外定义的运算符重载要声明为类的友元函数。

上面我们实现了双目运算符重载,下面我们来看看单目运算符的重载:

cpp 复制代码
class Clock
{
private:
    int hour;
    int minute;
    int second;
public:
    Clock(int h, int m, int s){hour = h; minute = m; second = s;}
    void show()
    {
        cout << hour << ":" << minute << ":" << second << endl;
    }

    //单目运算符重载,前置++,operator++()可以看作一个函数,没有形参
    //重载成成员函数
    void  operator++()
    {
        second++;
        if(second >= 60)
        {
            second = 0;
            minute++;
            if(minute >= 60)
            {
                minute = 0;
                hour++;
                if(hour >= 24)
                {
                    hour = 0;
                }
            }
        }
    }
    //单目运算符重载,后置++,operator++(int)可以看作一个函数,有一个int形参
    //重载成成员函数
    void  operator++(int)
    {
        second++;
        if(second >= 60)
        {
            second = 0;
            minute++;
            if(minute >= 60)
            {
                minute = 0;
                hour++;
                if(hour >= 24)
                {
                    hour = 0;
                }
            }
        }
    }


};

通过单目运算符在类内的定义我们可以看到对于前置++和后置++二者在重载时相差了一个int参数,没有该参数的是对前置++的重载有参数的是对后置++的重载,下面我们来看看单目运算符在类外的定义:

cpp 复制代码
/单目运算符重载,前置++,operator++()可以看作一个函数
    //重载成友元函数, 要比重载成员函数多一个参数
    void  operator++(Clock &c)
    {
        c.second++;
        if(c.second >= 60)
        {
            c.second = 0;
            c.minute++;
            if(c.minute >= 60)
            {
                c.minute = 0;
                c.hour++;
                if(c.hour >= 24)
                {
                    c.hour = 0;
                }
            }
        }
    }
    //单目运算符重载,前置++,operator++()可以看作一个函数, 后置++需要多一个int行参
    //重载成友元函数, 要比重载成员函数多一个参数
    void  operator++(Clock &c , int)
    {
        c.second++;
        if(c.second >= 60)
        {
            c.second = 0;
            c.minute++;
            if(c.minute >= 60)
            {
                c.minute = 0;
                c.hour++;
                if(c.hour >= 24)
                {
                    c.hour = 0;
                }
            }
        }
    }

对比在类内定义在类外定义要多一个参数,该参数为类对象的引用,用于在函数内对类中成员变量进行访问处理,然后我们还需要将其重载为类的友元函数。

下面我们来看看多态中的另一种形式虚函数。虚函数的关键字为virtual,在成员函数前加上virtual关键字后该函数就被声明为了虚函数。虚函数可以实现派生类函数对基类同名函数的重写,可以使用基类指针引用来调用派生类的同名函数。

cpp 复制代码
class X0
{
    private:
    public:
    virtual void display()  //声明并定义一个虚函数
    {
        cout << "X0:display()" << endl;
    }
};

class X1 : public X0
{
    private:
    public:
    void display()  //继承父类X0,同名函数也会变成虚函数
    {
        cout << "X1:display()" << endl;
    }
};

class Z1 : public X1
{
    private:
    public:
    void display()
    {
        cout << "Z1:display()" << endl;
    }
};

其中我们可以看到基类中的display函数就是一个虚函数,X1继承X0,Z1继承X1,因为虚函数可以继承当派生类中有与虚函数同名的函数也自动变成虚函数。

cpp 复制代码
X0 x0;
    X0 *p2;
    p2 = &x0;
    p2->display();
    X1 x1;
    p2 = &x1;
    p2->display();
    Z1 z1;
    p2 = &z1;
    p2->display();
    //当出现同名函数,且父类声明为虚函数时,子类继承父类后子类的同名函数也会变成虚函数
    //当父类指针赋值为子类对象时,通过该指针调用同名函数,子类的函数会覆盖父类的函数,然后最后显示的时子类的函数

当我们将派生类对象赋值给基类指针再用该指针来调用其中的同名函数时,调用的就是派生类中的同名函数因为当该父类的同名函数时虚函数时子类的函数会覆盖该函数,对此我们可以看看下面的内存图来更好的理解。

虽然我们的指针依旧时指向那块空间但是那块空间里面的虚函已经被子类同名函数所覆盖,所以用基类指针调用的就是派生类的同名函数。有了虚函数后我们就可以实现动态绑定,即派生类会覆盖基类的相关功能程序。但是虚函数有一定的限制:只有类的成员函数才能成为虚函数,静态成员函数不能成为虚函数,内联函数不能是虚函数,构造函数不能是虚函数。但是我们的析构函数一般是虚函数。

了解了虚函数的概念下面我们来看看虚继承的概念:

cpp 复制代码
#include <iostream>
using namespace std;

class B0
{
    private:
    public:
    B0()
    {
        cout << "B0()" << endl;
    }
    void display()
    {
        cout << "B0:display()" << endl;
    }
};

class B1 : virtual public B0               //虚继承
{
    private:
    public:
    B1()
    {
        cout << "B1()" << endl;
    }
};

class B2 : virtual public B0
{
    private:
    public:
    B2()
    {
        cout << "B2()" << endl;
    }
};

class D0 : public B1, public B2   //多继承
{
    private:
    public:
    D0()
    {
        cout << "D0()" << endl;
    }
};





int main(int argc, char const *argv[])
{
    D0 d0;
    d0.display();   //当B1和B2继承于B0时,B1和B2中继承了display函数
                    //然后当D0多继承于B1和B2时,再用D0创建的对象来调用display函数时就会产生歧义
                    //当我们将B1和B2都虚继承于B0时,B0就只会被创建一次就可以解决歧义的问题了
    return 0;
}

从上面这段函数中我们可以看到B1继承B0,B2继承B0,D0继承B1B2,那么我们在调用基类中的使用d0对象来调用display函数时,就会发生编译错误。因为B1和B2类继承B0所以该类中都会有B0的display函数,在用d0调用时编译器就不知道要调用的时是哪个类中的display函数了出现歧义编译报错,这里我们就可以使用虚继承,是B1和B2都虚继承B0那么在D0调用该函数时基类只会被创建一次所以就不会出现编译错误啦。

下面我们接着讲讲纯虚函数的概念

cpp 复制代码
class Object
{
private:
public:
    virtual void area() = 0; // 该函数为纯虚函数,含有纯虚函数的类叫抽象类,接口类,纯虚类
};

该段程序展现的就是纯虚函数的程序写法,有纯虚函数的类被成为抽象类,接口类,纯虚类等,当抽象类被继承时派生类必须实现该抽象类中的纯虚函数,或者在类中再声明为纯虚函数。

cpp 复制代码
class circle : public Object
{
    private:
    float r;
    public:
    circle(float rr) // 有参构造
    {
        r = rr;
    }
    void area()           //实现了纯虚函数
    {
        cout << "圆的面积为:" << M_PI * r * r << endl;
    }
};

class rect : public Object
{
    private:
    float length;
    float weith;
    public:
    rect(float l, float w) // 有参构造
    {
        length = l;
        weith = w;
    }
    void area()
    {
        cout << "矩形的面积为:" << length * weith << endl;
    }
};


int main(int argc, char const *argv[])
{
    Object *p = new circle(1); //基类指针指向创建的一个派生类对象
    p -> area();               //使用该指针可以直接调用派生类中实现的area函数
    p = new rect(2, 3);
    p -> area();

    return 0;
}

如上当我们在派生类中定义了纯虚函数后,我们可以直接使用基类指针来访问派生类中定义的函数,就好像这只是一个接口派生类给出了接口的具体实现,基类就可以直接通过指针来使用派生类中定义的函数了。

相关推荐
点云SLAM43 分钟前
C++包装器之类型擦除(Type Erasure)包装器详解(4)
c++·算法·c++17·类型擦除·c++高级应用·c++包装器·函数包装
源代码•宸43 分钟前
GoLang写一个火星漫游行动
开发语言·经验分享·后端·golang
csbysj20201 小时前
Redis 配置详解
开发语言
行走在电子领域的工匠1 小时前
台达ST:自定义串行通讯传送与接收指令COMRS程序范例四
开发语言·台达plc·st语言编程
t198751281 小时前
基于因子图与和积算法的MATLAB实现
开发语言·算法·matlab
霸王大陆1 小时前
《零基础学 PHP:从入门到实战》教程-模块四:数组与函数-1
android·开发语言·php
APIshop1 小时前
Java爬虫第三方平台获取1688关键词搜索接口实战教程
java·开发语言·爬虫
请为小H留灯1 小时前
Java快捷健(详细版)
java·开发语言
小年糕是糕手1 小时前
【C++同步练习】C++入门
开发语言·数据结构·c++·算法·pdf·github·排序算法