【C++的面向对象】-------C++ 的继承特性


1.派生类和基类的构造析构关系


【1】派生类并不继承基类的构造和析构函数,只继承成员变量和普通成员方法

  • 不继承,意思是派生类中确实没有,不包含基类的构造和析构函数
  • 派生类自己有自己的构造和析构,且构造和析构的规则和之前讲过的完全一样
  • 研究构造和析构函数时,一定要注意默认规则

【2】派生类的构造函数一定会调用基类的构造函数,析构也一样

  • 代码验证:在基类和派生类中都显示提供“默认构造”并添加打印信息,通过执行结果来验证
/***********person.hpp***************/
#ifndef  __PERSON_HPP__
#define  __PERSON_HPP__

#include <string>
#include <iostream>
using namespace std;

class person
{
public:
	//成员变量
	int age;
	string name;
	
	//构造函数
	person()
	{
		cout << "person()" << endl;
	}
	
	//析构函数
	~person()
	{
		cout << "~person()" << endl;
	}
	//成员方法
	void speak(void);
	void print(void);

private:
	bool male;
};

#endif
/***********person.cpp***************/
#include "person.hpp"

void person::speak(void)
{

}

void person::print(void)
{

}

/***********man.hpp***************/
#ifndef  __MAN_HPP__
#define  __MAN_HPP__

#include "person.hpp"

class man:public person
{
public:
	
	//构造函数
	man()
	{
		cout << "man()" << endl;
	}
	
	//析构函数
	~man()
	{
		cout << "~man()" << endl;
	}
	
	void work(void);
private:

};

#endif
/***********man.cpp***************/
#include "man.hpp"

void man::work(void)
{

}

/***********main.cpp***************/
#include "man.hpp"
#include "person.hpp"

int main()
{
	man man1;
	return 0;
}

实验结果

在这里插入图片描述

  • 通过代码执行结果看到的现象总结:派生类的构造函数执行之前,会先调用基类的构造函数,然后再调用自己的构造函数。而在派生类的析构函数之后,会先执行自己的析构函数,再执行基类的析构函数。
  • 代码验证:派生类的任意构造函数,可以显式指定调用基类的任意一个构造函数,通过参数匹配的方式(类似于函数重载)
/***********man.hpp***************/
#ifndef  __MAN_HPP__
#define  __MAN_HPP__

#include "person.hpp"

class man:public person
{
public:
	int length;
	//构造函数
//	man()           简写  man:person()才是默认构造函数的完整写法   
	man():person("aston")
	{
		cout << "man()" << endl;
	}
	
	man(int mylen):person()
	{
		this->length = mylen;
		cout << "man(int mylen)" << endl;
	}
	//析构函数
	//~man():~person()          显示写了后面的基类析构就会编译报错
	~man()
	{
		cout << "~man()" << endl;
	}
	
	void work(void);
private:

};

#endif

/***********person.hpp***************/
#ifndef  __PERSON_HPP__
#define  __PERSON_HPP__

#include <string>
#include <iostream>
using namespace std;

class person
{
public:
	//成员变量
	int age;
	string name;
	
	//构造函数
	person()
	{
		cout << "person()" << endl;
	}
	//自定义构造函数
	person(string myname)
	{
		this->name = myname;
		cout << "person(string myname)" << endl;
	}
	
	//析构函数
	~person()
	{
		cout << "~person()" << endl;
	}
	//成员方法
	void speak(void);
	void print(void);

private:
	bool male;
};

#endif

在这里插入图片描述

  • 思考:为什么派生类的构造函数必须调用基类的1个构造函数,这样设计有什么合理性?

