Giáo trình Lập trình cơ bản C - Bài 13: Con trỏ

Bài 13  
Con trỏ  
Mục tiêu:  
Kết thúc bài học này, bạn thể:  
Hiểu con trỏ là gì, và con trỏ được sử dụng ở đâu  
Biết cách sử dụng biến con trỏ và các toán tử con trỏ  
Gán giá trị cho con trỏ  
Hiểu các phép toán số học con trỏ  
Hiểu các phép toán so sánh con trỏ  
Biết cách truyền tham số con trỏ cho hàm  
Hiểu cách sử dụng con trỏ kết hợp với mảng một chiều  
Hiểu cách sử dụng con trỏ kết hợp với mảng đa chiều  
Hiểu cách cấp phát bộ nhớ được thực hiện như thế nào  
Giới thiệu  
Con trỏ cung cấp một cách thức truy xuất biến mà không tham chiếu trực tiếp đến biến. Nó cung cấp  
cách thức sử dụng địa chỉ. Bài này sẽ đề cập đến các khái niệm về con trỏ và cách sử dụng chúng trong  
C.  
13.1 Con trỏ là gì?  
Một con trỏ một biến, chứa địa chỉ vùng nhớ của một biến khác, chứ không lưu trữ giá trị của  
biến đó. Nếu một biến chứa địa chỉ của một biến khác, thì biến này được gọi con trỏ đến biến thứ  
hai kia. Một con trỏ cung cấp phương thức gián tiếp để truy xuất giá trị của các phần tử dữ liệu. Xét  
hai biến var1 và var2, var1 có giá trị 500 và được lưu tại địa chỉ 1000 trong bộ nhớ. Nếu var2 được  
khai báo như một con trỏ tới biến var1,ơ sự biểu diễn sẽ như sau:  
Vị trí  
Bộ nhớ  
1000  
1001  
1002  
.
Giá trị  
lưu trữ  
500  
Tên  
biến  
var1  
.
1108  
1000  
var2  
Ở đây, var2 chứa giá trị 1000, đó địa chỉ của biến var1.  
Các con trỏ thể trỏ đến các biến của các kiểu dữ liệu cơ sở như int, char, hay double hoặc dữ liệu  
cấu trúc như mảng.  
Con trỏ  
181  
13.1.2 Tại sao con trỏ được dùng?  
Con trỏ thể được sử dụng trong một số trường hợp sau:  
Để trả về nhiều hơn một giá trị từ một hàm  
Thuận tiện hơn trong việc truyền các mảng chuỗi từ một hàm đến một hàm khác  
Sử dụng con trỏ để làm việc với các phần tử của mảng thay vì truy xuất trực tiếp vào các phần tử  
này  
Để cấp phát bộ nhớ động và truy xuất vào vùng nhớ được cấp phát này (dynamic memory  
allocation)  
13.2 Các biến con trỏ  
Nếu một biến được sử dụng như một con trỏ, phải được khai báo trước. Câu lệnh khai báo con trỏ  
bao gồm một kiểu dữ liệu cơ bản, một dấu *, và một tên biến. Cú pháp tổng quát để khai báo một biến  
con trỏ như sau:  
type *name;  
Ở đó type một kiểu dữ liệu hợp lệ bất kỳ, name là tên của biến con trỏ. Câu lệnh khai báo trên  
nói với trình biên dịch name được sử dụng để lưu địa chỉ của một biến kiểu dữ liệu type. Trong  
câu lệnh khai báo, * xác định rằng một biến con trỏ đang được khai báo.  
Trong ví dụ của var1 var2 trên, vì var2 một con trỏ giữ địa chỉ của biến var1 kiểu int, nó  
sẽ được khai báo như sau:  
int *var2;  
Bây giờ, var2 thể được sử dụng trong một chương trình để trực tiếp truy xuất giá trị của var1. Nhớ  
rằng, var2 không phải kiểu dữ liệu int nhưng nó là một con trỏ trỏ đến một biến kiểu dữ liệu int.  
Kiểu dữ liệu cơ sở của con trỏ xác định kiểu của biến mà con trỏ trỏ đến. Về mặt kỹ thuật, một con trỏ  
kiểu bất kỳ thể trỏ đến bất kỳ vị trí nào trong bộ nhớ. Tuy nhiên, tất cả các phép toán số học trên  
con trỏ đều có liên quan đến kiểu cơ sở của nó, vì vậy khai báo kiểu dữ liệu của con trỏ một cách rõ  
ràng là điều rất quan trọng.  
13.3 Các toán tử con trỏ  
Có hai toán tử đặc biệt được dùng với con trỏ: * &. Toán tử & một toán tử một ngôi và nó trả về  
địa chỉ của toán hạng. dụ,  
var2 = &var1;  
lấy địa chỉ vùng nhớ của biến var1 gán cho var2. Địa chỉ này là vị trí ô nhớ bên trong máy tính của  
biến var1 và nó không làm gì với giá trị của var1. Toán tử & thể hiểu trả về “địa chỉ của”. Vì  
vậy, phép gán trên có nghĩa là “var2 nhận địa chỉ của var1”. Trở lại, giá trị của var1 là 500 và nó dùng  
vùng nhớ 1000 để lưu giá trị này. Sau phép gán trên, var2 sẽ có giá trị 1000.  
Toán tử thứ hai, toán tử *, được dùng với con trỏ phần bổ xung của toán tử &, toán tử *. Nó là  
một toán tử một ngôi và trả về giá trị chứa trong vùng nhớ được trỏ bởi giá trị của biến con trỏ.  
Xem ví dụ trước, ở đó var1 có giá trị 500 và được lưu trong vùng nhớ 1000, sau câu lệnh  
182  
Lập trình cơ bản C  
var2 = &var1;  
var12 chứa giá trị 1000, và sau lệnh gán  
temp = *var2;  
temp sẽ chứa 500 không phải là 1000. Toán tử * thể được hiểều là “tại địa chỉ”.  
Cả hai toán tử * và & độ ưu tiên cao hơn tất ccác toán tử toán học ngoại trừ toán tử lấy giá trị âm.  
Chúng có cùng độ ưu tiên với toán tử lấy giá trị âm (-).  
Chương trình dưới đây in ra giá trị của một biến kiểu số nguyên, địa chỉ của được lưu trong một  
biến con trỏ, chương trình cũng in ra địa chỉ của biến con trỏ.  
#include <stdio.h>  
void main()  
{
int var = 500, *ptr_var;  
/* var is declared as an integer and ptr_var as a pointer  
pointing to an integer */  
ptr_var = &var; /*stores address of var in ptr_var*/  
/* Prints value of variable (var) and address where var is  
stored */  
printf(“The value %d is stored at address %u:”, var, &var);  
/* Prints value stored in ptr variable (ptr_var) and address  
where ptr_var is stored */  
printf(“\nThe value %u is stored at address: %u”,  
ptr_var, &ptr_var);  
/* Prints value of variable (var) and address where  
var is stored, using pointer to variable */  
printf(“\nThe value %d is stored at address:%u”, *ptr_var,  
ptr_var);  
}
Kết quả của dụ trên được hiển thị ra như sau:Một dụ về kết quả thực thi chương trình như sau:  
The value 500 is stored at address: 65500  
The value 65500 is stored at address: 65502  
The value 500 is stored at address: 65500  
Trong ví dụ trên, ptr_var chứa địa chỉ 65500, là địa chỉ vùng nhớ lưu trữ giá trị của var. Nội dung ô  
nhớ 65500 này có thể lấy được bằng cách sử dụng toán tử *, như *ptr_var. Lúc này *ptr_var trình  
bàytương ứng với giá trị 500, là giá trị của var. Bởi ptr_var cũng một biến, nên địa chỉ của nó có  
thể được in ra bằng toán tử &. Trong ví dụ trên, ptr_var được lưu tại địa chỉ 65502. Mã quy cách %u  
chỉ định cách in giá trị các tham số theo kiểu số nguyên không dấu (unsigned int).  
Con trỏ  
183  
Nhớ lại là, một biến kiểu số nguyên chiếm 2 bytes bộ nhớ. vậy, giá trị của var được lưu trữ tại địa  
chỉ 65500 và trình biên dịch cấp phát ô nhớ kế tiếp 65502 cho ptr_var. Tương tự, một số thập phân  
kiểu float yêu cầu 4 bytes và kiểu double yêu cầu 8 bytes. Các biến con trỏ lưu trữ một giá trị nguyên.  
Với hầu hết các chương trình sử dụng con trỏ, kiểu con trỏ thể xem như một giá trị 16-bit chiếm 2  
bytes bộ nhớ.  
Chú ý rằng hai câu lệnh sau cho ra cùng một kết quả.  
printf(“The value is %d”, var);  
printf(“The value is %d”, *(&var));  
Gán giá trị cho con trỏ  
Các giá trị thể được gán cho biến con trỏ thông qua toán tử &. Câu lệnh gán sẽ là:  
ptr_var = &var;  
Lúc này địa chỉ của var được lưu trong biến ptr_var. Cũng thể gán giá trị cho con trỏ thông qua  
một biến con trỏ khác trỏ đến một phần tử dữ liệu có cùng kiểu.  
ptr_var = &var;  
ptr_var2 = ptr_var;  
Giá trị NULL cũng thể được gán đến một con trỏ bằng số 0 như sau:  
ptr_var = 0;  
Các biến cũng thể được gán giá trị thông qua con trỏ của chúng.  
*ptr_var  
=
10;  
sẽ gán 10 cho biến var nếu ptr_var trỏ đến var.  
Nói chung, các biểu thức chứa con trỏ cũng theo cùng qui luật như các biểu thức khác trong C.  
Điều quan trọng cần chú ý phải gán giá trị cho biến con trỏ trước khi sử dụng chúng; nếu không chúng  
thể trỏ đến một giá trị không xác định nào đó.  
Phép toán số học con trỏ  
Chỉ phép cộng trừ là các toán tử thể thực hiện trên các con trỏ. dụ sau minh họa điều này:  
int var, *ptr_var;  
ptr_var = &var;  
var = 500;  
Trong ví dụ trên, chúng ta giả sử rằng var được lưu tại địa chỉ 1000. Sau đó, giá trị 1000 sẽ được lưu  
vào ptr_var. Vì kiểu số nguyên chiếm 2 bytes, nên sau biểu thức:  
ptr_var++ ;  
ptr_var sẽ chứa 1002 mà KHÔNG phải 1001. Điều này có nghĩa là ptr_var bây giờ trỏ đến một số  
nguyên được lưu tại địa chỉ 1002. Mỗi khi ptr_var được tăng lên, nó sẽ trỏ đến số nguyên kế tiếp và  
bởi vì các số nguyên là 2 bytes, ptr_var sẽ được tăng trị là 2. Điều này cũng tương tự với phép toán  
giảm trị.  
184  
Lập trình cơ bản C  
Đây một vài ví dụ.  
++ptr_var or ptr_var++  
Trỏ đến số nguyên kế tiếp đứng sau var  
Trỏ đến số nguyên đứng trước var  
Trỏ đến số nguyên thứ i sau var  
--ptr_var or ptr_var--  
ptr_var + i  
ptr_var - i  
++*ptr_var or (*ptr_var)++  
*ptr_var++  
Trỏ đến số nguyên thứ i trước var  
Sẽ tăng trị var bởi 1  
Sẽ tác động đến giá trị của snguyên kế tiếp sau var  
Mỗi khi một con trỏ được tăng giá trị, sẽ trỏ đến ô nhớ của phần tử kế tiếp. Mỗi khi nó được giảm  
giá trị, sẽ trỏ đến vị trí của phần tử đứng trước nó. Với những con trỏ trỏ tới các ký tự, xuất hiện  
bình thường, bởi mỗi tự chiếm 1 byte. Tuy nhiên, tất cả những con trỏ khác sẽ tăng hoặc giảm trị  
tuỳ thuộc vào độ dài kiểu dữ liệu mà chúng trỏ tới.  
Như đã thấy trong các ví dụ trên, ngoài các toán tử tăng trị giảm trị, các số nguyên cũng thể  
được cộng vào và trừ ra với con trỏ. Ngoài phép cộng trừ một con trỏ với một số nguyên, không có  
một phép toán nào khác có thể thực hiện được trên các con trỏ. Nói rõ hơn, các con trỏ không thể được  
nhân hoặc chia. Cũng như kiểều float và double không thể được cộng hoặc trừ với con trỏ.  
So sánh con trỏ.  
Hai con trỏ thể được so sánh trong một biểu thức quan hệ. Tuy nhiên, điều này chỉ thể nếu chai  
biến này đều trỏ đến các biến có cùng kiểu dữ liệu. ptr_a ptr_b là hai biến con trỏ trỏ đến các phần  
tử dữ liệu a b. Trong trường hợp này, các phép so sánh sau đây là có thể thực hiện:  
ptr_a < ptr_b  
ptr_a > ptr_b  
ptr_a <= ptr_b  
Trả về giá trị true nếu a được lưu trữ ở vị trí trước b  
Trả về giá trị true nếu a được lưu trữ ở vị trí sau b  
Trả về giá trị true nếu a được lưu trữ ở vị trí trước b hoặc ptr_a và ptr_b trỏ  
đến cùng một vị trí  
ptr_a >= ptr_b  
ptr_a == ptr_b  
ptr_a != ptr_b  
Trả về giá trị true nếu a được lưu trữ ở vị trí sau b hoặc ptr_a và ptr_b trỏ đến  
cùng một vị trí  
Trả về giá trị true nếu cả hai con trỏ ptr_a và ptr_b trỏ đến cùng một phần tử dữ  
liệu.  
Trả về giá trị true nếu cả hai con trỏ ptr_a và ptr_b trỏ đến các phần tử dữ liệu  
khác nhau nhưng có cùng kiểu dữ liệu.  
ptr_a == NULL Trả về giá trị true nếu ptr_a được gán giá trị NULL (0)  
Tương tự, nếu ptr_begin ptr_end trỏ đến các phần tử của cùng một mảng thì,  
ptr_end - ptr_begin  
sẽ trả về số bytes cách biệt giữ hai vị trí mà chúng trỏ đến.  
13.4 Con trỏ mảng một chiều  
Tên của một mảng thật ra là một con trỏ trỏ đến phần tử đầu tiên của mảng đó. vậy, nếu ary một  
mảng một chiều, thì địa chỉ của phần tử đầu tiên trong mảng thể được biểu diễn &ary[0] hoặc  
đơn giản chỉ ary. Tương tự, địa chỉ của phần tử mảng thứ hai có thể được viết như &ary[1] hoặc  
ary+1,... Tổng quát, địa chỉ của phần tử mảng thứ (i + 1) có thể được biểu diễn &ary[i] hay (ary+i).  
Như vậy, địa chỉ của một phần tử mảng bất kỳ thể được biểu diễn theo hai cách:  
Sử dụng hiệu & trước một phần tử mảng  
Sử dụng một biểu thức trong đó chỉ số được cộng vào tên của mảng.  
Con trỏ  
185  
Ghi nhớ rằng trong biểu thức (ary + i), ary tượng trưng cho một địa chỉ, trong khi i biểu diễn số  
nguyên. Hơn thế nữa, ary là tên của một mảng mà các phần tử thể cả kiểều số nguyên, ký tự,  
số thập phân,(dĩ nhiên, tất cả các phần tử của mảng phải có cùng kiểu dữ liệu). vậy, biểu thức ở  
trên không chỉ một phép cộng; thật ra là xác định một địa chỉ, một số xác định của các ô nhớ .  
Biểu thức (ary + i) một sự trình bày cho một địa chỉ chứ không phải một biểu thức toán học.  
Như đã nói ở trước, số lượng ô nhớ được kết hợp với một mảng sẽ tùy thuộc vào kiểu dữ liệu của  
mảng cũng như kiến trúc của máy tính. Tuy nhiên, người lập trình chỉ thể xác định địa chỉ của  
phần tử mảng đầu tiên, đó là tên của mảng (trong trường hơp này là ary) và số các phần tử tiếp sau  
phần tử đầu tiên, đó là, một giá trị chỉ số. Giá trị của i đôi khi được xem như một độ dời khi được  
dùng theo cách này.  
Các biểu thức &ary[i] (ary+i) biểu diễn địa chỉ phần tử thứ i của ary, và như vậy một cách logic là  
cả ary[i] *(ary + i) đều biểu diễn nội dung của địa chỉ đó, nghĩa là, giá trị của phần tử thứ i trong  
mảng ary. Cả hai cách có thể thay thế cho nhau và được sử dụng trong bất kỳ ứng dụng nào khi người  
lập trình mong muốn.  
Chương trình sau đây biểu diễn mối quan hệ giữa các phần tử mảng địa chỉ của chúng.  
#include<stdio.h>  
void main()  
{
static int ary[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};  
int i;  
for (i = 0; i < 10; i ++)  
{
printf(“\n i = %d , ary[i] = %d , *(ary+i)= %d “, i,  
ary[i], *(ary + i));  
printf(“&ary[i] = %X , ary + i = %X”, &ary[i], ary + i);  
/* %X gives unsigned hexadecimal */  
}
}
Chương trình trên định nghĩa mảng một chiều ary, có 10 phần tử kiểu số nguyên, các phần tử mảng  
được gán giá trị tương ứng là 1, 2, ..10. Vòng lặp for được dùng để hiển thị giá trị địa chỉ tương  
ứng của mỗi phần tử mảng. Chú ý rằng, giá trị của mỗi phần tử được xác định theo hai cách khác  
nhau, ary[i] và *(ary + i), nhằm minh họa sự tương đương của chúng. Tương tự, địa chỉ của mỗi phần  
tử mảng cũng được hiển thị theo hai cách. Kết quả thực thi của chương trình trên như sau:  
i=0 ary[i]=1  
i=1 ary[i]=2  
i=2 ary[i]=3  
i=3 ary[i]=4  
i=4 ary[i]=5  
i=5 ary[i]=6  
i=6 ary[i]=7  
i=7 ary[i]=8  
i=8 ary[i]=9  
*(ary+i)=1  
*(ary+i)=2  
*(ary+i)=3  
*(ary+i)=4  
*(ary+i)=5  
*(ary+i)=6  
*(ary+i)=7  
*(ary+i)=8  
*(ary+i)=9  
&ary[i]=194  
&ary[i]=196  
&ary[i]=198  
&ary[i]=19A  
&ary[i]=19C  
&ary[i]=19E  
&ary[i]=1A0  
&ary[i]=1A2  
&ary[i]=1A4  
ary+i = 194  
ary+i = 196  
ary+i = 198  
ary+i = 19A  
ary+i = 19C  
ary+i = 19E  
ary+i = 1A0  
ary+i = 1A2  
ary+i = 1A4  
ary+i = 1A6  
i=9 ary[i]=10 *(ary+i)=10 &ary[i]=1A6  
Kết quả này trình bày rõ ràng sự khác nhau giữa ary[i] - biểu diễn giá trị của phần tử thứ i trong mảng,  
&ary[i] - biểu diễn địa chỉ của nó.  
186  
Lập trình cơ bản C  
Khi gán một giá trị cho một phần tử mảng như ary[i], vế trái của lệnh gán có thể được viết ary[i]  
hoặc *(ary + i). Vì vậy, một giá trị thể được gán trực tiếp đến một phần tử mảng hoặc nó có thể  
được gán đến vùng nhớ địa chỉ của nó là phần tử mảng. Đôi khi cần thiết phải gán một địa chỉ đến  
một định danh. Trong những trường hợp như vậy, một con trỏ phải xuất hiện trong vế trái của câu lệnh  
gán. Không thể gán một địa chỉ tùy ý cho một tên mảng hoặc một phần tử của mảng. vậy, các biểu  
thức như ary, (ary + i) &ary[i] không thể xuất hiện trong vế trái của một câu lệnh gán. Hơn thế  
nữa, địa chỉ của một mảng không thể thay đổi một cách tùy ý, vì thế các biểu thức nary++ là không  
được phép. Lý do là vì: ary địa chỉ của mảng ary. Khi mảng được khai báo, bộ liên kết đã quyết  
định mảng được bắt đầu ở đâu, dụ, bắt đầu ở địa chỉ 1002. Một khi địa chỉ này được đưa ra, mảng sẽ  
ở đó. Việc cố gắng tăng địa chỉ này lên là điều nghĩa, giống như khi nói  
x = 5++;  
Bởi hằng không thể được tăng trị, trình biên dịch sẽ đưa ra thông báo lỗi.  
Trong trường hợp mảng ary, ary cũng được xem như một hằng con trỏ. Nhớ rằng, (ary + 1) không  
di chuyển mảng ary đến vị trí (ary + 1), nó chỉ trỏ đến vị trí đó, trong khi ary++ cố găng dời ary sang  
1 vị trí.  
Địa chỉ của một phần tử không thể được gán cho một phần tử mảng khác, mặc dù giá trị của một phần  
tử mảng thể được gán cho một phần tử khác thông qua con trỏ.  
&ary[2] = &ary[3];  
ary[2] = ary[3];  
/* không cho phép*/  
/* cho phép*/  
Nhớ lại rằng trong hàm scanf(), tên các tham biến kiểu dữ liệu cơ bản phải đặt sau dấu (&), trong khi  
tên tham biến mảng ngoại lệ. Điều này cũng dễ hiểu. Vì scanf() đòi hỏi địa chỉ bộ nhớ của từng biến  
dữ liệu trong danh sách tham số, trong khi toán tử & trả về địa chỉ bộ nhớ của biến, do đó trước tên  
biến phải dấu &. Tuy nhiên dấu & không được yêu cầu đối với tên mảng, bởi vì tên mảng tự biểu  
diễn địa chỉ của nó.Tuy nhiên, nếu một phần tử trong mảng được đọc, dấu & cần phải sử dụng.  
scanf(“%d”, *ary)  
/* đối với phần tử đầu tiên */  
scanf(“%d”, &ary[2])/* đối với phần tử bất kỳ */  
13.4.1 Con trỏ mảng nhiều chiều  
Một mảng nhiều chiều cũng thể được biểu diễn dưới dạng con trỏ của mảng một chiều (tên của  
mảng) một độ dời (chỉ số). Thực hiện được điều này là bởi một mảng nhiều chiều một tập hợp  
của các mảng một chiều.Ví dụ, một mảng hai chiều thể được định nghĩa như một con trỏ đến một  
nhóm các mảng một chiều kế tiếp nhau. Cú pháp báo mảng hai chiều thể viết như sau:  
data_type (*ptr_var)[expr 2];  
thay vì  
data_type array[expr 1][expr 2];  
Khái niệm này có thể được tổng quát hóa cho các mảng nhiều chiều, đó là,  
data_type (*ptr_var)[exp 2] .... [exp N];  
thay vì  
data_type array[exp 1][exp 2] ... [exp N];  
Con trỏ  
187  
Trong các khai báo trên, data_typekiểu dữ liệu của mảng, ptr_varlà tên của biến con trỏ,  
array là tên mảng, exp 1, exp 2, exp 3, ... exp Nlà các giá trị nguyên dương xác định  
số lượng tối đa các phần tử mảng được kết hợp với mỗi chỉ số.  
Chú ý dấu ngoặc () bao quanh tên mảng dấu * phía trước tên mảng trong cách khai báo theo dạng  
con trỏ. Cặp dấu ngoặc () là không thể thiếu, ngược lại cú pháp khai báo sẽ khai báo một mảng của các  
con trỏ chứ không phải một con trỏ của một nhóm các mảng.  
dụ, nếu ary một mảng hai chiều có 10 dòng và 20 cột, nó có thể được khai báo như sau:  
int (*ary)[20];  
thay vì  
int ary[10][20];  
Trong sự khai báo thứ nhất, ary được định nghĩa một con trỏ trỏ tới một nhóm các mảng một chiều  
liên tiếp nhau, mỗi mảng có 20 phần tử kiểu số nguyên. Vì vậy, ary trỏ đến phần tử đầu tiên của  
mảng, đó là dòng đầu tiên (dòng 0) của mảng hai chiều. Tương tự, (ary + 1) trỏ đến dòng thứ hai của  
mảng hai chiều, ...  
Một mảng thập phân ba chiều fl_ary thể được khai báo như:  
float (*fl_ary)[20][30];  
thay vì  
float fl_ary[10][20][30];  
Trong khai báo đầu, fl_aryđược định nghĩa như một nhóm các mảng thập phân hai chiều có kích  
thước 20 x 30 liên tiếp nhau. Vì vậy, fl_arytrỏ đến mảng 20 x 30 đầu tiên, (fl_ary + 1) trỏ đến  
mảng 20 x 30 thứ hai,...  
Trong mảng hai chiều ary, phần tử tại dòng 4 và cột 9 có thể được truy xuất sử dụng câu lệnh:  
ary[3][8];  
hoặc  
*(*(ary + 3) + 8);  
Cách thứ nhất là cách thường được dùng. Trong cách thứ hai, (ary + 3) là một con trỏ trỏ đến dòng thứ  
4. Vì vậy, đối tượng của con trỏ này, *(ary + 3), tham chiếu đến toàn bộ dòng. Vì dòng 3 là một mảng  
một chiều, *(ary + 3) là một con trỏ trỏ đến phần tử đầu tiên trong dòng 3, sau đó 8 được cộng vào con  
trỏ. vậy, *(*(ary + 3) + 8) là một con trỏ trỏ đến phần tử 8 (phần tử thứ 9) trong dòng thứ 4. Vì vậy  
đối tượng của con trỏ này, *(*(ary + 3) + 8), tham chiếu đến tham chiếu đến phần tử trong cột thứ 9  
của dòng thứ 4, đó là ary [3][8].  
nhiều cách thức để định nghĩa mảng, và có nhiều cách để xử lý các phần tử mảng. Lựa chọn cách  
thức nào tùy thuộc vào người dùng. Tuy nhiên, trong các ứng dụng có các mảng dạng số, định nghĩa  
mảng theo cách thông thường sẽ dễ dàng hơn.  
Con trỏ chuỗi  
Chuỗi đơn giản chỉ một mảng một chiều kiểu tự. Mảng và con trỏ mối liên hệ mật thiết, và  
như vậy, một cách tự nhiên chuỗi cũng sẽ mối liên hệ mật thiết với con trỏ. Xem trường hợp hàm  
188  
Lập trình cơ bản C  
strchr(). Hàm này nhận các tham số một chuỗi một tự để tìm kiếm tự đó trong mảng,  
nghĩa là,  
ptr_str = strchr(strl, ‘a’);  
biến con trỏ ptr_str sẽ được gán địa chỉ của tự ‘a’ đầu tiên xuất hiện trong chuỗi str. Đây  
không phải vị trí trong chuỗi, từ 0 đến cuối chuỗi, mà là địa chỉ, từ địa chỉ bắt đầu chuỗi đến địa chỉ  
kết thúc của chuỗi.  
Chương trình sau sử dụng hàm strchr(), đây chương trình cho phép người dùng nhập vào một chuỗi  
một tự để tìm kiếm. Chương trình in ra địa chỉ bắt đầu của chuỗi, địa chỉ của tự, vị trí  
tương đối của tự trong chuỗi (0 là vị trí của tự đầu tiên, 1 là vị trí của tự thứ hai,...). Vị trí  
tương đối này là hiệu số giữa hai địa chỉ, địa chỉ bắt đầu của chuỗi địa chỉ nơi mà ký tự cần tìm đầu  
tiên xuất hiện.  
#include <stdio.h>  
#include <string.h>  
void main ()  
{
char a, str[81], *ptr;  
printf(“\nEnter a sentence:”);  
gets(str);  
printf(“\nEnter character to search for:”);  
a = getche();  
ptr = strchr(str, a);  
/* return pointer to char*/  
printf(“\nString starts at address: %u”, str);  
printf(“\nFirst occurrence of the character is at address: %u”,  
ptr);  
printf(“\nPosition of first occurrence (starting from 0)is: %d”,  
ptr-str);  
}
Kết quả của dụ trên được hiển thị ra như sau:Một dụ về kết quả thực hiện chương trình như sau:  
Enter a sentence: We all live in a yellow submarine  
Enter character to search for: Y  
String starts at address: 65420.  
First occurrence of the character is at address: 65437.  
Position of first occurrence (starting from 0) is: 17  
Trong câu lệnh khai báo, biến con trỏ ptr được thiết đặt để chứa địa chỉ trả về từ hàm strchr(), vì vậy  
đây một địa chỉ của một tự (ptr kiểu char).  
Hàm strchr() không cần thiết phải khai báo nếu thư viện string.h được khai báo.  
13.5 Cấp phát bộ nhớ  
Cho đến thời điểm này thì chúng ta đã biết là tên của một mảng thật ra là một con trỏ trỏ tới phần tử  
đầu tiên của mảng. Hơn nữa, ngoài cách định nghĩa một mảng thông thường thể định nghĩa một  
mảng như một biến con trỏ. Tuy nhiên, nếu một mảng được khai báo một cách bình thường, kết quả  
một khối bộ nhớ cố định được dành sẵn tại thời điểm bắt đầu thực thi chương trình, trong khi điều  
này không xảy ra nếu mảng được khai báo như một biến con trỏ. Sử dụng một biến con trỏ để biểu  
Con trỏ  
189  
diễn một mảng đòi hỏi việc gán một vài ô nhớ khởi tạo trước khi các phần tử mảng được xử lý. Sự cấp  
phát bộ nhớ như vậy thông thường được thực hiện bằng cách sử dụng hàm thư viện malloc().  
Xem ví dụ sau. Một mảng số nguyên một chiều ary có 20 phần tử thể được khai báo như sau:  
int *ary;  
thay vì  
int ary[20];  
Tuy nhiên, ary sẽ không được tự động gán một khối bộ nhớ khi nó được khai báo như một biến con  
trỏ, trong khi một khối ô nhớ đủ để chứa 10 số nguyên sẽ được dành sẵn nếu ary được khai báo như là  
một mảng. Nếu ary được khai báo như một con trỏ, số lượng bộ nhớ thể được gán như sau:  
ary = malloc(20 *sizeof(int));  
Sẽ dành một khối bộ nhớ có kích thước (tính theo bytes) tương đương với kích thước của một số  
nguyên. Ở đây, một khối bộ nhớ cho 20 số nguyên được cấp phát. 20 con số gán với 20 bytes (một  
byte cho một số nguyên) và được nhân với sizeof(int), sizeof(int) sẽ trả về kết quả 2, nếu máy tính  
dùng 2 bytes để lưu trữ một số nguyên. Nếu một máy tính sử dụng 1 byte để lưu một số nguyên, hàm  
sizeof() không đòi hỏi ở đây. Tuy nhiên, sử dụng sẽ tạo khả năng uyển chuyển cho mã lệnh. Hàm  
malloc() trả về một con trỏ chứa địa chỉ vị trí bắt đầu của vùng nhớ được cấp phát. Nếu không gian bộ  
nhớ yêu cầu không có, malloc() trả về giá trị NULL. Sự cấp phát bộ nhớ theo cách này, nghĩa là, khi  
được yêu cầu trong một chương trình được gọi Cấp phát bộ nhớ động.  
Trước khi tiếp tục xa hơn, chúng ta hãy thảo luận về khái niêm Cấp phát bộ nhớ động. Một chương  
trình C có thể lưu trữ các thông tin trong bộ nhớ của máy tính theo hai cách chính. Phương pháp thứ  
nhất bao gồm các biến toàn cục cục bộ – bao gồm các mảng. Trong trường hợp các biến toàn cục và  
biến tĩnh, sự lưu trữ cố định suốt thời gian thực thi chương trình. Các biến này đòi hỏi người lập  
trình phải biết trước tổng số dung lượng bộ nhớ cần thiết cho mỗi trường hợp. Phương pháp thứ hai,  
thông tin có thể được lưu trữ thông qua Hệ thống cấp phát động của C. Trong phương pháp này, sự  
lưu trữ thông tin được cấp phát từ vùng nhớ còn tự do và khi cần thiết.  
Hàm malloc() một trong các hàm thường được dùng nhất, nó cho phép thực hiện việc cấp phát bộ  
nhớ từ vùng nhớ còn tự do. Tham số cho malloc() một số nguyên xác định số bytes cần thiết.  
Một dụ khác, xét mảng tự hai chiều ch_ary có 10 dòng và 20 cột. Sự khai báo và cấp phát bộ  
nhớ trong trường hợp này phải như sau:  
char (*ch_ary)[20];  
ch_ary = (char*)malloc(10*20*sizeof(char));  
Như đã nói trên, malloc() trả về một con trỏ trỏ đến kiểu rỗng (void). Tuy nhiên, vì ch_ary một  
con trỏ kiểu char, sự chuyển đổi kiểu cần thiết. Trong câu lệnh trên, (char*) đổi kiểu trả về của  
malloc() thành một con trỏ trỏ đến kiểu char.  
Tuy nhiên, nếu sự khai báo của mảng phải chứa phép gán các giá trị khởi tạo thì mảng phải được khai  
báo theo cách bình thường, không thể dùng một biến con trỏ:  
int ary[10] = {1,2,3,4,5,6,7,8,9,10};  
hoặc  
int ary[] = {1,2,3,4,5,6,7,8,9,10};  
190  
Lập trình cơ bản C  
dụ sau đây tạo một mảng một chiều sắp xếp mảng theo thứ tự tăng dần. Chương trình sử dụng  
con trỏ và hàm malloc() để gán bộ nhớ.  
#include<stdio.h>  
#include<malloc.h>  
void main()  
{
int *p, n, i, j, temp;  
printf("\n Enter number of elements in the array: ");  
scanf("%d", &n);  
p = (int*) malloc(n * sizeof(int));  
for(i = 0; i < n; ++i)  
{
printf("\nEnter element no. %d:", i + 1);  
scanf("%d", p + i);  
}
for(i = 0; i < n - 1; ++i)  
for(j = i + 1; j < n; ++j)  
if(*(p + i) > *(p + j))  
{
temp = *(p + i);  
*(p + i) = *(p + j);  
*(p + j) = temp;  
}
for(i = 0; i < n; ++i)  
printf("%d\n", *(p + i));  
}
Chú ý lệnh malloc():  
p = (int*)malloc(n*sizeof(int));  
Ở đây, p được khai báo như một con trỏ trỏ đến một mảng được gán bộ nhớ sử dụng malloc().  
Dữ liệu được đọc vào sử dụng lệnh scanf().  
scanf("%d",p+i);  
Trong scanf(), biến con trỏ được sử dụng để lưu dữ liệu vào trong mảng.  
Các phần tử mảng đã lưu trữ được hiển thị bằng printf().  
printf("%d\n", *(p + i));  
Chú ý dấu * trong trường hợp này, vì giá trị lưu trong vị trí đó phải được hiển thị. Không có dấu *,  
printf() sẽ hiển thị địa chỉ.  
free()  
Con trỏ  
191  
Hàm này có thể được sử dụng để giải phóng bộ nhớ khi nó không còn cần thiết.  
Dạng tổng quát của hàm free():  
void free(void *ptr );  
Hàm free() giải phóng không gian được trỏ bởi ptr, không gian được giải phóng này có thể sử dụng  
trong tương lai. ptr đã sử dụng trước đó bằng cách gọi đến malloc(), calloc(), hoặc realloc(), calloc()  
và realloc() (sẽ được thảo luận sau).  
dụ bên dưới sẽ hỏi bạn có bao nhiêu số nguyên sẽ được bạn lưu vào trong một mảng. Sau đó sẽ cấp  
phát bộ nhớ động bằng cách sử dụng malloc lưu số lượng số nguyên, in chúng ra, và sau đó xóa bộ  
nhớ cấp phát bằng cách sử dụng free.  
#include <stdio.h>  
#include <stdlib.h> /* required for the malloc and free functions */  
int main()  
{
int number;  
int *ptr;  
int i;  
printf("How many ints would you like store? ");  
scanf("%d", &number);  
ptr = (int *) malloc (number * sizeof(int)); /*allocate memory*/  
if(ptr != NULL)  
{
for(i = 0 ; i < number ; i++)  
{
*(ptr+i) = i;  
}
for(i=number ; i>0 ; i--)  
{
printf("%d\n", *(ptr+(i-1))); /*print out in reverse order*/  
}
free(ptr); /* free allocated memory */  
return 0;  
}
else  
{
printf("\nMemory allocation failed - not enough memory.\n");  
return 1;  
}
}
Kết quả như sau nếu giá trị được nhập vào 3:  
How many ints would you like store? 3  
2
1
0
calloc()  
192  
Lập trình cơ bản C  
calloc tương tự nmalloc, nhưng khác biệt chính là mặc nhiên các giá trị được lưu trong không gian  
bộ nhớ đã cấp phát là 0. Với malloc, cấp phát bộ nhớ thể có giá trị bất kỳ.  
calloc đòi hỏi hai đối số. Đối số thứ nhất số các biến bạn muốn cấp phát bộ nhớ cho. Đối số thứ  
hai là kích thước của mỗi biến.  
void *calloc( size_t num, size_t size );  
Giống như malloc, calloc sẽ trả về một con trỏ rỗng (void) nếu sự cấp phát bộ nhớ là thành công,  
ngược lại sẽ trả về một con trỏ NULL.  
dụ bên dưới chỉ ra cho bạn gọi hàm calloc như thế nào và tham chiếu đến ô nhớ đã cấp phát sử  
dụng một chỉ số mảng. Giá trị khởi tạo của vùng nhớ đã cấp phát được in ra trong vòng lặp for.  
#include <stdio.h>  
#include <stdlib.h>  
int main()  
{
float *calloc1, *calloc2;  
int i;  
calloc1 = (float *) calloc(3, sizeof(float));  
calloc2 = (float *) calloc(3, sizeof(float));  
if(calloc1 != NULL && calloc2 != NULL)  
{
for(i = 0; i < 3; i++)  
{
printf("\ncalloc1[%d] holds %05.5f ", i, calloc1[i]);  
printf("\ncalloc2[%d] holds %05.5f", i, *(calloc2 + i));  
}
free(calloc1);  
free(calloc2);  
return 0;  
}
else  
{
printf("Not enough memory\n");  
return 1;  
}
}
Kết quả:  
calloc1[0] holds 0.00000  
calloc2[0] holds 0.00000  
calloc1[1] holds 0.00000  
calloc2[1] holds 0.00000  
calloc1[2] holds 0.00000  
calloc2[2] holds 0.00000  
Trong tất cả các máy, các mảng calloc1 và calloc2 phải chứa các giá trị 0. calloc đặc biệt hữu dụng khi  
bạn đang sử dụng mảng đa chiều. Đây một dụ khác minh họa cách dùng của hàm calloc().  
Con trỏ  
193  
/* This program gets the number of elements, allocates  
spaces for the elements, gets a value for each  
element, sum the values of the elements, and print  
the number of the elements and the sum.  
*/  
#include <stdio.h>  
#include <stdlib.h>  
main()  
{
int *a, i, n, sum = 0;  
printf(“\n%s%s”, “An array will be created dynamically. \n\n”,  
“Input an array size n followed by integers: ”);  
scanf( “%d”, &n); /* get the number of elements */  
a = (int *) calloc (n, sizeof(int));  
/* get a value for each element */  
/* allocate space */  
for( i = 0; i < n; i++ )  
{
printf(“Enter %d values: “, n);  
scanf(“%d”, a + i);  
}
/* sum the values */  
for(i = 0; i < n; i++ )  
sum += a[i];  
free(a);  
/* free the space */  
/* print the number and the sum */  
printf(“\n%s%7d\n%s%7d\n\n”, “Number of elements: ”, n,  
“Sum of the elements: ”, sum);  
}
realloc()  
Giả sử chúng ta đã cấp phát một số bytes cho một mảng nhưng sau đó nhận ra là bạn muốn thêm các  
giá trị. Bạn thể sao chép mọi thvào một mảng lớn hơn, cách này không hiệu quả. Hoặc bạn thể  
cấp phát thêm các bytes sử dụng bằng cách gọi hàm realloc, mà dữ liệu của bạn không bị mất đi.  
realloc() nhận hai đối số. Đối số thứ nhất một con trỏ tham chiếu đến bộ nhớ. Đối số thứ hai là tổng  
số bytes bạn muốn cấp phát thêm.  
void *realloc( void *ptr, size_t size );  
Truyền 0 như đối số thhai thì tương đương với việc gọi hàm free.  
Một lần, realloc trả về một con trỏ rỗng (void) nếu thành công, ngược lại một con trỏ NULL được trả  
về.  
dụ này sử dụng calloc để cấp phát đủ bộ nhớ cho một mảng int có năm phần tử. Sau đó realloc  
được gọi để mở rộng mảng để thể chứa bảy phần tử.  
194  
Lập trình cơ bản C  
#include<stdio.h>  
#include <stdlib.h>  
int main()  
{
int *ptr;  
int i;  
ptr = (int *)calloc(5, sizeof(int *));  
if(ptr!=NULL)  
{
*ptr = 1;  
*(ptr + 1) = 2;  
ptr[2] = 4;  
ptr[3] = 8;  
ptr[4] = 16;  
/* ptr[5] = 32; wouldn't assign anything */  
ptr = (int *)realloc(ptr, 7 * sizeof(int));  
if(ptr!=NULL)  
{
printf("Now allocating more memory... \n");  
ptr[5] = 32; /* now it's legal! */  
ptr[6] = 64;  
for(i = 0;i < 7; i++)  
{
printf("ptr[%d] holds %d\n", i, ptr[i]);  
}
realloc(ptr, 0); /* same as free(ptr); - just fancier! */  
return 0;  
}
else  
{
printf("Not enough memory - realloc failed.\n");  
return 1;  
}
}
else  
{
printf("Not enough memory - calloc failed.\n");  
return 1;  
}
}
Kết quả:  
Now allocating more memory...  
ptr[0] holds 1  
ptr[1] holds 2  
ptr[2] holds 4  
ptr[3] holds 8  
Con trỏ  
195  
ptr[4] holds 16  
ptr[5] holds 32  
ptr[6] holds 64  
Chú ý hai cách khác nhau được sử dụng khi khởi tạo mảng: ptr[2] = 4 là tương đương với *(ptr + 2) =  
4 (chỉ dễ đọc hơn!).  
Trước khi sử dụng realloc, việc gán một giá trị đến phần tử ptr[5] không gây ra lỗi cho trình biên dịch.  
Chương trình vẫn thực thi, nhưng ptr[5] không chứa giá trị bạn đã gán.  
196  
Lập trình cơ bản C  
Tóm tắt bài học  
Một con trỏ cung cấp một phương thức truy xuất một biến mà không cần tham chiếu trực tiếp đến  
biến.  
Một con trỏ một biến, chứa địa chỉ vùng nhớ của một biến khác.  
Sự khai báo con trỏ bao gồm một kiểu dữ liệu cơ sở, một dấu *, và một tên biến.  
Có hai toán tử đặc biệt được dùng với con trỏ: * và &.  
Toán tử & trả về địa chỉ bộ nhớ của toán hạng.  
Toán tử thứ hai, *, phần bổ xung của toán tử &. Nó trả về giá trị được chứa trong vị trí bộ nhớ  
được trỏ bởi con trỏ.  
Chỉ có phép cộng và phép trừ là có thể được thực thi với con trỏ.  
Hai con trỏ thể được so sánh trong một biểu thức quan hệ chỉ khi cả hai biến này cùng trỏ đến  
các biến có cùng kiểu dữ liệu.  
Các con trỏ được truyền tới hàm như các đối số.  
Một tên mảng thật ra là một con trỏ trỏ đến phần tử đầu tiên của mảng.  
Một hằng con trỏ một địa chỉ; một biến con trỏ một nơi để lưu địa chỉ.  
Bộ nhthể được cấp phát khi cần dùng bằng cách dùng các hàm malloc(),calloc(),realloc(). Sự  
cấp phát bộ nhớ theo cách này được gọi sự cấp phát bộ nhớ động.  
Con trỏ  
197  
Kiểm tra tiến độ học tập  
1. Một _________ cung cấp một phương thức truy xuất một biến mà không tham chiếu trực tiếp đến  
biến.  
A. Mảng  
B. Con trỏ  
C. Cấu trúc  
D. Tất cả đều sai  
2. Các con trỏ không thể trỏ đến các mảng.  
(Đúng/Sai)  
3. __________ của con trỏ xác định kiểu của các biến mà con trỏ thể trỏ đến.  
A. Kiểu  
B. Kích thước  
C. Nội dung  
D. Tất cả đều sai  
4. Có hai toán tử đặc biệt được dùng với con trỏ ____ và _____.  
A. ^ và %  
C. * và &  
B. ; và ?  
D. Tất cả đều sai  
5. Chỉ có ________ và __________ là những phép toán có thể được thực hiện trên các con trỏ.  
A. Cộng, Trừ  
C. Chia, Cộng  
B.Nhân, Chia  
D. Tất cả đều sai  
6. Hai con trỏ thể được so sánh chỉ khi cả hai biến này đang trỏ đến các kiểu dữ liệu khác nhau.  
7. Sự cấp phát bộ nhớ theo cách này, nghĩa là, khi trong chương trình có yêu cầu được gọi là  
__________ .  
A. Cấp phát bộ nhớ động  
C. Cấp phát bộ nhớ nội dung  
B. Cấp phát bộ nhớ tĩnh  
D. Tất cả đều sai  
198  
Lập trình cơ bản C  
Bài tập tự làm  
1. Viết một chương trình để nhận vào một chuỗi và in ra nó nếu đó chuỗi đọc xuôi – ngược đều  
giống nhau.  
2. Viết một chương trình sử dụng con trỏ trỏ đến các chuỗi để nhận tên của một con thú và một con  
chim và trả về các tên theo dạng số nhiều.  
Con trỏ  
199  
doc 19 trang Thùy Anh 26/04/2022 6460
Bạn đang xem tài liệu "Giáo trình Lập trình cơ bản C - Bài 13: Con trỏ", để tải tài liệu gốc về máy hãy click vào nút Download ở trên

File đính kèm:

  • docgiao_trinh_lap_trinh_co_ban_c_bai_13_con_tro.doc