Scope & Lifetime

Scope

The scope of a variable/object refers to where in a program that the variable/object's name is visible.
When determining a scope of a variable/object, you should be asking youself: "Where can I see it?"

There are six levels of scope that we will discuss. They are:

Expression Scope

Block Scope

Function Scope

File Scope

Module/Object/Class Scope

Global Scope

Lifetime

The lifetime of an variable or object simply refers to the time from when it is created to when it is destroyed. In the first lab, you've seen two examples of lifetimes, those of local variables that are allocated on the stack during compilation and those of objects created with new which are allocated at runtime on the heap.

For an item that lives on the stack, its lifetime begins at declaration and ends when it goes out of scope.

For an item that lives on the heap, its lifetime begins when it is created with new and ends when delete is called on a pointer that points to the item.

Declaration vs. Definition

Item Declaration Definition Combined Declaration & Definition
General Where an item is
first named in the code.
Where an item is first given
value/meaning in the code.
When both happen at the same time.
Variable
int x;
x = 2001;
int x = 2001;
Function Prototype in included .h file
int foo(int x);
Function definition in .cpp file
double foo(int x){
  return 1.0 / (x*x);
}
Function definition in primary .cpp file,
above main function and no .h file
double foo(int x){
  return 1.0 / (x*x);
}
This is old school C-style programming and is
considered bad form in C++ and this course.

How Scope, Lifetime, Declarations, and Definitions Relate

Unlike your typical variable that lives on the stack, heap allocated memory doesn't have a scope, because the memory doesn't actually have a unique name associated with it; there's just a pointer (or more) that points to it. The pointer itself will have a scope and lifetime that is independent of the memory and what's stored there.

Two different examples showing the difference between stack and heap allocated variables.
if (x > 0){
  int k; //<-------- k is declared, but not defined
         //           and has block scope
  k = x*3 + 1; //<---k is defined
  cout << k << endl;
} //<--------------- k goes out of scope
  //                 and is destroyed (lifetime ends)
cout << x << endl;
int *p;  //<-- p is declared, but not defined
if (x > 0){
  int *q = new int; //<-- q is declared and defined and an
                    //    int is allocated on the heap
  *q = x*3 + 1;  //<-- the int on the heap is filled with
                 //    the value of x*3+1 via *q;
  p = q; //<-- p is defined and now points
         //    to the same int in memory as q
} //<-- q goes out of scope
cout << *p << endl; //<-- the int still exists and
                    //    can still be found via p
delete p;  //<-- the int's lifetime ends and the memory is freed
           //    but p is still alive and in scope and could be used again

C++ practical examples

Global variable in a single file program

The following program computes factorials for the user, and keeps track of how many times the factorial function was called with a global variable.


#include <iostream>
using namespace std;

int gcount = 0;

int fact(int n) {
  gcount++;
  if (n == 0) return 1;
  return n*fact(n-1);
}

int main() {
  int k;
  while(cin >> k && k >= 0)
    cout << "k! = " << fact(k) << endl;

  cout << "fact was called " << gcount << " times" << endl;
  return 0;
}

The variable gcount is a global variable. It's scope is called global, and its lifetime is the duration of the program. (Note: if I were to declare a local variable named gcount, it would "mask" the global variable, and any reference to the name gcount would give me the local rather than global.) Global variables, by the way, are usually bad! Typically adding more paramters to functions allows you to do whatever you were trying to do with globals, without the problems of globals. If a global variable is given an initializer, as in this example, it is initialized when it is created, at the very start of the program's execution.

Global variable in a multi-file program

With functions we have the prototype, which declares that a certain function exists, and the definition, which actually defines what the function does. When global variables are used in a multi-file program, we need a mechanism for doing the same thing for our global variables - we need to distinguish the definition (there can be only one) from declarations (of which there may be many). The declarations are needed to tell the other files that this global variable exists, just like prototypes do for functions. Consider this example:

main.cpp

#include <iostream>
using namespace std;
#include "fact.h"
int main() {
  int k;
  while(cin >> k && k >= 0) {
    cout << "k! = " << fact(k) << endl;
  }
  cout << "fact was called " << gcount << " times" << endl;
  return 0;
}

fact.h

// returns the factorial of n
int fact(int n);

// counts times fact's called
extern int gcount;
\______________/
 declares the global variable "count"  

fact.cpp

#include "fact.h"

int gcount = 0;
\___________/
 defines the global variable count

int fact(int n) {
  gcount++;
  if (n == 0)
    return 1;
  return n*fact(n-1);
}

The global variable gcount is actually defined in fact.cpp. That means the variable really lives in fact.o. However, the variable is declared (i.e. its existence is announced) in fact.h. That way when gcount is referred to in main.cpp, which is compiled independently from fact.cpp, the compiler knows from the #include "fact.h" what the name gcount refers to.

