Bài giảng lập trình hướng đối tượng - Thầy Cường Học viện bưu chính viễn thông TP HCM
Trang 1Chương 2
Lớp, đối tượng và tính đóng gói
• Lớp
• Hàm tạo, hàm hủy
• Tham số của hàm tạo
• Nội tuyến tự động
• Gán đối tượng
• Truyền các đối tượng sang hàm
• Trả đối tượng từ hàm
• Hàm friend
Trang 3I/ Lớp (class)
• Cú pháp khai báo lớp
class class_name {
private :
// khai báo các biến ;
// khai báo các hàm;
public :
// khai báo các biến ;
// khai báo các hàm;
} objects_list ;
class_name tên của lớp do người dùng định nghiã
objects_list danh sách các đối tượng, có thể tùy chọn
Các biến, các hàm khai báo bên trong một lớp gọi là các thành viên của lớp đó
Từ khoá Ý nghiã
class khai báo một lớp
private : qui định các biến, các hàm là các thành viên riêng của lớp, bên
ngoài lớp không thể truy cập được
public : qui định các biến, các hàm là các thành viên chung của lớp, có
thể truy cập chúng từ các thành viên khác của lớp và bởi các thành phần khác của chương trình có chứa lớp đó
• Cú pháp định nghiã hàm thành viên
data_type class_name :: func_name (arg_list)
{
// body of function
}
data_type kiểu dữ liệu của phương thức trả về
class_name tên lớp chứa hàm
:: toán tử phân giải phạm vi (scope resolution operator)
Trang 4func_name tên hàm
arg_list danh sách các đối số
Ví dụ 1.1 Khai báo lớp có tên "myclass"
myclass ob1, ob2; // khai báo 2 đối tượng có tên ob1, ob2
ob1.set_a(10); // thiết lập giá trị 10 cho bản sao cuả biến a của ob1
ob2.set_a(99); // thiết lập giá trị 99 cho bản sao cuả biến a của ob2
cout << ob1.get_a() << "\n";
cout << ob2.get_a() << "\n";
return 0;
}
Trang 5Ví dụ 1.2 Lỗi biên dịch khi truy cập đến biến riêng a từ bên ngoài lớp myclass
int main()
{
myclass ob1, ob2;
ob1.a = 10; // ERROR! cannot access private member
ob2.a = 99; // by non-member functions
myclass ob1, ob2;
// here, a is accessed directly
Trang 6Ví dụ 1.4 Tạo lớp stack dùng để chứa các ký tự
#include <iostream.h>
#define SIZE 10
// Declare a stack class for characters
class stack {
char stck[SIZE]; // holds the stack
int tos; // index of top-of-stack
public:
void init(); // initialize stack
void push(char ch); // push character on stack
char pop(); // pop character from stack
cout << "Stack is empty";
return 0; // return null on empty stack
Trang 7for(i=0; i<3; i++) cout << "Pop s1: " << s1.pop() << "\n";
for(i=0; i<3; i++) cout << "Pop s2: " << s2.pop() << "\n";
return 0;
}
@ Kết quả xuất dữ liệu của chương trình ?
• Lưu ý
Khai báo lớp là một trừu tượng logic để định nghiã một kiểu dữ liệu mới
Khai báo một đối tượng dựa vào lớp, tạo ra một thực thể vật lý (có điạ chỉ trong bộ nhớ) có kiểu dữ liệu đó
Trang 8Mỗi đối tượng của một lớp có bản sao riêng của các biến được khai báo trong lớp
Bài tập I
1 Hãy tạo lớp card để giữ các mục nhập catalog thẻ thư viện, chưá tựa đề sách (kiểu chuỗi), tên tác giả (kiểu chuỗi) và số bản (kiểu nguyên) Dùng hàm thành viên chung store() để lưu trữ thông tin về sách và hàm thành viên chung show() để hiển thị thông tin Viết chương trình thực hiện yêu cầu trên
2 Tạo lớp hàng đợi (queue) để giữ hàng các số nguyên Tạo một kích thước hàng dài 100 số nguyên Viết chương trình thực hiện yêu cầu trên
II/ Hàm tạo & hàm hủy
1/ Khái niệm
Hàm tạo (constructor) có cùng tên với lớp, là hàm thành phần của một lớp,
không có kiểu trả về
Mục đích của hàm tạo nhằm tạo ra các khởi đầu cho một đối tượng
Hàm tạo được gọi tự động mỗi khi đối tượng của lớp đó được tạo ra
2/ Khai báo
class class_name {
// khai báo các biến và hàm ;
public :
// khai báo các biến và hàm ;
class_name() ; // khai báo hàm tạo
} objects_list ;
Ví dụ 2.1 Lớp myclass có hàm tạo myclass() và hàm show()
#include <iostream.h>
Trang 9Đối với các đối tượng chung, một hàm tạo của đối tượng được gọi một lần khi
chương trình bắt đầu thi hành lần đầu
Đối với các đối tượng riêng, hàm tạo của đối tượng được gọi mỗi khi lệnh khai báo
được thi hành
3/ Khái niệm hàm hủy (destructor)
Hàm hủy có cùng tên với lớp, có kèm theo dấu ~ đứng trước, là hàm thành phần của
một lớp, không có kiểu trả về
Mục đích của hàm hủy nhằm thi hành một số tác động khi đối tượng bị hủy bỏ, chẳng hạn một đối tượng yêu cầu cấp phát bộ nhớ khi đối tượng được tạo ra và giải
Trang 10phóng bộ nhớ khi đối tượng bị hủy bỏ
Hàm hủy được gọi tự động mỗi khi đối tượng của lớp đó bị hủy bỏ
4/ Khai báo
class class_name {
// khai báo các biến và hàm ;
public :
// khai báo các biến và hàm ;
~class_name(); // khai báo hàm hủy
Trang 11• Lưu ý : Không thể biết được địa chỉ của hàm tạo hoặc hàm hủy
Ví dụ 2.3 Dùng hàm tạo stack() để tự động khởi đầu ngăn xếp khi đối tượng được tạo
#include <iostream.h>
#define SIZE 10
// Declare a stack class for characters
class stack {
char stck[SIZE]; // holds the stack
int tos; // index of top-of-stack
public:
stack(); // constructor
void push(char ch); // push character on stack
char pop(); // pop character from stack
};
// Initialize the stack
Trang 12cout << "Stack is empty\n";
return 0; // return null on empty stack
Trang 13s2.push('y');
s1.push('c');
s2.push('z');
for(i=0; i<3; i++) cout << "Pop s1: " << s1.pop() << "\n";
for(i=0; i<3; i++) cout << "Pop s2: " << s2.pop() << "\n";
return 0;
}
Ví dụ 2.4 Dùng hàm tạo strtype() và hàm hủy ~strtype() để tự động cấp phát
bộ nhớ cho chuổi *p và giải phóng bộ nhớ khi đối tượng bị hủy
Trang 15s2.show();
return 0;
}
Ví dụ 2.5 Dùng đối tượng của lớp timer để xác định khoảng thời gian
khi một đối tượng kiểu timer được tạo và cho đến khi bị hủy
Trang 16Chương trình này dùng hàm thư viện chuẩn clock() để trả về số chu kỳ đồng hồ xảy
ra từ khi chương trình bắt đầu chạy Chia giá trị này cho hằng số CLK-TCK để chuyển thành giá trị giây (CLK-TCK định nghiã số tic-tắc của đồng hồ trong một giây)
- Lớp stopwatch có 2 biến riêng start và end lưu số giây
- Viết hàm tạo để đạt thời gian trôi qua lúc đầu về zero
- Hai hàm thành viên start() và stop() để lần lượt mở và tắt chế độ định giờ
- Hàm thành viên show() để hiển thị thời gian trôi qua
- Viết hàm hủy để tự động hiển thị thời gian trôi qua khi đối tượng stopwatch bị hủy
3 Sửa lỗi trong đoạn chương trình sau :
Trang 171/ Khái niệm
Hàm tạo có thể có các tham số, chỉ cần bổ sung các tham số thích hợp trong khai báo lớp và trong định nghĩa hàm tạo
Khi khai báo một đối tượng, cần chỉ rõ các tham số này làm đối số
Ví dụ 3.1 Hàm tạo myclass(int x) có một tham số
Trang 18• Lưu ý hàm hủy không có tham số Do không có cơ chế nào để truyền đối số cho một đối tượng bị hủy
Ví dụ 3.2 Có thể truyền nhiều tham số cho hàm tạo
Trang 19#include <iostream.h>
#define SIZE 10
// Declare a stack class for characters
class stack {
char stck[SIZE]; // holds the stack
int tos; // index of top-of-stack
char who; // identifies stack
public:
stack(char c); // constructor
void push(char ch); // push character on stack
char pop(); // pop character from stack
Trang 20cout << "Stack " << who << " is empty\n";
return 0; // return null on empty stack
// This will generate some error messages
for(i=0; i<5; i++) cout << "Pop s1: " << s1.pop() << "\n";
for(i=0; i<5; i++) cout << "Pop s2: " << s2.pop() << "\n";
Trang 22// use variables to construct ob
myclass ob(x, y);
ob.show();
return 0;}
Trang 23Bài tập III
1 Viết chương trình thay đổi lớp stack để cho nó cấp phát bộ nhớ động cho ngăn xếp chứa các ký tự Kích thước ngăn xếp được chỉ rõ bằng một tham số với hàm tạo Hàm hủy giải phóng bộ nhớ động
2 Hãy tạo lớp t_and_d để truyền ngày và giờ hệ thống hiện hành như một tham số
cho hàm tạo của nó khi được tạo ra Lớp gồm có hàm thành viên hiển thị ngày giờ này lên màn hình Dùng các hàm ngày và giờ chuẩn trong thư viện chuẩn để tìm và hiện thị ngày
3 Viết chương trình tạo lớp box có hàm tạo được truyền 3 giá trị double, diễn tả độ dài các cạnh của hộp Hãy cho lớp box tính thể tích của hình lập phương và lưu trữ kết qủa trong biến double Tạo hàm thành viên vol() để hiển thị thể tích của mỗi đối tượng box
IV/ Nội tuyến tự động (in-line)
1/ Khái niệm
Khi định nghiã hàm thành phần là đủ ngắn thì có thể đặt định nghiã trong khai báo
hàm trong lớp đó Hàm này sẽ được gọi là hàm nội tuyến (in-line func.) và không
cần dùng từ khoá inline đứng trước hàm đó
// divisible() is defined here and automatically in-lined
int divisible() { return !(i%j) ; }
};
Trang 24samp(int a, int b) { i = a; j = b; } // inline constructor
int divisible() { return !(i%j); }
};
Ví dụ 4.3 Một hàm ngắn ở bên trong một khai báo lớp
cho dù đặc điểm nội tuyến tự động là không có giá trị
class myclass {
int i;
public:
myclass(int n) { i = n; }
Trang 25void show() { cout << i; }
Một đối tượng được gán cho một đối tượng khác để cả hai đối tượng có cùng kiểu,
khi đó sự sao chép từng bit của các thành viên dữ liệu được thực hiện
void set(int i, int j) { a = i; b = j; }
void show() { cout << a << ' ' << b << "\n"; }
Trang 26void set(int i, int j) { a = i; b = j; }
void show() { cout << a << ' ' << b << "\n"; }
void set(int i, int j) { a = i; b = j; }
void show() { cout << a << ' ' << b << "\n"; }
};
Trang 27char stck[SIZE]; // holds the stack
int tos; // index of top-of-stack
public:
stack(); // constructor
void push(char ch); // push character on stack
char pop(); // pop character from stack
Trang 28cout << "Stack is empty\n";
return 0; // return null on empty stack
s2 = s1; // now s1 and s2 are identical
for(i=0; i<3; i++) cout << "Pop s1: " << s1.pop() << "\n";
for(i=0; i<3; i++) cout << "Pop s2: " << s2.pop() << "\n";
Trang 29return 0;
}
c/ Đối với các đối tượng có sử dụng chuổi, khi thực hiện phép gán đối tượng phải
chắc chắn rằng không hủy bỏ các thông tin cần thiết trong đối tượng đó
Trang 31VI/ Truyền các đối tượng sang hàm
1/ Việc truyền các đối tượng cho hàm giống như truyền các đối số thông thường
Tham số của hàm có kiểu dữ liệu là kiểu lớp, và đối số truyền cho hàm chính là đối tượng
Giống như các kiểu dữ liệu khác, theo ngầm định, tất cả các đối tượng được truyền bởi giá trị cho một hàm
Ví dụ 6.1 Truyền một đối tượng cho hàm
#include <iostream.h>
class samp {
int i;
public:
Trang 33however
void sqr_it(samp o)
{
o.set_i( o.get_i() * o.get_i() ) ;
cout << "Copy of a has i value of " << o.get_i();
sqr_it(a) ; // a passed by value, displays 100
cout << "But, a.i is unchanged in main: ";
cout << a.get_i(); // displays 10
/* Set o.i to its square This affects the calling argument */
void sqr_it(samp *o)
{
Trang 34sqr_it(&a); // pass a's address to sqr_it(), displays 100
cout << "Now, a in main() has been changed: ";
cout << a.get_i(); // displays 100
return 0;
}
4/ Bản sao của đối tượng
Khi truyền một đối tượng cho hàm, một bản sao của đối tượng được thực hiện, có nghĩa là một đối tượng mới xuất hiện Do đó khi hàm kết thúc làm việc, bản sao của đối tượng đó (đối số của hàm) sẽ bị hủy
Điều này làm nảy sinh hai vấn đề :
- Hàm tạo của đối tượng được gọi khi bản sao thực hiện ?
- Hàm hủy của đối tượng được gọi khi bản sao bị hủy ?
Khi bản sao của một đối tượng được thực hiện để dùng gọi hàm, thì hàm tạo không được gọi Do hàm tạo thường được dùng để khởi đầu một khiá cạnh nào đó của đối
tượng Khi truyền một đối tượng cho một hàm, bạn cần đến trạng thái hiện hành của đối tượng chứ không phải trạng thái ban đầu
Khi hàm kết thúc và bản sao bị hủy, hàm hủy được gọi
Ví dụ 6.4
#include <iostream.h>
class samp {
int i;
Trang 35~samp() { cout << "Destructing\n"; }
int get_i() { return i; }
Hàm hủy của bản sao đối tượng được gọi khi hàm kết thúc có thể là nguyên nhân của
nhiều vấn đề
Chẳng hạn, nếu đối tượng có hàm tạo cấp phát bộ nhớ động hoặc hàm hủy giải phóng bộ nhớ động, thì bản sao của đối tượng sẽ giải phóng bộ nhớ động khi hàm
Trang 36hủy của nó được gọi Điều này làm cho đối tượng gốc bị hỏng và trở thành vô dụng
~dyna() { free(p); cout << "freeing \n"; }
int get() { return *p; }
// Return negative value of *ob.p
int neg(dyna ob)
Trang 37cout << neg(o) << "\n"; // freeing
// 10 dyna o2(20);
cout << o2.get() << "\n"; // 20
cout << neg(o2) << "\n"; // freeing
// -20 cout << o.get() << "\n"; // 20 do *p được cấp địa chỉ vùng nhớ động
cout << neg(o) << "\n"; // freeing
// -20 return 0; // freeing o
// Null pointer assignment
}
Giải thích nguyên nhân gây lổi ?
• Có thể khắc phục bằng cách truyền điạ chỉ của đối tượng cho hàm Vì sẽ không có đối tượng mới được tạo ra và không có hàm hủy của bản sao đối tượng được gọi khi hàm trả về
Tuy nhiên, giải pháp tốt nhất là sử dụng hàm tạo bản sao (copy constructor), cho
phép định nghiã cách thức tạo các bản sao của các đối tượng (xem chương sau)
Bài tập VI
1 Viết chương trình tạo lớp stack trong ví dụ 5.3 chương 2, bổ sung hàm showstack() để truyền một đối tượng kiểu stack Cho hàm này hiển thị nội dung của ngăn xếp
2 Tìm lỗi sai trong chương trình này
Trang 38dyna(int i);
~dyna() { free(p); cout << "freeing \n"; }
int get() { return *p; }
// Return negative value of *ob.p
int neg(dyna ob)
Trang 39}
VII/ Trả đối tượng từ hàm
• Đối tượng trả về từ hàm thông qua câu lệnh return Khi đó một đối tượng tạm
được tự động tạo ra để giữ giá trị trả về
Chính đối tượng này thực sự được trả về bởi hàm Sau khi giá trị được trả về, đối tượng này bị hủy, sự hủy đối tượng tạm có thể gây ra tác dụng ngoài ý muốn
void show() { cout << s << "\n"; }
void set(char *str) { strcpy(s, str); }
Trang 40int main()
{
samp ob;
// assign returned object to ob
ob = input(); // Enter a string: Hello ↵
~samp() { if(s) free(s); cout << "Freeing s\n"; }
void show() { cout << s << "\n"; }
Trang 41// Return an object of type samp
// assign returned object to ob
ob = input(); // This causes an error!!!!
ob.show();
return 0;
}
Hãy giải thích kết quả của chương trình ?
Enter a astring: Hello
Hello
Null pointer assignment
Khi một đối tượng được trả về từ một hàm, một đối tượng tạm được dùng để thực
hiện sự trả về sẽ làm hủy bộ nhớ (được cấp phát cho chuổi s) của đối tượng được gọi Để tránh tình trạng này có thể sử dụng hàm tạo bản sao
Trang 42Bài tập VII
1 Viết chương trình tạo lớp Who, cho hàm tạo của Who nhận một đối số ký tự mà đối số này được dùng để nhận dạng một đối tượng
Hãy cho hàm tạo hiển thị một thông báo tương tự thông báo sau đây khi tạo ra một đối tượng :
Constructing who #x Trong đó x là ký tự nhận dạng
Hãy tạo hàm make_who() để trả về đối tượng Who Hãy cho mỗi đối tượng một tên duy nhất
VIII/ Các hàm friend
• Một hàm friend không phải là hàm thành viên của lớp nhưng có thể truy cập các thành viên riêng của lớp
Một hàm friend được định nghiã như một hàm bình thường Nếu nằm bên trong một lớp, hàm friend sẽ có từ khoá friend đứng trước
Hàm friend không được kế thừa từ lớp dẫn xuất (xem chương 6)
• Các lợi ích khi dùng hàm friend :
+ sự quá tải toán tử (xem chương 5)
+ tạo ra các kiểu hàm nhập/xuất nào đó (xem chương 8)
+ tạo hàm để truy cập các thành viên riêng của hai hay nhiều lớp khác nhau