You are on page 1of 101

Con Trỏ

(Pointer)
Nội dung
▪ Biến tĩnh vs. Biến động
▪ Con trỏ
▪ Các phép toán trên con trỏ
▪ Con trỏ và mảng một chiều
▪ Cấp phát vùng nhớ động
▪ Con trỏ cấp 2
▪ Con trỏ và mảng nhiều chiều
▪ Mảng con trỏ
▪ Con trỏ hằng, void
▪ Con trỏ hàm (option)
Khai báo biến trong C
❖ Quy trình xử lý của trình biên dịch
▪ Dành riêng một vùng nhớ với địa chỉ duy nhất để lưu biến đó.
▪ Liên kết địa chỉ ô nhớ đó với tên biến.
▪ Khi gọi tên biến, nó sẽ truy xuất tự động đến ô nhớ đã liên kết với tên biến

Ví dụ: int a = 5; // Giả sử địa chỉ lưu trữ biến a là 0x6


0x2 0x6 0xA

… 5

int B[3]; // Giả sử địa chỉ lưu trữ biến mảng B là 0x2
0x2 0x6 0xA 0xE


Biến tĩnh vs. Biến động
▪ Một số hạn chế có thể gặp phải khi sử dụng các biến tĩnh:
o Cấp phát ô nhớ dư, gây ra lãng phí ô nhớ.
o Cấp phát ô nhớ thiếu, chương trình thực thi bị lỗi.
▪ Biến động:
o Khi chạy chương trình, kích thước của biến, vùng nhớ và địa chỉ vùng nhớ được cấp phát cho
biến có thể thay đổi.
o Sau khi sử dụng xong có thể giải phóng để tiết kiệm chỗ trong bộ nhớ.

▪ Biến (variable): Biến là một ô nhớ đơn lẻ hoặc một vùng nhớ được hệ điều hành cấp
phát cho chương trình C++ nhằm để lưu trữ giá trị vào bên trong vùng nhớ đó.
Ví dụ: int m; // một vùng nhớ có kích thước 4 bytes sẽ được cấp phát.
Virtual memory & Physical memory
❖ Virtual memory & Physical
memory
▪ Chúng ta chỉ có thể trỏ đến
vùng nhớ ảo (virtual
memory) trên máy tính, còn
việc truy xuất đến bộ nhớ vật
lý (physical memory) từ bộ
nhớ ảo phải được thực hiện
bởi thiết bị phần cứng có tên
là Memory management unit
(MMU) và một chương trình
định vị địa chỉ bộ nhớ gọi
là Virtual address space .
Variable address & address-of operator
▪ Địa chỉ của biến (variable address) mà chúng ta nhìn thấy thật ra chỉ là những giá
trị đã được đánh số thứ tự đặt trên Virtual memory.
▪ Để lấy được địa chỉ ảo của biến trong chương trình, chúng ta sử dụng toán tử lấy
địa chỉ (address-of operator) ‘&’ đặt trước tên biến.

Ví dụ:
int x = 5;
cout << x; // print the value of variable x
cout << &x; // print the memory address of variable x
Reference
▪ Tham chiếu (Reference): Mục đích của tham chiếu trong C++ là tạo ra một biến
khác có cùng kiểu dữ liệu nhưng sử dụng chung vùng nhớ với biến được tham
chiếu đến.
▪ Ví dụ:

Lưu ý: address-of operator và


reference dùng chung 1 ký hiệu
&

Như vậy, mọi hành vi thay đổi giá trị của i_ref đều tác động trực tiếp đến i1.
Lưu ý: Biến tham chiếu sẽ có địa chỉ cố định sau khi khởi tạo. Chúng ta không thể tham chiếu lại lần nữa.
Dereference operator
▪ Toán tử trỏ đến (dereference operator) hay còn gọi là indirection operator (toán tử
điều hành gián tiếp) được kí hiệu bằng dấu sao "*" cho phép chúng ta lấy ra giá trị
của vùng nhớ có địa chỉ cụ thể.
▪ Ví dụ:
Dereference operator
▪ Ngoài việc truy xuất giá trị trong vùng nhớ của một địa chỉ cụ thể, toán tử trỏ đến
(dereference operator) còn có thể dùng để thay đổi giá trị bên trong vùng nhớ đó.
Dereference operator
▪ Toán tử trỏ đến cho phép chúng ta thao tác trực tiếp trên Virtual memory mà không
cần thông qua định danh (tên biến).
▪ Khác với tham chiếu (reference), toán tử trỏ đến (dereference operator) không tạo
ra một tên biến khác, mà nó truy xuất trực tiếp đến vùng nhớ có địa chỉ cụ thể
trên Virtual memory.
Con trỏ (Pointer)
▪ Địa chỉ của biến là một con số, có thể tạo biến khác để lưu địa chỉ của biến này.
▪ Con trỏ (pointer) là một biến được dùng để lưu trữ địa chỉ của biến khác.
▪ Khác với tham chiếu, con trỏ là một biến có địa chỉ độc lập so với vùng nhớ mà nó trỏ đến,
nhưng giá trị bên trong vùng nhớ của con trỏ chính là địa chỉ của biến (hoặc địa chỉ ảo) mà nó
trỏ tới.

❖ Kiểu con trỏ cho phép:


o Truyền tham số kiểu địa chỉ
o Biểu diễn các kiểu, cấu trúc dữ liệu động
o Lưu trữ dữ liệu trong vùng nhớ heap
Khai báo biến con trỏ
▪ Khai báo con trỏ trong C/C++: Kiểu con trỏ phải được định nghĩa trên một kiểu cơ
sở đã được định nghĩa trước đó (theo kiểu dữ liệu của biến mà con trỏ trỏ tới)

<kiểu_cơ_sở> *<Tên_con_trỏ>;
▪ Ví dụ:
int x; // x là kiểu int
int *px; // px là con trỏ đến vùng nhớ kiểu int
int *p1, *p2; // // p1 và p2 là con trỏ đến vùng nhớ kiểu int
float *pf; // pf là con trỏ đến vùng nhớ kiểu float
double *pd; // pd là con trỏ đến vùng nhớ kiểu double
char c, d, *pc; // c và d kiểu char ; pc là con trỏ đến vùng nhớ kiểu char
Lưu ý: Dấu sao “*” trong con trỏ không phải là toán tử trỏ đến (dereference
operator) , nó là cú pháp được ngôn ngữ C/C++ qui định
Con trỏ (Pointer)
▪ Lưu ý: Kiểu dữ liệu của con trỏ không mô tả giá trị địa chỉ được lưu trữ bên trong
con trỏ, mà kiểu dữ liệu của con trỏ dùng để xác định kiểu dữ liệu của biến mà nó
trỏ đến trên bộ nhớ ảo.
Con trỏ và toán tử lấy địa chỉ
❖ Con trỏ và toán tử lấy địa chỉ & (address-of operator)
▪ “&”: toán tử lấy địa chỉ của 1 biến
▪ Địa chỉ của tất cả các biến trong chương trình đều đã được chỉ định từ khi khai báo
▪ Gán giá trị cho con trỏ: Giá trị mà biến con trỏ lưu trữ là địa chỉ của biến khác có
cùng kiểu dữ liệu với biến con trỏ.

Do đó, chúng ta cần sử dụng address-of operator để lấy ra địa chỉ ảo của biến rồi mới
gán cho con trỏ được. Lúc này, biến ptr sẽ lưu trữ địa chỉ ảo của biến value.
Con trỏ và toán tử lấy địa chỉ
▪ Ví dụ:
int a = 5;
int *p; // khai báo biến con trỏ p
p = &a; // gán địa chỉ của biến a cho p ( hay p trỏ tới a)
int *q = &a ; // khởi tạo giá trị cho con trỏ q trỏ tới vùng nhớ của a
Con trỏ (Pointer)
▪ Ví dụ:
Con trỏ (Pointer)
▪ Ví dụ:

Lý do mà chúng ta gán được địa chỉ của biến value cho con trỏ
kiểu int (int *) là vì address-of operator của một biến
kiểu int trả về giá trị kiểu con trỏ kiểu int (int *).
Con trỏ và toán tử trỏ đến
❖ Con trỏ và toán tử trỏ đến *
(dereference operator) #include <stdio.h>
▪ “*”: toán tử truy xuất giá trị của #include <iostream>
using namespace std;
vùng nhớ được quản lý bởi con trỏ.
char g = 'z';
int main()
{
char c = 'a';
char *p;
p = &c;
printf("%c\n", *p);
// hoac dung cout
cout<< *p <<endl;
p = &g;
cout<< *p <<endl;
return 0;
}
Ví dụ:
int a = 5; // giả sử biến a được lưu trữ tại địa chỉ 0xB
int *p; // giả sử biến con trỏ p được lưu trữ tại địa chỉ 0xF
p = &a;
Câu hỏi: kết quả của các đoạn code sau là gì?
cout << a;
cout<< &a;
cout<< p;
cout<<*p;
cout<<&p;
Con trỏ và toán tử gán
❖ Con trỏ và toán tử gán “=”
▪ Khi có hai con trỏ cùng kiểu thì chúng ta có thể gán trực tiếp mà không cần sử
dụng toán tử lấy địa chỉ address-of operator.
Con trỏ và toán tử gán
▪ Ví dụ : Có sự khác biệt rất quan trọng khi thực hiện các phép gán:

int i = 10, j = 14;


int* p = &i;
int *q = &j;
*p = *q;
Lưu ý: Khác với tham chiếu (reference), một con
trỏ có thể trỏ đến địa chỉ khác trong bộ nhớ ảo
sau khi đã được gán giá trị.

int i = 10, j = 14; Tham chiếu (reference) không thể thay đổi địa chỉ
int *p = &i; sau lần tham chiếu đầu tiên.
int *q = &j;
p = q;
Địa chỉ của biến mảng
▪ Ví dụ :
Lưu ý
❖ Khởi tạo kiểu con trỏ:
▪ Khi mới khai báo, biến con trỏ được đặt ở địa chỉ nào đó (không biết trước).
➔ chứa giá trị không xác định
➔ trỏ đến vùng nhớ không biết trước.

▪ Con trỏ trong ngôn ngữ C/C++ vốn không an toàn. Nếu sử dụng con trỏ không
hợp lý có thể gây lỗi chương trình.
Lưu ý
❖ Con trỏ chưa được gán địa chỉ
▪ Biến con trỏ có thể không cần khởi tạo giá trị ngay khi khai báo. Nhưng thực hiện
truy xuất giá trị của con trỏ bằng dereference operator khi chưa gán địa chỉ cụ thể
cho con trỏ, chương trình có thể bị đóng bởi hệ điều hành
Con trỏ NULL
▪ Do đó, khi khai báo con trỏ nhưng chưa có địa chỉ khởi tạo cụ thể, chúng ta nên gán
cho con trỏ giá trị NULL (giá trị NULL trong thư viên <stdlib.h>).
▪ Giá trị đặc biệt NULL để chỉ rằng con trỏ không quản lý vùng nào. Giá trị này
thường được dùng để chỉ một con trỏ không hợp lệ.
#include <stdlib.h>
#include <iostream>
using namespace std;

int main()
{
int *p = NULL;
if (p == NULL)
cout <<"con tro khong hop le" << endl;
else
cout << *p;
return 0;
}
Ví dụ
▪ Câu hỏi : Kiểm tra kết quả dòng lệnh sau

#include <iostream>
using namespace std;
int main()
{
int a = 10;
int *p = &a;
*p = 5;
cout << a << endl;
cout << *p << endl;
return 0;
}
Ví dụ #include <iostream>
using namespace std;
int main()
▪ Câu hỏi: cho biết kết quả chương trình sau {
int i = 10, j = 14, k;
int *p = &i;
int *q = &j;
Biến Giá trị *p += 1;
i p = &k;
j
*p = *q;
p = q;
k
*p = *q;
p cout << "i = " << i << ":" << &i <<endl;
q cout << "j = " << j << ":" << &j <<endl;
cout << "k = " << k << ":" << &k <<endl;
cout << "*p = " << *p << ":" << p <<endl;
cout << "*q = " << *q << ":" << q <<endl;
return 0;
}
Ví dụ
▪ Câu hỏi: cho biết kết quả sau

int n = 50;
int *p;
p = &n ;
cout << n <<endl;
cout << &n <<endl;
cout << p <<endl;
cout << *p <<endl;
cout << *(&n) << endl;
cout << &p <<endl;
Truyền đối số cho hàm:
▪ Các cách truyền đối số cho hàm:
o Truyền bằng tham trị
o Truyền bằng tham chiếu
o Truyền bằng địa chỉ

void hoanvi_1(int x, int y) void hoanvi_2(int &x, int &y)


