C Programming

Chapter 15
Capstone C Software Project

Bachelors Program in Computer Science Engineering
C Programming   •   Post Graduate

Capstone C Software Project

Introduction

A capstone project is an integrative software-development activity in which a learner applies programming language knowledge, software-engineering principles, data structures, testing methods, documentation practices, and professional judgement to construct a usable system. In C programming, the capstone is especially valuable because C exposes the relationship between source code, memory, data representation, compilation, operating-system services, and hardware-oriented execution.

At postgraduate level, the objective is not merely to produce a program that works for a few sample inputs. A credible capstone must demonstrate problem formulation, requirements analysis, architectural reasoning, modular design, defensive programming, systematic testing, performance awareness, maintainability, and ethical responsibility. The completed software should be explainable, reproducible, testable, and defensible under review.

A suitable project may be a command-line inventory system, a log analyser, a text-indexing engine, a student-record manager, a file-compression utility, a sensor-data processor, or a network-oriented systems tool. The particular application is less important than the quality of the engineering process and the justification of design decisions.

Learning Objectives

After completing this chapter, the learner should be able to:

  1. Transform an application problem into precise functional and non-functional requirements.
  2. Design a modular C program using appropriate data structures and abstract interfaces.
  3. Manage dynamic memory, files, errors, and resource ownership safely.
  4. Organise compilation using header files, separate translation units, and a build system.
  5. Apply testing, debugging, static analysis, and performance evaluation systematically.
  6. Assess maintainability, portability, security, usability, and ethical implications.
  7. Produce professional project documentation and critically evaluate the finished system.

1. From Problem Statement to Engineering Specification

A weak project begins with a vague goal such as “create a library system.” A stronger project defines users, operations, constraints, data assumptions, failure conditions, and measurable outcomes. The first artefact should therefore be a software requirements specification.

Functional requirements

Functional requirements describe what the system does. For a command-line inventory system they might include:

  • import product records from a delimited file;
  • add, update, and remove an item;
  • search by identifier or name;
  • calculate the total value of stock;
  • export a report;
  • reject malformed or duplicate records.

Non-functional requirements

Non-functional requirements describe qualities and constraints. Examples include:

  • correctness: every accepted transaction preserves inventory consistency;
  • performance: lookup of an identifier should remain responsive for a specified dataset size;
  • portability: the program should compile with a conforming C compiler on Linux and Windows;
  • reliability: an interrupted write must not silently destroy the existing database;
  • security: input must not cause buffer overflow, uncontrolled resource use, or path traversal;
  • maintainability: modules should have clear interfaces and limited coupling.

Requirements should be testable. “The interface must be easy to use” is weak; “an operator can complete an item search using at most three prompts” is more measurable, although usability still requires qualitative evaluation.

Scope and acceptance criteria

Scope limits are essential. A postgraduate project should identify explicitly what is excluded. Acceptance criteria connect requirements to evidence. For example, “the import function rejects a record with a missing price and reports its line number” can be verified by a test case and a captured result.

A requirements traceability matrix is useful:

RequirementDesign componentTest evidenceStatus
Import valid recordsparser.ctest_import_validPlanned
Reject missing priceparser.ctest_import_missing_pricePlanned
Search by identifierinventory.ctest_search_idPlanned
Preserve data after failed writestorage.cfault-injection testPlanned

2. Architectural Design of a C Capstone

Architecture describes the major components and their dependencies. A robust small C system commonly separates presentation, application logic, domain data, persistence, and infrastructure.

Layered architecture for a C capstone project User interface Application services Domain model Persistence Utilities and OS services
Layered architecture for a C capstone project
Figure: Layered architecture for a C capstone project

Figure: A layered architecture in which higher layers depend on lower-level services through controlled interfaces.

The arrows represent dependency, not necessarily data movement. The user-interface module should not directly manipulate file formats or internal linked-list nodes. Instead, it calls application services, which use domain operations and persistence interfaces. This separation makes testing easier and permits a text interface to be replaced later by another interface.

Modules and interfaces

A module should hide implementation details behind a header file. For example, inventory.h may expose operations such as:

typedef struct Inventory Inventory;
typedef struct {
    unsigned id;
    char name[64];
    unsigned quantity;
    double unit_price;
} Item;

Inventory *inventory_create(void);
void inventory_destroy(Inventory *inv);
int inventory_add(Inventory *inv, const Item *item);
const Item *inventory_find(const Inventory *inv, unsigned id);

