Home‎ > ‎CS 11M‎ > ‎

Classes and File Structure

Introduction 

So far all of our programs have been contained in a single source file. This is good for beginners but not sustainable in big software projects. Here are some examples of C/C++ projects:
  • The Linux operating system
    • Language: C and Assembly 
    • Lines of code: > 10 Million
    • Source files: 41,000
  • The Arduino 
    • Language: C, C++, Assembly and Java
    • Lines of code: 250,000
    • Source Files: 1,800
Today you'll learn how to separate your code into source files (*.c and *.cpp files) and header files (*.h files) and why it's necessary. We'll also discuss data hiding and encapsulation which is a technique for helping programmers work together effectively. 

Function Prototypes 

Arduino helps beginners by hiding one of the necessities of the C and C++ languages: The need for function prototypes. A function prototype is a promise that a function will exist:

int findMin(vector<int> &vect);   // Semicolon at the end: This is a prototype (or function declaration)

int findMin(vector<int> &vect) {  // Left brace: This is a function definition. 
  ...
}

Notice:
  • A function prototype (also called a function declaration) is the first line of the function
  • A prototype shows the compiler what arguments the function takes and what it returns
  • The compiler knows the signature of the function by looking at the prototype 
In C and C++ a function's signature must be available to the compiler before it can be used. Examine these two block of code. This one is legal:

int myFunc(int num) {
  return num + 1; 
}

void loop() {
  int n = myFunc(10);
}

This one is not legal because the compiler hasn't seen myFunc() yet:

void loop() {
  int n = myFunc(10); // Compiler doesn't know what myFunc() is yet.
}

int myFunc(int num) {
  return num + 1; 
}

The reason you've never noticed this problem in your Arduino projects is because the Arduino IDE automatically adds function prototypes to the top of your source. So the program above becomes this:

int myFunc(int num);

void loop() {
  int n = myFunc(10); // Compiler sees the prototype.
}

int myFunc(int num) {
  return num + 1; 
}

Starting today you will write code outside of *.ino files and you will have to understand this function of C/C++. 

Summary

  • Function prototypes are the first line of the function punctuated with a semicolon. 
  • The compiler must see a definition or a declaration (prototype) of a function before it can be used in code. 
  • The Arduino IDE automatically adds prototypes for you. 

Header Files

A header file is a file that contains only prototypes. It's purpose is to allow you to use the functions and classes defined in a library. Header files are necessary because every time the C/C++ compiler encounters a function definition (a function with code) it compiles it. When the program is linked if there are multiple compiled copies of a function it's an error. 

Exercise 1: Create a Multifile Project

Start by creating a sketch named multifile_project.ino. Next follow this process to add source files to an Arduino project: 
  1. Pull Down the arrows as shown
  2. Name your new file MyLibrary.h
  3. Repeat steps 1 and 2 to add a new file MyLibrary.cpp
If you've done the above steps your Arduino IDE should have tabs with your new source files:

Add my function that allows you to use a vector directly with cout into MyLibrary.cpp:

MyLibrary.cpp 
#include "MyLibrary.h"

// Insertion operator for vectors operator<<()
ostream &operator<<(ostream &os, const vector<string> &v) {
  os << "vector: { "; 
  for (auto element : v) {
    os << element << " ";
  }
  os << "}";
  return os;
}

Now add the prototype of the function to MyLibrary.h

MyLibrary.cpp 
#include <ArduinoSTL.h>

using namespace std; 

ostream &operator<<(ostream &os, const vector<string> &v);

As you can see the prototype is identical to the function definition but with a semicolon rather than braces and code. Now you can use the library by adding an #include statement into your sketch. Copy the following code to your multifile_project.ino file:

multifile_project.ino
#include <ArduinoSTL.h>
#include "MyLibrary.h"

using namespace std; 

void setup() {
  Serial.begin(9600);
  vector<string> v {"Hello", "Library", "World"};
  cout << v << endl;
}

void loop() {
}

Notice
  • Using #include effectively does a copy-and-paste of the file in the place of the #include 
  • Both the ino file and the cpp file include the headers
  • System headers are included with #include <...> and local headers are included with #include "..."

Why Prototypes? 

Notice what happens when you add a function definition to your code. Add the following function definition to your MyLibrary.h file: 

