Limesn
Limesn
发布于 2026-09-18 / 21 阅读
1

第五章 运算符重载

一、 友元(Friend)相关

友元有什么作用? 友元有哪些形式?

1. 友元有什么作用?

  • 突破封装性:在C++中,类的私有(private)和保护(protected)成员只能被类自身的成员函数访问。友元提供了一种机制,允许外部的函数或另一个类访问这些私有成员。
  • 提高效率:在某些频繁访问私有成员的场景下,使用友元可以避免频繁调用公共的 get/set 接口,从而减少函数调用的开销,提高程序运行效率。
  • 方便运算符重载:在重载某些运算符(如 <<、>> 或需要隐式转换左操作数的二元运算符)时,通常需要将其声明为友元函数,以便直接访问类的私有数据。

2. 友元有哪些形式?

  • 友元函数(普通函数):在类中声明,但定义在类外部的普通函数。
  • 友元类:将一个类(例如类B)声明为另一个类(类A)的友元,那么类B的所有成员函数都可以访问类A的私有和保护成员。
  • 友元成员函数:只将另一个类中的某一个特定成员函数声明为友元,而不是整个类。

⚠️ 核心注意点:友元关系是单向的(A是B的友元,不代表B是A的友元),且不能被继承,也不具有传递性(A是B的友元,B是C的友元,不代表A是C的友元)。


二、 运算符重载(Operator Overloading)相关

