Data Types
Basic types, type modifiers, variables, constants, conversions, and type limits.
A type is a contract between source code and the implementation. It determines which values an expression may represent, which operations are meaningful, how much storage an object needs, and how stored bits are interpreted. Choosing a type is therefore a design decision, not merely syntax.
Objects and Values
An object is a region of data storage whose contents can represent a value. A declaration such as
int score = 87;
introduces an object named score, gives it type int, and initializes it with the value 87.
Keep these actions distinct:
int attempts; /* declaration; initial value is indeterminate here */
attempts = 1; /* assignment */
int limit = 3; /* declaration plus initialization */
Reading an uninitialized automatic object can produce undefined behaviour. Initialize at the declaration whenever a meaningful initial value exists.
Assignment replaces the stored value after an object already exists. Initialization creates the object’s first value. They can look similar, but language rules differ for arrays, const objects, and composite types.
Variables
A variable is a named object whose stored value may change during its lifetime. Its declaration fixes the type, while initialization supplies the first value and later assignment replaces that value:
double balance = 100.0; /* declaration and initialization */
balance = 125.5; /* assignment */
The type does not change when the value changes. An assignment converts its right operand to the variable’s type, which can lose range or precision. Keep variables in the narrowest useful scope and use names that express roles and units, such as elapsed_seconds rather than value.
Basic Types
Integer Types
C supplies character and signed or unsigned integer types. Their exact widths depend on the implementation, but their minimum ranges and ordering are specified.
char initial = 'S';
short room_count = 120;
int population = 50000;
long distance = 900000L;
long long stars = 1000000000000LL;
The standard guarantees:
sizeof(char) <= sizeof(short) <= sizeof(int)
<= sizeof(long) <= sizeof(long long)
It does not guarantee that every relation is strict. On common platforms, int is 32 bits, but portable code does not assume that without a documented platform contract.
char, signed char, and unsigned char are three distinct types. Plain char is either signed or unsigned depending on the implementation. Use char for textual characters, signed char for explicitly small signed numbers, and unsigned char when examining raw object bytes.
Type Modifiers
The modifiers signed, unsigned, short, and long produce related arithmetic types:
unsigned int item_count = 0U;
long int file_offset = 0L;
unsigned long long mask = 0ULL;
long double estimate = 0.0L;
Some words may be omitted: unsigned means unsigned int; long means long int. Writing the full type can be clearer in instructional code.
Unsigned types represent values from zero through an implementation-defined maximum. Their arithmetic is performed modulo one more than the maximum value. They are useful for bit patterns and quantities whose wrapping is intentional. They are not automatically the safest choice for every non-negative quantity: expressions mixing signed and unsigned operands can surprise readers.
Boolean Type
C17 has the type _Bool. The standard header <stdbool.h> supplies the more readable aliases bool, true, and false:
#include <stdbool.h>
bool is_open = true;
Converting zero to Boolean yields false; converting any nonzero scalar value yields true. Logical and comparison operators produce int values 0 or 1, which convert naturally to bool.
Use Boolean names that read as predicates:
bool has_permission;
bool is_finished;
Floating Types
C provides float, double, and long double:
float sensor_value = 1.25f;
double average = 1.0 / 3.0;
long double reference = 3.141592653589793238L;
double is the usual default for general floating-point work. Unsuffixed floating literals such as 1.25 have type double; the f suffix produces float, and L produces long double.
Floating-point representations approximate most real numbers with finite precision. For example, decimal 0.1 usually has no exact binary representation. This is normal:
#include <stdio.h>
int main(void) {
double sum = 0.1 + 0.2;
printf("%.17g\n", sum);
return 0;
}
A typical result is 0.30000000000000004. Do not compare measured floating values for exact equality without first deciding an appropriate tolerance model.
Void Type
void represents the absence of a value or an incomplete object type. It appears in three important forms:
void print_banner(void); /* returns no value; accepts no arguments */
void *storage; /* generic object pointer */
You cannot define an ordinary object of type void; its size and representation are not defined. void * is a pointer type and is covered in Chapter 9.
Declarations
A declaration combines a base type with a declarator:
unsigned long record_count = 0UL;
Read a simple declaration as “record_count is an unsigned long.” More elaborate pointer and function declarators are introduced only after their underlying concepts.
Avoid packing unrelated declarations onto one line:
int width = 0;
int height = 0;
This is easier to modify safely than int width = 0, height = 0;, particularly when pointers later appear.
Scope and Lifetime Preview
Where an object is declared affects where its name is visible and how long its storage exists. For now, prefer block-local variables:
int main(void) {
int total = 0; /* visible from here to the end of this block */
return total;
}
Functions and Chapter 11 develop scope, storage duration, and ownership in detail.
Constants
The word “constant” is used for several related ideas.
Literal Constants
10, 3.5, and 'A' are constants written directly in source. Their spelling influences their type:
10is the first suitable type amongint,long, andlong longunder rules for decimal literals.10Uis unsigned.0xFFis hexadecimal.075is octal, not decimal seventy-five.2.5fisfloat.
Avoid leading zeroes on decimal-looking integer literals because they change the base.
Symbolic Constants
An enumeration or a const object can give a value a meaningful name:
enum { MAX_ATTEMPTS = 3 };
const double freezing_celsius = 0.0;
const means an object must not be modified through that name after initialization. It does not necessarily create a compile-time integer constant suitable for every context in C. An enumeration constant is often suitable for array bounds known at translation time.
The preprocessor can also substitute tokens:
#define BUFFER_SIZE 256
Macros have no type and obey token-substitution rules, so prefer language-level constants where they meet the need. Chapter 14 treats macros carefully.
Magic Values
Compare:
if (attempts >= 3) { /* Why three? */ }
with:
enum { MAX_ATTEMPTS = 3 };
if (attempts >= MAX_ATTEMPTS) { /* policy is named */ }
A named constant records meaning and creates one maintenance point.
Type Limits
Standard headers expose implementation limits.
#include <float.h>
#include <limits.h>
#include <stdio.h>
int main(void) {
printf("int: %d through %d\n", INT_MIN, INT_MAX);
printf("unsigned int maximum: %u\n", UINT_MAX);
printf("double decimal digits: %d\n", DBL_DIG);
printf("double maximum: %e\n", DBL_MAX);
return 0;
}
sizeof reports storage in bytes, where one byte is exactly sizeof(char) units. A C byte has at least eight bits; CHAR_BIT from <limits.h> gives the actual number.
For exact-width integer requirements, <stdint.h> defines types such as int32_t only when the implementation provides a type with exactly that width. It also supplies broadly useful types such as int_least32_t, uint_fast16_t, intptr_t when available, and uintmax_t.
Conversions
C frequently converts values automatically. Correct code predicts those conversions instead of relying on intuition.
Integer Promotions
Integer types narrower than int usually undergo integer promotion before arithmetic:
unsigned char left = 200;
unsigned char right = 50;
int sum = left + right;
On common implementations both operands promote to int, so the addition produces 250 as an int before assignment. The arithmetic is not necessarily performed in unsigned char.
Usual Arithmetic Conversions
When arithmetic operands have different types, C finds a common real type. In simplified form:
- Floating operands dominate lower floating or integer ranks.
- Integer operands are promoted.
- Remaining signed and unsigned ranks determine the common integer type.
Examples:
double a = 5 / 2; /* integer division first: 2, then 2.0 */
double b = 5.0 / 2; /* common type double: 2.5 */
long c = 10 + 20L; /* int 10 converts to long */
The position of the conversion matters. Assigning to double cannot recover a fractional part already discarded by integer division.
Signed–Unsigned Mixtures
This expression is dangerous:
int debt = -1;
unsigned int balance = 1U;
if (debt < balance) {
/* A reader may expect this branch. */
}
Here the operands are int and unsigned int, which have the same rank, so debt is converted to unsigned int. Converting -1 produces UINT_MAX, and the branch is not taken. Keep related quantities in compatible types and enable conversion warnings.
Explicit Casts
A cast requests a conversion:
double mean = (double)total / count;
This cast is meaningful: it ensures floating division. A cast should document a conversion that is understood and safe. It must not be used merely to silence a warning:
int count = (int)huge_value; /* still wrong if huge_value is out of range */
Validate range before narrowing.
Overflow and Precision
Signed Overflow
If a signed integer operation produces a mathematical result outside the type’s range, behaviour is undefined. The implementation is not required to wrap:
if (value > INT_MAX - increment) {
/* handle overflow */
} else {
value += increment;
}
The guard must itself avoid overflowing. value + increment > INT_MAX would be too late if the addition already overflows.
Unsigned Wraparound
Unsigned arithmetic is reduced modulo one more than the type’s maximum:
unsigned int n = UINT_MAX;
n += 1U; /* defined result: 0 */
Defined does not mean desirable. A wrapped length can cause a later buffer-size defect. Treat unexpected wraparound as an error even though the language defines it.
Floating Range
Floating arithmetic can round, overflow to an infinity where supported, underflow toward very small values or zero, and propagate not-a-number values. <float.h> describes characteristics, while <math.h> provides mathematical operations and tests.
Never infer decimal precision directly from sizeof. Use FLT_DIG, DBL_DIG, and related macros.
Formatted I/O Contracts
Formatted I/O depends on exact types. Common printf conversions include:
| Type | printf conversion |
|---|---|
int | %d |
unsigned int | %u |
long | %ld |
long long | %lld |
double | %f, %e, %g |
long double | %Lf, %Le, %Lg |
size_t | %zu |
For printf, a float argument is promoted to double. For scanf, %f expects float * and %lf expects double *; confusing input and output conventions is a frequent source of memory corruption. Chapter 4 develops these contracts.
Choosing a Representation
Choose a type from the meaning of the value before thinking about storage size. “It is a number” is not enough. Ask whether the value is a count, a signed difference, a monetary amount, a physical measurement, a truth value, or a code that merely looks numeric.
| Meaning | Useful first choice | Question that may change it |
|---|---|---|
| small signed count | int | Can the documented maximum exceed INT_MAX? |
| object size or index | size_t | Must a negative sentinel be represented? |
| exact money in minor units | a suitably wide integer | Can multiplication or accumulation overflow? |
| measured quantity | double | Is decimal exactness a legal/accounting requirement? |
| yes/no state | bool | Are there actually more than two states? |
| raw byte | unsigned char | Is it text that needs character interpretation? |
A student mark might fit in unsigned char, but int is often the clearer working type: arithmetic promotes the smaller integer anyway, input functions naturally target larger types, and the saved bytes are irrelevant for one object. In a hundred-million-element array, representation size may matter. Type selection depends on both the value domain and the surrounding scale.
Do not use unsigned types merely to announce that negative values are invalid. Unsigned arithmetic cannot represent a negative error, and subtracting a larger value from a smaller one wraps. Validation enforces a domain rule; the type supplies a representable set. They are related but not interchangeable.
Invoice Case
This complete program keeps money in cents so the exact subtotal does not depend on binary floating-point representation. It widens before multiplication and checks whether the final narrowing to long long is safe.
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int item_count = 1200;
int cents_per_item = 349;
int discount_percent = 15;
if (item_count < 0 || cents_per_item < 0 ||
discount_percent < 0 || discount_percent > 100) {
fputs("invalid invoice data\n", stderr);
return EXIT_FAILURE;
}
if (item_count != 0 &&
cents_per_item > LLONG_MAX / item_count) {
fputs("subtotal is too large\n", stderr);
return EXIT_FAILURE;
}
long long subtotal =
(long long)item_count * cents_per_item;
if (discount_percent != 0 &&
subtotal > LLONG_MAX / discount_percent) {
fputs("discount calculation is too large\n", stderr);
return EXIT_FAILURE;
}
long long discount =
subtotal * discount_percent / 100;
long long due = subtotal - discount;
printf("subtotal: %lld.%02lld\n",
subtotal / 100, subtotal % 100);
printf("discount: %lld.%02lld\n",
discount / 100, discount % 100);
printf("due: %lld.%02lld\n",
due / 100, due % 100);
return EXIT_SUCCESS;
}
The cast appears on an operand, before multiplication. If the program instead wrote:
long long subtotal = item_count * cents_per_item;
both operands would still have type int, so the multiplication would occur as int; assignment to long long would happen only afterward. A wider destination cannot repair an overflow that has already occurred.
For the chosen values, the type-and-value trace is:
(long long)item_count -> long long value 1200
cents_per_item -> int, converted to long long
1200LL * 349LL -> 418800LL
418800LL * 15 / 100 -> 62820LL
418800LL - 62820LL -> 355980LL
The percentage calculation deliberately rounds down to the nearest cent because it uses integer division. That is a policy, not an unavoidable fact. A financial specification might require half-up, half-even, or another rounding rule. Types make certain operations possible; the application must still define their meaning.
The multiplication guards assume non-negative inputs, which were validated first. The second guard matters even though the eventual discount is smaller than the subtotal: the intermediate product by discount_percent occurs before division by 100 and must also fit. For arbitrary signed operands, multiplication checks require more cases because both signs and LLONG_MIN matter. A guard is correct only under its stated preconditions.
Object Timeline
Declaration, initialization, assignment, conversion, and lifetime answer different questions:
int attempts; /* declaration; value is indeterminate */
attempts = 1; /* assignment begins a meaningful state */
double shown = attempts; /* initialization plus conversion */
attempts = 2; /* changes attempts, not shown */
After the final statement, attempts is 2 and shown remains 1.0. The two objects do not stay linked. Assignment copies a value converted to the destination type; it does not create a formula that will be recomputed later.
For every object, be able to state:
- when its name is visible;
- when its storage exists;
- whether a value has been stored before each read;
- which conversions occur when a new value is assigned;
- when the object ceases to exist.
Later chapters separate scope from storage duration in more detail, but this timeline already prevents many uninitialized-read and stale-value defects.
Floating Comparisons
Binary floating types cannot represent every decimal fraction exactly. A computation that is mathematically 0.3 may be stored just above or below it. Exact equality is appropriate when values were copied unchanged, when testing an exact sentinel deliberately chosen for the representation, or when the calculation itself guarantees the same result. It is often inappropriate for independently computed measurements.
An absolute comparison has the form:
double difference = a > b ? a - b : b - a;
if (difference <= tolerance) {
/* close enough under this domain's absolute scale */
}
The tolerance belongs to the problem. A difference of 0.001 may be negligible for a distance in kilometres and unacceptable for a laboratory calibration. For quantities spanning very different magnitudes, a relative tolerance compares the difference with the larger magnitude. Values close to zero still need an absolute floor. There is no universal epsilon that makes every floating comparison correct.
Special floating values complicate ordering. A not-a-number value compares unequal to every value, including itself. Infinity may be a legitimate intermediate in some numerical work and an error in other domains. Validate isfinite when a contract requires an ordinary finite measurement rather than assuming every successful arithmetic expression produced one.
Integer Families
The basic names describe minimum capabilities and ordering relationships, not one universal byte layout. C guarantees:
sizeof(char) == 1
sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long)
sizeof(long) <= sizeof(long long)
One C byte contains CHAR_BIT bits and need not be eight bits, although eight is overwhelmingly common on general-purpose systems. The standard headers let code ask instead of guess.
<stdint.h> may provide exact-width types such as int32_t only when the implementation has a type with exactly that width and no padding bits under the required definition. It also provides minimum-width families such as int_least32_t, and typically fast families such as int_fast32_t. Exact width is useful for specified binary formats; it is not automatically the fastest arithmetic type.
Use <inttypes.h> format macros when printing fixed-width integer types portably:
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
int32_t sample = INT32_C(120000);
printf("sample=%" PRId32 "\n", sample);
Do not assume that int32_t is a typedef for int on every implementation and then hard-code %d. The typedef chooses a compatible underlying type; the macro chooses its matching format text.
Data Models
Common 64-bit platforms do not all assign the same width to long. One family commonly uses 64-bit long and pointers; another uses 32-bit long with 64-bit pointers. Code that stores a pointer difference or file size in long because “this is a 64-bit machine” confuses platform marketing with a type contract.
sizeof reports storage size in C bytes. Limits report representable ranges. Neither alone describes semantic suitability. A 64-bit unsigned counter still wraps when subtracting in the wrong direction, and a 32-bit signed field may be exactly correct for a protocol even if long is wider.
Narrowing Gate
Whenever a value moves to a type with a smaller or different range, validate in a type capable of representing both the candidate and the destination limits:
long candidate = 50000L;
if (candidate < SHRT_MIN || candidate > SHRT_MAX) {
fputs("value does not fit in short\n", stderr);
} else {
short accepted = (short)candidate;
printf("accepted %hd\n", accepted);
}
The cast appears only after the proof. If candidate is unsigned while the lower destination limit is negative, comparisons themselves need compatible reasoning; blindly mixing signed and unsigned limits can invalidate the gate.
Conversion from floating to integer deserves a separate gate. The fractional part is discarded toward zero, and behaviour is undefined when the finite integral result cannot be represented by the destination integer type. Check finiteness and range before converting. Decide whether truncation, floor, ceiling, or domain-specific rounding is intended—casting implements only truncation toward zero.
Conversion from integer to floating can lose exactness even when range is sufficient. A double commonly represents all integers only up to a finite consecutive limit; beyond it, neighbouring integers may map to the same floating value. If an identifier, money count, or timestamp must remain exact, do not route it through floating arithmetic casually.
Units Are Contracts
The type system sees these as identical:
double seconds = 5.0;
double metres = 12.0;
double wrong = seconds + metres;
The operation is valid C and invalid physics. Names, records, function boundaries, and review enforce units when the language type does not. A conversion function makes intent auditable:
double kilometres_to_metres(double kilometres) {
return kilometres * 1000.0;
}
Store values in one canonical unit inside a subsystem and convert at input/output boundaries. Mixing Celsius and Fahrenheit or seconds and milliseconds is a representation error even when every value fits perfectly in double.
For competitive programming, concise numeric code is useful only after bounds are derived from constraints. If n <= 200000 and each value is at most 10^9, a sum may approach 2 * 10^14, which does not fit in 32-bit signed int but fits comfortably in a typical 64-bit signed type. Derive the maximum mathematical result before choosing the accumulator.
Conversion Audit
For any nontrivial expression, annotate:
- the type of each operand before promotion;
- integer promotions;
- the common type selected for the operation;
- whether the mathematical result fits that type;
- the conversion into the destination;
Apply the audit especially to mixed signs, small integer types, multiplication feeding a wider destination, and conditional expressions whose branches have different types. Compiler conversion warnings are valuable leads, but the source author still needs a domain argument showing why a conversion is safe or why the types should change.
Conversion Hazards
- Reading an uninitialized automatic object: initialize before the first read.
- Assuming fixed widths: query limits or use documented fixed-width types.
- Expecting assignment to change earlier arithmetic:
double x = 1 / 2;stores0.0. - Mixing signed and unsigned values casually: conversions may reverse an expected comparison.
- Assuming signed overflow wraps: it is undefined behaviour.
- Using casts as warning erasers: prove range and intent first.
- Comparing computed floating values exactly: define a tolerance appropriate to the problem.
- Mismatching format specifiers and arguments: the result may be undefined, not merely ugly output.
Type Reasoning
- A type determines a value set, representation constraints, and valid operations.
- Declaration, initialization, and assignment are distinct events.
- C provides character, signed and unsigned integer, Boolean, floating, and void types.
- Exact widths are not implied by familiar names such as
intandlong. - Integer promotions and usual arithmetic conversions occur before many operations.
- Signed overflow is undefined; unsigned arithmetic wraps by definition; floating arithmetic rounds.
- Limits, format specifiers, and conversions must be handled through documented type contracts.
Type Problems
Name the Types
- Distinguish an object, a type, and a value.
- Why can plain
charnot be assumed to hold negative values? - What is the difference between
const int limit = 10;and#define LIMIT 10? - State the difference between signed and unsigned overflow.
Predict the Values
- Predict the values of
aandbafterdouble a = 7 / 2; double b = 7 / 2.0;. - Determine the types involved in
1.0f + 2.0,1U + 2L, and'A' + 1. Confirm with documentation or a small program. - On your implementation, print
CHAR_BIT, integer limits, floating precision macros, and every basic type’s size.
Repair Conversions
- Correct a program that prints a
size_twith%d. - Find two independent defects in code that stores
-1in an unsigned count and then testscount < 0. - Rewrite an average calculation that performs integer division accidentally.
Model the Data
- Write a temperature converter using
double. Print results to two decimal places and label the units. - Write a checked addition function for non-negative
intvalues. It should report whether the result fits without performing an overflowing expression.
Probe the Machine
- Print
0.1 + 0.2with 6, 10, and 17 significant digits. Explain why formatting changes visibility but not the stored value. - Compile a signed-overflow example with optimisation disabled and enabled. Do not use observed wrapping as evidence of a language guarantee; explain why.