Functions and Program Structure in C

Learn to organize C programs the way real projects are built. Write functions with parameters, return values and prototypes, see why C passes arguments by value, and trace calls and recursion. Control where variables are visible and how long they live with scope and storage classes, including static and extern. Use the preprocessor for headers, macros and conditional compilation, split a program into source and header files, and build it with make so that only changed files are recompiled.

BeginnerLesson 2 of 8 in Programming in C Course

What you will learn

  1. Break a program into functions that take parameters and return results, declare them with prototypes, understand that C passes arguments by value, and trace how calls, including recursive ones, run and return.
    • Define functions that take inputs and return a result
      Write function definitions with a return type, parameters and a body, use return and void correctly, and call functions inside expressions.
    • Declare functions with prototypes
      Declare a function before its first call with a prototype, know what the compiler checks and converts, and avoid empty parentheses and implicit declarations.
    • Pass arguments by value and return the result
      Explain that every C argument is copied into its parameter, predict what a function can and cannot change, and use return values to send results back.
    • Trace function calls and recursion
      Follow how each call gets its own parameters and local variables and returns to its caller, and write and trace recursive functions with a base case.
  2. Tell apart the scope of a name and the lifetime of a variable, use static local variables to keep state between calls, and weigh the costs of global variables.
    • Decide where a name can be used (scope)
      Apply block scope, file scope and prototype scope, predict which variable a name refers to when names are shadowed, and avoid scope errors.
    • Choose how long a variable lives with storage classes
      Distinguish automatic and static storage duration, use static local variables to remember values between calls, and know what auto, register, static and extern mean.
    • Share data with global variables carefully
      Define and use file-scope (global) variables, explain their visibility and initialization, and prefer parameters and return values where a global would add hidden coupling.
  3. Use #include, #define and macros safely, and select code at compile time with conditional directives, predefined macros and compiler flags.
    • Include files and define macros
      Explain what the preprocessor does before compilation, include headers, define constant and function-like macros, and avoid the classic macro pitfalls.
    • Compile code conditionally
      Use #if, #ifdef, #ifndef, #elif, #else and #endif with -D flags and predefined macros to include or exclude code at compile time, including include guards.
  4. Compile several source files separately and link them, write header files that share declarations, and control which names other files can see with static and extern.
    • Compile and link several source files
      Describe the stages from source file to executable, compile files separately into object files, link them, and tell compile errors from linker errors.
    • Write header files
      Put shared declarations into a guarded header, include it where it is needed, including in its own source file, and keep definitions out of it.
    • Control linkage with static and extern
      Use extern to share one variable between files and static to keep functions and variables private to their file, and fix the linker errors that come from getting this wrong.
  5. Write makefiles whose rules and dependencies rebuild only what changed, and keep them short with variables, automatic variables, pattern rules and phony targets.
    • Write makefile rules and dependencies
      Write rules with targets, prerequisites and tab-indented recipes, predict which commands make runs after a change, and list header dependencies correctly.
    • Simplify makefiles with variables and pattern rules
      Use variables such as CC and CFLAGS, the automatic variables $@, $< and $^, pattern rules, built-in rules and phony targets to keep a makefile short and correct.

Functions and Program Structure in C study guide

5 chapters

1

Write, declare and call functions

A function gives a task a name, a set of inputs, and a clear way to produce a result. You will build definitions and prototypes, then follow values into calls and back to their callers. Small traces make the rules visible: a parameter receives a copy, a return value needs somewhere to go, and a recursive call must eventually stop. The examples use C17; explicit parameter lists also make the code clear when compiling as C23.

1.1 Define functions that take inputs and return a result

A function packages one task so that another part of the program can call it. Give it a name that describes that task, and choose its inputs and result before writing its body. In an ordinary hosted C program, execution begins in main.

Build a definition
int square(int n)
{
    return n * n;
}

The definition has a return type (int), a name (square), a parameter list (int n), and a body inside braces. The parameter is the local variable that receives an input. An argument is the value supplied by a call: in square(6), that value is 6. The call produces 36. Define every function at file level, outside other functions; standard C does not allow nested function definitions.

Each parameter needs its own type. Write int max(int a, int b), with a comma separating the declarations. In a call, supply values without their types: max(8, 3).

Return a value to the caller
int max(int a, int b)
{
    if (a > b)
        return a;
    return b;
}

A return immediately ends the current call. For max(8, 3), the first return runs, so the second is never reached. A returned expression is converted to the function's return type. Choose that type to preserve the kind of result you need.

For a fractional result, use floating-point arithmetic throughout:

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

The call c_to_f(37.0) returns 98.6. An integer version using c * 9 / 5 + 32 returns 98, because the division discards the fraction. Reordering it to 9 / 5 * c + 32 makes 9 / 5 evaluate to 1 first, giving 69 for 37. A floating-point return type alone cannot undo an integer calculation already performed inside the body.

Use the result

A value-returning call is an expression. With the definition above, square(3) + square(4) gives 25. In square(square(2)), the inner call returns 4, which becomes the outer call's argument, giving 16. A statement such as square(5); is legal, but discards the returned value.

Printing and returning do different jobs. printf displays text; return supplies a result that the caller can store or use in another expression. If an ordinary non-void function reaches its closing brace without returning and the caller uses its result, behavior is undefined. Compile with -Wall -Wextra to help find missing returns.

Functions that produce no value

A void function performs a task without returning a value. It can leave early with return;. For example, void print_line(int width) can print a line of dashes, but int x = print_line(5); is invalid because the call provides no value. To take no inputs, write void in the parameter list, as in int read_sensor(void).

int main(void) returns a status to the host environment. Use return 0; for success; <stdlib.h> also supplies EXIT_SUCCESS and EXIT_FAILURE. Reaching the end of main returns zero automatically. Other value-returning functions do not get that exception.

Key Takeaways

  • A definition states a return type, name, typed parameters, and body.
  • A parameter receives an argument supplied by a call.
  • return ends the call; printing does not supply its result.
  • Use void for no returned value and (void) for no parameters.
  • Choose arithmetic and return types that preserve the needed result.

1.2 Declare functions with prototypes

A function call needs a declaration that the compiler has already seen. If the definition comes later, put a prototype before the first call. The prototype tells the compiler how to check the call without supplying the function's body.

Separate the promise from the body
#include <stdio.h>

double c_to_f(double c);

int main(void)
{
    printf("%.1f\n", c_to_f(25.0));
    return 0;
}

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

The prototype ends with a semicolon. The definition has a body and no semicolon between its parameter list and opening brace. A useful layout is headers, prototypes, main, then other definitions. The output here is 77.0.

Parameter names are optional in prototypes: double c_to_f(double); declares the same function. Names such as c can document an input, but they need not match the definition's names. Every declaration must agree on the return type and parameter types. A used function needs one definition; repeating compatible prototypes does not create more functions.

What the compiler checks

With a prototype, the compiler checks the argument count and converts each supplied value to the declared parameter type. Given double c_to_f(double c);, both c_to_f() and c_to_f(20.0, 5.0) have the wrong count and require a diagnostic.

A numeric type difference can instead be a permitted conversion:

int half(int n)
{
    return n / 2;
}

For half(7.9), conversion first gives the parameter the value 7. Integer division then produces 3. For half(-7.9), conversion gives -7, and division produces -3. Both discard fractional parts toward zero. The call does not preserve the original double value merely because the argument was written with a decimal point. Add -Wconversion when you want warnings about such conversions.

Say explicitly when there are no parameters
DeclarationC17 meaningC23 meaning
int read_sensor(void);No parametersNo parameters
int read_sensor();Parameter information unspecifiedNo parameters

In C17, an empty parameter list in a declaration prevents full argument checking; it does not authorize arbitrary arguments to the eventual function. Use (void) consistently. GCC 15 changed its default C dialect to gnu23, so -std=c17 makes the intended standard explicit when following these examples.

Repair a declaration problem

Calling an undeclared function violates modern C. Include the standard header that declares it, such as <stdio.h> for printf, or add your own matching prototype. Current GCC and Clang normally reject implicit declarations.

A prototype such as double area(double r); cannot match a definition returning float and taking float. Include a module's header in the file that defines its functions so the compiler can compare them. Do not try to fix a type mismatch by adding a cast at the call: correct the declaration and definition to describe the same function.

Key Takeaways

  • Place a declaration before the first call.
  • A prototype ends with a semicolon; a definition supplies the body.
  • Prototypes check argument counts and specify argument conversions.
  • Use (void) for no parameters in both C17 and C23.
  • Include the declaring header and keep every declaration compatible.
  • A permitted numeric conversion can still lose a fraction.

1.3 Pass arguments by value and return the result

C passes every argument by value: each parameter starts with a copy of its argument's value. Assigning to an ordinary integer parameter changes that copy, even if it shares a name with a caller's variable.

Trace the copy

With <stdio.h> included and this definition before its call:

void double_it(int n)
{
    n *= 2;
    printf("inside: %d\n", n);
}

The caller runs:

int score = 21;
double_it(score);
printf("after: %d\n", score);

The output is inside: 42, then after: 21. The parameter changes from 21 to 42; no statement assigns to the caller's score.

Keep a returned result
int doubled(int n)
{
    return n * 2;
}
Caller statementEffect on a score initially equal to 21
doubled(score);Discards 42; score stays 21
score = doubled(score);Stores 42 in score

The function computes a value; the caller's assignment updates its variable. Returning a local's value is safe: int total = a + b; return total; copies the value out before the automatic local ceases to exist. Calculations using only parameters and returned results can be tested without arranging hidden shared state.

See why a value-only swap fails
void swap(int a, int b)
{
    int t = a;
    a = b;
    b = t;
}

After int p = 3, q = 7; swap(p, q);, the caller still has p == 3 and q == 7. The body exchanged its copies. Changing caller objects through parameters requires their addresses, passed as pointer values.

Sequence arguments when order matters

Arguments may be expressions such as a + 1 or square(b). All finish before the called function's body begins, but their evaluation order is unspecified. Two calls to a state-changing next_ticket() in one print can supply tickets in either order. Use separate statements:

int first = next_ticket();
int second = next_ticket();
printf("%d %d\n", first, second);

Avoid f(i, i++): its unsequenced read and modification of i give undefined behavior.

Key Takeaways

  • Parameters receive copies of argument values.
  • Store a returned result with an assignment when the caller needs an update.
  • A value-only swap changes its parameters, not the caller's variables.
  • Use separate statements when argument evaluation order matters.

1.4 Trace function calls and recursion

A call pauses its caller until a result is ready. Tracing the active calls explains how a program returns to the right place and why recursion needs a stopping rule.

Keep one record per active call

Most implementations use a call stack. Each active call has a frame recording its parameter values, automatic local variables, and where to continue in the caller. The standard specifies the behavior without requiring this particular memory layout.

If main calls print_table, and print_table calls c_to_f, the converter returns first. Execution resumes inside print_table, which later returns to main. Ordinary automatic locals belong to one call. A static local is different: all calls share its one stored value.

Follow a recursive calculation down

A recursive function calls itself on a smaller problem. A base case returns directly; a recursive case moves toward it.

int factorial(int n)
{
    if (n <= 1)
        return 1;
    return n * factorial(n - 1);
}

For factorial(4), the first call cannot finish its multiplication until factorial(3) returns. That call waits for factorial(2), which waits for factorial(1).

Active callWork waiting for a result
factorial(4)4 * factorial(3)
factorial(3)3 * factorial(2)
factorial(2)2 * factorial(1)
factorial(1)Base case: return 1

There are four active factorial calls at the deepest point. Each has its own n; reaching n == 1 does not change the earlier calls' values.

Follow the returns up

The base call returns 1. The call with n == 2 now computes 2 * 1 and returns 2. The next returns 3 * 2, or 6; the first returns 4 * 6, or 24. Fill return values into a trace on the way back up, after the called function's result is known.

The stopping condition must be reachable for the supported inputs. If the test were n == 1, starting at zero would keep calling with -1, -2, and lower values instead of reaching one. Continuing recursion exhausts resources and can crash. The broader n <= 1 test stops these inputs, though the mathematical factorial is used for nonnegative integers.

Check both the depth and the result type

Stopping correctly does not guarantee that the result fits. On a platform with a 32-bit int, the largest value is 2147483647. factorial(12) gives 479001600, which fits; factorial(13) would need 6227020800, which does not. Signed integer overflow has undefined behavior.

A loop can compute factorial while keeping one running product in one function call. Recursion is useful when the next problem has the same structure at a smaller size. For positive integers, summing digits is another example: sum_digits(472) needs 2 + sum_digits(47), then 2 + 7 + sum_digits(4), giving 13. A trace should show both the shrinking input and the later combination of returned values.

Key Takeaways

  • Calls finish in reverse nesting order.
  • Each call has its own parameters and automatic locals.
  • Recursion needs a reachable base case and progress toward it.
  • Trace calls downward and returned values upward.
  • Check that recursive results fit their numeric type.
2

Control where variables are visible and how long they live

A name can be visible for a small part of the source while its variable exists for the entire program. Keeping those two properties separate makes local variables, static locals, and globals much easier to understand. You will follow names through nested blocks, compare fresh values with remembered ones, and choose how a function gets the data it needs. That choice affects how easily you can trace a wrong result and test a function by itself.

2.1 Decide where a name can be used (scope)

Scope is the part of the source where a name can be used. Read the braces and declarations to work out which variable an expression names; the order of calls does not change these rules.

Find the scope boundary

A variable declared inside a block has block scope, starting just after its declarator and ending at that block's closing brace. Parameters are visible within their function bodies. A declaration in a for header, such as for (int i = 0; i < 10; i++), makes i available through that loop, but not after it.

A name declared outside all functions has file scope, from its declaration to the end of the translation unit. Functions above that declaration need an earlier declaration to use it. A parameter name in a prototype has prototype scope, ending with the prototype. Thus double c_to_f(double c); can be followed by a definition that names the parameter degrees.

Trace shadowing from the inside out
#include <stdio.h>

int x = 10;

void show(void)
{
    printf("%d\n", x);
}

int main(void)
{
    int x = 5;
    {
        int x = 1;
        printf("%d\n", x);
    }
    printf("%d\n", x);
    show();
    return 0;
}

The inner print uses 1, the next uses 5, and show prints 10. The nearest visible declaration of x wins. Leaving the inner block reveals the outer local again. Calling show does not make main's local visible inside it: show was defined where the file-scope x is visible.

Different functions may each declare a local i, count, or result. These are different variables because their declarations belong to different blocks.

Declare a value where its later uses can see it

This code fails to compile:

if (temp > 30.0) {
    int warning = 1;
}
printf("%d", warning);

The print is outside warning's scope. If the value must be used afterward, declare and initialize it before the conditional:

int warning = 0;
if (temp > 30.0) {
    warning = 1;
}
printf("%d", warning);

Initialization also supplies the value for the path where the condition is false.

Avoid an accidental new variable

Declaring int total = 0; inside a loop creates a new local each time its block is entered. If you intended to accumulate into an outer total, use an assignment such as total += value;. Enable -Wshadow to help find hidden names.

Scope starts before the initializer is evaluated. Inside a block, int x = x + 1; reads the new, uninitialized x, even if an outer x exists. Use a different name if the initializer needs the outer value.

Scope describes where a name is visible in source code. Storage duration describes how long its object exists while the program runs. A static local has block scope while its stored value exists for the entire run.

Key Takeaways

  • Block scope ends at the matching closing brace.
  • File-scope names are visible below their declarations.
  • An inner declaration shadows a visible outer declaration.
  • A called function cannot see its caller's automatic locals.
  • Scope starts before an initializer is evaluated.
  • Scope and storage duration answer different questions.

2.2 Choose how long a variable lives with storage classes

Storage duration describes how long a variable's storage exists. Use automatic variables for work belonging to one call and static storage when a value must survive between calls.

Compare a fresh value with a remembered value

Parameters and ordinary block-scope variables have automatic storage duration. Each function call gets its own instances. A declaration without an initializer, such as int sum;, supplies no usable starting value. Initialize an accumulator before reading it: int sum = 0;.

File-scope variables and block-scope variables declared static have static storage duration. They exist for the entire program run. Initialization happens once before program startup; absent an explicit initializer, numeric objects start at zero.

int next_ticket(void)
{
    static int n = 0;
    n++;
    return n;
}

Three calls in separate statements return 1, 2, and 3. The initializer does not run again at function entry. Removing static creates a fresh automatic n = 0 for each call, so all three calls return 1.

The name n still has block scope. Other functions cannot name it directly. Recursive calls share the same static local too; they do not receive separate static copies as they do with automatic locals.

Initialize once, assign when needed

A static numeric initializer must be a constant expression in C. static int limit = 12; is valid; static int limit = read_sensor(); is invalid. Perform a runtime assignment in executable code when a value must come from a call. A statement such as n = 0; runs whenever execution reaches it, so placing it at the start of next_ticket would reset the counter on every call.

Read the storage-class specifiers
SpecifierMeaning in this C17 code
autoExplicit automatic storage for a block variable; usually omitted
registerA hint about fast access; taking its address is forbidden
static inside a functionRetained storage with a local name
static at file scopeInternal linkage, keeping the name within its translation unit
externA declaration referring to an existing object or function

Modern compilers choose register allocation themselves, so writing register rarely improves code. auto int x; means the same as an ordinary local int x; in C17. C23 also permits type inference with auto x = 3.5;, which declares a double.

C17 also has _Thread_local, spelled thread_local in C23, for thread storage. The grammar groups typedef with storage-class specifiers even though it names a type instead of choosing storage. In C17, only one storage-class specifier normally appears in a declaration; _Thread_local can also accompany static or extern.

A block-scope extern int count; does not create an automatic local copy. It declares an object with linkage and static storage duration. Always consider where the declaration appears before deciding what a storage-class keyword does.

Key Takeaways

  • Initialize automatic variables before reading them.
  • Static storage lasts for the entire program run and defaults to zero.
  • A static local retains its value while its name remains local.
  • A static initializer runs once; an assignment runs when reached.
  • The location of static determines whether its main role is lifetime or linkage.

2.3 Share data with global variables carefully

A global variable is declared at file scope, outside every function. It retains a value for the whole run and can give several functions access to shared state.

Follow the shared object
#include <stdio.h>

double total_km = 0.0;

void add_trip(double km)
{
    total_km += km;
}

int main(void)
{
    add_trip(12.5);
    add_trip(7.25);
    printf("%.2f\n", total_km);
    return 0;
}

Both calls update the same object, producing 19.75. The parameter km is still a copy; the function separately assigns to a global it can name. Pass by value does not prevent that assignment.

Globals have static storage duration. Numeric globals default to zero and require constant-expression initializers: int error_count; starts at 0, and double limit = 2.5 * 4; starts at 10.0.

Check visibility and shadowing

A global name is visible only below its declaration. A function above the definition needs an earlier declaration, such as extern double total_km;.

