Summary
C++ is a compiled, statically typed language in which every entity’s type must be known to the compiler. Its distinguishing characteristic for programmers coming from C# or Java is value semantics: assignment copies values by default; sharing an object requires explicit pointers or references. This page covers the language fundamentals through Chapter 1 of Stroustrup’s A Tour of C++ (3rd ed., C++20), with notes on how they differ from C#.
(Stroustrup, A Tour of C++, 3rd ed., see source-a-tour-of-cpp)
Compilation model
C++ programs are compiled to native machine code by a compiler (e.g., MSVC, Clang, GCC) and linked into an executable for a specific platform. The resulting binary is not portable between platforms — portability means the source code compiles on multiple targets, not that a single binary runs everywhere.
This contrasts with C# and Java, where code compiles to an intermediate representation (IL / bytecode) and runs on a virtual machine. C++ is faster to start up and closer to hardware, but has no garbage collector and no runtime type-checking safety net by default.
Fundamental types
C++ provides a small set of hardware-mapped types. Their sizes are implementation-defined (not fixed by the language) but reflect machine capabilities:
| Type | Meaning | Typical size |
|---|---|---|
bool | Boolean: true or false | 1 byte |
char | Character (or small integer) | 1 byte |
int | Integer | 4 bytes |
double | Double-precision floating-point | 8 bytes |
unsigned | Non-negative integer; use for bitwise operations | 4 bytes |
For fixed-width types, use standard aliases such as int32_t, uint64_t (from <cstdint>).
C# comparison: C# has int (always 32 bits), double, bool, char (UTF-16, 2 bytes). C#‘s types are specified by the language standard; C++‘s are specified by the hardware.
Initialisation
C++ offers multiple initialisation syntaxes:
double d1 = 2.3; // traditional C-style
double d2 {2.3}; // braced-init (preferred)
double d3 = {2.3}; // also valid
int i1 = 7.8; // i1 becomes 7 — silently truncates!
int i2 {7.8}; // ERROR: narrowing conversion rejectedThe braced {} form prevents narrowing conversions — accidental implicit truncation. Prefer it for declarations where the type is named.
auto type deduction: when the type is obvious from the initialiser, use auto:
auto b = true; // bool
auto i = 123; // int
auto d = 1.2; // double
auto z = sqrt(y); // whatever sqrt() returnsUse auto to avoid repetition; be explicit where the type matters for correctness (e.g., double vs float precision).
Scope and lifetime
A declaration introduces a name into a scope. The three main scopes:
- Local scope: inside a function or block
{ }— destroyed at the closing brace - Class scope: members of a class — destroyed when the object is destroyed
- Namespace scope: declared in a namespace outside any function — destroyed at programme end
void f(int arg) // arg: local scope
{
string s {"Hello"}; // s: local scope
// s is destroyed here when f() returns
}Objects are constructed when they are declared and destroyed when they go out of scope. This deterministic lifetime is the foundation of RAII.
C# comparison: C# class instances live until the garbage collector claims them. C++ objects on the stack live until scope exit — predictable and deterministic.
const and constexpr
Two distinct notions of immutability:
| Keyword | Meaning | When evaluated |
|---|---|---|
const | ”I promise not to change this” | Run time (value may depend on runtime input) |
constexpr | ”Evaluate this at compile time” | Compile time (value must be a constant expression) |
constexpr int dmv = 17; // compile-time constant
const double sqv = sqrt(var); // run-time constant: computed once, then immutable
constexpr double square(double x) { return x*x; } // constexpr function
constexpr double s = 1.4 * square(17); // OK: compile-time
const double s2 = 1.4 * square(var); // OK: run-timeA constexpr function can be called with non-constant arguments but then produces a non-constant result.
C# comparison: C# const is compile-time only (like C++ constexpr). C#‘s readonly is closest to C++ const (set once, not necessarily at compile time).
Pointers
A pointer holds the memory address of another object:
char v[6]; // array of 6 chars
char* p = &v[3]; // p points to v's fourth element
char x = *p; // dereference: x = value at address pKey pointer operators in declarations:
T*— pointer to T&var— address of var*ptr— value at address ptr (dereference)
The null pointer: use nullptr (not 0 or NULL):
double* pd = nullptr; // pd points to nothing
if (pd) // same as pd != nullptr
// ...Dereferencing a null or dangling pointer is undefined behaviour — the programme may crash or silently corrupt memory.
Pointer arithmetic is valid for arrays: p++ advances to the next element. This is how the range-for loop is implemented internally.
References
A reference is an alias for an existing object:
int x = 2;
int& r = x; // r is bound to x
r = 7; // modifies x through r; x is now 7Key differences from pointers:
| Property | Pointer (T*) | Reference (T&) |
|---|---|---|
| Can be null | Yes (nullptr) | No — must bind to a valid object |
| Can be rebound | Yes | No — binding is permanent |
| Requires dereference | Yes (*p) | No — implicitly dereferenced |
| Can do arithmetic | Yes | No |
Const reference is the standard way to pass large objects to functions without copying:
void sort(vector<double>& v); // pass by reference: sorts v in-place
double sum(const vector<double>& v); // pass by const ref: reads but cannot modifyC# comparison: C# references are always non-nullable (unless nullable reference types are enabled) and are automatically dereferenced. C++ makes the null/non-null distinction explicit via the type system.
Value semantics
C++ has value semantics by default: assignment copies:
int x = 2;
int y = x; // y is a copy of x; modifying y does not affect xThis is true for all types, including user-defined ones. Sharing requires explicit pointers or references.
C# comparison: in C#, class instances have reference semantics — assignment copies the reference, not the object. Two variables can point to the same object. In C++, assignment copies the object; you must use a pointer or reference to share.
Range-for loop
C++ range-for iterates over any sequence:
int v[] = {0, 1, 2, 3, 4, 5};
for (auto x : v) // copy each element
cout << x << '\n';
for (auto& x : v) // reference: can modify
++x; // increments each elementThe range-for works with any type that provides begin() and end() — standard containers, arrays, strings, and any user-defined type implementing those functions.
In practice
- Use
{}for initialisation to catch narrowing conversions at compile time. - Use
autofor type deduction where the type is obvious from context. - Use
conston any variable or parameter that should not change — it documents intent and enables compiler optimisations. - Use
constexprfor values needed at compile time (array sizes, template arguments, performance-critical constants). - Prefer
nullptrto0orNULL. - Pass large objects by
const referenceto avoid copies. - Minimise pointer use in application code; prefer references where the object is guaranteed to exist.
Open questions
- Stroustrup’s advice to prefer references over pointers assumes object lifetimes are clear. In game engine code with complex ownership (entities, components, pools), lifetime management is often subtle. Where does the reference/pointer choice matter most in Unreal Engine?
- C++20 modules (import/export) reduce compile-time overhead dramatically but are not yet universally adopted. Unreal Engine uses headers. How does this affect Unreal-specific C++ patterns?
Related
- cpp-classes-and-oop — user-defined types, constructors, virtual functions
- cpp-memory-management — RAII, destructors, smart pointers
- cpp-templates — parameterised types and functions
- csharp-variables-and-types — C# counterpart for comparison
- csharp-oop-fundamentals — C# OOP for contrast with C++ class model
- source-a-tour-of-cpp