Tạo chuỗi có định dạng C (không in chúng)


100

Tôi có một hàm chấp nhận một chuỗi, đó là:

void log_out(char *);

Khi gọi nó, tôi cần tạo một chuỗi được định dạng nhanh chóng như:

int i = 1;
log_out("some text %d", i);

Làm cách nào để thực hiện việc này trong ANSI C?


Chỉ, vì sprintf()trả về một int, điều này có nghĩa là tôi phải viết ít nhất 3 lệnh, như:

char *s;
sprintf(s, "%d\t%d", ix, iy);
log_out(s);

Bất kỳ cách nào để rút ngắn điều này?


1
Tôi tin tưởng rằng nguyên mẫu hàm thực sự là: extern void log_out (const char *, ...); bởi vì nếu không, lệnh gọi đến nó là sai (quá nhiều đối số). Nó nên sử dụng một con trỏ const vì không có lý do gì để log_out () sửa đổi chuỗi. Tất nhiên, bạn có thể nói rằng bạn muốn truyền một chuỗi đơn cho hàm - nhưng không thể. Một tùy chọn sau đó là viết phiên bản varargs của hàm log_out ().
Jonathan Leffler

Câu trả lời:


90

Sử dụng sprintf .

int sprintf ( char * str, const char * format, ... );

Ghi dữ liệu đã định dạng vào chuỗi Soạn một chuỗi có cùng văn bản sẽ được in nếu định dạng được sử dụng trên printf, nhưng thay vì được in, nội dung được lưu trữ dưới dạng chuỗi C trong bộ đệm được trỏ bởi str.

Kích thước của bộ đệm phải đủ lớn để chứa toàn bộ chuỗi kết quả (xem snprintf để biết phiên bản an toàn hơn).

Một ký tự rỗng kết thúc sẽ tự động được thêm vào sau nội dung.

Sau tham số định dạng, hàm cần ít nhất bao nhiêu đối số bổ sung nếu cần cho định dạng.

Thông số:

str

Con trỏ đến bộ đệm nơi lưu trữ chuỗi C kết quả. Bộ đệm phải đủ lớn để chứa chuỗi kết quả.

format

Chuỗi C chứa một chuỗi định dạng tuân theo các thông số kỹ thuật giống như định dạng trong printf (xem printf để biết thêm chi tiết).

... (additional arguments)

Tùy thuộc vào chuỗi định dạng, hàm có thể mong đợi một chuỗi các đối số bổ sung, mỗi đối số chứa một giá trị được sử dụng để thay thế mã định dạng trong chuỗi định dạng (hoặc một con trỏ đến vị trí lưu trữ, cho n). Phải có ít nhất bao nhiêu đối số trong số này bằng số giá trị được chỉ định trong mã định dạng. Các đối số bổ sung bị hàm bỏ qua.

Thí dụ:

// Allocates storage
char *hello_world = (char*)malloc(13 * sizeof(char));
// Prints "Hello world!" on hello_world
sprintf(hello_world, "%s %s!", "Hello", "world");

35
Rất tiếc! Nếu có thể, hãy sử dụng các hàm biến thể 'n'. Tức là snprintf. Họ sẽ yêu cầu bạn đếm kích thước bộ đệm của bạn và do đó đảm bảo chống vượt quá.
dmckee --- cựu điều hành kitten

7
Có, nhưng anh ấy đã yêu cầu một hàm ANSI C và tôi không chắc liệu snprintf là ansi hay thậm chí là posix.
akappa

7
Ah. Tôi đã bỏ lỡ nó. Nhưng tôi được giải cứu bởi tiêu chuẩn mới: các biến thể 'n' là chính thức trong C99. FWIW, YMMV, v.v.
dmckee --- mèo con người điều hành cũ

1
snprintf không phải là cách an toàn nhất để đi về. Bạn nên sử dụng snprintf_s. Xem msdn.microsoft.com/en-us/library/f30dzcf6(VS.80).aspx
joce

2
@Joce - họ hàm C99 snprintf () khá an toàn; tuy nhiên họ snprintf_s () có hành vi rất khác nhau (đặc biệt là về cách xử lý trunction). NHƯNG - Hàm _snprintf () của Microsoft không an toàn - vì nó có thể khiến bộ đệm kết quả chưa kết thúc (C99 snprintf () luôn kết thúc).
Michael Burr

15

Nếu bạn có hệ thống tuân thủ POSIX-2008 (bất kỳ hệ điều hành Linux hiện đại nào), bạn có thể sử dụng asprintf()chức năng an toàn và tiện lợi : Nó sẽ malloc()đủ bộ nhớ cho bạn, bạn không cần phải lo lắng về kích thước chuỗi tối đa. Sử dụng nó như thế này:

char* string;
if(0 > asprintf(&string, "Formatting a number: %d\n", 42)) return error;
log_out(string);
free(string);

Đây là nỗ lực tối thiểu bạn có thể có được để tạo chuỗi theo cách an toàn. Các sprintf()mã bạn đã trong câu hỏi là thiếu sót sâu sắc:

  • Không có bộ nhớ được cấp phát phía sau con trỏ. Bạn đang ghi chuỗi vào một vị trí ngẫu nhiên trong bộ nhớ!

  • Ngay cả khi bạn đã viết

    char s[42];

    bạn sẽ gặp rắc rối sâu sắc, bởi vì bạn không thể biết số nào để đặt vào dấu ngoặc.

  • Ngay cả khi bạn đã sử dụng biến thể "an toàn" snprintf(), bạn vẫn sẽ gặp nguy hiểm rằng các chuỗi của bạn bị cắt ngắn. Khi ghi vào một tệp nhật ký, đó là một mối quan tâm tương đối nhỏ, nhưng nó có khả năng cắt bỏ chính xác thông tin mà lẽ ra hữu ích. Ngoài ra, nó sẽ cắt bỏ ký tự cuối dòng ở cuối, dán dòng nhật ký tiếp theo vào cuối dòng viết không thành công của bạn.

  • Nếu bạn cố gắng sử dụng kết hợp malloc()snprintf()để tạo ra hành vi chính xác trong mọi trường hợp, bạn sẽ nhận được lượng mã nhiều gấp đôi so với mức tôi đã đưa ra asprintf()và về cơ bản lập trình lại chức năng của asprintf().


Nếu bạn đang xem xét việc cung cấp một trình bao bọc log_out()có thể tự nhận printf()danh sách tham số kiểu, bạn có thể sử dụng biến thể vasprintf()lấy một va_listlàm đối số. Đây là cách triển khai hoàn toàn an toàn của trình bao bọc như vậy:

//Tell gcc that we are defining a printf-style function so that it can do type checking.
//Obviously, this should go into a header.
void log_out_wrapper(const char *format, ...) __attribute__ ((format (printf, 1, 2)));

void log_out_wrapper(const char *format, ...) {
    char* string;
    va_list args;

    va_start(args, format);
    if(0 > vasprintf(&string, format, args)) string = NULL;    //this is for logging, so failed allocation is not fatal
    va_end(args);

    if(string) {
        log_out(string);
        free(string);
    } else {
        log_out("Error while logging a message: Memory allocation failed.\n");
    }
}

2
Lưu ý rằng asprintf()đây không phải là một phần của tiêu chuẩn C 2011 cũng không phải là một phần của POSIX, thậm chí không phải là POSIX 2008 hoặc 2013. Nó không phải là một phần của TR 27431-2: xem Bạn có sử dụng các chức năng 'an toàn'
Jonathan Leffler

hoạt động như sự quyến rũ! Tôi đã gặp phải vấn đề, khi giá trị "char *" không được in đúng cách trong nhật ký, tức là chuỗi được định dạng không phù hợp bằng cách nào đó. mã đã được sử dụng, "asprintf ()".
ký sinh trùng

11

Tôi nghe có vẻ như bạn muốn có thể dễ dàng chuyển một chuỗi được tạo bằng cách sử dụng định dạng kiểu printf vào hàm mà bạn đã có với một chuỗi đơn giản. Bạn có thể tạo một hàm wrapper bằng cách sử dụng stdarg.hcác phương tiện và vsnprintf()(có thể không có sẵn, tùy thuộc vào trình biên dịch / nền tảng của bạn):

#include <stdarg.h>
#include <stdio.h>

// a function that accepts a string:

void foo( char* s);

// You'd like to call a function that takes a format string 
//  and then calls foo():

void foofmt( char* fmt, ...)
{
    char buf[100];     // this should really be sized appropriately
                       // possibly in response to a call to vsnprintf()
    va_list vl;
    va_start(vl, fmt);

    vsnprintf( buf, sizeof( buf), fmt, vl);

    va_end( vl);

    foo( buf);
}



int main()
{
    int val = 42;

    foofmt( "Some value: %d\n", val);
    return 0;
}

Đối với các nền tảng không cung cấp triển khai tốt (hoặc bất kỳ triển khai nào) của snprintf()họ các quy trình, tôi đã sử dụng thành công miền gần như công khai snprintf()từ Holger Weiss .


Hiện tại, người ta có thể cân nhắc sử dụng vsnprintf_s.
amalgamate

3

Nếu bạn có mã log_out(), hãy viết lại nó. Rất có thể, bạn có thể làm:

static FILE *logfp = ...;

void log_out(const char *fmt, ...)
{
    va_list args;

    va_start(args, fmt);
    vfprintf(logfp, fmt, args);
    va_end(args);
}

Nếu cần thêm thông tin ghi nhật ký, thông tin đó có thể được in trước hoặc sau khi thông báo hiển thị. Điều này giúp tiết kiệm phân bổ bộ nhớ và kích thước bộ đệm không rõ ràng, v.v. Bạn có thể cần khởi tạo logfpbằng 0 (con trỏ null) và kiểm tra xem nó có phải là null hay không và mở tệp nhật ký nếu thích hợp - nhưng mã trong hiện tại vẫn log_out()phải xử lý điều đó.

Ưu điểm của giải pháp này là bạn có thể đơn giản gọi nó như thể nó là một biến thể của printf(); thực sự, nó là một biến thể nhỏ trên printf().

Nếu bạn không có mã log_out(), hãy xem xét liệu bạn có thể thay thế nó bằng một biến thể chẳng hạn như biến thể được nêu ở trên hay không. Việc bạn có thể sử dụng cùng một tên hay không sẽ phụ thuộc vào khung ứng dụng của bạn và nguồn cuối cùng của log_out()hàm hiện tại . Nếu nó nằm trong cùng một tệp đối tượng với một hàm không thể thiếu khác, bạn sẽ phải sử dụng một tên mới. Nếu bạn không thể tìm ra cách sao chép chính xác, bạn sẽ phải sử dụng một số biến thể giống như những biến thể được đưa ra trong các câu trả lời khác để phân bổ một lượng bộ nhớ thích hợp.

void log_out_wrapper(const char *fmt, ...)
{
    va_list args;
    size_t  len;
    char   *space;

    va_start(args, fmt);
    len = vsnprintf(0, 0, fmt, args);
    va_end(args);
    if ((space = malloc(len + 1)) != 0)
    {
         va_start(args, fmt);
         vsnprintf(space, len+1, fmt, args);
         va_end(args);
         log_out(space);
         free(space);
    }
    /* else - what to do if memory allocation fails? */
}

Rõ ràng, bây giờ bạn gọi log_out_wrapper()thay vì log_out()- nhưng việc phân bổ bộ nhớ, v.v. được thực hiện một lần. Tôi bảo lưu quyền được phân bổ quá mức không gian bởi một byte không cần thiết - Tôi đã không kiểm tra lại xem độ dài được trả về vsnprintf()có bao gồm giá trị null kết thúc hay không.


3

Không sử dụng sprintf.
Nó sẽ làm tràn String-Buffer của bạn và làm hỏng Chương trình của bạn.
Luôn sử dụng snprintf


0

Tôi chưa làm điều này, vì vậy tôi sẽ chỉ vào câu trả lời đúng.

C có quy định cho các hàm nhận số toán hạng không xác định, sử dụng <stdarg.h>tiêu đề. Bạn có thể xác định hàm của mình là void log_out(const char *fmt, ...);và lấy va_listnội dung của hàm. Sau đó, bạn có thể cấp phát bộ nhớ và gọi vsprintf()với bộ nhớ được cấp phát, định dạng vàva_list .

Ngoài ra, bạn có thể sử dụng hàm này để viết một hàm tương tự với hàm sprintf()đó sẽ cấp phát bộ nhớ và trả về chuỗi được định dạng, tạo ra nó nhiều hơn hoặc ít hơn như trên. Nó sẽ là một rò rỉ bộ nhớ, nhưng nếu bạn chỉ đăng xuất, nó có thể không thành vấn đề.


-2

http://www.gnu.org/software/hello/manual/libc/Variable-Arguments-Output.html đưa ra ví dụ sau để in ra stderr. Bạn có thể sửa đổi nó để sử dụng chức năng nhật ký của mình thay thế:

 #include <stdio.h>
 #include <stdarg.h>

 void
 eprintf (const char *template, ...)
 {
   va_list ap;
   extern char *program_invocation_short_name;

   fprintf (stderr, "%s: ", program_invocation_short_name);
   va_start (ap, template);
   vfprintf (stderr, template, ap);
   va_end (ap);
 }

Thay vì vfprintf, bạn sẽ cần sử dụng vsprintf, nơi bạn cần cung cấp một bộ đệm thích hợp để in.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.