Con trỏ là một khái niệm sử dụng rất nhiều khi học về cấu trúc dữ liệu và giải thuật, phần cây hoặc danh sách liên kết. Ở đây mình sẽ nói sâu vào khái niệm này.(lưu ý đây là bài viết sâu về kỹ thuật nên nếu không hiểu thì hỏi chứ không được phàn nàn sao nó quá khó hiểu nhé )
Trước tiên là nói về bộ nhớ:
– Bộ nhớ trong máy tính là một dải các ô nhớ liên tiếp, mỗi ô 1 byte sắp theo thứ tự, kiểu như mảng byte trong C vậy. Ô đầu tiên có thứ tự 0, và vì mình sẽ tìm đến một ô cụ thể nhờ vào thứ tự của nó, nên nói cách khác là ô đầu tiên có địa chỉ 0, các ô kế tiếp có địa chỉ tăng dần 1,2,3…
– Vậy địa chỉ trong bộ nhớ chính là 1 số nguyên không âm, mỗi địa chỉ sẽ chỉ đến 1 byte duy nhất trong bộ nhớ.
– Tất cả dữ liệu muốn được CPU xử lý đều phải được tải vào bộ nhớ, khi cần cộng trừ nhân chia, so sánh các kiểu thì đưa địa chỉ ô nhớ chứa dữ liệu rồi yêu cầu CPU xử lý. Các lệnh cũng vậy, muốn chạy được thì phải đọc vào bộ nhớ sau đó yêu cầu CPU thực hiện 1 câu lệnh nào đó bằng cách đưa địa chỉ cho nó.
* Ta đưa địa chỉ cho CPU bằng cách lưu địa chỉ đó vào 1 vùng nhớ đặc biệt, người ta gọi vùng nhớ đó là register (thanh ghi). Với mỗi dòng CPU thì chỉ có 1 số cố định thanh ghi. Kích thước thanh ghi sẽ quyết định ta có thể lưu vào số số lớn đến đâu, hay nói cách khác ta có thể yêu cầu CPU xử lý dữ liệu ở một vị trí có địa chỉ lớn đến đâu trong bộ nhớ.
– Kích thước phổ biến hiện tại của các thanh ghi CPU là 64 bit, có nghĩa là các CPU này có thể truy xuất đến các ô nhớ có địa chỉ lớn nhất tới 0 – (2 mũ 64)-1, các CPU xưa thì 32 bit, hoặc 16 bit. Từ đây ta phân biệt ra CPU 64bit và 32 bit. Lưu ý là do tính tương thích ngược nên các CPU 64 bit vẫn hỗ trợ các thanh ghi 32 bit, hoặc 16 bit, ví dụ với các CPU Intel 64 bit, thanh ghi để chỉ cho CPU biết địa chỉ lệnh tiếp theo cần thực hiện là RIP, nhưng nó vẫn tồn tại thanh ghi EIP (32bit), và IP (16bit).
Con trỏ là một loại biến đặc biệt, bản chất của nó là 1 biến chứa địa chỉ của 1 vùng nhớ. Chính vì chứa địa chỉ 1 vùng nhớ nên nó có kích cỡ bằng đúng kích thước thanh ghi của CPU, và nó cũng dùng để chứa 1 số nguyên không âm.- Khi ta nói con trỏ p trỏ đến biến x (p = &x), có nghĩa là biến p chứa địa chỉ của biến x, biến x lớn tới đâu thì p cũng chỉ có kích thước bằng 1 thanh ghi, hay nói cách khác cái nhà to đến đâu thì biển số nhà cũng giống nhau. Việc thay đổi giá trị của x chẳng ảnh hưởng tới p.- Khi ta muốn lấy giá trị của vùng nhớ do p trỏ đến ta viết *p, vì p = &x, nên *p chính là x.
– p có thể trỏ đến bất kỳ vị trí vào, cho dù đó là 1 biến tĩnh, một biến trong stack hay được cấp phát trên heap (dùng các hàm kiểu như alloc, malloc, new…), nhưng điều đó không đồng nghĩa với việc bạn luôn đọc được nội dung nơi nó trỏ tới (không phải lúc nào bạn có địa chỉ nhà thì cũng có thể xem trong nhà đó có gì).
Khi bạn xây dựng các cấu trúc như danh sách liên kết hay cây, sử dụng con trỏ để cấp phát động là một cách vô cùng hữu hiệu, vì nó cho phép bạn chỉ lấy đúng khối lượng bộ nhớ cần thiết, và có thể trả lại khi không dùng nữa. Bởi việc cấp phát bộ nhớ động sẽ trả về 1 vùng nhớ có địa chỉ không xác định trước (từ trước khi xin cấp phát bạn không thể biết nó sẽ ở đâu), nên bạn phải có 1 cách để lưu lại địa chỉ đó bằng con trỏ để sử dụng về sau.
Cấu trúc dữ liệu là một môn rất hay để bạn hiểu được cách máy tính hoạt động. Dù trong thực tế hầu như các cấu trúc dữ liệu và thuật toán bạn được học đều có sẵn trong các thư viện. Nhưng hiểu cách chúng hoạt động sẽ là cách học “bền vững” để bạn có thể học sâu hơn và đi xa hơn với công việc lập trình.
2 thoughts on “CÂU CHUYỆN VỀ CON TRỎ (POINTER)”