170 companion flashcards · AI-assisted study content · Open the deck →
This deck covers core concepts in C++ programming, with a strong focus on object-oriented design and modern C++ features. You'll find questions on classes and structs, inheritance, virtual functions, operator overloading, namespaces, and resource management techniques like RAII and smart pointers. Together, these topics build a solid mental model of how C++ organizes data, manages memory, and supports reusable, polymorphic code.
It's well suited for intermediate learners who already know basic C++ syntax and want to deepen their understanding of the language's distinctive features. The material is also helpful for students reviewing for exams or developers preparing for technical interviews, since many of these questions probe principles that come up regularly in real-world C++ development.
To get the most out of these cards, try to connect each answer back to a small code example you can write and run on your own. When a concept like virtual functions or smart pointers feels abstract, experimenting with a short program will make the behavior concrete and far easier to recall later.
Finally, space your review sessions across several days rather than cramming. C++ has many overlapping ideas, such as the relationships between classes, structs, and inheritance hierarchies, and repeated exposure over time is the best way to keep the distinctions clear in your memory.
C++ is built around four foundational principles of object-oriented programming: encapsulation, which bundles data and the functions that operate on it together; abstraction, which hides implementation details behind a clean interface; inheritance, which lets new classes derive from existing ones to establish is-a relationships and reuse code; and polymorphism, which lets a single interface represent different underlying types. Classes are the central mechanism for applying these principles. A class is a user-defined type that groups data members and member functions, declared with the class keyword. The closely related struct keyword defines essentially the same kind of type, with the only default difference being access control: class members are private by default, while struct members are public by default. Both can hold methods, constructors, and participate in inheritance.
Access in C++ is governed by three levels. public members are visible everywhere, protected members are accessible to the class itself, its friends, and any derived classes, and private members are restricted to the class and its friends. The default for a class is private, while the default for a struct is public. When the encapsulation of private or protected data must be relaxed for a specific helper, a class can grant access with friend declarations: a friend function is a non-member function declared inside the class with the friend keyword that gains access to non-public members, and a friend class grants the same access to every member of another class, which is useful for tightly coupled types such as iterators over a container. Const correctness is the discipline of marking every value or function that should not change as const. const int x = 5 forbids reassignment, a function declared void foo(const std::string& s) declares a parameter it will not modify, and a member declared int getVal() const promises not to modify the object. Using const consistently prevents accidental mutation and enables compiler optimizations.
The static keyword has several distinct meanings depending on context. A static local variable inside a function persists across calls and is initialized only once. A static class member belongs to the class itself rather than to any object, so all instances share a single copy and it must be defined in exactly one translation unit, as in the line int MyClass::count = 0. A static member function is similarly tied to the class: it has no this pointer and can only access static members, and is called through the class name. Every non-static member function receives an implicit this pointer referring to the object on which it was called; this has type ClassName* const, or const ClassName* inside a const member function. Const member functions, marked with a trailing const, are the only kind that can be called on const objects. To allow internal caches, lazy initialization, or mutexes to be updated without violating that promise, a member may be declared mutable, which exempts it from const-correctness restrictions.
Inheritance allows a derived class to inherit members from a base class, declared with a colon and access specifier in the derived class header. It promotes code reuse and expresses an is-a relationship between types. C++ supports multiple inheritance, where a class can derive from several base classes at once, and it provides override and final specifiers from C++11: override asserts that a method overrides a base virtual function and produces a compile error if it does not, while final prevents further overriding in derived classes, or further derivation when applied to a class. When a derived class declares a name that matches one in the base, the base name is hidden in the derived class even when signatures differ, a phenomenon called name hiding; a using Base::name declaration in the derived class can unhidde overloaded base functions. The std namespace holds the entire C++ Standard Library, so identifiers like std::cout and std::vector are qualified by it, while user code can group its own identifiers into namespaces to avoid name collisions and bring them into scope with using namespace.
Polymorphism in C++ comes in two forms. Compile-time, or static, polymorphism relies on function overloading, which allows multiple functions in the same scope to share a name as long as their parameter lists differ, on operator overloading, and on templates to choose the right operation at compile time. A binary operator may be overloaded as a member of the left operand's class, where the left operand becomes *this, or as a non-member function, often a friend, taking both operands explicitly; the stream insertion operator is the canonical non-member case so the left operand can be a stream. Runtime, or dynamic, polymorphism uses virtual functions and inheritance, dispatching through a per-class table of function pointers called a vtable. Each object of a class with virtual functions carries a hidden vptr set by every constructor that points to its class's vtable; calling a virtual function through a base pointer or reference fetches the function pointer from the vtable at runtime and invokes the most-derived override. A pure virtual function, declared with the = 0 suffix, has no implementation in the base class, and any class that contains one becomes an abstract class that cannot be instantiated directly.
Abstract classes serve as interfaces or partial blueprints. C++ has no interface keyword, but the convention is a class containing only pure virtual functions and no data members, which derived classes must fully implement to be instantiable as concrete classes. When a derived class inherits from a base that holds resources, declaring a virtual destructor in the base ensures that deleting the object through a base pointer invokes the derived destructor first, releasing derived-class resources. Multiple inheritance introduces the diamond problem: if two bases share a common ancestor, the most-derived class would normally hold two copies of that ancestor. Virtual inheritance, declared with the virtual keyword on the base specifier, guarantees a single shared base subobject and resolves the ambiguity. Operators that cannot be overloaded in C++ are the scope resolution operator, member access, member access through pointer, and the ternary; you also cannot invent new operator symbols.
Constructors are special member functions automatically invoked when an object is created. They share the name of the class and have no return type. A default constructor is one that can be called with no arguments; the compiler generates one only if no other constructor is declared, and you can force its creation with = default. A parameterized constructor accepts arguments used to initialize member variables, and a copy constructor initializes a new object as a copy of an existing one, taking a const reference to the class type, and is invoked when passing or returning by value or when explicitly initializing one object from another. A delegating constructor calls another constructor of the same class from its initializer list to avoid code duplication, and a conversion constructor is a non-explicit single-argument constructor that permits implicit conversion from its argument type to the class type, which can be blocked by marking it explicit.
Initialization is preferred over assignment inside the constructor body. The member initializer list, written after a colon following the constructor parameter list, initializes members before the constructor body runs and is required for const members, references, and base class subobjects. Default member initializers specified at the declaration site, such as int count = 0, supply a default value whenever the constructor's initializer list omits that member. The destructor, named with a leading tilde, runs when an object's lifetime ends to release resources, and is called automatically when stack objects go out of scope or when heap objects are deleted. The Rule of Five states that if a class defines any of the destructor, copy constructor, copy assignment, move constructor, or move assignment, it should define all five to keep resource management correct. The Rule of Zero recommends the opposite: if the class uses only RAII types such as smart pointers and standard containers for its resources, the compiler-generated defaults suffice and no special members need to be written by hand.
RAII, which stands for Resource Acquisition Is Initialization, ties resource lifetime to object lifetime: resources are acquired in the constructor and released in the destructor, guaranteeing cleanup even when exceptions occur. Smart pointers are the canonical RAII wrappers for dynamic memory. std::unique_ptr provides exclusive ownership and cannot be copied, only moved, deleting the managed object when the smart pointer goes out of scope. std::shared_ptr allows shared ownership via reference counting, deleting the managed object when the last shared_ptr is destroyed, with use_count exposing the current count. std::weak_ptr is a non-owning reference to a shared_ptr-managed object that does not increment the reference count, used to break circular references and accessed through lock to obtain a temporary shared_ptr. A shallow copy duplicates pointer values but not what they point to, leaving two objects sharing the same memory, while a deep copy duplicates the pointed-to data so each object owns an independent copy, which is necessary when a class manages raw dynamic memory.
Move semantics enable the transfer of resources from one object to another without copying, dramatically improving performance for temporaries. The mechanism relies on rvalue references, written with double ampersand, which bind to temporary objects and let functions steal their internals. std::move is not a runtime operation but a cast that converts an lvalue into an rvalue reference, signaling that the source may be moved from and leaving the source in a valid but unspecified state. A forwarding reference, written as T&& in a deduced template context, binds to both lvalues (becoming T&) and rvalues (becoming T), and std::forward<T>(arg) preserves the original value category, enabling perfect forwarding into another function with no extra copies. std::swap exchanges two objects in constant time via move semantics, and the copy-and-swap idiom implements assignment by copying the source and swapping with *this, providing strong exception safety and reusing the copy and move logic. References themselves come in two forms: a plain reference is an alias for a named object that cannot be null or be reseated, while a const reference can bind to temporaries as well as named objects and is the standard way to pass large objects to functions without copying.
Templates let you write generic code that works with any type. A function template such as template<typename T> T max(T a, T b) { return (a > b) ? a : b; } defines a family of functions, and the compiler generates a concrete version for each type used, a process called template instantiation. A class template follows the same idea for types: template<typename T> class Stack { std::vector<T> data; public: void push(T val); T pop(); } is used as Stack<int> s. Template specialization provides a custom implementation for a particular type, with full specialization covering one specific type and partial specialization covering a subset of the template parameters. Non-type template parameters accept values rather than types, as in template<int N> struct Buffer { char data[N]; }, and from C++20 they may also be literal class types.
Variadic templates accept any number of template arguments using parameter packs, as in template<typename... Args> void f(Args... args). The packs are expanded using recursion in older code or, since C++17, more concisely with fold expressions, which reduce a parameter pack over a binary operator such as +, *, &&, ||, or the comma operator. Template metaprogramming uses templates to perform computation at compile time, producing highly optimized code but with a notoriously steep learning curve; std::tuple, std::integral_constant, and many type traits are implemented this way.
Compile-time constraint of templates relies on SFINAE, the principle that Substitution Failure Is Not An Error: when substituting template parameters fails for an overload, that overload is silently removed rather than aborting compilation. std::enable_if<Cond, T> yields T only when Cond is true, and std::void_t detects whether an expression is well-formed, together enabling compile-time checks for type properties. C++20 concepts replace most SFINAE boilerplate with named compile-time predicates written in the natural syntax template<std::integral T> T add(T a, T b) { return a + b; }, producing clearer error messages and more readable constraints. The three-way comparison operator, known as the spaceship operator, returns a comparison category type, and using = default on it makes the compiler generate all six relational operators automatically. constexpr marks a variable or function as evaluable at compile time whenever given constant expressions, with consteval in C++20 requiring compile-time evaluation; constexpr implies const, but const alone does not imply compile-time evaluation. The inline keyword serves a different role: it allows a function to be defined identically in multiple translation units without violating the One Definition Rule, and modern compilers also use it as a hint to expand the body at call sites.
The standard library containers fall into three families. Sequence containers preserve insertion order and include vector, deque, list, array, and forward_list. Associative containers store ordered keys, typically in a balanced binary search tree, and include set, map, multiset, and multimap. Unordered associative containers store keys in a hash table for average constant-time lookup and include unordered_set, unordered_map, unordered_multiset, and unordered_multimap. Choosing between them is guided by access patterns: std::vector offers constant-time random access and amortized constant-time push_back in a contiguous block, making it the default sequence container; std::deque supports constant-time insertion and removal at both ends without requiring a single contiguous block; and std::list is a doubly-linked list that offers constant-time insertion and deletion given an iterator but no random access and higher per-element memory overhead due to its pointers.
For key-value data, std::map stores pairs in a red-black tree with unique sorted keys and logarithmic-time lookup, while std::unordered_map stores them in a hash table with average constant-time lookup at the cost of requiring a hash function and losing key ordering. std::set is the value-only counterpart to std::map, holding unique sorted elements with logarithmic-time operations. std::array differs from std::vector by having a fixed compile-time size stored on the stack, which makes it preferable for small constant-size collections where no growth is needed. std::pair holds two values accessed through .first and .second, and std::tuple generalizes this to any number of values accessed through std::get<I>. std::initializer_list represents a brace-enclosed list passed to a function or constructor, which is what makes std::vector<int> v{1, 2, 3} work, and the broader brace initialization syntax, often called uniform initialization, prevents narrowing conversions at compile time, making int y{3.14} a hard error.
Iterators are generalized pointers that provide a uniform way to traverse containers. The categories input, output, forward, bidirectional, and random access describe what operations an iterator supports. begin() returns a mutable iterator that allows modifying elements, while cbegin() returns a const_iterator with read-only access. Reverse iterators obtained via rbegin() and rend() traverse a container from end to beginning. The range-based for loop for (const auto& elem : vec) works with anything that exposes begin() and end(), and the ampersand avoids copies. The <algorithm> header provides a rich set of generic functions operating on iterator pairs, including std::sort, std::find, std::count_if, std::transform, std::accumulate, and std::for_each. The auto keyword lets the compiler deduce types from initializers, dramatically reducing verbosity for iterators, lambdas, and complex template expressions.
Modern C++ introduces a host of utilities that improve expressiveness and safety. Lambda expressions are anonymous function objects defined inline with the syntax [capture](params) -> return_type { body }. The capture clause controls how local variables are pulled in: [x] captures x by value, [&x] by reference, [=] captures all used variables by value, [&] captures all by reference, and combinations such as [=, &x] capture all by value except x by reference. std::function<R(Args...)> is a polymorphic function wrapper that can store any callable, including lambdas, function pointers, functors, and bind expressions. std::bind in <functional> produces a new callable with arguments partially bound using placeholders, though lambdas usually make std::bind unnecessary. std::ref and std::cref wrap a reference into a copyable object so that parameter templates can bind by reference rather than copy.
Several header-only types model possibly absent or one-of-several values without resorting to raw pointers or error codes. std::optional<T> may or may not contain a T and replaces sentinel-based or out-parameter patterns. std::variant is a type-safe union holding one of the listed alternative types, queried with std::holds_alternative and accessed with std::get. std::any is a type-safe single-value container for any copy-constructible type, retrieved with std::any_cast. std::string_view is a non-owning reference to a contiguous character sequence, cheap to pass by value and ideal for read-only string parameters. std::span extends the same idea to arbitrary element types, providing a safe pointer-plus-size view over contiguous memory such as a std::vector or a C array. Structured bindings in C++17 unpack aggregates, pairs, and tuples into named variables, as in auto [key, value] = *map.begin, replacing std::tie in many cases. C++17 also introduces init statements in if and switch, allowing scoped variables such as if (auto it = m.find(k); it != m.end()) to live for the entire branch. C++20 adds coroutines, where functions can be suspended and resumed using co_await, co_yield, and co_return, enabling generators, lazy pipelines, and asynchronous I/O without callback chains; the std::format facility, modeled on Python's, for type-safe text formatting; and modules, which replace header inclusion with export module math and import math, providing faster builds, better isolation, and freedom from macro leakage. std::bit_cast in C++20 reinterprets an object's bit representation as another type at compile time with well-defined results, replacing the older memcpy-based type punning trick. std::launder blocks the compiler from assuming a pointer still refers to the old object after placement new has revived the memory underneath it.
Several long-standing idioms in C++ center on grouping and qualifying names. A namespace groups related identifiers to avoid name collisions, accessed with the scope resolution operator or pulled into scope with using namespace. The enum class introduced in C++11 provides type-safe scoped enumerations whose values do not implicitly convert to integers and do not pollute the enclosing scope, accessed as Color::Red. The std::chrono library offers type-safe durations and clocks; std::system_clock reports wall-clock time, std::steady_clock is monotonic and ideal for measuring intervals, and std::high_resolution_clock provides the smallest tick period available. The std::filesystem library, standardized in C++17, offers portable paths and directory traversal, including std::filesystem::directory_iterator. Strings themselves are managed by std::string, which provides size and length queries, substr, find, replace, append, concatenation via operator+, and element access through operator[] or the bounds-checked at(); the in-memory std::stringstream supports stream-style insertion and extraction for building and parsing strings, and std::regex in <regex> provides standard-library regular expression support through std::regex_match, std::regex_search, and std::regex_replace, although it is widely regarded as cumbersome and slow compared with dedicated libraries.
Pointers and references are the two ways to refer indirectly to objects. A reference is an alias for another object: an lvalue reference T& binds to a named object, cannot be null, and cannot be reseated, while a const reference const T& can bind to temporaries as well as named objects and is the standard way to pass large objects to functions without copying. A pointer is a variable that stores a memory address and can be null, reseated, and used with pointer arithmetic; reading or writing through a null or dangling pointer is undefined behavior. nullptr, introduced in C++11, is the preferred null pointer constant because it avoids the integer-versus-pointer ambiguity of the traditional NULL macro and improves overload resolution. Const interacts with pointers in three useful ways: int* const p is a const pointer that cannot be reseated but whose pointee can change, const int* p is a pointer to const that cannot modify the pointee but can be reseated, and const int* const p locks down both. A void* holds the address of an object of unknown type but cannot be dereferenced directly and must be cast to a typed pointer before use, which is why it appears mainly in C-style APIs. A function pointer stores the address of a function and can be invoked through that pointer, useful for callbacks; in modern C++, std::function or a lambda is usually preferable. A dangling reference points to memory whose lifetime has ended, and returning a reference to a local variable is one of the classic ways to create one.
Memory in C++ comes from two principal sources. Stack allocation is fast and automatic: a local variable lives in the current stack frame and is freed when the scope exits. Heap allocation uses new and delete, and persists until explicitly freed. The new and delete operators are type-safe and call constructors and destructors, unlike the C library functions malloc and free, which only allocate raw bytes; mixing them across the two systems is undefined behavior. delete releases a single object and calls one destructor, while delete[] releases a C-style array allocated with new T[n] and calls the destructor for every element, and mismatching the two is also undefined behavior. Placement new constructs an object at a preallocated memory address, such as in a custom allocator or memory pool, and the corresponding cleanup requires an explicit destructor call followed by operator delete. A memory leak occurs when dynamically allocated memory is never freed and gradually exhausts the process; the recommended remedy is RAII through smart pointers and standard containers rather than raw new and delete.
C++ offers four named casts that make the intent of each conversion explicit, in contrast to the C-style cast (int)x whose meaning is hidden. static_cast performs compile-time type conversions with type checking and is used for numeric conversions, upcasting in an inheritance hierarchy, and void*-to-typed-pointer conversions. const_cast is the only cast that can add or remove const or volatile qualifiers, but modifying an originally const object through the result is undefined behavior. reinterpret_cast performs low-level, implementation-defined bitwise conversions between pointer and integer types and should be used only when the platform's memory layout is fully understood. dynamic_cast safely converts pointers and references within an inheritance hierarchy with runtime type checking enabled by RTTI, returning nullptr for a pointer or throwing std::bad_cast for a reference when the cast is invalid. The volatile keyword tells the compiler that a variable's value may change outside its control, such as a memory-mapped hardware register, and disables certain optimizations; const volatile means the program cannot modify the variable but something else might. Unlike std::atomic, however, volatile provides no thread-safety guarantees.
C++ handles errors through exceptions and the try-catch construct. A try block contains code that may throw, and catch blocks match against thrown exception types; exceptions propagate up the call stack until a matching handler is found. The recommended pattern is to throw by value but catch by const reference, as in catch (const std::exception& e), which avoids object slicing and unnecessary copies while still letting the handler examine the polymorphic object. std::exception is the base of the standard exception hierarchy; important derived classes include std::runtime_error for runtime problems, std::logic_error for logical mistakes, std::bad_alloc for failed memory allocation, and std::out_of_range for index errors. The noexcept specifier declares that a function will not throw, which permits the compiler to optimize and is particularly important for move operations, destructors, and swap; if a noexcept function does throw, std::terminate is called. std::terminate is also invoked when an exception escapes a noexcept function during stack unwinding, when an exception is thrown during stack unwinding, or when no catch handler matches a propagating exception. Stack unwinding is the process of destroying automatic objects in each frame as the exception travels upward, which is what makes RAII cleanup reliable. A function that may throw can declare so explicitly with noexcept(false), but this is rarely needed. Functions offer four levels of exception safety: nothrow (never throws), strong (either the operation completes or the state is unchanged), basic (no leaks and the object remains usable), and none (anything may happen). All of these guarantees are checked at runtime, in contrast to compile-time checks such as template instantiation, type checks, and constant folding that the compiler performs while translating source code into object code.
Concurrency in C++ is built around threads, tasks, and synchronization. std::thread launches a new OS thread running a callable, and every std::thread must be joined or detached before destruction or the program terminates. std::async runs a callable asynchronously and returns a std::future whose get() retrieves the result or rethrows an exception. std::promise lets a producer set a value or exception, while the associated std::future lets a consumer retrieve it, providing a one-shot channel between threads. std::mutex provides mutual exclusion, but locking and unlocking by hand is error-prone, so the standard library offers RAII wrappers: std::lock_guard is the lightweight option with no extra features, while std::unique_lock additionally supports deferred locking, manual unlock, and ownership transfer. std::scoped_lock in C++17 can lock several mutexes at once with a deadlock-avoidance algorithm. A std::condition_variable allows threads to wait until a predicate is satisfied, enabling blocking queues and similar patterns. A deadlock occurs when two or more threads each hold a lock and wait for a lock held by the other, and is prevented by acquiring locks in a fixed order or by using std::scoped_lock. A race condition occurs when program behavior depends on the non-deterministic timing of concurrent accesses to shared data, producing different results on each run; the remedies are synchronization through mutexes or atomic operations. std::atomic in <atomic> provides lock-free thread-safe access to a value with operations such as fetch_add, and supports explicit memory ordering.
The compilation model in C++ centers on translation units. A translation unit is the result of preprocessing a source file together with the headers it includes. The preprocessor runs before compilation and handles #include to insert a file, #define to define a textual macro, #ifdef and #ifndef for conditional compilation, and #pragma for implementation-defined hints. The text-substitution nature of #define means it has no type or scope and can introduce subtle bugs, so typed, scoped const or constexpr constants are preferred. Headers are typically protected by include guards or #pragma once so that repeated inclusion in a single translation unit does not cause multiple-definition errors. The One Definition Rule requires exactly one definition of each non-inline function or variable across the entire program, while inline functions and class definitions may appear in many translation units identically. A declaration introduces a name and its type, such as extern int x, while a definition provides storage or a function body, as in int x = 5; definitions are also declarations, but declarations may appear many times. Two kinds of assertions help verify invariants: the runtime assert(expr) from <cassert> aborts the program when the condition is false and is disabled when NDEBUG is defined, while the compile-time static_assert(cond, message) produces a compilation error if the condition fails. Undefined behavior covers anything the standard does not define, including signed integer overflow, null-pointer dereference, reading uninitialized memory, and out-of-bounds access; the compiler may assume such situations never occur and optimize accordingly. Implementation-defined behavior, by contrast, must be documented by the implementation, and examples include the size of int, byte order, and the value of a char after overflow.
&&, binds to temporary objects (rvalues):void process(std::string&& s);noexcept declares that a function will not throw exceptions:void process() noexcept { }noexcept function does throw, std::terminate is called. It helps the compiler optimize code and is important for move operations.= default:MyClass() = default;using declaration imports a name from a namespace or base class into the current scope:using Base::foo; makes foo accessible directly in the derived class. using namespace std; brings all std names into scope.std::swap exchanges the values of two objects:template<typename T> void swap(T& a, T& b) { T tmp = std::move(a); a = std::move(b); b = std::move(tmp); }std::bind (in <functional>) creates a new callable by binding arguments to a function:auto f = std::bind(add, 5, std::placeholders::_1);
f(3); // 8std::tuple, std::integral_constant, and SFINAE. It produces highly optimized code but can be hard to read.#pragma once) prevent a header from being processed more than once per translation unit:#ifndef FOO_H
#define FOO_H
// ...
#endifDrill this topic
170 flashcards on Cpp Programming — free, no signup needed to start.
Study Cpp Programming flashcardsLearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.