Lặp lại thông qua một mảng (hoặc cấu trúc khác) dữ liệu là một việc khá phổ biến trong lập trình. Và cho đến nay, chúng ta đã đề cập đến nhiều cách khác nhau để làm như vậy: với vòng lặp và chỉ mục (vòng lặp for và vòng lặp while), với con trỏ và con trỏ số học và với vòng lặp for dựa trên phạm vi:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

#include <array>
#include <cstddef>
#include <iostream>
 
int main()
{
  // The type is automatically deduced to std::array<int, 7> (Requires C++17).
  // Use the type std::array<int, 7> if your compiler doesn't support C++17.
  std::array data{ 0, 1, 2, 3, 4, 5, 6 };
  std::size_t length{ std::size(data) };
 
  // while-loop with explicit index
  std::size_t index{ 0 };
  while (index != length)
  {
    std::cout << data[index] << ' ';
    ++index;
  }
  std::cout << '\n';
 
  // for-loop with explicit index
  for (index = 0; index < length; ++index)
  {
    std::cout << data[index] << ' ';
  }
  std::cout << '\n';
 
  // for-loop with pointer (Note: ptr can't be const, because we increment it)
  for (auto ptr{ &data[0] }; ptr != (&data[0] + length); ++ptr)
  {
    std::cout << *ptr << ' ';
  }
  std::cout << '\n';
 
  // ranged-based for loop
  for (int i : data)
  {
    std::cout << i << ' ';
  }
  std::cout << '\n';
 
  return 0;
}

Việc lặp lại bằng cách sử dụng các chỉ số(index) sẽ phải nhập nhiều hơn nếu chúng ta chỉ sử dụng chỉ số để truy cập các phần tử. Nó cũng chỉ hoạt động nếu vùng chứa (ví dụ: mảng) cung cấp quyền truy cập trực tiếp vào các phần tử (mảng làm được, nhưng một số loại vùng chứa khác, chẳng hạn như danh sách, thì không).

Việc lặp lại với con trỏ và con trỏ số học thì rất dài dòng và có thể gây nhầm lẫn cho người đọc không biết các quy tắc của con trỏ số học. con trỏ Số học cũng chỉ hoạt động nếu các phần tử nằm liên tiếp trong bộ nhớ (điều này đúng với mảng, nhưng không đúng với các loại vùng chứa khác, chẳng hạn như danh sách(list), cây(tree) và bản đồ(map)).

Dành cho người đọc nâng cao

Con trỏ (không phải là con trỏ số học) cũng có thể được sử dụng để lặp qua một số cấu trúc không tuần tự. Trong danh sách liên kết, mỗi phần tử được kết nối với phần tử trước bằng một con trỏ. Chúng ta có thể lặp lại danh sách bằng cách theo dõi chuỗi con trỏ.

Vòng lặp for dựa trên phạm vi thú vị hơn một chút, vì cơ chế lặp qua vùng chứa của chúng ta bị ẩn – tuy nhiên, chúng vẫn hoạt động cho tất cả các loại cấu trúc khác nhau (mảng, danh sách(list), cây(tree), bản đồ(map), v.v.). Làm thế nào để làm việc này? Họ sử dụng trình lặp(iterators).

1. Trình lặp lại

Trình vòng lặp(iterators) là một đối tượng được thiết kế để duyệt qua một vùng chứa(mảng, vector,..) (ví dụ: các giá trị trong một mảng hoặc các ký tự trong một chuỗi), cung cấp quyền truy cập vào từng phần tử khi nó lặp.

Một vùng chứa có thể cung cấp các loại trình vòng lặp khác nhau. Ví dụ: một vùng chứa mảng có thể cung cấp một trình vòng lặp chuyển tiếp đi qua mảng theo thứ tự chuyển tiếp và một trình vòng lặp ngược đi qua mảng theo thứ tự ngược lại.

