【Essential C++学习笔记】第五章 面向对象编程风格

2023-11-20 18:44

本文主要是介绍【Essential C++学习笔记】第五章 面向对象编程风格,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本章节书里讲的比较杂,索性用自己的方式总结了一波,有些没有实质内容的章节就跳过了

文章目录

  • 第五章 面向对象编程风格
    • 5.1 面向对象编程概念
      • 1)[继承](https://blog.csdn.net/qq_62718027/article/details/125922249)
        • 1.继承格式
        • 2.访问权限和继承
        • 3.派生类和基类构造函数初始化列表的执行顺序
      • 2)多态
        • 1.多态的实现
        • 2.虚函数
        • 3.C++11新增的 override 和 final
          • ① `override`
          • ② `final`
        • 4.重载,覆盖(重写),重定义(隐藏)
        • 5.接口继承和实现继承
      • 3)总结
    • 5.4 不带继承的多态
      • 第一,**找出所有子类(一些类)共通的操作行为**
      • 第二,**找出哪些操作行为(基类成员函数)必须根据不同的派生类而有不同的实现方式**
      • 第三,**找出每个操作行为(基类成员函数)的访问层级**
    • 5.10 运行时的鉴定机制
      • typeid[运算符](https://so.csdn.net/so/search?q=运算符&spm=1001.2101.3001.7020)
      • type_info类
      • static_cast运算符
      • dynamic_cast运算符

第五章 面向对象编程风格

5.1 面向对象编程概念

面向对象编程概念最主要特质:继承多态

1)继承

  • 继承是将一群相关的类组织起来,分享这些类之间的共通数据操作行为
  • 继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
  • 当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类
  • 继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。之前接触的复用都是函数复用,继承是类设计层次的复用
  • 间接利用指向抽象基类的指针或引用来操作一个类系统中的各个类对象,而非直接操作各个实际对象,这有什么好处呢?就是可以不在改动原有程序的前提下,加入或移除任何一个派生类
1.继承格式
class 新类的名字:继承方式 继承类的名字{};// 基类class Animal {// eat() 函数// sleep() 函数};//派生类class Dog : public Animal {// bark() 函数};
2.访问权限和继承

① 访问权限

访问publicprotectedprivate
同一个类yesyesyes
派生类yesyesno
外部的类yesnono
  • 基类private成员无论以什么方式继承到派生类中都是不可见的。
  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public。

② 继承后的子类成员访问权限

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类不可见在派生类不可见在派生类不可见

③ 派生类继承了什么?

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

④ 同名的成员变量和成员函数

在有些时候,父类和子类中出现了同一个成员变量,如下name,这个时候编译器是以子类为优先。想要访问父类的name成员变量,就需要加上修饰

cout << people::name<< endl;

同样一个函数print在父类和子类中都存在,编译器会默认调用子类中匹配的函数,(函数重载是在同一个作用域,这里父类和子类是两个作用域)

3.派生类和基类构造函数初始化列表的执行顺序

构造函数调用顺序:基类 > 成员类 > 派生类

析构函数和构造函数相反,编译器默认先调用子类的析构函数,再调用父类的析构函数。

#include <iostream>
using namespace std;class A {public:A() {cout << "call A()" << endl;}~A() {cout << "call ~A()" << endl;}
};class B : A {public:B(int val) : A(), value(val) {val = 0;    // 重新赋值cout << "call B()" << endl;}~B() {cout << "call ~B()" << endl;}private:int value;
};int main() {B b(10);return 0;
}/*
* 结果如下
* call A()
* call B()
* call ~B()
* call ~A()
* 说明放在初始化列表的部分在构造函数之前执行
*/

2)多态

多态字面意思就是多种形态。

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

举个例子:买票

普通人全价,学生半价,军人全价但优先购票…

这就是对买票这一个具体行为,不同人去完成时产生的不同结果。

1.多态的实现

C++的多态必须满足两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
#include <iostream>
using namespace std;class Person
{
public:virtual void display(){cout << "全价票" << endl;}
};class Student :public Person
{
public:virtual void display() // //子类完成对父类虚函数的重写{cout << "半价票" << endl;}
};void BuyTicket(Person* p)
{p->display();
}int main(){Student st;Person p;BuyTicket(&st);  //半价票BuyTicket(&p);	//全价票system("PAUSE");return 0;
}
/*  将传地址改为传对象,不满足多态条件后,调用的都是父类的函数
void BuyTicket(Person p)
{p.display();
}
int main(){Student st;Person p;BuyTicket(st);  //全价票BuyTicket(p);	//全价票system("PAUSE");return 0;
}
*/

如果满足多态,编译器会调用指针指向对象的虚函数,而与指针的类型无关。如果不满足多态,编译器会直接根据指针的类型去调用虚函数。

