C++灵魂所在之---继承篇

在C++的学习过程之中,那么继承与多态这两个区别于C语言的特性你一定要了解,如果想要学好C++,那么继承与多态必须要了解清楚,尤其是多态,但是要了解多态首先你又必须了解继承先,不过即使这两者都十分了解了,也不敢说已经掌握了C++,因为这只不过是C++之中的冰山一角。(有兴趣的可以了解一下网上说的C++的四种境界)

闲话就说到这,开始正式内容了,关于C++之中的继承,我把在继承内容所学到的内容与大家分享分享。如果有什么不对的地方,欢迎大家提出来!

我学习一个内容的时候,总是喜欢从定义入手,然后根据这个定义再去猜想它有什么功能,然后再去验证我的猜想(当然并非每一个问题都这样,一般都是遇到一些比较重要的概念的时候), 来看一下定义:继承(inheritance)是面对象程序使可以用的最重要的,它允许程序员在保持原有类特性的基础上进行扩展,增加加能。这样产生新的类,称为派生类。继承呈现了面对象程序设计的层次结构,体现了由简单到复杂的认知过程。

换种说法就是所谓“继承”就是在一个已存在的类的基础上建立一个新的类。已存在的类称为“基类(base class)”或“父类(father class)”,新建的类称为“派生类(derived class)”或“子类(son
class )”。一个新类从已有的类那里获得其已有特性,这种现象称为类的继承。

来看一张图,你就会了解地更清楚了:

说到了继承,那么就不得不提到派生了,因为这两个概念总是出现在一起,或者说谁也离不开谁,通过继承,一个新建子类从已有的父类那里获得父类的特性。从另一角度说,从已有的类(父类)产生一个新的子类,称为类的派生。类的继承是用已有的类来建立专用类的编程技术。派生类继承了基类的所有数据成员和成员函数,并可以对成员作必要的增加或调整。一个基类可以派生出多个派生类,每一个派生类又可以作为基类再派生出新的派生类,因此基类和派生类是相对而言的。一代一代地派生下去,就形成类的继承层次结构。相当于一个大的家族,有许多分支,所有的子孙后代都继承了祖辈的基本特征,同时又有区别和发展。与之相仿,类的每一次派生,都继承了其基类的基本特征,同时又根据需要调整和扩充原
有的特征。

关于什么是继承这个概念说完之后,那么就开始下一个内容:继承定义格式,同样来看一张图

public、protected、private这三者又称为访问限定符,用来定义继承关系

再来看一下不同的继承方式下的成员变量访问控制关系的不同:

下面用代码来验证一下:

class Base {
public:    
    Base()    
    {
        cout << "B()" << endl;
    }
    ~Base()    
    {
        cout << "B()" << endl;
    }
    void ShowBase()
    {
        cout << "_pri = " << _pri << endl;
        cout << "_pro = " << _pro << endl;
        cout << "_pub = " << _pub << endl;
    }
private:    
    int _pri;
protected:    
    int _pro;
public:    
    int _pub;
};

class Derived :public Base
{
public:    
    Derived()    
    {
        cout << "D()" << endl;
    }
    ~Derived()    
    {
        cout << "D()" << endl;
    }
    void ShowDerived()    
    {
        cout << "_d_pri = " << _d_pri << endl;
        cout << "_d_pro = " << _d_pro << endl;
        cout << "_d_pub = " << _d_pub << endl;
    }
private:    
    int _d_pri;
protected:    
    int _d_pro;
public:
    int _d_pub;
};

首先我们来看一下子类与父类在创建对象的时候有什么关系,首先创建一个基类和一个派生类的对象。

int main()
{
    Base b;
    Derived d;
    return 0;
}

我们会发现运行上面的程序之后,父类调用自己的构造函数没有什么问题,子类也调用了父类的构造函数,这是为什么呢?其实这点要理解也很简单,我们可以将子类的对象中的内容看做成两部分构成,一部分就是它继承父类的内容,还有一部分就是它自己独有的内容,我们都知道在创建派生类对象的时候,它会去调用构造函数,既然它是由两部分构成的,那么自然它需要两个构造函数来共同构造它,用图来解释一下更直观:

在这里有必要给说明一下以下三条小提示:

1、基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表。

2、基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数。

3、基类定义了带有形参表构造函数,派生类就一定定义构造函数

同名隐藏

同样地,还是上面的两个类,我们创建两个对象,一个是父类对象,一个是子类对象,我们可以调用分别里面的

ShowBase()方法以及ShowDerived()方法,这显然没有任何问题,但是如果我们将这两个方法都改成Show()方法,那么这样又会怎么样呢?这就涉及到了同名隐藏的概念!

什么是同名隐藏呢?同名隐藏就是:两个成员函数(包括成员变量)处在不同的作用域之中,但是名字相同(返回值、参数列表可以相同也可以不相同),此时如果你想用派生类的对象去调用基类中的同名方法就无法成功了,不过也有解决的方法,就是在方法前面加上作用域。来看一下在代码:

