You are on page 1of 43

CHƯƠNG 2.

LỚP VÀ ĐỐI TƯỢNG (CLASS & OBJECT)

Mục đích của chương


Trong chương này trình bày các vấn đề sau:
 Khai báo và sử dụng 1 lớp
 Khai báo và sử dụng đối tượng, mảng đối tượng, con trỏ đối tượng, tham chiếu đối
tượng
 Hàm thiết lập và hàm hủy bỏ
 Các hàm trực tuyến
 Hàm bạn
2.1 Khai báo lớp
Lớp là khái niệm trung tâm của lập trình hướng đối tượng. Nó là tập hợp các đối
tượng có cùng thuộc tính và hành vi. Một lớp bao gồm các thuộc tính (các thành phần dữ
liệu) và các hàm thành phần (các phương thức). Sau đây là dạng tổng quát định nghĩa 1
lớp:

class <tên_lớp>
{
[ phạm vi truy xuất : ]
<khai báo các thành phần dữ liệu (thuộc tính)>
[ phạm vi truy xuất : ]
<khai báo các hàm thành phần (phương thức)>
};

Trong đó: Tên lớp: Tên lớp do người lập trình tự đặt theo quy cách đặt tên trong C+
+ (Tên trong C++ bắt đầu bằng dấu gạch thấp hoặc ký tự, không chứa dấu cách, không
chứa ký tự đặc biệt, không trùng với từ khóa trong C++).
2.2 Khai báo các thành phần của lớp
2.2.1 Phạm vi truy xuất
Khi xây dựng một class, chắc chắn bạn sẽ phải xác định phạm vi truy cập cho các
thuộc tính và phương thức trong class đó. Mục đích của việc này nhằm quy định các
thành phần nào có thể được truy cập, thay đổi từ bên ngoài, thành phần nào là riêng tư.
Có thể hiểu phạm vi truy xuất này cũng giống như biến toàn cục và biến cục bộ.
Biến toàn cục có thể được truy cập từ tất cả các hàm sau khai báo nó, còn biến cục bộ chỉ
có thể được truy cập nội bộ trong hàm.
Trong C++, phạm vi truy cập được xác định qua 3 từ khóa: public, private và
protected.
- public: Các thành phần mang thuộc tính này đều có thể được truy cập từ bất kỳ
hàm nào, dù ở trong hay ngoài lớp.
- private: Các thành phần mang thuộc tính này chỉ có thể được truy cập bên trong
phạm vi lớp. Vì trong C++ cho phép định nghĩa phương thức ngoài khai báo lớp
nên phạm vi lớp được hiểu là bên trong khai báo lớp và bên trong các định nghĩa
thuộc lớp.
- protected: Các thành phần mang thuộc tính này chỉ có thể được truy cập bên trong
phạm vi lớp và các lớp con kế thừa nó. Như vậy, nếu một lớp không có lớp con kế
thừa nó thì phạm vi protected cũng giống như private.
Một ngoại lệ chỉ có trong C++ đó là định nghĩa friend. Một hàm hoặc lớp friend
có thể truy cập vào các thành phần private và protected của lớp với hàm đó (hàm friend)
hoặc với các đối tượng khác (lớp friend) với điều kiện phải được khai báo trước trong
lớp.
Một số lưu ý về phạm vi truy xuất trong C++:
- Phạm vi truy xuất trong C++ được xác định trong qua các nhãn trong khai báo lớp.
Nhãn bao gồm từ khóa và dấu hai chấm.
- Mỗi nhãn có phạm vi ảnh hưởng từ lúc khai báo đến khi gặp nhãn khác hoặc hết
khai báo lớp.
- Nếu không chỉ rõ nhãn đầu tiên thì ngầm định nó có phạm vi truy cập là private.
2.2.2 Các thành phần dữ liệu (thuộc tính) của lớp
Các thành phần dữ liệu (thuộc tính) của lớp: Có thể là các biến, mảng, con trỏ có
kiểu chuẩn (int, float, char, char*, long, double,…) hoặc kiểu ngoài chuẩn đã định nghĩa
trước (cấu trúc, hợp, lớp,…). Thuộc tính của lớp không thể có kiểu của chính lớp đó,
nhưng có thể là kiểu con trỏ lớp này, ví dụ:
class A
{ …
A x ; // Không cho phép, vì x có kiểu lớp A
A *p ; // Cho phép, vì p là con trỏ kiểu lớp A

};
Khai báo thành phần dữ liệu: Tương tự như khai báo biến , cú pháp:
<kiểu dữ liệu > <tên_thành_phần>;
Lưu ý:
- Trong các khai báo của các thuộc tính, có thể sử dụng từ khóa static nhưng không
được sử dụng các từ khóa auto, register, extern trong khai báo các thuộc tính.
- Thành phần dữ liệu không được khởi tạo giá trị ban đầu.
Các thành phần dữ liệu của lớp thường (không bắt buộc) khai báo là private để
đảm bao tính giấu kín, bảo vệ an toàn dữ liệu của lớp, không cho phép các hàm bên ngoài
xâm nhập vào dữ liệu của lớp.
2.2.3 Các thành phần hàm (phương thức của lớp)
Các phương thức của lớp: Có thể được được định ngay nghĩa bên trong lớp hoặc có
thể định nghĩa ngoài định nghĩa lớp. Khi định nghĩa bên ngoài lớp phải khai báo phương
thức đó trong lớp.
Các phương thức thường được khai báo là public để chúng có thể được gọi tới từ
các hàm khác trong chương trình. Các phương thức trong lớp có thể được định nghĩa theo
2 cách như sau:
- Cách 1: Định nghĩa phương thức ngay trong lớp. Khi định nghĩa phương thức ngay
trong lớp chúng ta định nghĩa giống như hàm thông thường.
- Cách 2: Khai báo phương thức trong lớp và định nghĩa phương thức ngoài lớp. Khai
báo phương thức trong lớp vẫn khai báo theo cú pháp hàm thông thường.
<kiểu_trả_về> <tên_phương_thức> ([đối_số]) ;
Riêng việc định nghĩa phương thức ngoài lớp phải tuân theo cú pháp sau:
<kiểu_trả_về> tên_lớp :: <tên_phương_thức> ([đối_số])
{
// <thân phương thức>
}
Và dù phương thức được định nghĩa trong lớp hay ngoài lớp thì đều có khả năng
truy xuất đến các thành phần khác trong lớp là như nhau.
Trong thân phương thức của 1 lớp (giả sử lớp A) có thể sử dụng:
- Các thuộc tính của lớp A
- Các phương thức của lớp A
- Các hàm tự do trong chương trình, vì phạm vi của các hàm tự do là toàn chương
trình
Giá trị trả về của phương thức có thể có kiểu bất kỳ, gồm cả kiểu chuẩn và kiểu
ngoài chuẩn.
Khai báo lớp hình chữ nhật gồm có 2 thành phần dữ liệu là chiều dài d và chiều rộng r,
các phương thức trong lớp gồm nhập, in, tính chu vi, tính diện tích của hình chữ nhật.
class HCN
{
private:
float d,r;
public:
void nhap()
{
cout<<“nhap chieu dai: ”; cin>>d;
cout<<“nhap chieu rong: ”; cin>>r;
}
void inthongtin()
{
cout<<”(”<<d<<”,”<<r<<”)”;
}

float tinhchuvi()
{
return 2*(d+r);
}
float tinhdientich()
{
return d*r ;
}
};
Có thể khai báo các phương thức trong lớp và định nghĩa các phương thức ngoài lớp
như sau:
class HCN
{
private:
float d,r;
public:
void nhap() ;
void inthongtin(HCN h);
float tinhchuvi();
float tinhdientich();
};
void HCN :: nhap()
{
cout<<“nhap chieu dai: ”; cin>>d;
cout<<“nhap chieu rong: ”; cin>>r;
}
void HCN :: inthongtin()
{
cout<<”(”<<d<<”,”<<r<<”)”;
}

float HCN :: tinhchuvi()


{
return 2*(d+r);
}
float HCN :: tinhdientich()
{
return d*r ;
}
2.3 Biến, mảng và con trỏ đối tượng
2.3.1 Đối tượng
Trong C++, khi lớp được định nghĩa, chỉ có đặc tả của đối tượng được định nghĩa,
không có vùng nhớ hay ô chứa nào được cấp phát. Để sử dụng dữ liệu và truy cập vào
các hàm định nghĩa trong lớp, chúng ta cần tạo đối tượng. Cú pháp khai báo tạo đối tượng
trong C++ như sau:
tên_lớp tên_biến_đối_tượng;
Khi đó vùng nhớ được cấp phát cho một biến có kiểu lớp sẽ cho ta một khung của
đối tượng bao gồm dữ liệu là các thể hiện cụ thể của các mô tả dữ liệu trong khai báo lớp
và các thông điệp gửi tới các hàm thành phần.
Mỗi đối tượng sở hữu một tập các biến tương ứng với tên và kiểu của các thuộc
tính định nghĩa trong lớp. Chúng được gọi là các biến thể hiện của đối tượng. Trong đó,
tất cả các đối tượng cùng một lớp chung nhau định nghĩa của các phương thức.
Lớp là một kiểu dữ liệu, vì vậy cũng có thể khai báo con trỏ hay tham chiếu đến
một đối tượng thuộc lớp, và bằng cách ấy có thể truy nhập gián tiếp đến đối tượng. Tuy
vậy cần lưu ý, con trỏ và tham chiếu không phải là một thể hiện của lớp.
Trong ví dụ 2.1, lớp HCN đã được định nghĩa, chúng ta có thể tạo các đối tượng
h1, h2, h3 có kiểu lớp HCN như sau:
HCN h1, h2, h3;
2.3.2 Mảng đối tượng
Vì lớp là một kiểu dữ liệu, nên chúng ta cũng có thể khai báo mảng các đối tượng
theo cú pháp sau:
tên_lớp tên_mảng_đối_tượng [số_phần_tử] ;
Có thể khai báo mảng đối tượng H gồm 20 phần tử như sau:
HCN H[20];
Mỗi đối tượng su khi khai báo sẽ được cấp phát một vùng nhớ riêng để chứa các
thuộc tính của chúng. Nhưng sẽ không có vùng nhớ riêng để chứa các phương thức cho
mỗi đối tượng. Các phương thức sẽ được sử dụng chung cho tất cả các đối tượng cùng
lớp. Và như vậy, về bộ nhớ cấp phát thì đối tượng giống như cấu trúc. Kích thước mỗi
đối tượng sẽ đúng bằng tổng kích thước của các thuộc tính.
2.3.3 Thuộc tính của đối tượng
Mỗi thuộc tính đều thuộc về một đối tượng, vì vậy không thể viết tên thuộc tính
một cách riêng rẽ mà bao giờ cũng phải có tên đối tượng đi kèm, giống như cách viết
trong cấu trúc của C++, cú pháp như sau:
Tên_đối_tượng . tên_thuộc_tính ;
Tên_mảng_đối_tượng[chỉ_số] . tên_thuộc_tính ;
Với các đối tượng h1, h2, h3 và mảng H có thể viết như sau:
h1.d // thuộc tính d của đối tượng h1
h2.d // thuộc tính d của đối tượng h2
h3.r // thuộc tính r của đối tượng h3
h1.d = 50; // gán 50 cho h1.d
h2.r = 5.5; // gán 5.5 cho h2.r
H[5].d // thuộc tính d của phần tử H[5]
H[5].r = 10; // gán 10 cho H[5].r
H[0].d = 100; // gán 10 cho H[0].d
2.3.4 Sử dụng các phương thức
Giống như hàm, phương thức được sử dụng thông qua lời gọi. Tuy nhiên, lời gọi
phương thức bao giờ cũng phải có tên đối tượng để chỉ rõ phương thức thực hiện trên các
thuộc tính của đối tượng nào. Cú pháp sử dụng phương thức như sau:
Tên_đối_tượng . tên_phương_thức (các_đối_số) ;
Tên_mảng_đối_tượng[chỉ_số].tên_phương_thức(các_đối_số) ;
Chẳng hạn lời gọi:
//thực hiện nhập các thuộc tính d, r của đối tượng h1
h1 . nhap();
// thực hiện nhập các thuộc tính H[i].d, H[i].r
H [i] . nhap();
Chương trình minh họa sử dụng lớp và đối tượng, gồm nhập và in ra màn hình thông tin
về hình chữ nhật đó.
Ví dụ 2.1:
#include<iostream.h>
#include<conio.h>
class HCN
{
private:
float d,r;
public:
void nhap() ;
void inthongtin();
};
void HCN :: nhap()
{
cout<<"nhap chieu dai: "; cin>>d;
cout<<"nhap chieu rong: "; cin>>r;
}
void HCN :: inthongtin()
{
cout<<"("<<d<<","<<r<<")";
}
int main()
{
HCN h1;
cout<<"Nhap thong tin hinh chu nhat:"<<endl;
h1.nhap();
cout<<"In thong tin hinh chu nhat: ";
h1.inthongtin();
return 0;
}
Trường hợp sử dụng mảng các đối tượng minh họa qua chương trình sau. Chương
trình nhập vào thông tin 5 hình chữ nhật, in ra màn hình giá trị lớn nhất của chu vi các
hình chữ nhật đã nhập.
Ví dụ 2.2:
#include<iostream.h>
#include<conio.h>
class HCN
{
private:
float d,r;
public:
void nhap() ;
void inthongtin();
float tinhchuvi();
};
void HCN :: nhap()
{
cout<<"nhap chieu dai: "; cin>>d;
cout<<"nhap chieu rong: "; cin>>r;
}
void HCN :: inthongtin()
{
cout<<"("<<d<<","<<r<<")";
}
float HCN :: tinhchuvi()
{
return 2*(d+r);
}

