Chương trình C đầu tiên
Sau bài này bạn sẽ làm được
- Giải thích được ý nghĩa của từng token trong chương trình Hello World
- Phân biệt được int main(void) với các biến thể sai chuẩn
- Mô tả được đường đi từ lệnh ./a.out đến khi main chạy
- Tìm được năm lỗi trong một chương trình ngắn
Chương trình Hello World chỉ có năm dòng, nhưng mỗi ký tự trong đó đều có lý do tồn tại. Bài này giải thích từng thành phần, rồi chứng minh một điều mà phần lớn người học C không biết: hàm main không phải nơi chương trình bắt đầu chạy.
#Chương trình đầu tiên
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}Hello, World!
#Mổ xẻ từng thành phần
| Thành phần | Ý nghĩa |
|---|---|
| #include | Chỉ thị tiền xử lý. Có dấu # nghĩa là việc của preprocessor, không phải câu lệnh C, nên không có dấu chấm phẩy ở cuối. |
| <stdio.h> | Header của thư viện nhập xuất chuẩn, chứa khai báo của printf. Dấu <> nghĩa là tìm trong thư mục hệ thống, dấu nháy kép nghĩa là tìm cạnh file hiện tại trước. |
| int | Kiểu trả về của main. Hệ điều hành nhận số nguyên này để biết chương trình chạy thành công hay thất bại. |
| main | Tên hàm mà hệ thống quy ước sẽ gọi để bắt đầu phần mã của bạn. |
| (void) | Nghĩa là không nhận tham số nào. Đây là điểm rất dễ nhầm, xem mục biến thể bên dưới. |
| { } | Khối lệnh. Nhóm các câu lệnh lại, đồng thời tạo ra một phạm vi mới. |
| printf | Hàm in ra màn hình theo định dạng. Nó trả về số ký tự đã in, một chi tiết ít ai để ý. |
| "Hello, World!\n" | Chuỗi ký tự. Phần \n là ký tự xuống dòng, tính là một ký tự chứ không phải hai. |
| ; | Kết thúc một câu lệnh. Thiếu nó là lỗi biên dịch ngay lập tức. |
| return 0 | Kết thúc main và trả 0 cho hệ điều hành. Theo quy ước, 0 nghĩa là thành công. |
#Giải phẫu hàm main
Mọi định nghĩa hàm trong C đều có đúng bốn phần. Nắm được cấu trúc này là bạn đọc được mọi hàm C, không riêng gì main.
#Các biến thể của main
Bạn sẽ gặp nhiều cách viết main khác nhau trên internet. Không phải cách nào cũng đúng chuẩn.
void main(void) // Sai chuẩn, dù nhiều trình biên dịch chấp nhận
int main() // Truoc C23: chua noi gi ve tham so
int main(void) // Dung
int main(int argc, char *argv[]) // Dung, khi can tham so dong lenhĐiểm khác biệt giữa main() và main(void) tinh tế nhưng quan trọng. Trước C23, ngoặc rỗng nghĩa là chưa nói gì về tham số, nên trình biên dịch không kiểm tra được nếu bạn gọi hàm với số tham số sai. Còn void nghĩa là khẳng định không có tham số nào. Từ C23, hai cách viết này đã đồng nghĩa, nhưng thói quen viết (void) vẫn đúng ở mọi phiên bản.
#main không phải điểm bắt đầu
Đây là phần thú vị nhất của bài. Khi bạn gõ ./hello, có rất nhiều việc xảy ra trước khi dòng printf đầu tiên được thực thi.
Tự kiểm chứng
Đừng tin, hãy kiểm tra. Địa chỉ điểm vào ghi trong file thực thi không trùng với địa chỉ của main:
Entry point address: 0x1040
0000000000001040 T _start 0000000000001129 T main
Chứng minh bằng mã chạy được
Chương trình dưới đây có mã chạy trước và sau main. Nếu main thật sự là điểm bắt đầu và kết thúc, điều này không thể xảy ra.
#include <stdio.h>
#include <stdlib.h>
__attribute__((constructor))
static void truoc_main(void) { puts("1. constructor"); }
__attribute__((destructor))
static void sau_main(void) { puts("4. destructor"); }
static void luc_thoat(void) { puts("3. atexit"); }
int main(void)
{
atexit(luc_thoat);
puts("2. main");
return 0;
}1. constructor 2. main 3. atexit 4. destructor
#Mã thoát
Giá trị mà main trả về đi thẳng tới hệ điều hành. Trong shell, bạn đọc nó bằng $?. Đây là nền tảng để nối các lệnh với nhau bằng &&.
Hello, World!
0
0nghĩa là thành công. Hằng sốEXIT_SUCCESStrong<stdlib.h>cũng bằng 0.- Khác 0 nghĩa là có lỗi.
EXIT_FAILUREthường bằng 1. - Từ C99, nếu
mainchạy hết mà không córeturn, trình biên dịch tự hiểu làreturn 0. Nhưng vẫn nên viết rõ ra.
#Bài tìm lỗi
Chương trình dưới đây có năm lỗi. Hãy tìm hết trước khi biên dịch, sau đó biên dịch để đối chiếu với thông báo của trình biên dịch.
#include <stdio.h>;
int Main(void)
{
printf("Hello World\n")
printf("So la: %d\n");
return 0
}Xem đáp án
- Dòng 1: thừa dấu chấm phẩy sau
#include <stdio.h>. Chỉ thị tiền xử lý không kết thúc bằng dấu chấm phẩy. - Dòng 3:
Mainviết hoa chữ M. C phân biệt chữ hoa và chữ thường, phải làmain. - Dòng 5: thiếu dấu chấm phẩy sau lời gọi
printf. - Dòng 6: có
%dtrong chuỗi định dạng nhưng không truyền đối số tương ứng. Đây là hành vi không xác định, nguy hiểm hơn hẳn bốn lỗi còn lại vì chương trình vẫn biên dịch được. - Dòng 7: thiếu dấu chấm phẩy sau
return 0.
Tự làm thử
- In tên, mã số sinh viên và lớp của bạn trên ba dòng riêng biệt.
- In một hình vuông rỗng bằng dấu sao, cạnh 5 ký tự, chỉ dùng các lời gọi
printf. - Bỏ
\nở cuối chuỗi rồi chạy lại. Quan sát dấu nhắc shell xuất hiện ở đâu và giải thích. - Thử
return 42;rồiecho $?. Sau đó thửreturn 300;và giải thích kết quả nhận được. - Chạy lại chương trình
startup.cnhưng bỏ dòngatexit. Thứ tự in ra thay đổi thế nào?
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
#includelà chỉ thị tiền xử lý, không phải câu lệnh C, nên không có dấu chấm phẩy.- Hàm gồm bốn phần: kiểu trả về, tên hàm, danh sách tham số, thân hàm.
- Hai cách viết hợp chuẩn là
int main(void)vàint main(int argc, char *argv[]).void mainsai chuẩn. - Điểm vào thật sự của chương trình là
_start, không phảimain. Kiểm chứng bằngreadelf -h. - Giá trị
maintrả về là mã thoát, đọc bằng$?, và chỉ giữ 8 bit thấp. - Lỗi nguy hiểm nhất là lỗi vẫn biên dịch được, ví dụ chuỗi định dạng thiếu đối số.