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

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

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

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

關(guān)鍵注意事項(xiàng)
是否自動(dòng)添加終止符:
- 如果
src的長(zhǎng)度 小于n,strncpy會(huì)在復(fù)制完src后,在dest中填充'\0'直到n個(gè)字符。 - 如果
src的長(zhǎng)度 大于等于n,dest不會(huì)自動(dòng)添加終止符,此時(shí)需要手動(dòng)添加dest[n-1] = '\0',否則可能導(dǎo)致字符串處理異常。
避免緩沖區(qū)溢出
n 應(yīng)不超過 dest 的大?。òńK止符空間)。
例如:char dest[5]; strncpy(dest, src, 5); 可能導(dǎo)致溢出(因?yàn)?dest 的有效空間只有 4 個(gè)字符 + 1 個(gè)終止符)
2.3 拷貝越界被截?cái)嗟那闆r舉例
// gcc strncpy2.c -o strncpy2
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello, World"; //原字符串11個(gè)字符,再加上結(jié)尾'\0'則占12個(gè)字符
char dest[10] = {0}; //目標(biāo)存儲(chǔ)空間只有10個(gè)
char other[10] = {0}; //隨后再定一個(gè)10個(gè)大小的other來驗(yàn)證是否被越界拷貝了
int destSize = sizeof(dest);
strncpy(dest, src, destSize-1);
dest[destSize] = '\0';
printf("復(fù)制后的字符串: %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;
}
運(yùn)行結(jié)果如下:

可以看到,使用strnpy,通過指定拷貝的長(zhǎng)度:
- 數(shù)據(jù)如果超出長(zhǎng)度,則被截?cái)啵⑶矣薪Y(jié)尾符
- 確保了其它數(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}; //拷貝的目標(biāo)位置
char other[10] = "xy"; //隨后一個(gè)位置用于測(cè)試
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); //確認(rèn)other就是在dest之后
int destSize = sizeof(dest);
//不規(guī)范1:這里的原字符串長(zhǎng)度大于destSize,并且沒有手動(dòng)添加字'\0'結(jié)尾符
strncpy(dest, "12345abcdefg", destSize);
printf("復(fù)制后的字符串: %s\n", dest); //dest與后面的other連成一個(gè)字符串了!
printf("other[0]: %c\n", other[0]); //由于strncpy拷貝了destSize,后面的other沒有影響
//不規(guī)范2:這次拷貝的字符串長(zhǎng)度小于destSize, 并且沒有手動(dòng)添加字'\0'結(jié)尾符
strncpy(dest, src, 5);
printf("復(fù)制后的字符串: %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'結(jié)尾符的,所以是6
int srcLen = strlen(src);
printf("srcSize:%d, srcLen:%d\n", srcSize, srcLen);
strncpy(dest, src, srcSize);
printf("復(fù)制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
return 0;
}
運(yùn)行結(jié)果如下:

strncpy的安全性只是在于拷貝的長(zhǎng)度可控,避免越界訪問。
但當(dāng)要拷貝的數(shù)據(jù)長(zhǎng)度大于目標(biāo)空間時(shí),數(shù)據(jù)被截?cái)?,但沒有提示,如果直接使用截?cái)嗟淖址?,也?huì)出現(xiàn)意想不到的的問題。
3 strlcpy
strlcpy是一個(gè)更安全的字符串復(fù)制函數(shù),并可以返回實(shí)際拷貝的長(zhǎng)度。
不過需要注意的是,strlcpy 并非標(biāo)準(zhǔn) C 庫函數(shù),而是在 BSD 系統(tǒng)(如 macOS)中存在,
Linux 系統(tǒng)需要通過 #include <bsd/string.h> 引入,并額外鏈接 libbsd,另外也要安裝一下這個(gè)庫:
sudo apt-get install libbsd-dev
3.1 函數(shù)原型
size_t strlcpy(char *dest, const char *src, size_t size);
將 src 的內(nèi)容復(fù)制到 dest,確保 dest 以 '\0' 結(jié)尾,并返回 src 的原始長(zhǎng)度(不包含終止符)。
參數(shù):
dest:目標(biāo)字符串指針(需提前分配空間)。src:源字符串指針(必須以'\0'結(jié)尾)。size:dest的最大容量(包括終止符'\0')。
返回值:
src的實(shí)際長(zhǎng)度(不包含'\0')。若返回值 ≥size,說明復(fù)制時(shí)發(fā)生了截?cái)唷?/li>
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("復(fù)制后的字符串: %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;
}
運(yùn)行結(jié)果:

