程序设计范式-Week1
程序设计范式-第1周小结
成绩组成
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时间等资源)。工厂和工厂之间是相互独立的。
-
线程就是工厂里的工人。一个工厂(进程)可以有很多工人(线程)。所有工人共享工厂的资源(厂房、仓库、电力)。工人们协同工作,共同完成工厂的生产任务。
当一个工厂(进程)启动时,它至少会有一个工人,这就是主线程。
多线程的意义
既然有了进程,为什么还需要线程?
-
更高效的并发:创建和切换线程的代价远小于进程。对于需要同时完成多项任务的程序(如Web服务器同时处理成百上千个请求),使用多线程比创建大量进程要高效得多。
-
充分利用多核CPU:一个进程内的多个线程可以被调度到多个CPU核心上真正并行执行,极大地提高了程序的执行效率。例如,一个视频处理软件可以用一个线程解码视频,一个线程处理音频,另一个线程接收用户输入。
-
资源共享与通信方便:线程共享内存,使得数据交换非常高效,无需像进程间通信那样需要复杂的内核介入。
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)一样使用的全新数据类型,但它们的行为和含义完全由你决定。 -
重要性:这是对问题域进行建模的关键。你可以创建
Student、BankAccount、Matrix等类型,让代码更直观、更易于理解和维护。 -
示例:
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)。一旦初始化后,对引用的所有操作都是在操作它所引用的原始变量。
-
主要用途:
-
Pass-by-reference function arguments (按引用传递函数参数):允许函数修改传入的实参的值,同时避免了按值传递时拷贝大对象的开销。
-
作为函数的返回值。
-
-
示例:
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++引入了
new和delete运算符来代替C的malloc()和free(),前者会在分配内存时调用构造函数和析构函数,并且不需要进行类型转换。 -
支持面向对象编程: 提供了类、继承、多态等特性,便于构建更复杂的程序。
-
支持泛型编程: 通过模板(Template)实现,编写通用、高效的代码(如STL)。
-
引入了引用: 提供了比指针更安全、更直观的传递方式。
-
引入了命名空间: 解决了大型项目中变量名、函数名冲突的问题。
-
保护遗留代码
“兼容C”的直接结果。
更安全
C++通过语言特性减少了程序员犯错的机会:
-
RAII(资源获取即初始化): 这是C++最重要的安全理念。通过类的构造和析构函数自动管理资源(如内存、文件句柄、锁)。当对象离开作用域时,其析构函数会自动被调用,从而确保资源被释放,几乎完全避免了内存泄漏。
-
更强的类型检查: C++编译器在类型检查上比C编译器更严格,能在编译期捕获更多错误。
-
const关键字: 提供了更强大的常量定义和函数参数保护机制。
更易于调试
-
RAII: 由于资源管理是自动的,调试时无需跟踪每一个
malloc和free,大大减少了因内存管理错误(如悬空指针、重复释放)导致的难以定位的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;
}
};
继承的好处:
-
代码复用:
Cat和Lion不需要重新定义name,age属性和getName(),setAge()等方法,直接从Animal那里就获得了。这避免了重复代码。 -
建立层次体系:代码结构非常清晰,
Catis-anAnimal,Lionis-anAnimal,符合我们的真实认知。
多态 (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;
}