Bài 8.3: Hàm void và ứng dụng trong C++

Chào mừng các bạn quay trở lại với chuỗi bài học về C++! Ở các bài trước, chúng ta đã cùng nhau tìm hiểu về khái niệm hàm, cách khai báo, định nghĩa và gọi hàm, cũng như các loại tham số khác nhau. Chúng ta cũng đã quen thuộc với các hàm trả về một giá trị cụ thể, ví dụ như hàm trả về int, double, hay string.

Hôm nay, chúng ta sẽ đi sâu vào một loại hàm đặc biệt, đóng vai trò cực kỳ quan trọng trong lập trình C++: hàm void. Vậy hàm void là gì và tại sao chúng ta lại cần nó? Hãy cùng khám phá nhé!

Hàm void là gì?

Trong C++, khi chúng ta khai báo một hàm, chúng ta cần chỉ định kiểu dữ liệu mà hàm đó sẽ trả về sau khi thực hiện xong công việc của mình. Ví dụ:

int congHaiSo(int a, int b) {
    return a + b; // Trả về một giá trị kiểu int
}

double tinhDienTichHinhTron(double banKinh) {
    return 3.14 * banKinh * banKinh; // Trả về một giá trị kiểu double
}

Tuy nhiên, không phải lúc nào hàm cũng cần trả về một giá trị. Đôi khi, chúng ta muốn hàm chỉ thực hiện một hành động hoặc một tập hợp các công việc mà không cần cung cấp một kết quả tính toán cụ thể cho nơi gọi nó. Đây chính là lúc kiểu trả về void phát huy tác dụng.

void trong C++ là một từ khóa đặc biệt, dùng để chỉ ra rằng một hàm không trả về bất kỳ giá trị nào. Nghĩa đen của void là "trống rỗng" hoặc "không có gì".

Khi khai báo một hàm với kiểu trả về là void, chúng ta thông báo cho trình biên dịch và các lập trình viên khác biết rằng hàm này sẽ thực hiện một nhiệm vụ nào đó, có thể là hiển thị thông tin ra màn hình, ghi dữ liệu vào file, thay đổi trạng thái của một đối tượng, v.v., nhưng không cung cấp một kết quả để sử dụng trong các biểu thức.

Cú pháp của Hàm void

Cú pháp khai báo và định nghĩa hàm void cũng tương tự như các hàm thông thường, chỉ khác ở chỗ chúng ta thay kiểu dữ liệu trả về bằng từ khóa void:

void tenHam(kieuThamSo1 thamSo1, kieuThamSo2 thamSo2, ...) {
    // Các câu lệnh thực hiện hành động
    // ...
}

Ví dụ đơn giản nhất về một hàm void:

#include <iostream>

// Khai báo và định nghĩa một hàm void
void chaoMung() {
    // Các câu lệnh trong hàm
    cout << "Chào mừng bạn đến với thế giới hàm void!" << endl;
    cout << "Đây là một hàm không trả về giá trị." << endl;
} // Hàm kết thúc tại đây

int main() {
    // Gọi hàm chaoMung() để nó thực hiện công việc của mình
    chaoMung();

    cout << "Chương trình kết thúc." << endl;

    return 0;
}

Giải thích code:

  • Chúng ta khai báo hàm chaoMung với kiểu trả về là void và không có tham số (()).
  • Trong thân hàm, chúng ta sử dụng cout để in ra hai dòng chữ.
  • Trong hàm main, chúng ta đơn giản là gọi hàm chaoMung bằng tên của nó theo sau bởi dấu ngoặc đơn ().
  • Khi chaoMung(); được thực thi, luồng chương trình nhảy vào thân hàm chaoMung, thực hiện các lệnh in, sau đó quay trở lại điểm gọi (sau chaoMung();) trong main và tiếp tục thực hiện lệnh tiếp theo (cout << "Chương trình kết thúc.").
  • Không có giá trị nào được "nhận lại" từ lời gọi hàm chaoMung().

Tại sao lại sử dụng Hàm void?

