Skip to content

Structures

  • User-defined data types
  • A structure for grouping related variables
  • A structure as an abstraction (a computer lab consists of desks and computers)
  • Field
  • Structure initialization
  • Pointer to a structure
  • Field offset
  • The -> operator

Analyze what happens in the examples:

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A a { 1, 2 };
std::cout << a.f1 << std::endl;
std::cout << a.f2 << std::endl;
}
Answer:

By default, brace initialization syntax can be used for any structure. It assigns values to the fields one by one, in the order in which they are declared.

It prints 1 and 2.

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A a = A{ 1, 2 };
std::cout << a.f1 << std::endl;
std::cout << a.f2 << std::endl;
}
Answer:

The same thing happens as in the previous example.

What is the type of the expression A{ 1, 2 }?

The type of a variable does not have to be an integer type (int, uint8_t); it can also be user-defined.

In this example, A is a user-defined data type.

A{ 1, 2 } is an expression of type A. More precisely, the static (known at compile time) type of the value obtained by evaluating the expression A{ 1, 2 } is A.

How can a type be something other than int?

You can think of such an expression as a piece of memory with two fields, f1 and f2, whose values are 1 and 2, as in the image below. Think of it as temporary data simply suspended in the air; it is not stored anywhere in RAM.

01_example_value_of_type_of_struct

The fact that the expression has type A means that the result of evaluating it can be stored in a variable of type A. Now this “suspended” value is placed in variable a. It can be placed there because a has type A, which is compatible with the type of expression A{ 1, 2 } (also type A).

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
int a = A{ 1, 2 };
std::cout << a << std::endl;
}
Answer:

It will not compile because the expression A{ 1, 2 } has type A, while a has type int. A value of type A cannot be stored in a variable of type int.

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A a;
a.f1 = 5;
int b = a.f1;
a.f2 = b;
std::cout << a.f1 << std::endl;
std::cout << a.f2 << std::endl;
std::cout << b << std::endl;
}
Answer:

You can read from and write to each field individually.

It prints 5 three times.

#include <iostream>
struct A
{
int f;
};
int main()
{
A a{ 1 };
A b{ a };
a.f = 2;
std::cout << a.f << std::endl;
std::cout << b.f << std::endl;
}
Answer:

A a{ 1 } creates a variable a with field f = 1.

A b{ a } creates a new variable b and copies the values of all fields from a into it. At this point b.f becomes 1, but as a separate copy in b’s own memory.

So a.f = 2 changes only a, while b stays untouched.

It prints 2 and 1.

#include <iostream>
struct A
{
int f;
};
int main()
{
A a;
int* b = &a.f;
*b = 5;
std::cout << a.f << std::endl;
}
Answer:

You can obtain the address of a field inside a variable of a structure type.

In the line int* b = &a.f, the dot in &a.f is evaluated first, giving access to field f inside a; its address is then obtained using &.

Value 5 is stored in a.f.

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A a;
a.f1 = 5;
a.f2 = 6;
A b;
b = a;
std::cout << a.f1 << std::endl;
std::cout << a.f2 << std::endl;
std::cout << b.f1 << std::endl;
std::cout << b.f2 << std::endl;
}
Answer:

b = a copies the values of all fields of a into b.

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A a;
a.f1 = 5;
a.f2 = 6;
A b;
b.f1 = 7;
b = a;
std::cout << a.f1 << std::endl;
std::cout << a.f2 << std::endl;
std::cout << b.f1 << std::endl;
std::cout << b.f2 << std::endl;
}
Answer:

b = a does not know which fields have already been initialized. It copies all fields indiscriminately.

In the end, b has f1 = 5, f2 = 6.

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A a;
a.f1 = 5;
a.f2 = 6;
A* pa = &a;
A b = *pa;
std::cout << b.f1 << std::endl;
std::cout << b.f2 << std::endl;
}
Answer:

In the initialization of b, *pa is essentially equivalent to directly accessing a. This is the same situation as above.

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A a;
a.f1 = 5;
a.f2 = 6;
A* pa = &a;
a.f1 = 7;
A b = *pa;
std::cout << b.f1 << std::endl;
std::cout << b.f2 << std::endl;
}
Answer:

b.f1 receives 7. The & operator does not take the address of the values f1 = 5, f2 = 6; it takes the address of variable a. When dereferencing that address, you always get the current value of a.

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A a;
a.f1 = 5;
A* pa = &a;
(*pa).f2 = 6;
pa->f1 = 7;
std::cout << a.f1 << std::endl;
std::cout << a.f2 << std::endl;
}
Answer:

*pa is essentially equivalent to directly accessing a.

(*pa).f2 = 6 -> a.f2 = 6.

pa->f1 means “go to the variable at the address in pa, then access field f1.” This can also be written as (*pa).f1. In practice, it is equivalent to a.f1.

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A a{};
A* pa { &a };
*pa = { 5, 6 };
std::cout << pa->f1 << std::endl;
std::cout << pa->f2 << std::endl;
}
Answer

*fp = { 5, 6 } -> *(&a) = { 5, 6 } -> a = { 5, 6 }

This means a.f1 = 5, a.f2 = 6.

fp->f1 reads what is currently in a. Since 5 was written there earlier, it prints 5.

Similarly, fp->f2 prints 6.

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A* fp { &A };
*fp = { 5, 6 };
std::cout << fp->f1 << std::endl;
}
Answer:

This is not allowed because structure A itself stores no data. Data can be stored in a variable of type A, which must be created first.

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
int* fp { &A.f1 };
*fp = 5;
std::cout << *fp << std::endl;
}
Answer:

This is not allowed. The explanation is the same as in the previous example.

#include <iostream>
struct A
{
int* f1;
};
int main()
{
int num = 1;
A a;
a.f1 = &num;
*a.f1 = 2;
std::cout << a.f1 << std::endl;
std::cout << num << std::endl;
}
Answer:

In *a.f1, * is applied after ., that is: *a.f1 -> *(&num) -> num. After that, num is overwritten with 2.

It prints the address of num, then 2.

#include <iostream>
struct A
{
int* f[2];
};
int main()
{
int var1;
int var2;
A a { .f = { &var1, &var2 } };
*a.f[0] = 1;
*a.f[1] = 2;
int** b = a.f;
int c = **b;
std::cout << var1 << std::endl;
std::cout << var2 << std::endl;
std::cout << a.f[0] << std::endl;
std::cout << a.f[1] << std::endl;
std::cout << b << std::endl;
std::cout << c << std::endl;
}
Answer:

The line A a { .f = { &var1, &var2 } }; initializes a, putting the address of var1 into field a.f[0] and the address of var2 into a.f[1].

In the line *a.f[0] = ..., execution follows the address stored in a.f[0]: *a.f[0] -> *(a.f[0]) -> *(&var1) -> var1.

In int** b = a.f;, a.f is equivalent to &(a.f[0]). This gives the address of the first element in array f in a.

It prints:

  • 1, 2 as the values of var1 and var2;
  • the addresses of var1 and var2 as the values of a.f[0] and a.f[1];
  • c is equal to 1.
#include <iostream>
struct Nested
{
int f;
};
struct A
{
Nested nested;
int f;
};
int main()
{
A a {
.nested = { .f = 1 },
.f = 2,
};
a.f = 3;
a.nested = { 5 };
a.nested.f = 6;
std::cout << a.f << std::endl;
std::cout << a.nested.f << std::endl;
}
Answer:

Other structures can be nested inside a structure. This is called nesting, and it is used constantly in programming.

Here, in the end, a.f equals 3, and a.nested.f equals 6.

#include <iostream>
struct Nested
{
int f;
};
struct A
{
Nested nested;
int f;
};
int main()
{
A a { .nested = { 1 }, .f = 2 };
A b { a };
a.nested.f = 3;
a.f = 4;
std::cout << b.nested.f << std::endl;
std::cout << b.f << std::endl;
std::cout << a.nested.f << std::endl;
std::cout << a.f << std::endl;
}
Answer:

A b{ a } copies the values of all fields, including the whole nested structure.

