Introduction
Studying the C language for the first time is being absurdly fun and enriching. Besides being a language that carries decades of computer science history, studying it reveals fundamental concepts that remain present in modern technologies. For this reason, studying it is an enormous pleasure.
In the previous post C — Hello, world!, using the simple Hello, world! program in C as an example, I explored some internal mechanisms of the language — such as the use of standard libraries and the roles of the preprocessor, compiler, and linker.
This sequence of posts (divided into two parts), however, deals solely with the concepts covered in Chapter 1 of the K&R book (The C Programming Language, 2nd Edition), so more advanced features of the C language are not discussed, such as pointers, structures (structs), or deeper details about memory management. The chapter's focus is to present the fundamentals of the language through small programs and exercises.
Despite the simplicity of the examples, some parts tend to go unnoticed by beginners — especially considering that the book presents several concepts implicitly.

My impressions on typing and runtime
It was a surprise to discover that C is considered a weakly typed language, while Python is strongly typed. I believe my surprise comes from the fact that in C the language usually demands that the programmer be explicit; however, it surprised me to realize how many implicit conversions C allows.
Examples:
int x = 65;
char c = x; // int silently becomes a char → 'A'
printf("%c", x); // prints 'A', without needing a conversion
# explicit conversion required
int("5") + 3 # output: 8
str(5) + "abc" # output: "5abc"
There are much more interesting and sophisticated examples of implicit conversions in C, but these are already sufficient to illustrate the idea discussed in this post.
An interesting point I noticed when learning about printf is that, in C, much type information exists only during compilation. When the program is running, a value stored in memory does not necessarily carry information about its type. To the processor, those are just bits.
That is why, when writing printf("%d %f", x, y);, the format string does not only serve to organize the output. It also tells printf how to interpret the received arguments. The %d indicates that the next argument should be treated as an integer; %f, as a floating-point number.
Runtime and ASCII
Illustrating in practice, during the execution of a C program, the value 65 can be interpreted in different ways depending on context: as the integer 65, as the character 'A' in the ASCII table, as the hexadecimal value 0x41, among other possibilities.
In languages that keep more information available at runtime, like Python, objects carry metadata associated with them. In a simplified way, we can imagine something like:
object
├── value: 65
└── type: int
This allows generic functions to automatically discover how to handle a value without the programmer needing to provide extra instructions, as happens with print().
This difference illustrates two distinct execution models. While Python keeps more information available at runtime, C delegates a larger portion of that work to the compilation phase, resulting in smaller, simpler programs with fewer abstractions between the code and the machine.
The design of printf reveals an important characteristic of C: during execution, a value does not necessarily "know" what it is. That is why it falls to the programmer to provide extra information so that generic functions can correctly interpret the data received.
Character streams
One of the first fascinating concepts that K&R covers is character streams. The standard library treats text input and output as character flows, regardless of where that data comes from or where it goes.
From this simple model, the book shows how it is possible to create small and useful programs. Many examples are inspired by traditional Unix tools, such as line, word, and character counters.
The central idea is that these programs do not need to know the origin of the data. They simply receive a sequence of characters, process that sequence, and produce an output. This abstraction is one of the characteristics that made Unix tools so powerful: small programs can be combined to accomplish larger tasks.
Based on my reading, the concept in the example below is one of the most valuable in chapter 1:
#include <stdio.h>
/* copy input to output; 2nd version */
main()
{
int c;
while ((c = getchar()) != EOF)
putchar(c);
}
At first, I was surprised by the number of important concepts hidden in this extremely simple example. Although the code only copies input to output, it presents a fundamental pattern: consuming data from a stream, processing it as it arrives, and producing a result.
Many of the exercises in this chapter reuse this same idea in different ways: instead of just copying characters, the program starts counting, filtering, transforming, or analyzing that data.
while ((c = getchar()) != EOF)
The line while ((c = getchar()) != EOF) reveals important details about input in C.
The while creates a loop that will keep executing as long as getchar() returns a value different from EOF. The getchar() function reads a character from the input and returns its value; when there is no more data available, it returns a special value called EOF (End-of-File), indicating that the input stream has ended.
It is important to note that EOF is not a character. It is not stored at the end of a file nor is it a symbol the program receives. It is a special value used by the standard library input functions to indicate that reading cannot continue.
In the terminal, a common way to send this signal is by using CTRL+D on Unix systems, which informs the system that there is no more data available on the input.
Another brilliant detail is that the assignment happens inside the while condition. First the program reads a character and stores it in c; then it compares that value with EOF. This allows the reading itself to control the continuation of the loop, avoiding an extra call to getchar() just to check whether the input has ended.
And the int c?
I may seem repetitive when I say I was surprised, but C really did surprise me quite a lot during my studies.
Faced with the first examples, I came across the variable c being declared as int. The doubt was instantaneous: if we are reading characters, why not use char?
The answer introduces an important concept: in C, a character is internally represented by a number. The char type is a small integer type used to store these values, and conventions like the ASCII table define a relationship between numbers and characters. Example in practice:
char c = 65;
printf("%c", c); // output: 'A'
printf("%d", c); // output: 65 in ASCII
The ASCII table is a convention created to standardize this representation. For example, within that table, the letter 'A' corresponds to the number 65. This means that, to the computer, the letter is not stored as a special entity called "A"; it is stored as a sequence of bits that, following the ASCII convention, represents the numeric value 65, associated with the symbol 'A'.
The original ASCII table uses 128 possible values (0 to 127) to represent all its characters.
That fact alone does not explain the reason for int c, after all, if char is represented as a number, why does getchar() need to return int and not char?
The answer begins to reveal an important characteristic of C: values are not just abstract concepts, they have a physical representation in memory. The size of the variable matters, because it defines which values it can store.
A char normally occupies 1 byte, while an int occupies more space. Additionally, there is the difference between signed and unsigned types: signed types can represent both negative and positive values, while unsigned types represent only positive values.
The key to understanding char lies here: it needs to represent characters. In the case of the original ASCII table, that means values from 0 to 127. However, getchar() does not return only characters; it also needs to return a special value called EOF, which indicates that there is no more data to read.
Since EOF has the value -1, it needs to be different from all possible characters. That is why getchar() uses int, which has enough space to represent all characters and also that special value.
Exercise 1-9. Write a program to copy its input to its output, replacing each string of one or more blanks by a single blank.
In this exercise the statement asks that the input be copied to the output, however, replacing consecutive spaces with just a single space.
Example:
In practice, if the input is hello world from C — the output should be hello world from C.
Solution
#include <stdio.h>
int main() {
int c, past_c;
past_c = 0;
for (; (c = getchar()) != EOF; ) {
if (c == ' ') {
if (past_c == ' ') {
continue;
}
}
putchar(c);
past_c = c;
}
return 0;
}
/* Equivalent more compact version.
* The idea is simple: print the character, except when the current character
* and the previous character are both spaces.
if (!(c == ' ' && past_c == ' ')) {
putchar(c);
}
*/
Concepts applied in this exercise:
- Character stream processing.
- Accumulated context.
- Decision-making based on the current character and previous information.
This exercise introduces an important idea — the current character does not always contain enough information for the program to make a decision; sometimes it is necessary to preserve some information about what happened previously.
In the next example, this same idea appears in a more general form through the concept of state machines.
State machines
The next powerful example shown by K&R is the introduction to the concept of state machines. In the example below, we have a program that counts lines, words, and characters received through the input.
Therefore, if the input is hello world — the output should be 1 2 12 (1 line break, 2 words, 12 characters). The total number of characters is 12 because the line break \n is also part of the input. When pressing Enter, the terminal sends this control character to the program, which is also counted by the counter.
#include <stdio.h>
#define IN 1 /* inside a word */
#define OUT 0 /* outside a word */
/* count lines, words, and characters in input */
main()
{
int c, nl, nw, nc, state;
state = OUT;
nl = nw = nc = 0;
while ((c = getchar()) != EOF) {
++nc;
if (c == '\n')
++nl;
if (c == ' ' || c == '\n' || c == '\t')
state = OUT;
else if (state == OUT) {
state = IN;
++nw;
}
}
printf("%d %d %d\n", nl, nw, nc);
}
This example clearly demonstrates how programs can use state machines to maintain information about the current context and make future decisions. The received character alone is not sufficient to determine the behavior of the program; it is necessary to know which state it is in. In this case, the program maintains two possible states: inside a word (IN) or outside a word (OUT). From that information, it can decide when a new word has begun and increment the counter correctly.
Exercise 1-12. Write a program that prints its input one word per line.
Here the statement asks for something simple: break the line at the end of each word found.
Example
If the input is hello world — the output should be:
hello
world
This was the first exercise in the book where I really racked my brain. My first instinct was to print a line break every time I found a space — and that is exactly what does not work. The problem is that spaces can appear consecutively: two spaces in a row, a tab after a line break, and so on. If I broke the line for every separator, the program would print blank lines where it should not.
The part that took a while to make sense was realizing that the program needed to store context information: not just "which character am I reading now?", but "what was the current context of the program when it received this character?". The state exists precisely for this. When a separator is found while the state is IN, it means a word has ended — and only at that moment should the line break happen. Consecutive separators are silently ignored, because the state is already OUT.
The difficulty was not detecting characters or separators, but realizing that an action can depend on the relationship between the current character and the state the program was in.
Solution
#include <stdio.h>
#define IN 1
#define OUT 0
// Exercise 1-12. Write a program that prints its input one word per line.
int main() {
int c, state;
state = OUT;
while ((c = getchar()) != EOF) {
if (c != ' ' && c != '\n' && c != '\t') { // If "c" is not a separator: we found a character of a word
putchar(c);
state = IN;
}
if (c == ' ' || c == '\n' || c == '\t') { // If "c" is a separator, we check if a word has ended
if (state == IN) { // [!] Did the separator close a word that was being read?
printf("\n"); // If so, break the line
state = OUT;
}
}
}
return 0;
}
Concepts applied in this exercise:
- State machine (IN / OUT).
- State as a representation of the current context of the program.
- Identification of state transitions.
- Word counting based on context changes.
This is probably exactly the shift in mindset that the exercise proposes. Initially it seems like each character should generate an individual action: if it is a letter, print it; if it is a space, break the line. However, a space alone does not mean a word has ended. It only represents the end of a word when the program knows it was inside a word before it.
That is the central idea of state: storing the current context of the program while it processes a sequence of data. When a separator ends a word, the program acts.
Final thoughts
Upon finishing this first part of the chapter, I had the impression that K&R teaches much more than it appears to. The examples are small, but they hide concepts that help understand how programs really work.
Understanding why getchar() returns an int, why there is an EOF, or why a program needs to maintain a state does not only serve to solve exercises. It serves to build a mental model about which information the computer has, which it does not have, and how programs transform data into decisions.
Although the chapter is an introduction to the C language, the feeling is that it is teaching something more fundamental: a way of thinking about software.
The next post about Chapter 1 of K&R will cover concepts and exercises involving arrays in C. It was precisely in that part that I began to notice how many things the book considers obvious and leaves implicit, so there is still plenty of interesting detail to capture.