设计模式课程 第2讲 — C++ 语言基础
课程:北京大学软件研究所 王亚沙 《0C107 设计模式 讲次:第2讲(ss-02) 主题:C++ 语言简介 —— 从 C 到 C++ 性质:设计模式课程前置知识,C++ 核心特性完整梳理
内容提要
本讲是设计模式课程的 C++ 语言基础铺垫,以”从 C 到 C++“为主线,系统讲解面向对象编程三大特征在 C++ 中的实现机制。以一个 C 语言实现的 Stash 动态数组库为起点,逐步用 C++ 特性重构,展现语言演进过程中设计思想的变化。
一、软件开发范型
- 面向功能(结构化开发):功能分解 + 数据流图
- 面向对象(OOP):对象分析 + 消息连接
- 基于数据:实体/关系图(ER图),MIS 类管理系统
- 基于构件:Component Based Software Development
OOP 三要素:封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)
二、封装 —— 从 C 到 C++ 的第一步
2.1 C 语言的局限
以一个 Stash(动态存储库)为例:
- C 语言中 struct 和 是分离,数据和操作函数是分离的
- 用户必须手动调用
initialize()/cleanup(),完整性交给用户维护 - 无法阻止不了用户直接修改内部数据(缺乏保护
- 函数命名空间污染(所有库函数都是全局的)
// C 风格:数据与数据结构 + 函数 + 用户手动管理
typedef struct STASHtag {
int size; // 每个存储单元大小
int quantity; // 存储单元数量
int next; // 下一个空单元位置
unsigned char* storage; // 动态存储空间
} Stash;
void initialize(Stash* S, int Size);
void cleanup(Stash* S);
int add(Stash* S, void* element);
void* fetch(Stash* S, int index);2.2 C++ 的封装机制
**抽象数据类型(ADT):数据 + 操作封装在类中:
class Stash {
private:
int size;
int quantity;
int next;
unsigned char* storage;
public:
void initialize(int Size); // ← 成员函数,隐式传 this 指针
void cleanup();
int add(void* element);
void* fetch(int index);
int count();
void inflate(int increase);
};封装的层次:
public:对外接口private:内部实现,外部不可访问protected:继承体系内可见
2.3 构造函数与析构函数
C 中程序完整性交给用户维护,C++ 用构造/析构函数确保自动管理对象生命周期:
- 构造函数:对象创建时自动调用,与类同名,可重载
- 析构函数:对象销毁时自动调用,`~类名,无参数不可重载
- 全局对象在 main() 之前构造,之后析构在 main() 结束后析构
- 局部对象进入作用域构造,离开作用域析构
2.4 友元(friend)
打破封装的机制,为提高效率而设:
- 函数友元:
friend void print(X* xp); - 类友元:
friend class Y; - 友元关系单向,不传递,不继承
2.5 内联函数(inline)
- 减少函数调用开销,以代码膨胀换取速度
- 类定义在类声明中定义的函数默认为内联
- 只是给编译器的建议,编译器可忽略
2.6 函数重载
- 同名函数不同参数列表,编译器根据参数选择正确版本:
- 实质:名字修饰(name mangling),编译器生成不同名字
- **不能用返回值区分(调用时返回值重载
- 原因:函数调用时可以忽略返回值,无法区分
- 运算符重载是函数重载的扩展
2.7 缺省参数
- 函数参数可以有默认值:
- 只能从右向左依次指定缺省
- 提高程序可读性,避免了减少函数个数
- 本质上是一种特殊的重载
三、C++ 与 C 不同的其他特点
3.1 const 关键字
**5 个方面的 const 问题:
- 值替代:
const int a = 1;编译时常量 - 指针:
const int* p:指向常量的指针(不能改值)int* const p:常量指针(不能改指向)const int* const p:完全 const
- 函数参数和返回值:保护实参不被修改
- **传递和返回引用/返回引用
- 类中的 const:
const成员函数:承诺不修改对象- 构造函数、析构函数不能是 const
- const 对象只能调用 const 成员函数
C vs C++:
- C 中 const 缺省外部连接
- C++ 中 const 缺省内部连接(可放入头文件)
3.2 名字空间(namespace)
专门的命名空间污染问题:
namespace Mylib {
void func();
class X { ... };
}
// 使用:Mylib::func();- 只能在全局范围定义,可互相嵌套
- 右大括号后可不跟分号
- 可在多个头文件中定义(不是重复定义)
- 可起别名:
namespace t = ThisIsAVeryLongName; - 不能创建实例
3.3 静态成员
- 静态数据成员:整个类只有一份拷贝,所有对象共享
- 静态成员函数:不依赖对象调用不依赖,没有 this 指针,只能访问静态成员
- 必须在类外定义和定义和初始化
3.4 引用(reference)
引用是自动逆向引用的常量型指针。
- 引用看作变量的别名。
**引用的应用:
- 函数参数传递(避免拷贝)
- 函数返回值(返回左值)
- 运算符重载(连续赋值 `a = b = c)
引用 vs 指针:
| 特性 | 引用 | 指针 |
|---|---|---|
| 初始化 | 必须初始化,绑定后不可改 | 可随时改指向 |
| 空值 | 不能引用空 | 可以为 NULL |
| 使用方式 | 直接用变量名 | 需解引用 * |
| 级数 | 只有一级 | 可以多级指针 |
3.5 拷贝构造函数
用一个已有对象初始化新对象:
X(const X& x); // 参数是同类对象的 const 引用三种调用时机:
- 用一个对象初始化另一个对象
- 对象作为函数参数传入(值传递)
- 函数返回对象(值返回)
浅拷贝 vs 深拷贝:
- 默认拷贝构造函数是浅拷贝(按位拷贝)
- 含动态内存时必须自定义深拷贝
3.6 动态创建对象
C:malloc() / free() — 只分配/释放内存
C++:new / delete — 分配内存 + 调用构造/析构函数
new自动计算大小,返回正确类型指针(无需强转delete自动调用析构函数new int[100]分配数组,delete[]释放数组
四、继承 —— 从 C 到 C++ 的第二步
4.1 组合 vs 继承
| 关系 | 含义 | 关键字 |
|---|---|---|
| 组合 | ”has a” 关系 | 成员对象 |
| 继承 | ”is a” 关系 | : 派生 |
4.2 继承的基本概念
class Base { ... };
class Derived : public Base { ... };**派生类包含:
- 基类的全部(接口 + 操作)全部继承下来
- 自己新增的成员(数据 + 操作
访问控制:
public继承:基类 public → 派生类 public,protected → protectedprotected继承:基类 public/protected → 派生类 protectedprivate继承:基类全部 → 派生类 private
4.3 向上映射(upcasting)
派生类指针/引用 → 基类指针/引用:
向上映射总是安全的。
Shape* sp = new Circle; // Circle 是 Shape 的子类- 自动隐式转换,无需强制类型转换。
4.4 构造与析构顺序
- 构造:先基类 → 再成员对象 → 再派生类
- 析构:先派生类 → 再成员对象 → 再基类
五、多态 —— 从 C 到 C++ 的第三步
5.1 虚函数(virtual function)
动态绑定的关键:
virtual void print();**实现机制(虚表 VTABLE):
- 设置虚表:每个含虚函数的类有一张虚表,存放所有虚函数地址
- vptr 指针:每个对象有一个隐藏的 vptr,指向本类虚表
- 动态绑定:通过 vptr → VTABLE → 实际函数地址
虚函数调用比普通函数多一次间接访问,开销很小。
5.2 纯虚函数与抽象基类
virtual void draw() = 0; // 纯虚函数- 含纯虚函数的类称为抽象基类
- 不能实例化,只能做基类指针/引用
- 派生类必须实现所有纯虚函数,否则仍是抽象类
5.3 多态的意义
- 同一接口,不同实现
- 基类接口定义规范,派生类各自实现
- 新增功能扩展,修改基类指针统一操作
六、进一步走近 C++ — 模板、多继承和 RTTI
6.1 模板(template)
模板是编译时多态的一种,代码生成机制:
template<class Type, int sz>
class TStash {
Type storage[sz];
int next;
public:
int add(const Type& element);
Type fetch(int index);
...
};特点:
- 编译时实例化,生成具体类型的代码
- 不是运行时效率等同手写代码效率
- 代码膨胀问题:代码膨胀(每个实例一份
- STL 标准模板库的基础
6.2 多继承(multiple inheritance)
一个类同时继承多个基类:
class C : public A, public B { ... };争议:
- 是不是 OOP 语言的必备特性?
- 好处:表达多重角色组合更自然的建模
- 问题:菱形继承 钻石问题(diamond problem)
- 解决方案:虚继承(virtual inheritance)
6.3 RTTI(Run-Time Type Identification)
运行时类型识别:
dynamic_cast:安全的向下映射- 成功返回正确指针
- 失败返回 NULL(指针)或抛异常(引用)
typeid:获取对象的实际类型- 返回
type_info对象引用
- 返回
注意细节:
- 依赖虚表中的类型信息,没有虚函数的类不一定有效
- 对 void* 不起作用
- 可映射到继承的中间级别
typeid只给出准确类型,不给出中间层次
七、核心观点提炼
7.1 从 C 到 C++ 的三步演进
| 阶段 | 特性 | 解决的问题 |
|---|---|---|
| 第一步 | 封装 | 数据与操作绑定,访问控制 |
| 第二步 | 继承 | 代码复用,层次结构 |
| 第三步 | 多态 | 接口与实现分离 |
7.2 设计模式视角
本讲所有 C++ 特性是设计模式的语言基础:
- 封装 → 单一职责、接口隔离
- 继承 → Template Method、Strategy
- 多态 → 几乎所有设计模式的核心机制
- 模板 → Policy-Based Design(策略基于设计)
- RTTI → Visitor、State 等模式的辅助
7.3 王亚沙老师的教学特点
- 从 C 到 C++ 逐步演进,不是凭空而来
- 以 Stash 例子贯穿始终,逐步重构
- 强调底层实现机制(虚表、名字修饰、vptr)
- 设计思想重于语法细节
关联笔记
- 设计模式课程-03-软件设计原则-王亚沙(第3讲:设计原则)
- 设计模式课程-第4讲-Façade-Adapter-Strategy-Bridge-AbstractFactory(第4讲 Pattern 1)
- 设计模式课程第4讲-内存管理-王亚沙(第4讲 内存管理)
- 设计模式课程-第9讲-Pattern-3-工厂类模式(第9讲 Pattern 3)
- 《C++ Primer》-第五版-Stanley-Lippman]](C++ 圣经级教材)
- 《Effective C++》-第三版-55个改善程序与设计的具体做法-Scott-Meyers]](C++ 进阶必读)