<< Home

Chapter 1: From C to C++

C++ began as an extension of C that added strong abstraction tools while keeping tight control over performance and memory layout. This chapter sets the historical stage, shows how the compilation model works, explains why undefined behavior exists, highlights the signature features that differ from C, identifies which C parts remain essential, surveys language versions from C++98 to C++26, and teaches how to read standard library headers with confidence.

Why C++ grew from C

C provided portable systems programming with direct access to memory and predictable performance. As projects grew, developers needed stronger ways to manage complexity without losing that control. C++ grew from C by adding classes, templates, overloading, and zero cost abstractions that compile away in optimized builds. The design goal was to layer expressive tools on top of C’s power while keeping the programmer in charge of costs.

Zero cost abstraction as a guiding idea

A zero cost abstraction adds no overhead compared with a hand written C style equivalent once compiled with typical optimizations. For example, std::span carries a pointer and a size; the optimizer can inline checks or remove them entirely when proven unnecessary. You get clarity in code while retaining predictable machine code.

Backward compatibility and the C subset

C++ aimed to compile much existing C code with minor adjustments. This choice kept the ecosystem large and practical. It also meant C++ inherited C’s compilation model, pointer arithmetic, and many low level concerns; the language then built safety and structure on top through RAII, references, and strong typing.

💡 When porting C code, begin by isolating ownership boundaries and replacing manual resource cleanup with RAII (Resource Acquisition Is Initialization) wrappers; you will keep performance while gaining reliability. RAII means you acquire a resource in an object’s constructor and you release it in the destructor. The lifetime of the resource is bound to the lifetime of the object. When the object goes out of scope, cleanup happens automatically and predictably.

Classes solved real world problems

Early large C systems suffered from scattered invariants and manual resource discipline. Classes let you centralize invariants in constructors and destructors; overloading gives natural syntax for domain types; templates let you write algorithms once for many types with no runtime cost.

The compilation model

C++ compiles each source file after preprocessing into a translation unit, then links the resulting object files into an executable or library. Understanding this pipeline explains why duplicate definitions fail, why inline semantics matter, and why headers must be designed carefully.

The translation unit boundary

A translation unit is the source file after applying all #include directives and macros. Name lookup, inline choices, and template instantiation can depend on what the unit sees. One design goal is to keep headers self contained and minimal so each unit compiles reliably.

Managing declarations and definitions

Headers declare interfaces; source files define them. Re declarations are allowed, but multiple definitions across translation units cause linker errors. Place function and class declarations in headers; put non inline definitions in exactly one source file.

Using include guards and embracing modules

Traditional headers use include guards or #pragma once to prevent multiple inclusion. Modern C++ also supports modules that express interfaces explicitly and speed up builds. You can keep headers for interop while migrating performance sensitive parts to modules over time.

// file: math.hpp
#ifndef MATH_HPP
#define MATH_HPP
int add(int a, int b);
#endif
// file: math.cpp
#include "math.hpp"
int add(int a, int b) { return a + b; }
⚠️ Avoid defining non inline variables or non inline functions in headers; doing so will produce multiple-definition linker errors when several translation units include that header.

The order of compilation steps

The pipeline is preprocessing, compilation to object code, template instantiation during or after parsing as needed, optimization, and linking. Templates in headers are typically defined entirely in the header so every translation unit can instantiate them when required.

// A header-only template must include its definition
template<class T>
T square(T x) { return x * x; }

Undefined behavior and the cost of speed

Undefined behavior allows compilers to assume certain bad states never occur; in return the optimizer can remove checks and generate faster code. The trade off is that violating rules can produce arbitrary results that vary by build, platform, or input.

Cmmon undefined behavior patterns

Frequent sources include out of bounds access, use after free, signed integer overflow, dereferencing null pointers, and violating strict aliasing. Many of these issues are invisible at first because release builds optimize away evidence of failure.

// Signed overflow is undefined; result can vary
int mul(int a, int b) { return a * b; }

Applying tools to catch problems early

Use address, undefined, and thread sanitizers during development; combine with static analysis and assertions. These tools insert checks that make misbehavior visible while you still have context to fix it.

💡 Compile with -fsanitize=address,undefined on Clang or GCC during tests; many subtle lifetime and overflow bugs will surface quickly.

Designing APIs that reduce risk

Prefer safe views like std::span, return std::optional for missing values, and document preconditions as runtime asserts in debug builds. By moving checks to boundaries you keep hot loops lean.

Key differences from C

C++ keeps C’s low level power but adds first class language features for safe composition. Four ideas change daily coding: references for aliasing without null, overloading for natural APIs, namespaces for modular names, and classes for encapsulated invariants.

Using references to express intent

A reference must bind to an object at initialization and cannot be reseated. This models required aliases. Combine with const to promise read only access. For optional references prefer pointers or std::optional<std::reference_wrapper<T>>.

void scale(double &r, double factor) { r *= factor; }

Overloading functions and operators

Overloading lets you use the same function name for related operations. This reduces mental overhead and makes generic code easier to read, as long as conversions and ambiguity are managed carefully.

int area(int w, int h);
double area(double r); // circle

