[Статья] Самоучитель по программированию для начинающих

[Статья] Самоучитель по программированию для начинающих  

  By: ББ on 2020-03-08 17 ч.

[Статья] Самоучитель по программированию для начинающих

Краткий Самоучитель по Основам Программирования и по Компьютерным Технологиям в вопросах и ответах для начинающих

Вы пришли сюда чтобы набирать знания. Вы готовы экспериментировать со своим компьютером и провести за ним половину вашей жизни, возясь с самодельными программами и наблюдая, как они оживают. Вы хотите узнать, как я прошел/прошла этот путь и как я ко всему этому отношусь. Вы в правильном месте.

Перед тем как мы начнем занятия, я хочу сказать несколько вещей:

1. Вы видите кусок кода. Что с ним нужно сделать? Нужно заставить его работать. Для чего? Для того, чтобы можно было с ним играться и экспериментировать. Тот, кто этого не делает, тот ничему не научится.

2. Эта книга должна быть у вас на столе.

KR-C-Part-2.png

По английски djvu (копия, поменяйте расширение на djvu)
То же, но pdf (копия)
По русски, хороший перевод, pdf (копия)

Дело не в том, что я заставляю вас учить именно этот язык программирования. Просто на нем даются примеры в старых книжках, а также в справочных страницах, встроенных в Linux. Это основной язык программирования, многие остальные языки являются лишь обертками над ним (поверьте мне). К тому же синтаксис языка настолько всем понравился, что другие решили в своих языках сделать так же, поэтому все языки программирования немного похожи.

3. Что делать, если вы встретили незнакомое слово? Загляните в словарик, который дается в конце самоучителя. Если не помогло, то используйте поисковую систему (любую). Опять не помогло? Спросите здесь, в этом топике.


Глава 1. Берем быка за рога

Что такое компилятор??

Compiler, программа, которая берет ваш код в виде простого текстового файлика, и произведет на свет бинарник - файл, способный запускаться.

Программу можно собирать по частям. Результатом может быть не только самостоятельное приложение, например, можно сделать библиотеку, которая сама по себе не запускается, но из нее торчат функции, которые другие программы могут использовать.

Линкер (linker) - программа, которая склеивает новую программу из кусков и делает так, чтобы все адреса указывали туда, куда надо, чтобы все куски работали вместе.

Зачем в программе комментарии??

Комментарий - лучший друг программиста. Он позволяет писать произвольный текст прямо в коде, в том числе с использованием кириллицы. Выглядит вот так:

/* это комментарий, я могу писать в нем то, что хочу */

Комментарии используются для общения программистов между собой. Добавление комментария, объясняющего работу сложного куска кода считается признаком хорошего тона.

Кроме того, очень удобно временно закомментировать ненужный фрагмент кода, если нужно, чтобы он перестал работать, а выбрасывать жалко.

Компилятор игнорирует комментарии, для него главное, чтобы знаки начала и конца комментария были на месте.


Что такое переменная??

Маленький кусочек оперативной памяти, в котором можно хранить числовое значение. Переменную можно прочитать и использовать ее значение где-нибудь. Можно записать в переменную новое значение, при этом прежнее значение теряется. Например, можно сложить значение переменной с единичкой, а результат потом записать обратно (инкрементирование).

У каждой переменной есть размер, который измеряется в байтах. Очень часто переменная имеет размер машинного слова, например на 32-битном intel размер слова - 32 бита (4 байта).

У каждой переменной есть свой тип. Тип - это ... как бы вам объяснить. От типа зависит, каким образом эта переменная обрабатывается, какие инструкции процессора понадобятся.

Основные типы:

  • int - переменная, хранящая целое число (без дробей), по размеру часто равная машинному слову (4 либо 8 байт), работает быстро, много инструкций не требует, удобная во всех отношениях. Максимальное значение зависит от размеров переменной и от того, знаковая она или нет

  • unsigned int - то же, только беззнаковая (обрабатывается немного по другому)

  • char - по традиции, размер этой переменной равен одному байту. Может хранить число от -128 до +127

  • unsigned char - хранит число от 0 до 255, занимает один байт.

  • long int - в два раза больше чем int

  • long long int - в два раза больше чем long int

  • float - переменная, использующая вычисления с плавающей запятой (floating point). Пользоваться с осторожностью.

  • double - то же, двойная точность, размер в два раза больше


Для того, чтобы получить размер переменной, есть оператор sizeof(), который вычисляется компилятором на этапе сборки. В скобки нужно поставить саму переменную, либо название типа. Это полезно, например, если нужно прямо на ходу выделить немного места для переменной, понадобится указать размер куска памяти в байтах, в который она поместится.


Если вы хотите завести себе переменную, нужно выбрать, где она будет храниться. Тут есть варианты:

  • Глобальная переменная. Видна во всем блоке из любой функции. Будет висеть в памяти, пока программа не закончится. Новички ее очень любят. Не надо заморачиваться и думать, где она видна, а где нет.

  • Автоматическая переменная. Можно завести переменную внутри функции. Память для нее выделяется на ходу во время старта функции (в стеке, ну или в регистре). Когда функция выходит, считается, что эта память освобождена. Переменная видна только внутри функции. Можно изъ**нуться и сделать указатель на переменную и передать этот указатель в другую функцию (дочернюю), чтобы та могла писать по указателю, но тут надо быть осторожным, после освобождения пользоваться переменной нельзя (нужно просто не передавать этот указатель в родительскую функцию и не хранить его в глобальной переменной).

  • Иногда нужно выделять место для переменной прямо на ходу, при этом чтобы переменная держалась в памяти до тех пор, пока она нужна. Для этого есть куча (heap). Чтобы выделить место в куче, нужно дернуть системный вызов malloc(), при этом указать размер нужного куска в байтах. В замен мы получим указатель на этот кусок (или ошибку, если места больше нет). Этот кусок будет держаться до тех пор, пока мы не освободим его вручную с помощью free().

Spurdo просил добавить, что когда наша программа запускается, ей выделяется адресное пространство, в котором помещается куча и стек. Создавать кучу не надо, она будет доступна при старте программы.

Можно надергать себе несколько кусков, а потом в каждом куске хранить указатель на следующий кусок, и так объединять куски в цепочки (linked list, линкованный список). Нужно только указатель на первый кусок не потерять (глобальная переменная тут в самый раз).

Если указатель указывает на освобожденный кусок, его лучше поменять, чтобы он указывал по адресу 0x00000000 (NULL), чтобы избежать использования после освобождения (это не обязательно, просто правило хорошего тона). Многие уязвимости в программах основываются на этом трюке использования после освобождения.

Переменные имеют разный размер в зависимости от машины??

Да. Я не знаю, почему так. Есть переменные фиксированного размера:

  • uint32_t - 32 бита, буква u значит unsigned

  • uint16_t - 16 бит

  • uint8_t - 8 бит и другие...


Что такое указатель??

Обычная переменная, размер такой же, как int. Способ использования совсем другой, ведь это адрес в памяти. Можно читать и писать по этому адресу, можно менять сам адрес. Можно заниматься адресной арифметикой (про это будет отдельная интересная глава).

Процесс доставания значения из памяти называется разЫменовывание (dereferencing).
У любого объекта, хранящегося в памяти, можно взять адрес (и сохранить его в указатель).

Чтобы разыменовать указатель, нужно знать размер и тип элемента, на который он указывает. В самом деле, сколько байт нужно оттуда прочитать, чтобы разыменовать указатель на int? Правильно, sizeof(int) (точный размер зависит от машины, для которой компилировался бинарник).

Что будет, если я разыменую указатель, который указывает не туда??
Буквально то и будет. Компьютер прочитает значение по указанному адресу. Если процессор в этот момент находится в режиме реальном (или kernel mode), то просто прочитается память. Таким образом можно прочитать bios загрузчик, таблицу прерываний, другие программы, висящие в памяти. Если была запись по неправильному указателю, то это приведет к поломке других программ и повисанию системы (не сразу, постепенно). Лечится только ребутом.

С появлением режима защищенного, можно сказать, стало лучше. Такое обращение приводит к срабатыванию защиты и вылету программы (сразу, или чуть погодя). Так называемый Segmentation Fault - это он и есть.

Как хранить отрицательные числа в компьютере??

Компьютер хранит числа в виде единичек и нулей, верно? Как это выглядит?

Берем unsigned char, пишем в него какое-нибудь число от балды, например 42.

0b00101010
0x2a

Нам нужно отличать десятичные числа от двоичных и шестнадцатиричных. Префикс 0b значит двоичную систему (доступны цифры 0 и 1), префикс 0x значит шестнадцатеричную (десять цифр и еще шесть латинских букв). Префикса нет - значит обычное десятичное число. Если вначале 0 - значит восьмеричная система (после 7 идет 0). В любом случае старшие разряды находятся слева, младшие справа.

Зачем нужна шестнадцатеричная (hexadecimal) система??

В ней удобно записывать значения байтов. Если вы будете исследовать содержимое жесткого диска или залезете в чужую программу с помощью отладчика, то вы увидете байты в hex нотации. Каждый байт разделяется на две половинки (nibbles) по четыре бита. Каждому биту полагается одна 16-ричная цифра от 0 до f.

А зачем восьмеричная??

Это редкая система используется больше для традиции, можно задавать разрешение на доступ к файлам таким способом. Каждой такой цифре соответствует не 4 а 3 бита. Число 777 значит три группы по три бита, все выставленные в единички (разрешить все права для всех, никогда не выставляйте такой приоритет на вашем файле с паролями).


Ну как хранить отрицательные числа в компьютере??

Максимальное число для unsigned char - это 255

0b11111111
0xff

Погоди, а что будет, если превысить значение переменной??

Будет переполнение, после 255 идет 0, потом 1, потом 2 и т. д.

Как узнать, что случилось переполнение??

Никак. Перед сложением двух чисел нужно их проверить, будет переполнение или нет. Есть еще флаг переполнения в статус-регистре (carry flag), но пользоваться им почему-то не разрешает язык. Тут есть свои заморочки.


И все таки как хранить отрицательные числа в компьютере??

Signed отличается от unsigned тем, что старший бит зарезервирован под знак. Если он равен единице, значит число меньше нуля (отрицательное).

 3 0b00000011 0x03
 2 0b00000010 0x02
 1 0b00000001 0x01
 0 0b00000000 0x00
-1 0b11111111 0xff
-2 0b11111110 0xfe
-3 0b11111101 0xfd
-4 0b11111100 0xfc

Такой хитрый способ называется Two's complement (en.wikipedia.org/wiki/Two's_complement)

Если значение переменной постепенно увеличивать, то после +127 наступит -128, потом -127 итд

 126 0b01111110 0x7e
 127 0b01111111 0x7f
-128 0b10000000 0x80
-127 0b10000001 0x81
-126 0b10000010 0x82
-125 0b10000011 0x83

После -1 наступит переполнение, будет 0, потом 1, потом 2 итд. В процессоре включится флаг переноса (carry flag), но мы его игнорируем.

-2 0b11111110 0xfe
-1 0b11111111 0xff
 0 0b00000000 0x00
 1 0b00000001 0x01


Как конвертировать число в другой знак (как сделать число отрицательным)??

Очень просто. Инвертируем все биты (0 = 1; 1 = 0), затем добавляем единичку.

   7 0b00000111
~
= -8 0b11111000
+1
= -7 0b11111001

А обратно??

То же самое. Инвертируем все биты и добавляем единичку.

  -7 0b11111001
~
=  6 0b00000110
+1
=  7 0b00000111

Как сконвертировать число между системами счисления??

Берем программу-калькулятор, переключаем в инженерный режим, находим переключатель систем счисления, вводим число, смотрим, как это число меняется при переключении систем.

А вручную??

Вы уроки информатики в школе помните? Берем двоичное число 0b00010010
Каждый разряд означает какую-то цифру, единичка значит, что мы берем эту цифру, нолик - не берем. Чем старше разряд, тем выше число (степень двойки).

  0   0   0   1   0   0   1   0
128  64  32  16   8   4   2   1

16 + 2 = 18

На самом деле число 16 нужно не "взять", а умножить на 1, а затем взять (взять один раз)


Обратно:

      0       1   8
1100100    1010   1

сложение в столбик
             = 0b00000000
+ 0b00001010 = 0b00001010
+ 0b00000001 = 0b00001011
+ 0b00000001 = 0b00001100
+ 0b00000001 = 0b00001101
+ 0b00000001 = 0b00001110
+ 0b00000001 = 0b00001111
+ 0b00000001 = 0b00010000
+ 0b00000001 = 0b00010001
+ 0b00000001 = 0b00010010

Число 0b00000001 взять восемь раз, число 0b1010 (десятичное 10) взять один раз, сложить это все вместе.

Вместо восьми единичек можно было умножить единичку на шесть (сначала единичку на два, потом единичку на четыре, потом сложить).

Умножение на два делается просто, сдвигаем биты влево на один шаг. Освободившееся место справа займет нолик.
Умножение на четыре - это два умножения на два.



Что такое функция??

Кусок кода, который висит в памяти и ждет, пока его вызовут. Функции могут вызывать другие функции. Инструкции для процессора выполняются по очереди (процессор пытается параллелить выполнение чтобы немного увеличить скорость, но все равно получается по очереди). После того, как кусок кода отработает, процессор должен прыгнуть обратно туда, откуда функция была вызвана, для этого есть специальная инструкция (ret).

Как функция узнает, куда ей надо прыгнуть в конце??

Интрукция ret достает адрес возврата из стека и прыгает по нему.

Откуда в стеке взялся адрес возврата??

Инструкция call, перед тем как прыгнуть на указанный адрес (на первую инструкцию той функции, которую мы хотим вызвать), заталкивает адрес возврата в стек. Адрес возврата - это то место, которое идет после инструкции call.

Если адрес возврата в стеке испорчен, инструкция ret все равно по нему прыгнет??

Да. Многие вирусы проникают в систему именно так, модифицируя стек, если программист расположил буфер в стеке и прозевал проверку на границы.

Зачем функция что-то возвращает??

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

Есть функции, которые ничего не возвращают. Есть те, которые возвращают указатель на что-то (либо нулевой указатель, если что-то пошло не так).

Аргументы, зачем они??

Маленькие куски данных, которые передаются (копируются) в функцию и которые нужны ей для работы. Например, чтобы открыть файл на чтение, нужно передать функции fopen() имя этого файла. Как? Нужно, чтобы строка с именем лежала где-то в памяти, указатель на строку передаем как аргумент функции, а уже сама функция будет читать буквы оттуда. Если все пойдет хорошо, функция вернет указатель на структуру FILE. Можно открыть несколько файлов одновременно, и эти указатели помогут не запутаться, какой файл где.

Аргументы заталкиваются в стек перед вызовом функции. Следом заталкивается адрес возврата. Вызываемая функция может их читать и менять, это не влияет на переменные в родительской функции.

Функция может вызываться одновременно из нескольких мест. В каждом случае стек будет свой и аргументы будут свои. Если вы увлекаетесь многотредовостью, то у каждого треда должен быть свой стек.

Аргументы нужно давать в правильном порядке с правильными типами и размерами, потому что функция будет читать их из стека. Компилятор проверяет, чтобы все сходилось, если не сходится, компилятор отказывается собирать программу.

Может ли функция вызывать саму себя??

Да, это называется рекурсия. При каждом вызове в стек засовывается новая копия аргументов функции и адрес возврата. Если этот процесс зациклится, то так можно быстро засрать свободное место, и будет Stack Overflow с последующим вылетом.

А вообще рекурсия выручает часто, если аккуратно пользоваться.

Что такое стек (stack)??

Это область памяти, лежит рядом с кучей (heap) и растет ей навстречу. В нее можно заталкивать (push) значения и вытаскивать их в обратном порядке (pop). Фишка в том, что не надо знать адрес в памяти, чтобы это делать. Процессор сам подвинет указатель стека (stack pointer) на новое место.

При заталкивании указатель стека уменьшается на величину элемента (= размер машинного слова), затем это машинное слово пишется в память по указанному адресу.

При вытаскивании сначала читается память, затем указатель увеличивается обратно.

То есть стек растет вниз. Это похоже на школьную тетрадку. Один конспект (heap) пишется как обычно, другой (stack) пишется в ту же тетрадку, но в перевернутую вверх тармашками (начиная с последней страницы). Однажды два конспекта встретятся.

Стек используется главным образом для хранения адресов возврата и прочего барахла, функции размещают свои автоматические переменные в стеке. Главное не забыть в конце вернуть указатель стека на место (язык программирования позаботится об этом, вручную редактировать указатель стека вам никто не даст (не, ну есть способ сделать и это, с помощью ассемблерной вставки, наверное)).

Spurdo просил добавить, что слова "куча" и "стек" я использую в контексте управления памятью. Потому что в университете студенты учат структуры данных, среди которых тоже есть куча и стек.

Если функции могут вызывать друг друга, то какая из них включится первой??

Если мы делаем приложение, а не библиотеку, то это приложение должно как-то запускаться. Одна из функций должна быть главной.
Такая функция есть, она должна называться main. Она запустится автоматически, через ее аргументы можно прочитать аргументы командной строки чтобы выяснить, что хотел пользователь, когда запускал нашу программу. Возвращаемое значение (тип int) после завершения программы станет будущим exit-code, по нему другие программы, вызвавшие нашу программу, смогут выяснить, хорошо закончилась программа, или не очень.

Практика!!

Берем код. Пример кода на языке Си

Самый простой пример - это helloworld, их вы найдете не мало, весь интернет только ими и наполнен. Мы пойдем дальше, прочитаем аргумент коммандной строки (наша программа выводит ту фразу, которую мы захотим):

#include <stdio.h>

int main (int argc, char **argv) {
  fprintf(stdout, "argument #1 = %s\n", argv[1]);
  return 0;
};

Загрузите этот текст в редактор, который оборудован подсветкой синтаксиса, вы сразу увидите, что редактор выделяет цветом строковые литералы (которые между двойными кавычками), а внутри них escape-последовательности (\n - символ переноса строки), и кучу всего другого. Красиво, правда?

Что такое строковый литерал??

