Menu

Tham số hàm trong C++: truyền tham trị, tham chiếu, const

Cách đối số đi vào hàm C++: truyền tham trị và truyền tham chiếu, tham chiếu const để đọc nhanh mà không sửa, đối số mặc định, con trỏ, và những cái bẫy sao chép âm thầm làm chậm chương trình.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Tham số và đối số

Tham số (parameter) của một hàm là các biến có tên trong phần định nghĩa hàm; đối số (argument) là các giá trị thật bạn đưa vào khi gọi hàm. Trang trước đã cho thấy cách định nghĩa và gọi hàm; trang này nói về cách các giá trị đó thật sự đi vào hàm, vì C++ cho bạn nhiều cách và lựa chọn ảnh hưởng tới cả tính đúng đắn lẫn tốc độ.

Mặc định trong C++ là truyền tham trị (pass by value): hàm nhận được một bản sao.

Bên trong addTen, n là một biến riêng được khởi tạo từ score. Gán lại n chỉ tác động lên bản sao đó, nên score trong main vẫn nguyên vẹn. Cách này an toàn và dễ đoán, hàm không thể vô tình phá dữ liệu của bạn, và đó chính là lý do nó là mặc định.

Truyền tham chiếu: cho hàm sửa biến của nơi gọi

Đôi khi bạn muốn hàm sửa biến của nơi gọi. Thêm & vào kiểu tham số và nó trở thành một tham chiếu (reference), một bí danh của biến gốc chứ không phải bản sao:

Khác biệt duy nhất so với ví dụ đầu là dấu &, nhưng giờ n và score là cùng một đối tượng. Đây là cách chuẩn để "trả về" nhiều hơn một giá trị hoặc cập nhật một thứ ngay tại chỗ. Một ứng dụng kinh điển là hoán đổi hai biến:

Thiếu &, swapValues sẽ đảo hai bản sao và main không thấy thay đổi nào cả, một bug rất phổ biến của người mới.

Tham chiếu const: truy cập chỉ đọc mà không tốn kém

Truyền tham trị sao chép đối số. Với một int thì chẳng tốn gì, nhưng sao chép một string hay vector lớn ở mỗi lần gọi là công sức lãng phí thật sự. Cách sửa là tham chiếu const (const T&): bạn có tốc độ của tham chiếu (không sao chép) cùng một lời hứa được trình biên dịch bảo đảm là không sửa đối số.

Một quy tắc tiện dụng: truyền các kiểu có sẵn nhỏ (int, double, char, bool, con trỏ) theo giá trị, và truyền các đối tượng lớn mà bạn chỉ cần đọc bằng tham chiếu const. Chỉ dùng T& không const khi bạn thật sự có ý định sửa đối tượng của nơi gọi.

Một điểm dễ nhầm tinh tế: int& n thông thường không gắn được vào một giá trị tạm hay một literal. addTen(5) trong ví dụ đầu sẽ không biên dịch được nếu tham số là int&, vì 5 không phải một biến để làm bí danh. const int& thì gắn được vào 5, thêm một lý do khiến tham chiếu const được dùng rộng rãi đến vậy.

Đối số mặc định

Bạn có thể cho một tham số giá trị dự phòng để nơi gọi được phép bỏ qua nó. Nếu thiếu đối số, giá trị mặc định sẽ được dùng:

Có hai quy tắc hay làm người học vấp. Thứ nhất, giá trị mặc định phải nằm ở cuối: khi một tham số đã có giá trị mặc định, mọi tham số sau nó cũng phải có. Bạn không thể viết void f(int a = 1, int b) vì sẽ không có cách nào truyền b mà bỏ qua a. Thứ hai, khi hàm được khai báo trong header và định nghĩa ở nơi khác, hãy chỉ đặt giá trị mặc định trong phần khai báo, đừng bao giờ lặp lại trong phần định nghĩa, vì lặp lại là lỗi biên dịch.

Truyền mảng và vector

Mảng thô bị suy biến thành con trỏ khi truyền vào hàm, nên hàm mất thông tin về kích thước của nó; gần như lúc nào bạn cũng phải truyền kèm độ dài:

Vì mảng đã thành con trỏ, sizeof(arr) bên trong sum sẽ cho kích thước của một con trỏ chứ không phải của mảng, một bug khét tiếng. Trong C++ hiện đại, hãy ưu tiên std::vector (hoặc std::span trong C++20), truyền bằng tham chiếu const, vì nó tự mang theo kích thước:

Để ý const&: bỏ nó đi thì mỗi lần gọi sẽ sao chép cả vector. Với vector bốn phần tử thì vô hại, nhưng với một triệu phần tử thì đó là một cái hố hiệu năng âm thầm.

Tham số con trỏ

Bạn cũng có thể truyền một con trỏ (T*). Giống tham chiếu, cách này cho hàm chạm tới dữ liệu của nơi gọi, nhưng con trỏ có thể trỏ sang chỗ khác hoặc là null, nên nó là công cụ đúng khi "không có giá trị" là một lựa chọn hợp lệ:

Nơi gọi truyền &value để chia sẻ địa chỉ của nó, và hàm ghi qua *out. Khác biệt chính so với tham chiếu: con trỏ có thể là nullptr, nên hàm nhận con trỏ phải kiểm tra trước khi giải tham chiếu. Bỏ qua bước kiểm tra đó và giải tham chiếu một con trỏ null là undefined behavior, thường làm chương trình sập. Nếu "không có giá trị" không bao giờ có nghĩa, tham chiếu sẽ gọn hơn vì ngay từ đầu nó không thể là null.

Tiếp theo: tham chiếu

Tham số là nơi tham chiếu phát huy giá trị, nhưng bản thân tham chiếu là một tính năng riêng: bí danh bạn có thể tạo cho bất kỳ biến nào, không chỉ trong chữ ký hàm. Trang tiếp theo đi sâu vào cách tham chiếu hoạt động: cách khai báo, vì sao phải khởi tạo ngay lập tức, khác biệt giữa tham chiếu lvalue và tham chiếu const, và những cách tinh vi khiến một tham chiếu trở thành tham chiếu treo.

Câu hỏi thường gặp

Truyền tham trị và truyền tham chiếu trong C++ khác nhau thế nào?

Truyền tham trị (pass by value) sao chép đối số vào tham số, nên thay đổi bên trong hàm không ảnh hưởng tới nơi gọi. Truyền tham chiếu (int&) biến tham số thành bí danh của biến bên phía nơi gọi, nên thay đổi sẽ thấy được ở bên ngoài. Dùng void f(int x) để sao chép và void f(int& x) để sửa biến gốc.

Khi nào nên dùng tham số tham chiếu const trong C++?

Dùng const T& khi bạn muốn đọc một đối tượng lớn mà không sao chép và không cho hàm sửa nó, ví dụ void print(const string& s). Cách này cho bạn tốc độ của truyền tham chiếu cùng sự an toàn của truyền tham trị. Với kiểu nhỏ như int hay char, truyền tham trị thông thường cũng nhanh y như vậy.

Đối số mặc định trong C++ là gì?

Đối số mặc định cho phép một tham số nhận giá trị dự phòng khi nơi gọi bỏ qua nó, ví dụ void greet(string name = "there"). Các giá trị mặc định phải nằm ở những tham số cuối cùng (bên phải nhất), và bạn chỉ ghi chúng trong phần khai báo, không ghi trong phần định nghĩa nếu hai phần này tách riêng.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu