C++ Basics
- Description: A note on the C++ basics — constants, declarations, tokens, storage qualifiers, initialization forms, enums, attributes, and alignment
- My Notion Note ID: K2A-B1-1
- Created: 2018-09-14
- Updated: 2026-02-28
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Constants and Immutability
- 2. Declarations
- 3. Storage Qualifiers
- 4. Initialization Forms
- 5. Enums
- 6. Attributes
- 7. Alignment and
std::byte - 8. Aggregate Types and POD
1. Constants and Immutability
1.1 const vs #define
- Both define constants but behave very differently. Prefer
const/constexpr(C++11) in modern code.
| Aspect | const |
#define |
|---|---|---|
| Type safety | Has a type; compiler performs type checking | No type; simple text substitution |
| Scope | Respects scope rules | Global text replacement |
| Debugging | Visible in debugger | Replaced before compilation; invisible to debugger |
| Testability | Can be used in unit tests and assertions | Cannot be inspected programmatically |
#define MAX_SIZE 100 // No type, no scope, no safety
const int kMaxSize = 100; // Type-safe, scoped, debuggable
constexpr int kMaxSize2 = 100; // C++11: compile-time constant
1.2 Pointer to const vs const Pointer
constposition relative to*decides what's immutable.
int value = 42;
// Pointer to const: cannot modify the pointed-to value through this pointer
const int* p1 = &value; // OK
int const* p2 = &value; // Same as above (equivalent syntax)
// *p1 = 10; // ERROR: cannot modify through pointer-to-const
// Const pointer: cannot change what the pointer points to
int* const p3 = &value; // Must be initialized
// p3 = &other; // ERROR: cannot reassign const pointer
*p3 = 10; // OK: can modify the value
// Const pointer to const: both are immutable
const int* const p4 = &value; // Must be initialized
// *p4 = 10; // ERROR
// p4 = &other; // ERROR
- Mnemonic: read right-to-left.
const int*= "pointer to const int."int* const= "const pointer to int."
2. Declarations
2.1 Variable Declaration Gotchas
- Multi-variable declarations on one line — easy to misread.
-
Only the last variable in a chain receives the initializer:
// Only 'index' is initialized to 0; 'column' and 'row' are uninitialized int column, row, index = 0; // All three are initialized to 0 int column = 0, row = 0, index = 0; // Chain assignment also works int column, row, index; column = index = row = 0; -
*binds only to the immediately following identifier:int* a, b, c; // This is NOT three pointers. It is equivalent to: // int* a; -- pointer to int // int b; -- plain int // int c; -- plain int // To declare three pointers: int *a, *b, *c; // Or better yet, one declaration per line: int* a; int* b; int* c;
2.2 Tokens
- Token = smallest element the compiler recognizes. Six categories:
- Keywords — reserved words (
if,class,virtual, ...) - Identifiers — user-defined names (variables, functions, classes)
- Constants — literals (
42,3.14,'a',true) - String literals —
"hello" - Special symbols — punctuation/delimiters (
[] () {} , ; * = #) - Operators —
+,-,<<,>
2.3 Comma Operator
- Evaluates all operands left-to-right, returns the last.
- Rarely used intentionally — avoid for clarity.
int a = (1, 2, 3); // a == 3; all expressions evaluated, last one returned
3. Storage Qualifiers
3.1 mutable
- Allows a non-static class member to be modified inside a
constmember function.
Why mutable is needed
constmember fn promises logical const (observable state unchanged), not bitwise const (no bytes change).- Internal bookkeeping not part of observable state — caches, lazy-init flags, mutexes, debug counters.
- Modifying bookkeeping in
constmethod is logically fine, but type system can't tell logical-const from bitwise-const. mutable= escape hatch: "OK to modify inconstmethod, not part of observable state."- Without
mutable:- Drop
constfrom method → contagious; everyconst-ref caller breaks. const_cast→ legal but ugly, easy to misuse.
- Drop
Common use cases
- Memoization / caches — cache an expensive computation; cached value is internal, not observable.
- Lazy initialization — defer constructing a heavy member until first access.
- Mutex inside
constmethods — locking requires the mutex itself to be mutable. - Reference-counted handles —
std::shared_ptr's control block updates a count even when the pointee isconst. - Debug instrumentation — access counters, timing data, etc.
Example
#include <iostream>
class Example {
public:
Example(int val) : val_(val) {}
int getVal() const {
// This is a const method, but we can modify mutable members
++access_count_;
return val_;
}
int getAccessCount() const { return access_count_; }
private:
int val_;
mutable int access_count_ = 0; // Can be modified in const methods
};
int main() {
const Example ex(5);
std::cout << ex.getVal() << std::endl; // 5 (access_count_ = 1)
std::cout << ex.getVal() << std::endl; // 5 (access_count_ = 2)
std::cout << ex.getAccessCount() << std::endl; // 2
return 0;
}
getVal()isconst— returned value unchanged across calls.access_count_tick-up = internal, not observable →mutablecorrectly allows it.
3.2 volatile
volatile— value may change outside program flow.- Compiler must read/write from memory each access. No register caching, no eliding "redundant" reads.
volatile int hardware_register; // Value may change outside program control
When to use volatile
- 3 legitimate uses, all involve memory modified outside the standard C++ execution model:
- Memory-mapped I/O — hardware registers in the program's address space (status flags, device control).
- Signal handlers — flag set in handler, polled in main flow (use
volatile std::sig_atomic_t). setjmp/longjmp— locals modified betweensetjmpandlongjmpmust bevolatileto be reliably observable.
Example: signal handler flag
#include <csignal>
#include <iostream>
volatile std::sig_atomic_t signal_received = 0;
void handler(int) {
signal_received = 1; // safe to assign inside a signal handler
}
int main() {
std::signal(SIGINT, handler);
while (!signal_received) {
// Without volatile, the compiler could observe that nothing in the
// visible flow modifies signal_received and turn this into an
// infinite loop. volatile forces a fresh read every iteration.
}
std::cout << "Caught signal, exiting." << std::endl;
return 0;
}
When NOT to use volatile
- Not thread safety. No atomic reads/writes, no inter-thread ordering, no inter-thread visibility.
- For threads:
std::atomic<T>,std::mutex,std::condition_variable.
// WRONG: volatile does not make this thread-safe
volatile int counter = 0;
// ++counter from multiple threads is still a race condition
// CORRECT
std::atomic<int> counter{0};
// ++counter is atomic across threads
- Rule of thumb:
volatiletalks to the outside world (hardware, signals, OS jumps);std::atomictalks to other threads.
3.3 extern
- Declares var/fn defined in another translation unit. Compiler looks up at link time, not in current TU.
// file1.cpp -- definition
int myVar = 42;
// file2.cpp -- declaration (uses the variable defined in file1.cpp)
extern int myVar;
int main() {
std::cout << myVar << std::endl; // 42
return 0;
}
extern "C"— C linkage when calling C functions from C++.
extern "C" {
void c_library_function(int arg);
}
4. Initialization Forms
- Many init forms in C++ — not equivalent.
int a; // default initialization (uninitialized for built-ins)
int b = 0; // copy initialization
int c(0); // direct initialization
int d{0}; // direct-list (brace) initialization
int e = {0}; // copy-list initialization
int f{}; // value initialization (zero-initialized)
std::vector<int> v1; // default ctor
std::vector<int> v2(5); // FIVE zero-initialized ints
std::vector<int> v3{5}; // ONE element with value 5 (initializer_list wins!)
std::vector<int> v4{5, 10}; // TWO elements: 5, 10
std::vector<int> v5(5, 10); // FIVE elements: 10, 10, 10, 10, 10
struct Point { int x; int y; };
Point p{1, 2}; // aggregate initialization
Point q = {.x = 1, .y = 2}; // designated initializers (C++20)
Prefer {} (brace) in modern C++:
- No narrowing conversions —
int x{3.14};is error;int x = 3.14;silently truncates. - Avoids most vexing parse —
Widget w(Foo());declares a function;Widget w{Foo{}};constructs an object. - Uniform syntax — aggregates, classes, arrays.
- Trap: class with
initializer_listctor → brace prefers it.std::vector<int> v{5};= 1 element (=5), not 5-element vector. Use()for count-based ctor.
5. Enums
- 2 kinds: traditional (unscoped) and scoped (
enum class, C++11).
// Unscoped enum (C-style) — names leak into the surrounding scope, implicitly converts to int
enum Color { Red, Green, Blue };
int c = Red; // OK: implicit conversion
if (c == 0) { /* ... */ }
// Scoped enum (enum class) — names are scoped, no implicit conversion
enum class Priority { Low, Medium, High };
Priority p = Priority::High;
// int n = p; // ERROR: no implicit conversion
int n = static_cast<int>(p); // OK: explicit
Always prefer enum class in modern code:
- No name leakage —
Priority::High, not justHigh. - No implicit conversions to/from
int. - Specify underlying type —
enum class Flags : uint8_t { ... }.
Bitmask enums — opt back into bitwise operators:
enum class Perm : unsigned { Read = 1, Write = 2, Exec = 4 };
constexpr Perm operator|(Perm a, Perm b) {
return static_cast<Perm>(static_cast<unsigned>(a) | static_cast<unsigned>(b));
}
Perm p = Perm::Read | Perm::Write;
6. Attributes
- Standardized annotations (C++11+) for diagnostics/optimizations.
| Attribute | Meaning |
|---|---|
[[nodiscard]] |
Warn if the return value is ignored |
[[nodiscard("reason")]] (C++20) |
Warn with a specific message |
[[deprecated]] |
Warn on use |
[[deprecated("use foo instead")]] |
With message |
[[maybe_unused]] |
Suppress unused-variable warnings |
[[fallthrough]] |
Acknowledge intentional switch fallthrough |
[[likely]] / [[unlikely]] (C++20) |
Branch prediction hints |
[[no_unique_address]] (C++20) |
Allow zero-size empty subobjects |
[[nodiscard]] int compute() { return 42; }
compute(); // warning: ignoring return value
[[deprecated("use compute() instead")]]
int legacy_compute();
void process(int x, [[maybe_unused]] int debug_id) {
// debug_id only used in debug builds; no warning in release
}
switch (n) {
case 1: do_one();
[[fallthrough]];
case 2: do_two_or_one(); break;
case 3: do_three(); break;
}
if (cond) [[likely]] {
// hot path
}
[[nodiscard]]on factories/fallible ops — catches "forgot to check return" bugs at compile time. Most useful in practice.
7. Alignment and std::byte
alignofqueries,alignasrequests memory alignment.
alignof(int); // typically 4 (4-byte alignment)
alignof(double); // typically 8
struct alignas(64) CacheLine { // aligned to a 64-byte cache line
int data[16];
};
alignas(16) char buffer[256]; // 16-byte aligned buffer
Common uses:
- Cache-line alignment — avoid false sharing in multi-threaded code.
- SIMD intrinsics — require 16- or 32-byte alignment.
- Hardware — specific alignment (e.g., DMA buffers).
std::byte(C++17,<cstddef>) — type-safe byte for raw memory, not arithmetic.- No implicit conversion to/from
char/int.
#include <cstddef>
std::byte b{0xFF}; // ok
b = std::byte{0x10}; // ok
// int n = b; // ERROR: no implicit conversion
int n = std::to_integer<int>(b); // explicit: 16
b |= std::byte{0x01}; // bitwise ops are defined
- Use for opaque memory — serialization buffers, generic byte streams, hashing input.
- Use
unsigned charif you need arithmetic on bytes.
8. Aggregate Types and POD
- Layout/behavior classifications. Matter for serialization, ABI compatibility,
memcpy-based copying.
Aggregate types
- Aggregate = array, or class with:
- No user-declared (or inherited) constructors.
- No private/protected non-static data members.
- No virtual functions.
- No virtual or non-public bases (since C++17).
- Brace-init + designated initializers (C++20):
struct Point { int x; int y; };
Point p{1, 2}; // aggregate init
Point q = {.x = 1, .y = 2}; // designated init (C++20)
Trivially copyable
- Trivially copyable — copy/move/destruct all trivial (compiler-generated, no user logic).
- Safe to
memcpy.
Standard layout
- Standard layout:
- All non-static data members same access control.
- No virtual functions or virtual bases.
- C-compatible struct interpretation safe.
POD (Plain Old Data)
- POD = trivial + standard layout.
- Trivial is stricter than trivially-copyable — also requires trivial default ctor.
- Byte-for-byte C-struct compatible. Use for binary serialization, OS APIs, network protocols.
| Trait | What it means |
|---|---|
std::is_aggregate_v<T> |
Aggregate-initializable |
std::is_trivially_copyable_v<T> |
Safe to memcpy |
std::is_standard_layout_v<T> |
C-compatible layout |
std::is_pod_v<T> (deprecated in C++20) |
Both trivial AND standard layout |
struct A { int x; }; // aggregate, trivially copyable, standard layout, POD
struct B { int x; B(int) {} }; // not aggregate (has ctor)
struct C { int x; private: int y; }; // not standard layout (mixed access)
struct D { virtual void f(); }; // not POD (has virtual)
static_assert(std::is_trivially_copyable_v<A>);
static_assert(!std::is_aggregate_v<B>);
- C++20:
std::is_poddeprecated. Use the precise trait —std::is_trivially_copyable_vorstd::is_standard_layout_v. POD as a single concept is no longer the right vocabulary.