Hàm trong C++: tham số mặc định, nạp chồng hàm và tham chiếu

Hàm trong C++: tham số mặc định, nạp chồng hàm và tham chiếu - Khuê Nguyễn Creator

Trong bài này chúng ta sẽ học hàm trong C++: cách khai báo và định nghĩa hàm, truyền giá trị và truyền tham chiếu, tham số mặc định, nạp chồng hàm, và cuối cùng là tách chương trình ra nhiều file. Hàm main của chương trình thư viện sau mấy bài vừa rồi đã khá dài, học xong bài này các bạn sẽ chia nó thành từng phần nhỏ, mỗi phần làm đúng một việc.

Bài 8 trong Serie Lập trình C++ từ A tới Z

Hàm trong C++ là gì?

Hàm (function) là một khối code được đặt tên, làm một việc cụ thể. Viết một lần, gọi ở bao nhiêu chỗ cũng được.

Ví dụ đời thường: chiếc máy xay sinh tố. Các bạn cho nguyên liệu vào (tham số), bấm nút (gọi hàm), máy trả ra ly sinh tố (giá trị trả về). Bên trong máy làm gì thì người dùng không cần quan tâm.

Viết chương trình thành nhiều hàm có 3 lợi ích:

  • Không lặp code: công thức tính tiền phạt viết một chỗ, dùng ở mọi nơi. Đổi quy định thì sửa đúng một chỗ.
  • Dễ đọc: main chỉ còn là danh sách các bước như hienMenu(), tinhTienPhat(), đọc như đọc mục lục.
  • Dễ kiểm tra: mỗi hàm nhỏ có thể chạy thử riêng.

Khai báo và định nghĩa hàm

Một hàm gồm kiểu trả về, tên hàm, danh sách tham số trong ngoặc tròn và thân hàm. Ví dụ hàm tính tiền phạt trả sách muộn:

#include <iostream>

// Khai bao ham (prototype)
int tinhTienPhat(int soNgayMuon);

int main()
{
    std::cout << "Muon 10 ngay, phat: " << tinhTienPhat(10) << " dong\n";
    std::cout << "Muon 20 ngay, phat: " << tinhTienPhat(20) << " dong\n";
    return 0;
}

// Dinh nghia ham
int tinhTienPhat(int soNgayMuon)
{
    constexpr int HAN = 14;
    constexpr int PHAT_MOI_NGAY = 2000;

    if (soNgayMuon <= HAN)
        return 0;
    return (soNgayMuon - HAN) * PHAT_MOI_NGAY;
}

Hàm tinhTienPhat trả về tiền phạt 0 đồng khi mượn 10 ngày, 12000 đồng khi mượn 20 ngày

  • int đầu tiên là kiểu trả về. Hàm không trả về gì thì dùng void.
  • int soNgayMuon là tham số. Khi gọi tinhTienPhat(20), giá trị 20 được gán vào soNgayMuon.
  • return kết thúc hàm ngay lập tức và trả giá trị về chỗ gọi. Nhờ vậy khi chưa quá hạn, hàm return 0 luôn mà không cần else.

Dòng int tinhTienPhat(int soNgayMuon); phía trên main gọi là khai báo hàm (prototype). Nó báo trước cho trình biên dịch biết hàm này nhận gì, trả về gì, phần thân viết ở chỗ khác. Bỏ dòng này đi mà vẫn để phần định nghĩa nằm dưới main thì g++ báo lỗi:

g++ báo lỗi tinhTienPhat was not declared in this scope khi gọi hàm trước khi khai báo

Trình biên dịch đọc file từ trên xuống. Tới dòng gọi hàm trong main mà chưa gặp khai báo nào thì nó không biết tinhTienPhat là gì. Có 2 cách sửa: thêm prototype lên đầu file, hoặc chuyển cả hàm lên trên main.

Truyền giá trị và truyền tham chiếu

Truyền giá trị

Mặc định C++ truyền giá trị (pass by value): hàm nhận một bản sao của đối số. Sửa tham số trong hàm không ảnh hưởng tới biến bên ngoài.

Tham chiếu là gì?

Tham chiếu (reference) là một tên gọi khác của một biến đã có, khai báo bằng dấu & sau kiểu dữ liệu:

#include <iostream>

int main()
{
    int soSach = 10;
    int& bietDanh = soSach;   // bietDanh la ten khac cua soSach

    bietDanh = 20;
    std::cout << "soSach = " << soSach << ", bietDanh = " << bietDanh << '\n';
    std::cout << "Dia chi soSach:   " << &soSach << '\n';
    std::cout << "Dia chi bietDanh: " << &bietDanh << '\n';
    return 0;
}

Tham chiếu bietDanh và biến soSach có cùng giá trị và cùng địa chỉ

Giống như một người tên khai sinh là “An” và biệt danh là “Tèo”: gọi tên nào cũng là một người. Gán bietDanh = 20 thì soSach cũng thành 20, và địa chỉ của 2 tên trùng nhau. Địa chỉ trên máy các bạn sẽ khác, nhưng 2 dòng luôn giống nhau.

Tham chiếu bắt buộc phải gắn với một biến ngay khi khai báo, và sau đó không đổi sang biến khác được. Đây là điểm khác lớn nhất so với con trỏ mà chúng ta sẽ học sau.

Truyền tham chiếu vào hàm

Tham chiếu phát huy tác dụng khi làm tham số của hàm. So sánh 2 hàm tăng số lượng sách:

#include <iostream>

void tangSoLuongBan(int soLuong)       // truyen gia tri: nhan ban sao
{
    soLuong = soLuong + 1;
}

void tangSoLuongThat(int& soLuong)     // truyen tham chieu: lam viec tren bien goc
{
    soLuong = soLuong + 1;
}

int main()
{
    int soSach = 5;

    tangSoLuongBan(soSach);
    std::cout << "Sau tangSoLuongBan:  " << soSach << '\n';

    tangSoLuongThat(soSach);
    std::cout << "Sau tangSoLuongThat: " << soSach << '\n';
    return 0;
}

Truyền giá trị không đổi biến gốc, truyền tham chiếu tăng soSach từ 5 lên 6

Sơ đồ truyền giá trị tạo bản sao, truyền tham chiếu dùng chung một ô nhớ với biến gốc

Hàm tangSoLuongBan sửa bản sao, nên soSach vẫn là 5. Hàm tangSoLuongThat nhận tham chiếu, soLuong chính là soSach, nên sửa là biến gốc đổi theo.

Trong C, muốn hàm sửa được biến bên ngoài thì phải truyền con trỏ và viết *p khắp nơi, như bài truyền tham chiếu, tham trị và con trỏ trong C. Sang C++, tham chiếu làm việc đó gọn hơn nhiều.

Truyền tham chiếu hằng const&

Truyền giá trị một std::string dài hay một đối tượng lớn nghĩa là sao chép toàn bộ dữ liệu mỗi lần gọi hàm. Muốn không sao chép mà cũng không cho hàm sửa đối số, dùng tham chiếu hằng const&:

#include <iostream>
#include <string>

void inTieuDe(const std::string& tieuDe)   // khong sao chep, khong sua duoc
{
    std::cout << "=== " << tieuDe << " ===\n";
}

int main()
{
    std::string ten = "Danh sach sach";
    inTieuDe(ten);
    inTieuDe("Sach dang muon");             // const& nhan duoc ca chuoi tam thoi
    return 0;
}

Hàm nhận const std::string& in tiêu đề từ biến và từ chuỗi tạm

Quy tắc chọn cách truyền tham số mà các bạn sẽ dùng suốt serie:

Tham số Hàm có sửa không? Cách truyền
Kiểu nhỏ: int, double, char, bool Không Truyền giá trị: int x
Kiểu lớn: std::string, mảng, đối tượng Không Tham chiếu hằng: const std::string& s
Bất kỳ kiểu nào Có, muốn biến gốc đổi theo Tham chiếu: int& x

Đây cũng là lý do bài vòng lặp viết for (const std::string& ten : danhSach): duyệt danh sách sách mà không sao chép từng cuốn.

Tham số mặc định

C++ cho phép gán sẵn giá trị mặc định cho tham số. Gọi hàm mà không truyền tham số đó thì giá trị mặc định được dùng:

#include <iostream>

void inDuongKe(char kyTu = '-', int doDai = 20)
{
    for (int i = 0; i < doDai; i++)
        std::cout << kyTu;
    std::cout << '\n';
}

