C++教程 第18章 类的高级特性
第18章 类的高级特性
友元
友元是C++中一种特殊的访问控制机制,它允许特定的函数或类访问另一个类的私有成员和保护成员。虽然友元会破坏封装性,但在某些场景下,它可以提高代码的灵活性和性能。
友元函数
友元函数是指可以访问类的私有成员和保护成员的非成员函数。
1 |
|
友元类
一个类可以声明另一个类为友元,这样友元类的所有成员函数都可以访问原类的私有成员和保护成员。
1 |
|
成员函数作为友元
一个类的成员函数可以被声明为另一个类的友元。
1 |
|
友元的底层实现
友元的编译期处理:
- 友元关系是在编译期确定的,不是运行时
- 编译器在处理类定义时,会记录友元声明
- 当编译友元函数或友元类的成员函数时,会检查是否有访问权限
友元与封装性:
- 友元破坏了封装性,允许外部代码访问私有成员
- 但友元关系是单向的,A是B的友元不意味着B是A的友元
- 友元关系也不具有传递性,A是B的友元,B是C的友元,不意味着A是C的友元
友元与继承:
- 友元关系不会被继承,派生类不会继承基类的友元关系
- 基类的友元不会自动成为派生类的友元
友元的性能影响
访问速度:
- 友元函数直接访问私有成员,避免了通过公共接口的函数调用开销
- 对于频繁调用的函数,使用友元可以提高性能
内联可能性:
- 友元函数通常定义在头文件中,更容易被编译器内联
- 内联友元函数可以进一步减少函数调用开销
内存布局:
- 友元关系不影响类的内存布局
- 类的大小和成员变量的布局不受友元声明的影响
友元的最佳实践
最小化友元使用:
- 只在必要时使用友元,优先考虑公共接口
- 友元应该是特例,不是常规做法
友元的作用域:
- 尽可能将友元声明限制在最小必要的范围内
- 优先使用成员函数作为友元,而不是整个类作为友元
友元与运算符重载:
- 对于需要访问两个对象私有成员的运算符重载(如+、-等),友元函数是理想选择
- 对于单目运算符(如++、–等),成员函数通常更合适
友元与工厂模式:
- 在工厂模式中,工厂类需要创建产品类的实例并初始化其私有成员
- 此时,将工厂类声明为产品类的友元是合理的
友元与测试:
- 在单元测试中,可以将测试类声明为被测类的友元
- 这样测试代码可以直接访问被测类的私有成员,进行更全面的测试
友元的高级应用
友元与模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19template <typename T>
class Pair {
private:
T first;
T second;
public:
Pair(T f, T s) : first(f), second(s) {}
// 模板友元函数
template <typename U>
friend std::ostream& operator<<(std::ostream& os, const Pair<U>& pair);
};
template <typename U>
std::ostream& operator<<(std::ostream& os, const Pair<U>& pair) {
os << "(" << pair.first << ", " << pair.second << ")";
return os;
}友元与命名空间:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20namespace Math {
class Vector3D {
private:
double x, y, z;
public:
Vector3D(double x, double y, double z) : x(x), y(y), z(z) {}
// 声明另一个命名空间中的函数为友元
friend Vector3D Physics::calculateForce(const Vector3D&);
};
}
namespace Physics {
Vector3D calculateForce(const Math::Vector3D& velocity) {
// 可以访问Vector3D的私有成员
double mass = 1.0;
return Math::Vector3D(mass * velocity.x, mass * velocity.y, mass * velocity.z);
}
}友元与CRTP(奇异递归模板模式):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25template <typename Derived>
class Base {
private:
int value;
public:
Base(int v) : value(v) {}
void doSomething() {
static_cast<Derived*>(this)->implementation();
}
// 允许派生类访问私有成员
friend Derived;
};
class Derived : public Base<Derived> {
public:
Derived(int v) : Base<Derived>(v) {}
void implementation() {
// 可以访问Base的私有成员
std::cout << "Derived implementation, value: " << value << std::endl;
}
};
友元的替代方案
公共接口:
- 优先提供公共getter和setter方法
- 虽然有函数调用开销,但保持了封装性
委托:
- 将需要访问私有成员的操作委托给类的成员函数
- 外部代码调用公共成员函数,由它内部处理私有成员
内部类:
- 在类内部定义一个辅助类,处理需要访问私有成员的操作
- 内部类自动具有访问外部类私有成员的权限
PIMPL(指针to实现)模式:
- 将私有成员放在一个单独的实现类中
- 公共类持有实现类的指针
- 这种模式可以减少编译依赖,同时保持封装性
友元是C++中一种强大但有争议的特性。正确使用友元可以提高代码的灵活性和性能,但过度使用会破坏封装性,使代码难以维护。在实际开发中,应该谨慎使用友元,权衡其利弊,并遵循最佳实践。
类的静态成员
静态成员是C++中一种特殊的成员,它属于类而不是对象,所有对象共享同一个静态成员。静态成员在面向对象编程中有着广泛的应用,如计数器、配置信息、单例模式等。
静态成员变量
静态成员变量是类的所有对象共享的变量,不属于任何单个对象。
1 |
|
静态成员函数
静态成员函数属于类而不是对象,可以在没有对象的情况下调用。
1 |
|
静态成员的底层实现
静态成员变量的存储:
- 静态成员变量存储在全局数据区,而不是对象的内存空间中
- 它们在程序启动时分配内存,程序结束时释放内存
- 所有对象共享同一个静态成员变量的实例
静态成员函数的实现:
- 静态成员函数不包含
this指针 - 它们在内存中只有一份拷贝,与普通函数类似
- 静态成员函数不能访问非静态成员,因为没有
this指针
- 静态成员函数不包含
静态成员的初始化:
- 静态成员变量必须在类外部定义和初始化
- 初始化顺序在同一个编译单元内是确定的,但在不同编译单元间是不确定的
- C++17引入了内联静态成员变量,可以在类内部初始化
静态成员的线程安全性
静态成员变量的线程安全问题:
- 多个线程同时访问和修改静态成员变量可能导致竞态条件
- 需要使用互斥锁或其他同步机制来保护
线程安全的静态成员:
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 ThreadSafeCounter {
private:
static int count;
static std::mutex mtx;
public:
ThreadSafeCounter() {
std::lock_guard<std::mutex> lock(mtx);
++count;
}
~ThreadSafeCounter() {
std::lock_guard<std::mutex> lock(mtx);
--count;
}
static int getCount() {
std::lock_guard<std::mutex> lock(mtx);
return count;
}
};
// 初始化静态成员
int ThreadSafeCounter::count = 0;
std::mutex ThreadSafeCounter::mtx;C++11+的线程安全初始化:
- C++11标准保证静态局部变量的初始化是线程安全的
- 这使得Meyer’s单例模式成为实现线程安全单例的最佳选择
静态成员的内存布局
类的内存布局:
- 静态成员变量不占用类的内存空间
- 类的大小由非静态成员变量决定
- 静态成员函数也不占用类的内存空间
静态成员的地址:
- 静态成员变量有固定的内存地址,在程序启动时分配
- 可以通过
&ClassName::staticMember获取静态成员变量的地址 - 静态成员函数的地址可以通过
&ClassName::staticMemberFunction获取
静态成员与虚函数:
- 静态成员函数不能是虚函数
- 因为虚函数的调用需要
this指针,而静态成员函数没有this指针
静态成员的高级应用
内联静态成员变量(C++17+):
1
2
3
4
5
6class Config {
public:
// C++17+:内联静态成员变量,可以在类内部初始化
inline static const std::string version = "1.0.0";
inline static const int maxConnections = 100;
};静态成员与模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25template <typename T>
class Array {
private:
static size_t objectCount;
T* data;
size_t size;
public:
Array(size_t s) : size(s), data(new T[s]) {
++objectCount;
}
~Array() {
delete[] data;
--objectCount;
}
static size_t getObjectCount() {
return objectCount;
}
};
// 为每个模板实例化定义静态成员变量
template <typename T>
size_t Array<T>::objectCount = 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
28
29
30
31
32
33
34
35
36class Singleton {
private:
static Singleton* instance;
int value;
// 私有构造函数,防止外部创建实例
Singleton() : value(0) {}
// 防止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
static void destroyInstance() {
delete instance;
instance = nullptr;
}
void setValue(int v) {
value = v;
}
int getValue() const {
return value;
}
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;Meyer’s 单例模式(线程安全):
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
26class Singleton {
private:
int value;
// 私有构造函数
Singleton() : value(0) {}
public:
// 静态成员函数返回静态局部变量
static Singleton& getInstance() {
static Singleton instance; // C++11+保证线程安全的初始化
return instance;
}
void setValue(int v) {
value = v;
}
int getValue() const {
return value;
}
// 防止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
静态成员的性能考虑
内存使用:
- 静态成员变量在全局数据区只存储一份,节省内存
- 对于频繁使用的常量,使用静态成员可以避免重复创建
访问速度:
- 静态成员变量的访问速度与全局变量相当,比成员变量稍快
- 静态成员函数的调用速度与普通函数相当,比成员函数稍快(因为没有
this指针)
初始化开销:
- 静态成员变量在程序启动时初始化,增加了程序的启动时间
- 对于大型静态对象,初始化开销可能较大
静态成员的最佳实践
合理使用静态成员:
- 对于所有对象共享的数据,使用静态成员变量
- 对于与对象状态无关的操作,使用静态成员函数
- 避免过度使用静态成员,以免破坏面向对象的封装性
静态成员的命名约定:
- 使用大写字母或特定前缀标识静态成员
- 提高代码的可读性和可维护性
静态成员的初始化:
- 确保静态成员变量在使用前正确初始化
- 对于依赖于其他静态成员的情况,注意初始化顺序
- 优先使用C++17的内联静态成员变量,简化代码
静态成员与继承:
- 静态成员在继承层次结构中是共享的
- 派生类可以访问基类的静态成员
- 派生类可以隐藏基类的静态成员,但不能覆盖它们
静态成员的测试:
- 静态成员的状态会在测试之间保持
- 在单元测试中,需要确保静态成员的状态不会影响其他测试
- 考虑在测试前后重置静态成员的状态
静态成员的常见应用场景
- 计数器:跟踪类的实例数量
- 配置信息:存储应用程序的配置参数
- 常量定义:定义类级别的常量
- 单例模式:确保类只有一个实例
- 工厂模式:创建对象的工厂方法
- 工具函数:与对象状态无关的工具方法
- 缓存:存储共享的缓存数据
- 全局状态:管理应用程序的全局状态
静态成员是C++中一种强大的特性,它提供了一种在类级别共享数据和行为的机制。正确使用静态成员可以提高代码的效率和可维护性,但过度使用可能会导致代码难以理解和测试。在实际编程中,应该根据具体情况权衡使用静态成员的利弊。
类的常量成员
常量成员是C++中实现常量正确性(Const Correctness)的重要机制,它确保了对象在不同上下文中的正确使用。常量成员包括常量成员变量和常量成员函数。
常量成员变量
常量成员变量必须在构造函数的初始化列表中初始化,且在对象的生命周期内不能改变。
1 |
|
常量成员函数
常量成员函数承诺不会修改对象的状态。
1 |
|
常量成员的底层实现
常量成员变量的存储:
- 常量成员变量与普通成员变量存储在同一内存区域
- 它们占用对象的内存空间,增加对象的大小
- 编译器会在编译期检查对常量成员变量的修改
常量成员函数的实现:
- 常量成员函数的
this指针类型是const ClassName* const - 非常量成员函数的
this指针类型是ClassName* const - 这种类型差异使得常量成员函数无法修改对象的状态
- 常量成员函数的
mutable关键字:
mutable关键字允许在常量成员函数中修改特定的成员变量- 被
mutable修饰的成员变量不受常量成员函数的限制 - 通常用于缓存、计数等不影响对象逻辑状态的成员
常量的传递性
const与函数参数:
- 常量引用参数可以接受常量和非常量实参
- 非常量引用参数只能接受非常量实参
- 按值传递的参数不受const修饰符的影响
const与函数返回值:
- 返回常量值可以防止返回值被意外修改
- 返回常量引用可以防止通过引用修改原对象
- 对于内置类型,返回const值没有实际意义
const与成员函数链:
- 常量成员函数只能调用其他常量成员函数
- 非常量成员函数可以调用常量和非常量成员函数
- 这确保了常量对象的状态不会被修改
常量成员的高级应用
const与运算符重载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Vector2D {
private:
double x, y;
public:
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// 常量成员函数重载运算符+
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
// 非常量成员函数重载运算符+
Vector2D& operator+=(const Vector2D& other) {
x += other.x;
y += other.y;
return *this;
}
// 常量成员函数重载运算符==
bool operator==(const Vector2D& other) const {
return x == other.x && y == other.y;
}
};const与指针:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Data {
private:
int value;
public:
Data(int v) : value(v) {}
// 返回常量指针
const int* getValuePtr() const {
return &value;
}
// 返回非常量指针
int* getValuePtr() {
return &value;
}
};const与智能指针:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class Resource {
private:
int value;
public:
Resource(int v) : value(v) {}
int getValue() const {
return value;
}
void setValue(int v) {
value = v;
}
};
// 指向常量对象的智能指针
std::unique_ptr<const Resource> constResource = std::make_unique<Resource>(42);
// constResource->setValue(100); // 错误:不能修改常量对象
std::cout << constResource->getValue() << std::endl; // 允许
// 常量智能指针
const std::unique_ptr<Resource> resourcePtr = std::make_unique<Resource>(42);
// resourcePtr = std::make_unique<Resource>(100); // 错误:不能修改常量智能指针
resourcePtr->setValue(100); // 允许:可以修改指向的对象
常量成员的性能考虑
编译器优化:
- const成员函数提供了更强的不变性保证
- 编译器可以进行更多的优化,如常量折叠、函数内联等
- 常量对象的访问可能会被优化为直接访问内存
线程安全:
- 常量成员函数通常是线程安全的(如果没有修改mutable成员)
- 多个线程可以同时调用常量成员函数
- 这使得常量成员函数成为多线程编程中的理想选择
代码可读性:
- const限定符明确了函数的行为
- 提高了代码的可读性和可维护性
- 使代码的意图更加清晰
常量成员的最佳实践
尽可能使用const:
- 对于不修改对象状态的成员函数,添加const限定符
- 对于不修改参数的函数参数,添加const限定符
- 对于不修改返回值的函数,考虑返回const值
const与重载:
- 为const和非const对象提供适当的重载版本
- 确保const版本和非const版本的行为一致
- 优先实现const版本,然后让非const版本调用const版本
合理使用mutable:
- 只对真正需要在const成员函数中修改的成员变量使用mutable
- 通常用于缓存、计数等不影响对象逻辑状态的成员
- 避免过度使用mutable,以免破坏常量正确性
const与引用传递:
- 对于大对象,优先使用const引用传递
- 避免不必要的拷贝,同时确保对象不被修改
- 这是一种高效且安全的参数传递方式
const与移动语义:
- 注意const对象不能被移动,只能被拷贝
- 移动语义需要修改源对象,与const语义冲突
- 对于需要移动的对象,不要声明为const
常量正确性的重要性
编译期错误检测:
- const限定符可以在编译期检测到潜在的错误
- 避免了运行时错误的可能性
- 提高了代码的安全性和可靠性
接口清晰性:
- const限定符明确了接口的契约
- 告诉用户哪些函数会修改对象,哪些不会
- 提高了代码的可理解性和可维护性
优化机会:
- const限定符为编译器提供了更多的优化机会
- 可以生成更高效的机器代码
- 提高了程序的性能
线程安全性:
- const成员函数天然具有更好的线程安全性
- 减少了多线程编程中的同步开销
- 提高了程序的并发性能
常量正确性是C++编程中的重要概念,它通过编译期检查确保了对象的正确使用,提高了代码的安全性、可读性和可维护性。正确理解和使用常量成员,对于编写高效、安全、可维护的C++代码至关重要。
类的类型转换
类的类型转换是C++中一种强大的特性,它允许在不同类型之间进行转换,包括从其他类型转换为类类型(转换构造函数)和从类类型转换为其他类型(类型转换运算符)。
转换构造函数
转换构造函数是一个参数的构造函数,用于将其他类型转换为类类型。
1 |
|
类型转换运算符
类型转换运算符用于将类类型转换为其他类型。
1 |
|
类型转换的底层实现
转换构造函数的实现:
- 转换构造函数是一个特殊的构造函数,只有一个参数
- 当编译器需要将其他类型转换为类类型时,会调用转换构造函数
- 转换构造函数的调用是在编译期决定的
类型转换运算符的实现:
- 类型转换运算符是一个特殊的成员函数,没有返回类型
- 当编译器需要将类类型转换为其他类型时,会调用类型转换运算符
- 类型转换运算符的调用也是在编译期决定的
隐式转换与显式转换:
- 隐式转换:编译器自动进行的转换
- 显式转换:使用static_cast、dynamic_cast等运算符进行的转换
- explicit关键字可以防止隐式转换,只允许显式转换
类型转换的性能考虑
转换开销:
- 类型转换可能会产生开销,特别是当转换涉及到复杂的计算或内存分配时
- 应尽量减少不必要的类型转换
- 对于频繁执行的代码路径,应考虑避免类型转换
内联可能性:
- 类型转换运算符和转换构造函数通常是内联的
- 内联可以减少函数调用的开销
- 对于简单的转换,内联可以完全消除转换开销
移动语义:
- 类型转换时可以使用移动语义来减少拷贝开销
- 例如,在转换构造函数中使用移动构造来初始化成员变量
类型转换的最佳实践
使用explicit关键字:
- 对于转换构造函数和类型转换运算符,优先使用explicit关键字
- 这可以防止意外的隐式转换,提高代码的安全性
- 只在确实需要隐式转换的情况下省略explicit关键字
避免歧义转换:
- 确保类型转换不会产生歧义
- 避免定义多个可能导致歧义的转换路径
- 使用explicit关键字可以减少歧义的可能性
转换的语义正确性:
- 类型转换应该具有直观的语义
- 转换后的结果应该符合用户的预期
- 避免定义语义不明确的类型转换
性能优化:
- 对于频繁执行的类型转换,应优化转换的性能
- 考虑使用内联、移动语义等技术减少转换开销
- 对于复杂的转换,考虑缓存转换结果
类型转换的高级应用
转换构造函数与运算符重载:
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
28class Complex {
private:
double real;
double imag;
public:
// 转换构造函数
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 转换构造函数(从int转换)
Complex(int r) : real(r), imag(0) {}
// 运算符重载
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
// 类型转换运算符
operator double() const {
return real; // 返回实部
}
};
// 使用
Complex c1 = 5; // 隐式转换:int -> Complex
Complex c2 = 3.14; // 隐式转换:double -> Complex
Complex c3 = c1 + c2; // 运算符重载
double realPart = c3; // 隐式转换:Complex -> double类型转换与模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22template <typename T>
class Wrapper {
private:
T value;
public:
Wrapper(const T& v) : value(v) {}
// 转换构造函数:从其他Wrapper类型转换
template <typename U>
Wrapper(const Wrapper<U>& other) : value(static_cast<T>(other.getValue())) {}
T getValue() const {
return value;
}
// 类型转换运算符:转换为其他类型
template <typename U>
operator U() const {
return static_cast<U>(value);
}
};安全的布尔转换:
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
37class SafeBool {
private:
bool value;
// 私有类型,用于安全的布尔转换
struct BoolType {
int dummy;
};
public:
SafeBool(bool v) : value(v) {}
// 安全的布尔转换运算符
operator BoolType*() const {
return value ? reinterpret_cast<BoolType*>(1) : nullptr;
}
// 防止与其他类型的比较
bool operator!() const {
return !value;
}
};
// 使用
SafeBool b1(true);
SafeBool b2(false);
if (b1) {
std::cout << "b1 is true" << std::endl;
}
if (!b2) {
std::cout << "b2 is false" << std::endl;
}
// 错误:不能与整数比较
// if (b1 == 1) {}
类型转换的陷阱
意外的隐式转换:
- 隐式转换可能会导致意外的行为
- 例如,一个接受double参数的函数可能会意外地接受一个自定义类型的对象
- 使用explicit关键字可以防止这种情况
转换歧义:
- 当存在多个可能的转换路径时,会产生歧义
- 例如,一个类同时有转换构造函数和类型转换运算符
- 应避免定义可能导致歧义的转换
转换链:
- 编译器最多只会进行一次用户定义的类型转换
- 例如,A -> B -> C 的转换不会自动进行
- 需要显式地进行每一步转换
性能问题:
- 频繁的类型转换可能会影响性能
- 特别是当转换涉及到复杂的计算或内存分配时
- 应尽量减少不必要的类型转换
类型转换与现代C++
C++11的显式转换运算符:
- C++11引入了explicit关键字用于类型转换运算符
- 这使得类型转换运算符的行为与转换构造函数一致
- 推荐使用explicit关键字来防止意外的隐式转换
C++11的移动语义:
- 移动语义可以用于类型转换,减少拷贝开销
- 例如,在转换构造函数中使用移动构造来初始化成员变量
C++17的保证拷贝省略:
- C++17引入了保证拷贝省略,减少了类型转换中的拷贝开销
- 这使得返回值优化更加可靠
类型转换的设计原则
最小惊讶原则:
- 类型转换的行为应该符合用户的预期
- 避免定义令人惊讶的类型转换
显式优于隐式:
- 优先使用显式转换,而不是隐式转换
- 这可以提高代码的可读性和安全性
单一职责原则:
- 类型转换应该只负责转换,不应该执行其他操作
- 避免在类型转换中执行复杂的计算或副作用
性能考虑:
- 类型转换应该尽可能高效
- 对于频繁执行的转换,应优化其性能
类型转换是C++中一种强大的特性,它可以使代码更加灵活和直观。然而,类型转换也可能导致意外的行为和性能问题。正确理解和使用类型转换,对于编写高效、安全、可维护的C++代码至关重要。
类的嵌套和局部类
类的嵌套和局部类是C++中两种特殊的类定义方式,它们提供了一种封装和组织代码的机制,特别是在处理复杂的类层次结构时非常有用。
嵌套类
嵌套类是在另一个类的内部定义的类。
1 |
|
局部类
局部类是在函数内部定义的类。
1 |
|
嵌套类的底层实现
嵌套类的作用域:
- 嵌套类的作用域被限制在外部类内部
- 外部类可以直接访问嵌套类的成员,包括私有成员
- 嵌套类不能直接访问外部类的非静态成员,因为它没有外部类的实例
嵌套类的内存布局:
- 嵌套类是一个独立的类,有自己的内存布局
- 嵌套类的大小不影响外部类的大小
- 嵌套类的对象存储在独立的内存空间中
嵌套类的访问控制:
- 嵌套类的访问权限由其在外部类中的声明位置决定
- 如果嵌套类声明在外部类的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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46class LinkedList {
private:
// 嵌套的节点类
class Node {
public:
int data;
Node* next;
Node(int d) : data(d), next(nullptr) {}
};
Node* head;
public:
LinkedList() : head(nullptr) {}
void add(int data) {
Node* newNode = new Node(data);
if (!head) {
head = newNode;
} else {
Node* current = head;
while (current->next) {
current = current->next;
}
current->next = newNode;
}
}
void display() const {
Node* current = head;
while (current) {
std::cout << current->data << " ";
current = current->next;
}
std::cout << std::endl;
}
~LinkedList() {
while (head) {
Node* temp = head;
head = head->next;
delete temp;
}
}
};嵌套类与迭代器模式:
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
52class Vector {
private:
int* data;
size_t size;
size_t capacity;
public:
Vector() : data(nullptr), size(0), capacity(0) {}
void push_back(int value) {
if (size >= capacity) {
size_t newCapacity = capacity == 0 ? 1 : capacity * 2;
int* newData = new int[newCapacity];
for (size_t i = 0; i < size; ++i) {
newData[i] = data[i];
}
delete[] data;
data = newData;
capacity = newCapacity;
}
data[size++] = value;
}
size_t getSize() const { return size; }
// 嵌套的迭代器类
class Iterator {
private:
int* ptr;
public:
Iterator(int* p) : ptr(p) {}
int& operator*() const { return *ptr; }
Iterator& operator++() {
++ptr;
return *this;
}
bool operator!=(const Iterator& other) const {
return ptr != other.ptr;
}
};
Iterator begin() const { return Iterator(data); }
Iterator end() const { return Iterator(data + size); }
~Vector() {
delete[] data;
}
};嵌套类与策略模式:
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
76
77
78
79
80class SortStrategy {
public:
virtual void sort(int* data, size_t size) = 0;
virtual ~SortStrategy() {}
};
class Sorter {
private:
// 嵌套的排序策略实现
class BubbleSort : public SortStrategy {
public:
void sort(int* data, size_t size) override {
for (size_t i = 0; i < size - 1; ++i) {
for (size_t j = 0; j < size - i - 1; ++j) {
if (data[j] > data[j + 1]) {
std::swap(data[j], data[j + 1]);
}
}
}
}
};
class QuickSort : public SortStrategy {
private:
void quickSort(int* data, int low, int high) {
if (low < high) {
int pivot = partition(data, low, high);
quickSort(data, low, pivot - 1);
quickSort(data, pivot + 1, high);
}
}
int partition(int* data, int low, int high) {
int pivot = data[high];
int i = low - 1;
for (int j = low; j < high; ++j) {
if (data[j] <= pivot) {
++i;
std::swap(data[i], data[j]);
}
}
std::swap(data[i + 1], data[high]);
return i + 1;
}
public:
void sort(int* data, size_t size) override {
quickSort(data, 0, size - 1);
}
};
SortStrategy* strategy;
public:
enum StrategyType {
BUBBLE_SORT,
QUICK_SORT
};
Sorter(StrategyType type) {
switch (type) {
case BUBBLE_SORT:
strategy = new BubbleSort();
break;
case QUICK_SORT:
strategy = new QuickSort();
break;
default:
strategy = new BubbleSort();
}
}
void sort(int* data, size_t size) {
strategy->sort(data, size);
}
~Sorter() {
delete strategy;
}
};
嵌套类和局部类的性能考虑
嵌套类的性能:
- 嵌套类的成员函数调用开销与普通类相同
- 嵌套类的对象创建和销毁开销与普通类相同
- 嵌套类不会引入额外的性能开销
局部类的性能:
- 局部类的成员函数通常是内联的,减少了函数调用开销
- 局部类的对象存储在栈上,创建和销毁开销很小
- 局部类适合用于临时的、小范围的任务
内存使用:
- 嵌套类的对象存储在独立的内存空间中
- 局部类的对象存储在函数的栈帧中
- 两者都不会增加额外的内存开销
嵌套类和局部类的最佳实践
嵌套类的最佳实践:
- 封装实现细节:使用嵌套类来封装外部类的实现细节
- 逻辑关联:当一个类只与另一个类相关时,使用嵌套类
- 访问控制:根据需要选择适当的访问权限
- 命名空间污染:使用嵌套类来避免命名空间污染
局部类的最佳实践:
- 临时使用:只在函数内部临时需要一个类时使用局部类
- 小范围:局部类的作用域应限制在最小必要的范围内
- 简单实现:局部类应该保持简单,避免复杂的实现
- 避免静态成员:记住局部类不能有静态成员
代码组织:
- 清晰的层次结构:使用嵌套类来创建清晰的层次结构
- 逻辑分组:将相关的类组织在一起
- 可读性:确保嵌套和局部类的使用不会降低代码的可读性
性能优化:
- 内联成员函数:对于简单的成员函数,使用内联来提高性能
- 避免动态内存分配:对于局部类,优先使用栈上存储
- 最小化开销:确保嵌套和局部类的使用不会引入不必要的开销
嵌套类和局部类的设计原则
单一职责原则:
- 每个嵌套类和局部类应该有单一的职责
- 避免创建过于复杂的嵌套类层次结构
封装原则:
- 使用嵌套类来封装实现细节
- 只暴露必要的接口给外部代码
最小惊讶原则:
- 嵌套类和局部类的行为应该符合用户的预期
- 避免创建令人惊讶的嵌套类层次结构
代码重用:
- 当多个函数需要相同的局部类时,考虑将其提升为嵌套类
- 当多个类需要相同的嵌套类时,考虑将其提升为独立的类
嵌套类和局部类的应用场景
嵌套类的应用场景:
- 迭代器实现:为容器类实现迭代器
- 策略模式:实现不同的策略算法
- 工厂模式:实现对象创建的工厂方法
- 适配器模式:实现接口适配器
- 内部数据结构:实现内部使用的数据结构
局部类的应用场景:
- 临时数据处理:处理函数内部的临时数据
- 回调对象:创建临时的回调对象
- 异常处理:创建临时的异常类
- 测试代码:在测试函数中创建测试用的类
嵌套类和局部类与现代C++
C++11的lambda表达式:
- lambda表达式提供了一种更简洁的方式来创建临时的函数对象
- 对于简单的场景,lambda表达式可以替代局部类
- 对于复杂的场景,局部类仍然是必要的
C++11的嵌套类改进:
- C++11允许在模板类中定义嵌套类模板
- C++11允许在嵌套类中使用外部类的类型参数
C++14的泛型lambda:
- 泛型lambda提供了更灵活的类型推导
- 进一步减少了对局部类的需求
嵌套类和局部类的陷阱
嵌套类的陷阱:
- 过度使用:避免过度使用嵌套类,以免使代码变得复杂
- 循环依赖:避免嵌套类和外部类之间的循环依赖
- 访问权限:注意嵌套类的访问权限,确保它们被正确使用
局部类的陷阱:
- 作用域限制:记住局部类的作用域被限制在函数内部
- 静态成员:局部类不能有静态成员
- 外部变量访问:局部类只能访问函数中的静态变量和外部变量
性能陷阱:
- 过度内联:避免在局部类中定义过于复杂的内联函数
- 栈溢出:避免在局部类中创建过大的对象,以免导致栈溢出
嵌套类和局部类是C++中强大的特性,它们提供了一种封装和组织代码的机制。正确使用嵌套类和局部类可以提高代码的可读性、可维护性和安全性。然而,过度使用或不当使用它们可能会使代码变得复杂和难以理解。在实际编程中,应该根据具体情况权衡使用嵌套类和局部类的利弊。
类的模板
类模板的基本概念
类模板允许我们创建通用的类,能够处理不同类型的数据。
1 |
|
类模板的特化
类模板特化允许我们为特定类型提供自定义实现。
1 |
|
类的继承和多态高级特性
虚析构函数
虚析构函数确保在删除指向派生类对象的基类指针时,能够正确调用派生类的析构函数。
1 |
|
纯虚函数和抽象类
纯虚函数是没有实现的虚函数,包含纯虚函数的类是抽象类,不能实例化。
1 |
|
虚函数和多态
虚函数允许派生类重写基类的方法,实现多态。
1 |
|
类的设计原则
封装
封装是将数据和操作数据的方法组合在一起,对外部隐藏实现细节。
继承
继承允许我们创建基于现有类的新类,重用代码并扩展功能。
多态
多态允许我们使用基类指针或引用指向派生类对象,调用派生类的方法。
抽象
抽象是通过抽象类和纯虚函数,定义接口而不提供实现。
组合
组合是将一个类的对象作为另一个类的成员,实现代码重用。
聚合
聚合是一种特殊的组合,其中成员对象的生命周期独立于容器对象。
最小特权原则
只授予类和函数必要的访问权限,避免不必要的暴露。
单一职责原则
一个类应该只有一个引起它变化的原因,专注于单一功能。
开放/封闭原则
类应该对扩展开放,对修改封闭。
里氏替换原则
派生类应该可以替换其基类,而不破坏程序的正确性。
接口隔离原则
客户端不应该依赖它不使用的接口。
依赖倒置原则
高层模块不应该依赖低层模块,两者都应该依赖抽象。
总结
本章介绍了类的高级特性,包括:
- 友元:友元函数、友元类和成员函数作为友元
- 静态成员:静态成员变量和静态成员函数
- 常量成员:常量成员变量和常量成员函数
- 类型转换:转换构造函数和类型转换运算符
- 嵌套和局部类:嵌套类和局部类的定义和使用
- 类模板:类模板的基本概念和特化
- 继承和多态高级特性:虚析构函数、纯虚函数和抽象类
- 类的设计原则:封装、继承、多态、抽象、组合、聚合等设计原则
这些高级特性使C++的类更加灵活和强大,能够适应各种复杂的编程需求。通过合理使用这些特性,你可以设计出更加模块化、可维护和可扩展的代码。
类的高级特性是C++的重要组成部分,也是成为优秀C++程序员的必备知识。通过不断学习和实践,你会逐渐掌握这些特性的使用技巧,并能够设计出更加优雅和高效的类。



