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?
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?
Câu trả lời:
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 , TR1 và con 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 .
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;
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.
}
Để tóm tắt (sau nhận xét từ Ogre Psalm33 ), RAII dựa trên ba khái niệm:
Đ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 new
và 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
/ finally
cho trường hợp đó) ...
"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 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 terminate
hoặc sử dụng exit
mã 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 đó) .
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 ...
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:
Đ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 đó.
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ế ở đó!
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.
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.
Đã có rất nhiều về cách không rò rỉ, nhưng nếu bạn cần một công cụ để giúp bạn theo dõi rò rỉ, hãy xem:
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.)
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.
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
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ó.
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
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.
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.
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
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:
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ỏ.
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.
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.
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.
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!
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ọ.
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.