第6章:嵌入式程序设计
本章对应原书第 253~333 页。软考视角:本章通常占上午题 5~8 分,是技术类章节中分值最重的之一。语言翻译原理(编译/解释、词法-语法分析、正规式与有限自动机)、C 语言细节(指针、结构体对齐、大端小端)、C++ 面向对象(类、继承、虚函数、异常)都是高频考点,下午题也会考 C 程序填空(链表、栈、队列算法)。
本章在讲什么?(先看这个)
嵌入式编程要直接操作硬件,所以既需要"贴近机器"的汇编语言,也需要"高级的低级语言"C——一行 C 代码能代替多行汇编,且调试维护更容易。本章就是为编程打底子:先讲程序设计语言的基本原理(源程序怎么被翻译成机器能执行的代码),再讲汇编语言的格式,然后是本章篇幅最大的C 程序设计(从预处理指令一路讲到指针和链表),最后是C++ 面向对象基础。学习时要特别关注软考的特色考法:编译器工作阶段排序、正规式与自动机、C/C++ 代码阅读题。
核心概念拆解
6.1 程序设计语言基础
6.1.1 语言概述:机器语言和汇编语言合称低级语言(汇编用 ADD、SUB 等助记符代替 0/1 序列,但仍是面向机器的语言);Java、C、C++、C#、Python 等是高级语言。源程序必须经"语言处理程序"翻译才能执行,翻译程序分为三类:汇编程序(翻译汇编语言源程序)、编译程序、解释程序。
「考点」编译与解释的根本区别:编译方式下,机器上运行的是与源程序等价的目标程序,源程序和编译程序都不再参与执行过程;解释方式下,解释程序和源程序(或其等价表示)参与运行,控制权在解释程序。简单说:解释器翻译时不产生独立的目标程序,编译器产生独立目标程序。
程序设计语言的定义涉及三个方面:语法(组成语法成分的规则,分词法规则和语法规则,可用形式语言描述)、语义(语法成分的含义,分静态语义——编译时可确定,和动态语义——运行时才能确定)、语用(记号与使用者的关系)。实现还涉及语境(编译环境和运行环境)。
6.1.2 语言的分类与特点:了解发展脉络——FORTRAN(第一个高级语言,接近数学公式,用于数值/高性能计算)、ALGOL60(里程碑,用 BNF 巴科斯范式描述语法,引入局部性、动态、递归等概念)、PASCAL(沃斯设计,结构化,曾主导教学)、C(20 世纪 70 年代,兼顾高级语言和汇编特点,UNIX 及其软件多用 C 编写,适用于系统级编程和实时处理,因此在嵌入式领域广泛应用)、C++(80 年代,增加类机制,面向对象)、Java(90 年代,保留 C++ 基本语法去掉不良特征)、Python(解释型,可翻译成字节码在 Python 虚拟机上运行)、VB.NET(编译成中间代码 MSIL,由 CLR 通用语言运行时执行)、JavaScript(脚本语言,嵌入 HTML)。
程序设计范型分四类,是经典考点:
| 范型 | 基础/特点 | 代表语言 |
|---|---|---|
| 命令式(过程式) | 基于动作,计算是动作的序列 | FORTRAN、ALGOL、COBOL、C、Pascal |
| 面向对象 | 始于 Simula(首次提出对象和类);特征:封装、继承、多态 | C++、Java、Smalltalk |
| 函数式 | 以 λ-演算为基础;LISP(1958 年,为人工智能设计)中程序和数据形式等价,大量使用递归 | LISP |
| 逻辑型 | 以形式逻辑为基础,建立在关系理论和一阶谓词理论上,关键操作是模式匹配,用 Horn 子句表达事实和规则 | PROLOG(适用于自动定理证明、专家系统、自然语言理解) |
结构化程序设计本质上属于命令式范型,特点:自顶向下逐步求精、按模块组装、程序只包含顺序、分支、循环三种结构且每种结构单入口单出口。
6.1.3 语言的基本成分:
- 数据成分:数据具有类型、名称、作用域、存储类别、生存期等属性。常量只有右值(值/内容),变量兼有左值(存储单元/地址)和右值。C/C++ 数据类型:基本类型(int、char、float/double、bool),void 类型,用户定义类型(enum),构造类型(数组、结构、联合),指针类型(type),抽象数据类型(类类型)。其中布尔类型和类类型是 C++ 在 C 基础上扩充的*。
- 运算成分:算术、关系、逻辑运算,C/C++ 还提供位运算;运算符要规定优先级和结合性。
- 控制成分:理论上已证明,可计算问题的程序都可以用顺序、选择、循环三种控制结构描述。while 型先判断条件后执行循环体,do-while 型先执行循环体后判断条件。
- 函数:函数定义(函数首部+函数体)、函数声明(函数原型,告诉编译器参数个数、类型及返回值类型,参数表只需列出类型)、函数调用。C 程序的所有函数定义相互独立、不允许嵌套定义(PASCAL 允许)。有且仅有一个 main 函数作为程序运行起点。
「考点」参数传递是必考题。C 语言采用传值调用(Call by value):形参不能向实参传递信息,
swap(int x,int y)交换不了实参 a、b。要用指针作形参(传地址,本质上仍是传值),或用 C++ 的引用调用(形参为引用类型时,对形参的访问和修改就是针对实参的)。
6.1.4 翻译基础(本章最"编译原理"的部分,也是软考最爱):
编译器的工作过程分 6 个阶段:
- 词法分析:从左到右逐字符扫描源程序,识别出一个个"单词"(关键字、标识符、常数、运算符、分隔符等),输出二元组(单词种类,单词自身的值)。依据是词法规则。例:
X:=Y+Z*60词法分析后成为id1:=id2+id3*60。 - 语法分析:根据语法规则把单词序列分解成各类语法成分(短语、子句、句子、程序),构造语法树,确定输入串是否构成语法上正确的程序。词法分析和语法分析本质上都是对源程序的结构进行分析。
- 语义分析:检查静态语义错误、收集类型信息供代码生成使用。主要工作是类型分析和检查(如整除取余的运算对象有浮点数就是类型不匹配)。声明语句要做符号表的填查。
- 中间代码生成:中间代码与具体机器无关,设计原则是"容易生成、容易被翻译成目标代码"。最常用的是与汇编指令相似的三地址码,实现常采用四元式:
(运算符, 运算对象1, 运算对象2, 运算结果)。 - 代码优化:可在中间代码或目标代码生成阶段进行;中间代码不依赖具体机器,此时的优化基于控制流和数据流分析,原则是程序的等价变换规则(如编译时已知的常数转换、化简临时变量、提取公共子表达式、循环优化)。
- 目标代码生成:把中间代码变成特定机器上的绝对指令代码、可重定位指令代码或汇编指令代码,与具体机器密切相关。
贯穿全过程的两项工作:符号表管理(记录各符号信息,可始于词法分析阶段,使用有时延续到目标代码运行阶段)和出错处理。错误分类:动态错误(运行时发生,如除数为零、数组下标越界,属动态语义错误);静态错误(编译时发现,包括语法错误——单词拼写、标点、缺操作数、括号不匹配等,和静态语义错误——运算符与运算对象类型不匹配等)。
「考点」编译器的前端包括词法分析到中间代码生成各阶段,后端包括中间代码优化和目标代码生成及优化。以中间代码为分水岭,前端与机器无关、后端与机器有关——所以同一语言只需开发一个前端,针对不同机器开发后端即可。
词法分析基础:词法规则可用 3 型文法(正规文法)或正规表达式描述,其产生的字符串集合称为正规集。正规式的运算符:|(或)、连接(·,可省略)、*(闭包),优先级从高到低为闭包 > 连接 > 或。两个正规式表示的正规集相同则等价。手写正规式要熟悉基本套路:a(a|b)* 表示以 a 为首字符的串,(a|b)*abb 表示以 abb 结尾的串。
有限自动机是识别正规集的装置:
- 确定的有限自动机 DFA:五元组 (S, Σ, f, S0, Z),f 是 S×Σ→S 的单值部分映像——每个状态对每个输入字符的后继状态唯一。可用状态转换图和状态转换矩阵表示。
- 不确定的有限自动机 NFA:f 是 S×Σ→2^S 的映像(后继状态是一个集合),有向弧上的标记可以是 ε。
- DFA 是 NFA 的特例;对每个 NFA M 都存在一个 DFA N,使得 L(M)=L(N)。
- 识别:从初态到某终态的路径上弧标记连成的串等于输入串,则该串可被识别(接受)。若初态同时是终态,则空串 ε 可被识别。
「考点」给一个状态转换图(如识别 "ababaa" 的 DFA),问某字符串能否被识别——要在纸上按图走一遍路径,注意"串结束但未到终态"和"半路无匹配弧"都算不能识别。
语法分析:程序设计语言的语法常采用上下文无关文法描述。文法是产生式系统,如 E→E+T | T,T→T*F | F,F→(E) | N——从该文法可看出"乘优先级高于加、运算符写在中间、可加括号",并能对 2+3*4 做出推导(分析树/语法树)。
解释器基础:解释程序在词法、语法、语义分析方面与编译程序原理基本相同,区别是不生成目标程序,直接执行源程序或其内部形式。实现方式:A——源程序被直接解释执行(逐字符扫描,效率很低);B/C——先转成中间代码再解释,中间代码常采用逆波兰表示,方式 C 的中间代码更接近机器语言(高级语言与低级中间代码呈 1:n 对应),典型实例是 PASCAL 的 P-代码解释系统,可移植性好。
「考点」编译 vs 解释的三方面比较:效率——编译方式更高(源程序只需编译一次可多次运行;解释方式可能反复扫描源程序、反复做类型检查,还占更多内存);灵活性——解释方式更高(可"在运行中"修改程序、更精确定位错误);可移植性——解释方式更高(装好解释器,源程序无需修改即可在新机器运行)。Java 虚拟机的 just-in-time(即时编译)技术:代码第一次运行时编译,其后不再编译。
6.2 汇编语言程序设计
汇编语言(Assembly Language)用助记符代替操作码、用标号(Symbol)代替地址,也称符号语言;汇编程序(汇编器)把它翻译成机器指令,该过程称为汇编。特点三条:机器相关性(每种处理器有专属语法和汇编器)、高速度和高效率(占用内存少、执行快)、编写和调试的复杂性(直接控制硬件,面面俱到,调试困难)。驱动程序、嵌入式操作系统和实时运行程序都需要汇编。
汇编源程序的三类语句:
- 指令语句:汇编后产生机器代码,被 CPU 直接执行,如 MOV、ADD、SUB、AND;可分为传送、算术运算、逻辑运算、移位、转移、处理器控制等指令。
- 伪指令语句:指示汇编程序在汇编时完成某些工作(分配存储单元、给符号赋值),经汇编后不产生机器代码——这是与指令语句的关键区别(指令语句的操作在程序运行时完成)。常设五类:常数定义(ARM 中
x EQU 50)、存储定义(ARM 中str DCB "this is a test"分配并初始化字节单元)、汇编控制(IF/ELSE/ENDIF、WHILE/WEND)、开始(ARM 中ENTRY指定入口点)、结束(END通知源文件结束)。 - 宏指令语句:把重复使用的程序段定义为宏,宏指令语句就是宏的引用。
每条汇编指令语句由 4 个区组成:标号区、操作码区、操作数区、注释区。标号代表该指令的内存单元地址。
ARM 汇编:以段(section)为单位组织源文件,段分代码段(只读)和数据段(可读写);源程序至少一个代码段。汇编后生成可执行映像文件,含:一个或多个只读代码段、零或多个含初始值的数据段、零或多个不含初始值(初始化为 0)的数据段。AREA 段名 CODE, READONLY 定义段。子程序调用用 BL 指令:它把返回地址保存到 LR 寄存器,再把 PC 设为子程序首条指令地址;返回时把 LR 送 PC 即可。子程序调用通常用寄存器 r0~r3 传递参数和返回结果。
原书还给出一个 x86 汇编驱动 8255 控制器进行 A/D 采集的完整例子(配置工作模式控制字、用 C 口 PC0/PC7 控制并查询 AD574 的 STS 状态位、从 A 口 B 口读取 12 位数据并合并),展示了"初始化外设—位控输出—查询等待—读数合并"的典型底层编程流程,读懂思路即可。
6.3 C 程序设计基础
C 是面向过程的结构化语言,兼具高级和低级语言特点,是嵌入式开发最普遍的语言;国家标准 GB/T 28169-2011 是应用 C 语言进行嵌入式软件开发的编码规范。
6.3.1 C 程序基础:
- 预处理指令:以 # 开头,编译前由预处理器处理。常用:
#include(文件包含)、#define(宏定义)、#ifdef/#ifndef(条件编译)。宏定义可提高可移植性、可读性,嵌入式中也常用宏代替函数以提高性能,如#define MIN(A,B) ((A)<(B)?(A):(B))。预定义宏(名称以两个下画线开头结尾,不能被 #undef 或重定义):__DATE__、__TIME__、__FILE__、__FUNC__、__LINE__、__STDC__、__STDC_HOSTED__。条件编译#ifdef 标识符 … #else … #endif:标识符已定义过则编译程序段 1,否则编译程序段 2。#pragma最复杂,是机器/操作系统专有的编译指示;#pragma pack(n)表示变量以 n 字节对齐,网络程序常用#pragma pack(1)紧缩以减少流量并兼容各系统。 - 基本数据类型:char(-128~127)、unsigned char(0~255)、short int、int/unsigned int(与机器有关)、long int、float(3.4E±38,7 位有效数字)、double(1.7E±308,15 位有效数字)、long double。void 不对应具体的值,只用于特定场合。
- 变量与声明:一个变量有且仅有一个定义;引用其他文件中定义的变量用
extern声明(声明不分配存储空间)。volatile:告知编译器该变量的值无任何持久性、不要优化——因为变量可能在程序外被改变(如硬件寄存器),必须从内存位置或设备端口重新读取,不能使用寄存器中的缓存值。这是嵌入式 C 的高频考点。 - 字面量:整型默认十进制,前缀 0 表示八进制、0x/0X 表示十六进制(
x=055是八进制 45 即十进制 37!);U/u 后缀指定 unsigned。浮点字面量默认 double,F/f 后缀为 float,L/l 为 long double。字符字面量用单引号,转义字符如\n换行、\r回车。字符串用双引号,编译系统自动在结尾加\0。 - const 常量 vs 宏定义常量:const 常量有数据类型,编译器可做类型安全检查,有些调试工具还能对其调试;宏定义常量没有数据类型,只做字符替换、不进行类型安全检查,替换可能产生意料不到的错误。
- 标识符与作用域:以字母或下画线开头,区分大小写,C89 规定 31 字符以内、C99 规定 63 字符以内;关键字不能作标识符。作用域分块作用域、函数作用域、函数原型作用域、文件作用域,可嵌套;变量应定义在尽可能小的作用域内并设初值。
- 数组:下标从 0 开始,n 个元素最后一个是
[n-1],访问越界可能严重错误。数组维数和大小在定义时确定、运行时不能改变。初始化表个数须 ≤ 元素个数;int primes[10]={1,2,3,5,7};后 5 个元素自动为 0。二维数组按行顺序存放。用 sizeof 可计算数组字节数。 - 字符数组与字符串:字符串以
'\0'结尾,n 个字符的字符串至少需要长度 n+1 的字符数组。"A"与'A'不同(前者是含 '\0' 的两个字符)。最短字符串""只含一个结尾符。 - 枚举:枚举成员是常量;未赋值时从 0 依次加 1;某成员赋值后,其后成员依次加 1,如
enum Color{eRED=5,eBLUE,eYELLOW,Egreen=30,Esilvergrey=40,eBurgundy};则 eBLUE=6、eYELLOW=7、eBurgundy=41。 - 结构体:把不同数据项聚合为整体;成员运算符
.访问成员;结构体变量存储空间不少于所有成员所占空间之和,且因对齐可能产生填充——成员相同但排列顺序不同,sizeof 结果可能不同(原书例子:char+int 顺序的 structA 为 8 字节,char+short+int 顺序的 structB 为 12 字节)。 - 位域:把一个字节中的二进位划分为几个区域,格式为
类型说明符 位域名: 位域长度;,如struct bs{int a:1; int :2; int b:3; int c:2;};(中间的无名域 2 位不使用)。 - 共用体:所有成员占用同一段内存,变量存储空间大小等于占用空间最大的成员的大小;不能直接引用共用体变量,只能引用其成员。
- 运算符与表达式:自增自减——前缀
++value先增 1 再取新值参与运算,后缀value++先取值参与运算再增 1;只能作用于变量。关系运算结果为 1 或 0;不能用关系运算符比较字符串内容("HELLO"<"BYE"比较的是地址)。逻辑运算短路求值:a&&b中 a 为假就不再算 b;a||b中 a 为真就不再算 b。条件运算符?:是 C 中唯一的三目运算符。逗号表达式的值是最后一个表达式的值。位运算 6 种:~(取反)、&(与)、|(或)、^(异或)、<<(左移)、>>(右移),操作数必须是整型,还可组合成 &=、|=、^=、<<=、>>=。sizeof 计算表达式或类型的字节数(与系统相关)。类型转换:系统自动进行类型提升(int+long→long,float+double→double);强制转换(类型名)(表达式)只得到所需类型的中间变量,原变量类型不变;(int)(x+y)与(int)x+y含义不同。 - 语句:if 的嵌套中,else 总是与离它最近且没有 else 匹配的 if 配对。switch 的表达式值必须是字符型或整型;case 匹配成功后顺序执行直到 break,否则贯穿到底(编译器通常构造跳转表快速定位,而不是逐一比较)。break 在多重循环中只终止一重循环;continue 结束本次循环转下一次;return 仅用于从函数返回。
6.3.2 函数:标准库函数随编译系统发布;调用前必须声明。C 程序参数传递是值传递(地址也是一种值):调用时才为形参分配单元并复制实参的值,修改形参不影响实参。数组作为参数是引用传递方式——只传数组首地址(且必须同时把元素个数告诉被调函数),形参数组元素被修改时实参数组同步改变,优点是避免复制每个元素、节省内存和时间。递归函数两个基本要素:边界条件(递归出口)和递归模式(递归体);递归比非递归需要更多执行时间和内存空间。
6.3.3 存储管理:
- 内存布局(Linux 进程为例):正文段(代码和只读数据,只读、可共享,常执行程序只需一个副本)→ 数据区(已初始化的全局变量和静态局部变量;未初始化部分称 BSS 段,程序开始执行前内核将其初始化为 0)→ 堆(动态存储区,由程序员 malloc/calloc/realloc 申请、free 释放,不释放则程序结束时由操作系统回收;向高地址增长;分配需要系统维护空闲链表、可能产生内存碎片和用户态/核心态转换)→ 栈(局部变量和函数调用信息,向低地址增长,由操作系统管理)。内存受限系统应尽量避免动态内存分配,多采用静态分配。
- 大端模式与小端模式:大端(Big-endian)——高位字节存低地址端;小端(Little-endian)——低位字节存低地址端。0x12345678 从 0x4000 存起:大端依次存 12 34 56 78,小端依次存 78 56 34 12。常用 CPU 中 PowerPC、IBM、Sun、KEIL C51 采用大端,x86、DEC 采用小端,很多 ARM、DSP 为小端(有些 ARM 可由硬件选择)。Java 和所有网络通信协议使用大端模式。判断方法例:int a=0x12345678 强转 char* 后看首字节,是 0x12 则大端、0x78 则小端。
6.3.4 指针(C 的精华,下午题核心):
- 指针是内存单元的地址;存储地址的变量称指针变量。全局指针变量自动初始化为 NULL,局部指针变量初值随机——未初始化的指针可能导致内存访问错误,这是常见错误来源。
&是地址运算符,*是间接运算符。void* 可与任意类型匹配,使用前必须转换为明确类型。 - 指针加减运算以所指类型存储宽度为单位:
p+1对 int 是p+1*sizeof(int),对 char 是p+1*sizeof(char)。 - 指针与堆:malloc/calloc/realloc 申请、free 释放;释放空指针没有意义。内存泄漏:程序中存在未释放(且地址丢失、无法再访问)的内存块,持续泄漏导致性能降低甚至崩溃;嵌入式存储空间有限,应尽量采用静态分配。
- 指针与数组:数组名是常量指针(值不能改变,
arr++错误,指针变量ptr++允许);int *ptr = arr;则元素可用*ptr、*(ptr+1)…引用。二维数组a[i][j]概念地址为a+(i×n+j)*sizeof(int);指向"含 3 个 int 的一维数组"的指针是int (*ptr)[3],其 +1 跨一行。 - 常量指针与指针常量:
const int *p(const 在 * 左边)是常量指针——指向的对象是常量,*p=5错误但 p 可改指向;int * const p(const 在 * 右边)是指针常量——指针本身是常量,p 不能再指向其他对象,但*p=5可以。 - 指针与函数:指针作参数可修改实参(swap(&x,&y));在被调函数中修改指针参数本身的值不能影响实参——要用
char **p二级指针传&ptr才能让调用者的指针指向新申请的内存。const 修饰参数可防止不当修改(strcpy(char *to, const char *from))。不能把局部变量的地址作为返回值——函数返回后局部变量存储空间即被释放。 - 函数指针:保存函数入口地址,如
int (*Compare)(const char*, const char*) = strcmp;,三种调用等价:strcmp("Tom","Tim")、(*Compare)("Tom","Tim")、Compare("Tom","Tim")。赋值时参数和返回类型必须匹配。 - 链表:结点
typedef struct node{int data; struct node *next;}Node, *LinkList;;p 指向结点时,p->next 指向下一个结点,p->next=p使结点指向自己。原书例题 DeleteList 演示了在有序单链表中删除 (minK, maxK) 区间元素的写法:用 q、p 两个一前一后的指针,q 始终指向 p 的前驱,删除 p 结点时先q->next=p->next再free(p),这是链表删除题的标准套路(下午题考过多次)。
6.3.5 栈和队列:
- 栈:一端存取,先进后出(FILO)/后进先出,操作端叫栈顶(top),另一端栈底(bottom)。基本运算:初始化、判空、push 入栈、pop 出栈、top 读栈顶(不修改栈顶指针)。数组实现的顺序栈要判断栈满,栈满入栈发生上溢。链栈不必另设头指针——链表头指针就是栈顶指针。经典应用:后缀表达式求值——从左到右扫描,遇运算对象压栈,遇运算符弹出运算对象计算再把结果压栈(注意弹出顺序:先弹的是第二个操作数)。例:
46+5*(120-37)的后缀式为46 5 120 37 - * +。 - 队列:先进先出(FIFO),插入端叫队尾(rear),删除端叫队头(front)。顺序队列入队只改尾指针、出队只改头指针。循环队列(通过取余运算构成环状):入队
Q.rear=(Q.rear+1)%MAXSIZE,出队Q.front=(Q.front+1)%MAXSIZE。队空与队满时都有 Q.rear==Q.front,无法据此区分状态,两种解决办法:①设标志域;②牺牲一个元素空间,约定"尾指针所指位置的下一个位置是头指针"表示队满((rear+1)%MAXSIZE == front即满),头尾指针相等表示队空。
6.3.6 C 程序内嵌汇编:C 语言与硬件之间靠编译器联系,某些 C 标准不支持的硬件特性操作可由编译器提供;必要时用 asm volatile 在 C 中内嵌汇编,通过占位符(%0、%1)和约束("r" 让编译器自动关联通用寄存器,"=a"、"=b" 指明具体寄存器)实现 C 变量与汇编的交互。
6.4 C++ 程序设计基础
C++ 是 C 的扩展和超集,同时支持过程式和面向对象程序设计泛型;优势是执行效率,适用于系统软件、工具软件、游戏引擎,通信和图像处理领域广泛应用。
6.4.1 面向对象基本概念(概念定义题高频):
- 对象:基本的运行时实体,既包括数据(属性)也包括作用于数据的操作(行为),把属性和行为封装为一个整体。封装是信息隐蔽技术,目的是使对象的使用者和生产者分离、定义和实现分开。对象由对象名、属性、方法(操作)三部分组成。
- 消息:对象之间进行通信的构造;发送方不需要知道接收方如何响应。
- 类:定义一组大体上相似的对象;类是对象之上的抽象,对象是类的具体化(实例)。类之间"一般-特殊"关系是 is-a 关系(汽车是一种交通工具),特殊类是子类、一般类是父类。
- 继承:父类和子类之间共享数据和方法的机制;一个子类一个父类叫单重继承,两个及以上父类叫多重继承。
- 多态:不同对象收到同一消息可产生完全不同的结果。Cardelli 和 Wegner 把多态分为 4 类:参数多态、包含多态、过载多态、强制多态;参数多态和包含多态称为通用多态(参数多态应用广泛,被称为最纯的多态),过载多态和强制多态称为特定多态;包含多态最常见的例子是子类型化;过载多态指同一名字在不同上下文中代表不同含义。
- 动态绑定:绑定是把过程调用和响应调用的代码结合的过程;编译时进行叫静态绑定,运行时进行叫动态绑定——同一消息可调用不同方法,与继承和多态相联系。
- 面向对象五大原则:①单一责任原则(SRP)——一个类只做一种类型的责任,修改一个类的原因有且只有一个;②开关原则(OCP)——对扩展开放、对修改封闭;③里氏替换原则(LSP)——任何父类可以出现的地方都可以用子类实例替换;④依赖倒置原则(DIP)——高层与低层模块都依赖于抽象,抽象不依赖细节(注:原书此处条目文字与英文名对应有排版错位,按标准表述记忆:ISP 接口分离原则——使用多个专门的接口比单一总接口好;DIP 依赖倒置原则——依赖于抽象而不是具体);⑤接口分离。
6.4.2 C++ 程序基础:
- 数据类型:强类型语言;在 C 基础上扩展了 bool(true/false)和 wchar_t(宽字符,2 字节)、char16_t(2 字节)、char32_t(4 字节)。
- 输入/输出:C 主要用 stdio 库,C++ 主要用 iostream 流库(
#include <iostream>提供基本 I/O,#include <iomanip>提供格式化);标准输出流 cout(屏幕)、标准输入流 cin(键盘)。 - 引用:为对象起别名,如
double &num2 = num1;,num2 与 num1 绑定表示同一对象。引用与指针的三点不同:①不存在空引用(必须连接到合法对象);②引用一旦初始化就不能再引用另一个对象(指针可以改指向);③引用必须在创建时初始化(指针可以不初始化)。 - 内联函数:函数定义前加 inline,编译器把函数体直接放在调用处,省去普通函数调用时栈记录的创建和释放开销。注意:内联函数应简洁;函数体中不能有循环语句、if 或 switch 语句,否则即使写了 inline 也按非内联处理;要被调用前声明;通常放头文件中共享。
- 函数重载:一组功能相似的函数可同名,编译器根据参数个数或类型区分;形参变量名不同不能作为重载依据;不允许函数名相同、参数个数类型也相同而仅返回值不同;实参类型不匹配时编译器会自动转换,转换后仍匹配不到则报错。
6.4.3 类与对象:
- 类定义:
class 类名{ public: …; private: …; };,类定义结尾必须有分号。成员分数据成员和成员函数。三种访问权限:public(类外可访问)、private(只能被本类成员函数访问)、protected(能被本类和派生类的成员函数访问)。数据成员通常私有;公有成员函数是类的接口。不指定访问说明符时默认为 private。成员函数可在类外定义:返回类型 类名::函数名(形参表){…},::是域运算符。struct 和 union 中也可定义函数,其成员默认访问控制为 public(class 默认 private)。 - 对象是类的实例(占内存单元),类不占内存。对象分全局对象、局部对象、成员对象(随包含它的对象创建而创建、消亡而消亡)和动态对象(new 创建于堆区,用 delete 撤销)。访问成员:
对象名.成员函数名(实参表)或指向对象的指针->成员函数名(实参表)。 - 构造函数:名字与类名相同,没有返回类型和返回值,对象创建时自动调用;对象创建之后就不能再调用构造函数(
sl.stack();是错误的)。构造函数可以重载;不带参数(或所有参数都有默认值)的构造函数称默认构造函数。常量数据成员和引用数据成员不能在声明时初始化、也不能用赋值初始化,必须在构造函数的成员初始化表(函数头和函数体之间,如A():z(x),y(10){x=0;})中初始化。 - 析构函数:类名前加 ~,没有返回类型和返回值,不带参数、不能重载,对象销毁时自动调用做清理(如 delete 指针成员指向的内存块)。调用时机:全局对象在程序运行结束时、自动对象在离开其作用域时、动态对象在用 delete 运算符时销毁。析构函数的调用顺序与构造函数相反。用户未显式定义时,编译器会隐式生成内联的公有的构造/析构函数,但默认构造函数不涉及用户定义的数据成员或申请内存的初始化。
- 静态成员:static 数据成员仅创建和初始化一次,程序开始执行时创建,被该类所有对象共享——它属于类而不属于对象。static 成员函数属于类,其中仅能访问静态数据成员,不能访问非静态数据成员,也不能调用非静态成员函数;类外调用方式为
类名::成员函数名(实参表)。 - this 指针:指向调用该成员函数的对象,是隐含指针,不能被显式声明;存在于类的非静态成员函数中。
a.g(5)实际被编译成A::g(&a,5)。 - 友元:在类定义中用 friend 指定的全局函数(友元函数)、其他类(友元类)或其他类的成员函数(友元类成员函数)可以直接访问该类的 private 和 protected 成员。友元是数据保护和数据存取效率之间的一种折衷,提高了程序设计的灵活性。
6.4.4 继承与多态:
- 继承形式:
class 派生类名: 访问权限 基类名{…}。派生类自动获得除基类私有成员以外的所有数据成员和成员函数。构造顺序:先调用基类构造函数(在派生类构造函数的初始化表中调用),再调用派生类自己的构造函数;析构顺序正好相反。若派生类构造函数未显式调用基类构造函数,编译器会先调用基类的默认构造函数。 - 访问说明符对基类成员在派生类中的影响:public 继承——基类公有成员仍为派生类公有成员、保护成员仍为保护成员;protected 继承——基类公有和保护成员均变为派生类的保护成员;private 继承——基类公有和保护成员均变为派生类的私有成员。
- 重置(覆盖)与实现:派生类定义与基类同名同签名同返回类型的函数:对普通成员函数称为重置(覆盖),对虚成员函数称为实现。
- 虚函数与动态绑定:C++ 中动态绑定通过虚函数实现——成员函数原型前加 virtual。基类中定义为虚函数的成员,在所有派生类中也保持为虚函数(派生类中可省略 virtual)。要达到动态绑定效果,基类和派生类的对应函数名字、返回类型、参数个数和类型都必须相同。机制:普通函数调用在编译时绑定(早期绑定/静态绑定),基类指针指向派生类对象时,通过虚函数在运行时根据指针实际指向的对象类型决定调用哪个版本的函数。
- 纯虚函数与抽象类:纯虚函数是在虚函数参数表后加 =0:
virtual void method(void)=0;。含有纯虚函数的类称为抽象类,C++ 不允许用抽象类创建对象,它只能被继承,实际起接口作用。原书例:抽象类 Figure 提供纯虚函数 getArea(),Triangle、Rectangle、Square 派生并各自实现,用Figure *figures[]数组循环调用figures[i]->getArea()计算不同图形面积——这是动态绑定的标准演示。 - 虚函数的限制:只有类的成员函数才可以是虚函数;静态成员函数不能是虚函数;构造函数不能是虚函数;析构函数可以是虚函数,而且常常将析构函数定义为虚函数。
6.4.5 异常处理:异常是程序运行时刻的不正常情况(new 取不到内存、除数为 0、运算溢出、数组越界、参数无效等),要求程序立即处理。传统处理方式(终止程序、返回错误值、返回合法值使程序处于非法状态、调用错误处理回调)各有缺陷。C++ 异常机制把错误和处理分开:库函数抛出异常,调用者捕获处理。关键字:try(放可能产生异常的工作代码)、catch(异常处理器,紧随 try 之后,第一个参数与抛出对象匹配的处理器捕获该异常)、throw(检测到错误时抛出异常对象;也可在函数声明中用 throw(异常类型列表) 声明函数可能抛出的异常,void f() throw(); 表示不抛出任何异常)。原书例:divide 函数在除数为 0 时 throw,test 函数 try/catch 捕获后提示重新输入,再次失败则由 main 的 catch 兜底提示重新运行程序。
6.4.6 类库:C++ 标准库分标准函数库(继承自 C,由不属于任何类的通用函数组成)和类库。常见类库:STL(C++ 标准模板库,工业强度,提供容器和算法)、Boost(C++ 准标准库,标准库后备)、MFC(微软基础类库,以 C++ 类封装 Windows API 和应用程序框架)。
初学者容易踩的坑 / 易错考点
- 八进制前导 0:
x=055不是 55,而是八进制 45(十进制 37)。浮点字面量默认 double 而不是 float。 - 结构体 sizeof 与对齐:成员相同、顺序不同,sizeof 可能不同(8 字节 vs 12 字节);共用体大小等于最大成员。考 sizeof 计算时要先画内存布局。
- else 配对:else 总是与最近的、尚未匹配的 if 配对,缩进不改变配对关系;switch 忘写 break 会贯穿执行。
- 传值 vs 传址 vs 引用:
swap(int x,int y)换不了实参;要在函数内修改指针变量本身(而不是它指向的内容),必须传二级指针。数组参数是引用传递方式(传首地址)。 - const 与指针:
const int *p(指向常量的指针,p 不可改)与int * const p(指针常量,p 不可改)方向别记反——const 在 * 左边管对象,在 * 右边管指针*。 - 虚函数的限制:构造函数不能是虚函数、静态成员函数不能是虚函数,常作为判断题;抽象类不能实例化,含纯虚函数才叫抽象类。
- 静态成员的归属:static 数据成员属于类、所有对象共享、只初始化一次;static 成员函数不能访问非静态成员。
- 循环队列判满:
front == rear既可能是空也可能是满,必须靠"牺牲一个空间"或标志位区分——直接用相等判断"满"是典型错误。 - 编译阶段错误分类:拼写错误、括号不匹配是语法错误;类型不匹配是静态语义错误;除零、下标越界是动态(语义)错误——三者的归属经常互换成选项。
- 递归调用代价:递归耗更多时间和内存,嵌入式栈小更要注意(呼应第 5 章"禁止递归"的约束)。
小结 & 备考提示
本章是"语言全景图":程序先被词法、语法、语义分析处理成中间代码(四元式),再优化并生成目标代码(前端与机器无关、后端与机器有关);解释器不生成目标程序,灵活但效率低。汇编靠伪指令指导汇编器、指令语句生成机器码。C 的核心是存储管理(栈/堆/BSS、大端小端、volatile)和指针(与数组、函数、链表的组合)。C++ 的核心是封装-继承-多态三件套加虚函数动态绑定。
必须记住的关键词:编译六阶段与前后端 / 正规式与有限自动机(DFA/NFA) / 传值调用与引用 / 大端小端与 volatile / 虚函数-纯虚函数-抽象类。