Marc said:
Dear all,
I'm a bit confused about the use of the const qualifier. I've studied
the ANSI version of K&R but this does not seem to provide an
explanation which corresponds to how const may be used (and is used).
E.g. it seems that (according to gcc) both "const int var" and "int
const var" are the same. Likewise, "const int *var"and "int const *
var"seem to mean the same. Writing "int * const **var" seems to mean
that the "int *" is constant.
Qualifiers like 'const', 'volatile', and 'restrict' can be freely mixed
with type names like int or short or float of struct tm, without
changing the meaning of the declaration. However, the order of
qualifiers and '*' does matter. In the declaration;
int * const cpi
The 'const' qualifier applies to 'cpi' itself. In the declaration
int const * pci
the 'const' qualifier applies to the thing pointed at by pci.
I'd appreciate it if somebody could point me to some (preferably on-
line) documentation which describes exactly how to use const.
That is a big, complicated subject, and I don't know of a good online
tutorial. My summary:
Declaring something const makes it a constraint violation to attempt to
modify it. This is a good thing, because implementations are required to
diagnose constraint violations. If you can defer declaring an object
until it already has it's initial value, and if you know that the
initial value should never be changed, then it's a good idea to declare
it const, because that will ensure that you get diagnostic messages if
you made a mistake, and your code actually changes the value of the
object. You might need to fix the code which changes the value, or you
might need to remove the 'const' qualifier, but the diagnostic is useful
because it points out to you the conflict between what your code does
and what you thought it should do.
You can usually work around the restrictions by using a cast:
*(int *)pci = 3;
This is perfectly legitimate in itself (but see below), and there are
sometimes legitimate reasons for doing so. However, you should
understand that whenever you use an cast, you're telling the compiler
"This looks bad, but I know what I'm doing". Always be very sure that
you do indeed know what you're doing, before using any explicit cast.
Treat any cast you find in anyone's code, including your own, as a
danger sign.
Defining an object to be 'const' is a more complicated issue from simply
declaring it. The result is that if your code uses the above technique
to attempt to modify the value of that object, the behavior of your
program is undefined. The change might simply fail to happen, or your
program might abort(), but there's a literal infinity of other possible
behaviors that are generally less likely than those two.
On some platforms, it's possible for your compiler to lock a piece of
memory against changes after initializing it. If so, the 'const'
qualifier on an object definition gives the compiler permission to do
so. That's an easy way to think about it, and I recommend keeping that
model in mind. However, even on machines where there's no such
capability, an implementation is still permitted to generate code based
upon the assumption that such an object's value will never be changed.
As a result, arbitrarily bizarre things can happen if that assumption is
violated. For instance:
In one part of the code:
const int i=5;
In another part of the code:
int j = 10*sizeof(int);
Instead of loading an actual value of 10 into a register, a conforming
implementation could load the current value of 'i' and multiply by 2.
If you've changed the value of 'i', that will result in an invalid value
being given to j, despite the fact that there's no obvious connection
between them. That's an extremely unlikely way of handling such code,
but it's no less dangerous than other more likely possibilities that are
harder to explain.