Try   HackMD

08 лекция - C++, подробно

VLA и const

VLA - Variable-length array, массив переменной длинны (определяет свою длинну в момент исполнения программы)

Переменная внутри скобочек при создании массива, int a[b]; - это всегда VLA, даже если b - const переменная, в си. Подробнее про это в си
(const по переменной всё что делает - запрещает её изменять и требует иниц. при создании, => такие переменные могут быть инициализированны в момент запуска программы, например - другими переменными, которые зависят от ввода)

В си++ нет, не было и не ожидается VLA вообще (в стандарте, в некоторых компиляторах включенно в качестве расширения компилятора)
Следовательно const переменные которые могут быть инициализированны в момент компиляции - подходят для создания обычных массивов. Если мы попытаемся в си++ сделать массив длинны которая определяется в момент исполнения прграммы - по стандарту оно не должно скомпилироваться, однако некоторые компиляторы поддерживают VLA и в си++!
Ещё в си++ есть специальное слово - constexpr, оно говорит, что эта переменная обязана быть иниц. в момент компиляции.

Про разделение на файлы

#include не подключает никуда никакие файлы!

a.cpp

int g(int a, int b)
{
    return a + b;
}

main.cpp

int g(int, int); //прототип функции, что она где-то существует

int main()
{
    int result = g(3, 5); //result == 8
}

Чтобы функция g() была доступна в main.cpp нужно написать ей прототип в main.cpp!. Прототип позволяет вызвать функцию реализованную в другом файле, не инклуды. #include<...> - это простая текстовая вставка всего содержимого файла внутри <>, чтобы не писать прототипы ручками в каждом новом .сpp файле! Инклуды не подключают никаких библиотек, функций и прочего, это делают прототипы!

За жизнь

Динамические dll библиотеки требуются в момент запуска, а статические (вшиваются в программу) в момент компиляции


В си++ есть три типа переменных:

  1. Локальные
  2. Внутри класса, принято выделять именем
  3. Глобальные, принято называть с g_ Так же принято по разному выделять public и private поля класса, так же константы времени компиляции (обычно заглавными буквами, как #define)

На си в функции пустые скобки - g() означает, что у функции неизвестное количество, и типы аргументов(её можно вызвать с любым количеством любых аргументов) Отсутвие аргументов можно указать void в скобках - g(void)
На c++ отсутвие аргументов в скобках эквивалент void, что у функции нет аргументов!

В си так по историческим причинам, в первых версиях языка вообще не было прототипов, и когда мы пишем функцию без прототипа - компилятор считает, эта функция существует, принимает те параметры, которые мы ей передали и возвращает int, тогда были только int, прототип это способ чтобы компилятор проверил что бы всё совпало (вызов с реализцией). Язык C наследник языка B, в которых вообще не было типов.


Глобальные обьекты

У static есть 2 значения:

  1. static по глобальному обьекту - ограничивает видимость этого обьекта этим файлом, время жизни останется тем же (по умолчанию видимость глобального обекта - вся программа) (глобальная переменная, функция, структура) (так можно писать функции в .h файлов, но она будет в копии у каждого .cpp который подключил .h)

  2. static по переменной в функции - означет, что эта переменная одна на все функции

int b = 0;    //глобальная переменная b

int f(int c)
{
    static a = c + 5;    // иниц. происходит один раз только в первый вызов
    a++;    // будет исполняться каждый вызов f()
    return a;
}

int main()
{
    int res;
    res = f(1); //res == 7
    res = f(5); //res == 8
}

Пременная a живет всю программу, но область видимости (имени a) у неё только в функции f(), переменная b живет всю программу и области видимости у неё вся программа. (глобальные переменные можно называть как статические в функции, но тогда их не будет видно в этой функции)


В СИ глобально живущие обьекты (так же static переменные) могут быть инициализированны только ПРОСТЫМИ выражением вычисляемым в момент компиляции, НЕ зависящими от других глобальных обьектов:

int a = 3;        //скомпилируется
int b = 3*4;      //скомпилируется
//int с = a + 2;  //НЕ скомпилируется

int f(void)
{
    static a = 3;        //скомпилируется
    static b = 3*4;      //скомпилируется
    //static с = a + 2;  //НЕ скомпилируется
}

int main (void)
{
    return 0;
}

В C++ разрешили иниц. глобальные переменные сложными выражениями, например:

int f()
{
    printf("Call f\n");
    return 3;
}

int a = f();    //a == 3

int main()
{
    printf("Hello world!\n");
    return 0;
}

Вывод после запуска:

Call f
Hello world!

Глобальные переменные иниц ДО main()!

Настоящее начало программы происходит в: недрах OC -> С runtime (даже в C++) -> main

С runtime:

  • иниц себя
  • вычисляет argc, argv (аргументы main)
  • иниц глобальные переменные
  • вызывает main()
  • после main вызывает деструкторы глобальных обьектов (в си++)

В C++ также разрешили иниц. глобальные переменные глобальными обьектыми: (разделим код на файлы)

a.cpp

extern int a;   //обьявление переменной (прототип)

int f()
{
    return a + 5;
}

main.cpp

int f();    //обьявление функции (прототип)

//int b = f(); //так НЕ скомпилируется, потому что a ещё не иниц.

int a = 5;

int b = f(); //так скомпилируется

Ещё один случай:

a.cpp

int a = 5;

main.cpp

extern int a;
int b = 3 + a; //так НЕЛЬЗЯ
//тк порядок инициализации между РАЗНЫМИ файлыми неопределен

порядок можно опредлелить, но оч сложно


Давайте вернемся к первому примеру:

int f(int a)
{
    static b = a + 2;    // иниц. происходит один раз только в первый вызов
    return b;
}

int main()
{
    int res;
    res = f(1); //res == 7
}

Распаралеллим этот код, тогда у кучи тредов будет неиниц глобальная (по времени жизни) переменная, которую оин должны иниц каким-то своим значением, они одновременно начнут писать туда свои переменные и может получиться мусор, однако - по стандарту b должно быть иниц только один раз, что означает, что это место образует критическую секцию, которая сольет все треды в один и выстроит их в порядок очереди, этот код очень трудоемкий и содержит в себе кучи вызовов системных функций, ожиданиями, вызовами ядра и т.д.
Если бы тут было просто static b = 2; этого всего бы не было, то, что нам разрешили писать static b = a + 2; выглядит как почти ничего не изменилось, а на самом деле всё радикально изменилось! (Это задача консенсуса - договориться какая переменная, из какого треда должна иниц переменную b)
В этом вся идея C++, что тут более тяжелые вещи, но эти вещи могут быть значительно дольше исполняться.

Namespace

#include <math.h>

namespace my //без него не скомпилируется 
    //тк функция пересечется с стандартной
{
    unsigned int abs(int a)
    {
        if(a >= 0)
            return a;
        else
        {
            unsigned int abs_a = a; // нужно тк в стандарте 
            //знаковое переполнение не определенно!
            //(a = -a; - эквивалент инверт биты + 1) 

            abs_a = -abs_a; //эквивалент инверт биты + 1, 
            //только без Undefined Behavior 
            //(случался из-за переполнения) 

            return abs_a;
        }
    }   
}

int main()
{
    abs(-5);     // вызовется стандартная
    my::abs(-5); // вызовется наша
    return 0;
}

Чтобы не писать постоянно свои namespace, в стандартной библиотеки си++ все функции убрали в namespace std (в том числе сишные, вместо math.h стало cmath и также с остальными).

Теперь каждый вызов стандартной функции надо начинать с std::, что заметно бесит, так что разработчики добавили возможность не писать постоянно название namespace - using namespace std; (их можно написать несколько, главное чтобы не пресеклись функции (имя + те же аргументы))

Анонимные namespace:

namespace
{
    int f() {return 1;}
}

Делает магическое имя namespace и сразу же пишет для него using namespace
Таким образом пытались избавиться от static по глобальным обьектам, и даже задиплекейтили его в стандарте 2003, но андипликейтили в стандарте 2011, поняв абсурдность, подробнее.

namespace единственное, что делает - ещё больше заворачивает имена функции в name mangling

namespace можно обернуть в другой namespace

Шаблоны

Посмотрим на наши функции abs:

float abs(float a)
{
    if(a >= 0)
        return a;
    else
        return -a;
}

int abs(int a)
{
    if(a >= 0)
        return a;
    else
        return -a;   
}

Они выглядят как дословная копипаста тела функции, различие только в типах
Для таких случаев придумали шаблоны:


template <typename T> T abs(T a) 
//здесь не создается никакого кода, вообще 
{
    if(a >= 0)
        return a;
    else
        return -a;
}

int main()
{
    int a = abs(-5);
    float b = abs(-5.0); //будет созданна функция double 
    
    return 0;
}

В <> указываются аргументы шаблона, типы или целочисленные константы времени компиляции.

Шаблонные функции не создают код совсем! Компилятор создает копию функции только при обнаружении вызова с новым типом. (создает функцию подставив вместо шаблона какой-то тип)

Шаблоны - способ автокопипасты когда разница только в типе.

Вместо typename можно писать class - template <class T>


template <typename T> T max(T a, T b)
{
    return a > b ? a : b;    
}

// template <typename T, typename G> T max(T a, G b)
// {
//     return a > b ? a : b;    
// } плохое решение проблеммы

int main()
{
    //int a = max(5, 10.0); //не скомпилируется 
    //тк непонятно какую функции нужно создать, int или double
    int a = max(5, (int)10.0); //скомпилируется, но не интерестно
    
    int a = max<int>(5, 10.0); //явное указание аргументов шаблона 
    
    return 0;
}

T можно использовать внутри шаблона в любом месте, где можно использовать тип.

Можно сделать специальные типы шаблоннов для конкретных типов:

    
template <typename T> T abs(T a) //общая версия
{
    if(a >= 0)
        return a;
    else
        return -a;
}

template <typename int> unsigned int abs(int a)  
//спец версия для int
{
    if(a >= 0)
        return a;
    else
        return -((unsigned int)a);
}

(Напомни мне добавить то, что я знаю про шаблонны)

Ссылки


void f(int a, int *b, int &c) 
    //a - локальная копия, b - указатель на int (хранит адрес), 
    //с - ссылка, синтаксический сахар на указатель 
{
    a = 3;
    *b = 5;
    с = 7;
}

int main()
{
    int a = 1, b = 2, c = 3;
    
    f(a, &b, c); //a == 1, b == 5, с == 7
    //&b - адрес переменной b
    //f(a, &b, 5); //не скомпилируется, 
    //тк ссылка должна быть привязанна к какому-то обьекту
    //f(a, &b, с + 1); //не скомпилируется, 
    //тк у с + 1 нет адреса
}

Ссылки - синтаксический сахар на указатели, выглядит как обьект, но на самом деле указатель, не может быть привязанна к NULL, не может быть привязанна к константе, НЕ меняется после создания.


int main()
{
    int x = 4;
    int &y = x;
    
    y = 5; //x == 5
    
    int &z = y; //ссылается на x
    
}

Нельзя создать ссылку на ссылку, будет указывать на то, что указывает первая ссылка

Это были так называемые l-value ссылки, умеют связываться с обьектами, не с значениями!

int &&y - r-value ссылка, то что умеет связываться со значениями, но не с обьектами!

Если коротко: l-value - то, что может стоят слева от знака = r-value - то, что может стоять справа от знака =

const int &y - константная ссылка может связаться с обьектом и со значением, но через неё нельзя менять обьект!

const int &&y тоже имеет некоторый смысл, подробнее

auto

auto (в старом значении) - это место размещения данных, по локальным переменным в стандарте 3 возможных варианта, где размещать обьект:\

  1. auto - в секции данных в стеке (по умолчанию)
  2. static - в секции данных в сегменте иниц данных
  3. регистр - в регистре процессора (по желанию компилятора, сегодня компиляторы почти никогда так не делают)

auto в новом значении - тип этого обьекта такой, который в него присваивается. (то есть можно писать перед иеменем переменной только в момент иниц.) (взять тип справа от знака =) (придумали чтобы не писать длинный типы посл шаблонов)

auto f() //вычислить возвращаемый тип по return
{
    
    return 0;
}

auto вычисляется в момент компиляции и подставляет нужный тип

Оглавление

Full - C_C++, 8 лекция, 16.04.2022, Скаков