Hàm


Khái niệm

Hàm là một đoạn chương trình, gồm các câu lệnh mô tả một số thao tác nhất định và có thể được thực hiện (được gọi) từ nhiều vị trí khác nhau trong chương trình.

Ví dụ

  • Hàm abs(x) tính giá trị tuyệt đối của số x;
  • Hàm sqrt(y) tính căn bậc hai của số y;
  • Hàm min(a, b) tính số bé nhất trong 2 số a, b;
  • ...

Các hàm ví dụ trên là các hàm được thiết kế sẵn trong C++. Ngoài ra ta cũng thể tự viết các hàm cho riêng mình.

Hàm tự định nghĩa

Hàm trả về giá trị

Trong C++, hàm trả về giá trị là một hàm mà sau khi thực hiện xong, nó trả lại một giá trị cụ thể cho nơi gọi hàm.

Cú pháp:

C++
<kiểu_trả_về> <tên_hàm>(<danh_sách_tham_số>) {
    // Các câu lệnh của hàm
}

Trong đó:

  • <kiểu_trả_về> là kiểu dữ liệu cho đầu ra của hàm, có thể là kiểu số nguyên như int, kiểu số thực như double, kiểu string, ...
  • <tên_hàm> là tên dùng để gọi hàm. Quy tắc đặt tên hàm giống quy tắc đặt tên biến.
  • <danh_sách_tham_số> là dữ liệu đầu vào của hàm. Danh sách này là các câu lệnh khai báo các biến tham số, các biến được cách nhau bằng dấu phẩy. Một hàm có thể có nhiều tham số, hoặc không có tham số nào.

Giá trị trả về của hàm được chỉ định thông qua kiểu trả về của hàm và sử dụng từ khóa return. Một hàm trả về giá trị cần có ít nhất một câu lệnh return có thể truy cập đến, và sẽ kết thúc thực thi hàm khi gặp đến câu lệnh return (giống như lệnh break của vòng lặp).

Bản chất int main() cũng là một hàm. Đây là hàm bắt đầu của mọi chương trình C++ và giá trị trả về là một số nguyên cho biết chương trình gặp lỗi hay không. Câu lệnh return 0 cho biết chương trình đã chạy thành công, các giá trị trả về khác \(0\) báo hiệu chương trình gặp lỗi.

Ví dụ 1

Hàm tính chu vi của một hình chữ nhật có 2 cạnh độ dài nguyên \(a, b\).

Mã nguồn

C++
int chu_vi(int a, int b) {
    return (a + b) * 2;
}

Hàm trên nhận 2 tham số là 2 số nguyên \(a, b\) và trả về một số nguyên là chu vi hình chữ nhật. Bây giờ ta có thể gọi hàm chu_vi() nhiều lần trong hàm main():

Mã nguồn

C++
#include <bits/stdc++.h>
using namespace std;

int chu_vi(int a, int b) {
    return (a + b) * 2;
}

int main() {
    cout << chu_vi(2, 5) << '\n';
    cout << chu_vi(3, 3);

    return 0;
}

Đầu ra

14
12

Trong lời gọi hàm chu_vi(2, 5), số 2 và số 5 được gọi là đối số của hàm.

Ví dụ 2

Hàm nhận vào một string \(s\) và một kí tự \(c\), trả về số lần xuất hiện của kí tự \(c\) trong xâu \(s\).

Mã nguồn

C++
int dem_ki_tu(string s, char c) {
    int dem = 0;
    // Duyệt qua từng kí tự trong xâu
    for (int i = 0; i < s.length(); i++) {
        if (s[i] == c) {
            dem++;
        }
    }
    return dem;
}

Ví dụ 3

Hàm nhận vào 3 số dương là độ dài của 3 cạnh, và trả về true nếu 3 cạnh này có thể tạo thành một tam giác hợp lệ, ngược lại trả về false:

Mã nguồn

C++
bool tam_giac(double a, double b, double c) {
    // Áp dụng bất đẳng thức tam giác
    if ((a + b > c) && (b + c > a) && (c + a > b)) {
        return true;
    }
    return false;
}

Hàm trên có thể viết ngắn gọn thành:

Mã nguồn

C++
bool tam_giac(double a, double b, double c) {
    return (a + b > c) && (b + c > a) && (c + a > b);
}

Lưu ý

  • Chương trình sẽ gặp lỗi khi chỉ định kiểu giá trị trả về của hàm, nhưng hàm không trả về giá trị gì. Ví dụ, hàm dưới gặp lỗi nếu tham số đầu vào chia hết cho 3:

Mã nguồn

C++
int so_du_cho_3(int a) {
    int du = a % 3;
    if (du == 1) {
        return 1;
    }
    else if (du == 2) {
        return 2;
    }
}

