【C++】继承

目录

继承的基本概念

继承的使用

成员隐藏

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

派生类的默认成员函数

构造函数

拷贝构造

赋值重载

析构函数

特殊成员的继承

友元函数

静态成员

多继承

菱形继承

虚继承原理

菱形继承的分析

其他方案 


继承的基本概念

在使用类时,我们将其作为一个抽象的概念来描述一类事物,从而实现对一类事物的管理。例如几类之间只有毫厘之差,只有个别的成员不同,若是每个类都重复规定变量,未免显得过于麻烦。

【C++】继承_第1张图片

有没有一种方式,让我们在这个共同成员的基础上增加成员?那就不得不介绍面向对象三大特征中的继承了!!

我们找到并归纳三者的共通点成人这个类,之后通过像是继承家产那样,继承该类的成员,其中被继承的类我们称之为父类或基类,继承下来的类称为子类或派生类

【C++】继承_第2张图片

继承的使用

落实到代码之中十分简单,在声明类的时候加上要继承的类以什么方式继承即可。具体继承方式看下表。

类成员/继承方式 public 继承 protected 继承 private 继承
基类的public成员 派生类的 public 成员 派生类的 protected 成员

派生类的 private 成员

基类的protected
成员
派生类的 protected 成员 派生类的 protected 成员 派生类的 private 成员
基类的private成
在派生类中不可见 在派生类中不可见

在派生类中不可见

虽然说这个看起有些麻烦,其实只要记住小小取小这个口诀即可,但一般也只使用public继承。

在学继承之前,类中 protected 和 private 两个访问限定符基本上没有差别,但在继承中,private 成员在子类中是不可见的。因此,我们若是定义一个类的成员同时想要其能被派生类访问,最好不要将其设置成 private 成员

enum sex
{
	male,
	female
};

class Person    //定义基类
{
public:
protected:
	string name;
	string tel;
	string address;
	enum sex;
};

class Student : public Person  //定义派生类
{
public:
protected:
	int code;
};

【C++】继承_第3张图片

成员隐藏

之前我们讲过,类拥有自己的作用域,而继承下来的成员与该类本身的成员并不在同一个作用域中。

若类本身的成员与继承下来的成员名字相同,为了避免歧义便会构成成员隐藏,子类成员将屏蔽对父类同名成员的直接访问,优先访问自己的成员

class math
{
public:
	int mymath(int a, int b)
	{
		return a + b;
	}
};

class sub : public math
{
public:
	int mymath(int a, int b)
	{
		return a - b;
	}
};

int main()
{
	sub s;
	cout << s.mymath(5, 1);
	return 0;
}

如上述代码中,基类有一个 mymath 函数进行两数相加,而派生类同样拥有一个同名函数进行两数相减,这种情况下便会出现成员隐藏,因此当我们通过类调用函数,最后调用的便是两数相减的函数

但是成员的隐藏并不是只保留一份,我们还是可以通过指定类域达到访问基类的成员的效果。

【C++】继承_第4张图片

为了避免出现不必要的问题,最好不要定义同名的成员。 

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

当派生类赋值给基类时,编译器会找到二者相同的部分进行拷贝,该行为称作切片。同时,这个过程中并不会发生类型转换

【C++】继承_第5张图片

若是使用引用接收的话,该引用的部分就是子类中父类部分的别名。下图中两个成员的指针都是一样的。

【C++】继承_第6张图片

不仅如此,继承中的赋值是只能由子类赋值给父类,而父类不能赋值给子类。

【C++】继承_第7张图片

同样,父类指针可以用于接收子类地址,而父类地址经过强制类型转换后一样能够被子类指针接收。

【C++】继承_第8张图片

派生类的默认成员函数

构造函数

经过上面的讲解,我们知道派生类继承了基类的成员,那么具体是如何做到的呢?下面我们一起来看看派生类的默认成员函数

class Person
{
public:
	Person(string name = "")
		:_name(name)
	{}
	Person(Person& p)
		:_name(p._name)
	{}
protected:
	string _name;
};

class Student : public Person
{
public:
	Student(string name = "", int code = 0)
		:Person(name)         //初始化列表中调用基类的构造函数进行初始化
		,_code(code)
	{}
protected:
	int _code;
};

