Bài 5.1: Vòng lặp for và các biến thể trong C++

Chào mừng quay trở lại với chuỗi bài viết về C++! Trong lập trình, rất nhiều lần chúng ta cần thực hiện cùng một tác vụ lặp đi lặp lại. Thay vì viết cùng một đoạn code nhiều lần, chúng ta sử dụng các cấu trúc điều khiển lặp, hay còn gọi là vòng lặp. C++ cung cấp nhiều loại vòng lặp khác nhau, và trong bài viết này, chúng ta sẽ đi sâu vào một trong những loại phổ biến và mạnh mẽ nhất: vòng lặp for cùng với các biến thể và cách sử dụng hiệu quả của nó.

Vòng lặp for cực kỳ linh hoạt và thường được sử dụng khi bạn biết trước số lần lặp hoặc khi bạn cần kiểm soát chặt chẽ quá trình lặp dựa trên một biến đếm. Hãy cùng tìm hiểu kỹ nhé!

1. Cú pháp cơ bản của vòng lặp for

Vòng lặp for truyền thống trong C++ có cấu trúc ba phần rất rõ ràng:

for (khởi tạo; điều kiện; cập nhật) {
    // Khối lệnh sẽ được thực thi lặp đi lặp lại
}

Hãy phân tích từng phần:

  1. khởi tạo (initialization): Phần này chỉ được thực thi một lần duy nhất trước khi vòng lặp bắt đầu. Nó thường dùng để khai báo và khởi tạo biến điều khiển vòng lặp (biến đếm). Bạn có thể khai báo biến ngay tại đây.
  2. điều kiện (condition): Đây là một biểu thức boolean được kiểm tra trước mỗi lần lặp. Nếu biểu thức này cho kết quả true, khối lệnh bên trong vòng lặp sẽ được thực thi. Nếu kết quả là false, vòng lặp sẽ dừng lại.
  3. cập nhật (update): Phần này được thực thi sau mỗi lần hoàn thành khối lệnh bên trong vòng lặp. Nó thường dùng để thay đổi giá trị của biến điều khiển vòng lặp (ví dụ: tăng hoặc giảm biến đếm).

Lưu ý quan trọng: Vòng lặp for sẽ tiếp tục chạy miễn là điều kiện còn đúng (true).

Ví dụ cơ bản: Đếm từ 1 đến 5

Hãy xem một ví dụ kinh điển về vòng lặp for để in ra các số từ 1 đến 5:

#include <iostream>

int main() {
    for (int i = 1; i <= 5; ++i) {
        cout << "Số: " << i << endl;
    }
    return 0;
}

Giải thích code:

  • #include <iostream>: Bao gồm thư viện nhập xuất để sử dụng coutendl.
  • int main(): Hàm chính của chương trình.
  • for (int i = 1; i <= 5; ++i): Đây là vòng lặp for.
    • int i = 1;: Phần khởi tạo. Một biến int tên là i được khai báo và gán giá trị ban đầu là 1. Biến i này chỉ tồn tại trong phạm vi của vòng lặp for.
    • i <= 5;: Phần điều kiện. Trước mỗi lần lặp, chương trình kiểm tra xem i có nhỏ hơn hoặc bằng 5 không.
    • ++i;: Phần cập nhật. Sau khi thực hiện khối lệnh { ... }, giá trị của i được tăng lên 1.
  • cout << "Số: " << i << endl;: Khối lệnh bên trong vòng lặp. Nó sẽ in ra chuỗi "Số: " tiếp theo là giá trị hiện tại của i và xuống dòng.

Quá trình chạy của vòng lặp:

  1. Khởi tạo: i được đặt là 1.
  2. Kiểm tra điều kiện: 1 <= 5true. Thực thi khối lệnh.
  3. Thực thi khối lệnh: In ra "Số: 1".
  4. Cập nhật: i trở thành 2.
  5. Kiểm tra điều kiện: 2 <= 5true. Thực thi khối lệnh.
  6. Thực thi khối lệnh: In ra "Số: 2".
  7. Cập nhật: i trở thành 3.
  8. Kiểm tra điều kiện: 3 <= 5true. Thực thi khối lệnh.
  9. Thực thi khối lệnh: In ra "Số: 3".
  10. Cập nhật: i trở thành 4.
  11. Kiểm tra điều kiện: 4 <= 5true. Thực thi khối lệnh.
  12. Thực thi khối lệnh: In ra "Số: 4".
  13. Cập nhật: i trở thành 5.
  14. Kiểm tra điều kiện: 5 <= 5true. Thực thi khối lệnh.
  15. Thực thi khối lệnh: In ra "Số: 5".
  16. Cập nhật: i trở thành 6.
  17. Kiểm tra điều kiện: 6 <= 5false. Vòng lặp kết thúc.

