allenfrostline

C++ Key Points


2018-09-03

This is the first post of my ambitious plan trying to enumerate as many key points about the C++ language as I can. These notes are only for personal reviewing purposes and shall definitely be used commercially by anyone interested. Comment below for any missing C++ syntax or features. 👍🏻

Header

Basically there’re only one thing that needs attention. For standard libraries we use < and > and for local libraries we use quotes.

#include <iostream>
#include "helloworld.h"

These are files we declare functions and classes we want to use or implement in main files.

Standard Input & Output

In C++, by loading the iostream library we can read and write by

#include <iostream>

int main() {
    int a;
    std::cin >> a;
    std::cout << a << std::endl;
}

Namespaces

Normally libraries come with classes, e.g. for iostream we need to use std everytime we need to print something. With namespaces we can reduce redundance.

#include <iostream>
using std::cout;
using std::cin;
using std::endl;

int main() {
    int a;
    cin >> a;
    cout << a << endl;
}

We may also use using namespace std; which sometimes can cause problems.

Data Types

There are a variety of data types in C++. For real numbers we have

Type Bytes Range
float $4$ $\pm 3.4E\pm38$
double $8$ $\pm 1.7E\pm308$

where we should pay enough attention to the $\pm$. For general integers we have

Type Bytes Range
short $2$ $-2E15$ to $2E15-1$
int $4$ $-2E31$ to $2E31-1$
long $8$ $-2E63$ to $2E63-1$

and for each type we also have an unsigned version that starts from 0 and covers the same length of range.

We may notice that long has a smaller range than float despite the fact that the first data type actually costs more bytes than the latter. This is because the 4 bytes (or 32 bits) of a float $V$ is not stored equally in RAM, but rather

$$ V = (-1)^S \cdot M \cdot 2^E $$

where $S$ is the first bit, and $E$ the second through the ninth bits, and $M$ for the tenth and so forth. So in a sense, because float is more “sparse”, the long type has a smaller range.

Apart from other fundamental types like char and bool, we can also define our own data types or use types defined in libraries, e.g. std::string. We may also use type aliases like

typedef double OptionPrice;

Operators

We have operators for fundamental types:

Function Operator
assignment =
arithmetic + - * /
comparison > < <= >=
equality/nonequality == !=
logical && ||
modulo %

In C++ there’re a set of short-cuts as follows:

Full Operator Short-cut
i = i + 1; i++; i += 1;
i = i - 1; i--; i -= 1;
i = i * 1; i *= 1;
i = i / 1; i /= 1;

We may also use prefix and postfix in assignment, which are totally different. After

int x = 3;
int y = x++;

we have $x = 4$ and $y = 3$. After

int x = 3;
int y = ++x;

we have $x = y = 4$.

Functions

A general template for a C++ function:

resType f(argType1 arg1, argType2 arg2, ...) {
    Do something;
    return res;
}

Notice we may write multiple functions with the same resType but with different arguments, which we call “parameter overloading”. Meanwhile, even withou parameter overloading we can still use a function of double on int, because int takes up less bytes and the implicit conversion is safe. We call it widening or promotion. In contrast, narrowing can be dangerous and cause a build warning.

Build Process

Comments

In C++ we have two kinds of comments.

// This is inline comment

/*
These are
block comments
*/

References

In C++, people usually pass variables into functions by two methods: either by value or by reference. The first way creates a copy of the variable and nothing will happen to the original one. For the second, anything we do in the function will take effect on the original variable itself. The original variable must be declared once we create a reference, so

int x = 1;
int& refx = x;

will compile, while below will not:

int x = 1;
int& refx;
refx = x;

References can be extremely useful, especially when the original variable is a large object and making a copy costs considerable time and memory. However, this is potentially risky when we don’t want to mess up with the original object when calling a function. So we need const references.

Const References

There’re two situations we should take care when using the const keyword with references. First, we can make a reference of a const variable, and we cannot change the value of it:

const int x = 1;
const int& refx = x;
x = 2;    // error
refx = 2; // error

We may also bind a const reference to a variable when the original itself is not const:

