sprintf、vsprintf、sprintf_s、vsprintf_s、_snprintf、_vsnprintf、snprintf、vsnprintf 函数辨析
看了題目中的幾個函數名是不是有點頭暈?為了防止以后總在這樣的細節里糾纏不清,今天我們就來好好地辨析一下這幾個函數的異同。
實驗環境:
Windows下使用VS2017
Linux下使用gcc4.9.4
?
為了驗證函數的安全性我們設計了如下結構
const int len = 4; #pragma pack(push) #pragma pack(1) struct Data {char buf[len];char guard;Data(){for (int i = 0; i < len; ++i){buf[i] = '*';}guard = 0xF;}void Display(){std::cout << "sizeof(Data) = " << sizeof(Data) << std::endl;std::cout << "buf = " << buf << std::endl;std::cout << "guard = " << (unsigned int)guard << std::endl;if (guard != 0xF){std::cout << "memory has been broken." << std::endl;}std::cout << "---------------" << std::endl;} }; #pragma pack(pop)當我們把數據寫到Data.buf字段中去的時候,如果發生了內存越界的情況,Data.gurad字段的內存會被修改。我們以此來推斷函數的安全性。
一、sprintf(Linux/Windows)
Linux下的函數原型:int sprintf(char *str, const char *format, ...);
測試代碼:
在VS2017環境中,這個函數被標記為不安全的,如果使用了,編譯器會報警告,如果非要使用,必須在編譯的時候增加宏定義:_CRT_SECURE_NO_WARNINGS,告訴編譯器忽略安全警告。在Linux下此函數可以正常使用。而且這個函數在Windows下和Linux下行為也是一樣的。具體如下:
1.當源數據的長度【小于】len,sprintf把數據完整的寫到目標內存,并保證尾部以0結尾,返回寫入的字節數。此時該函數的行為是安全的。
例如:
?sprintf(data.buf,?"%d",?12);?
輸出:
sizeof(Data) = 5 buf = ****燙燙燙 guard = 15 --------------- ret = 2 sizeof(Data) = 5 buf = 12 guard = 15 ---------------?
2.當源數據的長度【等于】len,sprintf把數據完整的寫到目標內存,并在目標內存的尾部多寫入一個0,返回寫入的字節數。此時該函數已經發生拷貝越界的情況了。所以,當用戶以為分配的內存剛剛好滿足拷貝需求的時候,其實已經發生了潛在的風險。
例如:
?sprintf(data.buf,?"%d",?1234);?
輸出:
sizeof(Data) = 5 buf = ****燙燙燙 guard = 15 --------------- ret = 4 sizeof(Data) = 5 buf = 1234 guard = 0 memory has been broken. ---------------3.當源數據的長度【大于】len,sprintf把數據完整的寫到目標內存,返回寫入的字節數,壓根不管內存越界的情況,甚至連個錯誤碼都不返回。
例如:
?sprintf(data.buf,?"%d",?123456);?
輸出:
sizeof(Data) = 5 buf = ****燙燙燙 guard = 15 --------------- ret = 6 sizeof(Data) = 5 buf = 123456 guard = 53 memory has been broken. ---------------總結:以上三組實驗結果,在Windows和Linux下均可以得到驗證,可見sprintf函數的安全系數幾乎為0,不推薦大家使用。
vsprintf的行為與sprintf一樣。
?
二、sprintf_s(Windows only)
為了彌補sprintf函數的不足,高版本的MSVC環境中引入了sprintf_s函數,在調用的時候支持用戶傳入目標內存的長度,函數原型可以簡略的表示為:
?int?sprintf_s(char?*buf, size_t buf_size,?const?char?*format, ...);?
1.當源數據的長度【小于】len,sprintf把數據完整的寫到目標內存,并保證尾部以0結尾,返回寫入的字節數。此時該函數的行為是安全的。
例如:
?sprintf_s(data.buf, len,?"%d",?12);?
輸出:
sizeof(Data) = 5 buf = ****燙燙燙 guard = 15 --------------- ret = 2 sizeof(Data) = 5 buf = 12 guard = 15 ---------------2.當源數據的長度【等于】或者【大于】len的時候,調用此函數將會觸發斷言。Debug模式下會彈出運行時錯誤提示框,告訴用戶"Buffer too small";Release模式下程序會直接崩潰。
例如:
?sprintf_s(data.buf, len,?"%d",?1234);?
Debug模式下執行,會觸發assert,如下圖:
總結:sprintf_s函數只能在Windows下使用,雖然不會出現寫壞內存的情況,但是會觸發assert,導致程序中斷,使用起來也要慎重。
vsprintf_s的行為與sprintf_s一樣。
?
三、_snprintf(Windows only)
也許是覺得sprintf_s也不夠安全,MSVC環境中還引入了一個名為_snprintf的函數,其函數原型和sprintf_s類似,可以表示為:
?int?_snprintf(char?*buf, size_t buf_size,?const?char?*format, ...);?
其表現行為如下:
例1,當源數據的長度【小于】len,能保證完整寫入,并以0結尾,返回實際寫入的字節數:
?_snprintf(data.buf, len,?"%d",?12);?
輸出:
sizeof(Data) = 5 buf = ****燙燙燙 guard = 15 --------------- ret = 2 sizeof(Data) = 5 buf = 12 guard = 15 ---------------例2,當源數據的長度【等于】len,能保證完整寫入,結尾不做任何處理,返回實際寫入的字節數:
?_snprintf(data.buf, len,?"%d",?1234);?
輸出:
sizeof(Data) = 5 buf = ****燙燙燙 guard = 15 --------------- ret = 4 sizeof(Data) = 5 buf = 1234燙燙燙 guard = 15 ---------------例3,當源數據的長度【大于】len,最多寫入【len】個字符,結尾不錯任何處理,返回【-1】:
?_snprintf(data.buf, len,?"%d",?123456);?
輸出:
sizeof(Data) = 5 buf = ****燙燙燙 guard = 15 --------------- ret = -1 sizeof(Data) = 5 buf = 1234燙燙燙 guard = 15 ---------------總結:_snprintf函數只能在Windows下使用,最多寫入【size】個字符,永遠不破壞內存,也不會觸發中斷,但不能保證目標內存以0結尾。通過返回值可以知道函數調用是否成功,返回值>=0的時候,表示調用成功,返回了實際寫入的字符數;返回值為-1的時候,表示目標內存太小,導致調用失敗,但是已經盡力做了填充。
_vsnprintf的行為與_snprintf一樣。
?
四、snprintf(Linux/Windows)
Linux下的函數原型為:
?int?snprintf(char?*str, size_t size,?const?char?*format, ...);?
這個函數在Windows和Linux下均可以使用,并且行為一致。即:最多寫入【size-1】個字符到目標內存,并保證以0結尾。返回值是【應該寫入的字節數】,而不是【實際寫入的字節數】
例1,當源數據的長度【小于】len,能保證完整寫入,并以0結尾,返回實際寫入的字節數:
?snprintf(data.buf, len,?"%d",?12);?
輸出:
sizeof(Data) = 5 buf = ****燙燙燙 guard = 15 --------------- ret = 2 sizeof(Data) = 5 buf = 12 guard = 15 ---------------例2:當源數據的長度【等于】len,實際上只寫入了【len-1】個字符,最后一個字符用0填充,但返回值卻是【len】:
?snprintf(data.buf, len,?"%d",?1234);?
輸出:
sizeof(Data) = 5 buf = ****燙燙燙 guard = 15 --------------- ret = 4 sizeof(Data) = 5 buf = 123 guard = 15 ---------------例3,當源數據的長度【大于】len,最多也只寫入【len-1】個字符,最后一個字符用0填充,但返回值卻是【應該要寫入的字節數】:
?snprintf(data.buf, len,?"%d",?123456);?
輸出:
sizeof(Data) = 5 buf = ****燙燙燙 guard = 15 --------------- ret = 6 sizeof(Data) = 5 buf = 123 guard = 15 ---------------總結:snprintf函數,可以在Linux/Windows雙平臺下使用,最多寫入【size-1】個字符,永遠不會破壞內存,也不會觸發中斷,并總能保證目標內存能以0結尾。唯一的問題是返回值不可靠,無法推斷調用是否失敗。
vsnprintf的行為與snprintf一樣。
寫到這里,sprintf系列的相關函數都講完了,貌似沒有一個完美的函數。不過既然知道了它們的具體行為,就可以根據應用場景挑選適合的函數。
?
補充:既然已經寫到這兒了,就順便利用這個機會順便把strcpy函數簇也研究一下吧。
測試代碼:
int main() {Data data;data.Display();const char * ret = strncpy(data.buf, "12345678", len);std::cout << "ret = " << ret << std::endl;data.Display();std::cin.get();return 0; }一、strcpy(Linux/Windows)
函數原型為:char *strcpy(char *dest, const char *src);
最古老的字符串拷貝函數,原理很簡單,從源字符串依次拷貝字符到目標地址,直到遇到0為止,如遇到內存重疊的時候,需要特殊處理。總是返回實際寫入的字符數,不會處理內存越界的情況,也是毫無安全性,在此不做贅述。
二、strcpy_s(Windows only)
是Windows獨有的函數,原型可以描述為:
int strcpy_s(char *dest, size_t size, const char *src);
注意返回值不再是目標字符串的首地址,而是一個int。
當源字符串長度【小于】或【等于】目標內存的時候,此函數可以安全執行,返回值為【0】,當源字符串長度【大于】目標內存的時候,此函數會觸發assert斷言,導致程序中斷。這個函數不會導致內存破壞。
三、strncpy_s(Windows only)
是Windows獨有的函數,原型可以描述為:
int strncpy_s(char *dest, size_t dest_size, const char *src, size_t count);
返回值也是一個int。
這個函數除了能指定目標內存的大小,還能指定拷貝的字符數量,相當于做了雙重保護。
但是注意必須滿足【count <= dest_size - 1】,這個函數才能正確調用,否則也會觸發assert中斷。
四、strncpy(Linux/Windows)
函數原型:char *strncpy(char *dest, const char *src, size_t size);
行為與strcpy類似,從源字符串依次拷貝字符到目標地址,直到遇到0或者目標內存已寫滿為止,最多拷貝【size】個字符。這個函數不會破壞內存,也不會導致程序中斷,但是無法保證目標字符串以0結尾。
例如:
輸出:
sizeof(Data) = 5 buf = ****燙燙燙 guard = 15 --------------- ret = 1234燙燙燙 sizeof(Data) = 5 buf = 1234燙燙燙 guard = 15 ---------------總結
以上是生活随笔為你收集整理的sprintf、vsprintf、sprintf_s、vsprintf_s、_snprintf、_vsnprintf、snprintf、vsnprintf 函数辨析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vuejs集成simditor
- 下一篇: Find命令的使用和解析