C++继承

继承的概念和定义

继承的概念

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

继承定义

继承定义格式

在这里插入图片描述

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "perter";
	int  _age = 18;
};
//继承后父类的Person成员(成员函数+成员变量)都会成为子类的一部分,这里体现出Stuident复用Person的成员。
class Student :protected Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
protected:
	int _stuid;
	int _major;
};

继承基类成员访问方式的变化

基类中被不同访问限定符修饰的成员(成员变量,成员函数),当以不同的继承方式继承时,该成员的最终访问方式也会变化。
变化方式如图
在这里插入图片描述
总结
在基类和派生类继承关系中:
如图,我们可以认为三种访问限定符的权限大小为:

public > protected > private;

1: 基类private成员在派生类中无论以方式继承都是不可见的。
这里的不可见是指基类的私有成员还是继承到派生类对象中,但是语法上限制派生类对象不管在类里面还是在类外都不能去访问它。

注意;
如果基类A的公有成员以private的方式继承,此时该公有成员在派生类B的继承方式变为派生类的private成员,此时派生类B可以在派生类中对该成员进行访问,但是如果有一个派生类C继承了B,那么此时B就对于C而言变成了基类,基类的private成员在派生类C中都是不可见的。所以,继承过后成员变量的访问权限改变影响的是下一次继承。

2: 使用关键字时class 的默认继承方式为private, 使用struct 时默认继承方式为public,不过我们最好显示的写出继承关系。

3: 基类的私有成员在派生类中都是不可见的(无论以什么方式继承),基类中的其他成员在派生类中的访问方式== Min( 成员在基类中的访问限定符和继承方式权限最小的)该访问方式影响的是下一个派生类。

4:基类中的private成员在派生类中不可以访问,protected成员在派生类中可以访问。对于类内外来说,都不可以访问。

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "perter";
	int  _age = 18;
};
//private继承,此时基类在派生类的访问方式变为private,该访问权限改变影响着下一次继承。此时依旧可以访问基类中的protected成员。
class Student : private  Person
{
public:
	void Print() 
	{
		cout << _name << endl;
		cout << _age << endl;
	}
protected:
	int _stuid;
};
//Student的成员访问方式为private,Student1派生类中不可以访问Student中的private成员的。
class Student1 : public Student
{
public:
	void Print1()
	{
		cout << _name << endl;  //
		cout << _age << endl;
	}
};

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

1: 派生类对象可以赋值给 基类的对象/ 基类的指针 / 基类的引用

2: 基类对象不能赋值给派生类。

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
public:
	string _name = "perter";
	int  _age = 18;
};
class Student : public  Person
{
public:
	void Print()
	{
		cout << _name << endl;
		cout << _age << endl;
	}
protected:
	int _stuid;
};


int main()
{
	Person p;
	Student s;
	Person  p1 = s;   //派生类对象赋值给基类对象
	Person* p2 = &s;   //派生类对象赋值给基类指针
	Person& p3 = s;    //派生类对象赋值给基类引用
	s = p;             //基类对象不能赋值给派生类对象
}
	

例如
派生类对象赋值给基类对象
又叫切割,将派生类中基类的那一部分赋值。
在这里插入图片描述
派生类对象赋值给基类指针:
p指向的内容为Student派生类中基类的那一部分。
在这里插入图片描述

派生类对象赋值给基类的引用:
取子类中父类的的那一部分引用。
在这里插入图片描述

继承中的作用域

1: 在继承体系中基类和派生类都有独立的作用域。

2: 基类和派生类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,编译器优先从最近的作用域寻找,这种情况叫做隐藏。,也叫重定义。所以如果在子类成员函数中访问基类成员,可以是使用 基类::基类成员 显示访问。

3:对于基类和派生类来说,只要函数满足函数名相同就构成隐藏。

注意
隐藏是对于基类和派生类两个作用域而言的,只要函数成员满足函数名相同就构成隐藏,而函数重载对于同一个作用域而言,并且对形参类型,个数,返回值也有限制。

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
public:
	string _name = "perter";
	int  _age = 18;
	int _stuid = 1;
};
class Student : public  Person
{
public:
	void Print(int a = 0,int b = 1)
	{
		cout << _name << endl;            
		cout << _age << endl;
		cout << _stuid << endl;            //11
		cout << Person::_stuid <<endl;     // 1
		Person::Print();                  //peter, 18
	}

protected:
	int _stuid = 11;
};

int main()
{
	Student s;
	s.Print();
}

派生类的默认成员函数

派生类中6个默认成员函数:
在这里插入图片描述

