【C++入门精讲22】四种类型转换 + 常见设计模式 + Redis/MySQL(后端面试复习完整版)
前言 :本篇在原《【C++入门精讲22】常见设计模式》的基础上做了大幅扩充:新增 C++ 四种显式类型转换 、代理模式 ,并补入后端面试高频的 Redis / MySQL 复习要点。原有章节的知识点与代码全部保留,代码中的明显笔误已顺手修正(
sever→server、intance→instance)。
一、知识点总回顾(移动语义 + 智能指针)
核心知识点
-
移动语义
- 作用:转移对象资源所有权,避免低效深拷贝,提升性能
- 触发方式:
std::move()将对象转为右值引用 - 核心函数:移动构造函数
类名(类名&&)、移动赋值重载返回值 operator=(类名&&) - 编译器默认生成函数:无参构造、拷贝构造、移动构造、拷贝赋值、移动赋值、析构函数
-
智能指针(RAII 资源管理)
- 核心思想:利用类生命周期自动管理堆裸指针,构造获取资源,析构释放资源,防止内存泄漏
- 自定义智能指针必备接口:构造函数、析构函数、
operator->(指针访问)、operator*(解引用)、get()(获取原生指针)、reset()(手动释放资源) - C++11 标准智能指针
unique_ptr:独占式智能指针,同一资源仅能被一个指针管理shared_ptr:共享式智能指针,内部引用计数,多指针共管同一资源weak_ptr:弱引用指针,不增加引用计数 ,专门解决shared_ptr循环引用问题,必须依托shared_ptr使用
对应代码
cpp
// 综合应用
/*
* 回顾知识点:
* 1. 移动语义: 将某一个对象的数据转移到另一个对象中,避免不必要的拷贝,
* 1) std::move() 触发
* 2) 移动构造函数:类名(类名&&)
* 3) 移动赋值重载:返回类型 operator=(类名&&)
*
* 一个类编译器默认提供无参构造,拷贝构造,移动构造,拷贝赋值,移动赋值,析构函数
*
* 2. 智能指针:依据类的生命周期(构造函数,析构函数)去自动管理原生指针,也叫做裸指针
* 1) 自定义智能指针:
* 构造函数(数据类型 *):RAII(资源获取即初始化)
* 析构函数:回收原始的指针
* operator->():指针访问
* operator*():指针解引用
* 数据类型 * get():获取原始指针,返回原始指针,但原始指针的生命周期仍然受智能指针控制。
* 使用 get() 时需要小心,避免与智能指针的自动管理机制冲突。
* void reset():手动释放原始指针
*
* 2) C++11 提供的智能指针:类模板
* auto_ptr<数据类型>:放弃
* unique_ptr<数据类型>:独占
* shared_ptr<数据类型>:共享
* weak_ptr<数据类型>:弱引用
* 必须指向 shared_ptr,但是不增加引用计数
* 用于解决 shared_ptr 的循环引用问题
*/
二、C++ 四种显式类型转换
C 语言风格的 (type)expr 强制转换几乎什么都能转,编译器很难帮我们检查错误。C++ 引入了四种显式类型转换运算符,各自职责明确,见名知义、便于排查。
1. static_cast:编译期可检查的常规转换
常见用途:
- 基本数据类型之间转换(如
double → int) void*与其他指针类型之间转换- 有继承关系的指针或引用之间转换(含向上转型与向下转型)
- 调用显式转换构造函数
cpp
double value = 3.14;
int number = static_cast<int>(value); // 结果为 3
⚠️ 向下转型时不会进行运行时安全检查:基类指针实际不指向派生类对象时,转出来就是野指针。
cpp
Derived* derived = static_cast<Derived*>(base); // 不检查,后果自负
2. dynamic_cast:多态类型之间的运行时安全转换
主要用于具有多态性的类之间进行安全转换,尤其是从基类向派生类转换(安全向下转型)。
cpp
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {};
Base* base = new Derived;
Derived* derived = dynamic_cast<Derived*>(base);
if (derived != nullptr) {
// 转换成功
}
特点:
- 基类通常必须是多态类,即至少有一个虚函数(靠 RTTI 运行时类型信息实现)
- 指针转换失败时返回
nullptr - 引用转换失败时抛出
std::bad_cast - 会进行运行时类型检查,因此比
static_cast更安全,但有一定运行时开销
3. const_cast:添加或移除 const / volatile 属性
cpp
int value = 10;
const int* p1 = &value;
int* p2 = const_cast<int*>(p1);
*p2 = 20; // 合法,因为原对象 value 本身不是 const
⚠️ 特别注意 :如果对象本身定义时就是 const,移除 const 后再修改它会产生未定义行为(UB)。
cpp
const int value = 10;
int* p = const_cast<int*>(&value);
*p = 20; // 未定义行为!
4. reinterpret_cast:底层位模式的重新解释
用于进行底层、位模式层面的类型重新解释,不改变二进制数据,只换一种"看法"。
常见用途:
- 不相关的指针类型之间转换
- 指针与足够大的整数类型之间转换
- 底层系统编程和硬件操作
cpp
int value = 10;
int* p = &value;
std::uintptr_t address = reinterpret_cast<std::uintptr_t>(p);
它非常危险,因为基本不提供类型安全保证,通常只应该在底层编程中使用。
总结对比
| 转换方式 | 主要作用 | 安全性 |
|---|---|---|
static_cast |
常规、编译期可检查的转换 | 较高 |
dynamic_cast |
多态类之间的运行时安全转换 | 高 |
const_cast |
添加或移除 const/volatile |
谨慎使用 |
reinterpret_cast |
底层、不相关类型之间的重新解释 | 最低 |
简单记忆:
- 普通转换 :
static_cast - 多态安全转换 :
dynamic_cast - 修改常量属性 :
const_cast - 底层强制解释 :
reinterpret_cast
三、设计模式基础介绍
核心知识点
- 设计模式 :共 23 种经典设计模式,用于规范类与代码结构,简化设计、提升代码扩展性、可维护性。设计模式是针对常见软件设计问题总结出来的可复用解决方案。
- 三大分类:创建型模式 (单例、工厂、构建器......)、结构型模式 (代理、装饰器......)、行为型模式(观察者......)。
cpp
// 1.设计模式:类的设计模式(23种),分三大类型(创建类型,结构类型,行为类型)
// 简化类的设计,提高程序的扩展性等
四、单例设计模式
核心知识点
- 单例模式 :整个程序生命周期内,类有且仅有一个实例对象,并提供一个全局访问入口。
- 两大实现方式
- 懒汉式:第一次使用对象时才创建实例,延迟加载。
- 饿汉式:程序启动、类加载时就提前创建实例,无论是否使用。
- 单例实现步骤
- 私有化构造函数,禁止外部创建对象;
- 删除拷贝构造、移动构造、拷贝赋值,禁止克隆对象;
- 提供
public静态成员函数,对外获取唯一实例; - 饿汉式:定义静态类对象作为唯一实例;懒汉式:在静态函数内定义局部静态对象。
常见使用场景
- 配置管理器、日志系统、数据库连接池、线程池、Redis 客户端管理器
对应代码(WebServer 完整示例)
cpp
#if 0
#include <iostream>
using namespace std;
// 定义web服务器:只创建一个类的对象
// 单例设计过程:1)私有化构造函数 2)删除拷贝构造和移动构造(整个程序只能有一个,那么就不能被克隆或移动)
// 3)提供一个public静态函数,返回一个类的对象
// 4) 懒汉式:静态成员函数中创建局部静态对象
// 5) 饿汉式:定义静态成员类对象,在静态成员函数中返回这个类的对象
class WebServer {
int port;
string root; // web资源文件的根目录
WebServer(int port = 443, string root = "/webroot") : port(port), root(root) {}
WebServer(const WebServer&) = delete;
WebServer& operator=(const WebServer&) = delete;
WebServer(WebServer&&) = delete; // 删除移动构造
public:
void start() {
cout << "WebServer Running localhost:" << port << endl;
}
void shutdown() {
cout << "WebServer Stop" << endl;
}
// 静态的成员函数的参数表尽量同构造函数的参数表相同
static WebServer& getInstance(int port, string root) {
// 懒汉式:局部的static对象,在程序运行期间,只会定义一次
// static WebServer server(port, root);
// return server;
return instance;
}
// 饿汉模式
static WebServer instance; // 只是声明
};
void runServer() {
WebServer::getInstance(8080, "/webroot").start();
}
void stopServer() {
WebServer::getInstance(8080, "/webroot").shutdown();
}
WebServer WebServer::instance(8080); // 定义
int main() {
WebServer::getInstance(8080, "/webroot").start();
}
#endif
面试补充:Meyers Singleton(最推荐的写法)
cpp
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 局部静态对象
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
cpp
Singleton& instance = Singleton::getInstance();
关键点 :从 C++11 开始,函数内静态变量的初始化是线程安全 的,因此这种写法一般称为 Meyers Singleton,既天然线程安全又不用手写双检锁,面试首选答案。
优缺点
优点:
- 保证实例唯一;节省资源;方便全局访问。
缺点:
- 相当于引入全局状态;类之间容易产生强耦合;不利于单元测试;使用不当可能违背单一职责原则。
五、工厂设计模式
5.1 模式分类 & 核心知识点
- 简单工厂
- 特点:一个工厂类生产所有产品,产品类型固定,新增产品需要修改工厂代码,违反开闭原则。
- 开闭原则:对扩展开放,对原有代码修改关闭。
- 工厂方法
- 特点:一个工厂只负责生产一种产品,不同产品对应专属工厂,遵循开闭原则;但类的数量会增加。
- 抽象工厂
- 特点:一个工厂可以生产同系列 / 多类组合产品(如手机、平板、电脑),解决工厂方法单一产品的局限性。也叫创建"产品族",例如:Windows 风格按钮、文本框一套;Linux 风格按钮、文本框一套。
- 通用结构:抽象产品接口 → 具体产品类 → 抽象工厂接口 → 具体工厂类。
- 核心思想 :把对象的创建过程 封装起来,调用者只关心"我要什么",不关心"具体怎么创建",不需要直接
new具体对象。
优点:创建和使用解耦;隐藏复杂的对象创建过程;便于扩展和统一管理对象。
5.2 对应代码
cpp
#if 0
#include <iostream>
#include <memory>
#include <string>
using namespace std;
// 3.工厂设计模式:思考产品如何通过工厂生产出来的
// 简单工厂:工厂只需要知道用户要什么产品类型,即可以生产出来,生产的东西只能是固定的,
// 不能生产它没有的,违反了开闭原则。
// 开闭原则:在不改现有代码的基础上,对扩展开放,对修改关闭
// 工厂方法:不同的产品可以由不同的工厂生产出来,例如:苹果手机由苹果工厂生产(只能生产手机),
// 小米手机由小米工厂生产
// 抽象工厂:不同的工厂可以生成相同类型或组合的产品,即一个工厂可以生产多个不同类型的产品,
// 例如:苹果工厂可以生产苹果手机、苹果电脑等,可以生产组合或套装产品(或同系列)
// 产品类:抽象类或接口
class IPhone {
public:
// 产品的功能
virtual void tell(string num) = 0;
};
// 定义具体的产品:某一种类型
class HWPhone : public IPhone {
public:
void tell(string num) {
cout << "HWPhone tell: " << num << endl;
}
};
class VivoPhone : public IPhone {
public:
void tell(string num) {
cout << "VivoPhone tell: " << num << endl;
}
};
class MIPhone : public IPhone {
public:
void tell(string num) {
cout << "MIPhone tell: " << num << endl;
}
};
// 简单工厂:产品类型固定,定义好之后无法扩展
class PhoneFactory {
public:
enum Type {
HW,
VIVO,
MI
};
// 依据用户提供的产品类型来生产
static shared_ptr<IPhone> create(Type type = HW) {
switch (type) {
case HW: return shared_ptr<IPhone>(new HWPhone());
case VIVO: return shared_ptr<IPhone>(new VivoPhone());
default: return shared_ptr<IPhone>(new MIPhone());
}
}
};
#if 0
int main() {
shared_ptr<IPhone> phone = PhoneFactory::create();
phone->tell("110");
shared_ptr<IPhone> phone2 = PhoneFactory::create(PhoneFactory::VIVO);
phone2->tell("120");
return 0;
}
#endif
// 工厂方法设计模式:每一个工厂只负责一种产品的生产
class IFactory {
public:
virtual shared_ptr<IPhone> create() = 0;
};
class HWFactory : public IFactory {
public:
shared_ptr<IPhone> create() {
return shared_ptr<IPhone>(new HWPhone());
}
};
#if 0
int main() {
// 需要HWPhone,只需要通过它的工厂生产即可。
shared_ptr<IPhone> phone = HWFactory().create();
phone->tell("110");
return 0;
}
#endif
#if 1
// 抽象工厂:工厂可以生产同类型或品牌的系列产品
class IPad {
public:
virtual void playGame() = 0;
};
class IComputer { // 电脑产品接口
public:
virtual void softDev() = 0;
};
class HWPad : public IPad {
public:
void playGame() {
cout << "HWPad playGame()" << endl;
}
};
class XMPad : public IPad {
public:
void playGame() {
cout << "XMPad playGame()" << endl;
}
};
class VIvoPad : public IPad {
public:
void playGame() {
cout << "VIvoPad playGame()" << endl;
}
};
class HWComputer : public IComputer {
public:
void softDev() {
cout << "HW" << endl;
}
};
class XMComputer : public IComputer {
public:
void softDev() {
cout << "XM" << endl;
}
};
class VIvoComputer : public IComputer {
public:
void softDev() {
cout << "VIvo" << endl;
}
};
// 抽象工厂的接口类
class IFactory {
public:
virtual shared_ptr<IPhone> createPhone() = 0;
virtual shared_ptr<IPad> createPad() = 0;
virtual shared_ptr<IComputer> createComputer() = 0;
};
class HWFactory : public IFactory {
public:
shared_ptr<IPhone> createPhone() {
return shared_ptr<IPhone>(new HWPhone());
}
shared_ptr<IPad> createPad() {
return shared_ptr<IPad>(new HWPad());
}
shared_ptr<IComputer> createComputer() {
return shared_ptr<IComputer>(new HWComputer());
}
};
class XMFactory : public IFactory {
public:
shared_ptr<IPhone> createPhone() {
return shared_ptr<IPhone>(new MIPhone());
}
shared_ptr<IPad> createPad() {
return shared_ptr<IPad>(new XMPad());
}
shared_ptr<IComputer> createComputer() {
return shared_ptr<IComputer>(new XMComputer());
}
};
class VIvoFactory : public IFactory {
public:
shared_ptr<IPhone> createPhone() {
return shared_ptr<IPhone>(new VivoPhone());
}
shared_ptr<IPad> createPad() {
return shared_ptr<IPad>(new VIvoPad());
}
shared_ptr<IComputer> createComputer() {
return shared_ptr<IComputer>(new VIvoComputer());
}
};
#endif
#endif
5.3 面试补充:三种工厂一句话总结
| 模式 | 一句话总结 | 开闭原则 |
|---|---|---|
| 简单工厂 | 一个工厂根据参数生产固定产品 | ❌ 违反 |
| 工厂方法 | 一个工厂只生产一种产品,一物一厂 | ✅ 遵循 |
| 抽象工厂 | 一个工厂生产一族(组合)产品 | ✅ 遵循 |
六、代理模式 Proxy(新增)
核心知识点
- 代理模式 :为真实对象提供一个代理对象 ,客户端通过代理间接访问 真实对象;代理可以在调用真实对象之前或之后增加额外逻辑(权限检查、日志等)。
- 属于结构型设计模式。
常见场景
- 权限检查
- 日志记录
- 缓存
- 远程调用 RPC
- 延迟加载
- 事务控制
- Spring AOP(动态代理的经典应用)
示例代码
cpp
#include <iostream>
#include <memory>
class Service {
public:
virtual void request() = 0;
virtual ~Service() = default;
};
class RealService : public Service {
public:
void request() override {
std::cout << "执行真实业务\n";
}
};
class ServiceProxy : public Service {
private:
std::unique_ptr<RealService> realService;
public:
ServiceProxy() : realService(std::make_unique<RealService>()) {}
void request() override {
std::cout << "权限检查\n"; // 调用前的额外逻辑
realService->request(); // 调用真实对象
std::cout << "记录日志\n"; // 调用后的额外逻辑
}
};
常见代理类型
- 静态代理:代理类在编译前已经写好;
- 动态代理:程序运行时动态生成代理对象;
- 远程代理:代理远程服务,例如 RPC 客户端;
- 虚拟代理:需要时才创建资源较大的真实对象;
- 保护代理:控制访问权限。
面试高频:代理模式 vs 装饰器模式
- 代理模式 侧重控制对象的访问(权限、延迟、远程);
- 装饰器模式 侧重动态增强对象的功能(套一层加行为)。
- 结构几乎一样,区别在意图。
七、观察者模式
核心知识点
- 观察者模式 :俗称发布 - 订阅模式,属于行为型设计模式。
- 两大核心角色
- 主题(发布者):维护核心数据、观察者列表;提供注册 / 注销观察者、数据更新、批量通知接口。
- 观察者(订阅者):实现统一更新接口,接收主题推送的消息并响应。
- 运行逻辑:主题数据发生变化 → 主动通知所有已注册观察者 → 观察者执行自身更新逻辑。观察者是被动的:关注主题的数据变化时,被主题通知,然后执行自己的更新函数。
对应代码
cpp
#if 0
#include <iostream>
#include <vector>
#include <thread>
#include <iomanip>
#include <chrono>
#include <sstream>
using namespace std;
// 4.观察者模式:简化版的发布与订阅模式
// 组成:主题,观察者
// 主题:核心数据(天气,GPS位置,网络时间)
// 主题必须有:添加或删除观察者,更新数据函数,通知接口
// 观察者:必须存在接收主题通知的函数(如 update)
// 观察者是被动性的:当我们关注的主题数据发生变化时,会被主题通知,然后执行自己的更新函数
// 设计观察者接口类
class INTObserver {
public:
virtual void update(string timeStr) = 0;
};
// 设计主题类
class NetworkTimerServer {
string current_time; // 数据
vector<shared_ptr<INTObserver>> allOb; // 所有注册的观察者
public:
NetworkTimerServer() : current_time("") {}
// 注册观察者
void regist(shared_ptr<INTObserver> ob) {
allOb.push_back(ob);
}
// 取消注册观察者
void unregist(shared_ptr<INTObserver> ob) {
auto it = allOb.begin();
while (it != allOb.end()) {
if (it->get() == ob.get()) {
allOb.erase(it);
break;
}
}
}
void setCurrentTime(const string& timeStr) {
this->current_time = timeStr;
notify();
}
string getCurrentTime() {
return this->current_time;
}
void notify() {
// 通知所有观察者数据发生了变化
for (auto ob : allOb) {
ob->update(current_time);
}
}
};
// 设计具体的观察者
class PhoneObserver : public INTObserver {
public:
void update(string timeStr) override {
cout << "PhoneObserver Current Time: " << timeStr << endl;
}
};
class PadObserver : public INTObserver {
public:
void update(string timeStr) override {
cout << "PadObserver Current Time: " << timeStr << endl;
}
};
class WatchObserver : public INTObserver {
int i;
public:
WatchObserver() : i(0) {}
void update(string timeStr) override {
if (i % 4 == 0)
cout << "WatchObserver Current Time: " << timeStr << endl;
i++;
}
};
int main() {
unique_ptr<NetworkTimerServer> nts(new NetworkTimerServer());
shared_ptr<INTObserver> ob1 = shared_ptr<INTObserver>(new PhoneObserver());
shared_ptr<INTObserver> ob2 = shared_ptr<INTObserver>(new PadObserver());
shared_ptr<INTObserver> ob3 = shared_ptr<INTObserver>(new WatchObserver());
nts->regist(ob1);
nts->regist(ob2);
nts->regist(ob3);
// 模拟时钟
for (int i = 0; i < 10; i++) {
auto ct = chrono::system_clock::now();
time_t tt = chrono::system_clock::to_time_t(ct);
ostringstream oss;
oss << put_time(localtime(&tt), "%Y-%m-%d %H:%M:%S");
// 更新时间
nts->setCurrentTime(oss.str());
// 休息1s
this_thread::sleep_for(chrono::seconds(1));
}
return 0;
}
#endif
八、构建器模式(建造者模式)
核心知识点
- 构建器模式 :用于成员属性繁多、构造函数重载复杂的类,拆分对象创建步骤,实现流式构建。
- 实现步骤
- 私有化外部类构造函数,禁止外部直接实例化;
- 定义静态内部构建器类 Builder(内部类可以访问外部类的私有成员);
- Builder 中提供链式成员函数,逐个设置对象属性;
- 最终通过
build()方法生成并返回完整对象。
- 流式构建:成员函数返回构建器自身引用,支持连续调用。
对应原版代码
cpp
#if 0
#include <iostream>
#include <memory>
using namespace std;
// 1. 构建器模式
// 将复杂性创建对象的类,设计为分层次的创建过程。
// 复杂性创建,如:类的成员属性较多,构造函数重载也非常多
// 构建器模式的设计过程:【流式构建器】
// 1) 私有化构造函数, 隐藏类对象的创建过程
// 2) 在类内设计静态公开的成员类(内部类)为构建器类 Builder
// 3) 在Builder 类内,设计公开的成员函数,提供属性的设置为构建过程(层次)
// 最后提供一个 build() 函数返回外部类的对象。
class Computer {
string vendor; // 厂家
string cpuName; // 处理器名称
int cpuNum; // 内核数
int memorySize; // 内存大小
int ssdSize; // 硬盘大小
// ... 其他大量的属性
private:
Computer() : vendor("中国"), cpuName("Intel"), cpuNum(2), memorySize(4), ssdSize(40) {}
Computer(string vendor) : vendor(vendor), cpuName("Intel"), cpuNum(2), memorySize(4), ssdSize(40) {}
Computer(string vendor, string cpuName) : vendor(vendor), cpuName(cpuName), cpuNum(2), memorySize(4), ssdSize(40) {}
Computer(string vendor, string cpuName, int cpuNum)
: vendor(vendor), cpuName(cpuName), cpuNum(cpuNum), memorySize(4), ssdSize(40) {}
Computer(string vendor, string cpuName, int cpuNum, int memorySize)
: vendor(vendor), cpuName(cpuName), cpuNum(cpuNum), memorySize(memorySize), ssdSize(40) {}
Computer(string vendor, string cpuName, int cpuNum, int memorySize, int ssdSize)
: vendor(vendor), cpuName(cpuName), cpuNum(cpuNum), memorySize(memorySize), ssdSize(ssdSize) {}
public:
void show() {
cout << vendor << " 生产 " << cpuName << "(" << cpuNum << ") 内存 " << memorySize << "G";
cout << " ssd " << ssdSize << " G" << endl;
}
// 静态内部类:可以访问外部类的私有成员
static class Builder {
shared_ptr<Computer> computer;
public:
Builder() {
computer = shared_ptr<Computer>(new Computer());
}
// 设计构建过程成员函数:如果返回构建器类的引用,称之为流式构建
Builder& vendor(string v) {
computer->vendor = v;
return *this;
}
Builder& cpuNum(int n) {
computer->cpuNum = n;
return *this;
}
Builder& memSize(int n) {
computer->memorySize = n;
return *this;
}
Builder& ssdSize(int n) {
computer->ssdSize = n;
return *this;
}
shared_ptr<Computer> build() {
return computer;
}
};
};
int main() {
shared_ptr<Computer> c = Computer::Builder()
.cpuNum(8).memSize(32).ssdSize(1024).build();
c->show();
return 0;
}
#endif
九、后端面试扩展:Redis 核心要点(新增)
简历上写了 Redis,面试官一定会问。以下按"五种基本类型 → 持久化 → 锁 → 主从 → 缓存三兄弟"的顺序复习。
9.1 Redis 五种基本数据类型
Redis 是基于内存的键值数据库,速度快、支持持久化、支持多种数据结构。经典的五种基本类型:String、Hash、List、Set、Sorted Set(ZSet)。
1)String 字符串
可以保存字符串、整数、浮点数、JSON、二进制数据。
redis
SET name "Tom"
GET name
DEL name
EXISTS name
# 设置过期时间
SET code "123456" EX 60
EXPIRE code 60
TTL code
# 计数
SET count 0
INCR count
INCRBY count 10
DECR count
使用场景:缓存对象、分布式锁、验证码、计数器、Session。
2)Hash 哈希
一个 Key 对应多个字段和值,类似 Map<String, Map<String, String>>,适合保存对象(用户信息、商品信息)。
redis
HSET user:1 name "Tom" age 20
HGET user:1 name
HGETALL user:1
HDEL user:1 age
HEXISTS user:1 name
HINCRBY user:1 age 1
3)List 列表
按插入顺序排列的字符串集合,底层支持从两端添加和删除元素。
redis
LPUSH messages "A"
RPUSH messages "C"
LRANGE messages 0 -1
LPOP messages
RPOP messages
# 阻塞获取
BLPOP messages 10
BRPOP messages 10
使用场景:消息队列、最新消息列表、时间线、栈和队列。不过正式消息队列场景通常优先考虑 Redis Stream、RabbitMQ 或 Kafka。
4)Set 集合
无序、元素不重复。
redis
SADD tags "Java" "C++" "Redis"
SMEMBERS tags
SISMEMBER tags "Redis"
SREM tags "Java"
SCARD tags
# 集合运算
SINTER set1 set2 # 交集
SUNION set1 set2 # 并集
SDIFF set1 set2 # 差集
使用场景:用户标签、共同好友、点赞用户集合、去重、抽奖。
5)Sorted Set 有序集合(ZSet)
元素不重复,每个元素带 score,按分数排序。
redis
ZADD ranking 100 "Tom"
ZADD ranking 90 "Jack"
ZRANGE ranking 0 -1 WITHSCORES
ZREVRANGE ranking 0 -1 WITHSCORES
ZSCORE ranking "Tom"
ZRANK ranking "Tom"
ZINCRBY ranking 10 "Tom"
ZREM ranking "Jack"
使用场景:排行榜、延迟队列、按时间排序、带权重的任务管理。
9.2 Redis 持久化策略:RDB 与 AOF
RDB(快照)
在某个时间点生成整个 Redis 数据集的快照,可以理解为定期全量备份。
- 优点:文件紧凑、适合备份、恢复速度快、对性能影响较小
- 缺点:两次快照之间的数据可能丢失;数据量大时
fork子进程可能造成短暂停顿 - 适合:可接受分钟级数据丢失、需要快速恢复、定期全量备份
AOF(写命令日志)
记录每条写命令(SET / INCR / DEL......),重启后重新执行这些命令恢复数据。
三种刷盘策略:
| 策略 | 行为 | 特点 |
|---|---|---|
always |
每次写操作都同步到磁盘 | 安全性最高,性能较低 |
everysec |
每秒同步一次(最常用) | 性能和安全的折中,极端情况丢约 1 秒数据 |
no |
由操作系统决定何时刷盘 | 性能较高,安全性较低 |
AOF 重写 :文件会越来越大,Redis 根据当前数据状态生成更精简的命令集合。例如 SET count 1 + INCR count + INCR count 可压缩为 SET count 3。
RDB vs AOF 对比
| 对比项 | RDB | AOF |
|---|---|---|
| 保存方式 | 数据快照 | 写命令日志 |
| 文件大小 | 较小 | 较大 |
| 恢复速度 | 较快 | 较慢 |
| 数据安全性 | 可能丢失较多数据 | 通常最多丢失约 1 秒 |
| 适合场景 | 备份、快速恢复 | 对数据完整性要求较高 |
实际项目中经常同时开启 RDB 和 AOF 。Redis 新版本还支持混合持久化:文件开头保存 RDB 数据,后面追加 AOF 增量命令,兼顾恢复速度和数据安全性。
9.3 悲观锁与乐观锁
悲观锁
认为数据很可能被其他事务修改,所以操作之前先加锁。
MySQL 示例(FOR UPDATE 加排他锁):
sql
BEGIN;
SELECT stock FROM product WHERE id = 1 FOR UPDATE;
UPDATE product SET stock = stock - 1 WHERE id = 1;
COMMIT;
- 优点:冲突发生时处理简单,适合写冲突频繁的场景
- 缺点:阻塞其他事务、并发能力较低、加锁顺序不当可能死锁
乐观锁
认为大多数情况下不会冲突,更新时再检查数据是否被修改(通常用版本号)。
sql
SELECT id, stock, version FROM product WHERE id = 1; -- 假设 version = 5
UPDATE product
SET stock = stock - 1,
version = version + 1
WHERE id = 1
AND version = 5
AND stock > 0;
受影响行数为 0,说明数据已被其他事务修改或库存不足,程序可重试。
- 优点:不长期持有数据库锁,并发性能好,适合读多写少、冲突较少
- 缺点:冲突时需要重试,冲突频繁时性能下降,业务代码更复杂
Redis 中的乐观锁(WATCH)
redis
WATCH stock
GET stock
MULTI
DECR stock
EXEC
如果 WATCH 到 EXEC 期间 stock 被其他客户端修改,EXEC 会执行失败。注意:Redis 事务主要保证命令按顺序执行,不等同于 MySQL 的 ACID 事务,不支持传统意义上的事务回滚。
9.4 Redis 主从复制
主节点 Master 负责写,从节点 Replica 复制主节点数据、通常负责读。
大致过程:从节点连接主节点 → 主节点全量或部分同步 → 全量同步时主节点生成 RDB 并发送 → 从节点加载 RDB → 后续写命令持续复制。
- 全量复制:第一次建立复制关系或无法增量复制时,完整复制全部数据
- 增量复制:断开时间较短时,主节点把断开期间缺失的命令发给从节点(依赖复制积压缓冲区)
优点 :读写分离、提升读性能、数据冗余、为高可用打基础。
问题 :复制是异步的,可能短暂不一致;存在复制延迟;单纯主从不能自动故障转移 → 需要自动故障转移时用 Redis Sentinel(哨兵) 或 Redis Cluster。
9.5 缓存穿透、击穿、雪崩(面试必问三兄弟)
1)缓存穿透
查询数据库中根本不存在 的数据:缓存没有、数据库也没有,每次请求都打到数据库(例如恶意请求 GET /user?id=-100000)。
解决方法:
- 缓存空值 :查不到也把空结果写入缓存(
user:-100000 → null),但设较短过期时间 - 布隆过滤器 :请求先经过布隆过滤器再查缓存和数据库。特点:判断"不存在"时一定不存在;判断"可能存在"时不一定真存在;不会漏判已有元素
- 参数校验:如用户 ID 必须大于零、长度合法等
2)缓存击穿
某个访问量非常大的热点 Key 突然过期 ,大量请求同时访问数据库。特点是单个热点 Key 失效。
解决方法:
- 互斥锁:只允许一个线程查询数据库并重建缓存,其他线程等待
- 逻辑过期:缓存不直接删除数据,而是保存逻辑过期时间;发现过期后一个线程异步重建,其他请求暂时返回旧数据(牺牲短时一致性保可用性)
- 热点数据永不过期:不设物理过期时间,由后台任务主动更新
3)缓存雪崩
大量缓存 Key 在同一时间失效 ,或者 Redis 服务整体不可用,导致大量请求进入数据库。
解决方法:
- 过期时间加随机值(如基础 30 分钟 + 随机 0~5 分钟),不让大量 Key 同时过期
- Redis 主从、哨兵或集群(高可用)
- 限流、熔断和降级
- 多级缓存、热点数据提前预热
- 数据库做好容量保护
三者对比
| 问题 | 核心原因 | 典型解决方案 |
|---|---|---|
| 缓存穿透 | 查询根本不存在的数据 | 缓存空值、布隆过滤器 |
| 缓存击穿 | 单个热点 Key 失效 | 互斥锁、逻辑过期 |
| 缓存雪崩 | 大量 Key 同时失效或 Redis 故障 | 随机过期、高可用、限流降级 |
十、后端面试扩展:MySQL 核心要点(新增)
10.1 增删改查
示例表:
sql
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
age INT,
email VARCHAR(100) UNIQUE,
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT:
sql
INSERT INTO users (name, age, email) VALUES ('Tom', 20, 'tom@example.com');
INSERT INTO users (name, age, email) VALUES
('Tom', 20, 'tom@example.com'),
('Jack', 22, 'jack@example.com');
DELETE (执行删除语句时必须特别检查 WHERE 条件!):
sql
DELETE FROM users WHERE id = 1;
DELETE FROM users WHERE age < 18;
TRUNCATE TABLE users; -- 清空整张表
DELETE 与 TRUNCATE 都能删除所有数据,但:DELETE 逐行删除、可带 WHERE;TRUNCATE 不能带 WHERE、更快、通常重置自增计数;两者在事务、日志和触发器方面行为不同。
UPDATE (不写 WHERE 会修改所有记录):
sql
UPDATE users SET age = 21 WHERE id = 1;
UPDATE users SET age = 21, email = 'new@example.com' WHERE id = 1;
SELECT:
sql
SELECT * FROM users;
SELECT id, name, age FROM users;
SELECT id, name FROM users WHERE age >= 18 AND status = 1;
SELECT * FROM users WHERE name LIKE 'T%'; -- 模糊查询
SELECT * FROM users WHERE age BETWEEN 18 AND 30; -- 范围查询
SELECT * FROM users WHERE status IN (1, 2); -- 集合查询
SELECT * FROM users WHERE email IS NULL; -- 空值判断(不能写 = NULL)
排序、分页、分组:
sql
SELECT * FROM users ORDER BY age DESC, id ASC;
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20; -- 等价于 LIMIT 20, 10
SELECT status, COUNT(*) AS user_count FROM users GROUP BY status;
SELECT status, COUNT(*) AS user_count FROM users GROUP BY status HAVING COUNT(*) >= 10;
WHERE在分组前 过滤行;HAVING在分组后过滤分组结果。
10.2 多表连接
sql
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
amount DECIMAL(10, 2) NOT NULL
);
sql
-- INNER JOIN:只返回两表能匹配的数据
SELECT u.name, o.amount
FROM users AS u
INNER JOIN orders AS o ON u.id = o.user_id;
-- LEFT JOIN:返回左表全部,右表匹配不上的为 NULL
SELECT u.name, o.amount
FROM users AS u
LEFT JOIN orders AS o ON u.id = o.user_id;
-- 经典应用:查询没有订单的用户
SELECT u.id, u.name
FROM users AS u
LEFT JOIN orders AS o ON u.id = o.user_id
WHERE o.id IS NULL;
-- RIGHT JOIN:返回右表全部(实际开发中 LEFT JOIN 更常见,交换表位置即可代替)
10.3 事务与 ACID
事务是数据库中作为一个整体执行的一组操作,例如转账的两步必须同时成功或同时失败。
sql
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 发生异常时 ROLLBACK;
| 特性 | 含义 | InnoDB 实现机制 |
|---|---|---|
| 原子性 Atomicity | 操作要么全部成功,要么全部失败 | Undo Log |
| 一致性 Consistency | 事务前后数据库从一个合法状态到另一个合法状态 | 目标,靠机制 + 正确业务逻辑共同保证 |
| 隔离性 Isolation | 并发事务互不干扰 | 锁、MVCC、Undo Log |
| 持久性 Durability | 提交后的修改永久保存,崩溃不丢失 | Redo Log |
10.4 事务并发问题
- 脏读 :读到了另一个事务尚未提交的数据(对方回滚后,读到的就是无效数据)
- 不可重复读 :同一事务内两次读取同一条记录,结果不同(别人改了并提交)
- 幻读 :同一事务内两次执行相同条件的范围查询 ,返回的记录数量不同(别人插入/删除了记录)
- 简化理解:不可重复读重点是某一行的内容 变化;幻读重点是符合条件的记录集合变化
- 丢失更新:两个事务同时读取并更新同一条数据,其中一个的结果覆盖另一个。避免方式:悲观锁、乐观锁、原子更新语句、合适的隔离级别
10.5 四种事务隔离级别
从低到高:读未提交 → 读已提交 → 可重复读 → 串行化。
- READ UNCOMMITTED 读未提交:可以读其他事务未提交的数据;脏读、不可重复读、幻读都可能出现;实际很少使用
- READ COMMITTED 读已提交:只能读已提交的数据;避免脏读,但可能有不可重复读、幻读;Oracle、PostgreSQL 默认
- REPEATABLE READ 可重复读 :同一事务多次读取同一数据结果一致;避免脏读、不可重复读;MySQL InnoDB 的默认级别 。InnoDB 通过 MVCC、Next-Key Lock 在很多情况下还能避免幻读,但需要区分快照读和当前读,不能绝对化理解
- SERIALIZABLE 串行化:最高隔离级别,事务近似串行执行;避免所有并发问题,但并发性能最低
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 |
| 读已提交 | 避免 | 可能 | 可能 |
| 可重复读 | 避免 | 避免 | 标准定义下可能,InnoDB 有额外处理 |
| 串行化 | 避免 | 避免 | 避免 |
sql
SELECT @@transaction_isolation; -- 查看当前隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 设置当前会话隔离级别
10.6 索引
索引是帮助数据库快速查找数据的数据结构,可以理解为书的目录 :没有目录逐页查找,有目录快速定位。InnoDB 常用索引结构是 B+ 树。
优点 :提高查询速度;提高排序分组效率;唯一索引保证数据唯一性;覆盖索引可减少回表。
缺点:占用磁盘空间;插入/删除/更新需维护索引,写成本变高;不合理的索引可能不被使用。
常见索引类型:
sql
PRIMARY KEY (id) -- 主键索引:唯一、非空,InnoDB 用它组织表数据
CREATE UNIQUE INDEX idx_users_email ON users(email); -- 唯一索引(注意 MySQL 对 NULL 的处理规则)
CREATE INDEX idx_users_age ON users(age); -- 普通索引
CREATE INDEX idx_users_status_age ON users(status, age); -- 联合索引
-- 全文索引:用于文本搜索,复杂搜索场景常用 Elasticsearch
聚簇索引与二级索引:
- 聚簇索引 :InnoDB 表数据直接存储在主键索引的叶子节点中(叶子节点保存完整行数据),一张表通常只有一个
- 二级索引 :叶子节点保存索引列 + 对应记录的主键值 ;用二级索引查完整数据时,先在二级索引找到主键,再到主键索引查完整记录------这个过程叫回表
覆盖索引:查询需要的字段全部在索引中,直接从索引获得结果,不需要回表:
sql
CREATE INDEX idx_users_status_name ON users(status, name);
SELECT status, name FROM users WHERE status = 1; -- 只读索引即可
联合索引与最左前缀原则(可以把联合索引想象成:先按 name 排序,name 相同时按 age 排序,age 相同时按 status 排序):
sql
CREATE INDEX idx_users_name_age_status ON users(name, age, status);
-- 能有效利用索引:
WHERE name = 'Tom'
WHERE name = 'Tom' AND age = 20
WHERE name = 'Tom' AND age = 20 AND status = 1
-- 一般无法有效利用这个联合索引的最左部分:
WHERE age = 20
索引设计时字段顺序非常重要 。MySQL 优化器可能有索引扫描、跳跃扫描等方式,实际情况应通过 EXPLAIN 判断,不要把最左前缀理解成绝对规则。
常见索引失效或效果不佳的情况:
sql
-- 1. 对索引列做函数操作
WHERE YEAR(created_at) = 2025 -- ❌
WHERE created_at >= '2025-01-01'
AND created_at < '2026-01-01' -- ✅ 范围
-- 2. 隐式类型转换:字段是字符串,却用数字比较
WHERE phone = 13800138000 -- ❌
WHERE phone = '13800138000' -- ✅
-- 3. 前置模糊查询
WHERE name LIKE '%Tom' -- ❌ B+ 树难以利用前缀定位
WHERE name LIKE 'Tom%' -- ✅ 可用索引范围查询
-- 4. 联合索引不满足合适的字段顺序:索引(name, age) 却只查 WHERE age = 20
-- 5. 数据量太小或返回数据太多:优化器可能判断全表扫描成本更低
查看执行计划:
sql
EXPLAIN SELECT * FROM users WHERE email = 'tom@example.com';
重点关注:type(访问类型)、key(实际使用的索引)、rows(预计扫描行数)、Extra(额外信息),以及是否出现 Using filesort、Using temporary、Using index。
10.7 数据库连接与连接池
应用程序访问 MySQL 的基本过程:加载数据库驱动 → 建立 TCP 连接 → 身份认证 → 发送 SQL → 执行 → 返回结果 → 释放连接。
每次请求都新建连接成本高,所以实际项目使用数据库连接池 :提前建立一定数量的连接,请求到达时借 一个连接,执行 SQL 后归还。
主要优点:减少频繁创建连接的开销;限制数据库连接总数;提高系统吞吐量;便于管理连接超时、检测和回收。
常见参数:初始连接数、最小空闲连接数、最大连接数、获取连接超时时间、空闲连接回收时间、连接最大生命周期、连接有效性检测。
使用注意:
- 用完连接必须归还;事务结束后再归还
- 使用参数化查询,避免 SQL 注入
- 不要长时间占用连接;最大连接数不是越大越好
- 连接池容量应结合应用并发量和数据库承载能力设置
参数化查询(防 SQL 注入):
cpp
// 错误方式:用户可能输入恶意内容,产生 SQL 注入
std::string sql = "SELECT * FROM users WHERE name = '" + input + "'";
正确思路是使用预编译和参数绑定:
sql
SELECT * FROM users WHERE name = ?;
除了更安全,预编译语句通常也更便于复用。
10.8 视图
视图是基于查询结果创建的虚拟表。
sql
CREATE VIEW active_users AS
SELECT id, name, email FROM users WHERE status = 1;
SELECT * FROM active_users;
CREATE OR REPLACE VIEW active_users AS
SELECT id, name, email, age FROM users WHERE status = 1; -- 修改视图
DROP VIEW active_users; -- 删除视图
优点:
- 简化复杂查询:把复杂的多表查询封装成一个视图
- 控制数据访问:只暴露允许查看的字段(隐藏密码、身份证号等敏感字段)
- 提供逻辑抽象:底层表部分变化时,向上层提供相对稳定的查询接口
注意事项:
- 普通视图通常不单独存储实际数据,查询视图本质仍查询底层表
- 复杂视图可能影响性能
- 含聚合、分组、多表连接的视图通常不能直接更新
- 视图不能代替合理的表结构和索引设计;不要无限嵌套视图
十一、面试速记总结
C++ 四种类型转换
static_cast:常规编译期转换;dynamic_cast:多态安全向下转型(失败返回nullptr/ 抛bad_cast);const_cast:增删 const;reinterpret_cast:底层位模式重解释(最危险)。
设计模式
- 单例:保证一个类只有一个实例(Meyers Singleton 线程安全);
- 工厂:封装对象创建过程(简单工厂违反开闭、工厂方法一厂一物、抽象工厂一厂多产品族);
- 代理:控制对真实对象的访问,添加权限、日志等功能(与装饰器的区别在意图);
- 观察者:发布订阅,主题推送、观察者被动响应;
- 构建器:多参数复杂对象分步流式构建。
Redis
- String:缓存、计数器、分布式锁;Hash:对象;List:队列、列表;Set:去重、交并差集;ZSet:排行榜、延迟任务。
- RDB:定期快照,恢复快,可能丢较多数据;AOF:写命令日志,更安全,文件更大恢复更慢;实际项目可以同时用(新版还有混合持久化)。
缓存问题
- 穿透:查询不存在的数据(缓存空值、布隆过滤器);
- 击穿:单个热点 Key 失效(互斥锁、逻辑过期);
- 雪崩:大量 Key 同时失效或 Redis 故障(随机过期、高可用、限流降级)。
锁
- 悲观锁:先加锁再操作,适合冲突频繁;乐观锁:更新时检查版本号,适合冲突较少。
MySQL
- ACID:原子性、一致性、隔离性、持久性;
- 隔离级别:读未提交、读已提交、可重复读、串行化;InnoDB 默认可重复读;
- 索引提高读性能,但增加存储和写入成本;InnoDB 常用 B+ 树索引;
- 联合索引重点掌握最左前缀原则;用
EXPLAIN分析 SQL; - 使用连接池复用数据库连接;使用预编译和参数绑定避免 SQL 注入;
- 视图是基于查询定义的虚拟表。
十二、附加练习代码
12.1 图形面积计算(多态 + 智能指针)
cpp
#if 0
#include <iostream>
#include <memory>
using namespace std;
#define pai 3.14
class Shape {
public:
virtual void alcArea() = 0;
};
class Circle : public Shape {
private:
double t;
double area;
public:
Circle(double r, double area = 0) : t(r), area(area) {}
void alcArea() override {
area = pai * t * t;
cout << "圆的面积是" << area << endl;
}
};
int main() {
unique_ptr<Shape> s(new Circle(5.0));
s.get()->alcArea();
}
#endif
抽象基类定义纯虚函数实现多态;
unique_ptr管理堆对象,自动释放资源。
12.2 vector + for_each + Lambda 练习
cpp
#if 0
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v;
v.emplace_back(1); // emplace_back 原位构造,效率高于 push_back
v.emplace_back(3);
v.emplace_back(5);
v.emplace_back(7);
v.emplace_back(9);
for (auto v : v) {
cout << v << endl;
}
for_each(v.begin(), v.end(), [](int v) { cout << v * 2 << endl; });
return 0;
}
#endif
全文总结
- 四种类型转换 :
static_cast(常规)、dynamic_cast(多态安全)、const_cast(常量)、reinterpret_cast(底层),各司其职,别再用 C 风格强转。 - 前置复习:移动语义优化拷贝效率,各类智能指针基于 RAII 实现内存自动管理。
- 单例模式:全局唯一实例,懒汉 / 饿汉,面试报 Meyers Singleton(C++11 静态局部变量线程安全)。
- 工厂模式:简单工厂(违反开闭)、工厂方法(一厂一物)、抽象工厂(一厂多系列产品)。
- 代理模式:控制访问 + 前置后置增强,与装饰器的区别在意图。
- 观察者模式:发布订阅模型,主题推送消息,观察者被动响应。
- 构建器模式:解决多参数复杂对象创建,支持流式调用。
- Redis:五种基本类型、RDB/AOF 持久化、乐观悲观锁、主从复制、缓存穿透击穿雪崩。
- MySQL:CRUD 与多表连接、事务 ACID、并发问题与四种隔离级别、B+ 树索引与最左前缀、连接池与视图。
- 附加练习巩固:C++ 多态、智能指针、STL 容器与算法综合使用。
如果这篇对你有帮助,点个赞 + 收藏再走吧~有疑问欢迎评论区交流。