Hubbry Logo
C++ classesC++ classesMain
Open search
C++ classes
Community hub
C++ classes
logo
7 pages, 0 posts
0 subscribers
Be the first to start a discussion here.
Be the first to start a discussion here.
Contribute something
C++ classes
C++ classes
from Wikipedia

A class in C++ is a user-defined type or data structure declared with any of the keywords class, struct or union (the first two are collectively referred to as non-union classes) that has data and functions (also called member variables and member functions) as its members whose access is governed by the three access specifiers private, protected or public. By default access to members of a C++ class declared with the keyword class is private. The private members are not accessible outside the class; they can be accessed only through member functions of the class. The public members form an interface to the class and are accessible outside the class.

Instances of a class data type are known as objects and can contain member variables, constants, member functions, and overloaded operators defined by the programmer.

Differences between a struct and a class in C++

[edit]

In C++, a class defined with the class keyword has private members and base classes by default. A structure is a class defined with the struct keyword.[1] Its members and base classes are public by default. In practice, structs are typically reserved for data without functions. When deriving a struct from a class/struct, default access-specifier for a base class/struct is public. And when deriving a class, default access specifier is private.

Aggregate classes

[edit]

An aggregate class is a class with no user-declared constructors, no private or protected non-static data members, no base classes, and no virtual functions.[2] Such a class can be initialized with a brace-enclosed comma-separated list of initializer-clauses.[3] The following code has the same semantics in both C and C++.

struct C {
    int a;
    double b;
};

struct D {
    int a; 
    double b;
    C c;
};

// initialize an object of type C with an initializer-list
C c = {1, 2.0};

// D has a sub-aggregate of type C. In such cases initializer-clauses can be nested
D d = {10, 20.0, {1, 2.0}};

POD-structs

[edit]

A POD-struct (Plain Old Data Structure) is a non-union aggregate class that has no non-static data members of type non-POD-struct, non-POD-union (or array of such types) or reference, and has no user-defined assignment operator and no user-defined destructor.[1] A POD-struct could be said to be the C++ equivalent of a C struct. In most cases, a POD-struct will have the same memory layout as a corresponding struct declared in C.[4] For this reason, POD-structs are sometimes colloquially referred to as "C-style structs".[5]

Properties shared between structs in C and POD-structs in C++

[edit]
  • Data members are allocated so that later members have higher addresses within an object, except where separated by an access-specifier.[6]
  • Two POD-struct types are layout-compatible if they have the same number of nonstatic data members, and corresponding nonstatic data members (in order) have layout-compatible types.[7]
  • A POD-struct may contain unnamed padding.[8]
  • A pointer to a POD-struct object, suitably converted using a reinterpret cast, points to its initial member and vice versa, implying that there is no padding at the beginning of a POD-struct.[8]
  • A POD-struct may be used with the offsetof macro.[9]

Declaration and usage

[edit]

C++ classes have their own members. These members include variables (including other structures and classes), functions (specific identifiers or overloaded operators) known as member functions, constructors and destructors. Members are declared to be either publicly or privately accessible using the public: and private: access specifiers respectively. Any member encountered after a specifier will have the associated access until another specifier is encountered. There is also inheritance between classes which can make use of the protected: specifier.

The this keyword

[edit]

To facilitate classes' ability to reference themselves, C++ implements the this keyword for all member functions. The this keyword acts as a pointer to the current object.[10] Its type is that of a pointer to the current object.

The this keyword is especially important for member functions with the class itself as the return value:

Complex& operator+=(const Complex& c) noexcept {
    this->realPart += c.realPart;
    this->imagPart += c.imagPart;
    return *this;
}

As stated above, this is a pointer, so the use of the asterisk (*) is necessary to convert it into a reference to be returned.

Global and local class

[edit]

A class defined outside all functions is a global class because its objects can be created from anywhere in the program. If it is defined within a function body then it's a local class because objects of such a class are local to the function scope.

Basic declaration and member variables

[edit]

Non-union classes are declared with the class or struct keyword. Declaration of members are placed within this declaration.

using std::string;

struct Person {
    string name;
    int age;
};
using std::string;

class Person {
public:
    string name;
    int age;
};

The above definitions are functionally equivalent. Either code will define objects of type Person as having two public data members, name and age. The semicolons after the closing braces are mandatory.

After one of these declarations (but not both), Person can be used as follows to create newly defined variables of the Person datatype:

import std;

using std::string;

struct Person {
    string name;
    int age;
};

int main() {
    Person a;
    Person b;
    a.name = "Calvin";
    b.name = "Hobbes";
    a.age = 30;
    b.age = 20;
    std::println("{}: {}", a.name, a.age);
    std::println("{}: {}", b.name, b.age);
}

Executing the above code will output

Calvin: 30
Hobbes: 20

Member functions

[edit]

An important feature of the C++ class are member functions. Each datatype can have its own built-in functions (referred to as member functions) that have access to all (public and private) members of the datatype. In the body of these non-static member functions, the keyword this can be used to refer to the object for which the function is called. This is commonly implemented by passing the address of the object as an implicit first argument to the function.[11] Take the above Person type as an example again:

import std;

using std::string;

class Person {
private:
    string name;
    int age = 5;
public:
    // "name" and "age" are the member variables. The "this" keyword is an
    // expression whose value is the address of the object for which the member
    // was invoked. Its type is "const Person*", because the function is declared
    // const.
    void printData() const {
        std::println("{}: {}", name, age);
    }
};

In the above example the printData method is declared in the body of the class and defined by qualifying it with the name of the class followed by ::. Both name and age are private (default for class) and printData is declared as public which is necessary if it is to be used from outside the class.

With the member function printData, printing can be simplified into:

a.printData();
b.printData();

where a and b above are called senders, and each of them will refer to their own member variables when the printData() function is executed.

Prior to the introduction of modules (i.e. using headers), one would usually separate the class or structure declaration (called its interface) and the definition (called its implementation) into separate units. The interface, needed by the user, would be kept in a header file and the implementation would be kept separately in either source or compiled form.

Inheritance

[edit]

The layout of non-POD classes in memory is not specified by the C++ standard. For example, many popular C++ compilers implement single inheritance by concatenation of the parent class fields with the child class fields, but this is not required by the standard. This choice of layout makes referring to a derived class via a pointer to the parent class type a trivial operation.

For example, consider

struct P {
    int x;
};
struct C : public P {
    int y;
};

An instance of P with a P* p pointing to it might look like this in memory:

┏━━━━┓
┃P::x┃
┗━━━━┛
↑
p

An instance of C with a P* p pointing to it might look like this:

┏━━━━┳━━━━┓
┃P::x┃C::y┃
┗━━━━┻━━━━┛
↑
p

Therefore, any code that manipulates the fields of a P object can manipulate the P fields inside the C object without having to consider anything about the definition of C's fields. A properly written C++ program shouldn't make any assumptions about the layout of inherited fields, in any case. Using the static_cast or dynamic_cast type conversion operators will ensure that pointers are properly converted from one type to another.

Multiple inheritance is not as simple. If a class D inherits P and C, then the fields of both parents need to be stored in some order, but (at most) only one of the parent classes can be located at the front of the derived class. Whenever the compiler needs to convert a pointer from the D type to either P or C, the compiler will provide an automatic conversion from the address of the derived class to the address of the base class fields (typically, this is a simple offset calculation).

For more on multiple inheritance, see virtual inheritance.

The final keyword limits the ways in which a class can be subclassed.[12] Subclasses of a class are prevented from overriding methods marked as final by the parent class.[13][14] Final classes cannot be inherited.[14] This allows devirtualization, the removal of the use of vtables for method lookup, thus allowing the inlining of method calls on final classes.[15][16]

// final in a class declaration declares that a class cannot be extended
class Z final : public X, public Y {
public:
    // final in a method signature declares that a method cannot be overridden further
    void someOperation() override final {
        // do something here
    }
};

final is not a reserved word in C++, and is instead defined as a contextual keyword, in order to not conflict with uses of the identifier 'final' in existing codebases.[17][18]

Overloaded operators

[edit]

In C++, operators, such as + - * /, can be overloaded to suit the needs of programmers. These operators are called overloadable operators.

By convention, overloaded operators should behave nearly the same as they do in built-in datatypes (int, float, etc.), but this is not required. One can declare a structure called Integer in which the variable really stores an integer, but by calling Integer * Integer the sum, instead of the product, of the integers might be returned:

