第12章 面向对象编程简介

面向对象编程的基本概念

面向对象编程(Object-Oriented Programming,OOP)是一种编程范式,它将数据和操作数据的方法封装在一起,组成对象,通过对象之间的交互来实现程序功能。

核心概念

  1. 对象(Object):现实世界中的实体在程序中的表示,具有状态(属性)和行为(方法)
  2. 类(Class):对象的蓝图或模板,定义了对象的属性和方法
  3. 封装(Encapsulation):将数据和操作数据的方法封装在一起,隐藏内部实现细节
  4. 继承(Inheritance):从已有类派生出新类,继承父类的属性和方法
  5. 多态(Polymorphism):不同对象对同一消息做出不同的响应

类和对象

类的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ClassName {
private:
// 私有成员(只能在类内部访问)
数据类型 成员变量;

public:
// 公共成员(可以在任何地方访问)
数据类型 成员变量;
返回类型 成员方法(参数列表);

protected:
// 保护成员(可以在类内部和子类中访问)
数据类型 成员变量;
返回类型 成员方法(参数列表);
};

对象的创建和使用

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
// 类的定义
class Person {
private:
std::string name;
int age;

public:
// 构造函数
Person(const std::string& n, int a) : name(n), age(a) {}

// 成员方法
void setName(const std::string& n) {
name = n;
}

std::string getName() const {
return name;
}

void setAge(int a) {
age = a;
}

int getAge() const {
return age;
}

void introduce() const {
std::cout << "Hello, my name is " << name << " and I am " << age << " years old." << std::endl;
}
};

// 对象的创建和使用
int main() {
// 创建对象
Person person1("Alice", 25);

// 使用对象的方法
person1.introduce();

// 修改对象的属性
person1.setName("Bob");
person1.setAge(30);

// 再次使用对象的方法
person1.introduce();

return 0;
}

封装

封装的概念

封装是将数据和操作数据的方法封装在一起,隐藏内部实现细节,只暴露必要的接口给外部使用。

访问控制

C++提供了三种访问控制修饰符:

  1. private:私有成员,只能在类内部访问
  2. public:公共成员,可以在任何地方访问
  3. protected:保护成员,可以在类内部和子类中访问

封装的优点

  1. 数据隐藏:隐藏内部实现细节,提高安全性
  2. 代码重用:封装的代码可以被多次使用
  3. 可维护性:修改内部实现不影响外部代码
  4. 接口清晰:只暴露必要的接口,使代码更易理解

示例

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
class BankAccount {
private:
// 私有成员,外部无法直接访问
std::string accountNumber;
double balance;

public:
// 构造函数
BankAccount(const std::string& accNum, double initialBalance)
: accountNumber(accNum), balance(initialBalance) {}

// 公共方法,用于访问和修改私有成员
void deposit(double amount) {
if (amount > 0) {
balance += amount;
std::cout << "Deposited: $" << amount << std::endl;
std::cout << "New balance: $" << balance << std::endl;
} else {
std::cout << "Invalid deposit amount" << std::endl;
}
}

void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
std::cout << "Withdrew: $" << amount << std::endl;
std::cout << "New balance: $" << balance << std::endl;
} else {
std::cout << "Invalid withdrawal amount" << std::endl;
}
}

double getBalance() const {
return balance;
}

std::string getAccountNumber() const {
return accountNumber;
}
};

int main() {
BankAccount account("123456789", 1000.0);

// 可以通过公共方法访问和修改
account.deposit(500.0);
account.withdraw(200.0);

// 无法直接访问私有成员
// account.balance = 5000.0; // 错误:balance是私有成员

return 0;
}

继承

继承的概念

继承是从已有类派生出新类,新类继承父类的属性和方法,同时可以添加自己的属性和方法,或者重写父类的方法。

基类和派生类

  • 基类(Base Class):被继承的类,也称为父类
  • 派生类(Derived Class):从基类派生的类,也称为子类

继承的语法

1
2
3
class DerivedClass : accessSpecifier BaseClass {
// 派生类的成员
};

其中,accessSpecifier 可以是:

  • public:公有继承,基类的public成员在派生类中仍然是public,protected成员仍然是protected
  • protected:保护继承,基类的public和protected成员在派生类中都是protected
  • private:私有继承,基类的public和protected成员在派生类中都是private

示例

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
55
56
57
58
59
60
61
62
// 基类
class Animal {
protected:
std::string name;

public:
Animal(const std::string& n) : name(n) {}

void eat() {
std::cout << name << " is eating." << std::endl;
}

void sleep() {
std::cout << name << " is sleeping." << std::endl;
}
};

