Hello! Welcome to your seventh lesson in the "C++ for ESP32" course.
Introduction
In our last lesson, we distinguished between stack and heap memory, establishing a key principle for embedded systems: prefer the fast, automatic memory of the stack whenever possible. We saw that variables can live in different memory regions and have different lifetimes.
Today, we'll build on that by learning how to protect those variables from unintended changes. The learning outcome for this lesson is to apply the const keyword to variables, pointers, and member functions.
The const keyword is a promise you make to the compiler—and to other programmers reading your code—that a piece of data will not be modified. Using const correctly, a practice known as "const correctness," is a hallmark of professional C++ code. It allows the compiler to catch a whole class of bugs at compile time, makes your code's intent clearer, and can even open up opportunities for optimization. For resource-constrained devices like the ESP32, this level of compile-time safety is invaluable.
1. The Basics: Constant Variables
The most straightforward use of const is to declare a variable whose value cannot be changed after it's initialized. Think of it as defining a named constant rather than a variable.
// This is a compile-time constant. Its value cannot be changed.
const int SENSOR_PIN = 34;
// This is also a constant.
const float PI = 3.14159;
// Attempting to change it will result in a compiler error.
// SENSOR_PIN = 35; // ERROR: assignment of read-only variable 'SENSOR_PIN'
This is superior to using #define for constants because const variables are type-safe and respect scope, which helps prevent subtle bugs.
Let's start with a video from The Cherno that introduces the fundamental idea behind the const keyword and its simplest application.
Please watch from the beginning to 02:15. This section explains the purpose of const and demonstrates its basic use with variables.
2. The Tricky Part: const and Pointers
When const is used with pointers, its meaning depends on its position. This is a common point of confusion, but it's essential for understanding how to work with data safely. There are three combinations to learn.
To make sense of the syntax, a helpful trick is to read the declaration from right to left.
-
Pointer to a Constant Value (
const int*)- Declaration:
const int* ptr;(orint const* ptr;— they are equivalent). - Read it: "ptr is a pointer to an integer that is constant."
- Meaning: You cannot change the value at the memory location the pointer points to through this pointer. You can, however, change where the pointer itself points.
int x = 10; int y = 20; const int* ptr = &x; // *ptr = 15; // ERROR: You promised not to change the value it points to. ptr = &y; // OK: You can change which address the pointer holds. - Declaration:
-
Constant Pointer to a Value (
int* const)- Declaration:
int* const ptr = &some_int;(Must be initialized). - Read it: "ptr is a constant pointer to an integer."
- Meaning: You cannot change the address the pointer holds (i.e., you can't make it point to something else). You can, however, change the value at the location it points to.
int x = 10; int y = 20; int* const ptr = &x; *ptr = 15; // OK: The value at the address can be changed. // ptr = &y; // ERROR: You promised the pointer itself would not change. - Declaration:
-
Constant Pointer to a Constant Value (
const int* const)- Declaration:
const int* const ptr = &some_int;(Must be initialized). - Read it: "ptr is a constant pointer to an integer that is constant."
- Meaning: You can change neither the pointer itself nor the value it points to. It's locked down completely.
int x = 10; const int* const ptr = &x; // *ptr = 15; // ERROR: The value is constant. // ptr = &y; // ERROR: The pointer is constant. - Declaration:
This is a dense topic, so let's watch a detailed explanation.
The same video from The Cherno provides an excellent breakdown of these three pointer scenarios.
Watch from 02:15 to 05:48. Pay close attention to the code examples for each of the three types of const pointers.
To reinforce this, you can also review the "Const Keyword With Pointer Variables" section in the GeeksForGeeks article, which provides clear code examples for each case.
3. const Correctness: Member Functions and Objects
The concept of const extends powerfully to classes and objects. This is where the idea of "const correctness" really comes into play.
const Member Functions
You can declare a member function as const. This is a promise that calling this function will not change the observable state of the object.
class TemperatureSensor {
private:
int sensorPin;
float lastReading;
public:
TemperatureSensor(int pin) : sensorPin(pin), lastReading(0.0) {}
// This is a const member function. It promises not to change
// sensorPin or lastReading.
float readValue() const {
// lastReading = analogRead(sensorPin); // ERROR! Cannot modify a member variable.
return analogRead(sensorPin) * 0.1; // OK, just reading.
}
void calibrate() {
// This is NOT a const function, because it modifies the object's state.
// ... calibration logic that might change internal variables ...
}
};
The const keyword goes after the function's parameter list.
Why is this so important?
Because it determines what you can do with const objects. A const object can only call const member functions.
This is extremely relevant to what we learned about references. A common and efficient pattern is to pass large objects to functions by const reference to avoid making a copy.
void printTemperature(const TemperatureSensor& sensor) {
// 'sensor' is a const reference. We are promising not to change it.
float temp = sensor.readValue(); // OK, because readValue() is a const function.
// sensor.calibrate(); // ERROR! Cannot call a non-const function on a const reference.
std::cout << "Temperature: " << temp << std::endl;
}
If readValue() were not marked const, the compiler would assume it might change the object, and the call inside printTemperature would be forbidden, generating a compile-time error.
This video introduces the philosophy of 'const correctness' using a real-world analogy, which helps frame why these rules are so beneficial.
Watch the following segments: Introduction (00:00 - 01:44): Understand the core idea behind const correctness. Rule 1: Const References (01:44 - 03:17): This directly connects to our use of const references for function parameters. Rules 3 & 4: Const Getters and Methods (04:10 - 06:32): This explains the need to mark member functions as const and the common errors beginners face.
The mutable Keyword
What if a const member function needs to change a member variable for a legitimate, non-observable reason, like caching a computed value or for debugging? The mutable keyword provides a specific exception. A member variable declared mutable can be modified inside a const member function. Use it sparingly.
The Cherno briefly explains the mutable keyword as an escape hatch from const's restrictions.
Watch from 11:08 to 12:01 to see how and why mutable is used.
4. Best Practices and Common Pitfalls
Applying const consistently is a discipline. Here are some key guidelines to follow, which will help you write safer and more professional code.
The C++ const Keyword: A Practical Guide
This article, "The C++ const Keyword: A Practical Guide", has an excellent section summarizing common mistakes. Reading this will help you avoid them in your own code.
Find and read the section titled "Common Pitfalls and How to Avoid Them". It clearly lists five key points to remember.
In summary, the key rules of const correctness are:
- Use
constfor variables that shouldn't change. - Pass objects by
constreference when the function doesn't need to modify them. - Mark any member function that does not modify the object's state as
const. - Be very careful with
const_cast—it's usually a sign of a design problem. - Understand the difference between a
constpointer and a pointer-to-const.
Conclusion
Mastering const is a significant step toward writing professional C++ code. It's not just about adding a keyword; it's about designing your code to be robust, clearly communicating your intentions, and leveraging the compiler as a safety net.
Key Takeaways:
constvariables must be initialized and cannot be changed.- The position of
constwith pointers matters: it can make the data constant, the pointer constant, or both. constmember functions promise not to modify the object's state and are the only functions that can be called onconstobjects orconstreferences.- Adhering to "const correctness" prevents a wide range of bugs by enforcing your design intentions at compile time.
In our next lesson, we will explore basic inheritance, a fundamental pillar of object-oriented programming that allows you to create new classes that reuse, extend, and modify the behavior of existing ones. We'll see that const correctness plays an important role there as well.