Cách viết đúng đảm bảo luôn có câu lệnh return ở cuối hàm:

Mã nguồn

C++
int so_du_cho_3(int a) {
    int du = a % 3;
    if (du == 1) {
        return 1;
    }
    else if (du == 2) {
        return 2;
    }
    return 0;
}
  • Cần đảm bảo giá trị trả về của hàm có cùng kiểu dữ liệu với kiểu chỉ định của hàm. Ví dụ sau sẽ gặp lỗi:

Mã nguồn

C++
int so_du_cho_3(int a) {
    int du = a % 3;
    if (du == 1) {
        return "Du 1"; // Lỗi
    }
    ...
}

Hàm không trả về giá trị

Ta sử dụng từ khóa void thay cho kiểu dữ liệu trả về của hàm để khai báo một hàm không trả về giá trị. Các hàm không trả về giá trị thường được sử dụng để thực hiện các tác vụ như in, đọc - ghi, cập nhật trạng thái chương trình. Hàm không trả về giá trị tập trung vào việc thực thi chứ không phải tính và trả về.

Ví dụ 1

Viết hàm in ra tổng, hiệu, tích, thương của hai số nguyên \(x\)\(y\).

Mã nguồn

C++
void in_ra(int x, int y) {
    cout << x << " + " << y << " = " << x + y << '\n';
    cout << x << " - " << y << " = " << x - y << '\n';
    cout << x << " * " << y << " = " << x * y << '\n';
    cout << x << " / " << y << " = " << x / y << '\n';
}

Từ khoá return có thể được dùng để kết thúc hàm.

Ví dụ 2

Viết hàm in các số từ \(l\) đến \(r\) cho đến khi gặp số chia hết cho 10 thì dừng lại.

Mã nguồn

C++
void in_ra(int l, int r) {
    for (int i = l; i <= r; i++) {
        cout << i << "\n";

        if (i % 10 == 0) {
            return;
        }
    }
}

Lợi ích của việc sử dụng hàm

Việc sử dụng hàm giúp cho code trở nên mạch lạc, dễ đọc, có khả năng tái sử dụng, tránh được việc lặp đi lặp lại cùng một dãy lệnh nào đó tương tự nhau trong một chương trình.

Ví dụ 1

Cho các cặp số \((a, b), (c, d), (e, f), (g, h)\). Hãy in ra tổng, hiệu, tích, thương của các cặp số mà không sử dụng hàm.

Mã nguồn

C++
#include <bits/stdc++.h>

using namespace std;

int a, b, c, d, e, f, g, h;

int main() {
    cin >> a >> b >> c >> d >> e >> f >> g >> h;

    cout << a << " + " << b << " = " << a + b << '\n';
    cout << a << " - " << b << " = " << a - b << '\n';
    cout << a << " * " << b << " = " << a * b << '\n';
    cout << a << " / " << b << " = " << a / b << '\n';

    cout << c << " + " << d << " = " << c + d << '\n';
    cout << c << " - " << d << " = " << c - d << '\n';
    cout << c << " * " << d << " = " << c * d << '\n';
    cout << c << " / " << d << " = " << c / d << '\n';

    cout << e << " + " << f << " = " << e + f << '\n';
    cout << e << " - " << f << " = " << e - f << '\n';
    cout << e << " * " << f << " = " << e * f << '\n';
    cout << e << " / " << f << " = " << e / f << '\n';

    cout << g << " + " << h << " = " << g + h << '\n';
    cout << g << " - " << h << " = " << g - h << '\n';
    cout << g << " * " << h << " = " << g * h << '\n';
    cout << g << " / " << h << " = " << g / h << '\n';

    return 0;
}

Ví dụ 2

Cho các cặp số \((a, b), (c, d), (e, f), (g, h)\). Hãy in ra tổng, hiệu, tích, thương của các cặp số sử dụng hàm.

Mã nguồn

C++
#include <bits/stdc++.h>

using namespace std;

int a, b, c, d, e, f, g, h;

void in_ra(int x, int y) {
    cout << x << " + " << y << " = " << x + y << '\n';
    cout << x << " - " << y << " = " << x - y << '\n';
    cout << x << " * " << y << " = " << x * y << '\n';
    cout << x << " / " << y << " = " << x / y << '\n';
}

int main() {
    cin >> a >> b >> c >> d >> e >> f >> g >> h;

    in_ra(a, b);
    in_ra(c, d);
    in_ra(e, f);
    in_ra(g, h);

    return 0;
}

Sử dụng hàm còn giúp người lập trình dễ xác định và sửa lỗi khi chương trình có lỗi. Giả sử chương trình được viết có cấu trúc một vài hàm tuần tự như sau:

Mã nguồn

C++
#include <bits/stdc++.h>

using namespace std;

// Hàm 1

// Hàm 2

// Hàm 3

...

int main() {
    // Thực hiện chương trình

    return 0;
}

Nếu chương trình xảy ra lỗi, thay vì phải dò tìm lỗi trong toàn bộ chương trình, có thể thử lần lượt các đoạn chương trình nhỏ là Hàm 1, Hàm 2, Hàm 3, ... để xem từng hàm có lỗi hay không, giúp nhanh chóng xác định được đoạn mã nguồn nào gây ra lỗi trong chương trình.

Một hàm còn có thể tự gọi lại chính nó, kĩ thuật này được gọi là đệ quy, một kĩ thuật khó sẽ được giới thiệu trong quyển sách tiếp theo trong chuỗi sách lập trình của HNCode.

Hình được vẽ bằng kĩ thuật đệ quy.

Phạm vi của biến

Chương V - Phép toán so sánh, câu lệnh điều kiện, chúng ta đã đề cập đến khối lệnh được quy định trông cặp ngoặc nhọn {}.

Các biến khởi tạo bên trong một khối lệnh sẽ chỉ có hiệu lực trong khối lệnh đó. Khi kết thúc khối lệnh, các biến sẽ bị hủy bỏ.

Ví dụ

Chương trình sau sẽ xảy ra lỗi:

Mã nguồn

C++
if (a > 0 && b > 0) {
    int chu_vi = (a + b) * 2;
    int dien_tich = a * b;
}
cout << chu_vi << " " << dien_tich; // Lỗi

Tương tự, một biến được khai báo trong hàm cũng sẽ bị hủy khi hàm hết thúc.

Mã nguồn

C++
#include <bits/stdc++.h>

using namespace std;

int a, b;

void tong(int x, int y) {
    int s = x + y;
}

int main() {
    cin >> a >> b;
    sum(a, b);
    cout << s; // Lỗi

    return 0;
}

Biến toàn cục

Biến toàn cục là biến được khai báo ở trước hàm int main(), không nằm trong hàm hay khối lệnh nào, có giá trị khởi tạo mặc định và có thể được sử dụng ở bất cứ đâu trong chương trình.

Ví dụ

Mã nguồn

C++
#include <bits/stdc++.h>

using namespace std;

bool x;
int n;
double m;
int a[5];

int main() {
    cout << x << '\n';
    cout << n << '\n';
    cout << m << '\n';
    for(int i = 0; i < 5; i++) { 
        cout << a[i] << " ";
    }

    return 0;
}

Đầu ra

0
0
0
0 0 0 0 0

Biến cục bộ

Biến cục bộ là biến được khai báo trong một hàm hay một khối lệnh, có giá trị khởi tạo không xác định, biến cục bộ chỉ có thể được sử dụng trong hàm hay khối lệnh mà nó được khai báo.

Ví dụ

Mã nguồn

C++
#include <bits/stdc++.h>

using namespace std;

int main() {
    bool x;
    int n;
    double m;
    int a[5];

    cout << x << '\n';
    cout << n << '\n';
    cout << m << '\n';
    for(int i = 0; i < 5; i++) { cout << a[i] << " "; }

    return 0;
}

Đầu ra

0
16980236
4.24399e-314
-2 6422376 4199631 4200224 65535

Lưu ý

Giá trị khởi tạo của biến cục bộ là không xác định, kết quả đầu ra có thể khác biệt trên từng máy tính và phiên bản C++ khác nhau.

Tham số

Tham số (parameter) hay tham số hình thức là các thành phần khi bạn xây dựng hàm, xem xét ví dụ dưới đây thì a, b, c sẽ được gọi là tham số.

Đối số (argument) hay tham số chính thức là các giá trị bạn truyền vào trong hàm khi gọi hàm, xem xét ví dụ dưới đây thì m, n, p được gọi là đối số.

Khi bạn gọi hàm thì lần lượt giá trị của các đối số sẽ được gán cho tham số, trong ví dụ dưới thì m được gán cho a, n được gán cho b, p được gán cho c:

Ví dụ

Mã nguồn

C++
#include <bits/stdc++.h>

using namespace std;

void in_ra(long long a, long long b, long long c) {
    // a = 100, b = 200, c = 300
    a += b + c;
    cout << a << " " << b << " " << c << '\n'; // 600 200 300
}

int main() {
    int m = 100, n = 200, p = 300;
    in_ra(m, n, p);
    // thay đổi tham số không ảnh hưởng tới đối số 
    cout << m << " " << n << " " << p << '\n'; // 100 200 300

    return 0;
}

Đầu ra

600 200 300
100 200 300

Lưu ý

Kiểu dữ liệu của đối số và tham số nên trùng nhau, hoặc kiểu dữ liệu của tham số nên lớn hơn kiểu dữ liệu của đối số. Ví dụ bạn xây dựng 1 hàm có tham số là long long thì nó có thể áp dụng với 1 số int nhưng ngược lại thì không.

