C++: Bài 23: Con trỏ (Pointer)

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
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ỏ

kiểu_dữ_liệu  *tên_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ỏ

kiểu_dữ_liệu tên_biến;
kiểu_dữ_liệu  *tên_con_trỏ;
tên_con_trỏ=&tên_biến; //cho con trỏ trỏ tới biến (bản chất là gán địa chỉ của biến cho 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:

int num=5; //→ &num là địa chỉ của num.
int *pnum; //pnum là 1 con trỏ kiểu int
pnum= &num; //pnum chứa địa chỉ biến num

, lúc này *pnum cũng có giá trị là 5.

Hai câu lệnh sau đây là tương đương:

num=100; //gán 100 cho num cũng chính là gán 100 cho *pnum
hoặc:
*pnum=100; //gán 100 cho *pnum cũng chính là gán 100 cho num

Ví dụ 2:

#include<iostream>
using namespace std;

main() {
  int a, *pa;
  a=5;
  pa=&a; //cho pa tr ti 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:

Con trỏ - 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 li
  f=(float*)p; //đúng, nh ép kiu con tr nguyên v kiu float

  return 0;
}

Ví dụ 2:

#include<iostream>
using namespace std;

main() {
  int a;
  char *c;
  c=&a; //xut hin li
  c=(char*)a; //đúng, nh ép kiu

  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]; //mng 10 phn t  int *pV;
  int i;
  pV=&V[0]; //gán đa ch ca phn t đu tiên ca mng V cho con tr pV
  for(i=0;i<10;i++){
    *pV=i; //gán giá tr i cho phn t mà p đang tr đến
    pV++; //p được tăng lên 1 đ ch đến phn 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; //hoc 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.

Con trỏ đến con trỏ trong C ++

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;

  // cha đa ch ca biến var
  ptr = &var;

  // cha đa ch ca con tr ptr
  pptr = &ptr;

  // ly giá tr ca 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 ca biến sec
  cout << "Number of seconds: " << sec << endl;

  return 0;
}

void getSeconds(unsigned long *par) {
  // ly s giây (second) hin thi (ti thi đim hàm được gi)
  *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() {
  // mt mng kiu int gm 5 phn t.
  int balance[5] = {1000, 2, 3, 17, 50};
  double avg;

  // truyn mng balance ti hàm getAverage().
  avg = getAverage(balance, 5);

  // hin th kết qu là giá tr tr v ca 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 to và tr v các s ngu 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 đ gi hàm đã được đnh nghĩa  trên.
main() {
  // to mt con tr tr ti mt 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
» Tiếp: Bài 24: Tham chiếu
« Trước: Bài 22. Chuỗi (String)
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!