Try   HackMD

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

Типы данных с плавующей точкой:

IEEE 754-2008 - как представляются числа с плавающей точкой в современном компьютере (=> в C/C++)
Вольный пересказ
(стандарт IEEE-754 (не стандарт языка, стандарт железа, некоторые моменты перенял си))

Их всего 3 типа

  1. float = (32 бита) соответствует single precision IEEE 754-2008 10^(± 38)

  2. double = (64 бита) соответствует double precision IEEE 754-2008 10^(± 308)

  3. long double: (не договорились, про совместимость между компиляторами можно не думать)

    • может совпадать с double

    • может быть (128 бит) quad precision IEEE 754-2008, есть GCC, в x86 реализованно программно (очень медленно)


    • может быть (80 бит) extended precision (не входит в стандарт IEEE 754-2008, аппаратно реализован в x86, (быстрее любой программной реализации))

Диапазон в 10^(± 308) НЕ означает что будут храниться 308 десятичных знаков - означает, что максимальное значение по экспоненте будет 308 и оно будет неточным (будут храниться первые цифры)

Обычно мы не хотим использовать long double

double как минимум вдвое медленнее чем float (может быть сильно медленнее)

Константы

Целые константы реализуются как int, то есть 10 будет константной типа int (можно подправить тип написав 10u или 10llu, тогда она будет unsigned int и unsigned long long соответсвенно)

long long a = 1000000 * 1000000; //получиться черти что, вообще UB
//тк переполнение не определенно для signed 

long long b = 1000000ll * 1000000ll; //приведеться в long long и правильно посчитается

Переполнение знакового (signed) int - Undefined Behavior

Дробные константы реализуются как double, то есть 10.9 будет константной типа double (можно подправить тип написав 10.9f тогда она будет float)

Си всю арфметику считает в одинаковых типах! Нельзя к int прибавить unsigned. В случае, когда типы разные - копилятор их приводит к одному, например в случае int + unsigned int победит unsigned int

float a = 5.7f; a += 5.6; В этом случае a сначала сконвертируется в double сложится с константой и обратно сконвертируется в float.

Можно создать 16ричные и 8ричные костанты в тексте, 16ричные константы с плавающей точкой. (0x43534P234 - 16ричное с точкой, после p экспанента)

Ввод/Вывод

Форматы ввода семейства функций printf/scanf:

  • %i - десятичные, также 16/8ричные с префиксом (0x/0) (int)
  • %u - десятичные беззнаковые (unsigned int)
  • %x - HEX (16рич), без префикса

Форматы вывода немного другие, в часности %i и %d с printf работают одинакого, выводят десятичное число, чтобы вывести число в HEX надо использовать %x


Дробные:

  • float - %f
  • double - %lf (сокр long float)

Однако, если мы перпутаем в printf - %f и %lf (скормим в %f double или в %lf float) то всё отработает хорошо, тк printf - vararg функция, а vararg автоматически расширяет float до double. Это только в printf. (типы которые короче int (например char) автоматически расширяются до int)

Файловый ввод/вывод

fopen - открыть файл, принимает 2 аргумента

  1. путь (просто имя файла если в тек папке - то, откуда запускали исполняемый файл .exe), или абсолютный/относительный путь, только надо не забывать, что в пути надолибо удвоить обратный слэш - \\, либо писать путь через прямые - /
  2. режим (как открыть файл):
    • "r" - на чтение, если файла нет - вернет NULL.
    • "w" - на запись, если файла нет - создает, если есть - обнуляет файл.
    • "a" - на дозапись, если файла нет - создаст, если есть - откроет на конце файла.

Подробнее

У режима есть модификаторы - t или b, текстовый и двоичный (бинарный) соответственно, применяется после режима - "rt", "wb", по умолчанию всегда текстовый режим.

Можно открыть на запись/чтение одновременно - добавить + к режиму (до модификатора), например: r+b или w+, использовать очень аккуратно

Если происходит какая либо ошибка, нет файла/не удалось создать/нет прав - fopen вернет NULL, специальное значение (NULL == 0). Рекомендуется всегда проверять, тк легко получить null pointer exeption - падение программы.

