03 Classes
Enumeration
Basic enum types in C++ are problematic. Take a look at the following example.
enum class Color { red, green, blue };
enum class TrafficLight { green, yellow, red };
Depending on the compiler, this code may not compile or crash during runtime.
This is because the enum values red and green are defined twice (TrafficLight shadows Color).
This can be solved by using the enum class syntax.
enum class Color { red, green, blue };
enum class TrafficLight { green, yellow, red };
Enum Class Output Operator
Enum classes do not have a default output operator (unlike integers). The output operator must be custom defined if required.
This is done by overloading the << operator, outside the enum class definition!
enum class Color { red, green, blue };
// keyword friend cannot be used!
std::ostream& operator<<(std::ostream& os, const Color& c) {
switch (c) {
case Color::red: return os << "red";
case Color::green: return os << "green";
case Color::blue: return os << "blue";
}
return os;
}
Classes
Classes are a user-defined types. They are used to group data and functions that operate on that data.
C++ has two ways to define classes, struct and class.
The only difference between the two is the default access level of members.
In struct members are public by default, in class members are private by default.
Class Definition
A class definition consists of the following parts:
class Person {
// private by default
// data members
std::string m_name;
int m_age;
void incAge(); // private member function
public: // public section
// member functions
Person(const char name[], int age); // constructor
std::string getName() const; // getter
void setAge(int age); // setter
};
Sample Classes
Synthesized Contructors
Two constructors are synthesized by the compiler if they are not defined by the programmer.
The move constructor is not synthesized if the copy constructor is defined.
class Point {
// Copy constructor
Point(const Point& point);
// Move constructor
Point(Point&& point) == default;
//Point(Point&& point) == delete; // delete move constructor, if necessary
};
Point p1(1, 2);
Point p2(p1); // copy constructor
Point p3 = p1; // copy constructor (only if not initialized)
### Static Attributes Static attributes are shared between all instances of a class.
class Point {
static int s_nInstances;
static const int s_version = 1; // initialization only for const
Point() {
++s_nInstances;
}
int getNumberOfInstances() {
return s_nInstances;
}
};
int Point::s_nInstances = 0; // initialize
std::cout << Point::getNumberOfInstances() << std::endl; // retrieve
Memory Footprint
Sometimes object size is larger than expected. The reason: Padding and alignment.
Let’s see an example.
#include <cstdlib>
struct S1 {
uint64_t a; // 8 byte
uint32_t b; // 4 byte
uint16_t c; // 2 byte
uint64_t d; // 8 byte
};
struct S2 {
uint64_t a; // 8 byte
uint32_t b; // 4 byte
uint64_t c; // 8 byte
uint16_t d; // 2 byte
};
int main() {
std::cout << sizeof(S1) << std::endl; // 24
std::cout << sizeof(S2) << std::endl; // 32
}
These two structs are identical in terms of the size of their members. However, the size of the structs is different.
Why this is the case can be explained by looking at the memory layout of the structs.
S1:
| a | a | a | a | a | a | a | a |
| b | b | b | b | c | c | p | p |
| d | d | d | d | d | d | d | d |
S2:
| a | a | a | a | a | a | a | a |
| b | b | b | b | p | p | p | p |
| c | c | c | c | c | c | c | c |
| d | d | p | p | p | p | p | p |
Structure Padding
The location of objects in memory is aligned to the size of the CPU registers. This is done to improve performance, because the CPU can only access memory in chunks of the size of the registers and loading is time-intensive.
These registers are usually 32 or 64 bit wide. Therefore, objects are aligned to 4 or 8 byte boundaries.
struct S1 {
uint32_t a; // 4 byte
uint8_t b, c, d; // 3 byte + 1 byte padding
uint16_t e; // 2 byte + 2 byte padding
// total: 12 byte
// (total size has to be a multiple of the largest member)
}
| a | a | a | a | b | c | d | p | e | e | p | p |
p = padding
Structure Packing
This behavior can be changed by using the #pragma pack directive.
The pack directive allows the below struct to fit into the expected 22 bytes. (Would also work with 2).
#pragma pack(1) // set packing to 1 byte
struct S1 {
uint64_t a; // 8 byte
uint32_t b; // 4 byte
uint16_t c; // 2 byte
uint64_t d; // 8 byte
};
#pragma pack() // reset
Resource Allocation is Initialization (RAII)
RAII is a programming pattern used to ensure that resources are properly released. Part of RAII is to ensure that resource acquisition occures during initialization (in the constructor), and that, in case of any error during initialization, the resource is properly released.
When an object is not properly constructed, the destructor is not called, which could lead to memory leaks. To prevent this, the constructor should only succeed if all allocation was successful.