C++11
一. 右值引用与移动构造函数
左值:是指一个对象,它有一个持久的身份,并且其地址可以被取得。在C++中,大多数变量和对象都是左值。
右值:一个表示数据的表达式(如字面常量、函数的返回值、表达式的返回值),且不可以获取它的地址(取地址);它只能在赋值符号的右边。右值通常是不可以改变的值。
那么我们就可以很容易地知道:
-
左值引用:给左值取别名
-
右值引用:给右值取别名
- 左值引用只能引用左值
- 左值引用(lvalue reference)是一个引用,它必须绑定到一个左值上。例如,
int &ref = someVariable;
这里的ref
是一个左值引用,它必须绑定到一个已经存在的左值someVariable
上。
- 左值引用(lvalue reference)是一个引用,它必须绑定到一个左值上。例如,
- const左值引用可以引用左值,也可以引用右值
- const左值引用意味着这个引用不能用来修改它所引用的对象。由于右值是不可变的(至少从逻辑上讲),因此可以使用const左值引用来引用右值。例如,
const int &ref = someFunction();
这里someFunction()
返回一个右值,但是由于引用是const的,所以可以安全地绑定到这个右值上。
- const左值引用意味着这个引用不能用来修改它所引用的对象。由于右值是不可变的(至少从逻辑上讲),因此可以使用const左值引用来引用右值。例如,
- 右值引用只能引用右值
- 右值引用(rvalue reference)使用
&&
语法,并且只能绑定到右值上。右值引用的主要用途是实现移动语义和完美转发。例如,int &&ref = std::move(someVariable);
这里std::move
将左值someVariable
转换为右值,然后右值引用ref
可以绑定到这个右值上。
- 右值引用(rvalue reference)使用
- 左值可以通过move(左值)来转化为右值
std::move
是C++11引入的一个函数模板,它可以将左值转换为右值引用,从而允许我们使用移动构造函数或移动赋值操作符来避免不必要的复制操作。然而,std::move
并不真正移动任何东西;它只是产生一个右值引用。实际的资源转移(如内存指针或句柄的交换)是通过移动构造函数或移动赋值操作符来完成的。
左值引用的意义在于:
1.函数传参:实参传给形参时,可以减少拷贝。
2.函数传返回值时,只要是出了作用域还存在的对象,那么就可以减少拷贝。
然而,对于函数返回局部对象的情况,即使使用左值引用作为返回值,如果返回的对象在函数返回后就被销毁(即它的生命周期仅限于函数内部),那么试图引用这个对象就会导致未定义行为。这是因为左值引用要求引用的对象在引用期间必须保持有效。那还需要多次的拷贝构造,导致消耗较大,效率较低。
为了解决这个问题,C++11引入了右值引用和移动语义。当函数返回一个局部对象时,如果这个对象即将被销毁,我们可以使用右值引用和移动构造函数来避免拷贝。通过移动构造函数,我们可以将局部对象的资源(如内存指针、句柄等)转移到另一个对象中,而不是进行深拷贝。这样,我们既避免了不必要的拷贝,又确保了返回的对象在函数返回后仍然有效。
在没有右值引用之前,我们是如何解决函数传返回值的拷贝问题呢?通过输出型参数。
右值引用示例:
当构造传左值,就走拷贝构造,当构造传右值,就走移动构造。
先是调用无参构造函数生成了一个对象tmp,然后在A = getMyClass的时候会执行一次拷贝构造函数,其中A这个对象会被getMyClass()返回的对象实例化,实例化以后,getMyClass()里边生成的临时对象tmp会被析构掉。A的生命周期结束以后呢,也会执行一次析构函数。
以上的程序看起来是没有问题的,在这个对象所包含的数据量如果很庞大的情况下,是有必要去做优化的。
其中A可以直接使用getMyClass生成的临时对象tmp,就可以使用右值引用去做到,这里的话就要去实现右值引用的构造函数。
右值引用构造函数又叫做移动构造函数。
在执行A = getMyClass的时候调用了移动构造函数,在执行移动构造函数的时候,把tmp对象里边的num指针赋值给a对象的num指针,然后把tmp对象的num指针置为空指针(防止浅拷贝导致的问题),tmp不再拥有这块堆内存的资源,就把临时变量tmp的堆内存转移给了对象A。
为什么在执行A = getMyClass的时候移动构造会被调用呢,而不是去调用拷贝构造函数?
因为在执行构造函数的时候,编译器会去判断等号右边的返回的是否是临时对象,如果是临时对象就会调用移动构造函数,不是临时对象就会调用拷贝构造函数。
二. 空指针nullptr
nullptr 出现的目的是为了替代 NULL。 在某种意义上来说,传统 C++ 会把 NULL 和 0 视为同⼀种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定 义为 ((void*)0),有些则会直接将其定义为 0。C++ 不允许 直接将 void * 隐式转换到其他类型,但如果 NULL 被定义为 ((void*)0),那么当编译 char *ch = NULL; 时,NULL 只好被定义为 0。而这依然会产⽣问题,将导致了 C++ 中重载特性 会发生混乱,考虑:
void func(int);
void func(char *);
对于这两个函数来说,如果 NULL 又被定义为了 0 那么 func(NULL) 这个语句将 会去调用 func(int),从⽽导致代码违反直观。
为了解决这个问题,C++11 引⼊了 nullptr 关键字,专门用来区分空指针和0。nullptr 的类型 为nullptr_t,能够隐式 的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。 当需要使用 NULL 时候,养成直接使用 nullptr 的习惯。
三. Lambda 表达式
Lambda 表达式实际上就是提供了⼀个类似匿名函数的特性,而匿名函数则是在需要⼀个函数,但是又不想费力去命名⼀个函数的情况下去使用的。 利用 lambda 表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,并且使代码更可读。
从本质上来讲,lambda 表达式只是⼀种语法糖,因为所有其能完成的工作都可以用其它稍微复杂的代码来实现,但是它简便的语法却给 C++ 带来了深远的影响。 从广义上说, lamdba 表达式产生的是函数对象。在类中,可以重载函数调用运算符(),此时类的对象将具有类似函数的行为,我们称这些对象为函数对象(Function Object)或者仿函数(Functor)。相比 lambda表达式,函数对象有自己独特的优势。
lambda 表达式⼀般都是从方括号[]开始,然后结束于花括号{},花括号里面就像定义函数那 样,包含了 lamdba 表达式体。
// 指明返回类型,托尾返回类型
auto add = [](int a, int b) -> int { return a + b; };
// ⾃动推断返回类型
auto multiply = [](int a, int b) { return a * b; };
int sum = add(2, 5); // 输出:7
int product = multiply(2, 5); // 输出:10
最前边的 [] 是 lambda 表达式的⼀个很重要的功能,就是 闭包。 先说明⼀下 lambda 表达式的大致原理:每当你定义⼀个 lambda 表达式后,编译器会自动生成⼀个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。 那么在运行时,这个 lambda 表达式就会返回⼀个匿名的闭包实例,其实⼀个右值。所以,我们上面的 lambda 表达式的结果就是⼀个个闭包实例。 闭包的⼀个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为 lambda 捕捉块。
捕获的方式可以是引用也可以是复制,但是具体说来会有以下几种情况来捕获其所在作用域中的变量:
- [ ]:默认不捕获任何变量
- [=]:默认以值捕获所有变量
- [&]:默认以引用捕获所有变量
- [x]:仅以值捕获x,其它变量不捕获
- [&x]:仅以引用捕获x,其它变量不捕获
- [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获
- [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获
- [this]:通过引用捕获当前对象(其实是复制指针)
- [*this]:通过传值方式捕获当前对象;
[ capture-list ] ( params ) mutable(optional) constexpr(optional)(c++17)
exception attribute -> ret { body }
// 可选的简化语法
[ capture-list ] ( params ) -> ret { body }
[ capture-list ] ( params ) { body }
[ capture-list ] { body }
- capture-list:捕捉列表,它不能省略
- params:参数列表,可以省略(但是后面必须紧跟函数体)
- mutable:可选,将 lambda 表达式标记为 mutable 后,函数体就可以修改传值方式捕获的变量
- constexpr:可选, C++17 ,可以指定 lambda 表达式是⼀个常量函数
- exception:可选,指定 lambda 表达式可以抛出的异常
- attribute:可选,指定 lambda 表达式的特性
- ret:可选,返回值类型
- body:函数执行体。
四. 泛化的常量表达式
int N = 5;
int arr[N];
编译器会报错: error: variable length array declaration not allowed at file scope int arr[N]; ,但 N 就是5,不过编译器不知道这⼀点,于是我们需要声明为 const int N = 5 才可以。但C++11的泛化常数给出了解决方案:
constexpr int N = 5; // N 变成了⼀个只读的值
int arr[N]; // OK
constexpr 告诉编译器这是⼀个编译期常量,甚至可以把⼀个函数声明为编译期常量表达式。
constexpr int getFive(){ return 5; }
int arr[getFive() + 1];
五. 初始化列表和统一的初始化语法
C++11 提供了 initializer_list 来接受变长的对象初始化列表:
class A{
public:
A(std::initializer_list<int> list);
};
A a = {1, 2, 3}
initializer_list
是C++11标准库提供的一种模板类型,用于表示某种特定类型的值的数组。这种类型的主要目的是为了更方便地给容器类(如vector
、string
等)或其他需要多个初始化值的函数提供初始化值。
initializer_list
中的值都是常量,无法修改。你可以使用花括号{}
来初始化一个initializer_list
对象,例如:
initializer_list<int> lst{1, 2, 3, 4};
此外,initializer_list
还提供了一些常用的成员函数,如size()
用于获取列表中的元素数量,begin()
和end()
用于获取指向列表开始和结束位置的迭代器。
initializer_list
的一个主要应用场景是在函数参数中,特别是当函数的参数数量不确定时。通过使用initializer_list
,你可以向函数传递任意数量的同类型参数,而无需编写可变参数模板或重载多个函数。例如:
void printNumbers(initializer_list<int> nums) {
for (auto num : nums) {
cout << num << " ";
}
cout << endl;
}
int main() {
printNumbers({1, 2, 3, 4, 5}); // 输出: 1 2 3 4 5
return 0;
}
不同的数据类型具有不同的初始化语法。如何初始化字符串?如何初始化数组?如何初始化多维数组?如何初始化对象?
列表初始化(List Initialization)是C++11引入的一个新特性,它允许使用花括号{}
来初始化变量、数组、结构体等。这种初始化方式具有更加统一和直观的语法,同时也提供了一些优势,例如防止窄化转换(narrowing conversion)等。窄化转换是指将一个具有更大范围的值转换为一个较小范围的类型时,可能导致丢失精度或截断。在C++中,窄化转换是一种类型转换的错误,因为它可能导致数据丢失或错误的结果。列表初始化在C++11中引入,部分是为了解决窄化转换的问题。当使用花括号{}
进行列表初始化时,会禁止窄化转换,从而提高了代码的类型安全性。
X x1 = X{1,2};
X x2 = {1,2}; // 此处的'='可有可⽆
X x3{1,2};
X* p = new X{1,2};
struct D : X
{
D(int x, int y) :X{x,y} { /* … */ };
};
struct S
{
int a[3];
// 对于旧有问题的解决⽅案
S(int x, int y, int z) :a{x,y,z} { /* … */ };
};
六. 类型推导
C++ 提供了 auto 和 decltype 来静态推导类型,在我们知道类型没有问题但又不想完整地写出类型的时候, 便可以使用静态类型推导。
for(vector<int>::iterator it = v.begin(); it != v.end(); ++it);
// 可以改写为
for(auto it = v.begin(); it != v.end(); ++it);
decltype的主要用途有两个:推导变量的类型和推导函数返回类型。 decltyp(e) 规则如下:
-
若e为⼀个无括号的变量、函数参数、类成员,则返回类型为该变量/参数/类成员在源程序中的声明类型
- 推导变量的类型:通过使用decltype关键字,可以根据初始化器的类型来推导变量的类型。例如,如果有一个变量int x = 5,那么使用decltype(x) y;就可以推导出y的类型为int。
-
否则的话,根据表达式的值分类(value categories),设 T 为 e 的类型
-
若 e 是⼀个左值(lvalue,即“可寻址值”),返回 T&
-
若 e 是⼀个临终值(xvalue),则返回值为 T&&
-
在这个例子中,
rvalueRef
是一个对x
的右值引用,是一个临终值。因此,decltype(rvalueRef)
返回int&&
,anotherRvalueRef
也被声明为int&&
类型。
-
-
若 e 是⼀个纯右值(prvalue),则返回值为 T
-
七. 构造函数委托
委托构造函数(Delegating Constructors)是C++11引入的另一个重要特性,它允许构造函数在同一个类中调用另一个构造函数。这样做的主要目的是减少代码重复,提高代码的复用性和清晰度。
在C++11之前,如果你有多个构造函数,它们之间有共同的初始化代码,你不得不在每个构造函数中重复这些代码。而委托构造函数允许你将共同的初始化代码放在一个构造函数中,然后让其他构造函数调用这个构造函数,从而避免代码重复。
八. final 和 override
C++ 借由虚函数实现运行时多态,但 C++ 的虚函数又很多脆弱的地方: 无法禁止子类重写它。
可能到某⼀层级时,我们不希望子类继续来重写当前虚函数了。 容易不小心隐藏父类的虚函数。比如在重写时,不小心声明了⼀个签名不⼀致但有同样名称的新函数。
C++11 提供了 final 来禁止虚函数被重写或禁止类被继承, override 来显示地重写虚函数(如果派生类中的函数没有正确地重写基类中的虚函数(例如,函数签名不匹配),编译器会发出错误信息)。 这样编译器给我们不小心的行为提供更多有用的错误和警告。
九. default 和 delete
我们知道编译器会为类自动生成⼀些方法,比如构造和析构函数。
现在我们可以显式地指定和禁止这些自动行为了。
在上述 classA 中定义了 classA(T value) 构造函数,因此编译器不会默认生成⼀个无参数的构造函数了,如果我们需要可以手动声明,或者直接 = default 。
十. assert(编译、运行、预处理)
C++ 提供了两种方式来 assert :⼀种是 assert 宏,另⼀种是预处理指令 #error 。 前者在运行期间起作用,后者是预处理期间起作用。
assert
宏用于在运行时检查一个条件是否为真。如果条件为真(非零),则assert
什么也不做;如果条件为假(零),则程序会在该点终止执行,并打印一条错误消息。
如果断言失败,这个字符串会被包含在输出的错误信息中。
注意事项
assert
主要用于调试阶段,不应用于生产环境。在生产代码中,应该使用更合适的错误处理机制,如异常处理或返回错误码。assert
不应该用于检查用户输入的有效性或程序状态,因为这些检查应该始终执行,而不仅仅是在调试阶段。assert
宏的行为可能因编译器而异,有些编译器可能提供更多的功能或选项。因此,在使用assert
时,最好查阅你所使用的编译器的文档。
#error
是C++预处理器的一个指令,它用于在预处理阶段产生一个编译错误。当预处理器遇到#error
指令时,它会立即停止处理,并显示#error
指令后面的文本作为错误信息。
在这个例子中,如果__DEBUG
没有在包含这个头文件之前被定义,预处理器就会产生一个编译错误,并显示提供的错误消息。
#error
指令通常用于在编译时检查某些条件是否满足,比如检查是否定义了必要的宏或包含了必要的头文件。它有助于在编译早期就捕获潜在的配置错误或不一致性。
但是它们对模板都不好使,因为模板是编译期的概念。
static_assert
是C++11引入的一个关键字,用于在编译时进行静态断言。它的主要作用是在编译阶段检查某个条件是否为真,如果条件不满足(即为假),则会在编译时产生一条错误信息,并终止编译过程。 static_assert 关键字的使用方式如下:
static_assert(常量表达式, 提示字符串);
其中:
常量表达式
:是一个在编译时就能确定结果的表达式。如果表达式的值为true
(或非零),则static_assert
不会做任何事情;如果表达式的值为false
(或零),则会产生编译错误。提示字符串
:是一个可选参数,用于在断言失败时提供一个自定义的错误消息,帮助开发者更快速地定位和解决问题。
static_assert
的使用不受限制,可以在全局作用域、命名空间中、类作用域中以及函数作用域中使用。
在这个例子中,myArray.size()
是一个编译时常量表达式,因为std::array
的大小是在编译时确定的。
然而,如果你有一个std::vector
或其他动态大小的容器,你不能在编译时检查其大小,因为容器的大小是在运行时确定的。对于这种情况,你只能在运行时进行检查:
assert
是在运行时执行的。如果vvv.size()
不等于desiredSize
,程序会在运行时通过调用std::abort
终止执行(在调试模式下,通常会显示一个错误消息)。注意,assert
只在调试模式下起作用,如果定义了NDEBUG
宏,则assert
语句会被忽略。
十一. 智能指针
从比较简单的层面来看,智能指针是RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制对普通指针进行的一层封装。这样使得智能指针的行为动作像一个指针,本质上却是一个对象,这样可以方便管理一个对象的生命周期。
在C++中,智能指针一共定义了4种:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。其中,auto_ptr 在 C++11已被摒弃,在C++17中已经移除不可用。
-
unique_ptr
unique_ptr是独享被管理对象指针所有权(owership)的智能指针。unique_ptr对象封装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。unique最常见的使用场景,就是替代原始指针,为动态申请的资源提供异常安全保证。
unique_ptr没有复制构造函数,不支持普通的拷贝和赋值操作,因为unique_ptr独享被管理对象指针所有权。
unique_ptr虽然不支持普通的拷贝和赋值操作,但却可以将所有权进行转移,使用std::move方法即可。
-
shared_ptr
我们提到的智能指针,很大程度上就是指的shared_ptr,shared_ptr也在实际应用中广泛使用。它的原理是使用引用计数实现对同一块内存的多个引用。在最后一个引用被释放时,指向的内存才释放,这也是和 unique_ptr 最大的区别。当对象的所有权需要共享(share)时,share_ptr可以进行赋值拷贝。
shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指向的堆内存。
上面这种写法是错误的,因为右边得到的是一个原始指针,前面我们讲过shared_ptr本质是一个对象,将一个指针赋值给一个对象是不行的。
上面的写法,可以获取shared_ptr的原始指针。
不能将一个原始指针初始化多个shared_ptr
上面代码就会报错。原因也很简单,因为p3、p6都要进行析构删除,这样会造成原始指针pointer被删除两次,自然要报错。
循环引用问题
shared_ptr最大的坑就是循环引用。
该部分代码会有内存泄漏问题。原因是:
1.main 函数退出之前,Father 和 Son 对象的引用计数都是 2。
2.son 指针销毁,这时 Son 对象的引用计数是 1。
3.father 指针销毁,这时 Father 对象的引用计数是 1。
4.由于 Father 对象和 Son 对象的引用计数都是 1,这两个对象都不会被销毁,从而发生内存泄露。
为避免循环引用导致的内存泄露,就需要使用 weak_ptr。weak_ptr 并不拥有其指向的对象,也就是说,让 weak_ptr 指向 shared_ptr 所指向对象,对象的引用计数并不会增加。
使用 weak_ptr 就能解决前面提到的循环引用的问题,方法很简单,只要让 Son 或者 Father 包含的 shared_ptr 改成 weak_ptr 就可以了。
1.main 函数退出前,Son 对象的引用计数是 2,而 Father 的引用计数是 1。
2.son 指针销毁,Son 对象的引用计数变成 1。
3.father 指针销毁,Father 对象的引用计数变成 0,导致 Father 对象析构,Father 对象的析构会导致它包含的 son_ 指针被销毁,这时 Son 对象的引用计数变成 0,所以 Son 对象也会被析构。
我们该如何选择智能指针:
如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr。这样的情况包括
1.有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
2.两个对象包含都指向第三个对象的指针;
3.STL 容器包含指针。很多 STL 算法都支持复制和赋值操作,这些操作可用于 shared_ptr,但不能用于 unique_ptr(编译器发出 warning)和 auto_ptr(行为不确定)。如果你的编译器没有提供 shared_ptr,可使用 Boost 库提供的 shared_ptr。
如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。如果函数使用 new 分配内存,并返还指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。这样,所有权转让给接受返回值的 unique_ptr,而该智能指针将负责调用 delete。
十二. 正则表达式
正则是一种规则,它用来匹配(进而捕获、替换)字符串。这种规则需要“模式”、“字符串”这两样东西,“模式”根据正则规则,来处理“字符串”。
这种规则被许多语言支持,C++11以后才支持正则。
正则由元字符和普通字符组成。普通字符就代表它原本的含义;元字符的意义不同于该字符本来的含义,而是有特殊的意义和功能。
根据其意义功能划分,可将元字符划分为:
- 具有特殊意义的元字符
\
:\字符能够改变字符原本的含义^
:字符指示字符串的头,且要求字符串以`字符`开头,不占位。`^`表示一个真正的符号。$
:$字符
指示字符串的尾,且要求字符串以字符
结尾,不占位。\$
表示一个真正的$符号。()
:分组,大正则中包含小正则。可以改变默认的优先级。在模式中可以使用\1
来表示第一组已然捕获到的东西。\b
:指示字符串的边界(头/尾/空格左/空格右),字符\b
要求边界的左边是字符,\b字符
要求边界的右边是字符。.
:表示一个除了\n
以外的任意一个字符。\.
表示一个真正的.符号。|
:字符串1|字符串2
表示一个字符串,该字符串是字符串1、字符串2中的一个。|
在正则中的优先级比较混乱,所以建议加上足够多的括号来分组。[]
:[字符1字符2字符3...]
表示一个字符,该字符是字符1、字符2、字符3……中的某一个。中括号中出现的所有字符都是代表本身意思的字符(没有特殊含义),如[.]
只能匹配.
符号,而不能匹配任意符号。[^字符1字符2字符3...]
表示一个字符,该字符不是字符1、字符2、字符3……中的任何一个[a-z]
表示一个字符,该字符是a、b、c……z中的某一个[^a-z]
表示一个字符,该字符不是a、b、c……z中的任何一个\w
:表示一个字符,该字符是数字、字母、下划线中的某一个。等价于`[(0-9)(a-z)(A-Z)(_)]``\W
:表示一个字符,该字符不是数字、字母、下划线中的任何一个。\d
表示一个字符,该字符是0、1、2……9中的某一个\D
表示一个字符,该字符不是0、1、2……9中的任何一个\s
表示一个字符,该字符是空白符(空格、制表符、换页符)
- 量词元字符
*
:字符*
要求字符
出现0到多次+
:字符+
要求字符
出现1到多次?
:字符?
要求字符
出现0次或1次{n}
:字符{n}
要求字符
出现n次{n,}
:字符{n,}
要求字符
出现n到多次{n,m}
:字符{n,m}
要求字符
出现n到m次
例如:
char regex_filename[] = “[a-zA-Z_] [a-zA-Z_0-9]*\\.[a-zA-Z0-9]+”;
上面的正则表达式可以解释如下:
在正则表达式中,字符类的定义(用[]
括起来的部分)是用来匹配任意一个在括号内的字符。字符的顺序在这个上下文中是无关紧要的。因此,对于[a-zA-Z_],它匹配单个字符,这个字符可以是任意小写字母(a-z
)、大写字母(A-Z
)或下划线(_
),而顺序并不会影响匹配结果。简而言之,[a-zA-Z_]
表示“匹配一个小写字母、大写字母或下划线中的任意一个”,顺序无所谓。
第二部分[a-zA-Z_0-9]*
表示文件名的第二个字符开始可以是字母、数字或下划线,这部分是可选的,可以出现零次或多次。
\\.
匹配点.
字符(点在正则表达式中是特殊字符,表示任意单个字符,所以需要用反斜杠\
进行转义,而在C字符串中反斜杠也是特殊字符,需要再次使用反斜杠进行转义)。
最后的[a-zA-Z0-9]+
表示文件扩展名至少包含一个字母或数字。
常用的正则表达式操作
在C++中,<regex>
库提供了几个与正则表达式相关的类,包括std::regex
用于表示正则表达式,以及std::smatch
用于保存匹配结果。常见的操作包括匹配、搜索、替换和分割字符串。
- 匹配:检查整个字符串是否符合正则表达式的模式。
- 搜索:在字符串中查找符合模式的子串。
- 替换:替换字符串中所有匹配正则表达式的部分。
- 分割:根据匹配到的模式分割字符串。
regex_match
- 用途:
regex_match
用来检查一个完整的字符串是否完全符合正则表达式的模式。换句话说,整个字符串从开始到结束都需要与正则表达式匹配,才会返回true
。 - 示例:如果你有一个正则表达式
\d+
(表示一个或多个数字),regex_match
只会对完全由数字组成的字符串返回true
。对于字符串"123",它返回true
;但对于"abc123"或"123abc",即使字符串中包含数字,它也会返回false
,因为整个字符串并不完全符合模式。
用法一
bool flag = regex_match(str, r1);
这里检查字符串str
是否完全匹配正则表达式r1
。因为str
从开始到结束都是小写字母和数字的组合,且r1
的模式是匹配一串小写字母或数字,所以str
与r1
是匹配的,flag
将会是true
。
用法二
bool flag1 = regex_match(str, regex("\\d+"));
在这个例子中,创建了一个临时的正则表达式对象regex("\\d+")
来匹配一个或多个数字。因为str
开头有非数字字符(“hhh”),regex_match
要求整个字符串完全符合正则表达式,所以这里str
并不完全匹配\\d+
,flag1
将会是false
。
用法三
bool flag2 = regex_match(str.begin()+5, str.end(), regex("\\d+"));
这个例子稍微复杂一点。它使用了regex_match
的一个重载版本,该版本接受一个字符序列的开始和结束迭代器,而不是整个字符串。str.begin()+5
指向字符串的第六个字符(str
中的最后一个’3’),因此这个调用实际上是检查字符串"3"是否匹配正则表达式\\d+
。因为"3"是一个数字,匹配\\d+
,所以flag2
将会是true
。
regex_search
- 用途:
regex_search
用来搜索字符串中的任意部分,以查找与正则表达式模式匹配的子串。只要字符串中有任何一部分符合正则表达式,regex_search
就会返回true
,不需要整个字符串完全匹配。
[a-z_]+
意味着至少有一个小写字母或下划线。
regex_search(mystr, m, regexp);
在字符串mystr
中搜索与正则表达式regexp
匹配的子串。匹配结果被存储在smatch
对象m
中。regex_search
函数会搜索整个字符串,但只返回第一个匹配的结果。如果要找到所有匹配结果,需要使用循环和迭代器,并在每次迭代中更新搜索的起始位置。
for (auto x : m) cout << x << " ";
这行代码遍历m
中的所有匹配项,并打印它们。smatch
类型的对象m
是一个容器,它包含所有匹配的子串。在这个例子中,尽管正则表达式可能与字符串中的多个部分匹配,但由于使用了regex_search
(而非regex_search
的循环调用),m
只包含第一个完整的匹配及其子匹配(如果有的话)。
-
在每次找到匹配项之后,通过
m.suffix().first
更新searchStart
,这样下一次调用regex_search
时就会从上一次匹配结束的位置开始搜索。 -
循环继续直到
regex_search
不能再在字符串中找到匹配项,此时循环结束。 -
在C++中,当使用
std::regex_search
配合std::smatch
对象进行正则表达式匹配时,std::smatch
是用来存储匹配结果的。这个对象除了能够给你匹配到的文本,还能提供额外的信息,比如匹配文本之前和之后的文本。std::smatch
继承自std::match_results
,其中包含了一系列用于访问匹配结果的成员函数。其中,
m.suffix()
是std::match_results
的一个成员函数,它返回一个std::sub_match
对象,这个对象表示最后一次匹配操作之后的所有未匹配的剩余字符串。std::sub_match
对象,如同std::smatch
,也提供了对匹配片段的访问,包括匹配的文本和匹配前后的文本。m.suffix().first
是访问std::sub_match
对象的first
成员,它是一个迭代器,指向m.suffix()
返回的剩余字符串的第一个字符。在regex_search
的上下文中,m.suffix().first
实际上提供了一个迭代器,指向上一次匹配操作之后剩余未匹配文本的开始位置。使用
m.suffix().first
的一个常见场景是在字符串中连续搜索多个匹配项。每次regex_search
找到一个匹配项后,可以使用m.suffix().first
作为下一次搜索的起点,这样就可以继续在字符串的剩余部分查找下一个匹配项,直到整个字符串被搜索完毕。这个方法允许程序逐个处理所有匹配项,而不仅仅是第一个匹配项。简而言之,
m.suffix().first
在使用正则表达式进行字符串搜索时,提供了一种有效的方法来更新搜索的起始位置,以便连续查找所有匹配项。
十三. 增强的元组
在 C++ 中已有⼀个 pair 模板可以定义⼆元组,C++11 更进⼀步地提供了变长参数的 tuple 模板:
ub_match对象的
first成员,它是一个迭代器,指向
m.suffix()返回的剩余字符串的第一个字符。在
regex_search的上下文中,
m.suffix().first`实际上提供了一个迭代器,指向上一次匹配操作之后剩余未匹配文本的开始位置。
使用m.suffix().first
的一个常见场景是在字符串中连续搜索多个匹配项。每次regex_search
找到一个匹配项后,可以使用m.suffix().first
作为下一次搜索的起点,这样就可以继续在字符串的剩余部分查找下一个匹配项,直到整个字符串被搜索完毕。这个方法允许程序逐个处理所有匹配项,而不仅仅是第一个匹配项。
简而言之,m.suffix().first
在使用正则表达式进行字符串搜索时,提供了一种有效的方法来更新搜索的起始位置,以便连续查找所有匹配项。
[外链图片转存中…(img-aZsG8sa6-1717401468963)]
十三. 增强的元组
在 C++ 中已有⼀个 pair 模板可以定义⼆元组,C++11 更进⼀步地提供了变长参数的 tuple 模板: