C語言安全的進行字符串拷貝的幾種方法
引言
在C語言編程中,字符串是通過定義char數(shù)組來保存,并通過指針以及字符串拷貝函數(shù)(如strcpy)來實現(xiàn)字符串的拷貝,無法方便的像C++中使用等號直接對std::string類型的字符串賦值。
使用strcpy這類接口,在字符串長度不太明確的情況下,可能處出現(xiàn)拷貝越界的情況,給程序引入了不安全的因素。本篇就來討論下,在C語言中,如何安全的使用strcpy這類函數(shù)進行字符串的拷貝。
1 strcpy
strcpy是C語言標準庫中的一個最基礎的字符串處理函數(shù),可以把源字符串復制到目標字符串。
1.1 函數(shù)原型
char *strcpy(char *dest, const char *src);
將 src 所指向的以空字符('\0')結尾的字符串復制到 dest 所指向的數(shù)組中,同時會復制終止空字符
參數(shù):
dest是目標字符串的指針src是源字符串的指針
返回值:
- 返回目標字符串的指針
1.2 使用示例
一個基礎的strcpy使用示例,需要確保目標數(shù)組足夠大
// gcc strcpy1.c -o strcpy
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello";
char dest[10] = {0}; // 確保目標數(shù)組足夠大
strcpy(dest, src);
printf("復制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
return 0;
}
運行結果如下:

注意事項
- 目標數(shù)組大小要足夠:
strcpy不會檢查目標數(shù)組的大小,一旦目標數(shù)組的空間不足以容納源字符串,就會導致緩沖區(qū)溢出,這是非常危險的,可能會引發(fā)程序崩潰或者產(chǎn)生安全漏洞。 - 源字符串必須以空字符結尾:如果源字符串沒有以
'\0'結尾,strcpy會一直復制內(nèi)存中的數(shù)據(jù),直到遇到空字符為止,這會造成未定義行為。 - 避免自我復制:不要將字符串復制到自身,否則會導致數(shù)據(jù)被破壞。
1.3 拷貝越界情況舉例
測試一下,如果要拷貝的字符串長度大于目標存儲空間,會是什么結果。
// gcc strcpy2.c -o strcpy2
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello, World"; //原字符串11個字符,再加上結尾'\0'則占12個字符
char dest[10] = {0}; //目標存儲空間只有10個
char other[10] = {0}; //隨后再定一個10個大小的other來驗證是否被越界拷貝了
strcpy(dest, src);
printf("復制后的字符串: %s\n", dest);
printf("dest addr:%p\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
printf("other addr:%p\n", other);
size_t bytes = (char*)other - (char*)dest;
printf("other (other-dest):%zu\n", bytes);
for (int i=0; i<sizeof(other); i++)
{
printf("other[%d]:%x(%c)\n", i, other[i], other[i]);
}
return 0;
}
運行結果如下:

