Chuyển đến nội dung

Chức năng & Truyền tham số

Nắm vững các cách khác nhau để truyền đối số trong C++, hiểu tối ưu hóa giá trị trả về, nạp chồng hàm và các thuộc tính hiện đại như [[nodiscard]].

Chức năng: Sự trừu tượng cốt lõi

Hàm là đơn vị cơ bản của tổ chức mã trong C++. Không giống như nhiều ngôn ngữ khác, C++ cung cấp cho bạn quyền kiểm soát chi tiết về cách các đối số được truyền đến và trả về từ các hàm. Sự kiểm soát này không chỉ là cú pháp - nó còn có ý nghĩa thực sự về hiệu suất.

Cách bạn truyền tham số sẽ xác định xem hàm có hoạt động với bản sao của đối số, tham chiếu đến bản gốc hay con trỏ đến bản gốc hay không. Chọn cơ chế phù hợp là một trong những quyết định quan trọng nhất mà bạn đưa ra khi thiết kế giao diện của một chức năng.

Parameter Passing Mechanisms

C++ supports four primary ways to pass arguments. Each has different trade-offs regarding performance, safety, and semantics:

parameter_passing.cppc++23
#include <iostream>
#include <string>
#include <vector>

// 1. Pass by VALUE — the function gets a copy
//    Use for: small types (int, double, char), when you need a local copy
void square(int x) {           // x is a copy of the argument
    x = x * x;                 // Modifying x does NOT affect the caller
    std::cout << "Inside: " << x << '\n';
}

// 2. Pass by REFERENCE — the function works on the original
//    Use for: when you need to modify the caller's variable
void double_it(int& x) {       // x IS the caller's variable
    x *= 2;                    // This DOES modify the caller's variable
}

// 3. Pass by CONST REFERENCE — read-only access to the original, no copy
//    Use for: large objects you only need to read
void print_all(const std::vector<std::string>& names) {
    for (const auto& name : names) {
        std::cout << name << '\n';
    }
    // names.push_back("Eve");  // ERROR: names is const
}

// 4. Pass by POINTER — explicit nullable indirection
//    Use for: optional parameters (can be nullptr), C API interop
void maybe_increment(int* ptr) {
    if (ptr != nullptr) {       // Must check for null!
        ++(*ptr);
    }
}

int main() {
    int val = 5;

    square(val);
    std::cout << "After square: " << val << '\n';      // Still 5 (copied)

    double_it(val);
    std::cout << "After double_it: " << val << '\n';   // Now 10 (modified)

    std::vector<std::string> people = {"Alice", "Bob", "Charlie"};
    print_all(people);         // No copy of the vector — efficient

    maybe_increment(&val);     // Pass address explicitly
    std::cout << "After maybe_increment: " << val << '\n';  // 11

    maybe_increment(nullptr);  // Safe: the function checks for null
}
Đầu ra

Nhấn Chạy để biên dịch và chạy ví dụ này.

Thực hành tốt

Hãy làm theo các nguyên tắc sau trong Nguyên tắc cốt lõi của C++ để chọn cách truyền tham số:

- Sao chép rẻ và không bao giờ sửa đổi? Truyền theo giá trị (int,double,char,bool, cấu trúc nhỏ,std::string_view).
- Đắt để sao chép và chỉ đọc? Chuyển quaconst&(std::string,std::vector, vật thể lớn).
- Cần sửa đổi biến của người gọi? Chuyển qua&(tham chiếu không phải hằng).
- Có thể là null? Truyền qua con trỏ. Nhưng thay vào đó hãy xem xétstd::optional&.
- Lấy quyền sở hữu (chìm)? Truyền theo giá trị và di chuyển (được đề cập trong mô-đun ngữ nghĩa di chuyển).

Theo nguyên tắc chung, các đối tượng lớn hơn 2-3 con trỏ (16-24 byte trên 64-bit) phải được truyền theo tham chiếu thay vì theo giá trị.

  • Truyền các loại nhỏ, rẻ tiền theo giá trị: int, double, char, bool, string_view
  • Pass large read-only objects by const reference: const std::string&, const std::vector&
  • Chỉ chuyển qua tham chiếu không phải const khi hàm cần sửa đổi đối tượng của người gọi
  • Truyền bằng con trỏ khi nullptr là đối số hợp lệ - nhưng ưu tiên std::Optional nếu có thể
  • Khi nghi ngờ, hãy chuyển qua tham chiếu const - nó an toàn và hiệu quả

Tối ưu hóa giá trị trả về (RVO/NRVO)

Mối quan tâm chung của những người mới bắt đầu là việc trả lại các đối tượng lớn theo giá trị sẽ tốn kém do sao chép. Trong C++ hiện đại, mối lo ngại này hầu như luôn vô căn cứ nhờ Tối ưu hóa giá trị trả về (RVO) và Tối ưu hóa giá trị trả về được đặt tên (NRVO).

RVO (được đảm bảo kể từ C++17): Khi một hàm trả về tạm thời (ví dụ:return std::vector{1,2,3};), trình biên dịch phải xây dựng đối tượng trực tiếp trong bộ nhớ của trình gọi. Không có sự sao chép hoặc di chuyển nào xảy ra.

