Lập trình cấu trúc trên BP7
Lê Minh Hoàng
Lập trình là một công việc khó đòi hỏi phải có tư duy nhanh nhạy và sự tuân thủ kỷ luật
nghiêm ngặt trong phong cách lập trình. Tư duy tốt để có thể nắm bắt được cấu trúc bài
toán và tìm ra cách giải, còn phong cách tốt là để biến tất cả những cố gắng của trí não và
đôi tay thành kết quả cuối cùng. Một điều rất hay thấy ở người mới học lập trình là có thể
họ nắm bắt được thuật toán rất nhanh nhưng do cách làm việc thiếu hiệu quả nên tiêu tốn
rất nhiều thời gian thậm chí càng làm càng bị rối trong hết lỗi này đến lỗi khác. Như vậy
một phong cách lập trình tốt trước hết phải là một phong cách viết chương trình có xác
suất xảy ra lỗi thấp nhất, và cho dù có xảy ra lỗi thì cũng dễ dàng chỉnh sửa. Còn việc
viết các chương trình cỡ trung bình và lớn mà không có lỗi thì trừ khi sau này người ta
sáng tạo ra các máy lập trình chứ nhân loại thì chẳng có ai viết chương trình máy tính mà
không có lỗi cả.
Ta sẽ phân tích hai vấn đề: 'Làm thế nào để hạn chế lỗí và 'nếu có lỗi thì tìm và sửa
thế nàó.
Hạn chế lỗi thường được quyết định bởi hai yếu tố:
Môi trường lập trình và phong cách lập trình. Cách đây vài chục năm với một máy tính cỡ
lớn dùng băng đục lỗ, thì rõ ràng khả năng hạn chế và tìm lỗi là rất ít bởi mọi lỗi đều dẫn
tới kết quả sai hoặc treo máy, hay với một ngôn ngữ lập trình cấp thấp thì ta chỉ có thể viết
những đơn vị chương trình nhỏ mà thôi, bởi tính phi cấu trúc của nó làm cho số lỗi ước
tính sẽ tăng theo bình phương chiều dài chương trình. Như vậy, xét cho cùng, phải phát
triển các ngôn ngữ lập trình (ADA, PASCAL, C++, BASIC) và chương trình dịch (TP,
Borland C, Delphi, VB), phải sáng tạo ra các cách thiết kế (Từ trên xuống, hướng sự kiện,
hướng đối tượng) chính là để giảm bớt công sức cho các lập trình viên trong việc sửa
lỗi để có thể tạo ra những sản phẩm lớn hơn, hoàn thiện hơn, ít lỗi hơn. Rất may cho chúng
ta là được làm việc với thế hệ máy tính nhanh, ổn định và thân thiện như ngày nay và với
một trình biên dịch như BP7 có khả năng bắt lỗi rất tốt và hỗ trợ lập trình cấu trúc.
Phong cách lập trình cấu trúc trên BP7 nên thế nào để hạn chế lỗi?
- Viết một chương trình trên BP7 phải tuân theo các bước: Tìm hiểu yêu cầu bài toán →
tìm thuật toán → xây dựng cấu trúc dữ liệu và dựng giải thuật → Viết chương trình + Thử
+ Sửa lỗi → Nâng cấp → Hoàn thành. Không được đảo lộn hay bỏ qua bước nào. Lưu ý
riêng bước viết chương trình + thử + sửa lỗi, không phải ta viết xong cả chương trình rồi
mới thử mà khi có bất kỳ cơ hội nào để thử chạy chương trình (dù là chỉ xong một phần
nhỏ) ta chạy thử ngay để có thể tạm yên tâm rằng phần vừa viết không có lỗi.
- Viết chương trình theo đúng thiết kế từ trên xuống, đầu tiên viết chương trình chính
trước, gồm các bước rất tổng quát, tiếp theo cụ thể hoá từng bước bằng cách viết các
chương trình con tương ứng, thậm chí đôi khi tại mỗi bước cụ thể hoá ta chỉ làm đến mức
độ nào đó rồi lại viết tiếp các chương trình con v.v…cách làm này dựa trên nguyên tắc
'chia để trị' rất hiệu quả và đã được thực tế chứng minh. Có một vài ý kiến còn cho rằng, để
tiện quan sát, tất cả các chương trình con viết theo cách trên không nên có độ dài quá một
trang màn hình. Một số người có thói quen viết chương trình cứ từ đầu chí cuối, làm hết
chương trình con rồi đến chương trình chính. Cách làm như vậy không logic một chút nào
bởi khi chưa viết ra chương trình chính, ta đâu đã xác định được nhiệm vụ của các chương
trình con, lại càng không xác định được là phải truyền cho chương trình con bao nhiêu
tham số và là những tham số nào. Đối với chương trình nhỏ thì có thể không nhận thấy
điều đó nhưng với những chương trình lớn thì cách làm như trên sẽ phải trả giá rất đắt
bằng rất nhiều động tác xoá đi viết lại.
- Viết chương trình thì khó tránh phải lỗi cú pháp (Syntax), thiếu dấu thiếu chữ trong việc
soạn chương trình cũng là thường tình. Các lỗi thiếu ';', thiếu ngoặc, thiếu ':', tên chưa khai
báo, v.v…thì chỉ cần trình biên dịch báo lỗi đúng chỗ đó là sửa được ngay. Cái lỗi cú pháp
sửa mất nhiều thời gian nhất là lỗi sai cấu trúc khối (Có 'begin', thiếu 'end;' hay có 'repeat'
thiếu 'until' v.v…). Trình biên dịch không thể chỉ ra được chỗ thiếu bởi ví dụ như có
'begin' mà không có 'end;' thì chữ 'end;' thiếu có thể cho ở nhiều nơi khác nhau mà vẫn hợp
lý cả. Trình dịch chỉ thông báo lỗi chung chung là thiếu ';' mà thôi và như vậy thời gian của
chúng ta lại lãng phí vào việc dò trong cả chương trình xem thiếu chỗ nào. Trong khi lỗi đó
sẽ không bao giờ xảy ra nếu như khi soạn chương trình, mỗi khi gõ xong phần mở khối thì
ta gõ luôn phần kết khối và lùi vào gõ đoạn giữa khối sau. Điều đó không những làm cho
khối lệnh được sáng sủa, mở khối kết khối thẳng hàng mà còn làm ta không phải bận tâm
gì về lỗi thiếu cấu trúc khối nữa.
- Đặt tên gợi nhớ chức năng, không ngại đặt tên dài, trước khi dùng biến phải cân nhắc
xem tầm hoạt động của nó là địa phương hay toàn cục, nhiệm vụ của nó để làm gì. Nên đặt
tên tiếng Anh bởi nếu đặt tiếng Việt khi viết vào máy tính không có dấu dễ bị hiểu theo
nghĩa khác. Còn một số việc nhỏ nữa tuy không quan trọng lắm: Từ khoá viết thường, tên
thủ tục và hàm chuẩn viết hoa đầu từ tiếng Anh. Ví dụ: not, xor, and, or, mod, div, SizeOf,
LongInt, Integer, Abs, Sqrt, Sin, Cos, v.v… Các tên định nghĩa trong chương trình khi khai
báo viết hoa, thường thế nào thì thống nhất trong cả chương trình viết hoa, thường như thế.
Sau dấu phân cách từ (phẩy hoặc chấm, chấm phẩy…) nên có 1 dấu cách, trước và sau dấu
gán := đều có dấu cách. Đọc thêm các chương trình mẫu của Borland để rõ điều này.
- Hạn chế tối đa việc viết các thủ tục và hàm lồng nhau quá nhiều cấp, gây rối trong việc
đọc chương trình.
Nếu ta viết chương trình mà bị lỗi thì sửa như thế nào?
Như đã nói ở trên, ta nói sửa lỗi ở đây là sửa lỗi sai trong cài đặt thuật toán chứ không nói
đến lỗi syntax nữa. Muốn sửa lỗi cài đặt giải thuật thì về cơ bản các kỹ thuật gỡ rối là
khoanh vùng xác định lỗi tức là thu hẹp phạm vi dò tìm tới khi xác định chính xác lỗi và
sửa.
Lỗi chương trình xảy ra khi ta có một bộ dữ liệu cho vào chương trình chạy được kết quả
không theo ý muốn. Khi đó ta sử dụng trình gỡ rối Debugger của BP7 như sau:
Bước 1: Mở cửa số Watches theo dõi giá trị bằng cách bấm Ctrl + F7 hay chọn menu
Debug/Ađ Watch. Gõ tên biến hay biểu thức cần theo dõi, tên biến sẽ hiện ra trong cửa sổ
Watches. Dùng phím F6 để luân chuyển hoạt động giữa cửa sổ soạn thảo và cửa sổ
Watches. Bấm Ctrl + F5 hay chọn menu Window/Size/Move sau đó điều chỉnh vị trí và
kích thước hai cửa sổ sao cho phù hợp nhất, tốt nhất không nên che nhau.
Bước 2: Bấm F8 (Run/ Step over) để thực hiện chương trình từng bước, khi đó trên cửa sổ
soạn thảo sẽ có một vạch ngang cho biết chương trình đã chạy tới dòng nào. Mỗi lần bấm
F8 thì chương trình chạy đúng 1 dòng và dừng lại, thông báo giá trị các biến được theo dõi
trong cửa số Watches. Nhớ rằng mỗi lần chạy qua 1 bước, ta phải tự tính xem, nếu đúng
thì giá trị biến theo dõi phải là bao nhiêu và so sánh với kết quả trong cửa sổ
Watches, nếu giống thì dò tiếp, nếu sai thì chắc chắn dòng lệnh vừa chạy qua là dòng lệnh
gây lỗi, ta đã khoanh vùng được một lần.
Bước 3: Nếu dòng lệnh gây lỗi chỉ là một dòng lệnh tương đối đơn giản: như biểu thức số
học chẳng hạn thì nhiều khả năng do ta gõ sai hoặc gõ thừa thiếu một yếu tố gì đó, ta xem
kỹ lại biểu thức đó và sửa. Nhưng nếu dòng lệnh gây lỗi lại là lời gọi chương trình con thì
sao. Ta dừng việc gỡ rối bằng cách bấm Ctrl + F2 (Run/ Program reset) và bắt đầu lại từ
đầu, chỉ có điều khi đến dòng lệnh gây lỗi ta không bấm F8 chạy qua nữa mà ấn F7 (Run/
Trace into) để trình gỡ rối truy vết vào chương trình con và lại thực hiện chạy từng bước
(Step over − F8) hay truy vết (Trace into − F7) tiếp tục thu hẹp phạm vi tìm lỗi.
Lưu ý:
1. Cửa sổ Watches có thể theo dõi cùng lúc nhiều biến
2. Để tiết kiệm thao tác cho đỡ phải bấm F8 quá nhiều ta có thể di chuyển con trỏ tới một
dòng và bấm F4 (Run/ Go to cursor) để cho chương trình sẽ chạy tới dòng đó thì dừng lại,
hiện ra vạch ngang và từ đó gỡ rối tiếp.
3. Khi gỡ rối ta phải dùng khá nhiều cửa sổ, chính vì vậy mà những cửa sổ nào không cần
nhất thiết nên đóng lại để khỏi tốn bộ nhớ và che lấp các cửa sổ có ích.
4. Ta cần phải kết hợp cả kỹ thuật đưa giá trị trung gian ra màn hình và tạo điểm dừng theo
điều kiện để tiện gỡ rối vòng lặp, ví dụ:
Xét đoạn chương trình
for i := 1 to 1000 do
for j := 1 to 1000 do
begin
S1;
S2;
end;
Giả sử ta biết được lệnh S1 là lệnh gây lỗi, nhưng không phải lúc nào cũng lỗi, nó chỉ gây
lỗi khi i = 501 và j =1000 (điều này có thể biết được bằng cách đưa giá trị trung gian của i
và j ra màn hình). Nếu ta sử dụng lệnh chạy từng bước F8 thì không ổn bởi như vậy ta sẽ
phải bấm 500x1000 lần. Khi đó ta làm như sau, di chuyển con trỏ đến dòng chứa lệnh S1.
Chọn Debug/ Ađ breakpoint. Gõ vào ô Condition điều kiện: (i = 501) and (j = 1000) sau đó
bấm Enter để máy nhận. Cuối cùng chỉ việc bấm Ctrl+F9 để chạy, khi chạy đến dòng chứa
lệnh S1 mà có i = 501 và j = 1000 thì máy sẽ dừng và hiện vạch ngang cho gỡ rối tiếp (gỡ
theo vết vào chương trình con S1 chẳng hạn).
5. Cuối cùng, rất quan trọng trong gỡ rối là phải xác định chính xác lỗi, đi sửa lung tung
theo kiểu 'có bệnh vái bốn phương' là điều tối kỵ, bởi sửa không đúng chỗ thì lỗi ngày càng
tai hại.
I. Một vài kỹ thuật nhỏ trong BP7
1. Break, Continue, Exit, Halt và Goto.
Trong một vòng lặp, nếu gặp lệnh Break thì vòng lặp đó sẽ bị ngừng vô điều kiện.
Ví dụ:
Nhập vào hai số x và y, tính thương của x/y. Quá trình cứ tiếp tục cho tới khi nhập y = 0:
repeat
Readln(x, y);
if y = 0 then Break;
Writeln(x / y);
until False; {Không cần điều kiện thoát}
Trong một vòng lặp, nếu gặp lệnh Continue thì toàn bộ lệnh của vòng lặp nằm phía sau
continue sẽ bị bỏ qua, máy kiểm tra ngay điều kiện xem còn lặp tiếp không, nếu còn lặp
tiếp thì làm lần lặp kế tiếp luôn.
Ví dụ:
Nhập vào số x kiểu LongInt và kiểm tra dữ liệu Abs(x)≤ 46340; nếu đúng, in ra x
2
, nếu
không bắt nhập lại. (Bởi 46341
2
tràn phạm vi LongInt)
repeat
Readln(x);
if Abs(x) > 46340 then Continue;
Writeln(Sqr(x));
until Abs(x) <= 46340;
Trong một chương trình con, nếu chạy tới lệnh Exit thì chương trình con sẽ thoát ra ngay.
Chạy tới lệnh Halt ở bất cứ nơi đâu, chương trình sẽ dừng vô điều kiện. (Điều này tương
đương với lệnh Exit nằm ở thân chương trình chính}
Bốn lệnh kể trên không có ở mọi thế hệ PASCAL. Nói chung cũng có những cấu trúc
tương đương để thay, nhưng dùng được các lệnh này một cách uyển chuyển và hợp lý sẽ
làm cho đỡ tốn công sức nhiều, (đặc biệt là Break và Exit).
Để dễ nhớ công dụng, ta có thể quan niệm như sau: Break là Goto tới một nhãn ngay phía
dưới bên ngoài vòng lặp. Continue là Goto đến điểm kiểm tra điều kiện lặp. Exit là Goto
tới cuối chương trình con (chữ End;). Còn Halt thực sự không phải lệnh Goto.
Nói chung, Goto là lệnh phi cấu trúc nên ta cố gắng sử dụng càng hạn chế càng tốt, bằng
cách thay nó bằng các lệnh kể trên và các lệnh có cấu trúc. Tuy nhiên, cũng không nên quá
cứng nhắc, nếu muốn thoát vô điều kiện cùng một lúc 7, 8 vòng lặp For thì nếu không dùng
Goto, ta sẽ phải thay bằng các cấu trúc lặp với số lần không biết trước (Repeat, While) hay
sử dụng 7, 8 lệnh Break, chương trình sẽ trở nên khó đọc hơn và chạy chậm hơn. Hạn chế
không có nghĩa là cấm dùng (cũng như đệ quy vậy thôi), ta phải xác định được nhược điểm
của Goto để viết các chú thích đúng chỗ giúp cho chương trình sáng sủa và dễ bảo trì.
2. Hệ cơ số 16 (hexa)
Trong hệ cơ số 16, có 16 chữ số, ký hiệu như sau: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E,
F. Về mặt giá trị các chữ số từ A tới F mang giá trị từ mười đến mười lăm.
Để chuyển đổi từ hệ thập phân sang hệ 16 ta có thể dùng phép chia liên tiếp với số chia là
16.
Ví dụ: Đổi số 43982 sang hệ 16
- 43982/16 = 2748 dư 14 (dư E)
- 2748 /16 = 171 dư 12 (dư C)
- 171/16 = 10 dư 11 (dư B)
- 10/16 = 0 dư 10 (dư A).
Vậy số 43982 sẽ bằng ABCE trong hệ 16, trong BP7, viết $ABCE cũng như là viết 43982.
Đổi số từ hệ hexa sang hệ thập phân có thể đổi bằng cách nhân từng chữ số Hexa với các
luỹ thừa tương ứng của 16.
$ABCE = 10*16
3
+ 11 * 16
2
+ 12 * 16 + 14 = 43982.
Để đổi một số trong hệ nhị phân ra hệ Hexa, ta xét từ hàng đơn vị lên, đổi từng nhóm 4
chữ số nhị phân được một chữ số hệ 16.
10 1010 1011 1100 1110
(2)
= 2ABCE
(16)
Phép chuyển ngược lại, ta đổi một chữ số Hexa thành nhóm 4 chữ số nhị phân. Chứng
minh điều trên khá dễ dàng.
Xét hai lệnh gán sau với I là biến Integer:
I := $0000FFFF
I := $FFFFFFFF
Mới trông thì tưởng như $0000FFFF < $FFFFFFFF, nên lẹnh gán thứ nhất tràn số $FFFF =
65535) thì lệnh gán thứ hai sai là tất nhiên, nhưng thực ra không phải như vậy.
Các hằng số hexa 16 bít (≤ 4 chữ số hexa) được BP7 coi như các giá trị LongInt 32 bit
$0000FFFF = 0000 0000 0000 0000 1111 1111 1111 1111 = 65535 thì tràn Integer
$FFFFFFFF = 1111 1111 1111 1111 1111 1111 1111 1111 = -1. (Không tràn)
Ta nhớ lại cách lưu trữ giá trị LongInt trong máy tính, bít đầu tiên = 1 tức là số âm và giá
trị số âm đó sẽ bằng giá trị dãy 31 chữ số tiếp theo trừ đi 2
31
. nên kết quả sẽ bằng -1. Viết
số -1 trong hệ Hexa là viết như vậy đấy, nếu như không muốn dùng ký pháp không chính
tắc -$1.
3. Tổ chức bộ nhớ trong chế độ thực
Mọi byte nhớ trong chế độ thực đều được đánh địa chỉ (như đánh số nhà vậy). Hệ thống
địa chỉ được đánh số từ 0 cho tới $FFFFF (Từ 0 tới 1048575, có 2
20
= 1MB ô nhớ) bộ nhớ