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 second part of the series, we cover the basics of C syntax, variables, standard types, console I/O using scanf/printf, format specifiers, type modifiers, enum, and type casting.

Sau khi đã cài đặt thành công môi trường lập trình C ở Bài 1, trong bài học này chúng ta sẽ cùng tìm hiểu về cấu trúc cơ bản của một chương trình C, cách sử dụng biến để lưu trữ dữ liệu, các kiểu dữ liệu và format specifiers, type modifiers, hằng số, enum, ép kiểu, và cách nhập xuất giá trị ra màn hình Console.

1. Phân tích cấu trúc một chương trình C

Hãy xem lại đoạn mã nguồn kinh điển mà chúng ta đã chạy ở bài trước:

hello.c
#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}
  • #include <stdio.h>: Chỉ thị tiền xử lý yêu cầu trình biên dịch chèn thư viện Standard Input/Output chứa các hàm nhập xuất dữ liệu chuẩn (như printf, scanf).
  • int main(): Hàm chính của chương trình. Mọi chương trình C khi chạy đều sẽ tìm và bắt đầu thực thi từ dòng đầu tiên bên trong hàm main này. int biểu thị kiểu trả về của hàm là một số nguyên.
  • printf("..."): Hàm xuất dữ liệu ra màn hình. Ký tự \n đại diện cho việc xuống dòng (New line).
  • return 0;: Trả về giá trị 0 cho hệ điều hành, báo hiệu chương trình đã chạy thành công tốt đẹp không có lỗi xảy ra.

2. Biến & Các kiểu dữ liệu cơ bản

Biến (Variable) là tên đại diện cho một phân vùng nhớ trong máy tính dùng để lưu trữ dữ liệu tạm thời khi chương trình chạy.

Trong C, trước khi sử dụng biến, bạn bắt buộc phải khai báo rõ ràng kiểu dữ liệu của biến đó. Các kiểu dữ liệu cơ bản bao gồm:

  • Kiểu số nguyên: int (thường chiếm 4 bytes bộ nhớ trên máy tính 32-bit/64-bit hiện đại).
  • Kiểu số thực (dấu phẩy động): float (thường chiếm 4 bytes) và double (thường chiếm 8 bytes).
  • Kiểu ký tự: char (luôn chiếm 1 byte), dùng để lưu một ký tự hoặc một số nguyên nhỏ.

⚠️ Lưu ý quan trọng về kiến trúc máy tính: Kích thước thực tế của các kiểu dữ liệu trong C (ngoại trừ char luôn luôn là 1 byte) không cố định tuyệt đối mà phụ thuộc vào kiến trúc CPU (8-bit, 16-bit, 32-bit hay 64-bit) và hệ điều hành/trình biên dịch. Bạn có thể tham khảo bảng đặc tả chuẩn tại tài liệu C Type System (Hệ thống kiểu C) và chi tiết về các mô hình dữ liệu (Data models như LP64, ILP32) tại tài liệu Arithmetic types trên cppreference.com. Ví dụ, kiểu int sẽ có kích thước là 2 bytes trên dòng vi điều khiển nhúng 8-bit/16-bit (như Arduino Uno), nhưng là 4 bytes trên CPU x86/ARM hiện đại. Để viết mã nguồn tương thích cao, hãy luôn sử dụng toán tử sizeof() để đo kích thước vùng nhớ thay vì tự nhập số cố định.

variables.c
#include <stdio.h>

int main() {
    int age = 18;
    double score = 9.5;
    char grade = 'A';

    printf("Tuoi: %d\n", age);      // %d dung cho so nguyen (int)
    printf("Diem: %.2f\n", score);  // %.2f dung cho so thuc, lay 2 chu so sau dau phay
    printf("Xep loai: %c\n", grade); // %c dung cho ky tu (char)

    return 0;
}

3. Nhập dữ liệu từ bàn phím bằng scanf

Để nhận dữ liệu do người dùng nhập vào từ bàn phím, ta sử dụng hàm scanf. Lưu ý: ta phải truyền địa chỉ của biến vào hàm bằng cách thêm toán tử & (address-of) trước tên biến. Nếu thiếu dấu &, chương trình sẽ gặp lỗi Segmentation Fault hoặc hành vi không xác định (Undefined Behavior).

scanf_demo.c
#include <stdio.h>