// 派生类 - 公有继承
class Dog : public Animal {
public:
Dog(const std::string& n) : Animal(n) {}

// 新增方法
void bark() {
std::cout << name << " is barking." << std::endl;
}

// 重写基类方法
void eat() {
std::cout << name << " is eating bones." << std::endl;
}
};

// 派生类 - 公有继承
class Cat : public Animal {
public:
Cat(const std::string& n) : Animal(n) {}

// 新增方法
void meow() {
std::cout << name << " is meowing." << std::endl;
}

// 重写基类方法
void eat() {
std::cout << name << " is eating fish." << std::endl;
}
};

int main() {
Dog dog("Rex");
dog.eat(); // 调用重写的方法
dog.sleep(); // 调用继承的方法
dog.bark(); // 调用新增的方法

Cat cat("Whiskers");
cat.eat(); // 调用重写的方法
cat.sleep(); // 调用继承的方法
cat.meow(); // 调用新增的方法

return 0;
}

继承的优点

  1. 代码重用:继承父类的代码,减少重复代码
  2. 层次结构:建立类的层次结构,更符合现实世界的模型
  3. 多态支持:为多态提供基础
  4. 扩展性:易于扩展现有代码

多态

多态的概念

多态是指不同对象对同一消息做出不同的响应,即同一操作作用于不同的对象会产生不同的结果。

编译时多态和运行时多态

  • 编译时多态:通过函数重载和运算符重载实现
  • 运行时多态:通过虚函数和继承实现

虚函数

虚函数是在基类中声明的,允许派生类重写的函数。使用 virtual 关键字声明。

示例

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
55
// 基类
class Shape {
public:
// 虚函数
virtual void draw() {
std::cout << "Drawing a shape." << std::endl;
}

// 虚析构函数
virtual ~Shape() {
std::cout << "Destructing a shape." << std::endl;
}
};

// 派生类
class Circle : public Shape {
public:
// 重写虚函数
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}

~Circle() {
std::cout << "Destructing a circle." << std::endl;
}
};

// 派生类
class Rectangle : public Shape {
public:
// 重写虚函数
void draw() override {
std::cout << "Drawing a rectangle." << std::endl;
}

~Rectangle() {
std::cout << "Destructing a rectangle." << std::endl;
}
};

int main() {
// 基类指针指向派生类对象
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();

// 多态:调用派生类的draw方法
shape1->draw(); // 输出 "Drawing a circle."
shape2->draw(); // 输出 "Drawing a rectangle."

// 释放内存
delete shape1;
delete shape2;

return 0;
}

多态的优点

  1. 灵活性:代码可以处理不同类型的对象
  2. 可扩展性:添加新的派生类不需要修改现有代码
  3. 可维护性:代码更清晰,更易于维护
  4. 接口一致性:不同类型的对象通过相同的接口进行操作

抽象类和接口

抽象类

抽象类是包含纯虚函数的类,不能直接实例化,只能作为基类被继承。

纯虚函数

纯虚函数是在基类中声明的,没有实现的虚函数,派生类必须实现它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class AbstractClass {
public:
// 纯虚函数
virtual void pureVirtualFunction() = 0;

// 普通虚函数
virtual void virtualFunction() {
// 实现
}

// 普通成员函数
void memberFunction() {
// 实现
}
};

接口

在C++中,接口是一种特殊的抽象类,只包含纯虚函数,没有数据成员。

示例

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
// 接口
class Drawable {
public:
virtual void draw() = 0; // 纯虚函数
virtual ~Drawable() = default;
};

// 实现接口的类
class Circle : public Drawable {
public:
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};

class Rectangle : public Drawable {
public:
void draw() override {
std::cout << "Drawing a rectangle." << std::endl;
}
};

// 使用接口
void drawShape(Drawable& shape) {
shape.draw();
}

int main() {
Circle circle;
Rectangle rectangle;

drawShape(circle); // 多态
drawShape(rectangle); // 多态

return 0;
}

面向对象编程的设计原则

1. 单一职责原则(Single Responsibility Principle)

一个类应该只有一个引起它变化的原因,即一个类只负责一项功能。

2. 开放-封闭原则(Open-Closed Principle)

软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。

3. 里氏替换原则(Liskov Substitution Principle)

子类应该能够替换其父类,并且程序的行为不会改变。

4. 接口隔离原则(Interface Segregation Principle)

客户端不应该依赖它不使用的接口,应该将庞大的接口拆分为更小的、更具体的接口。

5. 依赖倒置原则(Dependency Inversion Principle)

高层模块不应该依赖低层模块,两者都应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

6. 组合优于继承原则(Composition Over 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
35
36
37
38
// 继承方式
class Engine {
public:
virtual void start() = 0;
};

class GasolineEngine : public Engine {
public:
void start() override {
std::cout << "Gasoline engine starting..." << std::endl;
}
};

class Car {
private:
GasolineEngine engine; // 固定使用汽油发动机
public:
void start() {
engine.start();
}
};

// 组合方式
class Car {
private:
std::unique_ptr<Engine> engine; // 可以使用任何发动机
public:
Car(std::unique_ptr<Engine> e) : engine(std::move(e)) {}

void start() {
engine->start();
}

// 可以动态更换发动机
void setEngine(std::unique_ptr<Engine> e) {
engine = std::move(e);
}
};

面向对象编程的优点

  1. 代码重用:通过继承和组合实现代码重用
  2. 模块化:将代码组织为独立的对象,便于管理和维护
  3. 可维护性:封装使得修改内部实现不影响外部代码
  4. 可扩展性:通过继承和多态易于扩展现有代码
  5. 可理解性:代码结构更符合现实世界的模型,易于理解
  6. 安全性:封装保护数据,减少错误

面向对象编程的缺点

  1. 复杂性:相比过程式编程,OOP的概念和实现更复杂
  2. 性能开销:继承和多态可能带来一定的性能开销
  3. 过度设计:可能导致过度设计,增加不必要的复杂性
  4. 学习曲线:学习OOP的概念和设计模式需要一定的时间

C++20新特性:概念(Concepts)

C++20引入了概念(Concepts),用于约束模板参数,提高模板代码的可读性和错误信息的清晰度:

概念的基本概念

  • 概念:对类型的约束,指定类型必须满足的条件
  • 约束:使用概念来限制模板参数的类型
  • requires表达式:定义概念的具体约束条件

概念的使用示例

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

// 定义概念:算术类型
template <typename T>
concept Arithmetic = std::is_arithmetic_v<T>;

// 使用概念约束模板参数
template <Arithmetic T>
T add(T a, T b) {
return a + b;
}

// 更复杂的概念:可比较类型
template <typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
{ a > b } -> std::convertible_to<bool>;
{ a == b } -> std::convertible_to<bool>;
};

// 使用概念约束模板参数
template <Comparable T>
T max(T a, T b) {
return a > b ? a : b;
}

// 标准库概念的使用
template <std::integral T>
T square(T x) {
return x * x;
}

template <std::floating_point T>
T square(T x) {
return x * x;
}

int main() {
// 使用add函数
std::cout << "Add integers: " << add(1, 2) << std::endl;
std::cout << "Add doubles: " << add(1.5, 2.5) << std::endl;

// 使用max函数
std::cout << "Max integer: " << max(10, 20) << std::endl;
std::cout << "Max string: " << max(std::string("apple"), std::string("banana")) << std::endl;

// 使用square函数
std::cout << "Square integer: " << square(5) << std::endl;
std::cout << "Square double: " << square(2.5) << std::endl;

return 0;
}

概念的优点

  1. 类型安全:在编译时检查类型约束,避免运行时错误
  2. 错误信息清晰:当类型不满足约束时,提供更清晰的错误信息
  3. 代码可读性:通过概念名称明确表达模板参数的要求
  4. 代码重用:概念可以被多个模板使用,提高代码重用性
  5. 灵活性:概念可以组合使用,创建更复杂的类型约束

面向对象编程与其他编程范式的比较

与过程式编程的比较

  • 过程式编程:关注过程和函数,将程序分解为一系列函数调用
  • 面向对象编程:关注对象和交互,将程序分解为一系列对象的交互

与函数式编程的比较

  • 函数式编程:关注纯函数,避免状态和副作用
  • 面向对象编程:关注对象的状态和行为,允许状态变化

与泛型编程的比较

  • 泛型编程:关注类型参数化,实现代码重用
  • 面向对象编程:关注对象和继承,实现代码重用

小结

本章介绍了面向对象编程的基本概念,包括:

  1. 面向对象编程的核心概念:对象、类、封装、继承、多态
  2. 类和对象:类的定义、对象的创建和使用
  3. 封装:数据隐藏、访问控制
  4. 继承:基类和派生类、继承的语法和优点
  5. 多态:编译时多态和运行时多态、虚函数
  6. 抽象类和接口:纯虚函数、抽象类的使用
  7. 面向对象编程的设计原则:单一职责、开放-封闭、里氏替换、接口隔离、依赖倒置
  8. 面向对象编程的优缺点:代码重用、模块化、可维护性等优点,以及复杂性、性能开销等缺点
  9. 与其他编程范式的比较:与过程式编程、函数式编程、泛型编程的比较

面向对象编程是现代编程语言的重要特性,它提供了一种更符合人类思维方式的编程方法,使得代码更易于理解、维护和扩展。掌握面向对象编程的概念和技巧,对于编写高质量的C++程序至关重要。

在后续章节中,我们将更深入地学习C++的面向对象特性,包括类的设计、构造函数和析构函数、运算符重载、模板等内容。