We have thus far been declaring our variables outside of any function body. Variables declared in this way are considered global variables.
We can access these variables anywhere within the same file.
// A global variable
int x { 5 };
void Increment() {
x++; // We have access to x here
}
int copyOfX { x }; // And here
int main() {
cout << x; // And here
}
This is often not a good thing - when we have lots of variables, having them accessible and modifiable from anywhere can become very messy. Additionally, when our software spans multiple files, which it will do later, there are further complications to worry about.
We will soon see many different ways to organize our data in preparation for more complex programs, but for now, let's introduce the local scope.
Names that are declared within the body of a function have local scope. These variables are only available to that function.
We have already seen an example of local scope with our function parameters. Parameters have local scope:
int Increment(int x) {
return x + 1;
}
// x is not defined in this scope
int CopyOfX { x };
We can also declare additional variables within the body of a function. Those variables will also have local scope, and only be available within that function.
int Health { 100 };
bool isDead { true };
void TakeDamage(int Damage) {
int DamageToInflict = isDead ? 0 : Damage;
Health -= DamageToInflict;
}
// Neither of these variables are defined in this scope
int MyVariable { Damage };
int AnotherVariable { DamageToInflict };
We can note that from the global scope, we cannot access variables inside the function scope. However, from the function scope, we can access variables in the global scope.
This is an important concept. We can think of scopes as being nested inside of each other. The function scope is nested within the global scope where the function is defined.
These relationships are often described in terms of a family tree - a function's scope might be a "child" of the global scope; that global scope would be the "parent" of the function's scope.
With this relationship, a child has access to its parent scope, but a parent does not have access to the scope of any of its children.
If a line of code is accessing a variable that does not exist in the same scope as that line of code, the compiler will then check the parent scope.
If it's not defined there either, it will check the parent of that scope. It will keep moving up the tree until it either finds the variable, or reaches the top at which point it will throw an error.
What is the value of Result
after running this code?
int x { 1 };
int Add(int y) {
return x + y;
}
int Result { Add(2) };
Because of this nesting, it is possible to have multiple variables of the same name, accessible within the same scope:
int x { 1 };
int GetNumber() {
int x { 2 };
return x;
}
In this scenario, both the x
from the local scope and the x
from the parent scope are accessible in our GetNumber
function.
This code will work, and the function returns 2
, because the narrowest available scope is used before the compiler checks outer scopes.
However, where possible, this variable shadowing (sometimes called name masking) is something to avoid, as it can make our code a bit more difficult to understand for humans.
What is the value of Result
after running this code?
1int x { 1 };
2int Add(int y) {
3 int x { 2 };
4 return x + y;
5}
6int Result { Add(x) };
7
What value will GetNumber
return?
1int x { 1 };
2
3int GetNumber() {
4 x = 2;
5 int x { 3 };
6 return x;
7}
8
What will be the value of Result
after running the following code? Only the highlighted line has changed from the previous question.
1int x { 1 };
2
3int GetNumber() {
4 x = 2;
5 int x { 3 };
6 return x;
7}
8
9int Result { x + GetNumber() };
10
What will be the value of Result
after running the following code? Only the highlighted line has changed from the previous question.
1int x { 1 };
2
3int GetNumber() {
4 x = 2;
5 int x { 3 };
6 return x;
7}
8
9int Result { GetNumber() + x };
10
We have been creating block scopes (sometimes called statement scopes) when we use things like if
statements. These blocks, enclosed by {
and }
have their own scope, where we can also define variables.
1int CalculateDamage(int Damage) {
2 if (isEnraged) {
3 int DamageToInflict = Damage * 2;
4 } else {
5 int DamageToInflict = Damage;
6 }
7
8 // DamageToInflict is not defined in this scope
9 return DamageToInflict;
10}
11
This above example will result in an error - but not for the reason many might assume. We are creating two variables with the same name, but this is not an issue, because the two variables are being created in different scopes.
We have a variable inside the scope of the if
block, and another inside the scope of the else
block. The issue comes when we try to access either of these variables on line 9.
The variables are in the scopes created by the if
and else
blocks. These blocks are children of the function, but the function scope does not have access to its children. Therefore, line 9 is not valid.
Like with earlier examples, we can access and modify variables in parent scopes. Therefore, we can approach this problem by defining the variable in the function scope, and then modifying it within the child scopes created by the if
and else
blocks.
With those changes, our code would work correctly:
int CalculateDamage(int Damage) {
int DamageToInflict;
if (isEnraged) {
DamageToInflict = Damage * 2;
} else {
DamageToInflict = Damage;
}
return DamageToInflict;
}
The above code does let us demonstrate some properties of scopes, but it's always reflecting on our entire approach. Is there a simpler way to write this function?
Remembering earlier topics on using early return statements, we could simplify things to this:
int CalculateDamage(int Damage) {
if (isEnraged) {
return Damage * 2;
}
return Damage;
}
Or, we could use a ternary statement to create the same logic in a single line of code:
int CalculateDamage(int Damage) {
return isEnraged ? Damage * 2 : Damage;
}
What is the value of Result
after running this code?
int Maths(int x, int y, bool subtract) {
if (subtract) {
int SubtractResult = x - y;
} else {
int AdditionResult = x + y;
}
return subtract ? SubtractResult : AdditionResult;
}
int Result { Maths(1, 2, false) };
We've seen a few scenarios now where we add {
and }
to our code.
The most common case has been if
statements. We add {
and }
to do multiple things when our if
statement is true.
There are many names for these - "compound statements", "block statements" or simply "blocks".
We can introduce these blocks independant of any other code. They don't need to be attached to if
statements or anything else.
The main effect of this is that it creates a new scope.
int main() {
string Message { "Parent Scope" };
cout << Message;
{
string Message { "Child Scope" };
cout << Message;
}
cout << Message;
}
This program will log out "Parent Scope"
, "Child Scope"
, and then "Parent Scope"
again.
Adding arbitrary blocks like this is a rather niche thing to do, but hopefully this example did help solidify our knowledge of child and parent scopes.
Up next, we're going to introduce forward declarations, which will allow us to work around the restriction that all our functions have to be defined before we can use them.
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way