Programming In CUnit 129 min read

Debugging, Testing, and Header Files in C

Unit 12 of Programming In C covers debugging techniques (logical errors, syntax errors, runtime errors), testing strategies (unit testing, black-box vs white-box testing), and header files (standard vs user-defined, preprocessor directives include, define). It explains how to use assert.h, stdio.h, and custom headers,

TAKEAWAYS:

  • Header files (#include) provide reusable code libraries (e.g., stdio.h for I/O, math.h for sqrt()) and avoid code duplication.
  • Debugging identifies logical errors (incorrect logic), syntax errors (grammar mistakes), and runtime errors (crashes like division by zero).
  • Testing includes unit testing (testing individual functions), black-box testing (input/output validation), and white-box testing (checking internal logic).
  • Recursive functions (e.g., Fibonacci) must have a base case and recursive case; improper recursion causes stack overflow.
  • Tools like gcc -Wall, printf() debugging, and IDE debuggers (e.g., Code::Blocks) help trace errors.
  • Header files use preprocessor directives (#include, #define, #ifdef) to control compilation and avoid multiple inclusions.

Header Files: Roles and Usage

Header files (.h) are text files containing function declarations, macros, and global variables shared across multiple source files. They improve code reusability and modularity.

Types of Header Files

Type Example Files Purpose
Standard Library stdio.h, math.h Provide built-in functions (e.g., printf(), sin()).
User-Defined mylib.h Store custom functions/variables for reuse across programs.

How Header Files Work

  1. Preprocessor Directive: #include <stdio.h> or #include "mylib.h" inserts the header’s content into the source file.
  2. Guard Macros: Prevent multiple inclusions of the same header:
    #ifndef MYLIB_H
    #define MYLIB_H
    // Function declarations
    #endif
    
    Figure: Header Guard Mechanism
    flowchart TD
      A["Source File"] -->|"#include 'mylib.h'"| B["Preprocessor"]
      B --> C["Check MYLIB_H defined?"]
      C -->|"No"| D["Insert header content\nDefine MYLIB_H"]
      C -->|"Yes"| E["Skip insertion"]

Example: Custom Header File

File: mylib.h

#ifndef MYLIB_H
#define MYLIB_H

```figure
{"type":"network","nodes":["main.c","mylib.h","mylib.c","stdio.h"],"edges":[["main.c","mylib.h","include"],["main.c","stdio.h","include"],["mylib.c","mylib.h","include"]],"directed":true,"caption":"Dependency graph of the custom header file example"}

int add(int a, int b); float divide(float x, float y);

#endif

**File: `main.c`**
```c
#include <stdio.h>
#include "mylib.h"  // User-defined header

int main() {
    printf("Sum: %d\n", add(5, 3));
    return 0;
}

File: mylib.c (Implementation)

#include "mylib.h"

int add(int a, int b) { return a + b; }
float divide(float x, float y) { return x / y; }

Compile & Run:

gcc main.c mylib.c -o program
./program

Output:

Sum: 8

Debugging: Identifying and Fixing Errors

Debugging is the process of finding and fixing errors in code. Errors are classified as:

Error Type Description Example
Syntax Error Violates C grammar rules. Missing semicolon (;).
Runtime Error Occurs during execution (e.g., crashes). Division by zero (10/0).
Logical Error Program runs but produces wrong output. Incorrect loop condition.

Debugging Techniques

  1. Compiler Warnings: Use gcc -Wall to enable all warnings:
    gcc -Wall myprogram.c
    
    Example warning:
    warning: implicit declaration of function ‘add’ [-Wimplicit-function-declaration]
    
printf("x = %d\n", x);x = x + 5;printf("x after addition = %d\n", x);TOP
Print debugging stack for tracing variable `x`
  1. Print Debugging: Insert printf() statements to trace variable values:

    int x = 10;
    printf("x = %d\n", x);  // Debug output
    x = x + 5;
    printf("x after addition = %d\n", x);
    
  2. IDE Debuggers: Tools like Code::Blocks, VS Code, or GDB allow:

    • Step-by-step execution (breakpoints).
    • Variable inspection (watch variables).
    • Call stack analysis (see function calls).
  3. Assertions (assert.h): Check assumptions during development (disabled in release builds):

    #include <assert.h>
    int x = 5;
    assert(x > 0);  // Crashes if x <= 0
    

Testing Strategies

Testing ensures the program works as intended. Key methods:

stateDiagram-v2
    [*] --> test_add
    test_add --> assert1: add(2,3)==5
    assert1 --> assert2: add(-1,1)==0
    assert2 --> assert3: add(0,0)==0
    assert3 --> [*]
    state assert1 {
      note: Pass/Fail
    }
    state assert2 {
      note: Pass/Fail
    }
    state assert3 {
      note: Pass/Fail
    }

Unit testing flow for add() function

1. Unit Testing

Test individual functions in isolation. Example: Test the add() function from mylib.h.

#include "mylib.h"
#include <assert.h>

void test_add() {
    assert(add(2, 3) == 5);
    assert(add(-1, 1) == 0);
    assert(add(0, 0) == 0);
}

int main() {
    test_add();
    return 0;
}

2. Black-Box Testing

Test the program without knowing its internal logic. Focus on input/output pairs. Example: For a login() function, test:

  • Valid username/password → Success.
  • Empty input → Error.
  • Wrong password → Failure.

3. White-Box Testing

Test internal logic (e.g., loops, conditionals). Useful for critical functions like:

  • Bank transaction validation (check balance before deducting).
  • Pathao’s order routing (validate driver availability).

Recursive Functions

A recursive function calls itself to solve smaller instances of the same problem. Key Components:

  1. Base Case: Terminates recursion (e.g., fib(0) = 0).
  2. Recursive Case: Breaks the problem into smaller subproblems.

Example: Fibonacci Sequence

Problem: Generate the first 10 Fibonacci numbers (starting from 0). Recursive Solution:

#include <stdio.h>

```figure
{"type":"tree","nodes":[0,1,1,2,3,5,8,13,21,34],"highlight":[3,5,8],"caption":"First 10 Fibonacci numbers (recursive calls for fib(4) highlighted)"}

int fib(int n) { if (n <= 1) return n; // Base case return fib(n-1) + fib(n-2); // Recursive case }

int main() { for (int i = 0; i < 10; i++) { printf("%d ", fib(i)); } return 0; }

**Output**:

0 1 1 2 3 5 8 13 21 34

Trace Table for fib(4):

Call Stack fib(4) fib(3) fib(2) fib(1) fib(0)
Step 1 4
Step 2 fib(3) + fib(2) 3
Step 3 fib(2) + fib(1) 2
Step 4 fib(1) + fib(0) 1 0
Step 5 1 + 0 = 1
Result 2 + 1 = 3
Final 3 + 1 = 4

Warning: This recursive approach is inefficient (exponential time). Use memoization or iterative methods for large n.


In the Real World

  1. eSewa (Nepal):

    • Header Files: Uses modular code (e.g., payment.h, user_auth.h) to separate logic for payments and authentication.
    • Debugging: Tests edge cases like failed transactions or duplicate payments using unit tests.
  2. Khalti’s Loan Interest Calculation:

    • Recursion: Could theoretically model compound interest recursively (though iterative methods are preferred for performance).
    • Testing: White-box tests validate interest calculations for different tenures (e.g., 1 year vs. 5 years).
  3. Pathao’s Order Queue:

    • Data Structures + Debugging: Uses queues to manage orders. Debugging ensures no order is lost during peak hours (e.g., Dashain).
    • Header Files: Separates order.h (order processing) from driver.h (driver assignment).
  4. NTC’s Traffic Route Optimization:

    • Testing: Black-box tests simulate traffic scenarios (e.g., road closures) to validate route suggestions.
    • Debugging: Logs (printf()) track why a route fails (e.g., high congestion).

Exam Tip

  1. Header Files:

    • Always explain the purpose of #include, #define, and guard macros (#ifndef).
    • Common Exam Question: Write a custom header file for a given function (e.g., factorial()).
    • Marks Tip: Show both declaration (.h) and definition (.c) files.
  2. Debugging:

    • Differentiate between syntax, runtime, and logical errors.
    • Practical Tip: For logical errors, use printf() to trace variables (e.g., loop counters).
  3. Testing:

    • For unit testing, write assertions or test cases (e.g., test_add()).
    • For black-box testing, list input-output pairs (e.g., login("user", "pass") → Success).
  4. Recursion:

    • Must Include:
      • Base case.
      • Recursive case.
      • Example trace (like the fib(4) table above).
    • Avoid: Infinite recursion (forgetting the base case).
  5. Code Structure:

    • Separate declarations (headers) from definitions (source files).
    • Use #include <stdio.h> for standard libraries and #include "mylib.h" for user-defined headers.

fibonacci sequence spiralVisual representation of Fibonacci numbers in nature (e.g., pinecones). (Image: Raymondprucher, CC BY-SA 4.0, via Wikimedia Commons)

Based on the PU BE Computer (PU) syllabus for Programming In C, unit 12.

Discussion

Loading…