Tại sao việc sử dụng 'mới' gây rò rỉ bộ nhớ?


131

Tôi đã học C # trước và bây giờ tôi bắt đầu với C ++. Theo tôi hiểu, toán tử newtrong C ++ không giống với toán tử trong C #.

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());

Câu trả lời:


464

Chuyện gì đang xảy ra

Khi bạn viết T t;bạn đang tạo một đối tượng loại Tthời lượng lưu trữ tự động . Nó sẽ được làm sạch tự động khi đi ra khỏi phạm vi.

Khi bạn viết new T()bạn đang tạo một đối tượng loại Tthời lượng lưu trữ động . Nó sẽ không được làm sạch tự động.

mới mà không dọn dẹp

Bạn cần chuyển một con trỏ tới nó deleteđể dọn sạch nó:

mới với xóa

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

mới với 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 lượng lưu trữ động, lưu trữ con trỏ đến đối tượng được phân bổ trong đối tượng thời lượng lưu trữ tự động sẽ tự động xóa nó.

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

mới với Automatic_pulum

Đây là một thành ngữ phổ biến đi theo tên không mô tả RAII ( Tài nguyên là khởi tạo tài nguyên ). Khi bạn có được một tài nguyên cần dọn dẹp, bạn sẽ 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 làm sạch 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 cứ thứ gì bạn thích.

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

Cũng có một cái cũ (tiền C ++ 11) có tên auto_ptr nhưng hiện tại nó không dùng nữa vì nó có hành vi sao chép lạ.

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


4
@ user1131997: rất vui vì bạn đã đặt câu hỏi này. Như bạn có thể thấy không dễ để giải thích trong các bình luận :)
R. Martinho Fernandes

@ R.MartinhoFernandes: câu trả lời tuyệt vời. Chỉ một câu hỏi. Tại sao bạn sử dụng return bởi tham chiếu trong hàm toán tử * ()?
Kẻ hủy diệt

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

Cảm ơn rất nhiều vì đã khuyên "lưu trữ con trỏ đến đối tượng được phân bổ trong một đối tượng thời lượng lưu trữ tự động xóa nó tự động." Nếu chỉ có một cách để yêu cầu các lập trình viên học mẫu này trước khi họ có thể biên dịch bất kỳ C ++ nào!
Andy

35

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 của điều này, bạn có một đối tượng trên heap không có con trỏ tới nó, vì vậy không thể xóa.

Các mẫu khác:

A *object1 = new A();

là rò rỉ bộ nhớ chỉ khi bạn quên deletebộ nhớ được phân bổ:

delete object1;

Trong C ++, có các đối tượng có lưu trữ tự động, các đối tượng được tạo trên ngăn xếp, được tự động xử lý và các đối tượng có lưu trữ động, trên heap, mà bạn phân bổ newvà được yêu cầu giải phóng chính mìnhdelete . (đây là tất cả đại khái)

Hãy nghĩ rằng bạn nên có một deletecho mọi đối tượng được phân bổ new.

BIÊN TẬP

Hãy nghĩ về nó, object2không phải là một rò rỉ bộ nhớ.

Đoạn mã sau chỉ là để tạo điểm nhấn, đó là một ý tưởng tồi, đừng bao giờ thích 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 truyền bằng tham chiếu, nó sẽ là đối tượng chính xác được trỏ đến new B(). Do đó, nhận địa chỉ của nó bằng cách &otherxó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 điều này. Nó chỉ ở đây để làm cho một điểm.


2
Tôi cũng nghĩ như vậy: 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 cũng không phải rò rỉ, vì hàm tạo của nó có thể tự gắ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
Luôn luôn thật hấp dẫn khi viết những câu trả lời "có thể làm điều này nhưng không"! :-) Tôi biết cảm giác
Kos

11

Cho 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

Việc gán giá trị của cái này cho cái kia sẽ không thay đổi vị trí của chúng, nhưng nó sẽ thay đổi nội dung của chúng:

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

Theo trực giác, con trỏ "đối tượng" hoạt động theo cùng một cách:

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

Bây giờ, hãy xem ví dụ của bạn:

A *object1 = new A();

Này được gán giá trị của new A()để object1. Giá trị là một con trỏ, ý nghĩa 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ệ, nó chỉ để giải thích)

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


Ví dụ thứ hai, bạn đang sao chép đối tượng trỏ vào. Giá trị là nội dung của đối tượng đó, không phải con trỏ thực tế. 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ỏ vào bộ nhớ được phân bổ, và do đó chúng tôi không thể giải phóng nó. delete &object2;có vẻ như nó sẽ hoạt động, nhưng bởi vì &object2 != &*(new A()), nó không tương đương delete (new A())và không hợp lệ.


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ó một từ khóa "mới" tạo ra một đối tượng nhưng không giống như trong Java hay C #, đó không phải là cách duy nhất để tạo một đối tượng.

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

  • tự động
  • năng động

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

Trong một chức năng, bạn sẽ tạo nó theo cách này:

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

Trong một lớp bạn thường sẽ tạo 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 sẽ tự động bị hủy 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 sau, đối tượng b bị phá hủy cùng với thể hiện của A trong đó nó là thành viên.

Các đối tượng được phân bổ mới khi bạn cần kiểm soát thời gian tồn tại của đối tượng và sau đó nó yêu cầu xóa để phá hủy nó. Với kỹ thuật được gọi là RAII, bạn xử lý việc xóa đối tượng tại điểm bạn tạo bằng cách đặt nó vào trong một đối tượng tự động và chờ cho hàm hủy của đố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 của shared_ptr đang chia sẻ đối tượng bị phá hủy.

Nói chung, trong khi mã của bạn có thể có nhiều lệnh gọi mới, bạn nên hạn chế các lệnh gọi để xóa và phải luôn đảm bảo các lệnh này được gọi từ các hàm hủy hoặc các đối tượng "deleter" được đưa vào các con trỏ thông minh.

Kẻ hủy diệt 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ó vài rò rỉ bộ nhớ.


4
Có nhiều hơn automaticdynamic. Cũng có static.
Vịt Mooing

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

Dòng này là nguyên nhân của rò rỉ. Chúng ta hãy chọn điều này một chút ..

object2 là một biến loại B, được lưu trữ tại địa chỉ 1 (Có, tôi đang chọn các 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ỏ tới một đối tượng 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 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 đã hủy đăng ký con trỏ bằng cách sử dụng* dữ liệu mà con trỏ trỏ đến (dữ liệu trong địa chỉ 2). Điều này thực sự 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 BẢN SAO chứ không phải bản gốc.

Bây giờ, đây là vấn đề:

Bạn không bao giờ thực sự lưu trữ con trỏ đó bất cứ nơi nào bạn có thể sử dụng nó! Khi nhiệm vụ này kết thúc, con trỏ (bộ nhớ trong địa chỉ 3, mà bạn đã sử dụng để truy cập địa chỉ 2) 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 đó và do đó không thể dọn sạch bộ nhớ trong địa chỉ2. 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 điều giống nhau ngồi trong bộ nhớ. Một cái bạn có thể truy cập, cái khác bạn không thể (vì bạn đã mất đường dẫn đến nó). Đó là lý do tại sao đây là rò rỉ bộ nhớ.

Tôi muốn đề 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. Chúng là một chủ đề nâng cao và có thể mất một chút thời gian để nắm bắt, nhưng việc sử dụng chúng sẽ là vô giá đối với bạn.


8

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

Cách thức hoạt động của khách sạn này là bạn đặt phòng và nói với người khuân vác khi bạn rời đi.

Nếu bạn lập trình đặt phòng và rời đi mà không nói với người khuân vác, người khuân vác sẽ nghĩ rằng căn phòng đó vẫn đang được sử dụng và sẽ không cho phép bất cứ ai khác sử dụng nó. Trong trường hợp này có một 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 nó) thì máy tính nghĩ rằng bộ nhớ vẫn đang được sử dụng và sẽ không cho phép bất kỳ ai khác sử dụng nó. Đây là một rò rỉ bộ nhớ.

Đây không phải là một sự tương tự chính xác nhưng nó có thể giúp đỡ.


5
Tôi khá thích 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 biết về 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 đã vượt qua cuộc phỏng vấn đó bởi vì tôi thực sự có thể giải thích rò rỉ bộ nhớ (và các vấn đề luồng) cho một người không lập trình theo cách cô ấy hiểu.
Stefan

7

Khi tạo object2bạn đang tạo một bản sao của đối tượng bạn đã tạo mới, nhưng bạn cũng đang mất con trỏ (không bao giờ được gán) (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 làm object2một tài liệu tham khảo.


3
Đó là một thực tế cực kỳ tồi tệ khi 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.
Tom Whittock

3
Thực hành cực kỳ xấu, nhỉ? Bạn nghĩ gì về con trỏ thông minh sử dụng đằng sau hậu trường?

3
@Blindy con trỏ thông minh (ít nhất là những con trỏ được triển khai thực hiện) sử dụng con trỏ trực tiếp.
Luchian Grigore

2
Chà, thành thật mà nói, toàn bộ ý tưởng không phải là tuyệt vời, phải không? Trên thực tế, tôi thậm chí không chắc mẫu thử trong OP sẽ thực sự hữu ích ở đâu.
Mario

7

Chà, bạn tạo ra rò rỉ bộ nhớ nếu đôi khi bạn không giải phóng bộ nhớ mà bạn đã phân bổ bằng newtoán tử bằng cách chuyển một con trỏ tới bộ nhớ đó chodelete toán tử.

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 object1con trỏ của bạn vượt quá phạm vi, bạn sẽ bị rò rỉ bộ nhớ, vì bạn sẽ bị mất con trỏ và vì vậy không thể sử dụngdelete toán tử trên nó.

Và đây

B object2 = *(new B());

bạn đang loại bỏ con trỏ được trả về bởi new B()vì vậy không bao giờ có thể chuyển con trỏ đó sang deletebộ nhớ để được giải phóng. Do đó một bộ nhớ bị rò rỉ.


7

Đây là dòng này ngay lập tức bị rò rỉ:

B object2 = *(new B());

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

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

A *object1 = new A();

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


4
Vui lòng không sử dụng heap / stack khi giải thích lưu trữ động / tự động.
Pubby

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

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

2
Hmm Tôi muốn có một câu trả lời riêng cho nó, tức là giống như của tôi nhưng thay thế heap / stack bằng những gì bạn nghĩ tốt nhất. Tôi muốn tìm hiểu làm thế nào bạn muốn giải thích nó.
mattjgalloway
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.