【3】为什么派生类的构造(析构)必须调用基类的某个构造(析构)

  • 牢记构造函数的2大作用:初始化成员分配动态内存
  • 派生类和基类各自有各自的构造函数和析构函数,所以是各自管理各自的成员初始化,各自分配和释放各自所需的动态内存
  • 继承的语言特性,允许派生类调用基类的构造和析构函数,以管理派生类从基类继承而来的那些成员。
  • 明确:派生类的构造和析构处理的永远是派生类自己的对象,只是派生类对象模板中有一部分是从基类继承而来的而已。

【4】其他几个细节

  • 派生类构造函数可以直接全部写在派生类声明的class中,也可以只在clas中声明时只写派生类构造函数名和自己的参数列表,不写继承基类的构造函数名和参数列表,而在派生类的cpp文件中再写满整个继承列表,这就是语法要求
// xx.hpp
man(int mylen);

// xx.cpp 
man::man(int mylen):person()
{
	this->length = mylen;
	cout << "man(int mylen)" << endl;
}
  • 派生类析构函数则不用显式调用,直接写即可直接调用基类析构函数。猜测是因为参数列表问题。
	//~man():~person()          显示写了后面的基类析构就会编译报错
	~man()
	{
		cout << "~man()" << endl;
	}
  • 构造函数的调用顺序是先基类再派生类,而析构函数是先派生类再基类,遵循栈规则。
  • 派生类的构造函数可以在调用基类构造函数同时,用逗号间隔同时调用初始化式来初始化派生类自己的成员
	/*            :继承基类,初始化式   */
	man(int mylen):person(),length(mylen)
	{
		//this->length = mylen;     上面用初始化式初始化了
		cout << "man(int mylen)" << endl;
	}

【5】派生类做的三件事

  • 吸收基类成员:除过构造和析构函数以外的所有成员全部吸收进入派生类中
  • 更改继承的成员。1是更改访问控制权限(根据继承类型还有成员在基类中的访问类型决定) 2是同名覆盖(派生类中同名成员覆盖掉基类中)
  • 添加派生类独有的成员。

2.派生类和基类的同名成员问题


【1】派生类中再实现一个基类中的方法会怎样

  • 代码实验:派生类和基类中各自实现一个内容不同但函数原型完全相同的方法,会怎么样
  • 结论:基类对象调用的是基类的方法,派生类对象调用执行的是派生类中重新提供的方法
  • 这种派生类中同名同参方法替代掉基类方法的现象,叫做:重定义(redefining),也有人叫做隐藏
  • 隐藏特性生效时派生类中实际同时存在2份同名同参(但在不同类域名中)的方法,同时都存在,只是一个隐藏了另一个

【2】派生类中如何访问被隐藏的基类方法

  • 派生类对象直接调用时,隐藏规则生效,直接调用的肯定是派生类中重新实现的那一个
  • 将派生类强制类型转换成基类的类型,再去调用则这时编译器认为是基类在调用,则调用的是基类那一个,隐藏规则被绕过了,一般不推荐
  • 在派生类内部,使用父类::方法()的方式,可以强制绕过隐藏规则,调用父类实现的那一个

【3】注意和总结

  • 其实不止成员方法,成员变量也遵循隐藏规则。
  • 隐藏规则本质上是大小作用域内同名变量的认领规则问题,实际上2个同名成员都存在当前派生类的对象内存中的
  • 隐藏(重定义,redefining),与重载(overload)、重写(override,又叫覆盖),这三个概念一定要区分清楚。

3.子类和父类的类型兼容规则


【1】何为类型兼容规则

  • C和C++都是强类型语言,任何变量和对象,指针,引用等都有类型,编译器根据类型来确定很多事
  • 派生类是基类的超集,基类有的派生类都有,派生类有的基类不一定有,所以这2个类型间有关联
  • 派生类对象可以cast后当作基类对象,而基类对象不能放大成派生类对象,否则就可能会出错
  • 考虑到指针和引用与对象指向后,派生类和基类对象的访问规则就是所谓类型兼容规则。

【2】类型兼容规则的常见情况

