Chương 08 – Con Trỏ

 

Khái niệm

Con trỏ là một biến dùng để chứa địa chỉ. Vì có nhiều loại địa chỉ nên cũng có nhiều kiểu con trỏ tương ứng. Kiểu con trỏ int dùng để chứa địa chỉ biến kiểu int. Tương tự ta có con trỏ kiểu float, kiểu double,…
Cũng như với 1 biến bất kỳ nào khác, con trỏ cần được khai báo trước khi sử dụng.
Toán tử lấy địa chỉ (&)
Địa chỉ: Khi khai báo biến, máy sẽ cấp phát cho biến một khoảng nhớ. Địa chỉ của biến là số thứ tự của byte đầu tiên trong một dãy các byte liên tiếp mà máy dành cho biến (các byte được đánh số từ 0).
Máy phân biệt các loại địa chỉ như các biến. Ví dụ như: địa chỉ kiểu int, kiểu float,…
Vào thời điểm mà chúng ta khai báo một biến thì nó phải được lưu trữ trong một vị trí cụ thể trong bộ nhớ. Chúng ta không quyết định nơi nào biến đó được đặt, điều đó làm tự động bởi trình biên dịch và hệ điều hành. Nhưng khi hệ điều hành đã gán một địa chỉ cho biến thì chúng ta cần biết biến đó được lưu trữ ở đâu. Điều này được thực hiện bằng cách đặt trước tên biến một dấu và (&).
Ví dụ: x = &y;
Giải thích: Gán cho biến x địa chỉ của biến y vì khi đặt trước tên biến y dấu & ta không nói đến nội dung của biến nữa mà chỉ nói đến địa chỉ của nó trong bộ nhớ.
Giả sử biến y được đặt trọng ô nhớ có địa chỉ là 1202 và có các câu lệnh như sau:
            y = 30;
            z = y;
            x = &y;
            Kết quả: z = 30; x = 1202;
Chú ý: Có thể định nghĩa con trỏ như sau: Những biến lưu trữ địa chỉ của biến khác được gọi là con trỏ. Ở ví dụ trên, biến x là con trỏ.
Toán tử tham chiếu (*)
Bằng cách sử dụng con trỏ chúng ta có thể truy xuất trực tiếp đến giá trị được lưu trữ trong biến được trỏ bởi nó bằng cách đặt trước tên biến con trỏ một dấu (*).
Ví dụ: Giả sử biến y được đặt trong ô nhớ có địa chỉ là 1202 và có các câu lệnh như sau:
            y = 30;
            z = y;
            x = &y;
            t = *y;
            Kết quả: biến t sẽ mang giá trị là 30.
Khai báo biến kiểu con trỏ
Vì con trỏ có khả năng tham chiếu trực tiếp đến giá trị mà chúng trỏ tới nên cần thiết phải chỉ rõ kiểu dữ liệu nào mà một biến con trỏ trỏ tới khi khai báo.
Cấu trúc khai báo:
            Kiểu_dữ_liệu       *Tên_con_trỏ;
Chú ý: kiểu dữ liệu ở đây là kiểu dữ liệu được trỏ tới, không phải là kiểu của bản thân con trỏ.
Ví dụ:
            int    *x; //Khai báo con trỏ x kiểu int
            float   *t; //Khai báo con trỏ t kiểu float
Chú ý: dấu (*) khi khai báo biến kiểu con trỏ chỉ có nghĩa rằng đó là một con trỏ, không liên quan đến toán tử tham chiếu.
Ví dụ đơn giản về sử dụng con trỏ: khai báo, sử dụng toán tử tham chiếu, toán tử lấy địa chỉ:
#include<iostream.h>
main()
{
int     x1 = 5, x2 = 15;
int    *y;
          y = &x1;
          *y = 10;
          y = &x2;
          *y = 20;
         cout<<“Gia tri 1 = “<<x1<<“\n Gia tri 2 = “<<x2;
return 0;
}
Kết quả: x1 = 10; x2 = 20;
Các phép toán
Phép gán
Phép gán chỉ nên thực hiện phép gán cho con trỏ cùng kiểu. Muốn gán các con trỏ khác kiểu phải dùng phép ép kiểu như ví dụ sau:
            int       x;
            char    *pc;
            pc = (char*)(&x); //ép kiểu
Phép tăng giảm địa chỉ
Ví dụ 1: Các câu lệnh: float   x[30], *px;
                       px  = &x[10];
Cho biết px là con trỏ float trỏ tới phần tử x[10]. Kiểu địa chỉ float là kiểu địa chỉ 4 byte (mỗi giá trị float chứa trong 4 byte), nên các phép tăng giảm địa chỉ được thực hiện trên 4 byte. Nói cách khác:
            px + i trỏ tới phần tử x[10+i];
            px – i trỏ tới phần tử x[10-i];
