Learn C Programming

Lesson 1 of 7 · Variables and Data Types

Module 2 · Variables and Data Types

Variables: Named Boxes in Memory

FreeReading

In this lesson

  • Declare, initialise and assign, and say which of the three each line does.
  • Print a variable's address with %p and explain what that number is.
  • Explain why reading a variable you never wrote to is a bug, even when it prints 0.

Zara labels every jar in her kitchen: RICE, SUGAR, TEA. The labels are not the food. They are how she finds the food without opening ten lids.

Then she tries to pour a litre of milk into a tea cup. The cup does not stretch. It holds what it holds, and the rest goes on the floor.

A variable in C is a labelled jar with a fixed size. This lesson is about the label, the size, and the address written on the shelf underneath.

A name, a size and an address

When you write one line like this, four separate things happen.

#include <stdio.h>

int main(void)
{
    int marks = 90;
    printf("%d\n", marks);
    return 0;
}
90
  1. The compiler sets aside a block of memory. For an int that is 4 bytes on the Playground.
  2. It writes in its own notes that the name marks refers to that block.
  3. It records that those bytes are to be read as a whole number.
  4. It stores the bit pattern for 90 into them.

Three of those four are the type doing its job. The type is not decoration. It decides the size, the reading and what you may do with the value.

So a variable is three things at once: a name, a type, and a place. Beginners learn the name and forget the other two, and that is where the surprises come from.

The declaration, with every part named

int      marks      =      90     ;
 |         |        |       |     |
 |         |        |       |     end of the statement
 |         |        |       the first value (optional)
 |         |        the assignment operator
 |         the name you choose
 the type: how big the box is and how to read it
  • The type comes first and never changes for that name.
  • The name follows the rules further down this lesson.
  • = here means "put this in", not "is equal to".
  • Leave out = 90 and you have a box with nothing useful in it.

Declare, initialise, assign: three different lines

Three words, and people use them as if they were one. They are not.

#include <stdio.h>

int main(void)
{
    int marks;            /* declaration: reserve the box, leave it alone */
    marks = 90;           /* assignment: put a value in an existing box */
    int total = 250;      /* declaration and initialisation, one line */

    printf("%d %d\n", marks, total);
    return 0;
}
90 250

Declaring tells the compiler a name and a type. Initialising gives the box its first value at the moment it is created. Assigning puts a value into a box that already exists.

Initialising and assigning look identical, because both use =. The difference is when it happens, and whether there was ever a moment where the box held something unknown.

So int total = 250; is one statement and int total; total = 250; is two. The second one has a gap in the middle, and that gap is where bugs live.

Assignment is not equality

In maths, x = x + 1 is nonsense. In C it is an ordinary instruction.

Read = from right to left: work out the whole right side first, then store the answer in the box on the left. Say "gets" in your head, never "equals".

#include <stdio.h>

int main(void)
{
    int x = 5;
    x = x + 1;        /* x gets the old x plus one */
    printf("%d\n", x);
    return 0;
}
6

The old value is read, 1 is added to the copy, and the answer goes back into the same box. Nothing is ever solved.

So a line can mention a variable on both sides, and the right side always uses the value from before the line ran.

The address is real, and you can print it

The block of memory the compiler set aside sits at a numbered position. That number is the variable's address.

&marks means "the address of marks". You have already used it without knowing: scanf("%d", &n) hands scanf the address so it can write into your box.

#include <stdio.h>

int main(void)
{
    int a = 1;
    int b = 2;
    int c = 3;

    printf("a = %d at %p\n", a, (void *)&a);
    printf("b = %d at %p\n", b, (void *)&b);
    printf("c = %d at %p\n", c, (void *)&c);
    return 0;
}

One run, on one machine, printed this:

a = 1 at 0x7ffd1c4a2b9c
b = 2 at 0x7ffd1c4a2b98
c = 3 at 0x7ffd1c4a2b94

Your numbers will be different. They change between machines and often between runs, so this is the one program in the module whose output nobody can promise.

Two things in that run are worth noticing. The addresses are written in hexadecimal, which lesson 4 of Module 1 explained. And they are 4 apart, because an int takes 4 bytes and the three boxes sit next to each other.

(void *) is a cast, which Module 4 covers properly. %p wants an address in its most general form, and the cast is how you say "treat this as a plain address".

So & is not magic and not new. It is the third fact about a variable, the one you could not see until now.

Three int variables: name, value, size and address Memory, one row of bytes a b c 1 2 3 4 bytes 4 bytes 4 bytes address n n + 4 n + 8 the name the value the size, set by the type the address, printed by %p Three facts about every variable: a name you chose, a size the type chose, and an address the machine chose. You control only the first one.
Figure 1. Three int variables side by side. The addresses are 4 apart because the type says 4 bytes.

Reading a variable you never wrote to

This is the most important paragraph in the lesson, so read it twice.

