一、 友元(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个):
.(成员访问运算符).*(成员指针访问运算符)::(作用域解析运算符)?:(三目条件运算符)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)主要包括:
- 普通函数和静态成员函数。
- 函数指针。
- 函数对象(重载了
operator()的类对象)。 - Lambda 表达式(本质是编译器生成的匿名函数对象)。
std::bind生成的绑定对象。- 成员函数指针(需配合对象使用)。
13. 嵌套类的访问规则是什么?
- 外部类外部 -> 内部类:取决于内部类本身的访问修饰符。如果内部类是
public的,外部可以通过外部类::内部类来实例化;如果是private或protected,外部无法访问。 - 外部类 -> 内部类:外部类没有直接访问内部类非静态成员的特权(除非是友元)。外部类仅能像普通类一样访问内部类的
public成员。外部类可以声明内部类的对象。 - 内部类 -> 外部类:内部类没有对外部类的特殊访问权限。内部类不能直接访问外部类的非静态成员(只能访问外部类的静态成员、类型别名等)。若要访问外部类的非静态成员,必须通过外部类的对象指针或引用,且该成员必须是外部类授予了友元权限的。
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. 核心思想是什么?
上述概念的共同核心思想是:
- 延迟计算(Lazy Evaluation):把耗时的操作(如内存分配、数据拷贝、资源加载)推迟到不得不执行的时候。
- 按需分配(On-Demand Allocation):只在真正需要资源的时候才去索取,不需要绝不浪费。
- 资源利用率最大化:通过共享数据(如 COW)减少冗余内存占用,提升系统的整体吞吐量。
总结来说:无论是懒加载还是写时复制,本质上都是在时间(首次访问的延迟)和空间(内存占用、启动速度)之间做权衡。明白这些思想,有助于我们在架构设计中写出更高效、更节约资源的代码。