int main() {
    int tuoi;
    float diem;
    char kyTu;
    char ten[50]; // Mảng ký tự (chuỗi) tối đa 49 ký tự + 1 ký tự kết thúc '\0'

    // Đọc số nguyên
    printf("Nhap tuoi: ");
    scanf("%d", &tuoi);  // &tuoi = địa chỉ của biến tuoi

    // Đọc số thực
    printf("Nhap diem: ");
    scanf("%f", &diem);  // %f cho float, %lf cho double

    // ⚠️ Lưu ý: sau khi scanf đọc số, ký tự '\n' (Enter) vẫn còn trong bộ đệm (buffer).
    // Nếu gọi scanf("%c") ngay sau đó, nó sẽ đọc ký tự '\n' thay vì đợi nhập mới.
    // Cách xử lý: thêm khoảng trắng trước %c để bỏ qua whitespace.
    printf("Nhap mot ky tu: ");
    scanf(" %c", &kyTu);  // Dấu cách trước %c bỏ qua '\n' còn sót

    // ⚠️ Đọc chuỗi bằng %s: KHÔNG cần dấu & vì tên mảng đã là địa chỉ.
    // NGUY HIỂM: %s không giới hạn độ dài → tràn bộ đệm (buffer overflow)!
    // Luôn giới hạn: %49s (tối đa 49 ký tự cho mảng 50 phần tử).
    printf("Nhap ten (khong dau cach): ");
    scanf("%49s", ten);  // Không cần & cho mảng, giới hạn 49 ký tự

    printf("\n--- Ket qua ---\n");
    printf("Tuoi: %d\n", tuoi);
    printf("Diem: %.2f\n", diem);
    printf("Ky tu: %c\n", kyTu);
    printf("Ten: %s\n", ten);

    return 0;
}

Các lỗi thường gặp với scanf:

  • Quên dấu &: Với các kiểu int, float, double, char, bạn bắt buộc phải truyền địa chỉ bằng &. Riêng mảng (ví dụ char ten[50]) thì không cần vì tên mảng đã tự động chuyển thành con trỏ tới phần tử đầu tiên.
  • Tràn bộ đệm (Buffer Overflow) với %s: Nếu người dùng nhập chuỗi dài hơn kích thước mảng, dữ liệu sẽ ghi đè vùng nhớ không thuộc về mảng, gây ra lỗi bảo mật nghiêm trọng. Luôn giới hạn bằng %49s (cho mảng kích thước 50).
  • Ký tự \n sót trong buffer sau %d / %f: Khi bạn nhấn Enter sau khi nhập số, ký tự xuống dòng \n vẫn nằm trong bộ đệm stdin. Nếu lệnh scanf("%c") tiếp theo không có khoảng trắng phía trước, nó sẽ đọc ngay ký tự \n đó thay vì đợi người dùng nhập ký tự mới. Giải pháp: dùng scanf(" %c", &c) (thêm dấu cách trước %c).

4. Bảng Format Specifiers đầy đủ

Các format specifier (mã định dạng) dùng trong printfscanf để xác định kiểu dữ liệu cần xuất/nhập. Tham khảo đầy đủ tại cppreference.com - fprintf.

Specifier Kiểu dữ liệu Mô tả Ví dụ
%d int Số nguyên có dấu (decimal) printf("%d", -42);
%u unsigned int Số nguyên không dấu printf("%u", 42u);
%ld long Số nguyên dài có dấu printf("%ld", 100000L);
%lld long long Số nguyên rất dài (64-bit) printf("%lld", 9000000000LL);
%f float / double Số thực dấu phẩy động (printf dùng cho cả float lẫn double) printf("%.2f", 3.14);
%lf double Dùng trong scanf để đọc double (printf thì %f là đủ) scanf("%lf", &d);
%e float / double Ký pháp khoa học (scientific notation) printf("%e", 0.00123);1.230000e-03
%c char Một ký tự đơn printf("%c", 'A');
%s char* / char[] Chuỗi ký tự (kết thúc bằng \0) printf("%s", "Hello");
%p void* Địa chỉ con trỏ (dạng hex) printf("%p", (void*)&x);
%x unsigned int Số nguyên dạng thập lục phân (hexadecimal) printf("%x", 255);ff
%o unsigned int Số nguyên dạng bát phân (octal) printf("%o", 8);10
%% In ra ký tự phần trăm % printf("100%%");100%

5. Type Modifiers & sizeof

