C++ primer Plus 第十三章:类继承

13.1一个简单的基类

从一个类派生出另一个类的时候,原始类为基类,继承类为派生类。

如 创建一个汽车的类

class car
{
private:
    int mile;//里程
public:
    car(int aa=0):mile(aa){}
    void display(){cout<<mile<<endl;}
};

13.1.1派生一个类

如果我们需要一个电动汽车的类,而电动汽车的类必然包含汽车的里程,所以我们可以通过使用类的继承,直接从汽车类里派生出一个电动汽车的类,电动汽车的类包含汽车类里全部的数据成员,并且可以使用基类的方法。

class Dcar :public car
{
private:
    int maxn;
public:
    Dcar(int aa=0,int bb=0):maxn(bb),car(aa){};
    void display_maxn(){cout<<maxn<<endl;}
};

派生类里包含着基类的全部数据成员,除了这些我们直接定义需要的数据成员即可。

13.1.2构造函数:访问权限的考虑

派生类不能直接访问基类的私有成员,而必须通过基类的方法才可以进行访问

例如 Dcar的构造函数并不能直接设置基类的a成员变量的值,必须要通过基类的构造函数才可以,因此要在Dcar的构造函数的初始化列表中调用car的构造函数。

创建派生类的对象时,首先创建基类的对象,C++使用初始化列表语法来完成这种操作

如果不调用基类的构造函数,程序将使用基类默认的构造函数

有关派生类构造函数的要点如下:

1.首先创建基类对象

2.派生来构造函数应通过成员初始化列表将基类信息传递给基类的构造函数

3.派生类构造函数应初始化派生类新增数据成员

这里提一下,释放对象的顺序与创建对象的顺序相反,先执行派生类的析构函数,在执行基类的析构函数(跟栈有关)

13.1.3使用派生类

要使用派生类,程序必须能够访问基类声明,所以应该将这两种类的声明放在同一个头文件中,也可以不这样做。

13.1.4派生类和基类之间的特殊关系

1.派生类对象可以使用基类的方法,条件是方法不是私有的

2.基类指针可以在不进行显示类型转换的情况下指向派生类对象,基类引用可以在不进行显示类型转换的情况下引用派生类对象

Dcar dcar(1,2);
    car *p=&dcar;
    car &q=dcar;

但是基类的指针或引用只能调用基类的方法,不能使用派生类的新增方法。

3.可以隐式的将一个派生类的对象赋给基类的对象(实际上调用了默认的复制构造函数,因为基类引用可以引用派生类)

Dcar dcar(1,2);
car car1=dcar;
car1.display_mile();

car没有diaplay_maxn的方法,所以无法调用。也就是说复制构造函数只会复制基类拥有的部分

13.2继承:is——a关系

C++有三种继承方式:公有继承,私有继承,保护继承

公有继承建立的是一种is-a关系,就是派生类也是基类的一个对象,如有一个水果类,水果类可以派生出香蕉类、苹果类、葡萄类。

公有继承不能建立has-a关系,如午餐可能包括水果,但午餐通常不是水果,不能从水果类里派生出午餐类。我们可以将水果类作为成员放在午餐类里来实现这个操作。

公有继承不能建立is-like-a关系,例如 人们常说律师像鲨鱼,但是律师并不是鲨鱼,所以鲨鱼类不能派生出律师类

公有继承不能建立 is-implemented-as-a(作为……来实现)关系,例如数组可以实现栈,但栈不是数组,因此用数组类派生栈是不合适的。

公有继承不能建立uses-a关系,计算机使用打印机,从计算机类派生出打印机类是不合适的。

13.3多态公有继承

在使用派生类的时候可能会发生这种情况,即希望一个方法在派生类和基类中的行为是不同的,换句话说,方法的行为应取决于调用该方法的对象。这种行为叫多态——多种形态

如果实现这种操作呢?

使用virtual关键字使方法变为虚方法

这样程序将根据对象类型而不是引用或指针的类型来选择方法的版本

virtual可以用在一般的成员函数和析构函数上,构造函数不能使用virtual

如果不使用virtual

#include <iostream>

using namespace std;
class car
{
private:
    int mile;//里程
public:
    car(int aa=0):mile(aa){}
    void display(){cout<<mile<<endl;}
};
class Dcar :public car
{
private:
    int maxn;
public:
    Dcar(int aa=0,int bb=0):maxn(bb),car(aa){};
    void display(){cout<<maxn<<endl;}
};
int main()
{
    Dcar a(1,2);
    car *p=&a;
    p->display();
    return 0;
}

p->display()将使用基类的方法种的display,因为p的类型使指向基类的指针

如果使用virtual

#include <iostream>

using namespace std;
class car
{
private:
    int mile;//里程
public:
    car(int aa=0):mile(aa){}
    virtual void display(){cout<<mile<<endl;}//虚方法
};
class Dcar :public car
{
private:
    int maxn;
public:
    Dcar(int aa=0,int bb=0):maxn(bb),car(aa){};//虚方法
    virtual void display(){cout<<maxn<<endl;}
};
int main()
{
    Dcar a(1,2);
    car *p=&a;
    p->display();
    return 0;
}

程序将输出2,即虽然p是基类类型的指针,但却执行了派生类中的display方法

虚函数的其他用法见虚函数常见用法

如果想在派生类里使用基类的方法,需要加上作用域解析符和基类名才可以使用。

为什么要使用虚函数呢?举个例子,是因为如果基类和派生类中的dispaly成员函数名相同,而我们想通过一个函数来统一进行这个操作,如void dispaly(指针),那么这时候,指针是传谁的呢,前面我们可以知道基类的指针可以指向派生类的对象,于是这里指针类型肯定是基类指针型,但是有个问题是你用一个指向派生类对象的基类指针去执行display操作时,它会调用基类的display函数而不是派生类的。而当我们使用虚函数就不会这样了,程序会根据指针指向对象的类型去执行相应的display函数。

13.4静态联编和动态联编

将源代码中函数调用解释为执行特定函数代码块被称为函数名联编。C语言中,这非常简单,因为每个函数名对应一个不同的函数,在C++中,由于函数重载,编译器必须查看函数的参数以及参数名才能确定使用哪个函数,以上操作均可在编译过程中实现,于是被称为静态联编。

而虚函数的存在使编译器只能在程序运行时选择正确的虚函数的代码,这被称为动态联编

13.4.1指针和引用类型的兼容性

将派生类引用或指针转换为基类引用或指针被称为向上强制转换,相反,将基类的引用或指针转换为派生类的引用或指针被称为向下强制转换

如果不使用显示类型转换,向下强制转换是不允许的

举个例子

Dcar a(1,2);
car *p=&a;
car &q=a;
//上面这两个是向上强制转换,可以不进行显示类型转换
car b(1);
Dcar *p=(Dcar *)b;
//这yi个是向下强制转换,必须进行显示类型转换

13.4.2虚成员函数和动态联编

编译器默认使用静态联编,原因是可以提高效率,而且大部分基类的成员函数不需要在派生类中重新定义,所以默认是静态联编。

虚函数的工作原理:

对每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针。这种数组被称为虚函数表,虚函数表中存储了为类对象进行声明的虚函数地址。

我们可以通过一个图来清晰的看出虚函数的工作原理

 

13.4.3有关虚函数注意事项

在基类方法的声明中使用关键字virtual可以使该方法在基类以及所有派生类中式虚的(但是为了提高可读性,只要一个函数是虚的,就要在所有派生类中同名函数前均加上virtual)

构造函数不能作为虚函数

因为没有太大意义

析构函数应当是虚函数,除非类不作为基类

如果不使用虚析构函数,基类指针指向了派生类,那么基类指针调用析构函数,结果只会释放掉基类中数据成员的内存

友元不能是虚函数,因为友元不是类成员

返回类型协变