If add_trip instead contains:

double total_km = 0.0;
total_km += km;

it creates and updates a local object. The global stays unchanged. Remove the extra declaration to update the shared total; -Wshadow helps identify accidental hiding.

Make inputs explicit when sharing is unnecessary

A shared error counter can be useful when several functions report failures. But a calculation reading a global has an input absent from its parameter list. Tests must arrange that state, and tracing a wrong value requires finding its possible writers.

An alternative for the trip calculation is:

double with_trip(double total, double km)
{
    return total + km;
}

The caller uses total = with_trip(total, 12.5);. A test supplies both inputs directly, without any global setup.

Restrict shared names

Ordinary file-scope variables have external linkage. Share one through declarations and one definition; repeated definitions across files break that arrangement. File-scope static instead gives internal linkage, allowing another file to have its own independent same-named object. Both objects still last the whole run: the keyword limits linkage here, not lifetime.

Key Takeaways

  • A global keeps one value across calls and has static storage duration.
  • Pass by value does not stop a function from changing a visible global.
  • Shared state adds inputs and effects that a parameter list does not show.
  • Prefer explicit parameters and returned results when shared state is unnecessary.
3

Use the C preprocessor

Before C code is compiled, the preprocessor includes headers, expands macros, and selects conditional branches. These operations transform the code the compiler receives, so a short macro can change an expression in surprising ways. You will inspect expansions, group expressions safely, and use flags and include guards to control which text is kept. The key distinction is between choosing code during a build and choosing a path while a program runs.

3.1 Include files and define macros

The preprocessor transforms source text before the compiler checks C expressions and types. Its directives begin with #. Inspect its output with gcc -E file.c when an inclusion or macro expansion is hard to explain.

Include declarations and name constants

#include <stdio.h> inserts a standard header. Use #include "convert.h" for a project header: GCC first searches the including file's directory, then its configured header paths. The option -Iinclude adds a search directory. Inclusion copies header text; it does not link compiled functions.

An object-like macro substitutes tokens:

#define MAX_STUDENTS 30

Later uses of the token MAX_STUDENTS become 30. The preprocessor leaves the string "MAX_STUDENTS" and the different name MAX_STUDENTS_2 alone. Write no = and no trailing semicolon. With #define PI 3.14159;, return PI * r * r; expands to the broken code return 3.14159; * r * r;. A directive normally ends at the newline; a final backslash continues it.

Expand before calculating

A function-like macro has no space between its name and opening parenthesis:

#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))

SQUARE_BAD(2 + 3) becomes 2 + 3 * 2 + 3, which evaluates to 11. 100 / SQUARE_BAD(5) becomes 100 / 5 * 5, giving 100. The grouped macro gives 25 and 4 respectively. Parenthesize each parameter and the whole replacement expression so surrounding operators cannot change its grouping.

Writing #define SQUARE (x) ((x) * (x)) instead makes an object-like macro whose replacement begins with (x). The space changes the definition's meaning.

Parentheses do not prevent repeated evaluation

SQUARE(i++) expands to ((i++) * (i++)). It modifies i twice without sequencing, so behavior is undefined. A real function such as square(i++) evaluates the argument once before entering the body. Prefer functions for calculations, especially when an input expression changes state.

A function-like macro has no declared parameter or return types. Its expanded C code is still type-checked, but it has no prototype specifying argument conversions. Macros remain useful for include guards, conditional compilation, and integer constant expressions. In C17, a const int variable is not an integer constant expression; a macro or an enum constant can supply a case value. C23 also introduces constexpr.

Turn arguments into tokens or text
#define SHOW(expr) printf(#expr " = %d\n", expr)
#define CAT(a, b) a##b

SHOW(3 + 4); uses # to turn the argument into text and prints 3 + 4 = 7. CAT(temp, 1) uses ## to join tokens into the name temp1. Stringification uses the supplied argument tokens without expanding macros inside that occurrence; an ordinary occurrence of the argument is expanded.

Macros do not follow block scope. A definition remains active until #undef NAME or the end of the translation unit, even if the directive was written between braces. Remove an old definition with #undef before giving it different replacement text.

Key Takeaways

  • Preprocessing includes text and expands tokens before C compilation.
  • A macro definition has no = or trailing semicolon.
  • Parenthesize macro parameters and the full replacement expression.
  • Repeated macro arguments can repeat side effects; use a function for calculations.
  • # stringifies an argument and ## joins tokens.
  • A macro remains defined across block boundaries until it is undefined.

3.2 Compile code conditionally

Conditional compilation selects which source lines reach the compiler. It can enable debug output, choose platform code, or protect a header against repeated inclusion.

Test existence or test a value
#define DEBUG 0

#ifdef DEBUG
/* Kept: DEBUG exists. */
#endif

#if DEBUG
/* Removed: DEBUG expands to zero. */
#endif

#ifdef DEBUG asks whether the macro is defined. #if DEBUG evaluates its replacement value as an integer constant expression. Inside #if, an undefined identifier counts as zero; a misspelled macro can silently remove a branch. The expressions defined DEBUG and defined(DEBUG) both test existence.

Use #ifndef NAME for the opposite existence test. Close every conditional with #endif. #elif adds another condition and #else supplies the fallback. C23 adds #elifdef and #elifndef; C17 can express those tests using #elif defined(NAME) or #elif !defined(NAME).

Choose the setting at build time
#ifdef DEBUG
printf("debug: c = %.1f\n", c);
#endif

gcc -std=c17 -DDEBUG main.c defines DEBUG as 1 and keeps the print. Without that definition, the line is removed before compilation. -DLEVEL=2 supplies a value as if #define LEVEL 2 appeared before the file's text.

This differs from an ordinary if, whose condition is evaluated when the program runs. Preprocessor conditions cannot read an ordinary runtime variable. #if 0 removes a block, including code with existing /* */ comments that would make wrapping it in another such comment unsafe.

Guard a header
#ifndef CONVERT_H
#define CONVERT_H

double c_to_f(double c);

#endif

The first inclusion defines CONVERT_H and keeps the prototype. Later inclusions in that same translation unit skip the contents. Use a distinct guard name based on the header name; avoid leading underscores reserved for the implementation. Another translation unit begins its own preprocessing run and includes the declarations for itself. A guard does not stop different object files from defining the same external symbol.

Many compilers support #pragma once, but it is not an ISO C directive. Guards work with standard conditional directives.

Read the predefined information
NameInformation
__FILE__Current source file name as a string
__LINE__Current source line number
__DATE__, __TIME__Translation date and time
__STDC_VERSION__Selected standard version

Version values include 199901L for C99, 201112L for C11, 201710L for C17, and 202311L for C23. __func__ holds the current function name, but is a predefined identifier, not a macro.

A platform branch can test defined(_WIN32) or defined(__linux__), then use #else for a fallback. To reject an unsupported build explicitly, write a condition followed by #error "This program needs C11 or later" and #endif. An omitted #endif instead produces a directive error such as "unterminated conditional directive".

Key Takeaways

  • #ifdef tests existence; #if tests a value.
  • Undefined identifiers in #if evaluate as zero.
  • -DNAME=value supplies a build-time macro definition.
  • Include guards prevent repeated processing within one translation unit.
  • Predefined macros expose file, line, build-time, and standard information.
4

Split a program across source and header files

A larger program can keep related functions in separate source files while sharing their declarations through headers. Each source file is compiled independently, and the linker connects the resulting object files. You will build a small module, decide which names other files need, and diagnose missing or duplicate definitions. Including the same header in callers and in the defining file helps the compiler catch disagreements before the program runs.

Separate compilation turns each source file into an object file before linking the program. This lets a build reuse unchanged object files and gives related functions a clear place to live.

Follow the build stages

A translation unit is one .c file after preprocessing, including the contents of its included headers. Each unit is compiled independently.

StageResultGCC option that stops there
PreprocessingExpanded C text-E
CompilingAssembly text-S
AssemblingObject file-c
LinkingExecutableNormal final build

An object file contains machine code and symbol information about names it defines or needs. On Unix-like systems it usually has the suffix .o. The linker connects external name uses to definitions supplied by object files and libraries.

Compile two files and link them

Suppose convert.h declares double c_to_f(double c);, convert.c includes that header and defines the function, and main.c includes the header and calls it.

gcc -Wall -Wextra -std=c17 -c main.c
gcc -Wall -Wextra -std=c17 -c convert.c
gcc main.o convert.o -o temp

The first command produces main.o without linking. The second produces convert.o. The last makes the executable temp from both objects. You can also compile and link in one command:

gcc -Wall -Wextra -std=c17 main.c convert.c -o temp

Pass the source files for compilation and the object files for linking. Headers enter through #include; do not list them as separately compiled program sources. If only convert.c changes, reuse main.o, rebuild convert.o, and link again. If a shared header changes, rebuild every object whose source includes it.

Diagnose the stage that failed

A compile error concerns a translation unit: invalid syntax, a wrong argument count, or conflicting visible types. The linker cannot repair these.

A link error concerns the assembled program. If you build only main.c, the prototype can make compilation succeed while linking reports "undefined reference to c_to_f" or "Undefined symbols". The declaration promises a function; it does not supply its machine code. Add convert.c to a combined build, or convert.o to the link.

Two linked external definitions of the same function cause a duplicate-symbol error. The ordinary program has one main; linking two sources that both define it fails. File-private static functions can have matching names in different translation units because they have internal linkage.

Libraries also supply definitions. On many Linux systems, code using functions declared by <math.h> needs a math-library link option: gcc main.c -o app -lm. Keep that library option after the code that needs it.

Include the interface, compile the implementation

Avoid #include "convert.c". That copies function bodies into another translation unit. If convert.c is also compiled and linked, its external definitions appear twice. A module's .h file shares its declarations; its .c file is compiled once as a source file.

Key Takeaways

  • A translation unit is a preprocessed source file with its included text.
  • -c produces an object without linking.
  • Link every needed object file into the executable.
  • Missing and duplicate definitions are link problems.
  • Include headers; compile source files separately.

4.2 Write header files

A module's header shares its interface; its source supplies the implementation. Both callers and the implementation include the header so their declarations agree.

Declare the interface once

A guarded convert.h contains:

#ifndef CONVERT_H
#define CONVERT_H

double c_to_f(double c);
double f_to_c(double f);

#endif

The implementation in convert.c includes it before the bodies:

#include "convert.h"

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

double f_to_c(double f)
{
    return (f - 32.0) * 5.0 / 9.0;
}

A caller includes the same header and <stdio.h>. The call printf("%.1f %.1f\n", c_to_f(25.0), f_to_c(212.0)); prints 77.0 100.0.

If the implementation defines float c_to_f(float c), including its header exposes the conflicting types to the compiler. Independent compilation without that declaration can miss the disagreement.

Choose the right contents
Share in a headerKeep in a source
Function prototypesExternally linked function bodies
extern variable declarationsShared object definitions
Needed macros and typesPrivate helpers and state

A header containing int count = 0; defines an object in every including translation unit, causing duplicate external definitions. Even int count; is a tentative definition. GCC's default since version 10 diagnoses repeated copies at link time, while some toolchains merge common symbols and hide the bug. Use extern int count; in the header and one definition in a source.

Make inclusion reliable

A guard prevents repeated processing within a translation unit, including indirect inclusion through other headers. Compatible prototypes can repeat, but type definitions such as an enum can cause redefinition errors.

A self-contained header includes what its declarations need. If a prototype uses size_t, include <stddef.h> there. Callers should not need a special include order.

For project headers, quotes search the including file's directory first with GCC. Add a separate header directory with -Iinclude, as in gcc -Iinclude -std=c17 -c src/main.c. The implementation's object still needs to be linked.

Key Takeaways

  • Headers share prototypes, extern declarations, macros, and types.
  • Keep shared object definitions and externally linked function bodies in source files.
  • Include a module's header in its own implementation.
  • Use guards and include the headers that your declarations need.

4.3 Control linkage with static and extern

Linkage determines whether declarations in different places refer to the same entity. It is separate from scope, which describes where a name can be used, and storage duration, which describes how long an object exists.

Connect one name to one object
LinkageMeaning
ExternalDeclarations can refer to one entity across translation units
InternalDeclarations refer to an entity within one translation unit
NoneA local name, such as an ordinary parameter, identifies its own entity

Functions and ordinary file-scope variables have external linkage by default. File-scope static gives internal linkage. Ordinary automatic locals and parameters have no linkage.

To share one error counter, put these declarations in a guarded header:

extern int error_count;
void log_error(int code);

One source file includes that header and supplies:

int error_count = 0;

void log_error(int code)
{
    printf("error: %d\n", code);
    error_count++;
}

That source also includes <stdio.h> for printf. Other source files include the shared header, not a second definition. After three calls, every caller reading error_count refers to the same object and sees 3.

Distinguish declaration from definition

extern int error_count; declares an existing object and allocates no new storage. int error_count = 0; defines it. An initializer changes the case: at file scope, extern int error_count = 0; is a definition despite the keyword. Keep the initializer in the one defining file.

On ordinary public function declarations, extern is optional: double c_to_f(double c); already declares a function with external linkage. Include the public declaration in its defining file so the compiler can check compatibility.

Hide state behind functions
static int count = 0;

void counter_increment(void)
{
    count++;
}

int counter_value(void)
{
    return count;
}

The stored value exists for the whole run, but only this translation unit can refer to it by the name count. Put the public function prototypes in a header. Another file can increment and read the counter through those functions without directly changing the private variable. A helper function can also be static when other files do not need it.

Diagnose a mistaken linkage choice

A static int count; definition in a header creates an independent private counter in each including translation unit. Updating one does not update the others. Include guards do not merge these objects: each translation unit preprocesses its own header copy.

A function defined static in one translation unit cannot satisfy an external call from another; linking fails for that external name. Give only functions that other files need external linkage, and declare those in the header.

Inside a function, static gives a local variable retained storage without widening its scope. Outside functions, it gives an object or function internal linkage. File-scope variables have static storage duration with or without that keyword.

Key Takeaways

  • External linkage connects declarations across translation units.
  • Share a variable with extern declarations and one definition.
  • File-scope static makes an object or function private to its translation unit.
  • A header containing a static variable gives separate objects to including units.
  • An initializer makes a file-scope extern object declaration a definition.
  • Local static retention and file-scope static linkage have different roles.
5

Build multi-file programs with make

A correct build needs more than a list of compiler commands: it needs to know which files depend on which other files. A makefile records those relationships so that a source edit recompiles the affected object files and then relinks the program. You will trace those rebuild decisions, include header dependencies, and shorten repeated commands with variables and pattern rules. Dry runs make the plan visible before any recipe executes.

5.1 Write makefile rules and dependencies

make reads Makefile or makefile in the project directory and updates the requested target. Without a target argument, it normally builds the first ordinary target.

Record outputs and inputs

A rule starts with target: prerequisites. Its recipe is either on following tab-indented lines or after a semicolon on the same line:

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g

temp: main.o convert.o; $(CC) $(CFLAGS) -o temp main.o convert.o

main.o: main.c convert.h; $(CC) $(CFLAGS) -c main.c

convert.o: convert.c convert.h; $(CC) $(CFLAGS) -c convert.c

.PHONY: clean
clean:; rm -f temp main.o convert.o

Here temp is the default goal. make clean requests the cleanup action. If writing a recipe on the next line, use a literal tab: spaces can cause "missing separator". An editor's visible-whitespace setting helps spot the difference.

Trace the rebuild decisions

Make updates a target's prerequisites first. It then rebuilds the target if it is absent or any prerequisite has a newer modification time.

  • First build: compile both sources, then link temp—three commands.
  • No changes: no program recipes need to run.
  • Edit only convert.c: recompile it and relink—two commands; reuse main.o.
  • Edit convert.h: recompile both sources and relink—three commands.

The header affects both translation units, even though neither source was edited. Picture input files feeding objects, then objects feeding the executable. Changes propagate through those recorded relationships.

Include header dependencies

The incomplete rule main.o: main.c misses edits to convert.h. Make does not read C source to discover includes; it follows declared prerequisites. List included headers or load compiler-generated dependency files to avoid linking stale objects.

Keep dependent shell operations together

Separate recipe lines normally use separate shells. A line containing only cd build does not change the directory for the next line. Use one command such as cd build && gcc -c ../main.c when the compile depends on that directory change. Preview planned recipes with make -n.

Key Takeaways

  • Rules name targets, prerequisites, and tab-indented recipes.
  • Make updates prerequisites before deciding whether to rebuild their target.
  • A missing target or newer prerequisite triggers a rebuild.
  • Header edits must trigger rebuilding every affected object.
  • Each recipe line normally runs in its own shell.

5.2 Simplify makefiles with variables and pattern rules

Variables and pattern rules remove repeated compiler commands while preserving the dependency graph. The examples place each recipe after a semicolon on its rule line. Shorten a makefile only after its build relationships are correct.

Name shared settings
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g
OBJS = main.o convert.o

temp: $(OBJS); $(CC) $(CFLAGS) -o $@ $^

%.o: %.c convert.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:; rm -f temp $(OBJS)

$(CC) expands the compiler variable; $(CFLAGS) expands its flags. CPPFLAGS conventionally holds preprocessor options such as -DDEBUG or -Iinclude. The flags -Wall -Wextra enable warnings, -g adds debugging information, and -O2 enables optimization. Undefined make variables expand to empty text. Write $(CC), not $CC: the latter means the one-letter variable C, followed by a literal C.

Read automatic variables inside a rule
VariableValue in the current rule
$@Target name
$<First prerequisite
$^All prerequisites, removing duplicate names
$?Prerequisites newer than the target

For main.o, the pattern's stem is main, so %.c becomes main.c. The compile recipe uses $< to pass that source to the compiler, and $@ to name main.o. The header is a dependency, but is not passed as another compilation input.

In the link rule, all prerequisites are object files. $^ expands to main.o convert.o, and $@ becomes temp. Using $< there would pass only main.o, losing the definitions in convert.o.

GNU make has built-in rules that can compile x.c to x.o using CC, CPPFLAGS, and CFLAGS. An explicit dependency rule without a recipe can use that built-in recipe. GNU make can even build a simple program with make hello from hello.c without a makefile.

Mark action targets as phony

clean names an action, not a file to create. .PHONY: clean ensures the action runs even if a file named clean exists. rm -f tolerates files already being absent. An all target that groups several programs is usually phony too.

Generate header lists and inspect rebuilds

Add -MMD -MP to compile flags to write a .d file alongside each object. -MMD records user-header dependencies; -MP adds empty rules that help when a recorded header is removed. After the ordinary default target, load the lists with:

-include $(OBJS:.o=.d)

The substitution turns object names into dependency-file names. The leading minus tolerates missing .d files on the first build. Remove these generated files in clean along with the objects.

make -n previews commands, make -B forces rebuilding, and make -j4 permits up to four recipes at once. Correct dependencies matter for parallel builds too. Changing CFLAGS text alone does not make existing objects older than a prerequisite. After changing flags, use make -B or a clean rebuild so every object uses the new settings.

Key Takeaways

  • Use $(NAME) to expand a named make variable.
  • $@, $<, and $^ name the target, first prerequisite, and all prerequisites.
  • Pattern rules reuse one recipe for files with matching stems.
  • Phony targets run actions even when matching files exist.
  • Generated .d files record user-header dependencies.
  • Force or clean a rebuild after changing compiler flags.

Functions and Program Structure in C examples

5 Walkthroughs

1

Build and trace a function call

19 steps
Write a function, declare it, keep its result, and trace a recursive calculation.
  1. 1
    IntroductionStart with a small task

    Follow a value into a function, then find where its result goes. We will build a definition and prototype, distinguish a copied parameter from its caller, and trace recursive calls down to a base case and back through their returned values.

  2. 2
    ExampleGive a task a typed input

    We want a function that adds a fixed adjustment to an integer reading.

    int corrected(int reading)
    {
        return reading + 4;
    }
    

    int before the name describes the result. int reading declares a parameter, and the body computes the returned value. Define this function outside all other function bodies; standard C does not support nested definitions.

  3. 3
    ExampleUse a call as an expression

    For int result = corrected(9);, the argument 9 initializes the parameter reading. The return expression produces 13, and the caller's assignment stores that value in result.

    The call can also sit inside another expression: 2 * corrected(9) gives 26. Writing only corrected(9); still runs the call but discards its returned value.

  4. 4
    TipChoose between returning and printing

    A calculation can return a result without printing it. That lets a caller store it, compare it, or print it later.

    A function declared void can perform an output task but supplies no value for an assignment. Inside it, return; leaves without a value. In a value-returning function, make sure each result-producing path reaches an appropriate return; displaying a number does not supply it to the caller.

  5. 5
    ChallengeCheck the arithmetic before the return

    What does this function return for average_pair(5, 8)?

    double average_pair(int a, int b)
    {
        return (a + b) / 2;
    }
    

    ---reveal---

    It returns 6.0: integer addition gives 13, integer division gives 6, and return conversion gives 6.0. Use 2.0 to make the division floating-point if you need 6.5. The return type cannot restore a fraction already discarded.

  6. 6
    ExplanationTell the compiler before the call

    When the body comes later, place int corrected(int reading); before the call. This prototype lets the compiler check argument count and conversions. Its semicolon marks a declaration; the definition supplies the body.

  7. 7
    Key InsightSpecify an empty input list

    Use int read_sensor(void); for no parameters in both C17 and C23. In C17, int read_sensor(); leaves parameter information unspecified; C23 changes that spelling to mean none. Select C17 explicitly with -std=c17.

  8. 8
    ChallengeTrace a permitted conversion

    The compiler has seen int corrected(int reading);. What does corrected(6.8) return, and must the call be rejected?

    ---reveal---

    The call can compile. The prototype converts 6.8 to the integer 6, then the function adds 4 and returns 10. A conversion warning can help expose lost information; use -Wconversion. A prototype checks a call using its declared types, but not every different numeric argument type is an error.

  9. 9
    ExampleDraw two separate values

    Consider a different function:

    void change(int reading)
    {
        reading += 4;
    }
    

    The caller runs int sample = 9; change(sample);. At entry, sample and the parameter each hold 9 in separate objects. The body changes its parameter to 13. When the call returns, sample still holds 9: there was no assignment to it.

  10. 10
    ConnectionLet the caller keep a result

    Use sample = corrected(sample); to retain the result. The function computes from a copy; the caller then stores its returned value. Matching names would not connect their objects—the assignment updates sample.

  11. 11
    ChallengeFind the discarded call

    Starting with int sample = 7;, the caller runs:

    corrected(sample);
    sample = corrected(sample);
    

    What is sample afterward?

    ---reveal---

    The first call computes 11 and discards it, leaving sample at 7. The second also receives 7, returns 11, and this time the caller stores it. The final value is 11, not 15; a call does not update its argument variable merely by returning a changed value.

  12. 12
    ExplanationAdd a stopping rule for recursion

    A recursive function calls itself on a smaller problem:

    int total_to(int n)
    {
        if (n <= 0) return 0;
        return n + total_to(n - 1);
    }
    

    For a positive input, subtracting one moves toward the base case. The base case answers directly. The recursive case cannot finish its addition until the smaller call returns.

  13. 13
    ExampleWrite the calls on the way down

    Trace total_to(3). The first call holds n == 3 and waits to add 3 to total_to(2). That call holds its own n == 2 and waits for total_to(1).

    The call with n == 1 waits for total_to(0), which returns 0 immediately. At that deepest point, four calls are active. Do not replace the earlier values of n with zero; they belong to different calls.

  14. 14
    ExampleFill the results on the way back

    Once total_to(0) returns 0, the waiting work can finish:

    • The call with n == 1 returns 1 + 0, or 1.
    • The call with n == 2 returns 2 + 1, or 3.
    • The call with n == 3 returns 3 + 3, or 6.

    Most implementations keep these active calls in stack frames. Each return resumes the caller after its call, then releases that call's automatic state.

  15. 15
    ChallengeCheck whether a base case is reachable

    Suppose the base test in total_to is changed to n == 0, and the recursive step still uses n - 1. What goes wrong when the initial argument is -2?

    ---reveal---

    The inputs become -2, -3, -4, and continue away from zero. The stopping test exists but is unreachable along that path. Continued recursion can exhaust stack space. The n <= 0 version stops this input immediately.

  16. 16
    TipCheck the size of the answer too

    A terminating recursive calculation can still exceed its return type. With a 32-bit int, factorial(12) fits, while the mathematical value of factorial(13) exceeds the maximum 2147483647.

    Signed overflow is undefined behavior. Before choosing recursive inputs, check both that calls stop and that the intermediate and final arithmetic fit the types you use. A loop also needs the arithmetic check.

  17. 17
    TipSeparate calls when order matters

    All argument expressions finish before the called function's body begins, but C does not specify their evaluation order. If two calls change shared state, compute their results in separate statements before passing those results onward.

    Avoid f(i, i++): it reads and changes i without the required sequencing. Pass by value does not make an unsafe argument expression safe.

  18. 18
    ChallengeCombine declaration, conversion, and calls

    With total_to declared before use, a caller writes int x = 2; total_to(x); x = total_to(3.9);. What is the final x?

    ---reveal---

    The first result is discarded, so x stays 2. The second argument converts to 3; the recursive trace returns 6, which the assignment stores in x. The declaration controls the conversion, recursion computes the value, and the caller decides whether to keep it.

  19. 19
    RecapUse three checkpoints for a call

    For a call, check its declaration, trace its copied inputs, and locate the use of its return value. For recursion, track each active call separately and combine results outward from the base case.

2

Follow names and retained values

13 steps
Trace a block, a static counter, and a shared total without confusing their objects.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionStart with a small task
  2. 2
    ExampleRead the nearest visible declaration
  3. 3
    ExplanationKeep the scope where the function was written
  4. 4
    ChallengeRepair a value needed after a block
  5. 5
    ExampleWatch an automatic counter restart
  6. 6
    ExampleRetain the counter with static
  7. 7
    ChallengeDistinguish initialization from assignment
  8. 8
    TipRead the keyword in its location
  9. 9
    ExampleTrace a global update
  10. 10
    ChallengeFind the hidden local
  11. 11
    ConnectionMake a calculation easier to test
  12. 12
    ChallengeCombine scope and remembered state
  13. 13
    RecapAnnotate names and lifetimes separately
3

Inspect code before compilation

13 steps
Expand expressions, select a debug branch, and walk through a header guard.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionStart with a small task
  2. 2
    ExplanationSee what inclusion actually supplies
  3. 3
    ExampleReplace tokens before using operator rules
  4. 4
    ExampleProtect inner and outer grouping
  5. 5
    ChallengeFind what parentheses cannot repair
  6. 6
    TipKeep macro rules separate from scope rules
  7. 7
    ExampleTest a setting by existence and value
  8. 8
    ChallengeSelect a branch from a compiler flag
  9. 9
    ExampleWalk through the first header inclusion
  10. 10
    Key InsightStart a fresh guard state in another unit
  11. 11
    ConnectionLocate a diagnostic in the source
  12. 12
    ChallengeCombine expansion and branch selection
  13. 13
    RecapInspect the transformation in order
4

Build one program from two sources

14 steps
Create a header and implementation, link their objects, and protect private state.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionStart with a small task
  2. 2
    ExplanationFollow one translation unit to an object
  3. 3
    ExampleWrite the shared declaration
  4. 4
    ExamplePut the implementation in its own source
  5. 5
    ChallengePlace one shared variable correctly
  6. 6
    ExampleCompile separately, then link
  7. 7
    ChallengeRepair an undefined reference
  8. 8
    TipDo not copy the implementation into the caller
  9. 9
    ExampleKeep a counter inside the module
  10. 10
    ExplanationSeparate three properties of the counter
  11. 11
    ChallengeExplain two independent counters
  12. 12
    TipMake a header work on its own
  13. 13
    ChallengeRepair privacy and interface together
  14. 14
    RecapMatch each need to its build stage
5

Build only what changed

15 steps
Write dependencies, trace rebuilds, and shorten a makefile without losing its meaning.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionStart with a small task
  2. 2
    ExampleRecord the executable and object inputs
  3. 3
    ExampleGive an object a recipe
  4. 4
    ExampleRun the first build and a source rebuild
  5. 5
    ChallengeTrace a shared-header edit
  6. 6
    TipInspect the plan before running it
  7. 7
    ExplanationReplace repeated settings with variables
  8. 8
    ExampleExpand the link recipe
  9. 9
    ExampleUse one compile pattern for both objects
  10. 10
    ChallengeChoose a source without compiling the header
  11. 11
    TipMake cleanup an action
  12. 12
    ConnectionLet the compiler record header dependencies
  13. 13
    ChallengeKeep a parallel rebuild correct
  14. 14
    ChallengeApply changed flags to the whole program
  15. 15
    RecapCheck the graph before trusting the output

Functions and Program Structure in C practice questions

50 Practice Questions in 5 sets

Set 1: Follow values through function calls10 Practice Questions

Question 1 of 10

What is the parameter in int triple(int value) { return value * 3; }?
  1. triple
  2. int
  3. value
  4. value * 3
Show answerHide answer
Answer: C
The parameter is value, the variable initialized from the supplied argument. triple names the function; value * 3 is the returned expression, not a parameter.

Question 2 of 10

What does void report(int code) allow a caller to do?
  1. Store report(4) in an int.
  2. Call report(4); as a statement.
  3. Add report(4) to an integer.
  4. Return report(4) from an int function.
Show answerHide answer
Answer: B
A void call can perform its task as a statement but supplies no result. The int code parameter describes an input, so it does not make the call usable in an integer assignment.

Question 3 of 10

What value does double portion(int total) { return total / 4; } return for portion(11)?
  1. 2.0
  2. 2.75
  3. 3.0
  4. 2.5
Show answerHide answer
Answer: A
Both operands of total / 4 are integers, so the body computes 11 / 4 as 2. That result is then converted to double, giving 2.0. A return type of double does not make the earlier division floating-point; 2.75 would require a floating-point operand.

Question 4 of 10

A definition of int capacity(void) appears below main. What declaration above main fully describes a no-input call in C17?
  1. int capacity();
  2. void capacity(int);
  3. int capacity(void) { }
  4. int capacity(void);
Show answerHide answer
Answer: D
int capacity(void); explicitly declares an integer result and no parameters. In C17, int capacity(); leaves parameter information unspecified instead of fully describing the no-input call.

Question 5 of 10

With int fifth(int n) { return n / 5; } declared before use, what is fifth(-14.8)?
  1. -3
  2. -2
  3. -2.96
  4. -14
Show answerHide answer
Answer: B
Conversion to the parameter type first discards the fraction toward zero, giving n == -14. Integer division by 5 then gives -2. Choosing -3 would round downward instead of applying C's toward-zero rule for these conversions and integer division.

Question 6 of 10

A header declares double stretch(int n);. What definition is compatible with that declaration?
  1. int stretch(int count) { return count * 1.5; }
  2. double stretch(double n) { return n * 1.5; }
  3. double stretch(void) { return 1.5; }
  4. double stretch(int count) { return count * 1.5; }
Show answerHide answer
Answer: D
The return type must remain double and the parameter type must remain int; the parameter name may change. Keeping the name n while changing its type to double still conflicts with the prototype. Compatibility depends on the declared types and parameter list, not on matching parameter names.

Question 7 of 10

What is printed by the following complete program?

#include <stdio.h>
void lower(int n) { n -= 4; }
int main(void) {
    int level = 17;
    lower(level);
    printf("%d\n", level);
}
  1. 17
  2. 13
  3. 4
  4. 0
Show answerHide answer
Answer: A
The call copies 17 into the parameter n, and the subtraction changes only that parameter. level in main still holds 17 when printed. The tempting result 13 would require assigning a returned result to level or another operation that actually changes the caller's object.

Question 8 of 10

What is the final value of x?

int adjust(int x) { x += 3; return x * 2; }
/* In main: */
int x = 4;
adjust(x);
x = adjust(x + 1);
  1. 22
  2. 28
  3. 16
  4. 14
Show answerHide answer
Answer: C
The first call computes 14 but discards it, leaving the caller's x at 4. The next call receives 5, increases its local to 8, and returns 16, which is assigned to the caller. A result of 22 incorrectly keeps the first call's local update as though it changed the caller before the second call.

Question 9 of 10

What does depth(3) return?

int depth(int n) {
    if (n == 0) return 2;
    return 1 + depth(n - 1);
}
  1. 3
  2. 4
  3. 5
  4. 6
Show answerHide answer
Answer: C
The calls descend from 3 to 0, where the base case returns 2. Each of the three waiting calls adds one, producing 3, 4, then 5. A result of 3 counts the recursive steps but loses the value supplied by the base case.

Question 10 of 10

What value does int answer = collect(4.9); store?

int collect(int n) {
    if (n <= 1) return 1;
    return n + collect(n - 2);
}
  1. 7
  2. 8
  3. 6
  4. 5
Show answerHide answer
Answer: A
The int parameter receives 4, so the active calls use 4, 2, and 0. The last returns 1; the earlier calls return 2 + 1 == 3 and 4 + 3 == 7. Getting 6 incorrectly lets the zero-input call return zero, although the actual base case returns one.
Set 2: Trace local names and remembered state10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

Where can the name i from for (int i = 0; i < 6; i++) { } be used?
  1. Throughout the containing function.
  2. Throughout the remaining source file.
  3. Within every function called by the loop.
  4. Within that for statement.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

What values does this program print?

#include <stdio.h>
int size = 9;
void show(void) { printf("%d ", size); }
int main(void) {
    int size = 4;
    show();
    printf("%d", size);
}
  1. 9 4
  2. 4 4
  3. 9 9
  4. 4 9
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

What is printed?

int total = 0;
for (int i = 1; i <= 3; i++) {
    int total = i;
    total += 2;
}
printf("%d", total);
  1. 6
  2. 0
  3. 12
  4. 5
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

Why does int amount = amount + 2; inside a block fail to initialize from an outer amount?
  1. The outer name loses its stored value.
  2. All initializer expressions run before scope starts.
  3. The new name is in scope in its initializer.
  4. Both declarations identify the same local object.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

How many times is static int calls = 0; initialized during one program run?
  1. Once, before program startup.
  2. Once per call to its function.
  3. Once at each level of recursion.
  4. Once each time it is read.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

What values do three separate calls to tick return?

int tick(void) {
    static int n = 5;
    n += 2;
    return n;
}
  1. 7, 7, 7
  2. 5, 7, 9
  3. 7, 9, 11
  4. 5, 5, 5
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

What is result after int result = visit(2);?

int visit(int n) {
    static int calls = 0;
    calls++;
    if (n == 0) return calls;
    int inner = visit(n - 1);
    return inner + calls;
}
  1. 6
  2. 9
  3. 7
  4. 3
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A calculation int permitted(int n) reads a global limit. Why might the same argument produce a different result on a later call?
  1. Passing an int argument changes its source variable.
  2. Each call resets every file-scope object to zero.
  3. A local name in the caller replaces the global.
  4. Another operation may have changed limit.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

What two integers are printed?

#include <stdio.h>
int count = 6;
int advance(int n) { count += n; return count; }
int main(void) {
    int count = 2;
    count = advance(count);
    int next = advance(1);
    printf("%d %d", count, next);
}
  1. 3 4
  2. 9 9
  3. 2 7
  4. 8 9
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A function needs to initialize a retained numeric variable with read_sensor() at runtime. Why is static int first = read_sensor(); invalid in C17?
  1. A static local cannot be read after initialization.
  2. A static numeric initializer must be constant.
  3. A function cannot return a value to a local.
  4. A static local is initialized on every call.
Show answerHide answer

Save this lesson to see this answer.

Set 3: Expand macros and choose build-time branches10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

What text does #define LIMIT 18 replace in later source code?
  1. The characters LIMIT inside "LIMIT".
  2. The standalone token LIMIT.
  3. The suffix of the name LOCAL_LIMIT.
  4. The entire statement containing LIMIT.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

What is the value of 5 * INC(3)?

#define INC(x) x + 1
  1. 16
  2. 20
  3. 19
  4. 15
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

Why does #define LIMIT 12; make int count = LIMIT * 2; invalid?
  1. The macro cannot replace a token after =.
  2. A numeric macro needs a declared return type.
  3. The semicolon becomes part of the expression.
  4. The macro changes count into a constant name.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

After this snippet, what are the values of a and b?

#define TWICE(x) x + x
#define GROUP(x) ((x) + (x))
int a = 30 / TWICE(2 + 1);
int b = 30 / GROUP(2 + 1);
  1. a = 5, b = 5
  2. a = 10, b = 5
  3. a = 19, b = 10
  4. a = 19, b = 5
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

What is the behavior of CUBE(i++) with this definition?

#define CUBE(x) ((x) * (x) * (x))
  1. It evaluates the argument once and cubes that value.
  2. It increments three times in a specified left-to-right order.
  3. It is rejected because a macro cannot take expressions.
  4. It has undefined behavior from repeated unsequenced changes.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

With #define TRACE 0, what does #ifdef TRACE do?
  1. Skips its branch because TRACE expands to zero.
  2. Keeps its branch because TRACE is defined.
  3. Rejects the definition because zero is not a name.
  4. Reads a runtime variable named TRACE.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

Why might #if VERBOSE silently skip a branch when the build defines only VERB0SE?
  1. The undefined identifier VERBOSE counts as zero.
  2. The two names share one definition because they look alike.
  3. Every undefined preprocessor name is a fatal compile error.
  4. The directive waits for a runtime value of VERBOSE.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

With -DLEVEL=2, what value reaches the compiler for result?

#define STEP(x) ((x) + 1)
#if defined(LEVEL) && LEVEL > 2
int result = STEP(5);
#elif LEVEL == 2
int result = 2 * STEP(3);
#else
int result = STEP(1);
#endif
  1. 6
  2. 7
  3. 8
  4. 2
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

What text is printed by this complete program?

#include <stdio.h>
#define JOIN(a, b) a##b
#define PRINT(x) printf(#x "=%d", x)
int main(void) {
    int item2 = 7;
    PRINT(JOIN(item, 2));
}
  1. item2=7
  2. JOIN(item, 2)=7
  3. JOIN(item, 2)=2
  4. item2=2
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A guarded header contains int mode = 2;. Two separate source files include it and are compiled, then linked together. Why does the guard not prevent a duplicate-definition error?
  1. The second source reuses the first source's guard state.
  2. The guard turns both definitions into shared declarations.
  3. Each translation unit includes its own external definition.
  4. The linker removes guard names before checking duplicates.
Show answerHide answer

Save this lesson to see this answer.

Set 4: Diagnose headers, objects, and linkage10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

What does gcc -std=c17 -c driver.c normally produce on a Unix-like system?
  1. An object file named driver.o.
  2. An executable named driver.
  3. Assembly text named driver.s.
  4. A header file named driver.h.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A caller includes a correct prototype for reading, but linking only its object reports an undefined reference to reading. What should be added?
  1. Another compatible prototype, in the caller.
  2. A guard around the existing function declaration.
  3. The object defining reading, in the link inputs.
  4. The same header, as a separate compiled input.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A header declares int classify(double x);, but its source defines double classify(double x). What makes the mismatch visible during compilation of the defining source?
  1. Add the defining source to a later link command.
  2. Put a second prototype only in the calling source.
  3. Add an include guard only around the caller's code.
  4. Include that same header in the defining source.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

What linkage does static int cached = 3; have when declared outside every function?
  1. External linkage.
  2. Internal linkage.
  3. No linkage.
  4. Automatic linkage.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A guarded header has static int errors; and void record(void);. One source includes it and defines record to increment errors; another includes it and calls record twice, then prints its own errors. What is printed, and why?
  1. 2, because both includes name the same shared counter.
  2. 1, because the guard permits only one successful call.
  3. 0, because the two sources have independent counters.
  4. A link error, because both static counters are external.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

What belongs in a header to let several sources use one int failures defined in a single source?
  1. int failures = 0;
  2. extern int failures;
  3. extern int failures = 0;
  4. static int failures = 0;
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

main.c includes "calc.c"; calc.c includes a guarded calc.h and defines public int calc(int n). Building gcc main.c calc.c -o run reports two definitions of calc. What change preserves separate compilation and checks calls correctly?
  1. Include calc.h in main.c, keeping both sources in the build.
  2. Remove calc.c from the build and keep its inclusion in main.c.
  3. Keep both sources and add another include guard to calc.h.
  4. Include main.c inside calc.c so both definitions agree.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

One source defines static int quota = 9; and int quota_value(void) { return quota; }. Another includes a header declaring both extern int quota; and int quota_value(void);, then uses both names. What is the correct interface repair while keeping the stored quota private?
  1. Remove static from the variable and keep all uses of quota.
  2. Add static int quota; to the header and keep direct reads.
  3. Repeat extern int quota; in the caller before every use.
  4. Remove the external variable declaration and use quota_value() to read it.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A header declares size_t packet_count(void);. What makes the header self-contained?
  1. Ask every caller to include <stddef.h> after the header.
  2. Move the declaration into each source with a copied type name.
  3. Include the implementation source before the header in callers.
  4. Include <stddef.h> before that declaration in the header.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

Two sources each include a header with extern int jobs = 1;. One increments jobs, and the other prints it. What repair creates one shared object while keeping both sources type-checked?
  1. Use extern int jobs; in the header and one source definition.
  2. Use static int jobs = 1; in the header and no source definition.
  3. Keep the header unchanged and guard it with a new macro name.
  4. Remove the header and initialize jobs in both sources.
Show answerHide answer

Save this lesson to see this answer.

Set 5: Plan reliable incremental builds10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

What makes an existing ordinary make target need rebuilding after its prerequisites are updated?
  1. Its source file has more lines than before.
  2. A prerequisite has a newer modification time.
  3. Its recipe contains more than one command.
  4. The same command was run on a previous day.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

The executable viewer depends on main.o and screen.o; each object depends on its matching source. Only screen.c becomes newer than its object, and all other inputs are unchanged. What recipes are needed?
  1. Compile main.c, then link viewer.
  2. Compile both sources, then link viewer.
  3. Link viewer without compiling either source.
  4. Compile screen.c, then link viewer.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A source includes settings.h, but its object rule lists only the source file. Why can editing the header leave the program unchanged after make?
  1. Make has no declared path from the header to the object.
  2. Headers are always processed only at program runtime.
  3. The linker automatically restores the previous header contents.
  4. A source include prevents its object from depending on headers.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

For the link rule tool: main.o engine.o, what do $@ and $^ expand to?
  1. main.o and engine.o
  2. tool and main.o
  3. tool and main.o engine.o
  4. main.o engine.o and tool
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

For the rule below, what compiler inputs and output are used when make builds sensor.o? Assume CPPFLAGS = -DTRACE=1 and CFLAGS = -std=c17.

%.o: %.c shared.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
  1. Source sensor.c, output sensor.o, with -DTRACE=1 -std=c17.
  2. Sources sensor.c shared.h, output sensor.o, with -DTRACE=1 -std=c17.
  3. Source sensor.o, output sensor.c, with -DTRACE=1 -std=c17.
  4. Source sensor.c, output shared.h, with -DTRACE=1 -std=c17.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A fresh build has timestamp order shared.h < main.o < helper.o < app. Rules are app: main.o helper.o, main.o: main.c shared.h, and helper.o: helper.c. Only shared.h is then edited. Both sources include it. What happens, and what is wrong?
  1. Both objects rebuild before linking; the dependency graph is complete.
  2. Only helper.o rebuilds before linking; main.o misses a dependency.
  3. Neither object rebuilds; headers cannot trigger object recipes.
  4. Only main.o rebuilds before linking; helper.o misses a dependency.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

Why can a file named clean stop an unmarked clean: rule with no prerequisites from removing objects?
  1. Make reserves every target named clean for built-in compilation.
  2. The shell refuses commands from targets that have matching files.
  3. Make may treat that existing target as already up to date.
  4. The compiler removes cleanup recipes when optimization is enabled.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A project loads generated .d files after its executable rule. main.d lists main.o: main.c a.h; worker.d lists worker.o: worker.c b.h a.h. All outputs are current, then only a.h is edited. What should make -n show?
  1. Compile only main.c, then link; execute none of those recipes.
  2. Compile both sources, then link; execute none of those recipes.
  3. Compile both sources, then link; execute all three recipes.
  4. Link only; execute no recipes because dependency files already exist.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A project has app: main.o work.o, main.o: main.c shared.h, and work.o: work.c. All outputs are current. CFLAGS changes from -O0 to -O2, then only shared.h is edited. Both recipes use CFLAGS. What does ordinary make do, and what ensures consistent new flags?
  1. Rebuild both objects and link; no further rebuild is needed for the new flags.
  2. Rebuild main.o and link; force a full rebuild to update work.o too.
  3. Relink only and keep both objects; change the header again to apply the flags.
  4. Rebuild work.o and link; force a full rebuild to update main.o too.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

app depends on main.o and codec.o. Each object has a complete source/header dependency rule; only codec.h is shared by both sources. After editing that header, why is make -j4 app able to compile concurrently but still link correctly?
  1. The link may start after either object finishes and reuse the other old object.
  2. Each object waits for the other object, then the link starts with both old files.
  3. Both object recipes may run together; the link waits for both to finish.
  4. The link runs first to reserve app, then both object recipes replace it.
Show answerHide answer

Save this lesson to see this answer.

Stuck on your own question? Homework Help

Key Terms

68 Key Terms, each with its definition

A

Argument
An argument is an expression supplied by a call to provide a parameter's initial value.
In cube(5 + 1), the argument is evaluated to 6 before the body of cube starts.
Argument conversion
An argument conversion changes a supplied argument value to the parameter type specified by a visible function prototype.
Passing 5.8 to an int parameter converts it to 5; it does not round to the nearest integer.
auto
The keyword auto explicitly marks automatic storage in C17; C23 also uses it to infer a type from an initializer.
In C17, auto int level; has the same storage behavior as an ordinary local int level;.
Automatic storage duration
Automatic storage duration gives ordinary locals and parameters storage tied to their block execution or function call.
Two active recursive calls have distinct automatic parameters even though their names match.
Automatic variable
An automatic variable is set by make for the current rule, giving values such as its target or prerequisite names.
The automatic variable $@ names sensor.o while its object-file recipe is being executed.

B

Base case
A base case directly answers an input without making another recursive call.
The factorial base case returns 1, allowing the waiting multiplications in earlier calls to finish.
Block scope
Block scope limits a local name to the region from just after its declarator to the end of its containing block.
A variable declared inside an if block cannot be named by a print after that block closes.

C

Call stack
A call stack is the usual implementation structure that tracks active function calls and lets them return in reverse nesting order.
A converter called by a table printer returns before the table printer returns to main.
Caller
A caller is the function that invokes another function and resumes after it returns.
When main calls read_sensor, main is the caller waiting to receive the returned reading.
Conditional compilation
Conditional compilation keeps or skips source branches according to preprocessing tests before the resulting code is compiled.
A debug print removed by conditional compilation is absent from the resulting executable.

