c++继承(基本篇)

x33g5p2x  于2022-03-03 转载在 C/C++  
字(10.4k)|赞(0)|评价(0)|浏览(549)

1.继承的基本概念及其定义

1.1继承的基本概念

1.2继承的定义

2.基类和派生类对象的赋值转换

3.继承中的作用域

4.派生类的默认成员函数

4.1基类派生类构造函数析构函数的调用顺序

5.继承与友元

6.继承中的静态成员变量

7.复杂的菱形继承及菱形虚拟继承

8.继承的总结和反思

1.继承的基本概念及其定义

1.1继承的基本概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特
性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,
体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

  1. #include<iostream>
  2. using namespace std;
  3. class Person
  4. {
  5. public:
  6. void Print()
  7. {
  8. cout << "name:" << _name << endl;
  9. cout << "age:" << _age << endl;
  10. }
  11. protected:
  12. string _name = "peter"; // 姓名
  13. int _age = 18; // 年龄
  14. };
  15. // 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和
  16. //Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。
  17. //调用Print可以看到成员函数的复用。
  18. class Student : public Person
  19. {
  20. protected:
  21. int _stuid; // 学号
  22. };
  23. class Teacher : public Person
  24. {
  25. protected:
  26. int _jobid; // 工号
  27. };
  28. int main()
  29. {
  30. Student s;
  31. Teacher t;
  32. s.Print();
  33. t.Print();
  34. return 0;
  35. }

1.2继承的定义

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class Person {
  5. public:
  6. Person(const string& name="Peter", int age=18)
  7. :_name(name)
  8. ,_age(age)
  9. {}
  10. string _name;
  11. int _age;
  12. };
  13. 子类 继承方式 父类
  14. class Men :public Person {
  15. public:
  16. int id;
  17. };
  18. int main() {
  19. return 0;
  20. }

在这里Men被称为派生类,也叫做子类,Person叫做父类也叫做基类。在class Men:public Person中**public表示的是继承方式 **。

1.2.2继承的几种方式:

1.2.3继承基类成员访问方式的变化情况:

总结:

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是
    被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能
    访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类
    的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的
    写出继承方式。
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用
    protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中
    扩展维护性不强。

2.基类和派生类对象的赋值转换

1.派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
2.基类对象不能赋值给派生类对象
3.基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast来进行识别后进行安全转换。

下面我们来看一个例子:

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class Person {
  5. public:
  6. Person(const string& name="Peter", int age=18,string sex="男")
  7. :_name(name)
  8. ,_age(age)
  9. ,sex(sex)
  10. {}
  11. protected:
  12. string _name;
  13. int _age;
  14. string sex;
  15. };
  16. class Men :public Person {
  17. public:
  18. int id;
  19. };
  20. int main() {
  21. Men M;
  22. return 0;
  23. }

