正则表达式
正则表达式(Regular Expression,常简写为regex、regexp或RE)。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。C++11开始支持正则表达式。
正则表达式非常强大,具体的实现算法有差异,所以会有多种实现方式。C++11支持6种正则表达式引擎。ECMAScript 是其中支持最多元素的引擎,也是regex默认支持的引擎。
- ECMAScript
- basic(POSIX Basic Regular Expressions)
- extended(POSIX Extended Regular Expressions )
- awk(POSIX awk)
- grep(POSIX grep )
- egrep(POSIX grep –E)
正则表达式主要两部分构成,特殊字符和普通字符。
字符 | 描述 |
\ | 转义字符 |
$ | 匹配字符行尾 |
* | 匹配前面的子表达式任意多次 |
+ | 匹配前面的子表达式一次或多次 |
? | 匹配前面的子表达式零次或一次 |
{m} | 匹配确定的m次 |
{m,} | 匹配至少m次 |
{m,n} | {m,n} |
. | 匹配任意字符 |
x | y |
[xyz] | 字符集合,匹配包含的任意一个字符 |
[^xyz] | 匹配未包含的任意字符 |
[a-z] | 字符范围,匹配指定范围内的任意字符 |
[^a-z] | 匹配任何不在指定范围内的任意字符 |
字符 | 描述 |
\w | 匹配字母或数字或下划线,任意一个字母或数字或下划线,即AZ,az,0~9,_中任意一个 |
\W | 匹配任意不是字母、数字、下划线的字符 |
\s | 匹配任意的空白符,包括空格、制表符、换页符等空白字符的其中任意一个,与”[ \f\n\r\t\v]”等效 |
\S | 匹配任意不是空白符的字符,与”[^\f\n\r\t\v]”等效 |
\d | 匹配数字,任意一个数字,0~9中的任意一个,等效于”[0-9]” |
\D | 匹配任意非数字的字符,等效于”[^0-9]” |
\b | 匹配一个字边界,即字与空格间的位置,也就是单词和空格之间的位置,不匹配任何字符,如,“er\b"匹配"never"中的"er”,但不匹配"verb"中的"er" |
\B | 非字边界匹配,“er\B"匹配"verb"中的"er”,但不匹配"never"中的"er" |
\f | 匹配一个换页符,等价于”\x0c”和”\cL” |
\n | 匹配一个换行符,等价于”\x0a”和”\cJ” |
\r | 匹配一个回车符,等价于”\x0d”和”\cM” |
\t | 匹配一个制表符,等价于”\x09”和”\cI” |
\v | 匹配一个垂直制表符,等价于”\x0b”和”\cK” |
\cx | 匹配”x”指示的控制字符,如,\cM匹配Control-M或回车符,”x”的值必须在”A-Z”或”a-z”之间,如果不是这样,则假定c就是"c"字符本身 |
regex_match
全文匹配,即要求整个字符串符合匹配规则,返回true或false
#include "iostream" #include "regex" using namespace std; int main() { std::string str="1111-1"; cout<<std::regex_match(str,regex("\\d{4}-\\d{1,2}"))<<endl;//匹配”四个数字-一个或两个数字“,返回值为1匹配成功 str="1320550505@qq.com"; cout<<regex_match(str,regex("\\d{1,}@qq.com"))<<endl;//匹配”QQ邮箱“,返回值为1匹配成功 return 0; }
regex_search
搜索匹配,即搜索字符串中存在符合规则的子字符串。
#include "iostream" #include "regex" using namespace std; int main() { string str= "hello2019-02-03word"; smatch match; regex regex1("(\\d{4})-(\\d{1,2})-(\\d{1,2})");//搜索规则()表示把内容拿出来,不加()的话只有match[0],没有[1][2][3] std::cout<<regex_search(str,match,regex1);//找到了返回1 std::cout<<match[0];//2019-02-03 std::cout<<match[1];//2019 std::cout<<match[2];//02 std::cout<<match[3];//03 return 0; }
#include "iostream" #include "regex" using namespace std; int main() { string str = "hello2019-02-03word,hello2019-02-03word,hello2019-02-03word "; smatch match; regex regex1("(\\d{4})-(\\d{1,2})-(\\d{1,2})");//搜索规则()表示把内容拿出来,不加()的话只有match[0],没有[1][2][3] string::const_iterator citer = str.cbegin(); while (regex_search(citer, str.cend(), match, regex1)) { citer = match[0].second;//将迭代器的指针指向匹配到的字符串后面的位置 for (size_t i = 1; i < match.size(); i++) { cout << match[i] << " ";//把三个括号里面的内容拿出来 } cout << endl; } return 0; }
regex_replace
替换匹配,即可以将符合匹配规则的子字符串替换为其他字符串。
#include "iostream" #include "regex" using namespace std; int main() { string str = "hello2019-02-03word,hello2019-02-03word,hello2019-02-03word"; regex regex1("-"); cout << regex_replace(str, regex1, "/") << endl;//hello2019/02/03word,hello2019/02/03word,hello2019/02/03word,函数的返回值就是代替之后的结果 return 0; }
chrono
关于chrono库该怎么用建议去我的另一篇专门写这个的文章看一看,有很多我自己敲的实例
新增数据结构
- std::forward_list:单向链表,只可以前进,在特定场景下使用,相比于std::list节省了内存,提高了性能
std::forward_list<int> fl = {1, 2, 3, 4, 5}; for (const auto &elem : fl) { cout << elem; }
- std::unordered_set:基于hash表实现的set,内部不会排序,使用方法和set类似
- std::unordered_map:基于hash表实现的map,内部不会排序,使用方法和map类似
- std::array:数组,在越界访问时抛出异常,建议使用std::array替代普通的数组
std::array是具有固定大小的数组。因此,它并不支持添加或删除元素等改变大小的操作。也就是说,当定义一个array时,除了指定元素类型,还要指定容器大小。
既然有了内置的数组,为什么还要引入array呢?
内置的数组有很多麻烦的地方,比如无法直接对象赋值,无法直接拷贝等等,同时内置的数组又有很多比较难理解的地方,比如数组名是数组的起始地址等等。相比较于如vector等容器的操作,内置数组确实有一些不方便的地方。因此,C++11就引入array容器来代替内置数组。
简单来说,std::array除了有内置数组支持随机访问、效率高、存储大小固定等特点外,还支持迭代器访问、获取容量、获得原始指针等高级功能。而且它还不会退化成指针给开发人员造成困惑。
使用array之前,需要包含头文件:
# include <array>
定义array时,需要指定其数据类型和大小,两者不可或缺。同时,array的大小不能使用变量来指定,但对于内置数组来说,是可以使用变量来指定数组大小的。
定义array时,可以使用{}来直接初始化,也可以使用另外的array来构造,但不可以使用内置数组来构造。
std::array提供了[]、at、front、back、data的方式来进行元素:
访问方式 | 含义 |
at | 访问指定的元素,同时进行越界检查 |
[] | 访问指定的元素 |
front | 访问第一个元素 |
back | back |
data | 返回指向内存中数组第一个元素的指针 |
和一般的容器一样,array还提供了迭代器的方式进行元素遍历和访问:
迭代器 | 含义 |
begin | 返回指向容器第一个元素的迭代器 |
end | 返回指向容器尾端的迭代器 |
rbegin | 返回指向容器最后元素的逆向迭代器 |
rend | 返回指向前端的逆向迭代器 |
array支持其它一些函数:
函数 | 含义 |
empty | 检查容器是否为空 |
size | 返回容纳的元素数 |
max_size | 返回可容纳的最大元素数 |
fill | 以指定值填充容器 |
swap | 交换内容 |
- std::tuple:元组类型,类似pair,但比pair扩展性好
typedef std::tuple<int, double, int, double> Mytuple; Mytuple t(0, 1, 2, 3); std::cout << "0 " << std::get<0>(t); std::cout << "1 " << std::get<1>(t); std::cout << "2 " << std::get<2>(t); std::cout << "3 " << std::get<3>(t);
新增算法
- all_of:检测表达式是否对范围[first, last)中所有元素都返回true,如果都满足,则返回true
std::vector<int> v(10, 2); if (std::all_of(v.cbegin(), v.cend(), [](int i) { return i % 2 == 0; })) { std::cout << "All numbers are even\n"; }
- any_of:检测表达式是否对范围[first,
last)中至少一个元素返回true,如果满足,则返回true,否则返回false,用法和上面一样 - none_of:检测表达式是否对范围[first,
last)中所有元素都不返回true,如果都不满足,则返回true,否则返回false,用法和上面一样 - find_if_not:找到第一个不符合要求的元素迭代器,和find_if相反
- find_if_not:找到第一个不符合要求的元素迭代器,和find_if相反
- copy_if:复制满足条件的元素
- itoa:对容器内的元素按序递增
- minmax_element:返回容器内最大元素和最小元素位置
int main() { std::vector<int> v = {3, 9, 1, 4, 2, 5, 9}; auto result = std::minmax_element(v.begin(), v.end()); std::cout << "min element at: " << *(result.first) << '\n'; std::cout << "max element at: " << *(result.second) << '\n'; return 0; } // min element at: 1 // max element at: 9
- is_sorted、is_sorted_until:返回容器内元素是否已经排好序。
🎂三、C++14新特性
函数返回值类型推导
C++14对函数返回类型推导规则做了优化,返回值也可推导,需要注意的几点
- 返回值类型推导也可以用在模板中
- 函数内如果有多个return语句,它们必须返回相同的类型,否则编译失败
- 如果return语句返回初始化列表,返回值类型推导也会失败
- 如果函数是虚函数,不能使用返回值类型推导
- 返回类型推导可以用在前向声明中,但是在使用它们之前,翻译单元中必须能够得到函数定义
- 回类型推导可以用在递归函数中,但是递归调用必须以至少一个返回语句作为先导,以便编译器推导出返回类型
lambda参数auto
在C++11中,lambda表达式参数需要使用具体的类型声明,在C++14中,对此进行优化,lambda表达式参数可以直接是auto:
#include "iostream" int main() { auto f = [](auto a){ std::cout<<a<<std::endl; }; f(5);//5 f("tom");//tom return 0; }
变量模板
C++14支持变量模板:
#include "iostream" template<class T> constexpr T pi = T(3.1415926); int main() { std::cout << pi<int> << std::endl;//3 std::cout << pi<double> << std::endl;//3.14 return 0; }
别名模板
C++14也支持别名模板:
#include "iostream" template<class T,typename K> constexpr T pi = T(3.1415926*(K)1.5); int main() { std::cout << pi<int,double> << std::endl;//4 std::cout << pi<double,int> << std::endl;//3.14159 return 0; }
constexpr的限制
C++11中constexpr函数必须必须把所有东西都放在一个单独的return语句中,而14中constexpr则无此限制
C++11中constexpr函数可以使用递归,在C++14中可以使用局部变量和循环
#include "iostream" constexpr int factorial(int n) { // C++11中不可,C++14中可以 int ret = 0; for (int i = 0; i < n; ++i) { ret += i; } return ret; } int main() { std::cout << factorial(5) << std::endl;//10 return 0; }
[[deprecated]]标记
C++14中增加了deprecated标记,修饰类、变、函数等,当程序中使用到了被其修饰的代码时,编译时被产生警告,用户提示开发者该标记修饰的内容将来可能会被丢弃,尽量不要使用。
struct [[deprecated]] A { }; int main() { A a; return 0; }
二进制字面量与整形字面量分隔符
C++14引入了二进制字面量,也引入了分隔符,防止看起来眼花哈~
#include "iostream" int main() { int a = 111'111'1111; std::cout<<a;//1111111111 return 0; }
std::make_unique
我们都知道C++11中有std::make_shared,却没有std::make_unique,在C++14已经改善。格式如下
struct A {}; std::unique_ptr<A> ptr = std::make_unique<A>();
std::shared_timed_mutex与std::shared_lock
C++14通过std::shared_timed_mutex和std::shared_lock来实现读写锁,保证多个线程可以同时读,但是写线程必须独立运行,写操作不可以同时和读操作一起进行。
struct ThreadSafe{ mutable std::shared_timed_mutex mutex; int value = 0; int getValue() const { std::shared_lock<std::shared_timed_mutex> lock(mutex); return value; } void setValue(int value) { std::unique_lock<std::shared_timed_mutex> loc(mutex); ThreadSafe::value = value; } };
std::integer_sequence
#include "iostream" #include "mutex" #include "shared_mutex" template<class T,T... ints> void func(std::integer_sequence<T,ints...> int_seq) { std::cout<<int_seq.size();//7 } int main() { func(std::integer_sequence<int, 9, 2, 5, 1, 9, 1, 6>{}); return 0; }
std::exchange
exchange内部实现如下,可以看见new_value的值给了obj,而没有对new_value赋值,这里相信您已经知道了它和swap的区别了吧!
template<class T, class U = T> constexpr T exchange(T& obj, U&& new_value) { T old_value = std::move(obj); obj = std::forward<U>(new_value); return old_value; }
swap是交换两个容器的值,exchange只是将一方的值给另一方
#include "iostream" #include "mutex" #include "shared_mutex" #include "vector" using namespace std; int main() { std::vector<int> v = {1}; std::vector<int> z = {1, 2}; std::exchange(v, z); cout << v.size() << endl;//2 cout << z.size() << endl;//2 for (int a: v) { cout << a << " "; } v = {1}; z = {1, 2}; std::swap(v, z); cout << v.size() << endl;//2 cout << z.size() << endl;//1 for (int a: v) { cout << a << " "; } return 0; }
std::quoted
C++14引入std::quoted用于给字符串添加双引号,直接看代码:
#include <iomanip> #include "iostream" using namespace std; int main() { string str = "hello world"; cout << str << endl;//hello world cout << std::quoted(str) << endl;//"hello world" return 0; }
🎂四、C++17新特性
结构化绑定
通过结构化绑定,对于tuple、map等类型,获取相应值会方便很多
#include <iomanip> #include "iostream" #include "vector" #include "tuple" using namespace std; auto fuc() { return tuple<int, double>(1, 2.0); } int main() { auto [i, d] = fuc(); std::cout << i << " " << d << endl; return 0; }
if-switch语句初始化
C++17之后可以这样:
#include <iomanip> #include "iostream" #include "vector" #include "tuple" using namespace std; int main() { if (int i = 0;i <= 5) { std::cout << i << std::endl;//0 } return 0; }
内联变量
C++17前只有内联函数,现在有了内联变量,我们印象中C++类的静态成员变量在头文件中是不能初始化的,但是有了内联变量,就可以达到此目的:
#include <iomanip> #include "iostream" #include "vector" #include "tuple" using namespace std; class tom { public: inline static int a = 0; }; int main() { std::cout << tom::a << std::endl;//0 return 0; }
折叠表达式
C++17引入了折叠表达式使可变参数模板编程更方便:
#include <iomanip> #include "iostream" #include "vector" #include "tuple" using namespace std; template <typename ... Ts> auto sum(Ts ... ts) { return (ts + ...); } int main() { int a {sum(1, 2, 3, 4, 5)}; // 15 std::string c{"hello "}; std::string b{"world"}; cout << sum(c, b) << endl; // hello world return 0; }
constexpr lambda表达式
C++17前lambda表达式只能在运行时使用,C++17引入了constexpr lambda表达式,可以用于在编译期进行计算。
#include <iomanip> #include "iostream" #include "vector" #include "tuple" using namespace std; int main() { constexpr auto lamb = []() { std::cout << 1; }; lamb(); return 0; }
😆小知识
constexpr函数有如下限制:
函数体不能包含汇编语句、goto语句、label、try块、静态变量、线程局部存储、没有初始化的普通变量,不能动态分配内存,不能有new delete等,不能虚函数。
namespace嵌套
namespace A { namespace B { namespace C { void func(); } } } // c++17,更方便更舒适 namespace A::B::C { void func();) }
__has_include预处理表达式
可以判断是否有某个头文件,代码可能会在不同编译器下工作,不同编译器的可用头文件有可能不同,所以可以使用此来判断:
#if defined __has_include #if __has_include(<iostream>) #define has_iostream 1 #include "iostream" #endif #endif int main() { #ifdef has_iostream std::cout<<"yes"; #elif printf("no"); #endif return 0; }
在lambda表达式用*this捕获对象副本
正常情况下,lambda表达式中访问类的对象成员变量需要捕获this,但是这里捕获的是this指针,指向的是对象的引用,正常情况下可能没问题,但是如果多线程情况下,函数的作用域超过了对象的作用域,对象已经被析构了,还访问了成员变量,就会有问题。
struct A { int a; void func() { auto f = [this] { cout << a << endl; }; f(); } }; int main() { A a; a.func(); return 0; }
所以C++17增加了新特性,捕获*this,不持有this指针,而是持有对象的拷贝,这样生命周期就与对象的生命周期不相关啦。
struct A { int a; void func() { auto f = [*this] { // 这里 cout << a << endl; }; f(); } }; int main() { A a; a.func(); return 0; }
新增Attribute
C++17 引入了新的 Attribute 语法来给函数、变量、类型等添加元数据信息。Attribute 可以用于优化、调试、静态分析等方面。下面是一些示例:
- [[nodiscard]]:用于指示一个函数的返回值应该被检查,如果没有使用返回值会产生编译器警告。
[[nodiscard]] int func() { return 0; } int main() { func(); // 编译器会提示忽略了未使用的返回值 return 0; }
- [[fallthrough]]:用于标记 switch 语句中故意落入下一个 case 的情况。
switch (n) { case 1: do_something(); [[fallthrough]]; case 2: do_something_else(); break; default: break; }
- [[maybe_unused]]:用于消除未使用变量的编译器警告。
void func([[maybe_unused]] int n) { // ... }
- [[deprecated]]:用于标记已经过时的函数或变量。
[[deprecated]] void old_func() { // ... }
- [[nodiscard(“message”)]]:用于指定警告消息,当返回值没有被使用时,输出指定的警告信息。
[[nodiscard("Please check the return value of this function.")]] int func() { return 0; }
😆小知识
Attribute 不是标准 C++ 的一部分,而是 GCC 和 Clang 支持的扩展功能,因此不同编译器的支持程度可能会有所不同。
字符串转换
C++17 新特性中新增的 from_chars() 和 to_chars() 函数。
from_chars() 和 to_chars() 函数都是用于将数据类型转换为字符序列或者从字符序列转换为数据类型。其中,from_chars() 用于将字符序列解析为数值类型,并返回解析后的结果及指向未解析部分的指针;to_chars() 则相反,用于将数值类型转换为字符序列,并返回指向输出缓冲区尾部的指针。
#include <charconv> #include <iostream> int main() { std::string str = "123"; int num; auto [p, ec] = std::from_chars(str.data(), str.data() + 1, num); if (ec == std::errc()) { std::cout << "The parsed number is: " << num << '\n'; std::cout << p << '\n';//指向后一位指针 } else std::cout << "Error parsing number\n"; return 0; }
用 to_chars() 函数将整型数值转换为字符序列
#include <charconv> #include <iostream> int main() { int num = 123; char buf[20]; auto [p, ec] = std::to_chars(buf, buf+sizeof(buf), num); if (ec == std::errc()) std::cout << "The string representation of the number is: " << buf << '\n'; else std::cout << "Error converting number\n"; return 0; }
std::variant
std::variant 是一个用于存储多个不同类型对象的类型,类似于 C++11 引入的 std::any,但 std::variant 只能存储预定义的一组类型(也就是 variant 的模板参数),而且它在编译期就已经确定了所包含的类型。与 std::any 不同,std::variant 仅支持对于自身包含的类型进行类型安全操作而避免了运行时开销,同时也更加灵活和易于使用。
下面是一个简单的示例,演示如何定义、初始化和访问 std::variant:
c++ #include <variant> #include <iostream> #include <string> int main() { std::variant<int, double, std::string> v; // 定义一个可存储 int、double 和 std::string 类型的 variant v = 3.1415926; // 将 double 值赋值给 variant double d = std::get<double>(v); // 使用 std::get 获取 variant 中的 double 值 std::cout << "The value of v is: " << d << '\n'; v = "Hello, world!"; // 将字符串赋值给 variant std::string s = std::get<std::string>(v); // 使用 std::get 获取 variant 中的字符串 std::cout << "The value of v is: " << s << '\n'; return 0; }
在上面的示例中,我们首先定义了一个可存储 int、double 和 std::string 类型的 variant。然后,我们将一个 double 值赋值给 variant,并使用 std::get 获取 variant 中的 double 值。接着,我们将一个字符串赋值给 variant 并使用 std::get 获取 variant 中的字符串。
需要注意的是,std::get() 函数是一个重载函数,它的函数名和参数类型用于指定要访问的 variant 中的具体类型,而在访问时如果该类型并不存在,则会抛出 std::bad_variant_access 异常。
除了使用 std::get() 函数外,还可以使用 std::visit() 函数,来对 variant 中的对象进行操作,例如:
#include <variant> #include <iostream> #include <string> struct PrintVisitor { void operator()(int i) const { std::cout << "The value of variant is: " << i << '\n'; } void operator()(double d) const { std::cout << "The value of variant is: " << d << '\n'; } void operator()(const std::string& s) const { std::cout << "The value of variant is: " << s << '\n'; } }; int main() { std::variant<int, double, std::string> v; v = 3.1415926; std::visit(PrintVisitor{}, v); return 0; }
在上述示例中,我们定义了一个 PrintVisitor 结构体,并重载了它的 operator() 函数,以处理 variant 中的 int、double 和 std::string 类型的数据。然后,我们使用 std::visit() 函数来对 variant 中的对象进行操作,同时也实例化了 PrintVisitor 结构体。由于我们只需要访问 variant 中的 double 值,所以在使用 std::visit() 时,我们传入了一个 PrintVisitor 的实例和 variant 对象,这样就可以输出 variant 中的 double 值。
总之,std::variant 类型为 C++ 提供了一种类型安全且高效的多类型数据存储方式,既避免了运行时开销,又提高了代码的灵活性。
std::optional
std::optional 是一个用于表示可能不存在值的类型,类似于指针,但是它可以避免空指针引发的异常。当某个值可能不存在时,我们可以将其封装到 std::optional 对象中,以避免在访问该值时出现异常。
下面是一个简单的示例,演示如何定义、初始化和访问 std::optional:
#include <optional> #include <iostream> int main() { std::optional<int> opt; if (opt.has_value()) { // 判断 optional 是否包含值 int i = opt.value(); // 使用 value() 获取 optional 中的值 std::cout << "The value of opt is: " << i << '\n'; } else { std::cout << "opt does not have a value\n"; } opt = 42; // 将一个值赋给 optional if (opt.has_value()) { int i = opt.value(); std::cout << "The value of opt is: " << i << '\n'; } else { std::cout << "opt does not have a value\n"; } return 0; }
#include <optional> #include <iostream> int main() { std::optional<int*> opt; if (opt.has_value()) { // 判断 optional 是否包含值 auto i = opt.value(); // 使用 value() 获取 optional 中的值 std::cout << "The value of opt is: " << i << '\n'; } else { std::cout << "opt does not have a value\n"; } return 0; }
在上面的示例中,我们首先定义了一个 std::optional 对象,并检查它是否包含值。由于该对象没有被初始化,因此不包含任何值。接着,我们将一个整数值赋给该对象,并再次检查它是否包含值。在检查时,我们使用了 has_value() 函数来判断 optional 是否包含值,并使用 value() 函数获取 optional 中的值。
需要注意的是,当 optional 对象未被初始化时,它并不包含任何值,此时访问其内容会抛出 std::bad_optional_access 异常。因此,在对 optional 进行访问之前,必须先确保它已经包含值。
除了使用 has_value() 和 value() 函数外,std::optional 还提供了一系列便捷的函数,如:
reset():将 optional 设置为无值状态
operator bool():将 optional 转换为 bool 值,true 表示 optional 包含值,false 表示无值
value_or(T&& default_value):获取 optional 中的值,如果 optional 为空,则返回提供的默认值