文章

程序设计范式-Week1

程序设计范式-第1周小结

程序设计范式-Week1

成绩组成

Examination:

  • activity in class & class performance 10%
  • Exercise 25%
  • Exam 20%
  • Project 45%

序曲

High-level Languages 高级语言

two groups:

  • Procedural languages - 面向过程 - 过程式编程像是用一整块木头雕刻,一步步地切削。
  • Object-oriented languages (OOP) - 面向对象 - 面向对象编程则是先用乐高积木块(对象)组装成轮子、车门、车窗等部件,然后再把这些部件组合成一辆完整的汽车。

面向过程

早期的高级语言通常称为过程语言。 过程语言的特点是连续的线性命令集。此类语言的重点是结构。 包括 C、COBOL、Fortran、LISP、Perl、HTML、VBScript 等。

面向对象(OOP)

大多数面向对象的语言都是高级语言。 OOP 语言的重点不是结构,而是建模数据。 程序员使用称为类的数据模型的“蓝图”进行编码。 OOP 语言的示例包括 C++、Visual Basic.NET 和 Java 等。 使用类(Class)和对象(Object)来描述数据。

进程和线程

比喻:工厂 vs. 车间工人

  • 进程就是一个工厂。工厂有自己独立的资源:比如自己的厂房、仓库、电力系统、原材料(这些相当于系统分配的内存、文件句柄、CPU时间等资源)。工厂和工厂之间是相互独立的。

  • 线程就是工厂里的工人。一个工厂(进程)可以有很多工人(线程)。所有工人共享工厂的资源(厂房、仓库、电力)。工人们协同工作,共同完成工厂的生产任务。

当一个工厂(进程)启动时,它至少会有一个工人,这就是主线程

多线程的意义

既然有了进程,为什么还需要线程?

  1. 更高效的并发:创建和切换线程的代价远小于进程。对于需要同时完成多项任务的程序(如Web服务器同时处理成百上千个请求),使用多线程比创建大量进程要高效得多。

  2. 充分利用多核CPU:一个进程内的多个线程可以被调度到多个CPU核心上真正并行执行,极大地提高了程序的执行效率。例如,一个视频处理软件可以用一个线程解码视频,一个线程处理音频,另一个线程接收用户输入。

  3. 资源共享与通信方便:线程共享内存,使得数据交换非常高效,无需像进程间通信那样需要复杂的内核介入。

C++特性

C++ 语言中最核心、最重要的特性。

Classes (类)

  • 解释类是 C++ 实现面向对象编程的基石。它是一个用户定义的蓝图或模板,用于创建对象。类将数据(称为属性或成员变量) 和对这些数据进行操作的函数(称为方法或成员函数) 捆绑在一起。

  • 简单比喻汽车的设计图纸就是一个,它规定了汽车应有的属性(颜色、品牌、速度)和行为(启动、加速、刹车)。根据这张图纸生产出来的每一辆具体的汽车就是一个对象

  • 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Dog {
private:        // 访问修饰符:私有部分,对外隐藏
    string name; // 成员变量(属性)
    int age;

public:         // 访问修饰符:公共部分,对外接口
    // 成员函数(方法)
    void bark() {
        cout << name << " says: Woof!" << endl;
    }
    void setAge(int newAge) {
        age = newAge;
    }
};

User-defined types (用户定义类型)

  • 解释类(class)、结构体(struct)、联合体(union)、枚举(enum 都属于用户定义类型。这意味着你可以创建像内置类型(如 int, float)一样使用的全新数据类型,但它们的行为和含义完全由你决定。

  • 重要性:这是对问题域进行建模的关键。你可以创建 StudentBankAccountMatrix 等类型,让代码更直观、更易于理解和维护。

  • 示例

    1
    2
    3
    4
    5
    6
    
    class Complex { // 定义了一个表示“复数”的新类型
    public:
    double real;
    double imag;
    };
    Complex a = {1.0, 2.0}; // 像使用 int 一样使用 Complex
    

Operator overloading (运算符重载)

  • 解释允许你为自定义类型赋予运算符(如 +, -, ==, <<)新的含义。这使得用户自定义类型的对象可以像内置类型一样进行直观的运算。

  • 示例

1
2
3
4
5
6
Complex operator+(const Complex& a, const Complex& b) {
    return {a.real + b.real, a.imag + b.imag};
}
Complex a = {1, 2}, b = {3, 4};
Complex c = a + b; // 现在可以直接用 + 号对两个复数相加
// c 变成了 {4, 6}

References (引用)

  • 解释引用是某个已存在变量的别名(alias)。一旦初始化后,对引用的所有操作都是在操作它所引用的原始变量。

  • 主要用途

    1. Pass-by-reference function arguments (按引用传递函数参数):允许函数修改传入的实参的值,同时避免了按值传递时拷贝大对象的开销。

    2. 作为函数的返回值。

  • 示例

1
2
3
4
5
6
7
8
void swap(int& a, int& b) { // a 和 b 是实参的引用
    int temp = a;
    a = b;
    b = temp;
}
int x = 10, y = 20;
swap(x, y); // 传递的是 x 和 y 的引用,函数内部修改会影响外部的 x 和 y
// 现在 x == 20, y == 10

Virtual Functions (虚函数)

  • 解释用于实现运行时多态(Runtime Polymorphism)。在基类中使用 virtual 关键字声明的成员函数,可以在派生类中被重写(Override)

  • 关键机制动态绑定(Dynamic Binding)。程序在运行时(而不是编译时)根据对象的实际类型来决定调用哪个版本的函数。

  • 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal {
public:
    virtual void makeSound() { // 虚函数
        cout << "Some animal sound" << endl;
    }
};
class Cat : public Animal {
public:
    void makeSound() override { // 重写虚函数
        cout << "Meow" << endl;
    }
};
Animal* myPet = new Cat();
myPet->makeSound(); // 输出 "Meow"。尽管指针类型是 Animal*,但调用的是 Cat 的 makeSound。

Templates (模板)

  • 解释一种宏-like 的机制,但它是类型安全的。它允许你编写通用代码,而不必预先指定具体的数据类型。编译器会根据你使用的类型自动生成特定的代码。

  • 主要用途

    • 创建通用容器:如 vector<T>list<T>,可以存放任何类型 T 的元素。

    • 编写通用算法:如 sort(),可以对任何类型的范围进行排序,只要该类型支持比较操作。

  • 示例

1
2
3
4
5
6
template <typename T> // T 是一个占位符类型
T getMax(T a, T b) {
    return (a > b) ? a : b;
}
int i = getMax(3, 7);     // T 被推导为 int
double d = getMax(6.34, 2.59); // T 被推导为 double

Exceptions (异常)

  • 解释一种错误处理机制,用于处理程序中可能发生的异常情况(如文件打开失败、除以零、内存分配失败)。它允许将错误处理代码与正常的程序流程分离开来,使代码更清晰。

  • 工作机制:使用 try, catch, throw 关键字。

    • throw: 在检测到错误的地方“抛出”一个异常。

    • try: 包裹可能抛出异常的代码块。

    • catch: 在后面捕获并处理特定类型的异常。

  • 示例

1
2
3
4
5
6
7
8
9
10
11
try {
    int* myArray = new int[1000]; // 可能抛出 std::bad_alloc 异常
    // ... 一些可能抛出其他异常的操作 ...
    throw runtime_error("A custom error occurred!"); // 抛出一个自定义异常
}
catch (const bad_alloc& e) {
    cerr << "Memory allocation failed: " << e.what() << endl;
}
catch (const exception& e) { // 捕获所有标准异常
    cerr << "Standard exception: " << e.what() << endl;
}

C++语言的设计哲学、优势和应用场景

兼容C,但优于C

  • Compliant with C: 在大多数情况下,C语言的代码可以直接被C++编译器编译。这意味着海量的、成熟的C语言库和遗留代码可以在C++项目中继续使用。这是C++成功的关键因素之一,因为它降低了从C迁移到C++的成本和风险。

  • Better than C: C++在C的基础上增加了许多现代特性,使其更强大、更安全:

    • 类型安全更强: C++引入了 newdelete 运算符来代替C的 malloc()free(),前者会在分配内存时调用构造函数和析构函数,并且不需要进行类型转换。

    • 支持面向对象编程: 提供了类、继承、多态等特性,便于构建更复杂的程序。

    • 支持泛型编程: 通过模板(Template)实现,编写通用、高效的代码(如STL)。

    • 引入了引用: 提供了比指针更安全、更直观的传递方式。

    • 引入了命名空间: 解决了大型项目中变量名、函数名冲突的问题。

保护遗留代码

“兼容C”的直接结果。

更安全

C++通过语言特性减少了程序员犯错的机会:

  • RAII(资源获取即初始化): 这是C++最重要的安全理念。通过类的构造和析构函数自动管理资源(如内存、文件句柄、锁)。当对象离开作用域时,其析构函数会自动被调用,从而确保资源被释放,几乎完全避免了内存泄漏

  • 更强的类型检查: C++编译器在类型检查上比C编译器更严格,能在编译期捕获更多错误。

  • const 关键字: 提供了更强大的常量定义和函数参数保护机制。

更易于调试

  • RAII: 由于资源管理是自动的,调试时无需跟踪每一个 mallocfree,大大减少了因内存管理错误(如悬空指针、重复释放)导致的难以定位的bug。

  • 面向对象: 将数据和对数据的操作封装在一起,代码结构更清晰。当bug出现时,更容易定位到相关的类和方法。

  • 异常处理: 提供了结构化的异常处理机制(try/catch),可以将错误处理代码与正常流程代码分离,使错误传播和捕获更清晰。

支持数据抽象

数据抽象是面向对象编程的基石。它意味着只向外界提供关键信息,而隐藏其后台实现细节。

  • 在C++中,你通过 class 来实现数据抽象。

  • 你将数据成员(属性)设置为 private(对外隐藏),并通过公共的成员函数(方法)来操作这些数据。

  • 例如,一个 Stack 类可以提供 push()pop() 方法,而用户不需要关心栈内部是用数组还是链表实现的。

支持4种编程和设计风格

  • Procedure-Based Programming (过程化编程)

像C语言一样,专注于函数(过程)和算法步骤。C++完全支持这种自顶向下、函数分解的风格。

  • Object-Based Programming (基于对象的编程)

这种风格使用了封装数据抽象(即使用类),但不使用继承和多态。它关注的是将数据和操作绑定在一起创建独立的、自包含的模块,而不是构建类之间的层次关系。

  • Object-Oriented Programming (面向对象编程)

在基于对象的基础上,增加了继承多态,从而可以建立类之间的层次关系,实现代码复用和接口抽象。这是C++最强大的特性之一。

  • Generic Programming (泛型编程)

一种强调代码通用性的风格,通过模板(Template) 实现。编写函数或类时不指定具体的数据类型,等到使用时再确定。C++的标准模板库(STL)是泛型编程的典范,提供了通用的容器(如 vector, map) 和算法(如 sort, find)。

封装、继承、多态

封装 (Encapsulation)

核心思想:隐藏内部实现细节,只暴露安全的操作接口。
好比:一个遥控器。你只需要按按钮,不需要知道里面的电路是怎么工作的。

代码示例:

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
// 这是一个「动物」类 (Class),它体现了封装思想
#include <iostream>
#include <string>
#include <vector>

// 封装 (Encapsulation)
class Animal {
private:
    // 1. 将关键数据设为 private,实现"打包"和"隐藏"
    std::string name;
    int age;
    std::string healthStatus; // 健康状况是内部敏感数据

    // 2. 私有方法,只有类内部自己能调用
    void performHealthCheck() {
        // 复杂的体检逻辑
        if (age > 10) {
            healthStatus = "Needing care";
        }
    }

public:
    // 构造函数:初始化数据
    Animal(const std::string& name, int age) 
        : name(name), age(age), healthStatus("Healthy") {}

    // Getter方法:外部只能通过此方法获取名字
    std::string getName() const {
        return name;
    }

    // Setter方法:外部只能通过此方法修改年龄,加入逻辑检查
    void setAge(int newAge) {
        if (newAge > 0) { // 检查逻辑:年龄必须大于0
            age = newAge;
        } else {
            std::cout << "Invalid age!" << std::endl;
        }
    }

    // 行为方法:暴露一个简单的"叫"的行为
    virtual void makeSound() {
        std::cout << name << " makes a sound." << std::endl;
    }

    // 提供一个公开接口来触发内部私有方法
    void checkUp() {
        performHealthCheck();
        std::cout << "Health status: " << healthStatus << std::endl;
    }

    // 虚析构函数确保正确释放派生类对象
    virtual ~Animal() = default;
};

封装的好处:

  • 安全性healthStatus 是私有的,外部代码无法直接改成 "Very Healthy",必须通过严格的 checkUp() 流程。这防止了数据被不合理地修改。

  • 易用性:用户(其他程序员)只需要知道调用 makeSound() 动物就会叫,不需要知道是怎么叫的。以后即使 makeSound 的内部实现变了,调用它的代码也无需更改。

  • 可维护性:所有与 Animal 相关的数据和操作都捆绑在一起,结构清晰。

继承 (Inheritance)

核心思想:子类自动拥有父类的属性和方法,并可以扩展自己独有的特性。
好比:「猫」是一种「动物」,它天生就会吃和睡,但它还会抓老鼠。

代码示例:

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
// Cat 类 继承自 Animal 类
class Cat : public Animal { // Cat 公开继承 Animal
private:
    std::string breed; // 子类独有属性

public:
    // 子类构造函数,调用父类构造函数
    Cat(const std::string& name, int age, const std::string& breed)
        : Animal(name, age), breed(breed) {}

    // 重写(Override)父类的方法
    void makeSound() override {
        std::cout << getName() << " says: Meow Meow!" << std::endl;
    }

    // 子类独有行为
    void scratch() {
        std::cout << getName() << " is scratching the sofa!" << std::endl;
    }
};

class Lion : public Animal {
public:
    Lion(const std::string& name, int age) : Animal(name, age) {}

    // 重写父类方法
    void makeSound() override {
        std::cout << getName() << " says: Roar!!!" << std::endl;
    }

    void hunt() {
        std::cout << getName() << " is hunting." << std::endl;
    }
};

继承的好处:

  • 代码复用CatLion 不需要重新定义 name, age 属性和 getName(), setAge() 等方法,直接从 Animal 那里就获得了。这避免了重复代码。

  • 建立层次体系:代码结构非常清晰,Cat is-an AnimalLion is-an Animal,符合我们的真实认知。

多态 (Polymorphism)

核心思想:同一操作(方法)作用于不同的对象,可以有不同的解释和实现。
好比:动物园园长对大家喊一声“大家叫一声!”。猫会“喵喵”,狮子会“吼”,鸟会“啾啾”。园长不需要知道具体是谁,只要知道是动物就行。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main() {
    // 多态的典型应用:父类指针指向子类对象
    Animal* myAnimal1 = new Cat("Whiskers", 2, "Siamese");
    Animal* myAnimal2 = new Lion("Simba", 5);
    Animal* myAnimal3 = new Animal("Generic", 3);

    // 创建一个动物容器,存放各种不同的动物
    std::vector<Animal*> animals = {myAnimal1, myAnimal2, myAnimal3};

    std::cout << "The zoo is noisy today:" << std::endl;

    // 多态的魅力:遍历容器,让每个动物都叫一声
    for (const auto& animal : animals) {
        animal->makeSound(); // 同一行代码,产生多种不同的形态
    }

    // 释放内存
    for (const auto& animal : animals) {
        delete animal;
    }

    return 0;
}

输出:

1
2
3
Whiskers says: Meow Meow!
Simba says: Roar!!!
Generic makes a sound.

多态的好处:

  • 接口统一,简化调用Zoo 类的 main 方法不需要写 if (是猫) then 喵喵 else if (是狮子) then 吼 这种冗长的判断。它只需要统一调用 animal.makeSound()。具体怎么叫,由对象自己的实际类型决定。

  • 极佳的扩展性:如果明天动物园新来了一只 Bird(也继承自 Animal),我只需要创建 Bird 对象并把它加入 animals 数组即可。Zoo 类的代码一行都不需要改! 这符合著名的“开闭原则”(对扩展开放,对修改关闭)。

三者相辅相成

封装是基础,它把事物变成独立的对象。

继承使得我们可以基于现有对象创建有层次关系的新对象。

多态则让这些有层次的对象在使用时可以互换,让程序变得无比灵活和强大。

Override(重写)和Overload(重载)

核心比喻

  • Overload (重载):好比同一个词的不同用法。

    “开” 这个动词:

    • 开门 (开+门)

    • 开车 (开+车)

    • 开灯 (开+灯)

    都是“开”,但根据后面接的宾语不同,含义和操作完全不同。这就是重载——同一个类中方法名相同,但参数列表不同

  • Override (重写):好比子承父业,但儿子用更新的技术改进了父亲的方法。

    父亲的创业方法:跑业务() { 挨家挨户上门推销 }
    儿子继承公司后:跑业务() { 通过网络直播带货 }

    方法的目的都是“跑业务”,但具体实现被儿子用新方式彻底改写了。这就是重写——发生在父子类(继承关系)中,方法名和参数列表都必须完全相同,但实现不同

1. Overload (重载) 示例

发生在 同一个类 MathOperations 中。

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
#include <iostream>
class MathOperations {
public:
    // 重载第1版:两个int相加
    int add(int a, int b) {
        std::cout << "Adding two integers: ";
        return a + b;
    }

    // 重载第2版:两个double相加
    // 参数类型不同 -> 构成重载
    double add(double a, double b) {
        std::cout << "Adding two doubles: ";
        return a + b;
    }

    // 重载第3版:三个int相加
    // 参数个数不同 -> 构成重载
    int add(int a, int b, int c) {
        std::cout << "Adding three integers: ";
        return a + b + c;
    }

    // 错误示例:仅返回类型不同,**不构成重载**,会导致编译错误
    // double add(int a, int b) { return a + b; }
};

int main() {
    MathOperations ops;
    
    std::cout << ops.add(1, 2) << std::endl;       // 调用第1版
    std::cout << ops.add(3.14, 2.71) << std::endl; // 调用第2版
    std::cout << ops.add(1, 2, 3) << std::endl;    // 调用第3版
    
    return 0;
}

输出:

1
2
3
Adding two integers: 3
Adding two doubles: 5.85
Adding three integers: 6
2. Override (重写) 示例

发生在 继承关系 中:Animal (父类) 和 Dog/Cat (子类)。

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
#include <iostream>

// 父类
class Animal {
public:
    // 虚函数,允许子类重写
    virtual void makeSound() const {
        std::cout << "Some generic animal sound" << std::endl;
    }

    void eat() { // 非虚函数,不希望子类重写
        std::cout << "Eating..." << std::endl;
    }
};

// 子类 Dog
class Dog : public Animal {
public:
    // 重写 (Override) 父类的 makeSound 方法
    // 方法签名完全相同: void makeSound() const
    void makeSound() const override { // C++11 引入 override 关键字确保正确重写
        std::cout << "Woof! Woof!" << std::endl;
    }
};

// 子类 Cat
class Cat : public Animal {
public:
    // 重写 (Override) 父类的 makeSound 方法
    void makeSound() const override {
        std::cout << "Meow! Meow!" << std::endl;
    }
};

int main() {
    Animal* myAnimal; // 声明一个父类指针

    Dog myDog;
    Cat myCat;

    // 多态的魔力:同一指针,不同行为
    myAnimal = &myDog;
    myAnimal->makeSound(); // 输出: Woof! Woof! (调用的是 Dog 的实现)

    myAnimal = &myCat;
    myAnimal->makeSound(); // 输出: Meow! Meow! (调用的是 Cat 的实现)

    // 非虚函数不能被重写,没有多态性
    myAnimal->eat(); // 总是输出: Eating... (调用的是 Animal 的实现)

    return 0;
}

输出:

1
2
3
Woof! Woof!
Meow! Meow!
Eating...
简单记:
  • 重载(Overload)水平方向的多个方法,在同一个类里并列存在。

  • 重写(Override)垂直方向的覆盖,在继承链上子类覆盖父类。

C++ 中 命名空间(Namespace)

核心问题

核心问题:全局命名污染(Global Name Pollution)

在 C 语言(以及没有命名空间的 C++)中,所有在函数之外定义的变量、函数等都位于全局作用域(global scope)。这就导致了一个严重的问题:整个程序中,所有全局名字都必须是唯一的

C++ 引入了命名空间来解决这个痛点。命名空间就像一个姓氏,它为其中定义的标识符(变量、函数、类等)加了一个前缀,从而将全局作用域划分为不同的、互不干扰的域。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 我的数学库可以放在一个叫 "math" 的命名空间里
namespace math {
    void sort(int array[], int size) {
        // ... 我的排序算法 ...
    }
}

// 图形库可以放在一个叫 "graphics" 的命名空间里
namespace graphics {
    void sort(GraphicObject objects[], int count) {
        // ... 图形库的排序算法 ...
    }
}

现在,即使函数名都叫 sort,但因为它们的“姓氏”(命名空间)不同,它们就是完全不同的函数,不会产生任何冲突。

如何使用呢?

1. 使用作用域解析运算符 ::(就像告诉别人“找老张家的儿子”和“找老李家的儿子”) 例如:

1
2
3
4
5
6
7
8
9
int main() {
    int my_array[10];
    GraphicObject my_objects[5];

    math::sort(my_array, 10);       // 调用 math 命名空间下的 sort
    graphics::sort(my_objects, 5);  // 调用 graphics 命名空间下的 sort

    return 0;
}

2. 使用 using 指令(相当于在当前的对话环境中,默认“张”姓指的是“张三他爸家的张”):整个命名空间导入当前作用域

例如:

1
2
3
4
5
6
7
8
using namespace math; // 告诉编译器,如果找不到名字,默认去 math 命名空间里找

int main() {
    int my_array[10];
    sort(my_array, 10); // 因为上面的 using 指令,这里默认调用的是 math::sort
    // graphics::sort(...) 仍然需要全名
    return 0;
}

注意:在头文件或大型项目中应谨慎使用 using namespace …,以免引入新的歧义。

3. 同时使用作用域解析运算符 :: 以及 using指令:特定符号引入当前作用域

例如:

1
2
3
4
5
6
7
8
using math::sort; // 告诉编译器,默认sort来自于math命名空间

int main() {
    int my_array[10];
    sort(my_array, 10); // 因为上面的 using 指令,这里默认调用的是 math::sort
    // graphics::sort(...) 仍然需要全名
    return 0;
}
本文由作者按照 CC BY 4.0 进行授权