在 snprintf 的输出中看到的杂散字符 [英] Stray characters seen at output of snprintf

查看:28
本文介绍了在 snprintf 的输出中看到的杂散字符的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在 C 中有一个字符串创建函数,它接受一个 结构数组 作为它的参数,并根据预定义格式输出一个字符串(如 Python 中的列表列表).
这是功能

I have a string creating function in C which accepts an array of structs as it's argument and outputs a string based on a predefined format (like a list of list in python).
Here's the function

typedef struct
{
    PacketInfo_t PacketInfo;
    char Gnss60[1900]; 
    //and other stuff...
} Track_json_t;

typedef struct 
{
    double latitude;
    double longitude;
} GPSPoint_t;

typedef struct
{
    UInt16          GPS_StatusCode;
    UInt32          fixtime;
    GPSPoint_t      point;
    double          altitude;
    unsigned char GPS_Satilite_Num;
} GPS_periodic_t;

unsigned short SendTrack()
{
    Track_json_t i_sTrack_S;
    memset(&i_sTrack_S, 0x00, sizeof(Track_json_t));
    getEvent_Track(&i_sTrack_S);
    //Many other stuff added to the i_sTrack_S struct...
    //Make a JSON format out of it
    BuildTrackPacket_json(&i_sTrack_S, XPORT_MODE_GPRS);
}

Track_json_t *getEvent_Track(Track_json_t *trk)
{
    GPS_periodic_t l_gps_60Sec[60];
    memset(&l_gps_60Sec, 0x00,
           sizeof(GPS_periodic_t) * GPS_PERIODIC_ARRAY_SIZE);
    getLastMinGPSdata(l_gps_60Sec, o_gps_base);
    get_gps60secString(l_gps_60Sec, trk->Gnss60);
    return trk;
}

void get_gps60secString(GPS_periodic_t input[60], char *output)
{
    int i = 0;
    memcpy(output, "[", 1); ///< Copy the first char as [
    char temp[31];
    for (i = 0; i < 59; i++) { //Run for n-1 elements
        memset(temp, 0, sizeof(temp));
        snprintf(temp, sizeof(temp), "[%0.8f,%0.8f],",
            input[i].point.latitude, input[i].point.longitude);
        strncat(output, temp, sizeof(temp));
    }
    memset(temp, 0, sizeof(temp)); //assign last element
    snprintf(temp, sizeof(temp), "[%0.8f,%0.8f]]",
             input[i].point.latitude, input[i].point.longitude);
    strncat(output, temp, sizeof(temp));
}

所以函数的输出一定是格式字符串

So the output of the function must be a string of format

