1.8. Mảng dữ liệu

Trong rất nhiều chương trình, chúng ta cần sử dụng đến nhiều biến có kiểu dữ liệu giống nhau và đảm nhiệm những vai trò tương tự nhau. Một ví dụ dễ gặp là danh sách người dùng của 1 trang web : cái này cần đến 1 số lượng lớn biến kiểu chuỗi ký tự string. Hoặc trường hợp của bảng thành tích ghi lại 10 người đứng đầu trong một trò chơi, chúng ta cần sử dụng 10 ô nhớ khác nhau chứa dữ liệu kiểu int.

C++ nói riêng và các ngôn ngữ lập trình nói chung cung cấp cho chúng ta 1 phương thức để nhóm tất cả những dữ liệu tương đồng nhau này vào cùng 1 chỗ. Phương thức đó gọi là mảng (hoặc bảng) dữ liệu.

Trong chương này, tôi sẽ nói với các bạn về cách thao tác với 2 loại mảng. Loại thứ 1 là những mảng mà ta xác định trước về kích thước như trong ví dụ về bảng thành tích của trò chơi và loại thứ 2 là những mảng có kích thước thay đổi liên tục như danh sách người dùng của trang web, luôn không ngừng tăng lên. Đương nhiên bởi vì kiểu mảng thứ nhất với kích thước cố định thì dễ thao tác hơn, vậy nên chúng ta sẽ bắt đầu với mảng kiểu này.

Mảng cố định

Trong phần mở đầu, tôi đã nói với các bạn về lợi ích của mảng là cho phép lưu trữ nhiều biến có cùng kiểu dữ liệu. Sau đây là ví dụ về bảng thành tích của siêu cấp trò chơi mà các bạn sẽ viết trong tương lai.

1 ví dụ sử dụng mảng

Nếu bạn muốn hiển thị danh sách 5 người chơi xuất sắc nhất cùng với số điểm của họ, bạn sẽ cần đến 2 danh sách : 1 dành cho tên của người chơi và 1 dành cho số điểm của họ. Chúng ta sẽ phải khai báo 10 biến để có thể lưu những thông tin này. Sau đây là ví dụ :

string nguoiChoi1("Superman");
string nguoiChoi2("Batman ");
string nguoiChoi3("Spidey");
string nguoiChoi4("Wolverine");
string nguoiChoi5("Wonder woman");

int diemThanhTich1(11);
int diemThanhTich2(7);
int diemThanhTich3(5);
int diemThanhTich4(3);
int diemThanhTich5(2);

Và cần phải hiện chúng ra màn hình nữa chứ, thật quá nhiều việc .

cout << "1) " << nguoiChoi1 << " " << diemThanhTich1 << endl;
cout << "2) " << nguoiChoi2 << " " << diemThanhTich2 << endl;
cout << "3) " << nguoiChoi3 << " " << diemThanhTich3 << endl;
cout << "4) " << nguoiChoi4 << " " << diemThanhTich4 << endl;
cout << "5) " << nguoiChoi5 << " " << diemThanhTich5 << endl;

Cả tá dòng lệnh phải viết ! Hãy tưởng tượng nếu chúng ta muốn hiển thị không phải là 5 mà là 100 người chơi đứng đầu, vậy sẽ cần đến 200 biến và 100 dòng lệnh hiển thị ra màn hình từa tựa nhau ! Tốt nhất là hãy dừng lại và nghĩ cách khác hợp lý hơn vì nếu không lượng công việc của bạn sẽ nhiều không tưởng.

Đây là lúc mà chúng ta sẽ cần dùng đến mảng. Chúng ta sẽ khai báo 100 thành tích cao nhất cũng như tên của 100 người chơi cùng lúc. Chúng ta chỉ tạo ra 2 ô nhớ, 1 dành cho 100 biến kiểu int và ô còn lại cho 100 biến kiểu string. Thật thần kỳ phải không ?

! Trong 1 mảng thì các biến phải có 1 liên hệ nào đó với nhau thì mảng mới có ý nghĩa. Ví dụ việc để tuổi cúa chú cún cưng và số người dùng của trang web trong cùng 1 mảng là hoàn toàn vô nghĩa dù là chúng có cùng kiểu dữ liệu là int.

Trong ví dụ này, chúng ta cần 100 biến, có nghĩa là 100 chỗ trong mảng. Thuật ngữ tin học gọi đó là kích thước mảng. Nếu kích thước mảng không thay đổi, chúng ta gọi đó là mảng cố định, chính là thứ chúng ta cần lúc này.

Khai báo mảng cố định

Trong C++, 1 biến  được xác định bởi tên biến và kiểu dữ liêu. Điều này vẫn đúng khi áp dụng cho mảng bởi vì nói cho cùng, đây cũng chỉ là 1 kiểu biến đặc biệt. Khai báo mảng vì thế cũng khá giống với cú pháp khi khai báo biến, chỉ là có thêm 1 thông số để xác định kích thước của mảng.

Kiểu_dữ_liệu tên_biến [kích_thước]

Chúng ta chỉ ra kiểu dữ liệu của mảng, tên mảng và cuối cùng là kích thước ở trong dấu []. Hãy xem 1 ví dụ khai báo mảng.

#include <iostream>
using namespace std;
int main(){
   int bangThanhTich[5];       //Mang chua 5 bien int
   double gocTamGiac[3];   //Mang gom 3 bien double chua gia tri cac goc cua 1 tam giac
   return 0;
}

Hãy xem qua hình vẽ quen thuộc miêu tả bộ nhớ của chúng ta :

Chúng ta vẫn có 2 vùng nhớ gắn với nhãn của chúng. Khác biệt là lần này mỗi vùng đã bị chia nhỏ ra thành các ô : 3 ô với gocTamGiac và 5 ô với bangThanhTich. Lúc này thì các ô đó vẫn chưa được gán giá trị nên trong đấy sẽ chứa 1 giá trị bất kỳ nào đó.

Chúng ta cũng có thể khai báo bảng sử dụng 1 hằng số kiểu int hoặc unsigned int làm kích thước. Đơn giản chỉ cần viết tên của hằng số đó vào giữa dấu [].

int const kichThuoc(20);   //Kich thuoc mang
double bangThanhTich[kichThuoc];

! Kích thước mảng PHẢI là hằng số.

Tôi khuyến khích các bạn nên dùng hằng số làm kích thước cho mảng thay vì đưa trực tiếp giá trị kích thước vào giữa dấu []. Đây là 1 thói quen tốt nên tập.

Vậy là chúng ta đã có chỗ trong bộ nhớ, giờ chỉ việc đi sử dụng nó thôi.

Thao tác với các phần tử của mảng

Mỗi ô trong mảng có thể được sử dụng giống như bất cứ một biến nào khác. Điều bạn cần chỉ là cách thức để chỉ ra xem bạn muốn dùng ô nào. Để làm thế, bạn cần tên của mảng và số thứ tự của ô nhớ mà bạn muốn dùng. Như trong ví dụ về mangThanhTich, bạn có thể truy cập vào 5 biến : ô thứ nhất của bangThanhTich, ô thứ 2, … cho tới ô thứ 5.

Đẻ truy cập vào từng ô nhớ, cú pháp là tenMang[soThuTuONho]. Duy nhất 1 chú ý nho nhỏ, đó là số thứ tự trong mảng bắt đầu từ 0 chứ không phải 1. Vậy nên ô đầu tiên có chỉ số là 0 và tất cả các chỉ số đều lệch đi 1 đơn vị. Ví dụ như để sử dụng ô thứ 3 của bangThanhTich, bạn phải viết :

bangThanhTich[2] = 5;

vì 3 – 1 = 2, ô thứ 3 có chỉ số là 2. Nếu bạn muốn điền giá trị vào mảng như trong ví dụ đầu bài học, chúng ta có thể viết :

int const soThanhTich(5);           //Kich thuoc mang 
int bangThanhTich[soThanhTich];   //Khai bao mang 
bangThanhTich[0] = 11;  //Dien o thu 1 
bangThanhTich[1] = 7;  // Dien o thu 2 
bangThanhTich[2] = 5;   // Dien o thu 3 
bangThanhTich[3] = 3;   // Dien o thu 4 
bangThanhTich[4] = 2;   // Dien o thu 5

