Tham trị, Tham chiếu và Con trỏ


Trong lập trình, tham trị, tham chiếu và con trỏ là ba phương thức khác nhau để truyền dữ liệu trong toàn bộ chương trình. Nắm được chắc chắn những khái niệm này và sử dụng một cách thuần thục sẽ là bàn đạp vững chắc đến trên con đường trở thành lập trình viên giỏi. Ở bài viết này, mình sẽ giải thích ba khái niệm này một cách dễ hiểu nhất kèm theo những ví dụ cụ thể.

Mình sẽ dùng C++ cho những ví dụ minh họa trong bài

Giới thiệu

Khi sử dụng hàm (function) trong lập trình. Ta có quyền lựa chọn có truyền tham số cho hàm hay không.


Ngoài việc xác định bao những tham số phù hợp cho hàm, một việc quan trọng cần phải cân nhắc nữa đó là phương pháp truyền tham số đó. Chúng ta có ba phương pháp: 
  1. Tham trị
  2. Tham chiếu
  3. Con trỏ

Tham trị

Tham trị là cách được sử dụng phổ biến trong lập trình. Tham trị chỉ đơn giản là sao chép giá trị được đưa vào.
Như bạn có thể thấy trong hình minh họa, hai biến "Tham số truyền vào" và "Biến số trong hàm" tuy có cùng giá trị nhưng thực chất lại là hai biến khác nhau nằm trên hai địa chỉ khác nhau. Vì vậy, khi giá trị của một trong hai biến thay đổi thì cũng không ảnh hưởng đến giá trị của biến còn lại.

Chúng ta có thể thấy được biểu hiện của chúng thông qua một hàm đổi giá trị biến đơn giản. 

Khi chạy chương trình, ta được kết quả ở dưới
Rõ ràng, những biến ở trong hàm đã đổi giá trị của chúng cho nhau. Thế nhưng, khi hàm kết thúc, các tham số mà ta truyền vào vẫn không thay đổi.
Như mình đã giải thích trước đó, chúng thật ra là hai biến khác nhau, "arg1" chỉ mang giá trị giống với "var1" chứ không thực chất là "var1". Vì vậy, dù bạn có làm gì "arg1" thì "var1" cũng không bị ảnh hưởng.

Đây là khái niệm dễ hiểu nhầm trong lập trình. Mình thấy có nhiều trường hợp các bạn cố gắng thay đổi giá trị của tham số truyền vào trong hàm nhưng lại không để ý là đang truyền tham trị hay tham số. Từ đó, xảy đến các lỗi logic không đáng có.

Tham chiếu (Reference)

Vậy, để hàm đổi giá trị của chúng ta hoạt động thì phải làm như thế nào?

Bằng cách sử dụng phương pháp truyền tham chiếu, ta không kêu chương trình sao chép giá trị tham số truyền vào mà sử dụng chính biến số đó để tương tác trong hàm.

Có thể hình dung tham chiếu chỉ là việc gắn thêm một tên gọi khác cho tham số được truyền vào.
Hai biến thực chất là một, chỉ là bây giờ bạn có thể truy cập đến biến này bằng hai tên khác nhau. Giống như việc đặt nickname cho biến vậy. Nhờ vậy, ta có thể dễ dàng chỉnh sửa tùy ý biến của chúng ta trong hàm và việc chỉnh sửa này cũng sẽ có tác động đến tham số bên ngoài.

Sử dụng phương thức truyền tham số trong C++ khá đơn giản, bạn chỉ cần thêm ký tự "&" trước tên biến mà bạn muốn sử dụng phương pháp truyền tham chiếu trong khai báo hàm.


Khi chạy chương trình, kết quả sẽ đúng như những gì ta mong đợi
Cuối cùng hàm swap của ta cũng hoạt động. Phew~

Bonus
Thật ra tham chiếu không chỉ dùng trong tham số của hàm mà bạn hoàn toàn có thể tạo một biến là tham chiếu tới biến khác. Với cú pháp:
<Kiểu dữ liệu> &<Tên biến>=<Tên biến được tham chiếu>

Cách hoạt động của biến tham chiếu này cũng tương tự như khái niệm chúng ta đã trình bày trước đó. Nó chỉ đóng vai trò như một "nickname" của biến được tham chiếu tới.

Con trỏ (Pointer)

Con trỏ là một trong những khái niệm mà cá nhân mình thấy rất thú vị. Đồng thời, đây là khái niệm chỉ có ở ngôn ngữ C++. Con trỏ là một hinh ảnh trừu tượng mà các lập trình viên gọi những biến chứa giá trị là địa chỉ của biến khác. Trước khi đi vào sâu hơn, ta cần biết "địa chỉ của biến" ở đây có nghĩa là gì đã.

Địa chỉ vùng nhớ (Address) 

Khi chúng ta khai báo biến trong chương trình, thật ra chúng ta đang yêu cầu máy cấp phát một vùng nhớ để ta có thể lưu trữ giá trị. Và để máy tính có thể dễ dàng kiểm soát các vùng nhớ, mỗi vùng nhớ được gán cho một địa chỉ cụ thể. Bạn có thể sử dụng toán tử "&" để biết địa chỉ vùng nhớ đang được cấp phát.
Chương trình trên khai báo một biến số rồi xuất ra địa chỉ vùng nhớ được cấp phát cho biến đó. Bạn có thể tự chạy chương trình để kiểm chứng. Do cách thức hoạt động của bộ nhớ cấp phát ngẫu nhiên (Random Access Memory hay còn gọi là RAM) nên mỗi lần chạy, chương trình sẽ trả về địa chỉ vùng nhớ khác nhau

Ví dụ, khi lần đầu chạy chương trình trên, mình có được
Nhưng ở lần thứ 2 là

Khái niệm con trỏ

Như đã nói, khi ta sử dụng một biến để lưu địa chỉ của biến khác, tức là ta đang sử dụng con trỏ. Thông qua địa chỉ đó, ta có thể dễ dàng truy cập giá trị của biến được trỏ đến. Bạn có thể hình dung nó như một mũi tên trỏ (point) tới vùng nhớ của một biến khác.

Khai báo và sử dụng con trỏ

Để có thể bắt đầu sử dụng con trỏ, ta thêm dấu "*" trước tên biến, nhưng cách gán giá trị cho con trỏ sẽ hơi khác một chút. Cú pháp của khai báo con trỏ là:
<Kiểu dữ liệu> *<Tên biến>=<Địa chỉ của biến được trỏ tới>
Ta cũng có thể khai báo bằng cách khác, chậm rãi và dễ hiểu hơn

Hai cách trên, cách nào bạn sử dụng cũng đều ổn cả.
Giá trị mà con trỏ giữ sẽ là giá trị địa chỉ vùng nhớ. Để có thể lấy được giá trị trong địa chỉ đó, ta cần sử dụng dấu "*" trước biến con trỏ.
Chạy chương trình, ta hiển nhiên thấy, giá trị trong con trỏ  và địa chỉ vùng nhớ là một. Đồng thời, thông qua con trỏ, ta cũng lấy được giá trị trong vùng nhớ đó tới.
Tương tự tham chiếu, con trỏ cũng cho phép ta tương tác với giá trị trong vùng nhớ và thay đổi giá trị đó. Ta cũng có thể sử dụng con trỏ như một tham số của hàm giống như tham chiếu vậy (nhớ thay dấu "&" thành "*" nhé).

Tại sao lại sử dụng con trỏ?

Nếu con trỏ cho những chức năng như tham chiếu thì tại sao chúng ta không sử dụng mỗi tham chiếu thôi?

Thông qua thực tế, ứng dụng của con trỏ giúp ích được rất nhiều trong lập trình. Như việc quản lý biến ở vùng nhớ Heap (mà mình sẽ có một bài viết về các vùng nhớ để giải thích) hay dễ dàng thay đổi vị trí trỏ tới khiến con trỏ linh động hơn tham chiếu (tham chiếu một khi đã gán giá trị thì không thay đổi được). Bạn hoàn toàn có thể viết lại hàm swap ở trên thay vì đổi giá trị trong vùng nhớ, ta có thể hoán đổi vùng nhớ mà hai con trỏ trỏ tới. Mình sẽ để lại việc thực hiện cho các bạn.

Kết thúc

Vậy là chúng ta đã đi qua ba phương pháp để truyền dữ liệu xung quanh chương trình. Riêng về con trỏ, đồng thời cũng là cách lưu trữ dữ liệu mới. Mong các bạn tìm được nhiều điều hữu ích thông qua bài viết. 

Nhận xét

Bài đăng phổ biến từ blog này

Phép phân tích ma trận A=LU

Độc lập tuyến tính và phụ thuộc tuyến tính

Thuật toán tính lũy thừa nhanh. Giải thích một cách đơn giản