引言
观前提醒:由于这里的内容实在是过于复杂,所以单独开设一个来讲这个问题。
这里的内容实在是有点难和复杂,代码量也相对较多,因为仅仅通过文字是很难去理解这里实质的东西。这里整理的可能仅仅是让这些概念更容易理解,并没有去深究其中的内核,如果是用c++来真正实践的,我建议去看更加深层次的去加深认识。如果喜欢我的文章,也欢迎各位加我微信给我打一块钱(bushi)
免责声明:我也不是什么大佬,这里的东西可能会存在一些问题,如有错误欢迎各位讨论指正。
其他c艹笔记:c++学习笔记
基本概念
面向对象的程序有三大特性,封装、继承和多态。封装是基础,继承是核心,多态是补充。例如我们定义的类,其实就是一种封装,把我们需要的数据、函数等封装为一个整体。那么继承,就可以提高软件的重用性,比如我一个类表示圆,我又需要一个类表示圆柱,这样我就可以在原有的圆类添加或修改一些结构、数据构造出圆柱类,这样就能减少对半径等数据的重复定义。这就是继承的目的。
已经存在的类我们称为基类(父类),之后构造出来的类为派生类。派生类继承了基类的数据成员和成员函数。
注意:基类的构造函数,拷贝构造函数,析构函数和重载赋值运算符函数不能被继承。
我们可以用下面的图表示类的继承关系。
一个派生类可以有一个或多个基类,一般有一个基类的我们叫单继承,多个基类叫多继承。
单继承
下面我们用数轴,平面坐标和空间坐标来举例子。
#include <iostream>
using namespace std;
//为了简化代码方便阅读,以下类就不加修改数据的函数成员
class shuzhou {
public:
shuzhou(int newx=0) {
x = newx;
}
int getx() {
return x;
}
private:
int x;
};
//这里我们定义了一个简单的数轴类,接下来我们通过继承来定义平面坐标类
class pingmian :public shuzhou {
public:
pingmian(int newx=0,int newy=0):shuzhou(newx) {//显示调用构造函数,这里如果不懂先不用深究
y = newy;
}
void print() {
cout << getx() << ',' << y; //这里的getx()也可以写成this->getx()
}
private:
int y;
};
//下面为主函数
int main(void) {
pingmian a(2, 1);
a.print();
}
在派生类中,我们不能直接使用x来获取数据,但是我们可以调用基类的成员函数来获取。
我们这里提前引入一张图,这张图在后面我们应该还会提到
这张图可能一开始会有一些看不懂,我可以先把目前一些重要的结论列出来,后面我们需要再次来认识这张图。
- 基类的私有数据通过任何继承都无法被派生类(对象)直接访问
- ((公有继承的派生类)的对象) 可以直接访问基类的公有成员(写个括号方便断句)
那么我们写一个例子
#include <iostream>
using namespace std;
class shuzhou {
public:
shuzhou(int newx = 0) {
x = newx;
}
void print() {
cout << "数轴" << endl;
}
void setx(int m) {
x = m;
}
int getx() {
return x;
}
private:
int x;
};
class pingmian :public shuzhou {
public:
pingmian(int newx = 0, int newy = 0) :shuzhou(newx) {
y = newy;
}
void sety(int m) {
y = m;
}
int gety() {
return y;
}
private:
int y;
};
class kongjian :public pingmian {
public:
kongjian(int newx = 0, int newy = 0,int newz=0) :pingmian(newx,newy) {//显示调用构造函数
z = newz;
}
void setz(int m) {
z = m;
}
int getz() {
return z;
}
private:
int z;
};
//下面为主函数
int main(void) {
kongjian s(1, 2, 3);
cout << s.getx() << '#' << s.gety() << '#' << s.getz() << endl;//输出1#2#3
s.setx(4);
s.sety(5);
s.setz(6);
cout << s.getx() << '#' << s.gety() << '#' << s.getz() << endl;//输出4#5#6
}
上面的代码有一点长,其实就是从数轴再到平面再到空间,然后通过继承的方式并添加了用来读取和修改数据的函数。会有人好奇,为什么不能直接在类内用x,y的方式来获取数据,而一定要用到函数来读取、修改?例如我在kongjian类中添加以下函数成员。
void print() {
cout << x << y << z;
}
当然,这样写肯定是不对的,编译器会报错,这是为什么呢?
我们回到刚刚那张表中,我特意强调了一句话,基类的私有数据通过任何继承都无法被派生类(对象)直接访问。
我们可以用画图的形式来解释一下这个问题。
其实基类的私有数据是会转移到派生类的私有数据,但是无法在内部调用,我们还是可以通过继承的公有函数进行调用。不过同样,这张图也能让你认识到公有继承的派生类的对象可以直接访问基类的公有成员。
保护成员
类其实本身就有3中类型,public(公有),private(私有),protected(保护)。至于为什么现在讲,因为保护成员主要是用于继承的。
在单个类中,保护成员和私有成员是比较类似的,类自身可以访问而类外不能访问。在公有或保护继承的方式下,派生类内部是可以直接访问基类的保护成员的,且不能访问私有成员。
class a {
public:
int x;
private:
int y;
protected:
int z;
};
class b:public a {
void f() {
x = 1; //可以访问
y = 2; //报错,无法访问
z = 3; //可以访问
}
};
在类的层面上讲,保护成员是可以在派生类内被调用,但是派生类的对象仍然是无法直接访问保护成员。
继承方式
这里的文字实在太多了,内容也特别多,但是说来说去,最终还是这一张图。
这张图是最直观的展现不同继承方式下派生类的访问权限,只要这张图懂了,前面的内容全部over。
赋值兼容
赋值兼容是指在公有继承方式下,凡是需要基类对象的地方都可以使用派生类对象,但不能反着来,需要派生类的对象不能使用基类对象。
上面那句话有点拗口,这一点其实也比较好理解,你可以理解为内容更丰富的可以给内容少的赋值,而内容少的不能给内容多的赋值。(其实也不是赋值,这里写赋值是方便理解,能理解赋值就行)
- 派生类对象可以赋值给基类对象
- 派生类对象的地址可以赋值给基类指针
- 派生类对象可以初始化基类引用
- 前面说的3个看起来很牛逼其实本质就是需要基类对象的地方可以使用派生对象
举个小栗子,就上面一样的代码把,建议直接跳转到main函数。
#include <iostream>
using namespace std;
//这里不用看-----------------------------------
class shuzhou {
public:
shuzhou(int newx = 0) {
x = newx;
}
void print() {
cout << "数轴" << endl;
}
void setx(int m) {
x = m;
}
int getx() {
return x;
}
private:
int x;
};
class pingmian :public shuzhou {
public:
pingmian(int newx = 0, int newy = 0) :shuzhou(newx) {
y = newy;
}
void sety(int m) {
y = m;
}
int gety() {
return y;
}
private:
int y;
};
//这里不用看-----------------------------------
//
//下面为主函数
int main(void) {
pingmian s(1, 2);
shuzhou a = s; //直接赋值
shuzhou* p = &s; //可以赋值基类指针
shuzhou& t = s; //初始化基类引用
}
单继承下的构造函数和析构函数
内容很多,看起来很恐怖,但如果理解了的话非常简单,这里主要的考点在于单继承下派生类构造函数的执行顺序,就是我为一个派生类赋值,他究竟是先调用基类的构造函数还是派生类的构造函数。
我们先上结论
- 首先调用基类构造函数(调用并非执行)
- 如果基类含有对象数据成员,则按它们在基类中的声明次序,依次调用基类对象数据成员的构造函数。
- 执行基类构造函数函数体体中的内容
- 如果派生类含有对象数据成员,则按它们在派生类中的声明次序,依次调用派生类对象数据成员的构造函数
- 最后执行派生类构造函数的函数体中的内容
很难读吗,有点难,我们画个图来解释一下,为什么这么调用,我们看下面的代码
#include <iostream>
using namespace std;
class superxmy {
public:
superxmy() {
cout << "super" << endl;
}
private:
int nb;
};
class shuzhou {
public:
shuzhou(int newx = 0) {
cout << "shouzhou" << endl;
}
private:
superxmy n;
};
class pingmian :public shuzhou {
public:
pingmian(int newx = 0, int newy = 0) :shuzhou(newx) {
cout << "pingmian" << endl;
}
private:
superxmy b;
};
//下面为主函数
int main(void) {
pingmian a;
}
superxmy是随便一个类,主要是演示含有对象数据成员的调用情况
首先我们定义了pingmian对象a,第一步,调用pingmian的构造函数
这里显示调用基类的构造函数
这里注意,在执行函数之前,需要初始数据成员,所以第三部其实来到了superxmy类
接着我们先执行了superxmy的构造函数,又返回执行了shuzhou的构造函数,回到了pingmian类
在pingmian构造函数执行前,又要初始化数据成员,所以又来到了superxmy类,最终再执行构造函数内的内容。
最后的打印输出是
super
shuzhou
super
pingmian
非常完美
多继承机制
其实多继承机制和单继承机制没什么不同的,不过注意一点,多继承机制会增加命名冲突的可能,在这种情况下,派生类成员会“遮蔽”基类成员,若要访问被遮蔽的成员,需要使用作用域限定符。
还有就是在多继承的时候,可能出现从一个间接基类派生多次,这时从不同的派生类路径继承下来的基类成员就有多个副本,可以将同一个间接基类声明为虚基类,这样就只会出现一个副本。
如果只是简单的公开继承,则x会出现2个副本,而声明为虚基类,则只会出现1个副本
声明虚基类的语法如下
class 派生类名:virtual 继承方式 基类名{
.....
}
多态
这里其实能延伸的东西太多太多了,我实在是学术不精,如果真的要深究请另请他人。
多态其实就是一个东西在不同的环境下有不同的作用,比如一个厕所,根据来的人的性别来实时选择不同的厕所,我们可以简单的把这种选择理解为绑定。根据不同的绑定时间分为静态绑定和动态绑定。静态绑定其实比较好理解,举个小栗子,函数重载就是一个静态绑定,根据传入的参数来绑定不同的函数体。动态绑定我也不会举例子,不过要记住,只有满足特定条件的类的成员函数才可能是动态绑定的。
运行多态必须满足以下几点
- 必须存在继承关系,即类层次结构,基类和派生类之间满足赋值兼容。
- 必须存在虚函数,虚函数在基类中声明。虚函数是动态绑定的基础,用来表示基类和派生类的成员函数之间的关系。
- 必须存在基类指针或基类引用,通过基类指针或引用来调用虚函数
虚函数
虚函数使用virtual关键字来限定函数成员。在基类的某个成员被声明为虚函数后,此函数在派生类自动成为虚函数,也就是说你在派生类声明同样的函数时前面可以加也可以不加virtual。
再直白点,其实就是在基类中的成员函数如果要在派生类中重新定义,就要声明为虚函数,否则为非虚函数。
这里有一点容易混淆,做题的时候也会出现,一定要注意,派生类成员会“遮蔽”基类成员。
虚函数的模板:
virtual 返回类型 函数名 (参数){
}
虚析构函数
c艹的构造函数不能为虚函数,这一点如果不知道为什么的,重新回到前面再看该博客。但是析构函数可以是虚函数。为什么要用虚构造函数呢,我这里就不放代码了,画个图吧
我们定义了一个基类指针指向B类对象,然后我们通过delete来销毁指针,但注意,通过基类指针释放派生类对象时调用的是基类的析构函数,因此此时释放的是基类的内存空间,而没有释放派生类的
这就导致了内存泄漏。
我们把基类的析构函数构造为虚函数,那么派生类的析构函数也为虚函数,这样执行动态绑定先调用派生类析构再调用基类析构。
抽象类
这里就讲讲概念把,我现在人也很抽象。
纯虚函数是在基类声明的虚函数且没有函数体,格式如下:
virtual 函数类型 函数名(形参)=0;
含有纯虚函数的类称为抽象类。注意,是“含有”,只要有一个纯虚函数那就是抽象类。抽象类只能作为其他类的基类。
如果抽象类的派生类重定义了所有纯虚函数,那么这个派生类就会成为具体类,否则还是抽象类。
不可以创建抽象类对象,但可以声明指针和引用。
Q.E.D.