继承是面向对象的重要手段.通过继承定义一个类,继承是类型之间的关系建模,共享公有的东西,实现各自本质不同的东西。
一、继承关系(public继承、protected继承、private继承)
private/protected限定符都是限定直接访问,它们之间有什么区别?
基类的私有成员在派生类中是不能被访问的,如果一些基类成员不想被基类对象直接访问,但需要在派生类中访问,就定义为保护成员。可以看出保护成员限定符是因继承才出现的.
成员访问限定符&继承关系
实现一个简单的继承关系
class Person{
public:
Person(const string& _name)
:name(_name){}
void Display(){
std::cout<<name<<std::endl;
}
protected:
string name;
};
class Student:public Person{
public:
Student(const string& _name,int num = 0)
:Person(_name),number(num){}
int number;
};
三种继承关系下基类成员在派生类的访问关系变化
继承方式 | 基类的public成员 | 基类的protected成员 | 基类的private成员 | 继承引起的访问控制关系变化概括 |
---|---|---|---|---|
public继承 | 仍为public成员 | 仍为protected成员 | 不可见 | 基类的公有成员和保护成员在子类的访问属性都不变 |
protected继承 | 变为protected继承 | 变为protected继承 | 不可见 | 基类的公有成员和保护成员都成为子类的保护成员 |
private继承 | 变为private成员 | 变为private成员 | 不可见 | 基类的公有成员和保护成员都称为子类的保护成员 |
总结:
- 基类的
私有成员
在派生类中是不能被访问的,如果一些基类成员不想被基类对象直接访问,但需要在派生类中访问,就定义为保护成员
。可以看出保护成员限定符是因继承才出现的. - public继承是一个
接口继承
,保持is-a原则
,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象. protected/private继承
是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是has-a原则
,所以非特殊情况下不会使用这两种继承关系,在绝大多数场景下使用的都是公有继承- 不管是哪种继承方式,在派生类内部都可以访问基类的
公有成员
和保护成员
,但是基类的私有成员存在但是在子类中不可见
。(不能直接访问) - 使用关键字
class
时默认的继承方式是private
,使用struct
时默认的继承方式是public
二、继承与转换——赋值兼容规则——public继承
赋值兼容:不同类型数据之间的自动转换和赋值
基类和派生类对象之间也存在赋值兼容关系,在需要基类对象的任何地方,都可以用其派生类对象来替代.
class Person{
public:
protected:
string name;
};
class Student:public Person{
public:
int number;
};
- 派生类对象可以赋值给基类对象,即用派生类对象从基类继承来的数据成员,逐个赋值给基类对象的数据成员【切片/切割】(基类对象不能赋值给派生类对象)
Person p;
Student s;
p = s
- 派生类对象的地址可以赋值给指向基类对象的指针
Person* p1 = &s;
- 派生类对象可以初始化基类对象的引用
Person& r2 = s;
- 派生类的指针/引用不能指向基类对象(可以通过强制类型转换完成)
Student* p2 = (Student*)&p;
Student& r2 = (Student&)p;
p2->_stunum = 10;
r2._stunum = 20;//程序崩溃?
- 如果函数的形参是
基类对象
或基类对象的引用
,在调用函数时可以用派生类对象作为实参
三、继承体系中的作用域
- 继承体系中基类和派生类都有独立的作用域
- 基类和派生类中有同名成员时,构成隐藏,子类成员会隐藏父类成员的直接访问.(在子类成员中,可以通过
基类::基类成员
来访问) - 实际继承体系中最好不要定义同名成员
案列1
class Person{
public:
string _name;
int _stunum;
};
class Student : public Person{
public:
int _stunum;
};
void test(){
Person p;
p._stunum = 10;
Student s;
s._stunum = 20;
s.Person::_stunum = 200;
}
若要改变派生类的_stunum
,需要:s.Person::_stunum = 200;
案列2
class A {
public:
void fun1(){
std::cout<<"A::fun1()"<<std::endl;
}
};
class B : public A {
public:
void fun2(int i){
std::cout<<"B::fun2()"<<std::endl;
}
};
void test(){
B _b;
_b.fun();
}
上述代码执行后?
A.两个f1构成重载
B.两个f1构成隐藏
C.代码编不过
为什么不是重载而是隐藏?
B类
继承A类
,在B类
中出现两个同名的
成员函数fun()
,两者构成隐藏
,子类隐藏
父类的同名成员
,而重载
是在同一作用域下的
.所以此处的_b.fun()
调用的应该是子类的fun()
,但是此处未给fun()
传参,所以代码编译不通过
.
正确姿势:
_b.A::fun(); //基类的fun()
_b.fun(5); //派生类的fun()
案例3
class Person
{
public:
void f1()
{
cout << "Person::f1()" << endl;
}
string _name;
int _stunum;
};
class Student : public Person
{
public:
void f()
{
cout << "Student::f()" << endl;
}
void f1()
{
cout << "Student::f1()" << endl;
}
void f2()
{
cout << _stunum << endl;
cout << "Student::f2()" << endl;
}
void f3()
{
_stunum = 3;
cout << "Student::f3()" << endl;
}
void f4()
{
f();
cout << "Student::f4()" << endl;
}
int _stunum;
};
int main()
{
Student *p = NULL;
p->f1();
p->f2();
p->f3();
p->f4();
return 0;
}
p->f1();
代码编译通过,正常输出Student::f()
隐含的this指针虽然是空,但并未解引用,所以代码正常p->f2()
编译通过,程序会崩溃
this指针指向空,this->_stunum会崩溃
p->f3()
编译通过,程序会崩溃
this指针指向空,this->_stunum会崩溃p->f4()
代码正常输出 不会通过this指针去寻找f()这个函数 因为这个函数并不在对象上而是存在代码段上,同时隐含的this指针虽然是空,但是并未解引用,所以代码正常输出.
Student::f()
Student::f4()
四、派生类的6个默认成员函数
在继承关系中,若子类没有显示定义如下6个成员,编译系统会默认合成这6个默认成员函数:
- 构造函数
- 拷贝构造函数
- 析构函数
- 赋值运算符重载
- 取地址操作符重载
- const修饰的取地址操作符重载
案列:实现一个不能被继承的类
思路一:
类的对象
的构造是通过构造函数
实现的,如果将构造函数
声明为私有
或者保护
类型,那么在类外是不能调用构造函数
的,也就是不能在类外定义出对象
,这个类也就不能被继承
- 在类中定义一个
公有的
成员接口
返回成员函数生成的对象
,之后在类外定义类时,只需调用这个函数
即可. 但是,在类外
只能通过类的对象
调用类的成员函数
,因此这里应该将这个成员函数声明为静态的
.
class AA {
public:
static AA* getObjOnHeap(const int num){
return new AA(num); //在堆上
}
static AA getObjOnStack(const int num){
return AA(num); //在栈上
}
int getNum(){
return _aa;
}
private:
AA(const int aa) : _aa(aa) {} //构造函数
protected:
int _aa;
};
class BB : public AA {
};
思路二:
首先定义一个辅助类Help
,它的构造函数
是私有的
,然后让AA类
虚继承
Help类
,同时AA类
也要是Help类
的友元类
- Help类的作用是什么?AA类为什么要虚继承Help类?
虚继承
功能:当出现了菱形继承
的时候,使用虚继承
可以防止二义性,即派生类
不会继承多个基类.这里有什么用呢?
虚继承如何解决这种二义性的呢?
AA类的派生类的构造函数会直接去调用Help的构造函数,而Help的构造函数是私有的,因此AA类的派生类因此无法构造对象.
- AA类为什么是Help的友元类?
为了让AA类
直接访问Help类
的构造函数,AA类
要构造对象.
template <typename T>
class Help {
friend T; //T类是一个模板类 是Help类的模板类
public:
private:
Help(){}
~Help(){}
};
class AA : virtual public Help<AA> {
public:
AA(const int aa) : _aa(aa) {}
~AA() {}
int get_aa(){
return _aa;
}
protected:
int _aa;
};
c++11的已经加入了final关键字,直接在类后面加上final关键字,就可以防止该类被继承
五、单继承&多继承
-
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
-
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
- 菱形继承
- 菱形继承对象模型:
Assistant的对象中有两份Person成员,菱形继承存在二义性和数据冗余的问题
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 _major; //专业 一个在读博士生是学生 还是本科生的老师
};
void test(){
Assistant a;
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
system("pause");
}
虚继承:解决菱形继承的二义性和数据冗余的问题
- 虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间问题
- 虚继承体系看起来好复杂,在实际应用中我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗.
class Person {
public:
string _name;
};
class Student : virtual public Person {
protected:
int _num;
};
class Teacher : virtual public Person {
protected:
int _id;
};
class Assistant : public Student, public Teacher {
protected:
string _major;
};
void test(){
Assistant a;
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
system("pause");
}
这样改变一个另一个也会改变:
底层实现方式【待补充】
六、虚函数&多态
虚函数:类的成员函数前面加virtual关键字,则这个成员函数称为虚函数
虚函数重写:当在子类定义了一个与父类完全相同的虚函数时,则称子类的这个函数重写(也称覆盖)了父类这个虚函数
多态:
- 不同对象收到相同的消息时,产生不同的动作
- 用一个名字定义不同的函数,这些函数执行不同但又相似的动作,从而可以使用相同的方式来调用这些具有不同功能的同名函数
当使用基类的指针或引用调用重写的虚函数时,当指向父类调用的就是父类的虚函数,指向子类调用的就是子类的虚函数
- 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同(协变除外)
- 基类中定义了虚函数,在派生类中该函数始终保持虚函数特性
- 只有类的成员函数才能定义为虚函数
- 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual
- 构造函数不能为析构函数,虽然可以将operator=定义为虚函数,但是最好不要这样做,因为在使用时容易引起混淆
- 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义行为
- 最后把类的析构函数声明为虚函数. (why? 另外析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是是构成覆盖,这里因为编译器做了特殊处理,编译器编译时会都变为destucter)
纯虚函数:
在成员函数
的形参
后面写上 = 0
,则成员函数为纯虚函数
. 包含纯虚函数
的类叫做抽象类
(也叫接口类
),抽象类不能实例化对象
.纯虚函数在派生类
中定义以后,派生类才能实例化出对象
- 友元与继承
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
class Student;
class Person {
friend void display(Person& p, Student& s);
public:
string get_name(){
return _name;
}
protected:
string _name;
};
class Student : public Person{
public:
int get_stuNum(){
return _stuNum;
}
protected:
int _stuNum;
};
void display(Person& p, Student& s){
std::cout<<p._name<<std::endl;
std::cout<<s.get_name()<<std::endl;
// display()不是Student的友元函数 不能直接访问继承来的_name
// std::cout<<s._name<<std::endl;
std::cout<<s.get_stuNum()<<std::endl;
//display()不是Student的友元函数 不能直接访问_stuNum
//s._stuNum 的方式是错误
}
void test(){
Person p;
Student s;
display(p, s);
}
- 继承与静态成员
基类定义了static成员,则整个继承体系里面只有一个这样的成员.无论派生出多少个子类,都只有一个static成员实例
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 test(){
Student s1;
Student s2;
Student s3;
Graduate s4;
std::cout<<"num of person:"<<Person::_count<<std::endl;
Person::_count = 0;
std::cout<<"num of person:"<<Person::_count<<std::endl;
}
输出 4 0