2/9
2. Basic language elements
Fundamental types
C++ is a strongly static typed language. It means that all variables must necessarily have a type, and the type must be known by the compiler at the moment of the compilation (at the opposite of dynamic typed languages such as Python, JavaScript, etc, where types are deduced when the program runs).
C++ handle a set of fundamental types from which any further object and data structure are built. Fundamental C++ types are compatible with C types at the exception of boolean value only available in C++ .
C++ (as well as C) does not standardize the memory size in bytes of fundamental types [1]. It means that fundamental types may be encoded on more or less bytes depending on the architecture and system.
In practice, in our case, on standard computer and OS with x86 processor, fundamental types are encoded under the same common memory size (you can use the command sizeof to check the memory usage of fundamental types: test code).
Value types
For complete information on C++ fundamental types, please refers to the C++ documentation.
In our case, we will mostly encounter the following types
-
int
-
Signed integer encoded on 4 bytes (encodes integer between -2 147 483 648 to 2 147 483 647).
-
example:
int x = -5;
-
-
float
-
Single precision floating point number encoded on 4 bytes (norm IEEE 754).
-
Keep in mind that floating point number has limited precision. (ex. only encodes integer values exactly up to 16 777 216).
-
example:
float x = 4.5f;
-
-
bool
-
Boolean value true or false. [2]
-
example:
bool x = true;
-
-
unsigned int
-
Positive integer encoded on 4 bytes (encodes integer between 0 to 4 294 967 295).
-
example:
unsigned int x = 5u;
-
-
char and unsigned char
-
Used to encodes both ASCII character, or a number on one byte (encodes number between -128 to 127 for char, and 0 to 256 for unsigned char).
-
example:
char x = 'k';
-
You may further meet other common type such as
-
double: Double precision floating point number encoded on 8 bytes. Default type, when using litteral number notation
double x = 4.5;. -
short and unsigned short: Storage for short size integer on 2 bytes.
-
long int and unsigned long: Storage for long size integer on 8 bytes.
Casts
C++ is able to convert explicitly, or implicitly in some case, types between fundamental type. This is called static cast. There exists several ways to perform casting between type.
// declaration of double value
double a = 4.8;
//implicit cast
int b = a;
//explicit C++ static_cast (safest use for variable casting)
int c = static_cast<int>(a);
//C-type cast
int d = (int)a;
//C++-type constructor
int e = int(a);
Pointers
In addition of types designed to store user defined values, pointers types are used to store addresses and manipulate content of variables. For instance, int* stores the address of a variable of type int, and similarly for every type float*, char*, etc.
The type of the pointed variable is only necessary to handle the variable, but not to store the address. The generic type void* allows to store an address, where the type of the pointed variable can be anything (has to be casted before using the pointed value).
Note that pointers are central elements of C language, which may lead to memory access error when used incorrectly. C++, and especially modern approaches, are able to limit the explicit use of pointers for safer program and debug ease.
Helper types
As fundamental types may have varying size in memory, extra helper types may be defined and used in various library to ensure specific properties (minimal size in memory, etc).
The standard library for instance defines the type std::size_t as being an unsigned integer number able to index any container from the library. (In common architecture, std::size_t will be similar to unsigned long int.) Therefore indexing vectors and arrays from the standard library will use this type.
Initialization
Fundamental types declared within a local scope are not implicitly initialized. It means that if a value is not given to a declared variable, its value is indeterminated (value found at the memory location of the variable that may varies unexpectidily depending on the usage of the system).
⇒ To ease debugging, it is of good practice to never allow indeterminated values in your program. Always set an initial value to your variables, even if changed afterward.
Scope and variable lifetime
Values of C++ variables are, by default, allocated on the stack memory (fast creation, access, destruction, and automatic management).
Variables have block scope, where a block is defined by the curly brackets { }.
It means that variables are automatically erased from the memory when the curly bracket closing the block } in which it has been declared is met.
int main()
{
{
// a will exists in the current block
int a = 5;
std::cout << a << std::endl; // current block - a can be used
{
std::cout << a << std::endl; // child block - a can still be used
}
}// end of block where a was defined
// a no longer exists in this part of the program
// uncommenting the next line would results in error
// std::cout << a << std::endl;
return 0;
}
⇒ Good practices suggest that you should declare your variables in the closest possible scope for readability and maintainability purpose.
Further notes
-
Two variables with same name cannot be defined within the same block
-
A variable can be defined with the same name as a variable declared in a parent block. The most local variable will be used. This behavior is however to be avoided and corresponds to bad practice.
Constness
Variables are mutable by default, i.e. the value of their content can be modified after their declaration. When programming complex code and avoid unintentional errors due to unexpected value change, it is often preferable to keep as much as possible constant value. [3]
-
C++ * values declared as const have values that cannot be changed (checked by the compiler). The keyword const can be place before or after the type.
Example
const int a = 5; // const integer
int const b = 5; // same as previous line
// uncommenting the following lines would result in compiler error
// a = 6;
// b = 6;
⇒ As good practice behavior, always declare your variables as const by default. Don’t do it, only when you aim at modifying their values later one.
C++ statements
Equality tests
Equality are tested using the == syntax. (Ex. a==b).
For compatibility with C, equality returns 1 when the test is true, 0 otherwise.
Non-equality is tested with != syntax.
Conditionals
If statements can be expressed using the following syntax
if( condition == value ) {
... // some action
}
else if ( other_condition == other_value) {
... // some other action
}
else {
... // final action
}
Note that the equality test is optional in the if statement. By default, the condition is supposed to be valid if it is not equal to zero.
Brackets are optional if the action consists of a single statement.
Loops
// standard for loop
for( initialization; continue loop test; increment ) {
... // some action
}
// range for loop
for ( Type element : container ) {
... // some action
}
// while loop
while( continue loop test ) {
... // some action
}
// do while loop
do {
... //some action
} while( continue loop test );
Functions
General syntax of functions
TypeReturned functionName( TypeArg1 argument1, TypeArg2 argument2, ...)
{
... // some action
return someValue;
}
-
Function may not return any value, in this case their TypeReturned is void.
-
C++ (and not C) allows function overload, i.e. Function with the same name, but different arguments.
-
C++ (and not C) allows to overload operators such as +, -, *, etc. which is useful to follows math looking operations.
-
Functions may be fully declared after their use in code, but their signature (the first line) must be defined before.
-
Variables of functions are passed by copy, i.e. the content of the original variable is copied in the content of the variable received by the function.
Example of function declaration with overloading.
#include <iostream>
// Signature of functions declared before their use
// These two functions have the same name but different parameters
int maximum(int a, int b);
int maximum(int a, int b, int c);
int main()
{
int a = 8;
int b = 9;
int c = 10;
int max = maximum(a,b,c);
std::cout << max << std::endl;
return 0;
}
// Complete declaration of the function body
int maximum(int a, int b)
{
if ( a>b )
return a;
else
return b;
}
int maximum(int a, int b, int c)
{
// can call the function with two parameters
int max_ab = maximum(a,b);
int max_abc = maximum(max_ab,c);
return max_abc;
}
Example of function declaration with operator overloading.
#include <string>
#include <iostream>
std::string operator*(std::string s, int number_of_repetition);
int main()
{
std::string a = "Hello ";
std::string b = a*5;
std::cout<< a << "* " << 5 <<" = "<<b <<std::endl;
return 0;
}
std::string operator*(std::string s, int number_of_repetition)
{
std::string concatenated;
for(int k=0; k<number_of_repetition; ++k)
concatenated += s;
return concatenated;
}
This function allows the multiplication between a string and an integer \(n\) to result in the duplication of \(n\) identical string.
This program prints on the common output Hello * 5 = Hello Hello Hello Hello Hello.
Note that this example is shown for illustration purpose. It is generally bad practice to change the behavior of standard library type.
Address of variables
Content of variables can be accessed and manipulated through their address by the use of pointers.
-
The address of a variable
xis obtained with the syntax&x. -
Deferencing a pointer p, i.e. retrieving the content at a given address, is obtained with the syntax *p.
#include <iostream>
int main()
{
int a = 5;
// declare a pointer on integer
int* pa;
// pa contains the adress of the variable a
pa = &a;
// modifies the content at the adress contained in pa
*pa = 12;
// a now contains 12
std::cout << a << std::endl;
return 0;
}
Note that the declaration int* pa; doesn’t assign value to pointer pa. This is considered as bad practice as its value at this point of the code is undefined.
⇒ Conventionally un-initialized pointers should be set to the value 0, or more precisely, to the pointer type nullptr. [4]
Example
int *pa = nullptr;
// another valid solution in this case was to define directly
// int *pa = &a;
Constness and pointers
Pointers can have two levels of constness depending on the placement of const with respect to the * indicating either that
-
the address contained in the pointer cannot be changed
// pa can only contains the address of a, and cannot be reassigned later.
int* const pa = &a;
-
the content pointed at this address cannot be changed through this pointer
// the value of a cannot be modified through pa
const int* pa = &a;
int const* pa = &a; //similar syntax
Note
-
It is possible to set a fully constant pointer with constant value with the syntax
const int* const pa = &a;
const int const* pa = &a; // similar syntax
-
It is possible to set a pointer with constant value to a non-constant variable. However, it is forbidden to set a pointer with non-constant value to a constant variable.
const int a = 5; // constant variable
int b = 5; // non constant variable
const int* p1 = &a; // Allowed - constant pointer to constant variable
// int* p2 = &a; // Forbidden - non-constant pointer to constant value
const int* p3 = &b; // Allowed - constant pointer to non-constant variable
int* p4 = &b; // Allowed - non constant pointer to non constant variable
References
References have been introduced in C++ . They allow to fit the behavior of an alias on a given variable.
A references r is declared using the syntax &r (take care not to confuse with the address symbol when not used in declaration).
#include <iostream>
int main()
{
int a = 5;
// reference to the variable a
int &ra = a;
// references are used like any variable
ra = 12;
// a now contains the integer value 12
std::cout << a << std::endl;
return 0;
}
Note that the behavior of reference may be closed than pointers in allowing to change the content of another variable, but possesses a simpler syntax. References, once declared, follows exactly the syntax of simple variables and don’t need deferencing.
As such, references can be viewed as a syntaxic sugar version of pointers used to access the content to other variable.
-
References must necessarily be assigned to a variable when declared. It is forbidden to declare un-initialized reference.
int &ra; // doesn't compile
-
Reference cannot be reassigned. They will only act as aliases on the variable they have been assigned during declaration.
int a = 5;
int b = 8;
// Assign reference r to a
int &r = a;
// Copy the value of b (8) in a
// (But does not assign r to the variable b, r remains an alias on a.)
r = b;
-
Assigning a reference from another one is similar to assigning the reference to the original variable
int a = 5;
int& r1 = a;
int& r2 = r1; // Similar to int& r2 = a;
These limitations also makes references generally less error-prone (and thus safer) to use than pointers.
Const references
References can be qualified as const, meaning that the value of the data cannot be modified through the use of the reference.
Similarly to pointers
-
const references can be alias on const and non const variables
-
non const references can only be alias on non const variables
const int a = 5; // constant variable
int b = 5; // non constant variable
const int& r1 = a; // Allowed - constant reference to constant variable
// int& r2 = a; // Forbidden - non-constant reference to constant value
const int& r3 = b; // Allowed - constant reference to non-constant variable
int& r4 = b; // Allowed - non-constant reference to non-constant variable
-
const references also have the advantage of being able to refer also to temporary variables.
// Allowed - constant reference to temporary variable
const int &r1 = 5;
// Forbidden - the value 5 is in a temporary variable (called rvalue) and cannot be taken as non const reference
// int &r = 5;
Passing argument to functions
C++ pass variables as arguments of functions by copy. It means that every argument received by a function is a copy of the value contained by the original variable.
Symbolically, you can consider functionArgument = originalVariable;
In practice, it means that
-
changing the value of the variable in the function doesn’t change the value of the original variable.
-
passing variable containing a lot of data to a function may be costly as the content is copied (ex. passing a vector of multiple values).
There is two ways to still be able to change the content of the original value, as well as ensuring that argument passing remains computationally light.
-
Passing the variable by its address
-
The pointer variable received in the function contains a copy of the address of the original variable. Modifying the value at this address leads to the modification of the original variable.
-
Pointers are stored on 8 octets independently of the size of the pointed data, thus they are efficiently copied.
-
-
Passing the variable as reference
-
The argument received in the function is an alias on the original variable. Its value can thus be modified.
-
References are lightweight variables, commonly implemented internally using address of variable (while being transparent to the programmer).
-
#include <iostream>
void pass_a_copy(int a)
{
a = 12;
}
void pass_a_pointer(int* b)
{
*b = 12;
}
void pass_a_reference(int& c)
{
c = 12;
}
int main()
{
int a = 5;
pass_a_copy(a);
// a still equals 5
std::cout << a << std::endl;
int b = 5;
pass_a_pointer(&b);
// a equals 12
std::cout << b << std::endl;
int c = 5;
pass_a_reference(c);
// a equals 12
std::cout << c << std::endl;
return 0;
}
General rules to pass argument
As a reminder, good practice programming suggest that all variables should be, by default, non modifiable. Therefore, argument of functions should, by default, not allow to modify the original variable.
Moreover, arguments of functions should be passed as efficiently as possible, without requiring useless copy of data.
The following rules of argument passing can be followed in most situations.
Consider a variable x.
-
If you don’t want to modify x within the function (most case scenario)
-
If x is a fundamental type (or lightweight variable)
⇒ pass it as a direct copy.Type x -
Otherwise (large variable in memory)
⇒ pass it as const reference.const Type& x
-
-
If you want to modify x within the function
⇒ Pass it by non const reference. [5]Type& x
#include <iostream>
#include <vector>
void no_modification(int x);
void no_modification(const std::vector<int>& x);
void modification(int& x);
void modification(std::vector<int>& x);
int main()
{
// Example of lightweight variable
int x_lightweight = 5;
// Example of larger variable
// Vector of 10000 numbers
std::vector<int> x_heavy;
x_heavy.resize(10000);
// initialize all values to zero
for(size_t k=0; k<x_heavy.size(); ++k)
x_heavy[k] = 0;
no_modification(x_lightweight);
no_modification(x_heavy);
modification(x_lightweight);
modification(x_heavy);
return 0;
}
// Case 1
// - no modification of x
// - x is a lightweight variable (int)
// => pass by copy
void no_modification(int x)
{
std::cout << x << std::endl;
}
// Case 2
// - no modification of x
// - x is a large variable in memory
// => pass by const reference
void no_modification(const std::vector<int>& x)
{
int sum = 0;
for(size_t k=0; k<x.size(); ++k)
sum += x[k];
std::cout << sum << std::endl;
}
// Case 3 (I)
// - modification of x
// => pass by non-const reference
void modification(int& x)
{
x = 12;
}
// Case 3 (II)
// - modification of x
// => pass by non-const reference
void modification(std::vector<int>& x)
{
for(size_t k=0; k<x.size(); ++k)
x[k] = 12;
}