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 const
s 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 extern
s 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.