[Tự học C++] Tìm hiểu về Vòng lặp For-each trong C++ » Cafedev.vn

Trong bài – Mảng và vòng lặp, chúng ta đã được thấy những ví dụ trong đó sử dụng một vòng lặp for để lặp qua từng phần tử của một mảng.

Ví dụ:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: [email protected]
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
#include <iterator> // std::size
 
int main()
{
    constexpr int scores[]{ 84, 92, 76, 81, 56 };
    constexpr int numStudents{ static_cast<int>(std::size(scores)) };
 
    int maxScore{ 0 }; // keep track of our largest score
    for (int student{ 0 }; student < numStudents; ++student)
        if (scores[student] > maxScore)
            maxScore = scores[student];
 
    std::cout << "The best score was " << maxScore << '\n';
 
    return 0;
}

Trong khi cung cấp một cách thuận tiện và linh hoạt để lặp qua một mảng, các vòng lặp for cũng khiến code của bạn trở nên lộn xộn, và làm cho các lập trình viên dễ mắc phải lỗi off-by-one.

C++ 11 đã giới thiệu một loại vòng lặp mới gọi là vòng lặp for-each (còn được gọi là vòng lặp for dựa trên phạm virange based for loop), cung cấp một phương thức đơn giản hơn và an toàn hơn cho những trường hợp mà chúng ta muốn lặp qua toàn bộ phần tử của một mảng (hoặc một cấu trúc dữ liệu dạng danh sách khác nào đó).

1. Vòng lặp For-each

Câu lệnh thực hiện vòng lặp for-each sẽ có cú pháp trông giống như sau:

for (element_declaration : array)
   statement;

Khi chương trình thực thi đến câu lệnh này, vòng lặp sẽ lặp qua từng phần tử trong mảng, gán giá trị của phần tử mảng hiện tại cho biến được khai báo trong element_declaration. Để có kết quả tốt nhất, element_declaration phải có cùng kiểu dữ liệu với các phần tử của mảng, nếu không thì lỗi chuyển đổi kiểu giá trị (type conversion error) sẽ xuất hiện.

Cùng xem một ví dụ đơn giản sử dụng vòng lặp for-each để in ra tất cả các phần tử của một mảng có tên fibonacci:

#include <iostream>
 
