Trong bài học trước, chúng ta đã khám phá một chiến lược tìm kiếm các vấn đề bằng cách chạy các chương trình của chúng ta và sử dụng phỏng đoán để giải quyết vấn đề. Trong bài học này, chúng ta sẽ khám phá một số chiến thuật cơ bản để thực sự đoán ra và thu thập thông tin để giúp tìm ra vấn đề.

1. Chiến thuật gỡ lỗi # 1: Comment code của bạn

Hãy bắt đầu với một điều dễ dàng. Nếu chương trình của bạn thể hiện hành vi sai lầm, một cách để giảm số lượng code bạn phải tìm kiếm là comment một số code và xem vấn đề còn tồn tại không. Nếu vấn đề vẫn còn, code comment sẽ không gây ra lỗi.

Hãy xem xét các code sau đây:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

int main()
{
    getNames(); // ask user to enter a bunch of names
    doMaintenance(); // do some random stuff
    sortNames(); // sort them in alphabetical order
    printNames(); // print the sorted list of names
 
    return 0;
}

Giả sử chương trình này được cho là in tên người dùng nhập theo thứ tự bảng chữ cái, nhưng chương trình này in chúng theo thứ tự bảng chữ cái ngược. Vấn đề ở đâu? Là getNames nhập tên không chính xác? Là sortNames sắp xếp chúng ngược? Là printNames in chúng ngược? Nó có thể là bất cứ thứ gì trong số đó. Nhưng chúng ta có thể nghi ngờ doMaintenance() không liên quan đến vấn đề này, vì vậy hãy comment nó lại.

int main()
{
    getNames(); // ask user to enter a bunch of names
//    doMaintenance(); // do some random stuff
    sortNames(); // sort them in alphabetical order
    printNames(); // print the sorted list of names
 
    return 0;
}

Nếu vấn đề biến mất, thì doMaintenance phải gây ra vấn đề và chúng ta nên tập trung chú ý vào đó.

Tuy nhiên, nếu sự cố vẫn còn (có nhiều khả năng), thì chúng ta biết doMaintenance không có lỗi và chúng ta có thể loại trừ toàn bộ hàm khỏi tìm kiếm của chúng ta. Điều này không giúp chúng ta hiểu được vấn đề thực sự là trước hay sau cuộc gọi đến doMaintenance , nhưng nó làm giảm số lượng code mà chúng ta phải xem qua sau đó.

Đừng quên những hàm bạn đã comment để bạn có thể bỏ qua chúng sau này!

2. Chiến thuật gỡ lỗi # 2: Xác thực luồng code của bạn

Một vấn đề phổ biến khác trong các chương trình phức tạp hơn là chương trình đang gọi một hàm quá nhiều hoặc quá ít lần (bao gồm cả không).

Trong các trường hợp như vậy, có thể hữu ích khi đặt các câu lệnh ở đầu các hàm của bạn để in tên của hàm. Theo cách đó, khi chương trình chạy, bạn có thể thấy các hàm nào đang được gọi.

Mẹo

Khi in thông tin cho mục đích gỡ lỗi, hãy sử dụng std :: cerr thay vì std :: cout. Một lý do cho điều này là std :: cout có thể được đệm, điều đó có nghĩa là có thể có một khoảng dừng giữa khi bạn hỏi std :: cout để xuất thông tin và khi nào nó thực sự xảy ra. Nếu bạn xuất bằng std :: cout và sau đó chương trình của bạn gặp sự cố ngay lập tức, std :: cout có thể có hoặc chưa thực sự xuất ra. Điều này có thể đánh lừa bạn về vấn đề là ở đâu. Mặt khác, std :: cerr không có bộ đệm, có nghĩa là bất cứ điều gì bạn gửi đến nó sẽ xuất ra ngay lập tức. Điều này giúp đảm bảo tất cả đầu ra gỡ lỗi xuất hiện càng sớm càng tốt (với chi phí của một số hiệu suất, điều mà chúng ta thường không quan tâm khi gỡ lỗi).

Hãy xem xét chương trình đơn giản sau đây không hoạt động chính xác:

#include <iostream>
 
int getValue()
{
	return 4;
}
 
int main()
{
    std::cout << getValue;
 
    return 0;
}

Mặc dù chúng ta hy vọng chương trình này sẽ in giá trị 4 , nhưng nó thực sự sẽ in các giá trị khác nhau trên các máy khác nhau. Trên máy của tác giả, nó được in:

00101424

Hãy thêm một số câu lệnh gỡ lỗi vào các hàm này:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
 
int getValue()
{
std::cerr << "getValue() called\n";
	return 4;
}
 
int main()
{
std::cerr << "main() called\n";
    std::cout << getValue;
 
    return 0;
}

Mẹo

Khi thêm các câu lệnh gỡ lỗi tạm thời, có thể hữu ích để không thụt lề chúng. Điều này làm cho chúng dễ dàng hơn để tìm cách loại bỏ sau này.

Bây giờ khi các hàm này thực thi, chúng sẽ xuất tên của chúng, biểu thị rằng chúng được gọi:

main() called
00101424

Bây giờ chúng ta có thể thấy rằng hàm getValue không bao giờ được gọi. Phải có một số vấn đề với code gọi hàm. Chúng ta hãy xem xét kỹ hơn về dòng đó:

std::cout << getValue;

Ôi, nhìn kìa, chúng ta đã quên dấu ngoặc đơn trong lệnh gọi hàm. Nó nên là:

#include <iostream>
 
int getValue()
{
std::cerr << "getValue() called\n";
	return 4;
}
 
int main()
{
std::cerr << "main() called\n";
    std::cout << getValue(); // added parenthesis here
 
    return 0;
}

Điều này bây giờ sẽ tạo ra đầu ra chính xác

main() called
getValue() called
4

Và chúng ta có thể loại bỏ các câu lệnh gỡ lỗi tạm thời.

3. Chiến thuật gỡ lỗi # 3: In các giá trị

Với một số loại lỗi, chương trình có thể tính toán hoặc chuyển sai giá trị.

Chúng ta cũng có thể xuất giá trị của các biến (bao gồm các tham số) hoặc biểu thức để đảm bảo rằng chúng là chính xác.

Hãy xem xét chương trình sau đây được cho là thêm hai số nhưng không hoạt động chính xác:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
 
int add(int x, int y)
{
	return x + y;
}
 
void printResult(int z)
{
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return x;
}
 
int main()
{
	int x{ getUserInput() };
	int y{ getUserInput() };
 
	std::cout << x << " + " << y << '\n';
 
	int z{ add(x, 5) };
	printResult(z);
 
	return 0;
}

Đây là một số đầu ra từ chương trình này:

Enter a number: 4
Enter a number: 3
4 + 3
The answer is: 9

Điều đó không đúng. Bạn có thấy lỗi không? Ngay cả trong chương trình ngắn này, có thể khó phát hiện ra. Hãy thêm một số code để gỡ lỗi các giá trị của chúng ta:

#include <iostream>
 
int add(int x, int y)
{
	return x + y;
}
 
void printResult(int z)
{
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return x;
}
 
int main()
{
	int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
	int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
 
	std::cout << x << " + " << y << '\n';
 
	int z{ add(x, 5) };
std::cerr << "main::z = " << z << '\n';
	printResult(z);
 
	return 0;
}

Đây là đầu ra trên:

Enter a number: 4
main::x = 4
Enter a number: 3
main::y = 3
4 + 3
main::z = 9
The answer is: 9

Các biến xy đang nhận được các giá trị đúng, nhưng biến z thì không. Vấn đề phải nằm giữa hai điểm đó, điều này làm cho hàm add là một nghi ngờ chính.

Hãy sửa đổi chức năng thêm:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
 
int add(int x, int y)
{
std::cerr << "add() called (x=" << x <<", y=" << y << ")" << '\n';
	return x + y;
}
 
void printResult(int z)
{
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return x;
}
 
int main()
{
	int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
	int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
 
	std::cout << x << " + " << y << '\n';
 
	int z{ add(x, 5) };
std::cerr << "main::z = " << z << '\n';
	printResult(z);
 
	return 0;
}

Bây giờ chúng ta sẽ nhận được đầu ra:

Enter a number: 4
main::x = 4
Enter a number: 3
main::y = 3
add() called (x=4, y=5)
main::z = 9
The answer is: 9

Biến y có giá trị 3, nhưng bằng cách nào đó, hàm add của chúng ta có giá trị 5 cho tham số y. Chúng ta phải thông qua lập luận sai. Đảm bảo đủ:

 int z{ add(x, 5) };

Nó đây rồi Chúng ta đã lấy 5 thay vì giá trị của biến y làm đối số. Đó là một sửa chữa dễ dàng, và sau đó chúng ta có thể loại bỏ các câu lệnh gỡ lỗi.

4. Thêm một ví dụ nữa

Chương trình này rất giống với chương trình trước, nhưng cũng không hoạt động như nó nên:

#include <iostream>
 
int add(int x, int y)
{
	return x + y;
}
 
void printResult(int z)
{
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return --x;
}
 
int main()
{
	int x{ getUserInput() };
	int y{ getUserInput() };
 
	int z { add(x, y) };
	printResult(z);
 
	return 0;
}

Nếu chúng ta chạy code này và xem như sau:

Enter a number: 4
Enter a number: 3
The answer is: 5

Hừm, có gì đó không ổn. Nhưng ở đâu?

Hãy sử dụng code này với một số gỡ lỗi:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
 
int add(int x, int y)
{
std::cerr << "add() called (x=" << x << ", y=" << y << ")\n";
	return x + y;
}
 
void printResult(int z)
{
std::cerr << "printResult() called (z=" << z << ")\n";
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
std::cerr << "getUserInput() called\n";
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return --x;
}
 
int main()
{
std::cerr << "main() called\n";
	int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
	int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
 
	int z{ add(x, y) };
std::cerr << "main::z = " << z << '\n';
	printResult(z);
 
	return 0;
}

