Con trỏ là một khái niệm mà ban đầu đối với nhiều người có thể gây nhầm lẫn, đặc biệt khi nói đến việc sao chép các giá trị con trỏ xung quanh và vẫn tham chiếu cùng một khối bộ nhớ.
Tôi đã thấy rằng sự tương tự tốt nhất là coi con trỏ là một mảnh giấy có địa chỉ nhà ở trên đó và khối bộ nhớ mà nó tham chiếu như ngôi nhà thực tế. Tất cả các loại hoạt động có thể được giải thích dễ dàng.
Tôi đã thêm một số mã Delphi xuống bên dưới và một số nhận xét khi thích hợp. Tôi đã chọn Delphi vì ngôn ngữ lập trình chính khác của tôi, C #, không thể hiện những thứ như rò rỉ bộ nhớ theo cùng một cách.
Nếu bạn chỉ muốn tìm hiểu khái niệm con trỏ cấp cao, thì bạn nên bỏ qua các phần có nhãn "Bố cục bộ nhớ" trong phần giải thích bên dưới. Họ dự định đưa ra các ví dụ về bộ nhớ có thể trông như thế nào sau các hoạt động, nhưng về bản chất chúng có mức độ thấp hơn. Tuy nhiên, để giải thích chính xác cách thức tràn bộ đệm thực sự hoạt động, điều quan trọng là tôi đã thêm các sơ đồ này.
Tuyên bố miễn trừ trách nhiệm: Đối với tất cả ý định và mục đích, phần giải thích và bố trí bộ nhớ mẫu này được đơn giản hóa rất nhiều. Có nhiều chi phí hoạt động hơn và nhiều chi tiết hơn bạn sẽ cần biết nếu bạn cần xử lý bộ nhớ ở mức độ thấp. Tuy nhiên, đối với ý định giải thích bộ nhớ và con trỏ, nó đủ chính xác.
Giả sử lớp THouse được sử dụng dưới đây trông như thế này:
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
Khi bạn khởi tạo đối tượng ngôi nhà, tên được đặt cho hàm tạo sẽ được sao chép vào trường riêng FName. Có một lý do nó được định nghĩa là một mảng có kích thước cố định.
Trong bộ nhớ, sẽ có một số chi phí liên quan đến việc phân bổ nhà, tôi sẽ minh họa điều này dưới đây như sau:
--- [ttttNNNNNNNNNN] ---
^ ^
| |
| + - mảng FName
|
+ - trên cao
Vùng "tttt" nằm trên đầu, thông thường sẽ có nhiều hơn cho các loại thời gian chạy và ngôn ngữ khác nhau, như 8 hoặc 12 byte. Điều bắt buộc là bất kỳ giá trị nào được lưu trữ trong khu vực này sẽ không bao giờ bị thay đổi bởi bất kỳ thứ gì ngoài bộ cấp phát bộ nhớ hoặc các thói quen hệ thống cốt lõi, hoặc bạn có nguy cơ làm hỏng chương trình.
Phân bổ bộ nhớ
Nhận một doanh nhân để xây dựng ngôi nhà của bạn, và cung cấp cho bạn địa chỉ đến ngôi nhà. Trái ngược với thế giới thực, việc phân bổ bộ nhớ không thể được chỉ định nơi phân bổ, nhưng sẽ tìm một vị trí phù hợp với đủ chỗ và báo cáo lại địa chỉ cho bộ nhớ được phân bổ.
Nói cách khác, doanh nhân sẽ chọn vị trí.
THouse.Create('My house');
Bố cục bộ nhớ:
--- [ttttNNNNNNNNNN] ---
1234 nhà tôi
Giữ một biến với địa chỉ
Viết địa chỉ đến ngôi nhà mới của bạn trên một tờ giấy. Bài viết này sẽ phục vụ như là tài liệu tham khảo của bạn đến ngôi nhà của bạn. Không có mảnh giấy này, bạn bị lạc và không thể tìm thấy ngôi nhà, trừ khi bạn đã ở trong đó.
var
h: THouse;
begin
h := THouse.Create('My house');
...
Bố cục bộ nhớ:
h
v
--- [ttttNNNNNNNNNN] ---
1234 nhà tôi
Sao chép giá trị con trỏ
Chỉ cần viết địa chỉ trên một tờ giấy mới. Bây giờ bạn có hai mảnh giấy sẽ đưa bạn đến cùng một ngôi nhà, không phải hai ngôi nhà riêng biệt. Bất kỳ nỗ lực nào để theo địa chỉ từ một tờ giấy và sắp xếp lại đồ đạc trong ngôi nhà đó sẽ khiến cho ngôi nhà kia đã được sửa đổi theo cách tương tự, trừ khi bạn có thể phát hiện rõ ràng rằng đó thực sự chỉ là một ngôi nhà.
Lưu ý Đây thường là khái niệm mà tôi gặp nhiều vấn đề nhất khi giải thích với mọi người, hai con trỏ không có nghĩa là hai đối tượng hoặc khối bộ nhớ.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1
v
--- [ttttNNNNNNNNNN] ---
1234 nhà tôi
^
h2
Giải phóng bộ nhớ
Phá dỡ nhà. Sau đó, bạn có thể sử dụng lại giấy cho một địa chỉ mới nếu bạn muốn hoặc xóa nó để quên địa chỉ đến ngôi nhà không còn tồn tại.
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
h := nil;
Ở đây trước tiên tôi xây dựng ngôi nhà, và giữ địa chỉ của nó. Sau đó, tôi làm một cái gì đó cho ngôi nhà (sử dụng nó, ... mã, để lại như một bài tập cho người đọc), và sau đó tôi giải phóng nó. Cuối cùng tôi xóa địa chỉ từ biến của tôi.
Bố cục bộ nhớ:
h <- +
v + - trước khi miễn phí
--- [ttttNNNNNNNNNN] --- |
1234 Nhà tôi <- +
h (bây giờ không ở đâu) <- +
+ - sau khi rảnh
---------------------- | (lưu ý, bộ nhớ có thể vẫn còn
xx34 Nhà của tôi <- + chứa một số dữ liệu)
Con trỏ lủng lẳng
Bạn nói với doanh nhân của bạn để phá hủy ngôi nhà, nhưng bạn quên xóa địa chỉ khỏi mảnh giấy của bạn. Khi sau này bạn nhìn vào mảnh giấy, bạn đã quên rằng ngôi nhà không còn ở đó nữa và đi thăm nó, với kết quả không thành công (xem thêm phần về một tài liệu tham khảo không hợp lệ bên dưới).
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
Sử dụng h
sau cuộc gọi để .Free
có thể làm việc, nhưng đó chỉ là may mắn thuần túy. Nhiều khả năng nó sẽ thất bại, tại một địa điểm của khách hàng, ở giữa một hoạt động quan trọng.
h <- +
v + - trước khi miễn phí
--- [ttttNNNNNNNNNN] --- |
1234 Nhà tôi <- +
h <- +
v + - sau khi miễn phí
---------------------- |
xx34 Nhà tôi <- +
Như bạn có thể thấy, h vẫn chỉ vào phần còn lại của dữ liệu trong bộ nhớ, nhưng vì nó có thể không hoàn thành, sử dụng nó như trước có thể thất bại.
Bộ nhớ bị rò rỉ
Bạn bị mất mảnh giấy và không thể tìm thấy ngôi nhà. Ngôi nhà vẫn đứng ở đâu đó và khi bạn muốn xây dựng một ngôi nhà mới, bạn không thể sử dụng lại vị trí đó.
var
h: THouse;
begin
h := THouse.Create('My house');
h := THouse.Create('My house'); // uh-oh, what happened to our first house?
...
h.Free;
h := nil;
Ở đây chúng tôi ghi đè lên nội dung của h
biến với địa chỉ của một ngôi nhà mới, nhưng ngôi nhà cũ vẫn còn ... ở đâu đó. Sau mã này, không có cách nào để đến được ngôi nhà đó, và nó sẽ bị bỏ lại. Nói cách khác, bộ nhớ được phân bổ sẽ được phân bổ cho đến khi ứng dụng đóng lại, lúc đó hệ điều hành sẽ phá hỏng nó.
Bố trí bộ nhớ sau lần cấp phát đầu tiên:
h
v
--- [ttttNNNNNNNNNN] ---
1234 nhà tôi
Bố cục bộ nhớ sau khi cấp phát thứ hai:
h
v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234 Nhà tôi 5678 Nhà tôi
Một cách phổ biến hơn để có được phương pháp này là chỉ cần quên giải phóng một cái gì đó, thay vì ghi đè lên nó như trên. Theo thuật ngữ Delphi, điều này sẽ xảy ra với phương pháp sau:
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
Sau khi phương thức này được thực thi, không có chỗ nào trong các biến của chúng tôi có địa chỉ ngôi nhà tồn tại, nhưng ngôi nhà vẫn ở ngoài đó.
Bố cục bộ nhớ:
h <- +
v + - trước khi mất con trỏ
--- [ttttNNNNNNNNNN] --- |
1234 Nhà tôi <- +
h (bây giờ không ở đâu) <- +
+ - sau khi mất con trỏ
--- [ttttNNNNNNNNNN] --- |
1234 Nhà tôi <- +
Như bạn có thể thấy, dữ liệu cũ được giữ nguyên trong bộ nhớ và sẽ không được bộ cấp phát bộ nhớ sử dụng lại. Bộ cấp phát theo dõi những vùng bộ nhớ đã được sử dụng và sẽ không sử dụng lại chúng trừ khi bạn giải phóng nó.
Giải phóng bộ nhớ nhưng giữ tham chiếu (hiện không hợp lệ)
Phá hủy ngôi nhà, xóa một trong những mảnh giấy nhưng bạn cũng có một mảnh giấy khác có địa chỉ cũ trên đó, khi bạn đến địa chỉ, bạn sẽ không tìm thấy một ngôi nhà, nhưng bạn có thể tìm thấy thứ gì đó giống với tàn tích của một.
Có lẽ bạn thậm chí sẽ tìm thấy một ngôi nhà, nhưng đó không phải là ngôi nhà ban đầu bạn được cung cấp địa chỉ, và do đó, bất kỳ nỗ lực nào để sử dụng nó như thể nó thuộc về bạn có thể thất bại khủng khiếp.
Đôi khi, bạn thậm chí có thể thấy rằng một địa chỉ lân cận có một ngôi nhà khá lớn được thiết lập trên đó chiếm ba địa chỉ (Đường chính 1-3) và địa chỉ của bạn nằm ở giữa nhà. Bất kỳ nỗ lực nào để coi phần đó của ngôi nhà 3 địa chỉ lớn như một ngôi nhà nhỏ cũng có thể thất bại khủng khiếp.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1.Free;
h1 := nil;
h2.OpenFrontDoor; // uh-oh, what happened to our house?
Ở đây, ngôi nhà đã bị phá hủy, thông qua các tài liệu tham khảo h1
, và trong khi h1
cũng bị xóa, h2
vẫn còn địa chỉ cũ, lỗi thời. Truy cập vào ngôi nhà không còn có thể hoặc có thể không hoạt động.
Đây là một biến thể của con trỏ lơ lửng ở trên. Xem bố trí bộ nhớ của nó.
Bộ đệm tràn
Bạn di chuyển nhiều thứ vào nhà hơn mức bạn có thể phù hợp, tràn vào nhà hàng xóm hoặc sân. Khi chủ sở hữu của ngôi nhà lân cận đó về nhà sau, anh ta sẽ tìm thấy tất cả những thứ anh ta sẽ xem xét của riêng mình.
Đây là lý do tôi chọn một mảng có kích thước cố định. Để thiết lập giai đoạn, giả sử rằng ngôi nhà thứ hai mà chúng ta phân bổ sẽ, vì một số lý do, được đặt trước ngôi nhà đầu tiên trong bộ nhớ. Nói cách khác, ngôi nhà thứ hai sẽ có địa chỉ thấp hơn ngôi nhà thứ nhất. Ngoài ra, chúng được phân bổ ngay cạnh nhau.
Do đó, mã này:
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
Bố trí bộ nhớ sau lần cấp phát đầu tiên:
h1
v
----------------------- [ttttNNNNNNNNNN]
5678 Nhà của tôi
Bố cục bộ nhớ sau khi cấp phát thứ hai:
h2 h1
vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
1234 Nhà khác của tôi ở đâu đó
^ --- + - ^
|
+ - ghi đè
Phần thường gây ra sự cố là khi bạn ghi đè lên các phần quan trọng của dữ liệu bạn đã lưu trữ mà thực sự không nên thay đổi ngẫu nhiên. Ví dụ, có thể không có vấn đề gì khi các phần của tên của nhà h1 bị thay đổi, về mặt làm hỏng chương trình, nhưng việc ghi đè lên phần trên của đối tượng rất có thể sẽ bị sập khi bạn cố gắng sử dụng đối tượng bị hỏng, như sẽ ghi đè các liên kết được lưu trữ đến các đối tượng khác trong đối tượng.
Danh sách liên kết
Khi bạn theo dõi một địa chỉ trên một tờ giấy, bạn đến một ngôi nhà, và tại ngôi nhà đó có một mảnh giấy khác có địa chỉ mới trên đó, cho ngôi nhà tiếp theo trong chuỗi, v.v.
var
h1, h2: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
Ở đây chúng tôi tạo ra một liên kết từ nhà của chúng tôi đến cabin của chúng tôi. Chúng ta có thể theo chuỗi cho đến khi một ngôi nhà không có NextHouse
tài liệu tham khảo, có nghĩa là nó là ngôi nhà cuối cùng. Để truy cập tất cả các ngôi nhà của chúng tôi, chúng tôi có thể sử dụng mã sau đây:
var
h1, h2: THouse;
h: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
...
h := h1;
while h <> nil do
begin
h.LockAllDoors;
h.CloseAllWindows;
h := h.NextHouse;
end;
Bố cục bộ nhớ (được thêm NextHouse làm liên kết trong đối tượng, được ghi chú bằng bốn LLLL trong sơ đồ bên dưới):
h1 h2
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234Home + 5678Cabin +
| ^ |
+ -------- + * (không có liên kết)
Trong thuật ngữ cơ bản, địa chỉ bộ nhớ là gì?
Một địa chỉ bộ nhớ trong điều khoản cơ bản chỉ là một số. Nếu bạn nghĩ bộ nhớ là một mảng lớn byte, thì byte đầu tiên có địa chỉ 0, tiếp theo là địa chỉ 1 và cứ thế trở lên. Điều này được đơn giản hóa, nhưng đủ tốt.
Vì vậy, bố trí bộ nhớ này:
h1 h2
vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234 Nhà tôi 5678 Nhà tôi
Có thể có hai địa chỉ này (ngoài cùng bên trái - là địa chỉ 0):
Điều đó có nghĩa là danh sách được liên kết của chúng tôi ở trên có thể giống như thế này:
h1 (= 4) h2 (= 28)
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234Home 0028 5678Cabin 0000
| ^ |
+ -------- + * (không có liên kết)
Nó là điển hình để lưu trữ một địa chỉ "không nơi nào" là địa chỉ không.
Trong thuật ngữ cơ bản, một con trỏ là gì?
Một con trỏ chỉ là một biến giữ một địa chỉ bộ nhớ. Thông thường bạn có thể yêu cầu ngôn ngữ lập trình cung cấp cho bạn số của nó, nhưng hầu hết các ngôn ngữ lập trình và thời gian chạy đều cố gắng che giấu sự thật rằng có một số bên dưới, chỉ vì bản thân con số đó không thực sự có ý nghĩa gì với bạn. Tốt nhất là nghĩ về một con trỏ như một hộp đen, tức là. bạn không thực sự biết hoặc quan tâm đến cách nó được thực hiện, miễn là nó hoạt động.