At the moment of copying, a holds nested.f = 1, f = 2, so b receives the same values: b.nested.f = 1, b.f = 2. But it is a separate copy in b’s own memory: the Nested structure itself is stored inside A by value, not by address.

So a.nested.f = 3 and a.f = 4 affect only a, while b stays untouched.

It prints 1, 2, 3, 4.

struct A
{
int value;
A other;
};
Answer:

A structure cannot contain itself because it would then occupy an infinite amount of memory. It is possible to include a pointer to another such structure, because its size does not depend on the size of the structure.

#include <iostream>
struct Node
{
int value;
Node* next;
};
int main()
{
Node end{};
end.value = 1;
end.next = nullptr;
Node start{};
start.value = 2;
start.next = &end;
Node* current = &start;
std::cout << current->value << std::endl;
current = current->next;
std::cout << current->value << std::endl;
current = current->next;
std::cout << current->value << std::endl;
}
Answer:

This creates a linked list, a data structure used very often in programming.

Each node holds a pointer to another node of the same type.

The null pointer in the last node of the list (nullptr) marks the end of the list.

The code prints 2, then 1, and then crashes on the last line when it dereferences a null pointer (segmentation fault).

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A a;
std::cout << sizeof(a) << std::endl;
std::cout << sizeof(A) << std::endl;
}
Answer:

sizeof(a) gives the size of a variable in bytes. sizeof(A) gives the size in bytes of a variable of type A, if one were created. Both forms are equivalent.

The result is 8 because each structure has 2 ints, each of which occupies 4 bytes.

#include <iostream>
struct A
{
int f1[4];
int* f2;
};
int main()
{
std::cout << sizeof(A) << std::endl;
}
Answer:
  • int f1[4] is 4 ints, 4 bytes each — 16 bytes;
  • int* f2 is 8 bytes on a 64-bit processor.

The total is 24 bytes.

23. Field address from a structure address

Section titled “23. Field address from a structure address”
#include <iostream>
struct A
{
int f;
};
int main()
{
A a { 1 };
A* pa { &a };
int* pf { &pa->f };
*pf = 2;
std::cout << a.f << std::endl;
}
Answer:

This demonstrates that you can take the address of a field after applying ->. & is applied after ->, just as it is after ..

&pa->f -> &(pa->f) -> &((*pa).f) -> &(a.f)

It prints 2.

24. Field address relative to a structure address

Section titled “24. Field address relative to a structure address”
#include <iostream>
struct A
{
int f;
};
int main()
{
A a { };
A* pa { &a };
int* pf { &pa->f };
ptrdiff_t diff { reinterpret_cast<uint8_t*>(pf) - reinterpret_cast<uint8_t*>(pa) };
std::cout << diff << std::endl;
}
Answer:

It prints 0.

The first field of a structure and the structure itself are always located at the same memory address.

#include <iostream>
struct A
{
uint8_t f1;
int f2;
uint8_t f3;
};
int main()
{
std::cout << sizeof(A) << std::endl;
}
Answer:

This is where alignment comes into play. Alignment leaves empty spaces between fields. This is done because it allows the processor to read data from memory faster.

First, the largest field size is determined; it is usually no more than 16 bytes. In this example, it is int — 4 bytes.

Now divide memory into 4-byte slots. If the next field does not fit entirely into the remaining space in a 4-byte slot, it goes into the next one.

  • uint8_t f1 goes into the first byte of the first slot;
  • int f2 does not fit in the first slot after f1, so it goes into the next one. The remaining 3 bytes of the first slot are unused (padding bytes);
  • uint8_t f3 goes into the third slot;
  • The remaining 3 bytes of the third slot are unused.

In total, this gives 3 slots of 4 bytes each.

If a structure contains a field of another structure type, its slot is no smaller than the slots of that nested structure.

Alignment can be disabled using #pragma pack.

#include <iostream>
struct A
{
};
int main()
{
std::cout << sizeof(A) << std::endl;
}
Answer:

It prints 1.

According to the C++ standard, the size of an object cannot be less than 1, so that 2 objects of this type can be distinguished from each other. The reason is that 2 objects cannot have the same memory address.

Objects are covered in the next lab.

