Skip to content
Narrow screen resolution Wide screen resolution Auto adjust screen size Increase font size Decrease font size Default font size default color grey color
         
 | 
VNOI - Olympic tin học Việt Nam

Điểm tin VOJ

Số thành viên:6040
Số bài tập:1001
Số bài nộp:722923
Bài nộp hôm nay:0

Top 10 thành viên xuất sắc

HạngThành viênĐiểm
1mr_invincible587.9
2white_cobra418.6
3hieult403.4
4phaleq384.0
5vodanh9x368.2
6con_nha_ngheo352.0
7flash_mt350.2
8darksabers349.8
9yenthanh132345.3
10rockman9x_94343.1
Từ Pascal đến C (3) In E-mail
(17 votes)
Người viết: Ngô Minh Đức   
24/03/2008

Giới thiệu kiểu con trỏ

Trong ngôn ngữ C, kiểu con trỏ được sử dụng khắp mọi nơi. Nếu bạn hiểu rõ kiểu con trỏ thì sẽ dễ dàng thông thạo C hơn. Kiểu con trỏ trong C về cơ bản tương tự như Pascal, nhưng được dùng tự do hơn rất nhiều.

Con trỏ trong C  được dùng theo ba cách chính. Thứ nhất, chúng dùng để tạo ra cấu trúc dữ liệu động: những cấu trúc dữ liệu xây dựng từ những khối bộ nhớ được phân phối trên heap trong thời điểm chạy chương trình. Đây là cách duy nhất thể hiện tường minh trong Pascal. Thứ hai, con trỏ được dùng để điều khiển những tham biến đưa vào các hàm. Và thứ ba, chúng là một phương tiện để truy nhập thông tin chứa trong các mảng; cách này đặc biệt hữu dụng khi làm việc với xâu kí tự. Trong C, mảng và con trỏ có quan hệ mật thiết với nhau.

Trong nhiều trường hợp, lập trình viên C sử dụng con trỏ vì chúng giúp chương trình tối ưu hơn một chút. Tuy nhiên, chúng cũng làm cho chương trình khó hiểu hơn.

Những khái niệm cơ bản về con trỏ

Một biến là một vị trí trên bộ nhớ dùng để chứa giá trị. Ví dụ, khi bạn khai báo biến i có kiểu integer, 4 bytes trong bộ nhớ sẽ được dành ra một bên cho nó. Trong chương trình, bạn dùng tên gọi i để chỉ vùng nhớ này. Ở cấp ngôn ngữ máy, vùng nhớ này có một địa chỉ (memory address).

Con trỏ là một biến chứa địa chỉ vùng nhớ của một biến khác.

Đối với con trỏ, ta có hai phần cần quan tâm. Đó là bản thân con trỏ (chứa địa chỉ), và giá trị mà địa chỉ đó lưu giữ. Nếu chưa từng làm quen với con trỏ trước đây, bạn sẽ cảm thấy hai khái niệm này hơi rắc rối một chút.

Đọan chương trình dưới đây thể hiện một ví dụ về kiểu con trỏ:

#include <stdio.h>

 

void main() 

{   

    int i,j;   

    int *p;   /* con trỏ đến một số nguyên */

     p = &i;   

    *p=5;   

    j=i;   

    printf("%d %d %d\n",i,j,*p); 

}

Dòng int *p khai báo một con trỏ. Bạn có thể tạo một con trỏ đến bất kì kiểu dữ liệu gì: float, struct, char, v.v...

Dòng p=&i; có lẽ hơi mới đối với bạn. Trong C, & được gọi là tóan tử địa chỉ (address operator). Kí hiệu &i có nghĩa là “địa chỉ nhớ của biến i”. Do đó, câu lệnh p=&i; nghĩa là “gán cho p địa chỉ của i”. Sau khi thực hiện lệnh này, p sẽ trỏ đến i. Trước đó, p chứa một địa chỉ ngẫu nhiên nào đó, thông thường nếu sử dụng nó sẽ dẫn đến lỗi “segmentation fault”

Sau khi p trỏ đến i, địa chỉ nhớ của i bây giờ có 2 tên gọi – i*p. Do đó, lệnh *p=5 có nghĩa là i=5. Tiếp tục, lệnh j=i; sẽ đặt j bằng 5, và lệnh printf in ra 5 5 5.

Bạn hãy thử đọan mã sau:

#include <stdio.h>

 

void main() 

{   

    int i,j;   

    int *p;   /* con trỏ đến một số nguyên */

     printf("%d %d\n",p,&i);   

    p = &i;   

    printf("%d %d\n",p,&i); 

}

Đọan chương trình trên sẽ in ra giá trị địa chỉ chứa trong p, cùng với địa chỉ của i. Ban đầu biến p được gán bằng một giá trị nào đó hoặc bằng 0. Địa chỉ của i thường là một số khá lớn. Ví dụ, khi chạy đọan mã này, bạn có thể nhận được kết qủa sau:

0   2147478276

2147478276   2147478276

 

Bạn hãy tiếp tục thử chương trình sau:

#include <stdio.h>

  

void main() 

{   

    int *p;   /* con trỏ đến một số nguyên */

    

    printf("%d\n",*p); 

}

Đọan mã trên sẽ in ra giá trị mà p trỏ đến. Tuy nhiên, p lại chưa được khởi tạo; nó chứa địa chỉ 0. Do đó sẽ xuất hiện lỗi “segmentation fault”, bởi vì bạn đã dùng một con trỏ trỏ đến một vùng nhớ không hợp lệ.

 

 

Sử dụng con trỏ để truyền tham biến

Hầu hết lập trình viên C trước hết cần phải dùng con trỏ cho việc truyền tham biến. Giả sử bạn có một thủ tục đơn giản trong Pascal, có chức năng đổi chỗ hai số nguyên:

program samp; 

var a,b:integer;

 

procedure swap(var i,j:integer);   

var t:integer;   

begin     

    t:=i;     

    i:=j;     

    j:=t;   

end;

  

begin   

    a:=5;   

    b:=10;   

    writeln(a,b);   

    swap(a,b);   

    writeln(a,b); 

end.

 

Thủ tục Swap đổi chỗ được a và b vì nó sử dụng tham biến (var i, j: integer).

Trong C, không có cách chính thức nào để truyền tham biến như vậy; tất cả đều là tham trị. Bạn hãy thực thi đọan mã sau:

#include <stdio.h>  

 

void swap(int i, int j)   

{   

    int t;

    

    t=i;   

    i=j;   

    j=t; 

}

 

void main() 

{   

    int a,b;

    

    a=5; 

    b=10;   

    printf("%d %d\n",a,b);   

    swap(a,b);   

    printf("%d %d\n",a,b); 

}

Bạn sẽ thấy việc đổi chỗ không được thực hiện. Giá trị của ab được đưa vào thủ tục swap, nhưng không có giá trị nào được trả về.

Để thực thi đúng được thủ tục Swap, bạn phải sử dụng con trỏ như sau:

#include <stdio.h>  

 

void swap(int *i, int *j)   

{   

    int t;

    t = *i;   

    *i = *j;   

    *j = t; 

}

 

void main() 

{   

    int a,b;

    a=5;   

    b=10;   

    printf("%d %d\n",a,b);   

    swap(&a,&b);   

    printf("%d %d\n",a,b); 

}

 

Khi gọi thủ tục swap, chúng ta đưa vào các tham trị là địa chỉ của ab. Do đó i trỏ đến aj trỏ đến b. Bây giờ, i* là tên gọi khác của a, j* là tên gọi khác của b. Vì vậy đọan mã đổi chỗ *i*j nghĩa là đổi chỗ ab. Bạn để ý rằng cách dùng con trỏ như vậy cho chúng ta biết được địa chỉ thực của a b, do đó tác động được lên giá trị của chính hai biến này.

Giả sử bạn vô tình quên dấu & khi gọi thủ tục swap: swap(a,b); Điều này sẽ gây ra lỗi “segmentation fault”, bởi vì giá trị của a được truyền thay vì đúng ra phải là địa chỉ của nó. Do đó, i trỏ đến một vùng nhớ không hợp lệ dẫn đến lỗi hệ thống khi *i đựơc sử dụng.

