Cấu trúc và hoạt động của chương trình C
Trước khi bắt đầu bài học này, bạn nên ôn lại và thực hành biên dịch cũng như chạy thử chương trình C đơn giản đã được giới thiệu ở bài trước.
Ở mức cơ bản, cấu trúc của một chương trình C bao gồm các thành phần chính sau đây:
- Chỉ thị tiền xử lý (Preprocessor directive).
- Tập tin tiêu đề (Header file).
- Nguyên mẫu hàm (Function prototype).
- Biến và hằng số (Variable and constant).
- Hàm main().
- Hàm do người dùng định nghĩa (user-defined function).
- Câu lệnh và khối lệnh.
- Chú thích (Comment).
-
Chỉ thị tiền xử lý (Preprocessor directive)
Chỉ thị tiền xử lý là các câu lệnh đặc biệt trong ngôn ngữ C, được xử lý trước khi biên dịch mã nguồn. Một chỉ thị tiền xử lý bắt đầu bằng ký hiệu # và không có ký hiệu kết thúc, bao gồm một số loại cơ bản dưới đây:
- Khai báo thư viện và các tập tin tiêu đề (header files).
- Định nghĩa hằng số và macro.
- Chỉ thị biên dịch có điều kiện (conditional compilation).
-
Chỉ thị khai báo thư viện và các tập tin tiêu đề
-
Cú pháp
#include <Tên_thư_viện.h> //Chỉ thị tìm tập tin trong thư viện chuẩn #include "Tên_tập_tin.h" //Chỉ thị tìm tập tin trong thư mục hiện tại
-
Ví dụ
#include <stdio.h> //Chỉ thị sử dụng thư viện vào/ra chuẩn #include <math.h> //Chỉ thị sử dụng thư viện toán học #include "program.h" //Chỉ thị sử dụng tập tin program.h
-
-
Chỉ thị định nghĩa hằng số hoặc macro
-
Cú pháp
#define TÊN_HẰNG_SỐ Giá_trị #define TÊN_MACRO Biểu thức hoặc định nghĩa thay thế
-
Ví dụ
#define PI 3.14159 //Định nghĩa hằng số PI #define MAX(a,b) ((a) > (b) ? (a) : (b)) //Định nghĩa macro MAX
-
Chú ý
- Mặc dù không bắt buộc, nhưng theo quy ước ngầm định và để mã nguồn rõ ràng, tên hằng số và tên macro nên được viết in hoa, các từ cách nhau bởi một dấu gạch chân.
- Giữa tên và giá trị, biểu thức hoặc định nghĩa thay thế phải có ít nhất một khoảng trắng.
- Chỉ thị #define không phụ thuộc vào khối lệnh { ... } trong C, vì #define là chỉ thị tiền xử lý (preprocessor directive) chứ không phải câu lệnh của ngôn ngữ.
Một hằng số hoặc macro được định nghĩa bằng chỉ thị #define có hiệu lực kể từ dòng lệnh đó cho đến khi:
- Kết thúc tập tin nguồn(.c/.h), hoặc.
- Khi gặp chỉ thị #undef cùng tên.
-
-
Chỉ thị hủy định nghĩa đã tạo bằng #define
-
Cú pháp
#undef TÊN_HẰNG_SỐ hoặc TÊN_MACRO
-
Ví dụ
#undef PI //Hủy định nghĩa hằng số PI #undef MAX //Hủy định nghĩa macro MAX
-
-
Biên dịch có điều kiện #ifdef, #ifndef, #if, #else, #elif, #endif
Biên dịch có điều kiện (conditional compilation) là quá trình mà trình tiền xử lý quyết định đoạn mã nào sẽ được giữ lại để biên dịch và đoạn mã nào sẽ bị bỏ qua, dựa trên các macro hoặc hằng số đã được định nghĩa.
-
Công dụng của chỉ thị biên dịch có điều kiện
- Quản lý nhiều phiên bản chương trình: Cho phép bật/tắt hoặc thay đổi hành vi của chương trình tùy theo cấu hình mong muốn.
- Hỗ trợ đa nền tảng: Viết cùng một mã nguồn nhưng có thể thích ứng để chạy trên nhiều hệ điều hành hoặc kiến trúc phần cứng khác nhau.
- Gỡ lỗi và tối ưu hóa: Dễ dàng chuyển đổi giữa chế độ debug và release để phục vụ cho việc kiểm thử hoặc phát hành.
- Ngăn trùng lặp khi include: Sử dụng như một include guard để đảm bảo mỗi tập tin header chỉ được biên dịch một lần.
- Tùy chỉnh tính năng: Cho phép thêm/bỏ các chức năng cụ thể mà không cần chỉnh sửa hoặc xóa mã nguồn gốc.
-
Dạng #ifdef...#endif và #ifndef...#endif
Chỉ thị #ifdef … #endif kiểm tra một macro có được định nghĩa không; nếu macro được định nghĩa thì khối lệnh nằm giữa #ifdef và #endif sẽ được biên dịch, ngược lại khối lệnh đó sẽ bị bỏ qua.
Giả sử chúng ta định nghĩa một macro DEBUG:
#define DEBUG //Định nghĩa macro DEBUG
Khi đó, mã nguồn dành cho chế độ DEBUG sẽ được biên dịch:
#ifdef DEBUG //Mã nguồn của chế độ DEBUG #endif
Hoặc viết ở dạng tương đương:
#if defined(DEBUG) // Mã nguồn của chế độ DEBUG #endif
Chỉ thị #ifndef...#endif ngược với #ifdef...#endif; khối lệnh nằm giữa #ifndef và #endif chỉ được biên dịch khi macro chưa được định nghĩa. Chỉ thị này thường dùng làm include guard để đảm bảo mỗi tập tin header chỉ được biên dịch một lần.
#ifndef PROGRAM_H //Nếu PROGRAM_H chưa được định nghĩa #define PROGRAM_H //Định nghĩa PROGRAM_H //Nội dung tập tin header #endif
Hoặc viết ở dạng tương đương:
#if !defined(PROGRAM_H) //Nếu PROGRAM_H chưa được định nghĩa #define PROGRAM_H //Định nghĩa PROGRAM_H // Nội dung tập tin header #endif
-
Dạng #if...#endif
Chỉ thị #if … #endif kiểm tra điều kiện dựa trên hằng số hoặc macro đã định nghĩa; nếu điều kiện đúng, khối lệnh nằm giữa #if và #endif sẽ được biên dịch, ngược lại khối lệnh sẽ bị bỏ qua.
Giả sử chúng ta định nghĩa macro VERSION:
#define VERSION 1 //1-Free, 2-standard, 3-Professional
Khi đó chúng ta có thể viết mã nguồn cho từng phiên bản, ví dụ biên dịch phiên bản miễn phí:
#if VERSION == 1 //Mã nguồn của phiên bản miễn phí #endif
-
#if...#elif / #else...#endif
Chỉ thị #if...#elif...#else...#endif cho phép kiểm tra nhiều điều kiện khác nhau dựa trên hằng số hoặc macro đã định nghĩa.
#if defined(_WIN64) // Mã nguồn biên dịch cho Windows 64-bit #else #if defined(_WIN32) // Mã nguồn biên dịch cho Windows 32-bit #else #if defined(__linux__) // Mã nguồn biên dịch cho Linux #else #if defined(__APPLE__) // Mã nguồn biên dịch cho macOS #else // Hệ điều hành không xác định #endif #endif #endif #endif
Công dụng chính của biên dịch có điều kiện:
- Quản lý nhiều phiên bản chương trình: Cho phép bật/tắt hoặc thay đổi hành vi của chương trình tùy theo cấu hình mong muốn.
- Hỗ trợ đa nền tảng: Viết cùng một mã nguồn nhưng có thể thích ứng để chạy trên nhiều hệ điều hành hoặc kiến trúc phần cứng khác nhau.
- Gỡ lỗi và tối ưu hóa: Dễ dàng chuyển đổi giữa chế độ debug và release để phục vụ cho việc kiểm thử hoặc phát hành.
- Ngăn trùng lặp khi include: Sử dụng như một include guard để đảm bảo mỗi tập tin header chỉ được biên dịch một lần.
- Tùy chỉnh tính năng: Cho phép thêm/bỏ các chức năng cụ thể mà không cần chỉnh sửa hoặc xóa mã nguồn gốc.
/** * @file main_thread.c * @brief Entry point of the program. * * @project Learn C * @version 1.0 * @author hoctotlamhay.vn */ #include <stdio.h> #define FREE_VERSION //#define STANDARD_VERSION //#define PRO_VERSION int main() { //Chỉ định phiên bản cần biên dịch #if defined(FREE_VERSION) printf("This is the free version.\n"); #elif defined(STANDARD_VERSION) printf("This is the standard version.\n"); #elif defined(PRO_VERSION) printf("This is the professional version.\n"); #else printf("No version specified.\n"); #endif getchar(); return 0; }
/** * @file main_thread.c * @brief Entry point of the program. * * @project Learn C * @version 1.0 * @author hoctotlamhay.vn */ #include <stdio.h> int main() { //Chỉ định mã nguồn tùy theo hệ điều hành #if defined(_WIN64) printf("Running on Windows 64-bit.\n"); #elif defined(_WIN32) printf("Running on Windows 32-bit.\n"); #elif defined(__linux__) printf("Running on Linux.\n"); #elif defined(__APPLE__) printf("Running on macOS.\n"); #else printf("Operating system not supported.\n"); #endif getchar(); return 0; }
/** * @file main_thread.c * @brief Entry point of the program. * * @project Learn C * @version 1.0 * @author hoctotlamhay.vn */ #include <stdio.h> #define DEBUG_MODE //#define RELEASE_MODE int main() { //Chỉ định chế độ biên dịch #if defined(DEBUG_MODE) printf("Running in debug mode: Detailed logging, condition checks, assertions, etc.\n"); #elif defined(RELEASE_MODE) printf("Running in release mode: Logging disabled, performance optimized, etc.\n"); #else printf("No compilation mode specified.\n"); #endif getchar(); return 0; }
#ifndef PROGRAM_H //Nếu PROGRAM_H chưa được định nghĩa #define PROGRAM_H //Định nghĩa PROGRAM_H
/** * @file main_thread.c * @brief Entry point of the program. * * @project Learn C * @version 1.0 * @author hoctotlamhay.vn */ #include <stdio.h> #define SOUND //#define GRAPHICS //#define NETWORK int main() { //Chỉ định tính năng cần biên dịch #if defined(SOUND) printf("Sound feature is enabled.\n"); #endif #if defined(GRAPHICS) printf("Graphics feature is enabled.\n"); #endif #if defined(NETWORK) printf("Network feature is enabled.\n"); #endif #if !defined(SOUND) && !defined(GRAPHICS) && !defined(NETWORK) printf("No features are enabled.\n"); #endif getchar(); return 0; }
-
-
Chỉ thị #error và #pragma
Chỉ thị #error dùng để báo lỗi ngay khi biên dịch, thường kết hợp với biên dịch có điều kiện (#if, #elif, #else) để ngăn chặn biên dịch nếu điều kiện không thỏa mãn.
/** * @file main_thread.c * @brief Entry point of the program. * * @project Learn C * @version 1.0 * @author hoctotlamhay.vn */ #include <stdio.h> //Giả sử chương trình yêu cầu C99 trở lên #if __STDC_VERSION__ < 199901L #error "Compiler supporting the C99 standard or higher is required!" #endif int main() { printf("The program runs normally!\n"); getchar(); return 0; }
-
Chỉ thị #pragma dùng để truyền các lệnh đặc biệt tới trình biên dịch, thường là để tùy chỉnh hành vi biên dịch mà không làm thay đổi logic chương trình.
Đặc điểm chính của chỉ thị #pragma
- Mỗi trình biên dịch có thể hỗ trợ các #pragma khác nhau.
- Nếu trình biên dịch không nhận dạng #pragma nào đó, nó bỏ qua mà không báo lỗi.
Mục đích của chỉ thị #pragma
- Kiểm soát cảnh báo(#pragma warning).
- Tối ưu hóa mã nguồn (#pragma optimize).
- Quản lý include guard nhanh với #pragma once.
- Tùy chỉnh alignment, packing của struct, class.
- Các lệnh đặc biệt khác do trình biên dịch cung cấp.
-
Tập tin tiêu đề (Header file)
Trong ngôn ngữ lập trình C, tập tin tiêu đề là những tập tin có phần mở rộng .h, được sử dụng để khai báo các thành phần như hằng số, kiểu dữ liệu, macro, cấu trúc hoặc nguyên mẫu của hàm (function prototypes). Chúng đóng vai trò như một giao diện (interface), cho phép chương trình biết cách sử dụng các thành phần này mà không cần quan tâm đến phần định nghĩa chi tiết.
Đặc điểm chính của tập tin tiêu đề
- Chỉ chứa khai báo, không chứa định nghĩa chi tiết.
- Được sử dụng thông qua chỉ thị tiền xử lý #include.
- Tối ưu hóa tổ chức và quản lý mã nguồn.
- Cho phép chia sẻ thông tin giữa nhiều tập tin mã nguồn.
Ví dụ trong thư mục \include tạo tập tin đặt tên là program.h và nhập nội dung sau:
/** * @file program.h * @brief Header file for the main thread. * It declares constants, macros, and * function prototypes used in the project. * * @project Learn C * @version 1.0 * @author hoctotlamhay.vn */ #ifndef PROGRAM_H #define PROGRAM_H /** * @brief Mathematical constant for PI. */ #define PI 3.14159 /** * @brief Macro to get the maximum of two values. * * @param a First value. * @param b Second value. * @return The greater value between a and b. */ #define MAX(a, b) ((a) > (b) ? (a) : (b)) /** * @brief Calculates the circumference of a circle. * * @param radius The radius of the circle. * @return double The circumference of the circle. */ double circle_circumference(double radius); #endif //PROGRAM_H
-
Nguyên mẫu hàm (function prototype)
Trong ngôn ngữ C, nguyên mẫu hàm là một khai báo mô tả tên hàm, kiểu giá trị trả về và danh sách tham số của hàm, nhưng không chứa phần thân hàm. Nó thông báo cho trình biên dịch biết rằng một hàm với cấu trúc như vậy sẽ được định nghĩa ở một nơi khác trong chương trình.
Vai trò của nguyên mẫu hàm
- Giúp trình biên dịch kiểm tra lỗi cú pháp và kiểu dữ liệu: Trình biên dịch có thể phát hiện sự không khớp giữa lời gọi hàm và phần định nghĩa hàm.
- Cho phép gọi hàm trước khi định nghĩa: Trong C, một hàm phải được khai báo hoặc định nghĩa trước khi sử dụng. Việc dùng nguyên mẫu hàm cho phép đặt phần định nghĩa hàm ở phía sau (thường là sau main() hoặc trong một tập tin mã nguồn khác).
- Hỗ trợ tổ chức chương trình: Các nguyên mẫu thường được đặt trong tập tin tiêu đề (.h), giúp nhiều tập tin mã nguồn (.c) có thể dùng chung mà không cần lặp lại khai báo.
Cú pháp khai báo nguyên mẫu hàm
<Kiểu trả về> <Tên hàm>(<Danh sách các tham số>);
- Kiểu trả về: Kiểu dữ liệu mà hàm trả về, ví dụ int, double, void, v.v.
- Tên hàm: Tên định danh của hàm.
- Danh sách các tham số: Các tham số đầu vào, có thể có hoặc không.
Ở tập tin program.h bên trên, chúng ta đã khai báo nguyên mẫu hàm chu vi của hình tròn; Hãy khai báo thêm nguyên mẫu tính diện tích hình tròn như sau:
/** * @brief Calculates the area of a circle. * * @param radius The radius of the circle. * @return double The area of the circle. */ double circle_area(double radius);
Chi tiết về phương pháp khai báo, định nghĩa và sử dụng hàm chúng ta sẽ tìm hiểu ở phần sau của khóa học này.
-
Biến và hằng số (Variable and constant)
Biến (variable) là tên gọi do lập trình viên khai báo để tham chiếu đến một vùng nhớ trong máy tính, nơi lưu trữ dữ liệu khi chương trình thực thi. Giá trị của biến có thể thay đổi nhiều lần trong suốt vòng đời của chương trình.
Hằng số (constant) là một giá trị cố định, không thể thay đổi sau khi được khai báo. Sử dụng hằng số giúp cho chương trình ổn định và dễ bảo trì.
Trong cấu trúc tổng thể của chương trình C, vị trí khai báo biến và hằng sẽ xác định phạm vi sử dụng của chúng.
- Biến toàn cục: Là các biến được khai báo bên ngoài hàm main() hoặc các hàm khác, có phạm vi toàn chương trình. Nhờ vậy, bất kỳ hàm nào cũng có thể truy cập và sử dụng giá trị của chúng.
- Biến cục bộ: Là các biến được khai báo bên trong một hàm hoặc khối lệnh { }, chúng chỉ có hiệu lực trong phạm vi hàm hoặc khối lệnh đó và sẽ bị hủy khi hàm hoặc khối kết thúc.
- Hằng số: Có thể được khai báo bằng chỉ thị tiền xử lý #define hoặc từ khóa const, thường đặt ở đầu chương trình hoặc trong các tập tin header.
Chúng ta sẽ thảo luận chi tiết về biến và hằng số trong bài học sau.
-
Hàm main()
Hàm main() là một hàm đặc biệt trong ngôn ngữ lập trình C, được chuẩn C quy định là điểm khởi đầu duy nhất của mỗi chương trình.
Đặc điểm chính của hàm main():
- Mỗi chương trình C phải có một và chỉ một hàm main().
- Kiểu trả về của main thường là int, để thông báo mã trạng thái kết thúc cho hệ điều hành.
- Bên trong thân hàm main() chứa các câu lệnh điều khiển luồng xử lý chính của chương trình.
Hàm main() thường được khai báo theo hai dạng phổ biến sau đây:
- Hàm main() không có tham số
- Hàm main() có tham số
/** * @file main_thread.c * @brief Entry point of the program. * * @project Learn C * @version 1.0 * @author hoctotlamhay.vn */ int main() { //Các câu lệnh hoặc khối lệnh return 0; }
/** * @file main_thread.c * @brief Entry point of the program. * * @project Learn C * @version 1.0 * @author hoctotlamhay.vn */ int main(int argc, char *argv[]) { //Các câu lệnh hoặc khối lệnh return 0; }
Trong các bài học tiếp theo, chúng ta sẽ phân tích chi tiết hơn về cú pháp, tham số và cách sử dụng của hàm main().
-
Hàm do người dùng định nghĩa (User-defined function)
Trong ngôn ngữ C, ngoài hàm main() và các hàm có sẵn trong thư viện chuẩn, lập trình viên có thể tự định nghĩa các hàm riêng để giải quyết những nhiệm vụ cụ thể. Những hàm này được gọi là hàm do người dùng định nghĩa (user-defined function).
-
Đặc điểm của hàm do người dùng định nghĩa
- Được viết bởi lập trình viên để thực hiện một công việc cụ thể.
- Giúp chia nhỏ chương trình thành các phần dễ quản lý, dễ đọc và dễ bảo trì.
- Tăng khả năng tái sử dụng mã nguồn, có thể được sử dụng nhiều lần ở nhiều vị trí khác nhau.
- Có thể nhận vào tham số (input) và trả về kết quả (output).
-
Ví dụ sử dụng khai báo trong tập tin program.h
Hàm tính chu vi của hình tròn
/** * @brief Calculates the circumference of a circle. * * @param radius The radius of the circle. * @return double The circumference of the circle. */ double circle_circumference(double radius) { return 2 * PI * radius; }
Hàm tính diện tích của hình tròn
/** * @brief Calculates the area of a circle. * * @param radius The radius of the circle. * @return double The area of the circle. */ double circle_area(double radius) { return PI * radius * radius; }
-
Trong phần sau của khóa học này chúng ta sẽ tìm hiểu chi tiết hơn về phương pháp khai báo, định nghĩa và sử dụng hàm.
-
-
Câu lệnh và khối lệnh (Statement and block)
Trong ngôn ngữ C, chương trình được xây dựng từ các câu lệnh (statement). Một nhóm câu lệnh đặt trong cặp dấu ngoặc nhọn { } tạo thành khối lệnh (block). Về mặt cú pháp, khối lệnh cũng được xem như một dạng câu lệnh đặc biệt.
-
Câu lệnh
Câu lệnh là đơn vị cơ bản nhất trong chương trình C, mỗi câu lệnh mô tả một hành động cụ thể mà máy tính cần thực hiện, chẳng hạn như gán giá trị cho biến, gọi hàm, hoặc điều khiển luồng thực thi.
-
Đặc điểm của câu lệnh
- Hầu hết các câu lệnh kết thúc bằng dấu chấm phẩy (;), ngoại trừ khối lệnh và một số cấu trúc điều khiển đặc biệt (if, for, while, switch, v.v.).
- Các câu lệnh thường được thực thi theo đúng thứ tự xuất hiện trong chương trình (trừ khi bị thay đổi bởi cấu trúc điều khiển).
- Mỗi câu lệnh đảm nhận một nhiệm vụ cụ thể: Thao tác dữ liệu, gọi hàm, xử lý nhập/xuất, hoặc điều khiển luồng thực thi.
- Một câu lệnh có thể chứa biểu thức phức tạp hoặc gọi nhiều hàm lồng nhau.
-
Ví dụ
//Khai báo và khởi tạo biến int x = 10; int y = 5; int z = x + y; double radius = 10.0; //In ra giá trị ra màn hình printf("Sum(x + y) = %d\n", z); //Gọi hàm tính chu vi printf("Circumference of the circle: %f\n", circle_circumference(radius)); //Gọi hàm tính diện tích printf("Area of the circle: %f\n", circle_area(radius)); //Thông báo và chờ người dùng nhập ký tự từ bàn phím printf("Press Enter to continue..."); getchar();
-
-
Khối lệnh
Khối lệnh là một nhóm một hoặc nhiều câu lệnh được bao quanh bởi cặp dấu ngoặc nhọn { }. Khối lệnh giúp gộp các câu lệnh liên quan thành một đơn vị logic duy nhất, thường xuất hiện trong các cấu trúc điều khiển, thân hàm, hoặc khi cần giới hạn phạm vi của biến.
-
Đặc điểm của khối lệnh
- Một khối lệnh bắt đầu bằng dấu { và kết thúc bằng dấu }.
- Khối lệnh có thể chứa một hoặc nhiều câu lệnh bên trong.
- Khối lệnh được xem như một câu lệnh duy nhất trong ngữ cảnh của cấu trúc điều khiển (if, for, while, v.v.).
- Các biến khai báo bên trong khối lệnh chỉ có hiệu lực trong phạm vi của khối đó (phạm vi khối – block scope).
-
Ví dụ
Khối lệnh trong vòng lặp for.
for( ; ; ){ //Mã nguồn của vòng lặp }
Khối lệnh là thân của một hàm.
double circle_circumference(double radius) { return 2 * PI * radius; }
Chú ý: Trong C, thân của mỗi hàm (bao gồm cả hàm main) luôn được biểu diễn dưới dạng một khối lệnh.
-
-
-
Chú thích (Comment)