Bài 35.4: Hàm bạn và lớp bạn trong C++

Bài 35.4: Hàm bạn và lớp bạn trong C++
Chào mừng các bạn đến với bài viết tiếp theo trong chuỗi blog về C++ của FullhouseDev! Hôm nay, chúng ta sẽ cùng khám phá một cặp khái niệm khá đặc biệt trong C++: Hàm bạn (Friend function) và Lớp bạn (Friend class). Đây là những "người bạn" đặc quyền, được một lớp trao quyền truy cập vào cả những thành viên riêng tư (private) và được bảo vệ (protected) của nó – điều mà thông thường chỉ có các hàm thành viên của lớp đó mới làm được.
Tính đóng gói và "người bạn" đặc biệt
Một trong những nguyên tắc cốt lõi của Lập trình hướng đối tượng (OOP) là tính đóng gói (encapsulation). Tính đóng gói giúp bảo vệ dữ liệu bên trong lớp bằng cách giới hạn quyền truy cập từ bên ngoài. Các thành viên dữ liệu thường được khai báo là private
hoặc protected
, và chỉ có thể được truy cập hoặc sửa đổi thông qua các hàm thành viên công khai (public) của lớp (như getter/setter).
Tuy nhiên, trong một số trường hợp đặc biệt, bạn có thể cần một hàm không phải là thành viên của lớp, hoặc một lớp khác, có thể "nhìn thấy" và tương tác trực tiếp với dữ liệu riêng tư của lớp. Đây chính là lúc Hàm bạn và Lớp bạn phát huy tác dụng. Chúng giống như việc bạn cho phép một người bạn cực kỳ thân thiết biết mật khẩu vào ngôi nhà của mình vậy – quyền truy cập được cấp có chủ đích, nhưng nó cũng tiềm ẩn rủi ro nếu không được sử dụng cẩn thận.
Hãy cùng đi sâu vào từng loại "người bạn" này nhé.
Hàm bạn (Friend Function)
Hàm bạn là một hàm không phải là hàm thành viên của một lớp, nhưng được lớp đó cấp quyền truy cập vào các thành viên private
và protected
của nó.
Tại sao lại cần Hàm bạn?
Có một vài lý do bạn có thể muốn sử dụng hàm bạn:
- Thao tác với hai đối tượng thuộc các lớp khác nhau: Khi một hàm cần truy cập vào dữ liệu riêng tư của hai hoặc nhiều lớp khác nhau (ví dụ: một hàm hoán đổi giá trị giữa hai đối tượng).
- Quá tải toán tử (Operator Overloading): Đây là một trường hợp sử dụng rất phổ biến. Khi bạn muốn quá tải một toán tử nhị phân (binary operator) như
+
,-
,<<
,>>
mà toán hạng bên trái không phải là một đối tượng của lớp đó (ví dụ:int + MyClass
hoặccout << MyClass
). Trong trường hợp này, hàm quá tải không thể là hàm thành viên, và cần quyền truy cập vào dữ liệu riêng tư. - Hàm tiện ích (Utility function): Đôi khi có một hàm tiện ích gắn bó chặt chẽ với hoạt động bên trong của lớp nhưng không nhất thiết phải là một phương thức của lớp đó.
Làm thế nào để khai báo Hàm bạn?
Bạn khai báo một hàm là "bạn" của một lớp bằng cách đặt từ khóa friend
trước khai báo hàm đó bên trong định nghĩa lớp.
Ví dụ minh họa:
Giả sử chúng ta có một lớp đơn giản MyClass
với một thành viên dữ liệu riêng tư. Chúng ta muốn viết một hàm bên ngoài lớp này để hiển thị giá trị của thành viên riêng tư đó.
#include <iostream>
// Khai báo lớp MyClass
class MyClass {
private:
int privateData;
public:
// Constructor để khởi tạo privateData
MyClass(int data) : privateData(data) {}
// Khai báo hàm accessPrivateData là hàm bạn của MyClass
// Chú ý: friend được đặt trước khai báo hàm
friend void accessPrivateData(const MyClass& obj);
// Các hàm thành viên công khai khác nếu cần
};
// Định nghĩa hàm bạn accessPrivateData bên ngoài lớp
// Chú ý: Không có từ khóa friend ở đây khi định nghĩa
void accessPrivateData(const MyClass& obj) {
// Hàm này có thể truy cập trực tiếp privateData của obj
cout << "Du lieu private: " << obj.privateData << endl;
}
int main() {
MyClass myObj(123);
// Gọi hàm bạn. Hàm này có thể truy cập privateData của myObj
accessPrivateData(myObj);
return 0;
}
Giải thích code:
- Trong lớp
MyClass
, chúng ta khai báofriend void accessPrivateData(const MyClass& obj);
. Dòng này nói với trình biên dịch rằngaccessPrivateData
là một hàm bạn và có quyền truy cập vào các thành viên riêng tư củaMyClass
. - Hàm
accessPrivateData
được định nghĩa bên ngoài lớp, giống như bất kỳ hàm thông thường nào. Tuy nhiên, nhờ được khai báo làfriend
bên trongMyClass
, nó có thể truy cậpobj.privateData
một cách hợp lệ, mặc dùprivateData
được khai báo làprivate
. - Trong
main
, chúng ta tạo một đối tượngmyObj
và gọiaccessPrivateData
, truyềnmyObj
vào. Hàm này thực hiện đúng như mong đợi, in ra giá trị củaprivateData
.
Một ví dụ phổ biến hơn: Quá tải toán tử <<
Việc quá tải toán tử <<
(toán tử chèn luồng) để in đối tượng của lớp bạn ra cout
là một trường hợp kinh điển cần đến hàm bạn. Toán tử <<
nhận ostream&
làm toán hạng bên trái và đối tượng của bạn làm toán hạng bên phải (cout << myObj
). Vì toán hạng bên trái là ostream
, hàm quá tải không thể là hàm thành viên của lớp MyClass
.
#include <iostream>
class Coordinate {
private:
double x;
double y;
public:
Coordinate(double valX, double valY) : x(valX), y(valY) {}
// Khai báo hàm quá tải toán tử << là hàm bạn
// Nó cần truy cập x và y để in ra
friend ostream& operator<<(ostream& os, const Coordinate& coord);
};
// Định nghĩa hàm quá tải toán tử <<
ostream& operator<<(ostream& os, const Coordinate& coord) {
// Nhờ là friend, hàm này truy cập được coord.x và coord.y
os << "(" << coord.x << ", " << coord.y << ")";
return os; // Trả về luồng để có thể nối tiếp các thao tác << khác
}
int main() {
Coordinate p1(10.5, 20.1);
// Sử dụng toán tử << đã quá tải để in đối tượng Coordinate
cout << "Toa do cua p1: " << p1 << endl;
return 0;
}
Giải thích code:
- Lớp
Coordinate
có hai thành viên riêng tưx
vày
. - Chúng ta muốn dùng cú pháp
cout << p1;
để in tọa độ. - Hàm
operator<<
được khai báo là bạn của lớpCoordinate
. Nó nhậnostream&
(tham chiếu đến luồng xuất, ví dụcout
) vàconst Coordinate&
(tham chiếu đến đối tượng cần in) làm đối số. - Trong định nghĩa hàm
operator<<
, nó truy cập trực tiếpcoord.x
vàcoord.y
và chèn chúng vào luồngos
.
Lưu ý quan trọng về Hàm bạn:
- Hàm bạn không được gọi bằng đối tượng của lớp (ví dụ:
myObj.accessPrivateData()
là sai). Nó được gọi như một hàm thông thường (accessPrivateData(myObj);
). - Hàm bạn không có con trỏ
this
. Nó phải truy cập các thành viên của lớp thông qua đối tượng được truyền vào làm đối số (nếu có). - Tính "bạn" không được kế thừa. Nếu lớp A là bạn của lớp B, và lớp C kế thừa từ A, thì C không tự động trở thành bạn của B.
- Tính "bạn" không có tính bắc cầu. Nếu A là bạn của B, và B là bạn của C, thì A không tự động trở thành bạn của C.
Lớp bạn (Friend Class)
Tương tự như hàm bạn, Lớp bạn là một lớp được một lớp khác cấp quyền truy cập vào tất cả các thành viên private
và protected
của nó. Nếu một lớp A là bạn của lớp B, thì tất cả các hàm thành viên của lớp A có thể truy cập các thành viên riêng tư và được bảo vệ của đối tượng thuộc lớp B.
Tại sao lại cần Lớp bạn?
Lớp bạn thường được sử dụng khi có hai lớp gắn bó chặt chẽ với nhau và thường xuyên cần truy cập vào nội bộ của nhau để thực hiện chức năng chung, đặc biệt là khi việc cung cấp các hàm thành viên công khai để truy cập dữ liệu riêng tư sẽ làm phức tạp giao diện của lớp hoặc phá vỡ logic thiết kế. Ví dụ: khi thiết kế các cấu trúc dữ liệu phức tạp (như danh sách liên kết, cây), lớp nút (Node) có thể cần cho phép lớp quản lý cấu trúc đó (ví dụ: List, Tree) truy cập trực tiếp vào các con trỏ liên kết của nó.
Làm thế nào để khai báo Lớp bạn?
Bạn khai báo một lớp là "bạn" của một lớp khác bằng cách đặt từ khóa friend class
theo sau là tên lớp bạn bên trong định nghĩa lớp được kết bạn.
Ví dụ minh họa:
Hãy xem xét hai lớp SecretKeeper
(người giữ bí mật) và SecretAgent
(điệp viên bí mật). Chỉ SecretAgent
mới được phép biết bí mật bên trong SecretKeeper
.
#include <iostream>
#include <string>
// Khai báo trước (forward declaration) lớp SecretAgent
// Cần thiết vì SecretKeeper nhắc đến SecretAgent trước khi nó được định nghĩa đầy đủ
class SecretAgent;
// Khai báo lớp SecretKeeper
class SecretKeeper {
private:
string topSecretData;
public:
SecretKeeper(const string& secret) : topSecretData(secret) {}
// Khai báo SecretAgent là lớp bạn của SecretKeeper
// Điều này cho phép MỌI hàm thành viên của SecretAgent
// truy cập vào privateData của SecretKeeper
friend class SecretAgent;
// Các hàm thành viên công khai khác nếu cần
void somePublicMethod() const {
cout << "SecretKeeper: Day la mot phuong thuc cong khai." << endl;
}
};
// Khai báo và định nghĩa lớp SecretAgent
class SecretAgent {
public:
SecretAgent() {}
// Hàm thành viên của SecretAgent có thể truy cập privateData của SecretKeeper
void revealSecret(const SecretKeeper& sk) const {
cout << "SecretAgent: Bi mat da bi he lo! Data: " << sk.topSecretData << endl;
}
// Một hàm thành viên khác của SecretAgent, cũng có quyền truy cập
void anotherAgentMethod(const SecretKeeper& sk) const {
cout << "SecretAgent: Phuong thuc khac cung biet bi mat: " << sk.topSecretData << endl;
}
};
int main() {
SecretKeeper vault("Du lieu cuc mat!!!");
SecretAgent 007;
// Agent 007 co the goi ham de he lo bi mat cua vault
007.revealSecret(vault);
007.anotherAgentMethod(vault);
// Thu truy cap truc tiep (se loi bien dich)
// cout << vault.topSecretData << endl; // Lỗi: topSecretData là private
return 0;
}
Giải thích code:
- Chúng ta cần khai báo trước (forward declare)
SecretAgent
bởi vìSecretKeeper
sử dụng tênSecretAgent
trong khai báofriend class SecretAgent;
trước khiSecretAgent
được định nghĩa đầy đủ. - Trong
SecretKeeper
, dòngfriend class SecretAgent;
cấp quyền truy cập đầy đủ choSecretAgent
. - Bây giờ, trong lớp
SecretAgent
, bất kỳ hàm thành viên nào (nhưrevealSecret
vàanotherAgentMethod
) đều có thể truy cập các thành viên riêng tư (topSecretData
) của đối tượngSecretKeeper
được truyền vào. - Thao tác
cout << vault.topSecretData;
trongmain
vẫn sẽ gây lỗi biên dịch vìmain
không phải là hàm bạn hay lớp bạn củaSecretKeeper
.
Lưu ý quan trọng về Lớp bạn:
- Nếu lớp A là bạn của lớp B, thì B không tự động là bạn của A. Quan hệ bạn bè là đơn hướng (unidirectional).
- Tính "bạn" không được kế thừa hay có tính bắc cầu, giống như hàm bạn.
Khi nào nên (và không nên) sử dụng Friend?
Việc sử dụng friend
về cơ bản là phá vỡ nguyên tắc đóng gói. Nó giống như việc tạo ra một "lỗ hổng" có chủ đích trong lớp của bạn. Do đó, bạn nên sử dụng friend
một cách cẩn trọng và chỉ khi thực sự cần thiết.
Nên sử dụng khi:
- Quá tải các toán tử nhị phân như
<<
,>>
hoặc các toán tử mà toán hạng bên trái không phải là đối tượng của lớp (như+
trongint + MyClass
). - Thiết kế các hàm tiện ích hoặc hàm trợ giúp có mối quan hệ rất chặt chẽ với nội bộ của lớp và việc cung cấp các hàm thành viên công khai sẽ làm phức tạp lớp một cách không cần thiết.
- Trong một số mẫu thiết kế (design patterns) hoặc cấu trúc dữ liệu phức tạp nơi hai lớp thực sự cần làm việc ở mức độ "nội bộ" với nhau.
Không nên sử dụng khi:
- Chỉ vì "tiện". Nếu bạn có thể đạt được mục tiêu bằng cách sử dụng các hàm thành viên công khai (ví dụ: thông qua getter/setter), hãy ưu tiên cách đó.
- Khi quan hệ giữa các lớp không thực sự gắn bó chặt chẽ ở mức độ nội bộ.
- Như một cách để "lách luật" truy cập dữ liệu riêng tư mà không suy nghĩ kỹ về thiết kế tổng thể.
Việc sử dụng friend
quá mức có thể làm giảm khả năng bảo trì và hiểu code, vì nó làm mờ ranh giới giữa giao diện công khai và triển khai nội bộ của lớp.
Bài tập ví dụ: C++ Bài 24.B1: Quản lý điểm thi (OOP)
Quản lý điểm thi (OOP)
Đề bài
Trường FullHouse Dev muốn quản lý điểm thi của sinh viên. Quy tắc đánh giá như sau:
- Điểm trung bình = (Điểm giữa kỳ × 40%) + (Điểm cuối kỳ × 60%)
- Xếp loại:
- Nếu Điểm trung bình ≥ 9.0: Xuất sắc
- Nếu Điểm trung bình ≥ 7.5: Giỏi
- Nếu Điểm trung bình ≥ 5.0: Trung bình
- Nếu Điểm trung bình < 5.0: Yếu
Hãy nhập thông tin điểm thi của các sinh viên và tính toán điểm trung bình, xếp loại, điểm cao nhất, điểm thấp nhất và điểm trung bình của lớp.
Input Format
- Dòng đầu ghi số sinh viên (không quá 100 sinh viên)
- Mỗi sinh viên ghi trên 4 dòng:
- Tên sinh viên
- Điểm giữa kỳ
- Điểm cuối kỳ
- Mã sinh viên
Output Format
Ghi ra danh sách sinh viên đã được tính điểm gồm các thông tin:
- Mã sinh viên
- Tên sinh viên
- Điểm giữa kỳ
- Điểm cuối kỳ
- Điểm trung bình
- Xếp loại
Dòng cuối ghi điểm cao nhất, điểm thấp nhất và điểm trung bình của lớp (theo mẫu trong ví dụ).
Ví dụ
Dữ liệu vào:
3
Nguyen Van A
8.5
9.0
SV001
Tran Thi B
7.0
8.0
SV002
Le Van C
4.5
6.0
SV003
Dữ liệu ra:
SV001 Nguyen Van A 8.5 9.0 8.7 Xuat sac
SV002 Tran Thi B 7.0 8.0 7.6 Gioi
SV003 Le Van C 4.5 6.0 5.4 Trung binh
Diem cao nhat: 8.7
Diem thap nhat: 5.4
Diem trung binh cua lop: 7.23
Giải thích ví dụ mẫu:
- Đầu vào: Nguyen Van A 8.5 9.0 SV001
- Đầu ra: SV001 Nguyen Van A 8.5 9.0 8.7 Xuat sac
Dòng này hiển thị thông tin chi tiết về sinh viên SV001, bao gồm mã sinh viên, tên, điểm giữa kỳ, điểm cuối kỳ, điểm trung bình và xếp loại. Tổng điểm cao nhất, điểm thấp nhất và điểm trung bình của lớp cũng được tính toán và hiển thị ở dòng cuối.
Cách tính điểm trung bình cho Nguyễn Văn A: Điểm trung bình = (8.5 × 40%) + (9.0 × 60%) = 3.4 + 5.4 = 8.7 Chào bạn, đây là hướng dẫn giải bài tập Quản lý điểm thi sử dụng lập trình hướng đối tượng (OOP) trong C++, tập trung vào việc sử dụng các thư viện chuẩn (std) và cấu trúc code rõ ràng.
1. Phân tích bài toán và Xác định đối tượng:
Bài toán yêu cầu quản lý thông tin và điểm thi của nhiều sinh viên. Rõ ràng, đối tượng chính mà chúng ta cần mô hình hóa là SinhVien
. Mỗi SinhVien
sẽ có các thuộc tính và các hành vi liên quan đến điểm số của họ.
2. Thiết kế Lớp (Class) SinhVien
:
Thuộc tính (Member Variables): Một đối tượng
SinhVien
cần lưu trữ các thông tin sau:- Mã sinh viên (kiểu
string
) - Tên sinh viên (kiểu
string
) - Điểm giữa kỳ (kiểu
double
) - Điểm cuối kỳ (kiểu
double
) - Điểm trung bình (kiểu
double
- sẽ được tính toán) - Xếp loại (kiểu
string
- sẽ được tính toán)
- Mã sinh viên (kiểu
Phương thức (Member Functions): Lớp
SinhVien
nên có các hành vi sau:- Phương thức để nhập thông tin cho một sinh viên.
- Phương thức để tính điểm trung bình từ điểm giữa kỳ và cuối kỳ.
- Phương thức để xác định xếp loại dựa trên điểm trung bình.
- Phương thức để hiển thị thông tin chi tiết của sinh viên (bao gồm cả điểm trung bình và xếp loại đã tính).
3. Chi tiết về các Phương thức trong lớp SinhVien
:
- Nhập thông tin: Phương thức này sẽ đọc mã sinh viên, tên, điểm giữa kỳ, điểm cuối kỳ từ luồng nhập chuẩn (
cin
). Lưu ý: Khi đọc tên sinh viên sau khi đã đọc điểm (là số), cần xử lý ký tự xuống dòng còn lại trong bộ đệm nhập (ví dụ: dùngcin.ignore()
hoặc đọc rỗng một dòng trước khi dùnggetline
để đọc tên). - Tính điểm trung bình: Phương thức này sẽ áp dụng công thức:
diemTrungBinh = diemGiuaKy * 0.4 + diemCuoiKy * 0.6
. Sau khi tính toán, lưu kết quả vào thuộc tínhdiemTrungBinh
. - Xếp loại: Phương thức này sẽ sử dụng các câu lệnh điều kiện (
if-else if-else
) dựa trên giá trị củadiemTrungBinh
để gán giá trị tương ứng ("Xuất sắc", "Giỏi", "Trung bình", "Yếu") vào thuộc tínhxepLoai
. - Hiển thị thông tin: Phương thức này sẽ in ra các thuộc tính của sinh viên theo đúng định dạng yêu cầu: Mã SV, Tên SV, Điểm giữa kỳ, Điểm cuối kỳ, Điểm trung bình, Xếp loại. Cần sử dụng các công cụ định dạng của
cout
(ví dụ:fixed
,setprecision
từ<iomanip>
) để in điểm trung bình với số chữ số thập phân theo mẫu.
4. Quản lý Danh sách Sinh viên:
- Chúng ta cần một nơi để lưu trữ nhiều đối tượng
SinhVien
.vector<SinhVien>
là lựa chọn phù hợp và linh hoạt trong C++ để tạo một danh sách động các sinh viên. - Đọc số lượng sinh viên từ đầu vào.
- Sử dụng một vòng lặp để đọc thông tin cho từng sinh viên:
- Trong mỗi lần lặp, tạo một đối tượng
SinhVien
mới. - Gọi phương thức nhập thông tin của đối tượng đó.
- Sau khi nhập xong điểm giữa kỳ và cuối kỳ, gọi các phương thức tính điểm trung bình và xếp loại cho đối tượng này.
- Thêm đối tượng
SinhVien
đã hoàn chỉnh vàovector
.
- Trong mỗi lần lặp, tạo một đối tượng
5. Tính toán Thống kê Lớp:
- Sau khi đã đọc và xử lý thông tin của tất cả sinh viên và lưu vào
vector
, chúng ta cần duyệt qua vector này để tính toán điểm cao nhất, điểm thấp nhất và điểm trung bình của cả lớp. - Khởi tạo các biến để lưu trữ điểm cao nhất (ví dụ: bắt đầu bằng một giá trị rất nhỏ), điểm thấp nhất (bắt đầu bằng một giá trị rất lớn), và tổng điểm trung bình của tất cả sinh viên (bắt đầu bằng 0).
- Duyệt qua từng đối tượng
SinhVien
trongvector
:- So sánh
diemTrungBinh
của sinh viên hiện tại với điểm cao nhất đã lưu, cập nhật nếu cần. - So sánh
diemTrungBinh
của sinh viên hiện tại với điểm thấp nhất đã lưu, cập nhật nếu cần. - Cộng
diemTrungBinh
của sinh viên hiện tại vào biến tổng điểm.
- So sánh
- Sau khi duyệt hết vector, tính điểm trung bình của lớp bằng cách chia tổng điểm cho số lượng sinh viên.
- In kết quả thống kê theo đúng định dạng yêu cầu, cũng sử dụng định dạng số thập phân (
fixed
,setprecision
).
6. Các Thư viện Cần Thiết:
<iostream>
: Để nhập/xuất dữ liệu (cin
,cout
).<string>
: Để sử dụng kiểu dữ liệustring
.<vector>
: Để sử dụngvector
để lưu danh sách sinh viên.<iomanip>
: Để định dạng đầu ra số thập phân (fixed
,setprecision
).<limits>
hoặc<cfloat>
(tùy chọn): Để khởi tạo giá trị ban đầu cho min/max điểm một cách an toàn, hoặc đơn giản là dùng điểm của sinh viên đầu tiên nếu chắc chắn có ít nhất 1 sinh viên.
Tóm lại, các bước thực hiện chính:
- Khai báo và định nghĩa lớp
SinhVien
với các thuộc tính và phương thức cần thiết (nhập, tính điểm TB, xếp loại, in thông tin). - Trong hàm
main
:- Đọc số lượng sinh viên N.
- Tạo một
vector<SinhVien>
rỗng. - Lặp N lần để đọc thông tin từng sinh viên, tạo đối tượng
SinhVien
, gọi các phương thức tính toán, và thêm vào vector. - Duyệt vector để in thông tin từng sinh viên và đồng thời tính toán tổng điểm, tìm min/max điểm trung bình.
- Tính điểm trung bình của lớp từ tổng điểm.
- In ra kết quả thống kê lớp (min, max, trung bình).
Đây là cấu trúc và hướng đi chi tiết để giải bài toán bằng C++ và OOP. Chú trọng vào việc chia nhỏ bài toán thành các trách nhiệm của lớp SinhVien
và sử dụng vector
để quản lý danh sách.
Comments