See plus plus :) .
11. Scope, duration, and linkage summary
A variable’s duration determines when it is created and destroyed. Variables with automatic duration: are created at the point of definition, and destroyed when the block they are part of is exited. This includes: - Local variables - Function parameters Variables with static duration: are created when the program begins and destroyed when the program ends. This includes: - Global variables - Static local variables Variables with dynamic duration: are created and destroyed by programmer request. This includes: - Dynamically allocated variables
extern static (or thread_local) storage duration and external linkage static static (or thread_local) storage duration and internal linkage thread_local thread storage duration mutable object allowed to be modified even if containing class is const auto automatic storage duration Deprecated in C++11 register automatic storage duration and hint to the compiler to place in a register Deprecated in C++17
- When you write an implementation file (.cpp, .cxx, etc) your compiler generates a translation unit. This is the source file from your implementation plus all the headers you #included in it. Internal linkage refers to everything only in scope of a translation unit. External linkage refers to things that exist beyond a particular translation unit. In other words, accessible through the whole program, which is the combination of all translation units (or object files).

11.1. Internal linkage
-
An identifier’s linkage determines whether a declaration of that same identifier in a different scope refers to the same entity (object, function, reference, etc…) or not.
-
An identifier with no linkage means another declaration of the same identifier refers to a unique entity. Entities whose identifiers have no linkage include: Local variables Program-defined type identifiers (such as enums and classes) declared inside a block
-
An identifier with
internal linkagemeans a declaration of the same identifier within the same translation unit refers to the same object or function. Entities whose identifiers have internal linkage include:- Static global variables (initialized or uninitialized)
- Static functions
- Const global variables
- Unnamed namespaces and anything defined within them
-
To make things to internal linkage, we can:
staticglobal variables/functionsconstandconstexprglobals ((and thus don’t need the static keyword – if it is used, it will be ignored)) ## C- unnamed namespace { … } (modern C++)
-
static<global_variable>: make the global variable to internal linkage -
static<local_variable>: changes its duration from automatic duration to static duration. And its initializer is only executed once. -
static<const/constexpr local_varialbe>: used to avoid expensive local object initialization each time a function is called because it inits once time. -
Internal linkage for const global variables can change to external with the keyword
extern. e.g.extern const PI = 3.14; -
e.g.
// a.cpp ============================================================
#include <iostream>
static int g_internal { 42 }; // internal linkage (only in a.cpp)
static void helper() { // internal function
std::cout << "Helper in a.cpp\n";
}
int main() {
std::cout << g_internal << '\n'; // OK
helper(); // OK
return 0;
}
// main.cpp ============================================================
extern int g_internal; // ERROR: g_internal not visible outside a.cpp
void helper(); // ERROR: helper not visible outside a.cpp
int main() {
// g_internal; // linker error if uncommented
// helper(); // linker error if uncommented
return 0;
}
11.2. External linkage
-
An identifier with
external linkagemeans a declaration of the same identifier within the entire program refers to the same object or function. Entities whose identifiers have external linkage include:- Non-static functions
- Non-const global variables (initialized or uninitialized)
- Extern const global variables
- Inline const global variables
- Namespaces
-
functions: default external linkage -
global variables:non-const globals: external by default.const/constexpr globals: internal by default
-
To access an external global variable from another file, use
externwithout initializer (forward declaration). -
e.g.
// a.cpp ============================================================
#include <iostream>
int g_external { 100 }; // external by default
extern const int g_limit { 200 }; // const made external
void sayHello() { // external by default
std::cout << "Hello from a.cpp\n";
}
// main.cpp ============================================================
#include <iostream>
extern int g_external; // forward declaration
extern const int g_limit; // forward declaration > < const int g_limit: definition
void sayHello(); // forward declaration
int main() {
sayHello();
std::cout << g_external << " / " << g_limit << '\n';
return 0;
}
11.3. Inline functions and variables
When a call to min() is encountered, the CPU must store the address of the current instruction it is executing (so it knows where to return to later) along with the values of various CPU registers (so they can be restored upon returning). Then parameters x and y must be instantiated and then initialized. Then the execution path has to jump to the code in the min() function. When the function ends, the program has to jump back to the location of the function call, and the return value has to be copied so it can be output. This has to be done for each function call. All of the extra work that must happen to setup, facilitate, and/or cleanup after some task (in this case, making a function call) is called overhead. #include
int min(int x, int y) { return (x < y) ? x : y; } int main() { std::cout « min(5, 6) « ‘\n’; std::cout « min(3, 2) « ‘\n’; return 0; } For functions that are large and/or perform complex tasks, the overhead of the function call is typically insignificant compared to the amount of time the function takes to run. However, for small functions (such as min() above), the overhead costs can be larger than the time needed to actually execute the function’s code! In cases where a small function is called often, using a function can result in a significant performance penalty over writing the same code in-place. However, inline expansion has its own potential cost: if the body of the function being expanded takes more instructions than the function call being replaced, then each inline expansion will cause the executable to grow larger.
inline-expansion:- is a process where a function call is replaced by the code from the called function’s definition.
- use this to avoid such overhead cost.
- do not use the
inlinekeyword to request inline expansion for your functions, because optimizing compilers.
modernly-inline:- should not implement functions (with external linkage) in header files because it lead to
multiple definitionserror. - so we can use
inline-function, it’s useful for header-only libraries
- should not implement functions (with external linkage) in header files because it lead to
11.4. Sharing global constants
-
global constants as internal variables:
- Advantages:
- Works prior to C++16.
- Can be used in constant expressions in any translation unit that includes them.
- Downsides:
- Changing anything in the header file requires recompiling files including the header.
- Each translation unit including the header gets its own copy of the variable.
- e.g.
// constants.h:============================================================
#ifndef CONSTANTS_H
#define CONSTANTS_H
// Define your own namespace to hold constants
namespace constants
{
// Global constants have internal linkage by default
constexpr double pi { 3.14159 };
constexpr double avogadro { 6.0221413e23 };
constexpr double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
// ... other related constants
}
#endif
// main.cpp::============================================================
#include "constants.h" // include a copy of each constant in this file
#include <iostream>
int main()
{
std::cout << "Enter a radius: ";
double radius{};
std::cin >> radius;
std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';
return 0;
}
-
global constants as external variables:
- Advantages:
- Works prior to C++16.
- Only one copy of each variable is required.
- Only requires recompilation of one file if the value of a constant changes.
- Downsides:
- Forward declarations and variable definitions are in separate files, and must be kept in sync.
- Variables not usable in constant expressions outside of the file in which they are defined.
- e.g.
// constants.h:============================================================
#ifndef CONSTANTS_H
#define CONSTANTS_H
namespace constants
{
// Since the actual variables are inside a namespace, the forward declarations need to be inside a namespace as well
// We can't forward declare variables as constexpr, but we can forward declare them as (runtime) const
extern const double pi;
extern const double avogadro;
extern const double myGravity;
}
#endif
// constants.cpp:============================================================
#include "constants.h"
namespace constants
{
// We use extern to ensure these have external linkage
extern constexpr double pi { 3.14159 };
extern constexpr double avogadro { 6.0221413e23 };
extern constexpr double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
}
// main.cpp::============================================================
#include "constants.h" // include all the forward declarations
#include <iostream>
int main()
{
std::cout << "Enter a radius: ";
double radius{};
std::cin >> radius;
std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';
return 0;
}
-
global constants as inline variables:
- If you need global constants and your compiler is C++17 capable, prefer defining inline constexpr global variables in a header file.
- Advantages:
- Can be used in constant expressions in any translation unit that includes them.
- Only one copy of each variable is required.
- Downsides:
- Only works in C++17 onward.
- Changing anything in the header file requires recompiling files including the header.
- e.g.
// constants.h:============================================================
#ifndef CONSTANTS_H
#define CONSTANTS_H
// define your own namespace to hold constants
namespace constants
{
inline constexpr double pi { 3.14159 }; // note: now inline constexpr
inline constexpr double avogadro { 6.0221413e23 };
inline constexpr double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
// ... other related constants
}
#endif
// main.cpp::============================================================
#include "constants.h"
#include <iostream>
int main()
{
std::cout << "Enter a radius: ";
double radius{};
std::cin >> radius;
std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';
return 0;
}
13. Error Detection and Handling
14. Type Conversion, Type Aliases, and Type Deduction
Type conversions
│
├── Implicit conversions (compiler tự làm)
│ ├── Numeric promotions ← an toàn, không mất dữ liệu
│ │ ├── char → int
│ │ └── float → double
│ │
│ └── Numeric conversions ← có thể mất dữ liệu
│ ├── Widening conversions ← mở rộng, thường an toàn (int → double)
│ └── Narrowing conversions ← thu hẹp, có thể mất dữ liệu (double → int, int → char)
│
└── Explicit conversions (do lập trình viên ép kiểu)
├── static_cast<int>(3.14)
├── reinterpret_cast
├── const_cast
└── (int)3.14 // C-style cast
14.1. Implicit type conversion
- Implicit type conversion is performed automatically by the compiler when an expression of some type is supplied in a context where some other type is expected.
- numeric promotion is the conversion of certain smaller numeric types to certain larger numeric types (typically int or double).no data loss.
floating point promotion: a value of type float can be converted to a value of type double.integral promotions:- signed char or signed short can be converted to int.
- unsigned char, char8_t, and unsigned short can be converted to int if int can hold the entire range of the type, or unsigned int otherwise.
- If char is signed by default, it follows the signed char conversion rules above. If it is unsigned by default, it follows the unsigned char conversion rules above.
- bool can be converted to int, with false becoming 0 and true becoming 1.
- numeric conversion is a type conversion between fundamental types that isn’t a numeric promotion. A narrowing conversion is a numeric conversion that may result in the loss of value or precision.
14.2. Explicit type conversion
- C++ supports 5 different types of casts:
static_cast,dynamic_cast,const_cast,reinterpret_cast, andC-stylecasts -
C-stylecasts
- Using operator
(<type>)value. and the value to convert to placed immediately to the right of the closing parenthesis). e.g.(int)7/3
-
static_castcasts
- Using `static_cast
(value) - provides compile-time type checking
- less powerful than a C-style cast
- direct-initialized vs list-initialization
| Purpose | Example | Explaination |
|---|---|---|
| Cast object → object | static_cast |
Creates a new Base object copied from Derived (object slicing). |
| Cast object → reference | static_cast<Base&>(derived) | No copy — just changes the view of the same object to treat it as a Base. |
| Cast pointer → pointer | static_cast<Base*>(&derived) | Upcasts a Derived* pointer to a Base* pointer. |
14.3. Typedefs and type aliases
type aliases: use theusingkeyworldusing <NewtypeAlias> = <type>
typedefs: is an old way of creating an alias for a type. Usingtypedefkeyworld.typedef <type> <NewtypeAlias>
- Prefer
type aliasovertypedef
using MyDouble = double;
typedef double MyDouble;
typedef int (*FcnType)(double, char); // FcnType hard to find
using FcnType = int(*)(double, char); // FcnType easier to find
14.5. Type deduction using auto keyworld
- Type deduction allows the compiler to deduce the type of an object from the object’s initializer.
- using
autokeyworld. - Type deduction must have something to deduce from
- Type deduction drops const from the deduced type
- … (more)
- Use type deduction for your variables when the type of the object doesn’t matter.
- Auto can also be used as a function return type to have the compiler infer the function’s return type from the function’s return statements, though this should be avoided for normal functions.
- The auto keyword can also be used to declare functions using a trailing return syntax, where the return type is specified after the rest of the function prototype.
- e.g.
int add(int x, int y)
{
return (x + y);
}
// Using the trailing return syntax, this could be equivalently written as:
auto add(int x, int y) -> int
{
return (x + y);
}
#include <type_traits> // for std::common_type
std::common_type_t<int, double> compare(int, double); // harder to read (where is the name of the function in this mess?)
auto compare(int, double) -> std::common_type_t<int, double>; // easier to read (we don't have to read the return type unless we care)
15. Function Overloading and Function Templates
15.1. Function overloading
function overloadingallows us to create multiple functions with the same name, so long as each identically named function has different parameter types/numbers. Return types are not considered forfunction deleteusingdeletekeywork. delete means “I forbid this”, not “this doesn’t exist”.- e.g.
#include <iostream>
// This function will take precedence for arguments of type int
void printInt(int x)
{
std::cout << x << '\n';
}
// This function template will take precedence for arguments of other types
// Since this function template is deleted, calls to it will halt compilation
template <typename T>
void printInt(T x) = delete;
int main()
{
printInt(97); // okay
printInt('a'); // compile error
printInt(true); // compile error
return 0;
}
default-arguments: is a default value provided for a function parameter. Parameters with default arguments must always be the rightmost parameters, and they are not used to differentiate functions when resolving overloaded functions.
15.2. Function Templates
- The template system was designed to simplify the process of creating functions (or classes) that are able to work with different data types (that are compiled and executed).
template typesare sometimes called generic types, and programming using templates is sometimes called generic programming.placeholder typesuse for any parameter types, return types, or types used in the function body that we want to be specified later, by the user of the template.template parameter declarationdefines any template parameters that will be subsequently used.function templatesallow us to create a function-like definition that serves as a pattern for creating related functions. In a function template, we use type template parameters as placeholders for any types we want to be specified later. The syntax that tells the compiler we’re defining a template and declares the template types is called a template parameter declaration.- Using function templates in multiple files. should be defined in a header file, and then #included wherever needed.
template argument deductionto have the compiler deduce the actual type that should be used from the argument types in the function call.- e.g.
template <typename T> // this is the template parameter declaration defining T as a type template parameter , `typename` or `class` can be used
T max(T x, T y) // this is the function template definition for max<T>
{
return (x < y) ? y : x;
}
template<>
int max<int>(int x, int y) // the generated function max<int>(int, int)
{
return (x < y) ? y : x;
}
template<>
double max<double>(double x, double y) // the generated function max<double>(double, double)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max<int>(1, 2) << '\n'; // calls max<int>(int, int)
std::cout << max<>(1, 2) << '\n'; // deduces max<int>(int, int) (non-template functions not considered)
std::cout << max(1, 2) << '\n'; // calls max(int, int)
return 0;
}
function templates with multiple templatetypes example:
#include <iostream>
template <typename T, typename U>
auto max(T x, U y) // ask compiler can figure out what the relevant return type is
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(2, 3.5) << '\n';
return 0;
}
non-type template parameteris a template parameter with a fixed type that serves as a placeholder for a constexpr value passed in as a template argument.
#include <iostream>
template <int N> // int non-type template parameter
void print()
{
std::cout << N << '\n';
}
int main()
{
print<5>(); // no conversion necessary
print<'c'>(); // 'c' converted to type int, prints 99
return 0;
}
16. Compound Types: References and Pointers
| Type | Meaning | Examples |
|---|---|---|
| Fundamental | A basic type built into the core C++ language | int, std::nullptr_t |
| Compound | A type defined in terms of other types | int&, double*, std::string, Fraction |
| User-defined | A class type or enumerated type (Includes those defined in the standard library or implementation) (In casual use, typically used to mean program-defined types) |
std::string, Fraction |
| Program-defined | A class type or enumerated type (Excludes those defined in standard library or implementation) |
— |
Compound data types(also called composite data type) are data types that can be constructed from fundamental data types (or other compound data types).
16.1. lvalues and rvalues
lvaluesis an expression that evaluates to an identifiable object or function (or bit-field). Can be accessed via an identifier, reference, or pointer, and typically have a lifetime longer than a singleexpressionorstatement.rvaluesis an expression that evaluate to a value. Only exist within the scope of the expression in which they are used.lvaluescan be used anywhere anrvalueis expected.- An assignment operation requires its
leftoperand to be a modifiablelvalueexpression. And itsrightoperand to be arvalueexpression.
16.2. References
-
references is an alias for an existing object/function.
referenceitself is like aconst pointer- Declared as
<type>& reference_name - Any operation on the reference is applied to the object being referenced.
- All references must be initialized.
- Cannot be reseated.
- They aren’t objects
- Can only accept modifiable lvalue arguments (const or non-const)
- Declared as
-
pass-by-reference allows us:
- to pass arguments to a function without making copies of those arguments each time the function is called. (class types)
- to change the value of an argument
-
pass-by-const-reference guaranteeing that the function can not change the value being referenced.
-
lvalue-reference just a reference for an existing lvalue.
-
lvalue-reference-types determines what type of object it can reference by using a single ampersand
<type>&. -
lvalue-reference-variable is a variable that acts as a reference to an lvalue.
-
lvalue-reference-to-const can bind with const or non-const objects.
const <type>& name
The const applies to what is immediately to its left, unless there’s nothing to its left, in which case it applies to what’s on the right.
- E.g.
#include <iostream>
#include <string>
// ---------------------------
// Example of references
// ---------------------------
void increment(int& x) { // pass-by-reference (modifiable lvalue reference)
x += 1; // changes original argument
}
void printConstRef(const std::string& s) { // pass-by-const-reference
// s cannot be modified here
std::cout << "Const-ref: " << s << "\n";
}
int main() {
int a = 10;
// ---- Reference basics ----
int& ref = a; // reference must be initialized
ref = 20; // modifies 'a', since ref is just an alias
std::cout << "a = " << a << "\n"; // prints 20
// Cannot reseat: once 'ref' is bound to 'a', it cannot be bound to another variable
int b = 30;
// ref = &b; invalid, would assign value instead of rebinding
// ---- Pass by reference ----
increment(a); // modifies original 'a'
std::cout << "a after increment = " << a << "\n"; // prints 21
// ---- Pass by const reference ----
std::string text = "Hello";
printConstRef(text); // avoids making a copy
// ---- Lvalue reference variable ----
int& lref = a; // lref is an lvalue-reference-variable to 'a'
lref += 5; // modifies 'a'
std::cout << "a after lref += 5: " << a << "\n";
// ---- Lvalue reference to const ----
const int x = 100;
const int& cref1 = x; // bind to const object
const int& cref2 = a; // also works with non-const object
std::cout << "cref1 = " << cref1 << ", cref2 = " << cref2 << "\n";
// ---- They aren't objects ----
// sizeof(ref) == sizeof(a), because ref is just an alias
std::cout << "sizeof(a) == sizeof(ref): "
<< (sizeof(a) == sizeof(ref)) << "\n";
return 0;
}
16.3. Pointer
-
address-of-operator (&
) returns the memory address of its operand, but not as an address literal. Instead, it returns a pointer to the operand. This pointer holds the address value, and when passed to cout, the stream simply prints that value. -
dereference-operator (*) returns the value at a given memory address as an lvalue, used to access the object being pointed at.
-
pointer is an object that holds a memory address as its value:
- declared as
<type>* ptr_name - This allows us to store the address of some other object to use later.
- we should init the pointers.
- the size of pointer is allways the same (32 or 64-bit architecture)
- can assign an invalid pointer a new value, such as
nullptr wild pointer: pointer that has not been initialized is sometimes called a .dangling pointer: pointer that is holding the address of an object that is no longer valid
- declared as
-
pointer-type is a type that specifies a pointer (like reference-type) by using an asterisk
(<type>*).The type of the pointer has to match the type of the object being pointed at. -
null-pointer (its type is
std::nullptr_t) means something has no value. It often associated with memory address 0. -
pointer-to-const: that points to a value that cannot be modified through the pointer, but the pointer itself is not const.
- declared as
const <type> ptr_name*. - cannot change the value being pointed to, but can make the pointer point to a different address.
- may also point to non-const variables.
- declared as
-
const-pointer: whose stored address cannot be changed after initialization, but the value at that address can be modified.
- declared as
<type>* const ptr_name. - fixed to one address, but we can change the value at that address.
- declared as
-
const-pointer-to-const: cannot be reseated (address fixed) and cannot modify the value it points to.
- declared as
const <type>* const ptr_name. - can only be dereferenced to read the value.
- declared as
pointer-to-pointer void allocateArray(int** ptr, int size) { ptr = new int[size]; // allocate memory and update the original pointer } int myArray = nullptr; allocateArray(&myArray, 5); myArray[0] = 10; // works delete[] myArray;
- e.g.
#include <iostream>
#include <cstdint> // for uintptr_t
int main() {
int x = 42;
// address-of operator (&) returns a pointer to x (not an address literal)
int* ptr = &x;
// Printing the pointer: cout prints the stored address value
std::cout << "Address of x (&x): " << &x << "\n";
std::cout << "Value stored in pointer (ptr): " << ptr << "\n";
// dereference operator (*) gives access to the value at the stored address
std::cout << "Value of x via *ptr: " << *ptr << "\n";
// a pointer is an object that holds a memory address
// we can change its value
ptr = nullptr;
std::cout << "Pointer reset to nullptr: " << ptr << "\n";
// pointer type is declared with '*'
double d = 3.14;
double* pd = &d; // type matches: double* for double
// uintptr_t lets us see the raw numeric value of the address
std::cout << "Numeric address of d: " << (uintptr_t)pd << "\n";
std::cout << "Value of d via *pd: " << *pd << "\n";
// NullPTR =======
int* myNullPtr {}; // value-initialized to nullptr
int* myNullPtr2 {nullptr}; // explicitly initialized to nullptr
// Old C-style (still works, but less safe in C++):
// int* myNullPtr3 {NULL}; // requires <cstddef>
// Const ptr =======
int a = 10;
int b = 20;
// 1. pointer-to-const (const int*)
const int* p1 = &a; // can point to non-const variable
// *p1 = 15; // error: cannot modify value through p1
p1 = &b; // can point to another address
std::cout << "p1 points to: " << *p1 << '\n';
// 2. const-pointer (int* const)
int* const p2 = &a; // must be initialized, fixed address
*p2 = 30; // can modify the value at that address
// p2 = &b; // error: cannot change stored address
std::cout << "p2 points to: " << *p2 << '\n';
// 3. const-pointer-to-const (const int* const)
const int* const p3 = &b; // fixed address + read-only value
// *p3 = 40; // error: cannot modify value
// p3 = &a; // error: cannot reseat pointer
std::cout << "p3 points to: " << *p3 << '\n';
return 0;
}
- pointer-to-pointers:
- a pointer that holds the address of another pointer.
- Using two asterisks to declare a pointer to pointer.
- e.g.
int** ptrptr; - Usages:
- Dynamically allocate an array of pointers
- e.g.
int** array { new int*[10] }; // allocate an array of 10 int pointers - Two-dimensional dynamically allocated arrays
- e.g.
int x { 7 }; // non-constant int (*array)[5] { new int[x][5] }; // rightmost dimension must be constexpr
- void-pointers: Also known as the generic pointer, is a special type of pointer that can be pointed at objects of any data type
- Dereferencing a void pointer is illegal. Instead, the void pointer must first be cast to another pointer type before the dereference can be performed.
- We do not know what type of object it is pointing to, deleting a void pointer will result in undefined behavior.
- function-pointers:
-
e.g.
// fcnPtr is a pointer to a function that takes no arguments and returns an integer int (*fcnPtr)(); -
Assigning a function to a function pointer: just like a normal pointer, and the type (parameters and return type) of the function pointer must match the type of the function.
-
e.g.
// function prototypes int foo(); double goo(); int hoo(int x); // function pointer initializers int (*fcnPtr1)(){ &foo }; // okay int (*fcnPtr2)(){ &goo }; // wrong -- return types don't match! double (*fcnPtr4)(){ &goo }; // okay fcnPtr1 = &hoo; // wrong -- fcnPtr1 has no parameters, but hoo() does int (*fcnPtr3)(int){ &hoo }; // okay -
Calling a function using function pointer: There are two way to do this
- Explicitly derefence
- Implicitly derefence
-
e.g.
int foo(int x) { return x; } int main() { int (*fcnPtr)(int){ &foo }; // Initialize fcnPtr with function foo (*fcnPtr)(5); // call function foo(5) through fcnPtr. int (*fcnPtr2)(int){ &foo }; // Initialize fcnPtr with function foo fcnPtr2(5); // call function foo(5) through fcnPtr2. return 0; } -
Passing functions as arguments to other functions: One of the most useful things to do with function pointers is pass a function as an argument to another function. Functions used as arguments to another function are sometimes called callback functions.
-
16.4. Pass by value/reference/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 pointer that holds the address of str
{
std::cout << *ptr << '\n'; // print the value via the dereferenced pointer
}
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); // pass str by address, does not make a copy of str
return 0;
}
- pass-by-address allows us: ~ pass-by-references
- to pass arguments to a function without making copies of those arguments each time the function is called. (class types)
- to change the value of an argument
- #null checking
Pass by reference when you can, pass by address when you must
- !! C++ really passes everything by value
16.5. Return by value/reference/address
-
T returnValue(…): returns a copy (or move) of the object. The caller gets its own value.
-
T& returnReference(…) returns a reference to an existing object. The caller does not own it, so the object must outlive the reference.
-
T * returnAddress(…) returns a pointer (an address) to an object. The caller must handle the pointer carefully (ensure it’s valid and points to a live object).
-
return-by-reference:- avoids making a copy of the object.
- the referenced object must live beyond the scope of the function, otherwise the reference will dangle.
- never return a non-static local variable or temporary by reference.
-
return-by-addressworks almost identically to return-by-reference. -
return-by-valuejust make a copy
Prefer return by reference over return by address unless the ability to return “no object” (using nullptr) is important.
- e.g.
#include <iostream>
int global = 42;
// Return by value: makes a copy
int returnValue() {
int x = 10;
return x; // copy returned
}
// Return by reference: must refer to existing object
int& returnReference() {
return global; // safe: global outlives the function
}
// Return by address: returns a pointer
int* returnAddress(bool valid) {
if (valid)
return &global; // valid pointer
else
return nullptr; // no object
}
int main() {
int a = returnValue();
std::cout << "By value: " << a << '\n';
int& b = returnReference();
std::cout << "By reference: " << b << '\n';
b = 100; // modifies global
std::cout << "Global after modification: " << global << '\n';
int* c = returnAddress(true);
if (c) std::cout << "By address: " << *c << '\n';
int* d = returnAddress(false);
if (!d) std::cout << "By address: got nullptr\n";
}
16.6. In/Out Params
-
in-parameters:are typically passedby valueorby const reference -
out-parameters:a function parameter that is used only for the purpose of returning information back to the caller.- Avoid out-parameters (except in the rare case where no better options exist).
- Prefer pass by reference for non-optional out-parameters.
-
e.g.
#include <iostream>
#include <string>
// In-parameter by value (cheap to copy)
void greet(std::string name) {
std::cout << "Hello, " << name << "!\n";
}
// In-parameter by const reference (avoid copy for large objects)
int length(const std::string& text) {
return text.size();
}
// Out-parameter by reference (rare case)
void square(int input, int& output) {
output = input * input;
}
int main() {
// in-parameter by value
greet("Alice");
// in-parameter by const reference
std::string msg = "Hello World";
std::cout << "Length = " << length(msg) << "\n";
// out-parameter by reference (not preferred, but possible)
int result;
square(5, result);
std::cout << "Square = " << result << "\n";
}
16.7. Type deduction (auto) with pointers, references, and const
https://www.learncpp.com/cpp-tutorial/type-deduction-with-pointers-references-and-const/
16.8. std::optional
https://www.learncpp.com/cpp-tutorial/stdoptional/
18. Compound Types: Enums and Structs
program-defined-typesare types that programmers create themself.
In C++, struct, class, and union automatically create a new type name, so you don’t need to prefix variables with the keywords struct or union as in C.
18.1. Enumerations
enumis a compound types where every possible value is defined as a symbolic constant.- Named starting with a capital letter. Named enumerators starting with a lower case letter.
unscoped-enum: put their enumerator names into the same scope as the enumeration definition itselfscoped-enum: keep their enumerators inside the enum’s own scope.Usingenum classkeyworld.using enum <EnumName>statement imports all the emnumerators from an enum into the current scope.- putting your enumerations inside a named scope region (such as a namespace or class) so the enumerators don’t pollute the global namespace.
- Specify the base type of an enumeration only when necessary.
- e.g.
#include <iostream>
#include <cstdint> // for uint8_t
// Good naming style:
// Enum name: Capitalized
// Enumerator names: lowercase
enum Color {
red,
green,
blue
};
// Scoped enum — enumerators are inside the enum’s scope
enum class Shape {
circle,
square,
triangle
};
// Scoped enum inside a namespace — prevents name pollution
namespace Game {
enum class Direction {
up,
down,
left,
right
};
}
// Scoped enum with explicit base type
enum class Status : uint8_t {
ok = 0,
error = 1,
unknown = 2
};
int main() {
// Using unscoped enum
Color c = red; // direct access (same scope)
std::cout << "Color value: " << c << "\n";
// Using scoped enum
Shape s = Shape::circle; // must use scope name
if (s == Shape::circle)
std::cout << "Shape is circle\n";
// Scoped enum inside namespace
Game::Direction dir = Game::Direction::up;
if (dir == Game::Direction::up)
std::cout << "Direction is up\n";
// Scoped enum with base type
Status st = Status::ok;
if (st == Status::ok)
std::cout << "Status OK (base type uint8_t)\n";
return 0;
}
18.2. Struct
- A struct is a class type (just like
classesorunion), allows us to bundle multiple variables together into a single type. As such, anything that applies to class types applies to structs. - Defining structs using
structkeywords. - Access struct members:
- Use member selection operator(dot operator)
.for reference/object. - Use arrow operator
->for pointers.ptr->id = (*ptr).id
- Use member selection operator(dot operator)
- Initialization:
- using brace-initialization
{}or by defining default member values. - should provide a default value for all members
- Struct aggregate initializations:
- using brace-initialization
Employee frank = { 1, 32, 60000.0 }; // copy-list initialization using braced list
Employee joe { 2, 28, 45000.0 }; // list initialization using braced list (preferred)
-
Passing and returning structs:
- Passingy reference (efficient and avoids copying)
- Passing temporary
- Create a struct variable and return
- Returning a temporary (unnamed/anonymous) object
-
Struct size and data structure alignment:the size of a struct will be at least as large as the size of all the variables it contains. But it could be larger! For performance reasons, the compiler will sometimes add gaps into structures this is called padding. We can minimize padding by defining your members in decreasing order of size.(e.g., double → int → char).
-
e.g.
#include <iostream>
#include <string>
using namespace std;
// Define a struct
struct SensorData {
double voltage{0.0}; // Default initialization
int id{0};
char status{'N'}; // 'N' = normal, 'E' = error
string label{"Unknown"};
// Member function
void print() const { // Const class objects and const member functions
cout << "Sensor " << id
<< " [" << label << "] "
<< "Voltage: " << voltage
<< " Status: " << status << endl;
}
};
// Function that accepts struct by reference
void updateVoltage(SensorData &data, double newV) {
data.voltage = newV;
}
// Function returning a temporary struct
SensorData makeSensor(int id, double v, const string &label) {
return {v, id, 'N', label};
}
int main() {
// Initialization using braces
SensorData s1{3.3, 1, 'N', "Temperature"};
s1.print();
// Pointer access
SensorData *ptr = &s1;
ptr->status = 'E';
ptr->print();
// Passing by reference
updateVoltage(s1, 4.8);
s1.print();
// Returning temporary struct
SensorData s2 = makeSensor(2, 5.0, "Pressure");
s2.print();
cout << "Size of struct = " << sizeof(SensorData) << " bytes" << endl;
return 0;
}
18.3. Class template
- a class template is a template definition for instantiating class types (structs, classes, or unions). Class template argument deduction (CTAD) is a C++17 feature that allows the compiler to deduce the template type arguments from an initializer.
- Using class template in a function:
- e.g.
#include <iostream>
template <typename T>
struct Pair
{
T first{};
T second{};
};
template <typename T>
constexpr T max(Pair<T> p)
{
return (p.first < p.second ? p.second : p.first);
}
int main()
{
Pair<int> p1{ 5, 6 }; // instantiates Pair<int> and creates object p1
std::cout << p1.first << ' ' << p1.second << '\n';
Pair<double> p2{ 1.2, 3.4 }; // instantiates Pair<double> and creates object p2
std::cout << p2.first << ' ' << p2.second << '\n';
Pair<double> p3{ 7.8, 9.0 }; // creates object p3 using prior definition for Pair<double>
std::cout << p3.first << ' ' << p3.second << '\n';
std::cout << max<int>(p1) << " is larger\n"; // explicit call to max<int>
return 0;
}
// Compiler ===================================================================================
#include <iostream>
// A declaration for our Pair class template
// (we don't need the definition any more since it's not used)
template <typename T>
struct Pair;
// Explicitly define what Pair<int> looks like
template <> // tells the compiler this is a template type with no template parameters
struct Pair<int>
{
int first{};
int second{};
};
// Explicitly define what Pair<double> looks like
template <> // tells the compiler this is a template type with no template parameters
struct Pair<double>
{
double first{};
double second{};
};
int main()
{
Pair<int> p1{ 5, 6 }; // instantiates Pair<int> and creates object p1
std::cout << p1.first << ' ' << p1.second << '\n';
Pair<double> p2{ 1.2, 3.4 }; // instantiates Pair<double> and creates object p2
std::cout << p2.first << ' ' << p2.second << '\n';
Pair<double> p3{ 7.8, 9.0 }; // creates object p3 using prior definition for Pair<double>
std::cout << p3.first << ' ' << p3.second << '\n';
return 0;
}
-
Using class templates in multiple files:Just like function templates, class templates are typically defined in header files
-
Alias templates: is a template that can be used to instantiate type aliases.
-
e.g.
// Alias templates must be defined in global scope
template <typename T>
using Coord = Pair<T>; // Coord is an alias for Pair<T>
19. Classes
-
A class is a
user-defined blueprintused to create objects. It defines theproperties and behaviorsthatall objects of that typeshare. -
An object is an
instance of a class. It representsa real entityand containsactual valuesfor the class’s attributes. -
An instance is a
specific objectcreated from a class. (In practice, “object” and “instance” are often used interchangeably.) -
Four Pillars of OOP in C++:
-
Abstraction is the
process of hiding the implementation detailsand onlyshowing the essential details or featuresto the user. Itallows to focus onwhat an objectdoesrather thanhow it doesit. It is achieved usingabstract classes(classes that have at least one pure virtual function). -
Encapsulation is the
process of bundling data and methods into a single unit(class) andrestricting direct accessto some components. Data is hidden and accessed through public methods. It is achieved using access specifiers likeprivate, protected, and public. -
Inheritance is
a mechanismwhere a derived class acquires the properties and behaviors of a base class, forming anis-arelationship. It improves code reuse and extensibility. It is implemented using:followed by an access specifierpublic, private, protected. -
Polymorphism means
many forms. It allows thesame interface (function or method)tobehave differentlydepending on the context. It is achieved through: -Compile-time polymorphism:function overloading, operator overloading-Runtime polymorphism:virtual functions
-
-
Overview:
// ===== File: main.cpp =====
#include <iostream>
#include <string>
#include <utility>
#include <vector>
using namespace std;
// ===== File: Person.h =====
class Person {
private: // (14.5) Data hiding and encapsulation
string name;
int age;
public:
// (14.9) Constructor
Person(string n, int a) : name{std::move(n)}, age{a} {
cout << "Constructor: " << name << endl;
}
// (14.12) Delegating constructor
Person() : Person("Unknown", 0) {}
// (14.14) Copy constructor
Person(const Person& other) : name{other.name}, age{other.age} {
cout << "Copy constructor called for " << name << endl;
}
// (15.4) Destructor
~Person() {
cout << "Destructor: " << name << endl;
}
// (14.3) Member function
void print() const { // (14.4) const member function
cout << name << " (" << age << ")\n";
}
// (14.6) Access function
string getName() const { return name; }
int getAge() const { return age; }
// (14.7) Return reference to data member
int& getAgeRef() { return age; }
// (14.16) Converting constructor (implicit)
Person(int a) : name{"Anon"}, age{a} {}
// (14.17) constexpr constructor (simple example)
constexpr Person(const char* n, int a, bool) : name{n}, age{a} {}
// (15.1) Function chaining using hidden this pointer
Person& setName(const string& n) { name = n; return *this; }
Person& setAge(int a) { age = a; return *this; }
// (15.7) Static member function
static void hello() { cout << "Hello from Person class!\n"; }
// (15.8) Friend non-member function
friend void showSecret(const Person& p);
};
// ===== File: Person.cpp =====
void showSecret(const Person& p) { // (15.8) Friend function
cout << "Friend: " << p.name << " is " << p.age << " years old.\n";
}
// ===== File: Employee.h =====
class Employee : public Person { // Inheritance for demonstration
private:
double salary;
public:
Employee(string n, int a, double s)
: Person(std::move(n), a), salary{s} {}
void print() const { // Override
cout << "[Employee] ";
Person::print();
cout << "Salary: $" << salary << "\n";
}
// (15.9) Friend class
friend class HR;
};
// ===== File: HR.h =====
class HR {
public:
void adjustSalary(Employee& e, double newSalary) {
e.salary = newSalary;
cout << "HR adjusted salary!\n";
}
};
// ===== File: Counter.h =====
class Counter {
private:
static int count; // (15.6) Static member variable
public:
Counter() { ++count; }
~Counter() { --count; }
static int getCount() { return count; } // (15.7)
};
int Counter::count = 0;
// ===== File: Box.h =====
// (15.5) Class template with member functions
template <typename T>
class Box {
private:
T value;
public:
explicit Box(T v) : value{v} {}
T get() const { return value; }
void set(T v) { value = v; }
};
// ===== File: main.cpp (continued) =====
int main() {
cout << "=== OOP Example ===\n";
// Constructors
Person p1("Alice", 25);
Person p2 = p1; // Copy constructor
Person p3; // Delegating constructor
p1.print();
// Function chaining
p1.setName("Bob").setAge(30).print();
// Static function
Person::hello();
// Friend function
showSecret(p1);
// Employee & HR
Employee e1("Charlie", 35, 50000);
HR hr;
e1.print();
hr.adjustSalary(e1, 60000);
e1.print();
// Static member variable usage
Counter c1, c2;
cout << "Counter count: " << Counter::getCount() << endl;
// Template class
Box<int> intBox(123);
cout << "Box value: " << intBox.get() << endl;
// (14.16) Implicit converting constructor
Person p4 = 42; // age=42, name="Anon"
p4.print();
// (14.17) constexpr example
constexpr Person p5("ConstExpr", 10, true);
p5.print();
cout << "=== End ===\n";
return 0;
}
19.0. Shallow copying & Deep copying
-
Shallow copying:
- C++ does not know much about our class, so the
default copy constructoranddefault assignment operatorsuse memberwise copy and thencopy each member of the class individually. - Simple classes => work well.
- Classes handling dynamically allocated memory => just copy the address of the pointer => does not allocate any memory => causes problems.
- C++ does not know much about our class, so the
-
Deep copying:
- Allocates memory for the copy and then copies the actual values, so that the copy lives in memory distinct from the source.
- The original and the copy will not affect each other in any way.
- This requires write our own
default copy constructoranddefault assigment operators
-
Role of three:
If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.This ensures proper resource management and avoids shallow copy problems.
- Role of five:
Extends the Rule of Three in C++11 and later. In addition to the destructor, copy constructor, and copy assignment operator, it includes the move constructor and move assignment operator. This allows efficient transfer of resources instead of copying.
- Role of zero:
The best practice is to write classes that do not manage resources directly, letting the compiler generate all special member functions automatically. This avoids the need to define destructors or copy/move operations manually.
19.1. Introduce
-
A class is a program-defined compound type that can have many member variables with different types (this point is different from structure).
-
A class is a program-defined compound type that bundles both data and functions that work on that data.
-
Examples:
#include <iostream>
class MyClass{
private:
int variable1;
int variable2;
public:
MyClass(int v1, int v2){
variable1 = v1;
variable2 = v2;
}
void printVariables(){
std::cout << variable1 << "-" <<variable2;
}
};
int main()
{
std::cout<<"Hello Classes";
MyClass myObject = MyClass(3,4);
myObject.printVariables();
return 0;
}
19.2. Member Variables/ Functions:
- The variable/functions that belong to a class type are called member variables/ functions.
- In C, structs only have data members, not member functions.
19.3. Const class objects and const member functions
- Const class objects: just like with normal variables, we can make our class type objects const or constexpr.
- Const member function: is a member function that guarentees it will not modify the object or call any non-constant member functions.
- Syntax
<returnType> <nameFunction> const(<params>){}
- Syntax
19.4. Temporary class objects
- A temporary object (sometimes called an
anonymous objector anunnamed object) is an object that has no name and exists only for the duration of a single expression. - e.g.
#include <iostream>
class IntPair
{
private:
int m_x{};
int m_y{};
public:
IntPair(int x, int y)
: m_x { x }, m_y { y }
{}
int x() const { return m_x; }
int y() const{ return m_y; }
};
void print(IntPair p)
{
std::cout << "(" << p.x() << ", " << p.y() << ")\n";
}
int main()
{
// Case 1: Pass variable
IntPair p { 3, 4 };
print(p);
// Case 2: Construct temporary IntPair and pass to function
// When the function call returns, the temporary object is destroyed.
print(IntPair { 5, 6 } );
// Case 3: Implicitly convert { 7, 8 } to a temporary Intpair and pass to function
print( { 7, 8 } );
return 0;
}
19.5. Public and private members and access specifiers
- By default, all members of a
structare public members. - By default, the members of a class are
private.
19.6. Access functions
TODO:
19.7. Member functions returning references to data members
TODO:
19.8. Encapsulation - The benefits of data hiding
TODO:
19.9. Constructors
- We are normally using the aggregate to initialize the class type(Aggregate initialization means the use of brace-enclosed initializer lists to initialize all members of an aggregate (ie an array or struct)), but it does not work as soon as we make member variables private.
- A constructor is a special member function that is used to initialize class type objects. That is automatically called after a non-aggregate class type object is created.
- perform initialization of any member variables (via a member initialization list)
- perform other setup functions (via statements in the body of the constructor). This might include things such as error checking the initialization values, opening a file or database, etc…
- Syntax:
- Constructors must have the same name as the class (with the same capitalization).
- For template classes, this name excludes the template parameters.
- Constructors have no return type (not even void).
19.10. Constructor - member initializer lists
-
The member initializer list is defined after the constructor parameters. It begins with a colon (
:), and then lists each member to initialize along with the initialization value for that variable, separated by a comma (,). We must use a direct form of initialization here (preferably using braces({}), but parentheses(()) works as well) .Using copy initialization (with an equals(=)) does not work here. -
e.g.:
Foo(int x, int y) : m_x { x }, m_y { y }
{
// m_x = x; this is an assignment
}
- Members in the member initializer list should be listed in the order in which they are defined in the class
- Prefer using the member initializer list to initialize your members over assigning values because in case where members are required to be initialized (such as for data members that are const or references) assignment will not work.
19.11. Constructor - Default and default arguments
- A default constructor is a constructor that accepts no arguments.
- Because constructors are functions, we can:
- Constructors with default arguments
- Overloaded constructors
- An implicit default constructor is generated by the compiler when the class has no user-declared constructors. This constructor has nothing.
- An explicitly default constructor is used in case we already create the constructor ourselves, but also want the compiler to generate the default constructor. Using keywork
default - e.g.
class Foo
{
private:
int m_x {};
int m_y {};
public:
Foo() = default; // generates an explicitly defaulted default constructor
Foo(int x, int y)
: m_x { x }, m_y { y }
{
std::cout << "Foo(" << m_x << ", " << m_y << ") constructed\n";
}
};
int main()
{
Foo foo{}; // calls Foo() default constructor
return 0;
}
19.12. Constructor - Delegating
- Delegating constructors allow to delegate (transfer responsibility for) initialization to another constructor from the same class type.
- Simply call the constructor in the member initializer list
- Use of the
statickeyword for the const variables member allows us to have a single member that is shared by all class objects. - e.g.
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
private:
std::string m_name { "???" };
int m_id { 0 };
public:
Employee(std::string_view name)
: Employee{ name, 0 } // delegate initialization to Employee(std::string_view, int) constructor
{
}
Employee(std::string_view name, int id)
: m_name{ name }, m_id { id } // actually initializes the members
{
std::cout << "Employee " << m_name << " created\n";
}
};
int main()
{
Employee e1{ "James" };
Employee e2{ "Dave", 42 };
}
19.13. Constructor - Copy
- A copy constructor is a constructor that is used to initialize an object with an existing object of the same type. After the copy constructor executes, the newly created object should be a copy of the object passed in as the initializer.
- An implicit copy constructor: C++ will create a public implicit copy constructor for us if we do not provide a one.
- An explicitly copy constructor: by explicitly define our own copy constructor
- e.g.
// Copy constructor
Fraction(const Fraction& fraction)
// Initialize our members using the corresponding member of the parameter
: m_numerator{ fraction.m_numerator }
, m_denominator{ fraction.m_denominator }
{
// do other things
}
- Using
= defaultto generate a default copy constructor. - Using
= deleteto prevent copies.
// Explicitly request default copy constructor
Fraction(const Fraction& fraction) = default;
Fraction fCopy { f };
// Delete the copy constructor so no copies can be made
Fraction(const Fraction& fraction) = delete;
Fraction f { 5, 3 };
Fraction fCopy { f }; // compile error: copy constructor has been deleted
The rule of three is a well known C++ principle that states that if a class requires a user-defined copy constructor, destructor, or copy assignment operator, then it probably requires all three. In C++11, this was expanded to the rule of five, which adds the move constructor and move assignment operator to the list. Not following the rule of three/rule of five is likely to lead to malfunctioning code. We’ll revisit the rule of three and rule of five when we cover dynamic memory allocation
19.14. Class initialization and copy elision
- For variables:
int a; // no initializer (default initialization)
int b = 5; // initializer after equals sign (copy initialization)
int c( 6 ); // initializer in parentheses (direct initialization)
// List initialization methods (C++11)
int d { 7 }; // initializer in braces (direct list initialization)
int e = { 8 }; // initializer in braces after equals sign (copy list initialization)
int f {}; // initializer is empty braces (value initialization)
- For object with class types:
#include <iostream>
class Foo
{
public:
// Default constructor
Foo()
{
std::cout << "Foo()\n";
}
// Normal constructor
Foo(int x)
{
std::cout << "Foo(int) " << x << '\n';
}
// Copy constructor
Foo(const Foo&)
{
std::cout << "Foo(const Foo&)\n";
}
};
int main()
{
// Calls Foo() default constructor
Foo f1; // default initialization
Foo f2{}; // value initialization (preferred)
// Calls foo(int) normal constructor
Foo f3 = 3; // copy initialization (non-explicit constructors only)
Foo f4(4); // direct initialization
Foo f5{ 5 }; // direct list initialization (preferred)
Foo f6 = { 6 }; // copy list initialization (non-explicit constructors only)
// Calls foo(const Foo&) copy constructor
Foo f7 = f3; // copy initialization
Foo f8(f3); // direct initialization
Foo f9{ f3 }; // direct list initialization (preferred)
Foo f10 = { f3 }; // copy list initialization
return 0;
}
- For all types of initialization:
- When initializing a class type, the set of constructors for that class are examined, and overload resolution is used to determine the best matching constructor. This may involve implicit conversion of arguments.
- When initializing a non-class type, the implicit conversion rules are used to determine whether an implicit conversion exists.
- List initialization disallows narrowing conversions.
- Copy initialization only considers non-explicit constructors/conversion functions.
- List initialization prioritizes matching list constructors over other matching constructors.
19.15. Converting constructors and the explicit keyword
- For example:
#include <iostream>
class Foo
{
private:
int m_x{};
public:
Foo(int x)
: m_x{ x }
{
}
int getX() const { return m_x; }
};
void printFoo(Foo f) // has a Foo parameter
{
std::cout << f.getX();
}
int main()
{
printFoo(5); // we're supplying an int argument
return 0;
}
-
The compiler will look to see if there is any constructor that it can be use to perform (5) -> Foo.
-
A converting constructor is a constructor that can be used to perform an implicit conversion is called a converting constructor. By default, all constructors are converting constructors.
-
Only one user-defined conversion may be applied.
-
The
explicitkeyword is used to to tell the compiler that a constructor should not be used as a converting constructor.- For constructors with a separate declaration (inside the class) and definition (outside the class), the explicit keyword is used only on the declaration.
- Explicit constructors can be used for direct and direct list initialization
- Prefer use this key work for constructors that take a single argument.
-
e.g.
#include <iostream>
class Dollars
{
private:
int m_dollars{};
public:
explicit Dollars(int d) // now explicit
: m_dollars{ d }
{
}
int getDollars() const { return m_dollars; }
};
void print(Dollars d)
{
std::cout << "$" << d.getDollars();
}
int main()
{
print(5); // compilation error because Dollars(int) is explicit
Dollars d1(5); // ok
Dollars d2{5}; // ok
return 0;
}
- Return by value and explicit constructors: when we return a value from a function, if that value does not match the return type of the function, an implicit conversion will occur. Just like with pass by value, such conversions cannot use explicit constructors.
- e.g.
#include <iostream>
class Foo
{
public:
explicit Foo() // note: explicit (just for sake of example)
{
}
explicit Foo(int x) // note: explicit
{
}
};
Foo getFoo()
{
// explicit Foo() cases
return Foo{ }; // ok
return { }; // error: can't implicitly convert initializer list to Foo
// explicit Foo(int) cases
return 5; // error: can't implicitly convert int to Foo
return Foo{ 5 }; // ok
return { 5 }; // error: can't implicitly convert initializer list to Foo
}
int main()
{
return 0;
}
19.16. Constexpr aggregates and classes:
@TODO:
19.17. The hidden “this” pointer and member function chaining
- Cpp utilizes a hidden pointer named
this. This is a const pointer that holds the address of the current implicit object. - e.g.
simple.setID(2);
Simple::setID(&simple, 2); // note that simple has been changed from an object prefix to a function argument!
// implementation
void setID(int id) { m_id = id; }
static void setID(Simple* const this, int id) { this->m_id = id; }
- Explainations:
- When we call simple.setID(2), the compiler actually calls Simple::setID(&simple, 2), and simple is passed by address to the function. The function has a hidden parameter named this which receives the address of simple. Member variables inside setID() are prefixed with this->, which points to simple. So when the compiler evaluates this->m_id, it’s actually resolving to simple.m_id.
- All non-static member functions have a this const pointer that holds the address of the implicit object.
thisalways points to the object being operated on - Using this to create a reset function for a class back to default state:create a new object (using the default constructor), and then assign that new object to the current implicit object
void reset()
{
*this = {}; // value initialize a new object and overwrite the implicit object
}
thisandconst objects:
#include <iostream>
using namespace std;
class Something {
private:
int value;
public:
Something(int v) : value(v) {}
// Non-const member function
void setValue(int v) {
cout << "Non-const setValue() called" << endl;
value = v; // allowed: can modify object
}
// Const member function
int getValue() const {
cout << "Const getValue() called" << endl;
// value = 10; // would be an error: cannot modify inside const function
return value;
}
// Non-const getValue (overload)
int getValue() {
cout << "Non-const getValue() called" << endl;
return value;
}
};
int main() {
Something a(5);
const Something b(10);
// Non-const object: can call both const and non-const functions
cout << a.getValue() << endl; // calls non-const version
a.setValue(7); // OK
cout << a.getValue() << endl;
// Const object: can only call const functions
cout << b.getValue() << endl; // calls const version
// b.setValue(20); // error: cannot call non-const on const object
return 0;
}
Why this a pointer and not a reference: Since the this pointer always points to the implicit object (and can never be a null pointer unless we’ve done something to cause undefined behavior), you may be wondering why this is a pointer instead of a reference. The answer is simple: when this was added to C++, references didn’t exist yet.
19.18. Classes and header files
- Member functions can be defined outside the class definition just like non-member functions. The only difference is that we must prefix the member function names with the name of the class type so the compiler knows we’re defining a member of that class type rather than a non-member. e.g.
void MyClass::myFnc(){//}.- Putting class definitions in a header file.
- Prefer to put your class definitions in a header file with the same name as the class.
- Trivial member functions (such as access functions, constructors with empty bodies,Default arguments for member functions, etc…) can be defined inside the class definition.
- Prefer to define non-trivial member functions in a source file with the same name as the class.
- Inline member functions:
- Member functions defined inside the class definition are implicitly inline.
- Member functions defined outside the class definition are not implicitly inline (and thus are subject to the one definition per program part of the one-definition rule). This is why such functions are usually defined in a code file (where they will only have one definition across the whole program).
- Alternatively, member functions defined outside the class definition can be left in the header file if they are made inline.
- e.g.
// Date.h:
#ifndef DATE_H
#define DATE_H
class Date
{
private:
int m_year{};
int m_month{};
int m_day{};
public:
Date(int year, int month, int day);
void print() const;
int getYear() const { return m_year; }
int getMonth() const { return m_month; }
int getDay() const { return m_day; }
};
#endif
// Date.cpp
#include "Date.h"
Date::Date(int year, int month, int day) // constructor definition
: m_year{ year }
, m_month{ month }
, m_day{ day }
{
}
void Date::print() const // print function definition
{
std::cout << "Date(" << m_year << ", " << m_month << ", " << m_day << ")\n";
};
19.19. Nested types (member types)
- Class types support another kind of member:
nested types(also called member types). To create a nested type, you simply define the type inside the class, under the appropriate access specifier. - Define any nested types at the top of your class type.
- Outside the class, we must use the fully qualified name:
OuterClass::InerClass p{}; nested classesare members of the outer class, they can access any private members of the outer class that are in scope.- A nested type cannot be forward declared prior to the definition of the enclosing class.\ư
19.20. Destructors
- For example, the classes that use a resource (most often memory, but sometimes files, databases, network connections, etc…) often need to be explicitly sent or closed before the class object using them is destroyed. In other cases, we may want to do some record-keeping prior to the destruction of the object, such as writing information to a log file, or sending a piece of telemetry to a server. The term “clean up” is often used to refer to any set of tasks that a class must perform before an object of the class is destroyed in order to behave as expected. If we have to rely on the user of such a class to ensure that the function that performs clean up is called prior to the object being destroyed, we are likely to run into errors somewhere.
-
destructor is a special member function that is called automatically when an object of a non-aggregate class type is destroyed.
-
Syntax:
- The destructor must have the same name as the class, preceded by a tilde (
~). - The destructor can not take arguments.
- The destructor has no return type.
- The destructor must have the same name as the class, preceded by a tilde (
-
A class can only have a single destructor.
-
An implicit destructor: If a non-aggregate class type object has no user-declared destructor, the compiler will generate a destructor with an empty body. It is effectively just a placeholder.
-
e.g.
class NetworkData
{
private:
std::string m_serverName{};
DataStore m_dataQueue{};
public:
NetworkData(std::string_view serverName)
: m_serverName { serverName }
{
}
~NetworkData()
{
sendData(); // make sure all data is sent before object is destroyed
}
void addData(std::string_view data)
{
m_dataQueue.add(data);
}
void sendData()
{
// connect to server
// send all data
// clear data
}
};
int main()
{
NetworkData n("someipAddress");
n.addData("somedata1");
n.addData("somedata2");
return 0;
}
19.21. Class templates with member functions
-e.g.
#include <ios> // for std::boolalpha
#include <iostream>
template <typename T>
class Pair
{
private:
T m_first{};
T m_second{};
public:
// When we define a member function inside the class definition,
// the template parameter declaration belonging to the class applies
Pair(const T& first, const T& second)
: m_first{ first }
, m_second{ second }
{
}
bool isEqual(const Pair<T>& pair);
};
// When we define a member function outside the class definition,
// we need to resupply a template parameter declaration
template <typename T>
bool Pair<T>::isEqual(const Pair<T>& pair)
{
return m_first == pair.m_first && m_second == pair.m_second;
}
int main()
{
Pair p1{ 5, 6 }; // uses CTAD to infer type Pair<int>
std::cout << std::boolalpha << "isEqual(5, 6): " << p1.isEqual( Pair{5, 6} ) << '\n';
std::cout << std::boolalpha << "isEqual(5, 7): " << p1.isEqual( Pair{5, 7} ) << '\n';
return 0;
}
19.22. Static member
-
Static member variables: are static duration members that are shared by all objects of the class. Static members exist even if no objects of the class have been instantiated. Prefer to access them using the class name, the scope resolution operator, and the members name.
-
Syntax:
static <type> <name>{<value>} -
Static member variables are shared by all objects of the class
-
Static members are not associated with class objects
-
Static members are global variables that live inside the scope region of the class.
-
Access static members using the class name and the scope resolution operator (
::). -
Defining and initializing static member variables:
- Initialization of static member variables inside the class definition
- Make your static members
inlineorconstexprso they can be initialized inside the class definition (.h).
-
Static member functions: are member functions that can be called with no object. They do not have a *this pointer, and cannot access non-static data members.
- Can access to the satatic members via non-static function but requires us to instantiate an object to call
- Because static member functions are not attached to an object, they have no this pointer
- Can directly access other static members (variables or functions), but not non-static members.
-
e.g.
#include <iostream>
#include <string>
class Counter {
public:
// Static member variable (shared by all instances)
static inline int count {0}; // C++17+ inline initialization
static constexpr const char* name {"App"}; // compile-time constant
Counter() {
++count; // Increment static variable each time an object is created
}
~Counter() {
--count; // Decrement on destruction
}
// Static member function
static void showCount() {
std::cout << "Count: " << count << '\n';
// std::cout << id; not allowed — no access to non-static members
}
// Non-static function accessing static member
void showInfo() const {
std::cout << "Instance -> " << name << " | Current count: " << count << '\n';
}
private:
int id {0}; // non-static member variable
};
// (Alternative old-style definition — no longer needed with inline)
// int Counter::count = 0;
int main() {
Counter c1, c2;
c1.showInfo(); // Access static via non-static method
Counter::showCount(); // Access static function via class name
Counter c3;
Counter::showCount();
return 0;
}
19.23. Friend non-member functions
-
A friend declaration (using the
friendkeyword) can be used to tell the compiler that some other class or function is now a friend. In C++, a friend is a class or function (member or non-member) that has been granted full access to the private and protected members of another class. In this way, a class can selectively give other classes or functions full access to their members without impacting anything else. -
A friend function is a function (member or non-member) that can access the private and protected members of a class as though it were a member of that class. In all other regards, the friend function is a normal function.
#include <iostream>
class Accumulator
{
private:
int m_value { 0 };
public:
void add(int value) { m_value += value; }
// Here is the friend declaration that makes non-member function void print(const Accumulator& accumulator) a friend of Accumulator
// member function but it have friend keyword, it is instead treated as a non-member function
friend void print(const Accumulator& accumulator);
};
// non-member function
void print(const Accumulator& accumulator)
{
// Because print() is a friend of Accumulator
// it can access the private members of Accumulator
std::cout << accumulator.m_value;
}
int main()
{
Accumulator acc{};
acc.add(5); // add 5 to the accumulator
print(acc); // call the print() non-member function
return 0;
}
- We can also define a friend non-member inside a class.
- Prefer non-friend functions to friend functions
- Multiple friends: A function can be a friend of more than one class at the same time. ( Class forward declarations serve the same role as function forward declarations)
19.24. Friend classes and friend member functions
- A friend class is a class that can access the private and protected members of another class.
- e.g.
#include <iostream>
class Storage
{
private:
int m_nValue {};
double m_dValue {};
public:
Storage(int nValue, double dValue)
: m_nValue { nValue }, m_dValue { dValue }
{ }
// Make the Display class a friend of Storage
friend class Display;
};
class Display
{
private:
bool m_displayIntFirst {};
public:
Display(bool displayIntFirst)
: m_displayIntFirst { displayIntFirst }
{
}
// Because Display is a friend of Storage, Display members can access the private members of Storage
void displayStorage(const Storage& storage)
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
else // display double first
std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
}
void setDisplayIntFirst(bool b)
{
m_displayIntFirst = b;
}
};
int main()
{
Storage storage { 5, 6.7 };
Display display { false };
display.displayStorage(storage);
display.setDisplayIntFirst(true);
display.displayStorage(storage);
return 0;
}
- A friend member function is a specific member function of one class that is granted access to the private and protected members of another class.
- e.g.
#include <iostream>
class Storage; // forward declaration for class Storage
class Display
{
private:
bool m_displayIntFirst {};
public:
Display(bool displayIntFirst)
: m_displayIntFirst { displayIntFirst }
{
}
void displayStorage(const Storage& storage); // forward declaration for Storage needed for reference here
};
class Storage // full definition of Storage class
{
private:
int m_nValue {};
double m_dValue {};
public:
Storage(int nValue, double dValue)
: m_nValue { nValue }, m_dValue { dValue }
{
}
// Make the Display::displayStorage member function a friend of the Storage class
// Requires seeing the full definition of class Display (as displayStorage is a member)
friend void Display::displayStorage(const Storage& storage);
};
// Now we can define Display::displayStorage
// Requires seeing the full definition of class Storage (as we access Storage members)
void Display::displayStorage(const Storage& storage)
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
else // display double first
std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
}
int main()
{
Storage storage { 5, 6.7 };
Display display { false };
display.displayStorage(storage);
return 0;
}
19.25. Ref qualifiers
- C++11 introduced a little known feature called a ref-qualifier that allows us to overload a member function based on whether it is being called on an lvalue or an rvalue implicit object. Using this feature, we can create two versions of getName() – one for the case where our implicit object is an lvalue, and one for the case where our implicit object is an rvalue.
- e.g.
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
private:
std::string m_name{};
public:
Employee(std::string_view name): m_name { name } {}
const std::string& getName() const & { return m_name; } // & qualifier overloads function to match only lvalue implicit objects
std::string getName() const && { return m_name; } // && qualifier overloads function to match only rvalue implicit objects
};
// createEmployee() returns an Employee by value (which means the returned value is an rvalue)
Employee createEmployee(std::string_view name)
{
Employee e { name };
return e;
}
int main()
{
Employee joe { "Joe" };
std::cout << joe.getName() << '\n'; // Joe is an lvalue, so this calls std::string& getName() & (returns a reference)
std::cout << createEmployee("Frank").getName() << '\n'; // Frank is an rvalue, so this calls std::string getName() && (makes a copy)
return 0;
}
20. Inheritance
20.1. Object Relationships
-
Learned about some different kinds of relationships between two objects.
-
The process of building complex objects from simpler ones is called object composition.
-
There are two types of object composition:
composition, andaggregation. -
Composition exists when a member of a class has a part-of relationship with the class. In a composition relationship, the class manages the existence of the members.
- To qualify as a composition, an object and a part must have the following relationship:
- The part (member) is part of the object (class)
- The part (member) can only belong to one object (class) at a time
- The part (member) has its existence managed by the object (class)
- The part (member) does not know about the existence of the object (class)
- Typically implemented via normal member variables, or by pointers where the class manages all the memory allocation and deallocation. If you can implement a class as a composition, you should implement a class as a composition.
- To qualify as a composition, an object and a part must have the following relationship:
-
Aggregations exists when a class has a has-a relationship with the member. In an aggregation relationship, the class does not manage the existence of the members.
- To qualify as an aggregation, an object and its parts must have the following relationship:
- The part (member) is part of the object (class)
- The part (member) can belong to more than one object (class) at a time
- The part (member) does not have its existence managed by the object (class)
- The part (member) does not know about the existence of the object (class)
- Typically implemented via pointer or reference.
- To qualify as an aggregation, an object and its parts must have the following relationship:
-
Associations are a looser type of relationship, where the class uses-an otherwise unrelated object.
- To qualify as an association, an object and an associated object must have the following relationship:
- The associated object (member) is otherwise unrelated to the object (class)
- associated object (member) can belong to more than one object (class) at a time
- The associated object (member) does not have its existence managed by the object (class)
- The associated object (member) may or may not know about the existence of the object (class)
- May be implemented via pointer or reference, or by a more indirect means (such as holding the index or key of the associated object).
- To qualify as an association, an object and an associated object must have the following relationship:
-
In a dependency, one class uses another class to perform a task. The dependent class typically is not a member of the class using it, but rather is temporarily created, used, and then destroyed, or passed into a member function from an external source.
-
In a container class one class provides a container to hold multiple objects of another type. A value container is a composition that stores copies of the objects it is holding. A reference container is an aggregation that stores pointers or references to objects that live outside the container.
-
std::initializer_list can be used to implement constructors, assignment operators, and other functions that accept a list initialization parameter.
-
std::initializer_list lives in the <initializer_list> header.
| Property\Type | Composition | Aggregation | Association | Dependency |
|---|---|---|---|---|
| Relationship type | Whole/part | Whole/part | Otherwise unrelated | Otherwise unrelated |
| Members can belong to multiple classes | No | Yes | Yes | Yes |
| Members existence managed by class | Yes | No | No | No |
| Directionality | Unidirectional | Unidirectional | Unidirectional or bidirectional | Unidirectional |
| Relationship verb | Part-of | Has-a | Uses-a | Depends-on |
20.2. Introduce Inheritance
Inheritanceallows us to reuse classes by having other classes inherit their members.- Syntax: after the class declaration, we use a colon
:, the wordpublicand the name of the class we wish to inherit. - e.g.
class Base
{
public:
int m_id {};
Base(int id=0)
: m_id { id }
{
}
int getId() const { return m_id; }
};
class Derived: public Base
{
public:
double m_cost {};
Derived(double cost=0.0)
: m_cost { cost }
{
}
double getCost() const { return m_cost; }
};
C++ constructsderived classes in phases, starting with the most-base class (at the top of the inheritance tree) and finishing with the most-child class (at the bottom of the inheritance tree).
20.3. Constructors and initialization of derived classes
Constructors: the derived class constructor is responsible for determining which base class constructor is called. If no base class constructor is specified, the default base class constructor will be used. In that case, if no default base class constructor can be found (or created by default), the compiler will display an error.Destructors:When a derived class is destroyed, each destructor is called in the reverse order of construction.- In more detail:
- Memory for the derived class is set aside (enough for both the base and derived portions).
- The appropriate derived class constructor is called.
- The base class object is constructed first using the appropriate base class constructor. If no base class constructor is specified, the default constructor will be used.
- The initialization list of the derived class initializes members of the derived class.
- The body of the derived class constructor executes.
- Control is returned to the caller.
20.4. Inheritance and access specifiers
- If we do not choose an inheritance type, C++ defaults to private inheritance
private-inaccessibledoes not affect the way that the derived class accesses members inherited from its parent! It only affects the code trying to access those members through the derived class.- e.g.
// Inherit from Base publicly
class Pub: public Base
{
// Public inheritance means:
// Public inherited members stay public (so m_public is treated as public)
// Protected inherited members stay protected (so m_protected is treated as protected)
// Private inherited members stay inaccessible (so m_private is inaccessible)
};
// Inherit from Base protectedly
class Pro: protected Base
{
// Protected inheritance means:
// Public inherited members stay protected (so m_public is treated as protected)
// Protected inherited members stay protected (so m_protected is treated as protected)
// Private inherited members stay inaccessible (so m_private is inaccessible)
};
// Inherit from Base privately
class Pri: private Base
{
// Private inheritance means:
// Public inherited members become private (so m_public is treated as private)
// Protected inherited members become private (so m_protected is treated as private)
// Private inherited members stay inaccessible (so m_private is inaccessible)
};
class Def: Base // Defaults to private inheritance
{
};
20.5. Calling inherited functions and overriding behavior
- Adding new functionality to a derived class: we can inherit the base class functionality and then add new functionality, modify existing functionality, or hide functionality we don’t wan
- Calling inherited functions: Just call as base class do, we derived.baseFunction() is called, the compiler looks to see if a function named baseFunction() has been defined in the Derived class. It hasn’t. So it moves to the parent class (in this case, Base), and tries again there. Base has defined an baseFunction() function, so it uses that one.
- Redefining behaviors: To modify the way a function defined in a base class works in the derived class, simply redefine the function in the derived class.
- Adding to existing functionality: To have a derived function call a base function of the same name, simply do a normal function call, but prefix the function with the scope qualifier of the base class.
- Overload resolution in derived classes: By putting the using-declaration
using Base::function(); insideDerived, we are telling the compiler that allBasefunctions namedfunctionshould be visible inDerived, which will cause them to be eligible for overload resolution. As a result,Base::function()is selected overDerived::function(double)when callbaseObject.function(5). - e.g.
#include <iostream>
using namespace std;
// ===== Base class =====
class Base {
public:
void baseFunction() {
cout << "Base::baseFunction()" << endl;
}
void greet() {
cout << "Hello from Base!" << endl;
}
void function(int x) {
cout << "Base::function(int) called with " << x << endl;
}
};
// ===== Derived class =====
class Derived : public Base {
public:
// 1. Redefining (overriding) behavior
void greet() {
cout << "Hello from Derived!" << endl;
}
// 2. Adding to existing functionality
void greetWithBase() {
cout << "Derived part first -> ";
Base::greet(); // call the Base version explicitly
}
// 3. Hiding base function by defining same name
void baseFunction() {
cout << "Derived::baseFunction()" << endl;
}
// 4. Overload resolution
void function(double x) {
cout << "Derived::function(double) called with " << x << endl;
}
// Bring Base::function(int) into scope for overload resolution
using Base::function;
};
int main() {
Derived d;
cout << "\n--- Calling inherited function ---" << endl;
d.Base::baseFunction(); // explicitly call base version
d.baseFunction(); // calls Derived::baseFunction()
cout << "\n--- Redefining behavior ---" << endl;
d.greet(); // calls Derived version
cout << "\n--- Adding to existing functionality ---" << endl;
d.greetWithBase(); // calls Derived + Base
cout << "\n--- Overload resolution ---" << endl;
d.function(10); // selects Base::function(int)
d.function(3.14); // selects Derived::function(double)
return 0;
}
20.6. Hiding inherited functionality
- Changing an inherited member’s access level: we can change an inherited member’s access specifier in the derived class, by using a using declaration to identify the (scoped) base class member that is having its access changed in the derived class, under the new access specifier.
- e.g.
#include <iostream>
class Base
{
private:
int m_value {};
public:
Base(int value)
: m_value { value }
{
}
protected:
void printValue() const { std::cout << m_value; }
};
class Derived: public Base
{
public:
Derived(int value)
: Base { value }
{
}
// Base::printValue was inherited as protected, so the public has no access
// But we're changing it to public via a using declaration
using Base::printValue; // note: no parenthesis here
};
- Hiding functionality: we can hide functionality that exists in the base class, so that it can not be accessed through the derived class, by changing the relevant access specifier.
- e.g.
#include <iostream>
class Base
{
public:
int m_value{};
};
class Derived : public Base
{
private:
using Base::m_value;
public:
Derived(int value) : Base { value }
{
}
};
int main()
{
Derived derived{ 7 };
std::cout << derived.m_value; // error: m_value is private in Derived
Base& base{ derived };
std::cout << base.m_value; // okay: m_value is public in Base
return 0;
}
- Deleting functions in the derived class: we can also mark member functions as deleted in the derived class, which ensures they can’t be called at all through a derived object. By using
deletekeyworld. - We can still call the base class version either by explicitly qualifying it with the base class name or by upcasting the derived object to the base type.
- e.g.
#include <iostream>
class Base
{
private:
int m_value {};
public:
Base(int value)
: m_value { value }
{
}
int getValue() const { return m_value; }
};
class Derived : public Base
{
public:
Derived(int value)
: Base { value }
{
}
int getValue() const = delete; // mark this function as inaccessible
};
int main()
{
Derived derived { 7 };
// The following won't work because getValue() has been deleted!
std::cout << derived.getValue();
// 1. We can call the Base::getValue() function directly
std::cout << derived.Base::getValue();
// 2. we can upcast Derived to a Base reference and getValue() will resolve to Base::getValue()
// Base& ref = static_cast<Base&>(derived);
// std::cout << ref.getValue();
std::cout << static_cast<Base&>(derived).getValue();
return 0;
}
20.7. Multiple inheritance
- C++ provides the ability to do multiple inheritance. Multiple inheritance enables a derived class to inherit members from more than one parent.
- Avoid multiple inheritance unless alternatives lead to more complexity.
21. Virtual Functions - Polymorphism
- A virtual function that resolves to
the most-derived version of the functionthat existsbetween the base and derived class.
C++ allows you to set base class pointers and references to a derived object. This is useful when we want to write a function or array that can work with any type of object derived from a base class. Without virtual functions, base class pointers and references to a derived class will only have access to base class member variables and versions of functions. A virtual function is a special type of function that resolves to the most-derived version of the function (called an override) that exists between the base and derived class. To be considered an override, the derived class function must have the same signature and return type as the virtual base class function. The one exception is for covariant return types, which allow an override to return a pointer or reference to a derived class if the base class function returns a pointer or reference to the base class. A function that is intended to be an override should use the override specifier to ensure that it is actually an override. The final specifier can be used to prevent overrides of a function or inheritance from a class. If you intend to use inheritance, you should make your destructor virtual, so the proper destructor is called if a pointer to the base class is deleted. You can ignore virtual resolution by using the scope resolution operator to directly specify which class’s version of the function you want: e.g. base.Base::getName(). Early binding occurs when the compiler encounters a direct function call. The compiler or linker can resolve these function calls directly. Late binding occurs when a function pointer is called. In these cases, which function will be called can not be resolved until runtime. Virtual functions use late binding and a virtual table to determine which version of the function to call. Using virtual functions has a cost: virtual functions take longer to call, and the necessity of the virtual table increases the size of every object containing a virtual function by one pointer. A virtual function can be made pure virtual/abstract by adding “= 0” to the end of the virtual function prototype. A class containing a pure virtual function is called an abstract class, and can not be instantiated. A class that inherits pure virtual functions must concretely define them or it will also be considered abstract. Pure virtual functions can have a body, but they are still considered abstract. An interface class is one with no member variables and all pure virtual functions. These are often named starting with a capital I. A virtual base class is a base class that is only included once, no matter how many times it is inherited by an object. When a derived class is assigned to a base class object, the base class only receives a copy of the base portion of the derived class. This is called object slicing. Dynamic casting can be used to convert a pointer to a base class object into a pointer to a derived class object. This is called downcasting. A failed conversion will return a null pointer. The easiest way to overload operator« for inherited classes is to write an overloaded operator« for the most-base class, and then call a virtual member function to do the printing.
21.1. Pointers and references to the base class of derived objects
- Pointers, references, and derived classes: We can not only assign
Derived pointers and referencestoDerived objects, but also assignBase pointers and referencestoDerived objects.
chỉ có thể gọi các hàm/thành viên có trong Base -> need virtual Derived chứa phần dữ liệu của Base ở đầu đối tượng, nên con trỏ Base* có thể trỏ đến vùng đó mà không sai về mặt địa chỉ. Compiler đảm bảo phần đầu tiên của Derived có cùng layout với Base.
- e.g.
#include <iostream>
int main()
{
Derived derived{ 5 };
Derived& rDerived{derived};
Derived* rDerived{&derived};
// These are both legal!
Base& rBase{ derived }; // rBase is an lvalue reference (not an rvalue reference)
Base* pBase{ &derived };
std::cout << "derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';
std::cout << "rBase is a " << rBase.getName() << " and has value " << rBase.getValue() << '\n';
std::cout << "pBase is a " << pBase->getName() << " and has value " << pBase->getValue() << '\n';
return 0;
}
- Use for pointers and references to base classes: let us pass any derived object to a single function instead of writing one for each class.
However, since
rBaseis anBased reference, callingBase.fucntion()runsBase::fucntion()unless the function isvirtual.
21.2. Virtual functions and polymorphism
-
Virtual functions:
- is a special type of member function that, when called, resolves to the
most-derived versionof the function for the actual type of the object being referenced or pointed to. - is considered a match if it has the same signature (name, parameter types, and whether it is const) and return type as the base version of the function. Such functions are called overrides.
- to make a function virtual, simply place the
virtualkeyword before the function declaration.
- is a special type of member function that, when called, resolves to the
-
e.g.
#include <iostream>
#include <string_view>
class Base
{
public:
virtual std::string_view getName() const { return "Base"; } // note addition of virtual keyword
virtual ~Base() = default; // virtual destructor
};
class Derived: public Base
{
public:
virtual std::string_view getName() const { return "Derived"; }
};
int main()
{
Derived derived {};
Base& rBase{ derived };
std::cout << "rBase is a " << rBase.getName() << '\n';
return 0;
}
// RESULT: rBase is a Derived
-
Return types of virtual functions: the return type of a virtual function and its override must match, otherwise the compilation will fail.
-
Do not call virtual functions from constructors or destructors: because of the class hadn’t even been created yet
-
polymorphism refers to the ability of an entity to have multiple forms (the term “polymorphism” literally means “many forms”)
- Compile-time polymorphism refers to forms of polymorphism that are resolved by the compiler. These include function overload resolution, as well as template resolution.
- Runtime polymorphism refers to forms of polymorphism that are resolved at runtime. This includes virtual function resolution.
21.3. The override and final specifiers, and covariant return types
-
The override specifier: the
overridespecifier can be applied to any virtual function to tell the compiler to enforce that the function is an override.overridespecifier is placed at the end of a member function declaration.- If a member function is const and an override, the const must come before override.
- We should use the virtual keyword on virtual functions in a base class.
-
The final specifier: The final specifier can be used to tell the compiler to enforce a virtual function to be unable to override.
- used in the same place the override specifier is
-
e.g.
#include <iostream>
using namespace std;
// Base class
class Animal {
public:
virtual void speak() const { cout << "Animal speaking\n"; }
virtual void move() { cout << "Animal moving\n"; }
};
// Derived class
class Dog : public Animal {
public:
// override: tells compiler this must override a virtual function
void speak() const override { cout << "Dog barking\n"; }
// final: prevents further overriding in subclasses
void move() final { cout << "Dog running\n"; }
};
// Further derived class
class Puppy : public Dog {
public:
// OK: override speak()
void speak() const override { cout << "Puppy yapping\n"; }
// ERROR: move() is final in Dog, cannot override
// void move() override { cout << "Puppy hopping\n"; }
};
int main() {
Animal* a = new Puppy();
a->speak(); // → "Puppy yapping"
a->move(); // → "Dog running"
delete a;
}
- Covariant return types
#include <iostream>
#include <string_view>
class Base
{
public:
// This version of getThis() returns a pointer to a Base class
virtual Base* getThis() { std::cout << "called Base::getThis()\n"; return this; }
void printType() { std::cout << "returned a Base\n"; }
};
class Derived : public Base
{
public:
// Normally override functions have to return objects of the same type as the base function
// However, because Derived is derived from Base, it's okay to return Derived* instead of Base*
Derived* getThis() override { std::cout << "called Derived::getThis()\n"; return this; }
void printType() { std::cout << "returned a Derived\n"; }
};
int main()
{
Derived d{};
Base* b{ &d };
d.getThis()->printType(); // calls Derived::getThis(), returns a Derived*, calls Derived::printType
b->getThis()->printType(); // calls Derived::getThis(), returns a Base*, calls Base::printType
return 0;
}
21.4. Virtual destructors, virtual assignment, and overriding virtualization
- Virtual destructors: in the case that you will want to provide your own destructor (particularly if the class needs to deallocate memory).
- e.g.
#include <iostream>
class Base
{
public:
// virtual ~Base() = default; // generate a virtual default destructor
virtual ~Base() // note: virtual
{
std::cout << "Calling ~Base()\n";
}
};
class Derived: public Base
{
private:
int* m_array {};
public:
Derived(int length)
: m_array{ new int[length] }
{
}
virtual ~Derived() // note: virtual
{
std::cout << "Calling ~Derived()\n";
delete[] m_array;
}
};
int main()
{
Derived* derived { new Derived(5) };
Base* base { derived };
delete base;
return 0;
}
// OUTPUT:
// Calling ~Derived()
// Calling ~Base()
- Ignoring virtualization: incase we want to ignore the virtualization of a function.
- e.g.
#include <string_view>
class Base
{
public:
virtual ~Base() = default;
virtual std::string_view getName() const { return "Base"; }
};
class Derived: public Base
{
public:
virtual std::string_view getName() const { return "Derived"; }
};
#include <iostream>
int main()
{
Derived derived {};
const Base& base { derived };
// Calls Base::getName() instead of the virtualized Derived::getName()
std::cout << base.Base::getName() << '\n';
return 0;
}
If we intend our class to be inherited from, make sure our destructor is virtual and public. If we do not intend our class to be inherited from, mark our class as final.
21.5. Early binding and late binding
-
early binding (static binding): when a direct call is made to a non-member function or a non-virtual member function, the compiler can determine which function definition should be matched to the call. -
e.g.
#include <iostream> using namespace std; class Animal { public: void speak() { // NOT virtual cout << "Animal speaks\n"; } }; class Dog : public Animal { public: void speak() { // Hides Animal::speak() cout << "Dog barks\n"; } }; void print(int x) { cout << "int\n"; } void print(double x) { cout << "double\n"; } int main() { Animal* a = new Dog(); a->speak(); // Early binding: Non-virtual member function print(5); // Early binding: int version chosen at compile time } -
late binding (or in the case of virtual function resolution, dynamic dispatch): when a function call can’t be resolved until runtime.#include <iostream> using namespace std; class Animal { public: virtual void speak() { // Virtual! cout << "Animal speaks\n"; } }; class Dog : public Animal { public: void speak() override { cout << "Dog barks\n"; } }; int main() { Animal* a = new Dog(); a->speak(); // Late binding } -
virtual tableis a lookup table of functions used to resolve function calls in a dynamic/late binding manner.
Early binding/static dispatch = direct function call overload resolution Late binding = indirect function call resolution Dynamic dispatch = virtual function override resolution
21.6. Pure virtual functions, abstract base classes, and interface classes
Pure virtual (abstract) functions and abstract base classes(like abstract class/function in java):- syntax: assign the function the value 0. e.g.
virtual int getValue() const = 0; // a pure virtual function - the class will becomes and abstract base class, it can not be instantiated. (create a object using abstract base class as the type)
- the derived class must define a body for the pure virtual function, if not, it will be considered abstract base class as well.
- Any class with pure virtual functions should also have a virtual destructor
- syntax: assign the function the value 0. e.g.
Pure virtual functions with definitions(like default in java):- We can create pure virtual functions that have definitions. Abstract function
- When providing a definition for a pure virtual function, the definition must be provided separately (not inline)
- e.g.
#include <iostream>
#include <string>
#include <string_view>
class Animal // This Animal is an abstract base class
{
protected:
std::string m_name {};
public:
Animal(std::string_view name)
: m_name(name)
{
}
const std::string& getName() const { return m_name; }
virtual std::string_view speak() const = 0; // note that speak is a pure virtual function
virtual ~Animal() = default;
};
std::string_view Animal::speak() const
{
return "buzz"; // some default implementation
}
class Dragonfly: public Animal
{
public:
Dragonfly(std::string_view name)
: Animal{name}
{
}
std::string_view speak() const override// this class is no longer abstract because we defined this function
{
return Animal::speak(); // use Animal's default implementation
}
};
int main()
{
Dragonfly dfly{"Sally"};
std::cout << dfly.getName() << " says " << dfly.speak() << '\n';
return 0;
}
Interface classes: is a class that has no member variables, and where all of the functions are pure virtual- e.g.
#include <string_view>
class IErrorLog
{
public:
virtual bool openLog(std::string_view filename) = 0;
virtual bool closeLog() = 0;
virtual bool writeError(std::string_view errorMessage) = 0;
virtual ~IErrorLog() {} // make a virtual destructor in case we delete an IErrorLog pointer, so the proper derived destructor is called
};
Virtual base classes:- are used in virtual inheritance in a way of preventing multiple “instances” of a given class appearing in an inheritance hierarchy when using multiple inheritances. (e.g. we may want only one copy of
PoweredDevice(contructing) to be shared by bothScannerandPrinter. ) - using
virtualkeyworld in the inheritance list of the derived class - there is only one base object. The base object is shared between all objects in the inheritance tree and it is only constructed once
- for the constructor of the most derived class, virtual base classes are always created before non-virtual base classes, which ensures all bases get created before their derived classes.
- the most derived class is responsible for constructing the virtual base class. (
Copieris responsible for creatingPoweredDevice)
- are used in virtual inheritance in a way of preventing multiple “instances” of a given class appearing in an inheritance hierarchy when using multiple inheritances. (e.g. we may want only one copy of
- e.g.
class PoweredDevice
{
};
class Scanner: virtual public PoweredDevice
{
};
class Printer: virtual public PoweredDevice
{
};
class Copier: public Scanner, public Printer
{
};
21.7. Object slicing
slicingmeans the assigning of a Derived class object to a Base class object is called object.- When a derived class object is assigned to a base class object in C++, the derived class object’s extra attributes are sliced off (not considered) to generate the base class object
- e.g.
#include <iostream>
#include <iostream>
#include <string_view>
class Base
{
protected:
int m_value{};
public:
Base(int value)
: m_value{ value }
{
}
virtual ~Base() = default;
virtual std::string_view getName() const { return "Base"; }
int getValue() const { return m_value; }
};
class Derived: public Base
{
public:
Derived(int value)
: Base{ value }
{
}
std::string_view getName() const override { return "Derived"; }
};
// slicing is much more likely to occur accidentally with
void printNameAc(const Base base) // note: base passed by value, not reference
{
std::cout << "Name is a " << base.getName() << '\n';
}
// slicing here can all be easily avoided by making the function parameter a reference instead of a pass by value
void printName(const Base& base) // note: base now passed by reference
{
std::cout << "Name is a " << base.getName() << '\n';
}
int main()
{
Derived derived{ 5 };
std::cout << "derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';
Base& ref{ derived };
std::cout << "ref is a " << ref.getName() << " and has value " << ref.getValue() << '\n';
Base* ptr{ &derived };
std::cout << "ptr is a " << ptr->getName() << " and has value " << ptr->getValue() << '\n';
// Because ref and ptr are of type Base, ref and ptr can only see the Base part of derived -- the Derived part of derived still exists, but simply can’t be seen through ref or // ptr. However, through use of virtual functions, we can access the most-derived version of a function.
Base base{ derived }; // what happens here?
std::cout << "base is a " << base.getName() << " and has value " << base.getValue() << '\n';
// only the Base portion of the Derived object is copied. The Derived portion is not
// WARNING
printNameAc(derived); // we expect that this should print "Name is a Derived", but accidentally led to unbehaviour // Derived
// OK
printName(derived);
return 0;
}
// OUTPUT
derived is a Derived and has value 5
ref is a Derived and has value 5
ptr is a Derived and has value 5
base is a Base and has value 5
Name is a Base
Name is a Derived
21.8. Dynamic casting
- Parent -> Child: DownCasting
- Child -> Parent: UpCasting
- We can use
dynamic_castto perform downcasting.- If a dynamic_cast fails, the result of the conversion will be a null pointer.
- e.g.
int main()
{
Base* b{ getObject(true) };
Derived* d{ dynamic_cast<Derived*>(b) }; // use dynamic cast to convert Base pointer into Derived pointer
if (d) // make sure d is non-null
std::cout << "The name of the Derived is: " << d->getName() << '\n';
// DOwncasting for references
Derived apple{1, "Apple"}; // create an apple
Base& b{ apple }; // set base reference to object
Derived& d{ dynamic_cast<Derived&>(b) }; // dynamic cast using a reference instead of a pointer
std::cout << "The name of the Derived is: " << d.getName() << '\n'; // we can access Derived::getName through d
delete b;
return 0;
}
-
We can also use the
static_castto do perform downcasting.- it will “succeed” even if the Base pointer isn’t pointing to a Derived object.
- the result will be in undefined behavior
-
dynamic_cast vs static_cast: usestatic_castunless you’redowncasting, in which casedynamic_castis usually a better choice. However, you should also consider avoiding casting altogether and just usevirtual functions.
21.8. Printing inherited classes using operator«
- Refer to the learncpp.com
22. Overloading Operators
-
Almost any existing operator in C++ can be overloaded. The exceptions are:
conditional (?:), sizeof, scope (::), member selector (.), pointer member selector (.*), typeid, and the casting operators. -
We can only overload the operators that exist.
-
At least one of the operands in an overloaded operator must be a user-defined type.
-
Overloading the plus operator (+) is as simple as declaring a function named
operator+, giving it two parameters of the type of the operands we want to add, pickingan appropriate return type, and then writing the function. -
Not everything can be overloaded as a friend function: The assignment (=), subscript ([]), function call (()), and member selection (->) operators must be overloaded as member functions, because the language requires them to be.
The following rules of thumb can help you determine which form is best for a given situation: If you’re overloading assignment (=), subscript ([]), function call (()), or member selection (->), do so as a member function. If you’re overloading a unary operator, do so as a member function. If you’re overloading a binary operator that does not modify its left operand (e.g. operator+), do so as a normal function (preferred) or friend function. If you’re overloading a binary operator that modifies its left operand, but you can’t add members to the class definition of the left operand (e.g. operator«, which has a left operand of type ostream), do so as a normal function (preferred) or friend function. If you’re overloading a binary operator that modifies its left operand (e.g. operator+=), and you can modify the definition of the left operand, do so as a member function.
- e.g.
#include <iostream>
class Cents
{
private:
int m_cents {};
public:
Cents(int cents) : m_cents{ cents } { }
// add Cents + Cents using a friend function
friend Cents operator+(const Cents& c1, const Cents& c2);
// 3. Using member function
// Overload Cents + int
Cents operator+(int value) const;
int getCents() const { return m_cents; }
};
// 1. Using friend function note: this function is not a member function!
Cents operator+(const Cents& c1, const Cents& c2)
{
// use the Cents constructor and operator+(int, int)
// we can access m_cents directly because this is a friend function
return c1.m_cents + c2.m_cents;
}
// 2. Using normal function
// note: this function is not a member function nor a friend function!
Cents operator-(const Cents& c1, const Cents& c2)
{
// use the Cents constructor and operator+(int, int)
// we don't need direct access to private members here
return { c1.getCents() - c2.getCents() };
}
// 3. Using member function
// note: this function is a member function!
// the cents parameter in the friend version is now the implicit *this parameter
Cents Cents::operator+ (int value) const
{
return Cents { m_cents + value };
}
int main()
{
Cents cents1{ 6 };
Cents cents2{ 8 };
Cents centsSum{ cents1 + cents2 };
std::cout << "I have " << centsSum.getCents() << " cents.\n";
Cents centsSub{ cents1 - cents2 };
std::cout << "I have " << centsSub.getCents() << " cents.\n";
const Cents cents3 { cents1 + 2 };
std::cout << "I have " << cents3.getCents() << " cents.\n";
return 0;
}
22.1. Arithmetic Using Friend Functions ( + - * /)
- All of the arithmetic operators are binary operators, so they take two operands.
- There are 2 common ways to overide the operators
- Member function: The assignment (=), subscript ([]), function call (()), and member selection (->) operators must be overloaded as member functions, because the language requires them to be.
- Nonmember function (friend): we won’t be able to use a member overload if the left operand is either not a class (e.g. int), or it is a class that we can’t modify (e.g. std::ostream).
- A member operator only works if the left side is an object of the class.
- e.g.
23. I/O
23.1. I/O Streams
-
It is a part of the STL.
-
I/O is implemented with
streams. -
stream is a sequence of bytes that can be accessed sequentially. It may produce or consume amounts data over time.
- input stream: used to hold input data from a data producer.
- output stream: used to hold output data for a particular data consumer.
- e.g. when writing/input data to an device, the device may not be ready to accept that data, so the data will sit in the stream.
- [C++] <=> streams <=> [os]
-
I/O in C++: we can use the STL classes to deal with streams
- input stream:
istream& extraction operation (>>) is used to remove values from the stream - output stream:
ostream& insertio operation (<<) is used to put vlaues in the stream.\ iostreamcan handle both i & o
- input stream:
-
Standard streams:
cin: anistreamobject tied to the standard inputcout: anostreamobject tied to the standard outputcerr: anostreamobject tied to the standard error - unbuffered outputclog: anostreamobject tied to the standard error - buffered output
23.2. Input with istream
- Use
extraction operator (>>)to read information from an input stream. It skips whitespace (blanks, tabs, and newlines). Useget(), getLine()to not discard the whitespace. manipulatoris an object that is used to modify a stream when applied with theextraction (>>)orinsertion (<<)operators.
23.3. Output with ostream
https://www.learncpp.com/cpp-tutorial/output-with-ostream-and-ios/
23.4. Stream classes for string
-
String streams provide a buffer to hold data but are not connected to an I/O channel.
-
The primary uses of the string stream:
- Display data later then or proccess i/o data.
- Get data into a string stream
- Get data from a string stream
- Convertion strings/numbers
- Clear a string stream
-
e.g:
#include <sstream>
#include <string>
int main() {
std::stringstream os{};
// input
os << "0xF";
std::cout << os.str();
os.str("0x1 0x2");
std::cout << os.str();
// output
std::string bytesStr = os.str();
std::cout << bytesStr;
os.str("0x0 0xF 0xE 0x2");
os >> bytesStr;
std::cout <<bytesStr;
// conversions
int byte_low = 0xFFF;
int byte_high = 0x001;
os.clear();
os << byte_low << ' ' << byte_high;
std::cout << os.str();
}
23.5. Validation
https://www.learncpp.com/cpp-tutorial/stream-states-and-input-validation/
23.6. File I/O
-
There are three basics file I/O classes:
ifstream: derived fromistreamofstream: derived fromostreamfstream: derived fromstream
-
important: We have to explicitly setup to use file streams:
-
- Open a file: instantiate an object of file I/O class, with the name of the file as a param
-
- Use the insertion («) or extraction (») to r/w
-
- Close a file: call the close() or let file I/O go out of scope.
-
- To delete a file, simply use the remove() function.
- To delete a file, simply use the remove() function.
-
-
File output:
#include <fstream>
#include <iostream>
#include <string>
static void fileOutput() {
std::ofstream outfile{"grs_bytes.csv"};
if (!outfile || !outfile.is_open()) {
std::cerr << "[E] Cannot create the output file";
return;
}
// Put bytes data to the file
// put string
std::string elfBytes{
R"""(
time_s,
gsr_value 0.0, 45.27761157741693 0.005, 41.69912812397066 0.01,
38.13110177547114 0.015, 35.32162785580394 0.02,
31.75617843363382 0.025, 28.352875321528607 0.03,
25.23210282654006 0.035, 21.769688905641132 0.04,
17.99031153059391 0.045, 15.073732055666543 0.05,
15.13550182371759 0.055, 14.69547985289048 0.06,
14.867397107985468 0.065, 14.982082556093832 0.07,
14.893751010484861 0.075, 14.877034044202343 0.08,
14.820590581790071 0.085, 15.1065350504897 0.09,
15.152287796727098 0.095, 14.764395300078201 0.1,
15.118189654760348 0.105, 15.473255351586635 0.11,
14.913896402347113
\n )"""};
outfile << elfBytes;
elfBytes =
"0x0 0x0"
"0x0 0x0"
"0x0 0x0";
outfile << elfBytes;
elfBytes =
"0xF 0xA \
0xE 0xB \
0x0 0x0";
outfile << elfBytes;
outfile.put('E'); // put char
outfile.close();
}
- File input:
static void fileInput() {
std::ifstream inFile{"grs_bytes.csv"};
if (!inFile.is_open()) {
std::cerr << "Error to open file";
}
std::string inputStr{};
std::cout << "========" << std::endl;
while (
inFile >>
inputStr) { // Note that ifstream returns a 0 if we’ve reached the end of the file (EOF)
std::cout << inputStr;
}
std::cout << "========" << std::endl;
// not skip whitespace
inFile.close();
inFile.open("grs_bytes.csv"); // explicitly call open()
inputStr.clear();
while (std::getline(inFile, inputStr)) {
std::cout << inputStr << std::endl;
}
std::cout << "========" << std::endl;
inFile.close();
}
- File Mode:
app: opens the file in append modeate: seeks to the end of the file before reading/writingbinary: opens the file in binary mode (instead of text mode)in: opens the file in read mode (default for ifstream)out: opens the file in write mode (default for ofstream)trunc: erases the file if it already exists
23.6 Ramdom File I/O
- File pointer: Each file stream class contains a file pointer that is used to keep track of the current read/write position within the file
- Ramdom access with
seekg() & seekp() - IOS seek flags:
beg: the offset is relative to the beginning of the file (default)cur: the offset is relative to the current location of the file pointerend: the offset is relative to the end of the file
In programming, a newline (‘\n’) is actually an abstraction. On Windows, a newline is represented as sequential CR (carriage return) and LF (line feed) characters (thus taking 2 bytes of storage). On Unix, a newline is represented as a LF (line feed) character (thus taking 1 byte of storage).
// assume iofile is an object of type fstream iofile.seekg(iofile.tellg(), std::ios::beg); // seek to current file position