C++ Programming
C++ Program Structure, Translation and Language Fundamentals
PGCP-AC
C++ is a compiled, statically typed language designed for direct control of resources as well as high-level abstraction. A correct C++ program depends on more than syntax: declarations must agree across files, definitions must satisfy the one-definition rule, object files must be linked and runtime operations must remain within the language's defined behaviour. This chapter builds that complete model from source text to executable program.
1. What C++ is
C++ supports several complementary styles:
- procedural programming organizes work into functions and control flow;
- object-oriented programming combines state and behaviour in classes and uses encapsulation and polymorphism;
- generic programming expresses algorithms in terms of type requirements through templates;
- resource-oriented programming ties resource lifetime to object lifetime through RAII;
- functional techniques use immutable values, lambdas, algorithms and composable operations.
These styles can coexist. A class may own a file resource, a template may operate on that class and an ordinary function may coordinate the operation.
C++ evolved from C but has its own type, initialization, overload, lifetime, exception and template rules. Valid C code is not automatically good or even valid C++. Prefer C++ abstractions such as containers, strings, smart pointers and deterministic objects over copying manual C patterns without considering object lifetime.
2. A minimal hosted program
#include <iostream>
int main() {
std::cout << "Hello, C++\n";
return 0;
}
#include <iostream> makes declarations for standard stream facilities available in the translation unit. main is the entry function of a hosted program and has return type int. std::cout is the standard output stream.
Reaching the closing brace of main is equivalent to returning zero. Zero conventionally reports successful termination to the host environment; a nonzero value conventionally reports failure. EXIT_SUCCESS and EXIT_FAILURE from <cstdlib> express portable status intent.
The implementation initializes standard facilities before entering main and performs normal static-duration cleanup after it returns.
3. Source character categories and tokens
Translation first turns source into preprocessing tokens and later language tokens. Important token categories are:
- identifiers: programmer-defined names such as
total; - keywords: reserved words such as
class,returnandconstexpr; - literals: values such as
42,3.5,'A'and"text"; - operators: symbols such as
+,==and::; - punctuators: structural symbols including braces, commas and semicolons.
Identifiers are case-sensitive, so count, Count and COUNT are different. An identifier cannot be a keyword. Names beginning with reserved underscore patterns belong to the implementation and should not be invented in application code.
Whitespace separates tokens where necessary. Single-line comments begin with //; block comments use /* ... */ and do not nest.
4. Expressions, statements and blocks
An expression computes a value, designates an object or produces side effects:
price * quantity
counter++
std::cout << message
A statement is a complete unit of execution. Expression statements usually end in a semicolon. Compound statements group statements in braces and introduce a block scope:
if (quantity > 0) {
const double total = price * quantity;
std::cout << total << '\n';
}
total exists only within the block. A declaration is itself a statement in many contexts. An accidental semicolon after if (condition); creates an empty controlled statement and is a common defect.
5. Declarations and definitions
A declaration introduces a name and enough type information for later use:
double area(double radius); // function declaration
extern int active_users; // declaration, normally not definition
A definition creates an entity or supplies its implementation:
double area(double radius) {
return 3.14159 * radius * radius;
}
int active_users = 0;
Every definition is also a declaration, but not every declaration is a definition. A compiler can type-check a call from a declaration. The linker still needs the corresponding function or object definition when the program odr-uses it.
Declarations repeated across files must agree. A mismatched declaration can cause compile errors, unresolved symbols or undefined behaviour depending on how it differs.
6. Scope, storage and lifetime
Scope answers where a name can be used. Common scopes include block, function parameter, class, namespace and global namespace scope.
Storage duration answers how long an object's storage exists:
- automatic: normally one block execution;
- static: entire program;
- thread: one object per thread for the thread's lifetime;
- dynamic: controlled through allocation and ownership.
Lifetime is the interval during which an object exists as its type. Storage may exist before construction or after destruction without containing a live object. This distinction becomes critical with placement construction, unions, raw memory and manual allocation.
Name visibility, storage duration and object lifetime are related but not interchangeable.
7. Namespaces
Namespaces group names and prevent collisions:
namespace billing {
double calculate_tax(double amount);
}
double tax = billing::calculate_tax(1000.0);
Standard-library names are in std. A using declaration introduces one selected name:
using std::cout;
A using directive such as using namespace std; makes all visible names candidates during lookup. It can cause ambiguity and should not appear in public headers. Explicit qualification communicates origin and scales better.
An unnamed namespace gives names internal linkage within one translation unit. Nested namespaces can be written compactly in modern C++: namespace company::module { ... }.
8. The preprocessing phase
The preprocessor handles directives before ordinary compilation. Common directives include:
#includefor textual inclusion;#defineand#undeffor macros;#if,#ifdef,#ifndefand#endiffor conditional inclusion;#pragmafor implementation-specific control.
#include conceptually inserts the named header's preprocessed content. Angle brackets normally search implementation-configured include locations; quotes normally search relative project locations first, followed by configured locations.
Macros perform token substitution without C++ type rules:
#define SQUARE(x) ((x) * (x))
SQUARE(i++) modifies i twice, demonstrating why constants, inline functions, templates and constexpr functions are safer than function-like macros.
9. Headers and inclusion guards
A header normally contains declarations, type definitions, templates and suitable inline definitions shared by translation units:
#ifndef GEOMETRY_CIRCLE_HPP
#define GEOMETRY_CIRCLE_HPP
double area(double radius);
#endif
The guard prevents the same header content from being processed repeatedly within one translation unit. #pragma once is widely supported but historically implementation-specific.
Headers should be self-contained: including one should provide everything required to compile its contents. Do not rely on another unrelated header having been included first.
Avoid non-inline function definitions and ordinary non-inline global variable definitions in headers because every including source file can produce a definition.
10. Translation units
A translation unit is roughly one source file after preprocessing has expanded its includes and conditional directives. Each source file is compiled separately:
main.cpp + included headers → main translation unit → main.o
math.cpp + included headers → math translation unit → math.o
Separate compilation improves build scalability but means the compiler usually sees only one translation unit at a time. Headers communicate declarations and definitions that must be visible, especially templates.
Changing a source file typically requires recompiling that file. Changing a widely included header can require recompiling every dependent translation unit.
11. The translation and build pipeline
A practical pipeline is:
- preprocessing handles directives and inclusion;
- compilation parses, type-checks and translates;
- assembly creates machine-code object files;
- linking combines objects and libraries and resolves symbols;
- loading maps the executable and required libraries into a process;
- execution begins and eventually enters
main.
Compilers often expose several steps through one driver command:
g++ -std=c++17 -Wall -Wextra -Wpedantic main.cpp math.cpp -o app
-std=c++17 selects the language mode. Warnings reveal suspicious but accepted code. Treating warnings seriously catches conversions, shadowing, missing returns and unused results before they become defects.
12. Compile-only and link steps
Files may be built separately:
g++ -std=c++17 -Wall -Wextra -c main.cpp
g++ -std=c++17 -Wall -Wextra -c math.cpp
g++ main.o math.o -o app
-c creates an object file without linking. The final command resolves references and produces the executable.
If main.cpp calls a correctly declared function whose definition was never linked, compilation can succeed and linking fails with an undefined reference or unresolved external symbol. If two object files provide forbidden multiple definitions, linking usually reports duplication.
13. The one-definition rule
The one-definition rule or ODR, governs how many definitions an entity may have across a program. Ordinary non-inline functions and externally linked variables must have one program definition when odr-used.
Classes, templates and inline functions may appear identically in several translation units, normally through headers. Their definitions must satisfy the ODR's equivalence requirements.
C++17 inline variables allow a header definition intended to denote one program entity:
inline constexpr int maximum_retries = 3;
inline is primarily an ODR and linkage facility in modern C++; it does not command the optimizer to substitute a function body.
14. Linking and libraries
A static library contributes selected object code to the executable at link time. A shared or dynamic library is loaded as a separate binary dependency, often at program start.
Link order can matter with some static-library toolchains because unresolved symbols are processed in sequence. Binary compatibility also matters: compiler ABI, library version, architecture, debug/runtime settings and build flags must be compatible.
A header supplies compile-time declarations. A library supplies compiled definitions. Having one without the other is insufficient when code uses the facility.
15. Diagnostics by phase
Classify failures before fixing them:
| Failure | Typical example |
|---|---|
| Preprocessing | Missing included file, unmatched conditional |
| Compilation | Syntax error, type mismatch, inaccessible member |
| Linking | Missing definition, duplicate external definition |
| Loading | Required shared library unavailable |
| Runtime exception/error | Explicit exception or OS failure |
| Logic defect | Program runs but computes the wrong defined result |
| Undefined behaviour | Program violates a rule with no required outcome |
The compiler is not required to diagnose every rule violation. Link success also does not prove defined runtime behaviour.
16. Defined, unspecified, implementation-defined and undefined
Defined behaviour has requirements prescribed by the standard. Implementation-defined behaviour lets an implementation choose and document one possibility, such as aspects of fundamental type sizes. Unspecified behaviour permits one of several possibilities without requiring documentation. Undefined behaviour imposes no requirements on that execution.
Examples of undefined behaviour include out-of-bounds access, dereferencing an invalid pointer, signed integer overflow and using an object outside its lifetime.
Undefined behaviour does not mean “random value” or “exception.” An optimizer may assume it never occurs, so observed debug output cannot establish a portable result. Sanitizers can detect many violations during testing but cannot prove their absence.
17. Standard streams
<iostream> declares standard stream objects:
std::cin: standard input;std::cout: normal output;std::cerr: diagnostic output, commonly unbuffered or promptly flushed;std::clog: buffered diagnostic output.
std::cout << "value = " << value << '\n';
'\n' inserts a newline. std::endl inserts a newline and flushes the stream. Unnecessary flushing can reduce performance, so use endl only when a flush is intended.
Stream insertion is type-aware and can be overloaded for user-defined types. Always check input state before relying on extracted values.
18. Language standards and portability
C++ evolves through published standards. C++17 introduced features including structured bindings, if constexpr, inline variables, fold expressions, std::optional, std::variant and filesystem support.
auto [key, value] = pair;
if constexpr (std::is_integral_v<T>) {
// compiled only for matching instantiations
}
Select the standard explicitly in builds and continuous integration. A compiler version and language mode are separate facts: a new compiler may default to an older mode, while an old compiler may only partly implement a requested standard.
Portable C++ also avoids assumptions about byte width, integer sizes, endianness, filesystem layout and extensions unless the target environment documents them.
19. Program organization
A small multi-file project might use:
project/
├── include/
│ └── geometry/circle.hpp
├── src/
│ ├── circle.cpp
│ └── main.cpp
└── CMakeLists.txt
Headers publish interfaces. Source files hold implementation that need not be visible. Build systems track dependencies, compilation options, include paths, libraries and platform differences.
Minimize header dependencies with forward declarations where a complete type is not needed, but do not sacrifice clarity. Private implementation techniques can reduce rebuild coupling for stable library interfaces.
20. C++ and C interoperability
C++ overloads names and commonly uses implementation-specific name mangling. A C-compatible declaration can request C language linkage:
extern "C" int legacy_calculate(int);
This affects linkage, not type safety, object lifetime or memory ownership. C interfaces cannot directly express C++ overloads, templates, exceptions or classes.
Do not let exceptions cross a C ABI boundary. Define who allocates, who frees, which encoding is used and how failures are reported.
Practical considerations
| Mistake | Correct approach |
|---|---|
Putting using namespace std; in a header | Qualify names or use narrow declarations |
| Defining ordinary globals in headers | Use one source definition or a valid inline variable |
| Assuming inclusion compiles a library | Link the required definitions |
| Treating all failures as compiler errors | Identify preprocessing, compile, link, load or runtime phase |
| Using macros for typed calculations | Prefer functions, templates or constexpr |
| Assuming undefined behaviour has a predictable output | Treat the execution as invalid |
Using std::endl for every line | Use newline unless flushing is required |
| Relying on compiler defaults | Select standard and warnings explicitly |
Worked multi-file trace
circle.hpp declares double area(double);. circle.cpp includes the header and defines the function. main.cpp includes the same header and calls it.
The preprocessor gives each source file the declaration. Each source compiles independently. The main object file contains a reference to area; the circle object file contains its definition. The linker connects them. Omitting circle.o leaves the reference unresolved even though both source files were individually valid.
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.