目录
继承的基本概念
继承的使用
成员隐藏
基类与派生类对象的赋值转换
派生类的默认成员函数
构造函数
拷贝构造
赋值重载
析构函数
特殊成员的继承
友元函数
静态成员
多继承
菱形继承
虚继承原理
菱形继承的分析
其他方案
继承的基本概念
🥑在使用类时,我们将其作为一个抽象的概念来描述一类事物,从而实现对一类事物的管理。例如几类之间只有毫厘之差,只有个别的成员不同,若是每个类都重复规定变量,未免显得过于麻烦。
🥑有没有一种方式,让我们在这个共同成员的基础上增加成员?那就不得不介绍面向对象三大特征中的继承了!!
🥑我们找到并归纳三者的共通点成人这个类,之后通过像是继承家产那样,继承该类的成员,其中被继承的类我们称之为父类或基类,继承下来的类称为子类或派生类。
继承的使用
🥑落实到代码之中十分简单,在声明类的时候加上要继承的类和以什么方式继承即可。具体继承方式看下表。
类成员/继承方式 | 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;
};
成员隐藏
🥑之前我们讲过,类拥有自己的作用域,而继承下来的成员与该类本身的成员并不在同一个作用域中。
🥑若类本身的成员与继承下来的成员名字相同,为了避免歧义便会构成成员隐藏,子类成员将屏蔽对父类同名成员的直接访问,优先访问自己的成员。
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
🥑如上述代码中,基类有一个 mymath 函数进行两数相加,而派生类同样拥有一个同名函数进行两数相减,这种情况下便会出现成员隐藏,因此当我们通过类调用函数,最后调用的便是两数相减的函数。
🥑但是成员的隐藏并不是只保留一份,我们还是可以通过指定类域达到访问基类的成员的效果。
🥑为了避免出现不必要的问题,最好不要定义同名的成员。
基类与派生类对象的赋值转换
🥑当派生类赋值给基类时,编译器会找到二者相同的部分进行拷贝,该行为称作切片。同时,这个过程中并不会发生类型转换。
🥑若是使用引用接收的话,该引用的部分就是子类中父类部分的别名。下图中两个成员的指针都是一样的。
🥑不仅如此,继承中的赋值是只能由子类赋值给父类,而父类不能赋值给子类。
🥑同样,父类指针可以用于接收子类地址,而父类地址经过强制类型转换后一样能够被子类指针接收。
派生类的默认成员函数
构造函数
🥑经过上面的讲解,我们知道派生类继承了基类的成员,那么具体是如何做到的呢?下面我们一起来看看派生类的默认成员函数。
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;
};
🥑在派生类的构造函数中,基类的成员必须调用基类的构造函数进行初始化,否则便会报错。
🥑即便我们并未实现该类的构造函数,但在子类的初始化列表中还是会自动地调用基类的默认构造。
拷贝构造
🥑理解了上面的构造函数,接下来的拷贝构造就是小菜一碟,我们需要用父类的拷贝构造拷贝父类部分。
🥑其实在这里还发生了切片,基类拷贝构造的参数为基类的引用,我们传了派生类的引用过去,自然发生了切片。
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;
}
🥑通过调用堆栈窗口,我们看到编译器在反复调用运算符重载这个函数,细心的同学应该已经注意到了,我们写的两个函数都是都一个名字,因此构成了隐藏,因此每次走到调用赋值重载时调用的都是自己。
🥑只要在使用前限定类域之后,我们便可以调用到基类的赋值重载了,这下就完成了赋值重载的实现。
Student operator =(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_code = s._code;
}
return *this;
}
析构函数
🥑若是只是这样写的话,编译器会直接报错,这是因为析构函数经过处理后函数名都会变成 destructor,于是两个析构函数便构成了隐藏。
🥑不过我们也可以通过限定访问限定符指定调用父类的析构函数,但这时又出现一个问题,父类的析构被多调用了一次。
🥑这是因为,子类析构函数结束时,编译器会自动调用其父类的析构函数,并不需要我们显式调用。也说明了:
🥑继承的析构函数中,我们得先析构子类部分的成员,再析构父类成员。
🥑正如构造类时,子类初始化完成前要先调用父类的构造函数,这也是借用了栈的思想。于是在析构的时候自然先析构子类再析构父类。
🥑也可以这样理解,子类中可能涉及调用父类内容,而父类中必定不会调用子类的内容,因此先释放子类。
特殊成员的继承
友元函数
🥑父类的友元函数并不会继承给子类,如下代码中可以看到,在访问子类成员时便出现了问题。
🥑若想子类也能够被该函数访问到的话,则需要在子类中再次进行友元的声明。
静态成员
🥑静态变量则于友元不同,是会被继承下来的,此时这个静态变量则变成了子类与父类都共享。
🥑例如此时我们就可以通过静态成员记录当前所有相关类的数量(只展现了父类的写法,子类就是普通的继承)。
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
🥑无论是子类还是父类调用构造函数时,都必定会调用到父类的构造函数, 因此只要构造一个类我们就将计数加一,当类调用析构函数时便将计数减一,此时计数的数字便是当前子类成员与父类成员的总和。
多继承
🥑继承中我们不仅可以继承一次,也可以多次继承。
🥑如下,我们在上方代码的基础上再次进行继承,可以看到该类根据规则继承了父类的成员。
class Graduate : public Student
{
protected:
string _major;
};
🥑但是过多的继承也不一定是件好事,一不小心可能就会变成菱形继承并造成数据的冗余。
菱形继承
🥑菱形继承,顾名思义它继承的路线类似于菱形,实际上就是一个类同时继承了两个有相同基类的派生类。
🥑此时的类中会有很多重复冗余的数据,从而造成了空间的浪费。
🥑不仅如此,在默认使用时会出现二义性,编译器并不知道访问的是其中哪个变量。只有通过访问限定符才能找到对应变量。但实际上还是没有解决空间冗余的问题。
🥑在后期的规范中,引入了虚继承来解决菱形继承带来的问题,在类的声明中加上 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位环境)。
🥑可以看到从 B 类继承下来的部分中有一个 A 类,同样 C 类中也有 A 类的数据,同样的数据根本就不需要两份,不仅造成数据冗余,同时在访问上也给我们造成了困扰。
🥑当我们使用虚继承后,内存上的数据好像发生了些许的变化。
🥑可以看到,A 类的部分在整个类的最下方,而原来位置则是一个指针。打开其中一个指针我们可以看到其中的内容。
🥑其中的数字并非十进制,而是十六进制,因此其中存的数字其实是 20,这时我们便发现从存储当前指针的位置往下走 20 个字节后恰好是 A 类部分的位置。
🥑为了能够找到共有的成员,因此我们在其原来的位置放了地址,而地址的内部记录偏移量,帮助我们找到原来的变量。
🥑看到这里你可能会充满疑惑,这样子看起来使用虚继承是否消耗更多了呢?既多存了指针,指针内部还有数据。
🥑实际上,由于同一类中各个对象中变量的偏移量都相等,因此都共用指针指向的一张表,大大地减少了内存的消耗。
🥑而表中的偏移量只有一个,之后根据编译器内存对齐的规则便能访问到所有成员。
菱形继承的分析
🥑菱形继承的危害往往比我们想象中来得复杂,最后我们一起看看下面这个例题吧。
//菱形继承的危害
class A
{
public:
A(string s1 = "")
{
cout
🥑可以看到,我们实例化了一个 D 对象,当调用对应类的构造函数时便会输出当前类的名字,那么最后输出的结果会是什么样子呢?
🥑首先,调用的肯定是 D 的构造函数,但在打印语句前要先走初始化列表中的内容,其中自然是通过调用基类的构造函数了,由于我们使用了虚继承,A 类只会初始化一次,同时在多继承中,继承部分初始化的顺序取决于声明中的先后顺序,与初始化列表无关。
🥑在 D 的继承声明中,先写的是继承 C 才是继承 B 因此调用的时候便是先 C 后 B,所以最后的结果便是 ACBD。
其他方案
🥑使用多继承的时候自然要十分小心,但我们也可以使用组合的方法达到类似的效果。
🥑所谓组合就是在新的类中以成员的形式包含原来的类。
class A
{
public:
int _a;
};
class B
{
public:
class A;
int _b;
};
🥑使用继承时父类的内容就是我的内容,该操作耦合度高,但是可以直接使用所有的成员。
🥑使用组合便是一种包含的关系,降低了耦合度,通过一个成员来访问其中的其他成员。
🥑至于究竟使用哪一种方式还是见仁见智,最好还是减少多继承的使用,因为万一写出了菱形继承处理起来会相当的麻烦。
🥑好了,今天 【C++】继承 的相关内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
在当今信息化高速发展的时代,推荐系统是一个热门的话题和技术领域,一些云厂商也提供了推荐系统的SaaS服务比如亚马逊云科技的 Amazon Personalize 来解决客户从无到有迅速构建推荐系统的痛点和难点。在我们的日常生活中,推荐系统随处可见,比如我们经常…