int main()
{
	person pn1;
	man mn1;

	person pn2 = mn1;     //子类对象可以直接初始化或直接赋值给父类对象
	
	person* p = &mn1;    //类型兼容
	p->speak();          //执行的是person类里面的speak()
	mn*p = &person;      //编译报错,派生类是基类的超集
	
	person& rp = mn1;    //类型兼容
	rp.speak();			 //执行的是person类里面的speak()
	mn& rp = pn1;        //编译报错,派生类是基类的超集
	
	return 0;
}
  • 子类对象可以当作父类对象使用,也就是说子类对象可以无条件隐式类型转换为一个父类对象
  • 子类对象可以直接初始化或直接赋值给父类对象
  • 父类指针可以直接指向子类对象
  • 父类引用可以直接引用子类对象

【3】总结

  • 派生类对象可以作为基类的对象使用,但是只能使用从基类继承的成员
  • 类型兼容规则是多态性的重要基础之一。
  • 子类就是特殊的父类 (base *p = &child;)

4.继承的优势与不良继承


【1】何为不良继承

  • 鸵鸟不是鸟问题。因为鸵鸟从鸟继承了fly方法但是鸵鸟不会飞
  • 圆不是椭圆问题。因为圆从椭圆继承了长短轴属性然而圆没有长短轴属性
  • 不良继承是天然的,是现实世界和编程的继承特性之间的不完美契合

【2】如何解决不良继承

  • 修改继承关系设计。既然圆继承椭圆是一种不良类设计就应该杜绝。去掉继承关系,两个类可以继承自同一个共同的父类,不过该类不能执行不对称的setSize计算,然后在圆和椭圆这2个子类中分别再设计以区分。
  • 所有不良继承都可以归结为“圆不是椭圆”这一著名具有代表性的问题上。在不良继承中,基类总会有一些额外能力,而派生类却无法满足它。这些额外的能力通常表现为一个或多个成员函数提供的功能。要解决这一问题,要么使基类弱化,要么消除继承关系,需要根据具体情形来选择。

5.组合介绍以及与继承对比


【1】什么是组合

  • composition,组合,就是在一个class内使用其他多个class的对象作为成员
  • 用class tree做案例讲解
class shugen
{

}	

class shugan
{

}

class shuzhi
{

}

class shuye
{

}

class tree
{
	shugen gen;
	shugan gan;
	shuzhi zhi;
	shuye  ye;
}
  • 组合也是一种代码复用方法,本质也是结构体包含

【2】继承与组合的特点对比

  • 继承是a kind of(is a)关系,具有传递性,不具有对称性
  • 组合是a part of(has a)的关系,
  • 继承是白盒复用。因为类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的。
  • 继承的白盒复用特点,一定程度上破坏了类的封装特性,因为这会将父类的实现细节暴露给子类
  • 组合属于黑盒复用。被包含对象的内部细节对外是不可见的,所以它的封装性相对较好,实现上相互依赖比较小
  • 组合中被包含类会随着包含类创建而创建,消亡而消亡。组合属于黑盒复用,并且可以通过获取其它具有相同类型的对象引用或指针,在运行期间动态的定义组合。而缺点就是致使系统中的对象过多。
  • OO设计原则是优先组合,而后继承

6.多继承及其二义性问题


【1】多继承

  • 多继承就是一个子类有多个父类

【2】多继承的二义性问题1

  • 场景:C多继承自A和B,则C中调用A和B的同名成员时会有二义性
/************test.hpp*************/
#ifndef  __TEST_HPP__
#define  __TEST_HPP__

class A
{
public:
	void set(int a);    //设置a的值
	void printA(void);
	
private:
	int val1;
};

class B
{
public:
	void set(int a);    //设置a的值
	void printB(void);
	
private:
	int val2;
};

class C:public A,public B
{

};

#endif 

/************test.cpp*************/
#include "test.hpp"
#include <iostream>

using namespace std;