! Chú ý ô cuối cùng có chỉ số là 4 chứ không phải 5

Duyệt mảng

Một trong những điểm mạnh của mảng là cho phép chúng ta duyệt các dữ liệu chứa trong đó bằng cách sử dụng vòng lặp. Chúng ta cũng có thể thông qua đó thực hiện các xử lý trên từng ô của mảng như là hiển thị giá trị của chúng chẳng hạn.

Bởi vì chúng ta đã biết trước kích thước của mảng, sử dụng vòng lặp for là hợp lý nhất. Hay nhất là càng có thể tận dụng biến đếm i của vòng lặp để giúp truy cập đên ô thứ i trong mảng. Cứ như chúng sinh ra để dành cho nhau vậy.

int const soThanhTich (5);           //Kich thuoc mang
int bangThanhTich[soThanhTich];   //Khai bao mang

bangThanhTich[0] = 11;
bangThanhTich[1] = 7;
bangThanhTich[2] = 5;
bangThanhTich[3] = 3;
bangThanhTich[4] = 2;  

for(int i(0); i< soThanhTich; i++){
    cout << bangThanhTich [i] << endl;
}

Biến i sẽ mang các giá trị 0, 1, 2, 3 rồi 4 nên là các giá trị bangThanhTich[0], bangThanhTich[1],… sẽ được chuyển dần dần cho lệnh cout để in ra màn hình.

! Cần chú ý để biến đếm của vòng lặp không vượt qua giá trị kích thước của mảng nếu không chương trình sẽ bị lỗi.Chỉ số tối đa của bangThanhTich soThanhTich-1 nên những giá trị của biến đếm trong ví dụ phải nằm trong khoảng từ 0 tới soThanhTich -1.

Rồi bạn sẽ thấy mảng kết hợp với vòng lặp là một công cụ rất mạnh và hay được sử dụng.

Một ví dụ nhỏ

Tôi đề nghị là chúng ta thực hành 1 ví dụ nhỏ hơi có chút rắc rối. Chúng ta sẽ sử dụng C++ để tính điểm trung bình trong năm học của một người. Chúng ta sẽ lưu tất cả điểm số trong năm vào 1 mảng và sử dụng vòng lặp for để tính giá trị trung bình. Sau đây là lời giải với từng bước chú thích.

Bước thứ nhất là khai báo 1 mảng để lưu các điểm số. Bởi vì các điểm số sẽ là số thập phân, vậy nên chúng ta sẽ sử dụng kiểu double.

int const soDiem(6);
double diem[soDiem];

Bước thứ 2 là điền mảng với các giá trị điểm số.

int const soDiem(6);
double diem[soDiem];

diem[0] = 12.5;
diem[1] = 19.5;
diem[2] = 6.;
diem[3] = 12;
diem[4] = 14.5;
diem[5] = 15;

! Tôi nhắc lại là chỉ số của ô thứ nhất là 0, ô thứ 2 là 1 và cứ thế tiếp diễn

Để tính điểm trung bình, trước hết chúng ta phải tính tổng số điểm sau đó chia cho số điểm nhận được trong năm. Chúng ta đã có số điểm, chính là giá trị của hằng số soDiem. Vậy nên chỉ còn cần tính tổng số điểm nữa là được.

Phép tính này được thực hiện trong 1 vòng lặp duyệt qua tất cả các dữ liệu trong mảng.

double trungBinh(0);
for(int i(0); i<soDiem; i++){
   trungBinh += diem[i];   //Cong tat ca cac diem voi nhau
}
//Toi cuoi vong lap ta se co tong so diem (79.5)
//Lay tong so diem chia cho so diem
trungBinh /= soDiem;

Thêm 1 câu lệnh nhỏ để hiển thị kết quả, thế là ta đã có chương trình tính điểm trung bình.

#include <iostream>
using namespace std;
int main(){
   int const soDiem(6);
   double diem[soDiem];
   
   diem[0] = 12.5;
   diem[1] = 19.5;
   diem[2] = 6.;
   diem[3] = 12;
   diem[4] = 14.5;
   diem[5] = 15;

   double trungBinh(0);
   for(int i(0); i<soDiem; i++){
      trungBinh += diem[i];   //Cong tat ca cac diem voi nhau
   }
   //Toi cuoi vong lap ta se co tong so diem (79.5)
   //Lay tong so diem chia cho so diem
   trungBinh /= soDiem;
   cout << "Diem trung binh cua ban la : " << trungBinh << endl;
   return 0;
}

Kết quả nhận được khi chạy chương trình là

Hoàn hảo !

Các mảng và các hàm

Chắc hẳn các bạn chưa quên ngay các hàm chứ hả ? Chúng ta cần bỏ ra ít thời gian để nói lại về nó. Thật buồn là bạn sẽ thấy rằng các mảng và các hàm không thân thiện với nhau cho lắm.

Hạn chế đầu tiên là không bao giờ viết 1 hàm trả về giá trị là 1 mảng cố định. Điều này là không thế.

Hạn chế thứ 2 là 1 mảng cố định làm thông số luôn luôn được truyền qua tham chiếu. Bạn không cần dùng đến dấu & bởi vì việc này sẽ được C++ làm 1 cách tự động. Điều đó nghĩa là khi bạn đưa 1 mảng cho hàm xử lý, mảng này có thể bị thay đổi.

Hãy xem 1 hàm nhận 1 mảng làm thông số.

void ham(double mang[]){
    //…
}

! Chú ý là không viết gì bên trong dấu []

Nhưng đấy không phải là tất cả ! Rất thường xuyên, khi chúng ta muốn duyệt mảng với vòng lặp for ở trong hàm, chúng ta thiếu đi 1 dữ kiện cần thiết. Các bạn đoán ra không?

Kích thước mảng ! Ở trong khai báo hàm lúc trước, chúng ta không có cách nào biết được giá trị kích thước của mảng. Bạn bắt buộc phải thêm vào 1 thông số của hàm để cho biết kích thước của mảng là bao nhiêu. Vậy nên chúng ta có :

void ham(double mang[], int kichThuoc){
    //…
}

Thật buồn phải không ! Thế nhưng đừng trách tôi, tôi không phải là người đã tạo ra C++.

Để luyện tập, tôi nghĩ sao các bạn không thử viết 1 hàm để tính giá trị trung bình của các dữ liệu trong 1 mảng nhỉ.

Sau đây là phiên bản của tôi. Đừng nên xem trước khi tự bạn viết ra phiên bản của mình.

/*
 *  Ham tinh gia tri trung binh cua cac so trong mang
 *  - mang : mang so lieu ma chung ta muon tinh trung binh
 *  - kichThuoc : kich thuoc cua mang
 */
double trungBinh(double mang[], int kichThuoc){
   double ketQuaTB(0);
   for(int i(0); i<kichThuoc;i++)   {
      ketQuaTB += mang[i];   //Cong tat ca cac gia tri
   }
   ketQuaTB /= kichThuoc;
   return ketQuaTB;
}

Thế là đủ rồi. Kế tiếp !

Mảng động

Khác biệt đầu tiên khi dùng mảng động so với mảng cố định xuất hiện ngay ở đầu chương trình. Bạn cần thêm dòng #include <vector> để sử dụng kiểu mảng này.

! Vì lý do trên mà chúng ta hay dùng vector mỗi khi muốn nói đến mảng động. Trong phần tới, đôi khi tôi sẽ sử dụng thuật ngữ này.

Khác biệt lớn thứ 2 nằm ở trong cú pháp khai báo mảng.

vector<kiểu_dữ_liệu> tên_biến (kích_thước)

Ví dụ để khai báo 1 mảng chứa 5 số nguyên

#include <iostream>
#include <vector> // Dung quen!
using namespace std;
int main(){
   vector<int> mang(5);
   return 0;
}

Cần phải chú ý 3 điều ở đây:

  • Kiểu dữ liệu không nằm ở đầu dòng như khi khai báo các biến khác
  • Chúng ta sử dụng 1 cái tên lạ hoắc với dấu <> ở đầu
  • Kích thước mảng nằm trong dấu () chứ không phải []

