Bài 8.2: Tham số và kiểu trả về của hàm trong C++

Chào mừng các bạn trở lại với chuỗi bài học về C++! Sau khi đã làm quen với khái niệm cơ bản về hàm ở bài trước, hôm nay chúng ta sẽ đào sâu vào hai thành phần cực kỳ quan trọng, quyết định cách hàm hoạt động và tương tác với phần còn lại của chương trình: tham số (parameters) và kiểu trả về (return type).

Hãy hình dung một hàm như một "cỗ máy" chuyên biệt. Để cỗ máy đó hoạt động, chúng ta cần đưa nguyên liệu vào (đó chính là tham số). Sau khi xử lý xong, cỗ máy sẽ cho ra sản phẩm (đó là giá trị được trả về). Việc hiểu rõ cách đưa nguyên liệu vào và nhận sản phẩm ra là chìa khóa để sử dụng sức mạnh của hàm một cách hiệu quả nhất.

1. Tham số của hàm (Function Parameters)

Tham số là những giá trị mà bạn truyền vào một hàm khi gọi nó. Chúng cho phép hàm hoạt động với các dữ liệu khác nhau mỗi lần được gọi, làm cho hàm trở nên linh hoạt và tái sử dụng được.

Khi định nghĩa một hàm, bạn khai báo các tham số bên trong cặp dấu ngoặc đơn (), sau tên hàm. Mỗi tham số cần có kiểu dữ liệutên.

Cú pháp tổng quát:

kieu_tra_ve ten_ham(kieu_du_lieu1 ten_tham_so1, kieu_du_lieu2 ten_tham_so2, ...) {
    // Nội dung hàm
}

Ví dụ:

int tinh_tong(int a, int b) {
    return a + b;
}

Trong C++, có ba cách chính để truyền tham số vào hàm, mỗi cách có mục đích và hiệu ứng khác nhau:

1.1. Truyền tham số theo giá trị (Pass by Value)

Đây là cách truyền tham số mặc định. Khi bạn truyền một biến theo giá trị, một bản sao của giá trị của biến đó được tạo ra và đưa vào hàm. Bất kỳ thay đổi nào được thực hiện trên tham số bên trong hàm sẽ không ảnh hưởng đến biến gốc bên ngoài hàm.

Ưu điểm: An toàn, hàm không thể vô tình thay đổi dữ liệu gốc. Nhược điểm: Tốn kém bộ nhớ và thời gian nếu truyền các đối tượng lớn (vì phải tạo bản sao).

Hãy xem ví dụ minh họa:

#include <iostream>

using namespace std;

void tangGiaTri(int n) {
    cout << "Trong ham (truoc): " << n << endl;
    n += 10;
    cout << "Trong ham (sau): " << n << endl;
}

int main() {
    int d = 5;
    cout << "Ngoai ham (truoc): " << d << endl;
    tangGiaTri(d);
    cout << "Ngoai ham (sau): " << d << endl;
    return 0;
}

Output:

Ngoai ham (truoc): 5
Trong ham (truoc): 5
Trong ham (sau): 15
Ngoai ham (sau): 5

Giải thích code:

  • Biến diem có giá trị ban đầu là 5.
  • Khi gọi tang_gia_tri(diem), một bản sao của giá trị 5 được tạo ra và gán cho tham số so bên trong hàm tang_gia_tri.
  • Hàm thay đổi giá trị của bản sao so lên 15.
  • Khi hàm kết thúc, bản sao so bị hủy.
  • Biến diem bên ngoài hàm hoàn toàn không bị ảnh hưởng và vẫn giữ nguyên giá trị 5.
1.2. Truyền tham số theo tham chiếu (Pass by Reference)

Khi truyền tham số theo tham chiếu, bạn không truyền một bản sao mà là một bí danh (alias) hoặc tên gọi khác cho biến gốc. Mọi thay đổi được thực hiện trên tham số bên trong hàm sẽ ảnh hưởng trực tiếp đến biến gốc bên ngoài hàm.

