Trong phần lớn thời lượng của chương này, chúng ta đã được học về những ưu điểm của việc giữ kín dữ liệu khi cài đặt các class và các thành viên (biến thành viên, hàm thành viên) bên trong class. Tuy nhiên, đôi khi bạn sẽ gặp phải những tình huống mà trong đó, bạn sẽ thấy có các class và các hàm nằm ở bên ngoài các class đó, hoạt động chặt chẽ cùng với nhau. Ví dụ, bạn có thể có một class để lưu trữ dữ liệu, và một hàm (hoặc một class khác) hiển thị dữ liệu lên màn hình. Mặc dù class thực hiện việc lưu trữ dữ liệu và phần code hiển thị dữ liệu đã được tách biệt để bảo trì dễ dàng hơn, nhưng phần code hiển thị dữ liệu thực sự gắn bó một cách chặt chẽ và gắn kết với các chi tiết của class lưu trữ dữ liệu. Do đó, trong trường hợp này, việc che giấu các chi tiết của class lưu trữ dữ liệu khỏi phần code hiển thị dữ liệu sẽ không có nhiều ý nghĩa.
Trong những tình huống như thế này, ta có hai lựa chọn:
1) Cứ để cho phần code hiển thị dữ liệu sử dụng được những hàm được công khai (public) của class lưu trữ dữ liệu. Tuy nhiên, cách này tiềm tàng một số nhược điểm. Đầu tiên, phải xác định rõ những hàm thành viên nào sẽ được công khai, việc này sẽ mất thời gian, và có thể làm lộn xộn giao diện code của class lưu trữ dữ liệu. Thứ hai, khi hiển thị công khai một số hàm cho phần code hiển thị dữ liệu có thể sử dụng được, thì class lưu trữ dữ liệu cũng đồng thời phơi bày những hàm công khai đó cho những đối tượng mà nó không mong muốn thấy được. Không có cách nào để nói được rằng “hàm này được bố trí công khai chỉ để cho phần code hiển thị dữ liệu sử dụng”.
2) Sử dụng các class bạn và các hàm bạn, bằng cách này, bạn có thể cấp cho phần code hiển thị dữ liệu của mình quyền truy cập tới các chi tiết private (riêng tư) của class lưu trữ dữ liệu. Điều này cho phép phần code hiển thị dữ liệu có thể truy cập trực tiếp tới tất cả các biến thành viên và hàm thành viên private của class lưu trữ dữ liệu, trong khi vẫn đảm bảo các đối tượng không mong muốn không thể truy cập vào được! Trong bài này, chúng ta sẽ tìm hiểu kỹ hơn về class bạn (friend class) và hàm bạn (friend function).
Nội dung chính
1. Hàm bạn(Friend functions)
Hàm bạn là một hàm có thể truy cập đến các thành viên private (gồm cả các biến thành viên và các hàm thành viên) của một class, như thể nó là một thành viên của class đó. Ngoại trừ đặc điểm ở trên ra thì hàm bạn hoàn toàn giống với hàm bình thường. Một hàm bạn có thể chỉ là một hàm bình thường, hoặc là một hàm thành viên của một class khác. Để khai báo một hàm bạn, chỉ cần sử dụng từ khóa friend ở trước phần nguyên mẫu (prototype) của hàm mà bạn muốn nó trở thành bạn của class. Có thể khai báo hàm bạn bên trong phần public (công khai) hoặc phần private (riêng tư) của class đều được.
Dưới đây là ví dụ về việc sử dụng hàm bạn:
/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
class Accumulator
{
private:
int m_value;
public:
Accumulator() { m_value = 0; }
void add(int value) { m_value += value; }
// Make the reset() function a friend of this class
friend void reset(Accumulator &accumulator);
};
// reset() is now a friend of the Accumulator class
void reset(Accumulator &accumulator)
{
// And can access the private data of Accumulator objects
accumulator.m_value = 0;
}
int main()
{
Accumulator acc;
acc.add(5); // add 5 to the accumulator
reset(acc); // reset the accumulator to 0
return 0;
}
Trong ví dụ này, chúng ta đã khai báo một hàm có tên là reset() nhận vào một đối tượng của class Accumulator, và gán giá trị 0 cho biến m_value. Bởi vì hàm reset() không phải là một thành viên của class Accumulator, nên theo lẽ thường hàm reset() sẽ không thể truy cập tới các thành viên private của class Accumulator. Tuy nhiên, bởi vì class Accumulator đã khai báo cụ thể hàm reset() này là bạn của nó, nên hàm reset() sẽ có được quyền truy cập tới các thành viên private của class Accumulator.
Lưu ý rằng, chúng ta phải truyền vào một đối tượng của class Accumulator cho hàm reset(). Điều này là do hàm reset() không phải là một hàm thành viên của class Accumulator. Nó không có con trỏ *this, và cũng không sở hữu đối tượng nào của class Accumulator để có thể làm việc cùng, trừ khi ta cung cấp cho nó.
Dưới đây là một ví dụ khác:
class Value
{
private:
int m_value;
public:
Value(int value) { m_value = value; }
friend bool isEqual(const Value &value1, const Value &value2);
};
bool isEqual(const Value &value1, const Value &value2)
{
return (value1.m_value == value2.m_value);
}
Trong ví dụ này, chúng ta khai báo hàm isEqual() là bạn của class Value. Hàm isEqual() nhận vào hai đối tượng Value làm tham số truyền vào. Bởi vì hàm isEqual() là bạn của class Value, nên nó có thẻ truy cập tới các thành viên private của tất cả các đối tượng Value. Trong trường hợp này, nó sử dụng quyền truy cập đó để thực hiện một phép so sánh trên hai đối tượng, và trả về true nếu chúng bằng nhau.
Mặc dù cả hai ví dụ trên trông có vẻ như được bố trí sẵn, không thực tế lắm, tuy nhiên ví dụ sau rất giống với các trường hợp mà chúng ta sẽ gặp trong chương 9, khi thảo luận về việc nạp chồng toán tử (operator overloading)!
2. Nhiều bạn(Multiple friends)
Một hàm có thể là bạn của nhiều class cùng một lúc. Ví dụ, cùng xét đoạn code ví dụ sau:
/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
#include <iostream>
class Humidity;
class Temperature
{
private:
int m_temp;
public:
Temperature(int temp=0) { m_temp = temp; }
friend void printWeather(const Temperature &temperature, const Humidity &humidity);
};
class Humidity
{
private:
int m_humidity;
public:
Humidity(int humidity=0) { m_humidity = humidity; }
friend void printWeather(const Temperature &temperature, const Humidity &humidity);
};
void printWeather(const Temperature &temperature, const Humidity &humidity)
{
std::cout << "The temperature is " << temperature.m_temp <<
" and the humidity is " << humidity.m_humidity << '\n';
}
int main()
{
Humidity hum(10);
Temperature temp(12);
printWeather(temp, hum);
return 0;
}
Có hai điều đáng lưu ý về ví dụ này. Thứ nhất, bởi vì PrintWeather là bạn của cả hai class, nên nó có thể truy cập tới dữ liệu private từ các đối tượng thuộc cả hai kiểu class. Thứ hai, hãy chú ý tới dòng code nằm ở đầu ví dụ:
class Humidity;
Đây là một khai báo nguyên mẫu lớp (class prototype), cho trình biên dịch biết rằng chúng ta sẽ định nghĩa một class có tên Humidity trong tương lai. Nếu không có dòng code này, trình biên dịch sẽ nói với chúng ta rằng nó không biết Humidity là gì, khi phân tích cú pháp (parsing) cái phần nguyên mẫu (prototype) của hàm PrintWeather() bên trong class Temperature. Nguyên mẫu lớp (class prototype) đóng vai trò giống như các nguyên mẫu hàm (function prototype) – chúng nói cho trình biên dịch biết cái gì đó trông như thế nào, để trình biên dịch có thể biết được rằng, à, cái này có thể sử dụng được ngay bây giờ, rồi định nghĩa sau cũng được. Tuy nhiên, không giống như các hàm, các class không có kiểu trả về hoặc tham số truyền vào, vì vậy các class prototypes đều chỉ có cú pháp đơn giản là từ khóa class theo sau bởi tên của class muốn tạo nguyên mẫu, ví dụ: class ClassName, trong đó ClassName là tên của class cụ thể.
3. Lớp bạn(Friend classes)
Việc biến toàn bộ một lớp trở thành bạn của một lớp khác là hoàn toàn có thể. Điều này cho phép tất cả các thành viên của lớp bạn có thể truy cập tới các thành viên private của lớp khác. Đây là một ví dụ:
/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
#include <iostream>
class Storage
{
private:
int m_nValue;
double m_dValue;
public:
Storage(int nValue, double dValue)
{
m_nValue = nValue;
m_dValue = dValue;
}
// Make the Display class a friend of Storage
friend class Display;
};
class Display
{
private:
bool m_displayIntFirst;
public:
Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
void displayItem(const Storage &storage)
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
else // display double first
std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
}
};
int main()
{
Storage storage(5, 6.7);
Display display(false);
display.displayItem(storage);
return 0;
}
Bởi vì class Display là bạn của class Storage, nên bất cứ thành viên nào của class Display mà sử dụng một đối tượng của class Storage thì đều có thể truy cập trực tiếp tới các thành viên private của class Storage. Đoạn chương trình trên sẽ in ra kết quả sau:
6.7 5
Có một số lưu ý bổ sung về các lớp bạn. Thứ nhất, mặc dù Display là bạn của Storage, nhưng Display không có quyền truy cập trực tiếp vào con trỏ *this của các đối tượng thuộc lớp Storage. Thứ hai, chỉ vì Display là bạn của Storage, điều đó không có nghĩa là Storage cũng là bạn của Display. Nếu muốn hai lớp là bạn của nhau, cả hai lớp đó đều phải khai báo lớp kia là bạn của mình. Cuối cùng, nếu lớp A là bạn của lớp B, và lớp B là bạn của lớp C, điều đó không có nghĩa là lớp A là bạn của lớp C.
Hãy cẩn thận khi sử dụng các hàm bạn và lớp bạn, bởi vì việc này có thể cho phép các hàm bạn hoặc lớp bạn vi phạm tính đóng gói (encapsulation) của OOP. Nếu các thông tin chi tiết của một lớp bị thay đổi, thì các chi tiết của lớp bạn cũng sẽ bị buộc phải thay đổi. Do đó, hãy hạn chế sử dụng các hàm bạn và lớp bạn ở mức tối thiểu.
4. Các hàm thành viên là bạn
Thay vì làm cho cả một class trở thành bạn, ta có thể chỉ biến một hàm thành viên trở thành bạn cũng được. Điều này được thực hiện tương tự như khi biến một hàm thông thường thành bạn, ngoại trừ việc ta phải thêm tiền tố className:: vào phía trước tên hàm thành viên, ví dụ: Display::displayItem.
Tuy nhiên, trong thực tế, điều này có thể phức tạp hơn một chút so với dự kiến. Hãy thử biến đổi ví dụ trước một chút bằng cách làm cho hàm Display::displayItem trở thành một hàm (thành viên) bạn:
/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
class Display; // forward declaration for class Display
class Storage
{
private:
int m_nValue;
double m_dValue;
public:
Storage(int nValue, double dValue)
{
m_nValue = nValue;
m_dValue = dValue;
}
// Make the Display::displayItem member function a friend of the Storage class
friend void Display::displayItem(const Storage& storage); // error: Storage hasn't seen the full definition of class Display
};
class Display
{
private:
bool m_displayIntFirst;
public:
Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
void displayItem(const Storage &storage)
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
else // display double first
std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
}
};
Tuy nhiên, đoạn code trên sẽ không hoạt động. Để làm cho một hàm thành viên trở thành hàm bạn, trình biên dịch phải đã nhìn thấy được toàn bộ phần code định nghĩa của class của hàm thành viên bạn này (chứ không chỉ là một forward declaration – một cái khai báo trước). Bởi vì class Storage đã không nhìn thấy được phần code định nghĩa đầy đủ của class Display, nên trình biên dịch sẽ báo lỗi tại đoạn code mà chúng ta đang cố gắng làm cho hàm thành viên trở thành hàm bạn.
Thật may là điều này có thể được giải quyết dễ dàng bằng cách di chuyển phần code định nghĩa class Display lên trước phần code định nghĩa class Storage:
/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
class Display
{
private:
bool m_displayIntFirst;
public:
Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
void displayItem(const Storage &storage) // error: compiler doesn't know what a Storage is
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
else // display double first
std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
}
};
class Storage
{
private:
int m_nValue;
double m_dValue;
public:
Storage(int nValue, double dValue)
{
m_nValue = nValue;
m_dValue = dValue;
}
// Make the Display::displayItem member function a friend of the Storage class
friend void Display::displayItem(const Storage& storage); // okay now
};
Tuy nhiên, bây giờ chúng ta lại gặp phải một vấn đề khác. Bởi vì hàm thành viên Display::displayItem() sử dụng đối tượng của class Storage như là một tham số tham chiếu, và chúng ta vừa mới di chuyển phần code định nghĩa class Storage xuống bên dưới phần code định nghĩa class Display, nên trình biên dịch sẽ phàn nàn rằng nó không biết Storage là cái gì. Chúng ta không thể sửa lỗi này bằng cách sắp xếp lại thứ tự các đoạn code định nghĩa class, bởi nếu làm vậy ta sẽ hoàn tác (undo) giải pháp sửa lỗi trước đó.
Chúng ta lại tiếp tục gặp may khi lỗi này cũng có thể sửa trong một vài bước đơn giản. Đẩu tiên, chúng ta có thể định một forward declaration – khai báo trước cho class Storage. Tiếp theo, chúng ta có thể di chuyển phần code định nghĩa của hàm Display::displayItem() ra nêm ngoài class Display, đặt ở sau phần code định nghĩa đầy đủ của class Storage. Sau khi sửa như trên, ta sẽ được:
/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
#include <iostream>
class Storage; // forward declaration for class Storage
class Display
{
private:
bool m_displayIntFirst;
public:
Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
void displayItem(const Storage &storage); // forward declaration above needed for this declaration line
};
class Storage // full definition of Storage class
{
private:
int m_nValue;
double m_dValue;
public:
Storage(int nValue, double dValue)
{
m_nValue = nValue;
m_dValue = dValue;
}
// Make the Display::displayItem member function a friend of the Storage class (requires seeing the full declaration of class Display, as above)
friend void Display::displayItem(const Storage& storage);
};
// Now we can define Display::displayItem, which needs to have seen the full definition of class Storage
void Display::displayItem(const Storage &storage)
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
else // display double first
std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
}
int main()
{
Storage storage(5, 6.7);
Display display(false);
display.displayItem(storage);
return 0;
}
Bây giờ, mọi thứ sẽ được biên dịch đúng: Phần khai báo trước (forward declaration) của class Storage là đủ để cho khai báo hàm Display::displayItem() bên trong class Display là đúng, phần code định nghĩa đầy đủ của class Display giúp cho việc khai báo hàm Display::displayItem() là hàm bạn của lớp Storage trở nên hợp lý, và phần code định nghĩa đầy đủ của class Storage là đủ để thỏa mãn được tính đúng đắn của đoạn code định nghĩa hàm thành viên Display::displayItem(). Nếu bạn cảm thấy hơi khó hiểu, hãy cố gắng đọc lại những dòng comments ở trong đoạn chương trình phía trên.
Nếu bạn cảm thấy việc khai báo các hàm thành viên trở thành hàm bạn này thật là nhảm nhí, thì thật ra nó đúng là như vậy đấy =)). Và thật may là, việc này chỉ cần thiết khi chúng ta cố gắng làm tất cả mọi thứ bên trong một file duy nhất. Có một giải pháp tốt hơn đó là đặt từng phần code định nghĩa của mỗi class vào bên trong các file header riêng biệt, với các phần code định nghĩa các hàm thành viên ở bên trong các file .cpp tương ứng. Theo cách đó, tất cả các phần code định nghĩa class sẽ ngay lập tức có thể được nhìn thấy (visible) bên trong các file .cpp, và chúng ta cũng sẽ không cần phải sắp xếp lại thứ tự của các class hoặc các hàm!
5. Tổng kết
Một hàm bạn hoặc class bạn là một hàm hoặc class mà có thể truy cập tới các thành viên private của một class khác như thế nó là thành viên của class đó. Điều này cho phép hàm bạn hoặc class bạn có thể làm việc một cách chặt chẽ và gắn kết với class khác, mà không khiến cho class khác phải phơi bày ra các thành viên private của mình (ví dụ: Thông qua các hàm truy cập).
Việc kết bạn được sử dụng không phổ biến khi hai hoặc nhiều class cần làm việc với nhau một cách chặt chẽ và gắn kết, hoặc phổ biến hơn nhiều, khi định nghĩa các toán tử nạp chồng – overloading operators
Bạn có thể tham khảo thêm series tự học C++ này.