CPP Notes
Initialization
It is recommended to initialize value upon creation with a values. We can choose to add specific value, or default zero initialization. Multiple values can be initialized at the same time
// default initialization without value
int width
int x, y;
Copy-Initialization
Initialization inherited from C language. It copies the value to the variable. Not commonly used because it is less efficient form of initialization. It is best practice to initialize a single variable on a line.
int width = 5; // copy-initialization
Direct-initialization
Introduced for more efficient initialization of complex objects. It was superseded by direct-list-initialization.
int d { 7 }; // direct-list-initialization (initial value in braces)
Direct List Initialization
Modern way to initialize objects in C++. One of main benefits of this initialization is due to narrowing conversions.
If you try to list-initialize a variable using a value that the variable can not safely hold, compiler will raise an error. Narrowing convention only applies to the list initialization, any subsequent assignment is allowed.
int width { 5 }; // direct-list-initialization of initial value 5 into variable width (preferred)
int height = { 6 }; // copy-list-initialization of initial value 6 into variable height (rarely used)
int w1 { 4.5 }; // compile error: list-init does not allow narrowin conversion - fraction can't be saved in int
w1 = 4.5; // assignemnt is allowed
int width {}; // value-initialization / zero-initialization to value 0
int x ( 5 ), y (6)
IOSTREAM
iostream is used by importing it as a library
#include <iostream>
std::count
Character output prints characters to console as standard output. Characters are not directly written to standard output, but they are buffered and flushed periodically.
Newline
There are 2 common ways to move cursor to next line.
std::endl- adds a new line character and flushes the buffer. Useful when we want to explicitly flush the buffer."\n"- adds a new line character without flushing the buffer. More efficient thanstd::endl. It is preferred to use"and helps avoid inadvertent multicharacter literals.
std::cin
std::cin (which stands for “character input”) reads input from keyboard. Typically used to input data from console to variable
std::cout << "Hello " << "World" << std::endl; // print Hello world! to console
std::cout << "Enter your age:"
int x {};
std::cin >> x;
Functions
Simple example of using functions.
#include <iostream>
int getUserInput() {
std::cout << "Enter an integer: ";
int input;
std::cin >> input;
return input;
}
int main() {
std::cout << "Wellcome to first program" << "\n";
const int x { getUserInput()};
const int y { getUserInput() };
std::cout << x << " * " << y << " = " << x * y << "\n";
return EXIT_SUCCESS;
}
Value Parameters
Value parameters are passed to a function by value. They are copied into the matching parameter.
#include <iostream>
void printValues(int x, int y)
{
std::cout << x << '\n';
std::cout << y << '\n';
}
Preprocessor
Preprocessor accepts code files and produces a translation unit. Translation unit is prepared code that is the input to the compiler. Some tasks done by the preprocessor are:
- Strips out comments
- Ensures each file ends with a new line
- Processes #include
Preprocessor directive
Also called a directive, it is an instruction that start # and ends with a new line character (/n) instead of semicolon ;. These directives tell the preprocessor to perform certain text manipulation tasks.
#include
The #include directive is replaced by the preprocessor with the content of included file.
#define
#define creates a macro that defines how input text is converted into replacement text.
#include <iostream>
#define COLOR "green"
int main()
{
std::cout << "My name is: " << MY_NAME << '\n';
return 0;
}
Header Files
Typically header files are used to propagate one or more several declaration into a code file. They should follow name of the cpp implementation. Following ODR (One Definition Rule), no implementation should be defined in the header file.
// mathutil.h
int multiply(int x, int y);
// mathutil.cpp
#include "mathutil.h"
int multiply(int x, int y) {
return x * y;
}
//main.cpp
#include <iostream>
#include "mathutil.h"
int main() {
int x { 2 }
int y { 4 }
std::cout << x << " * " << y << " = " << multiply(x, y) << "\n";
return 0;
}
Header files can contain another header files.
// file: foo.h
#include <string_view> // required to use std::string_view
std::string_view getApplicationName(); // std::string_view used here
Header File Best Practices
- Header guards should be always included
- No variables are defined
- Has the same name as its associated c++ file
- Contains only declaration of function contained in its associated cpp file
- Includes all header files for functionality it needs
- Includes only what it requires
- No .cpp files should be included
- Includes documentation on what something does and how to use the header. Describe how the code works in source files.
Constants
Constant is a named variable that can not be changed. There is no specific naming convention.
Best Practice
- Variable should be constant wherever possible
- Values for parameters should not be constants
const int size { 10 }
Literals
Meaning of literal values can not be redefined. For example, value 3 can never be different.
Literal values have their type. It is typically deduced from the value. Sometimes we need to change the default type assigned to the literal. This can be done with Suffixes.
std::cout << 5u << '\n'; // 5u is type unsigned int
C Strings
C Strings are string literals placed between two double (")quotes. We use single quotes (') for char literals. Strings are not fundamental type in C++. They are inhereted from C language and are called C strings.
- Each string terminates with a special character, null terminator
\0. - Strings are constant types created at the beginning
Control Flow
if
if (condition) {
true_statement;
}
else {
false_statement;
}
Single line statements can avoid parenthesis.
if (age >= minDrinkingAge) purchaseBeer();
else std::cout << "No drinky for you\n".
switch
Switch expression must evaluate to integral type.
void printDigitName(int x)
{
switch (x)
{
case 1:
std::cout << "One";
break;
case 2:
std::cout << "Two";
break;
case 3:
std::cout << "Three";
break;
default:
std::cout << "Unknown";
break;
}
}
while
While uses loop variable to identify terminating condition. It always should be a signed as unsigned integers can lead to unpredictable results.
int count{ 1 };
while (count <= 10)
{
std::cout << count << ' ';
++count;
}
std::cout << "done!\n";
Sometime we use infinite loops intentionally.
while (true)
{
// this loop will execute forever
}
do while
do
statement; // can be a single statement or a compound statement
while (condition);
for
Statement for is prefered to while when there is obvious loop variable.
for (init-statement; condition; end-expression)
statement;
It is possible to emit any or all for statements.
int i{ 0 };
for ( ; i < 10; ) // no init-statement or end-expression
{
std::cout << i << ' ';
++i;
}
Compound Data Types
C++ supports the following compound types:
- Functions
- C-style Arrays
- Pointer types:
- Pointer to object
- Pointer to function
- Pointer to member types:
- Pointer to data member
- Pointer to member function
- Reference types:
- L-value references
- R-value references Enumerated types:
- Unscoped enumerations
- Scoped enumerations Class types:
- Structs
- Classes
- Unions
lvalue
Expression that evaluates to an identifiable object or function (or bit-field). Entities with identities can be accessed via an identifier, reference, or pointer, and typically have a lifetime longer than a single expression or statement. They can be modifiable or non-modifiable (aka constants)
rvalue
It is an expression that is not an lvalue. Rvalue expressions evaluate to a value. For example, we can break following statement int x = 10; into, x - lvalue and 10 - rvalue.
lvalue vs rvalue
Following are rules of thumb to distinguish lvalues from rvalues:
- lvalue (locator value): Has a identifiable memory address. If you can take its address using the ampersand (&) operator, it’s an lvalue. It usually has a name and persists beyond a single expression.
- Rvalue expressions are those that evaluate to values, including literals and temporary objects that do not persist beyond the end of the expression.
int return5()
{
return 5;
}
int main()
{
int x{ 5 }; // 5 is an rvalue expression
const double d{ 1.2 }; // 1.2 is an rvalue expression
int y { x }; // x is a modifiable lvalue expression
const double e { d }; // d is a non-modifiable lvalue expression
int z { return5() }; // return5() is an rvalue expression (since the result is returned by value)
int w { x + 1 }; // x + 1 is an rvalue expression
int q { static_cast<int>(d) }; // the result of static casting d to an int is an rvalue expression
return 0;
}
Lvalue References
Also called as a references act as an reference for existing values such as variables. The type of a reference determine what type of object it can reference. Lvalue reference type is identified by a ampersand & in the type specifier for example int&.
We can create a variables holding lvalue references. We can use reference to modify object that is being referenced.
#include <iostream>
int main()
{
int x { 5 }; // normal integer variable
int& ref { x }; // ref is now an alias for variable x
std::cout << x << ref << '\n'; // print 55
x = 6; // x now has value 6
std::cout << x << ref << '\n'; // prints 66
ref = 7; // the object being referenced (x) now has value 7
std::cout << x << ref << '\n'; // prints 77
return 0;
}
Lifetime of a lvalue and its reference is independent. It can lead to a dangling reference when a object being reffered to is dead while its reference is still alive. This can lead to undefined behavior.
References are not objects and they don’t occupy memory. Wherever possible, they are replaced by compiler with the referenced value.
Passing an argument to a function can be done by:
- copy a value - copying the value
- pass a value - passing a reference to the method instead of the value
Pass by Reference
Primitive types are often as small as few bytes. For those it is more efficient to pass the value instead of passing their reference. This is not the case for more complex types such as strings. We want to limit unnecessary creating copies of classes wherever possible.
Passing by reference is defined by declaring a reference rather than a value type parameter. When a value is passed to the function it is bound to the reference instead to the value.
#include <iostream>
void printMe(int& xRef, int x) {
std::cout << &xRef << " - " << &x << "\n";
}
void addOne(int& x) {
++x; // modifies object
}
int main() {
int x { 5 };
std::cout << &x << "\n";
printMe(x, x);
addOne(x);
std::cout << x << "\n";
}
// Output:
// 0x16b66f56c
// 0x16b66f56c - 0x16b66f544
// 6
In above example you can see that the x in the main function has the same address as the xRef in printMe function.
Because x in printMe function was copied it has different address.
Pass by reference enable us to modify the underlying value. Because the reference enables us to mutate the value, we can not pass references to constants. This linmits our use of references.
Pointers
We use & to get address to value - once we have an address, we can use a dereference operator * to access to access the value at given address.
#include <iostream>
int main() {
int x { 5 };
int* ref { &x };
std::cout << "x=" << x << "\nmemory_address=" << ref << "\nref_val=" << *ref << "\n";
return 0;
}
A pointer is an object that holds a memory address as its value.
Similar to references, we use a pointer type by adding a start as a postfix to the value type, for example int* pointer holds address to int value.
A pointer is a variable, hence it is not initialized by default. Uninitialized pointer is called a wild pointer, containing a garbage address. It is a best practice to always initialize a pointer to a known value.
Pointer assignment can:
- Change what it is pointing at
- By assigning a new address to the pointer
- Change the value being pointed at
- By assigning a dereferenced pointer a new value
#include <iostream>
int main() {
int x { 5 };
int* ref { &x };
std::cout << "x=" << x << "\nmemory_address=" << ref << "\nref_val=" << *ref << "\n";
*ref = 6;
std::cout << "x=" << x << "\nmemory_address=" << ref << "\nref_val=" << *ref << "\n";
int y { 7 };
ref = &y;
std::cout << "x=" << x << "\nmemory_address=" << ref << "\nref_val=" << *ref << "\n";
}
References and Pointers
Pointers behave similarly to referfences. They store address to a value and can be dereferenced to the value. They are not the same and have following differences:
| Pointer | Reference | |
|---|---|---|
| Holds memory address | explicitly | internally |
| Indirection is managed | manually (*, &) |
by compiler |
| Can be null | Yes | No |
| can be reseated | Yes | No |
| Syntax overhead | *ptr, ptr-> |
No |
| Is a class | Yes | No |
It is preffered to use lvalue references instead of pointers. Here is a decision table to help with choice:
- Is null a valid state? → Pointer
- Can the target change? → Pointer
- Interfacing with C? → Pointer
- Otherwise → Reference
Pass by Reference and Address
When dealing with objects larger than few bytes, it is more efficient to pass them by reference, or address.
#include <iostream>
#include <string>
void printByValue(std::string val) // The function parameter is a copy of str
{
std::cout << val << '\n'; // print the value via the copy
}
void printByReference(const std::string& ref) // The function parameter is a reference that binds to str
{
std::cout << ref << '\n'; // print the value via the reference
}
void printByAddress(const std::string* ptr) // The function parameter is a reference that binds to str
{
std::cout << *ptr << '\n'; // print the value via the reference
}
int main()
{
std::string str{ "Hello, world!" };
printByValue(str); // pass str by value, makes a copy of str
printByReference(str); // pass str by reference, does not make a copy of str
printByAddress(&str);
return 0;
}
References are idiomatic to C++, hence we should use pass by reference unless there is a specific reason to pass an address. Some of the benefits of pass by reference are:
- The argument always exists - we don’t need to check nullability
- The syntax is cleaner - the caller can pass variable normally
We use pass by addresss when:
- We want to communicate that the argument can be null
- We need to reseat the pointer - point the pointer to another object, e.g. in linked list
The modern C++ guideline (C++ Core Guidelines: F.16/F.60) sums it up well: use a reference when the parameter must exist; use a pointer when it’s optional.