Part B - Foundations
Basic Concepts
Identify some basic conceptual differences with respect to C rules
Set the stage for introducing the broader scope of the C++ language
"Correctness, simplicity, and clarity comes first"
(Sutter, Alexandrescu, 2005)
Keywords
| Types
| Declarations
| Scope
| Functions
| References
| Summary
| Exercises
The C++ language shares much of its foundational syntax with C.
The two languages have remained closely related to one another as they have evolved.
The first volume of this multi-volume set "Programming Computers Using C" describes
the more common syntax that the two languages share.
This chapter describes concepts of the C++ language that are basic to the
enhancements that it offers with respect to C. Topics that are identical
to those covered in the first volume are not reviewed here. The topics covered
here address the differences that C++ brings to the pair of languages and include
types, declarations, definitions, scope, function rules and pass by reference.
Keywords
Keywords are the reserved words that cannot be used as identifiers.
C++11 has 84 keywords as listed below. The keywords shared with
C appear
in grey. The italicized keywords are alternative tokens
for operators.
alignas alignof and and_eq asm auto bitand
bitor bool break case catch char char16_t
char32_t class compl const constexpr const_cast continue
decltype default delete do double dynamic_cast else
enum explicit export extern false float for
friend goto if inline int long mutable
namespace new not not_eq noexcept nullptr operator
or or_eq private protected public register reinterpret_cast
return short signed sizeof static static_assert static_cast
struct switch template this thread_local throw true
try typedef typeid typename union unsigned using
virtual void volatile wchar_t while xor xor_eq
A C++ compiler will successfully compile a C program that does not use
any of these keywords as identifiers if that program satisfies
C++'s type safety requirements. We call such a C program
a clean C program.
Types
C++ types include the fundamental types of the core language
and programmer-defined compound types.
Fundamental Types
The fundamental types include:
- Integral Types
- bool - not in C
- char
- int - short, long, long long
- Floating Point Types
- float
- double - long double
bool
bool stores a logical
value: either true and
false.
The ! operator reverses the value:
!true is false
and !false is true.
Although ! is self-inverting on
bool types, it is not self-inverting on
other types.
Be careful with ints.
true promotes to an int
of value 1, while false promotes to an int of value 0. Applying the
! operator to an
int value other than 0 produces
a value of 0, while applying the !
operator to an int value of 0
produces a value of 1. That is, the following
code snippet displays 1 (not 4)
int x = 4;
cout << !!x;
|
1
|
Compound Types
A compound type is a type that is composed of other types. (The C
language uses the term derived type.) The keywords struct
and class identify a compound type.
C++ requires the use of either keyword in the definition of the
compound type:
// Modular Example
// Transaction.h
struct Transaction {
int acct; // account number
char type; // credit 'c' debit 'd'
double amount; // transaction amount
};
|
and in a forward declaration:
struct Transaction; // forward declaration
|
A forward declaration informs the compiler that the type is a valid
compound type without defining the type.
C++ does not require use of the keyword in prototypes or
memory allocations: The C equivalent is shown on the right.
// Modular Example - C++
// Transaction.h
struct Transaction {
int acct;
char type;
double amount;
};
void enter(Transaction*);
void display(const Transaction*);
// ...
int main() {
Transaction tr;
// ...
}
|
// Modular Example - C
// Transaction.h
struct Transaction {
int acct;
char type;
double amount;
};
void enter(struct Transaction*);
void display(const struct Transaction*);
// ...
int main() {
struct Transaction tr;
// ...
}
|
auto
The auto keyword declares a variable or object
and deduces its type from the type of its initializer. The initializer is
a necessary part of any auto definition.
For example,
auto x = 4; // x is of type int and initialized to 4
auto y = 3.5; // y is of type double and initialized to 3.5
|
auto lets us simplify our coding when the
compiler already knows the type of the variable or object. This
will prove particularly useful later when working with standard libraries.
Declarations
A declaration associates an entity with a type.
The entity may be a variable, an object or a function.
That is, a declaration specifies how the compiler should interpret
the entity's identifier.
For example,
identifies x as an int,
stores its values in binary representation and allows x
in expressions that take an int as an operand.
Definitions
A definition is a declaration that attaches a meaning to an identifier.
A definition may only appear once within its code block or translation unit.
This is C++'s one-definition rule.
For example, the following two definitions attach meanings to Transaction
and to display():
struct Transaction {
int acct; // account number
char type; // credit 'c' debit 'd'
double amount; // transaction amount
};
|
void display(const Transaction* tr) { // definition of display
cout << "Account " << tr->acct << endl;
cout << (tr->type == 'd' ? " Debit $" : " Credit $") << endl;
cout << tr->amount << endl;
}
|
We cannot redefine Transaction or display()
within the same code block or translation unit.
Definitions are Executable Statements
Each definition is an executable statement.
We may embed any definition amongst other executable statements.
For example, we may place a definition within a condition or an initializer:
if ((int c = getchar()) != EOF)
// ...
for (int i = 0; i < n; i++)
//...
|
Declarations are not necessarily Definitions
Forward declarations and function prototypes are declarations
that are not definitions. They associate an identifier with a type,
but do not attach meaning to the identifier. We may repeat
such declarationsmany times within the same code block or translation
unit.
For example, we may include a header file that contains function prototypes
and forward declarations several times in a single implementation file.
However, if the header file contains a definition, we must ensure
that the translation unit that includes the header file does not break
the one-definition rule.
Avoiding Multiple Definitions
A definition that appears more than once within the same translation
unit violates the one-definition rule and generates a compiler
error. This can occur if the header file of one module
includes the header file of another module and the implementation
file includes both header files.
Consider the program shown in the figure below. It consists of
three modules: main, Transaction
and iostream.

The implementation file of the main module
calls a function (add()) that receives the address
of a Transaction type. The module's header
file prototypes this function:
The initial version of the header file
main.h contains:
// main.h
#define NO_TRANSACTIONS 3
void add(double*, const Transaction*);
|
The header file Transaction.h
contains the definition of the Transaction
type:
// Modular Example
// Transaction.h
struct Transaction {
int acct; // account number
char type; // credit 'c' debit 'd'
double amount; // transaction amount
};
void enter(Transaction* tr);
void display(const Transaction* tr);
|
The implementation file for the main module
includes both header files:
// Definition Conflict
// main.cpp
#include <iostream>
using namespace std;
#include "main.h" // prototype for add()
#include "Transaction.h" // prototypes for enter() and display()
int main() {
int i;
double balance = 0.0;
Transaction tr;
for (i = 0; i < NO_TRANSACTIONS; i++) {
enter(&tr);
display(&tr);
add(&balance, &tr);
}
cout << "Balance " << balance << endl;
}
void add(double* bal, const Transaction* tr) {
bal += (tr->type == 'd' ? -tr->amount : tr->amount);
}
|
Compiling this code generates an error to the effect that
Transaction* is undeclared. The
compiler analyzed the translation unit in a sequential fashion.
When it encounters the prototype for add(),
it does not know whether Transaction is valid
or not.
Including the Transaction.h in
main.h would resolve this error
but break the one-definition rule:
// main.h
#define NO_TRANSACTIONS 3
#include "Transaction.h" // BREAKS THE ONE-DEFINITION RULE!
void add(double*, const Transaction*);
|
The new error would state that
the Transaction type has been defined more than once:

Forward Declaration
One solution to this conflict is to inform the compiler that the
identifier Transaction is a valid
compound type, without defining the type.
A forward declaration can achieve this.
For example,
// main.h
#define NO_TRANSACTIONS 3
struct Transaction; // forward declaration
void add(double*, const Transaction*);
|
A forward declaration is like a function prototype. It provides
the compiler with just enough information to accept an identifer as a
valid compound type and subsequently perform type checking as the need
arises.
Scope
The scope of a declaration is the portion over which the
identifier is visible. Its potential scope is the largest
portion of a program over which the identifier may be valid.
The scope is the portion or portions of the potential scope over
which the declaration actually has effect.
For example, in the following program the second declaration shadows the first
so that the scope of the first declaration is discontinuous:
// scope.cpp
#include <iostream>
using namespace std;
int main() {
int i = 6;
cout << i << endl;
for (int j = 0; j < 3; j++) {
int i = j * j;
cout << i << endl;
}
cout << i << endl;
}
|
6
0
1
4
6
|
A declaration in a block is local to its block.
We say that it has block scope. Its potential scope begins
at its declaration and ends at the end of its block. We say
that the identifier is a local variable or object.
It is local to the block.
For example, in the following code snippet, the
counter i, declared within the for
statement, goes out of scope immediately after the closing brace:
for (int i = 0; i < 4; i++) {
cout << "The value of i is " << i << endl;
} // i goes out of scope here
|
We cannot refer to i after the closing brace.
A variable or object declared within a block goes out of scope
immediately before the block's closing brace.
for (int i = 0; i < 3; i++) {
int j = 2 * i;
cout << "The value of j is " << j << endl;
} // j goes out of scope here
|
The scope of j extends from its definition just after
the start of each iteration and ends just before the end of each iteration.
On the other hand, i remains in scope throughout the
iteration.
Function Rules
C++ functions follow slightly stricter rules than do C functions. These
tighter rules enable language features such as overloading and references,
which are unavailable in the C language.
Prototypes
A prototype declaration consists of the function's return type, its identifier
and all of its parameter types. The order of its parameter types matters.
The parameter identifiers are optional.
The prototype for a function with no parameters has an empty parameter list.
The keyword void, which the C language uses with prototypes
that have no parameters is redundant in C++. We omit the keyword in such cases.
Prototypes Required
C++ promotes the language's type safety by requiring the prototype for a
fucntion wherever a call to the function appears before the function definition.
The compiler uses the prototype to check that the argument types in the
function call match the corresponding parameter types in the prototype.
For example, the compiler will generate an error on the following program
to the effect that printf is undeclared:
int main() {
printf("Hello C++\n");
}
|
To correct this error, we include its prototype:
#include <cstdio>
using namespace std;
int main() {
printf("Hello C++\n");
}
|
Overloading
C++ distinguishes function definitions by their name, their parameter types
and the order of their parameter types. Two functions with the same
identifier but different parameter types or differently ordered parameter
types are distinct functions. that is, several identically named
functions can have distinct meanings. If a function identifier has
more than one definition, we say that that function has been overloaded.
For example, to display data on the standard output device,
let us define two functions with the same name display(),
but with different parameter types:
void display(int x) {
cout << x << endl;
}
void display(int* x, int n) {
for (int i = 0; i < n; i++)
cout << x[i] << ' ';
cout << endl;
}
|
The compilation stage generates two separate definitions of display():
one for each set of parameters. The linker stage binds each function call to
the appropriate definition based on the argument types in the function call.
Signature
A function's signature identifies the function uniquely.
The signature consists of
- the function identifier
- the parameter types (without any const modifiers or address of operators)
- the order of the parameter types
type identifier ( type identifier, ... , type identifier )
Note that the return type and the parameter identifiers are not
part of a function's signature.
The compiler maintains uniqueness by renaming each function using its
identifier, its parameter types and the order of its parameter types.
We refer to this renaming process as identifier mangling.
Default Parameter Values
The rightmost parameters of a function may have default values.
The first function declaration in a translation unit specifies the
default values.
A declaration with default values takes either of the two following forms:
type identifier(type, ..., type = value);
type identifier(type, ..., type identifier = value);
The assignment operator followed by a value identify the default value
for each paramter.
Specifying default values for function parameters minimizes the need
for multiple function definitions where the bodies of the functions
are identical in every respect except for the values of the parameters.
For example,
// Default Parameter Values
// default.cpp
#include <iostream>
using namespace std;
void display(int, int = 5, int = 0);
int main() {
display(6, 7, 8);
display(6);
display(3, 4);
}
void display(int a, int b, int c) {
cout << a << ", " << b << ", " << c << '\n';
} |
6, 7, 8
6, 5, 0
3, 4, 0
|
Each call to display() must include enough arguments
to initialize the parameters that don't have default values. In this
example, each call must include at least one argument.
An argument corresponding to a parameter that has a default value
override the default value.
Pass By Reference
C++ offers an alternative mechanism to passing by address called pass by
reference. Pass-by-reference code is more readable than pass-by-address
code.
The decalaration of a parameter in a function header that is passed by reference
takes the form
type identifier(type& identifier, ... )
The & identifies the parameter
as an alias for, rather than a copy of, the corresponding argument in the function
call. The identifier is the name of the alias within the function itself.
Any change to the value of a parameter received by reference represents a change to
the value of the corresponding argument in the function call.
Comparison Example
Consider the code for a function that swaps the values stored
in two different memory addresses. The following two programs
represent pass-by-address and pass-by-reference solutions.
The program on the left uses pointers. The program on the
right uses references:
// Swapping values by address
// swap1.cpp
#include <iostream>
using namespace std;
void swap ( char *a, char *b );
int main ( ) {
char left;
char right;
cout << "left is ";
cin >> left;
cout << "right is ";
cin >> right;
swap(&left, &right);
cout << "After swap:"
"\nleft is " <<
left <<
"\nright is " <<
right <<
endl;
}
void swap ( char *a, char *b ) {
char c;
c = *a;
*a = *b;
*b = c;
}
|
// Swapping values by reference
// swap2.cpp
#include <iostream>
using namespace std;
void swap ( char &a, char &b );
int main ( ) {
char left;
char right;
cout << "left is ";
cin >> left;
cout << "right is ";
cin >> right;
swap(left, right);
cout << "After swap:"
"\nleft is " <<
left <<
"\nright is " <<
right <<
endl;
}
void swap ( char &a, char &b ) {
char c;
c = a;
a = b;
b = c;
}
|
Pass by reference syntax is slightly simpler.
To pass a variable or an object by reference, we
add the address of operator in the prototype and the
function definition. This operator instructs the
compiler to implement pass by reference.
We omit the address of operator on the corresponding arguments
in the function call and dereferencing operators in the prototype
or within the function definition.
Technically, a reference is an address that cannot be changed.
The compiler converts references to pointers with unmodifiable addresses,
relieving us from implementing pointer syntax explicitly within our coding.
Summary
- a bool type holds either
a true value or a false value
- C++ only requires the struct keyword in the definition of the compound type itself
- a declaration associates an identifier with a type
- a definition attaches meaning to an identifier and is an executable statement
- a definition is a declaration, but a declaration is not necessarily a definition
- the scope of a declaration is the part or parts of the program throughout which the declaration is valid
- a C++ prototype must list all of the function's parameter types
- a function's signature consists of its identifier, its parameter types, and the order of its parameter types
- a function is overloaded by changing its signature but retaining its identifier
- the & operator in the prototype and function header instructs the compiler
to pass by reference
- pass by reference syntax replaces and simplifies pass by address syntax in most cases
Exercises
|