Để khai báo tham số theo tham chiếu, bạn sử dụng ký hiệu & trước tên tham số.

Ưu điểm:

  • Cho phép hàm thay đổi giá trị của biến gốc.
  • Hiệu quả hơn khi truyền các đối tượng lớn vì không phải tạo bản sao.
  • Tránh được các vấn đề với con trỏ (sẽ nói sau).

Nhược điểm: Hàm có thể vô tình thay đổi dữ liệu gốc nếu không cẩn thận.

Ví dụ minh họa:

#include <iostream>

using namespace std;

void tangGiaTriRef(int &n) {
    cout << "Trong ham (truoc): " << n << endl;
    n += 10;
    cout << "Trong ham (sau): " << n << endl;
}

int main() {
    int d = 5;
    cout << "Ngoai ham (truoc): " << d << endl;
    tangGiaTriRef(d);
    cout << "Ngoai ham (sau): " << d << endl;
    return 0;
}

Output:

Ngoai ham (truoc): 5
Trong ham (truoc): 5
Trong ham (sau): 15
Ngoai ham (sau): 15

Giải thích code:

  • Biến diem có giá trị ban đầu là 5.
  • Khi gọi tang_gia_tri_ref(diem), tham số so bên trong hàm trở thành một bí danh cho biến diem bên ngoài. Chúng cùng tham chiếu đến một ô nhớ duy nhất.
  • Hàm thay đổi giá trị của so = so + 10;. Thực tế là nó đang thay đổi trực tiếp giá trị của biến diem bên ngoài lên 15.
  • Khi hàm kết thúc, biến diem giữ giá trị đã được thay đổi là 15.
1.3. Truyền tham số theo con trỏ (Pass by Pointer)

Truyền tham số theo con trỏ là cách truyền địa chỉ bộ nhớ của biến gốc vào hàm. Để truy cập hoặc thay đổi giá trị tại địa chỉ đó, bạn cần sử dụng toán tử giải tham chiếu (*).

Để khai báo tham số theo con trỏ, bạn sử dụng ký hiệu * trước tên tham số. Khi gọi hàm, bạn truyền địa chỉ của biến bằng toán tử &.

Ưu điểm:

  • Cho phép hàm thay đổi giá trị của biến gốc (tương tự tham chiếu).
  • Có thể truyền con trỏ nullptr (hoặc NULL trong C cũ) để chỉ ra rằng không có đối tượng nào được truyền.
  • Cần thiết trong một số trường hợp lập trình cấp thấp hoặc giao tiếp với C API.

Nhược điểm:

  • Cú pháp phức tạp hơn (cần toán tử *&).
  • Nguy hiểm hơn (có thể truy cập vào vùng nhớ không hợp lệ nếu con trỏ sai hoặc là nullptr).

Ví dụ minh họa:

#include <iostream>

using namespace std;

void tangGiaTriPtr(int *p) {
    cout << "Trong ham (truoc - dia chi): " << p << endl;
    cout << "Trong ham (truoc - gia tri): " << *p << endl;
    *p += 10;
    cout << "Trong ham (sau - gia tri): " << *p << endl;
}

int main() {
    int d = 5;
    cout << "Ngoai ham (truoc): " << d << endl;
    cout << "Dia chi cua d: " << &d << endl;
    tangGiaTriPtr(&d);
    cout << "Ngoai ham (sau): " << d << endl;
    return 0;
}

Output:

Ngoai ham (truoc): 5
Dia chi cua d: (dia chi cua bien d)
Trong ham (truoc - dia chi): (dia chi cua bien d)
Trong ham (truoc - gia tri): 5
Trong ham (sau - gia tri): 15
Ngoai ham (sau): 15

