When reviewing code, I apply the following rules:
const for function parameters passed by reference where the
function does not modify (or free) the data pointed to.
int find(const int *data, size_t size, int value);
const for constants that might otherwise be defined using a #define or an enum. The compiler can locate the data in read-only memory (ROM) as a result (although the linker is often a better tool for this purpose in embedded systems).
const double PI = 3.14;
Never use const in a function prototype for a parameter passed by
value. It has no meaning and is hence just 'noise'.
// don't add const to 'value' or 'size'
int find(const int *data, size_t size, const int value);
Where appropriate, use
const volatile on locations that cannot be changed by the program but might still change. Hardware registers are the typical use case here, for example a status register that reflects a device state:
const volatile int32_t *DEVICE_STATUS = (int32_t*) 0x100;
Other uses are optional. For example, the parameters to a function within the function implementation can be marked as const.
// 'value' and 'size can be marked as const here
int find(const int *data, const size_t size, const int value)
or function return values or calculations that are obtained and then never change:
char *repeat_str(const char *str, size_t n)
const size_t len = strlen(str);
const size_t buf_size = 1 + (len * n);
char *buf = malloc(buf_size);
These uses of
const just indicate that you will not change the variable; they don't change how or where the variable is stored. The compiler can of course work out that a variable is not changed, but by adding
const you allow it to enforce that. This can help the reader and add some safety (although if your functions are big or
complicated enough that this makes a great difference, you arguably have other
problems). Edit - eg. a 200-line densely coded function with nested loops and many long
or similar variable names, knowing that certain variables never change might
ease understaning significantly. Such functions have been badly designed or
const. You will probably hear the term "const poisoning".
This occurs when adding
const to a function parameter causes 'constness' to
Edit - const poisoning: for example in the function:
int function_a(char * str, int n)
if we change
const, we must then ensure that
fuction_b also takes
const. And so on if
function_b passes the
str on to
etc. As you can imagine this could be painful if it propagates into many
separate files/modules. If it propagates into a function that cannot be
changed (eg a system library), then a cast becomes necessary. So sprinkling
const around in existing code is perhaps asking for trouble. In new code
though, it is best to
const qualify consistently where appropriate.
The more insidious problem of
const is that it was not in the original
language. As an add-on it doesn't quite fit. For a start it has two meanings
(as in the rules above, meaning "I'm not going to change this" and "this cannot be modified"). But more than that, it can be dangerous. For example, compile and
run this code and (depending upon the compiler/options) it may well crash when
const char str = "hello world\n";
char *s = strchr(str, '\n');
*s = '\0';
strchr returns a
char* not a
const char*. As its call parameter is
const it must cast the call parameter to
char*. And in this case that
casts away the real read-only storage property. Edit: - this applies generally to vars in read-only memory. By 'ROM', I mean not just physical ROM but any memory that is write-protected, as happens to the code section of programs run on a typical OS.
Many standard library functions behave in the same way, so beware: when you
have real constants (ie. stored in ROM) you must be very careful not to
lose their constness.