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

Con trỏ trong Ngôn ngữ lập trình C – Phần 3 : Bài 12
access_time 10/9/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 3

1. Các phép toán với con trỏ

Nếu p được khai báo là một biến con trỏ có kiểu dữ liệu bất kỳ nào đó trong Ngôn ngữ C và được khởi tạo theo đúng cách, giống như các biến đơn giản khác, con trỏ *p, có rất nhiều các phép toán có thể được thực thi với nó. Bởi vì * là gắn với giá trị địa chỉ, làm việc với *p có nghĩa  làm việc với biến có địa chỉ hiện đang được giữ bởi p.

Khi xây dựng biểu thức có chứa biến con trỏ, thì biểu thức đó có thể chứa các phép toán đại số, quan hệ, logic điều đó có nghĩa là cách thức xây dựng biểu thức có chứa con trỏ cũng giống như khi xây dựng các biểu thức với các biến thông thường khác. Tuy nhiên điều này không hẳn đúng vì đặc chưng con trỏ là làm việc với địa chỉ. Với các phép toán đại số thao tác với con trỏ chỉ hỗ trợ như sau :

+ Phép gán con trỏ với một con trỏ khác cùng kiểu; Bản chất của phép toán này là việc thiết lập địa chỉ mà của một biến con trỏ sang một địa chỉ mới. Với phép gán con trỏ với một số nguyên, về mặt bản chất thì đây là phép toán mang tính hình thức, vì không có một hằng số nguyên nào ngoại trừ 0 có thể gắn cho con trỏ.

+ Phép toán Cộng và trừ của con trỏ với số nguyên.
+ Phép trừ hoặc so sánh 2 con trỏ (Trong mảng giới hạn) trỏ đến các phần tử trong mảng.

+ Phép tăng hoặc giảm của con trỏ (Trong mảng giới hạn) trỏ đến các phần tử trong mảng. Với mảng số nguyên, khi con trỏ tăng thêm 1 điều đó có nghĩa địa chỉ con trỏ sẽ tăng thêm 2 hoặc 4 vì mỗi int có kích cỡ là 2 bytes hoặc 4 bytes.
+ Khi gán giá trị 0 cho con trỏ; điều đó đồng nghĩa với giá trị con trỏ sẽ là NULL, mà con trỏ null sẽ không trỏ đến đâu cả.

Phép toán đại số sau sẽ không được thực hiện với con trỏ :

+ Phép cộng với 2 con trỏ.

+ Phép nhân con trỏ với một số.

+ Phép chia con trỏ với một số.

1.1. Phép gán

Con trỏ với toán tử gán có thể được sử dụng nếu các điều kiện sau được đáp ứng :

+ Toán hạng bên trái là một con trỏ và bên phải toán hạng là một hằng con trỏ null (Ví dụ int *p = NULL);

+ Sử dụng con trỏ có kiểu void để gán giá trị cho các kiểu dữ liệu khác nhau.

Ví dụ :

int a=5;

double b=3.1415;

void *vp;

vp=&a;

vp=&b;

+ Hai con trỏ cùng kiểu có thể gán cho nhau (Lúc này nó sẽ cùng chỉ đến một địa chỉ).

Ví dụ về việc sử dụng phép gán với con trỏ void và các vấn đề cần lưu ý.


Hình số 1: Con trỏ void và phép gán

Câu lệnh tại dòng 10 : print(“\n *vp=%d”, *vp); bị lỗi vì lý do con trỏ vp có kiểu void, tuy con trỏ void có thể lưu trữ bất kể giá trị nào mà nó trỏ đến. Do vậy thực hiện câu lệnh ở dòng số 8:

vp = ip;

Có nghĩa là vp đã trỏ đến địa chỉ của biến con trỏ ip trỏ đến hay nói cách khác trỏ đến địa chỉ của int i = 5;

Tuy nhiên khi gặp câu lệnh print(“\n *vp=%d”, *vp) thì trình biên dịch sẽ lập tức báo lỗi, muốn không bị lỗi thì phải chuyểu kiểu (ép kiểu) về int như sau :

Sửa câu lệnh tại dòng 10 : print(“\n *vp=%d”, *(int *)vp);

Khi đó chạy lại chương trình sẽ có kết quả như sau:


Hình số 2: Con trỏ void, phép gán và cơ chế ép kiểu

1.2. Cộng trừ với số nguyên

Trong một câu lệnh có liên quan tới phép cộng ‘+’ giữa con trỏ và một số nguyên và chúng ta đã thực hiện khi làm việc với mảng thông qua con trỏ, thì kết quả nó sẽ trả về một con trỏ.

Biểu thức arr + 3 (Với arr là tên mảng số nguyên) trả về một con trỏ trỏ đến địa chỉ của phần tử mảng arr[3].  (arr + 3) có kiểu (int *) và (arr + 3) == &arr[3], trong khi arr[3] là giá trị lưu trữ của arr[3] hay arr[3] có kiểu int vậy 2 biểu thức này là khác nhau.

Cả arr + 3 và &arr[3] đều đại diện cho một con trỏ trỏ đến phần tử trong mảng nguyên với chỉ mục là 3. Khi làm việc với mảng hay xâu ký tự bất kỳ biểu thức [] đều có thể được thay thế thông qua việc sử dụng con trỏ kết hợp với phép toán ‘+’.

Ví dụ : Thay vì chúng ta sử dụng arr[3] thì chúng ta sử dụng *(arr + 3).

Do vậy chúng ta có thể sử dụng phép toán cộng ‘+’ hoặc phép toán trừ ‘-‘ gắn với con trỏ khi làm việc với kiểu dữ liệu mảng. Nếu các bạn sử dụng phép cộng bằng cách cộng thêm N (với n số nguyên) thì giá trị con trỏ sẽ tăng thêm N phần tử hay tương đương với địa chỉ của phần tử thứ N trong mảng.

arr + N == &arr[N];

*(arr + N) == arr[N];

Ví dụ:


Hình số 3: Phép cộng với tên mảng

Trong chương trình trên chúng ta thấy a[i] == *(a + i);

Giống như các phép toán khác, các lập trình viên có thể sử dụng các toán tử tăng ‘++’ hoặc ‘--’ để thao tác với mảng; Lưu ý khi sử dụng toán tử một ngôi tăng hoặc giảm thì các bạn không thể sử dụng tên mảng được vì tên mảng là một hằng con trỏ. Khi đó các bạn cần phải khai báo con trỏ riêng để thực hiện.

Ví dụ :


 Hình số 4: Phép cộng với con trỏ trỏ đến mảng

Chương trình trên cho thấy, thay vì chúng ta sử dụng tên mảng a như con trỏ thì chúng ta sử dụng con trỏ p trỏ đến mảng a, sau đó sử dụng p++ và vòng lặp for để duyệt các phần tử trong mảng thông qua con trỏ p.

Lưu ý khi sử dụng phép toán ‘++’ và ‘--‘ như sau :

+ (*ip)++; Sẽ tương đương với câu lệnh sau :

+ int temp; temp = *ip; *ip = *ip + 1;

Có nghĩa là câu lệnh (*ip)++; thực hiện tăng giá trị nguyên tại địa chỉ mà con trỏ ip trỏ đến.

*ip++; Sẽ tương đương với câu các câu lệnh sau :

*(ip++);
int* temp;

temp = ip, ip = ip + 1;

Có nghĩa là *ip + 1 là tăng địa chỉ của ip lên 1 đơn vị chỉ mục trong mảng.

Tưng tự với phép cộng ‘+’ và phép tăng ‘++’ thì các lập trình viên có thể sử dụng phép trừ ‘-’ và phép giảm ‘--’.

Ví dụ:


Hình số 5: Phép trừ với con trỏ

Mục đích của chương trình trên là sử dụng phép trừ để duyệt mảng a và in ra giá trị của các phần tử trong mảng a.

Với dòng code thứ 9,*(p–i) trả về giá trị của phần tử a[i], chúng ta có thể thay thế bằng

p[–i] hay ta có

*(p–i) == p[-i];

Tương tự với mảng, chúng ta có thể sử dụng các toán tử ‘+’, ‘++’ và ‘-’, ‘--’ với xâu ký tự.

Ví dụ


Hình số 6: Phép toán ++ với string

1.3. So sánh con trỏ

C cho phép 2 con trỏ được so sánh với nhau, nếu 2 con trỏ bằng nhau điều đó có nghĩa là 2 con trỏ cùng chỉ tới một địa chỉ. Trong lập trình đặc biệt là thao tác với mảng dữ liệu và string, các lập trình viên thường xuyên sử dụng các phép toán so sánh để làm việc với con trỏ.

Ví dụ: Sử dung phép toán so sánh với con trỏ để tiến hành duyệt và in ra giá trị của mảng nguyên.


Hình số 7: Phép toán so sánh con trỏ

Khi thực hiện vòng lặp for, thì tại mỗi lần duyệt thì phép toán so sánh con trỏ p lại được thực hiện. Ở đây giá trị đầu tiên của con trỏ p sẽ lưu trữ địa chỉ cơ sở của a, sau đó mỗi lần lặp thì địa chỉ của p sẽ tăng lên là 1; và thực hiện phép so sánh với địa chỉ cuối cùng của mảng đó là a + 4;

Khi làm việc với dữ liệu mảng ký tự (hay string), sử dụng con trỏ sẽ tạo thuận lợi rất lớn cho lập trình viên.

Ví dụ : Viết chương trình thực hiện đảo chuỗi ký tự ví dụ :

Chuỗi “manas” -> “sanam”

Hàm void reverse(char *string), với tham số hình thức là con trỏ kiểu char;

Thuật toán xây dựng hàm đảo chuổi (reverse) :

+ Sử dụng con trỏ trái (left pointer) và con trỏ phải (right pointer) chạy 2 đầu chuỗi ký tự

+ Khởi tạo giá trị con trỏ trái trỏ đến địa chỉ đầu tiên của chuỗi ký tự, con trỏ phải trỏ đến địa chỉ chứa ký tự cuối cùng của chuỗi

+ Dùng while để chạy với điều kiện chừng nào địa chỉ con trỏ trái còn nhỏ hơn địa chỉ con trỏ phải thì thay đổi giá trị lưu trữ tại vị trí con trỏ trái và con trỏ phải đang nắm giữa

+ Kết thúc vòng lặp while giá trị của chuỗi đã được đảo giá trị.

Code minh họa Hàm reverse như sau:

void reverse(char *string) {

  char *lp = string;         /* left pointer */

  char *rp = &string[strlen(string)-1]; /* right pointer */

  char tmp;

  while(lp < rp){

                    tmp = *lp;

                    *lp = *rp;

                    *rp = tmp;

                    lp++;

                    rp––;

    }

}

Code minh họa toàn bộ chương trình


Hình số 8: Hàm đảo chuỗi

Khi nhập chuỗi "Student" kết quả sau khi gọi hàm sẽ in ra được là "tnedutS"

vertical_align_top
share
Chat...