Recently, I’ve been learning embedded programming from udemy and there was a section dedicated const
.
The section was interesting because despite programming in C a lot during my duration of my undergrad, I have never taken a look at const
in much detail. I am sure there is more to const
than
what the course provides but I still learned a lot from this section itself.
const
is a type qualifier that is often used in C and C++ programming, especially appearing in the standard libraries frequently. An example is the function definition of strlen
:
size_t strlen(const char *s);
The main purpose of const
keyword is to document to programmers and compilers that the object cannot be mutated after initialization. A type qualifier is simply just giving additional information
about the object to both the programmer and the compiler. Compilers can parse const
to perform various optimization and it may place the object to ROM (Read Only Memory) since modifications are
not supposed to be possible anyways. However, do not bet on your const
objects to be stored in ROM (I believe it’s usually just stored on the stack anyways unless it’s scope is global
).
Let’s look at a simple example of const
before I delve into what I learned from the course:
const int x = 10;
x = 20;
Example 1: Illegal assignment to a const int
If we were to compile the code above, we would get the following error:
test.c: In function ‘main’:
test.c:6:4: error: assignment of read-only variable ‘x’
x = 20;
^
As expected, the compiler would stop the programmer from changing the value of x
since its qualified as a const
. So far, everything I said should be familiar to any C programmer. But please bear with
a bit longer.
How do we declare a variable to be a const
?
In the example above you would assume that the proper way is to declare the qualifier before the type, but it doesn’t matter where you place const
in this example.
const int x = 10;
can also be written as int const x = 10;
It’s all a matter of preference on how you read the line. For instance, const int x = 10;
can be read as “x is a type int that is a const” and the latter can be read as “x is a const of type int”.
What about pointers? How does const
work with pointers? Let’s see an example using the definition of strlen
. const char *s
means that the function will not modify the string you pass into the
function. Let’s take a look at a simple implementation of strlen
I made up on the spot:
size_t strlen(const char *s) {
size_t size = 0;
for (;*s; ++s) {
size++;
}
return size;
}
Example 2: A simple implementation of strlen
As you can see from this implementation, you can modify locally the value of str
to traverse through the string. This implementation does not stop you from incrementing the pointer of the string you
pass into the function as long as you don’t modify the contents of what str
is pointing to. We can see this with another example, where we point const char *s
to a totally different area in memory than the one we passed into the function.
#include <string.h>
#include <stdio.h>
int puts_f(const char *s) {
char *s2 = "baka";
s = s2;
printf("%s\n", s);
}
int main () {
char s[5];
strncpy(s, "zaku", 4);
s[4] = '\0';
puts_f(s);
}
Example 3: Changing the value of a pointer of the declaration const char *
The output of example 3 would be baka
instead of the string, zaku
, which we passed into the function puts_f
. This example is another illustration how const char *s
only qualifies char
to be non-mutable but not the pointer s
itself.
However, this does not mean a function with the argument const char *s
cannot modify the string you are pointing to. As I said const
qualifier is just for documentation purpose and your compiler
will try to prevent any modification. You can explicitly remove the const
qualifier when you assign its value to another pointer. With the new pointer, you can modify the original string as seen in example 4:
#include <stdio.h>
#include <string.h>
size_t strlen_f(const char *str) {
size_t size = 0;
char *s = (char *) str;
s[0] = 'h';
for (;*str; ++str) {
size++;
}
return size;
}
int main() {
char s[5];
strncpy(s, "zaku", 4);
s[4] = '\0';
printf("the length of %s is ", s);
printf("%d\n", strlen_f(s));
printf("string after calling strlen_f: %s\n", s);
return 0;
}
Example 4: Modifying the string passed into the function as const char *
The above code is an example of being able to mutate the string you passed into a function as const char *str
as seen with the output below:
the length of zaku is 4
string after calling strlen_f: haku
So what happens if we swap the order of the const?
As I said in my example with const int x
, that the order of the qualifier does not matter. However, when dealing with pointers, it can get a bit confusing. Like before where const int x
and int const x
are the same, const char *s
and char const *s
are equivalent. But the following statements below mean very different things.
const int *s;
int *const s;
const int *const s;
We’ve already seen in our example 4 that const char *s
does not allow the pointer s
make any modifications but does not stop any other pointers from modifying whatever s
is
pointing to. We also learned from example 3 that s
can point to different areas of memory as long as it does not attempt to mutate the values of the memory it points to. The same applies to any other
data type such as const int *s
. We will be exploring the behaviors of const
using the data type int
instead of char
for simplicity sakes but the logic is the same.
So what is the difference between int const *s
and int *const s
? When reading a variable declaration, you can read the statement from right to left. For instance, int const *s
can be read as
s is a pointer to a const
of type int
. Similarly, we can read int *const s
as s is a const
pointer to a int
. See the difference?
The major difference is the placement of const
between the pointer *
(it’s only a pointer symbol during variable declaration. Anywhere else, it’s a dereference operator. Weird isn’t it). The first
declaration means s is pointing to some int
that is non-mutable. The latter refers that the pointer s
is non-mutable but the value in memory is still mutable. I’ll present two examples to make
this clear.
int *const s = 1;
int *s2 = 2;
s = s2;
Example 5: Attempt to make an illegal assignment of int *const s
to another pointer:
If we were to compile this, we would get the following error:
test3.c: In function ‘main’:
test3.c:7:4: error: assignment of read-only variable ‘s’
s = s2;
^
In example 3, we saw that we could change the address where s
points to. So why can we no longer do that when we change the declaration of s
to int *const s
? As I said before, s
is no longer
a pointer pointing to somewhere that is “write-protected”. With the new declaration, the address where s
points to is “write-protected” while the data is not as seen below:
int *const s = (int *)malloc(sizeof(int));
*s = 1;
printf("before modification: %d\n", *s);
*s = 2;
printf("after modification: %d\n", *s);
Example 6: Changing the value pointed by int *const
:
Output:
before modification: 1
after modification: 2
Example 6 illustrates that modifications are allowed to variables declared as int *const
as long as the pointer is not changed. Looking at all these examples I have provided so far, you should be able
to know the behavior of the declaration: const int *const s
Following the behavior of const int *s
and int *const s
, const int *const s
means that both the pointer and the memory it is pointing to is non-mutable. So we cannot make any modification of
any kind to s
.
Summary
- the order where you place
const
matters when dealing with pointers const int *x
- x is a pointer to aconst
intint *const x
- x is aconst
pointer to an intconst int *const x
- x is aconst
pointer to aconst
int- Compiler will try to prevent writes to a
const
but there are ways to forgo the protection const
serves mainly for documentation purposes to the developers and the compiler- compiler can try to use the additional information for optimization