C cung cấp các type modifier (từ khóa bổ sung kiểu) để thay đổi kích thước và phạm vi giá trị của các kiểu số nguyên cơ bản. Tham khảo chi tiết tại cppreference.com - Arithmetic types.

  • short — Số nguyên ngắn, thường 2 bytes (tối thiểu 16-bit theo chuẩn C).
  • long — Số nguyên dài, thường 4 bytes (32-bit) trên Windows, 8 bytes (64-bit) trên Linux/macOS 64-bit.
  • long long — Số nguyên rất dài, luôn tối thiểu 8 bytes (64-bit) theo chuẩn C99.
  • unsigned — Chỉ lưu số không âm (0 trở lên), phạm vi dương gấp đôi so với signed.
  • signed — Lưu cả số âm và dương (mặc định cho int).
sizeof_demo.c
#include <stdio.h>
#include <stdint.h>  // Thư viện kiểu dữ liệu kích thước cố định

int main() {
    printf("=== Kich thuoc cac kieu du lieu co ban ===\n");
    printf("char:        %zu bytes\n", sizeof(char));
    printf("short:       %zu bytes\n", sizeof(short));
    printf("int:         %zu bytes\n", sizeof(int));
    printf("long:        %zu bytes\n", sizeof(long));
    printf("long long:   %zu bytes\n", sizeof(long long));
    printf("float:       %zu bytes\n", sizeof(float));
    printf("double:      %zu bytes\n", sizeof(double));
    printf("long double: %zu bytes\n", sizeof(long double));

    printf("\n=== Unsigned variants ===\n");
    printf("unsigned int:       %zu bytes\n", sizeof(unsigned int));
    printf("unsigned long long: %zu bytes\n", sizeof(unsigned long long));

    printf("\n=== Kieu co dinh tu stdint.h (portable) ===\n");
    printf("int8_t:   %zu bytes\n", sizeof(int8_t));
    printf("int16_t:  %zu bytes\n", sizeof(int16_t));
    printf("int32_t:  %zu bytes\n", sizeof(int32_t));
    printf("int64_t:  %zu bytes\n", sizeof(int64_t));
    printf("uint8_t:  %zu bytes\n", sizeof(uint8_t));
    printf("uint16_t: %zu bytes\n", sizeof(uint16_t));
    printf("uint32_t: %zu bytes\n", sizeof(uint32_t));
    printf("uint64_t: %zu bytes\n", sizeof(uint64_t));

    return 0;
}

Khi nào dùng stdint.h? Khi bạn cần mã nguồn chạy đúng trên mọi nền tảng (portable code), đặc biệt trong lập trình nhúng (embedded), giao thức mạng, hoặc xử lý tệp nhị phân. Các kiểu như int32_t, uint8_t đảm bảo kích thước chính xác bất kể kiến trúc CPU. Tham khảo: cppreference.com - Fixed width integer types.

6. Hằng số, Enum & Ép kiểu

A. Hằng số: const vs #define

C có hai cách phổ biến để định nghĩa hằng số (giá trị không thay đổi):

  • const — Tạo biến hằng có kiểu dữ liệu rõ ràng, được kiểm tra bởi trình biên dịch. An toàn hơn, dễ debug hơn.
  • #define — Chỉ thị tiền xử lý (preprocessor macro), thay thế văn bản trước khi biên dịch. Không có kiểu dữ liệu, không chiếm bộ nhớ, nhưng dễ gây lỗi tinh vi nếu không cẩn thận.
const_vs_define.c
#include <stdio.h>

// Cách 1: #define — thay thế văn bản, KHÔNG có kiểu dữ liệu
#define PI 3.14159265
#define MAX_SIZE 100

// Cách 2: const — biến hằng CÓ kiểu, được trình biên dịch kiểm tra
const double E = 2.71828182;
const int MAX_STUDENTS = 50;

int main() {
    // PI không có kiểu → trình biên dịch không cảnh báo nếu dùng sai ngữ cảnh
    printf("PI = %f\n", PI);

    // E có kiểu double → trình biên dịch sẽ cảnh báo nếu bạn truyền sai kiểu
    printf("E = %f\n", E);

    // ⚠️ Cạm bẫy của #define:
    #define SQUARE(x) x * x
    printf("SQUARE(3+1) = %d\n", SQUARE(3+1)); // Kết quả: 7, KHÔNG phải 16!
    // Vì macro mở rộng thành: 3+1 * 3+1 = 3+3+1 = 7
    // Sửa đúng: #define SQUARE(x) ((x) * (x))

    return 0;
}