struct Integer {
    Integer() = default;
    Integer(int j): 
        i{j} {}

    Integer operator*(const Integer& k) const {
        return Integer(i + k.i);
    }

    int i = 0;
};

The code above made use of a constructor to "construct" the return value. For clearer presentation (although this could decrease efficiency of the program if the compiler cannot optimize the statement into the equivalent one above), the above code can be rewritten as:

Integer operator*(const Integer& k) const {
    Integer m;
    m.i = i + k.i;
    return m;
}

Programmers can also put a prototype of the operator in the struct declaration and define the function of the operator in the global scope:

struct Integer {
    Integer() = default;
    Integer(int j): 
        i{j} {}

    Integer operator*(const Integer& k) const;

    int i = 0;
};
 
Integer Integer::operator*(const Integer& k) const {
    return Integer(i * k.i);
}

i above represents the sender's own member variable, while k.i represents the member variable from the argument variable k.

The const keyword appears twice in the above code. The first occurrence, the argument const integer& k, indicated that the argument variable will not be changed by the function. The second incidence at the end of the declaration promises the compiler that the sender would not be changed by the function run.

In const integer& k, the ampersand (&) means "pass by reference". When the function is called, a reference to the variable will be passed to the function, rather than the value of the variable.

Note that associativity and precedence of operators cannot be changed.

Binary overloadable operators

[edit]

Binary operators (operators with two arguments) are overloaded by declaring a function with an "identifier" operator (something) which calls one single argument. The variable on the left of the operator is the sender while that on the right is the argument.

Integer i = 1; 
/* we can initialize a structure variable this way as
   if calling a constructor with only the first
   argument specified. */
Integer j = 3;
/* variable names are independent of the names of the
   member variables of the structure. */
Integer k = i * j;
std::println("{}", k.i);

'3' would be printed.

The following is a list of binary overloadable operators:

Operator General usage
+ - * / % Arithmetic calculation
^ & ! << >> Bitwise calculation
< > == != <= >= Logical comparison
&& Logical conjunction
|| Logical disjunction
= <<= >>= Compound assignment
, (no general usage)

The '=' (assignment) operator between two variables of the same structure type is overloaded by default to copy the entire content of the variables from one to another. It can be overwritten with something else, if necessary.

Operators must be overloaded one by one, in other words, no overloading is associated with one another. For example, < is not necessarily the opposite of >.

Unary overloadable operators

[edit]

While some operators, as specified above, takes two terms, sender on the left and the argument on the right, some operators have only one argument - the sender, and they are said to be "unary". Examples are the negative sign (when nothing is put on the left of it) and the "logical NOT" (exclamation mark, !).

Sender of unary operators may be on the left or on the right of the operator. The following is a list of unary overloadable operators:

Operator General usage Position of sender
+ - Positive / negative sign right
* & Dereference right
! ~ Logical / bitwise NOT right
++ -- Pre-increment / decrement right
++ -- Post-increment / decrement left

The syntax of an overloading of a unary operator, where the sender is on the right, is as follows:

return_type operator@ ()

When the sender is on the left, the declaration is:

return_type operator@ (int)

@ above stands for the operator to be overloaded. Replace return_type with the datatype of the return value (int, bool, structures etc.)

The int parameter essentially means nothing but a convention to show that the sender is on the left of the operator.

const arguments can be added to the end of the declaration if applicable.

Overloading brackets

[edit]

The square bracket [] and the round bracket () can be overloaded in C++ classes. The square bracket must contain exactly one argument, initialiser list (since C++11) or arbitrary parameter pack (since C++23), while the round bracket can contain any specific number of arguments, or no arguments.

The following declaration overloads the square bracket.

return_type operator[] (arg1, arg2, ...))

The content inside the bracket is specified in the argument part.

Round bracket is overloaded a similar way.

return_type operator() (arg1, arg2, ...)

Contents of the bracket in the operator call are specified in the second bracket.

In addition to the operators specified above, the arrow operator (->), the starred arrow (->*), the new keyword and the delete keyword can also be overloaded. These memory-or-pointer-related operators must process memory-allocating functions after overloading. Like the assignment (=) operator, they are also overloaded by default if no specific declaration is made.

Constructors

[edit]

Sometimes programmers may want their variables to take a default or specific value upon declaration. This can be done by declaring constructors.

using std::string;

Person::Person(string name, int age) {
    this->name = name;
    this->age = age;
}

Member variables can be initialized in an initializer list, with utilization of a colon, as in the example below. This differs from the above in that it initializes (using the constructor), rather than using the assignment operator. This is more efficient for class types, since it just needs to be constructed directly; whereas with assignment, they must be first initialized using the default constructor, and then assigned a different value. Also some types (like references and const types) cannot be assigned to and therefore must be initialized in the initializer list.

using std::string;

Person(string name, int age): 
    name{name}, age{age} {}

Note that the curly braces cannot be omitted, even if empty.

Default values can be given to the last arguments to help initializing default values.

using std::string;

Person(string name = "", int age = 0): 
    name{name}, age{age} {}

When no arguments are given to the constructor in the example above, it is equivalent to calling the following constructor with no arguments (a default constructor):

Person(): 
    name{""}, age{0} {}

The declaration of a constructor looks like a function with the same name as the datatype. In fact, a call to a constructor can take the form of a function call. In that case an initialized Person type variable can be thought of as the return value:

int main() {
    Person r = Person("Wales", 40);
    r.print();
}

An alternate syntax that does the same thing as the above example is

int main() {
    Person r("Wales", 40);
    r.printData();
}

Specific program actions, which may or may not relate to the variable, can be added as part of the constructor.

Person() {
    std::println("Hello!");
}

With the above constructor, a "Hello!" will be printed when the default Person constructor is invoked.

Default constructor

[edit]

Default constructors are called when constructors are not defined for the classes.

struct A {
    int b;
};
// Object created using parentheses.
A* a = new A();  // Calls default constructor, and b will be initialized with '0'.
// Object created using no parentheses.
A* a = new A;  // Allocate memory, then call default constructor, and b will have value '0'.
// Object creation without new.
A a;  // Reserve space for a on the stack, and b will have an unknown garbage value.

However, if a user defined constructor was defined for the class, both of the above declarations will call this user defined constructor, whose defined code will be executed, but no default values will be assigned to the variable b.

Destructors

[edit]

A destructor is the inverse of a constructor. It is called when an instance of a class is destroyed, e.g. when an object of a class created in a block (set of curly braces "{}") is deleted after the closing brace, then the destructor is called automatically. It will be called upon emptying of the memory location storing the variables. Destructors can be used to release resources, such as heap-allocated memory and opened files when an instance of that class is destroyed.

The syntax for declaring a destructor is similar to that of a constructor. There is no return value and the name of the function is the same as the name of the class with a tilde (~) in front.

~Person() {
    std::println("I'm deleting {} with age {}", name, age);
}

Similarities between constructors and destructors

[edit]
  • Both have same name as the class in which they are declared.
  • If not declared by user both are available in a class by default but they now can only allocate and deallocate memory from the objects of a class when an object is declared or deleted.
  • For a derived class: During the runtime of the base class constructor, the derived class constructor has not yet been called; during the runtime of the base class destructor, the derived class destructor has already been called. In both cases, the derived class member variables are in an invalid state.

Class templates

[edit]

In C++, class declarations can be generated from class templates. Such class templates represent a family of classes. An actual class declaration is obtained by instantiating the template with one or more template arguments. A template instantiated with a particular set of arguments is called a template specialization.

Properties

[edit]

The syntax of C++ tries to make every aspect of a class look like that of the basic datatypes. Therefore, overloaded operators allow classes to be manipulated just like integers and floating-point numbers, arrays of classes can be declared with the square-bracket syntax (some_structure variable_name[size]), and pointers to classes can be dereferenced in the same way as pointers to built-in datatypes.

Memory consumption

[edit]

The memory consumption of a structure is at least the sum of the memory sizes of constituent variables. Take the TwoNums structure below as an example.

struct TwoNums {
    int a;
    int b;
};

The structure consists of two integers. In many current C++ compilers, integers are 32-bit integers by default, so each of the member variables consume four bytes of memory. The entire structure, therefore, consumes at least (or exactly) eight bytes of memory, as follows.

+----+----+
| a  | b  |
+----+----+

However, the compiler may add padding between the variables or at the end of the structure to ensure proper data alignment for a given computer architecture, often padding variables to be 32-bit aligned. For example, the structure

struct BytesAndSuch { 
    char c;
    char C;
    char D;
    short int s;
    int i;
    double d;
};

could look like

+-+-+-+-+--+--+----+--------+
|c|C|D|X|s |XX|  i |   d    |
+-+-+-+-+--+--+----+--------+

in memory, where X represents padded bytes based on 4 bytes alignment.

As structures may make use of pointers and arrays to declare and initialize its member variables, memory consumption of structures is not necessarily constant. Another example of non-constant memory size is template structures.

Bit fields

[edit]

Bit fields are used to define the class members that can occupy less storage than an integral type. This field is applicable only for integral types (int, char, short, long, etc.) and enumeration types (e.g. std::byte) and excludes float or double.

struct A { 
    unsigned a:2;  // Possible values 0..3, occupies first 2 bits of int
    unsigned b:3;  // Possible values 0..7, occupies next 3 bits of int
    unsigned :0;  // Moves to end of next integral type
    unsigned c:2; 
    unsigned :4;  // Pads 4 bits in between c & d
    unsigned d:1;
    unsigned e:3;
};
  • Memory structure
	 4 byte int  4 byte int
	[1][2][3][4][5][6][7][8]
	[1]                      [2]                      [3]                      [4]
	[a][a][b][b][b][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ]

	[5]                      [6]                      [7]                      [8]
	[c][c][ ][ ][ ][ ][d][e] [e][e][ ][ ][ ][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ]

Unions are also allowed to have bit-field members:

union A { 
    unsigned a:2;
    unsigned b:3;
    unsigned :0; // Does nothing
    unsigned c:2; 
    unsigned :4; // Does nothing in this case; if the bit-field's width were large enough, it would change the union's size to fit
    unsigned d:1;
    unsigned e:3;
};

Pass by reference

[edit]

Many programmers prefer to use the ampersand (&) to declare the arguments of a function involving structures. This is because by using the dereferencing ampersand only one word (typically 4 bytes on a 32 bit machine, 8 bytes on a 64 bit machine) is required to be passed into the function, namely the memory location to the variable. Otherwise, if pass-by-value is used, the argument needs to be copied every time the function is called, which is costly with large structures.

Since pass-by-reference exposes the original structure to be modified by the function, the const keyword should be used to guarantee that the function does not modify the parameter (see const-correctness), when this is not intended.

See also

[edit]

References

[edit]
Revisions and contributorsEdit on WikipediaRead on Wikipedia
from Grokipedia
In C++, a class is a user-defined type that serves as the foundation for by encapsulating data members and member functions within a single unit, enabling features like encapsulation, , and polymorphism. Classes were first introduced by in 1979 as part of his work on "C with Classes," an extension of the language inspired by 67 to support abstract data types while maintaining efficiency for . The initial implementation of classes became available internally at in 1983, marking the early evolution toward the standardized C++ language. Classes in C++ are declared using the class keyword (or struct for default access), followed by the class name and a body enclosed in braces, where members are defined. members represent the state of objects, while member functions define their and include constructors for initialization, for cleanup, and other special functions like copy constructors. Access to members is controlled by specifiers such as [public](/page/Public) (accessible from anywhere), private (accessible only within the class and by friends), and protected (accessible within the class, friends, and derived classes), with class defaulting to private access and struct to . Key features of C++ classes include support for , where a derived class can extend or override base class members to promote , and polymorphism achieved through virtual functions that enable runtime binding for . Classes can also be abstract if they contain pure virtual functions, preventing direct instantiation, or polymorphic to support runtime type identification via tools like dynamic_cast and typeid. Classes support nested classes for modular design (available since early C++); modern standards, such as and later, further enhance classes with features like constexpr constructors for compile-time evaluation and move semantics for efficient resource transfer. These elements make classes versatile for building complex, maintainable software while adhering to the C++ standard's emphasis on zero-overhead abstraction.

Fundamentals

Basic Declaration and Syntax

In C++, a class is a user-defined type that serves as the foundation for by encapsulating data and functions. The basic declaration of a class uses the class keyword followed by an optional attribute list, the class name, an optional base class clause, an opening brace, a member specification (which includes declarations of data members, member functions, and other nested types), a closing brace, and a terminating . This structure defines the class type completely if all members are declared inline, or incompletely if only a forward declaration is provided. A of a class, which introduces the class name as an incomplete type without specifying its members, follows the syntax class identifier;. This is useful for declaring pointers or references to the class in header files before its full , allowing mutual references between classes or minimizing compilation dependencies without revealing implementation details. For instance, in scenarios involving circular dependencies, such as two classes each containing a pointer to the other, forward declarations enable the to recognize the types early. The following example illustrates a simple class declaration with a private data member and a public member function:

cpp

