Like so many features of modern programming languages, blocks were introduced by Algol. A block is a delimited sequence of statements. Programming languages delimit blocks in many ways. For example, Pascal delimits blocks with begin and end, and Python uses indentation. C++, Java, C#, and many other programming languages delimit blocks with an opening and closing brace: {....}. These are block structured languages, meaning programs written in these languages consist of blocks, which form function bodies, block or compound statements, classes, namespaces, etc. Understanding blocks is fundamental to understanding C++'s behavior.
The Python programming language uses indentation to delimit blocks. However, C++ and Java use the curly braces "{" and "}" as the block delimiter. If you are transitioning from Python to C++, please focus on how C++ uses the braces in the following discussion.
The Hello World example presented three accepted programming styles illustrating where programmers place the braces defining a function's body - a block. Significantly, all the examples indented the body's statements. Control statements and their attendant blocks make consistent indentation critical. Indentation does not form any part of the C++ syntax, and the compiler ignores it. Nevertheless, indentation is valuable because it makes code easier for people to read and understand. The fundamental idea is to reflect the code's logical structure with its physical layout.
for (int i = 0; i < 10; i++) cout << "Hello, World" << endl; cout << "Goodbye, World" << endl; |
for (int i = 0; i < 10; i++) cout << "Hello, World" << endl; cout << "Goodbye, World" << endl; |
for (int i = 0; i < 10; i++) cout << "Hello, World" << endl; cout << "Goodbye, World" << endl; |
(a) | (b) | (c) |
How and how much programmers indent is a matter of personal style, less important than indenting consistently. Always indent the same number of spaces, and don't intermix spaces and tab characters, even if the tab stops correspond to the number of spaces you use. You might wonder, "What difference does it make if I mix spaces and tabs?" It may not make any difference if every always views the code with the same editor and if the editor is always configured the same way. But if you mix spaces and tabs and view the code with a different editor or tab-stop settings, the indentation can become ragged and much harder to read. The code may not look perfect if readers switch editors or change tab stops, but it will look much better and be easier to read if you don't mix spaces and tabs.
Computer scientists often define a programming language's grammar like C++ with a metalanguage expressed in BNF. Although a detailed study of grammars is beyond the scope of an introductory programming course, we'll look at a small part of the C++ grammar to help us understand the central role of blocks and some unexpected language features.
(i) | statement: exression-statement compound-statement selection-statement iteration-statement |
|
(ii) | expression-statement:
expressionopt ; |
|
(iii) | compound-statement: { statement-seqopt } |
|
(iv) | statement-seq: statement statement-seq statement |
|
(v) | selection-statement: if ( condition ) statement if ( condition ) statement else statement switch ( condition ) statement |
|
(vi) | iteration-statement: while ( condition ) statement do statement while ( expression ) ; for ( ... ) statement |
|
(a) | (b) |
The replacement rules presented in Figure 1 suggest that a selection (an if or switch) or iteration (a loop) statement can replace a simple statement. The rules also suggest that a block or compound statement can replace a simple statement. The nesting and grouping the replacement rules allow (i.e., C++ allows) mean we can create powerful and complex statements in a program. Before exploring more complex control statements, we must understand a few more block-related concepts. Focus your attention on the braces and semicolons in the next figure.
"{" and "}" Optional | "{" and "}" Required |
---|---|
if (. . .)
cout << counter << endl;
Or
if (. . .) { cout << counter << endl; } |
if (. . .) { cout << "Hello, World!" << endl; cout << counter << endl; } |
Together, the second and third rules of Figure 1 suggest that blocks may be nested. Furthermore, each block represents a different scope. Scope can apply to many program parts, but it's easiest to understand when applied to variables.
A variable's scope is the part of a program where the variable is visible and usable. When a variable is defined inside of a block, its scope is limited to that block; that is, a variable defined inside a block is not visible or accessible outside of the defining block. A new block can be created anywhere in a program, even inside another block. If blocks can be nested, then scopes can also be nested. For the compiler to generate machine code that uses a variable, it must first locate where it is defined. When searching for a variable's definition, the compiler begins with the current scope and then searches outward in the surrounding scopes until it locates the variable.
In the case of automatic variables, the variable's scope affects when the program allocates and deallocates its memory, and when it initializes the variable. The program allocates the variable's memory, initializes it when it comes into scope, and deallocates it when it goes out of scope. Previously, I claimed that the program allocated memory when a variable was defined. That claim was an oversimplification: the definition arranges for the program to allocate memory when execution enters the variable's scope. That is the essence of an automatic variable: the program automatically allocates memory when the variable comes into scope and automatically deallocates memory when the variable goes out of scope. Furthermore, the program automatically initializes the variable whenever it comes into scope.
Local Scope | Global Scope | |
---|---|---|
{ int counter = 10; . . . } |
int counter = 10; int main() { . . . } |
|
Memory Allocation | Whenever the variable comes into scope | Once, when the program loads |
Memory Deallocation | Whenever the variable goes out of scope | When the program terminates |
Variable Initialization | Whenever the variable comes into scope | Once, when the program loads |
Uniqueness Rule
Variable names must be unique within each scope, meaning defining multiple variables with the same name in the same scope is a compile-time error. However, it is possible, but potentially quite confusing, to reuse a variable name in nested scopes:
if ( . . . )
{
int counter; // definition 1
if ( . . . )
{
int counter; // definition 2
. . . .
do something with counter // uses definition 2 counter
}
do something with counter // uses definition 1 counter
}
Ellis, M. A. & Stroustrup, B. (1990). The Annotated C++ Reference Manual. Reading, MA : Addision-Wesley Publishing Company.