C++语言—面向对象之继承详解

类的继承: 

我们一直以来接触的函数在一定程度上完成了代码的复用,这里介绍面向对象的重要复用机制—继承

继承机制是面向对象程序设计使代码可以复用的重要手段,他允许我们在保持原有类特性的基础上进行扩展,增加功能。这样产生的类,称为派生类。继承呈现了面向对象程序的层次性,体现了从简单到复杂的认知过程

继承的简单使用:

class Person
{
public:
	void Display()
	{
		cout << _name << "-" << _sex << "-" << _age << endl;
	}
public:
	const char* _name;
	const char* _sex;
	int _age;
};
class Student : public Person
{
public:
	void BuyTicket()
	{
		cout << "买票半价" << endl;
	}
};

void test()
{
	Person p;
	p._name = "鸟哥";
	p._sex = "男";
	p._age = 20;
	p.Display();

	Student s;
	s._name = "鸟";
	s._sex = "男";
	s._age = 15;
	s.Display();
	s.BuyTicket();
}

 继承后的效果:

 C++语言—面向对象之继承详解_第1张图片

 继承的定义的格式:

C++语言—面向对象之继承详解_第2张图片

继承的方式有三种:公有继承,私有继承,保护继承。三种继承关系下基类三种访问权限的成员在派生类中的访问权限如下:

基类成员/继承方式 public继承 protected继承 privated继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的privated成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的privated成员
基类的private成员 不可见只能通过基类接口访问 不可见只能通过基类接口访问 不可见只能通过基类接口访问

 

 

 

 

 

总结:

  1. 基类private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访 问,就定义为protected。可以看出保护成员限定符是因继承才出现的
  2. public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一 个父类对象
  3. protected/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承
  4. 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员, 基类的私有成员存在但是在子类中 不可见(不能访问)
  5. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写 出继承方式
  6. 在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承

is-a原则:继承关系,例如正方形和长方形在性质层面是一种继承关系

has-a原则:组合关系,例如轮胎和车是一种组合关系

至于使用哪个原则,应该是具体场景具体对待,看两种事物之间是继承关系还是组合关系,是继承关系就用is-a,是组合关系就用has-a。

可能会有这样的场景,两种事物之间既可以是继承关系,又可以是组合关系,这时该用哪个我们初学者的确很难回答出来,多年的C++开发者建议用has-a,他们的实践经验是这种情况下is-a很难做下去。 

切片 / 切割:

C++语言—面向对象之继承详解_第3张图片

  1. 子类对象可以赋值给父类对象(切片)
  2. 父类对象不能赋值给子类对象
  3. 父类的指针/引用可以指向子类对象,但是他并不能访问所有成员,它只能访问到子类继承了自己的成员
  4. 子类的指针/引用不能指向父类对象

通过强转子类的指针/引用可以指向父亲对象,但是这种情况下父亲对象指针其实是一种越界访问行为,所以等到作用域结束后程序会崩溃。因为此时父亲对象指针指向的范围扩大了,而扩大的那一部分不属于它的合法指向范围(合法域),虽然那块区域的大小刚好是_num的大小。

C++语言—面向对象之继承详解_第4张图片

