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ị 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ố hoặc macro.
- Chỉ thị biên dịch có điều kiện (conditional compilation).
-
Chỉ thị khai báo thư viện
-
Cú pháp
#include <Tên_thư_viện.h> //Chỉ thị sử dụng thư viện -
Ví dụ
#include <stdio.h> //Chỉ thị sử dụng thư viện vào/ra chuẩn #include <string.h> //Chỉ thị sử dụng thư viện xử lý chuỗi #include <math.h> //Chỉ thị sử dụng thư viện toán học
-
-
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(value1, value2) ((value1) > (value2) ? (value1) : (value2)) //Định nghĩa macro MAX -
Chú ý
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
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.
-
Danh sách chỉ thị biên dịch có điều kiện
# Chỉ thị Ý nghĩa 1 #ifdef Kiểm tra một macro có được định nghĩa không. Nếu macro được định nghĩa, khối lệnh giữa #ifdef và #endif sẽ được biên dịch. 2 #ifndef Ngược với #ifdef, khối lệnh giữa #ifndef và #endif chỉ được biên dịch khi macro chưa được định nghĩa. Thường dùng làm include guard để đảm bảo mỗi header chỉ được biên dịch một lần. 3 #if Kiểm tra một biểu thức hằng số (có thể là hằng số, macro, toán tử hoặc hàm defined()). Nếu biểu thức khác 0, khối lệnh giữa #if và #endif sẽ được biên dịch; ngược lại sẽ bị bỏ qua. 4 #else Đi kèm với #if, #ifdef hoặc #ifndef. Nếu điều kiện trước đó sai, khối lệnh giữa #else và #endif sẽ được biên dịch. Đây là chỉ thị tùy chọn và chỉ có tối đa một #else trong một khối. 5 #elif Được dùng sau #if hoặc các #elif khác để kiểm tra một biểu thức hằng số khác nếu điều kiện trước đó sai. Nếu đúng, khối lệnh sau #elif sẽ được biên dịch; nếu không, tiếp tục xét các nhánh #elif hoặc #else sau đó. 6 #endif Dùng để kết thúc một khối điều kiện bắt đầu bằng #if, #ifdef hoặc #ifndef. Mọi #else và #elif (nếu có) cũng phải nằm trong khối này và được đóng lại bởi #endif. -
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.
-
Minh họa chỉ thị #ifdef
Giả sử chúng ta định nghĩa một macro DEBUG:
#define DEBUG //Định nghĩa macro DEBUGKhi đó, mã nguồn nằm giữa #ifdef DEBUG...#endif sẽ được biên dịch:
#ifdef DEBUG //Mã nguồn của chế độ DEBUG #endifHoặc viết ở dạng tương đương:
#if defined(DEBUG) // Mã nguồn của chế độ DEBUG #endif -
Minh họa chỉ thị #ifndef
#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 #endifHoặ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 -
Minh họa chỉ thị #if
Giả sử chúng ta định nghĩa macro VERSION:
#define VERSION 1 //1-Free, 2-standard, 3-ProfessionalKhi đó, biên dịch mã nguồn cho phiên bản miễn phí:
#if VERSION == 1 //Mã nguồn của phiên bản miễn phí #endif -
Minh họa chỉ thị #if...#elif / #else...#endif
#if defined(_WIN64) // Mã nguồn biên dịch cho Windows 64-bit #elif defined(_WIN32) // Mã nguồn biên dịch cho Windows 32-bit #elif defined(__linux__) // Mã nguồn biên dịch cho Linux #elif defined(__APPLE__) // Mã nguồn biên dịch cho macOS #else // Hệ điều hành không xác định #endif
-
-
Chỉ thị #error và #pragma
-
Khái niệm
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.
//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!" #endifChỉ 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 value1 First value. * @param value2 Second value. * @return The greater value between value1 and value2. */ #define MAX(value1, value2) ((value1) > (value2) ? (value1) : (value2)) /** * @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(void) { //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ụ mã nguồn cho các hàm 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.
-
-
-
Hoạt động của chương trình C
Một chương trình viết bằng C sau khi biên dịch thành công (tạo ra các tập tin *.exe trên Windows hoặc binary trên Linux/macOS) có thể được thực thi độc lập.
Hoạt động của một chương trình C có thể mô tả qua các bước tổng quát dưới đây:
-
Khởi động
- Hệ điều hành nạp chương trình vào bộ nhớ RAM.
- Mã khởi động (startup code, do trình biên dịch liên kết) được thực thi để thiết lập môi trường, sau đó gọi hàm main().
- Cấp phát stack cho các biến cục bộ, nạp các biến toàn cục và hằng số từ vùng dữ liệu tĩnh.
-
Thực thi các câu lệnh trong hàm main()
Các câu lệnh trong hàm main() sẽ được thực hiện tuần tự từ trên xuống dưới. Ngoại trừ khi gặp:
- Lời gọi hàm: Chương trình tạm thời chuyển sang thực thi hàm được gọi, thực hiện xong trở lại vị trí gọi hàm.
- Cấu trúc điều khiển (if, switch): Quyết định nhánh lệnh nào sẽ được thực thi.
- Vòng lặp (for, while, do-while): Lặp lại khối lệnh cho đến khi thỏa mãn điều kiện dừng.
- Lệnh nhảy (goto, break, continue, return): Thay đổi luồng thực thi của chương trình.
-
Thực thi lời gọi hàm
Khi một hàm được gọi (hàm thư viện hoặc hàm do người dùng định nghĩa), chương trình sẽ tạm thời chuyển sang thực thi hàm đó. Khi hàm kết thúc, chương trình tiếp tục với câu lệnh ngay sau lời gọi hàm.
-
Kết thúc
Chương trình C kết thúc khi hàm main() kết thúc. Nếu không có lệnh return, giá trị trả về mặc định là 0. Nếu có lệnh return kèm giá trị, giá trị này được trả về cho hệ điều hành dưới dạng mã trạng thái (exit code).
Theo quy ước:
- Giá trị bằng 0: Chương trình thực thi thành công.
- Giá trị khác 0: Chương trình thực thi lỗi hoặc trạng thái khác do lập trình viên quy định.
-