Chapter 4: Data Types and Variables
C programs work with values of specific types. Each type controls how many bytes a value uses, how it is represented in memory, the operations that make sense, and how input and output behave. This chapter introduces the fundamental arithmetic types, the common modifiers and qualifiers, rules for scope and lifetime, and how conversions and promotions occur during expression evaluation.
sizeof at compile time and the macros in <limits.h> and <float.h>.
Primitive types
C defines arithmetic types for integers and real numbers. The core set that appears in almost every program includes char, int, float, and double. There is also long double for higher precision where supported.
char and character representation
char is an integer type large enough to hold any basic execution character. It can be signed or unsigned; this is implementation defined. Character constants like 'A' and escape sequences like '\n' have type int in older code and type int or char depending on context; prefer using unsigned char for raw byte data.
// Inspect the signedness of char at compile time
#include <stdio.h>
int main(void) {
if ((char)-1 < 0) {
printf("char is signed\n");
} else {
printf("char is unsigned\n");
}
return 0;
}
unsigned char. This avoids negative values that can surprise bitwise code.
Integer types built around int
Integer types come in sizes related to short, int, long, and long long. Each can be signed or unsigned. The rank of these types controls promotions and conversions later in this chapter.
| Category | Examples | Typical width | Size check at compile time |
| Small | signed char, unsigned char, short |
8 to 16 bits | sizeof(signed char) == 1 by definition |
| Regular | int, unsigned int |
16 to 32 bits | sizeof(int) is implementation defined |
| Wide | long, unsigned long, long long |
32 to 64 bits | sizeof(long), sizeof(long long) |
int is 32 bits. On some systems long is 32 bits and on others it is 64 bits. Use <stdint.h> types like int32_t and uint64_t when you need exact widths.
Floating types
Floating types usually follow IEEE 754 binary formats. The precision and range are available as macros such as FLT_DIG, DBL_MAX, and LDBL_EPSILON from <float.h>. Conversions between integers and floating values are covered in section c04d.
#include <float.h>
#include <stdio.h>
int main(void) {
printf("float digits: %d\n", FLT_DIG);
printf("double max: %e\n", DBL_MAX);
return 0;
}
Inspecting sizes and limits
Use sizeof and the limits headers to discover properties at compile time and run time.
#include <stdio.h>
#include <limits.h>
#include <float.h>
int main(void) {
printf("sizeof(int) = %zu\n", sizeof(int));
printf("INT_MAX = %d\n", INT_MAX);
printf("DBL_MIN = %e\n", DBL_MIN);
return 0;
}
Type modifiers and qualifiers
Modifiers change the range or precision of a type. Qualifiers change how objects of that type may be accessed or updated. Modifiers bind to arithmetic types. Qualifiers bind to any type.
Using signed and unsigned integers
signed integers represent negative and positive values. unsigned integers represent non negative values only. Conversions between the two are value preserving when the source fits the destination; otherwise results depend on the representation and can wrap modulo two to the power of the width.
#include <stdio.h>
int main(void) {
unsigned int u = 4000000000u;
int s = (int)u; // implementation defined for values that do not fit
printf("%u %d\n", u, s);
return 0;
}
Using const for read only intent
const marks an object as not modifiable through that name. It does not make the value a compile time constant by itself. Pointers to const and const pointers are different forms; the position of const matters.
int x = 10;
const int cx = 20; // cannot write through cx
int *p = &x; // pointer to int
const int *pc = &x; // pointer to const int (data is read only through pc)
int * const cp = &x; // const pointer to int (pointer is fixed)
const int * const cpc = &x; // const pointer to const int
int * const is a const pointer to int. This simple rule reduces confusion with multiple qualifiers.
Using volatile for externally changed data
volatile tells the compiler that a value can change outside the program flow. The compiler then avoids certain optimizations and always performs an actual read or write. This is used for memory mapped device registers, flags set by signal handlers, and shared memory where synchronization occurs across translation units.
extern volatile unsigned int status_reg;
while ((status_reg & 1u) == 0u) {
/* wait for ready flag set by hardware … */
}
volatile does not provide atomic operations or mutual exclusion. Use appropriate synchronization primitives for concurrency needs.
Variable scope and lifetime
Scope tells where a name is visible. Storage duration tells when an object exists in memory. Linkage tells whether a name refers to the same object across translation units. All three affect how you design interfaces and manage memory.
Types of scope
Block scope applies to identifiers declared inside braces. Function scope applies to labels used with goto. File scope applies to identifiers declared at the top level of a translation unit.
int g = 1; // file scope, external linkage by default
void f(void) {
int x = 2; // block scope, automatic storage
{
int x = 3; // inner block hides outer x
printf("%d\n", x);
}
printf("%d\n", x);
}
Working with storage duration
Automatic storage duration objects are created on entry to the enclosing block and destroyed on exit. Static storage duration objects exist for the entire program run. Allocated storage is obtained by malloc and friends and must be released by free.
#include <stdlib.h>
void counter(void) {
static int calls = 0; // static storage, value persists
calls++;
/* use calls … */
}
int *make_array(size_t n) {
int *p = malloc(n * sizeof *p);
if (!p) { return NULL; }
return p;
}
sizeof *p over sizeof(type) in allocations. This stays correct if the pointed type changes.
Linkage types
Identifiers with external linkage refer to the same entity across translation units. Internal linkage restricts a name to the current translation unit. No linkage means each declaration is a distinct entity.
// file a.c
int shared = 42; // external linkage
static int hidden = 7; // internal linkage
// file b.c
extern int shared; // refers to the same object defined in a.c
static for internal linkage when a function is private to a file.
Type conversions and promotion rules
Expressions often combine different types. C applies a set of promotions and conversions to produce a common type. Understanding these rules prevents subtle bugs, especially with signed and unsigned mixes and with floating and integer combinations.
Integer promotions
Types with rank less than int promote to int if int can represent all values of the original type; otherwise they promote to unsigned int. This happens for char, signed char, unsigned char, short, and unsigned short when used in expressions.
unsigned char a = 200;
unsigned char b = 100;
printf("%d\n", a + b); // both promote, addition occurs as int
Usual arithmetic conversions
When binary operators combine different arithmetic types, both operands convert to a common real or integer type. If either operand is floating, the other converts to the widest floating type present. Otherwise both operands undergo integer promotions and then convert to the type with the higher rank and signedness rules.
| If either is | Then convert both to |
long double |
long double |
double |
double |
float |
float |
| Otherwise | apply integer promotions, then convert to the type with higher rank; if ranks are equal and exactly one is unsigned, convert to the unsigned type |
Signed and unsigned interactions
If an unsigned type has rank greater than or equal to the signed type, the signed value converts to unsigned. This can change negative numbers into large positive values.
#include <stdio.h>
int main(void) {
int s = -1;
unsigned int u = 1u;
if (s < u) {
printf("implementation and ranks decide …\n");
}
printf("s + u = %u\n", s + u); // operands convert to unsigned int
return 0;
}
Casts, truncation, and overflow
Explicit casts request a conversion. Converting from a wider integer to a narrower one can truncate. Converting a floating value to an integer rounds toward zero and is undefined if the value is outside the range of the destination type.
#include <math.h>
double d = 3.9;
int i = (int)d; // becomes 3
unsigned char c = (unsigned char)1025; // truncation, keeps low 8 bits
Default argument promotions in variadic calls
Arguments to variadic functions such as printf undergo default promotions. float promotes to double and integer types smaller than int promote to int or unsigned int. Format specifiers must match the promoted types.
#include <stdio.h>
void demo(void) {
float f = 1.0f;
printf("%f\n", f); // ok, f promotes to double
char ch = 'A';
printf("%d\n", ch); // ok, ch promotes to int
}
PRIu64 and related macros from <inttypes.h> for portable printf with fixed width integers. Example: printf("%" PRIu64 "\n", value);
Balancing precision and performance
double often gives better numerical stability than float at a modest cost. When you must interoperate with graphics or signal processing code that expects float buffers, convert at the edges and keep computations in double internally.