初识C++继承

Posted 玄鸟轩墨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初识C++继承相关的知识,希望对你有一定的参考价值。

写在前面

在谈着这个之前,我们需要先说说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++继承_c++_04

初识C++继承_子类_05

类成员/继承方式

public继承

protected继承

private

public 修饰

子类 public 成员

子类 protected成员

子类 private成员

protected 修饰

子类 protected成员

子类 protected成员

子类 private成员

private修饰

在派生类中不可见

在派生类中不可见

在派生类中不可见

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

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

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

不可见 VS 没有继承

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

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

初识C++继承_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++继承_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++继承_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++继承_c++_17

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

class Student : public Person

public:
Student(const char* name, int num)
:_num(num)
,Person(name) // 这 才是 最正确 的动作

cout << "Student()" << endl;

protected:
int _num; //学号
;

初识C++继承_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++继承_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++继承_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

总结

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

以上是关于初识C++继承的主要内容,如果未能解决你的问题,请参考以下文章

C++初识

c++提高编程 2 .STL初识

初识Java

编程打卡: C++ 语言程序设计: 继承与派生: 习题

初识Java

— 初识Java