A declared variable that was never given a value holds an indeterminate value. It is not zero. It is whatever bytes were already sitting at that address.

#include <stdio.h>

int main(void)
{
    int never_set;
    int set_properly = 0;

    printf("set:   %d\n", set_properly);
    printf("unset: %d\n", never_set);
    return 0;
}

On the Playground's compiler this builds without a single message, and the second line printed 0 on the runs we made.

That 0 is the dangerous outcome. It looks like proof that C sets variables to zero. It is not. The value is not promised, and a program that depends on it is broken whether or not today's run is correct.

A local compiler with -Wall turned on does say something: warning: 'never_set' is used uninitialized. The Playground's command line has no -Wall on it, so you get silence.

So the rule is simple and has no exceptions: give every variable a value on the line that declares it.

Naming: what the compiler checks, and what your reader checks

The compiler checks four rules, and it is strict about all four.

  • Letters, digits and underscores only. No spaces, no hyphens, no other punctuation.
  • The first character may not be a digit.
  • A keyword is taken. int, return and double cannot be names.
  • Case matters. total, Total and TOTAL are three different variables.

Your reader checks a fifth rule, and it is the one that costs you marks in a code review.

#include <stdio.h>

int main(void)
{
    /* Hard to read: nobody knows what these hold. */
    int a = 40, x1 = 35, temp = 75;

    /* Easy to read: the name is the explanation. */
    int maths_marks = 40;
    int physics_marks = 35;
    int total_marks = maths_marks + physics_marks;

    printf("%d %d %d\n", a, x1, temp);
    printf("%d\n", total_marks);
    return 0;
}
40 35 75
75

This track writes total_marks, not totalMarks. That style is called snake_case, and it is what the C standard library and most C code have always used.

Pick one style and never mix the two in one file. The choice matters less than the consistency.

One variable per line

C lets you declare several names in one statement. It is legal, and this track avoids it.

#include <stdio.h>

int main(void)
{
    int a = 1, b, c = 3;      /* b has no value: easy to miss */

    b = 2;
    printf("%d %d %d\n", a, b, c);
    return 0;
}
1 2 3

In that line, b sits between two initialised names and is not initialised itself. The eye slides over it.

So write one declaration per line, each with its value. It costs two lines and saves an afternoon.

Example 1: the smallest thing a variable is for

Store one number under a name, then print it. Change the number, run again.

#include <stdio.h>

int main(void)
{
    int stall_number = 7;

    printf("Stall %d\n", stall_number);
    return 0;
}
Stall 7

The name is doing real work already. printf("Stall 7\n") would print the same line and would be a different program: one that cannot ever print a different stall.

Run in Compiler
Example 2: swapping two values with a third box

Zara needs the two numbers to change places. The trick is that assignment copies, so the first copy has to be parked somewhere.

#include <stdio.h>

int main(void)
{
    int a = 10;
    int b = 20;
    int spare = 0;

    spare = a;        /* park the old a */
    a = b;            /* a gets b */
    b = spare;        /* b gets the parked value */

    printf("a = %d, b = %d\n", a, b);
    return 0;
}
a = 20, b = 10

Delete the spare line and write a = b; b = a; instead. Both boxes end up holding 20, because the first line destroyed the old a before the second line could read it.

Run in Compiler
Example 3: the one a beginner actually writes

Read three marks, add them, print the total and each name's size. This is a whole small program with nothing borrowed from later modules.

#include <stdio.h>

int main(void)
{
    int maths = 0;
    int physics = 0;
    int chemistry = 0;

    scanf("%d %d %d", &maths, &physics, &chemistry);

    int total = maths + physics + chemistry;

    printf("Total: %d\n", total);
    printf("Each mark takes %d bytes.\n", (int)sizeof(maths));
    return 0;
}
Total: 252
Each mark takes 4 bytes.

That output is for the input 90 85 77. Every variable is initialised on its own line before scanf touches it, which costs nothing and removes one whole class of bug.

Run in Compiler

Where this is used

  • A game's score. The original Doom source keeps each player's health and ammunition as plain int fields in a struct called player_t. The score on your screen is one of those boxes being printed.
  • A bank balance. Core banking code stores a balance as a whole number of the smallest unit, paisa or cents, in a 64-bit integer. Lesson 3 explains why it is never a decimal type.
  • A sensor reading. An Arduino sketch reads a temperature into an int and prints it. That is the same three lines you just wrote, on a chip the size of a stamp.
  • A loop counter. Almost every loop in the Linux kernel declares int i and counts with it. That one name appears tens of thousands of times in the source.

Common mistakes

1. Using a variable before anything was put in it.

int total;
printf("%d\n", total);

The Playground's GCC 12 prints no message at all, because the check that finds this lives behind -Wall and the Playground does not pass it. A local gcc -Wall says warning: 'total' is used uninitialized. The fix is int total = 0;, and it is not optional just because today's run printed 0.

2. Assigning to a name that was never declared.

total = 5;
printf("%d\n", total);

GCC 12 says error: 'total' undeclared (first use in this function) and stops. C has no way to create a variable by assigning to it, which some other languages do. Add the type in front of the first mention.

3. Starting a name with a digit.

int 2nd_place = 3;

GCC 12 gives two messages: error: invalid suffix "nd_place" on integer constant and then error: expected identifier or '(' before numeric constant. The first one reads oddly because the compiler started reading 2 as a number and then found letters glued to it. Write second_place.

4. Putting text in an int box.

int marks = "90";

On the Playground's GCC 12 this is a warning: initialization of 'int' from 'char *' makes integer from pointer without a cast, and the program builds and prints a large meaningless number. GCC 14 makes the same line an error. "90" in double quotes is text; 90 with no quotes is a number.

Brain teaser

Zara runs this three times and gets 0 every time. She writes in her notes: "an int with no value starts at 0 in C".

#include <stdio.h>

int main(void)
{
    int counter;

    printf("%d\n", counter);
    return 0;
}

Say exactly what is wrong with her conclusion. Then answer the harder half: name two changes, neither of them to the counter line, that could make the same program print something else.

Three identical runs prove one thing about three runs. Think about what else is sitting at that address just before main starts, and about who decides that.

Exercise 1Easy

Kenji is testing the input box of a new tool. He wants the smallest possible program that proves a number went in and came out again.

Input. One line with one integer n.

Output. One line, exactly: n stored, with the number in place of n.

Constraints. -1000000 <= n <= 1000000.

Sample. Input 42 gives 42 stored. One space before the word.

#include <stdio.h>

int main(void)
{
    int n = 0;
    scanf("%d", &n);

    /* One printf. Nothing else. */

    return 0;
}

Graded in this module's Problems lesson as echo-number. The negative end of the range is a real hidden test.

Run in Compiler
Exercise 2Medium

Bob is sorting two boxes of books and needs their labels swapped. Use a third variable, exactly as Example 2 does.

Input. One line with two integers a b.

Output. One line with the two numbers in the other order, separated by one space.

Constraints. -1000000 <= a, b <= 1000000.

Sample. Input 10 20 gives 20 10.

#include <stdio.h>

int main(void)
{
    int a = 0;
    int b = 0;
    int spare = 0;
    scanf("%d %d", &a, &b);

    /* Three assignments, then one printf. */

    return 0;
}

Graded as swap-with-third. Printing b and then a in one printf also passes, and it teaches you nothing, so do it the long way first.

Run in Compiler
Exercise 3Hard

Maria wants the average of three marks, printed with two decimal places. This one needs an idea this lesson has only pointed at, and lesson 3 explains it in full.

Input. One line with three integers a b c.

Output. One line with their average, to exactly two decimal places.

Constraints. 0 <= a, b, c <= 100.

Sample. Input 90 85 77 gives 84.00. Input 1 1 2 gives 1.33.

#include <stdio.h>

int main(void)
{
    int a = 0;
    int b = 0;
    int c = 0;
    scanf("%d %d %d", &a, &b, &c);

    /* Careful: (a + b + c) / 3 throws the fraction away. */
    printf("%.2f\n", 0.0);
    return 0;
}

Not graded in this module. Dividing one whole number by another gives a whole number in C, which is exactly why 1 1 2 is the interesting input here. Lesson 3 gives you the fix.

Run in Compiler

Common doubts

  • Does a variable stop existing when the program ends?

    Yes, and earlier than that. It lives from its declaration to the closing brace of the block it sits in. Module 7 covers that properly, under the name scope.

  • Can I change a variable's type later?

    No. The type is fixed at the declaration and stays fixed. You can copy the value into a different variable of another type, which Module 4 covers.

  • Why does scanf need & when printf does not?

    printf only reads your value, so a copy is enough. scanf has to write into your box, so it needs the address. Module 11 explains the machinery.

  • Is int x = 0; slower than int x;?

    Not in any way you could measure. The compiler usually puts the zero there for free, and a wrong answer is slower than any instruction.

  • How long can a variable name be?

    Long enough. C17 promises the first 63 characters are significant, which no sensible name ever reaches. Choose a name you can say out loud.

Key takeaways

  • A variable is three things: a name you chose, a type that fixes the size, and an address.
  • Declaring reserves the box, initialising fills it at birth, assigning fills it later.
  • = means "gets". The right side is worked out first, then stored on the left.
  • &name is the address, and %p prints it; scanf has always used it.
  • An uninitialised variable holds an indeterminate value, and the Playground warns about nothing.
  • One declaration per line, each with a value, and names in snake_case.

Next you will find out how big that box really is. Then you will meet the day a number is one too large to fit in it.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Integer Types, Their Ranges, and Overflow