Khi nào một hàm hủy C ++ được gọi?


118

Câu hỏi cơ bản: khi nào một chương trình gọi phương thức hủy của lớp trong C ++? Tôi đã được thông báo rằng nó được gọi bất cứ khi nào một đối tượng đi ra ngoài phạm vi hoặc bịdelete

Các câu hỏi cụ thể hơn:

1) Nếu đối tượng được tạo thông qua một con trỏ và con trỏ đó sau đó bị xóa hoặc được cung cấp một địa chỉ mới để trỏ tới, liệu đối tượng mà nó đang trỏ tới có gọi hàm hủy của nó không (giả sử không có gì khác trỏ đến nó)?

2) Tiếp theo câu hỏi 1, điều gì xác định thời điểm một đối tượng đi ra khỏi phạm vi (không liên quan đến thời điểm một đối tượng rời khỏi một {khối} nhất định). Vậy, nói cách khác, khi nào thì một hàm hủy được gọi trên một đối tượng trong danh sách liên kết?

3) Bạn có bao giờ muốn gọi một trình hủy theo cách thủ công không?


3
Ngay cả những câu hỏi cụ thể của bạn cũng quá rộng. "Con trỏ đó sau đó đã bị xóa" và "được cung cấp một địa chỉ mới để trỏ tới" khá khác nhau. Tìm kiếm thêm (một số trong số này đã được trả lời), sau đó đặt câu hỏi riêng cho những phần bạn không thể tìm thấy.
Matthew Flaschen

Câu trả lời:


74

1) Nếu đối tượng được tạo thông qua một con trỏ và con trỏ đó sau đó bị xóa hoặc được cung cấp một địa chỉ mới để trỏ tới, liệu đối tượng mà nó đang trỏ tới có gọi hàm hủy của nó không (giả sử không có gì khác trỏ đến nó)?

Nó phụ thuộc vào loại con trỏ. Ví dụ, con trỏ thông minh thường xóa các đối tượng của chúng khi chúng bị xóa. Con trỏ thông thường thì không. Điều này cũng đúng khi một con trỏ được thực hiện để trỏ đến một đối tượng khác. Một số con trỏ thông minh sẽ phá hủy đối tượng cũ, hoặc sẽ phá hủy nó nếu nó không còn tham chiếu nữa. Những con trỏ thông thường không có trí thông minh như vậy. Họ chỉ giữ một địa chỉ và cho phép bạn thực hiện các thao tác trên các đối tượng mà họ trỏ đến bằng cách thực hiện cụ thể.

2) Tiếp theo câu hỏi 1, điều gì xác định thời điểm một đối tượng đi ra khỏi phạm vi (không liên quan đến thời điểm một đối tượng rời khỏi một {khối} nhất định). Vậy, nói cách khác, khi nào thì một hàm hủy được gọi trên một đối tượng trong danh sách liên kết?

Điều đó phụ thuộc vào việc triển khai danh sách liên kết. Các tập hợp điển hình phá hủy tất cả các đối tượng chứa của chúng khi chúng bị phá hủy.

Vì vậy, một danh sách liên kết các con trỏ thường sẽ phá hủy các con trỏ nhưng không phá hủy các đối tượng mà chúng trỏ tới. (Điều nào có thể đúng. Chúng có thể là tham chiếu của các con trỏ khác.) Tuy nhiên, một danh sách liên kết được thiết kế đặc biệt để chứa các con trỏ, có thể xóa các đối tượng theo cách riêng của nó.

Một danh sách liên kết các con trỏ thông minh có thể tự động xóa các đối tượng khi các con trỏ bị xóa hoặc làm như vậy nếu chúng không còn tham chiếu. Tất cả là tùy thuộc vào bạn để chọn các phần làm theo những gì bạn muốn.

3) Bạn có bao giờ muốn gọi một trình hủy theo cách thủ công không?

Chắc chắn rồi. Một ví dụ là nếu bạn muốn thay thế một đối tượng bằng một đối tượng khác cùng loại nhưng không muốn giải phóng bộ nhớ chỉ để cấp phát lại nó. Bạn có thể phá hủy đối tượng cũ tại chỗ và xây dựng đối tượng mới tại chỗ. (Tuy nhiên, nói chung đây là một ý tưởng tồi.)

// pointer is destroyed because it goes out of scope,
// but not the object it pointed to. memory leak
if (1) {
 Foo *myfoo = new Foo("foo");
}


// pointer is destroyed because it goes out of scope,
// object it points to is deleted. no memory leak
if(1) {
 Foo *myfoo = new Foo("foo");
 delete myfoo;
}

// no memory leak, object goes out of scope
if(1) {
 Foo myfoo("foo");
}

2
Tôi nghĩ ví dụ cuối cùng của bạn đã khai báo một hàm? Đó là một ví dụ về "phân tích cú pháp khó chịu nhất". (Vấn đề tầm thường hơn nữa là tôi đoán bạn có nghĩa là new Foo()với số vốn 'F'.)
Stuart Golodetz

1
Tôi nghĩ Foo myfoo("foo")không phải là Hầu hết các phân tích cú pháp Vexing, nhưng char * foo = "foo"; Foo myfoo(foo);là.
Cosine

Nó có thể là một câu hỏi ngu ngốc, nhưng không nên delete myFoođược gọi trước Foo *myFoo = new Foo("foo");? Hoặc nếu không, bạn sẽ xóa đối tượng mới tạo, không?
Matheus Rocha

Không có myFootrước Foo *myFoo = new Foo("foo");dòng. Dòng đó tạo ra một biến hoàn toàn mới có tên myFoo, phủ bóng bất kỳ biến hiện có nào. Mặc dù trong trường hợp này, không có cái nào hiện có vì phần myFootrên nằm trong phạm vi của if, cái đã kết thúc.
David Schwartz

1
@galactikuh "Con trỏ thông minh" là một thứ hoạt động giống như một con trỏ tới một đối tượng nhưng cũng có các tính năng giúp quản lý vòng đời của đối tượng đó dễ dàng hơn.
David Schwartz

20

Những người khác đã giải quyết các vấn đề khác, vì vậy tôi sẽ chỉ xem xét một điểm: bạn có bao giờ muốn xóa một đối tượng theo cách thủ công.

Câu trả lời là có. @DavidSchwartz đã đưa ra một ví dụ, nhưng đó là một ví dụ khá bất thường. Tôi sẽ đưa ra một ví dụ về điều mà rất nhiều lập trình viên C ++ sử dụng mọi lúc: std::vector(và std::dequemặc dù nó không được sử dụng nhiều).

Như hầu hết mọi người đều biết, std::vectorsẽ cấp phát một khối bộ nhớ lớn hơn khi / nếu bạn thêm nhiều mục hơn mức phân bổ hiện tại của nó có thể giữ. Tuy nhiên, khi nó thực hiện điều này, nó có một khối bộ nhớ có khả năng chứa nhiều đối tượng hơn hiện tại trong vector.

Để quản lý điều đó, những gì vectorbên dưới là cấp phát bộ nhớ thô thông qua Allocatorđối tượng (đối tượng này, trừ khi bạn chỉ định khác, có nghĩa là nó sử dụng ::operator new). Sau đó, khi bạn sử dụng (ví dụ) push_backđể thêm một mục vào vector, bên trong vectơ sử dụng a placement newđể tạo một mục trong phần (trước đây) không sử dụng của không gian bộ nhớ của nó.

Bây giờ, điều gì sẽ xảy ra khi / nếu bạn eraselà một mục từ vector? Nó không thể chỉ sử dụng delete- điều đó sẽ giải phóng toàn bộ khối bộ nhớ của nó; nó cần phải phá hủy một đối tượng trong bộ nhớ đó mà không phá hủy bất kỳ đối tượng nào khác hoặc giải phóng bất kỳ khối bộ nhớ nào mà nó kiểm soát (ví dụ: nếu bạn erase5 mục từ một vectơ, thì ngay lập tức push_backthêm 5 mục nữa, nó được đảm bảo rằng vectơ sẽ không phân bổ lại bộ nhớ khi bạn làm như vậy.

Để làm điều đó, vector trực tiếp phá hủy các đối tượng trong bộ nhớ bằng cách gọi rõ ràng hàm hủy, không phải bằng cách sử dụng delete.

Nếu tình cờ, ai đó đã viết một vùng chứa bằng cách sử dụng lưu trữ liền kề gần giống như một vectorhiện (hoặc một số biến thể của điều đó, giống như std::dequethực sự), bạn gần như chắc chắn muốn sử dụng kỹ thuật tương tự.

Ví dụ, hãy xem xét cách bạn có thể viết mã cho một bộ đệm vòng tròn.

#ifndef CBUFFER_H_INC
#define CBUFFER_H_INC

template <class T>
class circular_buffer {
    T *data;
    unsigned read_pos;
    unsigned write_pos;
    unsigned in_use;
    const unsigned capacity;
public:
    circular_buffer(unsigned size) :
        data((T *)operator new(size * sizeof(T))),
        read_pos(0),
        write_pos(0),
        in_use(0),
        capacity(size)
    {}

    void push(T const &t) {
        // ensure there's room in buffer:
        if (in_use == capacity) 
            pop();

        // construct copy of object in-place into buffer
        new(&data[write_pos++]) T(t);
        // keep pointer in bounds.
        write_pos %= capacity;
        ++in_use;
    }

    // return oldest object in queue:
    T front() {
        return data[read_pos];
    }

    // remove oldest object from queue:
    void pop() { 
        // destroy the object:
        data[read_pos++].~T();

        // keep pointer in bounds.
        read_pos %= capacity;
        --in_use;
    }
  
~circular_buffer() {
    // first destroy any content
    while (in_use != 0)
        pop();

    // then release the buffer.
    operator delete(data); 
}

};

#endif

Không giống như các thùng chứa tiêu chuẩn, điều này sử dụng operator newoperator deletetrực tiếp. Để sử dụng thực tế, bạn có thể muốn sử dụng một lớp cấp phát, nhưng hiện tại nó sẽ làm nhiều việc để phân tâm hơn là đóng góp (dù sao cũng là IMO).


9
  1. Khi bạn tạo một đối tượng với new, bạn có trách nhiệm gọi delete. Khi bạn tạo một đối tượng với make_shared, kết quả shared_ptrchịu trách nhiệm giữ số lượng và gọi deletekhi số lượng sử dụng bằng không.
  2. Đi ra khỏi phạm vi có nghĩa là rời khỏi một khối. Đây là khi hàm hủy được gọi, giả sử rằng đối tượng không được cấp phát với new(tức là nó là một đối tượng ngăn xếp).
  3. Khoảng thời gian duy nhất khi bạn cần gọi hàm hủy một cách rõ ràng là khi bạn phân bổ đối tượng với một vị trínew .

1
Có đếm tham chiếu (shared_ptr), mặc dù rõ ràng là không dành cho con trỏ đơn giản.
Pubby

1
@Pubby: Điểm tốt, chúng ta hãy thúc đẩy thực hành tốt. Câu trả lời đã được chỉnh sửa.
MSalters

6

1) Các đối tượng không được tạo 'thông qua con trỏ'. Có một con trỏ được gán cho bất kỳ đối tượng nào bạn 'mới'. Giả sử đây là ý của bạn, nếu bạn gọi 'delete' trên con trỏ, nó sẽ thực sự xóa (và gọi hàm hủy trên) đối tượng là con trỏ dereferences. Nếu bạn gán con trỏ cho một đối tượng khác sẽ có hiện tượng rò rỉ bộ nhớ; không có gì trong C ++ sẽ thu gom rác cho bạn.

2) Đây là hai câu hỏi riêng biệt. Một biến vượt ra ngoài phạm vi khi khung ngăn xếp mà nó được khai báo được bật ra khỏi ngăn xếp. Thông thường đây là khi bạn rời khỏi một khối. Các đối tượng trong một đống không bao giờ vượt ra ngoài phạm vi, mặc dù con trỏ của chúng trên ngăn xếp có thể. Không có gì đặc biệt đảm bảo rằng hàm hủy của một đối tượng trong danh sách liên kết sẽ được gọi.