2.虚函数
  • virtual修饰的关键字就是虚函数,且虚函数只能是类中非静态的成员函数

  • 子类和父类中的虚函数拥有相同的名字,返回值,参数列表,那么称子类中的虚函数重写了父类的虚函数

  • 虚函数可以不实现(定义)。不实现(定义)的虚函数是纯虚函数。 virtual int area() = 0;` = 0 告诉编译器,函数没有主体。那么纯虚函数有什么用呢?

    1,强制子类重写虚函数,完成多态。
    2,表示某些抽象类。

  • 虚函数重写的两个例外:

    1. 协变:子类的虚函数和父类的虚函数的返回值可以不同,也能构成重载。但对返回值有要求:需要子类的返回值是一个子类的指针或者引用,父类的返回值是一个父类的指针或者引用,且返回值代表的两个类也成继承关系。这个叫做协变。
    2. 析构函数的重写:析构函数名字天生不一样,怎么实现多态?
    /*test.1
    B继承了A,他们的析构函数没有重写。
    */
    class A
    {public:~A(){cout << "~A()" << endl;}
    };
    class B : public A
    {public:~B(){cout << "~B()" << endl;}
    };A* a = new B; //把B的对象切片给A类型的指针。delete a; //实际调用的是A的析构函数/*test.2
    B继承了A,他们的析构函数没有重写。
    */
    class A {public:virtual ~A() {cout << "~A()" << endl;}
    };class B : public A {public:~B() {cout << "~B()" << endl;}
    };A* a = new B; //把B的对象切片给A类型的指针。delete a; //有进行了重写,会先调用B的析构函数,后调用A的析构函数
    
3.C++11新增的 override 和 final
override

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。(定义在子类)

感觉没多大意义,有点画蛇添足,既然都想到了要写override,那还能忘记要重写基类的虚函数?或者说写错?

final

final修饰的虚函数无法重写。用final修饰的类无法被继承。final像这个单词的意思一样,这就是最终的版本,不用再更新了。(定义在父类)

子类对有final修饰的虚函数进行重写,会报错

4.重载,覆盖(重写),重定义(隐藏)
作用域组成要求
重载两者处在同一作用域函数名相同,参数、返回值不同
覆盖(重写)两者分别在基类和派生类的作用域函数名、参数、返回值相同(协变除外)两者都是虚函数
重定义(隐藏)两者分别在基类和派生类的作用域函数名相同,参数、返回值不同子类和父类的同名函数不是重定义就是重写
5.接口继承和实现继承

普通函数的继承就是实现继承,虚函数的继承就是接口继承。子类继承了函数的实现,可以直接使用。虚函数重写后只会继承接口,重写实现。所以如果不用多态,不要把函数写成虚函数

#include <iostream>
using namespace std;class A
{
public:virtual void fun(int val = 0){cout << "A->val = " << val << endl;}void Fun(){fun();//传过来一个子类指针调用fun()}
};class B:public A
{
public:virtual void fun(int val = 1){cout << "B->val = " << val << endl;}
};int main(){B b;b.Fun();system("PAUSE");return 0;
}

3)总结

  • 继承特性让我们可以定义一整群互有关联的类,并共享通用的接口
  • 多态则让我们可以用一种与类型无关的方式来操作类对象,通过抽象基类的指针或引用来操纵一整群互有关联的类的共同接口
  • 动态绑定在程序执行时隐居抽象基类的指针或引用所指的是基类对象的类型才能决定。

5.4 不带继承的多态

定义一个抽象基类

第一,找出所有子类(一些类)共通的操作行为

所有子类共通的操作行为,这些行为代表的是基类的公有接口

第二,找出哪些操作行为(基类成员函数)必须根据不同的派生类而有不同的实现方式

找出哪些操作行为(基类成员函数)必须根据不同的派生类而有不同的实现方式。
如果某操作行为(基类成员函数)必须根据不同的派生类而有不同的实现方式,这个操作行为就要成为整个类继承体系的虚函数即该成员函数(至少在基类中)的声明前加virtual。

第三,找出每个操作行为(基类成员函数)的访问层级

某个操作行为(类成员函数)能让一般程序(在该类主体和该类的派生类如果有派生类的话外)访问这个成员函数,那么我们将这个成员函数的声明/定义放进基类的public:里。
某个操作行为(类成员函数))在基类之外不需要被用到,自然不用在基类之外访问这个成员函数。我们就把这个成员函数的声明/定义放进基类的private:里
某个操作行为(类成员函数)在该类的派生类(如果有的话)主体/对象可以被访问,却不许一般程序访问。 那么我们就把这个成员函数的声明/定义放进基类的protected:里。

5.10 运行时的鉴定机制

typeid运算符

  • 这是运行时类型鉴定机制得一部分,由程序语言支持。
  • typeid运算符让我们得以查询多态化的类指针/引用,来获得类指针/引用所指对象的实际类型。用法举例
#include<typeinfo>
inline const char* num_sequence::what_am_i()const{return typeid(*this).name();}

type_info类

  • type_info类支持相等和不相等两个操作,举个例子:
num_sequence *ps=&fib;
//...
if(typeid(*ps)==typeid(Fibonacci))
{}
//...

这一堆代码是测试ps这个基类指针是否指向某个派生类类对象。

static_cast运算符

用static_cast运算符来强制转换(无条件转换)基类指针变为派生类指针。用法举例:

if(typeid(*ps)==typeid(Fibonacci))
{Fibonacci *pf=static_cast<Fibonacci*>(*ps);pf->gen_elems(64);//经过static_cast运算符转换指针变为派生类指针//这种写法得以被编译器承认
}

不过static_cast有个潜在危险,就是编译器无法确定转换类指针从基类到派生类是不是转换对了,所以我们加了if(typeid(*ps)==typeid(Fibonacci)){}如果typeid运算符(这里是==和())运算结果为真,就执行类指针的无条件转换。

dynamic_cast运算符

dynamic_cast运算符没有这种潜在危险。其用法举例:

	if(Fibonacci *pf=dynamic_cast<Fibonacci*>(ps)){pf->gen_elems(64);//经过dynamic_Cast运算符转换指针变为派生类指针//这种写法得以被编译器承认}

这篇关于【Essential C++学习笔记】第五章 面向对象编程风格的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/396670

相关文章

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