Điều gì xảy ra với rác trong C ++?


51

Java có một GC tự động thỉnh thoảng dừng lại Thế giới, nhưng xử lý rác trên một đống. Bây giờ các ứng dụng C / C ++ không có các đóng băng STW này, việc sử dụng bộ nhớ của chúng cũng không tăng lên vô hạn. Làm thế nào là hành vi này đạt được? Làm thế nào là các đối tượng chết được chăm sóc?


38
Lưu ý: stop-the-world là một lựa chọn triển khai của một số người thu gom rác, nhưng chắc chắn không phải tất cả. Ví dụ, có các GC đồng thời chạy đồng thời với trình biến đổi (đó là những gì các nhà phát triển GC gọi là chương trình thực tế). Tôi tin rằng bạn có thể mua phiên bản thương mại của JVM J9 mã nguồn mở của IBM có bộ sưu tập tạm dừng đồng thời. Azul Zing có trình thu thập "tạm dừng" không thực sự tạm dừng nhưng cực kỳ nhanh để không có tạm dừng đáng chú ý (tạm dừng GC của nó theo cùng thứ tự với chuyển đổi ngữ cảnh của luồng hệ điều hành, thường không được xem là tạm dừng) .
Jörg W Mittag

14
Hầu hết các (kéo dài) C ++ chương trình tôi sử dụng làm có sử dụng bộ nhớ rằng phát triển thời gian qua unboundedly. Có thể bạn không có thói quen để các chương trình mở trong hơn một vài ngày tại một thời điểm?
Jonathan Cast

12
Hãy xem xét rằng với C ++ hiện đại và các cấu trúc của nó, bạn không còn cần phải xóa bộ nhớ theo cách thủ công (trừ khi bạn sau khi tối ưu hóa đặc biệt), bởi vì bạn có thể quản lý bộ nhớ động thông qua con trỏ thông minh. Rõ ràng, nó bổ sung một số chi phí phát triển C ++ và bạn cần cẩn thận hơn một chút, nhưng đó không phải là một điều hoàn toàn khác, bạn chỉ cần nhớ sử dụng cấu trúc con trỏ thông minh thay vì chỉ gọi thủ công new.
Andy

9
Lưu ý rằng vẫn có thể có rò rỉ bộ nhớ trong ngôn ngữ được thu gom rác. Tôi không quen thuộc với Java, nhưng thật không may, rò rỉ bộ nhớ khá phổ biến trong thế giới .NET được quản lý. Các đối tượng được tham chiếu gián tiếp bởi một trường tĩnh không được tự động thu thập, các trình xử lý sự kiện là một nguồn rò rỉ rất phổ biến và tính chất không xác định của việc thu gom rác khiến nó không thể tránh hoàn toàn nhu cầu giải phóng tài nguyên theo cách thủ công (dẫn đến IDis mẫu). Tất cả đã nói, mô hình quản lý bộ nhớ C ++ được sử dụng đúng cách vượt trội hơn nhiều so với thu gom rác.
Cody Grey

26
What happens to garbage in C++? Nó thường không được biên dịch thành một thực thi?
BJ Myers

Câu trả lời:


100

Lập trình viên có trách nhiệm đảm bảo rằng các đối tượng họ tạo thông qua newsẽ bị xóa thông qua delete. Nếu một đối tượng được tạo, nhưng không bị phá hủy trước khi con trỏ cuối cùng hoặc tham chiếu đến nó vượt ra khỏi phạm vi, nó sẽ rơi qua các vết nứt và trở thành Rò rỉ bộ nhớ .

Thật không may cho C, C ++ và các ngôn ngữ khác không bao gồm GC, điều này chỉ đơn giản là chồng chất theo thời gian. Nó có thể khiến một ứng dụng hoặc hệ thống hết bộ nhớ và không thể phân bổ các khối bộ nhớ mới. Tại thời điểm này, người dùng phải dùng đến việc kết thúc ứng dụng để Hệ điều hành có thể lấy lại bộ nhớ đã sử dụng.

Theo như giảm thiểu vấn đề này, có một số điều làm cho cuộc sống của một lập trình viên dễ dàng hơn nhiều. Chúng chủ yếu được hỗ trợ bởi bản chất của phạm vi .

int main()
{
    int* variableThatIsAPointer = new int;
    int variableInt = 0;

    delete variableThatIsAPointer;
}

