初识C++继承
Posted 玄鸟轩墨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初识C++继承相关的知识,希望对你有一定的参考价值。
写在前面
在谈着这个之前,我们需要先说说C++的几大特性,封装继承,多态...注意,实不置这三种,只不过他们是基础罢了,大家面试的时候注意一点.我们已经学过了封装,今天就开始继承吧,我们最好按照简单的学习来,这里的语法可能有点难,但是我们用的时候一定要偏简单一点.
继承
说实话,C++的那些大佬也考虑了很多方式,把继承搞得很复杂,管是继承方式就有三种,所以后面的语言尽力把这个知识点给简化了,我们学习C++确实需要些时间来思考.多的不说,现在看看继承究竟是什么.
什么是 继承
我查了一些资料,里面对继承的概念简述的还是比较详细的.
继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。(来源:维基百科).
我举一个不太恰当的例子,当张三的父亲离世后,张三继承了父亲的遗产,这里张三就是"子类",其父亲便是"父类",张三拥有父亲的遗产,但是除此之外他可能也有自己的财富。
为何要 继承
我们可以举一个例子来帮助我们理解,假设我们要做一个学校人员管理系统,在一个学校里面是存在老师,学生...角色的,我们要是给每一个角色都封装一个类,想想每一个类里面都存在名字,年龄...等相同的成员变量,想想都头疼,假如我们把这个相同的属性拿出来,单独作为一个类,让其他的类继承它不就可以了吗.这就是继承的作用,代码复用,避免重复造轮子.
如何 继承
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;
继承了父类的什么
这里我现给大家一个不恰当的结论,可以这么说,子类继承了父类的成员函数和成员变量.这里面也是存在很大的问题的.
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++的继承方式我就感到一镇头疼,要知道我们访问修饰限定符也是存在三种的,这一计算就是九种情况.这个我们很多人都带来了巨大的困难.
类成员/继承方式 | public继承 | protected继承 | private |
public 修饰 | 子类 public 成员 | 子类 protected成员 | 子类 private成员 |
protected 修饰 | 子类 protected成员 | 子类 protected成员 | 子类 private成员 |
private修饰 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
大家想不要慌,这九种情况还是很好区分的,我们可以得到下面的两条结论.
- private 成员 无论是什么继承方式 在 子类种不可见
- 其余的成员是 父类成员与继承方式相比较 权限较小的那一个 权限比较 public > protected > private
关于这个情况,我们不用担心,一般都是public继承,父类里面大多是protected成员,很少使用其他的.
不可见 VS 没有继承
我们需要看看究竟什么是不可见,什么是没有继承.没有继承可以了解,这里主要看看什么是不可见.
所谓的不可见是你在子类中无法直接访问这个类型,但是有确确实实的继承了.
那么这里就有一个问题了,我们是不是可以间接的去访问这个成员啊,是的,我们可以通过函数来访问,这里就不和大家分享了,下去有兴趣的话可以自己试试.
继承特性
对于继承,父子类之间具有很多的特性,其中比较关键的有两个。
- 切片
- 隐藏
切片
我们可以这么认为,父类是可以接受子类的类型的,这就像一个水到渠成的事,不是什么类型的转化,类似一种父亲可以教导孩子,这也可以认为是向上调整.这也是后面多态的基础.
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去 .
我们可以看看下面的例子.
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继承,其他的方式是不可以的.
赋值
我分别说一下它们的情况,里面有一点细节要来分享一下.
对于直接把子类赋值给父类,这是会调用父类的赋值函数,编译器会自动把从属父类的内容那一部分给切片了,回去赋值给父类对象.
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;
引用
如果是用父类去引用子类的对象,就相当于给子类对象里面的属于父类的取了一个别名.
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;
指针
指针更好理解,指针指向的就是子类当中属于父类的那些东西.
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;
子类可以接受父类吗
这个普通的方法是不可以的,在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;
这里我也给一个结论,如果我们要访问一个成员变量,编译器优先调用子类里面的,如果子类里面不存在,那么就去父类里面找,要是我们确实想想用父类里面的,就直接在用类域来声明.
成员函数
现在变量已经说完了,我们现在可以谈谈函数的隐藏了,再谈隐藏之前,我们先问问一个问题,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;
;
子类的默认成员函数
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;
显示调用父类构造函数
如果我们想要调用父类的构造函数,我们该如何去做?
我们直接 在初始化列表里面去初始化父类的成员会怎么样? 看到答案是不行的
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; //学号
;
我们把父类的成员在构造函数体内进行再次赋值,我们发现这是可以的,但是这里我们需要知道原理,初始化列表是声明和定义的地方,函数体内是再次赋值结果也证明了,在初始化列表里面是调用了父类的默认构造函数的.
class Student : public Person
public:
Student(const char* name, int num)
:_num(num)
_name = name; // 再次 赋值
cout << "Student()" << endl;
protected:
int _num; //学号
;
在初始化列表里面显示调用构造函数,这才是最正确的做法.
class Student : public Person
public:
Student(const char* name, int num)
:_num(num)
,Person(name) // 这 才是 最正确 的动作
cout << "Student()" << endl;
protected:
int _num; //学号
;
父类是先构造的吗
是的,我们可以认为,在子类中,父类当作一个成员变量,而且是首先声明的变量,是先需要帮助父类构造,才构造子类的.
class Student : public Person
public:
Student(const char* name, int num)
:_num(num)
,Person(name) // 这 才是 最正确 的动作
cout << "Student()" << endl;
protected:
int _num; //学号
;
拷贝构造
谈完了构造函数,我们需要谈谈拷贝构造了,这个就比较简单了.这里我在强调一遍,父类看作一个自定义类型变量,他会调用自己的拷贝构造,而且是首先调用.这里唯一的问题是在初始化列表中,我们该如何传入父类拷贝构造的函数,我们该传什么类型呢?要知道,继承是可以切片的,传入子类就可以了.
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;
赋值重载
赋值重载和拷贝构造差不多,不过这是在函数体内调用,因为赋值重载是没有初始化列表的,注意一点就可以了.
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;
析构函数
这里析构函数需要有点问题,我们确实需要好好看看.按照我们上面的想法,不久是析构函数吗?可以,我先让父类析构,最后在析构子类的(这个顺序是不对的),我们也这么做.
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++在设计析构函数的,函数名有点问题.记住父子类的析构函数构成构成隐藏关系,这是由于析构函数被编译器统一处理为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; //学号
;
这里我们就可以析构了,现在又出来一个问题了,我们好象把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++继承的主要内容,如果未能解决你的问题,请参考以下文章