Mở rộng chương 4
1. Tham số và Đối số
1.1. Định nghĩa cốt lõi
- Tham số là biến được khai báo trong phần định nghĩa hàm, dùng để nhận dữ liệu đầu vào cho hàm.
- Đối số là giá trị cụ thể truyền vào hàm tại thời điểm gọi.
Ví dụ nhanh:
- Trong hàm
int tong(int a, int b), thìa,blà tham số. - Khi gọi
tong(3, 5), thì3,5là đối số.
1.2. Bảng phân biệt nhanh
| Tiêu chí | Tham số | Đối số |
|---|---|---|
| Vị trí xuất hiện | Trong phần khai báo/định nghĩa hàm | Trong lời gọi hàm |
| Vai trò | Nơi nhận dữ liệu của hàm | Dữ liệu thực tế được đưa vào |
| Thời điểm tồn tại | Khi khai báo hàm | Tại nơi gọi hàm |
| Có thể là biểu thức | Không | Có, ví dụ x + 1, 2 * y, 42 |
| Ví dụ | int a trong void f(int a) | 10 trong f(10) |
1.3. Trình biên dịch kiểm tra lời gọi hàm như thế nào?
Khi gặp một lời gọi hàm, trình biên dịch thường kiểm tra tuần tự:
- Số lượng đối số có khớp danh sách tham số hay không.
- Kiểu dữ liệu có tương thích hay không (khớp trực tiếp hoặc chuyển đổi ngầm định hợp lệ).
- Loại giá trị của đối số (rvalue/lvalue) có phù hợp với kiểu tham số hay không.
rvalue/lvalue là gì?
-
lvalue (Locator Value - Giá trị có vị trí): *
- Là những đối tượng có địa chỉ bộ nhớ xác định và tồn tại qua nhiều dòng lệnh.
- Nó có thể nằm ở bên trái (Left) hoặc bên phải của toán tử gán =.
- Ví dụ: Các biến như
int a = 5;hayint a = b. Ở đây,avàblà một lvalue vì nó có ô nhớ riêng biệt.
-
rvalue (Read Value / Right Value - Giá trị tạm thời):
- Là những giá trị tạm thời, không có địa chỉ bộ nhớ cố định (thường nằm trên thanh ghi của CPU hoặc bị hủy ngay sau khi biểu thức kết thúc).
- Nó chỉ có thể nằm ở bên phải (Right) của toán tử gán =.
- Ví dụ: Các hằng số (như
5,3.14), kết quả của một phép tính (nhưa + b), hoặc giá trị trả về từ một hàm (nhưtong(3, 4)).
Nguồn tham khảo: Value Category
Tại sao phải kiểm tra rvalue/lvalue
Xét ví dụ sau:
Stdin:
Stdout:
Trình biên dịch kiểm tra lần lượt:
-
Số lượng đối số: Khớp (1 đối số).
-
Kiểu dữ liệu: Khớp. Hàm yêu cầu kiểu int, đối số truyền vào là 20 cũng là kiểu int.
-
Kiểm tra loại giá trị (lvalue/rvalue): THẤT BẠI.
Nguyên nhân:
-
Hàm
tangGiaTri()nhận vào một tham chiếuint& x. Bản chất của tham chiếu là tạo ra một cái tên khác trỏ tới cùng một ô nhớ với đối số truyền vào, để khi thay đổix, đối số gốc cũng thay đổi theo. -
Ở dòng (1),
alà lvalue. Nó có ô nhớ. Hàm có thể trỏxtới ô nhớ củaavà tăng nó lên 11. Ở thời điểm này vẫn chưa có lỗi. -
Ở dòng (2),
20là rvalue, vì vậy nó không có địa chỉ bộ nhớ cố định. Bản chất của truyền tham chiếu là đểxtrở thành một cái tên khác, dùng chung một ô nhớ với đối số truyền vào. Tuy nhiên, vì20chỉ là một giá trị tạm thời chứ không có ô nhớ thật sự, việc ép biếnxphải dùng chung ô nhớ với một thứ không tồn tại là vô lý. Do đó, trình biên dịch sẽ báo lỗi trong trường hợp này.
Kết luận: trình biên dịch C++ có quy tắc: Tham chiếu thông thường không thể ràng buộc với một rvalue. Nó phải báo lỗi ngay từ khâu biên dịch để ngăn chặn lỗi ở thời điểm runtime.
1.4. Sai lầm dễ nhầm lẫn
-
- Đối số bắt buộc phải là biến: thực tế có thể là hằng số hoặc biểu thức.
-
- Trùng tên là cùng biến: biến trùng tên ở hàm khác nhau vẫn là biến khác nhau.
-
- Thứ tự đối số không quan trọng: thứ tự ảnh hưởng trực tiếp đến kết quả.
-
- Sửa tham số luôn làm đổi biến gốc: điều này còn phụ thuộc hàm nhận theo cách nào (tham trị, tham chiếu, con trỏ).
1.5. Phạm vi hoạt động của tham số và biến trong hàm
Mỗi lần gọi hàm, chương trình tạo một khung ngăn xếp (stack frame) mới. Trong khung đó có:
- Các tham số của hàm.
- Các biến cục bộ khai báo trong hàm.
Vì vậy:
- Tham số/biến cục bộ của hàm chỉ dùng được bên trong hàm đó.
- Dù trùng tên với biến ở hàm khác, chúng vẫn là các thực thể bộ nhớ khác nhau.
Xem lại phần phạm vi biến ở Chương 2 - Mục 2.2.4. Phạm vi của biến.
Ví dụ:
Stdin:
Stdout:
1.7. Sơ đồ tư duy tóm tắt
2. Truyền tham trị, tham chiếu, tham chiếu hằng và con trỏ
2.1. Khái niệm
Bốn cách này mô tả cách hàm nhận dữ liệu từ nơi gọi, cũng chính là cách các hàm giao tiếp với nhau:
- Truyền tham trị: hàm nhận bản sao dữ liệu. Mọi thay đổi bên trong hàm chỉ tác động lên bản sao, không ảnh hưởng đến biến gốc ở nơi gọi.
- Truyền tham chiếu: hàm nhận cái tên khác của biến gốc và có thể sửa dữ liệu của biến gốc.
- Truyền tham chiếu hằng (const reference): hàm nhận một cái tên khác nhưng dưới chế độ chỉ được đọc (Read-only). Đây là sự kết hợp giữa đặc điểm không sao chép dữ liệu của phương pháp truyền tham chiếu (tránh lãng phí bộ nhớ khi sao chép các đối tượng lớn ra ô nhớ mới) và không cho phép sửa đổi đến từ từ khóa
const. Nếu hàm cố tình thay đổi dữ liệu, trình biên dịch sẽ báo lỗi. Đây là cách tối ưu nhất khi truyền các đối tượng lớn (nhưstd::string,std::vectorhoặcarray) mà bạn không có nhu cầu thay đổi chúng. - Truyền con trỏ: hàm nhận địa chỉ ô nhớ của biến gốc.Tương tự tham chiếu, hàm có thể truy cập và sửa đổi biến gốc thông qua việc giải tham chiếu (dereferencing). Điểm khác biệt lớn nhất là con trỏ có thể nhận giá trị
NULL(hoặcnullptr). Dùng khi bạn cần làm việc với mảng, cấp phát động, hoặc khi tham số truyền vào có thể "không tồn tại" (giá trị null).
2.2. Bảng phân biệt rõ ràng
| Cách truyền | Khai báo tham số | Có sửa dữ liệu gốc không? | Nhận được rvalue không? | Có thể truyền nullptr không? | Khi nên dùng |
|---|---|---|---|---|---|
| Tham trị | void f(int x) | Không | Có | Không áp dụng | Dữ liệu nhỏ, chỉ cần đọc |
| Tham chiếu | void f(int& x) | Có | Không | Không | Cần sửa trực tiếp biến gốc |
| Tham chiếu hằng | void f(const int& x) | Không | Có | Không | Chỉ đọc, tránh sao chép dữ liệu lớn |
| Con trỏ | void f(int* p) | Có (nếu p hợp lệ) | Không trực tiếp | Có | Cần thao tác địa chỉ hoặc cần thao tác trên mảng, cấu trúc dữ liệu phức tạp |
2.3. Mô tả luồng dữ liệu
2.4. Ví dụ
Stdin:
Stdout:
2.5. Phân biệt thêm các khái niệm const dễ nhầm
- Tham chiếu hằng:
const int& x-> không sửa được dữ liệu quax. - Con trỏ tới hằng:
const int* p-> không sửa được dữ liệu qua*p, nhưng có thể trỏptới địa chỉ khác. - Con trỏ hằng:
int* const p-> không đổi được địa chỉp, nhưng vẫn có thể sửa giá trị mà con trỏpđang trỏ tới bằng phép giải tham chiếu*p.
Stdin:
Stdout:
Tài liệu tham khảo: