- Learn
- Computer Science & IT
- Programming
- Programming in C
- Functions and Program Structure in C
Functions and Program Structure in C
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
- 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 resultWrite function definitions with a return type, parameters and a body, use
returnandvoidcorrectly, and call functions inside expressions. - Declare functions with prototypesDeclare 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 resultExplain 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 recursionFollow 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.
- Define functions that take inputs and return a result
- 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 classesDistinguish 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 carefullyDefine and use file-scope (global) variables, explain their visibility and initialization, and prefer parameters and return values where a global would add hidden coupling.
- Decide where a name can be used (scope)
- Use #include, #define and macros safely, and select code at compile time with conditional directives, predefined macros and compiler flags.
- Include files and define macrosExplain what the preprocessor does before compilation, include headers, define constant and function-like macros, and avoid the classic macro pitfalls.
- Compile code conditionallyUse #if, #ifdef, #ifndef, #elif, #else and #endif with -D flags and predefined macros to include or exclude code at compile time, including include guards.
- Include files and define macros
- 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 filesDescribe the stages from source file to executable, compile files separately into object files, link them, and tell compile errors from linker errors.
- Write header filesPut 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 externUse 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.
- Compile and link several source files
- 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 dependenciesWrite 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 rulesUse variables such as
CCandCFLAGS, the automatic variables$@,$<and$^, pattern rules, built-in rules and phony targets to keep a makefile short and correct.
- Write makefile rules and dependencies
Functions and Program Structure in C study guide
1Write, 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.
returnends the call; printing does not supply its result.- Use
voidfor 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
| Declaration | C17 meaning | C23 meaning |
|---|---|---|
int read_sensor(void); | No parameters | No parameters |
int read_sensor(); | Parameter information unspecified | No 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 statement | Effect 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 call | Work 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.
2Control 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
| Specifier | Meaning in this C17 code |
|---|---|
auto | Explicit automatic storage for a block variable; usually omitted |
register | A hint about fast access; taking its address is forbidden |
static inside a function | Retained storage with a local name |
static at file scope | Internal linkage, keeping the name within its translation unit |
extern | A 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
staticdetermines 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.
3Use 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
| Name | Information |
|---|---|
__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
#ifdeftests existence;#iftests a value.- Undefined identifiers in
#ifevaluate as zero. -DNAME=valuesupplies a build-time macro definition.- Include guards prevent repeated processing within one translation unit.
- Predefined macros expose file, line, build-time, and standard information.
4Split 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.
4.1 Compile and link several source files
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.
| Stage | Result | GCC option that stops there |
|---|---|---|
| Preprocessing | Expanded C text | -E |
| Compiling | Assembly text | -S |
| Assembling | Object file | -c |
| Linking | Executable | Normal 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.
-cproduces 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 header | Keep in a source |
|---|---|
| Function prototypes | Externally linked function bodies |
extern variable declarations | Shared object definitions |
| Needed macros and types | Private 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,
externdeclarations, 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
| Linkage | Meaning |
|---|---|
| External | Declarations can refer to one entity across translation units |
| Internal | Declarations refer to an entity within one translation unit |
| None | A 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
externdeclarations and one definition. - File-scope
staticmakes 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
externobject declaration a definition. - Local
staticretention and file-scopestaticlinkage have different roles.
5Build 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; reusemain.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
| Variable | Value 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
.dfiles record user-header dependencies. - Force or clean a rebuild after changing compiler flags.
Functions and Program Structure in C examples
1Build and trace a function call
19 steps
- 1IntroductionStart 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.
- 2ExampleGive 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; }intbefore the name describes the result.int readingdeclares a parameter, and the body computes the returned value. Define this function outside all other function bodies; standard C does not support nested definitions. - 3ExampleUse a call as an expression
For
int result = corrected(9);, the argument9initializes the parameterreading. The return expression produces13, and the caller's assignment stores that value inresult.The call can also sit inside another expression:
2 * corrected(9)gives26. Writing onlycorrected(9);still runs the call but discards its returned value. - 4TipChoose 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
voidcan 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 appropriatereturn; displaying a number does not supply it to the caller. - 5ChallengeCheck 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 gives13, integer division gives6, and return conversion gives6.0. Use2.0to make the division floating-point if you need6.5. The return type cannot restore a fraction already discarded. - 6ExplanationTell 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. - 7Key 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. - 8ChallengeTrace a permitted conversion
The compiler has seen
int corrected(int reading);. What doescorrected(6.8)return, and must the call be rejected?---reveal---
The call can compile. The prototype converts
6.8to the integer6, then the function adds4and returns10. 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. - 9ExampleDraw two separate values
Consider a different function:
void change(int reading) { reading += 4; }The caller runs
int sample = 9; change(sample);. At entry,sampleand the parameter each hold9in separate objects. The body changes its parameter to13. When the call returns,samplestill holds9: there was no assignment to it. - 10ConnectionLet 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 updatessample. - 11ChallengeFind the discarded call
Starting with
int sample = 7;, the caller runs:corrected(sample); sample = corrected(sample);What is
sampleafterward?---reveal---
The first call computes
11and discards it, leavingsampleat7. The second also receives7, returns11, and this time the caller stores it. The final value is11, not15; a call does not update its argument variable merely by returning a changed value. - 12ExplanationAdd 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.
- 13ExampleWrite the calls on the way down
Trace
total_to(3). The first call holdsn == 3and waits to add3tototal_to(2). That call holds its ownn == 2and waits fortotal_to(1).The call with
n == 1waits fortotal_to(0), which returns0immediately. At that deepest point, four calls are active. Do not replace the earlier values ofnwith zero; they belong to different calls. - 14ExampleFill the results on the way back
Once
total_to(0)returns0, the waiting work can finish:- The call with
n == 1returns1 + 0, or1. - The call with
n == 2returns2 + 1, or3. - The call with
n == 3returns3 + 3, or6.
Most implementations keep these active calls in stack frames. Each return resumes the caller after its call, then releases that call's automatic state.
- The call with
- 15ChallengeCheck whether a base case is reachable
Suppose the base test in
total_tois changed ton == 0, and the recursive step still usesn - 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. Then <= 0version stops this input immediately. - 16TipCheck 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 offactorial(13)exceeds the maximum2147483647.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.
- 17TipSeparate 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 changesiwithout the required sequencing. Pass by value does not make an unsafe argument expression safe. - 18ChallengeCombine declaration, conversion, and calls
With
total_todeclared before use, a caller writesint x = 2; total_to(x); x = total_to(3.9);. What is the finalx?---reveal---
The first result is discarded, so
xstays2. The second argument converts to3; the recursive trace returns6, which the assignment stores inx. The declaration controls the conversion, recursion computes the value, and the caller decides whether to keep it. - 19RecapUse 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.
2Follow names and retained values
13 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionStart with a small task
- 2ExampleRead the nearest visible declaration
- 3ExplanationKeep the scope where the function was written
- 4ChallengeRepair a value needed after a block
- 5ExampleWatch an automatic counter restart
- 6ExampleRetain the counter with static
- 7ChallengeDistinguish initialization from assignment
- 8TipRead the keyword in its location
- 9ExampleTrace a global update
- 10ChallengeFind the hidden local
- 11ConnectionMake a calculation easier to test
- 12ChallengeCombine scope and remembered state
- 13RecapAnnotate names and lifetimes separately
3Inspect code before compilation
13 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionStart with a small task
- 2ExplanationSee what inclusion actually supplies
- 3ExampleReplace tokens before using operator rules
- 4ExampleProtect inner and outer grouping
- 5ChallengeFind what parentheses cannot repair
- 6TipKeep macro rules separate from scope rules
- 7ExampleTest a setting by existence and value
- 8ChallengeSelect a branch from a compiler flag
- 9ExampleWalk through the first header inclusion
- 10Key InsightStart a fresh guard state in another unit
- 11ConnectionLocate a diagnostic in the source
- 12ChallengeCombine expansion and branch selection
- 13RecapInspect the transformation in order
4Build one program from two sources
14 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionStart with a small task
- 2ExplanationFollow one translation unit to an object
- 3ExampleWrite the shared declaration
- 4ExamplePut the implementation in its own source
- 5ChallengePlace one shared variable correctly
- 6ExampleCompile separately, then link
- 7ChallengeRepair an undefined reference
- 8TipDo not copy the implementation into the caller
- 9ExampleKeep a counter inside the module
- 10ExplanationSeparate three properties of the counter
- 11ChallengeExplain two independent counters
- 12TipMake a header work on its own
- 13ChallengeRepair privacy and interface together
- 14RecapMatch each need to its build stage
5Build only what changed
15 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionStart with a small task
- 2ExampleRecord the executable and object inputs
- 3ExampleGive an object a recipe
- 4ExampleRun the first build and a source rebuild
- 5ChallengeTrace a shared-header edit
- 6TipInspect the plan before running it
- 7ExplanationReplace repeated settings with variables
- 8ExampleExpand the link recipe
- 9ExampleUse one compile pattern for both objects
- 10ChallengeChoose a source without compiling the header
- 11TipMake cleanup an action
- 12ConnectionLet the compiler record header dependencies
- 13ChallengeKeep a parallel rebuild correct
- 14ChallengeApply changed flags to the whole program
- 15RecapCheck the graph before trusting the output
Functions and Program Structure in C practice questions
Set 1: Follow values through function calls10 Practice Questions
Question 1 of 10
int triple(int value) { return value * 3; }?Show answerHide answer
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
void report(int code) allow a caller to do?Show answerHide answer
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
double portion(int total) { return total / 4; } return for portion(11)?Show answerHide answer
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
int capacity(void) appears below main. What declaration above main fully describes a no-input call in C17?Show answerHide answer
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
int fifth(int n) { return n / 5; } declared before use, what is fifth(-14.8)?Show answerHide answer
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
double stretch(int n);. What definition is compatible with that declaration?Show answerHide answer
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);
}
Show answerHide answer
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);
Show answerHide answer
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);
}
Show answerHide answer
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);
}
Show answerHide answer
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
i from for (int i = 0; i < 6; i++) { } be used?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);
}
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);
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
int amount = amount + 2; inside a block fail to initialize from an outer amount?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
static int calls = 0; initialized during one program run?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;
}
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;
}
Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
int permitted(int n) reads a global limit. Why might the same argument produce a different result on a later call?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);
}
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
read_sensor() at runtime. Why is static int first = read_sensor(); invalid in C17?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
#define LIMIT 18 replace in later source code?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
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
#define LIMIT 12; make int count = LIMIT * 2; invalid?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);
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))
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
#define TRACE 0, what does #ifdef TRACE do?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
#if VERBOSE silently skip a branch when the build defines only VERB0SE?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
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));
}
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
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?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
gcc -std=c17 -c driver.c normally produce on a Unix-like system?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
reading, but linking only its object reports an undefined reference to reading. What should be added?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
int classify(double x);, but its source defines double classify(double x). What makes the mismatch visible during compilation of the defining source?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
static int cached = 3; have when declared outside every function?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
int failures defined in a single source?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?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
size_t packet_count(void);. What makes the header self-contained?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
extern int jobs = 1;. One increments jobs, and the other prints it. What repair creates one shared object while keeping both sources type-checked?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
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
settings.h, but its object rule lists only the source file. Why can editing the header leave the program unchanged after make?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
tool: main.o engine.o, what do $@ and $^ expand to?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 $@
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
clean stop an unmarked clean: rule with no prerequisites from removing objects?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
.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?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
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?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?Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
A
Argument
cube(5 + 1), the argument is evaluated to 6 before the body of cube starts.Argument conversion
5.8 to an int parameter converts it to 5; it does not round to the nearest integer.auto
auto explicitly marks automatic storage in C17; C23 also uses it to infer a type from an initializer.auto int level; has the same storage behavior as an ordinary local int level;.Automatic storage duration
Automatic variable
$@ names sensor.o while its object-file recipe is being executed.B
Base case
1, allowing the waiting multiplications in earlier calls to finish.Block scope
if block cannot be named by a print after that block closes.C
Call stack
main.Caller
main calls read_sensor, main is the caller waiting to receive the returned reading.Conditional compilation
D
Declaration
main lets a call be checked even when the function body comes later.Default goal
all or the final executable first prevents an ordinary make run from stopping at one object.defined operator
defined operator yields one if its macro-name operand is defined and zero if it is not.#if defined(DEBUG) to test existence even when DEBUG has replacement text 0.Directive
# and controls the transformation of source text.#include "sensor.h" inserts header text into the current preprocessing run.Dry run
make -n prints ordinary build recipes without executing them, exposing the planned compiler and linker commands.E
extern
extern can declare a linked entity without defining an object, provided no initializer makes it a definition.extern int errors;, while one source file supplies the initialized object definition.External linkage
F
File scope
Function
Function definition
double scale(double factor) must agree with the prototype included in both caller and implementation.Function prototype
double scale(double factor); tells the compiler to convert its argument to double.Function-like macro
G
Generated dependency file
-MMD can write sensor.d, which causes a later header edit to rebuild sensor.o.Global variable
main.H
Header file
Hidden dependency
I
Include guard
Internal linkage
check without a clash at link time.L
Linkage
static restricts its linkage to one translation unit.Linker
M
Make variable
$(CFLAGS).CC assignment changes the compiler text used by recipes that reference $(CC).Module
Multiple definition
main cause a multiple-definition or duplicate-symbol error.N
No linkage
O
Object file
sensor.c, the build can replace sensor.o while reusing an unchanged main.o.Object-like macro
#define LIMIT 16 replaces the token LIMIT but leaves "LIMIT" untouched.P
Parameter
int cube(int n), the parameter n receives a separate input value for each call.Pass by value
n inside a function leaves the caller's ordinary integer variable unchanged.Pattern rule
% to match a shared filename stem so one dependency pattern and recipe can build many targets.%.o: %.c matches sensor.o with sensor.c by using sensor as the shared stem.Phony target
.PHONY, so an existing file with that name cannot suppress its recipe.clean phony ensures cleanup still runs if a file named clean appears in the directory.Predefined macro
__FILE__ and __LINE__ can identify where its message originates.Preprocessor
gcc -E sample.c to inspect the expanded code before the compiler checks C expressions.Prerequisite
Prototype scope
value while the definition names the same parameter n.R
Recipe
Recursion
Recursive case
register
register marks a local with an optimization hint that compilers may ignore and forbids taking its address.register to a loop counter does not force a modern compiler to use a processor register.Return value
double result without printing it.Rule
main.o lists both main.c and the project headers included during its compilation.S
Scope
Self-contained header
size_t includes <stddef.h> before that declaration.Shadowing
total in a loop can leave the intended outer accumulator unchanged.Shared state
Side effect
Stack frame
factorial(4), four factorial calls have separate active frames.Static local variable
static, retaining one stored value across repeated calls.Static storage duration
Storage duration
Stringification
# before a macro parameter in replacement text to turn the supplied argument tokens into a string literal.3 + 4 to display the expression alongside its computed value.T
Target
Tentative definition
extern that can supply a definition.int count; in a shared header is not a portable replacement for an extern declaration.Token pasting
## in a macro replacement list to combine neighboring tokens into a single preprocessing token.temp and 2 can form the identifier temp2 in the expanded source.Translation unit
U
Undefined reference
sensor.o from the link can leave an undefined reference to a public reading function that only sensor.o defines.Unspecified argument order
V
void
void specifies no returned value as a return type, or no parameters when used alone in a parameter list.void printing function performs its output task, but cannot supply the right side of an assignment.#
#error
#error directive requires a diagnostic when reached during preprocessing, allowing an unsupported build to be rejected.#error when the chosen C standard is older than the program requires.About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.