The keyword const was one of the C++ features that found its way into ISO C. When an analogous keyword, volatile, was invented by the ISO C Committee, the “type qualifier” category was created.
const and volatile are part of an identifier’s type, not its storage class. However, they are often removed from the topmost part of the type when an object’s value is fetched in the evaluation of an expression—exactly at the point when an lvalue becomes an rvalue. These terms arise from the prototypical assignment “L=R”; in which the left side must still refer directly to an object (an lvalue) and the right side need only be a value (an rvalue). Thus, only expressions that are lvalues can be qualified by const or volatile or both.
The type qualifiers may modify type names and derived types. Derived types are those parts of C’s declarations that can be applied over and over to build more and more complex types: pointers, arrays, functions, structures, and unions. Except for functions, one or both type qualifiers can be used to change the behavior of a derived type.
const int five = 5;
declares and initializes an object with type const int whose value is not changed by a correct program. The order of the keywords is not significant to C. For example, the declarations:
int const five = 5;
const five = 5;
are identical to the above declaration in its effect.
const int *pci = &five;
declares an object with type pointer to const int, which initially points to the previously declared object. The pointer itself does not have a qualified type—it points to a qualified type, and can be changed to point to essentially any int during program execution. pci cannot be used to modify the object to which it points unless a cast is used, as in the following:
*(int *)pci = 17;
If pci actually points to a const object, the behavior of this code is undefined.
extern int *const cpi;
says that somewhere in the program there exists a definition of a global object with type const pointer to int. In this case, cpi’s value will not be changed by a correct program, but it can be used to modify the object to which it points. Notice that const comes after the * in the above declaration. The following pair of declarations produces the same effect:
typedef int *INT_PTR; extern const INT_PTR cpi;
These declarations can be combined as in the following declaration in which an object is declared to have type const pointer to const int:
In hindsight, readonly would have been a better choice for a keyword than const. If one reads const in this manner, declarations such as:
char *strcpy(char *, const char *);
are easily understood to mean that the second parameter is only used to read character values, while the first parameter overwrites the characters to which it points. Furthermore, despite the fact that in the above example, the type of cpi is a pointer to a const int, you can still change the value of the object to which it points through some other means, unless it actually points to an object declared with const int type.
The two main uses for const are to declare large compile-time initialized tables of information as unchanging, and to specify that pointer parameters do not modify the objects to which they point.
The first use potentially allows portions of the data for a program to be shared by other concurrent invocations of the same program. It may cause attempts to modify this invariant data to be detected immediately by means of some sort of memory protection fault, since the data resides in a read-only portion of memory.
The second use helps locate potential errors before generating a memory fault during that demo. For example, functions that temporarily place a null character into the middle of a string are detected at compile time, if passed a pointer to a string that cannot be so modified.
So far, the examples have all used const because it’s conceptually simpler. But what does volatile really mean? To a compiler writer, it has one meaning: take no code generation shortcuts when accessing such an object. In ISO C, it is a programmer’s responsibility to declare every object that has the appropriate special properties with a volatile qualified type.
The usual four examples of volatile objects are:
An object that is a memory-mapped I/O port
An object that is shared between multiple concurrent processes
An object that is modified by an asynchronous signal handler
An automatic storage duration object declared in a function that calls setjmp, and whose value is changed between the call to setjmp and a corresponding call to longjmp
The first three examples are all instances of an object with a particular behavior: its value can be modified at any point during the execution of the program. Thus, the seemingly infinite loop:
flag = 1; while (flag);
is valid as long as flag has a volatile qualified type. Presumably, some asynchronous event sets flag to zero in the future. Otherwise, because the value of flag is unchanged within the body of the loop, the compilation system is free to change the above loop into a truly infinite loop that completely ignores the value of flag.
The fourth example, involving variables local to functions that call setjmp, is more involved. The fine print about the behavior of setjmp and longjmp notes that there are no guarantees about the values for objects matching the fourth case. For the most desirable behavior, it is necessary for longjmp to examine every stack frame between the function calling setjmp and the function calling longjmp for saved register values. The possibility of asynchronously created stack frames makes this job even harder.
When an automatic object is declared with a volatile qualified type, the compilation system knows that it has to produce code that exactly matches what the programmer wrote. Therefore, the most recent value for such an automatic object is always in memory and not just in a register, and is guaranteed to be up-to-date when longjmp is called.