Nội dung bài viết
La Kien Vinh Một trong những nỗi đau nhức nhói khá lớn cho những người mới bắt đầu với C++ đó là khó phân biệt được khái niệm tham chiếu và con trỏ. Trong bài viết này, tôi sẽ diễn đạt sự khác biệt theo 1 hướng tiếp cận cực kỳ dễ hiểu. Nên lưu ý rằng, khi bàn về tham chiếu thì nó chỉ tồn tại trong C++.

Giới thiệu

Một trong những nỗi đau không nhỏ đối với lập trình viên mới vào nghề đó là phân biệt các khái niệm, trong đó với C++ thì việc phân biệt Tham chiếu và Con trỏ cũng là vấn đề hay tranh luận. Do đó, tôi viết bài viết này nhằm diễn đạt nó theo lối suy nghĩ bình dị nhất để giúp các bạn thông suốt.

Tiền đề bài viết

Việc phân biệt 2 khái niệm này là 1 phần trong các bài giảng dạy của tôi dành cho các học trò, tôi viết ra 1 phần cũng muốn giảm công việc giảng đi giảng lại 1 vấn đề cả trăm lần và 1 phần cũng muốn chia sẻ các kiến thức mà mình đã nghiệm ra được trên con đường tìm tòi cho các bạn.

Bên cạnh đó, khi phỏng vấn các ứng viên, tôi cũng hay hỏi về kiến thức này, nhưng có rất nhiều sai lầm mà các bạn gặp phải khi trả lời câu hỏi này mà điều này tôi biết thông thường là đến từ thói quen.

Đối tượng hướng đến

Dành cho các lập trình viên C++ đang kẹt ở khái niệm Tham chiếu, Tham trị, Tham biến, Con trỏ, Pass by Value, Pass by Reference.

Nhắc lại một thói quen sai lầm

Khi nói về tham chiếu (tham biến), chúng ta thường nhắc đến 1 hàm tiêu biểu đó là thay đổi giá trị giữa 2 biến.

void swap(int & a, int & b)

Tôi gọi đó là thói quen sai lầm vì, khi nói về tham chiếu, việc ứng dụng nó chỉ mỗi cho hàm swap 2 giá trị thì thật là lãng phí cả 1 kiến thức rộng.

void swap(int & a, int & b)

Vậy như thế nào mới là đầy đủ, các bạn hãy theo dõi từng bước bên dưới.

Nhắc lại các phương pháp định nghĩa biến

Các cách định nghĩa biến

int integerNumber;

int integerArray[10];

int * integerPointer;

Vấn đề nằm ở đây, ta học sót 1 kiến thức nhỏ. Đó là trong C++, ta có thể ĐẶT 1 TÊN MỚI CHO 1 BIẾN CŨ.

Đặt tên mới cho 1 biến cũ

Giả sử ta đã có 1 biến như sau

int integerNumber;

Ta sử dụng biến đó như 1 cách thông thường như ví dụ

#include <iostream>

int main()
{
	int integerNumber;
	integerNumber = 5;

	std::cout << integerNumber; // Ket qua: 5

	return 0;
}

Đặt tên mới cho biến integerNumber như trên theo các bước

  • Bước 1: xác định kiểu của integerNumber (trong trường hợp này là kiểu int).
  • Bước 2: lưu ý toán tử & trong khai báo biến mới.
int integerNumber = 5;

int & newIntegerNumberName = integerNumber;

newIntegerNumberName lúc này là cái tên mới cho biến integerNumber => 1 vùng nhớ có 2 tên. Vậy lúc ta thay đổi giá trị ở tên biến nào thì khi truy cập biến còn lại cũng là giá trị đã thay đổi đó. Ví dụ

#include <iostream>

int main()
{
	int integerNumber;
	integerNumber = 5;

	std::cout << integerNumber; // Ket qua: 5
	
	int & newIntegerNumberName = integerNumber;
	
	newIntegerNumberName = 10;
	
	std::cout << integerNumber; // Ket qua: 10

	return 0;
}

Bàn luận thêm cho vấn đề trên

Trong thuật ngữ thì người ta gọi là khai báo thêm 1 tham chiếu, tuy nhiên tôi khuyến khích bạn hiểu nó theo 1 nghĩa đơn giản đó là ĐẶT THÊM TÊN MỚI CHO BIẾN CŨ (thật chất, tất cả cũng là tham chiếu). Điều này sẽ giảm được việc hiểu lầm với con trỏ.

Để hiểu thêm ta thử nghiệm với 1 con trỏ

int* p;

Để đặt tên khác cho p ta cũng chú tâm vào 2 bước sau

  • Bước 1: xác định kiểu của p (trong trường hợp này là kiểu int*).
  • Bước 2: lưu ý toán tử & trong khai báo biến mới.

Vậy công thức đặt thêm tên khác cho vùng nhớ của p đó là

int* & p2 = p;

Rút ra nhận xét và kết luận

Vì là đặt lại tên khác do đó, sẽ không tốn kém thêm bộ nhớ khi thao tác.

Vì là đặt lại tên khác do đó, sẽ không có bộ nhớ khi định nghĩa, dẫn đến khai báo sau là sai: int & a = 5; Bạn bắt buộc phải chỉ định ngay a phải là tên mới của biến nào, tức là sau dấu bằng phải là 1 biến kiểu int.

Nhìn lại vấn đề khai báo biến với việc truyền tham số vào hàm

XÉT HÀM SAU

#include <iostream>

void swap(int a, int b) // (1)
{
	int c = a;
	a = b;
	b = c;
}

int main()
{
	int x = 5, y = 10;
	
	swap(x, y); // (2)
	
	std::cout << x << " " y; // Ket qua: 5 10
}

GIẢI THÍCH

Tại (1)(2) sẽ làm cho điều sau xảy ra

int a = xint b = y.

Mà khai báo này tức là ta đang định nghĩa 2 vùng nhớ mới và sao chép giá trị x, y vào ab.

Như vậy, nếu như hàm swap có prototype như sau

void swap(int & a, int & b) thì điều như sau sẽ xảy ra

int & a = xint & b = y.

Hiểu đơn giản có nghĩa là ta đang đặt thêm tên mới cho xayb.

Đó là lý do vì sao 2 giá trị của x, y được thay đổi nếu ta "truyền tham chiếu" (1 cách gọi mà bản thân tôi không thích).

Dành cho bạn

1. Dựa vào các kiến thức trên, bạn hãy so sánh sự khác biệt của 2 hàm sau

void swap(int & a, int & b)
{
	int c = a;
	a = b;
	b = c;
}

void swap(int * a, int * b)
{
	int c = *a;
	*a = *b;
	*b = c;
}

2. Giải thích lý do vì sao biên dịch chương trình sau sẽ ra lỗi

#include <iostream>

void func(int & a)
{
	std::cout << "STDIO: " << a;
}

int main()
{
	func(3);
}

Nếu bạn hiểu cặn kẽ được 2 bài tập trên, sẽ giúp ích được rất nhiều.

THẢO LUẬN
ĐÓNG