3) Không hẳn. Có thể có Deep Magic sẽ đề xuất cách khác, nhưng thông thường, bạn muốn đối sánh từ khóa 'mới' với từ khóa 'xóa' và đặt mọi thứ cần thiết vào trình hủy của bạn để đảm bảo nó tự dọn dẹp đúng cách. Nếu bạn không làm điều này, hãy nhớ nhận xét trình hủy cùng với hướng dẫn cụ thể cho bất kỳ ai sử dụng lớp về cách họ nên dọn dẹp tài nguyên của đối tượng đó theo cách thủ công.


3

Để đưa ra câu trả lời chi tiết cho câu hỏi 3: vâng, có những trường hợp (hiếm) khi bạn có thể gọi hàm hủy một cách rõ ràng, đặc biệt là khi đối chiếu với một vị trí mới, như dasblinkenlight quan sát.

Để đưa ra một ví dụ cụ thể về điều này:

#include <iostream>
#include <new>

struct Foo
{
    Foo(int i_) : i(i_) {}
    int i;
};

int main()
{
    // Allocate a chunk of memory large enough to hold 5 Foo objects.
    int n = 5;
    char *chunk = static_cast<char*>(::operator new(sizeof(Foo) * n));

    // Use placement new to construct Foo instances at the right places in the chunk.
    for(int i=0; i<n; ++i)
    {
        new (chunk + i*sizeof(Foo)) Foo(i);
    }

    // Output the contents of each Foo instance and use an explicit destructor call to destroy it.
    for(int i=0; i<n; ++i)
    {
        Foo *foo = reinterpret_cast<Foo*>(chunk + i*sizeof(Foo));
        std::cout << foo->i << '\n';
        foo->~Foo();
    }

    // Deallocate the original chunk of memory.
    ::operator delete(chunk);

    return 0;
}

Mục đích của loại điều này là tách phân bổ bộ nhớ khỏi cấu trúc đối tượng.


2
  1. Con trỏ - Con trỏ thông thường không hỗ trợ RAII. Nếu không có một sự rõ ràng delete, sẽ có rác. May mắn thay, C ++ có con trỏ tự động xử lý điều này cho bạn!

  2. Phạm vi - Hãy nghĩ về thời điểm một biến trở nên vô hình với chương trình của bạn. Thông thường điều này nằm ở cuối {block}, như bạn đã chỉ ra.

  3. Phá hủy thủ công - Không bao giờ cố gắng này. Chỉ cần để phạm vi và RAII làm điều kỳ diệu cho bạn.


Lưu ý: auto_ptr không được dùng nữa, vì liên kết của bạn đã đề cập.
tnecniv

std::auto_ptrkhông được chấp nhận trong C ++ 11, vâng. Nếu OP thực sự có C ++ 11, anh ta nên sử dụng std::unique_ptrcho một chủ sở hữu duy nhất hoặc std::shared_ptrcho nhiều chủ sở hữu được tính tham chiếu.
chrisaycock

'Phá hủy thủ công - Không bao giờ thử điều này'. Tôi rất thường xếp hàng đợi các con trỏ đối tượng đến một luồng khác bằng cách sử dụng lệnh gọi hệ thống mà trình biên dịch không hiểu. Việc 'phụ thuộc' vào con trỏ phạm vi / tự động / thông minh sẽ khiến ứng dụng của tôi thất bại thảm hại vì các đối tượng bị chuỗi gọi điện xóa trước khi chuỗi người dùng xử lý chúng. Sự cố này ảnh hưởng đến các đối tượng và giao diện bị giới hạn phạm vi và refCounted. Chỉ con trỏ và xóa rõ ràng sẽ làm.
Martin James

@MartinJames Bạn có thể đăng ví dụ về lệnh gọi hệ thống mà trình biên dịch không hiểu được không? Và bạn đang triển khai hàng đợi như thế nào? Không std::queue<std::shared_ptr>?tôi đã phát hiện ra rằng pipe()giữa một nhà sản xuất và người tiêu dùng đề làm đồng thời dễ dàng hơn rất nhiều, nếu việc sao chép không phải là quá đắt.
trò chơi chrisaycock

