Trong bài này, chúng ta sẽ tiếp tục thảo luận phần kiến thức về mảng đã bắt đầu trong bài – Arrays (Phần I)

1. Khởi tạo mảng cố định

Có thể coi các phần tử mảng giống như các biến thông thường, và do đó, chúng không được khởi tạo khi được tạo ra.

Có thể khởi tạo một mảng bằng cách khởi tạo từng phần tử mảng:

int prime[5]; // hold the first 5 prime numbers
 prime[0] = 2;
 prime[1] = 3;
 prime[2] = 5;
 prime[3] = 7;
 prime[4] = 11;

Tuy nhiên, cách làm này vô cùng cồng kềnh, và gần như không thể thực hiện được khi mảng có kích thước lớn hơn.

May mắn thay, C++ có cung cấp một cách thuận tiện hơn để khởi tạo toàn bộ mảng, thông qua việc sử dụng một initializer list – trình khởi tạo dạng danh sách. Ví dụ sau sẽ tương đương với đoạn code ở ví dụ bên trên:

int prime[5] = { 2, 3, 5, 7, 11 }; // use initializer list to initialize the fixed array

Nếu trong list – danh sách (Ví dụ:{1, 2, 3} chính là một cái list) có số lượng các phần từ khởi tạo(ý là các giá trị nằm bên trong cặp dấu {}, ví dụ: {1, 2, 3} thì 1, 2 và 3 chính là các initializers) lớn hơn độ dài mảng, trình biên dịch sẽ báo lỗi.

Tuy nhiên, nếu số lượng initializers bên trong list nhỏ hơn độ dài mảng, các phần tử mảng còn lại (mà chưa được chỉ định khởi tạo trong list) sẽ được khởi tạo thành 0 (hoặc bất kỳ giá trị nào đó tuỳ vào kiểu dữ liệu của mảng, đối với một kiểu dữ liệu cơ bản không phải kiểu số nguyên, ví dụ: chuyển đổi thành 0.0 đối với kiểu double). Đây gọi là zero initialization – khởi tạo 0.

Ví dụ sau sẽ mô tả zero initialization:

/**
* 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>
 
int main()
{
    int array[5] = { 7, 4, 5 }; // only initialize first 3 elements
 
    std::cout << array[0] << '\n';
    std::cout << array[1] << '\n';
    std::cout << array[2] << '\n';
    std::cout << array[3] << '\n';
    std::cout << array[4] << '\n';
 
    return 0;
}

đoạn code trên sẽ in ra:

7
4
5
0
0

Do đó, để khởi tạo tất cả các phần tử của một mảng thành 0, bạn có thể làm như sau:

// Initialize all elements to 0
int array[5] = { };
 
// Initialize all elements to 0.0
double array[5] = { };

Kể từ C++ 11, nên sử dụng cú pháp uniform initialization – khởi tạo đồng dạng sau:

int prime[5] { 2, 3, 5, 7, 11 }; // use uniform initialization to initialize the fixed array
// note the lack of the equals sign in this syntax

2. Mảng có kích thước không được chỉ định khi khai báo

Nếu bạn đang khởi tạo một mảng cố định có các phần tử sử dụng initializer list, trình biên dịch có thể tự suy ra độ dài của mảng cho bạn, và lúc này bạn có thể bỏ qua việc khai báo rõ ràng/tường minh độ dài của mảng

Hai dòng code dưới đây là tương đương nhau:

int array[5] = { 0, 1, 2, 3, 4 }; // explicitly define length of the array
int array[] = { 0, 1, 2, 3, 4 }; // let initializer list set length of the array

Việc này không chỉ tiết kiệm thời gian gõ bàn phím, mà nó còn giúp bạn không cần phải cập nhật lại độ dài của mảng nếu bạn thêm hoặc xóa các phẩn tử sau nay.

3. Mảng và enums

Một trong những vấn đề lớn trong việc lập tài liệu đối với mảng là các chỉ số mảng kiểu nguyên không cung cấp bất kỳ thông tin nào cho người lập trình về ý nghĩa của chúng.  Xét một lớp gồm 5 sinh viên:

const int numberOfStudents(5);
int testScores[numberOfStudents];
testScores[2] = 76;

Ai đại diện cho testScore[2]?  Nhìn bằng mắt thường ta không thể biết chính xác được.

Điều này có thể được giải quyết bằng cách thiết lập một enumeration – kiểu dữ liệu liệt kê, trong đó, một enumerator (phần tử liệt kê) sẽ ánh xạ tới từng chỉ số mảng:

enum StudentNames
{
    KENNY, // 0
    KYLE, // 1
    STAN, // 2
    BUTTERS, // 3
    CARTMAN, // 4
    MAX_STUDENTS // 5
};
 
int main()
{
    int testScores[MAX_STUDENTS]; // allocate 5 integers
    testScores[STAN] = 76;
 
    return 0;
}

Theo cách này, việc xác định mỗi phần tử mảng đại diện cho cái gì sẽ trở nên rõ ràng hơn. Lưu ý rằng, một enumerator bổ sung có tên MAX_STUDENTS đã được thêm vào trong đoạn code khai báo enum StudentNames. Cái enumerator này được sử dụng trong quá trình khai báo mảng để đảm bảo mảng luôn có được độ dài phù hợp (Bởi vì độ dài mảng phải là một số mà lớn hơn chỉ số phần tử mảng lớn nhất). Điều này hữu ích cho cả mục đích lập tài liệu, và mảng sẽ tự động thay đổi kích thước nếu một enumerator khác được thêm vào:

enum StudentNames
{
    KENNY, // 0
    KYLE, // 1
    STAN, // 2
    BUTTERS, // 3
    CARTMAN, // 4
    WENDY, // 5
    MAX_STUDENTS // 6
};
 
int main()
{
    int testScores[MAX_STUDENTS]; // allocate 6 integers
    testScores[STAN] = 76; // still works
 
    return 0;
}

Lưu ý rằng “mẹo” này chỉ hoạt động nếu bạn không thay đổi các giá trị enumerator theo cách thủ công/bằng tay!

4. Mảng và enum class

Các enum classes không có được khả năng chuyển đổi ngầm định thành số nguyên, vì vậy nếu bạn thử đoạn code 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/
*/

enum class StudentNames
{
    KENNY, // 0
    KYLE, // 1
    STAN, // 2
    BUTTERS, // 3
    CARTMAN, // 4
    WENDY, // 5
    MAX_STUDENTS // 6
};
 
int main()
{
    int testScores[StudentNames::MAX_STUDENTS]; // allocate 6 integers
    testScores[StudentNames::STAN] = 76;
 
    return 0;
}

Bạn sẽ nhận được một lỗi trình biên dịch. Điều này có thể được giải quyết bằng cách sử dụng static_cast để chuyển đổi enumerator thành số nguyên:

int main()
{
    int testScores[static_cast<int>(StudentNames::MAX_STUDENTS)]; // allocate 6 integers
    testScores[static_cast<int>(StudentNames::STAN)] = 76;
 
    return 0;
}

Tuy nhiên, cách này tương đối dài dòng, vì vậy tốt hơn hết là nên sử dụng một enum tiêu chuẩn bên trong một namespace:

namespace StudentNames
{
    enum StudentNames
    {
        KENNY, // 0
        KYLE, // 1
        STAN, // 2
        BUTTERS, // 3
        CARTMAN, // 4
        WENDY, // 5
        MAX_STUDENTS // 6
    };
}
 
int main()
{
    int testScores[StudentNames::MAX_STUDENTS]; // allocate 6 integers
    testScores[StudentNames::STAN] = 76;
 
    return 0;
}

5. Truyền mảng cho các hàm

 Mặc dù việc truyển một mảng cho một hàm thoạt nhìn trông giống như truyền một biến thông thường, tuy nhiên sâu trong bản chất C++, mảng được xử lý theo cách khác.

Khi một biến bình thường được truyền vào hàm theo giá trị (pass by value), C++ sẽ sao chép giá trị của đối số vào tham số của hàm. Bởi vì tham số hàm là một bản sao (của đối số truyền vào), nên việc thay đổi giá trị của tham số hàm này sẽ không làm thay đổi giá trị của đối số truyền vào ban đầu.

Tuy nhiên, do việc sao chép các mảng lớn có thể tiêu tốn nhiều chi phí tài nguyên, nên C++ sẽ không sao chép mảng khi mảng đó được truyền vào một hàm. Thay vào đó, mảng thực tế sẽ được truyền. Điều này có một tác dụng phụ là cho phép các hàm được thay đổi trực tiếp giá trị của các phần tử mảng.