在派生类的构造函数中,基类的成员必须调用基类的构造函数进行初始化,否则便会报错。

【C++】继承_第9张图片

即便我们并未实现该类的构造函数,但在子类的初始化列表中还是会自动地调用基类的默认构造。

拷贝构造

理解了上面的构造函数,接下来的拷贝构造就是小菜一碟,我们需要用父类的拷贝构造拷贝父类部分。

其实在这里还发生了切片,基类拷贝构造的参数为基类的引用,我们传了派生类的引用过去,自然发生了切片。

Person(const Person& p)     //传子类引用时发生切片
	:_name(p._name)
{}

Student(const Student& s)
    :Person(s)        //调用父类的拷贝构造
	,_code(s._code)   //初始化自己的成员
{}

赋值重载

这里我们若是直接这样写的话则会出错并显示栈溢出

Person operator =(const Person& p)
{
	_name = p._name;
	return *this;
}

Student operator =(const Student& s)
{
	if (this != &s)
	{
		operator=(s);
		_code = s._code;
	}
	return *this;
}

通过调用堆栈窗口,我们看到编译器在反复调用运算符重载这个函数,细心的同学应该已经注意到了,我们写的两个函数都是都一个名字,因此构成了隐藏,因此每次走到调用赋值重载时调用的都是自己。 

【C++】继承_第10张图片

只要在使用前限定类域之后,我们便可以调用到基类的赋值重载了,这下就完成了赋值重载的实现。

Student operator =(const Student& s)
{
	if (this != &s)
	{
		Person::operator=(s);
		_code = s._code;
	}
	return *this;
}

【C++】继承_第11张图片

析构函数

若是只是这样写的话,编译器会直接报错,这是因为析构函数经过处理后函数名都会变成 destructor,于是两个析构函数便构成了隐藏

【C++】继承_第12张图片

不过我们也可以通过限定访问限定符指定调用父类的析构函数,但这时又出现一个问题,父类的析构被多调用了一次。 

【C++】继承_第13张图片

这是因为,子类析构函数结束时,编译器会自动调用其父类的析构函数,并不需要我们显式调用。也说明了:

继承的析构函数中,我们得先析构子类部分的成员,再析构父类成员

正如构造类时,子类初始化完成前要先调用父类的构造函数,这也是借用了的思想。于是在析构的时候自然先析构子类再析构父类。

【C++】继承_第14张图片

也可以这样理解,子类中可能涉及调用父类内容,而父类中必定不会调用子类的内容,因此先释放子类。 

特殊成员的继承

友元函数

父类的友元函数并不会继承给子类,如下代码中可以看到,在访问子类成员时便出现了问题。

【C++】继承_第15张图片

若想子类也能够被该函数访问到的话,则需要在子类中再次进行友元的声明。

静态成员

静态变量则于友元不同,是会被继承下来的,此时这个静态变量则变成了子类与父类都共享

例如此时我们就可以通过静态成员记录当前所有相关类的数量(只展现了父类的写法,子类就是普通的继承)。

class Person
{
public:
    Person(string name = "")
		:_name(name)
	{
		_count++;
	}
    ~Person()
	{
		_count--;
	}
    int getcount()
	{
		return _count;
	}
protected:
	string _name;
	static int _count;
};

int Person:: _count = 0;

int main()
{
	Person p1;
	Person p2;
	Student s1;
	cout << p1.getcount() << endl;
	return 0;
}

无论是子类还是父类调用构造函数时,都必定会调用到父类的构造函数, 因此只要构造一个类我们就将计数加一,当类调用析构函数时便将计数减一,此时计数的数字便是当前子类成员与父类成员的总和。

多继承

继承中我们不仅可以继承一次,也可以多次继承

如下,我们在上方代码的基础上再次进行继承,可以看到该类根据规则继承了父类的成员

class Graduate : public Student
{
protected:
	string _major;
};

【C++】继承_第16张图片

但是过多的继承也不一定是件好事,一不小心可能就会变成菱形继承并造成数据的冗余。

菱形继承

菱形继承,顾名思义它继承的路线类似于菱形,实际上就是一个类同时继承了两个有相同基类的派生类