Giải thích code:

  • Biến diem có giá trị ban đầu là 5.
  • Khi gọi tang_gia_tri_ptr(&diem), địa chỉ của biến diem được truyền vào hàm và gán cho con trỏ so_ptr.
  • Bên trong hàm, so_ptr giữ địa chỉ của diem. Để làm việc với giá trị tại địa chỉ đó (tức là giá trị của diem), chúng ta dùng toán tử giải tham chiếu *.
  • Dòng *so_ptr = *so_ptr + 10; đọc giá trị tại địa chỉ mà so_ptr đang trỏ tới (là 5), cộng thêm 10 (thành 15), và ghi lại giá trị 15 đó vào chính địa chỉ mà so_ptr đang trỏ tới (tức là ô nhớ của diem).
  • Kết quả là biến diem bên ngoài hàm bị thay đổi thành 15.

Khi nào sử dụng Pass by Value, Reference, hoặc Pointer?

  • Pass by Value: Khi bạn chỉ cần giá trị của biến và không muốn (hoặc không cần) hàm làm thay đổi biến gốc. Tốt cho các kiểu dữ liệu cơ bản nhỏ (int, float, bool...).
  • Pass by Reference: Khi bạn muốn hàm có thể thay đổi giá trị của biến gốc hoặc khi bạn muốn truyền các đối tượng lớn một cách hiệu quả (tránh sao chép). Thường là lựa chọn ưu tiên hơn con trỏ trong C++ hiện đại khi cần thay đổi giá trị gốc. Sử dụng const & nếu bạn muốn truyền tham chiếu để hiệu quả nhưng không cho phép hàm thay đổi giá trị.
  • Pass by Pointer: Khi bạn cần làm việc với địa chỉ bộ nhớ, có thể cần con trỏ nullptr, hoặc khi giao tiếp với các thư viện C. Cần cẩn trọng hơn.

2. Kiểu trả về của hàm (Function Return Type)

Kiểu trả về là kiểu dữ liệu của giá trị mà hàm sẽ gửi lại cho phần chương trình đã gọi nó sau khi hoàn thành công việc.

Khi định nghĩa hàm, bạn khai báo kiểu trả về trước tên hàm.

Cú pháp:

kieu_tra_ve ten_ham(...) {
    // Nội dung hàm
    return gia_tri_co_cung_kieu_tra_ve; // Sử dụng từ khóa return
}
2.1. Trả về một giá trị cụ thể

Nếu hàm thực hiện một phép tính hoặc tạo ra một kết quả nào đó, nó có thể trả về giá trị đó cho nơi gọi hàm.

Ví dụ:

#include <iostream>

using namespace std;

int tong(int a, int b) {
    return a + b;
}

double dtHCN(double d, double r) {
    if (d <= 0 || r <= 0) {
        return -1.0;
    }
    return d * r;
}

int main() {
    int a = 10, b = 20;
    int t = tong(a, b);
    cout << "Tong cua " << a << " va " << b << " la: " << t << endl;

    double cd = 5.5, cr = 3.2;
    double dt = dtHCN(cd, cr);
    cout << "Dien tich HCN (" << cd << "x" << cr << ") la: " << dt << endl;

    double dtS = dtHCN(-2.0, 5.0);
    cout << "Dien tich HCN (-2.0x5.0) la: " << dtS << endl;
    return 0;
}

Output:

Tong cua 10 va 20 la: 30
Dien tich HCN (5.5x3.2) la: 17.6
Dien tich HCN (-2.0x5.0) la: -1

Giải thích code:

  • Hàm tinh_tong được khai báo với kiểu trả về là int. Nó tính tổng và sử dụng return ket_qua; để gửi giá trị ket_qua (kiểu int) trở lại nơi gọi hàm.
  • Trong main, int tong = tinh_tong(so1, so2); gọi hàm tinh_tong, nhận giá trị 30 được trả về, và gán 30 đó vào biến tong.
  • Hàm tinh_dien_tich_hcn có kiểu trả về là double. Nó kiểm tra đầu vào và trả về dai * rong hoặc -1.0.
  • Trong main, giá trị double được trả về từ tinh_dien_tich_hcn được lưu vào biến dien_tich hoặc dien_tich_sai.