The incomplete type struct Inventory prevents clients from depending on the internal representation. The implementation may use an array, linked list, hash table, or tree without changing callers. This is an example of information hiding and representation independence.

Choosing data structures

The choice must follow workload and constraints rather than fashion. A contiguous dynamic array often offers good cache locality and simple iteration. A linked list permits inexpensive insertion at known positions but has poor locality and linear search. A hash table can provide expected constant-time lookup under suitable assumptions, but requires collision handling and careful resizing. A balanced tree provides ordered operations with logarithmic height.

For an inventory with n items, typical asymptotic costs are:

OperationDynamic array, unsortedSorted arrayHash table, expectedBalanced tree
Search by keyO(n)O(log n)O(1)O(log n)
InsertO(1) amortised at endO(n)O(1) expectedO(log n)
Ordered traversalO(n)O(n)Not naturally orderedO(n)

These are asymptotic descriptions, not performance guarantees for every input. Hash-table bounds assume an effective hash function, controlled load factor, and an appropriate collision strategy.

3. C-Specific Engineering Concerns

Ownership and lifetime

Every dynamically allocated object requires a clear owner and a defined destruction path. A practical ownership policy answers three questions: who allocates the object, who may modify it, and who releases it? Ambiguous ownership produces leaks, double frees, and use-after-free errors.

A function that returns allocated memory should document whether the caller owns the result. A function accepting a pointer should specify whether it merely borrows the pointer or assumes ownership. Const qualification communicates non-modification but does not itself establish lifetime.

A useful review technique is to draw an ownership table:

ResourceAcquired byReleased byFailure concern
Inventory objectinventory_createcaller via inventory_destroypartial allocation
File handlestorage modulesame storage moduleearly-return leak
Input bufferparserparser or caller by contractstale pointer

Error handling

C does not provide exceptions, so errors must be represented explicitly. Common approaches include return codes, nullable pointers, output parameters, and errno for selected library failures. A function should distinguish invalid input, unavailable resources, corrupted data, and internal invariant violations where the caller can respond differently.

Error handling must occur at the point where recovery is possible. A low-level parser can report a malformed field; the user-interface layer can then print a useful message containing the record number. Messages should not expose sensitive information such as passwords or private file paths.

Input validation

Never assume that input is correctly sized or formatted. fgets is generally preferable to unbounded input functions because it accepts a buffer size. Numeric conversion should check both syntax and range. For example, strtoul provides an end pointer and allows the programmer to detect trailing characters, overflow, and an empty conversion.

Validation should include:

  • length limits;
  • character and encoding assumptions;
  • numeric range checks;
  • duplicate-key checks;
  • file-size and record-count limits;
  • rejection of unexpected delimiters or embedded control characters.

Files and persistence

Text formats are readable and convenient for small systems, but delimiters, quoting, locale, floating-point representation, and partial writes create complications. Binary formats can be compact but may be non-portable because of padding, byte order, type size, and representation differences. If a binary format is used, define its layout explicitly rather than writing arbitrary structures directly with fwrite.

A safer update strategy is to write a complete new file to a temporary path, flush and close it successfully, and then replace the old file using an operating-system operation with documented semantics. The exact durability guarantees depend on the operating system and filesystem; closing a stream alone does not always guarantee physical persistence after power loss.

4. Build, Portability, and Reproducibility

A project should compile from a clean directory using documented commands. Separate compilation commonly follows this pattern:

gcc -std=c17 -Wall -Wextra -Wpedantic -g -c main.c inventory.c storage.c parser.c
gcc main.o inventory.o storage.o parser.o -o inventory_app

The exact compiler and flags may vary, but warnings should be treated as design feedback. A Makefile or equivalent build script should express dependencies so that only affected files are rebuilt. Header guards or #pragma once prevent repeated inclusion, while careful header design reduces compile-time coupling.

Portability requires attention to implementation-defined and undefined behaviour. Do not assume that int has a particular width, that a pointer fits into an integer, that text files use one newline convention, or that structure layout is identical across machines. Use fixed-width integer types such as uint32_t only when the required width is genuinely part of the design, and check conversion and formatting rules appropriately.

Undefined behaviour is not merely an unusual result; the C standard imposes no requirements on the outcome. Out-of-bounds access, use of an uninitialised value, signed integer overflow, invalid shifts, and use-after-free must therefore be eliminated rather than tested around.

5. Verification, Testing, and Quality Assurance

Testing should be derived from requirements and risk. Unit tests isolate modules; integration tests examine interactions; system tests exercise complete workflows; acceptance tests determine whether stated requirements are met. Regression tests preserve previously corrected behaviour.

Boundary-value analysis is particularly important in C. Test empty files, maximum-length names, zero quantities, maximum representable values accepted by the specification, duplicate identifiers, missing delimiters, malformed numbers, inaccessible files, and allocation failures where practical.

A test oracle defines the expected result. For a total inventory value, if each item has quantity qᵢ and unit price pᵢ, the total is:

V = ∑ᵢ₌₁ⁿ qᵢpᵢ

Here, V is the total stock value, n is the number of items, qᵢ is the quantity of item i, and pᵢ is its unit price. The formula assumes that quantities and prices are valid and that the chosen numeric representation provides sufficient range and precision. For financial applications, binary floating-point values may introduce rounding error; scaled integer units, such as cents, are often preferable when the currency precision is fixed.

Dynamic analysis and static analysis

Compiler warnings identify suspicious constructs but cannot prove correctness. Static analysers can detect paths involving null dereferences, leaks, and incompatible types. Dynamic tools can detect memory errors during execution. Sanitizer-supported builds, for example, can help expose addressability and undefined-behaviour problems. These tools complement—not replace—reasoned review and tests.

Performance evaluation

Performance claims require a defined workload, environment, and measurement method. Record dataset size, compiler version, optimisation level, machine characteristics, and repeated-run procedure. Distinguish elapsed time, CPU time, memory consumption, and throughput. A single timing is weak evidence because operating-system scheduling, caching, and background activity influence results.

6. Worked Design Example: An Inventory Management System

Consider an illustrative project for a small laboratory that maintains equipment and consumable supplies.

Background and problem

The laboratory currently stores records in spreadsheets. Duplicate identifiers occur, outdated quantities are used in procurement decisions, and monthly reports require manual consolidation. The proposed C application must operate offline on modest computers and store data in a controlled local file.

Analysis

The principal entities are Item and Inventory. An item has an identifier, name, quantity, unit price, and optional category. The system must support import, search, update, deletion, reporting, and persistent storage. The system does not initially include multi-user access, network synchronisation, or online payment.

The expected workload is approximately 50,000 items, with frequent identifier searches and occasional ordered reports. A hash table is therefore a reasonable internal representation for identifier lookup, while a separate traversal or sorted reporting operation can support ordered output. The design should not expose the hash table to the interface layer.

Approach and solution

The project is divided into these modules:

  • ui.c: command parsing, prompts, and presentation;
  • inventory.c: item operations and invariants;
  • hashmap.c: key-to-item indexing;
  • storage.c: file loading, validation, and atomic replacement strategy;
  • report.c: totals and formatted reports;
  • error.c: error codes and diagnostic formatting;
  • tests/: unit and integration tests.

An invariant states that every stored identifier is unique and that quantity and price satisfy the specified non-negative ranges. The inventory_add operation checks the identifier before insertion. The persistence layer validates every record before it becomes visible to the domain layer. A failed import leaves the existing inventory unchanged by loading into a temporary structure and committing only after all records pass validation.

Suppose three accepted records are:

ItemQuantityUnit price
A1012.50
B480.00
C253.20

Using the stock-value formula, the result is 10 × 12.50 + 4 × 80.00 + 25 × 3.20 = 125.00 + 320.00 + 80.00 = 525.00 currency units. If prices are represented as integer cents, the calculation uses 1,250, 8,000, and 320 cents respectively, producing 52,500 cents and avoiding binary floating-point rounding for these values.

Outcome evaluation

A successful prototype is not judged solely by its menu. Evidence should include a requirements matrix, architecture diagram, source repository, build instructions, test report, memory-analysis results, sample data, and a limitations statement. The team should demonstrate that malformed records are rejected without corrupting existing data and that all allocated resources are released on normal and error paths.

Learning points

The case illustrates that data structure selection follows workload; persistence is a correctness concern rather than an afterthought; module boundaries reduce change impact; and test evidence must address failure paths as seriously as normal paths.

7. Project Management and Professional Practice