class Person
{
public:
//基类构造
	Person(const string& 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)
	{
		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()" << endl;
	}
//拷贝构造
	Student(const Student& s)
		:Person(s)
		, _num(s._num)
	{
	}
//赋值
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}
//析构
	~Student()
	{
		cout << "~Student" << endl;
		//
		Person::~Person();

	}
protected:
	int _num;
};
int main()
{
	Student s("ppp", 1);   //调用构造函数.
	Student s1 = s;        //调用拷贝构造
	
	Student s2("yzh", 2);   
	s2 = s1;               //调用赋值.
	return 0;
}

注意:
拷贝构造:
我们已知基类拷贝构造形参为Person& p,所以在子类调用时很难从子类中拿到父类对象传参,但是我们可以传子类对象,引用的为子类对象中父类的那一部分,也叫做切片.
赋值:
在调用基类赋值时一定要标明作用域,因为基类赋值和派生类赋值同名,为隐藏关系,如果不标明,编译器会默认调用自身赋值,这样就会无限循环.
析构
1:由于多态的处理,派生类的析构和基类的析构会被统一处理为destructtor(),此时它们互为隐藏关系,所以在调用基类析构时需要标明作用域.
2: 由于函数栈帧先进后出原则,创建一个派生类的对象时先构造基类再构造派生类,所以根据先构造的后析构原则,编译器在派生类析构函数后面会主动调用基类析构.这样才能保证先析构派生类后析构基类.而自己显示写无法保证先析构派生类后析构基类.

继承与友元

友元关系不能继承,也就是说基类的友元函数可以访问基类的私有和保护成员,但是不能访问派生类的私有和保护成员.
比如:
Display函数时基类Person的友元,Display希望能够访问基类和派生类的私有和保护成员,但是Display函数并不是派生类Student的友元,即Display函数无法访问派生类Student中的私有成员和保护成员.

class Person
{
public:
	friend void Display(const Person& p, const Student& s);
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; //无法访问
}
int main()
{
	Person p;
	Student s;
	Display(p, s);
	return 0;
}

解决办法:
如果我们也想让Display友元函数也能够访问派生类Student的私有和保护成员,可以在派生类中添加友元函数声明.

class Student : public Person
{

	friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum; //学号
};

继承与静态成员

如果基类中定义了static静态成员,说明这个静态成员只在静态区,无论派生出多少个子类,都只有一个static成员实例.

using namespace std;
//基类
class Person
{
public:
	Person()
	{
		_count++;
	}
protected:
	string _name; //姓名
public:
	static int _count; //统计人的个数。
};
//类外定义,类型,名字,初始值.
//静态成员类外初始化.
int Person::_count = 0; 
//派生类
class Student : public Person
{
protected:
	int _stuNum; //学号
};
//派生类
class Graduate : public Person
{
protected:
	string _seminarCourse; //研究科目
};
int main()
{
	Person p;
	Student s;
	++Person::_count;
	cout << Student::_count << endl;  //3
	cout << Student::_count << endl;  //3
	//打印的地址也相等.
	cout << &Person::_count << endl;//00007FF76B093444
	cout << &Student::_count << endl;
	//00007FF76B093444
	return 0;
}

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

单继承:
一个子类只有一个直接父类时称这个继承关系为单继承.
在这里插入图片描述
多继承:
一个子类有两个或两个以上直接父类时称这个继承关系为多继承.
在这里插入图片描述
菱形继承:
菱形继承是多继承中的一种特殊情况…
在这里插入图片描述

从下面的成员构造可以看出,菱形继承有数据冗余和二义性问题.
数据冗余:
Assistant的对象的Person成员会存在两份.
在这里插入图片描述

using namespace std;
//基类
class Person
{
public:
	string _name; //姓名
};
class Student : public Person
{
protected:
	int _stuNum; //学号
};
class Teacher : public Person
{
protected:
	int _id;
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse;
};
int main()
{
	Assistant a;
	//菱形继承的二义性问题,对访问哪个_a不明确.
	a._name = "yzh"; 
}


** 二义性解决办法**:
因为Assistant对象继承的基类为Student和Teacher,但是Student和Teacher都继承了基类Person.所以当访问Assistant对象中的_name成员会出现访问不明确.
所以当菱形继承时,在访问Assistant对象中的_name时我们可以指定Assistant继承基类的类域,希望访问哪个类域的_name就指定哪个.

int main()
{
	Assistant a;
	//菱形继承的二义性问题,对访问哪个_a不明确.
	a._name = "yzh"; 
	
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}

但是,仍然不能解决数据冗余的问题,此时便要用到菱形虚拟继承.

菱形虚拟继承

菱形虚拟继承可以解决菱形继承中的二义性读和数据冗余问题.


1:此时,我们便可以直接访问Assisstant对象中的成员_name了,并且我们指定访问Assistant中的Student基类和Teacher基类中的_name都是同一个结果,这便解决了二义性问题.
2:并且当我们打印Assistant对象中Student基类和Teacher基类中的_name时,也是同一个地址.这便解决了数据冗余问题.

菱形虚拟继承原理

例如:

class A
{
public:
	int _a;
};
// class B : public A
class B :  public A
{
public:
	int _b;
};
// class C : public A
class C :  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;
	return 0;
}

现在,我们来看看菱形继承时,借助内存窗口观察对象成员的模型.
在这里插入图片描述
也就是说,D类对象当中的成员分布如下:
D类对象中有两个类A的成员,这也就菱形继承导致数据冗余和二义性的原因.
在这里插入图片描述

现在,我们从内存窗口中看看菱形虚拟继承各窗口分布情况.

class A
{
public:
	int _a;
};
class B : virtual public A
{
public:
	int _b;
};
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._c = 4;
	d._b = 3;
	d._d = 5;
	return 0;
}

在这里插入图片描述
也就是对象D中菱形虚拟继承的内存分布如下:
1:D中B里面继承了A,D中C里面继承了A,他们的排布按照函数栈帧由低到高进行分布,分别为先B中的成员由低到高到C中的成员进行分布,最后再为D.
2: 将B,C中的_a成员变为了两个指针,这两个指针中第一个位置预留的值为零,第二个位置存的是偏移量(距离公共成员_a的距离.如果我们要从类B中访问到公共内置成员_a,必须通过B中指针的偏移量去访问到公共内置成员_a.
在这里插入图片描述

菱形虚拟继承中虚指针应用

测试代码如下:

class A
{
public:
	int _a;
};
class B :  public A
{
public:
	int _b;
};
class C : public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
void Func( B* ptr )
{
	cout << ptr->_a << endl;
}
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._c = 4;
	d._b = 3;
	d._d = 5;
	B* pb = &d;
	Func(pb);
	return 0;
}

1:对于函数Func()来说,如果传的是类B对象的地址,那么就是正常调用,可是,如果传的是派生类对象的地址,那么此时便会发生继承中的赋值转换,那么此时ptr指向内容为派生类对象中与基类共有的部分.所以,如果虚基类没有统一模型,进而虚指针来寻找虚表的话,此时ptr根本不知道公共成员_a的位置.
2: 如果虚基类中有多个内置成员也不需要添加多个虚指针,此时只要通过虚指针继续添加偏移量就可以找到其他内置成员的.

继承的总结和反思

继承与组合

is-a关系:
继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象.
例如:
人<——学生, 植物<——玫瑰花,车类<——宝马类

class Car
{
protected:
	string _colour; 
	string _num; 
};
class BMW : public Car
{
public:
	void Drive()
	{
		cout << "this is BMW" << endl;
	}
};

has-a关系:
has-a是一种组合关系,就是一个对象中包含了另外一个对象.
例如:
轮胎和车

class Tire
{
protected:
	string _brand; //品牌
	size_t _size; //尺寸
};
class Car
{
protected:
	string _colour; //颜色
	string _num; //车牌号
	Tire _t; //轮胎
};

注意:
继承和组合关键点在于访问权限不同.
如果是组合关系,那么Tire中protected内置成员就无法在Car直接访问,
如果是继承关系,那么Tire中的protected内置成员就可以在Car直接访问.

class Tire
{
protected:
	string _brand; 
	int _size =1; 
};
class Car
{
public:
	string _colour; 
	string _num;
	Tire _t;
public:
	void Func()
	{
		cout << _t._size << endl; //不可以访问.
	}
};
int main()
{
	Car c;
}

总结:
1:继承允许你根据基类的实现来定义派生类的实现,这种通过生成派生类的复用通常被称为白箱复用(White-boxreuse).术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对于派生类可见,继承一定程度破坏了基类的封装,基类的改变对派生类有很大的影响,派生类和基类间的依赖性关系很强,耦合度高。
2:组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口,这种复用风格被称之为黑箱复用(Black-box reuse),因为对象的内部细节是不可见的,对象只以“黑箱”的形式出现,组合类之间没有很强的依赖关系,耦合度低,优先使用对象组合有助于你保持每个类被封装,组合中我们可以通过被组合对象的接口进行访问来访问被组合对象成员
3:实际中我们应该尽量使用组合,组合的耦合度低,如果类与类之间的关系既符合继承也符合组合,那么优先选择组合.

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暂停更新

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值