从C语言到C++_22(继承)多继承与菱形继承+笔试选择题(上)

简介: 从C语言到C++_22(继承)多继承与菱形继承+笔试选择题

回顾一下面向对象三大特性:封装、继承、多态。(其它特性:反射、抽象...)

前面我们学了封装,封装带来了上面好处?:

① C++ Stack 类设计和 C 设计 Stack 对比,封装更好、访问限定符 + 类   狭义。

② 迭代器设计,如果没有迭代器,容器访问只能暴露底层结构。 -> 使用复杂、

使用成本很高,对使用者要求极高。

封装了容器底层结构,不暴露底层结构的情况,提供统一的访问容器的方式,

降低使用成本,简化使用。

③ stack/queue/priority_queue 的设计 —— 适配器模式。

现在我们讲讲继承。


1. 继承

1.1 继承的概念

继承(inheritance)机制是面向对象程序设计,使代码可以复用的最重要的手段。

它允许程序员在保持原有类特性的基础上进行扩展,以增加功能。这样产生新的类,称为派生类。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

以前我们接触的复用都是函数复用,而继承是类设计层次的复用。

举例:比如我们要设计一个图书管理系统,每个角色的权限是不同的。

角色类:学生、老师、保安、保洁、后勤…… 为了区分这些角色,我们就要设计一些类出来:

class Student
{
    string _name;
    string _tel;
    string _address;
    int _age;
    // ...
 
    string _stuID;  // 学号
};
 
class Teacher
{
    string _name;
    string _tel;
    string _address;
    int _age;
    // ...
 
    string _wordID;  // 工号
};

不难发现其存在大量冗余部分,有些信息是公共的,有些信息是每个角色独有的。


对于有些数据和方法是每个角色都具有的,我们每次都写一边,这就导致设计重复了。


我们说了代码是要讲究复用的,我们要想办法去做一个 "提取" ,把共有的成员变量提取出来。


解决方案:设计一个 Person 类

// 把共有的东西写进来
class Person 
{
    string _name;
    string _tel;
    string _address;
    int _age;
};

然后使用 "继承" 去把这些共有的东西运送给各个角色,先看语法:

class Person 
{
    string _name;
    string _tel;
    string _address;
    int _age;
};
 
// Student 公有继承了 Person 
class Student : public Person 
{
    string _stuID;  // 学号
};
 
// Teacher 公有继承了 Person 
class Teacher : public Person 
{
    string _wordID;  // 工号
};

这就是继承。在需要称为子类的类的类名后加上冒号,并跟上继承方式和父类类名即可。

比如说我们这里希望让 Student 以 public 的继承方式继承自 Person。

为了能够演示继承的效果,我们给 Person 类加上个 Print 打印函数:

(把成员函数也继承下来了)

#include<iostream>
using namespace std;
 
class Person 
{
public:
    void Print()
    {
        cout << "name: " << _name << endl;
        cout << "age:  " << _age << endl;
        cout << endl;
    }
protected:
    string _name = "user";
    string _tel;
    string _address;
    int _age = 18;
};
 
class Student : public Person
{
    string _stuID;  // 学号
};
 
class Teacher : public Person 
{
    string _wordID;  // 工号
};
 
int main()
{
    Person p;
    p.Print();
 
    Student s;
    s.Print();
 
    Teacher t;
    t.Print();
 
    return 0;
}


1.2  继承的定义格式

我们还是拿刚才的 Person 和 Student 举例:

class Student : public Person // Student是子类(派生类),public是继承方式,Person是父类(基类)
{
    string _stuID;  // 学号
};

Student是子类(派生类),public是继承方式,Person是父类(基类)

把 Person 和 Student 看作是父子关系是比较容易理解的。

子承父业,孩子 Student 从父亲 Person 那里继承一些 "东西" ,

这里的继承方式是 public,即公有继承,还有其他的一些继承方式。


1.3 访问限定符和继承方式

三种访问限定符,分别是 public(公有)、protected(保护)、private(私有)。

这一听名字就能知道,公有就是随便用,保护和私有就是藏起来一点点不让你随便用得到。

① public 修饰的成员,可以在类外面随便访问(直接访问)。

② protected 和 private 修饰的成员,不能在类外随便访问。

③ 定义成 protected 可以让父类成员不能在类外直接访问,但可以在子类中访问。

public、protected、private 不仅仅是访问限定符,它们也可以表示继承的三种继承方式:

三种访问限定符和三种继承方式相碰撞,就产生了 3×3=9 种情况:

① 父类的 private 成员在子类种无论以何种方式继承都是不可见的。这里的不可见指的是父类的私有成员还是被继承到了子类对象中,但是语法上限制了子类对象不管在类里面还是类外都不能去访问父类的 private 成员。


② 父类 private 成员在子类种不能被访问,如果父类成员不想在类外被直接访问,但是想让它们在子类中能被访问,可定义为 protected。 不难看出,保护成员限定符是因继承才出现的。


③ 实际上,上面的表格我们通过观察不难发现,父类的私有成员在子类都是不可见的,父类的其他成员在子类的访问方式 == Min(成员在父类的访问限定符,继承方式):public > protected > private。


④ 使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是 public,但是最好还是显式的写出继承方式,提高代码可读性。


⑤ 一共 9 种组合,实际上是大佬们早期设计的时候想复杂了,实际中父类成员基本都是保护和公有,继承方式基本都是用公有继承,几乎很少使用 protected / private 继承。 而且也不提倡使用 protected / private 继承,因为 protected / private 继承下来的成员都只能在子类里使用,实际扩展维护性不强。

1.4 继承中的赋值

子类对象可以赋值给父类的对象、父类的指针、父类的引用:

#include<iostream>
using namespace std;
 
class Person
{
protected:
    string _name;
    string _age;
};
 
class Student : public Person 
{
public:
    string _stuID;  // 学号
};
 
int main()
{
    Student s;
    // 子类对象可以赋值给父类对象/指针/引用
    Person p = s;
    Person* pp = &s;
    Person& rp = s;
 
    return 0;
}

这种操作我们称之为 "切割"(或切片),寓意是把子类中父类的那部分切过来赋值过去。  

注意事项:

父类对象不能赋值给子类对象

② 父类的指针可以通过强转赋值给子类的指针,但是必须是父类的指针是指向子类对象时才是安全的。这里父类如果是多态类型,可以使用 RTTI(Run-Time Type Information,即运行时类型识别)的 dynamic_cast 来进行识别后进行安全转换。(后面讲解,先了解)

1.5 继承中的作用域

继承体系中的父类和子类都有独立的作用域,如果子类和父类有同名成员,

此时子类成员会屏蔽父类对同名成员的直接访问,这种情况叫做 "隐藏" (也叫重定义)。

在子类成员函数中,可以使用如下方式进行显式访问:

基类::基类成员

注意事项:

① 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

② 实际运用中在继承体系里最好不要定义同名的成员。父类成员名称不要和子类成员名称冲突。

代码演示:父类和子类的成员函数同名的场景(注意父类和子类的 _num)

#include<iostream>
using namespace std;
 
class Person
{
protected:
    string _name = "李华";                // 姓名
    string _num = "56465456456464";  // 身份证号
};
 
class Student : public Person
{
public:
    void Print() 
    {
        cout << "姓名:" << _name << endl;
        cout << "身份证号:" << Person::_num << endl;  // 指定是Person的_num
        cout << "学号:" << _num << endl;  // 默认在自己作用域内找_num
    }
protected:
    string _num = "1007";  // 学号
};
 
int main(void)
{
    Student s1;
    s1.Print();
 
    return 0;
}

观察下列代码,A::func 和 B::func 的关系是重载还是隐藏?

#include<iostream>
using namespace std;
 
class A
{
public:
  void func() 
  {
    cout << "func()" << endl;
  }
};
 
class B : public A 
{
public:
  void func(int i)
  {
    A::func();
    cout << "func(int i) -> " << i << endl;
  }
};
 
int main() 
{
  B b;
  b.func(10);
}

解读:函数重载要求在同一作用域,我们说了,子类和父类都有独立的作用域,

