【C++】字符串拷贝/内存拷贝的应用
一、字符串拷贝
这里主要讨论字符串的转换和拼接工作,
对于C++来说,最好用又简单的字符串拷贝方式是什么呢,那必然是
s += "Hello World!";
接下来,我们实际测试一下,分析几种字符串拼接的性能
场景:若干个键值对,以一定方法组合成字符串,累积拷贝到char数组中,这一操作被X个用户分别执行1次
键值对:int-int类型
对比方案:sstream/snprintf/+=/append
例如:
vector{1:3,2:4,3:111,4:333,432:444}
-> "1,3;2,4;3,111;4,333;432,444;"
我们先定义键值对,组合方式和轮次等基础信息,模拟一个vector存储键值对
我们设计若干个字符串拼接函数,输入参数是char*,最终结果保存到char*中
使用map保存这些函数和他的名字,用于调用和输出打印
typedef void(*pVoid)(char*);//定义别名才能放在vector中
map<string, pVoid> FuncMap;//保存函数指针的map
struct KV1
{
int Key;
int Value1;
};
vector<KV1> Datas;
const char Sym1 = ',';
const char Sym2 = ';';
const int NUM = 10;//键值对数量
const int Round = 10000;//执行轮次
const int ARRLEN = 1024;//输出字符串最大长度
void Init()
{
for (size_t i = 0; i < NUM; ++i)
{
static KV1 kv;
kv.Key = i;
kv.Value1 = i + rand() % 100;
Datas.push_back(kv);
}
FuncMap.insert(make_pair("GetCharSnprintf:", GetCharSnprintf));//使用snprintf
FuncMap.insert(make_pair("GetCharSprintf:", GetCharSprintf));//使用sprintf
FuncMap.insert(make_pair("GetCharSSTream:", GetCharSSTream));//使用sstream
FuncMap.insert(make_pair("GetCharStringPlus:", GetCharStringPlus));//使用+=
FuncMap.insert(make_pair("GetCharStringAppend:", GetCharStringAppend));//使用append
}
每个函数执行Round次,模拟Round个用户分别操作一次,并统计操作时间
void CalTime(const pair<string, pVoid>& FuncPair)
{
cout.width(20);
cout << FuncPair.first;
void(*p)(char*) = FuncPair.second;//获取函数指针
TimeStart = clock(); //程序开始计时
for (int i = 0; i <= Round; i++)
{
char Str[ARRLEN];
memset(Str, 0, sizeof(Str));
(*p)(Str);//调用函数
if (i == 0)
{
cout << Str << endl;//打印第一组数据保证赋值结果正确
}
}
TimeEnd = clock(); //程序结束用时
double endtime = (double)(TimeEnd - TimeStart) / CLOCKS_PER_SEC;
cout.width(20);
cout << "Total time:" << endtime * 1000 << "ms" << endl; //ms为单位
}
在main函数里初始化并调用
int main() {
Init();
for (auto i : FuncMap)
{
CalTime(i);
}
return 0;
}
描述一下工作,我们需要从vector中取出键值对,组成一个字符串并输出
接下来我们一个个来看需要测试的函数
首先是+=,将键值对的int转换为string,和组装字符依次进行'+='
void GetCharStringPlus(char* Str)
{
string s;
for (size_t i = 0; i < Datas.size(); ++i)
{
s += IntToString(Datas[i].Key) + "," + IntToString(Datas[i].Value1) + ";";
}
const char* c = s.c_str();
strncpy_s(Str, ARRLEN, c, strlen(c));
}
接下来append,和'+='唯一的区别是不能写在同一行
void GetCharStringAppend(char* Str)
{
string s;
for (size_t i = 0; i < Datas.size(); ++i)
{
s.append(IntToString(Datas[i].Key));
s.append(",");
s.append(IntToString(Datas[i].Value1));
s.append(";");
}
const char* c = s.c_str();
strncpy_s(Str, ARRLEN, c, strlen(c));
}
再来是sstream,和+=同样简洁的语法
void GetCharSSTream(char* Str)
{
static stringstream ss;
ss.str("");
for (size_t i = 0; i < Datas.size(); ++i)
{
ss << Datas[i].Key << "," << Datas[i].Value1 << ";";
}
//这里需要用tmp保存结果, 如果用const char*内容将会被释放
string tmp = ss.str();
strncpy_s(Str, ARRLEN, tmp.c_str(), tmp.size());
}
同样很小巧的sprintf
void GetCharSprintf(char* Str)
{
for (size_t i = 0; i < Datas.size(); ++i)
{
char tmp[ARRLEN];
sprintf_s(tmp, ARRLEN, "%d%s%d%s", Datas[i].Key, ",", Datas[i].Value1, ";");
strcat_s(Str, ARRLEN, tmp);
}
}
最后是傻瓜式的snprintf,代码有点恐怖
void GetCharSnprintf(char* Str)
{
_snprintf_s(Str, ARRLEN - 1, ARRLEN - 1, "%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s%d%s",
Datas[0].Key, ",", Datas[0].Value1, ";",
Datas[1].Key, ",", Datas[1].Value1, ";",
Datas[2].Key, ",", Datas[2].Value1, ";",
Datas[3].Key, ",", Datas[3].Value1, ";",
Datas[4].Key, ",", Datas[4].Value1, ";",
Datas[5].Key, ",", Datas[5].Value1, ";",
Datas[6].Key, ",", Datas[6].Value1, ";",
Datas[7].Key, ",", Datas[7].Value1, ";",
Datas[8].Key, ",", Datas[8].Value1, ";",
Datas[9].Key, ",", Datas[9].Value1, ";"
);
}
好了,执行main函数,对10对键值对组合拼接10000次,猜一猜谁是效率最高的小伙伴(我猜是snprintf)
GetCharSSTream: 0,41;1,68;2,36;3,3;4,73;5,29;6,84;7,65;8,70;9,73;
Total time:885ms
GetCharSnprintf: 0,41;1,68;2,36;3,3;4,73;5,29;6,84;7,65;8,70;9,73;
Total time:129ms
GetCharSprintf: 0,41;1,68;2,36;3,3;4,73;5,29;6,84;7,65;8,70;9,73;
Total time:182ms
GetCharStringAppend: 0,41;1,68;2,36;3,3;4,73;5,29;6,84;7,65;8,70;9,73;
Total time:2465ms
GetCharStringPlus: 0,41;1,68;2,36;3,3;4,73;5,29;6,84;7,65;8,70;9,73;
Total time:2979ms
可以看到结果都是正确的,
snprintf果不其然速度最快,只需要129ms,但是扩展性很差,vector新增元素就要开始毁灭式编程
sprintf与snprintf相差无几,需要182ms,扩展性能也很好,值得使用
sstream速度排行第三,885ms,怀疑是输入流的处理耽误了时间
最后append和+=的速度不忍直视,主要是int2string进行了string的拷贝,严重消耗性能,而且+=比append还要更慢一些
到这里应该就明白,在实际应用中选择合适的方法是一件多么重要的事情
二、内存拷贝
设想一种情况,我们有结构体StructA、StructB、StructC如下表示
struct structA
{
int ID[3];
char Level[3];
char TimeStamp[3][32];
};
struct structB
{
int ID[3];
char Level[3];
char TimeStamp[3][32];
};
struct structC
{
int ID1;
char Level1;
char TimeStamp1[32];
int ID2;
char Level2;
char TimeStamp2[32];
int ID3;
char Level3;
char TimeStamp3[32];
};
现在我们因为一些原因要将各个Struct的数据相互拷贝,应该如何实现?
很自然的,我们可以为结构体每一个字段进行赋值,例如从A到B以及B到C的拷贝
void CopyA2B(const structB& a, structB& b)
{
for (int i = 0; i < 3; ++i)
{
b.ID[i] = a.ID[i];
b.Level[i] = a.Level[i];
strcpy_s(b.TimeStamp[i], a.TimeStamp[i]);
}
}
void CopyB2C(const structB& b, structC& c)
{
c.ID1 = b.ID[0];
c.ID2 = b.ID[1];
c.ID3 = b.ID[2];
c.Level1 = b.Level[0];
c.Level2 = b.Level[1];
c.Level3 = b.Level[2];
strcpy_s(c.TimeStamp1, b.TimeStamp[0]);
strcpy_s(c.TimeStamp2, b.TimeStamp[1]);
strcpy_s(c.TimeStamp3, b.TimeStamp[2]);
}
看起来实在是太臃肿了!我们尝试定义一个宏让他美观一点
#define COPYA2B(i, A, B)\
B.ID[i] = A.ID[i];\
B.Level[i] = A.Level[i];\
strcpy_s(B.TimeStamp[i], A.TimeStamp[i]);
void CopyA2B(const structB& a, structB& b)
{
for (int i = 0; i < 3; ++i)
{
COPYA2B(i, a, b);
}
}
#define COPYB2C(i, B, C)\
C.ID##i = B.ID[i-1];\
C.Level##i = B.Level[i-1];\
strcpy_s(C.TimeStamp##i, B.TimeStamp[i-1]);
void CopyB2C(const structB& b, structC& c)
{
COPYB2C(1, b, c);
COPYB2C(2, b, c);
COPYB2C(3, b, c);
}
define的功劳使得函数似乎好看了一些,其实本质上没有发生变化,
如果新增Struct的字段都要劳神去修改,那么有没有一种方法能一劳永逸呢
办法肯定是有的,先看看A2B,我们使用神奇的memcpy
void CopyA2B(const structA& a, structB& b)
{
memcpy(&b, &a, sizeof(b));
}
一行代码直接解决,只要保持StructA和StructB结构一致,就再也不用做修改了!
再看看B和C,他们的结构不一致,显然不能直接使用memcpy
仔细观察一下可以发现,其实C只是将B(数组)全部展开了
这时我们使用一套组合拳offsetof+memcpy
void CopyB2C(const structB& b, structC& c)
{
//先构造出和B一样的结构
struct _TMP_
{
int ID;
char Level;
char TimeStamp[32];
}tmp[3];
//初始化
memset(&tmp[0], 0, sizeof(tmp));
//赋值
for (int i = 0; i < 3; ++i)
{
tmp[i].ID = b.ID[i];
tmp[i].Level = b.Level[i];
strcpy_s(tmp[i].TimeStamp, b.TimeStamp[i]);
}
//计算需要拷贝给C的大小
int iMemCpySize = offsetof(struct structC, TimeStamp3) - offsetof(struct structC, ID1) + sizeof(c.TimeStamp3);
//tmp全量拷贝到C
memcpy(&c.ID1, &tmp[0], iMemCpySize);
}
我们根据数据在内存中实际存储的方式,模拟出与C结构相同的结构体
计算C的最后一个成员与第一个成员的地址偏移量,这样就可以使用memcpy啦