class Person
{
public:
	void Display()
	{
		cout<<_name<

继承中的作用域: 

  1. 在继承体系中基类和派生类都有自己独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽掉父类对同名成员的直接访问(在子类成员函数中可使用基类::基类成员访问)--隐藏 --重定义
  3. 在实际中继承体系中最好不要定义同名的成员
class Person
{
public:
	Person(const char* name,int id)
		:_name(name)
		,_num(id)
	{}
	void Display()
	{
		cout << _num << endl;
	}
protected:
	string _name;  //姓名
	int _num;  //id 
};
class Student : public Person
{
public:
	Student(const char* name,int id,int num)
		:Person(name,id)
		,_num(num)
	{}
	void Display()
	{
		cout << "id:" << Person::_num << endl;
		cout << "学号" << _num << endl;
	}
protected:
	int _num;  //学号 
};

void test()
{
	Person p("鸟哥",1111);
	Student s("鸟",2222,0000);

	p.Display();  //输出父类id
	s.Person::Display();  //输出子类id
	s.Display();  //输出的子类id和学号
}

这里首次提到隐藏(重定义)的概念,注意它是在不同作用域,只要成员的名字一样就会被隐藏起来,与参数返回值都无关;而重载是同一作用域内当参数类型、参数个数或参数顺序不同时可以使用同名函数

派生类默认构造函数:

继承体系下,派生类中如果没有显式定义这六个默认成员函数,编译器则会合成(注意这里的用词:之前没学继承之前说的是生成)这六个默认的成员函数

生成::不依赖于任何东西,只是编译器根据类的定义简单生成基于基础类型的默认成员函数

合成::必须依赖于基类,编译器根据基类的相应成员函数的行为来合成派生类的默认成员函数

class Person  //父类
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person(const char* name)" << endl;
	}
	Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
	Person& operator =(const Person& p)
	{
		cout << "Person& operator =(const Person& p)" << endl;
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name;  //姓名
};

class Student : public Person  //子类
{
public:
	Student(const char* name,int num)  //子类构造函数
		:Person(name)
		,_num(num)
	{
		cout << "Student(const char* name,int num)" << endl;
	}
	Student(const Student& s)  //子类拷贝构造函数
		:Person(s)
		,_num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}
	Student& operator = (const Student& s)  //子类的赋值运算符重载函数
	{
		if (this != &s)
		{
			Person::operator=(s);  //发生切片行为
			_num = s._num;
		}
		cout << "Student& operator = (const Student& s)" << endl;
		return *this;
	}
	~Student()  //子类的析构函数
	{
		cout << "~Student()" << endl;
	}
private:
	int _num;  //学号
};

void test()
{
	Student s("鸟",1111);

	Student s1 = s;

	Student s2("小亮", 2222);

	s2 = s;
}

派生类对象的构造与析构:

继承体系下派生类和基类构造函数的调用次序

继承体系下派生类和基类析构函数的调用次序

C++语言—面向对象之继承详解_第5张图片

有了对派生类构造函数和析构函数的认识,我们再来看看上面程序的运行结果:

C++语言—面向对象之继承详解_第6张图片

说明:

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

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

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

问题:

如何实现一个不能被继承的类?

基类的构造函数访问权限设置成私有的。 

继承体系下派生类的对象模型

对象模型为对象中非静态成员变量在内存中的布局形式,与成员函数无关,因此以下类中只给出了成员变量

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

C++语言—面向对象之继承详解_第7张图片

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

C++语言—面向对象之继承详解_第8张图片

菱形继承:

C++语言—面向对象之继承详解_第9张图片

菱形继承的对象模型:

C++语言—面向对象之继承详解_第10张图片

从上面的菱形继承的对象模型中,我们发现Assistant类中存在两份Person对象,因此在派生类中访问继承于基类的成员变量时,会存在数据冗余及二义性的问题

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._name = "鸟哥";  //错误提示:对_name的访问不明确

	//显示指定访问那个父类的成员
	A.Student::_name = "一号鸟";
	A.Teacher::_name = "二号鸟";
}

C++语言—面向对象之继承详解_第11张图片

虚拟继承

虚拟继承能够解决以上菱形继承的二义性和数据冗余问题

在继承权限前加上virtual关键字即可构成虚拟继承

虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示

class Person
{
public:
	string _name;  //姓名
};

class Student : virtual public Person
{
protected:
	int _num;  //学号
};

class Teacher :virtual public Person
{
protected:
	int _id;  //职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse;  //主修课程
};

void test()
{
	Assistant A;

	A._name = "鸟哥";  //对_name的访问没有问题

	//显示指定访问父类的成员
	A.Student::_name = "一号鸟";
	A.Teacher::_name = "二号鸟";
}

  C++语言—面向对象之继承详解_第12张图片C++语言—面向对象之继承详解_第13张图片

从以上图中可以看到的是,不管你以那种方式对父类成员赋值,首先都是支持的并且后一次赋值会覆盖掉前面的赋值,这里面的原因是构成虚拟继承后,菱形继承的对象模型发生了本质上的一个变化。

虚基表:

C++语言—面向对象之继承详解_第14张图片

你可能感兴趣的:(C++语言—面向对象之继承详解)