继承
-
概念
-
继承机制是面向程序设计使代码可以复用的最重要的手段,它允许在保持原有类型性的基础上进行扩展,增强功能,这样产生的新类,称为派生类.
1. 对继承的理解
a.继承的理解: class(类)级别的代码复用
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。
//这里体现出了Student和Teacher复用了Person的成员。
//下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。
//调用Print可以看到成员函数的复用。
class Student : public Person //继承了Person 的内容
{
protected:
int _stuid= 4170903; // 学号
};
class Teacher : public Person
{
protected:
int _jobid=147632; // 工号
};
int main(){
Person p;
Student s;
Teacher t;
p.Print();
s.Print();
t.Print();
return 0;
}
监视结果
- 运行结果
-
由上图可以看到Student和Teacher类中出现了自己内部并没有定义的Person类那是因为他们从Person类哪里 继承 了Person类中的内容,所以它们内部多出了一部分来来自基类的"遗产"。
-
继承格式
-
下图中的Person是父类,也称基类,Student为子类,也称为派生类
- 其中基类“:”之后的public为继承方式,其决定了父类中的成员在子类中的可见性和操作性
继承的图例说明
- 派生类中‘’多‘’出了自己没有定义的内容.
注意:Student类中此时只有 _name, _age , _stuid三个变量,但是并没有Person这个内部类,Teacher类中也没有Person这个类
- 继承的方式
- 访问方式
2.继承关系和访问权限
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类中的protected成员 | 派生类中的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
1.继承方式和基类中的访问方式结合起来就有9种
子类的访问方式,在派生类中的访问方式
取两者中权限最小者
2.不管在子类中是如何继承的,基类中的私有成员在子
类中都是不可见的,但是它确实存在与子类中.(基类
中的私有成员时基类中独有的)
3.基类的私有成员在子类都是不可见。基类的其他成员
在子类的访问方式 == Min(成员在基类的访问限定符,
继承方式),public > protected > private。
4.基类中的保护类型在子类中可以被访问,但是在子
类之外定义的变量不可以访问基类中被保护的内容。
5.使用关键字class时默认的继承方式是private,
使用struct 时默认的继承方式是public,不过最好显
示的写出继承方式
6.基类private成员在派生类中无论以什么方式继承
都是不可见的。这里的不可见是指基类的私有成员还是
被继承到了派生类对象中,但是语法上限制派生类对象不管
在类里面还是类外面都不能去访问它
【总结】
-
protected:在类外无法访问,但是在子类内部可以访问
-
private:在类外和子类中都无法访问
-
public继承:不改变基类成员在子类中的访问权限
-
protected继承:基类成员在子类中的最低访问权限为protected的
-
private继承:基类成员在子类中的最低访问权限为private的
3.基类和派生类对象赋值转换
- 切片:把派生类中基类的内容切下来 赋给基类的操作
简单理解就是:派生类在 “报答” 基类的给予之恩的时候想多向基类提供些资源,但是基类只拿走了属于自己的那部分,将不属于自己的那部分切除了。因为多的部分他承受不了,只能拿走自己所能接受的那部分
切片操作图示
-
简单理解
父类对象在接收子类对象的时候子类多出的部分可以丢弃.但是子类对象若想接收父类对象的时候,由于父类对象比子类对象的内存少,子类对象的指针就会存在内存访问越界的情况 -
多的可以将多出的部分切除后给少的赋值,但少的没办法变出一部分来弥补给多的
-
切片理解
-
1.派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用这里有个形象的说法叫切片或者切割。寓意把派生类的内容中将父类中的那部分切下来赋值给父类。
-
2.父类对象不能赋值给子类对象
-
父类指针只能看到和自己类型大小的空间,而派生类的空间有可能大于父类的空间,所以基类的指针不能赋给派生类指针
-
指针的类型决定指针可以看到多大的空间
- char* :可以看到一个字节的内容
- int * : 可以看到四个字节的内容
- 子类对象的地址可以赋给父类的指针(父类指针可以在子类对象所占空间大小中只访问自己能访问到的空间大小),指针类型决定访问的空间大小
-
子类引用可以赋值给父类指针:引用的底层就是指针
-
父类的指针不能赋给子类指针:可能会存在访问越界的风险(若子类中没有定义新的成员,则基类于与子类的大小相同就不存在访问越界的情况)
-
需要做强制类型转换:Student *ptr =(Studennt *) &p;
- 强转存在风险:若子类中定义了新的变量,强转父类指针类型就存在访问越界的风险。
强行转换
int a=10;
int *p=&a;
int c=10;
p=(int*) c;
隐式类型转换
int a=10;
float b=1.5;
a=b; //隐式的将b变量转换成了int形
- 隐式类型转换:类型相似的变量可以进行相互赋值,通过隐式类型转换。
3.继承中的作用域
-
1.在继承体系中基类和派生类都有独立的作用域。
-
2.子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义.(在子类成员函数中,可以使用 基类::基类成员 显示访问)
-
3.需要注意的是
- 成员变量隐藏:子类相同名称的成员隐藏基类同名的成员,通过基类作用域访问基类成员变量
- 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 4.注意在实际使用的过程中在继承体系里面最好不要定义同名的成员
【拓展】
- 重载: 函数在同一个作用域中,函数名相同但参数不同
- 重定义/隐藏/重写: 子类函数和父类函数名字相同
3.1:类的成员变量的隐藏实例
- No1:
#include<iostream>
using namespace std;
class Parent{
public:
void Print(){
cout<<"In Parent!"<<endl;
}
int sum=1;
};
class child1:public Parent{
public:
void Print(){
cout<<"In child1!"<<endl;
}
int sum=2;
};
int main(){
Parent p;
child1 c1;
p.Print();
c1.Print();
c1.Parent::Print();
cout<<"p.sum="<<p.sum<<endl;
cout<<"c1.sum="<<c1.sum<<endl;
cout<<"c1.Person::sum="<<c1.Parent::sum<<endl;
cout<<"c1.child1::sum="<<c1.child1::sum<<endl;
return 0;
}
//当子类和父类的成员变量/成员函数同名时,
//子类会将父类中的同名变量/函数隐藏掉
//若需访问需要加上作用域限定符
- 运行结果
-
由程序的运行结果可以看到在你不指明作用域的时候,对象在调用基类或着派生类中同名的成员的时候会默认调用派生类的。将基类的成员屏蔽掉。如果你需要调用基类的成员的时候需要显示的指出其作用域。
-
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,简单理解:在学生组织中,当新的学生会主席诞生后就会 ‘‘继承’’ 上一个呼主席的权利和义务。本来两个主席有一部分权力是相同的,但是老师让同学去找主席的时候,在不指明是那个主席的时候,同学一般都会去找新的主席.因为同学会默认老师说的是新的主席
-
No2:
#include<iostream>
#include<string>
//函数的隐藏
using namespace std;
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 Test()
{
B b;
b.fun(10);
//b.fun();//调不到fun()函数,因为已经被b中的fun(int)函数隐藏了
//需要加上作用域限定符,如下语句
b.A::fun();
}
int main(){
Test();
return 0;
}
//重载:函数在同以作用域,函数名相同,参数不同
//重定义:重定义/隐藏:子类函数和父类函数名相同就会构成隐藏
//子类会把父类同名的函数隐藏掉
- 运行结果
- 由运行结果可以看出在不指明作用域的时候对象默认调用的是派生类的成员函数。将基类的同名函数给隐藏了。
4.派生类的默认成员函数
测试一
#include<iostream>
#include<string>
using namespace std;
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:
//完成父类对象的初始化,子类的构造函数先调用基类的构造函数,
//再执行子类的构造函数
//必须先调用父类的构造函数和拷贝构造进行初始化
Student(const char* name, int num)
: Person(name)
, _num(num)
{
cout << "Student()" << endl;
}
//如果子类不显示的调用父类的拷贝构造,则调用父类的默认构造函数
//如果子类显示的调用父类类的拷贝构造,就不会再去调用父类的默认构造
Student(const Student& s)
//(Student *this,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)
{
//operator =(s);不可以这样写
Person::operator =(s);//此处调用的this指针为子类对象的this指针
_num = s._num;
}
return *this; //若if中没有显示的写出父类的赋值运算符重载的话,
//子类中不会调用父类的赋值运算符重载
}
//析构函数:不允许显示调用父类的析构函数
~Student()
{
//~Person();坑:同名隐藏,编译器底层对析构函数的名字做了修改为了使用多条调不动
//编辑器将~Student()与~Person()都修改成了~destructor
//Person::~Person(); //可以调动
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
void Test()
{
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
}
int main(){
Test();
return 0;
}
//构造顺序:先是基类构造—》派生类构造
//析构顺序:派生类析构--》基类析构
//原因为函数栈帧的排列顺序
- 运行结果
-
该图中的调用关系可以验证继承中的6大默认成员函数的调用顺序与联系
-
【调用关系总结】
-
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成。
-
a.派生类的构造函数必须调用基类的构造函数初始化属于基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
-
b.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
-
c.派生类的operator=必须要调用基类的operator=完成基类的复制。
-
d.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
-
e.派生类对象初始化先调用基类构造再调派生类构造。
-
f.派生类对象析构清理先调用派生类析构再调基类的析构
常见面试题:实现一个不能被继承的类
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
// C++11给出了新的关键字final禁止继承
class NonInherit final
{};
-
重点:将构造函数私有化,致使派生类无法继承基类的内容
-
不能被继承的类
-
C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
-
C++11给出了新的关键字final禁止继承
5.继承与友元
- 友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
#include<iostream>
using namespace std;
class B;
//此处声明类型B
class A{
public:
friend void disPlay(const A& a,const B& b);
//上面声明就是为了使用这个友元函数
A()
:_id(10)
{
cout << "A()" << endl;
}
~A(){
cout << "~A()" << endl;
}
A(const A& a){
cout << "A(const A& a)" << endl;
}
protected:
int _id;
};
class B :public A
{
public:
B()
:_name("jcak")
{
cout << "B()" << endl;
}
~B(){
cout << "~B()" << endl;
}
B(const B& b){
cout << "B(const B& b)" << endl;
}
protected:
char* _name;
};
void disPlay(const A& a, const B& b){
cout << a._id << endl;
//cout << b._name << endl;
//此处访问会报错,既友元关系不能继承,
//基类友元不能访问子类的私有成员和保护成员
}
int main(){
A a;
B b;
disPlay(a, b);
return 0;
}
6.继承与静态成员
- 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都在使用这一个static成员实例 。
#include<iostream>
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;
}
- 输出的结果如下图
由程序的运行结果可以看到,该程序基类中定义的
static 成员在其所派生出的子类中都只有一个
static 成员实例