C++三大特性之——继承

一.继承概念及定义方式

概念:
继承机制是面向对象程序设计时,让代码可以复用的一种手段。它允许在保持原类的基础之上,对这个类的内容进行扩展,延申,由此而产生的新类称之为子类,又名派生类。(继承是类设计层次的复用)。
定义方式:

//定义一个管理学校人员的类
//基类(父类)
class Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << "年龄:" << _age << endl;
	}
private:
	string _name = "txt";
	int _age = 23;
};
//派生类(子类)
class Student :public Person
{
protected:
	int _studentID;//学号
};
//派生类(子类)
class Teacher : Person//默认其继承方式为私有(private)
{
protected:
	int _jobID;//工号
};

其中Student,Teacher为Person的派生类,public为继承方式,Person为基类。
注意:
基类成员在派生类中的访问方式 = min(成员在基类中的访问限定符,派生类的继承方式)
在这里插入图片描述
对上表的总结:
1.基类的private成员在派生类的继承中都是不可见的,但不可见是指语法上的限制,即派生类无论在类外还是类里边,都不能访问基类的私有成员。其实基类的私有成员还是会被继承到其派生类的对象中的。
2.如果基类的私有成员不想在类外直接被访问,但需要在其派生类中可以被访问,此时就需要将这个私有成员改为保护的(protected)。这里就体现出了private与protected访问权限的区别了。
3.在使用关键字class时,默认的继承方式为private,而struct关键字默认的继承方式为public。建议在定义时将继承方式标写清楚。

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

1.派生类对象可以赋值给基类的对象/基类的指针/基类的引用,就是派生类将自己继承的部分在赋值给基类,这个过程也被叫做切片或者切割。
2.基类的对象不可赋值给派生类对象。
3.基类的指针在指向派生类对象的情况下,可以通过强制类型转换的方式赋值给派生类指针。

class Person
{
protected:
	int _ID;
	int _age;
};
class Student : public Person
{
protected:
	int _telnum = 158;//注意:此处要初始化一下,不然下边的赋值会中断
};
int main()
{
	Student first;
	Person one;
	
    one = first; //赋值给基类的对象
	//first = one;//编译不通过
	Person* second = &first;//赋值给基类的指针
	Person &third = first;//赋值给基类的引用
    
    //基类的指针可以通过强制类型转换赋值给派生类的指针
	Person* p = &first;
	Student *pp = (Student*)p;
	
	return 0;
}

三.继承的作用域

1.基类和派生类在继承体系中都有独立的作用域。
2.若基类与派生类中存在同名的成员,就会发生隐藏(重定义)的情况,即派生类成员将会屏蔽基类对同名成员的访问。如果还想在派生类中访问基类的这个成员,则需要使用:基类名称::基类成员的方式进行显示访问。
3.如果是成员函数隐藏,则只需要函数名相同即可。
4.在派生类中最好不要定义同名的成员或函数,以避免混淆。
成员隐藏

class Person
{
protected :
 string _name = "txt"; // 姓名
 int _num = 610; // 身份证号
};
class Student : public Person
{
public:
 void Print()
 {
 cout<<" 姓名:"<<_name<< endl;
 //隐藏后想在派生类中访问基类成员,则需要显示访问
 cout<<" 身份证号:"<<Person::_num<< endl;
 cout<<" 学号:"<<_num<<endl;
 }
protected:
 int _num = 170; // 学号
};

函数隐藏

class A {
public:
 void fun()
 {
 cout << "func()" << endl;
 }
};
class B : public A {
public:
 void fun(int i)//与基类函数同名,构成隐藏
 {
 A::fun();
 cout << "func(int i)->" <<i<<endl;
 }
};

四.派生类的默认成员函数

1.构造函数:派生类构造函数会先调用基类的构造函数,去初始化从基类继承的成员。如果基类没有写默认构造函数,则需要派生类构造函数的初始化列表显示调用基类的默认构造函数。
2.拷贝构造函数:必须调用基类的拷贝构造函数完成所继承的基类成员的拷贝初始化。
3.operator=:与上同理。
3.析构函数:派生类的析构函数在调用完成后会自动调用基类的析构函数,以保证派生类对象先清理派生类成员之后在清理基类成员的顺序(构造时压栈的顺序)。

