3. Scope, Arrays, Pointers and Memory Addresses
This tutorial introduces you to scope, arrays, pointers and memory in C.
Assumed Background
It is assumed that you are familiar with the material covered in tutorials 1 and 2 and the associated readings.
Associated Reading
The following textbook sections are relevant (in addition to those listed in earlier tutorials):
- Kernigan & Ritchie (2nd ed.) - sections 6.1 to 6.4, 6.7 and sections 5.1 to 5.10
- Harbison & Steele - sections 5.1 to 5.11
An online C Reference Manual (by Martin Leslie) is available. Relevant sections include (but are not limited to):
Scope
The scope of a variable is the part(s) of the program in which that variable can be used. Variables declared within a function (including the parameters to the function) are only accessible within that function - these are called local variables, i.e. they are said to have local scope. These are sometimes called automatic variables because memory is allocated for these variables automatically every time the function is called (and the memory is reclaimed when the function returns). "Allocating memory" in this case means associating a variable with a particular memory cell (or cells) having a particular address (or addresses). "Reclaiming memory" means that those memory cells are now free to be used for other purposes (e.g. local variables for a different function call).
Variables declared outside of a function are known as global or external variables. They are said to have global scope, though in reality their scope extends from the point of definition to the end of the file. Such variables can be changed from within functions, however, if a local variable is given the same name as a global variable it hides the global variable - i.e. it is no longer possible to access that global variable for the remainder of that function.
Consider the following program. What do you think it will output? Run it and see - and make sure you understand why you're getting the result you are.
#include <stdio.h>
/* Global variables */
int a = 2;
int b, c;
/* Function prototypes */
void function_one(int);
int function_two(int);
void function_three(int,int);
int main() {
int a;
int d = 10;
a = 3;
b = 1;
c = 5;
b++;
function_one(a);
d = function_two(b);
function_three(d, a);
return 0;
}
void function_one(int c) {
a--;
b -= c;
return;
}
int function_two(int a) {
c = 10;
return a * a;
}
void function_three(int a, int f) {
printf("%d %d %d %d\n", a, b, c, f);
}
It is bad practice to write programs like this, because they are very hard to follow and debug. In general, global variables should be avoided where possible.
Other things to note in the program above:
- Variables can be initialised at the same time they are declared, e.g.
int d = 10; - For a void function (i.e. a function that returns no value), there is no need to include a return statement. In the example above, function_one() has a return statement, but function_three() does not. Neither needs one, but it is acceptable to include one.
Arrays
Arrays allow data items of the same type to be grouped together for more convenient access. For example, instead of declaring many individual variables:
int a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, average;
/* read data in from user */
scanf("%d", &a1);
scanf("%d", &a2);
scanf("%d", &a3);
scanf("%d", &a4);
scanf("%d", &a5);
scanf("%d", &a6);
scanf("%d", &a7);
scanf("%d", &a8);
scanf("%d", &a9);
scanf("%d", &a10);
scanf("%d", &a11);
scanf("%d", &a12);
/* calculate average - note that this does integer arithmetic (i.e.
** we will get the quotient when dividing by 12, the remainder will
** be discarded
*/
average = (a1+a2+a3+a4+a5+a6+a7+a8+a9+a10+a11+a12) / 12;
it is possible to use arrays:
int a[12], i, average, sum;
/* read data in from user */
for (i=0; i < 12; i++) {
scanf("%d", &a[i]);
}
/* calculate average */
for (i=0; i < 12; i++) {
sum += a[i];
}
average = sum/12;
The declaration
int a[12];
declares a to be an array of 12 integers. These integers can then be accessed using
the syntax a[expr] where expr is an expression
which evaluates to an integer between 0 and 11 inclusive. (Arrays in C are 0-based,
i.e. an array of size n will have elements indexed from 0 to n-1). Some other example
array declarations are below:
- double d[10];
- declares d to be an array of 10 double precision floating point numbers
- char name[20];
- declares name to be an array of 20 characters. Character arrays are used to store strings in C (discussed below).
- int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
- declares and b>initialises an array of integers. In this case no array size is given - it is deduced from the number of elements in the initialisation string (12 in this case).
- int nums[10] = {1, 2, 3, 4, 5};
- declares an array of 10 integers - the first 5 of which are initialised to the values given (and the remaining 5 will be initialised to 0)
Note: Array types are NOT passed by value
Note that when an array is passed to a function, it is not passed by value. This is because an array variable does not contain the array data - it just contains a "pointer" to (i.e. memory address of) the first element in the array. We'll cover pointers shortly.
Exercises
- Write a C program that prompts the user to enter 10 digits and then prints them out in reverse order. e.g. if the user enters (5, 7, 8, 1, 1, 4, 5, 9, 0, 1) the program outputs "1095411875"
- Extend your combination lock design from previously to work with a 10 digit code instead of a 2 digit code.
- Write a C program that prompts the user to enter 20 digits and then prints them out sorted in order from lowest number to highest number.
Addresses
The address of a variable can be found using the "&" (address-of) operator. The following program is an example of using the & for getting memory addresses:
#include <stdio.h>
int main () {
int a;
int b;
unsigned char c;
unsigned char d;
printf ("the memory address of the variable a is %x\n", &a);
printf ("the memory address of the variable b is %x\n", &b);
printf ("the memory address of the variable c is %x\n", &c);
printf ("the memory address of the variable d is %x\n", &d);
return 0;
}
On my system when I run this program i get:
the memory address of the variable a is
12ff4c
the memory address of the variable b is 12ff48
the memory address of the variable c is 12ff47
the memory address of the variable d is 12ff46
"c:\t7\lcc\tute4.exe"
Return code 0
Execution time 0.125 seconds
Press any key to continue...
On this particular system the int type is implemented as a 32 bit number, note that as before char is a 8 bit number. This implies that my two int variables (a and b) require 4 bytes of storage wheras my two char variables require only a single byte. Using the program output we can determine that a takes up 4 bytes or storage starting at 0x12ff4c. Completing the memory map for all 4 variables we get:
| Address | Data |
| 0x12ff46 | d |
| 0x12ff47 | c |
| 0x12ff48 | b |
| 0x12ff49 | b |
| 0x12ff4a | b |
| 0x12ff4b | b |
| 0x12ff4c | a |
| 0x12ff4d | a |
| 0x12ff4e | a |
| 0x12ff4f | a |
Modify the program as shown below and write down the memory map.
#include <stdio.h>
int main () {
int a;
unsigned char b;
unsigned char c;
unsigned char d;
int e;
unsigned char f;
printf ("the memory address of the variable a is %x\n", &a);
printf ("the memory address of the variable b is %x\n", &b);
printf ("the memory address of the variable c is %x\n", &c);
printf ("the memory address of the variable d is %x\n", &d);
printf ("the memory address of the variable e is %x\n", &e);
printf ("the memory address of the variable f is %x\n", &f);
return 0;
}
What can be said about how LCC is mapping variables into memory?
Is the compiler forcing natural alignment of variables?
Pointers
A pointer variable can be used to store and use addresses in C code. They allow us to work with and manipulate memory addresses. A pointer variable can be declared using the following syntax:
type *p;
This declares p to be a pointer to something of type "type". For example:
int a; int *p; p = &a;
This code declares p to be a pointer to an integer and makes it point to a (i.e. p will hold the address of a). You may wish to look through and run the following program to get a better idea of how pointer syntax may be used:
#include <stdio.h>
int main () {
int a;
int *p;
a = 0x25;
printf("the data for the variable a is %x\n", a);
printf("the address of variable a is %x\n", &a);
p = &a;
printf("the address pointed to by pointer p is %x\n", p);
}
Note that pointer declarations may be mixed with normal declarations. The declarations below are identical to the ones above and declare a to be an integer and p to be a pointer to an integer:
int a, *p; int* p, a; int *p, a;
This means that the "*" is associated with the variable name - not the type. For this reason, the second of these forms is NOT preferred - it looks as if a would be a pointer to an integer, but this is NOT the case. When in doubt, use a new line for each individual declaration.
Dereferencing a Pointer
A pointer can be dereferenced (i.e. the thing being pointed to can be accessed) using the "*" operator (known as the indirection or dereferencing operator). For example if p is a pointer to and integer, then *p will evaluate to the integer being pointed to. Modifying the program above we can demonstrate this:
#include <stdio.h>
int main () {
int a;
int *p;
a = 0x25;
printf("the data for the variable a is %x\n", a);
printf("the address of variable a is %x\n", &a);
p = &a;
printf("the address pointed to by pointer p is %x\n", p);
printf("the data at the address pointed to by p is %x\n", *p);
printf("address %x contains data %x\n", &a, a);
printf("address %x contains data %x\n", p, *p);
}
The following (artificial) example gives more examples of how a pointer can be dereferenced and how pointers don't always have to point to the same thing.
int x = 1; int y = 2; int z = 3; int *ip; ip = &x; /* ip points to x */ y = *ip; /* y will take on the value of the integer pointed to by ip */ ip = &z; /* ip now points to z */ *ip = 0; /* The integer pointed to by ip will take on the value 0 */
Pointers and Arrays
Pointers are often used for accessing arrays, in fact the name of an array can be treated as a pointer to the first element on the array. Consider the following code:
/* Declare a to be an array of 10 integers and p to be a pointer to a integer */ int a[10]; int *p; /* Set p to point to the first element in the array */ p = &a[0]; /* We could have written p = a; here */ /* Set the first element in the array to be 1 */ *p = 1; /* Increment our pointer by 1 - this means p now points to the next integer */ p++; /* Set the next element in the array to be 2 */ *p = 2;
Note that we use pointer arithmetic in this example (by adding 1 to p). Arithmetic on pointers is treated specially - adding i to a pointer means the pointer will now point to the i-th object of that type beyond the pointer. (It does not add i to the memory address - it will add i *sizeof(type) where type is the type being pointed to.)
This also means an array reference
a[i]
can also be written
*(a+i)
The two forms are equivalent. It also follows that
&a[i]
is the same as
a+i
which is the reason we can replace
p = &a[0];
with
p = a;
if we wanted to. Note however that an array name is not a variable. We can not write a++ to advance the array position.
Note that strings are often referred to as type char* rather than char[].
Passing Pointers to Functions
All arguments to functions are passed by value - i.e. the value of the variable passed is copied for use in the function. (We saw in an earlier tutorial that arrays were an exception to this. This is because an array name is effectively just a pointer to the memory where the array is stored - we don't copy the whole array.) By passing pointers to functions, we enable the function access the actual variable/structure rather than a copy. (The function is passed a copy of the pointer - i.e. we can't change the value of the pointer in the calling function, but we can dereference the pointer and change what it points to.)
Consider a function which attempts to swap two variable values, being called with
swap(a,b);
void swap(int x, int y) { /* WRONG */
int temp;
temp = x;
x = y;
y = temp;
}
This function does not swap the variable values - it just swaps copies of a and b. By using pointers to a and b, however, we can make a swap function that swaps the values of two variables:
void swap(int *x, int *y) {
int temp;
temp = *x;
*x = *y;
*y = temp;
}
This function would be called as swap(&a, &b); rather than swap(a, b);
Exercises
- Write a program that prompts the user for 5 integer inputs and stores them in an array. The program then prints out the address of each array element alongside the data of each array element. I.e. the program prints out a memory map of your array.
- Write a program that prompts the user for 5 integer inputs and stores
them in an array. The program then uses the swap function above to swap
function to swap the 1st and 4th elements of the array. The program then
prints out the modified contents of the array.
