C++面向对象总结——继承

引言

类是对现实中事物的抽象,类的继承和派生的层次结构则是对自然界中事物分类、分析的过程在程序设计中的体现。


一,继承的概念及语法

继承是类与类之间的关系,是一个很简单很直观的概念,与现实世界中的继承类似,例如儿子继承父亲的财产。
继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。被继承的类称为父类或基类,继承的类称为子类或派生类。
派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能。
以下是两种典型的使用继承的场景:

  • 当你创建的新类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承,这样不但会减少代码量,而且新类会拥有基类的所有功能。
  • 当你需要创建多个类,它们拥有很多相似的成员变量或成员函数时,也可以使用继承。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承,既可以节省代码,也方便后续修改成员
// 基类
class Animal {
    // eat() 函数
    // sleep() 函数
};


//派生类
class Dog : public Animal {
    // bark() 函数
};

💙继承权限和继承方式

C++继承的一般语法为:

class 派生类名:[继承方式] 基类名{
    派生类新增加的成员
};

继承方式限定了基类成员在派生类中的访问权限,包括 public(公有的)、private(私有的)和 protected(受保护的)。此项是可选项,如果不写,默认为 private(成员变量和成员函数默认也是 private)。
现在我们知道,public、protected、private 三个关键字除了可以修饰类的成员,还可以指定继承方式。

不同的继承方式会影响基类成员在派生类中的访问权限。

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。

总的来说:继承方式代表的是父类属性在派生类的最低呈现。

 派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。

 我们可以根据访问权限总结出不同的访问类型,如下所示(访问基类的成员):

 一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

二,派生类的构造函数

上面我们说基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,基类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数。

在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。

这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数

下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:

#include<iostream>
using namespace std;

class shape//定义形状基类
{
public://公有成员函数
    //初始化基类参数列表
    shape(float w,float h):width(w),height(h){}
protected://保护成员变量
    float width;
    float height;
};

class cuboid :public shape//定义长方体派生类
{
public :
    //派生类构造函数指明基类构造函数shape
     cuboid(float w,float l,float h):shape(w,h),length(l){}
     float getarea()
    {
        return width * height*length;
    }
private:
    float length;
};

int main()
{
    cuboid Cuboid(20,30,40);    //实例化cuboid类的对象
    float area = Cuboid.getarea();
    cout << "面积:" << area << endl;
    return 0;
}

运行结果为:

需要注意的是:

cuboid(float w,float l,float h):shape(w,h),length(l){}

shape(w,h)就是基类的构造函数,并将w和h作为实参传递给它,length(l)是派生类的参数初始化列表,用逗号隔开。

也可以将基类构造函数的调用放在参数初始化表后面:

cuboid(float w,float l,float h):length(l),shape(w,h){}

但是不管它们的顺序如何,派生类构造函数总是先调用基类构造函数再执行其他代码(包括参数初始化表以及函数体中的代码)。

 另外,函数头部是对基类构造函数的调用,而不是声明,所以括号里的参数是实参,它们不但可以是派生类构造函数参数列表中的参数,还可以是局部变量、常量等,例如:

cuboid(float w,float l,float h):shape(20,10),length(l){}

💙构造函数的调用顺序

从上面的分析中可以看出,基类构造函数总是被优先调用,这说明创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数,如果继承关系有好几层的话,例如:

A --> B --> C

那么创建 C 类对象时构造函数的执行顺序为:

A类构造函数 --> B类构造函数 --> C类构造函数

还有一点要注意:派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的

🧡基类构造函数调用规则

事实上,通过派生类创建对象时必须要调用基类的构造函数,这是语法规定。换句话说,定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。请看下面的例子:

#include<iostream>
using namespace std;

class shape//定义形状基类
{
public://公有成员函数
    //初始化基类参数列表
    shape(float w,float h):width(w),height(h){}

    //默认构造函数
    shape():width(20),height(30){}
protected://保护成员变量
    float width;
    float height;
};

