Структура и понимание работы программы "Hello, World!"

Практический курс по C/C++: https://stepik.org/course/193691

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

Часто в учебниках или на лекциях для этой цели приводят программу, печатающую на экране строку «Hello, World!». Я не стану нарушать эту традицию и поступлю ровно также, тем более, что пример достаточно удачный.

/* Моя первая программа */
#include <stdio.h>
 
int main(void)
{
    printf("Hello, World\n");
    return 0;
}

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

Итак, у нас имеется простейшая программа. Давайте теперь построчно разберем, что здесь к чему. В первой строчке записан комментарий. Обычно, это некий текст, который пишется для программистов с разъяснением работы отдельных операторов или фрагментов кода. Компилятор игнорирует все комментарии и не переводит их в машинный код.

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

/* Это
многострочный
комментарий
*/

С появлением стандарта C99 добавились однострочные комментарии, обозначаемые символами //. Например, комментарий в нашей программе можно оформить и так:

// Моя первая программа

Но, обычно, однострочные комментарии используют для разъяснения отдельных операторов программы, например:

printf("Hello, World\n");   // вывод строки на экран

Следующая строчка программы – это, так называемая, директива препроцессора. Подробно о них мы еще будем говорить. Здесь отмечу только, что все директивы начинаются с символа шарп ‘#’ и следом идет ее имя. В частности, директива #include выполняет включение содержимого заголовочного файла stdio.h в то место, где записана эта директива. Сразу возникает вопрос, зачем это потребовалось в программе? Дело в том, что база языка Си весьма ограничена. Изначально в нем не реализовано даже функций ввода/вывода. Поэтому для вывода какой-либо информации на экран нам необходимо воспользоваться внешней библиотечной функцией printf(), которая записана ниже в программе. Так вот, в текстовом файле stdio.h, в частности, прописано объявление функции printf(). Обратите внимание, объявление функции, но не ее реализация. Компилятор языка Си так устроен, что для формирования объектного файла текущего модуля ему достаточно дать определение (объявление) функции printf(), а конкретная реализация добавится в программу на этапе линковки кода, то есть, редактором связей. Эта реализация будет взята из библиотечного откомпилированного модуля, поставляемого вместе с компилятором языка Си. Поэтому, все что нужно будет сделать линковщику – это найти фрагмент машинного кода для функции printf() и добавить в исполняемый файл. Или же, при использовании динамических библиотек (DLL), реализация многих стандартных функций не добавляется в скомпилированный файл, а берется из соответствующей динамической библиотеки, хранимой в ОС. Но на данном этапе, для нас важно лишь одно: для успешного транслирования программы в машинный код компилятору достаточно дать лишь описание используемых функций, а их реализации добавит позже редактор связей. Вот для этого и нужна в программе директива #include с подключением файла stdio.h, имя которого является сокращением от английских слов:

standard input ouput

(стандартный ввод/вывод). А расширение h – это первая буква от слова header (заголовок). Получается заголовочный файл стандартного ввода/вывода.

В следующей строчке программы идет объявление функции с именем main. Опять же, зачем это понадобилось? Забегая вперед, отмечу, что функции – это активные элементы программы, которые выполняют, заключенные в них операторы (инструкции). Вся программа на языке Си – это, по сути, наборы разных функций и их вызов в запрограммированном порядке. Так вот, первая функция, с которой начинается выполнение программы, должна называться main. Почему именно main? Потому, что так решил разработчик этого языка Деннис Ритчи в далеком 1972 году. Все компиляторы языка Си формируют машинный код так, что после загрузки программы в память компьютера, ОС передает управление сначала, так называемой, секции кода, как правило, с переходом по метке _start. Там выполняются некоторые подготовительные действия, а затем, вызывается функция main(). И это всегда так. Мало того, если в программе будет отсутствовать такая функция, то при компиляции появится сообщение об ошибке.

Итак, в программе на языке Си обязательно должна присутствовать функция с именем main, которой передается управление после запуска программы и выполнения некоторых подготовительных действий. Условно этот процесс показан на рисунке.

Далее, перед именем этой функции записано ключевое слово int. Оно означает целочисленный тип данных и то, что функция main() возвращает целочисленное значение. Обо всем этом мы еще подробно будем говорить, здесь я лишь даю краткие пояснения. После имени main должны идти круглые скобки и в них прописаны параметры функции. Так как в нашем примере функция main() не имеет параметров, то в круглых скобках указано ключевое слово void, которое можно перевести с английского как «пустой». Далее, в фигурных операторных скобках прописываются операторы, которые будут последовательно выполняться (сверху-вниз) при вызове функции main(). В частности, в нашей программе, сначала будет вызвана библиотечная функция printf() для вывода указанной строки в консоль, а затем, сработает оператор:

return 0;

Опять же, забегая вперед, оператор return выполняет два действия. Первое, он завершает текущий вызов функции main(). И второе, в нем указывается значение, которое будет возвращено этой функцией. Почему здесь прописано значение 0? Дело в том, что это значение, затем, через системный вызов _exit будет передано операционной системе. Собственно, этот системный вызов и завершает программу, а вовсе не функция main(). Так вот, значение 0 говорит ОС о том, что программа выполнила свою функцию и успешно решила поставленную перед ней задачу. Все другие значения, отличные от нуля, будут означать ошибки выполнения программы. Например, мы что то планируем записать в файл, а ОС не позволяет открыть указанный файл на запись. Тогда программа может выдать сообщение об ошибке и вернуть код, отличный от нуля. Хотя, в современной практике программирования, обычно, так не поступают и просто выводят сообщение об ошибке с кодом завершения 0.

И пару слов о функции printf(), которую, конечно же, мы позже будем рассматривать подробнее. Название функции – это сокращение от английских слов:

print formatted (форматированный вывод)

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

Наконец, обратите внимание на символ ‘\n’ в конце строки. Так прописываются спецсимволы, т.е. ставится слеш и, затем символ или код символа. В частности, ‘\n’ означает перевод курсора на новую строку. То есть, если бы мы следом еще раз вызвали функцию printf() и напечатали какую-либо информацию, то она была бы отображена на следующей строчке.

Вот, в целом, общее описание нашей простой программы, которая выводит на экран сообщение «Hello, World!». Возможно, некоторых из вас может поразить объем информации, необходимой для понимания даже самых простых программ на Си. Да, это действительно так. Но это не удивительно. Если вспомнить историю появления языка Си, то он был разработан программистами для программистов, как заменитель языка Ассемблер. А не для того, чтобы его можно было последовательно изучать конструкция за конструкцией, как, например, язык Python. Хотя, и в языке Си нет ничего особенно сложного. Основная трудность – это ответственный и профессиональный подход к написанию программ, т.к. язык дает огромную свободу действий. А свобода подразумевает не меньшую ответственность. Именно ответственность за грамотное использование ресурсов лежит на плечах программиста языка Си. Но это вознаграждается возможностями, которые перед нами открываются. Именно поэтому язык Си остается незаменимым в ряде направлений, о которых я вам уже рассказывал на предыдущих занятиях.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме