Skip to main content

Variable descriptors

The C language allows variables to be declared with some specific specifier to provide the compiler with additional information about the behaviour of the variable. Its main purpose is to help the compiler to optimise the code and sometimes to have an impact on the behaviour of the program.

const

The const specifier indicates that the variable is read-only and must not be modified.

const double PI = 3.14159;
PI = 3; // error reported

The const in the above example means that the value of the variable PI should not be changed. If it does, the compiler will report an error.

For arrays, const means that the array members should not be modified.

const int arr[] = {1, 2, 3, 4};
arr[0] = 5; // error is reported

In the above example, const makes it impossible to modify the members of the array arr.

For pointer variables, const can be written in two ways, with different meanings. If const precedes *, it means that the value pointed to by the pointer is not modifiable.

// const means that the value pointed to *x cannot be modified
int const * x
// or
const int * x

In the following example, modifying the value pointed to by x causes an error to be reported.

int p = 1
const int* x = &p;

(*x)++; // error is reported

If const is followed by *, it means that the address contained in the pointer is not modifiable.

// const means that the address x cannot be modified
int* const x

In the following example, modifying x causes an error to be reported.

int p = 1
int* const x = &p;

x++; // error is reported

The two can be combined.

const char* const x;

In the above example, the pointer variable x points to a string. The two consts mean that the memory address contained in x, and the string pointed to by x, cannot be modified.

One use of const is to prevent the modification of function arguments within the function body. If a parameter will not be modified inside the function, you can add the const specifier to that parameter when the function is declared. That way, the person using the function will see the const inside the prototype and know that the argument array remains unchanged before and after the function is called.

void find(const int* arr, int n);

In the example above, the function find has a const specifier for the argument array arr, which means that the array will remain unchanged inside the function.

One case to note is that if a pointer variable points to a const variable, then that pointer variable should not be modified either.

const int i = 1;
int* j = &i;
*j = 2; // error reported

In the example above, j is a pointer variable to the variable i, i.e. j and i point to the same address. j itself does not have a const specifier, but i does. In this case, the value pointed to by j cannot be modified either.

static

The static descriptor has different meanings for global and local variables.

(1) It is used for local variables (located inside the block scope).

When static is used for a local variable declared inside a function, it means that the value of the variable will be retained after each execution of the function and will not be initialized on the next execution, similar to a global variable that is only used inside the function. As a result of the implementation of the function each time, the initialization of the variable, which can improve the speed of implementation of the function, see "function" chapter.

(2) Used for global variables (located outside the block scope).

static for global variables declared outside the function, that the variable is only used for the current file, other source code files can not refer to the variable, that is, the variable will not be linked (link).

A variable modified by static cannot be initialised with a value equal to the variable, it must be a constant.

int n = 10;
static m = n; // error reported

In the example above, the variable m has the static modifier, and it will report an error if its value is equal to the variable n, which must be equal to a constant.

Functions that are only used inside the current file can also be declared as static, indicating that the function is only used in the current file and that other files can define functions with the same name.

static int g(int i);

auto

The auto descriptor indicates that the variable is stored in memory space allocated autonomously by the compiler, and exists only in the scope in which it was defined, and is automatically freed when it exits the scope.

Since variables that are not extern (external variables) are autonomously allocated by the compiler, which is the default behaviour, this specifier has no practical effect and is generally omitted.

auto int a;
// is equivalent to
int a;

extern

The extern descriptor means that the variable is declared in another file and there is no need to allocate space for it in the current file. It is usually used to indicate that the variable is shared by multiple files.

extern int a;

In the above code, a` is an extern` variable, which means that the variable is defined and initialized in other files and there is no need to allocate storage space for it in the current file.

However, the variable is declared with an initialization and extern becomes invalid.

// extern is invalid
extern int i = 0;

// Equivalent to
int i = 0;

In the above code, the declaration of extern for variable initialization is invalid. This is to prevent multiple externs from initializing the same variable more than once.

By declaring a variable with extern inside the function, it is equivalent to the variable being stored statically and its value being fetched externally each time it is executed.

The function itself is extern by default, i.e. the function can be shared by external files and extern is usually omitted. If you only want the function to be available in the current file, then you need to prefix the function with static.

extern int f(int i);
// Equivalent to
int f(int i);

register

The register specifier indicates to the compiler that the variable is frequently used and should provide the fastest read, so it should be put into a register. However, the compiler can ignore this specifier and does not necessarily follow this instruction.

register int a;

In the above example, register prompts the compiler that the variable a will be used frequently and to provide the fastest read speed for it.

register is only valid for variables declared inside blocks of code.

A variable set to register cannot get its address.

register int a;
int *p = &a; // Error reported by compiler

In the above example, &a will report an error because the variable a may be placed inside a register and cannot get the memory address.

If the array is set to register, the address of the whole array or any of the array members cannot be fetched either.

register int a[] = {11, 22, 33, 44, 55};

int p = a; // error reported
int a = *(a + 2); // report an error

Historically, the CPU had an internal cache, called a register. Compared to memory, registers are much faster to access, so using them can increase speed. But they are not in memory, so they have no memory address, which is why it is not possible to get the address of a pointer to them. Modern compilers have made huge advances and will optimise the code as much as possible, following their own rules to decide how to make good use of registers and get the best execution speed, so they may ignore the register specifier inside the code and not guarantee that they will always put these variables into registers.

volatile

The volatile specifier indicates that the declared variable, which may change unexpectedly (i.e. other programs may change its value), is not under the control of the current program, so the compiler should not optimise this type of variable and should look up its value each time it is used. This descriptor is commonly used in the programming of hardware devices.

volatile int foo;
volatile int* bar;

The purpose of volatile is to prevent the compiler from optimising the behaviour of variables, see the following example.

int foo = x;
// other statements, assuming no change in the value of x
int bar = x;

In the above code, since the variables foo and bar are both equal to x and the value of x has not changed, the compiler may put x into the cache, read the value directly from the cache (rather than from the original memory location of x), and then assign values to foo and bar. If x is set to volatile, the compiler will not put it into the cache and fetch the value of x from the original location each time, because other programs may change x between reads.

restrict

The restrict specifier allows the compiler to optimise certain code. It can only be used for pointers, indicating that the pointer is the only way to access the data.

int* restrict pt = (int*) malloc(10 * sizeof(int));

In the above example, restrict indicates that the variable pt is the only way to access the memory allocated by malloc.

The variable foo, in the following example, cannot use the restrict modifier.

int foo[10];
int* bar = foo;

In the above example, the variable foo points to memory that can be accessed with foo as well as bar, so it would not be possible to set foo to restrict.

If the compiler knows that a block of memory can only be accessed in one way, it may be able to optimise the code better because it doesn't have to worry about the value being modified elsewhere.

restrict, when used for function arguments, indicates that there is no overlap between the memory addresses of the arguments.

void swap(int* restrict a, int* restrict b) {
int t;
t = *a;
*a = *b;
*b = t;
}

In the above example, the restrict in the function argument declaration indicates that the memory addresses of the arguments a and b do not overlap.