Quản lý bộ nhớ trong C++ – Memory management in C++

1. Tổng quan về quản lý bộ nhớ trong C++

Quản lý bộ nhớ là một chủ đề lớn trong các ngôn ngữ lập trình. C++ cung cấp nhiều lựa chọn cho cách quản lý bộ nhớ (có rất nhiều cách, nhưng chúng ta sẽ tập trung ở phần khởi tạo bộ nhớ).

C++ quan là ngôn ngữ quản lý bộ nhớ đơn giản trong hầu hết các trường hợp. Chúng ta sẽ có những phương pháp cấp cao ( thường được ưa chuộng hơn), và những chi tiết về khía cạnh cấp dưới như sử dụng các câu lệnh new, delete, new[], delete[] , thường ẩn bên dưới các lớp mô hình cấp cao hơn.

2. Bộ thu nhặt rác (Garbage Collection) và RAII.

Bộ gom rác (Garbage collection) được xử lý với việc quản lý bộ nhớ động, với nhiều mức độ tự động khác nhau, các thao tác xây dựng lời gọi bộ thu gom, nỗ lực dọn rác ( bộ nhớ đã được sử dụng bởi các đối tượng ứng dụng sẽ không bao giờ được truy cập hoặc thay đổi lại). Điều này thường được coi là một tính năng quan trọng trong các ngôn ngữ lập trình gần đây, đặc biệt là nếu chúng ngăn chặn việc quản lý bộ nhớ một cách thủ công, điều đó có thể đến một số lỗi nghiêm trọng khi lập trình và do đó đòi hỏi ở người lập trình có một mức độ cao về kinh nghiệm lập trình. Những lỗi trong việc quản lý bộ nhớ hầu hết là do sự xung đột trong thời gian thực thi, làm cho chúng ta khó mà phát hiện chính xác đó là lỗi gì.

C++ hỗ trợ tùy chọn việc thu gom rác và một số bổ sung trong việc thu gom rác ( thông thường là trong lời gọi bộ phận gom rác Boehm) đã tồn tại. Các chuẩn C++ xác định việc thực hiện của ngôn ngữ và nó nhấn mạnh nền tảng mở cho những phần mở rộng, một ví dụ về C++ của Sun là biên dịch sản phẩm không bao gồm libgc library (một bộ gom rác bảo thủ).

Không giống như nhiều ngôn ngữ bậc cao khác, C++ không áp đặc việc sử dụng thu gom rác, và phương thức chủ đạo của C++ cho việc quản lý bộ nhớ là đừng gánh vác việc sử dụng bộ gom rác tự động. Hầu hết các phương pháp thu thập rá phổ biến trong C++ là sử dụng một thuật ngữ có tên là RAII (Resource Acquisition Is Initialization). Ý tưởng chính đằng sau RAII là một nguồn tài nguyên thu được vào thời gian khởi tạo hay không, là thuộc sở hữu của một đối tượng, và đối tượng hủy sẽ tự động giải phóng tài nguyên ở một thời điểm thích hợp. Điều này cho phép C++ thông qua các RAII để hỗ trợ các định việc dọn dẹp các tài nguyên, từ một phương pháp làm việc để giải phóng bộ nhớ nó cũng có thể được dùng để giải phóng các tài nguyên khác (file handles, mutexes, database connections, transactions, …).

Trong trường hợp không thu gom rác thải mặc đinh, RAII là một cách mạnh mẽ để đảm bảo rằng nguồn tài nguyên không bị rò rỉ ngay cả trong những mã có thể gây ra trong trường hợp ngoại lệ được ném ra. Nó được đánh giá là tốt hơn các finally construct trong Java và các ngôn ngữ tương tự; khi một lớp sở hữu một nguồn tại nguyên, Java yêu cầu mỗi người dùng lớp đó phải sử dụng khối lệnh try/finally. Trong C++ lớp cung cấp một hàm hủy, và lập trình viên không cần phải làm bất cứ điều gì ngoại trừ đảm bảo rằng các đối tượng bị hủy khi họ kết thúc nó (thông thường thì không phải làm gì, ví dụ trong trường hợp: đối tượng là một biến cục bộ hay một dữ liệu thành viên của một đối tượng khác).

Đối với các ứng dụng thông thường, các lớp thích hợp đã được viết ra: nhiều trường hợp đơn giản trong việc quản lý bộ nhớ được bao phủ bởi std:: string và std:: vector (cùng với các container chuẩn như: std::map và std::list).

3. Quản lý bộ nhớ so với ngôn ngữ C.

Rất nhiều lập trình viên đến với C++ từ C với việc quản lý bộ nhớ một cách thủ công, đặc biệt là trong việc sử lý chuỗi.

Dưới đây là một so sánh đơn giản giữa một chương trình C và C++ với các chức năng tương tự. Cả 2 ví dụ bỏ qua xử lý lỗi sẽ có mặt trong mã thực.

Code C++

clip_image002

So với Code C

clip_image004

Ta thấy rằng, phiên bản code C++ ngắn hơn và không chứa bất kỳ mã nào rõ ràng để làm việc cấp phát bộ nhớ bao nhiêu, phân phối hoặc bộ nhớ miễn phí, không cần biết các chi tiết thực hiện của ‘getstr()’, tất cả những điều đó được xử lý bởi lớp tiêu chuẩn xử lý chuỗi <string>.

4. Con trỏ (smart pointers) trong việc quản lý bộ nhớ

Trong khi con trỏ có nhiều ứng dụng trong C++ hơn là quản lý bộ nhớ đơn giản, chúng thường được sử dụng để quản lý thời gian sống của các đối tượng khác cấp phát động.

Một loại con trỏ được định nghĩa là bất kỳ loại lớp đa năng operator->, operator*, hay operator->* . Một điều cần lưu ý ngay là “con trỏ” là, trong ý thức, con trỏ không thực sự ở tất cả – nhưng đa năng hóa toán tử này cho phép một con trỏ có thông minh có thể hành xử giống như xây dựng trong con trỏ, và nhiều mã lệnh có thể được viết đó làm việc với cả 2 “real” con trỏ (pointers) và con trỏ thông minh ( smart pointers).

4.1 Std:: auto_ptr

Chỉ các loại con trỏ trong tiêu chuẩn C++ 2003 là std::auto_ptr. Trong khi điều này đã được sử dụng nhất quán, không phải tất cả các thiết kế con trỏ đều khéo léo và có khả năng trong công việc.

Khả năng cung cấp:

– Mô phỏng thời gian tồn tại của một biến cục bộ hoặc biến thành viên cho một đối tượng đó thực sự là cấp phát động.

– Cung cấp một cơ chế cho việc “transfer of ownership – chuyển quyền sở hữu” của các đối tượng từ một chủ sở hữu khác.

Ví dụ đơn giản cho auto_ptr

clip_image005

Toán tử = trong auto_ptr làm việc theo một cách thông thường khác nhau. Cái nó làm là chuyển quyền sở hữu từ rhs (right hand side – phía bên phải) auto_ptr đến lhs (left hand side – phía bên tría) auto_ptr. Con trỏ rhs sẽ trỏ đến giá trị NULL;

Ví dụ:

clip_image006

Output:

clip_image008

Hành vi này rất hữu dụng khi nó được yêu cầu chỉ một con trỏ, trỏ đến một đối tượng cụ thể, nhưng con trỏ đó trỏ đến nó sẽ bị thay đổi. Nếu hành vi khác là yêu cầu, sủng dụng một trong các con bổ sung (boost pointers) là một lựa chọn tốt hơn.

· Lưu ý: Cách chuyển quyền sở hữu auto_ptr ít được sử dụng, và không được chấp thuận rộng rãi, nhưng nếu thực sự chú ý, nó có thể được sử dụng một cách hiệu quả.

4.2 Các con trỏ tăng cường (Boost Smart Pointes)

Thư viện tăng cường của C++ bao gồm 5 loại khác nhau của con trỏ thông minh, cùng với các std::autoptr, có thể được sử dụng trong hầu hết các tình huống quản lý bộ nhớ. Ngoài ra, một số con trỏ thông minh sẽ tăng lên ở các thư viện chuẩn của C++0x sửa đổi C++, khi nó được phát hành.

clip_image010

Bảng mô tả về sự tăng cường thư viện smart pointers.

4.3 Tạo kiểu con trỏ riêng (Creating your own smart pointer type)

Việc sử dụng con trỏ thường dẫn đến rò rỉ bộ nhớ nếu không thực sự cẩn thận. Để tránh điều này, chúng ta nên quản lý thủ công bộ nhớ heap-base. Vì vậy chúng ta phải tìm một container có thể tự động trả lại bộ nhớ trở lại hệ thống hoạt động khi chúng ta không sử dụng nó. Các hàm hủy của lớp có thể phù hợp với yêu cầu này.

Những gì chúng ta cần lưu trữ trong một con trỏ cơ bản là địa chỉ của bộ nhớ được cấp phát. Trong trường hợp này, chúng ta đơn giản là có thể sử dụng một con trỏ. Hãy nói rằng cúng tôi đang thiết kế một phần lưu trữ của bộ nhớ cho một con trỏ kiểu int.

clip_image012

Để đảm bảo rằng mỗi người sử dụng đặt một địa chỉ trong con trỏ khi thực hiện khởi tạo, chúng ta phải xác định hàm để chấp nhận một khai báo của con trỏ với các địa chỉ mục tiêu là như tham số, nhưng không phải “ mere declaration – khai báo gới hạn” của các con trỏ chính nó.

clip_image014

Bây giờ, chúng ta phải xác định lớp để “delete” các con trỏ khi các cá thể con trỏ này hủy.

clip_image016

Chúng ta có thể cho phép người dùng truy cập các dữ liệu được lưu trong con trỏ và làm cho nó hơn “pointer-like”. Trong trường hợp này, chúng ta có thể thêm một hàm để cung cấp truy cập vào các con trỏ thô, và đa năng hóa một số toán tử như: operator*operator->, để làm cho nó hành xử giống như một con trỏ thực.

clip_image018

Trên thực tế, chúng ta đã hoàn thành cơ bản và nó sẵn sàng để sử dụng, tuy nhiên, để thực hiện điều này “ homemade – tự chế” con trỏ làm việc với các dữ liệu khác loại và các lớp, chúng ta phải biến nó thành một lớp mẫu.

clip_image020

Điều này là một điều rất cơ bản và chỉ cung cấp tính năng cơ bản, và có thể nhiều vấn đề nghiêm trọng, chăng hạn như sao chép con trỏ này sẽ dẫn đến xóa 2 lần…

4.4 Những con trỏ khác (other smart pointers)

Ngoài auto_ptr, có rất nhiều con trỏ khác để trang trải các nhiệm vụ từ gói đối tượng COM, cung cấp tự động sự đồng bộ hóa cho việc truy cập đa luồng, hoặc cung cấp các giải pháp bộ nhớ cho các database interface.

5. Quản lý bộ nhớ thủ công với new, delete, …

Code trong C++ hiện đại có xu hướng sử dụng new khá hiếm, và delete thì rất hiếm khi. Từ quan điểm bộ nhớ, điều bất lợi của new là việc cấp phát bộ nhớ ra khỏi heap cho tới khi các đối tượng cục bộ được cấp phát bộ nhớ ra khỏi stack. Thời gian phân bổ của Heap chậm hơn thời gian phân bổ ra khỏi stack. Tuy nhiên, vẫn có khi làm như vậy thì thích hợp hơn, và một sự hiểu biết vững chắc về cách các phương tiện cấp thấp làm việc có thể giúp đỡ với sự hiều biết về những gì thường xảy ra “below the hood – bên dưới hệ thống”. Có nhiều lần khi mà newdelete quá cao cấp, và chúng ta phải trở lại với malloc free – nhưng những tình huống mà những ngoại lệ thực sự hiếm.

Ý tưởng cơ bản của new delete rất đơn giản: new tạo ra một đối tuwongjcuar một kiểu nhât định và đưa ra một con trỏ tới nó, và delete phá hủy một đối tượng được tạo ra bởi new, được đua ra một con trỏ đến nó. Lý do mà new delete tồn tại trong ngôn ngữ này là vì code thưởng không biết khi nờ nó được biên dịch mà các đối tượng sẽ cần phải tạo ra ở thời điểm thực thi, hoặc có bao nhiêu trong chúng. Như vậy new delete các biểu thức cho phép “dynamic – năng động” phân bổ của các đối tượng.

Ví dụ:

clip_image022

Đối với những người quen thuộc ngôn ngữ lập trình C, new được hiểu là một phiên bản của malloc: các loại biểu thức “new int” is “int*”. Do đó, C++ mà một cast sẽ là cần thiết để viết int * p = reinterpret_cast<int*> (malloc(sizeof*p)); cast không được yêu cầu khi được sử dụng new. Bởi vì new là kiểu nhận thức mới, nó cũng có thể khởi tạo các đối tượng vừa được tạo ra, gọi hàm khởi tạo nếu thích hợp. Ví dụ trên sử dụng khả năng này để khởi tạo int tạ ra để có giá trị 3. Một nâng cao mới của new delete so với malloc free là C++ tiêu chuẩn cung cấp một cách tiêu chuẩn để thay đổi cách cấp phát bộ nhớ cho new delete, trong C điều này thường đạt được bằng cách sử dụng một kỹ thuật không chuẩn được gọi là “interpositioning”.

Các toán tử cơ bản newdelete chỉ cấp phát cho một đối tượng duy nhất tại một thời điểm, chúng không được bổ sung bởi new[]delete[] để tự động cấp phát toàn bộ mảng. Sử dụng new[]delete[] thậm chí còn hiếm hơn so với sử dụng các toán tử cở sở new delete ; thường là std:: vector là một cách thuận tiện hơn để quản lý môt mảng cấp phát tự động.

– Lưu ý rằng, khi bạn tự động cấp phát một mảng các đối tượng, bạn phải viết delete[] khi giải phóng nó, không phải đơn giản là xóa. Trình biên dịch có thể không thường xuyên báo 1 lỗi, nếu bạn thực hiện sai điều này. Có khả năng code của bạn sẽ sụp đổ khi bạn chạy nó.

– Khi gọi delete[] chạy, đầu tiên nó lấy thông tin được lưu trữ bởi new[] mô tả về những yếu tố hiện diện trong việc cấp phát mảng động, và sau lời gọi hủy đối với mỗi phần tử trước khi thu hồi bộ nhớ. Các địa chỉ thực tế của vùng nhớ đã được cấp có thể khác với giá trị trả về vởi new[] để cho phép các vùng lưu trữ các yếu tố số; đây là một lý do ngẫu nhiên trộn lẫn hình thức của mảng new[] với các phần tử đơn của delete[] có thể dẫn đến treo máy.

– Cách lựu chọn thông minh của độc giả là có cần thiết phải nhớ từng câu lệnh new/ new[] và delete/delete[] đã sử dụng, và làm cho trình biên dịch có thể thay đổi ở đầu ra. Câu trả lời là hoàn toàn có thể, nhưng mà làm như vậy có thể sẽ phải tăng thêm chi phí cho mỗi đối tượng đơn cấp phát (như delete sẽ cần để làm việc cấp phát cho một đối tượng đơn hay một mảng) và một nguyên tắt thiết kế phía sau C++ khuyên bạ rằng “ don’t pay for what you don’t use – đừng trả tiền cho những thứ mà bạn không dùng”, do đó cần tạo ra sự cân bằng cho các đối tượng đơn được cấp phát hiệu quả, nhưng người dùng phải cẩn thận khi sử dụng các phương tiện cấp thấp.

6. Những lỗi thường gặp (Common mistakes)

Sử dụng typerdef

Hãy đọc đoạn code (bị lỗi) dưới đây:

clip_image024

Đoạn mã trên sẽ dẫn đến rò rỉ tài nguyên (hoặc trong một số trường hợp va chạm). Đó là một lỗi phổ biến để cấp phát một phần của mảng bộ nhớ với delete, nhưng không phải là “array delete” (tức là delete[]). Trong trường hợp này, typerdef gây ra ảo giác rằng “a_string” là một con trỏ trỏ đến một phần của bộ nhớ đủ cho một biến “char” , nhưng không phải là một phần của bộ nhớ. Do thực hiện sai delete, khác hơn là delete[], chỉ cấp phát bộ nhớ cho các phần tử đầu tiên của mảng được giải phóng, và cho phép bộ nhớ cho 99 “char” bị rò rỉ tài nguyên. Chỉ có 99 bytes bị rò rỉ trong trường hợp này, nhưng khi mảng được tổ chức phức tạp với rất nhiều thành viên dữ liệu không tĩnh, bộ nhớ sẽ rò rỉ tới hàng MB. Ngoài ra, với cùng một chương trình có chứa lỗi này chạy lại, một phần bộ nhớ khác sẽ bị rò rỉ.

Như vậy, trong đoạn mã trên:

clip_image026

Cần được sử thành:

clip_image028

Hoặc tốt hơn nữa, một lớp string như std::string nên được sử dụng thay vì một mảng ẩn sau typerdef.

7. Tài liệu tham khảo.

– C++ và lập trình hướng đối tượng – GS. Phạm Văn Ất – Nxb: Khoa học và kỹ thuật Hà Nội 1999.

– Memory Management: Algorithms and Implementations In C/C++ – Bill Bullunden.

– Wikibook:

o http://en.wikibooks.org/wiki/C++_Programming/Memory_Management

Advertisements

About thanhcuong1990

Handsome and talent!! ^^
This entry was posted in C++. Bookmark the permalink.

2 Responses to Quản lý bộ nhớ trong C++ – Memory management in C++

  1. homeqc says:

    bài viết rất hay.Thanks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s