C++: Bài 23: Con trỏ (Pointer)
Giải phóng thời gian, khai phóng năng lực
Tổng quan
Mỗi biến trong ngôn ngữ C++ đều có một tên, và tương ứng với nó là một vùng nhớ dùng để chứa giá trị của nó. Tuỳ theo kiểu dữ liệu của biến mà vùng nhớ dành cho biến có kích thước khác nhau. Ðịa chỉ của biến là số thứ tự của Byte đầu tiên tương ứng với biến đó. Ðịa chỉ của các biến có kiểu khác nhau là khác nhau, chẳng hạn như địa chỉ của hai biến kiểu int liên tiếp cách nhau 2 Byte, biến kiểu float là 4 Byte, biến kiểu double là 8 Byte.
Con trỏ có thể dùng để chứa địa chỉ của biến khác hoặc có thể chứa địa chỉ của hàm (con trỏ hàm). Do có nhiều loại địa chỉ nên cũng có nhiều loại biến con trỏ, ví dụ như con trỏ kiểu int dùng để chứa địa chỉ của biển kiểu int, con trỏ kiểu float dùng để chứa địa chỉ của biến kiểu float.
Muốn sử dụng được con trỏ (pointer), trước tiên phải có được địa chỉ của biến mà ta cần quan tâm bằng phép toán lấy địa chỉ & (đặt & trước tên biến). Kết quả của phép lấy địa chỉ & là con trỏ sẽ chứa địa chỉ của biến gán. Khi đó nếu biến con trỏ thay đổi giá trị thì biến đó cũng sẽ thay đổi giá trị.
Sau đây là một số tình huống phổ biến mà con trỏ được dùng tới: trả về nhiều hơn một giá trị từ một hàm, dùng con trỏ để truyền mảng giữa các hàm (sẽ thuận tiện hơn), làm việc với các phần tử của mảng thông qua con trỏ thay vì truy xuất trực tiếp tới chúng, cấp phát và truy xuất vùng nhớ động.
Con trỏ trong C++ là easy và fun. Một số tác vụ C++ được giải quyết dễ dàng hơn khi sử dụng con trỏ, một số tác vụ khác như cấp phát vùng nhớ động thì cần phải dùng đến con trỏ mới giải quyết được.
Cú pháp khai báo con trỏ
, lúc này, *tên_con_trỏ gọi là biến con trỏ (hay biến trỏ), nó có tác dụng như một biến thông thường, tức là nó dùng để lưu giá trị, còn tên_con_trỏ chính là con trỏ và nó được dùng để lưu địa chỉ của biến khác. Ví dụ:
int *p;
, thì *p là biến trỏ, còn p là con trỏ.
Cú pháp sử dụng con trỏ
, lúc này ta hoàn toàn có thể sử dụng biến trỏ *Tên_con_trỏ thay cho Tên_biến, mọi thao tác trên biến trỏ *Tên_con_trỏ đều tương đương với thao tác trên Tên_biến.
Ví dụ 1:
, lúc này *pnum cũng có giá trị là 5.
Hai câu lệnh sau đây là tương đương:
Ví dụ 2:
#include<iostream> using namespace std; main() { int a, *pa; a=5; pa=&a; //cho pa trỏ tới biến a *pa=a; //phép gán đúng cout<<"Dia chi va gia tri cua bien a la "<<&a<<" va "<<a<<endl; cout<<"Sau khi pa tro toi a thi pa = "<<pa<<" va *pa = "<<*pa; return 0; }
Một kết quả demo:
→ Như vậy, khi có lệnh gán pa=&a; → pa sẽ chứa địa chỉ của biến a, còn *pa sẽ chứa giá trị của biến a.
Tính toán trên biến con trỏ
Hai biến con trỏ cùng kiểu có thể gán cho nhau.
Ví dụ 1:
#include<iostream> using namespace std; main() { int a, *p, *q; float *f; a = 5; p=&a; q=p; //đúng f=p; //sẽ đưa ra lỗi f=(float*)p; //đúng, nhờ ép kiểu con trỏ nguyên về kiểu float return 0; }
Ví dụ 2:
#include<iostream> using namespace std; main() { int a; char *c; c=&a; //xuất hiện lỗi c=(char*)a; //đúng, nhờ ép kiểu return 0; }
Một con trỏ có thể được cộng, trừ với một số nguyên (int, long) để cho kết quả là một con trỏ.
Ví dụ 3:
#include<iostream> using namespace std; main() { int a, *p, * p10; a=5; p=&a; p10 = p + 10; //đúng return 0; }
Ví dụ 4:
#include<iostream> using namespace std; main() { int V[10]; //mảng 10 phần tử int *pV; int i; pV=&V[0]; //gán địa chỉ của phần tử đầu tiên của mảng V cho con trỏ pV for(i=0;i<10;i++){ *pV=i; //gán giá trị i cho phần tử mà p đang trỏ đến pV++; //p được tăng lên 1 để chỉ đến phần tử kế tiếp } return 0; }
→ Kết quả: V[0]=0, V[1]=1, ..., V[9]=9
Chú ý:
• Phép trừ 2 con trỏ cho kết quả là một số int biểu thị khoảng cách (số phần tử) giữa 2 con trỏ đó.
• Phép cộng 2 pointer là không hợp lệ, pointer không được nhân chia với 1 số nguyên hoặc nhân chia với nhau.
• p=NULL → Con trỏ p không trỏ đến đâu cả.
Con trỏ và mảng
Con trỏ và mảng là tương đương nhau, nghĩa là con trỏ có thể thay thế cho mảng và ngược lại, mảng cũng có thể thay thế cho con trỏ.
Mảng một chiều tương ứng với con trỏ có 1 sao (*), ví dụ: a[] tương ứng với *a; mảng 2 chiều tương ứng với con trỏ có 2 sao (**), ví dụ: a[][] tương ứng với **a.
Chú ý: Không nên sử dụng con trỏ khi nó chưa được khởi gán vì khi đó nó sẽ trỏ đến vị trí bất kỳ, đơn giản nhất trong trường hợp này là bạn có thể gán NULL cho con trỏ.
Ví dụ:
int a, *p; scanf ("%d", p); //không nên
→ Ta nên thay bằng các lệnh:
int a, *p; p=&a; //hoặc p=NULL scanf ("%d", p); //đúng
Con trỏ NULL
Luôn luôn là một phương pháp hay để gán con trỏ NULL cho một biến con trỏ trong trường hợp bạn không có địa chỉ chính xác để gán cho. Điều này được thực hiện tại thời điểm khai báo biến. Một con trỏ được gán NULL thì nó được gọi là con trỏ null.
Con trỏ NULL là một hằng số có giá trị bằng 0 được xác định trong một số thư viện chuẩn, bao gồm cả iostream. Xét chương trình sau:
#include<iostream> using namespace std; main() { int *ptr = NULL; cout << "Gia tri cua ptr la: " << ptr ; return 0; }
Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:
Gia tri cua ptr la: 0
Trên hầu hết các hệ điều hành, các chương trình không được phép truy cập bộ nhớ ở địa chỉ 0 vì bộ nhớ đó được hệ điều hành dành riêng. Tuy nhiên, địa chỉ bộ nhớ 0 có ý nghĩa đặc biệt; nó báo hiệu rằng con trỏ không nhằm mục đích trỏ đến vị trí bộ nhớ có thể truy cập được. Nhưng theo quy ước, nếu một con trỏ chứa giá trị NULL (không) thì nó được coi là trỏ đến không.
Để kiểm tra con trỏ null, bạn có thể sử dụng câu lệnh if như sau:
if(ptr) // true nếu ptr không NULL if(!ptr) // true nếu ptr NULL
Do đó, nếu tất cả các con trỏ không sử dụng thì đều được cung cấp giá trị NULL và bạn tránh sử dụng con trỏ null, bạn có thể tránh việc vô tình sử dụng sai con trỏ chưa được khởi tạo. Nhiều khi, các biến chưa khởi tạo chứa một số giá trị rác và việc gỡ lỗi chương trình trở nên khó khăn.
Con trỏ tới con trỏ
Một con trỏ tới một con trỏ là một dạng của nhiều hướng hoặc một chuỗi con trỏ. Thông thường, một con trỏ chứa địa chỉ của một biến. Khi chúng ta định nghĩa một con trỏ tới một con trỏ, con trỏ đầu tiên chứa địa chỉ của con trỏ thứ hai, con trỏ này sẽ trỏ đến vị trí chứa giá trị thực như hình dưới đây.
Một biến là một con trỏ đến một con trỏ phải được khai báo bằng cách đặt một dấu hoa thị bổ sung trước tên của nó. Ví dụ, sau đây là phần khai báo một con trỏ đến một con trỏ kiểu int:
int **var;
Khi một giá trị đích được con trỏ trỏ tới một con trỏ gián tiếp, việc truy cập giá trị đó yêu cầu phải áp dụng toán tử dấu hoa thị hai lần, như được minh họa bên dưới trong ví dụ sau:
#include <iostream> using namespace std; main() { int var; int *ptr; int **pptr; var = 3000; // chứa địa chỉ của biến var ptr = &var; // chứa địa chỉ của con trỏ ptr pptr = &ptr; // lấy giá trị của var, *ptr và **pptr cout << "Gia tri cua bien var: " << var << endl; cout << "Gia tri cua *ptr: " << *ptr << endl; cout << "Gia tri cua **pptr: " << **pptr << endl; return 0; }
Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:
Gia tri cua bien var :3000 Gia tri cua *ptr :3000 Gia tri cua **pptr :3000
Truyền con trỏ tới hàm
C++ cho phép bạn truyền con trỏ đến một hàm. Để làm như vậy, chỉ cần khai báo tham số của hàm là kiểu con trỏ.
Dưới đây là ví dụ đơn giản trong đó chúng ta truyền một con trỏ kiểu unsigned long cho một hàm và trong hàm đó sẽ thay đổi giá trị của biến con trỏ.
#include <iostream> #include <ctime> using namespace std; void getSeconds(unsigned long *par); main() { unsigned long sec; getSeconds(&sec); // In ra giá trị của biến sec cout << "Number of seconds: " << sec << endl; return 0; } void getSeconds(unsigned long *par) { // lấy số giây (second) hiện thời (tại thời điểm hàm được gọi) *par = time( NULL ); return; }
Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả dạng như sau:
Number of seconds: 1294450468
Hàm có thể chấp nhận một con trỏ, cũng có thể chấp nhận một mảng như được thể hiện trong ví dụ sau:
#include <iostream> using namespace std; // khai báo hàm: double getAverage(int *arr, int size); main() { // một mảng kiểu int gồm 5 phần tử. int balance[5] = {1000, 2, 3, 17, 50}; double avg; // truyền mảng balance tới hàm getAverage(). avg = getAverage(balance, 5); // hiển thị kết quả là giá trị trả về của hàm getAverage() cout << "Gia tri trung binh la: " << avg << endl; return 0; } double getAverage(int *arr, int size) { int i, sum = 0; double avg; for (i = 0; i < size; ++i) { sum += arr[i]; } avg = double(sum) / size; return avg; }
Khi đoạn mã trên được biên dịch cùng nhau và được thực thi, nó sẽ tạo ra kết quả sau:
Average value is: 214.4
Trả về con trỏ từ hàm
Như chúng ta đã thấy ở trên về cách C++ cho phép trả về một mảng từ một hàm, thì tương tự C++ cũng cho phép bạn trả về một con trỏ từ một hàm. Để làm điều này thì bạn sẽ phải khai báo một hàm trả về một con trỏ như trong ví dụ sau:
int* myFunction() { . . . }
Điểm thứ hai cần nhớ là, không nên trả lại địa chỉ của một biến cục bộ bên ngoài hàm, vì như vậy bạn sẽ phải xác định biến cục bộ là biến tĩnh.
Bây giờ, hãy xem xét hàm sau, hàm này sẽ tạo ra 10 số ngẫu nhiên và trả về chúng bằng cách sử dụng tên mảng đại diện cho một con trỏ, tức là địa chỉ của phần tử mảng đầu tiên.
#include<iostream> #include<ctime> #include<cstdlib> using namespace std; // hàm này tạo và trả về các số ngẫu nhiên. int * getRandom( ) { static int r[10]; // set the seed srand( (unsigned)time( NULL ) ); for (int i = 0; i < 10; ++i) { r[i] = rand(); cout << r[i] << endl; } return r; } // hàm main để gọi hàm đã được định nghĩa ở trên. main() { // tạo một con trỏ trỏ tới một số nguyên int. int *p; p = getRandom(); for ( int i = 0; i < 10; i++ ) { cout << "*(p + " << i << ") : "; cout << *(p + i) << endl; } return 0; }
Khi đoạn mã trên được biên dịch cùng nhau và được thực thi, nó tạo ra kết quả như sau:
11490 8872 23379 3280 10640 13463 20701 24679 28520 2423 *(p + 0) : 11490 *(p + 1) : 8872 *(p + 2) : 23379 *(p + 3) : 3280 *(p + 4) : 10640 *(p + 5) : 13463 *(p + 6) : 20701 *(p + 7) : 24679 *(p + 8) : 28520 *(p + 9) : 2423
Giải phóng thời gian, khai phóng năng lực