Welcome to your fourth lesson. In our last session, we focused on project structure, learning how to use header guards to prevent multiple inclusion errors and keep our code clean and scalable. We've managed the structure of our code; now, it's time to manage the data within it.
This lesson marks our deep dive into the C type system, a topic you've highlighted as important for reinforcing your understanding. We will explore C's fundamental integer types—short, int, long, and their unsigned variants. You will learn that unlike in some other languages, the size of these types is not absolute. By the end of this lesson, you will be able to explain why their sizes can vary across different systems and use the sizeof operator to verify their memory footprint on your own machine. This knowledge is the essential first step toward mastering memory management and writing efficient, portable code.
The Integer Family: A Trade-off Between Range and Memory
In systems programming, and especially in game development, every byte of memory can count. C provides a family of integer types to give you, the programmer, precise control over this trade-off. A variable that only needs to store a player's level (e.g., 1-99) doesn't require the same amount of memory as one storing, for instance, a high score that could run into the billions.
The basic integer types you'll work with, in increasing order of guaranteed minimum size, are:
charshort int(or justshort)intlong int(or justlong)long long int(or justlong long)
Each of these types can be modified with the signed or unsigned keyword.
signed(the default): The variable can hold both positive and negative values.unsigned: The variable can only hold non-negative values (zero and positive). This effectively doubles the maximum positive value the type can store compared to its signed counterpart.
The following video gives a fast-paced overview of these types, their typical memory usage, and the crucial unsigned modifier.
The video "C data types" by Bro Code provides a clear, example-driven tour of the integer family.
Please watch the following segments to see these types in action: char and unsigned char: Notice how unsigned doubles the positive range from [-128, 127] to [0, 255]. short: Observe its 2-byte size and corresponding limited range. int: The most common integer type, typically 4 bytes. long long: Used for extremely large numbers, occupying 8 bytes.
Choosing unsigned is a logical decision when you know a value will never be negative, such as a player's health, an item count in an inventory, or a timer. This not only communicates the variable's purpose but also gives you a larger positive range for the same amount of memory.
The Portability Problem: A long isn't always a long
A critical concept in C is that the standard does not strictly define the exact byte size for types like int and long. It only guarantees minimum ranges, such as:
shortis at least 16 bits (2 bytes).intis at least 16 bits (2 bytes).longis at least 32 bits (4 bytes).long longis at least 64 bits (8 bytes).
The actual size of these types depends on the combination of the CPU architecture (32-bit vs. 64-bit) and the compiler/OS conventions. This is known as the data model. The three most common data models you will encounter are:
| Data model | int |
long |
long long |
pointer (void*) |
Common On |
|---|---|---|---|---|---|
| ILP32 | 32-bit | 32-bit | 64-bit | 32-bit | 32-bit systems (Windows, Linux, macOS) |
| LP64 | 32-bit | 64-bit | 64-bit | 64-bit | 64-bit systems (Linux, macOS) |
| LLP64 | 32-bit | 32-bit | 64-bit | 64-bit | 64-bit Windows |
Notice the key difference: on 64-bit Linux and macOS, a long is 64 bits, but on 64-bit Windows, a long is still 32 bits. This is a classic source of bugs when porting code between operating systems.
The following article explains this concept brilliantly.
This article by Nick Desaulniers is a fantastic exploration of why type sizes vary and how data models like LP64 and LLP64 are the cause.
Read the article from the beginning to the Conclusion. Pay close attention to the code examples where the author compiles the same program for different architectures (-m32 flag) and the summary table that defines the data models.
The Solution: Verifying Sizes with the sizeof Operator
Since we cannot assume the size of a type, C provides an operator to ask the compiler directly: sizeof. This operator returns the size of a type or a variable in bytes. It is a compile-time operator, meaning the values are resolved when you build your program, not when you run it.
This next video provides a comprehensive tutorial on how to use sizeof.
sizeof Operator | C Programming Tutorial
The video "sizeof Operator | C Programming Tutorial" from Portfolio Courses explains the operator's function, return type, and usage with types, variables, and arrays.
Please watch from the start of the video. Focus on these key points: How to use sizeof with a type name, like sizeof(int). The fact that its return type is size_t, an unsigned integer type, and why the correct printf format specifier is %zu. The technical detail that sizeof returns "char-sized units," which on virtually all modern systems is equivalent to bytes. How to use sizeof with a variable, like sizeof(x).
Practical Verification
Let's put this knowledge into practice. The following program uses sizeof to print the memory footprint of all the integer types on your system.
#include <stdio.h>
int main(void) {
printf("--- Integer Type Sizes on This System ---\n");
printf("Size of char: \t\t%zu byte(s)\n", sizeof(char));
printf("Size of short: \t\t%zu byte(s)\n", sizeof(short));
printf("Size of int: \t\t%zu byte(s)\n", sizeof(int));
printf("Size of long: \t\t%zu byte(s)\n", sizeof(long));
printf("Size of long long: \t%zu byte(s)\n", sizeof(long long));
printf("Size of a pointer: \t%zu byte(s)\n", sizeof(void*));
printf("\n--- Unsigned Variants ---\n");
printf("Size of unsigned char: \t\t%zu byte(s)\n", sizeof(unsigned char));
printf("Size of unsigned short: \t%zu byte(s)\n", sizeof(unsigned short));
printf("Size of unsigned int: \t\t%zu byte(s)\n", sizeof(unsigned int));
printf("Size of unsigned long: \t\t%zu byte(s)\n", sizeof(unsigned long));
printf("Size of unsigned long long: \t%zu byte(s)\n", sizeof(unsigned long long));
return 0;
}
Your Task:
- Save the code above into a file named
sizes.c. - Compile and run it on your machine:
gcc -Wall -Wextra -Werror -o sizes sizes.cfollowed by./sizes. - Observe the output. Based on the data models table, can you identify which model your system uses? (If you are on a modern 64-bit Mac or Linux machine, you will likely see
longas 8 bytes, indicating LP64. If you are on 64-bit Windows, you will likely seelongas 4 bytes, indicating LLP64). - Bonus Challenge: If you are on Linux or macOS, re-compile the program for a 32-bit architecture using the
-m32flag:gcc -m32 -o sizes32 sizes.c. Run./sizes32and compare the output. You should see the sizes change to match the ILP32 data model.
This simple exercise provides definitive proof that type sizes are not universal and demonstrates your power to verify them for any platform you target. The image below summarizes the typical sizes and ranges you'll find on common 32-bit and 64-bit systems.

Conclusion
In this lesson, you've taken a significant step toward understanding how C handles data at a low level. You now know that C offers a variety of integer types to balance memory usage and value range, and that the unsigned modifier is a powerful tool for both documenting intent and expanding the positive range of a variable.
Most importantly, you've learned that you cannot make assumptions about type sizes. They are dictated by the target system's data model. The sizeof operator is your essential tool for writing portable code by allowing you to programmatically determine the exact memory footprint of any data type.
Key Takeaways:
- C's integer types (
char,short,int,long,long long) provide a trade-off between memory use and the range of values they can store. - The
unsignedkeyword restricts a type to non-negative values, doubling its maximum positive value. - The exact size of integer types is not fixed by the C standard but is determined by the system's data model (e.g., ILP32, LP64, LLP64).
- The
sizeofoperator is a compile-time tool that returns the size of a type or variable in bytes, enabling you to write portable and memory-aware code.
Now that we understand that each integer type has a fixed, finite size on a given system, a crucial question arises: what happens if we try to store a number that is too large for its container? This phenomenon, called integer overflow, is the subject of our next lesson.