C++继承

文章目录

  • 继承
    • 父类和子类对象赋值转换
    • 继承中的作用域
    • 子类的默认成员函数
  • 继承的种类
  • 继承和组合
  • 总结


继承

介绍:

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

继承之后的父类成员(成员函数+成员变量)都会称为子类的一部分

  • 继承方式:只是表示继承基类成员之后的权限,是否子类可以调用。如果是public,对于基类中public和protected的内容可以调用
  • 继承基类除去友元函数之外的所有内容,不论修饰符是什么,子类中都拥有,只是能否使用,能否看到的问题。
  • 整个继承体系中,静态成员只有一份,不能放在子类中,子类可以根据修饰符判断是否能使用
  • 静态成员函数不能被重写,即不能被virtual修饰
  • 继承之后,存放在子类中的是基类的变量,如果有虚函数,还会有虚表的存在
class Person
{
public:
	int _name;
	int _age;
};

//继承的语法就是 : 修饰符 父类 
class Student : public Person
{
public:
	int _id;
};

int main()
{
	Person p;
	Student s;
	
	return 0;
}

C++继承_第1张图片

继承定义:

C++继承_第2张图片

public:可以被类中,子类、类外访问

protected:可以被类中,子类访问

private:只能在当前类中访问

类中成员/成员函数的修饰符与继承方式相互组合,有3x3=9种组合方式,组合效果为修饰符和继承方式范围更小的那个,public>protected>private , 如public与protected,继承结果为protected.

private和protected在继承之前是没有什么区别的,继承的时候,父类private修饰的成员不能被继承,protected继承方式得到的成员变量能再被其子类继承,而private不可以(断代)。

C++继承_第3张图片

最为常用的是public继承。

总结:

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

  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符(protected)是因继承才出现的。

  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected>private。

  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。

  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

父类和子类对象赋值转换

子类对象可以赋值给父类的对象、指针、引用。称为赋值兼容转换,又称切片,将子类中父类拥有的成员变量,赋值给父类对象

C++继承_第4张图片

class Person
{
public:
	void Print()
	{
		cout << _name << " " << _age << endl;
	}
	Person(const string& name = "张三", const int age = 18)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p)
	{
		cout << "Person拷贝" << endl;
	}
	string _name;	
	int _age;
};

class Student : public Person
{
public:
	Student()
		:Person()
		,_Stuid(0)
	{}
	Student(const Student& s)
		:Person(s)
	{
		cout << "Student拷贝" << endl;
	}
protected:
	int _Stuid;
};
int main()
{
	Person p;
	p.Print();
	Student s;
	p = s;  //向上转型
	Person* p1 = &s;  //指针
	Person& p2 = s;   //引用
	//s = p;  不能直接向上转型,因为s中可能有特有的成员变量,我们想要实现向下转型,应该先向上转型,然后再向下转型,实现向下转型的目的是为了使用子类的方法
	
	//基类的指针可以通过强制类型转换赋值给派生类的指针
	Person* ptr;
	ptr = &s;
	ptr->_name = "李四";

	Student* s1 = (Student*)ptr; //这种情况转换时虽然可以,但是会存在越界访问的问题
	s1->_age = 10;
	return 0;
}

继承中的作用域

在继承中,根据就近原则,如果当前类中没有,就去父类中寻找,然后再去外部寻找(全局变量)

当子类和父类中拥有同名成员变量/成员函数的时候,我们根据就近原则,先访问子类,隐藏父类对应的成员,这叫做重定义(隐藏)

如果指定访问哪一个作用域中的成员,加上父类名:成员名,使得编辑器按照指定作用域去寻找,如果没找到,报错

由上所述,实际中在继承体系里面,最好不要定义同名的成员

class Person
{
public:
	void Print()
	{
		cout <<"Person:"<< _name << " " << _age << endl;
	}
	Person(const string& name = "张三", const int age = 18)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p)
	{
		cout << "Person拷贝" << endl;
	}
	string _name;
	int _age;
};

class Student : public Person
{
public:
	Student()
		:Person()
		, _Stuid(0)
	{}
	Student(const Student& s)
		:Person(s)
	{
		cout << "Student拷贝" << endl;
	}
	void Print(int num)  //此时Student和Person中的Print的参数不同,这是重载吗?  实际上不是的,因为重载是在同一作用域中的,这只是隐藏/重定义
	{
		cout << "Student:" << _name << " " << _age << " " << _Stuid << endl;
	}
protected:
	int _Stuid;
};

int main()
{
	Student s;
	s.Print(0);  //优先访问子类Student中的Print函数
	s.Person::Print();  //指定作用域去寻找Person中的Print方法
	return 0;
}

