C là ngôn ngữ biên dịch: không có gì chạy được cho đến khi trình biên dịch (compiler) dịch mã nguồn của bạn thành mã máy cho đúng CPU và hệ điều hành của bạn. Bước đó chỉ là một lệnh, nhưng hiểu nó làm gì sẽ biến phần lớn thông báo lỗi từ bí ẩn thành hiển nhiên.
Hai lệnh
Lưu đoạn này thành hello.c:
#include <stdio.h>
int main(void) {
printf("Hello, C!\n");
return 0;
}
Sau đó, trong cùng thư mục:
gcc hello.c -o hello
./hello
Lệnh đầu tiên biên dịch. Lệnh thứ hai chạy kết quả. Trên Windows, lệnh chạy là hello trong Command Prompt hoặc .\hello trong PowerShell: không có tiền tố ./, vì Windows mặc định tìm trong thư mục hiện tại còn các shell Unix thì cố ý không làm vậy.
Nếu bỏ -o hello, gcc đặt tên đầu ra là a.out (hoặc a.exe), đó là lý do các hướng dẫn cũ kết thúc bằng ./a.out. Luôn đặt tên cho đầu ra; chỉ tốn bốn ký tự mà tránh được nhầm lẫn.
Đây là cùng chương trình đó trong trình soạn thảo trên trình duyệt, nơi cả hai bước được làm giúp bạn:
"Biên dịch" thật sự làm gì
gcc hello.c -o hello trông như một bước. Thật ra là bốn bước, và mỗi bước có thể thất bại với một loại lỗi riêng.
1. Tiền xử lý. Trước khi code C nào được biên dịch, bộ tiền xử lý xử lý mọi dòng bắt đầu bằng #. #include <stdio.h> được thay đúng nghĩa đen bằng nội dung của tệp header đó; các macro #define được mở rộng; các khối #ifdef được giữ lại hoặc xóa đi. Kết quả là một tệp C lớn không còn dòng # nào. Bạn có thể xem nó:
gcc -E hello.c
Lệnh đó in ra hàng trăm dòng, gần như toàn bộ là nội dung stdio.h được dán vào.
2. Biên dịch. Code C sau tiền xử lý được phân tích, kiểm tra kiểu và dịch sang hợp ngữ (assembly) cho CPU của bạn. Đây là nơi sinh ra lỗi cú pháp, lỗi kiểu dữ liệu và các cảnh báo.
3. Hợp dịch. Hợp ngữ trở thành một tệp object: mã máy có các chỗ trống ở những nơi gọi tới hàm của các tệp khác.
gcc -c hello.c # produces hello.o, stops before linking
4. Liên kết. Các tệp object được ghép lại với thư viện chuẩn C, mọi chỗ trống được điền bằng địa chỉ thật, và kết quả là một tệp thực thi. Đây là nơi sinh ra các lỗi "undefined reference".
Bài học thực tế: lỗi biên dịch chỉ vào một dòng trong mã nguồn của bạn, còn lỗi liên kết thì không, vì việc liên kết diễn ra sau khi mọi dòng đều đã được chấp nhận.
Các cờ nên dùng ngay từ ngày đầu
Lệnh gcc file.c -o prog trần trụi âm thầm chấp nhận rất nhiều code nguy hiểm. Bốn cờ sẽ thay đổi điều đó.
gcc -std=c17 -Wall -Wextra -g hello.c -o hello
-Wallbật các cảnh báo phổ biến. Dù tên là vậy, nó không phải "tất cả" cảnh báo, mà là bộ cảnh báo hợp lý.-Wextrathêm nhiều cảnh báo hơn, gồm tham số không dùng tới và một số lỗi so sánh.-std=c17cố định tiêu chuẩn ngôn ngữ để code của bạn có cùng ý nghĩa trên mọi máy. Dùng-std=c99nếu bạn đang theo tài liệu cũ hơn.-ggiữ lại thông tin gỡ lỗi, đểgdbhaylldbhiển thị được đúng các dòng mã nguồn khi chương trình crash.
Thêm hai cờ bạn sẽ cần về sau:
-O2bật tối ưu hóa cho bản build release. Hãy tắt nó khi đang học: code đã tối ưu khó gỡ lỗi hơn, và các cảnh báo đôi khi thay đổi.-fsanitize=address,undefined(gcc và clang) khiến chương trình dừng với một thông báo rõ ràng ngay khi nó đọc ngoài phạm vi hoặc gặp undefined behavior. Đây là cờ hữu ích nhất cho việc học C.
Tự thử các cảnh báo xem. Chương trình này biên dịch và chạy được, nhưng có hai bug thật:
count không bao giờ được gán giá trị, nên chương trình in ra bất kỳ byte nào tình cờ nằm ở đó. Với -Wall, trình biên dịch sẽ nói rõ: 'count' is used uninitialized. Cảnh báo trong C gần như không bao giờ là nhiễu: hãy coi chúng là những lỗi bạn chưa gặp phải.
Biên dịch nhiều tệp
Chương trình thật được chia ra nhiều tệp. Hãy truyền tất cả cho gcc:
gcc -std=c17 -Wall main.c utils.c -o myprog
Hoặc biên dịch từng tệp riêng rồi liên kết ở cuối, đó là cách các hệ thống build làm để khi sửa một tệp không phải build lại tất cả:
gcc -c main.c # -> main.o
gcc -c utils.c # -> utils.o
gcc main.o utils.o -o myprog
Một số thư viện cần cờ liên kết rõ ràng. Thư viện toán là thứ người mới nào cũng gặp:
gcc calc.c -o calc -lm
Không có -lm, dùng sqrt từ math.h vẫn biên dịch bình thường rồi thất bại lúc liên kết với lỗi undefined reference to sqrt: khai báo nằm trong header, nhưng phần code lại nằm trong một thư viện không ai yêu cầu.
Đọc thông báo lỗi
Lỗi của C ngắn gọn nhưng nhất quán. Ba ví dụ sau bao quát phần lớn những gì bạn sẽ gặp lúc đầu.
Thiếu dấu chấm phẩy được báo ở dòng kế tiếp, vì trình biên dịch vẫn tiếp tục đọc:
hello.c:5:5: error: expected ';' before 'return'
Cần sửa ở dòng 4, không phải dòng 5. Mỗi khi lỗi chỉ vào một dòng trông hoàn toàn đúng, hãy kiểm tra dòng ngay trước nó.
Thiếu header trông như một bí ẩn về một hàm mà bạn chắc chắn đã viết đúng tên:
hello.c:4:5: warning: implicit declaration of function 'printf'
Điều đó nghĩa là trình biên dịch chưa từng thấy khai báo của printf, tức bạn đã quên #include <stdio.h>. Từ C99 trở đi, đây là lỗi chứ không chỉ là cảnh báo.
Lỗi liên kết hoàn toàn không có số dòng:
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x1a): undefined reference to `helper'
Trình biên dịch tin lời bạn rằng helper tồn tại ở đâu đó; trình liên kết đi tìm và không thấy. Hoặc bạn chưa bao giờ viết định nghĩa, hoặc bạn viết sai tên, hoặc bạn quên truyền tệp chứa nó cho gcc.
Luôn sửa lỗi đầu tiên. Lỗi trong C kéo theo nhau: một dòng sai có thể sinh ra hai mươi thông báo, và mười chín cái biến mất khi lỗi đầu tiên được sửa.
Mã thoát
return 0 trong main không phải để trang trí. Đó là mã thoát (exit status) của chương trình, và shell đọc được nó:
Sau khi chạy, echo $? trên macOS/Linux (hoặc echo %errorlevel% trên Windows) in ra con số đó. Các script và công cụ build dùng nó để quyết định có chạy tiếp hay không. Quy ước này là tuyệt đối: 0 là thành công, còn giá trị khác 0 là mã lỗi do bạn tự chọn. stdlib.h định nghĩa EXIT_SUCCESS và EXIT_FAILURE nếu bạn thích dùng tên.
Từ C99, chạy tới cuối main mà không có return được coi như return 0, nhưng viết rõ ra thì dễ hiểu hơn, và với mọi hàm khác thì bắt buộc phải viết.
Câu hỏi thường gặp
Làm sao biên dịch và chạy một chương trình C?
Lưu code thành hello.c, rồi chạy gcc hello.c -o hello để build và ./hello để chạy (trên Windows chỉ cần hello). Nếu không tìm thấy gcc, bạn cần cài trình biên dịch trước, hoặc dùng trình biên dịch C online.
gcc -o có tác dụng gì?
-o đặt tên cho tệp đầu ra. gcc hello.c -o hello tạo ra một tệp thực thi tên hello. Không có -o, gcc ghi ra a.out (hoặc a.exe trên Windows), đó là lý do rất nhiều hướng dẫn cũ chạy ./a.out.
Nên luôn dùng những cờ gcc nào?
gcc -std=c17 -Wall -Wextra -g yourfile.c -o yourprog. -Wall -Wextra bật các cảnh báo bắt được bug thật, -std=c17 cố định phiên bản ngôn ngữ, còn -g giữ lại thông tin gỡ lỗi để trình gỡ lỗi hiển thị được mã nguồn. Thêm -O2 khi bạn cần tốc độ cho bản build release.
"undefined reference to" trong C nghĩa là gì?
Đó là lỗi của trình liên kết (linker): trình biên dịch đã chấp nhận lời gọi hàm, nhưng không tìm thấy định nghĩa của hàm đó. Nguyên nhân thường gặp là gõ sai tên, quên biên dịch tệp .c thứ hai, hoặc dùng hàm toán học mà không liên kết thư viện toán (-lm).