You are on page 1of 36

CÔNG TY CỔ PHẦN LUMI VIỆT NAM

Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội


0904 665 965
www.lumi.vn

BÀI 9: Con trỏ


1. Mục tiêu bài học:
Con trỏ là một công cụ hiệu quả giúp tăng hiệu năng của chương trình và
thực tế nhiều nhiệm vụ không thể thực hiện được nếu như không sử dụng
con trỏ. Trong bài viết này chúng ta sẽ tìm hiểu về con trỏ trong ngôn ngữ
lập trình C theo các vấn đề chính sau:

○ Khai báo con trỏ.

○ Truy cập con trỏ.

○ Sử dụng con trỏ hiệu quả.

○ Sử dụng con trỏ nâng cao

2. Tài liệu tham khảo


Tổng quan về con trỏ

SỰ CHUYỂN HƯỚNG (INDIRECTION)

1, Con trỏ rất giống khái niệm về chuyển hướng mà bạn sử dụng trong
cuộc sống hàng ngày. Giả sử bạn cần mua một hộp mực mới cho máy in
cho công ty của mình. Tất cả giao dịch mua đều được thực hiện bởi phòng
mua hàng. Bạn gọi Joe của phòng mua hàng và yêu cầu anh ấy đặt cho
bạn một hộp mực mới Sau đó Joe gọi đến cửa hàng ở địa phương để đặt
hộp mực. Có thể thấy là bạn không tự mình trực tiếp đặt hộp mực từ cửa
hàng (chuyển hướng).
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

2, Ở trong ngôn ngữ lập trình, chuyển hướng là khả năng tham chiếu đến
một cái gì đó sử dụng tên, tham chiếu, hoặc vùng chứa, thay vì sử dụng
giá trị của chính nó. Hình thức chuyển hướng phổ biến nhất là thao tác một
giá trị thông qua địa chỉ bộ nhớ của nó.

3, Con trỏ cung cấp một phương tiện gián tiếp để truy cập giá trị của một
hạng mục dữ liệu cụ thể. Biến có giá trị là địa chỉ bộ nhớ. Giá trị của của
con trỏ là địa chỉ của một vị trí khác trong bộ nhớ mà có chứa giá trị.

TỔNG QUAN VỀ CON TRỎ

1, Có nhiều lý do để giải thích sự hợp lý khi bạn thông qua phòng mua
hàng để đặt mua hộp mực (bạn không phải biết chính xác hộp mực đó
được đặt từ cửa hàng nào). Việc sử dụng con trỏ trong C cũng đem lại
nhiều hiệu quả tốt.

2, Sử dụng con trỏ trong chương trình là một trong những công cụ hữu ích
nhất có sẵn trong ngôn ngữ C, tuy nhiên con trỏ cũng là một trong những
khái niệm khó hiểu nhất của ngôn ngữ C. Điều quan trọng là bạn phải nắm
được khái niệm này ngay từ đầu và duy trì ý tưởng rõ ràng về việc nó hoạt
động như thế nào khi bạn đào sâu tìm hiểu về nó.

3, Trình biên dịch phải biết kiểu dữ liệu lưu trữ trong biến mà nó trỏ đến.
Cần biết bộ nhớ bị chiếm bao nhiêu hoặc làm thế nào để xử lý nội dung
của bộ nhớ mà nó trỏ đến. Mỗi con trỏ sẽ được liên kết đến một loại biến
cụ thể. Nó có thể sử dụng chỉ để trỏ đến biến của loại đó. Ví dụ như con trỏ
kiểu "pointer to int" chỉ có thể trỏ đến biến kiểu int.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

4, Bạn hãy xem ví dụ sau: Giá trị của &numbers chính là địa chỉ mà
number được lưu trữ. Giá trị địa chỉ này được sử dụng để khởi tạo cho
biến con trỏ pnumber ở câu lệnh tiếp theo.

TẠI SAO LẠI SỬ DỤNG CON TRỎ

1, Việc truy cập dữ liệu chỉ thông qua các biến là rất hạn chế.

Với con trỏ, bạn có thể truy cập bất kỳ vị trí nào (ví dụ bạn có thể coi bất kì
vị trí nào của bộ nhớ như là biến ) và thực hiện số học với con trỏ.

2, Con trỏ trong C giúp sử dụng mảng và chuỗi dễ dàng hơn.

Con trỏ cho phép bạn tham chiếu đến cùng một không gian trong bộ nhớ
từ nhiều vị trí khác nhau. Có nghĩa là bạn có thể cập nhật bộ nhớ ở một vị
trí và sự thay đổi đó có thể được nhìn thấy từ vị trí khác trong chương trình
của bạn.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Cũng có thể tiết kiệm không gian bộ nhớ bằng cách có thể chia sẻ các
thành phần trong cấu trúc dữ liệu của bạn

4, Con trỏ cho phép hàm thay đổi dữ liệu được truyền vào hàm như là 1
tham biến.

5, Con trỏ được sử dụng để tối ưu chương trình, giúp chương trình chạy
nhanh hơn hoặc sử dụng ít bộ nhớ hơn.

6, Con trỏ cho phép chúng ta lấy được nhiều giá trị từ hàm. Một hàm có
thể trả về một giá trị, nhưng truyền nhiều đối số như biến thì chúng ta có
thể nhận về nhiều hơn 1 giá trị từ con trỏ.

7, Với con trỏ, bộ nhớ động có thể được tạo ra theo cách mà chương trình
sử dụng. Chúng ta có thể tiết kiệm bộ nhớ từ khai báo tĩnh (thời gian biên
dịch).

8, Con trỏ giúp chúng ta thiết kế và phát triển cấu trúc dữ liệu phức tạp, ví
dụ như stack, queue, hoặc linked list

9, Con trỏ cung cấp quyền truy cập bộ nhớ trực tiếp cho các lập trình viên.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Khai báo con trỏ

KHAI BÁO CON TRỎ

1, Con trỏ không được khai báo như là biến thông thường.

pointer ptr; // Đây không phải là cách để khai báo 1 con trỏ

2, Nếu nói một biến là con trỏ thì là chưa đủ. Bạn phải chỉ định loại biến mà
con trỏ trỏ tới. Các loại biến khác nhau sẽ chiếm dung lượng lưu trữ khác
nhau. Một số xử lý của con trỏ yêu cầu kiến thức về kích thước lưu trữ đó.

Bạn khai báo một con trỏ cho một biến kiểu int với:

int * pnumber;

3, Kiểu của biến pnumber là int*. Biến này có thể lưu trữ địa chỉ của bất kì
biến nào có kiểu int.

4, Khoảng trắng giữa dấu hoa thị * và tên con trỏ là tùy ý. Người lập trình
sử dụng khoảng trắng trong khai báo và bỏ nó khi tham chiếu ngược đến
một biến.

5, Giá trị của con trỏ là địa chỉ, và nó được thể hiện nội bộ như là một số
nguyên không dấu ở hầu hết các hệ điều hành. Tuy nhiên, bạn không nên
nghĩ con trỏ là kiểu int. Bởi vì có những điều bạn có thể làm với số nguyên
mà bạn không thể làm với con trỏ, và ngược lại. Ví dụ như bạn có thể nhân
một số nguyên với một số nguyên khác, nhưng bạn không thể nhân một
con trỏ với một con trỏ khác.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

6, Con trỏ thực sự là một kiểu mới, không phải là kiểu integer. %p đại diện
cho thông số định dạng của con trỏ.

7, Khi khai báo 1 con trỏ thì bạn cần thiết phải khởi tạo giá trị cho con trỏ
đó.

CON TRỎ NULL

1, Bạn có thể khai báo một con trỏ để nó không trỏ đến bất kì cái gì.

int *pnumber = NULL;

2, NULL là hằng số được định nghĩa trong thư viện tiêu chuẩn của ngôn
ngữ C. Đối với con trỏ thì nó tương đương giá trị 0.

3, NULL là giá trị đảm bảo con trỏ không trỏ đến bất kì vị trí nào trong bộ
nhớ. Có nghĩa là nó ngăn chặn hoàn toàn việc ghi đè bộ nhớ bởi sử dụng
con trỏ mà không trỏ đến một cái gì xác định. Để sử dụng NULL thì bạn
thêm chỉ định #include <stddef.h> trong file mã lệnh của bạn.

TOÁN TỬ ĐỊA CHỈ

1, Nếu bạn muốn khởi tạo biến với địa chỉ của biến mà bạn đã khai báo thì
sử dụng địa chỉ của toán tử: &

int number = 99;

int *pnumber = &number;

Giá trị pnumber đầu tiên là địa chỉ của biến number.

Phần khai báo số phải nằm trước phần khai báo con trỏ mà trỏ đến địa chỉ
của nó.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Trình biên dịch phải có sẵn không gian và địa chỉ cho số để sử dụng nó để
khởi tạo pnumber.

CHÚ Ý

Không có gì đặc biệt về việc khai báo một con trỏ. Có thể khai báo các biến
thông thường và con trỏ trong cùng một câu lệnh:

double value, *pVal, fnum;

⇒chỉ biến thứ hai, pVal, là con trỏ

Hay một ví dụ khác:

int *p, q;

⇒Phần trên khai báo một con trỏ, p có kiểu int*, và một biến, q, có kiểu int

Một nhầm lẫn khá phổ biến đó là nghĩ cả p và q đều là con trỏ. Ngoài ra,
bạn nên sử dụng các tên bắt đầu bằng p để chỉ định tên con trỏ.

Truy cập con trỏ

TRUY CẬP GIÁ TRỊ CỦA CON TRỎ

1, Bạn sử dụng toán tử điều hướng * để truy cập giá trị của biến được trỏ
đến bởi con trỏ. Toán tử * còn được gọi là toán tử tham chiếu ngược bởi vì
bạn sử dụng nó để "tham chiếu ngược" con trỏ.

int number = 15;

int *pointer = &number;

int result = 0;
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

⇒Biến con trỏ chứa địa chỉ của biến number. Bạn có thể sử dụng nó trong
biểu thức để tính toán một giá trị mới cho biến result.

result = *pointer + 5;

Biểu thức *pointer sẽ tính toán giá trị được lưu trữ ở địa chỉ mà chứa con
trỏ. Giá trị được lưu trữ trong number là 15, vì vậy result = 15 + 5 ⇒ result
= 20.

2, Toán tử điều hướng * cũng là biểu tượng của phép nhân, và nó dược
sử dụng để chỉ định kiểu của con trỏ.

Phụ thuộc vào vị trí của dấu hoa thị, trình biên dịch sẽ hiểu là sử dụng nó
là toán tử chuyển hướng, hay là dấu nhân, hay là một phần của việc chỉ
định kiểu. Có nghĩa là bối cảnh sử dụng sẽ xác định ý nghĩa của nó.

Các bạn hãy xem 1 ví dụ về truy cập giá trị con trỏ như sau nhé:
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

HIỂN THỊ GIÁ TRỊ CỦA CON TRỎ

1, Để xuất ra địa chỉ của biến, bạn sử dụng định dạng kiểu xuất ra %p. Lúc
này chương trình sẽ xuất ra giá trị con trỏ dưới dạng địa chỉ bộ nhớ ở dạng
thập lục phân.

2, Con trỏ chiếm 8 byte và địa chỉ có 16 kí tự thập lục phân nếu máy tính
của bạn chạy hệ điều hành 64-bit và trình biên dịch hỗ trợ địa chỉ 64-bit.

Một số trình biên dịch chỉ hỗ trợ địa chỉ 32-bit, trong trường hợp đó, địa chỉ
sẽ là 32-bit.

Bạn hãy xem ví dụ về hiển thị địa chỉ con trỏ như sau:

2, Hãy nhớ rằng, bản thân con trỏ có địa chỉ, cũng như bất kì một biến số
nào, cho nên bạn sử dụng %p làm thông số chuyển đổi để hiển thị một địa
chỉ.

Bạn sử dụng toán tử & để tham chiếu địa chỉ mà biến pnumber chiếm giữ.

Ngoài ra thì ép kiểu thành void* sẽ ngăn chặn cảnh báo từ trình biên dịch.

Thông số %p mong đợi giá trị là một kiểu con trỏ nào đó, nhưng kiểu của
&pnumber là "trỏ đến 1 con trỏ của kiểu số nguyên (pointer to pointer to
int)"
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

HIỂN THỊ SỐ BYTE MÀ CON TRỎ SỬ DỤNG

1, Bạn sử dụng toán tử sizeof để lấy được số byte mà một con trỏ chiếm.
Nếu trên máy tính con trỏ chiếm 8 byte thì địa chỉ bộ nhớ sẽ là 64 bit.

2, Toán tử sizeof trả về kiểu size_t. size_t là kiểu số nguyên được định
nghĩa lại. Bạn có thể nhận được cảnh báo từ trình biên dịch khi sử dụng
sizeof.

Để ngăn chặn cảnh báo, bạn có thể ép kiểu đối số thành kiểu int như sau:

Bây giờ bạn hãy mở Code::Blocks và gõ thử đoạn mã lệnh sau vào để
thực hành về cách truy cập với con trỏ trong bài học này nhé.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Cách sử dụng con trỏ

TỔNG QUAN

1, Ngôn ngữ C cung cấp một số thao tác cơ bản mà bạn có thể sử dụng
với con trỏ.

Bạn có thể gán địa chỉ cho con trỏ. Giá trị được gán có thể là tên mảng,
biến đứng sau toán tử địa chỉ (&), hoặc một con trỏ thứ hai khác.

2, Bạn có thể tham chiếu ngược con trỏ. Toán tử * cung cấp giá trị được
lưu trữ trong vị trí được trỏ đến (như đã học ở bài trước).

3, Bạn có thể lấy một địa chỉ con trỏ. Toán tử & cho bạn biết con trỏ được
lưu trữ ở đâu.

4, Bạn có thể thực hiện các phép toán số học cho con trỏ

⇒Sử dụng toán tử + để thêm một số nguyên vào một con trỏ hoặc thêm
con trỏ vào số nguyên (số nguyên được nhân với số byte trong kiểu được
trỏ đến, và thêm vào địa chỉ gốc) để tăng dần từng địa chỉ cho con trỏ (hữu
ích trong mảng khi di chuyển sang phần tử tiếp theo).
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

⇒Sử dụng toán tử - để trừ một số nguyên từ con trỏ (số nguyên được
nhân với số byte trong kiểu trỏ đến và bị trừ bởi địa chỉ gốc) để giảm dần
từng địa chỉ cho con trỏ (hữu ích trong mảng khi di chuyển về phần tử
trước).

5, Bạn có thể thấy sự khác biệt giữa hai con trỏ bằng cách sử dụng 2 con
trỏ trỏ đến các phần tử nằm trong cùng một mảng để tìm ra khoảng cách
giữa các phần tử.

6, Bạn có thể sử dụng toán tử quan hệ để so sánh giá trị của hai con trỏ có
cùng kiểu.

7, Có hai hình thức của phép trừ với con trỏ. Bạn có thể trừ một con trỏ từ
một con trỏ khác để lấy được số nguyên, hoặc bạn có thể trừ số nguyên từ
một con trỏ để lấy được con trỏ.

8, Chú ý khi tăng dần hoặc giảm dần con trỏ và gây ra lỗi "vượt quá giới
hạn" của mảng. Trình biên dịch sẽ không theo dõi việc liệu một con trỏ có
vẫn trỏ đến phần tử của mảng hay không.

SỬ DỤNG CON TRỎ TRONG BIỂU THỨC

1, Giá trị được tham chiếu bởi con trỏ có thể được sử dụng trong biểu thức
số học. Nếu một biến được định nghĩa có kiểu "pointer to integer" thì nó
được đánh giá sử dụng quy tắc số học số nguyên. Bạn hãy tham khảo ví
dụ sau nhé:
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Câu lệnh cuối sẽ tăng giá trị của biến number thêm 25. Toán tử * cho biết
bạn đang truy cập nội dung mà biến gọi pnumber đang trỏ đến.

2, Nếu con trỏ trỏ đến biến x thì có nghĩa là con trỏ đó được định nghĩa là
con trỏ với cùng kiểu dữ liệu là x. Sử dụng *pointer trong biểu thức giống
với việc sử dụng x trong cùng một biểu thức.

3, Như đã nói ở bài trước, một con trỏ được định nghĩa là "pointer to int" có
thể lưu trữ địa chỉ của bất kì biến kiểu int nào.

int value = 999;

pnumber = &value;

*pnumber += 25;

Với câu lệnh này thì sẽ tăng thêm 25 đơn vị cho biến value, và giá trị mới
của value bây giờ là 1024.

4, Con trỏ có thể chứa địa chỉ của bất kì biến nào có kiểu phù hợp. Bạn có
thể sử dụng biến con trỏ để thay đổi giá trị của nhiều biến khác nhau, miễn
là nó có kiểu tương thích với kiểu của con trỏ.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Sau đây là 1 đoạn mã lệnh khi thao tác với con trỏ:

ON TRỎ
NHẬN DỮ LIỆU INPUT
1, Khi gọi hàm scanf() để nhập giá trị, chúng ta sử dụng toán tử & để lấy
địa chỉ của biến. Trên biến lưu trữ giá trị đầu vào (đối số thứ 2).

Khi bạn đã có một con trỏ chứa địa chỉ, bạn có thể sử dụng tên con trỏ như
là đối số cho hàm scanf(). Bạn có thể tham khảo ví dụ sau đây:
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

KIỂM TRA NULL

1, Có một quy tắc mà bạn phải nhớ, đó là không tham chiếu ngược con trỏ
không được khởi tạo. Sau đây là 1 ví dụ không tốt về gán giá trị cho con
trỏ:

Dòng thứ hai có nghĩa là lưu trữ giá trị 5 ở nơi mà con trỏ trỏ đến, tuy
nhiên pt có giá trị ngẫu nhiên, không biết là 5 sẽ được đặt ở đâu. Nó có
thể đi đến một nơi không kiểm soát được, hoặc nó có thể ghi đè dữ liệu
hoặc mã lệnh, hoặc có thể làm chương trình bị phá hủy (crash).

2, Khi tạo một con trỏ chỉ cấp phát bộ nhớ để lưu trữ chính nó, không cấp
phát bộ nhớ để lưu trữ dữ liệu.

Trước khi bạn sử dụng con trỏ, nó cần được gán cho địa chỉ bộ nhớ mà đã
được cấp phát. Bạn phải gán địa chỉ của biến đang tồn tại cho con trỏ,
hoặc có thể sử dụng hàm malloc() để cấp phát bộ nhớ trước.

3, Chúng ta đã biết rằng, khi khai báo con trỏ mà không trỏ đến cái gì,
chúng ta nên khởi tạo nó là NULL.

int *pvalue = NULL;

NULL là biểu tượng đặc biệt trong C đại diện cho con trỏ tương đương với
0. Dưới đây cũng gán con trỏ với null sử dụng 0.

int *pvalue = 0;
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Vì NULL tương đương với 0, nếu bạn muốn kiểm tra xem giá trị pvalue có
NULL hay không, bạn có thể làm như thế này:

if (!pvalue) {…}

4, Bạn muốn kiểm tra NULL trước khi tham chiếu ngược con trỏ. Thường
được thực hiện khi con trỏ được truyền đến hàm.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Con trỏ & kiểu dữ liệu hằng

TỔNG QUAN

1. Trong bài học trước về con trỏ cơ bản, chúng ta đã biết con trỏ cũng là
một biến thông thường mà giá trị nó có thể chứa là địa chỉ của vùng nhớ
khác. Như vậy, từ khóa const cũng có thể được sử dụng cho con trỏ như
các biến có kiểu dữ liệu khác. Tuy nhiên, tùy vào vị trí đặt từ khóa const
khi khai báo con trỏ mà nó lại có những ý nghĩa khác nhau.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

2, Khi chúng ta sử dụng khai báo const cho một biến hoặc một mảng, nó
sẽ cho trình biên dịch biết rằng nội dung của biến/mảng sẽ không bị thay
đổi bởi chương trình.

Với con trỏ, chúng ta phải cân nhắc hai điểm khi sử dụng khai báo const.

⇒Liệu con trỏ có bị thay đổi hay không?

⇒Liệu giá trị mà con trỏ trỏ đến có bị thay đổi hay không?

3, Bạn có thể sử dụng từ khóa const khi bạn khai báo con trỏ để chỉ ra
rằng giá trị được trỏ đến không được phép thay đổi. Ví dụ như sau:

long value = 9999L;

const long *pvalue = &value; // khai báo 1 con trỏ cho 1 hằng số

Bạn đã khai báo giá trị được trỏ tới bởi pvalue là const. Trình biên dịch sẽ
kiểm tra bất kì câu lệnh nào cố gắng thay đổi giá trị được trỏ đến bởi
pvalue, và sẽ báo lỗi ở câu lệnh đó.

Câu lệnh sau sẽ dẫn đến có báo lỗi từ trình biên dịch:

*pvalue = 8888L; // lỗi bởi vì đã thay đổi con trỏ hằng số


CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

CON TRỎ HẰNG

int x = 10, y = 20;

const int *px = &x;

Khi ta khai báo 1 con trỏ có thêm từ khóa const phía trước như trên.
Ta hiểu rằng con trỏ px là 1 con trỏ hằng. Con trỏ hằng là con trỏ có
thể trỏ đến 1 vùng nhớ hằng. Đặc điểm của con trỏ này là nó là con trỏ
chỉ đọc (read-only), người dùng có thể thông qua nó đọc giá trị vùng
nhớ mà nó trỏ đến nhưng không thể thông qua nó ghi lại giá trị vào
vùng nhớ đó.

Do đó, ta có các lệnh như sau:

*px = 15; // ERROR do cố ghi lại giá trị cho vùng nhớ qua con trỏ hằng

px = &y; // OK

x = 15; // OK

Chúng ta sử dụng con trỏ hằng thường cho 2 mục đích.


● Trỏ đến 1 vùng nhớ hằng. Vùng nhớ mà ở 1 số IDE sẽ chặn việc
sử dụng con trỏ thông thường trỏ đến (ví dụ 1).
● Không muốn người dùng thay đổi giá trị vùng nhớ thông qua con
trỏ hằng khai báo (ví dụ 2).
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Ví dụ 1:
#include <stdio.h>

#include <stdlib.h>

const int x = 8; // x được lưu vào vùng nhớ hằng

int main()

const int *pt = &x; // khai báo con trỏ hằng để trỏ đến vùng nhớ hằng

printf("x = %d\n",*pt);

return 0;

Kết quả: x = 8

Ví dụ 2:
#include <stdio.h>

#include <stdlib.h>

int x = 8;

int main()

const int *pt = &x;

*pt = 1; // ERROR, không được ghi giá trị thông qua con trỏ hằng

printf("x = %d\n",x);

return 0;

}
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

HẰNG CON TRỎ

int x = 10, y = 20;

int* const px = &x;

Khi khai báo như trên là hiểu con trỏ px là 1 hằng con trỏ. Đặc điểm
của con trỏ này là nó chỉ có thể trỏ đến 1 địa chỉ duy nhất và sau đó
không thể thay đổi địa chỉ trỏ được nữa. Khác với con trỏ hằng thì
hằng con trỏ có thể đọc ghi giá trị vùng nhớ thông qua chính bản thân
con trỏ đó. Ta có các lệnh như sau:
*px = 15; // OK

px = &y; // ERROR vì cố tình chuyển đổi địa chỉ trỏ của con trỏ

KẾT HỢP CON TRỎ HẰNG VÀ HẰNG CON TRỎ

c.
int x = 10; y = 20;

const int* const px = &x;

Khi này, bạn không thể thay đổi vùng nhớ con trỏ px đang trỏ tới và cũng
không thể thay đổi giá trị vùng nhớ đó thông qua (*px).
*px = 15; // ERROR

px = &y; // ERROR
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Con trỏ kiểu void

TỔNG QUAN

1, Tên kiểu void có nghĩa là không có kiểu nào cả. Con trỏ có kiểu void* có
thể chứa địa chỉ của hạng mục dữ liệu của bất kì kiểu nào.

Con trỏ void* thường được sử dụng như là kiểu tham số hoặc kiểu giá trị
trả về với hàm xử lý dữ liệu theo cách không phụ thuộc vào kiểu.

2, Bất kì loại con trỏ nào cũng có thể được truyền dưới giá trị của kiểu
void*. Con trỏ void không biết kiểu của đối tượng mà nó trỏ đến, vì vậy, nó
không thể tham chiếu ngược một cách trực tiếp.

Để có thể tham chiếu ngược được thì con trỏ void phải được ép kiểu rõ
ràng sang kiểu con trỏ khác.

3, Địa chỉ của biến kiểu int có thể được lưu trữ trong biến con trỏ có kiểu
void*. Khi bạn muốn truy cập một giá trị số nguyên ở địa chỉ lưu trữ trong
con trỏ void*, trước tiên bạn sẽ phải ép kiểu con trỏ thành kiểu int*.

Bạn có thể tham khảo cách xử lý với con trỏ void qua đoạn mã lệnh sau
đây:
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Con trỏ và mảng

TỔNG QUAN

1, Một mảng (array) là một tập hợp các giá trị cùng kiểu mà bạn có thể
tham chiếu bằng cách sử dụng tên duy nhất. Một con trỏ là một biến có giá
trị địa chỉ bộ nhớ mà có thể tham chiếu đến biến hoặc hằng số có kiểu xác
định.

⇒Bạn có thể sử dụng con trỏ để giữ địa chỉ của nhiều biến khác nhau ở
nhiều thời điểm khác nhau (nhưng bắt buộc phải cùng kiểu).

2, Mảng và con trỏ có vẻ hơi khác nhau, nhưng chúng có liên quan chặt
chẽ và đôi khi có thể được dùng thay thế cho nhau. Một trong những ví dụ
phổ biến nhất của con trỏ trong C là sử dụng con trỏ trỏ đến mảng. Lí do
chính của việc sử dụng con trỏ trỏ đến mảng là vì sự tiện ích về mặt kí hiệu
và hiệu quả cho chương trình. Con trỏ trỏ đến mảng thường giúp mã lệnh
sử dụng ít bộ nhớ hơn và thực thi nhanh hơn.

MẢNG VÀ CON TRỎ

1, Nếu bạn có 1 mảng với 100 số nguyên.

int values[100];

Bạn có thể định nghĩa con trỏ gọi là valuesPtr, con trỏ này có thể sử dụng
để truy cập số nguyên được lưu trữ trong mảng này.

int *valuesPtr;
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

2, Khi bạn định nghĩa con trỏ được sử dụng để trỏ đến phần tử của mảng,
bạn không chỉ định con trỏ có kiểu là "pointer to array" mà bạn chỉ định con
trỏ trỏ đến kiểu của phần tử được lưu trữ trong mảng. Để gán valuesPtr trỏ
đến phần tử đầu tiên của mảng giá trị, bạn sẽ viết như sau:

valuePtr = values;

3, Toán tử địa chỉ không được sử dụng. Trình biên dịch C xử lý sự xuất
hiện của tên mảng values mà không có chỉ mục [] đi kèm như con trỏ trỏ
đến mảng. Việc chỉ định mảng values có tác dụng tạo ra con trỏ trỏ đến
phần tử đầu tiên của values[0].

4, Một cách tương đương để tạo ra con trỏ trỏ đến vị trí bắt đầu của giá trị
đó là áp dụng toán tử địa chỉ cho phần tử đầu tiên của mảng.

valuePtr = &values[0];

Sử dụng cái nào cũng được, tùy thuộc vào lựa chọn của người lập trình

TỔNG KẾT

Hai biểu thức ar[i] và *(ar+i) có ý nghĩa tương đương.

Cả hai đều được thực thi với ar là tên của 1 mảng và ar là biến con trỏ.

Sử dụng biểu thức như ar++ sẽ chỉ hoạt động nếu ar là biến con trỏ.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Các phép toán trên con trỏ

CON TRỎ SỐ HỌC

1, Như chúng ta đã biết, giá trị lưu trữ bên trong vùng nhớ của con trỏ là
địa chỉ, có kiểu dữ liệu là unsigned int (số nguyên không dấu). Do đó,
chúng ta có thể thực hiện các phép toán trên con trỏ. Nhưng kết quả của
các phép toán thực hiện trên con trỏ sẽ khác các phép toán số học thông
thường về giá trị và cả ý nghĩa.

2, Sức mạnh thật sự của việc sử dụng con trỏ đến mảng sẽ phát huy tác
dụng khi bạn muốn xử lý theo trình tự các phần tử trong mảng.

*valuesPtr ; // có thể được sử dụng để truy cập vào số nguyên đầu tiên
của mảng giá trị, đó là values[0] để tham chiếu values[3] qua biến
valuesPtr, bạn có thể thêm 3 vào valuesPtr và sau đó áp dụng toán tử điều
hướng:

*(valuesPtr + 3);

Biểu thức *(valuesPtr + i) có thể được sử dụng để truy cập vào giá trị nằm
trong values[i].

Ví dụ để gán values[10] là 27, bạn có thể làm như sau:

values[10] = 27;

hoặc sử dụng valuesPtr, bạn có thể:

*(valuesPtr + 10) = 27;


CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

3, Toán tử tăng dần ++ và giảm dần -- đặc biệt hữu ích khi xử lý với con
trỏ.

Sử dụng toán tử tăng dần với con trỏ có tác dụng tương tự như thêm một
vào con trỏ

Sử dụng toán tử giảm dần có tác dụng như trừ một toán tử từ con trỏ.

++valuesPtr; // gán con trỏ valuesPtr trỏ đến phần tử thứ 2 của mảng
values(là values[1])

--valuesPtr; // gán valuesPtr trỏ đến số nguyên trước đó trong mảng


values, giả sử rằng valuesPtr không trỏ đến vị trí đầu của mảng values.

4, Bạn có thể mở Code Blocks để chạy đoạn mã lệnh này để hiểu thêm về
các xử lý số học với con trỏ nhé:
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Để truyền một mảng cho một hàm, bạn chỉ cần chỉ định tên mảng.

Để tạo một con trỏ đến một mảng, bạn cũng chỉ cần chỉ định tên mảng.

Có nghĩa là trong lệnh gọi đến hàm arraySum(), những gì được truyền đến
hàm thực chất là con trỏ trỏ đến giá trị mảng.

Điều này giải thích cho việc tại sao bạn có thể thay đổi phần tử của mảng
từ trong hàm.

5, Bạn có thể thắc mắc tại sao tham số chính thức bên trong hàm lại không
được khai báo là con trỏ.

int arraySum(int *array, const int n)

⇒Câu lệnh trên hoàn toàn là hợp lệ. Con trỏ và mảng có quan hệ mật thiết
với nhau trong C. Đó là lí do tại sao bạn có thể khai báo mảng có kiểu
"array of ints" (mảng của các số nguyên) hoặc kiểu "pointer to int" (con trỏ
kiểu số nguyên) trong hàm arraySum.

6, Nếu bạn định sử dụng số chỉ mục để tham chiếu đến các phần tử của
mảng mà được truyền vào hàm, hãy khai báo tham số chính thức tương
ứng là một mảng. Điều này phản ánh chính xác hơn việc sử dụng mảng
của hàm.

Bạn hãy tham khảo ví dụ đoạn mã lệnh bên trên được viết dưới dạng sử
dụng con trỏ chứ không phải là mảng:
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

TỔNG KẾT

Nếu bạn khai báo

int urn[3];

int *ptr1, *ptr2;

Thì danh sách các xử lý hợp lệ và không hợp lệ được liệt kê ra như sau:

Các hàm xử lý mảng thực sự dùng con trỏ làm đối số. Bạn có sự lựa chọn
giữa kí hiệu mảng và kí hiệu con trỏ để viết hàm xử lý mảng. Sử dụng kí
hiệu mảng sẽ thấy rõ việc hàm đang xử lý với mảng. Kí hiệu mảng có giao
diện quen thuộc hơn với người lập trình hơn là FORTRAN, Pascal,
Modula-2, hoặc BASIC.

Các lập trình viên khác có thể quen với việc xử lý bằng con trỏ và có thể
thấy là sử dụng kí hiệu con trỏ sẽ tự nhiên hơn vì nó gần hơn với ngôn
ngữ máy, và với một số trình biên dịch, nó sẽ giúp cho mã lệnh thưc thi
hiệu quả hơn.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Truyền tham chiếu

TỔNG QUAN

Với bài học về hàm, chúng ta đã tìm hiểu về cách truyền giá trị cho hàm.
Khi truyền đối số cho hàm ở dạng giá trị, giá trị của đối số được sao chép
vào tham số của hàm. Và đối số sẽ không bị thay đổi sau lời gọi hàm.
Chúng ta còn có thêm một kiểu truyền dữ liệu vào cho hàm nữa, đó là
truyền địa chỉ vào hàm (Pass arguments by address). Do đó, kiểu tham số
của hàm có thể nhận giá trị là địa chỉ của biến.

TRUYỀN THAM TRỊ (TRUYỀN THAM SỐ CHO


HÀM LÀ 1 GIÁ TRỊ CỤ THỂ)

1, Có hai cách khác nhau để truyền dữ liệu vào hàm:

⇒Truyền tham trị

⇒Truyền tham chiếu

2, Truyền tham trị giá trị là khi hàm copy giá trị thực tế của một đối số nằm
trong tham số chính thức của hàm. Khi đó nếu có thay đổi với tham số
trong hàm sẽ không ảnh hưởng đến đối số.

Bạn hãy tham khảo mã lệnh về việc truyền tham trị của hàm hoán đổi 2 số
nguyên như sau:
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Khi đó nếu bạn gọi hàm swap() trên để hoán đổi 2 số nguyên như sau, thì
bạn sẽ thấy là 2 số nguyên không được hoán đổi gì cả. Bạn hãy mở Code
Blocks lên và gõ đoạn mã lệnh này vào để chạy thử nhé.

TRUYỀN DỮ LIỆU BẰNG CON TRỎ

1, Con trỏ và hàm kết hợp khá tốt với nhau. Bạn có thể truyền con trỏ là
đối số đến hàm và bạn cũng có thể có hàm trả về con trỏ.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

2, Truyền tham chiếu sẽ copy địa chỉ của đối số nằm trong tham số chính
thức Địa chỉ được sử dụng để truy cập đối số thực tế sử dụng để gọi hàm.
Có nghĩa là những thay đổi với tham số sẽ ảnh hưởng đến đối số truyền
vào.

3, Để truyền giá trị theo tham chiếu, con trỏ đối số được truyền đến hàm
cũng như là các giá trị khác. Bạn cần khai báo tham số của hàm theo kiểu
con trỏ.

Bây giờ bạn hãy xem xét hàm hoán đổi 2 số nguyên được viết dưới dạng
truyền tham chiếu như sau:

Lúc này ở hàm main() chúng ta sẽ phải gọi như thế này:
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Bạn hãy mở Code Blocks lên và gõ đoạn mã lệnh này vào để chạy thử, để
thấy rõ sự khác nhau với 2 của hai hàm hoán đổi số nguyên nhé.

TỔNG HỢP CÚ PHÁP

1, Bạn có thể truyền hai kiểu biến cho một hàm:

Cách 1: function1(x);

⇒ với cách này, bạn truyền giá trị của x và hàm phải được khai báo với
kiểu giống như x:

int function1(int num);

Cách 2: function2(&x);

⇒Với cách này, bạn truyền địa chỉ của x và yêu cầu khai báo hàm có con
trỏ trỏ đến đúng kiểu.

int function1(int* ptr);


CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

THAM SỐ CON TRỎ HẰNG (CONST POINTER)

Bạn có thể sử dụng từ khóa const khi truyền tham biến cho hàm. Lúc này
hàm sẽ coi đối số được truyền cho tham số là hằng số.

Bạn sử dụng từ khóa const cho tham số là con trỏ để chỉ định rằng một
hàm sẽ không thay đổi giá trị mà đối số con trỏ trỏ đến. Ví dụ như sau:

Kiểu của tham số, pmessasge, là con trỏ trỏ đến hằng chuỗi ký tự.

Bạn cũng có thể chỉ định chính con trỏ là const, nhưng điều này hơi nhạy
cảm vì địa chỉ được truyền theo cách là truyền giá trị.

Lúc này ở trong hàm bạn không thể thay đổi nội dung đang được trỏ bởi
pmesssage.

TRẢ VỀ CON TRỎ TỪ HÀM

1, Trả về con trỏ từ hàm là tính năng đặc biệt mạnh mẽ trong ngôn ngữ C.
Lúc đó bạn có thể không chỉ giá trị đơn lẻ, mà có thể là 1 tập các giá trị.

2, Bạn sẽ phải khai báo hàm trả về con trỏ:

int * myFunction () {

}
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

3.Tổng kết bài học


Trong bài học này, các bạn đã được tìm hiểu khái niệm con trỏ và một số
khái niệm có liên quan. Việc sử dụng con trỏ thường có một số hoạt động
chủ yếu:

○ Khai báo một con trỏ.

○ Gán địa chỉ cho con trỏ bằng toán tử &.

○ Truy cập giá trị tại một địa chỉ cụ thể bằng toán tử *.

Thử liên hệ một chút với cuộc sống thực tế, tưởng tượng rằng con đường
nhà bạn (street) là bộ nhớ ảo, trên con đường đó có rất nhiều ngôi nhà
(house), mỗi ngôi nhà đều được đánh số thứ tự gọi là địa chỉ nhà (house's
address). Chúng ta tạm hình dung số người ở trong mỗi ngôi nhà (content)
tương đương với nội dung của mỗi ô trên bộ nhớ ảo. Như vậy, address-of
operator (&house) sẽ trả về địa chỉ của ngôi nhà, dereference operator
(*&house) sẽ lấy ra số lượng người bên trong ngôi nhà có địa chỉ được xác
định. Để sử dụng con trỏ trỏ đến mỗi ngôi nhà, chúng ta phải sử dụng một
con trỏ kiểu House (giống với kiểu của từng ngôi nhà), giả sử con trỏ kiểu
House được khai báo là House *h_ptr; thì con trỏ h_ptr có thể trỏ đến bất
kỳ ngôi nhà nào trên con đường, và nó còn có thể thay đổi nội dung bên
trong từng ngôi nhà mà nó trỏ đến.
CÔNG TY CỔ PHẦN LUMI VIỆT NAM
Số 38 Đỗ Đức Dục, Nam Từ Liêm, Hà Nội
0904 665 965
www.lumi.vn

Con trỏ (Pointer) là một công cụ mạnh mẽ đặc trưng của ngôn ngữ C.
Con trỏ cho phép chúng ta trực tiếp quản lý dung lượng của chương trình
trên bộ nhớ ảo. Nhưng bên cạnh đó, việc sử dụng con trỏ không hợp lý có
thể gây lãng phí tài nguyên của hệ thống máy tính. Chúng ta sẽ cùng tìm
hiểu các kỹ thuật quản lý bộ nhớ ảo của chương trình trong các bài học
tiếp theo.

Lưu ý: Việc khai báo biến con trỏ có thể không cần khởi tạo giá trị ngay khi
khai báo. Nhưng thực hiện truy xuất giá trị của con trỏ bằng (*) dereference
operator khi chưa gán địa chỉ cụ thể cho con trỏ, chương trình có thể bị
đóng bởi hệ điều hành. Nguyên nhân là do con trỏ đang nắm giữ một giá trị
rác, giá trị rác đó có thể là địa chỉ thuộc một vùng nhớ đang được ứng
dụng khác sử dụng, hoặc giá trị vượt quá giới hạn của bộ nhớ.

You might also like