[[[12.12345678,12.12345678],[12.12345678,12.12345678],...]

[[12.12345678,12.12345678],[12.12345678,12.12345678],...]

但有时我会得到一个看起来像的字符串

But at times I get a string which looks like

[[12.12345678,12.12345678],[55.01[12.12345678,12.12345678],...]
[[21.28211567,84.13454083],[21.28211533,21.22[21.28211517,84.13454000],..]

[[12.12345678,12.12345678],[55.01[12.12345678,12.12345678],...]
[[21.28211567,84.13454083],[21.28211533,21.22[21.28211517,84.13454000],..]

以前,我在函数 get_gps60secString 处出现缓冲区溢出,我使用 snprintfstrncat 修复了该问题.

Previously, I had a buffer overflow at the function get_gps60secString, I fixed that by using snprintf and strncat.

注意:这是一个嵌入式应用程序,此错误每天发生一两次(共 1440 个数据包)

Note: This is an embedded application and this error occur once or twice a day (out of 1440 packets)

问题
1.会不会是snprintf/strncat进程中断导致的?
2. 这可能是由内存泄漏、覆盖堆栈或其他一些其他原因引起的分段问题引起的吗?
基本上我想了解什么可能导致损坏的字符串.

Question
1. Could this be caused by an interrupt during the snprintf/strncat process?
2. Could this be caused by a memory leak, overwriting the stack or some other segmentation issue caused else where?
Basically I would like to understand what might be causing a corrupt string.

很难找到原因并修复此错误.

Having a hard time finding the cause and fixing this bug.


我使用了 chux's 功能.以下是最小、完整且可验证的示例


I used chux's function. Below is the Minimal, Complete, and Verifiable Example

/*
 * Test code for SO question https://stackoverflow.com/questions/5216413
 * A Minimal, Complete, and Verifiable Example
 */

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <stdbool.h>
#include <signal.h>
#include <unistd.h>

typedef unsigned short UInt16;
typedef unsigned long  UInt32;

#define GPS_PERIODIC_ARRAY_SIZE  60
#define GPS_STRING_SIZE          1900

/* ---------------------- Data Structs --------------------------*/
typedef struct
{
    char Gnss60[GPS_STRING_SIZE];
} Track_json_t;

typedef struct
{
    double          latitude;
    double          longitude;
} GPSPoint_t;

typedef struct
{
    UInt16          GPS_StatusCode;
    UInt32          fixtime;
    GPSPoint_t      point;
    double          altitude;
    unsigned char GPS_Satilite_Num;
} GPS_periodic_t;

/* ----------------------- Global --------------------------------*/
FILE *fptr; //Global file pointer
int res = 0;
int g_last = 0;
GPS_periodic_t l_gps_60Sec[GPS_PERIODIC_ARRAY_SIZE];

/* ----------------------- Function defs --------------------------*/

/* At signal interrupt this function is called.
 * Flush and close the file. And safly exit the program */
void userSignalInterrupt()
{
    fflush(fptr);
    fclose(fptr);
    res = 1;
    exit(0);
}

/* @brief From the array of GPS structs we create a string of the format
 * [[lat,long],[lat,long],..]
 * @param   input   The input array of GPS structs
 * @param   output  The output string which will contain lat, long
 * @param   sz      Size left in the output buffer
 * @return  0       Successfully completed operation
 *          1       Failed / Error
 */
int get_gps60secString(GPS_periodic_t input[GPS_PERIODIC_ARRAY_SIZE], 
                       char *output, size_t sz) 
{
    int cnt = snprintf(output, sz, "[");
    if (cnt < 0 || cnt >= sz)
        return 1;
    output += cnt;
    sz -= cnt;

    int i = 0;
    for (i = 0; i < GPS_PERIODIC_ARRAY_SIZE; i++) {
        cnt = snprintf(output, sz, "[%0.8f,%0.8f]%s", 
                input[i].point.latitude, input[i].point.longitude, 
                i + 1 == GPS_PERIODIC_ARRAY_SIZE ? "" : ",");
        if (cnt < 0 || cnt >= sz)
            return 1;
        output += cnt;
        sz -= cnt;
    }

    cnt = snprintf(output, sz, "]");
    if (cnt < 0 || cnt >= sz)
        return 1;
    return 0; // no error
}

/* @brief   Create a GPS struct with data for testing. It will populate the
 * point field of GPS_periodic_t. Lat starts from 0.0 and increases by 1*10^(-8)
 * and Long will dstart at 99.99999999 and dec by 1*10^(-8)
 *
 * @param   o_gps_60sec Output array of GPS structs
 */
void getLastMinGPSdata(GPS_periodic_t *o_gps_60sec)
{
    //Fill in GPS related data here
    int i = 0;
    double latitude = o_gps_60sec[0].point.latitude;
    double longitude = o_gps_60sec[0].point.longitude;
    for (i = 0; i < 60; i++)
    {
        o_gps_60sec[i].point.latitude = latitude +  (0.00000001 * (float)g_last + 
                                        0.00000001 * (float)i);
        o_gps_60sec[i].point.longitude = longitude -  (0.00000001 * (float)g_last + 
                                        0.00000001 * (float)i);
    }
    g_last = 60;
}

/* @brief   Get the GPS data and convert it into a string
 * @param   trk Track structure with GPS string
 */
int getEvent_Track(Track_json_t *trk)
{
    getLastMinGPSdata(l_gps_60Sec);
    get_gps60secString(l_gps_60Sec, trk->Gnss60, GPS_STRING_SIZE);

    return 0;
}

int main()
{
    fptr = fopen("gpsAno.txt", "a");
    if (fptr == NULL) {
        printf("Error!!\n");
        exit(1);
    }

    //Quit at signal interrupt
    signal(SIGINT, userSignalInterrupt);

    Track_json_t trk;
    memset(&l_gps_60Sec, 0x00, sizeof(GPS_periodic_t) * GPS_PERIODIC_ARRAY_SIZE);

    //Init Points to be zero and 99.99999999
    int i = 0;
    for (i = 0; i < 60; i++) {
        l_gps_60Sec[i].point.latitude =  00.00000000;
        l_gps_60Sec[i].point.longitude = 99.99999999;
    }

    do {
        memset(&trk, 0, sizeof(Track_json_t));
        getEvent_Track(&trk);

        //Write to file
        fprintf(fptr, "%s", trk.Gnss60);
        fflush(fptr);
        sleep(1);
    } while (res == 0);

    //close and exit
    fclose(fptr);
    return  0;
}

注意:上述代码中没有重新创建错误.
因为这没有 strcat 陷阱.我在嵌入式应用程序中测试了这个功能.通过这个我能够发现 snprintf 返回一个错误并且创建的字符串最终是:

Note: Error was not recreated in the above code.
Because this doesn't have the strcat pitfalls. I tested this function in the embedded application. Through this I was able to find that the snprintf returns an error and the string created ended up to be:

[17.42401750,78.46098717],[17.42402083,53.62

[17.42401750,78.46098717],[17.42402083,53.62

它到此结束(因为 return 1).

It ended there (because of the return 1).

这是否意味着传递给 snprints 的数据已损坏?这是一个浮点值.它怎么会损坏?

Does this mean that the data which was passed to snprints corrupted? It's a float value. How can it get corrupted?

解决方案
自从我将 sprintf 函数更改为不直接处理 64 位数据的函数后,该错误就没有出现过.

Solution
The error have not been seen since I changed the sprintf function with one that doesn't directly deal with 64 bits of data.

这是函数modp_dtoa2

/** \brief convert a floating point number to char buffer with a
 *         variable-precision format, and no trailing zeros
 *
 * This is similar to "%.[0-9]f" in the printf style, except it will
 * NOT include trailing zeros after the decimal point.  This type
 * of format oddly does not exists with printf.
 *
 * If the input value is greater than 1<<31, then the output format
 * will be switched exponential format.
 *
 * \param[in] value
 * \param[out] buf  The allocated output buffer.  Should be 32 chars or more.
 * \param[in] precision  Number of digits to the right of the decimal point.
 *    Can only be 0-9.
 */
void modp_dtoa2(double value, char* str, int prec)
{
    /* if input is larger than thres_max, revert to exponential */
    const double thres_max = (double)(0x7FFFFFFF);
    int count;
    double diff = 0.0;
    char* wstr = str;
    int neg= 0;
    int whole;
    double tmp;
    uint32_t frac;

    /* Hacky test for NaN
     * under -fast-math this won't work, but then you also won't
     * have correct nan values anyways.  The alternative is
     * to link with libmath (bad) or hack IEEE double bits (bad)
     */
    if (! (value == value)) {
        str[0] = 'n'; str[1] = 'a'; str[2] = 'n'; str[3] = '\0';
        return;
    }

    if (prec < 0) {
        prec = 0;
    } else if (prec > 9) {
        /* precision of >= 10 can lead to overflow errors */
        prec = 9;
    }

    /* we'll work in positive values and deal with the
       negative sign issue later */
    if (value < 0) {
        neg = 1;
        value = -value;
    }


    whole = (int) value;
    tmp = (value - whole) * pow10[prec];
    frac = (uint32_t)(tmp);
    diff = tmp - frac;

    if (diff > 0.5) {
        ++frac;
        /* handle rollover, e.g.  case 0.99 with prec 1 is 1.0  */
        if (frac >= pow10[prec]) {
            frac = 0;
            ++whole;
        }
    } else if (diff == 0.5 && ((frac == 0) || (frac & 1))) {
        /* if halfway, round up if odd, OR
           if last digit is 0.  That last part is strange */
        ++frac;
    }

    /* for very large numbers switch back to native sprintf for exponentials.
       anyone want to write code to replace this? */
    /*
      normal printf behavior is to print EVERY whole number digit
      which can be 100s of characters overflowing your buffers == bad
    */
    if (value > thres_max) {
        sprintf(str, "%e", neg ? -value : value);
        return;
    }

    if (prec == 0) {
        diff = value - whole;
        if (diff > 0.5) {
            /* greater than 0.5, round up, e.g. 1.6 -> 2 */
            ++whole;
        } else if (diff == 0.5 && (whole & 1)) {
            /* exactly 0.5 and ODD, then round up */
            /* 1.5 -> 2, but 2.5 -> 2 */
            ++whole;
        }

        //vvvvvvvvvvvvvvvvvvv  Diff from modp_dto2
    } else if (frac) {
        count = prec;
        // now do fractional part, as an unsigned number
        // we know it is not 0 but we can have leading zeros, these
        // should be removed
        while (!(frac % 10)) {
            --count;
            frac /= 10;
        }
        //^^^^^^^^^^^^^^^^^^^  Diff from modp_dto2

        // now do fractional part, as an unsigned number
        do {
            --count;
            *wstr++ = (char)(48 + (frac % 10));
        } while (frac /= 10);
        // add extra 0s
        while (count-- > 0) *wstr++ = '0';
        // add decimal
        *wstr++ = '.';
    }

    // do whole part
    // Take care of sign
    // Conversion. Number is reversed.
    do *wstr++ = (char)(48 + (whole % 10)); while (whole /= 10);
    if (neg) {
        *wstr++ = '-';
    }
    *wstr='\0';
    strreverse(str, wstr-1);
}

推荐答案

这是我在 C 中安全字符串处理的(部分)指南.通常,我会提倡动态内存分配而不是固定长度的字符串,但在在这种情况下,我假设在可能有问题的嵌入式环境中.(尽管应该始终检查类似的假设.)

Here's (part of) my unabashedly opinionated guide on safe string handling in C. Normally, I would promote dynamic memory allocation instead of fixed-length strings, but in this case I'm assuming that in the embedded environment that might be problematic. (Although assumptions like that should always be checked.)

所以,首先:

  1. 任何在缓冲区中创建字符串的函数都必须明确告知缓冲区的长度.这是没有商量余地的.

很明显,填充缓冲区的函数不可能检查缓冲区溢出,除非它知道缓冲区在哪里结束.希望缓冲区足够长"不是一个可行的策略.如果每个人都仔细阅读文档(他们没有)并且所需的长度永远不会改变(它会),那么记录所需的缓冲区长度"就可以了.唯一剩下的就是一个额外的参数,它应该是 size_t 类型(因为这是 C 库函数中需要长度的缓冲区长度的类型).

As should be obvious, it's impossible for a function filling a buffer to check for buffer overflow unless it knows where the buffer ends. "Hope that the buffer is long enough" is not a viable strategy. "Document the needed buffer length" would be fine if everyone carefully read the documentation (they don't) and if the required length never changes (it will). The only thing that's left is an extra argument, which should be of type size_t (because that's the type of buffer lengths in the C library functions which require lengths).

忘记strncpystrncat 的存在.也忘记 strcat.他们不是你的朋友.

Forget that strncpy and strncat exist. Also forget about strcat. They are not your friends.

strncpy 专为特定用例而设计:确保初始化整个固定长度的缓冲区.它不是为普通字符串设计的,并且由于它不保证输出是以 NUL 结尾的,因此它不会产生字符串.

strncpy is designed for a specific use case: ensuring that an entire fixed-length buffer is initialised. It is not designed for normal strings, and since it doesn't guarantee that the output is NUL-terminated, it doesn't produce a string.

如果你无论如何都要用 NUL 终止自己,你最好使用 memmovememcpy 如果你知道源和目标不重叠,这应该几乎总是如此.由于您希望 memmove 在短字符串的字符串末尾停止(strncpy 不是),请测量字符串strnlen 的长度优先:strnlen 取最大长度,这正是您要移动最大字符数时所需的长度.

If you're going to NUL-terminate yourself anyway, you might as well use memmove, or memcpy if you know that the source and destination don't overlap, which should almost always be the case. Since you'll want the memmove to stop at the end of the string for short strings (which strncpy does not do), measure the string length first with strnlen: strnlen takes a maximum length, which is precisely what you want in the case that you are going move a maximum number of characters.

示例代码:

/* Safely copy src to dst where dst has capacity dstlen. */
if (dstlen) {
  /* Adjust to_move will have maximum value dstlen - 1 */
  size_t to_move = strnlen(src, dstlen - 1);
  /* copy the characters */
  memmove(dst, src, to_move);
  /* NUL-terminate the string */
  dst[to_move] = 0;
}

strncat 有一个稍微更合理的语义,但它实际上从来没有用,因为为了使用它,你已经知道你可以复制多少字节.为了知道这一点,在实践中,您需要知道输出缓冲区中剩余多少空间,并知道您需要知道复制将从输出缓冲区中的哪个位置开始.[注1].但是,如果您已经知道复制将从哪里开始,那么从头开始搜索缓冲区以找到复制点又有什么意义呢?如果您确实让 strncat 进行搜索,您有多确定您之前计算的起点是正确的?

strncat has a slightly more sensible semantic, but it's practically never useful because in order to use it, you already have to know how many bytes you could copy. In order to know that, in practice, you need to know how much space is left in your output buffer, and to know that you need to know where in the output buffer the copy will start. [Note 1]. But if you already know where the copy will start, what's the point of searching through the buffer from the beginning to find the copy point? And if you do let strncat do the search, how sure are you that your previously computed start point is correct?

在上面的代码片段中,我们已经计算了副本的长度.我们可以将其扩展为在不重新扫描的情况下进行追加:

In the above code snippet, we already computed the length of the copy. We can extend that to do an append without rescanning:

/* Safely copy src1 and then src2 to dst where dst has capacity dstlen. */
/* Assumes that src1 and src2 are not contained in dst. */
if (dstlen) {
  /* Adjust to_move will have maximum value dstlen - 1 */
  size_t to_move = strnlen(src1, dstlen - 1);
  /* Copy the characters from src1 */
  memcpy(dst, src1, to_move);
  /* Adjust the output pointer and length */
  dst += to_move;
  dstlen -= to_move;
  /* Now safely copy src2 to just after src1. */
  to_move = strnlen(src2, dstlen - 1);
  memcpy(dst, src2, to_move);
  /* NUL-terminate the string */
  dst[to_move] = 0;
}

可能是我们想要在创建字符串后dstdstlen的原始值,也可能是我们想知道我们插入了多少字节全部进入dst.在这种情况下,我们可能希望在复制之前复制这些变量,并保存移动的累积总和.

It might be that we want the original values of dst and dstlen after creating the string, and it might also be that we want to know how many bytes we inserted into dst in all. In that case, we would probably want to make copies of those variables before doing the copies, and save the cumulative sum of moves.

以上假设我们从一个空的输出缓冲区开始,但事实可能并非如此.由于我们仍然需要知道副本从哪里开始才能知道最后可以放多少个字符,因此我们仍然可以使用 memcpy;我们只需要先扫描输出缓冲区即可找到复制点.(只有在别无选择的情况下才这样做.循环执行而不是记录下一个复制点是 施莱米尔画家的算法.)

The above assumes that we're starting with an empty output buffer, but perhaps that isn't the case. Since we still need to know where the copy will start in order to know how many characters we can put at the end, we can still use memcpy; we just need to scan the output buffer first to find the copy point. (Only do this if there is no alternative. Doing it in a loop instead of recording the next copy point is Shlemiel the Painter's algorithm.)

/* Safely append src to dst where dst has capacity dstlen and starts
 * with a string of unknown length.
 */
if (dstlen) {
  /* The following code will "work" even if the existing string
   * is not correctly NUL-terminated; the code will not copy anything
   * from src, but it will put a NUL terminator at the end of the
   * output buffer.
   */
  /* Figure out where the existing string ends. */
  size_t prefixlen = strnlen(dst, dstlen - 1);
  /* Update dst and dstlen */
  dst += prefixlen;
  dstlen -= prefixlen;
  /* Proceed with the append, as above. */
  size_t to_move = strnlen(src, dstlen - 1);
  memmove(dst, src, to_move);
  dst[to_move] = 0;
}

  • 拥抱 snprintf.它真的是你的朋友.但始终检查其返回值.

  • Embrace snprintf. It really is your friend. But always check its return value.

    使用memmove,如上所述,有点笨拙.它需要您手动检查缓冲区的长度是否不为零(否则减去一个将是灾难性的,因为长度是无符号的),并且它需要您手动对输出缓冲区进行 NUL 终止,这很容易忘记,而且很多错误.它非常高效,但有时牺牲一点效率是值得的,这样您的代码更易于编写,更易于阅读和验证.

    Using memmove, as above, is slightly awkward. It requires you to manually check that the buffer's length is not zero (otherwise subtracting one would be disastrous since the length is unsigned), and it requires you to manually NUL-terminate the output buffer, which is easy to forget and the source of many bugs. It is very efficient, but sometimes it's worth sacrificing a little efficiency so that your code is easier to write and easier to read and verify.

    这将我们直接引向 snprintf.例如,您可以替换:

    And that leads us directly to snprintf. For example, you can replace:

    if (dstlen) {
      size_t to_move = strnlen(src, dstlen - 1);
      memcpy(dst, src, to_move);
      dst[to_move] = 0;
    }
    

    更简单的

    int copylen = snprintf(dst, dstlen, "%s", src);
    

    这可以做所有事情:检查 dstlen 是否为 0;只复制 src 中适合 dst 的字符,并正确地以 NUL 终止 dst(除非 dstlen 是0).并且成本极低;解析格式字符串 "%s" 只需要很少的时间,并且大多数实现都针对这种情况进行了很好的优化.[注2]

    That does everything: checks that dstlen is not 0; only copies the characters from src which can fit in dst, and correctly NUL-terminates dst (unless dstlen was 0). And the cost is minimal; it takes very little time to parse the format string "%s" and most implementations are pretty well optimised for this case. [Note 2]

    但是 snprintf 不是万能的.还有几个非常重要的警告.

    But snprintf is not a panacea. There are still a couple of really important warnings.

    首先,snprintf 的文档明确指出不允许任何输入参数与输出范围重叠.(所以它取代了 memcpy 但不是 memmove.)记住重叠包括 NUL 终止符,所以下面的代码试图将 str 反而会导致未定义行为:

    First, the documentation for snprintf makes clear that it is not permitted for any input argument to overlap the output range. (So it replaces memcpy but not memmove.) Remember that overlap includes NUL-terminators, so the following code which attempts to double the string in str instead leads to Undefined Behaviour:

    char str[BUFLEN];
    /* Put something into str */
    get_some_data(str, BUFLEN);
    
    /* DO NOT DO THIS: input overlaps output */
    int result = snprintf(str, BUFLEN, "%s%s", str, str);
    
    /* DO NOT DO THIS EITHER; IT IS STILL UB */
    size_t len = strnlen(str, cap - 1);
    int result = snprintf(str + len, cap - len, "%s", str);    
    

    第二次调用 snprintf 的问题在于,终止 str 的 NUL 恰好位于 str + len,即输出缓冲区.这是重叠的,所以是非法的.

    The problem with the second invocation of snprintf is that the NUL which terminates str is precisely at str + len, the first byte of the output buffer. That's an overlap, so it's illegal.

    关于snprintf 的第二个重要注意事项是它返回一个值,该值不能被忽略.返回的值不是由 snprintf 创建的字符串的长度.这是字符串在没有被截断以适合输出缓冲区的情况下的长度.

    The second important note about snprintf is that it returns a value, which must not be ignored. The value returned is not the length of the string created by snprintf. It's the length the string would have been had it not been truncated to fit in the output buffer.

    如果没有发生截断,那么结果就是结果的长度,必须严格小于输出缓冲区的大小(因为必须有空间给一个 NUL 终止符,也就是不视为结果长度的一部分.)您可以使用此事实来检查是否发生截断:

    If no truncation occurred, then the result is the length of the result, which must be strictly less than the size of the output buffer (because there must be room for a NUL terminator, which is not considered part of the length of the result.) You can use this fact to check whether truncation occurred:

    if (result >= dstlen) /* Output was truncated */
    

    这可以用于,例如,使用更大的动态分配缓冲区(大小为 result + 1;永远不要忘记需要 NUL-终止).

    This can be used, for example, to redo the snprintf with a larger, dynamically-allocated buffer (of size result + 1; never forget the need to NUL-terminate).

    但请记住,结果是一个 int —— 即一个有符号的值.这意味着 snprintf 无法处理很长的字符串.这在嵌入式代码中不太可能成为问题,但在可以想象字符串超过 2GB 的系统上,您可能无法安全地在 snprintf 中使用 %s 格式.这也意味着允许 snprintf 返回一个负值来表示错误.snprintf 的非常旧的实现返回 -1 以指示截断,或响应以缓冲区长度 0 调用.根据 C99(也不是 Posix 的最新版本),这不是标准行为,但您应该做好准备为了它.

    But remember that the result is an int -- that is, a signed value. That means that snprintf cannot cope with very long strings. That's not likely to be an issue in embedded code, but on systems where it's conceivable that strings exceed 2GB, you may not be able to safely use %s formats in snprintf. It also means that snprintf is allowed to return a negative value to indicate an error. Very old implementations of snprintf returned -1 to indicate truncation, or in response to being called with buffer length 0. That's not standard behaviour according to C99 (nor recent versions of Posix), but you should be prepared for it.

    snprintf 的标准兼容实现将返回一个负值,如果缓冲区长度参数太大而无法放入(有符号的)int;如果缓冲区长度正常但未截断的长度对于 int 来说太大,我不清楚预期的返回值是什么.如果您使用了导致编码错误的转换,也将返回负值;例如,一个 %lc 转换,其对应的参数包含一个不能转换为多字节(通常是 UTF-8)序列的整数.

    Standard-compliant implementations of snprintf will return a negative value if the buffer length argument is too big to fit in a (signed) int; it's not clear to me what the expected return value is if the buffer length is OK but the untruncated length is too big for an int. A negative value will also be returned if you used a conversion which resulted in an encoding error; for example, a %lc conversion whose corresponding argument contains an integer which cannot be converted to a multibyte (typically UTF-8) sequence.

    简而言之,您应该始终检查snprintf 的返回值(如果不这样做,最近的gcc/glibc 版本会产生警告),并且您应该准备好接受它.

    In short, you should always check the return value of snprintf (recent gcc/glibc versions will produce a warning if you do not), and you should be prepared for it to be negative.

    <小时>

    所以,抛开所有这些,让我们编写一个函数来生成一串坐标对:


    So, with all that behind us, let's write a function which produces a string of co-ordinate pairs:

    /* Arguments:
     *    buf      the output buffer.
     *    buflen   the capacity of buf (including room for trailing NUL).
     *    points   a vector of struct Point pairs.
     *    npoints  the number of objects in points.
     * Description:
     *    buf is overwritten with a comma-separated list of points enclosed in
     *    square brackets. Each point is output as a comma-separated pair of
     *    decimal floating point numbers enclosed in square brackets. No more
     *    than buflen - 1 characters are written. Unless buflen is 0, a NUL is
     *    written following the (possibly-truncated) output.
     * Return value:
     *    If the output buffer contains the full output, the number of characters
     *    written to the output buffer, not including the NUL terminator.
     *    If the output was truncated, (size_t)(-1) is returned.
     */
     size_t sprint_points(char* buf, size_t buflen,
                          struct Point const* points, size_t npoints)
     { 
       if (buflen == 0) return (size_t)(-1);
       size_t avail = buflen;
       char delim = '['
       while (npoints) {
         int res = snprintf(buf, avail, "%c[%f,%f]",
                            delim, points->lat, points->lon);
         if (res < 0 || res >= avail) return (size_t)(-1);
         buf += res; avail -= res;
         ++points; --npoints;
         delim = ',';
      }
      if (avail <= 1) return (size_t)(-1);
      strcpy(buf, "]");
      return buflen - (avail - 1);
    }
    

    注意事项

    1. 你经常会看到这样的代码:

    1. You will often see code like this:

    strncat(dst, src, sizeof(src)); /* NEVER EVER DO THIS! */
    

    告诉 strncat 不要从 src 附加更多的字符而不是 src 显然是没有意义的(除非 src> 未正确以 NUL 终止,在这种情况下,您会遇到更大的问题).更重要的是,它绝对没有保护您在输出缓冲区的末尾之外写入,因为您没有做任何事情来检查 dst 是否有空间容纳所有这些字符.所以它所做的就是消除关于strcat 不安全的编译器警告.由于此代码与 strcat 一样完全一样不安全,因此警告您可能会更好.

    Telling strncat not to append more characters from src than can fit in src is obviously pointless (unless src is not correctly NUL-terminated, in which case you have a bigger problem). More importantly, it does absolutely nothing to protect you from writing beyond the end of the output buffer, since you have not done anything to check that dst has room for all those characters. So about all it does is get rid of compiler warnings about the unsafety of strcat. Since this code is exactly as unsafe as strcat was, you probably would be better off with the warning.

    您甚至可能会找到一个理解 snprintf 的编译器,足以在编译时解析格式字符串,因此它的便利性是完全免费的.(如果您当前的编译器不这样做,毫无疑问,未来的版本会这样做.)对于 *printf 系列的任何使用,您应该永远尝试节约击键省略格式字符串 (snprintf(dst, dstlen, src) 而不是 snprintf(dst, dstlen, "%s", src).) 那是不安全的(如果 src 包含不重复的 %,则它具有未定义的行为).而且它慢得多,因为库函数必须解析要复制的整个字符串以查找百分号,而不是仅仅将其复制到输出中.

    You might even find a compiler which understands snprintf will enough to parse the format string at compile time, so the convenience comes at no cost at all. (And if your current compiler doesn't do this, no doubt a future version will.) As with any use of the *printf family, you should never try to economize keystrokes by leaving out the format string (snprintf(dst, dstlen, src) instead of snprintf(dst, dstlen, "%s", src).) That's unsafe (it has undefined behaviour if src contains an unduplicated %). And it's much slower because the library function has to parse the entire string to be copied looking for percent signs, instead of just copying it to the output.

    这篇关于在 snprintf 的输出中看到的杂散字符的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

  • 查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