Tham chiếu và con trỏ trong C++
Tại sao hôm nay có bài này?
Mình chẳng bao giờ code C, C++ nên chẳng bao giờ nhớ được các khái niệm về con trỏ. Nên mình không thích con trỏ tí nào cả. Nhưng khổ cái này trong một số trường hợp mình lại vẫn muốn hiểu nó, mà do không làm việc với C, C++ (và sau này có Golang) nên mình lại rất hay “quên”. Nên tổng hợp lại để khi nào quên thì vô đọc lại.
1. Hàm, đối số, tham số
Ví dụ ta có 1 hàm
Ta có 2 tham số (parameters) là a và b. Giả sử trong hàm main, gọi hàm foo
Ta có 2 đối số (arguments) là 1 và 2. Ta nói giá trị 1 và 2 được lưu tạm trong 2 tham số là a và b.
Trong C++ hỗ trợ nhiều kiểu truyền đối số khác nhau, tương ứng khai báo tham số trong hàm sẽ khác nhau:
- Truyền đối số là giá trị như phía trên.
- Truyền đố số là tham chiếu.
- Truyền đối số là địa chỉ.
1.1 Truyền đối số là giá trị
Đối số trong hàm con foo(int a, int b)
là a và b có vai trò là biến cục bộ, chỉ hoạt động bên trong hàm foo()
, khi hàm này kết thúc các tham số này sẽ bị hủy và giá trị truyền vào sẽ không tồn tại.
Mọi thay đổi của tham số bên trong hàm, không có tác động gì đến giá trị gốc được truyền vào ngoài hàm. Ví dụ
Ta được kết quả, như ta thấy giá trị của đối số a không thay đổi sau khi gọi hàm foo()
Điều này có nghĩa là cái mà tham số p tiếp nhận chỉ là bản sao giá trị của đối số a. Thử in địa chỉ của đối số và tham số:
Ta được kết quả:
Như ta thấy, đối số và tham số có địa chỉ khác nhau, dù có cùng giá trị.
1.2 Truyền đối số cho hàm là tham chiếu
Nhược điểm của truyền đối số cho hàm là giá trị là
- Cách duy nhất để có được đầu ra là phải trả về giá trị thông qua từ khóa
return
. - Hàm chỉ trả về một giá trị duy nhất cho mỗi lần gọi hàm.
Trong khi các ngôn ngữ bậc cao như python thì việc trả về nhiều giá trị rất đơn giản. Thực hiện truyền đối số cho hàm là tham chiếu giúp ta khắc phục nhược điểm trên.
Biến tham chiếu (reference variable) được xem như một tên khác của một biến nào đó có cùng kiểu dữ liệu. Sau khi được khai báo và khởi tạo, nó dùng chung vùng nhớ với biến mà nó tham chiếu tới.
Đặc điểm của biến tham chiếu là khi một trong hai biến bị hủy (biến tham chiếu hoặc biến gốc) do ra khỏi khối lệnh được khai báo, vùng nhớ của 2 biến này vẫn chưa bị hủy nếu còn ít nhất 1 biến quản lý.
Như ở ví dụ trên, biến temp
sau khi ra khỏi khối lệnh đã bị hủy, tuy nhiên vẫn tồn tại biến ref
kiểm soát vùng nhớ của biến temp
nên kết quả sẽ là:
Bây giờ ta có thể định nghĩa một hàm không có gía trị trả về (không có return
) tương tự như hàm có kết quả trả về.
Đều có kết quả trả về là:
Bây giờ lợi dụng đặc điểm này, ta có thể viết một hàm trả về hai giá trị mà không dùng return
, ví dụ tính diện tích và chu vi hình tròn với đầu vào là bán kính.
2. Con trỏ
Variable (biến) là một ô nhớ hoặc một vùng nhớ được hệ điều hành cấp phát cho chương trình dùng để lưu giá trị vào ô nhớ.
Để truy xuất giá trị của biến đang nắm giữ, chương trình cần tìm tới địa chỉ của biến và đọc giá trị. Thông thường ta không cần quan tâm tới địa chỉ vùng nhớ, ta chỉ cần gọi định danh của biến để lấy ra giá trị cần.
Chương trình không thể truy xuất trực tiếp vào vùng nhớ vật lý mà chỉ truy xuất trên địa chỉ của vùng nhớ ảo, hệ điều hành sẽ quản lý một page-table để mapping một địa chỉ ảo sang địa chỉ vật lý. Còn việc truy xuất tới vùng nhớ vật lý từ vùng nhớ ảo sẽ do phần cứng MMU (memory management unit) quản lý.
Ví dụ giá trị của biến và địa chỉ của biến
Tham chiếu thì sao nhỉ?
2.1 Toán tử trỏ đến
Ta có toán tử trỏ đến (dereference operator) được kí hiệu bởi dấu *
cho phép lấy giá trị của vùng nhớ có địa chỉ cụ thế. Ví dụ ta biết địa chỉ của biến x là 0x7ffeecb835c8
, giờ thay vì gọi định danh để lấy giá trị, ta gọi địa chỉ để lấy giá trị.
Kết quả đều giống nhau
Ngoài việc dùng để truy xuất giá trị trong vùng nhớ có địa chỉ cụ thể, toán tử trỏ đến còn dùng để thay đổi giá trị của vùng nhớ như cách ta xài định danh
2.2 Con trỏ
Nhắc lại là biến tham chiếu thì có cùng địa chỉ với biến mà nó tham chiếu tới. Địa chỉ của &x
giống địa chỉ của x
.
Con trỏ khác với biến tham chiếu là nó là biến độc lập nên có địa chỉ khác với vùng nhớ mà nó trỏ tới. Nhưng giá trị bên trong của con trỏ lại chính là địa chỉ của biến mà nó trỏ tới.
- Địa chỉ của con trỏ là
&(*x)
sẽ khác địa chỉ của biến x là&(x)
. - Giá trị của con trỏ
*x
chính là địa chỉ của biến x là&(x)
.
Khai báo và sử dụng
Ở trên ta khai báo kiểu dữ liệu của con trỏ chính là kiểu dữ liệu của biến mà ta muốn trỏ tới, và vì con trỏ lưu địa chỉ vùng nhớ của biến mà nó trỏ tới nên ta cần dùng address-of operator để lấy địa chỉ của biến value
trước khi gán cho con trỏ.
Bây giờ ta thử in ra địa chỉ vùng nhớ của con trỏ và địa chỉ vùng nhớ của biến mà con trỏ trỏ tới sẽ thấy là 2 giá trị khác nhau, như lúc đầu tiên nói.
Ok, cuối cùng quay trở lại với phép toán dereference operator, phép toán cho phép truy xuất giá trị của biến thông qua địa chỉ vùng nhớ của biến đó thay vì sử dụng định danh.
Kết quả tất nhiên là như nhau:
3. Ref
Bài viết tham khảo rất nhiều từ khóa học C++ Free của https://cpp.daynhauhoc.com, tuy nhiên với một người không làm chuyên về C++, mình lược bỏ một số phần theo mình là không cần thiết với mình cho đơn giản, dễ hiểu.