Như đã đề cập, hàm void được sử dụng khi mục đích chính của hàm là thực hiện một hành động (action) hoặc tạo ra một hiệu ứng phụ (side effect) thay vì tính toán và trả về một kết quả. Các hiệu ứng phụ phổ biến bao gồm:

  1. In thông tin ra màn hình hoặc file: Rất nhiều hàm void chỉ đơn giản là hiển thị dữ liệu.
  2. Thay đổi trạng thái của chương trình: Ví dụ: cài đặt cấu hình, thay đổi giá trị của biến toàn cục (thường không được khuyến khích), hoặc thay đổi trạng thái của đối tượng (sẽ học sau).
  3. Thực hiện một tập hợp các thao tác: Gom nhóm nhiều lệnh thành một hàm để dễ quản lý và tái sử dụng.
  4. Điều khiển luồng chương trình: Mặc dù hiếm, một hàm void có thể chứa vòng lặp hoặc điều kiện phức tạp.

Hãy xem xét thêm các ví dụ khác để hiểu rõ hơn.

Ví dụ 1: Hàm void với Tham số

Hàm void vẫn có thể nhận tham số để tùy chỉnh hành động của nó.

#include <iostream>
#include <string> // Để sử dụng string

// Hàm void nhận một tên và in lời chào cá nhân
void chaoCaNhan(string tenNguoiDung) {
    cout << "Xin chào, " << tenNguoiDung << "!" << endl;
    cout << "Chúc bạn một ngày tốt lành!" << endl;
}

int main() {
    string ten1 = "Minh";
    string ten2 = "Ngoc";

    chaoCaNhan(ten1); // Gọi hàm với tham số ten1
    chaoCaNhan(ten2); // Gọi hàm với tham số ten2

    return 0;
}

Giải thích code:

  • Hàm chaoCaNhan có kiểu trả về void và nhận một tham số kiểu string.
  • Nhiệm vụ của nó là dùng tham số nhận được để in ra một lời chào cá nhân hóa.
  • Trong main, chúng ta gọi chaoCaNhan hai lần với hai tên khác nhau. Mỗi lần gọi, hàm sẽ thực hiện cùng một hành động in ấn nhưng với dữ liệu khác nhau dựa trên tham số truyền vào.
Ví dụ 2: Hàm void thực hiện một tác vụ cụ thể

Một hàm void có thể làm nhiều việc hơn là chỉ in ra màn hình. Nó có thể thực hiện các tính toán nội bộ hoặc thao tác dữ liệu (nhưng không trả về kết quả của thao tác đó).

#include <iostream>
#include <vector> // Để sử dụng vector
#include <numeric> // Để sử dụng accumulate

// Hàm void tính tổng các phần tử trong vector và in ra
void tinhVaInTongVector(const vector<int>& duLieu) {
    if (duLieu.empty()) {
        cout << "Vector rỗng, không có gì để tính tổng." << endl;
        return; // Có thể sử dụng return; để thoát sớm khỏi hàm void
    }

    long long tong = 0; // Sử dụng long long để tránh tràn số với vector lớn
    for (int phanTu : duLieu) {
        tong += phanTu;
    }
    // Hoặc dùng accumulate:
    // long long tong = accumulate(duLieu.begin(), duLieu.end(), 0LL); // 0LL để đảm bảo kết quả là long long

    cout << "Tổng của các phần tử trong vector là: " << tong << endl;
}

int main() {
    vector<int> cacSo = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    vector<int> vectorRong;

    tinhVaInTongVector(cacSo);      // Gọi hàm với vector có dữ liệu
    tinhVaInTongVector(vectorRong); // Gọi hàm với vector rỗng

    return 0;
}

Giải thích code:

  • Hàm tinhVaInTongVector nhận một tham chiếu hằng đến vector<int>.
  • Nó thực hiện việc tính tổng các phần tử bên trong vector.
  • Quan trọng: Thay vì trả về giá trị tổng (để nơi gọi có thể sử dụng), hàm này in trực tiếp giá trị tổng ra màn hình.
  • Mục đích của hàm là thực hiện hành động in tổng, chứ không phải là cung cấp giá trị tổng. Do đó, void là kiểu trả về phù hợp.
  • Cũng có một ví dụ về việc sử dụng return; bên trong hàm void để thoát sớm nếu vector rỗng.
Ví dụ 3: Hàm void thay đổi giá trị thông qua Tham chiếu

Hàm void có thể gây ra hiệu ứng phụ bằng cách thay đổi giá trị của các biến được truyền vào dưới dạng tham chiếu.

#include <iostream>

// Hàm void nhận một tham chiếu đến số nguyên và tăng giá trị của nó
void tangGiaTri(int& soNguyen) { // Tham số là tham chiếu (int&)
    soNguyen = soNguyen + 5;    // Thay đổi giá trị của biến gốc
    cout << "Trong hàm: Giá trị sau khi tăng là " << soNguyen << endl;
}

int main() {
    int diemSo = 95;
    cout << "Trước khi gọi hàm: Điểm số là " << diemSo << endl;

    tangGiaTri(diemSo); // Gọi hàm và truyền biến diemSo

    cout << "Sau khi gọi hàm: Điểm số là " << diemSo << endl; // Giá trị diemSo đã thay đổi

    return 0;
}

Giải thích code:

  • Hàm tangGiaTri có kiểu trả về void. Nó không trả về một giá trị mới.
  • Tuy nhiên, nó nhận tham số dưới dạng tham chiếu (int& soNguyen). Điều này có nghĩa là soNguyen bên trong hàm không phải là một bản sao, mà là chính biến được truyền vào từ main (ở đây là diemSo).
  • Khi chúng ta thực hiện soNguyen = soNguyen + 5;, chúng ta thực sự đang thay đổi giá trị của biến diemSo trong hàm main.
  • Đây là một ví dụ điển hình về hiệu ứng phụ mà hàm void có thể tạo ra: thay đổi trạng thái bên ngoài hàm mà không cần trả về một giá trị mới.
Ví dụ 4: Hàm void gọi các Hàm khác

Hàm void thường được sử dụng để tổ chức các bước thực hiện bằng cách gọi các hàm khác, kể cả các hàm void khác hoặc hàm có trả về giá trị (nhưng nó không cần sử dụng giá trị trả về đó).

#include <iostream>

// Các hàm void thực hiện từng bước nhỏ
void buocKhoiTao() {
    cout << "Đang thực hiện bước Khởi tạo..." << endl;
    // Giả định có các lệnh khởi tạo phức tạp ở đây
}

void buocXuLy() {
    cout << "Đang thực hiện bước Xử lý dữ liệu..." << endl;
    // Giả định có các lệnh xử lý dữ liệu ở đây
}

void buocKetThuc() {
    cout << "Đang thực hiện bước Kết thúc..." << endl;
    // Giả định có các lệnh dọn dẹp/lưu trữ ở đây
}

// Hàm void tổng hợp các bước trên
void thucHienToanBoQuyTrinh() {
    cout << "--- Bắt đầu Quy trình ---" << endl;
    buocKhoiTao(); // Gọi hàm void khác
    buocXuLy();   // Gọi hàm void khác
    buocKetThuc();  // Gọi hàm void khác
    cout << "--- Kết thúc Quy trình ---" << endl;
}

int main() {
    thucHienToanBoQuyTrinh(); // Chỉ cần gọi hàm void chính
    return 0;
}

Giải thích code:

  • Chúng ta có ba hàm void nhỏ (buocKhoiTao, buocXuLy, buocKetThuc), mỗi hàm thực hiện một phần công việc.
  • Hàm thucHienToanBoQuyTrinh cũng là một hàm void. Nhiệm vụ của nó là điều phối, gọi lần lượt ba hàm nhỏ kia để thực hiện một quy trình hoàn chỉnh.
  • Trong main, chúng ta chỉ cần gọi duy nhất hàm thucHienToanBoQuyTrinh. Điều này giúp làm cho code trong main gọn gàng, dễ đọc và hiểu được luồng chính của chương trình. Chi tiết của từng bước được ẩn bên trong các hàm con.

