Tài liệu C
Tài liệu tham khảo C ngắn gọn, đi kèm ví dụ. Đọc khái niệm, xem code, rồi luyện tập trong một hành trình Coddy.
Bắt đầu khóa họcBắt đầu
- C là gìC là một ngôn ngữ biên dịch nhỏ gọn và nhanh ra đời năm 1972, đến nay vẫn chạy hệ điều hành, cơ sở dữ liệu và gần như mọi thiết bị nhúng. Đây là C là gì, C chạy ở đâu và vì sao C vẫn đáng học trong năm 2026.
- Cài đặt C (GCC)Cài một trình biên dịch C chạy được trên mọi hệ điều hành: MinGW-w64 trên Windows, clang của Apple trên macOS và gcc trên Linux, cùng cách kiểm tra cài đặt thành công và biên dịch tệp đầu tiên.
- Biên dịch và chạyBiến một tệp .c thành chương trình chạy được: lệnh gcc, những gì tiền xử lý, biên dịch và liên kết thật sự làm, các cờ nên dùng ngay từ ngày đầu, và cách đọc thông báo lỗi khi biên dịch thất bại.
- Cú pháp CMọi chương trình C đều có cùng một bộ khung: các lệnh include, hàm main, các câu lệnh kết thúc bằng dấu chấm phẩy và các khối lệnh trong cặp ngoặc nhọn. Đây là vai trò của từng phần và những quy tắc trình biên dịch thật sự bắt buộc.
- Chú thíchC có hai kiểu chú thích, một dòng // và nhiều dòng /* */, với lịch sử khác nhau và một cái bẫy khi lồng nhau. Đây là cách dùng cả hai, cùng những gì đáng chú thích và những gì không.
Biến và kiểu dữ liệu
- BiếnCách khai báo biến trong C, khác biệt giữa khai báo và khởi tạo, quy tắc đặt tên, khai báo nhiều biến cùng lúc, và vì sao biến chưa khởi tạo là bug phổ biến nhất của người mới học.
- Kiểu dữ liệuMỗi biến trong C có một kiểu quyết định kích thước và những gì nó chứa được. Đây là tất cả các kiểu cơ bản, kích thước thực tế, signed và unsigned, sizeof, limits.h và chuyện gì xảy ra khi tràn số.
- Hằng sốBa cách đặt tên cho một giá trị không bao giờ thay đổi trong C: từ khóa const, macro #define và hằng enum. Mỗi cách thực chất là gì và khi nào nên dùng cách nào.
- Toán tửMọi toán tử C đáng biết: số học, gán, so sánh, logic và tăng giảm, cùng cái bẫy chia số nguyên, cách phép chia lấy dư hoạt động với số âm, và một bảng độ ưu tiên bạn có thể tin cậy.
- BooleanC không có kiểu boolean cho đến C99. Số 0 là sai, mọi giá trị khác là đúng, phép so sánh trả về int, và stdbool.h bổ sung bool, true, false. Kèm theo đó là lỗi nhầm = với == mà thiết kế này tạo ra.
- Ép kiểuC chuyển đổi giữa các kiểu liên tục: đôi khi vì bạn yêu cầu bằng phép ép kiểu, nhưng thường là tự nó làm. Trang này trình bày quy tắc nâng kiểu, cú pháp ép kiểu tường minh, và những phép chuyển đổi âm thầm làm mất dữ liệu.
Luồng điều khiển
- if / elseCách C ra quyết định: câu lệnh if, else, chuỗi else if, if lồng nhau và toán tử ba ngôi, cùng quy tắc đúng sai (0 là sai, mọi giá trị khác là đúng) và bug `=` với `==` lặng lẽ làm hỏng điều kiện.
- switchCách câu lệnh switch trong C chọn một nhánh từ danh sách hằng số: nhãn case, vì sao break quan trọng, fall-through cố ý và vô tình, default, switch trên char và enum, và khi nào switch tốt hơn chuỗi else-if.
- Vòng lặp forCách lặp lại code bằng vòng lặp for trong C: phần đầu ba thành phần, đếm lên và đếm xuống, duyệt mảng với mẹo sizeof, vòng lặp lồng nhau, vòng lặp vô hạn, và các bug lệch một đơn vị và unsigned mà ai cũng từng mắc.
- Vòng lặp whileCách vòng lặp while trong C lặp lại cho đến khi điều kiện thay đổi: quy tắc kiểm tra điều kiện trước, vòng lặp dùng giá trị canh (sentinel), đọc đầu vào đến EOF, while(1), và chuyển đổi qua lại giữa for và while.
- Vòng lặp do-whileVòng lặp chạy thân trước của C: do-while đảm bảo chạy một lần trước khi kiểm tra ra sao, vì sao nó kết thúc bằng dấu chấm phẩy, mẫu menu và nhập lại mà nó sinh ra để giải quyết, và nó khác vòng lặp while thường thế nào.
- break và continuebreak thoát khỏi vòng lặp hoặc switch; continue bỏ qua phần còn lại để sang lượt kế tiếp. Cách mỗi lệnh hoạt động trong for, while và do-while, cách thoát vòng lặp lồng nhau bằng biến cờ hoặc goto, và những cách dùng sai khiến vòng lặp khó theo dõi.
Hàm
- HàmCách định nghĩa và gọi hàm trong C: kiểu trả về, tham số, giá trị trả về, hàm void, vì sao chia nhỏ chương trình lại quan trọng, và một ví dụ hoàn chỉnh ghép từ những hàm nhỏ có tên rõ ràng.
- Tham số hàmC truyền mọi thứ theo giá trị: hàm nhận bản sao, không bao giờ nhận bản gốc. Điều đó có ý nghĩa gì trong thực tế, con trỏ giả lập truyền tham chiếu ra sao, vì sao mảng lại khác, và cách trả về nhiều kết quả.
- Nguyên mẫu hàmVì sao C cần thấy hình dạng của hàm trước khi bạn gọi nó: cách viết nguyên mẫu hàm, sửa lỗi implicit declaration, đặt nguyên mẫu trong tệp header, và khác biệt thật sự giữa f(void) và f().
- Đệ quyCách một hàm C gọi chính nó: điều kiện dừng giúp nó kết thúc, ví dụ giai thừa và Fibonacci từng bước, vì sao Fibonacci viết ngây thơ chậm khủng khiếp, stack overflow thật ra là gì, và khi nào vòng lặp là lựa chọn tốt hơn.
- Phạm viMột biến C được nhìn thấy ở đâu và tồn tại bao lâu: phạm vi khối, tham số hàm, biến toàn cục ở phạm vi tệp, biến cục bộ static giữ giá trị giữa các lần gọi, hàm static, che khuất tên, và vì sao biến toàn cục hay gây rắc rối.
Con trỏ
- Con trỏCon trỏ là một biến lưu địa chỉ bộ nhớ. Bài này xây dựng khái niệm từ gốc: toán tử & và *, khai báo và dereference con trỏ, vì sao kiểu con trỏ quan trọng, và hàm swap() cho thấy vì sao con trỏ tồn tại.
- Số học con trỏCộng 1 vào một con trỏ không phải là cộng 1 byte: nó chuyển tới đối tượng tiếp theo cùng kiểu. Bài này nói về ptr+1, tăng và giảm con trỏ, hiệu hai con trỏ, so sánh con trỏ, duyệt mảng bằng con trỏ và quy tắc vị trí ngay sau phần tử cuối.
- Con trỏ và mảngTrong C, tên mảng biến thành con trỏ tới phần tử đầu tiên ở gần như mọi chỗ bạn dùng nó. Bài này giải thích sự suy biến đó, vì sao arr[i] chính là *(arr+i), vì sao kích thước mảng phải truyền riêng vào hàm, và con trỏ tới mảng khác mảng con trỏ thế nào.
- Con trỏ hàmHàm cũng có địa chỉ, và con trỏ hàm lưu địa chỉ đó. Trang này giải mã cú pháp khai báo, cho thấy typedef giúp nó dễ đọc hơn thế nào, rồi đi dần tới callback, qsort với hàm so sánh tự viết và bảng điều phối.
- Con trỏ NULLNULL là địa chỉ mà một con trỏ giữ khi nó không trỏ tới đâu cả. Bài này giải thích NULL thực chất là gì, vì sao dereference nó gây crash, nó khác gì với 0 và với con trỏ chưa khởi tạo, cùng những cách viết phòng thủ giúp code tránh lỗi null.
Mảng và chuỗi
- MảngCách khai báo và khởi tạo mảng trong C, truy cập phần tử bằng chỉ số bắt đầu từ 0, tính độ dài mảng bằng sizeof, duyệt các phần tử bằng vòng lặp, và vì sao đọc vượt quá cuối mảng là undefined behavior chứ không phải một thông báo lỗi.
- Mảng nhiều chiềuCách khai báo, khởi tạo và duyệt mảng 2 chiều trong C, bố cục row-major thực sự có nghĩa gì trong bộ nhớ, vì sao truyền mảng 2 chiều vào hàm cần số cột, và một ví dụ ma trận chi tiết.
- ChuỗiC không có kiểu chuỗi: một chuỗi là mảng char kết thúc bằng byte '\0'. Trang này xây dựng cách hình dung đó, rồi trình bày chuỗi hằng, cách in, cách duyệt chuỗi và vì sao không thể sao chép chuỗi bằng =.
- Hàm xử lý chuỗiTìm hiểu <string.h> qua ví dụ thực tế: đo độ dài bằng strlen, sao chép bằng strcpy và strncpy, nối chuỗi bằng strcat, so sánh bằng strcmp, tìm kiếm bằng strchr và strstr, và cách tính kích thước buffer để không hàm nào ghi tràn.
- Chuyển đổi chuỗiChuyển đổi giữa văn bản và số trong C: vì sao atoi không báo được lỗi, cách dùng strtol và strtod đúng cách với endptr và errno, đổi số thành chuỗi bằng snprintf, và mẹo xử lý từng chữ số với '0'.
Bộ nhớ động
- Bộ nhớ độngVì sao có heap, malloc cấp cho bạn bộ nhớ có kích thước quyết định lúc chạy ra sao, mẫu p = malloc(n * sizeof *p), kiểm tra NULL, giải phóng đúng một lần, và một mảng động được làm từ đầu đến cuối.
- calloc và realloccalloc cấp cho bạn vùng nhớ đã được đặt về 0 và nhân kích thước một cách an toàn; realloc mở rộng một khối bạn đã ghi dữ liệu. Bài này nói về cả hai, cách dùng con trỏ tạm để realloc thất bại không gây rò rỉ, và khi nào calloc tốt hơn malloc cộng memset.
- Rò rỉ bộ nhớRò rỉ bộ nhớ thực chất là gì, ba cách chương trình C gây ra nó, nguyên tắc sở hữu giúp phòng tránh, và cách tìm phần còn lại bằng valgrind và -fsanitize=address. Cuối bài là một chương trình bị rò rỉ được sửa từng bước.
- Stack và heapDữ liệu của bạn thật sự nằm ở đâu: lưu trữ tự động trên stack, lưu trữ động trên heap, và lưu trữ tĩnh như vùng thứ ba, kèm bug con trỏ treo kinh điển khi trả về một biến cục bộ, và một quy tắc để lựa chọn.
Struct, union và enum
- StructCách gom các giá trị liên quan vào một kiểu bằng struct trong C: khai báo, truy cập thành viên bằng toán tử chấm, designated initializer, mảng struct, struct lồng nhau, và quy tắc sao chép khi truyền vào hàm khiến ai cũng bất ngờ.
- Struct và con trỏCách trỏ tới một struct trong C: toán tử mũi tên, vì sao (*p).x cần dấu ngoặc, truyền struct bằng con trỏ để sửa nó hoặc tránh sao chép, cấp phát struct bằng malloc và xây dựng một nút danh sách liên kết.
- typedefCách typedef đặt tên mới cho một kiểu có sẵn trong C: mẫu typedef struct có tag và không có tag, typedef cho con trỏ hàm và mảng, và trường hợp duy nhất typedef gây hại: che giấu một con trỏ.
- UnionCách union trong C lưu nhiều kiểu dữ liệu trên cùng các byte: vì sao kích thước của nó bằng thành viên lớn nhất, vì sao bạn phải đọc đúng thành viên vừa ghi, và cách tagged union (enum kết hợp union) giúp dùng union an toàn.
- enumCách định nghĩa enum trong C: đánh số tự động, giá trị tường minh, dùng enum trong switch để trình biên dịch bắt các case bị thiếu, chuyển enum thành chuỗi và các quy ước đặt tên giúp code dễ đọc.
Đầu vào và đầu ra
- printfprintf trong C thật sự hoạt động thế nào: mô hình chuỗi định dạng, các đặc tả bạn dùng hằng ngày, độ rộng và độ chính xác để căn cột, in số thực cho gọn, giá trị trả về, và vì sao printf(user_input) là một lỗ hổng bảo mật.
- scanfCách nhập dữ liệu bằng scanf trong C: vì sao bắt buộc có &, khoảng trắng được xử lý thế nào, lỗi tràn bộ đệm của %s và cách sửa bằng độ rộng, kiểm tra giá trị trả về, bug ký tự xuống dòng còn sót, và fgets như một lựa chọn chắc chắn hơn.
- Đặc tả định dạngMọ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.
- Xử lý tệpCách đọc và ghi tệp trong C: bảng chế độ của fopen, kiểm tra NULL, ghi bằng fprintf và fputc, đọc bằng fgets, fscanf và fgetc, vòng lặp đọc từng dòng chuẩn mực và khi nào dùng chế độ nhị phân.
- Đối số dòng lệnhCách một chương trình C đọc dòng lệnh của nó: cấu tạo của argc và argv, duyệt qua các đối số, argv[0] chứa gì, chuyển chuỗi thành số bằng strtol thay vì atoi, và một máy tính nhỏ kết hợp tất cả.
Bộ tiền xử lý
- Bộ tiền xử lýBộ tiền xử lý sửa văn bản mã nguồn trước khi trình biên dịch kịp nhìn thấy: dán nội dung header vào, thay thế macro, bật hoặc tắt từng đoạn code. Đây là toàn bộ họ chỉ thị và cách xem kết quả của nó.
- MacroMacro dạng đối tượng và macro dạng hàm với #define: vì sao mọi tham số và cả phần thân đều cần ngoặc, cái bẫy tính toán nhiều lần, macro nhiều dòng với do-while(0), và khi nào hàm hoặc const là công cụ tốt hơn.
- Tệp headerCách chia một chương trình C ra nhiều tệp: thứ gì thuộc về tệp .h, thứ gì thuộc về tệp .c, include guard để chặn include hai lần, biên dịch nhiều tệp cùng lúc và chia sẻ biến toàn cục bằng extern.
- Biên dịch có điều kiệnBiên dịch code khác nhau cho từng bản build bằng #ifdef, #ifndef, #if, #elif và #else: công tắc debug, nhánh theo nền tảng, định nghĩa macro từ dòng lệnh bằng -D và dùng #if 0 để vô hiệu hóa code.
Thư viện chuẩn
- Hàm toán họcMột vòng qua math.h: sqrt, pow, fabs, floor và ceil, round, fmod, nhóm hàm lượng giác và logarit, INFINITY và NAN, cùng cờ liên kết -lm, nguyên nhân của lỗi undefined reference kinh điển.
- Số ngẫu nhiênCách tạo số ngẫu nhiên trong C với rand() và RAND_MAX, vì sao chỉ khởi tạo seed bằng srand(time(NULL)) đúng một lần, đưa giá trị vào một khoảng bằng % cùng độ lệch nhỏ mà nó gây ra, số thực ngẫu nhiên và dãy số lặp lại được.
- Thư viện chuẩnMỗi header chuẩn cung cấp cho bạn những gì: stdio.h, stdlib.h, string.h, math.h, time.h, ctype.h, limits.h, stdbool.h, stdint.h và assert.h, mỗi header kèm một ví dụ ngắn chạy được.
Lỗi và gỡ lỗi
- Segmentation faultSegfault nghĩa là chương trình đã chạm vào vùng nhớ không thuộc về nó. Đây là năm nguyên nhân gây ra gần như mọi segfault, mỗi nguyên nhân kèm một ví dụ tối giản và cách sửa, cùng cách tìm đúng dòng gây lỗi bằng gdb và AddressSanitizer.
- Lỗi thường gặpNhững lỗi mà lập trình viên C nào cũng mắc ít nhất một lần: thiếu dấu chấm phẩy, = thay cho ==, khai báo ngầm định, scanf thiếu &, so sánh chuỗi bằng ==, chia số nguyên, biến chưa khởi tạo và lệch một đơn vị. Mỗi lỗi gồm triệu chứng, nguyên nhân và cách sửa.
- Undefined behaviorUndefined behavior là code mà chuẩn C không đặt ra bất kỳ yêu cầu nào, nên chuyện gì cũng có thể xảy ra, kể cả việc trình tối ưu xóa mất các bước kiểm tra của bạn. Trang này giải thích nguyên nhân, vì sao "máy mình chạy được" không chứng minh được gì, và các cờ giúp phát hiện nó.