myObject = new myClass (); PostMessage (aHandle, WM_APP, 0, LPPARAM (myObject));
Martin James

1

Bất cứ khi nào bạn sử dụng "mới", nghĩa là đính kèm một địa chỉ vào một con trỏ, hoặc nói cách khác, bạn yêu cầu không gian trên heap, bạn cần phải "xóa" nó.
1. vâng, khi bạn xóa một cái gì đó, hàm hủy sẽ được gọi.
2.Khi trình hủy của danh sách liên kết được gọi, trình hủy của đối tượng được gọi. Nhưng nếu chúng là con trỏ, bạn cần xóa chúng theo cách thủ công. 3. khi không gian được xác nhận bởi "mới".


0

Đúng vậy, một hàm hủy (hay còn gọi là dtor) được gọi khi một đối tượng vượt ra khỏi phạm vi nếu nó nằm trên ngăn xếp hoặc khi bạn gọi deletemột con trỏ tới một đối tượng.

  1. Nếu con trỏ bị xóa qua deletethì dtor sẽ được gọi. Nếu bạn gán lại con trỏ mà không gọi deletetrước, bạn sẽ bị rò rỉ bộ nhớ vì đối tượng vẫn tồn tại trong bộ nhớ ở đâu đó. Trong trường hợp sau, dtor không được gọi.

  2. Một triển khai danh sách liên kết tốt sẽ gọi dtor của tất cả các đối tượng trong danh sách khi danh sách đang bị hủy (vì bạn đã gọi một số phương thức để hủy nó hoặc chính nó đã vượt ra khỏi phạm vi). Điều này phụ thuộc vào việc triển khai.

  3. Tôi nghi ngờ điều đó, nhưng tôi sẽ không ngạc nhiên nếu có một số tình huống kỳ quặc ngoài đó.


1
"Nếu bạn gán lại con trỏ mà không gọi xóa trước, bạn sẽ bị rò rỉ bộ nhớ vì đối tượng vẫn tồn tại trong bộ nhớ ở đâu đó.". Không cần thiết. Nó có thể đã bị xóa thông qua một con trỏ khác.
Matthew Flaschen

0

Nếu đối tượng được tạo không thông qua một con trỏ (ví dụ: A a1 = A ();), hàm hủy được gọi khi đối tượng bị hủy, luôn luôn khi hàm nơi đối tượng nằm kết thúc. Ví dụ:

void func()
{
...
A a1 = A();
...
}//finish


hàm hủy được gọi khi mã được thực thi đến dòng "kết thúc".

Nếu đối tượng được tạo thông qua một con trỏ (ví dụ: A * a2 = new A ();), thì hàm hủy được gọi khi con trỏ bị xóa (xóa a2;). Nếu điểm không bị xóa bởi người dùng hoặc cho một địa chỉ mới trước khi xóa nó, rò rỉ bộ nhớ đã xảy ra. Đó là một lỗi.

Trong danh sách được liên kết, nếu chúng ta sử dụng std :: list <>, chúng ta không cần quan tâm đến trình mô tả hoặc rò rỉ bộ nhớ vì std :: list <> đã hoàn thành tất cả những điều này cho chúng ta. Trong danh sách liên kết do chính chúng ta viết, chúng ta nên viết bộ mô tả và xóa con trỏ một cách rõ ràng, nếu không sẽ gây rò rỉ bộ nhớ.

Chúng tôi hiếm khi gọi một trình hủy theo cách thủ công. Nó là một chức năng cung cấp cho hệ thống.

Xin lỗi vì vốn tiếng anh nghèo của tôi!


Không đúng khi bạn không thể gọi trình hủy theo cách thủ công - bạn có thể (ví dụ: xem mã trong câu trả lời của tôi). Có gì là đúng là đại đa số thời gian bạn không nên :)
Stuart Golodetz

0

Hãy nhớ rằng Constructor của một đối tượng được gọi ngay sau khi bộ nhớ được cấp phát cho đối tượng đó và trong khi hàm hủy được gọi ngay trước khi phân bổ bộ nhớ của đối tượng đó.

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.