CS105: Introduction to Computer Programming: C++

User-defined Types

There are several ways to define new types in C++.

Enumerated Types

Enumerated types look like this:

enum MonthT {
  JANUARY = 100,
  FEBRUARY,
  MARCH,
  APRIL,
  MAY,
  JUNE,
  JULY,
  AUGUST,
  SEPTEMBER,
  OCTOBER,
  NOVEMBER,
  DECEMBER,
};

int main() {

  MonthT month = APRIL;
  cout << "month is: " << month << endl;

  return 0;
}

If you don't define values for the enumerated constants, the first one will default to 0 and the rest will be incremented by 1. In the above code, since the first enumerated constant (JANUARY) is set to 100, APRIL will have a value of 103.

Typedefs

The typedef keyword allows you to create a new type from some old type. For example:

  typedef int FakeInt;
  typedef unsigned int uint;

The above code defines two new types: FakeInt, which is just another way of saying int, and uint, which is a shortcut for an unsigned map.

Structs

C++ has inherited the old-skool C way of defining data types, known as the struct:

struct Date {
  int day;
  int month;
  int year;
};

int main() {
  Date d;
  d.day = 12;
  d.month = 9;
  d.year = 2007;
}

Unions

Unions are similar to structs, but all elements of a union share the same memory. For example:

union mix_t {
  int val;
  char bytes[4];
};

int main() {
  mix_t myMix;
  myMix.val = 258;

  cout << "myMix.val is " << myMix.val << endl;
  cout << "myMix.bytes[0] is " << static_cast<int>(myMix.bytes[0]) << endl;
  cout << "myMix.bytes[1] is " << static_cast<int>(myMix.bytes[1]) << endl;
  cout << "myMix.bytes[2] is " << static_cast<int>(myMix.bytes[2]) << endl;
  cout << "myMix.bytes[3] is " << static_cast<int>(myMix.bytes[3]) << endl;
}

In the above code, the val and bytes fields of the union share the same space in memory. Setting val will affect what is in the bytes array. Because they often rely on specific byte counts, unions are not very portable.

Classes

C++ also has the idea of a class. Classes are very similar to structs:

class Date {
public:
  int day;
  int month;
  int year;
};

int main() {
  Date d;
  d.day = 12;
  d.month = 9;
  d.year = 2007;
}

Note the access specifier public. That tells the compiler that the day, month, and year variables should be accessible to anyone outside of the class. Other access specifiers are private and protected.

The default access specifier for classes is private. Only functions within a class (or friends of the class) can access private data. Structs, on the other hand, have public access by default, which means anyone can access their internals.

One of the ideas behind classes was that they can contain both data and functions:

#include <iostream>
using std::cout;

/**
 * Our brilliant Date class, which holds information about a date.
 */
class Date {
public:

  /**
   * Constructor for the Date class - used to initialize Date data
   */
  Date( int m, int d, int y ) {
    month = m;
    day = d;
    year = y;
  }

  /**
   * Getter/setter functions for month, day, and year
   */
  int getMonth() { return month; }
  int getDay() { return day; }
  int getYear() { return year; }
  void setMonth( int m ) { month = m; }
  void setDay( int d ) { day = d; }
  void setYear( int d ) { year = d; }

  /**
   * Display this date to standard out
   */
  void displayDate() {
    cout << getMonth() << "/" << getDay() << "/" << getYear();
  }

private:

  int month;
  int day;
  int year;
};


int main() {
  // create an instance of the Date class and display it
  Date d( 9, 12, 2007 );
  d.displayDate();
}

Separating the definition from the implmentation

When creating large projects, it can be useful to split your class definitions into header (.h) and implementation (.cc) files. For example, the code above might be split into three files:

Here's what those files might look like:

Date.h

#ifndef DATE_H
#define DATE_H

/**
 * Our brilliant Date class, which holds information about a date.
 */
class Date {
public:

  /**
   * Constructor for the Date class - used to initialize Date data
   */
  Date( int m, int d, int y );

  /**
   * Getter/setter functions for month, day, and year
   */
  int getMonth();
  int getDay();
  int getYear();
  void setMonth( int m );
  void setDay( int d );
  void setYear( int d );

  /**
   * Display the Date to standard output
   */ 
  void displayDate();

private:

  int month;
  int day;
  int year;
};
#endif

Date.cc

#include <iostream>
using std::cout;
#include "Date.h"


Date::Date( int m, int d, int y ) {
  month = m;
  day = d;
  year = y;
}

int Date::getMonth() { return month; }
int Date::getDay() { return day; }
int Date::getYear() { return year; }
void Date::setMonth( int m ) { month = m; }
void Date::setDay( int d ) { day = d; }
void Date::setYear( int d ) { year = d; }


void Date::displayDate() {
  cout << getMonth() << "/" << getDay() << "/" << getYear();
}

main.cc

#include "Date.h"

int main() {
  // create an instance of the Date class and display it
  Date d( 9, 12, 2007 );
  d.displayDate();
}
Now that we've got three files instead of one, our compile command is gonna be a bit more complicated. We could use g++ -Wall -Werror Date.cc main.cc -o main, but as we add more files, that approach won't scale. A common solution to this problem is to define how the code should be compiled using a Makefile, and then using the make command to compile the code.