Organizing with namespaces

Namespaces prevent collisions across libraries by grouping related declarations. Use nested namespaces for structure. For local convenience bring in selective names with using at minimal scope.

namespace geom::poly { int sides(int n); }

Building invariants into classes and RAII

Classes bind data and operations. Constructors establish invariants; destructors release resources. The RAII pattern ensures cleanup runs on scope exit even during exceptions or early returns.

class File {
FILE* f;
public:
explicit File(const char* path) : f(std::fopen(path, "r")) {}
~File() { if (f) std::fclose(f); }
auto get() const -> FILE* { return f; }
};
⚠️ Prefer std::unique_ptr with a custom deleter or std::filesystem helpers for production code; a simple wrapper example is useful for teaching principles.

The C subset you still use

Modern C++ code still leans on core C ideas such as the object model, pointer arithmetic in constrained contexts, bitwise operations, and the portable ABI expectations that C established. Many system interfaces remain C based, and C++ interoperates with them smoothly.

Interfacing with C

Linkers expect specific symbol names. C++ performs name mangling to encode types; C does not. Declare C functions with extern "C" to keep a C compatible name so the linker can match them correctly.

extern "C" int c_api_call(int);

Working with POD layouts and memcpy

Plain old data structures with standard layout can be copied by std::memcpy. Once a type has non trivial constructors or virtual functions, prefer memberwise copy or moves; raw copying can violate invariants.

Using bitwise and fixed width integers

Flags, device registers, and protocols often require bitwise operations. Favor fixed width types such as std::uint32_t for clarity about size and overflow behavior.

std::uint32_t flags = 0u;
flags |= 1u << 3; // set bit 3
💡 Prefer enum class with explicit bit masks when possible; the stronger typing prevents accidental mixing of unrelated flags.

Standard versions at a glance

The language evolves through ISO standards that add features while keeping strong focus on compatibility and performance. The table lists headline additions you will encounter most often in real projects.

VersionYearHighlights
C++98/031998/2003Templates, exceptions, namespaces, iostreams, STL containers and algorithms
C++112011Move semantics, auto, range for, lambdas, constexpr, nullptr, unique_ptr/shared_ptr, threads
C++142014Generic lambdas, variable templates, relaxed constexpr, make_unique
C++172017string_view, optional, variant, filesystem, structured bindings, if with init
C++202020Concepts, ranges, coroutines, modules, constexpr on more library, calendar/time zones
C++232023std::expected, mdspan, print, span extensions, constexpr further expanded
C++262026Planned refinements, executors and networking candidates, pattern matching proposals, more ranges views

Selecting a standard level in your builds

Pass a flag to your compiler to pick a standard, for example -std=c++20 on GCC or Clang, or /std:c++20 on MSVC. Choose the newest your toolchain and platform support; newer standards give simpler code and better diagnostics.

# CMake
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

Planning for portability across compilers

Different vendors deliver features on their own schedules. For widely used libraries, gate code with feature test macros like __cpp_concepts and provide simple fallbacks. This approach keeps one codebase workable across many environments.

#if __cpp_concepts >= 201907
template<class T> concept Number = requires(T v) { v + v; };
#else
// … provide a traits-based substitute
#endif
⚠️ Feature test macros indicate presence, not completeness; verify corner cases in your CI matrix when relying on very new library facilities.

Reading the standard library headers

Standard headers package related facilities behind stable names such as <vector>, <span>, and <chrono>. Learning the naming patterns helps you guess where things live and what lifetime or iterator requirements they expect.

Mapping components to headers

Containers live in headers named after them, algorithms in <algorithm>, iterators in <iterator>, string utilities in <string> or <string_view>, formatting in <format>, and time utilities in <chrono>. If a facility builds on another it often includes that dependency for you.

USynopsis style and <...> placeholders

The standard presents each header with a synopsis that sketches declarations using placeholders such as <...> for template parameters and … for omitted sections. Real headers are implementation specific; treat the synopsis as a contract rather than a literal file.

// <vector> synopsis style sketch, not a literal header
namespace std {
template<class T, class Allocator = allocator<T>>
class vector {
  // … members and typedefs
};
}

Including only what you need and letting ADL work

Include the most specific header that declares the symbols you use. For free functions that participate in templates, place them in your own namespace and rely on argument dependent lookup so generic algorithms can find them without global using declarations.

⚠️ ADL (Argument Dependent Lookup) means that when a function call is made, the compiler searches not only the current scope but also the namespaces associated with the argument types. This lets generic code find the correct overload without global using directives.
#include <algorithm>
#include <vector>

namespace geom {
struct Point { double x, y; };
}

int main() {
std::vector<geom::Point> pts{{0,0}, {1,1}};
std::ranges::for_each(pts, [](auto const& p){ /* … */ });
}
💡 When a header name and a component share a name, include that header directly; this keeps compile times lower and makes intent obvious in code review.

Distinguishing headers from modules in modern code

Headers remain universal; modules provide faster builds and clearer interfaces where supported. You can import standard library modules in some environments, for example import std;. Keep headers as the default for portability and phase in modules where the toolchain is ready.