2. Các biến thể và sự linh hoạt của vòng lặp for

Sức mạnh của for nằm ở sự linh hoạt của ba phần trong dấu ngoặc đơn (). Bạn có thể tùy chỉnh chúng theo nhiều cách khác nhau.

2.1. Bỏ qua các phần (hoặc tất cả!)

Bạn hoàn toàn có thể bỏ trống một hoặc nhiều phần trong cú pháp for. Tuy nhiên, bạn vẫn cần giữ dấu chấm phẩy ; để phân cách các phần.

  • Bỏ qua phần khởi tạo: Khi biến đếm đã được khởi tạo trước vòng lặp.

    #include <iostream>
    
    int main() {
        int i = 1; // Khởi tạo biến i bên ngoài
    
        for (; i <= 5; ++i) { // Phần khởi tạo bị bỏ trống
            cout << "Số: " << i << endl;
        }
        return 0;
    }
    
  • Bỏ qua phần cập nhật: Khi việc cập nhật biến đếm được thực hiện bên trong khối lệnh của vòng lặp.

    #include <iostream>
    
    int main() {
        for (int i = 1; i <= 5; ) { // Phần cập nhật bị bỏ trống
            cout << "Số: " << i << endl;
            i++; // Cập nhật biến i bên trong khối lệnh
        }
        return 0;
    }
    
  • Bỏ qua điều kiện: Nếu bạn bỏ qua điều kiện, vòng lặp sẽ được coi là luôn đúng (true), dẫn đến một vòng lặp vô hạn trừ khi bạn sử dụng break để thoát. Cú pháp for (;;) là cách phổ biến để tạo vòng lặp vô hạn.

    #include <iostream>
    
    int main() {
        int count = 0;
        for (;;) { // Vòng lặp vô hạn
            cout << "Đang lặp..." << endl;
            count++;
            if (count == 3) {
                break; // Thoát khỏi vòng lặp khi count đạt 3
            }
        }
        cout << "Vòng lặp đã dừng." << endl;
        return 0;
    }
    

    Giải thích: Vòng lặp này sẽ chạy mãi mãi nếu không có câu lệnh break. Chúng ta sử dụng biến count để đếm số lần lặp và thoát khi cần thiết.

2.2. Sử dụng nhiều biến trong khởi tạo và cập nhật

Bạn có thể khai báo và khởi tạo nhiều biến, cũng như thực hiện nhiều thao tác cập nhật trong các phần tương ứng, bằng cách phân cách chúng bằng dấu phẩy ,.

#include <iostream>

int main() {
    for (int i = 0, j = 10; i < 5 && j > 5; ++i, --j) {
        cout << "i: " << i << ", j: " << j << endl;
    }
    return 0;
}

Giải thích: Vòng lặp này khởi tạo hai biến ij, chạy khi cả i < 5j > 5 cùng đúng, đồng thời tăng i và giảm j sau mỗi lần lặp.

2.3. Lặp ngược hoặc bước nhảy tùy ý

Bạn không nhất thiết phải lặp theo chiều tăng dần từng bước 1.

  • Lặp ngược (Đếm lùi):

    #include <iostream>
    
    int main() {
        for (int i = 5; i >= 1; --i) {
            cout << "Đếm lùi: " << i << endl;
        }
        return 0;
    }
    

    Giải thích: Khởi tạo i = 5, điều kiện là i >= 1, và cập nhật là giảm i đi 1 (--i).

  • Bước nhảy tùy ý: Tăng hoặc giảm biến đếm với một giá trị khác 1.

    #include <iostream>
    
    int main() {
        // Lặp các số chẵn từ 0 đến 10
        for (int i = 0; i <= 10; i += 2) {
            cout << "Số chẵn: " << i << endl;
        }
        return 0;
    }
    

    Giải thích: Phần cập nhật sử dụng i += 2 để tăng i lên 2 sau mỗi lần lặp.

3. Vòng lặp for lồng nhau (Nested for loops)

Bạn có thể đặt một vòng lặp for bên trong khối lệnh của một vòng lặp for khác. Điều này thường được sử dụng để làm việc với các cấu trúc dữ liệu hai chiều (như ma trận) hoặc in ra các mẫu hình.

#include <iostream>

