1. Trang chủ
  2. » Công Nghệ Thông Tin

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

21 580 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 21
Dung lượng 584,11 KB

Nội dung

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ư

Trang 1

Tham khảo toàn diện về

Trang 2

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 3

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:

Trang 4

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:

1: #include <stdio.h> 1: #include <iostream>

2: int main () 2: int main ()

Trang 5

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)

Trang 6

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:

Trang 7

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 (17762000) là địa chỉ của 2 biến, còn những số bên trong (251776) 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;

Trang 8

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à:

Trang 9

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ụ:

đị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 10

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ỉ (&)

đượ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"

Vào lúc này, với những ví dụ đã viết ở trên

*pInt = 25

Trang 11

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ử

Trang 12

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

int (*pFoo) () = NULL; //C

Ở ví dụ C++ chúng ta giả sử rằng function mà function pointer trỏ đến là static member function của TMyClass

non-int (TMyClass::*pt2Member)(float, char, char) = NULL; //C++

int (TMyClass:: *pt2ConstMember)(float , char , char ) const = NULL ; //C++

Ngày đăng: 29/06/2014, 08:20

TỪ KHÓA LIÊN QUAN

TÀI LIỆU CÙNG NGƯỜI DÙNG

TÀI LIỆU LIÊN QUAN

w