int main()
{
HCN H[5];
int i;
float Maxcv;
//Nhap chieu dai và chieu rong cua 5 hinh chu nhat
for(i=0;i<5;i++)
{
cout<<"Nhap thong tin hinh chu nhat thu "<<
i+1<<":"<<endl;
H[i].nhap();
}
// Tìm chu vi lon nhat
Maxcv = H[0].tinhchuvi();
for(i=1;i<5;i++)
{
if (Maxcv < H[i].tinhchuvi())
Maxcv = H[i].tinhchuvi();
}
cout<<"Gia tri lon nhat cua chu vi là: "<<Maxcv;
return 0;
}
2.3 5 Con trỏ đối tượng
Con trỏ đối tượng dùng để chứa địa chỉ của biến, mảng đối tượng. Cú pháp khai
báo như sau:
Tên_lớp *con_trỏ ;
Chẳng hạn với lớp HCN có thể khai báo:
HCN *p1, *p2, *p3; // khai báo 3 con trỏ p1, p2, p3
HCN h1, h2; // khai báo 2 đối tượng h1, h2
HCN H[20]; // khai báo mảng đối tượng H
Khi đó có thể thực hiện các câu lệnh sau:
p1 = &h1;// p1 chứa địa chỉ của h1, hay p1 trỏ tới h1
p2 = H;
hoặc
p2 = &H[0]; /*p2 trỏ tới đầu mảng H hoặc p2 trỏ tới
phần tử đầu tiên của mảng H, 2 lệnh này tương
đương nhau*/
p3 = new HCN; /*Tạo 1 đối tượng và chứa địa chỉ của nó
vào p3*/
Để sử dụng thuộc tính của đối tượng thông qua con trỏ ta viết như sau:
Tên_con_trỏ -> Tên_thuộc_tính
Tên_mảng_đối_tượng[chỉ_số] -> Tên_thuộc_tính
Lưu ý: Nếu con trỏ chứa địa chỉ đầu của mảng, có thể dùng con trỏ như tên mảng.
Như vậy sau khi thực hiện các câu lệnh trên thì :
p1 -> d và h1.d là như nhau
p2[i] -> r và H[i] . rlà như nhau
Chương trình dưới đây minh họa cho việc sử dụng con trỏ trong chương trình.
Ví dụ 2.3:
#include<iostream.h>
#include<conio.h>
class HCN
{
private:
float d,r;
public:
void nhap() ;
void inthongtin();
};
void HCN :: nhap()
{
cout<<"nhap chieu dai: "; cin>>d;
cout<<"nhap chieu rong: "; cin>>r;
}
void HCN :: inthongtin()
{
cout<<"("<<d<<","<<r<<")";
}
int main()
{
HCN *p;
int i,n;
p = new HCN[n+1];
cout<<"nhap n: ";
cin>>n;
cout<<"Nhap thong tin cho cac hinh chu
nhat:"<<endl;
for(i=0;i<n;i++)
{
cout<<"nhap kich thuoc hcn thu "<<i+1<<endl;
p[i].nhap();
cout<<endl;
}
cout<<"In thong tin hinh chu nhat: ";
for(i=0;i<n;i++)
{
cout<<"thong tin hcn thu "<<i+1;
p[i].inthongtin();
cout<<endl;
}
return 0;
}
2.3.6 Phép gán các đối tượng
Có thể thực hiện phép gán giữa 2 đối tượng cùng kiểu. Ví dụ a, b là 2 đối tượng
cùng kiểu lớp phân số PS, lớp PS có 2 thành phần dữ liệu là tử số ts và mẫu số ms. Khi
đó phép gán đối tượng b cho đối tượng a thực chất là gán các thành phần dữ liệu của đối
tượng b cho đối tượng a.
PS a, b;
a(1,3);
b = a;

Hình 2.1. Đối tượng phân số a, b


Tuy vậy, khi các đối tượng trong phép gán chứa các thành phần dữ liệu động, việc
sao chép lại không liên quan đến các vùng dữ liệu đó. Trong ví dụ sau, nếu a, b là 2 đối
tượng có 2 thành phần dữ liệu tĩnh là x, y và 1 thành phần dữ liệu động z thì việc gán đối
tượng b cho đối tượng a không liên quan đến vùng dữ liệu động z, mà chỉ xảy ra việc gán
trên vùng dữ liệu tĩnh x, y.
b = a;

Hình 2.2. Mô tả phép gán phân số


2.4 Đối của phương thức, con trỏ this
2.4.1 Con trỏ this là đối thứ nhất của phương thức
Chúng ta cùng xét lại phương thức nhap() của lớp HCN
void HCN :: nhap()
{
cout<<"nhap chieu dai: "; cin>>d;
cout<<"nhap chieu rong: "; cin>>r;
}
Trong phương thức này, chúng ta sử dụng tên các thuộc tính d, r một cách đơn
độc. Điều này có vẻ như mâu thuẫn với quy tắc phải sử dụng thuộc tính là 1 theo đối
tượng cụ thể. Nhưng C++ cho phép sử dụng con trỏ đặc biệt this trong các phương thức.
Các thuộc tính viết trong phương thức được hiểu là thuộc một đối tượng do con trỏ this
trỏ tới. Và phương thức nhap() có thể viết một cách tường minh như sau:
void HCN :: nhap()
{
cout<<"nhap chieu dai: "; cin>>this->d;
cout<<"nhap chieu rong: "; cin>>this->r;
}
Như vậy, phương thức bao giờ cũng có ít nhất một đối là con trỏ this và nó luôn
luôn là đối đầu tiên của phương thức.
2.4.2 Tham số ứng với đối con trỏ this
Xét lời gọi tới phương thức nhap() trong ví dụ 2.1:
HCN h1;
h1.nhap();
Trong trường hợp này, tham số truyền cho con trỏ this chính là địa chỉ của h1:
this = &h1
Do đó:
this -> d chính là h1.d
this -> r chính là h1.r
Như vậy câu lệnh h1.nhap(); sẽ nhập dữ liệu cho các thuộc tính của h1. Từ đó có
thể kết luận: Tham số truyền cho đối con trỏ this chính là địa chỉ của đối tượng đi kèm
với phương thức trong lời gọi phương thức.
2.4.3 Các đối khác của phương thức
Ngoài các đối đặc biệt this (các đối này không xuất hiện một cách tường minh),
phương thức còn có các đối khác được khai báo như trong các hàm. Đối của phương thức
có thể có kiểu bất kỳ (gồm cả kiểu chuẩn và ngoài chuẩn).
Đối đầu tiên sẽ ngầm định là con trỏ this, các đối tiếp theo sẽ lần lượt là các đối
khai báo tường minh.
2.5 Hàm khởi tạo (Contructor)
2.5.1 Chức năng của hàm khởi tạo
Hàm khởi tạo là một phương thức đặc biệt của lớp, dùng để tạo dựng một đối
tượng mới. Khi một đối tượng được khai báo, đầu tiên chương trình dịch sẽ cấp phát bộ
nhớ cho đối tượng, sau đó sẽ gọi đến hàm tạo. Hàm tạo sẽ khởi gán giá trị cho các thuộc
tính của đối tượng và có thể thực hiện một số công việc khác nhằm chuẩn bị cho đối
tượng mới.
2.5.2 Cách viết hàm khởi tạo:
Khi viết hàm khởi tạo cần tuân theo các quy tắc sau:
- Tên của hàm khởi tạo bắt buộc phải trùng với tên lớp.
- Không có kiểu nên không cần khai báo.
- Không có giá trị trả về.
Cú pháp:
<tên_lớp> ([ds_tham_số]);
Định nghĩa hàm khởi tạo bên ngoài khai báo lớp như sau:
<tên_lớp> :: <tên_lớp> ([ds tham số])
{
//thân hàm
}
Chương trình sau minh họa cho việc định nghĩa và sử dụng hàm khởi tạo. Giả sử
trong lớp HCN có định nghĩa một hàm khởi tạo, việc xây dựng và sử dụng hàm khởi tạo
như sau [2]:
Ví dụ 2.4
#include<iostream.h>
#include<conio.h>
class HCN
{
private:
float d,r;
public:
HCN (float dx, float rx);
void inthongtin();
};
HCN ::HCN(float dx, float rx)
{
d = dx;
r = rx;
}
void HCN :: inthongtin()
{
cout<<"("<<d<<","<<r<<")";
}
int main()
{
HCN h1(30,20);
cout<<"In thong tin hinh chu nhat: ";
h1.inthongtin();
return 0;
}
Có thể có nhiều khởi tạo trong cùng lớp (chồng các hàm thiết lập). Và khi một lớp
có nhiều hàm khởi tạo, việc tạo các đối tượng phải kèm theo các tham số phù hợp với
một trong các hàm khởi tạo đã khai báo. Ví dụ sau minh họa sử dụng lớp có nhiều hàm
khởi tạo[2].
Ví dụ 2.5
#include<iostream.h>
#include<conio.h>
class HCN
{
private:
float d,r;
public:
HCN ();
HCN (float dx, float rx);
void inthongtin();
};
HCN ::HCN()
{
d = 0;
r = 0;
}
HCN ::HCN(float dx, float rx)
{
d = dx;
r = rx;
}
void HCN :: inthongtin()
{
cout<<"("<<d<<","<<r<<")";
}
int main()
{
HCN h1; // Đúng, tham số phù hợp với hàm khởi tạo không tham số
HCN h2(5); // Lỗi vì tham số không phù hợp với hàm khởi tạo
HCN h3(30,20);//Đúng, tham số phù hợp với hàm khởi tạo có 2 tham số
cout<<"In thong tin hinh chu nhat: ";
h1.inthongtin();
h3.inthongtin();
return 0;
}
Hàm khởi tạo cũng có thể sử dụng các tham số có giá trị ngầm định, ví dụ sau
minh họa cách sử dụng [2]:
Ví dụ 2.6
#include<iostream.h>
#include<conio.h>
class HCN
{
private:
float d,r;
public:
HCN (float dx, float rx=0);
void inthongtin();
};
HCN ::HCN(float dx, float rx=0)
{
d = dx;
r = rx;
}
void HCN :: inthongtin()
{
cout<<"("<<d<<","<<r<<")";
}
int main()
{
HCN h1; // Lỗi, không hàm khởi tạo không tham số và không có hàm khởi
//tạo với tất cả các tham số có giá trị ngầm định
HCN h2(5); // Đúng vì tham số thứ 2 nhận giá trị ngầm định là 0
HCN h3(30,20);// Đúng, phù hợp với hàm khởi tạo có 2 tham số
cout<<"In thong tin hinh chu nhat: ";
h2.inthongtin();
h3.inthongtin();
return 0;
}
- Nếu trong lớp không xây dựng hàm khởi tạo thì chương trình dịch tự động sinh
hàm khởi tạo ngầm mặc định. Hàm khởi tạo ngầm mặc định không làm bất cứ nhiệm vụ
nào ngoài việc “lấp chỗ trống”. Và đôi khi người ta cũng gọi hàm khởi tạo không có tham
số do người sử dụng định nghĩa là hàm khởi tạo ngầm định.
- Hàm khởi tạo mặc định được gọi tự động khi khai báo thể hiện của lớp. Tuy vậy,
khi cần khai báo mảng các đối tượng, bắt buộc phải có một hàm khởi tạo không có tham
số.
Minh họa việc sử dụng hàm khởi tạo không có tham số:
Ví dụ 2.7 [2]:
#include<iostream.h>
#include<conio.h>
class HCN
{
private:
float d,r;
public:
HCN (float dx, float rx);
void inthongtin();
};
HCN ::HCN(float dx, float rx)
{
d = dx;
r = rx;
}
void HCN :: inthongtin()
{
cout<<"("<<d<<","<<r<<")";
}
int main()
{
HCN h1(30,20);
HCN H[50]; // Lỗi
// vì không cung cấp thông số cần thiết cho hàm thiết lập
cout<<"In thong tin hinh chu nhat: ";
h1.inthongtin();
return 0;
}
Để giải quyết lỗi trong tình huống này, hoặc là bỏ luôn hàm khởi tạo hai tham số
nhưng khi đó khai báo h1 sẽ không đúng. Do vậy thường sử dụng giải pháp định nghĩa
thêm một hàm khởi tạo không tham số nữa như sau:

Ví dụ 2.8 [2]
#include<iostream.h>
#include<conio.h>
class HCN
{
private:
float d,r;
public:
HCN ()
{
d = r = 0;
}
HCN (float dx, float rx);
void inthongtin();
};
HCN ::HCN(float dx, float rx)
{
d = dx;
r = rx;
}
void HCN :: inthongtin()
{
cout<<"("<<d<<","<<r<<")";
}
int main()
{
HCN h1(30,20);
HCN H[50];
cout<<"In thong tin hinh chu nhat: ";
h1.inthongtin();
cout<<"In thong tin 10 hinh chu nhat: ";
for (int i=0; i<10;i++)
{
H[i].inthongtin();
cout<<endl;
}
return 0;
}
Còn một giải pháp khác không cần định nghĩa thêm hàm khởi tạo không tham số.
Khi đó cần khai báo giá trị ngầm định cho các tham số của hàm thiết lập hai tham số như
sau:
Ví dụ 2.9 [2]
#include<iostream.h>
#include<conio.h>
class HCN
{
private:
float d,r;
public:
HCN (float dx=0, float rx=0);
void inthongtin();
};
HCN ::HCN(float dx=0, float rx=0)
{
d = dx;
r = rx;
}
void HCN :: inthongtin()
{
cout<<"("<<d<<","<<r<<")";
}
int main()
{
HCN h1(30,20);
HCN H[50];
//Đúng vì các đối tượng được tạo nhờ hàm thiết lập 2 tham
// số có giá trị ngầm định là 0 và 0
cout<<"In thong tin hinh chu nhat: ";
h1.inthongtin();
cout<<"In thong tin 10 hinh chu nhat: ";
for (int i=0; i<10;i++)
{
H[i].inthongtin();
cout<<endl;
}
return 0;
}
2.5.3 Hàm khởi tạo sao chép (copy constructor)
Khi lớp không có các thuộc tính kiểu con trỏ hoặc tham chiếu, thì dùng hàm khởi
tạo mặc định là đủ. Khi lớp có các thuộc tính kiểu con trỏ hoặc tham chiếu thì phải sử
dụng hàm khởi tạo sao chép.
Hàm khởi tạo sao chép sử dụng một đối kiểu tham chiếu đối tượng để khởi gán
cho đối tượng mới. Hàm khởi tạo sao chép được viết theo mẫu:
<tên_lớp> (const <tên_lớp> &<dt>)
{
/* các câu lệnh dùng các thuộc tính của đối
tượng đối tượng để khởi gán cho các thuộc tính của
đối tượng mới*/
}
Chương trình sau minh họa việc xây dựng và sử dụng các hàm khởi tạo. Chương trình
thực hiện trên lớp đa thức DT [2].
Ví dụ 2.10:
#include<iostream.h>
#include<conio.h>
#include<math.h>
class DT
{
private:
int n; //bac da thuc
double *a; //tro toi vung nho chua cac he so
da thuc a0, a1,...
public:
DT()
{
this->n=0;
this->a=NULL;
}
DT(int n1)
{
this->n=n1;
this->a=new double[n1+1];
}
DT (const DT &d)
{
this->n=d.n;
this->a=new double[d.n + 1];
for(int i=0;i<=d.n;i++)
this->a[i]=d.a[i];
}

void nhap(DT &d)


{
cout<<"nhap bac da thuc: ";
cin>>d.n;
d.a=new double[d.n + 1];
cout<<"nhap cac he so cua da thuc:\n";
for(int i=0;i<=d.n;i++)
{
cout<<"he so bac "<<i<<"=";
cin>>d.a[i];
}
}
void in(DT d)
{
cout<<"(";
for(int i=0;i<=d.n;i++)
cout<<d.a[i]<<", ";
cout<<")";
}
};
int main()
{
DT d;
cout<<"nhap da thuc d: "; d.nhap(d);
DT u(d); //sao chep doi tuong d cho doi tuong u
//va nguoc lai
cout<<"\nda thuc d in la: "; d.in(d);
cout<<"\nda thuc u in la: "; u.in(d);
u.nhap(d);
cout<<"\nda thuc d in la: "; d.in(d);
cout<<"\nda thuc u in la: "; u.in(d);
d.nhap(u);
cout<<"\nda thuc d in la: "; u.in(u);
cout<<"\nda thuc u in la: "; d.in(u);
return 0;
}
2.6 Hàm hủy (Destructor)
2.6.1 Chức năng của hàm hủy
Hàm hủy là một phương thức của lớp có chức năng ngược với hàm khởi tạo. Hàm
hủy được gọi trước khi giải phóng (xóa bỏ) một đối tượng để thực hiện một số công việc
có tính dọn dẹp trước khi đối tượng được hủy bỏ như giải phóng một vùng nhớ mà đối
tượng đang quản lý, xóa đối tượng khỏi màn hình nếu như nó đang hiển thị,…
Việc hủy bỏ một đối tượng thường xảy ra trong 2 trường hợp sau:
- Trong các toán tử và trong các hàm giải phóng bộ nhớ, như delete, free, …
- Giải phóng các biến, mảng cục bộ khi thoát khỏi hàm, phương thức.
2.6.2 Cách viết hàm hủy
Hàm hủy được viết theo quy tắc sau:
~ <tên_lớp> ();
Trong đó:
- Kiểu của hàm: Kiểu của hảm hủy giống với hàm khởi tạo là hàm không có kiểu,
không có giá trị trả về.
- Tên hàm: Tên của hàm hủy gồm 1 dấu ngã đứng trước và tên lớp.
- Đối số: Hàm hủy không có đối số.
Xây dựng hàm hủy cho lớp đa thức DT [2]:
Ví dụ 2.11:
#include<iostream.h>
#include<conio.h>
#include<math.h>
class DT
{
private:
int n; //bac da thuc
double *a; //tro toi vung nho chua cac he so
da //thuc a0, a1,...
public:
~DT()
{
this->n=0;
delete this->a;
}

};
….

2.6.3 Hàm hủy mặc định và hàm hủy tường minh


Nếu trong lớp không định nghĩa hàm hủy, thì một hàm hủy mặc định không làm gì
cả được phát sinh. Đối với nhiều lớp thì hàm hủy mặc định là đủ, và không cần đưa vào
một hàm hủy mới.
Hàm hủy được gọi trước khi đối tượng được thu hồi, tác dụng chính của nó dùng
để dọn dẹp và thu hồi tài nguyên động đã cấp phát. Đối với lớp chỉ có các biến thành viên
tĩnh, ta không thể thu hồi lại được nên không cần định nghĩa destructor.
Trong ngôn ngữ C/C++, tài nguyên động sẽ không được tự động giải phóng. Ngay
cả khi đã kết thúc chương trình, hệ thống vẫn đánh dấu vùng nhớ được sử dụng. Điều này
sẽ gây lỗi rò rỉ bộ nhớ (memory leak) nếu cấp phát quá nhiều. Vì thế, ta cần định nghĩa
destructor để giải phóng vùng nhớ đã cấp phát nếu trong lớp có thành viên con trỏ và có
cấp phát tài nguyên động.
2.7 Các hàm trực tuyến (Inline)
Việc tổ chức chương trình thành các hàm có ưu điểm là chia chương trình thành
các đơn vị độc lập, làm cho chương trình được tổ chức một cách khoa học dễ kiểm soát,
dễ phát hiện lỗi, dễ phát triển, dễ mở rộng và giảm kích thước chương trình. Tuy nhiên
hàm có nhược điểm là làm giảm tốc độ chạy chương trình do phải thực hiện một số thao
tác có tính thủ tục mỗi khi gọi hàm như cấp phát vùng nhớ cho các đối và các biến cục
bộ, truyền dữ liệu của các tham số cho các đối, giải phóng vùng nhớ trước khi thoát khỏi
hàm.
Giải pháp khắc phục các nhược điểm trên của hàm là biến hàm thành hàm trực
tuyến (inline). Để biến một hàm thành trực tuyến ta viết thêm từ khóa inline vào trước
khai báo nguyên mẫu hàm như sau:
inline float f(….);
float f(….)
{
//…
}
Nếu không dùng nguyên mẫu thì viết từ khóa này trước dòng đầu tiên của định
nghĩa hàm:
inline float f(….)
{
//…
}
Hàm inline trong C++ là khái niệm mạnh được sử dụng phổ biến với các lớp. Nếu
một hàm là inline, thì trình biên dịch (compiler) đặt một bản sao code của hàm đó tại mỗi
vị trí mà hàm đó được gọi tại thời điểm biên dịch.
Một định nghĩa hàm trong một định nghĩa lớp là một định nghĩa hàm inline, ngay cả
khi không sử dụng định danh inline. Như vậy mọi hàm thành phần được định nghĩa trong
lớp đều được trình biên dịch xử lý theo kiểu inline.
2.8 Hàm bạn
2.8.1 Hàm và hàm bạn
Hàm (function) có các tính chất sau:
Phạm vi của hàm là toàn bộ chương trình, vì vậy hàm có thể được gọi từ bất kỳ
chỗ nào. Như vậy trong các phương thức cũng có thể sử dụng hàm và trong thân hàm
cũng có thể gọi tới các phương thức (public) của lớp.
Đối của hàm có thể là các đối tượng, tuy nhiên có một hạn chế là trong thân hàm
không cho phép truy nhập tới các thuộc tính của các đối này.
Hàm bạn: (Friend function)
Một hàm bình thường không thể truy nhập tới các thuộc tính private của lớp.
Nhưng nếu trong lớp có khai báo hàm đó là hàm bạn của lớp thì hàm đó có quyền truy
xuất tới các thuộc tính private giống như các phương thức của lớp đó.
Như vậy, khi cho phép các hàm bạn hay lớp bạn, nguyên tắc đóng gói dữ liệu lại
bị vi phạm. Tuy nhiên, sự vi phạm đó hoàn toàn chấp nhận được và tỏ ra rất hiệu quả
trong một số tình huống đặc biệt. Giả sử chúng ta đã có định nghĩa lớp véc tơ vector, lớp
ma trận matrix. Và muốn định nghĩa một hàm nhân một ma trận với một véc tơ. Nếu
không cho phép hàm bạn, lớp bạn thì ta không thể định nghĩa hàm này bởi nó không thể
là hàm thành phần của lớp ma trận hay lớp vec tơ và càng không thể là hàm tự do ngoại
trừ khai báo tất cả các thuộc tính trong 2 lớp này là public. Rõ ràng trong những tình
huống tương tự thì hàm bạn, lớp bạn giúp giải quyết vấn đề rất hiệu quả. Nó giống như
việc cấp thẻ ra vào ở các cơ quan, chỉ những người được cho phép mới được cấp thẻ ra
vào cơ quan đó.
2.8.2 Tính chất của hàm bạn
Hàm friend trong C++ của một lớp được định nghĩa bên ngoài phạm vi lớp đó,
nhưng nó có quyền truy cập tất cả thành viên private và protected của lớp đó. Ngay cả
khi các nguyên mẫu cho hàm friend xuất hiện trong định nghĩa lớp, thì các hàm friend
không là các hàm thành viên.
Một friend có thể là một hàm, một mẫu hàm, hoặc hàm thành viên, hoặc một lớp
hoặc một mẫu lớp, trong trường hợp này, toàn bộ lớp và tất cả thành viên của nó là
friend.
Trong C++ có các kiểu bạn bè sau:
- Hàm tự do là bạn của một lớp.
- Hàm tự do là bạn của nhiều lớp
- Hàm thành phần của một lớp là bạn của một lớp khác
- Tất cả các hàm thành phần của một lớp là bạn của một lớp khác
Sau đây chúng ta sẽ xem xét cụ thể cách khai báo, định nghĩa và sử dụng hàm bạn,
lớp bạn trong từng tình huống nêu trên:
2.8.2.1Hàm tự do là bạn của lớp
Một hàm tự do có thể là bạn của một lớp nếu trong chính lớp đó phải đưa ra khai
báo hàm bạn của mình:
friend <kiểu_trả_về> Tên_hàm (Danh_sách_đối_số);
Việc định nghĩa hàm bạn bên ngoài lớp giống như một hàm thông thường
Lưu ý:
- Trong hàm bạn không còn tham số ngầm định this như trong hàm thành phần.
- Việc định nghĩa hàm bạn bên ngoài lớp giống như một hàm thông thường
Chẳng hạn lớp hình chữ nhật HCN dưới đây có định nghĩa một hàm bạn tính diện
tích hình chữ nhật. Với các hàm thành phần của lớp như hàm khởi tạo, hàm in thông tin
và hàm tính chu vi của hình chữ nhật có tham số ngầm định là con trỏ this, nhưng hàm
bạn tính diện tích thì không có tham số ngầm định này.
Ví dụ 2.12
#include<iostream.h>
class HCN
{
private:
float d,r;
public:
HCN (float d,float r)
{
this->d=d;
this->r=r;
}
float tinhchuvi();
friend float tinhdientich(HCN h);
void inthongtin();
};
void HCN :: inthongtin()
{
cout<<"("<<d<<","<<r<<")";
}
float HCN :: tinhchuvi()
{
return 2*(d+r);
}
float tinhdientich(HCN h)
{
return h.d*h.r ;
}
int main()
{
HCN h(20,15);
cout<<"In thong tin hinh chu nhat: ";
h.inthongtin();
cout<<" co chu vi la: "<<h.tinhchuvi()<<" va co
dien tich la: "<<tinhdientich(h);
cout<<endl;
return 0;
}
2.8.2.2 Hàm thành phần của một lớp là bạn của một lớp khác
Giả thiết có 2 lớp A và B, trong B có một hàm thành phần f khai báo như sau:
int f (char, A);
Nếu f có nhu cầu truy xuất vào các thành phần riêng của A thì f cần phải được
khai báo là bạn của lớp A ở trong lớp A bằng câu lệnh
friend int B :: f (char , A);
Để biên dịch được các khai báo của lớp A có chứa các khai báo bạn bè với lớp B,
chương trình dịch cần phải biết trước nội dung lớp B, nghĩa là khai báo của B phải được
khai báo trước biên dịch của A
Ngược lại, khi biên dịch khai báo int f(char, A) trong lớp B, chương trình dịch
không nhất thiết phải biết chi tiết nội dung của A, nó chỉ cần biết rằng A là một lớp bằng
khai báo: class A;
Vậy sơ đồ khai báo và định nghĩa phải như sau:
class A;
class B
{ ...
int f (char, A) ; …
...
};
class A
{ ...
friend int B :: f (char , A);
.. .
};
int B :: f (char, A)
{ … }
Chương trình sau minh họa cho kiểu bạn bè này, hàm tính tích là hàm thành viên của lớp
ma trận và là hàm bạn của lớp vec tơ [1]:
Ví dụ 2.13
#include <iostream.h>
int n;
class vector;
class matran
{
private:
int mt[3][3];
public:
matran(double t[3][3])
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
mt[i][j] = t[i][j];
}
}
}
vector nhan(vector b);
};
class vector
{
private:
int vt[3];
public:
vector(double v1=0,double v2=0,double v3=0)
{
vt[0] = v1;
vt[1] = v2;
vt[2] = v3;
}
void invt()
{
for (int i = 0; i < 3; i++)
cout << vt[i]<< " ";
cout << endl;
}
friend vector matran:: nhan(vector b);
};
vector matran::nhan(vector b)
{
int i,j;
double sum;
vector kq;
for (int i = 0; i < 3; i++)
{
sum = 0;
for (int j = 0; j < 3; j++)
sum = sum + mt[i][j] * b.vt[j];
kq.vt[i]=sum;
}
return kq;
}
int main()
{
vector V(1,2,3);
vector KQ;
double dlm[3][3]={1,2,3,4,5,6,7,8,9};
matran M=dlm;
KQ = M.nhan(V);
cout<<"Ket qua phep nhan la: ";
KQ.invt();
return 0;
}

2.8.2.3 Tất cả các hàm thành phần của một lớp là bạn của một lớp khác
Để tất cả các hàm thành phần của lớp B là bạn của lớp A, trong khai báo của lớp A
phải có chỉ thị:
class A
{ .. .
friend class B;
.. .
}
Khi đó còn gọi lớp B là bạn của lớp A (lớp bạn)
Lưu ý: Để biên dịch khai báo của lớp A, phải đặt khai báo
class B; trước khai báo lớp A.
Trong chương trình sau, lớp ma trận mt và lớp vec tơ vt là bạn bè của nhau. Khi đó tất cả
các phương thức của lớp này đều là bạn của lớp kia và ngược lại [1].
Ví dụ 2.14
#include <conio.h>
#include <iostream.h>
class mt;
class vt;
class mt
{
private:
double a[10][10];
int n;
public:
friend class vt;
mt()
{
n=0;
}
void nhap();
void in();
vt tich(const vt &y);
mt tich(const mt &b) ;
};
class vt
{
private:
double x[10];
int n;
public:
friend class mt;
vt()
{
n=0;
}
void nhap();
void in();
vt tich(const mt &b);
double tich(const vt &y) ;
};
void mt::nhap()
{
cout << "\n cap ma tran: " ;
cin >> n;
for (int i=1; i<=n; ++i)
for (int j=1; j<=n; ++j)
{
cout << "\nphan tu hang " << i << " cot " << j << " =
" ;
cin >> a[i][j];
}
}
void mt::in()
{
for (int i=1; i<=n; ++i)
{
cout << "\n" ;
for (int j=1; j<=n; ++j)
cout << a[i][j] << " " ;
}
}
void vt::nhap()
{
cout << "\n cap vec to: " ;
cin >> n;
for (int i=1; i<=n; ++i)
{
cout << "\nphan tu thu " << i << " = " ;
cin >> x[i];
}
}
void vt::in()
{
for (int i=1; i<=n; ++i)
cout << x[i] << " " ;
}
vt mt::tich(const vt &y)
{
vt z;
int i,j;
for (i=1; i<=n; ++i)
{
z.x[i] = 0.0 ;
for (j=1; j<=n; ++j)
z.x[i] += a[i][j]*y.x[j];
}
z.n = n;
return z;
}
mt mt::tich(const mt &b)
{
mt c;
int i,j,k;
for (i=1; i<=n; ++i)
for (j=1; j<=n; ++j)
{
c.a[i][j] = 0.0 ;
for (k=1; k<=n; ++k)
c.a[i][j] += a[i][k]*b.a[k][j];
}
c.n = n;
return c;
}
vt vt::tich(const mt &b)
{
vt z;
int i,j;
for (j=1; j<=n; ++j)
{
z.x[j] = 0.0 ;
for (i=1; i<=n; ++i)
z.x[j] += b.a[i][j]*x[i];
}
z.n = n;
return z;
}
double vt::tich(const vt &y)
{
double tg=0.0;
for (int i=1; i<=n; ++i)
tg += x[i]*y.x[i];
return tg;
}
int main()
{
mt a,b,c;
vt x,y;
cout << "\nma tran a";
a.nhap();
cout << "\nma tran b";
b.nhap();
cout << "\nma tran c";
c.nhap();
cout << "\nvec to x";
x.nhap();
cout << "\nvec to y";
y.nhap();
mt d= a.tich(b);
vt u = d.tich(y);
vt v = x.tich(c);
double s = v.tich(u);
cout << "\n\nvec to v\n";
v.in();
cout << "\n\nma tran d";
d.in();
cout << "\n\nvec to y\n";
y.in();
cout << "\n\ns= vdy = " << s;
return 0;
}
2.8.3 Hàm bạn của nhiều lớp
Một hàm tự do có thể là bạn của nhiều lớp nếu trong chính các lớp đó phải đưa ra
khai báo hàm bạn của mình:
friend <kiểu_trả_về> Tên_hàm (Danh_sách_đối_số);
Việc định nghĩa hàm bạn bên ngoài lớp giống như một hàm thông thường
Lưu ý:
- Trong hàm bạn không còn tham số ngầm định this như trong hàm thành phần.
- Việc định nghĩa hàm bạn bên ngoài lớp giống như một hàm thông thường
- Khi khai báo các hàm phải đảm bảo các lớp được sử dụng trong hàm đã được khai
báo trước đó (không cần định nghĩa trước).
Chương trình dưới đây thực hiện phép nhân 1 ma trận vuông cấp 3x3 với 1 véc tơ
3 chiều. Trong chương trình xây dựng hàm tính tích là một hàm tự do và là bạn của 2 lớp
ma trận và lớp vec tơ [1].
Ví dụ 2.15
#include <iostream.h>
int n;
class vector; //khai báo trước vì lớp matran có sử dụng lớp này
class matran
{
private:
int mt[3][3];
public:
matran(double t[3][3])
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{

mt[i][j] = t[i][j];
}
}
}
friend vector nhan(matran a,vector b);
};
class vector
{
private:
int vt[3];
public:
vector(double v1=0,double v2=0,double v3=0)
{
vt[0] = v1;
vt[1] = v2;
vt[2] = v3;
}
void invt()
{
for (int i = 0; i < 3; i++)
cout << vt[i]<< " ";
cout << endl;
}
friend vector nhan(matran a,vector b);
};
vector nhan(matran a,vector b)
{
int i,j;
double sum;
vector kq;
for (int i = 0; i < 3; i++)
{
sum = 0;
for (int j = 0; j < 3; j++)
sum = sum + a.mt[i][j] * b.vt[j];
kq.vt[i]=sum;
}
return kq;
}
int main()
{
vector V(1,2,3);
vector KQ;
double dlm[3][3]={1,2,3,4,5,6,7,8,9};
matran M=dlm;
KQ = nhan(M,V);
cout<<"Ket qua phep nhan la: ";
KQ.invt();
return 0;
}
TÓM TẮT NỘI DUNG
Chương 2 gồm các nội dung hướng dẫn chi tiết cách xây dựng lớp trong ngôn ngữ
lập trình C++. Một lớp gồm các thành phần dữ liệu (gọi là thuộc tính) và các hàm thành
phần (gọi là phương thức). Trong chương 2 hướng dẫn chi tiết cách khai báo và sử dụng
các thuộc tính và các phương thức cơ bản trong lớp, trong chương trình. Mỗi nội dung
đều có các ví dụ minh họa cụ thể.

You might also like