Khởi đầu với con trỏ trong Ngôn ngữ lập trình C - Bài 10

Khởi đầu với con trỏ trong Ngôn ngữ lập trình C - Bài 10
access_time 10/7/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 1 - Bài số 10

1. Giới thiệu

Con trỏ trong lập trình C là một phần rất quan trọng. Trong ngôn ngữ lập trình C có quá nhiều vấn đề có thể giải quyết được thông qua con trỏ. Sử dụng con trỏ sẽ hiệu quả hơn so với các giải pháp khác. Nhưng dường như con trỏ cũng làm cho các đoạn mã trở lên khó hiểu hơn. Đi đôi với độ hiệu quả và sức mạnh của con trỏ thì lập trình viên cũng đối mặt với độ phức tạp khi sử dụng con trỏ.

Con trỏ cũng có thể là nguyên nhân gây ra các lỗi mới xuất hiện của chương trình, các lỗi của con trỏ có thể làm cho chương trình sụp đổ một cách ngẫu nhiên và cũng chính con trỏ gây khó khăn cho các lập trình viên khi thực hiện debug lỗi của chương trình. Tuy nhiên không vì những vấn đề kể trên mà con trỏ vẫn là một cấu trúc lập trình mạnh mẽ trong ngôn ngữ C/C++ so với các ngôn ngữ lập trình khác, và được các lập trình viên tin dùng.  

2. Khai báo và khởi tạo giá trị biến Con trỏ

Con trỏ cung cấp cách thức truy vấn tới biến mà không cần phải tham chiếu trực tiếp tới biến đó. Cơ chế được sử dụng ở đây là địa chỉ của một biến. Các câu lệnh của chương trình có thể tham chiếu tới biến một cách gián tiếp bằng cách sử dụng địa chỉ của biến đó.

Một biến con trỏ là một biến lưu trữ thông tin địa chỉ của một biến khác, nói một cách khác thì biến con trỏ không lưu giá trị theo cách truyền thống, thay vào đó nó lưu giữ địa chỉ của một biến khác. Nó được gọi là con trỏ vì lý do đơn giản rằng con trỏ lưu trữ địa chỉ và con trỏ “trỏ” tới một địa chỉ cụ thể.

Một con trỏ chỉ đến một biến đó bằng cách lưu giữ một bản sao địa chỉ của biến đó. Bởi vì con trỏ nắm giữ  địa chỉ hơn là nắm giữa giá trị (thông thường) do vậy con trỏ sẽ có 2 phần :

+ Tự biến con trỏ sẽ có địa chỉ của chính nó

+ Giá trị lưu trữ trên địa chỉ mà nó trỏ đến.

Con trỏ có thể được sử dụng :

+ Gọi thông qua địa chỉ : Thực hiện gọi làm thông qua địa chỉ của hàm bằng cách sử dụng con trỏ hàm.

+ Trả về nhiều giá trị thay về chỉ trả về một giá trị của hàm.

+ Truyền tham số có kiểu mảng và string đối với hàm.

+ Thao tác với mảng thông qua con trỏ một cách dễ dàng thông qua việc di chuyển địa chỉ của con trỏ (Sử dụng con trỏ trỏ đến mảng hoặc sử dụng tên mảng như con trỏ).

+ Với các cấu trúc dữ liệu phức tạp như struct, danh sách liên kết, cây nhị phân (Duyệt và thao tác với cây nhị phân).

+ Làm việc với bộ nhớ thông qua các hàm cấp phát bộ nhớ như malloc(), trả về vị trí của vùng bộ nhớ rảnh.

+ Biên dich nhanh hơn, hiệu quả hơn, so với các dẫn xuất kiểu dữ liệu khác như  array.

2.1 Khai báo biến con trỏ

Các biến có kiểu con trỏ cũng như các biến thông dụng khác. Trước khi sử dụng thì phải khai báo chúng. Một biến nói chung sẽ có giá trị, phạm vi, vòng đời tồn tại và tên của biến.

Toán tử con trỏ (*) trong ngôn ngữ C được gọi là toán tử “giá trị tại địa chỉ”. Nó trả về giá trị được lưu trữ tại địa chỉ cụ thể. Giá trị tại toán tử địa chỉ cũng được gọi là toán tử gián tiếp (Toán tử tham chiếu ngược hay indirection operator). Một biến con trỏ được khai báo với tên kèm theo với dấu *. Cú pháp khai báo như sau :

datatype * pointer_variable;

datatype : Kiểu dữ liệu mà con trỏ nắm giữ tại địa chỉ mà nó trỏ đến

pointer_variable: Tên biến con trỏ.
Ví dụ :

char *ptr;

trong câu lện khai báo ở trên, ptr là biến con trỏ trỏ đến dữ liệu có kiểu char. Char không phải là kiểu dữ liệu của ptr mà nó là kiểu dữ liệu tại địa chỉ mà ptr sẽ trỏ đến. Vậy ptr được định danh là biến con trỏ, char là kiểu dữ liệu cụ thể chứa tại địa chỉ là ptr nắm giữ. Biến con trỏ là các biến lưu trữ giá trị là địa chỉ bộ nhớ. Tại các địa chỉ mà con trỏ nắm giữ sẽ có chứa giá trị.

Bảng sau liệt kê khai báo các biến con trỏ tương ứng với một số kiểu dữ liệu cơ bản :


Hình số 1 : Ví dụ khai báo biến con trỏ

Kích cỡ các biến khi khai báo con trỏ phụ thuộc vào kích cỡ của kiểu dữ liệu mà của biến con trỏ. Kích cỡ có thể khác nhau khi lập trình viên biên dịch trên các máy tính 32bit hoặc 64 bít.

Ví dụ :


Hình số 2 : Ví dụ kích cỡ dữ liệu biến con trỏ khi biên dịch HĐH 64

2.2 Khởi tạo giá trị

Cần lưu ý rằng, không giống như một biến thông thường, một biến con trỏ phải được khởi tạo với một địa chỉ cụ thể trước khi sử dụng chúng. Một trong những nguyên nhân gây ra lỗi của chương trình khi sử dụng biến con trỏ là quyên không thiết lập giá trị khởi tạo cho biến con trỏ. Những lỗi này có thể rất khác biệt khi thực hiện gỡ lỗi vì các lỗi này thường xuất hiện muộn khi chạy chương trình.

Hãy xem xét chương trình sau đây :


Hình số 3 : Khai báo biến con trỏ không khởi tạo giá trị

Khi biên dịch chương trình, biến con trỏ p sẽ trỏ đến một địa chỉ nào đó và in ra giá trị của địa chỉ mà biến p trỏ đến, tuy nhiên biến p không được khởi tạo do vậy nó có thể chưa giá trị 0 hoặc giá trị địa chỉ ngẫu nhiên nào đó.

Một con trỏ không được sử dụng cho đến khi nó được gán một địa chỉ có ý nghĩa. Sử dụng con trỏ chưa được khởi tạo đúng cách sẽ gây ra kết quả không thể đoán trước. Khi một chương trình bắt đầu thực thi, con trỏ chưa được khởi tạo sẽ chứa một địa chỉ chưa biết nào đó trong bộ nhớ. Chính xác hơn, con trỏ đó sẽ có một giá trị chưa biết và nó là nguyên nhân gây ra một kết quả không mong muốn.

Cú pháp khởi tạo giá trị biến con trỏ:

pointer_variable = & variable;

pointer_variable: Tên biến con trỏ

variable: Tên biến có cùng kiểu dữ liệu với biến con trỏ

&: Toán tử định địa chỉ của biến mà con trỏ trỏ đến.

Ví dụ:


Hình số 4 : Khởi tạo giá trị cho biến con trỏ

Trong chương trình trên :

Biến i có giá trị 5, và địa chỉ của i là 0061FF18

Biến con trỏ ptr trỏ đến địa chỉ i do vậy giá trị của biến con trỏ ptr sẽ là 0061FF18.

2.3 Toán tử Indirection (Gián tiếp) và Dereferencing (Tham chiếu ngược).

Trong lập trình C, để trả về một giá trị cụ thể mà biến con trỏ lưu trữ thì chúng ta sử dụng toán tử gián tiếp hoặc toán tử tham chiếu ngược

Ví dụ

Int i = 5;

Int *p = &i;

Printf(“Value *p is %d”, *p);

Kết quả *p = 5; *p được gọi là toán tử gián tiếp hoặc toán tử tham chiếu ngược.

Ví dụ về việc sử dụng toán tử Gián tiếp hoặc toán tử tham chiếu ngược:


Hình số 5 : Toán tử tham chiến ngược

 

Trong ví dụ trên biến con trỏ ptr trỏ đến địa chỉ của num;

Sử dụng toán tử tham chiến ngược để thiết lập giá trị : *Ptr = 15; khi đó giá trị của num = 15 vì con trỏ ptr đã trỏ đến địa chỉ của num;

Vì vậy “indirection operator” còn được gọi là “dereference the pointer” (Tham chiếu ngược lại tới con trỏ.

Ví dụ áp đơn giản với con trỏ:


Hình số 6 : Ví dụ đơn giản với con trỏ

Sử dụng con trỏ pa để trỏ đến a; con trỏ pb để trỏ đến b, sử dụng scanf để nhập giá trị cho a,b thông qua con trỏ pa, pb; sử dụng toán tử *pc để tính tổng của *pb, *pc;

Kết quả khi nhập 6 và 7 vào, kết quả sẽ in ra giá trị là 13;

3. Con trỏ void

Một con trỏ void là con trỏ có kiểu đặc biệt, nó có thể trỏ đến bất kỳ kiểu dữ liệu nào, từ dữ liệu kiểu integer, float hoặc dữ liệu kiểu string (tập hợp các character). Hạn chế duy nhất của nó là không thể tham chiếu trực tiếp (toán tử * không thể được sử dụng) vì chiều dài của nó luôn không xác định. Hơn nữa, chuyển kiểu hoặc ép kiểu phải được sử dụng để biến con trỏ kiểu void thành con trỏ của một loại dữ liệu cụ thể , ví dụ :


Hình số 7 : Ví dụ con trỏ kiểu void

4. Con trỏ NULL

Giả sử một biến

int a ;

Được khai báo nằm ngoài phạm vi của một hàm và không được thiết lập giá trị khởi tạo. Trình biên dịch ANSI C sẽ tự thiết lập giá trị cho biến a = 0; Tương tự, một khi biến con trỏ chưa khởi tạo giá trị ban đầu, thì trình biên dịch ANSI sẽ tự động thiết lập giá trị ban đầu cho biến con trỏ đó, trình biên dịch ANSI C tự động xử lý vấn đề này nhằm đảm bảo một cách chắc chắn rằng biến con trỏ này sẽ không tự động trỏ đến địa chỉ của một đối tượng nào đó của C hoặc địa chỉ của một hàm nào đó.

Để nhằm tránh gây lỗi khi lập trình với con trỏ, khi khai báo một biến con trỏ, nếu con trỏ đó chưa sử dụng thì để chắc chắn chúng ta thiết lập giá trị ban đầu cho biến con trỏ là NULL.

Con trỏ NULL là con trỏ đặc biệt, nó không trỏ đến đâu cả. Để sử dụng con trỏ NULL thì các lập trình viên cần sử dụng hằng NULL. Hằng NULL được định nghĩa sẵn trong các file header như : <stdio.h>, <stdlib.h>, and <string.h>; Để thiết lập giá trị cho con trỏ NULL chúng ta sử dụng cú pháp

data type *name-variable = NULL;

data type : Kiểu dữ liệu của con trỏ

name-variable : Tên biến

Ví dụ :

#include <stdio.h>

int *ip = NULL;

Để kiểm tra con trỏ đó có NULL hay không thì chúng ta sử dụng toán tử so sánh. Ví dụ :

if(ip != NULL) {

printf(“%d\n”, *ip);

}

Với kiểu dữ liệu int *ip = null; Khi đó giá trị ip sẽ = 0;

Ví dụ :


Hình số 8 : Con trỏ kiểu NULL

Khi in giá trị của con trỏ p; sẽ có giá trị là 0 (p = 0);

Trong 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ớ tại địa chỉ 0 vì bộ nhớ đó là được bảo lưu bởi hệ điều hành. Nó không phải là trường hợp con trỏ trỏ đến một địa chỉ bộ nhớ được dành riêng bởi hệ điều hành. Tuy nhiên địa chỉ bộ nhớ 0 có ý nghĩa đặc biệt, nó báo hiệu rằng con trỏ là không có ý định trỏ đến một vị trí bộ nhớ có thể truy cập.

Với con trỏ NULL khi thiết đặt với dữ liệu con trỏ kiểu char char *p = NULL. Khi in giá trị của p thì p có giá trị là (null);

Ví dụ :


Hình số 9 : Con trỏ kiểu NULL với string

Trong C chuẩn %s chỉ đến mảng char (có nghĩa tương đương với string).
vertical_align_top
share
Chat...