Tải bản đầy đủ (.docx) (49 trang)

Hướng dẫn lập trình hướng đối tượng với C++

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (418.03 KB, 49 trang )

Hướng dẫn lập trình hướng đối tượng với C++
Chào mọi người trong congdongcviet. Mình là mem mới, mới vào diễn đàn không lâu.
Thực ra hồi trước lúc đang học C cũng có vào diễn đàn rồi nhưng chủ yếu mục đích là
vào “chơm” tài liệu và có thắc mắc gì thì nhờ các cao thủ trợ giúp chứ cũng chả pốt
piếc gì hết
Mình thấy mọi người hướng dẫn rất nhiệt tình, thậm chí ngay cả bác Ácmin lúc nào cũng thấy online trợ giúp mọi người. Nghĩ lại thấy mình cũng “tư lợi” quá,
chỉ nghĩ đến bản thân. Haizzz … bây giờ thấy lương tâm nó cắn rứt quá, hix hix
.
Dạo này mới tập tẹ học lập trình hướng đối tượng (bằng C++), thấy cũng hay hay, hiểu
hiểu nên muốn viết mấy bài chia sẻ những gì mình học được về OOP cũng như về C++,
gọi là đóng góp chút gì đó cho lương tâm nó đỡ cắn rứt. Hy vọng giúp ích cho một số
bạn. Mình nói trước là mình cũng mới học thơi nên biết gì viết nấy, nếu có gì sai sót
mong mọi người tham gia góp ý. Đây là bài đầu tiên

BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT
ORIENTED PROGRAMMING
Tất cả các ngơn ngữ lập trình đều sinh ra để hỗ trợ một hoặc một số phong cách lập
trình hay một mơ hình lập trình nào đó (programming paradigm). Vì vậy trước khi bắt
tay vào học ngơn ngữ ta nên tìm hiểu sơ lược về mơ hình lập trình được ngơn ngữ hỗ
trợ mà ta dự định viết chương trình theo mơ hình đó. Cụ thể, nói “lập trình hướng đối
tượng với C++” thì ta phải biết sơ sơ về hướng đối tượng trước khi “ngâm cứu” C++. Vì
vậy bài đầu tiên này mình muốn dành để nói về lập trình hướng đối tượng là gì, và quan
điểm của giới lập trình về nó như thế nào, tại sao nó lại là một mơ hình tiên tiến và bạn
sẽ khơng phải hối hận khi bỏ thời gian và công sức ra để học nó.
Ngày xửa ngày xưa, khoảng ba chục năm về trước, quy mơ các của các dự án phần
mềm cịn nhỏ, các lập trình viên gần như có thể viết ngay được chương trình mà khơng
cần suy nghĩ nhiều (giả sử rằng khơng có lập trình viên nào bị thiểu năng về trí tuệ
). Thời đó lập trình cấu trúc (structured programming) hay cịn gọi lập trình thủ tục
(procedural programing) là kỹ thuật lập trình chủ yếu. Tớ sẽ nói sơ qua một chút về kỹ
thuật này (trong phạm vi hiểu biết). Theo quan điểm của lập trình cấu trúc, người ta
xem chương trình là một “cơng việc lớn” cần phải xử lý. Để giải quyết “công việc lớn”


này, người ta tìm cách chia thành các phần cơng việc nhỏ hơn và mỗi phần này sẽ được
quẳng cho một hàm đảm nhiệm. Chương trình chính sẽ gọi đến mỗi hàm vào những
thời điểm cần thiết. Trong mỗi hàm, nếu như phần cơng việc vẫn cịn lớn, thì ta lại chia
nhỏ tiếp cho tới khi vấn đề trở nên đủ đơn giản. Và dĩ nhiên để giải quyết những phần
con đó ta cũng phải quẳng chúng cho các hàm tương ứng. Quá trình này được gọi là
“làm mịn” hay “tinh chế từng bước” (stepwise refinement). Việc trao đổi dữ liệu giữa
các hàm được thực hiện thông qua việc truyền đối số hoặc các biến, mảng tồn cục.
Như vậy có thể coi chương trình là một tập hợp các hàm được thiết kế để xử lý các
phần công việc được giao. Các ngơn ngữ lập trình hướng thủ tục thường gặp là C,
Pascal, FORTRAN … và cả C++. Tuy nhiên C++ còn được thiết kế để hỗ trợ cả lập trình
hướng đối tượng nữa. Một chương trình viết theo hướng cấu trúc sẽ tập trung vào quá
trình xử lý. Nghĩa là mỗi câu lệnh chỉ dẫn cho máy tính làm một việc gì đó, kiểu như:
nhận 2 số ngun từ bàn phím, cộng chúng lại với nhau, rồi đem chia đôi, hiển thị kết
quả lên màn hình. Một chương trình là một tập các chỉ dẫn. Lập trình cấu trúc tỏ ra khá
hiệu quả khi quy mơ chương trình cịn nhỏ, nhưng khi quy mơ chương trình lớn dần lên
và phức tạp hơn thì nó bộc lộ nhiều khiếm khuyết. Có thể nêu ra một số vấn đề sau:
1. Trọng tâm vào “hành động” hơn là “dữ liệu”: thực tế dữ liệu là cái tối


thượng mà chúng ta quan tâm. Mọi chương trình đều nhằm mục đích nhét dữ
liệu vào input rồi chờ đợi kết quả ở output. Rõ ràng mục đích của ta là dữ liệu
đầu ra, mặc kệ chương trình nó muốn xử lý cái gì thì xử lý, ta chỉ quan tâm đến
kết quả đầu ra có đạt yêu cầu hay khơng. Tuy nhiên lập trình cấu trúc q chú
trọng đến việc thiết kế các hàm (hành động) mà xem nhẹ dữ liệu, đây là hạn
chế thứ nhất.
2. Tính bảo mật của dữ liệu khơng cao: (nếu như khơng muốn nói là khơng có).
Dữ liệu trong chương trình gần như là của chung, và có thể dễ dàng truy cập
hay sửa đổi một cách vô tội vạ. Những hàm không phận sự cũng có thể tọc
mạch vào vùng dữ liệu mà nó “chằng liên quan” và sửa đổi nó
. Điều này làm

chương trình rất dễ phát sinh lỗi đặc biệt là những “lỗi tinh vi” hoặc “lỗi logic”.
Và khi có lỗi thì rất khó debug vì phạm vi khoanh vùng là rất rộng (vì ai cũng có
thể tọc mạch vào dữ liệu nên không biết nghi cho thằng nào). Đây là hạn chế
thứ hai.
3. Tách rời dữ liệu với “hành động” liên quan: không phải tất cả các hàm
được viết ra để dùng cho tất cả dữ liệu, và ngược lại. Mỗi nhóm dữ liệu chỉ sử
dụng một nhóm các hàm “dành riêng cho chúng”. Trong lập trình, việc “đóng
gói” dữ liệu và hàm liên quan được gọi là “mơ-đun hóa” (modularization). Điều
này có hai cái lợi. Thứ nhất, các hàm và dữ liệu được nhóm lại với nhau nên “gọn
gàng” hơn và dễ kiểm sốt hơn. Thứ hai, thơng thường chỉ những hàm trong
khối mới có thể truy nhập vào dữ liệu của khối. Do đó hạn chế sự tọc mạch từ
bên ngồi, tính bảo mật dữ liệu cao hơn, hạn chế lỗi và phạm vi khoanh vùng lỗi
sẽ được thu hẹp. Tuy nhiên, lập trình cấu trúc khơng làm được điều này. Đây là
hạn chế thứ ba.
4. Phụ thuộc nặng nề vào cấu trúc dữ liệu và thuật toán: minh chứng cho
điều này là câu nói nổi tiếng của bác Niklaus Wirth (creator of Pascal):
Algorithms + Data Structures = Programs. Cũng xin nói thêm mơ hình lập trình
hướng cấu trúc được dựa trên mơ hình tốn học của Bohm và Guiseppe (nói thật
là mình khơng biết hai bác này
), theo đó, một chương trình máy tính đều có
thể viết dựa trên ba cấu trúc là: tuần tự (sequence), lựa chọn hay rẽ nhánh
(selection) và lặp (repetition). Vì vậy một chương trình được xem là một chuỗi
các hành động liên tiếp để đi đến kết quả cuối cùng. Và việc thiết kế chương
trình phụ thuộc nặng nề vào việc dùng giải thuật gì và tổ chức dữ liệu như thế
nào. Điều này làm cho việc thiết kế là rất “không tự nhiên” vì nó làm cho q
trình thiết kế phụ thuộc vào cài đặt và khi quy mơ chương trình lớn dần lên sẽ
rất khó triển khai. Đồng thời khi có thay đổi về cấu trúc dữ liệu hoặc nâng cấp
chương trình gần như ta phải viết lại hầu hết các hàm liên quan và sửa đổi lại
thuật tốn
vì mỗi cấu trúc dữ liệu chỉ phù hợp với một số thuật tốn nhất

định. Đây là hạn chế thứ tư.
5. Khơng tận dụng được mã nguồn: mặc dù hàm là một phát minh quan trọng
để tăng cường khả năng sử dụng lại mã nguồn, tuy nhiên trong lập trình cấu
trúc điều này không triệt để. Ta vẫn phải viết lại những đoạn code hao hao giống
nhau để thực hiện những công việc tương tự nhau. Ví dụ: trong C, hàm hàm int
min(int x, int y) có nhiệm vụ tính tốn và trả về min trong hai số nguyên
được truyền vào, còn hàm float min(float x, float y) cũng làm nhiệm vụ
tương tự nhưng là với số thực. Rõ ràng nội dung hai hàm này là giống nhau đến
99%, có khác thì chỉ khác mỗi kiểu int và float, thế nhưng trong C ta vẫn phải
viết hai hàm khác nhau. Trong C++, với định hướng đối tượng ta có thể viết một
hàm dùng để dùng cho mọi kiểu int, float, double. Ngoài ra còn nhiều điểm
mạnh khác mà OOP mang lại để tận dụng tối đa khả năng sử dụng lại mã nguồn
như tính kế thừa (inheritance), đa hình (polymorphism). Đây là hạn chế thứ năm


của lập trình cấu trúc.
Nói chung mình chỉ mới bới ra được có thế thơi, ai biết thêm cái nào thì bổ sung nhé. Rõ
ràng với nhiều hạn chế như vậy thì lập trình cấu trúc khơng phải là giải phải pháp tốt.
Và những nỗ lực để vá những lỗ hổng này dẫn đến sự ra đời của một kỹ thuật lập trình
mới lập trình hướng đối tượng (object oriented programming – OOP). Mình cũng nói sơ
qua một chút về OOP. Khác với lập trình cấu trúc, OOP coi chương trình là tập hợp của
các đối tượng có quan hệ nào đó với nhau. Mỗi đối tượng có dữ liệu và phương thức của
riêng mình. Ví dụ một đối tượng Human sẽ có các dữ liệu như: tên, ngày sinh, tuổi, số
chứng minh nhân dân, nghề nghiệp, … blah blah … và được đóng gói cùng các phương
thức đi kèm ví dụ phương thức set_name() sẽ cho phép nhập tên , get_name() sẽ cho
phép lấy tên của đối tượng, tương tự ta cũng cho các phương thức như set_ID(),
get_ID() cho chứng minh nhân dân … Các đối tượng sử dụng những phương thức này để
giao tiếp với bên ngoài. Việc này trước giúp dữ liệu được quan tâm đúng mức, và an
toàn hơn. Mọi truy cập đến dữ liệu đều được kiểm sốt thơng qua các phương thức
được cung cấp sẵn nên hạn chế được những truy cập bất hợp pháp. Tức là đã giải quyết

được ba hạn chế đầu tiên của lập trình cấu trúc. Thứ hai, những thay đổi nào đó về dữ
liệu chỉ ảnh hưởng đến một số lượng hàm nhất định và thay vì phải viết lại hầu hết các
hàm thì ta chỉ phải viết lại một số hàm có liên quan trực tiếp đến sự thay đổi đó. Ví dụ
thành phần dữ liệu name biểu thị tên của một đối tượng Human vì một lý do nào đó
được đổi thành full_name thì những hàm liên quan trực tiếp đến name như set_name()
hay get_name() mới phải viết lại, còn những hàm như set_ID(), get_ID() hay thậm chí
những hàm gọi hàm set_name() và get_name() thì chẳng việc gì cả. Điều này thuận lợi
cho việc nâng cấp và bảo trì. Tức là hạn chế thứ tư đã được giải quyết. OOP cũng cung
cấp những khái niệm về kế thừa và đa hình giúp tận dụng tối đa khả năng sử dụng lại
mã nguồn để giảm bớt vất vả cho lập trình viên cũng như tăng chất lượng phần mềm.
Ví dụ chúng ta có thể tạo ra một lớp (class) mới là Girl, kế thừa từ lớp Human. Khi đó,
một đối tượng thuộc lớp Girl sẽ có đầy đủ các thuộc tính và phương thức của Human,
và ta chỉ cần bổ sung thêm những phần khác như số đo ba vịng: round_1, round_2,
round_3
… Vì thể khơng phải viết lại toàn bộ code cho lớp Girl. Cụ thể như thế nào
thì mình sẽ đề cập trong những bài post sau. Đây chỉ là bài mở đầu để giúp mọi người
so sánh giữa kỹ thuật OOP với kỹ thuật lập trình cấu trúc truyền thống và có những
hình dung cơ bản về OOP, những ưu điểm mà nó mang lại, và vì sao nó lại là một kỹ
thuật được ưa chuộng nhất hiện nay. Trong những năm gần đây, lập trình đã dịch
chuyển từ hướng cấu trúc sang hướng đối tượng vì những ưu điểm và khả năng mạnh
mẽ của nó. Thực tế hiện nay OOP được sử dụng rộng rãi trong các dự án phần mềm,
cịn lập trình cấu trúc chỉ chiếm một phần rất nhỏ thường là giải quyết những vấn đề có
quy mơ nhỏ hoặc dùng trong giảng dạy để giúp người học bước đầu làm quen với lập
trình. Đấy là mình cũng chỉ nghe thiên hạ nói thế thôi chứ cũng mới học OOP nên cũng
không biết là thực tế doanh nghiệp bây giờ nó viết phần mềm bằng ngơn ngữ gì cả.
Nhưng có điều mình cảm nhận được đúng là OOP lập trình sướng hơn hơn lập trình cấu
trúc nhiều, ít ra là cái khoản thiết kế nó trực quan hơn, rõ ràng hơn, thật hơn. Cịn nếu
để ý kỹ thì những cài đặt chi tiết trong hướng đối tượng suy cho cùng vẫn là lập trình
cấu trúc, có điều chúng được tổ chức tốt hơn và được phủ lên một giao diện mang tính
hướng đối tượng mà thôi.

Hết bài 1
p/s: mệt quá, phải nghỉ phát đã, bao giờ có sức thì viết tiếp
__________________

Vấn đề khơng phải là bước nhanh, mà là
luôn luôn bước
Đã được chỉnh sửa lần cuối bởi first_pace : 28-02-2011 lúc 07:35 PM.


Những đặc trưng cơ bản của lập trình hướng đối tượng
BÀI 2. NHỮNG ĐẶC TRƯNG CƠ BẢN CỦA OOP
Chúng ta sẽ xem xét sơ qua một số khái niệm và thành phần chính của OOP nói chung
và của C++ nói riêng
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. Nếu ngồi đời có
cái cơng nơng thì khi thiết kế ta cũng bê ngun cả cái cơng nơng vào trong chương
trình, và như vậy chương trình là tập hợp tất cả các đối tượng có liên quan với nhau. 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 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.
2. Lớp (Classes)
Trong khi đối tượng là một thực thể xác định thì lớp lại là một khái nhiệm trừu tượng. Có
thể so sánh lớp như “kiểu dữ liệu cịn” đối tượng là “biến” có kiểu của lớp. Ví dụ:
lớp Cơng_nơng có thể được mơ tả như sau:
Lớp Cơng_nơng
Thuộc tính:



Nhãn hiệu (ví dụ Lamborghini)



Màu xe



Giá xe



Vận tốc tối đa (ví dụ 300 km/h)

Phương thức:


Khởi động



Chạy thẳng



Rẽ trái / phải




Dừng



Tắt máy

Một khai báo:
C++ Code:
Cơng_nơng cơng_nơng_của_tơi;
Hồn tồn tương tự như khai báo:
C++ Code:

Lựa chọn code | Ẩn/Hiện code

Lựa chọn code | Ẩn/Hiện code
int my_integer;
Tạo một lớp mới tương tự như tạo ra một kiểu dữ liệu mới – kiểu người dùng tự định


nghĩa (user-defined type)
Lớp là “khuôn” để đúc ra các đối tượng.Một đối tượng thuộc lớp Cơng_nơng sẽ có đầy
đủ những thuộc tính và phương thức như được mơ tả ở trên, trong trường hợp
nàycông_nông_của_tôi được đúc ra từ “khuôn” Công_nông. Có một sự tương ứng
giữa lớp và đối tượng nhưng bản chất thì lại khác nhau. Lớp là sự trừu tượng hóa của
đối tượng, cịn đối tượng là một sự thể hiện (instance) của lớp. Đối tượng là một thực
thể có thực, tồn tại trong hệ thống, cịn lớp là khái niệm trừu tượng chỉ tồn tại ở dạng
khái niệm để mơ tả đặc tính chung cho đối tượng. Tất cả những đối tượng của một lớp
sẽ có thuộc tính và phương thức giống nhau.
3. Sự đóng gói và trừu tượng hóa dữ liệu (Encapsulation & Data Abstraction)

Nhìn lại thí dụ trên thì mỗi đối tượng thuộc lớp Cơng_nơng sẽ có cả các thuộc tính và
phương thức được “đóng gói” chung lại. Muốn truy cập vào các thành phần dữ liệu bắt
buộc phải thông qua phương thức, và các phương thức này tạo ra một giao diện để đối
tượng giao tiếp với bên ngoài. Giao diện này giúp cho dữ liệu được bảo vệ và ngăn chặn
những truy cập bất hợp pháp, đồng thời tạo ra sự thân thiện cho người dùng. Ví dụ: nếu
như trong C, một xâu được lưu trữ trong một mảng str nào đó, muốn biết độ dài của
xâu ta phải gọi hàm strlen() trong thư viện<string.h> thì trong C++, nếu str là một
đối tượng thuộc lớp string thì tự nó “biết” kích thước của mình, và chỉ cần
gọi str.size() hoặc str.length() là nó sẽ trả về độ dài của xâu str. Người dùng
hồn tồn khơng cần biết cài đặt chi tiết bên trong lớp string như thế nào mà chỉ cần
biết “giao diện” để có thể giao tiếp với một đối tượng thuộc lớp string là ok. Điều này
dẫn đến sự trừu tượng hóa dữ liệu. Nghĩa là bỏ qua mọi cài đặt chi tiết và chỉ quan tâm
vào đặc tả dữ liệu và các phương thức thao tác trên dữ liệu. Đặc tả về
lớp Cơng_nơng ở trên cũng là một sự trừu tượng hóa dữ liệu.
4. Sự kế thừa (Inheritance)
Những ý tưởng về lớp dẫn đến những ý tưởng về kế thừa. Trong cuộc sống hàng ngày
chúng ta thấy rất nhiều ví dụ về sự kế thừa (tất nhiên là không phải thừa kế vê tài
sản
). Ví dụ:lớp động vật có thể phân chia thành nhiều lớp nhỏ hơn như lớp côn
trùng, lớp chim, lớp động vật có vú, khơng có vú … blah blah … hay lớp phương tiện có
thể chia thành các lớp nhỏ hơn như xe đạp, xe thồ, xe tăng, xích lơ, … Các lớp nhỏ hơn
được gọi là lớp con (subclass) hay lớp dẫn xuất (derived class) còn các lớp phía trên gọi
là lớp cha (super class) hay lớp cơ sở (base class). Một nguyên tắc chung là các lớp con
sẽ có các đặc điểm chung được thừa hưởng từ các lớp cha mà nó kế thừa. Ví dụ lớp cơn
trùng và động vật có vú đều sẽ có những đặc điểm chung của lớp động vật. Và do đó ta
chỉ cần bổ sung những đặc điểu cần thiết thay vì viết lại tịan bộ code. Điều này giảm
gánh nặng cho các lập trình viên và do đó góp phần giảm chi phí sản xuất cũng như
bảo trì, nâng cấp phần mềm.
5. Tính đa hình và sự q tải (Polymorphism & Overloading)
Giả sử ta xây dựng một lớp String để “đúc” ra các đối tượng lưu trữ xâu ký tự, ví dụ ta

có 3 đối tượng s1, s2, s3 thuộc lớp String. Ta muốn thiết kế lớp String sao cho câu
lệnh
C++ Code:
Lựa chọn code | Ẩn/Hiện code
s3 = s1 + s2 ;
sẽ thực hiện việc nối xâu s2 vào đuôi xâu s1 rồi gán kết quả cho xâu s3. Nếu như vậy
cơng việc lập trình trơng sẽ “tự nhiên” hơn. Nhưng thật khơng may ngơn ngữ lập trình
khơng cung cấp sẵn điều này. Sử dụng các toán tử (operators) + và = như trên sẽ gây
lỗi. Tuy nhiên C++ cung cấp một cơ chế cho phép lập trình viên “định nghĩa lại” các
toán tử này để dùng trong các mục đích khác nhau. Việc định nghĩa lại cách sử dụng
toán tử được gọi là “quá tải toán tử” (operator overloading). Một số người gọi nó là “nạp
chồng tốn tử” nhưng mình thích dùng từ q tải hơn vì nghe nó có vẻ “cơ khí”
. C+
+ cho phép q tải hầu hết các tốn tử thơng dụng như +, -, *, /, [], <<, >>,
… Ngoài việc cho phép quá tải tốn tử, C++ cịn cho phép “q tải hàm” (function
overloading), cái này mình sẽ nói kỹ hơn ở bài khác. Nói chung overloading là một cách


cho phép ta sử dụng một toán tử hoặc hàm bằng những cách khác nhau tùy theo ngữ
cảnh, và đó một trường hợp của “tính đa hình” (polymorphism), một tính năng rất quan
trọng của OOP.
Hết bài 2

BÀI 3. MỘT CHƯƠNG TRÌNH C++ ĐƠN GIẢN
Bây giờ chúng ta sẽ xem xét một chương trình C++ đơn giản sau
C++ Code:
Lựa chọn code | Ẩn/Hiện code
// my first program in C++
#include <iostream>
using namespace std;

int main(){
cout << “Hello, Girl” << endl;
return 0;
}
Dòng đầu tiên là một chú thích (comment). Tất cả những gì từ sau ký hiệu // đến hết
dòng được hiểu là chú thích và bị trình biên dịch bỏ qua, hồn tồn khơng gây ảnh
hưởng gì đến hoạt động của chương trình. Mục đích duy nhất của chú thích là làm tăng
tính sáng sủa của chương trình, ta dùng chú thích để giải thích ngắn gọn mục đích của
đoạn code hay của chương trình là gì. Trong ví dụ này, chú thích cho biết đây là chương
trình đầu tiên bằng C++ của tơi. Ta có thể chú thích trên nhiều dịng bằng cặp ký
hiệu /* Here are your comments */. Tuy nhiên tớ nghĩ dùng những chú thích ngắn gọn
trên một dịng sẽ tốt hơn. Thêm nữa, chúng ta nên hạn chế sử dụng chú thích bừa bãi.
Chỉ dùng khi thực sự cần thiết, và nên ngắn gon súc tích. Như vậy giúp ta trọng tâm
hơn vào những phần chính và giúp chương trình khơng bị rối. Hãy để các đoạn code tự
nói lên ý nghĩa của chúng.
Dịng thứ hai là một chỉ thị tiền xử lý (preprocessor directive). Tất cả những gì bắt đầu
bằng # đều là chỉ thị tiền xử lý và được xử lý bởi bộ tiền xử lý trước khi chương trình
được dịch . Nó khơng phải là một câu lệnh (lưu ý mọi câu lệnh đều phải kết thúc bởi
dấu chấm phẩy – semicolon )mà là một chỉ thị hướng dẫn preprocessor nạp nội dung
của tệp <iostream> vào. Việc này giống như ta copy toàn bộ nội dung của
tệp <iostream> rồi paste vào đúng vị trí của chỉ thị #include <iostream>.
<iostream> là một header file liên quan đến những thao tác nhập/ xuất cơ bản. Nó
chứa những khai báo (declarations) cần thiết cho nhập/ xuất, ví dụ trong trường hợp
này sẽ được dùng bởi cout và toán tử <<. Thiếu những khai báo này trình biên dịch sẽ
khơng nhận ra cout và sẽ báo lỗi. Vì vậy cần thiết phải include <iostream>. Chú ý:
đôi khi ta thấy một số chương trình viết
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
Trong khi một số thì lại viết

C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream.h>
Hai cách viết này là khác nhau. Những file có phần mở rộng .h là những file “cũ” có từ
thời kỳ sơ khai của C++ và phần lớn trong số đó kế thừa và phát triển dựa trên các file
của ngôn ngữ C. Khi ANSI và ISO cơng bố chuẩn cho C++ thì các standard header file
mới đều khơng có phần mở rộng. Nói chung thì New Standard Header File so với Classic
Standard Header File không khác nhau nhiều lắm, cái sau cải tiến và hoàn thiện một số
khiếm khuyết của cái trước. Tất nhiên là những cái gì theo chuẩn mới thì thơng thường


sẽ tốt hơn. Tớ sẽ nói rõ hơn về phần này trong phần I/O stream.
Dòng thứ ba đề cập đến một khái niệm đó là “namespace” (đơi khi cịn được gọi là
name scope). Thơng thường một chương trình có chứa nhiều định danh (identifiers)
thuộc nhiều phạm vi (scope) khác nhau. Đôi khi một đối tượng trong phạm vi này bị
trùng tên với một đối tượng khác trong một phạm vi khác. Điều này dẫn đến xung đột
và gây lỗi biên dịch. Sự chồng chéo tên (identifier overlapping ) có thể xảy ra ở nhiều
cấp độ khác nhau, đặc biệt là trong các thư viện cung cấp bởi bên thứ ba. C++
standard nỗ lực giải quyết vấn đề này bằng cách sử dụng namespace. Mỗi namespace
xác định một phạm vi mà trong đó các định danh được nhận biết, ngồi phạm vi này
chúng sẽ không được nhận biết. Để sử dụng một thành phần trong namespace ta có
thể dùng câu lệnh như sau
C++ Code:
Lựa chọn code | Ẩn/Hiện code
my_namespace::member;
Câu lệnh trên sử dụng một identifier có tên là member trong namespace có tên là
my_namespace. Rõ ràng khi một namespace khác (ví dụ: your_namespace) cũng có
một thành phần tên là member thì việc dùng hai tên này không sợ bị chồng chéo lên
nhau. Toán tử :: là toán tử “phân giải phạm vi” (binary scope resolution operator). Trong
câu lệnh trên toán tử :: cho biết rằng định danh member được sử dụng nằm trong

phạm vi của namespace tên là my_namespace chứ không phải your_namespace. Quay
trở lại chương trình của ta, nhận thấy trong hàm main, dịng thứ 5 có sử dụng cout và
endl. Đây là hai định danh được khai báo trong namespace std. Để chương trình “nhận
biết” được cout và endl thì ta có thể dùng cú pháp như vừa nói ở trên tức dòng lệnh thứ
5 được viết lại là:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
std::cout << “Hello, Girl” << std::endl;
Tuy nhiên, rõ ràng cách viết trên là dài dòng. Nếu ta sử dụng nhiều hơn các đinh danh
trong namespace std thì mỗi lần dùng ta lại phải viết thêm std::, vì vậy để có thể sử
dụng được tồn bộ các định danh trong namespace std ta dùng câu lệnh như dòng thứ
3:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
using namespace std;
Những dòng còn lại là định nghĩa hàm main(). Đây là hàm quan trọng nhất trong
chương trình và có nhiệm vụ điều phối và kiểm sốt tồn bộ chương trình, nó gọi
những hàm khác khi cần thiết. Tuy nhiên mình muốn nói một điều hơi bất cập một tý.
Khi mình đọc các tài liệu về C++ thì tất cả đều nói hàm main được gọi và xử lý
trước mọi hàm khác trong chương trình. Điều này có ln ln đúng? Phần này
mình nói hơi ngồi lề một tý, nó liên quan đến constructor của class nên nếu bạn nào
chưa học đến phần này thì có thể bỏ qua. Xét một chương trình sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;
// định nghĩa lớp My_class
class My_class{
private:
int number;

public:
My_class(){ number = 0; } // constructor
}
My_class global_var; // khai báo một biến toàn cục
// hàm main
int main(){


cout << “Is main always called first ?” << endl;
return 0;
}
Biến global_var được khai báo toàn cục bên ngoài tất cả mọi hàm. Khi khai báo
biến global_var thì theo nguyên tắc phải gọi đến constructor của lớp My_class để
khởi tạo number = 0. Vì vậy thực tế trong chương trình trên
constructor My_class() được gọi trước main.
Bây giờ trở lại vấn đề chính, ta sẽ vẫn tiếp tục phân tích nốt mấy câu lệnh còn lại.
Chúng ta để ý đến dòng thứ 5.
C++ Code:
Lựa chọn code | Ẩn/Hiện code
cout << “Hello, Girl” << endl;
Dịng này có tác dụng in dịng text nằm giữa hai dấy nháy kép, cụ thể là “Hello,
Girl” lên màn hình. Chúng ta sẽ phân tích kỹ hơn một chút về nguyên tắc hoạt động
của nó, tuy nhiên chỉ là một sự mô tả rất thô sơ. Để hiểu biết kỹ hơn chúng ta cần biết
những kiến thức về đối tượng, quá tải toán tử, và nhiều vấn đề khác nữa. Trong C, để in
một đoạn văn bản lên màn hình ta có thể dùng hàm printf(). Điều này dễ làm cho ta
lầm tưởng cout cũng là một hàm, nhưng không phải thế. C là ngôn ngữ hướng thủ tục,
cịn C++ là ngơn ngữ hướng đối tượng. Và cout là một đối tượng (object). Nó được định
nghĩa sẵn trong C++ tương ứng với dòng xuất chuẩn (standard output stream). Stream
là một khái niệm trừu tượng được hiểu như luồng dữ liệu (data flow). standard output
stream thông thường được “kết nối” (connected to) hay “chảy” (flows to) tới màn hình.

Tốn tử << được gọi là tốn tử chèn dịng xuất (insertion output stream operator). Nó
ra lệnh chuyển những nội dung của đối tượng bên tay phải sang đối tượng bên tay trái
(giống như chiều mũi tên của toán tử << luôn). Ở đây endl (đối tượng này được khai
báo trong namespace std như đã nói ở trên và tác dụng của nó là kết thúc một dịng,
chuyển sang dịng mới) được chuyển sang bên trái cho xâu ký tự nằm trong dấu nháy
kép. Sau đó tồn bộ dữ liệu này được chuyển sang cho cout, mà cout lại kết nối tới màn
hình nên kết quả là trên màn hình in ra dòng text: Hello, Girl và con trỏ chuyển
xuống dịng mới. Có thể mơ tả bởi hình vẽ sau:
Câu lệnh cuối cùng là:
C++ Code:

Lựa chọn code | Ẩn/Hiện code
return 0;
Câu lệnh này là một cách thông thường để kết thúc hàm main. Nó báo cho trình biên
dịch biết là chương trình kết thúc thành cơng, khơng có lỗi.
Chương trình trên mặc dù rất đơn giản nhưng nó trình bày được cấu trúc chung của của
một chương trình C++. Những bài sau mình sẽ giới thiệu những tiện ích thơng dụng
của C++ và cách sử dụng chúng.
Hết bài 3

Lớp và đối tượng
BÀI 5b. CLASSES & OBJECTS (PART 2)
3. 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 :
C++ Code:
Lựa chọn code | Ẩn/Hiện code



Student studentA; // khai báo đối tượng studentA thuộc lớp Student
studentA.set_name(“Bill Gates”); // gán tên cho studentA là “Bill Gates”
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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
// 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):
C++ Code:
Lựa chọn code | Ẩn/Hiện code
Student studentA; // khai báo đối tượng studentA thuộc lớp Student
studentA.name=”Bill Gate”; // error
4. Ưu điểm của việc đóng gói dữ liệu và phương thức trong một đơn vị thống
nhất – lớp
Việc đóng gói dữ liệu kết hợp với quy định phạm vi truy nhập cho các thành phần của lớp

có nhiều ưu điểm.
Thứ nhất: tạo ra sự gọn gàng dễ kiểm sốt.
Việc đóng gói dữ liệu và các phương thức liên quan giúp chương trình gọn gàng hơn, lập
trình viên dễ kiểm sốt hơn vì tất cả đều được gói gọn trong phạm vi của lớp.
Thứ hai: trừu tượng hóa dữ liệu, thơng qua “giao diện”, tạo thuận lợi cho người
dùng
Việc cung cấp các hàm thành viên để thao tác trên các dữ liệu của đối tượng tạo sự “thân
thiện” cho người dùng. Trong ví dụ lớp Student ở trên, để nhập tên cho một đối tượng ta
chỉ cần gọi hàm set_name thông qua tên đối tượng mà không cần quan tâm đến cài đặt
chi tiết như thế nào.
Thứ ba: tính bảo mật của dữ liêu được nâng cao
Để truy cập đến các dữ liệu private của một đối tượng bắt buộc phải thông qua hàm
thành viên. Tức mọi “giao tiếp” với đối tượng đều phải thông qua “giao diện” mà ta đã
quy định trước. Ví dụ: nhập tên cho studentA thì bắt buộc phải dùng hàm set_name, lấy
tên thì dùng get_name. Do đó sẽ tránh được những truy cập và sửa đổi bất hợp pháp,
đồng thời nếu phát sinh lỗi thì sẽ dễ khoanh vùng hơn. Ví dụ khi yêu cầu trả về mã số
sinh viên của studentA thì phát hiện một số lỗi nào đó. Rõ ràng những lỗi đó chỉ có thể
do các hàm có liên quan trực tiếp
đếnstudent_code như set_student_code hoặc get_student_code chứ không thể
là set_name hay get_name được.
Thứ tư: tăng cường tính độc lập và ổn định hơn cho các thành phần sử dụng lớp
trong chương trình
Giả sử vì một lý do nào đó mà thành phần name buộc phải đổi lại thành full_name thì
chương trình sẽ phải chỉnh sửa lại một chút. Tuy nhiên chỉ những hàm thành viên nào liên
quan trực tiếp đến name mới phải sửa đổi, tức là các hàm set_name và get_name sẽ phải


sửa lại name thành full_name. Tuy nhiên, các hàm gọi đến
hàm set_name và get_name thì khơng hề phải sửa lại, bởi vì nó khơng biết cài đặt chi
tiết bên trong set_name và get_name như thế nào mà chỉ biết “giao diện”

của set_name và get_name vẫn thế, do đó chương trình không phải chỉnh sửa nhiều.
Hết bài 5b

Hàm tạo (constructor)
BÀI 6. HÀM TẠO (CONSTRUCTOR)
Bài này mình sẽ dành để viết về constructor trong C++. Tại sao phải
dùng constructor, dùng nó như thế nào, và những vấn đề cần lưu ý khi sử
dụng constructor sẽ là những nội dung chính được đưa ra.
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:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
#include <string>
using namespace std;
// 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(){
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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code

Rectangle my_rectangle; // khai báo đối tượng my_rectangle thuộc lớp
Rectangle
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 u
cầu tính diện tích của my_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).
2. Hàm tạo (constructor)
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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
class Rectangle{
private:

int width;
int height;
public:
// 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
C++ Code:

Lựa chọn code | Ẩn/Hiện code
Rectangle my_rectangle;
sẽ tạo ra một đối tượng my_rectangle có width=0 và height=0.
3. Thiết lập giá trị bất kỳ 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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
int a=10;

int b=100;
int c=1000;
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++ hồn tồ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:
Lựa chọn code | Ẩn/Hiện code
class Rectangle{
private:
int width;
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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
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òn rectB là 12.
Cách thứ hai: dùng đối số mặc định (default arguments)
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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code

class Rectangle{
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:
C++ Code:

Lựa chọn code | Ẩn/Hiện code
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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
Rectangle rectA; // sẽ gán width=0, height=0
Rectangle rectB(4); // sẽ gán width=4, height=0
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.
C++ Code:
Lựa chọn code | Ẩn/Hiện code
// lỗi đặt đối số mặc định khi định nghĩa hàm
Rectangle::Rectangle(int a=0, int b=0){ // error
width=a;
height=b;
}
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ả ngồ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ẽ 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ố:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
class Rectangle{
private:
int width;
int height;
public:
// constructor
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 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?
C++ Code:
Lựa chọn code | Ẩn/Hiện code
Rectangle my_rectangle(1,2); // 1 thằng thì ok
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à ok
Hết bài 6

Hàm trong C++
BÀI 7a. FUNCTIONS (PART 1)
- from alpha to omega Bài này mình sẽ nói về một số vấn đề nâng cao về hàm trong C++. Vì vậy các bạn cần
phải có một số kiến thức nhất định về hàm. Nói là nâng cao cho nó ốch chứ thực ra nếu
học C++ thì trước sau gì cũng phải biết đến mấy thứ này. Mình sẽ cố gắng trình bày thật
đầy đủ dễ hiểu. Dưới đây là liệt kê những phần sẽ được đề cập trong bài:


Tại sao phải dùng hàm?




Khai báo và định nghĩa hàm (function declarations & function definitions)



Truyền đối số cho hàm (passing arguments to functions)



Trả về giá trị của hàm (returning value from functions)



Đối số mặc định (default argument)




Quá tải hàm (function overloading)



Hàm nội tuyến (inline function)



Phạm vi và lớp lưu trữ (scope and storage classes)




Vai trò của biến toàn cục (role of global variable)



Đối hằng và hàm hằng (const arguments & const functions)

1. Tại sao phải dùng hàm – why, why, why?
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. Không thể giải quyết kiểu “một phát xong ngay”. Kinh nghiệm của các bậc tiền bối
trong lập trình cho thấy rằng, 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. Như
mình đã nói trong bài 1, khi giải quyết một “cơng việc lớn” ta phải chia nhỏ cơng việc
đó ra, mỗi phần sẽ quẳng cho một hàm đảm nhiệm. 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 <string.h> 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. Nói túm lại, nếu bạn
khơng muốn viết chương trình theo kiểu “trâu bị” và “cục súc” thì bạn phải dùng hàm
2. Khai báo và định nghĩa một hàm (function declarations & function
definitions)
Một nguyên tắc muôn thủa 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 đó (chắc compiler cho rằng hành động dùng mà không
xin phép của bạn là một sự "xúc phạm" với nó nên nó bực, nó khơng dịch cho
). 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
ngun mẫu hàm (prototype). Cịn nếu ta viết ln 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)
C++ Code:
Lựa chọn code | Ẩn/Hiện code
<kiểu_trả_về> <tên_hàm>(danh_sách_tham_số);
Ví dụ:
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 chú 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. n tâm đi, anh không lừa chú đâu”
Đị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:


C++ Code:
Lựa chọn code | Ẩn/Hiện code
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:

C++ Code:
Lựa chọn code | Ẩn/Hiện code
int square(int n){
return n*n;
}
Đị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ụ:

C++ Code:

Lựa chọn code | Ẩn/Hiện code
int min(int a, int b); // a và b là tham số
minimum=min(x,y); // x, y đối số được truyền vào cho hàm


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ụ

C++ Code:

Lựa chọn code | Ẩn/Hiện code

int min(int, int); // khai báo ngun 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
}
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. Let’s go!
Truyền hằng (passing constants)
Xét hàm square ở trên, câu lệnh:
C++ Code:

Lựa chọn code | Ẩn/Hiện code
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à một hằng số kiểu
int. điều này hoàn tồ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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
n=10;
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-byreference). 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.
a. Truyền bằng tham trị (pass-by-value)
Xét đoạn chương trình sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;
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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
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 minmà 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àmmin 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 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 hố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.
b. 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-with-references), và truyền tham chiếu thơng qua con trỏ
(pass-by-reference-with-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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;
int main(){
int x; // khai báo một biến nguyên

int *ptr; // khai báo một con trỏ kiểu nguyên
ptr=&x; // ptr trỏ tới x hay gán địa chỉ của x cho ptr
x

*ptr=10; // gán giá trị 10 cho vùng nhớ mà ptr trỏ tới, cụ thể ở đây là
cout << x << endl; // in giá trị của x, bây giờ là 10

return 0;
}
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 hố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ỏ:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;
void swap(int* a, int* b){ // hoán đổi nội dung hai biến cho nhau
int temp;
temp=*a;
*a=*b;
*b=temp;
}
int main(){
int x=5;
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à
Trích dẫn:

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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
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 hố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àmmin ở trên ta hồn tồ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++
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;
int main(){
int x; // khai báo biến nguyên x
int &ref=x; // tham chiếu ref là nickname của x
ref=10; // gán ref=10, nghĩa là x cũng bằng 10
cout << x << endl; // in giá trị của x, tức là 10, lên màn hình
return 0;

}
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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
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 (giống như thằng Bờm với con của bố thằng
Bờm là một thằng, giả thiết bố thằng Bờm chỉ đẻ được một thằng
). 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.
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;
// 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 tồ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:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
// theo con trỏ
swap(&x, &y);

// theo tham chiếu
swap(x, y);
Rõ ràng thằng dưới nhìn “thân thiện” hơn thằng trên (tự dưng để cái dấu & ở trước trơng
nó chướng mắt
). Hơn nữa tham chiếu đã gắn với biến nào rồi thì cố định ln, khơng
thay đổi được, cịn con trỏ khơng thích trỏ biến này nữa thì có thể trỏ sang biến khác, nên
nếu lỡ tay mà ta cho nó “trỏ lung tung” thì khơng biết đằng nào mà lần.
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 tồn bảo mật
của truyền theo tham trị nhưng lại tận dụng được lợi thế về chi phí bộ nhớ và
thời gian như truyền theo tham chiếu không? Câu trả lời đói là dùng tham chiếu
hằng. Chúng ta sẽ xem chương trình sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;
int min(const int& a, const int& b){
return (a}
int main(){
int x=5;
int y=7;
int minimum=min(x,y); // gọi hàm min tính giá trị nhỏ nhất rồi gán cho
minimum
cout << "minimum= " << minimum << endl;
return 0;
}
Chú ý vào header của hàm:
C++ Code:
Lựa chọn code | Ẩn/Hiện code

int min(const int& a, const int& b)
Việc đặt từ khóa const trước kiểu của tham số a và b như trên được gọi là truyền theo
tham chiếu hằng. Với từ khóa const này, ta vẫn truyền trực tiếp biến x, y vào cho
hàmmin nhưng hàm min khơng có quyền “sửa đổi” giá trị của x, y mà chỉ được dùng
những thao tác không làm ảnh hưởng đến x, y như so sánh, lấy giá trị của x, y để tính
tốn, … Nếu cố tình sửa đổi x, y sẽ gây lỗi. Xét một ví dụ như sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
int example(const int& a){
a=20; // lỗi vì cố tình sủa đổi tham chiếu hằng


return a;
}
Việc sử dụng tham chiếu hằng như trên là một ví dụ về nguyên tắc “quyền ưu tiên tối
thiểu” (the principle of least privilege), một nguyên tắc nền tảng trong lập trình. Trong
trường hợp này nghĩa là chỉ trao cho hàm min những quyền ưu tiên tối thiểu thao tác
trên dữ liệu để nó đủ thực hiện nhiệm vụ, không hơn. Rõ ràng hàm min chỉ cần so sánh
hai đối số truyền vào để xem thằng nào nhỏ hơn rồi trả về giá trị. Vì vậy truyền theo
tham chiếu hằng là phương án đảm bảo nguyên tắc trên.
Truyền cấu trúc dữ liệu (passing data structures)
Tạm thời mình chỉ giới thiệu cấu trúc đơn giản nhất là mảng (arrays). Còn những cấu
trúc dữ liệu phức tạp hơn, nếu có điều kiện mình sẽ nói trong dịp khác. Như ta biết tên
mảng là một con trỏ hằng, trỏ đến phần tử đầu tiên của mảng. Vì vậy truyền mảng
giống như truyền con trỏ vậy. Chương trình sau gọi hàm input để nhập các phần tử vào
một mảng, và output để xuất các phần tử của mảng:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;

void input(int*, int); // nguyên mẫu hàm input
void output(int*, int); // nguyên mẫu hàm output
int main(){
int num; // biến lưu số lượng phần tử mảng
int *ptr; // con trỏ quản lý mảng
cout << "Enter number of elements: " << endl;
cin >> num; // nhập số lượng phần tử mảng
ptr=new int[num]; // cấp phát bộ nhớ động cho con trỏ ptr
cout << "Enter elements: " << endl;
input(ptr, num); //nhập mảng
cout << "Here are elements of the array: " << endl;
output(ptr, num); // xuất mảng
}

return 0;

// định nghĩa hàm input
void input(int* a, int n){
for(int i=0; icout << "element "<< i+1 << "= ";
cin >> a[i];
}
}
// định nghĩa hàm output
void output(int* a, int n){
for(int i=0; icout << a[i] << " ";
}
}
nếu test thử kết quả sẽ như sau

Trích dẫn:

Enter number of elements:
4
Enter elements:
element 1= 1
element 2= 2


element 3= 0
element 4= 8
Here are elements of the array:
1208
Lưu ý, do mảng tương tự con trỏ nên truyền mảng bao giờ cũng là truyền theo tham
chiếu, không phải theo tham trị.
Hết bài 7a

BÀI 7b. FUNCTIONS (PART 2)
- from alpha to omega 4. Trả về giá trị của hàm (returning value from functions)
Khi hồn tất nhiệm vụ, hàm có thể trả về một giá trị nào đó cho tên hàm. Ví dụ
hàm square trả về giá trị là bình phương của đối số truyền vào. Kiểu trả về của hàm
quyết định kiểu của giá trị được trả về. Nó có thể là bất cứ kiểu built-in nào (như char,
in, long, double, … ) hoặc các kiểu người dùng định nghĩa
như Rectangle hay Student mà ta đã xây dựng ở những bài trước. Hàm cũng có thể
trả về giá trị là một con trỏ hoặc một tham chiếu. Phần này mình sẽ tập trung vào
những vấn đề cần lưu ý khi trả về một con trỏ hay hoặc một tham chiếu cho tên hàm.
Trả về một con trỏ (returning a pointer)
Khi nào ta dùng hàm để trả về con trỏ? Có rất nhiều trường hợp bạn trả lại con trỏ cho lời
gọi hàm. Nhắc lại, xâu ký tự là một con trỏ hằng. Bây giờ mình sẽ viết một chương trình
convert một xâu ký tự thành chữ hoa. Trong chương trình có hàm to_upper nhận vào

một xâu ký tự ASCII-8 bit, đổi hết các ký tự thành ký tự hoa, rồi trả về xâu viết hoa:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;
// hàm đổi sang chữ hoa
char* to_upper(char* str){
int length=strlen(str);
for(int i=0; iif(str[i]>=97 && str[i]<=122){ // nếu là chữ thường
str[i]-=32; // đổi thành chữ hoa
}
}
return str; // trả về xâu (là một con trỏ)
}
// hàm main
int main(){
char s1[]="Hey, baby ! you are crazy";
cout << s1 << endl;
char* s2;
s2=new char[strlen(s1)]; // cấp phát bộ nhớ động cho con trỏ s2
s2=strcpy(s2,to_upper(s1)); // copy kết quả đổi xâu s1 thành chữ hoa
cho xâu s2
cout << s2 << endl;


delete [] s2;
return 0;
}
Kết quả sẽ là:

Trích dẫn:

Hey, baby ! you are crazy
HEY, BABY ! YOU ARE CRARY
Đây là một ví dụ về việc trả về con trỏ cho hàm, tránh nhầm lẫn hàm trả về con trỏ
với con trỏ hàm (function pointer). Bởi vì con trỏ hàm là một vấn đề tương đối phức
tạp nên mình sẽ viết riêng một bài.
Trả về tham chiếu (returning a reference)
Hàm trả về tham chiếu tức là giá trị của hàm trả về là một tham chiếu đến một biến nào
đó. Hàm trả về tham chiếu có dạng:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
<kiểu_trả_về>& <tên_hàm>(danh_sách_tham_số){
// thân hàm
return var; // trả về tham chiếu đến biến var
}
Như đã phân tích trong bài trên (7a) mục so sánh ưu nhược điểm giữa truyền theo tham
chiếu và tham trị, thì dùng tham chiếu có lợi hơn về mặt hiệu suất (performance) vì nó
cho phép thao tác trực tiếp trên các biến. Nói chung, trong mọi trường hợp dùng tham
chiếu sẽ có lợi hơn tham trị, nếu cần đảm bảo an tồn cho dữ liệu thì dùng tham chiếu
hằng. Vì vậy, chỗ nào có thể dùng được tham chiếu thì nên dùng. Nhưng lưu ý rằng nếu
khơng cẩn thận sẽ rất dễ mắc lỗi, đó là hiện tượng “tham chiếu treo” (dangling
reference), nghĩa là tham chiếu tới một đối tượng "không tồn tại", và gây là một lỗi
logic. Chúng ta khơng thể dự đốn được hành vi của chương trình. Xét chương trình sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;
int& dangling_square(int n){ // hàm tính bình phương trả về một tham chiếu
treo

int sqr=n*n; // sqr là biến cụ bộ
return sqr; // trả về tham chiếu tới sqr
}
// hàm main
int main(){
int x=5;
int y=dangling_square(x); // tính bình phương của x rồi gán cho y
cout << y << endl;
return 0;
}
Mình đã test chương trình trên, kết quả nó vẫn chạy ngon lành, cho kết quả đúng. Thực
sự mình cũng khơng hiểu thế này là thế nào? Về nguyên tắc thì việc trả về tham chiếu tới
biến sqr như trong hàm dangling_square ở trên là “không ổn” , nhưng có thể là mấy cái
compiler bây giờ nó được tối ưu tinh vi nên nhận biết được "ý định" của chúng ta. Nó
issue một cái warning như sau (IDE mình dùng là Dev C++ 4.9.9.2)
Trích dẫn:

[Warning] reference to local variable `sqr' returned
Rõ ràng thằng compiler cũng nhận ra điều gì đó “khơng ổn”. Tại sao nó lại “khơng ổn”?
Để ý hàm dangling_square ta thấy biến sqr được khai báo trong hàm, do đó nó là một
biến cục bộ (local variable). Nó chỉ “sống” khi hàm dangling_square thực thi. Khi hàm


kết thúc nó cũng “die” theo hàm ln. Ở chương trình trên của ta, trước khi chết nó
kịp return một cái tham chiếu. Trong hàm main câu lệnh:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
int y=dangling_square(x);
sẽ gán giá trị của dangling_square(x) cho y, nhưng giá trị này là một tham chiếu tới
một thằng đã "chết", vì vậy ta hồn tồn khơng thể dự đốn được hành vi của chương

trình.
Khi nào ta có thể sử dụng tham chiếu mà khơng sợ bị dính tham chiếu treo? Câu
trả lời là tất cả những trường hợp mà biến trả về vẫn “sống” sau khi hàm kết thúc. Ví dụ
các biến trả vềcó phạm vi rộng hơn phạm vi của hàm (như các biến toàn cục), hàm
thành viên trả về tham chiếu tới các thành phần dữ liệu. Tuy nhiên chẳng ai làm điều này
cả bởi vì nó phá vỡ tính bảo mật của dữ liệu. Thơng qua tham chiếu này dữ liệu có thể bị
sửa đổi, điều này giống như “đục một cái lỗ qua bức tường private” vậy.
5. Đối số mặc định (default arguments)
Cái này mình đã nói qua trong bài nói về constructor, bây giờ mình sẽ nói rõ hơn. Khi khai
báo một hàm ta có thể chỉ định những giá trị mặc định cho các tham số. Nếu như khi gọi
hàm, những đối số tương ứng với những vị trí này bị khuyết (khơng được truyền) thì
những giá trị mặc định sẽ được thay thế vào đó. Do đó chúng được gọi là đối mặc định
(default arguments). Xét chương trình sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;
int default_arg(int , int =1, int =2); // nguyên mẫu hàm với hai đối mặc
định
int default_arg(int a, int b, int c){ // định nghĩa hàm
return a*b*c;
};
int main(){
// truyền đủ đối, các giá trị mặc dịnh không được dùng
cout << default_arg(2, 3, 4) << endl;
// khuyết đối ở vị trí thứ 3, giá trị mặc định c=2 được dùng
cout << default_arg(2, 3) << endl;
// khuyết đối ở vị trí thứ 2 và thư 3, giá trị mặc định b=1, c=2 được

dùng


cout << default_arg(2) << endl;
return 0;
}
Kết quả là
Trích dẫn:

24
12
4
Để tránh sự nhập nhằng, C++ yêu cầu các đối mặc định phải được đặt sang vị trí bên
phải nhất và thứ tự ưu tiên sử dụng giá trị mặc định sẽ từ phải sang trái. Ví dụ trong
chương trình trên, câu lệnh:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
cout << default_arg(2, 3) << endl;


chỉ truyền vào hai đối là 2 và 3, trong khi hàm u cầu ba đối, vì vậy vị trí thứ 3 là vị trí
khuyết, và được sử dụng mặc định c=2.
6. Quá tải hàm (function overloading)
C++ cho phép nhiều hàm trùng tên nhau trong cùng một phạm vi, miễn là danh sách
tham số của chúng khác nhau (khác về số lượng tham số hoặc nếu cùng số lượng
thì các tham số ở những vị trí tương ứng phải khác kiểu). Khả năng này được gọi
là “quá tải hàm” (function overloading). Giả sử một hàm có
tên overloaded_func được quá tải thành tầm chục cái hàm cùng tên thì khi bắt gặp lời
gọi hàm overloaded_func, compiler sẽ xem xét qua chục hàm này để tìm ra hàm phù
hợp nhất dựa vào việc so sánh các đối số truyền vào với danh sách tham số ở header
hàm (về số lượng cũng như kiểu ở các vị trí tương ứng). Ví dụ như chương trình sau:
C++ Code:

Lựa chọn code | Ẩn/Hiện code
#include <iostream>
using namespace std;
// khai báo ba hàm cùng tên
int min(int, int);
int min(int, int, int);
double min(double, double);
// hàm tính min 2 số nguyên
int min(int a, int b){
cout << "Call function 1: int min(int a, int b)" << endl;
int minimum=a;
if(breturn b;
}
return minimum;
}
// hàm tính min 3 số nguyên
int min(int a, int b, int c){
cout << "Call function 2: int min(int a, int b, int c)" << endl;
int minimum=a;
if(bminimum=b;
}
if(cminimum=c;
}
return minimum;
}
// hàm tính min hai số double
double min(double a, double b){

cout << "Call function 3: double min(double a, double b)" << endl;
float minimum=a;
if(breturn b;
}
return minimum;
}
// hàm main
int main(){
// gọi hàm thứ nhất
int x=min(2,5);
cout << x << endl << endl;


×