class cuboid :public shape//定义长方体派生类
{
public :
    //初始化派生类参数列表
cuboid(float w,float l,float h):shape(w,h),length(l){}
   //派生类默认构造函数
cuboid() :length(50){}
//
     float getarea()
    {
        return width * height*length;
    }
private:
    float length;
};

int main()
{
    cuboid Cuboid1(20,30,40);    //实例化cuboid类的对象
    cuboid Cuboid2;
    float area1 = Cuboid1.getarea();
    float area2 = Cuboid2.getarea();
    cout << "Cuboid1的面积:" << area1 << endl;
    cout << "Cuboid2的面积:" << area2 << endl;

    return 0;
}

创建对象 Cuboid1时,执行派生类的构造函数 shape(float w,float h):width(w),height(h){},它指明了基类的构造函数。

创建对象 Cuboid2时,执行派生类的构造函数cuboid():length(50){}它并没有指明要调用基类的哪一个构造函数,从运行结果可以很明显地看出来,系统默认调用了不带参数的构造函数,也就是shape(){}

如果将基类 shape中不带参数的构造函数删除,那么会发生编译错误,因为创建对象 Cuboid2时需要调用 shape类的默认构造函数, 而 shape类中已经显式定义了构造函数,编译器不会再生成默认的构造函数。

三,派生类的析构函数

和构造函数类似,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。
另外析构函数的执行顺序和构造函数的执行顺序也刚好相反:

  • 创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
  • 而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。

请看下面的例子:

#include<iostream>
using namespace std;

class A
{
public:
    A() { cout << "A create" << endl; }
   ~A() { cout << "A destoryed" << endl; }
};
class B :public A
{
public:
    B() { cout << "B create" << endl; }
   ~B() { cout << "B destoryed" << endl; }
};
class  C :public B
{
public:
    C() { cout << "C create" << endl; }
   ~C() { cout << "C destoryed" << endl; }
};
int main()
{
    C test;
    return 0;
}

 四,向上转型(将派生类赋值给基类)

在 C/C++ 中经常会发生数据类型的转换,例如将 int 类型的数据赋值给 float 类型的变量时,编译器会先把 int 类型的数据转换为 float 类型再赋值;反过来,float 类型的数据在经过类型转换后也可以赋值给 int 类型的变量。

数据类型转换的前提是,编译器知道如何对数据进行取舍。例如:

int a = 10.9;
cout << a << endl;

输出结果为 10,编译器会将小数部分直接丢掉(不是四舍五入)。如果是int 转float类型,编译器会自动添加小数(用0添加)。

类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)。相应地,将基类赋值给派生类称为向下转型(Downcasting)。

1️⃣将派生类对象赋值给基类对象

#include<iostream>
using namespace std;
class A
{
public:
    A(int a):m_a(a){}
    void Cout()
    {
        cout << "Class A: m_a=" << m_a << endl;
    }
public:
    int m_a;
};
class B:public A
{
public:
    B(int a,int b):A(a), m_b(b){}
    void Cout()
    {
        cout << "Class B: m_a=" << m_a << ", m_b=" << m_b << endl;
    }
private:
    int m_b;
};

int main()
{
    A aa(2);
    B bb(3, 4);
    aa.Cout();
    bb.Cout();
    aa = bb;
    aa.Cout();
    bb.Cout();
}

本例中 A 是基类, B 是派生类,aa、bb 分别是它们的对象,由于派生类 B 包含了从基类 A 继承来的成员,因此可以将派生类对象 bb 赋值给基类对象 aa。通过运行结果也可以发现,赋值后 aa 所包含的成员变量的值已经发生了变化。

赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。对象之间的赋值不会影响成员函数,也不会影响 this 指针。
将派生类对象赋值给基类对象时,会舍弃派生类新增的成员,也就是“大材小用”,如下图所示:

  可以发现,即使将派生类对象赋值给基类对象,基类对象也不会包含派生类的成员,所以依然不同通过基类对象来访问派生类的成员。

