设计模式课程 第2讲 — C++ 语言基础

课程:北京大学软件研究所 王亚沙 《0C107 设计模式 讲次:第2讲(ss-02) 主题:C++ 语言简介 —— 从 C 到 C++ 性质:设计模式课程前置知识,C++ 核心特性完整梳理

内容提要

本讲是设计模式课程的 C++ 语言基础铺垫,以”从 C 到 C++“为主线,系统讲解面向对象编程三大特征在 C++ 中的实现机制。以一个 C 语言实现的 Stash 动态数组库为起点,逐步用 C++ 特性重构,展现语言演进过程中设计思想的变化。


一、软件开发范型

  1. 面向功能(结构化开发):功能分解 + 数据流图
  2. 面向对象(OOP):对象分析 + 消息连接
  3. 基于数据:实体/关系图(ER图),MIS 类管理系统
  4. 基于构件: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 问题:

  1. 值替代:const int a = 1; 编译时常量
  2. 指针:
    • const int* p:指向常量的指针(不能改值)
    • int* const p:常量指针(不能改指向)
    • const int* const p:完全 const
  3. 函数参数和返回值:保护实参不被修改
  4. **传递和返回引用/返回引用
  5. 类中的 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)

引用是自动逆向引用的常量型指针。

  • 引用看作变量的别名。

**引用的应用:

  1. 函数参数传递(避免拷贝)
  2. 函数返回值(返回左值)
  3. 运算符重载(连续赋值 `a = b = c)

引用 vs 指针:

特性引用指针
初始化必须初始化,绑定后不可改可随时改指向
空值不能引用空可以为 NULL
使用方式直接用变量名需解引用 *
级数只有一级可以多级指针

3.5 拷贝构造函数

用一个已有对象初始化新对象:

X(const X& x);  // 参数是同类对象的 const 引用

三种调用时机:

  1. 用一个对象初始化另一个对象
  2. 对象作为函数参数传入(值传递)
  3. 函数返回对象(值返回)

浅拷贝 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 → protected
  • protected 继承:基类 public/protected → 派生类 protected
  • private 继承:基类全部 → 派生类 private

4.3 向上映射(upcasting)

派生类指针/引用 → 基类指针/引用:

向上映射总是安全的。

Shape* sp = new Circle;  // Circle 是 Shape 的子类
  • 自动隐式转换,无需强制类型转换。

4.4 构造与析构顺序

  • 构造:先基类 → 再成员对象 → 再派生类
  • 析构:先派生类 → 再成员对象 → 再基类

五、多态 —— 从 C 到 C++ 的第三步

5.1 虚函数(virtual function)

动态绑定的关键:

virtual void print();

**实现机制(虚表 VTABLE):

  1. 设置虚表:每个含虚函数的类有一张虚表,存放所有虚函数地址
  2. vptr 指针:每个对象有一个隐藏的 vptr,指向本类虚表
  3. 动态绑定:通过 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)
  • 设计思想重于语法细节

关联笔记