Bài 13: Con trỏ [Lý Thuyết]
Mục tiêu:
Kết thúc bài học này, bạn có 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ỏ là một biến, 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 là 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ư là một con
trỏ tới biến var1, sự biểu diễn sẽ như sau:
Vị
trí Giá
trị
Tên
Bộ nhớ lưu
trữ biến
1000
500
var1
1001
1002
.
.
1108
1000
var2
Ở đây, var2 chứa giá trị 1000, đó là địa chỉ của biến var1.
Các con trỏ có 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ó cấu trúc như mảng.
13.1.2 Tại sao con trỏ được dùng?
Con trỏ có 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
và 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ỏ, nó 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 là một kiểu dữ liệu hợp lệ bất kỳ,
và 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 là name được sử dụng để lưu địa chỉ của
một biến có 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 và var2 ỏ trên,
vì var2 là một con trỏ giữ địa chỉ của biến var1 có kiểu int,
nó sẽ được khai báo như sau:
int *var2;
Bây giờ, var2 có 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 có kiểu dữ liệu int nhưng
nó là một con trỏ trỏ đến một biến có 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ỏ có kiểu bất kỳ có 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ỏ: * và &. Toán tử & là một toán tử một ngôi và nó trả về
địa chỉ của toán hạng. Ví 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ử & có thể hiểu là 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ử *, là phần bổ sung của 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
var2 =
&var1;
var2 chứa giá trị 1000, và sau lệnh gán:
temp =
*var2;
temp sẽ
chứa 500, là giá trị của biến mà var2
trỏ đến. Toán tử * có
thể được hiểu là: “giá trị của”.
Cả hai toán tử * và & có độ ưu tiên cao hơn tất cả cá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 nó được lưu trong một biến con trỏ, và chương trình cũng in ra địa
chỉ của biến con trỏ.
#include <stdio.h>
#include <conio.h>
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);
getch();
}
Kết quả của ví dụ trên như sau:
Trong ví dụ trên, ptr_var chứa địa chỉ 2686788, là
địa chỉ vùng nhớ lưu trữ giá trị của var. Nội dung ô nhớ 2686788 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 tương ứng với giá trị 500, là giá trị của var. Bởi
vì ptr_var cũng là 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ỉ 2686784. 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).
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ị có 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 có 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 có thể được gán đến
một con trỏ bằng số 0 như sau:
ptr_var = 0;
Các biến cũng có 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 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 có 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 và trừ là các toán tử có thể thực hiện trên các con
trỏ. Ví 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 là 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ị.
Đây là một vài ví dụ:
++ptr_var or
ptr_var++
|
Trỏ
đến số nguyên kế tiếp đứng sau var
|
--ptr_var or
ptr_var--
|
Trỏ
đến số nguyên đứng trước var
|
ptr_var + i
|
Trỏ
đến số nguyên thứ i sau var
|
ptr_var - i
|
Trỏ
đến số nguyên thứ i trước var
|
++*ptr_var or
(*ptr_var)++
|
Sẽ
tăng trị var bởi 1
|
*ptr_var++
|
Sẽ
tác động đến giá trị của số nguyên kế tiếp sau var
|
Mỗi khi một con trỏ được tăng giá trị, nó sẽ trỏ đến ô nhớ của
phần tử kế tiếp. Mỗi khi nó được giảm giá trị, nó 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ự, nó xuất hiện bình thường,
bởi vì mỗi ký 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ị và
giảm trị, các số nguyên cũng có thể được cộng vào và trừ ra với con trỏ. Ngoài
phép cộng và 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ỏ có thể được so sánh trong một biểu thức quan hệ. Tuy
nhiên, điều này chỉ có thể nếu cả hai biến này đều trỏ đến các biến có cùng
kiểu dữ liệu. ptr_a và ptr_b là hai biến con
trỏ trỏ đến các phần tử dữ liệu a và 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
|
Trả
về giá trị true nếu a được lưu trữ ở vị trí trước b
|
ptr_a > ptr_b
|
Trả
về giá trị true nếu a được lưu trữ ở vị trí sau b
|
ptr_a <= ptr_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
|
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í
|
ptr_a == ptr_b
|
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.
|
ptr_a != ptr_b
|
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 và 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ỏ và 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ì vậy, nếu ary là một mảng một chiều, thì địa
chỉ của phần tử đầu tiên trong mảng có thể được biểu diễn là &ary[0] hoặc
đơn giản chỉ là 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 là &ary[i] hay (ary+i).
Như vậy, địa chỉ của một phần tử mảng bất kỳ có thể được biểu diễn theo hai
cách:
Ø Sử dụng ký 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.
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ử có thể là 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ì vậy, biểu thức ở trên không chỉ là một phép
cộng; nó 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) là một sự trình bày cho một địa chỉ chứ không
phải là 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ư là kiến trúc của máy tính. Tuy
nhiên, người lập trình chỉ có 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ư là một độ dời khi được dùng theo cách này.
Các biểu thức &ary[i] và (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] và *(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
và địa chỉ của chúng.
#include<stdio.h>
#include<conio.h>
main()
{
static int ary[10] =
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int i;
for (i = 0; i <
10; i ++)
{
printf("\ni=%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
}
getch();
}
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ị và đị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ả của chương trình
trên:
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, và &ary[i] -
biểu diễn địa chỉ của nó.
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 là ary[i] hoặc *(ary
+ i). Vì vậy, một giá trị có 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ớ mà đị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ì vậy,
các biểu thức như ary, (ary + i) và &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 như ary++ là
không được phép. Lý do là vì: ary là đị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, ví 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 vô nghĩa, giống như khi nói
x
= 5++;
Bởi vì hằng không thể được tăng giá 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ư
là 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 có thể được gán cho một phần tử khác
thông qua con trỏ.
&ary[2] = &ary[3]; /*
không cho phép*/
ary[2]
= ary[3]; /*
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 là 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 có 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ỏ và mảng
nhiều chiều
Một mảng nhiều chiều cũng có 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) và một độ dời (chỉ số). Thực
hiện được điều này là bởi vì một mảng nhiều chiều là 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 có thể được định nghĩa như là 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 có 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];
Trong các khai báo trên, data_type là kiểu dữ liệu của
mảng, ptr_var là tên của biến con trỏ, array là tên mảng,
và exp 1, exp 2, exp 3, ... exp N là 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 và 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.
Ví dụ, nếu ary là 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
là 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 có 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ư là 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_ary trỏ đế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ì 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].
Có 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ỏ và chuỗi
Chuỗi đơn giản chỉ là một mảng một chiều có kiểu ký tự. Mảng và
con trỏ có 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ẽ
có mối liên hệ mật thiết với con trỏ. Xem trường hợp hàm strchr(). Hàm
này nhận các tham số là một chuỗi và một ký tự để tìm kiếm ký 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 ký tự ‘a’
đầu tiên xuất hiện trong chuỗi str. Đây không phải là 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 là chương trình cho phép người dùng nhập vào một chuỗi và một ký tự
để tìm kiếm. Chương trình in ra địa chỉ bắt đầu của chuỗi, địa chỉ của ký tự,
và vị trí tương đối của ký tự trong chuỗi (0 là vị trí của ký tự đầu tiên, 1 là
vị trí của ký 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 và địa chỉ nơi mà ký tự cần tìm đầu tiên xuất
hiện.
#include <stdio.h>
#include <conio.h>
#include <string.h>
main ()
{
char
a, str[81], *ptr;
printf("\nEnter a sentence:");
gets(str);
printf("\nEnter
character to search for:");
a
= getchar();
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);
getch();
}
Kết quả:
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 là một
địa chỉ của một ký tự (ptr có kiểu char).
Để sử dụng hàm strchr(),
thư viện string.h phải đượ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 có thể định nghĩa một mảng như là 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ả là 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ư là một biến con trỏ.
Sử dụng một biến con trỏ để biểu 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ử có 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ư là 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ư là một con trỏ, số
lượng bộ nhớ có 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 nó 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 là 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 và 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ữ là 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() là 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() là một số nguyên xác định số bytes cần
thiết.
Một ví dụ khác: xét mảng ký 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 là một con trỏ
kiểu char, sự chuyển đổi kiểu là 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};
Ví dụ sau đây tạo một mảng một chiều và 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 <conio.h>
#include<malloc.h>
main()
{
int *p, n, i, j, temp;
printf("\nEnter 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("\n%d", *(p + i));
getch();
}
p = (int*)malloc(n*sizeof(int));
Ở đây, p được khai báo như một con trỏ trỏ đến một mảng và đượ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ỉ.
Ø Hàm free()
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).
Ví 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 và
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 <conio.h>
#include <stdlib.h> /* required for the malloc and
free functions */
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("\n%d",
*(ptr+(i-1))); /*print out in reverse order*/
}
free(ptr); /* free allocated memory */
}
else
{
printf("\nMemory allocation failed - not enough
memory.\n");
}
getch();
}
Kết quả như sau nếu giá trị
được nhập vào 3:
Ø Hàm calloc()
calloc tương
tự như malloc, 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ớ có thể có giá trị bất kỳ.
calloc đòi
hỏi hai đối số. Đối số thứ nhất là số các biến mà 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 nó sẽ trả về một
con trỏ NULL.
Ví 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 <conio.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);
}
else
{
printf("Not
enough memory\n");
}
getch();
}
Kết quả:
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 là một ví dụ khác minh họa cách dùng của hàm calloc().
/* 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 <conio.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)); //allocate space
//get a value for each element
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);
getch();
Ø Hàm 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 có thể sao chép mọi thứ vào một mảng
lớn hơn, cách này không hiệu quả. Hoặc bạn có 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 là 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ư là đối số thứ hai 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ề.
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 để có thể chứa bảy phần tử.
#include<stdio.h>
#include <conio.h>
#include <stdlib.h>
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! */
}
else
printf("Not
enough memory - realloc failed.\n");
}
else
printf("Not
enough memory - calloc failed.\n");
getch();
}
Kết quả:
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ị mà bạn đã gán.
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ỏ là 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, *, là 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ỏ có 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ỏ là một địa chỉ; một biến
con trỏ là một nơi để lưu địa chỉ.
Ø Bộ nhớ có thể đượ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 là sự cấp phát bộ nhớ động.
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ỏ có 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ỏ là ____ và _____.
A.
^ và %
|
B.
; và ?
|
C.
* 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ừ
|
B.Nhân,
Chia
|
C.
Chia, Cộng
|
D.
Tất cả đều sai
|
6. Hai con trỏ có 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.
(Đúng/Sai)
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
|
B.
Cấp phát bộ nhớ tĩnh
|
C.
Cấp phát bộ nhớ nội dung
|
D.
Tất cả đều sai
|
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 đó là 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.
0 nhận xét:
Đăng nhận xét