最近在对一块string对象进行拷贝到char数组的时候发现部分数据缺失了。
原因是strncpy在复制的时候,在遇到'\0'时,先复制过去,然后的把dest剩下置为了'\0'。所以,一旦源字符串中存在'\0'则会导致源数据被截断。为此我们可以采用memcpy进行拷贝操作(snprintf 拷贝的时候遇到'\0' 也会停止)
#include <iostream>
#include <cstdio>
int main()
{
char src[] = "abcdefg\0hijklmn";
char dest[100];
memset(dest, 0, sizeof(dest));
strncpy(dest, src, sizeof(dest));
std::cout << dest << std::endl;
return 0;
}
可以看到,发生了截断
一.各个拷贝函数的比较
1.strncpy
strncpy比strcpy稍微安全点,strncpy顶多拷贝n个字节,所以在dest不够的情况下,不会像strcpy出现拷贝越界。但也只是稍微安全点而已,如果src长度比n大,那么dest就不是以'\0'结尾的了,需要手动方式自行添加'\0'结束符(而用strcpy函数复制时目的串后会自动补'\0')。所以通常要这么使用:
strncpy(buf, str, n);
if (n > 0)
{
buf[n - 1]= '\0';
}
给一串字符设置'\0'的方式有两种,分别为string[n]=0或string[n]='\0'。因为'\0'在ASCII码表中的编号就是十进制数0
#include <iostream>
#include <cstdio>
int main()
{
char src[] = "abcdefg1234567890";
char dest[5];
memset(dest, 0, sizeof(dest));
strncpy(dest, src, sizeof(dest));
std::cout << dest << std::endl;
dest[4] = 0;
std::cout << dest << std::endl;
return 0;
}
这段可以印证上面说到的两点:
●如果src长度比n大,那么dest就不是以'\0'结尾的了,所以abced后边是乱码
●'\0'在ASCII码表中的编号就是十进制数0
在高效方面strncpy也做得不好,它的大致实现这样的:
char *strncpy(char *dest, const char *src, size_t n)
{
size_t i;
for (i = 0; i < n && src[i] != '\0'; i++) {
dest[i] = src[i];
}
for ( ; i < n; i++) {
dest[i] = '\0';
}
return dest;
}
当src长度比n小时,未使用的字符,它会帮你填充为0,但我们往往会开一个大的Buffer来接收数据,如此必然有大量的多余空间,这样不仅速度慢,而且还会导致实存的消耗上涨。总之,strncpy的即不怎么安全,也不怎么高效。
2.memcpy
至于memcpy:void *memcpy(void *dest, const void *src, size_t n);
memcpy不安全,它只是做简单的拷贝而已,不过它很高效,如果你能保证不会越界的话,就使用memcpy吧。其实memcpy的实现就是一个一个赋值,strcpy也是一个一个赋值,不过,memcpy做了点优化,64位机器下,它会64位64位地拷贝,即8个字节8个字节地拷贝,对于除8的余数,再做单独处理,而strcpy是一个字节一个字节地拷贝,所以当有大量字符串需要拷贝时,理论上memcpy会比strcpy快8倍。
3.snprintf
int snprintf(char *str, size_t size, const char *format, cahr *source_str)
通过snprintf(dst_str, dst_size, “%s”, source_str); 可以将source_str拷贝到dst_str。snprintf的特点是安全,不管怎么着,它都能保证结果串str以\0结尾,哪怕dst_size不够大,它都能做好截断,同时在末尾添加上'\0'。
#include <iostream>
#include <cstdio>
int main()
{
char src[] = "abcdefg1234567890";
char dest[5];
memset(dest, 0, sizeof(dest));
snprintf(dest, sizeof(dest), "%s", src);
std::cout << dest << std::endl;
return 0;
}
因为末尾的数据被自动用\0 来作为结束符。
在性能方面,当source_str远长于dst_size时,该函数却很低效的,其他情况下该函数即安全又高效。
当source_str远长于dst_size时,该函数低效,是因为该函数的返回值为source_str的长度(不包括'\0'),那么它就必须将source_str全部过一遍,哪怕并不拷贝到dst_str中去。
注意,当source_str长度比dst_size小时,它不会把末尾的多余字符置零,所以它是很高效的,不会多做无用功。
二.测试对比
在Release条件下,g++编译器
#include <iostream>
#include <cstdio>
#include <chrono>
using namespace std::chrono;
int main()
{
{
char src[] = "abcdefg1234567890";
char dest[2048];
int len = 0;
auto start = high_resolution_clock::now();
for(int i = 0; i < 20000000; ++i){
memset(dest, 0, sizeof(dest));
len = strlen(src);
len = sizeof(dest) - 1 > len? len: sizeof(dest) -1;
memcpy(dest, src, len);
dest[len] = '\0';
}
auto end = high_resolution_clock::now();
std::cout <<"memcpy cost time: " << duration_cast<milliseconds>(end - start).count()<<" ms" << std::endl;
}
{
char src[] = "abcdefg1234567890";
char dest[2048];
int len = 0;
auto start = high_resolution_clock::now();
for(int i = 0; i < 20000000; ++i) {
memset(dest, 0, sizeof(dest));
strncpy(dest, src, sizeof(dest));
}
auto end = high_resolution_clock::now();
std::cout <<"strncpy cost time: " << duration_cast<milliseconds>(end - start).count()<<" ms" << std::endl;
}
{
char src[] = "abcdefg1234567890";
char dest[2048];
int len = 0;
auto start = high_resolution_clock::now();
for(int i = 0; i < 20000000; ++i) {
memset(dest, 0, sizeof(dest));
snprintf(dest, sizeof(dest), "%s", src);
}
auto end = high_resolution_clock::now();
std::cout <<"snprintf cost time: " << duration_cast<milliseconds>(end - start).count()<<" ms" << std::endl;
}
return 0;
}
可以看到在循环1千万次,memcpy的速度是最快的。