Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 3.115 phút đọc

printf

Sau bài này bạn sẽ làm được

  • Giải thích giá trị trả về của printf và dùng được nó
  • Dùng đúng các escape sequence, kể cả \r cho thanh tiến trình
  • Giải thích ba chế độ đệm và vì sao stderr không bị đệm
  • Biết khi nào cần gọi fflush

printf là hàm bạn gõ nhiều nhất trong suốt khóa học. Bài này nói về ba thứ ít ai để ý: nó trả về giá trị gì, các ký tự thoát, và cơ chế đệm quyết định khi nào chữ thật sự hiện ra màn hình. Riêng phần định dạng sẽ dành cả bài sau.

#Cách dùng cơ bản

int printf(const char *format, ...);

Tham số đầu là chuỗi định dạng. Các tham số sau là giá trị cần in, số lượng tùy ý.

in.c
#include <stdio.h>

int main(void)
{
    printf("Chuỗi thường\n");

    printf("Số: %d, Thực: %.2f, Ký tự: %c, Chuỗi: %s\n",
           42, 3.14159, 'A', "xin chào");

    return 0;
}

#Giá trị trả về

printf trả về số ký tự đã in ra, hoặc một số âm nếu gặp lỗi. Rất ít người dùng giá trị này, nhưng nó có ích trong vài tình huống.

#include <stdio.h>

int main(void)
{
    int n = printf("Xin chào\n");   /* in 9 ký tự: 8 chữ và 1 xuống dòng */

    printf("Vừa in %d ký tự\n", n);

    /* Ứng dụng thật: căn cột khi không biết trước độ dài */
    int rong = printf("Tên: %s", "Nguyễn Văn A");

    for (int i = rong; i < 40; ++i) {
        putchar('.');
    }
    printf(" xong\n");

    return 0;
}

#Escape sequence

Một số ký tự không gõ thẳng vào chuỗi được, ví dụ dấu nháy kép vì nó đánh dấu hết chuỗi. Chúng được viết bằng dấu gạch chéo ngược kèm một chữ cái.

Ký hiệuÝ nghĩaGhi chú
\nXuống dòngLà MỘT ký tự, không phải hai
\tTab ngangThường nhảy tới cột chia hết cho 8
\\Dấu gạch chéo ngượcCần khi viết đường dẫn Windows
\"Dấu nháy képDùng trong chuỗi
\'Dấu nháy đơnDùng trong hằng ký tự
\0Ký tự nullĐánh dấu kết thúc chuỗi
\rVề đầu dòngKhông xuống dòng, dùng làm thanh tiến trình
\aTiếng chuôngNhiều terminal đã tắt
%%Dấu phần trămKhông phải escape, là quy tắc của printf

Ứng dụng của ký tự về đầu dòng

Ký tự \r đưa con trỏ về đầu dòng hiện tại mà không xuống dòng mới. Kết hợp với fflush, nó tạo ra thanh tiến trình cập nhật tại chỗ.

tiendo.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    for (int i = 0; i <= 100; i += 5) {
        printf("\rTiến độ: [%-20.*s] %3d%%",
               i / 5, "####################", i);
        fflush(stdout);          /* bắt buộc, vì chưa có ký tự xuống dòng */
        usleep(100000);
    }
    printf("\nXong\n");

    return 0;
}

#Ba luồng chuẩn

Mọi chương trình C khi khởi động đều được cấp sẵn ba luồng. Bạn không cần mở chúng.

LuồngSố hiệuDùng để
stdin0Đọc dữ liệu vào, mặc định là bàn phím
stdout1In kết quả bình thường, mặc định là màn hình
stderr2In thông báo lỗi và chẩn đoán

printf("...") chính là cách viết tắt của fprintf(stdout, "...").

/* Mẫu đúng khi báo lỗi */
if (!f) {
    fprintf(stderr, "Không mở được file %s\n", ten_file);

    return 1;
}

#Cơ chế đệm

Ghi ra màn hình là một lời gọi hệ thống, mà lời gọi hệ thống thì tốn thời gian. Nếu mỗi printf đều gọi thẳng xuống nhân hệ điều hành, chương trình sẽ chậm hàng chục lần. Vì vậy thư viện chuẩn gom chữ vào một bộ đệm trước.

printf ghi vào bộ đệm rồi mới ra màn hình, còn fprintf ra stderr thì đi thẳng.
Chế độ đệmÁp dụng choXả khi nào
Đệm theo dòngstdout khi ra terminalGặp ký tự xuống dòng, hoặc đầy, hoặc gọi fflush
Đệm đầystdout khi ra file hoặc pipeĐầy bộ đệm, thường 4 KB, hoặc gọi fflush
Không đệmstderrNgay lập tức

Thí nghiệm chứng minh

dem.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    printf("A không xuống dòng");
    fprintf(stderr, "B là lỗi\n");
    sleep(2);
    printf(" C\n");

    return 0;
}
terminal
# Ra terminal: B hiện ngay, A và C hiện cùng lúc sau 2 giây
./dem
# Ra pipe: B ra trước, A và C ra khi chương trình kết thúc
./dem | cat
# Xem thứ tự thật trong file
./dem > ketqua.txt 2>&1 && cat ketqua.txt

Ba cách kiểm soát

fflush(stdout);                        /* xả ngay một lần */

setvbuf(stdout, NULL, _IONBF, 0);      /* tắt đệm hoàn toàn, chậm nhưng an toàn */

setvbuf(stdout, NULL, _IOLBF, 0);      /* ép đệm theo dòng kể cả khi ra file */

Tự làm thử

  1. In số 255 ở bốn hệ: mười, tám, mười sáu chữ thường, mười sáu chữ hoa. Có tiền tố đầy đủ.
  2. Dùng giá trị trả về của printf để in một bảng hai cột tự căn, không dùng độ rộng cố định.
  3. Viết thanh tiến trình chạy từ 0 tới 100 phần trăm. Thử bỏ fflush và quan sát khác biệt.
  4. Chạy chương trình dem.c theo cả ba cách ở trên, ghi lại thứ tự chữ hiện ra và giải thích.
  5. Viết chương trình in ra rồi cố tình gây lỗi truy cập bộ nhớ. So sánh log nhận được khi in bằng printf và khi in bằng fprintf(stderr, ...).

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • printf trả về số ký tự đã in, hoặc số âm nếu lỗi.
  • \n là một ký tự, và \r đưa con trỏ về đầu dòng mà không xuống dòng.
  • stdout đệm theo dòng khi ra terminal, đệm đầy khi ra file, còn stderr không đệm.
  • Thông báo lỗi luôn ghi ra stderr để không bị mất và tách được khỏi dữ liệu.
  • Chuỗi không có \n thì phải gọi fflush(stdout) nếu muốn thấy ngay.