Bây giờ hãy chạy lại chương trình với cùng các đầu vào:

main() called
getUserInput() called
Enter a number: 4
main::x = 3
getUserInput() called
Enter a number: 3
main::y = 2
add() called (x=3, y=2)
main::z = 5
printResult() called (z=5)
The answer is: 5

Bây giờ chúng ta có thể thấy ngay một cái gì đó không ổn: Người dùng đang nhập giá trị 4 , nhưng x chính đang nhận giá trị 3 . Một cái gì đó sai giữa nơi người dùng nhập dữ liệu vào và nơi giá trị đó được gán cho biến x chính. Hãy chắc chắn rằng chương trình đang nhận được giá trị chính xác từ người dùng bằng cách thêm một số mã gỡ lỗi vào hàm getUserInput :

#include <iostream>
 
int add(int x, int y)
{
std::cerr << "add() called (x=" << x << ", y=" << y << ")\n";
	return x + y;
}
 
void printResult(int z)
{
std::cerr << "printResult() called (z=" << z << ")\n";
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
std::cerr << "getUserInput() called\n";
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
std::cerr << "getUserInput::x = " << x << '\n'; // added this additional line of debugging
	return --x;
}
 
int main()
{
std::cerr << "main() called\n";
	int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
	int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
 
	int z{ add(x, y) };
std::cerr << "main::z = " << z << '\n';
	printResult(z);
 
	return 0;
}

Và đầu ra:

main() called
getUserInput() called
Enter a number: 4
getUserInput::x = 4
main::x = 3
getUserInput() called
Enter a number: 3
getUserInput::x = 3
main::y = 2
add() called (x=3, y=2)
main::z = 5
printResult() called (z=5)
The answer is: 5

Với dòng gỡ lỗi bổ sung này, chúng ta có thể thấy rằng đầu vào của người dùng được nhận chính xác vào biến x của getUserInput. Tuy nhiên, bằng cách nào đó, biến x chính đang nhận giá trị sai. Vấn đề phải nằm giữa hai điểm đó. Thủ phạm duy nhất còn lại là giá trị trả về từ hàm getUserInput . Hãy nhìn vào dòng đó kỹ hơn.

 return --x;

Hmmm, thật kỳ lạ. --Biểu tượng đó là gì trước x? Chúng ta chưa đề cập đến điều đó trong các hướng dẫn này, vì vậy đừng lo lắng nếu bạn không biết ý nghĩa của nó. Nhưng ngay cả khi không biết ý nghĩa của nó, thông qua các nỗ lực sửa lỗi của bạn, bạn có thể chắc chắn chắc chắn rằng dòng cụ thể này có lỗi – và do đó, có khả năng --biểu tượng này đang gây ra sự cố.

Vì chúng ta thực sự muốn getUserInput chỉ trả về giá trị của x , hãy xóa --và xem điều gì sẽ xảy ra:

#include <iostream>
 
int add(int x, int y)
{
std::cerr << "add() called (x=" << x << ", y=" << y << ")\n";
	return x + y;
}
 
void printResult(int z)
{
std::cerr << "printResult() called (z=" << z << ")\n";
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
std::cerr << "getUserInput() called\n";
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
std::cerr << "getUserInput::x = " << x << '\n';
	return x; // removed -- before x
}
 
int main()
{
std::cerr << "main() called\n";
	int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
	int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
 
	int z{ add(x, y) };
std::cerr << "main::z = " << z << '\n';
	printResult(z);
 
	return 0;
}

Và bây giờ là đầu ra:

main() called
getUserInput() called
Enter a number: 4
getUserInput::x = 4
main::x = 4
getUserInput() called
Enter a number: 3
getUserInput::x = 3
main::y = 3
add() called (x=4, y=3)
main::z = 7
printResult() called (z=7)
The answer is: 7

Chương trình hiện đang hoạt động chính xác. Ngay cả khi không hiểu những gì --đang làm, chúng ta có thể xác định dòng code cụ thể gây ra sự cố và sau đó khắc phục sự cố.

5. Tại sao sử dụng in lỗi để gỡ lỗi không tốt

Mặc dù việc thêm các câu lệnh gỡ lỗi vào các chương trình cho mục đích chẩn đoán là một kỹ thuật thô sơ phổ biến và là một hàm (đặc biệt là khi một trình gỡ lỗi không có sẵn vì một số lý do), nó không tuyệt vời vì nhiều lý do:

  1. Đoạn in gỡ lỗi làm lộn xộn code của bạn.
  2. Các câu lệnh gỡ lỗi làm lộn xộn đầu ra của chương trình của bạn.
  3. Các câu lệnh gỡ lỗi phải được xóa sau khi bạn hoàn thành chúng, điều này làm cho chúng không thể tái sử dụng.
  4. Các câu lệnh gỡ lỗi yêu cầu sửa đổi code của bạn để thêm và xóa, điều này có thể giới thiệu các lỗi mới.

Chúng ta có thể làm tốt hơn. Chúng ta sẽ khám phá làm thế nào trong các bài học trong tương lai.

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!