File scope variables

A file scope variable is one whose lifetime is the duration of the program, just like global variables, but whose scope is confined to the compilation unit in which it is defined. A file scope variable is defined outside of any enclosing { }'s, just like a global, but the definition is prefixed with the word static. For example, the following file dump.cpp implements the dump function whose prototype appears in dump.h. The function writes any strings it's sent to a file called dump.txt.

dump.cpp

#include "dump.h"
#include <fstream>
using namespace std;

static ofstream Out("dump.txt"); <-- File scope variable!

void dump(string s) {
  Out << s << endl;
}

Because we proceeded the definition of the output file stream Out by the keyword static, it has file scope rather than global scope. It lives for the duration of the program, but is only visible (accessible) inside ofdump.cpp. You can't use it from other files!

Local scope variables that live forever (almost)

If you prefix the definition of a local variable with the word static, it does something a bit different. In this context, it changes the lifetime of the variable, but leaves the scope unchanged. So, we get the same behaviour from this code:

void dump(string s)
{
  static ofstream Out("dump.txt");
  Out << s << endl;
}

as we did from the version with a file scope variable with two crucial differences: First, the output file stream Out is not visible (i.e. not in scope) anywhere outside of the function dump, and second, Out is not created until the first time the function dump is called. This means, for example, that if dump is never called,the file dump.txt is never created. In the previous version, which used file scope, if dump was never called, the filedump.txt would still be created. It would simply be empty.

To summarize:

Dynamic scope: A scope C++ doesn't have

C++ is a big language, meaning it has all sorts of features. One feature it doesn't have is yet another kind of scope - dynamic scope. From the perspective of understanding the subject of programming languages better, however, we need to discuss it.

If a variable name has dynamic scope (remember, scope is really all about names), then the object to which it refers is the first object with the same name you run across when going back up the stack of function calls. For example, suppose we have the following function definitions:

void f(){
  /* x is declared as a
  dynamic scope variable */
  cout << x + x;
}
void g(){
  int x = 5;
  f();
}
void h(){
  string x = "tu";
  f();
  g();
}

Now, if inside main we call g(), when f() gets called it'll look to the previous call on the stack and find g(), see that it has a variable with the name x, use it in the expression x + x, and 10 will be printed out.

If inside main we call h(), when f() gets called the first time, it'll look to the previous call on the stack and find h(), see that it has a variable x defined (the string "tu") and use it in the expression x + x. Since + means concatenation for strings, "tutu" will be printed out. The second time f() get's called, it'll be called from g() which was called from h(). We'll look to the next entry on the call stack, which is g(), use it's definition of x and once again 10 will be printed out.

Dynamic scoping causes some problems! The main one being that niether you nor the compiler can figure out what the dynamic variable refers to at compile time. In the previous example you couldn't even figure out what type it had! Only at runtime can that be known. This means the compiler can't do type-checking. This means you have a hard time reading programs (what's x?). This even makes programs run more slowly. That's why C++ doesn't have dynamic scope. A dynamic scope name cannot be bound to an actual object at compile time (like local variables) or link time (like global variables) but only at run-time. This creates extra overhead and extra possibilities for the program to crash while it's running!

const and how it affects scope

If you put the keyword const before a variable's definition, it makes it a constant - i.e. you cannot change its value. (This means you better initialize the object in it's definition, by the way!) When you make a constdefinition outside of any { }'s (i.e. non-local), it also implicitly makes the definition static, so you have file scope. Say you have the following two files in your program:

file1.cpp file2.cpp
double PI = 3.14159;
...
double PI = 3.14159;
...

You've got an error, because the global variable PI is defined twice. However, consider the following:

file1.cpp file2.cpp
const double PI = 3.14159;
...
const double PI = 3.14159;
...

Because the const gives both definitions file scope, there's no name conflict. This means constant variable definitions can be put inside header files (regular global variable definitions cannot, because of multiple definition problems), which is pretty convenient. The main thing to understand here, is that you want the values of const "variables" to be available at compile time so the names can be replaced with the values. If your const were global, the definition (which has the value) would not be visible within other compilation units - i.e. the value would not be known 'til link time. Too late then, everything's already compiled!

static and inline functions

Functions are global in the sense that they are visible everywhere in the program and may only be defined once. The static keyword may proceed a function definition, giving the function file scope. Thus if you have the following two files in your program:

file1.cpp file2.cpp
static double f(double x)
{
  return x + x;
}
static double f(double x)
{
  return x * x;
}

there's no problem with multiple definitions, because the double f(double x) from file1.cpp isn't visible in file2.cpp and vice versa.

The keyword inline does the same thing in this context as static, but it tells the compiler that you'd like the function's code to be "inlined", which means the body gets stuck directly in the calling code and the function call itself goes away. This can help performance. More of an FYI than anything else.