第14章 类和封装

封装的概念

封装是面向对象编程的三大核心特性之一,它将数据和操作数据的方法捆绑在一起,形成一个独立的单元(类),并控制对数据的访问权限。

封装的目的

  1. 数据隐藏:将内部实现细节隐藏起来,只暴露必要的接口
  2. 安全性:防止外部代码意外修改内部数据
  3. 模块化:将相关的代码组织在一起,提高代码的可维护性
  4. 可扩展性:可以在不影响外部代码的情况下修改内部实现

访问控制

C++通过访问修饰符实现封装,主要有三种访问修饰符:

public(公有)

public成员可以被任何代码访问,通常用于定义类的接口。

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
public:
void setName(const std::string& name) {
this->name = name;
}

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

private(私有)

private成员只能被类的成员函数和友元访问,通常用于定义类的内部数据和实现细节。

protected(保护)

protected成员可以被类的成员函数、友元和派生类访问,通常用于定义需要被派生类访问的内部数据。

类的设计原则

接口与实现分离

将类的接口(public成员)与实现(private成员)分离,使外部代码只依赖于接口,而不依赖于实现细节。

最小化接口

只暴露必要的接口,避免暴露内部实现细节,减少外部代码对内部实现的依赖。

数据抽象

通过抽象数据类型(ADT)将数据和操作数据的方法捆绑在一起,形成一个独立的单元。

封装的实现

构造函数

构造函数用于初始化对象的状态,是封装的重要组成部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Date {
private:
int year, month, day;
public:
Date(int y, int m, int d) {
// 验证参数的有效性
if (y < 1 || m < 1 || m > 12 || d < 1 || d > getDaysInMonth(y, m)) {
throw std::invalid_argument("Invalid date");
}
year = y;
month = m;
day = d;
}

// 其他成员函数...
private:
int getDaysInMonth(int y, int m) {
// 实现...
}
};

析构函数

析构函数用于清理对象的资源,是封装的重要组成部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FileHandler {
private:
FILE* file;
public:
FileHandler(const char* filename, const char* mode) {
file = fopen(filename, mode);
if (!file) {
throw std::runtime_error("Failed to open file");
}
}

~FileHandler() {
if (file) {
fclose(file);
}
}

// 其他成员函数...
};

成员函数

成员函数是类的行为,用于操作类的内部数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Stack {
private:
std::vector<int> elements;
public:
void push(int value) {
elements.push_back(value);
}

int pop() {
if (elements.empty()) {
throw std::runtime_error("Stack is empty");
}
int value = elements.back();
elements.pop_back();
return value;
}

bool isEmpty() const {
return elements.empty();
}
};

封装与信息隐藏

信息隐藏的原则

  1. 最小知识原则:一个对象应该只了解与其直接相关的对象
  2. 单一职责原则:一个类应该只有一个引起它变化的原因
  3. 开闭原则:软件实体应该对扩展开放,对修改关闭

信息隐藏的实现

通过将数据成员声明为private,并提供public的访问器和修改器方法来实现信息隐藏。

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
class BankAccount {
private:
std::string accountNumber;
double balance;
public:
BankAccount(const std::string& number, double initialBalance)
: accountNumber(number), balance(initialBalance) {}

void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}

void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
}
}

double getBalance() const {
return balance;
}

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

封装的最佳实践

1. 使用访问器和修改器方法

为private数据成员提供public的访问器(getter)和修改器(setter)方法,而不是直接暴露数据成员。

2. 验证输入数据

在修改器方法中验证输入数据的有效性,确保对象的状态始终保持一致。

3. 使用const成员函数

对于不修改对象状态的成员函数,声明为const成员函数。

4. 避免使用全局变量

尽量使用类的成员变量代替全局变量,提高代码的封装性。

5. 使用命名约定

为private成员变量使用特殊的命名约定,如在变量名前加下划线,以便于区分。

6. 现代C++类设计特性

移动语义(C++11+)

使用移动语义提高对象的性能,避免不必要的拷贝操作:

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
class String {
private:
char* data;
size_t length;
public:
// 构造函数
String(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}

// 移动构造函数
String(String&& other) noexcept : data(other.data), length(other.length) {
other.data = nullptr;
other.length = 0;
}

// 移动赋值运算符
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
return *this;
}

// 析构函数
~String() {
delete[] data;
}

// 禁止拷贝
String(const String&) = delete;
String& operator=(const String&) = delete;
};

默认成员函数控制(C++11+)

显式控制默认成员函数的生成:

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
class MyClass {
private:
int value;
public:
// 默认构造函数
MyClass() = default;

// 带参数的构造函数
MyClass(int v) : value(v) {}

// 拷贝构造函数
MyClass(const MyClass&) = default;

// 移动构造函数
MyClass(MyClass&&) = default;

// 拷贝赋值运算符
MyClass& operator=(const MyClass&) = default;

// 移动赋值运算符
MyClass& operator=(MyClass&&) = default;

// 析构函数
~MyClass() = default;
};

委托构造函数(C++11+)

使用委托构造函数减少代码重复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Date {
private:
int year, month, day;
public:
// 主构造函数
Date(int y, int m, int d) : year(y), month(m), day(d) {
// 验证日期有效性
}

// 委托构造函数
Date() : Date(2000, 1, 1) {}

Date(int y) : Date(y, 1, 1) {}

Date(int y, int m) : Date(y, m, 1) {}
};

继承构造函数(C++11+)

使用继承构造函数简化派生类的构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
private:
int value;
public:
Base(int v) : value(v) {}
};

class Derived : public Base {
private:
std::string name;
public:
// 继承基类构造函数
using Base::Base;

// 额外的构造函数
Derived(int v, const std::string& n) : Base(v), name(n) {}
};

示例:封装的实现

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
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <string>
#include <stdexcept>

class Rectangle {
private:
double width;
double height;
public:
// 构造函数
Rectangle(double w, double h) {
setWidth(w);
setHeight(h);
}

// 访问器方法
double getWidth() const {
return width;
}

double getHeight() const {
return height;
}

// 修改器方法
void setWidth(double w) {
if (w <= 0) {
throw std::invalid_argument("Width must be positive");
}
width = w;
}

void setHeight(double h) {
if (h <= 0) {
throw std::invalid_argument("Height must be positive");
}
height = h;
}

// 计算面积
double area() const {
return width * height;
}

// 计算周长
double perimeter() const {
return 2 * (width + height);
}
};

int main() {
try {
Rectangle rect(5.0, 3.0);
std::cout << "Width: " << rect.getWidth() << std::endl;
std::cout << "Height: " << rect.getHeight() << std::endl;
std::cout << "Area: " << rect.area() << std::endl;
std::cout << "Perimeter: " << rect.perimeter() << std::endl;

// 修改尺寸
rect.setWidth(7.0);
rect.setHeight(4.0);
std::cout << "\nAfter modification:" << std::endl;
std::cout << "Width: " << rect.getWidth() << std::endl;
std::cout << "Height: " << rect.getHeight() << std::endl;
std::cout << "Area: " << rect.area() << std::endl;
std::cout << "Perimeter: " << rect.perimeter() << std::endl;

// 尝试设置无效值
// rect.setWidth(-1.0); // 会抛出异常
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}

return 0;
}

封装与设计模式

单例模式

单例模式确保一个类只有一个实例,并提供一个全局访问点。

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
class Singleton {
private:
static Singleton* instance;

// 私有构造函数,防止外部创建实例
Singleton() {}

// 私有析构函数
~Singleton() {}

// 防止拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
// 全局访问点
static Singleton* getInstance() {
if (!instance) {
instance = new Singleton();
}
return instance;
}

// 其他成员函数...
};

// 初始化静态成员
Singleton* Singleton::instance = nullptr;

工厂模式

工厂模式通过工厂类创建对象,隐藏对象的创建细节。

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
class Product {
public:
virtual void use() = 0;
virtual ~Product() = default;
};

class ConcreteProductA : public Product {
public:
void use() override {
std::cout << "Using Product A" << std::endl;
}
};

class ConcreteProductB : public Product {
public:
void use() override {
std::cout << "Using Product B" << std::endl;
}
};

class Factory {
public:
static std::unique_ptr<Product> createProduct(const std::string& type) {
if (type == "A") {
return std::make_unique<ConcreteProductA>();
} else if (type == "B") {
return std::make_unique<ConcreteProductB>();
}
return nullptr;
}
};

总结

封装是C++面向对象编程的重要特性,它通过访问修饰符控制对数据的访问权限,将数据和操作数据的方法捆绑在一起,形成一个独立的单元。合理使用封装可以提高代码的安全性、可维护性和可扩展性,是现代C++编程中的重要技术之一。