对应代码验证:

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class Person {
  5. public:
  6. Person(const string& name="Peter", int age=18,string sex="男")
  7. :_name(name)
  8. ,_age(age)
  9. ,sex(sex)
  10. {}
  11. protected:
  12. string _name;
  13. int _age;
  14. string sex;
  15. };
  16. class Men :public Person {
  17. public:
  18. int id;
  19. };
  20. int main() {
  21. Men M;
  22. //子类对象可以父类的指针
  23. Person* p = &M;
  24. //子类对象可以赋值给父类的引用
  25. Person& t = M;
  26. //子类对象可以赋值给父类对象,反过来不行
  27. Person tmp = M;
  28. // 3.基类的指针可以通过强制类型转换赋值给派生类的指针
  29. Person* parent = new Person("李四",25, "男");
  30. Men* sub = (Men*)parent;
  31. return 0;
  32. }

 3.继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定
    义。(在子类成员函数中,可以使用 基类::基类成员 显示访问
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

5.特别要注意的是如果父类中和子类中出现了同名函数这叫做隐藏或者重定义不是函数重载,函数重载的前提是要在同一作用域。

示例1:

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. // Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
  5. class Person
  6. {
  7. protected:
  8. string _name = "小李子"; // 姓名
  9. int _num = 111; // 身份证号
  10. };
  11. class Student : public Person
  12. {
  13. public:
  14. void Print()
  15. {//如果没有指明作用域会优先访问子类中的同名变量或者函数
  16. cout << " 姓名:" << _name << endl;
  17. cout << " 身份证号:" << Person::_num << endl;
  18. cout << " 学号:" << _num << endl;
  19. }
  20. protected:
  21. int _num = 999; // 学号
  22. };
  23. void Test()
  24. {
  25. Student s1;
  26. s1.Print();
  27. };
  28. int main() {
  29. Test();
  30. return 0;
  31. }

示例2:

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. // B中的fun和A中的fun不是构成重载,因为不是在同一作用域
  5. // B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
  6. class A
  7. {
  8. public:
  9. void fun()
  10. {
  11. cout << "func()" << endl;
  12. }
  13. };
  14. class B : public A
  15. {
  16. public:
  17. void fun(int i)
  18. {
  19. A::fun();
  20. cout << "func(int i)->" << i << endl;
  21. }
  22. };
  23. void Test()
  24. {
  25. B b;
  26. b.fun(10);
  27. };
  28. int main() {
  29. Test();
  30. return 0;
  31. }

如果我们想要让父类中的同名函数在子类中可见。c++11中让父类中的同名函数在子类中可见通过using关键字,说白了就是让父类中的同名函数在子类中以重载的方式使用。

示例:

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. // B中的fun和A中的fun不是构成重载,因为不是在同一作用域
  5. // B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
  6. class A
  7. {
  8. public:
  9. void fun()
  10. {
  11. cout << "func()" << endl;
  12. }
  13. };
  14. class B : public A
  15. {
  16. public:
  17. //c++11中让父类中的同名函数在子类中可见通过using关键字,
  18. // 说白了就是让父类中的同名函数在子类中以重载的方式使用
  19. using A::fun;
  20. void fun(int i)
  21. {
  22. cout << "func(int i)->" << i << endl;
  23. }
  24. };
  25. void Test()
  26. {
  27. B b;
  28. b.fun();
  29. };
  30. int main() {
  31. Test();
  32. return 0;
  33. }

总结:

成员函数被重载的特征:

1.在同一个作用域内。

2.函数名字相同。

3.参数不同。

4.virtual可有可无

覆盖是指派生类函数覆盖基类的同名函数

1.作用域不同

2.函数名字相同

3.参数相同

4.基类函数必须有virtual关键字

隐藏是指派生类的函数屏蔽了基类中与之同名的函数,规则如下

1.如果派生类的函数名和基类中的函数名相同但是参数个数不同,此时不管有无virutal,基类函数都会被隐藏

2.如果派生类的函数名和基类中的函数名相同但是参数个数也相同,但是基类函数无virtual,此时基类函数也会被隐藏。

 4.派生类的默认成员函数

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个
成员函数是如何生成的呢?

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函
    数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类
    对象先清理派生类成员再清理基类成员的顺序
  5. 派生类对象初始化先调用基类构造再调派生类构造
    6.派生类对象析构清理先调用派生类析构再调基类的析构。

4.1基类派生类构造函数析构函数的调用顺序

在继承中 如果子类继承了父类在定义子类对象时:

1.先调用父类的构造函数在调用子类的构造函数。

2.析构时则与之相反先调用子类的构造函数在调用父类的析构函数

3.拷贝构造函数与operator=与构造函数类似

4.如果基类中写了构造函数并且需要在基类中调用构造函数可以在子类中的初始化参数列表中显示调用构造函数

例:

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class Person{
  5. public:
  6. Person(const char* name = "peter")
  7. : _name(name)
  8. {
  9. cout << "Person()" << endl;
  10. }
  11. Person(const Person&p)
  12. :_name(p._name)
  13. {
  14. cout << "Person(const Person& p)" << endl;
  15. }
  16. Person& operator=(const Person& p)
  17. {
  18. cout << "Person operator=(const Person& p)" << endl;
  19. if (this != &p)
  20. _name = p._name;
  21. return *this;
  22. }
  23. ~Person()
  24. {
  25. cout << "~Person()" << endl;
  26. }
  27. protected:
  28. string _name; // 姓名
  29. };
  30. class Student : public Person
  31. {
  32. public:
  33. Student(const char* name, int num)
  34. : Person(name)
  35. , _num(num)
  36. {
  37. cout << "Student()" << endl;
  38. }
  39. Student(const Student& s)
  40. : Person(s)
  41. , _num(s._num)
  42. {
  43. cout << "Student(const Student& s)" << endl;
  44. }
  45. Student& operator = (const Student& s)
  46. {
  47. cout << "Student& operator= (const Student& s)" << endl;
  48. if (this != &s)
  49. {
  50. Person::operator =(s);
  51. _num = s._num;
  52. }
  53. return *this;
  54. }
  55. ~Student()
  56. {
  57. cout << "~Student()" << endl;
  58. }
  59. protected:
  60. int _num; //学号
  61. };
  62. void Test()
  63. {
  64. Student s1("jack", 18);
  65. Student s2(s1);
  66. Student s3("rose", 17);
  67. s1 = s3;
  68. }
  69. int main() {
  70. Test();
  71. return 0;
  72. }

面试题:如何定义一个不可以被继承的类:

答:将该类的构造函数或者析构函数设置为私有即可。 

5.继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

示例:

  1. class Student;
  2. class Person
  3. {
  4. public:
  5. friend void Display(const Person& p, const Student& s);
  6. protected:
  7. string _name; // 姓名
  8. };
  9. class Student : public Person
  10. {
  11. protected:
  12. int _stuNum; // 学号
  13. };
  14. void Display(const Person& p, const Student& s)
  15. {
  16. cout << p._name << endl;
  17. cout << s._stuNum << endl;
  18. }
  19. int main()
  20. {
  21. Person p;
  22. Student s;
  23. Display(p, s);
  24. }

在上面这个例子中Display这个函数是基类的友元函数但是他不是子类的友元函数,就像我们的爸爸有他自己的朋友但是爸爸的朋友不一定是我们的朋友。所以父类的友元函数不能访问子类的友元函数。

6.继承中的静态成员变量

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一
个static成员实例 。

示例:

  1. class Person
  2. {
  3. public :
  4. Person () {++ _count ;}
  5. protected :
  6. string _name ; // 姓名
  7. public :
  8. static int _count; // 统计人的个数。
  9. };
  10. int Person :: _count = 0;
  11. class Student : public Person
  12. {
  13. protected :
  14. int _stuNum ; // 学号
  15. };
  16. class Graduate : public Student
  17. {
  18. protected :
  19. string _seminarCourse ; // 研究科目
  20. };
  21. void TestPerson()
  22. {
  23. Student s1 ;
  24. Student s2 ;
  25. Student s3 ;
  26. Graduate s4 ;
  27. cout <<" 人数 :"<< Person ::_count << endl;
  28. Student ::_count = 0;
  29. cout <<" 人数 :"<< Person ::_count << endl;
  30. }

运行结果:

 7.复杂的菱形继承及菱形虚拟继承

我们首先来看一下单继承:

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况

菱形继承的问题:从上面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题.

我们来看一个例子:

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class A {
  5. public:
  6. int m_age;
  7. };
  8. class B :public A {
  9. };
  10. class C :public A {
  11. };
  12. class D :public B, public C {
  13. };
  14. void Test() {
  15. D d;
  16. }
  17. int main() {
  18. return 0;
  19. }

在上面这个代码中D继承了两份m_age分别来自他的基类B和C。

此时如果我们使用d访问m_age则会出现二义性,如图所示:

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class A {
  5. public:
  6. int m_age;
  7. };
  8. class B :public A {
  9. };
  10. class C :public A {
  11. };
  12. class D :public B, public C {
  13. };
  14. void Test() {
  15. D d;
  16. cout << d.m_age << endl;
  17. }
  18. int main() {
  19. return 0;
  20. }

如果我们此时要访问需要指明作用域,指明m_age是那个基类的。那如何证明呢?下面博主介绍一种方式:

首先我们点击项目属性:

然后找到c/c++中的命令行

找到命令行:

在其他选项中输入 

/d1 reportAllClassLayout 是查看所有类的布局

/d1 reportSingleClassLayoutXX  其中"XX"是填入你想查看的类的名字

3. 配置之后,保存。然后重新编译该项目,在[输出]中可看到下图

我们可以看到DD类中继承了两份m_age,这完全是重复的,并且访问m_age时还需要指明作用域才可以访问。

为了解决这个问题c++中引入了虚拟继承来解决菱形继承的二义性和数据冗余的问题。

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class Person
  5. {
  6. public:
  7. string _name; // 姓名
  8. };
  9. class Student : virtual public Person
  10. {
  11. protected:
  12. int _num; //学号
  13. };
  14. class Teacher : virtual public Person
  15. {
  16. protected:
  17. int _id; // 职工编号
  18. };
  19. class Assistant : public Student, public Teacher
  20. {
  21. protected:
  22. string _majorCourse; // 主修课程
  23. };
  24. void Test()
  25. {
  26. Assistant a;
  27. a._name = "peter";
  28. }
  29. int main() {
  30. Test();
  31. return 0;
  32. }

注意:虚拟继承不要在其他地方去使用。

下面我们来探究一下菱形继承中虚拟继承的原理:为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系。

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class A
  5. {
  6. public:
  7. int _a;
  8. };
  9. // class B : public A
  10. class B : virtual public A
  11. {
  12. public:
  13. int _b;
  14. };
  15. // class C : public A
  16. class C : virtual public A
  17. {
  18. public:
  19. int _c;
  20. };
  21. class DD : public B, public C
  22. {
  23. public:
  24. int _d;
  25. };
  26. int main()
  27. {
  28. DD d;
  29. d.B::_a = 1;
  30. d.C::_a = 2;
  31. d._b = 3;
  32. d._c = 4;
  33. d._d = 5;
  34. return 0;
  35. }

同样的我们按照上面的操作在输出列表中查看对象模型

下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A
同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指
针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。

 8.继承的总结和反思

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有
    菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在
    复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。

3. 继承和组合

1.public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
优先使用对象组合,而不是类继承 。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用
(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。
继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关
系很强,耦合度高。
2.对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对
象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),
因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,
耦合度低。优先使用对象组合有助于你保持每个类被封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适
合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就
用组合。

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. // Car和BMW Car和Benz构成is-a的关系
  5. class Car {
  6. protected:
  7. string _colour = "白色"; // 颜色
  8. string _num = "陕ABIT00"; // 车牌号
  9. };
  10. class BMW : public Car {
  11. public:
  12. void Drive() { cout << "好开-操控" << endl; }
  13. };
  14. class Benz : public Car {
  15. public:
  16. void Drive() { cout << "好坐-舒适" << endl; }
  17. };
  18. // Tire和Car构成has-a的关系
  19. class Tire {
  20. protected:
  21. string _brand = "Michelin"; // 品牌
  22. size_t _size = 17; // 尺寸
  23. };
  24. class Car {
  25. protected:
  26. string _colour = "白色"; // 颜色
  27. string _num = "陕ABIT00"; // 车牌号
  28. Tire _t; // 轮胎
  29. };
  30. int main()
  31. {
  32. return 0;
  33. }

最后:如果觉得对您有帮助的话劳烦您动动您的小手在下方点个赞,或者发现错误请在评论区留言。

相关文章