C++第七弹---继承

继承

  1. 继承的相关概念
    继承是一个进程,通过继承,一个对象可以获得另一个对象的属性(包括函数),并可向其中加入属于自己的一些特征。作为C++语言的一种重要机制,用继承的方法可以自动为一个类提供来自另一个类的操作和数据结构,进而使程序设计人员在一个一般的类的基础上很快建立一个新的类,而不必从零开始设计每个类。

    当一个类被其他的类继承时,被继承的类称为基类(可不是鸡肋_),又称为父类。继承其他类属性的类称为派生类,又称为子类。

    一般情况下,继承的进程起源于一个基类的定义,基类定义了其所有派生类的公有属性。从本质上讲,基类具有同一类集合中的公共属性,派生类继承了这些属性,并且增加了自己特有的属性。从任何已存在的类继承的实质就是建造新的派生类。

    实例:假设有一个基类 Shape,Rectangle 是它的派生类

#include <iostream>
using namespace std;
 
// 基类
class Shape 
{
   public:
      void setWidth(int w)
      {
         width = w;
      }
      void setHeight(int h)
      {
         height = h;
      }
   protected:
      int width;
      int height;
};
 
// 派生类
class Rectangle: public Shape
{
   public:
      int getArea()
      { 
         return (width * height); 
      }
};
 
int main(void)
{
   Rectangle Rect;
 
   Rect.setWidth(5);
   Rect.setHeight(7);
 
   // 输出对象的面积
   cout << "Total area: " << Rect.getArea() << endl;
 
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Total area: 35

  1. 继承与组合
    组合类:一个类里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员。

    创建组合类的对象:首先创建各个内嵌对象,难点在于构造函数的设计。创建对象时既要对基本类型的成员进行初始化,又要对内嵌对象初始化。
    示例:

#include <iostream>
#include <cmath>
using namespace std;
 
class Point{
    private:
        int x, y;
    
    public:
        Point(int a = 0, int b = 0)
        {
            x = a; y = b;
            cout << "Point construction: " << x << ", "<< y << endl;
        }
        Point(Point &p) // copy constructor,其实数据成员是int类型,默认也是一样的 
        {
            x = p.x;
            y = p.y;
            cout << "Point copy construction: " << x << ", "<< y << endl; 
        }
        int getX()
        {
            return x;
        }
        int getY()
        {
            return y;
        }
}; 
 
class Line{
    private:
        Point start, end;
    
    public:
        Line(Point pstart, Point pend):start(pstart), end(pend) // 组合类的构造函数对内前对象成员的初始化必须采用初始化列表形式
        {
            cout << "Line constructior " << endl;
        }
        float getDistance()
        {
            double x = double(end.getX()-start.getX());
            double y = double(end.getY()-start.getY());
            return (float)(sqrt)(x*x+y*y);
        }
};
 
int main()
{
    Point p1(10,20),p2(100,200);
    Line line(p1,p2);
    cout << "The distance is: "<<line.getDistance()<<endl; return 0;
}

创建组合类的对象,构造函数的执行顺序:先调用内嵌对象的构造函数,然后按照内前对象成员在组合类中的定义顺序,与组合类构造函数的初始化列表顺序无关。然后执行组合类构造函数的函数体。析构函数调用顺序相反。

上例中当创建Point类对象p1, p2时调用Point类构造函数2次;当创建组合类Line对象line时,调用了组合类Line的构造函数(首先是参数:需要实参传递给形参,需要调用Point类的拷贝构造函数2次,注意参数传递顺序是从右到左;然后是初始化列表:再调用Point类的拷贝构造函数2次,分别完成内前对象成员start, end的初始化);然后才开始执行Line类的构造函数的函数体。

输出:

Point construction: 10, 20
Point construction: 100, 200
Point copy construction: 100, 200
Point copy construction: 10, 20
Point copy construction: 10, 20
Point copy construction: 100, 200
Line constructior
The distance is: 201.246

  1. 类的分解及抽象类
    为了不模糊概念在这里我们就简单的阐述一下类的分解,前面的教程我们着重讲述了类的继承,继承的特点就是,派生类继承基类的特性,进行结构扩张,这种逐步扩张,逐步在各派生类中分解彼此不同特性的过程其实就是类的分解。

    拿前面交通工具类的程序进行思考,由交通工具派生出来的汽车类,飞机类,是具备更具体特性的描述的类,而对于交通工具这一个基类来说,它的特性是模糊的,广泛的,如果建立一个交通工具类的对象并没有实际意义,为了对这种没有必要能够建立对象的类进行约束,c++引入了抽象类的特性,抽象类的约束控制来源于纯虚函数的定义。

    生命一个类的成员函数为纯虚函数的意义在于让c++知道该函数并无意义,它的作用只是为派生类进行虚函数重载保留位置。

纯虚函数的定义方法就是在在普通的虚函数后面加上"=0",类中一旦有纯虚函数的定义那么这个类就再也不能创建此类的对象了,我们把这种类叫做抽象类。

抽象类的示例代码如下:

#include <iostream> 
using namespace std; 

class Vehicle 
{ 
  public: 
    Vehicle(float speed,int total) 
    { 
      Vehicle::speed = speed; 
      Vehicle::total = total; 
    } 
    virtual void ShowMember()=0;//纯虚函数的定义 
  protected: 
    float speed; 
    int total; 
}; 
class Car:public Vehicle 
{ 
  public: 
    Car(int aird,float speed,int total):Vehicle(speed,total) 
    { 
      Car::aird = aird; 
    } 
    virtual void ShowMember()//派生类成员函数重载 
    { 
      cout<<speed<<"|"<<total<<"|"<<aird<<endl; 
    } 
  protected: 
    int aird; 
}; 

int main() 
{ 
  //Vehicle a(100,4);//错误,抽象类不能创建对象 
  Car b(250,150,4); 
  b.ShowMember(); 
  system("pause"); //等待用户输入任意键继续
} 

多重继承

在实际中,一个派生类可以派生出两个或者多个基类,派生类从两个或者多个基类中继承所需要的属性,在这种情况下,C++允许一个派生类同时继承多个基类。这称为多重继承。

多重继承的声明方法:

class D:public A,private:B,protected C
{
  类D新增的成员
}

D是多重继承的派生类,它以公有继承的方式继承A类,以私有继承的方式继承B类,以保护继承的方式继承C类。按照不同的方式继承A,B,C属性,确定各基类的成员在派生中的访问权限。

多重派生的构造函数

派生类构造函数名(总参数列表)基类1构造函数(参数列表),基类2构造函数(参数列表),基类2构造函数(参数列表),基类3构造函数(参数列表)
{
  派生类中新增成员数据初始化语句
}

C++继承具有二义性,如果出现以下几种情况:
1)两个基类中有同名的成员;
2)两个基类和派生类中都有同名成员,修改C类;
3)类A和B是从同一个基类派生的。
这样编译时会出现模糊性,无法确定调用哪个类。

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

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