class Base {
public:
	Base()
	{
		_pri = 0x04;
		_pro = 0x05;
		_pub = 0x06;
		cout << "B()" << endl;
	}
	~Base()
	{
		cout << "~B()" << endl;
	}
	void /*ShowBase()*/Show()
	{
		cout << "_pri = " << _pri << endl;
		cout << "_pro = " << _pro << endl;
		cout << "_pub = " << _pub << endl;
	}
	void Show(int a)
	{
		cout << a << endl;
	}
private:
	int _pri;
protected:
	int _pro;
public:
	int _pub;
};

class Derived :public Base
{
public:
	Derived()
	{
		_d_pri = 0x01;
		_d_pro = 0x02;
		_d_pub = 0x03;
		cout << "D()" << endl;
	}
	~Derived()
	{
		cout << "~D()" << endl;
	}
	void /*ShowDerived()*/ Show()
	{
		cout << "_d_pri = " << _d_pri << endl;
		cout << "_d_pro = " << _d_pro << endl;
		cout << "_d_pub = " << _d_pub << endl;
	}
private:
	int _d_pri;
protected:
	int _d_pro;
public:
	int _d_pub;
};

int main()
{
	Base b;
	Derived d;
	b.Show();
	d.Show();
	d.Show(1);                       //此处编译期间就会报错
	d.Base::Show();                  可以通过这样的方式来访问同名的函数
	return 0;
}

基类与派生类类型上的兼容性

这里无非就是两个是否可以相互转化(这里主要讨论指针),相互赋值的关系,派生类可以给基类赋值(可以这样理解基类需要的内容派生类都有,因为派生类继承了基类中的内容),但是反过来基类不能直接给派生类赋值,需要进行强转(前提是在公有继承下)。还是用上面的类来做一下测试:

int main()
{
	Base * b;
	Derived * d;
	d = new Derived;
	b = new Base;
	b = d;                           //派生类可以给基类赋值
	b->ShowBase();                   //调用函数也不会出问题
	//d = &b;                        //报错 “=”: 无法从“Base **”转换为“Derived *”
	d = (Derived*)&b;                //这样编译没有问题,但是尽量不要这样做,可能会出现无法预计的错误
	d->ShowDerived();                //如果你用上面这样的方式会带来错误,里面打印了随机值
	d->ShowBase();
	return 0;
}

关于这部分内容总结一下就是:

1. 子类对象可以赋值给父类对象(切割/切片)

2. 父类对象不能赋值给子类对象

3. 父类的指针/引用可以指向子类对象

4. 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)

写到这里先来一个小小的总结之后再开始下一部分内容:

1. 基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要 在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

2. public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类 对象也都是一个父类对象。

3. protetced/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分, 是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的 都是公有继承。

4. 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存 在但是在子类中不可见(不能访问)。

5. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最 好显示的写出继承方式。

6. 在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承.

7.友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有 一个static成员实例。(静态成员可以被继承)

还有一个问题就是关于继承关系之中的构造函数、析构函数调用过程的问题,我用衣一幅图来表示一下:

关于菱形继承的问题

在继承这块内容的学习过程之中,一定会遇到一个“诡异”的问题就是关于菱形继承的问题,既然说它“诡异”,那么他到底是如何“诡异”的呢?主要是因为它不常见(我只见到输入、输出流在库函数的实现之中用到了菱形继承,有兴趣的可以去了解一下),正是因为这样,导致它显得有些“诡异”。

还是先来看一张图:

说到菱形继承,就不得不说到虚继承的概念,对于虚继承,就是为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。

class A{}; //基类

class B:public A{};//子类

class C:public A{};

class D:public B,public C();

如上代码中A,B,C,D就构成了一个菱形继承,如果不用虚基类来实现菱形继承就会导致模糊调用的现象,所谓模糊调用就是说在D的内存中会保留两个基类A的对象,如何解决这个问题,利用虚基类就能很好的解决这个问题,即可改为:

class B:virtual public A{};//子类

class C:virtual public A{};

我们可以进一步了解一下,来看一个代码:

class Base
{
public:
	int _pub;
};

class Derived_one :/*virtual*/ public Base
{
public:
	int _por;
};

class Derived_two :/*virtual*/ public Base
{
public:
	int _pri;
};

class Derived : public Derived_one, public Derived_two
{
public:
	int _num;
};

int main()
{
	Derived d;
	cout << sizeof(d) << endl;
	return 0;
}

上述代码在不是虚继承的情况下结果为20,这很容易理解,但是如果改为虚继承,那么结果又发生了变化,结果变成了24,这又是为什么呢?我们都知道,如果不引人虚拟继承的概念,那么上面的代码就会有数据二义性的问题产生(里面有两个来自Base类的数据成员),但是引入虚拟继承之后,这个二义性问题就可以解决了,还是用图来说明吧:

关于菱形继承这一块我讲的不够详细,如果你想深入了解一下:了解更多菱形继承(含有虚函数)

最后总结一下虚继承:

1. 虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。

2. 虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得 已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的 损耗。

时间: 2024-11-02 10:47:14

C++灵魂所在之---继承篇的相关文章

JS对象继承篇

JS对象继承篇 ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的 原型链 其基本思路是利用原型让一个引用类型继承另一个引用类型的属性和方法 function Person(){ this.name = "Person"; } Person.prototype.getName = function(){ return this.name; }; function SuperPerson(name,sex){ this.name = name; this.sex

seo的三大灵魂所在

seo的三大灵魂,我认为是:用户体验.逻辑思维.数据分析.下面seo小鑫给大家介绍一下这三大灵魂分别的含义. 一.用户体验 用户体验就是搜索引擎追求的目标,也是算法中赋予页面和网站权重最主要的依据. 1.优质的内容资源--最有可能满足用户搜索信息的需求. 2.合理的内链--给用户提供尽可能多的用户体验. 3.优质的友链和外链--代表网站得到用户的认可. 4.高点击.低跳出.高滞留--满足用户的需要. 二.逻辑思维 1.能够通过搜索引擎的特性分析出seo优化的构成要素,并区分出主次和优先顺序.(一

8.继承篇

1.概念 继承是指一个对象直接使用另一对象的属性和方法. 2.作用 新类(即派生类)将获取基类的所有非私有数据和行为以及新类为自己定义的所有其他数据或行为.因此,新类具有两个有效类型:新类的类型和它继承的类的类型. 3.扩展 在. NET中,如果创建一个类,则该类总是在继承.这缘于.NET的面向对象特性,所有的类型都最终继承自共同的根System.Object类.可见,继承是.NET运行机制的基础技术之一,一切皆为对象,一切皆于继承.继承具有可扩展性

Java继承篇总结

继承: 1)概念 把多个类中相同的成员给提取出来定义到一个独立的类中.然后让这多个类和该独立的类产生一个关系,这多个类就具备了这些内容.这个关系叫继承. 1.1)定义类时,通过"继承"一个现有的类,子类可以具有父类中的所有属性和方法. 2)Java中如何表示继承呢?格式是什么呢? A:用关键字extends表示 B:格式: class 子类名 extends 父类名 {} 2.1)案例 public class Animal { String name; String color; p

面向对象 (二) 继承篇a

1.继承的概念:什么是继承?继承是子类调用父类的属性和方法.什么是子类于父类?父类:基类 类似一片树林中第一课树,所有的树都是通过它继承的子类:派生类 继承父类的属性与方法.并且拥有自己和父类不同的成员.因此,常说,子类比父类拥有的更多.2.继承需要注意的问题1.父类不能继承子类的成员2.构造方法不能被继承,但是可以在子类中进行base(指定引用)3.子类能继承的有属性.方法(行为)4.0 继承中.私有属性能被继承.但是不能访问.4.关于内存中的情况:占内存创建一个子类对象,构建子类之前,必须创

《JavaScript语言精粹》—— 继承篇

四.继承 (1) 伪类 JavaScript不直接让对象从其他对象那里继承,反而插入了一个多余的间接层:通过构造器函数产生对象. 当一个函数对象被创建时,Function构造器产生的函数对象会运行类似这样的一些代码: this.prototype = { constructor : this}; 新函数对象被赋予一个prototype属性,它的值是一个包含constructor属性且属性值为该新函数的对象.这个prototype对象是存放继承特征的地方.因为JavaScript语言没有提供一种方

Javascript 继承篇

1. Prototype 链(Prototype chaining) Javascript 是一种动态语言,实现一个目标通常有多种方式,继承也不例外.首先我们介绍下实现继承最普遍的方式 :利用 Prototype 链. 这里假设你已经对 prototype 以及 __proto__ 有了一定的了解,否则请先参考 Javascript 之 Prototype prototype 链的示意图: 接下来的例子我们都采用类似示意图中的3层继承结构:最顶层为一个 Sharp 类,它有一个 名为 TwoDS

灵魂拷问第1篇:能不能说一说浏览器缓存?

缓存是性能优化中非常重要的一环,浏览器的缓存机制对开发也是非常重要的知识点.接下来以三个部分来把浏览器的缓存机制说清楚: 强缓存 协商缓存 缓存位置 强缓存 浏览器中的缓存作用分为两种情况,一种是需要发送HTTP请求,一种是不需要发送. 首先是检查强缓存,这个阶段不需要发送HTTP请求. 如何来检查呢?通过相应的字段来进行,但是说起这个字段就有点门道了. 在HTTP/1.0和HTTP/1.1当中,这个字段是不一样的.在早期,也就是HTTP/1.0时期,使用的是Expires,而HTTP/1.1使

灵魂拷问第6篇:谈谈你对重绘和回流的理解

我们首先来回顾一下渲染流水线的流程: 接下来,我们将来以此为依据来介绍重绘和回流,以及让更新视图的另外一种方式——合成. 回流 首先介绍回流.回流也叫重排. 触发条件 简单来说,就是当我们对 DOM 结构的修改引发 DOM 几何尺寸变化的时候,会发生回流的过程. 具体一点,有以下的操作会触发回流: 一个 DOM 元素的几何属性变化,常见的几何属性有width.height.padding.margin.left.top.border 等等, 这个很好理解. 使 DOM 节点发生增减或者移动. 读