2012-01-12 22 views
125

Tôi đã học C# đầu tiên, và bây giờ tôi bắt đầu với C++. Theo tôi hiểu, toán tử new trong C++ không giống với cái trong C#.Tại sao việc sử dụng 'mới' gây rò rỉ bộ nhớ?

Bạn có thể giải thích lý do rò rỉ bộ nhớ trong mã mẫu này không?

class A { ... }; 
struct B { ... }; 

A *object1 = new A(); 
B object2 = *(new B()); 
+0

Gần trùng lặp: [Thu gom rác tự động trong tiêu chuẩn C++?] (Http://stackoverflow.com/questions/1695042/is-garbage-collection-automatic-in-standard-c) – nobar

Trả lời

450

gì đang xảy ra

Khi bạn viết T t; bạn đang tạo một đối tượng kiểu T với thời gian lưu trữ tự động. Nó sẽ tự động được dọn sạch khi nó nằm ngoài phạm vi.

Khi bạn viết new T() bạn đang tạo đối tượng thuộc loại T với thời lượng lưu trữ động. Nó sẽ không được dọn dẹp tự động.

new without cleanup

Bạn cần phải vượt qua một con trỏ đến nó để delete để làm sạch nó lên:

newing with delete

Tuy nhiên, ví dụ thứ hai của bạn là tồi tệ hơn: bạn đang dereferencing con trỏ, và tạo một bản sao của đối tượng. Bằng cách này bạn sẽ mất con trỏ đến đối tượng được tạo ra với new, vì vậy bạn không bao giờ có thể xóa nó ngay cả khi bạn muốn!

newing with deref

Bạn nên làm gì

Bạn nên thích thời gian lưu trữ tự động. Cần một đối tượng mới, chỉ cần viết:

A a; // a new object of type A 
B b; // a new object of type B 

Nếu bạn cần thời gian lưu trữ động, lưu con trỏ đến đối tượng được phân bổ trong đối tượng thời lượng lưu trữ tự động xóa đối tượng đó tự động.

template <typename T> 
class automatic_pointer { 
public: 
    automatic_pointer(T* pointer) : pointer(pointer) {} 

    // destructor: gets called upon cleanup 
    // in this case, we want to use delete 
    ~automatic_pointer() { delete pointer; } 

    // emulate pointers! 
    // with this we can write *p 
    T& operator*() const { return *pointer; } 
    // and with this we can write p->f() 
    T* operator->() const { return pointer; } 

private: 
    T* pointer; 

    // for this example, I'll just forbid copies 
    // a smarter class could deal with this some other way 
    automatic_pointer(automatic_pointer const&); 
    automatic_pointer& operator=(automatic_pointer const&); 
}; 

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically 
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically 

newing with automatic_pointer

Đây là một thành ngữ phổ biến mà đi bằng tên RAII không-rất-mô tả (Resource Acquisition là khởi). Khi bạn có được một tài nguyên cần dọn dẹp, bạn hãy gắn nó vào một đối tượng có thời lượng lưu trữ tự động để bạn không cần phải lo lắng về việc dọn dẹp nó. Điều này áp dụng cho bất kỳ tài nguyên nào, có thể là bộ nhớ, mở tệp, kết nối mạng hoặc bất kỳ thứ gì bạn ưa thích.

Điều này automatic_pointer điều đã tồn tại ở nhiều dạng khác nhau, tôi vừa cung cấp nó để đưa ra ví dụ. Một lớp học rất giống nhau tồn tại trong thư viện chuẩn được gọi là std::unique_ptr.

Cũng có một phiên bản cũ (trước C++ 11) có tên auto_ptr nhưng hiện không được dùng vì nó có hành vi sao chép lạ.

Và sau đó có một số ví dụ thông minh hơn, chẳng hạn như std::shared_ptr, cho phép nhiều con trỏ trỏ đến cùng một đối tượng và chỉ xóa nó khi con trỏ cuối cùng bị hủy.

+4

@ user1131997: vui vì bạn đã thực hiện một câu hỏi khác. Như bạn có thể thấy nó không phải là rất dễ dàng để giải thích trong ý kiến ​​:) –

+0

@ R.MartinhoFernandes: câu trả lời xuất sắc. Chỉ một câu hỏi. Tại sao bạn sử dụng return by reference trong hàm operator *()? – Destructor

+0

@Destructor trả lời trễ: D. Trả về bằng tham chiếu cho phép bạn sửa đổi điểm mốc, vì vậy, bạn có thể thực hiện, ví dụ: '* p + = 2', giống như bạn làm với con trỏ bình thường. Nếu nó không trở lại bằng cách tham chiếu, nó sẽ không bắt chước hành vi của con trỏ bình thường, đó là ý định ở đây. –

7

Khi tạo object2 bạn đang tạo một bản sao của đối tượng mà bạn đã tạo với mới, nhưng bạn cũng đang mất đi sự (không bao giờ giao) con trỏ (vì vậy không có cách nào để xóa nó sau này). Để tránh điều này, bạn phải thực hiện một tham chiếu object2.

+3

Thực tiễn cực kỳ tồi tệ đối với lấy địa chỉ của một tham chiếu để xóa một đối tượng. Sử dụng một con trỏ thông minh. –

+3

Thực hành đáng kinh ngạc, eh? Bạn nghĩ gì về con trỏ thông minh sử dụng đằng sau hậu trường? – Blindy

+3

@Blindy con trỏ thông minh (ít nhất là được thực hiện một cách rõ ràng) sử dụng con trỏ trực tiếp. –

7

Đó là dòng này được ngay lập tức bị rò rỉ:

B object2 = *(new B()); 

Ở đây bạn đang tạo ra một đối tượng mới B trên heap, sau đó tạo ra một bản sao trên stack. Một trong đó đã được cấp phát trên heap không còn có thể được truy cập và do đó bị rò rỉ.

Dòng này không phải là ngay lập tức bị rò rỉ:

A *object1 = new A(); 

Sẽ có một chỗ rò rỉ nếu bạn không bao giờ delete d object1 mặc dù.

+4

Vui lòng không sử dụng đống/ngăn xếp khi giải thích lưu trữ động/tự động. – Pubby

+2

@Pubby tại sao không sử dụng? Bởi vì lưu trữ năng động/tự động luôn là đống, không phải ngăn xếp? Và đó là lý do tại sao không cần chi tiết về stack/heap, tôi có đúng không? –

+4

@ user1131997 Heap/stack là các chi tiết triển khai. Họ quan trọng cần biết, nhưng không liên quan đến câu hỏi này. – Pubby

7

Vâng, bạn tạo ra rò rỉ bộ nhớ nếu bạn không có lúc nào giải phóng bộ nhớ bạn đã cấp phát bằng toán tử new bằng cách chuyển con trỏ tới bộ nhớ đó tới toán tử delete.

Trong hai trường hợp của bạn ở trên:

A *object1 = new A(); 

Ở đây bạn không sử dụng delete để giải phóng bộ nhớ, vì vậy nếu và khi con trỏ object1 của bạn đi ra khỏi phạm vi, bạn sẽ có một rò rỉ bộ nhớ, bởi vì bạn sẽ mất con trỏ và vì vậy không thể sử dụng toán tử delete trên đó.

Và đây

B object2 = *(new B()); 

bạn đang vứt bỏ con trỏ trả về bởi new B(), và do đó không bao giờ có thể vượt qua con trỏ đến delete cho bộ nhớ được giải phóng. Do đó một rò rỉ bộ nhớ khác.

34

Một từng bước giải thích:

// creates a new object on the heap: 
new B() 
// dereferences the object 
*(new B()) 
// calls the copy constructor of B on the object 
B object2 = *(new B()); 

Vì vậy, vào cuối năm này, bạn có một đối tượng trên heap không có con trỏ đến nó, vì vậy nó không thể xóa.

Các mẫu khác:

A *object1 = new A(); 

là một rò rỉ bộ nhớ chỉ nếu bạn quên delete cấp phát bộ nhớ:

delete object1; 

Trong C++ có đối tượng với lưu trữ tự động, những người tạo ra trên stack , được tự động xử lý và đối tượng có bộ nhớ động, trên heap mà bạn phân bổ với new và được yêu cầu tự giải phóng với delete.(đây là tất cả gần đúng)

Hãy nghĩ rằng bạn cần có một delete cho mọi đối tượng được phân bổ với new.

EDIT

Hãy đến với suy nghĩ của nó, object2 không phải là một rò rỉ bộ nhớ.

Các mã sau chỉ là làm cho một điểm, đó là một ý tưởng tồi, đừng bao giờ như mã như thế này:

class B 
{ 
public: 
    B() {}; //default constructor 
    B(const B& other) //copy constructor, this will be called 
         //on the line B object2 = *(new B()) 
    { 
     delete &other; 
    } 
} 

Trong trường hợp này, vì other được thông qua tham khảo, nó sẽ là đối tượng chính xác được chỉ định bởi new B(). Do đó, nhận địa chỉ của nó bằng cách &other và xóa con trỏ sẽ giải phóng bộ nhớ.

Nhưng tôi không thể nhấn mạnh điều này, đừng làm vậy. Nó chỉ ở đây để làm cho một điểm.

+2

Tôi đã suy nghĩ giống nhau: chúng tôi có thể hack nó để không bị rò rỉ nhưng bạn sẽ không muốn làm điều đó. object1 không phải bị rò rỉ, hoặc vì hàm tạo của nó có thể đính kèm chính nó vào một loại cấu trúc dữ liệu nào đó sẽ xóa nó tại một số điểm. – CashCow

+2

Nó luôn luôn chỉ SO hấp dẫn để viết những "nó có thể làm điều này nhưng không" câu trả lời! :-) Tôi biết cảm giác – Kos

+0

Một lý do cho downvote xin vui lòng? –

9

Trong C# và Java, bạn sử dụng mới để tạo một thể hiện của bất kỳ lớp nào và sau đó bạn không cần phải lo lắng về việc hủy nó sau này.

C++ cũng có từ khóa "mới" tạo đối tượng nhưng không giống như trong Java hoặc C#, đó không phải là cách duy nhất để tạo đối tượng.

C++ có hai cơ chế để tạo ra một đối tượng:

  • tự động
  • động

Với sự sáng tạo tự động bạn tạo đối tượng trong một môi trường có phạm vi: - trong một chức năng hoặc - là thành viên của một lớp (hoặc cấu trúc).

Trong một hàm bạn sẽ tạo ra nó theo cách này:

int func() 
{ 
    A a; 
    B b(1, 2); 
} 

Trong một lớp học mà bạn thường sẽ tạo ra nó theo cách này:

class A 
{ 
    B b; 
public: 
    A(); 
};  

A::A() : 
b(1, 2) 
{ 
} 

Trong trường hợp đầu tiên, các đối tượng bị phá hủy tự động khi khối phạm vi được thoát. Đây có thể là một hàm hoặc một khối phạm vi trong một hàm.

Trong trường hợp thứ hai, đối tượng b bị phá hủy cùng với thể hiện của A mà trong đó nó là một thành viên.

Đối tượng được cấp phát mới khi bạn cần kiểm soát tuổi thọ của đối tượng và sau đó yêu cầu xóa để hủy bỏ đối tượng đó. Với kỹ thuật được gọi là RAII, bạn quan tâm đến việc xóa đối tượng tại điểm mà bạn tạo ra bằng cách đặt nó vào trong một đối tượng tự động và chờ cho phép hủy đối tượng tự động đó có hiệu lực.

Một đối tượng như vậy là shared_ptr sẽ gọi logic "deleter" nhưng chỉ khi tất cả các phiên bản shared_ptr đang chia sẻ đối tượng sẽ bị hủy.Nói chung, trong khi mã của bạn có thể có nhiều cuộc gọi đến mới, bạn nên có các cuộc gọi hạn chế để xóa và phải luôn đảm bảo chúng được gọi từ các đối tượng hủy hoặc "deleter" được đưa vào con trỏ thông minh.

Trình phá hoại của bạn cũng không bao giờ nên ném ngoại lệ.

Nếu bạn làm điều này, bạn sẽ có ít rò rỉ bộ nhớ.

+4

Có nhiều hơn 'tự động' và' động'. Cũng có 'tĩnh'. –

11

Với hai "đối tượng":

obj a; 
obj b; 

Họ sẽ không chiếm cùng một vị trí trong bộ nhớ. Nói cách khác, &a != &b

Gán giá trị của một cho bên kia sẽ không thay đổi vị trí của họ, nhưng nó sẽ thay đổi nội dung của họ:

obj a; 
obj b = a; 
//a == b, but &a != &b 

trực giác, con trỏ "đối tượng" làm việc theo cách giống nhau:

obj *a; 
obj *b = a; 
//a == b, but &a != &b 

Bây giờ, chúng ta hãy nhìn vào ví dụ của bạn:

A *object1 = new A(); 

Giá trị này chỉ định giá trị là new A() đến object1. Giá trị là một con trỏ, có nghĩa là object1 == new A(), nhưng &object1 != &(new A()). (Lưu ý rằng ví dụ này không phải là mã hợp lệ, chỉ giải thích)

Vì giá trị của con trỏ được giữ nguyên, chúng tôi có thể giải phóng bộ nhớ nó trỏ đến: delete object1; Do quy tắc của chúng tôi, hoạt động này giống như delete (new A()); không bị rò rỉ.


Ví dụ thứ hai, bạn đang sao chép đối tượng được chỉ định. Giá trị là nội dung của đối tượng đó, không phải là con trỏ thực. Như trong mọi trường hợp khác, &object2 != &*(new A()).

B object2 = *(new B()); 

Chúng tôi đã mất con trỏ tới bộ nhớ được cấp phát và do đó chúng tôi không thể giải phóng bộ nhớ được phân bổ. delete &object2; có vẻ như nó sẽ hoạt động, nhưng vì &object2 != &*(new A()), nó không tương đương với delete (new A()) và không hợp lệ.

9
B object2 = *(new B()); 

Đường này là nguyên nhân của sự rò rỉ. Hãy chọn cách này một chút ..

object2 là một biến loại B, được lưu trữ tại địa chỉ nói 1 (Có, tôi đang chọn số tùy ý ở đây). Ở bên phải, bạn đã yêu cầu một B mới, hoặc một con trỏ đến một đối tượng thuộc loại B. Chương trình sẵn sàng cung cấp cho bạn và gán B mới của bạn cho địa chỉ 2 và cũng tạo ra một con trỏ trong địa chỉ 3. Bây giờ, cách duy nhất để truy cập dữ liệu trong địa chỉ 2 là thông qua con trỏ trong địa chỉ 3. Tiếp theo, bạn dereferenced con trỏ bằng cách sử dụng * để có được dữ liệu mà con trỏ trỏ đến (dữ liệu trong địa chỉ 2). Điều này có hiệu quả tạo ra một bản sao của dữ liệu đó và gán nó cho object2, được gán trong địa chỉ 1. Hãy nhớ rằng, đó là một COPY, không phải bản gốc.

Bây giờ, đây là sự cố:

Bạn chưa bao giờ lưu trữ con trỏ đó ở bất cứ đâu bạn có thể sử dụng! Một khi nhiệm vụ này được hoàn thành, con trỏ (bộ nhớ trong address3, mà bạn sử dụng để truy cập address2) nằm ngoài phạm vi và ngoài tầm với của bạn! Bạn không còn có thể gọi xóa trên nó và do đó không thể dọn sạch bộ nhớ trong address2. Những gì bạn còn lại là một bản sao của dữ liệu từ address2 trong address1. Hai trong số những thứ giống nhau trong trí nhớ.Một cái bạn có thể truy cập, cái kia bạn không thể (bởi vì bạn đã mất đường dẫn đến nó). Đó là lý do tại sao đây là một rò rỉ bộ nhớ.

Tôi sẽ đề xuất đến từ nền C# của bạn mà bạn đọc rất nhiều về cách con trỏ trong C++ hoạt động. Họ là một chủ đề nâng cao và có thể mất một thời gian để nắm bắt, nhưng việc sử dụng chúng sẽ vô giá đối với bạn.

7

Nếu nó dễ dàng hơn, hãy nghĩ về bộ nhớ máy tính giống như khách sạn và chương trình là khách hàng thuê phòng khi họ cần.

Cách khách sạn này hoạt động là bạn đặt phòng và thông báo cho người khuân vác khi bạn rời đi.

Nếu bạn lập trình sổ sách và rời đi mà không nói với nhân viên khuân hành, người khuân hành lý sẽ cho rằng phòng vẫn đang được sử dụng và sẽ không cho phép bất kỳ ai khác sử dụng. Trong trường hợp này có rò rỉ phòng.

Nếu chương trình của bạn cấp phát bộ nhớ và không xóa nó (nó chỉ dừng sử dụng) thì máy tính cho rằng bộ nhớ vẫn đang được sử dụng và sẽ không cho phép bất kỳ ai sử dụng nó. Đây là một rò rỉ bộ nhớ.

Đây không phải là sự tương tự chính xác nhưng có thể hữu ích.

+5

Tôi khá giống như sự tương tự, nó không hoàn hảo, nhưng nó chắc chắn là một cách tốt để giải thích rò rỉ bộ nhớ cho những người mới với nó! – AdamM

+1

Tôi sử dụng điều này trong một cuộc phỏng vấn cho một kỹ sư cao cấp tại Bloomberg ở London để giải thích rò rỉ bộ nhớ cho một cô gái nhân sự. Tôi đã trải qua cuộc phỏng vấn đó bởi vì tôi đã có thể thực sự giải thích rò rỉ bộ nhớ (và các vấn đề luồng) cho một lập trình viên không theo cách cô ấy hiểu. – Stefan

Các vấn đề liên quan