第一课

程序设计语言

语言:数学上的表示,让计算机理解

语法、语义、语用

句型

Backus-Naur Form EBNF/语法图

identifer 标识符

不可代替的部分:A/D:terminate(终结符号) = sigma字母表:ascii码

image-20220216102935989

推导:不断从左步替换右步:_a9是不是identifier

从ID开始:左步可以替换右步 |是或的意思

image-20220216103651292

从识别符号开始(ID),可以推导出符号串,就是该语言可识别的符号串 可写成推导树

计算机就用该规则识别符号串

从上往下:deduct(推导) 从下往上:reduce(归纳)

语言:在特定的字母表(ascii)上,按照特定的规则(BNF),所构成的符号串的集合

  • 符号串无穷无尽,因为包含recursive

什么执行识别过程?Compiler(编译器)把语言等价翻译成机器表示 等价翻译:与状态机等价?

image-20220216104025126

  • RG约束最强,语言集合最小Regular Grammar正则文法,类比*.doc–Finite automata有限状态自动机
  • CFG:Context free grammar上下文无关文法 Push Down Automata
  • CSG:Context sensitive grammar Linear Bounded Automata线性界限自动机
  • PSG:phrase structure grammar短语结构文法 Turing machine(可计算性的全集)

formal method

image-20220216104335995

编译器翻译过程:语义,去等价翻译

大部分程序设计语言词法:符合RG 大部分语法:符合CFG 随着自动机的提升,表示越来越复杂 超出CFG的范围:用ad hoc描述,不会用CSG,因为太复杂

语用:一个意思,表达可能不一样 对语言的执行是有帮助的

编程

image-20220216111029093

每一个语句的执行都有前后条件做限制 符号的演化:calculus 但有时候没有明确的科学限制

debug不仅考虑控制流,还要考虑数据流

写程序时的经验:编程范式(共4种)

image-20220216112600362

冯诺依曼(基于图灵机):可以算出来,一定知道数据、步骤、次序、所占空间(通过存储、指令,完成output)

命令式编程(基于冯诺依曼,分2类)

  • Procedural:沿着过程

  • OO:写表示的时候确实是Object和class(高级程序设计语言范畴),真正执行时仍然基于运行环境

对比java(pure),在虚拟机解释下,体现OO特征。但c++底层执行:知道当前对象的状态,和执行到函数的哪一步

函数副作用在这里有用,跟下面不一样

哪些编程范式可以从冯诺依曼解放出来?函数式编程:有理论依据 不是基于图灵机 组合数学 数理逻辑?

不想知道细节,只关心一个个功能,通过计算的叠加完成计算任务

image-20220216114318192

声明式编程

  • 函数式:
    • 函数副作用:值不仅仅受参数影响,还受上下环境影响,如与全局变量有关 不能有函数副作用
      • 先调哪个函数没有影响,可以并行(如两个不同载体)从哪里开始运行由系统决定
        • 如排版,先变成5号字、再加黑bold,或换顺序,没有影响 如画图渲染,把所有运行一遍
  • 逻辑式程序设计:推理image-20220216132902494
    • 不断输入规则、fact,判断问题(也可能不知道,推不出来)
    • 专家系统:医生、地质经验输入,通过事实、采集样本,让计算机代替专家
    • 目前活跃的有prolog,如算24,平常枚举 这里不知道怎么计算(隐藏细节)描述清楚问题即可

C++语言发展路径

image-20220216135826340

开始的想法:能不能用科学计算,现在已被数据处理替代

Algol:

  • 提出scope:作用域(程序的局部化)提出栈等概念
  • 提出复合语句
  • 提出软件复用:代码复用,要做模块
  • 提出函数:最重要的是参数传递
  • 提出递归:增强、简洁
  • 提出动态数组

Algol 68引入C++结构化编程的思想

C++OO 从Simula 67引入

Go语言:重写后台 增加携程(并发程序设计)垃圾回收 内存安全

c语言运行效率高,开发效率低

image-20220216193052950

即使发明了也不能代替:早期很多代码已经被编译成二进制码了,程序正确性不能被证明

第二课

C++的诞生

Fortran一致性差,与机器绑定紧密

CPL bcpl注重细节,不够简洁 B写unix操作系统发展到C

第3个基因OO从Simula 67开始 计算:一大类软件叫做Simulation System仿真系统需要对问题求数值解 没有直接的数学模型做对应 这个人做核反应堆,选择FORTRAN或者ALGOL60 打破LIFO栈结构 想要设计一个语言兼容Algol60

  • 有能描述行为的动作
  • 指针来管理
  • 有供分析的数据
  • 对数据要有管理

Simula 1:只是核电的仿真系统

之后的Simula 67:有封装/虚函数/多态/垃圾回收

  • 发现有共性:能不能增强复用 抽象共性为Class 不同的class有共性->继承

  • 原来都是从顶向下,现在也能自底向上

没有继续发展下去的原因:1Born:停在68 在欧洲 2:贵3:缺IDE4:没更多作品 5:体积大 6:并发和数据类型表示不够好

程序=算法+数据结构

image-20220223111012157

image-20220223112301423

存在主义:注重个体 尊重所有有益的工具

1979:带类的C 要兼容C 接近机器和问题 不能舍弃C中危险丑陋特性而付出效益的代价

linker:连接兼容性重于代码兼容性 实现+测试稳步前进

C++:用户产业界大学 运行环境 硬件+OS 标准化ANSI ISO(国际化)

image-20220223114641712

cPP:只翻译class,不检查语法 Cfront:语法检查

现在编译器的结构:LLVM

image-20220223115506834

C++ 兼顾细节与抽象 与错误相比更重要的是能做什么 程序员可以被信任

课程内容

image-20220223123736330

1结构化 2OO 3final

第三课

结构化程序设计部分

程序=数据结构+算法

1、数据 2、计算控制 3、组织 4、构造数据类型(array struct union pointer)

数据:有名字、值、性质(const为常量只有读权限一定设为const)和DataType

int x = 8;初始化 int x;取值范围 16位时-32768-32767 用sizeof来看

ADT抽象数据类型 因为数表示的范围有限,所以可数、离散

用补码:模 好计算 溢出overflow要自己控制

移植程序在不同机器怎么办?用typedef int INT16;//别名 INT16 y;这样移植时就可用typedef short INT16;就可以了

int x = 5; int y = 2; x/y=2 定义了同类型的计算语义

builtin datatype:char int float double

image-20220225143944850局部变量在stack new int 在heap

image-20220225150314242

强:在类型系统的要求 s1=”12”; s2=8; s1=s1+s2;弱类型可能可以理解

静:compile/link阶段能确定类型 run以后要根据状态来决定类型:动态类型

image-20220225150616586

多态:动态的表现 要看实际调用 约束是Figure必须是line rect的父类

image-20220225151301222

tom其实不是duck类型,不符合这个函数的关系,但有这个quack函数,就可以调用:完全动态的形式

程序不能做证明:只能用测试 类型安全不能用于测试

image-20220225151600524

怎么在不同类型之间计算?表达式 要考虑优先级、结合性

double x = 1.0 + 3/2;逐个趋强 1.0+1=2.0

image-20220225153042827

副作用:要去掉表达式的副作用,要让程序员清晰易懂 谨防overflow

新版本机制:“1+2”+3 元程序设计 可以让系统帮忙做类型推演

image-20220225153335845

不加分号:表达式 加分号:语句 操作符重载

image-20220225153952358

A(1) = 8;是不行的,因为x是局部变量

类型不同时,先计算右边,如double x = 1 + 2/3;

image-20220225154105331

不能做重载 如果yz都是左值,可以写x>0?y:z=8;

image-20220225154302153

整个表达式的值是最后一个的值,如这里是6

image-20220225154718057

交换a b

语句:加分号

image-20220225155951606

可以通过重载,输出自己定义的数据类型,很方便

image-20220225160449176

for循环有三要素(初始化;终止条件;改变循环控制变量)

switch相比if,只需要比较一次,开销小 一般避免出现字面常量(1、2)而选用符号常量(如const double PI = 3.14; const int RED = 0;)更好enum:枚举类型(enum Color{RED = -8, BLUE, GREEN}默认是0,也可以赋值)把一组关联的常量变成新的类型

日志也经常用switch case 示例:

image-20220225161231596

image-20220225161653191

可以把表放到文件当中:方便汉化等 修改资源文件

image-20220225161955583

问题1:一定有很高的效率吗?问题2:有没有办法解决?通过compiler后的呈现,方法:table driven

1:enum范围? 2:通过compiler后的呈现?

第四课

switch

image-20220302102128621

image-20220302102813841

跳转表:0x034-0x0fc:200byte 有值:跳到如0x56214 再指向string地址 无:default 存0x23e

ja:无符号大于50 gotodefault

image-20220302103011864

以空间换时间image-20220302103313742

image-20220302103636309

差值即是字符串长度,如number为7

如果enum范围太大呢?类似平衡二叉树

image-20220302104116789

jg:大于

image-20220302104247712

image-20220302104450631通过实验让gcc做出选择。目前不能让用户自己选。

image-20220302104631223

OO:数据和代码的封装,是整体的封装 compiler编译后依然分开在code和数据

全局变量:data区 局部变量:栈 很多地址要link时才能确定:include库等

image-20220302110214399

同名时:在不同阶段会检查符号表并报错

不能全部作为全局变量:没有递归,因为不能载入 局部变量的代价是stack的申请退回空间等操作

heap:用函数显式申请 也要记得还 否则容易内存泄漏

函数重载:符号表里会有内部的函数名,指示参数类型->可以区分

第五课

函数与引用

内联函数

第六课

作用域

namespace

第七课

数组

int a[3];//a是数组类型

sizeof(a);//表示数组大小 等于3*sizeof(int)

a是指针?f(a) 这样就不能求a的大小,所以要传size 其实不是?

字符串数组初始化等价于自动在后面加’\0’(访问则用数组遍历的形式)

如果不加’\0’就会显示烫0xcc

image-20220316100923266

int a[2] [3] 等价于 int[3] a[2]

参数传递:缺省第1维,但不能缺省第2维 因为int[3]基类型 一维数组要知道

升降维:可看成两个元素组成的一维数组

image-20220316103800572

java可以有不规则数组ragged array,因为java不需要知道内存排列

但C++没有不规则数组 元素必须整齐排列(相当于矩阵填满了)

image-20220316104232292

Struct

不同元素在空间中进行排列,默认public,通常与指针联用

赋值同类型:下图不能赋值,只有A a, b; typedef A, B才能叫同类型