B. Enum (Kiểu liệt kê)

enum cho phép định nghĩa một tập hợp các hằng số nguyên có tên, giúp mã nguồn dễ đọc và bảo trì hơn. Tham khảo: cppreference.com - Enumeration.

enum_demo.c
#include <stdio.h>

// Giá trị mặc định: RED=0, GREEN=1, BLUE=2
enum Color { RED, GREEN, BLUE };

// Gán giá trị tùy chỉnh: MON=1, TUE=2, ..., SUN=7
enum Weekday { MON = 1, TUE, WED, THU, FRI, SAT, SUN };

int main() {
    enum Color favColor = GREEN;
    enum Weekday today = FRI;

    printf("Mau yeu thich: %d\n", favColor);  // In ra: 1
    printf("Hom nay la thu: %d\n", today);     // In ra: 5

    // Dùng enum trong switch-case
    switch (favColor) {
        case RED:   printf("Do\n"); break;
        case GREEN: printf("Xanh la\n"); break;
        case BLUE:  printf("Xanh duong\n"); break;
    }

    return 0;
}

C. Ép kiểu (Type Casting)

Ép kiểu là việc chuyển đổi giá trị từ kiểu dữ liệu này sang kiểu khác. C hỗ trợ hai hình thức:

  • Ép kiểu ngầm định (Implicit Casting) — Trình biên dịch tự động chuyển đổi kiểu khi cần, theo quy tắc "kiểu nhỏ → kiểu lớn" (integer promotion). Ví dụ: intdouble.
  • Ép kiểu tường minh (Explicit Casting) — Lập trình viên chỉ định rõ bằng cú pháp (kiểu_mới)giá_trị.
type_casting.c
#include <stdio.h>

int main() {
    // === Ép kiểu ngầm định (Implicit) ===
    int a = 5;
    double b = a;  // int → double: không mất dữ liệu
    printf("a = %d, b = %f\n", a, b);  // b = 5.000000

    // Integer promotion: trong biểu thức hỗn hợp, int tự động thăng cấp
    int x = 5;
    double y = 2.0;
    double result = x + y;  // x được promote thành double trước khi cộng
    printf("x + y = %f\n", result);  // 7.000000

    // === Ép kiểu tường minh (Explicit) ===
    int numerator = 7;
    int denominator = 2;
    // Không ép kiểu: phép chia nguyên, mất phần thập phân!
    printf("7/2 = %d\n", numerator / denominator);           // 3
    // Ép kiểu: chia số thực, giữ phần thập phân
    printf("7/2 = %f\n", (double)numerator / denominator);   // 3.500000

    // ⚠️ CẢNH BÁO: Ép kiểu từ lớn → nhỏ có thể MẤT DỮ LIỆU
    double big = 123456.789;
    int truncated = (int)big;  // Mất phần thập phân!
    printf("big = %f, truncated = %d\n", big, truncated);  // 123456

    long long huge = 5000000000LL;  // Vượt phạm vi int (max ~2.1 tỷ)
    int overflow = (int)huge;       // Tràn số! Kết quả không dự đoán được.
    printf("huge = %lld, overflow = %d\n", huge, overflow);

    // === Quy tắc Integer Promotion ===
    // char và short luôn được promote thành int trong biểu thức số học.
    char c1 = 100, c2 = 100;
    // c1 * c2 = 10000, vượt phạm vi char (max 127),
    // nhưng KHÔNG tràn vì c1 và c2 được promote thành int trước khi nhân.
    int product = c1 * c2;
    printf("c1 * c2 = %d\n", product);  // 10000

    return 0;
}
📝 Kiểm tra kiến thức bài 2
Cho đoạn mã: int a = 7; int b = 2; printf("%f", (double)a / b);. Kết quả in ra màn hình là gì?

Related Articles

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

Lesson 3: Operators, Precedence & Bitwise Operations in C Bài 3: Toán tử, Thứ tự ưu tiên & Phép toán Bitwise trong C Lesson 1: Setting up C Programming Environment on macOS & Linux Bài 1: Hướng dẫn cài đặt môi trường lập trình C trên macOS & Linux Back to C Series Overview Quay lại Lộ trình C Series