Здравствуйте, дорогие друзья! Мы начинаем курс по языку программирования Си. На предыдущем занятии мы с вами увидели принцип обработки команд процессором, когда в памяти содержалась последовательность чисел:
B8 22 11 00 FF 01 CA 31 F6 53 8B 5C 24 04 8D 34 48 39 C3 72 EB C3
Как видите, язык Ассемблер дает заметное удобство при создании программ. Теперь программисту достаточно запомнить общие команды, вроде movl (переместить из памяти или регистра информацию в память или регистр); addl (сложить содержимое регистров или памяти); xorl (побитовая операция XOR) и т.д. Запоминать числовое (кодовое) представление команд процессора уже не нужно. Достаточно написать программу на уровне мнемоник, а затем, с помощью компилятора языка Ассемблер, перевести ее в числовой вид (машинные коды) понятные центральному процессору компьютера.
Обратите внимание, я здесь употребил слово «компилятор». В программировании под ним понимается специальная исполняемая программа, которая переводит текст программы, написанной программистом на каком-либо языке программирования, как правило, в машинные коды. Именно в этом смысле я буду использовать слово «компилятор».
Разбор «Hello, World!» на Си
Возвращаясь к фрагменту программы на Ассемблере, мы видим в нем первую ступеньку упрощения описания логики задачи, по сравнению с уровнем машинных кодов. Однако уровень Ассемблера все равно остается достаточно низким. При создании более-менее состоятельных программ, трудоемкость работы программиста остается очень высокой, а значит, высока и стоимость разработки конечного продукта. Кроме того, система команд (мнемоник) языка Ассемблер может несколько меняться при переходе от одной архитектуры процессора к другой, так как у каждого типа процессоров может быть свой набор команд, своя разрядность, свои особенности работы. Поэтому перенос программы с одного компьютера на другой может вызывать большие трудности.
Для преодоления этих и некоторых других недостатков стали создавать языки, на которых можно описывать логику программы на более высоком (абстрактном) уровне. В частности, на прошлом занятии мы увидели, что столкнувшись с проблемой переноса ОС с компьютера PDP-7 на компьютер PDP-11, Кен Томпсон решил воспользоваться языком высокого уровня.
Сначала это был язык B, но из-за его ограниченности в 1972 году Деннис Ритчи создал другой новый, на тот момент, язык Си. Время показало, что язык Си хорошо сочетает более высокий уровень программирования, по сравнению с языком Ассемблер, а программы, написанные на Си, качественно и эффективно можно перевести на уровень машинных кодов с помощью компилятора языка Си.
То есть, конечный результат получается таким, словно программу изначально написал профессиональный программист на Ассемблере, а не на языке высокого уровня Си. Это одно из ключевых преимуществ языка Си перед другими языками высокого уровня. В результате, грамотно написанная программа, будет выполняться на компьютере с максимально возможной скоростью и разумно, без излишеств использовать его ресурсы. Это свойство языка Си незаменимо в ряде направлений, например: игровой индустрии (разработка движков); дополненной реальности; обучение нейронных сетей; создание надежных программ управления оборудованием в реальном режиме времени и многое другое. Именно по этим причинам язык Си остается востребованным и будет таковым до тех пор, пока программы пишет человек привычным для нас сейчас способом.
Как работает C/C++?
Этапы перевода (трансляции) текстов программы в исполняемый код
Итак, все, что требуется от программиста – это создать один или несколько файлов с текстами программы, которая решает поставленные задачи. Каждый такой независимый файл в программировании часто называют модулем. Так как мы говорим о языке Си, то все эти текстовые варианты программ далее переводятся (транслируются) на уровень машинных кодов. Как это происходит?
Сначала каждый модуль независимо пропускается через тестовый препроцессор. Его задача в программе найти все, так называемые, директивы (указания) для этого препроцессора и выполнить их. Что это за директивы, мы с вами еще будем говорить. В результате, исходный текст программы несколько меняется.
После этого, преобразованные тексты подаются на компилятор (также независимо друг от друга), которые сначала проходят через лексический анализ программы. На этом этапе выделяются возможные синтаксические ошибки. Если ошибок не обнаружено, то программа далее переводится непосредственно в машинные коды. На выходе получаются объектные файлы модулей.
Но в этих объектных файлах отсутствуют связи с другими модулями (если они были прописаны в тексте программы), реализации библиотечных функций (если они были использованы), код запуска всей программы. Все это делает на последнем этапе линкер (по-простому, редактор связей). Он связывает все объектные файлы модулей в единый исполняемый файл, добавляет в него необходимые реализации библиотечных функций и код запуска для текущей операционной системы. На выходе получается окончательный результат в виде исполняемого файла.

Почему все сделано именно так? Конечно, для полного ответа на этот вопрос, его нужно задать разработчикам языка Си, но некоторые моменты вполне логичны и понятны. Первое. Независимая компиляция каждого модуля позволяет сократить время перевода большого проекта в машинный код, состоящего из десятков, а то и сотен текстовых файлов.
Первый раз, конечно, придется скомпилировать все файлы и сформировать объектные файлы каждого модуля. Но, при последующем изменении программы в отдельных модулях, достаточно будет перекомпилировать только их, а потом собрать проект с помощью линкера (линковщика, редактора связей), используя ранее созданные объектные файлы других не измененных модулей. В целом, такой подход заметно экономит время при изменении и отладке проекта.
Второе. Линкер добавляет в итоговые исполняемые файлы только те реализации библиотечных функций, которые используются в программе. Ничего дополнительного, лишнего на выходе не образуется.
Третье. Использование компиляторов для разных архитектур процессоров и разных ОС позволяет относительно просто и быстро переносить ранее написанную программу с одной ОС на другую, или с одной архитектуры процессора на другую. И, так как компиляторы языка Си реализованы практически везде, на всех платформах, то программа на языке Си оказывается самой переносимой среди многих других языков высокого уровня.
Стандарты языка Си
Конечно, компиляторы языка Си пишутся разными людьми в разных фирмах. А, значит, они и работают с некоторыми отличиями. В то же время, программист на Си не должен задумываться об особенностях этих компиляторов и программы, написанные разными программистами, должны корректно переводиться (транслироваться) в машинный код разными компиляторами. Добиться этого можно только стандартизацией. Программисты по всему миру должны договориться между собой о синтаксисе и наборе команд, используемых в языке Си.
Первым таким неформальным стандартом стала книга Брайана Кернигана и Денниса Ритчи «Язык программирования Си». На первых этапах становления языка Си этого было достаточно.
Стандарт ANSI C (ISO C)
Но по мере того, как все больше и больше программистов по всему миру стали использовать этот язык программирования, остро встал вопрос создания вполне официального стандарта, который бы, к тому же, включал все полезные новшества этого нового языка. С этой целью в 1983 году национальный институт стандартизации США – ANSI (American National Standards Institute) образовал специальную рабочую группу (комитет) под названием X3J11. И в 1989 году миру был предъявлен новый стандарт, который сейчас часто называют ANSI C или C89. Правда, официально он был принят только в 1990 году, поэтому вместо C89 иногда используют запись C90. Но это означает, по сути, одно и то же.
- доверять программисту;
- не мешать программисту делать то, что он считает необходимым;
- без необходимости не усложнять язык, сохранять его простоту;
- каждая операция языка должна иметь только один способ выполнения;
- операция должна выполняться максимально быстро, даже в ущерб переносимости языка.
Стандарт C99
- в интернационализации (поддержке различных международных языков) на программном уровне;
- в устранении некоторых неточностей предыдущей версии языка стандарта ANSI C;
- в повышении стабильности математических вычислений для возможности безопасного использования языка в научных проектах.
Другие стандарты
Язык Си стал хорошей базой для развития и создания нового подобного и во многом похожего на него языка программирования C++. Его автором считается Бьёрн Страуструп – сотрудник Bell Laboratories. В этом языке появилась объектно-ориентированная составляющая, шаблоны классов и функций и некоторые другие полезные инструменты для современной разработки программ любого уровня сложности.
Новые стандарты языка Си, относятся уже к языку С++. Например, в 2011-м году был принят стандарт известный, как C11. Но бурная деятельность различных комитетов стандартизации на это не закончилась и они выпустили еще стандарты в 2014-м, 2017-м, 2020-м годах.
Как вы догадываетесь, этот безудержный процесс, скорее всего, будет продолжаться, пока существует язык Си и его развитие в виде языка С++. Но я остановлюсь на стандарте C99, принятом в 1999-м году. На мой взгляд, он удачно сочетает возможности и красоту языка Си, а также сохраняет его дух, заложенный создателем Деннисом Ритчи в далеком 1972 году.
Источник: proproprogs.ru
Этапы трансляции программы.
Превращение текста на языке высокого уровня в машинный код проходит в несколько этапов:
На первом этапе происходит препроцессорная обработка текста.
На втором этапе создается промежуточный (объектный) файл.
На третьем этапе несколько объектных файлов компонуются в единый исполняемый файл, который может быть загружен в память компьютера и выполнен.
Библиотечные файлы хранятся в объектном виде и присоединяются к программе пользователя на этапе компоновки. Ход трансляции приведен на рис. 1.5.
После того, как программа оттранслирована, её можно выполнить, для чего используется специальная программа, называемая загрузчиком.
Структура программы на языке Си
Любая программа на языке Си состоит из одной или более функций.
Одна из этих функций (главная) должна иметь имя main() Отличительным признаком функции служат круглые скобки, а аргумент может и отсутствовать. Тело функции заключено в фигурные скобки и представляет собой набор операторов, каждый из которых оканчивается символом «точка с запятой».
При запуске программы пользователя, операционная система передает управление на функцию main() и тем самым начинается выполнение программы. От других функций существующих в программе функция main() отличается тем, что её нельзя вызвать изнутри программы, а ее параметры, если они есть, задаются операционной системой. Обычно, хотя это и не обязательно, main() бывает первой функцией программы.

Рис. 1.5. Этапы трансляции текста программы.
Область директив препроцессора находится перед функцией main()
/* Простейшая программа, выводящая приветствие на экран дисплея */
#include stdio.h> // директива препроцессора
void main()
printf(«Hello, worldn»);
/* Простейшая программа, выводящая приветствие на экран дисплея */
являются комментарием, который в данном случае кратко объясняет, что делает программа. Все символы, помещенные между /* и */, игнорируются компилятором, и этим можно свободно пользоваться, чтобы сделать программу более понятной.
Инструкция printf(«Hello, worldn»); — это вызов функции printf, которая выполняет печать своего первого аргумента — текстовой строки. Функция printf находится в стандартной библиотеке stdio.h
Результатом работы программа явится сообщение :
Hello, world
Рассмотрим подробнее функцию printf(), ей можно передать любое количество параметров, причем первый параметр обязательно должен быть текстовой строкой, описывающей формат вывода. При вызове функция печатает строку, стоящую первой. Если в этой строке встречаются специальные комбинации символов, начинающиеся с символа % , функция подставляет вместо них значения последующих параметров.
Приведем наиболее часто используемые комбинации:
%s — печать текстовой строки
%c — печать отдельного символа
%d, %i — печать целого числа
%f, %e, %l — печать вещественного числа
Например, запишем инструкцию, которая выводит в одной строке значения переменных a, b и с целого типа (int), в качестве разделителя между переменными будем использовать знак «:» printf(» %d:%d:%d»,a,c,b);
Этот оператор имеет 4 параметра, разделенных запятыми, первый определяет формат (форму) и типы данных, выводимых на экран, 2,3 и 4 параметры – имена переменных.
если значения переменных a,b и c соответственно равны 5,10,25, то результат на экране дисплея выглядит следующим образом:
Функция printf() позволяет не только выводить любые данные (как числовые так и текстовые), но и форматировать их, например, снабжать числовую информацию текстовыми комментариями, переводить строки, делать отступы и тому подобное.
Например, предыдущий пример можно отформатировать следующим образом:
printf(«na= %dtb= %dtc= %d»,a,b,c);
Результат на экране дисплея выглядит следующим образом:
a=5 b=10 c=25
Функция printf() «перевела строку» (символ n), перед выводом числа поставила комментарий «а=», после каждого числа вывела знак «табуляции» (символ t).
Логические операции и операции отношения.
Наряду с арифметическими операциями, которые используются для всевозможных вычислений, в языках программирования есть и логические операции, которые используются для проверки условий. Логические операции иногда называют операциями отношения, значение переменной или константы они сравнивают с литералом, или со значением другой переменной или константы.
Результат сравнения имеет логический тип (bool) – TRUE (истина или 1) либо FALSE (ложь или 0).
Рассмотрим подробнее операции отношения, к ним относятся:
>= больше или равно;
= = равно (проверка на равенство)
Операции отношения по рангу младше арифметических операций, так что выражения типа :
Приведем примеры проверки простых логических условий :
if (i>7) результат 1(да) если i больше 7, и 0 (нет) – в противном случае
if (i==j) результат 1 если i равно j
if (x+1 != k) результат 1 если x+1 не равно k
Чаще всего ошибки совершают при проверке на равенство, обратите внимание, что в этом случае необходимо ставить два знака «=»,
выражение подобное if (i=j) неверно!
но компилятор подобные ошибки не диагностирует , так как интерпретирует данное выражение следующим образом : if ((i=j)!=0), то есть сначала заносит значение j в переменную i, а затем сравнивает результат с нулем.
Если нам необходимо проверить сложное условие, то есть объединить несколько простых логических выражений в единое сложное выражение, то понадобятся логические связки, так называемые логические операции.
К логическим операциям относятся:
|| логическое ИЛИ (дизъюнкция), бинарная операция.
! логическое НЕ (отрицание), унарная операция;
Так же как и у операций отношения, у логических операций результат логический (бинарный), либо 1 (TRUE) либо 0 (FALSE).
Примеры применения логических связок (сложных условий) :
Чтобы записать логическое условие, соответствующее следующей математической записи aib, потребуется логическая связка :
if ( i>a i b)
Чтобы записать логическое условие, соответствующее следующей математической записи a>i>b, потребуется логическая связка || :
Источник: studfile.net
Фазы трансляции
Программы на языках C и C++ состоят из одного или нескольких исходных файлов, каждый из которых содержит часть текста программы. Исходный файл, а также его включаемые файлы, файлы, включенные с помощью #include директивы препроцессора, но не включающие разделы кода, удаленные директивами условной компиляции, такими как #if , называются блоками преобразования.
Исходные файлы можно переводить в разное время. На самом деле, распространено преобразование только неактуальных файлов. Преобразованные записи преобразования можно обработать в отдельные файлы объектов или библиотеки объектного кода. Затем эти отдельные преобразованные записи преобразования объединяются для создания исполняемой программы или библиотеки динамической компоновки (DLL). Дополнительные сведения о файлах, которые могут использоваться в качестве входных данных для компоновщика, см. в разделе Link input Files.
Записи преобразования могут взаимодействовать следующим образом.
- Вызовы функций с внешней компоновкой.
- Вызовы функций-членов класса с внешней компоновкой.
- Прямое изменение объектов с внешней компоновкой.
- Прямое изменение файлов.
- Межпроцессное взаимодействие (только для приложений на базе Microsoft Windows).
В следующем списке описываются этапы преобразования файлов компилятором.
Сопоставление символов
Символы в исходном файле сопоставляются с внутренним представлением источника. На этом этапе последовательности триграфа преобразуются в односимвольное внутреннее представление.
Соединение строк
Все строки, заканчивающиеся обратной косой чертой ( ), за которой следует символ новой строки, соединяются со следующей строкой исходного файла, образуя логические строки из физических строк. Если он не пуст, то исходный файл должен заканчиваться символом новой строки, которому не предшествует обратная косая черта.
Выделение лексем
Исходный файл разделен на токены предварительной обработки и символы пробела. Каждый комментарий в исходном файле заменяется одним пробелом. Символы новой строки сохраняются.
Предварительной обработки
Выполняются директивы предварительной обработки, и макросы разворачиваются в исходный файл. Оператор #include вызывает преобразование любого включенного текста, начинающееся с указанных выше трех шагов преобразования.
Сопоставление кодировки
Все члены и escape-последовательности в исходной кодировке преобразуются в эквивалентные значения в кодировке выполнения. В Microsoft C и C++ исходная кодировка и кодировка выполнения являются кодировками ASCII.
Объединение строк
Все смежные строковые и расширенные строковые литералы объединяются. Например, «String » «concatenation» преобразуется в «String concatenation» .
Перевод
Все токены анализируются синтаксически и семантически; эти токены преобразовываются в объектный код.
Компоновка
Все внешние ссылки можно использовать для создания исполняемой программы или библиотеки динамической компоновки.
Компилятор выдает предупреждающие сообщения или сообщения об ошибках во время преобразования, если обнаруживает синтаксические ошибки.
Компоновщик разрешает все внешние ссылки и создает исполняемую программу или DLL, объединяя одну или несколько отдельных обработанных записей преобразования со стандартными библиотеками.
Источник: learn.microsoft.com