Con trỏ trong Ngôn ngữ lập trình C – Phần 2 : Bài 11

Con trỏ trong Ngôn ngữ lập trình C – Phần 2 : Bài 11
access_time 10/8/2019 12:00:00 AM
person Nguyễn Mạnh Hùng

Con trỏ trong Ngôn ngữ lập trình C – Phần 2 : Bài 11

1. Sử dụng con trỏ

1.1. Gọi bằng địa chỉ

Một trong những ứng dụng điển hình của con trỏ đó là việc hỗ trợ gọi thông qua tham chiếu. Tuy nhiên ngôn ngữ lập trình C, không hỗ trợ việc bằng cách sử dụng gọi thông qua tham chiếu như các ngôn ngữ PASCAL, FORTRAN (Lưu ý là C++ hỗ trợ vấn đề này). Thông thường khi viết các hàm trong chương trình, các hàm sẽ bao gồm các tham số đi kèm, trong ngôn ngữ lập trình C thuần chỉ hỗ trợ việc sử dụng truyền giá trị cho hàm thông qua giá trị của tham số (Hay còn gọi là tham trị).

Việc sử dụng tham trị để truyền giá trị của hàm thực chất là sử dụng bảng dữ liệu copy của các tham số truyền vào, điều này làm cho các giá trị thực sự của các biến khi làm tham số truyền vào cho hàm là không thay đổi giá trị khi ra khỏi phạm vi của hàm (Các bạn xem lại bài giới thiệu hàm trong Ngôn ngữ lập trình C).

Muốn thay đổi giá trị của các biến được sử dụng tham tham số khi truyền vào cho hàm thì chúng ta phải trỏ trực tiếp tới các địa chỉ của các biến được sử dụng làm tham số truyền vào cho hàm này, cơ chế đó người ta gọi là cơ chế truyền giá trị thông qua tham chiếu.

Vì C thuần không hỗ trợ truyền tham chiếu khi sử dụng hàm, tuy nhiên lập trình viên hoàn toàn có thể sử dụng con trỏ để thực hiện. Điều đó có nghĩa là sử dụng con trỏ để làm tham số truyền vào cho hàm, khi đó trình biên dịch khi thực hiện lời gọi hàm, thì nó có thể truy vấn đến vùng bộ nhớ được sử dụng bởi con trỏ và làm thay đổi nội dung mà con trỏ đó chứa. Vậy khi thoát khỏi phạm vi của hàm thì giá trị của biến truyền vào hàm đó sẽ thay đổi. Cách thức này được gọi là truyền tham số thông qua địa chỉ.

Ví dụ :


Hình số 1: Sử dụng tham số con trỏ với hàm

Kết quả chạy chương trình giá trị của x, y đã được tráo đổi cho nhau.

1.2. Hàm trả về nhiều giá trị.

Hàm thường chỉ trả về một giá trị và khi đối số được truyền bằng giá trị, hàm được gọi là không thể thay đổi các giá trị các tham số, các lập trình viên có thể sử dụng con trỏ để giải quyết bài toàn viết một hàm trả về nhiều giá trị.

Ví dụ : Viết một hàm tính diện tích và chu vi hình tròn với bán kính nhập vào từ bàn phím. Sử dụng con trỏ để thực hiện : float compute(float r, float *p); hàm compute được xây dựng với 2 mục đích

+ Giá trị trả về là diện tích hình tròn;

+ Sử dụng tham số con trỏ p để lưu giá trị của chu vi đường tròn.

Code mô tả hàm : float compute(float r, float *p); như sau :


Hình số 2: Hàm trả về nhiều giá trị

Khi nhập giá trị bán kính là 5.5; Kết quả in ra diện dich là 95.03 và chi vi là 34.55;

Vậy các lập trình viên hoàn toàn có thể áp dụng cơ chế sử dụng con trỏ là tham số truyền vào cho hàm để thực hiện yêu cầu viết một hàm trả về nhiều giá trị.

1.3. Hàm trả về Con trỏ

Ngôn ngữ lập trình C cho phép các Hàm có thể trả về con trỏ. Khi một hàm trả về con trỏ điều đó có nghĩa là nó sẽ trỏ đến vùng dữ liệu khi thực hiện gọi hàm hoặc một biến toàn cục.

Ví dụ: Viết hàm int *pointMax(int *x, int *y)  thực hiện so sánh 2 số nguyên, kết quả trả về là con trỏ trỏ đến địa chỉ của tham số chứa giá trị lớn hơn.

Minh họa Code hàm  int *pointMax(int *x, int *y);


Hình số 3 : Hàm trả về con trỏ

