Tải bản đầy đủ (.pdf) (21 trang)

Win32 Programming Tutorial  Tham khảo toàn diện về Con trỏ trong C/C++ ppt

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (584.11 KB, 21 trang )






Trang 1
Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net














Tham khảo toàn diện về
“Con trỏ” trong C/C++
















Reverse Engineering Association
Win32ProgrammingTutorial

For more updated info, please check





Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net

MỤC LỤC

I. Con trỏ 3
1.
Một số khái niệm 3
2. Biến 3
a) Biến 3
b) Ví dụ 4

3. Con trỏ 5
a)
Khái niệm 6
b) Tại sao phải dùng con trỏ 7
c) Một số thao tác cơ bản với con trỏ 9
II. Con trỏ hàm 11
1. Một số khái niệm 11
2. Con trỏ hàm 11
a)
Khái niệm 11
b) Cú pháp 12
c) Định nghĩa một con trỏ hàm 12
3. Kiểu quy ước gọi của hàm 13
4. Gán một hàm (địa chỉ hàm) vào/cho con trỏ hàm 13
5. Gọi hàm sử dụng con trỏ hàm 15
6. Tại sao sử dụng con trỏ hàm 16
7. Định nghĩa con trỏ hàm bằng từ khóa typedef 20
III. Tài liệu tham khảo 21




















Trang 2

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
I. Con trỏ
1. Một số khái niệm
Con trỏ (pointer) đơn giản là địa chỉ của một đối tượng trong bộ nhớ. Thông
thường, các đối tượng có thể được truy xuất bằng một trong 2 cách: trực tiếp bằng đại
diện hoặc gián tiếp bằng con trỏ. Các biến con trỏ được định nghĩa trỏ tới các đối tượng
có một kiểu dữ liệu cụ thể sao cho khi con trỏ bị hủy đi thì vùng nhớ mà đối tượng đuợc
cấp phát sẽ đư
ợc giải phóng hoặc thu hồi.
Các con trỏ thường được dùng để tạo ra các đối tượng động trong lúc thực thi
chương trình. Không giống như các đối tượng bình thường (biến cục bộ và toàn cục)
được cấp phát và lưu trữ trong ngăn xếp (stack), một đối tượng động được cấp phát vùng
nhớ từ vùng lưu trữ khác được gọi là heap. Các đối tượng không t
uân theo quy luật thông

thường. Phạm vi của chúng được điều khiển bởi các lập trình viên.
Tham chiếu (reference) cung cấp một tên tượng trưng cho đối tượng, gọi là alias
(tên đại diện). Truy xuất một đối tượng thông qua một tham chiếu giống như truy xuất
thông qua tên của đối tượng. Tham chiếu nâng cao tính hữu dụng của con trỏ và sự tiện
lợi của việc truy xuất trực tiếp các đối tượng. Chúng được sử dụng để hổ trợ gọi thông
qua tham chiếu của các tham số hàm
đặc biệt khi các đối tượng lớn được truyền tới hàm.
2. Biến
Khái niệm con trỏ trong ngôn ngữ C/C++ là một phần quan trọng và khá phức tạp,
mang lại rất nhiều tiện lợi cũng như rắc rối. Nếu như muốn giải thích cặn kẽ về vai trò,
chức năng, hoạt động cũng như những khái niệm khác liên quan tới con trỏ thì tôi e rằng
cần khoảng vài chương sách mới đủ. Tôi tạm mượn câu nói “Ngôn ngữ C/C++ không
phải chỉ có con trỏ, nhưng nếu chưa hiểu về con trỏ thì kể như chưa học C/C++” để nói
lên vài trò của con trỏ. Bởi vì nhiều khái niệm khác trong C liên quan mật thiết tới con trỏ
như: chuỗi
ký tự, mảng, hàm CallBack, các kiểu dữ liệu trừu tượng như danh sách liên
kết, cấu trúc cây, và mở rộng ra trong C++ như lớp, các hàm thành viên, V-Table,
a) Biến
Để hiểu được con trỏ thì trước tiên bạn phải hiểu về biến. Tr
ong C/C++, biến là một
vùng trong bộ nhớ, do đó mỗi biến đều có 1 địa chỉ. Khi bạn khai báo một biến, bạn phải
cung cấp tên biến, kiểu dữ liệu của biến đó. Khi bạn muốn thao tác với một biến thì bạn
sử dụng tên gọi của nó, còn kiểu dữ liệu giúp cho trình biên dịch
biết là biến đó cần bao
nhiêu byte trong bộ nhớ và nó sẽ cấp phát vùng nhớ tương ứng cho biến đó. Ví dụ, bạn
khai báo 1 biến kiểu integer:
int var;

Trang 3


Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
Khi biên dịch tới đây, trình biên dịch sẽ cấp phát bộ nhớ cho biến var này là 2 byte
hoặc 4 bytes (tùy thuộc vào kiến trúc máy tính). Giả sử kiểu int cần 2 bytes, và trình biên
dịch sẽ gán cho biến var hai byte trong bộ nhớ bắt đầu tại địa chỉ xxx. Sau đó, trình biên
dịch sẽ điền tên và địa chỉ của biến var này vào trong một bảng (chỉ sử dụng khi biên
dịch). Bạn có thể hiểu nôm na bảng này gồm 2 cột: một cột là tên biến và một cột là địa
chỉ của biến. Mỗi biến trong chuơng trì
nh sẽ chiếm một dòng trong bảng đó, khi biên dịch
gặp dòng lệnh gán:
var = 25;
Đầu tiên, nó sẽ tìm xem trong bảng có biến nào tên là var hay không, nếu nó tìm
không thấy thì nó sẽ khai báo lỗi là bạn dùng một biến chưa khai báo:
error Cxxx: 'i' : undeclared identifier
Nếu tìm thấy biến này thì nó sẽ điền giá trị là 25 vào 2 byte tại địa chỉ xxx.
b) Ví dụ
Về cơ bản bộ nhớ máy tính là một dã
y các bytes. Mỗi byte được đánh địa chỉ cụ
thể. Hình dưới đây tượng trưng một dãy bytes, từ địa chỉ 924 tới địa chỉ 940, trong bộ
nhớ máy tính:

Bạn có chương trình như sau:
Mã lệnh C Mã lệnh C++
1: #include <stdio.h> 1: #include <iostream>

2: int main () 2: int main ()
3: { 3: {
4: float fl=3.14; 4: float fl=3.14;
5: printf (“%.2f\n”,fl) 5: std::count << fl << std::endl
6: return 0; 6: return 0;
7: } 7: }
Khi biên dịch tới dòng (4) của chương trình, thì trình biên dịch sẽ cấp vùng nhớ
cho biến fl. Trong ví dụ này, giả sử rằng biến float cần 4 bytes:


Trang 4

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
Khi biến fl được sử dụng ở dòng (5), trình biên dịch sẽ làm 2 bước như sau:
+ Chương trình sẽ tìm và lấy địa chỉ dành cho biến fl (trong ví dụ này là 924)
+ Lấy nội dung lưu tại địa chỉ 924
 Tổng quát lên, khi bất kỳ một biến nào đó được sử dụng, thì trình biên dịch
cũng làm 2 bước trên để lấy nội dung của biến.
Minh họa giá trị của biến được khởi tạo lưu trữ trong bộ nhớ máy tính có thể bị hiểu
nhầm. Hãy nhìn hình, giá trị 3 đựơc lưu tại địa chỉ 924, dấm chấm được lưu tại địa chỉ 925,
giá trị 1 được lưu tại đại chỉ 926, và cuối cùng giá trị 4 đựơc lưu tại địa chỉ 927. Hãy luôn
nhớ rằng, trong thực tế máy tính sẽ sử dụng một thuật toán để chuyển đổi số kiểu chấm
động (float point number) 3.14 thành dãy các bit 0 và 1. Mỗi byte có 8 bits 0 và 1, vì thế 4

bytes float sẽ lưu trữ 32 bits 0 và 1. Bất kể con số này là 3.14 hay -273.15, thì nó luôn luôn
được lưu trữ trong 4 bytes bộ nhớ.
3. Con trỏ
Ví dụ trên minh họa cho chúng ta cách khai báo, sử dụng cũng như biểu diễn
chúng trên bộ nhớ như thế nào. Đây là các biến có kích thước và kiểu dữ liệu xác
định. Người ta gọi các biến kiểu này là biến tĩnh. Khi khai báo biến tĩnh, việc cấp
phát vùng nhớ cho các biến này luôn luôn được thực hiện mà không cần biết trong
quá trình thực thi chương trình có sử dụng hết vùng nhớ đã được cấp hay không?
Mặt khác, các biến tĩnh dạng này sẽ tồn tại t
rong suốt thời gian thực thi chương trình
mặc dù có những biến chỉ được sử dụng trong suốt chương trình.
Một số hạn chế có thể gặp phải khi sử dụng các biến tĩnh:
 Cấp phát vùng nhớ thừa  lãng phí vùng nhớ
 Cấp phát vùng nhớ thiếu, chương trình thực thi bị lỗi.
Để tránh những hạn chế trên, ngôn ngữ C cung cấp c
ho ta một loại biến đặc biệt
gọi là biến động với các đặc điểm sau:
 Chỉ phát sinh trong quá trình thực hiện chương trình chứ không phát
sinh lúc bắt đầu chương trình.
 Khi chạy chương trình, kích thước của biến, vùng nhớ và địa chỉ vùng
nhớ được cấp phát cho biến có thể thay đổi.
 Sau khi sử dụng xong có thể giải phóng để tiết kiệm chỗ trong bộ nhớ.
Tuy nhiên các biến động không có địa chỉ nhất định nên ta không thể truy cập đến
chúng được. Vì thế, ngôn ngữ C lại cung cấp cho ta một loại biến đặc biệt nữa để khắc
phục tình trạng này, đó là biến con trỏ (pointer).



Trang 5


Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
a) Khái niệm
Con trỏ là một biến lưu trữ địa chỉ của một biến khác có kiểu dữ liệu cụ thể.
Một con trỏ có tên gọi giống như bất kỳ biến khác và cũng có một kiểu dữ liệu
quy định loại biến nào mà nội dung của nó tham chiếu đến.
Con trỏ là một biến đặc biệt trong C, giá trị mà nó lưu trữ luôn được hiểu là
địa chỉ trong bộ nhớ. Giả sử bạn có 2 biến, một biến kiểu nguyên (
integer), một
biến là kiểu con trỏ và giả sử cả 2 biến đều có giá trị là 1000. Khi đó, giá trị mà
biến kiểu int lưu trữ đụơc hiểu một con số nguyên có giá trị là 1000, còn giá trị
mà con trỏ lưu trữ được coi là địa chỉ thứ 1000 trong bộ nhớ.
Con trỏ được dùng để lưu trữ địa chỉ của những biến khác trong chương
trình, vì t
hế khi khai báo ngoài dấu *, bạn cần phải khai báo rõ thêm là con trỏ sẽ
“trỏ” đến một biến kiểu gì (kiểu int, long, hay kiểu char, ). Ví dụ, bạn muốn lưu
trữ địa chỉ của biến kiểu int, thì bạn khai báo như sau:
int *pInt;
Ở đây, pInt là tên con trỏ, dấu * cho trình biên dịch biết đó là con trỏ - một biến
đặc biệt chứ không phải những biến bình thường khác và từ khóa int đứng đầu
báo cho trình biên dịch biết là con trỏ pInt dùng để lưu trữ địa chỉ của những biến
kiểu int. Bây giờ, chúng ta muốn gán địa chỉ của biến integer đã đựơc khai báo
từ trước cho con trỏ pInt thì làm thế nào?
 Để gán địa chỉ của một biến cho c

on trỏ  dùng toán tử lấy địa chỉ của biến
(&). Ví dụ:
pInt = & var;
Giả sử biến var đang được lưu trữ tại địa chỉ 1776 và nếu bạn viết như sau:
var = 25;
int temp = var;
pInt = & var;
Kết quả sẽ giống như sơ đồ dưới đây:


Trang 6

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
Chúng ta đã gán cho biến temp nội dung của biến var, nhưng với biến pInt
chúng ta sẽ gán địa chỉ mà hệ điều hành lưu giá trị của biến var, chúng ta đã giả
sử địa chỉ đó là 1776.
Khi gặp dòng lệnh
pInt = & var thì trình dịch cũng thực hiện bình
thường như những biến khác: đầu tiên nó tìm trong bảng xem có biến nào tên là
pInt hay không, sau đó nó dịch đến dấu = và nó hiểu là phải gán một giá trị cho
biến pInt. Tiếp đến là toán tử lấy giá trị (&), khi gặp toán tử đó, trình dịch sẽ hiểu
là cần phải gán địa chỉ của biến var cho pInt (tức là 1776), chứ không phải là
giá trị của bản thân biến var (tức là 25).

Vậy là đến đây chúng ta có như sau: giá trị của var là 25, giá trị của pInt là

1776 (địa chỉ của biến var). Vì con trỏ cũng là biến cho nên nó cũng phải có địa
chỉ trong bộ nhớ, còn số byte cần thiết cho con trỏ có thể là 2 hoặc 4 byte phụ
thuộc hệ điều hành (DOS hay Win32). Giả sử địa chỉ của pInt là 2000, khi đó
chúng ta có sơ đồ sau:

Bên trên là tên biến, những số bên trên (1776 và 2000) là địa chỉ của 2 biến,
còn những số bên trong (25 và 1776) là giá trị của 2 biến đó.
b) Tại sao phải dùng con trỏ
Nếu chỉ dùng c
on trỏ để lưu giá trị là những địa chỉ trong bộ nhớ, thì chẳng
việc gì người ta lại phải nghĩ ra kiểu con trỏ làm gì cho nó phức tạp, bởi vì thực
ra địa chỉ trong bộ nhớ cũng là một số nguyên, và tất nhiên là dùng một biến kiểu
số nguyên là đủ. Sở dĩ người ta phải nghĩ ra kiểu con trỏ là bởi vì có những lúc
bạn không thể thao tác trực tiếp với các biến thông qua tên gọi, mà bạn phải thao
tác với chúng một cách gián tiếp,
thông qua địa chỉ của nó, tức là dùng con trỏ.
Ví dụ, để thay đổi giá trị của biến var từ 25 thành 30, thông thường bạn viết là:
var = 30;



Trang 7

Reverse Engineering Association





Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
Nhưng nếu bạn muốn gán giá trị 30 cho biến var tại một điểm nào đó trong
chương trình, mà tại điểm đó biến var bị "che" mất (tức là tại điểm đó không
"tồn tại" biến nào tên là var), bạn chỉ biết địa chỉ của biến đó thôi, thì bạn làm
thế nào?  Câu trả lời là dùng con trỏ. Ví dụ:
*pInt = 30;
Ở đây, dấu * có ý nghĩa khác với khi khai báo con trỏ pInt. Nó báo cho
trình dịch biết là bạn muốn gán giá trị 30 cho biến mà địa chỉ của nó đang do
pInt lưu trữ. Vì trình dịch biết giá trị của pInt (hiện là 1776) nên kết quả là số
30 sẽ được viết vào 2 byte (do pInt trỏ tới kiểu int có 2 byte) bắt đầu tại địa chỉ
1776. Đến đây thì chúng ta có sơ đồ sau:

Như bạn thấy, mặc dù bạn không gán số 30 cho biến var trực tiếp thông qua
tên gọi, mà bạn chỉ dùng con trỏ, nhưng kết quả là bây giờ biến var sẽ có giá trị
mới là 30, chứ không phải là 25 như trước nữa.

Kết luận: chúng ta có thể thao tác với 1 biến bằng 2 cách: dùng tên biến hoặc
dùng con trỏ, trỏ đến biến đó. Tức là:
var = 30
hoặc
*pInt = 30;
là tương đương nhau. Tương tư, nếu như integer là một biến kiểu int thì:


var = integer ;


*pInt = integer ;


cũng tương đương nhau.


Trang 8

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
c) Một số thao tác cơ bản với con trỏ
i. Toán tử lấy địa chỉ (&)
Vào
thời điểm mà chúng ta khai báo một biến thì nó phải được lưu trữ trong
một địa chỉ cụ thể trong bộ nhớ. Và chúng ta không biết địa chỉ của biến đó - thật
may mắn rằng điều đó đã được làm tự động bởi trình biên dịch và hệ điều hành,
nhưng một khi hệ điều hành đã gán một địa chỉ cho biến thì chúng ta có thể biết biến
đó đư
ợc lưu trữ ở đâu.
Điều này có thể được thực hiện bằng cách đặt trước tên biến một dấu và (&), có
nghĩa là "địa chỉ của". Ví dụ:
int *pInt;
int var;
 pInt = &var;
pInt = &var sẽ gán cho biến pInt địa chỉ của biến var, vì khi đặt trước tên
biến var dấu và (&) chúng ta không còn nói đến nội dung của biến đó mà chỉ nói đến

địa chỉ của nó trong bộ nhớ.
Giả sử rằng biến var được đặt ở ô nhớ có địa chỉ 1776 và chúng ta viết như sau:
var = 25;
int temp = var;
pInt = &var;
Kết quả sẽ giống như trong sơ đồ dưới đây:

Chúng ta đã gán cho temp nội dung của biến andy như chúng ta đã làm rất lần nhiều
khác trong những phần trước nhưng với biến pInt chúng ta đã gán địa chỉ mà hệ điều
hành lưu giá trị của biến var, chúng ta vừa giả sử nó là 1776.



Trang 9

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
ii. Toán tử lấy tham chiếu (*)
Bằng cách sử dụng con trỏ chúng ta có thể truy xuất trực tiếp đến giá trị được
lưu trữ trong biến được trỏ bởi nó bằng cách đặt trước tên biến con trỏ một dấu sao
(*) - ở đây có thể được dịch là "giá trị được trỏ bởi". Vì vậy, nếu chúng ta viết:
int temp = *pInt;
(chúng ta có thể đọc nó là: "temp bằng giá trị được trỏ bởi pInt" temp sẽ mang giá trị
25, vì pInt bằng 1776 và giá trị trỏ bởi 1776 là 25.


Bạn phải phân biệt được rằng pInt có giá trị 1776, nhưng *pInt (với một dấu
sao đằng trước) trỏ tới giá trị được lưu trữ trong địa chỉ 1776, đó là 25. Hãy chú ý sự
khác biệt giữa việc có hay không có dấu sao tham chiếu.
temp = pInt; // temp bằng pInt( 1776 )
temp = *pInt; // temp bằng giá trị được trỏ bởi(25)
Toán tử lấy địa chỉ (&)
Nó được dùng như là một tiền tố của biến và có thể được dịch là "địa chỉ của", vì vậy
&variable có thể được đọc là "địa chỉ của variable".
Toán tử tham chiếu (*)
Nó chỉ ra rằng cái cần được tính toán là nội dung được trỏ bởi biểu thức được coi như là một
địa chỉ. Nó có thể được dịch là "giá trị được trỏ bởi"
*mypointer được đọc là "giá trị được trỏ bởi mypointer".
Vào lúc này, với những ví dụ đã viết ở trên
var = 25;
pInt = &var;
bạn có thể dễ dàng nhận ra tất cả các biểu thức sau là đúng:
var = 25
&var = 1776
pInt = 1776
*pInt = 25

Trang 10

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++

NhatPhuongLe
www.reaonline.net
II. Con trỏ hàm
1. Một số khái niệm
Pointer/Pointee: một con trỏ “pointer” sẽ lưu một reference đến một biến khác
được biết như là pointee của nó. Con trỏ có thể được thiết lập giá trị NULL có nghĩa
là nó refer đến một pointee nào. (Trong C và C++, giá trị NULL có thể được sử dụng
như là giá trị boolean false).
Dereference: toán tử dereference trên con trỏ cho phép truy nhập vào pointee của
nó. Một pointer chỉ có thể bị dereference sau khi nó được thiết lập trỏ đến một pointee
cụ thể. Một pointer mà không có pointee thì là bad pointer và không thể bị
dereference.
Bad Point
er: một pointer mà không được trỏ vào một pointee thì là “bad” và
không thể dereference. Trong C và C++, việc dereference một bad pointer đôi khi gây
xung đột ngay lập tức và làm hỏng bộ nhớ của chương trình đang chạy, gây nên
“không biết đường nào mà lần”. Kiểu lỗi này rất khó để theo dõi. Trong C và C++, tất
cả các pointer bắt đầu bằng các giá trị ngẫu nhiên (bad values), do đó rất dễ tình cờ sử
dụng bad pointer. Những đoạn mã đúng sẽ thiết lập mỗi poi
nter có một good value
trước khi sử dụng chúng. Chính vì vậy sử dụng bad pointer là một lỗi rất phổ biến
trong C/C++. Với Java và các ngôn ngữ khác, các pointers được tự động bắt đầu với
giá trị NULL, do đó quá trình dereference sẽ được dễ dàng detect nên các chương
trình Java dễ gỡ lỗi này hơn nhiều.
Pointer assignment: một phép gán giữa hai con trỏ như p = q; sẽ làm cho hai
pointer trỏ vào cùng một pointee. Nó sẽ không copy vùng nhớ của pointee. Sau phép
gán thì cả hai pointer sẽ chỉ vào c
ùng một vùng nhớ của pointee.
2. Con trỏ hàm
a) Khái niệm

Trong phần trên, bạn biết được rằng con trỏ là một biến lưu giữ địa chỉ của một
biến khác. Con trỏ hàm (Function Pointers hay Functors) cũng tương tự, nhưng
thay vì nó trỏ tới địa chỉ của biến thì bây giờ nó trỏ tới địa chỉ của hàm.
Một mảng nArray có 10 phần tử được khai báo như sau:
int nArray[10];
Như bạn đã tìm hiểu, thật ra một mảng là một con trỏ hằng. Cũng như với khai
báo trên nó định nghĩa một biến nArray hay một con trỏ hằng trỏ tới mảng 10 phần tử.
Khi chúng ta truy xuất con trỏ (mảng) bằng cách *nArray hoặc nArray[nIndex] chỉ có
phần tử có nIndex thích hợp mới được truy xuất.

Trang 11

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
Bây giờ, bạn có một câu lệnh khai báo một hàm không có tham số đầu vào và
kiểu trả của hàm là kiểu số nguyên:
int foo();
Nếu bạn đoán rằng foo là một con trỏ hằng trỏ tới một hàm, thì bạn đã chính xác.
Khi một hàm được gọi thông qua toán tử (), thì con trỏ hàm được truy xuất, và thực thi
hàm.
b) Cú pháp
Dựa và
o cú pháp thì có hai loại function pointer khác nhau:
 Function pointer trỏ đến C function hoặc static C++ member function

 Function pointer tới non-static C++ member function.
Sự khác biệt cơ bản là tất cả pointer đến non-static member function cần một
tham số ẩn: con trỏ this tới instance của class. Vậy chỉ cần nhớ rằng có hai loại
function pointer không tương thích với nhau.
c) Định nghĩa một con trỏ hàm
Vì functi
on pointer không khác gì hơn một biến nên nó phải được define giống
như thông thương. Ví dụ dưới đây chúng ta khai báo các function pointer tên là
pt2Function, pt2Member và pt2ConstMember. Chúng trỏ đến function và lấy một
biến float và hai biến char và trả về một số int. .
int (*
pFoo
) () = NULL; //C
int (*
pt2Function
)(float, char, char) = NULL; //C
Ở ví dụ C++ chúng ta giả sử rằng function mà function pointer trỏ đến là non-
static member function của TMyClass
int (TMyClass::*
pt2Member
)(float, char, char) = NULL; //C++
int (TMyClass::*
pt2ConstMember
)(float, char, char) const = NULL; //C++

Trang 12

Reverse Engineering Association





Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
3. Kiểu quy ước gọi của hàm
Thông thường chúng ta không phải quan tâm về kiểu quy ước gọi (calling
convention) của một hàm. Trình biên dịch giả định rằng cdecl là kiểu quy ước mặc
định nếu chúng ta không sử dụng một kiểu quy ước khác. Kiểu quy ước gọi hàm nói
cho trình biên dịch biết cách truyền tham số và cách tạo ra một hàm. Ví dụ về những
kiểu quy ước gọi hàm khác là stdcall, pascal, fastcall. Nếu hàm và function con trỏ
hàm khác nhau về kiểu quy ước gọi thì chúng cũng không tương thích với nhau và
không thể thực hiện phép gán con trỏ hàm vào địa chỉ của hàm
kia. Đối với trình biên
dịch của Borland và Microsoft thì cần khai báo kiểu quy ước gọi hàm ở giữa kiểu trả
về và tên hàm hay tên con trỏ hàm. Đối với GNU GCC thì sử dụng từ khóa
__attribute__: viết khai báo hàm theo sau bởi từ khóa __attribute__ và sau đó là trạng
thái của kiểu quy ước gọi:
void __cdecl DoIt(float a, char b, char c); //Borland and Microsoft
void DoIt(float a, char b, char c) __attribute__((cdecl)); //GNU GCC
4. Gán một hàm (địa chỉ hàm) vào/cho con trỏ hàm
Rất dễ dàng để gán một địa chỉ của một hàm vào con trỏ hàm. Đơn giản chỉ cần
lấy tên của hàm hoặc hàm thành phần thích hợp. Mặc dù hầu hết các trình biên dịch
đều hỗ trợ việc đó nhưng tốt hơn hết là chúng ta sử dụng toán tử địa chỉ & và đặt trước
các hàm để viết những đoạn mã portable. Chúng ta cũng phải sử dụng tên đầy đủ của
hàm
thành viên bao gồm tên lớp và toán tử phạm vi (::). Chúng ta cũng phải đảm bảo
rằng chúng ta được quyền truy nhập vào hàm ở bên trong toán tử phạm vi đó.
// C
int DoIt (float a, char b, char c) {

printf("DoIt
\n
");
return a+b+c;
}
int DoMore(float a char b, char c)const { ,
printf("DoMore
\n
");
return a-b+c;
}

// Phép gán địa chỉ (ngắn gọn) một hàm cho con trỏ hàm
pt2Function = DoIt;

// Phép gán (đầy đủ) bằng cách sử dụng toán tử địa chỉ &
pt2Function = &DoMore;




Trang 13

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe

www.reaonline.net
// C++

class TMyClass {
public:
int DoIt(float a, char b, char c) {
cout << "TMyClass::DoIt"<< endl;
return a+b+c;
};
int DoMore(float a, char b, char c) const {
cout << "TMyClass::DoMore" << endl;
return a-b+c;
};

/* more of TMyClass */

};
// Phép gán địa chỉ (ngắn gọn) một hàm cho con trỏ hàm
pt2ConstMember = &TMyClass::DoMore;

// Chú ý:
pt2Member
cũng là một con trỏ hàm hợp lệ tới hàm &DoIt
pt2Member = &TMyClass::DoIt;
Có thể ví dụ trên làm bạn không hiểu lắm, sau đây ta xét một ví dụ khác đơn giản hơn.
Trong ví dụ sau, ta thực hiện phép gán địa chỉ của một hàm (foo) cho con trỏ hàm
(*pFoo)

Một lỗi thường gặp nhất là sai phép gán:
pFoo = goo();

Với câu lệnh này, nó sẽ thực hiện lệnh gán giá trị trả của hàm goo() cho con trỏ pFoo,
đây không phải là điều bạn muốn. Bạn muốn gán hàm (hay địa chỉ hàm) goo() cho con
trỏ pFoo, không phải là giá trị trả về từ hàm goo(). Do đó, bạn không cần dấu ngoặc
đơn.
pFoo = goo;

Trang 14

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
Chú ý: Một con trỏ hàm luôn trỏ đến một hàm đặc biệt nên tất cả những hàm mà
chúng ta muốn sử dụng với cùng một con trỏ hàm thì phải có cùng tham số và giá trị
trả về. Nói một cách khác là cùng prototype (khai báo hàm).

5. Gọi hàm sử dụng con trỏ hàm
Ngoài việc gán hàm (hay địa chỉ của hàm) cho con trỏ hàm, thì việc thứ 2 mà bạn
thường phải làm là gọi hàm sử dụng con trỏ hàm. Có 2 cách để thực hiện:
 Sử dụng toán tử tham chiếu (*), cách này gọi hàm thông qua con trỏ hàm
(cách tường minh)

 Thay vì sử dụng con trỏ hàm, thì ta có thể gọi hàm thông qua tên hàm (cách
không tường minh).

Như bạn thấy, cách gọi hàm thông qua tên hàm thì trông giống như một lời gọi

hàm thông thường. Hãy nhớ rằng, đối với một hàm bình thường thì nó là một con
trỏ luôn trỏ tới tên của chính nó.




Trang 15

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
6. Tại sao sử dụng con trỏ hàm
Trong nhiều trường hợp con trỏ hàm có thể hữu dụng, ví dụ như bạn cần viết
một hàm sắp xếp mảng một chiều, nhưng đồng thời bạn cũng muốn người dùng có thể
chỉ định sắp xếp theo thứ tự nào (tăng dần hay giảm dần). Chúng ta hãy quan sát vấn
đề này để áp dụng chính xác cho việc sắp xếp mảng, cũng như từ ví dụ này bạn có thể
tổng quát hóa vấn đề lên
cũng như xử lý các vấn đề khác tương tự.
Nguyên tắt của tất cả các thuật toán sử dụng: số phần tử cần được sắp xếp,
so sánh các số với nhau theo từng đôi một và sắp xếp lại thứ tự của chúng dựa vào kết
quả trả về của việc so sánh. Do đó, bằng cách thay đổi thuật toán so sánh (mỗi thuật
toán so sánh có thể viết riêng thà
nh một hàm để thực hiện chức năng so sánh), chúng
ta có thể thay đổi cách thức mà hàm đó sắp xếp mà không ảnh hưởng tới phần mã lệnh
còn lại của mã nguồn.

Đây là thuật toán toán sắp xếp Selection Sort (sắp xếp theo phương pháp chọn):

Bây giờ, hãy thay thế dòng lệnh thực hiện việc so sánh 2 phần tử của mảng bằng
một hàm khác cũng thực hiện cùng một chức năng. Hàm so sánh của chúng ta sẽ so
sánh 2 số nguyên và trả về một giá trị luận lý (kiểu Boolean), nó sẽ có dạng như sau:



Trang 16

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
Và đây là thuật toán sắp xếp Selection Sort sử dụng hàm Ascending để thực hiện
việc so sánh:

Để cho thủ tục Selection Sort (hàm gọi - caller) quyết định việc sắp xếp sẽ được
thực hiện như thế nào, thay vì viết hàm Ascending và sử dụng nó trong mã lệnh của
thuật toán sắp xếp, thì bạn hãy để cho caller cung cấp cách hàm sắp xếp của riêng nó.
Bằng cách làm này, bạn tránh được hard-coded, tức là tính tái sử dụng không cao,
hàm Ascending chỉ dùng được trong thuật toán này mà thôi, qua thuật toán khác
không thể dùng lại được.
Bởi vì hàm so sánh của caller sẽ so sánh 2 số integer và trả về giá trị kiểu
Boolean, ta sử dụng một con trỏ hàm có dạng như sau:
bool (*pComparison)(int, int);



Trang 17

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
Như vậy, chúng ta đã để cho caller truyền vào một con trỏ hàm để thực hiện việc
so sánh như một tham số đầu vào của hàm, và sau đó chúng ta sẽ sử dụng nó để so
sánh.
Đây là mã lệnh đầy đủ của thuật toán Selection Sort, sử dụng con trỏ hàm như là
một tham số cho phép người dùng chỉ định việc sắp xếp theo thứ tự nào:


Trang 18

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net


Chương trình cho ra kết quả như sau:

Tới đây, bạn có thể “chế” ra những hàm so sánh khác để phục vụ cho thuật toán
sắp xếp. Ví dụ như là sắp xếp mảng sao cho các phần tử chẳn ở đầu mảng, phần tử lẻ
ở cuối mảng:

Chương trình cho ra kết quả như sau:
2 4 6 8 1 3 5 7 9

Trang 19

Reverse Engineering Association




Tham khảo toàn diện về Con trỏ trong C/C++
NhatPhuongLe
www.reaonline.net
Như bạn thấy, việc sử dụng con trỏ hàm trong ngữ cảnh này là một cách khá hay
để cho caller có thể “hook” một số hàm mà bạn đã viết trước đây, giúp bạn tái sử dụng
mã lệnh, tránh hard-coded. Trước đây, nếu bạn muốn sắp xếp 1 mảng theo thứ tự giảm
dần và tăng dần, thì bạn phải viết 2 hàm sắp xếp, một theo thứ tự tăng dần, 1 cho thứ
tự giảm dần. Bâ
y giờ bạn chỉ cần viết 1 hàm sắp xếp, mà nó có thể sắp xếp theo bất kỳ
thứ tự nào hoặc bất kỳ cách gì, đó là do caller quyết định.
7. Định nghĩa con trỏ hàm bằng từ khóa typedef
Phải thấy rằng sử dụng con trỏ hàm rất tiện lợi, tuy nhiên việc gán địa chỉ một
hàm cho nó và việc truy xuất con trỏ hàm rất dễ bị sai do phải viết nguyên prototype
của nó trong caller. Ví dụ, bạn có một con trỏ hàm như sau:

bool (*pComparison)(int, int);
Hàm sắp xếp muốn sử dụng nó một cách tùy biến, thì phải truyền nó vào như
một tham số đầu vào của hàm:
void SelectionSort(int *anArray, int nSize, bool (*pComparison)(int, int))
Để hạn chế điều này, người ta tìm cách “định nghĩa lại” con trỏ hàm bằng từ
khóa typedef.
Từ khóa typedef có tác dụng định nghĩa lại kiểu nào đó bằng một tên khác.
Trong lập trình C cơ bản, ta thường sử dụng từ khóa này định nghĩa cho một cấu trúc:
struct HocSinh
{
char
Hoten[30];
char Truong[30];
float Diem;
};
Khi ta muốn khai báo một mảng có tên là hsArray có 100 phần tử, thì bạn phải
khai báo:
struct HocSinh hsArray[100];
Để cho gọn khi khai báo trong chương trình, ta dùng từ khóa typedef để định
nghĩa lại cấu trúc HocSinh thành HS và xem HS như kiểu dữ liệu mới, mà kiểu dữ
liệu này tương tự như kiểu cấu trúc:
typedef struct HocSinh HS;

Trang 20

Reverse Engineering Association






Trang 21
Tham khảo toàn diện về trong C/C++
NhatPhuongLe
www.reaonline.net
Con trỏ

Reverse Engineering Association
cũng có thể định nghĩa theo kiểu:
typedef struct HocSinh
{
char
Hoten[30];
char Truong[30];
float Diem;
}HS;
Khi đó, trong chương trình ta không phải viết
struct HocSinh hsArray[100];
mà viết như sau:
HS hsArray[100];
Tương tự cho con trỏ hàm, thay vì định nghĩa bình thường ta sẽ định nghĩa như sau:
bool (*pComparison)(int, int);
bây giờ sử dụng từ khóa typedef, ta định nghĩa như sau:
typedef bool (*pComparison)(int, int);
Lúc này, trong chương trình có một hàm nào đó nhận con trỏ hàm như một tham
số đầu vào của nó, nếu như chưa sử dụng typedef để định nghĩa, bạn phải viết hàm
như sau:
void Caller(int *anArray, int nSize, bool (*pComparison)(int, int))
thì bây giờ bạn sẽ viết như sau:
void Caller(int *anArray, int nSize, pComparison pfn)

III. Tài liệu tham khảo
i. Con trỏ,
ii. Con trỏ trong C, />
iii. Pointers, />
iv. Introduction to Pointers, />
v. Function Pointers, />
vi. The Function Pointer Tutorials, />

×