#include <iostream>
struct A
{
int a;
int b;
};
int main()
{
std::cout << offsetof(A, a) << std::endl;
std::cout << offsetof(A, b) << std::endl;
}
Answer:

offsetof is evaluated at compile time and gives the byte offset of a specified field from the beginning of a structure.

It prints 0 for a and 4 for b.

#include <iostream>
struct A
{
int arr[2];
};
int main()
{
A a { { 1, 2 } };
A b { { 3, 4 } };
b = a;
std::cout << b.arr[0] << std::endl;
std::cout << b.arr[1] << std::endl;
}
Answer:

It prints 1, 2.

29. Copying a structure with an array field

Section titled “29. Copying a structure with an array field”
#include <iostream>
struct A
{
int arr[2];
};
int main()
{
A a { { 1, 2 } };
A b { a };
a.arr[0] = 3;
std::cout << b.arr[0] << std::endl;
std::cout << b.arr[1] << std::endl;
std::cout << a.arr[0] << std::endl;
std::cout << a.arr[1] << std::endl;
}
Answer:

A b{ a } copies the values of all fields, including the whole array, element by element.

At the moment of copying, a holds arr[0] = 1, arr[1] = 2, so b receives the same values: b.arr[0] = 1, b.arr[1] = 2. But it is a separate copy in b’s own memory: the array arr itself is stored inside A by value, not by address.

So a.arr[0] = 3 affects only a, while b stays untouched.

It prints 1, 2, 3, 2.

30. Copying a structure with a pointer field

Section titled “30. Copying a structure with a pointer field”
#include <iostream>
struct A
{
int* pa;
int a;
};
int main()
{
A s1 { .pa = nullptr, .a = 1 };
s1.pa = &s1.a;
A s2 { s1 };
*s2.pa = 2;
std::cout << s1.a << std::endl;
std::cout << s2.a << std::endl;
}
Answer:

A s2{ s1 } copies the values of all fields, but for the pointer it copies the address itself, not the data at that address. This is called a shallow copy.

At the moment of copying, s1 holds a = 1, pa = &s1.a, so s2 receives the same values: s2.a = 1, s2.pa = &s1.a. Note: s2.pa points to s1.a, not to s2.a.

Therefore *s2.pa = 2 follows the address &s1.a and overwrites s1.a, while s2.a stays untouched.

It prints 2, 1.

#include <iostream>
struct A
{
int f1;
int f2;
};
int main()
{
A arr[3]{};
arr[0].f2 = 1;
arr[1] = A{ 2, 3 };
A copy { arr[2] };
copy.f1 = 4;
copy.f2 = 5;
std::cout << arr[0].f1 << std::endl;
std::cout << arr[0].f2 << std::endl;
std::cout << arr[1].f1 << std::endl;
std::cout << arr[1].f2 << std::endl;
std::cout << arr[2].f1 << std::endl;
std::cout << arr[2].f2 << std::endl;
}
Answer:

The line A arr[3]{};:

  • Defines an array of three variables of type A. In total, memory is allocated for 6 ints.
  • The braces mean “initialize with zeros”. Each array element is filled with the default A, namely A{}, which means all 6 ints become zero.

In arr[0].f2 = 1;, arr[0] accesses the memory of the first variable in the array. This variable has type A (not int! It is very important to understand this). Its second field (.f2) is assigned 1 (= 1).

arr[1] = A{ 2, 3 }; overwrites the entire second element with the result of expression A{ 2, 3 }. This overwrites both fields.

A copy { arr[2] }; reads the third element of the array, copying what is there into temporary variable copy. copy holds a copy of the value from arr[2] of type A (that is, it has a copy of all fields). Therefore, subsequent changes to copy do not affect arr[2].

0
1
2
3
0
0

Explain in words what happens in the memory_example_2 example. You can copy the code file and write comments directly in the code explaining what happens. Use the Excel table from the example to visualize the memory layout.

You do not necessarily have to comment on every step; you can instead explain what is printed at each stage, and why (what a given pointer points to at a given moment, what is currently stored in memory, and so on). You can also use a debugger for a better understanding.