int main() {
    // In ra một hình vuông 3x3 dấu *
    for (int i = 0; i < 3; ++i) { // Vòng lặp ngoài (điều khiển hàng)
        for (int j = 0; j < 3; ++j) { // Vòng lặp trong (điều khiển cột)
            cout << "* "; // In dấu * và khoảng trắng
        }
        cout << endl; // Xuống dòng sau khi hoàn thành một hàng
    }
    return 0;
}

Giải thích:

  • Vòng lặp ngoài chạy 3 lần (i từ 0 đến 2). Mỗi lần vòng lặp ngoài chạy, nó sẽ thực thi toàn bộ vòng lặp bên trong.
  • Vòng lặp trong chạy 3 lần (j từ 0 đến 2) cho mỗi lần vòng lặp ngoài chạy.
  • Kết quả là khối lệnh cout << "* "; được thực thi tổng cộng 3 * 3 = 9 lần.
  • cout << endl; được thực thi sau mỗi khi vòng lặp trong hoàn thành, tức là sau mỗi hàng.

Output sẽ là:

* * * 
* * * 
* * *

4. Vòng lặp range-based for (C++11 trở lên)

Từ C++11, một biến thể mới của vòng lặp for được giới thiệu, gọi là range-based for loop (vòng lặp for dựa trên phạm vi). Vòng lặp này được thiết kế để duyệt qua tất cả các phần tử trong một dãy (range), chẳng hạn như mảng, vector, chuỗi (string), và các container khác một cách dễ dàng và an toàn hơn.

Cú pháp rất đơn giản:

for (kiểu_dữ_liệu tên_biến : dãy) {
    // Khối lệnh thực thi cho mỗi phần tử
}
  • kiểu_dữ_liệu tên_biến: Khai báo một biến sẽ nhận giá trị của từng phần tử trong dãy trong mỗi lần lặp. kiểu_dữ_liệu nên khớp với kiểu dữ liệu của các phần tử trong dãy. Thường dùng auto để trình biên dịch tự suy luận kiểu hoặc const auto& để tránh sao chép không cần thiết và đảm bảo không làm thay đổi giá trị của phần tử gốc (trừ khi bạn muốn thay đổi, khi đó dùng auto&).
  • dãy: Đây là container hoặc mảng mà bạn muốn duyệt qua.
Ví dụ với range-based for
#include <iostream>
#include <vector> // Cần cho vector
#include <string> // Cần cho string

int main() {
    // Ví dụ với mảng
    int mang_so[] = {10, 20, 30, 40, 50};
    cout << "Duyệt mảng:" << endl;
    for (int so : mang_so) {
        cout << so << " ";
    }
    cout << endl << endl;

    // Ví dụ với vector
    vector<string> danh_sach_ten = {"Alice", "Bob", "Charlie"};
    cout << "Duyệt vector:" << endl;
    for (const string& ten : danh_sach_ten) { // Sử dụng const auto& để hiệu quả hơn
        cout << ten << " ";
    }
    cout << endl << endl;

    // Ví dụ với string (chuỗi ký tự)
    string loi_chao = "Xin chao!";
    cout << "Duyệt chuỗi:" << endl;
    for (char ky_tu : loi_chao) {
        cout << ky_tu << "-";
    }
    cout << endl;

    return 0;
}

Giải thích:

  • Vòng lặp đầu tiên duyệt qua từng phần tử kiểu int trong mảng mang_so. Biến so lần lượt nhận giá trị 10, 20, 30, 40, 50.
  • Vòng lặp thứ hai duyệt qua từng phần tử kiểu string trong vector danh_sach_ten. Việc sử dụng const string& (hoặc const auto&) là cách làm tốt để tránh sao chép toàn bộ chuỗi trong mỗi lần lặp, đặc biệt với các phần tử lớn.
  • Vòng lặp thứ ba duyệt qua từng ký tự kiểu char trong chuỗi loi_chao.

Ưu điểm của range-based for:

  • Đơn giản hơn: Cú pháp gọn gàng, ít boilerplate code hơn so với for truyền thống khi chỉ cần duyệt qua tất cả phần tử.
  • Ít lỗi hơn: Giảm thiểu lỗi liên quan đến việc quản lý chỉ số (index), điều kiện dừng, hoặc cập nhật biến đếm sai.
  • Dễ đọc hơn: Thể hiện rõ ý định của bạn là "thực hiện điều gì đó cho mọi phần tử trong dãy".

Nhược điểm (hoặc giới hạn):

  • Bạn không có trực tiếp chỉ số (index) của phần tử hiện tại. Nếu bạn cần chỉ số, bạn phải dùng vòng lặp for truyền thống hoặc tự thêm một biến đếm vào vòng lặp range-based for.
  • Không phù hợp khi bạn cần lặp với bước nhảy tùy ý, lặp ngược (một cách tự nhiên), hoặc khi điều kiện dừng không liên quan trực tiếp đến việc duyệt hết container.