在C++中,只要原来的返回类型是指向基类的指针或引用,新的返回类型是指向派生类的指针或引用,覆盖的方法就可以改变返回类型。这样的类型称为协变返回类型

覆盖要求函数具有完全相同的入参。

一般覆盖具有相同的返回值,否则会提示错误

class A
{

public:
    virtual double display()
    {
        return 1.0;
    }
};
class B:public A
{
public:
    virtual int display()
    {
        return 2;
    }
};

上面这个例子就会报错

这个规则对返回类型协变而言,则有所放松。覆盖的返回值不区分基类或派生类。从语意上理解,一个派生类也是一个基类。如下:

class A
{
  int a;
public:
    A(int aa):a(aa){}
    virtual A& display()//返回基类的引用
    {
        A a(1);
        return a;
    }
};
class B:public A
{
    int b;
public:
    B(int aa,int bb):A(aa),b(bb){}
    virtual B& display()//返回派生类引用
    {
        B a(1,2);
        return a;
    }
};

注意 不能返回对象的值,能这样做的原因是基类的引用以引用派生类

13.5 访问控制

关键字protected

protected与private相似,在类外只有公有类成员才可以访问protected成员。

两者之间的区别只有在考虑到类的继承时才会体现出来,派生类的成员可以直接访问基类的保护成员,对于基类的保护成员,派生类将其看作是自己的私有成员

C++这样做的原因是提高效率。前面讲过执行一个函数会用到入栈 出栈等操作,如果我们每次取基类中的私有成员变量都是通过基类的公有成员函数取势必会造成浪费,所以就有了保护成员,用于派生类直接访问基类的私有成员

13.6抽象基类

抽象基类是解决如下问题:
加入现在需要开发一个图形类,表示圆与椭圆(注意,圆是一种特殊的椭圆)。所以初步考虑从椭圆类中派生出圆类。但是现在遇到一个问题,圆与椭圆的面积计算公式不同,所以需要建立一个ABC,抽象出圆与椭圆的共性,圆类、椭圆类都继承ABC。
圆类与椭圆类相异的方法(求面积)需要在ABC中声明成纯虚函数。


下面为纯虚函数的声明方式:

virtual double Area() const = 0;
  • ABC至少要包含一个纯虚函数;
  • 一旦类包含纯虚函数,将不能创建该类的对象;
  • 被声明成纯虚函数的方法,在ABC中只能声明,不能被定义;

派生类中的Area函数仍可以正常的当作虚函数使用,不会产生任何影响。

13.7继承和动态内存分配

前面我们知道当类中使用了new时,我们需要给它手动定义复制构造函数和重载赋值运算符。

在类的继承中,派生类和基类中其中有一个用来new时,我们是否要都要给他们复制构造函数和重载赋值运算符呢?

下面请看这两种情况

1.派生类不使用new 而基类使用了new

首先我们需要给基类手动定义复制构造函数和重载赋值运算符,析构函数加上delete[]

派生类是否需要手动复制构造函数和重载赋值运算符,析构函数加上delete[]呢?

首先,析构函数不用,派生类的析构函数执行完后会自动调用基类的析构函数,所以派生类使用默认析构函数即可

其次,复制构造函数只需要保持默认,在初始化列表上调用基类的复制构造函数即可

再者,类的默认赋值运算符将自动使用基类的赋值运算符对基类的组件进行赋值,所以不需要考虑派生类的赋值运算符

举个例子

#include <iostream>
#include<string.h>
#include<cstdio>
using namespace std;

//基类
class A
{
    char *p;
    int len;
public:
    A(const char *a)
    {
        len=strlen(a);
        p= new char[len+1];
    }
    A(const A &q)//复制构造函数
    {
        len=q.len;
        delete[] p;
        p=new char[len+1];
        strcpy(p,q.p);
        cout<<"基类复制构造函数调用"<<endl;
    }
    A& operator =(const A &q)//重载 = 运算符
    {
        if(this==&q)
        {
            return *this;
        }
        delete [] p;
        len=q.len;
        p=new char[len+1];
        strcpy(p,q.p);
        cout<<"基类重载=使用"<<endl;
        return *this;
    }
    char *getid()//返回p指针
    {
        return p;
    }
    ~A()
    {
        delete [] p;
    }
};

