This C programming guide is currently only available in Vietnamese. Please toggle the language switch (🇻🇳) in the top navigation to read the full article.

In this eleventh part of the series, we cover multi-file project organization in C, header files, header guards, the extern and static keywords for linkage control, the C preprocessor in depth (macros, conditional compilation, stringification, token pasting), and build tools like Makefile and CMake.

Khi dự án C phát triển lớn hơn vài trăm dòng code, việc nhồi nhét tất cả vào một file duy nhất trở nên bất khả thi. Trong bài học này, chúng ta sẽ học cách tổ chức mã nguồn chuyên nghiệp với nhiều file, làm chủ bộ tiền xử lý (Preprocessor) mạnh mẽ của C, và sử dụng các công cụ build tự động.

1. Tại sao cần chia mã nguồn thành nhiều file?

Trong thực tế, các dự án phần mềm thường có hàng nghìn đến hàng triệu dòng code. Viết tất cả vào một file duy nhất sẽ gặp phải nhiều vấn đề nghiêm trọng:

  • Khó bảo trì: File quá dài khiến việc đọc, tìm kiếm và sửa lỗi trở nên cực kỳ khó khăn.
  • Không thể làm việc nhóm: Nhiều lập trình viên không thể cùng chỉnh sửa một file mà không xung đột (merge conflicts).
  • Biên dịch chậm: Mỗi lần thay đổi nhỏ, toàn bộ file phải được biên dịch lại từ đầu.

Giải pháp là tách mã nguồn thành các module theo nguyên tắc Separation of Concerns (Phân tách mối quan tâm):

  • File tiêu đề (.h) - Interface: Khai báo "giao diện" công khai: struct, typedef, nguyên mẫu hàm, macro, hằng số.
  • File mã nguồn (.c) - Implementation: Định nghĩa chi tiết các hàm, chứa các hàm nội bộ (static).

Lợi ích lớn nhất: biên dịch gia tăng (Incremental Compilation) - chỉ cần biên dịch lại file nào bị thay đổi, tiết kiệm rất nhiều thời gian cho dự án lớn.

2. Header Files & Source Files

Một module trong C thường gồm một cặp file: .h.c cùng tên.

