pandoc-pages/pages/mipt_cxx1/04_merged.md

339 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 2.2 Kinds of memory
Условно в первом приближении при запуске программы ей дают непрервыный кусок памяти по адресам которых она может обращаться.
Условно эта память делится на три части - data, text и stack.
`data` - область памяти, где хранится все, что определено на всей области программы - то есть, в частности, глобальные переменные.
`text` - машинный код вашей программы.
`stack` - по дефолту примерно 8 MiB. Область ответственная за уровни вложенности программы, которая "как бы" хранит все в том порядке в котором оно определяется. (Хотя на самом деле компилятор вам не обязан ничего и может переопределять порядок или добавление, видоизменение, игнорирование как хочет)
При какой-либо рекурсии мы на стек добавляем не только все плокальные переменные функции, но и указатель на то место, где он был вызыван, иначе
мы не узнаем куда вернуться.
Сам стек примерно вмещает 1 миллион запусков пустой "рекурсии".
Динамическая память - память выделяемая в процессе выполнения программы
```C++
int *p = new int; // выделение памяти на int и получение его адреса
```
`new` -- создает объект данного типа в динамической памяти.
> new - это какое-то шаманство, которое пока разобрано не будет.
> Выполняется в 200-300 раз дольше чем сложение в хорошем случае.
> А в плохом сколь угодно плохо) ($\geq 10000$)
Утечка памяти(memory leak)
```C++
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.
Статическая память
```C++
{
static int z;
}
```
Теперь `z` лежит в вышеупомянутой `data`, ей предвыделило чуть больше памяти на этапе компиляции, специально для `z` и оно будет храниться там на протяжении всей программы (но доступно только из своей области видимости).
**Важно**. Ключевое слово `static` имеет разный смысл для переменных внутри функций и глобальных переменных.
Для глобальных переменных `static` влияет на линковку(internal linkage, что бы это ни значило), противоположность
ключевому слову `extern`.
Ниже приведены примеры переполнения стека(из-за рекурсии)
```C++
#include <iostream>
int f(int x) {
std::cout << '\n';
++x;
f(x);
}
int main() {
f(0);
}
// переполнение стека
```
```C++
#include <iostream>
int f() {
static int x = 0;
std::cout << '\n';
++x;
f();
}
int main() {
f();
}
// переполнение стека, but cooler
```
```C++
#include <iostream>
int f() {
int *p = new int(5);
std::cout << p << ' ' << *p << std::endl;
delete (++p);
f();
}
int main() {
f(0);
}
// переполнение стека
```
# 2.3 Arrays
```C++
#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[]` взаимозаменяемы, поэтому следующий код вызовет ошибку компиляции
```C++
#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[]`.
```C++
#include <iostream>
int main() {
int *p = new int[100];
delete[] p;
}
```
Если пытаться сделать `delete[]` от обычного указателя или наоборот - это runtime error
```C++
#include <iostream>
int main() {
std::vector<int> v(10);
v[-1] = 10000; // надругательство
return 0;
delete[] &v[0]; // надругательство
return 0;
}
```
```C++
#include <iostream>
int main() {
// Variable lenght array (VLA)
int n = 100;
std::cin >> n;
int a[n]; // с cin нельзя, с n = 100 - можно
}
```
Убрана обратная совместимость с C, чтобы не сдвигать переменные на стеке на какое-то неопредленное число, которое мы узнаем только в
рантайме. Но большинство компиляторов таки поддерживают VLA.
```C++
#include <iostream>
int main() {
const char* s = "abcdef"; // сам строковый литерал всегда хранится в статической памяти, сам указатель на него - на стеке
std::cout << (int)(s[4]) << '\n';
}
```
**Идентификатор `const`**.
Запись `const char *` значит, что мы вправе менять указатель, но не вправе менять то, что лежит под ним.
[Здесь][rt-lt] подробно описано, как читать такие объявления.
**Null-terminated strings**. В языке Си есть конвенция отождествлять строку с последовательностью байт, которая заканчивается на `0`
(для записи NUL символа можно использовать литерал `'\0'`).
Если нам дан указатель на начало, легко, например узнать длину строки, или скопировать одну в другую
```C
size_t
my_strlen(const char *s)
{
size_t len = 0;
while (*s++) {
len++;
}
// если гарантируется, что s заканчивается нулевым байтом, то
// мы никогда не выйдем за границы выделенной памяти, то есть данный код корректный
return len;
}
void
my_strcpy(char *dst, const char *src)
{
while ((*dst++ = *src++));
// Для того, чтобы не было UB, нужно, чтобы dst, src
// заканчивались нулем и в dst было достаточно места
}
```
```C++
#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()`.
# 2.4 Functions and pointers to functions
Указатель на функцию это "указатель на код", его также можно передавать в другие функции.
Объявляется он как `return-type (*name)(arguments)`.
Опять же, по [этой ссылке][rt-lt] можно найти гайд по чтению этих записей.
```C++
#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; // нельзя, так как функция не определена
}
```
Вызов функции по указателю медленнее, чем ее вызов напрямую, поэтому лучше
избегать их если можно(например, с помощью шаблонов).
```C++
#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 функции. Ниже адаптирован пример
[отсюда](https://www.gnu.org/software/libc/manual/html_node/Variadic-Example.html)
**TL;DR** Не пишите C-style variadic функции в C++. Будьте ОЧЕНЬ аккуратны при использовании variadic функций в C/C++.
```C
// C-style variadic function
int
variadic_sum(size_t count, ...)
{
va_list ap;
size_t i;
int sum;
va_start(ap, count);
sum = 0;
for (i = 0; i < count; ++i) {
sum += va_arg(ap, int); // макрос принимает va_list и тип аргумента, который мы ждём
}
va_end(ap); // может не делать ничего, но по стандарту нужно
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);` из соображений безопасности**.
[rt-lt]: https://cseweb.ucsd.edu/~gbournou/CSE131/rt_lt.rule.html