Chapter 1 Summary and Quiz
Review the compile-and-run pipeline, variables as named memory, addresses and the stack, and reading input with scanf.
Programs, variables, and memory recap
You have completed the first chapter of this course. You started with a text file the CPU cannot execute and ended with a program that takes a number from outside itself and writes it into memory you can point at. Let's review the key ideas from each lesson before you test yourself.
Your First C Program
A running program is instructions and data sitting in memory, executed one at a time by the CPU, and C is unusual among modern languages in not hiding that from you. It is why memory is this course's map. Getting from hello.c to something the machine can run takes four stages, and one gcc command runs them all: preprocess (-E) pastes in every #include and #define as pure text substitution, compile (-S) checks your C and translates it into assembly for your CPU, assemble (-c) turns that into machine code in an object file, and link joins your object file to the standard library so that a call to printf reaches code that genuinely exists. That last stage is why undefined reference to 'helper' reads so differently from a compiler error: the source was fine, the body was missing. Inside the program, int main(void) is where execution begins, (void) states that there are no parameters where empty parentheses would only mean "unspecified", and return 0; reports success to the operating system. printf's first argument is a format string: \n is an escape sequence for a single newline, %d is a conversion specifier standing in for an int, and printf adds no newline of its own. That first argument should always be a literal you wrote yourself, because text that arrived from a user belongs in an argument, never in that slot. A specifier with no matching argument is undefined behaviour, meaning the standard places no requirement whatsoever on what the program does, and -Wall reports it with the file, line, column, and a caret under the offending specifier. That is a bug report filed automatically before you ran anything, and warning-free is the only acceptable state for your code.
Variables Are Bytes in Memory
A declaration such as int age = 25; does two jobs. int age reserves a patch of memory big enough for the type and gives it a name, and the initializer = 25 writes a value into that patch. Bytes carry no label saying what they represent, so the type is what decides how many bytes are reserved and how they are interpreted. int holds whole numbers, double holds a very close approximation of a real number rather than always the number itself, and each type has its own specifier: %d for an int, %f for a double, with %.2f style precision controlling the printed decimal places. An assignment names no type, which is exactly what distinguishes it from a declaration, and it overwrites the same bytes: score = score + 5; works out the right-hand side from the current contents and stores the result back over them. sizeof measures the patch. It is an operator rather than a function, worked out while compiling so it costs nothing at run time, and its result has type size_t, which is printed with %zu. The 4 bytes for an int and 8 for a double are facts about this platform, not about C, which guarantees minimum ranges rather than exact widths. A declaration with no initializer reserves the bytes and writes nothing into them, leaving their contents indeterminate, and reading them is undefined behaviour. The same uninitialized program printed count is 0 on three consecutive runs and count is 2 once the optimization level changed, which is the point: a result that looks repeatable proves nothing. Initialize every variable at the point you declare it.
Addresses and the Stack
Memory is one enormous row of bytes numbered from zero upward, and a byte's number is its address, the only name the hardware has for a location. &x is the address of x, the number of the first byte the variable occupies, and %p is how you print one. Always cast the argument, as in printf("%p\n", (void *)&age);, because %p expects an untyped address and this is the one mismatch in the chapter that -Wall -Wextra will not report for you. Addresses appear in hexadecimal behind an 0x prefix, and they change from run to run because the operating system deliberately places a program's memory somewhere new each time it starts, a defence called address space layout randomization. Read a printed address as the answer to "where was it that time", never as a property of the variable. Put the pieces together and a variable is completely described by three facts: & says where it starts, sizeof says how far it extends, and the type says how to read what is in there. Variables declared inside a function live in a region called the stack, which is divided into one frame per function call, so main's locals sit in main's frame and are gone the moment main returns. They cluster together, but their order and spacing inside the frame are the compiler's business, so never write code that assumes a layout.
Reading Input with scanf
scanf is the mirror of printf. It takes a format string too, but its specifiers read instead of write, and the one difference that matters is the reason addresses came first. printf is handed the value because it only reads, while scanf is handed the address because it has to write. A copy of 25 is enough to print, but storing into a copy would change nothing you could see afterwards, so &age gives scanf the location of the real bytes. Leave the & off and you pass a value where an int * is expected: -Wall names the mismatch exactly, and one run of that mistake ended with AddressSanitizer reporting a WRITE memory access to address 0, which is simply the number scanf found in the variable and treated as a destination. Because scanf writes through an address, and an address says nothing about how many bytes live there, its specifiers have to name the type exactly. An int is %d in both directions, but a double is %f to print and %lf to read. One call can perform several conversions given one address each in the same order, and whitespace in the format string skips any run of spaces, tabs, or newlines, so input can be spread across lines freely. scanf returns the number of conversions it completed, and on input it cannot convert it returns a smaller count and leaves those variables untouched, which is precisely why the initializer you wrote at the declaration is what keeps the result meaningful. The chapter fits into one short program:
#include <stdio.h>
int main(void)
{
int age = 0;
int converted = scanf("%d", &age);
printf("scanf converted %d value(s)\n", converted);
printf("age holds %d, lives at %p, and occupies %zu bytes\n", age, (void *)&age, sizeof age);
return 0;
}
Given the input 25:
scanf converted 1 value(s)
age holds 25, lives at 0xfbffb34f0020, and occupies 4 bytes
A variable initialized at its declaration, written through its address by scanf, reported on by a conversion count, and described by its value, its location, and its size.
Key Terminology
- Preprocess, compile, assemble, link: The four build stages, producing expanded text, assembly, an object file, and finally an executable
- Format string: The first argument of
printfandscanf, copied literally except where an escape sequence or a conversion specifier appears - Escape sequence: A backslash spelling of a character with no literal spelling, such as
\nfor a newline - Conversion specifier: A
%placeholder naming a type:%dforint,%fand%lffordouble,%zuforsize_t,%pfor an address - Undefined behaviour: A construct the C standard places no requirement on, so no observed result, however repeatable, is a guarantee
- Declaration: A statement such as
int age = 25;that reserves memory sized for a type and gives those bytes a name - Initializer: The
= 25part of a declaration, the value written into the reserved bytes - Indeterminate: The state of bytes reserved by a declaration with no initializer; reading them is undefined behaviour
- sizeof: The compile-time operator giving the number of bytes a type or variable occupies
- size_t: The unsigned integer type C uses for sizes and counts, printed with
%zu - Address: The number of a byte in memory, the only name the hardware has for a location
- Hexadecimal: Base 16, digits 0 to 9 then a to f, the conventional notation for addresses, marked by an
0xprefix - Address space layout randomization: The operating system's practice of placing a program's memory at a new location on every run
- Stack: The region of memory holding variables declared inside a function
- Frame: The stack's per-call subdivision, holding one call's locals until that call returns
- Conversion count:
scanf's return value, the number of conversions it actually completed
Looking Forward
You can now build a program, reserve memory for a value, find out where that memory is, and let the outside world write into it. What every program so far has in common is that it runs straight through from top to bottom and does the same thing whatever the input. Chapter 2 changes that with the if statement, which is what turns scanf's conversion count from a number you print into a decision the program acts on.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Chapter 1 Summary and Quiz - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!