Điều này có nghĩa là kiểu màng này không hoàn toàn giống mảng cố định. Tuy vậy, các bạn sẽ thấy rằng việc duyệt mảng về cơ bản cũng tương tự.

Trước đó, có 2 mẹo nhỏ bạn nên biết.

Chúng ta có thể điền tất cả các ô trong mảng với 1 giá trị bằng cách thêm 1 thông số vào trong dấu ().

vector<int> mang(5, 3);  //Mang gom 5 so nguyen co gia tri la 3
vector<string> danhSachTen(12, "Vo danh"); // 12 chuoi ky tu co gia tri “Vo danh”

Chúng ta cũng có thể tạo ra 1 mảng không có ô nào bằng cách không viết dấu ().

vector<double> mang; //Mang gom 0 so thap phan

? Tại sao lại tạo ra 1 mảng trống không chứa ô dữ liệu nào?

Bạn có nhớ là kích thước của mảng chúng ta đang xét có thể thay đổi không. Bạn luôn có thể thêm các ô vào mảng đó. Kiên nhẫn 1 chút rồi bạn sẽ biết hết thôi.

Truy cập đến các phần tử của mảng

Việc khai báo mảng động thì rất khác so với mảng cố định, thế nhưng để truy cập đến dữ liệu trong mảng thì phương pháp lại hoàn toàn không đổi. Chúng ta sẽ dùng dấu [] và các chỉ số với chỉ số ô đầu tiên là 0.

Chúng ta có thể viết lại ví dụ bên trên bằng cách sử dụng vector.

int const soThanhTich(5);           //Kich thuoc mang
vector<int> bangThanhTich(soThanhTich);   //Khai bao mang

bangThanhTich[0] = 11; //Dien o thu 1
bangThanhTich[1] = 7;  // Dien o thu 2
bangThanhTich[2] = 5;  // Dien o thu 3
bangThanhTich[3] = 3;  // Dien o thu 4
bangThanhTich[4] = 2; // Dien o thu 5

Không thể đơn giản hơn được nữa.

Thay đổi kích thước

Hãy đi vào vấn đề chính : thay đổi kích thước của mảng. Bắt đầu bằng cách thêm vào 1 ô nhớ ở cuối mảng.

Chúng ta phải dùng hàm push_back(). Dòng lệnh bắt đầu với tên của mảng, theo sau là 1 dấu . rồi push_back với dấu ngoặc () trong đấy có chứa giá trị chúng ta muốn chuyển cho ô nhớ.

Hãy nhìn hình minh họa bộ nhớ sau

1 ô nhớ đã được tự động thêm vào cuối mảng. Máy tính thần kỳ thật !

Và đương nhiên là bạn có thể thêm vào nhiều ô nhớ, ô này nối tiếp ô kia.

vector<int> mang(3,2); //Mang chua 3 so nguyen co gia tri la 2
mang.push_back(8);  //Them 1 o chua so 8
mang.push_back(7);  //Them 1 o chua so 7
mang.push_back(14); //Va 1 o nua chua so 14
//Mang bay gio chua : 2 2 2 8 7 14             

? Thế kích thước mảng chỉ có thể tăng lên thôi à ?

Đương nhiên là không. Những người sáng tạo ra C++ đều đã tính đến mọi thứ.

Chúng ta có thể xóa ô cuối cùng trong bảng bàng cách sử dụng hàm pop_back(). Hàm này cách dùng giống với push_back() nhưng bạn không cần đưa thông số.

vector<int> mang(3,2); //Mang gom 3 so 2
mang.pop_back(); //Chi con 2 so!
mang.pop_back(); //Chi con 1 so

! Chú ý đừng xóa quá tay. 1 mảng không thể có ít hơn 0 ô. Hợp lý chứ.

Tôi chắc rằng không cần nói thêm gì về việc này nữa.

Vấn đề nhỏ cuối cùng chúng ta cần giải quyết đó là làm sao xác định được kích thước của bảng nếu nó thay đổi liên tục. Thật tốt là chúng ta có 1 hàm là size() : mang.size() sẽ trả về số lượng ô có trong mang.

vector<int> mang(5,4); //Mang gom 5 so 4
int const kichThuoc(mang.size());
//Bien chua kich thuoc cua mang
//Gia tri cua kichThuoc la 5
Bài tập nho nhỏ

Cách tốt nhất để bạn có thể nhanh ghi nhớ tất cả những thứ này trong đầu là hãy thử tự viết lại bài tập tính điểm trung bình ở bên trên nhưng lần này hãy sử dụng các vector.

Dưới đây là đoạn mã của tôi

#include <iostream>
using namespace std;
int main(){
  vector<double> diem;
  diem.push_back(12.5);  
  diem.push_back(19.5);
  diem.push_back(6.);
  diem.push_back(12);
  diem.push_back(14.5);
  diem.push_back(15);  

  double trungBinh(0);
  for(int i(0); i<diem.size(); i++){
    trungBinh += diem[i];   //Cong tat ca cac diem voi nhau
  }
  trungBinh /= diem.size();
  cout << "Diem trung binh cua ban la : " << trungBinh << endl;
  return 0;
}

2 chuong trình bạn đã viết làm cùng 1 việc nhưng với các cách thức khác nhau. Về cơ bản, luôn có nhiều cách khác nhau để cùng đạt 1 mục đích. Mỗi người sẽ chọn cách mình thấy thích và phù hợp.

Các vector và các hàm

Sử dụng các mảng động làm thông số cho hàm dễ chịu hơn nhiều so với khi sử dụng các mảng cố định. Chúng ta chỉ cần sử dụng cung cấp cho hàm thông số loại vector<kiểu_dữ_liệu> là được. Không cần thiết phải thêm 1 thông số thứ 2 cho hàm vì kích thước mảng có thể được xác định nhờ hàm size().

void ham(vector<int> a){
    //…
}

Quá dễ đúng không? Nhưng chúng ta thậm chí có thể làm tốt hơn thế. Các bạn có nhớ là tôi từng nhắc đến tham chiếu hằng trong bài học trước chứ. Với những thông số kiểu mảng với rất nhiều phần tử, sử dụng tham chiếu hằng sẽ giúp chúng ta tiết kiệm được rất nhiều tài nguyên khi tránh được việc chép tất cả thông số vào bộ nhớ.

! Trong trường hợp này thì hàm không thể thay đổi được nội dung của mảng. Nếu muốn thay đổi, bạn chỉ việc sử dụng tham chiếu thường, không có từ khóa const là được.

Để gọi 1 hàm có thông số kiểu vector không có gì khó khăn cả, chỉ cần viết tên của mảng vào vị trí của thông số là được.

vector<int> mang(3,2);  
ham(mang);          //Dua thong so ‘mang’ cho ham vua dinh nghia o tren

! 1 hàm nhận vector<int> làm thông số thì đừng đưa cho nó biến kiểu vector<char>. Tuyệt đối không thể nhầm lẫn giữa các kiểu của những dữ liệu trong mảng.

Nếu các bạn cũng giống tôi, muốn chia mã nguồn ra thành các tệp nguồn và tệp tiêu đề, hãy nhớ thêm #include <vector> vào trong tệp tiêu đề cũng như thêm std:: vào trước vector giống như khi chúng ta đã làm với string.

#ifndef MANG_H_INCLUDED
#define MANG_H_INCLUDED

#include <vector>

void ham(std::vector<int>& mang);

#endif // MANG _H_INCLUDED

Ngoài ra chúng ta cũng có thể viết 1 hàm trả về kết quả là 1 vector. Các bạn chắc sẽ không thấy lạ nếu tôi viết định nghĩa hàm như sau

vector<double> ham(int a){
    //...
}

cho 1 hàm trả về 1 mảng chứa các số thập phân.

Tuy nhiên, cần lưu ý là việc này sẽ dẫn đến việc tạo ra 1 bản sao của mảng ở cuối đoạn xử lý hàm. Để tránh điều này, người ta thường thiên về việc truyền bằng tham chiếu cho hàm 1 thông số kiểu mảng và thay đổi giá trị của nó. Nếu bạn muốn nâng cao hiệu suất của chương trình, luôn luôn chú ý đến cách sử dụng các mảng 1 cách hữu hiệu.

Mảng đa chiều

Từ đầu bài học, tôi đã nhắc đến việc chúng ta sẽ tạo ra các mảng dữ liệu có thể chứa bất cứ thứ gì. Có những mảng số nguyên, mảng số thực, mảng chuỗi ký tự, vv … Và đương nhiên là có những mảng chứa các … mảng !

Một vài trong số các bạn sẽ nhăn trán và nghĩ không hiểu điều này thì có lợi ích gì. Tôi đề nghị chúng ta sẽ bắt đầu với hình vẽ quen thuộc của bộ nhớ và biết đâu, các bạn sẽ thấy khái niệm này bớt kỳ quặc 1 chút.

Hình vuông to màu vàng, như mọi khi, thể hiện 1 biến trong bộ nhớ. Đây là 1 mảng có 5 phần tử ngăn cách nhau bởi những đường kẻ đậm. Bên trong mỗi phần tử đó lại được chia ra thành 4 ô nhỏ hơn mà giá trị của mỗi ô là không xác định.

Không quá khó hiểu như tưởng tượng phải không ? Đúng vậy, mảng đa chiều chính là một ma trận mà mỗi mắt là 1 biến độc lập.

Khai báo mảng đa chiều

Để khai báo mảng đa chiều, chúng ta đặt kích thước mỗi chiều vào trong dấu [] và xếp chúng nối tiếp nhau sau tên mảng.

kiểu_dữ_liệu tên_mảng[kích_thước_X][ kích_thước_Y]

Mảng như trong hình vẽ sẽ được khai báo như sau

int mang[5][4]

Hoặc chúng ta có thể sử dụng các hằng số như sau

int const kichThuocX(5);
int const kichThuocY(4);
int mang[kichThuocX][kichThuocY];

Thật dễ dàng nhỉ !

Truy cập đến các phần tử của mảng

Dù tôi không nói thì chắc các bạn cũng đoán ra được. Để truy cập tới 1 phần tử của mảng thì ta phải cung cấp tọa độ của ô nhớ trong ma trận.

Ví dụ mang[0][0] là ô trên cùng bên trái, mang[0][1] là ô ngay bên dưới nó và mang[1][0] là ô ở ngay cạnh bên phải.

Như chúng ta đã biết thì ô cuối cùng của 1 mảng sẽ có chỉ số là kích thước của mảng trừ đi 1. Vì vậy, ô ở hàng dưới cùng và ngoài cùng bên phải của mảng trong ví dụ sẽ phải là mang[kichThuocX-1][kichThuocY-1] hay mang[4][3].

Đưa mọi thứ đi xa hơn …

Nếu bạn cần những thứ phức tạp hơn, bạn có thể tạo ra mảng 3 chiều, hay thậm chí nhiều hơn giống như sau

double mangSieuCap[5][4][6][2][7];

Thế nhưng đừng yêu cầu tôi vẽ hình minh họa cho bạn trong trường hợp này. Tôi có thể khẳng định với bạn là các lập trình viên chuyên nghiệp cũng rất hiếm khi sử dụng tới mảng nhiều hơn 3 chiều trừ khi bạn định viết 1 chương trình siêu siêu siêu phức tạp. Mảng 3 chiều thì thông dụng hơn, thường dùng trong các chương trình giả lập không gian như giả lập bay, giả lập thực nghiệm, vv… và tất cả những thứ đại lượng trong khoa học như tốc độ, gia tốc, vv…

Các bạn cũng có thể tạo ra mảng động đa chiều bằng cách sử dụng các vector. Ví dụ mảng động 2 chiều :

vector<vector<int> > mang;

Vấn đề là đây không thực sự là 1 ma trận 2 chiều mà đúng hơn là 1 mảng các dòng. Ta có thể bắt đầu thêm từng dòng vào trong mảng.

mang.push_back(vector<int>(5));   //Dong dau tien co 5 o
mang.push_back(vector<int>(3,4)); //Them dong thu 2 co 3 o chua cac so 4

Như các bạn đã thấy, mỗi dòng lại có chiều dài khác nhau. Thế nhưng để truy cập vào các dòng, chúng ta vẫn sử dụng phương pháp quen thuộc.

mang[0].push_back(8);     //Them 1 o vao dong dau tien

Để truy cập vào từng ô thì phương pháp cũng không hề thay đổi. Tuy nhiên chúng ta phải chắc chắn rằng ô mà chúng ta muốn truy cập đến phải trong giới hạn của mảng.

mang[2][3] = 9;     //Thay doi gia tri o nho o dong thu 2 cot thu 3

! Mảng đa chiều vector không phải là 1 giải pháp hữu hiệu để truy cập vào bộ nhớ vì nó làm giảm hiệu suất của chương trình. Vì thế người ta thường hay sử dụng các mảng đa chiều cố định hơn trừ khi việc có thể thay đổi kích thước mảng là yếu tố bắt buộc.

Trường hợp của các chuỗi ký tự

1 tiết lộ nho nhỏ trước khi kết thúc chương này, đó là kiểu string mà chúng ta vẫn hay sử dụng, thực chất là 1 kiểu mảng.

Chúng ta không nhận ra điều này vì những người tạo ra ngôn ngữ C++ đã giấu nó đi. Nhưng thực tế thì nó không khác gì một mảng chứa các ký tự. Thậm chí nó có khá nhiều điểm chung với các vector.

Truy cập vào các ký tự

Lợi ích của điều này là chúng ta có thể truy cập vào từng ký tự trong chuỗi và thay đổi chúng. Và thật bất ngờ là chúng ta cũng sẽ sử dụng các dấu [] cho việc truy cập này.

#include <iostream>
#include <string>
using namespace std;

int main(){
   string ten("Harry");
   cout << "Ban ten la" << ten << "." <<endl;

   ten[0] = 'B';  //Thay doi chu dau tien
   cout << "A khong, ten ban la " << ten << "!" << endl;
   return 0;
}

Kết quả nhận được là :

Chưa hết, chúng ta vẫn còn có thể làm nhiều hơn thế.

Các chuỗi và các hàm

Chúng ta có thể sử dụng hàm size() để biết số ký tự và hàm push_back() để thêm các chữ vào cuối chuỗi giống như với các vector.

string cau("Xin chao Tan Binh ! Chao mung ban den voi the gioi cong nghe thong tin.");  //71 ky tu
cout << "Cau nay chua " << cau.size() << " ky tu." << endl;

Nhưng khác với các mảng, chúng ta thậm chí có thể thêm nhiều ký tự cùng 1 lúc bằng cách sử dụng dấu +=.

#include <iostream>
#include <string>
using namespace std;

int main(){
   string ten("Clark");
   string ho("Ken");

   string tenDayDu;    //1 chuoi ky tu trong
   tenDayDu += ten;    //Them vao ten
   tenDayDu += " ";    //Them vao dau cach
   tenDayDu += ho;     //Cuoi cung la them ho

   cout << "Ban ten la " << tenDayDu << "." << endl;
   return 0;
}

Và kết quả không quá khó đoán :

Tóm tắt bài hoc :
  • Các mảng là 1 chuỗi các biến trong bộ nhớ. Ví dụ 1 mảng có 4 ô tương đương với 4 biến chứa trong 4 ô nhớ nằm sát nhau.
  • 1 mảng sẽ được khởi tạo như ví dụ sau : int bangThanhTich[4]; cho 1 mảng có 4 ô
  • Ô đầu tiên trong mảng có chỉ số là 0
  • Nếu kích thước của mảng thay đổi liên tục, hãy sử dụng mảng động sử dụng kiểu vector : vector<int> mang(5);
  • Có thể tạo ra các mảng đa chiều, ví dụ như int mang[5][4] là 1 mảng có 5 dòng 4 cột.
  • Chuỗi ký tự cũng có thể được coi là mảng mà mỗi ký tự là 1 ô của mang.