Условно в первом приближении при запуске программы ей дают непрервыный кусок памяти по адресам которых она может обращаться.
Условно эта память делится на три части - data, text и stack.
data
- область памяти, где хранится все, что определено
на всей области программы - то есть, в частности, глобальные
переменные.
text
- машинный код вашей программы.
stack
- по дефолту примерно 8 MiB. Область ответственная
за уровни вложенности программы, которая “как бы” хранит все в том
порядке в котором оно определяется. (Хотя на самом деле компилятор вам
не обязан ничего и может переопределять порядок или добавление,
видоизменение, игнорирование как хочет)
При какой-либо рекурсии мы на стек добавляем не только все плокальные переменные функции, но и указатель на то место, где он был вызыван, иначе мы не узнаем куда вернуться. Сам стек примерно вмещает 1 миллион запусков пустой “рекурсии”.
Динамическая память - память выделяемая в процессе выполнения программы
int *p = new int; // выделение памяти на int и получение его адреса
new
– создает объект данного типа в динамической
памяти.
new - это какое-то шаманство, которое пока разобрано не будет. Выполняется в 200-300 раз дольше чем сложение в хорошем случае. А в плохом сколь угодно плохо) ( ≥ 10000)
Утечка памяти(memory leak)
void foo() {
int* p = new int(5);
...
// without delete
return;
}
Если не вызвать delete
, то память останется условно
занятой, но провзаимодействовать мы с ней никак не сможем, так как
потеряли адреса.
В отличие от многих других языков в C++ нет сборщика мусора, поэтому надо убирать за собой.
Дважды удалять один и тот же указатель - UB(double free or corruption).
Провзаимодействовать с указателем и вызвать delete
-
UB.
Вызвать delete
не от указателя созданного
new
- UB.
После delete p
разыменовать p
- UB.
Статическая память
{
static int z;
}
Теперь z
лежит в вышеупомянутой data
, ей
предвыделило чуть больше памяти на этапе компиляции, специально для
z
и оно будет храниться там на протяжении всей программы
(но доступно только из своей области видимости).
Важно. Ключевое слово static
имеет
разный смысл для переменных внутри функций и глобальных переменных. Для
глобальных переменных static
влияет на линковку(internal
linkage, что бы это ни значило), противоположность ключевому слову
extern
.
Ниже приведены примеры переполнения стека(из-за рекурсии)
#include <iostream>
int f(int x) {
std::cout << '\n';
++x;
(x);
f}
int main() {
(0);
f}
// переполнение стека
#include <iostream>
int f() {
static int x = 0;
std::cout << '\n';
++x;
();
f}
int main() {
();
f}
// переполнение стека, but cooler
#include <iostream>
int f() {
int *p = new int(5);
std::cout << p << ' ' << *p << std::endl;
delete (++p);
();
f}
int main() {
(0);
f}
// переполнение стека
#include <iostream>
int main() {
int a[10];
int b[5] = {1, 2, 3, 4, 5};
int c[5]{};
std::cout << *(a + 3) << '\n'; // 4
int *p = a + 3;
// p[i] == *(p + i), причем i[p] == p[i], потому что в терминах указателей это буквально i + p == p + i
std::cout << p[-2] << '\n'; // 2
}
int*
и int[]
взаимозаменяемы, поэтому
следующий код вызовет ошибку компиляции
#include <iostream>
void f(int a[5]) {
;
}
void f(int* p) {
;
}
int main() {
int a[5] = {1, 2, 3, 4, 5};
int* b[5]; // массив из 5 указателей на int*, а не указатель на массив из 5 int
}
Массив можно выделить в динамической памяти (который на самом деле
указатель на начало) с помощью конструкции new T[]
.
#include <iostream>
int main() {
int *p = new int[100];
delete[] p;
}
Если пытаться сделать delete[]
от обычного указателя или
наоборот - это runtime error
#include <iostream>
int main() {
std::vector<int> v(10);
[-1] = 10000; // надругательство
vreturn 0;
delete[] &v[0]; // надругательство
return 0;
}
#include <iostream>
int main() {
// Variable lenght array (VLA)
int n = 100;
std::cin >> n;
int a[n]; // с cin нельзя, с n = 100 - можно
}
Убрана обратная совместимость с C, чтобы не сдвигать переменные на стеке на какое-то неопредленное число, которое мы узнаем только в рантайме. Но большинство компиляторов таки поддерживают VLA.
#include <iostream>
int main() {
const char* s = "abcdef"; // сам строковый литерал всегда хранится в статической памяти, сам указатель на него - на стеке
std::cout << (int)(s[4]) << '\n';
}
Идентификатор const
.
Запись const char *
значит, что мы вправе менять
указатель, но не вправе менять то, что лежит под ним.
Здесь подробно описано, как читать такие объявления.
Null-terminated strings. В языке Си есть конвенция
отождествлять строку с последовательностью байт, которая заканчивается
на 0
(для записи NUL символа можно использовать литерал
'\0'
).
Если нам дан указатель на начало, легко, например узнать длину строки, или скопировать одну в другую
size_t
(const char *s)
my_strlen{
size_t len = 0;
while (*s++) {
++;
len}
// если гарантируется, что s заканчивается нулевым байтом, то
// мы никогда не выйдем за границы выделенной памяти, то есть данный код корректный
return len;
}
void
(char *dst, const char *src)
my_strcpy{
while ((*dst++ = *src++));
// Для того, чтобы не было UB, нужно, чтобы dst, src
// заканчивались нулем и в dst было достаточно места
}
#include <iostream>
#include <cstring>
int main() {
const char* s = "abc\0def"; // сам строковый литерал всегда хранится в статической памяти, сам указатель на него - на стеке
std::cout << strlen(s) << ' ' << s << '\n'; // выведет '3 abc', потому что из-за поддержки C-style string все пытается прыгать вокруг присутствия /0 ровно в конце строки
}
Под каждым std::string
лежит C-style null-terminated
строка, указатель на начало которой получить вызвав метод
.c_str()
.
Указатель на функцию это “указатель на код”, его также можно
передавать в другие функции. Объявляется он как
return-type (*name)(arguments)
.
Опять же, по этой ссылке можно найти гайд по чтению этих записей.
#include <iostream>
int sqr(int x) {
return x * x;
}
int f(int x);
void sort(int *begin, int *end, bool(*cmp)(int, int));
int main() {
int x = 0;
std::cout << &x << '\n';
int(*p)(int) &sqr; // синтаксически корректное определние ссылки на функцию
std::cout << (void*)&sqr; // адрес sqr в памяти
int (*q)(int) = &f; // нельзя, так как функция не определена
}
Вызов функции по указателю медленнее, чем ее вызов напрямую, поэтому лучше избегать их если можно(например, с помощью шаблонов).
#include <iostream>
int sqr(int x) {
return x * x;
}
double sqr(double x) {
return x * x;
}
int main() {
int (*p)(int) = sqr;
double (*pp)(double) = sqr;
double (*ppp)(double) = (double(*)(double))(p);
std::cout << (void*)p << ' ' << (void*)pp << '\n'; // выведет 2 разных адреса, потому что оно взяло разные функции
}
Существуют также variadic функции. Ниже адаптирован пример отсюда
TL;DR Не пишите C-style variadic функции в C++. Будьте ОЧЕНЬ аккуратны при использовании variadic функций в C/C++.
// C-style variadic function
int
(size_t count, ...)
variadic_sum{
va_list ap;
size_t i;
int sum;
(ap, count);
va_start
= 0;
sum for (i = 0; i < count; ++i) {
+= va_arg(ap, int); // макрос принимает va_list и тип аргумента, который мы ждём
sum }
(ap); // может не делать ничего, но по стандарту нужно
va_end
return sum;
}
Данный код очень небезопасен по многим причинам. Первый аргумент
count
лежит на стеке, поэтому компилятор знает, как его
достать. А дальше могут лежать аргументы произвольных типов, которые
могут занимать разное количество памяти.
Как тогда вытащить их со стека? ap
указывает на текущую
позицию(сокращение от argument pointer
), а
va_arg
приводит ap
к типу, в нашем случае
int
, и прибавляет количество байт, которое этот тип
занимает.
Можно подумать, что в va_start
мы передаем количество
аргументов, но это неправда. va_start
это
макрос, а не функция, поэтому у него есть немного
контекста, например, адрес переменной count
. Он берет
указатель на count
и прибавляет к нему число байт, которое
count
занимает, чтобы указать на первый variadic
аргумент.
Тем самым, вторым аргументом в va_start
нужно указать
последний не variadic аргумент функции.
Одна из самых известных variadic функций с которой вы наверняка
сталкивались, это printf
. Если передать в
printf
недостаточное число аргументов, слишком много
аргументов, или тип аргумента не совпадет со спецификатором(например, вы
передали char*
в %d
), то произойдет
UB как раз из механизма работы va
. Например, оно
может брать аргументы выше по стеку, поэтому никогда не
передавайте в printf
недоверенный спецификатор формата,
например, prinf(username);
из соображений
безопасности.