Способ ввести в память строку из букв, буквы попадут в память друг за другом, по одному байту (char) на букву. Если присутствует кириллица, то на каждую букву кириллицы будет вставляться два байта (multibyte character) кодировки UTF-8 (если вы на Linux'е), об этом потом будет разговор.

Строковые литералы защищены от записи. Если программа попытается на ходу поменять в них буквы, будет SegFault.

В конце автоматически добавляется нулевой байт. Нулевой байт имеет сокровенное значение - он означает конец последовательности символов. В языке Си строка может быть любой длины, лишь бы на конце был нулевой байт. Если его нет, то при выводе на экран вслед за строкой высыпится то, что лежало в памяти возле нее. Ничего не напоминает? (Heartbleed, там механизм немного другой, но последствия похожие).

В соседнем лагере (Turbo Pascal, Delphi) строки устроены немного по другому, там длина строки хранится в первых двух байтах строки, отсюда ограничение на ее длину 65535 байт. У турбо паскаля вообще 255 (один байт).

Я ничего не понела, можно по подробнее про этот пример кода??

#include <stdio.h>

Директива include говорит, что нужно пойти в директорию, где лежат инклУды (хИдеры, header), открыть текстовый файл с указанным именем, и вывалить его содержимое прямо в текст программы. Там будут объявления функций, будут указаны аргументы, которые функции принимают, их типы, возвращаемые значения. Без них компилер будет ругаться, что не может проверить, правильно ли мы вызываем функции. До кучи там лежат всякие констАнты, енУмы, прочее барахло.

int main (int argc, char **argv) {

Ага, int значит, что сейчас будет что-то, что является типом int либо возвращает тип int. main - ага, после него идут круглые скобки, значит это имя функции, внутри скобок аргументы, а внутри фигурных скобок лежит тело функции - собственно, сами команды, которые будут выполнены. Имя могло бы быть любым, но если мы хотим, чтобы функция сработала автоматически, она должна называться main. avrc и argv - это имена для аргументов они видны только внутри функции, вместо avrc argv можно придумать любые другие имена. argc - это цифра, хранящая число аргументов, argv - сами аргументы, argv - это указатель на массив строк, если его разыменовать, получится указатель на char, который можно сунуть в fprintf. Используя адресную арифметику можно аккуратно прочитать все строки, по одной на каждый аргумент командной строки.

  fprintf(stdout, "argument #1 = %s\n", argv[1]);

Незнакомое название, есть круглые скобки, значит это вызов функции (мы ведь внутри функции, верно?). похоже, это функция из стандартной библиотеки, открываем терминал вводим 'man fprintf', выскакивает малуал на функции семейства printf. Они занимаются тем, что выводят поток символов. Первый аргумент - это указатель на структуру FILE - мы можем выбрать, в какой файл писать, круто. В качестве файла мы вставляем stdout - это какая-то переменная, которая ссылается на стандартный поток вывода, она определена где-то наверху. Второй аргумент - строка формата, то есть сюда нужно отправить указатель на первую букву в строке, мы воткнули сюда строковый литерал, который записал строку в память, а вместо себя вернул указатель на ее первую букву, как обычно. \n - это так выглядит символ переноса строки. %s - значит здесь вместо %s вставится содержимое слежующего аргумента, который должен быть указателем на char. Действительно, мы даем ему этот указатель. Есть хитрый способ, с помошью которого можно создать функцию, которая принимает переменное число аргументов, printf - как раз такая. Эта функция возвращает количество букв, которые она хотела записать, мы, однако, не используем это значение, никуда не сохраняем его.

  return 0;

Кодовое слово, требует прервать выполнение функции и вернуть значение 0, ведь мы внутри функции, которая возвращает целое число (int). По традиции 0 означает успешное завершение программы.

Что означает единичка в квадратных скобках??

Индекс массива. Мы достаем из массива элемент №1 (нумерация начинается с нуля). Это адресная арифметика, мы займемся ей позже (эта веселая тема требует отдельного разговора).

Как это запустить??

Берем Linux, открываем терминал, вводим

gcc

Компилятор ругается, что ему не дали входной файл. Значит у нас есть компилятор! Можем продолжать. Создаем простой текстовый файлик, копируем туда код, дадим файлу имя programma1.c с расширением .c в конце

Открываем терминал на этот раз в той папке где лежит файлик:

gcc -Wall programma1.c -o programma1

gcc - наш компилятор
-Wall - включить все предупреждающие сообщения
programma1.c - имя входного файла
-o programma1 - имя выходного бинарника

Рядом с файлом появился еще один. Это бинарник. Можем запустить его:

./programma1
argument #1 = (null)

Наша программа умеет что-то печатать в консоль. Круто. Почему (null)? Потому что мы передали в функцию fprintf нулевой указатель вместо строки. Попробуем по иному:

./programma1 Hello Runion
argument #1 = Hello

Неплохо. Мы дали программе аргументы, первый аргумент попал на выход. Второе слово - это второй аргумент. Попробуем по иному:

./programma1 'Hello Runion'
argument #1 = Hello Runion

У вас получилось запустить этот код? Поздравляю, самое сложное позади, дальше будет проще. Поиграйтесь с кодом, попробуйте по пускать ошибки и смотреть, как компилятор будет ругаться на них. Он покажет вам номер строки, которая его не устраивает.

Продолжение в следующем посте. Устал(а) писать. На вычитку нет сил.

Ах, да! Этот цикл статей я публикую только на Runion. Nikkon, прикрепи топик наверх, пусть посетители школы, прежде чем постить, сначала прочитают мой самоучитель. Вопросы тоже можно слать сюда, в эту тему.

:)

[Статья] Самоучитель по программированию для начинающих  

  By: Slili on 2020-03-09 22 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Доброго времени суток, ББ. Больше спасибо за труд, на рунионе такого не встречал. Хотел в некоторых моментах дать небольшие пояснение(возможно неуместные для новичков).

В моменте про компилятор добавил бы информацию о препроцессорной обработке. Также думаю для новичков полезно объяснить как происходит компановка/лингкова файлов, где какие файлы вылезают на каждом из процессов.

Что такое компилятор??

Compiler, программа, которая берет ваш код в виде простого текстового файлика, и произведет на свет бинарник - файл, способный запускаться.
Программу можно собирать по частям. Результатом может быть не только самостоятельное приложение, например, можно сделать библиотеку, которая сама по себе не запускается, но из нее торчат функции, которые другие программы могут использовать.
Линкер (linker) - программа, которая склеивает новую программу из кусков и делает так, чтобы все адреса указывали туда, куда надо, чтобы все куски работали вместе

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

Считаю что это необходимо знать, если в будущем потенциальный программист заинтересуется что же там происходит или вдруг захочет пошалить и начнет изучать С++11 (lvalue и rvalue ссылки) =)

Например, можно сложить значение переменной с единичкой, а результат потом записать обратно (инкрементирование).

Также думаю неплохо было бы добавить про основные типы ошибок, которые могут встретиться. Успехов в следующих выпусках. Думаю данный формат будет многим интересен.

4 года назад читала Кернигана и Ритчи, надо будет перечитать  :)

 Вложения

[Статья] Самоучитель по программированию для начинающих  

  By: Passer on 2020-03-10 02 ч.

Re: [Статья] Самоучитель по программированию для начинающих

ББ пишет:

Compiler, программа, которая берет ваш код в виде простого текстового файлика, и произведет на свет бинарник - файл, способный запускаться.

Программу можно собирать по частям. Результатом может быть не только самостоятельное приложение, например, можно сделать библиотеку, которая сама по себе не запускается, но из нее торчат функции, которые другие программы могут использовать.

Линкер (linker) - программа, которая склеивает новую программу из кусков и делает так, чтобы все адреса указывали туда, куда надо, чтобы все куски работали вместе.

Как раз-таки линкер (компоновщик) и создаёт исполняемый файл.  В целом, у C процесс выглядит следующим образом:

1. Исходный код (file.c, file.*****p)
   V Препроцессинг V
2. Препроцессированный код (file.i, file.ii)
   V Компиляция V
3. Ассемблерный код (file.s)
   V Ассемблирование V
4. Объектный код, объектный модуль (file.o)
   V Компоновка V
5. Исполняемый файл (file)

Кажется, что такие "мелочи" не критичны, но в итоге формируют у новичка неправильно понимание  основ программирования.

ББ пишет:

Маленький кусочек оперативной памяти, в котором можно хранить числовое значение. Переменную можно прочитать и использовать ее значение где-нибудь.

Не проще сразу написать, что это поименованная область оперативной памяти, чем запутывать ещё сильнее, говоря про какие-то кусочки? :)

ББ пишет:

У каждой переменной есть свой тип. Тип - это ... как бы вам объяснить. От типа зависит, каким образом эта переменная обрабатывается, какие инструкции процессора понадобятся.

Очередной пример усложнения путём попытки упрощения. Тип данных - это диапазон значений и операций над ними. В качестве понятного для гуманитария (или ребёнка) примера можно привести тип "мебель" - она может быть деревянной, мягкой и т.д. (возможные значения). На ней можно сидеть, лежать и т.д. (возможные операции).

ББ пишет:

Обычная переменная, размер такой же, как int. Способ использования совсем другой, ведь это адрес в памяти. Можно читать и писать по этому адресу, можно менять сам адрес. Можно заниматься адресной арифметикой (про это будет отдельная интересная глава).

Тут новичок совсем не поймёт. Можно прямо сказать, что указатель - это переменная, значением которой является адрес другой переменной.

В целом, частью понятно, частью только запутает новичков, но затея хорошая, если доработать ;)

[Статья] Самоучитель по программированию для начинающих  

  By: ББ on 2020-03-10 11 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Slili пишет:

В моменте про компилятор добавил бы информацию о препроцессорной обработке.

Будет. Будет разговор про константы, макросы и все такое.

Slili пишет:

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

Стоп! Стоп, не надо путаницы. И то и другое - инкремент. Суть одна, и там и там значение читается, увеличивается на единицу и пишется назад (всегда пишется, никаких копий не надо).

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

Откройте книгу на странице про инкременты и читайте.

Еще есть знаменитый парадокс ++i + ++i тоже про это.


Passer пишет:

Очередной пример усложнения путём попытки упрощения. Тип данных - это диапазон значений и операций над ними. В качестве понятного для гуманитария (или ребёнка) примера можно привести тип "мебель" - она может быть деревянной, мягкой и т.д. (возможные значения). На ней можно сидеть, лежать и т.д. (возможные операции).

Еще можно объяснить, как работает телевизор, рассказывая про гномиков, которые бегают и рисуют картинку. Вы пытаетесь охватить все возможные языки программирования и дать общее определение,которое в общем и в целом верное, я соглашусь.

Я в своем самоучителе делаю упор на два языка - ассемблер и си. Мой самоучитель больше для тех, кто любит копаться в железе, кто хочет знать, как это устроено под капотом. Дело в том, что от типа переменной зависит именно то, какие инструкции процессора попадут в бинарник. Сложение чисел происходит примерно одинаково, но при умножении, например, учитывается, знаковая переменная или без-знаковая, от этого зависит, какую инструкцию брать - умножение со знаком или без знака. Причем размер вроде бы одинаковый - четыре байта они и есть четыре байта, а обращение с этими четыремя байтами сильно зависит от типа.

Еще переменную можно кастануть (cast) из одного типа в другой. Об этом еще будет разговор.

Passer пишет:

Тут новичок совсем не поймёт. Можно прямо сказать, что указатель - это переменная, значением которой является адрес другой переменной.

Ну да, так и есть. А если указатель указывает не на другую переменную а на непонятно что, или вообще указатель равен нулю? Он ведь от этого не перестает быть указателем, верно?

[Статья] Самоучитель по программированию для начинающих  

  By: Slili on 2020-03-10 18 ч.

Re: [Статья] Самоучитель по программированию для начинающих

ББ, спасибо за указание почитать, обязательно перечитаю спустя время.
Вопрос: постфиксные и префиксные инкремент для вас одно и то же? Как я понял по ответу - да.

И то и другое - инкремент. Суть одна, и там и там значение читается, увеличивается на единицу и пишется назад

Зачем же интересно Ритчи решил создать язык где ++i и i++ тоже самое, совсем что ли, а за ним ещё последовал Страуструп после этого, совсем парни заработались. Ведь действительно постификсные и префиксные тоже самое, ей богу. А Страуструп ещё дальше пошел, взял и зачем для одинаковых операторов, взял и перезагрузку операторов сделал разную. Ахахахах, вот умора  :yao:

Другое дело, если этот инкремент участвует в выражении

А что для вас инкремент выражение или/и ариметический оператор? Как я понимаю, вы понимаете что это арифметический оператор. i = i + 1 является для вас выражением?

Я понимаю про ++i, все ровно, увеличили, передали значение по ссылке обратно. Но неужели вы думаете что i++, одно и то же.
И вы даже наверно думаете что что если я запущу 100500 потоков и на каждом единовеременно буду делать 100500 рекурсивных циклов с ++i и i++ будет тоже самое. Нет, поверьте, во втором случае компьютер будет просить вас пощадить его и опустить в мир иной(вы же по железу). Потому что копии бьют больно

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

Иду ровно по вашему примеру. Берем складываем два числа одно из них с инкрементом, тааак, понятно. Создаю переменные три переменые типа  int

int x = 0, y = 1, z = 2;
x = y + (z++); // x = 3

Так, понятно, мой z = 3, где-то создался наверно, но потерялся и использовалось почему-то предыдущее значение.

int x = 0, y = 1, z = 2;
x = y + (++z); // x = 4

А тут все так хорошо. Попробую по-другому

int x = 0, y = 1, z = 2;
++x = y + z; // x = 3

Работает, ура. Попробую иначе

int x = 0, y = 1, z = 2;
x++ = y + z; //  undefined behavior

Не работает. Инкременты одинаковые, а работают по-разному, засада какая-то!!!!
Может все-таки инкременты/декременты постфиксные и префиксные разные? :petrosian:


Суть одна

Прочитал я про инкременты у Кернигана и Ритчи максимально не подробно и поверхностно. Написано что происходит снаружи, а внутри, к сожалению, нет


ББ, с удовольствием продолжу чтение цикла ваших статей и буду ждать новых выпусков. Надеюсь не забросите

[Статья] Самоучитель по программированию для начинающих  

  By: Unknown123 on 2020-03-10 18 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Не имею ничего против еще одного учебника по C, но интересно, зачем? Языку сто лет в обед, учебников и так море
P. S: Видел самые разные стили написания кода, но точку с запятой после определения функции вижу впервые


PGP: pgp.mit.edu/pks/lookup?op=get&search=0x7E65C45EFFBC9453
Jabber ID: [email protected] (plain, OMEMO, OpenPGP, no OTR)
Почта: [email protected] (читаю не часто, если не отвечаю долго, то пишите в жаббер)
IT-шник(подробнее при личном общении), хочу найти удаленную работу в "даркнете", буду рад предложениям

[Статья] Самоучитель по программированию для начинающих  

  By: Slili on 2020-03-11 07 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Unknown123 пишет:

Не имею ничего против еще одного учебника по C, но интересно, зачем? Языку сто лет в обед, учебников и так море

Возможно в конце будет что-то нетривиальное и выходящее из курса типичных уроков по С или в промежутках.

[Статья] Самоучитель по программированию для начинающих  

  By: ББ on 2020-03-11 08 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Slili пишет:

Я понимаю про ++i, все ровно, увеличили, передали значение по ссылке обратно.

Нет там никаких ссылок. Это не Java.  :finger:

Slili пишет:

Может все-таки инкременты/декременты постфиксные и префиксные разные?

Ну конечно разные. Набор опкодов, который увеличивает переменную, одинаковый. Вопрос в том, где он стоит: до или после. :)

[Статья] Самоучитель по программированию для начинающих  

  By: mizantrop on 2020-03-11 09 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Вот этот курс лучшее, что встречал по С для совсем новичков, он базовый (первый опыт автора, есть огрехи по повествованию), сейчас уже есть  доработаный но платный

[Статья] Самоучитель по программированию для начинающих  

  By: Lamarr on 2020-03-11 10 ч.

Re: [Статья] Самоучитель по программированию для начинающих

ББ пишет:

Нет там никаких ссылок. Это не Java.  :finger:

Мсье путает C и C++.

К слову, ни

x++ = y + z;

ни

++x = y + z;

не скомпилируется компилятором чистого Си. Обе версии инкремента в чистом Си не являются lvalue.

А в плюсах компилятор хавает

++x = y + z;

но не хавает

x++ = y + z;

по той же причине.
О каком undefined behavior речь - не понятно.

Вообще, это конечно унылая попытка показать, кто здесь самый умный, и спор ради спора. Во-первых, использование инкремента как lvalue это нечитаемая хуйня из под коня, и просто не нужно. Во-вторых - новичку в контексте этого самоучителя оно тем более не нужно. А когда станет нужно, тогда будет достаточно опыта и знаний самому найти стандарт/документацию, и узнать, как оно работает.

 Вложения

[Статья] Самоучитель по программированию для начинающих  

  By: ББ on 2020-03-11 10 ч.

Re: [Статья] Самоучитель по программированию для начинающих

mizantrop пишет:

сейчас уже есть  доработаный но платный

:o

mizantrop, ты забыл вставить ссылку. Какой курс ты имел ввиду?


Lamarr пишет:

К слову, ни

x++ = y + z;

ни

++x = y + z;

не скомпилируется компилятором чистого Си. Обе версии инкремента в чистом Си не являются lvalue.

Обе эти две вещи не имеют смысла. Зачем увеличивать переменную на единицу чтобы потом сразу же стереть и записать новое значение y + z ? Не удивительно, что компилятор не собирает код, который ты сам не понимаешь.

 Вложения

[Статья] Самоучитель по программированию для начинающих  

  By: ББ on 2020-03-12 16 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Продолжаем практику!!

Предыдущий пример (№1) умеет читать аргументы коммандной строки. Его нужно доработать, чтобы он проверял, есть ли там эти самые аргументы, чтобы он не пытался читать несуществующие. argv - это указатель на массив указателей, размер (sizeof(argv)) == 4 байта (размер машинного слова) - ну, то есть, это обычный указатель, он указывает на начало области в памяти, где один за другим лежит несколько других указателей, каждый из них указывает на первый символ какой-то строки.

Переменная argc (тип int) - сообщает количество элементов в массиве argv. По стандарту, там сзади добавляется еще один элемент - нулевой указатель, на всякий случай. Он не учитывается в argc. Именно на него мы напоролись в тот раз (argument #1 = (null)).

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

Пример №2:

#include <stdio.h>

int main (int argc, char **argv) {

  int i;
  for (i = 0; i < argc; i++) {
    fprintf(stdout, "argument #%i = %s\n", i, argv[i]);
  };

  return 0;
}

Создаем исходный файлик, компилируем, запускаем:

gcc -Wall programma2.c -o programma2

./programma2 Hello Runion
argument #0 = ./programma2
argument #1 = Hello
argument #2 = Runion

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

Квадратные скобки означают, что нужно прочитать память по указателю argv (со смещением) и использовать это значение. argv имеет тип "указатель на указатель на char" (весит 4 байта). После разыменовывания получится "Указатель на char" (тоже 4 байта, мы только что прочитали эти байты из памяти).

Цифра внутри скобок - это смещение (индекс массива). С его помощью мы можем прочитать не только тот элемент, на который указывает argv, но и то, что лежит рядом с ним. Единица значит, что нужно отступить вправо на четыре байта (один шаг) и прочитать значение (размер значения тоже четыре байта). Почему четыре? Потому что это адресная арифметика - размер шага равен размеру элемента массива (зависит от типа указателя).

argv определен как указатель на указатель на char, значит, шаг адресной арифметики == 4 (sizeof(*char))

Если и этот указатель разыменовать (еще одни квадратные скобки), тогда шаг == 1 (sizeof(char))

Можно изъ**нуться и кастануть указатель в другой тип, тогда то, что висит в памяти, останется как есть, а шаг арифметики будет другой, об этом позже.

Тот указатель, который мы получили, указывает на первый символ строки, которая лежит где-то в памяти. Этот указатель можно отдать в printf(), и функция будет читать первый символ, а затем остальные, пока не доберется до нулевого байта (метка конца строки).

Попробуем по иному. Будем сами читать буквы одну за другой и вставлять знаки переноса строки между ними. Получится вертикальный столбик с буквами.

Пример №3:

#include <stdio.h>

int main (int argc, char **argv) {

  int i, ii;
  for (i = 0; i < argc; i++) {
    ii = 0;

    while (argv[i][ii]) {
      fprintf(stdout, "%c\n", argv[i][ii]);
      ii++;
    };

    fprintf(stdout, "\n");
  };

  return 0;
}

Вывод программы:

gcc -Wall programma3.c -o programma3

./programma3 Hello Runion
.
/
p
r
o
g
r
a
m
m
a
3

H
e
l
l
o

R
u
n
i
o
n

Вроде работает :)