int main()
{
    constexpr int fibonacci[]{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
    for (int number : fibonacci) // iterate over array fibonacci
       std::cout << number << ' '; // we access the array element for this iteration through variable number
 
    std::cout << '\n';
 
    return 0;
}

Kết quả in ra là:

0 1 1 2 3 5 8 13 21 34 55 89

Hãy cùng xem xét kỹ hơn về cách thức hoạt động của đoạn chương trình này. Đầu tiên, vòng lặp for sẽ được thực thi, và biến number được gán giá trị là phần tử đầu tiên của mảng, có giá trị là 0. Tiếp theo chương trình sẽ thực thi câu lệnh in ra màn hình, số 0 được in ra. Sau đó vòng lặp for sẽ được thực thi lại, và biến number được gán cho giá trị của phần tử thứ hai trong mảng, là giá trị 1. Câu lệnh in dữ liệu lại được thực thi, lần này số 1 được in ra. Vòng lặp for tiếp tục lặp lần lượt qua từng phần tử của mảng fibonacci, và mỗi lần lại thực thi câu lệnh bên trong phần thân của vòng lặp for, cho đến khi không còn phần tử nào trong mảng để lặp nữa. Lúc đó, vòng lặp sẽ kết thúc, và chương trình tiếp tục thực thi các câu lệnh khác (trong ví dụ này là trả về 0 cho hệ điều hành).

Lưu ý rằng biến number không phải là một chỉ số mảng. Nó được gán cho giá trị của phần tử mảng tại lần lặp hiện tại.

2. Vòng lặp for each và từ khóa auto

Bởi vì element_declaration phải có cùng kiểu dữ liệu với các phần tử mảng, chúng ta có thể sử dụng từ khóa auto trong trường hợp này. Từ khóa auto giúp C++ tự suy ra kiểu dữ liệu của các phần tử mảng cho chúng ta.

Dưới đây là một ví dụ sử dụng từ khóa auto:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: [email protected]
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
 
int main()
{
    constexpr int fibonacci[]{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
    for (auto number : fibonacci) // type is auto, so number has its type deduced from the fibonacci array
       std::cout << number << ' ';
 
    std::cout << '\n';
 
    return 0;
}

3. Vòng lặp for-each và tham chiếu

Trong ví dụ về vòng lặp for-each dưới đây, các phần tử mảng được khai báo theo kiểu giá trị:

std::string array[]{ "peter", "likes", "frozen", "yogurt" };
    for (auto element : array) // element will be a copy of the current array element
        std::cout << element << ' ';

Điều này có nghĩa là mỗi phần tử mảng được duyệt qua sẽ được sao chép vào trong biến element. Việc sao chép các phần tử mảng có thể gây tiêu tốn nhiều tài nguyên của máy tính, và hầu hết mọi lúc chúng ta đều thực sự chỉ muốn nhắm đến phần tử gốc. May mắn thay, chúng ta có thể sử dụng tham chiếu để giải quyết vấn đề này:

 std::string array[]{ "peter", "likes", "frozen", "yogurt" };
    for (auto &element: array) // The ampersand makes element a reference to the actual array element, preventing a copy from being made
        std::cout << element << ' ';

Trong ví dụ trên, biến element sẽ là một tham chiếu tới phần tử mảng được lặp đến, hiện tại, để tránh việc phải tạo ra bản sao của giá trị. Đồng thời, bất kỳ sự thay đổi nào diễn ra đối với biến element, cũng sẽ ảnh hưởng tới mảng array đang được lặp, điều mà không thể xảy ra nếu biến element chỉ là một biến thường.

Và, tất nhiên, nếu bạn muốn sử dụng phẩn tử mảng theo kiểu read-only (kiểu chỉ đọc, không cho phép ghi), hãy sử dụng từ khóa const cho biến element:

std::string array[]{ "peter", "likes", "frozen", "yogurt" };
    for (const auto &element: array) // element is a const reference to the currently iterated array element
        std::cout << element << ' ';

Quy luật: Trong phần khai báo phần tử lặp của vòng lăp for-each, nếu các phần tử mảng của bạn không thuộc kiểu dữ liệu cơ bản, hãy sử dụng tham chiếu hoặc tham chiếu hằng (const reference) để cải thiện hiệu năng của chương trình.

4. Viết lại ví dụ về maxScore sử dụng vòng lặp for-each

Dưới đây là ví dụ ở đầu bài học hôm nay, được viết lại bằng cách sử dụng vòng lặp for-each:

Lưu ý rằng trong ví dụ này, chúng ta đã không cần phải lấy kích thước của mảng hay sử dụng chỉ số mảng để lặp qua từng phần tử. Chúng ta có thể truy cập trực tiếp phần tử mảng thông qua biến score, và mảng được duyệt đã phải có thông tin về kích thước rồi. Một mảng đã bị phân rã thành một con trỏ thì không thể được sử dụng trong vòng lặp for-each.

5. Vòng lặp for-each và các cấu trúc dữ liệu không phải mảng

Vòng lặp For-each không chỉ hoạt động với các mảng cố định, nó còn có thể hoạt động với nhiều loại cấu trúc dữ liệu dạng danh sách khác, chẳng hạn như các vectors (ví dụ: std::vector), linked lists (danh sách liên kết), trees (cây), và maps. Hiện tại, chúng ta chưa từng đề cập đến các loại cấu trúc dữ liệu này, vì vậy hãy đừng lo lắng nếu bạn không biết về chúng. Chỉ cần nhớ rằng vòng lặp for-each sẽ cung cấp một phương thức linh hoạt và tổng quát (hay còn gọi là chung) để lặp qua không chỉ mảng mà còn các cấu trúc dữ liệu dạng danh sách khác.

#include <iostream>
#include <vector>
 
int main()
{
    std::vector fibonacci{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; // note use of std::vector here rather than a fixed array
 
    for (auto number : fibonacci)
        std::cout << number << ' ';
 
    std::cout << '\n';
 
    return 0;
}

6. Vòng lặp for-each không hoạt động với con trỏ trỏ đến một mảng

Để có thể lặp qua mảng, vòng lặp for-each cần biết được kích thước của mảng. Bởi vì không thể biết được kích thước của những mảng mà đã được phân rã thành con trỏ, nên vòng lặp for-each sẽ không hoạt động được với chúng.

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: [email protected]
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
 
int sumArray(int array[]) // array is a pointer
{
    int sum{ 0 };
 
    for (auto number : array) // compile error, the size of array isn't known
        sum += number;
 
    return sum;   
}
 
int main()
{
     int array[]{ 9, 7, 5, 3, 1 };
 
     std::cout << sumArray(array) << '\n'; // array decays into a pointer here
 
     return 0;
}

Tương tự, vòng lặp for-each cũng sẽ không hoạt động với mảng động vì lý do này.

7. Tôi có thể lấy được chỉ số (index) của phần tử hiện tại không? 

Vòng lặp for-each không cung cấp một cách trực tiếp nào để lấy được chỉ số mảng (index) của phần tử hiện tại. Điều này là bởi vì vòng lặp for-each có thể được sử dụng trong rất nhiều cấu trúc dữ liệu dạng danh sách mà không thể đánh chỉ số được (ví dụ: linked list)!

8. Tổng kết

Vòng lặp for-each cung cấp một cú pháp ưu việt để lặp qua một mảng khi chúng ta cần truy cập tới tất cả các phần tử của mảng, theo thứ tự tuần tự về phía trước. Nên ưu tiên sử dụng vòng lặp for-each hơn là vòng lặp for tiêu chuẩn trong những trường hợp có thể sử dụng nó. Để tránh việc tạo ra các bản sao của mỗi phần tử, thì lý tưởng nhất, biến dùng để duyệt vòng lặp for-each (được khai báo bên trong cặp dấu () của vòng lặp for-each) nên là một tham chiếu.

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!