Trong bài học trước, chúng ta đã tìm hiểu về std::array, cái mà sở hữu những tính năng như mảng cố định được tích hợp sẵn trong C++, nhưng ở một dạng an toàn hơn và dễ sử dụng hơn.

Tương tự như vậy, thư viện standard của C++ cũng cung cấp một chức năng giúp làm việc với các mảng động an toàn và dễ dàng hơn. Chức năng này được đặt tên là std::vector.

Không giống như std::array, cái mà bám sát theo một chức năng cơ bản được tích hợp sẵn là mảng cố định, std::vector đi kèm với một số thủ thuật bổ sung. Những thủ thuật mang tính hỗ trợ này đã làm cho std::vector trở thành một trong những công cụ hữu ích và linh hoạt nhất có trong bộ công cụ lập trình của C++.

1. Giới thiệu về std::vector

Được giới thiệu trong C++ 03, std::vector cung cấp chức năng mảng động có khả năng tự xử lý việc quản lý bộ nhớ của chính nó. Điều này nghĩa là bạn có thể tạo ra những mảng có độ dài được thiết lập tại runtime (thời điểm chương trình chạy), mà không cần phải cấp phát và giải phóng bộ nhớ một cách rõ ràng/tường minh bằng toán tử newdelete. std::vector tồn tại trong header <vector>.

Khai báo một std::vector rất đơn giản:

/**
* 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
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <vector>
 
// no need to specify length at initialization
std::vector<int> array; 
std::vector<int> array2 = { 9, 7, 5, 3, 1 }; // use initializer list to initialize array
std::vector<int> array3 { 9, 7, 5, 3, 1 }; // use uniform initialization to initialize array (C++11 onward)
 
// as with std::array, the type can be omitted since C++17
std::vector array4 { 9, 7, 5, 3, 1 }; // deduced to std::vector<int>

Lưu ý rằng trong cả trường hợp chưa khởi tạo và đã được khởi tạo, bạn không cần phải chỉ định rõ độ dài mảng tại compile time (thời điểm biên dịch chương trình). Điều này là do std::vector sẽ tự động cấp phát bộ nhớ động cho những phần nội dung của nó theo yêu cầu.

Giống như std::array, việc truy cập các phần tử mảng có thể được thực hiện thông qua toán tử [] (cách này sẽ không bao gồm việc kiểm tra giới hạn mảng) hoặc hàm at() (cách này sẽ bao gồm cả việc kiểm tra giới hạn mảng).

array[6] = 2; // no bounds checking
array.at(7) = 3; // does bounds checking

Trong cả hai trường hợp, nếu bạn yêu cầu một một phần tử nằm ngoài giới hạn cuối của mảng, vector sẽ không tự động thay đổi kích thước.

Kể từ C++ 11, bạn cũng có thể gán các giá trị cho một std::vector bằng cách sử dụng danh sách khởi tạo (initializer-list):

array = { 0, 1, 2, 3, 4 }; // okay, array length is now 5
array = { 9, 8, 7 }; // okay, array length is now 3

Trong trường hợp này, vector sẽ tự điều chỉnh kích thước để khớp với số lượng các phần tử mảng được cung cấp.

2. Tự động dọn dẹp vùng nhớ ngăn chặn lỗi memory leak (rò rỉ bộ nhớ)

Khi một biến vector nằm ngoài phạm vi đoạn code mà chương trình đang chạy (goes out of scope), nó sẽ tự động giải phóng những phần bộ nhớ mà nó kiểm soát (nếu cần). Điều này không chỉ tiện dụng (bởi vì bạn không phải tự giải phóng bộ nhớ), mà nó còn giúp ngăn ngừa lỗi rò rỉ bộ nhớ (memory leaks). Xét đoạn code sau:

void doSomething(bool earlyExit)
{
    int *array{ new int[5] { 9, 7, 5, 3, 1 } };
 
    if (earlyExit)
        return;
 
    // do stuff here
 
    delete[] array; // never called
}

Nếu biến earlyExit được gán là true, mảng array sẽ không bao giờ được giải phóng, và bộ nhớ sẽ bị rò rỉ.

Tuy nhiên, nếu biến array là một std::vector, điều này sẽ không xảy ra, bởi vì bộ nhớ sẽ được giải phóng ngay sau khi biến array nằm ngoài phạm vi đoạn code mà chương trình đang chạy (goes out of scope) (bất kể hàm có bị thoát ra sớm hay không). Điều này làm cho std::vector an toàn hơn nhiều so với việc bạn phải tự chú ý đến việc giải phóng bộ nhớ.

3. Vector tự ghi nhớ độ dài của mình

Không giống như mảng động được tích hợp sẵn của C++, cái mà không biết được độ dài của mảng mà nó đang trỏ tới là bao nhiêu, std::vectors tự theo dõi độ dài của chính nó. Chúng ta có thể lấy được độ dài của vector thông qua hàm size():

#include <iostream>
#include <vector>
 
void printLength(const std::vector<int>& array)
{
    std::cout << "The length is: " << array.size() << '\n';
}
 
int main()
{
    std::vector array { 9, 7, 5, 3, 1 };
    printLength(array);
 
    return 0;
}

Ví dụ trên in ra:

The length is: 5

Giống như với std::array, hàm size() sẽ trả về một giá trị thuộc kiểu nested type là size_type (nested type – kiểu dữ liệu lồng, là kiểu dữ liệu được khai báo bên trong phần thân của một kiểu dữ liệu khác) (kiểu dữ liệu đầy đủ trong ví dụ trên sẽ là std::vector<int>::size_type), nó là một số nguyên không dấu (unsigned integer).

4. Thay đổi kích thước của vector

Thay đổi kích thước của một mảng đã được cấp phát bộ nhớ động được tích hợp sẵn của C++ là rất phức tạp. Trong khi đó, thay đổi kích thước của một std::vector chỉ đơn giản là gọi hàm resize().

#include <iostream>
#include <vector>
 
int main()
{
    std::vector array { 0, 1, 2 };
    array.resize(5); // set size to 5
 
    std::cout << "The length is: " << array.size() << '\n';
 
    for (int i : array)
        std::cout << i << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Kết quả in ra là:

The length is: 5
0 1 2 0 0

Có hai điều cần lưu ý ở đây. Thứ nhất, khi chúng ta thay đổi kích thước của vector, Giá trị của các phần tử mảng hiện tại được giữ nguyên! Thứ hai, giá trị của các phần tử mới sẽ được khởi tạo thành giá trị mặc định của kiểu dữ liệu cụ thể (ví dụ: giá trị mặc định của kiểu integer là 0).

Kích thước của vector cũng có thể được thay đổi thành nhỏ hơn:

#include <vector>
#include <iostream>
 
int main()
{
    std::vector array { 0, 1, 2, 3, 4 };
    array.resize(3); // set length to 3
 
    std::cout << "The length is: " << array.size() << '\n';
 
    for (int i : array)
        std::cout << i << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Đoạn code này sẽ in ra:

The length is: 3
0 1 2

Việc thay đổi kích thước của một vector tiêu tốn khá nhiều tài nguyên máy tính để tính toán, vì vậy bạn nên cố gắng giảm thiểu số lần bạn làm như vậy. Nếu bạn cần một vector có số lượng phần tử cụ thể, nhưng không muốn gán giá trị cho các phần tử này tại thời điểm khai báo, bạn có thể tạo ra một vector với các phần phần tử mang giá trị mặc định như sau:

/**
* 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
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
#include <vector>
 
int main()
{
    // Using direct initialization, we can create a vector with 5 elements,
    // each element is a 0. If we use brace initialization, the vector would
    // have 1 element, a 5.
    std::vector<int> array(5);
 
    std::cout << "The length is: " << array.size() << '\n';
 
    for (int i : array)
        std::cout << i << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Kết quả in ra là:

5
0 0 0 0 0

Chúng ta sẽ nói về lý do tại sao khởi tạo trực tiếp (direct initialization) và khởi tạo bằng cặp dấu ngoặc nhọn (brace-initialization) lại được xử lý khác nhau trong một chương sau. Có một nguyên tắc nhỏ ở đây là, nếu kiểu dữ liệu thuộc dạng danh sách và bạn không muốn khởi tạo nó bằng một danh sách (khởi tạo kiểu initializer-list), hãy sử dụng khởi tạo trực tiếp (direct initialization).

5. Thực hiện nén nhiều giá trị bools

std::vector còn sở hữu một thủ thuật hay khác. Có một cài đặt đặc biệt dành cho std::vector kiểu bool mà có thể nén 8 giá trị booleans vào trong một byte! Quá trình nén này đã được cài đặt sẵn, bạn chỉ cần sử dụng nó như một tính năng hỗ trợ trong việc lập trình, và thủ thuật này cũng sẽ không thay đổi cách mà bạn sử dụng std::vector.

/**
* 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
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <vector>
#include <iostream>
 
int main()
{
    std::vector<bool> array { true, false, false, true, true };
    std::cout << "The length is: " << array.size() << '\n';
 
    for (int i : array)
        std::cout << i << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Kết quả in ra là:

The length is: 5
1 0 0 1 1

6. Nhiều hơn nữa

Lưu ý rằng đây chỉ là một bài viết mang tính chất giới thiệu, nhằm giới thiệu về những điều cơ bản của std::vector. Trong bài 7.10, chúng ta sẽ nói nhiều hơn về một số khả năng bổ sung của std::vector, bao gồm sự khác biệt giữa chiều dài và dung lượng (capacity) của vector, và tìm hiểu sâu hơn về cách mà std::vector xử lý việc cấp phát bộ nhớ.

7. Tổng kết

Bởi vì các biến thuộc kiểu dữ liệu std::vector sẽ tự xử lý việc quản lý bộ nhớ của riêng chúng (điều này giúp ngăn chặn lỗi rò rỉ bộ nhớ – memory leaks), chúng cũng tự ghi nhớ độ dài của mình, và có thể dễ dàng thay đổi kích thước, chúng tôi khuyên bạn nên sử dụng std::vector trong hầu hết các trường hợp cần tới mảng động.

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