strlcpy的特性
自動(dòng)處理終止符
strlcpy會(huì)確保dest以'\0'結(jié)尾,即使src被截?cái)唷?/li>- 最多復(fù)制
size - 1個(gè)字符到dest,并在末尾添加'\0'。
安全避免溢出
- 無論
src多長(zhǎng),dest都不會(huì)溢出。 - 若
src長(zhǎng)度 ≥size,函數(shù)會(huì)截?cái)?src,僅復(fù)制size - 1個(gè)字符。
返回值的用途
- 返回src的實(shí)際長(zhǎng)度,可用于檢測(cè)截?cái)嗲闆r:
if (strlcpy(dest, src, size) >= size) {
printf("警告:源字符串被截?cái)啵n");
}
4 上述3種函數(shù)對(duì)比
| 函數(shù) | 是否自動(dòng)添加終止符 | 溢出風(fēng)險(xiǎn) | 截?cái)嗵幚?/th> |
|---|---|---|---|
strcpy | ? | ? | 不截?cái)?,可能溢?/td> |
strncpy | ? | ? | 截?cái)嗟谎a(bǔ)終止符 |
strlcpy | ? | ? | 截?cái)嗖⒀a(bǔ)終止符 |
- strcpy使用簡(jiǎn)單,在數(shù)據(jù)長(zhǎng)度不確定的情況下使用會(huì)有風(fēng)險(xiǎn)
- strnpy指定了拷貝長(zhǎng)度嗎,但仍有數(shù)據(jù)被截?cái)嗟娘L(fēng)險(xiǎn)
- strlcpy可以通過返回值檢測(cè)是否被截?cái)啵枰~外庫的支持
5 自定義strlcpy
基于上述分析,可以對(duì)strnpy進(jìn)行自定義封裝改造,實(shí)現(xiàn)類似strlcpy,這樣也不需要額外庫的支持。
5.1 對(duì)strnpy進(jìn)行封裝實(shí)現(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ù)錯(cuò)誤
}
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ù)被截?cái)?
}
retun 0; //正??截?
}
5.2 測(cè)試驗(yàn)證
// 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ù)錯(cuò)誤
}
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ù)被截?cái)?
}
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("復(fù)制后的字符串: %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;
}
運(yùn)行結(jié)果:

6 總結(jié)
本篇介紹了C語言中如何安全的進(jìn)行字符串拷貝,首先測(cè)試了在使用strcpy、strncpy、strlcpy進(jìn)行字符串拷貝時(shí)可能遇到的問題,然后對(duì)比這兩種方式的基礎(chǔ)差別,最后通過自定義封裝strncpy來實(shí)現(xiàn)安全拷貝字符串的功能。
以上就是C語言安全的進(jìn)行字符串拷貝的幾種方法的詳細(xì)內(nèi)容,更多關(guān)于C語言進(jìn)行字符串拷貝的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Qt實(shí)現(xiàn)驗(yàn)證碼相關(guān)功能的代碼示例
驗(yàn)證碼的原理基于人類視覺和計(jì)算機(jī)視覺的差異性,通過給用戶顯示一些難以被機(jī)器識(shí)別的圖形或文字,讓用戶進(jìn)行人機(jī)交互,確認(rèn)自己的身份,這樣可以有效保護(hù)網(wǎng)站安全,所以本給大家介紹了Qt實(shí)現(xiàn)驗(yàn)證碼相關(guān)功能的代碼示例,感興趣的朋友可以參考下2024-01-01
C語言數(shù)組和指針,內(nèi)存之間的關(guān)系
這篇文章主要介紹了C語言數(shù)組和指針,內(nèi)存之間的關(guān)系,首先論證一維數(shù)組和一級(jí)指針之前的關(guān)系,我們常常使用一級(jí)指針指針的方式訪問一維數(shù)組,只有對(duì)內(nèi)存的理解到位才能理解它們直接的關(guān)系。需要的小伙伴可以參考一下2022-02-02
C++鏈表實(shí)現(xiàn)通訊錄設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了C++鏈表實(shí)現(xiàn)通訊錄設(shè)計(jì),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
C++ vector從入門到模擬實(shí)現(xiàn)過程
這段文章詳細(xì)講解了C++ STL中的vector容器,從其優(yōu)勢(shì)、基礎(chǔ)刪查改操作到迭代器失效等,并提供了模擬實(shí)現(xiàn)和常見陷阱的解析,幫助開發(fā)者全面掌握vector的用法,重點(diǎn)強(qiáng)調(diào)了reserve的用法、迭代器失效的處理和深拷貝、淺拷貝的區(qū)別,感興趣的朋友一起看看吧2026-06-06
c++11中關(guān)于std::thread的join的詳解
這篇文章主要介紹了c++11中關(guān)于std::thread的join詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
關(guān)于VS2019 C++項(xiàng)目同時(shí)出現(xiàn)LNK2005 和LNK1169 error 的解決辦法
這篇文章主要介紹了關(guān)于VS2019 C++項(xiàng)目同時(shí)出現(xiàn)LNK2005 和LNK1169 error 的解決辦法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04
Matlab實(shí)現(xiàn)將圖像序列合并為視頻的方法詳解
MATLAB是一種高性能語言,用于操縱矩陣、執(zhí)行技術(shù)計(jì)算、繪圖等。它代表矩陣實(shí)驗(yàn)室。借助這個(gè)軟件,我們可以從圖像中創(chuàng)建視頻。這篇文章主要介紹了Matlab實(shí)現(xiàn)將圖像序列合并為視頻的四個(gè)方法,希望對(duì)大家有所幫助2023-03-03