5. Sử dụng breakcontinue trong vòng lặp for

Hai câu lệnh breakcontinue cung cấp khả năng kiểm soát luồng thực thi bên trong vòng lặp.

  • break: Ngay lập tức thoát khỏi vòng lặp chứa nó gần nhất. Chương trình sẽ tiếp tục thực thi câu lệnh ngay sau vòng lặp.

    #include <iostream>
    
    int main() {
        cout << "Ví dụ với break:" << endl;
        for (int i = 1; i <= 10; ++i) {
            if (i == 6) {
                cout << "Gặp số 6, thoát vòng lặp." << endl;
                break; // Thoát khỏi vòng lặp for
            }
            cout << i << " ";
        }
        cout << endl << "Đã thoát vòng lặp." << endl;
        return 0;
    }
    

    Giải thích: Vòng lặp này được thiết lập để chạy từ 1 đến 10. Tuy nhiên, khi i đạt giá trị 6, câu lệnh break được thực thi, khiến vòng lặp kết thúc ngay lập tức. Output sẽ chỉ in các số từ 1 đến 5, sau đó là thông báo "Gặp số 6, thoát vòng lặp." và cuối cùng là "Đã thoát vòng lặp.".

  • continue: Bỏ qua phần còn lại của khối lệnh trong lần lặp hiện tại và chuyển sang lần lặp tiếp theo (thực hiện phần cập nhật và kiểm tra điều kiện).

    #include <iostream>
    
    int main() {
        cout << "Ví dụ với continue:" << endl;
        for (int i = 1; i <= 10; ++i) {
            if (i == 5) {
                cout << "Bỏ qua số 5..." << endl;
                continue; // Bỏ qua phần còn lại của lần lặp này
            }
            cout << i << " ";
        }
        cout << endl << "Kết thúc vòng lặp." << endl;
        return 0;
    }
    

    Giải thích: Vòng lặp này cũng chạy từ 1 đến 10. Khi i đạt giá trị 5, câu lệnh continue được thực thi. Điều này làm cho phần cout << i << " "; bị bỏ qua chỉ trong lần lặp đó. Vòng lặp sẽ tiếp tục với i = 6. Output sẽ in các số từ 1 đến 10, nhưng thiếu số 5.

Cả breakcontinue đều hữu ích để tinh chỉnh hành vi của vòng lặp dựa trên các điều kiện đặc biệt.

6. Một vài lưu ý và mẹo nhỏ

  • Phạm vi biến: Biến được khai báo trong phần khởi tạo của vòng lặp for (ví dụ: int i = 0;) chỉ tồn tại trong phạm vi của chính vòng lặp đó. Sau khi vòng lặp kết thúc, bạn không thể truy cập biến i nữa.
  • Vòng lặp vô hạn: Cẩn thận với các điều kiện luôn đúng hoặc phần cập nhật không làm cho điều kiện sai đi. Ví dụ: for (int i = 0; i < 10; ) { cout << i; } là vòng lặp vô hạn vì i không bao giờ thay đổi.
  • Lỗi off-by-one (lệch 1 đơn vị): Đây là lỗi phổ biến khi sử dụng vòng lặp for truyền thống. Cẩn thận với việc sử dụng < so với <= trong điều kiện lặp. Ví dụ: lặp 5 lần, bắt đầu từ 0: for (int i = 0; i < 5; ++i) (i sẽ là 0, 1, 2, 3, 4 - 5 giá trị). Lặp 5 lần, bắt đầu từ 1: for (int i = 1; i <= 5; ++i) (i sẽ là 1, 2, 3, 4, 5 - 5 giá trị).
  • Chọn loại vòng lặp:
    • Sử dụng for truyền thống khi bạn cần kiểm soát chính xác số lần lặp, cần truy cập chỉ mục, lặp với bước nhảy không phải 1, hoặc lặp ngược.
    • Sử dụng range-based for khi bạn chỉ đơn giản muốn duyệt qua tất cả các phần tử trong một container/dãy từ đầu đến cuối mà không cần quan tâm đến chỉ mục. range-based for thường an toàn và dễ đọc hơn trong trường hợp này.

Vòng lặp for là một công cụ cơ bản nhưng vô cùng mạnh mẽ trong lập trình C++. Nắm vững cách sử dụng cả cú pháp truyền thống và range-based for, cùng với breakcontinue, sẽ giúp bạn giải quyết hiệu quả nhiều bài toán lặp trong chương trình của mình.

Comments

There are no comments at the moment.