Có hai kiểu truyền tham số: truyền tham trị và truyền tham chiếu.

Tham trị

Truyền tham trị là truyền cho đối số một bản sao. Mọi hành động trên tham trị không ảnh hưởng tới đối số gốc.

Ví dụ

Mã nguồn

C++
#include <bits/stdc++.h>

using namespace std;

void thay_doi(int n) {
    n = 4;
    cout << n << '\n'; // 4
}

int main() {
    int n;
    n = 2;
    thay_doi(n);
    cout << n << '\n'; // 2

    return 0;
}

Đầu ra

4
2

Ta sử dụng truyền tham trị cho hàm thay_doi(int n) nên giá trị của đối số n không thay đổi trong hàm main().

Tham chiếu

Trong truyền tham trị, hàm chỉ nhận được bản sao của giá trị đối số, nên không thể thay đổi giá trị gốc bên ngoài hàm. Ngược lại, trong truyền tham chiếu, hàm nhận được \textbf{địa chỉ của đối số} sử dụng kí hiệu &, cho phép nó thay đổi trực tiếp giá trị ban đầu. Mọi hành động trên tham chiếu sẽ luôn ảnh hưởng tới đối số gốc.

Ví dụ

Mã nguồn

C++
#include <bits/stdc++.h>

using namespace std;

void thay_doi(int &n) {
    n = 4;
    cout << n << '\n'; // 4
}

int main() {
    int n;
    n = 2;
    thay_doi(n);
    cout << n << '\n'; // 4

    return 0;
}

Đầu ra

4
4

Ta sử dụng truyền tham chiếu cho hàm thay_doi(int &n) nên giá trị của đối số n bị thay đổi theo hàm.

Hàm có sẵn trong C++

Thư viện chuẩn của C++ cung cấp nhiều hàm có sẵn giúp ích cho lập trình viên.

Hàm toán học

Tên hàm Giá trị trả về Kiểu trả về Ví dụ
sqrt(x) Căn bậc hai của \(x\) double sqrt(25) \(\rightarrow\) 5
abs(x) Giá trị tuyệt đối của \(x\) int / double abs(-7) \(\rightarrow\) 7
floor(x) Giá trị làm tròn xuống của \(x\) double floor(2.8) \(\rightarrow\) 2
ceil(x) Giá trị làm tròn lên của \(x\) double ceil(2.1) \(\rightarrow\) 3
round(x) Giá trị làm tròn gần nhất của \(x\) double round(2.5) \(\rightarrow\) 3.0
min(a, b) Giá trị nhỏ nhất trong hai số \(a, b\) int / double min(6, 9) \(\rightarrow\) 6
max(a, b) Giá trị lớn nhất trong hai số \(a, b\) int / double max(6, 9) \(\rightarrow\) 9

Hàm xử lí kí tự

Tên hàm Giá trị trả về Kiểu trả về Ví dụ
tolower(c) Kí tự chữ thường của \(c\) int tolower('A') \(\rightarrow\) 'a'
toupper(c) Kí tự chữ hoa của \(c\) int toupper('b') \(\rightarrow\) 'B'
isalpha(c) true nếu \(c\) là kí tự chữ cái bool isalpha('x') \(\rightarrow\) true
isdigit(c) true nếu \(c\) là kí tự chữ số bool isdigit('5') \(\rightarrow\) true
isalnum(c) true nếu \(c\) là kí tự chữ cái / chữ số bool isalnum('x') \(\rightarrow\) true
isspace(c) true nếu \(c\) là kí tự dấu cách bool isspace(' ') \(\rightarrow\) true

Hàm xử lí xâu

Ví dụ với xâu s = "HNCode", ta có các hàm sau:

Tên hàm Tác vụ thực thi Ví dụ
s.length() / s.size() Trả về độ dài xâu s s.length() \(\rightarrow\) 6
s.empty() Trả về true nếu xâu rỗng s.empty() \(\rightarrow\) false
s.substr(pos, len) Trả về xâu con từ vị trí pos, độ dài len s.substr(2, 3) \(\rightarrow\) "Cod"
s.find(x) Trả về vị trí đầu tiên tìm thấy x trong xâu s s.find('H') \(\rightarrow\) 0
s.insert(pos, x) Chèn xâu x vào vị trí pos trong xâu s s.insert(2, "HN"); \(s \rightarrow\) "HNHNCode"
s.erase(pos, len) Xoá len kí tự từ vị trí pos trong xâu s s.erase(2, 3) \(s \rightarrow\) "HNe"
s.replace(pos, len, x) Thay thế len kí tự từ vị trí pos trong xâu s bằng xâu x s.replace(1, 5, "ello"); \(s \rightarrow\) "Hello"