关于stl:C-STL-array容器

一、前言array 容器是 C++ 11 规范中新增的序列容器,就是在 C++ 一般数组的根底上,增加了一些成员函数和全局函数。在应用上,它比一般数组更平安,且效率并没有因而变差。和其它容器不同,array 容器的大小是固定的,无奈动静的扩大或膨胀,只容许拜访或者替换存储的元素。 二、详解array 容器以类模板的模式定义在 <array> 头文件,并位于命名空间 std 中。因而,在应用该容器之前,代码中需引入 <array> 头文件,并默认应用 std 命令空间。即: #include <array>using namespace std;在 array<T,N> 类模板中,T 用于指明容器中的存储的具体数据类型,N 用于指明容器的大小,N 必须是常量,不能用变量示意。 1、初始化① std::array<int, 10> values; ② std::array<int, 10> values {};③ std::array<double, 10> values {2,1,4,9};以上都能够对array进行初始化,区别在于:①各个元素的值是不确定的(array 容器不会做默认初始化操作)②将所有的元素初始化为 0 或者和默认元素类型等效的值③能够初始化一部分值,残余的均初始化值默认为0 2、成员函数成员函数性能begin()返回指向容器中第一个元素的随机拜访迭代器end()返回指向容器最初一个元素之后一个地位的随机拜访迭代器,通常和 begin() 联合应用rbegin()返回指向最初一个元素的随机拜访迭代器rend()返回指向第一个元素之前一个地位的随机拜访迭代器cbegin()和 begin() 性能雷同,只不过在其根底上减少了 const 属性,不能用于批改元素cend()和 end() 性能雷同,只不过在其根底上,减少了 const 属性,不能用于批改元素crbegin()和 rbegin() 性能雷同,只不过在其根底上,减少了 const 属性,不能用于批改元素crend()和 rend() 性能雷同,只不过在其根底上,减少了 const 属性,不能用于批改元素size()返回容器中以后元素的数量,其值始终等于初始化 array 类的第二个模板参数 Nmax_size()返回容器可包容元素的最大数量,其值始终等于初始化 array 类的第二个模板参数 Nempty()判断容器是否为空,和通过 size()==0 的判断条件性能雷同,但其效率可能更快at(n)返回容器中 n 地位处元素的援用,该函数主动查看 n 是否在无效的范畴内,如果不是则抛出 out_of_range 异样front()返回容器中第一个元素的间接援用,该函数不适用于空的 array 容器back()返回容器中最初一个元素的间接利用,该函数同样不适用于空的 array 容器。data()返回一个指向容器首个元素的指针。利用该指针,可实现复制容器中所有元素等相似性能fill(val)将 val 这个值赋值给容器中的每个元素array1.swap(array2)替换 array1 和 array2 容器中的所有元素,但前提是它们具备雷同的长度和类型3、实例1)初始化// testArray.cpp : 此文件蕴含 "main" 函数。程序执行将在此处开始并完结。//#include <iostream>#include <array>using namespace std;int main(){ std::array<int, 5> arrayInt; std::array<int, 5> arrayInt2{}; std::array<int, 5> arrayInt3{ 2,6,4,3 }; //遍历array的值 for (int i = 0; i < arrayInt.size(); i++) { std::cout << "输入arrayInt[" << i << "]=" << arrayInt[i] << std::endl; } for (int i = 0; i < arrayInt2.size(); i++) { std::cout << "输入arrayInt2[" << i << "]=" << arrayInt2[i] << std::endl; } for (int i = 0; i < arrayInt3.size(); i++) { std::cout << "输入arrayInt3[" << i << "]=" << arrayInt3[i] << std::endl;; } system("pause"); return 0;}后果如下: ...

May 6, 2021 · 2 min · jiezi

关于stl:STL专题-03-相关例题选自ACM

度度熊学队列 度度熊正在学习双端队列,他对其翻转和合并产生了很大的趣味。 初始时有 N 个空的双端队列(编号为 1 到 N ),你要反对度度熊的 Q 次操作。 ①1 u w val 在编号为 u 的队列里退出一个权值为 val 的元素。(w=0 示意加在最后面,w=1 示意加在最初面)。 ②2 u w 询问编号为 u 的队列里的某个元素并删除它。( w=0 示意询问并操作最后面的元素,w=1 示意最初面) ③3 u v w 把编号为 v 的队列“接在”编号为 u 的队列的最初面。w=0 示意程序接(队列 v 的结尾和队列 u 的结尾连在一起,队列v 的结尾作为新队列的结尾), w=1 示意逆序接(先将队列 v 翻转,再程序接在队列 u 前面)。且该操作实现后,队列 v 被清空。Input有多组数据。 对于每一组数据,第一行读入两个数 N 和 Q。 接下来有 Q 行,每行 3~4 个数,意义如上。 N≤150000,Q≤400000 1≤u,v≤N,0≤w≤1,1≤val≤100000 所有数据里 Q 的和不超过500000Output对于每组数据的每一个操作②,输入一行示意答案。 留神,如果操作②的队列是空的,就输入−1且不执行删除操作。Sample Input2 101 1 1 231 1 0 2332 1 1 1 2 1 23331 2 1 233333 1 2 12 2 02 1 12 1 02 1 1Sample Output23-1233323323333 ...

April 28, 2021 · 3 min · jiezi

关于stl:STL专题-02-相关例题选自ACM

产生冠军 有一群人,打乒乓球较量,两两捉对撕杀,每两个人之间最多打一场较量。球赛的规定如下:如果A战胜了B,B又战胜了C,而A与C之间没有进行过较量,那么就认定,A肯定能战胜C。如果A战胜了B,B又战胜了C,而且,C又战胜了A,那么A、B、C三者都不可能成为冠军。依据这个规定,无需循环较量,或者就能确定冠军。你的工作就是面对一群较量选手,在通过了若干场撕杀之后,确定是否曾经实际上产生了冠军。Input输出含有一些选手群,每群选手都以一个整数n(n<1000)结尾,后跟n对选手的比赛结果,比赛结果以一对选手名字(中距离一空格)示意,前者战败后者。如果n为0,则示意输出完结。Output对于每个选手群,若你判断出产生了冠军,则在一行中输入“Yes”,否则在一行中输入“No”。Sample Input3Alice BobSmith JohnAlice Smith5a cc dd eb ea d0Sample OutputYesNo #include<iostream>#include<algorithm>#include<string>#include<string.h>#include<cstdio>#include<queue>#include<stack> #include<set> using namespace std;//#include<bits/stdc++.h>//万能头文件 //字符翻转 int main(){ int t = 0; while(cin >> t){ if(t == 0){ break; } getchar(); string s1,s2; set<string>str1,str2; while(t--){ cin >> s1 >> s2; str1.insert(s1); str1.insert(s2); str2.insert(s2); } if(str1.size() - str2.size() == 1){ cout << "Yes" << endl; }else{ cout << "No" << endl; } } return 0;} 人见人爱A-B加入过上个月月赛的同学肯定还记得其中的一个最简略的题目,就是{A}+{B},那个题目求的是两个汇合的并集,明天咱们这个A-B求的是两个汇合的差,就是做汇合的减法运算。(当然,大家都晓得汇合的定义,就是同一个汇合中不会有两个雷同的元素,这里还是揭示大家一下) 呵呵,很简略吧?Input每组输出数据占1行,每行数据的开始是2个整数n(0<=n<=100)和m(0<=m<=100),别离示意汇合A和汇合B的元素个数,而后紧跟着n+m个元素,后面n个元素属于汇合A,其余的属于汇合B. 每个元素为不超出int范畴的整数,元素之间有一个空格隔开.如果n=0并且m=0示意输出的完结,不做解决。Output针对每组数据输入一行数据,示意A-B的后果,如果后果为空集合,则输入“NULL”,否则从小到大输入后果,为了简化问题,每个元素前面跟一个空格.Sample Input3 3 1 2 3 1 4 73 7 2 5 8 2 3 4 5 6 7 8 0 0Sample Output2 3 NULL ...

April 28, 2021 · 3 min · jiezi

黑马程序员C进阶STL

缘起之前吧,虽然看了和买了一些CPP的书,可能学习方法不对,又或者自已能力真的有限,各种知识点没有串起来,总是在面试时突击一下,2019年开始all in到CPP项目上后呢,有发心要把知识串起来。立足于现有电脑里的视频资料,忽然看到之前下载的“黑马程序员”系列,听C++基础(OO)部分时,感觉自已学的都串起来,于是坚持听完。差不多听了80%的时候,32W,自已2T的硬盘毫无征兆的挂了(外接的3.5读盘器也不行)。33W(0812-0818)计划开始看“C++进阶STL”,并复习之前的OO内容的,怕东西丢失了,自已学完一部分知识后写份学习笔记吧。下载的内容day05(不确定全不全,感觉不全) day01:函数模板,类模板的语法day02:MyArray的实现、类型转换、异常(比OO里面讲得详细一些)day03:输入输出流、STL基础、string和vectorday04:deque、listday05:multimap、函数对象、算法内容day01资料不在U盘里,回去补充下day0202-03:MyArray的实现 冷不丁的还提了一下C++11的右值引用自已要思考并纸上实现一下,看看知识的差距在哪。04-06:类型转换 用得比较多也就是static_cast<>()了收获&感想收获搜的时候,也看到了自已学习day01、day02的一些笔记(都不够系统),站在自已来自,那些当时要笔记的部分东西,已经内化到我的知识体系了(比如4种类型转换,函数模板,类模板语法【模板但我没找到项目中应用的地方】)也看到了一些书上没有提及到的坑,比如: 分离写法中#include<hpp>的写法;在类模板中使用friend关键字;同样的修改,g++不认识,而vs认识。老师用vs讲的,我写例子用的是codeblock,clion都是基于g++纠正了自已觉得STL是用库的想法,之前的学习方法都是对着书本运行一下代码,但项目中真正解决问题时,比如用map在怎么去写好,还是要搜索引擎,多用,横向的多关联思考,纵向的往里面思考才行。感想是不是视频不太全啊,毕竟是网盘上下载的,day05的课程,前2.5天都在讲的是C++OO里的高级一点的知识,后面2.5讲的是用法(虽然还没有听),模板的语法讲得也浅了些,模板特化都没有讲。

August 17, 2019 · 1 min · jiezi

PAT A1060 科学记数法经典例题(全string库解决)

挺操蛋的一道题,我他妈的都服了。。。出这道题我怕是毙了首先题目里就有几个坑:1.可能有前导零,比如说000.00012.可能有零,比如说000.0000000哎,思路感觉最重要,对于字符串处理一定要有思路,知道先干嘛,后干嘛;首先就要去除前导零,把他变成一个纯净的浮点数;去除前导零,我们就可以进行分类讨论,因为必定会出现两种情况:1.第一位是小数点,此时该数为小数;2.第一位是数字,此时该数为大于零的数;对于第一种情况,我们应该注意e和位数的关系;例如.0002,其e一定是符号位到第一位不为零的数字的距离,也就是0.210^-3。所以对于第一种情况,只需要寻找第一位不为零的数字,过一位e–,从而使得得到纯净小数的时候,也能得到指数;注意:两种情况都是不含小数点的数,“0.”后面输出的时候再加对于第二种情况,我们应该先寻找小数点;这里e的记录方式和第一种情况类似,每过一位,e++;比如44.2,我们应该是0.44210^2,而此时e过了两位;当我们找到小数点之后,就应该删除小数点,使我们得到纯净的连续数字;对于以上两种情况,我们都得到了纯净的连续数字,也就是非小数,不包含小数点的数字,接下来就是对保留位数进行判断;对于一种情况,就是通过以上步骤,00.00,最后得到的序列为空,此时e=0,作为零的特殊情况;后续就是对精度计算,并且对不足位进行补0操作;例如现在得到了12,我们要求精度位4位,所以先建立一个空字符串s,遍历的同时对精度位进行计算,当s=“12"时,还差两个精度位,所以补两个0,输出s=“1200"最后进行比较的时候就是对指数和保留部分比较,后续输出的时候再s前加上“0.”输出就行;代码如下所示:#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>#include<string>using namespace std;using std::vector;int n;string deal(string s,int& e){ int k=0; while(s.length()>0&&s[0]==‘0’){ s.erase(s.begin()); //去除前导零; } if(s[0]==’.’){ //小于零的数字 s.erase(s.begin()); while(s.length()>0&&s[0]==‘0’){ s.erase(s.begin()); //找到首位非零元素 e–; } }else{ while(k<s.length()&&s[k]!=’.’){ k++; //寻找小数点 e++; } if(k<s.length()){ //说明有小数点,进行删除 s.erase(s.begin()+k); } } if(s.length()==0){ e=0;//取出前导零为0说明实际为0 } int num=0; k=0; string res; while(num<n){ if(k<s.length()) //如果有数字存在 res+=s[k++]; else res+=‘0’; num++; } return res;}int main(){ string s1,s2,s3,s4; cin>>n>>s1>>s2; int e1=0,e2=0; s3=deal(s1,e1); s4=deal(s2,e2); if(s3==s4&&e1==e2){ cout<<“YES 0."<<s3<<"*10^"<<e1<<endl; }else{ cout<<“NO 0."<<s3<<"*10^"<<e1<<” 0."<<s4<<"*10^"<<e2<<endl; } system(“pause”); return 0;} ...

February 21, 2019 · 1 min · jiezi

PAT A1052

这个需要注意的是相关的string转整数或者double的函数;详见这个链接blog#include <iostream>#include <string>using namespace std;bool isPrime(int n) { if (n == 0 || n == 1) return false; for (int i = 2; i * i <= n; i++) if (n % i == 0) return false; return true;}int main() { int l, k; string s; cin >> l >> k >> s; for (int i = 0; i <= l - k; i++) { string t = s.substr(i, k); int num = stoi(t); if (isPrime(num)) { cout << t; return 0; } } cout << “404\n”; return 0;} ...

February 20, 2019 · 1 min · jiezi

STL String常用函数备注

对于String库的函数用的比较少,常规的以下size()函数什么的不在列举,但是有些函数很好用,手写做一下备注,省的到处查;1.string使用print输出:常规条件下string只能使用cin和cout,但是使用printf输出借助函数也可以;使用c_str()函数string str=“abcd”;printf("%s\n",str.c_str())l2.insert插入:用两种形式:第一种,insert(pos,string)在pos位置插入string,注意这里的pos是要插入的string第一个字符应该在的位置;例如insert(3,str2),str2插入到从零开始算第四个位置,也就是存储的是str2的第一位;第二种,insert(it,it2,it3)这里的it代表的是迭代器指针,第一个指向要插入的位置,后两个指向插入string的起始位置和末位置;例如:str.insert(str.begin()+3,str2.begin(),str2.end());这里的插入位置的迭代器和上一种相同,就是插入string第一个字符的起始位置;3.erase()删除区间内的元素:第一种 删除单个元素:例如:str.erase(str.begin()+4);这里删除的是迭代器指向的元素;第二种 删除一个区间内的所有元素例如:str.erase(str.begin()+2,str,end()-1);这里的区间是闭区间,包括迭代器指向的元素;第三种 删除区间内元素的另一种形式形式为str.erase(pos,length)例如:str.erase(3,2);这里删除的是pos位开始的length个字符,length从1开始计数,意为删除元素的总个数;4.clear()清空函数:清空字符串内的所有元素;5.substr()截取函数:substr(pos,len)返回从pos开始,总长度为len的子串;6.find()查询子串函数:str.find(str2)当str2是str的子串时,返回其在str中第一次出现的位置,如果不是子串,返回string::npos(该变量常认为是find查询失败的标志);也有形式:str.find(str2,pos),也就是从pos位开始匹配str2,返回值也是第一次出现的位置,只不过给查询的位置给定了一个起始的距离;7.replace()替换函数:str.replace(pos,len,str2)代表str的pos开始,len长度的子串替换为str2;str.replace(it1,it2,it3)代表str的迭代器1,2内的刺穿替换为str2,注意,这里的str第二个迭代器it2指向的是尾后位置;

February 18, 2019 · 1 min · jiezi

iterator_traits获取迭代器类型

结论: std::iterator_traits 用于Iterator类型比如有这么一个需求,给随意的一个迭代器移动距离 , 伪代码:template <typename Iter, typename Distance>void move_iter(Iter& iter, Distance d){ if( iter is std::random_access_iterator_tag) //随机迭代器随意加减 iter +=d; else //别的迭代器 ….}问题是如何判断一个迭代器类型 ,使用 iterator_traits;前提 , 5种迭代器是继承关系 ,随意看一眼即可:struct input_iterator_tag { // identifying tag for input iterators };struct _Mutable_iterator_tag { // identifying tag for mutable iterators };struct forward_iterator_tag : input_iterator_tag, _Mutable_iterator_tag { // identifying tag for forward iterators };….看一下iterator_traits:template<class _Iter> struct iterator_traits { // get traits from iterator _Iter typedef typename _Iter::iterator_category iterator_category; … 还有一堆typedef的东西, 省略 }iterator_traits 和 remove_reference 内部实现差不多, 只是一堆typedef ,主要用于获取类型;先简单看一下iterator_traits 怎么用: //太长了? // iterator_traits<迭代器类型>::iterator_category //iterator_category 就是一个被typedef 的 5个结构体中的其中一个 cout << typeid(std::iterator_traits<list<int>::iterator>::iterator_category).name() << endl; cout << typeid(std::iterator_traits<deque<int>::iterator>::iterator_category).name() << endl; /* 输出: struct std::bidirectional_iterator_tag struct std::random_access_iterator_tag*/用于判断迭代器类型的就是iterator_category ,而他本身就是5种迭代器的其中一个;接下来就可以修改第一份伪代码了 . 大致是这样:template <typename Iter, typename Distance>void move_iter(Iter& iter, Distance d){ if(typeid(std::random_access_iterator_tag) == typeid(std::iterator_traits<Iter>::iterator_category)) iter += d; else if … …}用RTTI这类东西总是觉得,本来就可以在编译的时候完成的,干嘛非等到运行时;在修改一下 , 下面代码用了哑元 , 3个重载的template function:template <typename Iter , typename Dist>void do_move_iter(Iter & iter, Dist d , std::random_access_iterator_tag) //随机迭代{ iter += d;}template <typename Iter , typename Dist>void do_move_iter(Iter & iter, Dist d , std::bidirectional_iterator_tag) //双向{ if( d>= 0){ while(d–) ++iter; } else { while(d++) –iter; }}template <typename Iter , typename Dist>void do_move_iter(Iter & iter, Dist d , std::input_iterator_tag) // forward继承了input;{ if( d < 0) throw std::out_of_range(“d < 0”); while(d–) ++iter;}template <typename Iter, typename Distance>void move_iter(Iter& iter, Distance d){ do_move_iter(iter,d, std::iterator_traits<Iter>::iterator_category());}int main(){ vector<int> vi{1,2,3,4}; vector<int>::iterator iter = vi.begin(); //随机迭代器 move_iter(iter,2); //移动2个距离 cout << *iter << endl; //3. ok的} ...

January 7, 2019 · 2 min · jiezi

膨胀的template class成员函数

前提://2个不同的array类std::array<int,10> arr10;std::array<int,5> arr5;如要编写如上的template classtemplate <typename T ,std::size_t length>class XArr {…public: void insert() {}};XArr<int,10> a10; //产生一个XArr<int,10> class XArr<int,5> a5; //产生XArr<int,5>a10.insert() //产生一个实现a5.insert() //又一个实现将产生2个类, 2份insert实现(如果有调用的话)膨胀的template class 的原因是参数因此让目标代码膨胀减少的方法是,提取一个template父类://一个模版父类,需要用到的函数放在这儿template < typename T>class XBase{public: void insert() {} //类型一致的公用一套代码};template <typename T ,std::size_t length>class XArr : public XBase<T>{public: void test() { cout << FUNCTION << endl;}};XArr<int,10> a10; //产生一个XArr<int,10> class XArr<int,5> a5; //产生XArr<int,5>a10.insert() //XBase<int>::inserta5.insert() //与上面的insert 同一实现虽然无法减少XArr 类的实例化 ,至少减少了insert函数的实例化简单验证一下:union{ void (XArr<int,10>::*pfunc1)(); void (XArr<int,5>::*pfunc2)(); int addr; } up; up.pfunc1= &XArr<int,10>::test; //XArr 自己的函数将生成2份 cout << up.addr << endl; up.pfunc2 = &XArr<int,5>::test; cout << up.addr << endl; up.pfunc1= &XArr<int,10>::insert; //TBase<int> 的insert 只有一份 cout << up.addr << endl; up.pfunc2 = &XArr<int,5>::insert; cout << up.addr << endl; ...

January 6, 2019 · 1 min · jiezi

模版基类引出的问题

先给结论:不要对模版基类做任何假设前提:有如下一个模版类template <typename T>class TBase{ public: void f(){ cout << FUNCTION << endl;}};1.我们都知道 TBase<int> , TBase<char> … 等等实例化的类 互相都没有关系;2.特化本质上就是接管了编译器的工作 ; 比如 template<> class TBase<int>{…} 相当于 接管了编译器去生成TBase<int>;好了.有了以上的前提. 下面进入正题:模版基类引出的问题下面有这么一个类:template <typename T>class TChild : public TBase<T>{ //继承一个模版类 public: void doit(){ f(); //有些编译器能通过,有些则不能 ; 可以 this->f() .跳过编译器当前的检查 }};一般严格的编译器都将编译失败. 原因 : 编译器做了一个假设 TBase 有可能被特化, 被特化的版本中可能并未提供 f 接口;比如:对Tbase<int> 特化template <> //一个空的TBase<int>类class TBase<int>{};int main(int argc, char *argv[]){ TChild<int> t1; t1.doit(); //不严格的编译器得到这里才出错; return 0;}

January 5, 2019 · 1 min · jiezi

一次 macOS 下 C++ 的 STL 踩坑记录

背景最近有在做 RocketMQ 社区的 Node.js SDK,是基于 RocketMQ 的 C SDK 封装的 Addon,而 C 的 SDK 则是基于 C++ SDK 进行的封装。然而,却出现了一个诡异的问题,就是当我在消费信息的时候,发现在 macOS 下得到的消息居然是乱码,也就是说 Linux 下居然是正常的。重现首先我们要知道一个函数是 const char* GetMessageTopic(CMessageExt* msg),用于从一个 msg 指针中获取它的 Topic 信息。乱码的代码可以有好几个版本,是我在排查的时候做的各种改变:// 往 JavaScript 的 object 对象中插入键名为 topic 的值为 GetMessageTopic// 第一种写法:乱码Nan::Set( object, // v8 中的 JavaScript 层对象 Nan::New(“topic”).ToLocalChecked(), Nan::New(GetMessageTopic(msg)).ToLocalChecked());// 另一种写法:乱码const char* temp = GetMessageTopic(msg);Nan::Set( object, // v8 中的 JavaScript 层对象 Nan::New(“topic”).ToLocalChecked(), Nan::New(temp).ToLocalChecked());// 第三种写法:乱码string GetMessageColumn(CMessageExt* msg, char* name){ // … const char* orig = GetMessageTopic(msg); int len = strlen(orig); char temp[len + 1]; memcpy(temp, orig, sizeof(char) * (len + 1)); return temp;}const char* temp = GetMessageColumn(msg, “topic”);Nan::Set( object, // v8 中的 JavaScript 层对象 Nan::New(“topic”).ToLocalChecked(), Nan::New(temp).ToLocalChecked());并且很诡异的是,当我在调试第三种写法的时候,我发现在 const char* orig = GetMessageTopic(msg); 这一部的时候 orig 的值是正确的。而一步步单步运行下去,一直到 memcpy 执行结束的时候,orig 内存块里面的字符串居然被莫名其妙修改成乱码了。参考如下:这就不能忍了。当我锲而不舍的时候,发现当我改成这样之后,返回的值就对了:string GetMessageColumn(CMessageExt* msg, char* name){ // … const char* orig = GetMessageTopic(msg); int len = strlen(orig); int i; char temp[len + 1]; for(i = 0; i < len + 1; i++) { temp[i] = orig[i]; } // 做一些其它操作 return temp;}const char* temp = GetMessageColumn(msg, “topic”);Nan::Set( object, // v8 中的 JavaScript 层对象 Nan::New(“topic”).ToLocalChecked(), Nan::New(temp).ToLocalChecked());但问题在于,在“其它操作”中,orig 还是会变成一堆乱码。当前返回能正确的原因是因为我在它变成乱码之前,用可以“不触发”变成乱码的操作先把 orig 的字符串给赋值到另一个字符数组中,最后返回那个新的数组。问题看似解决了,但是这种诡异、危险的行为始终是我心中的一颗丧门钉,不处理总之是慌的。RocketMQ C++ SDK 源码查看在排查的过程中,我去看了 RocketMQ 的 C++ 和 C SDK 的实现,我把重要的内容摘出来:class MQMessage {public: string::string getTopic() const { return m_topic; } …private: string m_topic; …}// MQMessageExt 是继承自 MQMessageconst char* GetMessageTopic(CMessageExt *msg) { … return ((MQMessageExt ) msg)->getTopic().c_str();}我们阅读一下这段代码,在 GetMessageTopic 中,先得到了一个 getTopic 的 STL 字符串,然后调用它的 c_str() 返回 const char。一切看起来是那么美好,没有问题。但我后来在多次调试的时候发现,对于同一个 msg 进行调用 GetMessageTopic 得到的指针居然不一样!我是不是发现了什么新大陆?诚然,msg->getTopic() 返回了一个字符串对象,并且是通过拷贝构造从 m_topic 那边来的。依稀记得大学时候看的 STL 源码解析,根据 STL 字符串的 Copy-On-Write 来说,我没做任何改变的情况下,它们不应该是同源的吗?事实证明,我当时的这个“想当然”就差点让我查不出问题来了。柳暗花明在我捉鸡了好久之后一直毫无头绪之后,在参考资料 1 中获得了灵感,我开始打开脑洞(请原谅我这个坑还找了很久,毕竟我主手武器还是 Node.js),会不会现在的 String 都不是 Copy-On-Write 了?但是 Linux 下又是正常的哇。后来我在网上找是不是有人跟我遇到一样的问题,最后还是找到了端倪。不同的 stl 标准库实现不同, 比如 CentOS 6.5 默认的 stl::string 实现就是 『Copy-On-Write』, 而 macOS(10.10.5)实现就是『Eager-Copy』。说得白话一点就是,不同库实现不一样。Linux 用的是 libstdc++,而 macOS 则是 libc++。而 libc++ 的 String 实现中,是不写时拷贝的,一开始赋值就采用深拷贝。也就是说就算是两个一样的字符串,在不同的两个 String 对象中也不会是同源。其实深挖的话内容还有很多的,例如《Effective STL》中的第 15 条也有提及 String 实现有多样性;以及大多数的现代编译器中 String 也都有了 Short String Optimization 的特性;等等。回到乱码 Bug得到了上面的结论之后,这个 Bug 的原因就知道了。((MQMessageExt *) msg)->getTopic() 得到了一个函数中的栈内存字符串变量。在 Linux 中,就算是栈内存变量,但是它的 c_str() 还是源字符串指向的指针,所以函数声明周期结束,这个栈内存中的字符串被释放,c_str() 指向的内存还坚挺着;在 macOS 下,由于字符串是栈内存分配的,字符串又是深拷贝,所以 c_str() 的生命周期是跟着字符串本身来的,一旦函数调用结束,该字符串就被释放了,相应地 c_str() 对应内存中的内容也被释放。综上所述,在 macOS 下,我通过 GetMessageTopic() 得到的内容其实是一个已经被释放内存的地址。虽然通过 for 可以趁它的内存块被复制之前赶紧抢救出来,但是这种操作一块已经被释放的内存行为总归是危险的,因为它的内存块随时可能被覆盖,这也就是之前乱码的本质了。更小 Demo 验证对于 STL 在这两个平台上不同的行为,我也抽出了一个最小化的 Demo,各位看官可以在自己的电脑上试试看:#include <stdio.h>#include <string>using namespace std;string a = “123”;string func1(){ return a;}int main(){ printf(“0x%.8X 0x%.8X\n”, a.c_str(), func1().c_str()); return 0;}上面的代码在 Linux 下(如 Ubuntu 14.04)运行会输出两个一样的指针地址,而在 macOS 下执行则输出的是两个不一样的指针。小结在语言、库的使用中,我们不能去使用一个没有明确在文档中定义的行为的“特性”。例如文档中没跟你说它用的是 Copy-On-Write 技术,也就说明它可能在未来任何时候不通知你就去改掉,而你也不容易去发现它。你就去用已经定义好的行为即可,就是说 c_str() 返回的是字符串的一个真实内容,我们就要认为它是跟随着 String 的生命周期,哪怕它其中有黑科技。毕竟,下面这个才是 C++ reference 中提到的定义,我们不能臆想人家一定是 COW 行为:Returns a pointer to a null-terminated character array with data equivalent to those stored in the string.The pointer is such that the range [c_str(); c_str() + size()] is valid and the values in it correspond to the values stored in the string with an additional null character after the last position.这一样可以引申到 JavaScript 上来,例如较早的 ECMAScript 262 第三版对于一个对象的定义中,键名在对象中的顺序也是未定义的,当时就不能讨巧地看哪个浏览器是怎么样一个顺序来进行输出,毕竟对于未定义的行为,浏览器随时改了你也不能声讨它什么。好久没写文了,码字能力变弱了。以上。参考资料《Why does calling c_str() on a function that returns a string not work?》《Why a new C++ Standard Library for C++11?》《Effective STL》第 15 条:注意 String 实现的多样性《C++ 之 stl::string 写时拷贝导致的问题》《C++ 再探 String 之eager-copy、COW 和 SSO 方案》《C++ Short String Optimization stackoverflow 回答集锦以及我的思考》 ...

December 7, 2018 · 2 min · jiezi