Menu

Đặc tả định dạng trong C: tra cứu đầy đủ printf và scanf

Mọi đặc tả định dạng của C ở một chỗ: %d, %s, %c, %f, %p và các đặc tả khác nghĩa là gì, các bổ ngữ độ dài l, ll, h và z, độ rộng, độ chính xác và cờ, cùng những đặc tả khác nhau giữa printf và scanf.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Một đặc tả định dạng (format specifier) cho printf biết cách biến một giá trị thành văn bản, và cho scanf biết cách biến văn bản thành giá trị. Đây là phần duy nhất của C mà mặc định trình biên dịch không giúp được bạn, vì hàm không nhìn thấy kiểu của các đối số, nên trang này là bảng để bạn đối chiếu.

Hình dạng tổng quát của một đặc tả là:

%[flags][width][.precision][length]conversion

Chỉ % và chữ cái chuyển đổi là bắt buộc. %-8.2lf dùng cả năm thành phần.

Bảng chuyển đổi

Đặc tảÝ nghĩaĐối số của printfĐối số của scanf
%dsố nguyên thập phân có dấuintint *
%isố nguyên có dấuint (giống %d)int * (còn nhận 0x và hệ 8 bắt đầu bằng 0)
%usố thập phân không dấuunsigned intunsigned int *
%fsố thực dạng thập phândoublefloat *
%Fnhư %f, INF/NAN viết hoadouble-
%e / %Eký hiệu khoa họcdoublefloat *
%g / %Gdạng ngắn hơn giữa %f / %edoublefloat *
%a / %Asố thực dạng thập lục phândoublefloat *
%cmột ký tựint (char được nâng kiểu)char *
%schuỗichar * (kết thúc bằng '\0')char * (bộ đệm)
%pđịa chỉ con trỏvoid *void **
%x / %Xsố thập lục phân không dấuunsigned intunsigned int *
%osố bát phân không dấuunsigned intunsigned int *
%nlưu số ký tự đã ghi đến lúc đóint *int *
%%một ký tự %không cókhớp với một ký tự %
%[...]scanset (chỉ có trong scanf)-char *

Có hai mục cần cảnh báo. %n ghi vào bộ nhớ thay vì in ra, và là cơ chế đứng sau lỗ hổng chuỗi định dạng (format-string vulnerability): đừng bao giờ để văn bản của người dùng lọt vào chuỗi định dạng. Còn %p đòi hỏi một void *; truyền thẳng một int * là không xác định dù mọi trình biên dịch thực tế vẫn in ra địa chỉ.

Bổ ngữ độ dài

Bổ ngữ độ dài (length modifier) nằm giữa độ chính xác và chữ cái chuyển đổi, và cho biết đối số thực sự lớn bao nhiêu.

Chuyển đổi có dấu (d i) và không dấu (u x o) nhận các kiểu khác nhau: %lu cần một unsigned long, không phải long, và nhầm lẫn ở đây là undefined behavior chứ không phải lỗi hiển thị nhỏ.

Bổ ngữVới d i (printf / scanf)Với u x o (printf / scanf)
hhint (từ signed char) / signed char *unsigned int (từ unsigned char) / unsigned char *
hint (từ short) / short *unsigned int (từ unsigned short) / unsigned short *
(không có)int / int *unsigned int / unsigned int *
llong / long *unsigned long / unsigned long *
lllong long / long long *unsigned long long / unsigned long long *
zkiểu kích thước có dấu / con trỏ tới nósize_t / size_t *
jintmax_t / intmax_t *uintmax_t / uintmax_t *
tptrdiff_t / ptrdiff_t *kiểu không dấu tương ứng của ptrdiff_t

Và với nhóm số thực và ký tự:

Bổ ngữĐi vớiKiểu trong printfKiểu trong scanf
(không có)f e g adoublefloat *
lf e g adouble (được chấp nhận, không có tác dụng)double *
Lf e g along doublelong double *
lc swint_t / wchar_t *wchar_t *

Những tổ hợp bạn thực sự hay gõ:

%zu là đặc tả cần thuộc lòng. sizeof, strlen và mọi kích thước của thư viện chuẩn đều trả về size_t, chiếm 8 byte trên hệ thống 64 bit trong khi int chiếm 4. printf("%d", strlen(s)) là một bug thật mà trông như vẫn chạy trên một số nền tảng, hãy dùng %zu.

Bổ ngữ hh và h tồn tại chủ yếu cho scanf, nơi kích thước đích quan trọng. Trong printf, một short được nâng lên int trước khi gọi, nên %d vẫn in đúng; %hd chỉ để thể hiện ý định.

Kiểu có độ rộng cố định

Với các kiểu trong <stdint.h> như int32_t, đặc tả có tính di động là các macro trong <inttypes.h>:

PRId32 khai triển thành đúng các chữ cái cho nền tảng của bạn, và các chuỗi literal đứng cạnh nhau được nối lại, vì thế chuỗi định dạng bị tách làm ba mảnh. Nó trông xấu; cách thay thế là ép kiểu sang long long và dùng %lld, thường dễ đọc hơn và luôn đúng.

Cờ

Cờ (flag) đứng ngay sau % và thay đổi cách trình bày. Chúng chỉ áp dụng cho printf.

CờTác dụngVí dụKết quả
-thêm khoảng đệm sau giá trị thay vì trước%-6d| với 4242 |
+luôn in dấu cho giá trị có dấu%+d với 42+42
dấu cáchin một dấu cách ở chỗ lẽ ra là +% d với 42 42
0đệm bằng số 0 thay vì dấu cách%06d với 42000042
#dạng thay thế: 0x cho %x, 0 cho %o, giữ dấu chấm thập phân cho %g%#x với 2550xff

