[Học lập trình C++] Chương 1: 1.11 – Debugging chương trình của bạn

1.11 – Debugging chương trình của bạn
Lỗi cú pháp và lỗi ngữ nghĩa
Chương trình có thể khó khăn, và có nhiều cách để mắc sai lầm. Lỗi tạo ra rơi vào một trong hai trường hợp: Lỗi cú pháp và lỗi ngữ nghĩa.
Một lỗi cú pháp xuất hiện khi bạn viết một câu lệnh mà không hợp lệ theo ngữ pháp của ngôn ngữ C++. Điều này bao gồm các lỗi như quên dấu chấm phẩy, không khai báo biến, thiếu dấu ngoặc móc, hoặc ngoặc tròn, và không kết thúc chuỗi. Ví dụ, chương trình sau chứa đựng một vài lỗi cú pháp:
#include <iostream>; // preprocessor statements can't have a semicolon on the end

int main()
{
    std:cout < "Hi there; << x; // invalid operator (:), unterminated string (missing "), and undeclared variable
    return 0 // missing semicolon at end of statement
}

May mắn thay, trình biên dịch sẽ bắt được lỗi cú pháp và tạo ra một cảnh báo hay lỗi, vì vậy bạn sẽ dễ dàng xác định và sửa lỗi vấn đề. Sau đó chỉ cần biên dịch lại cho đến khi tìm ra được tất cả các lỗi.
Một khi chương trình của bạn biên dịch một cách đúng đắn, kết quả thu được vẫn có thể không đáng tin cậy. Một lỗi ngữ nghĩa xuất hiện khi một câu lệnh đúng cú pháp, nhưng không thực hiện những gì người lập trình mong đợi.
Thỉnh thoảng những lỗi này sẽ làm cho chương trình của bạn bị ngừng, giống như trường hợp chia cho số 0:
#include <iostream>

int main()
{
    int a = 10;
    int b = 0;
    std::cout << a << " / " << b << " = " << a / b; // division by 0 is undefined
    return 0;
}

Thỉnh thoảng những trường hợp tạo ra giá trị sai:
1
2
3
4
5
6
7
#include <iostream>

int main()
{
    std::cout << "Hello, word!"; // spelling error
    return 0;
}
or
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int add(int x, int y)
{
    return x - y; // function is supposed to add, but it doesn't
}

int main()
{
    std::cout << add(5, 3); // should produce 8, but produces 2
    return 0;
}

Thật không may, trình biên dịch không thể bắt được những lỗi dạng này, bởi vì trình biên dịch chỉ biết những gì bạn viết, không biết những gì bạn mong muốn.
Trong ví dụ phía trên, lỗi dễ dàng phát hiện. Nhưng trong những chương trình không bình thường thì nhiều lỗi ngữ nghĩa sẽ không dễ dàng tìm ra bằng mắt thường.
May mắn thay, trình gỡ rối sẽ giải quyết vấn đề đó.
Trình gỡ rối
Một trình gỡ rối là một chương trình máy tính mà cho phép người lập trình điều khiển chương trình thực thi và quan sát chuyện gì xảy khi trong khi chạy chương trình. Ví dụ, lập trình viên có thể dùng trình gỡ rối để thực thi chương trình theo từng dòng, kiểm tra các giá trị của các biến trong quá trình chạy. Bằng cách so sánh giá trị của biến thật với giá trị mong đợi, hoặc xem thứ tự thực thi trong code, có thể giúp rất nhiều trong việc phát hiện lỗi ngữ nghĩa.
Trình gỡ rối sớm nhất, giống như gdb, có một giao diện dòng lệnh nơi mà lập trình viên phải gõ lệnh trực tiếp để làm chúng hoạt động. Trình gỡ rối sau này (giống như trình gỡ rối tubo của Borland’s) mang đến một giao diện để hoạt động với nó một cách dễ dàng. Hầu hết những IDE có sẵn ngày nay đều có trình gỡ rối tích hợp – Đó là trình gỡ rối được xây dựng cùng với trình soạn thảo, vì vậy bạn có thể gỡ rối sử dụng môi trường giống như bạn dùng để viết chương trình của bạn (không cần phải chuyển đổi chương trình).
Tất cả trình gỡ rối hiện nay chứa đựng những tính năng chuẩn và cơ bản như nhau. Tuy nhiên, có một ít sự khác nhau về cách sắp xếp các menu, và phím tắt.
Stepping
Stepping là một tính năng của trình gỡ rối để thực thi từng dòng lệnh trong chương trình của bạn. Điều này cho phép bạn xem xét từng dòng code trong sự cô lập để xác định xem nó có hoạt động giống như mong muốn.
Thực ra có 3 lệnh stepping khác nhau: Step into, step over, and step out. Chúng ta sẽ tìm hiểu từng cái một.
Step into
Lệnh step into thực thi dòng lệnh kế tiếp. Nếu dòng này là một lời gọi hàm, step into đi vào hàm và trả điều khiển vào đỉnh của hàm.
Hãy quan sát một ví dụ đơn giản:

2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

void printValue(int nValue)
{
    std::cout << nValue;
}

int main()
{
    printValue(5);
    return 0;
}

#include <iostream>

void printValue(int nValue)
{
    std::cout << nValue;
}

int main()
{
    printValue(5);
    return 0;
}

Như bạn đã biết, khi chạy chương trình, thực thi sẽ bắt đầu với việc gọi hàm main(). Bởi vì chúng ta muốn gỡ rối bên trong hàm main(), hãy bắt đầu sử dụng lệnh step into.
Trong Visual Studio 2005 Express, vào menu debug và chọn Step into hoặc nhấn F11.
Nếu bạn đang sử dụng một IDE, tìm lệnh Step into trong menu và chọn nó. Khi bạn làm điều này, có hai thứ xảy ra. Đầu tiên, bởi vì ứng dụng của chúng ta là một chương trình hiển thị trên màn hình, một của sổ đầu ra console sẽ mở ra. Nó sẽ trống trơn bởi vì chúng ta chưa xuất ra gì cả. Thứ hai, bạn sẽ nhìn thấy một vài kiểu đánh dấu xuất hiện bên trái của hàm main(). Trong Visual 2005 Express, sự đánh dấu này là hàng màu vàng. Nếu bạn đang sử dụng một IDE khác, bạn sẽ nhìn thấy một cái gì đó phục vụ mục đích tương tự.
Đánh dấu hàng này cho thấy rằng dòng đang được trỏ sẽ được thực thi kế tiếp. Trong trường hợp này trình gỡ rối đang nói cho chúng ta rằng dòng kế tiếp sẽ được thực thi trong dấu ngoặc móc.
Điều này nghĩa là dòng kế tiếp sẽ được thực thi là gọi hàm printValue(). Chọn “Step into” lần nữa. Bởi vì hàm printValue là một lời gọi hàm, chúng ta “Step into” hàm, và mũi tên sẽ nhảy lên đỉnh của hàm printValue().
Chọn “Step into” Để thực thi dấu ngoặc nhọn mở của hàm printValue(). Tại điểm này, hàng sẽ chỉ đến std::cout << nValue;
Chọn “Step over” lần này (đây sẽ thực thi dòng code này mà không thực thi toán tử <<). Bởi vì câu lệnh cout đã được thực thi, bạn sẽ nhìn thấy giá trị 5 xuất hiện trong của sổ ngõ ra.
Chọn “Step into” lần nữa để thực thi dấu ngoặc nhọn đóng của printValue(). Tại điểm này printValue() đã thực hiện xong và quyền điều khiển được trả vè cho hàm main().
Bạn sẽ chú ý rằng mũi tên lại trỏ tới printValue()!
Trong khi bạn có thể nghĩ rằng trình gỡ rối định gọi là hàm printValue(), trong thực tế trình gỡ rối chỉ cho bạn biết rằng nó đang trở về từ lời gọi hàm.
Chọn “Step into” hai lần nữa. Tới điểm này, chúng đã thực thi tất cả các dòng lệnh của chương trình, vì thế chúng ta đã hoàn thành. Một vài trình gỡ rối sẽ kết thúc phần debugging một các tự động. Visual Studio thì không, vì vậy nếu bạn đang sử dụng Visual Studio, chọn “Stop debugging” từ debug menu. Đây kết thúc quá trình debugging.
Chú ý rằng “Stop debugging” có thể được sử dụng tại bất kì điểm nào trong quá trình debugging để kết thúc phần debugging.
Step over
Giống như “Step into”, lệnh “Step over” thực thi dòng lệnh tiếp theo của code. Nếu dòng lệnh là một lời gọi hàm, “Step over” thực thi tất cả code trong hàm và tra điều khiển đến bạn sau khi hàm đã được thực thi.
Chú ý đối với Code::Blocks, “Step over” được gọi là “Next line”.
Hãy xem một ví dụ của việc sử dụng này trong ví dụ giống như chương trình ở trên:
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

void printValue(int nValue)
{
    std::cout << nValue;
}

int main()
{
    printValue(5);
    return 0;
}

“Step into” chương trình cho đến khi câu lệnh tiếp theo được thực thi là gọi hàm printValue().
Thay vì thực hiện từng bước trong hàm printValue(), chọn “Step over” thay thế. Trình gỡ rối sẽ thực thi hàm (in ra giá trị 5 trong của sổ đầu ra) và sau đó trả điều khiển về cho dòng kế tiếp (return 0;)
Step over cung cấp một cách tiện lợi để bỏ qua hàm khi bạn đã chắc chắn rằng nó hoạt động hoặc không cần phải debug.
Step out
Không giống như hai lệnh stepping, “Step out” không chỉ thực thi dòng kế tiếp của code. Thay vào đó, nó thực thi tất cả các code còn lại trong hàm hiện tại của bạn, và trả điều khiển cho bạn khi hàm kết thúc.
Hãy quan sát một ví dụ của việc sử dụng trong nó trong chương trình sau đây.
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

void printValue(int nValue)
{
    std::cout << nValue;
}

int main()
{
    printValue(5);
    return 0;
}

“Step into” chương trình cho đến khi ở bên trong hàm prinValue().
Sau đó chọn “Step out”. Bạn sẽ chú ý giá trị 5 xuất hiện trên ngõ ra, và trình gỡ rối trả điều khiển cho bạn sau khi hàm kết thúc.
Run to cursor
Trong khi stepping hữu ích cho việc kiểm tra từng dòng code đơn, trong một chương trình lớn, nó có thể mất thời gian để chạy từng bước như vậy, bạn chỉ cần trỏ vào nơi mà bạn muốn kiểm tra chi tiết.
May mắn là các trình gỡ rối hiện nay cung cấp một vài công cụ để giúp chúng ta debug chương trình một cách hiệu quả.
Lệnh hữu ích đầu tiên thường được gọi là Run to cursor. Lệnh này thực thi chương trình giống như bình thường cho đến khi nó tới được dòng code mà bạn chọn. Sau đó nó trả điểu khiển về cho bạn để bạn có thể debug bắt đầu từ điểm đó.
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

void printValue(int nValue)
{
    std::cout << nValue;
}

int main()
{
    printValue(5);
    return 0;
}

Đơn giản đặt con trỏ của bạn tại dòng std::cout << nValue; bên trong hàm printValue(), sau đó phải chuột và chọn “Run to cursor.
Bạn sẽ chú ý rằng mũi tên chỉ đến dòng sẽ được thực thi kế tiếp dòng mà bạn chọn. Chương trình của bạn được thực thi đến điểm này và hiện giờ đang đợi cho lệnh debugging tiếp theo.
Run
Một khi bạn đang trong quá trình debuging một chương trình, bạn có thể nói với trình gỡ rối chạy cho đến hết chương trình (hoặc breakpoint kế tiếp, cái mà chúng ta sẽ thảo luận tiếp theo đây). Trong Visual Studio 2005 Express, lệnh này được gọi là Continue. Trong trình gỡ rối khác, nó có thể được gọi là “Run” hoặc “Go”.
Nếu bạn đã cùng theo dõi suốt ví dụ này, bạn sẽ ở trong hàm printValue(). Chọn lệnh run, và chương trình của bạn sẽ hoàn thành thực thi và sau đó kết thúc.
Breakpoints
Chủ đề cuối cùng chúng ta sẽ nó về breakpoint. Một breakpoint là một điểm đánh dấu đặc biệt nói với trình gỡ rối đặc biệt để ngừng việc thực thi của chương trình tại điểm breakpoint khi chạy trong chế độ debug.
Để thiết lập mọt breakpoint trong Visual Studio 2005 Express, tìm đến menu Debug và chọn “Toggle Breakpoint” (bạn có thể phải chuột, chọn breakpoint -> Insert Breakpoint). Bạn sẽ nhìn thấy một biểu tượng mới xuất hiện:
Tiếp tục và đặt một breakpoint trên dòng std::cout << nValue;
Bây giờ, chọn “Step into” để bắt đầu phần debugging, và sau đó “Continue” để trình gỡ rối chạy code của bạn, và hãy quan sát breakpoint hoạt động ra sao. Bạn sẽ chú ý rằng thay vì chạy tất cả mọi cách để kết thúc chương trình, trình gỡ rối đã ngừng tại breakpoint!
Breakpoints là một công cụ cực kì hữu ích nếu bạn muốn kiểm tra một phần đặc biệt của code. Đơn giản đặt một breakpoint tại định của phần code đó, thông báo với trình gỡ rối chạy, và trình gỡ rối sẽ tự động ngừng lại mỗi lần nó gặp phải breakpoint. Sau đó bạn có thể dùng lệnh stepping tại đó để xem chương trình chạy từng dòng một.
Một chý ý cuối cùng: Cho đến bây giờ, chúng ta đã sử dụng “Step into” để bắt đầu phần debugging. Tuy nhiên, nó có thể nói với trình gỡ rối chỉ chạy từ đầu cho tới kết thúc chương trình ngay lập tức. Trong Visual Studio 2005 Express, điều này được thực hiện bằng cách chọn “Start debugging” từ Debug menu. Trình gỡ rối khác sẽ có lệnh tương tự. Khi bạn sử dụng trong liên kết với breakpoints, có thể giảm thiểu số lượng lệnh bạn cần dùng để có được mục tiêu chi tiết hơn việc sử dụng lệnh stepping.
Kết luận

Chúc mừng bạn, bây giờ bạn đã biết những cách chính để cho trình gỡ rối thực hiện trên code của bạn. Tuy nhiên, đây chỉ là một nửa những lợi ích của debugger mà thôi. Trong bài học tiếp theo sẽ nói về làm thế nào để kiểm tra giá trị của các biến mà chúng ta đang debuging, cũng như một vài cửa sổ của thông tin thêm vào chúng ta có thể sử dụng để giúp debug code của chúng ta

Nhận xét

Bài đăng phổ biến