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 này sẽ được sinh ra chỉ khi mà chúng cần thiết, nhưng rất dễ dàng để gọi chúng (đôi khi chúng ta gọi mà không nhận thức được)
const Empty e1; // default constructor;
// destructor
Empty e2(e1); // copy constructor
e2 = e1; // assignment operator
Empty *pe2 = &e2; // address-of operator (non-const)
const Empty *pe1 = &e1; // address-of operator (const)
Thế bên trong những hàm đó sẽ làm gì?
Default constructor và destructor không thực sự làm gì cả. Chúng chỉ đơn giản là cho phép chúng ta tạo và hủy object. Chú ý rằng hàm hủy mà trình biên dịch tự sinh ra không phải là virtual trừ phi nó được sử dụng cho một class kế thừa từ một class khác có hàm hủy là virtual. Toán tử lấy địa chỉ mặc định chỉ đơn giản là trả về địa chỉ của object. Những hàm này được viết như sau:
inline Empty::Empty() {}
inline Empty::~Empty() {}
inline Empty * Empty::operator&() { return this; }
inline const Empty * Empty::operator&() const
{ return this; }
Còn đối với copy constructor và assignment operator thì luật là: default copy constructor (assignment operator) sẽ thực hiện lệnh copy (assignment) của tất cả các data member mà không phải static của class. Nghĩa là nếu m là data member có kiểu T không phải là static của class C và C thì không khai báo copy constructor (assignment operator), m sẽ được copy (assign) sử dụng copy constructor (assignment operator) được định nghĩa cho kiểu T nếu có. Nếu không có, luật này sẽ được áp dụng “đệ quy” cho các data member của m cho đến khi có một copy constructor (assignment operator) hoặc built-in type (int, double, pointer …) được tìm thấy. Mặc định thì đối tượng của các kiểu built-in có copy constructor (assignment operator) bằng cách copy từng bit. Đối với class mà kế thứ từ một class khác thì luật này được áp dụng với từng mức của cây thừa kế (inheritance hierarchy), vì vậy các hàm copy constructor và assignment operator người dùng tự định nghĩa sẽ được gọi ở bất cứ mức nào mà nó khai báo.
Hãy xem xét thêm một ví dụ, một template NamedObject mà thực thể của nó là những lớp cho phép kết hợp tên và object.
template<class T>
class NamedObject
{
public:
NamedObject(const char *name, const T& value);
NamedObject(const string& name, const T& value);
...
private:
string nameValue;
T objectValue;
};
Bởi vì lớp NamedObject được khai báo có ít nhất một hàm tạo (constructor), nên trình biên dịch sẽ không sinh ra default constructor, nhưng bởi vì lớp đó không khai báo copy constructor hoặc assignment operator nên trình biên dịch sẽ tự sinh ra các hàm này (nếu cần)
Hãy xem các lệnh gọi sau gọi đến copy constructor
NamedObject<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2(no1); // calls copy constructor
Trong trường hợp này trình biên dịch sẽ tự sinh copy constructor hoặc assignment operator đúng. Tuy nhiên một khi trình biên dịch thất bại khi copy constructor (assignment operator) của các data member thì nó sẽ từ chối để tự sinh ra các copy constructor (assignment operator) của class.
Ví dụ hãy thay đổi nameValue là một reference đến một string và ObjectValue là một const T
class NamedObject
{
public:
// this ctor no longer takes a const name, because name-
// Value is now a reference-to-non-const string. The char*
// ctor is gone, because we must have a string to refer to
NamedObject(string& name, const T& value);
... // as above, assume no
// operator= is declared
private:
string& nameValue; // this is now a reference
const T objectValue; // this is now const
};
Điều gì sẽ xảy ra khi
string newDog("Persephone");
string oldDog("Satch");
NamedObject<int> p(newDog, 2); // as I write this, our dog
// °Persephone is about to
// have her second birthday
NamedObject<int> s(oldDog, 29); // the family dog Satch
// (from my childhood)
// would be 29 if she were
// still alive
p = s; // what should happen to
// the data members in p?
Bạn hãy thử tự lý giải vì sao trình biên dịch sẽ báo lỗi trong trường hợp này nhé.
rhs = Right Hand Side ???
yes
“Bạn hãy thử tự lý giải vì sao trình biên dịch sẽ báo lỗi trong trường hợp này nhé.” –> vì objectValue là const nên ko gán được s.objectValue cho p.objectValue, right?
Error: NamedObject has a const member objectValue and can not be assigned
Chính xác đó là một lỗi. Ngoài ra còn một lỗi nữa là thay đổi reference cho refer đến một object khác. Không có cách như vậy (xem ở bài phân biệt pointer và reference). Ở đây nameValue đã được thay bằng sử dụng reference. Sau đó thì không thể cho reference này refer đến một object khác được.