Sections
Can We Not Use Arrays?§
We have seen that a non-primitive type can solve a syntax and memory-organization problem.
For example, even without array syntax, we can conceptually represent the same collection of values:
Code 1§
int data1 = 1;
int data2 = 3;
int data3 = 5;
int data4 = 7;
int data5 = 9;
for (int i = 0; i < 5; i++)
{
std::print(*(&data1 + i));
}
Code 2§
int data[] = { 1, 3, 5, 7, 9 };
for (int i = 0; i < 5; i++)
{
std::print(data[i]);
}
Conceptually, both pieces of code represent five int values arranged consecutively.
So, at the fundamental level, we are already capable of expressing the underlying data without introducing array syntax. We use arrays because:
- They are much more efficient to write, read, and understand.
- They explicitly communicate that the data is structured as a sequence.
- They give C++ a formal way to treat these values as one collection.
Architecture and compiler optimization
The example above is intended to demonstrate the language-level idea, not to guarantee a particular physical memory layout on every implementation.
In a real program, the compiler and architecture can optimize the representation of variables. Alignment, registers, optimization, and other implementation details can cause separately declared variables to be represented differently from the simple consecutive-memory model.
Therefore, manually relying on separately declared variables being consecutive is not something portable C++ code should do.
An array, on the other hand, explicitly tells C++ that these values form one array object, whose elements have a defined contiguous relationship.
This is an important distinction: the fact that we can conceptually express something using primitive types does not make the higher-level abstraction unnecessary. The higher-level type communicates the intended relationship to the language and allows the compiler to handle it appropriately.
Structure§
A structure solves a similar class of organization problem, but for related data that may have different types and named meanings.
An array gives us:
multiple values
↓
same type
↓
ordered sequence
↓
accessed by index
A structure gives us:
multiple values
↓
possibly different types
↓
grouped into one object
↓
accessed by member name
For example, consider these separate variables:
Code 1§
int nowYear = 2026;
int nowMonth = 8; // August
int nowDay = 18;
int spiderManCreationYear = 1962;
int spiderManCreationMonth = 8; // August
int spiderManCreationDay = 10;
// Maybe I need to update the date of 'now' tomorrow, or some time later
nowDay = 19;
// Or later
nowMonth = 9;
nowDay = 20;
We can instead group the related information into a structure:
Code 2§
struct Date
{
int year;
int month;
int day;
};
Date now = { .year = 2026, .month = 8, .day = 18 };
Date spiderManCreation = { 1962, 8, 10 };
The first initialization explicitly names each member:
Date now = { .year = 2026, .month = 8, .day = 18 };
The member names can be omitted:
Date spiderManCreation = { 1962, 8, 10 };
When the member names are omitted, the values must follow the order in which the members were defined:
struct Date
{
int year;
int month;
int day;
};
Therefore:
1962 → year
8 → month
10 → day
We can then access individual members using .:
now.day = 19;
now.month = 9;
now.day = 20;
The . notation can be mentally read as:
"the member of"
So:
now.month
means:
"the
monthmember ofnow."
Structure Definitions Do Not Create Objects§
The definition:
struct Date
{
int year;
int month;
int day;
};
does not itself allocate a Date object.
It tells C++ what the Date type looks like.
Memory is allocated when we actually create an object of that type:
Date now = { .year = 2026, .month = 8, .day = 18 };
now is an instance of Date.
We can create multiple instances:
Date now = { .year = 2026, .month = 8, .day = 18 };
Date spiderManCreation = { 1962, 8, 10 };
Each instance contains its own year, month, and day.
The structure definition itself does not allocate storage for any particular instance.
Methods§
A method is a function that belongs to a structure or class.
For example:
struct Date
{
int year;
int month;
int day;
int getYearIntervalFrom(Date oldDate)
{
return year - oldDate.year;
}
};
Now:
Date now = { .year = 2026, .month = 8, .day = 18 };
Date spiderManCreation = { 1962, 8, 10 };
int yearInterval = now.getYearIntervalFrom(spiderManCreation);
now.getYearIntervalFrom(...) means:
Call the
getYearIntervalFrommethod belonging tonow.
The method can access the members of the current Date object directly:
return year - oldDate.year;
Here:
year
refers to the year member of the current object, while:
oldDate.year
refers to the year member of the oldDate object.
So a method can also access members of another instance of the same structure when it has access to that instance.
Special Methods§
Some methods have special purposes and special syntax.
Constructor§
A constructor is a special member function that is automatically invoked when an object is constructed.
It has the same name as the structure or class and has no return type.
struct Date
{
int year = 2026;
int month = 1;
int day = 1;
Date()
{
std::println("Date constructed");
}
};
Date someDate{};
When someDate is constructed, the constructor runs automatically:
Date constructed
A constructor is part of the object's construction and initialization process. It is not a normal function that we have to explicitly invoke.
Constructors can also receive arguments:
struct Date
{
int year;
int month;
int day;
Date(int y, int m, int d)
: year(y), month(m), day(d)
{
}
};
Date someDate{ 2026, 8, 18 };
Here, constructing someDate invokes:
Date(int y, int m, int d)
with:
y = 2026
m = 8
d = 18
Destructor§
A destructor is another special member function.
It has the same name as the structure or class, but with ~ in front:
struct Date
{
int year = 2026;
int month = 1;
int day = 1;
Date()
{
std::println("hihi");
}
~Date()
{
std::println("byebye");
}
};
The destructor is automatically invoked when the object's lifetime ends.
For example:
{
Date randomDate{};
std::println("Other things");
}
The execution is:
Date is constructed
↓
"hihi"
↓
"Other things"
↓
the block ends
↓
randomDate's lifetime ends
↓
destructor runs
↓
"byebye"
So the destructor is useful for performing cleanup when an object's lifetime ends.
Note
An object's lifetime ending is related to scope, Stacks and Scopes
Ways of Initialization§
Default Member Initializers§
A structure's members can be given default member initializers:
struct Date
{
int year = 2026;
int month;
int day;
};
Now consider:
Date someDate{ 1, 2 };
The supplied values initialize the first members:
year = 1
month = 2
The remaining day member has no supplied value and no default member initializer, so it is value-initialized to:
day = 0
Therefore:
year = 1
month = 2
day = 0
The default member initializer for year is not used because we explicitly supplied a value for year.
If we instead write:
Date someDate{};
then the default member initializer is used:
year = 2026
month = 0
day = 0
So the general idea is:
A supplied initializer takes precedence over a member's default member initializer. If no value is supplied, the default member initializer is used if one exists.
Initialization List§
Constructors can use an initialization list to initialize members.
struct Date
{
int year = 2026;
int month;
int day;
Date(int y, int m, int d)
: year(y), month(m), day(d)
{
if (month > 12)
{
month = 1;
}
}
};
The initialization list is:
: year(y), month(m), day(d)
It means:
year ← initialized using y
month ← initialized using m
day ← initialized using d
For:
Date someDate(2026, 4000, 1);
the construction process can be thought of as:
constructor is invoked
↓
year is initialized with 2026
month is initialized with 4000
day is initialized with 1
↓
constructor body begins
↓
month > 12
↓
month = 1
The important distinction is that member initialization happens before the constructor body executes.
The initialization list is therefore not simply another way of assigning values inside the constructor body. It is specifically part of the object's initialization.
this§
Notice that we previously avoided giving the constructor parameters the same names as the members:
struct Date
{
int year;
int month;
int day;
Date(int y, int m, int d)
: year(y), month(m), day(d)
{
}
};
What if we want to use the same names?
struct Date
{
int year = 2026;
int month;
int day;
Date(int year, int month, int day)
: year(year), month(month), day(day)
{
if (month > 12)
{
this->month = 1;
}
}
};
Here, the constructor parameters have the same names as the members.
This is an example of shadowing.
Inside the constructor:
month
refers to the nearest month variable, which is the constructor parameter.
To explicitly refer to the member of the current object, we can use:
this->month
this refers to the current object.
Therefore:
this->month
can be mentally read as:
"the
monthmember of the current object."
In the initialization list:
: year(year), month(month), day(day)
the left side identifies the member being initialized, while the right side refers to the constructor parameter.
So:
year(year)
↑ ↑
member parameter
is unambiguous in the context of an initialization list.
Note
Technically, this is a pointer to the current object. Pointers and Arrays
Heap§
Discussion
No comments yet — yours could open the discussion.