【C++】字符串拷贝/内存拷贝的应用

932 Views

一、字符串拷贝

这里主要讨论字符串的转换和拼接工作,
对于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啦

留下回复

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