Tải bản đầy đủ

Tìm hiểu lập trình hướng đối tượng với c++

TRƯỜNG ĐẠI HỌC BÁCH KHOA HÀ NỘI
VIỆN ĐIỆN TỬ VIỄN THÔNG

----------

ĐỒ ÁN 1
Chủ đề: Xử Lí Ảnh
Phần 1: Tìm hiểu lập trình hướng đối tượng với C++

Hà Nội, tháng 09/2016
TRƯỜNG ĐẠI HỌC BÁCH KHOA HÀ NỘI


Lập trình hướng đối tượng với C++
VIỆN ĐIỆN TỬ VIỄN THÔNG
*********

ĐỒ ÁN 1
Chủ đề: Xử Lí Ảnh
Phần 1: Tìm hiểu lập trình hướng đối tượng với C++


Hà Nội, tháng 09/2016.

2


Lập trình hướng đối tượng với C++

Mục lục

3


Lập trình hướng đối tượng với C++

1. Lập trình hướng đối tượng C++
1.1. Mục tiêu
Giúp người đọc hiểu về lập trình hướng đối tượng và ngôn ngữ lập trình C++.
1.2. Các khái niệm lập trình hướng đối tượng
1.2.1. Khái niệm lập trình hướng đối tượng
Khái niệm hướng đối tượng được xây dựng trên nền tảng của khái niệm lập trình
có cấu trúc và sự trừu tượng hóa dữ liệu. Sự thay đổi căn bản ở chỗ, một chương trình
hướng đối tượng được thiết kế xoay quanh dữ liệu mà chúng ta có thể làm việc trên
đó, hơn là theo bản thân chức năng của chương trình. Điều này hoàn toàn tự nhiên
một khi chúng ta hiểu rằng mục tiêu của chương trình là xử lý dữ liệu.
Lập trình hướng đối tượng cho phép chúng ta tổ chức dữ liệu trong chương trình
theo một cách tương tự như các nhà sinh học tổ chức các loại thực vật khác nhau.
Theo cách nói lập trình đối tượng, xe hơi, cây cối, các số phức, các quyển sách đều
được gọi là các lớp (Class).
Một lớp là một bản mẫu mô tả các thông tin cấu trúc dữ liệu, lẫn các thao tác
hợp lệ của các phần tử dữ liệu. Khi một phần tử dữ liệu được khai báo là phần tử của
một lớp thì nó được gọi là một đối tượng (Object). Các hàm được định nghĩa hợp lệ
trong một lớp được gọi là các phương thức (Method) và chúng là các hàm duy nhất có
thể xử lý dữ liệu của các đối tượng của lớp đó. Mộtthực thể (Instance) là một vật thể
có thực bên trong bộ nhớ, thực chất đó là một đối tượng (nghĩa là một đối tượng được
cấp phát vùng nhớ).
1.2.2. Khái niệm sự đóng gói
Sự đóng gói là cơ chế ràng buộc dữ liệu và thao tác trên dữ liệu đó thành một thể
thống nhất, tránh được các tác động bất ngờ từ bên ngoài. Thể thống nhất này gọi là
đối tượng.
Trong một đối tượng, dữ liệu hay thao tác hay cả hai có thể là riêng (Private)

hoặc chung (Public) của đối tượng đó. Thao tác hay dữ liệu riêng là thuộc về đối
tượng đó chỉ được truy cập bởi các thành phần của đối tượng, điều này nghĩa là thao
tác hay dữ liệu riêng không thể truy cập bởi các phần khác của chương trình tồn tại
ngoài đối tượng. Khi thao tác hay dữ liệu là chung, các phần khác của chương trình có
thể truy cập nó mặc dù nó được định nghĩa trong một đối tượng. Các thành phần
chung của một đối tượng dùng để cung cấp một giao diện có điều khiển cho các thành
thành riêng của đối tượng.

4


Lập trình hướng đối tượng với C++
1.2.3. Khái niệm tính kế thừa
Chúng ta có thể xây dựng các lớp mới từ các lớp cũ thông qua sự kế thừa. Một
lớp mới còn gọi là lớp dẫn xuất (derived class), có thể thừa hưởng dữ liệu và các
phương thức của lớp cơ sở (base class) ban đầu. Trong lớp này, có thể bổ sung các
thành phần dữ liệu và các phương thức mới vào những thành phần dữ liệu và các
phương thức mà nó thừa hưởng từ lớp cơ sở. Mỗi lớp (kể cả lớp dẫn xuất) có thể có
một số lượng bất kỳ các lớp dẫn xuất. Qua cơ cấu kế thừa này, dạng hình cây của các
lớp được hình thành. Dạng cây của các lớp trông giống như các cây gia phả vì thế các
lớp cơ sở còn được gọi là lớp cha (parent class) và các lớp dẫn xuất được gọi là lớp
con (child class).
1.2.4. Khái niệm tính đa hình
Đó là khả năng để cho một thông điệp có thể thay đổi cách thực hiện của nó theo
lớp cụ thể của đối tượng nhận thông điệp. Khi một lớp dẫn xuất được tạo ra, nó có thể
thay đổi cách thực hiện các phương thức nào đó mà nó thừa hưởng từ lớp cơ sở của
nó. Một thông điệp khi được gởi đến một đối tượng của lớp cơ sở, sẽ dùng phương
thức đã định nghĩa cho nó trong lớp cơ sở. Nếu một lớp dẫn xuất định nghĩa lại một
phương thức thừa hưởng từ lớp cơ sở của nó thì một thông điệp có cùng tên với
phương thức này, khi được gởi tới một đối tượng của lớp dẫn xuất sẽ gọi phương thức
đã định nghĩa cho lớp dẫn xuất.
1.3. C++ Căn bản
1.3.1. Cấu trúc một chương trình C++
Đâu tiên, chúng ta có hàm main
int main()
Sau đó, chúng ta có phần thân của hàm main là cặp dấu ngoặc nhọn đứng sau từ
khóa main, cuối thân hàm mainlà giá trị trả về của hàm main.
int main()
{
return 0;
}
Tiếp đến, chúng ta có những dòng lệnh đặt bên trong thân hàm main.
int main()
{
cout << "This is a command" << endl;

5


Lập trình hướng đối tượng với C++
return 0;
}
Bên cạnh những dòng lệnh, chúng ta còn có những dòng comment.
// This comment is located outside main function
/* We can put comment everywhere in a C++ file */
int main()
{
// We are coding inside main function
cout << "This is a command" << endl;
return 0;
}
Và cuối cùng là những thư viện cần thiết để compiler có thể hiểu được những
lệnh đã được định nghĩa sẵn trong ngôn ngữ lập trình C++.
1.3.2. Xuất, nhập trong C++
- Xuất dữ liệu ra màn hình.
Để in dữ liệu của một biểu thức nào đó ra màn hình (standard output device) ta
dùng câu lệnh sau:
1. cout << exp ; // output content of expression

Hoặc cho nhiều biểu thức:
2. cout << exp_1 << exp_2 << … << exp_n ; // output content of expression 1, 2,

…,n
Ta cũng có thể viết câu lệnh trên trên nhiều dòng:
3. cout << exp_1
4.

<< exp_2

5.

<< exp_3

6.



7.

<< exp_n;

Nhập dữ liệu vào từ bàn phím
Bàn phím là thiết bị nhập chuẩn (standard input device). Để vào dữ liệu từ bàn
phím cho các biến ta có thể dùng cin vùng toán tử >>. Cú pháp sẽ như sau:
-

8. cin >> var; // read data into variable

6


Lập trình hướng đối tượng với C++
Hoặc cho nhiều biến:
9. cin >> var_1 >> var_2 >> … >> var_n;

Khi gặp những câu lệnh như thế này chương trình sẽ “pause” lại để chờ chúng ta
nhập dữ liệu vào từ bàn phím. Câu lệnh cin >> var_1 >> var_2 >> … >> var_n; coi
các ký tự trắng là ký tự phân cách các lần nhập dữ liệu. Các ký tự trắng (white space
characters) bao gồm: dấu cách, dấu tab, và ký tự xuống dòng (new line). Ví dụ a, b là
hai biến kiểu int, thì câu lệnh: cin >> a >> b;

7


Lập trình hướng đối tượng với C++
1.4. Đối tượng và Lớp
1.4.1. Đối tượng (Objects)
Khi thiết kế một chương trình theo tư duy hướng đối tượng người ta sẽ không
hỏi “vấn đề này sẽ được chia thành những hàm nào” mà là “vấn đề này có thể giải
quyết bằng cách chia thành những đối tượng nào”. Tư duy theo hướng đối tượng làm
cho việc thiết kế được “tự nhiên” hơn và trực quan hơn. Điều này xuất phát từ việc
các lập trình viên cố gắng tạo ra một phong cách lập trình càng giống đời thực càng
tốt. Tất cả mọi thứ đều có thể trở thành đối tượng trong OOP, nếu có giới hạn thì đó
chính là trí tưởng tượng của bạn. Đối tượng là một thực thể tồn tại trong khi chương
trình chạy. Nó có các thuộc tính (Attributes) và phương trức (Methods) của riêng
mình.
1.4.2. Lớp (Class)
- Định nghĩa lớp:
Các class được tạo ra bằng cách sử dụng từ khóa class. Chúng ta sẽ xem xét một
ví dụ về định nghĩa lớp. Giả sử chúng ta lập một lớp Student trong đó lưu trữ các
thông tin về sinh viên cũng như chứa các hàm để thao tác trên các dữ liệu này.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.

#include
#include
using namespace std;
// class definition
class Student{
private:
string name; // tên sinh viên
int age; // tuổi
string student_code; // mã số sinh viên
public:
// set information
void set_name(string); // nhập tên
void set_age(int); // nhập tuổi
void set_student_code(string); // nhập mã sinh viên
// get information
string get_name(); // lấy tên
int get_age(); // lấy tuổi
string get_student_code(); // lấy mã sinh viên
};

Ta sẽ phân tích định nghĩa trên của lớp Student. Đầu tiên là từ khóa class, sau đó
là tên lớp mà người dùng muốn tạo (ở đây là Student). Phần còn lại nằm trong
cặp ngoặc móc {}chứa những thành của lớp. Những dữ liệu như: name, age,
8


Lập trình hướng đối tượng với C++
student_code được gọi là các thành phần dữ liệu (data members), còn các hàm
như: set_name(), get_name(), … được gọi là các hàm thành viên (member
functions) hay phương thức (methods). Thông thường các data members được để
ở chế độ private, còn các member functions thì ở chế độpublic.
Từ khóa private và public là hai access-specifier quy định quyền truy nhập đối
với các thành phần trong lớp, nó sẽ có hiệu lực cho các thành phần của lớp đứng
sau nó cho đến khi gặp một access-specifier khác. Tất cả các thành phần được
khai báo là public sẽ có thể được truy cập “thoải mái” bất cứ chỗ nào lớp có hiệu
lực. Ví dụ nó có thể được truy cập bởi hàm thành viên của của một lớp khác,
hoặc các hàm tự do trong chương trình. Các thành phần private thì được bảo mật
cao hơn. Chỉ những thành phần của lớp mới có thể truy nhập đến chúng. Mọi cố
gắng truy nhập bất hợp pháp từ bên ngoài đều sẽ gây lỗi. Do đó ta có thể mô tả
cú pháp chung để định nghĩa một lớp như sau:
class Class_name{
private:
// các thành phần private
public:
// các thành phần public
};
-

Định nghĩa các hàm thành viên cho lớp:

Trong định nghĩa trên của lớp mới chỉ khai báo các nguyên mẫu hàm (function
prototypes) chứ hoàn toàn chưa có thân hàm. Ta sẽ phải định nghĩa các hàm này.
Có hai cách định nghĩa các hàm thành viên: định nghĩa hàm thành viên ngay
trong định nghĩa lớp hoặc khai báo nguyên mẫu trong lớp, còn định nghĩa bên
ngoài lớp.
Định nghĩa hàm ngay trong định nghĩa lớp
Khi đó định nghĩa lớp được viết lại như sau:
1. class Student{
2.
private:
3.
string name;
4.
int age;
5.
string student_code;
6.
public:
7.
// set information
8.
void set_name(string str){ name=str; }
9.
void set_age(int num){ age=num; }

9


Lập trình hướng đối tượng với C++
10.
11.
12.
13.
14.
15.
16. };

void set_student_code(string str){ student_code=str; }
// get information
string get_name(){ return name; }
int get_age(){ return age; }
string get_student_code(){ return student_code; };

Ta nhận thấy các hàm đã được định nghĩa luôn trong nghĩa lớp. Tuy nhiên cách
này không phải là cách tốt. Với bài này thì các hàm còn đơn giản, còn ngắn.
Nhưng trong thực tế khi ta xây dựng các lớp những lớp phức tạp hơn thì số lượng
hàm sẽ nhiều hơn và dài hơn. Nếu định nghĩa trong lớp sẽ làm “mất mĩ quan” và
khó kiểm soát. Vì vậy trong định nghĩa lớp ta chỉ liệt kê các nguyên mẫu hàm,
còn khi định nghĩa, ta sẽ định nghĩa ra bên ngoài.
Khai báo hàm trong lớp, còn định nghĩa ngoài lớp
1. // class definition
2. class Student{
3.
private:
4.
string name;
5.
int age;
6.
string student_code;
7.
public:
8.
// set information
9.
void set_name(string);
10.
void set_age(int);
11.
void set_student_code(string);
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.

// get information
string get_name();
int get_age();
string get_student_code();
};
// member function definitions
// set name
void Student::set_name(string str){
name=str;
}
// set age
void Student::set_age(int num){
age=num;
}
// set student code
10


Lập trình hướng đối tượng với C++
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.

void Student::set_student_code(string str){
student_code=str;
}
// get name
string Student::get_name(){
return name;
}
// get age
int Student::get_age(){
return age;
}
// get student code
string Student::get_student_code(){
return student_code;
}
Cú pháp để định nghĩa một hàm thành viên bên ngoài lớp là:

1.
2.
3.
4.
5.
6.
7.
8.
9.

::(danh_sách_tham_số){
// định nghĩa thân hàm ở đây
}
Ví dụ hàm set_name
void Student::set_name(string str){
name=str;
}
Cú pháp này chỉ rõ rằng hàm ta đang định nghĩa là hàm thành viên của
lớp Student vì nó được chỉ định bởi toán tử phân giải phạm vi :: (trong trường
hợp này là Student:: ), nghĩa là hàm này nằm trong phạm vi lớp Student.
Có một sự khác nhau giữa hai cách định nghĩa hàm này không phải chỉ ở góc độ
“thẩm mỹ”. Theo cách thứ nhất: định nghĩa luôn trong định nghĩa lớp, thì hàm
được coi là “hàm nội tuyến” hay inline. Thông thường khi muốn một hàm làm
một việc gì đó thì ta phải làm một việc là “gọi hàm” (invoke). Việc gọi hàm sẽ
phải tốn các chi phí về thời gian như gửi lời gọi hàm, truyền đối số, … điều này
có thể làm chậm chương trình. C++ cung cấp một giải pháp đó là “hàm nội
tuyến” bằng cách thêm vào từ khóa inline trước kiểu trả về của hàm như sau.

1. inline ::(danh_sách_tham_s

ố){
2.
// định nghĩa thân hàm ở đây
3. }

11


Lập trình hướng đối tượng với C++
Điều này “gợi ý” cho compiler sinh mã của hàm ở những nơi thích hợp để tránh
phải gọi hàm. Như vậy sẽ tránh được những chi phí gọi hàm nhưng ngược lại nó
làm tăng kích thước của chương trình. Vì cứ mỗi lời gọi hàm sẽ được thay thế
bởi một đoạn mã tương ứng. Vì vậy chỉ khai báo một hàm là inline khi kích
thước của nó không quá lớn và không chứa vòng lặp cũng như đệ quy. Hơn nữa
không phải hàm nào khai báo inline đều được hiểu là inline, vì đó chỉ là “gợi
ý” cho compiler. Nếu compiler nhận thấy hàm có kích thước khá lớn, xuất hiện
nhiều lần trong chương trình, hoặc có chứa các cấu trúc lặp, đệ quy thì nó có thể
“lờ đi” yêu cầu inline này. Hiện này các compiler đều được tối ưu rất tốt, vì vậy
không cần thiết phải dùng inline. Đó là sự khác biệt của các hàm thành viên được
định nghĩa ngay trong lớp so với các hàm thành viên được định nghĩa bên ngoài.
Truy cập đến những thành phần của lớp
Để truy cập đến các thành phần của lớp ta dùng toán tử chấm (selection dot
operator) thông qua tên của đối tượng. Ví dụ đoạn chương trình sau gọi
hàm set_name để nhập tên cho đối tượng studentA và gọi hàm get_name để lấy
tên của đối tượng :
-

1. Student studentA; // khai báo đối tượng studentA thuộc lớp Student
2. studentA.set_name(“Bill Gates”); // gán tên cho studentA là “Bill Gates”
3. cout << studentA.get_name(); // in ra tên đối tượng studentA

Kết quả thu được là màn hình hiển thị dòng văn bản “Bill Gates”. Để ý lại định
nghĩa của hàm set_name và get_name:
1.
2.
3.
4.
5.
6.
7.
8.

// set name
void Student::set_name(string str){
name=str;
}
// get name
string Student::get_name(){
return name;
}
Ta nhận thấy name là thành phần dữ liệu được khai báo private. Điều đó nghĩa là
chỉ có những hàm thành viên mới có quyền truy nhập đến nó (sau này ta sẽ biết
thêm một trường hợp nữa, đó là hàm bạn – friend, cũng có khả năng truy nhập
đến các thành phần private). Hàm set_name và get_name là hai hàm thành viên
của lớp Student nên nó có thể truy nhập và thao tác được trên dữ liệu name.
Nhưng nỗ lực truy nhập trực tiếp và các thành phần private mà không thông qua
hàm thành viên như ví dụ sau sẽ gây lỗi biên dịch (compilation error):

1. Student studentA; // khai báo đối tượng studentA thuộc lớp Student
2. studentA.name=”Bill Gate”; // error

12


Lập trình hướng đối tượng với C++
1.5. Hàm tạo (Constructor)
1.5.1. Vấn đề đặt ra
Giả sử ta tạo ra một lớp Rectangle (hình chữ nhật) như sau:
C++ Code:
1. #include
2. #include
3. using namespace std;
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.

// class definition
class Rectangle{
private:
int width; // chiều rộng
int height; // chiều cao
public:
// set width & height
void set_width(int); // nhập chiều rộng
void set_height(int); // nhập chiều cao
// get width & height
int get_width(); // lấy chiều rộng
int get_height(); // lấy chiều cao
// calculate area
int area(); // tính diện tích
};
// member function definitions
// set width
void Rectangle::set_width(int a){
width=a;
}
// set height
void Rectangle::set_height(int b){
height=b;
}
// get width
int Rectangle::get_width(){
return width;
}
// get height
int Rectangle::get_height(){

13


Lập trình hướng đối tượng với C++
41.
42.
43.
44.
45.
46.
47.

return height;
}
// calculate area
int Rectangle::area(){
return height*width;
}

Điều gì sẽ xảy ra khi ta gọi hàm tính diện tích area trước khi thiết lập chiều
rộng và chiều cao cho hình chữ nhật như trong đoạn chương trình sau:
1. Rectangle my_rectangle; // khai báo đối tượng my_rectangle thuộc lớp

Rectangle
2. cout << my_rectangle.area() << endl; // in ra màn hình diện tích của
my_rectangle
Giá trị thu được trên màn hình có thể là một số âm ! Câu lệnh thứ nhất khai báo
đối tượng my_rectangle, chương trình sẽ cấp phát bộ nhớ cho các thành phần dữ
liệu width vàheight, giả sử width rơi vào ô nhớ mà trước đó có lưu trữ giá trị 20,
còn height rơi vào ô nhớ trước đó có lưu trữ giá trị -3. Ngay sau đó, câu lệnh thứ hai
yêu cầu tính diện tích củamy_rectangle rồi hiển thị ra màn hình, và kết quả ta thu
được là diện tích my_rectangle bằng -60 ! Để đảm bảo mọi đối tượng đều được khởi
tạo hợp lệ trước khi nó được sử dụng trong chương trình, C++ cung cấp một giải pháp
đó là hàm tạo (constructor).
1.5.2. Hàm tạo
Constructor là một hàm thành viên đặc biệt có nhiệm vụ thiết lập những giá trị
khởi đầu cho các thành phần dữ liệu khi đối tượng được khởi tạo. Nó có tên giống hệt
tên lớp để compiler có thể nhận biết được nó là constructor chứ không phải là một
hàm thành viên giống như các hàm thành viên khác. Trong constructor ta có thể gọi
đến các hàm thành viên khác. Một điều đặc biệt nữa là constructor không có giá trị trả
về, vì vậy không được định kiểu trả về nó, thậm chí là void. Constructor phải được
khai báo public. Constructor được gọi duy nhất một lần khi đối tượng được khởi tạo.
Những lớp không khai báo tường minh constructor trong định nghĩa lớp, như lớp
Rectangle ở trên của chúng ta, trình biên dịch sẽ tự động cung cấp một “constructor
mặc định" (default constructor). Construtor mặc định này không có tham số, và cũng
không làm gì cả. Nhiệm vụ của nó chỉ là để lấp chỗ trống. Nếu lớp đã khai báo
constructor tường minh rồi thì default constructor sẽ không được gọi. Bây giờ ta sẽ
trang bị constructor cho lớp Rectangle:
1. class Rectangle{
2.
private:
3.
int width;
4.
int height;
5.
public:

14


Lập trình hướng đối tượng với C++
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.

// constructor
Rectangle();
/* các hàm khác khai báo ở chỗ này */
};
// member function definitions
// constructor
Rectangle::Rectangle(){
width=0;
height=0;
}
/* các hàm khác định nghĩa ở đây */
Khi đó câu lệnh

1. Rectangle my_rectangle;

sẽ tạo ra một đối tượng my_rectangle có width=0 và height=0.
1.5.3. Thiết lập giá trị cho các thành phần dữ liệu khi khởi tạo đối tượng
Một vấn đề được đặt ra là có thể khởi tạo những giá trị nhau khác cho các đối
tượng ngay lúc khai báo không? Giống như với kiểu int:
1. int a=10;
2. int b=100;
3. int c=1000
4.

;
C++ hoàn toàn cho phép chúng ta làm điều này. Có một số cách để thiết lập
những giá trị khác nhau cho các thành phần dữ liệu trong khi khai báo.
-

Cách thứ nhất: viết thêm một hàm tạo nữa có tham số

C++ hoàn toàn không giới hạn số lượng constructor. Chúng ta thích viết bao
nhiêu constructor cũng ok. Đây chính là khả năng cho phép quá tải hàm của C++
(function overloading), trong trường hợp của ta là quá tải hàm tạo. Tức là cùng
một tên hàm nhưng có thể định nghĩa theo nhiều cách khác nhau để dùng cho
những mục đích khác nhau. Để quá tải một hàm (bất kỳ) ta chỉ cần cho các
hàm khác nhau về số lượng tham số , kiểu tham số còn giữ nguyên tên hàm. Tạm
thời cứ thế đã, tớ sẽ đề cập rõ hơn trong một bài riêng cho functions. Bây giờ ta
sẽ bổ sung thêm một constructor nữa vào định nghĩa lớp Rectangle:
C++ Code:
1. class Rectangle{
2.
private:
3.
int width;

15


Lập trình hướng đối tượng với C++
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.

int height;
public:
// constructor
Rectangle(); // hàm tạo không có tham số
Rectangle(int, int); // hàm tạo với hai tham số
/* các hàm khác khai báo ở chỗ này */
};
// member function definitions
// constructor with no parameters
Rectangle::Rectangle(){
width=0;
height=0;
}
// constructor with two parameters
Rectangle::Rectangle(int a, int b){
width=a;
height=b;
}
/* các hàm khác định nghĩa ở đây */

Bây giờ ta sẽ test bằng chương trình sau:
1.
2.
3.
4.
5.

Rectangle rectA; // gọi hàm tạo không tham số
Rectangle rectB(3,4); // gọi hàm tạo có tham số
cout << rectA.area() << endl; // kết quả là 0
cout << rectB.area() << endl; // kết quả là 12
C++ sẽ tự nhận biết để gọi constructor phù hợp. Trong đoạn chương trình trên,
câu lệnh thứ nhất khởi tạo đối tượng rectA nhưng không kèm theo truyền tham
số vào, nên compiler sẽ gọi tới hàm tạo thứ nhất, tức hàm tạo không có tham số.
Sau câu lệnh này rectA đều có width và height đều bằng 0. Câu lệnh thứ hai khởi
tạo đối tượng rectB, nhưng đồng thời truyền vào hai đối số là 3 và 4. Do đó
compiler sẽ gọi đến hàm tạo thứ hai. Sau câu lệnh
này rectB có width=3 còn height=4. Và kết quả ta được diện tích thằng rectA là
0, cònrectB là 12.
-

Cách thứ hai: dùng đối số mặc định.

Chúng ta vẫn làm việc với lớp Rectangle ở trên và sẽ chỉ dùng một hàm tạo
nhưng “chế biến” nó một chút:
1. class Rectangle{

16


Lập trình hướng đối tượng với C++
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.

private:
int width;
int height;
public:
// constructor
Rectangle(int =0, int =0); // hàm tạo với đối số mặc định
/* các hàm khác khai báo ở chỗ này */
};
// member function definitions
// constructor with default arguments
Rectangle::Rectangle(int a, int b){
width=a;
height=b;
}
/* các hàm khác định nghĩa ở đây */

Chúng ta chú ý đến khai báo của hàm tạo:
1. Rectangle(int =0, int =0);

Khai báo này cho biết, khi khai báo đối tượng, nếu đối số nào bị khuyết (tức
không được truyền vào) thì sẽ được mặc định là 0. Và để đảm bảo không xảy ra
sự nhập nhằng, C++ yêu cầu tất cả những đối số mặc định đều phải tống
sang bên phải nhất (rightmost), tức ngoài cùng bên phải. Vì vậy:
1. Rectangle rectA; // sẽ gán width=0, height=0
2. Rectangle rectB(4); // sẽ gán width=4, height=0
3. Rectangle rectC(2,6); // sẽ gán width=2, height=6

Chú ý: giá trị mặc định (ví dụ int =0) chỉ được viết lúc khai báo hàm, chứ không
phải lúc định nghĩa hàm. Nếu ta viết lại những giá trị mặc định này trong danh
sách tham số lúc định nghĩa hàm sẽ gây lỗi biên dịch.
1. // lỗi đặt đối số mặc định khi định nghĩa hàm
2. Rectangle::Rectangle(int a=0, int b=0){ // error
3.
width=a;
4.
height=b;
5. }

1.5.4. Hàm tạo mặc định
Như đã nói ở trên, nếu ta không cung cấp hàm tạo cho lớp thì compiler sẽ làm
điều đó thay chúng ta. Nó sẽ cung cấp một hàm tạo không tham số và không làm gì cả
ngoài việc lấp chỗ trống. Đôi khi hàm tạo không có tham số do người dùng định nghĩa
cũng được gọi là hàm tạo mặc định (hay ngầm định). Chúng ta xem xét chuyện gì sẽ

17


Lập trình hướng đối tượng với C++
xảy ra nếu như không có hàm tạo ngầm định khi khai báo một mảng các đối tượng. Ví
dụ vẫn là lớp Rectangle với hàm tạo hai tham số:
1. class Rectangle{
2.
private:
3.
int width;
4.
int height;
5.
public:
6.
// constructor
7.
Rectangle(int, int); // hàm tạo với hai tham số
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.

/* các hàm khác khai báo ở chỗ này */
};
// member function definitions
// constructor with 2 parameters
Rectangle::Rectangle(int a, int b){
width=a;
height=b;
}
/* các hàm khác định nghĩa ở đây */

Nếu như ta khai báo một mảng tầm chục thằng Rectangle thì chuyện gì sẽ xảy
ra?
1. Rectangle my_rectangle(1,2); // 1 thằng thì ok
2. Rectangle rect_array[10]; // chục thằng thì có vấn đề - error

Điều này là do ta cần khai báo 10 thằng Rectangle nhưng lại không cung cấp đủ
tham số cho chúng, vì hàm tạo yêu cầu hai tham số cần phải được truyền vào. Giải
quyết chuyện này bằng cách bổ sung thêm một hàm tạo không có tham số hoặc chỉnh
lại tất cả các tham số của hàm tạo hai tham số bên trên thành dạng đối số mặc định là
xong.

18


Lập trình hướng đối tượng với C++
1.6. Hàm (Functions)
1.6.1. Tại sao phải dùng hàm
Hàm là một tập các câu lệnh được nhóm lại dưới một cái tên, gọi là tên hàm,
dùng để thực hiện một công việc xác định nào đó. Những vấn đề thực tế thường rất
lớn và phức tạp. Cách tốt nhất để phát triển cũng như bảo trì một phần mềm là phân
chia và tổ chức nó thành những khối nhỏ hơn, đơn giản hơn. Kỹ thuật này được biết
với tên gọi quen thuộc là “chia-để-trị” (devide-and-conquer). Tư tưởng chia-để-trị là
một trong những nguyên lý quan trọng của lập trình cấu trúc, tuy nhiên lập trình
hướng đối tượng cung cấp những cách thức phụ trợ mạnh mẽ hơn để tổ chức chương
trình. Nếu từng phần công việc vẫn còn lớn thì lại chia nhỏ tiếp cho tới khi đủ đơn
giản, và tương tự cũng có các hàm tương ứng với những phần này. Đó là nguyên nhân
thứ nhất dẫn đến việc sử dụng hàm. Một nguyên nhân nữa thúc đẩy việc sử dụng hàm
là khả năng tận dụng lại mã nguồn. Một hàm khi đã được viết ra có thể được sử dụng
lại nhiều lần. Ví dụ: hàm strlen trong thư viện của C được viết để tính chiều
dài của một xâu bất kỳ, vì vậy khi muốn tính độ dài của một xâu nào đó ta chỉ việc gọi
hàm này là ok, thay vì lại phải viết một đoạn chương trình loằng ngoằng để đếm từng
ký tự trong xâu.
1.6.2. Khai báo và định nghĩa một hàm
Một nguyên tắc của C và C++ là mọi thứ cần phải được khai báo trước lần sử
dụng đầu tiên. Bạn không thể sử dụng một biến hay hàm nếu như không nói trước cho
trình biên dịch biết điều đó. Vì vậy trước khi sử dụng hàm ta phải khai báo. Nếu ta chỉ
khai báo tên hàm còn viết định nghĩa thân hàm ở chỗ khác thì đó là sự khai báo bình
thường (Declaration) hay khai báo nguyên mẫu hàm(Prototype). Còn nếu ta viết luôn
cả thân hàm thì đó là một sự định nghĩa hàm (Definition).
-

Khai báo nguyên mẫu hàm (function prototype declaration)

1. (danh_sách_tham_số);

Ví dụ:
2. int square(int); // tính bình phương của một số nguyên

Khai báo này giống như việc bạn nói với trình biên dịch: “này compiler, sẽ có
một hàm kiểu như thế xuất hiện trong chương trình, vì vậy nếu chú nhìn thấy chỗ
nào gọi cái hàm này thì đừng có xoắn, anh sẽ viết định nghĩa nó ở một xó nào
đấy trong chương trình.”
-

Định nghĩa hàm (function definition)

Bây giờ giả sử thằng compiler nó tạm thời “tin” theo lời chúng ta, rằng sẽ có
định nghĩa đầy đủ cho cái nguyên mẫu được khai báo trên kia, và nó bắt đầu dịch
tiếp. Giả sử nó gặp một câu lệnh như sau:

19


Lập trình hướng đối tượng với C++
1. x=square(y); // giả thiết x, y đã được khai báo trước

Vì đã được thông báo từ trước nên nó sẽ “không xoắn”, mà bắt đầu tìm định
nghĩa cho hàm này, vì nó vẫn tin vào “lời hứa” của chúng ta. Nếu nó tìm mà
không thấy, nghĩa là chúng ta đã “lừa” nó, nó sẽ báo lỗi. Vì vậy ta phải cung cấp
định nghĩa cho hàm như đã cam kết. Dưới đây là định nghĩa cho hàm square:
1. int square(int n){
2.
return n*n;
3. }

Định nghĩa này bao gồm phần header (hay còn gọi là declarator) và theo sau nó
là phần thân hàm (body). Phần header phải tương thích với nguyên mẫu hàm,
nghĩa là phải có cùng kiểu trả về, cùng tên, cùng số lượng tham số và cùng kiểu
tham số ở những vị trí tương ứng.
Một số chú ý nhỏ
Tham số (parameters) khác với đối số. Tham số (hay còn gọi là tham số
hình thức) là những biến tượng trưng ở trong danh sách tham số, xuất hiện
lúc khai báo nguyên mẫu hoặc định nghĩa hàm, còn đối số là dữ liệu
truyền vào cho hàm khi hàm được gọi. Ví dụ:
1. int min(int a, int b); // a và b là tham số
2. minimum=min(x,y); // x, y đối số được truyền vào cho hàm
-

o

Trong danh sách tham số ở khai báo nguyên mẫu có thể chỉ cần nêu kiểu
dữ liệu của của tham số mà không cần nêu tham số, lúc định nghĩa mới
cần. Ví dụ
int min(int, int); // khai báo nguyên mẫu không có tham số hình thức mà chỉ
có kiểu

int min(int a, int b){ // bây giờ mới cần tham số hình thức
// thân hàm ở đây
}
o

1.
2.
3.
4.
5.

1.6.3. Truyền đối số cho hàm (Passing Arguments to Functions)
Đối số (argument) là một mẩu dữ liệu nào đó như một giá trị nguyên, một ký tự
thậm chí là cả một cấu trúc dữ liệu hết sức rối rắm như một mảng các đối tượng chẳng
hạn, được truyền vào cho hàm. Có nhiều cách truyền đối số cho hàm, ta sẽ xem xét
các cách này và phân tích ưu nhược điểm của chúng.
-

Truyền hằng (passing constants)

Xét hàm square ở trên, câu lệnh:
1. x=square(10);

Sẽ thực hiện tính bình phương của 10, rồi gán kết quả thu được cho biến x. Sau
câu lệnh này x có giá trị là 100. Ta thấy đối truyền vào cho hàm square ở đây là
20


Lập trình hướng đối tượng với C++
một hằng số kiểu int. điều này hoàn toàn hợp lệ miễn là hằng truyền vào có kiểu
tương thích với kiểu của tham số hình thức. Ta cũng có thể truyền cho hàm một
hằng ký tự, hoặc hằng xâu ký tự. Ví dụ cho việc này là hàm printf của C.
Truyền biến (passing variables)

-

Đây là cách truyền đối số phổ biến nhất cho hàm. xét đoạn chương trình sau:
1. n=10;
2. x=square(n);

Kết quả thu được sau khi kết thúc đoạn chương trình trên vẫn là x=100. Tuy
nhiên truyền biến cho hàm có một số điều “thú vị”. Ta có thể truyền biến cho
hàm dưới hai hình thức là truyền bằng tham trị (pass-by-value) và truyền bằng
tham chiếu (pass-by-reference). Mỗi cách có một ưu, nhược điểm riêng và ta sẽ
phân tích chúng để đưa ra cách tối ưu nhất.
Truyền bằng tham trị (pass-by-value)
Xét đoạn chương trình sau:
1. #include
2. using namespace std;
o

3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.

int min(int a, int b){
return (a}
int main(){
int x=5;
int y=10;
int z=min(x,y); // z là giá trị nhỏ nhất trong hai giá trị x, y
cout << "min= " << z << endl; // hiển thị giá trị nhỏ nhất
return 0;
}

Chúng ta đều đoán được kết quả là màn hình hiển thị min= 5, nhưng thực
sự thì chương trình trên hoạt động như thế nào? Ta để ý vào câu lệnh:
1. int z=min(x,y);
Khi gặp câu lệnh này, compiler sẽ gọi đến hàm min và thực hiện truyền x
và y làm đối số. Tuy nhiên, đây là truyền theo tham trị. Tức là x, y không
được truyền trực tiếp vào trong hàm min mà compiler thực hiện một công
đoạn như sau: đầu tiên nó tạo ra hai biến tạm a, b có kiểu int, rồi copy giá
trị của x, y vào hai biến đó. Sau đó hai biến tạm đó được tống vào trong
hàm min và thực tế hàm min đang thao tác trên “bản sao” của x và y chứ
không phải trực tiếp trên x, y. Điều này có cái lợi mà cũng có cái hại. Cái

21


Lập trình hướng đối tượng với C++
lợi là do không bị thao tác trực tiếp nên các biến ban đầu (ở đây là x và y)
sẽ không có khả năng bị "dính" những sửa đổi không mong muốn do
hàm min gây ra. Còn cái hại là nếu như ta muốn sửa đổi giá trị của biến
ban đầu thì lại không được (ví dụ muốn hoán đổi nội dung của hai biến x,
y cho nhau) vì mọi thao tác là trên bản sao của x, y chứ không phải trên x,
y. Thêm nữa, khi tạo bản sao cần phải tạo ra những biến tạm copy dữ liệu
từ biến gốc sang biến tạm. Điều này gây ra những chi phí về bộ nhớ cũng
như về thời gian, đặc biệt khi kích thước của các đối số lớn hoặc được
truyền nhiều lần.

22


Lập trình hướng đối tượng với C++
Truyền theo tham chiếu (pass-by-reference)
Như đã nói ở trên truyền theo tham trị không truyền bản thân biến vào mà
chỉ truyền bản sao cho hàm. Do đó có những hạn chế nhất định của nó.
Bây giờ mời bà con và cô bác ngâm cứu cách truyền thứ hai, truyền theo
tham chiếu (passing-by-reference). Có hai cách để truyền theo tham
chiếu là truyền tham chiếu thông qua tham chiếu (pass-by-reference-withreferences), và truyền tham chiếu thông qua con trỏ (pass-by-referencewith-pointers). Nghe có vẻ hơi lằng nhằng nhưng mình sẽ giải thích ngay
bây giờ.
Truyền tham chiếu thông qua con trỏ
Chắc chắn các bạn đã quen thuộc với con trỏ rồi nên mình sẽ không nói
nhiều về phần này. Tuy nhiên có thể mình sẽ dành ra một bài để viết riêng
về mục con trỏ nếu thấy cần thiết để đảm bảo tính hệ thống. Nhắc lại, con
trỏ là một biến đặc biệt lưu trữ địa chỉ của một biến mà nó trỏ tới. Cú pháp
khai báo con trỏ cũng như cách sử dụng nó được mình họa trong chương
trình sau:
1. #include
2. using namespace std;
o

3.
4. int main(){
5.
int x; // khai báo một biến nguyên
6.
int *ptr; // khai báo một con trỏ kiểu nguyên
7.
ptr=&x; // ptr trỏ tới x hay gán địa chỉ của x cho ptr
8.
9.
*ptr=10; // gán giá trị 10 cho vùng nhớ mà ptr trỏ tới, cụ thể ở đây là x
10.
cout << x << endl; // in giá trị của x, bây giờ là 10
11.
12.
return 0;
13. }

Chương trình trên nhắc lại những kiến thức hết sức cơ bản về con trỏ. Bây
giờ ta sẽ xem xét cách truyền đối số cho hàm thông qua con trỏ như thế
nào. Ví dụ chương trình sau thực hiện việc hoán đổi nội dung hai biến cho
nhau, một chương trình hết sức cổ điển gần như lúc nào cũng được lôi ra
làm ví dụ khi nói về truyền đối số bằng con trỏ:
1. #include
2. using namespace std;
3.
4. void swap(int* a, int* b){ // hoán đổi nội dung hai biến cho nhau
5.
int temp;
6.
temp=*a;
7.
*a=*b;
8.
*b=temp;
9. }
10.
11. int main(){
12.
int x=5;

23


Lập trình hướng đối tượng với C++
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29. }

int y=7;
// trước khi gọi swap
cout << "Before calling swap" << endl;
cout << "x= " << x << endl;
cout << "y= " << y << endl;
// gọi swap
swap(&x, &y);
// sau khi gọi swap
cout << "After calling swap" << endl;
cout << "x= " << x << endl;
cout << "y= " << y << endl;
return 0;
Nhận thấy kết quả sẽ là
Before calling swap
x= 5
y= 7
After calling swap
x=7
y=5
Mình sẽ giải thích về bản chất của cách truyền này. Để ý câu lệnh:

1. swap(&x, &y);

Câu lệnh này truyền địa chỉ của x và y chi hàm swap, và hàm swap cứ thế
mò thẳng đến vùng nhớ của x và y mà thao tác. Điều này nghĩa mọi mọi
thao tác trong hàm swap có thể làm thay đổi biến ban đầu, và do đó nó cho
phép hoán đổi nội dung của x, y cho nhau. Truyền tham chiếu thông qua
con trỏ cũng có cái lợi và cái hại. Cái lợi thứ nhất là nó cho phép thao tác
trực tiếp trên biến ban đầu nên có thể cho phép sửa đổi nội dung của biến
nếu cần thiết (như ví dụ hàm swap trên). Thứ hai, cũng do thao tác trực
tiếp trên biến gốc nên ta không phải tốn chi phí cho việc tạo biến phụ hay
copy các giá trị sang biến phụ. Cái hại là làm giảm đi tính bảo mật của dữ
liệu. Ví dụ trong trường hợp hàm min ở trên ta hoàn toàn không mong
muốn thay đổi dữ liệu của biến gốc mà chỉ muốn biết thằng nào bé hơn.
Nhưng nếu truyền theo kiểu con trỏ như thế này có khả năng ta “lỡ” sửa
đổi biến gốc và do đó gây ra lỗi (sợ nhất vẫn là những lỗi logic, nó không
chạy thì còn đỡ, nó chạy sai mới đểu).
Truyền tham chiếu thông qua tham chiếu
Tham chiếu (reference) là một khái niệm mới của C++ so với C. Nói nôm
na nó là một biệt danh hay nickname của một biến. Chương trình sau
minh họa đơn giản cách sử dụng tham chiếu trong C++
1. #include
24


Lập trình hướng đối tượng với C++
2. using namespace std;
3.
4. int main(){
5.
int x; // khai báo biến nguyên x
6.
int &ref=x; // tham chiếu ref là nickname của x
7.
8.
ref=10; // gán ref=10, nghĩa là x cũng bằng 10
9.
cout << x << endl; // in giá trị của x, tức là 10, lên màn hình
10.
return 0;
11. }

Một lưu ý về tham chiếu là nó phải được khởi tạo ngay khi khai báo. Câu
lệnh như sau sẽ báo lỗi:
1. int &ref; // lỗi không khởi tạo ngay khi khai báo

Mọi thay đổi về trên tham chiếu cũng gây ra những thay đổi tương tự trên
biến vì bản chất nó là hai cái tên cho cùng một biến. Vì vậy ta cũng có thể
dùng tham chiếu để truyền đối số cho hàm với tác dụng giống hệt con trỏ.
Bây giờ ta sẽ cải tiến lại hàm swap bên trên bằng cách dùng tham chiếu.
1. #include
2. using namespace std;
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.

1.
2.
3.
4.

// hàm swap
void swap(int& a, int& b){
int temp;
temp=a;
a=b;
b=temp;
}
int main(){

// gọi hàm swap
swap(x,y);

}
Nhận xét: về cơ bản tác dụng của việc truyền theo tham chiếu và truyền
theo con trỏ là hòan toàn như nhau, tuy nhiên dùng tham chiếu sẽ tốt hơn
vì nó làm cho “giao diện” của hàm thân thiện hơn. Hãy so sánh việc truyền
tham số của hai cách:
// theo con trỏ
swap(&x, &y);
// theo tham chiếu
swap(x, y);
Lợi ích của việc truyền tham chiếu hằng (const references)
Bây giờ ta lại đặt ra vấn đề: liệu có cách nào tận dụng được tính an
25


Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Tải bản đầy đủ ngay

×