C++ 继承(二)


上文链接—> 继承(一)

6.继承与友元

友元具体讲解—》友元复习
友元函数复习:

用法:在类体中用friend对某函数声明,此函数就称为本类的友元函数。
说明:

  • 友元函数可访问类的私有和受保护成员,但不是类的成员函数(没有this 指针)
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数,或者说,友元函数不仅可以是一个非成员函数,也可以是另一个类中的成员函数
  • 友元函数的调用与普通函数的调用和原理相同

友元函数关系不能被继承

class Student;//先声明
class Person
{
public:
	friend void Display(const Person& p, const Student& s);//声明display为Person类的友元函数,可以在类外调用访问其非公有成员
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum; // 学号
};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;//可见友元函数关系不能继承
}
void main()
{
	Person p;
	Student s;
	Display(p, s);
}

在这里插入图片描述

友元类复习:
用法:在类体中用friend对某个类声明,此类就称为本类的友元类
说明:

  • 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
  • 友元关系是单向的,不具有交换性。
    比如Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递,如果B是A的友元,C是B的友元,则不能说明C是A的友元。

友元类关系也不能被继承

class Student;//前置声明
class Person
{
	friend class Student;//声明学生类为人类的友元类,则学生类的成员函数相当于人类的友元函数,可以访问人类的非公有成员了
private:
	string _name;
	int _age;
};

class Student
{
public:
	void Display(Person& p)
	{
		cout << p._name << endl;//声明友元关系了的,就可以访问人类的非公有成员
	}
private:
	int _stuNum;
};
class Midstudent :public Student //中学生类继承学生类
{
public:
	void Display(Person& p)
	{
		cout << p._name << endl;//可见不能继承学生类对人类的友元类关系
	}
private:
	int _grade;
};

在这里插入图片描述

7.继承与静态成员

回顾静态数据成员特性:

  • 静态数据成员不属于单独某个对象,为同一类中所有对象所有,即所有对象都可以引用它;
  • 静态数据成员在内存中只占一份空间(并不是每个对象都为其开空间),如果改变这个值,所有对象中的这个静态数据成员的值也同时改变,这样可以节约空间,提高效率。
  • 静态数据成员不同与一般的数据成员,定义对象时,才为对象的一般数据成员分配空间,而静态数据成员不属于某一个对象,前面分配的空间不包括其所占的空间,是在所有对象之外单独开辟空间的。就是说,只要类中指定了静态数据成员,不管有没有定义对象,静态数据成员也可以被引用(类名::静态成员)。
  • 一般数据成员随对象的建立分配空间,随对象撤销时释放。静态数据成员在程序编译时分配空间,程序结束才释放空间。
class Person
{
public:
	static int _num;

}; 
int Person::_num = 0; //静态成员需要在类外初始化

class Student :public Person
{
public:
	//static int _num;//同名成员,构成隐藏。
	//要是没有同名静态成员,不构成隐藏的话,就是同一个(继承下来的)
};
//int Student::_num = 0;
int main()
{
	Person p;
	Student s;
	p._num++;
	s._num++;

	Person::_num=10;//因为是静态成员,不属于任何一个对象,而是属于类的,所以也可以使用类名加限定符显示引用
	s._num=5;//派生类引用
	return 0;
}

在这里插入图片描述
派生类也定义了一个同名static变量,构成隐藏了。
在这里插入图片描述

总结:
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。(除非派生类也定义了同名的静态成员函数)

8.复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承(或者说一个派生类只从一个基类派生),这种继承形成的层次是一个树形结构。
在这里插入图片描述
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。(一个派生类有两个和多个基类)。类似于图型结构。
在这里插入图片描述
菱形继承:菱形继承是多继承的一种特殊情况。
在这里插入图片描述

菱形继承的问题

菱形继承有数据冗余和二义性的问题。

//模拟菱形继承
class Person
{
public:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _num; //学号
};
class Teacher : public Person
{
protected:
	int _id; // 兼职职工编号
};
class Dastudent : public Student, public Teacher
{
protected:
	string _techerbook; // 教书课程
};
void Test()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Dastudent a;
	//a._name = "peter";
	// 需要显式指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}

二义性问题:
无法明确访问的是从哪个基类继承下来的成员变量
在这里插入图片描述
数据冗余问题:
在dastudent的对象中Person成员会有两份。

在这里插入图片描述

虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。
如上面的继承关系,在Student和Teacher继承Person时使用虚拟继承,即可解决问题。注意:虚拟继承不要在其他地方去使用(这是专门解决菱形继承问题的)。

在这里插入图片描述
注意:

  • 虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。
  • 经过虚基类的声明后,当基类通过多条派生路径被一个派生类继承的时候(即菱形继承),该派生类只继承该基类一次,即基类成员只被保留一次。

虚拟继承解决数据冗余和二义性的原理
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。

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* ptr = &d;
	cout << ptr << endl;
	cout << sizeof(d) << endl;

	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	
	return 0;
}

普通菱形继承分析
可以看到虚基类成员_a存在两个。数据冗余,且还存在二义性。
在这里插入图片描述
sizeof(d)的字节大小为(五个成员变量)*4=20.

虚拟菱形继承分析

在这里插入图片描述
在这里插入图片描述
其实这两个提前被保存的是指向虚基表的指针,指向的虚基表里保存的是表地址与虚基类成员的偏移量。通过表地址+偏移量便可以找到被派生类D只保留一份的虚基类成员的地址,从而便可以即解决了数据冗余,又解决了二义性的问题。

通过表地址和偏移量,派生类B中继承的基类成员_a和派生类C中继承的基类成员_a先后被赋值到同一个内存上(便是D只保留一份的虚基类成员)。
在这里插入图片描述
在这里插入图片描述
所以sizeof(d)的字节大小为(四个成员变量+两个指针)*4=24.
在这里插入图片描述
多继承的总结和反思

  1. C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,且一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的面向对象语言中都没有多继承,如Java。

9.继承和组合

public继承是一种is-a(是)的关系。也就是说每个派生类对象都是一个基类对象
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见。
继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高(牵连性高)

组合是一种has-a(包含)的关系。假设B组合了A,每个B对象中都有一个A对象。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用。
因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低(牵连性低)。优先使用对象组合有助于你保持每个类被封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。

class X
{
protected:
	int _x;
};
// 继承 -> 白箱复用  (强关联关系)  is-a关系
class Y : public X
{
protected:
	int _y;
};



class M
{
protected:
	int _m;
};
// 组合 -> 黑箱复用 (弱关联关系)  has-a关系
class N
{
protected:
	M _mm;//类对象组合
	int _n;
};

注意如何在两种方式中选择:
两个类的关系更符合is-a就用继承
两个类的关系更符合has-a就用组合
都适用,优先使用组合

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值