Gọi Hàm void

Việc gọi hàm void rất đơn giản: bạn chỉ cần viết tên hàm và truyền các tham số (nếu có), kết thúc bằng dấu chấm phẩy ;.

tenHamVoid(thamSo1, thamSo2, ...);

Lưu ý quan trọng: Vì hàm void không trả về giá trị, bạn không thể sử dụng nó trong một biểu thức hoặc gán kết quả của nó cho một biến. Ví dụ, các dòng code sau sẽ gây lỗi biên dịch:

// SAI: Không thể gán kết quả của hàm void cho biến
// int ketQua = chaoMung();

// SAI: Không thể sử dụng hàm void trong biểu thức
// if (chaoMung()) { ... }

// SAI: Không thể in trực tiếp kết quả của hàm void
// cout << chaoMung();

Việc gọi hàm void đơn giản là một lệnh độc lập để yêu cầu chương trình thực hiện công việc được định nghĩa trong hàm đó.

Câu lệnh 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 trong thân hàm của nó. Tuy nhiên, bạn chỉ có thể sử dụng return; một mình, không có bất kỳ giá trị nào theo sau.

Mục đích của return; trong hàm void là để thoát khỏi hàm ngay lập tức khi gặp điều kiện nào đó.

#include <iostream>

void kiemTraTuoiVaIn(int tuoi) {
    if (tuoi < 0) {
        cout << "Lỗi: Tuổi không thể là số âm." << endl;
        return; // Thoát hàm ngay lập tức nếu tuổi âm
    }

    if (tuoi < 18) {
        cout << "Bạn là trẻ vị thành niên (" << tuoi << " tuổi)." << endl;
    } else {
        cout << "Bạn là người trưởng thành (" << tuoi << " tuổi)." << endl;
    }

    // Các lệnh khác ở đây sẽ không được thực thi nếu tuổi < 0
    cout << "Đã kiểm tra xong tuổi." << endl;
}

int main() {
    kiemTraTuoiVaIn(25);
    kiemTraTuoiVaIn(16);
    kiemTraTuoiVaIn(-5); // Sẽ in lỗi và thoát hàm sớm
    kiemTraTuoiVaIn(30); // Sẽ chạy bình thường

    return 0;
}

Giải thích code:

  • Hàm kiemTraTuoiVaIn kiểm tra giá trị tuoi.
  • Nếu tuoi nhỏ hơn 0, nó in ra thông báo lỗi và sử dụng return;. Lệnh này khiến hàm kết thúc ngay lập tức, bỏ qua các lệnh if và lệnh cout << "Đã kiểm tra xong tuổi." phía sau.
  • Nếu tuoi lớn hơn hoặc bằng 0, hàm sẽ tiếp tục thực thi các lệnh kiểm tra tuổi vị thành niên/trưởng thành và in ra dòng "Đã kiểm tra xong tuổi.".

Ứng dụng phổ biến của Hàm void

Hàm void có mặt ở khắp mọi nơi trong lập trình C++ thực tế. Một số ứng dụng phổ biến bao gồm:

  • Thiết lập chương trình (Initialization): Các hàm như void setupGame() hoặc void configureSystem() thường không cần trả về giá trị mà chỉ thực hiện các bước cài đặt ban đầu.
  • Vẽ giao diện (Drawing/Rendering): Trong đồ họa, các hàm như void drawPlayer(), void renderScene() chỉ có nhiệm vụ vẽ lên màn hình mà không tính toán ra giá trị cụ thể.
  • Xử lý sự kiện (Event Handling): Các hàm được gọi khi một sự kiện xảy ra (ví dụ: nhấn nút, di chuyển chuột) thường là void, vì công việc của chúng là phản ứng lại sự kiện chứ không phải trả về kết quả.
  • Ghi nhật ký (Logging): Hàm void logMessage(const string& msg) chỉ có nhiệm vụ ghi thông báo vào file hoặc console.
  • Các thao tác I/O (Input/Output): Nhiều hàm thao tác với file hoặc luồng dữ liệu (như cout << ...;) có thể được gói gọn trong hàm void.