Khi loại trình vòng lặp thích hợp được tạo, lập trình viên sau đó có thể sử dụng giao diện do trình vòng lặp để duyệt và truy cập các phần tử mà không phải lo lắng về loại trình duyệt đang được thực hiện hoặc cách dữ liệu được lưu trữ trong vùng chứa. Và bởi vì các trình vòng lặp C ++ thường sử dụng cùng một giao diện cho truyền tải (toán tử ++ để di chuyển đến phần tử tiếp theo) và truy cập (toán tử * để truy cập phần tử hiện tại), chúng ta có thể lặp qua nhiều loại vùng chứa khác nhau bằng một phương pháp nhất quán.

2. Con trỏ như một trình lặp

Loại trình lặp đơn giản nhất là con trỏ (sử dụng con trỏ số học) hoạt động cho dữ liệu được lưu trữ tuần tự trong bộ nhớ. Hãy cùng xem lại quá trình duyệt mảng đơn giản bằng cách sử dụng con trỏ và con trỏ số học:

#include <array>
#include <iostream>
 
int main()
{
  std::array data{ 0, 1, 2, 3, 4, 5, 6 };
 
  auto begin{ &data[0] };
  // note that this points to one spot beyond the last element
  auto end{ begin + std::size(data) };
 
  // for-loop with pointer
  for (auto ptr{ begin }; ptr != end; ++ptr) // ++ to move to next element
  {
    std::cout << *ptr << ' '; // Indirection to get value of current element
  }
  std::cout << '\n';
 
  return 0;
}

Đầu ra:

0 1 2 3 4 5 6

Ở trên, chúng ta đã khai báo hai biến: begin (trỏ đến đầu vùng chứa của chúng ta) và end (đánh dấu điểm kết thúc). Đối với mảng, điểm đánh dấu kết thúc thường là vị trí trong bộ nhớ mà phần tử cuối cùng sẽ ở nếu vùng chứa chứa thêm một phần tử.

Sau đó, con trỏ lặp lại giữa đầu và cuối, và phần tử hiện tại có thể được truy cập bằng cách chuyển hướng thông qua con trỏ.

Lưu ý

Bạn có thể bị cám dỗ để tính toán điểm đánh dấu kết thúc bằng cách sử dụng toán tử address-of và cú pháp mảng như sau:

int* end{ &array[std::size(array)] };

Nhưng điều này gây ra hành vi không xác định, vì mảng [std :: size (array)] truy cập vào một phần tử nằm ở cuối mảng.

Thay vào đó, hãy sử dụng:

int* end{ array.data() + std::size(array) }; // data() returns a pointer to the first element

3. Trình lặp thư viện chuẩn

Lặp lại(iterators) là một hoạt động phổ biến mà tất cả các vùng chứa thư viện chuẩn cung cấp hỗ trợ trực tiếp cho việc lặp lại. Thay vì tính toán điểm đầu và điểm cuối của riêng mình, chúng ta có thể chỉ cần yêu cầu vùng chứa cho điểm bắt đầu và điểm kết thúc thông qua các hàm được đặt tên thuận tiện là begin () và end ():

#include <array>
#include <iostream>
 
int main()
{
  std::array array{ 1, 2, 3 };
 
  // Ask our array for the begin and end points (via the begin and end member functions).
  auto begin{ array.begin() };
  auto end{ array.end() };
 
  for (auto p{ begin }; p != end; ++p) // ++ to move to next element.
  {
    std::cout << *p << ' '; // Indirection to get value of current element.
  }
  std::cout << '\n';
 
  return 0;
}

Kết quả

1 2 3

header của  trình lặp(iterator) cũng chứa hai hàm chung (std :: begin và std :: end) có thể được sử dụng:

#include <array>
#include <iostream>
#include <iterator> // For std::begin and std::end
 
int main()
{
  std::array array{ 1, 2, 3 };
 
  // Use std::begin and std::end to get the begin and end points.
  auto begin{ std::begin(array) };
  auto end{ std::end(array) };
 
  for (auto p{ begin }; p != end; ++p) // ++ to move to next element
  {
    std::cout << *p << ' '; // Indirection to get value of current element
  }
  std::cout << '\n';
 
  return 0;
}

Kết quả

1 2 3

Hiện tại, đừng lo lắng về các loại trình vòng lặp, chúng ta sẽ truy cập lại các trình vòng lặp trong chương sau. Điều quan trọng là trình lặp sẽ quan tâm đến các chi tiết của việc lặp qua vùng chứa. Tất cả những gì chúng ta cần là bốn thứ: điểm bắt đầu, điểm kết thúc, toán tử ++ để di chuyển trình lặp đến phần tử tiếp theo (hoặc cuối) và toán tử * để nhận giá trị của phần tử hiện tại.

4. Quay lại vòng lặp dựa trên phạm vi

Tất cả các kiểu có hàm thành viên bắt đầu và kết thúc hoặc có thể được sử dụng với std :: begin và std :: end đều có thể được sử dụng trong các vòng for- dựa trên phạm vi (vì thông tin kiểu không chứa độ dài của mảng).

#include <array>
#include <iostream>
 
int main()
{
  std::array array{ 1, 2, 3 };
 
  // This does exactly the same as the loop we used before.
  for (int i : array)
  {
    std::cout << i << ' ';
  }
  std::cout << '\n';
 
  return 0;
}

Bạn sẽ tìm hiểu cách thêm các hàm vào các kiểu của mình sau để chúng cũng có thể được sử dụng với các vòng lặp for dựa trên phạm vi.

Vòng for dựa trên phạm vi không phải là thứ duy nhất sử dụng trình vòng lặp. Chúng cũng được sử dụng trong std :: sort và các thuật toán khác. Bây giờ bạn đã biết chúng là gì, bạn sẽ nhận thấy chúng được sử dụng khá nhiều trong thư viện chuẩn.

5. Vô hiệu hóa trình lặp (trình vòng lặp bị treo)

Cũng giống như con trỏ và tham chiếu, các trình vòng lặp có thể bị “treo lơ lửng” nếu các phần tử được lặp qua địa chỉ thay đổi hoặc bị phá hủy. Khi điều này xảy ra, chúng ta nói rằng trình lặp đã bị vô hiệu. Việc truy cập một trình lặp không hợp lệ sẽ tạo ra hành vi không xác định.

Một số thao tác sửa đổi vùng chứa (chẳng hạn như thêm một phần tử vào std :: vector) có thể có tác dụng phụ là khiến các phần tử trong vùng chứa thay đổi địa chỉ. Khi điều này xảy ra, các trình lặp hiện có cho các phần tử đó sẽ bị vô hiệu. Tài liệu tham khảo tốt về C ++ cần lưu ý các hoạt động vùng chứa nào có thể hoặc sẽ làm mất hiệu lực của các trình vòng lặp. Ví dụ, hãy xem phần “Hủy bỏ hiệu lực lặp lại” của vectơ std :: trên cppreference.

Đây là một ví dụ về điều này:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

#include <iostream>
#include <vector>
 
int main()
{
	std::vector v { 1, 2, 3, 4, 5, 6, 7 };
 
	auto it { v.begin() };
 
	++it; // move to second element
	std::cout << *it << '\n'; // ok: prints 2
 
	v.erase(it); // erase the element currently being iterated over
 
	// erase() invalidates iterators to the erased element (and subsequent elements)
	// so iterator "it" is now invalidated
	
	++it; // undefined behavior
	std::cout << *it << '\n'; // undefined behavior
	   
	return 0;
}

Cài ứng dụng cafedev để dễ dàng cập nhật tin và học lập trình mọi lúc mọi nơi tại đây.

Nguồn và Tài liệu tiếng anh tham khảo:

Tài liệu từ cafedev:

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

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