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 4
Quá tải hàm
• Quá tải hàm tạo
• Hàm tạo bản sao
• Hàm với các đối số mặc định
• Tính không xác định khi quá tải hàm
• Điạ chỉ của hàm quá tải
Trang 3I/ Quá tải hàm tạo (constructor overloading )
Có thể quá tải hàm tạo của một lớp, nhưng không quá tải hàm hủy
Hàm tạo của lớp phải phù hợp với cách mà đối tượng của lớp đó được khai báo Nếu không lỗi thời gian biên dịch sẽ xảy ra
Có 3 lý do cần quá tải hàm tạo :
+ để có tính linh hoạt
+ để hổ trợ mảng
+ để tạo các hàm tạo bản sao
Hạn chế : nếu thực hiện quá tải nhiều lần có thể tạo ra tác dụng hủy hoại trên lớp
• Quá tải hàm tạo với khởi đầu một đối tượng hoặc không khởi đầu đối tượng
myclass o1(10); // declare with initial value
myclass o2; // declare without initializer
cout << "o1: " << o1.getx() << '\n';
cout << "o2: " << o2.getx() << '\n';
return 0;
}
Trang 4• Quá tải hàm tạo để cho các đối tượng riêng lẽ lẫn các mảng đối tượng xảy ra trong chương trình
myclass o2[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // declare with initializers
int i;
for(i=0; i<10; i++) {
cout << "o1[" << i << "]: " << o1[i].getx();
Trang 6• Quá tải hàm tạo khi mảng động của lớp được cấp phát Điều này giúp giải quyết hạn chế trong chương 3, phần IV/2, một mảng động không thể được khởi đầu, xem ví dụ 4.6
myclass ob(10); // initialize single variable
p = new myclass[10]; // can't use initializers here
// initialize all elements to ob
for(i=0; i<10; i++) p[i] = ob;
for(i=0; i<10; i++) {
cout << "p[" << i << "]: " << p[i].getx();
cout << '\n';
}
Trang 7Thực hiện việc kiểm tra giới hạn biên cần thiết và chứng tỏ các hàm tạo sẽ hoạt động qua một chương trình ngắn
class strtype {
char *p;
int len;
public:
char *getstring() { return p; }
int getlength() { return len; }
Hàm tạo bản sao là kiểu đặc biệt của hàm tạo được quá tải
Khi một đối tượng được truyền cho một hàm, bản sao từng bit một của đối tượng đó được tạo ra và được truyền cho tham số của hàm để nhận đối tượng
Trang 8Tuy nhiên, có những trường hợp trong đó bản sao đồng nhất là như không mong muốn
Chẳng hạn, nếu đối tượng có con trỏ tới bộ nhớ được cấp phát, thì bản sao sẽ trỏ tới
cùng bộ nhớ như đối tượng gốc đã làm Do đó, nếu bản sao tạo ra sự thay đổi cho nội dung bộ nhớ thì nó cũng sẽ được thay đổi đối với đối tượng gốc Khi một hàm kết thúc, bản sao sẽ bị hủy và hàm hủy của nó được gọi Điều này dẫn đến những tác dụng không mong muốn làm ảnh hưởng đến đối tượng gốc
Khi một đối tượng được trả về từ một hàm tình trạng tương tự cũng sẽ xảy ra Trình
biên dịch sẽ tạo ra một đối tượng tạm để giữ bản sao của giá trị do hàm trả về Đối
tượng tạm này sẽ ra khỏi phạm vi một khi giá trị được trả về cho thủ tục gọi, khiến hàm hủy của đối tượng tạm được gọi Nếu hàm hủy hủy bỏ thứ gì cần cho thủ tục gọi, chẳng hạn nó giải phóng bộ nhớ cấp phát động, thì rắc rối sẽ xảy ra
Như vậy, cốt lõi của vấn đề trên là bản sao từng bit của đối tượng được tạo ra và
thực hiện Để ngăn chặn vấn đề này, lập trình viên cần xác định chính xác những gì xảy ra khi bản sao của một đối tượng được thực hiện để tránh được những tác dụng không mong muốn
Hàm tạo bản sao sẽ giải quyết được vấn đề trên Khi định nghĩa hàm tạo bản sao, lập trình viên có thể hoàn toàn kiểm soát chính xác những gì xảy ra khi bản sao của một đối tượng được thực hiện
• Cần phân biệt hai trường hợp trong đó giá trị của một đối tượng được truyền cho đối tượng khác :
+ Trường hợp thứ nhất là phép gán
+ Trường hợp thứ hai là sự khởi đầu, có thể xảy ra theo 3 cách :
- Khi một đối tượng được dùng để khởi đầu một đối tượng khác trong
câu lệnh khai báo
- Khi một đối tượng được truyền như tham số cho hàm
- Khi một đối tượng tạm được tạo ra dùng để làm giá trị trả về bởi một hàm
Hàm tạo bản sao chỉ áp dụng cho sự khởi đầu Nó không áp dụng cho phép gán
Hàm tạo bản sao không ảnh hưởng đến các phép gán
Trang 92/ Cú pháp
• Dạng tổng quát của hàm tạo bản sao
classname(const classname &obj) {
// body of constructor
}
obj là một tham chiếu tới một đối tượng được dùng để khởi đầu một đối tượng khác
• Dạng mở rộng của hàm tạo bản sao (có nhiều đối số)
classname(const classname &obj, int x = 0) {
// body of constructor }
Đối số thứ nhất là một tham chiếu tới đối tượng được sao chép, và các đối tượng khác đều mặc định Tính linh hoạt này cho phép tạo ra các hàm tạo bản sao có những công dụng khác
Ví dụ, lớp được gọi là myclass và y là đối tượng của myclass thì các lệnh sau đây sẽ dùng đến hàm tạo bản sao của myclass :
myclass x = y; // y explicitly initializing x
func1(y); // y passed as a parameter
y = func2(); // y receiving a returned object
Trong hai trường hợp đầu, một tham chiếu tới y sẽ được truyền cho hàm tạo bản sao Trường hợp thứ ba, một tham chiếu tới đối tượng được trả về bởi func2() sẽ được truyền cho hàm tạo bản sao
• Tạo một mảng số nguyên "an toàn" có kiểm tra giới hạn biên
Ví dụ 2.1
Trang 10/* This program creates a "safe" array class Since space for the array is dynamically allocated, a copy constructor is provided to allocate memory when one array object is used to initialize another */
array(const array &a);
void put(int i, int j) {
if(i>=0 && i<size) p[i] = j;
array::array(const array &a) {
Trang 11int i;
size = a.size;
p = new int[a.size]; // allocate memory for copy
if(!p) exit(1);
for(i=0; i<a.size; i++) p[i] = a.p[i]; // copy contents
cout << "Using copy constructor\n";
// put some values into the array
for(i=0; i<10; i++) num.put(i, i);
// display num
for(i=9; i>=0; i ) cout << num.get(i);
cout << "\n";
// create another array and initialize with num
array x = num; // this invokes copy constructor
// display x
for(i=0; i<10; i++) cout << x.get(i);
return 0;
}
@ Có nhận xét gì về kết quả khi chương trình này không sử dụng hàm tạo bản sao ?
@ Những câu lệnh sau không thể gọi được hàm tạo bản sao, tại sao ?
array a(10);
array b(10);
b = a; // does not call copy constructor
Trang 12• Hàm tạo bản sao giúp ngăn ngừa một số vấn đề liên quan đến việc truyền các kiểu đối tượng nào đó cho hàm
Trang 13@ Hãy tìm nguyên nhân gây ra lỗi ?
Ví dụ 2.3 Hiệu chỉnh chương trình trong ví dụ 2.2
/* This program uses a copy constructor to allow strtype objects to be passed to functions */
strtype(const strtype &o); // copy constructor
~strtype() { delete [] p; } // destructor
char *get() { return p; }
Trang 162 Tìm lỗi sai trong chương trình và yêu cầu chỉnh lý ?
cout << getval(a) << " " << getval(b) << "\n";
cout << getval(a) << " " << getval(b);
return 0;
}
Trang 17III/ Sử dụng các đối số mặc định (default argument)
• Đối số mặc định cho phép gán một giá trị mặc định cho một tham số khi không có đối số tương ứng được chỉ rõ khi hàm được gọi
Đối số mặc định liên quan đến sự quá tải hàm, sử dụng các đối số mặc định chủ
yếu là dạng ngắn của quá tải hàm
• Để gán một đối số mặc định cho một tham số thì sau tham số phải có dấu bằng
và một giá trị muốn mặc định nếu không có đối số tương ứng khi hàm được gọi
Hàm f() cho hai giá trị mặc định 0 đối với các tham số
void f(int a = 0, int b = 0);
Cú pháp này tương tự như khởi đầu một biến
Trang 18Hàm f() có thể gọi theo 3 cách :
+ nó được gọi vơí cả hai đối số được chỉ rõ
+ nó được gọi chỉ vơí đối số thứ nhất được chỉ rõ Trường hợp này, b sẽ mặc định về
0
+ f() có thể được gọi là không có đối số , a và b mặc định về 0
nghiã là các câu lệnh sau đều đúng :
f() ; // a and b default to 0
f(10) ; // a is 10, b defaults to 0
f(10, 99) ; // a is 10, b is 99
Lưu ý, trong ví dụ này, không có cách để mặc định a và chỉ rõ b
• Khi đối số mặc định đầu tiên được chỉ rõ, tất cả các tham số theo sau cũng phải có mặc định Ví dụ một phiên bản khác của f() sau đây sẽ tạo ra lỗi thời gian biên dịch :
void f(int a = 0, int b) // wrong! b must have default, too
Khi tạo một hàm có một hay nhiều đối số mặc định thì những đối số đó phải
chỉ rõ một lần hoặc trong định nghiã của hàm hoặc trong nguyên mẫu của nó chứ không được cả hai
Qui tắc này áp dụng ngay cả khi chỉ nhân đôi các ngầm định giống nhau
• Tất cả các tham số mặc định phải ở bên phải của các tham số không có mặc
định Hơn nữa, khi bắt đầu định nghĩa các tham số mặc định, không thể chỉ định các tham số không có mặc định
• Với các đối số mặc định, chúng phải là các hằng hoặc là các biến toàn cục
Trang 19Ví dụ 3.2 Các đối số mặc định liên quan đến sự quá tải hàm
// Compute area of a rectangle using overloaded functions
#include <iostream.h>
// Return area of a non-square rectangle
double rect_area(double length, double width)
{
return length * width;
}
// Return area of a square
double rect_area(double length)
// Compute area of a rectangle using default arguments
#include <iostream.h>
// Return area of a rectangle
Trang 20double rect_area(double length, double width = 0)
{
if(!width) width = length;
return length * width;
@ Như vậy, các đối số ngầm định thường đưa ra cách đơn giản để quá tải hàm
• Trong phần trước, một hàm tạo được quá tải chiû cho phép tạo ra các đối tượng
được khởi đầu vẫn không được khởi đầu Trong nhiều trường hợp, có thể tránh
quá tải một hàm tạo bằng cách cho nó một hay nhiều đối số ngầm định
myclass o1(10); // declare with initial value
myclass o2; // declare without initializer
Trang 21cout << "o1: " << o1.getx() << '\n';
cout << "o2: " << o2.getx() << '\n';
return 0;
}
@ Như vậy, bằng cách cho biến n giá trị mặc định zero, có thể tạo ra các đối tượng
có các giá trị đầu rõ ràng và các đối tượng có giá trị mặc định là đủ
• Đối số mặc định được tìm thấy khi một tham số được dùng để chọn một tùy chọn
Ví dụ 3.5
#include <iostream.h>
#include <ctype.h>
const int ignore = 0;
const int upper = 1;
const int lower = 2;
void print(char *s, int how = -1) ;
int main()
{
print("Hello There\n", ignore);
print("Hello There\n", upper);
print("Hello There\n"); // continue in upper
print("Hello there\n", lower);
print("That's all\n"); // continue in lower
Trang 22// reuse old case if none specified
if(how < 0) how = oldcase;
Trang 23Bài tập III
1 Tìm lỗi sai trong nguyên mẫu hàm sau :
char *f(char *p, int x = 0 , char *q) ;
2 Tìm lỗi sai trong nguyên mẫu dùng đối số mặc định sau :
int f(int count, int max = count) ;
3 Trong thư viện chuẩn C++ có hàm strtol(), có nguyên mẫu sau :
long strtol(const char *start, const **end, int base);
Hàm này chuyển đổi chuỗi số được trỏ tới bởi start thành số nguyên dài
Hãy tạo hàm mystrtol() hoạt động giống như strtol() với ngoại lệ là base được cho một đối số mặc định là 10 Viết chương trình chứng tỏ hàm mystrtol() hoạt động tốt
4 Hãy tạo hàm myclreol() để xoá dòng từ vị trí con trỏ hiện tại đến cuối dòng Hàm này có một tham số chỉ rõ vị trí của ký tự cần xoá Nếu không chỉ rõ tham số thì toàn bộ dòng sẽ tự động bị xoá Ngược lại, chỉ xoá những vị trí nào được chỉ rõ bởi tham số
IV/ Sự quá tải và tính không xác định (ambiguity)
Trang 24Khi quá tải hàm, có thể dẫn đến tính không xác định trong chương trình
Tính không xác định do quá tải gây ra có thể được đưa vào thông qua :
+ các chuyển đổi kiểu
+ các tham số tham chiếu
+ các đối số mặc định
+ hoặc do chính các hàm được quá tải
+ hoặc do cách gọi các hàm quá tải
Tính không xác định phải được loại bỏ trước khi chương trình được biên dịch
• Tính không xác định gây ra bởi các quy tắc chuyển đổi kiểu tự động trong C++
cout << f(x); // unambiguous - use f(float)
cout << f(y); // unambiguous - use f(double)
Trang 25cout << f(10); // ambiguous, convert 10 to double or float??
return 0;
}
@ Lỗi biên dịch có dạng Error : Ambiguity between 'f(float)' and 'f(double)'
@ Ví dụ trên minh họa tính không xác định có thể xảy ra khi một hàm quá tải được gọi
• Khi hàm được gọi với kiểu đối số sai, các quy tắc chuyển đổi tự động của C++
gây ra tình trạng không xác định Bản thân sự quá tải hàm ở ví dụ này tự nó không có tính không xác định
Trang 26• Tính không xác định xảy ra do quá tải các hàm, trong đó có sự khác biệt duy nhất là một hàm sử dụng một tham số tham chiếu và hàm kia sử dụng tham số mặc định gọi bằng giá trị
// this is inherently ambiguous
int f(int a, int &b)
@ Lỗi biên dịch ở hàm f(int a, int &b) có dạng
Error : 'f(int, int &)' cannot be distinguished from 'f(int, int)'
Trang 27• Tính không xác định xảy ra do quá tải hàm, trong đó có một hay nhiều hàm được quá tải dùng một đối số mặc định
cout << f(10, 2); // calls f(int, int)
cout << f(10); // ambiguous - call f(int) or f(int, int)???
return 0;
}
Trang 28V/ Tìm điạ chỉ của một hàm quá tải
Trong ngôn ngữ C, để biết điạ chỉ một hàm, dùng con trỏ p trỏ đến hàm đó, chẳng hạn
p = zap; // với hàm zap()
Trong ngôn ngữ C++, vấn đề hơi phức tạp hơn bởi vì hàm có thể được quá tải
Cơ chế : dùng cách khai báo con trỏ xác định điạ chỉ của hàm quá tải nào sẽ thu
// Output count number of spaces
void space(int count)
{
for( ; count; count ) cout << ' ';
}
// Output count number of chs
void space(int count, char ch)
Trang 29// and one character parameter
void (*fp2)(int, char);
fp1 = space; // gets address of space(int)
fp2 = space; // gets address of space(int, char)
1 Cho hai hàm quá tải Tìm điạ chỉ của mỗi hàm
int dif(int a, int b)
Trang 30Bài tập chương 4
1 Hãy quá tải hàm tạo date() trong ví dụ 1.3, chương 4 để cho nó nhận một tham số kiểu time_t
2 Tìm lỗi sai trong chương trình sau :
4 Tìm lỗi sai trong đoạn chương trình sau :
void compute(double *num, int divisor=1);
void compute(double *num);
//
compute(&x);
5 Hãy tạo hàm order() để nhận hai tham số tham chiếu nguyên Nếu đối số thứ nhất lớn hơn đối số thứ hai, hãy đảo ngược hai đối số Ngược lại, không tác động nào Ví dụ
Trang 31int x=1, y=0;
order(x, y);
6 Tại sao hai hàm quá tải sau vốn không xác định ?
int f(int a);
int f(int &a);
7 Cho lớp sau, hãy bổ sung các hàm tạo cần thiết để cho cả hai khai báo trong main() đều đúng
samp ob(88); // init ob's a to 88
samp obarray[10]; // noninitialized 10-element array
Trang 3210 Cho lớp sau đây, có thể cấp phát động một mảng các đối tượng này không ?