Глава 2. Основы, тонкости, всякие мелочи

Нужно ли рисовать алгоритмы?? (Да, нужно!!)

Берем чистую бумажку, рисуем алгоритм.

1.png

Представьте себя на месте процессора. Процессор будет шагать по нашим дорожкам. Если он встал на инструкцию, то он не пойдет дальше, пока не выполнит ее. Шагать можно только в одну сторону (по стрелке). Заднего хода тут нет. На разветвлении нужно сравнить два числа и принять решение, в какую сторону шагать дальше.

Прямоугольники - это внутренние вычисления. Трапеции (с наклоненными боковыми линиями) - это ввод/вывод, ромбики - это ветвление, тут нужно решить, вправо или влево. Кружочки (овалы) - это точки входа и завершения.

На сложных схемах полезно указывать названия отдельных линий (которые подсказывают, куда ведет линия). Если вы пользуйтесь оператором GOTO, то лучше указать на схеме названия меток (возле того места, куда прыгаем, а также на всех линиях, которые ведут туда).

Нарисовали? Теперь внимательно просмотрите и подумайте, в чем подлянка? На каком этапе программа опять повиснет? Это мысленное погружение поможет вам распутать код и найти причину неисправности. Если хотите добавить комментарий, объясняющий работу куска, или значения переменных, которые должны получиться в этом месте, не стесняйтесь, пишите это карандашем прямо туда.

Чем отличаются языки чистый Си и Си-Плюс-Плюс, это одно и то-же??

Нет, не одно и то-же.

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

73ab6ace.jpg

Бьёрн Страуструп - программист из AT&T - решил улучшить хороший язык программирования путем добавления в него бесконечного количества свистелок и перделок. В начале было неплохо, потом стало хуже.

Не, ну конечно местами плюсы удобные, например, там есть конструкторы. Вместо того, чтобы вручную выделять память, просто пишешь new Object, вызывается конструктор, который сам выделяет память для нового объекта, заполняет его, привязывает, куда надо. А при выходе из контекста автоматом вызывается деструктор, который освобождает память. Удобно.

Еще плюсы хвалят за большое количество библиотек и поддержку большего количества железа. Но у всего есть свои минусы.

C++ такой хороший, какие минусы тут могут быть??

  • Долго компилируется. Очень долго

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

  • Начинать изучение программирования с плюсов - вредно для кармы. Я гарантирую это

  • Плюсы в процессе освоения будут создавать трудности, которые вы будете героически преодолевать

  • Если вы программируете микроконтроллеры, скорее всего у вас будет мало ресурсов, плюсы могут оказаться жирноваты

  • ПЛЮСЫ ЭТО ЕРЕСЬ!!1!!!!1

Когда мне начать изучать плюсы??
Как можно позже. Поверьте мне, вы еще успеете погрузиться в эту пучину с головой, но сначала мы пройдем основы. Как вы увидите в моем самоучителе, никаких плюсов нам не понадобится.

Как выучить плюсы за 21 день??

Комикс

C_prosto.png

Почему нумерация массива начинается с нуля??

В компьютере много где нумерация начинается с нуля. Так удобнее. В случае с массивами, каким образом компьютер вычисляет адрес элемента, который нам понадобился? Берется размер элемента (он известен уже на этапе компиляции и зависит от типа), умножается на индекс массива, потом прибавляется значение указателя (который указывает на нулевой элемент). Готово, осталось только прочитать/записать/взять адрес.

Пример. После загрузки массив попал в память, на этот раз так получилось, что адрес массива == 0x004aea00.
Мы хотим прочитать нулевой элемент массива (который в самом начале). Берем размер элемента (4 байта, к примеру), умножаем на индекс [0], получаем 0x00000000, прибавляем адрес, получаем 0x004aea00, читаем четыре байта начиная с этого адреса в регистр процессора для последующей обработки.

Теперь нам понадобился элемент №100500, мы будем добираться до него, перематывая массив по одному элементу? Нет. Мы тупо умножаем 4 на 100500, получаем 0x00062250, прибавляем 0x004aea00, получаем 0x00510с50. Читаем оттуда 4 байта.

Можно ли таким способом читать несуществующие элементы??
Можно, но при этом будет читаться не массив, а то, что лежит рядом с ним (мусор). Если зайти слишком далеко, то это будет попытка чтения недоступной области, будет Segmentation Fault.

И никакой защиты нет??
Защиту программист должен придумывать сам. Тут два варианта: рядом с массивом хранить переменную, в которой хранится число элементов. Или как-то пометить последний элемент (сделать его равным нулю), но тогда, чтобы найти его, придется проверять все элементы подряд.

Указатили и массивы, они так похожи, в чем разница??

Действительно, и там и там работает адресная арифметика, и то и другое можно передать в функцию (и будет работать одинаково).

Разница в том, в каком разделе памяти эти вещи висят.

Массив - это несколько переменных одинакового типа, взятых несколько раз и поставленных в память друг за другом впритык (почти, тут есть оговорки). Массив можно завести прямо на ходу внутри функции (в стеке), можно сделать глобальный массив. Основная особенность массива - его фиксированный размер. Нужно заранее знать, сколько байт нам понадобится. sizeof() примененный к массиву скажет, сколько байт этот массив займет в памяти.

Другое дело - указатель. Он весит четыре байта (ну, или восемь, зависит от машины). Указывает куда хочет (не всегда туда, куда нужно). Можно на ходу выделить кусок памяти в куче (malloc) нужного размера и заставить указатель указывать на первый байт этого куска.

Хорошим тоном считается заставлять указатель указывать по адресу 0, если в нем нет надобности. Таким образом мы помечаем указатель как выключенный (выразимся так), чтобы случайно не обратиться к освобожденному фрагменту памяти.

sizeof() на указателе всегда вернет размер самого указателя (четыре байта).

Как же выяснить размер массива, на который указывает указатель??
Тут есть два способа: рядом с указателем держать переменную, которая хранит размер массива. Или завершающий нолик.

Я хочу передать функции указатель на кусок памяти, как функция узнает, сколько байт ей читать??
Два варианта: функция принимает два аргумента: указатель на начало куска и циферка - размер куска. Либо функция принимает только указатель и читает его, пока не встретится нулевой байт.

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

Функция printf, как ей пользоваться??

Эта функция относится к тем хитрым функциям, у которых количество аргументов может меняться. :o

В данном случае все зависит от строки формата. Строка формата - это обычная строка, которая выводится на экран почти так как есть, только включения вида %s %i приводят к тому, что функция читает свои дополнительные аргументы и вставляет их туда, например %i значит, что нужно прочитать переменную (тип int) и вывести ее в десятичной форме. %s - прочитать строку и вставить ее туда.

Функции printf, fprintf выводят текст в файловые потоки.

Функция snprintf печатает в память. Нужно указать размер целевого куска памяти, чтобы она случайно не записала лишнего. Очень полезно, когда нужно, например, склеить две строки.

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

Можно ли менять строку формата на ходу (подсунуть переменную вместо строкового литерала)??
Нежелательно. Работать будет, но компилятор не увидет ошибку, если она есть, и не предупредит. Особенно опасно, если строка пришла от пользователя, это будет большая дыра, через которую утечет адрес возврата, пароли и вообще все, что лежало в памяти.

  /* Мы прочитали строку из интернета, хотим вывести ее на экран, строка лежит в user_string */

  fprintf(stdout, user_string); /* ПЛОХО! */

  fprintf(stdout, "%s", user_string); /* Лучше. Надо не забыть символ переноса строки, если он уже там, то хорошо. */

Меняющееся количество аргументов. Это как??

Аргументы передаются через стек, верно? В каком порядке их нужно туда заталкивать? После выполнения функции кто должен вернуть указатель стека на место? Сама функция или вызывающий код?

Все эти мелочи вместе называются calling convention - соглашение о вызове функций.

В Юниксе повсеместно используется cdecl. Вызывающий код заталкивает аргументы начиная с последнего, чтобы первый аргумент попал на меньший адрес (стек растет вниз, помните?). Следом идет адрес возврата, потом уже запущенная функция может хранить в стеке, что хочет. При этом функция может читать аргументы, лежащие справа от того места, где хранится адрес возврата. Ближе всего первый аргумент, затем второй итд.

При возвращении вызывающий код ставит курсор стека назад.

Интересное свойство, если вызывающий код закинет больше аргументов, функция от этого не сломается. Можно через первый аргумент дать понять функции, сколько конкретно аргументов лежит в стеке.

Но есть ведь другие соглашения о вызове, какие там особенности??
В системе Windows популярна другая система - fastcall. Там все наоборот. Аргументы закидываются наоборот, сначала первый итд (или нет? :o). Некоторые аргументы передаются через регистры, экономится несколько циклов. Поэтому и называется fast-call. Понятное дело, количество аргументов фиксировано.

Есть еще целый зоопарк этих соглашений, все разные.

Продолжение в следующем посте.

[Статья] Самоучитель по программированию для начинающих  

  By: Lamarr on 2020-03-12 16 ч.

Re: [Статья] Самоучитель по программированию для начинающих

ББ пишет:

Вместо того, чтобы вручную выделять память, просто пишешь new Object, вызывается конструктор, который сам выделяет память для нового объекта, заполняет его, привязывает, куда надо. А при выходе из контекста автоматом вызывается деструктор, который освобождает память. Удобно.

доёбки