需要注意的是,雖然dest字符串通過printf正常輸出了,但實際是拷貝字符時,越界拷貝,雖然這里暫時沒有問題,但other中的內(nèi)容被篡改,如果后續(xù)需要使用other中的內(nèi)容,可能就會出現(xiàn)不符合預期的結果。
1.4關于是否會加上結尾符的驗證
// gcc strcpy3.c -o strcpy3
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello";
char dest[10] = {0}; // 確保目標數(shù)組足夠大
strcpy(dest, "12345abcd");
printf("復制后的字符串: %s\n", dest);
strcpy(dest, src);
printf("復制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
//再測試一下char數(shù)組逐個賦值后的拷貝(確保src2當做字符串時最后沒有‘\0')
char src2[2];
src2[0] = 'm';
src2[1] = 'n';
strcpy(dest, src2);
printf("復制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
return 0;
}
運行結果如下:

可以看到:
- 拷貝src字符串時,"Hello"被正??截?,后面一個’\0’也拷貝了,dest中剩下的則保持原樣
- 拷貝char src2[2]時,由于src2沒有自動被賦予的’\0’字符串結尾符,在strcpy賦值時,就只是復制了2個char數(shù)據(jù)本身,所有在printf打印時,就和后面的數(shù)據(jù)一起打印出來了,直到遇到了’\0’字符。
- 另外有點特殊的是,dest原有的內(nèi)容,被整體向后移動了。
2 strncpy
strncpy是 strcpy的安全版本,用于復制指定長度的字符串。
2.1 函數(shù)原型
char *strncpy(char *dest, const char *src, size_t n);
將 src 的前 n 個字符復制到 dest,不自動添加終止符 '\0'(除非 src 的長度小于 n)。
參數(shù):
dest:目標字符串指針(需提前分配足夠空間)。src:源字符串指針(必須以'\0'結尾)。n:最多復制的字符數(shù)。
返回值:
- 返回目標字符串的指針,也就是
dest。
它會復制最多 n 個字符,能夠防止緩沖區(qū)溢出。
不過要注意,如果源字符串的長度超過 n,dest 數(shù)組將不會以空字符結尾。
2.2 使用示例
// gcc strncpy1.c -o strncpy1
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello";
char dest[10] = {0}; // 確保目標數(shù)組足夠大
int destSize = sizeof(dest);
strncpy(dest, src, destSize-1); //最多只能復制destSize-1, 因為要加上結尾符
dest[destSize] = '\0'; //手動加上結尾符
printf("復制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
return 0;
}
運行結果如下:

關鍵注意事項
是否自動添加終止符:
- 如果
src的長度 小于n,strncpy會在復制完src后,在dest中填充'\0'直到n個字符。 - 如果
src的長度 大于等于n,dest不會自動添加終止符,此時需要手動添加dest[n-1] = '\0',否則可能導致字符串處理異常。
避免緩沖區(qū)溢出
n 應不超過 dest 的大小(包括終止符空間)。
例如:char dest[5]; strncpy(dest, src, 5); 可能導致溢出(因為 dest 的有效空間只有 4 個字符 + 1 個終止符)
2.3 拷貝越界被截斷的情況舉例
// gcc strncpy2.c -o strncpy2
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello, World"; //原字符串11個字符,再加上結尾'\0'則占12個字符
char dest[10] = {0}; //目標存儲空間只有10個
char other[10] = {0}; //隨后再定一個10個大小的other來驗證是否被越界拷貝了
int destSize = sizeof(dest);
strncpy(dest, src, destSize-1);
dest[destSize] = '\0';
printf("復制后的字符串: %s\n", dest);
printf("dest addr:%p\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
printf("other addr:%p\n", other);
size_t bytes = (char*)other - (char*)dest;
printf("other (other-dest):%zu\n", bytes);
for (int i=0; i<sizeof(other); i++)
{
printf("other[%d]:%x(%c)\n", i, other[i], other[i]);
}
return 0;
}
運行結果如下:

可以看到,使用strnpy,通過指定拷貝的長度:
- 數(shù)據(jù)如果超出長度,則被截斷,并且有結尾符
- 確保了其它數(shù)據(jù)不被越界訪問
2.4 不規(guī)范的使用舉例
// gcc strncpy3.c -o strncpy3
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello";
char dest[10] = {0}; //拷貝的目標位置
char other[10] = "xy"; //隨后一個位置用于測試
printf("other[0]: %c\n", other[0]);
printf("dest addr:%p\n", dest);
printf("other addr:%p\n", other);
size_t bytes = (char*)other - (char*)dest;
printf("other (other-dest):%zu\n", bytes); //確認other就是在dest之后
int destSize = sizeof(dest);
//不規(guī)范1:這里的原字符串長度大于destSize,并且沒有手動添加字'\0'結尾符
strncpy(dest, "12345abcdefg", destSize);
printf("復制后的字符串: %s\n", dest); //dest與后面的other連成一個字符串了!
printf("other[0]: %c\n", other[0]); //由于strncpy拷貝了destSize,后面的other沒有影響
//不規(guī)范2:這次拷貝的字符串長度小于destSize, 并且沒有手動添加字'\0'結尾符
strncpy(dest, src, 5);
printf("復制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
int srcSize = sizeof(src); //注意src的sizeof是包含了之后的'\0'結尾符的,所以是6
int srcLen = strlen(src);
printf("srcSize:%d, srcLen:%d\n", srcSize, srcLen);
strncpy(dest, src, srcSize);
printf("復制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
return 0;
}
運行結果如下:

strncpy的安全性只是在于拷貝的長度可控,避免越界訪問。
但當要拷貝的數(shù)據(jù)長度大于目標空間時,數(shù)據(jù)被截斷,但沒有提示,如果直接使用截斷的字符串,也會出現(xiàn)意想不到的的問題。
3 strlcpy
strlcpy是一個更安全的字符串復制函數(shù),并可以返回實際拷貝的長度。
不過需要注意的是,strlcpy 并非標準 C 庫函數(shù),而是在 BSD 系統(tǒng)(如 macOS)中存在,
Linux 系統(tǒng)需要通過 #include <bsd/string.h> 引入,并額外鏈接 libbsd,另外也要安裝一下這個庫:
sudo apt-get install libbsd-dev
3.1 函數(shù)原型
size_t strlcpy(char *dest, const char *src, size_t size);
將 src 的內(nèi)容復制到 dest,確保 dest 以 '\0' 結尾,并返回 src 的原始長度(不包含終止符)。
參數(shù):
dest:目標字符串指針(需提前分配空間)。src:源字符串指針(必須以'\0'結尾)。size:dest的最大容量(包括終止符'\0')。
返回值:
src的實際長度(不包含'\0')。若返回值 ≥size,說明復制時發(fā)生了截斷。
3.2 使用示例
// gcc strlcpy.c -o strlcpy -lbsd
#include <stdio.h>
#include <string.h>
#include <bsd/string.h> // Linux需要顯式包含
int main()
{
char src[] = "Hello, World";
char dest[10] = {0};
char other[10] = {0};
size_t len = strlcpy(dest, src, sizeof(dest));
printf("strlcpy ret:%zu\n", len);
if (len > sizeof(dest))
{
printf("copy oversize!\n");
}
printf("復制后的字符串: %s\n", dest);
printf("dest addr:%p\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
printf("other addr:%p\n", other);
size_t bytes = (char*)other - (char*)dest;
printf("other (other-dest):%zu\n", bytes);
for (int i=0; i<sizeof(other); i++)
{
printf("other[%d]:%x(%c)\n", i, other[i], other[i]);
}
return 0;
}
運行結果:

strlcpy的特性
自動處理終止符
strlcpy會確保dest以'\0'結尾,即使src被截斷。- 最多復制
size - 1個字符到dest,并在末尾添加'\0'。
安全避免溢出
- 無論
src多長,dest都不會溢出。 - 若
src長度 ≥size,函數(shù)會截斷src,僅復制size - 1個字符。
返回值的用途
- 返回src的實際長度,可用于檢測截斷情況:
if (strlcpy(dest, src, size) >= size) {
printf("警告:源字符串被截斷!\n");
}
4 上述3種函數(shù)對比
| 函數(shù) | 是否自動添加終止符 | 溢出風險 | 截斷處理 |
|---|---|---|---|
strcpy | ? | ? | 不截斷,可能溢出 |
strncpy | ? | ? | 截斷但不補終止符 |
strlcpy | ? | ? | 截斷并補終止符 |
- strcpy使用簡單,在數(shù)據(jù)長度不確定的情況下使用會有風險
- strnpy指定了拷貝長度嗎,但仍有數(shù)據(jù)被截斷的風險
- strlcpy可以通過返回值檢測是否被截斷,但需要額外庫的支持
5 自定義strlcpy
基于上述分析,可以對strnpy進行自定義封裝改造,實現(xiàn)類似strlcpy,這樣也不需要額外庫的支持。
5.1 對strnpy進行封裝實現(xiàn)自定義檢查
int my_strlcpy(char *dst, const char *src, size_t dstSize)
{
size_t srcLen = strlen(src);
if (NULL == dst || NULL == src || 0 == dstSize)
{
return -1; //參數(shù)錯誤
}
size_t maxCopyLen = dstSize - 1;
strnpy(dst, src, maxCopyLen);
dst[len] = '\0';
if (srclen > maxCopyLen)
{
printf("%d > %d, copy oversize! only copy:%s\n", srclen, maxCopyLen, dst);
return -2; //數(shù)據(jù)被截斷
}
retun 0; //正??截?
}
5.2 測試驗證
// gcc my_strlcpy.c -o my_strlcpy
#include <stdio.h>
#include <string.h>
int my_strlcpy(char *dst, const char *src, size_t dstSize)
{
size_t srcLen = strlen(src);
if (NULL == dst || NULL == src || 0 == dstSize)
{
return -1; //參數(shù)錯誤
}
size_t maxCopyLen = dstSize - 1;
strncpy(dst, src, maxCopyLen);
dst[maxCopyLen] = '\0';
if (srcLen > maxCopyLen)
{
printf("%zu > %zu, copy oversize! only copy:%s\n", srcLen, maxCopyLen, dst);
return -2; //數(shù)據(jù)被截斷
}
return 0; //正??截?
}
int main()
{
char src[] = "Hello, World";
char dest[10] = {0};
char other[10] = {0};
int ret = my_strlcpy(dest, src, sizeof(dest));
if (ret)
{
printf("err! my_strlcpy ret:%d\n", ret);
}
printf("復制后的字符串: %s\n", dest);
printf("dest addr:%p\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
printf("other addr:%p\n", other);
size_t bytes = (char*)other - (char*)dest;
printf("other (other-dest):%zu\n", bytes);
for (int i=0; i<sizeof(other); i++)
{
printf("other[%d]:%x(%c)\n", i, other[i], other[i]);
}
return 0;
}
運行結果:

6 總結
本篇介紹了C語言中如何安全的進行字符串拷貝,首先測試了在使用strcpy、strncpy、strlcpy進行字符串拷貝時可能遇到的問題,然后對比這兩種方式的基礎差別,最后通過自定義封裝strncpy來實現(xiàn)安全拷貝字符串的功能。
以上就是C語言安全的進行字符串拷貝的幾種方法的詳細內(nèi)容,更多關于C語言進行字符串拷貝的資料請關注腳本之家其它相關文章!
相關文章
關于VS2019 C++項目同時出現(xiàn)LNK2005 和LNK1169 error 的解決辦法
這篇文章主要介紹了關于VS2019 C++項目同時出現(xiàn)LNK2005 和LNK1169 error 的解決辦法,本文給大家介紹的非常詳細,對大家的學習工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04