Ở đây, chúng tôi tạo ra hai biến. Chúng tồn tại trong Phạm vi Khối , như được định nghĩa bởi các {}dấu ngoặc nhọn. Khi thực hiện di chuyển ra khỏi phạm vi này, các đối tượng này sẽ tự động bị xóa. Trong trường hợp này variableThatIsAPointer, như tên của nó ngụ ý, là một con trỏ tới một đối tượng trong bộ nhớ. Khi nó đi ra khỏi phạm vi, con trỏ sẽ bị xóa, nhưng đối tượng mà nó trỏ đến vẫn còn. Ở đây, chúng tôi deleteđối tượng này trước khi nó đi ra khỏi phạm vi để đảm bảo rằng không có rò rỉ bộ nhớ. Tuy nhiên, chúng tôi cũng có thể đã vượt qua con trỏ này ở nơi khác và dự kiến ​​nó sẽ bị xóa sau này.

Bản chất của phạm vi này mở rộng cho các lớp:

class Foo
{
public:
    int bar; // Will be deleted when Foo is deleted
    int* otherBar; // Still need to call delete
}

Ở đây, nguyên tắc tương tự được áp dụng. Chúng tôi không phải lo lắng về việc barkhi nào Foosẽ bị xóa. Tuy nhiên otherBar, chỉ có con trỏ bị xóa. Nếu otherBarlà con trỏ hợp lệ duy nhất cho bất kỳ đối tượng nào mà nó trỏ đến, có lẽ chúng ta nên chọn deletenó trong hàm Foohủy của nó. Đây là khái niệm lái xe đằng sau RAII

phân bổ tài nguyên (thu nhận) được thực hiện trong quá trình tạo đối tượng (cụ thể là khởi tạo), bởi nhà xây dựng, trong khi việc phân bổ tài nguyên (giải phóng) được thực hiện trong quá trình phá hủy đối tượng (cụ thể là hoàn thiện), bởi hàm hủy. Do đó, tài nguyên được đảm bảo được giữ giữa khi khởi tạo kết thúc và bắt đầu hoàn thành (giữ tài nguyên là một bất biến lớp) và chỉ được giữ khi đối tượng còn sống. Do đó, nếu không có rò rỉ đối tượng, thì không có rò rỉ tài nguyên.

RAII cũng là động lực tiêu biểu của Smart Pointers . Trong C ++ thư viện chuẩn, đây là std::shared_ptr, std::unique_ptrstd::weak_ptr; mặc dù tôi đã thấy và sử dụng các shared_ptr/ weak_ptrtriển khai khác tuân theo các khái niệm tương tự. Đối với những điều này, một bộ đếm tham chiếu theo dõi có bao nhiêu con trỏ tới một đối tượng nhất định và tự động deletelà đối tượng một khi không có thêm tham chiếu đến nó.

Ngoài ra, tất cả bắt nguồn từ các thực tiễn và kỷ luật đúng đắn đối với một lập trình viên để đảm bảo rằng mã của họ xử lý các đối tượng đúng cách.


4
đã xóa qua delete- đó là những gì tôi đang tìm kiếm. Tuyệt vời.
Ju Shua

3
Bạn có thể muốn thêm về các cơ chế phạm vi được cung cấp trong c ++, cho phép phần lớn mới và xóa được thực hiện chủ yếu là tự động.
whatsisname

9
@whatsisname không phải là mới và xóa được thực hiện tự động, đó là chúng không xảy ra trong nhiều trường hợp
Caleth

10
Con trỏ thông minhdelete sẽ tự động được gọi cho bạn nếu bạn sử dụng chúng vì vậy bạn nên cân nhắc sử dụng chúng mỗi khi không thể sử dụng bộ lưu trữ tự động.
Mary Spanik

11
@JuShua Lưu ý rằng khi viết C ++ hiện đại, bạn không cần phải thực sự có deletemã ứng dụng của mình (và từ C ++ 14 trở đi, giống với new), nhưng thay vào đó hãy sử dụng con trỏ thông minh và RAII để xóa các đối tượng heap. std::unique_ptrloại và std::make_uniquechức năng là sự thay thế trực tiếp, đơn giản nhất newdeleteở cấp mã ứng dụng.
hyde

82

C ++ không có bộ sưu tập rác.

Các ứng dụng C ++ được yêu cầu xử lý rác của chính họ.

Các lập trình viên ứng dụng C ++ được yêu cầu phải hiểu điều này.

Khi họ quên, kết quả được gọi là "rò rỉ bộ nhớ".


22
Bạn chắc chắn chắc chắn rằng câu trả lời của bạn cũng không chứa bất kỳ rác nào, cũng không phải là
nồi hơi

15
@leftaroundabout: Cảm ơn bạn. Tôi coi đó là một lời khen.
John R. Strohm

1
OK câu trả lời không có rác này có một từ khóa để tìm kiếm: rò rỉ bộ nhớ. Nó cũng tốt để bằng cách nào đó đề cập đến newdelete.
Ruslan

4
@Ruslan Quy định này cũng áp dụng đối với mallocfree, hoặc new[]delete[], hoặc bất kỳ allocators khác (như Windows của GlobalAlloc, LocalAlloc, SHAlloc, CoTaskMemAlloc, VirtualAlloc, HeapAlloc, ...), và cấp phát bộ nhớ cho bạn (ví dụ như thông qua fopen).
dùng253751

43

Trong C, C ++ và các hệ thống khác không có Trình thu gom rác, nhà phát triển được cung cấp các phương tiện theo ngôn ngữ và thư viện của nó để cho biết khi nào bộ nhớ có thể được thu hồi.

Cơ sở cơ bản nhất là lưu trữ tự động . Nhiều lần, ngôn ngữ tự đảm bảo rằng các mục được xử lý:

int global = 0; // automatic storage

int foo(int a, int b) {
    static int local = 1; // automatic storage

    int c = a + b; // automatic storage

    return c;
}

Trong trường hợp này, trình biên dịch có trách nhiệm biết khi nào các giá trị đó không được sử dụng và lấy lại bộ nhớ được liên kết với chúng.

Khi sử dụng lưu trữ động , trong C, bộ nhớ được phân bổ theo truyền thống mallocvà được thu hồi với free. Trong C ++, bộ nhớ được phân bổ theo truyền thống newvà được thu hồi với delete.

C đã không thay đổi nhiều trong những năm qua, tuy nhiên, những người esse C ++ hiện đại newdeletehoàn toàn và thay vào đó dựa vào các cơ sở thư viện (mà chính họ sử dụng newdeletethích hợp):

  • con trỏ thông minh là nổi tiếng nhất: std::unique_ptrstd::shared_ptr
  • nhưng container được nhiều hơn nữa trên diện rộng thực sự: std::string, std::vector, std::map, ... tất cả trong nội bộ quản lý bộ nhớ cấp phát động một cách minh bạch

Nói về shared_ptr, có một rủi ro: nếu một chu kỳ tham chiếu được hình thành, và không bị phá vỡ, thì có thể có rò rỉ bộ nhớ. Tùy thuộc vào nhà phát triển để tránh tình huống này, cách đơn giản nhất là tránh shared_ptrhoàn toàn và cách đơn giản thứ hai là tránh chu kỳ ở cấp độ loại.

Kết quả là rò rỉ bộ nhớ là không phải là một vấn đề trong C ++ , ngay cả đối với người dùng mới, miễn là họ ngưng sử dụng new, deletehoặc std::shared_ptr. Điều này không giống như C khi một kỷ luật trung thành là cần thiết, và nói chung là không đủ.


Tuy nhiên, câu trả lời này sẽ không đầy đủ nếu không đề cập đến người chị em sinh đôi bị rò rỉ bộ nhớ: con trỏ lơ lửng .

Một con trỏ lơ lửng (hoặc tham chiếu lơ lửng) là một mối nguy hiểm được tạo ra bằng cách giữ một con trỏ hoặc tham chiếu đến một đối tượng đã chết. Ví dụ:

int main() {
    std::vector<int> vec;
    vec.push_back(1);     // vec: [1]

    int& a = vec.back();

    vec.pop_back();       // vec: [], "a" is now dangling

    std::cout << a << "\n";
}

Sử dụng một con trỏ lơ lửng hoặc tham chiếu là Hành vi không xác định . Nói chung, may mắn thay, đây là một vụ tai nạn ngay lập tức; khá thường xuyên, điều này gây ra lỗi bộ nhớ trước tiên ... và đôi khi hành vi kỳ lạ mọc lên vì trình biên dịch phát ra mã thực sự kỳ lạ.

Hành vi không xác định là vấn đề lớn nhất với C và C ++ cho đến ngày nay, về mặt bảo mật / tính chính xác của các chương trình. Bạn có thể muốn kiểm tra Rust cho một ngôn ngữ không có Trình thu gom rác và không có Hành vi không xác định.