Это не совсем так. Конструктор не выделяет память для объекта, он вызывается после выделения памяти оператором new и предназначен для инициализации состояния и прочей хуиты. Объекты, созданные при помощи оператора new должны быть явно уничтожены оператором delete. При выходе из контекста происходит уничтожение только автоматических объектов. Собственно, это одни из тех самых многочисленных граблей плюсов.

[Статья] Самоучитель по программированию для начинающих  

  By: Slili on 2020-03-13 20 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Упс  =)

Lamarr пишет:

Мсье путает C и C++.

[Статья] Самоучитель по программированию для начинающих  

  By: ББ on 2020-03-14 15 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Доёбки - очень полезная вещь. Поправляйте меня, если что. Ок? :)

 Вложения

[Статья] Самоучитель по программированию для начинающих  

  By: ББ on 2020-03-15 07 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Продолжаем.

Получается, у каждой функции есть страница в мануале??

Да. Открываем терминал, вводим

man htonl

Если в системе установлен less, то мануальная страница откроется через него (удобно, есть поиск), иначе через more (неудобно, перематывает только в одну сторону).

Часто необходимо указать раздел, в котором ищется статья. Встроенные функции - это разделы 3 и 7, системные вызовы - 2.

man 2 open

Внизу страницы указаны ссылки на другие маны, в скобочках - номера разделов.

Так как же проверить, будет переполнение при сложении, или нет??

Если переменные unsigned (беззнаковые), то, согласно стандарту, они не переполняются, они оборачиваются по модулю (wrap-around). Поэтому алгоритм такой:

  • Складываем два числа

  • Ищем наибольшее слагаемое и проверяем, чтобы оно было меньше, чем результат

Если обе переменные знаковые, то тут надо по другому (способ с вычитанием):

if (a >= 0 && b >= 0) {
if (INT_MAX - a <= b) {
/* будет переполнение */
}
} else if (a < 0 && b < 0) {
if (a <= INT_MIN - b) {
/* опять переполнение */
}

/* Можно складывать. Если знаки разные, то переполнения не будет */
}

INT_MAX INT_MIN - это макросы, хранящие максимальное и минимальное значение для типа int. Они зависят от целевой машины. У каждого типа есть такие макросы.

Следует учитывать, что компилятор пытается оптимизировать код (всеми силами старается выкинуть проверки ради скорости), и это может привести к багам в будущем.

Статья, наполненная ужасами оптимизации.
Клирнет!! blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html

Если при сложении двух знаковых чисел будет переполнение, это и есть то самое undefined behaviour (неопределенное поведение). Помноженное на оптимизацию, оно приводит к непредсказуемым багам. Поэтому проверять надо перед сложением, а не после.


Кодировка UTF-8, что там с кириллицей??

Каждый, кто пишет программы, должен задумываться, что будет, если юзер попытается ввести кириллицу, диакритические знаки, иероглифы, хинди и проч. Из-за этого программу часто приходится дорабатывать. Сам процесс доработки программы называется интернационализация (internationalization, сокращенно i18n)

История кодировок - это длинный разговор, но в наиболее сжатой форме это выглядит примерно так.

Самая древняя кодировка символов, которую мы рассматриваем, это ASCii. Она задает латинские буквы (большие и маленькие), цифры, знаки препинания и еще специальные контрольные непечатные символы. Все это помещается в нижнюю половину таблицы ASCii (значения от нуля до 127).

Самое забавное (и приятное), что эта таблица остается нетронутой во всех остальных кодировках. Латиница читается одинаково, даже при неправильной кодировке. Это позволяет указывать кодировку документа, например, в HTML или в заголовках HTTP.

Верхняя половина таблицы использовалась для всего остального. В эпоху IBM PC там располагались символы псевдографики, которые позволяли рисовать на экране красивые рамочки.

Это файловый менеджер Norton Commander

Norton_Commander_5_51.png

Стали думать, как всунуть туда кириллицу и при этом не двигать псевдографику. Получилась кодовая страница 866

*****866.gif

С появлением Виндовса мелкомягкие решили выкинуть псевдографику.

Windows-1251 - популярная кодировка, в ней часто встречаются субтитры на торрентах и книги. Она однобайтная.

*****1251.gif

Вылезают проблемы. Как совместить, например, немецкие умлауты (две точки над ö) и кириллицу? Переключать страницы на ходу?

Эту проблему решает Юникод (unicode)

Чтобы хранить юникод, нужно выбрать способ представления (транспорт). Самый главный транспорт - это UTF-8 (Unicode Transport Format ширина слова 8 бит). UTF-8 совместим с нижней половиной ASCii - поэтому латинские буквы в юникоде, также как и в других кодировках, кодируются и выглядят одинаково.

UTF-8 очень популярен в Юниксах и Линуксах. Мои примеры кода по большей части для Линукса, благодаря UTF-8 они одинаково справляются с кириллицей и иероглифами.

Чтобы кодировать символы от 0 до 127 нужен один байт. На кириллицу понадобится по два байта на символ. Также возможны последовательности по три и четыре байта (раньше можно было еще больше, сейчас упразднили).

Количество байт в мультибайтовой последовательности можно определить по первому байту (по количеству старших единичек). У всей последовательности старшие биты всех байтов - единички.

utf8_bytes.png


Мой почтовый сервер, древний как говно мамонта, пускает только семь бит (младших) и режет все, что более 127, что делать??
Существует способ протащить юникод через системы с 7 значащими битами - UTF-7, или можно попробовать Ююк (UUencode) или base64.

У меня UTF-8 строка, в ней может вдруг встретиться нулевой байт (как часть multibyte-последовательности)??

Нет. Все байты в последовательностях имеют старший бит равный единице, то есть, они все в верхней половине ASCii таблицы. Завершающий ноль находится в нижней половине (меньше 128). То же самое про запятую, квадратные скобки, табуляцию. Эти символы находятся в нижней половине. Программа может ориентироваться по ним, например, загрузить одну строку, ориентируясь по знакам переноса строки. Наличие юникода не приведет к случайному разделению строки посередине.

Почему пример №3 некорректно отображает кириллицу??

Потому что мы разрезали multibyte-последовательность на отрывки по одному байту. Они отображаются как вопросительные знаки в квадратике.

Можно попробовать декодировать первый байт и по нему узнать количество хвостовых байт, потом вывести их вместе как один символ.

Разумеется, код работает правильно только на UTF-8, на Windows в режиме однобайтной кодировки лучше работает старый пример (без multibyte).

Пример №3а

#include <stdio.h>


int multibyte_check(unsigned int firstbyte) {
int i, result;
result = 0;
for (i = 7; i >= 0; i--) {

if (firstbyte & (1 << i)) result++;
else return result;

};
return result;
}


int main (int argc, char **argv) {

int i, ii, utf_tail;
for (i = 0; i < argc; i++) {
ii = 0;
utf_tail = 0;

while (argv[i][ii]) {
if (utf_tail == 0) utf_tail = multibyte_check(argv[i][ii]);
fprintf(stdout, "%c", argv[i][ii]);
if (utf_tail) utf_tail--;
if (utf_tail == 0) fprintf(stdout, "\n");
ii++;
};

fprintf(stdout, "\n");
};

return 0;
}

Вывод программы:

./programma3a Привет Рунион
.
/
p
r
o
g
r
a
m
m
a
3
a

П
р
и
в
е
т

Р
у
н
и
о
н

Little-endian, Big-endian, что это, как отличить, на что влияет??

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

У нас четыре байта. Нужно определить, какой из них является младшим? Тот, который имеет меньший адрес в памяти, или наоборот. Тут есть варианты:

Предположим, что у нас число 13 (0x0000000d). В нотации языка программирования младший байт расположен справа, а старший слева (big-endian - старшим концом вперед).

Как это число выглядит в памяти? Это зависит от машины, вот так это выглядит в памяти big-endian машины:

00 00 00 0d

Удобно, правда? Если смотреть отладчиком, то не надо заморачиваться и менять байты местами чтобы узнать, что за число. Big-endian машины - это процессоры Motorolla, Power-PC (старые макинтоши). В интернете все IP-адреса и порты указываются в big-endian.

На little-endian наоборот:

0d 00 00 00

Little-endian машины, это Intel, большинство Армов. Зачем так сделано? Есть свои причины. Например, указатель на int можно кастануть в указатель на char, тогда при попытке чтения прочитается младший байт. Если число маленькое, оно прочитается без проблем (по идее). То есть, не надо ничего двигать.

А вообще, это дело вкуса. Я не знаю, почему интелы решили взять little-endian, оно исторически так сложилось. Моя машина - little-endian.

Есть процессоры с переключаемым порядком байтов. Они могут и так и так.

Как отличить??
Ну, во первых, компилятор знает, для какой системы он собирает бинарник. Он пытается сообщить подробности о целевой системе с помощью макросов. В программе можно использовать условные включения, кусок кода, отвечающий за конвертирование байтов попадет в программу только если определен соответствующий макрос (понадобятся директивы #ifdef, #endif).

Во вторых, можно взять uint32_t, записать туда единицу, кастануть указатель в (char *), посмотреть, в каком байте окажется единица, в первом или в четвертом.

Как конвертировать??

__builtin_bswap32() - встроенная функция в GCC, в результате получается одна инструкция BSWAP

Если речь идет о номере порта в интернете, то понадобятся функции htonl(), htons() и другие. Они конвертируют между текущим (host) порядком и сетевым (network) порядком байт. То есть, поведение этих функций зависит от машины. Если машина изначально думает в режиме big-endian, то эти функции ничего не меняют.

Еще можно вручную поколдовать с указателями, либо побитовое AND и сдвиги.


Адресная арифметика, я не хочу юзать смещение, как подвинуть сам указатель??

Можно прибавить к указателю целое число. При этом указатель сдвигается не на количество байт, а на количество элементов. Шаг указателя зависит от типа. Инкремент/декремент тоже работает (также по шагам).

Адресная арифметика, хочется странного: как кастануть указатель в другой тип (пример кода №4)??

Мы читаем аргумент на входе, конвертируем его в int, делаем указатель (char *), указывающий на первый байт переменной, которая лежит в стеке. Выводим этот байт, и еще три байта справа от него.

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char **argv) {

if (argc < 2) {
fprintf(stdout, "Error: argument missing\n");
return 1;
};

int number = atoi(argv[1]);

unsigned char *p = (unsigned char *) &number;

fprintf(stdout, "Number = %i\nBytes: %02x %02x %02x %02x\n", number, p[0], p[1], p[2], p[3]);

fprintf(stdout, "Size of int = %i\n", sizeof(number));

return 0;
}