MyLibrary.h
int addTwo(int a, int b) {
  return a + b; 
}

When you compile the project you get this error: 

sketch/multifile_project.ino.cpp.o (symbol from plugin): In function `addTwo(int, int)':
(.text+0x0): multiple definition of `addTwo(int, int)'
sketch/MyLibrary.cpp.o (symbol from plugin):(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
exit status 1
Error compiling for board Arduino/Genuino Uno.

The error happens because each time you included the header into a C++ file a copy of the function was compiled. During the final stages of building the project (called linking) the compiler failed because it found multiple copies of a function with the same name. 

Turn In

When you're done with the class exercise you'll notice that your Arduino sketch folder has multiple files. Zip the whole directory together into a single file called multifile_project.zip and submit your zip file with the next assignment on Canvas. 

Class Prototypes 

So far you've created classes in one big declaration. This is convenient for small classes but not the best way to create a class in C++. The proper way to create a class is to split it into *.cpp  and *.h files. For example, the Color class you created should look a bit like this: 

class Color {
  public:
    Color(int r, int g, int b) {
      red = r;
      green = g;
      blue = b;
    }

    void display() {
      analogWrite(redLED, 255 - red);
      analogWrite(greenLED, 255 - green);
      analogWrite(blueLED, 255 - blue);
    }

    int red;
    int green;
    int blue;
};

The above mixes definition and declaration. To split them the class should be rewritten like this: 

class Color {
  public:
    Color(int r, int g, int b);

    void display();

    int red;
    int green;
    int blue;
};

Color::Color(int r, int g, int b)  {
  red = r;
  green = g;
  blue = b;
}

void Color::display()  {
  analogWrite(redLED, 255 - red);
  analogWrite(greenLED, 255 - green);
  analogWrite(blueLED, 255 - blue);
}

Notice:
  • The function definitions have been moved out of the class declaration.
  • The function names have the class name prepended with them:
    • The constructor (Color) becomes Color::Color 
    • The display function becomes Color::display 
  • The double colon (::) is the scope operator. 
Class prototypes make it possible to split classes between header and source files and are necessary for anything but the smallest classes. 

Class Privacy and Accessors 

So far we've only used public class members. Public members are accessible everywhere. Classes have the ability to "hide" (or encapsulate) members from the general public. That way the author of the class can reserve certain variables and functions for their own use. This promotes teamwork by giving programmers a space that's private. The public portion of a class is called it's class interface and is the agreed upon way to access the class. This encapsulation comes at a slight cost:

#include <ArduinoSTL.h>

using namespace std;

class Color {
  public:
    Color(int r, int g, int b) {
      red = r;
      green = g;
      blue = b;
    }

  private:
    int red;
    int green;
    int blue;

};

void setup() {
  Color c(1, 2, 3);
  c.red = c.green * 2;    // Error: Reading and assigning private member variables.
  cout << c.red << endl;  // Error: Reading a private member vairable.
}

The above code doesn't compile because the member variables red, green and blue are private. Private member cannot be used outside of the class. They can be used by any member function of the class, public or private. Given the example above how would the code in setup() regain the access that it had before the red, green and blue members were private? The class must add accessor functions.

#include <ArduinoSTL.h>

using namespace std;

class Color {
  public:
    Color(int r, int g, int b) {
      red = r;
      green = g;
      blue = b;
    }

  int getRed() {
    return red;
  }

  int getGreen() {
    return green;
  }

  int getBlue() {
    return blue;
  }

  void setRed(int r) {
    red = r;
  }

  void setGreen(int g) {
    green = g; 
  }

  void setBlue(int b) {
    blue = b;
  }
  
  private:
    int red;
    int green;
    int blue;

};

void setup() {
  Color c(1, 2, 3);
  c.setRed(c.getGreen() * 2);
  cout << c.getRed() << endl;
}

As you can see this makes the class much bigger. However, the added complexity is an important way to future-proof your class. If, sometime down the road, you wished to change the type or representation of the color variables you can easily do it with accessor version of the class by adding code to the accessors. If you allow foreign code to access your variable directly then you can't change your class. Ever.  

Summary 

In this lesson you learned:
  1. The importance of function prototypes 
  2. How to use a class prototype
  3. How to use header files to make your prototypes accessible 
  4. How public and private members operate 
  5. How to write accessors
Comments