目录
一、概念及定义1.1 概念1.2 定义1.3 继承方式与访问权限 二、基类与派生类对象的赋值转换三、继承中的作用域四、派生类的默认成员函数五、继承与友元六、继承与静态成员变量七、菱形继承与菱形虚拟继承八、继承与组合
一、概念及定义
1.1 概念
以前我们的接触过函数复用,而继承就是一种类复用,减少代码的重复性。继承可以在原有类的基础上扩展新的功能,产生新的类叫派生类或子类,原有类叫基类或父类。比如说学生类和老师类,它们共同的成员变量有名字和年龄,这时就可以写个Person类来处理公共的成员变量,不需要学生类和老师类自己再写名字和年龄的成员变量,只要写自己的独有的那部分即可。
1.2 定义
继承的写法:
class B : public A
B是子类,A是父类,中间要加冒号,public是继承方式(继承方式有3种)
如果是多继承:用逗号分开
class C : public A, public B
看下面代码:
class Person{public:void Print(){cout << _name << endl;cout << _age << endl;}protected:string _name = "yss";int _age = 19;};class Student : public Person{public:private:int _stuid;};class Teacher :public Person{public:private:int _jobid;};int main(){Student s;s.Print();return 0;}
Student类是子类,它的对象可以继承父类的Print函数,同时把父类的名字和年龄也继承下来。如果子类自己有Print函数,根据就近原则,那么只会调用自己的Print函数。
总结一下:
子类没有,父类有,子类对象用父类的;
不管父类有没有,子类有,子类对象用自己的
1.3 继承方式与访问权限
前面说到,继承方式有3种,分别是:
publicprotectedprivate访问权限也有3种,分别是:
public——类内外都可以访问protected——子类和自己可以访问private——只能自己访问它们之间的关系如下图:
总结为以下几点:
二、基类与派生类对象的赋值转换
基类对象与派生类对象之间是可以进行赋值转换的,只能派生类对象赋值给基类,不能基类对象赋值给派生类对象。这种转换也叫切割或者切片,是把派生类中继承基类的那部分切割出来再赋值给基类。派生类的对象可以赋值给基类的对象/指针/引用。
写法:
B bb;//子类对象A a1 = bb;//赋值给基类对象A* a2 = &bb;//赋值给基类指针A& a3 = bb;//赋值给基类引用
调试证明子类对象赋值给基类对象:
三、继承中的作用域
继承中子类和父类是两个是独立的作用域,父类和子类出现同名的成员,子类会屏蔽父类对同名成员的访问,这种情况叫隐藏,如果是函数,仅需要函数名相同即可。前面谈过,如果子类和父类出现同名的函数或者同名的成员,子类对象优先使用自己的,要使用父类的需要域作用限定符——::,指明调用父类的。
同名成员变量构成隐藏关系:
class Person{public:protected:string _name = "yss";int _age = 19;};class Student : public Person{public:void Print(){cout << _name << endl;cout << _age << endl;}private:string _name = "yyy";int _age = 29;int _stuid;};
调用父类的:
cout << Person::_name << endl;//类名+::cout << Person::_age << endl;
同名成员函数构成隐藏关系:
class Person{public:void Print(){cout << "Person::void Print()" << endl;}protected:string _name = "yss";int _age = 19;};class Student : public Person{public:void Print(){Person::Print();cout << "Student::void Print()" << endl;}private:int _stuid;};
调用子类的:
//Person::Print();cout << "Student::void Print()" << endl;
调用父类的:
Person::Print();//cout << "Student::void Print()" << endl;
四、派生类的默认成员函数
默认成员函数中的默认指没有传参的,或者我们不写编译器自动生成的。默认成员函数,对于内置类型完成值拷贝,对于自定义类型调用它的构造函数。继承中,派生类的有一部分是基类的,要完成这些基类的成员的构造/拷贝/赋值就必须先调用基类的构造函数/拷贝构造/赋值重载,再完成自己的。 析构函数呢?后面再说。
1️⃣构造:
class Person{public:Person(const char* name):_name(name){cout << "Person(const char* name)" << endl;}protected:string _name;};class Student : public Person{public:Student(const char* name, const int num):_num(num), Person(name)//调用父类的构造函数{cout << "Student(const char* name, const int num)" << endl;}private:int _num;};int main(){Student s("yss", 122);return 0;}
注:构造顺序与初始化列表的顺序无关,与成员变量声明的顺序有关。所以这里都是先完成父类成员的构造,再完成子类成员的构造
2️⃣拷贝构造:
Person(const Person& p):_name(p._name){cout << "Person(const Person& p)" << endl;}Student(const Student& s):_num(s._num),Person(s){cout << "Student(const Student& s)" << endl;}
注:调用父类的拷贝构造时,传的参数是子类的对象,而父类的形参是父类的对象,这里就用到了切片的知识,即子类的对象可以赋值给父类对象,子类对象转换为父类对象。
3️⃣赋值重载:
Person& operator=(const Person& p){if (this != &p){_name = p._name;cout << "Person& operator=(const Person& p)" << endl;}return *this;}//Student& operator=(const Student& s){if (this != &s){_num = s._num;Person::operator=(s);cout << "Student& operator=(const Student& s)" << endl;}return *this;}
注:父类与子类的赋值重载函数的函数名都是operator=,构成隐藏,因此要用域作用限定符指明调用父类的。
4️⃣析构:
~Person(){cout << "~Person()" << endl;}/~Student(){Person::~Person();cout << "~Student()" << endl;}
注:父类的析构与子类的析构不同名,为什么还构成隐藏,因为多态的一些原因,析构函数被特殊处理,父类的析构和子类的析构都被处理为destructor()
看下运行结果:
这里父类析构了两次,为什么呢?再看看在子类的析构函数里注释掉调用父类析构会怎样。
//Person::~Person();
我们发现析构函数的调用顺序与前面几个都不同,它是先调用子类的析构,再调用父类的析构。因为如果是先调用父类的析构,父类的成员此时就被清理了,假如子类有使用父类的成员(一般来说不会用),不就野指针了吗?所以为了保证不出错,析构的顺序是先子后父。
五、继承与友元
先直接上结论:友元不能被继承
例子:
class B;//声明B类,因为在A类里display函数的参数有B类类型的参数class A{public:friend void display(const A& a, const B& b);protected:int _a = 10;};class B :public A{public:protected:int _b = 20;};void display(const A& a, const B& b){cout << a._a << endl;//可访问cout << b._b << endl;//不可访问}int main(){A aa;B bb;display(aa, bb);return 0;}
子类也设置友元函数:
friend void display(const A& a, const B& b);
运行结果:
六、继承与静态成员变量
如果在基类定义一个静态成员变量,那么这个变量将在整个继承体系中起作用。
class A{public:A(){count++;}static int count;};int A::count = 0;class B :public A{public:B(){count++;}};int main(){B b;cout << b.count << endl;return 0;}
子类对象先调用父类的构造函数再调用自己的,一共是两次
七、菱形继承与菱形虚拟继承
菱形继承是在有多继承的情况下出现的,具体如下图:
菱形继承有哪些缺陷,看以下代码:
class Person{public:string_name;};class Student :public Person{protected:int _stuid;};class Teacher :public Person{protected:int _jobid;};class Mine :public Student, public Teacher{protected:int _age;};int main(){Mine m;m._name = "yss";return 0;}
Student类继承了_name,Teacher类也继承了_name,Mine类继承这两个类时不知道用谁的_name,有二义性。
解决二义性的办法:域作用限定符指明用哪个类的
m.Student::_name = "yss";m.Teacher::_name = "yyy";
但是还要一个问题——数据冗余。既然都是_name,为何不让它存在一个公共的区域呢
这里就需要使用虚拟继承,使用方法是在两个类继承同一个类时在这两个类设置virtual关键字。
代码:
class Student :virtual public Personclass Teacher :virtual public Person
这时不需要指明谁的了(当然,想指明谁也行),因为在同一个区域,后面定义覆盖前面定义的,不仅解决了二义性,而且解决了数据冗余问题。
它们的地址是一样的。
前面我们只是知道是什么,怎么办,但是我们还需要知道为什么,即菱形继承和菱形虚拟继承的原理
菱形继承的原理:
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 dd;dd.B::_a = 1;dd.C::_a = 2;dd._b = 3;dd._c = 4;dd._d = 5;return 0;}
菱形虚拟继承原理:
先加上virtual
class B : virtual public Aclass C :virtual public A
打开调试:
虚基表指针分别指向一个虚基表,可以找到表中的偏移量,通过偏移量就可以找到公共区域
定义其他类的对象也是同理的,通过切片转换。
总结:
多继承是C++语法的一个缺陷,有了多继承就会有菱形继承,从而导致一些问题,所以一般都是写单继承,而不是多继承
八、继承与组合
public继承是一种is-a的关系(也叫白箱复用),也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系(也叫黑箱复用),假设B组合了A,每个B对象中都有一个A对象。
继承的耦合度高,改变基类容易影响派生类;组合的耦合度低,两个类的关系不大。所以一般来说能用组合就用组合。