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 |
|---|---|---|---|
%d | số nguyên thập phân có dấu | int | int * |
%i | số nguyên có dấu | int (giống %d) | int * (còn nhận 0x và hệ 8 bắt đầu bằng 0) |
%u | số thập phân không dấu | unsigned int | unsigned int * |
%f | số thực dạng thập phân | double | float * |
%F | như %f, INF/NAN viết hoa | double | - |
%e / %E | ký hiệu khoa học | double | float * |
%g / %G | dạng ngắn hơn giữa %f / %e | double | float * |
%a / %A | số thực dạng thập lục phân | double | float * |
%c | một ký tự | int (char được nâng kiểu) | char * |
%s | chuỗi | char * (kết thúc bằng '\0') | char * (bộ đệm) |
%p | địa chỉ con trỏ | void * | void ** |
%x / %X | số thập lục phân không dấu | unsigned int | unsigned int * |
%o | số bát phân không dấu | unsigned int | unsigned int * |
%n | lư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) |
|---|---|---|
hh | int (từ signed char) / signed char * | unsigned int (từ unsigned char) / unsigned char * |
h | int (từ short) / short * | unsigned int (từ unsigned short) / unsigned short * |
| (không có) | int / int * | unsigned int / unsigned int * |
l | long / long * | unsigned long / unsigned long * |
ll | long long / long long * | unsigned long long / unsigned long long * |
z | kiểu kích thước có dấu / con trỏ tới nó | size_t / size_t * |
j | intmax_t / intmax_t * | uintmax_t / uintmax_t * |
t | ptrdiff_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ới | Kiểu trong printf | Kiểu trong scanf |
|---|---|---|---|
| (không có) | f e g a | double | float * |
l | f e g a | double (được chấp nhận, không có tác dụng) | double * |
L | f e g a | long double | long double * |
l | c s | wint_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ụng | Ví dụ | Kết quả |
|---|---|---|---|
- | thêm khoảng đệm sau giá trị thay vì trước | %-6d| với 42 | 42 | |
+ | luôn in dấu cho giá trị có dấu | %+d với 42 | +42 |
| dấu cách | in 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 42 | 000042 |
# | dạng thay thế: 0x cho %x, 0 cho %o, giữ dấu chấm thập phân cho %g | %#x với 255 | 0xff |
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 %e | số chữ số sau dấu thập phân (mặc định 6) |
%g | tổng số chữ số có nghĩa (mặc định 6) |
%s | số ký tự tối đa được in |
%d %i %u %x %o | số 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ống | printf | scanf |
|---|---|---|
Giá trị double | %f (float được nâng lên double) | %lf, vì %f nghĩa là float * |
| Độ rộng | kích thước trường tối thiểu, có đệm | lượ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ạng | in ra một dấu cách | bỏ 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ố.