写在前面:
- 本系列专栏主要介绍C++的相关知识,思路以下面的参考链接教程为主,大部分笔记也出自该教程,笔者的原创部分主要在示例代码的注释部分。
- 除了参考下面的链接教程以外,笔者还参考了其它的一些C++教材(比如计算机二级教材和C语言教材),笔者认为重要的部分大多都会用粗体标注(未被标注出的部分可能全是重点,可根据相关部分的示例代码量和注释量判断,或者根据实际经验判断)。
- 由于C++基本继承了C语言的所有内容,建议读者先阅读C语言系列的专栏,有一些重点是仅在C语言系列专栏中有介绍的(比如二级指针、预处理等)。
- 如有错漏欢迎指出。
参考教程:黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难_哔哩哔哩_bilibili
一、概述
1、基本概念
(1)继承是面向对象三大特性之一,有些类与类之间存在特殊的关系(如下图),定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性,这个时候就可以考虑利用继承的技术,减少重复代码。
(2)类的继承是新的类从已有类那里得到已有的特性,从已有的类产生新类的过程就是类的派生。在继承过程中,原有的类或已经存在的用来派生新类的类称为基类或父类,而由已经存在的类派生出的新类则称为派生类或子类。
(3)从派生类的角度,根据它所拥有的基类数目不同,可以分为单继承和多继承,一个类只有一个直接基类时称为单继承,一个类同时有多个直接基类时则称为多继承。
(4)任何一个类都可以派生出一个新类,派生类也可以再派生出新类。
(5)基类与派生类之间的关系如下:
①基类是对派生类的抽象,派生类是对基类的具体化。基类抽取了它的派生类的公共特征,而派生类通过增加信息将抽象的基类变为某种有用的类型,派生类是基类定义的延续。
②派生类是基类的组合。多继承可以看作是多个单继承的简单组合。
③公有派生类的对象可以作为基类的对象处理,这一点与类聚集(类的数据成员中有另一个类)是不同的,在类聚集中,一个类的对象只能拥有作为其成员的其它类的对象,但不能作为其它类对象而使用。
2、派生类的定义与构成
(1)定义派生类的一般格式如下:
class <派生类名> : <继承方式1> <基类名1>, <继承方式2> <基类名2>, …
{
public:
<派生类新定义成员> //公有成员
protected:
<派生类新定义成员> //保护成员
private:
<派生类新定义成员> //私有成员
};
①基类名是已有的类的名称,派生类名是继承原有类的特性而生成的新类的名称,单继承时只需定义一个基类,多继承时需要同时定义多个基类。
②继承方式即派生类的访问控制方式,用于控制基类中声明的成员在多大的范围内能被派生类的用户访问。继承方式有公有继承(public)、私有继承(private)和保护继承(protected)三种,如果不显式地给出继承方式,默认的类继承方式是私有继承。
③派生类新定义成员是指除了从基类继承来的所有成员之外,新增加的数据成员和成员函数。
(2)在一个派生类中,其成员由两部分构成,一部分是从基类继承得到的,另一部分是自己定义的新成员,所有这些成员仍然分为公有(public)、私有(private)和保护(protected)三种访问属性,其中从基类继承下来的全部成员构成派生类的基类部分,这部分的私有成员是派生类不能直接访问的(它们在子类中甚至不是私有成员,但是子类还是会将其继承下来),公有成员和保护成员则是派生类可以直接访问的,但是它们在派生类中的访问属性将随着派生类对基类的继承方式而改变。
(3)举例:
①例1(不使用继承):
#include<iostream>
using namespace std;
#include<string>
//普通实现页面
//Java页面
class Java
{
public:
void header()
{
cout << "首页、公开课、登录、注册…(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、站内地图…(公共底部)" << endl;
}
void left()
{
cout << "Java、Python、C++…(公共分类列表)" << endl;
}
void content()
{
cout << "Java学科视频" << endl;
}
};
//Python页面
class Python
{
public:
void header()
{
cout << "首页、公开课、登录、注册…(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、站内地图…(公共底部)" << endl;
}
void left()
{
cout << "Java、Python、C++…(公共分类列表)" << endl;
}
void content()
{
cout << "Python学科视频" << endl;
}
};
//C++页面
class CPP
{
public:
void header()
{
cout << "首页、公开课、登录、注册…(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、站内地图…(公共底部)" << endl;
}
void left()
{
cout << "Java、Python、C++…(公共分类列表)" << endl;
}
void content()
{
cout << "C++学科视频" << endl;
}
};
void test01()
{
cout << "Java下载视频页面如下:" << endl;
Java ja;
ja.header();
ja.footer();
ja.left();
ja.content();
cout << "----------------------" << endl;
cout << "Python下载视频页面如下:" << endl;
Python py;
py.header();
py.footer();
py.left();
py.content();
cout << "----------------------" << endl;
cout << "C++下载视频页面如下:" << endl;
CPP cp;
cp.header();
cp.footer();
cp.left();
cp.content();
}
int main() {
test01();
system("pause");
return 0;
}
②例2(使用继承):
#include<iostream>
using namespace std;
//继承实现页面(优点:减少重复代码)
//公共页面
class BasePage
{
public:
void header()
{
cout << "首页、公开课、登录、注册…(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、站内地图…(公共底部)" << endl;
}
void left()
{
cout << "Java、Python、C++…(公共分类列表)" << endl;
}
};
//Java页面
class Java :public BasePage
{
public:
void content()
{
cout << "Java学科视频" << endl;
}
};
//Python页面
class Python :public BasePage
{
public:
void content()
{
cout << "Python学科视频" << endl;
}
};
//C++页面
class CPP :public BasePage
{
public:
void content()
{
cout << "C++学科视频" << endl;
}
};
void test01()
{
cout << "Java下载视频页面如下:" << endl;
Java ja;
ja.header();
ja.footer();
ja.left();
ja.content();
cout << "----------------------" << endl;
cout << "Python下载视频页面如下:" << endl;
Python py;
py.header();
py.footer();
py.left();
py.content();
cout << "----------------------" << endl;
cout << "C++下载视频页面如下:" << endl;
CPP cp;
cp.header();
cp.footer();
cp.left();
cp.content();
}
int main() {
test01();
system("pause");
return 0;
}
3、派生类对基类成员的访问
(1)派生类继承了基类的全部数据成员和除了构造、析构函数之外的全部成员函数,但是这些成员在派生类中的访问属性在派生的过程中是可以调整的,继承方式控制了基类中具有不同访问属性的成员在派生类中的访问属性。
(2)下图说明了在不同继承方式下派生类对基类成员的访问能力,即基类成员在派生类中具有的访问控制属性。
①基类中的私有成员在派生类中是隐藏的,只能在基类内部访问。
②派生类中的成员不能访问基类中的私有成员,可以访问基类中的公有成员和保护成员。
(3)举例:
#include<iostream>
using namespace std;
class Base
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son1 :public Base
{
public:
void func()
{
m_A = 10; //父类中的公共权限成员到子类依然是公共权限成员
m_B = 10; //父类中的保护权限成员到子类依然是保护权限成员
//m_C = 10; 父类中的私有权限成员子类无法访问
}
};
class Son2 :protected Base
{
public:
void func()
{
m_A = 10; //父类中的公共权限成员到子类是保护权限成员
m_B = 10; //父类中的保护权限成员到子类依然是保护权限成员
//m_C = 10; 父类中的私有权限成员子类无法访问
}
};
class Son3 :private Base
{
public:
void func()
{
m_A = 10; //父类中的公共权限成员到子类是私有权限成员
m_B = 10; //父类中的保护权限成员到子类是私有权限成员
//m_C = 10; 父类中的私有权限成员子类无法访问
}
};
class GrandSon3 :public Son3
{
void func()
{
//m_A = 10; 父类中的私有权限成员子类无法访问
//m_B = 10; 父类中的私有权限成员子类无法访问
//m_C = 10; 父类中的私有权限成员子类无法访问
}
};
void test01()
{
Son1 s1;
s1.m_A = 100;
//s1.m_B = 100; 类外无法访问保护权限的内容
Son2 s2;
//s2.m_A = 100; 类外无法访问保护权限的内容
Son3 s3;
//s3.m_A = 1000; 类外无法访问私有权限的内容
}
int main() {
test01();
system("pause");
return 0;
}
二、派生类的构造函数和析构函数
1、派生类的构造函数
(1)派生类构造函数必须负责调用基类构造函数,并对其所需要的参数进行设置。
(2)派生类构造函数的一般格式如下(这里仅展示类内实现的格式):
<派生类名>(<总参数表>) : <基类名1>(<参数表1>), …, <基类名n>(<参数表n>),
<成员名1>(<参数表n+1>), …, <成员名n>(<参数表2n>)
{
<派生类构造函数体>
}
①派生类的构造函数名与类名相同。
②在构造函数的参数表中,给出了初始化基类数据以及新增数据成员所需要的全部参数,在总参数表之后的成员初始化列表中,列出需要使用参数进行初始化的基类名和成员名以及各自的参数表,各项之间使用逗号分隔。
(3)当派生类有多个基类时,处于同一层次的各个基类的构造函数的调用顺序取决于定义派生类时声明的顺序(自左向右),而与在派生类构造函数的初始化列表中给出的顺序无关。如果派生类的基类也是一个派生类,则每个派生类只需负责它的直接基类的构造,依次上溯。
(4)建立派生类对象时,构造函数的执行顺序如下:
①调用基类的构造函数对基类的数据成员进行初始化,调用顺序按照各个基类被继承时声明的顺序(自左向右)。
②对新增成员进行初始化,执行顺序按照各个成员在类中声明的顺序(自上而下)。
③执行派生类的构造函数体。
(5)如果基类的构造函数定义了一个或多个参数,派生类必须定义构造函数;如果基类中定义了默认构造函数或根本没有定义任何一个构造函数,在派生类构造函数的定义中可以省略对基类构造函数的调用,即省略“<基类名>(<参数表>)”。
2、派生类的析构函数
(1)与构造函数相同,派生类的析构函数在执行过程中也要对基类和新增成员进行操作,但它的执行过程与构造函数严格相反,即:
①对派生类新增成员进行清理,如果新增成员中包括成员对象,要调用成员对象的析构函数对成员对象进行清理。
②调用基类析构函数,对基类成员进行清理。
(2)派生类析构函数的定义与基类无关,与没有继承关系的类中的析构函数的定义完全相同。
3、举例
#include<iostream>
using namespace std;
class Base
{
public:
int m_A;
Base()
{
cout << "Base构造函数!" << endl;
}
~Base()
{
cout << "Base析构函数!" << endl;
}
protected:
int m_B;
private:
int m_C;
};
class Son :public Base
{
public:
int m_D;
Son()
{
cout << "Son构造函数!" << endl;
}
~Son()
{
cout << "Son析构函数!" << endl;
}
};
void test01()
{
//Base b; 先构造父类,再构造子类;析构的顺序与构造的顺序相反
Son s;
}
int main() {
test01();
system("pause");
return 0;
}
三、继承同名成员的处理方式
1、继承同名非静态成员的处理方式
(1)访问子类同名成员,直接访问即可;访问父类同名成员需要加作用域。
①子类对象可以直接访问到子类中同名成员。
②子类对象加作用域可以访问到父类同名成员。
③当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数。
<对象名>.<基类名>::<成员名> //派生类外访问数据成员
<对象名>.<基类名>::<成员名>(<参数表>) //派生类外调用成员函数
<基类名>::<成员名> //派生类内访问数据成员
<基类名>::<成员名>(<参数表>) //派生类内调用成员函数
(2)举例:
#include<iostream>
using namespace std;
class Base
{
public:
int m_A;
void func()
{
cout << "Base - func()函数调用" << endl;
}
void func(int a)
{
cout << "Base - func(int a)函数调用" << endl;
}
protected:
int m_B;
private:
int m_C;
};
class Son :public Base
{
public:
int m_A;
void func()
{
m_A = 10;
Base::m_A = 100;
cout << "Son - func()函数调用" << endl;
}
};
void test01()
{
Son s;
s.func();
cout << s.m_A << endl;
cout << s.Base::m_A << endl;
s.Base::func();
s.Base::func(100);
//s.func(100); 如果子类中出现和父类同名的成员函数,那么子类的同名成员会隐藏掉父类中所有的同名函数,需要加作用域
//当然,如果子类中没有出现和父类同名的成员函数,那么不加作用域也是可以的
}
int main() {
test01();
system("pause");
return 0;
}
2、继承同名静态成员的处理方式
(1)静态成员和非静态成员出现同名的处理方式一致,只不过有两种访问的方式,即通过对象和通过类名访问。
①访问子类同名成员,直接访问即可。
②访问父类同名成员,需要加作用域。
<对象名>.<基类名>::<成员名> //派生类外访问静态数据成员
<对象名>.<基类名>::<成员名>(<参数表>) //派生类外调用静态成员函数
<基类名>::<成员名> //访问静态数据成员
<基类名>::<成员名>(<参数表>) //调用静态成员函数
(2)举例:
#include<iostream>
using namespace std;
class Base
{
public:
int m_A;
static int m_D;
static void func(int a)
{
cout << "Base - func(int a)静态函数调用" << endl;
}
protected:
int m_B;
private:
int m_C;
};
int Base::m_D = 100;
class Son :public Base
{
public:
int m_A;
static int m_D;
static void func()
{
cout << "Son - func()静态函数调用" << endl;
}
};
int Son::m_D = 200;
void test01()
{
//通过对象访问
Son s;
cout << "Son::m_D = " << s.m_D << endl;
cout << "Base::m_D = " << s.Base::m_D << endl;
//通过类名访问
cout << "Son::m_D = " << Son::m_D << endl;
cout << "Base::m_D = " << Base::m_D << endl;
cout << "Base::m_D = " << Son::Base::m_D << endl; //第一个::代表通过类名方式访问,第二个::代表访问父类作用域下的静态成员
}
void test02()
{
//通过对象访问
Son s;
s.func();
s.Base::func(100);
//通过类名访问
Son::func();
Son::Base::func(100);
//子类出现和父类同名静态函数时,也会隐藏父类中所有的同名成员函数,想访问它们就要加作用域
}
int main() {
test01();
test02();
system("pause");
return 0;
}
四、多继承与虚基类
1、多继承
(1)C++允许一个类继承多个类。
(2)多继承的语法:
class <子类名> :<继承方式> <父类1>, <继承方式> <父类2>, …
(3)多继承可能会导致父类中有同名成员出现,访问(或调用)时需要加作用域区分,直接使用成员名只能访问到派生类的成员;另外在C++的实际开发中不建议用多继承。
<对象名>.<基类名>::<成员名> //派生类外访问数据成员
<对象名>.<基类名>::<成员名>(<参数表>) //派生类外调用成员函数
<基类名>::<成员名> //派生类内访问数据成员
<基类名>::<成员名>(<参数表>) //派生类内调用成员函数
(4)举例:
#include<iostream>
using namespace std;
class Base1
{
public:
int m_A;
Base1()
{
m_A = 100;
}
};
class Base2
{
public:
int m_A;
Base2()
{
m_A = 200;
}
};
class Son :public Base1, public Base2
{
public:
int m_A;
Son()
{
m_A = 300;
}
};
void test01()
{
Son s;
cout << "size of Son = " << sizeof(s) << endl; //三个成员变量,一个变量占4个字节
cout << "Son::m_A = " << s.m_A << endl;
cout << "Base1::m_A = " << s.Base1::m_A << endl;
cout << "Base2::m_A = " << s.Base2::m_A << endl;
}
int main() {
test01();
system("pause");
return 0;
}
2、虚基类
(1)两个或多个派生类继承同一个公共基类,同时又有某个子类同时继承者这些派生类,当对这个继承了诸多派生类的子类中说明的成员进行访问时,可能会出现二义性,这种二义性仅靠访问控制权限不同或类型不同都无法解决。
①羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用动物类的数据时,就会产生二义性,不知道应该使用来自羊还是驼的动物数据。
②草泥马继承自动物的数据继承了两份,其实这份数据只需要一份就可以。
(2)为了解决上述多继承带来的二义性问题,C++提出了虚基类,它可以使公共基类在诸多派生类中只产生一个子对象。
(3)虚基类的说明格式如下:
class <类名> : virtual <继承方式> <虚基类名>
关键字virtual与继承方式的位置无关,但必须位于虚基类名之前,且virtual只对紧随其后的基类名起作用。
#include<iostream>
using namespace std;
class BaseDad
{
public:
int m_Age;
};
class Base1 :virtual public BaseDad //继承前加上关键字virtual变成虚继承,BaseDad称为虚基类
{
public:
};
class Base2 :virtual public BaseDad
{
public:
};
class Son :public Base1, public Base2
{
public:
};
void test01()
{
Son s;
/*
s.Base1::m_Age = 18; //要加作用域,不然分不清是哪个父级的m_Age
s.Base2::m_Age = 28;
*/
//菱形继承导致m_Age数据有两份,但实际只需要一份的话,就会导致资源浪费,可以用虚继承解决这个问题
s.m_Age = 18;
cout << s.m_Age << endl; //只剩下一个数据,不会出现重名要加作用域声明的问题
}
int main() {
test01();
system("pause");
return 0;
}
(4)对于虚基类,由于派生类的对象中只有一个虚基类子对象,所以,在建立一个派生类的对象时,为保证虚基类子对象只被初始化一次,这个虚基类构造函数必须只被调用一次。将在建立对象时所指定的类称为最派生类,虚基类子对象的初始化由最派生类的构造函数通过调用虚基类的构造函数实现,所以最派生类的构造函数的初始化列表中必须列出对虚基类构造函数的调用,如果未列出则表示使用该虚基类的默认构造函数。