继承
- 前言
- 继承的概念及定义
- 继承的概念
- 继承定义
- 继承关系和访问限定符
- 基类和派生类对象赋值转换
- 继承中的作用域
- 派生类的默认成员函数
- 继承与友元
- 继承与静态成员
- **多重继承**
- 多继承下的类作用域
- 菱形继承
- 虚继承
- 使用虚基类
- 支持向基类的常规类型转换
前言
在需要写Father类和Mother类的时候,需要给这两个类写一些属性,像 名字,性别,年龄,爱好,电话,家庭地址等
,这两个类中会有一些共同的属性,把这些公共的属性进行提取,封装成一个Person类,Father和Mother继承Person,就不需要在写共同的属性了。这就是本章要说的继承。
继承的概念及定义
继承的概念
继承机制是面向对象程序设计是代码可以复用的最重要的手段,它允许程序猿在保持原有类特性的基础上进行扩展,增加功能,这样产生的新的类,称为派生类。继承呈现了面向对象程序设计的层次结构。层次结构的根部有一个基类,派生类就是直接或间接的从基类继承而来。基类负责定义在层次结构中所有类的共同拥有的成员,而每个派生类定义各自特有的成员。
class Person
{
public:void Print(){cout << "name>>" << _name << endl;cout << "age>>" << _age << endl;}string _name = "A";int _age = 18;
};class Student : public Person
{
protected:int _stuid = 123;
};int main()
{Student s;s.Print();return 0;
}
通过调试可以看出,派生类中有基类的成员,也有自己定义的特殊的成员。
继承定义
派生类 继承方式 基类
class Student : public Person
继承关系和访问限定符
继承基类的时候,访问限定符的不同,成员访问也会变化。
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类的不可见 | 在派生类中不可见 |
-
基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
-
基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。。
-
实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。
-
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
-
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强 。
基类和派生类对象赋值转换
通常情况下,如果我们想把引用活指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,或者对象的类型含有一个可接受的const类型转换规则。存在继承关系的类是也给重要的列外:我们可以将基类的指针或引用绑定到派生类对象上。 见上面代码将Student对象的地址赋值给Person*
。
之所以存在派生类向基类的类型转换是因为每个派生类对象都包含了一个基类的部分,而基类的引用或者指针可以绑定到该基类的部分上。
Student s;
Person* p1 = &s;
Person& p2 = s;
p1的过程中派生类会将父类的那一部分切出来拷贝过去,p2的过程则是子类中父类的部分的别名。只有派生类对象中的基类部分会被拷贝,移动或者赋值,它的派生类部分将被忽略掉。
s.Print();p1->_name = "B";p1->Print();p2._name = "C";p2.Print();
编译器在编译时无法确定某个特定的转换在运行时是否安全,这是因为编译器只能通过检查指针或者引用的静态类型来推断该转换是否合法。如果在基类中含有一个或多个虚函数,我们可以使用 dynamic_cast
请求一个类型转换,该转换的安全检查将在运行时执行。
- 从派生类向基类的类型转换只对指针或者引用类型有效
- 基类向派生类不存在隐式类型转换
- 和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行
自动类型转换只对指针或者引用有效,但是继承体系中的大多数类仍然(显示或隐式的)定义了拷贝控制成员。因此,我们通常能够将一个派生类对象拷贝,移动,或者赋值给一个基类对象。不过这种操作只处理派生类中的基类部分。
继承中的作用域
每个类定义自己的作用域,这个作用域内我们定义的成员,当存在继承关系时,派生类的作用域嵌套,在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析时,编译器将继续在外层的基类作用域中寻找改名字的定义。
和其他作用域一样,派生类也能重用定义在其直接基类或简介基类中的名字,此时定义在内层作用域(既派生类)的名字将隐藏定义在外层作用域(既基类)的名字。
class Person
{
public:void Print(){cout << "name>>" << _name << endl;cout << "age>>" << _age << endl;}string _name = "A";int _age = 18;
};class Student : public Person
{
public:void Print(){cout << "age>>" << _age << endl;cout << "Person.age>>" << Person::_age << endl;}protected:int _stuid = 123;int _age = 20;
};int main()
{Student s;s.Print();return 0;
}
输出结果是 20 18
派生类的成员将隐藏同名的基类成员。
如果想要访问基类中的成员,可以使用作用域运算符来使用隐藏的成员
class Person
{
public:void Print(){cout << "Person::Print()" << endl;}string _name = "A";int _age = 18;
};class Student : public Person
{
public:void Print(int i){cout << "Student::Print()" << endl;}
protected:int _stuid = 123;int _age = 20;
};
代码中的两个Print是什么关系呢?
隐藏
为什么不是重载呢?同名函数,参数不同,构成重载很合理。
但是重载有一个限定,同一个作用域。
派生类的默认成员函数
class Person
{
public:Person(const char* name = "A"):_name(name){}protected:string _name;int _age = 18;
};class Student : public Person
{
public:Student(const char* name = "B"):_id(0),_name(name){}
protected:int _id;
};
在父类中写了构造函数后,想在子类中给继承而来的name成员进行初始化,但是Student中的构造函数给name成员初始化的操作却报错了。
因为派生类的构造只能构造在派生类中新增的成员,要想调用基类的构造函数可以在派生类的初始化列表中加上 Person(name)
Student(const char* name = "B"):_id(0),Person(name)
如果不写 Person(name)
的话,派生类在执行构造函数的时候,会自动的去调用基类的构造函数(要有默认参数,不然会报错).
Student(const Student& s):Person(s) // 将基类 = 派生类,会有切片操作,将属于基类的部分进行切割。,_id(s._id){}
拷贝构造中也要调用父类Person才能完成。
如果在拷贝构造中不写Person(s),就会去调用默认拷贝构造函数。
Student& operator= (const Student& t){cout << "Student& operator= (const Student& t)" << endl;if (this != &t){Person::operator=(t);// 不写Person:: 会造成隐藏_id = t._id;}return *this;}
Person& operator= (const Person& n){cout << "Person& operator= (const Person& n)" << endl;if (this != &n){_name = n._name;}return *this;}
在基类和派生类中分别写一个赋值运算符,派生类也需要调用父类的赋值运算符。
在析构函数体执行完后,对象的成员会被隐式销毁,类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源。
class Person
{
public:Person(const char* name = "A"):_name(name){cout << "Person" << endl;}Person& operator= (const Person& n){cout << "Person& operator= (const Person& n)" << endl;if (this != &n){_name = n._name;}return *this;}~Person(){cout << "~Person" << endl;}protected:string _name;
};class Student : public Person
{
public:Student(const char* name = "B"):_id(0),Person(name){cout << "Student(const char* name = 'B')" << endl;}Student& operator= (const Student& t){cout << "Student& operator= (const Student& t)" << endl;if (this != &t){Person::operator=(t);_id = t._id;}return *this;}~Student(){Person::~Person();cout << "~Student" << endl;}Student(const Student& s):Person(s),_id(s._id){}protected:int _id;
};int main()
{Student s("张三");return 0;
}
后面由于多态的原因,析构函数的函数名被特殊处理了,统一处理成destructor。
在执行代码后
先给基类初始化,然后派生类初始化,在调用析构发现,基类的析构多调用了一次。
对象销毁的顺序正好与创建的顺序相反,创建对象的时候是先调用父类的构造函数,这里子类析构函数首先执行,然后是父类的析构函数,以此类推,沿着继承体系的反方向直至最后。
继承与友元
就像友元关系不能传递一样,有缘关系同样也不能继承,基类的友元在访问派生类成员时不具有特殊属性,类似的,派生类的友元也不能随意访问基类的成员。
class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name = "S"
};class Student : public Person
{
protected:int _id = 1;
};void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._id << endl;
}int main()
{Person p;Student s;Display(p, s);return 0;
}
不能继承友元关系,每个类负责控制各自成员的访问权限。
继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系只存在该成员的唯一定义。不管有多少个派生类,静态成员只有一个实例。
class Student;
class Person
{
public:string _name = "S";static int num;
};int Person::num = 0;class Student : public Person
{
protected:int _id = 1;
};int main()
{Person p;Student s;cout << &s.num << endl;cout << &p.num << endl;return 0;
}
输出结果会发现,这两个地址是一样的。
静态成员遵循通用的访问控制规则。
多重继承
一个子类只有一个直接父类时称这个继承为单继承。
多重继承是指一个子类有两个或以上直接父类,多继承的派生类继承了所有父类的属性。从概念上看不是很难,但是多个基类相互交织就会产生一些特殊的问题。
构造一个派生类的对象将同时构造并初始化它的所有基类子对象。与从一个基类进行的派生一样,多继承的派生类的构造函数初始值也只能初始化它的直接基类。
多继承下的类作用域
在只有一个基类的情况下,派生类的作用域嵌套在直接基类和间接基类的作用域中。查找过程沿着继承体系自底向上进行,直到找到所需要的名字,派生类的名字将隐藏基类的同名成员。
在多重继承的情况下,相同的查找过程在所有直接基类中同时进行。如果名字在多个基类中都被找到了,则对该名字的使用将具有二义性。
菱形继承
一个学生,它继承了A,也继承了B,A和B都继承了Person,前面也说过,派生类会继承基类的所有属性,如果A中存在age,B中也存在age,在Student中访问age就会导致二义性(不知道访问哪个),无法明确知道访问的是哪一个。
class Person
{
public:string _name;
};class A : public Person
{
public:string _id;
};class B : public Person
{
public:string _num;
};class Student : public A, public B
{
public:string _sex;
};int main()
{Student s;s._name = "张三";return 0;
}
当然,可以通过指定访问哪个父类的成员可以解决二义性的问题,但是数据冗余(浪费空间)的问题无法解决。
Student s;s.B::_name = "张三";s.A::_name = "李四";cout << s.B::_name << endl;cout << s.A::_name << endl;
当一个类拥有多个基类,有可能出现派生类从两个或更多基类中继承同名成员的情况,此时,不加前缀限定符直接使用该名字会引发二义性。
虚继承
在默认情况下,派生类中含有继承链上的每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中将包含该类的多个子对象。也就是上面的菱形继承造成的二义性和数据冗余问题。
C++给出了虚继承的机制来解决这两个问题。虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
class Person
{
public:string _name;
};class A : virtual public Person
{
public:string _id;
};class B :virtual public Person
{
public:string _num;
};class Student : public A, public B
{
public:string _sex;
};int main()
{Student s;s._name = "张三";cout << s._name << endl;return 0;
}
#include <iostream>using namespace std;class A
{
public:int _a;
};class B : public A
{
public:int _b;
};class C : public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
在调试的时候,查看内存,可以发现,这些数据都是挨着放的。
这是通过前缀限定符解决二义性问题的,可以看出,数据冗余问题还存在。
下面看通过虚继承的方式。
在代码最后添上了 d._a = 0;
这样,就没有数据冗余和二义性了。但是BC类中多了两个东西。
把这两个地址查看一下发现,指向的都是0,但是这个地址的下一个位置存的都有数据。其实通过计算可以发现,1到6的偏移量刚好是20,3到6的偏移量刚好是12.所以BC中多出来的地址存的是距离A的偏移量(相对距离)。
存找基类偏移量的表叫虚机表
为什么要把偏移量给存下来呢?上面说的切片操作,基类以引用或者指针的方式,把派生类=基类。
使用虚基类
我们指定虚基类的方式是添加关键字 virtual
class B :virtual public Person
通过上面的代码,我们将Person 定义为 B的虚基类。
vitrual说明符表示一种愿望,即在后续的派生类当中共享虚基类的同一份实例。
支持向基类的常规类型转换
不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。