
Полная версия
Ссылки и указатели в C++: от основ к безопасности и современному коду
Подсказка: используйте оператор взятия адреса & для получения адресов элементов массива. При выводе через std::cout адреса будут отображены в шестнадцатеричном виде. Обратите внимание, что разность между адресами arr[1] и arr[0] должна быть равна sizeof(int), что подтверждает последовательное размещение элементов в памяти.
Задача 5: Размер структуры с padding
Вы должны вычислить размер в байтах структуры, содержащей один элемент типа char, за которым следует элемент типа int. Учтите, что компилятор выравнивает поля структуры в памяти так, чтобы каждый элемент начинался по адресу, кратному его собственному размеру. Это может привести к вставке дополнительных неиспользуемых байтов (padding) между полями или в конце структуры, чтобы удовлетворить требованиям выравнивания. Не предполагайте, что размер структуры равен сумме размеров её полей – вместо этого рассмотрите, как именно происходит выравнивание на типичной архитектуре, где sizeof(int) равен 4.
Подсказка: после поля char c (размер 1 байт) компилятор вставляет 3 байта выравнивания, чтобы поле int i начиналось с адреса, кратного 4. Таким образом, первые 4 байта содержат c и padding, следующие 4 байта – значение i. Общий размер структуры выравнивается также до кратного размеру её самого выравниваемого поля, что в итоге даёт 8 байт.
Задача 6: Выравнивание структуры
Вы должны определить выравнивание структуры, содержащей сначала int, а затем double. Выравнивание структуры определяется требованием её самого строго выровненного члена – то есть элемента, которому нужно начинаться по адресу, кратному наибольшему значению. Поскольку double обычно требует 8-байтового выравнивания, а int – 4-байтового, выравнивание всей структуры будет равно 8. Это значение гарантирует, что при размещении массива таких структур каждый экземпляр начинается по адресу, подходящему для хранения double.
Подсказка: функция alignof возвращает степень выравнивания типа в байтах, которая совпадает с выравниванием его самого требовательного поля. В большинстве реализаций sizeof(double) равен 8, и его выравнивание также 8, поэтому alignof(S) возвращает 8, даже если int занимает меньше места и имеет меньшее требование выравнивания.
Задача 7: Адрес глобальной переменной
Вы должны написать программу, в которой объявляется глобальная переменная и локальная переменная внутри функции main. Затем необходимо вывести на экран адреса обеих переменных с помощью оператора взятия адреса (&). Программа должна наглядно демонстрировать размещение глобальных и локальных переменных в памяти: глобальная переменная располагается в сегменте данных, а локальная – в стеке, поэтому их адреса будут различаться, причём адрес глобальной переменной обычно меньше, чем у локальной.
Подсказка: объявите одну переменную вне всех функций (глобально), а другую – внутри main. Используйте std::cout и оператор & для вывода их адресов. Не забудьте подключить заголовок .
Задача 8: Размер массива vs указателя
Вы должны понять, почему применение оператора sizeof к имени массива даёт общий размер всех его элементов в байтах, в то время как применение того же оператора к указателю, даже если он указывает на начало этого массива, возвращает лишь размер самого адреса в памяти. Это связано с тем, что имя массива в большинстве контекстов неявно преобразуется в указатель на первый элемент, но при использовании sizeof такое преобразование не происходит – компилятор видит массив как фиксированный блок памяти известного на этапе компиляции размера. Указатель же всегда остаётся просто переменной, хранящей адрес, и его размер определяется разрядностью системы.
Подсказка: вспомните, как язык C++ трактует имя массива в контексте sizeof, и чем принципиально отличается тип int[5] от типа int*. Обратите внимание на то, что sizeof вычисляется на этапе компиляции и зависит от статического типа операнда, а не от его значения во время выполнения.
Задача 9: Выравнивание с alignas
Вы должны объявить переменную так, чтобы её выравнивание в памяти составляло ровно 16 байт, не полагаясь на естественное выравнивание её типа. Для этого используйте спецификатор выравнивания, который гарантирует, что адрес переменной будет кратен указанной степени двойки. После объявления выведите запрошенное выравнивание с помощью оператора, предназначенного для запроса выравнивания типа или объекта.
Подсказка: встроенная конструкция языка позволяет задать выравнивание при объявлении переменной, а другая – узнать, какое выравнивание применяется к объекту или типу. Убедитесь, что вы используете именно ту, что возвращает требуемое значение, а не размер или что-то иное.
Задача 10: Адрес в функции
Вы должны написать программу, в которой определяется функция без параметров. Внутри этой функции объявляется локальная переменная целочисленного типа и инициализируется значением 20. Сразу после объявления переменной программа должна вывести на экран её адрес в памяти с поясняющим текстом. Основная функция должна лишь вызывать эту вспомогательную функцию и завершаться.
Подсказка: локальные переменные размещаются в стековой памяти, и их адреса доступны только в течение выполнения функции, в которой они объявлены. Используйте оператор взятия адреса & для получения адреса переменной и выводите его с помощью std::cout.
Задача 11: Размер класса с виртуальными функциями
Вы должны написать программу, которая определяет и выводит размер объекта класса, содержащего хотя бы одну виртуальную функцию. Объявите класс с приватной или публичной виртуальной функцией без параметров и тела, создайте в функции main экземпляр такого класса (или используйте оператор sizeof без создания объекта) и выведите его размер с помощью std::cout. Убедитесь, что вывод соответствует размеру указателя на виртуальную таблицу на вашей платформе.
Подсказка: виртуальные функции заставляют компилятор неявно добавлять в каждый объект скрытый указатель на таблицу виртуальных методов (vptr). На 64-битных системах этот указатель занимает 8 байт, поэтому даже пустой класс с виртуальной функцией будет иметь размер 8. Не пытайтесь добавлять поля – размер определяется исключительно наличием механизма динамического полиморфизма.
Задача 12: Выравнивание массива
Вы должны написать программу, которая определяет выравнивание массива из десяти элементов типа double. Для этого объявите массив указанного типа и размера, затем используйте оператор alignof, чтобы получить требуемое выравнивание в байтах и выведите результат на стандартный поток вывода. Убедитесь, что ваш код соответствует стандарту C++ и не содержит лишних элементов или вычислений.
Подсказка: выравнивание массива определяется выравниванием его элементов, а не его размером. Тип double на большинстве платформ имеет выравнивание в 8 байт, и массив наследует это выравнивание. Используйте оператор alignof непосредственно к имени массива, как к типу, чтобы получить корректное значение без дополнительных манипуляций.
Задача 13: Адрес на куче
Вы должны написать программу, которая динамически выделяет память для одного целого числа, инициализирует его значением 30 и выводит на экран адрес этого участка памяти. Используйте оператор new для размещения переменной в куче (heap), а после вывода адреса корректно освободите выделенную память с помощью delete, чтобы избежать утечки ресурсов.
Подсказка: объявите указатель на int, выделите память с помощью new и сразу инициализируйте её значением 30 в скобках. Выведите значение указателя (адрес), а не то, на что он указывает. Не забудьте в конце освободить память – это не только хорошая практика, но и требование корректной работы с динамической памятью в C++.
Задача 14: Размер union
Вы должны написать программу, которая определяет и выводит размер в байтах объединения (union), содержащего как минимум два поля разного типа – например, один символ и одно целое число. Объединение устроено так, что все его члены разделяют одну и ту же область памяти, и его размер определяется наибольшим из размеров его членов с учётом выравнивания. Ваш код должен использовать оператор sizeof и вывести результат в формате "sizeof(U): X", где X – вычисленный размер.
Подсказка: объявите union с полями char и int, затем примените sizeof к типу этого объединения. Учтите, что на большинстве платформ размер int составляет 4 байта, а char – 1 байт, но из-за требований выравнивания объединение примет размер самого крупного элемента. Выведите результат с помощью std::cout, строго соблюдая указанный формат вывода.
Задача 15: Выравнивание bit-field
Вы должны определить, чему равен результат оператора alignof для структуры B, содержащей битовое поле int x:4 и поле char y. Вспомните, как выравнивание работает для структур с битовыми полями: выравнивание структуры определяется выравниванием её самого строгого (наиболее выровненного) члена. Учтите, что даже если битовое поле занимает всего несколько бит, его базовый тип (int) всё ещё влияет на выравнивание структуры. Посколь耙йте, как компилятор размещает такие поля в памяти, и соотнесите это с требуемым выравниванием для int.
Подсказка: несмотря на то, что char y требует выравнивания в 1 байт, наличие битового поля с базовым типом int заставляет компилятор выровнять всю структуру по границе int, которая обычно составляет 4 байта. Обратите внимание, что alignof возвращает выравнивание типа, а не его размер, и что выравнивание структуры не может быть меньше выравнивания любого из её членов, включая базовые типы битовых полей.
Задача 16: Сравнение адресов
Вы должны написать программу, в которой определяется разность адресов двух полей структуры, содержащей два целочисленных члена. Объявите структуру с именем S, содержащую два поля типа int – a и b. Создайте экземпляр этой структуры в функции main, а затем выведите на экран разность адресов полей b и a, используя оператор взятия адреса &. Обратите внимание: разность указателей одного типа вычисляется в количестве элементов между ними, а не в байтах.
Подсказка: хотя между полями a и b в памяти действительно находится 4 байта (размер int на большинстве платформ), арифметика указателей автоматически делит реальное смещение в байтах на размер типа, к которому относятся указатели. Поэтому выражение &s.b – &s.a вернёт 1, а не 4. Убедитесь, что ваш код выводит именно этот результат, не пытаясь вручную умножать или делить смещение – всё делается средствами языка.
Задача 17: Размер с packed
Вы должны написать программу, которая определяет размер структуры, содержащей один символ и одно целое число, при условии, что компилятор не вставляет дополнительные байты выравнивания между полями. Для этого используйте директиву управления выравниванием, чтобы упаковать поля структуры в памяти без промежутков. Выведите размер этой структуры в байтах с помощью оператора sizeof.
Подсказка: в C++ выравнивание полей структуры по умолчанию добавляет «пустые» байты между полями разного размера, чтобы ускорить доступ к данным. Чтобы отключить это поведение, примените #pragma pack(1) перед объявлением структуры и #pragma pack() после неё. Вспомните, что char занимает 1 байт, а int – обычно 4 байта на большинстве платформ.
Задача 18: Выравнивание over-aligned
Вы должны написать программу, в которой объявляется структура с выравниванием, превышающим естественное выравнивание её полей. Используйте спецификатор выравнивания alignas, чтобы гарантировать, что размер выравнивания структуры будет равен заданному значению – в данном случае 32 байтам. В функции main выведите значение, возвращаемое оператором alignof для этой структуры, чтобы убедиться, что компилятор действительно применяет указанное выравнивание.
Подсказка: стандартный тип int обычно имеет выравнивание 4 байта, но это не мешает вам явно запросить более строгое выравнивание для содержащей его структуры. Оператор alignof возвращает требуемое выравнивание типа в байтах, и его результат можно напрямую вывести с помощью std::cout. Убедитесь, что вы подключили заголовок
Задача 19: Адрес строки
Вы должны написать программу, которая выводит адрес строкового литерала в памяти. Объявите указатель на константную строку и инициализируйте его строкой "hello". Затем выведите значение этого указателя, явно приведя его к типу void*, чтобы избежать интерпретации как строки. Не забудьте корректно завершить вывод с помощью std::endl.
Подсказка: строковые литералы в C++ размещаются в секции памяти только для чтения, поэтому их адрес нельзя изменить. Чтобы std::cout не пытался напечатать содержимое строки, а показал именно адрес, требуется приведение к void*. Убедитесь, что синтаксис вывода корректен – в частности, проверьте количество двоеточий в std::endl.
Задача 20: Комбинированная: sizeof, alignof, &
Вы должны написать программу, которая выводит три значения для структуры M, содержащей поля short s и long l: её размер в байтах, выравнивание в памяти и разницу в байтах между адресами этих двух полей. Используйте операторы sizeof и alignof, а также приведение адресов полей к char* для вычисления смещения между ними. Убедитесь, что вывод соответствует поведению компилятора на платформе с 8-байтовым выравниванием для типа long.
Подсказка: компилятор может вставлять промежуточные байты между полями структуры, чтобы удовлетворить требованиям выравнивания. Адрес каждого поля можно получить с помощью оператора взятия адреса &, а разницу между ними – приведя оба адреса к char* и вычтя один из другого. Помните, что порядок полей в структуре совпадает с их объявлением, и выравнивание всей структуры определяется самым строгим выравниванием среди её полей.
Резюме
В этой главе мы заложили фундамент понимания памяти в C++: разобрали сегменты (стек, куча, статика, код и константная память), их особенности, анти-паттерны и лучшие практики. Узнали о виртуальной памяти для контекста, выравнивании (alignof), размерах (sizeof vs реальный с padding), жизненном цикле объектов и динамическом выделении с RAII. Практика с &, sizeof и alignof помогла закрепить теорию. Теперь вы готовы к глубокому погружению в указатели и ссылки они строятся на этих основах. Помните: правильное управление памятью ключ к надёжному коду!
Ответы на задачи
Ответы Глава 1. Память: стек, куча, статика
Ответ на задачу 1 (Адрес переменной на стеке)
#include
int main() {
int x = 10;
std::cout << "Адрес x: " << &x << std::endl;
return 0;
}
(Вывод: адрес в hex-формате, например, 0x7ffc12345678)
Ответ на задачу 2 (Размер простого типа)
#include
int main() {
std::cout << "Размер int: " << sizeof(int) << " байт" << std::endl;
return 0;
}
(Вывод: 4 байт)
Ответ на задачу 3 (Выравнивание базового типа)
#include
int main() {
std::cout << "Выравнивание double: " << alignof(double) << " байт" << std::endl;
return 0;
}
(Вывод: 8 байт)
Ответ на задачу 4 (Адрес элемента массива)
#include
int main() {
int arr[3] = {1, 2, 3};
std::cout << "Адрес arr[0]: " << &arr[0] << std::endl;
std::cout << "Адрес arr[1]: " << &arr[1] << std::endl;
return 0;
}
(Вывод: адреса, разница 4 байта)
Ответ на задачу 5 (Размер структуры с padding)
#include
struct S { char c; int i; };
int main() {
std::cout << "sizeof(S): " << sizeof(S) << std::endl;
return 0;
}
(Вывод: 8 байт)
Ответ на задачу 6 (Выравнивание структуры)
#include
struct S { int i; double d; };
int main() {
std::cout << "alignof(S): " << alignof(S) << std::endl;
return 0;
}
(Вывод: 8 байт)
Ответ на задачу 7 (Адрес глобальной переменной)
#include
int g = 5;
int main() {
int l = 10;
std::cout << "Адрес g: " << &g << std::endl;
std::cout << "Адрес l: " << &l << std::endl;
return 0;
}
(Вывод: разные адреса, g ниже)
Ответ на задачу 8 (Размер массива vs указателя)
#include
int main() {
int arr[5];
int* p = arr;
std::cout << "sizeof(arr): " << sizeof(arr) << std::endl;
std::cout << "sizeof(p): " << sizeof(p) << std::endl;
return 0;
}
(Вывод: 20 и 8)
Ответ на задачу 9 (Выравнивание с alignas)
#include
int main() {
alignas(16) int x;
std::cout << "alignof(x): " << alignof(x) << std::endl;
return 0;
}
(Вывод: 16 байт)
Ответ на задачу 10 (Адрес в функции)
#include
void func() {
int y = 20;
std::cout << "Адрес в func: " << &y << std::endl;
}
int main() {
func();
return 0;
}
(Вывод: адрес на стеке)
Ответ на задачу 11 (Размер класса с виртуальными функциями)
#include
class C { virtual void f() {} };
int main() {
std::cout << "sizeof(C): " << sizeof(C) << std::endl;
return 0;
}
(Вывод: 8 байт)
Ответ на задачу 12 (Выравнивание массива)
#include
int main() {
double arr[10];
std::cout << "alignof(arr): " << alignof(arr) << std::endl;
return 0;
}
(Вывод: 8 байт)
Ответ на задачу 13 (Адрес на куче)
#include
int main() {
int* p = new int(30);
std::cout << "Адрес на куче: " << p << std::endl;
delete p;
return 0;
}
(Вывод: адрес на heap)
Ответ на задачу 14 (Размер union)
#include
union U { char c; int i; };
int main() {
std::cout << "sizeof(U): " << sizeof(U) << std::endl;
return 0;
}
(Вывод: 4 байт)
Ответ на задачу 15 (Выравнивание bit-field)
#include
struct B { int x:4; char y; };
int main() {
std::cout << "alignof(B): " << alignof(B) << std::endl;
return 0;
}
(Вывод: 4 байт)
Ответ на задачу 16 (Сравнение адресов)
#include
struct S { int a; int b; };
int main() {
S s;
std::cout << "Разница адресов: " << (&s.b – &s.a) << std::endl;
return 0;
}
(Вывод: 1, но в указателях 4 байта)
Ответ на задачу 17 (Размер с packed)
#include
#pragma pack(1)
struct P { char c; int i; };
#pragma pack()
int main() {
std::cout << "sizeof(P): " << sizeof(P) << std::endl;
return 0;
}
(Вывод: 5 байт)
Ответ на задачу 18 (Выравнивание over-aligned)
#include
struct alignas(32) O { int i; }
int main() {
std::cout << "alignof(O): " << alignof(O) << std::endl;
return 0;
}
(Вывод: 32 байт)
Ответ на задачу 19 (Адрес строки)
#include
int main() {
const char* str = "hello";
std::cout << "Адрес строки: " << (void*)str << std:endl;
return 0;
}
(Вывод: адрес в RO-data)
Ответ на задачу 20 (Комбинированная: sizeof, alignof, &)
#include
struct M { short s; long l; };
int main() {
M m;
std::cout << "sizeof(M): " << sizeof(M) << std::endl;
std::cout << "alignof(M): " << alignof(M) << std::endl;
std::cout << "Разница адресов: " << reinterpret_cast
return 0;
}
(Вывод: 16, 8, 8)


