2020-08-01
Notes on the C++ language for CSI2372.
Like C, but object oriented.
Like Java, but far less intuitive.
Like a vaccum cleaner with burn, soak, espresso machine, and do taxes settings, C++ is a speedy swiss army knife with a billion functions.
#include <iostream>
// Like C, C++ programs have a main function
int main(){
// Print characters to standard output
std::cout << "Hello World!" << std::endl;
return 0;
}
C++ is designed to allow you to express ideas, but if you don’t have ideas or don’t have any clue about how to express them, C++ doesn’t offer much help.
— Bjarne Stroustrup1
C++ is a compiled and statically-typed language. Many possible bugs that you can write will be caught early by checks run during the compilation process. To get started, install the c++ build tools:
$ sudo apt-get install g++ make
Compile the hello world program with g++, like this:
$ g++ hello.cpp -o hello
$ ./hello
Hello World!
Congrats! You’ve just built and run your first C++ program. What a champion!
Before I touch on the myriad of complex build systems in C++, please take a look at the very simplest build system, make. This may look overwheliming at first, but I can explain:
# CSI2372 Generic Makefile
.PHONY: all
all: e1.o e2.o
e1.o: myFile1.cpp myFile1a.h myFile1b.h
g++ -Wall -o e1.o myFile1.cpp
e2.o: myfile2.cpp SetInt.h
g++ -Wall -o e2.o myfile2.cpp
Make clauses are structured like this:
target: dependencies
build-command
The .PHONY
special case will run every time make is executed.
This Makefile
is structured so e1.o
and e2.o
(the output for exercises
one and two for a CSI lab) will be compiled. Here are the steps:
all
target.e1.o
and e2.o
.e1.o
and e2.o
depend on are present, and builds them.By following these steps, make can ensure that, whenever it is run, all the executables that rely on old code are rebuilt, and you’re always running the latest compiled code.
For further commentary on compiling with header files, see the section Compiling with Headers.
In the following subsections, I’ll cover the most basic features of the language, which should be easy to pick up if you have previous programming experience with another programming language, especially C or Java.
Things in C++ are named with identifiers, which can be a series of characters and numbers from a-z, A-Z, 0-9, and underscores. An identifier cannot start with a number.
Basic C++ supports the following elementary types:
int, float, double, long double, char, and bool. With the #include<string>
library, the type string is also available for use.
// Simple variable declarations:
double x;
bool y;
int p;
Standard C++ hackery allows you to declare and initialize variables in the following ways:
double v = 32.123;
double v(32.123);
Additionally, you can declare and optionally initialize multiple variables at the same time:
int a=2, b=3, c, d, e=4;
The const keyword can be used to declare a variable which cannot be re-assigned:
const int height = 182;
The C programming language makes extensive use of arrays, and C++ continues that tradition. To initialize an array:
// type name[size?] = {elem elem elem};
// type name[size];
int scores[10];
int scores[] = { 1, 2, 3, 4 }
int scores[10] = { 1, 2, 3, 4 }
int scores[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }
int scores[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
Providing both a length and values will fill the non-specified values with the default value of the array’s class. For int
, zero is provided.
Print to the screen with cout:
cout << "X is 3! Wow!" << endl;
Take input with cin:
int x;
cout << "Enter your favourite integer:";
cin >> x;
// x now holds the user-entered number.
Control flow constructs allow logic to run under certain conditions.
if(x == 3){
cout << "X is 3! Wow!" << endl;
}else{
cout << "X is not 3." << endl;
}
A simple for loop that prints numbers up to a target:
int target = 200;
for(int i=0; i<target; i++){
cout << "i has value: " << i << endl;
}
A while loop to do the same:
int count = 0;
while (count <= 200) {
cout << "count has value: " << count << endl;
count++;
}
A do-while loop to do the same:
int count = 0;
do {
cout << "count has value: " << count << endl;
count++;
} while (count <= 200);
The ternary if, never nest this or your coworkers will hate you forever and thereafter.
string res = b ? "b is true" : "b is false";
Logic can be compartmentalized into functions that can be called from other parts of a C++ program.
C++, being a derivative of c, does not automatically hoist functions, so you must provide the declaration of a function at the top of a file, or in the header, if you plan to call the function within a different function that is instantiated above the original.
// return-type name( arguments with types ){
// function content
// return return-type
// }
int max(int a, int b){
return a > b ? a : b;
}
Within functions, static variables can be declared that won’t be changed from one call to the next, unless the program is reset.
int max(int a){
static b;
if( a > b ) b = a;
return b;
}
A function can be placed into the body of the parent it is called in with the inline keyword.
inline int max(int a){
static b;
if( a > b ) b = a;
return b;
}
Functions can be given default arguments by using the equals sign within the arguments in the instantiation:
int addOne(int a = 10){
return a + 1;
}
addOne(4); // returns 5
addOne(); // returns 11
You can provide multiple definitions for the same function name, as long as different arguments or argument types are specified:
int addOne(int a){
return a + 1;
}
float addOne(float a){
return a + 1;
}
You can call functions from other namespaces by prepending a function name with the namespace name, or you can use a namespace to make all the functions accessible to the current program.
Without namespace:
#include <iostream>
// ...
std::cout << "Hello World!" << std::endl;
With namespace:
#include <iostream>
using namespace std;
// ...
cout << "Hello World!" << endl;
You’ve probably noticed that just the std::
has been removed from the second program’s calls to the iostream functions.
To create your own namespace use the namespace keyword to wrap functions, variables, and classes:
namespace rcf{
int three(){
return 3;
}
}
// ...
rcf::three();
To pass input to a program, one may pass command line arguments.
It is easy to print all the arguments by iterating through the
elements given in argv
. Conveniently, the number of arguments
is passed as argc
, allowing C++ users to use a for loop to
iterate through the input.
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
cout << "Arguments passed to program:" << endl;
// The number of elements is given as 'argc'
for(int i=0; i<argc; i++)
{
// Print commas between elements
if(i!=0) cout << ", ";
// Print the positional argument
cout << argv[i];
}
// End the list
cout << endl;
return 0;
}
Attempting to cram a huge program into a single source file is very impractical, and you should regard anybody who does so as either incredibly lazy or more than a little bit masochistic. For this reason, Kernighan and Stroustrup have provided methods for splitting your code into multiple source and header files.
Typically, a header file will include function and class declarations needed by all files in the program, and logic needed only in a given file is kept there.
As it quickly becomes impractical to compile a large set of files, C++ has a great number of complex build systems to manage source and header files, along with required libraries. Typing very long build commands is, to say the least, prone to error.
A Makefile is the simplest way to compile a complex program made of more than a few files.
# TODO: Provide better example:
g++ a.cpp -o a.o
If you’ve only ever used Python, JavaScript, Ruby, or Java, and have never dabbled with an assembly language or C, you’re in for a treat.
In simple terms, a pointer holds an address.
That’s it. Remember this. A pointer holds the address of a memory location.
// & The address operator, an ampersand.
// * The pointer operator, a star.
Here’s a basic example of how pointers work:
int a = 3; // a holds 3
int *b = &a; // b points to the address of a
*b = 4; // the address that b points to now holds 4
(*b)++; // Increment the value that b points to
cout << a; // prints '5' to the console.
It’s a complex poetry, but with this simple tool, we can do a lot.
Probably one of the most important features of pointers is the ability to
iterate by a single (class-bit-width) in memory. We can do this because
pointers are fundamentally of the type lvalue
. Array names are pointers,
and using this pointer is the most common way to iterate through an array.
Note that the example below uses multiple methods to access array members.
int x[100]; // x is equivalent to &x[0]
// Fill the array with stuff
for(int i=0; i<100; i++) *(x+i) = i;
// Prints 0 .. 99
for(int i=0; i<100; i++) cout << x[i] << endl;
// Prints 0 .. 99
int i = 0;
while(i<100) cout << *(x+i++) << endl;
Memory can be allocated by the programmer with the new and delete operators.
int *x; // Empty pointer of type int
x = new int; // Allocate a new chunk of storage
*x = 4; // Place something in there
New can be used to allocate memory for any type of primitive or object.
// Allocate a single spot, an array,
// and a cube array of type *type*
new type;
new type[n];
new type[a][b][c];
Complimentary to new, we can also delete the objects and primitives that are referenced by pointers with the delete keyword.
int *x;
x = new int[100];
delete x;
It’s possible to set pointers to functions. Here, assume func is a previously defined function that returns a string and takes an integer as an argument.
string (*x)(int);
x = func;
x(3);
In this way, you can also require pointers to functions in function arguments.
Many C++ programs use traditional C style strings:
char x[] = {"Hello"};
char y[] = {'H','e','l','l','o','\0'};
C++ brings OOP to the C programming language.
Object oriented programming (commonly shortened to OOP,) involves the encapsulation of related groups of logic and properties into entities called objects, which are structured according to templates known as classes, structures, unions, and enumerations.
Modern C++ continues to make use of C’s structures, unions, and enumerations to hold sets of related data.
Structures, defined with the keyword struct, are simple storage devices for related variables. Once defined, you can instantiate many instances of a structure, and all of them will be assigned memory to hold the set of properties that you specify.
Simple usage of the struct keyword:
#include <iostream>
using namespace std;
int main()
{
struct item
{
string name;
int serial;
float cost_cad;
float weight_kg;
};
// Create an instance of item
item block;
// Set the properties of block
block.name = "Wooden Block";
block.serial = 1924573;
block.cost_cad = 7.99;
block.weight_kg = 1.0;
// Print the block properties:
cout << "Item: " << endl;
cout << "Name: " << block.name << endl;
cout << "Serial No: " << block.serial<< endl;
cout << "Cost (CAD): " << block.cost_cad << endl;
cout << "Weight (Kg): " << block.weight_kg << endl;
}
Any variable within a struct can be used just like any ordinary variable that was instantiated outside of a struct.
You can also instantiate an array with a structure type.
Unions are like enumerations, and can be instantiated with a list of variables with types. Unlike structures, when a union is instantiated, it may only populate one of the list of contained variables. This allows the same memory location to be populated with data of varying types.
#include <iostream>
using namespace std;
int main()
{
union item
{
int serial;
float cost_cad;
float weight_kg;
};
// Create an instance of item
item block;
// Set the properties of block
block.cost_cad = 7.99;
}
When a union is reassigned (for example, if serial was set after cost in the example above,) the value in memory is overwritten. If cost was printed after serial was set on the item, it would return the floating point representation of the integer which was set – probably a very negative number!
Enumerations are used to store groups of constants, so rather than using numbers to store states, easy-to-read, all-caps variables exist. Try it:
#include <iostream>
using namespace std;
int main()
{
enum color { red, green, blue, yellow };
cout << red << endl; // Prints '0'
}
By storing states as (implicit) integers, we can increase readability without sacrificing runtime performance. You cannot use the same name between enums within the same scope in C++.
Make your own type! The Typedef keyword enables programmers to visually simplify their programs (and confuse others) by reassigning types. For instance:
typedef long double wunderbarnumber;
wunderbarnumber x;
A class encapsulates a set of associated variables and functions. Let’s dive right in with our first class. We declare a class by stating all of its private and public methods and data.
#include <iostream>
using namespace std;
class Man
{
private:
string name;
int height;
int weight;
bool vegetarian;
bool drinker;
int chest;
int ribs;
int iq;
public:
Man(string name, int height, int weight);
void walk(float latitude, float longitude, float altitude);
void speak();
};
Let’s define the constructor that was declared in the class definition.
All we need to do is set all of the private variables to the input that will be given in the object instantiation call.
Man::Man(string n, int h, int w)
{
cout << "Instantiating a Man '"<< n << "'" << endl;
height = h;
weight = w;
name = n;
}
Now we can provide definitions for the rest of the function declarations.
void Man::speak()
{
cout << "My name is " << name << "." << endl;
cout << "I am " << height << "cm tall." << endl;
cout << "I weigh " << weight << "kg." << endl;
}
void Man::walk(float latitude, float longitude, float altitude)
{
cout << "Walking to " << latitude << ","
<< longitude << "," << altitude << endl;
}
We can now use the Man class in an example.
int main()
{
Man j = Man("Jeremy", 182, 70);
j.speak();
j.walk(51.05011, -114.08529, 1086.00);
}
Lastly, let’s run the thing and ensure that everything is working as expected
and giving the output we’d like. Save the code above as main.cpp
and compile
with g++
:
$ g++ main.cpp && ./a.out
Instantiating a Man 'Jeremy'
My name is Jeremy.
I am 182cm tall.
I weigh 70kg.
Walking to 51.0501,-114.085,1086
A class destructor is called, predictably, when an object is destroyed. Destructors have the same name as the class, but include a tilde (~
) at
the beginning of the name. Any cleanup that needs to occur before an object
is destroyed can occur in the constructor.
Destructors do not take arguments.
Man::~Man()
{
cout << "AAAUGH! My brothers will avenge me!" << endl;
}
By combining a constructor and destructor, we can dynamically allocate memory when an object is created based on current requirements, and ensure that the memory is freed when the object is destroyed.
#include <iostream>
using namespace std;
class Frigate
{
private:
// Let's track the remaining ammunition
// for a dynamic number of guns on board
int * gunAmmoStores;
public:
Frigate(int numGuns);
~Frigate();
};
Frigate::Frigate(int numGuns)
{
// Allocate the gun ammo tracker array
gunAmmoStores = new int[numGuns];
// Load 10 bullets into each cannon
for(int i=0; i<numGuns; i++) gunAmmoStores[i] = 10;
}
Frigate::~Frigate()
{
// De-allocate memory
delete gunAmmoStores;
}
int main()
{
Frigate x = Frigate(21);
}
Ha ha.
A static declaration enables the sharing of data between object of the same class.
class Frigate
{
private:
// All frigates use and can modify this
static int shipsInFleet;
// All frigates can use this number
static const int maxSpeed = 30;
public:
Frigate(int numGuns);
};
Vectors are dynamic arrays.
#include <iostream>
#include <vector>
using namespace std;
int main()
{
// Define as: vector<type> name
vector<int> v;
// Push a few elements
v.push_back(3);
v.push_back(9);
v.push_back(2);
v.push_back(8);
v.push_back(12);
// Insert something after '3'
v.insert(v.begin() + 1, 5);
// Remove the last element
v.erase(v.begin() + v.size()-1);
cout << "Vector: ";
for(unsigned int i=0; i < v.size(); i++)
{
if(i!=0) cout << ", ";
// alternatively, cout << v[i];
cout << v.at(i);
}
cout << endl;
// Wipe everything from the vector
v.clear();
if(v.empty())
{
cout << "The vector was cleared!";
}
}
# Run the program:
$ g++ main.cpp && ./a.out
Vector: 3, 5, 9, 2, 8
The vector was cleared!
Never forget that you are responsible for weaving the dreams of humanity into panes of cold logic, run by rocks which we tricked to think with lightning.
More importantly, never forget that computers are not us. They have no inherent humanity. Computers reflect the coldest, most logical and calculating part of ourselves. We must be cautious not to lose ourselves and forsake our souls by gazing too affectionately at the reflection on this black mirror.
We are the music makers,
And we are the dreamers of dreams,
Wandering by lone sea-breakers,
And sitting by desolate streams;
World-losers and world-forsakers,
On whom the pale moon gleams:
Yet we are the movers and shakers
Of the world for ever, it seems.2
Title: C++
Word Count: 3124 words
Reading Time: 15 minutes
Permalink:
→
https://manuals.ryanfleck.ca/cpp/