浅析C++继承与派生

测试环境:

Target: x86_64-linux-gnu

gcc version 5.3.1 20160413 (Ubuntu 5.3.1-14ubuntu2.1)

定义

要分析继承,首先当然要知道什么是继承:继承是面向对象程旭设计中使代码可以复用的最重要的手段,它允许程序员在原有类特性的基础上进行扩展,增加功能。这样产生的新类,就叫做派生类(子类)。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

继承的格式

class 子类名 :继承权限 基类名

比如下面分别定义了两个类:

class A
{
public:
	int pub;
protected:
	int pro;
private:
	int pri;
};

class B: public A
{
};

如上我们就说类B继承了类A,类B叫做类A的派生类或者子类,A类叫做B类的基类或者父类。

继承关系&访问限定符

之前学习类的成员访问限定符的时候都知道public, protected, private 这三种访问限定符的作用,public修饰的类成员可以在类外被访问,而protected与private则不可以。这三种访问权限又对应这三种继承关系:

继承关系可以影响子类中继承自父类的成员变量的访问权限,还是在上个栗子的基础上,我们定义一个B类对象进行如下操作;

int main()
{
	B b1;
	b1.pub;
	b1.pro;
	b1.pri;

	return 0;
}

编译则会报错:

会提示pro与pri变量访问权限分别为protected和private,我们不能在类外使用它们。类似的,在B中定义这样一个成员函数:

class B: public A
{
	void fun()
	{
		cout<<pub;
		cout<<pro;
		cout<<pri;
	}
};

会报这样的错:

即基类中的私有成员在子类中是不可见的。关于三种继承方式的成员访问权限总结如下表:

总结:

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

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

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

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

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

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

继承关系中构造/析构函数调用顺序

在现有类的基础上添加如下的构造与析构函数:

class A
{
public:
	A()
	{
		cout<<"A()"<<endl;
	}

	~A()
	{
		cout<<"~A()"<<endl;
	}

public:
	int pub;
protected:
	int pro;
private:
	int pri;
};

class B: public A
{
public:

	B()
	{
		cout<<"B()"<<endl;
	}

	~B()
	{
		cout<<"~B()"<<endl;
	}
};

然后,在main函数中定义一个类B的对象:B b; 编译运行,看看输出语句的顺序:

先基类构造,后子类构造;析构的时候先析构子类,后析构基类。依旧和以前一样,先构造的后析构(因为在栈上)。

让我们走进几行代码的反汇编世界:

这是程序现在运行到了b的定义语句。=> 所指,是当前运行的汇编语句。可以看到,第三条汇编语句调用了B类的构造函数。咦?怎么跟我们刚刚看到的顺序不太一样!不急,先往下看。直接 ni 运行到第三条汇编,然后用 si 命令跟进去:

可以看到,程序在正式进入B类的构造函数之前,先调用了A类的构造函数,照这么来看,可以推测出是编译器自动的在B类的构造数的初始化列表位置调用了A类的构造函数。还是让我们把程序看完:

果然,又进入了类A的构造函数。

从A类构造函数出来后,才正式进入类B构造函数。

出main函数作用域时,先调用了B类的构造函数

在B类构造函数的末尾调用了A类构造函数。整个过程与我们看到的输出信息一致。

如果类B中还有一个成员变量是一个类对象,那么构造与析构调用顺序又是哪样?

class T
{

public:
	T(int i = 1)
	{
		cout<<"T()"<<endl;
	}

	~T()
	{
		cout<<"~T()"<<endl;
	}
};

class A
{
public:
	A()
	{
		cout<<"A()"<<endl;
	}

	~A()
	{
		cout<<"~A()"<<endl;
	}

public:
	int pub;
protected:
	int pro;
private:
	int pri;
};

class B: public A
{
public:

	B()
	{
		cout<<"B()"<<endl;
	}

	~B()
	{
		cout<<"~B()"<<endl;
	}
public:
    T t;
};

还是刚刚的main函数,在运行一下程序:

很明显,先调用基类构造,然后是成员对象的构造函数,最后是该类自身的构造函数,析构函数顺序则相反。具体的汇编代码就不演示了。总结一下:

【说明】

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

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

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

继承体系中的作用域

  1. 基类和派生类是不同的作用域
  2. 同名隐藏:子类和父类中有同名成员时,子类成员将屏蔽父类对成员的直接访问。(在子类成员函数中,可以使用 基类::基类成员 访问父类成员)
  3. 在实际中在继承体系里面最好不要定义同名的成员

继承与转换--赋值兼容规则--(前提:public继承)

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

友元与继承

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

class Person
{
    friend void Display(Person &p , Student&s);
protected :
    string _name ;
};

class Student: public Person
{
protected :
    int _stuNum ;
};

void Display(Person &p , Student &s)
{
    cout<<p._name<<endl;
    cout<<s._name<<endl;
    cout<<s._stuNum<<endl;
}
void TestPerson1()
{
    Person p;
    Student s;
    Display (p, s); //error
}

继承与静态成员

基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。

单继承&多继承&菱形继承

【单继承】

一个子类只有一个直接父类时称这个继承关系为单继承。

【多继承】

一个子类有两个或以上直接父类时称这个继承关系为多继承。

【菱形继承】

例:

class Person
{
public :
	string _name ; // 姓名
};
class Student : public Person
{
	protected :
	int _num ; //学号
};
class Teacher : public Person
{
protected :
	int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
	string _majorCourse ; // 主修课程
};
void Test ()
{
	// 显示指定访问哪个父类的成员
	Assistant a ;
	a.Student ::_name = "xxx";
	a.Teacher ::_name = "yyy";
}