fclose - закрыть файл, принимает указатель на FILE (необходимо всегда закрывать файл, в противном случае, в лучшем случае, файл будет держаться до конца нашей программы (а возможно до перезагрузки компьютера))

Файлы обязательно надо закрывать! Желательно когда он перестал быть нужным, а не в конце программы. Тк этот файл блокируется пока он открыт нашей программой, так же под него выделенна память

Чтобы работать с файлами - мы можем использовать функции семейства printf/scanf, добавив f к названию функции, и ещё один первый аргумент - указатель на файл (то, что возвращает fopen)? напрмер: fscanf(in, "%i", &a);

int read_and_write_sum_file()
{
    int a, b;

    FILE *in = fopen("in.txt", "r"); //"r" - на чтение
    FILE *out = fopen("out.txt", "w"); // "w" - на запись

    if(in == NULL)    // проверка на открытие
    {
        printf("I can't open the input file\n");
        return 1;
    }

    if(out == NULL) 
    {
        printf("I can't open the output file\n");
        return 1;
    }

    fscanf(in, "%i %i", &a, &b);  //считать два числа
    fprintf(out, "%i\n", a+b);    //записать их сумму 

    //code//

    fclose(in);    // закрытие файлов
    fclose(out);  
    
    return 0;
}

В более низкоуровневых функциях (с - open, win - create file) можно указать какие операции мы хотим оставить другим пользователям.

C++ файловым (и не только) стандартным вводом и выводом лучше не пользоваться, тк под ними скрываются большие нетривиальные куски кода, которые медленно работают и плохо распаралеливаются (из-за глобальных переменных) (лучше в С++ использовать сишные функции ввода-вывода)

Модификатор b двоичный - это про переводы строк Какие бывают переводы строк:

  1. Windows - 0x0D 0x0A (13 10) (\r \n)
  2. Unix - 0x0A (10) (\n) (Linux) (new MacOC)
  3. Old MacOC - 0x0D (13) (\r)

По ASCII, на самом деле:

Когда мы пишем \n в текстовом режиме - с run time автоматически приобразует их в нужный перевод строки (под систему в которой мы сейчас), мы не хотим так в двоичном режиме (например, когда открываем картинку)

Если мы работаем с двоичными данными, мы всегда хотим открыть в режиме с модификатором b. (часто это забывают сделать пользователи linux, тк у них совпадет \n и системный перевод)

Потоки

Стандартный ввод/вывод на самом деле тоже файловый, можно перенаправить из консоли в файл (freeopen)

Есть три стандартных потока:

  1. stdin - поток ввода
  2. stdout - поток вывода
  3. stderr - поток ошибок (пример - fprintf(stderr, "Hi");)

Про ветвление


if(out == NULL)    //можно писать без {} (блока) если ОДНА строчка
    printf("I can't open the output file\n");
else    // else - если if не отработал - условие в скобочках ложное
    printf("OK");


//если строчек будет несколько (без блока) в if будет выполненна 
//ТОЛЬКО ПЕРВАЯ, вне зависимости от отступа

; посреди текста - это пустой оператор, её любят ставить после }, однако она не всегда там нужна и её не стоит пихать куда попало, например:

if(out == NULL)
{
    printf("I can't open the output file\n");   
};    //в данном случа означает, что цепочка if закончилась
else // НЕ скомпилируется, тк не найдет свой if
    printf("OK");

Так же можно создавать длинные цепочки if с помощью else if - отрицает все предыдущие if и принимает новое утверждение в скобках, например:

if(a == 5)
{
    printf("a == 5\n"); 
}
else if(b == 3)
{
    printf("a != 5, but b == 3\n");  
}
else
{
    printf("a != 5, and b != 3\n");  
}

If в скобках принимает логическое значение, то есть 0 или 1 (false или true), исполняет код под под if когда значение в скобках принимает логическую единицу, в связи с этим, и тем, что NULL ассоциируется с 0, например, можно короче записать проверку на неоткрытие файла:

if(!in) //! - инвертировать логическое значение, 0 -> 1, 1 -> 0    
    //in указатель на файл (результат fopen)
{
    printf("I can't open the input file\n");
}

//ТОЖЕ САМОЕ

if(in == NULL)
{
    printf("I can't open the input file\n");   
}

Циклы

  1. while
int a = 4;
while(a < 10)    //цикл будет выполняться
    //пока условие в скобках - логическая единица
{
    a++;
}
// a == 10;

  1. do while
int a = 10;
do //блок под do выполниться один раз
{
    a++;
} while(a < 10); //затем цикл будет выполняться
//пока условие в скобках - логическая единица

//a == 11;

рекомедуется писать while на строке с {, для большей понятности


  1. for
for(int i = 0; i < 10; i++) // int i = 0 происходит до цикла
    // i < 10 условие как в while
    // i++ происходит на каждом шаге в конце
{
    printf("%i\n", i);
}
//напечатно: 0 1 2 3 4 5 6 7 8 9

Эквивалент:

for(A; B; C)
{
    D;
}

// ТОЖЕ САМОЕ (ПОЧТИ)

{
    A;    //происходит до цикла, обычно создание переменной
    while(B)    //условие
    {
        D;
        C;    //Происходит на каждом шаге
    }    
}

В таком эквиваленте есть только одно расхождение, при использовании слова continue;

continue; - ключевое слово для цикла, означает пропустить всё до конца следующего шага (Оставляет С в for)

break; - ключевое слово для цикла, означает выйти из цикла

Массивы

int q[10];  // завести массив длиной 10 элементов и названием q
q[0];       // доступ к 1му (0му) элементу массива q

Индекс в массиве - любая целочисленная переменная (значение)

Индексироваться double и float нельзя!\

Синтаксически можно управлять циклом (итерироваться) с помощью переменных с плавающей точкой, но так делать НЕ надо! Нпример, потому что число 1/10 непредставимо в двоичной системе, значение всегда будет чуть больше или чуть меньше, но никогда не 1/10, то есть 1/10 * 10 != 1;, более того умножить 1/10 и сложить 1/10 с высокой вероятностью тоже окажется не равным, тк в float и double постояноо происходят окугления. И это всё мы не хотим видеть при управление циклом!

int q[10];   

Означает, что последний адрес в массиве 9! Чем больше программируешь - тем больше убеждаешься, что индексация с 0 - оочень удобно

Завести массив мы можем любого размера, однако надо помнить, что в 32 битных системах указатели размерности 32 бита, то есть ~4 миллиарда значений (байтиков, 4 гб), и мы не сможем создать массив больше 4х миллиардов, тк просто не хватить указателей (в 64 битных системах количество указателей в 4млрд раз больше - фактически, гораздо больше чем можно поставить оператичной памяти в компьютер)

Когда мы обращаемся по индексам си НЕ проверяет, что мы уместились в диапазон, например

int q[10];  
q[100]; // не так страшно, когда мы читаем её, получим мусор

q[100] = 0; //страшно здесь

Потому что мы пишем в рандомную ячейку памяти, она может хранить всё что угодно: другая переменная, служебные переменные, даже кусок кода => здесь может ничего не произойти, а могут дикие спец эффекты

(стандарт си не гарантирует ничего в этом случае)

Одна из сложностей языка "C" - нужно следить что ты не вышел за границы массива.

Сложность в том, что когда мы бажим в Undefine believer, нам стандарт ничего не обещает, полный undefine, по этому у нас программа может работать правильно, а у пользователя, у другого программиста или просто на том же компьютере в другое время - отработать неправильно или вообще упасть.

Многомерные массивы

int q[10][3];    // завести двумерный массив длиной 10 на 3 
// и названием q

q[1][2]         // доступ к 2 элементу 1 строки  массива q

q[z][y][x] принято так индексировать многомерные массивы, тк это больше всего похоже на то, как они храняться в памяти

Про размер

Размеры связанны с тем, где мы создаем переменные. Если мы создаем массив как обычную локальную переменную - общее ограничение на размер стека (пара мегабайт для каждой программы). Если мы попытаемся выделить массив больше, либо программа скажет нам stack overflow, либо просто тихо исчезнет, тк чтобы что-то сказать (сделать) программе нужно свободное место на стеке

stackoverflow.com - одноименный шикарный сайт (форум), вопросов и ответов на темы программирования.

Оглавление

Full - C/C++, 3 лекция, 26.02.2022, Скаков