1

初识C++继承_玄鸟轩墨的技术博客_51CTO博客

 1 year ago
source link: https://blog.51cto.com/u_15132397/5493321
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

初识C++继承

原创

玄鸟轩墨 2022-07-20 15:34:21 博主文章分类:C++ ©著作权

文章标签 父类 子类 c++ 继承 组合 文章分类 C/C++ 编程语言 yyds干货盘点 阅读数155

在谈着这个之前,我们需要先说说C++的几大特性,封装继承,多态...注意,实不置这三种,只不过他们是基础罢了,大家面试的时候注意一点.我们已经学过了封装,今天就开始继承吧,我们最好按照简单的学习来,这里的语法可能有点难,但是我们用的时候一定要偏简单一点.


说实话,C++的那些大佬也考虑了很多方式,把继承搞得很复杂,管是继承方式就有三种,所以后面的语言尽力把这个知识点给简化了,我们学习C++确实需要些时间来思考.多的不说,现在看看继承究竟是什么.

什么是 继承

我查了一些资料,里面对继承的概念简述的还是比较详细的.

继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。(来源:维基百科).

我举一个不太恰当的例子,当张三的父亲离世后,张三继承了父亲的遗产,这里张三就是"子类",其父亲便是"父类",张三拥有父亲的遗产,但是除此之外他可能也有自己的财富。

为何要 继承

我们可以举一个例子来帮助我们理解,假设我们要做一个学校人员管理系统,在一个学校里面是存在老师,学生...角色的,我们要是给每一个角色都封装一个类,想想每一个类里面都存在名字,年龄...等相同的成员变量,想想都头疼,假如我们把这个相同的属性拿出来,单独作为一个类,让其他的类继承它不就可以了吗.这就是继承的作用,代码复用,避免重复造轮子.

初识C++继承_c++

C++的继承可以说是让人头疼,大佬考虑的是在是太复杂了,继承方式有3种,每一种继承方式对于不同访问修饰限定符有不一样.我们需要来仔细看看.我们先等会再说继承的方式,先来看看.

class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protectedd:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};

class Student : public Person
{
protectedd:
int _stuid; // 学号
};

int main()
{
Student stu;
stu.Print();
return 0;
}
初识C++继承_子类_02

继承了父类的什么

这里我现给大家一个不恰当的结论,可以这么说,子类继承了父类的成员函数和成员变量.这里面也是存在很大的问题的.

class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
public:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};

class Student : public Person
{
protectedd:
int _stuid; // 学号
};

int main()
{
Student stu;
stu._name = "张三";
stu._age = 20;
stu.Print();
return 0;
}
初识C++继承_c++_03

前面我就说了,看到C++的继承方式我就感到一镇头疼,要知道我们访问修饰限定符也是存在三种的,这一计算就是九种情况.这个我们很多人都带来了巨大的困难.

初识C++继承_父类_04
初识C++继承_父类_05

类成员/继承方式

public继承

protected继承

private

public 修饰

子类 public 成员

子类 protected成员

子类 private成员

protected 修饰

子类 protected成员

子类 protected成员

子类 private成员

private修饰

在派生类中不可见

在派生类中不可见

在派生类中不可见

大家想不要慌,这九种情况还是很好区分的,我们可以得到下面的两条结论.

  • private 成员 无论是什么继承方式 在 子类种不可见
  • 其余的成员是 父类成员与继承方式相比较 权限较小的那一个 权限比较 public > protected > private

关于这个情况,我们不用担心,一般都是public继承,父类里面大多是protected成员,很少使用其他的.

不可见 VS 没有继承

我们需要看看究竟什么是不可见,什么是没有继承.没有继承可以了解,这里主要看看什么是不可见.

所谓的不可见是你在子类中无法直接访问这个类型,但是有确确实实的继承了.

初识C++继承_父类_06
初识C++继承_父类_07

那么这里就有一个问题了,我们是不是可以间接的去访问这个成员啊,是的,我们可以通过函数来访问,这里就不和大家分享了,下去有兴趣的话可以自己试试.

对于继承,父子类之间具有很多的特性,其中比较关键的有两个。

我们可以这么认为,父类是可以接受子类的类型的,这就像一个水到渠成的事,不是什么类型的转化,类似一种父亲可以教导孩子,这也可以认为是向上调整.这也是后面多态的基础.

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去 .

我们可以看看下面的例子.

初识C++继承_c++_08
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};

int main()
{
Person per;
Student stu;
per = stu;

Person& p = stu;
Person* p = &stu;
return 0;
}

这里要和大家提一下,这个切片是有要求的,子类继承父类的方式必须是pubilic继承,其他的方式是不可以的.

初识C++继承_继承_09

