Trong bài này chúng ta sẽ tìm hiểu mảng và con trỏ trong C++: mảng được xếp trong bộ nhớ thế nào, địa chỉ là gì, cách dùng con trỏ với toán tử & và *, nullptr, mối quan hệ giữa con trỏ và mảng, và cuối cùng là std::array của C++ hiện đại. Con trỏ là phần làm nhiều bạn sợ nhất, nên mình sẽ in địa chỉ thật ra màn hình để các bạn nhìn tận mắt chứ không chỉ học lý thuyết nhé.
Bài 11 trong Serie Lập trình C++ từ A tới Z
Mảng trong C++
Mảng (array) là một dãy các phần tử cùng kiểu, nằm liền nhau trong bộ nhớ. Thay vì khai báo 5 biến soLuong1, soLuong2… để lưu số cuốn còn trong kho của 5 đầu sách, chúng ta khai báo một mảng 5 phần tử:
#include <iostream>
int main()
{
int soLuong[5] = {3, 0, 7, 2, 5}; // so cuon con trong kho cua 5 dau sach
std::cout << "Phan tu dau: " << soLuong[0] << '\n';
std::cout << "Phan tu cuoi: " << soLuong[4] << '\n';
soLuong[1] = 10; // nhap them 10 cuon cho dau sach thu 2
int tong = 0;
for (int x : soLuong)
tong += x;
std::cout << "Tong so cuon: " << tong << '\n';
std::cout << "sizeof(soLuong) = " << sizeof(soLuong) << " byte, so phan tu = "
<< sizeof(soLuong) / sizeof(soLuong[0]) << '\n';
return 0;
}
- Khai báo
int soLuong[5]: kiểu phần tử, tên mảng, số phần tử trong ngoặc vuông. Số phần tử phải là hằng số biết lúc biên dịch, nhưconstexprở bài hằng và biến C++. - Chỉ số bắt đầu từ 0: mảng 5 phần tử có chỉ số từ 0 tới 4.
sizeof(soLuong)là kích thước cả mảng, 5 x 4 = 20 byte. Chia cho kích thước một phần tử thì ra số phần tử.
Mảng nằm trong bộ nhớ như thế nào?
Mỗi ô nhớ có một địa chỉ (address), giống như số nhà trên một con phố. Toán tử & đặt trước biến cho biết địa chỉ của nó. In thử địa chỉ của từng phần tử:
#include <iostream>
int main()
{
int soLuong[4] = {3, 10, 7, 2};
for (int i = 0; i < 4; i++)
std::cout << "soLuong[" << i << "] = " << soLuong[i]
<< " dia chi: " << &soLuong[i] << '\n';
return 0;
}
Địa chỉ được in ở dạng hệ 16. Để ý các địa chỉ cách nhau đúng 4, vì mỗi phần tử int chiếm 4 byte và các phần tử nằm sát nhau:
Chính vì nằm liền nhau nên truy cập soLuong[i] rất nhanh: máy chỉ cần lấy địa chỉ phần tử đầu, cộng thêm i x 4 byte là tới.
Con trỏ là gì?
Con trỏ (pointer) là một biến lưu địa chỉ của biến khác. Nếu biến thường là ngôi nhà, thì con trỏ là tờ giấy ghi địa chỉ ngôi nhà đó. Có tờ giấy thì tìm được tới nhà, vào nhà xem hay sửa đồ bên trong.
#include <iostream>
int main()
{
int soSach = 42;
int* p = &soSach; // p luu dia chi cua soSach
std::cout << "Gia tri soSach: " << soSach << '\n';
std::cout << "Dia chi soSach: " << &soSach << '\n';
std::cout << "Gia tri cua p: " << p << '\n';
std::cout << "*p (gia tri tai dia chi p): " << *p << '\n';
*p = 50; // sua soSach thong qua con tro
std::cout << "Sau *p = 50, soSach = " << soSach << '\n';
int* q = nullptr; // con tro chua tro vao dau
if (q == nullptr)
std::cout << "q chua tro vao dau\n";
return 0;
}
Hai toán tử cần nhớ:
&bien: lấy địa chỉ của biến.*con_tro: lấy (hoặc sửa) giá trị tại địa chỉ mà con trỏ đang giữ, gọi là giải tham chiếu (dereference).
int* p đọc là “p là con trỏ tới int“. Giá trị của p đúng bằng địa chỉ của soSach, và *p = 50 sửa thẳng vào soSach.
nullptr
Con trỏ chưa trỏ vào đâu thì gán bằng nullptr. Đây là từ khóa của C++11, thay cho NULL hay số 0 trong C vì nó có kiểu riêng, không bị nhầm với số nguyên khi nạp chồng hàm.
Lưu ý: Giải tham chiếu con trỏ nullptr hoặc con trỏ chưa khởi tạo (*q khi q là nullptr) sẽ làm chương trình crash với lỗi Segmentation fault. Luôn khởi tạo con trỏ, và kiểm tra if (p != nullptr) trước khi dùng con trỏ có thể rỗng.
Quan hệ giữa mảng và con trỏ trong C++
Tên mảng, khi dùng trong biểu thức, tự chuyển thành địa chỉ của phần tử đầu tiên. Vì vậy có thể gán thẳng mảng cho con trỏ:
#include <iostream>
int main()
{
int soLuong[4] = {3, 10, 7, 2};
int* p = soLuong; // ten mang chuyen thanh dia chi phan tu dau
std::cout << "*p = " << *p << '\n';
std::cout << "*(p + 1) = " << *(p + 1) << '\n';
std::cout << "p[2] = " << p[2] << '\n';
std::cout << "p = " << p << '\n';
std::cout << "p + 1 = " << p + 1 << " (tang " << sizeof(int) << " byte)\n";
return 0;
}
p + 1 không tăng địa chỉ lên 1 byte, mà tăng đúng bằng kích thước một phần tử, ở đây là 4 byte. Nhờ vậy *(p + 1) chính là phần tử tiếp theo, và p[2] viết gọn của *(p + 2). Phép toán này gọi là số học con trỏ (pointer arithmetic).
Truyền mảng vào hàm
Đây là chỗ mảng và con trỏ gây lỗi nhiều nhất. Viết hàm tính tổng số sách trong mảng:
#include <iostream>
int tinhTong(int mang[])
{
int soPhanTu = sizeof(mang) / sizeof(mang[0]); // sai!
int tong = 0;
for (int i = 0; i < soPhanTu; i++)
tong += mang[i];
return tong;
}
int main()
{
int soLuong[5] = {3, 10, 7, 2, 5};
std::cout << "Tong: " << tinhTong(soLuong) << '\n';
return 0;
}
Tổng đúng phải là 27, nhưng chương trình in ra 13. Khi truyền vào hàm, mảng phân rã (decay) thành con trỏ tới phần tử đầu, nên int mang[] thực chất là int* mang. sizeof(mang) giờ là kích thước một con trỏ (8 byte trên máy 64-bit), chia 4 ra 2, và hàm chỉ cộng 2 phần tử đầu: 3 + 10 = 13.
Cách sửa của C là truyền thêm số phần tử:
#include <iostream>
int tinhTong(const int* mang, int soPhanTu)
{
int tong = 0;
for (int i = 0; i < soPhanTu; i++)
tong += mang[i];
return tong;
}
int main()
{
int soLuong[5] = {3, 10, 7, 2, 5};
std::cout << "Tong: " << tinhTong(soLuong, 5) << '\n';
return 0;
}
const int* báo rằng hàm chỉ đọc mảng, không sửa. Cách này chạy đúng, nhưng phải luôn truyền kèm số phần tử, truyền sai là lỗi. Mục std::array bên dưới giải quyết gọn hơn.
Truy cập vượt ngoài mảng
C++ không kiểm tra chỉ số mảng. Viết i <= 5 thay vì i < 5 là đọc lố sang ô nhớ không thuộc mảng:
#include <iostream>
int main()
{
int soLuong[5] = {3, 10, 7, 2, 5};
int tong = 0;
for (int i = 0; i <= 5; i++) // sai: i chay toi 5
tong += soLuong[i];
std::cout << "Tong: " << tong << '\n';
return 0;
}
Lần chạy đầu in ra 27, trông như đúng. Thực ra soLuong[5] đọc phải một ô nhớ nằm ngoài mảng, lần này tình cờ chứa 0. Đây là undefined behavior: chạy đúng trên máy này, sai trên máy khác, hoặc ghi đè lên biến khác nếu là lệnh gán.
Cách bắt lỗi này là biên dịch thêm cờ -fsanitize=address (AddressSanitizer). Chương trình sẽ dừng ngay lúc truy cập sai và chỉ đúng dòng kho.cpp:9.
Lưu ý: Khi chương trình có mảng, con trỏ mà kết quả lúc đúng lúc sai, hãy biên dịch lại với -g -fsanitize=address để chạy thử. Cờ này làm chương trình chậm hơn nên chỉ dùng lúc tìm lỗi, không dùng cho bản chính thức.
std::array: mảng kiểu C++
std::array trong thư viện <array> là mảng có kích thước cố định giống mảng thô, nhưng khắc phục được các nhược điểm vừa gặp:
#include <iostream>
#include <array>
int tinhTong(const std::array<int, 5>& mang)
{
int tong = 0;
for (int x : mang)
tong += x;
return tong;
}
int main()
{
std::array<int, 5> soLuong = {3, 10, 7, 2, 5};
std::cout << "So phan tu: " << soLuong.size() << '\n';
std::cout << "Tong: " << tinhTong(soLuong) << '\n';
std::cout << "at(2) = " << soLuong.at(2) << '\n';
std::cout << "at(7) = " << soLuong.at(7) << '\n';
return 0;
}
- Biết số phần tử bằng
.size(), không cần mẹosizeof. - Truyền vào hàm bằng tham chiếu
const&như bài về hàm, không bị phân rã thành con trỏ. .at(i)kiểm tra chỉ số: vượt mảng thì dừng chương trình với thông báoout_of_rangerõ ràng, thay vì âm thầm đọc rác. Toán tử[]vẫn dùng được và không kiểm tra, nhanh hơn một chút.
Chương trình bị dừng ở at(7) là do ngoại lệ, phần xử lý ngoại lệ chúng ta sẽ học ở chương nâng cao. Còn khi cần mảng thay đổi được số phần tử lúc chạy, các bạn dùng std::vector.
So sánh con trỏ và tham chiếu
Con trỏ int* p |
Tham chiếu int& r |
|
|---|---|---|
| Có thể rỗng | Có (nullptr) |
Không |
| Bắt buộc khởi tạo | Không (nhưng nên) | Có |
| Đổi sang biến khác | Được | Không |
| Cách dùng | Phải viết *p |
Dùng như biến thường |
Số học (p + 1) |
Có | Không |
Quy tắc chọn: dùng tham chiếu bất cứ khi nào có thể, vì an toàn và dễ đọc hơn. Chỉ dùng con trỏ khi cần giá trị “không có gì” (nullptr), cần đổi sang đối tượng khác, hoặc làm việc với bộ nhớ cấp phát động mà chúng ta sẽ học ở bài sau.
Áp dụng vào chương trình thư viện
Hàm tìm sách theo mã trả về con trỏ tới phần tử tìm được, không tìm thấy thì trả về nullptr. Đây là trường hợp tham chiếu không làm được, vì tham chiếu không thể rỗng:
#include <iostream>
// Tra ve con tro toi phan tu co ma can tim, khong co thi tra ve nullptr
int* timMaSach(int* danhSach, int soPhanTu, int maCanTim)
{
for (int i = 0; i < soPhanTu; i++)
{
if (danhSach[i] == maCanTim)
return &danhSach[i];
}
return nullptr;
}
int main()
{
int maSach[6] = {1001, 1002, 1005, 1010, 1024, 1100};
int* ketQua = timMaSach(maSach, 6, 1024);
if (ketQua != nullptr)
std::cout << "Tim thay 1024 o vi tri " << ketQua - maSach << '\n';
ketQua = timMaSach(maSach, 6, 9999);
if (ketQua == nullptr)
std::cout << "Khong co sach ma 9999\n";
return 0;
}
ketQua - maSach là hiệu 2 con trỏ, cho biết 2 địa chỉ cách nhau bao nhiêu phần tử, tức là vị trí của cuốn sách trong mảng.
Bài tập
- Nhập 5 số nguyên vào mảng, in ra số lớn nhất và vị trí của nó.
- Viết hàm
void daoNguoc(int* mang, int soPhanTu)đảo ngược thứ tự các phần tử trong mảng. - Viết lại bài 1 bằng
std::array. Thử truy cập phần tử thứ 10 bằng[]và bằng.at(), so sánh kết quả. - Cho
int a[3] = {1, 2, 3}; int* p = a + 1;. Đoán giá trị của*p,p[1]và*(p - 1), rồi chạy thử để kiểm tra.
Lời giải sẽ có trong bài tổng hợp bài tập nhé.
Kết
Mảng và con trỏ trong C++ gắn liền với nhau: mảng là dãy ô nhớ liền kề, con trỏ là biến giữ địa chỉ, và tên mảng chuyển thành con trỏ tới phần tử đầu. Hiểu điều này sẽ giải thích được 2 lỗi kinh điển là sizeof sai khi truyền mảng vào hàm và đọc vượt ngoài mảng. Trong code C++ mới, các bạn nên ưu tiên std::array, tham chiếu, và chỉ dùng con trỏ thô khi thật sự cần.
Muốn tra cứu chi tiết về mảng, con trỏ và std::array, các bạn xem thêm tài liệu chuẩn trên cppreference.
Phần nền tảng về mảng và con trỏ giống hệt C, các bạn có thể đọc thêm bài mảng trong C và con trỏ trong C. Bài tiếp theo chúng ta sẽ dùng con trỏ để cấp phát bộ nhớ động với new và delete. Có chỗ nào chưa hiểu, các bạn để lại bình luận bên dưới nhé.




![Mảng và con trỏ trong C++: địa chỉ, nullptr và std::array 4 Sơ đồ mảng và con trỏ trong C++: mảng int soLuong[4] nằm liền nhau trong bộ nhớ, con trỏ p trỏ vào phần tử đầu, p + 1 cộng 4 byte](https://khuenguyencreator.com/wp-content/uploads/2026/10/mang-trong-bo-nho.png)

![Mảng và con trỏ trong C++: địa chỉ, nullptr và std::array 6 Con trỏ trỏ vào mảng, *(p + 1) và p[2] truy cập phần tử, p + 1 tăng 4 byte](https://khuenguyencreator.com/wp-content/uploads/2026/10/ket-qua-con-tro-va-mang.png)




