Session 1: Introduction
Code download
Download the code for the sessions from the GitHub page: Note: If you are on Windows: place your code in a directory without spaces or accents in the path.- E.g. C:\Cours\2A\43043\csc43043ep-lab-code, and not C:\Céline\Mes Cours\csc43043ep-lab-code
in order to verify that your environment is properly configured.
Minimal Program
Code: 01_first_program/a_introduction
-
- If you are on Windows: run the script from file
scripts/visual-studio-generate.batto generate a Visual Studio project, then open Visual Studio from the filea_introduction/build/project.sln(orproject.slnx). Be sure to compile in "RelWithDebInfo" mode to obtain the debugging options. -
- If you are on Mac/Linux: open VS Code from file
a_introduction/vscode.code-workspace(relwithDebInfo). Choose the "RelWithDebInfo" configuration to compile with the debugging options.
-
CMakeLists.txt: CMake configuration file describing the project and the source files to compile. -
CMakePresets.json: CMake configuration file only useful for VS Code. -
vscode.code-workspace: Workspace file to open the project in VS Code (Mac/Linux). -
script/visual-studio-generate.bat: Visual Studio project generation script (Windows only). -
src/: Directory containing the C++ source files (.cpp and .hpp). -
src/main.cpp: The main source file containing the functionmain().
#include <iostream> #include <cmath> int main() { std::cout << "Hello world" << std::endl; return 0; }
- #include <iostream>
-
- - #include allows including code from another file. It provides the mechanism for including external libraries in C++.
-
- #include <FileName> includes the file named
FileName. The angle brackets indicate that this file should preferably be searched for in system files (which is the case for iostream). The inclusion mechanism is very minimalist as the file content is literally copy-pasted during compilation. - - iostream corresponds to the C++ standard input/output library (iostream = Input Output Stream). It allows in particular displaying text on the command line.
- - cmath corresponds to the mathematical function library already existing in C. Libraries originating from the C language (cLibName).
- int main()
-
- Corresponds to the entry point, or "starting point", of the executable, i.e., the function that will be called automatically when you run the executable.
- Every C++ program must necessarily contain one (and only one) function called main.
- std::cout << "Hello World" << std::endl;
-
- std :: cout << standard syntax to display text on the command line (here "Hello World")
-
- - cout stands for Common Output. (refers by default to the command line unless standard output has been redirected)
- - std::cout is the full name of the cout object which is present in the namespace std (standard library).
- - :: is called the scope resolution operator. Here it allows finding the cout object in the std namespace.
- - << is a C++ operator (i.e., a symbol used as a function). Here the << operator is used to send the character string (string) to the std::cout object in order to display it on the command line.
- <<std::endl Adds a line break at the end of the output. endl refers to end of line.
- return 0; Return value of the executable (sent back to the calling process - for example the command line). By convention, a return value of 0 indicates that the program terminated without errors.
- - Every C++ code instruction must necessarily end with a semicolon ;
- - Unlike Python, but similar to Java, indentation and line breaks have no effect on code execution.
Functions
Functions follow the following syntaxreturnType functionName(type argumentName1, type argumentName2, etc.) { ... function code return value; }
int addition(int a, int b) { return a+b; }
- - A function that does not return a value will have void as its returnType.
- - A function that takes no arguments will simply have empty parentheses.
- - The first line describing the name and types of the function is called the signature or header of the function.
- - The rest is called the body or implementation of the function.
int addition(int a, int b) { return a+b; } int main() { // OK // The signature (and body) // is declared before. int c = addition(5,3); }
int addition(int a, int b); int main() { // OK // The signature is declared before. int c = addition(5,3); } int addition(int a, int b) { return a+b; }
int main() { // KO - does not compile, // the function "addition" // is not declared. int c = addition(5,3); } int addition(int a, int b) { return a+b; }
float norm(float x, float y, float z)
// The square of a value float x2 = x*x; // The square root of a value float y = std::sqrt(x); // An arbitrary power p of a value x (power not necessarily integer, more generic but more costly function than the two above). float y = std::pow(x, p); // Warning: Do not use the ^ or ** symbols which are not power operations in C++.
Fundamental C++ Types (Reminders)
You will mainly use two fundamental types in your code:- int: corresponds to an integer number. On our machines, an int is encoded on 4 bytes.
-
- e.g. int = 325;
- float: corresponds to a floating-point number called "single precision". On our machines, a float is also encoded on 4 bytes.
-
- e.g. float = 3.2f;
- bool: Boolean value that can take the value true/false.
-
- This type is introduced in C++ (does not exist in C), and allows more expressive code than using an integer.
- double: A floating-point number called "double precision". On our machines, a double is encoded on 8 bytes.
-
- e.g. double = 3.2;
- This type is the one used by default (floating-point number without extension), but in our case we will mostly use "float" which is compatible with the graphics card.
- char: A character whose value/character correspondence is given by the ASCII table.
- 1) Be careful when dividing two integers: you get the quotient of the Euclidean division as an integer and not a floating-point number.
int a = 5/2; // equals 2 (Euclidean division) int b = 5%2; // equals 1 (remainder of Euclidean division) // If you want to get 2.5, at least one of the two arguments must be a float float c = 5/2.0f; // OK - 2.5 float d = 5.0f/2; // OK - 2.5 float e = float(5)/2; // OK - 2.5 // ... etc
- 2) When you don't want to/don't know the type, it is possible to use auto. The compiler will then complete the type automatically.
auto a = 5; // a is an int auto b = 8.4f; // b is a float auto c = 4.2; // c is a double
-
- To simplify readability and understanding of your code, avoid using auto for simple types. This keyword is mainly practical for generic functions or to avoid having to write overly long types.
- 3) In C++, fundamental types are not initialized to specific values by default.
-
- e.g. "int a;" will not necessarily equal 0 if it is not explicitly defined with "int a=0;".
- \(\Rightarrow\) To avoid "undefined behavior", it is therefore preferable to initialize these values at the time of their declaration.
Arrays/Vectors
The C++ standard library (called STL) defines generic types of vectors/arrays of values stored contiguously in memory.- std::vector<T> is an array of values of type "T" (T must be specified at compilation) whose size can be modified during program execution. This is called a "dynamic" array.
- std::array<T, N> is an array of values of type "T" and of integer size "N". N must be known at compilation and cannot be modified later. This is called a "static" array.
- The values of a std::array are stored in so-called stack memory which is more efficient, but limited in size (typically 2MB). The values of a std::vector are stored in so-called heap memory which can be slightly more costly during allocation and intensive use, but which gives access to your entire RAM.
#include <iostream> #include <cmath> // Add inclusion for std::vector and std::array #include <vector> #include <array> // this one is optional here int main() { // Create an empty vector containing integers std::vector<int> vec; // the vector is empty at this state, its size is 0 // Can add elements one after the other (the vector is resized automatically) vec.push_back(5); vec.push_back(6); vec.push_back(2); // Check the size of the vector std::cout << "Vector has " << vec.size() << " elements" << std::endl; // Elements of the vector can be accessed via // vec[0], ..., vec[k], ..., vec[vec.size()-1]. std::cout <<"first element: " << vec[0] << std::endl; // You can write over an indexed element vec[1] = 12; // now the second element will be 12 // We can loop over all elements of a vector for (int k = 0; k < vec.size(); ++k) { std::cout << "Element " << k << " : " << vec[k] << std::endl; } // You should not access/write over an element beyond the size of the vector // Try to uncomment these two lines and see the result (the program may crash, but this is actually un undefined behavior that depends on your system). // vec[8568] = 12; // std::cout<<vec[8568]<<std::endl; // You can resize (or allocate an initial size) for the vector using .resize(N) vec.resize(10000); // previously existing elements (the 3 first values) are kept, and the new elements are initialized to 0 std::cout << "New vector size: " << vec.size() << std::endl; // Now it is legit to access/write on element vec[8568] vec[8568] = 12; std::cout << "Element 8568 : " << vec[8568] << std::endl; return 0; }
std::vector<float> generate_vector(int N);
int main() { std::vector<float> vec = generate_vector(101); std::cout<<vec[0]<<std::endl; // should be 0 std::cout<<vec[100]<<std::endl; // should be 1 std::cout<<vec[50]<<std::endl; // should be 0.5 std::cout<<vec[25]<<std::endl; // should be 0.25 return 0; }
float norm(std::vector<float> vec); // or // float norm(const std::vector<float>& vec);
Note that this is function overloading: two functions share the same name
norm, but with different signatures (types and/or number of parameters). The compiler automatically chooses the correct version to call based on the arguments passed.
The first signature
norm(std::vector<float> vec) passes the vector by copy: the entire content of the vector is duplicated in memory at each call. The second signature norm(const std::vector<float>& vec) passes the vector by constant reference: no copy is made, only a reference to the original vector is transmitted. The const keyword guarantees that the function will not modify the content of the vector. Passing by constant reference is preferred for large objects (vectors, strings, etc.) as it avoids a costly copy in memory and time.
Export ASCII
Code: 01_first_program/b_ascii_tree
We now place ourselves in the context of the directory 01_first_program/b_ascii_tree. Open the code in this directory and compile it following the same steps as above.
> In the main.cpp file, complete the function void print_tree(int levels) to display on the command line an ASCII tree whose height is given as a parameter.
The tree should resemble the example below:
/* * Example for `levels = 4` , `levels = 5` : * * * * * *** *** * ***** ***** * ******* ******* * ||| ********* * ||| */
- - Each line contains a certain number of spaces followed by asterisks.
- - The number of stars increases by 2 at each level.
- - The maximum width corresponds to the last foliage line.
- - The trunk can be coded separately after the main loop.
std::string. Note that one can concatenate std::string with the operator + and +=, or append at the end of the string with the function [string].append([to_add]).
The display in the main function will then be performed with the following call:
int main() { std::cout<< print_tree(4) <<std::endl; return 0; }
// filename: name of the output file // level: height of the tree void export_tree_file(const std::string& filename, int level);
ASCII Histogram
Code: 01_first_program/c_ascii_histogram
In this exercise, we want to count the occurrences of each word in a text file, then display the result as an ASCII histogram. The file src/input.txt contains a text that will serve as input.
std::map
To associate each word with its number of occurrences, we use the std::map<Key, Value> container from the standard library (requires#include <map>). A std::map is a dictionary that associates a key with a value (similar to a Python dictionary).
#include <map> #include <string> std::map<std::string, int> histogram; // Insertion / update of an entry histogram["hello"] = 1; // Increment: if the key does not yet exist, // it is automatically created with value 0, then incremented. histogram["hello"]++; // now equals 2 histogram["world"]++; // created with value 1 // Iterating over the map (keys are sorted alphabetically) for (const auto& it : histogram) { std::cout << it.first << " : " << it.second << std::endl; // it.first = the key (std::string) // it.second = the value (int) }
Reading a text file
To read a file, we usestd::ifstream (requires #include <fstream>). The >> operator allows reading words one by one (automatically separated by spaces and line breaks).
#include <fstream> #include <string> std::ifstream file("src/input.txt"); // Check if the file was opened successfully if (!file) { std::cerr << "Error: unable to open the file" << std::endl; } // Read word by word std::string word; while (file >> word) { std::cout << word << std::endl; // displays each word }
Exercises
> Implement the functionbuild_word_histogram that reads a text file and builds a std::map counting the occurrences of each word.
The function signature is:
std::map<std::string, int> build_word_histogram(const std::string& filename);
-
- Create an empty
std::map<std::string, int>. -
- Open the file with
std::ifstream. -
- Read words one by one with
while (file >> word). -
- For each word read, increment its counter in the map:
histogram[word]++. - - Return the map.
print_histogram that displays the content of the map as a horizontal ASCII histogram.
The function signature is:
void print_histogram(const std::map<std::string, int>& hist);
| character, then as many asterisks as the number of occurrences.
Example output (excerpt):
a | ************ and | ********* children | ****** the | ******************
-
- Iterate over the map with a loop:
for (const auto& it : hist) { ... } -
- Access the key with
it.firstand the value withit.second. - - Use a nested loop to display the asterisks.
PPM Image Export
Code: 01_first_program/d_ppm_image
In this part, we want to export a simple image in the PPM (Portable PixMap) format. It is an extremely minimalist image format, convenient for beginners because it requires no external libraries.
We will use the ASCII variant of the format, called P3 (the pixel values are written as numbers in a text file).
Image structure in memory
We use the following structure:- - Nx, Ny : width and height (in pixels)
-
- data : contiguous array of floats of size
3*Nx*Ny -
- Each pixel stores 3 components (R,G,B) in this order.
- Values are assumed to be in [0,1].
-
The index of pixel
(x,y)is:offset = 3 * (y*Nx + x)
set_pixel(x,y, r,g,b) writes the components at the correct position in the array.
Format PPM (P3)
A PPM ASCII file follows this format:P3 <Nx> <Ny> 255 r g b r g b r g b ... ...
-
- The first line
P3indicates PPM ASCII. - - The second line indicates the dimensions.
-
- The third line
255indicates the maximum value of a component. - - Then we write all the pixels, as integers between 0 and 255, separated by spaces (line breaks are free, but they are often placed at the end of a pixel row for readability).
Conversion float -> int
Our pixels are stored as floats between 0 and 1, while the PPM file requires integers between 0 and 255. We can use a conversion such as:-
- clamp:
x = max(0, min(1, x)) -
- scale:
x*255 -
- rounding:
int(x*255 + 0.5)
image_structure image in PPM ASCII (P3) format.
Signature:
void export_ppm(const image_structure &img, const std::string &filename);
-
- The file must be opened for writing with
std::ofstream. - - Write the PPM header (P3, dimensions, 255).
- - Then write the pixels as integers.
-
- Iterate over the image line by line (for example
yfromNy-1to0, thenxfrom0toNx-1). -
- End each
yline with a line break.
// Creating a file std::ofstream out(filename); // Writing out << "some text" << "\n";
Quick tests
Onceexport_ppm is implemented, you can create small images (e.g. 32x32) and visually verify the result.
> Do a first test:
- - White image (values initialized to 1)
- - A red pixel at (10,10)
output.ppm should be openable by some image software (or easily converted).
Note. If your viewer does not read PPM directly, you can use tools such as GIMP.
Generating simple procedural images
> Create a function that generates a horizontal black → white gradient:-
-
r=g=b = x/(Nx-1) -
- For all
y, for allx
-
- Choose a tile size
s(e.g.s=4) -
- A black tile if
(x/s + y/s) % 2 == 0, white otherwise
-
- For each pixel, choose
r,g,brandomly between 0 and 1 - - We can use the following syntax to generate a random float between 0 and 1:
float rand_float = static_cast <float> (rand()) / static_cast <float> (RAND_MAX);
-
- Center
(cx, cy) = (Nx/2, Ny/2) -
- Radius
R -
- If
(x-cx)^2 + (y-cy)^2 < R^2then red pixel, else white.