文章目录
- 前言
- 一、继承的语法
- 二、基类和派生类对象赋值转换
- 1.例子
- 2.继承中的作用域
- 3.派生类的默认成员
- 4.继承与友元
- 5.继承与静态成员
- 6.复杂的菱形继承和菱形虚拟继承
- 总结
前言
一、继承的语法
- class Person
- {
- public:
- void Print()
- {
- cout << "name:" << _name << endl;
- cout << "age:" << _age << endl;
- }
- protected:
- string _name = "peter";
- int _age = 18;
- };
- class Student:public Person
- {
- protected:
- int _stuid; //学号
- };
-
- int main()
- {
- Student s;
- s.Print();
- return 0;
- }
我们可以看到上面的代码中student类并没有print函数以及name和age变量,但是在调用的时候竟然能调用父类的成员函数:
为什么能成功调用呢?因为student继承了person,所以继承是完成了复用。父类也可以叫做基类,子类也可以叫做派生类。但是在继承中还分三种继承方式,公有继承,私有继承,保护继承,下面我们来看一下继承的规则:
我们通过上面的表格其实可以发现一些规律,比如公有继承中公有成员还是公有,保护成员还是保护,私有成员还是私有。其实规律就是类成员的继承方式是根据权限小的那个继承方式继承的。比如保护继承中公有成员还是保护(因为保护的权限小于公有的权限), 保护成员还是保护,私有成员还是私有因为私有的权限小于保护,而私有继承由于权限已经是最小的所以成员都是私有的,并且私有成员在派生类中不可见。
总结:
- class Person
- {
- public:
- void Print()
- {
- cout << "name:" << _name << endl;
- cout << "age:" << _age << endl;
- }
- protected:
- string _name = "peter";
- private:
- int _age = 18;
- };
- class Student :public Person
- {
- public:
- void func()
- {
- cout << "_name:" << _name << endl;
- //cout << "_age" << _age << endl;
- }
- protected:
- int _stuid; //学号
- };
- int main()
- {
- Student s;
- s.Print();
- s.func();
- return 0;
- }
我们发现受保护的成员派生类可以调用,而私有成员不可以访问:
我们可以看到保护和私有在当前类没有区别,在派生类就不一样了,私有在派生类不可见,而保护是在子类是可见的那么在什么时候我们会定义私有呢?当我们不想被子类继承就可以定义为私有。那我们将成员设为私有有什么办法可以在派生类中使用呢?当然可以,我们只需要在子类中调用父类的函数即可,如下:
- class Person
- {
- public:
- void Print()
- {
- cout << "name:" << _name << endl;
- cout << "age:" << _age << endl;
- }
- private:
- string _name = "peter";
- int _age = 18;
- };
- class Student :public Person
- {
- public:
- void func()
- {
- Print();
- }
- protected:
- int _stuid; //学号
- };
- int main()
- {
- Student s;
- s.Print();
- s.func();
- return 0;
- }
当然,我们的继承方式可以像类中默认权限一样可以不写,不写默认是私有继承,如下图:
同样的,struct的默认权限为公有,struct的默认继承权限也是公有的。
二、基类和派生类对象赋值转换
1.例子
我们先看一下下面的代码:
- int main()
- {
- int i = 5;
- double d = i;
- return 0;
- }
我们之前说过,像这样的两个类型不相同的赋值一定会发生隐式类型转换。int类型的i先给一个double类型的临时变量,再将临时变量给double d这个值。那么如果我们将一个子类给一个父类对象会发生什么呢?
- int main()
- {
- Student s;
- Person p = s;
- return 0;
- }
在公有继承下,子类可以赋值给父类,这里是天然的,不存在类型转换发生。因为在公有继承下,子类是一个特殊的父类,那么子类会有可能比父类多出来变量或对象,那该怎么解决呢?
其实就是把子类中父类的那部分切出来然后给父类,下面我们来验证一下:
我们不能直接将d给int &是因为这里发生了隐式类型转换,而临时变量是具有常性的所以我们加个const就解决了:
那么如果是父类和子类呢?我们试试:
我们能看到父类的引用能直接引用子类并且没有报错说"非常量限定",这就说明子类到父类的没有隐式类型转换,这也就证明了我们刚刚说的子类赋值给父类是天然的,不存在类型转换。那么子类可以赋值给父类,能把父类赋值给子类吗?
我们可以看到是不能的,下面我们总结一下:
2.继承中的作用域
- 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();
- };
-
- int main()
- {
- Test();
- return 0;
- }
我们可以看到person中有一个_num变量,student中也有一个同名的_num变量,在这种情况下我们如何知道要调用的是哪个变量呢?当我们想用父类的变量的时候我们需要在前面加上域作用限定符,子类的话直接用变量名即可,像上面代码这种情况就是父类的num和子类的num构成了隐藏。
当我们在子类中将域作用限定符拿掉,会自动调用子类中的同名变量num。
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
我们接着往下看:
- 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;
- }
- };
-
- int main()
- {
- B b;
- b.fun(10);
- return 0;
- }
A中的fun和B中的fun是什么关系呢?参数不同是函数重载吗?不是!因为重载是在同一个作用域,这里都不是一个作用域肯定不是函数重载了,那么是隐藏吗?答案是是的,因为成员函数只要函数名相同那就构成隐藏,隐藏默认调用本类的成员函数,想要调用父类的需要加域作用限定符。
3.派生类的默认成员
1.派生类的构造函数
- 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
- {
- protected:
- int _num; //学号
- };
-
- int main()
- {
- student s;
- return 0;
- }
我们看上面的代码,子类什么函数也没实现,现在我们创建一个子类对象,然后看看什么结果:
我们发现我们创建子类的对象,竟然调用的是父类的构造函数和析构函数,刚刚我们没写子类的构造函数,现在我们写一个看是什么结果:
我们发现居然报错非法的成员初始化,我们继承父类是有string _name的这是为什么呢?其实c++规定,在子类中初始化父类的成员要用父类的构造函数初始化,也就是说子类的归子类管,父类的归父类管,所以正确的构造函数应该是这样:
- student(const char* name,int num)
- :Person(name)
- ,_num(num)
- {
- cout << "student(const char* name)" << endl;
- }
上面是我们写了用父类的构造函数,如果我们不写会调用谁呢?:
我们在调试的时候发现,当我们没有显式调用父类的构造函数的时候编译器也会默认去初始化列表调用父类的构造函数,也就是说我们不写也可以完成任务。如果父类没有默认的构造该怎么办?那我们就必须显式的去调用了。
下面我们解释一下派生类的默认成员函数:
有了上面的知识我们再来实现一下子类的拷贝构造函数:
- student(const student& s)
- :Person(s)
- ,_num(s._num)
- {
- cout << "student(const student& s)" << endl;
- }
与刚才的构造函数不同的是,我们不写父类的构造函数是不会调用父类的构造函数的:
- student(const student& s)
- //:Person(s)
- :_num(s._num)
- {
- cout << "student(const student& s)" << endl;
- }
所以:派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝初始化。
下面我们再写一个赋值重载:
- student& operator=(const student& s)
- {
- if (this != &s)
- {
- operator=(s);
- _num = s._num;
- }
- return *this;
- }
我们发现出错了,栈溢出了,这是为什么呢?其实回想我们刚刚讲的知识,成员函数只要函数名相同就构成隐藏,也就是说子类和父类的operator=构成隐藏了,我们默认调用的是我们自己的赋值重载,而我们本意是想调用父类的赋值重载,所以我们修改一下:
- student& operator=(const student& s)
- {
- if (this != &s)
- {
- Person::operator=(s);
- _num = s._num;
- }
- return *this;
- }
这次我们发现成功赋值了,并且确实调用了父类的赋值。
下面我们实现一下析构函数,对于析构函数我们也像刚刚的思想一样:父类的东西让父类析构,然后子类再析构:
- ~student()
- {
- ~Person();
- cout << "~student()" << endl;
- }
然而当我们写出来却发现编译不过去:
这是为什么呢?因为每个类的析构函数都会被编译器处理为destructor(这个单词就是析构的意思),也就是说父类和子类的析构函数名字是一样的,又构成隐藏了,刚刚默认调用我们子类自己的析构所以出错了,下面我们修改一下:
- ~student()
- {
- Person::~Person();
- cout << "~student()" << endl;
- }
并且我们只保留一个对象来观察,下面我们来看看运行结果吧
这里怎么先调用了父类的析构,又调用了子类的析构又调用了父类的析构,我们就一个对象怎么调用多了一次父类的析构呢?按理说只有一个父类一个子类才对,这是怎么回事呢?我们先检查一下哪里多调用了:
我们发现在子类的析构中将父类的调用代码注释掉就没了那个多余的父类析构,这是什么原因呢?其实这是因为子类中析构函数不要显示的调用父类的析构,因为会自动调用父类的析构,为什么要这样做呢?因为要保证先后顺序,我们都知道,先声明的对象后析构,如下图:
所以要满足这样的规则我们就不能在析构函数中显式的调用父类的析构函数,因为如果我们显式调用那么就不能保证先构造的后析构的顺序了。所以:子类析构函数完成时,会自动调用父类的析构函数,保证先析构子再析构父。如下图:
对于为什么先析构子在析构父还有一个主要的原因,由于子类继承父类可能会比父类多出成员,一旦子类中有一个父类的指针,指针指向一段空间,一旦将父类析构了那么这个指针就变成野指针了,子类中用这个指针指向任意的成员都会报错,所以为了安全性而言也要先调用子类的析构再调用父类的析构。
4.继承与友元
我们先说结论:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
- class Student;
- 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;
- cout << s._stuNum << endl;
- }
- void main()
- {
- Person p;
- Student s;
- Display(p, s);
- }
对于上面的继承关系,person有一个友元函数,这个函数可以访问父类的成员变量,当子类继承父类后,我们用这个函数是会报错的,因为友元关系是不会被继承的:
那么该如何解决这种情况呢?我们只需要在子类中也声明一下友元关系即可:
- class Student;
- class Person
- {
- public:
- friend void Display(const Person& p, const Student& s);
- protected:
- string _name = "peter"; // 姓名
- };
- class Student : public Person
- {
- friend void Display(const Person& p, const Student& s);
- protected:
- int _stuNum = 10086; // 学号
- };
- void Display(const Person& p, const Student& s)
- {
- cout << p._name << endl;
- cout << s._stuNum << endl;
- }
- int main()
- {
- Person p;
- Student s;
- Display(p, s);
- return 0;
- }
5.继承与静态成员
- 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();
- return 0;
- }
我们可以看到,静态成员变量是所以对象所共有的,无论继承多少次,都只有一个count。下面我们验证一下:
通过上图可以看到父类中的name和子类中的name根本不是一个name,那么count呢?
通过验证我们也能发现静态成员变量确实只有一个,不管继承了多少次。
下面我们实现一个不能被继承的类:
- class A
- {
- private:
- A()
- {
-
- }
- };
- class B :public A
- {
-
- };
-
- int main()
- {
- B bb;
- return 0;
- }
一旦我们将构造函数私有化了那么就不能继承了。
这样的情况是因为我们不想我们的类被继承,那我们本类自己如何调用这个私有化的构造函数呢?
- class A
- {
- public:
- static A CreateObj()
- {
- return A();
- }
- private:
- A()
- {
-
- }
- };
- class B :public A
- {
-
- };
-
- int main()
- {
- //B bb;
- A::CreateObj();
- return 0;
- }
我们可以实现一个函数让这个函数返回一个A类型的匿名对象即可,由于函数必须由对象去调用我们无法创建一个对象,所以我们将这个函数设为static静态的函数,这样就可以用类名去调用这个函数了。
6.复杂的菱形继承与菱形虚拟继承
我们先看看单继承和多继承的区别:
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况
菱形继承有数据冗余和二义性的问题,现在我们通过代码调试来观察:
- 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 _majorCourse; // 主修课程
- };
- void Test()
- {
- // 这样会有二义性无法明确知道访问的是哪一个
- Assistant a;
- a._name = "peter";
- // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
- a.Student::_name = "xxx";
- a.Teacher::_name = "yyy";
- }
首先在上面的代码中类assistant继承了student和teacher,所以我们创建了assistant对象后发现访问成员变量name无法访问,编译器报错不明确,如下图:
这个问题解决起来很简单,就是将我们的变量前面加上域作用限定符,然后指定访问的是哪一个name变量,如下图:
那么数据冗余是什么呢?当一个子类继承两个父类的时候,这两个父类同样是继承另一个父类的,这样就会有很多相同的数据被继承到了子类中,一旦在项目中代码非常多非常复杂那么这种情况是非常浪费空间的,那么这个问题c++的祖师爷是如何解决的呢?这里就引入了虚继承,虚继承可以解决数据冗余和二义性的问题:
虚继承的语法就是在原先继承方式的前面加上virtual关键字。下面我们通过一个简单的菱形继承模型来看看c++祖师爷是如何解决数据冗余和二义性的问题:
- class A
- {
- public:
- int _a;
- };
- // class B : public A
- class B : virtual public A
- {
- public:
- int _b;
- };
- // class C : public A
- class C : virtual 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;
- }
我们先看一下不加virtual继承的情况:
通过上图我们可以看到B中继承的a在内存第一行第二行就是B中的成员b,C中继承的a在内存中第三行,最后一个红色的是d的值。那么我们再看看虚继承的结果:
我们发现虚继承中的内存地址和我们刚刚看到的完全不一样,虚继承里面存放的竟然是指针,B类中有一个指针和3这个值,C类中有一个指针和4这个值,那么这能代表什么呢?我们再开一个内存来看看刚刚里面存放的地址到底是什么东西:
我们发现这个地址里面的开头都是0,这是什么意思呢?
我们通过那个0下面的值发现,16进制转化为十进制分别是20和12,然后我们试着在第一个地址加上这个值发现,上面的地址加上这个值正好指向了A:
也就是说虚继承解决数据冗余和二义性的本质是通过偏移量找到A让其这三个类中的a变量都指向父类的那个变量的地址。
总结:
下面我们做一道多继承的题:
对于上面这道题选哪个选项呢?我们来解释一下:
首先D类指针p开了一个D的空间,然后进入D的构造函数,在D的构造函数中我们发现初始化列表对三个类都进行了初始化,而初始化的顺序是谁先继承谁就先初始化,A先被B继承所以先去调用A的构造函数初始化,随后打印class A,接着B类对象初始化,在B类初始化列表中我们发现又初始化一次A,那么A会成功初始化吗?答案是不会因为我们虚继承了,三份A只用初始化一次A,所以这次直接打印class B,然后初始化C,C与B同理打印class C,走完D的初始化列表后进入构造函数打印class D所以答案选A。最重要的是要知道虚继承后只有一份A走了构造函数。
下面我们再看一下组合和继承的区别:
组合的耦合度低,将类C改了类D不会受很大的影响,而继承一旦A改了B继承的很多成员都会随之改变。
总结