class Person
{
public:
	Person(const char* name="txt")
		:_name(name)
	{
		cout << "父类构造" << endl;
	}
	Person(const Person& name)
		:_name(name._name)
	{
		cout << "父类拷贝构造" << endl;
	}
		Person& operator=(const Person& p)
	{
		cout << "父类赋值重载" << endl;
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}
	~Person()
	{
		cout << "父类析构" << endl;
	}
private:
	  string _name;
};
class Student :public Person
{
public:
     	Student(const char* name, int num)
		:Person(name)
		, _ID(num)
	{
		cout << "子类构造" << endl;
	}
	Student(const Student& s)
		:Person(s)
		, _ID(s._ID)
	{
		cout << "子类拷贝构造" << endl;
	}
		Student& operator=(const Student& s)
	{
		cout << "子类复制重载" << endl;
		if (this != &s)
		{
			Person::operator=(s);//调用基类的赋值(其底层会发生切片赋值)
			_ID = s._ID;
		}
		return *this;
	}
	~Student()
	{
		cout << "子类析构" << endl;
	}
private:
	int _ID;
};

测试:

int main()
{
  //如果没有显示调用基类的构造函数,则编译器自动调用,就会导致,
  //构造的对象与实际名字不符合的现象,此处预想名字为tong,而实际生成的名字为txt
	Student a1("tong", 170);

	Student a2(a1);//拷贝构造
	Student a3 = a1;//赋值
	return 0;
}

调用顺序截图:
在这里插入图片描述
扩展:实现一个不能被 继承的类
1.由以上概念可知,如果派生类不可调到基类的构造函数或者析构函数就说明:这个类不能被继承;
2.在C++11中给出了一个关键字(final),可以直接解决问题。

方法一:将构造或者析构函数的范文权限写成私有的,让派生类不可访问
class A
{
  private:
  A()
  {}
  //~A()
  //{}
};
方法二:在类名字后边加上关键字final
class B final
{};

五.继承中的友元问题

基类的友元关系不可被继承:即基类的友元不能访问派生类的私有和保护成员(权限问题,一个类的友元可以随意访问本类的任意成员,但你不能因为继承了就可以访问人家子类的私有和保护成员了),但基类的友元是可以访问派生类的公有成员的。

class B;
class A
{
public:
	friend void func(const A& a, const B& b);
protected:
	int _age;
};
class B:public A
{
protected:    //改为public权限,基类友元便可访问
	int _ID;
};
void func(const A& a,const B& b)
{
	cout << a._age << endl;
	//cout << b._ID << endl;//语法编不过
}
int main()
{
	A a;
	B b;
	func(a, b);
	return 0;
}

六.继承体系中基类的静态成员

基类的静态成员:整个继承体系中中只有这样一个成员,无论其子类的多少,static成员只会有一个实例。

class A
{
public:
	A(){ ++_count;}
public:
	static int _count;
};
int A::_count = 0;
class B:public A
{
protected:
	int _b;
};
class C :public A
{
protected:
	int _c;
};
void main()
{
	A a;
	B b;//会先调用基类的构造函数,因此_count会加一次
	C c;//同上
	cout << " _count:" << A:: _count << endl;//输出:3
	A::_count = 0;
	cout << " _count:" << A::_count << endl;//输出:1
}

七.多继承与菱形继承

单继承:
一个子类只有一个直接父类,这个继承关系为单继承。如下A与B的关系

class A
{};
class B:public A
{};

多继承:
一个子类有俩个或以上的父类时,关系为多继承。如下C与A,B的关系

class A
{};
class B
{};
class C:public A,public B
{};

菱形继承:
多继承的一种特殊情况。如下所示的D类

class A
{
public:
    int _num;
};
class B:public A
{};
class C:public A
{};
class D:public B,public C
{};

菱形继承的问题:
会造成数据的冗余和二义性问题。如上的D类里,有A类的俩份一样的数据_num,而在D类中想要访问_num数据时,不能明确的肯定访问的是BC里边的哪一个_num数据。
解决方法:
1.显示的指定访问内容
2.加virtual关键字,虚拟继承

class A
{
public:
    int _num;
};
class B: virtual public A
{};
class C: virtual public A
{};
class D:public B,public C
{};

虚拟继承的原理:
在D类对象的内存存储中,将所继承的A放在最下面,让B通过一个虚基表指针去访问存储了B到A的偏移量的虚基表,这样就可以通过偏移量找到下边存储的A了。C也是同理。
注意:虚继承因为会引入间接性指针,因此其大小会增加四个字节的大小。

八.继承与组合的比较

1.public继承是一种is-a的关系,即每个子类对象都是一个基类对象。
2.组合是一种has-a的关系,即B组合了A,每个B对象中都会有一个A对象。
3.继承是一种白箱复用,派生类可见基类的内部细节,在一定程度上破环了基类的封装性。基类的改变会直接对派生类产生巨大的影响,耦合度高。
3.组合是一种黑箱复用,组合之间依赖性不高,因此耦合度低。
4.除非必要,尽量使用组合(例如:多态的实现必须使用继承)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值