17
Re: "Sử dụng một con trỏ lơ lửng, hoặc tham chiếu, là Hành vi không xác định . Nói chung, may mắn thay, đây là một sự cố ngay lập tức": Thật sao? Điều đó không phù hợp với kinh nghiệm của tôi cả; ngược lại, kinh nghiệm của tôi là việc sử dụng một con trỏ lơ lửng hầu như không bao giờ gây ra sự cố ngay lập tức. . .
ruakh

9
Vâng, vì để "treo lủng lẳng", một con trỏ phải nhắm mục tiêu bộ nhớ được phân bổ trước đó tại một thời điểm và bộ nhớ đó thường không có khả năng bị tách rời hoàn toàn khỏi quá trình sao cho nó không còn có thể truy cập được nữa, bởi vì nó sẽ là một ứng cử viên tốt để tái sử dụng ngay lập tức ... trong thực tế, con trỏ lơ lửng không gây ra sự cố, chúng gây ra sự hỗn loạn.
Leushenko

2
"Do rò rỉ bộ nhớ không phải là vấn đề trong C ++," Chắc chắn là như vậy, luôn có các ràng buộc C với các thư viện để xử lý, cũng như chia sẻ đệ quy hoặc thậm chí đệ quy unique_ptrs và các tình huống khác.
Vịt mướp

3
Không phải là một vấn đề trong C ++, ngay cả đối với người dùng mới, tôi sẽ đủ điều kiện cho những người dùng mới , những người không đến từ một ngôn ngữ giống như Java hoặc C tựa .
rẽ trái

3
@leftaroundabout: đó là trình độ "miễn là họ ngưng sử dụng new, deleteshared_ptr"; không có newshared_ptrbạn có quyền sở hữu trực tiếp nên không có rò rỉ. Tất nhiên, bạn có thể có con trỏ lơ lửng, v.v ... nhưng tôi e rằng bạn cần phải rời khỏi C ++ để thoát khỏi những điều đó.
Matthieu M.

27