Nội dung của file Header (.h)

  • Định nghĩa structtypedef
  • Nguyên mẫu hàm (Function prototypes)
  • Macro và hằng số (#define)
  • Khai báo kiểu dữ liệu (enum, union)

Nội dung của file Source (.c)

  • Định nghĩa (implementation) các hàm đã khai báo trong .h
  • Các hàm nội bộ đánh dấu static (không công khai ra ngoài)
  • Biến toàn cục nội bộ (static ở file scope)

Hai cách #include

include_demo.c
// Thu vien he thong - tim trong duong dan he thong (system include path)
#include <stdio.h>
#include <stdlib.h>

// File header cua du an - tim trong thu muc hien tai truoc
#include "student.h"
#include "utils/math_helper.h"

3. Header Guards & #pragma once

Khi nhiều file .c cùng #include một file header, hoặc các header include lẫn nhau (circular includes), trình biên dịch sẽ gặp lỗi định nghĩa trùng lặp (Redefinition error). Header Guards giải quyết vấn đề này.

Cách truyền thống: #ifndef / #define / #endif

student.h
#ifndef STUDENT_H    // Neu STUDENT_H chua duoc dinh nghia...
#define STUDENT_H    // ...thi dinh nghia no ngay bay gio

#include <stdio.h>

typedef struct {
    char name[50];
    int age;
    float gpa;
} Student;

// Nguyen mau ham (function prototypes)
void printStudent(Student s);
Student createStudent(const char* name, int age, float gpa);

#endif // STUDENT_H  // Ket thuc khoi bao ve

Cách hiện đại: #pragma once

#pragma once là chỉ thị không chuẩn (non-standard) nhưng được hầu hết trình biên dịch hiện đại hỗ trợ (GCC, Clang, MSVC). Ưu điểm: ngắn gọn, không lo đặt tên trùng macro.

student.h (pragma once)
#pragma once

#include <stdio.h>

typedef struct {
    char name[50];
    int age;
    float gpa;
} Student;

void printStudent(Student s);
Student createStudent(const char* name, int age, float gpa);

Ví dụ hoàn chỉnh: student.h + student.c + main.c

student.c
#include "student.h"
#include <string.h>

// Ham noi bo - chi dung trong file nay
static void capitalize(char* str) {
    if (str[0] >= 'a' && str[0] <= 'z') {
        str[0] -= 32;
    }
}

Student createStudent(const char* name, int age, float gpa) {
    Student s;
    strncpy(s.name, name, 49);
    s.name[49] = '\0';
    capitalize(s.name);
    s.age = age;
    s.gpa = gpa;
    return s;
}

void printStudent(Student s) {
    printf("Ten: %s | Tuoi: %d | GPA: %.2f\n", s.name, s.age, s.gpa);
}
main.c
#include <stdio.h>
#include "student.h"

int main() {
    Student s = createStudent("nguyen van a", 20, 8.5);
    printStudent(s);
    return 0;
}

4. Phạm vi liên kết: extern, static & Compilation Units

Từ khóa extern: Chia sẻ biến/hàm giữa các file

extern khai báo rằng một biến hoặc hàm được định nghĩa ở file khác. Nó không cấp phát bộ nhớ mới, chỉ "hứa" với trình biên dịch rằng biến đó tồn tại ở đâu đó.

config.c
// Dinh nghia bien toan cuc (cap phat bo nho o day)
int max_connections = 100;
const char* app_name = "MyApp";
server.c
#include <stdio.h>

// Khai bao extern - su dung bien tu config.c
extern int max_connections;
extern const char* app_name;

void startServer() {
    printf("Starting %s with max %d connections\n", app_name, max_connections);
}

Từ khóa static ở phạm vi file: Internal Linkage

Khi đặt static trước biến toàn cục hoặc hàm, phạm vi của chúng bị giới hạn chỉ trong file nguồn hiện tại. Các file khác không thể truy cập, giúp đóng gói (encapsulation) an toàn.

logger.c
static int log_count = 0;  // Chi file nay moi thay

static void writeToFile(const char* msg) {  // Ham noi bo
    // ... ghi vao file log
}

// Ham cong khai (external linkage) - file khac co the goi
void logMessage(const char* msg) {
    log_count++;
    writeToFile(msg);
}

Translation Unit & Lỗi Linker thường gặp

Một Translation Unit (đơn vị biên dịch) là một file .c sau khi đã xử lý tất cả #include và macro. Mỗi translation unit được biên dịch độc lập thành file object (.o), sau đó Linker ghép chúng lại.

Lỗi Linker thường gặp:

  • undefined reference to 'funcName': Hàm được khai báo (có prototype) nhưng không có file nào định nghĩa (implement) nó, hoặc bạn quên truyền file .c chứa hàm đó khi biên dịch.
  • multiple definition of 'varName': Biến toàn cục được định nghĩa (không phải extern) ở nhiều file. Giải pháp: dùng extern ở header và chỉ định nghĩa ở một file .c duy nhất.

5. Bộ tiền xử lý C (C Preprocessor) chuyên sâu

C Preprocessor chạy trước trình biên dịch, xử lý các chỉ thị bắt đầu bằng #. Nó thực hiện thay thế văn bản thuần túy (text substitution), không hiểu ngữ nghĩa C.

Macro dạng đối tượng (Object-like macros)

macros.c
#define PI          3.14159265358979
#define MAX_SIZE    1024
#define APP_VERSION "2.1.0"

// Su dung
double area = PI * r * r;
char buffer[MAX_SIZE];

Macro dạng hàm (Function-like macros)

function_macros.c
// LUON LUON dat ngoac quanh tham so va toan bo bieu thuc!
#define MAX(a, b)       ((a) > (b) ? (a) : (b))
#define SQUARE(x)       ((x) * (x))
#define ABS(x)          ((x) < 0 ? -(x) : (x))

// Vi du ve BUG neu khong dung ngoac:
// #define BAD_SQUARE(x)  x * x
// BAD_SQUARE(2 + 3) => 2 + 3 * 2 + 3 = 11 (sai! ky vong la 25)
// SQUARE(2 + 3)     => ((2 + 3) * (2 + 3)) = 25 (dung!)

// Canh bao: Macro co side effects!
// int a = 5;
// SQUARE(a++) => ((a++) * (a++)) => a tang 2 lan! (undefined behavior)

#undef - Huy dinh nghia macro

undef_demo.c
#define BUFFER_SIZE 256
// ... su dung BUFFER_SIZE ...

#undef BUFFER_SIZE          // Huy dinh nghia
#define BUFFER_SIZE 1024    // Dinh nghia lai voi gia tri moi

Stringification (#) va Token Pasting (##)

advanced_macros.c
#include <stdio.h>

// Stringification (#) - Bien tham so thanh chuoi ky tu
#define PRINT_VAR(var)  printf(#var " = %d\n", var)

// Token Pasting (##) - Noi cac token lai voi nhau
#define DECLARE_PAIR(type, name) \
    type name##_first;           \
    type name##_second;

int main() {
    int score = 95;
    PRINT_VAR(score);    // => printf("score" " = %d\n", score);
                         // => In ra: score = 95

    DECLARE_PAIR(int, point)  // Tao ra: int point_first; int point_second;
    point_first = 10;
    point_second = 20;

    return 0;
}

Macro dinh nghia san (Predefined Macros)

predefined_macros.c
#include <stdio.h>

int main() {
    printf("File: %s\n", __FILE__);        // Ten file hien tai
    printf("Line: %d\n", __LINE__);        // So dong hien tai
    printf("Function: %s\n", __func__);    // Ten ham hien tai (C99)
    printf("Date: %s\n", __DATE__);        // Ngay bien dich
    printf("Time: %s\n", __TIME__);        // Gio bien dich
    printf("C Standard: %ld\n", __STDC_VERSION__); // Phien ban C (vd: 201112L = C11)
    return 0;
}

Variadic Macros (Macro co so luong tham so thay doi)

variadic_macros.c
#include <stdio.h>

// Macro LOG - In kem ten file va so dong
#define LOG(fmt, ...) \
    printf("[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)

// ##__VA_ARGS__: dau ## giup xu ly truong hop khong co tham so phu
// (loai bo dau phay thua truoc khi khong co args)

int main() {
    LOG("Server started");              // [main.c:10] Server started
    LOG("Port: %d", 8080);             // [main.c:11] Port: 8080
    LOG("Host: %s, Port: %d", "localhost", 3000);
    return 0;
}

6. Bien dich co dieu kien (Conditional Compilation)

Bien dich co dieu kien cho phep bao gom hoac loai bo cac doan ma nguon dua tren dieu kien tai thoi diem bien dich. Day la cong cu cuc ky manh me de viet ma nguon da nen tang (cross-platform) va cau hinh linh hoat.

Cac chi thi co ban

conditional.c
#include <stdio.h>

// Dinh nghia macro DEBUG khi bien dich: gcc -DDEBUG main.c
// Hoac dinh nghia truc tiep trong code:
// #define DEBUG

#ifdef DEBUG
    #define DBG_PRINT(fmt, ...) \
        fprintf(stderr, "[DEBUG %s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#else
    #define DBG_PRINT(fmt, ...) // Khong lam gi ca (bi loai bo khi build Release)
#endif

// Kiem tra phien ban C
#if __STDC_VERSION__ >= 201112L
    #define C_VERSION "C11 or later"
#elif __STDC_VERSION__ >= 199901L
    #define C_VERSION "C99"
#else
    #define C_VERSION "C89/C90"
#endif

int main() {
    DBG_PRINT("Program started");
    printf("Compiled with: %s\n", C_VERSION);
    DBG_PRINT("Value of x = %d", 42);
    return 0;
}

Ma nguon da nen tang (Cross-platform code)

platform.c
#include <stdio.h>

// Phat hien he dieu hanh tai thoi diem bien dich
#if defined(_WIN32) || defined(_WIN64)
    #include <windows.h>
    #define CLEAR_SCREEN() system("cls")
    #define PATH_SEP '\\'
#elif defined(__linux__) || defined(__APPLE__)
    #include <unistd.h>
    #define CLEAR_SCREEN() system("clear")
    #define PATH_SEP '/'
#else
    #error "Unsupported platform!"
#endif

// Feature toggles
#ifndef MAX_USERS
    #define MAX_USERS 100  // Gia tri mac dinh neu khong duoc truyen tu command line
#endif

int main() {
    printf("Path separator: '%c'\n", PATH_SEP);
    printf("Max users: %d\n", MAX_USERS);
    // Bien dich voi: gcc -DMAX_USERS=500 platform.c
    return 0;
}

7. Quy trinh bien dich da file & Makefile nang cao

Bien dich thu cong (Manual compilation)

Quy trinh bien dich da file gom 2 buoc: bien dich tung file thanh object file (.o), roi lien ket (link) chung lai.

Terminal
# Buoc 1: Bien dich tung file .c thanh file object .o
gcc -c main.c -o main.o
gcc -c student.c -o student.o
gcc -c utils.c -o utils.o

# Buoc 2: Lien ket (link) tat ca file .o thanh file thuc thi
gcc main.o student.o utils.o -o my_program

# Hoac lam tat ca trong 1 lenh (tien loi nhung khong toi uu)
gcc main.c student.c utils.c -o my_program

Makefile voi Automatic Dependency Tracking

Makefile
# Bien (Variables)
CC      = gcc
CFLAGS  = -Wall -Wextra -std=c11
TARGET  = my_program
SRCS    = main.c student.c utils.c
OBJS    = $(SRCS:.c=.o)   # Thay .c thanh .o: main.o student.o utils.o

# Rule mac dinh
all: $(TARGET)

# Lien ket cac file object
$(TARGET): $(OBJS)
	$(CC) $(OBJS) -o $(TARGET)

# Pattern rule: bien dich bat ky file .c nao thanh .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Don dep file tam
clean:
	rm -f $(OBJS) $(TARGET)

# Phony targets (khong phai file that)
.PHONY: all clean

CMake - Cong cu build da nen tang

Doi voi cac du an lon can ho tro nhieu he dieu hanh va trình bien dich, CMake la cong cu build pho bien nhat. CMake sinh ra Makefile (hoac project file cho Visual Studio, Xcode, v.v.) tu file cau hinh CMakeLists.txt:

CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

# Tu dong tim tat ca file .c trong thu muc src/
file(GLOB SOURCES "src/*.c")

add_executable(my_program ${SOURCES})

# Them thu muc include
target_include_directories(my_program PRIVATE include/)
Terminal
# Tao thu muc build rieng (out-of-source build)
mkdir build && cd build
cmake ..
make

📥 Tải về mã nguồn mẫu: multifile_demo.c

📝 Kiểm tra kiến thức bài 11
Cho macro: #define SQUARE(x) ((x) * (x)). Khi gọi SQUARE(a++), điều gì xảy ra?

Related Articles

Bài viết liên quan trong series

Lesson 12: Interactive Data Structure Visualizer in the Browser Bài 12: Công cụ trực quan hoá Cấu trúc dữ liệu trong trình duyệt Lesson 10: Implementing Data Structures in C: Linked List, Stack and Queue Bài 10: Tự xây dựng cấu trúc dữ liệu trong C: Linked List, Stack & Queue Back to C Series Overview Quay lại Lộ trình C Series