image-20220316104628368

call by reference:会节约空间,但会带来函数副作用

对齐:不是7是12 padding:契合硬件,提高效率,空间换时间

可以指定对齐方式:如指定8字节,或pack(改变编译器的对齐方式, 不使用这条指令的情况下,编译器默认采取#pragma pack(8)也就是8字节的默认对齐方式,n值可以取(124816) 中任意一值。)

image-20220316105143538

Union

共享空间,节省空间,size是最大值,需要哪种数据类型就访问哪个

image-20220316105721945

二维数组定义矩阵,下标访问而不能用名称访问(如_a11, _a22)

占有的空间依然是9*int 每一块空间都有两个名字_a11或 _element[0] [0]

image-20220316110253501

修改成union后:注意空间一定要整齐 系统/嵌入式软件为了节省空间使用

image-20220316110513625

开300个:浪费了200个空间 所以定义FIGURE

光有draw不够,还需要type来标识类型

image-20220316111235359

FIGURE_TYPE t;

image-20220316111429154

image-20220316111506092

如果增加FIGURE_TYPE t,也不会增加空间(毕竟它占用的空间最小)

image-20220316111741310

draw和input:其中枚举类型不能直接input,要用赋值

多态性:三种类型都能放

image-20220316111952539

增加属性时在每个里面都添加:类似面向对象定义class 利用union可实现多态

image-20220316112046446

第八课

指针

指针的作用:管理地址信息

  • 读写数据
  • 调用代码

定义与基本操作

定义

int *p;//地址对应的内容是int

仅仅管理地址信息,但不间接操作数据、调用代码(不知道基类型) void*

int *p,q;//p是指针类型,q是int!所以用typedef

可以赋绝对内存地址:所以可以写操作系统等(作业不涉及)

  • 嵌入式软件:和其他设备交互,交互的点是扩展内存的地址,往特定地址写入数据,就可以通讯–就不需要串口、socket通讯等

image-20220323103310587

操作符

int *p;//也分配了一个地址0x7B77 刚开始矩形内内容为null 没有初始化

p=&x;//相当于x多了一个名字*p

如何对p做初始化?一定要初始化!!把p放在安全的位置上

  • 随时随地确认p在有效空间
  • 如int *p = &x;不知道怎么办??
  • Pointer Literal:指针常量null
    • 最开始ANSI C:为**((void)0)*
      • 但(void*)要求不对数据操作,否则随便赋值(不同类型的指针都能相互赋值,破坏内存系统)
    • C++:赋值为0
      • 但会产生二义性调用 func(int) func(char*)
    • C++11:nullptr 不一定是二进制0存储

image-20220323104321366

指针运算

  • 赋值:同类型 否则可以用int赋值double

    • image-20220323104650658
  • 加减:指针偏移 地址检索

    • p=p+1 相当于p =|p|+sizeof(int)保证操作数据时按照完整数据段
      • image-20220323104949954
    • 同类型可以相减,一般用于数组操作 (|p|-|q|)/sizeof(基类型)
      • image-20220323105203685
  • 比较:一般用== !=

  • 输出:字符串是特例cout<<p是内容

    • 因为cout是全局对象 <<函数:操作符重载 规定char*是字符串 转化成非char*就可以输出地址
    • image-20220323105546730
  • void*:管理内存 不能做运算,因为不知道sizeof(基类型)

    • 如果操作要做强制类型转换

      • image-20220323105914118

      • 作用是指针类型的公共接口:安全

        • 清零不需要知道数据类型,每一块赋0即可

        • image-20220323110523157

        • memset的实现:强制类型转换成char* memcpy showBytes同理

          • image-20220323110711474
  • 常量指针:const int c = 0;//c可读不可写

    • const int *cp;//cp是一个指针,指向const int cp对管理的数据空间只有读的权限 *cp=10 报错 *q = 8;可以

    • cp=&y;//可以!! 管理者cp只能读,管理者q可读可写

      • 作用:前提:有函数副作用 但有时候不想要
      • print(a);//空间太大 传指针print(&a);//效率高,但把写的权力交给print
      • 怎么兼顾效率和安全?*void print(const A a) 而且引用时也要用const
      • Use const whenever possible
    • image-20220323111656899

    • q=&c;//不可以!*q=8;相当于可改变常量的值

      • 但有时候可以去掉这种约束 如print(A *p) const A a;//a传不了了 怎么办

      • const_cast<int *>可以主动去掉const属性

      • image-20220323112147317

      • 假如print不诚信,其实改了数据,会怎么样?

      • *q=111 同样的单元有两个不同的值 问题:为什么不会覆盖128??

      • image-20220323114809074

      • compiler:编译时c换成128 *q换成111 提前把常量换成具体的值

      • showBytes可以访问Addr里的value 不能把常量指针给变量,除非可信任!

      • 如果参数不变,尽可能写成const!!!

      • image-20220323115028368

  • 指针常量:只有一次机会赋值单元,但是单元的内容可以改变 *p可改 p不可改

    • image-20220323115300768

指针和数组

  • &a[0]就是指针,可用于遍历数组 a[i]与*(p+i)等同
  • int *p=a;//a这里是表达式 f(a)//a也是表达式
  • int a[8];sizeof(a)可以得到大小,数组是一种数据结构 void(int a[], int n){sizeof(a)}无法得到大小
  • 当数组变量变成表达式后,进行了类型转换:数组类型—>int *const 所以要传n(size)
  • int b[]相当于变成了int * const b 空间不变数组可更改 不能写b++ 只能写p++
  • a[i]编译器会变为*(a+i) p也可以写成*(p+i)
  • image-20220323144417968

二维数组

  • int a[6][2]相当于12个连续空间 int *p = &a[0][0];

  • a[i][j]相当于*(p+i*2+j) i乘以2

  • a[0]相当于int[2] a6个T(T2个int) 高维是一维的叠加 int *p = a[0];//把两个int控制权交给p 可以

    • 如果访问2个之后的,其实越界了,但其实安全(本来就有这个空间)
  • 有没有二维指针?如q[0][0] 有 即int[2]* q=a;

    • q=a q的类型是6个T组成的一维数组 即T* 也可以用q=&a[0];进行初始化 q+1就是q+sizeof(T)
    • 具体写法:int (*q)[2] = a;或者 typedef int (*q)[2];都可以—>理解为typedef int[2] *q;
  • image-20220323153507709

  • 数组元素操作

  • 数组元素的指针表示法

  • sizeof(A);//返回10*sizeof(int) sizeof(A+1);//返回4(32位寻址时) 因为A变成表达式A+1是1个地址

  • image-20220323154318759

  • 降维:定义二维 传一维

    • maximum(A[0],2*4);//传递的是4个int组成的一维数组类型 是越界的访问形式
    • maximum(&A[0][0], 2*4); n可改为sizeof(A)/sizeof(A[0][0]) 数组长度变化时不用改
      • image-20220323163655929
  • 升维:定义一维 如何变成二维三维

    • 变成二维

      • typedef int T[2]; show((T *)b, 6);//T是类型

      • 因为定义指针q时也可用int (*q)[2]

        所以这里也能直接写成show((int (*)[2])b, 6);//说明b是指向2个int一维数组的指针

    • 变成三维

      • typedef int T1[3]; typedef T1 T2[2]; show((T2 *)b, 2);
      • show((int(*)[2][3])b, 2);
  • 内存空间数据块 解释二进制的方式依赖数据类型外存也是如此(word txt格式等)

  • 思考:怎么分行显示(4个int就换行) (i*2+j+1)%4==0 (i*6+j*3+k+1)%4==0

  • a[i][j]=*(a+i)+j a[i][j][k]=*(*(a+i)+j)+k

image-20220323165127598

image-20220323165340711

第九课

动态变量

  • 大小和生命周期不能由编译器确定,在非编译时刻确定

  • stack用于函数,heap用于动态变量,共用一块空间

    • image-20220325134811375
  • 申请空间通过函数实现,可创建一段连续空间,用指针或者取下标访问

    • image-20220325135213262
  • 多维数组怎么new?new int[12] 再用升维操作

    • C++:new 更清晰:typedef
    • C:(int *)malloc(字节数);//一定要把void*强制类型转换成int * 语法上有区别
    • 用new向系统申请空间时:进行有效性判定(有可能会失败!所以拿到指针需要判断是不是null
      • exception:可预见的/无法避免的(如文件破坏,socket端口被占用)
      • 如果申请不够会报bad_alloc
      • 可以手动释放内存:void f(); f(new_handler p);//有问题时可以调自己的函数来处理
  • new和malloc语义上也有区别:class A{}

    • p=(int *)malloc(sizeof(A)*20)不会调用构造函数
    • new A[20];//面向对象程序设计:RAII 每个对象获得生命周期时必须要被初始化 要调用constructor
    • C++建议用new OO时更好让对象处于有效状态

image-20220325140646180

  • 归还:delete

    • 写方括号:int[]可以不写 如果是A delete[]可以调用析构函数 delete只能释放内存

      • 释放内存还不够 析构函数要调用!
      • 如句柄 socket连接要取消 所以new[]就要用delete[]
    • A* P = new A[8]; delete[8] p;

      • delete[]这里的8写不写没区别,为什么?怎么知道还几个?

      • 如果用C:A *q=(A*)malloc(sizeof(A)*8);free(q);

      • sizeof(A)在编译时数值可以确定

        • free时怎么准确地还掉?cookie!!!

          • 建立一张表 名称(p) 地址 大小(8) q 地址 大小 表的大小在变化,这个方法不好
          • delete p/q时知道地址,那如何知道大小呢?cookie大小放初始地址上面的单元,就知道了size,然后还空间

          image-20220325143628613

      • 如果用*(p++)=0; delete p;有问题 大小找错了 还了不属于申请的空间 申请的指针不要动

      image-20220325143901317

    • 只要有new 就delete 防止内存泄漏

指针数组

image-20220325144316641

  • ping也是一个程序 *argv[]是指针数组 *env[]存了一些配置 没有给长度怎么办?结尾是null 可以遍历

  • 异步执行:调用另一个exe 如何判断返回值状态?用system(“test.exe”)等拿到返回值的状态(如01)

  • image-20220325150716222

  • 不知道传多少参数:…根据第一个参数的规范去拿参数

    • 因为从右往左入栈 后面的参数在第一个参数地址的上面
    • image-20220325151033166
  • 模拟写一个MyPrint

    • image-20220325151306567

    • 先定义变量,放在初始位置,然后按类型(用$约定 语言解析)拿不同参数,最后还掉

      • list:va_list就是char *
      • start:通过第一个参数s找到起始位置:list的地址+s的偏移量 找到起始位置
      • arg:因为有对齐 所以不直接用sizeof 而是用宏_INTSIZEOF()(一般都是用整数int对齐)
        • (x+n-1)&~(n-1)
        • image-20220325154819621
        • image-20220325152740525
      • end:指针变成0 变成不可用状态

      image-20220325153340420

    • 像MyPrint,max:第一个参数不一定是符号串,但一定要有一个具体参数

      • max要给出个数,否则不知道循环多少个
    • 万一违背原则(写1个参数传3个,写3个参数传1个),会访问不改访问的空间,改写的话可能改返回值,造成格式串攻击

指针和结构

  • ->和smart pointer(操作符重载)不同

    • image-20220325155636485
  • 如果传A a消耗大量空间 所以用A *a传地址—>加上const 防止函数副作用 被修改

    • call by reference:A &a 可以不用指针
    • image-20220325160148005
  • 多次申请的同类型动态数据放容器中管理:链表 多一块空间记录下一块空间的地址

    • 这种记录的管理方式也在硬盘里面(存下一个的地址):

      • 文件由磁道组成,最小单位是扇区。磁盘调度不能按顺序,那就不能记录顺序。所以这种情况要建表,文件从硬盘装载时查表,就知道文件需要有哪些块,然后统一进行磁盘调度,同时把表放到内存,2个原因导致效率高(FAT文件分配表:File Allocation Table)。这个表是确定的(格式化后地址就固定了),不会有变化。

      • image-20220325161554910

动态变量的应用(链表)

  • 根据访问的不同,可能是栈(在开头插)或者队列(在结尾插)
    • image-20220325161929539

插入

  • 表头插入

  • image-20220325162400600

    • 这里可以不判断head是否为null,直接p->next = head;head = p;
  • 表尾插入

  • image-20220325162550215

  • 插在某结点(值为a)的后面

    • 短路表达式:如果前面确定值,后面将被忽略
    • q!=null保证不会把null结点来引用 可叫做哨兵结点(也有说法说放一个值域为null的结点为开头,避免判断链表是否为空,简化边界处理)
    • image-20220325162847920
    • 希望链表满足一定的属性次序,所以不一定操作头尾(有序:折半查找)增删效率与查找相反
      • 主要的应用在数据库的索引(建立序关系)如果索引过强,增删效率低,查找效率高
    • 根据问题来调整,没有绝对的好坏
  • 插在某结点(值为a)的前面

    • 需要两个指针,因为不知道前面的

    • 加很多null,害怕空指针,因为有exception

      • 用guard node:head值为null除了头结点不会有其他为空

        image-20220325164505394

  • 结点删除

    • 同样需要两个结点记录
    • image-20220325164647367

练习:单项排序链

image-20220325164910809

image-20220325165208405

image-20220325165352004

问题:first是一个全局变量。如果作为main里面的局部变量,insert(first,n);会有问题吗??

链表代码实现

without dummy node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <iostream>
using namespace std;
struct Node{
int k;
Node *next;
};
//插入后返回新的头结点
Node *insert(Node *head, int val){
Node *n = new Node;
n->k = val;
n->next = nullptr;

Node *ans = head;
Node *prev = nullptr, *curr = head;
while (curr != nullptr && val >= curr->k){
prev = curr;
curr = curr->next;
}
// prev就是要插入的值
if(prev == nullptr){//当n是头结点
n->next = head;
ans = n;
}else{//插在prev和curr之间
n->next = prev->next;
prev->next = n;
}
return ans;
}
//删除后返回新的头结点
Node *remove(Node *head, int val){
if(head == nullptr){//链表为空,没有可删的东西
return head;
}
Node *ans = head;
Node *prev = nullptr, *curr = head;
while (curr != nullptr && val != curr->k){
prev = curr;
curr = curr->next;
}
if(curr != nullptr){//找到待删除结点
if(prev == nullptr){//删除的是头结点
ans = head->next;
delete head;
}else{
prev->next = curr->next;
delete curr;
}
}
return ans;

}

void print(const Node *head){//const 因为不需要改内容
if(head == nullptr){
cout << "<null>" << endl;
return;
}
const Node *p = head;//p对管理的数据空间只有读权限
while (p != nullptr){
cout << p->k;
if(p->next != nullptr){
cout << "->";
}
p = p->next;
}
cout << endl;
}
//回收链表占用的空间
void release(Node *head){
if(head == nullptr){
return;
}
Node *p = head;
while(p != nullptr){
Node *q = p->next;//先把下一个指针记录 否则删了p后就不合法 可能地址值会变
delete p;
p = q;
}
}

int main(){
Node *head = nullptr;
head = insert(head, 2);
head = insert(head, 1);
head = insert(head, 3);
head = insert(head, 4);
head = insert(head, 0);

head = remove(head, 3);

print(head);

release(head);
}

with dummy node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <iostream>
#include <assert.h>
using namespace std;
struct Node{
int k;
Node *next;
};
//不返回,因为dummy一直不变
void *insert(Node *head, int val){
assert(head != nullptr);

Node *n = new Node;
n->k = val;
n->next = nullptr;

Node *prev = head, *curr = head->next;
while(curr != nullptr && val >= curr->k){
prev = curr;
curr = curr->next;
}
// 可以总保证prev不为null 简单了一点
prev->next = n;
n->next = curr;
}
//删除后返回新的头结点
void *remove(Node *head, int val){
assert(head != nullptr);

Node *prev = head, *curr = head->next;
while (curr != nullptr && val != curr->k){
prev = curr;
curr = curr->next;
}
if(curr != nullptr){//找到待删除结点
prev->next = curr->next;
delete curr;
}

}

void print(const Node *head){//const 因为不需要改内容
assert(head != nullptr);

if(head -> next == nullptr){
cout << "<null>" << endl;
return;
}

const Node *p = head->next;
while (p != nullptr){
cout << p->k;
if(p->next != nullptr){
cout << "->";
}
p = p->next;
}
cout << endl;
}
//回收链表占用的空间
void release(Node *head){
assert(head != nullptr);

Node *p = head;
while(p != nullptr){
Node *q = p->next;//先把下一个指针记录 否则删了p后就不合法 可能地址值会变
delete p;
p = q;
}
}

int main(){
Node *dummy = new Node;
dummy->k = -1;//-1在二进制补码里全1 可能有帮助
dummy->next = nullptr;
insert(dummy, 2);
insert(dummy, 1);
insert(dummy, 3);
insert(dummy, 4);
insert(dummy, 0);

remove(dummy, 3);

print(dummy);

release(dummy);
}

第十课

更正:不需要判断head是否为null 只要赋值就行

容器:

image-20220330101449125

指针与函数

函数指针:指向函数代码段的指针

  • double *fp1(int);//表示fp1是函数原型,参数int返回double
  • double (*fp1)(int)=&f;//这样写才行
  • (*fp1)(10);//就相当于f(10);
  • fp1,fp2是不同类型,不能进行相互赋值
  • 取地址符与f等价,fp1=f 所以可以fp1(10);
    • int a[8];cout << a;//此时a退化成指针,输出地址 cout<<&a;//此时a仍为数组,&a为地址
  • 形构一样,可以不断赋值

image-20220330102719781

函数指针

  • 原先扩展逻辑不好:前缀,op类型定死了

image-20220330103042935

getTask:把输入转化为任务结构

execute:分离op和op1,op2

  • add minus都是一个形构,用函数指针赋值:int (*fp)(int, int);fp=add,fp=minus fp(op1,op2)

switch-case:table driven 放入指针数组 就可以表驱动

  • 让指针数组和enum的指针一样

image-20220330103732094

利用op数组下标和enum下标后:

如果再增加int multi(int a, int b):只需要修改op数组和enum

  • 可以用宏制作(compose)一个程序
  • 如利用框架BEGIN_MESSAGE_MAP增加函数
  • 耦合度降低,程序维护性提升。
  • image-20220330104259493

冒泡排序:与数据本身无关,只是一个策略–>写泛型

  • 改数据类型:需要知道块大小(width)和初始地址(void *)块数量(num)

  • char *A=(char *)base;

  • char *tmp = (char *)malloc(width);//大小在调用后在确定分配的空间malloc 最后要free(tmp)

  • 内存之间的复制而不是赋值

  • memcpy(void *, void *, int size);//第j块拷贝到tmp

  • 序关系:两块数据没有类型,只有调用者知道序关系,如何传递给mySort?用函数指针 (*compare)让被调用者完成计算

    • int (*compare)(const void *elem1, const void *elem2)

    • 称为回调函数

    • icompare(const void *elem1, const void *elem2){

      ​ if(elem1->age > elem2->age)return 1;

      //我忘了强制类型转换!!

      }

    • 可用函数指针实现泛型,体现多态(一名多用)–>以后用template模板

image-20220330110140162

综上,函数指针1、实现公共框架 2、实现泛型,体现多态

image-20220330110301288

访问不同语义、同样接口的代码

多级指针

多级指针:基类型是指针

交换2个int:x和y地址值 交换2个char*:把原来的int变成char *就可以了

若写为char p1[]=”abcd”;//等价于char p1[5];这里的p1p2地址一旦创建后不能被移动,所以只能用strcpy来完成交换

数组名a指向是数值的第一个数据的‘首地址,不容修改,所以它不能随便的指向除了首地址的其他元素的地址。所以&a+1出现了直接跳出整个数组,把地址指向了最后一个元素的后面。

image-20220330112452190

引用

取别名,相当于修改原数组

image-20220330112642366

返回值可以为引用:x[j]即a[j]返回后多了一个名字max2,可以进行赋值(返回值要在调用者里面)

局部变量不能返回引用:在栈里面,函数调用完就回收!声明周期短!

OO操作符重载:此处引用可用于为左值的地方

image-20220330112913450

OOP

ADT:Stack约束

image-20220330113911900

OO:数据和行为封装在一起(Encapsulation)

封装理念:Information Hidding信息隐藏

  • 构造函数Stack()编译器会强制执行,保证对象初始化
  • 因为buffer[]私有,所以不能直接访问buffer,只能通过push/pop完成
  • 依然是Data Code Stack Heap不像Java有VM
  • 其实成员函数有默认的this,放在开头

image-20220330114831957

十个问题

1、一定会帮助生成默认构造函数A()

2、自动调用,一定要定义为public,有时定义成private?

3、什么情况下引入成员初始化表?初始化次序按照定义,而不是按照语句

4、为什么引入操作符重载:A a,b;a=b;//编译器帮忙重载

5、Late Binding:程序运行时才绑定 virtual虚函数,需要告诉编译器

image-20220330115950048

6、什么时候用virtual?主动向OS申请Late Binding

7、什么时候用private/protected继承?和public完全不同

8、有些操作符不能用全局函数实现重载,有些只能用全局函数

9、什么时候成员函数可以返回&引用?合理的情况

10、什么时候需要自主管理内存?new delete

image-20220330120337771

第十一课

OOP好处

image-20220408141155148

image-20220408141628518

好处仅在编译期间,编译后和非OO代码其实是一样的,有虚函数this

最大的差别:封装:函数放在结构体中间

  • 用户使用Stack不需要知道内部细节,如何初始化,但可以使用接口

image-20220408142424797

  • 面向过程:程序是命令的集合,可以线性化 函数切分成子函数降低复杂度
  • 面向对象:程序是对象的集合
    • 有继承、多态
  • 基于对象:Ada 把函数捆绑在对象内 但没有继承多态

image-20220408142348216

使用OO后开发效率,软件质量都能得到提升

封装

image-20220408143851891

大规模程序:分头文件和源文件

  • 头文件a.h
    • 声明public private变量函数
  • 源文件 a.cpp
    • 写完整的构造函数和类方法
    • 指明作用域:TDate::SetDate(int y, int m, int d)
  • 也可以不分开,像Java一样写在一起,把所有内容放在头文件(如inline)
    • 调用函数时,反复调用函数会花费大量时间在调用机制上。会建议编译器使用内联,但不保证编译器会这样实现。
    • 如果声明成inline函数,会直接用函数体替换函数调用,就可以不用函数调用机制
      • 不好,因为inline是代码的展开,可能让目标代码很长。
      • 所以通常只将小函数getset设置内联,大函数放在源文件里

编译时一个单元一个单元编译,需要其他单元时只需要把头文件拿过来(已声明),源代码在链接时做

  • 两个互相使用时?先声明两个函数

类:ADT 私有变量不能赋值,如果要赋值用set函数

Tdate g;就是一个对象,因为已经调用构造函数分配了内存,初始化好了 指针需要new

g:全局静态存储 t:栈区 p:堆区

  • 构造函数,进行值的初始化

构造函数

image-20220408144513177

  • 无返回类型:void都没有

  • 可重载:参数列表不同

  • 不写:编译器提供默认构造函数,无参数(写的话就不再提供,防止冲突)

    • 调用默认构造函数后,栈堆中的值都是不确定的值,全局、全局静态、局部静态会初始化为0。栈堆中的对象里的成员变量也是不确定的值
    • 主要是完成对象的初始化,而不是为对象内容的赋值。
      • 建立标识符
      • 为对象成员开辟内存空间
      • 根据语句为对象变量赋值
  • 可声明成public或private

    • private:class外部无法访问创建对象

      单例:只能让对象内部的方法调用,在类内部提供一个方法创建对象-接管对象创建

      也可以多例:指定对象个数

image-20220408144844805

A a1 = 1; 会调用A(int i)进行类型转换

用数组时:可以调用不同的构造函数,默认无参

目标:分配内存,但我们也要对变量初始化

成员初始化表

赋值初始化:先用构造函数进行了初始化,赋值时又要调自己的构造函数

成员初始化表:效率更高

image-20220408150816980

变量初始化:

  • A():y(1),z(x),x(0){x=100;}
  • 按声明次序初始化:先x后y后z
    • char *p;int size;需要调换次序,否则size没有初始化会出错
  • 不可以像java一样,只有static const int可以在类内部初始化
    • const、引用类型只能在声明时使用初始化列表进行初始化,不能进行赋值
    • 赋值只有在空间有了才能赋值,所以写在括号里
    • 构造函数可以重载
    • 成员对象若没有无参构造函数,也要在表中初始化

image-20220408151304723

  • 成员对象:B中有对象A,不是引用而是对象
    • 通过B的函数调用A

image-20220408151518554

数据成员太多时,容易出错,用声明式初始化

析构函数

image-20220408151753775

2种情况需要释放:其中程序员只需要负责堆上new的部分

栈:作用域结束,对象会自动消亡

堆:自己调用delete释放 否则会造成内存泄漏

  • 能不能用垃圾回收?Java不知道什么时候垃圾回收,实时性低
  • 垃圾回收只释放堆上的内存,效率障碍,文件操作的句柄、数据库连接忘记关闭,释放不了,但可以调用finalize(),下一次再回收:不确定什么时候回收,所以不太好

image-20220408153020773

C++内存管理:RAII(资源获取是初始化)利用构造的对象最终会被销毁的原则

  • 如果在堆上new了资源,一定要释放

析构函数声明成private的作用?

  • 强制自主控制对象存储分配(不能在栈上创建对象->只能在堆上)

编译器不允许声明对象,如A aa;因为在栈上,无法自主进行析构

但可以在堆上生成A p = new A; 发现delete p;失败,但可以在A内部写destroy。*p->destroy();

image-20220408153035771

s2=s1;//是s2重新开数组,还是拷贝s2头指针?

拷贝构造函数

image-20220408153725808

对同类对象进行初始化时:传参数时,返回值时

  • A a;
  • A b=a;

避免修改原类型:A(const A& a);

  • const类型:防止修改

  • 引用类型:节省了点空间。

    • 要是没写引用,则是值传递,会无限递归调用拷贝构造函数

    • #include <iostream>   
        
      using namespace std;
      class CExample  
      {  
          int m_nTest;  
      public:  
            
          CExample(int x):m_nTest(x) //带参数构造函数   
          {   
             cout << "constructor with argument/n";  
          }  
            
          CExample(const CExample & ex) //拷贝构造函数   
          {  
              m_nTest = ex.m_nTest;  
              cout << "copy constructor/n";  
          }  
            
          CExample& operator = (const CExample &ex)//赋值函数(赋值运算符重载)   
          {     
              cout << "assignment operator/n";  
              m_nTest = ex.m_nTest;  
              return *this;  
          }  
            
          void myTestFunc(CExample ex)  
          {  
          }  
      };  
        
      int main()  
      {  
          CExample aaa(2);  //constructor with argument     
          CExample bbb(3);  //constructor with argument  
          bbb = aaa;  //assignment operator  因为bbb已经实例化,不需要构造,只调用赋值函数 
          CExample ccc = aaa;  //copy constructor  但是ccc还没有实例化,因此调用的是拷贝构造函数
          bbb.myTestFunc(aaa);  //是aaa作为参数传递给bbb.myTestFunc(CExample ex), 即CExample ex = aaa;和第四个一致,所以还是拷贝构造函数
            
          return 0;     
      }  
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313

      > 假如拷贝构造函数参数不是引用类型的话, 那么将使得 ccc.CExample(aaa)变成aaa传值给ccc.CExample(CExample ex),即CExample ex = aaa,因为 ex 没有被初始化, 所以 CExample ex = aaa 继续调用拷贝构造函数,接下来的是构造ex,也就是 ex.CExample(aaa),必然又会有aaa传给CExample(CExample ex), 即 CExample ex = aaa;那么又会触发拷贝构造函数,就会永远递归下去。

      什么时候需要**自定义拷贝构造函数**?深拷贝

      - 如果析构掉s1,abcd也被析构,s2变成悬挂指针(指向的地址不知道存了什么东西)
      - 所以使用深拷贝(拷贝构造函数)

      ![image-20220413103027595](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318911.png)

      有自定义拷贝构造函数:调用默认构造函数而不是默认拷贝构造函数,认为a的部分不用拷

      - 如果想拷贝A,又想自定义:**写a(b,a);**显式调用成员对象的构造函数 通常要多写一个默认构造函数

      > generate返回时有一个拷贝,赋值时又进行了一次拷贝赋值,进行了3次对象创建,效率低(左值引用)
      >
      > - 3次:先建对象"test" 返回时拷贝一次 S=时又拷贝赋值给S

      这里有错,其实是将S传入generate(),拷贝构造就是针对S,只进行了1次。

      const A &:左值右值都可以绑定 拷贝构造

      A&&:只能绑定右值,左值仍然会用拷贝构造

      # 第十二课

      ## 移动构造函数

      ![image-20220413104654012](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318955.png)

      **左值**:变量

      **右值**:常数、表达式、函数调用

      右值引用:int &&a = 10;//a可修改

      const int &a = 10;或const int &&a = 10;//a不可修改

      右值绑定在const上,但不能绑定在非const(左值)上,因为不知道右值什么时候被销毁

      - 当然左值可以绑定在常量引用上

      generate()是右值,不能直接做修改

      - 右值引用,可以使用移动构造函数 如swap已经被优化
      - p是目标数组,把s.p移动到p,然后把s.p置为null
      - 这样就**不需要新的空间资源**
      - 若写成string &&S,持有引用,该地址则可以被使用

      若没有自定义拷贝构造、拷贝赋值、析构函数,编译器会合成默认的移动构造函数和移动赋值函数

      - 拷贝构造有变,一般移动构造也会变
      - 析构函数管理额外申请的资源,编译器不负责,所以没有默认的函数

      53原则:3析构函数、拷贝函数、移动函数

      不需要创建新资源:效率高

      ## 动态内存

      不确定有多少对象

      栈:局部变量,按值传递的参数

      堆:动态内存

      - C:malloc free
      - malloc(sizeof(A))不能调用构造函数(构造函数只能隐式调用)
      - C++:new delete 动态对象(除了分配内存还能调用构造函数、析构函数)

      new/delete:既是关键字,也是操作符(有权限进行重定义)为什么引入?**new调用构造函数 delete调用析构函数**

      ![image-20220413105554321](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318868.png)

      new A的返回值是A* 赋值给p new了后一定要delete 因为堆上不会自己调用,栈上代码离开区域,就会被释放(不同)

      ![image-20220413105803514](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318904.png)

      new也可以用于基本数据类型 int *i = new int;在堆上创建基本数据类型

      栈上的对象都有名称,堆上的对象都无名,**只能用指针访问**(指针也是数据类型,大小始终是计算机字长)

      ![image-20220413110045626](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318932.png)

      ![image-20220413111533970](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318979.png)

      ![image-20220413111708855](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318005.png)

      建议delete对象后,将指针置为null

      - delete intPtr;//把Object的内存块释放
      - intPtr = null;//把指针的内存释放
      - double free: 避免悬空指针dangling pointer 容易有段错误

      不知道指针类型,可以写void *p。但delete有点问题。

      - void *p;//若不指定类型,delete时不知道调用哪个析构函数,只会释放内存指向的Object,不会调用析构函数。所以需要显性进行类型转换
      - delete p;

      ![image-20220413113140183](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318038.png)

      - 先调用析构函数:调用多少次?new时在数组地址之前存储4字节大小的100,就可以依次调用析构函数
      - p是A数组首地址-4字节的地址,存储数组长度100
      - ![image-20220413122325290](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318327.png)
      - 再归还内存:不加[],认为是1个对象,只调用1次析构函数;同时会出现段错误,因为把中间的内存块去掉了,前面的和后面的都在,不连续了,知道长度但不知道数组起始位置也没什么用

      同样的,int *p=(int\*)malloc(size);free p;//p的大小也是存在头部

      **对于内置数据类型,不需要调用析构函数**,所以不加上4字节存储长度,delete p直接把完整内存释放

      - int *p;
      - p = new int[100];
      - delete p;//写delete []p;也不算错

      ![image-20220413113559247](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318362.png)

      自定义类型必须调用**默认构造函数**(无参)(老标准)

      - C++11统一初始化列表:new int[5]{0,1,2,3,4}也可以显式调用带参数的构造函数

      ![image-20220413114039735](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318394.png)

      ![image-20220413114030010](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318428.png)

      删除时先删除行,再删除列,最后删除chArray2

      因为在内存间多维数组是**一维连续内存块**,所以不会new chArray[ROWS],而是new int[12],访问a\[i][j]用a[i*4+j]

      - 用**下标操作符重载**

      ## const成员

      ![image-20220413114708351](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318449.png)

      **const必须在定义的地方初始化**,所以用**成员初始化表**,而不是事后赋值

      对于一个对象而言,值不变(分配在一个内存);不同的对象值不相同

      对于一个类而言值不变:

      - static const所有对象共享一份内存 **编译期常量**
      - 只能初始化在static const int x = 100;(只有static const int能在类里面初始化)不能在成员初始化表的地方写

      ![image-20220413115545057](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318471.png)

      对象A是const,指所有成员变量不应该被改变

      f更改了const,但编译器不知道f会改变值 所以引入**const成员函数**

      ![image-20220413115654417](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318592.png)

      在声明和定义时加上**const关键字**,有关键字a就可正常调用,否则会编译出错

      a是const对象,因此a不可变

      - 能不能骗编译器?f()const 左边编译时会出错,右边没问题
      - 编译器怎么检查?怎么做呢??编译器只能获得A.h 所以要**在可以调用的函数后加const**

      # 第十三课

      ## const成员

      ![image-20220420103159539](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318617.png)

      如何告诉编译器a.f()有误?编译时只能拿到a.h,所以在show()后面标const。同时a.cpp也会标记const。

      - 能不能骗编译器?f()加const,**不能骗!**
      - 变量什么时候不能被赋值?已经被声明成const
      - 其实f()=f(A *const this) 如果声明const 变为const A\* const this
      - A ***const** this:指针不可被修改
      - **const** A\* const this:指向的内容不可被修改

      其实const作用于参数,就类似于const A a(0,0);

      ![image-20220420103815831](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318662.png)

      **引用不能被更改**,但引用指向的值可以改,即使指向a也可以。

      *new int:\*是解引用操作符,可以把一个int赋给引用

      new int是指针,引用需要指向变量,所以将指针加*赋给

      - 按位const:每个内存值不变
      - 按逻辑const:对象概念上无变化,允许以成员为单位变化。**C++采用按逻辑**

      mutable关键字:不受const的限制,可以赋值

      const_cast<A*>(this)->x=1;//放入show(),编译可通过,相当于把const A\*变成A\*,可以进行赋值了

      ## 静态成员

      ![image-20220420104637405](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318700.png)

      如果把共享变量定义成全局变量:缺乏数据保护

      名污染:A类型有count,B类型有count

      所以把类共享的变量放在类自身:**类名规定名空间**

      ![image-20220420105301299](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318721.png)

      **加static**

      - 类对象共享:a,b中相同,因为是唯一的拷贝
      - 即使类A没有创建对象,全局数据区也有内存分配,程序结束后才释放-**不随对象的创建而分配内存,也不随对象的销毁而释放内存**

      **定义**:希望在类外部,只定义一次

      - 不适合放在构造函数或成员初始化中,因为创建对象时才调用。
      - 放在头文件中?每次引入头文件时会反复执行,重复定义

      所以通常在类的实现文件cpp中,如A::shared=0;//不用再写static

      - 若是static const int i = 0;//只能在声明的地方定义,如.h

      ![image-20220420105527114](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318743.png)

      静态成员函数:只能存取静态成员变量 类可以调静态函数和成员

      ![image-20220420105446253](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318893.png)

      允许通过类直接访问静态成员

      - a.f();A::f();//都行

      ![image-20220420105814617](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318919.png)

      可以**记录**当前类型创建了多少个对象

      - get方法用static,1个对象都没创建时也可以获取值
      - 静态成员**控制对象创建**:只有静态成员方法在没有对象时也能被调用

      int A::obj_count=0;//是定义不是赋值,int不可省略

      ![image-20220420110029203](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318950.png)

      private:接管

      public:static可以控制对象创建 如果原来有指向对象:返回;没有就创建 **单例**

      谁创建,谁归还:程序员创建,程序员用destroy归还

      ## 友元

      场景:private让类外部不能看到private成员,安全性高,降低耦合性。缺点是空间开销大。

      ![image-20220420112332938](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318992.png)

      - 初始化时不用分开行和列

      - 偏移不用写4个字节:按照指针类型进行偏移 p_data[i]=*(p+i)
      - 引用返回:避免拷贝,减少开销

      ![image-20220420112611301](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318031.png)

      每次通过element函数调用得到值。

      - 矩阵很大,不希望每次都进行调用

      ![image-20220420112804232](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318064.png)

      直接访问私有成员:效率会提高

      要相信这个函数才行,仍然有一定的数据保护:对于**相信的程序中实体,可以做高效存取**

      ![image-20220420113143888](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318414.png)

      关键字friend

      - **声明后就可以访问A的私有成员**!!!!
      - 编译时要先声明后使用,所以友元声明时前面要有对应的声明

      如果用外面的,用extern声明,要在链接时才知道是否正确

      - 对于全局函数:即使前面没有写void func(),也可以写extern
      - 但是对于**类里的友元函数,一定要先声明!**include或声明在前面

      声明友元时,尽量在前面都已声明!不管是函数还是类。(避免一些细节出错)

      ![image-20220420114009287](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318979.png)

      不能正常编译,因为不符合先声明后使用:Vector没声明过

      不完全的类型声明,满足编译器的内存检查:不能写Vector v,只能写引用Vector &v(和实际大小无关)

      - Vector v要拷贝,拷贝需要知道完整的类型声明(毕竟要构造个新的)

      友元关系是单向的,不具有交换性。如声明了类X是类Y的友元,不等于类Y一定是X的友元。

      友元不具有传递性,若类X是类Y的友元,类Y是类Z的友元,不一定类X是类Z的友元。必须在类Z中声明。

      相互使用类时有声明的问题

      ![image-20220420114848957](https://lapsey-pictures.oss-cn-shenzhen.aliyuncs.com/typora_imgs/202502032318999.png)

      写show(B &b)时,没有办法使用b.b;所以要放在**程序文件**cpp中,不能写在头文件(保证B的成员变量b已经出现)

      - 不能B include A, A include B 互为友元时AB的类定义感觉只能写在一个头文件里

      **相同类的各个对象互为friends(友元)**

      ```c++
      class complex{
      public:
      complex();
      int func(const complex& param){return param.re;//看这里}
      private:
      double re,img;
      };

      ...
      complex c1,c2;
      c2.func(c1);//看这里

原则:

image-20220420115408571

不是无脑get,set

  • NONE:如果不公开使用,直接不公开
  • 分为read/write功能,共4类

Law of Demeter:使朋友最少,朋友了解的类也最少

总结:管理对象构造:构造函数、析构函数、拷贝/移动构造

3种成员:const、静态、友元

第十四课

继承

image-20220420115732175

面向对象的核心机制。把类似但是不同的概念在软件中反映。

  • 相同的部分不用重写。
  • 不同的部分用继承、多态等方式增量开发。

只有稳定、不变的才进行复用:接口代码(描述层次性概念)否则父子类耦合性高

单继承

image-20220422104057635

class中的成员默认是private,而struct的成员默认为public。来自class的继承按照private继承处理,来自struct的继承按照public继承处理。只有这两个区别。

struct也可以继承。可以说除了这个都差不多。

“:“表示继承的含义,前面是派生类 后面是基类

派生类需要知道基类的内容,所以必须已经有定义

  • 没有继承时,protected和private相同
    • 仅仅在继承类中表示可以被派生类使用。
    • 派生类可以访问基类的保护成员,但不能访问基类对象的保护成员

继承有点像在子类中定义了一个父类的成员变量,看不见private,所以像set_ID()不对

  • 那有什么用?ID对派生类对象的正常工作很重要,也可以用父类的方法正常使用

image-20220422104246282

根据名称寻找

两个showInfo():调用派生类里的版本

但如果写showInfo(int x):vs.showInfo();会报错!C++先通过名字匹配,匹配不上才去找基类的版本

  • 先在undergraduate_Student的名空间里找,找到后就不会在基类中找
  • 函数隐藏:其他所有版本的函数都会被隐藏
  • 那岂不是不能做重载?用解决名空间问题的方法
    • using Student::showInfo;//把Student中的showInfo引入undergraduate_Student的名空间
    • 可见 再调用vs.showInfo();vs.showInfo(10);两种就都可以了
  • 继承:子类拥有一个父类的成员变量,拥有各自的名空间
  • 继承的本质:寻找名称的过程。所以C++可以转换为C语言编译?所以继承不是语言上的差别,而是理念上的差别。

构造函数、析构函数、拷贝构造函数(与资源创建相关)不能被继承:否则会出现子类自己新申请的资源,不能被释放

有两个类,是继承关系。如果在派生类中要用到基类中的元素,那么怎么去初始化它?当然是要利用基类的构造函数去初始化它啦

多态

希望根据基类对象和子类对象调用对应的版本?怎么做?

  • 加virtual!虚函数

image-20220422111259413

能不能改变基类中的访问控制?可以改一些

  • 可以将public改成private,不能将private改成public
  • 不能写char nickname[16];否则出现两个nickname(都有各自的名称)

如果不写:默认跟之前一样 private

  • private/public继承在访问父类时没有任何影响 对派生类本身无用
  • 但会影响派生类的用户:其他类访问或继承 如写private后nickname不能访问了

前向声明不需要写继承自哪里!!!只用写名字!!!

友元和protected

image-20220422111537325

为什么有错?因为派生类不能访问作为参数传入的基类对象b的受保护成员,只能访问派生类自己类中的基类的受保护成员(看作成员变量)

  • 否则定义派生类和友元,就可以避免受保护机制

友元不具有传递性,在继承关系中也不具有传递性(第一个函数可访问,第二个不可以访问)

image-20220422112158451

执行次序:基类构造函数、成员类构造函数、派生类构造函数(多个:按照声明的顺序调用构造函数)

image-20220422112542200

若要调非默认构造函数:成员初始化表指出(毕竟执行次序在前)

对于拷贝构造函数呢?B b2(b1);//没有指明基类如何拷贝,但子类又自定义了拷贝构造函数,所以基类部分没有默认拷贝构造函数,所以A此时调用默认构造函数

  • 指明:B(const B &b): A(b);//b派生自a,b包含a的数据成员,所以可以指明a要用拷贝构造函数

image-20220422170112572

构造函数不能被继承

  • 如果B没有新的成员变量,但要写许多与A构造版本相同的B的构造版本,很麻烦–>用语法糖(没有改变程序的语法,但提供简单写法方便使用)
    • using A::A;//不是继承,为派生类生成与基类形参完全相同的构造函数,派生类自己的成员就只会默认初始化

虚函数virtual

image-20220422114259009

为什么可以将派生类对象赋给基类对象?类型相容

赋值相容:b的内容拷贝到a空间,但是内存对不上

  • 在java中A a,a=b只是引用变化,没有影响
  • 在C++中ab都是对象,a=b合法,但不保证信息不丢失——内存对不上
    • 对象切片:只留下基类的对象,派生类多余的被丢失

A a=b;//过程其实是b作为参数传到A的拷贝构造函数**const A&**,函数只会选择b中A有的部分,所以最终进行了切片

image-20220422114658268

所以赋值给引用或指针,防止对象身份变化

image-20220422115237469

a=b;//ok会切片 b=a;//类型不相容 a.f();//赋值与否都是A 调用A::f();

  • func1(b);//调用A::f(); 静态绑定
  • func2(&b));//调用A::f(); 静态绑定
    • 根据声明的类型绑定。不管是A还是B,不看实际传入的类型

image-20220422115532364

  • 前期:编译时刻根据对象声明的静态类型确定
  • 动态:根据运行时刻实际对象类型确定,可根据派生类对象的不同类型决定。效率低,Java就是完全采用动态绑定的。所以C++默认前期绑定。

显式指出:virtual实现多态

第十五课

虚函数

image-20220422120029478

如果在基类中写了virtual,子类中如果不写virtual,也是动态绑定。

根据当前类型调用不同版本的f:重定义。

image-20220427203939002

  • 全局函数不可以:不能有多态

  • 静态不行:只要类就可以调用,与对象无关

  • 内联不行:编译时决定通过哪段代码替换实现(而对象要在运行期才确定类型),所以尽量不要把虚函数实现写在头文件里

  • 构造不行:获取资源,派生类比基类持有的资源往往更多,virtual后会根据派生类的实际类型去创建对象。但是,构造函数在创建对象时自动调用,不可能根据父类的指针或者引用去调用(不像func2(A *pa)一样),所以不会成为虚函数。如果要调要用虚函数表,也要知道实例类型(但构建恰恰是创建实例,矛盾)

  • 析构要:保证delete时调用派生类版本,释放所有资源

image-20220427205705374

  • p=&a;p->f();//调用A版本p->h();//A的h()会绑定到调用过程中
  • p=&b;p->f();//调用b版本
  • 编译时f()没有静态绑定的调用地址,运行时a\b都会将额外的信息记录在内存头上,创建a时把a.f()地址存在头上,b.f()地址存在头上

根据定义虚函数的不同,记录的地址不同:不确定在前面加上多少个内存(偏移量)用指针!可变长 虚函数表,保存函数的地址

  • image-20220427210258866

找到对象的指针->找到虚函数表里的表项->调用对应的函数指针

B重定义f():覆盖A,g还是原来的

前面一个实体,后面一个实体:函数调用或者类型转换

  • p-4:第一次取地址取到A_vtable(虚函数表的地址),第二次取地址取到A::f。p是this,默认的f(const A this)*
  • 虚函数表的地址在编译时就能确定,但在运行期才能放在对象的头上。而构造函数执行时,A_vtable,B_vtable还没有完成,所以也寻找不到构造函数的vtable。整个对象构造完毕,vtable才可以运行。
  • 所以(**((char *)p - 4))(p)等价于p->f()

显然静态绑定的效率高很多

image-20220427212250525

依次调用了哪些函数?

注意构造函数可以调用虚函数(没有虚函数表,没有虚函数性质,看成普通成员函数),编译器自动根据构造对象的类型调用

B b:构造函数 先调A::A(),A::f(),B::B()

p->f();//调用B::f()

p->g();//静态绑定,p是A类型,调用A::g()

f():不管当前的指针类型是A还是B,根据实际指向的对象类型(B b),所以调用B::f()

p->h();//A::h() B::f() A::g()

右上角的p->f():调B::f() A::g() 不对!实际上是B::f() B::g()

  • 因为动态绑定其实是用this->g()实现的,而this从B* cosnt this来,此时因为f是动态绑定,传入的类型是B*
    • h()因为是静态绑定,传入的是A*

难道每次都要看this的类型吗??

  • 静态绑定:根据普通的函数调用,名空间的一致
  • 动态绑定:内存对象的实际情况,对象身份的一致

h():非虚接口 不是虚函数,但可以呈现多态的特性

只有成员函数可以呈现出多态,如果全局函数想呈现多态呢?非虚接口

h():只有虚函数发生变化,非虚函数不发生变化

  • 若有f(){f1();f2();f3()}可以作为算法骨架,复用稳定的步骤f1(),f3(),对于多态的部分只需要定义f2()即可
  • 可定义动态算法:template method pattern 模板方法

如果C继承B时,void f不想是虚函数,但不知道是不是虚函数,很混乱:

提供语法机制:

  • 关键字override:指明该函数是虚函数的重定义 基类中声明过virtual 所以肯定不会有函数隐藏
  • 关键字final:指明该虚函数不能被子类重定义了

image-20220427213640892

  • f1对:const:this指向的内容不能改,没说f1的行为不能改

  • f2错:参数不能变 int f2()也不行 虚函数返回值和参数都相同,如果不是虚函数同名就可以达到覆盖的效果

建议都写override

纯虚函数

image-20220427214112230

纯虚函数:不需要实现的函数,作为框架

  • 加上=0:没有定义,意味着没有函数的实现,虚函数表中会保留一个位置,但不放地址,只有派生类给出定义后,地址才会被填写

可以在声明之外给出定义,可以复用父类的实现(只是调用,不是继承)

ac.f();会出错,因为没有地址,所以不允许纯虚函数创建对象(不能传值调用,防止对象切片,同时可作为基类使用)

  • 有纯虚函数的类是抽象类!
  • 抽象类提供框架、接口,派生类提供实现

image-20220427214340243

每个派生类有自己的display()

image-20220427234505312

image-20220427234531431

增加linux、unix都很简单,代码维护性和复用性很强

语言为设计服务:抽象工厂模式

image-20220427234908682

析构函数往往是虚函数

虚析构函数

image-20220427235154686

不能正确调用D的析构函数,而是B,不能正确释放资源

基类释放资源的部分依然会调用,但会额外调用delete name

  • 先释放成员函数,再释放B

image-20220428000342071

C.f()没有缺省参数值,编译器静态检查时不会报错,因为只知道声明的是A*,不知道运行时实际指向的对象类型

  • 左边输出1,右边输出0?但实际是左右均输出0
  • 缺省参数值用静态绑定,检查语法时根据基类型语法,用的时候不管后面定义没定义,只要f不带参数时就静态绑定x=0(即使B中override了)
    • 同时动态绑定会增加新的开销,所以不要定义

image-20220428093859070

第十六课

虚函数

image-20220504103110448

复用接口时复用类型,而不是复用实现(复用实现为了少写代码)

  • 非虚函数:接口和实现都被继承,要保证子类中不会发生变化
  • 一般虚函数:接口和缺省实现的代码会被继承。
  • 纯虚函数:只继承接口,子类必须实现

继承

如何确定继承?里氏替换原则:子类的对象能替换超类的实例,子类的实例之间也能互相替换

image-20220504105940835

基类的fly()不会抛出error,所以子类也不能抛出error,保证换一个子类对象,基类代码不用改变

Design by contract:契约式设计,保证一样

  • Pre-condition:确保参数满足条件
  • Post-condition:确保方法调用后的结果一样,不要多一个error
  • 不变式:正方形总满足s.width()==s.height()

不能让正方形单独调用setHeight\setWidth,否则破坏了不变式

  • 把方法设成私有?静态绑定时p是Rectangle类型,所以可以调用setHeight.
  • 设为virtual?然后在setHeight\setWidth中调用setLength?的确可以保证assert
    • 对于Widen方法,用setWidth方法变宽一点,不过要保证高度不变(与额外的性质冲突)

发现虚函数和非虚函数都不行:因为性质不满足非虚函数的性质,不符合契约式程序设计

image-20220504110409132

private:让Rectangle不允许调用setHeight\setWidth?不是,访问控制是静态绑定的。

  • 编译时,p是Rectangle,setHeight是public,可以通过检查
  • 运行时,p是Square,setHeight是private,也没有问题
  • 虚函数是动态,运行时;访问控制是静态,编译时。

所以因为这条性质,正方形不能继承长方形

image-20220504111907464

不要定义同名的成员函数,防止变来变去

  • 只定义虚函数
    • 尽量符合契约式设计
  • 不定义同名的成员函数

image-20220504113131437

  • 私有继承:复用基类中的代码,有protected成员,或者希望重载virtual function时,该技术才有意义
    • 组合只能使用被组合对象的公共部分
  • public继承:is-a private继承:has-a外界看不到,引入对象,能使用保护成员和虚函数
    • 万不得已(protected成员,希望重载virtual function)才使用
  • 设计层面上没有意义,只用于实现层面
  • b有Error,因为类型没有被继承(CHumanBeing的公开接口CStudent都没有,无法进行类型转换)
    • 不能采用框架式编程

多继承

image-20220504112948744

  • 不写[],默认private继承
  • 问题
    • 初始化顺序
    • 同名问题

image-20220504113243126

weight同名:设计的冗余

  • 说明Bed和Sofa有共同部分,可以进一步分解
image-20220504113540460 image-20220504113635641

SleepSofa还是有两个weight?只要一份就好

  • 菱形设计结构:顶层一份就好 virtual inheritance

虚继承

解决多继承中的名冲突问题。

image-20220504114027700

B::x和C::x就不冲突了:但是这是两份

虚继承:公共副本

image-20220504114136701

B、C虚继承自A,创建一个虚基类对象A,派生类存放一个指向虚基类的指针(接口是is-a,实现是has-a)

  • 语法上virtual public和public virtual一样
  • 初始化只需要1次,所以规定由最新派生的类来调用A的构造函数,如这里是D。所以D只有一份A对象。
  • 虚基类的构造函数优先于所有非虚基类的构造函数,因为非虚可能指向虚

基类A如果有一个virtual,则BC也有virtual/BC有同名的virtual函数f:D覆盖f时覆盖哪一个?同时覆盖,调D实现的版本

  • 如果一个类有虚函数,派生类中有多个虚函数表,同名的版本会被覆盖

如果想单独覆盖一个版本?留给大家

image-20220504115041303

有多少个基类,就有多少个虚函数表指针

  • 把所有虚函数的表都拷过来
  • 把覆盖了的虚函数写出来如D::v3
  • 自己定义的虚函数D:vD写在头上的虚函数表里

实现上单继承和多继承都是is-a

第十七课

image-20220511101610563

C++中多态体现在:

  • 函数重载(一名多用)
    • 虚函数多态是动态的,运行时确定
    • 函数重载是静态多态,编译时就确定
  • 类属多态:泛型编程,传入不同类型复用同一份代码
  • OO(前面讲的部分):虚函数

函数重载

image-20220511102403365

  • 返回值无所谓:先匹配名字,再匹配参数,就可以确定

    • 参数个数、顺序都能匹配,或者隐形类型转换

    • 更好匹配:不能比其他差,有一个参数更好

      • 如int到float/double,都不是更好匹配
      • 1完全精确,2整型提升(bool/enum->int),3标准转换(没有更好,一视同仁)
        • char到unsigned char/double,都是标准转换,一样的
    • 窄转换:允许 double->int/float

操作符重载

通过函数重载的方式,匹配名称、参数。

操作符重载就是函数重载

操作符重载

image-20220511103033988

用新函数取代add方法:重载operator +

  • 可以写成c=a+b

也可定义全局函数:传入两个参数,声明成友元函数

image-20220511103756106

  • 至少包含一个自定义类型,不能全是内置数据类型
  • 成员函数内不一定非要自定义运算符,因为this已经是自定义了

image-20220511104447664

不局限于class,其他类型也可以重载

  • 重载++,因为Day是自定义类型

  • 不能重载cout,但可以重载<<(用的最多)

    • ostream与cout相关,o<<代表本身的输出
    • 一定要返回ostream对象,需要连续使用(链式调用)
      • cout << d1 << d2 << d3;如cout << d1还是ostream

这个函数能不能作为成员函数重载?

  • 不行,否则只能传参数,ostream改不了了

参数一般都是引用。为了连续使用,返回ostream&对象

image-20220511105805983

四个操作符不能被重载

.成员访问运算符

.*成员指针访问运算符

  • void(A::*p_f)();//A类成员的函数指针,只能指向A的成员函数
  • a.*p_f就可以指向函数

::域操作符

?:条件操作符

  • 如果重载,维持原来操作,会发现a=1,b=1
    • 条件运算符会控制流程跳转->操作符重载是函数调用,所有参数都被计算出来,所有代码段都执行(导致a=1,b=1),程序理解有偏差

sizeof:不重载,参数是类型

可以重载

  • 类成员函数
  • 带有类参数的全局函数

双目操作符重载

image-20220511111127629

this必然是第一个参数,如示例中a是this

image-20220511111349685

友元函数:arg1,arg2至少有一个是自定义类型;而此时没有隐含的this参数,所以有两个参数

->是间接访问 []下标运算符 ()函数调用运算符 =赋值运算符 均不能重载

关于()和[]和->为什么不能重载?

  • 因为都有保留的操作顺序,第一个参数是对象,第二个参数是对象相关的参数,而全局函数调用无法保留这样的顺序,所以强制用成员函数进行重载,这样就可以保证第一个是this了
  • 成员函数的第一个参数是this,全局函数没办法做限制
  • =为什么不行??
    • 编译器会覆盖你的重载
    • 会影响运算顺序,比如说右值赋给左值,可能搞成左值赋给右值

image-20220514155952237

全局函数和成员函数都支持时,用全局函数作为补充。

10+obj?不支持,成员函数第一个参数必须是自定义类型

需要支持交换律时,全局函数可以作为补充

  • 单目最好是类的成员函数,只有自己本身一个参数
    • 类型转换函数只能是成员函数
    • =()[] ->
    • 需要修改状态的函数,返回*this
  • 双目最好是类的友元函数
    • 隐形类型转换:如CL(int i)的构造函数可以让传10+obj时,10转成CL;而成员函数需要精确匹配,这样不行
      • 转换分2种,自定义->内置,内置->自定义
    • 交换律,让可交换

image-20220511113150974

借助短路,p为0时不会执行strlen(p):短路规则是内置规则

image-20220511114608504

这里想要重载乘号*,返回一个乘起来的对象

  • return会拷贝,不好
  • 如果代码中有连续的*(如两次),返回的是第二次new的对象,没拿到第一次的引用,可能造成内存泄漏
  • static result:还可以复用?不好吗
    • 如果要比较两个结果,发现是永真式

所以+,*这种就用return的方法

  • 返回值优化,不会进行拷贝,就在调用的地方进行一次创建
    • 如果定义temp然后进行return(其实也会被优化,没有拷贝),直接return两种写法均可避免拷贝

image-20220511115110414

单目操作符作为类成员函数更稳妥

image-20220511115415254

a++:返回值,返回++之前的对象拷贝Counter

++a:返回左值,*this,就是一个变量Counter&

本来应该没有参数,但在postfix里面加int

  • 区分两个函数
  • int值有作用吗?没有 哑元参数:只区分函数

第十八课

特殊操作符重载

=

image-20220518101501254

  • 有自定义:用自定义;没有自定义:编译器提供
  • 不能被继承:派生类有基类没有的成员,如果继承,会造成部分成员被复制,新成员没有被复制
  • 参数是同类型的引用,返回值是同样类型的引用return *this
    • 如A &operator = (A &a)
    • 二目操作符,第一个对象是自己的this对象
    • a=b作为表达式有值,是赋值后的值;可以c=a=b链式赋值
  • 返回值要不要加const?反正只要值?
    • 链式赋值可以,其他写法不可以 如(a=b).f()
    • 不要过分地加约束条件

image-20220518104152494

char *p这种成员变量,要有深拷贝,防止悬挂指针

  • 释放旧空间(因为长度可能不一样),申请新空间

A a, b;a=b;和A a=b;不一样

  • 前者:a先调构造函数,再调赋值操作符重载

  • 后者:相当于A a(b);用b构造a,调拷贝构造函数

  • 看一开始有没有被构造

避免悬垂指针和内存泄露:深拷贝

  • 如果在重载时内存不够怎么办?a已经被破坏,但没有被新分配内存,资源错误
    • 先赋值,后释放!!!
    • char *指针,delete时加不加[]都可以,因为是内置数据类型,不调用析构函数,直接释放完整内存

image-20220518104828268

可能两个对象指向的是一个派生对象

证同测试:避免自我赋值

  • if(this==&rhs)

  • 也可以用id避免

  • 惊喜地发现先赋值后释放的代码可以避免自我赋值,效率上稍差一点,但比较简单——最好的写法

[]

image-20220518105539697

下标操作符,针对数组,向量结构

  • 针对申请了数组的类,希望访问成员
    • 要取到真正的变量,所以返回引用类型
    • 如果是const string,重载函数也要声明为const
      • this.p可以是const,p[i]不是const,可以改
      • 所以加了个const没问题
    • 但是cs[0]=’D’可执行,不希望
      • 非常量:希望返回char &
      • 常量:希望返回const char
        • 不能根据返回值的类型进行重载!

image-20220518111301381

写两个函数,可以这样重载吗?可以

  • 参数一样,const很特别,const string const this,上面只有string const this,所以两个版本的**this类型不一样

image-20220518111905959

二维数组:内部用一维数组实现

get方法可以赋值,希望能正常访问二维数组:目前为止不行data[1][2]

  • 下标操作符只能一个个解释,不存在重载一号操作符和二号操作符
    • 看data[1][0]和data[0][0]的偏移量是多少
  • 第一个操作符重载:int *operator[](int i){return p + i*n2}
  • 第二个操作符重载:在int*基础上偏移即可data.operator[](1)[2]

三维:第一个操作符重载:p+i*n2*n3 偏移一个面

  • 第二个操作符怎么重载?不应该用内置数据类型,用自定义类wrapper

  • 变data.operator[](1).operator[](2) 返回自定义object

image-20220518113311529

行列号num1,num2;可以一层层的定义下去

返回为Int *时,编译器会帮助转换(构造函数)

  • 不想转换怎么办?explict,加上则必须显示调用,不支持隐式类型转换

RAII:初始化被对象封装

()

函数调用:希望f()可以直接调用(2.4, 0, 8);

希望把函数作为带状态的对象传入

image-20220518114807952

把常用操作方便地作为函数重载

image-20220518115603961

对ostream对象重载,int() 成功了返回1,没成功返回0

语法:operator前面没有函数类型 目标类型由函数名决定

  • 确保只有一个类型转换运算符

第十九课

特殊操作符重载

->

image-20220525102752552

指针间接引用操作符。二元

怎么重载?

  • a->f()不行,因为a是对象,用a.f()
  • a.operator->(f)不行,参数不能是函数名称,且可能还有函数的参数:**->的另一个参数类型无法确定**

重载时将->按照一元操作符重载描述

  • a.operator->()是一元操作符,就把另一个参数扔掉
    • 必须返回指针类型吗?也不一定,可以返回定义过->重载的对象,如返回b,可以继续嵌套b->f()
  • 调用函数时:a.operator->()->f()

图中*getPen()复杂,所以重载箭头操作符:

image-20220525102842592

image-20220525103427984

在每一个可能退出的地方都要delete p,如p->f()里面可能会throws exception

  • 多出口函数:很难管理

image-20220525104604373

  • 用RAII解决:用资源A初始化对象AWrapper

    • AWrapper只封装A
    • 特性:栈上资源不需要我们回收,堆上才需要我们回收
    • AWrapper在栈上,把生命周期不确定的堆上资源封装到栈上,就变成智能指针
      • A *operator->(){return p;}重载后即可p->f()
    • 可以用Template模板:支持任意类型的T封装在内
  • 局限性:堆的生命周期变成和栈一样了,只在一个函数内使用

    • 如果A要在几个函数中共享,要用更复杂的方式

既然这个对象的生命期和test方法一样,那为什么A不直接在栈上创建?

  • 可以是从外传进来的
  • 可能是动态创建,程序运行中才知道
  • A太大了,不想在栈上创建

image-20220525104823449

new,delete

属于运算符,可以被重载。作用:分配内存,调用构造函数。

  • 不能改变意思,会分配/释放内存,再调用构造函数/析构函数
  • 重载加粗部分,对于频繁释放内存的程序,自己管理内存,可以提升效率
  • operator new在构造之前,operator delete在析构之后,所以必须是静态

image-20220525110442029

size_t:自动计算 需要保证返回内存块指针

注意operator new[]和operator new不一样

  • 如果只定义后者,new A[]调用的还是系统自带的

  • 重载可以有多个,可以定义多个参数,…部分可以有很多版本

  • placement new定位放置new:new的重载版本,可以把p传入,可以缓冲区提前分配,在栈上分配大块内存 A *a1 = new (p) A1…可以反复使用同一块内存

    • A* p=new (ptr)A;申请空间,其中ptr就是程序员指定的内存首地址
image-20220525112038890

可以不管size(自动),但可以使用

  • 全局operator new可以当malloc:new(sizeof(A)*10) 自定义的new里面也可以调全局的

image-20220525112548432

image-20220525112652504

new(size_t, ostream)可以收集统计数据

image-20220525112932408

重载后:自己分配内存和统一管理

image-20220525112952763

如果空间不足:申请更大空间,扩容

image-20220525113054398

还需要管理内存扩容的问题:线程池

image-20220531232650988

怎么要内存?再调系统的operator new

image-20220525113148108

多申请几个person,免得老申请;create new space后再加入容器

image-20220525113350563

已经申请的10个内存空间,怎么知道返回哪个地址?容器方式管理,就可以知道创建Person时放在哪个地方

  • 数组形式:a[0]/a[1]均为地址,需要额外空间int[] flag记录使用情况,每次均扫描(需要额外空间记录使用)

image-20220531232947793

  • 链表形式

    • 自嵌入式链表
      • 若使用,改变指针,返回第一个空闲的地址

    image-20220531233207710

image-20220531233332301

归还时直接串到链表上即可。

image-20220525114400089

  • 内存池架构:推广到不同类型的对象类型管理,变成封装
    • 写了*next后,可以把一块块连续申请的内存挂上去,变成内存池

image-20220525114442916

image-20220531233607147

pool是指针,指向一大块的首地址,不够就加一块

image-20220525114743393

很多应用非常时间敏感:对实时性、效率要求高

Java的GC会卡顿2秒,但实际中C++内存管理很重要

长处:管理内存、资源

image-20220601001313265

使用:

image-20220525115553305

delete p1,立马把now_avail指向开头

不断移动now_avail指针,指示空着的内存(如果有一大块为空,delete所有)

image-20220601001504070

image-20220525115829360

货架的物品一次可以new很多。

image-20220601001637334

同一块内存可以复用,就可以重载new/delete。对资源进行更好的管理

第二十课

模板

image-20220525120045134

泛型编程:复制出很多模板,自然可以实现泛型。

语法相同代码,实际含义其实不同:加整数和加字符串不一样

image-20220601103008384

实现不同类型的排序,可针对所有类型T做排序,传入int/double,T就会变成int/double

image-20220601103114300

宏可以不需要生命类型

缺陷:简单功能,不能进行类型检查(强类型语言会出错,如传int string比较会报错)

image-20220601103220817

需要定义的函数很多,无论怎样也定义不全

image-20220601103247493

实现cmp函数,传指针;缺点是实现复杂

image-20220601103525688

把特定类型变成泛型,T是类型参数名称,用T1/T2也行

声明:template 告诉编译器是模板代码

怎么用?sort(a, 100);sort(b, 200);发现sort是模板,就会看a的声明类型->把模板拷贝一遍->把T变成int

很重要:推导类型参数,隐式实例化

class C{…}

C a[300];

sort(a, 300);必须要重载操作符,否则不知道A[]是什么意思

T t = A[j];是拷贝构造,下面是赋值,如果要深拷贝就要重载=和copy constructer

image-20220601104241817

可以有多个类型参数

  • 可带普通参数:size是编译时常量,调用时就会给一个值,编译时替换,需要常量的地方就会有常量

image-20220601105050038

可以有默认模板参数,按理说顺序可以不管,但最安全的方式就是从右向左定义默认参数

  • 编译失败:因为t=0,但不知道类型
  • 所以函数默认参数必须配合默认模板参数,否则可能编译失败

image-20220601105251474

如果两个参数类型不一样怎么办?定义T1,T2也行,但简单来说重载函数即可

  • 同时使用函数模板和函数重载
    • 注意先调用函数重载!!!可简单解决问题

image-20220601105602165

stack int和stack double不一样,因为不是同一个类

  • 只要是分开定义,就定义类型参数
  • 和前面的区别:要显式实例化

image-20220601111247268

image-20220601111345981

使用到模板才进行实例化,否则编译器不会生成

image-20220601111431553

声明函数:extern

引入头文件:编译引入.h,链接时有.cpp,为什么会有error?

  • 链接时找不到max(double,double)版本,目标代码没有模板,只有已经实例化后的代码,所以只有max(int, int)
  • 那就自己实例化?但include时是编译之后的代码,file2.cpp看不见源代码,不能自己实例化
  • 所以完整定义一般在头文件,编译单元就有能力实例化模板
    • 但容易出现同名问题,不过注意一点就行,还是要把模板的完整定义放在头文件

image-20220601112328385

源编程:类似递归,enum是常量,不需要运行时计算

  • 输入一个数,变成一段程序

  • 图灵完备:支持循环、条件、记录数据

  • 代码本身不需要内存,全是常量

  • 函数式编程:不带副作用、没内存的函数完成

    • 有输入输出:输出用enum\typedef\const
    • 递归函数:有输入,有输出,支持递归
    • 支持选择
  • enum只支持整型数?反正所有数据类型都可以变成01

函数式编程适合的问题:产生一个数据,继续调,不需要中间存储

异常处理

image-20220601113427975

image-20220601113440807

发现异常立即处理其实未必妥当

image-20220601113608733

构造函数不返回值,也没有参数,怎么办?

  • 用专门的处理机制

image-20220601113802240

异常对象可以是类,也可以是基本类型

throw如果是对象,调用拷贝构造函数,和return一样

image-20220601114121253

派生类到基类是允许的,其他都不允许匹配不同类型

image-20220601114400108

abort终止程序

多种异常特别乱,要写多个catch,就写异常类:

image-20220601114518079

throw已经拷贝了一次,catch不想再拷贝

  • 所以用引用方式来捕获
  • 会尝试多继承,与父类多次尝试,有一个匹配就行

image-20220601114938651

  • 输出MyExceptionBase:考的不是catch的顺序,其实考构造函数。throw e是静态编译调用拷贝构造函数,只能从声明类型看,所以传出来后是base。

image-20220601115430568

…是语法,变参机制,如printf也是变参

初始化列表可以构造内存单元、构造文件,但不是构造函数

  • try的时候就要传列表,A() try{:},但避免麻烦就别用列表了

第二十一课(三十分以上)

异常处理

没有finally

image-20220608111349934

空的类,编译器会提供

  • 构造
  • 析构
  • 拷贝构造
  • 赋值操作符重载
  • 取地址操作符重载。经常使用指针
  • 常成员函数,const对象会调用下面的,重载时要想是否返回const
    • &、下标操作符需要想用不用const

image-20220608111919556

对象内存的归还用析构函数:防止资源泄露

image-20220608112151853

定义基类:用纯虚函数;子类实现,体现多态

image-20220608112321947

pa是一个堆上的对象,所以结束后会delete pa。

throw前要delete pa,否则会泄露资源。

  • Java可以用finally
  • C++没有finally,因为RAII希望把申请的资源封装成对象,资源获取过程是对象的初始化,资源的生命周期是对象的生命周期

image-20220608112625655

模板类,封装参数化类型指针ptr,通过->和*实现把对象当指针使用

  • 比其他指针智能:创建在栈上,销毁时会自动析构

image-20220608112949750

try-catch依旧,用智能指针后不用考虑delete

  • 离开花括号后就会被消解

WINDOW_HANDLE是指向窗体的指针(句柄)

image-20220608113417417

把WINDOW_HANDLE指针,封装在类里面

就不用写destroyWindow了,对象会自己调用析构函数

没有重载->和*操作符?可以正常使用w吗?

  • 用operator WINDOW_HANDLE(),类型转换操作符重载,会隐式类型转换WindowHandle成WINDOW_HANDLE指针,就可以掌控指针的所有权,不用再重载指针操作
  • 相比之下,Smart Pointer掌管着-> *的控制权,所以要重载;赋值操作也多一些

I/O处理

image-20220608114314529

<<是双目,尤其是ostream,所以是全局函数重载

<<和>>参数和返回值是引用

  • 加减乘除:返回对象
  • 支持链式、左值调用:返回引用,甚至加const

3D输出时不能显示z,因为2D时已经重载了全局的<<,应用于3D

  • 绿色:再定义一个函数
  • 2D的指针c指向b,打印的还是2D,因为是静态绑定,看声明的指针类型

image-20220608115223988

让全局函数成为非虚接口,a.display()是动态绑定的

全局函数不能是virtual,成员函数才能是virtual。

如=重载成成员函数可以是virtual,<<不行

image-20220608115624466

析构函数必须要virtual

image-20220608115645869

不知道push_back是什么类型,怎么办?

image-20220608115822216

不能构造成虚函数:第一反应是构造一个能变成虚函数的

image-20220608120007548

除非是虚函数,其他是静态绑定

array[i]–>*array + i*字长

不能把子类当基类传入,因为字长不一样(子类有新成员),print函数的参数是BST array[],array[i]不会打印想要的结果。

考试相关

1、简答题(10分)

  • C++的发展历史和贡献人物

2、程序阅读题

  • 有错告诉错误,没错告诉输出

3、编程题

  • 手写代码

题量有点大,要写快点