总言
主要介绍继承相关内容。
文章目录
- 总言
- 1、继承介绍
- 1.1、继承是什么
- 1.2、继承方式与访问限定符
- 1.3、继承作用域
- 2、基类和派生类对象赋值转换
- 2.1、子类对象可以赋值给父类对象/指针/引用
- 2.2、基类对象不能赋值给派生类对象
- 2.3、基类的指针可以通过强制类型转换赋值给派生类的指针
- 3、派生类的默认成员函数
- 3.1、构造函数
- 3.2、拷贝构造函数
- 3.3、赋值运算符重载
- 3.4、析构函数
- 3.5、取地址
- 4、继承与友元、静态成员
- 4.1、继承和友元
- 4.2、继承和静态成员
- 4.3、一道例题
- 5、菱形继承、菱形虚拟继承
- 5.1、单继承和多继承介绍
- 5.2、菱形继承和菱形虚拟继承
1、继承介绍
1.1、继承是什么
1)、继承是什么
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
2)、继承在类中复用性体现
此处我们以一个人物身份管理的场景演示:以下有三个类,Person、Student、Teacher
。Person类中定义的人物基本信息可以在Student、Teacher类中用到,因此可在Student和Teacher类中将Person类继承下来。
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
string _name = "peter"; // 姓名
int _age = 0; // 年龄
};
class Student
{
protected:
int _stuid; // 学号
};
class Teacher
{
protected:
int _jobid; // 工号
};
一个定义:在上述描述的继承关系中,Person是父类,也称作基类。Student是子类,也称作派生类。
语法格式如下:派生类 : 继承方式 基类
class Student : public Person //Student类以public的方式继承了Person类
{
protected:
int _stuid; // 学号
};
class Teacher : public Person //Teacher类以public的方式继承了Person类
{
protected:
int _jobid; // 工号
};
继承中复用性的体现,演示代码如下:
void test01()
{
//针对这两个类实例化创建对象
Student s1;
Teacher t1;
//二者都调用相同变量,赋值不同
s1._name = "张龙";
s1._age = 21;
t1._name = "公孙策";
t1._age = 46;
//验证最终结果是否独立
s1.Print();
t1.Print();
}
1.2、继承方式与访问限定符
1)、类的继承方式和类访问限定符介绍
继承方式有三种:public继承、protected继承、private继承
访问限定符也有三种:public访问、protected访问、private访问
对上述二者进行排列组合,可得继承基类成员访问方式的变化如下:
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
相关说明:
1、 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员虽然被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2、基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3、观察上表可知,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)
,public > protected > private
。
4、使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public。不过,一般情况下建议显示的写出继承方式。
5、在实际运用中一般使用都是public
继承,几乎很少使用protetced/private
继承,也不提倡使用protetced/private
继承,因为protetced/private
继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
总结:
1、protected、private
:类外不能访问,类里可以访问;
不可见
:类外、类里都不能访问
2、如果父类中有不想被子类继承的成员,可以定义为私有。
3、如果父类中有成员,想给子类复用,但又不想在类外暴露直接访问,可以将其定义为保护。(保护的价值体现)
4、实际常用如下:
相关演示:
代码演示一:基类成员公有,公有继承,可在子类中访问,也可在类外访问。
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
string _name = "peter";
int _age = 0;
};
class Student : public Person
{
public:
void Set(const char* name, int age)
{
_name = name;//子类中访问
_age = age;
}
protected:
int _stuid;
};
class Teacher : public Person
{
public:
void Set(const char* name, int age)
{
_name = name;//子类中访问
_age = age;
}
protected:
int _jobid;
};
void test01()
{
Student s1;
Teacher t1;
类外访问
s1._name = "学生A";
s1._age = 21;
t1._name = "教师A";
t1._age = 46;
s1.Print();
t1.Print();
s1.Set("学生B",18);
t1.Set("教师B",33);
s1.Print();
t1.Print();
}
代码演示二:基类成员公有,保护继承,可在子类中访问,不能在类外访问。
1.3、继承作用域
1)、问题说明
在C语言中,我们学习过局部域全局域,其限制的是变量的生命周期,在C++中,我们又引入了命名空间域、类域,其影响的是变量的访问。
问题1: 我们知道同一个类域中不能同时定义名称相同的变量,那么对于父类、子类,是否能同时在其内部各自定义相同变量?
回答: 可以。在继承体系中基类和派生类都有独立的作用域。
问题2: 同名变量能在父子类中同时存在,若子类从父类继承到该同名变量,那么在子类中访问该变量时,我们获取到的是子类自己的变量,还是父类的变量?
相关演示如下:
class Person
{
protected:
string _name = "景行止";
int _num = 40196;
};
class Student : public Person
{
public:
void Print()
{
cout << " name:" << _name << endl;
cout << " num:" << _num << endl;
}
protected:
int _num =35127;
};
void test02()
{
Student s1;
s1.Print();
};
由上述可知:子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
问题3: 那么,上述情况下,如何访问到父类中的该同名成员呢?
回答:在子类成员函数中,可以使用 基类::基类成员
显示访问
class Person
{
protected:
string _name = "景行止";
int _num = 40196;
};
class Student : public Person
{
public:
void Print()
{
cout << " name:" << _name << endl;
cout << " Person_num:" << Person::_num << endl;//父类默认隐藏,需要访问时要添加对应类域
cout << " Student_num:" << _num << endl;
cout << " Student_num:" << Student::_num << endl;
}
protected:
int _num =35127;
};
void test02()
{
Student s1;
s1.Print();
};
问题: 如下述代码,假如父类和子类中有共同的同名函数,其是否构成函数重载?
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
void test03()
{
B b;
b.fun(10);
};
回答:B中的fun
和A中的fun
不是构成重载,因为不是在同一作用域,二者构成隐藏关系。成员函数的隐藏,只需要函数名相同就构成隐藏。
其它:实际上,B里面有两个fun,一个是自己的,一个是从父类那继承下来的,我们直接访问到的是自己的,如果要访问父类的,可添加相应类域。注意这里类域的写法。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" << i << endl;
}
};
void test03()
{
B b;
b.fun(10);
b.A::fun();
};
2)、小结
1、在继承体系中基类和派生类都有独立的作用域。
2、子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。此时若想在子类中访问父类成员,可以使用 基类::基类成员
显示访问。
3、成员函数的隐藏,只需要函数名相同就构成隐藏。
4、在实际中在继承体系里面最好不要定义同名的成员。
2、基类和派生类对象赋值转换
引入: 我们知道同类型变量可以相互赋值,同类型类也能够相互赋值。那么,父类和子类非同类型类,其能否相互赋值转换呢?
2.1、子类对象可以赋值给父类对象/指针/引用
1)、概述
派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割,寓意把派生类中父类那部分切来赋值过去。
演示代码如下:
class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
int _No;
};
void test04()
{
Student sobj;
//子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
}
Person pobj = sobj;
子类对象被赋值给父类对象。实际上是子类中父类相关部分被赋值给父类。
Person* pp = &sobj;
子类对象的地址赋值给父类对象的指针。需要注意该指针能够指向的范围,仍旧是父类相关部分。
Person& rp = sobj;
子类对象赋值给父类的引用。同理,需要注意范围。从该行代码就能看出,子类赋值给父类,并非是强制类型转换、也不是隐式类型转换,因此这二者中间都会生成临时变量,临时变量具有常性,需要加const修饰。此处能够直接转换,说明这是父类子类赋值转换的一种语法支持。
注意事项:
这种赋值转换,前提条件是子类需要公有继承。
2.2、基类对象不能赋值给派生类对象
基类对象不能赋值给派生类对象。原因是父类缺少了子类中的一部分。
sobj = pobj;
sobj = (Student)pobj;//哪怕这里使用强制类型转换,也是不支持的。
2.3、基类的指针可以通过强制类型转换赋值给派生类的指针
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
PS:相关问题后续讲解。
3、派生类的默认成员函数
引入: 根据之前所学,类有六个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,默认成员函数对自定义类型和内置类型有其独立行为。由于子类继承父类中的成员,此处有一个问题,这些默认成员函数对父类成员的行为是什么样的?
3.1、构造函数
前提回顾: 默认生成的构造函数,对内置类型不做处理,对自定义类型会去调用它自己的构造函数。
派生类中构造函数说明: 子类中,编译器默认生成的构造函数,对自己的成员遵循以前的规则,对继承的父类成员,必须调用父类的默认构造函数初始化。
相关代码如下:
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;
}
Person& operator=(const Person& p)
{
cout << "Person& operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
protected:
int _num;
};
void test05()
{
Student s;
}
问题: 假如父类中,没有默认构造函数,这时候该怎么办?
说明: 首先,可以明确的是,如果父类没有默认构造(比如我们自己写的非默认构造函数),而子类中也没有做相关处理,那么,其结果是编译不通过。
Person(const char* name)
: _name(name)
{
cout << "Person()" << endl;
}
解决方案:基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
注意这里的调用方式:
1、直接在子类中对父类成员进行初始化是错误的,因为子类是把整个父类当作一个整体进行初始化的。即子类处理子类自己,而子类中的父类成员,让父类自己处理。
class Student : public Person
{
public:
Student(const char* name,int num)
:_name(name)
,_num(num)
{}
protected:
int _num;
};
void test05()
{
Student s("王朝",23);
}
子类中如何处理父类整体?写法如下:使用匿名对象构造。
class Person
{
public:
Person(const char* name)//构造函数:非默认
: _name(name)
{
cout << "Person()" << endl;
}
//……
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name,int num)
:Person(name)//子类初始化列表,对父类初始化的处理方式
,_num(num)
{
cout << "Student()" << endl;//验证代码
}
protected:
int _num;
};
void test05()
{
Student s("王朝",23);
}
3.2、拷贝构造函数
前提回顾: 默认生成的拷贝构造,对内置类型做浅拷贝,对自定义类型回去调用它自己的拷贝构造。(因此,若涉及深拷贝,则需要我们手动写)
派生类中拷贝构造函数说明: 子类中,编译器默认生成的拷贝构造函数,对自己的成员遵循以前的规则,对继承的父类成员,须调用父类的拷贝构造完成父类的拷贝初始化
假如涉及深拷贝,需要我们显示拷贝构造,在上述构造函数中我们知道,基类在派生类中是被当作整体看待的,因此拷贝构造初始化时,也要将其当作整体处理,但我们传入的参数并没有Person类,所以如何赋值就是一个问题。如何写?
Student(const Student& s)
:Person(s)
,_num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Person(s)
:这里就是基类和派生类赋值转换的运用体现。
3.3、赋值运算符重载
前提回顾: 编译器默认生成的赋值运算符重载,其行为和拷贝构造类似,对内置类型不做浅拷贝,对自定义类型会调用它的赋值运算符重载。
派生类中赋值运算符重载说明: 派生类的operator=必须要调用基类的operator=完成基类的复制。
一个错误写法:
Student& operator=(const Student& s)
{
if (this != &s)
{
operator=(s);
_num = s._num;
}
return *this;
}
如下述,运行该代码时其结果显示栈溢出,这是因为operator=(s);
,我们本意是想借助基类和派生类进行赋值转换,显示调用基类的operator=
,这里编译器将其理解成调用student自身的operator=
。
解决方案如下:添加类域作为区分
Student& operator=(const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
3.4、析构函数
前提回顾: 编译器默认的析构函数,对内置类型不做处理,对自定义类型会去调用它自己的析构函数。
派生类中析构函数的说明:
错误写法演示:
~Student()//写法一
{
~Person();
cout << "~Student()" << endl;
}
~Student()//写法二
{
Person::~Person();
cout << "~Student()" << endl;
}
结果如下:
写法一报错说明:由于多态的需要,析构函数名字会统一处理成destructor()
,导致子类的析构的函数跟父类析构函数构成隐藏。因此,如果要显示析构父类,则需要添上父类的类域。
写法二存在的问题:可以看到父类析构次数比子类析构多。这是因为为了保证先析构子类,再析构父类,实际中不需要显示调用父类的析构函数,每个子类析构函数后面,会自动调用父类析构函数。(PS:这里多次析构没报错,是因为该例子本身什么也没做)
总结: 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象的清理顺序:先清理派生类成员,再清理基类成员。
3.5、取地址
默认生成的即可使用,一般不需要显示写。
Student* operator&()
{
return this;
}
4、继承与友元、静态成员
4.1、继承和友元
前提回顾:若A类是B类的友元,则在A类中可以访问B类的非公有成员。但这种关系是单向的,不具有交换性。友元函数同理。
问题:由于子类继承父类,若父类中使用friend声明友元类,则该类是否为子类的友元类?
回答:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
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;//是否能访问Person保护/私有成员?
cout << s._stuNum << endl;//是否能访问Student保护/私有成员?
}
void main()
{
Person p;
Student s;
Display(p, s);
}
延伸:假如上述例子中,Display
想要访问子类私有/保护成员,该如何操作?
回答:在子类中将该函数/该类设置为其友元函数/友元类。
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);//父类中设置为友元函数
protected:
string _name;
};
class Student : public Person
{
public:
friend void Display(const Person& p, const Student& s);//子类中设置为友元函数
protected:
int _stuNum;
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
4.2、继承和静态成员
说明:基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
演示代码如下:
class Person
{
public:
//protected:
string _name;
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
public:
//protected:
int _stuNum;
};
int main()
{
Person p;
Student s;
s._name = "马汉";
p._name = "王朝";
cout << s._name << endl;
p._count++;
cout << p._count << " " << s._count << endl;
s._count++;
cout << p._count << " " << s._count << endl;
return 0;
}
4.3、一道例题
1)、例题一:
提问:如何定义一个不能被继承的类? 演示例子如下:
class A //父类
{
public:
A(){}
//……
protected:
int _a;
};
class B : public A //子类
{
//……
};
方案一(C++98):将父类的构造函数私有化。这时候,由于继承关系,父类构造在子类中不可见,因此子类对象实例化时,无法调用构造函数。
class A
{
//public:
private://方案一:构造函数设置为私有,那么根据继承方式,子类中为不可见
A(){}
//……
protected:
int _a;
};
class B : public A
{
//……
};
int main()
{
B b;
return 0;
}
方案二(C++11):final关键字。上述方案有一个缺点,在对象实例化时才得以体现,等同于间接起到效果,而该方案即使我们不实例化对象,也能达到题目要求。
class A final
{
public:
A(){}
//……
protected:
int _a;
};
class B : public A //error
{
//……
};
5、菱形继承、菱形虚拟继承
5.1、单继承和多继承介绍
1)、单继承和多继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
2)、一道例题
下面说法正确的是( )
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3
E:编译报错
F:运行报错
演示结果:可以看到答案选C。
分析如下:
1、public Base1, public Base2
,多继承,先继承的先定义,故在栈区,Base1地址小于Base2。
2、Base1* p1 = &d;
、 Base2* p2 = &d;
二者均为父类,故此处存在切片行为,需要注意父类指针的指向范围。
5.2、菱形继承和菱形虚拟继承
1)、菱形继承概念介绍
菱形继承是多继承的一种特殊情况。
演示例子:
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; // 主修课程
};
2)、存在的问题
菱形继承存在数据冗余和二义性的问题。
1、以上述1)中示例代码为例,对于数据冗余,在Assistant
的对象中Person
成员会有两份。
2、对于二义性,指Person类在Sutdent、Teacher中都得到继承,随后又同时被Assistant继承,因此若在Assistant中调用Person成员,性无法明确知道访问的是哪一个。
void Test()
{
//此处调用_name存在二义性
Assistant a;
a._name = "王朝";
}
3)、解决方案
1、指定类域访问:显示指定访问父类的成员,可以解决二义性问题,但数据冗余无法得到解决。
void test07()
{
Assistant a;
a.Student::_name = "王朝";
a.Teacher::_name = "马汉";
cout << a.Student::_name << endl;
cout << a.Teacher::_name << endl;
}
2、使用关键字virtual
:菱形虚拟继承。需要注意该关键字添加位置。
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person//virtual关键字
{
protected:
int _num; //学号
};
class Teacher : virtual public Person//virtual关键字
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
void test07()
{
Assistant a;
a.Student::_name = "王朝";
a.Teacher::_name = "马汉";
a._name = "张龙";
}
PS:在监视窗口我们看到的是多份,而实际上它们都指向一份。即菱形虚拟继承不仅能够解决二义性,还能够解决数据冗余问题。
4)、菱形虚拟继承的原理(对象存储模型)
未使用虚拟继承,情况如下:
class A
{
public:
int _a;
};
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
void test08()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
//d._a = 0;
d._b = 3;
d._c = 4;
d._d = 5;
}
使用虚拟继承,情况如下:
B和C中存储有两个指针,二者指向一张表,这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量,通过偏移量可以找到virtual继承下来的A。
class A
{
public:
int _a;
};
//class B : public A
class B : virtual public A
{
public:
int _b;
};
//class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
void test08()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._a = 0;
d._b = 3;
d._c = 4;
d._d = 5;
}
在上述菱形虚拟继承下,我们来探讨一下B/C类的大小:
class A
{
public:
int _a;
};
//class B : public A
class B : virtual public A
{
public:
int _b;
};
//class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
void test09()
{
B b;
b._a = 1;
b._b = 2;
cout << sizeof(b) << endl;
}
int main()
{
test09();
return 0;
}
分析如下:
问题:为什么B类中也要保存A的偏移量?
可以考虑以下情形:如下,我们有一个函数,其参数为B类指针,但根据切片相关知识,B类只能指向其对应类的首地址,那么如何找到A呢?此处就需要借助偏移量。
void func(B* ptr)
{
cout << ptr->_a << endl;
}
5)、其它菱形继承例子演示
例子一:
例子二:实际中标准输入输出流里就存在菱形继承。
其它:继承和组合说明。