Ví dụ sau đây sẽ minh hoạt khái niệm 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
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
 
void passValue(int value) // value is a copy of the argument
{
    value = 99; // so changing it here won't change the value of the argument
}
 
void passArray(int prime[5]) // prime is the actual array
{
    prime[0] = 11; // so changing it here will change the original argument!
    prime[1] = 7;
    prime[2] = 5;
    prime[3] = 3;
    prime[4] = 2;
}
 
int main()
{
    int value = 1;
    std::cout << "before passValue: " << value << "\n";
    passValue(value);
    std::cout << "after passValue: " << value << "\n";
 
    int prime[5] = { 2, 3, 5, 7, 11 };
    std::cout << "before passArray: " << prime[0] << " " << prime[1] << " " << prime[2] << " " << prime[3] << " " << prime[4] << "\n";
    passArray(prime);
    std::cout << "after passArray: " << prime[0] << " " << prime[1] << " " << prime[2] << " " << prime[3] << " " << prime[4] << "\n";
 
    return 0;
}
before passValue: 1
after passValue: 1
before passArray: 2 3 5 7 11
after passArray: 11 7 5 3 2

Trong ví dụ trên, biến value sẽ không bị thay đổi trong hàm main() bởi vì tham số value trong hàm passValue() chính là một bản sao chép của biến value trong hàm main(), chứ không phải là biến value thực tế chính gốc ban đầu. Tuy nhiên, vì tham số mảng trong hàm passArray()mảng thực tế, nên hàm passArray() có thể thay đổi trực tiếp giá trị của các phần tử mảng.

Lý do mà điều này xảy ra có liên quan đến cách các mảng được triển khai trong C++, chúng ta sẽ xem xét chủ đề này khi tìm hiểu xong về pointers (con trỏ). Hiện tại, bạn có thể coi đây là một đặc điểm của ngôn ngữ lập trình này.

Một lưu ý phụ, nếu bạn muốn đảm bảo một hàm sẽ không sửa đổi các phần tử mảng được truyền vào nó, bạn có thể sử dụng const array – mảng hằng:

// even though prime is the actual array, within this function it should be treated as a constant
void passArray(const int prime[5])
{
    // so each of these lines will cause a compile error!
    prime[0] = 11;
    prime[1] = 7;
    prime[2] = 5;
    prime[3] = 3;
    prime[4] = 2;
}

6. sizeof và mảng

Toán tử sizeof có thể được sử dụng trên các mảng và nó sẽ trả về tổng kích thước của mảng (chiều dài của mảng nhân với kích thước phần tử mảng). Lưu ý rằng, do cách mà C++ truyền mảng cho các hàm, nên sizeof có thể sẽ không hoạt động đúng đối với các mảng đã được truyền cho các hàm.

void printSize(int array[])
{
    std::cout << sizeof(array) << '\n'; // prints the size of a pointer, not the size of the array!
}
 