Bài tập ví dụ: C++ Bài 4.A3: Số đẹp

Hiếu là một người thích con số \(5\) nên anh ta đã tự định nghĩa một nguyên nguyên đẹp. Số tự nhiên \(N\) được gọi là số đẹp nếu cộng các chữ số của \(N\) lại ta có một số mà kết thúc bằng \(5\). Ví dụ một số số đẹp là \(14 ( 1 + 4 =5)\), \(654 (6 + 5 + 4= 15)\).

Cho một số \(N\), hãy kiểm tra xem \(N\) có phải là số đẹp hay không.

INPUT FORMAT

Dòng đầu tiên nhập vào số nguyên \(N (1 \leq N \leq 10^9)\).

OUTPUT FORMAT

Nếu \(N\) là số đẹp thì in ra YES ngược lại in ra NO.

Ví dụ 1:

Input
14
Ouput
YES

Ví dụ 2:

Input
653
Output
NO

Giải thích ví dụ mẫu:

  • Ví dụ 1:

    • Input: 14
    • Giải thích: Tổng các chữ số của 14 là 5, kết thúc bằng 5, nên là số đẹp.
  • Ví dụ 2:

    • Input: 653
    • Giải thích: Tổng các chữ số của 653 là 14, không kết thúc bằng 5, nên không phải số đẹp. <br>

Bài toán yêu cầu kiểm tra một số tự nhiên N có phải là "số đẹp" hay không, theo định nghĩa: tổng các chữ số của N phải kết thúc bằng 5.

Các bước thực hiện như sau:

  1. Đọc dữ liệu đầu vào: Cần đọc giá trị của số nguyên N từ bàn hình. Sử dụng cin để làm điều này.

  2. Tính tổng các chữ số của N:

    • Khởi tạo một biến để lưu tổng các chữ số, ban đầu gán bằng 0.
    • Sử dụng một vòng lặp. Vòng lặp này sẽ tiếp tục thực hiện miễn là số N vẫn lớn hơn 0.
    • Trong mỗi lần lặp:
      • Lấy chữ số cuối cùng của N bằng cách sử dụng toán tử modulo (%). Chữ số cuối cùng là N % 10.
      • Cộng chữ số vừa lấy được vào biến tổng.
      • Cập nhật lại N bằng cách chia N cho 10 và lấy phần nguyên (sử dụng toán tử chia /). Điều này loại bỏ chữ số cuối cùng khỏi N.
    • Khi vòng lặp kết thúc (N trở thành 0), biến tổng sẽ chứa tổng tất cả các chữ số ban đầu của N.
  3. Kiểm tra điều kiện "số đẹp":

    • Sau khi tính được tổng các chữ số, bạn cần kiểm tra xem tổng này có kết thúc bằng 5 hay không.
    • Một số kết thúc bằng 5 khi chia lấy dư cho 10 bằng 5.
    • Thực hiện phép kiểm tra: tổng_chữ_số % 10 == 5.
  4. In kết quả:

    • Nếu điều kiện ở bước 3 đúng, tức là tổng các chữ số kết thúc bằng 5, in ra "YES" bằng cout.
    • Ngược lại, in ra "NO" bằng cout.

Lưu ý khi triển khai bằng C++:

  • Bạn cần include thư viện <iostream> để sử dụng cincout.
  • Sử dụng kiểu dữ liệu int cho N và biến tổng là phù hợp với giới hạn 1 \leq N \leq 10^9.
  • Cấu trúc điều kiện if-else được sử dụng để in ra "YES" hoặc "NO".

Hướng giải này tập trung vào logic cơ bản và sử dụng các phép toán số học cùng vòng lặp, là cách tiếp cận hiệu quả cho bài toán này.

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.