Các cờ có thể kết hợp: %-+8.2f là độ rộng tối thiểu 8, hai chữ số thập phân, luôn có dấu, đệm sau con số. Nếu viết cả hai thì - thắng 0.

Độ rộng và độ chính xác

Độ rộng (width) là kích thước tối thiểu của trường: đầu ra được đệm cho đủ, và không bao giờ bị cắt cho vừa. Độ chính xác (precision, đứng sau dấu chấm) mang nghĩa khác nhau với từng nhóm:

Chuyển đổiĐộ chính xác nghĩa là
%f %esố chữ số sau dấu thập phân (mặc định 6)
%gtổng số chữ số có nghĩa (mặc định 6)
%ssố ký tự tối đa được in
%d %i %u %x %osố chữ số tối thiểu, đệm bằng số 0

Một dấu * thay cho một trong hai con số sẽ lấy giá trị đó từ một đối số int, được đọc trước chính giá trị cần in. Đó là cách tạo bảng có độ rộng cột tính lúc chạy.

Trong scanf, độ rộng có một nhiệm vụ khác và quan trọng hơn nhiều: nó giới hạn lượng đầu vào mà một phép chuyển đổi tiêu thụ, và đó là thứ duy nhất đứng giữa %s và lỗi tràn bộ đệm.

printf và scanf khác nhau ở đâu

Chúng trông giống nhau nhưng khác nhau ở bốn chỗ. Mỗi chỗ đều là nguồn bug thực sự:

Tình huốngprintfscanf
Giá trị double%f (float được nâng lên double)%lf, vì %f nghĩa là float *
Độ rộngkích thước trường tối thiểu, có đệmlượng đầu vào tối đa được đọc, có cắt bớt
Đối sốgiá trịcon trỏ: &x, hoặc tên mảng
Dấu cách trong chuỗi định dạngin ra một dấu cáchbỏ qua mọi chuỗi khoảng trắng

Hãy thử 2.5 hello. Chuỗi định dạng đọc nó và chuỗi định dạng in nó dùng chữ cái khác nhau cho cùng một double, và số 15 trong %15s giới hạn đầu vào ở 15 ký tự cộng ký tự kết thúc, đúng bằng kích thước của word.

Scanset của scanf

%[...] đọc một tập ký tự thay vì một từ ngăn cách bằng khoảng trắng. Đó là cách đọc cả một dòng bằng scanf, dù fgets vẫn là công cụ tốt hơn:

[^\n] nghĩa là "mọi ký tự trừ ký tự xuống dòng". %[0-9] chỉ đọc chữ số, %[abc] chỉ đọc ba chữ cái đó. Khác với %s, scanset không bỏ qua khoảng trắng ở đầu, nên một ký tự xuống dòng còn sót trong bộ đệm khiến nó không khớp được gì cả.

Dùng sai là undefined behavior

printf không thể nhìn thấy kiểu thật của đối số. Nó đọc đúng số byte mà đặc tả ngụ ý và hiểu chúng theo cách đặc tả nói. Vì vậy dùng sai không phải là trục trặc hiển thị, mà là đọc bộ nhớ như một thứ khác:

printf("%d\n", 3.5);        /* prints garbage: reads a double's bits as int */
printf("%s\n", 42);         /* treats 42 as an address - crash */
printf("%d\n", strlen(s));  /* size_t read as int - wrong on 64-bit */
printf("%f\n", 3);          /* int read as double - garbage */

Cách phòng vệ chỉ tốn một cờ. Biên dịch với -Wall và GCC sẽ đối chiếu mọi chuỗi định dạng literal với các đối số của nó:

gcc -Wall -Wextra program.c -o program

Nó báo cả bốn dòng ở trên ngay lúc biên dịch. Đây là cảnh báo đáng giá nhất trong C, và được bật sẵn trong trình soạn thảo ở trên.

Về mặt thực hành khi dùng các đặc tả này, như căn bảng thẳng hàng, in số thực dễ đọc, đọc đầu vào mà không dính lỗi xuống dòng kinh điển, xem bài printf và scanf.

Câu hỏi thường gặp

%d trong C nghĩa là gì?

%d là đặc tả chuyển đổi cho số nguyên thập phân có dấu, tức một int. Trong printf, nó in đối số dưới dạng chữ số hệ 10; trong scanf, nó đọc chữ số hệ 10 vào một int *. Trong printf, %i có cùng nghĩa, nhưng trong scanf %i còn chấp nhận số hệ 16 có 0x và số hệ 8 bắt đầu bằng số 0.

%f và %lf trong C khác nhau thế nào?

Trong printf thì thực tế không khác: đối số float được nâng lên double, nên %f in được cả hai, và C99 chấp nhận %lf như một từ đồng nghĩa. Trong scanf thì khác biệt là sống còn: %f ghi vào một float * (4 byte) còn %lf ghi vào một double * (8 byte). Dùng %f cho một double sẽ làm hỏng bộ nhớ.

In size_t hoặc long long trong C như thế nào?

%zu cho size_t (kiểu mà sizeof và strlen trả về) và %lld / %llu cho long long / unsigned long long. Dùng %d cho các kiểu này là undefined behavior: tình cờ chạy được trên hệ thống 32 bit và chạy sai trên hệ thống 64 bit, vì thế bug này tồn tại rất lâu.

Chuyện gì xảy ra nếu dùng sai đặc tả định dạng trong C?

Đó là undefined behavior, không phải sai số làm tròn. printf đọc đối số theo đúng kiểu mà đặc tả khai báo, nên printf("%d", 3.5) in ra giá trị rác còn printf("%s", 42) coi số 42 là một địa chỉ để đọc theo và thường gây sập. Hãy biên dịch với -Wall, cờ này khiến GCC đối chiếu chuỗi định dạng với các đối số.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu