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

Lâ  
̣
2007  
BÀI GIẢNG MÔN  
Lập trình hướng đối tượng và C++  
Phần A: Giới thiệu  
Chương 1: Lập trình hướng đối tượng và ngôn ngữ C++  
1. Sự phát triển của các kỹ thuật lập trình  
Phần này trình bày về một số kỹ thuật hay phương pháp lập trình được phát triển để  
giải quyết các vấn đề trong Tin học kể từ khi máy tính ra đời. Sự phát triển của các kỹ  
thuật lập trình liên quan chặt chẽ tới sự phát triển phần cứng của máy vi tính cũng như  
việc ứng dụng máy tính vào giải quyết các vấn đề trong thực tế. Chúng ta có thể chia  
các phương pháp lập trình thành các kiểu sau:  
• Lập trình không có cấu trúc  
• Lập trình hướng thủ tục  
• Lập trình theo kiểu module hóa  
• Lập trình hướng đối tượng  
Chúng ta sẽ lần lượt xem xét các kỹ thuật lập trình này.  
1.1Lập trình không có cấu trúc (hay lập trình tuyến tính)  
Thông thường mọi người bắt đầu học lập trình bằng cách viết các chương trình nhỏ  
và đơn giản chỉ chứa một “chương trình chính”. Ở đây một chương trình chính có  
nghĩa là một tập các lệnh hoặc câu lệnh làm việc với các dữ liệu toàn cục trong cả  
chương trình (các biến dùng trong chương trình là các biến toàn cục). Chúng ta có thể  
minh hoạ bằng hình vẽ sau đây:  
Lập trình không có cấu trúc. Chương trình chính  
thao tác trực tiếp trên các dữ liệu toàn cục  
Một số nhược điểm của lập trình không có cấu trúc:  
• Lập trình không có cấu trúc không có khả năng kiểm soát tính thấy được của dữ  
liệu. Mọi dữ liệu trong chương trình đều là biến toàn cục do đó có thể bị thay đổi  
bởi bất kỳ phần nào đó của chương trình.  
• Việc không kiểm soát được tính thấy được của dữ liệu dẫn đến các khó khăn  
trong việc gỡ lỗi chương trình, đặc biệt là các chương trình lớn.  
• Kỹ thuật lập trình không có cấu trúc có rất nhiều bất lợi lớn khi chương trình đủ  
lớn. Ví dụ nếu chúng ta cần thực hiện lại một đoạn câu lệnh trên một tập dữ liệu  
khác thì buộc phải copy đoạn lệnh đó tới vị trí trong chương trình mà chúng ta  
muốn thực hiện. Điều này làm nảy sinh ý tưởng trích ra các đoạn lệnh thường  
xuyên cần thực hiện đó, đặt tên cho chúng và đưa ra một kỹ thuật cho phép gọi và  
trả về các giá trị từ các thủ tục này.  
1
Lâ  
̣
2007  
1.2Lập trình thủ tục hay lập trình có cấu trúc  
Với lập trình thủ tục hay hướng thủ tục chúng ta có thể nhóm các câu lệnh thường  
xuyên thực hiện trong chương trình chính lại một chỗ và đặt tên đoạn câu lệnh đó  
thành một thủ tục. Một lời gọi tới thủ tục sẽ được sử dụng để thực hiện đoạn câu lệnh  
đó. Sau khi thủ tục thực hiện xong điều khiển trong chương trình được trả về ngay sau  
vị trí lời gọi tới thủ tục trong chương trình chính. Với các cơ chế truyền tham số cho  
thủ tục chúng ta có các chương trình con. Một chương trình chính bao gồm nhiều  
chương trình con và các chương trình được viết mang tính cấu trúc cao hơn, đồng thời  
cũng ít lỗi hơn. Nếu một chương trình con là đúng đắn thì kết quả thực hiện trả về luôn  
đúng và chúng ta không cần phải quan tâm tới các chi tiết bên trong của thủ tục. Còn  
nếu có lỗi chúng ta có thể thu hẹp phạm vi gỡ lỗi trong các chương trình con chưa  
được chứng minh là đúng đắn, đây được xem như trừu tượng hàm và là nền tảng cho  
lập trình thủ tục.  
Một chương trình chính với lập trình thủ tục có thể được xem là tập hợp các lời gọi  
thủ tục.  
Lập trình thủ tục. Sau khi chương trình con thực  
hiện xong điều khiển được trả về ngay sau vị trí lời  
gọi tới chương trình con  
Chương trình chính có nhiệm vụ truyền các dữ liệu cho các lời gọi cụ thể, dữ liệu  
được xử lý cục bộ trong chương trình con sau đó các kết quả thực hiện này được trả về  
cho chương trình chính. Như vậy luồng dữ liệu có thể được minh họa như là một đồ  
thị phân cấp, một cây:  
Lập trình hướng thủ tục. Chương trình chính  
phối hợp các lời gọi tới các thủ tục với các dữ  
liệu thích hợp là các tham số  
Lập trình hướng thủ tục là một kỹ thuật lập trình có nhiều ưu điểm. Khái niệm  
chương trình con là một ý tưởng rất hay, nó cho phép một chương trình lớn có thể  
2
Lâ  
̣
2007  
được chia thành nhiều chương trình con nhỏ hơn, đo đó dễ viết hơn và ít lỗi hơn. Để  
có thể sử dụng được các thủ tục chung hoặc một nhóm các thủ tục trong các chương  
trình khác, người ta đã phát minh ra một kỹ thuật lập trình mới, đó là kỹ thuật lập trình  
theo kiểu module.  
1.3Lập trình module  
Trong lập trình module các thủ tục có cùng một chức năng chung sẽ được nhóm lại  
với nhau tạo thành một module riêng biệt. Một chương trình sẽ không chỉ bao gồm  
một phần đơn lẻ. Nó được chia thành một vài phần nhỏ hơn tương tác với nhau qua  
các lời gọi thủ tục và tạo thành toàn bộ chương trình.  
Lập trình module. Chương trình chính là sự kết hợp  
giữa các lời gọi tới các thủ tục trong các module riêng  
biệt với các dữ liệu thích hợp  
Mỗi module có dữ liệu riêng của nó. Điều này cho phép các module có thể kiểm  
soát các dữ liệu riêng của nó bằng các lời gọi tới các thủ tục trong module đó. Tuy  
nhiên mỗi module chỉ xuất hiện nhiều nhất một lần trong cả chương trình.  
Yếu điểm của lập trình thủ tục và lập trình module hóa:  
• Khi độ phức tạp của chương trình tăng lên sự phụ thuộc của nó vào các kiểu dữ  
liệu cơ bản mà nó xử lý cũng tăng theo. Vấn đề trở nên rõ ràng rằng cấu trúc dữ  
liệu sử dụng trong chương trình cũng quan trọng không kém các phép toán thực  
hiện trên chúng. Điều này càng lộ rõ khi kích thước chương trình tăng. Các kiểu dữ  
liệu được xử lý nhiều trong các thủ tục của một chương trình có cấu trúc. Do đó khi  
thay đổi cài đặt của một kiểu dữ liệu sẽ dẫn đến nhiều thay đổi trong các thủ tục sử  
dụng nó.  
• Một nhược điểm nữa là khi cần dùng nhiều nhóm làm việc để xây dựng một  
chương trình chung. Trong lập trình có cấu trúc mỗi người sẽ được giao xây dựng  
một số thủ tục và kiểu dữ liệu. Những lập trình viên xử lý các thủ tục khác nhau  
nhưng lại có liên quan tới các kiểu dữ liệu dùng chung nên nếu một người thay đổi  
kiểu dữ liệu thì sẽ làm ảnh hưởng tới công việc của nhiều người khác, đặc biệt là  
khi có sai sót trong việc liên lạc giữa các thành viên của nhóm.  
3
Lâ  
̣
2007  
• Việc phát triển các phầm mềm mất nhiều thời gian tập trung xây dựng lại các  
cấu trúc dữ liệu cơ bản. Khi xây dựng một chương trình mới trong lập trình có cấu  
trúc lập trình viên thường phải xây dựng lại các cấu trúc dữ liệu cơ bản cho phù  
hợp với bài toán và điều này đôi khi rất mất thời gian.  
1.4Lập trình hướng đối tượng  
Trong lập trình hướng đối tượng trong mỗi chương trình chúng ta có một số các đối  
tượng (object) có thể tương tác với nhau, thuộc các lớp (class) khác nhau, mỗi đối  
tượng tự quản lý lấy các dữ liệu của riêng chúng.  
Lập trình hướng đối tượng. Các đối tượng tương tác  
với nhau bằng cách gửi các thông điệp.  
Chương trình chính sẽ bao gồm một số đối tượng là thể hiện (instance) của các lớp,  
các đối tượng này tương tác với nhau thực hiện các chức năng của chương trình. Các  
lớp trong lập trình hướng đối tượng có thể xem như là một sự trừu tượng ở mức cao  
hơn của các cấu trúc (struct hay record) hay kiểu dữ liệu do người dùng định nghĩa  
trong các ngôn ngữ lập trình có cấu trúc với sự tích hợp cả các toán tử và dữ liệu trên  
các kiểu đó.  
Các ưu điểm của lập trình hướng đối tượng:  
• Lập trình hướng đối tượng ra đời đã giải quyết được nhiều nhược điểm tồn tại  
trong lập trình có cấu trúc. Trong lập trình OOP có ít lỗi hơn và việc gỡ lỗi cũng  
đơn giản hơn, đồng thời lập trình theo nhóm có thể thực hiện rất hiệu quả. Ít lỗi là  
một trong các ưu điểm chính của OOP vì theo thống kê thì việc bảo trì hệ thống  
phần mềm sau khi giao cho người dùng chiếm tới 70% giá thành phần mềm.  
• Việc thay đổi các cài đặt chi tiết bên dưới trong lập trình OOP không làm ảnh  
hương tới các phần khác của chương trình do đó việc mở rộng qui mô của một  
chương trình dễ dàng hơn, đồng thời làm giảm thời gian cần thiết để phát triển  
phần mềm.  
• Với khái niệm kế thừa các lập trình viên có thể xây dựng các chương trình từ  
các phần mềm sẵn có.  
4
Lâ  
̣
2007  
• OOP có tính khả chuyển cao. Một chương trình viết trên một hệ thống nền  
(chẳng hạn Windows) có thể chạy trên nhiều hệ thống nền khác nhau (chẳng hạn  
Linux, Unix…).  
• OOP có hiệu quả cao. Thực tế cho thấy các hệ thống được xây dựng bằng OOP  
có hiệu năng cao.  
2. Một số khái niệm cơ bản của lập trình hướng đối tượng  
1.1Kiểu dữ liệu trừu tượng ADT(Astract Data Type)  
Một số người định nghĩa OOP là lập trình với các kiểu dữ liệu trừu tượng và các  
mối quan hệ giữa chúng. Trong phần này chúng ta sẽ xem xét các kiểu dữ liệu trừu  
tượng như là một khái niệm cơ bản của OOP và sử dụng một số ví dụ để minh họa.  
Định nghĩa về kiểu dữ liệu trừu tượng:  
Một kiểu dữ liệu trừu tượng là một mô hình toán học của các đối tượng dữ liệu tạo  
thành một kiểu dữ liệu và các toán tử (phép toán) thao tác trên các đối tượng đó. Chú ý  
là trong định nghĩa này các toán tử thao tác trên các đối tượng dữ liệu gắn liền với các  
đối tượng tạo thành một kiểu dữ liệu trừu tượng. Đặc tả về một kiểu dữ liệu trừu tượng  
không có bất kỳ một chi tiết cụ thể nào về cài đặt bên trong của kiểu dữ liệu. Việc cài  
đặt một kiểu dữ liệu trừu tượng đòi hỏi một quá trình chuyển đổi từ đặc tả của nó sang  
một cài đặt cụ thể trên một ngôn ngữ lập trình cụ thể. Điều này cho phép chúng ta  
phân biệt các ADT với các thuật ngữ kiểu dữ liệu (data type) và cấu trúc dữ liệu (data  
structure). Thuật ngữ kiểu dữ liệu đề cập tới một cài đặt cụ thể (có thể là kiểu built in  
hoặc do người dùng định nghĩa) của một mô hình toán học được đặc tả bởi một ADT.  
Cấu trúc dữ liệu đề cập tới một tập các biến có cùng kiểu được gắn kết với nhau theo  
một cách thức xác định nào đó.  
Ví dụ về kiểu dữ liệu trừu tượng: Số nguyên.  
Kiểu dữ liệu trừu tượng số nguyên: ADT Integer:  
Dữ liệu: một tập các chữ số và một dấu tiền tố là + hoặc -. Chúng ta ký hiệu cả số  
là N.  
Các toán tử:  
constructor: khởi tạo một số nguyên  
sub(k): trả về hiệu N – k.  
add(k): trả về tổng N + k.  
……  
End  
1.2Đối tượng (Objects) và lớp (Classes)  
Trong một chương trình hướng đối tượng chúng ta có các đối tượng. Các đối tượng  
này là đại diện cho các đối tượng thực trong thực tế. Có thể coi khái niệm đối tượng  
trong OOP chính là các kiểu dữ liệu trong các ngôn ngữ lập trình có cấu trúc. Mỗi một  
đối tượng có các dữ liệu riêng của nó và được gọi là các member variable hoặc là các  
data member. Các toán tử thao tác trên các dữ liệu này được gọi là các member  
function.  
Mỗi một đối tượng là thể hiện (instance) của một lớp. Như vậy lớp là đại diện cho  
các đối tượng có các member function giống nhau và các data member cùng kiểu. Lớp  
là một sự trừu tượng hóa của khái niệm đối tượng. Tuy nhiên lớp không phải là một  
ADT, nó là một cài đặt của một đặc tả ADT. Các đối tượng của cùng một lớp có thể  
chia sẻ các dữ liệu dùng chung, dữ liệu kiểu này được gọi là class variable.  
5
Lâ  
̣
2007  
1.3Kế thừa (Inheritance)  
Khái niệm kế thừa này sinh từ nhu cầu sử dụng lại các thành phần phần mềm để  
phát triển các phần mềm mới hoặc mở rộng chức năng của phần mềm hiện tại. Kế thừa  
là một cơ chế cho phép các đối tượng của một lớp có thể truy cập tới các member  
variable và function của một lớp đã được xây dựng trước đó mà không cần xây dựng  
lại các thành phần đó. Điều này cho phép chúng ta có thể tạo ra các lớp mới là một mở  
rộng hoặc cá biệt hóa của một lớp sẵn có. Lớp mới (gọi là derived class) kế thừa từ lớp  
cũ (gọi là lớp cơ sở base class). Các ngôn ngữ lập trình hướng đối tượng có thể hỗ trợ  
khái niệm đa kế thừa cho phép một lớp có thể kế thừa từ nhiều lớp cơ sở.  
Lớp kế thừa derived class có thể có thêm các data member mới hoặc các member  
function mới. Thêm vào đó lớp kế thừa có thể tiến hành định nghĩa lại một hàm của  
lớp cơ sở và trong trường hợp này người ta nói rằng lớp kế thừa đã overload hàm  
thành viên của lớp cơ sở.  
1.4Dynamic Binding (tạm dịch là ràng buộc động) và Porlymorphism (đa xạ  
hoặc đa thể)  
Chúng ta lấy một ví dụ để minh hoạ cho hai khái niệm này. Giả sử chúng ta có một  
lớp cơ sở là Shape, hai lớp kế thừa từ lớp Shape là Circle và Rectange. Lớp Shape là  
một lớp trừu tượng có một member function trừu tượng là draw(). Hai lớp Circle và  
Rectange thực hiện overload lại hàm draw của lớp Shape với các chi tiết cài đặt khác  
nhau chẳng hạn với lớp Circle hàm draw sẽ vẽ một vòng tròn còn với lớp Rectange thì  
sẽ vẽ một hình chữ nhật. Và chúng ta có một đoạn chương trình chính hợp lệ như sau:  
int main()  
{
Shape shape_list[4];  
int choose;  
int i;  
for(i=0;i<4;i++)  
{
cout << “Ngay muon ve hinh tron(0) hay hinh chu nhat(1)”;  
cin >> choose;  
if(choose==0)  
{
shape_list[i] = new Circle();  
}else{  
shape_list[i] = new Rectange();  
}
}
for(i=0;i<4;i++)  
{
shape_list[i]->draw();  
}
}
Khi biên dịch chương trình này thành mã thực hiện (file .exe) trình biên dịch không  
thể xác định được trong mảng shape_list thì phần tử nào là Circle phần tử nào là  
Rectange và do đó không thể xác định được phiên bản nào của hàm draw sẽ được gọi  
6
Lâ  
̣
2007  
thực hiện. Việc gọi tới phiên bản nào của hàm draw để thực hiện sẽ được quyết định  
tại thời điểm thực hiện chương trình, sau khi đã biên dịch và điều này được gọi là  
dynamic binding hoặc late binding. Ngược lại nếu việc xác định phiên bản nào sẽ được  
gọi thực hiện tương ứng với dữ liệu gắn với nó được quyết định ngay trong khi biên  
dịch thì người ta gọi đó là static binding.  
Ví dụ này cũng cung cấp cho chúng ta một minh họa về khả năng đa thể  
(polymorphism). Khái niệm đa thể được dùng để chỉ khả năng của một thông điệp có  
thể được gửi tới cho các đối tượng của nhiều lớp khác nhau tại thời điểm thực hiện  
chương trình. Chúng ta thấy rõ lời gọi tới hàm draw sẽ được gửi tới cho các đối tượng  
của hai lớp Circle và Rectange tại thời điểm chương trình được thực hiện.  
Ngoài các khái niệm cơ bản trên OOP còn có thêm một số khái niệm khác chẳng  
hạn như name space và exception handling nhưng không phải là các khái niệm bản  
chất.  
3. Ngôn ngữ lập trình C++ và OOP.  
Giống như bất kỳ một ngôn ngữ nào của con người, một ngôn ngữ lập trình là  
phương tiện để diễn tả các khái niệm, ý tưởng. Việc phát triển các chương trình hay  
phần mềm là quá trình mô hình hóa các trạng thái tự nhiên của thế giới thực và xây  
dựng các chương trình dựa trên các mô hình đó.  
Các chương trình thực hiện chức năng mô tả phương pháp cài đặt của mô hình.  
Các thế hệ ngôn ngữ lập trình: Có thể phân chia các thế hệ ngôn ngữ lập trình  
thành 4 thế hệ:  
1: vào năm 1954 – 1958 (Fortran I) với đặc điểm là các biểu thức toán học  
2: vào năm 1959 – 1961 (Fortran II, Cobol) với các thủ tục  
3: vào những năm 1962 – 1970 (Pascal, Simula) với đặc trưng là các khối, các  
lớp…  
4: đang phát triển chưa có dẫn chứng thực tế.  
Các ngôn ngữ này ngày càng cách xa ngôn ngữ máy và các trình biên dịch của  
chúng ngày càng phải làm việc nhiều hơn.  
1.1Sự phát triển của các ngôn ngữ lập trình hướng đối tượng  
1967 Simula  
1970 to 1983 Smalltalk  
1979 Common LISP Object System  
1980 Stroustrup starts on C++  
1981 Byte Smalltalk issue  
1983 Objective C  
1986 C++  
1987 Actor, Eiffel  
1991 C++ release 3.0  
1995 Java  
1983 to 1989 Language books with OO concepts  
7
Lâ  
̣
2007  
1989 to 1992 Object-oriented design books  
1992 to present Object-oriented methodology books  
Các ngôn ngữ lập trình khác  
Java  
Self  
Python  
Perl  
Prograph  
Modula 3  
Oberon  
Smalltalk Venders  
ParcPlace, Digitalk, Quasar  
Prolog++  
Ada 9X  
Object Pascal (Delphi)  
Object X, X = fortran, cobal, etc.  
C#.  
Như vậy là có rất nhiều ngôn ngữ lập trình hướng đối tượng đã ra đời và chiếm ưu  
thế trong số chúng là C++ và Java. Mỗi ngôn ngữ đều có đặc điểm riêng của nó và  
thích hợp với các lĩnh vực khác nhau nhưng có lẽ C++ là ngôn ngữ cài đặt nhiều đặc  
điểm của OOP nhất.  
1.2Ngôn ngữ lập trình C++.  
C++ là một ngôn ngữ lập trình hướng đối tượng được Bjarne Stroustrup (AT & T  
Bell Lab) (giải thưởng ACM Grace Murray Hopper năm 1994) phát triển từ ngôn ngữ  
C.  
C++ kế thừa cú pháp và một số đặc điểm ưu việt của C: ví dụ như xử lý con trỏ,  
thư viện các hàm phong phú đa dạng, tính khả chuyển cao, chương trình chạy nhanh  
…. Tuy nhiên về bản chất thì C++ khác hoàn toàn so với C, điều này là do C++ là một  
ngôn ngữ lập trình hướng đối tượng.  
Phần B: Ngôn ngữ C++ và lập trình hướng đối tượng  
Chương 2: Những khái niệm mở đầu  
1. Chương trình đầu tiên  
1.1Quá trình biên dịch một chương trình C++  
Tất cả các ngôn ngữ trên máy tính đều được dịch từ một dạng nào đó mà con người  
có thể hiểu được một cách dễ dàng (các file mã nguồn được viết bằng một ngôn ngữ  
bậc cao) sang dạng có thể thực hiện được trên máy tính (các lệnh dưới dạng ngôn ngữ  
máy). Các chương trình thực hiện quá trình này chia thành hai dạng được gọi tên là các  
trình thông dịch (interpreter) và các trình biên dịch (compiler).  
Trình thông dịch: Một trình thông dịch sẽ dịch mã nguồn thành các hành động  
(activity), các hành động này có thể bao gồm một nhóm các lệnh máy và tiến hành  
thực hiện ngay lập tức các hành động này. Ví dụ như BASIC là một ngôn ngữ điển  
hình cho các ngôn ngữ thông dịch. BASIC cổ điển thông dịch từng dòng lệnh thực  
hiện và sau đó quên ngay lập tức dòng lệnh vừa thông dịch. Điều này làm cho quá  
trình thực hiện cả một chương trình chậm vì bộ thông dịch phải tiến hành dịch lại các  
đoạn mã trùng lặp. BASIC ngày nay đã thêm vào qúa trình biên dịch để cải thiện tốc  
8
Lâ  
̣
2007  
độ của chương trình. Các bộ thông dịch hiện đại chẳng hạn như Python, tiến hành dịch  
toàn bộ chương trình qua một ngôn ngữ trung gian sau đó thực hiện bằng một bộ thông  
dịch nhanh hơn rất nhiều.  
Các ngôn ngữ làm việc theo kiểu thông dịch thường có một số hạn chế nhất định  
khi xây dựng các dự án lớn (Có lẽ chỉ duy nhất Python là một ngoại lệ). Bộ thông dịch  
cần phải luôn được lưu trong bộ nhớ để thực hiện các mã chương trình, và thậm chí  
ngay cả bộ thông dịch có tốc độ nhanh nhất cũng không thể cải thiện được hoàn toàn  
các hạn chế tốc độ.Hầu hết các bộ thông dịch đều yêu cầu toàn bộ mã nguồn cần phải  
được thông dịch một lần duy nhất. Điều này không những dẫn đến các hạn chế về kích  
thước của chương trình mà còn tạo ra các lỗi rất khó gỡ rối nếu như ngôn ngữ không  
cung cấp các công cụ hiệu quả để xác định hiệu ứng của các đoạn mã khác nhau.  
Trình biên dịch: Một trình biên dịch dịch mã nguồn trực tiếp thành ngôn ngữ  
assembly hoặc các lệnh máy. Kết quả cuối cùng là một file duy nhất hoặc các file chứa  
các mã máy. Đây là một quá trình phức tạp và đòi hỏi một vài bước. Quá trình chuyển  
đổi từ mã chương trình ban đầu thành mã thực hiện là tương đối dài đối với một trình  
biên dịch.  
Tùy thuộc vào sự nhạy cảm của người viết trình biên dịch, các chương trình sinh ra  
bởi một trình biên dịch có xu hướng đòi hỏi ít bộ nhớ hơn khi thực hiện, và chúng  
chạy nhanh hơn rất nhiều. Mặc dù kích thước và tốc độ thường là các lý do hàng đầu  
cho việc sử dụng một trình biên dịch, trong rất nhiều trường hợp đây không phải là các  
lý do quan trọng nhất. Một vài ngôn ngữ (chẳng hạn như C) được thiết kế để các phần  
tách biệt của một chương trình có thể được biên dịch độc lập hoàn toàn với nhau. Các  
phần này sau đó thậm chí có thể kết hợp thành một chương trình thực hiện cuối cùng  
duy nhất bởi một công cụ có tên là trình liên kết. Quá trình này gọi là separate  
compilation (biên dịch độc lập).  
Biên dịch độc lập có rất nhiều điểm lợi. Một chương trình nếu dịch ngay lập tức  
toàn bộ sẽ vượt quá các giới hạn của trình biên dịch hay môi trường biên dịch có thể  
được biên dịch theo từng phần. Các chương trình có thể được xây dựng và kiểm thử  
từng phần một. Nếu mọt phần nào đó đã làm việc đúng đắn nó có thể được lưu lại như  
là một khối đã hoàn thành. Tập các phần đã làm việc và được kiểm thử có thể kết hợp  
lại với nhau tạo thành các thư viện để các lập trình viên khác có thể sử dụng. Các đặc  
điểm này hỗ trợ cho việc tạo ra các chương trình lớn.  
Các đặc điểm gỡ lỗi của trình biên dịch đã cải tiến một cách đáng kể qua thời gian.  
Các trình biên dịch đầu tiên chỉ sinh ra mã máy, và lập trình viên phải chèn các câu  
lệnh in vào để xem thực sự chương trình đang làm gì. Điều này không phải lúc nào  
cũng hiệu quả. Các trình biên dịch hiện đại có thể chèn các thông tin về mã nguồn vào  
mã thực hiện của chương trình. Thông tin này sẽ được sử dụng bởi các bộ gỡ lỗi cấp  
độ nguồn đầy năng lực để chỉ ra chính xác điều gì đang diễn ra trong một chương trình  
bằng cách theo dấu (tracing) quá trình thực hiện của nó qua toàn bộ mã nguồn.  
Một vài trình biên dịch giải quyết vấn đề tốc độ biên dịch bằng cách thực hiện quá  
trình biên dịch trong bộ nhớ (in-memory compilation). Các trình biên dịch theo kiểu  
này lưu trình biên dịch trong bộ nhớ RAM. Đối với các chương trình nhỏ, quá trình  
này có thể xem như là một trình thông dịch.  
Quá trình biên dịch  
Để lập trình bằng C và C++ chúng ta cần phải hiểu các bước và các công cụ trong  
quá trình biên dịch. Một vài ngôn ngữ (đặc biệt là C và C++) bắt đầu thực hiện quá  
9
Lâ  
̣
2007  
trình biên dịch bằng cách chạy một bộ tiền xử lý đối với mã nguồn. Bộ tiền xử lý là  
một chương trình đơn giản thay thế các mẫu trong mã nguồn bằng các mẫu khác mà  
các lập trình viên đã định nghĩa (sử dụng các chỉ thị tiền xử lý: preprocessor  
directives). Các chỉ thị tiền xử lý được sử dụng để tiết kiệm việc gõ các đoạn chương  
trình thường xuyên sử dụng và tăng khả năng dễ đọc cho mã nguồn. Tuy nhiên các chỉ  
thị tiền xử lý này đôi khi cũng gây ra những lỗi rất tinh vi và khó phát hiện. Mã sinh ra  
bởi bộ tiền xử lý này thường được ghi lên một file tạm.  
Các trình biên dịch thường thực hiện công việc của nó theo hai pha. Đầu tiên là  
phân tích mã tiền xử lý. Bộ biên dịch chia mã tiền xử lý thành các đơn vị nhỏ và tổ  
chức chúng thành một cấu trúc gọi là cây. Ví dụ như trong biểu thức: “A+B” các phần  
tử “A”, “+”, “B” sẽ được lưu trên nút của cây phân tích.  
Một bộ tới ưu hóa toàn cục (global optimizer) đôi khi cũng được sử dụng để tạo ra  
mã chương trình nhỏ hơn, nhanh hơn.  
Trong pha thứ hai, bộ sinh mã duyệt qua cây phân tích và sinh ra hoặc là mã  
assemble hoặc mã máy cho các nút của cây. Nếu như bộ sinh mã tạo ra mã assembly,  
thì sau đó chương trình dịch mã assembler sẽ thực hiện công việc tiếp theo. Kết quả  
của hai trường hợp trên đều là một module object (một file thường có đuôi là .o hoặc  
.obj). Sau đó một bộ tối ưu hoá nhỏ (peep-hole) sẽ được sử dụng để loại bỏ các đoạn  
chứa các câu lệnh assembly thừa.  
Việc sử dụng từ “object” để mô tả các đoạn mã máy là một thực tế không đúng  
lắm. Từ này đã được dùng trước cả khi lập trình hướng đối tượng ra đời. Từ “object”  
được sử dụng có ý nghĩa như là từ “goal” khi nói về việc biên dịch, trong khi đó trong  
lập trình hướng đối tượng nó lại có nghĩa là “a thing with boundaries”.  
Trình liên kết kết hợp một danh sách các module object thành một chương trình  
thực hiện có thể nạp vào bộ nhớ và thực hiện bởi hệ điều hành. Khi một hàm trong một  
module object tạo ra một tham chiếu tới một hàm hoặc một biến trong một module  
object khác, trình liên kết sẽ sắp xếp lại các tham chiếu này; điều này đảm bảo rằng tất  
cả các hàm và dữ liệu external được sử dụng trong quá trình biên dịch là đều tồn tại.  
Trình liên kết cũng thêm vào các module object đặc biệt để thực hiện các hành động  
khởi động.  
Trình liên kết có thể tìm kiếm trên các file đặc biệt gọi là các thư viện để sắp xếp  
lại tất cả các tham chiếu tới chúng. Mỗi thư viện chứa một tập các module object trong  
một file. Một thư viện được tạo ra và bảo trì bởi một lập trình viên có tên là librarian.  
Kiểm tra kiểu tĩnh  
Trình biên dịch thực hiện kiểm tra kiểu trong pha đầu tiên của quá trình biên dịch.  
Quá trình kiểm tra này thực hiện kiểm thử việc sử dụng các tham số của các hàm và  
ngăn chặn rất nhiều lỗi lập trình khác nhau. Vì quá trình kiểm tra kiểu được thực hiện  
trong qúa trình biên dịch chứ không phải trong quá trình chương trình thực hiện nên nó  
được gọi là kiểm tra kiểu tĩnh.  
Một vài ngôn ngữ lập trình hướng đối tượng (Java chẳng hạn) thực hiện kiểm tra  
kiểu tại thời điểm chương trình chạy (dynamic type checking). Nếu kết hợp cả việc  
kiểm tra kiểu tĩnh và động thì sẽ hiệu quả hơn nhưng kiểm tra kiểu động cũng làm cho  
chương trình thực hiện bị ảnh hưởng đôi chút.  
C++ sử dụng kiểm tra kiểu tĩnh. Kiểm tra kiểu tĩnh báo cho lập trình viên về các lỗi  
về sử dụng sai kiểu dữ liệu trong quá trình biên dịch, và do đó tối ưu hóa tốc độ thực  
hiện chương trình. Khi học C++ chúng ta sẽ thấy hầu hết các quyết định thiết kế của  
10  
Lâ  
̣
2007  
ngôn ngữ đều tập trung vào củng cố các đặc điểm: tốc độ nhanh, hướng đối tượng, các  
đặc điểm mà đã làm cho ngôn ngữ C trở nên nổi tiếng.  
Chúng ta có thể không dùng tùy chọn kiểm tra kiểu tĩnh của C++ hoặc cũng có thể  
thực hiện việc kiểm tra kiểu động - chỉ cần viết thêm mã.  
Các công cụ cho việc biên dịch độc lập  
Việc biên dịch độc lập rất cần thiết nhất là đối với các dự án lớn. Trong ngôn ngữ  
C và C++, một lập trình viên có thể tạo ra các đoạn chương trình nhỏ dễ quản lý và  
được kiểm thử độc lập. Công cụ cơ bản để chia một chương trình thành các phần nhỏ  
là khả năng tạo ra các thay thế được đặt tên hay là các chương trình con. Trong C và  
C++ một chương trình con được gọi là một hàm, và các hàm là các đoạn mã có thể  
được thay thế trong các file khác nhau, cho phép thực hiện quá trình biên dịch độc lập.  
Nói một cách khác các hàm là các đơn vị nguyên tử của mã nguồn, vì chúng ta không  
thể đặt các phần khác nhau của hàm trong các file khác nhau nên nội dung của một  
hàm cần phải được đặt hoàn toàn trong một file (mặc dù các file có thể chứa nhiều hơn  
1 hàm).  
Khi chúng ta gọi đến một hàm, chúng ta thường truyền cho nó một vài tham số, đó  
là các giá trị mà chúng ta muốn hàm làm việc với khi nó thực hiện. Khi hàm thực hiện  
xong chúng ta thường nhận được một giá trị trả về, một gía trị mà hàm trả lại như là  
một kết quả. Cũng có thể viết các hàm không nhận các tham số và không trả về bất kỳ  
giá trị nào.  
Để tạo ra một chương trình với nhiều file, các hàm trong một file phải truy cập tới  
các hàm và dữ liệu trong các file khác. Khi biên dịch một file, trình biên dịch C hoặc  
C++ phải biết về các hàm và dữ liệu trong các file khác đặc biệt là tên và cách dùng  
chúng. Trình biên dịch đảm bảo các hàm và dữ liệu được sử dụng đúng đắn. Qúa trình  
báo cho trình biên dịch tên và nguyên mẫu của các hàm và dữ liệu bên ngoài được gọi  
là khai báo (declaration). Khi chúng ta đã khai báo một hàm hoặc biến trình biên dịch  
sẽ biết cách thức kiểm tra để đảm bảo các hàm và dữ liệu này được sử dụng đúng đắn.  
Including các file Header  
Hầu hết các thư viện đều chứa một số lượng đáng kể các hàm và biến. Để tiết kiệm  
công sức và đảm bảo sự nhất quán khi khai báo ngoài các phần tử này, C và C++ đã sử  
dụng một loại file được gọi là file header. Mỗi file header là một file chứa các khai báo  
ngoài cho 1 thư viện; theo qui ước các file này có phần mở rộng là .h, nhưng chúng ta  
cũng có thể dùng các đuôi file khác cho chúng chẳng hạn như .hpp hoặc .hxx.  
Lập trình viên tạo ra các file thư viện sẽ cung cấp các header file. Để khai báo các  
hàm và các biến bên ngoài thư viện người dùng đơn giản chỉ cần thực hiện include file  
header đó. Để include một file header chúng ta sử dụng chỉ thị tiền xử lý #include. Chỉ  
thị này sẽ báo cho bộ xử lý mở file header có tên tương ứng và chèn nội dung của file  
đó vào chỗ mà chỉ thị #include được sử dụng. Tên file sử dụng sau chỉ thị #include có  
thể nằm giữa hai dấu < và > hoặc giữa hai dấu “.  
Ví dụ: #include <header.h>  
Nếu chúng ta sử dụng chỉ thị include theo cách trên thì bộ tiền xử lý sẽ tìm file  
header theo cách đặc thù đối với cài đặt của chúng ta, nhưng thường thì sẽ có một vài  
đường dẫn mà chúng ta chỉ định cụ thể trong biến môi trường của trình biên dịch hoặc  
trên dòng lệnh để sử dụng cho việc tìm các file header. Cơ chế thiết lập các đường dẫn  
này phụ thuộc vào trình biên dịch và môi trường mà chúng ta làm việc.  
11  
Lâ  
̣
2007  
Ví dụ: #include “header.h”  
Chỉ thị tiền xử lý như trên thường có ý nghĩa là báo cho bộ tiền xử lý tìm file tương  
ứng trong thư mục hiện tại trước nếu không thấy thì sẽ tìm giống như trong trường hợp  
tên file include được đặt giữa hai dấu < và >.  
Nói chung thì đối với các file include chuẩn hoặc được sử dụng nhiều chúng ta nên  
đặc nó trong thư mục mặc định là include dưới thư mục cài đặt trình biên dịch và dùng  
chỉ thị theo kiểu <>, còn đối với các file đặc thù với ứng dụng cụ thể thì dùng kiểu tên  
file đặt giữa hai dấu “”.  
Trong quá trình phát triển của C++ các nhà cung cấp các trình biên dịch có các qui  
ước đặt tên khác nhau và các hệ điều hành lại có các hạn chế tên khác nhau đặc biệt là  
độ dài của tên file. Các vấn đề này gây ra các vấn đề về tính khả chuyển của chương  
trình. Để khắc phục vấn đề này người ta đã sử dụng một định dạng chuẩn cho phép các  
tên file header có thể dài hơn 8 ký tự và bỏ đi phần tên mở rộng.  
Để phân biệt một chương trình C và C++ đôi khi người ta còn dùng cách thêm một  
ký tự “c” vào trước tên của các file header, chi tiết này cũng được chấp nhận đối với C  
và C++.  
Quá trình liên kết  
Trình liên kết tập hợp các module object (thường là các file có phần mở rộng là .o  
hoặc .obj), được sinh ra bởi trình biên dịch, thành một chương trình có thể thực hiện  
được và hệ điều hành có thể nạp vào bộ nhớ và chạy. Đây là pha cuối cùng trong quá  
trình biên dịch.  
Các đặc điểm của các trình liên kết thay đổi phụ thuộc vào các hệ thống khác nhau.  
Nói chung chúng ta chỉ cần chỉ rõ cho trình liên kết biết tên của các module object và  
các thư viện mà chúng ta muốn liên kết, và tên của chương trình khả chạy cuối cùng.  
Một vài hệ thống đòi hỏi chúng ta cần phải tự gọi tới các trình liên kết. Tuy nhiên hầu  
hết các trình biên dịch hoàn chỉnh đều thực hiện hộ chúng ta công việc này.  
Sử dụng các thư viện  
Giờ đây chúng ta đã biết các thuật ngữ cơ bản, chúng ta có thể hiểu cách thức sử  
dụng một thư viện. Để sử dụng một thư viện cần phải:  
• Include file header của thư viện  
• Sử dụng các hàm và các biến trong thư viện  
• Liên kết thư viện vào chương trình khả chạy cuối cùng  
Các bước này cũng đúng với các module object không có trong các thư viện.  
Including một file header và liên kết các module object là các bước cơ bản để thực  
hiện việc biên dịch độc lập trong C và C++.  
Trình liên kết làm thế nào để tìm một file thư viện  
Khi chúng ta tạo ra một tham chiếu ngoài tới một hàm số hoặc một biến số trong C  
hoặc C++, trình liên kết, khi bắt gặp tham chiếu này, có thể thực hiện một trong hai  
việc sau: nếu nó chưa thấy phần định nghĩa của hàm hay biến này, nó sẽ thêm định  
danh vào danh sách các tham chiếu chưa được định nghĩa của nó. Nếu như trình liên  
kết đã bắt gặp định nghĩa của tham chiếu đó, tham chiếu sẽ được sắp xếp lại.  
Nếu như trình liên kết không tìm thấy định nghĩa của tham chiếu trong danh sách  
các module object nó sẽ tiến hành tìm kiếm trong các thư viện. Các thư viện có một  
vài loại chỉ số nên trình liên kết không cần thiết phải tìm kiếm hết trong các module  
12  
Lâ  
̣
2007  
objetc của thư viện – nó chỉ cần xem xét các phần chỉ mục. Khi trình liên kết tìm thấy  
một định nghĩa trong một thư viện, toàn bộ module object chứ không chỉ phần định  
nghĩa của hàm, sẽ được liên kết vào chương trình thực hiện. Chú ý rằng toàn bộ thư  
viện sẽ không được liên kết, chỉ có phần định nghĩa mà chương trình tham chiếu tới.  
Như vậy nếu chúng ta muốn tối ưu về kích thước của chương trình chúng ta có thể cho  
mỗi hàm vào một file khi xây dựng các thư viện riêng của mình. Điều này đòi hỏi công  
sức edit nhiều hơn nhưng cũng có thể có ích.  
Vì trình liên kết tìm kiếm các file theo thứ tự chúng ta có thể che đi sự tồn tại của  
một hàm thư viện bằng cách dùng hàm của chúng ta với phần định nghĩa và prototype  
y hệt như hàm thư viện. Tuy nhiên điều này cũng có thế gây ra các lỗi mà chúng ta  
không thể kiểm soát được.  
Khi một chương trình khả chạy được viết bằng C hoặc C++ được tạo ra, một số các  
thành phần nhất định sẽ được liên kết với nó một cách bí mật. Một trong các thành  
phần này chính là module khởi động (startup), module này chứa các thủ tục khởi tạo  
cần phải được thực hiện bất cứ khi nào một chương trình C hay C++ bắt đầu chạy. Các  
thủ tục này thiết lập stack và các biến khởi tạo nhất định trong chương trình.  
Trình biên dịch luôn thực hiện việc tìm kiếm trong các thư viện chuẩn để thực hiện  
liên kết các hàm chuẩn mà chúng ta dùng trong chương trình nên để dùng các hàm  
trong các thư viện chuẩn chúng ta đơn giản chỉ cần include file header của thư viện đó.  
Còn đối với các thư viện riêng do chúng ta tạo ra chúng ta cần chỉ rõ tên thư viện cho  
trình liên kết (chẳng hạn thư viện graphics không phải là một thư viện chuẩn).  
1.2Chương trình đầu tiên.  
Cách tốt nhất để học lập trình là xem các chương trình của người khác viết và  
học tập các kỹ thuật lập trình của họ. Sau đây là chương trình HelloWorld được  
viết bằng C++, một chương trình mà hầu hết các sách lập trình đều lấy làm ví dụ  
mở đầu.  
// Chương trình HelloWorld  
// File hello.cpp  
// In ra màn hình xâu “Hello, World!”  
#include <iostream.h> // Khai báo luồng cout để sử  
dụng  
int main() {  
cout << "Hello, World! I am "  
<< 20 << “ today.” endl;  
return 0;  
}
Điều đầu tiên chúng ta cần biết là một chương trình C hoặc C++ là một tập các  
hàm, biến và các lời gọi hàm. Khi chương trình thực hiện nó sẽ gọi đến một hàm đặc  
biệt mà bất cứ chương trình nào cũng có đó là hàm main.  
Về mặt thuật toán và nội dung chương trình này không có gì đặc biệt, nó in ra màn  
hình một dòng chào mừng: “Hello, World!”. Chúng ta sẽ lần lượt khám phá các đặc  
điểm của C++ qua các câu lệnh của chương trình đơn giản này.  
Hai dòng đầu tiên của chương trình là hai dòng chú thích, giới thiệu về chức năng  
của chương trình. C++ chấp nhận kiểu viết chú thích theo kiểu của C:  
13  
Lâ  
̣
2007  
/*  
chú thích có thể gồm nhiều dòng  
*/  
Nhưng đưa ra một kiểu chú thích khác tiện lợi hơn là:  
// chú thích, chú ý là chú thích này chỉ nằm trên một dòng  
Một số lập trình viên thích dùng kiểu chú thích của C++ hơn vì như thế dễ dàng  
phân biệt một chương trình C với một chương trình C++. Mặc dù đây không phải là  
một qui tắc bắt buộc song chúng ta nên dùng kiểu thứ hai và nếu có dùng kiểu thứ nhất  
thì cần phải theo một qui luật nhất định.  
Tiếp theo là một chỉ thị tiền xử lý #include. Ở đây chúng ta include file header  
iostream chứa các dòng vào ra chuẩn của C++. Thường khi chúng ta include một file  
header chúng ta nên có kèm một vài chú thích ngắn gọn về mục đích của file đó, chẳng  
hạn ở đây chúng ta include file header iostream là vì cần sử dụng đối tượng cout trong  
thư viện iostream.  
Tiếp theo là hàm main() có kiểu trả về là int và không nhận tham số nào. Giống  
như C tất cả các chương trình C++ đều có một và duy nhất một hàm main() và nếu  
chúng ta không nói gì có nghĩa là hàm main sẽ trả về một giá trị có kiểu int nên để  
tránh một vài rắc rối chúng ta nên xác định kiểu của hàm main là int và trả về 0 trước  
khi kết thúc hàm. Prototype của hàm main là: int main() có nghĩa là hàm này có thể  
nhận bất bao nhiêu tham số tuỳ ý.  
Trong câu lệnh tiếp theo chúng ta sử dụng đối tượng cout (console output) để in ra  
một loạt các tham số thông qua các toán tử “<<”. Chúng ta đã biết trong ngôn ngữ C  
toán tử “<<” là toán tử dịch bit trái nhưng trong C++ ngoài ý nghĩa là một toán tử dịch  
bit trái nó còn là một toán tử của đối tượng cout, đó chính là một minh họa cho khả  
năng overload các toán tử của C++ mà chúng ta sẽ học sau này. Cũng cần chú ý là câu  
lệnh này được viết trên nhiều dòng, C++ cho phép một câu lệnh có thể viết trên nhiều  
dòng. Trình biên dịch nhận biết sự kết thúc một câu lệnh trong C++ bằng cách nhận  
biết sự có mặt của các dấu “;”.  
endl là một hàm đặc biệt thuộc thư viện các luồng vào ra chuẩn nó kết thúc dòng  
hiện tại của cout là nhảy xuống dòng tiếp theo.  
Đối tượng cout có khả năng xử lý nhiều tham số tương ứng với các toán tử “<<”.  
Nó xem các tham số đó như là một dãy các ký tự, nếu là các kiểu dữ liệu khác (ngoài  
kiểu xâu: các ký tự giữa hai dấu “ và “) cout sẽ có hai cách thức xử lý. Thứ nhất nếu đó  
là các kiểu cơ bản chúng sẽ được chuyển thành một dãy các ký tự giữa hai dấu “, còn  
nếu là một kiểu tự định nghĩa (lớp hoặc struct) thì có thể sẽ gọi tới hàm overload toán  
tử của kiểu đó “<<”.  
Dòng lệnh cuối cùng là câu lệnh return 0 để phù hợp với prototype của hàm main  
được khai báo ban đầu.  
Để tiến hành biên dịch chương trình trên chúng ta thực hiện lệnh:  
Tcc –eHello hello.cpp. Kết quả nhận được là một file khả chạy có tên là hello.exe.  
2. Biến, hằng và tầm hoạt động của các biến  
2.1Cú pháp khai báo biến (variable declaration)  
Ý nghĩa của cụm từ “variable declaration” đã từng có những ý nghĩa trái ngược  
nhau và gây nhầm lẫn trong lịch sử, và việc hiểu đúng định nghĩa của cụm từ này là rất  
quan trọng trong việc hiểu đúng đắn mã chương trình. Một khai báo biến sẽ báo cho  
trình thông dịch biết các đặc điểm của một biến được khai báo. Mặc dù có thể đó là lần  
14  
Lâ  
̣
2007  
đầu tiên trình biên dịch bắt gặp biến đó trong quá trình biên dịch nhưng một khai báo  
biến đúng đắn sẽ đảm bảo rằng biến đó là tồn tại (đâu đó trong bộ nhớ) và nó là một  
biến có kiểu X.  
Cú pháp khai báo biến hợp lệ trong C++ là:  
<kiểu biến> tên biến;  
Trong đó “kiểu biến” là một kiểu dữ liệu hợp lệ và tên biến là một tên hợp lệ theo  
như định nghĩa trong C.  
Ví dụ: int a;  
Khi gặp một khai báo như trên trong quá trình biên dịch, trình biên dịch sẽ ngay lập  
tức tạo ra một vùng nhớ (có thể có thêm gía trị khởi tạo) của biến kiểu số nguyên và  
gán nhãn là a (xác định hay định nghĩa biến). Tuy nhiên đôi khi chúng ta chỉ muốn đơn  
giản khai báo một biến là tồn tại (ở đâu đó trong toàn bộ chương trình chứ không  
muốn ngay lập tức định nghĩa biến đó). Để giải quyết trường hợp này chúng ta sẽ dùng  
từ khóa extern, ví dụ:  
extern int a;  
Khai báo này sẽ báo cho trình biên dịch biết rằng biến có tên là a là tồn tại và nó đã  
hoặc sẽ được định nghĩa đâu đó trong chương trình.  
Ví dụ:  
// file: Declare.cpp  
// Ví dụ khai báo và định nghĩa biến  
extern int i; // khai báo và không định nghĩa  
float b; // khai báo và định nghĩa  
int i; // định nghĩa biến i  
int main() {  
b = 1.0;  
i = 2;  
}
Các biến có thể được khai báo ở bất kỳ một vị trí nào trong chương trình, điều này  
có đôi chút khác biệt so với các chương trình C.  
2.2Tầm hoạt động của các biến  
Khái niệm tầm hoạt động của các biến cho chúng ta biết khu vực (phần chương  
trình) mà một biến nào đó có thể được sử dụng hợp lệ và khu vực nào thì việc truy cập  
tới một biến là không hợp lệ. Tầm hoạt động của một biến bắt đầu từ vị trí mà nó được  
khai báo cho tới dấu “}” đầu tiên khớp với dấu “{“ ngay trước khai báo của biến đó.  
Có nghĩa là tầm hoạt động của một biến được xác định là trong cặp “{“ và “}” gần  
nhất bao nó. Tất nhiên tầm hoạt động của các biến có thể chồng lên nhau.  
2.3Khai báo biến ngay trong cú pháp của các câu lệnh điều khiển  
Như chúng ta đã biết trong các chương trình C++ việc khai báo biến là khá tự do.  
Các biến có thể được khai báo ở bất kỳ vị trí hợp lệ nào của chương trình miễn là  
chúng phải là xác định trước khi được sử dụng.  
Trong ngôn ngữ C và hầu hết các ngôn ngữ thủ tục khác lập trình viên bắt buộc  
phải khai báo các biến tại phần đầu tiên của mỗi thủ tục. Do đó khi đọc các file mã  
nguồn C chúng ta luôn thấy một loạt khai báo các biến sẽ được dùng mỗi thủ tục ở  
15  
Lâ  
̣
2007  
phần đầu của thủ tục. Điều này sẽ rất bất tiện khi một thủ tục có nhiều biến hoặc dài vì  
việc kiểm soát biến (tên, giá trị khởi tạo, tầm hoạt) sẽ trở nên khó khăn.  
Đi xa hơn cả việc cho phép khai báo bất kỳ vị trí nào hợp lệ trong chương trình  
C++ còn cho phép khai báo và khởi tạo các biến ngay bên trong biểu thức điều khiển  
của các vòng lặp for, while, do hoặc trong câu lệnh if, switch. Ví dụ:  
for(int i=0;i<10;i++){  
…..  
}
while(char c = cin.get() != ’q’){  
….  
}
if(char x = c == ‘a’ || c == ’b’){  
….  
}
switch(int i=cin.get()){  
case ‘A’: …; break;  
…..  
}
Mặc dù vậy việc khai báo như trên chỉ thường được dùng với các vòng lặp for vì  
đôi khi nó gây ra một số lỗi. Ví dụ câu lệnh:  
while( (char c = cin.get()) !=’q’ ){  
}
sẽ làm chúng ta ngạc nhiên với kết quả nhận được. Vì toán tử != có độ ưu tiên cao  
hơn toán tử gán = nên c sẽ nhận một giá trị có kiểu Bool và sau đó mới được convert  
sang kiểu char.  
2.4Các kiểu biến  
Biến toàn cục (global variable)  
Các biến toàn cục được định nghĩa bên ngoài tất cả các hàm và có thể được sử  
dụng trong tất cả các phần của chương trình (thậm chí ngay cả phần chương trình nằm  
trong một file mã nguồn khác). Các biến toàn cục không bị ảnh hưởng bởi các tầm  
hoạt động (chúng tồn tại cho tới khi chương trình kết thúc).  
Khi cần tham chiếu tới các biến toàn cục trong một file mà nó chưa được khai báo  
(biến này được khai báo trong một file khác) chúng ta sử dụng từ khóa extern để chỉ ra  
rằng biến đó là một biến toàn cục được khai báo trong file khác.  
Biến cục bộ (hay địa phương, local)  
Các biến địa phương thường được khai báo trong một phạm vi hay tầm hoạt động  
nhất định, thường là trong một hàm. Các biến địa phương này còn được gọi là các biến  
tự động vì chúng ta có thể sử dụng chúng một cách tự nhiên trong tầm hoạt động của  
chúng và bản thân chúng cũng tự động “out of scope” bên ngoài phạm vi hoạt động.  
Chúng ta có thể sử dụng từ khóa auto để làm rõ hơn điều này.  
Biến thanh ghi (register variable)  
Các biến thanh ghi là một loại biến cục bộ. Để khai báo các biến thanh nghi chúng  
ta dùng từ khóa register. Mục đích của việc khai báo các biến register là báo cho trình  
biên dịch biết để nó có thể làm cho việc truy cập vào các biến này với tốc độ càng  
16  
Lâ  
̣
2007  
nhanh càng tốt. Việc tăng tốc độ truy cập biến là phụ thuộc vào cài đặt tuy nhiên như  
ngụ ý của từ register điều này thường được thực hiện bằng cách đặt biến vào một  
thanh ghi. Không có gì đảm bảo là biến được khai báo là register sẽ được đặt trong  
một thanh ghi hoặc thậm chí tốc độ truy cập sẽ nhanh hơn. Đó chỉ là một gợi ý cho  
trình biên dịch.  
Không thể thực hiện các biến thanh ghi kiểu này, chúng cũng chỉ có thể là các biến  
địa phương, không thể là các biến toàn cục hoặc các biến tĩnh và nói chung chúng ta  
nên tránh dùng chúng.  
Biến tĩnh (static variable)  
Các biến tĩnh được khai báo bằng từ khóa static. Bình thường đối với một biến  
được khai báo cục bộ trong một hàm số, nó sẽ tự động bị loại bỏ khỏi bộ nhớ khi hàm  
được gọi thực hiện xong. Khi hàm được gọi thực hiện lại lần nữa, các biến cục bộ lại  
được khởi tạo lại và cứ thế. Tuy nhiên đôi khi chúng ta muốn lưu lại các giá trị của  
một biến số đã có được trong các lần gọi thực hiện trước của hàm, khi đó việc dùng  
biến static là hợp lý. Các biến static chỉ được khởi tạo lần đầu tiên khi hàm được gọi  
tới lần đầu tiên. Chúng ta có thể băn khoăn tự hỏi là vậy tại sao không dùng các biến  
toàn cục câu trả lời là các biến static có tầm hoạt động trong một thân hàm do đó  
chúng ta có thể thu hẹp các lỗi liên quan tới việc sử dụng biến này, có nghĩa khả năng  
lỗi là thấp hơn so với dùng biến toàn cục.  
Ngoài ý nghĩa trên từ khóa static thường có một ý nghĩa khác đó là “không thể sử  
dụng ngoài một phạm vi nhất định”. Khi từ khóa static được dùng để khai báo một tên  
hàm hoặc một biến nằm ngoài tất cả các hàm trong một file mã nguồn thì có nghĩa là  
biến đó chỉ có tầm hoạt động trong file đó mà thôi. Khi đó chúng ta nói là biến đó có  
tầm hoạt động file.  
2.5Liên kết biến khi biên dịch  
Để hiểu cách thức hoạt động của các chương trình C và C++ chúng ta cần phải hiểu  
quá trình liên kết diễn ra như thế nào. Có hình thức liên kết các biến khi biên dịch: liên  
kết trong và liên kết ngoài.  
Liên kết trong có nghĩa là bộ nhớ (vùng lưu trữ) được tạo ra để biểu diễn định danh  
chỉ cho file đang được biên dịch. Các file khác có thể sử dụng định danh đó đối với  
liên kết trong, hoặc với một biến toàn cục. Liên kết trong thường được thực hiện với  
các biến static.  
Liên kết ngoài có nghĩa là mỗi vùng nhớ được tạo ra để biểu diễn định danh cho tất  
cả các file đang được biên dịch. Các vùng nhớ này chỉ được tạo ra một lần và trình liên  
kết phải sắp xếp lại tất cả các tham chiếu tới vùng nhớ đó. Các tên hàm và các biến  
toàn cục có các liên kết ngoài và chúng có thể được truy cập trong các file khác bằng  
cách khai báo bằng từ khóa extern. Các biến định nghĩa ngoài các hàm (trừ các const)  
và các định nghĩa hàm là mặc định đối với liên kết ngoài. Chúng ta có thể buộc chúng  
thực hiện các liên kết trong bằng từ khóa static và chỉ rõ liên kết ngoài bằng từ khóa  
extern.  
Các biến cục bộ chỉ được sử dụng tạm thời, trên stack khi các hàm được gọi tới.  
Trình liên kết không biết tới chúng và do đó không có quá trình liên kết nào được thực  
hiện.  
2.6Các hằng  
17  
Lâ  
̣
2007  
Trong các trình biên dịch C cổ điển chúng ta có thể khai báo các hằng bằng cách sử  
dụng chỉ thị tiền xử lý, ví dụ:  
#define PI 3.14159  
Khi đó trong quá trình tiền xử lý bộ tiền xử lý sẽ thực hiện thay thế tất cả các ký  
hiệu PI mà nó gặp trong các file mã nguồn bằng giá trị 3.14159.  
Chúng ta vẫn có thể sử dụng cách này trong C++ tuy nhiên có rất nhiều vấn đề đối  
với kiểu khai báo này. Chúng ta không thể thực hiện kiểm tra kiểu đối với PI, không  
thể lấy địa chỉ của PI (vì thế không thể dùng con trỏ trỏ vào biến này). PI cũng không  
thể là một biến có kiểu người dùng định nghĩa. Cũng không thể xác định được tầm  
hoạt động của PI.  
C++ sử dụng từ khóa const để khai báo các hằng, cú pháp khai báo giống như khai  
báo biến chỉ khác là giá trị của hằng là không thay đổi.  
Các hằng trong C++ đều phải khởi tạo trước khi sử dụng. Các giá trị hằng cho các  
kiểu built-in được biểu diễn như là các số thập phân, bát phân, số hexa hoặc các số dấu  
phẩy động (đáng buồn là các số nhị phân được cho là không quan trọng) hoặc là các ký  
tự.  
Nếu không có các chỉ dẫn khai báo nào khác các hằng được coi là các số thập phân.  
Các hằng bắt đầu bởi số 0 được xem là các hằng trong hệ bát phân, còn 0x là các  
hằng trong hệ hexa. Các hằng dấu phẩy động được biểu diễn bởi phần thập phân và  
dạng mũ hóa ví dụ: 1e4, 1.4e4. Chúng ta có thể thêm các hậu tố f, F, L, l để chỉ rõ kiểu  
của các hằng loại này.  
Các hằng ký tự được biểu diễn giữa hai dấu ‘, nếu là ký tự đặc biệt thì có thêm dấu  
\ đứng trước.  
Biến kiểu volatile  
Trong khi từ khóa const có nghĩa là biến không thay đổi giá trị thì khai báo biến  
với từ khóa volatile có nghĩa là chúng ta không biết biến này sẽ thay đổi lúc nào và do  
đó trình biên dịch sẽ không thực hiện các tối ưu hóa dựa trên giả thiết về sự ổn định  
của biến này. Một biến volatile sẽ được đọc vào khi mà giá trị của nó được cần đến.  
Một trường hợp đặc biệt của các biến volatile là khi chúng ta viết các chương trình  
đa luồng. Ví dụ khi chúng ta đang chờ đợi một cờ nào đó đang được xử lý bởi một  
luồng khác thì biến cờ đó bắt buộc phải là volatile.  
Các biến volatile không có ảnh hưởng gì tới chương trình nếu chúng ta không thực  
hiện tối ưu hóa nó nhưng sẽ có thể có các lỗi rất tinh vi khi chúng ta tiến hành tối ưu  
hóa chương trình.  
3. Hàm trong C++  
Trong ngôn ngữ C cổ (không phải là ngôn ngữ C chuẩn mà chúng ta dùng hiện  
nay) chúng ta có thể thực hiện việc gọi hàm với số lượng tham số cũng như kiểu tham  
số tùy ý mà trình biên dịch sẽ không phàn nàn gì cả. Tất cả dường như đều tốt cho tới  
khi chúng ta chạy chương trình. Có thể chúng ta sẽ nhận được các kết quả rất khó hiểu  
mà không có bất cứ một dấu hiệu hay gợi ý nào về chúng. Đây có lẽ là một trong các  
lý do làm cho C trở thành một ngôn ngữ được đánh giá là ngôn ngữ Assembly cấp cao.  
Ngôn ngữ C chuẩn và C++ ngày nay có một cơ chế gọi là nguyên mẫu hay bản  
mẫu hàm (function prototype). Với cơ chế này chúng ta cần khai báo kiểu của các  
tham số của hàm, kiểu của hàm khi khai báo và định nghĩa chúng. Sự khai báo hay mô  
tả rõ ràng này được gọi là biểu mẫu của hàm. Khi hàm được gọi trình biên dịch sẽ sử  
18  
Lâ  
̣
2007  
dụng biểu mẫu của hàm để kiểum tra xem các tham số được truyền có đúng kiểu, số  
lượng cũng như giá trị trả về của hàm có được xử lý đúng hay không. Nếu như có các  
lỗi trong quá trình kiểm tra xảy ra trình biên dịch sẽ thông báo ngay cho lập trình viên  
biết trong quá trình biên dịch.  
Cú pháp khai báo một hàm như sau:  
<kiểu trả về của hàm> <tên hàm>(<danh sách các kiểu tham số và tên ngăn cách  
nhau bởi dấu phẩy>);  
Ví dụ: int max(int x, int y);  
Về bản chất chúng ta không cần có các tên tham biến, chúng chỉ thực sự cần khi  
chúng ta sử dụng chúng trong việc định nghĩa các hàm. Tuy nhiên điều này cũng  
không phải là bắt buộc đối với C++ (trong C là bắt buộc). Chúng ta có thể có một tham  
số nào đó không có tên và nó sẽ không được sử dụng trong thân hàm (tất nhiên vì nó  
không có tên). Khi chúng ta gọi tới hàm đó chúng ta vẫn phải truyền đúng các tham số.  
Tuy nhiên tác giả của hàm đó sau đó vẫn có thể sử dụng tham số ở đâu đó mà không  
cần thiết phải thay đổi các lời gọi hàm. Điều này rất tiện khi chúng ta không muốn có  
các lời cảnh báo về việc không sử dụng một tham số nào đó trong thân hàm.  
C và C++ có hai cách khác nhau để định nghĩa danh sách các tham số. Nếu chúng  
ta có một hàm func(), C++ sẽ hiểu là hàm này không có tham số, C lại hiểu là hàm này  
có thể có bất kỳ tham số nào. Một hàm func(void) sẽ được hiểu là không có tham số  
trong cả C và C++.  
Một trường hợp nữa xảy ra là khi chúng ta không xác định được số tham số cũng  
như kiểu tham số của hàm mà chúng ta muốn khai báo (gọi là một danh sách tham  
biến: variable argument list). Khi đó chúng ta sẽ sử dụng ký pháp (…). Tuy nhiên nên  
hạn chế sử dụng nó trong C++, chúng ta có nhiều cách khác để đạt được kết quả này  
mà không cần tới ký pháp đó.  
Các giá trị trả về của hàm  
Trong nguyên mẫu hàm chúng ta buộc phải chỉ rõ kiểu của hàm, nếu một hàm  
không có kiểu trả về thì kiểu của nó là void. Trong mỗi một thân hàm có kiểu bao giờ  
cũng có ít nhất một câu lệnh return. Khi gặp lệnh này trong quá trình thực hiện, hàm sẽ  
kết thúc.Trong các hàm không kiểu cũng có thể dùng return để thoát khỏi hàm.  
Một trong các điểm mạnh của ngôn ngữ C và C++ là một thư viện hàm rất phong  
phú và linh hoạt. Để sử dụng chúng, lập trình viên chỉ cần thực hiện include các file  
header chứa các prototype của chúng trong chương trình, phần còn lại sẽ tự do trình  
biên dịch và trình liên kết thực hiện.  
Chúng ta có thể tạo ra các thư viện hàm riêng cho mình để sử dụng. Tuy nhiên hãy  
xem kỹ phần manual của trình biên dịch trước khi thực hiện.  
4. Các cấu trúc điều khiển  
Các câu lệnh điều khiển là điều mà mọi lập trình viên cần phải biết trước khi viết  
bất cứ một chương trình nào. Chúng ta có các câu lệnh điều khiển: if-else, while, do,  
do-while, for và câu lệnh lựa chọn switch.  
Các câu lệnh điều kiện dựa trên kết quả đúng hoặc sai của một biểu thức điều kiện  
để xác định đường đi của chương trình. Trong C++ hai từ khóa true và false đã được  
đưa vào để biểu thị cho kết quả đúng hoặc sai của một biểu thức điều kiện, tuy nhiên  
các qui ước cũ vẫn có thể được dùng: một gía trị bất kỳ khác 0 sẽ được coi là đúng và  
một gía trị bằng 0 có nghĩa là sai.  
19  
Lâ  
̣
2007  
4.1Câu lệnh if-else  
Câu lệnh điều kiện if – else có thể được sử dụng dưới hai dạng khác nhau: có hoặc  
không có phần mệnh đề else. Cú pháp của hai dạng này như sau:  
if(expression)  
statement  
hoặc  
if(expression)  
statement  
else  
statement  
Biểu thức expression cho một giá trị true hoặc false. Phần câu lệnh “statement” có  
thể là một câu lệnh đơn kết thúc bằng một dấu chấm phẩy cũng có thể là một câu lệnh  
hợp thành, một tập các câu lệnh đơn giữa hai dấu { và }. Chú ý là phần câu lệnh  
statement cũng có thể bao gồm các câu lệnh điều kiện if – else.  
4.2Vòng lặp không xác định while  
Cú pháp:  
while (expression)  
statement  
Biểu thức được tính toán lần đầu tiên tại thời điểm bắt đầu của vòng lặp và sau đó  
được tính lại mỗi khi lặp lại quá trình thực hiện câu lệnh. Điều kiện để dừng vòng lặp  
không xác định while là giá trị của biểu thức expression bằng false.  
Như vậy điều cần chú ý ở đây là câu lệnh trong thân vòng lặp có thể không được  
thực hiện trong trường hợp biểu thức điều kiện cho giá trị false ngay lần đầu tính toán.  
Đôi khi chúng ta không cần sử dụng biểu thức điều kiện để kết thúc vòng lặp  
while, đó cũng là trường hợp đơn giản nhất của biểu thức điều kiện.  
4.3Vòng lặp không xác định do – while  
Sự khác biệt của vòng lặp do – while so với vòng lặp while là vòng lặp do – while  
thực hiện ít nhất một lần ngay cả khi biểu thức điều kiện cho giá trị false trong lần tính  
toán đầu tiên. Cú pháp của vòng lặp do – while:  
do  
Statement  
while(expression);  
Vì một vài lý do các lập trình viên thường ít sử dụng vòng lặp do – while hơn so  
với vòng lặp while.  
4.4Vòng lặp xác định for  
Vòng lặp for thực hiện một thao tác khởi tạo trước khi thực hiện lần lặp đầu tiên.  
Sau đó thực hiện quá trình kiểm tra điều kiện thực hiện của vòng lặp, và tại cuối mỗi  
lần thực hiện của vòng lặp thực hiện một thao tác nhảy qua một số giá trị nào đó của  
biến điều khiển. Cú pháp:  
for(initialization; conditional; step)  
statement  
Bất kỳ biểu thức nào trong các biểu thức initialization, conditional và step đều có  
thể là các biểu thức rỗng tuy nhiên trong trường hợp đó cần giữ lại các dấu chấm phẩy.  
20  
Tải về để xem bản đầy đủ
pdf 124 trang Thùy Anh 23800 Free
Bạn đang xem 20 trang mẫu của tài liệu "Giáo trình Lập trình hướngđối tượng và C++", để tải tài liệu gốc về máy hãy click vào nút Download ở trên

File đính kèm:

  • pdfgiao_trinh_lap_trinh_huongdoi_tuong_va_c.pdf