C++实战笔记(四)

智能指针

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+)"); //多次编译正则表达式对象,降低运行效率
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值