Professional Documents
Culture Documents
[2017 – 04 – 15]
A. Giới thiệu
1. Điều kiện để học được tài liệu này
Có kiến thức nền tảng vững chắc với ngôn ngữ C (hoặc C++).
Đã học xong 4 bài giảng trước đó (con trỏ phần 1 phần 4).
2. Dẫn nhập
Ở bài giảng về con trỏ phần 2 (bộ nhớ RAM), bạn đã được giới thiệu sơ lược về “con trỏ trỏ
đến con trỏ”.
byte byte … byte byte byte byte byte byte byte byte byte byte
0 1 400 401 402 403 404 405 406 407 408 409
n p = 400
Vấn đề là, con trỏ cũng là 1 biến nên nó cũng có địa chỉ, ta nhận thấy p có địa chỉ là 406.
Liệu có thể nào có 1 con trỏ q khác và nó lưu giá trị = 406. Woowwwww !!!
Con trỏ q trỏ đến con trỏ p, và con trỏ p trỏ đến n.
Lưu ý:
q lưu giá trị 406 và địa chỉ của q là 1 giá trị nào đó =)).
Và biết đâu, có 1 con trỏ r khác đang lưu địa chỉ của q. Có nghĩa là…
Ta nói rằng:
Trong bài học này, chúng ta sẽ vận dụng con trỏ đa cấp để tạo ra mảng động. Đồng thời chúng
ta sẽ khảo sát những tính chất hay ho của mảng động.
điều khiển
int n;
int *p = &n;
int **q = &p;
Ta có
p lưu địa chỉ của n nên p có thể điều khiển được n (hiển nhiên).
Tương tự, q lưu địa chỉ của p nên q có thể điều khiển được p.
*q = 401.
Điều gì xảy ra ? Giá trị của con trỏ p đã bị thay đổi thành 401.
điều khiển
Rất đơn giản đúng không, con trỏ đa cấp bản chất cũng là con trỏ mà thôi.
Con trỏ cấp 1 có kiểu int* sẽ điều khiển được số nguyên int.
Con trỏ cấp 2 có kiểu int** sẽ điều khiển được con trỏ cấp 1 có kiểu int*.
void test(int n)
{
n = 7;
}
int main()
{
int n = 0;
test(n);
return 0;
}
Chạy chương trình, bạn đoán xem sẽ in ra màn hình số 0 hay số 7 ? Chắc chắn là in ra số 0 rồi.
Học lập trình cơ bản là phải biết.
Vậy nếu muốn n ở hàm test liên quan đến n của hàm main thì sao ? Muốn in ra n = 7 thì sao ?
#include <stdio.h>
int main()
{
int n = 0;
test(&n);
// in ra giá trị của n
return 0;
}
Vì sao vậy ?
Ở hàm main, ta truyền địa chỉ của biến n vào hàm test.
pn có khả năng điều khiển được biến n, có thể thay đổi được giá trị n của hàm main.
điều khiển
C C++
#include <stdio.h> #include <iostream>
#include <stdlib.h> // malloc, free using namespace std;
test(a); test(a);
a[0] = 9; a[0] = 9;
return 0; return 0;
} }
byte … … …
nào đó
Bộ nhớ được cấp phát động
byte … … …
nào đó
Bộ nhớ được cấp phát động
Hàm test được giải phóng bộ nhớ hoàn toàn NHƯNG vùng nhớ được cấp phát động vẫn còn
đó.
Đồng thời, a ở hàm main vẫn là NULL lệnh gán a[0] = 9 là sai hoàn toàn .
Kết thúc chương trình, hàm main được giải phóng bộ nhớ NHƯNG vùng nhớ được cấp phát
động vẫn còn đó hiện tượng memory leaks.
C C++
#include <stdio.h> #include <iostream>
#include <stdlib.h> // malloc, free using namespace std;
test(&a); test(&a);
a[0] = 9; a[0] = 9;
free(a); delete[] a;
a = NULL; a = NULL;
return 0; return 0;
} }
điều khiển
byte … … …
300
Bộ nhớ được cấp phát động
Điều mà ta mong muốn là thay đổi con trỏ a ở hàm main, như vậy ở hàm test ta truyền vào địa
chỉ của a để điều khiển nó, bạn hãy suy nghĩ thật đơn giản nhé.
C C++
#include <stdio.h> #include <iostream>
#include <stdlib.h> // malloc, free using namespace std;
void NhapMang(int **pa, int *pn) void NhapMang(int **pa, int *pn)
{ {
int i;
printf("Nhap so luong phan tu = "); cout << "Nhap so luong phan tu = ";
scanf("%d", pn); cin >> *pn;
return 0; return 0;
} }
C C++
#include <stdio.h> #include <iostream>
#include <stdlib.h> // malloc, free using namespace std;
return a; return a;
} }
a = NhapMang(&n); a = NhapMang(&n);
return 0; return 0;
} }
Vấn đề là khi bạn muốn trả về 1 mảng bình thường, liệu có được hay không ?
int* NhapMang(int *pn) Với những gì mà bạn học được từ bài giảng trước đó,
{
int a[20]; bạn nhìn vào đoạn code trên xem có gì sai không ?
Bộ nhớ của hàm NhapMang bao gồm con trỏ
// nhập vào n
pn và 20 số nguyên của mảng a.
for (i = 0; i < (*pn); i++) Khi kết thúc hàm NhapMang, toàn bộ 20 số
{
// nhập a[i] nguyên của mảng a sẽ “bay theo mây khói”.
} Hàm NhapMang trả về địa chỉ của vùng nhớ đã
return a; bị giải phóng rất nguy hiểm sai.
}
int main()
{
int *a = NULL;
int n = 0;
a = NhapMang(&n);
return 0;
}
Bạn có thể khắc phục bằng cách tạo ra 1 struct và struct này lưu toàn bộ mảng, tuy nhiên đây
chỉ là xoay sở tạm thời.
Khi bạn học về cấp phát mảng động, bạn đã giải quyết được bài toán này đúng không ? Vấn đề
là hàm main bạn cần thêm lệnh giải phóng bộ nhớ.
Bài toán đặt ra: làm cho mảng a “to hơn” bằng cách thay đổi số lượng phần tử tối đa của mảng
là 21 phần tử.
Bạn sử dụng hàm realloc (trong thư viện stdlib.h) để cấp phát bộ nhớ lại.
Để giải quyết vấn đề trên, rất đơn giản, ta code như sau:
a = realloc(a, 21 * sizeof(int));
Hàm nhận vào con trỏ ptr (địa chỉ mà đã được cấp phát bộ nhớ từ trước).
Hàm nhận vào số lượng byte cần cấp phát (size).
Hàm trả về địa chỉ, nơi mà được cấp phát động vừa đủ size phần tử.
C
#include <stdlib.h> // malloc, free, realloc
int main()
{
int *a = NULL;
int n = 20;
a = (int*)malloc(n * sizeof(int));
// SỬ DỤNG MẢNG a
return 0;
}
Vì vậy, cách làm bình thường của chúng ta là… làm thủ công: giải phóng vùng nhớ cũ và cấp
phát lại vùng nhớ.
C++
#include <iostream>
using namespace std;
int main()
{
int *a = NULL;
int n = 20;
a = new int[n];
// SỬ DỤNG MẢNG a
delete[] b;
b = NULL;
n = n + 1;
return 0;
}
Nó là 1 cách diễn đạt “đối tượng đang sử dụng gắn liền với vùng nhớ nào”.
Ví dụ:
Giả sử x nằm tại địa chỉ 700 trong bộ nhớ. Ta nói rằng:
Ta cũng có thể nói rằng “con trỏ p đang trỏ đến x” hoặc “con trỏ p đang tham chiếu đến x”.
Tham chiếu là 1 khái niệm rất trừu tượng, bạn đừng mong muốn hiểu rõ nó. Bạn chỉ cần trải
nghiệm đủ nhiều là dần dần bạn tự hiểu.
C++
1 #include <iostream>
2 using namespace std;
3
4 int main()
5 {
6 int x = 5;
7 int &r = x;
8
9 r = 21;
10
11 cout << x << endl;
12
13 return 0;
14 }
21
C++
1 #include <iostream>
2 using namespace std;
3
4 int main()
5 {
6 int x = 5;
7 int &r = x;
8
9 r = 21;
10
11 cout << x << endl;
12
13 return 0;
14 }
Vùng nhớ từ byte 500 đến byte 503 được đặt tên là “x”.
Ở dòng code số 7: khai báo r là tham chiếu đến x. Như vậy vùng nhớ từ byte 500 đến byte 503
có thêm 1 cái tên mới, đó chính là “r”.
x, r
C++
1 #include <iostream>
2 using namespace std;
3
4 int main()
5 {
6 int x = 5;
7 int &r = x;
8
9 r = 21;
10
11 cout << x << endl;
12
13 return 0;
14 }
x, r
Điều này hoàn toàn tương tự như gán x = 21, bởi vì x và r đều là sự “trừu tượng hóa” của vùng
nhớ từ byte 500 đến byte 503.
Ví dụ sau đây minh họa ứng dụng của tham chiếu trong C++:
#include <iostream>
using namespace std;
void test(int k)
{
k = 3;
}
int main()
{
int x = 0;
test(x);
// in ra số 0
cout << x << endl;
return 0;
}
Chắc chắn in ra màn hình số 0, vì x ở hàm main không liên quan gì k ở hàm test.
int main()
{
int x = 0;
test(x);
// in ra số 3
cout << x << endl;
return 0;
}
Ta nói rằng: k ở hàm test là một tên gọi khác của x ở hàm main. k là tham chiếu đến x.
C++
#include <iostream>
using namespace std;
a = new int[n];
int main()
{
int *a = NULL;
int n = 0;
NhapMang(a, n);
delete[] a;
a = NULL;
return 0;
}
Bài 1. Nhập vào mảng, xuất mảng, tính tổng các phần tử của mảng.
Ví dụ mảng a = {9 8 7 6} a = {6 7 8 9}
Bài 4. Nhập, xuất mảng a. Tạo ra mảng b (được cấp phát động) chứa các phần tử là số lẻ trong
mảng a. Xuất ra mảng b.
Chương trình phải chia mã nguồn thành 3 file: file main, header BaiLam + code BaiLam.
Nhập vào 2 mảng a và b, tạo ra mảng c bằng cách ghép mảng a và mảng b. Mảng c chứa vừa
đủ số lượng phần tử.
Ví dụ:
a = { 9, 8, 7 } với na = 3
b = { 5, 3, 4, 6 } với nb = 4
c = { 9, 8, 7, 5, 3, 4, 6 } với nc = 7.
Mảng c chứa tối đa 7 phần tử (vừa đủ, không dư).
Nhập vào mảng a. Cắt mảng a tại vị trí vt để có được mảng b và mảng c.
Ví dụ:
Bài 7. Xóa phần tử cuối cùng của mảng. Mảng sau khi xóa phải vừa đủ số lượng phần tử.
Chương trình cho phép nhập mảng a, nhập vào vị trí cần xóa.