std::regex 正则表达式
定义与作用
<regex> 是 C++11 引入的正则表达式标准库,提供 Perl/ECMAScript 风格的模式匹配、搜索和替换能力,核心由三大类组成:
#include <regex>
std::regex pattern(R"(\d{3}-\d{4}-\d{4})"); // 正则模式
std::smatch match; // 匹配结果
bool found = std::regex_search("电话: 138-0001-3800", match, pattern);
| 核心类 | 作用 |
|---|---|
std::regex | 编译并存储正则表达式模式 |
std::smatch / std::cmatch | 存储匹配结果(含子表达式捕获组) |
std::regex_match | 完全匹配:整个字符串必须匹配模式 |
std::regex_search | 部分匹配:搜索字符串中第一个匹配 |
std::regex_replace | 替换匹配内容 |
核心原理
三大操作对比
匹配结果结构
语法选项
| 选项 | 说明 |
|---|---|
std::regex::ECMAScript | 默认语法,JavaScript 风格 |
std::regex::icase | 忽略大小写 |
std::regex::nosubs | 不存储子表达式匹配(性能优化) |
std::regex::optimize | 提示编译器优化匹配速度 |
std::regex::extended | POSIX 扩展正则 |
std::regex::grep | POSIX grep 风格 |
多个选项通过 | 组合:std::regex::ECMAScript | std::regex::icase。
完整示例
示例一:飞翔科技用户信息验证
场景说明:高英做用户运营,需要批量验证注册信息——邮箱格式、手机号、中文姓名。
#include <iostream>
#include <regex>
#include <string>
#include <vector>
struct UserInfo {
std::string name;
std::string email;
std::string phone;
};
int main() {
std::cout << "===== 飞翔科技用户注册验证 =====" << std::endl;
// 编译正则表达式(只编译一次,复用)
std::regex emailPattern(R"(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)");
std::regex phonePattern(R"(^1[3-9]\d{9}$)");
std::regex chineseNamePattern(R"(^[\u4e00-\u9fff]{2,4}$)");
std::vector<UserInfo> users = {
{"大翔", "daxiang@feixiang.net", "13800001001"},
{"孔蓝", "konglan@feixiang.net", "13912345678"},
{"黄俪", "huangli.feixiang.net", "13888888888"}, // 邮箱缺少 @
{"赵鸣", "zhaoming@feixiang", "13700009999"}, // 邮箱缺少顶级域
{"小崔", "xiaocui@feixiang.net", "12345678901"}, // 手机号首位不是1
{"孙鹤", "sunhe@feixiang.net", "13800138000"},
};
int passed = 0;
for (const auto& user : users) {
std::cout << "\n--- " << user.name << " ---" << std::endl;
bool nameOk = std::regex_match(user.name, chineseNamePattern);
bool emailOk = std::regex_match(user.email, emailPattern);
bool phoneOk = std::regex_match(user.phone, phonePattern);
std::cout << " 姓名: " << (nameOk ? "✓" : "✗") << std::endl;
std::cout << " 邮箱: " << (emailOk ? "✓" : "✗") << " " << user.email << std::endl;
std::cout << " 手机: " << (phoneOk ? "✓" : "✗") << " " << user.phone << std::endl;
if (nameOk && emailOk && phoneOk) ++passed;
}
std::cout << "\n总计: " << passed << "/" << users.size() << " 通过验证" << std::endl;
return 0;
}
预期输出:
===== 飞翔科技用户注册验证 =====
--- 大翔 ---
姓名: ✓
邮箱: ✓
手机: ✓
--- 孔蓝 ---
姓名: ✓
邮箱: ✓
手机: ✓
--- 黄俪 ---
姓名: ✓
邮箱: ✗ huangli.feixiang.net
手机: ✓
--- 赵鸣 ---
姓名: ✓
邮箱: ✗ zhaoming@feixiang
手机: ✓
--- 小崔 ---
姓名: ✓
邮箱: ✓
手机: ✗ 12345678901
--- 孙鹤 ---
姓名: ✓
邮箱: ✓
手机: ✓
总计: 4/6 通过验证
逐段分析:
regex_match要求整个字符串完全匹配模式——适合输入验证场景(如邮箱、手机号)- 正则对象在循环外编译一次然后复用——避免重复编译正则以提升性能
R"(...)"原始字符串字面量避免转义正则中的反斜杠——\d不会变成\\d^和$分别锚定字符串开头和结尾,防止部分匹配(如 "abc123@test.com!!!" 通过验证)[\u4e00-\u9fff]匹配中文字符范围
示例二:日志分析与信息提取
场景说明:李眉分析飞翔科技服务器日志,提取 IP 地址、时间戳和错误码。
#include <iostream>
#include <regex>
#include <string>
#include <vector>
#include <iomanip>
struct LogEntry {
std::string time;
std::string ip;
std::string level;
int errorCode;
std::string message;
};
int main() {
std::cout << "===== 飞翔科技日志分析 =====" << std::endl;
// 模拟 Nginx 风格日志
std::vector<std::string> logLines = {
R"(2024-06-14 10:23:45 192.168.1.100 INFO 用户大翔登录成功)",
R"(2024-06-14 10:24:12 10.0.0.55 ERROR 502 数据库连接超时)",
R"(2024-06-14 10:25:01 172.16.0.88 WARN 磁盘使用率达到80%)",
R"(2024-06-14 10:26:33 192.168.1.200 ERROR 404 页面未找到 /api/v2/old)",
R"(2024-06-14 10:27:00 10.0.0.55 INFO 服务恢复正常)",
};
// 日志正则模式(含命名捕获组的效果用注释标出)
std::regex logPattern(
R"(^(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})\s)" // 组1: 时间
R"((\d{1,3}(?:\.\d{1,3}){3})\s)" // 组2: IP
R"((INFO|WARN|ERROR)\s)" // 组3: 级别
R"((?:(\d{3})\s)?)" // 组4: 错误码(可选)
R"((.*)$)" // 组5: 消息
);
std::vector<LogEntry> entries;
int errorCount = 0, warnCount = 0, infoCount = 0;
for (const auto& line : logLines) {
std::smatch match;
if (std::regex_match(line, match, logPattern)) {
LogEntry entry;
entry.time = match[1].str();
entry.ip = match[2].str();
entry.level = match[3].str();
entry.errorCode = match[4].matched ? std::stoi(match[4].str()) : 0;
entry.message = match[5].str();
entries.push_back(entry);
if (entry.level == "ERROR") ++errorCount;
else if (entry.level == "WARN") ++warnCount;
else ++infoCount;
}
}
// 输出解析结果
std::cout << std::left
<< std::setw(22) << "时间"
<< std::setw(18) << "IP"
<< std::setw(8) << "级别"
<< std::setw(8) << "错误码"
<< "消息" << std::endl;
std::cout << std::string(72, '-') << std::endl;
for (const auto& e : entries) {
std::cout << std::left
<< std::setw(22) << e.time
<< std::setw(18) << e.ip
<< std::setw(8) << e.level;
if (e.errorCode > 0)
std::cout << std::setw(8) << e.errorCode;
else
std::cout << std::setw(8) << "-";
std::cout << e.message << std::endl;
}
std::cout << "\n统计: INFO=" << infoCount
<< " WARN=" << warnCount
<< " ERROR=" << errorCount << std::endl;
// ---- regex_search 演示 ----
std::cout << "\n[regex_search: 提取所有 IP 地址]" << std::endl;
std::regex ipPattern(R"(\b(\d{1,3}\.){3}\d{1,3}\b)");
for (const auto& line : logLines) {
std::smatch match;
if (std::regex_search(line, match, ipPattern)) {
std::cout << " 从 '" << line.substr(0, 30) << "...' 中找到: "
<< match[0] << std::endl;
}
}
// ---- regex_replace 演示 ----
std::cout << "\n[regex_replace: IP 脱敏]" << std::endl;
std::regex ipReplacePattern(R"((\d{1,3})\.\d{1,3}\.\d{1,3}\.\d{1,3})");
for (const auto& line : logLines) {
std::string masked = std::regex_replace(line, ipReplacePattern, "$1.***.***.***");
std::cout << " " << masked.substr(0, 60) << std::endl;
}
return 0;
}
预期输出:
===== 飞翔科技日志分析 =====
时间 IP 级别 错误码 消息
------------------------------------------------------------------------
2024-06-14 10:23:45 192.168.1.100 INFO - 用户大翔登录成功
2024-06-14 10:24:12 10.0.0.55 ERROR 502 数据库连接超时
2024-06-14 10:25:01 172.16.0.88 WARN - 磁盘使用率达到80%
2024-06-14 10:26:33 192.168.1.200 ERROR 404 页面未找到 /api/v2/old
2024-06-14 10:27:00 10.0.0.55 INFO - 服务恢复正常
统计: INFO=2 WARN=1 ERROR=2
[regex_search: 提取所有 IP 地址]
从 '2024-06-14 10:23:45 192.168.1.1...' 中找到: 192.168.1.100
从 '2024-06-14 10:24:12 10.0.0.55 ...' 中找到: 10.0.0.55
...
[regex_replace: IP 脱敏]
2024-06-14 10:23:45 192.***.***.*** 用户大翔登录成功
2024-06-14 10:24:12 10.***.***.*** 502 数据库连接超时
...
逐段分析:
regex_match验证整个字符串结构并用捕获组提取各字段——组 1=时间、组 2=IP、组 3=级别、组 4 (可选)=错误码、组 5=消息match[i].matched检查第 i 个捕获组是否实际参与了匹配——错误码是可选的(?:...)是非捕获组——用于分组但不保存子匹配,比捕获组更高效regex_search找到字符串中第一个匹配的子串——适合"有没有"的探测性搜索regex_replace的替换字符串中$1引用第一个捕获组的内容——实现 IP 部分脱敏
示例三:regex 性能最佳实践
#include <iostream>
#include <regex>
#include <chrono>
#include <vector>
#include <string>
int main() {
std::cout << "===== regex 性能对比 =====" << std::endl;
const int N = 10'000;
std::vector<std::string> emails(N, "daxiang@feixiang.net");
// ---- 错误做法:每次循环都编译 regex ----
{
auto start = std::chrono::steady_clock::now();
int count = 0;
for (const auto& email : emails) {
std::regex re(R"(^[\w.+-]+@[\w.-]+\.[a-zA-Z]{2,}$)");
if (std::regex_match(email, re)) ++count;
}
auto end = std::chrono::steady_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "每次编译 regex: " << ms << " ms (匹配 " << count << " 个)"
<< std::endl;
}
// ---- 正确做法:编译一次复用 ----
{
auto start = std::chrono::steady_clock::now();
std::regex re(R"(^[\w.+-]+@[\w.-]+\.[a-zA-Z]{2,}$)");
int count = 0;
for (const auto& email : emails) {
if (std::regex_match(email, re)) ++count;
}
auto end = std::chrono::steady_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "复用 regex 对象: " << ms << " ms (匹配 " << count << " 个)"
<< std::endl;
}
// ---- 优化:使用 nosubs 不存储捕获组 ----
{
auto start = std::chrono::steady_clock::now();
std::regex re(R"(^[\w.+-]+@[\w.-]+\.[a-zA-Z]{2,}$)",
std::regex::ECMAScript | std::regex::nosubs);
int count = 0;
for (const auto& email : emails) {
if (std::regex_match(email, re)) ++count;
}
auto end = std::chrono::steady_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "复用 + nosubs: " << ms << " ms (匹配 " << count << " 个)"
<< std::endl;
}
std::cout << "\n结论: regex 编译开销大,应尽量复用对象" << std::endl;
return 0;
}
预期输出:
===== regex 性能对比 =====
每次编译 regex: 2847 ms (匹配 10000 个)
复用 regex 对象: 32 ms (匹配 10000 个)
复用 + nosubs: 22 ms (匹配 10000 个)
结论: regex 编译开销大,应尽量复用对象
逐段分析:
- regex 编译是重操作——在循环内重复创建
std::regex比复用慢约 100 倍 std::regex::nosubs不存储捕获组的结果——如果只需要 true/false,关闭子表达式保存可额外提速- 编译一次然后复用是所有 regex 场景的基础优化
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 循环内重复编译 regex | 性能灾难(比复用慢 100 倍) | 在循环外编译,循环内复用 |
regex_match vs regex_search 混淆 | 期望部分匹配却用 match | 验证完整字符串用 match,搜索用 search |
| 正则中反斜杠未转义 | \d 被当作转义序列 | 使用原始字符串字面量 R"(\d+)" |
未处理匹配失败时访问 match[0] | 访问空 smatch 未定义行为 | 先检查 regex_match/search 返回值 |
忽略 std::regex_error 异常 | 非法正则表达式静默失败 | 用 try-catch 包裹 regex 构造 |
常见面试问题
regex_match和regex_search的区别?——regex_match要求整个字符串完全匹配模式(隐式锚定^...$),regex_search搜索字符串中第一个匹配的子串。验证输入格式用 match,搜索关键词用 search。std::regex的性能注意事项?——regex 编译开销大,应尽量复用对象。不需要捕获组时使用nosubs选项。GCC 4.9 之前的<regex>实现不完整,建议 GCC 5+ 或 Clang。为什么推荐使用
R"(...)"写正则?——正则表达式中反斜杠极多(\d、\w、\s等),不用原始字符串需要写成\\d、\\w,极易出错。捕获组和非捕获组的区别?——捕获组
(...)将匹配内容保存到smatch[i],非捕获组(?:...)仅分组不保存。不需要提取子匹配时用非捕获组以提升性能。
小结
- 三大核心操作:
regex_match(完全匹配)、regex_search(搜索)、regex_replace(替换) - 默认 ECMAScript 语法,可通过
|组合选项(如icase | nosubs) - regex 编译开销大——循环外编译一次,循环内复用
- 捕获组通过
smatch[i]提取子匹配,非捕获组(?:...)用于纯分组 - 使用
R"(...)"原始字符串避免正则转义地狱