C語言上總有些非常相近的接口函數,比如sprintf和snprintf就是其中的一對。以筆者多年的工作經驗,這對接口函數在平時的編程中,使用的頻度是非常高,只是你真的了解它們倆的區別嗎?
帶著這個問題,請跟隨筆者的思路梳理一遍sprintf和snprintf。通過閱讀本文,你將了解到以下內容:
sprintf和snpintf分別是什么?
sprintf和snprintf的區別與聯系
sprintf和snprintf的使用秘訣
sprintf和snpintf分別是什么?
【sprintf】的函數原型如下所示:
/**
功能: 把格式化的數據寫入某個字符串緩沖區
入參:format,輸出字符串的格式化列表,比如"%s %d %c"等
入參: [argument],format對應的不定參數列表,與printf的不定入參類似
出參:buffer,指向一段存儲空間,用于存儲格式化之后的字符串
返回值:返回寫入buffer 的字符數,出錯則返回-1. 如果 buffer 或 format 是空指針,
且不出錯而繼續,函數將返回-1,并且 errno 會被設置為 EINVAL。
備注:它是個變參函數
*/
int sprintf( char *buffer, const char *format, [ argument] … );
【snprintf】的函數原型如下所示:
/**
功能: 有長度限制地,把格式化的數據寫入某個字符串緩沖區
入參:format,輸出字符串的格式化列表,比如"%s %d %c"等
入參: [argument],format對應的不定參數列表,與printf的不定入參類似
入參:size,表示buffer指向存儲空間的大小
出參:buffer,指向一段存儲空間,用于存儲格式化之后的字符串
返回值:返回寫入buffer 的字符數,出錯則返回-1. 如果 buffer 或 format 是空指針,
且不出錯而繼續,函數將返回-1,并且 errno 會被設置為 EINVAL。
備注:它是個變參函數
*/
int snprintf( char *buffer, size_t size, const char *format, [ argument] … );
sprintf和snprintf的區別與聯系
通過對比sprintf和snprintf的函數原型,我們可以發現兩者其實完成相同功能的接口,都是將一段數據經格式化操作之后,轉換成一段字符串,通過接口傳入的buffer指針將格式化的字符串內容輸出。
我們細細比對兩個函數原型,我們會發現snprintf比sprintf多了一個表示buffer指針指向存儲空間的大小的入參size,那么它到底有什么作用呢?我們先來分析下snprintf接口的內部行為與size的關系:
如果格式化后的字符串長度 < size,則將此字符串全部復制到str中,并給其后添加一個字符串結束符('\0');
如果格式化后的字符串長度 >= size,則只將其中的(size-1)個字符復制到str中,并給其后添加一個字符串結束符('\0'),返回值為欲寫入的字符串長度。
看完這一段解釋之后,大概你就明白了,原來snprintf就是sprintf的安全版本,因為單從sprintf的內部行為來看,它是沒有辦法保證對buffer指針的賦值操作是沒有越界的,因為它壓根就不知道buffer的存儲空間多少有多大,所以它只能認為是【無窮大】。但是snprintf通過入參size,恰好可以很好的解決這個問題,它可以很明確的告知snprintf的內部操作,以size作為界線,當輸出的字符串長度要超過size時,應做出裁剪輸出。在很多的編程寶典中,都是推薦使用snprintf,而要求編程者盡可能地避免使用sprintf這種不安全接口。
sprintf和snprintf的使用秘訣
我們通過一段測試代碼來展示下兩者的使用方法,以及上一小結中提及的可能導致buffer溢出的嚴重問題:
//sprintf的用法
{
char buffer[10]; //定義一個只有10個字節空間的buffer數組
const int a = 12345; //定義一個int型的常量
const char *msg = "012345678901234567890"; //定義一個長度為20字節的字符串常量
sprintf(buffer, "%d", a); //將a變量按int類型打印成字符串,輸出到buffer中
/*
輸出分析:
輸出結果: buffer="12345"
因為最后輸出的buffer內容長度不超過10字節,所以此時sprintf操作是沒有溢出風險的
*/
sprintf(buffer, "%d+%s", a, msg); //將a變量和msg字符串通過“+”連接成一個字符串
/*
輸出分析:
由于buffer只有10個字節空間,而sprintf在執行字符串格式化輸出的時,并不知道buffer的真實長度,
所以它會將"12345+012345678901234567890"這整個字符串都拷貝到buffer空間上,這就導致了buffer存儲空間溢出了。
從存儲位置上分析,我們知道buffer空間屬于一個棧空間,在它自己的10字節之外的空間很明顯是其他棧變量的存儲空間,
一旦sprintf將10字節外的其他空間也操作了,這就有可能破壞了其他棧變量的內容,這有可能是致命的。
*/
}
//snprintf的用法
{
char buffer[10]; //定義一個只有10個字節空間的buffer數組
const int a = 12345; //定義一個int型的常量
const char *msg = "012345678901234567890"; //定義一個長度為20字節的字符串常量
snprintf(buffer, sizeof(buffer), "%d", a); //將a變量按int類型打印成字符串,輸出到buffer中
/*
輸出分析:
輸出結果: buffer="12345"
因為最后輸出的buffer內容長度不超過10字節,所以snprintf操作是沒有溢出風險的;
此種情況下,使用sprintf和snpintf都可以得到同樣的結果,且都不會產生數組溢出。
*/
sprintf(buffer, sizeof(buffer), "%d+%s", a, msg); //將a變量和msg字符串通過“+”連接成一個字符串
/*
輸出分析:
輸出結果是: buffer="12345+0123",加上一個'\0'的字符串結束符,
剛好占用了buffer的10字節的存儲空間,不存在任何的buffer溢出風險。而"0123"后面的字符串都被snprintf內部裁剪掉了,這就體現了snprintf操作安全的特性。
*/
}
通過以上分析,我們很好地認識到了sprintf的操作是不安全的。在C語言的語法上,指針的靈活性也帶來可能導致的指針溢出風險,而snprintf恰好就是解決了這個困惑的sprintf升級版本。
類似的,還有strcat和strncat、strcpy和strncpy等等。通過本文的方法,讀者也可以寫一小段測試代碼,好好捋一捋本文提及的這幾組函數,一起領悟下其他的奧秘和使用風險吧。
以上總結,均來自筆者多年的實踐經驗,如有發現不正確的陳述或錯誤的觀點,還望讀者指正,感激不盡。
-
C語言
+關注
關注
180文章
7604瀏覽量
136692 -
函數
+關注
關注
3文章
4327瀏覽量
62573 -
sprintf
+關注
關注
0文章
6瀏覽量
4020
發布評論請先 登錄
相關推薦
評論