Trong ngôn ngữ lập trình C, chuỗi ký tự đóng vai trò quan trọng trong việc xử lý văn bản và dữ liệu. Tuy nhiên, việc sử dụng chuỗi ký tự có thể gây nhầm lẫn, đặc biệt khi liên quan đến khởi tạo mảng char[] và con trỏ char*. Bài viết này sẽ đi sâu vào sự khác biệt cốt yếu giữa hai phương pháp này, đồng thời làm rõ các khía cạnh liên quan đến bộ nhớ, khả năng sửa đổi và hành vi không xác định (Undefined Behavior – UB).
Mục Lục
Khởi Tạo char[]: Mảng Ký Tự Có Thể Sửa Đổi
Khi bạn khởi tạo một mảng ký tự bằng một chuỗi ký tự, ví dụ:
char c[] = "abc";
thực chất bạn đang tạo một bản sao của chuỗi ký tự “abc” trong bộ nhớ. Các ký tự ‘a’, ‘b’, ‘c’ và ký tự null (”) sẽ được lưu trữ liên tiếp trong mảng c. Mảng này có thể được sửa đổi, tức là bạn có thể thay đổi giá trị của các phần tử trong mảng.
Đây thực chất là một cách viết tắt của việc khởi tạo mảng ký tự một cách tường minh:
char c[] = {'a', 'b', 'c', ''};
Khởi Tạo char*: Con Trỏ Tới Chuỗi Ký Tự Hằng
Ngược lại, khi bạn khởi tạo một con trỏ char* bằng một chuỗi ký tự, ví dụ:
char *c = "abc";
bạn đang tạo một con trỏ trỏ đến một chuỗi ký tự hằng (string literal) được lưu trữ trong bộ nhớ tĩnh (static memory). Chuỗi ký tự này thường được lưu trữ trong phân vùng .rodata (read-only data) của bộ nhớ.
Vùng nhớ read-only chứa chuỗi ký tự hằng.
Việc cố gắng sửa đổi nội dung của chuỗi ký tự hằng này sẽ dẫn đến hành vi không xác định (Undefined Behavior – UB). Điều này có nghĩa là chương trình của bạn có thể hoạt động không ổn định, gây ra lỗi hoặc thậm chí bị treo.
Ví Dụ Minh Họa
Để làm rõ hơn sự khác biệt, hãy xem xét ví dụ sau:
#include <stdio.h>
int main() {
char c1[] = "abc";
char *c2 = "abc";
c1[0] = 'x'; // Hợp lệ, mảng c1 có thể sửa đổi
printf("c1: %sn", c1);
// c2[0] = 'x'; // Gây ra lỗi hoặc hành vi không xác định (UB)
printf("c2: %sn", c2);
return 0;
}
Trong ví dụ trên, việc sửa đổi c1[0] là hợp lệ vì c1 là một mảng ký tự có thể sửa đổi. Tuy nhiên, việc cố gắng sửa đổi c2[0] sẽ gây ra lỗi hoặc hành vi không xác định, vì c2 là một con trỏ trỏ đến một chuỗi ký tự hằng.
Phân Tích ELF GCC 4.8 x86-64
Để hiểu rõ hơn về cách trình biên dịch xử lý hai trường hợp này, chúng ta có thể sử dụng công cụ objdump để xem mã assembly được tạo ra.
*Trường hợp `char`:**
char *s = "abc";
Mã assembly tương ứng:
8: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
f: 00
c: R_X86_64_32S .rodata
Đoạn mã này cho thấy rằng địa chỉ của chuỗi ký tự “abc” (được lưu trữ trong phân vùng .rodata) được gán cho con trỏ s.
Trường hợp char[]:
char s[] = "abc";
Mã assembly tương ứng:
17: c7 45 f0 61 62 63 00 movl $0x636261,-0x10(%rbp)
Đoạn mã này cho thấy rằng các ký tự ‘a’, ‘b’, ‘c’ và ký tự null được sao chép vào bộ nhớ stack (liên quan đến %rbp).
Tóm Tắt Sự Khác Biệt
| Đặc điểm | char[] |
char* |
|---|---|---|
| Lưu trữ | Bộ nhớ stack (hoặc bộ nhớ cấp phát động) | Bộ nhớ tĩnh (.rodata) |
| Khả năng sửa đổi | Có thể sửa đổi | Không thể sửa đổi (gây ra UB) |
| Bản chất | Mảng ký tự | Con trỏ tới chuỗi ký tự hằng |
| Thời gian sống | Phụ thuộc vào phạm vi của biến | Toàn bộ thời gian chạy của chương trình |
Kết Luận
Việc hiểu rõ sự khác biệt giữa khởi tạo char[] và char* là rất quan trọng để viết mã C an toàn và hiệu quả. Khi bạn cần một chuỗi ký tự có thể sửa đổi, hãy sử dụng char[]. Khi bạn chỉ cần một con trỏ tới một chuỗi ký tự hằng, bạn có thể sử dụng char*. Tuy nhiên, hãy cẩn thận không sửa đổi nội dung của chuỗi ký tự hằng này, vì điều đó có thể dẫn đến hành vi không xác định.
Tài Liệu Tham Khảo
- Dự thảo C99 N1256
- Tiêu chuẩn C11
- ELF Specification
- GCC Documentation