int main()
{
    inDuongKe();           // dung ca 2 gia tri mac dinh
    inDuongKe('=');        // doDai van la 20
    inDuongKe('*', 10);    // truyen du 2 tham so
    return 0;
}

Hàm inDuongKe với tham số mặc định in ra 3 đường kẻ khác nhau

Tham số mặc định hợp với những tham số mà hầu như lần gọi nào cũng dùng cùng một giá trị. Chỗ gọi hàm gọn hơn, chỉ truyền khi muốn khác đi.

Lưu ý: Tham số có giá trị mặc định phải nằm ở cuối danh sách: viết void f(int a = 1, int b) là lỗi biên dịch. Và nếu hàm có cả prototype lẫn định nghĩa thì chỉ ghi giá trị mặc định ở prototype, ghi ở cả 2 chỗ g++ cũng báo lỗi.

Nạp chồng hàm

Nạp chồng hàm (function overloading) là viết nhiều hàm cùng tên nhưng khác nhau về danh sách tham số: khác số lượng hoặc khác kiểu. Khi gọi, trình biên dịch tự chọn đúng hàm dựa vào đối số truyền vào.

Ví dụ chức năng tìm sách của thư viện, tìm theo mã, theo tên, hoặc theo tác giả và năm:

#include <iostream>
#include <string>

void timSach(int maSach)
{
    std::cout << "Tim theo ma: " << maSach << '\n';
}

void timSach(const std::string& tenSach)
{
    std::cout << "Tim theo ten: " << tenSach << '\n';
}

void timSach(const std::string& tacGia, int namXuatBan)
{
    std::cout << "Tim sach cua " << tacGia << " xuat ban nam " << namXuatBan << '\n';
}

int main()
{
    timSach(1024);
    timSach("Lap trinh C++");
    timSach("Bjarne Stroustrup", 2013);
    return 0;
}

Nạp chồng hàm timSach theo mã, theo tên và theo tác giả cùng năm xuất bản

Trong C, 3 hàm này phải đặt 3 tên khác nhau như timTheoMa, timTheoTen, timTheoTacGia. Sang C++, cùng một tên timSach cho cùng một ý nghĩa, người dùng hàm không phải nhớ nhiều tên.

Hai hàm chỉ khác nhau kiểu trả về thì không được coi là nạp chồng. Ví dụ int laySoLuong() và double laySoLuong() sẽ bị báo lỗi, vì khi gọi laySoLuong() trình biên dịch không có căn cứ gì để chọn.

Lỗi gọi hàm mơ hồ

Khi đối số không khớp chính xác với hàm nào mà chuyển đổi được sang nhiều hàm, g++ sẽ không đoán thay các bạn:

#include <iostream>

void inGia(int gia)    { std::cout << "Gia (int): " << gia << '\n'; }
void inGia(double gia) { std::cout << "Gia (double): " << gia << '\n'; }

int main()
{
    long gia = 125000;
    inGia(gia);
    return 0;
}

g++ báo lỗi call of overloaded inGia is ambiguous khi truyền biến long

Biến long chuyển được sang cả int lẫn double, mức độ “tốt” như nhau, nên g++ báo call of overloaded 'inGia(long int&)' is ambiguous. Cách sửa là cho đối số đúng kiểu (int gia), hoặc ép kiểu rõ ràng inGia(static_cast<int>(gia)).

Lưu ý: Chỉ nạp chồng các hàm làm cùng một việc. Hai hàm làm 2 việc khác nhau mà đặt chung tên sẽ khiến người đọc code hiểu nhầm, dù trình biên dịch không phàn nàn gì.

Tách chương trình ra nhiều file

Chương trình lớn dần thì một file main.cpp sẽ dài hàng nghìn dòng. Cách làm chuẩn là tách các hàm ra file riêng:

  • File header .h: chứa các khai báo hàm, giống như “mục lục” cho các file khác include vào.
  • File .cpp: chứa phần định nghĩa (thân) của các hàm.

File thu_vien.h:

#ifndef THU_VIEN_H
#define THU_VIEN_H

void hienMenu();
int tinhTienPhat(int soNgayMuon);

#endif

3 dòng #ifndef, #define, #endif gọi là include guard, đảm bảo nội dung file chỉ được chép vào một lần dù bị #include nhiều lần.