Вывод:

./programma4 666
Number = 666
Bytes: 9a 02 00 00
Size of int = 4

Little-endian машина (байты задом наперед), 32 бита. Все сходится.


Адресная арифметика, хочется странного: как превратить указатель в обычный int??

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char **argv) {
int number = 10;
int *pointer = &number;
unsigned int adress = (unsigned int) pointer;
fprintf(stdout, "number_value=%i adress=%ui\n", *pointer, adress);
return 0;
}

Эта программа делает переменную в стеке, берет адрес, сохраняет в указатель (pointer). Дальше мы конвертируем указатель в unsigned int (с посощью оператора приведения типа, type conversion) и печатаем получившуюся цифру.

./programma41
number_value=10 adress=3213363188

Можно и без приведения типа. Работать будет, но компилятор будет ругаться на несовместимость типов. Поставив оператор приведения типа мы говорим компилятору, что хотим именно этого.

Тип void, что это??

Особое кодовое слово, означает, что тип не определен (размер тоже).

Если мы хотим сделать свою функцию, которая не берет аргументов, то в списке аргументов можно поставить void, или просто оставить пустые скобки. Если функция ничего не возвращает, тогда нужно указать, что она возвращает void.

Хочу сделать переменную с типом void, получится??
Нет. Потому что неизвестно, сколько байт памяти выделять.

Можно ли сделать указатель на void??
Легко. Указатель будет весить четыре байта (или сколько там у вас занимает машинное слово). Его можно передавать в функции (типы кастуются автоматически).

void *pointer = NULL;

Как прочитать значение по указателю на void??
Сначала его нужно кастануть в другой тип, например в указатель на char (char *), потом читать как обычно. Тип void прочитать не получится, потому что непонятно, сколько байт читать.

Как подвинуть указатель void на четыре байта вправо??
Адресная арифметика тут не работает, потому что размер не известен. Рецепт тот же: кастануть в другой тип.

Прототипы функций, почему без них компилятор ругается??

В языке Си прежде чем использовать имя, его нужно объявить. Это имеет свои исторические причины. Ранние компиляторы были однопроходные. В качестве накопителей были большие магнитофоны с пленкой. Исходный код можно было подавать через пайп, а на пайпе нет перемотки назад.

Поэтому если компилятор видит незнакомое слово и после него нет скобок, то он ругается на неопределенное имя и не может собрать программу. Если скобки есть, то он домысливает функцию, но только возвращаемое значение он принимает за int, все аргументы тоже int, и ругается, когда обнаруживает, что это не так.

Прототип функции - это как объявление функции, только без тела (без фигурных скобок). Из него видно, какой тип возвращает функция, какие аргументы ей нужны (какого типа и в каком порядке). Еще там можно повесить кодовые слова, которые подсказывают компилятору тип соглашения о вызове, прочие мелочи.

Обычно прототипы функций складывают в заголовочные файлы. Чтобы использовать функцию atoi() нужно вставить в начале программы содержимое файла stdlib.h (треугольные скобки значат, что искать надо в папке с инклудами). Иначе компилятор будет ругаться, что не может проверить типы аргументов.


Если у меня в коде есть прототип функции, есть вызов функции, а тела функции нет, что тогда??

Компилятор объявляет функцию внешней, то есть ее имя попадет в таблицу импорта. Потом линкер будет пытаться в других модулях найти функцию с похожим именем, добавить в бинарник нужный модуль и проставить адрес на нее в таблице импорта. Если линкер ничего не находит, то начинает ругаться (undefined reference to "blablabla"). Иногда нужно указать линкеру, что мы используем дополнительные библиотеки и что функции нужно искать там.

Указатели на функции, ЗАЧЕМ??

Можно определить функцию, затем указатель на нее передать в другую функцию, чтобы та могла вызвать ее по указателю.

Указатель на функцию указывает на адрес первой инструкции этой функции. Для вызова, нужно прыгнуть по этому указателю, не забыв затолкать в стек аргументы. Компилятор проверяет, чтобы аргументы соответствовали типу указателя.

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

Такой подход используется в GTK. Мы можем рисовать на экране окошки с кнопочками. На каждую кнопку можно повесить обработчик. Мы пишем функцию, придумываем ей любое имя, но не вызываем ее сами, вместо этого мы передаем адрес этой функции в GTK, привязывая ее к кнопке. Аргументы и возвращаемое значение должны быть такими, чтобы GTK передал через них подсказки о том, какая кнопка нажата.

Такой стиль программирования называется event-driven (ориентированный на события), а наш обработчик - это call-back функция. Большую часть времени процессор проводит в функции gtk_main(), отвечая на события перерисовки окна (GTK справляется с этим сам и не беспокоит наш код). Если нажалась кнопка, он вызывает нашу функцию по указателю, потом возвращается опять вглубь.

Константы, макросы, енумы??

Есть такая вещь - магические числа (magic numbers). Это просто цифры, которые обозначают что-то, например, права доступа или режим работы функции.

Пример: функция, которая посылает сигналы на контроллер светофора на автомобильном перекрестке.

set_light(1);

Единичка, значит, красный. Двойка - красный + желтый. Тройка - зеленый.

Все работает, но есть две проблемы:

  • Если нумерация собьется, придется опять перекапывать весь код и поправлять циферки

  • Для человека, который будет исправлять этот код после вас, циферки мало что говорят, придется запоминать, что они значат, и вообще, можно же использовать константы, чтобы упростить жизнь себе и другим

#define STATE_RED 1
#define STATE_RED_YELLOW 2
#define STATE_GREEN 3
#define STATE_GREEN_BLINK 4
#define STATE_YELLOW 5
#define STATE_YELLOW_BLINK 6

set_light(STATE_RED);

Компилятор на этапе препроцессорной обработки заменит STATE_RED на 1. Так ведь удобнее, правда?

Магические цифры - это плохо. Константы - хорошо.

Компилятор может сам пронумеровать мои константы??
Да, это называется enum.

enum {STATE_RED,
STATE_RED_YELLOW,
STATE_GREEN,
STATE_GREEN_BLINK,
STATE_YELLOW,
STATE_YELLOW_BLINK};

set_light(STATE_RED);

Нумерация начнется с нуля. Можно задать индивидуальные значения, а остальные доверить компилятору, чтобы он пронумеровал их сам.

Макросы - те же константы, только с круглыми скобками. То, что в скобках, будет подставлено на нужное место. Так один макрос можно использовать в нескольких местах с разной 'начинкой'.

Многие функции - это на самом деле замаскированные макросы, которые вычисляются не на ходу программы, а на этапе сборки. Те же htonl(), htons().

А правда, что можно программным способом заставить все светофоры в городе переключиться на зеленый??

Дело в том, что самый простой и дешевый способ - это подключить огни светофора к микроконтроллеру без защитных устройств, которые помешают включить зеленый со всех сторон. Защита есть, но она чисто программная и может сломаться. Так что это вполне реально. Чтобы заставить светофор мигать, как дискотеку, достаточно перепрошить микроконтроллер (инфа не проверенная, жду опровержения с вашей стороны, хотя по логике вещей, вроде так и должно быть, дешевле ведь).

Структуры??

Удобный способ объединить переменные в группу. Переменные будут просто лежать в памяти подряд. Потом их можно будет копировать одним махом, выделять память, соединять их в цепочки.

Если у нас есть структура (сама, а не указатель на нее), то доступ к ее членам через точку.

x = my_struct.x;

Если у нас указатель, то его можно одновременно разименовать и прочитать один член структуры, вот так:

x = my_struct_pointer->x;

Можно ли хранить структуру внутри структуры??
Да, но только ту, которая была определена где-то наверху, до этого места.

А указатель на структуру??
Да, причем любой, в том числе на саму себя и те, которые будут определены позднее. Это позволяет объединить несколько структур в линкованные списки (на ходу добавлять, менять местами итд).

Размер структуры равен сумме размеров ее членов??
Почти. Компилятор пытается выровнить структуру по размеру машинного слова, поэтому он может добавлять выравнивающие байты (padding). Если сделать массив из структур, выравнивающие байты заставят структуры расположиться так, чтобы на их чтение уходило меньше циклов доступа памяти.

Можно указать опцию, чтобы компилятор упаковал структуру, убрал из нее все байты подложки. Тогда массивы будут весить меньше.

Я хочу сделать функцию, которая принимает структуру, мне нужно каждый раз указывать ключевое слово struct в типе??
Да. Или можно через typedef сделать еще один тип (без struct) и использовать его.

Булева алгебра: false + true умножить на true равно что??

true.

Булева алгебра, а также тип boolean названы в честь английского математика Джорджа Буля. Вместо чисел тут используются значения 'да', 'нет' (истина/ложь, true/false).

В языке Си нет типа boolean. Значения типа 'да' или 'нет' хранятся в машинном слове (int) в виде цифры 1 и 0. Вместо 1 можно ставить любую цифру (не нулевую).

Операторы сравнения < == > <= >= != также как и другие, занимаются тем, что вычисляют новое промежуточное значение. В данном случае результатом будет либо 1 либо 0, в зависимости от истинности.

Есть операторы булевой алгебры: || && (не путать с побитовыми операциями | &).

|| значит, булево ИЛИ. На выходе будет единица, если одно из выражений истинно.

&& значит, булево И. На выходе единица, если оба выражения истинны.

Эти операторы можно объединять в цепочки, главное не смешивать их типы (можно использовать круглые скобки для задания приоритета).

Если булевы операторы совмещаются со операторами сравнения, то у булевых операторов выше приоритет. Нужно поставить скобки, иначе будут сравниваться не числа, а результаты булевых операций.

Для любопытных: оператор || соответствует сложению в булевой математике (если оба операнда - ноли, то на выходе ноль). Оператор && соответствует умножению в булевой математике (если один из операндов - ноль, на выходе тоже ноль).

Тут нужно сделать сравнение. Язык Си выдает либо единицу, либо ноль. Javascript поступает хитрее. При операторе || он выдает не единицу, а значение левого операнда, если он true, иначе на выход идет второй операнд, и вызывающий код разбирается, true он или false.



Чудеса с булевой алгеброй: как запустить то, а потом, если то не сработает, то запустить это (или наоборот, если то не сработало, то это не запустится)??

В булевых выражениях могут использоваться функции. Причем, если значение не понадобилось, то функция не срабатывает.

func_one() && func_two();

Если func_one() вернет ноль, то func_two() вообще не запустится.

func_one() || func_two();

Наоборот, если func_one() вернет ноль, тогда следом сработает func_two().

Проверяем указатель. Если он не нулевой, то оставляем как есть, иначе выделяем для него память (потом нужно будет сразу же проверить, выделилась память, или нет).

pointer || (pointer = malloc(sizeof(*pointer)));

На имидж-бордах часто можно встретить фразу 'TITS || GTFO', она применяется, если участник заявил, что он является девушкой (в надежде, что ему перестанут грубить), и сообщество хочет, чтобы он доказал это с помощью фотографии. В противном случае он должен покинуть имидж-борду.

Одно издательство решило выпустить книгу, наполненную примерами Proof-of-Concept из одного хакерского журнала, и ее название, как не трудно догадаться, PoC || GTFO

Это не шутка.

poc_or_gtfo.png

Как проверить, что указатель не нулевой (простой способ)??

Просто поставить его в оператор if вместо условия. Если он равен нулю, это считается как false, иначе true. Восклицательный знак перед именем меняет логику на обратную.

struct entry *e = malloc(sizeof(struct entry));
if (!e) {
perror("malloc() fail");
exit(1);
};

Выделяем память на ходу, достаточную для того, чтобы туда влезла структура типа entry. Записываем адрес на начало куска в указатель 'e', проверяем, нулевой он, или нет.

Битовые операции: как установить определенные биты в единицы (и наоборот)??

Процессор умеет не только складывать числа в сумматоре (о том, как устроен сумматор, в следующей части). Он также может производить побитовые операции AND, OR, XOR, а также NOT. Причем на всех битах машинного слова одновременно.

Символы: & | ^ ~ (не путать с булевыми выражениями && ||)

  • Самое простое - это NOT (инвертирование всех битов, другое название - дополнение до единицы, ones' complement)

  • AND значит, что на выходе бит будет содержать единичку, если в соответствующих битах обоих входных слов лежат единички

  • OR - то же самое, только функция ИЛИ

  • XOR - взаимоисключающее ИЛИ

Прежде чем менять биты, нужно приготовить маску. Маска - это переменная такого же размера, в которой единичками отмечены биты, которые нам нужны.

x = x | mask;

Ставим нужные биты в единички, остальные не трогаем. Сокращенная запись: x |= mask;

Вместо mask можно поставить 0b00000100 или любое другое значение. Можно взять единичку и сдвигать ее влево нужное количество раз, чтобы поставить нужный бит, нумерация битов начинается с нуля (начало с младшего).

Установить бит №n в единицу:

x = x | (1 << n);

Взаимоисключающее ИЛИ нужно для того, чтобы переключить бит в другое значение.

x = x ^ mask; /* XOR */

Операция И нужна, чтобы оставить нужные биты как есть, остальные выключить. Здесь перед mask стоит знак инвертирования битов, чтобы приготовить маску наоборот.

x = x & ~mask; /* AND */

Некоторые функции принимают так называемые 'флаги' - переменную, в которой отдельные биты выставлены в единицы. Можно комбинировать маски с помощью оператора ИЛИ, каждая маска содержит один бит, выставлнный в нужном месте.

int fd = open("/home/bb_runion/filename.txt", O_CLOEXEC | O_RDONLY);

Можно проверить, установлен ли конкретный бит.

if (x & (1 << n)) {
/* bit n is set */
} else {
/* bit n is clear */
};

Можно проверить, четное число или нет, проверив младший бит. Для отрицательных значений тоже сойдет.

if (x & 1) {
/* нечетное */
} else {
/* четное */
};

Правда, что если стоит битовая операция в другом типе (не int), то компилятор конвертирует ее в тип int??
Да, по стандарту это так. Нужно вручную кастовать тип, чтобы уговорить компилятор не делать этого.

Carret return, потом line feed? Или наоборот? Как правильно??

Без путаницы. Line feed - это символ с кодом 0x0a, escape-последовательность '\n'. Carriage return - это символ с кодом 0x0d, последовательность '\r'.

В юниксе принято использовать один байт \n (0x0a) для обозначения конца строки.

В Windows и в DOS принята последовательность из двух байтов "\r\n" (0x0d 0x0a). То же самое в интернет-протоколах (HTTP).

При открытии файла нужно указать режим: текстовый или бинарный. Текстовый значит, что функции чтения будут менять поток на лету, заменять нативную последовательность на один символ \n (и обратно). Бинарный режим пишет, как есть, без изменений.


Практика!!

Читаем stdin, снабжаем его номерами строк, выводим в stdout. Пример кода №5.

#include <stdio.h>

int main (int argc, char **argv) {

char buf [9000]; /* объявили массив прямо в стеке. размер фиксированный */
long int number = 1;
char *ret;

while (1) {
ret = fgets(buf, sizeof(buf), stdin);
if (!ret) break;

fprintf(stdout, "Line %li: %s", number++, buf);
};

fprintf(stdout, "\n");
return 0;
}
Вывод
gcc -Wall linenum.c -o linenum
cat piece_of_text.txt | ./linenum
Line 1: Всю первую полосу занимала сентиментальная история о математике, который влюбился в цифровую машину. Пока дело ограничивалось таблицей умножения, он еще как-то владел собой, но, когда пошли нелинейные уравнения энной степени, принялся страстно обнимать ее клавиши, восклицая: «Дорогая! Я с тобой никогда не расстанусь!» и т. п. Удрученный этой безвкусицей, я заглянул в светскую хронику, но там только уныло перечислялось, кто, с кем и когда сконструировал потомство. Литературную колонку открывало стихотворение, которое начиналось так:
Line 2:
Line 3: Жил робот в Фуле дальней,
Line 4: И тумблер золотой
Line 5: Хранил он — дар прощальный
Line 6: Ах! робушки одной.
Line 7:
Line 8: Это удивительно напомнило мне какие-то известные стихи, но я не мог вспомнить автора. Еще там были сомнительного свойства анекдоты о людях, о роботах-людолизах, о происхождении людей от пещерных ублюдков и тому подобная чушь. Ехать оставалось еще полчаса, и я принялся изучать мелкие объявления: как известно, и в дрянной газетенке они бывают подчас весьма интересны. Однако и здесь меня ожидало разочарование. Один предлагал уступить сервобрата, другой учил космонавтике по переписке, третий брался разбить атомное ядро в присутствии заказчика.

Цветной вывод. В поток можно подмешать управляющие ANSI последовательности, которые влияют на цвет текста.

fprintf(stdout, "\x1b[31;40mRed Text\x1b[0m\n");

Попробуем вывести все цвета сразу, пример кода №6:

#include <stdio.h>

void output_color_text (char *str) {
fprintf(stdout, "\x1b[%sm %s ", str, str);
}

int main (int argc, char **argv) {

fprintf(stdout, "\n");

char buf [20];
int i, ii;

for (i = 0; i < 8; i++) {
for (ii = 0; ii < 8; ii++) {
snprintf(buf, sizeof(buf), "3%i;4%i", ii, i);
output_color_text(buf);
};
fprintf(stdout, "\x1b[0m\n");
};
for (i = 0; i < 8; i++) {
for (ii = 0; ii < 8; ii++) {
snprintf(buf, sizeof(buf), "9%i;4%i", ii, i);
output_color_text(buf);
};
fprintf(stdout, "\x1b[0m\n");
};
for (i = 0; i < 8; i++) {
for (ii = 0; ii < 8; ii++) {
snprintf(buf, sizeof(buf), "3%i;10%i", ii, i);
output_color_text(buf);
};
fprintf(stdout, "\x1b[0m\n");
};
for (i = 0; i < 8; i++) {
for (ii = 0; ii < 8; ii++) {
snprintf(buf, sizeof(buf), "9%i;10%i", ii, i);
output_color_text(buf);
};
fprintf(stdout, "\x1b[0m\n");
};

fprintf(stdout, "\x1b[0m\n\n");
return 0;
}

colors.png

[Статья] Самоучитель по программированию для начинающих  

  By: ББ on 2020-03-15 11 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Это еще не всё, будет продолжение :)

 Вложения

[Статья] Самоучитель по программированию для начинающих  

  By: Серый Филин on 2020-03-15 13 ч.

Re: [Статья] Самоучитель по программированию для начинающих

Офигеть, круто.

ББ:up:
Буду ждать продолжения!


Мама, не выключай мне интернет!

 Вложения