As software developers, we rely on variables to store and manipulate data in our programs. However, it is crucial to understand the scope of a variable and how it affects its accessibility and lifetime. In C and C++, the scope of a variable determines where in the program it can be used and for how long it will exist. In this blog post, we will be exploring the different types of scopes in C/C++ and the best practices for handling them to write clean, maintainable, and effective code.

We will look at global, local, and member scopes and how they affect the lifetime of variables. We will also discuss how to properly handle pointers, which have their own unique set of considerations when it comes to scope. By understanding the different types of scopes and how to handle them, you will be equipped to make conscious decisions about how you use variables in your code, leading to more reliable, efficient, and maintainable programs.

Variable Scope Awareness

Awareness of variable lifetimes and scopes, particularly when it comes to pointers, is critical to writing clean and effective C/C++ code. The lifetime of a variable is the period of time during which it is allocated memory and exists in the program. In C/C++, variables can have three different scopes: global scope, local scope, and member scope.

Global Scope Variables

Global scope variables are declared outside of all functions and are accessible throughout the entire program. They have a longer lifetime and persist throughout the execution of the program, but using too many global scope variables can lead to cluttered code and potential naming conflicts. However, in my mind, the more serious implications of improper use of a global variable is race conditions.

A race condition occurs when two or more threads access a shared resource, such as a global variable, simultaneously and the final result depends on the timing of the access. In a safety critical environment, where errors in the system can have severe consequences, race conditions can cause significant harm.

// Example of a global variable, including a potential race condition
int32_t g_temperature_C = 0;

void thread1(void)
{
  // Read the temperature from the sensor
  g_temperature_C = ReadTemperatureFromSensor();
}

void thread2(void)
{
  if ((g_temperature_C > 0) && (g_temperature_C < 70)) // !! Simple race condition
  {
    // Do some safety critical work
  }
  else
  {
    // Manage temperature out of bounds (i.e., cool down or heat up)
  }
}
Code language: C++ (cpp)

In the example above, thread2 is doing some safety critical work, but only when g_temperature_C is within a certain range, which is updated in thread1. If the temperature is out of bounds, then the system needs to take a different action. The issue here is that the wrong action can lead to serious consequences, either for the safety of the system, or in the case where humans are involved, the safety of the user.

In this case, a global variable is a poor choice of scope for g_temperature_C.

If you find you do have to use global variables, you can still limit their scope to the specific compilation unit where they are defined (i.e., the file where the variable is declared). You can do this by adding the static keyword to the variable declaration. The advantage to this is that it limits the scope of the variable to just the specific module, rather than the entire program.

// Limit scope of global variable to the specific compilation unit (i.e., this file)
static int32_t g_temperature_C = 0;
Code language: C++ (cpp)

Local Scope Variables

Local scope variables, on the other hand, are declared within a function or block and are only accessible within that specific scope. They have a shorter lifetime, are allocated on the stack, and are automatically deallocated from memory once the function or block has finished execution. Using local scope variables is recommended over global variables as they limit the potential for naming conflicts, allow for cleaner code, and also eliminate race conditions.

// Example of a local variable, resolving the race condition above
void thread2(void)
{
  int32_t l_temperature_C = ReadTemperatureFromSensor();
  if ((l_temperature_C > 0) && (l_temperature_C < 70)) // !! NO race condition
  {
    // Do some safety critical work
  }
  else
  {
    // Manage temperature out of bounds (i.e., cool down or heat up)
  }
}
Code language: C++ (cpp)

As you can see, the race condition from using a global variable is avoided here because the variable is local and cannot be changed outside of this function.

Member Scope Variables

Member scope variables, also known as class member variables, are declared within a class and are accessible by all member functions of that class. Their scope is tied to the lifetime of the object they are a member of.

#include <iostream>

class TemperatureSensor
{
public:
  TemperatureSensor() = default;

  void GetTemperature()
  {
    m_temp_C = ReadTemperatureFromSensor();
    return m_temp_C;
  }

private:
  int32_t m_temp_C{0};
};

int main()
{
  TemperatureSensor sensor;
  std::cout << "Temperature: " << sensor.GetTemperature() << std::endl;
  std::cout << "Temperature: " << sensor.GetTemperature() << std::endl;

  return 0;
}Code language: PHP (php)

You can think of the scope of member variables to be similar to that of static global variables. Instead of being limited to the compilation unit where they are declared, they are limited to the scope of the class that they are part of. Race conditions on member variables are a real possibility. Precautions must be taken to ensure you avoid them, such as proper locking or an improved architecture to avoid the race altogether.

Properly Scoping Pointers

Pointers are a powerful tool in C and C++, allowing you to efficiently work with data objects in your programs. However, naive usage of pointers can lead to significant problems, including hard to find bugs and difficult to maintain code.

In C and C++, pointers have their own lifetime, separate from the objects they point to. When a pointer goes out of scope, the object it referenced remains in memory but is no longer accessible. When dynamically allocating memory, this leads to memory leaks where the memory is not properly deallocated, leading to a buildup of memory usage over time.

Smart Pointers

To prevent memory leaks and ensure that your programs are efficient and reliable, it is important to handle pointers with care. Modern C++ provides smart pointers types, which automatically manage the lifetime of objects and deallocate them when they are no longer needed. Using smart pointer types of std::shared_ptr and std::unique_ptr, you can be assured that when you create (and allocate) a pointer to an object, that object is constructed (and initialized if following RAII principles) and the pointer is valid. Then, when that pointer goes out of scope, the object is destructed and the memory is deallocated.

#include <memory>
#include <iostream>

void PrintTemperature()
{
  // Create a unique pointer to a TemperatureSensor object
  std::unique_ptr<TemperatureSensor> pTS = std::make_unique<TemperatureSensor>();
  
  // Use the unique pointer within the scope of the current function
  std::cout << "Temperature: " << pTS->GetTemperature() << std::endl;
  
  // The unique pointer goes out of scope at the end of the main function
  // and its dynamically allocated memory is automatically deallocated

}Code language: PHP (php)

When working with raw pointers, it’s critical to be aware of the lifetime of the objects being pointed to. For example, if the lifetime of the object ends before the pointer is deallocated, the pointer becomes a “dangling pointer”. This can cause undefined behavior, such as crashing the program or returning incorrect results. Smart pointers are typically a better choice and avoid this risk by managing the lifetime of the object themselves.


In conclusion, understanding and properly handling the scope of variables in C/C++ is a crucial aspect of writing clean, maintainable, and effective code. By becoming familiar with global, local, and member scopes, and considering the lifetime and accessibility of variables, you can make informed decisions about how to use variables in your programs.

Additionally, pointers require their own set of considerations when it comes to scope, and it is essential to handle them with care to prevent memory leaks and other issues.

By following best practices and being aware of the potential pitfalls, you can ensure that your programs are reliable, efficient, and easy to maintain.

Last modified: February 13, 2023

Author

Comments

Write a Reply or Comment

Your email address will not be published.