子类的默认成员函数

  1. 子类的构造函数必须调用父类的构造函数初始化基类的那一部分成员如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用
  2. 子类的拷贝构造函数先调用父类的拷贝构造函数,完成父类的拷贝初始化
  3. 子类的operator=必须要调用父类的operator=完成父类的赋值。此时使用父类名:operator=的方式指定调用父类的operator=拷贝构造。
  4. 因为子类能访问父类成员,父类不能访问子类,所以万一子类中析构函数需要对于父类成员进行访问,我们此时先析构父类,那么就会导致子类无法访问该类成员,于是,规定先调度子类的析构函数,再调度父类的析构函数。

友元关系不能被继承,友元只是和父类的朋友关系,当然不能和子类称为朋友,所以对于子类,友元只能访问子类的public成员

父类定义的static静态成员,在整个继承体中只有一个static,也就是说,子类修改该成员,父类也会跟着变化。

class Person
{
public:
	Person()
	{
		num++;
	}
	void Print()
	{
		cout << num << endl;
	}
public:
	static int num;
};
int Person::num = 0;
class Student : public Person
{};

int main()
{
	//我们直到static在所有的Person对象和Student中只有一份,相当于放在静态区,大家都是直接访问该地址
	//记录子类和父类的个数
	Person p;
	Student s;
	cout << p.num << endl;
	return 0;
}

继承的种类

在C++中有单继承也有多继承。

多继承语法:多个父类用逗号隔开

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

C++继承_第5张图片

菱形继承:是一种特殊的多继承,只要在整个继承体系中多个类拥有共同的父类,那么就形成菱形继承

多继承带来的问题为:数据冗余和二义性

C++继承_第6张图片

为了解决菱形继承的问题,引入虚拟继承的概念。

虚拟继承:使用virtual关键字在导致菱形继承(重复继承A类)的多个类继承前加上virtual

C++继承_第7张图片

使用虚拟继承的方式,设置虚基表,在表中放置偏移量,我们根据偏移量就能找到A类成员的实际位置,这样就不会导致二义性的问题,也不会数据冗余,这个时候实际上只有一份A类成员变量。

虚基表:存放基类偏移量的表,本文是距离A类偏移量(相对距离)

继承和组合

  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。比如学生也是人
  • 组合是一种has-a 的关系,在B类中组合了A,就是B的成员变量中有A对象,比如轮胎和车
  • 优先选择组合而不是继承
    • 继承中基类很大程度上影响到派生类的实现,这一种复用叫做白箱复用,也就是我们在提前直到基类中成员,以此来设计派生类(要提前看懂基类成员意义),白箱(能看到)。基类的细节派生类能观察到,继承在一定程度上破坏了基类的封装,由于基类和派生类关联性太高,牵一发而动全身,也可以称为耦合度高。
    • 组合的方式,是另一种形式的复用,将另一个类的对象设计为当前类的成员变量,以此我们可调用另一个类的public的方法,但是我们不需要去了解另一个类实现原理,其改变并不会很影响当前类,也成为耦合度低
    • 所以优先使用组合,除了涉及到多态的时候我们必须使用继承,其他类之间的关系我们尽量使用组合的形式实现复用。

总结

多继承是一个C++语言设计的缺陷,虽然多继承确实更加贴合现实本身,但是菱形继承(多继承特殊情况)的出现导致数据冗余和二义性,以此祖师爷设计了虚基表(virtual关键字)的概念,引出了菱形虚拟继承。

关于继承方式,我们是否继承了private的内容?

实际上,我们继承了除去静态成员以外的所有内容,包括private修饰的成员,只是我们不能看到他,也不能调用他,静态成员只有一份,在静态区,在父类中,不会继承到子类中。

笔试面试题:

  • 什么是菱形继承?菱形继承的问题是什么?

菱形继承是多继承的一种特殊情况,由于多个类指向同一基类,并多继承给同一子类,由此形成酷似菱形的继承。菱形继承的问题是数据冗余和二义性

  • 什么是菱形虚拟继承?如何解决数据冗余和二义性?

菱形虚拟继承是在造成菱形继承的类的继承方式前加入virtual关键字,使得虚继承基类。通过虚基表的方式来解决数据冗余和二义性。

  • 继承和组合的区别?什么时候用继承?什么时候用组合?

继承的耦合度高,组合的耦合度低,在一般情况下尽量使用组合的方式,代码好维护,在涉及到多态的情况下必须使用继承。

你可能感兴趣的:(C++,c++,开发语言,继承)