Mảng (array) và chuỗi ký tự (string) là hai cấu trúc dữ liệu nền tảng và được sử dụng nhiều nhất trong lập trình C. Hiểu rõ cách chúng hoạt động ở mức bộ nhớ sẽ giúp bạn viết code hiệu quả, tránh các lỗi bảo mật nghiêm trọng như tràn bộ đệm (buffer overflow), và sẵn sàng cho các chủ đề nâng cao hơn như con trỏ và cấp phát bộ nhớ động.
1. Mảng một chiều (1D Arrays)
Khai báo và khởi tạo
Mảng là một tập hợp các phần tử cùng kiểu dữ liệu, được lưu trữ liên tiếp nhau trong
bộ nhớ. Chỉ số (index) bắt đầu từ 0.
// Cach 1: Khai bao roi gan gia tri tung phan tu
int scores[5];
scores[0] = 85;
scores[1] = 92;
scores[2] = 78;
scores[3] = 90;
scores[4] = 88;
// Cach 2: Khoi tao truc tiep khi khai bao
int scores[] = {85, 92, 78, 90, 88}; // Compiler tu dong tinh size = 5
// Cach 3: Khoi tao mot phan (phan tu con lai = 0)
int data[10] = {1, 2, 3}; // data[3]..data[9] deu bang 0
// Cach 4: Khoi tao tat ca bang 0
int zeros[100] = {0};
C KHÔNG kiểm tra giới hạn mảng (No Bounds Checking)
Khác với Java hay Python, C không có bất kỳ cơ chế kiểm tra giới hạn mảng nào tại thời điểm chạy (runtime). Truy cập ngoài phạm vi mảng là hành vi không xác định (undefined behavior) — chương trình có thể chạy bình thường, trả về giá trị rác (garbage value), hoặc crash ngay lập tức.
int arr[3] = {10, 20, 30};
// NGUY HIEM: Truy cap ngoai pham vi — Undefined Behavior!
printf("%d\n", arr[5]); // Doc gia tri rac tu vung nho khong thuoc mang
arr[-1] = 99; // Ghi de len vung nho khong thuoc mang — co the crash
Array decay — mảng suy biến thành con trỏ
Khi truyền mảng vào hàm, mảng tự động "suy biến" (decay) thành con trỏ tới phần tử đầu tiên. Điều này có nghĩa là:
arrtương đương với&arr[0]khi truyền vào hàm.-
Hàm nhận mảng không biết được kích thước của mảng — bạn phải truyền thêm tham số
size. -
Mẹo
sizeof(arr)/sizeof(arr[0])chỉ hoạt động trong phạm vi khai báo mảng, không hoạt động trong hàm nhận mảng.
Tìm giá trị lớn nhất và nhỏ nhất trong mảng
#include <stdio.h>
void findMaxMin(int arr[], int size, int *max, int *min) {
*max = arr[0];
*min = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] > *max) *max = arr[i];
if (arr[i] < *min) *min = arr[i];
}
}
int main() {
int numbers[] = {42, 17, 93, 8, 56, 71, 3, 88};
int size = sizeof(numbers) / sizeof(numbers[0]); // = 8, chi dung o day
int max, min;
findMaxMin(numbers, size, &max, &min);
printf("Max = %d, Min = %d\n", max, min); // Max = 93, Min = 3
return 0;
}
Chú ý cách khởi tạo: max và min đều lấy arr[0] làm mốc, chứ
không phải gán 0. Gán 0 sẽ sai ngay khi mảng toàn số âm — một lỗi kinh điển mà chương trình vẫn chạy
và vẫn in ra kết quả, chỉ là kết quả sai.
2. Mảng nhiều chiều (Multi-dimensional Arrays)
Khai báo mảng 2 chiều
Mảng 2 chiều thực chất là mảng của các mảng. Trong bộ nhớ, các phần tử được lưu theo thứ tự hàng trước (row-major order) — toàn bộ hàng 0 lưu trước, rồi đến hàng 1, hàng 2, v.v.
// Khai bao ma tran 3x4
int matrix[3][4] = {
{1, 2, 3, 4}, // Hang 0
{5, 6, 7, 8}, // Hang 1
{9, 10, 11, 12} // Hang 2
};
// Truy cap phan tu: matrix[hang][cot]
printf("%d\n", matrix[1][2]); // In ra: 7
Bố cục bộ nhớ (Memory Layout)
Mảng 2 chiều int m[3][4] thực tế được lưu liên tiếp trong bộ nhớ như sau:
Memory addresses (row-major order):
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│m[0]│m[0]│m[0]│m[0]│m[1]│m[1]│m[1]│m[1]│m[2]│m[2]│m[2]│m[2]│
│[0] │[1] │[2] │[3] │[0] │[1] │[2] │[3] │[0] │[1] │[2] │[3] │
├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ 12 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
◄──── Row 0 ─────► ◄──── Row 1 ─────► ◄──── Row 2 ─────►
Chuyển vị ma trận (Matrix Transpose)
#include <stdio.h>
#define ROWS 3
#define COLS 4
int main() {
int matrix[ROWS][COLS] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int transposed[COLS][ROWS]; // Dao kich thuoc
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
transposed[j][i] = matrix[i][j];
}
}
printf("Ma tran chuyen vi:\n");
for (int i = 0; i < COLS; i++) {
for (int j = 0; j < ROWS; j++) {
printf("%3d ", transposed[i][j]);
}
printf("\n");
}
return 0;
}
Mảng răng cưa (Jagged Arrays) bằng mảng con trỏ
C không hỗ trợ mảng răng cưa trực tiếp như Java. Tuy nhiên, bạn có thể mô phỏng bằng
mảng các con trỏ, trong đó mỗi con trỏ trỏ tới một mảng có kích thước khác nhau (được
cấp phát động bằng malloc).
#include <stdio.h>
#include <stdlib.h>
int main() {
int *rows[3]; // Mang 3 con tro
int sizes[] = {2, 5, 3}; // Moi hang co kich thuoc khac nhau
for (int i = 0; i < 3; i++) {
rows[i] = (int*) malloc(sizes[i] * sizeof(int));
for (int j = 0; j < sizes[i]; j++) {
rows[i][j] = (i + 1) * 10 + j; // Gan gia tri vi du
}
}
// In ra mang rang cua
for (int i = 0; i < 3; i++) {
printf("Hang %d (%d phan tu): ", i, sizes[i]);
for (int j = 0; j < sizes[i]; j++) {
printf("%d ", rows[i][j]);
}
printf("\n");
free(rows[i]); // Giai phong bo nho
}
return 0;
}
Điểm khác biệt so với mảng 2 chiều thật: ở đây mỗi hàng là một vùng nhớ riêng, cấp
phát độc lập, nên chúng không nằm liền nhau trong bộ nhớ. Đổi lại, mỗi hàng được dài ngắn tuỳ ý — và
cái giá phải trả là bạn phải tự free() từng hàng một trước khi giải phóng mảng con trỏ.
3. Chuỗi ký tự trong C (C Strings)
Trong C, không có kiểu dữ liệu string riêng biệt như Java hay Python. Chuỗi ký tự
trong C đơn giản là mảng các ký tự (char array) kết thúc bằng ký tự null
'\0' (có giá trị ASCII là 0).
String literal vs char array
// Cach 1: Con tro tro toi string literal (CHI DOC — nam trong text segment)
char *greeting = "Hello";
// greeting[0] = 'h'; // NGUY HIEM! Undefined behavior — co the crash
// Cach 2: Mang ky tu (CO THE GHI — nam tren stack)
char name[] = "Hello";
name[0] = 'h'; // OK! name bay gio la "hello"
// Cach 3: Khai bao tuong minh voi null terminator
char city[6] = {'H', 'a', 'N', 'o', 'i', '\0'};
// Cach 4: Khai bao mang voi kich thuoc lon hon
char buffer[100] = "Hello"; // 5 ky tu + '\0', 94 byte con lai = 0
Biểu đồ bộ nhớ của chuỗi ký tự
char name[] = "Hello";
Index: [0] [1] [2] [3] [4] [5]
┌─────┬─────┬─────┬─────┬─────┬─────┐
│ 'H' │ 'e' │ 'l' │ 'l' │ 'o' │ '\0'│
│ 72 │ 101 │ 108 │ 108 │ 111 │ 0 │ ← ASCII value
└─────┴─────┴─────┴─────┴─────┴─────┘
▲
Null terminator
(marks the end of the string)
⚠️ Lỗi thường gặp:
-
Quên null terminator:
char s[5] = {'H','e','l','l','o'};— không có'\0', các hàm xử lý chuỗi sẽ đọc tràn ra ngoài mảng. - Tràn bộ đệm (Buffer overflow): Sao chép chuỗi dài vào mảng ngắn mà không kiểm tra kích thước.
4. Thư viện <string.h> — Các hàm xử lý chuỗi
Thư viện <string.h> cung cấp các hàm cơ bản để thao tác với chuỗi và vùng nhớ. Dưới
đây là bảng tham khảo nhanh:
| Hàm | Mô tả | Lưu ý |
|---|---|---|
strlen(s)
|
Trả về độ dài chuỗi (không tính '\0') | O(n) — phải duyệt hết chuỗi |
strcpy(dest, src)
|
Sao chép src vào dest | ⚠️ Không kiểm tra kích thước dest! |
strncpy(dest, src, n)
|
Sao chép tối đa n ký tự | Không tự động thêm '\0' nếu src >= n |
strcat(dest, src)
|
Nối src vào cuối dest | ⚠️ dest phải đủ chỗ |
strncat(dest, src, n)
|
Nối tối đa n ký tự | Luôn thêm '\0' sau khi nối |
strcmp(s1, s2)
|
So sánh 2 chuỗi (trả về 0 nếu bằng) | Phân biệt hoa thường |
strncmp(s1, s2, n)
|
So sánh tối đa n ký tự đầu | |
strchr(s, c)
|
Tìm ký tự c đầu tiên trong s | Trả về con trỏ hoặc NULL |
strstr(haystack, needle)
|
Tìm chuỗi con needle trong haystack | Trả về con trỏ hoặc NULL |
strtok(s, delim)
|
Tách chuỗi thành các token | ⚠️ Thay đổi chuỗi gốc! Không thread-safe |
memcpy(dest, src, n)
|
Sao chép n byte từ src sang dest | Không an toàn nếu vùng nhớ chồng chéo |
memmove(dest, src, n)
|
Sao chép n byte (an toàn khi chồng chéo) | Chậm hơn memcpy một chút |
memset(s, c, n)
|
Đặt n byte đầu tiên của s thành giá trị c | Thường dùng để xoá mảng về 0 |
Cảnh báo bảo mật
⚠️ Tại sao gets() bị xoá khỏi C11? Hàm gets() đọc toàn bộ
dòng nhập mà không kiểm tra kích thước buffer, gây ra lỗ hổng
buffer overflow nghiêm trọng. Đây là một trong những lỗ hổng bảo mật bị khai thác
nhiều nhất trong lịch sử phần mềm. Thay vào đó, luôn sử dụng fgets().
Ví dụ: Phân tích dòng CSV bằng strtok
#include <stdio.h>
#include <string.h>
int main() {
char csv_line[] = "Nguyen Van A,25,Ha Noi,Developer";
char *token;
int field = 0;
const char *labels[] = {"Ten", "Tuoi", "Thanh pho", "Nghe nghiep"};
// Lan goi dau tien: truyen chuoi goc
token = strtok(csv_line, ",");
while (token != NULL) {
printf("%s: %s\n", labels[field], token);
field++;
// Cac lan goi tiep theo: truyen NULL de tiep tuc tach
token = strtok(NULL, ",");
}
return 0;
}
// Ket qua:
// Ten: Nguyen Van A
// Tuoi: 25
// Thanh pho: Ha Noi
// Nghe nghiep: Developer
Điều đáng nhớ nhất ở đây là strtok sửa trực tiếp chuỗi gốc: nó thay mỗi
dấu phân cách bằng '\0' rồi trả về con trỏ tới từng đoạn. Nên đừng bao giờ gọi nó trên
một string literal, và nếu còn cần chuỗi ban đầu thì phải sao chép ra trước.
5. Mảng ký tự và nhập chuỗi an toàn
So sánh các phương pháp nhập chuỗi
| Phương pháp | Đọc dấu cách? | Kiểm tra buffer? | An toàn? |
|---|---|---|---|
gets(buf)
|
Có | KHÔNG | ❌ Đã bị xoá khỏi C11 |
scanf("%s", buf)
|
Không (dừng tại dấu cách) | KHÔNG (mặc định) | ⚠️ Nguy hiểm |
fgets(buf, size, stdin)
|
Có | CÓ (tham số size) | ✅ Khuyên dùng |
Mẫu nhập liệu an toàn với fgets
#include <stdio.h>
#include <string.h>
int main() {
char name[50];
printf("Nhap ho ten cua ban: ");
// fgets doc toi da 49 ky tu + '\0', bao gom ca dau cach
if (fgets(name, sizeof(name), stdin) != NULL) {
// Xoa ky tu xuong dong '\n' o cuoi (fgets giu lai no)
size_t len = strlen(name);
if (len > 0 && name[len - 1] == '\n') {
name[len - 1] = '\0';
}
printf("Xin chao, %s!\n", name);
}
// Mau an toan: fgets + sscanf de doc so
char line[100];
int age;
printf("Nhap tuoi: ");
if (fgets(line, sizeof(line), stdin) != NULL) {
if (sscanf(line, "%d", &age) == 1) {
printf("Tuoi cua ban la: %d\n", age);
} else {
printf("Du lieu nhap khong hop le!\n");
}
}
return 0;
}
Minh hoạ lỗ hổng Buffer Overflow
#include <stdio.h>
#include <string.h>
int main() {
char password[8] = "secret";
char input[8];
printf("Nhap mat khau: ");
// NGUY HIEM: gets() khong kiem tra kich thuoc
// Neu nguoi dung nhap hon 7 ky tu, se tran sang bien password!
// gets(input); // KHONG BAO GIO DUNG HAM NAY!
// AN TOAN: Dung fgets thay the
fgets(input, sizeof(input), stdin);
size_t len = strlen(input);
if (len > 0 && input[len-1] == '\n') input[len-1] = '\0';
if (strcmp(input, password) == 0) {
printf("Truy cap thanh cong!\n");
} else {
printf("Sai mat khau!\n");
}
return 0;
}
Chương trình này có thể chạy "bình thường" trên máy bạn — và đó chính là điều nguy hiểm. Dữ liệu tràn ra đã ghi đè lên vùng nhớ kế bên; nó chỉ chưa gây hậu quả thấy được. Đây là lý do lỗi buffer overflow thường nằm im hàng năm trời trước khi có người tìm ra cách khai thác nó.
6. Kỹ thuật xử lý chuỗi nâng cao
Đảo ngược chuỗi tại chỗ (Two-pointer technique)
#include <stdio.h>
#include <string.h>
void reverseString(char *str) {
int left = 0;
int right = strlen(str) - 1;
while (left < right) {
// Hoan doi 2 ky tu o 2 dau
char temp = str[left];
str[left] = str[right];
str[right] = temp;
left++;
right--;
}
}
int main() {
char word[] = "Hello World";
printf("Truoc: %s\n", word);
reverseString(word);
printf("Sau: %s\n", word); // "dlroW olleH"
return 0;
}
Kiểm tra Palindrome (chuỗi đối xứng)
#include <stdio.h>
#include <string.h>
#include <ctype.h>
int isPalindrome(const char *str) {
int left = 0;
int right = strlen(str) - 1;
while (left < right) {
// Bo qua ky tu khong phai chu/so
while (left < right && !isalnum(str[left])) left++;
while (left < right && !isalnum(str[right])) right--;
// So sanh khong phan biet hoa thuong
if (tolower(str[left]) != tolower(str[right])) {
return 0; // Khong phai palindrome
}
left++;
right--;
}
return 1; // La palindrome
}
int main() {
printf("'racecar': %s\n", isPalindrome("racecar") ? "Palindrome" : "Khong");
printf("'hello': %s\n", isPalindrome("hello") ? "Palindrome" : "Khong");
printf("'A man a plan a canal Panama': %s\n",
isPalindrome("A man a plan a canal Panama") ? "Palindrome" : "Khong");
return 0;
}
Tự cài đặt hàm atoi (chuyển chuỗi thành số nguyên)
#include <stdio.h>
#include <ctype.h>
int myAtoi(const char *str) {
int result = 0;
int sign = 1;
int i = 0;
// Bo qua khoang trang o dau
while (isspace(str[i])) i++;
// Xu ly dau + hoac -
if (str[i] == '+' || str[i] == '-') {
sign = (str[i] == '-') ? -1 : 1;
i++;
}
// Chuyen tung ky tu so thanh gia tri
while (isdigit(str[i])) {
result = result * 10 + (str[i] - '0');
i++;
}
return sign * result;
}
int main() {
printf("' -42' => %d\n", myAtoi(" -42")); // -42
printf("'12345' => %d\n", myAtoi("12345")); // 12345
printf("'+100' => %d\n", myAtoi("+100")); // 100
printf("'abc' => %d\n", myAtoi("abc")); // 0
return 0;
}
Thao tác ký tự với <ctype.h>
Thư viện <ctype.h> cung cấp các hàm tiện ích để kiểm tra và chuyển đổi ký tự:
| Hàm | Mô tả | Vi du |
|---|---|---|
toupper(c)
|
Chuyển thành chữ hoa |
toupper('a') → 'A'
|
tolower(c)
|
Chuyển thành chữ thường |
tolower('Z') → 'z'
|
isdigit(c)
|
Kiểm tra có phải chữ số (0-9)? |
isdigit('5') → true
|
isalpha(c)
|
Kiểm tra có phải chữ cái? |
isalpha('x') → true
|
isalnum(c)
|
Kiểm tra chữ cái hoặc chữ số? |
isalnum('@') → false
|
isspace(c)
|
Kiểm tra khoảng trắng? |
isspace(' ') → true
|
📥 Tải về mã nguồn mẫu: arrays_strings.c
Thử thách nhỏ dành cho bạn:
Hãy viết một chương trình đếm số lần xuất hiện của mỗi ký tự (không phân biệt hoa thường) trong một
chuỗi nhập từ bàn phím, sử dụng mảng đếm int count[26] = {0};.
char *str = "Hello"; str[0] = 'h';
Bình luận