Phụ lục cho bài viết về auto_ptr: Tìm hiểu thêm về lệnh delete
Nhân tiện bài viết trước về auto_ptr, chúng ta ngó qua một chút về lệnh delete thông qua một số Q&A.
Nguồn: http://www.parashift.com/c++-faq-lite/freestore-mgmt.html#faq-16.11
Q: Lệnh delete p xóa con trỏ p hay xóa vùng nhớ trỏ đến bởi p?
A: Vùng nhớ được trỏ đến bởi p.
Khi viết delete p có nghĩa là delete_the_thing_pointed_to_by p. Chúng ta có tình huống tương tự khi giải phóng bộ nhớ bởi lệnh free. free( p ) thực chất là free_the_stuff_pointed_to_by( p ).
Q: Có an toàn không nếu gọi delete hai lần cho cùng một con trỏ?
A: Không, nếu không có lệnh new nào cho con trỏ đó xen vào giữa.
Đoạn mã sau có thể gây ra thảm họa
class Foo { ... };
void yourCode()
{
Foo* p = new Foo();
delete p;
delete p; //← thảm họa!
...
}
Lệnh delete thứ hai có thể gây ra những thảm họa như làm hỏng vùng nhớ heap, làm đổ vỡ chương trình, làm thay đổi một cách tùy ý các object đang tồn tại trong heap. Thật không may, những hậu quả này xảy ra một cách khá ngẫu nhiên. Một số môi trường chạy (runtime environment) có thể giúp bạn tránh được những hậu quả của việc delete hai lần trong một số trường hợp đơn giản. Tuy nhiên, delete một con trỏ hai lần vẫn là một việc làm tồi tệ.
Q: Có cần kiểm tra con trỏ NULL trước khi delete hay không?
A: Không
C++ đảm bảo rằng lệnh delete p sẽ không làm gì cả nếu p là một con trỏ NULL. Bởi vậy, lệnh if trong đoạn mã sau đây là thừa
if( p != NULL )
delete p;
Q: Điều gì thật sự xảy ra khi viết delete p?
A: delete p thực hiện hai việc: Gọi destructor của đối tượng được trỏ tới bởi p và giải phóng vùng nhớ của đối tượng đó. delete p có chức năng tương tự như đoạn mã sau đây, với p là con trỏ kiểu Fred*
Lớp auto_ptr trong C++
Nguồn: Tổng hợp từ Exceptional C++ của Herb Sutter và The C++ Standard Library của Nicolai M. Josuttis
auto_ptr là gì?
Các hàm trong các ngôn ngữ lập trình thường hoạt động theo quy trình sau đây
1- Cấp phát tài nguyên
2- Thực hiện các xử lí
3- Giải phóng tài nguyên
Nếu tài nguyên được cấp phát thông qua các đối tượng cục bộ, chúng sẽ được tự động giải phóng khi kết thúc hàm. Ngược lại, khi tài nguyên được cấp phát một cách tường minh và không gắn với một đối tượng nào, chúng phải được giải phóng một cách tường minh. Tài nguyên thường được cấp phát và giải phóng một cách tường minh thông qua các con trỏ. Cách sử dụng con trỏ phổ biến trong C++ là sử dụng toán tử new và delete như sau:
//Ví dụ 1(a): Đoạn mã không sử dụng auto_ptr
//
void f()
{
T* pt( new T ); //Cấp phát tài nguyên một cách tường minh
/*... các xử lí ...*/
delete pt; //Giải phóng vùng nhớ pt trỏ tới một cách tường minh
} //Kết thúc hàm, biến cục bộ pt được hủy một cách tự động
Hàm f() mang trong mình một lỗi tiềm ẩn: Người lập trình có thể quên không viết câu lệnh delete. Kể cả trong trường hợp có lệnh delete, nếu có một lệnh return được viết trước đó hoặc xảy ra một exception thì hàm f sẽ thoát ngay lập tức mà không thực hiện lệnh delete. Các trường hợp này nếu xảy ra đều dẫn đến lỗi memory leak. Giải pháp thông thường là chúng phải bắt tất cả các exception có thể xảy ra.
void f()
{
T* pt( new T ); //Cấp phát tài nguyên một cách tường minh
try{
/* ...Các xử lí, có thể xảy ra exception...*/
} catch( ... ) { //Với mọi exception xảy ra:
delete pt; //-giải phóng vùng nhớ pt trỏ tới
throw; //-ném ra exception
}
delete pt; //Không xảy ra exception, giải phóng vùng nhớ pt trỏ tới
} //Kết thúc hàm, biến cục bộ pt được hủy một cách tự động
Giải pháp này sẽ trở nên phức tạp khi có nhiều tài nguyên được cấp phát một cách tường minh. Chúng ta cần một con trỏ “thông minh” (smart pointer) có khả năng tự giải phóng vùng nhớ mà nó đang trỏ đến bất cứ khi nào bản thân con trỏ đó bị hủy. Con trỏ cũng là một biến cục bộ nên nó sẽ bị hủy một cách tự động khi hàm thoát ra bất kể theo cách bình thường hay bất thường. Bởi vậy, khi kết thúc hàm hoặc ra khỏi phạm vi (scope) của con trỏ thông minh, vùng nhớ được cấp phát động trước đó sẽ tự động được giải phóng mà không cần đến câu lệnh delete nữa. Lớp auto_ptr ra đời nhằm đáp ứng nhu cầu này.
Một auto_ptr đóng vai trò như chủ sở hữu (owner) của một đối tượng được cấp phát động. Bất cứ khi nào một auto_ptr bị hủy, đối tượng mà nó đang sở hữu cũng bị hủy theo. Mỗi đối tượng chỉ được sở hữu bởi duy nhất bởi một auto_ptr. Đoạn mã trở nên đơn giản hơn nhiều nhờ sử dụng auto_ptr như sau: (more…)
Từ khóa typename trong C++
So với các từ khóa phổ biến của C++ mà chúng ta hay gặp thì typename là một từ khóa mới, ra đời cùng với sự ra đời của khái niệm template. Mục đích của typename là để thông báo rằng tên (identifier) được viết ngay sau từ khóa là một tên kiểu (type). Cụ thể, typename được sử dụng trong hai trường hợp sau đây
1 – Từ khóa typename có thể được sử dụng thay cho từ khóa class trong các định nghĩa template:
Ví dụ, thay vì viết:
template < class T >
class MyClass {...};
Chúng ta có thể viết
template < typename T >
class MyClass {...};
Hai từ khóa typename và class được sử dụng đồng thời trong trường hợp này là do vấn đề lịch sử. Khi mới bắt tay vào viết đặc tả cho các template, ngài Stroustrup đã quyết định tái sử dụng từ khóa class thay vì giới thiệu một từ khóa mới. Một số người thích sử dụng typename hơn vì nó có vẻ mang tính đại diện hơn so với class (kiểu T không chỉ là class mà còn có thể là các kiểu dữ liệu cơ bản nữa). Trái lại, một số người thích dùng class hơn vì sẽ tốn ít thời gian gõ phím hơn
(để gõ từ typename chúng ta phải di chuyển ngón tay khắp bàn phím!). Một số khác thì thích sử dụng class hơn bởi họ muốn để dành typename cho trường hợp hai dưới đây. (more…)
Những hàm được viết và gọi “thầm lặng”
Tham khảo: Item 45 – Effective C++ by Scott Meyers
Để lập trình C++ một cách hiệu quả chúng ta nên hiểu compiler (trình biên dịch) đã làm những gì, biên dịch thế nào cho chúng ta.
Điều gì đã xảy ra nếu chúng ta khai báo một class thế này:
class Empty{};
Lớp đó có thực sự là chẳng có gì không? Thực ra có một số member function mà trình biên dịch “quá thông minh” đã thêm vào class đó rồi. Nghĩa là vì chúng ta không khai báo copy constructor, assignment operator, destructor, và một cặp toán tử lấy địa chỉ (address-of operators) thì trình biên dịch đã tự động thêm những hàm mặc định cho chúng ta rồi. Tất cả những hàm này đều là public. Nói một cách khác thì lớp trên sẽ giống hệt khi chúng ta viết như sau:
class Empty
{
public:
Empty(); // default constructor
Empty(const Empty& rhs); // copy constructor
~Empty(); // destructor - it's nonvirtual
Empty& operator=(const Empty& rhs); // assignment operator
Empty* operator&(); // address-of operators
const Empty* operator&() const;
};
Những hàm được viết và gọi “thầm lặng”
Tham khảo: Item 45 – Effective C++ by Scott Meyers
Để lập trình C++ một cách hiệu quả chúng ta nên hiểu compiler (trình biên dịch) đã làm những gì, biên dịch thế nào cho chúng ta.
Điều gì đã xảy ra nếu chúng ta khai báo một class thế này:
class Empty{};
Lớp đó có thực sự là chẳng có gì không? Thực ra có một số member function mà trình biên dịch “quá thông minh” đã thêm vào class đó rồi. Nghĩa là vì chúng ta không khai báo copy constructor, assignment operator, destructor, và một cặp toán tử lấy địa chỉ (address-of operators) thì trình biên dịch đã tự động thêm những hàm mặc định cho chúng ta rồi. Tất cả những hàm này đều là public. Nói một cách khác thì lớp trên sẽ giống hệt khi chúng ta viết như sau:
class Empty
{
public:
Empty(); // default constructor
Empty(const Empty& rhs); // copy constructor
~Empty(); // destructor - it's nonvirtual
Empty& operator=(const Empty& rhs); // assignment operator
Empty* operator&(); // address-of operators
const Empty* operator&() const;
};
Phân biệt pointer và reference
(Nguồn: Item 1 – More Effective C++ – Scott Meyers)
Một câu hỏi interview khá phổ biến trong C++ là phân biệt giữa pointer và reference. Khi nào thì sử dụng pointer và khi nào thì sử dụng reference?. Pointer và reference thoạt nhìn thì thấy nó khác nhau nhưng dường như chúng đều làm những công việc giống nhau. Chúng đều cho phép chúng ta truy nhập gián tiếp vào một đối tượng khác.
Điểm đầu tiên có thể nhận ra rằng không có một cái nào được gọi là null reference (trong khi có null pointer). Có nghĩa là một reference phải luôn luôn refer đến một object nào đó. Do đó, nếu bạn có một biến mà mục đích của nó là refer đến một object nào đó nhưng nó có thể refer đến không object nào cả, thì bạn nên sử dụng pointer bởi vì sau đó bạn có thể cho giá trị của nó là null. Mặt khác, nếu biến đó phải luôn luôn refer đến một object (design không cho phép có khả năng biến đó là null) thì bạn nên để biến đó là reference.
Thế nhưng, trường hợp này thì sao
char *pc = 0; // set pointer to null char& rc = *pc; // make reference refer to dereferenced null pointer
Declare a copy constructor and an assignment operator for classes with dynamically allocated memory
Source: Item 11 – Effective C++ – Scott Meyers
Con trỏ (pointer) trong C/C++ là một khái niệm vô cùng mạnh mẽ. Tuy nhiên nếu sử dụng chúng không cẩn thận thì chương trình sẽ phải trả giá rất đắt. Nó có thể gây crash và làm sụp đổ hệ thống. Trong bài viết này chúng ta sẽ xem xét khi chúng ta cần thể hiện một class mà có cấp phát bộ nhớ động (sử dụng pointer) thì cần chú ý những gì. Ví dụ chúng ta cần thể hiện một lớp String chẳng hạn.
// a poorly designed String class
class String
{
public:
String(const char *value);
~String();
... // no copy copy constructor or operator=
private:
char *data;
};
String::String(const char *value)
{
if (value) {
data = new char[strlen(value) + 1];
strcpy(data, value);
}
else {
data = new char[1];
*data = '';
}
}
inline String::~String() { delete [] data; }
C++ vs. Java: Mutability
Nguồn: http://mannu.livejournal.com/131085.html
Bài viết này là một thảo luận nhỏ được đề cập đến trong bài báo của Tahir rằng tại sao anh ta thích C++ hơn Java: “Why I Program in C++” (mà tôi sẽ đề cập khi có dịp).
Java thiếu rất nhiều đặc điểm của C++ và ngược lại. Nhưng có một đặc điểm của C++ mà tôi vẫn chưa quen được trong Java là khả năng có một const reference tới objects, làm cho những object là immutable khi sử dụng những references như vậy. Trong Java thì không phải tất cả instances (thể hiện) của một class nào là immutable (ví dụ java.lang.String) hoặc bất cứ một thể hiện nào là immutable. Mutability của một object được xác đính bởi design (thiết kế) của class đó và không có cách nào để nó chịu ảnh hưởng bởi cái reference đến trong hệ thống. Do đó tất cả đối tượng “String” trong Java là immutable, trong khi tất cả ArrayLists là mutable.
Để rõ hơn, sau đây là một lớp contact trong C++: (more…)
C++’s mutable and conceptual constness
Bài viết dưới đây được dịch từ http://www.highprogrammer.com/alan/rants/mutable.html – một tham luận sâu sắc giúp chúng ta hiểu chính xác về mutable.
Ở đầu bài viết tác giả đưa ra một “suy nghĩ” sai của một site nổi tiếng about.com
Trích từ about.com
“Từ khóa mutable được sử dụng để cho phép một data member nào đó của một const object có thể thay đổi được. Nó đặc biệt hữu ích nếu hầu hết member là constant nhưng một số ít có thể được thay đổi. Giả sử chúng ta thêm một “salary” member vào lớp Employee của chúng ta. Khi mà tên và ID có thể là const thì salary không nên là const. Và đây là lớp Employee: (more…)
[C++] Const and mutable members
Chúng ta sẽ bắt đầu bằng cách lược qua lại một vài khái niệm cơ bản của C++.
1. Const member function
class Date
{
int d, m, y;
public:
int day() const { return d; }
int month() const { return m; }
int year() const { return y; }
// …
};
Những member function của class này là const. Điều đó có nghĩa là những function đó không được thay đổi lớp đó. Ví dụ trong hàm year() bạn làm như sau sẽ gây ra lỗi biên dịch
inline int year() const
{
return y++; // error: attempt to change member value in const function
}
Một const member function có thể được thực hiện bởi cả non-const hay const object, trong khi một non-const member function chỉ có thể thực hiện bởi non-const object.
void f( Date &d, const Date& cd )
{
int i = d.year(); // ok
d.add_year(1); // ok
int j = cd.year(); // ok
cd.add_year(1); // error: can not change value of const cd
}
1 comment