友元

友元(friend)是C++中的一种特殊机制,它允许一个类将对其非公有成员(private和protected)的访问权授予指定的函数或类。友元打破了类的封装性,但提供了更高的灵活性和运行效率。

友元分为三种类型:

  1. 友元函数

  2. 友元类

  3. 友元成员函数

友元函数

友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员的非成员函数。

在类中声明友元函数的语法如下:

友元类

友元类是指一个类可以访问另一个类的所有成员(包括私有成员和保护成员)的类。当一个类被声明为另一个类的友元时,这个类的所有成员函数都成为另一个类的友元函数。

友元成员函数

友元成员函数是指将一个类的某个成员函数声明为另一个类的友元,使该成员函数可以访问另一个类的私有成员。

友元的使用场景

1. 运算符重载

当需要重载某些运算符(如<<>>等)时,这些运算符通常需要访问类的私有成员,此时可以将它们声明为友元函数。

2. 需要访问多个类的私有成员

当一个函数需要同时访问多个类的私有成员时,可以将该函数声明为这些类的友元。

3. 提高性能

在某些情况下,特别是在对某些成员函数多次调用时,由于参数传递、类型检查和安全性检查等都需要时间开销,使用友元可以提高程序的运行效率。

4. 辅助类和工具类

当设计辅助类或工具类来操作主类的内部数据时,可以将辅助类声明为主类的友元。

友元的特性

  1. 友元关系不能被继承:如果类B是类A的友元,类C继承自类B,类C不会自动成为类A的友元。

  2. 友元关系是单向的:如果类B是类A的友元,类A不一定是类B的友元,除非有明确声明。

  3. 友元关系不具有传递性:如果类B是类A的友元,类C是类B的友元,类C不会自动成为类A的友元。

  4. 友元声明位置:友元声明可以出现在类中的任何地方(private、protected或public部分),不受访问控制影响。

优点

  1. 提高程序运行效率:减少了类型检查和安全性检查等时间开销。

  2. 增加灵活性:使程序可以在封装和快速性方面做合理选择。

  3. 便于实现需要访问多个类私有成员的功能:可以灵活地实现需要访问若干类的私有或受保护成员才能完成的任务。

  4. 便于与非面向对象语言混合编程:便于与其他不支持类概念的语言(如C语言、汇编等)进行混合编程。

缺点

  1. 破坏封装性:友元机制破坏了类的封装性和数据隐藏性,使得非成员函数可以访问类的私有成员。

  2. 降低可维护性:过度使用友元会使代码结构变得复杂,难以维护。

  3. 增加耦合性:友元增加了类之间的耦合度,不利于代码的模块化。

Tip

  1. 谨慎使用:为了确保数据的完整性以及数据封装与隐藏的原则,建议尽量不使用或少使用友元。

  2. 合理场景:在需要提高性能、实现运算符重载或需要访问多个类私有成员的情况下,可以考虑使用友元。

  3. 友元声明位置:通常,将友元声明成组地放在类定义的开始或结尾是个好主意,以便于代码阅读和维护。

  4. 避免过度使用:过度使用友元会使代码难以理解和维护,应该在确实需要的情况下才使用。

运算符重载

+运算符重载

加号(+)是一个典型的双目运算符,需要两个操作数:一个在加号前,一个在加号后。在C++中,我们可以通过两种方式实现+运算符的重载:

  1. 类成员函数方式

  2. 全局函数方式

通过类成员函数实现+运算符重载

当使用类成员函数实现+运算符重载时,函数原型如下:

这种方式下,左操作数是调用该成员函数的对象(this),右操作数作为参数传入。

在上面的例子中,执行p1 + p2时,实际上调用的是p1.operator+(p2),返回一个新的Person对象,其m_age值为p1和p2的m_age之和。

通过全局函数实现+运算符重载

全局函数方式的运算符重载函数原型如下:

这种方式下,左右操作数都作为参数传入函数。

Tip

  1. 成员函数方式

    • 左操作数必须是类的对象

    • 可以访问类的私有成员

    • 适合当左操作数必须是该类对象的情况

  2. 全局函数方式

    • 左右操作数可以是不同类型

    • 需要访问私有成员时,可以将函数声明为友元

    • 适合需要支持交换律的情况(如 a+b = b+a)

    • 适合左操作数不是该类对象的情况(如 5 + obj)

Note

  1. 不能改变运算符的优先级和结合性

  2. 不能改变运算符的操作数个数

  3. 至少有一个操作数是用户定义的类型

  4. 不能创建新的运算符

  5. 某些运算符不能被重载(如 :: 、. 、.* 、?: 等)

我将搜索关于C++中+=运算符重载的相关信息,以便提供详细的解释和示例代码。

toolName: web_search
status: success
query: C++ += 运算符重载 示例

+=运算符重载

重载+=运算符的基本语法是:

作为类成员函数

作为成员函数重载+=运算符时,左操作数隐式地是当前对象(this指针),右操作数作为参数传入。

作为友元函数

虽然+=运算符通常作为成员函数实现,但在某些情况下也可以作为友元函数实现:

Note

  1. +=运算符重载函数通常返回对象的引用(&),这样可以支持连续操作,如a += b += c

  2. 重载+=运算符时,应该考虑同时重载+运算符,保持一致性。

  3. 对于自定义类,重载+=运算符可以提高代码的可读性和直观性。

  4. 返回引用类型可以避免创建不必要的临时对象,提高性能。

运行结果:

++运算符重载

++运算符有两种形式:前置++(如++a)和后置++(如a++)。这两种形式在行为和实现上有明显区别。

  1. 前置++:先增加变量的值,然后返回增加后的变量

  2. 后置++:先返回变量的当前值,然后再增加变量的值

C++通过函数参数来区分前置和后置++运算符重载

前置++通常比后置++更高效 ,因为:

  1. 前置++

    • 直接修改对象

    • 返回对象引用

    • 无需创建临时对象

  2. 后置++

    • 需要创建临时对象保存原始状态

    • 修改对象

    • 返回临时对象

对于简单的内置类型(如int),性能差异很小。但对于复杂的用户定义类型,前置++的性能优势更为明显。

Tip

  1. 同时实现前置和后置版本以保持一致性

  2. 优先使用前置++,特别是对于迭代器和复杂对象

  3. 前置++返回引用,后置++返回值

  4. 后置++的实现应该调用前置++,以避免代码重复

Note

  1. 前置++:T& operator++(),先增加再返回引用

  2. 后置++:T operator++(int),先返回副本再增加

[]运算符重载

[] 运算符(下标运算符)是 C++ 中常用的运算符之一,它允许我们像访问数组元素一样访问自定义类型的对象。通过重载 [] 运算符,我们可以为自定义类实现类似数组的访问语法,使代码更加直观和易于理解。

[] 运算符重载的基本语法如下:

具体来说,有两种常见的形式:

Note

  1. 必须是成员函数[] 运算符重载必须定义为类的成员函数,不能是全局函数。

  2. 通常返回引用:为了支持赋值操作(如 obj[i] = value),通常返回引用类型。

  3. 提供 const 版本:为了支持 const 对象的访问,通常还需要提供一个 const 版本的重载。

  4. 参数类型灵活:参数通常是整数类型(如 int),但也可以是其他类型(如字符串)。

  5. 边界检查:通常需要进行边界检查,防止越界访问。

简单的数组封装类

使用非整数类型作为索引

实现多维数组访问

代理类模式

有时我们需要在访问元素时执行特殊操作(如写时复制、延迟计算等),可以使用代理类模式:

Note

  1. 内存管理:如果类管理动态内存,确保正确处理内存分配和释放,避免内存泄漏。

  2. 边界检查:始终进行边界检查,防止越界访问导致的未定义行为。

  3. 返回引用:通常返回引用以支持赋值操作,但要确保引用的对象生命周期合适。

  4. const 正确性:提供 const 和非 const 版本的重载,以支持不同场景的使用。

  5. 异常安全:适当使用异常处理机制,确保在异常情况下不会导致资源泄漏。

<<输出运算符重载

在C++中,<<运算符原本是位左移运算符,但在C++标准库中被重载用于输出流操作。通过重载这个运算符,我们可以为自定义类型定义输出格式,使其能够直接与std::cout等输出流一起使用。

1. 友元函数

2. 成员函数

成员函数方式不常用,因为它改变了运算符的使用方式(obj << cout而不是cout << obj)。

Important

  1. 返回类型:返回std::ostream&引用,允许链式调用(如cout << obj1 << obj2

  2. 参数

    • 第一个参数是输出流的引用

    • 第二个参数通常是要输出的对象的常量引用

  3. const修饰:第二个参数通常用const修饰,表示不会修改对象

  4. 友元声明:通常在类内声明为友元,以访问私有成员

在继承体系中,派生类的<<运算符可以调用基类的<<运算符:

>>输入运算符重载

在C++中,>>运算符原本是位右移运算符,但在C++标准库中被重载用于输入流操作。通过重载这个运算符,我们可以为自定义类型定义输入格式,使其能够直接与std::cin等输入流一起使用。

1. 友元函数

2. 成员函数

成员函数方式不常用,因为它改变了运算符的使用方式(obj >> cin而不是cin >> obj)。

Important

  1. 返回类型:返回std::istream&引用,允许链式调用(如cin >> obj1 >> obj2

  2. 参数

    • 第一个参数是输入流的引用

    • 第二个参数是要输入的对象的非常量引用(因为需要修改对象)

  3. 错误处理:应该检查输入操作是否成功,并适当处理错误

  4. 友元声明:通常在类内声明为友元,以访问私有成员

  5. 输入验证:通常需要验证输入数据的有效性

成员访问运算符

C++中有两种主要的成员访问运算符:点运算符(.)和箭头运算符(->)。这两个运算符用于访问类或结构体的成员(包括变量和函数)。

点运算符 (.)

点运算符用于通过对象直接访问其成员。

箭头运算符 (->)

箭头运算符用于通过指针访问对象的成员。

箭头运算符(->)可以被重载,但有特殊规则:

重载箭头运算符时:

箭头运算符重载在两层和三层结构下的使用

在多层结构下,箭头运算符的重载可以实现链式调用,这在智能指针和代理类设计中非常有用。

箭头运算符重载有几个特殊规则:

  1. 必须是类的成员函数

  2. 不能有参数(是一个无参数的一元运算符)

  3. 必须返回指针或者另一个重载了箭头运算符的对象

两层结构

两层结构是指一个类重载箭头运算符,返回一个指针。

当编译器看到sp->doSomething()时,它会:

  1. 调用sp.operator->(),得到一个Resource*指针

  2. 对该指针应用箭头运算符,访问doSomething()方法

三层结构(链式调用)

三层结构是指一个类重载箭头运算符,返回另一个重载了箭头运算符的对象,形成链式调用。

执行l1->doSomething()时,编译器会:

  1. 调用l1.operator->(),得到一个Level2对象

  2. 对该对象调用operator->(),得到一个Level3对象

  3. 对该对象调用operator->(),得到一个Resource*指针

  4. 对该指针应用箭头运算符,访问doSomething()方法

实际应用场景

1. 智能指针

2. 代理模式

3. 延迟加载

Note

  1. 内存管理:在多层结构中,需要明确每一层的内存管理责任,避免内存泄漏。

  2. 返回值类型:链式调用中,中间层返回的是对象而非引用,可能导致性能问题。考虑返回引用以避免不必要的对象复制。

  3. 递归终止:链式调用必须最终返回一个指针,否则会导致编译错误

  4. 线程安全:在多线程环境中,需要考虑箭头运算符重载函数的线程安全性。

  5. 异常安全:箭头运算符重载函数可能抛出异常,需要确保异常安全。

迭代器与过滤器

函数对象(Functor)

函数对象是C++中一种特殊的对象,它可以像函数一样被调用。从技术上讲,函数对象是一个重载了函数调用运算符operator()的类的实例。

基本概念

函数对象是通过重载operator()(函数调用运算符)来实现的类对象,使其行为类似于函数。

相比普通函数,函数对象有以下优势:

1. 可以保存状态

2. 可以有多个重载版本的调用运算符

3. 可以作为模板参数传递

4. 内联优化

编译器通常可以将函数对象的调用内联化,从而提高性能。

函数对象的类型

1. 一元函数对象(Unary Functor)

接受一个参数的函数对象。

2. 二元函数对象(Binary Functor)

接受两个参数的函数对象。

3. 谓词函数对象(Predicate Functor)

返回布尔值的函数对象,常用于条件判断。

标准库中的函数对象

C++标准库在<functional>头文件中提供了许多预定义的函数对象:

1. 算术运算符

2. 比较运算符

3. 逻辑运算符

函数对象在STL算法中的应用

函数对象在STL算法中被广泛使用,特别是作为自定义比较器或转换器。

1. 排序算法

2. 查找算法

3. 转换算法

函数对象与Lambda表达式

C++11引入的Lambda表达式可以看作是函数对象的一种简化写法。

函数对象与std::function

C++11引入的std::function是一个通用的函数包装器,可以包装任何可调用对象,包括函数对象。

普通函数指针

函数指针是C++中一种特殊类型的指针,它指向函数而不是数据。函数指针允许我们在运行时选择要调用的函数,实现回调机制和策略模式等设计模式。

函数指针的声明语法如下:

例如,声明一个指向接受两个整数并返回整数的函数的指针:

基本用法

1. 定义和初始化函数指针

2. 作为函数参数

函数指针常用作函数参数,实现回调机制:

3. 作为函数返回值

函数也可以返回函数指针:

函数指针的语法可能比较复杂,可以使用typedef或C++11的using来简化:

函数指针数组

可以创建函数指针数组,存储多个函数指针:

函数指针与std::function

C++11引入的std::function是一个通用的函数包装器,可以存储、复制和调用任何可调用目标,包括函数指针:

函数指针与回调函数

函数指针常用于实现回调机制,例如在事件处理中:

函数指针与策略模式

函数指针可以用于实现策略模式,允许在运行时选择算法:

成员函数指针

成员函数指针是C++中一种特殊类型的函数指针,用于指向类的成员函数。与普通函数指针不同,成员函数指针需要考虑类的上下文,因此其语法和使用方式都更为复杂。

基本概念与语法

成员函数指针的基本声明语法如下:

例如,声明一个指向MyClass类中接受一个int参数并返回double的成员函数的指针:

将成员函数地址赋给成员函数指针:

注意:虽然对于普通函数指针,&运算符是可选的,但对于成员函数指针,&运算符是必需的。

调用成员函数指针需要一个类的实例(或指向实例的指针),使用特殊的调用运算符:

详细示例

使用typedef简化语法

高级应用

1. 成员函数指针数组

2. 在类中存储成员函数指针

3. 虚成员函数指针

4. 常量成员函数指针

对于const成员函数,需要特别声明指针类型:

5. 与std::function和std::bind结合使用

在现代C++中,可以使用std::functionstd::bind简化成员函数指针的使用:

6. 回调系统

7. 命令模式实现

Note

  1. 性能考虑:成员函数指针的调用通常比普通函数指针或直接函数调用慢,因为需要额外的间接寻址。

  2. 类型安全:使用typedefusing定义成员函数指针类型可以提高代码可读性和类型安全性。

  3. 现代替代方案:在现代C++中,考虑使用std::functionstd::bind或lambda表达式作为更灵活的替代方案。

  4. 多态行为:成员函数指针可以指向虚函数,并且会遵循C++的多态规则。

  5. 内存管理:在使用成员函数指针时,确保指向的对象在调用时仍然有效,避免悬垂指针问题。

  6. 调试难度:成员函数指针的语法复杂,可能导致调试困难,使用时应谨慎。

类型转换函数

类型转换函数是一种特殊的成员函数,具有以下特点:

基本语法:

一个类可以定义多个类型转换函数:

为了防止意外的隐式转换,可以使用explicit关键字:

类型转换函数也可以返回引用:

可以转换为用户自定义类型:

Note

类型转换函数可能导致二义性问题:

构造函数和类型转换函数都可以实现类型转换,但方向相反:

Tip

  • 只在有明确语义的情况下使用类型转换函数

  • 对可能导致数据丢失的转换使用explicit

  • 避免定义过多的类型转换函数,以减少二义性

  • 确保转换的行为符合直觉

  • 考虑使用命名函数作为替代(如toInt()toString()等)

嵌套类

嵌套类是在另一个类内部定义的类。嵌套类是外部类的成员,可以访问外部类的私有和保护成员(在某些条件下)。嵌套类提供了更好的封装性和逻辑组织。

基本概念和语法

嵌套类的特性

作用域和命名

前向声明

应用场景

迭代器模式

状态机模式

建造者模式

嵌套类与外部类的关系

访问外部类成员

友元关系

模板嵌套类


避免过度嵌套

合理使用访问控制

单例对象自动释放

利用另一个对象的生命周期管理资源

嵌套类+静态对象

atexit+destroy

atexit+pthread_once+destroy

智能指针

std::unique_ptr

std::shared_ptr

Meyer's Singleton

利用C++11的线程安全静态局部变量特性:

RAII 和自定义删除器

线程安全的单例模板

std::string的底层实现

std::string是C++标准库中最常用的字符串类,其底层实现经历了多个版本的演进。

传统实现

现代C++标准库实现通常采用以下三种策略之一。

Small String Optimization (SSO)

这是目前最流行的优化策略,用于避免小字符串的动态内存分配:

SSO的工作原理

Copy-on-Write (COW) 实现

这是较老的实现策略,现在已不常用(C++11后基本废弃):

现代实现示例(类似libstdc++)

std::string的底层实现经历了从简单的指针+长度+容量结构,到COW(写时复制),再到现代的SSO(小字符串优化)的演进。现代实现主要特点:

  1. SSO优化:小字符串直接存储在对象内部,避免堆分配

  2. 联合体设计:巧妙使用union节省内存空间

  3. 标志位技巧:使用最高位区分长短字符串模式

  4. 内存对齐:优化内存访问性能

  5. 增长策略:通常采用2倍增长避免频繁重新分配

这种设计在保持接口简单的同时,大大提升了字符串操作的性能,特别是对于常见的短字符串场景。