Có một sự khác biệt quan trọng giữa hai.
Mọi thứ không được phân bổ new
giống như các loại giá trị trong C # (và mọi người thường nói rằng các đối tượng đó được phân bổ trên ngăn xếp, đây có thể là trường hợp phổ biến / rõ ràng nhất, nhưng không phải lúc nào cũng đúng. Chính xác hơn, các đối tượng được phân bổ mà không sử dụng new
có lưu trữ tự động thời lượng
Mọi thứ được phân bổ vớinew
được phân bổ trên heap và một con trỏ tới nó được trả về, giống như các kiểu tham chiếu trong C #.
Bất cứ thứ gì được phân bổ trên ngăn xếp đều phải có kích thước không đổi, được xác định tại thời gian biên dịch (trình biên dịch phải đặt con trỏ ngăn xếp chính xác hoặc nếu đối tượng là thành viên của lớp khác, nó phải điều chỉnh kích thước của lớp khác) . Đó là lý do tại sao mảng trong C # là loại tham chiếu. Chúng phải như vậy, vì với các kiểu tham chiếu, chúng ta có thể quyết định trong thời gian chạy cần bao nhiêu bộ nhớ. Và tương tự áp dụng ở đây. Chỉ các mảng có kích thước không đổi (kích thước có thể được xác định tại thời gian biên dịch) mới có thể được phân bổ với thời gian lưu trữ tự động (trên ngăn xếp). Các mảng có kích thước động phải được phân bổ trên heap, bằng cách gọi new
.
(Và đó là nơi tương tự với C # dừng lại)
Bây giờ, mọi thứ được phân bổ trên ngăn xếp đều có thời lượng lưu trữ "tự động" (bạn thực sự có thể khai báo một biến là auto
, nhưng đây là mặc định nếu không có loại lưu trữ nào khác được chỉ định để từ khóa không thực sự được sử dụng trong thực tế, nhưng đây là nơi nó được sử dụng đến từ)
Thời lượng lưu trữ tự động có nghĩa là chính xác những gì nó nghe, thời lượng của biến được xử lý tự động. Ngược lại, bất cứ thứ gì được phân bổ trên heap đều phải được bạn xóa bằng tay. Đây là một ví dụ:
void foo() {
bar b;
bar* b2 = new bar();
}
Hàm này tạo ra ba giá trị đáng xem xét:
Trên dòng 1, nó khai báo một biến b
loại bar
trên ngăn xếp (thời lượng tự động).
Trên dòng 2, nó khai báo một bar
con trỏ b2
trên ngăn xếp (thời lượng tự động) và gọi mới, phân bổ một bar
đối tượng trên heap. (thời lượng động)
Khi hàm trả về, điều sau đây sẽ xảy ra: Đầu tiên, b2
đi ra khỏi phạm vi (thứ tự phá hủy luôn trái ngược với thứ tự xây dựng). Nhưng b2
chỉ là một con trỏ, vì vậy không có gì xảy ra, bộ nhớ mà nó chiếm giữ chỉ đơn giản là được giải phóng. Và quan trọng, bộ nhớ mà nó trỏ đến ( bar
ví dụ trên heap) KHÔNG được chạm vào. Chỉ con trỏ được giải phóng, vì chỉ con trỏ có thời lượng tự động. Thứ hai, b
đi ra khỏi phạm vi, vì vậy nó có thời lượng tự động, nên hàm hủy của nó được gọi và bộ nhớ được giải phóng.
Và bar
ví dụ về heap? Có lẽ nó vẫn ở đó. Không ai bận tâm để xóa nó, vì vậy chúng tôi đã bị rò rỉ bộ nhớ.
Từ ví dụ này, chúng ta có thể thấy rằng bất cứ thứ gì có thời lượng tự động đều được đảm bảo có hàm hủy của nó được gọi khi nó đi ra khỏi phạm vi. Điều đó hữu ích. Nhưng bất cứ điều gì được phân bổ trên heap đều kéo dài miễn là chúng ta cần nó và có thể có kích thước động, như trong trường hợp mảng. Điều đó cũng hữu ích. Chúng ta có thể sử dụng điều đó để quản lý phân bổ bộ nhớ của chúng tôi. Điều gì sẽ xảy ra nếu lớp Foo cấp phát một số bộ nhớ trên heap trong hàm tạo của nó và xóa bộ nhớ đó trong hàm hủy của nó. Sau đó, chúng ta có thể có được những điều tốt nhất của cả hai thế giới, phân bổ bộ nhớ an toàn được đảm bảo sẽ được giải phóng một lần nữa, nhưng không có giới hạn buộc mọi thứ phải ở trên ngăn xếp.
Và đó là khá chính xác cách mà hầu hết mã C ++ hoạt động. Nhìn vào thư viện tiêu chuẩn std::vector
chẳng hạn. Điều đó thường được phân bổ trên ngăn xếp, nhưng có thể được kích thước động và thay đổi kích thước. Và nó thực hiện điều này bằng cách phân bổ nội bộ bộ nhớ trên heap khi cần thiết. Người dùng của lớp không bao giờ thấy điều này, vì vậy không có cơ hội rò rỉ bộ nhớ hoặc quên làm sạch những gì bạn đã phân bổ.
Nguyên tắc này được gọi là RAII (Thu nhận tài nguyên là khởi tạo) và nó có thể được mở rộng cho bất kỳ tài nguyên nào phải được mua và phát hành. (ổ cắm mạng, tệp, kết nối cơ sở dữ liệu, khóa đồng bộ hóa). Tất cả chúng có thể được mua trong hàm tạo và được phát hành trong hàm hủy, vì vậy bạn được đảm bảo rằng tất cả các tài nguyên bạn có được sẽ được giải phóng trở lại.
Theo nguyên tắc chung, không bao giờ sử dụng mới / xóa trực tiếp từ mã cấp cao của bạn. Luôn luôn bọc nó trong một lớp có thể quản lý bộ nhớ cho bạn và điều đó sẽ đảm bảo nó được giải phóng trở lại. (Vâng, có thể có ngoại lệ cho quy tắc này. Đặc biệt, con trỏ thông minh yêu cầu bạn gọi new
trực tiếp và chuyển con trỏ đến hàm tạo của nó, sau đó tiếp quản và đảm bảo delete
được gọi chính xác. Nhưng đây vẫn là một quy tắc rất quan trọng )