因为不是在同一作用域,B 中的 func 和 A 中的 func 不可能构成重载,正确答案是构成隐藏。

B 中的 func 和 A 中的 func 构成隐藏,成员函数满足函数名相同就构成隐藏。


2. 子类(派生类)的默认成员函数

默认成员函数复习链接:从C语言到C++⑤(第二章_类和对象_中篇)(6个默认成员函数+运算符重载+const成员)_GR C的博客-CSDN博客


对于默认成员函数,如果不主动实现,编译器会自己生成一份。

那么这些默认成员函数在子类中,它们又是如何生成的?

2.1 子类的构造函数

① 父类成员需调用自己的构造完成初始化。 即子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。

② 如果 父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显式调用。

③ 子类对象初始化先调用父类构造再调子类构造。

代码演示:

#include <iostream>
using namespace std;
 
class Person 
{
public:
    Person(const char* name)
        : _name(name)
    {
        cout << "Person()" << endl;
    }
protected:
    string _name;
};
 
class Student : public Person 
{
public:
    Student(const char* name, int num)
        : Person(name)  // 父类成员,调用自己的构造完成初始化
        , _num(num)
    {
        cout << "Student()" << endl;
    }
 
protected:
    int _num;   // 学号
};
 
int main()
{
    Student s1("GR", 19);
 
    return 0;
}

调用父类构造函数初始化继承自父类的成员,自己再初始化自己的成员(规则参考普通类)。

析构、拷贝构造、赋值重载也是类似的。

思考:如何设计一个不能被继承的类?

将父类的构造函数私有化:

#include <iostream>
using namespace std;
 
class A
{
private:
    A()
    {}
 
protected:
  int _a;
};
 
class B : public A
{
 
};
 
int main()
{
    B b; // 只有实例化的时候才会报错
    return 0;
}

父类 A 的构造函数私有化后 B 就无法构造对象,因为 B 的构造函数必须要调用 A 的。

A a;  也不行了 ,A也没办法构造了,而且这只是C++98用的,

C++11提供了关键字 final 写在类的后面,表明这个类不能被继承:

#include <iostream>
using namespace std;
 
class A final
{
 
protected:
  int _a;
};
 
class B : public A // 错误  C3246 "B": 无法从 "A" 继承,因为它已声明为 "final"
{
 
};
 
int main()
{
    B b;
    return 0;
}

从C语言到C++_22(继承)多继承与菱形继承+笔试选择题(中):https://developer.aliyun.com/article/1521906

目录
相关文章
|
1月前
|
C++ 开发者
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
57 16
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
50 5
|
3月前
|
编译器 C++ 开发者
【C++】继承
C++中的继承是面向对象编程的核心特性之一,允许派生类继承基类的属性和方法,实现代码复用和类的层次结构。继承有三种类型:公有、私有和受保护继承,每种类型决定了派生类如何访问基类成员。此外,继承还涉及构造函数、析构函数、拷贝构造函数和赋值运算符的调用规则,以及解决多继承带来的二义性和数据冗余问题的虚拟继承。在设计类时,应谨慎选择继承和组合,以降低耦合度并提高代码的可维护性。
42 1
【C++】继承
|
2月前
|
算法 编译器 C语言
【C语言】C++ 和 C 的优缺点是什么?
C 和 C++ 是两种强大的编程语言,各有其优缺点。C 语言以其高效性、底层控制和简洁性广泛应用于系统编程和嵌入式系统。C++ 在 C 语言的基础上引入了面向对象编程、模板编程和丰富的标准库,使其适合开发大型、复杂的软件系统。 在选择使用 C 还是 C++ 时,开发者需要根据项目的需求、语言的特性以及团队的技术栈来做出决策。无论是 C 语言还是 C++,了解其优缺点和适用场景能够帮助开发者在实际开发中做出更明智的选择,从而更好地应对挑战,实现项目目标。
119 0
|
4月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
73 1
|
4月前
|
安全 编译器 程序员
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
34 0
|
1天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
50 13
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
40 5

热门文章

最新文章