Variables, Data Types & Constants

Every value a C program works with lives in a variable of some fixed, declared type — get comfortable with C's core types and how much space and range each one actually has, and a huge share of later bugs simply stop happening.

Variables, data types, and constants are what let a C program hold onto information at all. Every time your program works with a number, a character, a result, or a status value, C needs answers to a handful of questions up front: what kind of data is this, where does it live, how much memory does it need, what values can it hold, and is it allowed to change? This page answers all of those, one idea at a time, building from a single variable up to a full picture of C's type system.

What a variable actually is

A variable is a named object a C program uses to store a value — one that can be read, and usually changed, while the program runs.

Three separate things are happening on this one line: int is the data type, age is the variable's name, and 25 is the value it starts out holding.

It's worth being precise about this: age is not the value 25 itself. age is the name bound to a piece of storage that currently holds 25 — the name and the value are two different things, and that distinction matters the moment you assign age a different value later.

int age = 25;
age
   |
   v
+------+
|  25  |
+------+

Why programs need variables at all

Without variables, a program could only ever work with values typed directly into the code — completely inflexible. Variables let a program store information once and reuse it, compute with it, and change it, all without retyping the actual value everywhere it's needed.

total doesn't need to know the number 500 was ever involved — it's computed from price and quantity, so changing either one automatically changes the result the next time the program runs.

int price = 100;
int quantity = 5;
int total = price * quantity;

Declaring a variable

A variable is declared by writing a type followed by a name, in the form data_type variable_name;.

Each of these tells the compiler that a variable with this name and this type exists. None of them has been given a meaningful value yet — that's a separate step, covered next.

int age;
float temperature;
char grade;

Initializing a variable

Giving a variable its first value, right at the point it's declared, is called initialization.

Each of these variables is declared and given a starting value in the same statement.

int age = 25;
float temperature = 36.5f;
char grade = 'A';

Declaration, initialization, and assignment aren't the same thing

These three words get used loosely, but they mean three distinct actions.

int age;       // declaration: age now exists
int age = 25;  // initialization: age exists, and starts at 25
age = 30;      // assignment: an already-existing age gets a new value
int age = 25;   // declaration + initialization
age = 30;       // assignment

before:  age -> 25
after:   age -> 30

A variable can change

An ordinary variable can be reassigned as many times as the program needs.

The name count never changes — only the value stored underneath it does. Whether an object is allowed to do this at all is exactly what separates an ordinary variable from a constant, covered later on this page.

int count = 10;
count = 20;
count = 30;
count
  |
  v
10 -> 20 -> 30

What a data type is

A data type tells C what kind of value an object can hold, and gives the compiler the information it needs to work with that value correctly.

int age = 25;         // int: age is an integer
float temperature = 36.5f;  // float: a floating-point value
char grade = 'A';     // char: a character-sized value

Why C insists on knowing the type

Compare int x = 10; with float x = 10.5f; — these hold fundamentally different kinds of data, and the compiler needs to know which kind it's dealing with, because type affects how a value is represented in memory, how much storage it needs, which operations make sense on it, how expressions involving it get evaluated, and how it converts to and from other types.

Knowing that a and b are both int is exactly what lets the compiler apply the right rules to a + b — a completely different set of rules would apply if either operand were a float or a char.

int a = 10;
int b = 20;
int result = a + b;

The basic C data types, at a glance

C also has derived and user-defined types — arrays, pointers, structs, and more — but those each get their own dedicated topic later. This page focuses on the foundation everything else builds on.

C data types
  |-- void
  |-- integer types
  |     |-- char
  |     |-- signed integer types
  |     `-- unsigned integer types
  `-- floating types
        |-- float
        |-- double
        `-- long double

int

int holds whole numbers — positive, negative, or zero — like -100, -5, 0, 10, 25, or 1000.

An int can never preserve a fractional part. This doesn't store 10.5 — the value is converted according to C's usual rules, and the .5 is simply lost, leaving x holding 10.

int age = 25;
int count = 100;
int temperature = -10;
int x = 10.5;

char

char is a distinct integer type sized to hold a single character's worth of data, and it's what you reach for whenever you need one character rather than a whole number range.

Notice the single quotes around 'A' and 'R' — that's what marks these as character constants rather than something else entirely.

A character constant like 'A' is stored as a numeric character code — ASCII is extremely common, but C itself doesn't mandate that every execution environment use ASCII specifically. The exact numeric encoding is a property of the environment your program runs in, not a guarantee from the language.

char grade = 'A';
char initial = 'R';

float

The trailing f is what makes each literal an explicit float constant.

Drop the f and write 36.5 by itself, and that literal's type becomes double instead of float — worth knowing before the next section.

float temperature = 36.5f;
float price = 99.99f;

double

double is C's double-precision floating type, and it generally offers noticeably more precision than float — though the exact representation and precision are implementation-defined rather than fixed by the language itself.

double distance = 12345.6789;
double pi = 3.141592653589793;

void

void marks the deliberate absence of a value or type, in whichever context it's used.

Here, void appears twice with two different meanings: the first says this function returns no value at all, and the (void) parameter list says it takes no parameters.

void also shows up in pointer types, as in void *ptr;, but pointers get their own proper treatment in a later topic — no need to chase that thread yet.

void display(void)
{
    printf("Hello");
}

Integer type modifiers

C lets you modify a base integer type with signed, unsigned, short, and long.

These two lines declare the exact same type — int is often left implicit alongside a modifier like this.

unsigned int count;
signed int temperature;
short int value;
long int distance;
unsigned int count;
unsigned count;

Signed vs. unsigned

A signed integer type can represent both negative and positive values, while an unsigned integer type can only ever represent zero and positive values.

For an unsigned type with N value bits, the representable range runs from 0 to 2^N - 1. Signed types are different: their exact representation and range depend on the specific C implementation and language version, so it's best not to assume a particular bit layout unless you actually know the platform you're targeting.

int temperature = -10;      // signed: negative is fine
unsigned int count = 100;   // unsigned: nonnegative only
signed:    negative <- 0 -> positive
unsigned:            0 -> positive

short, int, long, and long long

Beyond plain int, C gives you short, long, and long long, each available in signed and unsigned forms where that applies.

Never assume int is always 4 bytes on every C implementation. The C standard guarantees relationships and minimum ranges between these types, but leaves their exact sizes up to the implementation — the only reliable way to know a type's actual size on your platform is to measure it directly with sizeof.

short a;
int b;
long c;
long long d;
printf("%zu\n", sizeof(int));

sizeof

sizeof reports the size, in bytes, of a type or an object.

sizeof produces a value of type size_t, which is why %zu is the correct format specifier for it.

sizeof(int) might well produce 4 on the platform you're using right now — but 4 is a common outcome, not a universal C rule. Treat it as something to check with sizeof, never something to assume.

int age = 25;
printf("%zu\n", sizeof(age));
printf("%zu\n", sizeof(int));

A quick reference across the basic types

Type              Typical purpose
----------------------------------------------------------------
char              Character-sized / integer data
signed char       Small signed integer
unsigned char     Small unsigned integer
short             Small integer
int               General-purpose integer
unsigned int      Nonnegative integer
long              Larger-range integer
long long         At least as wide as long, for larger integers
float             Single-precision floating-point
double            Double-precision floating-point
long double       Extended floating-point type, where supported
void              No value / no type information, in context

What a constant actually is

A constant is a value that doesn't change over the course of the program's execution — 10, 25, 3.14, 'A', and "Hello" are all examples of literal constants, written directly into the source.

Here, 25 itself is the constant — a fixed literal value used to initialize the variable age.

int age = 25;

Integer constants and their bases

An integer literal can be written in more than one base.

025 and 0x19 both actually equal 25 in decimal — they're just three different ways of writing the same value.

Binary integer constants like 0b11001 were only standardized starting with C23. Older C standards never defined this notation, so whether it compiles at all depends on which language mode or version your compiler is targeting.

int x = 25;    // decimal
int x = 025;   // octal (a leading 0 marks it)
int x = 0x19;  // hexadecimal
int x = 0b11001; // binary

Floating-point constants

A decimal floating-point literal like these has type double by default.

3.14
10.5
0.25
3.14f   // explicitly float
3.14L   // explicitly long double

Character constants

Character constants — 'A', 'B', '7', '\n' — are always written in single quotes.

Don't confuse 'A' with "A" — they are fundamentally different things in C. 'A' is a character constant; "A" is a string literal (really, a two-element array holding 'A' followed by a terminating null character). Strings get a full topic of their own later in this track.

char grade = 'A';
char digit = '7';

const-qualified variables

The const qualifier marks an object as not modifiable through that particular declaration.

Attempting to assign to max_users after it's declared const is invalid — a conforming compiler is required to diagnose it.

A useful way to hold the two apart: an ordinary variable's value can be modified after it's created; a const object's value cannot be modified through that object.

const int max_users = 100;
max_users = 200; // constraint violation

const isn't automatically a compile-time constant

This nuance trips people up: const int x = 10; makes x a const-qualified object, but that's not the same guarantee as a compile-time constant mechanism like a C++ constexpr or a preprocessor macro.

Depending on the exact context and which version of C you're targeting, a const-qualified object isn't automatically treated as an integer constant expression — and that distinction genuinely matters in places like array bounds, switch case labels, other compile-time expressions, and static initialization, all of which sometimes require a true constant expression rather than merely a const-qualified variable.

#define constants

The preprocessor offers a completely different way to name a constant value.

Before the compiler ever sees this code, the preprocessor has already replaced every occurrence of MAX_USERS with the literal text 100, following its own preprocessing rules.

A macro defined with #define is not a variable. Simply defining it doesn't create any typed C object at all — it's purely a textual substitution that happens before real compilation starts.

#define MAX_USERS 100

int users = MAX_USERS;
#define MAX_USERS 100
         |  preprocessing
         v
int users = 100;

const vs. #define

For most typed constants, const is the better choice precisely because it participates fully in C's type system, while a macro doesn't.

const                                   #define
---------------------------------------------------------------
C language feature                      Preprocessor feature
Has a real C type                       The macro itself has no C type
Creates an object when declared         Performs textual/token replacement
Follows normal C scope rules            Visibility follows preprocessing rules
Type-checked wherever it's used         No object-type checking on the macro itself
const int max = 100;
#define MAX 100

Variable naming rules

A C identifier may contain letters, digits, and the underscore character, but it can never begin with a digit.

All four of these are valid identifiers.

None of these are legal: 1age starts with a digit, student-age contains a hyphen (which C reads as subtraction, not a valid identifier character), and my age contains a space.

C identifiers are case-sensitive, so age, Age, and AGE name three completely different, unrelated objects — not the same variable written three ways.

age
student_age
count1
_temperature
1age
student-age
my age

Keywords can't be used as variable names

C reserves a set of words — int, float, char, return, if, else, while, for, struct, and const among them — that can never be used as identifiers.

This won't compile: return is a reserved C keyword, so it can never be repurposed as a variable name, no matter what type comes before it.

int return = 10;

Declaring multiple variables at once

Each name in a list like this can carry its own initializer.

For readability, spreading these across separate declarations is often easier to maintain, especially as the list of variables grows: int age = 20; int count = 10; int marks = 90;

int age, count, marks;
int age = 20, count = 10, marks = 90;

Why initialization matters

For an ordinary automatic local variable, x here holds an indeterminate value — its declaration never gave it one. Reading that value can amount to undefined behavior in exactly the situations where an indeterminate value isn't permitted to be used.

Whenever zero (or any specific starting value) is actually what you intend, write it explicitly — never rely on a local variable to already hold some sensible default.

int x;
printf("%d", x);
int x = 0;

Putting variables and types together in one program

Four different types, each doing exactly what it's suited for: int age holds a whole number, float height holds a decimal with less precision, char grade holds one character, and double salary holds a decimal that needs more precision than float would comfortably give it.

#include <stdio.h>

int main(void)
{
    int age = 21;
    float height = 5.8f;
    char grade = 'A';
    double salary = 45000.50;

    printf("Age: %d\n", age);
    printf("Height: %.1f\n", height);
    printf("Grade: %c\n", grade);
    printf("Salary: %.2f\n", salary);

    return 0;
}

How a variable's type shapes an expression

x and y hold genuinely different representations in memory, and C has to reconcile that difference the moment they appear together in one expression.

Here, x is converted to a float before the addition happens, so the result comes out as a float too. The full details of exactly when and how C performs these conversions — its "usual arithmetic conversions" — get their own proper treatment once operators and type conversion are covered.

int x = 10;
float y = 10.5f;
float result = x + y;
int x (10)
        \
         + ---> result
        /
float y (10.5)

Example: a variable actually changing

Running this prints Before: 10 followed by After: 20 — the object named count never goes away or gets recreated; only the value stored inside it changes.

#include <stdio.h>

int main(void)
{
    int count = 10;
    printf("Before: %d\n", count);

    count = 20;
    printf("After: %d\n", count);

    return 0;
}

Example: a constant in practice

const int max_score = 100; creates an int object that simply cannot be reassigned through this declaration for the rest of the program.

#include <stdio.h>

int main(void)
{
    const int max_score = 100;
    printf("Maximum score: %d\n", max_score);
    return 0;
}

A simple mental model for a variable

Identifier
      |
      v
 +----------+
 |  Value   |
 +----------+
      |
      v
  Data type
int age = 25;
  name       -> age
  type       -> int
  current    -> 25
  modifiable -> yes

const int max = 100;
  name       -> max
  type       -> const int
  current    -> 100
  modifiable -> no

Keeping variable, value, and type straight

These three ideas are easy to blur together, so it helps to name them explicitly against one example.

age is the identifier — the name used to refer to this object. int is the object's type. 25 is the value used to initialize it. None of these three words are interchangeable, even though they all show up on the same short line.

int age = 25;

What to carry forward

A variable is a named object whose stored value can generally change. A data type specifies what kind of data an object or expression represents. A constant is a value or object that isn't intended — or, in the case of const, isn't allowed — to change, depending on which kind of constant is being discussed.

Every variable, type, and constant this page covered fits into that one line of syntax — everything from here builds on this same shape.

A variable is a named piece of memory that holds a value of a specific type. C's core built-in types are int (whole numbers), float and double (decimals), char (a single character), and _Bool (true/false, from C99 onward). A constant is a value that's fixed and can't change after it's set.

Because C is statically typed, the compiler needs to know a variable's type up front to know how much memory to reserve for it and which operations are even valid on it — this is what lets C run so close to the metal with almost no runtime overhead.

Every single C program uses variables — there's no way to write anything meaningful without them. Choosing the right type matters enormously in embedded and systems programming, where every byte of memory is a real, finite resource.

A variable is like a labeled storage box of a fixed size — an int box is sized for whole numbers, a char box is exactly one letter-sized slot, and a double box is a bigger box built for decimals with lots of precision. You can't force a value too big for the box to fit inside it without something being cut off.

Declaring one variable of each core type Four different types are declared and initialized, then printed together using printf's format specifiers, one per type.

int age = 25; reserves 4 bytes (on almost every modern system) and stores the whole number 25 in them.

float price = 19.99f; reserves 4 bytes for a single-precision decimal; the trailing f tells the compiler this literal is a float, not the default double.

char grade = 'A'; reserves exactly 1 byte and stores the character A's underlying numeric (ASCII) code, 65.

const double PI = 3.14159; reserves 8 bytes for a double-precision decimal, and const tells the compiler this value must never be reassigned — attempting to change PI later would be a compile error.

printf's format string matches each %-specifier to the corresponding argument in order: %d for age, %.2f for price (2 decimal places), %c for grade, %f for PI.

Four separate boxes, each sized for its own type — none of them overlap in memory.

sizeof reveals exactly how big each box is sizeof is a compile-time operator (not a real function call) that reports exactly how many bytes a type occupies on the machine you're compiling for.

sizeof(int) evaluates at compile time to a number — typically 4 on today's mainstream systems, but the standard only guarantees at least 2 bytes.

%zu is the correct, portable format specifier for sizeof's result type (size_t, an unsigned type), avoiding a type-mismatch that %d would technically cause.

Each call reports a different size because each type reserves a different, fixed amount of memory — this is exactly what the earlier example's memory diagram was showing visually.

int count; printf("%d", count);

int count = 0; printf("%d", count);

A local variable that's declared but never initialized holds whatever garbage bits were already sitting in that memory — not zero. Printing it before assigning a real value produces an unpredictable result.

unsigned int a = 3; int b = -1; if (b < a) printf("b is smaller");

int a = 3; int b = -1; if (b < a) printf("b is smaller");

When a signed and an unsigned integer are compared, C implicitly converts the signed value to unsigned first — so -1 becomes a huge positive number, and b < a is (surprisingly) false. Keeping both operands the same signedness avoids this trap entirely.

Exact sizes are platform-dependent but conventionally: char is 1 byte, int is usually 4 bytes, float is 4 bytes, double is 8 bytes, and short/long/long long adjust an integer's range up or down from int's default.

signed vs. unsigned changes whether an integer type can represent negative numbers — an unsigned int trades away negative values for double the positive range.

sizeof(type) tells you exactly how many bytes a type occupies on the machine you're compiling for — never assume a size, measure it when it matters.

A char in C is really just a small integer under the hood (its ASCII/Unicode code point) — this is why you can do arithmetic directly on characters, like 'a' + 1 producing 'b's code.

const doesn't create a true compile-time constant the way #define or an enum does — it creates a variable the compiler enforces you can't reassign, but its value can still be computed at runtime.

Using float when you need real precision (like money) — float's ~7 significant digits of precision can introduce rounding errors that double's ~15-16 digits mostly avoid.

Assuming int is always exactly 4 bytes on every platform — it's guaranteed to be at least 16 bits by the C standard, but its exact size is implementation-defined.

Forgetting to initialize a local variable — an uninitialized local variable in C holds whatever garbage bits happened to already be in that memory, not zero.

Mixing signed and unsigned integers in a comparison, which can silently produce surprising results because the signed value gets implicitly converted to unsigned first.

Using double by default for any decimal value unless you have a specific, measured reason (like huge arrays in embedded memory) to use the smaller, less precise float.

Always initializing variables at the point of declaration (int count = 0;) as a defensive habit, even when you plan to assign a 'real' value moments later.

Using fixed-width types like int32_t or uint8_t (from <stdint.h>) in embedded or protocol code where the exact number of bits genuinely matters, rather than trusting int's platform-dependent size.

Integer promotion and implicit conversions In most expressions, C automatically promotes smaller integer types (char, short) to int before doing arithmetic — this is why 'a' + 1 works at all, and why char arithmetic can sometimes surprise you with an int-sized result where you expected a char-sized one.

The difference between a #define constant, a const variable, and an enum constant

What's the difference between float and double?

float is single-precision (typically 4 bytes, ~7 significant digits); double is double-precision (typically 8 bytes, ~15-16 significant digits). double is the safer default unless memory is extremely tight.

What value does an uninitialized local int variable hold in C?

An indeterminate value — whatever bits happened to already be sitting in that memory location. It is not guaranteed to be zero, unlike a global or static variable, which C does zero-initialize by default.

Is char signed or unsigned in C?

It's implementation-defined — on most platforms plain char is signed, but the C standard doesn't require it. Code that depends on char's signedness should use signed char or unsigned char explicitly.

What's the difference between declaring a variable and initializing it?

Declaring just tells the compiler a variable exists and what type it is (int age;); initializing gives it a value at the same moment it's declared (int age = 25;). A variable can be declared without being initialized, but every initialization implies a declaration.

Why does C require you to state a variable's type up front, unlike some other languages you may have used?

Because the type determines how much memory the variable needs, how its bits are interpreted, which operations are valid on it, and how it behaves in expressions — C never infers this at runtime, so it has to be fixed at compile time.

What's the practical difference between an ordinary variable and a const-qualified one?

An ordinary variable's value can be reassigned freely after it's created. A const-qualified variable's value cannot be modified through that declaration again once it's set — trying to do so is a compile-time constraint violation, not a runtime failure.

Why is const generally preferred over #define when you need a typed constant?

const participates fully in C's type system — it has a real type, follows normal scope rules, and gets type-checked everywhere it's used. A #define macro is pure textual substitution: it has no type of its own and no scope beyond preprocessing visibility.

Why are 1age and my age both invalid as variable names in C?

An identifier can never start with a digit, which rules out 1age. An identifier also can't contain a space — C would read my age as two separate tokens, not one name — and only letters, digits, and underscores are allowed at all.

What does this print?

10 — assigning a floating-point value to an int truncates the fractional part outright; nothing gets rounded, the .5 is simply discarded.

A very large number — the maximum value an unsigned int can hold on that platform (4294967295 on a typical 32-bit unsigned int). Subtracting 1 from an unsigned zero wraps around instead of going negative, since an unsigned type can never represent a negative value.

What does sizeof('A') typically evaluate to in C, and why does that surprise people coming from C++?

sizeof(int) — commonly 4, not 1. A character constant like 'A' actually has type int in C (unlike C++, where the same literal has type char), so measuring it with sizeof measures an int, not a single byte.

What happens when this is compiled?

It fails to compile. max is const-qualified, so assigning to it through that declaration is a constraint violation — a conforming compiler is required to report it as an error.

Is MAX a variable here? What does the compiler actually end up seeing?

No — MAX is a macro, not a variable. The preprocessor replaces it with the literal text 10 before real compilation starts, so the compiler sees int arr[10]; and has no idea a macro was ever involved.

Does this declare both a and b with the value 5?

No — only b is initialized to 5. a is declared but left with no initial value at all. Each comma-separated name in a multi-variable declaration needs its own = value if you want it initialized.

Can you assume short is smaller than int, which is smaller than long, in exact byte size, on every platform?

You can rely on the standard's guaranteed rank/range relationship (each is at least as wide as the one before it), but not on exact byte sizes — those are implementation-defined. Use sizeof on the actual platform if you need the real size.

What value does y actually hold, and why is it easy to misread?

y holds 16, not 20. A leading 0 marks an integer literal as octal in C, and 020 in base 8 equals 16 in decimal — a classic source of an off-by-a-lot bug for anyone skimming past that leading zero.

What's wrong with this code, and how would you fix it?

total is an automatic local variable that's never initialized, so printing it reads an indeterminate value — genuinely undefined behavior, not just "probably zero." Fix it by writing int total = 0; (or whatever starting value is actually intended).

What type does the literal 3.14 have before it's stored into f below, and does storing it there cost anything?

3.14 is a double literal by default. Storing it into f converts (narrows) it down to float, which can lose precision in the process — writing 3.14f directly would skip that extra conversion.

data_type variable_name = value;
int age = 25;
float temperature = 36.5f;
double price = 99.99;
char grade = 'A';
const int max = 100;
#include <stdio.h>

int main() {
    int age = 25;
    float price = 19.99f;
    char grade = 'A';
    const double PI = 3.14159;

    printf("Age: %d, Price: %.2f, Grade: %c, Pi: %f\n", age, price, grade, PI);
    return 0;
}
#include <stdio.h>

int main() {
    printf("int: %zu bytes\n", sizeof(int));
    printf("float: %zu bytes\n", sizeof(float));
    printf("double: %zu bytes\n", sizeof(double));
    printf("char: %zu bytes\n", sizeof(char));
    return 0;
}
int x = 10.5;
printf("%d", x);
unsigned int u = 0;
u = u - 1;
printf("%u", u);
printf("%zu\n", sizeof('A'));
const int max = 100;
max = 200;
#define MAX 10
int arr[MAX];
int a, b = 5;