Bây giờ bạn đã hiếu lý do tại so lệnh scanf lại không họat động khi bạn quên dấu & - scanf cũng dùng con trỏ để truyền tham biến. Thiếu &, scanf sẽ lưu giữ một địa chỉ không hợp lệ và gây ra lỗi.

Sử dụng con trỏ cho cấu trúc dữ liệu động

Tương tự như trong Pascal, C cũng cho phép sử dụng cấu trúc dữ liệu động(biến động, mảng động, danh sách liên kết, v.v...). Trước hết, bạn hãy xem đọan mã Pascal sau đây, khai báo một biến động kiểu integer:

program samp; 

var p:^integer; 

begin   

    new(p);   

    p^:=10;   

    writeln(p^);   

    dispose(p); 

end.

 

Đọan chương trình tương đương trong c như sau:

#include <stdio.h>

 

void main() 

{   

    int *p;

     p=(int *) malloc (sizeof(int));   

    *p=10;   

    printf("%d\n",*p);   

    free(p); 

}

Lệnh malloc tương tự như lệnh new trong Pascal. Nó phân phối một khối bộ nhớ với kích thước được chỉ định – trong trường hợp này là sizeof(int) bytes. Lệnh sizeof trong C trả về kích thước, tính theo bytes, của bất kì kiểu dữ liệu nào. Bạn cũng có thể viết là malloc(4), bởi vì sizeof(int) tương đương với 4 bytes trên hầu hết các lọai hệ thống. Tuy nhiên sử dụng sizeof, chương trình sẽ sáng sủa và dễ đọc hơn.

Hàm malloc trả về con trỏ đến khối bộ nhớ được cấp phát. Con trỏ này có kiểu chung nên chúng ta sẽ dùng (int *) để định kiểu lại con trỏ trở thành con trỏ đến số nguyên. Lệnh dispose trong Pascal được thay thế bằng lệnh free trong C. Nó giải phóng khối bộ nhớ được chỉ định.

Ví dụ thứ hai minh họa việc sử dụng biến động cho kiểu record. Đọan mã bằng Pascal như sau:

program samp; 

type rec=record     

        i:integer;     

        f:real;     

        c:char;   

     end; 

var p:^rec; 

begin   

    new(p);   

    p^.i:=10;   

    p^.f:=3.14;   

    p^.c='a';  

    writeln(p^.i,p^.f,p^.c);   

    dispose(p); 

end.

 

Trong ngôn ngữ C, ta có chương trình tương đương:

#include <stdio.h>

 

struct rec

{

    int i;

    float f;

    char c;

};

 

void main()

{

    struct rec *p;

     p=(struct rec *) malloc (sizeof(struct rec));

    (*p).i=10;

    (*p).f=3.14;

    (*p).c='a';

    printf("%d %f %c\n",(*p).i,(*p).f,(*p).c);

    free(p);

}

 

Lưu ý dòng sau:

(*p).i=10;

 

Bạn có thể sẽ thắc mắc tại sao viết như thế này lại không họat động:

*p.i=10;

 

Đó là vì thứ tự ưu tiên thực hiện tóan tử trong C. Trong C, tóan tử . độ ưu tiên cao hơn tóan tử *, do đó bạn phải có thêm dấu ngoặc thì câu lệnh mới hợp lệ.

C cung cấp một cách viết tắt cho câu lệnh (*p).i . Ta có hai câu lệnh sau tương đương với nhau:

(*p).i=10; 

p->i=10;

Bạn sẽ thấy cách viết thứ hai nhiều hơn khi sử dụng các record được trỏ đến bởi một biến pointer.

Bạn cũng cần lưu ý thêm, trong C, NULL thay thế cho nil của Pascal. NULL được khai báo trong thư viện stdio.h, vì vậy bạn cần phải sử dụng thư viện stdio.h khi làm việc với con trỏ.

Sử dụng con trỏ cho mảng

Trong ngôn ngữ C, mảng và con trỏ có quan hệ mật thiết với. Để dùng mảng hiệu qủa, bạn cần biết cách dùng con trỏ đối với chúng.

Bạn hãy bắt đầu với cách sử dụng mảng trong Pascal. Về quan điểm này, C không giống như Pascal, chúng ta sẽ thấy sự tương phản. Sau đây là một ví dụ về mảng trong Pascal:

program samp; 

const max=9; 

