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 đủ
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:
giao_trinh_lap_trinh_huongdoi_tuong_va_c.pdf

