
Interview questions related to C and C++ are relatively rare compared to those in the Java domain. This article is a valuable summary of C and C++ interview knowledge points.
const
Function
-
Modifies a variable to indicate that the variable cannot be changed;
-
Modifies a pointer, which can be a pointer to a constant (pointer to const) or a constant pointer (const pointer);
-
Modifies a reference, which can be a reference to a constant (reference to const), used for parameter types to avoid copying and prevent modification of the value by the function;
-
Modifies a member function to indicate that the member function cannot modify member variables.
const Pointers and References
- Pointers
- Pointer to a constant (pointer to const)
- Constant pointer (const pointer)
- References
- Reference to a constant (reference to const)
- No const reference exists because a reference itself is a const pointer
(For easier memory, think of values modified by const (after const) as unchangeable, as in the examples with p2 and p3 below.)
Usage
// Class
class A
{
private:
const int a; // Constant member, can only be assigned in the initialization list
public:
// Constructor
A() : a(0) { };
A(int x) : a(x) { }; // Initialization list
// const can be used to distinguish overloaded functions
int getValue(); // Regular member function
int getValue() const; // Constant member function, cannot modify any data members of the class
};
void function()
{
// Object
A b; // Regular object, can call all member functions, update constant member variables
const A a; // Constant object, can only call constant member functions
const A *p = &a; // Pointer variable, points to a constant object
const A &q = a; // Reference to a constant object
// Pointers
char greeting[] = "Hello";
char* p1 = greeting; // Pointer variable, points to character array variable
const char* p2 = greeting; // Pointer variable, points to character array constant (const after char indicates the character cannot be changed)
char* const p3 = greeting; // Constant pointer, points to character array variable (const after p3 indicates p3 itself cannot be changed)
const char* const p4 = greeting; // Constant pointer, points to character array constant
}
// Functions
void function1(const int Var); // The passed parameter cannot be changed within the function
void function2(const char* Var); // The content pointed to by the parameter pointer is constant
void function3(char* const Var); // The parameter pointer is constant
void function4(const int& Var); // Reference parameter is constant within the function
// Function return values
const int function5(); // Returns a constant
const int* function6(); // Returns a pointer to a constant variable, usage: const int *p = function6();
int* const function7(); // Returns a constant pointer to a variable, usage: int* const p = function7();
static
Function
-
Modifies a regular variable, changing the storage area and lifecycle of the variable, storing it in the static area, allocating space before the main function runs. If there is an initial value, it initializes with that value; if not, the system initializes it with a default value.
-
Modifies a regular function, indicating that the function’s scope is limited to the file where it is defined. In multi-developer projects, to prevent name conflicts with functions in other namespaces, functions can be declared static.
-
Modifies member variables, ensuring that all objects only store one instance of that variable, and it can be accessed without creating an object.
-
Modifies member functions, allowing access to the function without creating an object, but static functions cannot access non-static members.
this Pointer
-
The this pointer is a special pointer implicitly present in every non-static member function. It points to the object that calls the member function.
-
When a member function is called on an object, the compiler first assigns the object’s address to the this pointer, then calls the member function, and every time the member function accesses data members, it implicitly uses the this pointer.
-
When a member function is called, an implicit parameter is automatically passed to it, which is a pointer to the object where this member function resides.
-
The this pointer is implicitly declared as: ClassName const this, which means that the this pointer cannot be assigned a value; in const member functions of ClassName, the type of this pointer is: const ClassName const, indicating that the object pointed to by this cannot be modified (i.e., no assignment operations can be performed on the data members of this object);
-
This is not a regular variable but an rvalue, so the address of this cannot be taken (cannot &this).
In the following scenarios, it is often necessary to explicitly reference the this pointer:
- To achieve method chaining for objects;
- To avoid assignment operations on the same object;
- When implementing certain data structures, such as lists.
inline Functions
Characteristics
- Equivalent to writing the contents of the inline function at the point where the inline function is called;
- Equivalent to skipping the steps of entering the function and directly executing the function body;
- Similar to macros, but with type checking, truly possessing function characteristics;
- Compilers generally do not inline inline functions that contain loops, recursion, switch statements, or other complex operations;
- Functions defined within class declarations, except for virtual functions, are automatically treated as inline functions.
Usage
inline usage
// Declaration 1 (with inline, recommended)
inline int functionName(int first, int second,...);
// Declaration 2 (without inline)
int functionName(int first, int second,...);
// Definition
inline int functionName(int first, int second,...) {/****/};
// Class internal definition, implicit inline
class A {
int doA() { return 0; } // Implicit inline
}
// Class external definition, requires explicit inline
class A {
int doA();
}
inline int A::doA() { return 0; } // Requires explicit inline
Compiler Processing Steps for inline Functions
- Copies the inline function body to the point where the inline function is called;
- Allocates memory space for local variables used in the inline function;
- Maps the input parameters and return values of the inline function to the local variable space of the calling method;
- If the inline function has multiple return points, it transforms it into a branch at the end of the inline function code block (using GOTO).
Advantages and Disadvantages
Advantages
- Inline functions, like macro functions, will expand the code at the call site, eliminating the need for parameter stack pushing, stack frame allocation and recovery, and result returning, thus improving program execution speed.
- Compared to macro functions, inline functions perform safety checks or automatic type conversions during code expansion (like regular functions), while macro definitions do not.
- Member functions declared and defined simultaneously in a class are automatically converted to inline functions, allowing inline functions to access class member variables, which macro definitions cannot.
- Inline functions can be debugged at runtime, while macro definitions cannot.
Can virtual functions (virtual) be inline functions (inline)?
Are “inline virtual” member functions ever actually “inlined”?
- Virtual functions can be inline functions, and inline can modify virtual functions, but when virtual functions exhibit polymorphism, they cannot be inlined.
- Inlining is a suggestion to the compiler, while the polymorphism of virtual functions occurs at runtime, and the compiler cannot know which code will be called at runtime, so virtual functions exhibiting polymorphism (at runtime) cannot be inlined.
- The only time inline virtual can be inlined is when the compiler knows which class the called object belongs to (e.g., Base::who()), which only happens when the compiler has the actual object rather than a pointer or reference to the object.
Virtual function inline usage
#include <iostream>
using namespace std;
class Base
{
public:
inline virtual void who()
{
cout << "I am Base\n";
}
virtual ~Base() {}
};
class Derived : public Base
{
public:
inline void who() // Implicit inline if not written
{
cout << "I am Derived\n";
}
};
int main()
{
// The virtual function who() here is called through the specific object (b) of the class (Base), which can be determined at compile time, so it can be inline, but whether it is actually inlined depends on the compiler.
Base b;
b.who();
// The virtual function here is called through a pointer, exhibiting polymorphism, which needs to be determined at runtime, so it cannot be inline.
Base *ptr = new Derived();
ptr->who();
// Because Base has a virtual destructor (virtual ~Base() {}), when deleting, the destructor of the derived class (Derived) is called first, followed by the destructor of the base class (Base), preventing memory leaks.
delete ptr;
ptr = nullptr;
system("pause");
return 0;
}
volatile
volatile int i = 10;
- The volatile keyword is a type modifier that indicates that a variable declared with it can be changed by factors unknown to the compiler (operating system, hardware, other threads, etc.). Therefore, using volatile tells the compiler not to optimize such objects.
- Variables declared with the volatile keyword must be fetched from memory every time they are accessed (variables not modified by volatile may be fetched from the CPU register due to compiler optimization).
- const can be volatile (e.g., read-only status registers).
- Pointers can be volatile.
assert()
Assert is a macro, not a function. The prototype of the assert macro is defined in <assert.h> (C) and
Usage of assert()
#define NDEBUG // Adding this line disables assert
#include <assert.h>
assert( p != NULL ); // assert is not available
sizeof()
- sizeof returns the total size occupied by an array.
- sizeof returns the size occupied by the pointer itself.
#pragma pack(n)
Sets the alignment of structure, union, and class member variables to n bytes.
Usage of #pragma pack(n)
#pragma pack(push) // Save alignment state
#pragma pack(4) // Set to 4-byte alignment
struct test
{
char m1;
double m4;
int m3;
};
#pragma pack(pop) // Restore alignment state
Bit Fields
Bit mode: 2; // mode occupies 2 bits
A class can define its (non-static) data members as bit fields, which contain a certain number of binary bits. Bit fields are typically used when a program needs to pass binary data to other programs or hardware devices.
- The layout of bit fields in memory is machine-dependent.
- The type of bit fields must be integral or enumeration types, and the behavior of signed types in bit fields will depend on the specific implementation.
- The address-of operator (&) cannot be applied to bit fields, and no pointer can point to a class’s bit field.
extern “C”
- Functions or variables qualified with extern are of extern type.
- Variables and functions modified with extern “C” are compiled and linked in accordance with C language conventions.
The purpose of extern “C” is to allow the C++ compiler to treat the code declared with extern “C” as C language code, avoiding issues with symbol linkage due to C++ name mangling with symbols in C language libraries.
Usage of extern “C”
#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
struct and typedef struct
In C
// c
typedef struct Student {
int age;
} S;
Equivalent to
// c
struct Student {
int age;
};
typedef struct Student S; at this point S is equivalent to struct Student, but the two identifier namespaces are different.
Additionally, it is possible to define a void Student() {} that does not conflict with struct Student.
In C++
Due to changes in the compiler’s symbol resolution rules, it differs from C language.
1. If struct Student {…}; is defined in the class identifier space, when using Student me;, the compiler will search the global identifier table first, and if Student is not found, it will search within the class identifier.
This means that both Student and struct Student can be used, as shown below:
// cpp
struct Student {
int age;
};
void f( Student me ); // Correct, the "struct" keyword can be omitted
2. If a function with the same name as Student is defined afterwards, then Student only represents the function, not the structure, as shown below:
typedef struct Student {
int age;
} S;
void Student() {} // Correct, after definition "Student" only represents this function
//void S() {} // Error, symbol "S" has already been defined as an alias for "struct Student"
int main() {
Student();
struct Student me; // Or "S me";
return 0;
}
C++ struct and class
In general, struct is better seen as an implementation of a data structure, while class is better seen as an implementation of an object.
Differences:
The most fundamental difference is the default access control.
- Default inheritance access: struct is public, class is private.
- As an implementation of a data structure, struct has public default data access control, while class, as an implementation of an object, has private default member variable access control.
union
A union is a special class that saves space; a union can have multiple data members, but at any given time, only one data member can have a value. When one member is assigned a value, the other members become undefined. Unions have the following characteristics:
- Default access control is public.
- Can contain constructors and destructors.
- Cannot contain reference type members.
- Cannot inherit from other classes and cannot be a base class.
- Cannot contain virtual functions.
- Anonymous unions can directly access union members within the scope where they are defined.
- Anonymous unions cannot contain protected or private members.
- Global anonymous unions must be static.
Usage of union
#include<iostream>
union UnionTest {
UnionTest() : i(10) {};
int i;
double d;
};
static union {
int i;
double d;
};
int main() {
UnionTest u;
union {
int i;
double d;
};
std::cout << u.i << std::endl; // Outputs 10 from UnionTest union
::i = 20;
std::cout << ::i << std::endl; // Outputs 20 from global static anonymous union
i = 30;
std::cout << i << std::endl; // Outputs 30 from local anonymous union
return 0;
}
Implementing C++ Classes in C
C implements C++ object-oriented features (encapsulation, inheritance, polymorphism).
- Encapsulation: Use function pointers to encapsulate attributes and methods within a structure.
- Inheritance: Structure nesting.
- Polymorphism: Different function pointers for parent and child class methods.
explicit Keyword
- When the explicit keyword modifies a constructor, it can prevent implicit conversions and copy initialization.
- When the explicit keyword modifies a conversion function, it can prevent implicit conversions, but context conversions are exceptions.
Usage of explicit
struct A
{
A(int) { }
operator bool() const { return true; }
};
struct B
{
explicit B(int) {}
explicit operator bool() const { return true; }
};
void doA(A a) {}
void doB(B b) {}
int main()
{
A a1(1); // OK: direct initialization
A a2 = 1; // OK: copy initialization
A a3{ 1 }; // OK: direct list initialization
A a4 = { 1 }; // OK: copy list initialization
A a5 = (A)1; // OK: allows explicit conversion with static_cast
doA(1); // OK: allows implicit conversion from int to A
if (a1); // OK: uses conversion function A::operator bool() for implicit conversion from A to bool
bool a6(a1); // OK: uses conversion function A::operator bool() for implicit conversion from A to bool
bool a7 = a1; // OK: uses conversion function A::operator bool() for implicit conversion from A to bool
bool a8 = static_cast<bool>(a1); // OK: static_cast for direct initialization
B b1(1); // OK: direct initialization
B b2 = 1; // Error: object with explicit constructor cannot be copy initialized
B b3{ 1 }; // OK: direct list initialization
B b4 = { 1 }; // Error: object with explicit constructor cannot be copy list initialized
B b5 = (B)1; // OK: allows explicit conversion with static_cast
doB(1); // Error: object with explicit constructor cannot be implicitly converted from int to B
if (b1); // OK: object with explicit conversion function B::operator bool() can be contextually converted from B to bool
bool b6(b1); // OK: object with explicit conversion function B::operator bool() can be contextually converted from B to bool
bool b7 = b1; // Error: object with explicit conversion function B::operator bool() cannot be implicitly converted
bool b8 = static_cast<bool>(b1); // OK: static_cast for direct initialization
return 0;
}
friend Classes and Functions
- Can access private members.
- Break encapsulation.
- Friend relationships are not transitive.
- Friend relationships are unidirectional.
- The form and number of friend declarations are not restricted.
using
using Declarations
A using declaration statement introduces one member of a namespace at a time. It allows us to clearly know which name is being referenced in the program. For example:
using namespace_name::name;
Using Declarations for Constructors
In C++11, derived classes can reuse constructors defined by their direct base classes.
class Derived : Base {
public:
using Base::Base;
/* ... */
};
As shown above, for each constructor of the base class, the compiler generates a corresponding constructor for the derived class (with the same parameter list). The generated constructors are of the form: Derived(parms) : Base(args) { }
using Directives
A using directive makes all names in a specific namespace visible, so we no longer need to add any prefix qualifiers for them. For example:
using namespace_name name;
It is advisable to use using directives sparingly to avoid polluting the namespace.
Generally speaking, using a using declaration is safer than using a using directive because it only imports the specified name. If that name conflicts with a local name, the compiler will issue a warning. A using directive imports all names, including those that may not be needed. If there is a conflict with a local name, the local name will override the namespace version, and the compiler will not issue a warning. Additionally, the openness of namespaces means that the names of namespaces may be scattered across multiple locations, making it difficult to know exactly which names have been added.
Usage of using
It is advisable to use using directives sparingly.
using namespace std;
It is better to use using declarations.
int x;
std::cin >> x ;
std::cout << x << std::endl;
Or
using std::cin;
using std::cout;
using std::endl;
int x;
cin >> x;
cout << x << endl;
:: Scope Resolution Operator
Categories
- Global scope operator (::name): Used before type names (classes, class members, member functions, variables, etc.) to indicate the scope is the global namespace.
- Class scope operator (class::name): Used to indicate that the specified type’s scope is a specific class.
- Namespace scope operator (namespace::name): Used to indicate that the specified type’s scope is a specific namespace.
Usage of ::
int count = 11; // Global (::) count
class A {
public:
static int count; // Class A's count (A::count)
};
int A::count = 21;
void fun()
{
int count = 31; // Initialize local count to 31
count = 32; // Set local count value to 32
}
int main() {
::count = 12; // Test 1: Set global count value to 12
A::count = 22; // Test 2: Set class A's count to 22
fun(); // Test 3
return 0;
}
enum Enumeration Type
Scoped Enumeration Type
enum class open_modes { input, output, append };
Unscoped Enumeration Type
enum color { red, yellow, green };
enum { floatPrec = 6, doublePrec = 10 };
decltype
The decltype keyword is used to check the declared type of an entity or the type and value classification of an expression. Syntax:
decltype ( expression )
Usage of decltype
// Trailing return allows us to declare the return type after the parameter list
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
// Process sequence
return *beg; // Return a reference to an element in the sequence
}
// To use template parameter members, typename must be used
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
// Process sequence
return *beg; // Return a copy of an element in the sequence
}
References
Lvalue References
Regular references generally represent the identity of an object.
Rvalue References
Rvalue references must bind to rvalues (temporary objects, objects about to be destroyed) and generally represent the value of an object.
Rvalue references can implement move semantics and perfect forwarding, with two main purposes:
- To eliminate unnecessary object copies during interactions between two objects, saving computational storage resources and improving efficiency.
- To define generic functions more concisely and clearly.
Reference Folding
- X& & X& & X&& can be folded into X&
- X&& && can be folded into X&&
Macros
Macro definitions can achieve functionality similar to functions, but they are not functions. The “parameters” in the macro definition’s parentheses are not real parameters; during macro expansion, they undergo one-to-one replacement.
Member Initialization Lists
Benefits
- More efficient: eliminates the need for a call to the default constructor.
- In some cases, initialization lists must be used:
- Constant members, as constants can only be initialized and not assigned, must be placed in the initialization list.
- Reference types, as references must be initialized at definition and cannot be reassigned, must also be written in the initialization list.
- Classes without default constructors, as using initialization lists can avoid calling the default constructor for initialization.
initializer_list List Initialization
Use brace initializer lists to initialize an object, where the corresponding constructor accepts a std::initializer_list parameter.
Usage of initializer_list
#include <iostream>
#include <vector>
#include <initializer_list>
template <class T>
struct S {
std::vector<T> v;
S(std::initializer_list<T> l) : v(l) {
std::cout << "constructed with a " << l.size() << "-element list\n";
}
void append(std::initializer_list<T> l) {
v.insert(v.end(), l.begin(), l.end());
}
std::pair<const T*, std::size_t> c_arr() const {
return {&v[0], v.size()}; // Copy list initialization in return statement
// This does not use std::initializer_list
}
};
template <typename T>
void templated_fn(T) {}
int main()
{
S<int> s = {1, 2, 3, 4, 5}; // Copy initialization
s.append({6, 7, 8}); // List initialization in function call
std::cout << "The vector size is now " << s.c_arr().second << " ints:\n";
for (auto n : s.v)
std::cout << n << ' ';
std::cout << '\n';
std::cout << "Range-for over brace-init-list: \n";
for (int x : {-1, -2, -3}) // auto's rules allow this range for to work
std::cout << x << ' ';
std::cout << '\n';
auto al = {10, 11, 12}; // auto's special rules
std::cout << "The list bound to auto has size() = " << al.size() << '\n';
// templated_fn({1, 2, 3}); // Compilation error! " {1, 2, 3} " is not an expression,
// it has no type, so T cannot be deduced
templated_fn<std::initializer_list<int>>({1, 2, 3}); // OK
templated_fn<std::vector<int>>({1, 2, 3}); // Also OK
}
Object-Oriented Programming
Object-oriented programming (OOP) is a programming paradigm that incorporates the concept of objects, and it is also an abstract guideline for program development.
Characteristics of Object-Oriented Programming
The three main characteristics of OOP are encapsulation, inheritance, and polymorphism.
Encapsulation
Encapsulates objective entities into abstract classes, allowing classes to restrict access to their data and methods to trusted classes or objects while hiding information from untrusted ones. Keywords: public, protected, private. If not specified, defaults to private.
- Public members: can be accessed by any entity.
- Protected members: can only be accessed by subclasses and member functions of the class.
- Private members: can only be accessed by member functions of the class, friend classes, or friend functions.
Inheritance
- Base class (parent class) -> Derived class (child class)
Polymorphism
- Polymorphism refers to multiple states (forms). In simple terms, we can define polymorphism as the ability of messages to be displayed in multiple forms.
- Polymorphism is based on encapsulation and inheritance.
- C++ polymorphism classification and implementation:
- Ad-hoc Polymorphism (compile-time): function overloading, operator overloading.
- Subtype Polymorphism (runtime): virtual functions.
- Parametric Polymorphism (compile-time): class templates, function templates.
- Coercion Polymorphism (compile-time/runtime): basic type conversions, custom type conversions.
Static Polymorphism (compile-time/early binding)
Function overloading
class A
{
public:
void do(int a);
void do(int a, int b);
};
Dynamic Polymorphism (runtime/late binding)
- Virtual functions: use virtual to modify member functions, making them virtual functions.
Note:
- Regular functions (non-member functions) cannot be virtual functions.
- Static functions cannot be virtual functions.
- Constructors cannot be virtual functions (because the virtual table pointer is not present in the object’s memory space when the constructor is called; the virtual table pointer is formed only after the constructor call is completed).
- Inline functions cannot be virtual functions when exhibiting polymorphism; see: Can virtual functions (virtual) be inline functions (inline)?
Dynamic polymorphism usage
class Shape // Shape class
{
public:
virtual double calcArea()
{
...
}
virtual ~Shape();
};
class Circle : public Shape // Circle class
{
public:
virtual double calcArea();
...
};
class Rect : public Shape // Rectangle class
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
Shape * shape2 = new Rect(5.0, 6.0);
shape1->calcArea(); // Calls the method in the Circle class
shape2->calcArea(); // Calls the method in the Rectangle class
delete shape1;
shape1 = nullptr;
delete shape2;
shape2 = nullptr;
return 0;
}
Virtual Destructors
Virtual destructors are designed to solve the problem of a base class pointer pointing to a derived class object and deleting the derived class object using the base class pointer.
Usage of virtual destructors
class Shape
{
public:
Shape(); // Constructor cannot be a virtual function
virtual double calcArea();
virtual ~Shape(); // Virtual destructor
};
class Circle : public Shape // Circle class
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
shape1->calcArea();
delete shape1; // Because Shape has a virtual destructor, when deleting, the derived class destructor is called first, followed by the base class destructor, preventing memory leaks.
shape1 = NULL;
return 0;
}
Pure Virtual Functions
A pure virtual function is a special type of virtual function that does not provide a meaningful implementation in the base class and is declared as a pure virtual function, leaving its implementation to the derived classes of that base class.
virtual int A() = 0;
Virtual Functions and Pure Virtual Functions
- If a class declares a virtual function, this function is implemented, even if it is an empty implementation; its purpose is to allow this function to be overridden (override) in its subclasses, enabling the compiler to use late binding to achieve polymorphism. A pure virtual function is merely an interface, a function declaration that must be implemented in the subclass.
- Virtual functions in subclasses can be omitted; however, pure virtual functions must be implemented in subclasses to instantiate the subclass. The class with virtual functions is used for “implementation inheritance,” inheriting both the interface and the implementation of the parent class.
- Pure virtual functions focus on the uniformity of the interface, with implementation left to subclasses.
- A class containing pure virtual functions is called an abstract class, which cannot be instantiated directly but can only be used after being inherited and its virtual functions overridden. An abstract class can be inherited by subclasses that can also be abstract classes or regular classes.
- A virtual base class is the base class in virtual inheritance; see below for virtual inheritance.
Virtual Function Pointers and Virtual Function Tables
- Virtual function pointers: In objects of classes containing virtual functions, they point to the virtual function table, determined at runtime.
- Virtual function tables: Stored in the program’s read-only data segment (.rodata section, see: target file storage structure), they hold virtual function pointers. If a derived class implements a virtual function of the base class, it will overwrite the original virtual function pointer of the base class in the virtual table, created at compile time based on the class declaration.
Virtual Inheritance
Virtual inheritance is used to solve the diamond inheritance problem under multiple inheritance conditions (wasting storage space, causing ambiguity).
The underlying implementation principles depend on the compiler, generally achieved through virtual base class pointers and virtual base class tables. Each subclass that inherits virtually has a virtual base class pointer (occupying the storage space of one pointer, 4 bytes) and a virtual base class table (not occupying the storage space of the class object). (It should be emphasized that the virtual base class will still have copies in the subclass, but there will only be one copy at most, not that it does not exist in the subclass); when a subclass of virtual inheritance is inherited as a parent class, the virtual base class pointer will also be inherited.
In fact, vbptr refers to the virtual base table pointer (virtual base table pointer), which points to a virtual base class table (virtual table) that records the offset address of the virtual base class relative to the derived class; through the offset address, the members of the virtual base class can be found, and virtual inheritance does not need to maintain two identical copies of the common base class (virtual base class) like ordinary multiple inheritance, saving storage space.
Virtual Inheritance vs. Virtual Functions
- Similarities: Both utilize virtual pointers (which occupy class storage space) and virtual tables (which do not occupy class storage space).
- Differences:
Virtual Inheritance
- Virtual base classes still exist in the inheriting class, occupying storage space.
- The virtual base class table stores the offset of the virtual base class relative to the directly inheriting class.
Virtual Functions
- Virtual functions do not occupy storage space.
- The virtual function table stores the addresses of virtual functions.
Template Classes, Member Templates, and Virtual Functions
- Template classes can use virtual functions.
- A member template of a class (a member function that is itself a template) cannot be a virtual function.
Abstract Classes, Interface Classes, and Aggregate Classes
Abstract class: A class containing pure virtual functions.
Interface class: An abstract class containing only pure virtual functions.
Aggregate class: A class whose members can be accessed directly by the user and has a special initialization syntax. It meets the following characteristics:
- All members are public.
- No constructors are defined.
- No in-class initializations.
- No base classes and no virtual functions.
Memory Allocation and Management
malloc, calloc, realloc, alloca
- malloc: Allocates memory of a specified byte size. The initial value of the allocated memory is uncertain.
- calloc: Allocates memory that can accommodate a specified number of objects of a specified length. The initial value of each bit in the allocated memory is set to 0.
- realloc: Changes the length of previously allocated memory (increases or decreases). When increasing length, it may need to move the contents of the previously allocated area to another sufficiently large area, while the initial value in the newly added area is uncertain.
- alloca: Allocates memory on the stack. The memory will be automatically released when the program exits the stack. However, it should be noted that alloca is not portable and is difficult to implement on machines without traditional stacks. Alloca should not be used in programs that need to be widely portable. C99 supports variable-length arrays (VLA), which can be used to replace alloca.
malloc, free
Used for allocating and freeing memory.
Usage of malloc and free
Allocate memory and confirm whether the allocation was successful.
char *str = (char*) malloc(100);
assert(str != nullptr);
After freeing memory, set the pointer to null.
free(p);
p = nullptr;
new, delete
- new / new[]: Accomplishes two things: first, it calls malloc to allocate memory, then it calls the constructor (creates the object).
- delete/delete[]: Also accomplishes two things: first, it calls the destructor (cleans up resources), then it calls free to release space.
- new automatically calculates the required byte size when allocating memory, while malloc requires us to input the byte size for memory allocation.
Usage of new and delete
Allocate memory and confirm whether the allocation was successful.
int main()
{
T* t = new T(); // First memory allocation, then constructor
delete t; // First destructor, then memory release
return 0;
}
Placement new
Placement new allows us to pass an additional address parameter to new, thereby creating an object in a pre-specified memory area.
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }
- place_address is a pointer.
- initializers provide a (possibly empty) comma-separated list of initial values.
Is delete this legal?
Legal, but:
- It must be ensured that the this object is allocated via new (not new[], not placement new, not on the stack, not global, not as a member of another object).
- It must be ensured that the member function calling delete this is the last one to call this.
- It must be ensured that after calling delete this, there are no further calls to this.
- It must be ensured that after delete this, no one else uses it.
Defining Classes That Can Only Be Created on the Heap (Stack)
Only on the Heap
Method: Set the destructor to private.
Reason: C++ is a statically bound language, and the compiler manages the lifecycle of stack objects. When the compiler allocates stack space for class objects, it first checks the accessibility of the class’s destructor. If the destructor is inaccessible, the object cannot be created on the stack.
Can be on the Stack
Method: Overload new and delete to be private.
Reason: When creating objects on the heap, the new keyword is used, and the process is divided into two stages: the first stage finds available memory on the heap using new and allocates it to the object; the second stage calls the constructor to create the object. If the new operation is set to private, the first stage cannot be completed, and objects cannot be created on the heap.
Smart Pointers
In the C++ Standard Library (STL)
Header file: #include <memory>
C++98
std::auto_ptr<std::string> ps (new std::string(str));
C++11
For this discussion, readers are welcome to refer to previous articles, are you a “survivor of the future world”?
3.7 Divider
Three or more dashes can be used in a line to create a divider, and a blank line is needed above the divider. As follows:
- shared_ptr
- unique_ptr
- weak_ptr
- auto_ptr (deprecated in C++11)
- Class shared_ptr implements the concept of shared ownership. Multiple smart pointers point to the same object, and that object and its associated resources will be released when “the last reference is destroyed.” To perform this work in more complex scenarios, the standard library provides auxiliary classes such as weak_ptr, bad_weak_ptr, and enable_shared_from_this.
- Class unique_ptr implements the concept of exclusive ownership or strict ownership, ensuring that only one smart pointer can point to that object at any given time. Ownership can be transferred. It is particularly useful for avoiding memory leaks (resource leaks) — such as forgetting to delete after new.
shared_ptr
Multiple smart pointers can share the same object, and the last owner of the object is responsible for destroying it and cleaning up all resources associated with that object.
- Supports custom deleters to prevent cross-DLL issues (objects created with new in a dynamic link library (DLL) but deleted in another DLL), and automatically unlocks mutexes.
weak_ptr
weak_ptr allows you to share but not own an object. Once the last smart pointer that owns the object loses ownership, any weak_ptr will automatically become empty. Thus, outside of default and copy constructors, weak_ptr only provides a constructor that “accepts a shared_ptr.”
- Can break cycles of references (two objects that are no longer in use point to each other, making them appear to still be “in use”).
unique_ptr
unique_ptr is a type introduced in C++11, which helps avoid resource leaks during exceptions. It adopts exclusive ownership, meaning that an object and its corresponding resources can only be owned by one pointer at a time. Once the owner is destroyed or becomes empty, or starts owning another object, the previously owned object will be destroyed, and any corresponding resources will be released.
- unique_ptr is used to replace auto_ptr.
auto_ptr
Deprecated in C++11 due to the lack of language features such as “for construction and assignment” of std::move semantics, as well as other flaws.
Comparison of auto_ptr and unique_ptr
- auto_ptr can be copy assigned, and ownership is transferred after the copy; unique_ptr has no copy assignment semantics but implements move semantics.
- auto_ptr objects cannot manage arrays (destructors call delete), while unique_ptr can manage arrays (destructors call delete[]).
Type Conversion Operators
static_cast
- Used for conversions of non-polymorphic types.
- Does not perform runtime type checking (conversion safety is not as good as dynamic_cast).
- Typically used for converting numeric data types (e.g., float -> int).
- Can safely move pointers throughout the class hierarchy, upward conversion from subclass to superclass is safe, while downward conversion from superclass to subclass is unsafe (as the subclass may have fields or methods not present in the superclass).
dynamic_cast
- Used for conversions of polymorphic types.
- Performs runtime type checking.
- Only applicable to pointers or references.
- Conversion of ambiguous pointers will fail (return nullptr) but will not throw an exception.
- Can move pointers throughout the class hierarchy, including upward and downward conversions.
const_cast
- Used to remove const, volatile, and __unaligned attributes (e.g., converting const int type to int type).
reinterpret_cast
- Used for simple reinterpretation of bits.
- Abuse of reinterpret_cast can easily lead to risks. Unless the required conversion is inherently low-level, one should use one of the other type conversion operators.
- Allows any pointer to be converted to any other pointer type (e.g., char* to int* or One_class* to Unrelated_class*, but this is not safe).
- Also allows any integer type to be converted to any pointer type and vice versa.
- reinterpret_cast cannot discard const, volatile, or __unaligned attributes.
- A practical use of reinterpret_cast is in hash functions, mapping two different values to indices that almost never end up at the same index.
bad_cast
- dynamic_cast operator throws a bad_cast exception due to a failed forced conversion to a reference type.
Usage of bad_cast
try {
Circle& ref_circle = dynamic_cast<Circle&>(ref_shape);
}
catch (bad_cast b) {
cout << "Caught: " << b.what();
}
Runtime Type Information (RTTI)
dynamic_cast
- Used for conversions of polymorphic types.
typeid
- The typeid operator allows determining the type of an object at runtime.
- type_id returns a reference to a type_info object.
- If you want to obtain the data type of a derived class through a base class pointer, the base class must have virtual functions.
- Can only obtain the actual type of the object.
type_info
- The type_info class describes the type information generated by the compiler in the program. Objects of this class can effectively store pointers to the names of types. The type_info class can also store encoded values suitable for comparing whether two types are equal or comparing their order. The encoding rules and order of types are unspecified and may vary by program.
- Header file: typeinfo
Usage of typeid and type_info
#include <iostream>
using namespace std;
class Flyable // Can fly
{
public:
virtual void takeoff() = 0; // Take off
virtual void land() = 0; // Land
};
class Bird : public Flyable // Bird
{
public:
void foraging() {...} // Foraging
virtual void takeoff() {...}
virtual void land() {...}
virtual ~Bird(){}
};
class Plane : public Flyable // Plane
{
public:
void carry() {...} // Transport
virtual void takeoff() {...}
virtual void land() {...}
};
class type_info
{
public:
const char* name() const;
bool operator == (const type_info & rhs) const;
bool operator != (const type_info & rhs) const;
int before(const type_info & rhs) const;
virtual ~type_info();
private:
...
};
void doSomething(Flyable *obj) // Do something
{
obj->takeoff();
cout << typeid(*obj).name() << endl; // Outputs the type of the passed object ("class Bird" or "class Plane")
if(typeid(*obj) == typeid(Bird)) // Determine object type
{
Bird *bird = dynamic_cast<Bird *>(obj); // Object conversion
bird->foraging();
}
obj->land();
}
int main(){
Bird *b = new Bird();
doSomething(b);
delete b;
b = nullptr;
return 0;
}
References
https://github.com/huihut/interview#effective