Lớp và đối tượng (phần 3)
Một lớp có thể có các đối tượng của các lớp khác như các thành viên. Khi một đối tượng đi vào phạm vi, constructor của nó được gọi một cách tự động, vì thế chúng ta cần mô tả các tham số được truyền như thế nào tới các constructor của đối ...
Một lớp có thể có các đối tượng của các lớp khác như các thành viên. Khi một đối tượng đi vào phạm vi, constructor của nó được gọi một cách tự động, vì thế chúng ta cần mô tả các tham số được truyền như thế nào tới các constructor của đối tượng thành viên. Các đối tượng thành viên được xây dựng theo thứ tự mà trong đó chúng được khai báo (không theo thứ tự mà chúng được liệt kê trong danh sách bộ khởi tạo thành viên của constructor) và trước các đối tượng của lớp chứa đựng chúng được xây dựng.
Ví dụ 3.15: Chương trình sau minh họa các đối tượng như các thành viên của các đối tượng khác.
1: #include <iostream.h> 2: #include <string.h> 3: 4: class Date 5: { 6: public: 7: Date(int = 1, int = 1, int = 1900); //Constructor mặc định 8: void Print() const; //In ngày theo dạng Month/Day/Year 9: private: 10: int Month; //1-12 11: int Day; //1-31 12: int Year; //Năm bất kỳ 13://Hàm tiện ích để kiểm tra Day tương thích đối với Month và Year 14: int CheckDay(int); 15: }; 16: 17: class Employee 18: { 19: public: 20: Employee(char *, char *, int, int, int, int, int, int); 21: void Print() const; 22: private: 23: char LastName[25]; 24: char FirstName[25]; 25: Date BirthDate; 26: Date HireDate; 27: }; 28: 29: //Constructor: xác nhận giá trị tương thích của Month 30: //Gọi hàm CheckDay() để xác nhận giá trị tương thích của Day 31: Date::Date(int Mn, int Dy, int Yr) 32: { 33: if (Mn > 0 && Mn <= 12) 34: Month = Mn; 35: else 36: { 37: Month = 1; 38: cout << "Month " << Mn << " invalid. Set to Month 1." 39: << endl; 40: } 41: Year = Yr; 42: Day = CheckDay(Dy); 43: cout << "Date object constructor for date "; 44: Print(); 45: cout << endl; 46: } 47: 48: //Hàm xác nhận giá trị Day tương thích đưa vào Month và Year 49: int Date::CheckDay(int TestDay) 50: { 51: static int DaysPerMonth[13] = {0, 31, 28, 31, 30, 31, 52: 9; 9; 9; 9; 9; 9; # # # # 30, 31, 31, 30,31, 30, 31}; 53: 54: if (TestDay > 0 && TestDay <= DaysPerMonth[Month]) 55: return TestDay; 56: if (Month == 2 && TestDay == 29 && 57: ; ; (Year % 400 == 0 || (Year % 4 == 0 && Year % 100 != 0))) 58: return TestDay; 59: cout << "Day " << TestDay << "invalid. Set to Day 1." << endl; 60: return 1; 61: } 62: 63: //In đối tượng Date dạng Month/Day/Year 64: void Date::Print() const 65: { 66: cout << Month << '/' << Day << '/' << Year; 67: } 68: 69: Employee::Employee(char *FName, char *LName, 70: int BMonth, int BDay, int BYear, 71: int HMonth, int HDay, int HYear) 72: :BirthDate(BMonth, BDay, BYear), HireDate(HMonth, HDay, HYear) 73: { 74://Sao chép FName vào FirstName và phải chắc chắn rằng nó phù hợp 75: int Length = strlen(FName); 76: 77: Length = Length < 25 ? Length : 24; 78: strncpy(FirstName, FName, Length); 79: FirstName[Length] = ' '; 80: //Sao chép LName vào LastName và phải chắc chắn rằng nó phù hợp 81: Length = strlen(LName); 82: Length = Length < 25 ? Length : 24; 83: strncpy(LastName, LName, 24); 84: LastName[Length] = ' '; 85: cout << "Employee object constructor: " 86: << FirstName << ' ' << LastName << endl; 87: } 88: 89: void Employee::Print() const 90: { 91: cout << LastName << ", " << FirstName << endl << "Hired: "; 92: HireDate.Print(); 93: cout << " Birthday: "; 94: BirthDate.Print(); 95: cout << endl; 96: } 97: 98: int main() 99: { 100: Employee E("Bob", "Jones", 7, 24, 49, 3, 12, 88); 101: 102 cout << endl; 103: E.Print(); 104: cout << endl << "Test Date constructor with invalid values:" 105: << endl; 106: Date D(14, 35, 94); //Các giá trị Date không hợp lệ 107: return 0; 108: }
Chương trình gồm lớp Employee chứa các thành viên dữ liệu privateLastName, FirstName, BirthDate và HireDate. Các thành viên BirthDate và HireDate là các đối tượng của lớp Date mà chứa các thành viên dữ liệu private Month, Day và Year. Chương trình khởi tạo một đối tượng Employee, và các khởi tạo và các hiển thị các thành viên dữ liệu của nó. Chú ý về cú pháp của phần đầu trong định nghĩa constructor của lớp Employee:
Employee::Employee(char *FName, char *LName, int BMonth, int BDay, int BYear, int HMonth, int HDay, int HYear) :BirthDate(BMonth, BDay, BYear), HireDate(HMonth, HDay, HYear)
Constructor lấy tám tham số (FName, LName, BMonth, BDay, BYear, HMonth, HDay, và HYear). Dấu hai chấm trong phần đầu phân tách các bộ khởi tạo từ danh sách các tham số. Các bộ khởi tạo định rõ các tham số truyền chon constructor của các đối tượng thành viên. Vì thế BMonth, BDay và BYear được truyền cho constructor của đối tượng BirthDate, và HMonth, HDay, và HYear được truyền cho constructor của đối tượng HireDate. Nhiều bộ khởi tạo được phân tách bởi dấu phẩy.
Chúng ta chạy ví dụ 3.15, kết quả ở hình 3.16
Kết quả của ví dụ 3.15 (Hình 3.16)
Một đối tượng thành viên không cần được khởi tạo thông qua một bộ khởi tạo thành viên. Nếu một bộ khởi tạo thành viên không được cung cấp, constructor mặc định của đối tượng thành viên sẽ được gọi một cách tự động. Các giá trị nếu có thiết lập bởi constructor mặc định thì có thể được ghi đè bởi các hàm set.
Một hàm friend của một lớp được định nghĩa bên ngoài phạm vi của lớp đó, lúc này có quyền truy cập đến các thành viên private hoặc protected của một lớp. Một hàm hay toàn bộ lớp có thể được khai báo là một friend của lớp khác.
Để khai báo một hàm là một friend của một lớp, đứng trước prototype của hàm trong định nghĩa lớp với từ khóa friend. như sau:
friend <function-declarator>;
Để khai báo một lớp là friend của lớp khác như sau:
friend <class-name>;
Ví dụ 3.16: Chương trình sau minh họa khai báo và sử dụng hàm friend.
1: #include <iostream.h> 2: 3: class Count 4: { 5: friend void SetX(Count &, int); //Khai báo friend 6: public: 7: Count()//Constructor 8: { 9: X = 0; 10: } 11: void Print() const //Xuất 12: { 13: cout << X << endl; 14: } 15: private: 16: int X; 17: }; 18: 19: //Có thể thay đổi dữ liệu private của lớp Count vì 20: //SetX() khai báo là một hàm friend của lớp Count 21: void SetX(Count &C, int Val) 22: { 23: C.X = Val; //Hợp lệ: SetX() là một hàm friend của lớp Count 24: } 25: 26: int main() 27: { 28: Count Object; 29: 30: cout << "Object.X after instantiation: "; 31: Object.Print(); 32: cout << "Object.X after call to SetX friend function: "; 33: SetX(Object, 8); //Thiết lập X với một friend 34: Object.Print(); 35: return 0; 36: }
Chúng ta chạy ví dụ 3.16, kết quả ở hình 3.17
Kết quả của ví dụ 3.16 (Hình 3.17)Có thể chỉ định các hàm được đa năng hóa là các friend của lớp. Mỗi hàm được đa năng hóa phải được khai báo tường minh trong định nghĩa lớp như là một friend của lớp.
Khi một hàm thành viên tham chiếu thành viên khác của lớp cho đối tượng cụ thể của lớp đó, làm thế nào C++ bảo đảm rằng đối tượng thích hợp được tham chiếu? Câu trả lời là mỗi đối tượng duy trì một con trỏ trỏ tới chính nó – gọi là con trỏ this – Đó là một tham số ẩn trong tất cả các tham chiếu tới các thành viên bên trong đối tượng đó. Con trỏ this cũng có thể được sử dụng tường minh. Mỗi đối tượng có thể xác định địa chỉ của chính mình bằng cách sử dụng từ khóa this.
Con trỏ this được sử dụng để tham chiếu cả các thành viên dữ liệu và hàm thành viên của một đối tượng. Kiểu của con trỏ this phụ thuộc vào kiểu của đối tượng và trong hàm thành viên con trỏ this được sử dụng là khai báo const. Chẳng hạn, một hàm thành viên không hằng của lớp Employee con trỏ this có kiểu là:
Employee * const //Con trỏ hằng trỏ tới đối tượng Employee
Đối với một hàm thành viên hằng của lớp Employee con trỏ this có kiểu là:
const Employee * const //Con trỏ hằng trỏ tới đối tượng Employee mà là một hằng
Ví dụ 3.17: Chương trình sau minh họa sử dụng tường minh của con trỏ this để cho phép một hàm thành viên của lớp Test in dữ liệu X của một đối tượng Test.
1: #include <iostream.h> 2: 3: class Test 4: { 5: public: 6: Test(int = 0); // Constructor mặc định 7: void Print() const; 8: private: 9: int X; 10: }; 11: 12: Test::Test(int A) 13: { 14: X = A; 15: } 16: 17: void Test::Print() const 18: { 19: cout << " X = " << X << endl 20: << " this->X = " << this->X << endl 21: << "(*this).X = " << (*this).X << endl; 22: } 23: 24: int main() 25: { 26: Test A(12); 27: 28: A.Print(); 29: return 0; 30: }
Chúng ta chạy ví dụ 3.17, kết quả ở hình 3.18
Kết quả của ví dụ 3.17 (Hình 3.18)Một cách khác sử dụng con trỏ this là cho phép móc vào nhau các lời gọi hàm thành viên.
Ví dụ 3.18: Chương trình sau minh họa trả về một tham chiếu tới một đối tượng Time để cho phép các lời gọi hàm thành viên của lớp Time được móc nối vào nhau.
1: #include <iostream.h> 2: 3: class Time 4: { 5: public: 6: Time(int = 0, int = 0, int = 0); // Constructor mặc định 7: // Các hàm set 8: Time &SetTime(int, int, int); // Thiết lập Hour, Minute va Second 9: Time &SetHour(int); // Thiết lập Hour 10: Time &SetMinute(int); // Thiết lập Minute 11: Time &SetSecond(int); // Thiết lập Second 12: // Các hàm get 13: int GetHour() const; // Trả về Hour 14: int GetMinute() const; // Trả về Minute 15: int GetSecond() const; // Trả về Second 16: // Các hàm in 17: void PrintMilitary() const; // In t.gian theo dạng giờ quân đội 18: void PrintStandard() const; // In thời gian theo dạng giờ chuẩn 19: private: 20: int Hour; // 0 - 23 21: int Minute; // 0 - 59 22: int Second; // 0 - 59 23: }; 24: 25: // Constructor khởi động dữ liệu private 26: // Gọi hàm thành viên SetTime() để thiết lập các biến 27: // Các giá trị mặc định là 0 28: Time::Time(int Hr, int Min, int Sec) 29: { 30: SetTime(Hr, Min, Sec); 31: } 32: 33: // Thiết lập các giá trị của Hour, Minute, và Second 34: Time &Time::SetTime(int H, int M, int S) 35: { 36: Hour = (H >= 0 && H < 24) ? H : 0; 37: Minute = (M >= 0 && M < 60) ? M : 0; 38: Second = (S >= 0 && S < 60) ? S : 0; 39: return *this; // Cho phép móc nối 40: } 41: 42: // Thiết lập giá trị của Hour 43: Time &Time::SetHour(int H) 44: { 45: Hour = (H >= 0 && H < 24) ? H : 0; 46: return *this; // Cho phép móc nối 47: } 48: 49: // Thiết lập giá trị của Minute 50: Time &Time::SetMinute(int M) 51: { 52: Minute = (M >= 0 && M < 60) ? M : 0; 53: return *this; // Cho phép móc nối 54: } 55: 56: // Thiết lập giá trị của Second 57: Time &Time::SetSecond(int S) 58: { 59: Second = (S >= 0 && S < 60) ? S : 0; 60: return *this; // Cho phép móc nối 61: } 62: 63: // Lấy giá trị của Hour 64: int Time::GetHour() const 65: { 66: return Hour; 67: } 68: 69: // Lấy giá trị của Minute 70: int Time::GetMinute() const 71: { 72: return Minute; 73: } 74: 75: // Lấy giá trị của Second 76: int Time::GetSecond() const 77: { 78: return Second; 79: } 80: 81: // Hiển thị thời gian dạng giờ quân đội: HH:MM:SS 82: void Time::PrintMilitary() const 83: { 84: cout << (Hour < 10 ? "0" : "") << Hour << ":" 85: << (Minute < 10 ? "0" : "") << Minute << ":" 86: << (Second < 10 ? "0" : "") << Second; 87: } 88: 89: // Hiển thị thời gian dạng chuẩn: HH:MM:SS AM (hay PM) 90: void Time::PrintStandard() const 91: { 92: cout << ((Hour == 0 || Hour == 12) ? 12 : Hour % 12) << ":" 93: << (Minute < 10 ? "0" : "") << Minute << ":" 94: << (Second < 10 ? "0" : "") << Second 95: << (Hour < 12 ? " AM" : " PM"); 96: } 97: 100: int main() 101: { 102: Time T; 103: 104: // Các lời gọi móc nối vào nhau 105: T.SetHour(18).SetMinute(30).SetSecond(22); 106: cout << "Military time: "; 107: T.PrintMilitary(); 108: cout << endl << "Standard time: "; 109: T.PrintStandard(); 110: cout << endl << endl << "New standard time: "; 111: // Các lời gọi móc nối vào nhau 112: T.SetTime(20, 20, 20).PrintStandard(); 113: cout << endl; 114: return 0; 115: }
Các hàm thành viên SetTime(), SetHour(), SetMinute() và SetSecond() mỗi hàm đều trả về *this với kiểu trả về là Time &. Toán tử chấm liên kết từ trái sang phải, vì vậy biểu thức:
T.SetHour(18).SetMinute(30).SetSecond(22);
Đầu tiên gọi T.SetHour(18) thì trả về một tham chiếu tới đối tượng T là giá trị của lời gọi hàm này. Phần còn lại của biểu thức được hiểu như sau:
T.SetMinute(30).SetSecond(22);
T.SetMinute(30) gọi thực hiện và trả về tương đương của T. Phần còn của biểu thức là:
T.SetSecond(22);
Chúng ta chạy ví dụ 3.18, kết quả ở hình 3.19
Kết quả của ví dụ 3.18 (Hình 3.19)
Các đối tượng có thể được cấp phát động giống như các dữ liệu khác bằng toán tử new và delete. Chẳng hạn:
Time *TimePtr = new Time(1,20,26); …………….. delete TimePtr;
Toán tử new tự động gọi hàm constructor ,và toán tử delete tự động gọi destructor.
Bình thường, mỗi đối tượng của một lớp có bản sao chép của chính nó của tất cả các thành viên dữ liệu của lớp. Trong các trường hợp nhất định chỉ có duy nhất một bản chép thành viên dữ liệu đặc biệt cần phải dùng chung bởi tất cả các đối tượng của một lớp. Một thành viên dữ liệu tĩnh được sử dụng cho những điều đó và các lý do khác. Một thành viên dữ liệu tĩnh biểu diễn thông tin toàn lớp (class-wide). Khai báo một thành viên tĩnh bắt đầu với từ khóa static.
Mặc dù các thành viên dữ liệu tĩnh có thể giống như các biến toàn cục, các thành viên dữ liệu tĩnh có phạm vi lớp. Các thành viên tĩnh có thể là public, private hoặc protected. Các thành viên dữ liệu tĩnh phải được khởi tạo một lần (và chỉ một lần) tại phạm vi file. Các thành viên lớp tĩnh public có thể được truy cập thông qua bất kỳ đối tượng nào của lớp đó, hoặc chúng có thể được truy cập thông qua tên lớp sử dụng toán tử định phạm vi. Các thành viên lớp tĩnh private và protected phải được truy cập thông qua các hàm thành viên public của lớp hoặc thông qua các friend của lớp. Các thành viên lớp tĩnh tồn tại ngay cả khi đối tượng của lớp đó không tồn tại. Để truy cập một thành viên lớp tĩnh public khi các đối tượng của lớp không tồn tại, đơn giản thêm vào đầu tên lớp và toán tử định phạm vi cho thành viên dữ liệu. Để truy cập một thành viên lớp tĩnh private hoặc protected khi các đối tượng của lớp không tồn tại, một hàm thành viên public phải được cung cấp và hàm phải được gọi bởi thêm vào đầu tên của nó với tên lớp và toán tử định phạm vi.
Ví dụ 3.19: Chương trình sau minh họa việc sử dụng thành viên dữ liệu tĩnh private và một hàm thành viên tĩnh public.
1: #include <iostream.h> 2: #include <string.h> 3: #include <assert.h> 4: 5: class Employee 6: { 7: public: 8: Employee(const char*, const char*); // Constructor 9: ~Employee(); // Destructor 10: char *GetFirstName() const; // Trả về first name 11: char *GetLastName() const; // Trả về last name 12: // Hàm thành viên tĩnh 13: static int GetCount(); // Trả về số đối tượng khởi tạo 14: private: 15: char *FirstName; 16: char *LastName; 17: // static data member 18: static int Count; // Số đối tượng khởi tạo 19: }; 20: 21: // Khởi tạo thành viên dữ liệu tĩnh 22: int Employee::Count = 0; 23: 24://Định nghĩa hàm thành viên tỉnh mà trả về số đối tượng khởi tạo 25: int Employee::GetCount() 26: { 27: return Count; 28: } 29: 30: // Constructor cấp phát động cho first name và last name 31: Employee::Employee(const char *First, const char *Last) 32: { 33: FirstName = new char[ strlen(First) + 1 ]; 34: assert(FirstName != 0); // Bảo đảm vùng nhớ được cấp phát 35: strcpy(FirstName, First); 36: LastName = new char[ strlen(Last) + 1 ]; 37: assert(LastName != 0); // Bảo đảm vùng nhớ được cấp phát 38: strcpy(LastName, Last); 39: ++Count; // Tăng số đối tượng lên 1 40: cout << "Employee constructor for " << FirstName 41: << ' ' << LastName << " called." << endl; 42: } 43: 44: // Destructor giải phóng vùng nhớ đã cấp phát 45: Employee::~Employee() 46: { 47: cout << "~Employee() called for " << FirstName 48: << ' ' << LastName << endl; 49: delete FirstName; 50: delete LastName; 51: --Count; // Giảm số đối tượng xuống 1 52: } 53: 54: // Trả về first name 55: char *Employee::GetFirstName() const 56: { 57: char *TempPtr = new char[strlen(FirstName) + 1]; 58: assert(TempPtr != 0); // Bảo đảm vùng nhớ được cấp phát 59: strcpy(TempPtr, FirstName); 60: return TempPtr; 61: } 62: 63: // Trả về last name 64: char *Employee::GetLastName() const 65: { 66: char *TempPtr = new char[strlen(LastName) + 1]; 67: assert(TempPtr != 0); // Bảo đảm vùng nhớ được cấp phát 68: strcpy(TempPtr, LastName); 69: return TempPtr; 70: } 71: 72: int main() 73: { 74: cout << "Number of employees before instantiation is " 75: << Employee::GetCount() << endl; // Sử dụng tên lớp 76: Employee *E1Ptr = new Employee("Susan", "Baker"); 77: Employee *E2Ptr = new Employee("Robert", "Jones"); 78: cout << "Number of employees after instantiation is " 79: << E1Ptr->GetCount() << endl; 80: cout << endl << "Employee 1: " 81: << E1Ptr->GetFirstName() 82: << " " << E1Ptr->GetLastName() 83: << endl << "Employee 2: " 84: << E2Ptr->GetFirstName() 85: << " " << E2Ptr->GetLastName() << endl << endl; 86: delete E1Ptr; 87: delete E2Ptr; 88: cout << "Number of employees after deletion is " 89: << Employee::GetCount() << endl; 90: return 0; 91: }
Thành viên dữ liệu Count được khởi tạo là zero ở phạm vi file với lệnh:
int Employee::Count = 0;
Thành viên dữ liệu Count duy trì số các đối tượng của lớp Employee đã được khởi tạo. Khi đối tượng của lớp Employee tồn tại, thành viên Count có thể được tham chiếu thông qua bất kỳ hàm thành viên nào của một đối tượng Employee – trong ví dụ này, Count được tham chiếu bởi cả constructor lẫn destructor. Khi các đối tượng của lớp Employee không tồn tại, thành viên Count có thể vẫn được tham chiếu nhưng chỉ thông qua một lời gọi hàm thành viên tĩnh public GetCount() như sau:
Employee::GetCount()
Hàm GetCount() được sử dụng để xác định số các đối tượng của Employee khởi tạo hiện hành. Chú ý rằng khi không có các đối tượng trong chương trình, lời gọi hàm Employee::GetCount() được đưa ra. Tuy nhiên khi có các đối tượng khởi động hàm GetCount() có thể được gọi thông qua một trong các đối tượng như sau:
E1Ptr->GetCount()
Trong chương trình ở các dòng 34, 37, 58 và 67 sử dụng hàm assert() (định nghĩa trong assert.h). Hàm này kiểm tra giá trị của biểu thức. Nếu giá trị của biểu thức là 0 (false), hàm assert() in một thông báo lỗi và gọi hàm abort() (định nghĩa trong stdlib.h) để kết thúc chương trình thực thi. Nếu biểu thức có giá trị khác 0 (true) thì chương trình tiếp tục. Điều này rất có ích cho công cụ debug đối với việc kiểm tra nếu một biến có giá trị đúng. Chẳng hạn hàm ở dòng 34 hàm assert() kiểm tra con trỏ FirstName để xác định nếu nó không bằng 0 (null). Nếu điều kiện trong khẳng định (assertion) cho trước là đúng, chương trình tiếp tục mà không ngắt. Nếu điều kiện trong khẳng định cho trước là sai, một thông báo lỗi chứa số dòng, điều kiện được kiểm tra, và tên file trong đó sự khẳng định xuất hiện được in, và chương trình kết thúc. Khi đó lập trình viên có thể tập trung vào vùng này của đoạn mã để tìm lỗi.
Các khẳng định không phải xóa từ chương trình khi debug xong. Khi các khẳng định không còn cần thiết cho mục đích debug trong một chương trình, dòng sau:
#define NDEBUG
được thêm vào ở đầu file chương trình. Điều này phát sinh tiền xử lý bỏ qua tất cả các khẳng định thay thế cho lập trình viên xóa mỗi khẳng định bằng tay.
Chúng ta chạy ví dụ 3.19, kết quả ở hình 3.20
Kết quả của ví dụ 3.19 (Hình 3.20)Một hàm thành viên có thể được khai báo là static nếu nó không truy cập đến các thành viên không tĩnh. Không giống như các thành viên không tĩnh, một hàm thành viên tĩnh không có con trỏ this bởi vì các thành viên dữ liệu tĩnh và các hàm thành viên tĩnh tồn tại độc lập với bất kỳ đối tượng nào của lớp.
Chú ý: Hàm thành viên dữ liệu tĩnh không được là const.