var a,b:array[0..max] of integer;   

    i:integer; 

begin   

    for i:=0 to max do     

        a[i]:=i;   

    b:=a; 

end.

 

Các phần tử của mảng a được khởi tạo, và tất cả các phần tử của a được copy vào b. Do đó mảng a và mảng b trở thành giống nhau. Bạn hãy so sánh với phiên bản chương trình bằng C sau:

#define MAX 10

 

void main() 

{   

    int a[MAX];   

    int b[MAX];   

    int i;

     for(i=0; i<MAX; i++)

        a[i]=i;  

    b=a; 

}

 

Bạn sẽ thấy C sẽ không cho phép biên dịch đọan mã này. Nếu bạn muốn copy mảng a vào b, bạn có thể phải làm như sau:

for (i=0; i<MAX; i++)     

    a[i]=b[i];

 

Hoặc ngắn gọn hơn:

for (i=0; i<MAX; a[i]=b[i], i++);

 

Tốt hơn nữa, bạn hãy sử dụng công cụ memcpy trong thư viện string.h

Mảng trong C khác biệt ở chỗ, hai biến a b bản thân nó không phải là mảng mà chỉ là những con trỏ cố định đến mảng. Chúng trỏ đến khối bộ nhớ chứa mảng. Chúng chứa địa chỉ thực của hai mảng, nhưng bởi vì chúng là con trỏ cố định, nên địa chỉ của chúng không thế thay đổi. Do đó, dòng lệnh a=b không được thực thi.

 

Bởi vì ab là con trỏ, nên bạn có thể làm nhiều điều với chúng. Bạn hãy thử đọan mã sau:

#define MAX 10

 

void main() 

{   

    int a[MAX];    

    int b[MAX];   

    int i;   

    int *p,*q;

     

    for(i=0; i<MAX; i++); 

        a[i]=i;  

    p=a;   

    printf("%d\n",*p); 

}

 

Lệnh p=a; họat động bởi vì a là một con trỏ. Về bản chất, a trỏ đến phần tử 0 của mảng. Phần tử này là một số nguyên, do đó a trỏ đến một số nguyên. Do đó khai báo p là một con trỏ trỏ đến số nguyên rồi gán p=a; được C chấp nhận. Một cách khác hòan tòan tương tự là viết p=&a[0]; bởi vì a chứa địa chỉ của a[0], nên a tương đương với &a[0].

Bây giờ p trỏ đến phần tử 0 của mảng a, bạn có thể làm một vài điều lạ khác nữa. Biến a là địa chỉ cố định và không thể thay đổi, nhưng p thì không bắt buộc như vậy. C cho phép bạn duyệt mảng thông qua các phép tóan con trỏ. Ví dụ, nếu bạn viết p++; trình biên dịch biết rằng p là con trỏ trỏ đến số nguyên nên nó sẽ tăng p lên một số lượng bytes tương ứng để trỏ đến phần tử kế tiếp của mảng. Nếu p mà trỏ đến một cấu trúc có 100 bytes đi nữa, thì lệnh p++; cũng sẽ di chuyển p lên 100 bytes;

Bạn có thể copy mảng a vào b dùng con trỏ như đọan mã dưới đây:

p=a;   

q=b;   

for (i=0; i<MAX; i++)    

{     

    *q = *p;     

    q++;     

    p++;   

}

 

Bạn có thể viết gọn lại như sau:

p=a;   

q=b;   

for (i=0; i<MAX; i++)    

    *q++ = *p++;

Bạn vẫn có thể viết gọn hơn nữa:

for (p=a,q=b,i=0; i<MAX; *q++ = *p++, i++);

 

Giả sử ta cần một hàm dump nhận tham số là một mảng số nguyên và in nội dung của mảng ra stdout. Có hai cách để viết thủ tục này:

void dump(int a[],int nia) 

{   

    int i;    

     for (i=0; i<nia; i++)     

        printf("%d\n",a[i]); 

}

 

Hoặc:

void dump(int *p,int nia) 

{   

    int i;    

     for (i=0; i<nia; i++)     

        printf("%d\n",*p++); 

}

 

Tham số nia (number in array) cần thiết bởi vì kích thước của mảng chưa được biết trước.

 
< Trước   Tiếp >