D

Declaration
A declaration introduces a name and the type information needed to check its uses.
A declaration above main lets a call be checked even when the function body comes later.
Default goal
The default goal is the target make chooses without an explicit goal argument, normally the first ordinary target in the file.
Putting all or the final executable first prevents an ordinary make run from stopping at one object.
defined operator
The preprocessing defined operator yields one if its macro-name operand is defined and zero if it is not.
Use #if defined(DEBUG) to test existence even when DEBUG has replacement text 0.
Directive
A preprocessing directive begins with # and controls the transformation of source text.
The directive #include "sensor.h" inserts header text into the current preprocessing run.
Dry run
A dry run with make -n prints ordinary build recipes without executing them, exposing the planned compiler and linker commands.
Inspect a dry run after editing a header to see which object files make plans to rebuild.

E

extern
The keyword extern can declare a linked entity without defining an object, provided no initializer makes it a definition.
A shared header declares extern int errors;, while one source file supplies the initialized object definition.
External linkage
External linkage allows declarations in different translation units to identify the same object or function.
A public counter function has external linkage so callers in another source can link to its definition.

F

File scope
File scope extends from an outside-function declaration to the translation unit's end.
A helper defined above a global needs an earlier declaration before it can use that global.
Function
A function is a named block of executable code that performs a task when called.
A temperature-conversion function lets several parts of a program reuse one tested calculation.
Function definition
A function definition supplies the executable body of a function together with its name, return type, and parameter list.
The definition of double scale(double factor) must agree with the prototype included in both caller and implementation.
Function prototype
A function prototype is a declaration that specifies a function's return type and parameter types for checking calls.
The prototype double scale(double factor); tells the compiler to convert its argument to double.
Function-like macro
A function-like macro takes token arguments and substitutes them into replacement text before ordinary C compilation.
Parentheses in a function-like macro preserve grouping but do not stop repeated argument evaluation.

G

Generated dependency file
A generated dependency file records compiler-discovered source and header prerequisites for make to load in later builds.
Compiling with -MMD can write sensor.d, which causes a later header edit to rebuild sensor.o.
Global variable
A global variable is an object declared at file scope, with static storage duration and visibility below its declaration.
Two trip-adding functions can update one global distance total that remains available to main.

H

Header file
A header file shares declarations, macros, or types through textual inclusion.
A conversion header declares both directions of conversion without supplying their compiled bodies.
Hidden dependency
A hidden dependency is an input or state requirement that affects a function without appearing in its parameter list.
A calculation reading a global limit needs that limit arranged before the function can be tested.

I

Include guard
An include guard prevents repeated processing of a header within one translation unit.
Two headers can include a common guarded header without repeating its contents in the same unit.
Internal linkage
Internal linkage confines a name's shared identity to one translation unit, keeping its entity private from other units.
Two files can each define a static helper named check without a clash at link time.

L

Linkage
Linkage determines whether declarations of a name in different places refer to the same object or function.
A global can have static storage duration while static restricts its linkage to one translation unit.
Linker
A linker combines objects and libraries and resolves external names to definitions.
The linker needs the object defining a conversion function even when the caller included its prototype.

M

Make variable
A make variable names reusable text, such as compiler settings or object lists, expanded with a reference like $(CFLAGS).
Changing one CC assignment changes the compiler text used by recipes that reference $(CC).
Module
A module groups related C implementation code in a source file with a matching header.
A counter module can expose two public functions while keeping its stored count private.
Multiple definition
A multiple definition error occurs when a link receives conflicting definitions of the same externally linked name.
Two linked sources both defining main cause a multiple-definition or duplicate-symbol error.

N

No linkage
No linkage means a name identifies its own entity instead of connecting declarations through internal or external linkage.
Ordinary automatic locals and parameters have no linkage even when another function uses the same names.

O

Object file
An object file contains compiled machine code and symbol information that the linker can combine with other objects.
After editing only sensor.c, the build can replace sensor.o while reusing an unchanged main.o.
Object-like macro
An object-like macro associates a name with replacement tokens that are substituted when that name appears later.
The definition #define LIMIT 16 replaces the token LIMIT but leaves "LIMIT" untouched.

P

Parameter
A parameter is a function variable initialized with a copied argument value at entry.
In int cube(int n), the parameter n receives a separate input value for each call.
Pass by value
Pass by value is C's argument-passing rule: each parameter receives a copy of the value computed by its argument expression.
An assignment to parameter n inside a function leaves the caller's ordinary integer variable unchanged.
Pattern rule
A pattern rule uses % to match a shared filename stem so one dependency pattern and recipe can build many targets.
The rule %.o: %.c matches sensor.o with sensor.c by using sensor as the shared stem.
Phony target
A phony target names an action declared with .PHONY, so an existing file with that name cannot suppress its recipe.
Declaring clean phony ensures cleanup still runs if a file named clean appears in the directory.
Predefined macro
A predefined macro is supplied by the implementation, giving information such as the current source line or language version.
A diagnostic using __FILE__ and __LINE__ can identify where its message originates.
Preprocessor
The preprocessor transforms source text through directives such as inclusion, macro replacement, and conditional selection.
Run gcc -E sample.c to inspect the expanded code before the compiler checks C expressions.
Prerequisite
A prerequisite is a declared input or dependency that make checks and updates before deciding whether a target needs rebuilding.
A shared header is a prerequisite of every object whose translation unit includes that header.
Prototype scope
Prototype scope limits a parameter name to the function prototype that declares it.
A prototype may name its parameter value while the definition names the same parameter n.

R

Recipe
A recipe is the shell command sequence attached to a make rule, with ordinary recipe lines beginning with literal tabs.
The recipe for an object compiles its source, while the executable recipe links the needed objects.
Recursion
Recursion occurs when a function invokes itself to solve another instance of a problem.
A digit-summing function can process the last digit and recursively sum the digits that remain.
Recursive case
A recursive case is a branch that solves part of a problem by calling the same function on an input closer to a base case.
Subtracting one from a positive factorial input moves the recursive case toward its stopping test.
register
The keyword register marks a local with an optimization hint that compilers may ignore and forbids taking its address.
Adding register to a loop counter does not force a modern compiler to use a processor register.
Return value
A return value is the result a function supplies to its caller for use in an expression.
The return value of a conversion can be assigned to double result without printing it.
Rule
A make rule names a target, its prerequisites, and any recipe commands needed to update that target from those inputs.
The rule for main.o lists both main.c and the project headers included during its compilation.

S

Scope
The scope of a name is the region of source code where its declaration is visible.
A variable may still exist while its name is hidden by another declaration in a nested block.
Self-contained header
A self-contained header includes the headers required by its own declarations, making special include ordering unnecessary.
A header declaring a function with size_t includes <stddef.h> before that declaration.
Shadowing
Shadowing occurs when an inner declaration hides a visible outer name, causing uses in the inner scope to select the new entity.
Declaring a second total in a loop can leave the intended outer accumulator unchanged.
Shared state
Shared state is retained data that several operations can read or change.
A shared error count makes a later report depend on the earlier functions that updated it.
Side effect
A side effect is a change to program state or an interaction such as output, beyond simply computing an expression's value.
Incrementing a shared counter is a side effect, so the order of separate counter calls can matter.
Stack frame
A stack frame is a record for one active call, typically holding its parameters, automatic locals, and return location.
At the deepest point of factorial(4), four factorial calls have separate active frames.
Static local variable
A static local variable is a block-scope object declared static, retaining one stored value across repeated calls.
A static local ticket counter advances on each call instead of restarting at its initializer.
Static storage duration
Static storage duration means an object exists for the whole program run and is initialized once before program startup.
A file-scope numeric object with no explicit initializer begins at zero and keeps later assignments.
Storage duration
Storage duration describes how long an object's storage exists during execution, independently of where its name is visible.
A static local combines block scope with storage duration lasting the entire program run.
Stringification
Stringification uses # before a macro parameter in replacement text to turn the supplied argument tokens into a string literal.
A diagnostic macro can stringify 3 + 4 to display the expression alongside its computed value.

T

Target
A target is the output file or action that make is asked to update.
The executable target is usually placed first so running make without arguments builds the whole program.
Tentative definition
A tentative definition is a file-scope object declaration without an initializer or extern that can supply a definition.
Putting int count; in a shared header is not a portable replacement for an extern declaration.
Token pasting
Token pasting uses ## in a macro replacement list to combine neighboring tokens into a single preprocessing token.
A macro call joining temp and 2 can form the identifier temp2 in the expanded source.
Translation unit
A translation unit is one source file after preprocessing, together with its included text, translated independently.
Including the same header in two source files contributes declarations to two separate translation units.

U

Undefined reference
An undefined reference is a link failure in which a needed external name has no available definition in the linked inputs.
Omitting sensor.o from the link can leave an undefined reference to a public reading function that only sensor.o defines.
Unspecified argument order
Unspecified argument order means C does not choose a required evaluation order for the argument expressions of one call.
Two ticket-generating calls in one print may provide their numbers in either order.

V

void
The keyword void specifies no returned value as a return type, or no parameters when used alone in a parameter list.
A void printing function performs its output task, but cannot supply the right side of an assignment.

#

#error
The #error directive requires a diagnostic when reached during preprocessing, allowing an unsupported build to be rejected.
A version test can select #error when the chosen C standard is older than the program requires.

About this lesson

By StudeliReport a mistake

Make your own Study Materials

Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.

Get Started