Chuyển tới nội dung chính

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, b là tham số.
  • Khi gọi tong(3, 5), thì 3, 5 là đối số.

1.2. Bảng phân biệt nhanh​

Tiêu chíTham sốĐối số
Vị trí xuất hiệnTrong phần khai báo/định nghĩa hàmTrong lời gọi hàm
Vai tròNơi nhận dữ liệu của hàmDữ liệu thực tế được đưa vào
Thời điểm tồn tạiKhi khai báo hàmTại nơi gọi hàm
Có thể là biểu thứcKhôngCó, 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ự:

  1. Số lượng đối số có khớp danh sách tham số hay không.
  2. 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ệ).
  3. 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; hay int a = b. Ở đây, a và b là 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:

Loading...

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ếu int& 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 đổi x, đối số gốc cũng thay đổi theo.

  • Ở dòng (1), a là lvalue. Nó có ô nhớ. Hàm có thể trỏ x tới ô nhớ của a và tăng nó lên 11. Ở thời điểm này vẫn chưa có lỗi.

  • Ở dòng (2), 20 là 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à để x trở 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ì 20 chỉ là một giá trị tạm thời chứ không có ô nhớ thật sự, việc ép biến xphả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​

    1. Đố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.
    1. 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.
    1. Thứ tự đối số không quan trọng: thứ tự ảnh hưởng trực tiếp đến kết quả.
    1. 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.
Liên hệ kiến thức nền

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ụ:

Loading...

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ặc array) 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ặc nullptr). 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ềnKhai 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ôngCóKhông áp dụngDữ liệu nhỏ, chỉ cần đọc
Tham chiếuvoid f(int& x)CóKhôngKhôngCần sửa trực tiếp biến gốc
Tham chiếu hằngvoid f(const int& x)KhôngCóKhôngChỉ đọ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ếpCó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ụ​

Loading...

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 qua x.
  • Con trỏ tới hằng: const int* p -> không sửa được dữ liệu qua *p, nhưng có thể trỏ p tớ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.
Loading...

Stdin:

Stdout:


Tài liệu tham khảo:

  1. Value categories
  2. lvalues references and rvalues references in C++
  3. Const Reference vs Normal Parameter Passing in C++