The Structure of a C++ Program

Steven Zeil

Old Dominion University, Dept. of Computer Science

Table of Contents

1. Separate Compilation
1.1. Single File Programs
1.2. Multiple File C++ Programs
1.3. Separate Compilation
2. The # Preprocessor
2.1. #include
2.2. #define
2.3. #ifdef, #ifndef, #endif
3. Declarations and Definitions
3.1. General Rules for Decls & Defs
3.2. Decls&Defs: Variables
3.3. Decls&Defs: Functions
3.4. Decls&Defs: Data Types
4. Organizing Decls & Defs into Files
4.1. Header and Non-Header Source Files
4.2. Coupling and Cohesion
5. Example: the auction program
5.1. Dividing Things Up

1. Separate Compilation

  • C++ programs can range from a handful of statements to hundreds of thousands

  • May be written by one person or by a team

1.1. Single File Programs

  • OK for small programs (CS150)

  • But with large programs

    • compilation would take minutes, hours, maybe days

      • might break compiler

    • Team members would interfere with one another’s work.

      • "Are you still editing that file? You've had it all afternoon."

      • "What do you mean you're saving changes to the file? I've been editing it for the last 45 minutes!"

1.2. Multiple File C++ Programs

By splitting a program up into multiple files that can be compiled separately,

  • Team members can work in parallel on separate files

  • Files are compiled separately

    • each individual compilation is fast

  • Separately compiled code is linked to produce the executable

    • linking is much faster than compilation

1.3. Separate Compilation

  • Each file of source code

    • programming language text

  • is compiled to produce a file of object code

    • binary code, almost executable

    • exact addresses of variables and functions not known, represented by symbols

  • All object code files are linked to produce the executable

    • Linking mainly consists of replacing symbols by real addresses.

On large projects with hundreds to thousands of files,

  • Typically only a few files are changed on any one day

  • Often only the changed files need to be recompiled

  • Then link the changed and unchanged object code

2. The # Preprocessor

The preprocessor runs before the compiler proper. The preprocessor:

  • modifies the source code

  • processes preprocessor instructions

    • lines beginning with #

  • strips out comments

The common pre-processor instructions are

  • #include

    • insert a file

  • #define

    • define a macro

  • #ifdef, #ifndef, #endif

    • check to see if a macro has been defined

2.1. #include

  • Inserts a file or header into the current source code

  • Two versions

    • #include <headerName>

      • inserts a system header file from a location defined when the compiler was installed

    • #include "fileName"

      • inserts a file from the current directory

Example 1. #include (simple case)

  • Suppose we have three files: A.h, B.h, C.cpp

  • We ask the compiler to only run the preprocessor and save the result:

    g++ -E C.cpp > C.i
  • The result is file C.i

    • Note the presence of content from all three files

      • includes markers telling where the content came from


Example 2. #include (realistic case)

In real programs, most of the code actually seen by the compiler may come from #includes


Deja-Vu

  • Code that is in headers (.h files) may actually be compiled many times

  • Code that is in non-header (.cpp) files will be compiled only once

This distinction will be important later.

2.2. #define

  • Used to define macros (symbols that the preprocessor will later substitute for)

    • Sometimes used to supply constants

      #define VersionNumber "1.0Beta1"
      
      int main() {
         cout << "Running version "
              << VersionNumber
              << endl;
      
    • Much more elaborate macros are possible, including ones with parameters

2.3. #ifdef, #ifndef, #endif

Used to select code based upon whether a macro has been defined:

#ifdef __GNUG__
  /* Compiler is gcc/g++ */
#endif
#ifdef _MSC_VER
  /* Compiler is Microsoft Visual C++ */
#endif

#if, #define, and #include

  • All of these macros are used to reduce the amount of code seen by the actual compiler

  • Suppose we have three files: A2.h, B2.h, C2.cpp

  • We ask the compiler to only run the preprocessor and save the result:

    g++ -E C2.cpp > C2.i
  • The result is file C2.i

    • Note that the code from A2.h is included only once

    • Imagine now, if that were iostream instead of A2.h

3. Declarations and Definitions

  • Some of the most common error messages are

    • … is undeclared

    • … is undefined

    • … is defined multiple times

  • Fixing these requires that you understand the difference between declarations and definitions

    • and how they relate to the program structure

A declaration in C++

  • introduces (or repeats) a name for something

  • tells what kind of thing it is

  • gives programmers enough information to use it

A definition in C++

  • introduces (or repeats) a name for something

  • tells what kind of thing it is

  • tells what value it has and/or how it works

  • gives the compiler enough information to generate this and assign it an address

3.1. General Rules for Decls & Defs

  • All definitions are also declarations.

    • But not vice versa

  • A name must be declared before you can write any code that uses it.

  • A name can be declared any number of times, as long as the declarations are identical.

  • A name must be defined exactly once, somewhere within all the separately compiled files making up a program.

3.2. Decls&Defs: Variables

  • These are definitions of variables:

    int x;
    string s = "abc";
    MyFavoriteDataType mfdt (0);
  • These are declarations:

    extern int x;
    extern string s;
    extern MyFavoriteDataType mfdt;

3.3. Decls&Defs: Functions

  • Declaration:

    int myFunction (int x, int y);
  • Definition

    int myFunction (int x, int y)
    {
       return x + y;
    }
    
  • The declaration provides only the header. The definition adds the body.

3.4. Decls&Defs: Data Types

  • Data types in C++ are declared, but never defined.

  • These are declarations:

    typedef float Weight;
    typedef string* StringPointer;
    enum Colors {red, blue, green};
  • Later we will look at these type declarations

    struct S { ... };
    class C { ... };
    

4. Organizing Decls & Defs into Files

  • A C++ program consists of declarations and definitions.

  • These are arranged into files that are combined by

    • linking after separate compilation

    • #include'ing one file into another

  • These arrangements must satisfy the general rules:

    • A name must be declared before you can write any code that uses it.

    • A name can be declared any number of times, as long as the declarations are identical.

    • A name must be defined exactly once, somewhere within all the separately compiled files making up a program.

4.1. Header and Non-Header Source Files

A typical C++ program is divided into many source code files

  • Some are headers

    • Typically end in ".h"

    • May be #included from many different places

    • May #include other headers

    • Not directly compiled

  • The non-header files

    • Typically end in ".cpp", ".cc", or ".C"

    • Should never be #included from elsewhere

    • May #include headers

    • Are directly compiled

Can You See Me Now?...Now?...Now?

How often does the compiler process each line of code from a file?

  • For headers, any number of times

  • For non-headers, exactly once

Therefore a header file can only contain things that can legally appear multiple times in a C++ program - declarations

Division: headers and non-headers

  • Header files may contain only declarations

    • specifically, declarations that need to be shared with different parts of the code

  • Non-headers may contain declarations and definitions

    • Definitions can only appear in a non-header.

  • Never, ever, ever #include a non-header (.cpp) file

4.2. Coupling and Cohesion

  • How do we divide things into different headers?

  • Identify groups of declarations with

    • Low coupling - dependencies on other groups

    • High cohesion - dependencies within the group

Coupling

  • Something with high coupling has many dependencies on external entities

  • Something with low coupling has few dependencies on external entities

Cohesion

  • In something with high cohesion, all the pieces contribute to a well-defined, common goal.

  • In something with low cohesion, the pieces have little relation to one another

Extremely Low Cohesion

Dividing into modules

  • How do we divide up the non-header source files?

  • Usually pair them up with a header

    • one non-header file for each header file

    • The non-header file provide definitions for each declaration in the header that is it paired with.

  • Such pairs are a one of the most common forms of module

    • a group of related source code files

5. Example: the auction program

Read this description of the auction program. It's an example that we will use over and over throughout the semester.

The overall algorithm is pretty simple

main (fileNames[]){
  readItems;
  readBidders;
  readBids;
  for each item {
    resolveAuction (item, bidders, bids)
  }
}

      

We read in all the information about the auction, then resolve the bidding on each item, one at a time (in order by closing time of the bidding for the items).

The full implementation is shown here, but to the right I show a simplified version. The details of the code for each function body really aren't important right now (with a slight exception that I'll get into later). The most important thing during modularization is to know what the functions and data are and what role they play in the program.

From the description of the program and from a glance through the code, we might guess that the key modules that we can come up with would be:[1]

Items

Data and functions related to the items up for auction

Bidders

Data and functions related to the people bidding in the auction

Bids

Data and functions related to the bids placed by those people

If we then make a list of the functions and data in this program:

Functions Data
printTime nItems
noLaterThan MaxItems
readBidders itemNames
readItem itemReservedPrice
readItems itemEndHours
findBidder itemEndMinutes
readBids itemEndSeconds
resolveAuction nBidders
main MaxBidders
bidderNames
bidderBalances
nBids
MaxBids
bidBidder
bidAmounts
bidItems
bidHours
bidMinutes
bidSeconds

then we can pretty much assign these to our modules just be reading their names and, in a few cases, looking at the comments in the code that describe them:

Module Functions Data
Items readItem nItems
readItems MaxItems
itemNames
itemReservedPrice
itemEndHours
itemEndMinutes
itemEndSeconds
Bidders readBidders nBidders
findBidder MaxBidders
bidderNames
bidderBalances
Bids readBids nBids
MaxBids
bidBidder
bidAmounts
bidItems
bidHours
bidMinutes
bidSeconds
??? resolveAuction
noLaterThan
printTime
main

We have a few functions still unassigned. Looking at them, the functions printTime and noLaterThan are both concerned with handling "time", which suggests that a new "Time" module might be in order. The final two functions, resolveAuction and main, constitute the core algorithm, the "main controller" for this particular program, and so can be kept in the main cpp file. So our final division looks like:

Module Functions Data
Items readItem nItems
readItems MaxItems
itemNames
itemReservedPrice
itemEndHours
itemEndMinutes
itemEndSeconds
Bidders readBidders nBidders
findBidder MaxBidders
bidderNames
bidderBalances
Bids readBids nBids
MaxBids
bidBidder
bidAmounts
bidItems
bidHours
bidMinutes
bidSeconds
Time noLaterThan
printTime
(main) resolveAuction
main

5.1. Dividing Things Up

We can now prepare the modular version of this program. By default, each function or variable that we have identified gets declared in its module's header file and defined in its module's implementation file.

We can reduce possible coupling, though, by looking at the actual function bodies and checking to see if any of these declarations would only be used internally within a single module. A declarion only needs to appear in the header file if some code outside the module is using it. If it is only used from within its own module. it can be kept "hidden" inside that module's .cpp file.

For example, in the Items module, the function readItem, which reads a single item, is only called from inside the function readItems, which reads an entire array of items. Because readItem is only called by a function within its own module, it does no need to be listed in items.h.



[1] In later lessons we'll talk about better ways to identify good modules.


Discuss This Page:

(no threads at this time)