3. 哪些运算符能重载?哪些不能?

  • 能重载的运算符:绝大多数运算符都可以重载。包括算术运算符(+ - * / %)、关系运算符(== != < >)、逻辑运算符(&& || !)、位运算符(& | ^ ~ << >>)、赋值运算符(= += -=)、下标运算符([])、函数调用运算符(()等。
  • 不能重载的运算符(共5个):
    1. . (成员访问运算符)
    2. .* (成员指针访问运算符)
    3. :: (作用域解析运算符)
    4. ?: (三目条件运算符)
    5. sizeof (长度运算符)
    • 原因:这些运算符如果重载会引发严重的语义歧义,或者破坏C++的基础语法机制。

4. 运算符重载的本质是什么?

  • 运算符重载的本质是函数重载。当编译器遇到被重载的运算符时,它会将其转换为对特定函数的调用。这个函数的名字是由 operator 关键字加上要重载的运算符符号组成的(例如:operator+、operator<<)。它同样是遵循C++函数重载的规则(参数列表不同,实现不同)。

5. 运算符重载的形式有哪些?

  • 类的成员函数形式:作为类的非静态成员函数重载。此时左操作数隐含为当前对象(this指针)。
  • 类的非成员函数(通常为友元函数)形式:定义在类外部。如果是普通函数,可能需要通过公有接口访问私有成员;如果声明为友元函数,则可以直接访问私有成员。左操作数必须作为函数的第一个参数显式传递。

6. 什么情况下才需要进行运算符重载?只有内置类型可以重载吗?

  • 什么时候需要:当我们需要让**自定义类型(类/结构体)**的对象,像内置类型一样进行直观的操作时。例如:两个复数对象相加(c1 + c2),或者两个字符串对象拼接。重载的目的是为了让代码更直观、更易读。
  • 只有内置类型可以重载吗? 不是。 恰恰相反,内置类型(如 int, double)的运算符不能重载。运算符重载必须是针对自定义类型的,或者重载的运算符中至少有一个操作数是自定义类型(类、结构体或枚举)。如果试图对两个内置类型重载(如重定义 int + int),编译器会报错。

7. 运算符重载时要考虑哪些因素?

  • 语义一致性:重载后的运算符功能应当符合人们的直觉,不要重载 + 去实现减法,否则代码极其难以维护。
  • 是否作为成员函数:C++规定,赋值运算符 =、下标运算符 []、函数调用运算符 ()、成员访问箭头 -> 必须作为成员函数重载。其他运算符则灵活选择。
  • 返回值类型:要考虑是否返回引用(如 += 通常返回引用以支持链式操作 a += b += c),是否返回 const 对象(防止 (a + b) = c 这种非法操作)。
  • 友元与成员函数的选择:对于需要隐式转换第一个操作数的情况(例如 2 + complexObj),通常需要将运算符重载为非成员函数(往往配合友元);如果修改的是自身对象状态(如 +=),通常作为成员函数更合适。
  • 不要过度重载:重载运算符应当保持代码的清晰度,如果逻辑复杂到难以理解,不如定义一个具有明确命名的普通成员函数(如 add()、concat())。
  • 效率和异常安全:在实现过程中,注意对象的拷贝开销,尽量使用常量引用传递参数,并考虑异常安全的处理。

三、 资源管理与单例模式

8. 用一个类型去管理另一个资源类型时,如何实现资源的自动释放?原理是什么?

  • 实现方式:通过 RAII(Resource Acquisition Is Initialization,资源获取即初始化) 机制。具体做法是将资源的生命周期与一个栈对象的生命周期绑定。在对象的构造函数中获取资源,在析构函数中释放资源。
  • 原理:C++保证当栈对象离开其作用域时,其析构函数会被自动调用(无论是因为正常执行结束,还是因为抛出异常发生栈展开)。因此,将资源封装在类的析构函数中,就能确保资源在对象销毁时被自动释放,无需手动调用 delete 或 close。
  • 典型应用:智能指针(std::unique_ptr、std::shared_ptr)、std::lock_guard(管理锁)。

四、 指针、函数对象与嵌套类

10. 函数指针的语法形式是什么?怎么调用?

  • 语法形式:返回类型 (*指针变量名)(参数类型列表);
    例如:int (*pFunc)(int, int); 定义了一个指向“接收两个int返回int”的函数的指针。
  • 调用方式:
// 假设有函数 int add(int a, int b);
pFunc = add; // 或 &add
int result1 = (*pFunc)(1, 2); // 方式一:显式解引用(常用)
int result2 = pFunc(1, 2);    // 方式二:隐式调用(C++允许)

11. 成员函数指针的语法形式是什么?怎么调用(两种方式)?

  • 语法形式:返回类型 (类名::*指针变量名)(参数类型列表) [const];
    例如:void (MyClass::*pMemFunc)(int);
  • 调用方式:因为成员函数需要一个隐式的 this 指针,所以必须绑定到具体的对象或对象指针上调用:
MyClass obj;
MyClass* pObj = &obj;

// 方式一:通过对象调用(使用 .*)
(obj.*pMemFunc)(10); 

// 方式二:通过对象指针调用(使用 ->*)
(pObj->*pMemFunc)(20); 

12. 什么是函数对象?可调用实体有哪些?

  • 函数对象(Functor):重载了 operator() 的类的对象。它表现得像函数一样,可以像调用函数一样调用它。相比于普通函数,函数对象可以携带状态(通过类的成员变量)。
  • 可调用实体(Callable Entities)主要包括:
    1. 普通函数和静态成员函数。
    2. 函数指针。
    3. 函数对象(重载了 operator() 的类对象)。
    4. Lambda 表达式(本质是编译器生成的匿名函数对象)。
    5. std::bind 生成的绑定对象。
    6. 成员函数指针(需配合对象使用)。

13. 嵌套类的访问规则是什么?

  1. 外部类外部 -> 内部类:取决于内部类本身的访问修饰符。如果内部类是 public 的,外部可以通过 外部类::内部类 来实例化;如果是 private 或 protected,外部无法访问。
  2. 外部类 -> 内部类:外部类没有直接访问内部类非静态成员的特权(除非是友元)。外部类仅能像普通类一样访问内部类的 public 成员。外部类可以声明内部类的对象。
  3. 内部类 -> 外部类:内部类没有对外部类的特殊访问权限。内部类不能直接访问外部类的非静态成员(只能访问外部类的静态成员、类型别名等)。若要访问外部类的非静态成员,必须通过外部类的对象指针或引用,且该成员必须是外部类授予了友元权限的。

9. 如何实现单例自动释放?掌握两种就行(要求手写出来)

方法一:静态局部变量(C++11 推荐做法,天生线程安全且自动释放)
利用 C++11 的特性,局部静态变量的初始化是线程安全的,且程序结束时自动调用析构函数。

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance; // 程序结束时自动析构
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
    ~Singleton() = default;
};

