- [A.I System Programming] B1: Tìm hiểu lại C Core
- [A.I System Programming] B2: Tìm hiểu C Memory/Pointer
- [A.I System Programming] B3: Lịch sử của máy tính
- [A.I System Programming] B4: Motivation của GPU
- [A.I System Programming] B5: Chương trình Cuda C
- [A.I System Programming] B6: Cài đặt các công cụ và nền tảng cho CUDA Window
Ngôn ngữ C mang lại nhiều quyền kiểm soát hơn đối với cách chương trình sử dụng bộ nhớ của máy tính.
Mã C bao gồm các con trỏ
Con trỏ là một trong những thứ cơ bản nhất cần hiểu trong ngôn ngữ lập trình C. Vậy con trỏ là gì? Một con trỏ chỉ đơn giản là địa chỉ của một mẩu dữ liệu trong bộ nhớ.
1 – Thay vì truyền đi toàn bộ một bản sao của dữ liệu, bạn chỉ cần truyền một con trỏ.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 10 image 23 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-23.png)
Hãy tưởng tượng bạn có một cuốn bách khoa toàn thư dày 10.000 trang. Một người bạn muốn mượn nó để tra cứu.
- Không dùng con trỏ (Truyền bản sao): Bạn phải đi photocopy toàn bộ 10.000 trang đó và đưa cho bạn của bạn. Quá trình này tốn cực kỳ nhiều thời gian, công sức và tốn thêm một không gian khổng lồ để chứa bộ copy đó.
- Dùng con trỏ: Bạn chỉ cần viết số giá sách (địa chỉ) lên một tờ giấy note nhỏ xíu và đưa cho người bạn: “Nó ở ngăn số 3, kệ số 5 nhé”. Nhanh, gọn, lẹ.
Trong C, nếu bạn có một cục dữ liệu rất lớn (như một struct chứa hàng ngàn thông tin), việc copy nó mỗi lần gọi hàm sẽ làm chậm chương trình của bạn.
#include <stdio.h>
// Một cục dữ liệu khổng lồ (Giống như cuốn sách 10.000 trang)
struct DuLieuKhongLo {
int mang[10000];
};
// CÁCH 1: KHÔNG DÙNG CON TRỎ (Truyền bản sao - Chậm, tốn bộ nhớ)
// Máy tính phải copy toàn bộ 10.000 phần tử vào một biến 'ban_sao' mới
void XuLyBanSao(struct DuLieuKhongLo ban_sao) {
printf("Đang xử lý bản sao...\n");
}
// CÁCH 2: DÙNG CON TRỎ (Truyền địa chỉ - Nhanh, nhẹ)
// Máy tính chỉ cần truyền 1 địa chỉ duy nhất (như tờ giấy note)
void XuLyConTro(struct DuLieuKhongLo *dia_chi_du_lieu) {
printf("Đang xử lý trực tiếp từ địa chỉ...\n");
}
int main() {
struct DuLieuKhongLo du_lieu_cua_toi;
// Gọi hàm cách 2 sẽ chạy nhanh hơn rất nhiều!
XuLyConTro(&du_lieu_cua_toi);
return 0;
}2 – Bạn có thể muốn hai đoạn mã cùng hoạt động trên cùng một mẩu dữ liệu thay vì trên một bản sao tách biệt.
Không dùng con trỏ (Làm việc trên bản sao): Bạn tải một file Word báo cáo về máy tính và gửi qua email cho sếp. Sếp sửa lỗi chính tả trên file của sếp. Sửa xong, file trên máy bạn không hề thay đổi vì sếp đang sửa trên bản copy của sếp.
Dùng con trỏ: Bạn tạo một link Google Docs và gửi cho sếp (link này chính là con trỏ). Cả bạn và sếp đều đang nhìn vào và chỉnh sửa trên cùng một tài liệu duy nhất. Sếp gõ chữ nào, bạn thấy chữ đó.
Con trỏ giúp bạn làm cả hai việc này: tránh việc tạo bản sao và chia sẻ dữ liệu.
Bộ nhớ
Mỗi khi bạn khai báo một biến, máy tính sẽ tạo ra không gian cho nó ở một nơi nào đó trong bộ nhớ. Nếu bạn khai báo một biến bên trong một hàm như main(), máy tính sẽ lưu trữ nó trong một khu vực của bộ nhớ được gọi là stack (ngăn xếp). Nếu một biến được khai báo bên ngoài bất kỳ hàm nào, nó sẽ được lưu trữ trong phần globals (toàn cục) của bộ nhớ.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 11 image 25 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-25.png)
Máy tính có thể cấp phát, giả sử, vị trí bộ nhớ số 4.100.000 trong ngăn xếp (stack) cho biến x. Nếu bạn gán số 4 cho biến đó, máy tính sẽ lưu số 4 tại vị trí 4.100.000.
Nếu bạn muốn tìm ra địa chỉ bộ nhớ của biến, bạn có thể sử dụng toán tử &:
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 12 image 26 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-26.png)
Địa chỉ của biến cho bạn biết nơi để tìm thấy biến đó trong bộ nhớ. Đó là lý do tại sao một địa chỉ cũng được gọi là một con trỏ, bởi vì nó trỏ tới biến đó trong bộ nhớ.
Vấn đề khi truyền tham số bằng giá trị
Hãy tưởng tượng bạn đang viết một trò chơi trong đó người chơi phải tìm đường di chuyển xung quanh…
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 13 image 27 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-27.png)
Trò chơi sẽ cần kiểm soát rất nhiều thứ, như điểm số, mạng sống và vị trí hiện tại của người chơi. Bạn sẽ không muốn viết trò chơi thành một khối mã nguồn khổng lồ; thay vào đó, bạn sẽ tạo ra nhiều hàm nhỏ hơn, mỗi hàm sẽ thực hiện một việc gì đó hữu ích trong trò chơi:
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 14 image 28 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-28.png)
Tất cả những điều này thì có liên quan gì đến con trỏ? Hãy bắt đầu viết mã mà không cần lo lắng chút nào về con trỏ cả. Bạn cứ việc sử dụng các biến như bạn vẫn thường làm. Một phần chính của trò chơi sẽ là điều hướng con tàu của bạn xung quanh Hình chữ nhật Bermuda, vì vậy hãy đi sâu hơn vào việc mã nguồn sẽ cần làm gì trong một trong các hàm điều hướng.
Trò chơi sẽ theo dõi vị trí của người chơi bằng cách sử dụng vĩ độ (latitudes) và kinh độ (longitudes). Vĩ độ là khoảng cách người chơi ở về phía Bắc hay Nam, còn kinh độ là vị trí của họ ở phía Đông hay Tây. Nếu một người chơi muốn đi về phía Đông Nam, điều đó có nghĩa là vĩ độ của họ sẽ giảm xuống, và kinh độ của họ sẽ tăng lên:
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 15 image 29 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-29.png)
Vì vậy, bạn có thể viết một hàm go_south_east() nhận các đối số cho vĩ độ và kinh độ, sau đó nó sẽ tăng và giảm (các giá trị này):
Chương trình bắt đầu đặt một con tàu ở vị trí [32, –64], vì vậy nếu nó đi về hướng Đông Nam, vị trí mới của con tàu sẽ là [31, –63].
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 16 image 30 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-30.png)
Mã nguồn đáng lẽ phải di chuyển con tàu về phía Đông Nam từ [32, –64] đến vị trí mới tại [31, –63]. Nhưng nếu bạn biên dịch và chạy chương trình, điều này sẽ xảy ra:
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 17 image 33 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-33.png)
Vị trí của con tàu vẫn giữ nguyên chính xác như trước.
C truyền các đối số dưới dạng giá trị
Đoạn mã đã bị lỗi do cách mà ngôn ngữ C gọi hàm.
- Ban đầu, hàm
main()có một biến cục bộ tên làlongitudechứa giá trị 32.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 18 image 31 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-31.png)
- Khi máy tính gọi hàm
go_south_east(), nó sao chép giá trị của biếnlongitudesang đối sốlon. Đây thực chất chỉ là một phép gán từ biếnlongitudesang biếnlon. Khi bạn gọi một hàm, bạn không truyền bản thân biến đó làm đối số, mà chỉ truyền giá trị của nó. - Khi hàm
go_south_east()thay đổi giá trị củalon, hàm này chỉ đang thay đổi bản sao cục bộ của chính nó. Điều đó có nghĩa là khi máy tính quay trở lại hàmmain(), biếnlongitudevẫn giữ giá trị ban đầu của nó là 32.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 19 image 32 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-32.png)
Nhưng nếu đó là cách C gọi hàm, thì làm sao bạn có thể viết được một hàm có khả năng cập nhật một biến?
Sẽ rất dễ dàng nếu bạn sử dụng con trỏ…
Hãy thử truyền một con trỏ trỏ tới biến
Thay vì truyền giá trị của các biến vĩ độ và kinh độ, điều gì sẽ xảy ra nếu bạn truyền địa chỉ của chúng? Nếu biến kinh độ (longitude) nằm trong bộ nhớ ngăn xếp (stack) tại vị trí 4.100.000, điều gì sẽ xảy ra nếu bạn truyền số vị trí 4.100.000 làm tham số cho hàm go_south_east()?
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 20 image 34 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-34.png)
Nếu hàm go_south_east() được báo cho biết rằng giá trị vĩ độ (latitude) nằm ở vị trí 4.100.000, thì nó sẽ không chỉ có thể tìm thấy giá trị vĩ độ hiện tại, mà còn có thể thay đổi nội dung của biến vĩ độ gốc. Tất cả những gì hàm cần làm là đọc và cập nhật nội dung của vị trí bộ nhớ 4.100.000.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 21 image 35 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-35.png)
Bởi vì hàm go_south_east() đang cập nhật trực tiếp biến vĩ độ gốc, máy tính sẽ có thể in ra vị trí đã được cập nhật khi nó quay trở lại hàm main().
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 22 image 36 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-36.png)
| Hỏi: Tôi đã in vị trí của biến trên máy của mình và nó không phải là 4.100.000. Tôi có làm gì sai không? | Đáp: Bạn không làm gì sai cả. Vị trí bộ nhớ mà chương trình của bạn sử dụng cho các biến sẽ khác nhau từ máy này sang máy khác. |
| Hỏi: Tại sao các biến cục bộ được lưu trữ trong ngăn xếp (stack) và các biến toàn cục (globals) được lưu trữ ở một nơi khác? | Đáp: Các biến cục bộ và toàn cục được sử dụng theo những cách khác nhau. Bạn sẽ luôn chỉ có một bản sao duy nhất của một biến toàn cục, nhưng nếu bạn viết một hàm tự gọi chính nó (đệ quy), bạn có thể nhận được rất nhiều phiên bản của cùng một biến cục bộ. |
Con trỏ giúp việc chia sẻ bộ nhớ dễ dàng hơn
Đây là một trong những lý do chính để sử dụng con trỏ, nhằm cho phép các hàm chia sẻ bộ nhớ. Dữ liệu được tạo ra bởi một hàm có thể được sửa đổi bởi một hàm khác, miễn là nó biết nơi để tìm thấy dữ liệu đó trong bộ nhớ.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 23 image 37 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-37.png)
Những điểm chính
- Các biến được cấp phát không gian lưu trữ trong bộ nhớ.
- Các biến cục bộ tồn tại trong ngăn xếp / stack.
- Các biến toàn cục tồn tại trong khu vực toàn cục / globals.
- Con trỏ chỉ là các biến lưu trữ các địa chỉ bộ nhớ.
- Toán tử
&tìm địa chỉ của một biến.) - Toán tử
*có thể đọc nội dung của một địa chỉ bộ nhớ. - Toán tử
*cũng có thể thiết lập / thay đổi nội dung của một địa chỉ bộ nhớ.
| Hỏi: Các con trỏ (pointers) có phải là địa chỉ vật lý thực sự trên chip RAM không? | Đáp: Không hoàn toàn. Con trỏ lưu trữ địa chỉ ảo (virtual address) thuộc không gian địa chỉ (address space) của một tiến trình (process). Đối với tiến trình đó, đây là địa chỉ “thực”, nhưng nó không trỏ trực tiếp đến vị trí vật lý trên chip RAM. |
| Hỏi: “Không gian địa chỉ ảo” có nghĩa là gì? | Đáp: Hệ điều hành (OS) cung cấp cho mỗi tiến trình một lớp trừu tượng (abstraction layer). Tiến trình nhìn thấy bộ nhớ như một dải ô nhớ tuyến tính, liên tục, đánh số từ 0 đến N. Điều này giúp lập trình viên không phải quản lý việc tranh chấp bộ nhớ với các tiến trình khác đang chạy song song. VD: Discord và Zalo đều cùng chạy trên máy bạn, 2 tiến trình không cần lo về việc Discord cũng muốn lưu biến “tên người dùng” ở ô nhớ 1000, Zalo cũng muốn lưu “danh bạ” ở ô nhớ 1000. Thực tế hệ điều hành sẽ cấp phát cho tiến trình 1 mã khác, ví dụ Hà Nội và Bắc Ninh có 1 số tên đường giống nhau, còn bên dưới trên đường đó nhà số 1 đường Quang Trung ở Bắc Ninh không phải nhà số 1 đường Quang Trung ở Hà Nội. Việc quản lý đó là nội bộ trong tiến trình |
| Hỏi: Tại sao bộ nhớ thực tế lại không giống như vậy? | Đáp: Bộ nhớ vật lý (RAM) được quản lý theo các đơn vị gọi là khung trang (page frames). Một dữ liệu mà bạn thấy là “liên tục” trong bộ nhớ ảo có thể bị chia nhỏ và nằm rải rác ở nhiều vị trí khác nhau trên RAM vật lý. Việc tách biệt này cho phép Hệ điều hành thực hiện các kỹ thuật: Paging (Phân trang): Di chuyển các phần bộ nhớ ít dùng từ RAM xuống ổ cứng (Swap/Pagefile) để giải phóng không gian. Memory Protection (Bảo vệ bộ nhớ): Ngăn chặn tiến trình này truy cập trái phép vào dữ liệu của tiến trình khác. Relocation (Tái định vị): Di chuyển dữ liệu trong RAM mà không làm thay đổi giá trị của con trỏ trong chương trình. |
| Cơ chế nào chuyển đổi từ “địa chỉ ảo” trong con trỏ sang “địa chỉ vật lý”? | Đó là nhiệm vụ của MMU (Memory Management Unit) – một thành phần phần cứng trong CPU – kết hợp với Page Tables (Bảng trang) do Hệ điều hành quản lý. Mỗi khi bạn truy xuất một con trỏ, MMU sẽ tra cứu bảng này để tìm ra địa chỉ vật lý tương ứng trên RAM. |
| Cấu trúc vật lý của RAM phức tạp như thế nào? | Ở cấp độ phần cứng, RAM được tổ chức thành các Channels (Kênh), Ranks, và Banks. Dữ liệu được truy xuất thông qua các tín hiệu điện dựa trên địa chỉ dòng (Row) và cột (Column). Ngoài ra, các kiến trúc hiện đại như NUMA (Non-Uniform Memory Access) còn khiến tốc độ truy cập bộ nhớ khác nhau tùy thuộc vào khoảng cách vật lý giữa CPU và các thanh RAM. |
Hỏi: Tại sao tôi phải in các con trỏ ra bằng cách sử dụng chuỗi định dạng %p? | Đáp: Bạn không bắt buộc phải sử dụng chuỗi %p. Trên hầu hết các cỗ máy hiện đại, bạn có thể sử dụng %li (long integer), mặc dù trình biên dịch có thể đưa ra cho bạn một cảnh báo nếu bạn làm vậy. |
Hỏi: Tại sao định dạng %p lại hiển thị địa chỉ bộ nhớ ở định dạng hex (thập lục phân)? | Đáp: Đó là cách các kỹ sư thường dùng để nhắc tới các địa chỉ bộ nhớ. |
Truyền String vào hàm
Bạn đã biết cách truyền các giá trị đơn giản làm đối số (arguments) cho hàm, nhưng chuyện gì sẽ xảy ra nếu bạn muốn gửi một thứ gì đó phức tạp hơn vào hàm, chẳng hạn như một chuỗi (string)?
Chuỗi trong C thực chất là các mảng ký tự (arrays of characters). Điều đó có nghĩa là nếu bạn muốn truyền một chuỗi vào hàm, bạn có thể làm như thế này:
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 24 image 38 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-38.png)
Đối số msg được định nghĩa giống như một mảng, nhưng vì bạn sẽ không biết trước chuỗi đó dài bao nhiêu, nên đối số msg không bao gồm độ dài cụ thể. Điều này trông có vẻ đơn giản và dễ hiểu, nhưng có một điều hơi kỳ lạ đang diễn ra…
C có một toán tử (operator) gọi là sizeof, nó có thể cho bạn biết một thứ gì đó chiếm bao nhiêu byte không gian trong bộ nhớ. Bạn có thể gọi toán tử này kèm với một kiểu dữ liệu (data type) hoặc với một đoạn dữ liệu cụ thể:
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 25 image 39 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-39.png)
Nhưng một điều kỳ lạ sẽ xảy ra nếu bạn nhìn vào độ dài của chuỗi mà bạn đã truyền vào hàm:
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 26 image 40 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-40.png)
Thay vì hiển thị toàn bộ chiều dài thực tế của chuỗi, đoạn mã chỉ trả về 4 hoặc 8 byte. Chuyện gì đã xảy ra vậy? Tại sao nó lại nghĩ rằng chuỗi chúng ta truyền vào lại ngắn hơn?
Theo bạn, tại sao sizeof(msg) lại ngắn hơn chiều dài của toàn bộ chuỗi? msg thực chất là gì? Tại sao nó lại trả về các kích thước khác nhau trên các máy tính khác nhau?
Biến mảng (Array variables) giống như con trỏ (pointers)
Khi bạn tạo một mảng, biến mảng đó có thể được sử dụng như một con trỏ trỏ đến vị trí bắt đầu của mảng trong bộ nhớ. Khi C nhìn thấy một dòng mã trong một hàm như thế này:
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 27 image 41 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-41.png)
Máy tính sẽ dành riêng một không gian trên ngăn xếp (stack) cho từng ký tự trong chuỗi, cộng thêm ký tự kết thúc \0. Nhưng nó cũng sẽ liên kết địa chỉ của ký tự đầu tiên với biến quote. Mỗi khi biến quote được sử dụng trong mã, máy tính sẽ thay thế nó bằng địa chỉ của ký tự đầu tiên trong chuỗi. Trên thực tế, biến mảng hoạt động y hệt như một con trỏ:
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 28 image 42 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-42.png)
…vậy hóa ra hàm của chúng ta đã được truyền một con trỏ
Đó chính là lý do tại sao điều kỳ lạ kia lại xảy ra trong đoạn mã fortune_cookie(). Mặc dù trông có vẻ như bạn đang truyền một chuỗi (string) vào hàm fortune_cookie(), nhưng thực tế bạn chỉ đang truyền một con trỏ trỏ đến chuỗi đó mà thôi:
Và đó cũng là lý do tại sao toán tử sizeof lại trả về một kết quả kỳ quặc. Nó chỉ đang trả về kích thước của một con trỏ trỏ đến một chuỗi. Trên các hệ điều hành 32-bit, một con trỏ chiếm 4 byte bộ nhớ, và trên các hệ điều hành 64-bit, một con trỏ chiếm 8 byte.
Hỏi: sizeof có phải là một hàm (function) không? | Đáp: Không, nó là một toán tử (operator). |
| Hỏi: Sự khác biệt là gì? | Đáp: Một toán tử được trình biên dịch (compiler) biên dịch thành một chuỗi các chỉ thị. Nhưng nếu đoạn mã gọi một hàm, nó phải nhảy đến một phần mã riêng biệt khác. Nó không phải là một hàm vì: Cú pháp: Đối với các biểu thức (expressions), sizeof không bắt buộc phải có dấu ngoặc đơn (ví dụ: sizeof x là hợp lệ). Hàm luôn yêu cầu dấu ngoặc đơn để truyền đối số.Xử lý: Hàm được thực thi bởi CPU khi chương trình đang chạy. sizeof chủ yếu được xử lý bởi trình biên dịch trong quá trình chuyển đổi mã nguồn thành mã máy. |
Hỏi: Vậy sizeof được tính toán khi chương trình được biên dịch à? | Đáp: Đúng vậy. Trình biên dịch có thể xác định kích thước không gian lưu trữ ngay tại thời điểm biên dịch (compile time). |
Khi bạn viết:
int a = 10;
size_t s = sizeof(a);Trình biên dịch sẽ thấy a là kiểu int. Nếu trên hệ thống đó int chiếm 4 bytes, trình biên dịch sẽ chuyển mã nguồn trên tương đương với việc bạn viết:
int a = 10;
size_t s = 4; // Con số 4 được ghi thẳng vào mã máyTại thời điểm biên dịch (Compile-time)
Trong hầu hết các trường hợp, sizeof được tính toán hoàn toàn tại thời điểm biên dịch.
- Trình biên dịch kiểm tra kiểu dữ liệu (static type) của toán hạng (operand).
- Dựa trên bảng ký hiệu (symbol table) và quy tắc căn chỉnh bộ nhớ (memory alignment) của kiến trúc mục tiêu (ví dụ: x86 hoặc ARM), trình biên dịch xác định số lượng byte mà kiểu dữ liệu đó chiếm dụng.
- Trình biên dịch sau đó thay thế toàn bộ biểu thức
sizeof(...)bằng một hằng số (constant literal) trong mã máy.
Hệ quả: Không có lệnh gọi hàm (function call), không có việc nhảy địa chỉ bộ nhớ, và không tiêu tốn tài nguyên CPU khi chương trình đang chạy để tính toán giá trị này.
Tại thời điểm thực thi (Runtime) – Ngoại lệ
Có một trường hợp duy nhất sizeof được tính toán khi chương trình đang chạy: Mảng có kích thước thay đổi (Variable Length Array – VLA) trong tiêu chuẩn C99. Khi bạn khai báo int arr[n] với n là một biến, trình biên dịch không thể biết n bằng bao nhiêu cho đến khi chương trình chạy. Lúc này, sizeof(arr) sẽ tạo ra mã máy để tính toán kích thước dựa trên giá trị hiện tại của n.
| Hỏi: Tại sao con trỏ lại có kích thước khác nhau trên các máy tính khác nhau? | Đáp: Trên hệ điều hành 32-bit, một địa chỉ bộ nhớ được lưu trữ dưới dạng một số 32-bit. Đó là lý do tại sao nó được gọi là hệ thống 32-bit. 32 bit == 4 byte. Đó cũng là lý do tại sao hệ thống 64-bit lại sử dụng 8 byte để lưu trữ một địa chỉ. |
| Hỏi: Nếu tôi tạo một biến con trỏ, biến con trỏ đó có nằm trong bộ nhớ không? | Đáp: Có. Biến con trỏ thực chất chỉ là một biến dùng để lưu trữ một con số. |
| Hỏi: Vậy tôi có thể tìm địa chỉ của một biến con trỏ không? | Đáp: Có — bằng cách sử dụng toán tử &. |
| Hỏi: Tôi có thể chuyển đổi một con trỏ thành một con số thông thường không? | Đáp: Trên hầu hết các hệ thống là có. Các trình biên dịch C thường làm cho kiểu dữ liệu long có cùng kích thước với một địa chỉ bộ nhớ. Vì vậy, nếu p là một con trỏ và bạn muốn lưu nó vào một biến long tên là a, bạn có thể gõ a = (long)p. |
Nhưng biến mảng không hoàn toàn là con trỏ
Mặc dù bạn có thể sử dụng một biến mảng như một con trỏ, nhưng vẫn có một vài điểm khác biệt. Để thấy sự khác biệt này, hãy suy nghĩ về đoạn mã sau:
char s[] = "How big is it?";
char *t = s;sizeof(một mảng) là… kích thước của một mảng.
Bạn đã thấy rằng sizeof(một con trỏ) trả về giá trị 4 hoặc 8, bởi vì đó là kích thước của con trỏ trên các hệ thống 32-bit và 64-bit. Nhưng nếu bạn gọi sizeof trên một biến mảng, C đủ thông minh để hiểu rằng điều bạn muốn biết là mảng đó lớn cỡ nào trong bộ nhớ.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 29 image 43 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-43.png)
Địa chỉ của mảng… chính là địa chỉ của mảng.
Một biến con trỏ chỉ là một biến lưu trữ một địa chỉ bộ nhớ. Nhưng còn biến mảng thì sao? Nếu bạn sử dụng toán tử & trên một biến mảng, kết quả bằng chính biến mảng đó.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 30 image 44 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-44.png)
Nếu một lập trình viên viết &s, điều đó có nghĩa là “Địa chỉ của mảng s là gì?”. Địa chỉ của mảng s chỉ đơn giản là… s. Nhưng nếu ai đó viết &t, điều đó có nghĩa là “Địa chỉ của biến t là gì?”.
Một biến mảng không thể trỏ đi nơi khác.
Khi bạn tạo một biến con trỏ, máy tính sẽ cấp phát 4 hoặc 8 byte không gian để lưu trữ nó. Nhưng chuyện gì sẽ xảy ra nếu bạn tạo một mảng? Máy tính sẽ cấp phát không gian để lưu trữ mảng, nhưng nó sẽ không cấp phát bất kỳ bộ nhớ nào để lưu trữ bản thân cái “biến” mảng đó. Trình biên dịch chỉ đơn giản là gắn địa chỉ của phần tử bắt đầu của mảng vào đó.
Nhưng bởi vì các biến mảng không có không gian lưu trữ được cấp phát, điều đó có nghĩa là bạn không thể bắt chúng trỏ vào bất cứ thứ gì khác.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 31 image 45 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-45.png)
Tại sao mảng thực sự bắt đầu từ 0
Một biến mảng có thể được sử dụng như một con trỏ trỏ đến phần tử đầu tiên trong một mảng. Điều đó có nghĩa là bạn có thể đọc phần tử đầu tiên của mảng bằng cách sử dụng ký pháp ngoặc vuông hoặc sử dụng toán tử * như thế này:
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 32 image 46 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-46.png)
Nhưng vì một địa chỉ bộ nhớ chỉ là một con số, điều đó có nghĩa là bạn có thể thực hiện phép toán con trỏ (pointer arithmetic) và thực sự cộng thêm các giá trị vào một giá trị con trỏ để tìm địa chỉ tiếp theo. Vì vậy, bạn có thể sử dụng ngoặc vuông để đọc phần tử có chỉ số 2, hoặc bạn chỉ cần cộng 2 vào địa chỉ của phần tử đầu tiên:
printf("3rd order: %i drinks\n",
drinks[2]);
printf("3rd order: %i drinks\n",
*(drinks + 2));
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 33 image 47 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-47.png)
Nói chung, hai biểu thức drinks[i] và *(drinks + i) là hoàn toàn tương đương nhau. Đó là lý do tại sao các mảng bắt đầu bằng chỉ số 0. Chỉ số (index) thực chất chỉ là con số được cộng vào con trỏ để tìm vị trí của phần tử đó.
| Hỏi: Khi nào C điều chỉnh các tính toán của phép toán con trỏ? | Đáp: Nó xảy ra khi trình biên dịch đang tạo ra tệp thực thi (executable). Nó xem xét kiểu dữ liệu của biến và sau đó nhân các dấu cộng và trừ với kích thước của biến cơ sở. Nếu trình biên dịch thấy rằng bạn đang làm việc với một mảng int và bạn đang cộng 2, trình biên dịch sẽ nhân số đó với 4 (chiều dài của một int) và cộng thêm 8. |
Hỏi: C có sử dụng toán tử sizeof khi nó đang điều chỉnh phép toán con trỏ không? | Đáp: Về hiệu quả là có. Toán tử sizeof cũng được giải quyết tại thời điểm biên dịch, và cả sizeof lẫn các phép toán con trỏ sẽ sử dụng cùng các kích thước cho các kiểu dữ liệu khác nhau. |
Tại sao con trỏ có kiểu dữ liệu
Nếu con trỏ chỉ đơn thuần là các địa chỉ, vậy thì tại sao các biến con trỏ lại cần có kiểu dữ liệu (như int*, char*,…)? Tại sao bạn không thể lưu trữ tất cả các con trỏ vào một loại biến con trỏ chung duy nhất nào đó?
Lý do là vì phép toán con trỏ (pointer arithmetic) rất “láu cá”.
Nếu bạn cộng 1 vào một con trỏ kiểu char, con trỏ đó sẽ trỏ đến ngay địa chỉ bộ nhớ tiếp theo. Nhưng đó là bởi vì một ký tự (char) chỉ chiếm đúng 1 byte bộ nhớ.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 34 image 48 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-48.png)
Vậy chuyện gì xảy ra nếu bạn có một con trỏ kiểu int? Các số nguyên (int) thường chiếm 4 byte dung lượng, vì vậy nếu bạn cộng 1 vào một con trỏ kiểu int, mã nguồn sau khi biên dịch thực chất sẽ cộng 4 vào địa chỉ bộ nhớ đó.
int nums[] = {1, 2, 3};
printf("nums đang ở địa chỉ %p\n", nums);
printf("nums + 1 đang ở địa chỉ %p\n", nums + 1);![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 35 image 49 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-49.png)
Nếu bạn chạy đoạn mã này, hai địa chỉ bộ nhớ sẽ cách nhau nhiều hơn một byte. Do đó, các kiểu con trỏ tồn tại để trình biên dịch biết cần phải điều chỉnh bao nhiêu khi thực hiện phép toán con trỏ.
Sử dụng con trỏ để nhập dữ liệu
Cách yêu cầu người dùng nhập một chuỗi ký tự từ bàn phím bằng hàm scanf():
char name[40];
printf("Nhập tên bạn: ");
scanf("%39s", name); // scanf sẽ đọc 39 kí tự cộng thêm string terminatorHàm scanf() hoạt động như thế nào? Nó nhận vào một con trỏ kiểu char (char*), và trong trường hợp này, bạn đang truyền vào một biến mảng. Đến lúc này, chắc bạn đã lờ mờ hiểu tại sao nó lại nhận một con trỏ rồi chứ? Đó là vì hàm scanf() sẽ cập nhật nội dung của mảng đó. Những hàm cần cập nhật giá trị của một biến thì không cần giá trị của biến đó — chúng cần địa chỉ của nó.
Nhập số bằng scanf() (Entering numbers with scanf())
Vậy làm thế nào để nhập dữ liệu vào một trường số? Bạn thực hiện bằng cách truyền một con trỏ đến một biến số.
int age;
printf("Nhập tuổi của bạn: ");
scanf("%i", &age);Bởi vì bạn truyền địa chỉ của biến số vào hàm, scanf() có thể cập nhật nội dung của biến đó. Và để giúp bạn, bạn có thể truyền một chuỗi định dạng (format string) chứa các mã định dạng tương tự như loại bạn dùng trong hàm printf(). Bạn thậm chí có thể dùng scanf() để nhập nhiều thông tin cùng lúc:
int lanh_su, vi_do;
scanf("%i %i", &lanh_su, &vi_do);Có một… vấn đề nho nhỏ với hàm scanf(). Từ nãy đến giờ, tất cả các đoạn mã bạn viết đều rất cẩn thận đặt giới hạn cho số lượng ký tự mà scanf() sẽ đọc vào:
scanf("%39s", name);
Tại sao lại phải làm vậy? Suy cho cùng, scanf() dùng chung kiểu chuỗi định dạng như printf(), mà khi chúng ta in một chuỗi bằng printf(), ta chỉ dùng %s thôi mà.
À, nếu bạn chỉ dùng %s trong scanf(), rắc rối sẽ nảy sinh nếu ai đó cao hứng gõ quá nhiều:
char food[5];
printf("Nhập món ăn yêu thích: ");
scanf("%s", food); // Cực kỳ nguy hiểm!
printf("Món yêu thích là: %s\n", food);![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 36 image 50 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-50.png)
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 37 image 51 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-51.png)
Chương trình sẽ bị crash (sập). Lý do là vì scanf() ghi dữ liệu vượt xa khỏi phạm vi bộ nhớ được cấp phát cho mảng food.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 38 image 52 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-52.png)
scanf() có thể gây ra lỗi tràn bộ đệm (scanf() can cause buffer overflows)
Nếu bạn quên giới hạn độ dài của chuỗi khi đọc bằng scanf(), bất kỳ người dùng nào cũng có thể nhập vào lượng dữ liệu lớn hơn nhiều so với không gian lưu trữ của chương trình. Phần dữ liệu thừa đó sẽ bị ghi đè vào những vùng bộ nhớ vốn không được máy tính cấp phát cho mục đích đó.
Nếu may mắn, dữ liệu đó chỉ đơn giản là được lưu trữ ở đó và không gây ra chuyện gì. Nhưng rất có khả năng lỗi tràn bộ đệm (buffer overflow) sẽ gây ra các lỗi nghiêm trọng (bugs). Nó có thể được gọi là segmentation fault hoặc abort trap, nhưng dù thông báo lỗi hiện ra là gì, kết quả cuối cùng vẫn là chương trình của bạn bị “ngỏm”.
fgets() là một sự thay thế cho scanf()
Có một hàm khác mà bạn có thể sử dụng để nhập dữ liệu văn bản: fgets(). Giống như hàm scanf(), nó nhận một con trỏ kiểu char, nhưng không giống scanf(), hàm fgets() bắt buộc phải được cung cấp độ dài tối đa:
char food[5];
printf("Nhập món ăn yêu thích: ");
fgets(food, sizeof(food), stdin);Điều đó có nghĩa là bạn không thể “vô tình” quên thiết lập độ dài khi gọi fgets(); nó nằm ngay trong chữ ký hàm như một đối số bắt buộc. Ngoài ra, hãy lưu ý rằng kích thước bộ đệm của fgets() đã bao gồm cả ký tự kết thúc \0. Vì vậy, bạn không cần phải trừ đi 1 đơn vị độ dài như khi làm với scanf().
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 39 image 53 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-53.png)
Sử dụng sizeof với fgets()
Đoạn mã trên thiết lập độ dài tối đa bằng toán tử sizeof.
sizeoftrả về lượng không gian bị chiếm dụng bởi một biến.
- Trong đoạn mã trên,
foodlà một biến mảng, vì vậysizeoftrả về kích thước của mảng đó (là 5). - Nếu
foodchỉ là một biến con trỏ đơn thuần, toán tửsizeofsẽ chỉ trả về kích thước của một con trỏ (thường là 4 hoặc 8 byte tùy hệ điều hành).
Nếu bạn biết chắc mình đang truyền một biến mảng vào hàm fgets(), thì việc dùng sizeof là ổn. Nếu bạn chỉ truyền một con trỏ đơn giản, bạn nên nhập trực tiếp kích thước mà bạn muốn:
fgets(con_tro_chuoi, 20, stdin);Hàm fgets() thực chất bắt nguồn từ một hàm cũ hơn gọi là gets().
Mặc dù fgets() được xem là hàm an toàn hơn scanf(), sự thật là hàm gets() cũ còn nguy hiểm khủng khiếp hơn cả hai hàm kia cộng lại. Lý do ư? Hàm gets() hoàn toàn không có giới hạn nào cả:
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 40 image 54 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-54.png)
| Tiêu chí | scanf() | fgets() |
| Giới hạn | Có thể giới hạn dữ liệu, miễn là bạn nhớ thêm kích thước vào chuỗi định dạng. | Có giới hạn bắt buộc. Không gì có thể lọt qua được nó. |
| Nhiều trường | Có! Cho phép nhập nhiều trường dữ liệu có cấu trúc (ví dụ: ngày/tháng/năm). | fgets() chỉ cho phép nhập một chuỗi duy nhất vào bộ đệm. |
| Khoảng trắng | %s sẽ dừng lại ngay khi gặp dấu cách. Nhập “Pizza Bò” chỉ lấy được “Pizza”. | fgets() có thể đọc toàn bộ chuỗi bao gồm cả khoảng trắng. |
- stdin: Bạn có để ý đối số thứ ba của
fgetslàstdinkhông? Nó viết tắt của “standard input”, nghĩa là lấy dữ liệu từ bàn phím.
- Ký tự
\n: Một điểm cực kỳ quan trọng làfgets()thường đọc luôn cả ký tự xuống dòng (khi bạn nhấn Enter) vào trong chuỗi. Điều này khác vớiscanf().
Hằng chuỗi không bao giờ có thể cập nhật được
Một biến trỏ đến một hằng chuỗi (string literal) không thể được dùng để thay đổi nội dung của chính chuỗi đó:
char *cards = "JQK";(Biếncardstrỏ đến một hằng chuỗi – KHÔNG THỂ THAY ĐỔI)
Nhưng nếu bạn tạo một mảng (array) từ một hằng chuỗi, khi đó bạn có thể sửa đổi nó:
char cards[] = "JQK";(Đây là một mảng chứa bản sao của chuỗi – CÓ THỂ THAY ĐỔI)
Tất cả đều nằm ở cách mà ngôn ngữ C sử dụng bộ nhớ…
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 41 image 55 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-55.png)
Trong bộ nhớ: char *cards = "JQK" Để hiểu tại sao dòng mã này lại gây ra lỗi bộ nhớ, chúng ta cần đào sâu vào bộ nhớ của máy tính và xem chính xác máy tính sẽ làm gì.
- Máy tính nạp hằng chuỗi: Khi máy tính nạp chương trình vào bộ nhớ, nó đặt tất cả các giá trị hằng số — như hằng chuỗi
"JQK"— vào khối bộ nhớ hằng số (constant memory block). Phần bộ nhớ này là chỉ đọc (read-only). - Chương trình tạo biến
cardstrên ngăn xếp (stack): Ngăn xếp là phần bộ nhớ mà máy tính sử dụng cho các biến cục bộ: các biến nằm bên trong hàm. Biếncardssẽ nằm ở đây. - Biến
cardsđược gán địa chỉ của"JQK": Biếncardssẽ chứa địa chỉ của hằng chuỗi"JQK". Các hằng chuỗi thường được lưu trữ trong bộ nhớ chỉ đọc để ngăn chặn bất kỳ ai thay đổi chúng. - Máy tính cố gắng thay đổi chuỗi: Khi chương trình cố gắng thay đổi nội dung của chuỗi mà biến
cardsđang trỏ tới, nó không thể thực hiện được; chuỗi đó là chỉ đọc.
String literal (Hằng chuỗi): Là một chuỗi ký tự được viết trực tiếp trong mã nguồn nằm trong dấu ngoặc kép (ví dụ: "JQK"). Trong C, các hằng chuỗi này được lưu ở một vùng nhớ đặc biệt dành riêng cho dữ liệu không đổi.
Segmentation Fault (Lỗi phân đoạn): Thường là kết quả của việc cố gắng ghi dữ liệu vào vùng nhớ “chỉ đọc”, đây chính là lỗi mà đoạn code trên gặp phải.
Nếu bạn định thay đổi một chuỗi, hãy tạo một bản sao
Sự thật là nếu bạn muốn thay đổi nội dung của một chuỗi, bạn cần phải thao tác trên một bản sao. Nếu bạn tạo ra một bản sao của chuỗi đó trong một vùng nhớ không phải là “chỉ đọc”, sẽ chẳng có vấn đề gì xảy ra khi bạn cố gắng thay đổi các ký tự bên trong nó.
Nhưng làm thế nào để tạo một bản sao?
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 42 image 56 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-56.png)
Có lẽ bạn chưa thấy rõ tại sao điều này lại thay đổi được mọi thứ. Tất cả các chuỗi đều là mảng. Nhưng trong đoạn mã cũ, cards chỉ là một con trỏ. Trong đoạn mã mới, nó là một mảng. Nếu bạn khai báo một mảng tên là cards và sau đó gán cho nó một hằng chuỗi, mảng cards sẽ là một bản sao hoàn toàn mới. Biến đó không chỉ đơn thuần là trỏ vào hằng chuỗi nữa.
Sự thật là nếu bạn muốn thay đổi nội dung của một chuỗi, bạn cần phải thao tác trên một bản sao. Nếu bạn tạo ra một bản sao của chuỗi đó trong một vùng nhớ không phải là “chỉ đọc”, sẽ chẳng có vấn đề gì xảy ra khi bạn cố gắng thay đổi các ký tự bên trong nó.
Nhưng làm thế nào để tạo một bản sao? Ồ, đơn giản thôi, chỉ cần tạo chuỗi đó dưới dạng một mảng mới.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 43 image 57 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-57.png)
Có lẽ bạn chưa thấy rõ tại sao điều này lại thay đổi được mọi thứ. Tất cả các chuỗi đều là mảng. Nhưng trong đoạn mã cũ, cards chỉ là một con trỏ. Trong đoạn mã mới, nó là một mảng. Nếu bạn khai báo một mảng tên là cards và sau đó gán cho nó một hằng chuỗi, mảng cards sẽ là một bản sao hoàn toàn mới. Biến đó không chỉ đơn thuần là trỏ vào hằng chuỗi nữa. Nó là một mảng mới toanh chứa một bản sao “tươi rói” của hằng chuỗi đó.
Để thấy điều này hoạt động như thế nào trong thực tế, bạn cần nhìn vào những gì xảy ra trong bộ nhớ.
![[A.I System Programming] B2: Tìm hiểu C Memory/Pointer 44 image 58 - quochung.cyou PTIT](https://quochung.cyou/wp-content/uploads/2026/03/image-58.png)
Trong bộ nhớ: char cards[] = "JQK"
- Máy tính nạp hằng chuỗi: Như trước đó, khi máy tính nạp chương trình vào bộ nhớ, nó lưu trữ các giá trị hằng số — như chuỗi
"JQK"— vào bộ nhớ chỉ đọc (read-only memory). - Chương trình tạo một mảng mới trên ngăn xếp (stack): Vì chúng ta đang khai báo một mảng, chương trình sẽ tạo ra một mảng đủ lớn để chứa chuỗi
"JQK"— tương đương với kích thước của 4 ký tự (bao gồm cả ký tự kết thúc\0). - Chương trình khởi tạo mảng: Bên cạnh việc cấp phát không gian, chương trình cũng sẽ sao chép nội dung của hằng chuỗi
"JQK"vào bộ nhớ ngăn xếp (stack).
Vì vậy, sự khác biệt là: Đoạn mã gốc sử dụng một con trỏ để trỏ đến một hằng chuỗi chỉ đọc. Nhưng nếu bạn khởi tạo một mảng bằng một hằng chuỗi, bạn sẽ có một bản sao của các ký tự đó, và bạn có thể thay đổi chúng tùy thích.
Một cách để tránh vấn đề này trong tương lai là đừng bao giờ viết mã gán một con trỏ char đơn thuần cho một giá trị hằng chuỗi như:
char *s = "Some string";
Thực ra không có gì sai khi gán một con trỏ cho một hằng chuỗi — vấn đề chỉ xảy ra khi bạn cố gắng sửa đổi hằng chuỗi đó. Thay vào đó, nếu bạn muốn gán một con trỏ cho một hằng số, hãy luôn đảm bảo bạn sử dụng từ khóa const:
const char *s = "some string";
Bằng cách đó, nếu trình biên dịch thấy đoạn mã nào cố gắng sửa đổi chuỗi, nó sẽ báo lỗi biên dịch ngay lập tức:
s[0] = 'S';monte.c:7: error: assignment of read-only location
- Stack (Ngăn xếp): Là vùng nhớ dành cho các biến cục bộ. Vùng nhớ này có thể đọc và ghi thoải mái, đó là lý do tại sao khi copy chuỗi sang đây, bạn có thể sửa đổi được.
- const keyword: Đây là một “lời hứa” với trình biên dịch rằng: “Tôi sẽ không thay đổi giá trị này”. Nếu bạn vi phạm, trình biên dịch sẽ nhắc nhở bạn ngay thay vì để chương trình bị sụp đổ (crash) khi đang chạy.
| Hỏi: Tại sao trình biên dịch không báo luôn là tôi không được phép thay đổi chuỗi đó? | Đáp: Bởi vì chúng ta khai báo cards là một con trỏ char * đơn thuần, trình biên dịch không thể biết chắc chắn rằng biến đó sẽ luôn trỏ vào một hằng chuỗi trong suốt quá trình chạy. |
| Hỏi: Tại sao các hằng chuỗi lại được lưu trong bộ nhớ chỉ đọc? | Đáp: Vì chúng được thiết kế để làm hằng số. Nếu bạn viết một hàm để in chữ “Hello World”, bạn chắc chắn không muốn một phần khác của chương trình vô tình sửa nó thành “Goodbye World” đâu đúng không? |
| Hỏi: Có phải mọi hệ điều hành đều thực thi quy tắc “chỉ đọc” này không? | Đáp: Đại đa số là có. Một số phiên bản của gcc trên Cygwin thực tế cho phép bạn sửa đổi hằng chuỗi mà không phàn nàn gì. Nhưng hãy nhớ: làm vậy luôn luôn là sai. |
Hỏi: const thực sự có nghĩa là gì? Nó có làm cho chuỗi trở thành “chỉ đọc” không? | Đáp: Bản thân các hằng chuỗi đã là chỉ đọc rồi. Từ khóa const là để trình biên dịch sẽ “la làng” lên nếu bạn cố tình dùng biến đó để sửa đổi mảng/chuỗi, giúp bạn phát hiện lỗi sớm ngay khi viết code. |
Bộ nhớ ghi nhớ (Memory Memorizer)
- Ngăn xếp (Stack): Đây là vùng nhớ dành cho biến cục bộ. Mỗi khi bạn gọi một hàm, các biến cục bộ của hàm đó được tạo ra trên stack. Nó giống như một chồng đĩa: biến được thêm vào khi vào hàm và lấy ra khi thoát hàm. Điều kỳ lạ là stack thực sự hoạt động “ngược”: nó bắt đầu từ đỉnh bộ nhớ và phát triển dần xuống dưới.
- Heap: Đây là vùng nhớ dành cho bộ nhớ động: những dữ liệu được tạo ra khi chương trình đang chạy và tồn tại trong thời gian dài.
- Biến toàn cục (Globals): Là biến nằm ngoài tất cả các hàm và mọi hàm đều nhìn thấy. Chúng được tạo ra ngay khi chương trình bắt đầu chạy và bạn có thể cập nhật chúng thoải mái.
- Hằng số (Constants): Cũng được tạo ra khi chương trình bắt đầu, nhưng được lưu ở vùng nhớ chỉ đọc. Đây là nơi chứa các hằng chuỗi.
- Mã lệnh (Code): Cuối cùng là phân đoạn mã. Nhiều hệ điều hành đặt mã lệnh ở địa chỉ thấp nhất. Đây là nơi chứa mã máy thực sự sau khi biên dịch và nó cũng là chỉ đọc.