Hai cách để trỏ tới một thứ
Bạn đã biết con trỏ: những biến lưu một địa chỉ và cho phép bạn truy cập đối tượng nằm ở đó. Tham chiếu (reference) là công cụ còn lại mà C++ cung cấp để làm việc gián tiếp với một đối tượng đã có. Hai thứ này giống nhau đến mức người mới thường không biết nên chọn cái nào, nên trang này sẽ đặt chúng cạnh nhau.
Nói ngắn gọn: tham chiếu là một tên khác. Khi int& r = x; chạy xong, r chính là x: cùng một đối tượng, khác tên. Con trỏ là một đối tượng riêng tình cờ chứa địa chỉ của một đối tượng khác. Chỉ một khác biệt đó quyết định mọi thứ còn lại.
Tham chiếu là một tên khác
Tham chiếu phải được gắn với một đối tượng ngay lúc được tạo, và từ đó mọi lần dùng tham chiếu đều tác động lên đối tượng gốc.
Để ý rằng không có * để giải tham chiếu và không có & để "lấy địa chỉ" lúc sử dụng: bạn đọc và ghi alias y như một int bình thường. Dấu & trong int& alias là một phần của kiểu, không phải toán tử lấy địa chỉ.
Chúng khác nhau ở đâu
Những hành vi dưới đây chính là lý do cả hai công cụ cùng tồn tại. Đây là bảng nên ghi nhớ.
// reference pointer
// must be initialized? yes no (but should be)
// can be null? no yes (nullptr)
// can be reseated? no yes
// pointer arithmetic? no yes
// syntax to use it just the name *p or p->member
// taking address &ref == &original &p is the pointer's own address
Có hai điểm khiến nhiều người vấp nhất. Thứ nhất, tham chiếu không bao giờ gắn lại được: gán vào nó là sao chép một giá trị vào đối tượng được tham chiếu, chứ không làm tham chiếu trỏ sang thứ khác.
Ngược lại, con trỏ có thể trỏ sang chỗ khác bất cứ lúc nào:
Thứ hai, tham chiếu không bao giờ được phép null một cách hợp lệ, còn con trỏ thì có thể. Nhờ vậy "không có giá trị" biểu diễn được bằng con trỏ nhưng không bằng tham chiếu, một tính chất bạn sẽ dựa vào thường xuyên.
Chọn cho tham số của hàm
Đây là nơi lựa chọn này xuất hiện nhiều nhất. Khi một hàm cần đọc hoặc sửa đối tượng của nơi gọi, cả hai đều được, nhưng chúng thể hiện ý định khác nhau.
Phiên bản tham chiếu (addTax(cart)) không thể gọi với "không có gì", nên bên trong hàm bạn không bao giờ phải kiểm tra null: đối tượng chắc chắn tồn tại. Phiên bản con trỏ (applyDiscount(&cart)) cho thấy ngay tại chỗ gọi, qua dấu &, rằng đối số có thể bị thay đổi, và cho phép nơi gọi truyền nullptr để nói "không áp dụng". Hãy chọn cái có cam kết khớp với hàm của bạn.
Với tham số chỉ đọc có kiểu lớn, lựa chọn chuẩn là const T&: nó tránh sao chép và cam kết không sửa đổi. Xem bài tham số của hàm để hiểu thêm về truyền tham trị và truyền tham chiếu.
Một quy tắc đơn giản
Khi không chắc, hãy mặc định dùng tham chiếu và chỉ chuyển sang con trỏ khi bạn cần một khả năng mà tham chiếu không có:
- Dùng tham chiếu khi đối tượng luôn tồn tại và danh tính của nó không bao giờ đổi: trường hợp phổ biến với tham số hàm và tên gọi khác.
- Dùng con trỏ khi có bất kỳ điều nào sau đây:
- "Không có gì" là một trạng thái hợp lệ (đối số tùy chọn, một lần tìm kiếm có thể không thấy kết quả): con trỏ có thể là
nullptr. - Bạn cần trỏ tới các đối tượng khác nhau theo thời gian: con trỏ có thể gắn lại.
- Bạn đang quản lý vùng nhớ heap sẽ phải
delete(hoặc tốt hơn là một smart pointer), hoặc duyệt một mảng bằng số học con trỏ.
- "Không có gì" là một trạng thái hợp lệ (đối số tùy chọn, một lần tìm kiếm có thể không thấy kết quả): con trỏ có thể là
Nếu không điều nào ở trên đúng, tham chiếu là lựa chọn gọn và an toàn hơn, vì trình biên dịch tự đảm bảo "luôn hợp lệ, không bao giờ gắn lại" cho bạn.
Những lỗi thường gặp cần tránh
- Tưởng
ref = othersẽ gắn lại. Thực ra nó gán một giá trị vào đối tượng được tham chiếu. Tham chiếu gắn cố định suốt đời; nếu cần gắn lại, hãy dùng con trỏ. - Trả về tham chiếu (hoặc con trỏ) tới biến cục bộ.
int& f() { int x = 5; return x; }trả về một tham chiếu treo:xbị hủy khiftrả về, và dùng kết quả đó là hành vi không xác định. Con trỏ cũng dính cùng cái bẫy (return &x;). - Tạo ra một "tham chiếu null". Viết
int& r = *p;khiplànullptrlà hành vi không xác định ngay lúc giải tham chiếu, chứ không phải một tham chiếu "rỗng" an toàn. Hãy diễn đạt tính tùy chọn bằng con trỏ hoặcstd::optional. - Dùng con trỏ theo thói quen. Nếu đối số luôn tồn tại và bạn không bao giờ gắn lại, tham chiếu loại bỏ cả một nhóm phép kiểm tra null và crash. Đừng trả giá cho những khả năng bạn không dùng.
Tiếp theo: bộ nhớ động
Cho đến giờ, mọi đối tượng bạn tham chiếu hay trỏ tới đều được tạo tự động trên stack. Trang tiếp theo, bộ nhớ động, nói về new và delete: xin hệ điều hành cấp bộ nhớ lúc chạy, vì sao con trỏ (chứ không phải tham chiếu) sở hữu vùng nhớ đó, và việc quên giải phóng gây rò rỉ bộ nhớ như thế nào.
Câu hỏi thường gặp
Tham chiếu và con trỏ trong C++ khác nhau thế nào?
Tham chiếu là một tên khác (alias) của một đối tượng đã có: nó phải được khởi tạo, không bao giờ null và không thể chuyển sang tham chiếu tới đối tượng khác sau đó. Con trỏ là một biến riêng chứa địa chỉ: nó có thể null, có thể được gắn lại để trỏ tới chỗ khác và hỗ trợ số học con trỏ. Tham chiếu dùng cú pháp &, còn con trỏ dùng */->.
Khi nào nên dùng con trỏ thay vì tham chiếu trong C++?
Dùng con trỏ khi "không có gì" là một trạng thái hợp lệ (đối số tùy chọn, kết quả không tìm thấy), khi bạn cần gắn lại nó để trỏ tới các đối tượng khác nhau theo thời gian, hoặc khi bạn sở hữu vùng nhớ heap sẽ phải delete. Dùng tham chiếu khi đối tượng luôn tồn tại và không bao giờ đổi danh tính, điều đúng với hầu hết tham số của hàm.
Tham chiếu trong C++ có thể null không?
Không. Một tham chiếu hợp lệ luôn trỏ tới một đối tượng có thật, nên bạn không bao giờ cần kiểm tra null. Nếu bạn tạo tham chiếu từ một con trỏ null đã giải tham chiếu (int& r = *p; khi p là null), bạn nhận được hành vi không xác định chứ không phải một tham chiếu null. Khi cần diễn đạt "có thể không có gì", hãy dùng con trỏ hoặc std::optional.