C ++ có thứ gọi là RAII . Về cơ bản, điều đó có nghĩa là rác được dọn sạch khi bạn đi thay vì để nó thành một đống và để người dọn dẹp dọn dẹp sau bạn. (hãy tưởng tượng tôi trong phòng tôi đang xem bóng đá - khi tôi uống lon bia và cần những cái mới, cách C ++ là mang cái lon rỗng vào thùng trên đường đến tủ lạnh, cách C # là để nó trên sàn nhà và đợi người giúp việc đến đón họ khi cô ấy đến dọn dẹp).

Bây giờ có thể rò rỉ bộ nhớ trong C ++, nhưng để làm như vậy đòi hỏi bạn phải rời khỏi các cấu trúc thông thường và trở lại cách làm C - phân bổ một khối bộ nhớ và theo dõi nơi khối đó không có sự trợ giúp ngôn ngữ. Một số người quên con trỏ này và vì vậy không thể loại bỏ khối.


9
Con trỏ chia sẻ (sử dụng RAII) cung cấp một cách hiện đại để tạo rò rỉ. Giả sử các đối tượng A và B tham chiếu lẫn nhau thông qua các con trỏ được chia sẻ và không có gì khác tham chiếu đối tượng A hoặc đối tượng B. Kết quả là một rò rỉ. Tham chiếu lẫn nhau này là một vấn đề không phải là ngôn ngữ với bộ sưu tập rác.
David Hammen

@DavidHammen chắc chắn, nhưng với chi phí vượt qua hầu hết mọi đối tượng để đảm bảo. Ví dụ của bạn về con trỏ thông minh bỏ qua thực tế là chính con trỏ thông minh sẽ đi ra khỏi phạm vi và sau đó các đối tượng sẽ được giải phóng. Bạn cho rằng một con trỏ thông minh giống như một con trỏ, không phải nó, một đối tượng được truyền xung quanh trên ngăn xếp giống như hầu hết các tham số. Điều này không khác nhiều so với rò rỉ bộ nhớ gây ra trong các ngôn ngữ GC ,. ví dụ: một công cụ nổi tiếng trong đó việc loại bỏ một trình xử lý sự kiện khỏi lớp UI khiến nó âm thầm được tham chiếu và do đó bị rò rỉ.
gbjbaanb

1
@gbjbaanb trong ví dụ với các con trỏ thông minh, không con trỏ thông minh nào vượt quá phạm vi, đó là lý do tại sao có rò rỉ. Vì cả hai đối tượng con trỏ thông minh được phân bổ trong một phạm vi động , không phải là một từ vựng, nên mỗi đối tượng đều cố gắng chờ đợi đối tượng kia trước khi hủy. Thực tế là các con trỏ thông minh là các đối tượng thực trong C ++ và không chỉ các con trỏ chính xác là nguyên nhân gây ra rò rỉ ở đây - các đối tượng con trỏ thông minh bổ sung trong phạm vi ngăn xếp cũng chỉ vào các đối tượng chứa không thể tự hủy chúng khi chúng tự hủy. khác không.
Leushenko

2
Cách .NET không phải là để chuck nó trên sàn nhà. Nó chỉ giữ nó ở nơi đó cho đến khi người giúp việc đến. Và do cách .NET phân bổ bộ nhớ trong thực tế (không phải theo hợp đồng), heap giống như một ngăn xếp truy cập ngẫu nhiên. Nó giống như có một đống hợp đồng và giấy tờ, và thỉnh thoảng thực hiện nó để loại bỏ những thứ không còn hiệu lực nữa. Và để làm cho việc này dễ dàng hơn, những người sống sót sau mỗi lần loại bỏ được chuyển sang một ngăn xếp khác nhau, do đó bạn có thể tránh đi qua tất cả các ngăn xếp hầu hết thời gian - trừ khi ngăn xếp đầu tiên đủ lớn, người giúp việc không chạm vào người khác.
Luaan

@Luaan đó là một sự tương tự ... Tôi đoán bạn sẽ hạnh phúc hơn nếu tôi nói nó để lon nằm trên bàn cho đến khi người giúp việc đến dọn dẹp.
gbjbaanb

26

Cần lưu ý rằng, trong trường hợp của C ++, một quan niệm sai lầm phổ biến rằng "bạn cần thực hiện quản lý bộ nhớ thủ công". Thực tế, bạn thường không thực hiện bất kỳ quản lý bộ nhớ nào trong mã của mình.

Các đối tượng có kích thước cố định (có tuổi thọ phạm vi)

Trong phần lớn các trường hợp khi bạn cần một đối tượng, đối tượng sẽ có thời gian xác định trong chương trình của bạn và được tạo trên ngăn xếp. Điều này hoạt động cho tất cả các loại dữ liệu nguyên thủy tích hợp, nhưng cũng cho các trường hợp của các lớp và cấu trúc:

class MyObject {
    public: int x;
};

int objTest()
{
    MyObject obj;
    obj.x = 5;
    return obj.x;
}

Các đối tượng ngăn xếp được tự động loại bỏ khi chức năng kết thúc. Trong Java, các đối tượng luôn được tạo trên heap và do đó phải được loại bỏ bởi một số cơ chế như bộ sưu tập rác. Đây không phải là vấn đề đối với các đối tượng ngăn xếp.

Các đối tượng quản lý dữ liệu động (có phạm vi trọn đời)

Sử dụng không gian trên ngăn xếp hoạt động cho các đối tượng có kích thước cố định. Khi bạn cần một lượng không gian thay đổi, chẳng hạn như một mảng, một cách tiếp cận khác được sử dụng: Danh sách được gói gọn trong một đối tượng có kích thước cố định quản lý bộ nhớ động cho bạn. Điều này hoạt động vì các đối tượng có thể có chức năng dọn dẹp đặc biệt, hàm hủy. Nó được đảm bảo được gọi khi đối tượng đi ra khỏi phạm vi và thực hiện ngược lại với hàm tạo:

class MyList {        
public:
    // a fixed-size pointer to the actual memory.
    int* listOfInts; 
    // constructor: get memory
    MyList(size_t numElements) { listOfInts = new int[numElements]; }
    // destructor: free memory
    ~MyList() { delete[] listOfInts; }
};

int listTest()
{
    MyList list(1024);
    list.listOfInts[200] = 5;
    return list.listOfInts[200];
    // When MyList goes off stack here, its destructor is called and frees the memory.
}

Không có quản lý bộ nhớ nào trong mã nơi bộ nhớ được sử dụng. Điều duy nhất chúng ta cần đảm bảo là đối tượng chúng ta đã viết có hàm hủy phù hợp. Cho dù chúng tôi rời khỏi phạm vi như thế nào listTest, có thể thông qua một ngoại lệ hoặc đơn giản bằng cách quay lại từ nó, hàm hủy ~MyList()sẽ được gọi và chúng tôi không cần phải quản lý bất kỳ bộ nhớ nào.

(Tôi nghĩ rằng đó là một quyết định thiết kế hài hước khi sử dụng toán tử NOT nhị phân~ , để chỉ ra hàm hủy. Khi được sử dụng trên các số, nó đảo ngược các bit; tương tự, ở đây nó chỉ ra rằng những gì hàm tạo đã làm được đảo ngược.)

Về cơ bản tất cả các đối tượng C ++ cần bộ nhớ động đều sử dụng đóng gói này. Nó được gọi là RAII ("thu nhận tài nguyên là khởi tạo"), đây là một cách khá kỳ lạ để diễn đạt ý tưởng đơn giản mà các đối tượng quan tâm đến nội dung của chính họ; những gì họ có được là của họ để làm sạch.

Đối tượng đa hình và suốt đời vượt quá phạm vi

Bây giờ, cả hai trường hợp này đều dành cho bộ nhớ có thời gian tồn tại được xác định rõ ràng: Thời gian tồn tại giống như phạm vi. Nếu chúng ta không muốn một đối tượng hết hạn khi chúng ta rời khỏi phạm vi, có một cơ chế thứ ba có thể quản lý bộ nhớ cho chúng ta: một con trỏ thông minh. Con trỏ thông minh cũng được sử dụng khi bạn có phiên bản của các đối tượng có loại khác nhau khi chạy, nhưng có giao diện chung hoặc lớp cơ sở:

class MyDerivedObject : public MyObject {
    public: int y;
};
std::unique_ptr<MyObject> createObject()
{
    // actually creates an object of a derived class,
    // but the user doesn't need to know this.
    return std::make_unique<MyDerivedObject>();
}

int dynamicObjTest()
{
    std::unique_ptr<MyObject> obj = createObject();
    obj->x = 5;
    return obj->x;
    // At scope end, the unique_ptr automatically removes the object it contains,
    // calling its destructor if it has one.
}

Có một loại con trỏ thông minh khác std::shared_ptr, để chia sẻ các đối tượng giữa một số khách hàng. Họ chỉ xóa đối tượng được chứa của họ khi máy khách cuối cùng ra khỏi phạm vi, vì vậy chúng có thể được sử dụng trong các tình huống hoàn toàn không biết có bao nhiêu máy khách sẽ có và họ sẽ sử dụng đối tượng đó trong bao lâu.

Tóm lại, chúng tôi thấy rằng bạn không thực sự quản lý bộ nhớ thủ công. Tất cả mọi thứ được gói gọn và sau đó được chăm sóc bằng phương pháp quản lý bộ nhớ hoàn toàn tự động, dựa trên phạm vi. Trong trường hợp điều này là không đủ, con trỏ thông minh được sử dụng để đóng gói bộ nhớ thô.

Việc sử dụng con trỏ thô là chủ sở hữu tài nguyên ở bất kỳ nơi nào trong mã C ++, việc phân bổ thô bên ngoài các hàm tạo và deletecác lệnh gọi thô bên ngoài hàm hủy là điều gần như không thể quản lý khi ngoại lệ xảy ra và thường khó sử dụng một cách an toàn.

Tốt nhất: điều này hoạt động cho tất cả các loại tài nguyên

Một trong những lợi ích lớn nhất của RAII là nó không giới hạn bộ nhớ. Nó thực sự cung cấp một cách rất tự nhiên để quản lý các tài nguyên như tệp và ổ cắm (mở / đóng) và các cơ chế đồng bộ hóa như mutexes (khóa / mở khóa). Về cơ bản, mọi tài nguyên có thể được mua và phải được phát hành đều được quản lý theo cách chính xác giống như trong C ++ và không có quản lý nào còn lại cho người dùng. Tất cả được gói gọn trong các lớp thu nhận trong hàm tạo và giải phóng trong hàm hủy.

Ví dụ, một hàm khóa một mutex thường được viết như thế này trong C ++:

void criticalSection() {
    std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
    doSynchronizedStuff();
} // myMutex is released here automatically

Các ngôn ngữ khác làm cho điều này trở nên phức tạp hơn nhiều, bằng cách yêu cầu bạn thực hiện điều này bằng tay (ví dụ như trong một finallymệnh đề) hoặc chúng sinh ra các cơ chế chuyên biệt giải quyết vấn đề này, nhưng không phải theo cách đặc biệt tao nhã (thường là sau này trong cuộc sống của chúng, khi có đủ người chịu đựng những thiếu sót). Các cơ chế như vậy là các tài nguyên thử trong Java và câu lệnh sử dụng trong C #, cả hai đều là xấp xỉ RAII của C ++.

Vì vậy, để tóm tắt, tất cả những điều này là một tài khoản rất hời hợt của RAII trong C ++, nhưng tôi hy vọng rằng nó giúp người đọc hiểu rằng bộ nhớ và thậm chí quản lý tài nguyên trong C ++ thường không phải là "thủ công", mà thực sự chủ yếu là tự động.


7
Đây là câu trả lời duy nhất không gây hiểu lầm cho mọi người cũng như vẽ C ++ khó khăn hoặc nguy hiểm hơn thực tế.
Alexander Revo

6
BTW, nó chỉ được coi là thực hành xấu để sử dụng con trỏ thô làm chủ sở hữu tài nguyên. Không có gì sai khi sử dụng chúng nếu chúng chỉ ra thứ gì đó được đảm bảo để tồn tại lâu hơn chính con trỏ.
Alexander Revo

8
Tôi thứ hai Alexander. Tôi bối rối khi thấy "C ++ không có quản lý bộ nhớ tự động, hãy quên đi deletevà bạn đã chết" trả lời vượt quá 30 điểm và được chấp nhận, trong khi câu hỏi này có năm. Có ai thực sự sử dụng C ++ ở đây không?
Quentin

8

Đối với C cụ thể, ngôn ngữ cung cấp cho bạn không có công cụ để quản lý bộ nhớ được phân bổ động. Bạn hoàn toàn chịu trách nhiệm về việc đảm bảo mọi thứ đều *alloccó tương ứng freeở đâu đó.

Nơi mà mọi thứ trở nên thực sự khó chịu là khi phân bổ tài nguyên thất bại giữa chừng; Bạn có thử lại không, bạn có quay lại và bắt đầu lại từ đầu không, bạn có quay lại và thoát với một lỗi không, bạn có bảo lãnh hoàn toàn và để HĐH xử lý không?

Ví dụ: đây là một hàm để phân bổ một mảng 2D không liền kề. Hành vi ở đây là nếu một lỗi phân bổ xảy ra giữa chừng trong quá trình, chúng tôi sẽ khôi phục mọi thứ lại và trả về một dấu hiệu lỗi bằng con trỏ NULL:

/**
 * Allocate space for an array of arrays; returns NULL
 * on error.
 */
int **newArr( size_t rows, size_t cols )
{
  int **arr = malloc( sizeof *arr * rows );
  size_t i;

  if ( arr ) // malloc returns NULL on failure
  {
    for ( i = 0; i < rows; i++ )
    {
      arr[i] = malloc( sizeof *arr[i] * cols );
      if ( !arr[i] )
      {
        /**
         * Whoopsie; we can't allocate any more memory for some reason.
         * We can't just return NULL at this point since we'll lose access
         * to the previously allocated memory, so we branch to some cleanup
         * code to undo the allocations made so far.  
         */
        goto cleanup;
      }
    }
  }
  goto done;

/**
 * We encountered a failure midway through memory allocation,
 * so we roll back all previous allocations and return NULL.
 */
cleanup:
  while ( i )         // this is why we didn't limit the scope of i to the for loop
    free( arr[--i] ); // delete previously allocated rows
  free( arr );        // delete arr object
  arr = NULL;

done:
  return arr;
}

Mã này rất xấu với những cái đó goto, nhưng, trong trường hợp không có bất kỳ loại cơ chế xử lý ngoại lệ có cấu trúc nào, đây là cách duy nhất để giải quyết vấn đề mà không cần giải cứu hoàn toàn, đặc biệt là nếu mã phân bổ tài nguyên của bạn được lồng nhiều hơn sâu hơn một vòng. Đây là một trong số rất ít lần gotothực sự là một lựa chọn hấp dẫn; mặt khác, bạn đang sử dụng một loạt các cờ và ifbáo cáo thêm .

Bạn có thể làm cho cuộc sống của mình dễ dàng hơn bằng cách viết các hàm cấp phát / bộ phân phối chuyên dụng cho từng tài nguyên, đại loại như

Foo *newFoo( void )
{
  Foo *foo = malloc( sizeof *foo );
  if ( foo )
  {
    foo->bar = newBar();
    if ( !foo->bar ) goto cleanupBar;
    foo->bletch = newBletch(); 
    if ( !foo->bletch ) goto cleanupBletch;
    ...
  }
  goto done;

cleanupBletch:
  deleteBar( foo->bar );
  // fall through to clean up the rest

cleanupBar:
  free( foo );
  foo = NULL;

done:
  return foo;
}

void deleteFoo( Foo *f )
{
  deleteBar( f->bar );
  deleteBletch( f->bletch );
  free( f );
}

1
Đây là một câu trả lời tốt, ngay cả với các gototuyên bố. Đây là khuyến cáo thực hành trong một số lĩnh vực. Đây là một lược đồ thường được sử dụng để bảo vệ chống lại các trường hợp ngoại lệ tương đương trong C. Hãy xem mã hạt nhân Linux, chứa đầy các gotocâu lệnh - và không bị rò rỉ.
David Hammen

"mà không cần giải cứu hoàn toàn" -> một cách công bằng, nếu bạn muốn nói về C, đây có lẽ là một thực hành tốt. C là ngôn ngữ được sử dụng tốt nhất để xử lý các khối bộ nhớ đến từ một nơi khác hoặc chuyển các khối bộ nhớ nhỏ sang các quy trình khác, nhưng tốt nhất là không thực hiện cả hai cùng một lúc theo cách xen kẽ. Nếu bạn đang sử dụng các "đối tượng" cổ điển trong C, có khả năng bạn không sử dụng ngôn ngữ cho thế mạnh của nó.
Leushenko

Thứ hai gotolà ngoại lai. Sẽ dễ đọc hơn nếu bạn đổi goto done;thành return arr;arr=NULL;done:return arr;thành return NULL;. Mặc dù trong các trường hợp phức tạp hơn, thực sự có thể có nhiều gotos, bắt đầu hủy đăng ký ở các mức độ sẵn sàng khác nhau (điều sẽ được thực hiện bằng cách ngăn xếp ngoại lệ trong C ++).
Ruslan

2

Tôi đã học cách phân loại các vấn đề về bộ nhớ thành một số loại khác nhau.

  • Một lần nhỏ giọt. Giả sử một chương trình rò rỉ 100 byte khi khởi động, chỉ không bao giờ rò rỉ nữa. Theo đuổi và loại bỏ những rò rỉ một lần đó là tốt (tôi thích có một báo cáo rõ ràng bằng khả năng phát hiện rò rỉ) nhưng không cần thiết. Đôi khi có những vấn đề lớn hơn cần phải được tấn công.

  • Rò rỉ nhiều lần. Một chức năng được gọi là lặp đi lặp lại trong suốt quá trình kéo dài tuổi thọ chương trình thường xuyên rò rỉ bộ nhớ là một vấn đề lớn. Những giọt nước này sẽ tra tấn chương trình, và có thể cả HĐH, đến chết.

  • Tài liệu tham khảo lẫn nhau. Nếu các đối tượng A và B tham chiếu lẫn nhau thông qua các con trỏ được chia sẻ, bạn phải làm một cái gì đó đặc biệt, trong thiết kế của các lớp đó hoặc trong mã thực hiện / sử dụng các lớp đó để phá vỡ tính tuần hoàn. (Đây không phải là vấn đề đối với các ngôn ngữ được thu gom rác.)

  • Nhớ quá nhiều. Đây là anh em họ xấu xa của rò rỉ rác / bộ nhớ. RAII sẽ không giúp đỡ ở đây, cũng sẽ không thu gom rác. Đây là một vấn đề trong bất kỳ ngôn ngữ. Nếu một số biến hoạt động có một con đường kết nối nó với một đoạn bộ nhớ ngẫu nhiên, thì đoạn bộ nhớ ngẫu nhiên đó không phải là rác. Làm cho một chương trình trở nên hay quên để nó có thể chạy trong vài ngày là khó khăn. Làm một chương trình có thể chạy trong vài tháng (ví dụ, cho đến khi đĩa bị lỗi) là rất, rất khó.

Tôi đã không có một vấn đề nghiêm trọng với rò rỉ trong một thời gian dài. Sử dụng RAII trong C ++ rất nhiều giúp giải quyết những giọt nước và rò rỉ đó. (Tuy nhiên, người ta phải cẩn thận với các con trỏ được chia sẻ.) Quan trọng hơn là tôi đã gặp vấn đề với các ứng dụng mà việc sử dụng bộ nhớ tiếp tục tăng trưởng và phát triển do các kết nối không được kiểm soát tới bộ nhớ không còn sử dụng được nữa.


-6

Lập trình viên C ++ phải thực hiện hình thức thu gom rác của riêng mình khi cần thiết. Không làm như vậy sẽ dẫn đến cái gọi là 'rò rỉ bộ nhớ'. Điều khá phổ biến đối với các ngôn ngữ 'cấp độ cao' (như Java) đã được xây dựng trong bộ sưu tập rác, nhưng các ngôn ngữ 'cấp độ thấp' như C và C ++ thì khô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.