Constants in C and C++: const, define, constexpr Explained
Learn to declare constants in C and C++ using const, #define, enum, and constexpr. Includes examples, comparison table, and placement MCQ patterns.
C and C++ provide four ways to declare constants: the const keyword, #define macros, enum values, and C++‘s constexpr. Placement tests repeatedly probe the type-safety and scoping differences between them.
Why Constants Matter in Placement Tests
Placement MCQs on constants typically test three things. First, what the compiler rejects: assigning a new value to a const variable is a compile-time error, not a runtime warning. Second, what the preprocessor replaces: #define substitutions happen before the compiler sees the source, so type rules don’t apply. Third, what is known at compile time versus run time: this is the boundary constexpr clarifies in C++.
A candidate who treats const and #define as interchangeable tends to miss the questions that hinge on type checking, scope, and debuggability. These are not edge cases in placement rounds. They appear in technical assessments that include a C/C++ section.
Constant-related questions also appear in common data structures questions sections, where const parameters in function signatures are tested alongside algorithmic logic.
The const Keyword
The const keyword marks a variable as read-only. The compiler enforces this: any assignment to a const variable after initialization is a compile-time error, per the cppreference const qualifier specification.
Basic usage in C:
#include <stdio.h>
int main() {
const int MAX_LIMIT = 100;
printf("Max limit: %d\n", MAX_LIMIT);
/* MAX_LIMIT = 200; */ /* compile error: assignment of read-only variable */
return 0;
}
const works in both C and C++. In C, a const variable still occupies memory and has an address; it is not a true compile-time constant in C99/C11. In C++, a const int initialized with an integer literal is a compile-time constant and can appear as an array dimension or template argument.
const and Pointers
Pointer-const interactions are among the most common MCQ traps. The rule: read the declaration right to left.
const int *pmeans a pointer to a constant int. The int cannot change; the pointer can be redirected.int *const pmeans a constant pointer to an int. The pointer cannot be redirected; the int can change.const int *const plocks both.
#include <stdio.h>
int main() {
const int x = 10;
const int *p = &x; /* pointer to const int: OK */
/* *p = 20; */ /* compile error */
return 0;
}
A common question: which declaration prevents the pointer from being reassigned? The answer is int *const p, not const int *p. This is the single most frequent pointer-const mistake in placement rounds.
const in Function Parameters
Passing a pointer as const int * in a function parameter tells the caller that the function will not modify the value at that address. This pattern is standard in C and C++ API design. MCQs test it directly: which declaration gives a function read-only access to an array?
Array problems in C frequently use this pattern: the function takes a const int *arr parameter to signal it reads but does not write the array.
#define Macros
#define is a preprocessor directive. It performs textual substitution before the compiler sees the source, as described in the cplusplus.com preprocessor reference. There is no type, no scope, no memory allocation.
#include <stdio.h>
#define PI 3.14159
int main() {
printf("PI = %f\n", PI);
return 0;
}
The preprocessor replaces every occurrence of PI with 3.14159. The compiler never sees the name PI. This has two practical consequences.
First, #define constants cannot be inspected in a debugger by name. Second, macro arguments expand literally, which creates subtle bugs:
#define SQUARE(x) ((x) * (x))
/* SQUARE(i++) expands to ((i++) * (i++)) — undefined behavior */
const and constexpr don’t have this problem because they are compiler constructs, not text substitutions.
#define can also be removed with #undef. There is no equivalent operation for const variables. This makes #define useful for conditional compilation and include guards, but a poor choice for named numeric constants in modern code.
enum for Named Integer Constants
An enum assigns names to integer values and works in both C and C++.
#include <stdio.h>
enum Status { SUCCESS = 0, FAILURE = -1, PENDING = 1 };
int main() {
printf("Success code: %d\n", SUCCESS);
return 0;
}
In C, enum members share the global namespace. Two different enums cannot use the same member name without a conflict. C++ introduced scoped enums (enum class) to fix this:
enum class Color { Red, Green, Blue };
enum class Direction { North, South }; /* no collision with Color::Red */
Color c = Color::Red;
For placement MCQs: plain enum in C is preferable to #define for a set of related integer constants because it provides a named type. In C++, enum class is preferable to both, because it prevents implicit conversion to int and avoids name collisions.
String problems in C often use enum to represent return states (match, no-match, error), which appears in function-design questions.
constexpr in C++: Compile-time Constants
constexpr was introduced in C++11. It does everything const does and additionally guarantees compile-time evaluation when all inputs are compile-time constants, per the cppreference constexpr specifier reference.
#include <iostream>
constexpr int square(int x) {
return x * x;
}
int main() {
constexpr int result = square(5);
std::cout << "Square: " << result << "\n";
return 0;
}
square(5) is evaluated by the compiler, not at run time. The result is baked into the binary. This is useful for array sizes, template arguments, and any expression the compiler must know before the program runs.
When constexpr is called with runtime arguments, the compiler may evaluate it at runtime instead. The constexpr keyword means “evaluate at compile time if you can”; it is not a hard guarantee in all call sites.
const vs constexpr: Quick Comparison
| Feature | const | constexpr |
|---|---|---|
| Language | C and C++ | C++ (C++11 and later) |
| Type safe | Yes | Yes |
| Enforced scope | Yes | Yes |
| Compile-time guaranteed | No (may be runtime in C) | Yes (when inputs allow) |
| Applies to functions | No | Yes |
| Debuggable by name | Yes | Yes |
In modern C++, the default choice for a compile-time constant is constexpr. Use const when the value may not be known until runtime. Use #define only for include guards or platform-specific conditional blocks.
Comparison: Four Methods at a Glance
| Method | C | C++ | Type Safe | Scoped | Notes |
|---|---|---|---|---|---|
const | Yes | Yes | Yes | Yes | May be runtime in C; compile-time in C++ |
#define | Yes | Yes | No | No | Textual substitution; no debug name |
enum / enum class | Yes | Yes | Partial / Yes | No / Yes | Prefer enum class in C++ |
constexpr | No | Yes | Yes | Yes | C++11 and later; applies to functions |
The placement question that most often catches students is the type-safety row. #define PI 3.14159 has no type. If you pass PI where an int is expected, the preprocessor substitutes 3.14159 and the compiler silently truncates it. const double PI = 3.14159 fails loudly if misused with an integer context. That loud failure is a feature, not a bug.
The const vs. constexpr distinction is exactly the type-system reasoning that product-company technical rounds probe. If you want to take that understanding further, TinkerLLM gives you live LLM API calls for ₹299. You build a working project, not another static tutorial. The same first-principles thinking that separates compile-time from runtime in C++ is what you apply when designing which parts of an LLM pipeline run at inference time versus build time.
Primary sources
Frequently asked questions
What is the difference between const and #define in C?
const is a type-safe keyword enforced by the compiler and respects scope; #define is a preprocessor text substitution with no type checking and no scope boundaries.
Can const be used in C, not only in C++?
Yes. const works in both C and C++. In C99 and later, a const variable still occupies memory, so it cannot be used as an array dimension in standard C without a compiler extension.
What does constexpr mean in C++?
constexpr tells the compiler to evaluate an expression at compile time whenever all its inputs are compile-time constants. It also applies to functions, which const cannot do.
How does const interact with pointers in C?
Read the declaration right to left. 'const int *p' means the int value cannot change (the pointer can). 'int *const p' means the pointer cannot change (the int can). 'const int *const p' locks both.
When should I prefer enum over #define for integer constants?
Use enum when you need named integer values with type safety and scope. enum class in C++ prevents name collisions entirely. #define is suitable only for conditional compilation or include guards.
Is constexpr always evaluated at compile time?
Not always. When a constexpr function is called with runtime arguments, the compiler may evaluate it at runtime. Compile-time evaluation is guaranteed only when all inputs are compile-time constants.
A self-paced playground for building with LLMs.
TinkerLLM is FACE Prep's sister property. A guided environment for shipping real LLM applications, the kind of project that earns a paragraph on your resume, not a line.
Try TinkerLLM (₹299 launch)