我分别说一下它们的情况,里面有一点细节要来分享一下.

对于直接把子类赋值给父类,这是会调用父类的赋值函数,编译器会自动把从属父类的内容那一部分给切片了,回去赋值给父类对象.

class Person
{
public:
void operator=(Person& per)
{
cout << "void operator=()" << endl;
}
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};

int main()
{
Person per;
Student stu;
per = stu;
return 0;
}
初识C++继承_组合_10

如果是用父类去引用子类的对象,就相当于给子类对象里面的属于父类的取了一个别名.

class Person
{
public:
string _name = "张三"; // 姓名
string _sex = "男"; // 性别
int _age = 18; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};

int main()
{
Student stu;
Person& per = stu;
per._age++;
return 0;
}
初识C++继承_父类_11

指针更好理解,指针指向的就是子类当中属于父类的那些东西.

int main()
{
Student stu;
Person& per = stu;
Person* p = &stu;

cout << stu._age << endl;

per._age++;
cout << stu._age << endl;

p->_age++;
cout << stu._age << endl;
return 0;
}
初识C++继承_c++_12

子类可以接受父类吗

这个普通的方法是不可以的,在Java中这叫线下转型,但是C++里面还不太性,但是通过指针可以,这里先按下不表,后面多态的时候再和大家分享.

隐藏也是C++里面一个重要的概念,大家都知道语言里面是有作用域这个概念的,类也有类域,同一个类里面不能定义同名的成员变量,那么父类和子类是两个个类域,这里就可以定义一样的变量了,编译器优先使用子类的变量,这就是隐藏,其中成员函数也是如此.

子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定 义。(在子类成员函数中,可以使用 基类::基类成员 显示访问

这里给几个结论

  • 子类成员将屏蔽父类对同名成员的直接访问
  • 成员函数的隐藏,只需要函数名相同就构成隐藏

我们先来看看成员变量,主要看看如何访问父类的同名的变量该如何访问.

class Person
{
protected:
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};

class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 身份证号:" << Person::_num << endl; // 调用 父类 里面的隐藏的 变量
cout << " 学号:" << _num << endl;
}
protected:
int _num = 999; // 学号
};

int main()
{
Student stu;
stu.Print();
return 0;
}
初识C++继承_父类_13

这里我也给一个结论,如果我们要访问一个成员变量,编译器优先调用子类里面的,如果子类里面不存在,那么就去父类里面找,要是我们确实想想用父类里面的,就直接在用类域来声明.

现在变量已经说完了,我们现在可以谈谈函数的隐藏了,再谈隐藏之前,我们先问问一个问题,fun()和fun(int i)构成什么关系?记住这个一定不是函数重载,函数重载是需要在同一个作用域的.这是构成了隐藏,成员函数的隐藏,只需要函数名相同就构成隐藏.

class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
初识C++继承_组合_14

子类的默认成员函数

6个默认成员函数, **”**的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?这些默认函数确实有点问题,里面是比较困难的.这里我们先来和大家提出一个易于下面的理解的想法.我们把子类里面继承的父类作为一个成员变量,而且是第一个首先声明的成员变量.这个可能会帮助大家理解.这里面还是主要分享4个比较重要的函数.

子类实例化对象的时候,必须先把父类给默认构造函数给调用(或者显示调用)出来.这一点是我们需要知道的.

class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
protected:
string _name; // 姓名
};

class Student : public Person
{
public:
Student(const char* name, int num)
:_num(num)
{
cout << "Student()" << endl;
}
protected:
int _num; //学号
};

int main()
{
Student s1("jack", 18);
return 0;
}
初识C++继承_继承_15

显示调用父类构造函数

如果我们想要调用父类的构造函数,我们该如何去做?

我们直接 在初始化列表里面去初始化父类的成员会怎么样? 看到答案是不行的

class Person
{
public:
Person(const char* name = "peter")
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name; // 姓名
};

class Student : public Person
{
public:
Student(const char* name, int num)
:_name(name)
,_num(num)
{
cout << "Student()" << endl;
}
protected:
int _num; //学号
};
初识C++继承_继承_16

我们把父类的成员在构造函数体内进行再次赋值,我们发现这是可以的,但是这里我们需要知道原理,初始化列表是声明和定义的地方,函数体内是再次赋值结果也证明了,在初始化列表里面是调用了父类的默认构造函数的.

class Student : public Person
{
public:
Student(const char* name, int num)
:_num(num)
{
_name = name; // 再次 赋值
cout << "Student()" << endl;
}
protected:
int _num; //学号
};
初识C++继承_子类_17

在初始化列表里面显示调用构造函数,这才是最正确的做法.

class Student : public Person
{
public:
Student(const char* name, int num)
:_num(num)
,Person(name) // 这 才是 最正确 的动作
{
cout << "Student()" << endl;
}
protected:
int _num; //学号
};
初识C++继承_父类_18
父类是先构造的吗

是的,我们可以认为,在子类中,父类当作一个成员变量,而且是首先声明的变量,是先需要帮助父类构造,才构造子类的.

class Student : public Person
{
public:
Student(const char* name, int num)
:_num(num)
,Person(name) // 这 才是 最正确 的动作
{
cout << "Student()" << endl;
}
protected:
int _num; //学号
};
初识C++继承_子类_19

谈完了构造函数,我们需要谈谈拷贝构造了,这个就比较简单了.这里我在强调一遍,父类看作一个自定义类型变量,他会调用自己的拷贝构造,而且是首先调用.这里唯一的问题是在初始化列表中,我们该如何传入父类拷贝构造的函数,我们该传什么类型呢?要知道,继承是可以切片的,传入子类就可以了.

class Person
{
public:
Person(const char* name = "peter")
:_name(name)
{
cout << "Person()" << endl;
}

Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}

protected:
string _name; // 姓名
};

class Student : public Person
{
public:
Student(const char* name, int num)
:_num(num)
,Person(name) // 这 才是 最正确 的动作
{
cout << "Student()" << endl;
}

Student(const Student& s)
: _num(s._num)
, Person(s) // 会 发生 切片
{
cout << "Student(const Student& s)" << endl;
}
protected:
int _num; //学号
};

int main()
{
Student s1("jack", 18);
Student s2(s1);

return 0;
}
初识C++继承_父类_20

赋值重载和拷贝构造差不多,不过这是在函数体内调用,因为赋值重载是没有初始化列表的,注意一点就可以了.

class Person
{
public:
Person(const char* name = "peter")
:_name(name)
{
cout << "Person()" << endl;
}

Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}

protected:
string _name; // 姓名
};

class Student : public Person
{
public:
Student(const char* name, int num)
:_num(num)
, Person(name) // 这 才是 最正确 的动作
{
cout << "Student()" << endl;
}

Student& operator=(const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
Person::operator =(s); // 会 发生 隐藏 突破类域
_num = s._num;
}
return *this;
}

protected:
int _num; //学号
};

int main()
{
Student s1("jack", 18);
Student s2("joker", 18);

s2 = s1;

return 0;
}
初识C++继承_父类_21

这里析构函数需要有点问题,我们确实需要好好看看.按照我们上面的想法,不久是析构函数吗?可以,我先让父类析构,最后在析构子类的(这个顺序是不对的),我们也这么做.

class Person
{
public:
Person(const char* name = "peter")
:_name(name)
{
cout << "Person()" << endl;
}

~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};

class Student : public Person
{
public:
Student(const char* name, int num)
:_num(num)
, Person(name) // 这 才是 最正确 的动作
{
cout << "Student()" << endl;
}

~Student()
{
~Person();
cout << "~Student()" << endl;
}

protected:
int _num; //学号
};

int main()
{
Student stu("jack", 18);

return 0;
}
初识C++继承_继承_22

这里我们就疑惑了,为何会报错?我们好象使用的方法很正确啊,子类和父类没有构成隐藏的函数啊,这是为啥?这里是因为C++在设计析构函数的,函数名有点问题.记住父子类的析构函数构成构成隐藏关系,这是由于析构函数被编译器统一处理为destructor(),这是为了多态的需要,我们需要突破类域.

class Student : public Person
{
public:
Student(const char* name, int num)
:_num(num)
, Person(name) // 这 才是 最正确 的动作
{
cout << "Student()" << endl;
}

~Student()
{
Person::~Person();
cout << "~Student()" << endl;
}

protected:
int _num; //学号
};
初识C++继承_继承_23

这里我们就可以析构了,现在又出来一个问题了,我们好象把Person给析构了两次,幸亏我们父类里面没有使用delete,要不让绝对会delete两次,编译器绝对会报错.

那么这里我们就开始疑惑了,我们这不行那不行,到底怎么才是可以?我们什么都不做,这就可以了.

class Student : public Person
{
public:
Student(const char* name, int num)
:_num(num)
, Person(name) // 这 才是 最正确 的动作
{
cout << "Student()" << endl;
}

~Student()
{
cout << "~Student()" << endl;
}

protected:
int _num; //学号
};
初识C++继承_继承_24

我们需要对析构函数来进行一个总结,子类的析构函数不需要管父类,编译器会自动调用的.我们还要析构函数做一个总结,是先把子类的给析构,在把父类给析构的,这里符合先构造父类,在构造子类,符合栈的顺序.

友元和继承

我们如果把父类里面一个友元函数,这里需要说明一下,子类里面和这个函数是没有一点关系的,也就是友元是不能继承的.

class 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;
}
初识C++继承_c++_25

继承和静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例.这里面没有什么可以谈的.

class Person
{
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
};
class Graduate : public Student
{
};
int main()
{
cout << &Person::_count << endl;
cout << &Student::_count << endl;
cout << &Graduate::_count << endl;
return 0;
}
初识C++继承_继承_26

现实世界中,一个人可能是一个教师助教,也就是说他又两个身份,既是学生,有是老师,在C++ 中也支持这样的情况.这是C++最让我感到痛苦的地方,C++支持多继承,好家伙,多继承一出来,我们讨论的的难度要提上一个台阶.多继承里面存在很多问题,例如二义性和代码冗余,后面的很多语言都舍弃了多继承,例如Java,只允许单继承.

菱形继承是多继承的一种特殊情况.这里我们就以菱形继承来举列子.

class Person
{
public:
string _name; // 姓名
};

class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
初识C++继承_组合_27

我们是可以接受的,助教分别继承了teacher和student,它们两个有继承了Person,这就给代码造成了一定的冗余.

初识C++继承_子类_28
int main()
{
Assistant a;
return 0;
}
初识C++继承_组合_29

如果说空间小点还好,要是出现很大的空间,这就造成了空间的浪费,我们这里给person里面加上一个很大的数组,这就又极大的空间浪费.

class Person
{
public:
string _name; // 姓名
int arr[100000];
};
int main()
{
cout << sizeof(Assistant) << endl;
return 0;
}
初识C++继承_组合_30

谈完了代码冗余,这里还有一个问题,这就是二义性,我们想问,如何访问继承person里面的那些成员变量呢?这里面存在两个_name,就造成了不确定性.

int main()
{
Assistant a;
a._name = "peter";
return 0;
}
初识C++继承_子类_31

当然,如果我们实在是想要访问这里面的_name,也不是不可以,需要突破类域来访问.

int main()
{
Assistant a;

a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
初识C++继承_继承_32

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用.

我们先来实际看看多继承代码的冗余,这里我看看实际的内存,注意这里不要看什么监视窗口了,VS的监视窗口时经过美化过的.

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

int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;

d._b = 3;
d._c = 4;

d._d = 5;
return 0;
}
初识C++继承_子类_33

从这里我们可以看,D是先继承B的,所以这里先给B开辟空间,后面才是A的,我们也可以看出,这里面确实多开辟一些不必要的空间,例如_a 被开辟了两次.

现在我们就可以谈谈虚继承了,虚继承是把重复的类里面的内容只保留一份,解决代码的冗余和二义性.

class A
{
public:
int _a;
int arr[100000];
};

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()
{
cout << sizeof(D) << endl;
return 0;
}
初识C++继承_c++_34

我们需要看看代码的实际内存,也分析一下虚拟继承是怎么来的.通过这里我们就可以发现,虚继承重复的变量都被放在一起了,成了一份,这样就解决好了.

如果说,你只是想认识一下C++,到上面的哪个层次就完事了,要是想要深入,这里还有一个问题.

初识C++继承_继承_35

这两个是一个指针,这两个指针叫虚基表指针,指向的叫做虚基表(图上错了).

初识C++继承_c++_36

这里我们就疑惑了,我们要两个虚继表干啥?大家仔细看,发现虚基表指针指向的地址的下一个位置(4个字节),一个 是 20,另一个 是 12,我们要谈的就是这两个数,这两个数,我们知道公共区域的偏移量,看看吧.这样的话,无论这个公共的区域跑到哪,我们有偏移量,就可以找到它.

虚继承是如何发生切片的

注意,只要是我们发生了虚继承,一定是存在这个续集表的,就是为了规则统一,利于编译器工作.到这里我们就知道了,编译器先找到这个虚继表指针,计算出偏移量,得到相应的内存,然后把和独属于自己的一起拿出来就可以了,这也是虚继表的作用.

int main()
{
D d;
B b = d;
C c = d;
return 0;
}

继承是一个语法很多的知识点,但是实际应用上比较简单,我们一般不用那些看着很复杂的代码,像多继承,我们很少使用,即使是单继承,我们一般也是public继承.

继承和组合

组合就是让自定义类型作为类的一个成员就可以了.

  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象

这里面我还要讲述一个特性,在计算机中,我们类和类之间的联系越少越好,这也是软件工程提出的"高内聚,低耦合",无关的代码不要,模块与模块之间要自由.

所以在一定的程度上,继承是破坏了封装,父子类之间关系太紧密,继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可.继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高.

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封.

我们对于那些即适合继承又适合组合的,优先使用组合.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK