intTypePromotion=1
zunia.vn Tuyển sinh 2024 dành cho Gen-Z zunia.vn zunia.vn
ADSENSE

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

Chia sẻ: Bùi Việt Sơn | Ngày: | Loại File: PDF | Số trang:21

174
lượt xem
39
download
 
  Download Vui lòng tải xuống để xem tài liệu đầy đủ

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ủ đề:
Lưu

Nội dung Text: Win32 Programming Tutorial  Tham khảo toàn diện về Con trỏ trong C/C++

  1. Win32 o toàn diện về Con trỏ trong C/C++   Tham khả Programming Tutorial Tham khảo toàn diện về “Con trỏ” trong C/C++ NhatPhuongLe For more updated info, please check Trang 1 www.reaonline.net http://nhatphuongle.spaces.live.com Reverse Engineering Association
  2. Tham khảo toàn diện về Con trỏ trong C/C++ 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 NhatPhuongLe Trang 2 www.reaonline.net Reverse Engineering Association
  3. Tham khảo toàn diện về Con trỏ trong C/C++ 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 tuâ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. Trong 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; NhatPhuongLe Trang 3 www.reaonline.net Reverse Engineering Association
  4. Tham khảo toàn diện về Con trỏ trong C/C++ 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 1: #include 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
  5. Tham khảo toàn diện về Con trỏ trong C/C++ 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 trong 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 cho 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). NhatPhuongLe Trang 5 www.reaonline.net Reverse Engineering Association
  6. Tham khảo toàn diện về Con trỏ trong C/C++ 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ì thế 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 con 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: NhatPhuongLe Trang 6 www.reaonline.net Reverse Engineering Association
  7. Tham khảo toàn diện về Con trỏ trong C/C++ 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 con 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; NhatPhuongLe Trang 7 www.reaonline.net Reverse Engineering Association
  8. Tham khảo toàn diện về Con trỏ trong C/C++ 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 ; và *pInt = integer ; cũng tương đương nhau. NhatPhuongLe Trang 8 www.reaonline.net Reverse Engineering Association
  9. Tham khảo toàn diện về Con trỏ trong C/C++ 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. NhatPhuongLe Trang 9 www.reaonline.net Reverse Engineering Association
  10. Tham khảo toàn diện về Con trỏ trong C/C++ 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 NhatPhuongLe Trang 10 www.reaonline.net Reverse Engineering Association
  11. Tham khảo toàn diện về Con trỏ trong C/C++ 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 Pointer: 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 pointer 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. NhatPhuongLe Trang 11 www.reaonline.net Reverse Engineering Association
  12. Tham khảo toàn diện về Con trỏ trong C/C++ 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ì function 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++ NhatPhuongLe Trang 12 www.reaonline.net Reverse Engineering Association
  13. Tham khảo toàn diện về Con trỏ trong C/C++ 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; NhatPhuongLe Trang 13 www.reaonline.net Reverse Engineering Association
  14. Tham khảo toàn diện về Con trỏ trong C/C++ // C++ class TMyClass { public: int DoIt(float a, char b, char c) { cout
  15. Tham khảo toàn diện về Con trỏ trong C/C++ 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ó. NhatPhuongLe Trang 15 www.reaonline.net Reverse Engineering Association
  16. Tham khảo toàn diện về Con trỏ trong C/C++ 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: NhatPhuongLe Trang 16 www.reaonline.net Reverse Engineering Association
  17. Tham khảo toàn diện về Con trỏ trong C/C++ 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); NhatPhuongLe Trang 17 www.reaonline.net Reverse Engineering Association
  18. Tham khảo toàn diện về Con trỏ trong C/C++ 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: NhatPhuongLe Trang 18 www.reaonline.net Reverse Engineering Association
  19. Tham khảo toàn diện về Con trỏ trong C/C++ 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: 246813579 NhatPhuongLe Trang 19 www.reaonline.net Reverse Engineering Association
  20. Tham khảo toàn diện về Con trỏ trong C/C++ 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; NhatPhuongLe Trang 20 www.reaonline.net Reverse Engineering Association
ADSENSE

CÓ THỂ BẠN MUỐN DOWNLOAD

 

Đồng bộ tài khoản
2=>2