Điểm làm nên thương hiệu của C/C++ chính là con trỏ, một loại biến đặt biệt, nó lưu địa chỉ của những biến khác(đúng hơn là lưu địa chỉ của vùng nhớ). Nên nhớ rằng cái gì cũng có tính hai mặt, con trỏ(pointer) tuy mạnh nhưng cũng rất nguy hiểm nếu không biết cách sử dụng. Đọc đến đây chắc hẳn các bạn sẽ tự hỏi tại sao lại phải lưu địa chỉ của biến khác, tại sao lại gọi con trỏ là thương hiệu của C/C++, phần bên dưới sẽ giải đáp phần nào 2 câu hỏi đó.
Tổ chức bộ nhớ trong C/C++
Trước khi đi vào nguyên cứu con trỏ ta khảo sát sơ qua về bộ nhớ chương trình lúc chạy(runtime storage).Về cơ bản runtime storage được chia làm 4 phần như sau:
- Text segment(code segment): chương trình của ta được biên dịch sẽ ra mã máy, khi chương trình chạy thì mã máy đó sẽ được nạp vào đây. Ví dụ điển hình là khi ta khai báo: char st[] = "this is string"; thì chuổi "this is string" sẽ được lưu "cứng" vào vùng nhớ này.
- Global area: lưu biến toàn cục
- Stack segment: khi ta gọi 1 hàm thì nó sẽ được nạp vào đây, các biến được khai báo ở trong hàm(biến cục bộ) sẽ được lưu tại đây, khi hàm kết thúc thì toàn bộ dữ liệu trong này sẽ tự động giải phóng. Việc đọc ghi trong stack rất nhanh nhưng lưu ý rằng kích thước của nó lại khá bé vì thế nên chúng ta phải sử dụng 1 cách "tiết kiệm", như khi cần lưu trữ các dữ liệu có kích thước lớn thì stack không phải là sự lựa chọn thay vào đó ta lưu vào heap(lưu bằng cách nào thì bên dưới sẽ nói rỏ hơn) ngoài ra việc sử dụng đệ quy cũng có khả năng làm tràng stack.
- Heap segment: đây là nơi lưu trữ biến được cấp phát động, kích thước của heap khá lớn.
- nơi lưu trữ biến cục bộ
- kích thước bị giới hạn -> stack overflow
- bộ nhớ ở đây chỉ là tạm thời và tự động giải phóng
- truy cập nhanh nhưng kích bé
- kích thước lớn
- nơi lưu trữ biến cấp phát động
- không tự giải phóng -> memory leak
- sử dụng pointer để truy cập
- các dữ liệu kích thước lớn(như class, struct, array...) nên lưu ở đây
Mảng trong C/C++
Trong C/C++ việc khai báo 1 mảng(tỉnh) như sau:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int arr[10];// khai báo 1 mảng int 10 phần tử | |
arr[1] = 2512;// gán 2512 cho phần tử thứ hai của mảng |
Với việc khai báo như trên thì stack của chúng ta phải tốn 10 * sizeof(int) là 10 * 4 bytes để lưu, giả sử ta cần 1 mảng có 10000 phần tử thì stack của ta phải cần tới 10000 * 4 bytes để lưu trữ. Vì ta đã biết kích thước của stack khá nhỏ thế nên phải tìm cách lưu dữ liệu của mảng ở nơi khác để đảm bảo stack không bị tràng, heap chính là nơi chúng ta cần lưu.
Để lưu mảng 10 phần tử heap(thay vì stack) ta sẽ sử dụng cấp phát động nghĩa là bộ nhớ được cấp phát khi chương trình chạy. Khai báo như sau:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int * ptr_ arr = new int[10];// cấp phát 10 * sizeof(int) = 10 * 4 bytes trong heap | |
ptr_arr[1] = 2512;// gán giá trị 2512 cho phần tử thứ hai của mảng |
Khai báo một con trỏ trong C/C++
Con trỏ thực chất là biến nguyên(thường là 4 bytes) lưu địa chỉ của những biến khác(đúng hơn là lưu địa chỉ của vùng nhớ). Con trỏ được khai báo như sau:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
data_type * pointer_name; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int* ptr_i;// khai báo biến con trỏ kiểu int->lưu địa chỉ biến int | |
char* ptr_c;// khai báo biến con trỏ kiểu char->lưu địa chỉ biến char |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int var_i = 1303;// khai báo biến int lưu giá trị1303 | |
int * ptr_i;// khai báo con trỏ kiểu int | |
ptr_i = &var_i;// lưu địa chỉ của biên var_i vào con trỏ ptr_i | |
* ptr_i = 2512;// gán giá trị 2512 cho biến mà ptr_i trỏ đến | |
printf("%d\n", var_i);// sẽ hiển thị số 2512 ra màng hình | |
printf("0x%x\n", &var_i);// địa chỉ của biến var_i vd như 0x1c2af334 | |
printf("0x%x\n", ptr_i);// giá trị của con trỏ ptr_i là 0x1c2af334 | |
printf("%d\n", *ptr_i);// giá trị của biến mà ptr_i trỏ đến là 2512 | |
printf("0x%x\n", &ptr_i);// địa chỉ của con trỏ ptr_i vd như 0x1c2af338 |
Oke! giờ ta quay trở lại ví dụ về mảng ở phần trên. Ta có khai báo:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int * ptr_ arr = new int[10];// cấp phát 10 * sizeof(int) = 10 * 4 bytes trong heap |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
new data_type[number_of_elements];// đối với mảng | |
hoặc: | |
new data_type; // đối với 1 biến trả về con trỏ trỏ đến vùng nhớ được cấp phát |
Giả sử ta có khai đoạn lệnh:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int * ptr_i = new int;// khởi tạo 1 vùng nhớ 4 bytes và cho ptr_i trỏ đến | |
*ptr_i = 0x02010502;// gán giá trị cho vùng nhớ mà ptr_i trỏ đến |
Chú ý là với việc cấp phát động ta phải tự giải phóng vùng nhớ nếu không sử dụng nữa(khác với cấp phát tỉnh là chúng sẽ được tự động thu hồi khi kết thúc chương trình hoặc hàm...), để giải phóng vùng nhớ ta sử dụng delete pointer_name; đối với 1 biến và delete[] pointer_name với 1 mảng.
Đọc đến đây có ai thắc mắt tại sao con trỏ thực chất là 1 biến nguyên(thường là 4 bytes) thế tại sao lại phải khai báo đủ lại con trỏ nào là con trỏ kiểu int, con trỏ kiểu char, con trỏ kiểu float...Câu trả lời sẽ có trong đoạn tiếp theo.
Truy xuất thông qua con trỏ
Như đã biết thì con trỏ lưu địa chỉ của biến, và khi ta truy cập đến biến thông qua con trỏ thì nó phải xác định là biến đó có bao nhiêu bytes vì mỗi biến có 1 kiểu dữ liệu riêng(char 1 byte, int 4 bytes, double 8 bytes...) để đọc cho chính xác vì thế nên ta mới có các loại con trỏ khác nhau trỏ đến kiểu dữ liệu tương ứng. Ta có ví dụ cụ thể như sau:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int i = 0x3f20cc01; | |
char * ptr_c = (char *)&i; | |
short * ptr_s = (short *)&i; | |
int * ptr_i = &i; |
Một điểm cần chú ý nữa là kích thước của con trỏ trong chương trình không phụ thuộc vào kiểu dữ liệu mà nó trỏ đến cũng như không phụ thuộc vào loại con trỏ, kích thước của nó thực chất phụ thuộc vào trình biên dịch ví dụ như trong trình biên dịch GNU GCC cho 32bit thì con trỏ sẽ có 4 bytes.
Để lấy hoặc đọc giá trị của 1 con trỏ ta sử dụng * trước tên con trỏ đó. Ví dụ *ptr_i sẽ trả về tham chiếu đến vùng nhớ(biến) mà con trỏ ptr_i đã trỏ đến.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int var_i = 3393; | |
int * ptr_i = &var_i; | |
*ptr_i = 2512;// tương đương với var_i = 2512; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int arr[10];// khai báo 1 mảng int 10 phần tử |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int arr[10]; | |
arr[0] = 0; | |
arr[1] = 1; | |
int * ptr_arri = arr; | |
char * ptr_arrc = (char *)arr; | |
printf("%d %d", *(ptr_arri + 1), *(ptr_arrc + 1)); |
Mảng và con trỏ
char a[6] = {10, 20, 30, 40, 50, 60};Với biến a và p trong bộ nhớ như sau:
char* p = a;
1. a = ? &a = ? *a = ?
2. p = ? &p = ? *p = ?
3. p + 1 = ? (*p) + 1 = ? *(p + 1) = ?
4. &p + 1 = ? &a + 1 = ?
5. a++; -> a = ?
6. p++; -> p = ?
- Khá đơn giản, giá trị của a sẽ là địa chỉ của phần tử đầu tiên trong mảng là 0x..08, &a là địa chỉ của mảng a bằng 0x..08, *a là giá trị tại địa chỉ 0x..08 chính là giá trị của phần tử đầu tiên trong mảng là 10.
- Giá trị của p chính là địa chỉ mà nó trỏ đến, ở đây chính là địa chỉ của phần tử đầu tiên trong mảng a -> kết quả là 0x..08. &p là địa chỉ của con trỏ p và bằng 0x..04. *p chính là lấy giá trị phần tử mà con trỏ p đang trỏ đến chính là giá trị phần tử đầu tiên trong mảng a bằng 10.
- Vì p là con trỏ kiểu char nên p + 1 sẽ nhảy 1 bước 1 byte chính là địa chỉ của phần tử thứ 2 trong mảng là 0x..09. (*p) + 1 là lấy giá trị của phần tử đầu tiên trong mảng mà p trỏ đến rồi cộng thêm 1 chính là 11. *(p + 1) sẽ nhảy 1 bước 1 byte rồi lấy giá trị tại đó chính là giá trị của phần tử thứ 2 trong mảng mà p trỏ đến là 20.
- Ở đây cần chú ý, p là con trỏ vì thế nên &p sẽ trả về địa chỉ của 1 con trỏ, vì địa chỉ của con trỏ là 1 số nguyên và thường là 4 bytes vì thế nên &p + 1 sẽ nhảy 1 bước với độ dài là 4 bytes từ địa chỉ của p vậy kết quả sẽ là 0x..08. Tương tự ta thấy a là 1 mảng 6 phần tử kích thước 6 bytes vì thế nên &a sẽ trả về địa chỉ của mảng a nên &a + 1 sẽ nhảy 1 bước nhảy có độ dài 6 bytes từ địa chỉ của a vậy kết quả là 0x..0e.
- Chú ý rằng a ở đây là hằng con trỏ vì thế nên ta không thể thay đổi giá trị của nó được vậy kết quả sẽ là 1 lỗi biên dịch.
- p là con trỏ kiểu char nên p++ sẽ nhảy 1 bước nhảy với độ dài 1 byte vậy kết quả sẽ là địa chỉ phần tử thứ 2 trong mảng a là 0x..09.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int arr[10]; | |
int * ptr_arr = new int[10]; | |
printf("%d %d", sizeof(arr), sizeof(ptr_arr)); |
- đối với dòng đầu tiên int arr[10]; ta có mảng tên là arr -> có tên -> cấp phát tỉnh
- int* ptr_arr = new int[10]; ở đây thì mảng này có tên là gì? -> bó tay -> cấp phát động, sẽ có 1 số bạn cho rằng mảng này tên là mảng ptr_arr nhưng như thế là không đúng vì ptr_arr là con trỏ kiểu int chứ không phải là mảng như ví dụ trên đã chứng minh.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
char * ptr_c = "something"; | |
char arr[] = "something"; |
Con trỏ hàm
Con trỏ không chỉ lưu địa chỉ của biến mà nó còn có thể lưu địa chỉ của hàm. Để đơn giản trong sử dụng con trỏ hàm ta cần định nghĩa kiểu con trỏ hàm bằng typedef theo cú pháp như sau:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
return_data_type (* pointer_function_name)(data_type1, data_type2...); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int compare(int a, int b); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
typedef int (* ptr_compare)(int, int); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ptr_compare ptr_cmp = &compare;// gán địa chỉ của hàm compare cho biến ptr_compare | |
ptr_cmp(3393, 2512);// tương đương với compare(3393, 2512); |
Con trỏ của con trỏ
Như định nghĩa thì con trỏ cũng chỉ là 1 biến như những biến khác vì thế nên nó cũng có địa chỉ và và vùng nhớ riêng để lưu trữ. Khái niệm con trỏ của con trỏ(còn gọi là con trỏ cấp 2) chỉ đơn giản là 1 con trỏ trỏ đến 1 con trỏ khác thay vì trỏ đến 1 biến như đã xét ở trên.Khai báo như sau:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
data_type ** pointer_name; //định nghĩa 1 con trỏ tên là pointer_name và trỏ đến 1 con trỏ kiểu data_type |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int var_i = 2512; | |
int * ptr_i = &var_i; | |
int ** ptr2_i = &ptr_i; | |
*(*ptr2_i) = 3393; |
Các con trỏ cấp 3, cấp 4...đến cấp n cũng được định nghĩa tương tự.
Con trỏ và cấp phát động
Như các phần ở trên thì ta đã khái quát sơ bộ về cấp phát động và con trỏ, phần này sẽ đi sâu hơn về cấp phát động.Đầu tiên ta tua lại 1 chút về cấp phát tỉnh, cách mà ta vẩn thường làm xưa nay.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int a = 10; | |
int arr[1000]; | |
MyClass myClass; |
- Các biến được cấp phát trong stack -> bị giới hạn kích thước.
- Số lượng phần tử của mảng phải luông là 1 hằng số.
- Không thể chủ động giải phóng vùng nhớ khi ta không cần nữa.
- Dữ liệu được lưu trong heap nên có thể lưu được những dữ liệu kích thước lớn.
- Việc cấp phát diển ra lúc thực thi chương trình, nhờ đó kích thước của mảng có thể là 1 biến.
- Thu hồi vùng nhớ dể dàng khi không sử dụng.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
data_type * ptr_arr = new data_type[number_of_elements];// đối với mảng | |
hoặc: | |
data_type * ptr = new data_type; // đối với 1 biến ném ra 1 ngoại lệ nếu cấp phát thất bại |
Sau khi sử dụng xong vùng nhớ đã cấp phát thì ta phải giải phóng nó, sử dụng cú pháp như bên dưới để giải phóng 1 vùng nhớ:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
delete[] ptr_arr;// với ptr_arr là con trỏ trỏ đến 1 mảng | |
delete ptr;// với ptr là con trỏ trỏ đến 1 biến |
Sau khi giải phóng vùng nhớ thì bộ nhớ sẽ như sau:
Như đã thấy thì con trỏ ptr_arr vẩn còn trỏ đến vùng nhớ đã bị giải phóng vì nó vẩn còn lưu lại địa chỉ vùng nhớ đó, do đó sau khi giải phóng vùng nhớ ta nên gán con trỏ lại bằng NULL.
Như thế nào nếu khi cấp phát 1 vùng nhớ và cho con trỏ ptr_arr trỏ đến vùng nhớ đó sau đó lại gán con trỏ ptr_arr bằng NULL hoặc bằng 1 địa chỉ khác mà không giải phóng vùng nhớ:
Như trong hình thì con trỏ ptr_arr không còn trỏ đến vùng nhớ đó nữa nhưng vùng nhớ vẩn còn ở đó, như thế ta không cách nào giải phóng được vùng nhớ đã cấp phát vì ta không còn biết địa chỉ của nó trong bộ nhớ(vì con trỏ ptr_arr không còn lưu địa chỉ của vùng nhớ được cấp phát mà lại lưu 1 giá trị khác và trong trường hợp này ta không có con trỏ thứ 2 trỏ đến vùng nhớ được cấp phát đó), hiện tượng như thế gọi là memory leak.
Việc cấp phát động như con dao 2 lưỡi, nếu biết sử dụng sẽ tận dụng tối đa được tài nguyên bộ nhớ, nhưng nếu sử dụng không đúng cách có thể gấy thất thoát tài nguyên bộ nhớ và có thể gấy crash chương trình.
Code mẫu
Cuối cùng là sample về sử dụng con trỏ, chương trình này đơn giản là cho người dùng nhập vào 1 mảng và xuất ra mảng đã được sắp xếp sử dụng cấp phát động và con trỏ hàm:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <conio.h> | |
typedef bool (* ptr_cmp)(int, int); | |
// hoán đổi giá trị 2 số | |
void swap_elements(int * ptr_a, int * ptr_b) | |
{ | |
int temp = *ptr_a; | |
*ptr_a = *ptr_b; | |
*ptr_b = temp; | |
} | |
// sắp xép mảng tăng dần/giảm dần(phụ thuộc vào order) | |
void sort_arr(int * ptr_arr, int n_arr, ptr_cmp order) | |
{ | |
for(int i = 0; i < n_arr - 1; i++) | |
{ | |
for(int j = i + 1; j < n_arr; j++) | |
{ | |
if(order(ptr_arr[i], ptr_arr[j])) | |
{ | |
swap_elements(&ptr_arr[i], &ptr_arr[j]); | |
} | |
} | |
} | |
} | |
bool inc_compare(int a, int b) | |
{ | |
return a > b; | |
} | |
bool dec_compare(int a, int b) | |
{ | |
return a < b; | |
} | |
void print_arr(int * ptr_arr, int n_arr) | |
{ | |
for(int i = 0; i < n_arr; i++) | |
{ | |
printf("%d ", ptr_arr[i]); | |
} | |
} | |
int main() | |
{ | |
int n = 6; | |
// khởi tạo 1 mảng 6 phần tử trong heap | |
// và ptr_arr trỏ đến đó | |
int * ptr_arr = new int[n]; | |
// nhập dữ liệu cho mảng | |
printf("Nhap %d gia tri cho mang:\n", n); | |
for(int i = 0; i < n; i++) | |
{ | |
printf("Nhap phan tu thu %d: ", i); | |
scanf("%d", ptr_arr + i);// có thể thay thế bằng &ptr_arr[i] | |
} | |
printf("INC: "); | |
// sắp xếp tằng dần sử dụng con trỏ hàm | |
// trỏ đến hàm inc_compare trong bộ nhớ | |
sort_arr(ptr_arr, n, inc_compare); | |
print_arr(ptr_arr, n); | |
printf("\nDEC: "); | |
// sắp xếp giảm dần sử dụng con trỏ hàm | |
// trỏ đến hàm dec_compare trong bộ nhớ | |
sort_arr(ptr_arr, n, dec_compare); | |
print_arr(ptr_arr, n); | |
delete[] ptr_arr; | |
return 0; | |
} |
0 nhận xét :
Post a Comment