【C++】继承

本文详细介绍了C++中的继承机制,包括继承的概念和定义,基类与派生类对象的赋值转换,作用域规则,以及继承与友元、静态成员的关系。重点讨论了菱形继承及其问题,以及如何通过虚拟继承解决数据冗余和二义性。同时,探讨了继承与对象组合的区别,强调了组合在降低耦合度和提高代码维护性方面的优势。
摘要由CSDN通过智能技术生成

继承的概念及定义

1.继承的概念

在C++中,所谓“继承”就是在一个已存在的类的基础上建立一个新的类。 已存在的类称为“基类 ”或“父类”,新建的类称为“派生类 ”或“子类 ”。

继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能;继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

例如:现在定义要两个类Teacher和Student,他们有共同的成员:姓名、性别、年龄、学院等,不同的Teacher类里要有:职务,Student类里要有年级、班级等。那么共同的部分就可以定义在同一个类Person里,Teacher里定义职务,Student里定义年级和班级,共有的部分在自己类里不写,只要继承下来就行了。

class Person
{
public:
 void func()
 {
 	cout<<"hello world"<<endl;
 }
 
 string _name; // 姓名
 int _age; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。
class Student : public Person
{
protected:
 int _stuid; // 学号
};

class Teacher : public Person
{
protected:
 int _jobid; // 工号
};

2.继承的定义

定义格式
在这里插入图片描述
继承方式和访问限定符
在这里插入图片描述
在这里插入图片描述

  • 基类的protected成员:不能在类外直接被访问,但在派生类中能访问
  • 不可见:指的是基类的私有成员被继承到派生类对象中,但派生类对象在类内外都无法对这些私有成员进行访问
  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  • 在实际运用中一般使用都是public继承,不提倡使用protetced/private继承

基类和派生类对象的赋值转换

  • 切片。派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。
  • 基类对象不能赋值给派生类对象
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。

赋值兼容(切片)只能是在共有继承下

切片:把子类中的父类部分(切割出来)赋值给父类的对象/引用/指针,子类继承下来的父类私有成员在子类中是不可见(不可操作的),但切片赋值给p后,P是可以使用的
继承时父类一般不建议设置私有成员

int main()
{
	Student s;
    Person p;
	p = s;//对象赋值
	Person& ref = s;//赋值给父类引用
	Person* ptr = &s;//赋值给父类指针
    return 0;
}

这里的赋值不存在类型转换(不存在Student向Person类型转换)

int i = 1;
double d =2.2;
i = d;//隐式类型转换
const int& ri = d;//ri不能直接引用d,中间有类型转换,类型转换换产生临时变量
                //临时变量具有常性,需要加上const

私有继承是不能切片的,因为会涉及到权限的转换。私有继承下来的东西是私有的,切片后变成共有,权限放大,是不合理的。

父类是不能给子类赋值的,但有以下情况:

Person p;
Student s;

s = (Student)p;//行不通,类型转接也不行
//以下两种情况可行,但会有越界的风险
Student* ptr = (student*)&p;
Student& ptr = (student&)p;
//访问时不能越过子类中父类部分

继承中的作用域

  • 在继承体系中基类和派生类都有独立的作用域。
  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  • 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

    子类和父类出现同名成员:隐藏/重定义。同名函数参数是否相同也不会影响.

    函数重载要求在同一作用域,它们有各自作用域,不会构成重载,构成隐藏.

  • 注意在实际中在继承体系里面最好不要定义同名的成员
  class Person
{
public:
	void fun()
	{
		cout << "father" << endl;
		a = 1;
	}
protected:
	string _name = "李天王"; // 姓名
	int _num = 111; 	   // 身份证号
	int a;
};


class Student : public Person
{
public:
	void fun()
	{
		cout << "child" << endl;
	}
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << _num << endl;
		cout << Person::_num << endl;//访问父类成员的同名成员_num需指定作用域
	}
protected:
	string _name = "小李子";
	int _num = 999; // 学号
};

int main()
{
	Student s;
	s.fun();
	s.Person::fun();//使用父类中的同名函数需要指定作用域
					//如果成员名和函数名不相同,则不构成隐藏,就不需要指定域
	return 0}

ps:局部和全局可以有同名成员,当访问时优先访问局部,(局部优先、就近原则)

派生类的默认成员函数

派生类在C++中有11个默认成员函数,如果我们不写编译器会自己生成,重要的有四个,构造函数、拷贝构造、赋值、析构函数

想想以下问题
1.派生类的四个默认成员函数我们不写,编译器生成的会如何处理?
2.我们要写的话,应该怎么写?
3.如果我们自己写,又会如何处理?

派生类中如果我们不写默认成员函数,那么分两种情形讨论:
1.从父类继承下来的部分
调用父类的构造析构,拷贝构造和赋值使用父类提供的方法
2.派生类自己的部分
内置类型:构造析构不处理,拷贝和赋值进行浅拷贝或值拷贝
自定义类型:调用自定义类型自己的构造析构、拷贝、赋值
总结:继承下来的调用父类的处理,自己的安照普通类规则处理