int main()
{
    int array[] = { 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << sizeof(array) << '\n'; // will print the size of the array
    printSize(array);
 
    return 0;
}

Trên máy có kiểu số nguyên 4 bytes và con trỏ 4 bytes, đoạn code trên sẽ in ra màn hình:

32
4

(Bạn có thể nhận được một kết quả hơi khác một chút nếu kích thước của các kiểu dữ liệu trên máy của bạn là khác so với ví dụ được nêu)

Vì lý do này, hãy cẩn thận khi sử dụng hàm sizeof() trên mảng!

Lưu ý: Trên thực tế, các thuật ngữ “array size” (kích thước mảng) và “array lenght” (độ dài mảng) đều thường được sử dụng để chỉ độ dài của mảng (kích thước của mảng không hữu dụng trong hầu hết các trường hợp)

7. Xác định độ dài của một mảng cố định trước C++ 17

Một mẹo nhỏ: Chúng ta có thể xác định độ dài của một mảng cố định bằng cách chia kích thước của toàn bộ mảng cho kích thước của một phần tử mảng:

#include <iostream> // for std::cout
 
int main()
{
    int array[] = { 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << "The array has: " << sizeof(array) / sizeof(array[0]) << " elements\n";
 
    return 0;
}

Đoạn code trên sẽ in ra:

The array has 8 elements

Đoạn chương trình trên hoạt động thế nào? Đầu tiên, lưu ý rằng kích thước của toàn bộ mảng sẽ được tính bằng độ dài của mảng nhân với kích thước của một phần tử. Công thức: array size = array length * element size.

Sử dụng toàn học đại số, chúng ta có thể sắp xếp lại phương trình trên:

array length = array size / element size. Ta có sizeof(array) chính là kích thước mảng và sizeof(array[0]) là kích thước của một phần tử mảng. Vì vậy, phương trình của chúng ta sẽ trở thành:

    array length = sizeof(array) / sizeof(array[0])

Chúng ta thường sử dụng phần tử mảng tại vị trí 0 để đại diện cho một phần tử mảng, vì nó là phần tử duy nhất được đảm bảo là có tồn tại, cho dù chiều dài của mảng có là bao nhiêu.

Lưu ý rằng, mẹo này sẽ chỉ hoạt động nếu mảng được xét là một mảng có độ dài cố định, và bạn đang thực hiện thủ thuật này trong cùng một hàm mà mảng được khai báo ở bên trong hàm đó (chúng ta sẽ nói thêm về lý do mà hạn chế này lại tồn tại, trong một bài học khác của chương này).

8. Xác định độ dài của một mảng cố định trong C++ 17/C++ 20

Trong C++ 17, có một tùy chọn tốt hơn để sử dụng là hàm std::size(), được định nghĩa trong header <iterator>.

Dưới đây là ví dụ:

/**
* 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> // for std::cout
#include <iterator> // for std::size
 
int main()
{
    int array[] = { 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << "The array has: " << std::size(array) << " elements\n";
 
    return 0;
}

Kết quả được in ra:

The array has 8 elements

std::size() có ưu điểm là dễ nhớ hơn, nó sẽ hoạt động với các loại đối tượng khác (như std::Array và std::vector), và nó sẽ đưa ra lỗi trình biên dịch nếu bạn cố sử dụng nó trên một mảng cố định đã được truyền cho một hàm! Lưu ý rằng std::size() sẽ trả về một unsigned value – giá trị không dấu.

Trong C ++ 20, std :: ssize thậm chí còn tiện lợi hơn, vì nó trả về một giá trị có dấu.

Khuyến nghị: Hãy ưu tiên sử dụng std::size (C ++ 17) hoặc std::ssize (C ++ 20 trở đi), hơn là sử dụng thủ thuật chia độ dài mảng, nếu trình biên dịch của bạn đang ở phiên bản C++ 17/C ++ 20 và cao hơn.

9. Truy cập tới một phần tử nằm ngoài phạm vi mảng

Hãy nhớ rằng, một mảng có độ dài N sẽ có các phần tử mảng được đánh số từ 0 đến N-1. Vậy, điều gì sẽ xảy ra nếu bạn cố truy cập tới một phần tử nằm ngoài phạm vi của mảng?

Xét đoạn chương trình sau:

int main()
{
    int prime[5]; // hold the first 5 prime numbers
    prime[5] = 13;
 
    return 0;
}

Trong đoạn code trên, mảng của ta có độ dài là 5, nhưng chúng ta đang cố gắng ghi một số nguyên tố vào phần tử thứ 6 (nằm tại index 5 của mảng).

C++ sẽ không thực hiện bất kỳ kiểm tra nào để đảm bảo rằng các chỉ số truy cập phần tử mảng của bạn là hợp lệ với độ dài của mảng. Vì vậy, trong ví dụ trên, giá trị 13 sẽ được chèn vào phần bộ nhớ mà đáng ra phần tử thứ 6 nên nằm tại đó (nếu ban đầu, mảng được khai báo có độ dài là 6). Khi điều này xảy ra, bạn sẽ nhận được một lỗi undefined behavior – hành vi không xác định. Ví dụ: Việc ghi giá trị lên một vị  trí nằm ngoài phạm vi của mảng có thể gây ra ghi đè lên giá trị của một biến khác, hoặc khiến chương trình của bạn bị crash.

Mặc dù không thường xuyên xảy ra, nhưng C++ cũng sẽ cho phép bạn sử dụng một số âm làm chỉ số truy cập phần tử mảng, với kết quả không mong muốn tương tự như ở trên.

Quy tắc: Khi sử dụng mảng, hãy đảm bảo rằng các index – chỉ số truy cập phần tử mảng của bạn là hợp lệ đối với phạm vi của mảng!

Bài tập C về mảng một chiều

Bài tập C về mảng hai chiều

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