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;

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,

 int x;

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

one-definition rule

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: 

one-definition rule broken

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

Previous Reading  Previous: Modular Programming Next: Member Functions and Privacy   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo
Creative Commons License