Khi nhập giá trị a =3; b = 6; Kết quả sẽ in ra giá trị *p = 6;

Khi hàm pointMax được gọi bằng cách sử dụng cơ chế truyền dữ liệu thông qua địa chỉ của 2 tham số là a,b; Nếu giá trị a > b thì hàm pointMax sẽ trả về địa chỉ của biến a và ngược lại sẽ trả về địa chỉ của biến b;

2. Mảng và con trỏ

Trong lập trình C, con trỏ và mảng có liên quan không thể tách rời tuy nhiên nó cũng có những điểm khác biệt riêng.

2.1. Mảng một chiều và con trỏ

Một mảng là một tập hợp dữ liệu không trống, các phần tử trong mảng có cùng kiểu và được đánh số theo thứ tự (index), mỗi phần tử trong mảng được định danh thông qua chỉ số của mảng, khi thay đổi giá trị của một phần tử trong mảng sẽ không ảnh hưởng tới các phần tử khác.

Một mảng chiếm một khối lượng bộ nhớ liền kề. Vậy mảng được đặt trong bộ nhớ như là một khối bao gồm các địa chỉ của bộ nhớ liền kế nhau.

Ví dụ Khai báo mảng a bao gầm các phần tử : int a[]={10, 20, 30, 40, 50};

Khi đó hình ảnh mảng a trong bộ nhớ như sau :


Hình số 4: Mảng dữ liệu

 Các phần tử trong mảng được lưu trữ liên tiếp tại các địa chỉ trong bộ nhớ theo tứ tự tăng dần ví dụ :

a[0] = 2147478270

a[1] = 2147478274

a[2] = 2147478278

...

Do dữ liệu của mảng a là int; kích cỡ int là 4 byte; do vậy các giá trị địa chỉ liên tiếp của các phần tử trong mảng a sẽ cách nhau là 4 byte;

Ký hiệu mảng là một dạng con trỏ, tên mảng là chứa địa chỉ đầu tiên của mảng đó, địa chỉ của phần tử đầu tiên của mảng được gọi là địa chỉ cơ sở của mảng.

Như vậy Tên của mảng được tham chiếu đến một hằng địa chỉ (Đó chính là địa chỉ cơ sở của mảng).

Ví dụ :

int array[]={10, 20, 30, 40, 50};

printf(“%u %u”, array, &array[0]);

Kết quả trả về là : 2147478270, 2147478270;

Do tên mảng chứa địa chỉ của phần tử đầu tiên của mảng do vậy nếu chúng ta sử dụng toán tử &Tên mảng (Toán tử & để tham chiếu đến địa chỉ của tên mảng) thì cũng bằng giá trị của tên mảng; có nghĩa là :

int array[]={10, 20, 30, 40, 50};

printf(“%u %u”, array, &array);

Kết quả trả về là : 2147478270, 2147478270;

Vậy có nghĩa : array = &array; Tức là cả array và &array đều chứa địa chỉ cơ sở của mảng.

Chúng ta có thể truy vấn đến các phần tử riêng rẽ của mảng thông qua chỉ số mảng;

a[i] chính là phần tử i của mảng a; Ngoài ra thì chúng ta cũng có thể truy vấn đến từng của từng phần tử trong mảng thông qua cơ chế tham chiếu.

Ví dụ &a[i] trả về địa chỉ của phần tử thứ i của mảng a. Tuy nhiên do tên mảng chứa địa chỉ cơ sở của mảng do vậy để truy vấn đến địa chỉ của phần tử thứ i chúng ta sử dụng :

a+i có nghĩa

 &a[i] == a + i;

a[i] == *(a+i) == *(i+a);

Ví dụ minh họa


Hình số 5: Minh họa địa chỉ mảng

Vậy tên mảng sẽ được sử dụng như con trỏ. Thông qua tên mảng chúng ta có thể tham chiếu tới các địa chỉ và giá trị của từng phần tử trong mảng.

Vậy tổng kết lại với con trỏ và mảng một chiều như sau :

+ &a[0] == a; Chứa địa chỉ cơ sở của mảng a

+ &a[i] == a + i; Địa chỉ phần tử thứ i trong mảng a;

+ a[i] == *(a + i); Giá trị phần tử thứ i trong mảng a;

+ (&a[i] - &a[j]) == (i - j); với i và j là 2 vị trí index của mảng a;

Khi làm việc với mảng; các lập trình viên có thể sử dụng con trỏ trỏ đến màng để thao tác với mảng;

Ví dụ khai báo int *p = a;

Có nghĩa con trỏ p sẽ trỏ đến địa chỉ cơ sở mảng a; khi đó các thao tác duyệt mảng, truy vấn đến các phần tử riêng rẽ của mảng được thực hiện bình thường.

Code minh họa :


Hình số 6: Con trỏ trỏ đến địa chỉ cơ sở mảng a

Kết quả :

a[i] = *(p + i);

&a[i] = p + i;

Hình ảnh sau minh họa tương qua của con trỏ p và mảng a :


Hình số 7: Tương quan giữa Con trỏ và tên mảng

2.2. Truyền mảng vào cho hàm

Có thể sử dụng mảng để làm tham số truyền cho hàm. Các phần tử trong mảng có thể được sửa đổi mà không cần phải lo lắng về việc sử dụng tham chiếu hoặc tham chiếu ngược. Vì các lập trình viên có thể sử dụng thông qua con trỏ.

Khi hàm có tham số truyền vào là mảng thì các lập trình viên có thể sử dụng theo 2 cách :

int a[] or int *a

Cách 1 : sử dụng mảng bình thường int a[];

Cách 2: sử dụng con trỏ có cùng kiểu dữ liệu với mảng. Thực sự nếu chúng ta hiểu rõ bản chất và mối tương quan giữa mảng và con trỏ thì chúng ta cũng dễ dàng hiểu được vì sau có thể sử dụng cách 2.

Khi sử dụng tên mảng như là một tham số truyền vào của hàm, thì địa chỉ của phần tử đầu tiên của mảng sẽ được copy tới con trỏ cục bộ trực thuộc hàm. Các giá trị của các phần tử là không được copy. Biến cục bộ tương ứng được coi là biến con trỏ và nó có toàn bộ các đặc điểm của một biến con trỏ thông dụng.

Ví dụ :

#define MAX 50

int main(){

int arr[MAX],n;

 ...

 n = getdata(arr, MAX);

 show(arr, n);

 return 0;

}

int getdata(int a[], int n){

 ...

}

void show(int a[], int n){

 ...

}

Trong hàm int getdata(int a[], int n); a tên mảng và tham số hình thức của hàm getdata; a được sử dụng như là con trỏ có kiểu int.

Trong hàm main(), khi gọi  n = getdata(arr, MAX); lúc này arr chính là tham số chính thức khi thực hiện lời hàm, arr là một con trỏ trỏ đến địa chỉ đầu tiên của mảng arr[]; Do vậy các lập trình viên hoàn toàn có thể sử dụng con trỏ để thay thế cho tham số mảng làm đối số truyền vào của hàm.

Ví dụ :


Hình số 8: Tham số con trỏ tương ứng với mảng

2.3. Khác nhau giữa tên mảng và con trỏ

Có một vài điểu khác nhau giữa tên mảng và con trỏ như sau :

+ Khi bộ nhớ được cấp phát cho một mảng, địa chỉ cơ sở (địa chỉ của phần tử đầu tiên) của mảng là cố định không thể thay đổi trong suốt quá trình chạy chương trình. Khi đó tên mảng đóng vai trò là một hằng địa chỉ. Do vậy tên mảng khi được dùng như con trỏ sẽ không thể thay đổi giá trị địa chỉ của chính nó.

Ví dụ :

int i;

float a[5];

for(i = 0; i < 5; i++){

  *a = 0.0;

    a++; /* BUG: a = a + 1; */

 }

Lúc này sẽ xuất hiện lỗi vì khi thực hiện a++ có nghĩa là thay đổi địa chỉ của a hay thay đổi địa chỉ cơ sở của mảng a.

Tuy nhiên nếu khai báo con trỏ int *ptr ; sau đó ptr = a; Có nghĩa là sử dụng biến con trỏ để thao tác với mảng khi đó cú pháp ptr++ là hoàn toàn không xuất hiện lỗi, vì ptr++ là tăng địa chỉ của con trỏ ptr có nghĩa là sử dụng ptr để duyệt mảng a.

Code minh họa:


Hình số 9: Khác nhau con trỏ và tên mảng

 

+ Sử dụng sizeof(a) trả về kích cỡ mảng a; còn sizeof(ptr) trả về kích cỡ phần tử đầu tiên của mảng do ptr = a;

Ví dụ :

int a[]={10, 20, 30, 40, 50};

printf(“%d”, sizeof (a));

Kết quả là 20 với GCC vì mảng a có 5 phần tử, mỗi phần tử có kích cỡ 4 bytes;

Khi sử dụng con trỏ ptr trỏ đến mảng a; có nghĩa

int *ptr;

ptr = a;

printf(“%d”, sizeof (ptr));

Lúc này kết quả sẽ trả về 4 vì con trỏ ptr trỏ đến địa chỉ a[0];

+ Kích cỡ của mảng là cố định có nghĩa là không thay đổi được kích cỡ của mảng.

Điều đó có nghĩa là với mảng không thể sử dụng hàm realloc() để thay đổi kích cỡ bộ nhớ được còn với biến con trỏ hoàn toàn có thể thay đổi kích cỡ bộ nhớ đã cấp phát.

3. Con trỏ và xâu ký tự

Xâu ký tự (string) là mảng một chiều có kiểu char (Kiểu ký tự). Theo quy ước thì một xâu ký tự trong lập trình C được kết thúc bởi ký tự ‘\0’ hoặc ký tự null. Ký tự null là một byte được đánh dấu bởi tất cả các bít là off (false), nếu xét theo mã thập phân thì ký tự null có giá trị là 0. Lập trình viên có thể sử dụng ký tự đánh dấu kết thúc của xâu để xác định độ dài của xâu ký tự, tất nhiên độ dài lớn nhất của xâu ký là có giới hạn.

Kích cỡ của xâu ký tự bao gồm cả phần lưu trữ của ký tự đánh dấu sự kết thúc của xâu lý tự đó. Khi làm việc với xâu ký tự, giống như khi làm việc với mảng, các lập trình viên phải đảm bảo là mảng lưu trữ xâu ký tự đó không bị tràn kích cỡ.

Hằng xâu ký tự được lưu trữ trong cặp dấu “”, ví dụ “abc” sẽ có kích cỡ là 4 vì thực chất nó sẽ bao gồm cả ký tự kết thúc của xâu ký tự abc.

Lưu ý hằng xâu ký tự sẽ khác hằng ký tự, ví dụ “a” != ‘a’ và thực chất “a” sẽ có 2 giá trị bao gồm ‘a’ và ‘\0’.

Hằng xâu ký tự giống như tên mảng, tự bản thân nó sẽ là một con trỏ và trình biên dịch sẽ đối xử nó như con trỏ và giá trị của con trỏ chính là địa chỉ cơ sở của xâu ký tự.

Giống như mảng int, float; xâu ký tự (string) là một mảng các char, do vậy các lập trình viên hoàn toàn có thể in thông tin của mảng đó ra màn hình.

Ví dụ minh họa:


Hình số 10 : In ra thông tin của xâu ký tự

Trong chương trình trên, sử dụng for để duyệt mảng ký tự s[]; Điều kiện kiểm tra kết thúc xâu ký tự đó là s[i] != ‘\0’, sau đó sử dụng hàm putchar để in thông tin của một ký tự.

Ví dụ minh họa sử dụng xâu ký tự như một con trỏ.


Hình số 11 : Sử dụng xâu ký tự như con trỏ

Trong chương trình trên thay vì chúng ta sử dụng

char s[] = “Hello all student”;

char *ptr = s;

Hoặc

char *ptr = “Hello all student”

for(i = 0; *(ptr + i) != ‘\0’; ++i) thì chúng ta sẽ viết luôn là

for(i = 0; *(“Hello all student” + i) != ‘0’; ++i); cách viết hoàn toàn thực hiện được vì xâu ký tự được coi là mảng ký tự và được sử dụng như con trỏ.

Lưu ý khi sử dụng con trỏ với xâu ký tự :

Ví dụ :

char *p = “abc”;

printf(“%s %s \n”, p, p + 1);

Kết quả in ra sẽ là abc bc vì

Với câu lệnh char *p = “abc”; Con trỏ sẽ trỏ đến địa chỉ đầu tiên của mảng “abc”. Khi gặp câu lệnh printf(“%s”, p) thì nó sẽ in ra là “abc” lưu ý %s là string chứ không phải %c. Nếu chúng ta thay câu lệnh trên thành printf(“%c”, p) thì nó sẽ in ra là ‘a’.

Với p+1; có nghĩa là con trỏ p sẽ tăng thêm một ô bộ nhớ (lưu ý mỗi ô bộ nhớ có kiểu character sẽ là 2 bytes hoặc 4 bytes tùy theo hệ điều hành) có nghĩa là p + 1 sẽ trỏ sang địa chỉ của ‘b’ trong mảng chuỗi ký tự “abc” do vậy khi thực hiện in kết quả :

printf(“%s \n”, p + 1); sẽ ra kết quả là “bc”.

Ví dụ mô tả bản chất của các câu lệnh sau :

char *p = “abcde”; and char s[] = “abcde”;


Hình số 12 : Con trỏ mà xâu ký tự

Mảng char s[] thực chất sẽ có giá trị là : char s[] = {‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘\0’};

Con trỏ *p sẽ trỏ đến vị trí đầu tiên của xâu ký tự “abcde”.


 

 

 

 

 


vertical_align_top
share
Chat...