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:
#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.
#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).
#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ể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).
#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.
#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.
#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ụ:
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ị.
#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;
}
int a = 7; int b = 2; printf("%f", (double)a / b);. Kết quả in ra màn hình
là gì?
Comments
Bình luận