void A::set(int a)
{
	this->val1 = a;
}

void A::printA(void)
{
	cout << "val1 = " << this->val1 << endl;
}

void B::set(int a)
{
	this->val2 = a;
}

void B::printB(void)
{
	cout << "val2 = " << this->val2 << endl;
}

int main(void)
{
	C a;
	
	a.set(1);    //这句有二义性,因为编译器无法确定到底是想调用A类还是B类里面的set()函数
	
	a.A::set(1);   //没有二义性
	
	return 0;
}
  • 原因:C从A和B各自继承了一个同名(不同namespace域)成员,所以用C的对象来调用时编译器无法确定我们想调用的是哪一个
  • 解决办法1:避免出现,让A和B的public成员命名不要重复冲突。但这个有时不可控。
  • 解决办法2:编码时明确指定要调用哪一个,用c.A::func()明确指定调用的是class A的func而不是class B的
  • 解决办法3:在C中重定义func,则调用时会调用C中的func,A和B中的都被隐藏了
  • 总结:能解决,但是都没有很好的解决。

【3】多继承的二义性问题2

  • 场景:菱形继承问题。即A为祖类,B1:A, B2:A, C:B1,B2,此时用C的对象调用A中的某个方法时会有二义性
    在这里插入图片描述
/************test.hpp*************/
#ifndef  __TEST_HPP__
#define  __TEST_HPP__

class A
{
public:
	void set(int a);    //设置a的值
	void print(void);
	
private:
	int val1;
};

class B1:public A
{
public:
	void set(int a);    
	void print(void);
private:
	int valB1;
};

class B2:public A
{

};

class C:public B1,public B2
{
	
};

#endif 
/************test.cpp*************/
#include "test.hpp"
#include <iostream>

using namespace std;

void A::set(int a)
{
	this->val1 = a;
}

void A::print(void)
{
	cout << "val1 = " << this->val1 << endl;
}
void B1::set(int a)
{
	this->valB1 = a;
}
void B1::print(void)
{
	cout << "valB1 = " << this->valB1 << endl;
}

int main(void)
{
	C c;
	
	//c.set();       //有歧义
	
	c.B1::set(1);    //没有歧义
	c.B1::print();
	
	c.A::set(2);     //有歧义  
	c.A::print(); 
	return 0;
}
  • 分析:c.func()有二义性,c.A::func()也有二义性,但是c.B1::func()和c.B2::func()却没有二义性
  • 解决办法:和问题1中的一样,但是问题2更隐蔽,也更难以避免

【4】总结

  • 二义性就是歧义,好的情况表现为编译错误,不好的情况表现为运行时错误,最惨的情况表现为运行时莫名其妙
  • 随着系统的变大和变复杂,难免出现二义性,这不是程序员用不用心的问题,是系统自身带来的
  • 解决二义性问题不能靠程序员个人的细心和调试能力,而要靠机制,也就是编程语言的更高级语法特性
  • 虚函数、虚继承、纯虚函数、抽象类、override(重写,覆盖)、多态等概念就是干这些事的

7.虚继承解决菱形继承的二义性问题


【1】虚继承怎么用

  • 场景:菱形继承导致二义性问题,本质上是在孙子类C中有B1和B2中包含的2份A对象,所以有了二义性。
  • 虚继承解决方案:让B1和B2虚继承A,C再正常多继承B1和B2即可
class B1:virtual public A
{
public:
	void set(int a);    
	void print(void);
private: 
	int valB1;
};

class B2:virtual public A
{

};

C c;	
c.A::set(2);     //没有歧义
c.A::print(); 
  • 虚继承就这么简单,就是为了解决菱形继承的二义性问题而生,和虚函数(为了实现多态特性)并没有直接关系

【2】虚继承的实现原理

  • 虚继承的原理是:虚基类表指针vbptr和虚基类表virtual table
  • 参考博客:虚继承的原理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值