int y = 1;
const int& refy = y;
y = 2;    // success
refy = 2; // error

In this case we avoid making a copy while also keep the original variable safe from unexpected editing.

Pointers

There is a third way of passing a variable, that is pointers. Pointers are variables the points to their addresses in memory. We declare a pointer by

int* pi;            // legal but bad without initialization
int* pi = nullptr;  // good when there're no proper variables

which comes with two unique operators: & for the address of a variable, and $*$ for the dereference of a pointer.

int i = 123;
int* pi = &i;
cout << *pi;
123

New and Delete

You can create a pointer pointing to a piece of dynamic memory for later deletion, in case memory is being an issue in your program.

int *p = new int;
// or
int *p = NULL;
p = new int;
*p = 1;
delete p

Const Pointers

You can have pointers to a const variable, i.e. you cannot change its value through pointers.

const int x = 1;
const int* px = &x;
x = 2;   // error
*px = 2; // error
px += 1; // success

You can also have const pointers to variables, then you can change the value of the variable but never again the pointer (address) itself.

int x = 1;
int* const px = &x;
x = 2;   // success
*px = 2; // success
px += 1; // error

You can also have const pointers to const variables.

const int x = 1;
const int* const px = &x;
x = 2;   // error
*px = 2; // error
px += 1; // error

If/Else

Below is a general template for if/else structures in C++.

if (condition1) {
    statement1;
} else if (condition2) {
    statement2;
} else {
    statement3;
}

Switch

When there’re multiple conditions, we can also use the switch keyword.

switch (expression) {
    case value1:
        statement1;
        break;
    case value2:
        statement2;
        break;
    default:
        statement3;
}

While

One of the most popular loops is while loop.

while (condition) {
    statement;
}

It also has a variant called the do/while loop.

do {
    statement;
} while (condition);

which is slightly different from the while loop in sequence.

For

Another form of loop that keeps track of the iterator precisely.

for (initializer; condition; statement1) {
    statement2;
}

There is an unwritten rule that we usually write ++i in statement1 because compared with i++ which need to make a copy, ++i is more efficient. However, it’s arguably correct because modern compilers can surely optimize this defect.

Classes

A simple but intuitive example of classes is to describe a people in C++ (here we assume type string under the namespace std is used):

class Person {
    string name;
    string email;
    int stu_id;
};

We can also implement member functions in the class, just to make it more convenient:

class Person {
    string name;
    string email;
    int stu_id;

    string getName();
    string getEmail();
    int getStuID();

    void setName(string new_name);
    void setEmail(string new_email);
    void setStuID(int new_stu_id);
};

Protection

We have three levels of data protection in a class:

This means we can protect data in the class by declaring them as private while get access to them via public member functions:

class Person {
private:
    string name;
    string email;
    int stu_id;

public:
    string getName();
    string getEmail();
    int getStuID();

    void setName(string new_name);
    void setEmail(string new_email);
    void setStuID(int new_stu_id);
};

Constructor & Destructor

An instance created based on a class is called an object. To create an object, we may need a constructor, a copy constructor and a destructor.

class Person {
private:
    string name;
    string email;
    int stu_id;

public:
    Person();                                       // default constructor
    Person(string name, string email, int stu_id);  // normal constructor
    Person(const Person& another_person);           // copy constructor
    ~Person();                                      // destructor
    string getName();
    string getEmail();
    int getStuID();

    void setName(string name);
    void setEmail(string email);
    void setStuID(int stu_id);
};

Coding Style

According to this coding style we have in Person.h

#include <string>
using std::string;

class Person {
public:
   Person();
   Person(string name, string email, int stu_id);
   Person(const Person& another_person);
   ~Person();
   string GetName();
   string GetEmail();
   int GetStuID();
   void SetName(string name);
   void SetEmail(string email);
   void SetStuID(int stu_id);

private:
    string name_;
    string email_;
    int stu_id_;
};

In Person.cpp we implement the member functions of the class:

string Person::GetEmail() {
  return email_;
}

void Person::SetEmail(string email) {
   email_ = email;
}

...

Just keep in mind that the constructors as well as the destructor should also be implemented:

Person::Person() {
   name_ = "";
   email_ = "";
   stu_id_ = 0;
}

Person::Person(string name, string email, int stu_id) {
    name_ = name;
    email_ = email;
    stu_id_ = stu_id;
}

Person::Person(const Person& another_person) {
    name_ = another_person.name_;
    email_ = another_person.email_;
    stu_id_ = another_person.stu_id_;
}

Person::~Person() {}

We can also use the colon syntax for constructors:

Person::Person() : name_(""), email_(""), stu_id_(0) {}
Person::Person(string name, string email, int stu_id) : name_(name), email_(email), stu_id_(stu_id) {}
Person::Person(const Person& another_person) :
    name_(another_person.name_),
    email_(another_person.email_),
    stu_id_(another_person.stu_id_)
{}

Struct

a struct is a class with one difference: struct members are by default public, while class members are by default private.

struct Person {
    string name;
    string email;
    int stu_id;
}

Operator Overloading

For a newly created class we cannot use person2 = person1 if we want to assign the whole object person1 to person2. We have to use constructors. What we can do, instead, is to overload these operators (e.g. the assignment operator =) specifically for the class.

The overloadable operators include + - * / % ^ & | ~ ! = < > <= >= ++ -- << >> == != && || += -= *= /= &= |= ^= %= <<= >>= [] () -> ->* new new[] delete delete[].

The non-overloadable operators are :: .* . ?=.

void Person::operator=(const Person& another_person) {
    name_ = another_person.name_;
    email_ = another_person.email_;
    stu_id_ = another_person.stu_id_;
}

However, such overloading does not support chain assignment like person3 = person2 = person1. We need to return a reference in order to support that.

Person& Person::operator=(const Person& another_person) {
    name_ = another_person.name_;
    email_ = another_person.email_;
    stu_id_ = another_person.stu_id_;
    return *this;
}

where this is a pointer pointing to the object itself.

Another concern is self-assignment, which in some cases can be dangerous and in almost every situation is inefficient. To avoid self-assignment we need to detect and skip it.

Person& Person::operator=(const Person& another_person) {
    if (this != &another_person) {
        name_ = another_person.name_;
        email_ = another_person.email_;
        stu_id_ = another_person.stu_id_;
    }

    return *this;
}

Include Guards

In C++, a function can only be defined once. This is called the One Definition Rule (ODR). To avoid multiple including of the header files, we use include guards. This is being done by defining a macro at the beginning of each header file.

#ifndef MYCLASS_H
#define MYCLASS_H

class MyClass {
  ...
}

#endif

Containers

Here we introduce two of the most useful containers in the C++ Standard Library: std::vector and std::map. To initialize an empty vector, we use

#include <vector>
using std::vector;

int main() {
    vector<int> v;
    for (int i=0; i<=10; ++i) {
        v.push_back(i);
    }
}

and to initialize with a specific size, we do

#include <vector>
using std::vector;

int main() {
    vector<int> v(10);
    for (int i=0; i<=10; ++i) {
        v[i] = i;
    }
}

On the other hand, map containers are like dict in Python, which allows you to use indiced of any type, e.g. std::string.

#include <map>
using std::map;

int main() {
    map<unsigned int, string> zipcodes;
    zipcodes[60604] = "Chicago";
    zipcodes[60637] = "Hyde Park";
}

Data Abstraction

Data abstraction refers to the separation of interface (public functions of the class) and implementation:

Encapsulation

Encapsulation refers to combining data and functions inside a class so that data is only accessed through the functions in the class.

Friend

We can declare friend a function or class s.t. they can get access to the private and protected members of the base class.

class MyClass {
public:
	MyClass();
	~MyClass();
private:
	int my_data;

	friend void change_data(MyClass& obj);
}

and you can implement and use this function change_data globally in the function to change my_data.

Inheritance

Inheritance refers to based on the existing classes trying to:

A simple example would be

class Student {
public:
   Student(string name, string email, string major);
   string GetName();
   string GetEmail();
   string GetMajor();
private:
   string name_;
   string email_;
   string major_;
};

with meanwhile

class Employee {
public:
   Employee(string name, string email, string job);
   string GetName();
   string GetEmail();
   string GetJob();
private:
   string name_;
   string email_;
   string job_;
};

Apparently a lot of functions and data are repeated. What we’re gonna do is to build a base class and reuse it onto two derived classes. Note:

In actual coding, this is what we do:

class Person {
public:
   Person(string name, string email);
   string GetName();
   string GetEmail();
private:
   string name_;
   string email_;
};

with

class Student : public Person {
public:
   Student(string name, string email, string major);
   string GetMajor();
private:
   string major_;
};

and

class Employee : public Person {
public:
   Employee(string name, string email, string job);
   string GetJob();
private:
   string job_;
};

To initialize a base class, we define constructors just like what we did before:

Person::Person(string name, string email) : name_(name), email_(email) {}

while for derived classes, we need to call the base class constructor

Student::Student(string name, string email, string major) : Person(name, email), major_(major) {}
Employer::Employer(string name, string email, string job) : Person(name, email), job_(job) {}

A derived class can access members in the base class, subject to protection level restrictions. Protection levels public and private have their regular meanings in an inheritance class hierarchy: 􏰀

A derived class can also access protected members of a base class. If a class has protected members: 􏰀

Virtual

A base class uses the virtual keyword to allow a derived class to override (provide a different implementation) a member function. If a function is virtual (in the base class): 􏰀

class Base1 {
public:
    virtual void Fun1();
    virtual void Fun2();
    void Fun3();
};

and then functions like Fun1 will be revisable in inheritance. Note that the base class has to implement all functions no matter they’re virtual or not.

Pure Virtual & Abstract Classes

If we don’t give a default implementation of a virtual function, we call it pure virtual. This is been done by assigning =0 at the time of definition.

class Base2 {
public:
   virtual void Fun1() = 0;
};

In this case the base class does not need to implement this Fun1 and in contrast, the derived class must do so. A class with virtual functions is called an abstract class. Note that we cannot instantiate (make an object of) an abstract class until every virtual function is implemented.

Comparing Virtual & Pure Virtual

There’s a slight difference between normal member functions, virutal functions and pure virtual functions during inheritance.

Polymorphism

We use a pointer or a reference to a base class object to point to an object of a derived class, which we call the Liskov Substitution Principle (LSP).

Option* option1 = nullptr;
Option* option2 = nullptr;
option1 = new EuropeanCall(...);
EuropeanCall(...) option_;
option2 = option_;

More direct example may be as follows. Instead of writing separately

double Price(EuropeanCall option, ...) {
    return option.Price(...);
}
double Price(EuropeanPut option, ...) {
    return option.Price(...);
}

we can use polymorphism and write it w.r.t. the base class Option using a reference or pointer

double Price(Option& option, ...) {
    return option.Price(...);
}

Const Member Functions

For variables we declare constancy by

const int val = 10;

For constant objects, e.g.

class Student {
public:
    ...
    string GetName();
private:
    string name_;
    string email_;
};

string Person::GetName() {
    return name_;
}

when we call

const Student a('Allen', 'allen@gmail.com');
cout << a.GetName() << endl;

we meed a compile error. This is due to that the compiler does not know the function GetName is constant. To declare that we need

class Student {
public:
    ...
    string GetName() const;
private:
    string name_;
    string email_;
};

string Person::GetName() const {
    return name_;
}

When we have pure virtual constant member functions, we write like this: virtual type f(...) const = 0.

Mutable

A const member function cannot modify data members. The only exception of this issue is mutable data members.

class Student {
public:
    ...
    string GetName() const;
private:
    string name_;
    mutable string email_;
};

Override

The override keyword serves two purposes:

class base {
public: 
    virtual int foo(float x) = 0; 
};


class derived1: public base {
public:
    int foo(float x) override { ... }
}

class derived2: public base {
public: 
    int foo(int x) override { ... } 
};

In implementation of the pure virtual function foo in derived class derived1, we’re doing just as told by the base class. In derive2, with the override keyword we’ll get an error for overwriting the original virtual function by changing types; while without this keyword we’ll get at most just a warning.

Static

For non-static member we change an instance’s data and it’s done. Nothing will happen to other instances of the same derived class. For a static member function/data the association is built and we can change one and for all.

class Counter {
public:
    static int GetCount();
    static void Increment();
private:
    static int count_;
};

Function Objects

A regular function is generally

int AddOne(int x) {return x + 1;}

while a function object implementation is

class AddOne {
public:
    int operator()(const int& x) {return x + 1;}
};

and for the latter we can use its instances as objects, which still work as functions.

vector<int> values{1, 2, 4, 5};
for_each(values.begin(), values.end(), AddOne());

where AddOne() is an unnamed instance of the class AddOne.

Lambda

In C++ we have inline function definition as

int f = [](int x, int y) { return x + y; };
int x = [](int a, int b) { return a + b; }(1, 2);

The [] is called the capture operator and it has rules as follows.

OOP: Reviews

Below we introduce some features in STL.

Methods

Two of the most commenly seen methods are begin() and end()

Functions

We have binary_search, for_each, find_if and sort.

Structure

In the STL, algorithms are implemented as functions, and data types in containers.

Sorting and Searching in STL

int main() {
    vector<int> values {10, 1, 22, 12, 2, 7};
    sort(values.begin(), values.end());
    bool found = binary_search(values.begin(), values.end(), 12);
    cout << found << endl;
}

Copying Elements

int main() {
    vector<int> values1 {10, 1, 22, 12, 2, 7};
    vector<int> values2;
    copy(values1.begin(), values1.end(), back_inserter(values2));
}

Predicates

bool PersonSortCriterion(const Person& p1, const Person& p2) {
    return p1.GetAge() < p2.GetAge();
}


int main() {
    Person p1("Jim", 12);
    Person p2("Joe", 23);
    Person p3("Jane", 21);
    vector<Person>  ppl;
    ppl.push_back(p1);
    ppl.push_back(p2);
    ppl.push_back(p3);
    sort(ppl.begin(), ppl.end(), PersonSortCriterion);
}

STL & Lambda

By combining STL algorithms with lambdas in C++ can be very efficient. We can use lambdas in a loop without defining a function beforehand.

vector<int> v{1, 3, 2, 4, 6};
for_each(v.cbegin(), v.cend(), [](int elem) { cout << elem << endl; } )

We can also use it as a sorting criterion

std::vector<Person> ppl;
sort(ppl.begin(), ppl.end(), [](const Person& p1,  const Person& p2) {
    if (p1.GetAge() < p2.GetAge()) return true;
    else return false;
});

Templates

We can have templates of a function:

template <class T> T sum(T a, T b) {return a + b; }
sum(12, 13) // 25

We can also have templates of a class:

template <class T>
class Pair {
public:
    Pair(T a, T b) : First(a), Second(b) {}
private:
    T First, Second;
}

Pair<int> new_pair(12, 13);

Exception Handling

int x, y;
x = 1;
y = 2;
try {
    x += 1;
    y += x;
    if (y ==4) {
        throw 1;
    }
}
catch (int err) {
    if (err == 1) {
        cout << "Error 1" << endl;
    }
}

File I/O

#include <iostream>
#include <fstream>
#include <string>
using std::string;
using std::endl;
using std::cout;

int main() {
    ifstream f_in('in.txt');
    ofstream f_out('out.txt', ios::trunc);
    if (f_in.is_open() && f_out.is_open()) { cout << "Files opend!" << endl; } 
    string line;
    while (getline(f_in, line)) { f_out << line << endl; }
    f_in.close();
    f_out.close();
}

Specifically, for the open modes we have

Mode Description
ios::app Append to the end
ios::ate Go to the end of file on opening
ios::binary Open in binary mode
ios::in Open file for reading only
ios::out Open file for writing only
ios::nocreate Fail if you have to create it
ios::noreplace Fail if you have to replace
ios::trunc Remove all content if the file exists

References