Hướng dẫn chung để tránh rò rỉ bộ nhớ trong C ++ [đã đóng]


130

Một số mẹo chung để đảm bảo tôi không bị rò rỉ bộ nhớ trong các chương trình C ++ là gì? Làm thế nào để tôi tìm ra ai nên giải phóng bộ nhớ đã được phân bổ động?


26
Có vẻ khá xây dựng với tôi.
Shoerob

11
Đây là xây dựng. Và các câu trả lời được hỗ trợ bởi các sự kiện, chuyên môn, tài liệu tham khảo, vv Và xem số lượng câu trả lời / câu trả lời .. !!
Samitha Chathuranga

Câu trả lời:


40

Thay vì quản lý bộ nhớ theo cách thủ công, hãy thử sử dụng con trỏ thông minh nếu có.
Hãy xem Boost lib , TR1con trỏ thông minh .
Ngoài ra con trỏ thông minh hiện là một phần của tiêu chuẩn C ++ có tên là C ++ 11 .


1
Để biên dịch bằng g ++, người ta cần thêm param: -std = c ++ 0x
Paweł Szczur

hoặc bạn có thể biên dịch với g ++ bằng cách sử dụng giá trị cờ -std = c ++ 11
Mitchhash Rathore

200

Tôi hoàn toàn tán thành tất cả các lời khuyên về RAII và các con trỏ thông minh, nhưng tôi cũng muốn thêm một mẹo cấp cao hơn một chút: bộ nhớ dễ quản lý nhất là bộ nhớ bạn không bao giờ phân bổ. Không giống như các ngôn ngữ như C # và Java, nơi mọi thứ đều là tài liệu tham khảo, trong C ++, bạn nên đặt các đối tượng lên ngăn xếp bất cứ khi nào bạn có thể. Như tôi đã thấy một số người (bao gồm cả Dr Stroustrup) chỉ ra, lý do chính tại sao bộ sưu tập rác chưa bao giờ phổ biến trong C ++ là vì C ++ được viết tốt không tạo ra nhiều rác ngay từ đầu.

Đừng viết

Object* x = new Object;

hoặc thậm chí

shared_ptr<Object> x(new Object);

khi nào bạn có thể viết

Object x;

34
Tôi ước tôi có thể cho điểm này +10. Đây là vấn đề lớn nhất mà tôi thấy với hầu hết các lập trình viên C ++ hiện nay và tôi cho rằng đó là do họ đã học Java trước C ++.
Kristopher Johnson

Điểm rất thú vị - Tôi đã tự hỏi tại sao tôi gặp vấn đề về quản lý bộ nhớ C ++ ít thường xuyên hơn so với các ngôn ngữ khác, nhưng bây giờ tôi hiểu tại sao: nó thực sự cho phép mọi thứ đi vào ngăn xếp như trong vanilla C.
ArtOfWarfare

Vậy bạn sẽ làm gì nếu bạn viết Object x; và sau đó muốn ném x đi? nói x được tạo trong phương thức chính.
Yamcha

3
@ user1316459 C ++ cũng cho phép bạn tạo phạm vi khi đang bay. Tất cả những gì bạn phải làm là bọc trọn đời x trong vòng niềng răng như vậy: {Object x; x.DoS Something; }. Sau '}' cuối cùng, hàm hủy của x sẽ được gọi là giải phóng mọi tài nguyên mà nó chứa. Nếu x, chính nó, là bộ nhớ được phân bổ trên heap, tôi khuyên bạn nên gói nó trong unique_ptr để nó được dọn sạch dễ dàng và phù hợp.
David Peterson

1
Robert: vâng. Ross đã không nói "Không bao giờ viết [mã chứa mới]", anh ta nói "Đừng viết [đó] khi bạn chỉ có thể [đặt nó vào ngăn xếp]". Các đối tượng lớn trên heap sẽ tiếp tục là cuộc gọi phù hợp trong hầu hết các tình huống, đặc biệt là đối với mã chuyên sâu về hiệu năng.
codetaku

104

Sử dụng RAII

  • Quên bộ sưu tập rác (Thay vào đó sử dụng RAII). Lưu ý rằng ngay cả Trình thu gom rác cũng có thể bị rò rỉ (nếu bạn quên "null" một số tham chiếu trong Java / C #) và Trình thu gom rác sẽ không giúp bạn loại bỏ tài nguyên (nếu bạn có một đối tượng thu được xử lý một tệp, tệp sẽ không được giải phóng tự động khi đối tượng sẽ ra khỏi phạm vi nếu bạn không thực hiện thủ công trong Java hoặc sử dụng mẫu "vứt bỏ" trong C #).
  • Quên quy tắc "một lần trả lại cho mỗi chức năng" . Đây là một lời khuyên C tốt để tránh rò rỉ, nhưng nó đã lỗi thời trong C ++ vì sử dụng ngoại lệ (thay vào đó sử dụng RAII).
  • Và mặc dù "Mô hình bánh sandwich" là một lời khuyên C tốt, nó đã lỗi thời trong C ++ vì sử dụng ngoại lệ (thay vào đó sử dụng RAII).

Bài đăng này dường như được lặp đi lặp lại, nhưng trong C ++, mẫu cơ bản nhất cần biết là RAII .

Tìm hiểu cách sử dụng con trỏ thông minh, cả từ boost, TR1 hoặc thậm chí là thấp (nhưng thường đủ hiệu quả) auto_ptr (nhưng bạn phải biết giới hạn của nó).

RAII là cơ sở của cả an toàn ngoại lệ và xử lý tài nguyên trong C ++ và không có mô hình nào khác (sandwich, v.v.) sẽ cung cấp cho bạn cả hai (và hầu hết thời gian, nó sẽ không cung cấp cho bạn).

Xem bên dưới so sánh mã RAII và mã RAII:

void doSandwich()
{
   T * p = new T() ;
   // do something with p
   delete p ; // leak if the p processing throws or return
}

void doRAIIDynamic()
{
   std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

void doRAIIStatic()
{
   T p ;
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

Về RAII

Để tóm tắt (sau nhận xét từ Ogre Psalm33 ), RAII dựa trên ba khái niệm:

  • Một khi đối tượng được xây dựng, nó chỉ hoạt động! Không có được tài nguyên trong các nhà xây dựng.
  • Phá hủy đối tượng là đủ! Làm tài nguyên miễn phí trong hàm hủy.
  • Đó là tất cả về phạm vi! Các đối tượng có phạm vi (xem ví dụ doRAIIStatic ở trên) sẽ được xây dựng theo khai báo của chúng và sẽ bị hủy ngay khi thực thi thoát khỏi phạm vi, bất kể lối ra (trả lại, ngắt, ngoại lệ, v.v.).

Điều này có nghĩa là trong mã C ++ chính xác, hầu hết các đối tượng sẽ không được xây dựng newvà thay vào đó sẽ được khai báo trên ngăn xếp. Và đối với những người được xây dựng bằng cách sử dụng new, tất cả sẽ nằm trong phạm vi nào đó (ví dụ được gắn vào một con trỏ thông minh).

Là một nhà phát triển, điều này thực sự rất mạnh mẽ vì bạn sẽ không cần quan tâm đến việc xử lý tài nguyên thủ công (như được thực hiện trong C hoặc đối với một số đối tượng trong Java, sử dụng nhiều try/ finallycho trường hợp đó) ...

Chỉnh sửa (2012/02/12)

"Các đối tượng trong phạm vi ... sẽ bị phá hủy ... bất kể lối ra" không hoàn toàn đúng. Có nhiều cách để lừa đảo RAII. bất kỳ hương vị chấm dứt () sẽ bỏ qua việc dọn dẹp. exit (EXIT_SUCCESS) là một oxymoron trong vấn đề này.

- wilmustell

wilmustell hoàn toàn đúng về điều đó: Có những cách đặc biệt để lừa đảo RAII, tất cả đều dẫn đến quá trình dừng đột ngột.

Đó là những cách đặc biệt bởi vì mã C ++ không bị vấy bẩn khi chấm dứt, thoát, v.v. hoặc trong trường hợp ngoại lệ, chúng tôi muốn có một ngoại lệ chưa được xử lý để phá vỡ quy trình và làm hỏng hình ảnh bộ nhớ của nó, và không phải sau khi làm sạch.

Nhưng chúng ta vẫn phải biết về những trường hợp đó bởi vì, trong khi chúng hiếm khi xảy ra, chúng vẫn có thể xảy ra.

(ai gọi terminatehoặc sử dụng exitmã C ++ thông thường? ... Tôi nhớ phải xử lý vấn đề đó khi chơi với GLUT : Thư viện này rất hướng C, đi xa đến mức chủ động thiết kế nó để gây khó khăn cho các nhà phát triển C ++ như không quan tâm về ngăn xếp dữ liệu được phân bổ hoặc có các quyết định "thú vị" về việc không bao giờ quay lại từ vòng lặp chính của họ ... Tôi sẽ không bình luận về điều đó) .


Không phải lớp T sử dụng RAII để chắc chắn rằng doRAIIStatic () không bị rò rỉ bộ nhớ? Ví dụ T p (); p.doSandwich (); Tôi thực sự không biết nhiều về điều này.
Daniel O

@Ogre Psalm33: Cảm ơn bạn đã bình luận. Tất nhiên, bạn đúng. Tôi đã thêm cả hai liên kết vào trang Wikipedia RAII và một bản tóm tắt nhỏ về RAII là gì.
paercebal

1
@Shiftbit: Ba cách, theo thứ tự ưu tiên: _ _ _ 1. Đặt đối tượng thực vào trong thùng chứa STL. _ _ _ 2. Đặt con trỏ thông minh (shared_ptr) của các đối tượng vào trong thùng chứa STL. _ _ _ 3. Đặt các con trỏ thô bên trong thùng chứa STL, nhưng bọc thùng chứa để kiểm soát mọi truy cập vào dữ liệu. Trình bao bọc sẽ đảm bảo rằng hàm hủy sẽ giải phóng các đối tượng được phân bổ và các trình truy cập trình bao bọc sẽ đảm bảo không có gì bị hỏng khi truy cập / sửa đổi vùng chứa.
paercebal

1
@Robert: Trong C ++ 03, bạn sẽ sử dụng doRAIID ẩn trong một hàm phải trao quyền sở hữu cho hàm con hoặc hàm cha (hoặc phạm vi toàn cục). Hoặc khi bạn nhận được một giao diện cho một đối tượng đa hình thông qua một nhà máy (trả về một con trỏ thông minh, nếu nó được viết chính xác). Trong C ++ 11, trường hợp này ít xảy ra hơn vì bạn có thể làm cho đối tượng của mình có thể di chuyển được, do đó, việc sở hữu một đối tượng được khai báo trên ngăn xếp sẽ dễ dàng hơn ...
paercebal

2
@Robert: ... Lưu ý rằng việc khai báo một đối tượng trên ngăn xếp không có nghĩa là đối tượng đó không sử dụng heap bên trong (lưu ý phủ định kép ... :-) ...). Ví dụ: chuỗi std :: được triển khai với Tối ưu hóa chuỗi nhỏ sẽ có bộ đệm "trên ngăn xếp của lớp" cho các chuỗi nhỏ (~ 15 ký tự) và sẽ sử dụng một con trỏ tới bộ nhớ trong heap cho chuỗi lớn hơn ... Nhưng từ bên ngoài, std :: string vẫn là một loại giá trị mà bạn khai báo (thường là) trên ngăn xếp và bạn sử dụng như bạn sẽ sử dụng một số nguyên (trái ngược với: như bạn sẽ sử dụng giao diện cho lớp đa hình).
paercebal

25

Bạn sẽ muốn xem xét các con trỏ thông minh, chẳng hạn như con trỏ thông minh của boost .

Thay vì

int main()
{ 
    Object* obj = new Object();
    //...
    delete obj;
}

boost :: shared_ptr sẽ tự động xóa sau khi số tham chiếu bằng 0:

int main()
{
    boost::shared_ptr<Object> obj(new Object());
    //...
    // destructor destroys when reference count is zero
}

Lưu ý lưu ý cuối cùng của tôi, "khi số tham chiếu bằng 0, đó là phần thú vị nhất. Vì vậy, nếu bạn có nhiều người dùng đối tượng của mình, bạn sẽ không phải theo dõi xem đối tượng có còn được sử dụng hay không. con trỏ chia sẻ, nó bị phá hủy.

Đây không phải là thuốc chữa bách bệnh, tuy nhiên. Mặc dù bạn có thể truy cập con trỏ cơ sở, bạn sẽ không muốn chuyển nó sang API của bên thứ 3 trừ khi bạn tự tin với những gì nó đang làm. Rất nhiều lần, công cụ "đăng" của bạn lên một số chủ đề khác để hoàn thành công việc SAU KHI phạm vi tạo đã kết thúc. Điều này phổ biến với PostThreadMessage trong Win32:

void foo()
{
   boost::shared_ptr<Object> obj(new Object()); 

   // Simplified here
   PostThreadMessage(...., (LPARAM)ob.get());
   // Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}

Như mọi khi, hãy sử dụng nắp suy nghĩ của bạn với bất kỳ công cụ nào ...



11

Hầu hết các rò rỉ bộ nhớ là kết quả của việc không rõ ràng về quyền sở hữu đối tượng và tuổi thọ.

Điều đầu tiên cần làm là phân bổ trên Stack bất cứ khi nào bạn có thể. Điều này giải quyết hầu hết các trường hợp bạn cần phân bổ một đối tượng cho một mục đích nào đó.

Nếu bạn cần 'mới' một đối tượng thì phần lớn thời gian nó sẽ có một chủ sở hữu rõ ràng duy nhất trong suốt quãng đời còn lại. Trong tình huống này, tôi có xu hướng sử dụng một loạt các mẫu bộ sưu tập được thiết kế để 'sở hữu' các đối tượng được lưu trữ trong chúng bằng con trỏ. Chúng được thực hiện với các vectơ STL và các thùng chứa bản đồ nhưng có một số khác biệt:

  • Những bộ sưu tập này không thể được sao chép hoặc gán cho. (một khi chúng chứa các đối tượng.)
  • Con trỏ đến các đối tượng được chèn vào chúng.
  • Khi bộ sưu tập bị xóa, hàm hủy được gọi đầu tiên trên tất cả các đối tượng trong bộ sưu tập. (Tôi có một phiên bản khác, nơi nó xác nhận nếu bị phá hủy và không trống.)
  • Vì họ lưu trữ con trỏ, bạn cũng có thể lưu trữ các đối tượng được kế thừa trong các thùng chứa này.

Điểm yếu của tôi với STL là nó tập trung vào các đối tượng Giá trị trong khi trong hầu hết các đối tượng ứng dụng là các thực thể duy nhất không có ngữ nghĩa sao chép có ý nghĩa cần thiết để sử dụng trong các thùng chứa đó.


10

Bah, những đứa trẻ và những người thu gom rác mới của bạn ...

Các quy tắc rất mạnh về "quyền sở hữu" - đối tượng hoặc bộ phận nào của phần mềm có quyền xóa đối tượng. Xóa bình luận và tên biến khôn ngoan để làm cho nó rõ ràng nếu một con trỏ "sở hữu" hoặc "chỉ nhìn, không chạm". Để giúp quyết định ai sở hữu cái gì, hãy theo dõi càng nhiều càng tốt mẫu "sandwich" trong mỗi chương trình con hoặc phương pháp.

create a thing
use that thing
destroy that thing

Đôi khi cần phải tạo và phá hủy ở những nơi khác nhau; Tôi nghĩ rằng khó để tránh điều đó.

Trong bất kỳ chương trình nào yêu cầu cấu trúc dữ liệu phức tạp, tôi tạo một cây đối tượng rõ ràng nghiêm ngặt chứa các đối tượng khác - sử dụng con trỏ "chủ sở hữu". Cây này mô hình phân cấp cơ bản của các khái niệm miền ứng dụng. Ví dụ một cảnh 3D sở hữu các vật thể, ánh sáng, kết cấu. Vào cuối kết xuất khi chương trình thoát, có một cách rõ ràng để phá hủy mọi thứ.

Nhiều con trỏ khác được định nghĩa là cần thiết bất cứ khi nào một thực thể cần truy cập vào một thực thể khác, để quét qua các tia hoặc bất cứ thứ gì; đây là "chỉ nhìn". Đối với ví dụ cảnh 3D - một đối tượng sử dụng kết cấu nhưng không sở hữu; các đối tượng khác có thể sử dụng kết cấu tương tự. Sự phá hủy của một vật thể không gọi đến sự phá hủy của bất kỳ kết cấu nào.

Vâng, nó tốn thời gian nhưng đó là những gì tôi làm. Tôi hiếm khi bị rò rỉ bộ nhớ hoặc các vấn đề khác. Nhưng sau đó tôi làm việc trong lĩnh vực giới hạn của phần mềm đồ họa, thu thập dữ liệu và khoa học hiệu năng cao. Tôi không thường xử lý các giao dịch như trong ngân hàng và thương mại điện tử, GUI hướng sự kiện hoặc hỗn loạn không đồng bộ được nối mạng cao. Có lẽ những cách làm mới có lợi thế ở đó!


Tôi hoàn toàn đồng ý. Làm việc trong một môi trường nhúng, bạn cũng có thể không có sự sang trọng của các thư viện bên thứ ba.
simon

6
Tôi không đồng ý. trong phần "sử dụng thứ đó", nếu ném trả lại hoặc ngoại lệ, thì bạn sẽ bỏ lỡ thỏa thuận. Về hiệu năng, std :: auto_ptr sẽ không mất gì. Không phải là tôi không bao giờ viết mã giống như bạn làm. Chỉ là có sự khác biệt giữa mã bảo mật 100% và 99%. :-)
paercebal

8

Câu hỏi tuyệt vời!

nếu bạn đang sử dụng c ++ và bạn đang phát triển ứng dụng boud CPU và bộ nhớ thời gian thực (như trò chơi), bạn cần phải viết Trình quản lý bộ nhớ của riêng mình.

Tôi nghĩ tốt hơn bạn có thể làm là hợp nhất một số tác phẩm thú vị của các tác giả khác nhau, tôi có thể cung cấp cho bạn một số gợi ý:

  • Phân bổ kích thước cố định được thảo luận nhiều, ở khắp mọi nơi trong mạng

  • Phân bổ đối tượng nhỏ đã được Alexandrescu giới thiệu vào năm 2001 trong cuốn sách hoàn hảo "Modern c ++ design"

  • Một tiến bộ tuyệt vời (với mã nguồn được phân phối) có thể được tìm thấy trong một bài viết tuyệt vời trong Lập trình trò chơi Gem 7 (2008) có tên là "Công cụ phân bổ heap hiệu suất cao" được viết bởi Dimitar Lazarov

  • Một danh sách lớn các nguồn lực có thể được tìm thấy trong này bài viết

Đừng tự mình bắt đầu viết một công cụ phân bổ không đáng tin cậy ... TÀI LIỆU CỦA BẠN trước tiên.


5

Một kỹ thuật đã trở nên phổ biến với quản lý bộ nhớ trong C ++ là RAII . Về cơ bản, bạn sử dụng các hàm tạo / hàm hủy để xử lý phân bổ tài nguyên. Tất nhiên có một số chi tiết đáng ghét khác trong C ++ do an toàn ngoại lệ, nhưng ý tưởng cơ bản là khá đơn giản.

Vấn đề thường đi xuống một trong những quyền sở hữu. Tôi đặc biệt khuyên bạn nên đọc loạt C ++ hiệu quả của Scott Meyers và Modern C ++ Design của Andrei Alexandrescu.



4

Con trỏ thông minh ở mọi nơi bạn có thể! Toàn bộ các lớp rò rỉ bộ nhớ chỉ cần biến mất.


4

Chia sẻ và biết các quy tắc sở hữu bộ nhớ trong dự án của bạn. Sử dụng quy tắc COM làm cho các tham số nhất quán ([in] được sở hữu bởi người gọi, callee phải sao chép; [out] params thuộc sở hữu của người gọi, callee phải tạo một bản sao nếu giữ tham chiếu; v.v.)


4

valgrind là một công cụ tốt để kiểm tra rò rỉ bộ nhớ chương trình của bạn khi chạy.

Nó có sẵn trên hầu hết các hương vị của Linux (bao gồm cả Android) và trên Darwin.

Nếu bạn sử dụng để viết các bài kiểm tra đơn vị cho các chương trình của mình, bạn sẽ có thói quen hệ thống chạy valgrind trong các bài kiểm tra. Nó có khả năng sẽ tránh được nhiều rò rỉ bộ nhớ ở giai đoạn đầu. Nó cũng thường dễ dàng hơn để xác định chúng trong các thử nghiệm đơn giản trong một phần mềm đầy đủ.

Tất nhiên lời khuyên này vẫn hợp lệ cho bất kỳ công cụ kiểm tra bộ nhớ khác.


3

Ngoài ra, không sử dụng bộ nhớ được cấp phát thủ công nếu có lớp thư viện tiêu chuẩn (ví dụ: vectơ). Hãy chắc chắn rằng nếu bạn vi phạm quy tắc đó rằng bạn có một hàm hủy ảo.


2

Nếu bạn không thể / không sử dụng một con trỏ thông minh cho một cái gì đó (mặc dù đó phải là một lá cờ đỏ khổng lồ), hãy nhập mã của bạn bằng:

allocate
if allocation succeeded:
{ //scope)
     deallocate()
}

Điều đó là hiển nhiên, nhưng hãy đảm bảo bạn nhập nó trước khi bạn nhập bất kỳ mã nào trong phạm vi


2

Một nguồn thường xuyên của các lỗi này là khi bạn có một phương thức chấp nhận một tham chiếu hoặc con trỏ tới một đối tượng nhưng không rõ quyền sở hữu. Phong cách và bình luận quy ước có thể làm cho điều này ít có khả năng.

Đặt trường hợp hàm có quyền sở hữu đối tượng là trường hợp đặc biệt. Trong mọi tình huống xảy ra, hãy nhớ viết bình luận bên cạnh chức năng trong tệp tiêu đề cho biết điều này. Bạn nên cố gắng đảm bảo rằng trong hầu hết các trường hợp, mô-đun hoặc lớp phân bổ một đối tượng cũng chịu trách nhiệm giải quyết nó.

Sử dụng const có thể giúp rất nhiều trong một số trường hợp. Nếu một hàm sẽ không sửa đổi một đối tượng và không lưu trữ một tham chiếu đến nó vẫn tồn tại sau khi nó trả về, hãy chấp nhận một tham chiếu const. Từ việc đọc mã của người gọi, rõ ràng là chức năng của bạn không được chấp nhận quyền sở hữu đối tượng. Bạn có thể có cùng chức năng chấp nhận một con trỏ không phải là const và người gọi có thể hoặc không thể cho rằng quyền sở hữu được chấp nhận, nhưng với một tham chiếu const thì không có câu hỏi nào.

Không sử dụng tài liệu tham khảo không const trong danh sách đối số. Rất không rõ ràng khi đọc mã người gọi rằng callee có thể đã giữ một tham chiếu đến tham số.

Tôi không đồng ý với các ý kiến ​​đề xuất tham chiếu con trỏ đếm. Điều này thường hoạt động tốt, nhưng khi bạn gặp lỗi và nó không hoạt động, đặc biệt là nếu công cụ phá hủy của bạn làm một việc gì đó không tầm thường, chẳng hạn như trong một chương trình đa luồng. Chắc chắn cố gắng điều chỉnh thiết kế của bạn để không cần đếm tham chiếu nếu nó không quá khó.


2

Mẹo theo thứ tự quan trọng:

-Tip # 1 Luôn nhớ khai báo các hàm hủy của bạn là "ảo".

-Tip # 2 Sử dụng RAII

-Tip # 3 Sử dụng smartpoint boost

-Tip # 4 Đừng viết Smartpointers lỗi của riêng bạn, hãy sử dụng boost (trên một dự án tôi đang thực hiện ngay bây giờ Tôi không thể sử dụng boost và tôi đã phải gỡ lỗi con trỏ thông minh của riêng mình, tôi chắc chắn sẽ không lấy cùng một lộ trình một lần nữa, nhưng sau đó một lần nữa ngay bây giờ tôi không thể thêm tăng cường cho các phụ thuộc của chúng tôi)

-Tip # 5 Nếu một số chỉ trích thông thường / không hiệu năng (như trong các trò chơi có hàng ngàn đối tượng) hoạt động, hãy nhìn vào hộp chứa con trỏ tăng tốc của Thorsten Ottosen

-Tip # 6 Tìm tiêu đề phát hiện rò rỉ cho nền tảng bạn chọn, chẳng hạn như tiêu đề "vld" của Visual Leak Phát hiện


Tôi có thể đang thiếu một mẹo, nhưng làm thế nào 'trò chơi' và 'không quan trọng về hiệu suất' có thể nằm trong cùng một câu?
Adam Naylor

Trò chơi là một ví dụ về kịch bản quan trọng của khóa học. Có thể đã không rõ ràng ở đó
Robert Gould

Mẹo số 1 chỉ nên được áp dụng nếu lớp có ít nhất một phương thức ảo. Tôi sẽ không bao giờ áp đặt một hàm hủy ảo vô dụng lên một lớp không có nghĩa là làm lớp cơ sở trong cây thừa kế đa hình.
antred

1

Nếu bạn có thể, hãy sử dụng boost shared_ptr và auto_ptr chuẩn C ++. Những người truyền đạt ngữ nghĩa sở hữu.

Khi bạn trả về auto_ptr, bạn đang nói với người gọi rằng bạn đang trao cho họ quyền sở hữu bộ nhớ.

Khi bạn trả lại shared_ptr, bạn đang nói với người gọi rằng bạn có tham chiếu đến nó và họ tham gia quyền sở hữu, nhưng đó không phải là trách nhiệm của họ.

Những ngữ nghĩa này cũng áp dụng cho các tham số. Nếu người gọi chuyển cho bạn auto_ptr, họ sẽ trao quyền sở hữu cho bạn.


1

Những người khác đã đề cập đến các cách để tránh rò rỉ bộ nhớ ở nơi đầu tiên (như con trỏ thông minh). Nhưng một công cụ phân tích cấu hình và bộ nhớ thường là cách duy nhất để theo dõi các vấn đề về bộ nhớ một khi bạn có chúng.

Valgrind memcheck là một miễn phí tuyệt vời.


1

Chỉ dành cho MSVC, thêm phần sau vào đầu mỗi tệp .cpp:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

Sau đó, khi gỡ lỗi với VS2003 trở lên, bạn sẽ được thông báo về bất kỳ rò rỉ nào khi chương trình của bạn thoát (nó theo dõi mới / xóa). Nó cơ bản, nhưng nó đã giúp tôi trong quá khứ.


1

valgrind (chỉ có sẵn cho các nền tảng * nix) là một trình kiểm tra bộ nhớ rất đẹp


1

Nếu bạn định quản lý bộ nhớ của mình theo cách thủ công, bạn có hai trường hợp:

  1. Tôi đã tạo đối tượng (có lẽ là gián tiếp, bằng cách gọi một hàm phân bổ một đối tượng mới), tôi sử dụng nó (hoặc một chức năng mà tôi gọi là sử dụng nó), sau đó tôi giải phóng nó.
  2. Ai đó đã cho tôi tham khảo, vì vậy tôi không nên giải phóng nó.

Nếu bạn cần phá vỡ bất kỳ quy tắc nào trong số này, vui lòng ghi lại nó.

Đó là tất cả về quyền sở hữu con trỏ.


1
  • Cố gắng tránh phân bổ các đối tượng một cách linh hoạt. Miễn là các lớp có các hàm tạo và hàm hủy thích hợp, sử dụng một biến của loại lớp, không phải là một con trỏ tới nó và bạn tránh phân bổ động và phân bổ động vì trình biên dịch sẽ làm điều đó cho bạn.
    Trên thực tế đó cũng là cơ chế được sử dụng bởi "con trỏ thông minh" và được gọi là RAII bởi một số tác giả khác ;-).
  • Khi bạn truyền đối tượng cho các hàm khác, hãy ưu tiên các tham số tham chiếu hơn các con trỏ. Điều này tránh một số lỗi có thể.
  • Khai báo các tham số const, khi có thể, đặc biệt là các con trỏ tới các đối tượng. Bằng cách đó, các đối tượng không thể được giải phóng "một cách tình cờ" (trừ khi bạn bỏ const;;))).
  • Giảm thiểu số lượng địa điểm trong chương trình nơi bạn thực hiện cấp phát và phân bổ bộ nhớ. Ví dụ. nếu bạn phân bổ hoặc miễn phí cùng loại nhiều lần, hãy viết hàm cho nó (hoặc phương thức xuất xưởng ;-)).
    Bằng cách này, bạn có thể tạo đầu ra gỡ lỗi (địa chỉ được phân bổ và giải quyết, ...) một cách dễ dàng, nếu cần.
  • Sử dụng hàm nhà máy để phân bổ các đối tượng của một số lớp liên quan từ một hàm duy nhất.
  • Nếu các lớp của bạn có một lớp cơ sở chung với một hàm hủy ảo, bạn có thể giải phóng tất cả chúng bằng cách sử dụng cùng một hàm (hoặc phương thức tĩnh).
  • Kiểm tra chương trình của bạn với các công cụ như thanh lọc (không may là nhiều $ / € / ...).

0

Bạn có thể chặn các chức năng cấp phát bộ nhớ và xem liệu có một số vùng bộ nhớ không được giải phóng khi thoát khỏi chương trình (mặc dù nó không phù hợp với tất cả các ứng dụng).

Nó cũng có thể được thực hiện tại thời điểm biên dịch bằng cách thay thế các toán tử mới và xóa và các chức năng cấp phát bộ nhớ khác.

Ví dụ: kiểm tra trong trang web này [Cấp phát bộ nhớ gỡ lỗi trong C ++] Lưu ý: Có một mẹo để xóa toán tử cũng giống như thế này:

#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE

Bạn có thể lưu trữ trong một số biến tên của tệp và khi toán tử xóa quá tải sẽ biết đó là nơi nó được gọi từ đâu. Bằng cách này bạn có thể có dấu vết của mỗi lần xóa và malloc từ chương trình của bạn. Vào cuối chuỗi kiểm tra bộ nhớ, bạn sẽ có thể báo cáo khối bộ nhớ được phân bổ không bị 'xóa' xác định nó bằng tên tệp và số dòng mà tôi đoán bạn muốn gì.

Bạn cũng có thể thử một cái gì đó như BoundChecker trong Visual Studio, điều này khá thú vị và dễ sử dụng.


0

Chúng tôi bọc tất cả các chức năng phân bổ của chúng tôi bằng một lớp gắn thêm một chuỗi ngắn ở phía trước và một cờ canh ở cuối. Vì vậy, ví dụ bạn sẽ có một cuộc gọi đến "myalloc (pszSomeString, iSize, iAlocation) hoặc mới (" mô tả ", iSize) MyObject (); trong đó phân bổ nội bộ kích thước đã chỉ định cộng với đủ không gian cho tiêu đề và sentinel của bạn. , đừng quên nhận xét điều này cho các bản dựng không gỡ lỗi! Cần thêm một chút bộ nhớ để làm điều này nhưng lợi ích vượt xa chi phí.

Điều này có ba lợi ích - đầu tiên, nó cho phép bạn dễ dàng và nhanh chóng theo dõi mã nào bị rò rỉ, bằng cách thực hiện tìm kiếm nhanh mã được phân bổ trong một số 'vùng' nhất định nhưng không được dọn sạch khi các vùng đó được giải phóng. Nó cũng có thể hữu ích để phát hiện khi một ranh giới đã được ghi đè bằng cách kiểm tra để đảm bảo tất cả các trọng điểm vẫn còn nguyên vẹn. Điều này đã giúp chúng tôi tiết kiệm rất nhiều lần khi cố gắng tìm ra những sự cố được giấu kỹ hoặc những sai lầm trong mảng. Lợi ích thứ ba là theo dõi việc sử dụng bộ nhớ để xem ai là người chơi lớn - ví dụ, một bản mô tả nhất định trong MemDump cho bạn biết khi nào 'âm thanh' chiếm nhiều không gian hơn bạn dự đoán, chẳng hạn.


0

C ++ được thiết kế RAII trong tâm trí. Tôi thực sự không có cách nào tốt hơn để quản lý bộ nhớ trong C ++. Nhưng hãy cẩn thận không phân bổ các khối rất lớn (như các đối tượng đệm) trên phạm vi cục bộ. Nó có thể gây ra lỗi tràn ngăn xếp và, nếu có một lỗ hổng trong giới hạn kiểm tra trong khi sử dụng đoạn đó, bạn có thể ghi đè lên các biến khác hoặc trả về địa chỉ, dẫn đến tất cả các loại lỗ hổng bảo mật.


0

Một trong những ví dụ duy nhất về việc phân bổ và phá hủy ở những nơi khác nhau là tạo luồng (tham số bạn truyền). Nhưng ngay cả trong trường hợp này là dễ dàng. Đây là chức năng / phương thức tạo ra một chủ đề:

struct myparams {
int x;
std::vector<double> z;
}

std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...

Ở đây thay vì chức năng chủ đề

extern "C" void* th_func(void* p) {
   try {
       std::auto_ptr<myparams> param((myparams*)p);
       ...
   } catch(...) {
   }
   return 0;
}

Khá dễ phải không? Trong trường hợp tạo luồng không thành công, tài nguyên sẽ được tự động (xóa) bởi auto_ptr, nếu không quyền sở hữu sẽ được chuyển cho luồng. Điều gì xảy ra nếu luồng nhanh đến mức sau khi tạo, nó giải phóng tài nguyên trước

param.release();

được gọi trong hàm / phương thức chính? Không có gì! Bởi vì chúng tôi sẽ 'nói' auto_ptr bỏ qua giao dịch. Quản lý bộ nhớ C ++ có dễ không? Chúc mừng

Ema!


0

Quản lý bộ nhớ giống như cách bạn quản lý các tài nguyên khác (xử lý, tệp, kết nối db, ổ cắm ...). GC cũng sẽ không giúp bạn với họ.


-3

Chính xác một trở về từ bất kỳ chức năng. Bằng cách đó bạn có thể thực hiện giao dịch ở đó và không bao giờ bỏ lỡ nó.

Thật quá dễ để phạm sai lầm:

new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.

Câu trả lời của bạn không khớp với mã ví dụ ở đây? Tôi đồng ý với câu trả lời "chỉ một lần trả lại" nhưng mã ví dụ đang hiển thị những gì KHÔNG phải làm.
simon

1
Quan điểm của C ++ RAII chính xác là để tránh loại mã bạn đã viết. Trong C, đây có lẽ là điều đúng đắn. Nhưng trong C ++, mã của bạn bị thiếu sót. Ví dụ: Điều gì xảy ra nếu ném b () mới? Bạn rò rỉ a.
paercebal
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.