การสร้างสตริงที่จัดรูปแบบ C (ไม่ได้พิมพ์)


104

ฉันมีฟังก์ชันที่รับสตริงนั่นคือ:

void log_out(char *);

ในการเรียกมันฉันต้องสร้างสตริงที่จัดรูปแบบทันทีเช่น:

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

ฉันจะทำสิ่งนี้ใน ANSI C ได้อย่างไร?


เท่านั้นเนื่องจากsprintf()ส่งคืน int ซึ่งหมายความว่าฉันต้องเขียนอย่างน้อย 3 คำสั่งเช่น:

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

วิธีใดที่จะทำให้สิ่งนี้สั้นลง?


1
ฉันเชื่อว่าต้นแบบของฟังก์ชันเป็นจริง: extern void log_out (const char *, ... ); เพราะถ้าไม่เป็นเช่นนั้นการเรียกมันจะผิดพลาด (มีข้อโต้แย้งมากเกินไป) ควรใช้ตัวชี้ const เนื่องจากไม่มีเหตุผลที่ log_out () จะแก้ไขสตริง แน่นอนคุณอาจกำลังบอกว่าคุณต้องการส่งผ่านสตริงเดี่ยวไปยังฟังก์ชัน แต่ทำไม่ได้ ทางเลือกหนึ่งคือการเขียนเวอร์ชัน varargs ของฟังก์ชัน log_out ()
Jonathan Leffler

คำตอบ:


93

ใช้sprintf .

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

เขียนข้อมูลที่จัดรูปแบบเป็นสตริงเขียนสตริงที่มีข้อความเดียวกันกับที่จะพิมพ์หากใช้รูปแบบบน printf แต่แทนที่จะพิมพ์เนื้อหาจะถูกจัดเก็บเป็นสตริง C ในบัฟเฟอร์ที่ชี้โดย str

ขนาดของบัฟเฟอร์ควรใหญ่พอที่จะมีสตริงผลลัพธ์ทั้งหมด (ดู snprintf สำหรับเวอร์ชันที่ปลอดภัยกว่า)

อักขระ null ที่สิ้นสุดจะถูกต่อท้ายโดยอัตโนมัติหลังเนื้อหา

หลังจากพารามิเตอร์รูปแบบฟังก์ชันจะคาดหวังอาร์กิวเมนต์เพิ่มเติมอย่างน้อยที่สุดเท่าที่จำเป็นสำหรับรูปแบบ

พารามิเตอร์:

str

ชี้ไปยังบัฟเฟอร์ที่เก็บสตริง C ที่เป็นผลลัพธ์ บัฟเฟอร์ควรมีขนาดใหญ่พอที่จะมีสตริงผลลัพธ์

format

สตริง C ที่มีสตริงรูปแบบที่เป็นไปตามข้อกำหนดเดียวกันกับรูปแบบใน printf (ดูรายละเอียดใน printf)

... (additional arguments)

ขึ้นอยู่กับสตริงรูปแบบฟังก์ชันอาจคาดหวังลำดับของอาร์กิวเมนต์เพิ่มเติมซึ่งแต่ละตัวมีค่าที่จะใช้เพื่อแทนที่ตัวระบุรูปแบบในสตริงรูปแบบ (หรือตัวชี้ไปยังตำแหน่งที่จัดเก็บสำหรับ n) อาร์กิวเมนต์เหล่านี้ควรมีอย่างน้อยที่สุดเท่าจำนวนค่าที่ระบุในตัวระบุรูปแบบ อาร์กิวเมนต์เพิ่มเติมจะถูกละเว้นโดยฟังก์ชัน

ตัวอย่าง:

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

35
อ๊ะ! ถ้าเป็นไปได้ให้ใช้ฟังก์ชันการเปลี่ยนแปลง 'n' ได้แก่ snprintf. พวกเขาจะทำให้คุณนับขนาดบัฟเฟอร์ของคุณและด้วยเหตุนี้จึงมั่นใจได้จากการโอเวอร์รัน
dmckee --- อดีตผู้ดูแลลูกแมว

7
ใช่ แต่เขาขอฟังก์ชัน ANSI C และฉันไม่แน่ใจว่า snprintf เป็น ansi หรือแม้แต่ posix
akappa

7
อา. ฉันพลาดที่ แต่ฉันได้รับการช่วยเหลือจากมาตรฐานใหม่: ตัวแปร 'n' เป็นทางการใน C99 FWIW, YMMV ฯลฯ
dmckee --- อดีตผู้ดูแลลูกแมว

1
snprintf ไม่ใช่วิธีที่ปลอดภัยที่สุด คุณควรไปกับ snprintf_s ดูmsdn.microsoft.com/en-us/library/f30dzcf6(VS.80).aspx
joce

