【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啦

发布者

VC-Robot

游戏爱好者,动漫迷,C++修炼中,编程菜鸟,随性

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据