访问 public protected private
同一类 yes yes yes
派生类 yes yes no
外部的类 yes no no

我们根据不同的继承方式还总结其访问控制的区别,如下所示:

存取继承 public protected private
pubulic pubulic protected private
protected protected protected private
private private private private

一个派生类继承了所有的基类方法,但下列情况除外:基类的构造函数、析构函数和拷贝构造函数、基类的重载运算符、基类的友元函数。

  • 继承类型
    当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。

    我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

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

  • 虚拟继承
    虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如下:

class A

class B1:public virtual A;

class B2:public virtual A;

class D:public B1,public B2;

虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。

虚拟继承经典面试题

第一种情况               
class a                        
{
  virtual void func();
};

class b ::public virtual a
{
  virtual void foo();
};

第二种情况
class a
{
  virtual void func();
};

class b:: public a
{
  virtual void foo();
};

第三种情况
class a 
{
  virtual void func();
  char x;
};

classs b :: public virtual a
{
  virtual void foo();
};

第四种情况
class a
{
  virtual void func();
  char x;
};

class b :: public a
{
  virtual void foo();
}

如果对这四种情况分别求sizeof(a), sizeof(b)。结果是什么样的呢?下面是输出结果:(在vc6.0中运行)
第一种:4,12
第二种:4,4
第三种:8,16
第四种:8,8

因为每个存在虚函数的类都要有一个4字节的指针指向自己的虚函数表,所以每种情况的类a所占的字节数应该是没有什么问题的,那么类b的字节数怎么算呢?看“第一种”和“第三种”情况采用的是虚继承,那么这时候就要有这样的一个指针vptr_b_a,这个指针叫虚类指针,也是四个字节;还要包括类a的字节数,所以类b的字节数就求出来了。而“第二种”和“第四种”情况则不包括vptr_b_a这个指针,这回应该木有问题了吧。

  • 多重继承的构造顺序
    构造对象的规则需要扩展以控制多重继承。构造函数按以下顺序被调用:
    (1)任何虚拟继承的构造函数按照它们被继承的顺序构造;
    (2)任何非虚拟基类的构造函数按照它们被继承的顺序构造;
    (3)任何成员对象的构造函数按照它们声明的顺序调用;
    (4)类自己的构造函数。

多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

下面的实例中,基类 Shape 被派生为两个类,如下所示:

#include <iostream> 
using namespace std;
 
class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};
class Rectangle: public Shape{
   public:
      Rectangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Rectangle class area :" <<endl;
         return (width * height); 
      }
};
class Triangle: public Shape{
   public:
      Triangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Triangle class area :" <<endl;
         return (width * height / 2); 
      }
};
// 程序的主函数
int main( )
{
   Shape *shape;
   Rectangle rec(10,7);
   Triangle  tri(10,5);
 
   // 存储矩形的地址
   shape = &rec;
   // 调用矩形的求面积函数 area
   shape->area();
 
   // 存储三角形的地址
   shape = &tri;
   // 调用三角形的求面积函数 area
   shape->area();
   
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Parent class area
Parent class area

导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。

但现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual,如下所示:

class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      virtual int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};

修改后,当编译和执行前面的实例代码时,它会产生以下结果:

Rectangle class area
Triangle class area

此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。

正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。

注意:
C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数;形成多态必须具备三个条件:

  • 必须存在继承关系;

  • 继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字Virtual声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);

  • 存在基类类型的指针或者引用,通过该指针或引用调用虚函数;

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容

  • 3. 类设计者工具 3.1 拷贝控制 五种函数拷贝构造函数拷贝赋值运算符移动构造函数移动赋值运算符析构函数拷贝和移...
    王侦阅读 1,795评论 0 1
  • C++文件 例:从文件income. in中读入收入直到文件结束,并将收入和税金输出到文件tax. out。 检查...
    SeanC52111阅读 2,764评论 0 3
  • “明明昨天把东西放在这里,怎么找不到了呢?” “房间储物间太小了,完全放不下东西!” “屋子好脏,可是太累,还是改...
    徐徐而闻阅读 2,229评论 2 51
  • 落落西湖十月暮,阴雨寒风不阻步。 曲苑风荷留叶枯,听得微雨笛音妩。 点点池塘圈圈漪,扰碎树影梦中碧。 过冬梧柏半青...
    小木山庄的溜溜阅读 365评论 6 10
  • 一直不知道到底怎样才算一个受欢迎的人,开朗?活泼?淑女?一直努力的想成为他人眼中优秀的自己,慢慢的,慢慢的,走...
    心說阅读 281评论 0 0