//派生类
class B:public A
{
private:
    int x;
public:
    B(const char *a,int nx):A(a),x(nx){}
    B(const B& q):A(q)
    {
        x=q.x;
    }
};

int main()
{
    B  fun1("123",1);
    B fun2("234",2);
    fun2 = fun1;
    B fun3 = fun2;
    return 0;
}

运行结果

2.派生类使用new 而基类没有使用new

#include <iostream>
#include<string.h>
#include<cstdio>
using namespace std;
class B
{
private:
    int x;
public:
    B(int nx):x(nx) {}
    B(const B &q)
    {
        cout<<"基类复制构造函数被调用"<<endl;
    }
};


class A:public B
{
    char *p;
    int len;
public:
    A(const char *a,int nx):B(nx)
    {
        len=strlen(a);
        p= new char[len+1];
    }
    A(const A &q):B(q)//复制构造函数
    {
        len=q.len;
        delete[] p;
        p=new char[len+1];
        strcpy(p,q.p);
        cout<<"派生类复制构造函数调用"<<endl;
    }
    A& operator =(const A &q)//重载 = 运算符
    {
        if(this==&q)
        {
            return *this;
        }
        delete [] p;
        len=q.len;
        p=new char[len+1];
        strcpy(p,q.p);
        cout<<"派生类重载=使用"<<endl;
        return *this;
    }
    char *getid()//返回p指针
    {
        return p;
    }
    ~A()
    {
        delete [] p;
    }
};

int main()
{
    A x("123",3);
    A y=x;
    return 0;
}

这里注意一个细节,在派生类的复制构造函数的初始化列表中要调用基类的复制构造函数,否则会报错

3.派生类使用new 基类使用new

注意一点,类的默认赋值运算符将自动使用基类的赋值运算符对基类的组件进行赋值,而此时基类重载了= ,就不是能自动调用了,所以要在派生类的重载=里显式调用基类的重载=的函数

#include <iostream>
#include<string.h>
#include<cstdio>
using namespace std;
class B
{
private:
    char *x;
    int len1;
public:
    B(const char *a)
    {
        len1=strlen(a);
        x= new char[len1+1];
    }
    B(const B &q)
    {
        len1=q.len1;
        delete[] x;
        x=new char[len1+1];
        strcpy(x,q.x);
        cout<<"基类复制构造函数被调用"<<endl;
    }
    B&operator =(const B &q)
    {
        cout<<"基类重载="<<endl;
        if(this==&q)
        {
            return *this;
        }
        delete [] x;
        len1=q.len1;
        x=new char[len1+1];
        strcpy(x,q.x);
        return *this;
    }
};


class A:public B
{
    char *p;
    int len;
public:
    A(const char *a,const char *b):B(b)
    {
        len=strlen(a);
        p= new char[len+1];
    }
    A(const A &q):B(q)//复制构造函数
    {
        len=q.len;
        delete[] p;
        p=new char[len+1];
        strcpy(p,q.p);
        cout<<"派生类复制构造函数调用"<<endl;
    }
    A& operator =(const A &q)//重载 = 运算符
    {
        cout<<"派生类重载=使用"<<endl;
        B::operator=(q);//调用基类重载=函数
        if(this==&q)
        {
            return *this;
        }
        delete [] p;
        len=q.len;
        p=new char[len+1];
        strcpy(p,q.p);
        return *this;
    }
    char *getid()//返回p指针
    {
        return p;
    }
    ~A()
    {
        delete [] p;
    }
};

int main()
{
    A x("123","456");
    A y=x;
    A z("123","789");
    z=y;
    return 0;
}

运行结果

发布了37 篇原创文章 · 获赞 3 · 访问量 2369

猜你喜欢

转载自blog.csdn.net/Stillboring/article/details/105365000