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:
#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ệnStandard Input/Outputchứ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àmmainnày.intbiể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ị0cho 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.
Ba kiểu vừa kể trên đủ để viết chương trình đầu tiên có dữ liệu thật. Đoạn dưới đây khai báo mỗi kiểu
một biến rồi in ra — hãy để ý mỗi kiểu đi với một mã định dạng khác nhau trong
printf, và mục 4 ngay sau sẽ liệt kê đầy đủ các mã đó.
#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;
}
Điểm mấu chốt nằm ở chỗ mã định dạng phải khớp kiểu: %d đi với int,
%.2f đi với số thực và làm tròn 2 chữ số, %c đi với một ký tự. Ghép sai cặp
thì chương trình vẫn biên dịch nhưng in ra số rác — đây là lỗi phổ biến nhất của người mới.
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).
&age nghĩa là "số nhà của biến age". scanf cần số nhà chứ
không cần giá trị, vì việc của nó là ghi vào đó chứ không phải đọc ra.Segmentation Fault — hệ điều hành thấy chương trình chạm vào vùng nhớ không phải của nó và dừng chương trình ngay. Trên Terminal bạn sẽ thấy dòng
Segmentation fault (core dumped). Nghe đáng sợ, nhưng đây thực ra là loại lỗi
dễ chịu: nó dừng ngay tại chỗ sai.Undefined Behavior — tệ hơn nhiều. Chuẩn C nói "trường hợp này tôi không quy định gì cả", nên chương trình có thể chạy đúng hôm nay, sai vào tuần sau, hoặc chỉ hỏng trên máy người khác. Không có thông báo lỗi nào cả. Phần lớn cảnh báo trong bài này tồn tại là để bạn tránh đúng loại lỗi đó.
#include <stdio.h>
int main() {
int age;
float score;
char letter;
char name[50]; // Character array (string): 49 chars max + 1 terminator '\0'
// Read an integer
printf("Nhap age: ");
scanf("%d", &age); // &age = the address of the variable age
// Read a floating point number
printf("Nhap score: ");
scanf("%f", &score); // %f cho float, %lf cho double
// WARNING: after scanf reads a number, the '\n' (Enter) stays in the buffer.
// A following scanf("%c") would read that '\n' instead of waiting for input.
// The fix: put a space before %c so it skips any whitespace.
printf("Nhap mot ky tu: ");
scanf(" %c", &letter); // The space before %c skips the leftover '\n'
// Reading a string with %s: no & needed, an array name IS an address.
// DANGER: bare %s has no length limit -> buffer overflow!
// Always bound it: %49s (at most 49 chars for a 50-element array).
printf("Nhap name (khong dau cach): ");
scanf("%49s", name); // No & for an array, capped at 49 chars
printf("\n--- Ket qua ---\n");
printf("Tuoi: %d\n", age);
printf("Diem: %.2f\n", score);
printf("Ky tu: %c\n", letter);
printf("Ten: %s\n", name);
return 0;
}
Các lỗi thường gặp với scanf:
-
Quên dấu
&: Với các kiểuint,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ự
\nsót trong buffer sau%d/%f: Khi bạn nhấn Enter sau khi nhập số, ký tự xuống dòng\nvẫn nằm trong bộ đệm stdin. Nếu lệnhscanf("%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ùngscanf(" %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 printf và scanf để 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ớisigned. signed— Lưu cả số âm và dương (mặc định choint).
Vì kích thước phụ thuộc nền tảng như vừa nói, cách duy nhất để biết chắc là hỏi thẳng máy đang chạy. Chương trình dưới đây in kích thước thật của từng kiểu trên chính máy bạn:
#include <stdio.h>
#include <stdint.h> // Fixed-width integer types
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;
}
Chạy thử trên máy khác, bạn sẽ thấy vài con số đổi. Đó chính là lý do không nên viết số cứng (kiểu
"int là 4 byte") vào code: sizeof() luôn trả về sự thật của nền tảng đang chạy, còn con
số bạn nhớ trong đầu thì không.
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.
Khác biệt giữa hai cách chỉ lộ ra khi bạn dùng sai. Đoạn dưới đặt chúng cạnh nhau, và phần cuối cố
tình để lại một cái bẫy kinh điển của #define:
#include <stdio.h>
// Option 1: #define - pure text replacement, NO data type
#define PI 3.14159265
#define MAX_SIZE 100
// Option 2: const - a typed constant, checked by the compiler
const double E = 2.71828182;
const int MAX_STUDENTS = 50;
int main() {
// PI has no type -> the compiler cannot warn you if you misuse it
printf("PI = %f\n", PI);
// E is a double -> the compiler warns you if you pass the wrong type
printf("E = %f\n", E);
// The classic #define trap:
#define SQUARE(x) x * x
printf("SQUARE(3+1) = %d\n", SQUARE(3+1)); // Prints 7, NOT 16!
// Because the macro expands to: 3+1 * 3+1 = 3+3+1 = 7
// The fix: #define SQUARE(x) ((x) * (x))
return 0;
}
Dòng đáng nhớ nhất là SQUARE(3+1) in ra 7 chứ không phải 16. Vì
#define chỉ thay thế văn bản chứ không hiểu toán học, nó bung thành
3+1 * 3+1, và phép nhân được ưu tiên trước. Đó là lý do quy tắc chung là:
dùng const cho hằng số, chỉ dùng #define khi thật sự cần.
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.
Ví dụ dưới dùng hai kiểu enum: một cái để giá trị mặc định, một cái tự đánh số — rồi dùng
nó trong switch, nơi enum phát huy tác dụng rõ nhất:
#include <stdio.h>
// Default values: RED=0, GREEN=1, BLUE=2
enum Color { RED, GREEN, BLUE };
// Custom values: 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
// Using an enum in a switch statement
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ái lợi thấy rõ ở khối switch: viết case MON: đọc ra nghĩa ngay, còn
case 1: thì sáu tháng sau chính bạn cũng phải đi tra 1 là thứ mấy. Trình biên dịch cũng
cảnh báo được nếu bạn quên xử lý một giá trị trong enum.
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ụ:
int→double. -
É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ị.
Đoạn dưới đây gom cả bốn tình huống ép kiểu vào một chương trình chạy được: ngầm định, tường minh, mất dữ liệu khi thu nhỏ, và quy tắc thăng cấp số nguyên. Mỗi khối đều in kết quả ra để bạn đối chiếu với con số ghi trong comment:
#include <stdio.h>
int main() {
// === Implicit casting ===
int a = 5;
double b = a; // int -> double: nothing is lost
printf("a = %d, b = %f\n", a, b); // b = 5.000000
// Integer promotion: in a mixed expression, int is promoted automatically
int x = 5;
double y = 2.0;
double result = x + y; // x is promoted to double before the addition
printf("x + y = %f\n", result); // 7.000000
// === Explicit casting ===
int numerator = 7;
int denominator = 2;
// Without a cast: integer division, the fraction is thrown away!
printf("7/2 = %d\n", numerator / denominator); // 3
// With a cast: real division, the fraction is kept
printf("7/2 = %f\n", (double)numerator / denominator); // 3.500000
// WARNING: casting from a larger type to a smaller one CAN LOSE DATA
double big = 123456.789;
int truncated = (int)big; // The fractional part is gone!
printf("big = %f, truncated = %d\n", big, truncated); // 123456
long long huge = 5000000000LL; // Beyond int range (max ~2.1 billion)
int overflow = (int)huge; // Overflow! The result is unpredictable.
printf("huge = %lld, overflow = %d\n", huge, overflow);
// === The integer promotion rule ===
// char and short are always promoted to int in arithmetic expressions.
char c1 = 100, c2 = 100;
// c1 * c2 = 10000, far beyond char range (max 127),
// but it does NOT overflow: both are promoted to int before multiplying.
int product = c1 * c2;
printf("c1 * c2 = %d\n", product); // 10000
return 0;
}
Hai dòng đáng nhớ nhất trong đoạn trên nằm cạnh nhau: 7/2 cho 3, còn
(double)7/2 cho 3.500000. Cùng hai con số, khác nhau đúng một phép ép
kiểu — vì khi cả hai vế đều là int, C làm phép chia nguyên và vứt phần dư đi. Đây chính
là câu hỏi trong phần kiểm tra ngay dưới.
int a = 7; int b = 2; printf("%f", (double)a / b);. Kết quả in ra màn hình
là gì?
Bình luận