sprintf、vsprintf、sprintf_s、vsprintf_s、_snprintf、_vsnprintf、snprintf、vsnprintf 函數辨析

看了題目中的幾個函數名是否是有點頭暈?爲了防止之後總在這樣的細節裏糾纏不清,今天咱們就來好好地辨析一下這幾個函數的異同。安全

實驗環境:函數

Windows下使用VS2017
Linux下使用gcc4.9.4測試

 

爲了驗證函數的安全性咱們設計了以下結構spa

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字段的內存會被修改。咱們以此來推斷函數的安全性。設計

1、sprintf(Linux/Windows)code

Linux下的函數原型:int sprintf(char *str, const char *format, ...);
測試代碼:orm

int main()
{
    Data data;
    data.Display();
    int ret = sprintf(data.buf, "%d", 12);
    std::cout << "ret = " << ret << std::endl;
    data.Display();
    std::cin.get();
    return 0;
}

在VS2017環境中,這個函數被標記爲不安全的,若是使用了,編譯器會報警告,若是非要使用,必須在編譯的時候增長宏定義:_CRT_SECURE_NO_WARNINGS,告訴編譯器忽略安全警告。在Linux下此函數能夠正常使用。並且這個函數在Windows下和Linux下行爲也是同樣的。具體以下:blog

1.當源數據的長度【小於】len,sprintf把數據完整的寫到目標內存,並保證尾部以0結尾,返回寫入的字節數。此時該函數的行爲是安全的。
例如:內存

 sprintf(data.buf, "%d", 12); ci

輸出:

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同樣。

 

2、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同樣。

 

3、_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同樣。

 

4、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;
}

1、strcpy(Linux/Windows)
函數原型爲:char *strcpy(char *dest, const char *src);
最古老的字符串拷貝函數,原理很簡單,從源字符串依次拷貝字符到目標地址,直到遇到0爲止,如遇到內存重疊的時候,須要特殊處理。老是返回實際寫入的字符數,不會處理內存越界的狀況,也是毫無安全性,在此不作贅述。


2、strcpy_s(Windows only)
是Windows獨有的函數,原型能夠描述爲:
int strcpy_s(char *dest, size_t size, const char *src);
注意返回值再也不是目標字符串的首地址,而是一個int。
當源字符串長度【小於】或【等於】目標內存的時候,此函數能夠安全執行,返回值爲【0】,當源字符串長度【大於】目標內存的時候,此函數會觸發assert斷言,致使程序中斷。這個函數不會致使內存破壞。

3、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中斷。

4、strncpy(Linux/Windows)
函數原型:char *strncpy(char *dest, const char *src, size_t size);
行爲與strcpy相似,從源字符串依次拷貝字符到目標地址,直到遇到0或者目標內存已寫滿爲止,最多拷貝【size】個字符。這個函數不會破壞內存,也不會致使程序中斷,可是沒法保證目標字符串以0結尾。
例如:

strncpy(data.buf, "12345", len);

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = 1234燙燙燙
sizeof(Data) = 5
buf = 1234燙燙燙
guard = 15
---------------
相關文章
相關標籤/搜索