Từ khóa return không chỉ trả về giá trị mà còn kết thúc việc thực thi của hàm ngay lập tức. Mọi dòng lệnh sau return trong cùng một khối lệnh sẽ không được thực thi.

2.2. Kiểu trả về void

Nếu một hàm thực hiện một tác vụ (như in ra màn hình, ghi vào file, v.v.) nhưng không cần trả về một giá trị cụ thể nào cho nơi gọi hàm, bạn có thể khai báo kiểu trả về là void.

Ví dụ:

#include <iostream>
#include <string>

using namespace std;

void chaoMung(string t) {
    cout << "Xin chao, " << t << "!" << endl;
}

int main() {
    chaoMung("The Anh");
    chaoMung("Minh Ngoc");
    return 0;
}

Output:

Xin chao, The Anh!
Xin chao, Minh Ngoc!

Giải thích code:

  • Hàm chao_mung được khai báo với kiểu trả về là void.
  • Nó thực hiện công việc in lời chào ra màn hình dựa vào tham số ten.
  • Hàm không trả về bất kỳ giá trị nào cho nơi gọi, nên không thể sử dụng kết quả của nó trong biểu thức hoặc gán cho biến.
2.3. Sử dụng return; trong hàm void

Mặc dù hàm void không trả về giá trị, bạn vẫn có thể sử dụng từ khóa return; (không có giá trị sau nó) để thoát khỏi hàm sớm. Điều này hữu ích khi bạn muốn dừng thực thi hàm dựa trên một điều kiện nào đó.

Ví dụ:

#include <iostream>

using namespace std;

void ktTuoi(int t) {
    if (t < 0) {
        cout << "Tuoi khong the la so am!" << endl;
        return;
    }
    if (t < 18) {
        cout << "Ban chua du tuoi de truy cap." << endl;
    } else {
        cout << "Chao mung ban!" << endl;
    }
}

int main() {
    ktTuoi(25);
    ktTuoi(16);
    ktTuoi(-5);
    return 0;
}

Output:

Chao mung ban!
Ban chua du tuoi de truy cap.
Tuoi khong the la so am!

Giải thích code:

  • Hàm kiem_tra_tuoi nhận một tham số tuoi và có kiểu trả về là void.
  • Nếu tuoi nhỏ hơn 0, một thông báo lỗi được in ra và lệnh return; được thực thi. Điều này làm cho hàm dừng lại ngay lập tức, bỏ qua phần kiểm tra tuổi 18 bên dưới.
  • Nếu tuoi không âm, hàm tiếp tục kiểm tra điều kiện tuoi < 18.

3. Kết hợp Tham số và Kiểu trả về

Trong thực tế, hầu hết các hàm hữu ích đều sử dụng cả tham số (nhận đầu vào) và kiểu trả về (xuất kết quả).

Ví dụ về một hàm tính bình phương của một số (nhận tham số, trả về kết quả):

#include <iostream>

using namespace std;

int bp(int n) {
    return n * n;
}

int main() {
    int v = 7;
    int kq = bp(v);
    cout << "Binh phuong cua " << v << " la: " << kq << endl;
    return 0;
}

Output:

Binh phuong cua 7 la: 49

Giải thích code:

  • Hàm tinh_binh_phuong nhận một tham số int so.
  • Nó tính so * so và trả về kết quả này. Vì soint, kết quả so * so cũng là int, phù hợp với kiểu trả về int của hàm.
  • Trong main, tinh_binh_phuong(gia_tri) được gọi. Giá trị 7 của gia_tri được truyền vào hàm theo giá trị (một bản sao 7 được dùng). Hàm tính 7 * 7 = 49 và trả về giá trị 49.
  • Giá trị 49 được gán vào biến ket_qua_bp.

Comments

There are no comments at the moment.