Setup
Why C Matters for Quant Finance
Every serious pricing engine, Monte Carlo simulator, and finite difference solver in production is written in C or C++. The reason is not tradition — it is arithmetic throughput. A Monte Carlo pricer running 100,000 paths with 252 time steps per path executes roughly 25 million floating-point operations per call. In VBA that takes 15–20 seconds. In Python with NumPy it takes 1–3 seconds. In compiled C++ it takes under 100 milliseconds. That gap is the difference between a model you can run interactively on a desk and one you cannot.
C++ is a superset of C. Everything in this module is valid C++ (targeting C++17), and understanding it is a prerequisite for the object-oriented and template material that follows. In particular, pointers and manual memory management are concepts that the C++ standard library abstracts for you — but you cannot reason about std::vector, std::unique_ptr, or move semantics without first understanding what they are abstracting.
This module covers the language foundations from which all numerical C++ code is built: fundamental types, arrays, pointers, dynamic memory, the stack/heap distinction, functions, const correctness, and structs.
Theory
1. Fundamental Types and Variables
C++ provides a set of built-in arithmetic types. For numerical finance, three matter almost exclusively:
| Type | Typical size | Typical range / precision | Use case |
|---|---|---|---|
double | 8 bytes | ~15–17 significant decimal digits, ≈ ±1.8×10³⁰⁸ | Option prices, rates, vol, Greeks |
int | 4 bytes | −2,147,483,648 to 2,147,483,647 | Loop counters, path counts |
bool | 1 byte | true / false | Flags, option type (call/put) |
char | 1 byte | −128 to 127 (or 0–255 unsigned) | Characters, small integers |
The size of a type is implementation-defined — the standard guarantees relative ordering (sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long)) but not absolute sizes. Use sizeof to query at runtime, or prefer <cstdint> types (int32_t, int64_t) when exact widths matter.
#include <iostream>
int main() {
double spot = 105.0; // underlying price
double strike = 100.0; // option strike
int num_paths = 100000; // MC path count
bool is_call = true; // option type flag
std::cout << "sizeof(double) = " << sizeof(double) << "\n"; // 8
std::cout << "sizeof(int) = " << sizeof(int) << "\n"; // 4
std::cout << "sizeof(bool) = " << sizeof(bool) << "\n"; // 1
return 0;
}
Variables must be declared before use. C++17 requires declaration at the point of first use (no separate declaration/definition separation for locals). Always initialise: an unread local variable of scalar type holds an indeterminate value — reading it is undefined behaviour (UB).
2. Arrays
An array is a fixed-size, contiguous block of memory holding elements of a single type.
double weights[5] = {0.2, 0.2, 0.2, 0.2, 0.2}; // portfolio weights, sum = 1
Key properties:
- Zero-indexed:
weights[0]throughweights[4]. Accessingweights[5]is out-of-bounds — undefined behaviour. - Fixed size at compile time: the size must be a compile-time constant (or, as a compiler extension, VLAs — but these are non-standard in C++ and should not be used).
- Array name decays to a pointer:
weightsin most contexts is equivalent to&weights[0], adouble*. This decay loses the size information — a function receivingdouble*does not know how many elements are in the array.
Two-dimensional arrays are stored in row-major order: double M[3][4] occupies 12 doubles contiguously; M[i][j] is at offset i * 4 + j from &M[0][0]. This has cache-locality implications: iterating over columns in the inner loop of a matrix operation is cache-unfriendly.
// Row-major access — cache-friendly (inner loop over j, row i fixed)
double A[3][4] = {};
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 4; ++j)
A[i][j] = static_cast<double>(i * 4 + j);
3. Pointers
A pointer is a variable that stores the memory address of another variable.
double spot = 105.0;
double* p = &spot; // p holds the address of spot
// Dereference: access the value at the address
*p = 106.0; // modifies spot through the pointer
std::cout << spot; // prints 106.0
Pointer arithmetic is defined in units of the pointed-to type:
double weights[5] = {0.1, 0.2, 0.3, 0.2, 0.2};
double* p = weights; // p points to weights[0]
double* q = p + 1; // q points to weights[1]; advances by sizeof(double) = 8 bytes
double* end = weights + 5; // one-past-the-end sentinel; valid address, must not be dereferenced
This is the mechanism behind std::vector's iterator arithmetic. The pointer difference end - weights equals 5 (the count of elements).
The null pointer (nullptr, introduced in C++11) represents "no object". A pointer that does not yet point to a valid object should be set to nullptr. Dereferencing nullptr is undefined behaviour (on most platforms: a segfault).
double* p = nullptr;
if (p != nullptr)
*p = 3.14; // only dereference after null check
A dangling pointer is a pointer that previously held a valid address but whose target has since been destroyed (e.g., a local variable that has gone out of scope, or heap memory that has been freed). Dereferencing a dangling pointer is undefined behaviour — the memory may have been reused for something else, producing silent data corruption rather than a crash.
4. Dynamic Memory: new and delete
Variables declared locally live on the stack and are destroyed when the enclosing scope exits. To allocate memory whose lifetime is controlled explicitly (surviving scope exit, or sized at runtime), use the heap via new and delete.
// Allocating a single double on the heap
double* p = new double(3.14); // allocates sizeof(double) bytes; initialises to 3.14
*p = 2.72;
delete p; // releases the memory; p is now dangling — set to nullptr
p = nullptr;
// Allocating an array of n doubles on the heap
int n = 252; // trading days
double* path = new double[n](); // () zero-initialises
path[0] = 100.0;
// ... fill path ...
delete[] path; // MUST use delete[] for arrays; delete p is UB
path = nullptr;
Memory leak: if delete is omitted, the memory is never returned to the OS for the duration of the process. In a long-running server process (a pricing service, a risk engine), leaks accumulate until the process is killed or runs out of memory.
Double-free: calling delete twice on the same pointer is undefined behaviour. The heap's bookkeeping data structures can be corrupted, leading to arbitrary memory corruption later.
Rule of thumb for production C++: do not use new/delete directly. Use std::vector<double> for dynamic arrays and std::unique_ptr<T> for single objects. This module teaches the raw mechanics as foundation; the idiomatic replacements are covered in the classes module.
5. Stack vs. Heap
Understanding where data lives determines performance, safety, and lifetime semantics.
Stack:
- Allocation mechanism: the stack pointer register is decremented (on most architectures) when a function is called. Local variable space is reserved in the stack frame. On return, the stack pointer is restored — all locals are destroyed in O(1).
- Speed: essentially free. No syscall, no bookkeeping. The stack pointer move is one instruction.
- Size limit: typically 1–8 MB per thread (OS-configurable). A
double path[1000000](≈ 8 MB) on the stack will likely cause a stack overflow. - Lifetime: tied to the enclosing scope. Cannot be extended.
Heap:
- Allocation mechanism:
newcallsmallocunder the hood, which invokesbrk/mmapsyscalls (on Linux) to request pages from the OS, then maintains a free-list.new double[n]may take microseconds on the first call for a given size. - Speed: slower than stack allocation; not negligible in tight loops. Allocating and freeing millions of small objects in a Monte Carlo loop can dominate runtime.
- Size: limited by available RAM and virtual address space (effectively unbounded for practical purposes).
- Lifetime: until
deleteis called. Can outlive any scope, be passed between threads, be stored in global data structures.
Stack (per thread, ~1–8 MB):
┌────────────────────┐ ← stack pointer (grows down on x86)
│ local variables │
│ return addresses │
│ function params │
└────────────────────┘
Heap (process-wide, ~GBs):
┌────────────────────────────────────────────┐
│ /// allocated /// free /// ... │
└────────────────────────────────────────────┘
Practical rule: declare arrays whose size is known and small at compile time on the stack. Allocate arrays whose size is runtime-determined, or that need to survive the current scope, on the heap (via std::vector).
6. Functions: Pass by Value, Reference, and Const Reference
C++ functions accept arguments in three fundamentally different ways, with different performance and semantic implications.
Pass by value: a copy of the argument is made. The function works on the copy. The original is unmodified.
// Pass by value — spot is copied into the function parameter
double square(double x) { return x * x; }
Use for: small, cheap-to-copy types (scalars: int, double, bool).
Pass by reference: the function receives an alias to the original variable. Modifications inside the function affect the caller's variable.
// Pass by reference — modifies the caller's variable
void scale_portfolio(double* weights, int n, double factor) {
for (int i = 0; i < n; ++i)
weights[i] *= factor;
}
Or with reference syntax (equivalent, but no pointer arithmetic):
void rescale(double& x, double factor) { x *= factor; }
Use for: when the function must modify the caller's data (output parameters).
Pass by const reference: the function receives a read-only alias. No copy is made; no modification is permitted.
// Pass by const reference — no copy of the vector; cannot be modified
double portfolio_value(const double* weights, const double* prices, int n) {
double pv = 0.0;
for (int i = 0; i < n; ++i)
pv += weights[i] * prices[i];
return pv;
}
Use for: any type larger than a pointer (8 bytes on 64-bit) that the function should not modify. For a double (also 8 bytes), there is no performance difference between value and const reference; by convention, scalars are passed by value.
7. The const Qualifier
const is a promise enforced by the compiler: the qualified entity will not be modified. Applying const as broadly as possible documents intent and catches accidental mutations at compile time rather than runtime.
const double pi = 3.14159265358979323846; // compile-time constant; cannot be modified
Pointer to const: the value pointed to cannot be modified through this pointer, but the pointer itself can be reseated.
const double* p = &spot;
// *p = 110.0; // COMPILE ERROR: assignment of read-only location
p = &strike; // OK: p can point elsewhere
Const pointer: the pointer address cannot be changed, but the value it points to can.
double* const p = &spot;
*p = 110.0; // OK: value is mutable
// p = &strike; // COMPILE ERROR: assignment of read-only pointer
Const pointer to const: neither the address nor the value can be changed.
const double* const p = &spot;
// *p = 110.0; // COMPILE ERROR
// p = &strike; // COMPILE ERROR
Mnemonic: read from right to left. const double* const p reads as "p is a const pointer to a const double."
The practical rule: function parameters that are pointers or references through which the function will not write should always be const. This is not pedantry — it enables the compiler to catch bugs and communicate the function's contract to readers.
8. Structs
A struct is a named aggregate type grouping related data under a single name.
struct OptionParams {
double spot;
double strike;
double rate; // continuously compounded, annualised
double vol; // annualised implied vol (decimal, not percent)
double maturity; // time to expiry in years
bool is_call;
};
Members are accessed with the . operator on an object, or -> through a pointer:
OptionParams opt;
opt.spot = 105.0;
opt.strike = 100.0;
opt.rate = 0.05;
opt.vol = 0.20;
opt.maturity = 0.5;
opt.is_call = true;
OptionParams* ptr = &opt;
ptr->vol = 0.22; // equivalent to (*ptr).vol = 0.22
The key difference between struct and class in C++ is the default access level: struct members are public by default; class members are private by default. In all other respects they are identical.
Passing a struct by const reference avoids copying all its members:
double compute_intrinsic(const OptionParams& p) {
double intrinsic = p.is_call ? p.spot - p.strike : p.strike - p.spot;
return intrinsic > 0.0 ? intrinsic : 0.0;
}
Implementation
The following complete, compilable C++17 program demonstrates all concepts covered above. Every function is motivated by a quantitative finance context.
// foundations_demo.cpp
// Compile: g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -o foundations_demo foundations_demo.cpp
// Sanitizers (recommended during development): add -fsanitize=address,undefined
#include <iostream>
#include <cmath>
#include <cassert>
#include <cstddef>
// ---------------------------------------------------------------------------
// Struct: encapsulates Black-Scholes input parameters cleanly
// ---------------------------------------------------------------------------
struct OptionParams {
double spot; // current underlying price (S)
double strike; // option strike (K)
double rate; // risk-free rate, continuously compounded, annualised
double vol; // implied volatility, annualised (decimal: 0.20 = 20%)
double maturity; // time to expiry in years (τ)
bool is_call; // true = call, false = put
};
// ---------------------------------------------------------------------------
// Dot product: u · v = Σ u_i * v_i over n elements
// Used e.g. to compute portfolio value = weights · prices
// Pass by const pointer — function reads but does not modify the arrays.
// ---------------------------------------------------------------------------
double dot_product(const double* u, const double* v, int n) {
double result = 0.0;
for (int i = 0; i < n; ++i)
result += u[i] * v[i];
return result;
}
// ---------------------------------------------------------------------------
// Pointer arithmetic demo: iterate over a factor-loading array
// without using subscript notation.
// ---------------------------------------------------------------------------
double sum_array(const double* begin, const double* end) {
// begin and end are pointers; pointer subtraction gives element count
double total = 0.0;
for (const double* p = begin; p != end; ++p)
total += *p;
return total;
}
// ---------------------------------------------------------------------------
// Pass by reference: rescales a portfolio's weight vector in place.
// The double* is non-const because we intend to modify the elements.
// ---------------------------------------------------------------------------
void normalise_weights(double* weights, int n) {
double s = sum_array(weights, weights + n);
if (s == 0.0) return; // guard against zero-sum (all-zero weights)
for (int i = 0; i < n; ++i)
weights[i] /= s;
}
// ---------------------------------------------------------------------------
// Stack vs heap: allocate a path on the stack (fixed n) vs the heap
// (runtime n). For n = 252 (trading days) the stack version is fine;
// for n = 1,000,000 paths the heap is mandatory.
// ---------------------------------------------------------------------------
void stack_allocation_demo() {
const int N = 252;
double path[N]; // stack-allocated; destroyed on function return
path[0] = 100.0;
for (int i = 1; i < N; ++i)
path[i] = path[i - 1] * 1.001; // naive 0.1% daily drift
std::cout << "Stack path[251] = " << path[N - 1] << "\n";
// path is destroyed here — any pointer to path[0] would become dangling
}
void heap_allocation_demo(int n) {
double* path = new double[n](); // heap-allocated; zero-initialised
path[0] = 100.0;
for (int i = 1; i < n; ++i)
path[i] = path[i - 1] * 1.001;
std::cout << "Heap path[" << n - 1 << "] = " << path[n - 1] << "\n";
delete[] path; // explicit deallocation; omitting this is a memory leak
path = nullptr; // set to nullptr to prevent accidental use of dangling pointer
}
// ---------------------------------------------------------------------------
// const correctness: intrinsic value of an option — reads params, never writes
// ---------------------------------------------------------------------------
double intrinsic_value(const OptionParams& p) {
const double diff = p.is_call ? p.spot - p.strike : p.strike - p.spot;
return diff > 0.0 ? diff : 0.0;
}
// ---------------------------------------------------------------------------
// Struct usage: compute d1 and d2 (Black-Scholes auxiliary quantities)
// d1 = [ln(S/K) + (r + σ²/2)τ] / (σ√τ)
// d2 = d1 − σ√τ
// Both are used in every Black-Scholes Greek — centralising them in a struct
// avoids recomputing them and prevents sign errors from duplicated formulas.
// ---------------------------------------------------------------------------
struct BSAux {
double d1;
double d2;
double sqrtT; // σ√τ — stored to avoid recomputation
};
BSAux compute_d1d2(const OptionParams& p) {
BSAux aux;
aux.sqrtT = p.vol * std::sqrt(p.maturity);
aux.d1 = (std::log(p.spot / p.strike) + (p.rate + 0.5 * p.vol * p.vol) * p.maturity)
/ aux.sqrtT;
aux.d2 = aux.d1 - aux.sqrtT;
return aux;
}
// ---------------------------------------------------------------------------
// main: exercise all of the above with known inputs
// ---------------------------------------------------------------------------
int main() {
// --- Dot product ---
const double weights[5] = {0.15, 0.25, 0.30, 0.20, 0.10};
const double prices[5] = {42.0, 35.5, 120.0, 67.0, 88.5};
const double pv = dot_product(weights, prices, 5);
std::cout << "Portfolio value (weights · prices) = " << pv << "\n";
// Expected: 0.15*42 + 0.25*35.5 + 0.30*120 + 0.20*67 + 0.10*88.5
// = 6.30 + 8.875 + 36.00 + 13.40 + 8.85 = 73.425
// --- Pointer arithmetic + sum ---
const double factor_loadings[6] = {0.8, 1.1, 0.6, 1.3, 0.9, 1.0};
const double total = sum_array(factor_loadings, factor_loadings + 6);
std::cout << "Sum of factor loadings = " << total << "\n"; // 5.7
// --- Normalise weights in place ---
double w[3] = {1.0, 2.0, 3.0};
normalise_weights(w, 3);
std::cout << "Normalised weights: " << w[0] << ", " << w[1] << ", " << w[2] << "\n";
// Expected: 1/6 ≈ 0.1667, 2/6 ≈ 0.3333, 3/6 = 0.5
// --- Stack vs heap ---
stack_allocation_demo();
heap_allocation_demo(252);
// --- Intrinsic value ---
OptionParams call_atm = {100.0, 100.0, 0.05, 0.20, 0.5, true};
OptionParams put_itm = {90.0, 100.0, 0.05, 0.20, 0.5, false};
std::cout << "ATM call intrinsic = " << intrinsic_value(call_atm) << "\n"; // 0
std::cout << "ITM put intrinsic = " << intrinsic_value(put_itm) << "\n"; // 10
// --- d1/d2 computation ---
const BSAux aux = compute_d1d2(call_atm);
std::cout << "d1 = " << aux.d1 << ", d2 = " << aux.d2 << "\n";
// For S=K=100, r=0.05, σ=0.20, τ=0.5:
// d1 = (0 + (0.05 + 0.02)*0.5) / (0.20*√0.5) = 0.035 / 0.14142 ≈ 0.2475
// d2 = 0.2475 − 0.14142 ≈ 0.1061
return 0;
}
Compile and run:
g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -o foundations_demo foundations_demo.cpp
./foundations_demo
With AddressSanitizer (recommended during development to catch memory errors):
g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -fsanitize=address,undefined \
-o foundations_demo foundations_demo.cpp
./foundations_demo
Validation
Dot product check: dot_product([0.15, 0.25, 0.30, 0.20, 0.10], [42, 35.5, 120, 67, 88.5], 5) should return 73.425. The arithmetic:
Weight normalisation: input [1, 2, 3], sum = 6. Output [1/6, 1/3, 1/2] ≈ [0.1667, 0.3333, 0.5000]. Verify sum_array of the output returns 1.0.
d1/d2: for , , , :
Memory safety: run under Valgrind to detect leaks:
valgrind --leak-check=full --error-exitcode=1 ./foundations_demo
Expected output: "no leaks are possible" — all new[] calls are matched with delete[].
Compilation standard: all examples in this module compile under C++17 with -Wall -Wextra -Wpedantic -Werror without warnings or errors.
Limitations and Pitfalls
Array decay loses size information
When a C-style array is passed to a function, it decays to a pointer. The function receives double* — it does not know how many elements are present. This is why every array-processing function above takes a separate int n (count) or an end pointer. Passing the wrong count causes out-of-bounds access — undefined behaviour with no compiler diagnostic.
Production remedy: use std::vector<double> (carries its own size via .size()) or std::array<double, N> (fixed-size with size in the type). C-style arrays are appropriate only for very small compile-time-sized buffers or when interfacing with C APIs.
Manual memory management is error-prone
Raw new/delete is taught here as mechanical foundation, not as recommended practice. In production C++:
- Use
std::vector<double>for all dynamic arrays. It manages memory automatically via RAII (Resource Acquisition Is Initialisation): its destructor callsdelete[]unconditionally when the vector goes out of scope. - Use
std::unique_ptr<T>for single-object heap allocation. Ownership is explicit and transferred cleanly.
The specific failure modes to avoid with raw new/delete:
- Memory leak:
newwithout a matchingdelete. The allocator is not freed for the lifetime of the process. - Double-free:
deletecalled twice on the same pointer. Corrupts the heap allocator's free list. Produces undefined behaviour that may manifest far from the corrupting line. - Use-after-free: dereferencing a pointer after the memory has been freed. The memory may have been reused; you are reading or writing someone else's data.
- Array/scalar mismatch: using
deleteinstead ofdelete[]on an array allocation. The destructor for each element is only called for the first element; the rest leak.
Signed/unsigned comparison
Mixing int (signed) with std::size_t / array sizes (unsigned) produces compiler warnings and can cause subtle bugs when a negative signed value is implicitly converted to a very large unsigned value. Use static_cast<int>(v.size()) or std::ptrdiff_t for differences. With -Wsign-compare (included in -Wall), the compiler will flag these.
Integer overflow
int overflows silently on signed types — the result is undefined behaviour. For large path counts or index calculations, use std::size_t or int64_t.
Common undefined behaviour traps
| Pattern | Consequence |
|---|---|
int a[5]; a[5] = 0; | Out-of-bounds write; silent corruption |
int x; std::cout << x; | Uninitialized read; garbage value or UB |
delete p; delete p; | Double-free; heap corruption |
double* p = &local; return p; | Dangling pointer to stack variable |
delete p; *p = 0; | Use-after-free |
AddressSanitizer (-fsanitize=address) catches all of these at runtime with precise diagnostics.
Interview Angle
Level 1 (Junior Quant / Quant Developer)
"Explain the difference between stack and heap allocation. When would you use each?"
Expected answer: The stack is automatic storage — memory is allocated when a function is entered and freed when it returns, managed by the stack pointer with zero overhead. The heap is dynamic storage — new calls the allocator, which manages free lists and requests pages from the OS; it must be freed explicitly via delete. Stack is faster but limited in size (typically 1–8 MB per thread); heap is slower but effectively unbounded. Use the stack for local scalars and small fixed-size arrays; use the heap (via std::vector) for arrays whose size is known only at runtime, or that must outlive the current scope.
"What is a dangling pointer?"
Expected answer: A pointer that holds the address of memory that has been freed or gone out of scope. Dereferencing it is undefined behaviour — the memory may have been reused for another object, so you are reading or writing arbitrary data. Common causes: returning a pointer to a local variable, storing a raw pointer to heap memory after calling delete, or invalidating a std::vector's internal pointer by pushing elements that trigger a reallocation.
Level 2 (Mid-level / C++ Quant)
"What does const double* const p mean? Contrast with const double* p and double* const p."
Expected answer: Read right-to-left. const double* const p is "p is a const pointer to a const double" — neither the address stored in p nor the value at that address can be changed. const double* p is "p is a pointer to a const double" — the value cannot be changed through p, but p can be reseated to point elsewhere. double* const p is "p is a const pointer to a double" — p always points to the same address (cannot be reseated), but the value at that address can be modified.
"Why prefer const object& over object for function parameters receiving large objects? And when does the preference reverse?"
Expected answer: Passing by const reference avoids copying the argument — the function receives a direct alias (essentially a pointer that the compiler manages). For objects larger than a CPU register (any struct, class, or container), copying is more expensive than passing a reference. The preference reverses for small scalar types (int, double, float, bool): passing by value copies 4–8 bytes, which is cheaper than passing a pointer (also 8 bytes on 64-bit) and avoids an indirection on every access. The compiler can often pass scalars in registers when they are by-value; const reference may force a stack address.