If you have spare time
Watch some CppCon βBack to Basicsβ videos:
- Back to Basics: The Structure of a Program - Bob Steagall - CppCon 2020
- Back to Basics: RAII and the Rule of Zero - Arthur OβDwyer - CppCon 2019
- Back to Basics: Concurrency - Arthur OβDwyer - CppCon 2020
Mechanics of a C++ program
- Write source code
1.1 Unix extensions:
C,cc,cxx,c
1.2 GNU C++ extensions:C,cc,cxx,cpp,c++
- Compile the source code - Translate the code to machine code. The file containing the translation is the object code of the program.
- Link the object code with additional code - Combination of the source code with startup code and library object code to produce a runtime version. The final product is a file called the executable code, which contains a set of machine language instructions.
- Execute the program
Compilation and linking is done with:
- UNIX -
CC
- GNU C++ -
gcc,g++
( notes on the difference ), gcc small description
Preprocessor
The preprocessor processes a source file before compilation. It allows the definition of macros, which are abbreviations for longer constructs.
Directives
Lines which begin with #
:
#define IDENTIFIER [value]
- Replaces the occurrences ofIDENTIFIER
in the code withvalue
. Note thatvalue
is optional.#undef IDENTIFIER
- Removes the definition ofIDENTIFIER
.
Conditional directives allow the inclusion or exclusion of parts of the code if a certain condition is met:
#ifdef IDENTIFIER
- IfIDENTIFIER
is defined, then the code that follows is included until#endif
is reached.#ifndef IDENTIFIER
- IfIDENTIFIER
is not defined, then the code that follows is included until#endif
is reached.
Conditional directives are used, for example, to include headers only once:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
// header code
#endif // FOO_BAR_BAZ_H_
File inclusion:
#include <library>
- The contents of thelibrary
file are sent along with the source code. In essence, the contents of thelibrary
replace the#include
line. Note that the compiler tries to findlibrary
in the host systemβs file system that holds the standard header files.#include "library"
- Same as above, butlibrary
is looked for in the current working directory.-
#pragma
- Specify diverse options to the compiler that are specific to the platform/compiler.
Program structure
Large programs can be split into multiple files, which can be compiled (if needed) and linked to generate an executable program. For example, a program can be split into three files:
- A header file that contains structure declarations and prototypes for functions that use those structures.
- A source code file that contains the code for the functions.
- A source code file that uses those functions.
The following things are commonly found in headers:
- function prototypes
- symbolic constants defined with
#define
orconst
- structure, class, template declarations
- inline functions
trunk
βββ bin : for all executables (applications)
βββ lib : for all other binaries (static and shared libraries (.so or .dll))
βββ include : for all project header files, 3rd party files not present in `/usr/local/include` should be here
βββ src : for source files
βββ doc : for documentation
βββ build : for all the object files, removed by `clean`
βββ test : for testing
Example:
. project
βββ build
βββ include
β βββ project
β βββ Vector.hpp
β βββ [third party library]
βββ src
βββ Vector.cpp
βββ main.cpp
Strings
C-style strings
The last character is the null character, \0
.
char name[20]; // initialized with random data
char name[5] = {'j', 'h', 'o', 'n', '\0'};
char name[8] = {'j', 'h', 'o', 'n', '\0'}; // right padded with \0
char name[5] = "john"; // the \0 is understood
char name[8] = "john"; // right padded with \0
char name[] = "john"; // let the compiler count
Operations:
#include <cstring>
char source[] = "john"; // let the compiler count
char dest[10];
// size of the string
strlen(source); // 4
strlen(dest); // 10, 10 random characters, the 11th is \0
// copy `source` to `dest`
strcpy(dest, source);
// concat `dest` with `source`
strcat(dest, source);
Reading input:
char name[20];
cin >> name; // read until space or newline
cin.getline(name, 20); // read 20 characters or until newline
cin.get(name, 20); // read 20 characters or until before newline
C++ strings
#include <string>
string str; // ""
string name = "john";
cin >> name; // reads until space or newline
cin.getline(name); // reads until newline
Pointers
Given a variable, the address operator &
is used to get its address or location in memory.
int oranges = 5;
int apples = 6;
// location in memory e.g. 0x0065fd40
&oranges;
// location in memory e.g. 0x0065fd44
&apples;
// NOTE: the difference between them is 4 bytes, the size of int
Pointers are variables that store the addresses of values rather than the values themselves. To declare a pointer, we use the form typeName * pointerName
.
int oranges = 5;
int* p_oranges; // declare pointer to an int
p_oranges = &oranges; // assign address to pointer
sizeof(p_oranges); // 4 bytes
The dereferencing operator, *
, yields the value at the location.
int oranges = 5;
int* p_oranges = &oranges;
*p_oranges; // 5
*p_oranges = *p_oranges + 1; // update the value
oranges; // 6
*p_oranges; // 6
Always initialize a pointer to a definite address before applying the dereferencing operator.
int* p_int;
*p_int = 3; // value is lost forever
When a pointer is assigned to another pointer, the value stored is the address stored in the first pointer.
int oranges = 5; // value: 5, address: 0x000
int* p = &oranges; // value: 0x000, address: 0x004
int* q = p; // value: 0x000, address: 0x008
*q; // 5
If we want to create a pointer to a pointer, we use extra ββ. For the declaration, the number of ββ must be equal to the length of pointers (including this one). In the same fashion, we must use the same number of β*β for dereferencing.
int oranges = 5; // value: 5, address: 0x000
int* p = &oranges; // value: 0x000, address: 0x004
int** q = &p; // value: 0x004, address: 0x008
*p; // 5
**q; // 5
Pointer and arrays
C++ handles arrays internally using pointers, which may seem equivalent. An ordinary array variable name is interpreted as the address of the first element of the array. The bracket notation, []
, allows us to get/set elements of the array.
int numbers[] = {1, 2, 3};
numbers; // address 0x0065fd40
numbers[0]; // 1, the value allocated in memory
// NOTE: numbers ~ &numbers[0]
// since a pointer is a reference to an address we can also do
int* p_numbers = numbers;
*p_numbers; // 1, the value in memory accessed through pointer dereferencing
Adding one to a pointer variable increases its value by the number of bytes of the type to which it points.
int numbers[] = {1, 2, 3};
int* p_numbers = numbers;
p_numbers; // points to the first element of the array
p_numbers + 1; // points to the second element of the array
p_numbers + 2; // points to the third element of the array
// NOTE:
// numbers[0] == *(p_numbers)
// numbers[1] == *(p_numbers + 1)
// numbers[2] == *(p_numbers + 2)
The value &numbers
is the address of a 3-int block of memory, so even though &numbers[0] == numbers == &numbers
numerically, the value of &numbers + 1 != numbers + 1
because &numbers + 1
points to the next 3-int block of memory; however, numbers + 1
points to the second element of the initial 3-int block of memory.
numbers
is type pointer-to-int orint*
&numbers
is type pointer-to-array-of-3-int or(*int)[3]
The relationship between pointers and arrays also extends to C-style strings. For C++, a quoted string constant, strings in an array, and strings described by pointers are all handled equivalently.
char first[20] = "john";
const char* last = "smith"; // string literals are constant
cout << "I am the agent" << first << " " << last
Given a multidimensional array int a[][2] = { { 1, 2 } }
, a
is a pointer to the first element, which is a 2-element array (which is a pointer to the first of its elements). Therefore, a pointer to a
has the form of a pointer-to-array-of-2-int.
int a[][2] = { { 1, 2 } };
int (*b)[2] = a;
(*b)[0]; // 1
Array of pointers
int a = 1, b = 2;
int* p[2] = {&a, &b};
Since p
is a pointer to the first element, which is &a
, and &a
is another pointer, we can reference p
with a pointer to a pointer.
int** q = p;
Runtime allocation: new
Pointers are a sort of alias for accessed memory, which could be accessed by named variables (memory allocated at compile time). However, we can allocate memory at runtime with the new
operator. Runtime-allocated memory can be freed with the delete
operator.
Advantages of runtime-allocated memory:
- Memory is allocated only when needed.
Drawbacks of runtime-allocated memory:
- Memory allocated by
new
must be freed using thedelete
operator; otherwise, we have a memory leak, which is memory that is allocated but unused. If it grows too large, it can halt the execution of the program. - An attempt to free a block of memory that has been previously freed results in undefined behavior, i.e., donβt use
delete
twice on the same block of memory in succession.
Additional notes regarding runtime-allocated memory:
- Ordinary variables have their values stored in a memory region called the stack. Memory allocated with
new
has its values stored in a memory region called the heap.
// p_int address = 0x0065fd40
int* p_int = new int;
delete p_int;
int oranges = 5;
int* p_oranges = &oranges;
// INVALID since delete works only with memory allocated with new
delete p_oranges;
Dynamic arrays can be created with new typeName[count]
. A pointer can be assigned to the location of the first element of the dynamic array.
// dynamic array
int* p_array = new int[10];
// p_array points to the first element of the array
// *p_array is the value of the first element using pointer dereferencing
// p_array[0] is also the value of the first element using array notation
delete [] p_array;
Dynamic structures can be created with new structName
. When a pointer points to this block of memory, we can access the properties with the arrow membership operator, ->
.
struct person {
string name;
int age;
};
person* p_person = new person;
p_person->name = "john smith";
p_person->age = 25;
Functions
Steps to build a function:
- Provide a function prototype.
- Provide a function definition.
- Call the function.
// function prototype
double cube(double x);
int main() {
// function call
double q = cube(2.2);
}
// function definition
double cube(double x) {
return x * x * x;
}
Writing prototypes has the following advantages:
- The compiler correctly handles the function return value.
- The compiler checks for the use of the correct number of arguments.
- The compiler checks for the use of the correct type of arguments (performing conversion to the correct type if possible).
When a function is called with basic types for arguments, the function creates a new variable and initializes it with the same value, i.e., the function works with a copy of the basic types.
int main() {
double x = 1.3;
cube(x);
// ..
}
double cube(double x) {
// x is passed by value
// x is private to this function
return x * x * x;
}
However, we can instead pass the address of the basic type, which means that the function should be rewritten to use pointers.
int main() {
double x = 1.3;
cube(&x);
// ..
}
double cube(double* x) {
// x is passed by value
// x is private to this function
return (*x) * (*x) * (*x);
}
This is useful for complex structures if we want to save time/space by passing a reference to the structure instead of passing the entire structure.
struct person {
string name;
int age;
};
int main() {
person john = { "john doe", 25 };
greet(&john);
// ..
}
double cube(person* someone) {
// someone is private to this function
// someone is a pointer to the original person
someone->age; // 25
}
When a function is called with an array, whatβs actually sent is the name of the array, which is the address of the first element (a pointer-to-int, int *
). This is different from basic types because the array is not copied; instead, the function works with the original array.
const int k_size = 3;
int main() {
int a[k_size] = {1, 2, 3}
sum(a, k_size); // 6
cout << *a << endl; // 1
}
double sum(int* a, int k_size) {
// a is another pointer to the original array
// a is private to this function
int sum = 0;
for (int i = 0; i < k_size; ++i) {
sum += *a;
a++;
}
return sum;
}
Inline functions
When a program is executed and a function is about to be invoked, the following steps occur with the program:
- store the memory address of the next instruction
- copy function arguments to the stack
- jump to the memory address the function is located
- execute the function code
- jump back to the instruction stored
A small enhancement to speed up the program is to make the function inline. That is, the program replaces the function call with the function code, avoiding the jumps.
When to use it:
- the function is small and called very often
inline double cube(double x) { return x * x * x; }
Reference variables
A reference variable is a name that acts as an alias for a previously defined variable.
int p;
int& q = p;
In this context, &
is not the address operator. Instead, it serves as part of the type identifier. Like int*
is a pointer-to-int, int&
is a reference-to-int.
- A reference must be initialized to a defined variable when declared.
- A reference is like a const pointer, e.g.,
int& r_n = n;
is likeint* const r_n = &n;
.
int n = 5;
int* p_n = &n;
int& r_n = n;
// the following expressions can be used interchangeably
// - *p_n, r_n, n to get the value
// - p_n, &r_n, &n to get the address
Example with a function:
int main() {
int x = 2;
pow2(x); // 4
x; // 2
}
int pow2(int& x) {
// x is an alias to the x in main
return x * x;
}
Note that any change to x
in pow2()
will actually change the original value. To avoid this behavior, use const
, e.g., int pow2(const int &x)
.
Reference arguments should be used to:
- allow the modification of data inside a function
- speed the program by passing a reference instead of an entire data object
Classes
class Person {
// var, functions declared here are private by default
private:
// private vars and function prototypes
public:
// public vars and function prototypes
void sayHi();
};
Class member functions
- Class member functions can access the private components of the class.
- To identify which class a function definition belongs to, the
::
operator is used.
void Person::sayHi() { /* ... */ }
- If a class member function wonβt modify the created instance, then use the
const
qualifier for the function.
// function prototype
class Person {
// ..
void show() const;
}
// function definition
void Person::show() const { /* ... */ }
All class methods have a this
pointer set to the address of the object that invokes the method. Class members can be accessed through pointer dereferencing.
Class constructor/destructor
- A class has the default constructor by default. It has the form
Person() {}
. - Custom constructors/destructors can be defined as follows:
class Person {
string name;
int age;
public:
// implicit default constructor:
// Person() {}
Person(); // default constructor
Person(string &name); // operator overload
Person(string &name, int &age); // operator overload
~Person(); // default destructor
}
// constructor definition
Person::Person() {
// explicit default constructor
// NOTE: constructor/destructor returns the class object (no need to add return)
}
Person::Person(string& name) { /* ... */ }
Person::Person(string& name, int& age) { /* ... */ }
Person::~Person() { /* ... */ }
Class objects
int main() {
Person a; // default constructor
Person b = Person("john", 25); // with parameters
Person c("john", 25); // alternative syntax
Person* p_d = new Person("john", 25); // pointer-to-Person
b.show();
p_d->show();
}
Operator overloading
class Time {
public:
Time operator+(const Time& other) const;
}
// ..
Time Time::operator+(const Time& other) const {
Tim total;
// code for `total = other + *this`
return total;
}
// ..
int main() {
Time a, b;
Time c = a + b;
// translated to a.operator+(b)
}
Misc
Deciphering variable types
http://andybohn.com/deciphering-variable-types/
- Find the identifier and start there.
- Sweep to the right, translating the symbols you see. You should stop your sweep to the right when you get to the end of the type, or if you see a lone right parenthesis
)
. Seeing a left parenthesis(
is the start of a function symbol, so continue sweeping right. - Sweep left of the identifier until you run out of symbols, or you hit a left parenthesis
(
. If you hit the left parenthesis now, you should go back to part 2, sweeping right, but now on the outside of the enclosing)
, and continuing onto part 3 on the outside of the enclosing(
.
Reading examples
Read a number and the next line as a string:
// input:
// 1234\n
// a line of text
int year;
string name;
(cin >> year).get();
getline(cin, name);
Read until a char is found (note that cin >> ch
omits spaces):
char ch;
cin.get(ch); // or ch = cin.get();
while (ch != '#') {
// do something with ch
cin.get(ch);
}
Read until EOF:
int a, b;
// cin is an istream object that is casted to bool in this case
while (cin >> a >> b) { ... }
string str;
// same as before cin is casted to bool
while (getline(cin, str)) { ... }
char ch;
cin.get(ch);
// same as before cin is casted to bool
while (cin) { cin.get(ch); }
char ch;
while ((ch = cin.get()) != EOF) { ... }
Tokenize:
// example: split the following line by commas
// 1,2,hello
stringstream tokens(line);
string token;
string id, rank, description;
getline(tokens, id, ',')
getline(tokens, rank, ',')
getline(tokens, description)
Read/write files:
#include <fstream>
ifstream inFile;
inFile.open("input");
ofstream outFile;
outFile.open("output");
string line;
int n;
// reading input from file
getline(inFile, line);
inFile >> n;
// writing output to file
outFile << line;
outFile << n;
// close the stream
inFile.close();
outFile.close();
Read from a file, reusing the stdin stream, and write to a file, reusing the stdout stream. See
freopen
:
#include <cstdio>
freopen("input", "r", stdin);
freopen("output", "w", stdout);
// use cin here
// close the streams
fclose(stdin);
fclose(stdout);
Type casts
(long) value
long(value)
static_cast<long> (value)
// pointer cast
int* p_number = (int*) 0xB8000000;
Conversion between types
C++11
Declarations:
auto
: automatic type deduction.decltype
: creates a variable of the type indicated by an expression.
Range-based for
loop:
int numbers[] = {1, 2, 3, 4, 5};
for (int n : numbers) { ... }
for (int n : {1, 2, 3, 4}) { ... }
for (auto n : {1, 2, 3, 4}) { ... }
Multithreading
- Back to basics: Concurrency CppCon 2020 https://www.youtube.com/watch?v=F6Ipn7gCOsY
- Multithreading basics: https://classroom.udacity.com/courses/ud923
- Concurrent Programming with C++: https://www.youtube.com/playlist?list=PL5jc9xFGsL8E12so1wlMS0r0hTQoJL74M