Last Updated:

C++ : Lifetime and scope | Memory Class Specifications

Lifetime and scope C++

Introduction

The lifetime of a variable can be global or local. A variable with a global lifetime is characterized by the fact that during the entire time of program execution, a memory cell and a value are associated with it. A variable with a local lifetime is allocated a new memory cell each time it enters the block in which it is defined or declared. The lifetime of a function is always global.

The scope of an object (variable or function) determines in which parts of the program the name of this object is allowed.

The scope of the name begins at the declaration point, more precisely, immediately after the declarer, but before the initializer. Therefore, you can use the name as an initializing value for yourself.

int x = x;Strangely!

This is permissible, but not reasonable.

Before a name can be used in a C++ program, it must be declared (declared), i.e., the type of name must be specified so that the compiler knows what kind of entity the name refers to. The definition not only associates a type with a name, but also defines some entity that matches the name. In a C++ program, there must be exactly one definition for each name. There can be several ads. All declarations of an entity must be consistent by the type of that entity.

int count; 
int count;Error - Redefinition
extern int error_number; 
extern short error_number;Error – ad type mismatch

Declarations and definitions written inside a block are called internal or local. Declarations and definitions written outside of all blocks are called external or global.

1.1. Variables

Declaring a variable specifies the name and attributes of the variable. Defining a variable also allocates memory for it. In addition, the definition of a variable sets its initial value (explicitly or implicitly). Therefore, not every variable declaration is a variable definition. Declarations that are not defined include declarations of formal function parameters, as well as declarations with the extern memory class specification, which refer to a variable defined elsewhere in the program (see below).

Variable declarations in C++ have the following syntax:
[< memory class specification>] <type> <descriptor> [= <initializer>] [, <scriptor> [= <initializer>] ...];

The C++ language has four memory class specifications:

  • auto
  • register
  • static
  • extern

The auto and register memory class specifications can only be used internally.

1.1.1. Global Variables

A variable declared at the external level has a global lifetime. If there is no initializer, such a variable is initialized with a null value. The scope of a variable defined externally extends from the point where it is defined to the end of the source file. The variable is not available above its definition in the same source file. The scope of a variable can be extended to other program source files only if its definition does not contain a static memory class specification.

If a static memory class specification is specified in the variable declaration, other variables with the same name and any memory class can be defined in other source files. These variables will not be related in any way.

The extern memory class specification is used to declare a variable defined elsewhere in the program. Such declarations are used when you want to extend to a given source file the scope of a variable defined in another source file at the external level. The scope of the variable extends from the location of the advertisement to the end of the source file. In declarations that use the extern memory class specification, initialization is not allowed because they refer to variables whose values are defined elsewhere.

1.1.2. Local Variables

A variable declared at the internal level is available only in the block in which it is declared, regardless of the memory class. By default, it has an auto memory class. Variables of this class are placed on the stack. The variables of the auto memory class are not automatically initialized, so if there is no initialization in the declaration, the value of the memory class variable auto is considered undefined.

The register memory class specification requires that the variable be allocated memory in the register, if possible. Because working with registers is faster, the register memory class specification is typically used for variables that you want to access very frequently.

For each recursive block entry, a new set of auto and register memory class variables is generated. At the same time, each time the variables in the declaration of which the initializers are specified are initialized.

If a variable declared internally has a static memory specification, the scope remains the same and the lifetime becomes global. Unlike variables of the auto memory class, variables declared with the static memory class specification retain their value when they exit the block. Variables of the static memory class can be initialized with a constant expression. If there is no explicit initialization, the static memory class variable is automatically initialized with a null value. Initialization is performed once at compile time and is not repeated each time the sides are logged in. All recursive calls to this side will separate a single instance of a variable of the static memory class.

A variable declared with the extern memory class specification is a reference to a variable with the same name defined externally in any source program file. The purpose of an internal extern declaration is to make the definition of a variable available within a given block.

File 1

File 2

int a, b; 
static int c;

extern int d = 10;
int m[10];
int f(int x) { int y;

static int z;

static int c; ...
}
Global variables
// The variable cannot be // referenced from another file
// You cannot use the initializer with extern



// Variable of the auto class. Local lifetime and scope.
// Local variable of the static class. Has a global lifetime.
// Local static variable // hides global variable
сcc
extern int a; 
static double c;

int d = 10;
extern int m[];
auto int e; register int e;
int g(int x) { extern int b;

register int w; ...
}
Reference to a variable from file 1
// But you can declare a variable // with the same name in another file


// With extern, you can omit the size of the array
// You cannot use auto and register // at the external level

// Reference to a variable from file 1, // which will be available only in the function g
// Variable of the register class
аcb

1.1.3. Hiding Names

Declaring a name in a block can hide the declaration of that name in the enveloping block or the global name. That is, the name can be substituted inside the block and will refer to another entity there. After leaving the block, the name restores its former meaning. The hidden global name can be accessed by using the scope resolution operation ::. You can use the hidden name of a class member by qualifying it with the name of the class. You can use a hidden global name if you qualify it as a unary scope resolution operation.

intx;
 
void f()
  { double x = 0; Global variable hidden x
    ::x = 2; Assign a global variable x
    x = 2.5; Assigning a local variable x
   }

 
int f(int x) { ... }
 
class X
  {public:
    static int f() { ... }
   };
 
int ff()
  { return X::f(); }

There is no way to access a hidden local variable.

1.1.4. Announcements in terms and cycle for

To avoid accidental misuse of variables, it is better to enter them in the smallest possible scope. In particular, it is better to declare a local variable at the moment when it needs to be assigned a value. In this case, attempts to use the variable before it is initialized are excluded.

One of the most elegant applications of these ideas is declaring a variable in a condition. Let's look at an example.

if (double d = f(x)) y /= d;

The scope of a variable extends from the point of its declaration to the end of the statement controlled by the condition. If there were an else branch in the if statement, the scope of the variable would be both branches. dd

An obvious and traditional alternative is to declare a variable before a condition, but in this case the scope would begin before the vengeance of the use of the variable and would continue after the completion of its "conscious" life.

double d;
...
d2 = d;
...
if (d = f(x))
   y /= d;
...attention!!!
d = 2.0;

Declaring variables in conditions, in addition to giving logical advantages, also leads to a more compact source code.

The declaration in a condition must declare and initialize a single variable or constant.

You can also declare a variable in the initializing part of the for statement. In this case, the scope of the variable (or variables) extends to the end of the statement.

void f(int x[], int n) { for (int i = 0; i < n; i++) x[i] = i * i; }

If you want to know the index value after you leave the loop, you must declare the variable outside of the loop.

1.2. Functions

The declaration of a function (prototype) specifies its name, the type of return value, and the attributes of its formal parameters. Function declarations in C++ have the following syntax:
[<commission class specification>] <type> <name> (<list of formal parameters>);

A function definition specifies the body of a function, which is a composite operator that contains declarations and operators. The definition of a function also specifies its name, the type of return value, and the attributes of its formal parameters. A function definition has the following syntax:
[<commission class specification>] <type> <name> (<list of formal parameters>) { <the function body> }

Functions have a global lifetime.

A function definition can only be defined externally. The scope of the function extends from the definition to the end of the file. To use a call to a function above its definition, you need to write a function declaration (prototype). The prototype of a function can be located at the external level (then calling this function will be possible from any function of the source file) or at the internal level (then calling this function will be possible only from the block in which the prototype is located).

To use a function located in another source file, you must also use a prototype.

If a function is defined with the static memory class specification, it cannot be used in other source files in the program.

Embedded functions are internally composed (i.e., cannot be used in other program files), unless an external layout is explicitly specified using the extern keyword.

2. Namespaces

2.1. Namespace

Namespace is a mechanism for reflecting logical grouping. That is, if some ads can be combined according to some criterion, they can be placed in one namespace to reflect this fact.

Namespace is declared as follows:
namespace <name namespace> { <ads and definitions> }

You cannot declare a new namespace member outside of its definition using an explicit qualifier. This is done so that you can find all the names in the namespace definition and quickly identify errors such as typos and type mismatches.

Namespace is the realm of view. The usual local and global scopes and classes are namespaces. The usual scope rules apply to namespaces as well. If the name is pre-declared in a namespace or in a spanning area, you can then use it without problems. A name from another namespace can be used by explicitly specifying that space as a qualifier.

namespace N1
  {intg();
    char h();
   }
   
namespace N2
  {int f1();
    int f2();
   }
   
int N2::f1() Use the qualifier to indicate that the function f1 is declared // in the N2 namespace, not a global function.
  { return f2() + N1::g(); } Since the function f2 is a member of the namespace N2, there is no need for // to use a qualifier. However, without the use of the qualifier N1 // the function g would be considered undeclared because the terms of the namespace N1 // are not in the field of visibility in space N2.

2.2. Using declarations

If a name is often used outside of its namespace, it is inconvenient to use a qualifier each time. To avoid this, using declarations are used that introduce local synonyms.

namespace N2 { ... using N1::g; }The function g can now be used without a qualifier

Such synonyms should be made as local as possible to avoid naming conflicts.

A single using declaration makes all versions of the overloaded function visible.

2.3. Using Directives

The using directive makes all names in the namespace available.

namespace N2 { ... using namespace N1; }Now all functions from the N1 namespace can be used without a qualifier

We can assume that the using directive is a means of composing namespaces. Functions use the using directive for easy writing. However, the use of global using directives is best avoided whenever possible.

2.4. Unnamed namespaces

Sometimes it is useful to put ads in namespace in order to eliminate the possibility of name conflict. In this case, you can use unnamed namespaces.

namespace { void f() { ... } void g() { ... } ... }

Clearly, there must be a way to access members of an unnamed namespace. Therefore, an unnamed namespace implies the use of the using directive.

namespace XXX { void f() { ... } void g() { ... } ... } using namespace XXX;

Here XXX is some unique name that is given to the namespace by the compiler and is not known to the programmer.

Unnamed namespaces are different in different compilation units. You cannot access a member of an unnamed namespace from another compilation unit from one compilation unit from one compilation unit.

2.5. Searching for names

It is a rule rather than an exception that a function with a parameter of type T is defined in the same namespace as T. Therefore, if a function is not found in the context of its use, its parameters are searched in the namespace.

namespace NS1
  { class X { ... };
    void f(const X &x);
   }

void g(NS1::Xx)
  {f(x);
}

Compared to using explicit qualifiers, this rule of name search saves time when entering a program and does not lead to "contamination" of the namespace, as the using directive can do.

2.6. Aliases of namespaces

Short namespace names can conflict with each other. However, long namespace names are impractical when writing real code. This dilemma can be solved by creating a short alias for the long namespace.

namespace RFBR = Russian_Fund_of_Basic_Researches;

Aliases also allow the user to refer to a "library" and, in a single declaration, identify the library that is actually being used.

namespace Lib = Library_v2r15;

This can greatly alleviate the problem of changing the library version.

2.7. Consolidation and Selection

The combination of combining namespaces using using directives and selecting using declarations provides the flexibility required for most real-world tasks. Using these mechanisms, we can provide access to a variety of means in such a way as to resolve name conflicts and ambiguities that arise from unification.

namespace One
  { class Matrix { ... };
    class Vector { ... };
    ...
  }

namespace Two
  { class Vector { ... };
    class Matrix { ... };
    ...
  }

namespace Three
  { class Set { ... };
    class String { ... };
    ...
  }
namespace All
  { using Three::Set;
 
    using namespace One;
   
    using namespace Two;
   
    using One::Vector;
   
    using Two::Matrix;
   
    classList { ... };
  }

Names explicitly declared in the namespace, including names declared using using declarations, take precedence over names made available by using using directives. Therefore, conflicts will be resolved in favor of One::Vector and Two::Matrix. In addition, All::List will be used by default, regardless of whether there is a List in one space or two space.

3. Layout

3.1. Multi-File Programs

A file is a traditional unit for storing information in the file system and no less than a traditional compilation unit. As a rule, it is impossible to store a program in one file. In particular, the code of standard libraries and the operating system is not provided in the form of source code as part of a user program. Even storing all user code in a single file for real-size applications is both impractical and inconvenient. Splitting a program into files helps to emphasize its logical structure, makes it easier to understand, and allows the compiler to provide this logical structure.

When the compilation unit is a file, the entire compilation must be recompiled when changes are made (no matter how small) to it or to another file on which it depends. Even in the case of a small program, the amount of time spent on recompilation can be significantly reduced by splitting the program into files of the appropriate size.

In order to make separate compilation possible, the programmer must provide declarations that give the type information needed to parse the compilation unit separately from the rest of the program. Declarations in a program consisting of several separately compiled parts must be consistent in exactly the same way as in a program consisting of a single source file.

Organizing a program as a set of source files is commonly referred to as the physical structure of a program. The physical division of the program into different files should be determined based on the logical structure of the program. However, the logical and physical structures of the program do not have to be identical.

The names of functions, classes, templates, variables, namespaces, and enumerations must be consistent across all compilation units, unless these names are explicitly defined as local.

// file1.cpp int x = 1; int b = 0; extern int c; // file2.cpp int x; extern double b; extern int c;

This example contains three errors:

  • The variable is defined twice. x
  • The variable is declared twice with different types. b
  • the variable is declared twice but not defined. c

Errors of this type cannot be detected by a compiler that considers only one file at a time. However, most of these errors are detected by the linker.

If there is a name that can be used in a compilation unit other than the one in which it was defined, an external layout is said to occur. A name that can only be referenced in the unit of compilation in which it is defined is said to be composed internally.

An embedded function must be defined by identical definitions in each compilation unit in which it is used. Therefore, the following example is not just an example of bad taste – it is generally unacceptable.

// file1.cpp inline int f(int i) { return i; } // file2.cpp inline int f(int i) { return i + 1; }

By default, const and typedef imply an internal layout. Therefore, the following example is valid, although it may lead to errors.

// file1.cpp typedef int T; const int x = 7; // file2.cpp typedef void T; const int x = 8;

Global variables that are local to a single compilation unit are a typical source of errors and are best avoided. For consistency, it is best to put global consttypedef, and inline only in header files.

You can force a constant to be externally composed by explicitly declaring it using the extern keyword.

To ensure that the names in a given compilation unit are local, you can use unnamed namespaces. The effect of using unnamed space resembles an internal layout.

C programs and older C++ programs use the static keyword (leading to confusion) to indicate "use internal layout". It is better not to use static except inside functions and classes.

3.2. Header files

In all declarations, the types of the same objects, functions, classes, etc. must be consistent. Therefore, the source code processed by the compiler and then by the linker must be consistent. The simplest method of achieving consistency of declarations in different compilation units is to include header files containing interface information in source files that contain executable code and/or data definitions.

A rule of thumb states that a header file may contain:

  • Named namespaces
  • type definitions;
  • Declarations and template definitions
  • Feature announcements
  • define embedded functions;
  • Data declarations
  • define constants;
  • enumerations;
  • name announcements;
  • inclusion directives;
  • macrodefinitions;
  • Conditional compilation directives
  • Comments.

This rule is not a language requirement. It simply reflects a reasonable way to use header files to express the physical structure of a program. On the other hand, the header file should never contain:

  • definitions of normal functions;
  • data definitions;
  • unnamed namespaces.

3.3. The Single Definition Rule

Each specific class, enumeration, template, etc. must be defined in the program exactly once.

From a practical point of view, this means that there should be exactly one definition, for example, of a class, located somewhere in one file. Unfortunately, the rule of language cannot be so simple. A class definition can be included in two source files. Worse, the "file" concept is not part of the definition of the C++ language.

Therefore, the rule of the standard stating that there must be a unique definition of a class, template, etc. should be stated in a more complex form. This rule is called the "One-Definition Rule" (ODR). Specifically, two definitions of a class, pattern, or built-in function are acceptable as a definition of the same entity if and only if:

  1. they are in different compilation units;
  2. they are identical to lexeme by lexeme;
  3. the token values are the same in both compilation units.

Here are examples of three ways to violate the ODR rule.

file1.cpp
struct S1 {int a; charb; };
struct S1 {int a; charb; };

  Error – Cannot define structure twice in the same compilation unit
  file1.cpp
  struct S2 {int a; charb; };
  file2.cpp
struct S2 {int a; charbb; };

  Error – the names of the members of the definitions are different
  file1.cpp
typedef intX;
struct S3 { X a; charb; };
  file2.cpp
typedef charX;
struct S3 { X a; charb; }; Error – the meaning of X in files is different

Checking the consistency of class definitions across compilation units is typically beyond the capabilities of most C++ implementations. As a consequence, violation of the ODR rule can be a source of very subtle errors. Unfortunately, the technique of putting shared definitions in header files and then including them in the source file does not prevent the latest form of rule violation. Local typedef and macros can change the meaning of the declarations that are included. The best protection against such problems is to create as self-contained header files as possible.

 

3.4. Guardians of Inclusion

 

Real programs usually contain several header files. Trying to represent each logical module of a program as a consistent, self-contained fragment can cause some declarations to be redundant. Such redundancy can lead to errors, because the header file containing the definition of a class or built-in function can be included twice in the same compilation unit.

There are two possible solutions to this problem:

  • reorganize the program in such a way as to eliminate redundancy;
  • Find a method that allows you to re-include header files.

The first approach is quite tedious and impractical when creating real-size programs. In addition, redundancy may be necessary to ensure that individual parts of programs are meaningful in themselves.

The benefits of eliminating redundant inclusions and the resulting simplification of the program can be quite significant both logically and in terms of reducing compilation time. However, the analysis of such redundancy is very rarely complete, so a method of over-inclusion is required. The traditional solution is to insert so-called inclusion guards.

// file1.h #ifndef FILE1_H #define FILE1_H ... #endif

The contents of the file between the #ifndef and the #endif are ignored by the compiler if FILE1_H defined. In this case, the first time the file is viewed at compile time, its contents are read and the FILE1_H is determined. If the file is included in the compilation unit a second time, its contents are ignored. The method is not perfect, but it works and is widely used in C and C++ programs. All standard header files contain inclusion guardians.

Another way to use the inclusion guards is possible.

// file2.h #define FILE2_H ...

// file2.cpp #if ! defined(FILE2_H)

#include "file2.h" #endif