File thu_vien.cpp:

#include "thu_vien.h"
#include <iostream>

void hienMenu()
{
    std::cout << "\n1. Tinh tien phat  0. Thoat\n";
    std::cout << "Chon: ";
}

int tinhTienPhat(int soNgayMuon)
{
    constexpr int HAN = 14;
    constexpr int PHAT_MOI_NGAY = 2000;

    if (soNgayMuon <= HAN)
        return 0;
    return (soNgayMuon - HAN) * PHAT_MOI_NGAY;
}

File main.cpp giờ chỉ còn luồng chính của chương trình:

#include "thu_vien.h"
#include <iostream>

int main()
{
    int luaChon;
    do
    {
        hienMenu();
        std::cin >> luaChon;
        if (luaChon == 1)
        {
            int soNgay;
            std::cout << "So ngay da muon: ";
            std::cin >> soNgay;
            std::cout << "Tien phat: " << tinhTienPhat(soNgay) << " dong\n";
        }
    } while (luaChon != 0);
    return 0;
}

Header của mình thì include bằng nháy kép "thu_vien.h", thư viện chuẩn thì dùng ngoặc nhọn <iostream>. Khi biên dịch phải đưa tất cả file .cpp vào lệnh g++:

Biên dịch chương trình thư viện gồm main.cpp và thu_vien.cpp rồi chạy menu tính tiền phạt

Quên đưa thu_vien.cpp vào lệnh biên dịch thì sao? Đây chính là lỗi liên kết mà chúng ta đã gặp ở bài cú pháp C++ cơ bản và cách biên dịch:

Lỗi undefined reference to hienMenu khi quên đưa thu_vien.cpp vào lệnh g++

Bước biên dịch main.cpp vẫn qua vì header đã khai báo đủ các hàm. Tới bước liên kết thì không tìm thấy phần thân của hienMenu và tinhTienPhat, nên báo undefined reference.

Lưu ý: Tham số mặc định đặt trong header. Định nghĩa hàm (phần thân) thì đặt trong file .cpp, không đặt trong header: header được include vào 2 file .cpp thì hàm bị định nghĩa 2 lần và lỗi liên kết multiple definition.

Với VS Code, file tasks.json ở bài cài đặt chỉ biên dịch file đang mở. Với project nhiều file, các bạn đổi "${file}" thành "${fileDirname}/*.cpp" để biên dịch tất cả file .cpp trong thư mục.

Bài tập

  1. Viết hàm bool laNamNhuan(int nam) và dùng nó để in ra các năm nhuận từ 2000 tới 2030.
  2. Viết hàm void hoanDoi(int& a, int& b) đổi giá trị 2 biến. Thử bỏ dấu & đi và giải thích kết quả.
  3. Nạp chồng hàm tinhDienTich cho hình vuông (1 tham số), hình chữ nhật (2 tham số) và hình tròn (1 tham số kiểu double). Hình vuông và hình tròn đều có 1 tham số, có vấn đề gì không?
  4. Tách chương trình menu thư viện ở bài vòng lặp thành 3 file main.cpp, thu_vien.h, thu_vien.cpp, mỗi chức năng một hàm.

Lời giải sẽ có trong bài tổng hợp bài tập C++ cơ bản nhé.

Kết

Hàm trong C++ là công cụ đầu tiên để chia nhỏ chương trình, và C++ có thêm 3 thứ mà C không có: tham chiếu giúp sửa biến gốc mà không cần con trỏ, tham số mặc định cho chỗ gọi gọn hơn, nạp chồng hàm cho phép một tên dùng với nhiều kiểu dữ liệu. Nhớ quy tắc truyền tham số: kiểu nhỏ truyền giá trị, kiểu lớn truyền const&, cần sửa thì truyền &.

Muốn tra cứu chi tiết về hàm, tham số mặc định và cách chọn hàm nạp chồng, các bạn xem thêm tài liệu chuẩn trên cppreference.

Các bạn muốn so sánh với C có thể xem bài hàm trong lập trình C. Bài trước là vòng lặp trong C++, còn bài tiếp theo chúng ta sẽ làm quen với std::string và std::vector, 2 kiểu dữ liệu dùng nhiều nhất trong C++ hiện đại. Thấy bài viết có ích thì chia sẻ cho bạn bè nhé.

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *