[Java] Pass-by-value or Pass-by-reference?
Một trong những điểm khó khăn (và gây tranh cãi) cho những lập trình viên C++ khi lập trình Java là việc pass-by-value hay pass-by-reference. Ví dụ:
Object a = new Object("Object A"); //kỳ quặc với C++: Object* a = new ...
Object b = a;
b.setNewName("Object B");
Theo cách hiểu của C++ devs thì rõ ràng Object b thay đổi tên chẳng liên quan gì đến Object a cả. Thực tế theo Java thì khác, Object a sẽ có name là “Object B” chẳng khác gì Object b cả.
Để Object b hoàn toàn “độc lập” với Object a thì:
Object b = a.clone();
Thế nhưng hàm swap() trong Java lại chẳng hoạt động theo cách pass-by-reference.
private void swap(Type arg1, Type arg2) {
Type temp = arg1;
arg1 = arg2;
arg2 = temp;
}
Vậy đấy!!! Java thực sự đã làm như thế nào? Ẩn sau những cái lằng nhằng gây tranh cãi như vậy là gì?
Function Pointer
Bài viết về function pointer được trích dịch từ tài liệu này: http://www.newty.de/fpt/index.html
1 Giới thiệu
Function Pointer cung cấp một kỹ thuật lập trình cực kỳ thú vị, hiệu quả và “đầy màu sắc”. Chúng ta có thể sử dụng nó để thay thế câu lệnh switch/if, xây dựng quá trình late-binding hoặc implement hàm callback. Tiếc thay, có thể vì sự phức tạp của nó mà nó được đề cập rất ít trong hầu hết sách và tài liệu. Nếu có thì nó chỉ được trình bày một cách rất tóm tắt và sơ sài. Thực ra thì nó ít gây ra lỗi hơn so với pointer bình thường bởi vì chúng ta không bao giờ phải allocate hoặc de-allocate bộ nhớ cả. Tất cả việc chúng ta cần làm là hiểu nó làm gì và học cú pháp của nó. Nhưng hãy luôn tâm niệm rằng: hãy tự hỏi bạn có thực sự cần đến function pointer hay không? Rất tuyệt để thể hiện cách thức late-binding, thế nhưng sử dụng cấu trúc hiện tại của C++ làm cho đoạn mã trở nên dễ đọc và rõ ràng hơn. Một khía cạnh khác của late-binding là runtime: nếu bạn gọi một virtual function, chương trình sẽ xác định hàm nào được gọi. Nó làm điều đó bằng cách sử dụng V-Table mà chứa tất cả những hàm có thể gọi. Điều đó có vẻ hơi lãng phí mỗi lần gọi, và có thể bạn sẽ tiết kiệm một chút nếu sử dụng function pointer thay vì virtual function. Cũng có thể không …
1.1 Function Pointer là gì?
Function pointer là một pointer mà nó chỉ đến địa chỉ của một hàm. Bạn phải luôn giữ trong đầu rằng một chương trình chạy sẽ chiếm một không gian bộ nhớ xác định trong bộ nhớ chính. Cả đoạn chương trình thực thi đã được dịch từ mã mà bạn viết và các biến sử dụng đều được đưa vào trong không gian bộ nhớ này. Vì vậy một function trong chương trình của bạn không có gì khác hơn là một địa chỉ trong bộ nhớ.
1.2 Thay thế câu lệnh Switch như thế nào?
Khi chúng ta muốn gọi một hàm DoIt() ở một label xác định trong chương trình, chúng ta phải để lời gọi tới hàm DoIt() tại label đó. Sau đó biên dịch và mỗi khi chương trình chạy tới label đó thì hàm DoIt() sẽ được gọi. Mọi thứ đều ok, nhưng sẽ làm gì nếu giả sử chúng ta không biết tại thời điểm build-time (thời gian dịch) hàm nào sẽ được gọi? Nghĩa là chỉ đến lúc chạy ta mới biết ở label đó thì nên chạy DoIt() hay một hàm nào khác. Đó chính là lúc chúng ta muốn sử dụng đến callback-function hoặc là sử dụng kỹ thuật lấy ra từ một “pool” chứa các possible function. Tuy nhiên thì chúng ta có thể giải quyết vấn đề này bằng cách sử dụng lệnh switch, và lựa chọn lời gọi đến hàm thích hợp ở những nhánh khác nhau tùy theo giá trị biểu thức của switch. Nhưng vẫn có một cách khác là sử dụng function pointer. Trong ví dụ sau đây chúng ta thực hiện nhiệm vụ của bốn toán tử toán học cơ bản (+, -, *, /). Cách đầu tiên sử dụng switch và cách thứ hai sử dụng function pointer.
STL Function Object và các ứng dụng (1)
Function Object là gì?
Function object là một object được sử dụng như một function. Với một function object của lớp Foo, khi viết Foo() nghĩa là chúng ta đang gọi đến operator() của lớp Foo. Viết một function object nghĩa là viết operator() cho một lớp. Chúng ta đã biết operator của một lớp được viết như sau
class Foo
{
public:
return_type operator() ( parameter list ) {
statements;
}
/* Các public member khác */
private:
/* Các private member */
};
Cài đặt cụ thể cho operator() tùy thuộc vào ngữ cảnh sử dụng của function object. Qua cái nhìn đầu tiên, chúng ta thấy rằng cách viết này chính là sự phức tạp hóa của một hàm bình thường sau đây
return_type foo( parameter list ) {
statements;
}
Sự phức tạp hóa này mang lại ba lợi ích
1- Các function object là các object, bởi vậy chúng có trạng thái, còn các hàm bình thường thì không.
2- Các function object thuộc về một lớp nào đó. Bởi vậy, chúng ta có thể tham số hóa các kiểu dữ liệu bên trong function object thông qua template.
3- Các function object thường chạy nhanh hơn các hàm thông thường.
Không nên mất thời gian suy nghĩ về ba lợi ích này làm gì! Hãy nghiên cứu các ứng dụng của function object, chúng ta sẽ dễ dàng hiểu được những lợi ích của chúng.
Ứng dụng của function object
Các function object được sử dụng trong hai trường hợp sau đây
1- Làm tiêu chí sắp xếp cho các container
2- Làm tham số cho các STL algorithm
Việc một function object được sử dụng ở đâu sẽ quyết định cách viết operator() của lớp đó.
Function Object làm tiêu chí sắp xếp cho các container
Trong phần này, chúng ta sẽ xem xét các ứng dụng của function object trong việc tạo ra các tiêu chí sắp xếp cho các STL set. Các ví dụ này có thể mở rộng cho các STL associative container khác như multiset, map, multimap.
Khi chúng ta đưa các phần tử vào một set, chúng sẽ được sắp xếp sao cho hai phần tử liên tiếp phải thỏa mãn tiêu chí sắp xếp dành cho set đó. Nếu các phần tử của set là các kiểu cơ bản như int hay string, chúng ta có thể sử dụng các tiêu chí sắp xếp sẵn có như greater hay less. Ví dụ dòng khai báo dưới đây
std::set< std::string, greater > strSet;
khai báo một set với các phần tử là các STL string được sắp xếp theo thứ tự tăng dần. Tuy nhiên, nếu các phần tử cần đưa vào set có kiểu do người dùng định nghĩa, ví dụ là các đối tượng của một lớp, thì làm sao để xác định thứ tự của chúng trong set? Có hai cách thực hiện: Một là vẫn sử dụng các tiêu chí sẵn có là less và greater. Tuy nhiên, cách này chỉ thực hiện được nếu lớp đã định nghĩa sẵn operator < (cho tiêu chí less) hoặc operator > (cho tiêu chí greater). Không phải lớp nào cũng cung cấp sẵn các operator này, mà không phải lúc nào chúng ta cũng có quyền “nhảy” vào để thêm mã cho lớp, mà giả sử chúng ta có quyền đi nữa thì cũng không nên làm phức tạp hóa một lớp sẵn có. Cách thứ hai là chúng ta tự định nghĩa một tiêu chí sắp xếp mới, đây chính là lúc cần đến function object. Xem ví dụ sau đây: Giả sử chúng ta cần lưu các đối tượng của lớp Person vào một set. Định nghĩa của lớp Person như sau:
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…)
Programming Style (2)
Các idiom
Không biết dịch từ “idiom” thế nào cho chính xác. Tạm hiểu idiom là các chuẩn không bắt buộc nhưng được đa số người dùng tuân theo. Sử dụng các idiom giúp giảm bớt khả năng mắc lỗi đồng thời làm chương trình dễ đọc hơn và nhất là có vẻ “chuyên nghiệp” hơn
Sau đây là một số idiom phổ biến:
Các idiom cho mảng
Để duyệt qua n phần tử của một mảng và khởi tạo chúng, có các cách viết sau đây:
i = 0;
while ( i <= n – 1 )
array[ i++ ] = 1.0;
hoặc
for( i = 0; i < n; )
array[ i++ ] = 1.0;
hoặc
for( i = n; –i >= 0; )
array[ i ] = 1.0;
Tất cả những cách viết trên đều đúng, tuy nhiên idioms cho trường hợp này là:
for( i = 0; i < n; i++ )
array[ i ] = 1.0;
Idiom của vòng lặp duyệt qua các phần tử của một danh sách (list) là
for( p = list; p != NULL; p = p->next )
...
Đối với các vòng lặp vô hạn, idiom là
for ( ; ; ) ...
hoặc
while( 1 )
...
Các idiom cho xâu và kí tự
Xem đoạn mã sau đây:
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
leave a comment