Ví dụ 2: float  b[40][50];
            b trỏ tới đầu dòng thứ nhất  (phần tử b[0][0];
            b + 1 trỏ tới đầu dòng thứ hai (phần tử b[1][0];
….
Phép truy cập bộ nhớ
Có nguyên tắc là con trỏ float truy nhập tới 4 byte, con trỏ int truy nhập 2 byte, con trỏ char truy nhập 1 byte.
Ví dụ:
            float    *pf;
            int   *pi;
            char  *pc;
Khi đó: Nếu pf trỏ đến byte thứ 10001, thì *pf biểu thị vùng nhớ 4 byte liên tiếp từ byte 10001 đến 10004.
Nếu pi trỏ đến byte thứ 10001, thì *pi biểu thị vùng nhớ 2 byte là 10001 và 10002.
Nếu pc trỏ đến byte thứ 10001, thì *pc biểu thị vùng nhớ 1 byte là byte 10001.
Phép so sánh
Cho phép so sánh các con trỏ cùng kiểu, ví dụ nếu p1 và p2 là 2 con trỏ float thì:
            p1<p2 nếu địa chỉ p1 trỏ tới thấp hơn địa chỉ p2 trỏ tới.
            p1=p2 nếu địa chỉ p1 trỏ tới bằng địa chỉ p2 trỏ tới.
            p1>p2 nếu địa chỉ p1 trỏ tới cao hơn địa chỉ p2 trỏ tới.
Con trỏ hằng
Ví dụ có khai báo sau:
            int   a[20];
            int   *p;
            Lệnh sau sẽ hợp lệ: p = a;
ở đây p và a là tương đương và chúng có cùng thuộc tính, sự khác biệt duy nhất là chúng ta có thể gán một giá trị khác cho con trỏ p trong khi a luôn trỏ đến phần tử đầu tiên trong số 20 phần tử kiểu int mà nó được định nghĩa. Vì vậy không giống như p – đó là một biến con trỏ bình thường, a, là một con trỏ hằng. Lệnh gán sau đây là không đúng: a = p; bởi vì a là một mảng (con trỏ hằng) và không có giá trị nào có thể được gán cho các hằng.
Con trỏ mảng
Trong thực tế, tên của một mảng tương đương với địa chỉ phần tử đầu tiên của nó, giống như một con trỏ tương đương với địa chỉ của phần tử đầu tiên mà nó trỏ tới, vì vậy thực tế chúng hoàn toàn như nhau. Ví dụ, cho hai khai báo sau:
            int  numbers[20];
            int *p;
Lệnh sau sẽ hợp lệ:
            p = numbers;
Ở đây p và numbers là tương đương và chúng có cũng thuộc tính, sự khác biệt duy nhất là chúng ta có thể gán một giá trị khác cho con trỏ p trong khi numbers luôn trỏ đến phần tử đầu tiên trong số 20 phần tử kiểu int mà nó được định nghĩa với. Vì vậy, không giống như p – đó là một biến con trỏ bình thường, numbers là một con trỏ hằng. Lệnh gán sau đây là không hợp lệ:
            numbers = p;
bởi vì numbers là một mảng (con trỏ hằng) và không có giá trị nào có thể được gán cho các hằng.
#include <iostream.h>
int main ()
{
            int  a[5];
            int *p;
            p = a;  *p = 10;
            p++;  *p = 20;
            p = &a[2];  *p = 30;
            p = a + 3;  *p = 40;
            p = a;  *(p+4) = 50;
            for (int n=0; n<5; n++)
            cout << a[n] << “, “;
            return 0;
}
 
Khởi tạo con trỏ
Khi khai báo con trỏ có thể chúng ta sẽ muốn chỉ định rõ ràng chúng sẽ trỏ tới biến nào:
            int    number;
            int   *tommy = &number;
là tương đương với:
            int    number;
            int   *tommy;
            tommy = &number;
Trong một phép gán con trỏ chúng ta phải luôn luôn gán địa chỉ mà nó trỏ tới chứ không phải là giá trị mà nó trỏ tới. Cần phải nhớ rằng khi khai báo một biến con trỏ, dấu sao (*) được dùng để chỉ ra nó là một con trỏ, và hoàn toàn khác với toán tử tham chiếu. Đó là hai toán tử khác nhau mặc dù chúng được viết với cùng một dấu. Vì vậy, các câu lệnh sau là không hợp lệ:
            int number;
            int  *tommy;
            *tommy = &number;
Như đối với mảng, trình biên dịch cho phép chúng ta khởi tạo giá trị mà con trỏ trỏ tới bằng giá trị hằng vào thời điểm khai báo biến con trỏ:
            char * terry = “hello”;
trong trường hợp này một khối nhớ tĩnh được dành để chứa “hello” và một con trỏ trỏ tới kí tự đầu tiên của khối nhớ này (đó là kí tự h’) được gán cho terry. Nếu “hello” được lưu tại địa chỉ 1702, lệnh khai báo trên có thể được hình dung như thế này:
 
Cần phải nhắc lại rằng terry mang giá trị 1702 chứ không phải là ‘h’ hay “hello”.
Biến con trỏ terry trỏ tới một xâu kí tự và nó có thể được sử dụng như là đối với một mảng (hãy nhớ rằng một mảng chỉ đơn thuần là một con trỏ hằng). Ví dụ, nếu chúng ta muốn thay kí tự ‘o’ bằng một dấu chấm than, chúng ta có thể thực hiện việc đó bằng hai cách:
            terry[4] = ‘!’;
            *(terry+4) = ‘!’;
Hãy nhớ rằng viết terry[4] là hoàn toàn giống với viết *(terry+4) mặc dù biểu thức thông dụng nhất là cái đầu tiên. Với một trong hai lệnh trên xâu do terry trỏ đến sẽ có giá trị như sau:
 
Con trỏ trỏ tới con trỏ
C++ cho phép sử dụng các con trỏ trỏ tới các con trỏ khác giống như là trỏ tới dữ liệu. Để làm việc đó chúng ta chỉ cần thêm một dấu sao (*) cho mỗi mức tham chiếu.
            char  a;
            char *b;
            char **c;
            a = ‘z’;
            b = &a;
            c = &b;
giả sử rằng a,b,c được lưu ở các ô nhớ 7230, 8092 and 10502, ta có thể mô tả đoạn mã trên như sau:
                         a                     b                      c
                        “z”      ß        7230    ß        8092
                        7230                8092                10502
Điểm mới trong ví dụ này là biến c, chúng ta có thể nói về nó theo 3 cách khác nhau, mỗi cách sẽ tương ứng với một giá trị khác nhau:
            c là một biến có kiểu char ** mang giá trị 8092;
            *c là một biến có kiểu char * mang giá trị là 7230;
            **c là một biến có kiểu char mang giá trị ‘z’;
Con trỏ không kiểu
Con trỏ không kiểu là một loại con trỏ đặc biệt. Nó có thể trỏ tới bất kì loại dữ liệu nào, từ giá trị nguyên hoặc thực cho tới một xâu kí tự. Hạn chế duy nhất của nó là dữ liệu được trỏ tới không thể được tham chiếu tới một cách trực tiếp (chúng ta không thể dùng toán tử tham chiếu * với chúng) vì độ dài của nó là không xác định và vì vậy chúng ta phải dùng đến toán tử chuyển kiểu dữ liệu hay phép gán để chuyển con trỏ không kiểu thành một con trỏ trỏ tới một loại dữ liệu cụ thể.
Một trong những tiện ích của nó là cho phép truyền tham số cho hàm mà không cần chỉ rõ kiểu.
Ví dụ:
#include <iostream.h>
void increase (void* data, int type)
{
       switch (type)
  {
       case sizeof(char) : (*((char*)data))++; break;
       case sizeof(short): (*((short*)data))++; break;
       case sizeof(long) : (*((long*)data))++; break;
  }
}
int main ()
{
  char a = 5;
  short b = 9;
  long c = 12;
  increase (&a,sizeof(a));
  increase (&b,sizeof(b));
  increase (&c,sizeof(c));
  cout << (int) a << “, ” << b << “, ” << c;
  return 0;
}
Kết quả6, 10, 13
sizeof là một toán tử của ngôn ngữ C++, nó trả về một giá trị hằng là kích thước tính bằng byte của tham số truyền cho nó, ví dụ sizeof(char) bằng 1 vì kích thước của char là 1 byte.
Con trỏ hàm
C++ cho phép thao tác với các con trỏ hàm. Tiện ích này cho phép truyền một hàm như là một tham số đến một hàm khác. Để có thể khai báo một con trỏ trỏ tới một hàm chúng ta phải khai báo nó như là khai báo mẫu của một hàm nhưng phải bao trong một cặp ngoặc đơn () tên của hàm và chèn dấu sao (*) đằng trước.
Ví dụ:
#include <iostream.h>
int addition (int a, int b)
return (a+b); }
int subtraction (int a, int b)
return (a-b); }
int (*minus)(int,int) = subtraction;
int operation (int x, int y, int (*functocall)(int,int))
{
  int g;
  g = (*functocall)(x,y);
  return (g);
}
int main ()
{
  int m,n;
  m = operation (7, 5, &addition);
  n = operation (20, m, minus);
  cout <<n;
  return 0;
}
Trong ví dụ này, minus là một con trỏ toàn cục trỏ tới một hàm có hai tham số kiểu int, con trỏ này được gám để trỏ tới hàm subtraction, tất cả đều trên một dòng:
int (* minus)(int,int) = subtraction;
Advertisements
By dungkcna113 Posted in C/C++

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s