C++ 特性之继承
1.为什么要使用继承
继承性是一个非常自然的概念,现实世界中的许多事物都是具有继承性的。人们一般用层次分类的方法来描述他们的关系。例如,下图就是一个简单的汽车分类图:
在这个分类树中建立了一个层次结构,最高一层是最普遍、最一般的,每一层都比它的前一层更具体,低层具有高层的特性,同时也有与高层的细微不同。例如,确定了一辆车是客车以后,没有必要再指出它可以进行运输,因为客车本身就是从运输汽车类中派生出来的。
1.1 继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有的特性基础上进行扩展,增加功能,这样产生新的类,称作是派生类。继承呈现了面向对象程序设计的层析结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用。
例如:
class Person
{
public:
void Print(){
cout<<"name:"<<_name<<endl;
cout<<"age:"<<_age<<endl;
}
protected:
string _name = "Romeo"; //姓名
int _age = 18; //年龄
};
/*继承后父类的Person的成员(成员函数+变量)都会变成子类的一部分。这里
体现出了Student和Teacher复用了Person的成员。*/
class Student: public Person
{
protected:int _stuid; //学号
};
class Teacher:public Person
{
protected:int _jobid; //工号};
1.2 继承的定义
class Student:public Person
{
public:
int _stuid; //学号
char _major; //专业
Student称为 派生类;
第一行的public是继承方式;
Person称为基类。
以下是三种继承方式:
以下是继承基类成员访问方式的变化:
总结:
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接访问,但需要在派生类中能访问,就定义为protected。可见,保护成员限定符protected是因为继承才出现的。
3.表格里的访问方式都是取最小的“权限”。
4.使用关键字class时默认的继承方式是private,使用struct的默认继承方式是public,不过最好显示地写出继承方式。
5.**在实际运用中一般都使用的是public继承,几乎很少去使用protected/private继承,**也不提倡去使用。因为protected/private继承下来的成员都只能在派生类的类里面使用,实际中的扩展维护性不强。
2.基类和派生类对象赋值转换
a. 派生类对象可以赋值给基类对象/基类的指针/基类的引用。这里有个形象的说法叫“切片”或者“切割”。寓意把派生类中父类那部分切来赋值过去。
b. 基类对象不能赋值给派生类对象。
c. 基类的指针可以通过强制类型转换赋值给派生类指针。但是必须是基类的指针指向派生类对象时才是安全的。 这里的基类如果是多态类型,可以使用dynamic _cast来进行识别后进行安全转换。如下图所示:
示例代码:
class Person
{
protected:
string _name; //姓名
string _sex; //性别
int _age; //年龄
};
class Student:public Person
{
public:
int _No; //学号
};
void Test()
{
Student sobj;
//1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派生类对象
sboj = pobj; //error
//3.基类的指针可以通过强制类型转换赋值给派生类指针
pp = &sobj;
Student* ps1 = (Student*)pp;
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; //这种情况虽然可以,但会发生访问越界。
ps2->_No = 10;
}
int main()
{
Test();
return 0;
}
3.继承中的作用域
(1) 在继承体系中基类和派生类都有独立的作用域。
(2) 子类和父类中有同名成员,子类成员将屏蔽父****类对同名成员的直接访问,这种情况叫“隐藏”,也叫“重定义”(在子类成员函数中,可以使用 基类::基类成员 显示访问 )
(3) 需要注意的是如果成员函数的隐藏,只需要函数名相同就构成了隐藏。
(4) 注意:在实际工程中,继承体系里最好不要定义同名成员。
示例1:
//Student的_num和Person的_num构成隐藏关系,可以看出这样的代码虽然能跑,但是非常容易混淆
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; //学号
};
void Test()
{
Student s1;
s1.Print();
}
辨析:
a. 成员函数被重载的特征:
(1). 相同的范围(在同一个类中);
(2). 函数名字相同;
(3). 参数不同;
(4). virtual关键字可有可无。
b. 覆盖是指派生类函数覆盖基类函数。
(1). 不同的范围(分别位于基类和派生类);
(2). 函数名字相同;
(3). 参数相同;
(4). 基类函数必须有virtual关键字。
c. “隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1). 如果派生类的函数与基类函数同名,但是参数不同。此时,不论有无virtual, 基类的函数将被隐藏;
(2). 如果派生类的函数与基类的函数同名,并且形参也相同,但是基类函数没有virtual,此时,基类的函数被隐藏。
4. 派生类的默认成员函数
a. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分初始化成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用;
b. 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝初始化;
c. 派生类的operator=必须要调用基类的operator=完成基类的赋值;
d. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理,基类对象再清理的顺序;
e. 派生类对象初始化先调用基类构造函数再调用派生类构造函数;
f. 派生类对象析构清理先调用派生类的析构函数再调用基类的析构函数。
示例代码:
#include <iostream>
#include <string>
#include <stdlib.h>
using namespace std;
class Person
{
public:
Person(const char* name = "Romeo")
:_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:
Student(const char* name, int num)
:Person(name) //调用父类的构造函数完成从父类继承来的成员的初始化
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s) //调用父类的拷贝构造函数,其中也为“切片”操作,因为父类的拷贝构造函数的参数为引用
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s){
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s){
Person::operator=(s); //调用基类的
_num = s._num;
}
return *this;
}
~Student(){
//此处不需要显示调用父类的析构函数,编译器会自动调用
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
void Test()
{
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
}
int main()
{
Test();
system("pause");
return 0;
}
5. 继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
#include <iostream>
#include <string>
#include <stdlib.h>
using namespace std;
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; //error
cout << s._stuNum << endl; //error
}
6. 继承与静态成员
基类定义了static静态成员,则整个继承体系里只有一个这样的成员。无论派生出多少个子类,都有一个static成员实例。
示例代码:
#include <iostream>
#include <string>
#include <stdlib.h>
using namespace std;
class Person
{
public:
Person(){
++_count;
}
protected:
string _name; //姓名
public:
static int _count; //统计人数
};
int Person::_count = 0;
class Student :public Person
{
protected:
int _stuNum; //学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; //研究科目
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << "人数: " << Person::_count << endl;
Student::_count = 0;
cout << "人数: " << Person::_count << endl;
}
int main()
{
TestPerson();
system("pause");
return 0;
}
7.复杂的菱形继承和菱形虚拟继承
这里给大家贴了一个链接:
https://blog.csdn.net/audience_fzn/article/details/81463275
8. 继承里的指针偏移问题
class Base1{
public:
int _b1;
};
class Base2{
public:
int _b2;
};
class Derive : public Base1, public Base2{
int _d;
};
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
cout << "p1 = " << p1 << endl;
cout << "p2 = " << p2 << endl;
cout << "p3 = " << p3 << endl;
system("pause");
return 0;
}