看一下菱形继承的构造与析构函数调用顺序:

B类和C类继承A类,D类继承B和C类:

对照对象模型来看会很清楚。

虚继承--解决菱形继承的二义性和数据冗余的问题

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

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

实际存放情况:

两个Address处存放的是地址,这个地址所代表的空间存放了由当前Address这个位置到_name的偏移量。具体情况略显繁琐,不便演示,可以查看反汇编。

再看一下上面的类中虚继承的情况下构造与析构函数调用顺序:B类和C类虚继承A类

对照着对象模型看,只需要调用依次B类构造函数即可。

时间: 2024-08-02 09:48:52

浅析C++继承与派生的相关文章

程序设计实习MOOC / 继承和派生——编程作业 第五周程序填空题1

描述 写一个MyString 类,使得下面程序的输出结果是: 1. abcd-efgh-abcd- 2. abcd- 3. 4. abcd-efgh- 5. efgh- 6. c 7. abcd- 8. ijAl- 9. ijAl-mnop 10. qrst-abcd- 11. abcd-qrst-abcd- uvw xyz about big me take abcd qrst-abcd- 要 求:MyString类必须是从C++的标准类string类派生而来.提示1:如果将程序中所有 "My

C++继承与派生

2017-06-25 23:00:59 c++中的继承和派生是面向对象编程中的一个重要内容,通过继承可以实现代码的复用,同时继承也是实现多态性的基础. 一.c++继承的基本形式 class 派生类名:继承方式 基类名,继承方式 基类名 {}: 继承方式主要有三种,public ,private ,protected. 缺省条件下是private继承,三种中public继承用的最多,不同的继承方式决定了子类中从基类继承过来的成员的访问属性. public继承: 基类的public,protecte

四.OC基础--1.文档安装和方法重载,2.self和super&amp;static,3.继承和派生,4.实例变量修饰符 ,5.私有变量&amp;私有方法,6.description方法

四.OC基础--1.文档安装和方法重载, 1. 在线安装 xcode-> 系统偏好设置->DownLoads->Doucument->下载 2. 离线安装 百度xcode文档 3. 方法重载: 是指在一个类中定义多个同名的方法 在OC中没有重载 2.self和super&static, self和super: 1. self理解: 谁调用当前方法, self就代表谁. 比如: 在对象方法中,self代表的是对象, 因为只有对象才可以调用对象方法 在类方法中, self代表的

模块的封装之C语言类的继承和派生

[交流][微知识]模块的封装(二):C语言的继承和派生 在模块的封装(一):C语言的封装中,我们介绍了如何使用C语言的结构体来实现一个类的封装,并通过掩码结构体的方式实 现了类成员的保护.这一部分,我们将 在此的基础上介绍C语言类的继承和派生.其实继承和派生是一个动作的两种不同角度的表达 .当我们继承了一个基类而创造了一个新类时,派生的概念就诞生了.派生当然是从基类派生的.派生出来的类当然是继承了基类的 东西.继承和派生不是一对好基友,他们根本就是一个动作的两种不同的说法,强调动作的起始点的时候

3.继承与派生

1.类的继承与派生 - 类的继承:从已有类产生新类的过程.原有类称为基类或父类,产生的新类称为派生类或子类. - 派生类语法: class 派生类名:继承方式   基类名1,继承方式 基类名2,... { } - 单继承和多继承:基类个数决定 - 直接基类,间接基类 - 继承方式规定了如何访问从基类继承的成员 - 派生类成员是指除了从基类继承的所有成员之外,新增加的数据和函数成员 - 派生类生成过程:吸收基类成员->改造基类成员->添加新的成员,构造函数和析构函数都不被继承 2.访问控制 -

嵌入式linux C++语言(七)——继承与派生

嵌入式linux C++语言(七)--继承与派生 一.继承 在C++编程中软件可重用性(software reusability)是通过继承(inheritance)机制来实现的.类的继承,是新的类从已有类那里得到已有的特性.或从已有类产生新类的过程就是类的派生.原有的类称为基类或父类,产生的新类称为派生类或子类. 派生类的声明:class 派生类名:[继承方式] 基类名{派生类成员声明:};    一个派生类可以同时有多个基类,这种情况称为多重继承,派生类只有一个基类,称为单继承. 继承方式规

继承与派生,重载&lt;&lt;

///继承与派生 #include <iostream> using namespace std; class Point { public: Point (float a=0,float b=0):x(a),y(b) {} ///有默认参数的构造函数 void setPoint (float,float); ///重新设置坐标 ///读x的坐标,getX为常成员函数,只能访问类的数据,而不能更改 float getX() const { return x; } float getY() co

c++学习--继承与派生

继承和派生 1 含有对象成员(子对象)的派生类的构造函数,定义派生类对象成员时,构造函数的执行顺序如下: 1 调用基类的构造函数,对基类数据成员初始化: 2 调用对象成员的构造函数,对对象成员的数据成员初始化: 3 执行派生类的构造函数体,对派生类数据成员初始化. 代码如下:#include<iostream.h> class base{ int x; public: base(int i) { x=i; cout<<"基类的构造函数"<<endl;

面向对象(二)——继承、派生、组合以及接口

一.继承与派生 1.1 什么是继承 继承是一种创建新类的方式,在Python中,新建的类可以继承一个或多个父类,父类又可称为基类或超类,新建的类称为派生类或子类 Python中类的继承分为:单继承和多继承 class People: # 定义父类 def __init__(self,name,age): self.name=name self.age=age def walk(self): print('%s is walking' %self) class Teacher(People): #