方法二:嵌套类 + 静态成员(RAII 垃圾回收机制)
在单例类内部定义一个垃圾回收类,利用静态成员在程序结束时自动析构的特性,来 delete 单例指针。

class Singleton {
public:
    static Singleton* getInstance() {
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
        return m_instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    static Singleton* m_instance;

    // 内部垃圾回收类
    class Garbo {
    public:
        ~Garbo() {
            if (Singleton::m_instance) {
                delete Singleton::m_instance;
                Singleton::m_instance = nullptr;
            }
        }
    };
    static Garbo m_garbo; // 静态成员,程序结束时自动析构
};

// 静态成员初始化
Singleton* Singleton::m_instance = nullptr;
Singleton::Garbo Singleton::m_garbo;

五、 性能优化与设计思想

14. 什么是懒加载?什么是立即加载?

这两个概念常见于对象初始化、数据库查询(ORM框架)、前端资源加载等场景。

维度立即加载(Eager Loading)懒加载(Lazy Loading / 延迟加载)
时机程序启动或对象创建时,立刻分配资源、加载数据或初始化依赖对象在创建时并不真正初始化,而是等到第一次被真正使用时才去加载
优点使用时无需等待,响应速度快;代码逻辑简单,无需考虑并发安全节省启动时间和内存资源,避免不必要的计算或网络请求
缺点启动耗时较长,可能占用大量不必要的内存(如加载了从未使用的数据)首次调用会有延迟;在多线程环境下,需要解决线程安全和双重检查锁定(Double-Checked Locking)问题
举例C++单例模式中的“饿汉式”、网页一次性加载所有图片C++单例模式中的“懒汉式”、ORM框架的延迟加载(如Hibernate/MyBatis的 lazy="true")、前端图片可视区滚动加载

15. 掌握 std::string 的写时复制(Copy-On-Write, COW)原理

写时复制的核心思想是**“共享读,独立写”**,旨在优化内存和拷贝开销。

  • 原理:当发生字符串拷贝(拷贝构造或赋值)时,std::string 并不立即分配新内存和拷贝字符数据,而是让多个对象共享同一块内存(通过引用计数记录有多少个对象共享它)。当任何一个对象需要修改内容时,才会真正分配一块新的内存,将数据深拷贝过去,并修改自己的引用。
  • ⚠️ 现代 C++ 的重要提示:
    在 C++98/03 时代,部分编译器(如老版本 GCC)的 std::string 采用过 COW。但在 C++11 标准中,std::string 已经被明确禁止使用 COW 实现。
    • 原因:多线程下,引用计数的原子操作会带来性能损耗;并且 operator[]、迭代器等接口无法完美兼容 COW(比如 auto& c = str[0] 这种操作会破坏 COW 机制)。因此,现代 C++ 更倾向于使用 SSO(短字符串优化) 和普通深拷贝。

16. 核心思想是什么?

上述概念的共同核心思想是:

  1. 延迟计算(Lazy Evaluation):把耗时的操作(如内存分配、数据拷贝、资源加载)推迟到不得不执行的时候。
  2. 按需分配(On-Demand Allocation):只在真正需要资源的时候才去索取,不需要绝不浪费。
  3. 资源利用率最大化:通过共享数据(如 COW)减少冗余内存占用,提升系统的整体吞吐量。

总结来说:无论是懒加载还是写时复制,本质上都是在时间(首次访问的延迟)和空间(内存占用、启动速度)之间做权衡。明白这些思想,有助于我们在架构设计中写出更高效、更节约资源的代码。