2
@Joce - ฟังก์ชันตระกูล C99 snprintf () ค่อนข้างปลอดภัย อย่างไรก็ตามตระกูล snprintf_s () มีพฤติกรรมที่แตกต่างกันมาก (โดยเฉพาะอย่างยิ่งเกี่ยวกับวิธีจัดการ trunction) แต่ - ฟังก์ชัน _snprintf () ของ Microsoft ไม่ปลอดภัย - เนื่องจากอาจปล่อยให้บัฟเฟอร์ผลลัพธ์ไม่สิ้นสุด (C99 snprintf () จะถูกยกเลิกเสมอ)
Michael Burr

16

หากคุณมีระบบที่รองรับ POSIX-2008 (ลินุกซ์รุ่นใหม่ ๆ ) คุณสามารถใช้asprintf()ฟังก์ชันที่ปลอดภัยและสะดวกซึ่งจะmalloc()มีหน่วยความจำเพียงพอสำหรับคุณคุณไม่จำเป็นต้องกังวลเกี่ยวกับขนาดสตริงสูงสุด ใช้แบบนี้:

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

นี่เป็นความพยายามขั้นต่ำที่คุณสามารถสร้างสตริงได้อย่างปลอดภัย sprintf()รหัสที่คุณให้ในคำถามเป็นข้อบกพร่องลึก:

  • ไม่มีหน่วยความจำที่จัดสรรไว้ด้านหลังตัวชี้ คุณกำลังเขียนสตริงไปยังตำแหน่งสุ่มในหน่วยความจำ!

  • แม้ว่าคุณจะเขียน

    char s[42];
    

    คุณจะมีปัญหามากเพราะคุณไม่รู้ว่าจะใส่หมายเลขอะไรลงในวงเล็บ

  • แม้ว่าคุณจะใช้ตัวแปร "ปลอดภัย" snprintf()แต่คุณก็ยังคงเสี่ยงอันตรายที่สตริงของคุณจะถูกตัดทอน เมื่อเขียนลงในล็อกไฟล์นั่นเป็นข้อกังวลเล็กน้อย แต่ก็มีโอกาสที่จะตัดข้อมูลที่เป็นประโยชน์ออกไปได้อย่างแม่นยำ นอกจากนี้มันจะตัดอักขระ endline ต่อท้ายโดยติดกาวบรรทัดบันทึกถัดไปที่ส่วนท้ายของบรรทัดที่เขียนไม่สำเร็จของคุณ

  • หากคุณพยายามใช้ชุดค่าผสมmalloc()และsnprintf()เพื่อสร้างพฤติกรรมที่ถูกต้องในทุกกรณีคุณจะได้รหัสมากกว่าที่ฉันให้ไว้ประมาณสองเท่าasprintf()และโดยทั่วไปจะตั้งโปรแกรมการทำงานของasprintf().


หากคุณกำลังมองหาการจัดเตรียม Wrapper log_out()ซึ่งสามารถใช้printf()รายการพารามิเตอร์สไตล์ได้เองคุณสามารถใช้ตัวแปรvasprintf()ที่ใช้va_listเป็นอาร์กิวเมนต์ได้ นี่คือการใช้งานกระดาษห่อหุ้มที่ปลอดภัยอย่างสมบูรณ์แบบ:

//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
โปรดทราบว่าasprintf()ไม่ใช่ส่วนหนึ่งของ C 2011 มาตรฐานหรือเป็นส่วนหนึ่งของ POSIX ไม่ใช่แม้แต่ POSIX 2008 หรือ 2013 เป็นส่วนหนึ่งของ TR 27431-2: ดูคุณใช้ฟังก์ชัน
Jonathan Leffler

ใช้งานได้อย่างมีเสน่ห์! ฉันประสบปัญหาเมื่อค่า "char *" ไม่ได้รับการพิมพ์อย่างถูกต้องในบันทึกกล่าวคือสตริงที่จัดรูปแบบไม่เหมาะสมอย่างใด รหัสถูกใช้ "asprintf ()"
Parasrish

11

สำหรับฉันดูเหมือนว่าคุณต้องการที่จะสามารถส่งสตริงที่สร้างขึ้นโดยใช้การจัดรูปแบบสไตล์ printf ไปยังฟังก์ชันที่คุณมีอยู่แล้วซึ่งใช้สตริงง่ายๆ คุณสามารถสร้างฟังก์ชัน wrapper โดยใช้stdarg.hสิ่งอำนวยความสะดวกและvsnprintf()(ซึ่งอาจไม่พร้อมใช้งานขึ้นอยู่กับคอมไพเลอร์ / แพลตฟอร์มของคุณ):

#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;
}

สำหรับแพลตฟอร์มที่ไม่มีการใช้งานที่ดี (หรือการนำไปใช้งานใด ๆ ) ของsnprintf()ตระกูลกิจวัตรฉันใช้โดเมนสาธารณะเกือบทั้งหมดsnprintf()จาก Holger Weissสำเร็จแล้ว


ปัจจุบันอาจพิจารณาใช้ vsnprintf_s
ควบรวม

3

หากคุณมีรหัสให้log_out()เขียนใหม่ เป็นไปได้มากที่คุณสามารถทำได้:

static FILE *logfp = ...;

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

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

หากมีข้อมูลการบันทึกเพิ่มเติมที่จำเป็นสามารถพิมพ์ก่อนหรือหลังข้อความที่แสดง ซึ่งจะช่วยประหยัดการจัดสรรหน่วยความจำและขนาดบัฟเฟอร์ที่น่าสงสัยและอื่น ๆ คุณอาจต้องเริ่มต้นlogfpเป็นศูนย์ (ตัวชี้ค่าว่าง) และตรวจสอบว่าเป็นโมฆะหรือไม่และเปิดไฟล์บันทึกตามความเหมาะสม แต่โค้ดในที่มีอยู่log_out()ควรจะจัดการกับสิ่งนั้นอยู่ดี

ข้อดีของโซลูชันนี้คือคุณสามารถเรียกมันว่ามันเป็นตัวแปรของprintf(); แน่นอนมันเป็นความแตกต่างเล็ก ๆ น้อย ๆ printf()เกี่ยวกับ

หากคุณไม่มีรหัสให้log_out()พิจารณาว่าคุณสามารถแทนที่ด้วยตัวแปรเช่นที่ระบุไว้ข้างต้นได้หรือไม่ คุณสามารถใช้ชื่อเดียวกันได้หรือไม่นั้นขึ้นอยู่กับกรอบงานแอปพลิเคชันของคุณและแหล่งที่มาสูงสุดของlog_out()ฟังก์ชันปัจจุบัน หากอยู่ในไฟล์ออบเจ็กต์เดียวกันกับฟังก์ชันที่ขาดไม่ได้อื่นคุณจะต้องใช้ชื่อใหม่ หากคุณไม่สามารถหาวิธีการจำลองได้อย่างถูกต้องคุณจะต้องใช้ตัวแปรบางตัวเช่นเดียวกับที่ให้ไว้ในคำตอบอื่น ๆ ที่จัดสรรหน่วยความจำในปริมาณที่เหมาะสม

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? */
}

เห็นได้ชัดว่าตอนนี้คุณเรียกใช้log_out_wrapper()แทนlog_out()- แต่การจัดสรรหน่วยความจำและอื่น ๆ จะทำครั้งเดียว ฉันขอสงวนสิทธิ์ในการจัดสรรพื้นที่มากเกินไปโดยหนึ่งไบต์ที่ไม่จำเป็น - ฉันไม่ได้ตรวจสอบอีกครั้งว่าความยาวที่ส่งคืนโดยvsnprintf()รวมค่าว่างที่สิ้นสุดหรือไม่



0

ฉันยังไม่ได้ทำดังนั้นฉันจะชี้ไปที่คำตอบที่ถูกต้อง

C มีข้อกำหนดสำหรับฟังก์ชันที่ใช้จำนวนตัวถูกดำเนินการที่ไม่ระบุโดยใช้<stdarg.h>ส่วนหัว คุณสามารถกำหนดฟังก์ชันของคุณเป็นvoid log_out(const char *fmt, ...);และรับva_listฟังก์ชันภายใน จากนั้นคุณสามารถจัดสรรหน่วยความจำและโทรvsprintf()ด้วยหน่วยความจำที่จัดสรรรูปแบบและva_list.

อีกวิธีหนึ่งคุณสามารถใช้สิ่งนี้เพื่อเขียนฟังก์ชันที่คล้ายคลึงกับsprintf()ที่จะจัดสรรหน่วยความจำและส่งคืนสตริงที่จัดรูปแบบโดยสร้างขึ้นมากหรือน้อยตามด้านบน มันจะเป็นการรั่วไหลของหน่วยความจำ แต่ถ้าคุณเพิ่งออกจากระบบอาจไม่สำคัญ


-2

http://www.gnu.org/software/hello/manual/libc/Variable-Arguments-Output.htmlให้ตัวอย่างต่อไปนี้เพื่อพิมพ์ไปยัง stderr คุณสามารถแก้ไขเพื่อใช้ฟังก์ชันบันทึกของคุณแทน:

 #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);
 }

แทนที่จะใช้ vfprintf คุณจะต้องใช้ vsprintf ซึ่งคุณต้องจัดเตรียมบัฟเฟอร์ที่เพียงพอเพื่อพิมพ์ลงไป

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.