Dữ liệu kiểu con trỏ• Bộ nhớ RAM là mỗi chuỗi các ô nhớ có địa chỉ xác định được đánh số từ 0x0000 • Địa chỉ của một ô nhớ là vị trí của nó trong bộ nhớ RAM • Địa chỉ tối đa phụ thuộc và
Trang 1BÀI GIẢNG
KỸ THUẬT LẬP TRÌNH
Nguyễn Duy Đỉnh
-1
Trang 2Tuần 9 - 10 – Con trỏ
• Dữ liệu kiểu con trỏ
• Các phép toán với con trỏ
• Các lệnh cấp phát bộ nhớ
• Mảng động
• Truyền tham số mảng
2
Trang 3Dữ liệu kiểu con trỏ
• Bộ nhớ RAM là mỗi chuỗi các ô nhớ có địa chỉ xác định được đánh số từ 0x0000
• Địa chỉ của một ô nhớ là vị trí của nó trong bộ nhớ RAM
• Địa chỉ tối đa phụ thuộc vào dung lượng nhớ Ví dụ – RAM 256 Bytes có dải địa chỉ từ 0x00 đến 0xFF
– RAM 1M có dải địa chỉ từ 0x0 0000 đến 0xF FFFF – RAM 1G có dải địa chỉ từ 0x0000 0000 đến 0x3FFF FFFF –
• Bản đồ nhớ là cách bố trí các ô nhớ trong bộ nhớ RAM 3
Trang 4 Ví dụ, bản đồ nhớ của RAM 256 Bytes
Địa chỉ ô nhớ màu vàng - 0x0B; đỏ - 0x14; xanh – 0x29
Mỗi ô nhớ chiếm 1 Byte bộ nhớ
Dữ liệu kiểu con trỏ
0x00 0x10 0x20
0xF0 4
Trang 5Dữ liệu kiểu con trỏ
• Các biến khi được khai báo sẽ chiếm một ví trí nào đó trong bộ nhớ RAM
• Tên biến chính là cách mà ta gọi tên của ô nhớ
• Chẳng hạn, ô nhớ màu vàng, màu đỏ, màu xanh là cách mà ta gọi tên các
Trang 6Do biến a, b kiểu short int là kiểu dữ liệu 2 bytes nên mỗi biến a, b
sẽ chiếm 2 bytes (2 ô) trong bản đồ nhớ của bộ nhớ RAM
Chẳng hạn, giả sử bộ nhớ cấp cho a bắt đầu từ 0x0A
bộ nhớ cấp cho b bắt đầu từ 0x12
Dữ liệu kiểu con trỏ
0x00 0x10 0x20
0xF0 6
Trang 7 Do biến a, b kiểu short int là kiểu dữ liệu 2 bytes nên mỗi biến
a, b sẽ chiếm 2 bytes (2 ô) trong bản đồ nhớ của bộ nhớ RAM
Chẳng hạn, giả sử bộ nhớ cấp cho a bắt đầu từ 0x0A
bộ nhớ cấp cho b bắt đầu từ 0x12
Dữ liệu kiểu con trỏ
0x00 0x10 0x20
7
Trang 8 Gán a = 0x1234, nghĩa là ghi các giá trị 12 và 34 vào các ô nhớ tương ứng: [0x0A] = 12 và [0x0B] = 34
Tương tự, gán b = a<<1; //b=2468, nghĩa là ghi các giá trị 24
0xF0 8
Trang 9 Trên thực tế, khi máy tính thực hiện chương trình, nó chỉ quan tâm đến địa chỉ ô nhớ và làm việc với địa chỉ ô nhớ
a, b chẳng qua chỉ là tên mà người lập trình gọi ô nhớ sao cho thuận tiện khi lập trình
Thông thường, ta chỉ quan tâm đến giá trị của các biến a, b mà không cần biết nó ở vị trí nào trong bản đồ nhớ
Dữ liệu kiểu con trỏ
9
Trang 10 Tuy nhiên, trong rất nhiều trường hợp, địa chỉ và vùng bộ nhớ chiếm giữ của các biến này cũng rất quan trọng, giúp cho việc thực hiện thuật toán nhanh hơn, thuận tiện hơn, linh hoạt hơn
Chẳng hạn,
khi muốn thay đổi giá trị của tham số truyền vào một hàm
khi muốn làm việc với các ô nhớ liên tiếp nhau trong bộ nhớ RAM (mảng) hoặc trên ổ cứng (tệp)
khi muốn làm việc với danh sách liên kiết
khi muốn sử dụng bộ nhớ một cách linh hoạt
Dữ liệu kiểu con trỏ
10
Trang 11 Từ đó, Visual C định nghĩa kiểu dữ liệu con trỏ: là kiểu dữ liệu dùng để lưu địa chỉ của các đối tượng khác
Khai báo: thêm dấu * trước tên biến Ví dụ
int a, *b;
Khai báo biến a kiểu nguyên, biến b là biến con trỏ kiểu nguyên, lưu địa chỉ của ô nhớ nào đó mà giá trị của ô nhớ đó là kiểu int
Hay nói cách khác, biến b là một con trỏ, trỏ tới các phần tử có
kiểu int
Dữ liệu kiểu con trỏ
11
Trang 12 Ví dụ khai báo biến kiểu con trỏ:
float *x, *y;
FILE *f;
char *s;
x, y là các con trỏ kiểu float, lưu địa chỉ của các ô nhớ mà giá trị
của các ô nhớ đó có kiểu kiểu float, hay x, y trỏ tới các phần tử
kiểu float
f con trỏ kiểu file, trỏ tới các tệp
s con trỏ kiểu char, trỏ tới các phần tử kiểu char
Dữ liệu kiểu con trỏ
12
Trang 13 Các biến thông thường khi được khai báo sẽ nằm trong bộ nhớ Stack (có dung lượng mặc định là 1M Bytes)
Các biến con trỏ khi được khai báo sẽ nằm trong bộ nhớ Stack
Bản thân mỗi biến con trỏ chiếm 4 Bytes bộ nhớ
Tuy nhiên, vùng nhớ mà biến con trỏ quản lý là vùng nhớ nằm trong bộ nhớ Heap
Bộ nhớ Heap thường lớn hơn nhiều so với bộ nhớ Stack
Dữ liệu kiểu con trỏ
13
Trang 14 Toán tử & và toán tử *
Toán tử &: chỉ địa chỉ của một biến trong bản đồ nhớ
Toán tử *: chỉ giá trị của ô nhớ có địa chỉ mà biến con trỏ trỏ tới
Ví dụ:
int a, *b;
&a: địa chỉ của ô nhớ mà biến a chiếm trong bộ nhớ
*b: giá trị của ô nhớ mà con trỏ b trỏ tới
&b: địa chỉ của ô nhớ mà biến con trỏ b chiếm
Các phép toán với con trỏ
14
Trang 15 Ví dụ:
int a, b, *c;
a = 0x1234;
b = a << 1;
Với bản đồ nhớ dưới đây thì ta có: &a = 0x0A; &b = 0x12;
Giả sử con trỏ c đang trỏ tới ô nhớ 0x12 thì: *c = 2468;
Các phép toán với con trỏ
0x20
15
Trang 16 Phép gán:
short int *a, *b, c, d, *e;
a = &c; //Con trỏ a trỏ tới địa chỉ của biến c
b = &d; //Con trỏ b trỏ tới địa chỉ của biến d
*a = 0x1234; //Giá trị của ô nhớ mà a trỏ tới = 0x1234
//Tương đương với lệnh gán c = 0x1234
*b = *a; //Giá trị của ô nhớ mà b trỏ tới bằng giá //trị ô nhớ mà a trỏ tới bằng 0x1234
//Tương đương với lệnh gán d = 0x1234
e = a;//Con trỏ e trỏ tới địa chỉ mà con trỏ a //đang trỏ tới, tức là trỏ tới địa chỉ của //biến c
a = &d; //Con trỏ a trỏ tới địa chỉ của biến d, //không trỏ tới địa chỉ của biến c nữa
*e = d+1; //Giá trị của ô nhớ mà con trỏ e đang trỏ //đến tăng 1 đơn vị, tương đương với việc //tăng c 01 đơn vị = 0x1235
Các phép toán với con trỏ
16
Trang 17 Ví dụ: c ở địa chỉ 0x0A, d ở địa chỉ 0x12
short int *a, *b, c, d, *e;
17
Trang 18 Giả sử c ở địa chỉ 0x0A, d ở địa chỉ 0x12
short int *a, *b, c, d, *e;
0xF0 18
Trang 19 Giả sử c ở địa chỉ 0x0A, d ở địa chỉ 0x12
short int *a, *b, c, d, *e;
a = &c; b = &d;
Dự đoán kết quả của phép toán sau?
a ; //a trỏ tới ô nhớ có địa chỉ 0x08
b ++; //b trỏ tới ô nhớ có địa chỉ 0x14
a -= 2; //a trỏ tới ô nhớ có địa chỉ 0x04
b += 3; //b trỏ tới ô nhớ có địa chỉ 0x1A
Phép tịnh tiến: a += i hoặc b -= i sẽ dịch chuyển con trỏ
a hoặc b đi (i*sizeof(*a)) ô nhớ trong bản đồ nhớ theo hướng tiến hoặc lùi phụ thuộc vào dấu của phép tịnh tiến
Các phép toán với con trỏ
19
Trang 20 Lưu ý: con trỏ không có phép nhân (*) hoặc chia (/) mà chỉ có
phép gán (=), phép lấy địa chỉ (&), phép lấy giá trị (*) và phép tịnh tiến (+ hoặc -)
Các phép toán với con trỏ
20
Trang 21 Ta đã gặp một vài trường hợp hàm làm thay đổi giá trị của tham số truyền vào dòng lệnh gọi nó như:
Trang 22 Ví dụ 9.1: Lập trình hàm đổi giá trị 2 biến bất kỳ truyền vào
int a = 2, b = 5;
Swap(a, b);
Ứng dụng truyền tham số cho hàm
Hàm Swap(x, y) thực hiện việc đảo giá trị của các biến x, y trong nội dung hàm Tuy nhiên, khi hàm kết thúc, giá trị của x, y vẫn không bị thay đổi trong toàn cục
Nguyên nhân: Do hàm chỉ sử dụng
giá trị của các biến này mà không quan tâm đến nó là biến nào
22
Trang 23 Ví dụ 9.1: Lập trình hàm đổi giá trị 2 biến bất kỳ truyền vào
int a = 2, b = 5;
Ứng dụng truyền tham số cho hàm
Hàm Swap2(&x, &y) không quan tâm đến tên biến mà quan tâm đến ô nhớ lưu giá trị của biến
Hàm Swap2(&x, &y) thực hiện việc đảo giá trị của các ô nhớ mà con trỏ
x và y trỏ tới
23
Trang 24 Note:
Việc truyền tham số sẽ làm cho hàm trở nên linh hoạt hơn, không phụ thuộc vào biến toàn cục, do đó, có thể được dùng đi dùng lại trong các project khác nhau mà không phụ thuộc vào project cụ thể.
Việc truyền tham số sẽ đảm bảo tính đóng kín và tính kế
thừa cho hàm
Ứng dụng truyền tham số cho hàm
24
Trang 25 Ví dụ 9.2: Lập trình hàm GPTB2() giải phương trình bậc 2 với
các hệ số và các ẩn được truyền vào dưới dạng tham số
void GPTB2(float a, float b, float c, float *x1, float *x2)
float a, b, c, x1, x2;
GPTB2(a, b, c, &x1, &x2);
Ứng dụng truyền tham số cho hàm
25
Trang 26 Ví dụ 9.3: Lập trình hàm HeBac2() giải hệ 2 phương trình 2 ẩn
với các hệ số và các ẩn được truyền vào dưới dạng tham số
void HeBac2(float a1, float a2, float b1, float b2, float c1,
float c2, float *x, float *y)
float a1, a2, b1, b2, c1, c2, x1, x2;
HeBac2(a1, a2, b1, b2, c1, c2, &x1, &x2);
Ứng dụng truyền tham số cho hàm
26
Trang 27 Trước khi dùng biến kiểu con trỏ, ta đã chỉ rõ nó trỏ tới phần tử nào, địa chỉ nào trong bộ nhớ nói cách khác, ta đã khởi tạo cho nó
Chẳng hạn, ở ví dụ 9.3 trên, bên trong hàm HeBac2() ta đã truyền địa chỉ của các biến x1, x2 tới các con trỏ *x và *y để chỉ
rõ rằng 2 con trỏ x, y trỏ tới ô nhớ của các biến x1 và x2 toàn cục
Các biến x1, x2 là các biến toàn cục có địa chỉ xác định và không đổi trong bộ nhớ nên vùng nhớ mà con trỏ *x và *y trỏ tới đã xác định
Các lệnh cấp phát bộ nhớ
27
Trang 28 Tuy nhiên, có những trường hợp người lập trình muốn
Thêm vào các biến mới trong quá trình chương trình chạy và giải phóng chúng sau khi chúng hết nhiệm vụ
Sử dụng vùng nhớ độc lập với các vùng nhớ chứa các biến đã khai báo
Ví dụ, tìm lỗi sai trong đoạn chương trình sau:
int *a, *b, c;
a = &c; //Không có lỗi vì vùng nhớ chứa biến c //(địa chỉ của ô nhớ chứa giá trị biến //c) là đã xác định
*a = 0x1234;
*b = *a;
Các lệnh cấp phát bộ nhớ
28
Trang 29 Tuy nhiên, có những trường hợp người lập trình muốn
Thêm vào các biến mới trong quá trình chương trình chạy và giải phóng chúng sau khi chúng hết nhiệm vụ
Sử dụng vùng nhớ độc lập với các vùng nhớ chứa các biến đã khai báo
Ví dụ, tìm lỗi sai trong đoạn chương trình sau:
int *a, *b, c;
a = &c; //Không có lỗi vì vùng nhớ chứa biến c // (địa chỉ của ô nhớ chứa giá trị biến //c) là đã xác định
*a = 0x1234;
*b = *a; //Lỗi vì b chưa trỏ vào một vùng nhớ //xác
Các lệnh cấp phát bộ nhớ
29
Trang 30 Khi mới khai báo biến con trỏ mà chưa chỉ rõ nó được trỏ tới
địa chỉ nào, biến con trỏ sẽ mang giá trị là NULL
Khi đó, để dùng được con trỏ b, người lập trình cần phải thực
hiện cấp phát động bộ nhớ cho nó, theo nghĩa, cần phải chỉ rõ,
nó sẽ quản lý vùng nhớ nào, kích thước bao nhiêu, ?
Để thực hiện cấp phát động, trong ANSI C (C chuẩn), ta dùng hàm malloc(), calloc() và realloc()
Các hàm malloc() và calloc() nằm trong thư viện
stdlib.h nên khi dùng phải khai báo #include <stdlib.h>
Các lệnh cấp phát bộ nhớ
30
Trang 31 Cú pháp:
void *malloc(size_t size);
void *calloc(size_t nitems, size_t size);
void *realloc(void *ptr, size_t size);
malloc(): cấp phát vùng nhớ có kích thước size byte
calloc(): cấp phát vùng nhớ có kích thước nitems*size bytes
realloc(): cấp phát lại vùng nhớ có kích thước size byte Sau khi gọi lệnh realloc(), con trỏ sẽ chỉ quản lý vùng nhớ có kích thước do hàm realloc() cấp, bất kể trước đó nó được cấp vùng nhớ bao nhiêu
Do các hàm malloc(), calloc() và realloc() có kiểu void nên ta
phải ép nó về các kiểu tương ứng
Các lệnh cấp phát bộ nhớ
31
Trang 32 Sau đoạn chương trình trên, con trỏ a sẽ trỏ tới một vùng nhớ
có kích thước 4 bytes (sizeof(int))
Con trỏ b trỏ tới vùng nhớ có kích thước 40 Bytes gồm 10 block
4 Bytes
Các lệnh cấp phát bộ nhớ
32
Trang 35 Để giải phóng vùng nhớ được cấp phát bằng hàm malloc(), calloc() và realloc() ta dùng hàm free()
Trang 36 Hàm malloc(), calloc() và realloc() có cách dùng tương đối phức tạp và tương đối giống nhau
Trong C++/Visual C++, để thực hiện việc cấp phát bộ nhớ, ta
Trang 37 Để thay đổi vùng nhớ đã cấp cho con trỏ, ta dùng lại lệnh new
theo sau bởi kích thước vùng nhớ mới:
new kiểu[kích thước vùng nhớ mới];
Trang 38 Để thu hồi vùng nhớ cấp cho con trỏ, ta làm như sau:
Trang 39 Ví dụ: tìm lỗi sai trong đoạn code sau:
int *a, *b = new int;
Trang 40 Mảng cũng là một con trỏ đặc biệt, gọi là con trỏ mảng
Khi ta khai báo một mảng, ví dụ:
int a[10];
thì bản thân a là con trỏ mảng
Vùng nhớ mà con trỏ a quản lý sẽ là số phần tử * sizeof(int),
(trong trường hợp này, a sẽ quản lý 40 bytes bộ nhớ)
Con trỏ a khi đó sẽ luôn luôn trỏ tới phần tử đầu tiên trong
mảng (tức là *a luôn bằng a[0])
Mảng động
40
Trang 41 Mảng a[10] do đó gọi là mảng tĩnh, nghĩa là số phần tử của
mảng không thể thêm bớt trong quá trình chương trình chạy
Con trỏ mảng a gọi là con trỏ hằng, do địa chỉ mà nó trỏ tới
không thể thay đổi trong quá trình chạy (luôn luôn trỏ tới a[0])
Do đó, lệnh gán con trỏ a để trỏ tới địa chỉ khác hay tịnh
tiến/lùi con trỏ a đều không hợp lệ
Mảng động
41
Trang 42 Tuy nhiên, trong nhiều ứng dụng, chẳng hạn:
khi không biết chắc chắn số phần tử của mảng
khi số phần tử thường xuyên thay đổi
Khi đó, nảy sinh nhu cầu thêm bớt một/một số phần tử của mảng, thậm chí xóa hẳn mảng ra khỏi bộ nhớ trong thời gian chạy chương trình (Run Time)
Dùng cấu trúc mảng động
Mảng động
42
Trang 43 Khai báo mảng động giống như khai báo một con trỏ bình thường:
kiểu * tên_con_trỏ;
Sau đó, để cấp phát động cho con trỏ mảng, ta dùng lệnh:
tên_con_trỏ = new kiểu[số_phần_tử];
khi đó, tên_con_trỏ sẽ quản lý một vùng nhớ có kích thước bằng kiểu*số_phần_tử
Mảng động
43
Trang 44 Ví dụ, sau đoạn lệnh sau:
Lệnh sau sẽ gán khởi tạo các phần tử của mảng bằng 0
for (i = 0; i<10; i++) a[i] = 0;
Mảng động
44
Trang 45 Ví dụ, sau đoạn lệnh sau:
int *a;
a = new int[10];
Ngoài ra, ngay sau khi khởi tạo bằng lệnh new, con trỏ mảng
động a sẽ trỏ tới phần tử đầu tiên của mảng
Ta có thể thực hiện phép tịnh tiến/lùi trên con trỏ a Lưu ý, khi
đó, phần tử mà con trỏ a trỏ tới cũng sẽ tịnh tiến/lùi theo
Mảng động
45
Trang 46 Ví dụ:
int *a, i;
a = new int[10]; //a trỏ tới phần tử thứ 1
for (i=0; i<10; i++) a[i] = i;
a++; //a trỏ tới phần tử thứ 2
a += 5; //a trỏ tới phần tử thứ 7
*(a – 3) = 15; //phần tử thứ 4 gán bằng 15
for (i = 0; i<3; i++)
{*a = 2*i; a++;} //Kết thúc vòng lặp a trỏ
Trang 47 Ví dụ 9.4: Viết chương trình trên đọc file Data.in có cấu trúc
gồm 2 dòng:
Dòng đầu chứa tổng số phần tử n
Các dòng tiếp theo chứa n số nguyên
Số phần tử n là không biết trước và cũng không có giới hạn cụ thể
Sau khi đọc, in ra màn hình các phần tử của mảng
Mảng động
47
Trang 48• Tìm lỗi sai trong chương trình :
Trang 49• Chương trình được sửa lại
Trang 50 Khi lời gọi hàm được gọi, máy tính sẽ cấp phát bộ nhớ các tham
số và các biến địa phương của hàm
Các tham số và biến của hàm đều được khai báo trong Stack
Nếu số lượng tham số và biến quá nhiều sẽ nhanh chóng làm tràn Stack sinh ra lỗi chương trình
Đặc biệt, nếu ta truyền tham số kiểu mảng thì ngoài biến mảng
đã có ở toàn cục, máy tính sẽ cấp phát thêm một vùng nhớ tương đương nữa cho tham số mảng truyền vào chương trình con làm cạn kiệt Stack
Truyền tham số mảng
50
Trang 51
Khi gọi hàm BubbleSort(a), máy tính ngoài 40000 Bytes đã cấp cho mảng a, sẽ phải cấp thêm 40000 Bytes nữa cho bộ nhớ đệm của hàm BubbleSort()
Truyền tham số mảng
51
Trang 52 Trường hợp này, ta có thể truyền tham số kiểu con trỏ, sau đó lập trình với mảng động như bình thường Ví dụ:
BubbleSort(a);
Lúc này, khi gọi hàm, chương trình chỉ phải cấp thêm 4 Bytes trong bộ nhớ Stack để lưu biến con trỏ X Sử dụng bộ nhớ hiệu quả hơn
Truyền tham số mảng
52
Trang 53 Ví dụ 9.5: Hãy lập trình lại thuật toán Selection Sort dùng tham
số truyền vào là mảng cần sắp xếp và số phần tử của mảng
void SelectionSort(int *a, int n)
int X[10000], m;
Truyền tham số mảng
53
Trang 54 Ví dụ 9.6: Cho n dãy số a0, a1, , an Trọng số của một dãy là số có số lần
xuất hiện nhiều nhất lớn nhất trong dãy đó Ví dụ, cho 3 dãy:
a0: 1, 3, 2, 3, 4, 2, 5, 6, 3, 2 trọng số là 3
a1: 3, 2, 6, 4, 5, 1, 2, 1, 4, 2, 5, 6, 4 trọng số là 4
a2: 6, 6, 7 trọng số là 6
Hãy tìm dãy có trọng số lớn nhất
Input: file “TrongSo.in” gồm:
Dòng đầu tiên ghi số dãy số n
n dòng tiếp theo mỗi dòng ghi
Số đầu tiên ghi số phần tử của dãy thứ i
Các số còn lại ghi các số thuộc dãy thứ i
Output: file “TrongSo.out” gồm 1 dòng ghi 2 số cách nhau bởi dấu cách:
Số thứ tự của dãy có trọng số lớn nhất
Trọng số lớn nhất của dãy tìm được
Truyền tham số mảng
54