{ {
int t = x; x = y; y = t; int t = x; x = y; y = t;
} }

void main() void main()


{ {
int a = 1; b = 2; int a = 1; b = 2;
hoanvi_1(a, b); hoanvi_2(a, b);
cout<<a << b; cout<<a << b;
} }
Con trỏ (Pointer)
❖ Con trỏ - Truyền tham số địa chỉ (cho hàm)
#include <iostream> void change_1(int *v)
using namespace std; {
void change_2(int &); // dung tham chieu (*v) += 5;
void change_1(int *); // con tro truyen tham so dia chi cout<< "change 1: var = "<< (*v) << endl;
}
int main()
{
int var1 = 5;
int var2 = 5; void change_2(int &v)
change_1(&var1); // dung con tro truyen tham so dia chi {
cout << "main: change_1: var = " << var1 <<endl; v += 5;
change_2(var2); // dung tham chieu cout<< "change 2: var = "<< v << endl;
cout << "main: change_1: var = " << var2 <<endl; }
return 0;
}
Truyền bằng địa chỉ
▪ Ví dụ:

void hoanvi_3(int *x, int *y)


{
int t = *x; *x = *y; *y = t;
}

void main()
{
int a = 1; b = 2;
hoanvi_1(&a, &b);
cout<<a << b;
}
#include <iostream>
Phép toán trên con trỏ using namespace std;
int main()
{
❖ Con trỏ và toán tử “+, - ” với số nguyên int var = 5;
▪ Ta có thể cộng (+), trừ (-) con trỏ với 1 số int *p = &var;
nguyên N nào đó; kết quả trả về là 1 con int *q = p+2;
trỏ. cout <<"p = " << p << endl;
❑ Lưu ý : cout <<"p+1 = " << p+1 << endl;
▪ Con trỏ p có kiểu int, nên khoảng cách cout <<"q = " << q << endl;
giữa p và p + 1 là 4 bytes cout <<"p+2 = " << p+2 << endl;
cout <<"p+3 = " << p+3 << endl;
cout <<"q - p = " << q-p << endl;
❑ Lưu ý: return 0;
▪ Ta không thể cộng 2 con trỏ với nhau; }
▪ Phép trừ 2 con trỏ cùng kiểu sẽ trả về 1
giá trị nguyên (int). Đây chính là khoảng
cách (số phần tử) giữa 2 con trỏ đó.
Con trỏ và Mảng 1 chiều
▪ Biến kiểu mảng là địa chỉ tĩnh của một vùng nhớ, được xác định khi khai báo,
không thay đổi trong suốt chu kỳ sống.

▪ Biến con trỏ là địa chỉ động của một vùng nhớ, được xác định qua phép gán địa chỉ
khi chương trình thực thi.
Con trỏ và Mảng 1 chiều
❖ Mảng 1 chiều: int arr[10];
▪ Tên mảng arr là một hằng con trỏ
➔ không thể thay đổi giá trị của hằng này.
▪ arr là địa chỉ đầu tiên của mảng
➔ arr  &arr[0]
Con trỏ và Mảng 1 chiều
▪ Địa chỉ của int arr[] = { 32, 13, 66, 11, 22 };
mảng một chiều
và các phần tử //show address of arr in virtual memory
trong mảng một cout << &arr << endl;
chiều //show address of the first element of arr
Ví dụ: cout << &arr[0] << endl;
cout << arr << endl;
▪ Các cách viết sau đây // address
là tương đương cout << arr << endl;
cout << arr + 1 << endl;
o arr  &arr[0]  &arr cout << arr + 2 << endl;
//value
o arr+i  &arr[i] cout << *(arr) << endl;
cout << *(arr + 1) << endl;
o *(arr+i)  arr[i] cout << *(arr + 2) << endl;
Con trỏ và Mảng 1 chiều
int arr[100], *ptr;
▪ Con trỏ trỏ đến mảng một chiều
ptr = arr; // Cách 1
Ví dụ:
ptr = &arr[0]; // Cách 2

int arr[] = { 3, 5, 65, 23, 11 };


int *ptr = &arr[2]; //ptr point to the 3rd element
cout << *ptr << endl;
cout << *(ptr - 1) << endl; //access the second element of arr
cout << *(ptr + 2) << endl; //access the last element of arr

// sai : cout << *(arr-1) ; không dùng phép toán “-” cho biến kiểu mảng
• Lưu ý: Truy xuất đến phần tử thứ n của mảng (không sử dụng biến mảng)
int arr[100], *ptr;
ptr = arr;
arr[n] == p[n] == *(p + n) // 03 cách viết là tương đương
Con trỏ và Mảng 1 chiều
▪ Con trỏ trỏ đến mảng một chiều

int arr[] = { 3, 5, 65, 23, 11 };


int *ptr = arr; //ptr point to &arr[0] // arr = ptr : SAI
for (int i = 0; i < 5; i++)
{
cout << *(ptr + i) << " ";

cout << *(arr + i) << " ";

cout << ptr[i] << " ";

cout << arr[i] << " ";


}
Lưu ý:
▪ Mảng một chiều truyền cho hàm là địa chỉ của phần tử đầu tiên chứ không phải
toàn mảng.
Con trỏ cấu trúc
▪ Con trỏ có thể dùng để trỏ tới một kiểu dữ liệu kiểu cấu trúc (struct)
▪ Để truy xuất các thành phần trong cấu trúc thông qua con trỏ có 02 cách:
<tên biến con trỏ cấu trúc>-><tên thành phần>

(*<tên biến con trỏ cấu trúc>).<tên thành phần>


▪ Ví dụ: typedef struct PHANSO
{
int tu, mau;
};
PHANSO ps1, *pt = &p1; // pt là con trỏ

ps1.tu = 1; ps1.mau = 2;
pt->tu = 1; pt->mau = 2;
(*pt).tu = 1; (*pt).mau = 2;
Câu hỏi
1. Chương trình sau có lỗi hay không?

#include<iostream>
using namespace std;
int main()
{
int *x, y = 10;
*x = y;
*x += y++;
cout <<*x <<endl << y;
return 0;
}
Cấp phát bộ nhớ
❖ Vùng nhớ heap và vùng nhớ stack
▪ Vùng nhớ stack:
o Khai báo các biến, mảng hay các tham số trong
hàm, kể cả hàm main (gọi là biến cục bộ) ->
được lưu trong vùng nhớ stack.
o Khi kết thúc hàm, vùng nhớ của biến này sẽ tự
động bị thu hồi.
o Kích thước vùng nhớ stack khá hạn chế, giả sử
bạn muốn khai báo một mảng có kích thước 1
triệu phần tử hay lớn hơn, chương trình có thể
sẽ bị crash tuỳ theo dung lượng RAM mà bạn
có.

char ch_array_1[1024 * 1000];


char ch_array_2[1024 * 10000]; // tràn vùng nhớ Stack báo lỗi
Cấp phát bộ nhớ
❖ Vùng nhớ heap và vùng nhớ stack
▪ Vùng nhớ heap:
o Các biến, mảng được khai báo bằng kỹ thuật cấp phát động được lưu trong vùng
nhớ heap.
o Sau khi kết thúc hàm, vùng nhớ của các biến này không bị thu hồi. Và người lập
trình phải có nhiệm vụ thu hồi vùng nhớ của biến khi không còn sử dụng. Nếu quên
sẽ gây ra hiện tượng “rò rỉ” bộ nhớ (memory-leaking).
o Kích thước vùng nhớ heap tương đối thoải mái, có thể khai báo được mảng mới kích
thước lớn. (tuy nhiên vẫn phụ thuộc vào dung lượng của RAM)
Cấp phát bộ nhớ
❖ Cấp phát bộ nhớ tĩnh vs. cấp phát bộ nhớ động
▪ Cấp phát bộ nhớ tĩnh: được áp dụng cho biến static và biến toàn cục.
o Vùng nhớ của các biến này được cấp phát ngay khi chạy chương trình.
o Kích thước của vùng nhớ được cấp phát phải được cung cấp tại thời điểm biên dịch
chương trình.
o Đối với việc khai báo mảng một chiều, đây là lý do tại sao số lượng phần tử là hằng số.

Ví dụ : int i=10;
double Arr[1000]; // có thể dẫn đến dư thừa khi không dùng hết 1000 ô nhớ hoặc
không thể mổ rộng khi muốn dùng hơn 1000 phần tử
Cấp phát bộ nhớ
❖ Cấp phát bộ nhớ tĩnh vs. cấp phát bộ nhớ động
▪ Cấp phát bộ nhớ động: được sử dụng để cấp phát vùng nhớ cho các biến cục bộ,
tham số của hàm.
o Bộ nhớ được cấp phát tại thời điểm chương trình đang chạy, khi chương trình đi vào một
khối lệnh.
o Các vùng nhớ được cấp phát sẽ được thu hồi khi chương trình đi ra khỏi một khối lệnh.
o Kích thước vùng cần cấp phát cũng phải được cung cấp rõ ràng.
Cấp phát bộ nhớ động
▪ Có thể chỉ định vùng mới cho 1 con trỏ quản lý bằng các lệnh hàm malloc, calloc
(của C) hoặc toán tử new (của C++)
▪ Vùng nhớ do lập trình viên chỉ định phải được giải phóng bằng hàm free (của C)
hoặc toán tử delete (của C++))

Ví dụ: int *i = new int;


double *Arr = new double[n]; // nhập vào giá trị n cụ thể
Cấp phát bộ nhớ động
▪ Hàm malloc (memory allocation) và calloc (contiguous allocation) cho phép cấp phát
các vùng nhớ ngay trong lúc chạy chương trình.
Cấp phát bộ nhớ động
▪ Toán tử sizeof: nhận một tham số là bất kỳ kiểu dữ liệu nào và trả về kích cỡ của
kiểu dữ liệu đó.
▪ Ví dụ:

cout<< sizeof(char); // 1
cout<< sizeof(int); // 4 trả về kích cỡ của các kiểu dữ liệu trong C
cout<< sizeof(float); // 4
cout<< sizeof(double); // 8

cout<< sizeof(char*); // 4
cout<< sizeof(int*); // 4
trả về kích cỡ của các con trỏ tới các kiểu dữ liệu khác nhau
cout<< sizeof(float*); // 4
cout<< sizeof(double*); // 4
Cấp phát bộ nhớ động
▪ malloc: Hàm malloc() thực hiện cấp phát bộ nhớ bằng cách chỉ định số byte cần cấp
phát.
Hàm này trả về con trỏ kiểu void cho phép chúng ta có thể ép kiểu về bất cứ
kiểu dữ liệu nào (trả về NULL nếu không đủ bộ nhớ)
void* malloc (size_t size);
Ví dụ:
int *a = (int *) malloc(sizeof( int )); // cấp phát 4 bytes
int *arr = (int *) malloc( 10 * sizeof( int )); // cấp phát 40 bytes
float *b = (float *) malloc(100*sizeof(float)); // cấp phát 400 bytes

Ép kiểu dữ liệu cho con trỏ


Cấp phát bộ nhớ động
▪ calloc: hàm calloc() thực hiện cấp phát bộ nhớ gồm num phần tử và khởi tạo tất cả
các ô nhớ có giá trị bằng 0 (trả về NULL nếu không đủ bộ nhớ).
void* calloc (size_t num, size_t size);
Ví dụ:
int *b = (int *) calloc(10, sizeof( int ));
int *a = (int *) calloc(1, sizeof( int ));
char *s = (char *) calloc(100, sizeof(char ));
Cấp phát bộ nhớ động
▪ Giải phóng vùng nhớ: dùng hàm free() trong C.
▪ Khi thoát khỏi hàm, các biến khai báo trong hàm sẽ “biến mất”. Tuy nhiên các vùng
nhớ được cấp phát động vẫn còn tồn tại và được “đánh dấu” là đang “được dùng”
→ bộ nhớ của máy tính sẽ hết.
▪ Cú pháp : free(ptr); // ptr là con trỏ
Cấp phát bộ nhớ động
▪ Ví dụ: dùng malloc() or calloc() và free() nhập vào một mảng n số nguyên , in ra tổng
các phần tử trong mảng
#include <iostream>
// thu vien dung cap phat dong
#include <stdlib.h>
using namespace std;
int main() // …
{ cout << "Nhap cac gia tri: ";
int n, i, *ptr, sum = 0; for (i = 0; i < n; ++i)
cout<<"Nhap so luong phan tu:" ; {
cin >> n; cin >> *(ptr + i); // hoac dung cin >> ptr[i];
ptr = (int *)malloc(n * sizeof(int)); // cap phat dong sum += *(ptr + i); // hoac dung sum += ptr[i];
// ptr = (int *)calloc(n, sizeof(int)); // dung calloc }
// check if cap phat ko thanh cong cout<<"Tong = " << sum ;
if (ptr == NULL)
{ // thu hoi vung nho
printf("Co loi! khong the cap phat bo nho."); free(ptr);
return 0; return 0;
} }
// ….
Cấp phát bộ nhớ động
▪ Mở rộng vùng nhớ dùng realloc() #include <iostream>
#include <stdlib.h>
▪ Nếu việc cấp phát bộ nhớ động không using namespace std;
đủ hoặc cần nhiều hơn mức đã cấp int main()
phát, bạn có thể thay đổi kích thước {
int *ptr, i , n1, n2;
của bộ nhớ đã được cấp phát trước đó cin >> n1; // nhap n1
bằng cách sử dụng hàm. ptr = (int*) malloc(n1 * sizeof(int));
▪ Cú pháp : cout << "Dia chi cua vung nho vua cap phat:" << ptr;
cout << "\nNhap lai so luong phan tu: " ;
void *realloc(void *block, size_t size); cin >> n2;
// cap nhat lai vung nho
Cấp phát lại vùng nhớ có kích thước size do block trỏ ptr = (int*) realloc(ptr, n2 * sizeof(int));
đến trong vùng nhớ HEAP. cout << "Dia chi cua vung nho vua cap phat:" << ptr;
block == NULL ➔ sử dụng malloc // thu hoi vung nho
size == 0 ➔ sử dụng free free(ptr);
(trả về NULL nếu không đủ bộ nhớ). return 0;
}
Cấp phát bộ nhớ động
▪ Cấp phát động trong C++
o Cấp phát bộ nhớ động dùng toán tử new (Toán tử new được dùng để xin cấp phát vùng nhớ trên
phân vùng Heap của bộ nhớ ảo) (trả về NULL nếu không đủ bộ nhớ).
Cú pháp : một vùng nhớ
<data_type> *tencontro = new <data_type> ;
dãy vùng nhớ liên tục nhau (mảng một chiều)
<data_type> *tencontro = new <data_type> [num_of _elements];
Ví dụ:
int *ptr = new int; // dynamically allocate an integer
double *p_double = new double;
int *p_arr = new int[10]; // cho mảng 10 phần tử
char *c_str = new char [100]; // cho mảng 100 phần tử
❖ Điều khiến cho kỹ thuật Dynamic memory allocation khác với Static memory allocation là số lượng
phần tử có thể được cung cấp trong khi chương trình đang chạy. Ví dụ:
int num_of_elements;
cout << "Enter number of elements you want to create: ";
cin >> num_of_elements;
int *p_arr = new int[num_of_elements];
Cấp phát bộ nhớ động
▪ Cấp phát động trong C++
o Giải phóng bộ nhớ dùng toán tử delete
• Khi không muốn sử dụng tiếp vùng nhớ đã được cấp phát cho chương trình
trên Heap, chúng ta nên trả lại vùng nhớ đó cho hệ điều hành.
• Thật ra khi chương trình kết thúc, tất cả vùng nhớ của chương trình đều bị hệ điều
hành thu hồi, nhưng chúng ta nên giải phóng vùng nhớ không cần thiết càng sớm
càng tốt.
• Để xóa một vùng nhớ, chúng ta cần có một địa chỉ cụ thể, địa chỉ đó được giữ bởi
con trỏ sau khi gán địa chỉ cấp phát cho nó:
int *p = new int;
delete p;
• Đối với dãy vùng nhớ liên tục được cấp phát trên Heap, chúng ta cần thêm vào toán
tử [ ] để báo với hệ điều hành rằng vùng nhớ đã được cấp phát không dùng cho một
biến đơn. int *p_arr = new int[10];
delete[] p_arr;
Cấp phát bộ nhớ động
▪ Lưu ý:

Cấp phát bộ nhớ tĩnh Cấp phát bộ nhớ động

int Arr [100]; // khai báo mảng Arr


Nhập vào n và các phân tử của mảng Nhập vào n và các phân tử của mảng
int *Arr = new int[n];
-Không thề nhập n > 100 - Hạn chế sự lãng phí tài nguyên
-Nếu n < 100 - > dư thừa tài nguyên - Có thể mở rộng vùng nhớ khi thực thi chương
trình.

int Arr[n]; // Wrong


Cấp phát bộ nhớ động
Trong C Trong C++
▪ Tóm tắt
• Cấp phát động • Cấp phát động

- Hàm malloc -Toán tử new


int *a = (int *) malloc( N* sizeof( int )); int *p_int = new int;
double *Arr = new double[N];
-Hàm calloc (gán giá trị cho tất cả các phần tử của
vùng nhớ vừa cấp phát = 0)
int *ptr= (int *) calloc(N, sizeof(int));

- Hàm realloc (để thay đổi kích thước bộ nhớ đã


cấp phát với ptr là địa chỉ của bộ nhớ đã cấp phát)
ptr = (int *) realloc (ptr, M* sizeof(int)); •Thu hồi vùng nhớ
// ex : M > N -Toán tử delete
•Thu hồi vùng nhớ delete p_int;
-Hàm free delete [] Arr; // thêm toán tử [] để
free (ptr); báo với hệ điều hành rằng vùng
nhớ đã được cấp phát không dùng
cho một biến đơn.
Lưu ý
▪ Không cần kiểm tra con trỏ có NULL hay kô trước khi free hoặc delete.

▪ Cấp phát bằng malloc, calloc hay realloc thì giải phóng bằng free, cấp phát bằng
new thì giải phóng bằng delete.

▪ Cấp phát bằng new thì giải phóng bằng delete, cấp phát mảng bằng new [] thì giải
phóng bằng delete [].
Con trỏ và cấp phát động
▪ Lưu ý: có thể dùng con trỏ và cấp phát bộ nhớ động để biểu diễn cho mảng
▪ Làm việc trên con trỏ và cấp phát bộ nhớ động phải tuân thủ các qui tắc sau:
1. Khai báo con trỏ int *p;
2. Cấp phát bộ nhớ động cho con trỏ p = new int[10];
3. Kiểm tra xem cấp phát vùng nhớ thành công không? if (p == NULL) // lỗi
vùng nhớ
4. Thao tác theo yêu cầu // p!=NULL // cấp phát thành công, ví dụ p[0] = 100;
5. Giải phóng vùng nhớ delete[] p;
Con trỏ cấp 2
▪ Pointer to pointer ("Con trỏ trỏ đến con trỏ") là một loại con trỏ dùng để lưu trữ địa
chỉ của biến con trỏ.
▪ Chúng ta cần xem kiểu dữ liệu khi sử dụng toán tử address-of cho con trỏ sẽ trả về
giá trị kiểu gì.

int *ptr = NULL;


cout << typeid(&ptr).name() << endl; // int * *

▪ Khai báo : <data _type> **< pointer_to_pointer _name>;


Ví dụ: int **p2pi ; float **p2pf;

int *ptr = NULL;


int **p_to_p = &ptr;
Pointer to pointer int **p_to_p = &&value; //not valid
▪ Xét ví dụ:
int main()
{
int value = 100;
int *ptr = &value;
int **p_to_p = &ptr;
cout << p_to_p << endl; //print address of ptr
cout << *p_to_p << endl; //print address which hold by ptr
cout << **p_to_p << endl; //print value at address which hold by ptr
return 0;
}

p_to_p ptr value


p_to_p; //là &ptr
Address 3C 2B 1A
*p_to_p; //là ptr
Value 2B 1A 100
*(*p_to_p); //là *ptr
Pointer to pointer

Con trỏ cấp 2
▪ Con trỏ là tham số cho hàm.
o Làm sao thay đổi giá trị của con trỏ (không phải giá trị mà nó trỏ đến) sau khi gọi hàm?
▪ Xét ví dụ: viết hàm nhập và xuất mảng dùng con trỏ và cấp phát động

#include <iostream> void nhapmang(int *a, int &n)


#include <stdlib.h> { cin >> n;
using namespace std; a = new int[n];
void nhapmang(int *a, int &n); for (int i=0;i<n;i++)
void xuatmang(int *a, int n); cin >> a[i];
int main() }
{ void xuatmang(int *a, int n)
int *A,n; {
nhapmang(A,n); // A van la NULL, in ra loi for (int i=0;i<n;i++)
// sau khi ra khoi ham gia tri A ko thay doi cout << a[i] << " ";
xuatmang(A,n); }
delete[] A;
return 0; Nhận xét: biến con trỏ *A trong hàm main và biến con trỏ *a
} trong hàm nhapmang là khác biệt nhau.
Khi ra khỏi hàm nhapmang, giá trị của con trỏ *A không được
cập nhật
Con trỏ cấp 2
❑ Khắc phục : có 2 cách
▪ Cách 1: Dùng tham chiếu int *&p (trong C++) (vì con trỏ cũng là một biến nên
có thể dùng một biến để tham chiếu đén)
Ví dụ: chỉnh sửa lại hàm nhập mảng như sau:
void nhapmang(int *&a, int &n); // prototype
#include <iostream>
#include <stdlib.h>
void nhapmang(int *&a, int &n) using namespace std;
{ cin >> n; void nhapmang(int *&a, int &n);
a = new int[n]; void xuatmang(int *a, int n);
for (int i=0;i<n;i++) int main()
cin >> a[i]; {
} int *A,n;
nhapmang(A,n); // dung tham chieu
xuatmang(A,n);
delete[] A;
return 0;
}
Con trỏ cấp 2
❑ Khắc phục : có 2 cách
▪ Cách 2: Dùng con trỏ cấp 2 ( pointer to pointer) int **a ( sử dụng con trỏ a trỏ đến địa chỉ của con
trỏ A trong hàm main() . Hàm này sẽ thay đổi giá trị của con trỏ A gián tiếp thông qua con trỏ a)
Ví dụ: chỉnh sửa lại hàm nhập mảng như sau:
void nhapmang(int **a, int &n); // prototype
#include <iostream>
#include <stdlib.h>
using namespace std;
void nhapmang(int **a,int &n) void nhapmang(int **a, int &n);
{ cin >> n; void xuatmang(int *a, int n);
*a = new int[n]; int main()
for (int i=0;i<n;i++) {
cin >> (*a)[i]; // tuong duong cin>>A[i] int *A,n;
} nhapmang(&A,n); // dùng con trỏ cấp 2
xuatmang(A,n);
delete[] A;
return 0;
}
Array of pointers
▪ Pointer to pointer có thể được dùng để quản lý mảng một chiều kiểu con trỏ (int
*[]). Ví dụ
Thông thường, chúng ta sẽ sử dụng pointer to pointer để quản
int main() lý vùng nhớ được cấp phát trên Heap cho mảng một chiều chứa
các con trỏ.
{
int *p1 = NULL; int *p1 = NULL;
int *p2 = NULL; int *p2 = NULL;
int *p3 = NULL; int *p3 = NULL;
int *p[] = { p1, p2, p3 }; int **p_to_p = new int*[3];
int **p_to_p = p; p_to_p[0] = p1;
return 0; p_to_p[1] = p2;
} p_to_p[2] = p3;
Trong trường hợp này, p_to_p[0] tương đương với p[0]. delete[] p_to_p;
Con trỏ và mảng nhiều chiều
▪ Pointer to pointer cũng được dùng để quản lý mảng hai chiều được cấp
phát trên Heap.
▪ Với mảng hai chiều cấp phát trên Stack, chúng ta chỉ cần khai báo như
sau: int arr2D[3][5];
▪ Với mảng hai chiều cấp phát trên Heap sẽ rắc rối hơn. Ví dụ: dùng con
trỏ cấp 2 biễu diễn mảng hai chiều 3x5
#include <iostream>
using namespace std;
int main() // …
{ cout << "---------------------" << endl;
▪ **pToArrPtr;
int for (int i = 0; i < 3; i++)
//Cấp phát vùng nhớ cho 3 con trỏ kiểu (int {
*) for (int j = 0; j < 5; j++)
pToArrPtr = new int*[3]; // why int* ??? {
//Mỗi con trỏ kiểu (int *) sẽ quản lý 5 phần cout << pToArrPtr[i][j] << " ";
tử kiểu int }
for (int i = 0; i < 3; i++) cout << endl;
{ }
pToArrPtr[i] = new int[5]; //Giải phóng vùng nhớ cho từng dãy vùng nhớ
} mà 3 con trỏ đang quản lý
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
{ {
for (int j = 0; j < 5; j++) delete[] pToArrPtr[i];
{ }
cin >> pToArrPtr[i][j]; //Giải phóng cho 3 biến con trỏ chịu sự
} quản lý của pToArrPtr
delete[] pToArrPtr;
} return 0;
//.. }
Câu hỏi:
▪ Viết lại ví dụ trên (nhập và xuât mảng 2 chiều) bằng cách tách 2 hàm nhapmang và
xuatmang 2 chiểu tách ra riêng biệt với hàm main()
▪ Yêu cầu: Nhập vào mảng chiều kích thước nxm (n, m nhập từ bàn phím) dùng con trỏ
cấp 2. Dùng cấp phát động và chú ý thu hồi vùng nhớ khi kết thúc chương trình
▪ Gợi ý: dùng tham chiếu cho hàm nhập mảng

void nhapmang (int **&A, int &n, int &m); //prototype


void xuatmang (int **A, int n, int m); // prototype
Pointer to pointer to pointer to ...
▪ Chúng ta có thể khai báo những con trỏ có dạng như sau:
int ***ptrX3;
Int ****ptrX4;
int ******ptrX6;
▪ Tuy nhiên, việc thao tác với những con trỏ như thế này khá phức tạp
Con trỏ và hằng
▪ Pointer to Const

compiler ngăn chặn điều này để đảm bảo an toàn dữ liệu cho vùng nhớ của biến value
Con trỏ và hằng
▪ Pointer to Const (Con trỏ dùng để trỏ đến hằng)

Điều này có nghĩa Pointer to const là loại con trỏ chỉ có chức năng đọc nội dung của
vùng nhớ (bất kể vùng nhớ đó có phải hằng hay không) chứ không có chức năng ghi giá
trị vào vùng nhớ.
Con trỏ và hằng
▪ Const pointer : là loại con trỏ chỉ gán được địa chỉ một lần khi khởi tạo, điều này có
nghĩa sau khi trỏ đến vùng nhớ nào đó thì nó không thể trỏ đi nơi khác được.
Con trỏ và hằng
▪ Const pointer to const: con trỏ này sẽ có chức năng read-only, và nó cũng không
thể trỏ đến vùng nhớ khác sau khi đã khởi tạo

*
Câu hỏi
Đoạn code nào có thể biên dịch được?
int main() int main()
{ {
char str[] = "1234"; char str[] = “1234";
char *const p_str = str; const char *p_str = str;
for(int i = 0; i < strlen(str); p_str++) for(int i = 0; i < strlen(str); i++)
{ {
*p_str = ' '; *(p_str + i) = ' ';
} }
cout << p_str << endl; cout << p_str << endl;
return 0; return 0;
} }
Con trỏ void
▪ Có thể gọi là con trỏ tổng quát, là một kiểu dữ liệu đặc biệt của con trỏ. Con trỏ
kiểu void có thể trỏ đến bất kỳ đối tượng nào (với bất kỳ kiểu dữ liệu nào) có địa chỉ
cụ thể trên bộ nhớ ảo

có thể cho con trỏ void trỏ đến những con trỏ khác
Con trỏ void
▪ Con trỏ kiểu void không xác định được kiểu dữ liệu của vùng nhớ mà nó trỏ tới, chúng ta không thể
truy xuất trực tiếp nội dung thông qua toán tử dereference (*) được
▪ Yêu cầu: cần được ép kiểu một cách rõ ràng trước khi sử dụng toán tử dereference cho vùng nhớ mà
con trỏ đang nắm giữ.

vPtr và iPtr đều trỏ vào địa chỉ của biến value,
nhưng chúng ta chỉ có thể sử dụng toán
tử dereference lên con trỏ iPtr chứ không thể sử
dụng cho con trỏ vPtr
Con trỏ hàm (option)
❖ Con trỏ hàm:
▪ Hàm cũng đuợc lưu trữ trong bộ nhớ, tức là cũng có địa chỉ.
▪ Con trỏ hàm là con trỏ trỏ đến vùng nhớ chứa hàm và có thể gọi
hàm thông qua con trỏ đó

In ra : n = 0 Kết quả: (address) 0x013D1492

Khi muốn thực thi một hàm, chúng ta cần thêm cặp dấu ngoặc để truyền đối số vào cho hàm (nếu hàm không có tham số thì để
trống). Nếu chúng ta không sử dụng cặp dấu ngoặc, sử dụng tên hàm trả về địa chỉ của hàm trên bộ nhớ ảo. Và địa chỉ này có
thể được gán con một con trỏ có kiểu dữ liệu tương ứng (function pointer).
Con trỏ hàm
▪ Cú pháp của một con trỏ hàm có nhiều điểm khác biệt so với cách
khai báo con trỏ thông thường.
<return_type> (*<name_of_func_pointer>) ( <data_type_of_parameters> );
Ví dụ:
int (*pFoo) ();
void(*pSwap) (int &, int &);
bool (*comparisonFunc)(int, int);
▪ Gán giá trị cho con trỏ hàm
<biến con trỏ hàm> = <tên hàm>;
<biến con trỏ hàm> = &<tên hàm>;
Con trỏ hàm
Ví dụ

void swapValue(int &value1, int &value2) int main()


{ {
int temp = value1; void(*pSwap) (int &, int &) = swapValue;
value1 = value2; cout << pSwap << endl;
value2 = temp; cout << swapValue << endl;
} system("pause");
return 0;
}
Con trỏ hàm
▪ Lưu ý, khi cần lấy địa chỉ của hàm, chúng ta chỉ sử dụng duy nhất tên hàm,
không đặt thêm cặp dấu ngoặc vào.
Con trỏ hàm
❖ Gọi hàm thông qua con trỏ hàm
▪ Con trỏ hàm có thể được sử dụng như hàm thông qua toán tử dereference(*). Ví
dụ:
int main()
void swapValue(int &value1, int &value2) {
{ void(*pSwap) (int &, int &) = swapValue;
int temp = value1; cout << pSwap << endl;
value1 = value2; cout << swapValue << endl;
value2 = temp;
} int a = 1, b = 5;
cout << "Before: " << a << " " << b << endl;
(*pSwap)(a, b);
cout << "After: " << a << " " << b << endl;

system("pause");
return 0;
}
Con trỏ hàm
❖ Truyền tham số là con trỏ hàm
int Cong(int x, int y);
int Tru(int x, int y);
int TinhToan(int x, int y, int (*pheptoan)(int, int))
{
int kq = (*pheptoan)(x, y); // Gọi hàm
return kq;
}

void main()
{
int (*pheptoan)(int, int) = Cong;
int kq1 = TinhToan(1, 2, pheptoan);
int kq2 = TinhToan(1, 2, Tru);
}
Con trỏ hàm
❑ Sử dụng con trỏ hàm làm tham số
Ví dụ:
void selectionSort(int *arr, int length, bool (*comparisonFunc)(int, int))

▪ Một con trỏ hàm cũng là một biến con trỏ, do đó chúng ta có thể sử dụng con trỏ hàm là
tham số của một hàm nào đó.
▪ Khi tham số của hàm là con trỏ hàm, chúng ta sẽ truyền đối số là địa chỉ của một hàm.
Hàm được sử dụng làm đối số của hàm có thể gọi là callback function.
Ví dụ : Sắp xếp

Before sorted: 1 4 2 3 6 5 8 9 7
After sorted: 1 2 3 4 5 6 7 8 9
Con trỏ hàm
▪ Đặt ra trường hợp chúng ta muốn có sắp xếp mảng một chiều sử dụng thuật
toán selectionSort nhưng sắp xếp theo thứ tự giảm dần.
▪ Như vậy, chúng ta cần đến 2 hàm selectionSort để đáp ứng cho 2 trường hợp
mình kể trên. Trong khi cả 2 hàm selectionSort này chỉ khác nhau về toán tử so
sánh tại phép so sánh ở mệnh đề if trong vòng lặp.
Con trỏ hàm
Để giải quyết vấn đề này, đầu tiên mình cần có 2 hàm thực hiện công việc so sánh
như sau:

bool ascending(int left, int right)


{
return left > right;
}

bool descending(int left, int right)


{
return left < right;
}
Con trỏ hàm
▪ Hai hàm này chỉ có tác dụng thay thế phép so sánh trong mệnh đề if.
▪ Nếu chúng ta cần sắp xếp giá trị trong mảng theo thứ tự tăng dần, chúng ta sẽ thay
thế hàm ascending vào mệnh đề if trong hàm selectionSort như sau:
Con trỏ hàm

Lúc này, chúng ta vẫn còn cần thêm một hàm selectionSort khác và thay thế biểu thức so sánh trong mệnh đề if bằng
hàm descending để có thể sắp xếp mảng theo chiều giảm dần
Con trỏ hàm
▪ Chúng ta sẽ thêm vào tham số thứ 3 cho hàm selectionSort là một con trỏ hàm
dùng để trỏ đến hàm ascending hoặc descending tùy vào lời gọi
hàm selectionSort.
▪ Do hàm ascending và descending có cấu trúc kiểu trả về và tham số hoàn toàn
giống nhau, nên chúng ta có thể sử dụng chung một kiểu con trỏ hàm
Con trỏ hàm
▪ Con trỏ hàm dùng làm tham số thứ 3 của hàm selectionSort như sau:

bool (*comparisonFunc)(int, int);


Con trỏ hàm
Con trỏ hàm
Con trỏ hàm
▪ Nếu muốn đổi ngược lại thứ tự của mảng khi sắp xếp, chúng ta chỉ cần thay đối số
thứ 3 của hàm selectionSort là địa chỉ của hàm ascending:
Con trỏ hàm
▪ Với việc đặt thêm tham số thứ 3 của hàm selectionSort là 1 con trỏ hàm, chúng ta
có thể thiết kế thêm nhiều tùy chọn cho điều kiện sắp xếp mảng một chiều khác
nhau và vẫn có thể sử dụng cho hàm selectionSort.
▪ Ví dụ muốn thêm một kiểu sắp xếp có điều kiện khác là mọi số chẵn trong mảng
sẽ đứng trước, các số lẻ trong mảng sẽ đứng sau, và phần chẵn hay phần lẻ đều
được sắp xếp tăng dần
Con trỏ hàm
Bài Tập
1) Cho biết kết quả của các chương trình sau:
Bài Tập
2) Cho mảng số nguyên n phần tử.
a) Viết hàm nhập và xuât mảng dùng con trỏ và cấp phát động.
b) Tìm giá trị lớn thứ nhất và thứ nhì trong mảng
c) Sắp xếp các phần tử tăng dần

* Lưu ý: thu hồi vùng nhớ khi kết thúc chương trình
Bài Tập
3) Cho mảng số nguyên n phần tử.
a) Viết hàm nhập và xuât mảng dùng con trỏ và cấp phát động.
b) Xóa phần tử tại vị trí thứ k ra khỏi mảng (k nhập từ bàn phím) (lưu ý thay đổi vùng
nhớ)
c) Thêm phần tử x tại vị trí thứ j vào mảng (x, j nhập từ bàn phím) (lưu ý thay đổi
vùng nhớ)
d) Xóa tất cả các số nguyên tố ra khỏi mảng (nếu có) (lưu ý thay đổi vùng nhớ)

* Lưu ý: thu hồi vùng nhớ khi kết thúc chương trình
Bài Tập
4) Cho 2 mảng A và B lần lượt chứa N và M phần tử
a) Viết hàm nhập và xuât mảng dùng con trỏ và cấp phát động.
b) Nối hai mảng A và B thành mảng C.
c) Sắp xếp A và B tăng dần
d) Nối 2 mảng A và B tăng dần trong câu (c) thành một mảng tăng dần (yêu cầu:
không dùng thuật toán sắp xép trên mảng kết quả)

* Lưu ý: thu hồi vùng nhớ khi kết thúc chương trình
Bài Tập
5) Cho mảng 2 chiểu các số nguyên có kích thươc MxN
* yêu cầu dùng con trỏ cấp 2 (pointer to pointer) biểu diễn mảng hai chiều. Cấp phát
động và thu hồi vùng nhớ
a) Viết hàm nhập và xuất mảng 2 chiều.
b) Viết hàm tìm giá trị lớn nhất của mảng.
c) Tính tổng dòng thứ k (k nhập từ bàn phím, 0<=k < M)
d) Đếm xem trong mảng có bao nhiêu số bằng x (x nhập từ bàn phím)
Bài Tập
6) Viết chương trình nhập vào một mảng các số nguyên dương chưa biết trước số
phần từ và đồng thời không nhập số lượng phần tử từ đầu (nhập vào -1 để kết thúc ;
nhập đến đâu mở rộng mảng đến đó.

Gợi ý: trong mỗi bước lặp (for hoặc while – điều kiện dừng, nhập -1) mở rộng thêm
một vùng nhớ cho mảng để lưu giá trị mới.

*(option-không bắt buộc) Trong C++ (visual studio): tìm hiểu thư viện std::vector và
phương thức insert hoặc push_back tương ứng.
+ Sử dụng thư viện std::vector thì không cần quan tâm số lượng phần tử khi khai
báo biến

You might also like