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
- Preprocessing
- Compiling (where checks syntax and types)
- Linking
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:
- public: anyone have access
- private: only members can access
- protected: disccuss later
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
- We write each class definition in a separate header file
- We implement each class in a separate cpp file
- First letter of the class name is uppercase, e.g.
Person
- Public member functions start with a upper case letter, private members use lower case
- Member variable names end with underscore, e.g.
name_
.
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:
- A user of a class does not need to know how a member function in that class is implemented
- The interface shows you how to use it
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:
- Reuse code
- Improve maintainability and extensibility
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:
- A derived class inherits all members from the base class.
- A derived class may define additional members (data and/or function).
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 cannot access private members of a base class.
- A derived class can access public members of a base class.
A derived class can also access protected members of a base class. If a class has protected members:
- That class can access them
- A derived class of that class can access them
- Everone else cannot access them
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):
- The base class provides an implementation for that function; we call it the default implementation - A derived classes inherit the function interface (definition) as well as the default implementation - A derived class can provide a different implementation for that function (but it does not have to)
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.
- For a normal member function, you don’t necessarily need to implement it, but any derived class cannot change it
- For a virtual function, you can implement or not, and a derived class can redefine it
- For a pure virtual function, you must not implement it, and implementation is a must for any derived classes if you want to instantiate it
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:
- It shows the reader of the code that “this is a virtual method, that is overriding a virtual method of the base class.”
- The compiler also knows that it’s an override, so it can “check” that you are not altering/adding new methods that you think are overrides.
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.
[=]
captures everything by value (read but no write access)[&]
captures everything by reference (read and write access)[=,&x]
captures everything but x by value, and for x by reference[&,x]
captures everything but x by reference, and for x by value
OOP: Reviews
- Classes and Objects
- Data Abstraction and Encapsulation
- Inheritance
- Polymorphism
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
- Chanaka Liyanaarachchi. “Computing for Finance I” (2018).