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:
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.
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 |
|
|
|
Function | Prototype in included .h file
|
Function definition in .cpp file
|
Function definition in primary .cpp file, above main function and no .h file This is old school C-style programming and is considered bad form in C++ and this course. |
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.
|
|
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.
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.
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!
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:
static
, its scope is changed - it's then only visible within that compilation unit.
static
, its lifetime is changed - it then is born the very first time the definition is encountered in the normal flow of execution, and lives until the end of the program. Any subsequent times the flow of execution encounters the definition, it is ignored.
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:
|
|
|
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!
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 const
definition 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 |
|
|
You've got an error, because the global variable PI
is defined twice. However, consider the following:
file1.cpp | file2.cpp |
|
|
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!
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 |
|
|
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.