此时的类中会有很多重复冗余的数据,从而造成了空间的浪费。

【C++】继承_第17张图片【C++】继承_第18张图片 

【C++】继承_第19张图片

不仅如此,在默认使用时会出现二义性,编译器并不知道访问的是其中哪个变量。只有通过访问限定符才能找到对应变量。但实际上还是没有解决空间冗余的问题。

【C++】继承_第20张图片

在后期的规范中,引入了虚继承来解决菱形继承带来的问题,在类的声明中加上 virtual 就能够进行虚继承。

例如 class Student : virtual public Person

使用虚继承后,重复继承的成员就变成了同一个。

虚继承原理

为了方便演示,这里简单写了一个菱形继承进行讲解。

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;
};

此时还未使用虚继承,类内部的内容是这样的(当前为32位环境)。 

【C++】继承_第21张图片【C++】继承_第22张图片

可以看到从 B 类继承下来的部分中有一个 A 类,同样 C 类中也有 A 类的数据,同样的数据根本就不需要两份,不仅造成数据冗余,同时在访问上也给我们造成了困扰

当我们使用虚继承后,内存上的数据好像发生了些许的变化。

 【C++】继承_第23张图片

可以看到,A 类的部分在整个类的最下方,而原来位置则是一个指针。打开其中一个指针我们可以看到其中的内容。

其中的数字并非十进制,而是十六进制,因此其中存的数字其实是 20,这时我们便发现从存储当前指针的位置往下走 20 个字节后恰好是 A 类部分的位置。 

为了能够找到共有的成员,因此我们在其原来的位置放了地址,而地址的内部记录偏移量,帮助我们找到原来的变量。

看到这里你可能会充满疑惑,这样子看起来使用虚继承是否消耗更多了呢?既多存了指针,指针内部还有数据。

实际上,由于同一类中各个对象中变量的偏移量都相等,因此都共用指针指向的一张表,大大地减少了内存的消耗。

而表中的偏移量只有一个,之后根据编译器内存对齐的规则便能访问到所有成员。

菱形继承的分析

菱形继承的危害往往比我们想象中来得复杂,最后我们一起看看下面这个例题吧。

//菱形继承的危害
class A
{
public:
	A(string s1 = "")
	{
		cout << s1 << endl;
	}
};

class B : virtual public A
{
public:
	B(string s1 = "", string s2 = "")
		:A(s1)
	{
		cout << s2 << endl;
	}
};

class C : virtual public A
{
public:
	C(string s1 = "", string s3 = "")
		:A(s1)
	{
		cout << s3 << endl;
	}
};

class D :  public C ,public B
{
public:
	D(string s1, string s2, string s3, string s4)
		: B(s1, s2), C(s1, s3), A(s1)
	{
		cout << s4 << endl;
	}
};

int main()
{
	D* d = new D("class A", "class B", "class C", "class D");
	delete d;
	return 0;
}

可以看到,我们实例化了一个 D 对象,当调用对应类的构造函数时便会输出当前类的名字,那么最后输出的结果会是什么样子呢?

首先,调用的肯定是 D 的构造函数,但在打印语句前要先走初始化列表中的内容,其中自然是通过调用基类的构造函数了,由于我们使用了虚继承,A 类只会初始化一次,同时在多继承中,继承部分初始化的顺序取决于声明中的先后顺序与初始化列表无关

在 D 的继承声明中,先写的是继承 C 才是继承 B 因此调用的时候便是先 C 后 B,所以最后的结果便是 ACBD。

其他方案 

使用多继承的时候自然要十分小心,但我们也可以使用组合的方法达到类似的效果。

所谓组合就是在新的类中以成员的形式包含原来的类。

class A
{
public:
	int _a;
};

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

使用继承时父类的内容就是我的内容,该操作耦合度高,但是可以直接使用所有的成员。

使用组合便是一种包含的关系,降低了耦合度,通过一个成员来访问其中的其他成员。 

至于究竟使用哪一种方式还是见仁见智,最好还是减少多继承的使用,因为万一写出了菱形继承处理起来会相当的麻烦。


好了,今天 【C++】继承 的相关内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。

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