2️⃣将派生类指针赋值给基类指针

除了可以将派生类对象赋值给基类对象(对象变量之间的赋值),还可以将派生类指针赋值给基类指针(对象指针之间的赋值)。我们先来看一个多继承的例子,继承关系为:

 

#include <iostream>
using namespace std;
//基类A
class A{
public:
    A(int a);
public:
    void display();
protected:
    int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
    cout<<"Class A: m_a="<<m_a<<endl;
}
//中间派生类B
class B: public A{
public:
    B(int a, int b);
public:
    void display();
protected:
    int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
    cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
}
//基类C
class C{
public:
    C(int c);
public:
    void display();
protected:
    int m_c;
};
C::C(int c): m_c(c){ }
void C::display(){
    cout<<"Class C: m_c="<<m_c<<endl;
}
//最终派生类D
class D: public B, public C{
public:
    D(int a, int b, int c, int d);
public:
    void display();
private:
    int m_d;
};
D::D(int a, int b, int c, int d): B(a, b), C(c), m_d(d){ }
void D::display(){
    cout<<"Class D: m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}
int main(){
    A *pa = new A(1);
    B *pb = new B(2, 20);
    C *pc = new C(3);
    D *pd = new D(4, 40, 400, 4000);
    pa = pd;
    pa -> display();
    pb = pd;
    pb -> display();
    pc = pd;
    pc -> display();
    cout<<"-----------------------"<<endl;
    cout<<"pa="<<pa<<endl;
    cout<<"pb="<<pb<<endl;
    cout<<"pc="<<pc<<endl;
    cout<<"pd="<<pd<<endl;
    return 0;
}

运行结果:

Class A: m_a=4
Class B: m_a=4, m_b=40
Class C: m_c=400
-----------------------
pa=0x9b17f8
pb=0x9b17f8
pc=0x9b1800
pd=0x9b17f8

本例中定义了多个对象指针,并尝试将派生类指针赋值给基类指针。与对象变量之间的赋值不同的是,对象指针之间的赋值并没有拷贝对象的成员,也没有修改对象本身的数据,仅仅是改变了指针的指向。

我们将派生类指针 pd 赋值给了基类指针 pa,从运行结果可以看出,调用 display() 函数时虽然使用了派生类的成员变量,但是 display() 函数本身却是基类的。也就是说,将派生类指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,但不能使用派生类的成员函数。pa 本来是基类 A 的指针,现在指向了派生类 D 的对象,这使得隐式指针 this 发生了变化,也指向了 D 类的对象,所以最终在 display() 内部使用的是 D 类对象的成员变量,相信这一点不难理解。

概括起来说就是:编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据;编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的函数。

3️⃣将派生类引用赋值给基类引用

 引用在本质上是通过指针的方式实现的,既然基类的指针可以指向派生类的对象,那么我们就有理由推断:基类的引用也可以指向派生类的对象,并且它的表现和指针是类似的。

 修改上例中 main() 函数内部的代码,用引用取代指针:

int main(){
    D d(4, 40, 400, 4000);
   
    A &ra = d;
    B &rb = d;
    C &rc = d;
   
    ra.display();
    rb.display();
    rc.display();
    return 0;
}

运行结果:

Class A: m_a=4
Class B: m_a=4, m_b=40
Class C: m_c=400

ra、rb、rc 是基类的引用,它们都引用了派生类对象 d,并调用了 display() 函数,从运行结果可以发现,虽然使用了派生类对象的成员变量,但是却没有使用派生类的成员函数,这和指针的表现是一样的。

 小结:

向上转型后通过基类的对象、指针、引用只能访问从基类继承过去的成员(包括成员变量和成员函数),不能访问派生类新增的成员。

 

posted @ 2021-07-29 14:39  唯有自己强大  阅读(405)  评论(0编辑  收藏  举报