TỰ TIN HƠN KHI GẶP LỖI TRONG LẬP TRÌNH

02
06
'15

Bài viết này dành cho Sinh viên năm 1, 2 chuyên ngành Công nghệ thông tin đã và đang học lập trình C cơ bản và các Sinh viên yêu thích lập trình. Mục tiêu: Bài viết nhằm giúp sinh viên có thể phân biệt các loại lỗi trong lập trình, nhận biết các nguyên nhân lỗi nhanh và có thể sửa chữa triệt để các lỗi cơ bản trong lập trình. Từ đó sinh viên tự tin hơn trong việc giải quyết các lỗi mắc phải và yêu thích công việc lập trình hơn.

1.     Vì sao phải tuân thủ đúng quy trình phát triển phần mềm?
Khi bắt đầu giải quyết một bài toán trên máy tính hay thiết kết một phần mềm dù rất nhỏ thì cá nhân hay nhóm lập trình cần tuân thủ  đúng các bước trong quy trình phát triển phần mềm sau:
1.     Xác định yêu cầu (Specify the problem)
2.     Phân tích yêu cầu (Analysis the problem)
3.     Thiết kế giải thuật (Design the algorithm)
4.     Hiện thực giải thuật (Implement the algorimth)
5.     Kiểm thử và kiểm chứng (Test and Verify the program)
6.     Bảo trì và cập nhật phần mềm (Maintain and update the program)
Việc tuân thủ các bước trong quy trình này sẽ giúp ta hạn chế các lỗi lớn trong lập trình và giúp ta giải giải quyết các bài toán phức tạp, các chương trình lớn một cách chính xác và đúng với yêu cầu hơn. Bạn cần tránh việc đọc qua loa yêu cầu xong bắt đầu code theo ý nghĩ trong đầu, nghĩ đến đâu code đến đó mà chưa nắm rõ các yêu cầu , cũng như chưa xác định được cách thức cụ thể để giải quyết bài toán hay không biết giải pháp của mình có khả thi hay không. Với thói quen lập trình này này bạn chỉ có thể viết được các chương trình đơn giản và là một rào cản rất lớn để bạn có thể viết được các chương trình có độ khó và phức tạp cao.
Ngay cả khi bạn là một lập trình viên có nhiều năm kinh nghiệm, hiếm khi chương trình bạn chay ngay từ lần thử nghiệm đầu tiên. Hãy luôn nhớ rằng “ Nếu chương trình có thể sai, thì nó sẽ sai” , định luật Murphy dường như rất đúng trong  lập trình.
Trong thực tế  lỗi phần mềm rất thường xảy ra. Thuật ngữ Bugs đề cập các lỗi các lập trình viên gặp phải trong quá trình sửa lỗi chương trình hay gọi là “debugging”. Khi trình biên dịch tìm ra lỗi, máy tính sẽ hiển thị các thông báo chỉ ra cụ thể lỗi mắc phải hoặc nguyên nhân có thể gây ra lỗi. Nhưng không phải lúc nào  lỗi cũng dễ dàng phát hiện ra và đôi khi các thông báo lỗi gây hiểu lầm cho lập trình viên. Khi bạn có kinh nghiệm bạn sẽ trở nên giỏi hơn trong việc định vị lỗi và sữa chữa lỗi.
2.     Hiểu rõ hơn về Bugs
Lỗi (Bugs) được phân thành ba loại chính là:
·        Lỗi cú pháp (syntax errors)
·        Lỗi thực thi (run-time errors)
·        Lỗi luận lý (logic errors)
Độ khó để tìm ra lỗi cũng tăng dần theo thứ tự liệt kê các loại lỗi ở trên. Nghĩa là lỗi cú pháp sẽ dễ dàng phát hiện nhất và lỗi luận lý sẽ khó phát hiện nhất. Sau đây chúng ta sẽ phân tích sâu vào nguyên nhân, nguyên tắc để tránh lỗi cũng như một số lỗi thường gặp như là một kinh nghiệm để chúng ta chuẩn đoán lỗi nhanh và chính xác hơn.
3.     Lỗi cú pháp (syntax errors)

  • Lỗi cú pháp là gì?  

Lỗi cú pháp được phát hiện bởi trình biên dịch khi biên dịch chương trình. Nếu một câu lệnh mắc lỗi cú pháp, nó sẽ không được biên dịch và chương trình của bạn sẽ không được thực thi.
  • Làm thế nào để phát hiện nhanh các lỗi cú pháp?

Để tránh lỗi cú pháp bạn cần nắm vững các cú pháp đã được quy định trong Ngôn ngữ lập trình mà bạn sử dụng; Đọc kỹ các thông báo lỗi của trình biên dịch; Trong đa số các trường hợp nên bắt đầu sửa lỗi từ thông báo đầu tiên và biên dịch lại sau mỗi lần sửa xong một lỗi cú pháp vì trình biên dịch thường thông báo hàng loạt lỗi phát sinh từ một lỗi gốc.
Xét đoạn code trong ví dụ sau:

Ta nhận được các thông báo lỗi sau từ Visual C++ 2010:

Đoạn code trong ví dụ chỉ tồn tại một lỗi tại dòng code số 1 nhưng ta thấy trình biên dịch báo đến bốn lỗi. Trong Visual C++ ta có thể xem lỗi trong cửa sổ Output hoặc Error List. Thông thường ta lần vết từ thông báo lỗi đầu tiên và số thứ tự của dòng code để chẩn đoán nơi tồn tại lỗi và biên dịch lại mỗi khi sửa xong để tiếp tục tìm lỗi khác.
  • Các lỗi cú pháp và thông báo lỗi nào thường gặp?

Có vô số lỗi cú pháp mà lập trình viên thường mắc phải. Nếu là người mới học lập trình thì bạn nên cố gắng hết sức có thể để tránh những lỗi cú pháp liệt kê dưới đây:
  Chẩn đoán thông thường Thông báo Cách sữa lỗi
1 Thiếu dấu ‘;’ kết thúc câu lệnh Expected a ‘;’
Missing ‘;’ before identifier
Thêm dấu ‘;’ trước câu lệnh chứa identifier ‘…’
2 Chưa khai báo kiểu dữ liệu ‘…’ : undeclared identifier Khai báo dữ liệu
3 Khai báo dữ liệu nhiều lần '…' : redefinition; multiple initialization
 
Dùng tên khác hoặc xóa đi
4 Dùng hàm / thành viên không có tồn tại trong struct / class 'm' : is not a member of 's'
 
Thêm ‘m’ vào ‘s’
Hoặc xóa ‘m’
5 Các cặp ‘(‘ và ‘)’ không tương ứng Syntax Error: ’)’ expected Thêm hoặc xóa bớt cho tương ứng
6 Phép gán không tương đồng về kiểu dữ liệu, không thể gán '=' : cannot convert from 'type1' to 'type2'
 
Làm cho 2 vế của phép gán tương đồng hoặc có thể gán được
7 Hàm có kiểu trả về thiếu câu lệnh return ‘…’ must return a value Thêm câu lệnh return cuối hàm
8 Số lượng đối số truyền cho hàm khác với định nghĩa hàm ‘…’: Function does not take n argument Truyền số đối số cho hàm đúng như đã định nghĩa hàm
9 Kiểu của đối số truyền vào hàm không đúng '…' : cannot convert parameter n from ' ‘type 1' to 'type 2' Chỉnh sưa kiểu ‘type1’ cho giống với ‘type2’ đã định nghĩa trong hàm
...      
  • Chương trình đã chạy có tồn tại lỗi cú pháp không?

Câu trả lời là “ Có”. Một số lỗi cú pháp tồn tại trong chương trình nhưng trình biên dịch không báo lỗi hoặc bỏ qua hoặc chỉ cảnh báo (Warning) và vẫn thực thi chương trình.Thông thường, các lỗi này thường sẽ gây lỗi cấp cao hơn như lỗi run-time hay lỗi logic hoặc gây mất mát dữ liệu. Ví dụ một số lỗi cú pháp mà trình biên dịch không báo lỗi như sau:
·        Sử dụng  ‘;’ sau các cấu trúc điều khiển, cấu trúc lặp, làm mất hiệu lực của các cấu trúc:
  if (a == 0);
{
       c = d;
}
  for (int i = 0; i < 3; i ++);
  {
       printf ("Hello! ");
}
 
Nếu a khác 0 thì phép gán c = d vẫn thực hiện.
Câu lệnh in chỉ được thực hiện 1 lần thay vì 3 lần như mong muốn.
·        Sử dụng nhầm phép so sánh ‘=’ thay vì ‘= =’:
if (a = b)
{
     C ;
}
     b luôn bằng a, c sẽ không được thực thi nếu b khác 0. Điều này gây nên lỗi logic.
 
·        Sử dụng nhầm phép so sánh ‘=!’ thay vì ‘!=’:
if (a =! b)
{
     c;
}
     a được gán bằng với not(b). . Điều này gây nên lỗi logic.
·        Sử dụng thiếu chặt chẽ trong phép toán luận lý:
                                                     if (0 < a < 5)
                             {
                                       c;
                                                     }
                                  c luôn được thực thi vì if luôn đúng (0<a trả về 1, 1 < 5 luôn đúng)
·        Hàm không trả về giá trị:
int foo (a){
if (a)
{
        return(1);
}
}
                                  Trong một số trường hợp a=0, hàm không trả về giá trị gây lỗi run-time
 
Tóm lại, để hạn chế gặp lỗi cú trong lập trình ta không nên chỉ dựa vào thông báo lỗi để chắc chắn là chương trình không còn tồn tại lỗi cú pháp mà phải bảo đảm viết chính xác mỗi dòng lệnh trước khi sang dòng lệnh mới.
4.     Lỗi thực thi (run-time errors)
  • Lỗi thực thi là gì?

Lỗi thực thi xảy ra khi máy tính được ra lệnh thực hiện một hành động lỗi. Khi xảy ra lỗi run-time, máy tính dừng thực thi chương trình và hiển thị thông báo chuẩn đoán tại dòng lệnh gây ra lỗi.
  • Khi nào gặp lỗi run-time?

Một số lỗi run-time thường xảy ra khi:
·         Overflow : Kết quả tính toán cho quá lớn không thể lưu trữ vào biến
·         Divide by Zero: Chia một số cho 0.
·         Error Memory : Thực hiện truy cập vào vùng nhớ không xác định làm cho kết quả trả về không lường được. Ví dụ: truy cập phần tử nằm ngoài vùng giới hạn của mảng; Truy cập vào vùng nhớ heap sau khi đã giải phóng bộ nhớ.
·         Uninitialized Data Access : Truy cập vào bộ nhớ trước khi bộ nhớ được khởi tạo vì vậy không lường trước được kết quả trả
Một số ví dụ về lỗi Run-time và lỗi cú pháp trên bộ nhớ:
Overflow:
int main()
{
       int n = 1000;
       cout << Square(n);
       return 0;
}
short int Square (int nSize)
{
       return nSize * nSize;
}
Kết quả :16960
Ta thấy chương trình vẫn được biên dịch và thực thi nhưng cho kết quả sai là 16960 thay vì 1000000.
Khắc phục : hàm Square trả về kiểu dữ liệu lớn hơn kiểu int. Ví dụ: long int, hoặc dùng mảng để xử lý số lớn
 
 
Error Memory:  Lỗi bộ nhớ có thể phân làm hai loại lỗi trên bộ nhớ Heap và lỗi trên bộ nhớ Stack. Thông thường lỗi xảy ra khi:
·         Lỗi thu hồi bộ nhớ : Lệnh cấp phát và thu hồi phải đi cặp với nhau: trong C sử dụng malloc – free, C++ sử dụng new –delete. Tránh sử dụng nhầm lẫn giữa các cặp cấp phát và thu hồi giữa C và C++ vì ta không chắc lệnh thu hồi bộ nhớ thực hiện đúng như đã cấp phát
   char *s = (char*) malloc(5);
     delete s;
 
Kết quả : Chương trình vẫn chạy nhưng không chắc bộ nhớ được thu hồi đúng
Khắc phục : sử dụng lệnh free để giải phóng bộ nhớ thay vì delete
 
 
·         Không thu hồi bộ nhớ đã cấp phát (Memory leak): lỗi này xảy ra khi ta thiếu câu lệnh thu hồi bộ nhớ cho biến đã cấp phát
     char *pStr = (char*) malloc(512);
     return;
Kết quả : Lỗi cú pháp. Chương trình vẫn thực thi nhưng bộ nhớ không được thu hồi
Khắc phục : thêm câu lệnh free(pStr) trước câu lệnh return;
 
 
·         Lỗi không thể thu hồi bộ nhớ (Mising Deallocation):
     char* pStr = (char*) malloc(20);
     free(pStr);
     free(pStr); // Lỗi
Kết quả : Lỗi thực thi
Khắc phục : xóa câu lệnh free(pStr) ở cuối
 
 
    
void nhap(int *a,int n)
{
    a=new int[n];
     for(int i=0;i<n;i++)
        cin>> *(a+i);
}
void main(){
    int *x; int n=6;
     //
    nhap(x,n);
    delete[] x;
}
 
Kết quả : Thông báo lỗi: “Run-Time Check Failure #3 - The variable 'x' is being used without being initialized”
x chưa trỏ đến biến nào nên không thể thực hiện lệnh delete[] x được.
Khắc phục : Chuyển lệnh cấp phát bộ nhớ cho con trỏ x trong main tại vị trí //:      x=new int[n];
 
 
·         Không khởi tạo khi truy cập biến nằm trên bộ nhớ heap và  stack
     void main()
     {
           char *pStr=(char*) malloc(512);
           char c = pStr[0];//không khởi tạo
           system("pause");
     }
     void func()
     {
    int a;
    int b = a * 4; //không khởi tạo
     }
Kết quả : Lỗi thực thi
Khắc phục : khởi tạo
 
 
·         Lỗi truy cập đến phần tử mảng có chỉ vượt chỉ số mảng đã khai báo:
    
const int NUM_DAYS = 7;
void main()
{
     int temp[NUM_DAYS];
     int i;
     for (i=1; i<= NUM_DAYS; i++){
           cout <<"Enter a value: ";
           cin >> temp[i];
     }
}
Kết quả : lỗi thực thi. Thông báo lỗi: “Run-Time Check Failure #2 - Stack around the variable 'temperatures' was corrupted.”
Lỗi truy cập đến phần tử temp[i] khi I nằm ngoài mảng temp
Khắc phục : for (i = 0;i<NUM_DAYS; i++)
 
 

Làm cách nào để chẩn đoán code gây lỗi run-time?

Việc xác định code gây lỗi run-time rất khó khăn. Cách thông thường nhất là sử dụng công cụ các công cụ debug của phần mềm ngôn ngữ lập trình. Visual C++ 2010 hổ trợ người lập trình ba cách debug như sau:
   - Tính năng Point-to-view:
Khi chương trình gặp lỗi, sẽ hiển thị thông báo lỗi như sau

Bạn nhấn “Break” để dừng chương trình. Sau đó Visual Studio sẽ thông báo cho bạn vị trí dừng. Rê chuột đến tên của biến, tính năng point-to-view sẽ hiển thị giá trị mà biến đang lưu trữ. Nhờ vậy bạn có thể biết được giá trị biến tại thời điểm xảy ra lỗi và dễ dàng chẩn đoán nguyên nhân lỗi hơn.

   - Breaking point :
Tính năng này cho phép chương trình đang thực thi có thể dừng tại những vị trí bạn mong muốn hoặc nghi ngờ có lỗi. Các bước thực hiện như sau:
o   Đến dòng lệnh muốn đặt break point, click chuột vào cột bên trái màu xám. Một dấu chấm đỏ như hình trên sẽ xuất hiện. Khi debug, chương trình sẽ chạy tới đó rồi tự động dừng lại. Để xóa break point bạn click chuột lên chấm tròn đỏ một lần nữa.


o   Nhấn F5 để biên dịch chương trình. Khi chương trình dừng, bạn quan sát các thông báo trên cửa sổ Breakpoints ở góc phải bên dưới màn hình. Có nhiều chế độ để tiếp tục thực thi chương trình:
o   Step Over: nhấn F10 để thực thi từng dòng lệnh. Nhưng không nhảy vào hàm được gọi.
o   Step Into: nhấn F11 để tiếp tục thực thi từng dòng lệnh. Nếu có lời gọi hàm thì sẽ nhảy đến hàm được gọi.
o   Step Out: nhấn Shift+F11 để tiếp tục thực thi chương trình cho đến khi hàm được gọi trả về kết quả thì chương trình sẽ tiếp tục dừng
 
ü Cửa sổ Watch : chức năng này tương tự point-to-view nhưng tiện tích hơn là bạn có thể xem giá trị các biến trong cũng một cửa sổ. Để thêm biến vào cửa sổ Watch, bạn click chuột phải ngay tên biến, chọn “Add watch” hoặc gõ tên biến vào mục Name trên cửa sổ Watch.

Tóm lại, các công cụ debug hổ trợ bạn quan sát chương trình thực thi một cách chi tiết hơn. Việc xác định và sửa lỗi thực thi phụ thuộc rất nhiều vào kinh nghiệm lập trình của bạn.
5.     Lỗi luận lý (logic errors):
  • Lỗi luận lý là gì?

Là loại lỗi khó tìm thấy và sửa chữa nhất vì dấu hiệu lỗi không thể hiện rõ ràng. Thông thường các chương trình chạy thành công, nhưng nó không trả về kết quả như mong đợi. Trình biên dịch không thể chuẩn đoán lỗi luân lý, do đó lập trình viên phải là người kiểm tra toàn bộ từng dòng code của mình và đảm bảo chương trình chạy đúng như mong đợi.
  • Nguyên nhân gây nên lỗi luận lý ?

Lỗi luận lý tồn tại trong chương trình thường là do sự kết hợp của các nguyên nhân sau đây:
·        Bạn chưa thật sự hiểu yêu cầu của chương trình
·        Bạn chưa hiểu rõ các hoạt động của từng dòng code trong chương trình mình viết
·        Bạn đã bất cẩn trong khi lập trình
Lỗi logic phát hiện càng muộn thì càng gây thiệt hại và chi phí để khắc phục cao hơn. Vì vậy hãy kiểm tra chương trình của mình thật cẩn thận trước khi chuyển nó cho khách hàng của bạn sử dụng. Bạn phải tự tin là chương trình tôi viết hoạt động đúng với độ chính xác như khách hang đã mong đợi.

Làm thế nào để tránh lỗi luận lý trong chương trình?

Bạn hãy cố gắng hết sức có thể để tránh lỗi luận lý trong chương trình mình viết. Để làm được việc này bạn phải tuân thủ ba bươc đầu tiên trong thiết kế phần mềm: Xác định yêu cầu, Phân tích yêu cầu và thiết kế giải thuật. Các bước tiếp theo của quy trình là  cũng phải rà soát và kiểm chứng xem rằng chương trình đang thực hiện có đang được phát triển đúng hướng hay không. Hãy thực hiện từng bước thật cẩn thận, chính xác trong từng dòng code bạn viết và chay thử nghiệm với tất cả các tình huống có thể xảy ra để đảm bảo tính đúng đắn của chương trình. Để làm được như vậy bạn phải nắm vững những kiến thức cơ bản nhất trong lập trình, đừng ngần ngại khi phải học lai một số điều cơ bản khi bạn có cảm giác mình bị thiếu sót trong quá khứ. Phần mềm là do con người tạo ra, nó không thể không có sai sót, cho nên bạn hãy không ngừng nổ lực phát hiện những sai sót nhưng hãy nhớ rằng nó không thể hoàn hảo trong tất cả mọi khía cạnh mà đôi lúc bạn phải chấp nhận một số sai sót không thể sữa chữa trong điều kiện nhất định hoặc tốn nhiều chi phí, công sức để làm cho nó hoàn hảo. Một việc quan trọng nữa là khách hàng của bạn nên biết và chấp nhận những sai sót này.
Tổ Bộ môn Cơ bản Trường CĐ Công Nghệ Thủ Đức
--

Bộ môn
chuyên ngành

200x200
Mạng
máy tính
200x200
công nghệ
phần mềm
200x200
Đồ họa
200x200
Tin học
cơ sở