什么情况下需要自己写默认构造函数?
1.父类没有构造函数,需要自己写构造
(子类示例化时要调用父类的构造函数对继承的部分初始化,自己的部分可由编译器自己生成)
2.如果子类有资源需要释放时,需要自己显示写析构
3.如果子类存浅拷贝问题,需要自己显示写拷贝构造和赋值

让我们看以下代码,派生类的默认构造函数怎么写

class Person
{
public:
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << 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 = 1)
		//:_name(name) 子类访问不到父类的私有成员_name
		:Person(name) //调用父类构造函数初始化_name
		,_num(num)
	{}
	//s2(s1)
	Student(const Student& s)
		:Person(s)  //s自动切片传给父类拷贝构造函数
		,_num(s._num)
	{}
	//s2=s1
	Student& operator=(const Student& s)
	{
		if (this != &s)//判断是否自己给自己赋值
		{
			Person::operator=(s);//父子operator=重名构成了重载
			_num = s._num;
		}
	}
	//析构函数的名字会被统一处理成destructor() (原理需要多态的知识解释)
	//子类和父类的析构就构成隐藏,需要指定类域才能访问到:Person::~Person()
	~Student()
	{
		//子类析构函数结束时会自动调用父类的析构,所以这里不需显示调用父类析构
		// 这样可以保证先析构子类再析构父类,也能避免多次析构父类
		//构造时先父类再子类,析构时先子类后父类(栈上定义,后进先出)
	}
protected:
	int _num; // 学号
};

继承与友元的关系

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

继承和静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。父类和任意子类访问到的静态成员都是同一个。

静态成员不能在类内初始化,因为静态成员属于整个类,而不属于某个对象。

class A
{
	private:
	static int a  = 0;//静态成员在类内不能初始化,只能定义
	const int b = 0;//常量成员可以
	static const int c = 0;//可以编译,类内初始化的静态数据成员 
				//必须是具有不可变的常量整型类型
	static const double e = 1.234;//编译不通过
	const double e = 1.234;//编译通过
}

int A::a = 0;//在类外初始化静态成员

菱形继承和菱形虚拟继承

单继承 > 只有一个直接父类的
多继承 > 一个子类有两个或以上直接父类
菱形继承 > 多继承中的特殊情况
(只列举了最简单的情况)
只列举最简单的情况

1.菱形继承存在的问题

数据冗余和二义性
进行了菱形继承的子类,其直接父类都继承了一份原始父类的数据,则菱形继承的子类就会继承多份原始父类数据.而如果原始父类中有一个1w字节的数组,到最后的子类继承下来的空间大小就翻倍了。造成数据冗余

假如原始父类有一个成员_name,菱形继承的子类就继承了多个_name,当子类去访问_name时,会造成访问的对象不明确,即二义性。需要指定是哪个父类继承下来的_name

在使用多继承时一定要尽量避免使用菱形继承!!
多继承算是C++的缺陷之一,Java、Python等后来语言都没有多继承。

如何解决数据冗余和二义性?
使用虚继承 virtual

使用方法
判断哪个类会造成数据冗余和二义性,直接继承这个类的派生类就要使用虚继承
在这里插入图片描述

2.virtual是如何解决这些问题的?

使用虚继承后,子类里就只有一份原始父类的数据。其直接父类中的原始父类的数据,都相当于是是这一份数据的引用,是同一份。

从内存上看原理

//有四个类A、B、C、D,B和C继承了A,D继承了B和C,构成菱形继承
class A
{
public:
	int _a;
};
// class B : public A
class B : virtual public A
{
public:
	int _b;
};
// class C : public A
class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};


int main()

{
	//观察其在内存的分布情况
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	d._a = 6;
	return 0;
}

从内存上看,内存地址由小到大,先继承的放在前,后继承的放在后
在这里插入图片描述
在使用虚继承的情况下,B、C、D的_a使用的都是同一块空间
在这里插入图片描述

虚基表
在这里插入图片描述

3.继承与组合

  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
    (A就是B。例如Person定义是人,Student也是人,student is a Person)
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。(例如车和轮胎的关系,车上有轮胎,但不能说车就是轮胎。有比如链表和节点的关系)
组合
class AA
{
	char* arr[100];
};

class BB
{
	int c;
	AA a;//BB只能访问AA的共有成员,不能访问私有成员
};
例如链表和节点
struct ListNode
{
	ListNode* next;
	ListNode* prev;
	int data;
};

class List
{
public:
	void push_back()
	{}
private:
	ListNode* head;

};
  • 优先使用对象组合,而不是类继承
    完全符合is-a用继承
    完全符合has-a用组合,都可以用,优先用组合
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用
    (white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。
  • 继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关
    系很强,耦合度高。
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对
    象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),
    因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,
    耦合度低。优先使用对象组合有助于你保持每个类被封装。
  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

继承的耦合度高,基类成员的修改有可能对子类的影响很大。
继承那么复杂,能不能抛弃继承只用组合呢?答案是不能,组合不能切片,而且C++的多态是建立在继承之上的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值