| Home | Classes | Papers | Essays | Short Fiction | About |

CS211: Variables

Table of Contents

Introduction

Think back to your first time encountering an algebraic formula; say something as simple as y = x + 1. You likely learned that x is an unknown value, or at least a value which isn't known the moment you glanced at the formula. x is a stand-in, a signifier, a symbol, a name, i.e., something that represents the presence of some magnitude. x is a variable. The value attached to x is subject to change. The potential value attached to it is external to the formula itself.

Variables in Programming Languages

Variables within programming languages satisfy a similar, although not identical, position to variables within algebraic formulas1. Variables are names for data stored in the computer. When we write x = 1 we are saying that x is a name which represents the computational object 1 which exists somewhere within the hardware of the machine running the program. As the name suggests, variables can change values. For example, we can write x = x + 1 which increments the values of x.

Scope

All identifiers, including variable names, will have a scope associated with them. Scope has two facets in C: (1) the lexical scope; (2) the linkage scope. Here, we focus on hte lexical scope which refers to "the region of the program text within which the identifier's characteristics are understood"2. An example should be instructive:

 1: //Block 0, Global scope
 2: int g = 4;
 3: int main(){
 4:   //Block 1, Outer main method
 5:   int x = 1;
 6:   {
 7:     //Block 2, Inner main method
 8:     int y = 2;
 9:     {
10:       //Block 3, Innermost main method
11:       int z = 3;
12:     }
13:   }
14: } 

Listing 4 shows a C program which defines 4 int type variables: g, x, y, z, each one within a different code block. In C, braces, "{}", indicate the starting and closing of a code block, i.e., a collection of statements (you can think of statements as the sentences of programming languages). These blocks are the arbiter's of a variable's lexical scope (hereon referred to simply as "scope"). When a variable is defined in a given block, it can be "seen" by all statements/expressions within the same block as well as interior blocks. For example, g is visible at Block levels 0, 1, 2, and 3, despite it only being declared in Block 0. We call Block 0 the global scope—this refers to anything declared outside of a function block (such as the function main). Furthermore, x is visible at Block levels 1, 2, and 3, but not at Block 0. Every time a block is exited, all of the names defined therein are forgotten. To further emphasize this I will update Listing 4 so that z is referenced in Block 1 after it has been defined in Block 3:

 1: //Block 0, Global scope
 2: int g = 4;
 3: int main(){
 4:   //Block 1, Outer main method
 5:   int x = 1;
 6:   {
 7:     //Block 2, Inner main method
 8:     int y = 2;
 9:     {
10:       //Block 3, Innermost main method
11:       int z = 3;
12:     }
13:   }
14:   x = z + 1;
15: } 

When I try to compile this code, I get the following error:

josephraskind@stargazer:/tmp/scope$ gcc scope.c 
scope.c: In function ‘main’:
scope.c:14:7: error: ‘z’ undeclared (first use in this function)
   14 |   x = z + 1;
      |       ^
scope.c:14:7: note: each undeclared identifier is reported only once for each function it appears in

Why does the compiler say that z is undefined? I clearly define it in Block 3, no?

Again, this has to do with the way scope works in C. The moment we exit a block, we lose the ability to reach all of the identifiers defined within that block. This means z effectively vanishes from the perspective of the compiler the moment we exit Block 3 (Line 12).

Scope rules also allows us to reuse old identifiers so long as we do not use the same identifier in the same scope. For example, take a look at the following code which defines x twice in main:

1: int main(){
2:   int x = 1; //Declaring x
3:   int x = 2; //Declaring x, again
4: }

If I try to compile that code:

josephraskind@stargazer:/tmp/scope$ gcc dec.c 
dec.c: In function ‘main’:
dec.c:3:7: error: redefinition of ‘x’
    3 |   int x = 2; //Declaring x, again
      |       ^
dec.c:2:7: note: previous definition of ‘x’ was here
    2 |   int x = 1; //Declaring x
      |       ^

I get an error that tells me I "redefined" x. This happens because all identifiers within a given scope must be unique. I cannot have two different memory locations tied to the same name, there must be a one-to-one mapping within a scope between name and memory location at all times. If I wanted to fix the above error, I would need to define each x in their own, mutually exclusive scopes (I will leave that as an exercise for the reader).

Scope will become relevant again when we discuss functions!

What a Variable Really Is in C

In C, variables are not purely symbolic names. They are intimately tied up with the memory semantics of the C runtime system. They are truly just standins for locations in memory3.

To illustrate this I will write a simple C program which declares two int variables and adds them together and stores the result in a third variable:

1: int main(){
2:   int a = 100;
3:   int b = 200;
4:   int c = a + b;
5: }

Then I will compile the program down to its assembly representation with gcc -O0 -fno-omit-frame-pointer -fcf-protection=none -S add.c4. We can see the resultant assembly file here:

 1:         .file   "add.c"
 2:         .text
 3:         .globl  main
 4:         .type   main, @function
 5: main:
 6: .LFB0:
 7:         .cfi_startproc
 8:         pushq   %rbp
 9:         .cfi_def_cfa_offset 16
10:         .cfi_offset 6, -16
11:         movq    %rsp, %rbp
12:         .cfi_def_cfa_register 6
13:         movl    $100, -12(%rbp)
14:         movl    $200, -8(%rbp)
15:         movl    -12(%rbp), %edx
16:         movl    -8(%rbp), %eax
17:         addl    %edx, %eax
18:         movl    %eax, -4(%rbp)
19:         movl    $0, %eax
20:         popq    %rbp
21:         .cfi_def_cfa 7, 8
22:         ret
23:         .cfi_endproc
24: .LFE0:
25:         .size   main, .-main
26:         .ident  "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0"
27:         .section        .note.GNU-stack,"",@progbits

The main label shows where our program's code begins. We can see our constants clearly on Lines 13 and 14 whic must mean that -12(%rbp) and -8(%rbp) are our a and b, respectively. How can that be? It turns out that local variables, i.e. variables defined within a non-global scope, are placed on something called "the stack". It is nothing more than a location in memory which is pointed to by the %rbp register in x86-64. By the time we reach assembly language land, we no longer have symbolic names like a and b, but rather purely locations in memory managed by the C runtime constructed by the compiler.

We'll learn more about this when we learn calling conventions for C functions!

Exercises

  1. Look at Listing 4 again, and edit the code such that x is defined twice in main, but the compiler does not throw any errors.
  2. Take Listing 4 and compile it with gcc -O0 -fno-omit-frame-pointer -fcf-protection=none -S add.c. Identify which lines in the assembly output correspond to each of the three variable declarations. What register is used as the base for all three variables? What is the offset of each variable from that register, and why do you think the offsets are multiples of 4?
  3. Modify Listing 4 to add a fourth variable int d = c * 2 and recompile to assembly. What new instructions appear? Has the offset pattern changed? What does this tell you about how the compiler allocates space on the stack for local variables?
  4. Write a C program that declares a global variable int g = 10 and a local variable int g = 20 inside main. Print both. Does it compile? Which value gets printed and why? Now try to print the global g from inside the block that defines the local g—is it possible?
  5. The lecture states that once a block is exited its variables "vanish from the perspective of the compiler." Write a program that demonstrates this by attempting to access a variable from an inner block after that block has closed. Record the exact compiler error you receive and explain in your own words what it means in terms of scope.
  6. Earlier in the lecture, I wrote the code snippet x = x + 1 and referred to it as "incrementing" x. Outside of computer science, x = x + 1 likely looks like a nonsense statement. From what you now know about variables, how is that possible in a programming language? You can see for yourself by writing a program that defines an integer x, assign it some value, say, 100 and increment it by, say, 200. Check out the assembly code produced by gcc to help aid your understanding.

Footnotes:

1

All subsequent references to "variables" will be references to "variables" in the computer science sense.

2

C Programming Language - Second Edition, K&R.

3

This will return when we talk about pointers.

4

-O0 prevents gcc from performing optimizations, -fno-omit-frame-pointer forces gcc to show the frame pointer for all functions, -fcf-protection=none turns off branch protection, and -S stops compilation at the assembler.

Contact: [email protected] | rss feed | Compiled with org-mode | Licensed under CC BY-NC-SA 4.0