A capstone benefits from incremental development. A useful sequence is: requirements baseline, minimal vertical prototype, domain module, persistence, user interface, testing, hardening, and documentation. Each increment should remain buildable. Version control commits should describe coherent changes, and branches or review mechanisms should be used for risky modifications.

Project evaluation can combine correctness, design quality, testing depth, security, documentation, usability, and reflection. A technically sophisticated project with poor reproducibility is professionally weak. Conversely, a modest project with explicit assumptions, strong tests, and careful failure handling can demonstrate advanced engineering maturity.

Team projects require defined responsibilities, review records, and a mechanism for resolving design disagreements. Attribution should be accurate. Reusing external code requires compliance with its licence, preservation of notices where required, and verification that its security and maintenance status are acceptable.

8. Security, Ethics, and Governance

C programs are frequently used in embedded, operating-system, infrastructure, and security-sensitive contexts. Buffer overflows, format-string vulnerabilities, integer conversion errors, path traversal, denial-of-service through unbounded input, and information leaks are relevant risks even in educational projects.

Security should be treated as a lifecycle concern. Threat modelling identifies assets, entry points, trust boundaries, misuse cases, and mitigations. Least privilege limits damage if a process is compromised. Sensitive data should not be logged unnecessarily. Test data should be anonymised when it represents people or organisations.

Ethically, developers are responsible for honest performance claims, disclosure of known limitations, respect for intellectual property, and consideration of accessibility. If a system influences resource allocation or records personal information, the team should document data retention, access control, and correction procedures. Compliance requirements depend on jurisdiction and application domain; a capstone should not claim legal compliance without a relevant review.

9. Contemporary Developments and Critical Perspective

Modern C development increasingly combines traditional compilation with automated quality gates: continuous integration, reproducible builds, sanitizers, static analysis, fuzz testing, and dependency scanning. Fuzzing is particularly useful for parsers because it generates numerous unusual input sequences that humans may not anticipate.

AI-assisted programming can help generate test ideas, explain compiler diagnostics, or propose alternative implementations. However, generated code may contain memory errors, licence concerns, insecure assumptions, or incorrect APIs. The developer remains responsible for review, testing, attribution, and acceptance. AI should support engineering judgement, not replace requirements analysis or verification.

C remains important because it offers predictable low-level control, broad platform support, and a substantial systems ecosystem. Its cost is that safety properties are not automatically enforced. A postgraduate capstone should therefore treat explicit contracts, careful interfaces, automated analysis, and disciplined review as essential compensating practices.

Chapter Summary

A capstone C software project integrates programming, data structures, software architecture, memory management, file handling, testing, performance evaluation, documentation, and professional ethics. A successful project begins with testable requirements and controlled scope. Layered architecture and opaque module interfaces reduce coupling and enable independent testing. Data structures must be selected according to workload and operational requirements.

C-specific risks include undefined behaviour, memory leaks, invalid lifetime management, unsafe input, numeric errors, and non-portable assumptions. These risks are addressed through ownership contracts, validation, explicit error handling, compiler warnings, static analysis, sanitizers, boundary testing, and careful persistence design. Professional quality also requires reproducible builds, version control, transparent limitations, licence awareness, security analysis, and honest reporting. The capstone is therefore both a software artefact and an evidence-based demonstration of engineering judgement.

Key Terms

Requirement: A documented capability or constraint that the system must satisfy.

Acceptance criterion: A condition used to determine whether a requirement has been met.

Architecture: The high-level organisation of components and their dependencies.

Module: A cohesive implementation unit with a defined interface.

Information hiding: Concealing implementation details behind an abstraction boundary.

Invariant: A condition that must remain true during valid program execution.

Ownership: Responsibility for the lifetime and release of a resource.

Undefined behaviour: Execution for which the C standard imposes no requirements.

Defensive programming: Designing to detect, reject, or safely handle invalid conditions.

Translation unit: A source file after preprocessing, compiled as one unit.

Linkage: The rules determining how identifiers refer to entities across translation units.

Persistence: Retaining data beyond the execution of a program.

Regression test: A test that detects reintroduction of a previously corrected defect.

Static analysis: Examination of source or compiled code without executing the program.

Dynamic analysis: Analysis performed while the program executes.

Fuzz testing: Automated testing with numerous generated or mutated inputs.

Portability: The ability to build and run correctly across environments.

Traceability matrix: A mapping between requirements, design elements, and evidence.

Threat model: A structured description of assets, threats, trust boundaries, and mitigations.

Review Questions

Short Answer Questions

  1. Why should a capstone project define non-functional requirements in addition to functional requirements?
  2. What is the purpose of using an incomplete structure type in a C header file?
  3. Distinguish a memory leak from a use-after-free error.
  4. Why are compiler warnings insufficient as the only quality-assurance technique?
  5. State two reasons why directly writing a C structure to a binary file may reduce portability.

Long Answer Questions

  1. Explain how requirements, architecture, module interfaces, and tests should be connected in a capstone project.
  2. Compare dynamic arrays, linked lists, hash tables, and balanced trees for an identifier-based inventory application.
  3. Describe a robust strategy for loading and updating a persistent data file without exposing partially validated data.
  4. Discuss the major sources of undefined behaviour in C and propose a verification strategy for detecting them.
  5. Explain how security, licensing, privacy, and professional honesty should influence the design and reporting of a capstone project.

Analytical / Application Questions

  1. An application must process 1,000,000 records, perform frequent exact-key searches, and occasionally produce sorted reports. Recommend a data-structure strategy and justify its expected costs and trade-offs.
  2. A file import fails on record 8,420 after 8,419 records have been inserted into the live inventory. Analyse the defect in this design and propose a transactional correction.
  3. A program passes functional tests but crashes under malformed long input and reports increasing memory use after repeated imports. Identify likely classes of defects and design a diagnostic and remediation plan.

Multiple Choice Questions with Answers

Question 1

Which design decision best supports information hiding in a C module?

A. Expose the complete structure definition in every client header

B. Use an incomplete structure type and provide operations through a header

C. Allow every module to modify internal fields directly

D. Store all functions and data in one source file

Correct Answer: B. Use an incomplete structure type and provide operations through a header

Question 2

For an inventory with frequent exact-key searches, which structure generally provides expected O(1) lookup under appropriate assumptions?

A. Unsorted array

B. Singly linked list

C. Hash table

D. Stack

Correct Answer: C. Hash table

Question 3

What is the main purpose of loading imported records into a temporary structure before committing them?

A. To guarantee that parsing uses less memory

B. To prevent a partially invalid import from changing live data

C. To eliminate the need for input validation

D. To make all searches logarithmic

Correct Answer: B. To prevent a partially invalid import from changing live data

Question 4

Given quantities 10, 4, and 25 and unit prices 12.50, 80.00, and 3.20, what is the total inventory value?

A. 425.00 currency units

B. 505.00 currency units

C. 525.00 currency units

D. 545.00 currency units

Correct Answer: C. 525.00 currency units

Question 5

Which condition is an example of undefined behaviour in C?

A. Returning a documented error code

B. Reading an element within an array’s bounds

C. Using a pointer after the object it refers to has been freed

D. Comparing two unsigned integers

Correct Answer: C. Using a pointer after the object it refers to has been freed

Question 6

Which testing activity most directly examines interactions among several modules?

A. Unit testing

B. Integration testing

C. Lexical analysis

D. Code formatting

Correct Answer: B. Integration testing

Question 7

Why can scaled integer units be preferable to binary floating-point for fixed-precision currency?

A. They automatically sort records

B. They avoid representation issues for the specified smallest unit

C. They require no range checks

D. They make file access atomic

Correct Answer: B. They avoid representation issues for the specified smallest unit

Question 8

Which practice most improves build reproducibility?

A. Relying on undocumented compiler defaults

B. Compiling only the files changed during the last session

C. Providing a documented build script with explicit standards and dependencies

D. Suppressing all compiler warnings

Correct Answer: C. Providing a documented build script with explicit standards and dependencies

Question 9

What does a sanitizer-supported build primarily help reveal?

A. Organisational conflicts in a project team

B. Runtime memory and certain undefined-behaviour errors

C. Whether requirements are commercially profitable

D. Whether a licence permits code reuse

Correct Answer: B. Runtime memory and certain undefined-behaviour errors

Question 10

Which statement about AI-generated C code is most appropriate?

A. It is correct if it compiles once

B. It removes the need for security review

C. It may assist development but must be reviewed, tested, and checked for licensing concerns

D. It guarantees portability across all C implementations

Correct Answer: C. It may assist development but must be reviewed, tested, and checked for licensing concerns

C Programming
Chapter 15 of 15