智能指针
unique_ptr
unique_ptr是最简单、最容易使用的智能指针之一,在声明的时候必须用模板参数指定类型,例如:
unique_ptr<int> ptr1(new int(10)); //int智能指针
assert(*ptr1 = 10); // 使用*获取内容
assert(ptr1 != nullptr); //可以判断是否为空指针
unique_ptr<string> ptr2(new string("hello")); //string智能指针
assert(*ptr2 = "hello"); // 可以使用*获取内容
assert(ptr2->size() == 5); //可以使用"->"调取成员函数
需要特别注意,unique_ptr虽然叫指针,用起来也像指针,但它实际上并不是指针,而是一个对象。所以不要试图对unique_ptr调用delete,它会自动管理初始化的地址,在离开作用域时释放资源。
另外unique_ptr没有定义加减运算,不能随意移动指针地址,这就完全避免也指针越界等危险操作,让代码更安全:
ptr1++; //导致编译错误
ptr2 += 2; //导致编译错误
使用工厂函数make_unique来创建unique_ptr指针,make_unique定义在C++14中。
auto ptr3 = make_unique<int>(42);
assert(ptr3 && *ptr3 == 42);
auto ptr4 = make_unique<string>("Hello C++"); // 调用工厂函数创建智能指针
assert(!ptr4->empty());
如果我们使用C++11,可以参考下面的代码,自己实现一个简化版的make_unique():
template<class T, class... Args> //可变参数模板
std::unique_ptr<T>
my_make_unique(Args&&... args) //可变参数模板的入口参数
{
return std::unique_ptr<T>( //构造智能指针
new T(std::forward<Args>(args)...)); //"完美转发"
}
unique_ptr指针的所有权是唯一的,不允许共享,任何时候只能有一个"人"持有它。
为了实现这个目的,unique_ptr应用了C++转移语义,同时禁止了复制赋值,因此在向另一个unique_ptr赋值的时候要注意,必须用std::move()显式地声明所有权转移。赋值操作之后,所有权就被转移了,原来的unique_ptr变成了空指针,新的unique_ptr接替了所有权,从而保证了所有权的唯一性:
auto ptr1 = make_unique<int>(42); //调用工厂函数创建智能指针
assert(ptr1 && *ptr1 == 42); //智能指针有效
auto ptr2 = std::move(ptr1); //使用move()转移所有权
assert(!ptr1 && ptr2); //ptr1变成了空指针
因为unique_ptr禁止复制,只能转移,所以如果在定义类时将unique_ptr作为成员,那么类本身也是不可复制的。也就是说,unique_ptr会把它的“唯一所有权”特性传递给它的持有者。
共享指针
shared_ptr(共享指针)是一个比unique_ptr更智能的智能指针。
shared_ptr<int> ptr1(new int(10)); //int智能指针
assert(*ptr = 10); //此时智能指针有效
shared_ptr<string> ptr2(new string("hello")); //string智能指针
assert(*ptr2 == "hello");
auto ptr3 = make_shared<int>(42); //调用工厂函数创建智能指针
assert(ptr3 && *ptr3 = 42);
auto ptr4 = make_shared<string>("zelda"); //调用工厂函数创建智能指针
assert(!ptr4->empty()); //可以使用"->"调用成员函数
它的所有权可以被安全共享,即支持复制赋值,允许被多人同时持有,就像原始指针一样。
auto ptr1 = make_shared<int>(42); //调用工厂函数创建智能指针
assert(ptr1 && ptr1.use_count() == 1); //此时智能指针有效且唯一
auto ptr2 = ptr1; //直接复制赋值,不需要使用Move
assert(ptr1 && ptr2);
assert(ptr1 == ptr2); //shared_ptr可以直接比较
assert(ptr1.use_count() == 2); //均不唯一,且引用计数为2
assert(ptr2.use_count() == 2);
弱引用指针
shared_ptr的引用计数会导致一个新问题,就是循环引用,这是把shared_ptr作为类成员的时候极容易出现,典型的例子就是链表节点。
class Node final
{
public:
using this_type = Node; //类型别名
using shared_type = std::shared_ptr<this_type>;
public:
share_ptr next; //使用智能指针来指向下一个节点
};
auto n1 = make_shared<Node>();
auto n2 = make_shared<Node>();
assert(n1.use_count() == 1); //引用计数1
assert(n2.use_count() == 1); //引用计数1
n1->next = n2; //两个节点互指,形成了循环引用
n2->next = n1;
assert(n1.use_count() == 2); //引用计数为2
assert(n2.use_count() == 2); //无法减到0,无法销毁,导致内存泄漏
这时,我们需要用到weak_ptr。weak_ptr,顾名思义,功能很"弱",它专为打破循环引用而设计,只观察指针,不增加引用计数,但在需要的时候,weak_ptr可以调用成员函数lock(),获取shared_ptr.
以上例子,我们改用weak_ptr。
class Node final
{
public:
using this_type = Node; //类型别名
using shared_type = std::weak_ptr<this_type>;
public:
share_ptr next; //使用智能指针来指向下一个节点
};
auto n1 = make_shared<Node>();
auto n2 = make_shared<Node>();
assert(n1.use_count() == 1); //引用计数1
assert(n2.use_count() == 1); //引用计数1
n1->next = n2; //两个节点互指,形成了循环引用
n2->next = n1;
assert(n1.use_count() == 1); //因为使用了weak_ptr,引用计数为1
assert(n2.use_count() == 1); //打破循环引用,不会导致内存泄漏
if(!n1->next.expired()) //检查weak_ptr是否有效
{
auto ptr = n1->next().lock(); //lock获取shared_ptr
assert(ptr == n2);
}
weak_ptr的expired()相当于shared_ptr的use_count() == 0,用来判断weak_ptr是否有效。
weak_ptr的另一个重要作用是让类正确地自我创建shared_ptr。对象内部使用weak_ptr来保管this指针,然后调用lock()获取shared_ptr。
这就要用到辅助类enable_shared_from_this。需要自我管理的类必须以继承方式使用它,之后就可以用成员函数shared_from_this()创建shared_ptr。
原始字符串
C++11为字面量增加了一个“原始字符串”的新表示形式,比原来的引号多了一个大写字母R和一对圆括号,就像下面这样:
auto str = R"(nier:automata)"; //原始字符串
C++的字符串有转义的用法:在前面加上一个"\",就可以写出"\n" "\t"来表示回车、跳格等不可输出字符。有时我们不想要转义,只想要字符串的原始形式。
auto str1 = R"(char""'')"; //原样输出: char""''
auto str2 = R"(\r\n\t\")"; //原样输出:\r\n\t\"
auto str3 = R"(\\\$)"; //原样输出:\\\$
对于想要在原始字符串里写"引号+圆括号"的形式,C++也准备了对应的办法,就是在圆括号的左右两边加上最多16个字符的特别界定符,这样就保证不与字符串内容发生冲突。
例如,下面的原始字符串就添加了"=="作为界定符(也可以用其他任意字符序列):
auto str = R"==(R"(xxx)")=="; //原样输出:R"(xxx)"
字符串转换函数
C++11新增的转换函数
- stoi()/stol()/stoll()等可以把字符串转换为整数
- stof()/stod()等可以把字符串转换成浮点数
- to_string()可以把整数、浮点数转换成字符串
assert(stoi("42") == 42); //字符串转整数
assert(stol("253") == 253L); //字符串转长整数
assert(stod("2.0") == 2.0); //字符串转浮点数
assert(to_string(1984) == "1984"); //整数转字符串
字面值后缀
C++14新增了一个字面值后缀"s",明确地表示它是字符串类型,而不是C字符串。这样在声明的时候就可以利用自动类型推导,而且在其他用到字符串的地方,也可以省去声明临时字符串变量的麻烦,效率也会更高。
using namespace std::literals; //必须打开名字空间
auto str = "std string"s; //后缀s表示标准字符串,自动推导类型
assert("time"s.size() == 4); //标准字符串可以直接调用成员函数
字符串视图
C++17新增了一个字符串类string_view,它是一个字符串的视图,成本很低,内部只保存一个指针和长度,无论是复制还是修改都非常廉价。
class string_view
{
private:
const char* m_ptr;
std::size_t m_size;
public:
...
};
这段代码四个关键点:
1. 因为内部使用了常量指针,所以它是一个只读视图,只能查看字符串而无法修改,相当于"const string &",用起来很安全。
2.因为内部使用了字符指针,所以它可以直接从C字符串构造,没有"const string&"的临时对象创建操作,所以适用面积广成本低。
3.因为使用的是指针,所以必须要当心引用的内容可能会失效,这一点和weak_ptr有些类似,两者都是弱引用
4.因为它是一个只读视图,所以不能保证字符串末尾一定是以NULL,无法提供成员函数c_str(),也就是说,不能把string_view转换成NULL结尾的字符指针,不能把它用于C函数传参。
示例:
string_view sv; //默认构造函数
assert(sv.empty()); //字符串视图为空
sv = "fantasy"s;
assert(sv.size() == 7); //从string构造
sv = "c++";
assert(sv.size() == 3); //从C字符串构造
string_view sv2("std c++", 3); //指定字符数和长度构造
assert(sv2 == "std");
我们可以将string_view作为string的轻量级“替身”,用在只读、弱引用的场合,从而降低字符串的使用成本。
当形参是string_view时,你就可以传入string、 char*、字符串字面量(常量).
而如果以const string&为参数,则不能传入字符串字面量常量和 char*。只能用string。
字符串格式化
C++20新增了一个专门的格式化库format,它提供了一个专门的格式化函数format(),作用类似printf(),但使用了可变参数模板,从而既支持任意数量的参数,又实现了编译期类型检查,用起来非常安全。format()不再使用传统的“%”,而是使用与python/c#类似的"{}",可读性很好。
format("{}", 100L); //直接格式化输出整数
format("{0}-{0}, {1}, {2}", "hello", 2.718, 3.14); //可以用序号引用后面的参数
输出:
100
hello-hello, 2.718, 3.14
格式占位符的基本形式是"{序号:格式标志}",里面不仅可以用序号(从0开始)引用后面的参数,还可以添加各种标志来定制格式,简单列举如下:
- <:数据左对齐
- >:数据右对齐
- +:为数字添加正负号标记
- -:为负数添加“-”标记,正数无标记
- 空格:为负数添加“-”标记,正数前加空格
- b:格式化为二进制整数
- d:格式化为十进制整数
- o:格式化为八进制整数
- x/X:格式化为十六进制整数
- #:非十进制数字显示“0b”,“0o”,“0x”前缀
- 数字:格式化输出的宽度
format("{:>10}", "hello"); //右对齐,10个字符的宽度
format("{:04}"), {:+04}, 100L, 88); //指定填充和宽度,默认是十进制
format("{0:x}, {0:#X}", 100L); //格式化为十六进制
format("{:04o}, {:04b}", 7, 5); //格式化为八进制/二进制,宽度是4
输出:
hello
0100, +088
64, 0X64
0007, 0101
想要输出“{}”,就要用特殊的“{{}}”形式,例如:
format("{{xxx}}"); //输出{xxx}
正则表达式
C++11引入了正则表达式库regex,利用它的强大功能,我们能够任意操作字符串以及文本。
正则表达式是一种特殊的模式描述语言,专门为处理文本而设计,它定义了一套严谨的语法规则,按照这套规则去书写模式,就可以实现复杂的匹配、查找和替换工作。
基本语法规则:
- .:匹配任意单个字符
- $:匹配行的末尾
- ^:匹配行的开头
- ():定义子表达式,可以被引用或者重复
- *:表示元素可以重复任意多次
- +:表示元素可以重复一次或多次
- ?:表示元素可以重复0次或1次
- |:匹配它两侧的元素之一
- []:定义字符集合,列出单个字符、定义范围,或者是集合的补集
- \:转义符,特殊字符经转义后与自身匹配
C++正则表达式主要使用两个类:
regex:表示一个正则表达式,是basic_regex的特化形式
smatch:表示正则表达式的匹配结果,是match_results的特化形式
在创建正则表达式对象的时候,我们还可以传递一些特殊的标志,用于控制正则的处理过程,这些标志都位于名字空间std::regex_constants中,比较常用一下几种
- icase:匹配时忽略大小写,即大小写不敏感
- optimize:要求尽量优化正则表达式,但会增加正则表达式对象的构造时间
- ECMAScript:使用ECMAScript兼容语法,这也是默认的语法
- awk/grep/egrep/:使用awk/grep/egrep等语法。
忽略大小写且优化表达式
using namespace std::regex_constants; //打开标志所在的名字空间
regex reg1{"xyz", icase | optimize}; //忽略大小写且尽量优化
正则表达式算法:
- regex_match:完全匹配一个字符串
- regex_search:在字符串里查找一个正则匹配
- regex_replace:先正则查找再替换
所以,我们只要用regex定义好一个表达式对象,再调用相应的匹配算法,就可以立刻得到结果。不过在写正则表达式的时候,记得最好用原始字符串,不然转义符会让人备受折磨。
auto make_regex=[](const auto& txt) //创建正则表达式
{
return std::regex(txt);
};
auto make_match = []() //返回正则匹配结果
{
return std::smatch();
};
正则匹配
有了正则表达式对象之后,我们就可以调用regex_match检查字符串,它返回bool值,表示目标是否完全匹配正则表达式里定义的模式:
auto reg = make_regex(R"(^(\w+)\:(\w+)$)"); //使用原始字符串定义正则表达式
assert(regex_match("a:b", reg)); //正则匹配成功
assert(!regex_match("a,b", reg)); //正则匹配失败
regex_match还有一个包含3个参数的重载形式,如果匹配成功,结果就会储存在第二个参数what里,然后像数组那样去访问子表达式:
auto str = "neir:automata"s;
auto what = make_match(); //准备获取匹配的结果
assert(regex_match(str, what, reg)); //正则匹配成功
assert(what[1] == "neir"); //第一个子表达式
assert(what[2] == "automata"); //第二个子表达式
for(const auto &x : what){
cout << x << ','; //输出neir:automata,neir,automata,
}
使用regex_match时候要注意一点,如果想要获取捕获结果,那么目标字符串绝不能是临时对象,也就是说不能直接写字面值,若使用下面的写法会出现编译警告:
regex_match("xxx", what, reg); //不能是字面值等临时变量
因为匹配结果需要引用字符串,而临时变量在函数调用后就消失了,会导致引用无效。
正则查找
regex_search用法与regex_match非常相似,它不要求全文匹配,只要找到一个符合模式的子串就行,见下面示例代码:
auto str = "god of war"s; //待匹配的字符串
auto reg = make_regex(R"((\w+)\s(\+w))"); //使用原始字符串定义正则表达式
auto what = make_match(); //准备获取匹配的结果
auto found = regex_search(str, what, reg); //正则查找,和匹配类似
assert(found);
assert(!what.empty());
assert(what[1] == "god"); //第一个子表达式
assert(what[2] == "of"); //第二个子表达式
正则替换
regex_replace,它不是要匹配结果,而是要提供一个替换字符串。例如:
auto new_str = regex_replace( //正则替换,返回新字符串
str, //原字符串不改动
make_regex(R"(\w+$)"), //正则表达式对象
"peace" //需要指定替换的文字
);
cout << new_str << endl; //输出god of peace
regex_replace搭配"^$"可以完成字符串的"修剪"工作
cout << regex_replace(
" xxx ",
make_regex("^\\s+"), "") //删除前导字符
<< endl; //输出 "xxx "
cout << regex_replace(
" xxx---",
make_regex("\\-+$"), "") //删除后面的"-"
<< endl; //输出" xxx"
因为算法是只读的,所以regex_replace会返回修改后的新字符串。善用这一点,就可以把它的输出作为另一个函数的输入,用"函数套函数"的形式实现函数式编程:
cout << regex_replace( //嵌套正则替换
regex_replace(
str, //原字符串不改动
make_regex("\\w+$"),
"peace"
),
make_regex("^\\w+"),
"godness") //要替换的文字
<< endl; //输出godness of peace
regex_replace第三个参数(替换文本)也遵循了PCRE正则表达式标准,可以用"$N"来引用匹配的子表达式,用“$&”引用整个匹配结果,
cout << regex_replace(
"hello mike",
make_regex(R"((\w+)\s(\w+))"), //正则表达式对象,有两个子表达式
"$2-says-$1($&)") //替换引用了子表达式
<< endl; //输出"mike-says-hello(hello mike)"
使用regex时候,我们还要注意正则表达式的成本。因为正则表达式对象无法由C++编译器做静态检查,它只有在运行阶段才会由正则引擎处理,而语法检查和动态编译的代价很高,所以我们尽量不要反复创建正则表达式对象,能重用就重用。在使用循环的时候更要特别注意,一定要把正则表达式放到循环体外。
for(int i = 0; i < 100; ++i)
{
auto reg = make_regex(R"(\w+)"); //多次编译正则表达式对象,降低运行效率
}