class Simple { private: int value; // Private data member public: void setValue(int v) { value = v; } // Inline member function };

class Simple { private: int value; // Private data member public: void setValue(int v) { value = v; } // Inline member function };

This declares a class named Simple where value is accessible only within the class scope, and setValue provides controlled access from outside. Note that access specifiers like private and public organize the member specification, though their detailed semantics are covered elsewhere. Instantiation of a class creates objects of that type by declaring variables of the class type, which typically invokes a constructor to initialize the object. For example:

cpp

Simple obj; // Instantiates an object, calling the default constructor if available

Simple obj; // Instantiates an object, calling the default constructor if available

Details on constructors and initialization are covered in the Special Member Functions section. Class scope governs the visibility of names declared within the class: all members are introduced into the class's own scope, making them visible only inside the class body or when explicitly qualified using the class name (e.g., Simple::value). This encapsulation ensures that class-internal names do not pollute the enclosing scope unless brought in via qualified access or using declarations. Local classes, declared within a function, are restricted to that function's scope and cannot be used as types outside it.

Access Specifiers

Access specifiers in C++ are keywords that define the accessibility of class members, enabling encapsulation by restricting how data members and member functions can be used outside the class definition. The three primary access specifiers are public, private, and protected, each introduced by a label followed by a colon within the class body. These specifiers can appear multiple times in a class declaration, dividing members into sections with varying visibility levels. The public specifier allows members to be accessed from any scope, making them part of the class's external interface. In contrast, private restricts access to the class itself and its friends, preventing direct external manipulation to protect internal state. The protected specifier permits access within the class, its friends, and derived classes, providing a mechanism for controlled extension in hierarchies. By default, members of a class are private, emphasizing encapsulation for object-oriented design. For a struct, the default is public, aligning with its historical use as a simple data aggregate similar to C structs. Consider a basic example of a class using access specifiers:

cpp

class Example { private: int secret; // Accessible only within Example or its friends public: int visible; // Accessible from anywhere void setSecret(int value) { secret = value; } // Public function to modify private data protected: int semiPrivate; // Accessible in derived classes };

class Example { private: int secret; // Accessible only within Example or its friends public: int visible; // Accessible from anywhere void setSecret(int value) { secret = value; } // Public function to modify private data protected: int semiPrivate; // Accessible in derived classes };

Here, visible can be directly accessed as Example obj; obj.visible = 10;, while attempting obj.secret = 5; results in a compile-time error due to private access. Similarly, semiPrivate would cause an error if accessed directly but could be used in a derived class. Access specifiers have remained fundamentally unchanged since the C++98 standard, with no significant modifications in subsequent revisions including and C++23.

Structs and Classes

Key Differences

In C++, the primary distinction between a class and a struct lies in their default member access specifiers: members of a class are private by default, whereas members of a struct are public by default. This design choice enforces encapsulation in classes while maintaining compatibility with C-style data aggregation in structs. All other semantics, including , member functions, and constructors, are identical between the two. Despite this difference, class and struct are fully interchangeable in functionality. Any class definition can be rewritten as a struct by explicitly specifying access specifiers to match the desired visibility, and vice versa. For instance, consider a simple class that encapsulates a private integer data member with a public getter:

cpp

class ExampleClass { private: int value; public: int getValue() const { return value; } };

class ExampleClass { private: int value; public: int getValue() const { return value; } };

This can be equivalently expressed as a struct by adjusting the defaults:

cpp

struct ExampleStruct { int value; // Now public by default int getValue() const { return value; } };

struct ExampleStruct { int value; // Now public by default int getValue() const { return value; } };

Both definitions produce the same type with identical behavior, demonstrating their equivalence beyond access control. Historically, the struct keyword originated in C for defining simple aggregate data types with public access, emphasizing compatibility and low-level control. In contrast, Bjarne Stroustrup introduced class in the early 1980s as part of "C with Classes" to support object-oriented programming principles like data hiding and abstraction, inspired by Simula, with private access as the default to promote encapsulation. This duality allows C++ to bridge procedural C code with modern OOP while preserving backward compatibility. Apart from default access, there are no other functional differences at the language level.

Aggregate Classes and POD Types

In C++, an aggregate is an array type or a class type that satisfies specific criteria, enabling straightforward initialization without invoking constructors. For a class to qualify as an aggregate, it must have no user-declared or inherited constructors, no private or protected non-static data members, no virtual functions, no virtual base classes, and no base classes of non-trivial types (with refinements across standards: pre- allowed no user-declared constructors; prohibited virtual functions; permitted public base classes; tightened constructor rules to exclude inherited ones). All direct non-static data members must be and themselves aggregates or types, ensuring the class remains simple and compatible for brace initialization. Aggregate initialization uses brace-enclosed lists to directly assign values to members in declaration order, bypassing any constructors and supporting both copy and direct list forms since . For example, the syntax struct Point { int x, y; }; Point p = {1, 2}; or Point p{1, 2}; initializes p.x to 1 and p.y to 2, with adding designated initializers like Point p{.x = 1, .y = 2}; for named member assignment regardless of order (though values are still placed in declaration sequence). This form extends to arrays of aggregates, such as Point points[2] = {{1, 2}, {3, 4}};, and prohibits narrowing conversions since to ensure . A POD (Plain Old Data) struct is a non-union class that is both a trivial class and a standard-layout class, with all non-static data members also being POD types or arrays thereof; this category was deprecated in C++20 in favor of separate triviality and standard-layout checks via traits like std::is_trivial and std::is_standard_layout. Trivial classes have no user-provided constructors, destructors, copy/move operations, or virtual functions, while standard-layout classes ensure a predictable memory layout without multiple or virtual inheritance complications. POD types share key properties with C structs, including binary layout compatibility for interoperability (e.g., no additional padding beyond what C would insert, and no constructor/destructor overhead), making them suitable for direct memory manipulation or interfacing with C code. C++11 introduced the concepts of trivial and standard-layout types, relaxing the pre-C++11 POD definition (which required no user-declared constructors, no base classes, no virtual functions, and POD members) by decoupling these properties for finer-grained control, while C++20 further deprecated POD in library traits but retained the underlying behaviors for legacy and compatibility purposes. For instance, the following is a valid aggregate and POD struct:

cpp

struct PODPoint { int x; int y; };

struct PODPoint { int x; int y; };

This can be initialized as PODPoint pp{1, 2}; with C-compatible layout. In contrast, adding a private member or user-declared constructor disqualifies it as an aggregate:

cpp

struct NonAggregate { int x; private: int y; // Private member prevents aggregate status };

struct NonAggregate { int x; private: int y; // Private member prevents aggregate status };

Such a class requires explicit constructor use for initialization, losing direct brace-init compatibility.

Class Members

Data Members

Data members in a C++ class are variables declared within the class definition that store the state of objects of that class. They are also known as instance variables for non-static members, as each object maintains its own copy. Access to data members can be controlled using access specifiers such as , protected, or private. Non-static data members are declared inside the class body with a type specifier followed by the member name, optionally including an initializer starting from C++11. The syntax allows for types like fundamental types, pointers, references, or user-defined types. For example, an integer data member can be declared as int count;, a pointer as int* ptr;, and a as int& ref;, where the must be initialized in the constructor's initializer list since it cannot have an in-class default initializer. In-class initializers, introduced in C++11, permit expressions like int count = 0; directly in the declaration, which are used if no constructor initializer is provided. The initialization order of non-static data members occurs in the order of their declaration within the class, regardless of the order in the . This ensures deterministic , preventing issues from dependencies on uninitialized members. For instance, if a class declares int x; double y;, then x is initialized before y during object creation. Virtual base classes follow a specific order, but for direct members, declaration sequence governs. Certain data members can be marked with the mutable keyword, allowing modification even within const member functions or for const objects. Declared as, for example, mutable int cache;, mutable members are useful for internal state like caches or debug flags that do not affect the logical constness of the object. This feature, part of the core language since C++98, bypasses const restrictions only for these members. Static data members are shared across all objects of the class and declared inside the class with the static keyword, such as static int shared_value;. Unlike non-static members, they require a separate definition outside the class in a single translation unit, e.g., int MyClass::shared_value = 42;, to allocate storage. Static members can be const or constexpr, with in-class initializers allowed for integral types since C++11 and expanded in later standards. They are initialized before any objects of the class are created and can be accessed without an instance. C++ imposes restrictions on data members: they cannot include static local variables from functions, as members must be class-scope declarations. Additionally, the total size of non-static data members influences the , which is implementation-defined but typically limited by available and alignment requirements, with no explicit language-imposed upper bound beyond practical limits. may be inserted for alignment, affecting sizeof the class.

Member Functions

Member functions in C++ are functions declared within a class that operate on instances of that class or the class itself, enabling encapsulation of alongside . They can be defined either inline within the class declaration or separately outside it using the scope resolution operator :: to qualify the function name with the class name. For example, a declaration inside the class might look like void func();, with an inline definition as void func() { /* body */ }, or an out-of-class definition as void MyClass::func() { /* body */ }. Non-static member functions implicitly receive a pointer to the object instance as the first , referred to as this, allowing them to access and manipulate the object's non-static data members and other non-static members. This implicit enables operations like obj.member = value; to translate to (*this).member = value; under the hood. Such functions are invoked on an object using the dot operator (.) or arrow operator (->) for pointers, and they cannot be called without an instance unless qualified otherwise. Static member functions, declared with the static keyword, do not have an implicit this pointer and thus cannot access non-static members directly; they operate at the class level and are typically used for utility functions or accessing static data. They are called using the class name followed by ::, such as MyClass::staticFunc(), though they can also be invoked on an instance for convenience. For instance:

cpp

class MyClass { public: static void staticFunc() { // Access static members only } static int staticVar; }; int MyClass::staticVar = 0; MyClass::staticFunc(); // Call on class

class MyClass { public: static void staticFunc() { // Access static members only } static int staticVar; }; int MyClass::staticVar = 0; MyClass::staticFunc(); // Call on class

This design ensures static functions behave like regular functions but remain scoped to the class. To support operations on constant objects, member functions can be qualified with const after the parameter list, such as int getValue() const;, which promises not to modify the object's non-mutable data members by treating *this as a const reference. Non-const overloads can coexist with const versions, allowing flexible usage; for example, a const object calls the const variant, while a non-const object can call either. Mutable data members are an exception, as they can be modified even in const functions. Member functions support overloading, where multiple functions share the same name but differ in parameter lists, cv-qualifiers (like const or volatile), or reference qualifiers (such as & for lvalues or && for rvalues since ). Overloading is resolved based on the argument types and qualifiers, but not on return types or noexcept specifications. An example of ref-qualified overloads:

cpp

class Example { public: void func() & { /* lvalue this */ } void func() && { /* rvalue this */ } };

class Example { public: void func() & { /* lvalue this */ } void func() && { /* rvalue this */ } };

This allows tailored behavior for different object states during overload resolution.

The this Pointer

In C++, the this pointer is an implicit prvalue expression that provides the of the object on which a non-static member function is called, serving as a to the current instance of the class. It is automatically passed as a hidden argument by the to all non-static member functions, enabling access to the object's members within those functions. The this pointer cannot be used in static member functions, as they do not operate on a specific object instance, and it is never null in a valid function call on an existing object. The type of this depends on the member function's qualifiers: in a non-const member function, it is ClassName* const, where ClassName is the class name, making it a constant pointer to a non-const object; in a const member function, it becomes const ClassName* const, adding const qualification to the object as well. Similar adjustments apply for volatile or other cv-qualifiers on the function. This design ensures that modifications to the object are restricted appropriately, preventing accidental changes in const contexts. Common uses of this include disambiguating member names from local variables or parameters with the same name, such as this->member = value; to refer explicitly to the class member. It can also be passed to other functions for self-reference, for example, to base class methods or external APIs requiring the object pointer. A key application is returning *this from member functions to enable , allowing sequential calls like obj.setA(1).setB(2);. The following example illustrates chaining in a simple class:

cpp

class Example { public: Example& setValue(int val) { value = val; return *this; // Returns reference to current object } private: int value; }; // Usage Example ex; ex.setValue(42).setValue(100); // [Chaining](/page/Chaining) works via *this

class Example { public: Example& setValue(int val) { value = val; return *this; // Returns reference to current object } private: int value; }; // Usage Example ex; ex.setValue(42).setValue(100); // [Chaining](/page/Chaining) works via *this

In operator overloading, this facilitates self-referential operations, such as in assignment operators where *this = other; copies data to the current object. Since , this can be captured in lambda expressions defined within member functions, allowing the lambda to access the enclosing object's members via the captured pointer. For instance, capturing this by value (the default in C++17) or explicitly enables callbacks or deferred operations that operate on the original object, as shown below:

cpp

class LambdaExample { public: void process() { auto lambda = [this]() { /* Access this->members here */ }; lambda(); } };

class LambdaExample { public: void process() { auto lambda = [this]() { /* Access this->members here */ }; lambda(); } };

This feature extends the utility of this beyond direct member functions, integrating it with modern C++ constructs like lambdas while maintaining the implicit object semantics.

Special Member Functions

Constructors

In C++, a constructor is a special non-static member function of a class that is automatically invoked upon the instantiation of an object of that class type—such as through declaration of automatic variables, dynamic allocation, or temporary object creation—serving to initialize its data members and establish the object's initial state. It bears the same name as the class, has no return type (not even void), and can take parameters to customize initialization. Constructors may be declared with specifiers such as inline, constexpr (since C++11), consteval (since C++20), or explicit, but cannot have cv-qualifiers or ref-qualifiers. A default constructor is one that can be called with no arguments, either explicitly or implicitly. If no user-declared constructors of any kind are provided in the class, the implicitly declares a default constructor as an inline public member of the class; this implicit constructor is trivial for plain old data () types, performing no initialization beyond what is required for the underlying members. For non- classes without user-provided constructors, the implicit default constructor default-initializes base classes and members but performs no other actions. Since , default constructors can be explicitly defaulted using = default or deleted using = delete to control their generation. Parameterized constructors accept one or more arguments to allow flexible initialization, and multiple overloads can be defined to handle different scenarios. For instance, a constructor might take multiple parameters to set corresponding data members:

cpp

class Point { int x, y; public: Point(int a, int b) : x(a), y(b) {} // Parameterized constructor };

class Point { int x, y; public: Point(int a, int b) : x(a), y(b) {} // Parameterized constructor };

Special forms include the copy constructor, with signature Class(const Class& other), which initializes a new object as a copy of an existing one, and the move constructor (since C++11), with signature Class(Class&& other), which transfers resources from a temporary object to avoid unnecessary copying. These are typically user-defined for classes managing resources, but the compiler generates trivial versions if not explicitly provided and certain conditions are met. Constructors often use a member initializer list, introduced after the parameter list with a colon (:), to initialize base classes and non-static data members before the constructor body executes; this is the only way to initialize references, const members, or bases, and it ensures initialization occurs in declaration order regardless of list order. The pointer this is not available in the initializer list, as members are initialized prior to the body where this becomes valid. For example:

cpp

class Base { public: Base(int); }; class Derived : [public Base](/page/Public) { int value; public: Derived(int v) : Base(v), value(42) {} // Initializes Base first, then value };

class Base { public: Base(int); }; class Derived : [public Base](/page/Public) { int value; public: Derived(int v) : Base(v), value(42) {} // Initializes Base first, then value };

The constructor body, if present, executes after all initializations. Since C++11, delegating constructors allow one constructor to invoke another of the same class via the initializer list, reducing code duplication by reusing initialization logic; the delegating constructor's body must be empty, and only one delegation is permitted per constructor. An example is:

cpp

class Foo { public: Foo(char, int); // Target constructor Foo(int y) : Foo('a', y) {} // Delegating constructor };

class Foo { public: Foo(char, int); // Target constructor Foo(int y) : Foo('a', y) {} // Delegating constructor };

This feature supports constructor chaining similar to other languages but adheres to C++'s initialization rules. Also introduced in C++11, in-class initializers provide default values for non-static data members directly in the class definition, which are used if no explicit initializer appears in a constructor's member initializer list. These enable uniform default construction without requiring an initializer list in every constructor:

cpp

class Example { int a = 10; // In-class initializer public: Example() {} // Uses a=10 Example(int val) : a(val) {} // Overrides with val };

class Example { int a = 10; // In-class initializer public: Example() {} // Uses a=10 Example(int val) : a(val) {} // Overrides with val };

Such initializers make classes more aggregate-like but disqualify them from being aggregates if used. To prevent unintended implicit type conversions, the explicit keyword can be applied to a constructor (since C++98), requiring direct initialization or static_cast for invocation; this is particularly useful for single-argument constructors that might otherwise act as converting constructors. For example:

cpp

class Numeric { int value; public: explicit Numeric(int v) : value(v) {} // Prevents implicit conversion from int };

class Numeric { int value; public: explicit Numeric(int v) : value(v) {} // Prevents implicit conversion from int };

Since C++20, explicit can be conditional based on a constant expression evaluating to true. Without explicit, such constructors enable implicit conversions, which can lead to surprising behavior in function arguments or assignments.

Destructors

A destructor is a special member function of a class that is automatically invoked when the lifetime of an object ends, primarily to release resources acquired during the object's construction. It has the unique syntax ~ClassName(), taking no parameters and returning no value, distinguishing it from other member functions. Destructors cannot be inherited or overloaded but can be virtual, constexpr (since C++20), or declared as noexcept (since C++11). Destructors are invoked implicitly at the end of an object's lifetime, such as when leaving the scope of an object, explicitly via a delete expression for dynamically allocated objects, during stack unwinding from exceptions, or upon program termination for objects with static storage duration. If no user-defined destructor is provided, the implicitly declares a trivial destructor as an inline public member function with an empty body, which is constexpr-eligible since ; however, this implicit destructor becomes non-trivial if the class has a virtual base or non-trivial member destructors. In polymorphic hierarchies, declaring the base class destructor as virtual ~BaseClass() ensures that the appropriate derived class destructor is called when deleting an object through a base class pointer, preventing resource leaks or ; conversely, a non-virtual destructor in a base class can lead to incomplete cleanup if derived destructors are not invoked. The C++ Core Guidelines recommend that base class destructors be either public and virtual or protected and non-virtual to support safe polymorphic deletion. The destruction process follows a specific order: first, the destructor body executes; then, non-static data members are destroyed in the reverse order of their declaration; finally, virtual base classes are destroyed in the reverse order of construction, followed by non-virtual bases. This ordered cleanup is crucial for maintaining class invariants and releasing dependent resources correctly. must not throw exceptions, as doing so during stack unwinding terminates the program; the C++ Core Guidelines mandate that destructors be declared noexcept to enforce this. Destructors play a central role in the RAII (Resource Acquisition Is Initialization) idiom, where constructors acquire resources and destructors ensure their release, guaranteeing without manual intervention. For instance, consider a class managing dynamic memory:

cpp

class ResourceManager { private: int* data; public: ResourceManager(size_t size) : data(new int[size]) {} ~ResourceManager() { delete[] data; } // Releases allocated memory };

class ResourceManager { private: int* data; public: ResourceManager(size_t size) : data(new int[size]) {} ~ResourceManager() { delete[] data; } // Releases allocated memory };

In this example, the destructor prevents memory leaks by deallocating the array upon object destruction, even if an exception occurs post-construction. The RAII approach, emphasized since C++98, relies on this automatic invocation to handle resources like files, locks, or sockets deterministically. The core mechanics of destructors have remained stable since C++98, with enhancements like the noexcept specifier in C++11 for and prospective destructors in C++20 for improved overload resolution in certain contexts; however, the fundamental definition, invocation, and order have not changed. The C++ Core Guidelines further stress that any class acquiring resources must define a to release them explicitly, aligning with RAII principles to avoid leaks.

Inheritance

Defining Derived Classes

In C++, derived classes are defined by specifying one or more base classes in the class declaration, using a base-specifier list following a colon after the class name. The general syntax is class Derived : access-specifier Base { /* members */ };, where access-specifier can be public, private, or protected, controlling how the base class members are accessed in the derived class. Public inheritance makes public and protected members of the base class public and protected in the derived class, respectively, while private inheritance treats them as private. Protected inheritance makes public members protected in the derived class. For single , a derived class extends a single base class by inheriting its members and adding new ones. Consider a base class Shape with a member for area and a member function to calculate it:

cpp

class Shape { protected: double area; public: virtual void calculateArea() = 0; // Pure virtual function };

class Shape { protected: double area; public: virtual void calculateArea() = 0; // Pure virtual function };

A derived class Circle can inherit publicly from Shape, add its own radius member, and override the calculation:

cpp

class Circle : public Shape { private: double radius; public: Circle(double r) : radius(r) { calculateArea(); } void calculateArea() override { area = 3.14159 * radius * radius; } };

class Circle : public Shape { private: double radius; public: Circle(double r) : radius(r) { calculateArea(); } void calculateArea() override { area = 3.14159 * radius * radius; } };

Here, Circle inherits Shape's protected area member and implements the pure virtual function, forming an is-a relationship where a Circle is a Shape. Multiple inheritance allows a derived class to inherit from multiple base classes, specified as a comma-separated list in the base-specifier clause, each with its own access specifier. For example:

cpp

class Drawable { public: virtual void draw() = 0; }; class Resizable { public: virtual void resize(double factor) = 0; }; class ScalableShape : public Drawable, public Resizable { public: void draw() override { /* implementation */ } void resize(double factor) override { /* implementation */ } };

class Drawable { public: virtual void draw() = 0; }; class Resizable { public: virtual void resize(double factor) = 0; }; class ScalableShape : public Drawable, public Resizable { public: void draw() override { /* implementation */ } void resize(double factor) override { /* implementation */ } };

This enables ScalableShape to combine interfaces from both bases, inheriting their pure virtual functions to implement. was formalized in the C++98 standard, building on earlier designs to support complex hierarchies while raising awareness of issues like the diamond problem, where ambiguous paths from a common base can lead to duplication or access conflicts. When defining constructors for derived classes, base class constructors are invoked explicitly in the member initializer list before the derived class body executes, ensuring proper initialization order. The syntax uses a colon after the constructor parameter list, followed by base class constructor calls:

cpp

class Base { public: Base(int x) { /* initialize */ } }; class Derived : public Base { public: Derived(int x, int y) : Base(x), /* other initializers */ { /* derived body */ } };

class Base { public: Base(int x) { /* initialize */ } }; class Derived : public Base { public: Derived(int x, int y) : Base(x), /* other initializers */ { /* derived body */ } };

Base constructors are called in the order of declaration in the base-specifier list, regardless of the order in the initializer list, promoting predictable construction.

Base-Derived Relationships

In the context of C++ inheritance, base-derived relationships govern how members of a base class are accessed and behave in derived classes, enabling polymorphic behavior while enforcing encapsulation. Access to base class members in a derived class depends on the inheritance specifier used. In public inheritance, public members of the base class remain public in the derived class, and protected members remain protected, preserving the base class's access levels for derived objects. In contrast, private inheritance collapses both public and protected base members to private access in the derived class, restricting them to the derived class itself and preventing further exposure in subclasses. Derived classes can introduce members that interact with those of the base class through hiding or overriding. A non-virtual member in the derived class with the same name as a base class member hides the base member during name lookup, preventing access to the base version without explicit qualification, such as via Base::member. Overriding occurs specifically with virtual functions, where a derived class function replaces the base class's implementation at runtime, enabling polymorphism; non-virtual functions do not support this . Virtual functions form the foundation of runtime polymorphism in C++ and are declared using the virtual keyword in the base class. A function in the derived class overrides a base virtual function if their signatures match, ignoring certain qualifiers; since C++11, the override specifier can be appended to explicitly indicate this intent, resulting in a compile-time error if no virtual function is overridden. Pure virtual functions, declared with = 0, have no implementation in the base class and render it abstract, meaning objects of the class cannot be instantiated and derived classes must provide implementations to become concrete. To handle ambiguities in multiple inheritance, such as the diamond problem where a common base is inherited through multiple paths, virtual inheritance ensures a single shared subobject of the virtual base class in the most derived object. This is specified by prefixing the base class with virtual in the inheritance list, avoiding duplication and potential conflicts in member access or construction. For instance, in a hierarchy where classes A and B both virtually inherit from V, a class C inheriting from A and B will have only one instance of V, shared across paths. Polymorphism manifests when a base class pointer or points to a derived class object, invoking the derived class's overriding at runtime via . Consider the following example:

cpp

class Base { public: [virtual void display](/page/Virtual_function)() { /* Base implementation */ } }; class Derived : public Base { public: void display() override { /* Derived implementation */ } }; int main() { Base* ptr = new Derived(); ptr->display(); // Calls Derived::display() delete ptr; }

class Base { public: [virtual void display](/page/Virtual_function)() { /* Base implementation */ } }; class Derived : public Base { public: void display() override { /* Derived implementation */ } }; int main() { Base* ptr = new Derived(); ptr->display(); // Calls Derived::display() delete ptr; }

This demonstrates late binding, where the correct function is selected based on the object's actual type. It is advisable to declare destructors virtual in base classes to ensure derived class destructors are called when deleting objects through base pointers, preventing resource leaks.

Operator Overloading

Unary and Binary Operators

Operator overloading in C++ allows classes to define custom behavior for unary and binary operators, enabling intuitive syntax for user-defined types similar to built-in types. This feature treats operators as functions, where unary operators take a single argument (implicitly this for members) and binary operators take two. Overloading is performed by declaring member functions with the operator keyword followed by the operator symbol, adhering to strict rules on and semantics to maintain expected behavior. Unary operators, such as prefix increment (++), unary negation (-), and logical not (!), are typically overloaded as member functions that modify or return a transformed version of the object. The prefix increment, for example, uses the syntax Type& operator++(), returning a reference to the incremented object to allow chaining. In contrast, the postfix increment employs Type operator++(int), where the int parameter distinguishes it from the prefix version and signals the need to return the original value before modification. A simple implementation might look like this:

cpp

class Counter { private: int value; public: Counter(int v = 0) : value(v) {} Counter& operator++() { // Prefix: increments and returns *this ++value; return *this; } Counter operator++(int) { // Postfix: returns old value, then increments Counter old = *this; ++value; return old; } };

class Counter { private: int value; public: Counter(int v = 0) : value(v) {} Counter& operator++() { // Prefix: increments and returns *this ++value; return *this; } Counter operator++(int) { // Postfix: returns old value, then increments Counter old = *this; ++value; return old; } };

For unary negation, Type operator-() computes the negative, often returning a new object, while Type operator!() inverts a logical state, typically returning bool. These overloads ensure the operator applies to the left-hand object via this. Binary operators, like addition (+) and equality (==), are overloaded with syntax such as Type operator+(const Type& other) for non-modifying operations or bool operator==(const Type& other) for comparisons. The left operand is implicitly this in member functions, making them asymmetric; for symmetric operators like ==, the member form compares this against the right operand. An example for a vector class adding components:

cpp

class Vector { private: double x, y; public: Vector(double a = 0, double b = 0) : x(a), y(b) {} Vector operator+(const Vector& other) const { // Returns new sum return Vector(x + other.x, y + other.y); } bool operator==(const Vector& other) const { // Symmetric comparison return x == other.x && y == other.y; } };

class Vector { private: double x, y; public: Vector(double a = 0, double b = 0) : x(a), y(b) {} Vector operator+(const Vector& other) const { // Returns new sum return Vector(x + other.x, y + other.y); } bool operator==(const Vector& other) const { // Symmetric comparison return x == other.x && y == other.y; } };

Modifying binary operators, such as +=, return Type& to support chaining and alter this. Member overloads are preferred for non-symmetric operators, but free (non-member) functions can be used for symmetry or when the left operand is not a class type, explicitly passing both arguments. Certain operators have restrictions on overloading forms to preserve language semantics. Logical operators && and || can be overloaded as members or free functions, but overloads lose short-circuit evaluation, making them unsuitable for most uses and often avoided. Assignment (=) and subscript ([]) must be overloaded exclusively as member functions and cannot be free. In contrast, arithmetic and logical operators like +, -, !, and ++ have no such form restrictions, allowing both member and free implementations provided at least one operand is a class or enumeration type. Operator overloading has been a core feature since the C++98 standard, providing the foundational syntax for unary and binary operators as member functions. C++11 introduced lambdas, which create anonymous functor classes implicitly overloading operator() (a unary operator) for concise callable objects, enhancing flexibility in without altering core overloading rules.

Special Operators (Brackets and Arrow)

In C++, the subscript operator [] can be overloaded as a member function to provide array-like access to class elements, enabling intuitive indexing syntax for custom containers. The non-const overload typically has the signature Type& operator[](std::size_t index), returning a reference to allow both reading and modification, while the const overload const Type& operator[](const std::size_t index) const provides read-only access for const objects. This design ensures compatibility with standard container behaviors, such as those in std::vector, where bounds checking may be implemented but is not required by the language. For example, consider a simple class:

cpp

class SimpleVector { private: int* data; [size_t](/page/Size) size; [public](/page/Public): // Constructor and other members omitted for brevity int& operator[](std::[size_t](/page/Size) index) { return data[index]; // No bounds checking in this example } const int& operator[](std::[size_t](/page/Size) index) const { return data[index]; } };

class SimpleVector { private: int* data; [size_t](/page/Size) size; [public](/page/Public): // Constructor and other members omitted for brevity int& operator[](std::[size_t](/page/Size) index) { return data[index]; // No bounds checking in this example } const int& operator[](std::[size_t](/page/Size) index) const { return data[index]; } };

Usage like SimpleVector v(10); v[0] = 42; assigns via the reference return, which is a key restriction: returning by value would prevent lvalue assignments and mimic pass-by-value semantics unsuitable for mutable access. Prior to , the operator accepts exactly one argument; since , multi-dimensional subscripting is supported with additional parameters. The function call operator () can be overloaded to make class instances behave like functions, a feature commonly used in functor (function object) classes for customizable callable types. Its signature is flexible: ReturnType operator()(ArgTypes... args) const, where parameters match the intended call signature, and it may be marked const for immutable operations. This overload turns the class into a FunctionObject, integrating seamlessly with algorithms like std::for_each or lambda alternatives. An illustrative example is a linear function approximator:

cpp

class LinearFunctor { private: double a, b; public: LinearFunctor(double slope, double intercept) : a(slope), b(intercept) {} double operator()(double x) const { return a * x + b; } };

class LinearFunctor { private: double a, b; public: LinearFunctor(double slope, double intercept) : a(slope), b(intercept) {} double operator()(double x) const { return a * x + b; } };

Invoking LinearFunctor f(2.0, 1.0); double result = f(3.0); computes 7.0, demonstrating how this operator supports arbitrary argument lists and return types, with no non-member overload possible. The arrow operator -> is overloaded to simulate pointer dereferencing in classes like smart pointers, allowing member access through a single -> syntax. It must be a member function returning a pointer type, such as PointerType* operator->() const, where PointerType is the type of the pointed-to object; the return cannot be a reference or void in practice for standard pointer emulation. For instance, a basic unique pointer wrapper might implement:

cpp

template<typename T> class BasicPtr { private: T* ptr; public: // Constructor and other members omitted T* operator->() const { return ptr; } };

template<typename T> class BasicPtr { private: T* ptr; public: // Constructor and other members omitted T* operator->() const { return ptr; } };

With BasicPtr<int> p(new int(42)); *p = 10;, the operator enables direct member access like built-in pointers. To support chaining, such as p->member->next, the return type must be a raw pointer or another class with its own operator->, which is invoked recursively until a plain pointer is reached. This cannot be overloaded as a non-member function, ensuring it remains tied to the class's internal state.

Class Templates

Defining and Instantiating Templates

Class templates in C++ enable the creation of generic classes that can be parameterized by types or values, allowing a single class definition to generate multiple specialized classes at . This mechanism, introduced in the original C++ standard and refined in subsequent revisions, promotes and without runtime overhead. The syntax for defining a class template begins with the template keyword followed by a parameter list in angle brackets, then the class keyword and the class body. For a simple type-parameterized class, it takes the form template <typename T> class MyClass { /* members */ };, where T is a placeholder for the type to be substituted during instantiation. The keyword typename (or equivalently class) introduces type parameters, and multiple parameters can be specified, such as template <typename T, typename U> class Pair { T first; U second; };. Non-type parameters, like integral constants, are also supported, for example template <typename T, int Size> class [Array](/page/Array) { T data[Size]; };, where Size must be a compile-time constant. Since , default arguments for template parameters are permitted, enabling partial specification, as in template <typename T, typename Allocator = std::allocator<T>> class [Container](/page/Container) {};. Instantiation of a class template occurs when the generates a concrete class from the template by substituting the provided arguments. Implicit instantiation happens automatically upon usage, such as declaring MyClass<int> obj;, which creates a class equivalent to class MyClass_int { int data; }; with all members instantiated as needed. Explicit instantiation can be forced with template class MyClass<double>;, which generates the class definition without creating an object, useful for controlling instantiation points or in libraries to reduce compile times. The complete template definition must be visible at the point of implicit instantiation to ensure all members are properly generated. Member functions and nested types of a class template can be defined outside the class body using the template prefix and the fully qualified name, ensuring the has access to the template parameters. For instance:

cpp

template <typename T> class Stack { std::size_t top; std::size_t [size](/page/Size)_; T* [data](/page/Data); [public](/page/Public): Stack(std::size_t capacity) : top(0), [size](/page/Size)_(capacity), [data](/page/Data)(new T[capacity]) {} ~Stack() { delete[] [data](/page/Data); } void push(const T& val); T pop(); };

template <typename T> class Stack { std::size_t top; std::size_t [size](/page/Size)_; T* [data](/page/Data); [public](/page/Public): Stack(std::size_t capacity) : top(0), [size](/page/Size)_(capacity), [data](/page/Data)(new T[capacity]) {} ~Stack() { delete[] [data](/page/Data); } void push(const T& val); T pop(); };

cpp

template <typename T> void Stack<T>::push(const T& val) { if (top < [size](/page/Size)_) { [data](/page/Data)[top++] = val; } } template <typename T> T Stack<T>::pop() { if (top > 0) { return data[--top]; } throw std::out_of_range("Stack is empty"); }

template <typename T> void Stack<T>::push(const T& val) { if (top < [size](/page/Size)_) { [data](/page/Data)[top++] = val; } } template <typename T> T Stack<T>::pop() { if (top > 0) { return data[--top]; } throw std::out_of_range("Stack is empty"); }

This approach allows the implementation to be separated from the declaration, similar to non-template classes, but requires the template keyword to reintroduce the parameters. While this covers general instantiation for arbitrary types, mechanisms exist to provide custom implementations for specific types, known as specializations.

Template Specializations

Template specializations allow developers to customize the behavior of class templates for specific types or combinations of types, providing type-specific implementations while retaining the generality of the primary template. This mechanism is essential for optimizing performance, handling special cases like pointers or integral types, and enabling more expressive . Specializations are triggered during template instantiation, where the selects the appropriate definition based on the provided arguments. Full specialization, also known as explicit specialization, completely overrides the primary template for a particular set of template arguments. The syntax uses an empty template parameter list followed by the specific arguments: template <> class MyClass<void> { /* definitions */ };. For instance, consider a primary template template <typename T> class Buffer { /* general storage */ };; a full specialization for int might implement fixed-size allocation: template <> class Buffer<int> { int data[1024]; /* ... */ };. This approach is useful when the general template cannot efficiently or correctly handle a specific type, such as void, where no storage is needed. Partial specialization refines the primary template for a of arguments, keeping some parameters generic. The syntax specifies a of the primary's parameters: template <typename T> class MyClass<T*> { /* pointer-specific logic */ };. This is particularly valuable for pointer types, where operations like null checks or dereferencing may differ from value types. For example, a primary template template <typename T> class [Container](/page/Container) { /* general containment */ }; can have a partial specialization template <typename T> class [Container](/page/Container)<T*> { T* ptr; void store(T* p) { if (p) ptr = p; } /* null-safe storage */ };, ensuring safe handling of pointers. Another common case is specializing for char* to treat it as a string: in a StringWrapper template, template <> class StringWrapper<char*> { size_t len; /* compute length with strlen */ }; provides string-specific features like length calculation without generic overhead. Partial specializations must be declared after the primary template and are only available for class templates, not function templates. The compiler selects the most specific matching specialization during instantiation, using partial ordering rules to compare the primary template against all specializations. A partial specialization is chosen over the primary if it provides more constraints on the arguments, and among partial specializations, the one with the tightest match prevails; ambiguities result in compilation errors. For example, given a primary template <class T1, class T2> class A {}; and partials template <class T> class A<T, T*> {}; and template <class T> class A<int, T> {};, instantiating A<int, int*> selects the first partial as it matches both arguments more specifically. Since , variadic templates support specializations with parameter packs, allowing flexible handling of variable arguments: template <typename... Args> class VariadicContainer { /* pack processing */ };, with partial specializations like template <typename T> class VariadicContainer<T, int> {}; for mixed types. This extends customization to arbitrary argument counts. Member specializations can be defined within a class specialization or externally for specific members. Inside the class: template <typename T> class MyClass { void func(); }; template <typename T> class MyClass<T*> { void func() { /* pointer version */ } };. Externally: template <> void MyClass<int>::func() { /* full specialization for member */ };. This allows fine-grained overrides without redefining the entire class.

Usage and Properties

Memory Layout and Bit-Fields

In C++, the memory layout of a class object is determined by the order of declaration of its non-static data members, with implementations allocating them such that later declared members occupy higher addresses within the object. For classes with base classes, the layout begins with the base subobjects followed by the derived class's data members, adhering to the same ordering principle. This sequential allocation ensures predictable relative positioning, though the exact byte offsets may vary due to implementation choices. Compilers may insert padding bytes between non-static data members or after the last member to satisfy alignment requirements, which are fundamental properties of object types ensuring efficient access on the target architecture. For instance, consider a class with a char member (typically 1 byte, alignment 1) followed by an int member (4 bytes, alignment 4); the compiler often adds 3 padding bytes after the char to align the int to a 4-byte boundary, resulting in a sizeof of 8 bytes rather than 5.

cpp

class Example { char c; // 1 byte int i; // 4 bytes, preceded by 3 [padding](/page/Padding) bytes }; // sizeof(Example) == 8

class Example { char c; // 1 byte int i; // 4 bytes, preceded by 3 [padding](/page/Padding) bytes }; // sizeof(Example) == 8

Such is implementation-defined in amount but required to meet the class's overall alignment, which is the strictest alignment of any member. Since , the alignas specifier allows programmers to specify custom alignment for classes or members, potentially reducing or controlling , while alignof queries the alignment at . Bit-fields provide a mechanism for fine-grained control over memory allocation at the bit level, declared as non-static data members of or types with a specified width in bits. The syntax is type identifier : width;, where width is a constant expression, and adjacent bit-fields of compatible types may be packed into shared storage units, such as integers, to minimize space usage. The exact packing order (e.g., least significant bit first) and whether bit-fields straddle allocation unit boundaries are implementation-defined, but unnamed bit-fields with width greater than zero can serve as to force alignment. A common application is implementing flag collections, as in the following example where three bits represent boolean options packed into a single unsigned int:

cpp

class Flags { unsigned int read : 1; unsigned int write : 1; unsigned int execute : 1; // Remaining 29 bits available in a 32-bit int }; // sizeof(Flags) typically == 4

class Flags { unsigned int read : 1; unsigned int write : 1; unsigned int execute : 1; // Remaining 29 bits available in a 32-bit int }; // sizeof(Flags) typically == 4

Accessing bit-field members behaves as if lvalue-to-rvalue conversion occurs, but operations like taking addresses are restricted since bit-fields do not have unique addresses. For interoperability with C, classes that are both trivial and standard-layout (previously known as plain old data or POD types)—guarantee a layout compatible with corresponding C structs, including equivalent and bit-field rules, without additional C++-specific overhead. A standard-layout class lacks virtual functions, virtual bases, and certain member configurations that could introduce undefined layout, ensuring members are allocated contiguously in declaration order with only alignment-required . Aggregate classes, which are a subset with no user-provided constructors, no private or protected non-static data members, no base classes, and no virtual functions, inherit these layout properties without further complications from initialization semantics.

Passing and Returning Objects

In C++, class objects can be passed to functions in several ways, each with implications for performance, modification, and . Passing by value involves creating a copy of the object, which invokes the copy constructor and can be computationally expensive for large or complex classes containing dynamic resources like dynamically allocated memory. This approach is suitable for small, inexpensive-to-copy objects but is generally avoided for efficiency reasons in performance-critical code. Passing by avoids the overhead of copying by the original object. A non-const (Type&) allows the function to modify the object directly, while a const (const Type&) provides read-only access, extending the lifetime of temporaries if bound to one. This method is preferred for class objects, as it eliminates the need for the copy constructor and reduces both memory usage and execution time, particularly when dealing with types like std::string or custom containers. Passing by pointer (Type*) similarly avoids copying and enables modification, but introduces explicit null checks and potential ambiguities, as the pointer does not inherently manage the object's lifetime—leading to risks like dangling pointers if the object is destroyed prematurely. Unlike references, pointers can be reassigned to null or other addresses within the function, making them useful for optional or polymorphic arguments but requiring careful handling to prevent . For example, consider a simple class MyClass with a string member:

cpp

class MyClass { public: std::string data; MyClass(const std::string& d) : data(d) {} }; void byValue(MyClass obj) { // Copies obj, invokes copy constructor obj.data += " modified"; } void byConstRef(const MyClass& obj) { // No copy, read-only access std::cout << obj.data << std::endl; } void byRef(MyClass& obj) { // No copy, allows modification obj.data += " modified"; } void byPointer(MyClass* obj) { // No copy, but check for null if (obj) obj->data += " modified"; }

class MyClass { public: std::string data; MyClass(const std::string& d) : data(d) {} }; void byValue(MyClass obj) { // Copies obj, invokes copy constructor obj.data += " modified"; } void byConstRef(const MyClass& obj) { // No copy, read-only access std::cout << obj.data << std::endl; } void byRef(MyClass& obj) { // No copy, allows modification obj.data += " modified"; } void byPointer(MyClass* obj) { // No copy, but check for null if (obj) obj->data += " modified"; }

Calling byValue(MyClass("original")) creates a temporary copy, while byConstRef(MyClass("original")) binds a const to the temporary, avoiding the copy and extending its lifetime until the end of the full expression. In contrast, byPointer(nullptr) requires explicit null handling to avoid dereferencing issues. Returning class objects from functions traditionally involves copy initialization, but optimizations mitigate the cost. Return value optimization (RVO) elides the copy of a temporary object returned by value by constructing it directly in the caller's storage; named return value optimization (NRVO) extends this to named local variables under specific conditions, such as when the returned object is not a function parameter. In C++11, these elisions are permitted but not guaranteed, allowing fallback to move semantics if unavailable; however, mandates elision for prvalues, ensuring direct construction without invoking copy or move constructors in many cases. C++11 introduced move semantics to enable efficient resource transfer when returning or passing lvalues that will not be used afterward. The std::move utility casts an lvalue to an rvalue reference, signaling that the object may be "moved from," typically invoking a move constructor or assignment operator to steal resources like pointers rather than copying them. This is particularly valuable for returning locals, as in MyClass create() { MyClass obj("data"); return std::move(obj); }, where the move constructor transfers ownership cheaply, leaving the source in a valid but unspecified state. Automatic moves occur for temporaries, but explicit std::move is needed for named objects to avoid unnecessary copies. Uniform initialization, introduced in C++11, uses brace-enclosed lists ({}) to consistently initialize objects, including temporaries, and interacts with passing and returning by reducing ambiguity in conversions and narrowing. For instance, MyClass temp{{"data"}} creates a temporary via the constructor matching the initializer list, which can then be passed by const reference without copy overhead or returned with potential elision. This syntax prevents implicit narrowing conversions and supports aggregate initialization for classes without user-provided constructors, facilitating efficient temporary creation in function arguments or returns.

References

Add your contribution
Related Hubs
Contribute something
User Avatar
No comments yet.