17

I am trying to get a sense of how I should use const in C code. First I didn't really bother using it, but then I saw a quite a few examples of const being used throughout. Should I make an effort and go back and religiously make suitable variables const? Or will I just be waisting my time?

I suppose it makes it easier to read which variables that are expected to change, especially in function calls, both for humans and the compiler. Am I missing any other important points?

c00kiemonster
  • 22,241
  • 34
  • 95
  • 133
  • 1
    everything's a variable anyways. const is just, as you say, a hint that the assigned value shouldn't be changed. but you can fiddle with pointers and whatnot to change that value behind the compiler's back: http://stackoverflow.com/questions/3801557/can-we-change-the-value-of-a-constant-through-pointers – Marc B Jan 18 '13 at 15:16
  • Related: [Sell me on const correctness](http://stackoverflow.com/questions/136880/sell-me-on-const-correctness) (it's C++, but is also relevant to C). – netcoder Jan 18 '13 at 15:18
  • I wouldn't bother going back on applications that work and you are done with. If you have libraries though, it wouldn't be bad to make them use `const`. At some points you would have to give up though. For example `const` and multi-dimensional arrays really work terribly together. – Shahbaz Jan 18 '13 at 15:21
  • Yeah you're absolutely right, its about readability. As @Mark B says its easy enough to change the value of a const via the backdoor if someone wanted to, so you're just basically signalling your intent. C can be very terse as it is so anything that can assist readability has got to be good. – PeteH Jan 18 '13 at 15:23
  • There are two very different types of `const`ness in C++ that should not be confused. A `const` object is an object that cannot be modified by any means. A `const` pointer or reference is just a pointer or reference that cannot be *used* directly to modify an object without tricks, though the object can [still be modified other ways](http://ideone.com/3NzrYK). – David Schwartz Jan 18 '13 at 15:27
  • 1
    [**The C++ 'const' Declaration: Why & How**](http://duramecho.com/ComputerInformation/WhyHowCppConst.html) – Grijesh Chauhan Jan 18 '13 at 15:42

4 Answers4

15

const is typed, #define macros are not.

const is scoped by C block, #define applies to a file (or more strictly, a compilation unit).

const is most useful with parameter passing. If you see const used on a prototype with pointers, you know it is safe to pass your array or struct because the function will not alter it. No const and it can.

Look at the definition for such as strcpy() and you will see what I mean. Apply "const-ness" to function prototypes at the outset. Retro-fitting const is not so much difficult as "a lot of work" (but OK if you get paid by the hour).

Also consider:

const char *s = "Hello World";
char *s = "Hello World";

which is correct, and why?

cdarke
  • 42,728
  • 8
  • 80
  • 84
  • So in your example I would assume that I can't change the `"Hello World"` part in the first line (but I can make `s` point to some other string altogether), is that correct? I have seen that notation here and there too, is that how strings (that is, arrays of `char`) should be instantialized? – c00kiemonster Jan 18 '13 at 15:30
  • @c00kiemonster: If the contents can be modified, don't make it `const`. But in the example above, `s` *is* a constant and so can't be modified. – David Schwartz Jan 18 '13 at 15:31
  • 1
    @c00kiemonster: yes, the first example, with the `const` is correct. The second example is incorrect but unfortunately common. It would lead to an attempt to alter read-only memory (crash!). Long time ago Visual Studio 5 alllowed that, and gave some "interesting" effects. – cdarke Jan 18 '13 at 15:42
  • @cdarke no the second example is sadly correct but implemented defined... I hope some day C standard will change this fact as 99% of compiler don't allow it. I still not understand why compiler don't produce warning when their define the modification of string literal as undefined behavior. – Stargateur Apr 26 '18 at 06:49
  • @Stargateur: I suppose it depends on what you mean by "correct" – cdarke Apr 26 '18 at 07:01
7

How do I best use the const keyword in C?

Use const when you want to make it "read-only". It's that simple :)

stkent
  • 19,772
  • 14
  • 85
  • 111
P.P
  • 117,907
  • 20
  • 175
  • 238
3

Using const is not only a good practice but improves the readability and comprehensibility of the code as well as helps prevent some common errors. Definitely do use const where appropriate.

Ivaylo Strandjev
  • 69,226
  • 18
  • 123
  • 176
2

Apart from producing a compiler error when attempting to modify the constant and passing the constant as a non-const parameter, therefore acting as a compiler guard, it also enables the compiler to perform certain optimisations knowing that the value will not change and therefore it can cache the value and not have to read it fresh from memory, because it won't have changed, and it allows it to be immediately substituted in the code.

C const

const and register are basically the opposite of volatile and using volatile will override the const optimisations at file and block scope and the register optimisations at block-scope. const register and register will produce identical outputs because const does nothing on C at block-scope on gcc C -O0, and is redundant on -O1 and onwards, so only the register optimisations apply at -O0, and are redundant from -O1 onwards.

#include<stdio.h>

int main() {
    const int i = 1;
    printf("%d", i);
}
.LC0:
  .string "%d"
main:
  push rbp
  mov rbp, rsp
  sub rsp, 16
  mov DWORD PTR [rbp-4], 1
  mov eax, DWORD PTR [rbp-4] //load from stack isn't eliminated for block-scope consts on gcc C unlike on gcc C++ and clang C, even though value will be the same
  mov esi, eax
  mov edi, OFFSET FLAT:.LC0
  mov eax, 0
  call printf
  mov eax, 0
  leave
  ret

In this instance, with -O0, const, volatile and auto all produce the same code, with only register differing c.f.

#include<stdio.h>
const int i = 1;
int main() {
    printf("%d", i);
}
i:
  .long 1
.LC0:
  .string "%d"
main:
  push rbp
  mov rbp, rsp
  mov eax, DWORD PTR i[rip] //load from memory
  mov esi, eax
  mov edi, OFFSET FLAT:.LC0
  mov eax, 0
  call printf
  mov eax, 0
  pop rbp
  ret

with const int i = 1; instead:

i:
  .long 1
.LC0:
  .string "%d"
main:
  push rbp
  mov rbp, rsp
  mov eax, 1  //saves load from memory, now immediate
  mov esi, eax
  mov edi, OFFSET FLAT:.LC0
  mov eax, 0
  call printf
  mov eax, 0
  pop rbp
  ret

C++ const

#include <iostream>

int main() {
    int i = 1;
    std::cout << i;
}
main:
  push rbp
  mov rbp, rsp
  sub rsp, 16
  mov DWORD PTR [rbp-4], 1 //stores on stack
  mov eax, DWORD PTR [rbp-4] //loads the value stored on the stack
  mov esi, eax
  mov edi, OFFSET FLAT:_ZSt4cout
  call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
  mov eax, 0
  leave
  ret

#include <iostream>

int main() {
    const int i = 1;
    std::cout << i;
}
main:
  push rbp
  mov rbp, rsp
  sub rsp, 16
  mov DWORD PTR [rbp-4], 1 //stores it on the stack
  mov esi, 1               //but saves a load from memory here, unlike on C
                           //'register' would skip this store on the stack altogether
  mov edi, OFFSET FLAT:_ZSt4cout
  call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
  mov eax, 0
  leave
  ret

#include <iostream>
int i = 1;
int main() {

    std::cout << i;
    }
i:
  .long 1
main:
  push rbp
  mov rbp, rsp
  mov eax, DWORD PTR i[rip] //load from memory
  mov esi, eax
  mov edi, OFFSET FLAT:_ZSt4cout
  call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
  mov eax, 0
  pop rbp
  ret

#include <iostream>
const int i = 1;
int main() {

    std::cout << i;
    }
main:
  push rbp
  mov rbp, rsp
  mov esi, 1 //eliminated load from memory, now immediate
  mov edi, OFFSET FLAT:_ZSt4cout
  call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
  mov eax, 0
  pop rbp
  ret

C++ has the extra restriction of producing a compiler error if a const is not initialised (both at file-scope and block-scope). const also has internal linkage as a default on C++. volatile still overrides const and register but const register combines both optimisations on C++.

Even though all the above code is compiled using the default implicit -O0, when compiled with -Ofast, const surprisingly still isn't redundant on C or C++ on clang or gcc for file-scoped consts. The load from memory isn't optimised out unless const is used, even if the file-scope variable isn't modified in the code. https://godbolt.org/z/PhDdxk.

Lewis Kelsey
  • 4,129
  • 1
  • 32
  • 42