NRVO (được phép, không được bảo đảm): Khi một hàm trả về một biến cục bộ được đặt tên (ví dụ:return result;), trình biên dịch có thể xây dựng biến đó trực tiếp trong bộ nhớ của lệnh gọi. Tất cả các trình biên dịch chính đều thực hiện việc này trong thực tế, ngay cả trong chế độ gỡ lỗi.

Lời khuyên thiết thực: trả lại theo giá trị một cách tự do. Không sử dụng tham số đầu ra (void f(std::vector& out)) để tránh sao chép - hãy để trình biên dịch tối ưu hóa kết quả trả về.

Nạp chồng hàm & Đối số mặc định

C++ cho phép nhiều hàm có cùng tên nhưng danh sách tham số khác nhau. Trình biên dịch chọn kết quả phù hợp nhất dựa trên các đối số được cung cấp tại trang cuộc gọi:

overloading.cppc++23
#include <iostream>
#include <string>
#include <vector>

// Function overloading — same name, different parameter types/counts
void log(int value) {
    std::cout << "[INT] " << value << '\n';
}

void log(double value) {
    std::cout << "[DOUBLE] " << value << '\n';
}

void log(const std::string& value) {
    std::cout << "[STRING] " << value << '\n';
}

// Default arguments — provide fallback values
void greet(const std::string& name, const std::string& greeting = "Hello") {
    std::cout << greeting << ", " << name << "!\n";
}

// [[nodiscard]] (C++17) — warn if return value is ignored
[[nodiscard]] int compute(int a, int b) {
    return a * b + a;
}

// [[nodiscard]] with reason (C++20)
[[nodiscard("Ignoring error codes can hide failures")]]
int open_file(const std::string& path) {
    // Returns 0 on success, negative on error
    return 0;  // Simplified
}

// inline — suggests the compiler inline this function (also allows
// the function to be defined in a header without ODR violations)
inline int fast_max(int a, int b) {
    return (a > b) ? a : b;
}

int main() {
    log(42);               // Calls log(int)
    log(3.14);             // Calls log(double)
    log(std::string{"hi"});// Calls log(const string&)

    greet("Alice");                // Uses default: "Hello, Alice!"
    greet("Bob", "Good morning"); // Uses provided: "Good morning, Bob!"

    // int result = compute(3, 4);  // OK
    // compute(3, 4);               // WARNING: ignoring [[nodiscard]] return value

    int status = open_file("data.txt");  // OK: return value used
}
Đầu ra

Nhấn Chạy để biên dịch và chạy ví dụ này.

Lỗi thường gặp

Việc giải quyết tình trạng quá tải hàm tuân theo các quy tắc phức tạp và các lệnh gọi không rõ ràng là nguyên nhân phổ biến gây ra lỗi biên dịch. Hãy nhận biết những cạm bẫy sau:

1. Chuyển đổi ngầm tạo ra sự mơ hồ: Nếu bạn cóf(int)vàf(double), việc gọif(3.14f)(afloat) là không rõ ràng vìfloatchuyển đổi tốt như nhau thành cảintvàdouble. Cách khắc phục: thêm tình trạng quá tảif(float)hoặc sử dụngstatic_cast.

2. Đối số mặc định tương tác kém với tình trạng quá tải:void f(int x, int y = 0)vàvoid f(int x)tạo ra sự mơ hồ khi được gọi bằng một đối số.

3. tham chiếu hằng số so với giá trị:f(int)vàf(const int&)không rõ ràng đối với các đối số giá trị vì cả hai đều khớp tốt như nhau.

Khi trình biên dịch báo cáo tình trạng quá tải không rõ ràng, đừng đoán - hãy đọc thông báo lỗi một cách cẩn thận. Nó sẽ cho bạn biết những ứng cử viên nào quá tải và tại sao chúng đều tốt như nhau.

  • Chuyển đổi ngầm định có thể làm cho độ phân giải tình trạng quá tải trở nên mơ hồ — thêm các tình trạng quá tải rõ ràng để giải quyết
  • Các đối số mặc định và quá tải có thể xung đột - tránh cả hai trên cùng một tên hàm
  • Khi nghi ngờ, thông báo lỗi trình biên dịch liệt kê các ứng viên quá tải và sự mơ hồ
  • Sử dụng static_cast để chọn rõ ràng tình trạng quá tải khi gọi bằng các đối số khớp với nhiều tình trạng quá tải
Điểm chính
  • Truyền các loại nhỏ theo giá trị, các loại chỉ đọc lớn theo tham chiếu const và các loại có thể thay đổi theo tham chiếu không phải const
  • Trả về theo giá trị một cách tự do — RVO/NRVO loại bỏ sự sao chép trong thực tế
  • Nạp chồng hàm chọn kết quả phù hợp nhất dựa trên các loại đối số - hãy cẩn thận với sự mơ hồ từ các chuyển đổi ngầm định
  • Use [[nodiscard]] on functions whose return value must not be ignored (error codes, computed results)
  • Default arguments provide convenience but can conflict with overloads — use one or the other
  • inline allows functions to be defined in headers without violating the One Definition Rule

Câu đố — Kiểm tra kiến thức

(15 XP)

1. When should you pass a`std::vector<int>`by`const&`instead of by value?

2. Tối ưu hóa giá trị trả về (RVO) là gì?

3. Thuộc tính`[[nodiscard]]`làm gì?