Поддержать проект — Получить Больше

Программирование на языке C с нуля

Начните программировать сегодня!

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

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

Как говорила моя преподавательница по промышленной электронике: Набивайте руки...

Для поиска любого текста в Учебнике, нажмите комбинацию горячих клавиш Ctrl+F, и вверху над страницей откроется поле ввода текста для поиска, введите текст и нажмите ENTER:

Находите любой текст в Учебнике. С помощью комбинаций горячих клавиш Ctrl+G перейдите к следующему результату поиска, а с помощью Ctrl+Shift+G к предыдущему результату поиска. Или просто нажмите курсором на соответствующие кнопки (стрелка вверх и стрелка вниз) возле поля ввода текста.

Воспользуйтесь фильтром (если он есть в вашем обозревателе) для более точного поиска текста:

Уточните поиск с помощью фильтра.

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

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

В учебнике с помощью примеров программ и иллюстраций, показывающих результаты работы исходного кода, разбираются все ключевые аспекты языка С - от базовых до сложных. Описано как установить Компилятор и Редактор Кода для языка С и работать в нём.

Учебник подходит для:


Учебник обновляется и дополняется.

Заметили ошибку - напишите о ней автору alexanderksua@gmail.com


Компилятор https://www.mingw-w64.org/

Операционная система Windows 11 x86_32/64

Редактор Кода Visual Studio Code


Основа основ

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

Введение

C — это компактный компьютерный язык программирования общего назначения, часто называемый процедурным, созданный Деннисом Макалистером Ритчи (Dennis MacAlistair Ritchie) для операционной системы Unix на компьютере Digital Equipment Corporartion PDP-11 в 1972-м году.

Dennis MacAlistair Ritchie
Де́ннис Макалистэйр Ри́тчи (Dennis MacAlistair Ritchie). Родился 9 сентября 1941, Бронксвилл, штат Нью-Йорк, США. Американский программист, информатик и компьютерный специалист, известен по участию в создании языков программирования BCPL, B, C, расширения ALTRAN для языка программирования FORTRAN, участию в разработке операционных систем Multics и Unix. Умер предположительно 8-12 октября 2011, Беркли-Хайтс, США.

Этот новый язык программирования был назван C (читается по русски как "си") в честь его предшественника — языка В(читается по русски как "би"), представленного в 1970-м году.

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

Гибкость и переносимость языка C сделали его очень популярным, этот язык был формализован Национальным Американским Институтом Стандартизации (American National Standards Institute, ANSI). Стандарт ANSI однозначно определил все аспекты языка, избавив нас от сомнений о его точном синтаксисе.

ANSI C стал узнаваемым стандартом языка C. В этой книге рассматривается и описывается именно он.

Зачем изучать сейчас именно C ?

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

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

Зачем изучать программирование с помощью языка C?

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

Язык C существует уже долгое время, он застал создание новых языков программирования вроде Java, C++ и C#. Многие из них основаны на языке C, по крайней мере, отчасти, и при этом более громоздкие. C, как более компактный язык, лучше подходит для того, чтобы начать программировать, поскольку изучить его проще.

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

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

На С можно писать быстрые графические приложения, обработку многоканального аудио и работу с цифровыми музыкальными инструментами с помощью таких библиотек как SDL, SFML, RayLib и др. На современных компьютерах возможно генерировать тысячи кадров в секунду в графических приложениях и сотни кадров в секунду в видео-играх.

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

С даже можно применять для обработки алгоритмов внутри Веб-страниц. С помощью проекта Web-Assembly, код на C преобразуется в быстрый вэб-ассемблер, вызывается командами языка JavaScript и запускается на машине пользователя. Естественно, скорость работы таких приложений высочайшая и сопоставима с настольными приложениями написанными на С/С++, но работающих в окне вашего обозревателя интернета.

С - главный язык программирования микроконтроллеров, а благодаря мощным компиляторам - размер программы на С сжимается многократно, что критически важно при ограниченных объёмах памяти в микроконтроллере.

Практически все драйверы устройств вычислительных систем пишут на С/С++.

У С есть и своя специфика:

Стандартные библиотеки языка С

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

Для ANSI C определены несколько стандартных библиотек, содержащие опробованные и протестированные функции, которые могут быть использованы в ваших собственных программах, написанных на языке C.

Библиотеки содержатся в заголовочных файлах (англ. header - заголовок), каждый из которых имеет расширение .h Названия стандартных заголовочных файлов и их краткие описания приведены ниже. Интерфейс стандартной библиотеки C определяется следующей коллекцией заголовочных файлов:

Здесь можно узнать о текущем состоянии библиотек ANSI C
Заголовочные файлы Стандартной Библиотеки
Название Содержит функции:
<assert.h> Которые могут быть использованы для диагностики программы
<complex.h> Математики с комплексными числами
<ctype.h> Работы с символами
<errno.h> Макросов сообщающих об ошибках
<fenv.h> Среды для работы с числами с плавающей точкой
<float.h> Определения констант, используемых в арифметике с плавающей точкой
<inttypes.h> Преобразования форматов целочисленных типов
<iso646.h> Альтернативных вариантов написания операторов
<limits.h> Определения константных размеров типов данных языка C
<locale.h> Утилит локализации
<math.h> Математических действий
<setjmp.h> Которые могут быть использованы для того, чтобы нарушить обычную последовательность входа в функции и выхода из них
<signal.h> Обработки исключительных ситуаций, которые могут возникнуть в программе, обработка сигналов.
<stdarg.h> Которые могут быть использованы для выполнения перебора аргументов функций
<stdatomic.h> Макросов для работы с байтовыми и битовыми представлениями типов
<stdbit.h> Атомарных операций
<stdckdint.h> Макросов для выполнения проверенной целочисленной арифметики
<stddef.h> Общих определений макросов
<stdint.h> Целочисленных типов фиксированной ширины
<stdio.h> Ввода и вывода, типов и описания макросов. Эта библиотека используется в большинстве программ, написанных на языке C, и представляет почти треть всех библиотек языка C
<stdlib.h> Вспомогательные для преобразования чисел, выделения памяти и т. д.
<stdmchar.h> Транскодирования текста (будет в C29)
<string.h> Работы со строками
<tgmath.h> Предназначен для универсального доступа к математическим функциям, независимо от типа аргумента (макросы, оборачивающие <math.h> и <complex.h>)
<threads.h> Предназначен для работы с многопоточностью. Он предоставляет стандартный набор функций, типов и макросов для управления потоками исполнения, а также синхронизацией между ними.
<time.h> Манипулирования компонентами, представляющими дату и время
<uchar.h> Символьных утилит UTF-16 и UTF-32
<wchar.h> Расширенных многобайтовых и широких символьных утилит
<wctype.h> Для определения типа, содержащегося в данных с расширенными символами
Библиотеки устаревшие в стандарте С23
Название Содержит функции:
<stdalign.h> Удобные макросы Alignas и AlignOf
<stdbool.h> Макросы для логического типа
<stdnoreturn.h> Макрос удобства noreturn
Библиотеки устаревшие в стандарте С23 будут встречаться вам в программах написанных до 2023 года ещё очень долго. Вы даже можете попробовать работоспособность этих библиотек написав и скомпилировав программу с их подключением в исходный код на современном компиляторе. В зависимости от вашей дальнейшей деятельности как программиста, знать устаревающие библиотеки всё-таки нужно, так как в профессиональной среде программисты часто и долго поддерживают программное обеспечение написанное до них.

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

Установка компилятора

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

Программы на языке C изначально создаются как простые текстовые файлы, сохраняемые с расширением .c . Они могут быть написаны в любом текстовом редакторе, даже в программе Блокнот (Notepad) операционной системы Windows — никакого специального программного обеспечения не требуется.

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

Если текст программы содержит синтаксические ошибки, компилятор об этом сообщает, и исполняемый файл не будет построен.

Один из наиболее популярных компиляторов языка C — GNU C Compiler (GCC) — доступен бесплатно под лицензией General Public License (GPL). Он включен во все дистрибутивы операционной системы Linux. GNU C Compiler был использован для компилирования в исполняемый код всех примеров этой книги.

GNU является рекурсивным акронимом для фразы Gnu's Not Unix (Gnu — это не Unix) и произносится как «ГЭ-ЭН-У». Более подробную информацию вы можете найти, перейдя по адресу https://www.gnu.org/.

Если вы ранее были знакомы с программированием, то чтобы определить, имеет ли ваша операционная система компилятор GNU C Compiler, наберите в командной строке gcc -v и нажмите ENTER. Если компилятор доступен, он выведет на экран информацию о своей версии.

В операционной системе Windows, с помощью следующих шагов загрузим пакет Minimalist GNU for Windows (MinGW), который содержит компилятор GNU C Compiler посредством пакета MSYS2.

  1. Скачайте Visual Studio Code https://code.visualstudio.com/Download
  2. Установите Visual Studio Code
  3. Скачайте MSYS2 https://www.msys2.org/
  4. После установки MSYS2 введите команду pacman -S mingw-w64-ucrt-x86_64-gcc и нажмите ENTER
  5. Откройте Visual Studio Code
  6. Установите расширения C/C++ для VS Code от Корпорации Microsoft. Вы можете установить расширения C/C++, выполнив поиск по запросу "C/C++" в представлении "Расширения"(англ. EXTENSIONS) .
    • C/C++ IntelliSense, debugging, and code browsing.
    • C/C++ Extension Pack
    • C/C++ Themes
    Расширения для редактора кода позволяют писать программисту исходный код в более комфортных условиях.
  7. Подключите MSYS2 к Visual Studio Code согласно инструкции на странице https://code.visualstudio.com/docs/cpp/config-mingw
  8. Проверьте установку MinGW, откройте командную строку и введите по очереди команды, нажимая Enter после каждой:
    • gcc --version
    • g++ --version
    • gdb --version
    • gcc -v
    Ваша версия может отличаться, обычно в большую сторону версии. На изображении версия 15.1.0
  9. Если никаких ошибок и проблем не возникло, напишем первую программу:

Написание первой программы

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

<тип-данных> <имя-функции> (){
   <утверждения-которые-нужно-выполнить>;
}

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

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

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

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

Фигурные скобки {} содержат утверждения, которые должны быть выполнены при вызове функции. Каждое утверждение обязано заканчиваться точкой с запятой ; .

Традиционно при изучении языка программирования в первую очередь пишут программу, выводящую на экран сообщение Hello World :

  1. Откройте Проводник Windows и Создайте Папку с именем Learn_C на любом диске
  2. Внутри Папки с именем Learn_C создайте папку с именем hello
  3. Откройте Visual Studio Code
  4. Нажмите вверху слева File
  5. Нажмите Open Folder
  6. Укажите местоположение папки с именем hello
  7. Нажмите кнопку снизу справа в окне проводника "Выбор Папки"
  8. Может появится предупреждение о доверии автору содержимого внутри папки с именем hello
  9. Так как Автор - вы, то смело ставим галочку и жмём синюю кнопку "Yes, I trust the authors", тем самым подтверждаем доверие к содержимому в папке.
  10. Вверху слева видно, что вы находитесь внутри папки с именем hello
  11. Создадим наш текстовый файл исходного кода на языке С с именем hello.c . Файл автоматически будет создан внутри папки с именем hello :
    • Нажмите на иконку "New File"
    • Появилось поле ввода: введите имя файла и через точку его расширение
    • Введём hello.c
    • Жмём ENTER
    • В текстовое поле ввода исходного кода справа напечатаем первую строку кода:
#include <stdio.h>

Программа начинается с инструкции (препроцессора) для компилятора C, которая указывает подключить (англ. include - включить в состав) файл стандартной библиотеки функций ввода и вывода данных <stdio.h> (англ. standard in and out). Это сделает доступными в программе все функции, описанные внутри этого файла. Подходящее название данной инструкции — инструкция препроцессора или директива препроцессора, она всегда должна быть написана в начале страницы, до того, как будет обработан сам код программы ниже её.

Функции внутри заголовочного файла

На примере заголовочного файла <stdio.h> заглянём в часть его содержимого, любые другие заголовочные файлы устроены похожим образом:

Фрагмент внутренности файла заголовков функций <stdio.h>. Выделенная функция printf() как раз и вызывается в нашей первой программе из файла заголовков функций <stdio.h>, и при её вызове запустится код реализующий печать символов в консоль.

Пропустите две строки после инструкции препроцессора и добавьте пустую(без аргументов в круглых скобках) функцию main() :

int main(){

}

Такое объявление функции определяет, что после её выполнения функция должна будет вернуть значение типа int (подробнее о типах данных далее).

Внутри фигурных скобок вставьте строку кода, которая вызывает одну из функций, определённых в стандартной библиотеке ввода-вывода <stdio.h> , ставшую теперь доступной после написания инструкции препроцессора #include :

printf("Hello World!\n");

Внутри круглых скобок функции printf() определяется один строковой аргумент. В языке программирования C строки должны быть заключены в двойные кавычки "". Эта строка содержит текст Hello World! и управляющую последовательность \n, которая автоматически переводит каретку печатания символов в консоли к левому краю следующей(ниже) строки.

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

return 0;
По традиции возвращение значения 0 после выполнения программы указывает Операционной Системе(в нашем случаи - Windows), что программа выполнилась корректно.

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

#include <stdio.h>

int main(){
   printf("Hello World!\n");
   return 0;
}
Для сохранения можно нажать комбинацию горячих клавиш Ctrl+S.

Возле названия файла hello.c отображается белый кружок - это значит что текст исходного кода в файле не сохранён. Нажмите комбинацию горячих клавиш Ctrl+S чтобы сохранить файл, перед его компиляцией. Или нажмите вверху слева File - Save

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

Компилирование программы

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

Находясь в Visual Studio Code, нажмите комбинацию горячих клавиш Ctrl+~(клавиша буквы Ё) чтобы открыть консоль:

В консоли можно ввести команду gcc --help и нажать ENTER для просмотра списка всех опций компилятора:

Множество команд компилятора. Нам пока нужны будут всего пару штук.

Самая простая команда для компиляции нашего файла записывается в таком синтаксисе:

gcc <имя_исходного_файла>.c -o <имя_выходного_файла>.exe

Где:

В нашем случаи записываем в консоль так:

gcc hello.c -o hello.exe

И жмём ENTER

Если компиляция прошла успешно, без ошибок и предупреждений, то внутри нашей папки с именем hello будет создан файл hello.exe.

Запустим скомпилированный файл в консоли Visual Studio Code для его исполнения, с помощью простой команды:

./hello.exe

И жмём ENTER

Программа выполнена и в консоль выведен результат её работы - напечатана строка текста. Консоль готова принять следующую команду.

Понимание процесса компилирования

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

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

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

Компоновщик, иногда называют линковщик/линкер от англ. to link - соединять, связывать воедино.

Этапы процесса компиляции исходного кода.

Обычно временные файлы, созданные во время промежуточных шагов процесса компилирования, удаляются автоматически, но их можно восстановить с помощью дополнительной опции -save-temps, переданной в команде компилятору.

  1. В консоли введите команду
    gcc hello.c -save-temps -o hello.exe
    , а затем нажмите клавишу ENTER, чтобы повторно скомпилировать программу и сохранить временные файлы.
    Так они выглядят в Visual Studio Code
    А так в папке hello
  2. Откройте файл hello.i здесь в Visual Studio Code - кликните на него. Ваш исходный код окажется в самом конце файла, а перед ним будет располагаться код библиотеки <stdio.h>.
    Содержимое файла hello.i, более тысячи строк кода занимает содержимое библиотеки <stdio.h>
  3. Теперь откройте файл hello.s , чтобы увидеть ваш исходный код на С, преобразованный к низкоуровневым инструкциям Ассемблера. Обратите внимание, насколько менее дружелюбным он кажется по сравнению с кодом на языке C.
    Это Ассемблер, самый низкоуровневый язык программирования, на котором всё ещё может программировать человек. Так выглядит ваша программа hello.c на Ассемблере.
Программы, написанные на языке Ассемблера, потенциально могут исполняться быстрее, чем те, которые написаны на языке C, но их гораздо сложнее писать и обслуживать(вы должны в совершенстве знать архитектуру вычислительной машины: процессор, память и множество других электронных компонентов и т.п.). Для традиционного программирования - язык C приоритетнее. Всю оптимизацию вашего исходного кода С, вместо вас сделает компилятор. По мере совершенствования знания языка С, вы сможете напрямую в коде С указывать вычислительной машине как использовать архитектурные особенности процессора и памяти и компилятор сделает это для вас.

Заключения к Введению



Решение проблем с помощью Программирования

Введение

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

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

Подход к решению проблем

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

Логическое и численное решение задач включает в себя следующие основные шаги для решения любой проблемы:

  1. Проанализируйте проблему: это процесс понимания проблемы. Чтобы лучше понять проблему, вам нужно знать, какие входные данные, а какие будут их возможные результаты. Это просто означает знание требований для решения проблемы и её возможного решения. Также проверяется осуществимость решения проблемы, чтобы выяснить, можно ли решить проблему с существующими технологиями или нет.
  2. Разделите сложную проблему на небольшие, простые задачи: если проблема сложная и большая, то сначала разделите её на разные модули. Решите отдельные модули и объедините их для окончательного решения. (Это необязательный шаг.)
  3. Разработайте план решения проблемы: на этом этапе разрабатывается план решения проблемы. Он включает в себя запись проблемы в виде псевдокода, блок-схемы или в виде алгоритма. Псевдокод, блок-схемы и алгоритм представляют собой вашу пошаговую стратегию для разрешения проблемы. Этот шаг даёт логику для решения проблемы.
  4. Реализуйте план: он преобразует алгоритм или псевдокод в программу (актуальный код) с использованием подходящего языка программирования, такого как C, C++, Python и т.д. Введите программу в компьютер. Компьютер генерирует результат в выходных данных на основе программы и входных данных. Эти выходные данные можно использовать для принятия правильных решений.
  5. Обратное отслеживание: как только вы получите результат в выходных данных, вы можете вернуться назад и внести некоторые изменения в программу или план для дальнейшего улучшения результата. Вы можете подумать о том, чтобы решить проблему другим способом для получения лучших результатов. Этот шаг часто называется этапом тестирования и отладки.

Решение логических проблем

Давайте решим классическую логическую задачу: задачу о ёмкостях с водой. Допустим, вам поручено отмерять 4-ре литра воды с помощью двух кувшинов. Объём первого кувшина 3-и литра, а второго - 5-ть литров. Необходимо выполнить следующие шаги:

  1. Анализ проблемы: Сначала проверим, доступны ли нам все необходимые ингредиенты, такие как источник воды, кувшины и так далее, которые будут служить исходными данными. Решением же является возможность получения 4-ёх литров воды - это выходные данные.
  2. Разделение сложной проблемы на небольшие, простые задачи: Наша проблема довольно простая, поэтому нет необходимости делить её на простые задачи(пропустим этот шаг.)
  3. Разработать план: Псевдокод ниже представляет пошаговый план достижения решения проблемы с помощью описания её простыми словами:
    1. Наполните 5-литровый кувшин до краев.
    2. Перелейте воду из 5-литрового кувшина в 3-литровый, в 5-литровом кувшине останется 2-а литра.
    3. Опустошите 3-литровый кувшин.
    4. Перелейте оставшиеся 2-а литра воды из 5-литрового кувшина в 3-литровый.
    5. Снова наполните 5-литровый кувшин до краев.
    6. Осторожно перелейте воду из 5-литрового кувшина в 3-литровый до заполнения 3-литрового кувшина. Вылили 1 литр.
    7. В 5-литровом кувшине останется ровно 4 литра воды.
  4. Реализовать план: Преобразуйте алгоритм или псевдокод в программу (реальный код) с использованием подходящего языка программирования, например C. Введите программу в компьютер, и вы получите желаемый результат.
  5. Обратное отслеживание: Предположим, что после нескольких шагов вы обнаружите, что распределение воды не приводит к ожидаемому результату в 4-е литра в 5-литровом кувшине. Вместо того, чтобы сдаваться, вы возвращаетесь к предыдущему шагу, где у вас были другие варианты выбора (например, какой из кувшинов наполнять, когда наливать воду и так далее). Затем вы пробуете другой вариант алгоритма, чтобы увидеть, приведёт ли это к желаемому результату.

Решение численных проблем

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

  1. Проанализируем проблему: здесь мы должны взять три переменные: две для входных данных и одну для сохранения выходного результата. Пусть числа A и B являются входными данными, а C — выходными переменными. Итак, мы можем записать это следующим образом: Входные числа: Целые числа A и B. Выходное число: Целое число C.
  2. Разделим сложную задачу: Эта задача простая, поэтому нет необходимости делить её на мелкие модули.
  3. Разработаем план: Псевдокод - представление плана решения простым языком. Псевдокод для сложения двух чисел, A и B, и сохранение результата(суммы) в третье число, C, записывается следующим образом:
    1. Сумма двух чисел. Это Название(суть решения) задачи.
    2. Ввод: целые числа A и B.
    3. Вывод: целое число C.
    4. C = A + B.
    5. Печать C.
    6. Конец.
  4. Выполните план: теперь этот псевдокод может быть преобразован в язык программирования C:
    #include <stdio.h>
    
    int main(void)
    {
       int A = 0;
       int B = 0;
       int C = 0;
       printf("Enter two numbers A and B:\n");
    
       scanf("%d %d", &A, &B);
    
       C = A + B;
       printf("Summation of A and B is: %d\n", C);
    
       return 0;
    }
                
  5. Обратное отслеживание: Как только вы получили результат в качестве выхода, вы можете попробовать альтернативные методы решения. Обычно, в первую очередь альтернативные методы решения ищут если вывод(результат) не соответствует ожидаемому. Тогда вернитесь к плану и проверьте код: возможно допущена какая-то ошибка, устраните её, чтобы получить правильный вывод. Только после того как выполнение программы гарантированно выдаёт ожидаемый результат, можно заниматься следующими этапами улучшения исходного кода, а именно: переписывать исходный код с целью увеличения его скорости выполнения, или с целью уменьшения требуемого объёма памяти машины(подробнее читайте далее в учебнике).

Техники решения проблем

Алгоритмы, псевдокод и блок-схемы являются основными методами или инструментами для решения проблемы с использованием компьютера. Более того, они преобразуются в реальные компьютерные программы с использованием языков программирования.

Алгоритм

План, как решать проблему шаг за шагом без использования аппаратного и программного обеспечения, называется алгоритмом. Затем эти шаги записываются на языке программирования в виде программы. Так создаётся программа. Другими словами, алгоритм — это план для компьютерной программы.

Опишем Характеристики алгоритма:

Некоторые простые примеры алгоритмов:

Блок-схема

Блок-схема — это диаграмма, которая показывает шаги для решения проблемы в виде алгоритма. Она используется для визуальной демонстрации того, как работает алгоритм. Это графическое представление алгоритма. Иными словами, это ещё одна техника представления проблемы, как алгоритма но в виде диаграммы или рисунка. В ней используются различные фигуры, которые связаны друг с другом линиями. Линии связей показывают, как, от куда и куда протекают информация и управление. Ниже представлены фигуры, которые используются в этом учебнике при изображении блок-схем:

Все фигуры для построения блок-схем
Фигура(Изображение) Название Применение Пример
Старт Обычно так обозначается точка входа в главную функцию main() или Название алгоритма - какую задачу он решает.
Завершение программы Обычно так обозначается выход из главной функции main()
Утверждение Любое утверждение на языке С
Условный оператор Ветвление программы по условию либо в ветку 1(true/истина/да) либо в ветку 0(false/ложь/нет). Для простоты понимания - рекомендуется применять слова "ДА" и "НЕТ", как в ответах на вопросы в обычной жизни.
Переключатель варианта Выбирает один из вариантов выражения
Вариант переключателя Вариант, соответствующий значению утверждения в Переключателе
Объявление Функции Объявление Функции. Точка входа в функцию
Вызов функции Вызов функции. Перейти в функцию по её имени/адресу
Комментарий Добавление комментария. Обычно справа/сверху от фигуры, но может быть и вертикально по линии.
Процедура Асинхронная внешняя процедура, блок кода, модуль и т.п. - без возможности управления ею. Пока процедура не вернёт "завершение", "значение" или "ошибку" - Главная функция ждёт.
Параметры Формальные параметры необходимые для запуска кода. Инструкции препроцессора и т.п. данные необходимые до Главной функции.
Структура Своего рода "полка", у которой сверху указывается её название, а снизу список элементов данных содержащиеся в ней.
Начало цикла Сжатое визуальное представление начальной конструкции циклов(for, while, do-while)
Конец цикла Сжатое визуальное представление конечной конструкции циклов(for, while, do-while)
Отправить параметр Обычно обозначает отправку параметра/ов(снизу) какому либо получателю(сверху)
Получить параметр/ры Обычно обозначает получение параметра/ов(снизу) от какого либо получателя(сверху)
Отправить Чаще всего обозначают печать в консоль
Получить Чаще всего обозначают ввод данных пользователем из консоли
Часы Инициирование выполнения утверждения в заданный момент реальной даты/времени
Длительность Выполнение утверждения/ий в течении заданного промежутка времени
Таймер Подождать заданное время, прежде чем выполнить утверждение
Параллелизм Запустить два или более параллельных потока(веток) кода одновременно
Управление Параллельным фоновым процессом Запустить/Приостановить/Продолжить/Завершить независимый Параллельный фоновый процесс. Главная функция продолжает выполняться далее - не ждёт пока Выполняющийся фоновый процесс вернёт своё "завершение" или "ошибку"
Соединитель Буквенно-Цифровой индекс соединителя линии/ий алгоритма указывает на такой же соединитель на другом листе/странице

Блок-схемы условно делят на три типа по степени сложности:

  1. Простая линейная - Состоит из нескольких базовых фигур, с помощью которых изображают простейшие алгоритмы. Обычно не имеет ветвления. Каждое действие идёт строго вертикально сверху вниз - шаг за шагом. Обычно с такой блок-схемы начинается проектирование программы.
  2. Средняя линейная - Содержит в себе не большое количество фигур ветвления потока выполнения программы и один-два цикла. Как правило с помощью такой блок-схемы изображают детально работу конкретного алгоритма.
  3. Крупная не линейная - Может размещаться на одном большом или нескольких листах/экранах/страницах посредством разбиения схемы на модули, блоки, процедуры и т.п. Содержит множество фигур ветвлений потока, циклов, функций и т.д. Состоит из Простых и Средних блок-схем.
  4. Полная - Может размещаться на десятках и сотнях больших листах/экранах/страницах посредством разбиения схемы на модули, блоки, процедуры и т.п. Содержит множество фигур ветвлений потока, циклов, функций и т.д. Состоит из Простых и Средних и Крупных блок-схем.

Пример Простой линейной блок-схемы:

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

Пример Средней линейной блок-схемы:

Блок-схема алгоритма сравнения двух чисел и вывода на печать в консоль сообщения о том какое число больше или что числа равны.

Пример Крупной не линейной блок-схемы:

Блок-схема алгоритма работы мобильного приложения. Подготовительная стадия проектирования.

Псевдокод

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

Пример псевдокода на английском языке для сложения двух чисел:

1. START
2. INPUT: Integer A and B
3. C = A + B
4. Print C
5. END

Пример псевдокода на русском языке для сравнения двух чисел - какое больше, или равны ли два числа:

1. Начало
2. Ввести: Целые числа A и B
3. Если А больше В, тогда Печать "А больше В"
4. Иначе Если А равно В, тогда Печать "А равно В"
5. Иначе Печать "В больше А"
6. Конец

Пример псевдокода на английском языке для определения является ли число чётным или нечётным:

1. START
2. INPUT: Integer A
3. IF (A % 2) == 0 THEN Print "The number is EVEN"
4. ELSE Print "The number is ODD"
5. END

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

1. Начало
2. Ввести: Целые числа A и B
3. Числу Т присвоить число А
4. Числу А присвоить число В
5. Числу В присвоить число Т
6. Конец

В таблице объясняется разница между Блок-Схемой, Алгоритмом и Псевдокодом:

Разница между Блок-Схемой, Алгоритмом и Псевдокодом
Алгоритм Блок-схема Псевдокод
Это план о том, как шаг за шагом решить проблему. Блок-схема — это графическое представление алгоритма или шагов для решения проблемы. Это неформальный способ написания программы.
Сложно понять. Легко понять. Труднее понять по сравнению с алгоритмом.
Текст используется для написания алгоритма. Для рисования блок-схем используются различные фигуры, линии показывают как выполняется поток программы. Текст и математические символы используются для написания алгоритма.
Легко отлаживать. Трудно отлаживать. Труднее отлаживать по сравнению с алгоритмом.

Языки программирования

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

  1. Машинный уровень или низкоуровневый язык (НУЯП): Этот язык имеет только два символа, 0 и 1 (бинарные числа). Все данные и инструкции (программа) должны быть написаны в двоичном коде. Компьютер может непосредственно понимать код, написанный на этом языке, так как он может понимать только двоичный язык. Он также известен как машинный язык. Он не требует транслятора (компилятора, интерпретатора) для преобразования в другую форму, так как он непосредственно понятен компьютеру. Он очень близок к компьютеру, так как компьютер понимает только 0 или 1. Каждый компьютер имеет свой собственный машинный язык, поэтому он зависит от машины и запустить на другой не такой же машине его не получится. Писать программу на этом языке сложно, и ещё сложнее найти ошибки в ней. Вот пример того как такие низкоуровневые программы выглядят для программиста:
    • Гипотетическая программа для сложения двух чисел в двоичном формате. Следующая программа состоит только из трёх команд, записанных в двоичном виде:
      01010111 11010101
      11010101 10101011
      10101010 10101011
    • Чаще всего, программы на машинном уровне пишутся в восьмеричной или шестнадцатеричной системе счисления:
      57 D5
      D5 AB
      AA AB
  2. Язык ассемблера: он использует символы для написания программы. Эти символы называются мнёмониками, например такие ADD, SUB, MUL и DIV, которые гораздо легче запомнить чем шестнадцатеричные коды. Эти мнёмоники написаны на основе сокращения английских слов. Его также называют языком среднего уровня, потому что он использует как естественный язык, так и символы. Он обладает характеристиками языков высокого уровня (ВУЯП) и языков низкого уровня (НУЯП). Компьютер не понимает их напрямую. Программе, написанной на Ассемблере, для выполнения требуется транслятор, который так и называется ассемблер (англ. Assembler - Сборщик), для преобразования из языка ассемблера в язык низкого уровня (НУЯП). Все инструкции, написанные на языке Ассемблера, различаются в зависимости от машины, поэтому они зависят от машины(машино-зависимые, аппаратно-зависимые, архитектурно-зависимые); то есть программа, написанная на одном типе компьютера, не будет выполняться на компьютере другого типа. Этот язык очень близок к компьютеру, так как он зависит от машины. Писать программу на этом языке легче, и отладка (удаление ошибок) значительно проще по сравнению с языком низкого уровня (НУЯП), но сложнее по сравнению с каким либо языком высокого уровня (ВУЯП). Вот пример кода на языке низкого уровня (НУЯП) для сложения двух чисел:
    LOAD A
    ADD B
    STORE C

    Как вы уже знаете язык C скорее ближе к языкам Среднего Уровня чем к Высокоуровневым. Так что условно можно определять язык С, как значительно более удобная версия низкоуровневого языка но на основе синтаксиса как у высокоуровневых языков. Так, выполнение сложения двух чисел на C записывается крайне просто:

    C = A + B;
  3. Язык высокого уровня (ЯПВУ): Он упрощает программирование, используя естественный язык, такой как английский, для написания программы. Его легко изучить, так как он использует естественный язык. Компьютеры не могут напрямую понять код, написанный на этом языке, так как компьютер может понимать только двоичный язык. Поэтому требуется переводчик, чтобы преобразовать ЯВУ в ЯНУ, например, компилятор, интерпретатор и так далее. Он очень близок к человеку, потому что человек может легко его понять. Он не зависим от машины и портативен, поэтому его программу можно запустить на любом компьютере. Написать программу на этом языке очень просто, и отладка (удаление ошибок) также очень проста. Примеры ЯВУ: C, C++, JAVA, FORTRAN, PYTHON, PROLOG и так далее. Следующая программа на ЯПВУ. Пример: Программа на C для сложения двух чисел, которые машина запрашивает у пользователя через консоль(терминал):
    #include <stdio.h>
    
    int main()
    {
       int A;
       int B;
       int Sum = 0;	
       printf("Enter two numbers A and B : \n");	
       scanf("%d %d", &A, &B);	
       Sum = A + B;	
       printf("Sum of A and B is: %d", Sum);	
    
       return 0;	
    }

Трансляторы или обработчики языка

Языковые трансляторы или обработчики переводят или конвертируют программы, написанные на одном языке, на другой, например, из высокого уровня в низкий уровень. Программа, написанная на языке программирования, известна как исходный код; когда она транслируется на машинный язык, она становится объектным кодом. Простой механизм языковой трансляции показан на рисунке:

Существует в целом три типа языковых трансляторов:

  1. Компилятор: Это программа, которая преобразует высокоуровневый язык в низкоуровневый. Она принимает полную программу в качестве входных данных и превращает её в низкоуровневый код. Исходный код — это программа, написанная на высокоуровневом языке. Когда она преобразована в низкоуровневый код, она становится объектным кодом. Таким образом, компилятор превращает исходный код во что-то, что называется объектным кодом. Как только объектный код был получен, он выполняется для получения результата. Это преобразование называется компиляцией.

    Функции компилятора

    Основная задача заключается в преобразовании языков высокого уровня в языки низкого уровня. Найти ошибки в исходном коде в соответствии с синтаксисом (грамматикой) языка. Отобразить ошибки, чтобы программист мог их устранить. Выделяет пространство для программы в основной памяти. Генерирует объектный код.

    • Интерпретатор: он преобразует программу на высокоуровневом языке в программу на низкоуровневом языке построчно; то есть одно выражение за раз переводится и сразу же выполняется. Программные выражения напрямую переводятся построчно в машинный язык без генерации объектного кода. В отличие от компилятора, интерпретатор - это простая программа, занимающая меньше места в памяти. Интерпретация программы занимает больше времени по сравнению с компиляцией, потому что в интерпретаторе каждое выражение должно быть переведено по отдельности. Таким образом, процесс компиляции занимает меньше времени, чем интерпретация.
    • Ассемблер: Программное обеспечение, которое преобразует программу на языке ассемблера в машинный или низкоуровневый язык, называется ассемблером(сборщиком). Программа, написанная на языке ассемблера, известна как исходный код, а выходные данные ассемблера известны как объектный код. После получения объектного кода он выполняется, чтобы получить результат. Функции ассемблера:
      • Основная задача заключается в том, чтобы преобразовать ассемблер в низкоуровневый код.
      • Найти ошибку в исходном коде в соответствии с синтаксисом (грамматикой) языка.
      • Вывести сообщение ошибки, чтобы программист мог её устранить.
      • Выделить место для программы в основной памяти.
      • Сгенерировать объектный код.

      Кроме выше упомянутых программ, следующие программы также поддерживают написание и выполнение программы:

      • Редактор: программа, которая предоставляет платформу для написания, изменения и редактирования исходного кода или текста. Этот текстовый редактор имеет свои собственные команды для написания, изменения и редактирования исходного кода или текста.
      • Компоновщик: Он создает исполняемый файл с расширением .exe, комбинируя объектные файлы, сгенерированные компилятором или ассемблером, и дополнительные куски кода. Компоновщик ищет и добавляет все библиотеки, необходимые для создания исполняемого файла, в объектный файл. Он объединяет два или более файлов в один файл, например, объектный код нашей программы и объектный код библиотечных функций, в один исполняемый файл.
      • Загрузчик: Это программа, которая загружает исполняемый код от компоновщика, помещает его в основную память и подготавливает его к выполнению на компьютере. Она выделяет программному коду память в основной памяти.

      В настоящее время все ранее упомянутые инструменты объединены в один программный пакет, который называется средой разработки (IDE). Некоторые примеры IDE включают Turbo C, CodeBlocks, Visual Studio, Microsoft Visual C++, Eclipse и так далее.

Процесс компиляции и выполнения

Существует несколько основных этапов, связанных с переводом программы на языке C(исходный код) в исполняемый файл, вот они:

  1. Напишите программу в редакторе, которая является исходным кодом с расширением .c.
  2. Исходный код передаётся компилятору, и компилятор генерирует объектный код с расширением .o. Если возникает какая-либо ошибка(синтаксическая ошибка), программист снова открывает исходный код в редакторе, находит ошибку, устраняет её и компилирует код снова.
  3. Компоновщик связывает объектный код с некоторыми дополнительными файлами, такими как библиотечные файлы и другие куски кода, необходимые для выполнения программы. Компоновщик генерирует бинарный исполняемый файл с расширением .exe. Если возникает ошибка при компоновке, скажем, компоновщик не может связать файлы с объектным кодом, это называется ошибкой компоновки.
  4. Этот исполняемый файл загружается в основную память машины загрузчиком.
  5. После этого программа выполняется, и создается вывод в форме результата. Если возникает какая-либо ошибка (ошибка времени выполнения), программист снова открывает исходный код в редакторе, находит ошибку и устраняет её.

Изобразим визуально процесс Компиляции и Выполнения исходного кода в виде блок-схемы:

Процесс Компиляции и Выполнения программы.

Синтаксические и логические ошибки в компиляции

Язык программирования имеет набор правил для написания утвердительных предложений(утверждений). Этот набор правил известен как грамматика языка. Грамматика имеет два типа ошибок:

  1. Синтаксические ошибки: возникают, если программист нарушает правила грамматики при написании операторов. Например, отсутствие точки с запятой в конце оператора, использование не объявленных переменных в программе, неправильное написанных ключевых слов и так далее. Компилятор выявляет это во время компиляции. Изучите следующую программу:
    #include <stdio.h>
    void main()
    {
       int A = 2, B = 3, C;
       C = A + B
       printf("Sum is %d", C);
       getcha();
    }

    Напишите этот код, сохраните его и выполните. Какие ошибки компиляции сообщил Компилятор?

  2. Логические или семантические ошибки: они возникают, когда логика программы неверна. Это связано со смысловым значением утверждения. Например, если программист ставит знак минус вместо знака плюс, то такая ошибка не выявляется на этапе компиляции(так как синтаксис утверждения верный). Но она выявится во время выполнения, когда программа выдаст неверные результаты. Например:
    A + B = C;
    A = B / 0;
                
    Напишите и выполните программу ниже, где допущена логическая ошибка?:
    #include <stdio.h>
    int main()
    {
       int A = 6;
       int B = 3;
       int C;
       C = A + B / 2;
       printf("Average of A = %d and B = %d is: %d", A, B, C);   
    
       return 0;
    }

Разные файлы, создаваемые при написании, компиляции и выполнении программы на C

Итак, повторим основные три типа файлов создаваемые при написании программ на языке С и запомним чем они отличаются:

  1. Исходный код: Когда мы пишем программу, её необходимо сохранить с расширением .c. Процесс сохранения программы генерирует файл. Например, создаётся и сохраняется файл с именем sum.c. Этот файл и есть исходный код.
  2. Объектный код: Исходный код передаётся компилятору, а компилятор генерирует другой файл, известный как объектный код. Он имеет расширение .o, например, sum.o. Входными данными для компилятора является исходный код, а выводом компилятора является объектный код.
  3. Исполняемый код: с помощью компоновщика объектный код связывается с некоторыми дополнительными файлами, такими как библиотечные файлы и другие куски кода, необходимые для выполнения программы. Компоновщик генерирует бинарный исполняемый файл с расширением .exe, известный как исполняемый код. Например, объектный код sum.o преобразуется в sum.exe.

После успешного выполнения все три файла хранятся в рабочем каталоге C(в папке). Полный процесс показан на блок-схеме выше.

Заключение

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

Следующая глава будет посвящена введению и истории языка программирования C. Это необходимо, для понимания стратегии создания программ, т.е. как написать компьютерную программу имея лишь стратегию решения проблемы. Кроме того, мы обсудим основные компоненты программы на языке C и метод её выполнения.

Важные моменты

Важные вопросы

  1. Что вы имеете в виду под решением проблем? Напишите различные техники для этого с их преимуществами и недостатками.
  2. Опишите различные шаги, используемые в решении логических и числовых задач.
  3. Опишите различные этапы, используемые для решения реальной проблемы. Объясните их, взяв подходящий пример.
  4. Напишите алгоритм, блок-схему и псевдокод для задачи нахождения наибольшего из трех чисел.
  5. Напишите алгоритм, блок-схему и псевдокод для нахождения корней квадратного уравнения.
  6. Что такое язык программирования? Объясните его разные типы с их преимуществами и ограничениями.
  7. Что вы имеете в виду под языковыми трансляторами(переводчиками или обработчиками)? Объясните их различные типы.
  8. Опишите и обсудите процесс компиляции и выполнения.
  9. Опишите разницу между синтаксическими и логическими ошибками.
  10. Опишите разницу между компилятором и интерпретатором.
  11. Объясните разные файлы, генерируемые при написании, компиляции и выполнении программы на языке C.

Электронная Вычислительная Машина

Введение

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

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

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

Набор Инструкций, Утверждений, Команд это исходный код на языке С, которые программист пишет для выполнения задачи. Набор инструкций известен как программа. Набор программ известен как программное обеспечение(ПО). Электронное вычислительное устройство известно как аппаратное обеспечение(АО). Таким образом, компьютер — это совокупность аппаратного и программного обеспечения.

Схема ниже показывает базовые компоненты любой вычислительной машины:

ЭВМ состоит из Программного и Аппаратного обеспечения.

Блок-схема функциональных единиц/компонентов компьютера

В общем базовом случае, ЭВМ состоит из четырех функциональных блоков, которые соединены между собой, образуя компьютер:

Четыре базовые составные части любого компьютера - Процессор, Память, Устройства Ввода и Вывода.

Все Блоки соединены друг с другом через системные шины(провода) и в целом определяют архитектуру машины. На сегодня количество различных архитектур ЭВМ огромно. Существует три основных шины:

Блок Ввода

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

Устройство Ввода обычно выполняет три основные функции, которые заключаются в следующем:

Блок Вывода

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

Устройство Вывода как правило, выполняет три основных функции:

Устройства Вывода условно можно разделить на три типа:

Три основных типа Устройств Вывода.

Блок Центрального Процессора

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

Блок Памяти

Его задача - хранить данные и инструкции/программы. ЦП обращается к данным и программам в памяти и обрабатывает их. Если генерируется какой-либо промежуточный результат, он сохраняется в памяти, и окончательный результат также сохраняется в памяти. Память разбита на ячейки. Каждая ячейка имеет собственный адрес. ЦП обращается к памяти, генерируя адрес ячейки. Работа памяти с ЦП показана на рисунке:

Связи между ЦП и Памятью.

Компьютерная память условно делится на три типа:

На рисунке показано подключение первичной и вторичной памяти к ЦП:

Вторичная, Первичная, КЭШ и Регистры - основные виды памяти машины.
Стоит уточнить, что сами по себе Первичная и Вторичная носители памяти не умеют изменять, отправлять, получать данные - всеми этими процедурами управляет Процессор.

Данные и информация

Слово данные происходит от слова данное, которое является единственным числом. Данные состоят из сырых фактов и цифр, предоставленных компьютеру в качестве входных данных. Компьютер обрабатывает данные и преобразует их в информацию. Следовательно, информация определяется как обработанные данные. Например, оценки студентов – это данные; когда они обрабатываются, они становятся результатом, который является информацией. Ещё один пример: имена студентов в классе могут рассматриваться как данные, когда они обрабатываются и упорядочиваются в алфавитном порядке - тогда они становятся информацией. На рисунке показаны взаимосвязи между данными и информацией:

Компьютер преобразует Данные в Информацию.

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

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

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

Классификация компьютеров

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

Классификация по поколениям или историческому развитию

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

Классификация компьютеров по поколениям
Номер поколения Года существования Описание
0 До 1945 Механические компьютеры. Все компьютеры или другие инструменты, использованные для вычислений в то время, попадают в эту категорию. Примеры компьютеров нулевого поколения:

Преимущества: Они были самыми быстрыми вычислительными машинами своего времени.

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

1 1946 - 1956 Вакуумные лампы: это поколение стало началом эры цифровых компьютеров. Они использовали множество вакуумных ламп для вычислений. Для установки они обычно требовали огромное пространство: комнаты, залы или здания. Для ввода данных использовались перфокарты, а для хранения информации - магнитные барабаны. Они программировались на низком уровне, то есть в двоичной системе, которая состоит из 0 и 1. Вот несколько машин первого поколения:
  • Electronic Numerical Integrator and Calculator (ENIAC),
  • Universal Automatic Computer (UNIVAC),
  • Electronic Discrete Variable Automatic Computer (EDVAC)

Преимущества: Они были самыми быстрыми машинами своего времени. Быстрее, чем компьютеры нулевого поколения.

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

2 1957 - 1963 Они использовали транзисторы для своих вычислений. Они были быстрыми и компактными компьютерами по сравнению с предыдущим поколением. Они использовали магнитные ленты и диски для вторичного хранения. Они использовали перфокарты для ввода информации, полученной в виде распечаток. Их программировали с помощью символов или языка Ассемблера, который состоял из специальных символов, таких как SUM, MUL, LOAD и так далее. Примеры компьютеров второго поколения:
  • HONEYWELL,
  • IBM-7030,
  • CDC1604,
  • UNIVAC LARC

Преимущества:

  • Они были самыми быстрыми машинами своего времени.
  • Размер был меньше по сравнению с компьютерами первого поколения.
  • Скорость и устойчивость к неисправностям были высокими.
  • Потребляли меньше энергии и генерировали меньше тепла, чем компьютеры первого поколения.
  • Программировались на Ассемблере.

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

3 1964 - 1971

Они использовали интегральные схемы (ИС) для своих вычислений. Одна ИС способна выполнять задачи множества транзисторов и вакуумных ламп. Это были чипы на основе материала кремния, которые значительно повысили быстродействие и эффективность компьютеров. ИС были меньше, дешевле, быстрее и надежнее, чем транзисторы. Они использовали ИС с маломасштабной интеграцией (SSI), среднёмасштабной интеграцией (MSI) и крупномасштабной интеграцией (LSI). Чипы LSI, MSI и LSI могут содержать до десяти, ста и тысячи электронных компонентов соответственно(транзисторов, резисторов, диодов, конденсаторов и т.д.).

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

  • IBM 360/370,
  • PDP-8
  • PDP-11,
  • CDC 6600
Подробнее о ИС разной степени интеграции

Преимущества:

  • Они были самыми быстрыми машинами своего времени.
  • Они были меньше, более надежными, дешевле и потребляли меньше энергии.
  • У них была большая первичная и вторичная память.

Недостатки:

  • Они быстро перегревались, поэтому требовалось кондиционирование воздуха для поддержания температуры.
  • Для производства интегральных схем требуется очень сложная технология.
4 1972 - 1989

В 1971 году компания Intel разработала свой первый микропроцессор, модель 4004, который состоит из всех компонентов ЦП, то есть арифметико-логического устройства (АЛУ), устройства управления (УУ) и регистров, на одном чипе. Микропроцессоры стали начальными компонентами компьютеров четвертого поколения. Микропроцессор использует технологии VLSI или ULSI. Чип VLSI может содержать до десяти тысяч электронных компонентов, таких как транзисторы.

В то время память стала очень быстрая. Для основной использовалась полупроводниковая память, а для вторичной памяти - жёсткий диск. Было разработано много новых операционных систем, таких как WINDOWS, UNIX и MS-DOS. Их программировали на таких языках как B, C, C++ и других. Для ввода использовались клавиатура и мышь, а для вывода - монитор. Примеры: IBM PC, CRAY-1, CRAY-2 и другие являются компьютерами четвертого поколения.

Преимущества:

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

Недостатки:

  • Они не были интеллектуальными компьютерами.
5 1990 - Настоящее время

С помощью технологии ультра-широко-масштабной интеграции (ULSI) люди пытаются разработать интеллектуальные компьютеры. Работа всё ещё продолжается, и эта концепция известна как Искусственный Интеллект. Некоторые примеры ИИ включают: распознавание голоса, распознавание лиц, машинное обучение и так далее. Высокоскоростные компьютеры, такие как суперкомпьютеры, превращают концепцию ИИ в реальность. Эти компьютеры используют чипы (ULSI), которые могут содержать от десяти тысяч до миллиардов электронных компонентов.

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

Преимущества:

  • Снова, они являются самыми быстрыми машинами.
  • Они используют концепцию ИИ.
  • Меньше, доступнее, многоразовые, низкой стоимости и проще в использовании, чем их предшественники.
  • Огромные объёмы первичной и вторичной памяти.

Недостатки:

  • Они не являются полностью самостоятельно думающими.
Ведутся исследования в области Квантовых и Оптических компьютеров. И те и другие считаются следующим шагом после эры транзисторов - после 5 поколения.

Классификация по назначению

В зависимости от назначения компьютеры можно разделить на две категории:

Классификация по используемой технологии

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

Классификация на основе технологии может быть представлена в виде диаграммы:

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

Аналоговые - Это специализированные компьютеры. Эти компьютеры используются для измерения аналоговых сигналов. На рисунке ниже показана зависимость аналогового сигнала от времени. Аналоговый сигнал может имеет разное значение с течением времени. Например, такие электрические параметры как: сила тока, напряжение, сопротивление, ёмкость заряда, сила магнитного поля, и др. являются аналоговыми параметрами. Они представляют собой измеренные параметры таких физических величин как например: Скорость движения, скорость ускорения, расстояние одного объекта до другого объекта, температура, влажность, количество пыли в камере, угол поворота рычага и многие другие параметры. Следовательно аналоговый сигнал не имеет какого-то одного или нескольких фиксированных уровней значения.

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

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

Аналоговый сигнал отличается от Дискретного(цифрового).

Гибридные - Это сочетание цифровых и аналоговых компьютеров. Они обрабатывают как аналоговые, так и цифровые сигналы. Они используют аналого-цифровые преобразователи(АЦП - преобразует аналоговый сигнал в цифровой) и цифро-аналоговые преобразователи (ЦАП - преобразует цифровой сигнал в аналоговый).

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

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

Кроме того, условно, цифровые компьютеры можно поделить на следующие четыре типа:

Классификация по количеству пользователей

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

Характеристики компьютера

Сегодня современные компьютеры обладают следующими характеристиками:

Преимущества компьютера

Отметим некоторые преимущества компьютеров, компьютеры могут:

Ограничения компьютера

Однако, есть у электронных вычислительных машин и недостатки:

Применения компьютера

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

Заключение

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

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

Важные моменты

Вспомните следующие важные моменты, и подумайте о них:

Важные вопросы

Дайте ответы на вопросы ниже, если затрудняетесь - прочитайте ещё раз текст выше:

  1. Что такое компьютер? Объясните из чего он состоит. Приведите области его применения.
  2. В чём различие между данными и информацией. Что более полезно для человека?
  3. Чем отличаются первичная и вторичная память.
  4. Что такое шина? Сколько типов шин вы уже знаете. Объясните назначение каждой шины.
  5. Какие различные вычислительные устройства использовались в разных поколениях? Объясните главные их отличия.
  6. Что такое ИС - Интегральная Схема? Укажите различные их типы.
  7. Объясните поколения компьютеров. Сравните поколения между собой. Какое из них лучше и почему?
  8. Классифицируйте компьютеры на основе их исторического развития, назначения, использованной технологии, числа пользователей и размера.
  9. Обсудите разницу между микро-компьютерами и мини-компьютерами.
  10. Сравните супер-компьютеры и мейнфреймы. Также опишите их области применения.
  11. Опишите основные характеристики компьютера.
  12. Что такое концепция "мусор на вводе - мусор на выводе"(GIGO)? Объясните её.

Центральный Процессор и Память

Введение

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

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

Процессор

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

Из чего состоит Процессор

Основные компоненты ЦП следующие:

Арифметико-Логический Блок

Арифметико-логическое устройство отвечает как за арифметические, так и за логические функции. Арифметические функции включают сложение, вычитание, умножение, деление и т.д., а логические операции - И, ИЛИ, НЕ и т.д.

Блок Управления

Управляющее устройство служит нервной системой компьютера. Оно генерирует управляющие сигналы, которые управляют и контролируют все компоненты компьютера. Например, чтобы сложить два числа, оно выполняет следующую операцию:

Регистры Процессора

Регистры - это малогабаритная, быстро доступная память внутри ЦП. Они состоят из небольшого количества быстрой памяти. Размер регистров измеряется количеством бит, которые они могут содержать, например, 8-битный регистр, 16-битный регистр, 32-битный регистр, 64-битный регистр и так далее. Компьютер с большим количеством регистров может обрабатывать больше информации и наоборот. Регистры могут быть сгруппированы на две группы, которые следующие:

Инструкции внутри Процессора: Работа Процессора

Основная функция компьютера — выполнять программу (набор инструкций). Формат инструкции показан ниже. Согласно архитектуре компьютера по модели фон Неймана или концепции хранения программы, данные и инструкции (программа) хранятся в памяти. ЦП должен выполнить следующие два шага для обработки инструкции:

  1. В цикле выборки процессор читает (извлекает) инструкцию из памяти.
  2. Инструкция выполняется процессором в цикле исполнения.

Инструкция состоит из двух частей:

  1. Код операции(Опкод)
  2. Операнды
<Код_операции> <Операнд>

Опкод указывает, что должно быть сделано, например, сложение, умножение, деление и так далее. Он обозначает код операции. Операнды указывают данные/адрес, к которым будет применен опкод. Рассмотрим следующее арифметическое выражение:

a = b + c

Здесь a, b, c являются операндами, а +, = кодами операций. Инструкция выполняется процессором с помощью цикла инструкций или машинного цикла. Машинный цикл состоит из следующих двух циклов:

  1. Цикл выборки
  2. Цикл выполнения

Рассмотрим работу машинного цикла на блок-схеме:

Пошаговое выполнение одной инструкции

Когда ЦП выполняет инструкцию, он должен следовать ряду шагов, которые представлены на рисунке. Давайте рассмотрим инструкцию сложения двух чисел. Здесь инструкция - ADD, а данные - 50 и 60. ЦП извлекает и выполняет одну инструкцию с каждым тактом тактового генератора. Если ЦП завершает один машинный цикл за одну секунду, это называется одним герцем. Для выполнения одной инструкции выполняются следующие шаги:

  1. Извлечь инструкции и данные из основной памяти
  2. Декодировать инструкцию.
  3. Выполнить команду, то есть ADD(Сложение двух операндов).
  4. Сохранить результат в основной памяти.

Быстродействие Процессора

Скорость ЦП измеряется в IPS( англ. instructions per second ), что означает "инструкций в секунду". Это указывает на то, сколько инструкций выполняется процессором за одну секунду. Существует три основных фактора, влияющих на скорость выполнения инструкций ЦП:

  1. Тактовая частота,
  2. Количество используемых ядер,
  3. КЭШ-память.

Частота тактового сигнала

Тактовая частота относится к скорости, с которой ЦП может выполнять инструкции. Она измеряется в герцах(Гц). Тактовый генератор управляет работой ЦП. ЦП извлекает и выполняет одну инструкцию с каждым тактом тактового генератора. Циклы в секунду являются единицей измерения скорости тактовой частоты, и один герц равен одному циклу в одну секунду. Когда тактовая частота ЦП выше, он может обрабатывать инструкции с более высокой скоростью. Процессор с тактовой частотой 5,0 ГигаГерц(ГГц) выполняет 5 миллиардов циклов в секунду.

Общее количество ядер процессора

ЦП состоит из элемента, известного как ядро. Обычно ЦП состоит из одноядерного процессора. Большинство современных центральных процессоров имеют два, четыре и больше ядер. Например, двухъядерный ЦП содержит два ядра, в то время как четырёхъядерный ЦП содержит четыре ядра. Одноядерный процессор может извлекать и выполнять только одну инструкцию за раз, тогда как двухъядерный процессор может извлекать и выполнять две инструкции за раз. ЦП с четырьмя ядрами может выполнять ещё больше инструкций за то же время, чем процессор только с двумя ядрами.

КЭШ-память процессора

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

Блок Памяти

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

Иллюстрация связи между памятью и процессором.

Итак, память делиться на два основных типа

Главная (Внутренняя) Память

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

В общем, ОЗУ считается основной памятью. Она является энергозависимой - данные стираются, когда компьютер выключен. Вторичная память не подключена напрямую к ЦП. Она также называется вспомогательной памятью или третичной памятью.

Оперативное Запоминающее Устройство

ОЗУ - это сокращение от Оперативное Запоминающее Устройство. Оно хранит данные и инструкции перед обработкой их процессором и также хранит результат этой обработки. Оно известно как память с произвольным доступ (сокр.англ. - RAM - Random Accesses Memory - Память со случайным доступом), потому что сохраненное содержимое может быть напрямую получено из любого места произвольно или в любом порядке. Данные, которые в данный момент используются процессором, находятся в нём. Оно временно хранит данные или инструкции, так как данные стираются, когда отключается питание. Оно требует постоянно подключенного источника питания для хранения данных. Чтобы запустить данные или программу на компьютере, они сначала должны быть загружены в ОЗУ. Если памяти ОЗУ слишком мало, она не может удержать все необходимые данные или программы, которые нужны процессору. Когда это происходит, процессору приходится получать необходимые данные из вторичной памяти, что очень медленно и замедляет работу компьютера. Чтобы решить эту проблему, достаточно увеличить размер ОЗУ в компьютере. Существует два типа ОЗУ:

Постоянное Запоминающее Устройство

ПЗУ означает энергонезависимую память и относится к основной памяти(англ. - Read Only Memory - Постоянное Запоминающее Устройство). Это энергонезависимая память, поскольку она сохраняет данные даже при отключении питания. Она не требует постоянного источника питания для хранения данных. Это постоянная память. ПЗУ сохраняет фиксированные инструкции загрузки, используемые для процесса загрузки (для включения компьютера при подаче питания). ПЗУ можно только читать, его нельзя записывать или стирать пользователем компьютера. Запись осуществляется только один раз. Производитель компьютера хранит инструкции загрузки в ПЗУ. Компьютеры содержат небольшое количество ПЗУ, которое хранит такие программы, как базовая система ввода-вывода (англ. BIOS - Base Input Output System), используемая для загрузки компьютера при его включении. В процессе загрузки операционная система копируется из вторичной памяти(например с SSD(англ. Solid State Drive - Накопитель Твёрдого Состояния(твердотельный(не механический) накопитель))) в основную память(ОЗУ).

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

Вторичная (Внешняя) Память

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

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

Некоторые вторичные запоминающие устройства являются переносными-устройствами — это устройства, на которых данные записываются и физически удаляются или отключаются от компьютера, например, CD, DVD, магнитная лента и так далее. Встроенная-память не может быть физически удалена или отключена от компьютера, например, ОЗУ, ПЗУ и т.п. Широкая классификация компьютерной памяти может быть представлена на схеме ниже:

Последовательный доступ к памяти: магнитная лента

Предоставляет собой последовательный доступ к данным из носителя памяти; например, если нам нужно получить доступ к N-му месту данных, то нам сначала нужно пройти через все предыдущие N–1 места. Магнитная лента является хорошим примером носителя памяти с последовательным доступом. Магнитная лента состоит из барабана и собственно ленты, которая покрыта слоем магнитного материала. Магнитные ленты доступны в виде кассет, катушек и картриджей.

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

Информационная ёмкость магнитной ленты = плотность записи данных * длина ленты

Количество данных, которые могут быть сохранены на определенной длине ленты, называется плотностью записи данных. Этот параметр измеряется в bpi (англ. bits per inch - бит на дюйм). Магнитный ленточный накопитель читает и записывает данные на магнитную ленту. Механизм чтения и записи состоит из головки чтения/записи и головки стирания. Предварительно записанные данные стираются головкой стирания и записываются/читаются головкой записи/чтения.

Магнитная проволока и Оптическая Лента, принципиально по принципу работы ничем не отличаются. Разница лишь в конструкции механизмов чтения/записи/стирания.

Полу-свободный доступ к памяти

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

Оптический диск: Оптические диски используют луч лазерного света для чтения и записи данных на плоском круглом диске, покрытом специальным материалом (часто алюминием). Запись данных на диск называется прожигом диска, потому что лазерный свет сжигает материал, покрывающий диск, во время записи данных на диск. Данные хранятся в виде канавок (двоичной единицы или «включено» (из-за отражения)) и впадинок (двоичного нуля или «выключено» (отсутствие отражения)) на поверхности диска, формируемой лазерным лучом. Рисунок показывает поверхность оптического диска с канавками и впадинками.

Плотность записи на различные оптические диски. Характеристики размера впадинок(питов) и длинны волны лазеров.

Иерархия памяти

В современном компьютере взаимодействие памяти с ЦП основано на так называемой иерархии памяти, для повышения производительности и снижения стоимости компьютера. Иерархия памяти означает размещение её на разных уровнях в компьютере в зависимости от дальности размещения от Центрального Процессора.

Иерархию памяти можно представить в виде пирамиды, где скорость и стоимость памяти постепенно уменьшаются к основанию пирамиды, но увеличивается её объём. Так например 10 ГБ основной памяти дороже, чем 10 ГБ вторичной памяти. Время доступа ЦП к КЭШу гораздо меньше, чем к основной памяти, а размер увеличивается, т.о. размер КЭШа несоизмеримо мал по сравнению с размером основной памяти.

Память более высокого уровня (на вершине пирамиды) - ближе к ЦП(находится прямо внутри ЦП), она быстрее и дороже, однако её объём крайне мал. В то время как на более низком уровне (максимально далеко от ЦП) память очень медленная и дешёвая, а вот её объём огромный.

Память наивысшего уровня в иерархии включает в себя РЕГИСТРЫ Центрального Процессора, потом КЭШ-память, потом основную память и так далее(вниз к основанию пирамиды).

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

Триггеры или Защёлки отвечают за хранение одного бита данных, и могут хранить его состояние либо в 1-це, либо в 0-ле. Триггеры являются ключевым компонентом для создания регистров ЦП. Регистры ЦП расположены внутри ЦП и, следовательно, непосредственно доступны Процессору. Регистры ЦП хранят данные в виде так называемых битовых слов данных размером в 8, 16, 32, 64 бита и так далее.

КЭШ расположен между ультра-быстрыми регистрами и основной памятью. Он хранит часто используемые данные, которые нужно использовать Процессору снова и снова. КЭШ состоит из чипов SRAM. Хранение повторно необходимых данных позволяет избегать запроса доступа к данным в более медленной памяти (DRAM — основной памяти) со стороны Процессора, что увеличивает производительность компьютера, так как SRAM быстрее, чем чипы DRAM. Кэш-память обычно делится на уровни:

Размеры КЭШа в современных Процессорах суммарно уже измеряются в МегаБайтах или десятках МегаБайт.

Размеры ОЗУ в современных компьютерах уже измеряются в десятках ГигаБайт.

Размеры Вторичной памяти в современных компьютерах суммарно уже измеряются в ТераБайтах или десятках ТераБайт.

Измерение памяти

Наименьшая единица измерения памяти — это бит. Один бит означает либо 0 либо 1. Комбинация четырёх битов известна как ниббл, а комбинация восьми битов — как байт.

Приведём сравнительную таблицу объёма памяти и название значений исчисляемые в байтах:

Название объёма Описание Количество в степени двойки Количество в степени десяти Символы обозначения
1 Бит(двоичный разряд) 20 Бит бит (Bit)
2 Бита 21 Бит
3 Бита
1 Ниббл Четыре Бита 22 Бит Ниббл (Nibble)
5 Бит
6 Бит
7 Бит
Байт 8 Бит 23 Бит = 20 Байт б (B)
2 Байта 16 Бит 24 Бит = 21 Байт б (B)
4 Байта 32 Бита 25 Бит = 22 Байт б (B)
8 Байт 64 Бит 26 Бит = 23 Байт б (B)
16 Байт 128 Бит 27 Бит = 24 Байт б (B)
32 Байта 256 Бит 28 Бит = 25 Байт б (B)
64 Байта 512 Бит 29 Бит = 26 Байт б (B)
128 Байт 1024 Бит 210 Бит = 27 Байт б (B)
256 Байт 2048 Бит 211 Бит = 28 Байт б (B)
512 Байт 4096 Бит 212 Бит = 29 Байт б (B)
Килобайт 8192 Бит = 1024 Байта 213 Бит = 210 Байт ~103 Бит Кб (KB)
Мегабайт 1024 Килобайт 220 Байт ~106 Бит Мб (MB)
Гигабайт 1024 Мегабайт 230 Байт ~109 Бит Гб (GB)
Терабайт 1024 Гигабайт 240 Байт ~1012 Бит Тб (TB)
Петабайт 1024 Терабайт 250 Байт ~1015 Бит Пб (PB)
Экзабайт 1024 Петабайт 260 Байт ~1018 Бит Эб (EB)
Зеттабайт 1024 Экзабайт 270 Байт ~1021 Бит Зб (ZB)
Йоттбайт 1024 Зеттабайт 280 Байт ~1024 Бит Йб (YB)
Бронтобайт 1024 Йоттбайт 290 Байт ~1027 Бит Бб (BB)
Геопайт 1024 Бронтобайт 2100 Байт ~1030 Бит Геб (GeB)
Примеры преобразования единиц памяти

Компьютер использует слова для хранения информации. Слова, такие как байт, представляют собой определённое количество бит, которые обрабатываются одновременно. Размер слова варьируется от компьютера к компьютеру. Например, существуют 8-битные, 16-битные, 32-битные, 64-битные компьютеры и т.п.

  1. Сколько байт в 8 МБ? Объяснение: (8) × (1024) × (1024) байт = 8,388,608 байт.
  2. Сколько бит в 1 гигабайте? Объяснение: (1,024) × (1,024) × (1,024) × 8 бит = 8,589,934,592 бит
  3. Сколько бит в 8 гигабайтах? Объяснение: ((8) × (1,024) × (1,024) × (1,024) байт) × 8 бит = 68,719,476,736 бит
  4. 20 гигабайт = (20) × (1,024) Мегабайт
  5. Один миллиард символов = 1 Гигабайт. Объяснение: 1 гигабайт = 109 байт = один миллиард байт, где один байт эквивалентен одному символу.
  6. Если один гигабайт равен 230 байтам данных, то скольким битам равно 1,024 терабайт данных? Объяснение: 1 терабайт = 240 байт. Тогда 1,024 терабайт = 240 × 1,024 байт, следовательно 1,024 терабайт = 240 × 1,024 байт × 8 бит = 9,007,199,254,740,992 бит = 253 бит. Т.е. 210 байт × 240 байт × 23 бит = (степени с одинаковым основанием суммируются при умножении) = 253 бит.
  7. Если у вас есть устройство хранения объёмом 16 гигабайт, сколько файлов по 32 килобайта вы сможете поместить на этом устройстве? Объяснение: Переведём 16 гигабайт в Килобайты = 230 × 16 / 1024 = 16,777,216 килобайт. 16,777,216 килобайт / 32 килобайта = 524,288 файлов = 219 файлов. Т.е. 16 гигабайт / 32 килобайта = (24 × 230) / (25 × 210), при деление чисел в степени с одинаковым основанием, их степени вычитаются: (234) / (215) = 234-15 = 219.
  8. В памяти мы имеем 22,000 килобайт свободного пространства — примерно, сколько это мегабайт? Объяснение: Достаточно поделить 22,000 килобайт на 1024(т.е. один килобайт) = 21.484375 мегабайт, т.е. около 21 мегабайта.
  9. Какой степени двойки эквивалентно 8 гигабайт в байтах? Объяснение: 23 × 230 = 233 байт.
  10. У одного человека 1700 мегабайт данных на флешке, а у другого человека на флешке 1500 мегабайт данных. Поместятся ли на флешку третьего человека эти файлы, если размер флешки 4 гигабайта? Объяснение: Суммируем файлы первого и второго человека так: 1700 + 1500 = 3300 мегабайт. Разделим суммарный объём на 1024 и получим 3.22 гигабайта. 3.22 меньше 4 - значит все файлы поместятся.
  11. Сколько 3 мегабайтных фотографий поместится на флешке объёмом 30 гигабайт? Объяснение: Переведём 30 гигабайт в мегабайты и разделим на размер фотографии в 3 мегабайта так: (30 × 230) / (3 × 220) = 10 × 210 = 10 × 1024 = 10,240 фотографий.
  12. Ранжируйте объёмы памяти от наибольшего к наименьшему:
    • 512 мегабайт
    • 8 килобайт
    • Один миллиард байт
    • 1 терабайт
    Для этого проще всего привести все значения к единой единице измерения, т.е. наименьшей - это килобайт. Приведите все значения к килобайтам так:
    • 512 мегабайт = (29 × 220) / 210 = 219 килобайт
    • 8 килобайт
    • Один миллиард байт(не указанно в стандартном символьном обозначении, значит это число десятичное) = ~109 / ~103 = ~106 килобайт
    • 1 терабайт = 240 / 210 = 230 килобайт
    Порядок от наименьшего объёма к наибольшему такой:
    • 8 килобайт
    • 512 мегабайт = (29 × 220) / 210 = 219 килобайт
    • Один миллиард байт(не указанно в стандартном символьном обозначении, значит это число десятичное) = ~109 / ~103 = ~106 = ~220 килобайт
    • 1 терабайт = 230 килобайт

Заключение

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

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

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

Важные моменты

Важные вопросы

Компьютерное программное обеспечение

Введение

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

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

Программное Обеспечение (ПО)

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

Программа — это набор компьютерных инструкций или команд, написанных на языке программирования, таком как C, C++, Java и так далее.

Программное обеспечение в широком смысле делится на две категории:

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

Примеры системного ПО

Вот некоторые примеры системного программного обеспечения: драйверы устройств, компиляторы, интерпретаторы, компоновщики, загрузчики, BIOS и Операционные Системы(ОС). Наиболее известные ОС: Windows, Linux, UNIX и др. Опишем назначение различного Системного ПО:

Классификация программного обеспечения в зависимости от прав собственности

Чаще всего используются комбинации выше описанных методов

Операционные Системы (ОС)

Это системное программное обеспечение, которое работает как интерфейс между аппаратным обеспечением и пользователем и позволяет запускать другое прикладное программное обеспечение. Оно также выполняет функцию менеджера ресурсов, поскольку управляет ресурсами компьютера, такими как ЦП, память, файлы, устройства ввода-вывода и так далее. Примеры операционных систем (ОС): Linux, UNIX, Windows, Android и другие. Рисунок ниже показывает положение операционной системы в компьютере:

Позиция операционной системы в компьютере.

Структура ОС UNIX

Это многослойная структура. Структура операционной системы состоит из ядра(англ. Kernel) и командного интерпретатора(англ. Shell - оболочка):

Компоненты ОС UNIX.

Операционная система как менеджер ресурсов

Операционная система управляет всеми аппаратными ресурсами, такими как ЦП, память, устройства ввода-вывода и тому подобное, а также программными ресурсами, такими как программы, файлы, процессы, потоки и т. д. Именно поэтому её называют менеджером ресурсов. Рассмотрим, как операционная система управляет ресурсами:

Классификация операционных систем

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

Последовательное выполнение программы(серийная обработка)

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

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

Например, у нас есть четыре программы P1, P2, P3 и P4. P1 и P3 одного типа, а P2 и P4 - другого типа. Пусть максимальное время, выделенное ЦП для каждого процесса составит 2 секунды, и 2 секунды требуется для загрузки среды(окружения) для выполнения. Например, для программы на языке C нам нужно загрузить окружение для языка C, а для программы на языке JAVA нам нужно загрузить окружение языка JAVA.

Предположим, что они передаются системе в последовательности P1, P2, P3 и P4. Общее время, необходимое для завершения всех процессов, составит 16 секунд, как показано на диаграмме времени ниже. У нас есть два ресурса: ЦП и устройство ввода-вывода:

Диаграмма Ганта для системы последовательного выполнения задач.

Операционная Система пакетной обработки

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

Данные и программы (окружение), которые необходимо обработать, группируются в пакет и выполняются вместе. MS-DOS — это пакетная операционная система. В этом случае задания P1, P2, P3 и P4 делятся на два пакета: P1 и P3 в пакете #1, а P2 и P4 в пакете #2. Эти два пакета выполняются последовательно. Общее время завершения всех процессов составит 12 секунд, как показано на диаграмме времени ниже:

Диаграмма Ганта для системы пакетного выполнения задач.

Многопрограммная операционная система

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

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

На схеме ниже представлена память многопрограммной машины. Когда одна программа покидает ЦП(выгружается) и обращается к устройству ввода-вывода, другая программа сразу же загружается из памяти(подгружается), и ЦП выполняет её, и так далее:

Схема операционной системы многопрограммного выполнения задач.

При последовательном выполнении программ, когда P1 выполняет свой ввод-вывод, центральный процессор остаётся в простое. Только после завершения P1, в процессор загружается P2. Общее время завершения для всех задач в приведённом примере, при последовательном выполнении, составит 12 секунд, как показано на диаграмме времени ниже:

Диаграмма Ганта для ОС последовательного выполнения задач.

В многопрограммной ОС, когда P1 обращается к устройствам ввода-вывода, P2 загружается в ЦП, а когда P2 обращается к ввода-вывода, P3 загружается в ЦП и так далее. Общее время завершения для всех процессов для предыдущего примера при выполнении в многопрограммной ОС сокращается и составит 10 секунд, как показано на диаграмме времени ниже:

Диаграмма Ганта для ОС последовательного многопрограммного выполнения задач.

При таком планировании выполнения программ, ЦП никогда не остаётся в неактивном состоянии. Многопрограммность направлена на то, чтобы ЦП и устройства ввода-вывода, были занятыми постоянно, что приводит к максимально эффективному их использованию. Такие Операционные Системы как Microsoft Windows и Apple Mac OS являются Мультипрограммными.

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

Многозадачность ОС осуществляемая посредством разделения во времени

Многозадачная операционная система является логическим продолжением многопрограммности. Она использует как концепции многопрограммности, так и разделения во времени. Как следует из названия, одновременно выполняются несколько задач или программ. Например, возможно прослушивание музыки, в то время как вы вводите текст в WordPad и просматриваете интернет-страницу.

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

По истечении четырёх наносекунд процесс покидает ЦП, начинается обработка другого процесса, который и занимает ЦП. Этот цикл продолжается до тех пор, пока все процессы, которые выполняются, не исчерпают свой временной интервал.

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

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

Давайте рассмотрим три процесса: P1, P2 и P3, время обработки P1 = 2 нс, P2 = 4 нс и P3 = 6 нс. При этом временной квант составляет 2 нс. В первом цикле выполняются P1, P2 и P3. Во втором цикле выполняются P2 и P3, поскольку P1 завершён. Этот цикл продолжается до тех пор, пока все процессы не будут завершены. График Ганта представлен ниже:

Диаграмма Ганта для Многозадачной ОС с разделением времени выполнения.

Итак, Многозадачность = Многопрограммность + Разделение времени

Многозадачность использует концепцию переключения контекста, что означает замену задачи в ЦПУ, когда её время исполнения (максимально отведённое время) истекает, то внедряется следующая задача.

Многопоточная операционная система

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

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

Веб Обозреватель - это процесс П1, который может содержать несколько потоков, таких как ПП1.1, ПП1.2, ПП1.3, ПП1.4 и так далее. Многопоточность аналогична многозадачности, поскольку она позволяет обрабатывать несколько потоков одновременно, а не несколько процессов.

На рисунке ниже показано функционирование многопоточной операционной системы:

Многопоточная работа Операционной Системы.

МногоПроцессорная операционная система

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

Предположим, что процессы П1, П2, П3 и П4 ожидают выполнения. Если это двухъядерный процессор (два процессора), то два подпроцесса могут выполняться одновременно, и система будет работать вдвое быстрее, чем однопоточный процессор (один процессор).

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

Распределённая операционная система

В распределённой ОС данные хранятся и обрабатываются в нескольких разных местах или локализациях, соединённых через сеть. Все компьютеры, подключенные через сеть, имеют одну и ту же операционную систему. Распределённая операционная система (англ. DOS - Distributed operating system) направлена на улучшение управления аппаратными ресурсами. В сетевой операционной системе операционные системы на каждом компьютере могут быть разными, но в распределённой системе они одинаковы. Некоторые примеры DOS включают Solaris, Micros и Mach. На рисунке ниже показана распределённая операционная система:

Распределённая операционная система.

Сетевая операционная система

Эта система соединяет компьютеры в разных местах через сеть. Все компьютеры, подключенные к сети, могут иметь свою собственную операционную систему. В отличие от распределённой операционной системы, Сетевая операционная система может быть одинаковой или различной. Сетевая операционная система (англ. NOS - Network operating system) предназначена для обслуживания нескольких клиентов удалённо. Примеры NOS: Windows NT, Novell, UNIX и т.д.

Сетевые операционные системы используются в гетерогенных компьютерах и известны как слабо связанные системы. С другой стороны, Распределённые операционные системы представляют собой тесно связанные системы, которые обычно используются в однородных компьютерах или многопроцессорных системах.

Компьютеры, на основе Распределённых операционных систем, могут общаться друг с другом через общую память или сообщения. Однако Сетевая операционная система использует передачу файлов для упрощения общения между компьютерами.

В отличие от Распределённой операционной системы, которая обрабатывает ресурсы, такие как память, ЦП и так далее, глобально, Сетевая операционная система заботится об этих ресурсах на каждом компьютере индивидуально. На рисунке показана Сетевая операционная система:

Сетевая операционная система.

Операционная система реального времени

Система реального времени – это система с жёсткими временными рамками, которая имеет временные ограничения для завершения процессов в системе. Операционная система должна завершить процесс в пределах заданного временного ограничения. В противном случае система выйдет из строя. Эта операционная система предназначена для приложений реального времени. Компьютер симулирует все временные события с той же скоростью, что и в реальной жизни.

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

Трансляторы языков

Это Переводчики или Обработчики языков, они переводят или конвертируют программу с одного языка программирования, на другой. Например, с языка высокого уровня (англ. HLL - high-level language) на язык низкого уровня (англ. LLL - low-level language). На рисунке ниже показан процесс работы переводчика языка:

Процесс трансляции одного ЯП(языка программирования) в другой ЯП.

Программа, написанная на языке программирования, известна как исходный код; когда она переводится на машинный язык, она становится объектным кодом. Языковой транслятор преобразует исходный код в объектный код.

В общем виде существуют три типа языковых переводчиков, а именно:

  1. Компилятор: это программа, которая преобразует язык высокого уровня (HLL) в язык низкого уровня (LLL). Она принимает полный HLL-программный код на вход, преобразует его в язык низкого уровня и уведомляет об ошибках и предупреждениях, если таковые имеются. (англ. Compiler)
  2. Интерпретатор: Он преобразует программу из высокоуровневого языка (HLL) в низкоуровневый язык (LLL) построчно; одно выражение переводится и выполняется нёмедленно. (англ. Interpreter)
  3. Ассемблер: Ассемблер - это программа, которая переводит программу из ассемблера в машинный код или LLL. (англ. Assembler)

Дополнительно существуют ещё программы помогающие в написании и выполнении программ:

Вредоносное ПО

Слово "вредоносное ПО" происходит от двух слов: "вредоносный" и "программное обеспечение". Это вредоносное программное обеспечение. Оно предназначено для повреждения и уничтожения компьютеров и компьютерных систем. Это не легитимное программное обеспечение. Основная концепция различных видов вредоносного ПО приведена ниже:

Заключение

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

Важные моменты

Важные вопросы

  1. Опишите разницу между аппаратным и программным обеспечение компьютера.
  2. Что такое программное обеспечение? Напишите его виды.
  3. Что такое операционная система? Приведите её пример.
  4. Объясните роль операционной системы. Почему её называют менеджером ресурсов?
  5. Приведите несколько примеров компьютерного программного обеспечения.
  6. Опишите разницу между исходным кодом и объектным кодом.
  7. Опишите разницу между компиляторами и интерпретаторами.
  8. Что такое прикладное программное обеспечение? Приведите примеры.
  9. Что такое BIOS?
  10. Можете объяснить, что такое утилитарное программное обеспечение? Является ли оно необходимым программным обеспечением для установки пользователями в их компьютеры?
  11. Напишите короткую заметку о различных типах операционных системах.
  12. Напишите о различиях между прикладным и системным программным обеспечением.
  13. Что такое вредоносное ПО? Объясните его виды.
  14. Что такое языковой транслятор? Напишите краткое объяснение об этих понятиях:
    • Компилятор
    • Интерпретатор
    • Ассемблер
    • Редактор
    • Компоновщик
    • Загрузчик
  15. Напишите о различиях между сетевыми операционными системами и распределенными операционными системами.
  16. Что такое многозадачная операционная система?
  17. Напишите о различиях между многопрограммностью, многозадачностью и многопроцессорностью в ОС.
  18. Что вы понимаете под операционной системой пакетной обработки?
  19. Какова разница между Символьным Интерфейсом и Графическим Интерфейсом пользователя.
  20. Напишите о различиях между открытым и закрытым программным обеспечением.
  21. В чем разница между вирусами, червями и троянскими программами?

Теперь мы приступим к изучению различных типов систем счисления, таких как десятичная, двоичная, восьмеричная и шестнадцатеричная. Также будет уделено внимание преобразованию одной системы счисления в другую. Будут рассмотрены и другие известные двоичные кодирования на основе ASCII, BCD, EBCDIC, Excess-3 и кодов Грея.

Представление Чисел внутри Процессора и Памяти

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

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

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

Данные это Числа

Система счисления – это метод представления чисел с помощью набора символов и правил. Каждая числовая система определяется с помощью своей основы или радикса, который представляет собой общее количество символов, используемых в числовой системе. Например, в двоичной числовой системе основание равно 2, а в десятичной системе – 10.

Десятичные числа

Основание числа - это положительное целое число с основанием N, которое содержит в своих разрядах любые цифры меньше N.

Десятичные цифры, имеют основание 10, следовательно цифрами: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 можно представить любой разряд десятичного числа.

Пример десятичных чисел:

0, 456, 12988, 67890, 45433, 9

В нашей повседневной жизни мы пользуемся десятичными числами чаще всего.

Двоичные числа

Если число имеет основание 2, тогда любой его разряд может иметь цифру либо 0 либо 1.

Пример двоичных чисел:

0, 10, 11, 111, 0101, 11001100

Восьмиричные числа

Если число имеет основание 8, тогда любой его разряд может иметь цифры 0, 1, 2, 3, 4, 5, 6, 7.

Пример восьмиричных чисел:

0, 77, 67, 125, 7654, 7437

Шестнадцатиричные числа

Если число имеет основание 16, тогда любой его разряд может иметь цифры 0,1,2,3,4,5,6,7,9,A,B,C,D,E,F.

Да, для обозначения десятичных чисел 10, 11, 12, 13, 14, 15 используют шесть первых букв латинского алфавита: A, B, C, D, E, F

Соответственно: 10 это A, 11 это B, 12 это C, 13 это D , 14 это E , 15 это F.

Пример шестнадцатиричных чисел:

1, 0C, AB77, 12FE, 125D, 9A6C5E4, 0000FFFF, F

В программе С мы можем записывать шестнадцатиричные числа так:

Преобразование чисел из одного основания в другое

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

Предположим, что мы имеем некое число с определённым основанием b. Это число состоит из нескольких разрядов:

Число из N-разрядов с основанием b
b b ... b b b Значение разряда числа
n-1 n-2 ... 2 1 0 Номер разряда числа

Допустим что это Двоичное число. Преобразование данного числа в десятичное число делается по формуле:

b×2n-1 + b×2n-2 + ... + b×22 + b×21 + b×20 = десятичное число

Практический пример: Необходимо двоичное число 110110 преобразовать в десятичное. Делается это так:

Двоичное число 110110
1 1 0 1 1 0 Значение разряда числа
5 4 3 2 1 0 Номер разряда числа

Согласно формуле выше - подставим степени двойки, которые являются указателями номеров разрядов двоичного числа, начиная с нулевого:

1×25 + 1×24 + 0×23 + 1×22 + 1×21 + 0×20

Тогда получим:

1×32 + 1×16 + 0×6 + 1×4 + 1×2 + 0×1

Тогда получим:

32 + 16 + 0 + 4 + 2 + 0

Ответ:

54 (десятичное число).

Преобразование Двоичных чисел в Восьмиричные

Возьмём двоичное число 11010001101011:

Двоичное число 11010001101011
1 1 0 1 0 0 0 1 1 0 1 0 1 1 Значение разряда числа
13 12 11 10 9 8 7 6 5 4 3 2 1 0 Номер разряда числа

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

11 010 001 101 011

Крайняя левая группа не полная, дополним слева её нулём так:

011 010 001 101 011

Теперь, преобразуем эти двоичные группы из трёх разрядов в десятичные числа:

011(3) 010(2) 001(1) 101(6) 011(3)

Мы получили восьмиричное число 32163

Преобразование Двоичных чисел в Шестнадцатиричные

Возьмём двоичное число 11010001101011:

Двоичное число 11010001101011
1 1 0 1 0 0 0 1 1 0 1 0 1 1 Значение разряда числа
13 12 11 10 9 8 7 6 5 4 3 2 1 0 Номер разряда числа

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

11 0100 0110 1011

Крайняя левая группа не полная, дополним слева её двумя нулями так:

0011 0100 0110 1011

Теперь, преобразуем эти двоичные группы из четырёх разрядов в десятичные числа:

0011(3) 0100(4) 0110(6) 1011(11 это B)

Мы получили шестнадцатиричное число 346B

Преобразование Десятичных чисел в Двоичные

Данное преобразование строится на принципе остатка от деления. Метод такой:

Например возьмём десятичное число 351 и преобразуем его в двоичное.

Делим 351 на 2 и получаем остаток 1 (351/2 = 175 и 1 в остатке). Это самый младший разряд нашего будущего двоичного числа, т.е. при записи двоичного числа оно будет крайним ПРАВЫМ разрядом.

Делим 175 на 2 и получаем остаток 1 (175/2 = 87 и 1 в остатке). Это следующий перед младшим СЛЕВА разряд нашего будущего двоичного числа, т.е. при записи двоичного числа оно будет ВТОРЫМ С ПРАВА разрядом.

Продолжаем деление и запись остатков пока получается делить. Когда уже нет возможности делить на 2 - тогда мы достигли нашего самого старшего двоичного разряда, т.е. при записи двоичного числа оно будет крайним СЛЕВА разрядом.

Визуализация процесса преобразования Десятичного числа в Двоичное.

Наконец то, мы собираем все двоичные разряды с права на лево (см. изображение) согласно положению каждого разряда двоичного числа :

101011111

Преобразуйте двоичное число 111011011 в десятичное самостоятельно.

Преобразование Восьмиричных чисел в Двоичные

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

Пример, преобразования восьмиричного числа 763015 в двоичное:

Каждый разряд преобразуем в двоичное число:

7(111) 6(110) 3(11) 0(0) 1(1) 5(101)

Дополним нули слева:

7(111) 6(110) 3(011) 0(000) 1(001) 5(101)

Соединяем двоичные числа один рядом с другим и получаем готовое двоичное число:

111110011000001101

Преобразование Шестнадцатиричных чисел в Двоичные

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

Пример, преобразования шестнадцатиричного числа 0x2AF894 в двоичное:

Каждый разряд преобразуем в двоичное число:

2(10) A(1010) F(1111) 8(1000) 9(1001) 4(100)

Дополним нули слева:

2(0010) A(1010) F(1111) 8(1000) 9(1001) 4(0100)

Соединяем двоичные числа один рядом с другим и получаем готовое двоичное число:

001010101111100010010100

Преобразуйте Шестнадцатиричное число 0xF9D76A в Двоичное самостоятельно.

Преобразование Шестнадцатиричных чисел в Десятичные

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

Шестнадцатиричное число 0xAAD9FF
A A D 9 F F Значение разряда
5 4 3 2 1 0 Номер разряда

Пример, преобразования шестнадцатиричного числа 0xAAD9FF в двоичное:

Согласно формуле подставляем значения:

A×165 + A×164 + D×163 + 9×162 + F×161 + F×160 = Десятичное число

Заменим латинские буквы десятичными числами:

10×165 + 10×164 + 13×163 + 9×162 + 15×161 + 15×160 = Десятичное число

10×1048576 + 10×65536 + 13×4096 + 9×256 + 15×16 + 15×1 = Десятичное число

10485760 + 655360 + 53248 + 2304 + 240 + 15 = 11196927

Преобразуйте шестнадцатиричное число 0xC1E53B в десятичное самостоятельно.

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

Положительные целые числа

Эти числа также имеют название беззнаковые целые числа(англ. unsigned integer ).

Внутри памяти и процессора все числа всегда представлены в виде двоичных чисел.

Пример, положительное шестнадцатиричное число 0xFF эквивалентно десятичному числу 255, эквивалентно восьмиричному числу 0377, и все они представлены в компьютере как двоичное число 11111111.

Форма числа - дополнение к двум

Чтобы привести двоичное число к форме дополнения к двум, необходимо начать с крайнего ПРАВОГО разряда и продвигаться вперёд НАЛЕВО - к старшему разряду.

Если первая цифра 0 - пропускаем её не изменяя.

Если следующая цифра 0 - пропускаем её не изменяя.

Двигаемся в право пока не встретим 1 - пропускаем её не изменяя.

Все последующие цифры меняем на противоположные - вместо 0 ставим 1, а вместо 1 ставим 0.

Пример, преобразовать двоичное число 10101110001000 в дополненное к двум:

Визуализация процесса дополнения двоичного числа к двум.

Ещё пример, преобразовать двоичное число 00000000000001 в дополненное к двум:

Визуализация процесса дополнения двоичного числа к двум.

Ещё пример, преобразовать двоичное число 11111111111111 в дополненное к двум:

Визуализация процесса дополнения двоичного числа к двум.

Отрицательные целые числа (англ. negative integers)

Представим некое отрицательное целое десятичное число N, мы уже узнали и увидели, что любое десятичное число внутри компьютера представлено в виде строки битов(последовательности разрядов двоичного числа). Т.е. все числа представлены в вычислительной машине как последовательность(строка) битов, расположенных по старшинству разрядов - с ПРАВА на ЛЕВО. Справа - самый младший разряд, Слева - самый старший.

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

Давайте представим два десятичных числа 17 и -17 в двоичном виде.

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

Для представления десятичного числа 17 нам этих бит хватает:

17 в двоичной 10001. Дополним справа нулями для целостности байта:

17 в двоичной 00010001. Всё просто.

Теперь представим десятичное целое отрицательное число -17 в двоичном виде:

Возьмём двоичное представление числа 17(целое беззнаковое), и преобразуем по форме дополнения к двум:

00010001 в дополнении к двум будет иметь вид 11101111.

Битовая строка размером восемь бит(байт) 11101111 представляет в компьютере десятичное отрицательное целое число -17.

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

Например, двоичная строка из восьми бит(байт) вида 11111111 - представляет некое отрицательное число. Какое?

Применим преобразование по дополнению к двум:

11111111 дополнение к двум имеет вид 00000001.

Далее преобразуем двоичное число в десятичное, это 1.

Значит двоичное целое отрицательное число вида 11111111 это -1.

Ещё пример, двоичная строка из восьми бит(байт) вида 11111001 - представляет некое отрицательное число. Какое?

Применим преобразование по дополнению к двум:

11111001 дополнение к двум имеет вид 00000111.

Далее преобразуем двоичное число в десятичное, это 7.

Значит двоичное целое отрицательное число вида 11111001 это -7.

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

Всё довольно просто - Типы данных.

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

Все Типы данных в языке С имеют свои идентификаторы типа - однозначно указывающие компилятору - с каким типом данных и какие действия будет выполнять машина. Но об этом чуть позже.

Дробные числа

Двоичное дробное число состоит из двух частей: числа перед десятичной точкой и числа после десятичной точки, например, 1110.101

Где, 1110 является целым числом перед десятичной точкой, а 101 является дробной частью после десятичной точки (точки).

Преобразование Двоичных Дробных чисел в Десятичные Дробные

Выполним следующие шаги преобразования:

  1. Измените всю часть двоичного числа на его эквивалентное десятичное, как обычно.
  2. Выполните следующие шаги, чтобы преобразовать дробную часть двоичного числа в её десятичный эквивалент:
    • Разделите каждую цифру на 2.
    • Примените инкрементальную степень, начиная с 1 до знаменателя, то есть 2-1, слева направо.
    • Добавьте результат, чтобы получить дробную часть числа.
  3. Сложите целую и дробную части десятичного числа.

Пример преобразования 10101.1101 в десятичную дробь:

  1. Преобразуйте всю часть двоичного числа в её эквивалент в десятичной системе, как обычно. Число перед дробью (целое число) = 10101:
    (1) 1×24 + 0×23 + 1×22 + 0×21 + 1×20
    (2) 16 + 0 + 4 + 0 + 1 = 21
    
  2. Преобразуйте дробную часть двоичного числа в десятичную. Число после дробной части(дробная часть) = 1101:
    (1) 1×2-1 + 1×2-2 + 0×2-3 + 1×2-4
    (2) 1×1/2 + 1×1/4 + 0×1/8 + 1×1/16
    (3) 1×.5 + 1×.25 + 0×.125 + 1×.0625
    (4) .5 + .25 + 0 + .0625 = .8125
  3. Сложите целую и дробную части десятичного числа:
    (1) 21 + .8125
    (2) 21.8125
    Итак, Двоичная дробь 10101.1101 = Десятичной Дроби 21.8125

Преобразование Десятичных Дробных чисел в Двоичные Дробные

Десятичное дробное число состоит из двух частей, например, 45.54. Где, 45 является целым числом перед десятичной точкой, а 0.54 является дробной частью после десятичной точки.

Выполним следующие шаги:

  1. Преобразуйте целую часть десятичного числа в двоичное представление, как обычно.
  2. Следуйте данным шагам, чтобы преобразовать дробную часть десятичного числа в её двоичный эквивалент:
    1. Умножьте её на 2
    2. Соберите целую часть результата, то есть цифру перед точкой.
    3. Напишите цифру сверху вниз (смотрите пример).
  3. Сложите целую и дробную части двоичного числа, чтобы получить окончательный результат.

Пример преобразования 45.54 в двоичное дробное число:

  1. Преобразуйте целую часть десятичного числа в двоичный вид, как обычно. Число перед точкой(целое число) = 45:
    (1) 45/2 = 1 в остатке
    (2) 22/2 = 0 в остатке
    (3) 11/2 = 1 в остатке
    (4)  5/2 = 1 в остатке
    (5)  2/2 = 0 в остатке
    (6)        1 в остатке
    
    Десятичное 45 = двоичному 101101.
  2. Преобразуем дробную часть в двоичное представление. Здесь сразу обозначим, чтобы преобразовать десятичную дробную часть в двоичную, необходимы выбрать предел точности преобразования. Так если мы будем брать предел точности в пять знаков после точки, то и количество шагов преобразования будет не более пяти. Число после точки(дробная часть) = .54:
    (1) .54×2 = 1.08 - Перед точкой мы получили 1, записываем её, и далее не используем.
    (2) .08×2 = 0.16 - Перед точкой мы получили 0, записываем его, и далее не используем.
    (3) .16×2 = 0.32 - Перед точкой мы получили 0, записываем его, и далее не используем.
    (4) .32×2 = 0.64 - Перед точкой мы получили 0, записываем его, и далее не используем.
    (5) .64×2 = 1.28 - Перед точкой мы получили 1, записываем её. Здесь заканчиваем преобразование.
    Десятичная дробная часть .54 = двоичной дробной части .10001
  3. Сложите целую и дробную части двоичного числа числа:
    (1) 101101 + .10001
    (2) 101101.10001
    Следовательно, 45.54 приблизительно равно 101101.10001. Если вы преобразуете это двоичное дробное число в десятичное дробное вы увидите результат 45.53125, что близко к 45.54, а если увеличить точность преобразования дробной части до семи знаков так 101101.1000101, то мы получим число 45.5390625, которое значительно ближе к 45.54. Именно по этому, в цифровой технике и вычислениях так важен предел точности преобразования, чтобы результаты вычислений всегда были в допустимых пределах погрешности.
Восьмеричные дробные числа и Шестнадцатеричные дробные числа преобразуются в десятичные дробные числа также как и двоичные дробные числа, просто изменяем основание на 8 для восьмеричных и 16 для шестнадцатеричных.

Пример: Преобразуйте десятичное 139.29 в восьмеричное число.

  1. Преобразуйте целую часть десятичного числа в восьмеричное, как обычно (см. преобразование из десятичной системы в восьмеричную). Число перед дробной частью (целое число) = 139:
    (1) 139/8 = 3 в остатке
    (2)  17/8 = 1 в остатке
    (3)     2 = 2 в остатке
    Следовательно 139 в десятичной = 213 в восьмеричной.
  2. Преобразуйте дробную часть в восьмеричный эквивалент. Число после дроби (дробная часть) = .29:
    (1) .29×8 = 2.32 - Перед точкой мы получили 2, записываем её, и далее не используем.
    (2) .32×8 = 2.56 - Перед точкой мы получили 2, записываем его, и далее не используем.
    (3) .56×8 = 4.48 - Перед точкой мы получили 4, записываем его, и далее не используем.
    Следовательно .29 в десятичной = .224 в восьмеричной. С целью увеличения точности мы можем продолжать эти шаги.
  3. Соединяем целую и дробную части восьмеричного числа:
    (1) 213 + .224
    (2) 213.224
    Итак десятичное дробное число 139.29 = 213.224 в восьмеричной системе исчисления.

Пример: Преобразуйте восьмеричное 1047.1365 в десятичное число.

  1. Преобразуйте целую часть восьмеричного числа в десятичное, как обычно (см. преобразование из восьмеричную системы в десятичную). Число перед дробной частью (целое число) = 1047:
    (1) 1 × 83 + 0 × 82 + 4 × 81 + 7 × 80 + 1 × 8-1 + 3 × 8-2 + 6 × 8-3 + 5 × 8-4
    (2) 512 + 0 + 32 + 7 + 1/8 + 3/64 + 6/512 + 5/4096
    (3) 551 + .125 + .0468 + .0117 + .0012
    (4) 551.1847
    Следовательно 1047.1365 в восьмеричной = 551.1847 в десятичной.

Пример: Преобразуйте десятичное 2063.4799 в шестнадцатеричное число.

  1. Преобразуйте целую часть десятичного числа в шестнадцатеричное, как обычно. Число перед дробной частью (целое число) = 2063:
    (1) 2048/16 = 15 в остатке (15 = F)
    (2)  128/16 = 0 в остатке
    (3)       8 = 8 в остатке
    Следовательно 2063 в десятичной = 80F в шестнадцатеричной.
  2. Преобразуйте дробную часть в шестнадцатеричный эквивалент. Число после дробной части (дробная часть) = .4799
    (1) .4799×16 = 7.6784 - Перед точкой мы получили 7, записываем её, и далее не используем.
    (2) .6784×16 = 10.8544 - Перед точкой мы получили 10 (10 = A), записываем её, и далее не используем.
    (3) .8544×16 = 13.6704 - Перед точкой мы получили 13 (13 = D), записываем её, и далее не используем.
    Следовательно .4799 в десятичной = .DA7 в шестнадцатеричной.
  3. Соединяем целую и дробную части шестнадцатеричного числа:
    (1) 80F + .DA7
    (2) 80F.DA7
    Итак десятичное дробное число 2063.4799 = 80F.DA7 в шестнадцатеричной системе исчисления.

Регистры

Регистр - это элемент создания блока данных в памяти компьютера. Регистры хранят в себе исключительно битовые строки - т.е. последовательности бит представляющих числа и ничего более.

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

Чаще всего говорят о размере регистра - что означает: количество бит, которые он может хранить в себе.

Один бит - это минимальная ячейка памяти, которая может сохранять информацию, либо 0 либо 1.

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

Восемь бит - один байт.

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

Но такое позиционирование имеет отношение только к битам.

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

Итак, мы теперь знаем что Регистр это 8 бит или 1 байт.

Каждый байт размещён в памяти. У каждого байта есть свой адрес в памяти.

Адрес в памяти представлен в виде Шестнадцатиричного числа.

Говорят что адрес указывает на регистр/байт.

Рассмотрим два вида определения старшего и младшего байта адресов в памяти. Обычно адреса в памяти имеют размер 16-байт.

Вот наглядное представление размещения байтов данных в памяти.

Шестнадцатиричное число 0xABCDEF9012345678 размером в 8 байт(один байт - два разряда числа), записано в памяти по принципу младший байт в меньшем адресе, старший байт в большем адресе . Т.о. на картинке представлено восемь байт по восемь бит в каждом в шестнадцатиричном виде.

Если мы скажем компьютеру в нашей программе на С, что в памяти хранятся два числа по 4-ре байта каждый, какие это будут числа?

Опять же, данные записаные в памяти по принципу младший байт в меньшем адресе, старший байт в большем адресе . Т.о. на картинке представлены будут два числа по четыре байта по восемь бит в каждом в шестнадцатиричном виде. И будут иметь следующий вид: Число №1 это 0x12345678, Число №2 это 0xABCDEF90

Для Числа №1 (0x12345678) младший байт находится по меньшему адресу в памяти: По адресу 0x000000000065FE18 хранится байт 0x78, а старший байт 0x12 хранится по адресу 0x000000000065FE1B.

Для Числа №2 (0xABCDEF90) младший байт находится по меньшему адресу в памяти: По адресу 0x000000000065FE1C хранится байт 0x90, а старший байт 0xAB хранится по адресу 0x000000000065FE1F .

Определите какие числа хранятся в памяти, если мы напишем код С, в котором укажем, что в памяти хранятся беззнаковые(положительные) целые числа размером два байта? Сколько в памяти таких чисел? Преобразуйте все эти шестнадцатиричные числа в десятичные.

Определите какие числа хранятся в памяти, если мы напишем код С, в котором укажем, что в памяти хранятся целые числа со знаком(отрицательные) и размером два байта? Сколько в памяти таких чисел? Преобразуйте все эти шестнадцатиричные числа в десятичные.

Напомню вам, что программист указывает какой тип данных хранится в памяти машины. Для указания Типа данных в языке применяются идентификаторы типа. Скоро мы их изучим.

Связки в Памяти

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

Обычно, адреса в памяти это шестнадцатиричные числа.

Подробное изучение принципа хранения целых чисел в памяти

Мы рассмотрим так называемые концепции Big-Endian, и Little-Endian.

Итак, все данные хранятся в памяти - в регистрах. Каждый регистр имеет собственный адрес в памяти.

Концепция Big-Endian - Старший байт числа первым записывается в память.

Концепция Little-Endian - Младший байт числа первым записывается в память.

Первым записывается в память - значит использует адрес меньшего значения.

Давайте рассмотрим оба варианта хранения чисел в памяти.

У нас есть некое шестнадцатиричное число 0x1245A78F.

На С это можно написать такой строкой кода:

int A = 0x1245A78F;

Сравните адреса регистров, в которых хранятся данные - число 0x1245A78F:

Самостоятельно разместите байты данных по адресам в памяти, шестнадцатиричного целого знакового 6-ти байтового числа для каждого вида хранения:

int A = 0xABCDE789AA56;

Заключение

Сохранение значений переменных

Здесь показывается, как сохранять, получать и манипулировать различными типами данных с использованием контейнеров переменных в программах на языке C.

Создание переменных в программе

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

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

<тип-данных> <имя-переменной>;

Сразу после выполнения утверждения - объявления переменной, Операционная Система, выделяет где-то в памяти машины фрагмент памяти равный размеру обусловленного её типом. И автоматически, имени переменной присваивается младший адрес ячейки памяти с которой начинается значение переменной. Т.е. имя переменной (не очевидным образом) становится обладателем и адреса хранения значения в памяти.

При объявлении переменной вначале указывается тип данных, который будет храниться в переменной. Можно выбрать один из типов, описанных ниже. После типа данных следует пробел, а затем — выбранное имя переменной, соответствующее соглашениям, представленным выше. Как и любое другое утверждение языка C, объявление переменной должно заканчиваться точкой с запятой ; . Несколько переменных, имеющих одинаковый тип данных, могут быть объявлены одновременно в виде списка через запятую:

<тип-данных> <имя-переменной1>, <имя-переменной2>, <имя-переменной3>;

Однако для простоты чтения кода рекомендуется писать каждую переменную в отдельной строке:

<тип-данных> <имя-переменной1>; 
<тип-данных> <имя-переменной2>; 
<тип-данных> <имя-переменной3>;

В языке C существуют четыре типа данных, которые применяются чаще остальных. Они определяются с помощью ключевых слов языка C отмеченных в таблице светло-зелёным:

Типы данных в языке С
Категория Ключевые слова Размер (обычно) Описание Пример
Целое знаковое int 2 или 4 байта Целые числа (отрицательные и положительные)
int a = -5;
Целое знаковое short int, short 2 байта Короткие Целые числа (отрицательные и положительные)
short b = 10;
Целое знаковое long int, long 4 байта (или 8 байт) Длинные Целые числа (отрицательные и положительные)
long c = 100000;
Целое знаковое long long int, long long 8 байт Очень Длинные Целые числа (отрицательные и положительные)
long long d = 1*pow(e, 12);
Целое беззнаковое unsigned int, unsigned 2 или 4 байта Только положительные целые числа
unsigned int e = 5;
Целое беззнаковое unsigned short int, unsigned short 2 байта Короткие положительные целые числа
unsigned short f = 20;
Целое беззнаковое unsigned long int, unsigned long 4 байта (или 8 байт) Длинные положительные целые числа
unsigned long g = 1*pow(e, 6);
Целое беззнаковое unsigned long long int, unsigned long long 8 байт Очень Длинные положительные целые числа
unsigned long long h = 1*pow(e, 15);
Символьное char 1 байт Символы (ASCII) или маленькие числа
char ch = 'C';
Символьное беззнаковое unsigned char 1 байт Беззнаковый символ
unsigned char uch = 255;
Символьное знаковое signed char 1 байт Знаковый символ
signed char sch = -127;
С плавающей точкой float 4 байта Числа с плавающей точкой одинарной точности
float f = 3.141592;
С плавающей точкой double 8 байт Числа с плавающей точкой двойной точности
double d = 2.718281828;
С плавающей точкой long double 10-16 байт Числа с плавающей точкой расширенной точности
long double ld = 1.234567e+30;
Специальный тип void - Отсутствие типа данных
void function(void) {...}
Указатели <тип> * 4 или 8 байт Адрес в памяти, указывает на данные любого выбранного типа
int *ptr = &ch;

Типы данных выделяют разные объёмы машинной памяти для хранения данных. Самый малый объём — у типа char, для этого типа выделяется всего один байт памяти. Самый большой объём выделяется для типа long double — от 10 до 16 байт(в зависимости от машины, операционной системы и компилятора).

Значения типа char(одиночный символ) должны быть заключены в одинарные кавычки. Использование двойных кавычек предназначено только для строк(два и более символов).

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

В примере кода, ниже, различные переменные объявляются, а затем и инициализируются подходящими значениями, что описано в комментариях к коду — описательных текстах, заключенных между символами /* и */, которые компилятор игнорирует:

int num1;          /* Объявляем целочисленную переменные */
int num2;          /* Объявляем целочисленную переменные */
char letter;       /* Объявляем символьную переменную */
float decim = 7.5; /* Объявляем и инициализируем переменную с плавающей точкой */
num1 = 100;        /* Инициализируем целочисленную переменную */
num2 = 200;        /* Инициализируем целочисленную переменную */
letter = 'A';      /* Инициализируем символьную переменную */
Кстати, Спецификация языка явно не определяет длину имени переменной. Однако большинство компиляторов C накладывают практическое ограничение на максимальную длину имени переменной, обычно от 31 до 63 символов.

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

Типичные ошибки именования переменной.
Имя переменной Ошибка именования Исправление
A Хотя это допустимое(валидное) имя, оно слишком короткое и годится только для коротких учебных программ. Apples
Simple interest Пробел нельзя ставить. Simple_interest
2one С цифры имена не разрешается начинать писать. Two_one
simple@interest Специальные символы не разрешается использовать. simpleInterest

Значение переменной заданное числом называется константой и остаётся фиксированным на протяжении всего выполнения программы, пока мы не изменим его в программе. В утверждениях int a = 10; или float b = 12.5;, например, числа 10 и 12.5 являются целочисленной и дробной константами соответственно. Константа также известна как C-литерал. Константа бывает следующих четырёх основных видов:

Идентификаторы

Идентификаторы — это слова, определяемые пользователем. Это имя, обозначающее конкретную область памяти. Эти слова используются для именования переменных, функций, массивов, структур и т.п.. Правила для построения идентификаторов в языке C следующие:

Операторы

Операторы — это символы, которые выполняют операции над операндами. Например, в выражении a = b + c;, 'a', 'b' и 'c' — это операнды, где '=' и '+' — это операторы. Ниже приведены примеры некоторых операторов языка С:

Специальные символы

Специальные символы разделяют различные части программы или попросту обеспечивают её структурную организацию. Примеры специальных символов:

Опишем основные символы языка C в виде таблицы:

Название символов в языке С
Символ Название Символ Название Символ Название
~ Тильда = Равно ; Точка с запятой
% Процент & Амперсанд ] Правая(Закрывающая) Прямоугольная Скобка
| Вертикальная линия $ Доллар ! Восклицательный знак
@ Эт. Символ Вставки / Прямой Слэш , Запятая
+ Плюс ( Левая(Открывающая) Круглая Скобка { Левая(Открывающая) Фигурная Скобка
< Меньше чем * Звёздочка ? Вопросительный знак
> Больше чем \ Обратный Слэш . Точка
_ Нижнее подчёркивание ) Правая(Закрывающая) Круглая Скобка } Правая(Закрывающая) Фигурная Скобка
- Минус ` Апостроф # Знак Числа
^ Курсор : Двоеточие ' Одинарная Кавычка
[ Левая(Открывающая) Прямоугольная Скобка " Двойная Кавычка

Структурная организация кода на языке С

Следующие строки кода показывают общую структуру программы на C. Программа на C обычно состоит из заголовочных файлов, комментариев, типов данных, функций, операторов ввода, операторов обработки и операторов вывода. Общий синтаксис самой простой программы на C выглядит следующим образом:

/* Комментарий */
#include <имя_заголовочного_файла1>
#include <имя_заголовочного_файла2>
#include "имя_заголовочного_файла_пользователя"

void main()
{
   Объявления переменных;
   Утверждения ввода данных;
   Утверждения обработки данных;
   Утверждения вывода данных;
}

Комментарии

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

Комментарий в С бывают однострочные и многострочные. Однострочные начинаются с символов "//". Многострочные обрамляют текст в символы "/* */":

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

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

Отображение значений переменных в консоли и не только

Значение переменной может быть отображено с помощью функции printf(), которая уже была использована нами ранее для отображения сообщения Hello World. Формат отображения значения переменной должен быть определён как аргумент функции printf(), располагающийся в скобках — сначала следует спецификатор формата в двойных кавычках, а затем после запятой — имя переменной. Завершение утверждения - точка с запятой ;

printf("<%спецификатор-формата-печати>", <имя-переменной>);

Где: число хранимое в переменной с именем - <имя-переменной>, форматируется(преобразуется) в специально заданный вид, указываемый спецификатором форматирования(формата отображения) - <%спецификатор-формата-печати>

Таблица спецификаторов форматов printf() в языке C
Спецификатор формата печати Описание. Т.е. мы говорим: Напечатай это как... Тип аргумента Пример использования
%d Знаковое десятичное число int printf("%d", -42);
%i Знаковое десятичное число (аналог %d, различие при scanf()) int printf("%i", -42);
%u Беззнаковое десятичное число unsigned int printf("%u", 42);
%o Беззнаковое восьмеричное число unsigned int printf("%o", 42);
%x Беззнаковое шестнадцатеричное (строчные) unsigned int printf("%x", 42);
%X Беззнаковое шестнадцатеричное (заглавные) unsigned int printf("%X", 42);
%f Число с плавающей точкой (десятичное) double printf("%f", 3.14);
%F То же, что %f, но может отображать "INF", "NAN" (заглавными)
%e Экспоненциальная форма (строчные, например 1.23e+02) double printf("%e", 123.0);
%E Экспоненциальная форма (заглавные, например 1.23E+02) double printf("%E", 123.0);
%g Автоматический выбор %f или %e double printf("%g", 123.0);
%G Автоматический выбор %F или %E double printf("%G", 123.0);
%c Один символ int (символ) printf("%c", 'A');
%s Строка char * (Указатель) printf("%s", "Hello");
%p Указатель (адрес) void * (Указатель) printf("%p", ptr);
%% Символ % printf("%%");
Обратите внимание: Единичные символы должны обрамляться в одинарные кавычки, а строки — в двойные.

Как вы видите, перед символами ставится знак процента(%), за которым следуют необязательные флаги, изменяющие формат вывода. Например, спецификатор формата %d можно модифицировать, используя следующие флаги:

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

#include <stdio.h>

int main()
{
   int num = 123;    /* Объявили и инициализировали переменную типа int */
   float fl = 3.14;  /* Объявили и инициализировали переменную типа float */
   char ch = 'C';    /* Объявили и инициализировали переменную типа char */
   printf("num = %d, fl = %f, ch = %c\n", num, fl, ch); /* %d для num, %f для fl, %c для ch */

   return 0;
}

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

Модификаторы длины(количество байт): Используются перед спецификатором для указания типа большего/меньшего размера
Модификатор Описание типа данных Использование со спецификатором формата
h short или unsigned short %hd, %hu
l long или unsigned long %ld, %lu
ll long long или unsigned long long %lld, %llu
z size_t %zu, %zd
j intmax_t или uintmax_t %jd, %ju
t ptrdiff_t %td, %tu
L long double %Lf

Замечания:

Не переживайте если что-то в таблицах вам сейчас не понятно. По мере обучения будут описаны все аспекты.

Количество разрядов числа

Спецификатор формата позволяет убедиться, что выходные данные займут определённый объём места при печати, если указать число сразу же после символа %. Например, чтобы убедиться, что целое число всегда будет иметь как минимум семь разрядов, следует использовать спецификатор %7d. Если необходимо заполнить всё свободное пространство нулями, между спецификатором и числом следует приписать 0. Например, чтобы убедиться, что целое число всегда будет как минимум семь разрядов, и при этом свободные разряды окажутся заполнены нулями, следует использовать спецификатор %07d .

Спецификатор точности, который представляет собой точку и число, может быть использован со спецификатором %f для указания отображаемого количества знаков после запятой. Например, чтобы отобразить только два разряда, следует использовать спецификатор %.2f . Спецификатор точности может быть использован вместе со спецификатором минимального пустого пространства, чтобы управлять как минимальным объёмом пустого места, так и количеством разрядов числа. Например, чтобы отобразить семь разрядов числа, включая два после запятой, и заполнить пустые разряды нулями, используйте спецификатор вида %07.2f .

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

Вот пример исходного кода:

#include <stdio.h>

int main()
{    
    int num = 123;
    float fl = 3.141592; /* Объявили и инициализировали переменную типа float
      с шестью знаками после точки */
    printf("num = %d, fl = %f\n", num, fl);     /* Печать без указания модификаторов длины */
    printf("num = %07d, fl = %.2f\n", num, fl); /* Печать с модификаторами печати символов */

    return 0;
}

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

  1. Начните новую программу с названием vars.c с инструкции препроцессора, чтобы включить в код стандартные библиотечные функции ввода/вывода :
    #include <stdio.h>
  2. Добавьте функцию main(), в которой объявляются и инициализируются две переменные :
    int main()
    {
       int num = 100;
       double pi = 3.1415926536;
    }
  3. В функции main() после объявления переменных добавьте утверждения, позволяющие вывести в консоль значения переменных в разных форматах:
    printf("Integer is %d\n", num);
    printf("Values are %d and %f\n", num, pi);
    printf("%%7d displays %7d\n", num);
    printf("%%07d displays %07d\n", num);
    printf("Pi is approximately %1.10f\n", pi);
    printf("Right-aligned %20.3f rounded pi\n", pi);
    printf("Left-aligned  %-20.3f rounded pi\n", pi);
    Напоминаем, чтобы отобразить символ % с помощью функции printf(), добавьте перед ним ещё один символ %, как это показано в примере.
  4. В конце функции main() добавьте финальное утверждение, которое возвращает 0, чего требует объявление функции:
    return 0;
  5. Теперь сохраните файл vars.c и затем в командной строке запишите команду компилятору скомпилировать код и затем запустите программу, чтобы увидеть, как значения переменных будут выведены в разных форматах.
Обратите внимание, что в случае, если указано меньше десятичных разрядов, чем того требует число, значение с плавающей точкой будет округлено, а не обрезано.

Ввод значений переменных в программу, пользователем из консоли

Строки — это особый случай. Работа с ними продемонстрирована в разделе, посвящённом работе с массивами, далее.

Стандартная библиотека функций ввода-вывода <stdio.h> предоставляет функцию scanf(), которая может использоваться для получения данных от пользователя. Функция scanf() требует, чтобы в неё передавали два аргумента, определяющие тип данных и адрес значения переменной в памяти, куда оно должно быть сохранено.

Первый аргумент функции scanf() должен быть одним из спецификаторов формата из таблицы, приведённой выше, он помещается в двойные кавычки. Например так, "%d", если вводится целочисленное значение. Вторым аргументом функции scanf() должно быть имя переменной, перед которым стоит символ &, если только вводится не строка. Символ & имеет несколько применений в программировании на языке C, но в этом контексте он используется как операция адресации, что значит, что вводимые данные должны храниться в том участке памяти, который был зарезервирован для этой переменной.

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

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

Визуальная аналогия. Коробки - ячейки памяти, Номера на коробках - Адреса ячеек, Предметы внутри коробок - Данные определённого типа.

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

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

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

Функции printf() и scanf() позволяют программам взаимодействовать с пользователем посредством консоли. Часто такие программы так и определяют, как консольные.

  1. Начните новую программу setvars.c с инструкции препроцессора, включающей стандартную библиотеку функций ввода/вывода:
    #include <stdio.h>
  2. Добавьте функцию main(), в которой объявляются три переменные:
    int main()
    {
       char letter;
       int num1, num2;
    }
  3. Далее в блоке функции main() после объявления переменных добавьте утверждения, позволяющие пользователю ввести данные:
    printf("Enter any one keyboard character: ");
    scanf("%c", &letter);
    printf("Enter two integers separated by a Space: ");
    scanf("%d %d", &num1, &num2);
  4. Теперь добавьте утверждения, печатающие в консоль сохранённые данные:
    printf("Numbers inputed: '%d' and '%d'\n", num1, num2);
    printf("Letter inputed: '%c' ", letter);
    printf("Stored at: %p\n", &letter);
  5. В конце блока функции main() верните значение 0, чего требует объявление функции:
    return 0;
  6. Сохраните файл программы setvars.c, а затем скомпилируйте и выполните программу, введя запрашиваемые программой данные:
Напоминаем, спецификатор формата для печати в консоль адреса ячейки памяти, где хранится значение переменной в компьютере, в шестнадцатеричном виде, это %p .

Стоит обратить ваше внимание на шестнадцатеричное значение адреса, который указывает на ячейку в памяти, где размещён введённый вами символ. Посмотрите сколько байт имеет адрес в вашем компьютере. Например результат выполнения программы на моём ПК отобразил значение адреса как 00000088E81FF9DF, что в десятичном виде это число 588 миллиардов 009 миллионов 961 тысяч 951 ! Значит ли это, что в моей машине 588 Гигабайт Оперативного Запоминающего Устройства (сокр. ОЗУ)?

Нет, не значит. Это и есть те абстракции с которыми работает компилятор и ваша программа в среде Операционной Системы машины. Это абстрактные адреса, в виртуальной памяти операционной системы. Именно по этому, программисты чаще всего управляют памятью не через эти значения, а через имена указателей адресов. Подробнее далее.

Спецификаторы типов данных

Числовая переменная типа int способна содержать положительные и отрицательные значения. Они называются знаковыми. Диапазон допустимых значений по умолчанию может определяться операционной системой как длинный или короткий.

Если переменная типа int по умолчанию создаётся как длинная (что более вероятно), она, как правило, может иметь значения в диапазоне от –2147483648 до +2147483647.

С другой стороны, если переменная типа int создаётся как короткая (что менее вероятно), она, как правило, способна иметь значения от –32768 до +32767.

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

short int num1; /* Позволяет экономить память, например в микроконтроллере */
long int num2;  /* Позволяет работать с крупными числами */

Библиотечный заголовочный файл <limits.h> содержит определяемые реализацией размеры некоторых типов данных. Они доступны через константы. Для переменных типа int, размер которых не указан, это INT_MAX и INT_MIN. Аналогично для переменных типа short int это переменные SHRT_MAX и SHRT_MIN, для переменных типа long intLONG_MAX и LONG_MIN.

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

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

Неотрицательные беззнаковые переменные типа int могут быть объявлены с помощью ключевого слова-спецификатора unsigned в случае, только если в будущем переменная никогда не получит отрицательное значение. Переменная типа unsigned short int, как правило, будет иметь диапазон 0...65535, при этом она займёт столько же места в памяти, сколько и обычная переменная типа short int. Переменная типа unsigned long int, как правило, будет иметь диапазон 0...4294967295, при этом она займёт столько же места в памяти, сколько и обычная знаковая переменная типа long int.

Объём памяти, выделяемый для переменной типа int неопределённого размера (без спецификаторов), зависит от настроек операционной системы, но часто по умолчанию выделяется столько же памяти, сколько и для переменной типа long int.

Функция sizeof() может быть использована для того, чтобы узнать, какой объём памяти резервируется под переменные разного типа. Рекомендуется использовать минимально возможный объём памяти. Например, если заранее известно, что переменная будет содержать только положительные значения меньше 65535, предпочтительнее использовать тип данных unsigned short int, который резервирует только 2 байта памяти, вместо long int, резервирующего 4 байта. Эта рекомендация особенно важна при программировании встраиваемых систем на основе микроконтроллеров - объём ОЗУ которых часто ограничен десятками или сотнями килобайт.

Формат печати
Спецификатор формата печати Описание Пример
%e Экспоненциальная запись числа в компьютере 3.402823e+38
  1. Начните новую программу sizeof.c с инструкций препроцессора, подключающих стандартные библиотеки функций ввода/вывода и констант:
    #include <stdio.h>
    #include <limits.h>
  2. Добавьте функцию main(), в которой содержатся утверждения, позволяющие вывести на экран размер и диапазон типа данных short int
    int main()
    {
       printf("Short int size: %d bytes\n", sizeof(short int));
       printf("from %d to %d\n\n", SHRT_MAX, SHRT_MIN);
    
       return 0;
    }
  3. Далее добавьте утверждения, позволяющие вывести на экран раз мер и диапазон типа данных long int
    printf("Long int size: %d bytes\n", sizeof(long int));
    printf("from %ld to %ld\n\n", LONG_MAX, LONG_MIN);
  4. Теперь добавьте утверждения, выводящие на экран размер типа данных char :
    printf("Char size: %d bytes\n", sizeof(char));
    printf("from %d to %d\n\n", CHAR_MAX, CHAR_MIN);
  5. Типа данных float :
    printf("Float size: %d bytes\n", sizeof(float));
    printf("from %e to %e\n\n", FLT_MAX, FLT_MIN);
  6. Типа данных double :
    printf("Double size: %d bytes\n", sizeof(double));
    printf("from %e to %e\n\n", DBL_MAX, DBL_MIN);
  7. Типа данных long double :
    printf("Long Double size: %d bytes\n", sizeof(long double));
    printf("from %e to %e\n\n", LDBL_MAX, LDBL_MIN);
  8. Добавьте финальное утверждение, чтобы вернуть значение 0, чего требует объявление функции.
    return 0;
  9. Visual Studio Code выдаёт предупреждение о том, что константы для него не известны. Это значит, что предварительно читается содержимое заголовочных файлов, если это содержимое содержит необходимую информацию используемую в исходном коде ниже, то компиляция может пройти успешно. В противном случаи необходимо подключить недостающие библиотеки. Итак, проверьте исходный код на наличие ошибок и предупреждений :
    Исходный код sizeof.c с предупреждением об отсутствии необходимого заголовочного файла <float.h>. Если навести курсор мыши на подчёркнутые константы, то нам подсветится подсказка.
    Так как константы для типов данных float, double и long double, с которыми мы работаем в нашей программе, не содержатся в заголовочном файле <limits.h> , а содержаться в заголовочном файле <float.h> , то его необходимо добавить как и предыдущие заголовочные файлы, как инструкцию препроцессору :
    #include <float.h>
  10. Сохраните файл программы, а затем скомпилируйте и выполните программу, чтобы увидеть размеры и диапазоны типов данных.
    Исправленный исходный код sizeof.c

    Для оформления вывода данных в консоль в более презентабельном виде, можно переписать код с помощью управляющих символов, пробелов и текста :

    Обратите внимание на то, как в данном примере используется управляющая последовательность символов табуляции \t.
  11. #include <stdio.h>
    #include <limits.h>
    #include <float.h>
    
    int main(){
       printf("Size of :\n");
       printf("Short int \t%d bytes \t", sizeof(short int));
       printf("from %d to %d\n", SHRT_MAX, SHRT_MIN);
    
       printf("Long int \t%d bytes \t", sizeof(long int));
       printf("from %ld to %ld\n", LONG_MAX, LONG_MIN);
    
       printf("Char \t\t%d bytes \t", sizeof(char));
       printf("from %d to %d\n", CHAR_MAX, CHAR_MIN);
    
       printf("Float \t\t%d bytes \t", sizeof(float));
       printf("from %e to %e\n", FLT_MAX, FLT_MIN);
    
       printf("Double \t\t%d bytes \t", sizeof(double));
       printf("from %e to %e\n", DBL_MAX, DBL_MIN);
    
       printf("Long Double \t%d bytes\t", sizeof(long double));
       printf("from %e to %e\n", LDBL_MAX, LDBL_MIN);
    
       return 0;
    }
    Улучшенный вид печати в консоль результата работы программы.

Опишем основные последовательности символов для оформления печати в виде таблицы:

Последовательности символов для оформления печати. Пробельные символы.
Символы Название Применение
\b Пустое пространство Перемещает курсор на предыдущее положение
\t Горизонтальная табуляция Вставляет горизонтальную табуляцию
\v Вертикальная табуляция Вставляет вертикальную табуляцию
\r Возврат каретки Перемещает курсор в начало строки
\f Перенос страницы Перемещает начальную позицию следующей страницы
\n Новая строка Переход на следующую строку
\\ Обратный слэш Представляет обратный слэш
\' Одинарная кавычка Представляет Одинарную кавычку
\" Двойные кавычки Представляет Двойные кавычки
\? Вопросительный знак Представляет Вопросительный знак
\0 Нуль(Null) Используется как символ NULL в строковом значении
\a Сигнал тревоги (Звонок) Генерирует звуковой сигнал

Понятие и Использование Глобальных переменных

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

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

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

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

Повсеместное использование глобальных переменных в коде может вызвать конфликты имён.

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

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

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

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

  1. Напишем новую программу global_1.c . Подключим стандартную библиотеку функций ввода/вывода.
    #include <stdio.h>
  2. Объявите и инициализируйте глобальную статическую переменную, получить доступ к которой можно только из текущего файла исходного кода:
    static int sum = 100;
  3. Добавьте функцию main(), в которой объявляется необходимость использовать глобальную статическую переменную, а также выводится её значение:
    int main()
    {
       extern int sum;
       printf("Sum is %d\n", sum);   
    }
  4. Далее добавьте объявление второй глобальной переменной вне функции main(), а внутри функции main(), выведите её значение. Теперь исходный код выглядит так:
    #include <stdio.h>
    
    static int sum = 100;
    static int num;
    
    int main()
    {
       extern int sum;
       printf("Sum is %d\n", sum);
    
       extern int num;
       printf("Num is %d\n", num);
    }
  5. Добавьте финальное утверждение, чтобы вернуть значение 0, чего требует объявление функции, а затем сохраните файл:
    return 0;
    Исходный код программы global_1.c
  6. Создайте новый файл global_2.c. Внутри него объявите вторую глобальную переменную, затем сохраните этот файл:
    static int num = 200;
    Исходный код программы global_2.c
  7. Скомпилируйте оба файла исходного кода в один исполняемый файл, передав имя каждого из них в команде компилирования:
    gcc global_1.c global_2.c -o global.exe
    Ошибка компиляции возникла потому, что область видимости переменной num в файле global_2.c определена как глобальная только для файла в котором она объявлена и инициализирована.
    Ключевое слово static, добавленное к объявлению переменной num, ограничило её область видимости рамками только этого файла, что привело к ошибке компиляции.
  8. Исправим ошибку. Удалим ключевое слово static, добавленное к объявлению переменной num и сохраним файл global_2.c .
    Исправленный исходный код программы global_2.c
  9. Снова скомпилируйте оба файла исходного кода в один исполняемый файл, передав имя каждого из них команде компилирования
    gcc global_1.c global_2.c -o global.exe
    , а затем запустите программу, чтобы увидеть на экране значения глобальных переменных.
    Результат работы программы global.exe

Заключение

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

Размещение переменных в регистрах процессора

Ключевое слово register

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

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

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

Ключевое слово volatile

Полной противоположностью ключевого слова register является ключевое слово volatile (англ. volatile - непостоянный, шаткий, неустойчивый). Это значит, что переменная не должна помещаться в регистры, поскольку её значение способно измениться в любой момент даже без участия кода, окружающего её. Это бывает важно для глобальных переменных в крупных программах, которые могут изменять переменную сразу несколькими потоками вычислений.

Использование ключевого слова register может оказаться полезным для локальных переменных, применяющихся для хранения управляющего значения в цикле. На каждой итерации(повторении) цикла происходит обращение к переменной, которая хранит управляющее значение. Хранение этого значения в регистре способно ускорить выполнение цикла. Компилятор распознаёт структуру цикла и оптимизирует его в эффективный код Ассемблера.

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

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

Ключевые слова register и volatile описаны здесь лишь для полноты картины — в действительности они редко используются, поскольку в большинстве программ для хранения данных используются обычные переменные.
  1. Напишем новую программу register.c с подключенным заголовочным файлом стандартной библиотеки функций ввода/вывода :
    #include <stdio.h>
  2. Добавьте функцию main(), в которой объявляется и инициализируется переменная с использованием ключевого слова register. Эта переменная содержит управляющее значение цикла, равное 0:
    int main()
    {
        register int num = 0;
    }
  3. Теперь добавьте условие, позволяющее проверить, не превысило ли управляющее значение число 5, а затем пару фигурных скобок {}:
    #include <stdio.h>
    
    int main()
    {
        register int num = 0;
        while(num < 5)
        {
            
        }
    }
    Если для переменной, объявленной с помощью ключевого слова register, использовать операцию &, компилятор разместит переменную в ОЗУ, а не в регистре процессора.
  4. Между скобками цикла while (англ. while - до тех пор пока) добавьте утверждения, позволяющие в каждой итерации цикла увеличить управляющее значение и печатать его в консоль. Теперь код выглядит так:
    #include <stdio.h>
    
    int main()
    {
        register int num = 0;
        while(num < 5)
        {
            ++num;
            printf("Pass %d\n", num);
        }
    }
  5. Возвратим 0 при завершении программы:
    return 0;
  6. Сохраните, компилируйте и запустите файл программы, чтобы увидеть в консоли значение переменной, размещённой в регистре, на каждой итерации цикла. Теперь код выглядит так:
    #include <stdio.h>
    
    int main()
    {
        register int num = 0;
        while(num < 5)
        {
            ++num; /* тоже самое что и num = num + 1; */
            printf("Pass %d\n", num);
        }
        return 0;
    }
Результат выполнения программы register.c

Возможно понятия: итерация цикла и условие вам не совсем понятны? Чтобы понять их давайте представим себе процесс работы программы визуально - в виде блок-схемы. На блок-схеме отметим фигуры и линии связывающие фигуры. Линии показывают нам какой шаг алгоритма выполнятся следующим. Линии идут с верху вниз, слева на права, и только итерация цикла обозначена линией со стрелкой идущей вверх и на лево - в начало условия.

Структура алгоритма функции цикла while()

А вот как выглядит алгоритм согласно утверждениям на языке С:

Структура алгоритма функции цикла while() согласно утверждениям на языке С.
  1. При первой итерации цикла, процессор проверяет условие (num < 5) , переданное как аргумент функции while() на истинность. Если (num < 5), тогда условие истинно и мы движемся по линии от условия(жёлтый ромб) влево-вниз - по линии true. Так как num = 0, значит 0 < 5. Добавляем к значению переменной единицу - ++num; и печатаем результат первой итерации в консоль. Возвращаемся к проверке условия.
  2. На второй итерации проверяем условие - Если (num < 5), т.е. теперь уже 1 < 5. Условие по прежнему истинно. Добавляем единицу к переменной и печатаем это значение в консоль.
  3. Тоже самое на третей и четвёртой итерациях цикла.
  4. Но что будет после четвёртой итерации - когда 4 < 5, и добавляется единица к значению переменной и она становится равна 5.
  5. На пятой итерации проверяем условие 5 < 5. Очевидно что условие ложно - пять не меньше пяти. Тогда говорят, что условие цикла не выполняется и управление программой передаётся на линию false, т.е. выполняется утверждение
    return 0;
    и программа завершается.

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

Заключение

Сохранение переменных в Регистры или в Оперативную память зависит от назначения переменной. Если переменная нужна для итерации в Цикле - предпочтительнее записать её в Регистр, Если переменная нужна для общих вычислений - достаточно её сохранять в оперативную память.

Циклы и условия ветвления выполнения программы являются ключевым аспектом построения сложных программ.

Преобразование одних типов данных в другие типы данных

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

Денис Ритчи плавит металл.

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

#include <stdio.h>

int main()
{    
    int num = 31.41592; /* Объявили и инициализировали переменную типа int дробным числом */
    float fl = 42; /* Объявили и инициализировали переменную типа float целым числом */
    printf("num = %d, fl = %f\n", num, fl); /* Печать без указания модификаторов длины */
    
    return 0;
}

Результат печати в консоль:

В результате дробное число 31.41592 преобразовано к целочисленному типу как 31, а целое число 42 преобразовано к числу с плавающей точкой как 42.000000 .

Принудительное приведение типа переменной к иному типу данных

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

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

Если переменная типа float приводится к переменной типа int, её цифры после запятой просто удаляются — никакого округления не выполняется. Например, если выполнить приведение переменной float num = 5.75 к типу int, то результатом будет значение 5, а не 6.

Если переменная типа char приводится к переменной типа int, результатом окажется её ASCII-код, который соответствует символу. Символы в верхнем регистре располагаются в диапазоне от 65 до 90, а символы в нижнем регистре — от 97 до 122 включительно. Например, если выполнить приведение типа переменной char letter = 'A'; к типу int, результатом будет значение 65. Приведение целочисленных переменных к типу char создаст соответствующий целому числу символ.

ASCII (произносится как «аски») — это акроним («American Standard Code for Information Interexchange»), он является стандартом для простого текста. Вы можете просмотреть весь диапазон стандартных ASCII-кодов в таблицах ниже.
Управляющие текстом символы
Код Символ Описание Код Символ Описание
0 NUL null 16 DLE Выход из канала передачи данных
1 SOH Начало заголовка 17 DC1 Управление устройством 1
2 STX Начало текста 18 DC2 Контроль устройств 2
3 ETX Конец текста 19 DC3 Контроль устройств 3
4 EOT Конец передачи 20 DC4 Контроль устройств 4
5 ENQ Прошу подтверждения 21 NAK Не подтверждаю
6 ACK Подтверждаю 22 SYN Синхронизация
7 BEL Звонок, звуковой сигнал 23 ETB Конец блока текста
8 BS Возврат на один символ 24 CAN Отмена
9 TAB Горизонтальная табуляция 25 EM Конец носителя
10 LF Новая строка 26 SUB Подставить
11 VT Вертикальная табуляция 27 ESC Начало управляющей последовательности
12 FF Прогон страницы, новая страница 28 FS Разделитель файлов
13 CR Возврат каретки 29 GS Разделитель групп
14 SO Изменить цвет ленты 30 RS Разделитель записей
15 SI Обратно к предыдущему коду 31 US Разделитель юнитов
Печатаемые символы, цифры и латиница
Код Символ Код Символ Код Символ Код Символ
32 "Space" 56 8 80 P 104 h
33 ! 57 9 81 Q 105 i
34 " 58 : 82 R 106 j
35 # 59 ; 83 S 107 k
36 $ 60 < 84 T 108 l
37 % 61 = 85 U 109 m
38 & 62 > 86 V 110 n
39 ' 63 ? 87 W 111 o
40 ( 64 @ 88 X 112 p
41 ) 65 A 89 Y 113 q
42 * 66 B 90 Z 114 r
43 + 67 C 91 [ 115 s
44 , 68 D 92 \ 116 t
45 - 69 E 93 ] 117 u
46 . 70 F 94 ^ 118 v
47 / 71 G 95 _ 119 w
48 0 72 H 96 ` 120 x
49 1 73 I 97 a 121 y
50 2 74 J 98 b 122 z
51 3 75 K 99 c 123 {
52 4 76 L 100 d 124 |
53 5 77 M 101 e 125 }
54 6 78 N 102 f 126 ~
55 7 79 O 103 g 127 "Delete"
Чтобы узнать больше о расширенных наборах ASCII кодов посетите веб-сайт https://www.ascii-code.com/ или выполните поиск по запросу ASCII во Интернете.

Довольно часто необходимо выполнить приведение целочисленных значений к типу float, чтобы выполнять точные вычисления с учётом знаков после точки — в противном случае деление может создать "урезанный" целочисленный результат. Например, если объявить переменные int x = 7, y = 5;, а затем выполнить операцию деления float z = x / y;, результатом будет 1.000000. Для получения более точного результата необходимо использовать приведение типов: (float) x / (float) y, что приведёт к получению результата 1.400000 .

Прямой слеш, /, в языке C, представляет собой операцию обычного математического деления.

Приведение длинных точных чисел с плавающей точкой от типа double к более "короткому" типу float приводит не к обрезанию длинного значения, а к его округлению в шестом знаке после запятой. Например, приведение переменной double decimal = 0.1234569; к типу float , даст округлённый результат 0.123457 .

  1. Начните новую программу cast.c с инструкции препроцессора, подключающую стандартную библиотеку функций ввода/вывода:
    #include <stdio.h>
  2. Добавьте функцию main(), в ней объявите и инициализируйте переменные, имеющие различные типы данных:
    int main()
    {
       float num = 5.75;
       char letter = 'C';
       int zee = 90;
       int x = 7, y = 5;
       double decimal = 0.1234569;
    }
  3. Теперь добавьте утверждения, печатающие результат преобразования каждой переменной в консоль и завершите программу возвращением 0, как того требует объявление функции. Теперь код выглядит так:
    #include <stdio.h>
    
    int main()
    {
       float num = 5.75;
       char letter = 'C';
       int zee = 90;
       int x = 7, y = 5;
       double decimal = 0.1234569;
    
       printf("float cast to int:\t %d\n", (int)num);
       printf("char cast to int:\t %d\n", (int)letter);
       printf("int cast to char:\t %c\n", (char)zee);
       printf("float arithmitic:\t %f\n", (float)x/(float)y);
       printf("double cast to float:\t %d\n", (float)decimal);
    
       return 0;
    }
  4. Сохраните файл программы, а затем скомпилируйте и запустите её, чтобы увидеть в консоли результат каждого приведения типа:
    Пример синтаксиса приведения типов.
    Значения типа float, приведенные к типу int, обрезаются, а значения типа double, приведенные к типу float — округляются.

Создание Массива переменных

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

Элементы данных хранятся последовательно в ячейках массива, которые нумеруются начиная с 0. Первый элемент массива хранится в ячейке с номером 0, второй — в ячейке с номером 1, и т. д. Как и в системе хранения Little-Endian - младшая ячейка массива имеет наименьший порядковый номер, т.е. 0, а старшая ячейка массива имеет наибольший порядковый номер.

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

<тип-данных> <имя-массива> [<количество-элементов>];

Опционально массив может быть инициализирован при объявлении, значения каждого элемента оформляются как разделённый запятыми список элементов, обрамлённый фигурными скобками { }. Например, Массив из трёх целочисленных элементов может быть создан и инициализированный следующим образом:

int arr[3] = {1, 2, 3};

Это читается так: Массив с именем arr состоит из трёх [3] элементов, каждый элемент это число типа int. Элемент с номером 0 инициализирован числом 1, элемент с номером 1 инициализирован числом 2, элемент с номером 2 инициализирован числом 3.

К элементу массива допустимо обратиться с помощью имени массива, после которого ставятся квадратные скобки, содержащие номер элемента. Например, запись arr[0] позволит обратиться к первому элементу массива.

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

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

Одна из наиболее используемых особенностей массива при программировании на языке C заключается в том, что они могут хранить текстовые строки. Один элемент массива типа char способен хранить один символ. Добавление специального символа-терминатора \0(конец строки) в конец массива "повышает" его статус до строки. Например, строка может быть создана и инициализирована так:

char str[4] = {'C', 'a', 't', '\0'};

Использование массива символов, являющегося строкой, позволяет обратиться ко всей строке сразу с помощью только имени массива. Строка также может быть отображена с помощью функции printf() и спецификатора формата %s. Например, напечатать строку str можно так:

printf("%s", str);
  1. Начните новую программу array.c с инструкции препроцессора, подключающую стандартную библиотеку функций ввода/вывода:
    #include <stdio.h>
  2. Добавьте главную функцию, в которой объявляется массив из трёх целочисленных элементов:
    #include <stdio.h>
    
    int main(){
       int arr[3];
    }
    
  3. Теперь добавьте утверждения, инициализирующие массив целочисленными элементами, индивидуально назначая значение каждого элемента:
    #include <stdio.h>
    
    int main(){
       int arr[3];
       arr[0] = 100;
       arr[1] = 200;
       arr[2] = 300;
    }
    
  4. Теперь добавьте утверждение, создающее и инициализирующее массив символов, позволяющий хранить текстовую строку:
    #include <stdio.h>
    
    int main(){
       int arr[3];
       arr[0] = 100;
       arr[1] = 200;
       arr[2] = 300;
    
       char str[10]={'C',' ','p','r','o','g','r','a','m','\0'};
    }
    
  5. Далее добавьте утверждения, позволяющие вывести все элементы целочисленного массива и строки из массива символов в консоль. Завершите программу возвращением нуля:
    #include <stdio.h>
    
    int main(){
       int arr[3];
       arr[0] = 100;
       arr[1] = 200;
       arr[2] = 300;
    
       char str[10]={'C',' ','p','r','o','g','r','a','m','\0'};
    
       printf("1st element value: %d\n", arr[0]);
       printf("2nd element value: %d\n", arr[1]);
       printf("3rd element value: %d\n", arr[2]);
       printf("String: %s\n", str);
    
       return 0;
    }
    
  6. Сохраните файл программы, скомпилируйте и запустите программу, чтобы увидеть на экране значения, сохранённые в массивах переменных:
    Пример синтаксиса работы с массивами.
При создании массива, для каждого элемента, память выделяется соответственно его типу — например, один байт для каждого элемента типа char. Размер массива не может быть изменён динамически.

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

Одномерный массив

Выше мы рассмотрели так называемый одномерный массив данных. Такой массив это набор данных, и по смыслу, "скреплённых вместе" в единый блок данных в памяти машины. Хотя можно обратиться к каждому элементу массива отдельно через его номер или индекс. Одномерными такие массивы называются потому, что это, по сути, последовательность элементов идущих один за другим. Для простоты представления, в одномерном массиве первый элемент (индекс 0) расположен крайним слева и увеличиваются индексы на единицу, на право. Запомнить такой подход просто: Читаем с лева на право, как обычный текст в книге.

Аналогия одномерного массива в виде скреплённых вместе коробочек(элементов массива), упакованных в единый блок в памяти машины(коробка куда все они сложены).

Рассмотрим подробнее абстракцию понятия одномерного массива

Свойства Одномерного массива
Содержимое ячейки A B C D E
Номер ячейки/индекс [0] [1] [2] [3] [4]

Двухмерный массив

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

Аналогия Двухмерного массива в виде кубиков - 5 штук по ширине и 5 штук по длине(элементы массива), сложенные в коробку(блок памяти в машине).

Рассмотрим подробнее абстракцию понятия Двухмерного массива

Свойства Двухмерного массива
Номер/Индекс строки [0] A B C D E
Номер/Индекс строки [1] F G H I J
Номер/Индекс строки [2] K L M N O
Номер/Индекс строки [3] P Q R S T
Номер/Индекс строки [4] U V W X Y
Номер/Индекс столбца [0] [1] [2] [3] [4]

Например, в случае Двухмерного массива, приведенного выше, элемент по адресу/с индексом [1][2] содержит букву «Н». Где [1] - индекс строки, и [2] - индекс элемента в этой строке.

Двухмерные массивы годятся для хранения информации в форме сетки значений, имеющей координаты по осям X и Y. Создание трёхмерного массива, имеющего три индекса, позволит хранить трёхмерные координаты:

Трёхмерный массив

Такой массив имеет строки, столбцы и страницы. Т.е. каждый элемент имеет индекс указывающий на страницу, строку и номер элемента в строке.

Рассмотрим подробнее абстракцию понятия Трёхмерного массива представив его визуально

Представить визуально такой Трёхмерный массив элементов легко. Это как сложенные в коробку в несколько слоёв Двухмерные массивы.

Каждый слой кубиков с верху вниз - это страница. Самая верхняя страница имеет индекс [0]. каждая последующая страница имеет индекс больше на единицу от предыдущей.

Каждая страница - это уже знакомый нам Двухмерный массив состоящий из строк и столбцов - каждый со своим индексом.

Каждая строка - это естественно Одномерный массив. В нём у каждого элемента свой индекс.

Рассмотрим пример Трёхмерного массива, в котором сохранены Шестнадцатиричные числа типа char т.е. значения размером в один байт:

Свойства Трёхмерного массива. Страница [0] Верхняя (4х5 элементов)
Номер/Индекс строки [0] 0x20 0x21 0x22 0x23 0x24
Номер/Индекс строки [1] 0x25 0x26 0x27 0x28 0x29
Номер/Индекс строки [2] 0x30 0x31 0x32 0x33 0x34
Номер/Индекс строки [3] 0x35 0x36 0x37 0x38 0x39
Номер/Индекс столбца [0] [1] [2] [3] [4]
Страница [1] (4х5 элементов)
Номер/Индекс строки [0] 0x40 0x41 0x42 0x43 0x44
Номер/Индекс строки [1] 0x45 0x46 0x47 0x48 0x49
Номер/Индекс строки [2] 0x50 0x51 0x52 0x53 0x54
Номер/Индекс строки [3] 0x55 0x56 0x57 0x58 0x59
Номер/Индекс столбца [0] [1] [2] [3] [4]
Страница [2] (4х5 элементов)
Номер/Индекс строки [0] 0x60 0x61 0x62 0x63 0x64
Номер/Индекс строки [1] 0x65 0x66 0x67 0x68 0x69
Номер/Индекс строки [2] 0x70 0x71 0x72 0x73 0x74
Номер/Индекс строки [3] 0x75 0x76 0x77 0x78 0x79
Номер/Индекс столбца [0] [1] [2] [3] [4]
Страница [3] Нижняя (4х5 элементов)
Номер/Индекс строки [0] 0x2A 0x2B 0x2C 0x2D 0x2E
Номер/Индекс строки [1] 0x2F 0x3A 0x3B 0x3C 0x3D
Номер/Индекс строки [2] 0x3E 0x3F 0x4A 0x4B 0x4C
Номер/Индекс строки [3] 0x4D 0x4E 0x5A 0x5B 0x5C
Номер/Индекс столбца [0] [1] [2] [3] [4]

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

  1. Напишем программу matrix.c для работы с массивами. Подключите стандартную библиотеку ввода/вывода
    #include <stdio.h>
  2. Добавьте в код главную функцию и объявим двухмерный массив элементов целочисленного типа int. Указав два индекса - [2][3], мы тем самым создали двухмерный массив. Первый индекс указывает на количество строк, второй индекс указывает на количество элементов в каждой строке. Т.е. у нас две строки по три элемента в каждой.
    #include <stdio.h>
    
       int main()
    {
       int matrix[2][3] = { {'A', 'B', 'C'}, /*Это строка с индексом [0]*/
                            {1, 2, 3} }; /*Это строка с индексом [1]*/
    }
    
  3. Теперь добавьте утверждения, позволяющие отобразить содержимое всех элементов строки с индексом 0 в консоль.
    #include <stdio.h>
    
       int main()
    {
       int matrix[2][3] = { {'A', 'B', 'C'},
                            {1, 2, 3} };
    
       printf("Element [0][0] contains %c\n", matrix[0][0]);
       printf("Element [0][1] contains %c\n", matrix[0][1]);
       printf("Element [0][2] contains %c\n", matrix[0][2]);
    }
    
  4. Теперь добавьте утверждения, позволяющие отобразить содержимое всех элементов строки с индексом 1. Завершите программу добавив возвращение 0.
    #include <stdio.h>
    
       int main()
    {
       int matrix[2][3] = { {'A', 'B', 'C'},
                            {1, 2, 3} };
    
       printf("Element [0][0] contains %c\n", matrix[0][0]);
       printf("Element [0][1] contains %c\n", matrix[0][1]);
       printf("Element [0][2] contains %c\n", matrix[0][2]);
    
       printf("Element [1][0] contains %c\n", matrix[1][0]);
       printf("Element [1][1] contains %c\n", matrix[1][1]);
       printf("Element [1][2] contains %c\n", matrix[1][2]);
    
       return 0;
    }
    
  5. Сохраните файл программы, скомпилируйте и запустите программу, чтобы увидеть все значения элементов, сохранённые в двухмерном массиве .
    Пример синтаксиса программы для работы с Двухмерным массивом
  6. Подумайте, почему последние три символа отобразились как смайлики и сердечко?

Заключения

Установка значений переменных

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

Объявление констант в программе

В случае, когда в программе требуется использовать константное значение, которое никогда не изменится, оно должно быть объявлено точно так же, как и обычная переменная, но с использованием в объявлении ключевого слова const. Например, константа, представляющая неизменяемое число «миллион», может быть объявлена и инициализирована следующим образом:

const int MILLION = 1000000;

В объявлениях констант объект всегда должен инициализироваться. Программа не способна изменить исходное значение константы. Это охраняет её от случайностей — компилятор сообщит об ошибке, если программа попытается изменить исходное значение константы.

Имена констант должны соответствовать соглашениям именования переменных, приведенных выше, но традиционно в именах констант используются символы в верхнем регистре(CAPS LOCK), что позволяет с легкостью отличить их от имён переменных при чтении исходного кода.

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

  1. Напишем новую программу constant.c. Подключаем заголовочный файл стандартных функций ввода/вывода, главную функцию в которой объявляем константу и возвращаем 0
    #include <stdio.h>
    
    int main(){
       const float PI = 3.141593; /* по умолчанию у типа float всегда шесть знаков после точки */
       
       return 0;
    }
  2. Далее в блоке функции main() добавьте утверждения, объявляющие четыре переменные типа float.
    #include <stdio.h>
    
    int main(){
       const float PI = 3.141593;
       float diameter;
       float radius;
       float circle;
       float area;
    
       return 0;
    }
  3. Теперь добавьте утверждения, требующие от пользователя ввести значение переменной.
    #include <stdio.h>
    
    int main(){
       const float PI = 3.141593;
       float diameter;
       float radius;
       float circle;
       float area;
    
       printf("Enter the diameter of a circle in millimeters:\n");
       scanf("%f", &diameter); 
          /* Программа готова принимать значение типа float (формат печати в консоли %f), 
          и при получении значения сохранит его в памяти по адресу 
          (операция адрес &) хранения переменной с именем diameter */
    
       return 0;
    }
  4. Добавьте утверждения, вычисляющие значения трёх остальных переменных с использованием константного значения и значения, введенного пользователем.
    #include <stdio.h>
    
    int main(){
       const float PI = 3.141593;
       float diameter;
       float radius;
       float circle;
       float area;
    
       printf("Enter the diameter of a circle in millimeters:\n");
       scanf("%f", &diameter);       
       circle = PI * diameter;          /* Длина окружности */
       radius = diameter / 2;           /* Длина радиуса */
       area = PI * (radius * radius);   /* Площадь круга */
    
       return 0;
    }
    Помните, что операция адресации & должна стоять перед именем переменной при вызове функции scanf(), предназначенной для получения значений переменных из консоли.
  5. Теперь вставьте утверждения, позволяющие вывести рассчитанные значения с округлением до двух десятичных знаков.
    #include <stdio.h>
    
    int main(){
       const float PI = 3.141593;
       float diameter;
       float radius;
       float circle;
       float area;
    
       printf("Enter the diameter of a circle in millimeters:\n");
       scanf("%f", &diameter);
       circle = PI * diameter;
       radius = diameter / 2;
       area = PI * (radius * radius);
    
       printf("\n\tCircumference is %.2fmm\n", circle);
       printf("\n\tAnd the Area is %.2f sq.mm\n", area);
    
       return 0;
    }
    Этот знак *(звёздочка) в языке С является арифметической операцией умножения. Другие арифметические операции рассматриваются подробнее в следующей главе.
  6. Сохранить, скомпилировать и выполнить программу:
    Скомпилированная программа.
    Выполняем программу. Вводим диаметр круга в миллиметрах и жмём ENTER.
    Программа вычисляет Длину окружности и её Площадь.

Перечисление константных значений

Ключевое слово enum позволяет удобным способом создать последовательность числовых констант . Объявление последовательности может включать в себя имя, располагающееся после слова enum. Имена констант размещаются внутри скобок, списком элементов разделённых запятыми.

Каждая константа по умолчанию будет иметь значение, превосходящее предыдущее на 1. Если значение первой константы не указано, она будет иметь значение 0, следующая — 1, и т. д. Например, именованные константы дней недели могут получить числовые значения, начиная с нуля — enum {MON, TUE, WED, THU, FRI};. В этом случае константа WED будет иметь значение 2.

Константе при объявлении может быть назначено любое числовое значение, но следующая константа в списке будет иметь значение, превосходящее предыдущее на 1, если только ей также не будет назначено отдельное значение. Например, для того, чтобы начать последовательность с 0, а не с 1, назначим первой константе значение 1 — enum {MON = 1, TUE, WED, THU, FRI};. В этом случае константа WED будет иметь значение 3.

Список перечисленных констант также известен как перечисление и порой содержит повторяющиеся значения. Например, значение 0 может быть назначено константам с именами NIL и NONE.

В следующем примере перечисление представляет собой очки, начисляемые за шары при игре в бильярд. Оно содержит необязательные имена переменных, записанные ЗАГЛАВНЫМИ буквами, поскольку они являются константами.

  1. Напишем новую программу enum.c. Добавьте утверждение, которое объявляет перечисление констант, чьи значения начинаются с единицы:
    #include <stdio.h>
    
    int main()
    {
       enum SNOOKER {RED=1, YELLOW, GREEN, BROWN, BLUE, PINK, BLACK};
    
       return 0;
    }
    
  2. Далее объявите числовую переменную, в которой будет храниться сумма нескольких значений констант:
    #include <stdio.h>
    
    int main()
    {
       enum SNOOKER {RED=1, YELLOW, GREEN, BROWN, BLUE, PINK, BLACK};
    
       int total;
    
       return 0;
    }
    
  3. Добавьте утверждения, предназначенные для печати в консоль значений некоторых перечисленных констант:
    #include <stdio.h>
    
    int main()
    {
       enum SNOOKER {RED=1, YELLOW, GREEN, BROWN, BLUE, PINK, BLACK};
    
       int total;
    
       printf("\nI potted a red worth %d\n", RED);
       printf("\nThen a black worth %d\n", BLACK);
       printf("\nFollowed by another red worth %d\n", RED);
       printf("\nAnd finally a blue worth %d\n", BLUE);
    
       return 0;
    }
  4. Добавьте следующее утверждение, позволяющее рассчитать общую сумму константных значений, отображенных на предыдущем шаге:
    #include <stdio.h>
    
    int main()
    {
       enum SNOOKER {RED=1, YELLOW, GREEN, BROWN, BLUE, PINK, BLACK};
    
       int total;
    
       printf("\nI potted a red worth %d\n", RED);
       printf("\nThen a black worth %d\n", BLACK);
       printf("\nFollowed by another red worth %d\n", RED);
       printf("\nAnd finally a blue worth %d\n", BLUE);
    
       total = RED + BLACK + RED + BLUE;
    
       return 0;
    }
  5. Теперь добавьте утверждение, предназначенное для вывода рассчитанной суммы значений:
    #include <stdio.h>
    
    int main()
    {
       enum SNOOKER {RED=1, YELLOW, GREEN, BROWN, BLUE, PINK, BLACK};
    
       int total;
    
       printf("\nI potted a red worth %d\n", RED);
       printf("\nThen a black worth %d\n", BLACK);
       printf("\nFollowed by another red worth %d\n", RED);
       printf("\nAnd finally a blue worth %d\n", BLUE);
    
       total = RED + BLACK + RED + BLUE;
    
       printf("\nAll together I scored %d\n", total);
    
       return 0;
    }
  6. Сохраните, скомпилируйте и выполните программу, чтобы увидеть значения констант перечисления и вычисленную их сумму в консоли:
    Пример синтаксиса работы с перечислением.
Инициализируйте значением 1 константу BLUE в этом примере, что перезапустит процесс инкрементирования — константа PINK будет иметь значение 2, а константа BLACK — 3. Какие значения будут иметь оставшиеся константы RED, YELLOW, GREEN, BROWN ?
Какой будет результат вывода в консоль?
Имя перечисления (в этом случае SNOOKER) является опциональным(необязательным), оно может быть опущено, код сохранится, скомпилируется и выполнится без ошибок.

Создание данных Константного типа

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

Переменные нашего типа enum могут быть объявлены точно так же, как и переменные любого другого типа данных — с помощью следующего синтаксиса:

 <тип-данных> <имя-переменной>;

В примере, приведенном выше, создаётся перечисление с именем SNOOKER, которое допустимо рассмотреть как тип данных enum SNOOKER. Поэтому переменная с именем pair может быть создана с помощью объявления enum SNOOKER pair; она способна хранить значения перечисления, определяемого этим типом.

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

pair = (enum SNOOKER) 7;

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

С помощью объявления enum также можно создать переменную, указав имя переменной после последней скобки. Например, объявление enum BOOLEAN {FALSE, TRUE} flag; определяет тип данных enum и создает переменную flag этого типа.

Типы данных созданные программистом могут быть определены с помощью ключевого слова typedef и следующего синтаксиса:

typedef <определение> <имя-типа>;
Явное указание значений перечисляемого типа служит для напоминания его типа.(Хотя это необязательно).

Объявления пользовательских типов данных помогут сделать код программы более понятным. Например, если в программе часто объявляются переменные с типом unsigned short int, можно создать пользовательский тип данных, имеющий такие же модификаторы, например так:

typedef unsigned short int USINT;

Каждая переменная с типом unsigned short int USINT может быть объявлена с использованием типа данных USINT вместо unsigned short int.

  1. Напишем новую программу consttype.c. Добавьте заголовочный файл ввода/вывода, главную функцию, объявим и инициализируем перечисление констант, чьи значения начинаются с единицы, и возвратим 0 в завершении программы:
    #include <stdio.h>
    
    int main()
    {
        enum SNOOKER
        {
            RED = 1,
            YELLOW,
            GREEN,
            BROWN,
            BLUE,
            PINK,
            BLACK
        };
    
        return 0;
    }
  2. Далее объявите и проинициализируйте переменную определённого типа enum, а затем напечатайте её значение в консоль:
    #include <stdio.h>
    
    int main()
    {
       enum SNOOKER
       {
          RED = 1,
          YELLOW,
          GREEN,
          BROWN,
          BLUE,
          PINK,
          BLACK
       };
    
       enum SNOOKER pair = RED + BLACK;
       printf("Pair value: %d\n", pair);
    
       return 0;
    }
  3. Теперь добавьте утверждение, предназначенное для создания пользовательского типа данных:
    typedef unsigned short int USINT;
  4. Далее объявите и проинициализируйте переменную пользовательского типа данных и напечатайте её значение:
    USINT num = 16;
    printf("Unsigned short int value: %d\n", num);
  5. Сохраните, скомпилируйте и выполните программу, чтобы увидеть значение, назначенное переменной перечисляемого типа, и значение, назначенное переменной пользовательского типа данных:
    Синтаксис определения пользовательских типов данных.
Пользовательские типы данных должны быть определены в программе до того, как переменные этого типа будут созданы(объявленые и инициализированные).

Предопределение констант

Директива препроцессора #define может быть использована для указания текстовых значений констант, которые впоследствии могут быть использованы в программе, с помощью следующего синтаксиса:

#define <ИМЯ-КОНСТАНТЫ> "<текстовая-строка>"

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

#ifdef #define #endif

Директива препроцессора #ifdef является условием и может быть использована для проверки того, существует ли Предопределение константы. В зависимости от результата проверки, далее, может идти директива #define, позволяющая указать значение константы. #ifdef инструкция препроцессора также называется макросом. Каждый макрос должен оканчиваться директивой #endif .

К счастью, макросы препроцессора могут проверять заранее определённые компилятором константы, чтобы определить текущую операционную систему. Значения этих констант будут варьироваться на разных компьютерах, но на платформе Windows значением константы обычно является _WIN32, а на платформе Linux — linux. Директива препроцессора #ifdef может применить соответствующую текстовую строку для определения текущей платформы.

  1. Напишем новую программу define.c , начнём стандартно:
    #include <stdio.h>
    
    int main()
    {    
       return 0;
    }
  2. Добавьте ещё три директивы препроцессора, которые предопределяют заменяемые текстовые строки в исходном коде:
    #include <stdio.h>
    
    #define LINE "_________________________________" /* Просто знаки подчёркивания */
    #define TITLE "C Programming form Zero to Hero"
    #define AUTOR "You are - programmer"
    
    int main()
    {    
       return 0;
    }
  3. Далее добавьте условный макрос, который определяет текстовую строку, позволяющую идентифицировать платформу Windows:
    #include <stdio.h>
    
    #define LINE "_________________________________"
    #define TITLE "C Programming form Zero to Hero"
    #define AUTOR "You are - programmer"
    
    #ifdef _WIN32
        #define SYSTEM "Windows"
    #endif
    
    int main()
    {    
       return 0;
    }
  4. Далее добавьте условный макрос, который определяет текстовую строку, позволяющую идентифицировать платформу Linux:
    #include <stdio.h>
    
    #define LINE "_________________________________"
    #define TITLE "C Programming form Zero to Hero"
    #define AUTOR "You are - programmer"
    
    #ifdef _WIN32
        #define SYSTEM "Windows"
    #endif
    
    #ifdef linux
        #define SYSTEM "Linux"
    #endif
    
    int main()
    {    
       return 0;
    }
  5. Далее заполним функцию main(), в которой печатаем текстовую строку, подменённую препроцессором:
    #include <stdio.h>
    
    #define LINE "_________________________________"
    #define TITLE "C Programming form Zero to Hero"
    #define AUTOR "You are - programmer"
    
    #ifdef _WIN32
       #define SYSTEM "Windows"
    #endif
    
    #ifdef linux
       #define SYSTEM "Linux"
    #endif
    
    int main()
    {
       printf("\n\t%s\n\t%s", LINE, TITLE);
       printf("\n\tby\n\t%s\n\t%s\n", AUTOR, LINE);
       printf("\n\tOperating System: %s\n\n", SYSTEM);
    
       return 0;
    }
  6. Сохраните файл программы, а затем скомпилируйте и выполните программу, чтобы увидеть напечатанные строки в консоли:
    Пример применения #define #ifdef #endif - Предопределения строковых Констант.

Отладка с помощью Предопределений

В качестве альтернативы можно использовать директивы препроцессора #if, #else и #elif (else if). Они позволяют использовать условное ветвление в соответствии с результатом оценки.

Константа, предопределенная директивой препроцессора #define, может быть разопределена с помощью директивы #undef. Оценка также рассматривается в случае, если константа не определена, это делается с помощью директивы препроцессора #ifndef .

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

  1. Напишем новую программу debug.c:
    #include <stdio.h>
    
    int main()
    {
         return 0;
    }
  2. Добавьте ещё одну директиву препроцессора, предназначенную для создания макроса:
    #include <stdio.h>
    
    #define DEBUG 1
    
    int main()
    {
         return 0;
    }
  3. Далее в главную функцию добавьте директивы препроцессора, предназначенные для оценки и отчёта о состоянии макросов:
    #include <stdio.h>
    
    #define DEBUG 1
    
    int main()
    {
       #if DEBUG == 1
             printf("Debug status is 1\n");
          #elif DEBUG == 2
             printf("Debug status is 2\n");
          #else
             #ifdef DEBUG
                printf("Debug is defined!\n");
             #endif
             #ifndef DEBUG
                printf("Debug is not define!\n");
             #endif
          #endif
    
        return 0;
    }
  4. Сохраните файл программы, а затем скомпилируйте и выполните программу, чтобы увидеть сообщение о состоянии макроса:
    Состояние макроса №1. Обратите внимание как Visual Studio Code уже заранее затеняет строки кода, которые не будут выполняться, так как уже проверено значение условия.
  5. Измените значение макроса, а затем сохраните, перекомпилируйте и выполните программу снова, чтобы увидеть, что состояние макроса изменилось:
    #define DEBUG 2
    Состояние макроса №2. Обратите внимание как Visual Studio Code уже заранее затеняет строки кода, которые не будут выполняться, так как уже проверено значение условия.
  6. Измените значение макроса, а затем сохраните, перекомпилируйте и выполните программу снова, чтобы увидеть, что состояние макроса изменилось:
    #define DEBUG 3
    Состояние макроса №3. Обратите внимание как Visual Studio Code уже заранее затеняет строки кода, которые не будут выполняться, так как уже проверено значение условия.
    Будьте внимательны, Каждая проверка условий с помощью препроцессора должна оканчиваться директивой #endif
  7. Добавьте ещё одну директиву препроцессора в начале блока функции, а затем сохраните, перекомпилируйте и выполните программу снова, чтобы увидеть изменение:
    #undef DEBUG
    Состояние макроса №4. Обратите внимание как Visual Studio Code уже заранее затеняет строки кода, которые не будут выполняться, так как уже проверено значение условия.

Заключение

Выполнение вычислений

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

Выполнение арифметических операций

В зависимости от количества операндов, операции бывают трёх типов:

  1. Унарные - операция проводится только над одним операндом.
  2. Бинарные - операция проводится только с двумя операндами.
  3. Тернарные - операция проводится только при наличии трёх операндов.

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

Арифметические Операции
Символ Операции Операция Тип операции
+ Сложение Бинарная
- Вычитание Бинарная/Унарная
* Умножение Бинарная
/ Деление Бинарная
% Остаток после деления Бинарная
++ Инкремент Унарная
-- Декремент Унарная

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

Числа, используемые при работе с операциями для формирования выражений, называются операндами — в выражении 2 + 3 числа 2 и 3 являются операндами.

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

Известная загадка, гуляющая по Интернету: Сколько будет 2 + 2 * 2 ?

В программировании существуют специальные правила приоритетности выполнения операций скрытых в компиляторе языка. Дабы не полагаться на догадки - Всегда используйте круглые скобки в ваших математических действиях.

В зависимости от того, что будет взято в круглые скобки - будет получен разный результат. Если запись (2+2)*2 то это 8, а если 2+(2*2) это 6.

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

a = b * c - d % e / f; /* Плохая - непрозрачная запись математической операции */
a = (b * c) - ((d % e) / f); /* Хорошая - прозрачная запись математической операции */

Операция деления с остатком, %, делит первое заданное число на второе и возвращает только остаток от деления. Это полезно для определения кратности одного числа другому числу.

Операции Инкремента, ++, и Декремента, --, изменяют заданное число на 1 и возвращают результирующее значение. Чаще всего эти операции используются для подсчета итераций цикла. Операция Инкремента увеличивает значение на 1, операция Декремента уменьшает на 1.

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

  1. Напишем новую программу, стандартно, объявим и инициализируем переменные типа int:
    #include <stdio.h>
    
    int main()
    {
       int a = 4;
       int b = 8;
       int c = 1;
       int d = 1;
    
       return 0;
    }
    
  2. Далее в главной функции выведите на печать в консоль результат арифметических операций, произведённых над значениями переменных:
    #include <stdio.h>
    
    int main()
    {
       int a = 4;
       int b = 8;
       int c = 1;
       int d = 1;
    
       printf("\nAddition: %d\n", a + b);
       printf("Substraction: %d\n", b - a);
       printf("Multipication: %d\n", a * b);
       printf("Division: %d\n", b / a);
       printf("Modulus: %d\n\n", a % b);
    
       return 0;
    }
    
  3. Теперь в главной функции выведите результат постфиксных и префиксных операций инкремента:
    #include <stdio.h>
    
    int main()
    {
       int a = 4;
       int b = 8;
       int c = 1;
       int d = 1;
    
       printf("\nAddition: %d\n", a + b);
       printf("Substraction: %d\n", b - a);
       printf("Multipication: %d\n", a * b);
       printf("Division: %d\n", b / a);
       printf("Modulus: %d\n\n", a % b);
    
       printf("Pastfix Increment: %d\n", c++); /* Увеличение переменной на единицу 
             и сразу же сохранение значения в переменную */
       printf("Pastfix now: %d\n\n", c);
    
       printf("Prefix Increment: %d\n", ++d); /* Сохранение значения переменной без увеличения на единицу, 
             Увеличение на единицу будет совершено в следующем обращении к переменной */
       printf("Prefix now: %d\n\n", d);
    
       return 0;
    }
    
  4. Сохраните файл, а затем запустите программу, чтобы увидеть результат выполнения арифметических операций:
    Синтаксис простых арифметических действий
    Обратите внимание на то, что значение переменной мгновенно увеличивается только при постфиксных операциях. При использовании префиксных операций операнд оказывается увеличенным только при следующем обращении к нему.

Присваивание значений переменной

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

Операции присваивания значений
Операция Короткая запись Развёрнутая запись Читается
= a = b a = b Переменной a присваивается значение переменной b
+= a += b a = (a + b) Переменной a присваивается результат суммы переменных а и b
-= a -= b a = (a - b) Переменной a присваивается результат разницы переменных а минус b
*= a *= b a = (a * b) Переменной a присваивается результат произведения переменных а и b
/= a /= b a = (a / b) Переменной a присваивается результат деления переменной a на b
%= a %= b a = (a % b) Переменной a присваивается остаток от деления переменной a на b

Будьте бдительны! Отличайте всегда операцию присваивания =, от операции сравнения ==.

Операция += может пригодиться для того, чтобы добавить значение к уже существующему значению, которое хранится в переменной a. В табличном примере операция += сначала добавляет значение, хранимое в переменной b, к значению, хранимому в переменной a. Затем она присваивает результат этого действия переменной a.

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

В случае операции %= первый операнд a делится на второй операнд b, а затем остаток от операции присваивается переменной a.

  1. Стандартно напишем новую программу assign.c. Объявим две переменные:
    #include <stdio.h>
    
    int main()
    {
       int a;
       int b;
    
       return 0;
    }
  2. Далее в главной функции указываем напечатать результаты работы операций присваивания, произведённых над значениями переменных:
    #include <stdio.h>
    
    int main()
    {
       int a;
       int b;
    
       printf("Assigned Variables:\n");
       printf("a = %d\n", a = 8);
       printf("b = %d\n", b = 4);
    
       printf("\nAdded & assigned: ");
       printf("\tVariable a += b (a = 8 + 4), a = %d\n", a += b);
    
       printf("\nSubtracted & assigned:");
       printf("\tVariable a -= b (a = 12 - 4), a = %d\n", a -= b);
    
       printf("\nMultiplied & assigned:");
       printf("\tVariable a *= b (a = 8 * 4), a = %d\n", a *= b);
    
       printf("\nDivided & assigned:");
       printf("\tVariable a /= b (a = 32 / 4), a = %d\n", a /= b);
    
       printf("\nVodulated & assigned:");
       printf("\tVariable a %%= b (a = 8 %% 4), a = %d\n", a %= b);
    
        return 0;
    }
    Напомним, чтобы напечатать в консоль символ %, внутри функции printf() следует использовать комбинацию символов %%.
  3. Запустите программу, чтобы увидеть результат выполнения операций присваивания:
    Операции присваивания совмещённые с математическими операциями.

Сравнение значений

Операции для сравнения двух числовых значений, перечислены в таблице ниже.

Операции сравнения значений
Операция Способ сравнения значений
== Равенство
!= Неравенство
< Меньше
> Больше
<= Меньше или Равно
>= Больше или Равно

Операция равенства, ==, сравнивает два операнда и возвращает 1 (true, англ. true - истина), если их значения равны, в противном случае она вернёт значение 0 (false, англ. false - ложь). Операнды равны, если содержат одинаковое число, или же, если они являются символами, они равны в случае, когда совпадает числовое значение их ASCII-кодов.

Операция неравенства, !=, поступает наоборот — возвращает 1 (true), если операнды не равны, а в случаи если операнды равны вернёт значение 0 (false).

Операция < (меньше чем) выполняет сравнение, и возвращает значение 1 (true), если значение первого операнда меньше значения второго, в противном случае она возвращает значение 0 (false).

Операция > (больше чем) сравнивает два операнда и вернёт значение 1 (true), если значение первого операнда больше второго. В противном случаи, операция вернёт значение 0 (false).

Операция <= (меньше чем или равно) выполняет сравнение, и возвращает значение 1 (true), если значение первого операнда меньше или равно значению второго, в противном случае она возвращает значение 0 (false).

Операция >= (больше чем или равно) сравнивает два операнда и вернёт значение 1 (true), если значение первого операнда больше или равно второму. В противном случаи, операция вернёт значение 0 (false).

Эти операции часто используется для проверки счетчика цикла.

Операции сравнения полезны при проверке состояния двух переменных с целью выполнения условного ветвления в программе.

  1. Стандартно напишем новую программу comparison.c, и в главной функции объявим и инициализируем переменные целочисленного типа и типа символьного:
    #include <stdio.h>
    
    int main()
    {
       int zero = 0;
       int nil = 0;
       int one = 1;
    
       char upr = 'A';
       char lwr = 'a';
    
       return 0;
    }
  2. Далее в главной функции выведите результат выполнения операций сравнения над переменными:
    #include <stdio.h>
    
    int main()
    {
       int zero = 0;
       int nil = 0;
       int one = 1;
    
       char upr = 'A';
       char lwr = 'a';
    
       printf("\nEquality (0 == 0): %d\n", zero == nil);
       printf("Equality (0 == 1): %d\n", zero == one);
       printf("Equality ('A' == 'a'): %d\n", upr == lwr);
    
       printf("Inequality ('A' != 'a'): %d\n", upr != lwr);
        
       printf("Greater than (1 > 0): %d\n", one > nil);
    
       printf("Less than (1 < 0): %d\n", one < nil);
    
       printf("Greater or Equal than (0 >= 0): %d\n", zero >= nil);
    
       printf("Less or Equal than (1 <= 0): %d\n\n", one <= nil);
    
       return 0;
    }
    ASCII-код для символа верхнего регистра 'A' равен 65, а для символа нижнего регистра 'a' — 97, поэтому при их сравнении будет возвращено значение 0 (false - ложь).
  3. Сохраните файл программы, а затем скомпилируйте и запустите программу, чтобы увидеть результат выполнения операций сравнения:
    Работа операций сравнения значений переменных - возвращение 0 или 1 по результату сравнения.

Логические значения

Термин булев относится к системе логического мышления, разработанной английским математиком Джорджем Булем.
Джордж Буль (1815–1864). Один из основателей математической логики.

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

Логические Сравнения. Основные Операции.
Символ Операция читается так
&& И левый И правый операнды Истинны
|| ИЛИ левый ИЛИ правый операнд Истинный
! Поменять логическое значение на противоположное. Инвертировать.

Операция логического И(&&), сравнит два операнда и вернёт значение true только в том случае, если оба операнда имеют значение true. В противном случае, операция && вернёт значение false.

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

В отличие от операции И(&&), требующей, чтобы оба операнда имели значение true, операция логического ИЛИ(||), оценивает два операнда и возвращает значение true, если хотя бы один из них имеет значение true. Если ни один из операндов не имеет значения true, операция ИЛИ(||) вернёт значение false. В программе эта операция может быть полезной, если требуется проверить, было ли соблюдено хотя бы одно из условий.

Третья операция — логическое НЕТ(!), — является унарной операцией, она применяется только для одного операнда. Она возвращает инвертированное(противоположное) значение заданного операнда, поэтому, если переменная var имеет значение true, выражение !var вернёт значение false. Операция НЕТ(!) полезна при программировании, если нужно инвертировать значение переменной в успешных итерациях цикла с помощью утверждения вроде var = !var. Это гарантирует, что каждый раз значение будет инвертировано, и напоминает выключение и включение лампочки выключателем.

В языке C, 0 представляет собой значение false, а любое ненулевое значение, например, 9 — значение true.

  1. Начните стандартно новую программу logic.c, и в главной функции сразу объявите и инициализируйте переменные типа int:
    #include <stdio.h>
    
    int main()
    {
       int yes = 1;
       int  no = 0;
    
       return 0;
    }
  2. Далее в главной функции выведите результат выполнения логических операций над переменными:
    #include <stdio.h>
    
    int main()
    {
       int yes = 1;
       int  no = 0;
    
       printf("\nAND (no && no): %d\n", no && no);
       printf("AND (yes && no): %d\n", yes && no);
       printf("AND (yes && yes): %d\n", yes && yes);
    
       printf("OR (no || no): %d\n", no || no);
       printf("OR (yes || no): %d\n", yes || no);
       printf("OR (yes || yes): %d\n", yes || yes);
    
       printf("NOT (yes !yes): %d %d\n", yes, !yes);
       printf("NOT (no !no): %d %d\n\n", no, !no);
    
       return 0;
    }
  3. Сохраните файл программы, а затем скомпилируйте и выполните программу, чтобы увидеть результат выполнения логических операций:
    Результат работы Логических операций с булевыми значениями

Проверка условий

Возможно, самой любимой операцией программистов, пишущих на C, является условная операция ?: , также известная как тернарная(тройная) операция. Она сначала оценивает результат выполнения выражения (true или false), а затем выполняет одно из двух заданных утверждений в зависимости от результата оценки.

Условная тернарная операция имеет следующий синтаксис:

 <выражение-для-проверки> ? <если-true-выполнить-это> : <если-false-выполнить-это>;

Эта операция может быть использована, например, для оценки того, является ли заданное число чётным или нечётным путём проверки наличия остатка от деления на 2, а затем вывода соответствующей строки:

(7 % 2 != 0) ? printf ("Чётное число") : printf("Нечётное число");

В этом примере деление числа 7 на число 2 оставляет ненулевой остаток, поэтому выражение имеет значение true, а значит, будет выполнено первое утверждение, правильно описывая число как Нечётное.

Условная тернарная операция также может быть полезна для контроля за грамматикой в тексте, выводимом на экран, когда речь идёт о единственном и множественном числе, избегая неловких фраз наподобие «Имеем пять ножницы». Такую проверку легко выполнить внутри утверждения printf():

 printf("Имеем %d %s", (num == 1 ? "ножницы" : "ножниц", num ));

В этом примере в случае, если значение выражения равно true, значение переменной num равно 1 и будет использован вариант «ножницы», в противном случае будет использован вариант «ножниц».

Условная операция также может быть использована для присвоения соответствующих значений переменной в зависимости от результата проверки. Синтаксис выражения будет выглядеть так:

<переменная> = (<проверочное-выражение>) ? <если-true-назначить-это-значение> : <если-false-назначить-это-значение>

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

int num;
int a = 5;
int b = 2;
num = (a > b) ? a : b;

В данном примере значение переменной a больше значения переменной b, поэтому переменной num присваивается большее значение 5.

  1. Начните стандартно новую программу conditional.c, объявим и инициализируем целочисленную переменную:
    #include <stdio.h>
    
    int main()
    {
       int num = 7;
    
       return 0;
    }
  2. Добавьте условное утверждение, позволяющее вывести чётность значения переменной:
    #include <stdio.h>
    
    int main()
    {
       int num = 7;
    
       (num % 2 != 0) ? printf("\n%d is add\n", num) : printf("%d is even\n", num);
    
       return 0;
    }
  3. Добавьте условное утверждение, позволяющее вывести грамматически правильную фразу:
    #include <stdio.h>
    
    int main()
    {
       int num = 7;
    
       (num % 2 != 0) ? printf("\n%d is add\n", num) : printf("%d is even\n", num);
       printf("There %s ", (num == 1) ? "is" : "are");
       printf("%d %s\n", num, (num == 1) ? "apple" : "apples");
    
       return 0;
    }
  4. Уменьшите значение переменной, а затем снова добавьте условное утверждение, позволяющее вывести грамматически правильную фразу:
    #include <stdio.h>
    
    int main()
    {
       int num = 7;
    
       (num % 2 != 0) ? printf("\n%d is add\n", num) : printf("%d is even\n", num);
       printf("There %s ", (num == 1) ? "is" : "are");
       printf("%d %s\n", num, (num == 1) ? "apple" : "apples");
    
       num = 1;
    
       printf("There %s ", (num == 1) ? "is" : "are");
       printf("%d %s\n\n", num, (num == 1) ? "apple" : "apples");
    
       return 0;
    }
  5. Сохраните файл, а затем запустите программу, чтобы увидеть результат выполнения условных операций:
    Пример работы тернарного условного оператора.
Условную тернарную операцию лучше всего использовать для утверждений, имеющих всего две простые альтернативы.
Но всё же, несмотря на то, что условная тернарная операция проста в применении, её использование может снизить читабельность кода.

Итак, мы изучили три типа операций:

  1. Унарные - операция проводится только над одним операндом.
  2. Бинарные - операция проводится только с двумя операндами.
  3. Тернарные - операция проводится только при наличии трёх операндов.

Существует пять видов операций:

  1. Арифметические - Математика.
  2. Логические - Проверка Условий.
  3. Битовые - Булева алгебра.
  4. Присвоения - Краткая форма записи комбинации операции присвоения и дополнительной операции.
  5. Адресации - Работа с адресами памяти.
Основные Операции языка C
Символ Операции Операция Тип операции Вид операции
+ Сложение Бинарная Арифметическая
- Вычитание Бинарная/Унарная Арифметическая
* Умножение Бинарная Арифметическая
/ Деление Бинарная Арифметическая
% Остаток после деления Бинарная Арифметическая
++ Инкремент Унарная Арифметическая
-- Декремент Унарная Арифметическая
~ НЕ Двоичное Унарная Битовая
| ИЛИ Двоичное Бинарная Битовая
^ ИСКЛЮЧАЮЩЕЕ ИЛИ Логическое Бинарная Битовая
<< Сдвиг в Лево Бинарная Битовая
>> Сдвиг в Право Бинарная Битовая
& И Двоичное Бинарная Битовая
! НЕ Логическое Унарная Логическая
< Меньше чем Бинарная Логическая
<= Меньше или Равно Бинарная Логическая
> Больше чем Бинарная Логическая
>= Больше или Равно Бинарная Логическая
|| ИЛИ Логическое Бинарная Логическая
!= НЕ РАВНО Логическое Бинарная Логическая
&& И Логическое Бинарная Логическая
? : Если условие верно, то... Тернарная Логическая
= Присвоить Бинарная Присвоения
+= Прибавить и Присвоить Бинарная Присвоения
-= Вычесть и Присвоить Бинарная Присвоения
*= Умножить и Присвоить Бинарная Присвоения
/= Разделить и Присвоить Бинарная Присвоения
%= Остаток от Деления и Присвоить Бинарная Присвоения
<<= Сместить в Лево и Присвоить Бинарная Присвоения
>>= Сместить в Право и Присвоить Бинарная Присвоения
&= И Битовое и Присвоить Бинарная Присвоения
^= ИСКЛЮЧАЮЩЕЕ ИЛИ Битовое и Присвоить Бинарная Присвоения
|= ИЛИ Битовое и Присвоить Бинарная Присвоения
& Амперсанд Унарная Адресации
* Звёздочка Унарная Адресации

Кроме основных операций, существует и несколько специфических операций:

Специальные Операции языка C
Символ Операции Операция Вид операции
sizeof() sizeof() Возврат размера
. Точка Обращения к члену структуры
, Запятая Перечисление элементов
-> Стрелка Указатель на Структуру / Член структуры

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

Визуальные представления работы Операций сравнения - они же Условные операторы

Владимир Паронджанов. Aвтор ряда книг по визуальному алгоритмическому языку ДРАКОН - именно логику ДРАКОНа мы будем использовать при создании блок-схем в этом учебнике. Страница Владимира Паронджанова

Визуально представим работу условных операторов в виде блок-схем.

Итак, выше мы узнали об условных операторах языка С, результатом выполнения сравнения двух операндов, значений или утверждений, с помощью которых, они возвращают нам либо 0 (false) - Ложь, либо 1 (true) - Истина.

Понять работу условных операторов становится очень легко, если вместо английских слов true / false использовать слова да / нет как-бы отвечая на вопросы задаваемые условными операторами.

Опишем все фигуры для обозначения действий при составлении блок-схем в контексте языка программирования С:

Все фигуры для построения блок-схем
Фигура(Изображение) Название Применение Пример
Старт Обычно так обозначается точка входа в главную функцию main()
Завершение программы Обычно так обозначается выход из главной функции main()
Утверждение Любое утверждение на языке С
Условный оператор Ветвление программы по условию либо в ветку 1(true/истина/да) либо в ветку 0(false/ложь/нет)
Переключатель варианта Выбирает один из вариантов выражения
Вариант переключателя Вариант, соответствующий значению утверждения в Переключателе
Объявление Функции Объявление Функции. Точка входа в функцию
Вызов функции Вызов функции. Перейти в функцию по её имени/адресу
Комментарий Добавление комментария. Обычно справа/сверху от фигуры, но может быть и вертикально по линии.
Процедура Асинхронная внешняя процедура, блок кода, модуль и т.п. - без возможности управления ею. Пока процедура не вернётся "завершением" или "ошибкой" - Главная функция ждёт.
Параметры Формальные параметры необходимые для запуска кода. Инструкции препроцессора и т.п. данные До Главной функции.
Структура Название "полки" со списком элементов данных в ней
Начало цикла Сжатое визуальное представление начальной конструкции циклов(for, while, do-while)
Конец цикла Сжатое визуальное представление конечной конструкции циклов(for, while, do-while)
Отправить параметр Обычно обозначают отправку параметра/ов какому либо получателю
Получить параметр/ры Обычно обозначают получение параметра/ов от какого либо получателя
Отправить Чаще всего обозначают печать в консоль
Получить Чаще всего обозначают ввод данных пользователем из консоли
Часы Инициирование утверждения в заданный момент реальных даты/времени
Длительность Выполнение утверждения/ий в течении заданного промежутка времени
Таймер Подождать заданное время, прежде чем выполнить утверждение
Параллелизм Запустить два или более параллельных потока(веток) кода одновременно
Управление Параллельным фоновым процессом Запустить/Приостановить/Продолжить/Завершить независимый Параллельный фоновый процесс. Главная функция продолжает выполняться далее - не ждёт пока Выполняющийся фоновый процесс вернёт своё "завершение" или "ошибку".
Соединитель Буквенно-цифровой индекс соединителя линии/ий алгоритма указывает на такой же соединитель на другом листе/странице

Главные правила создания блок-схем

Опишем главные правила создания блок-схем, которых стоит придерживаться максимально строго и как правильно понимать блок-схемы когда мы на них смотрим:

  1. Главная задача блок-схем - Сделать программу Визуально Понятной!
  2. Движение по линиям идёт Сверху Вниз и Слева Направо. Исключением являются линии возврата в исходную точку условия.
  3. Линия всегда заходит в фигуру Сверху.
  4. Линия всегда выходит из фигуры Снизу. А для фигуры Условие обязательной является линия выходящая на право и вниз.
  5. Ветвление кода всегда изображено линией идущей Вправо-Вниз.
  6. На линиях ветвления по условию всегда указывайте их назначения: 1 или 0, ДА(истина) или НЕТ(ложь), true или false.
  7. Предпочтительнее, На линиях ветвления по условию указывать их назначения как ДА или НЕТ.
  8. Пересечение линий Запрещено!
  9. Слияние линий в одну ветку Разрешено.
  10. Линии со стрелками разрешены только при возврате кода в исходные точки. Такие точки всегда сверху слева или сверху справа.
  11. Слева блок-схемы всегда Маршрут Приемлемой Ситуации исхода выполнения алгоритма.
  12. Справа блок-схемы всегда Маршрут Худшего исхода выполнения алгоритма.
  13. Алгоритм строим исключительно на основе правил бинарной логики!
  14. Одинаковые Фигуры можно сливать в одну на основе правил бинарной логики!
  15. В Фигуры одного типа запрещено вписывать утверждения языка С другого типа.
  16. Добавляйте Фигуры "Комментарий" если считаете что блок-схема в этом месте не очевидна.

По мере составления блок-схем будем уточнять и расширять правила и особенности их создания.

Повторим изученное ранее и добавим новые знания

IF - Условный оператор ветвления потока выполнения программы

По сути if() это функция принимающая в качестве аргумента условный оператор, который возвращает либо 1 либо 0.

Как мы уже изучили: 1 - истинность условия, т.е. условие выполняется. 0 - условие не выполняется.

Читаются блоки IF-кода в вопросительное форме как в обычной речи:

Читаем блок IF-кода по человечески
IF-блок Читается так Код на C Блок-схема
if(A==B) A равно B ?
if(A == B)
{
   /*Сюда пишем код если проверка на Равенство выполняется*/
}
/*Выход из IF-блока если проверка на Равенство НЕ выполняется*/
if(A==B) else Если A равно B то... иначе это...
if(A == B)
{
   /*Сюда пишем код если проверка на Равенство выполняется*/
} else {
   /*Сюда пишем код если проверка на Равенство НЕ выполняется*/
}
/*Выход из IF-блока*/
if(A!=B) A не равно B ?
if(A != B)
{
   /*Сюда пишем код если проверка на Не Равенство выполняется*/
}
/*Выход из IF-блока если проверка на Не Равенство НЕ выполняется*/
if(A!=B) else Если A не равно B то... иначе это...
if(A != B)
{
   /*Сюда пишем код если проверка на Не Равенство выполняется*/
} else {
   /*Сюда пишем код если проверка на Не Равенство НЕ выполняется*/
}
/*Выход из IF-блока*/
if(A<B) A меньше B ?
if(A < B)
{
   /*Сюда пишем код если проверка на Меньше чем выполняется*/
}
/*Выход из IF-блока если проверка на Меньше чем НЕ выполняется*/
if(A<B) else Если A меньше B то... иначе это...
if(A < B)
{
   /*Сюда пишем код если проверка на Меньше чем выполняется*/
} else {
   /*Сюда пишем код если проверка на Меньше чем НЕ выполняется*/
}
/*Выход из IF-блока*/
if(A>B) A больше B ?
if(A > B)
{
   /*Сюда пишем код если проверка на Больше чем выполняется*/
}
/*Выход из IF-блока если проверка на Больше чем НЕ выполняется*/
if(A>B) else Если A больше B то... иначе это...
if(A > B)
{
   /*Сюда пишем код если проверка на Больше чем выполняется*/
} else {
   /*Сюда пишем код если проверка на Больше чем НЕ выполняется*/
}
/*Выход из IF-блока*/
if(A<=B) A меньше или равно B ?
if(A <= B)
{
   /*Сюда пишем код если проверка на Меньше или Равно выполняется*/
}
/*Выход из IF-блока если проверка на Меньше или Равно НЕ выполняется*/
if(A<=B) else Если A меньше или равно B то... иначе это...
if(A <= B)
{
   /*Сюда пишем код если проверка на Меньше или Равно выполняется*/
} else {
   /*Сюда пишем код если проверка на Меньше или Равно НЕ выполняется*/
}
/*Выход из IF-блока*/
if(A>=B) A больше или равно B ?
if(A >= B)
{
   /*Сюда пишем код если проверка на Больше или Равно выполняется*/
}
/*Выход из IF-блока если проверка на Больше или Равно НЕ выполняется*/
if(A>=B) else Если A больше или равно B то... иначе это...
if(A >= B)
{
   /*Сюда пишем код если проверка на Больше или Равно выполняется*/
} else {
   /*Сюда пишем код если проверка на Больше или Равно НЕ выполняется*/
}
/*Выход из IF-блока*/
if(A) Если A ? (Истина если значение А не 0 (не false))
if(A)
{
   /*Сюда пишем код если проверка на Истинность выполняется*/
}
/*Выход из IF-блока если проверка на Истинность НЕ выполняется*/
if(!A) Если Инверсия A ? (Истина если значение А не 1 (не true))
if(!A)
{
   /*Сюда пишем код если проверка на Ложность выполняется*/
}
/*Выход из IF-блока если проверка на Ложность НЕ выполняется*/

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

A и B - это любые переменные, значения, утверждения, условные операторы - т.е. внутрь условий можно добавлять вложенные условия. Вот примеры:

Вложенные IF-блоки внутрь IF-блоков с помощью else if комбинаций:

Такие вложения условий внутрь условий, можно назвать "лестница условий". С точки зрения создания прозрачного кода, нужно придерживаться правил №11-№12 - Слева блок-схемы всегда Маршрут Приемлемой Ситуации исхода выполнения алгоритма. Справа блок-схемы всегда Маршрут Худшего исхода выполнения алгоритма.

Пример кода запрашивающий у пользователя значение и по "лестнице условий" печатает в консоль результат:

Пример кода определяющий соответствие количества балов полученных студентом и по "лестнице условий" назначающий "Уровень" знаний студента и печатает в консоль результат:

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

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

Правило №13 - Алгоритм строим исключительно на основе правил бинарной логики! Пример кода реализует логическую проверку условий применяя бинарную логическую операцию И(&&), записанная инструкция препроцессору #include <stdbool.h> подключает заголовочный файл предоставляющий возможность применять в тексте исходного кода ключевые слова true и false. Прочитайте исходный код и изучите блок-схему, чтобы понять - что делает программа:

Правило №13 - Алгоритм строим исключительно на основе правил бинарной логики! Пример кода реализует логическую проверку условий применяя бинарные логические операции И(&&) и ИЛИ(||), записанная инструкция препроцессору #include <stdbool.h> подключает заголовочный файл предоставляющий возможность применять в тексте исходного кода ключевые слова true и false. Обратите внимание, в языке С логическая бинарная операция И(&&) имеет более высокий приоритет выполнения перед логической бинарной операцией ИЛИ(||). Прочитайте исходный код и внимательно изучите блок-схему, что бы понять, что делает программа:

Правило №13 - Алгоритм строим исключительно на основе правил бинарной логики! Пример кода реализует логическую проверку условий применяя бинарные логические операции И(&&) и ИЛИ(||), записанная инструкция препроцессору #include <stdbool.h> подключает заголовочный файл предоставляющий возможность применять в тексте исходного кода ключевые слова true и false. Обратите внимание, в языке С логическая бинарная операция И(&&) имеет более высокий приоритет выполнения перед логической бинарной операцией ИЛИ(||). Прочитайте исходный код и внимательно изучите блок-схему, что бы понять, что делает программа:

Правило №13 - Алгоритм строим исключительно на основе правил бинарной логики! Пример блок-схемы демонстрирует вложенность конструкции if...else внутри конструкции if...else.

Правило №13 - Алгоритм строим исключительно на основе правил бинарной логики! Пример блок-схемы демонстрирует вложенность двух конструкции if...else внутри конструкции if...else. В зависимости от выбранной ветки в первой конструкции if...else далее проверяется условие по одному из вложенных условий реализованных в конструкции if...else

Правило №13 - Алгоритм строим исключительно на основе правил бинарной логики! Пример программы демонстрирует вложенность двух конструкции if...else внутри конструкции if...else. В зависимости от выбранной ветки в первой конструкции if...else далее проверяется условие по одному из вложенных условий реализованных в конструкции if...else . Прочитайте исходный код и внимательно изучите блок-схему, что бы понять, что делает программа:

Подробнее рассмотрим блок-схему Тернарного условия. Ещё раз посмотрите на правильный синтаксис:

( A ) ? [ выполняется если A true ] : [ выполняется если  A false ];

Напомню, что любое значение переменной не равное 0 - является состоянием условия как TRUE, (A <> 0) == true, что тоже самое (A != 0) == true.

Внутри тернарного условия можно записывать обычное условие:

Вот эквивалентная запись тернарного условия:

(condition) ? (variable = valueTrue) : (variable = valueFalse);

и реализация его развёрнутого if...else варианта:

if(condition)
{
   variable = valueTrue;
}
else
{
   variable = valueFalse;
}

Пример блок-схемы и её код, реализующий тернарное условие. Переменная А = 7 == true:

Пример блок-схемы и её код, реализующий тернарное условие в альтернативной его записи(без блока if...else ). Переменная А = 0 == false:

Пример блок-схемы и её код, реализующий тернарное условие. Переменная А = 0 == false:

Пример блок-схемы и её код, реализующий тернарное условие в альтернативной его записи(без блока if...else ). Переменная А = 5 == true:

Обратите внимание, что при альтернативной записи тернарного условия - обязательны круглые скобки. Если вы не запишете круглые скобки - то на примере блок-схемы - у вас отсутствуют линии связи между фигурами. А вот что происходит при попытке компиляции приведённого ниже кода:

Скобки в тернарном условии так же нужно использовать если применяется вызов функций. Вот один вариант записи:

Вот другой вариант записи:

Напоминаю - прописывайте самостоятельно весь код приведённый в учебнике. Сохраняйте программы, компилируйте и запускайте их. Копирование кода из учебника в Visual Studio Code редактор - не позволяет образовываться в вашем мозге связям между нейронами отвечающими за запоминание визуальных образов(тексты, картинки) и движением пальцев рук. Когда же вы всё пишите самостоятельно то мозг запоминает гораздо лучше в процессе обучения на практике.

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

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

Битовые операции

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

Ранее мы уже изучили, что каждый байт состоит из восьми бит, каждый из которых может содержать 1 или 0. Все вместе они дают двоичное значение, представляющее десятичное число от 0 до 255. Для визуального представления, биты обозначаются в формате «справа-налево» от наименее значащего бита (Less Significant Bit, LSB) до наиболее значащего бита (Most Significant Bit, MSB). Двоичное число, приведённое ниже, представляет собой десятичное число 50.

Визуальное представление бит в байте - число 50
MSB LSB
Номер бита в байте 7 6 5 4 3 2 1 0
Десятичные разряды 128 64 32 16 8 4 2 1
Двоичные разряды 0 0 1 1 0 0 1 0

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

Битовые операции
Символ Операции Название Операции Выполняет
| (вертикальная черта) ИЛИ (Bitwise OR) Возвращает 1 в бите, если ИЛИ один ИЛИ другой из сравниваемых битов имеет значение 1
| 0 1
0 0 1
1 1 1
& (амперсанд) И (Bitwise AND) Возвращает 1 в бите, если И один И другой сравниваемых бита имеют значение 1
& 0 1
0 0 0
1 0 1
~ (тильда) ИНВЕРСИЯ (Bitwise NOT) ИНВЕРТИРУЕТ бит на противоположный
~ 0 1
1 0
^ (степень) Исключающее ИЛИ (Bitwise XOR) Возвращает 1 в бите, если только один из сравниваемых битов имеет значение 1
^ 0 1
0 0 1
1 1 0
<< (две угловые скобки влево) Битовый сдвиг влево (Bitwise left shift) Перемещает каждый бит, имеющий значение 1, на определённое количество разрядов влево
Пример: 0010 << 2 = 1000
>> (две угловые скобки вправо) Битовый сдвиг вправо (Bitwise right shift) Перемещает каждый бит, имеющий значение 1, на определённое количество разрядов вправо
Пример: 1000 >> 2 = 0010

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

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

  1. Начнём писать стандартно новую программу xor.c, объявим и инициализируем целочисленные переменные и распечатаем их значения в консоли:
    #include <stdio.h>
    
    int main()
    {
       int x = 10;
       int y = 5;
       printf("\nX = %d\nY = %d\n", x, y);
    
       return 0;
    }
  2. Добавьте три утверждения ИСКЛЮЧАЮЩИЕ ИЛИ (^), чтобы поменять местами значения переменных путём битовых манипуляций, распечатайте в консоль результат битовых операций над переменными:
    #include <stdio.h>
    
    int main()
    {
       int x = 10;
       int y = 5;
       printf("\nX = %d\nY = %d\n", x, y);
    
       x = x ^ y; /* 1010 ^ 0101 = 1111 (десятичное число 15) */
       y = x ^ y; /* 1111 ^ 0101 = 1010 (десятичное число 10) */
       x = x ^ y; /* 1111 ^ 1010 = 0101 (десятичное число 5) */
       printf("\nX = %d\nY = %d\n\n", x, y); /* В результате, переменные поменялись своими значениями */
    
       return 0;
    }
  3. Сохраните файл программы, а затем скомпилируйте и выполните программу, чтобы увидеть результат битовой операции ИСКЛЮЧАЮЩИЕ ИЛИ (^):
    Результат работы битовой операции ИСКЛЮЧАЮЩИЕ ИЛИ (^).
Не путайте битовые операции с логическими. Битовые операции оперируют двоичными значениями бит чисел, а логические - проверяют булевы значения.

Измерение размера данных определённого типа

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

Когда операнд, переданный параметром в функцию sizeof(), является именем типа данных, например, int, он должен быть помещён в скобки () — как в примерах, выше.

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

Простейшим хранилищем в языке C является тип данных char, который хранит один символ в одном байте памяти. Это значит, что функция вида sizeof(char) вернёт 1 (один байт).

#include <stdio.h>

int main(void)
{
   printf("\nSize of char data type is %zu byte.\n\n", sizeof(char));

   return 0;
}

Результат определения размера типа данных char:

Функция sizeof(char) возвращает значение в один байт.

Как правило, для типов данных int и float выделяется 4 байта машинной памяти, а для типа данных double — 8 байт, что позволит разместить весь диапазон его значений. Эти размеры не являются стандартизированными, они зависят от реализации вашей машины и компилятора и потому могут изменяться. Хорошим тоном при программировании является использование функции sizeof() для измерения размера выделенной памяти.

Стоит заметить, что современные персональные компьютеры практически все уже имеют многоядерные процессоры, 64-битные операционные системы, и более 4 Гб Оперативной Памяти. Это практически всегда гарантирует задействование компилятором - полномерных размеров типов данных, т.е. не урезанных.
НО! При программировании встроенных систем на основе микроконтроллеров - нужно наоборот стремится к минимально возможным размерам типов данных из-за ограничений объёмов Оперативной памяти и размеров хранилищ на этих системах, часто не имеющих возможности к увеличению объёма.

Объём памяти, выделенный для массивов переменных, складывается из количества байт, выделяемых для заданного типа данных, умноженного на число элементов массива. Например, выражение sizeof(int[3]), как правило, вернёт число 12 (3 элемента в массиве × по 4 байта каждый).

Особенно важно использовать функцию sizeof() для того, чтобы точно определить объём памяти, выделенный для определённых пользователем структур, которые могут иметь члены различных типов данных, поскольку часто между ними автоматически добавляется так называемая прослойка выравнивания. Это значит, что общий объём выделенной памяти может превосходить сумму объёмов памяти, выделенной для каждого члена. Логично было бы ожидать, что для структуры, содержащей переменные int score и char grade, будет выделено 5 байт (4 для int + 1 для char ), но, как правило, в таких ситуациях компилятором выделяется 8 байт. Это происходит потому, что (32 битные) компьютерные системы обычно считывают данные словами, имеющими размер 4 байта. Поэтому при выделении памяти добавляется прослойка, позволяющая дополнить объём выделяемой памяти до числа, кратного 4. Тем самым выравнивая блок памяти, и сделав его оптимальным для максимально быстрой обработки процессором.

  1. Стандартно начнём писать новую программу size.c, объявим и инициализируем переменную типа int
    #include <stdio.h>
    
    int main()
    {
       int num = 123456789;
    
       return 0;
    }
  2. В главной функции добавьте утверждения, позволяющие узнать объём памяти, выделенной для типа данных int (в вашей реализации), с помощью имени типа данных и имени переменной. Объявим и инициализируем переменную типа int:
    #include <stdio.h>
    
    int main()
    {
       int num = 123456789;
       printf("\nSize of 'int' data type is %d bytes", sizeof(int));
       printf("\nSize of 'num' variable is %d bytes\n", sizeof(num));
    
       return 0;
    }
  3. Теперь добавьте утверждение, позволяющее вывести на экран объём памяти, который выделяется для каждого элемента массива:
    #include <stdio.h>
    
    int main()
    {
       int num = 123456789;
       printf("\nSize of 'int' data type is %d bytes", sizeof(int));
       printf("\nSize of 'num' variable is %d bytes\n", sizeof(num));
       
       printf("\nSize of an 'int' array is %d bytes\n", sizeof(int[3]));
    
       return 0;
    }
  4. Определите структуру, содержащую одну переменную типа char и одну переменную типа int, а затем выведите на экран объём памяти, выделенный структуре, включая прослойку(байты) выравнивания:
    #include <stdio.h>
    
    int main()
    {
       int num = 123456789;
       printf("\nSize of 'int' data type is %d bytes", sizeof(int));
       printf("\nSize of 'num' variable is %d bytes\n", sizeof(num));
       
       printf("\nSize of an 'int' array is %d bytes\n", sizeof(int[3]));
    
       struct
       {
          int score;
          char grade;
       } result;
       printf("\nSize of a structure is %d bytes\n\n",  sizeof(result));
    
       return 0;
    }
    В этом примере приведён код для создания объекта типа struct{}, который представляет собой единый набор переменных различных типов данных. Тема структуры будет подробно рассмотрена далее, в данном же коде структура задействована для того, чтобы продемонстрировать, как можно измерить объём выделенной памяти для структуры с помощью функции sizeof().
  5. Сохраните файл программы, а затем скомпилируйте и выполните её:
    Результат работы программы печатающей в консоль размеры: типов данных, массива и структуры - с помощью функции sizeof() .

Флаги

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

Битовый флаг, как и настоящий - когда поднят, то говорят флаг установлен(1), а когда флага нет - говорят флаг сброшен(0).

Флаг считается установленным в значение 1, и сброшенным в значение 0. Бывают редкие случаи когда состояние Флага не определённое.

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

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

Исходное значение битовых флагов может быть установлено путём присвоения переменной десятичного значения, двоичный эквивалент которого имеет единицы в тех битах, которые должны установить битовый флаг. Например, десятичное число 8 имеет двоичный эквивалент 1000 (1 × 8 + 0 × 4 + 0 × 2 + 0 × 1), поэтому установленным будет четвёртый битовый флаг, если считать справа налево от наименее значащего бита.

Значения битовых флагов могут быть инвертированы с помощью битовой операции НЕ (~, ИНВЕРСИЯ), однако, следует использовать маску для нулей, идущих перед битовыми флагами, иначе каждый из них получит значение 1. По этому - маска блокирует изменение состояний тех Флагов, которые в данный момент времени ИНВЕРТИРОВАТЬ нельзя.

Например, чтобы ИНВЕРТИРОВАТЬ битовое поле из четырёх флагов, располагающееся с правой стороны байта, то к четырём левым битам должна быть применена маска. Десятичное число 15 имеет двоичное представление 00001111 (0 × 128 + 0 × 64 + 0 × 32 + 0 × 16 + 1 × 8 + 1 × 4 + 1 × 2 + 1 × 1), его разрешается использовать как маску с помощью операции И (&).

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

  1. Напишем новую программу bitflag.c . Объявим и инициализируем переменную типа int :
    #include <stdio.h>
    
    int main()
    {
       int flags = 8; /* Двоичное представление 1000 (1×8 + 0×4 + 0×2 + 0×1) */
    
       return 0;
    }
  2. Далее в главной функции назначьте переменной новое значение, чтобы значение 1 получил также второй флаг. Применим логическую битовую операцию ИЛИ(|):
    #include <stdio.h>
    
    int main()
    {
       int flags = 8; 
       flags = flags | 2; /* 1000 | 0010 = 1010 (Десятичное число 10) */
    
       return 0;
    }
  3. Добавьте утверждения, позволяющие печатать в консоли значения всех битовых флагов. Применим проверку условия с помощью записи тернарного условия:
    #include <stdio.h>
    
    int main()
    {
       int flags = 8;
       flags = flags | 2;
       
       printf("\nFlag 1: %s\n", ((flags & 1) > 0) ? "ON" : "OFF");
       printf("\nFlag 2: %s\n", ((flags & 2) > 0) ? "ON" : "OFF");
       printf("\nFlag 3: %s\n", ((flags & 4) > 0) ? "ON" : "OFF");
       printf("\nFlag 4: %s\n", ((flags & 8) > 0) ? "ON" : "OFF");
    
       return 0;
    }
  4. Добавьте утверждения, накладывающие маску на первые четыре бита байта, а затем инвертируйте все значения битовых флагов. Обратите внимание - приоритет логической битовой операции И(&) выше, и следовательно сначала выполнится она, а потом результат будет ИНВЕРТИРОВАН:
    #include <stdio.h>
    
    int main()
    {
       int flags = 8;
       flags = flags | 2;
       
       printf("\nFlag 1: %s\n", ((flags & 1) > 0) ? "ON" : "OFF");
       printf("Flag 2: %s\n", ((flags & 2) > 0) ? "ON" : "OFF");
       printf("Flag 3: %s\n", ((flags & 4) > 0) ? "ON" : "OFF");
       printf("Flag 4: %s\n", ((flags & 8) > 0) ? "ON" : "OFF");
    
       char mask = 15;        /* В двоичном представлении это 00001111 */
       flags = ~flags & mask; /* ~ (1010 & 1111) = 0101 */
    
       return 0;
    }
  5. Добавьте утверждения, позволяющие распечатать в консоль измененные значения всех битовых флагов, а также десятичное значение, представляющее этот шаблон:
    #include <stdio.h>
    
    int main()
    {
       int flags = 8;
       flags = flags | 2;
       
       printf("\nFlag 1: %s\n", ((flags & 1) > 0) ? "ON" : "OFF");
       printf("Flag 2: %s\n", ((flags & 2) > 0) ? "ON" : "OFF");
       printf("Flag 3: %s\n", ((flags & 4) > 0) ? "ON" : "OFF");
       printf("Flag 4: %s\n", ((flags & 8) > 0) ? "ON" : "OFF");
    
       char mask = 15;
       flags = ~flags & mask;
    
       printf("\nFlag 1: %s\n", ((flags & 1) > 0) ? "ON" : "OFF");
       printf("Flag 2: %s\n", ((flags & 2) > 0) ? "ON" : "OFF");
       printf("Flag 3: %s\n", ((flags & 4) > 0) ? "ON" : "OFF");
       printf("Flag 4: %s\n", ((flags & 8) > 0) ? "ON" : "OFF");
       printf("Flags decimal value is %d\n\n", flags);
    
       return 0;
    }
  6. Добавьте утверждение, позволяющее выполнить сдвиг установленных флагов на один бит влево, а затем выведите десятичное значение нового шаблона:
    #include <stdio.h>
    
    int main()
    {
       int flags = 8;
       flags = flags | 2;
       
       printf("\nFlag 1: %s\n", ((flags & 1) > 0) ? "ON" : "OFF");
       printf("Flag 2: %s\n", ((flags & 2) > 0) ? "ON" : "OFF");
       printf("Flag 3: %s\n", ((flags & 4) > 0) ? "ON" : "OFF");
       printf("Flag 4: %s\n", ((flags & 8) > 0) ? "ON" : "OFF");
    
       char mask = 15;
       flags = ~flags & mask;
    
       printf("\nFlag 1: %s\n", ((flags & 1) > 0) ? "ON" : "OFF");
       printf("Flag 2: %s\n", ((flags & 2) > 0) ? "ON" : "OFF");
       printf("Flag 3: %s\n", ((flags & 4) > 0) ? "ON" : "OFF");
       printf("Flag 4: %s\n", ((flags & 8) > 0) ? "ON" : "OFF");
       printf("Flags decimal value is %d\n\n", flags);
    
       flags = flags << 1; /* 0101 << 1 = 1010 */
       printf("Flags decimal value now is %d\n\n", flags);
    
       return 0;
    }
  7. Сохраните, скомпилируйте и запустите программу:
Часто довольно удобно использовать заранее определённые константы, представляющие значение каждого битового флага. Например:
#define FLAG1 1
#define FLAG2 2
#define FLAG3 4
#define FLAG4 8
Более крупные битовые поля могут быть созданы с использованием переменной, резервирующей больший объём памяти. Например, переменная типа int, для которой резервируется 4 байта, может вместить 32 флага.

Знакомство с приоритетами

Приоритет операций определяет порядок их выполнения в утверждениях языка C. Например, в выражении a = 6 + b * 3 приоритет операций определяет, в каком порядке будут выполнены операции сложения и умножения.

Операция умножения, *, находится выше, чем операция сложения, +, поэтому в выражении a = 6 + b * 3 сначала выполняется операция умножения.

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

Приоритет выполнения операций
Приоритет операций Операция Ассоциативность
Постфиксные
  • ++ Суффиксный/Постфиксный Инкремент
  • -- Суффиксный/Постфиксный Декремент
  • ( ) Вызов функции
  • [ ] Подстрочные индексы массива
  • . Доступ к члену структуры и объединения
  • -> Доступ к члену структуры и объединения посредством Указателя
  • (type){list} Составной литерал
Слева направо
Унарные
  • ++ Префиксный Инкремент
  • -- Префиксный Декремент
  • + Унарный Знак «плюс»
  • - Унарный Знак «минус»
  • ! Логическое НЕ
  • ~ Логическое Битовое НЕ(ИНВЕРСИЯ)
  • (type) Приведение Типа
  • * Разыменование
  • & Знак адреса
  • sizeof() Размер
  • _Alignof() Требование Выравнивания
Справа налево
Мультипликативные
  • * Умножение
  • / Деление
  • % Деление с остатком
Слева направо
Аддитивные
  • + Cложение
  • - Вычитание
Слева направо
Сдвига
  • << Cдвиг влево
  • >> Cдвиг вправо
Слева направо
Отношения
  • < Меньше
  • <= Меньше или равно
  • > Больше
  • >= Больше или равно
  • == Равенство
  • != Неравенство
Слева направо
Битовое И
  • & Битовое И
Слева направо
Битовое ИСКЛЮЧАЮЩЕЕ ИЛИ
  • ^ Битовое ИСКЛЮЧАЮЩЕЕ ИЛИ
Слева направо
Битовое ИЛИ
  • | Битовое ИЛИ
Слева направо
Логическое И
  • && Логическое И
Слева направо
Логическое ИЛИ
  • || Логическое ИЛИ
Слева направо
Тернарная Условная операция
  • ?: Тернарная Условная операция
Справа налево
Присваивания
  • =
  • +=
  • -=
  • *=
  • /=
  • %=
  • &=
  • ^=
  • |=
  • <<=
  • >>=
Справа налево
Запятая
  • , (Запятая)
Слева направо
  1. Напишем новую программу precedence.c, в которой проверим приоритет выполнения операций по умолчанию, и с принудительным назначением приоритета с помощью круглых скобок:
    #include <stdio.h>
    
    int main()
    {
       printf("\nDefault precedence ((2*3)+4)-5 : %d\n", 2*3+4-5);
       printf("Explicit precedence 2*((3+4)-5) : %d\n", 2*((3+4)-5));
    
       return 0;
    }
  2. В главной функции выведите число-результат вычисления выражения, следующего правилам приоритета для ассоциативности слева направо, а затем результат вычисления выражения с явно определённым приоритетом:
    #include <stdio.h>
    
    int main()
    {
       printf("\nDefault precedence ((2*3)+4)-5 : %d\n", 2*3+4-5);
       printf("Explicit precedence 2*((3+4)-5) : %d\n", 2*((3+4)-5));
    
       printf("\nDefault precedence (7*3)%%2 : %d\n", 7*3%2);
       printf("Explicit precedence 7*(3%%2) : %d\n", 7*(3%2));
    
       return 0;
    }
  3. Теперь выведите число-результат вычисления выражения, следующего правилам приоритета для ассоциативности справа налево, а затем результат вычисления выражения с явно определённым приоритетом:
    #include <stdio.h>
    
    int main()
    {
       printf("\nDefault precedence ((2*3)+4)-5 : %d\n", 2*3+4-5);
       printf("Explicit precedence 2*((3+4)-5) : %d\n", 2*((3+4)-5));
    
       printf("\nDefault precedence (7*3)%%2 : %d\n", 7*3%2);
       printf("Explicit precedence 7*(3%%2) : %d\n", 7*(3%2));
    
       int num = 9;
       printf("\nDefault precedence (8/2)*4 : %d\n", --num/2*sizeof(int));
        
       num = 9;
       printf("Explicit precedence 8/(2*4) : %d\n\n", --num/(2*sizeof(int)));
    
       return 0;
    }
  4. Сохраните файл программы, а затем скомпилируйте и выполните программу, чтобы увидеть результаты, следующие правилам приоритета.
    Для того чтобы напечатать в консоль символ % в функции printf(), следует использовать комбинацию %%.

Заключение

Различные Проверочные утверждения

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

Проверка значений выражений

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

Ключевое слово if используется для выполнения простой проверки условия, которое оценивает булево значение заданного выражения. Утверждения внутри скобок выполняются только в том случае, если значение выражения равно(возвращает) true. Синтаксис проверочного утверждения if выглядит так:

if (проверочное-выражение) {утверждения-которые-следует-выполнить-если-true}
Точка с запятой ; после закрывающей фигурной скобки } не ставится.

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

if ((условие_1) && (условие_2)) {утверждения}

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

if (условие_1)
{
   /* Выполнится если условие_1 TRUE */
   if(условие_2) {
      /* Выполнится если условие_2 TRUE */
   }
}
/* Выполнится если условие_1 FALSE */
Блок-схема условия вложенного в условие и реализующая логическое И (&&)
Несколько выражений сразу можно оценить, объединив утверждения if и else, например, так: if () {…} else if () {…}.

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

Часто является предпочтительным расширять утверждение if, добавляя к нему утверждение else, указав внутри скобок утверждения, которые должны быть выполнены в случае, если проверочное выражение будет иметь значение false. Синтаксис выглядит так:

if (проверочное-выражение) /* Если TRUE ... */
   {
      утверждения-которые-следует-выполнить-если-true /* ... То */
   }
else /* Иначе ... */
   {
      утверждения-которые-следует-выполнить-если-false /* ... Это */ 
   }

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

  1. Напишем программу ifelse.c и изобразим её блок-схему. Прочитайте исходный код и на блок-схеме проследите какая ветка кода исполнится в том или ином случаи? Что будет напечатано в консоль?
    #include <stdio.h>
    
    int main()
    {
       if (5 > 1)
       {
          printf("Yes, 5 is greater than 1\n");
    
          if (7 > 2)
          {
             printf("5 is greater than 1 and 7 is greater than 2\n");
          }
       }
    
       if (1 > 2)
       {
          printf("1st Expression is true\n");
       }
       else if (1 > 3)
       {
          printf("2nd Expression is true\n");
       }
       else
       {
          printf("Both expressions are false\n");
       }
    
       return 0;
    }
  2. Составим блок-схему алгоритма:
    Проследите взглядом выполнение кода, указывая курсором на одну за другой фигуру и читая исполняемые внутри этой фигуры утверждение. Укажите, что будет напечатано в консоль.
  3. Сохраните, скомпилируйте и запустите программу.
Напоминаю, значение true имеет числовое представление 1, а значение false — числовое представление 0. Поэтому выражение (5 > 1) является сокращенной версией выражения (5 > 1 == 1).

Ветвление кода с помощью операции switch()

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

Утверждение switch() работает довольно просто. Оно получает переданное значение как параметр, а затем ищет совпадение среди некоторого количества утверждений case.

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

Код, который должен быть выполнен при совпадении, находится внутри каждого утверждения case.

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

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

По сути вот так работает функция условного ветвления кода switch() с использованием ключевого слова break для выхода из switch().
А вот так работает функция условного ветвления кода switch() без ключевого слова break. Как только будет найдено совпадение case, все утверждения ниже также будут выполнены, включая утверждения в ветке default. Т.е. без ключевого слова break в каждом case, выход из функции switch() произойдёт только в ветке default. Как эффективно использовать такой принцип работы switch(), читайте далее.

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

Синтаксис утверждений switch() обычно выглядит так:

switch (значение)
{
case значение_1:
   утверждения-которые-нужно-выполнить-при-совпадении;
   break; /* Выйти из switch() */

case значение_2:
   утверждения-которые-нужно-выполнить-при-совпадении;
   break; /* Выйти из switch() */

case значение_3:
   утверждения-которые-нужно-выполнить-при-совпадении;
   break; /* Выйти из switch() */

default: /* Если ни одно case значение не совпало */
   утверждения-которые-нужно-выполнить-при-отсутствии-совпадений;
   /* break; сюда писать не имеет смысла */
}

Согласно стандарту ANSI C, никакие два утверждения case не могут иметь одинаковое значение.

Утверждение с меткой default не должно обязательно появляться в конце блока switch, но поместить его туда логично, поскольку в этом случае для него не потребуется утверждения break.

Когда для некоторого количества элементов нужно выполнить одинаковые утверждения, только итоговое утверждение case должно их содержать. Например, для того, чтобы вывести одинаковое сообщение при значениях 0, 1 и 2, следует использовать следующий код:

switch (num)
{
case 0:
case 1:
case 2:
    printf("Less than 3\n");
    break;
case 3:
    printf("Exactly 3\n");
    break;
default:
    printf("Greater than 3 or less than zero\n");
}
Блок-схема верхнего примера кода.

Для составления более компактного вида блок-схемы с фигурой switch применяется следующая визуальная конструкция:

  1. Напишите новую программу switch.c и изучите её визуальное представление в виде блок-схемы:
  2. Сохраните, скомпилируйте и выполните программу. Введите различные значения, чтобы увидеть соответствующее поведение функции switch():
В утверждениях switch ключевое слово case и значение, указанное рядом и символ двоеточия за ним, рассматриваются компилятором как уникальная метка.
  1. Поставим задачу для решения которой необходимо написать С исходный код.

    Программа должна напечатать строку в консоль, соответствующую значению целочисленной переменой int.

    Программа должна напечатать строку в консоль, соответствующую символу символьной переменой char.

    Используем функцию ветвления кода switch() для обеих задач.

  2. Составим блок-схему для явного визуального представления программы.
  3. Отобразим все интересующие нас ветки case для функции ветвления switch()
    Блок-схема двух функций ветвления кода switch() идущих одна за другой.
  4. Напишем, сохраним и выполним новую программу switchchar.c на основе блок-схемы выше:
    #include <stdio.h>
    
    int main()
    {
       int num = 2;
       char letter = 'b';
    
       switch (num)
       {
       case 1:
          printf("\nNumber is One\n");
          break;
       case 2:
          printf("\nNumber is Two\n");
          break;
       case 3:
          printf("\nNumber is Three\n");
          break;
       default:
          printf("\nNumber is Unrecognized\n");
       }
    
       switch (letter)
       {
       case 'a':
       case 'b':
       case 'c':
          printf("\nLetter is '%c'\n\n", letter);
          break;
       default:
          printf("\nLetter is Unrecognized\n\n");
       }
    
       return 0;
    }
  5. Выполним программу:
    Пример исходного кода синтаксиса функции условного ветвления switch().

Зацикливание с помощью счётчика

Цикл — это фрагмент кода программы, который повторяется автоматически. Однократное выполнение всех утверждений внутри цикла называется итерацией или проходом. Количество итераций цикла контролируется проверкой условия, выполняемой внутри цикла. Пока проверяемое выражение имеет значение true, цикл станет продолжаться (до тех пор, пока значение не станет false, в этот момент цикл закончится и программа продолжит своё выполнение далее).

В программировании на языке C существуют три варианта структуры цикла — циклы for(), циклы while() и циклы do...while(). Циклы for имеют следующий синтаксис:

for (<начальное-выражение> ; <проверочное-выражение> ; <инкремент>)
{
   утверждения;
}

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

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

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

Мы уже рассматривали примеры фигуры Цикла для создания блок-схем. Применим эти знания для создания блок-схемы простейшего цикла for. Такой цикл называет пустым бесконечным циклом. Благодаря специфичности синтаксиса языка программирования С, такой цикл можно изобразить в виде блок-схемы и написать его так:

Пустой бесконечный цикл - выполняется бесконечно, ничего не делает. Такой цикл бесполезен, и в тоже время опасен. Однажды начавшись, у такого цикла нет условия его завершения. Попав в такой цикл - программа зависает, до её принудительного завершения со стороны пользователя.
Пустой бесконечный цикл - выполняется бесконечно, ничего не делает. Такой цикл бесполезен, несмотря на объявление и инициализацию целочисленной переменной для проверочного выражения, так же опасен. Однажды начавшись, у такого цикла нет условия его завершения. Попав в такой цикл - программа зависает, до её принудительного завершения со стороны пользователя.
Пустой бесконечный цикл - выполняется бесконечно, ничего не делает. Такой цикл бесполезен, несмотря на объявление и инициализацию целочисленной переменной для проверочного выражения, и наличия проверочного выражения, так же опасен. Однажды начавшись, у такого цикла нет условия его завершения. Попав в такой цикл - программа зависает, до её принудительного завершения со стороны пользователя.
Пустой конечный цикл - выполняется фиксированное количество итераций, ничего не делает кроме изменения значения(инкремента) проверочного выражения. Однажды начавшись, такой цикл, имея условие его завершения - завершится автоматически.
Конечный цикл - выполняется фиксированное количество итераций печати в консоль. Однажды начавшись, такой цикл, имея условие его завершения - завершится автоматически.
Конечный цикл - выполняется фиксированное количество итераций печати в консоль. Однажды начавшись, такой цикл, имея условие его завершения - завершится автоматически. Обратите внимание на объявление и инициализацию целочисленной переменной для проверочного выражения. В примере применена логическая операция И(&&) значения 0 и функции printf(). Функция printf() может возвращать некое значение и инициализировать им целочисленную переменную. Если строка внутри функции printf() содержит управляющие символы \n или \0 в конце строки, то будет возвращено целое число означающее количество символов в строке до этого управляющего символа. А если строка содержит управляющий символ \n где-то внутри строки - между символами и символ \0 в конце строки, то тогда возвращается значение количества всех символов до символа \0. Подробнее о работе со строками далее в учебнике.
Конечный цикл - выполняется фиксированное количество итераций печати в консоль. Однажды начавшись, такой цикл, имея условие его завершения - завершится автоматически. Обратите внимание на конструкцию цикла внутри круглых скобок - эти утверждения печати в консоль могут выполняться даже находясь здесь при каждой итерации. Перепишите, сохраните и выполните программу этого примера, чтобы понять как работает итерация цикла for().
Конечный цикл - выполняется фиксированное количество итераций печати в консоль. Однажды начавшись, такой цикл, имея условие его завершения - завершится автоматически. Обратите внимание на конструкцию цикла внутри круглых скобок - часть конструкции вынесена вне круглых скобок, и размещена непосредственно в тело цикла - а именно инкремент счётчика(A++) (внутри тела цикла - внутри фигурных скобок). Перепишите, сохраните и выполните программу этого примера, чтобы понять как работает итерация цикла for().
Конечный цикл - выполняется фиксированное количество итераций печати в консоль. Однажды начавшись, такой цикл, имея условие его завершения - завершится автоматически. Обратите внимание, как мы уже знаем, счётчик цикла может быть как инкрементирован так и декрементирован - это пример декремента счётчика(A--). Перепишите, сохраните и выполните программу этого примера, чтобы понять как работает итерация цикла for().
Конечный цикл - выполняется фиксированное количество итераций печати в консоль. Однажды начавшись, такой цикл, имея условие его завершения - завершится автоматически. Обратите внимание, область видимости(действия) переменной инициализированной внутри круглых скобок(инициализация цикла), не видима за пределами тела цикла. Кстати, эта блок схема - пример изображения компактного вида цикла. При попытке запустить код - при наведении курсора на переменную А в строке 9, получаем ошибку вида:
Visual Studio Code, уже на этапе попытки сохранить код, предупреждает о том, что переменная А не определена.
Вспоминайте, ранее мы уже касались темы области видимости переменной, итак, для того чтобы мы могли использовать значение переменной в утверждении вне тела цикла, то и переменную нужно объявить раньше перед циклом.
Синтаксис языка С позволяет довольно гибко написать конструкцию цикла, так что переменную можно не только объявить но и инициализировать до цикла.
Из круглых скобок - инициализации цикла, можно перенести в тело цикла и его итерацию.
Можно даже в круглых скобках - инициализация цикла, инициализировать сразу два цикла, но есть нюанс. Обратите внимание на развёрнутую блок-схему - в особенности на то какой цикл обрабатывается первым, и какой и как вторым. Запустите программу и проверьте что печатается в консоль.
Изменим проверку условия для цикла B на 1. Обратите внимание на развёрнутую блок-схему - в особенности на то какой цикл обрабатывается первым, и какой и как вторым. Запустите программу и проверьте что печатается в консоль.
Изменим проверку условия для цикла A на 30. Обратите внимание на развёрнутую блок-схему - в особенности на то какой цикл обрабатывается первым, и какой и как вторым. Запустите программу и проверьте что печатается в консоль.
Снова изменим проверку условия для цикла A на 1. Обратите внимание на развёрнутую блок-схему - в особенности на то какой цикл обрабатывается первым, и какой и как вторым. Запустите программу и проверьте что печатается в консоль.
Внутри инициализации цикла возможно применять проверку логических условий. Запустите программу и проверьте что печатается в консоль. Вспомните - логический оператор И(&&) возвращает 1 (true), если оба операнда true; иначе если хотя бы один операнд false, логический оператор И(&&) возвращает 0 (false)
Внутри инициализации цикла изменим проверку условия B на 1. Запустите программу и проверьте что печатается в консоль. Вспомните - логический оператор И(&&) возвращает 1 (true), если оба операнда true; иначе если хотя бы один операнд false, логический оператор И(&&) возвращает 0 (false)
Внутри инициализации цикла изменим проверку условия A на 1. Запустите программу и проверьте что печатается в консоль. Вспомните - логический оператор И(&&) возвращает 1 (true), если оба операнда true; иначе если хотя бы один операнд false, логический оператор И(&&) возвращает 0 (false)
Внутри инициализации цикла изменим проверку условия A на 0. Запустите программу и проверьте что печатается в консоль. Вспомните - логический оператор И(&&) возвращает 1 (true), если оба операнда true; иначе если хотя бы один операнд false, логический оператор И(&&) возвращает 0 (false)
Внутри инициализации цикла изменим проверку условия A на меньше или равно 0. Запустите программу и проверьте что печатается в консоль. Вспомните - логический оператор И(&&) возвращает 1 (true), если оба операнда true; иначе если хотя бы один операнд false, логический оператор И(&&) возвращает 0 (false)
Запустите программу и проверьте что печатается в консоль. Вспомните - логический оператор ИЛИ(||) возвращает 1 (true), если хотя бы один операнд true; иначе если оба операнда false, логический оператор ИЛИ(||) возвращает 0 (false)
Внутри инициализации цикла изменим значение проверки условия A на 1. Запустите программу и проверьте что печатается в консоль. Вспомните - логический оператор ИЛИ(||) возвращает 1 (true), если хотя бы один операнд true; иначе если оба операнда false, логический оператор ИЛИ(||) возвращает 0 (false)
Внутри инициализации цикла изменим значение проверки условия B на 1. Запустите программу и проверьте что печатается в консоль. Вспомните - логический оператор ИЛИ(||) возвращает 1 (true), если хотя бы один операнд true; иначе если оба операнда false, логический оператор ИЛИ(||) возвращает 0 (false)
Внутри инициализации цикла изменим значение проверки условия B на значение большее чем A. Приращение B будем увеличивать на 5. Запустите программу и проверьте что печатается в консоль. Вспомните - логический оператор ИЛИ(||) возвращает 1 (true), если хотя бы один операнд true; иначе если оба операнда false, логический оператор ИЛИ(||) возвращает 0 (false)
Внутри инициализации цикла значение проверки условия может быть типами double, float. Запустите программу и проверьте что печатается в консоль. Вспомните - тип double - это числа имеющие как целую так и дробную часть после точки: до девяти знаков по умолчанию. Тип float - это числа имеющие как целую так и дробную часть после точки: до шести знаков по умолчанию.
Снова вспомните об области видимости переменных. Два цикла могут быть инициализированы одной переменной - локальной. В данном примере, локальная переменная A объявлена до обоих циклов. Запустите программу и проверьте что печатается в консоль.
Объявим и инициализируем булеву переменную A, целочисленную переменную B - как локальные переменные. Переменная A используется в инициализации цикла в качестве условия и при первой итерации цикла возвращается значение проверки условия как true. Внутри тела цикла мы инкрементируем значение переменной B и проверяем по условию - больше или равна она 3. Если да - тогда изменяем значение булевой переменной A на false - и цикл завершается. Запустите программу и проверьте что печатается в консоль.
Мы уже знаем что условные операторы могут быть вложенными один внутрь другого. Вот синтаксис вложения циклов for один внутрь другого. Внимательно изучите подробную блок-схему.
Вот пример вложения циклов for один внутрь другого. Внимательно изучите подробную блок-схему. Первый запускаемый программой цикл называется наружный/внешний(outer), вложенный внутрь него цикл называется внутренний(inner). Запустите программу и проверьте что печатается в консоль.

Заключение

Зацикливание с помощью условия

Цикл while() является альтернативой циклу for, подробно описанному выше. Цикл while также требует наличия начального выражения, проверочного выражения и инкремента/декремента, но они указываются не так явно, как в цикле for. Вместо того, чтобы расположиться в круглых скобках, начальное выражение должно находиться перед началом блока цикла, проверочное выражение должно быть помещено в круглые скобки после ключевого слова while, после которых располагаются фигурные скобки, содержащие в себе инкремент/декремент и утверждения, которые должны быть выполнены на каждой итерации.

Тело цикла while выполняется до тех пор пока проверка условия в его круглых скобках возвращает true. Как только условие возвратит false - цикл завершается.

Объявление и Инициализация переменной (для проверочного утверждения внутри круглых скобок), записывается до цикла:

Пример конструкции цикла while на простом коде. Согласно блок-схеме напишите и выполните программу.
Пример конструкции цикла while со встроенной в его тело {} конструкцией if...else. Согласно блок-схеме напишите и выполните программу.
Пример конструкции бесконечного цикла while. Согласно блок-схеме напишите и выполните программу. При запуске программы вы фактически запускаете бесконечный цикл. Для принудительной ручной остановки бесконечного цикла или любой зависшей программы в Visual Studio Code нажмите комбинацию горячих клавиш Ctrl+C.
Цикл while выполняется бесконечно.
Принудительное завершение цикла while вручную путём нажатия комбинации горячих клавиш Ctrl+C
Внутри цикла while можно записывать условные выражения. Выполните программу выше, детально изучите её поведение по блок-схеме.
Внутри цикла while можно записывать цикл while. Так же как и в цикле for, в цикле while можно создавать вложенные циклы.
Пример создания вложенного цикла while внутри цикла while. По аналогии, называются такие циклы один по отношению к другому: внешний и внутренний. Запустите программу из примера выше.

Заключение

Цикл с условием вида do...while()

Цикл do...while() - модифицированный цикл while(), в том плане, что утверждения внутри тела цикла do...while()(внутри фигурных скобок { }) однозначно выполнится минимум один раз. И только после этого будет проверенно условие проверочного выражения в круглых ( ) скобках:

Пример простейшего цикла do...while(). Напишите и выполните программу из этого примера.

Обратите внимание, на ветку true возвращающуюся в начало цикла do...while() - утверждение ниже стрелки и есть утверждением внутри тела цикла do...while(). Когда проверочное утверждение возвращает false - цикл завершается.

Пример простейшего цикла do...while(). Напишите и выполните программу из этого примера.

Обратите внимание, в цикле do...while() утверждение внутри тела цикла { } ВЫПОЛНИТСЯ ОДИН РАЗ, даже не смотря на то что A в проверочном утверждении возвращает false. В этом главная особенность цикла do...while().

Пример конечного цикла do...while(). Напишите и выполните программу из этого примера.

Инкремент/Декремент в цикле do...while() записывается внутри тела { } цикла.

Пример конечного цикла do...while(). Напишите и выполните программу из этого примера. Измените значения объявленных переменных для понимания реализации цикла.

Инкремент/Декремент в цикле do...while() записывается внутри тела { } цикла.

Пример вложенного цикла do...while() внутрь цикла do...while(). Напишите и выполните программу из этого примера. Измените значения объявленных переменных для понимания реализации цикла.
  1. Напишите новую программу dowhilearray.c для работы с одномерным массивом. Объявим целочисленную переменную-счётчик, целочисленный массива с пятью элементами в нём. Сначала добавим цикл while():
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int arr[3] = {10, 20, 30, 40, 50};
    
       i = 0;
       while (i < 3)
       {
          pritf("While: arr[%d] = %d\n", i, arr[i]);
          i++;
       }
    
       return 0;
    }
  2. Теперь добавим цикл do...while():
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int arr[3] = {10, 20, 30, 40, 50};
    
       i = 0;
       while (i < 3)
       {
          pritf("While: arr[%d] = %d\n", i, arr[i]);
          i++;
       }
    
       do
       {
          printf("Do while: arr[%d] = %d\n", i, arr[i]);
          i++;
       } while (i < 3);
    
       return 0;
    }
  3. Сохраните, скомпилируйте , исправьте ошибки в программе(изучите вывод напечатанного в консоли) и выполните программу - добившись идентичного вывода печати в вашей консоли.
  4. Измените значение в проверочном выражении на 0 (while(i < 0)) в обоих циклах, а затем перекомпилируйте программу, чтобы увидеть, что будет выполнена только первая итерация цикла do...while.

Заключение

Досрочный выход из циклов

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

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

  1. Напишем новую программу nobreak.c без досрочного выхода из цикла на основе блок-схемы задачи:
    Задача визуализированная в виде блок-схемы следующая: Вывести в консоль распечатку значений двух целочисленных переменных i и j в пределах их значений от 1 до 3 включительно. Должно быть реализовано решение на основе вложенных циклов.(Напоминаю, левая блок-схема подробного вида, правая - компактная). Пока мы изучаем основы языка С - для нас важно детальное понимание принципов работы алгоритмов.
  2. Изучив блок-схему реализации задачи, напишем исходный код:
    Внесите изменения в программу что бы получить группы по 4-ре значения - от 0 до 3 включительно.
    Измените код. Создайте вывод в консоль распечатку данного вида.
    Измените код. Создайте вывод в консоль распечатку данного вида.
  3. Перепишите код - добавьте утверждение break в самом начале блока внутреннего цикла срабатывающем при истинности условия, затем сохраните, скомпилируйте и запустите программу ещё раз. Блок-схема визуализации задачи выглядит так:
    Измените код. Создайте вывод в консоль распечатку данного вида.
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int j;
    
       for (i = 1; i < 4; i++)
       {
          for (j = 1; j < 4; j++)
          {
             if ((i == 2) && (j == 1))
             {
                printf("Breaks inner loop when i = %d and j = %d\n", i, j);
                break;
             }
             printf("Running i = %d, j = %d\n", i, j);
          }
       }
    
       return 0;
    }
    
    Объясните - почему программа выдала в консоль данную распечатку.

Продолжение выполнения циклов

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

В качестве примера кода, возьмём код предыдущего примера:

#include <stdio.h>

int main(void)
{
   int i;
   int j;

   for (i = 1; i < 4; i++)
   {
      for (j = 1; j < 4; j++)
      {
         if ((i == 2) && (j == 1))
         {
            printf("Breaks inner loop when i=%d and j=%d\n", i, j);
            break;
         }
         printf("Running i = %d, j = %d\n", i, j);
      }
   }

   return 0;
}
  1. Перепишем часть кода, применим ключевое слово continue так:
    #include <stdio.h>
    
    int main(void)
    {
        int i;
        int j;
    
        for (i = 1; i < 4; i++)
        {
            for (j = 1; j < 4; j++)
            {
                if ((i == 1) && (j == 1))
                {
                    printf("\nContinues inner loop when i = %d and j = %d\n", i, j);
                    continue;
                }
    
                if ((i == 2) && (j == 1))
                {
                    printf("Breaks inner loop when i = %d and j = %d\n", i, j);
                    break;
                }
                printf("Running i = %d, j = %d\n", i, j);
            }
        }
    
        return 0;
    }
    
  2. Сохраним, скомпилируем и выполним программу:
    В этом примере утверждение continue просто пропускает первую итерацию внутреннего цикла, когда внешний пытается запустить его в первый раз.
Визуализация программы в подробной и компактной блок-схемах. Проследите выполнение всей программы от фигуры к фигуре.

Переход к меткам

Ключевое слово goto в соответствии со своим названием позволяет потоку программы переходить к меткам , расположенным в других частях программы, примерно как гиперссылка на веб-странице. Однако в реальности это может привести к ошибкам и считается плохим приёмом программирования.

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

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

  1. Напишем новую программу jump.c для ознакомления с работой оператора goto
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int j;
    
       for (i = 1; i < 4; i++)
       {
          for (j = 1; j < 4; j++)
          {
                if ((i == 2) && (j == 1))
                {
                   goto end;
                }
             printf("Running i = %d j = %d\n", i, j);
          }
       }
    
       return 0;
    }
  2. Теперь добавьте end метку после закрывающей скобки внешнего цикла
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int j;
    
       for (i = 1; i < 4; i++)
       {
          for (j = 1; j < 4; j++)
          {
                if ((i == 2) && (j == 1))
                {
                   goto end;
                }
             printf("Running i = %d j = %d\n", i, j);
          }
       }
       end: /* Обратите внимание - метка должна быть закончена двоеточием! */
    
       return 0;
    }
  3. Сохраните, скомпилируйте и выполните программу.
    Применение устаревшего в программировании метода - Переход к метке.
    Блок-схемы(подробная и компактная) устаревшего в программировании метода - Переход к метке.

Как уже было отмечено - переход к меткам с помощью оператора goto является устаревшей практикой и не рекомендуется к применению. Однако в старых программах вы можете его встретить. В своих программах никогда не применяйте переход к меткам.

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

  1. Перепишем наш пример - избавимся от перехода к метке. Применим Условие проверки состояния Флага. Объявим и инициализируем целочисленную переменную flag:
    #include <stdio.h>
    
    int main(void)
    {
        int i;
        int j;
        int flag;
    
        for (i = 1; i < 4; i++)
        {
            for (j = 1; j < 4; j++)
            {
                if (flag)
                {
                    if ((i == 2) && (j == 1))
                    {
                        flag = 0;
                    }
                    else /* Обратите внимание на применение else в блоке if */
                    {
                        printf("Running i = %d j = %d\n", i, j);
                    }
                }
            }
        }
        return 0;
    }
    
  2. Сохраните, скомпилируйте и выполните переписанную программу.
    Современный подход к управлению выполнением кода по Условию с помощью Флага.
    Блок-схемы(подробная и компактная) современного метода в программировании - Переход по Условию с помощью Флага.

Заключение

  1. Ключевое слово if выполняет простую условную проверку, чтобы оценить значение заданного выражения — оно может возвратить либо true либо false.
  2. Ключевое слово else разрешается использовать для предоставления альтернативного набора утверждений, которые должны быть выполнены в случае, если утверждение if возвращает значение false.
  3. Предоставление программе альтернативных направлений выполнения после Условия называется условным ветвлением.
  4. Условное ветвление, выполняемое с помощью нескольких утверждений if...else, может быть выполнено более эффективно с помощью утверждения switch.
  5. Обычно утверждения case внутри блока switch должны заканчиваться утверждением break. Если необходимо выполнить одно утверждение для нескольких значений case - то утверждение ставится в последнём case, в предыдущих case слово break не записывается.
  6. Опционально блок switch может содержать утверждение default, с помощью которого указываются утверждения, которые должны быть выполнены в случае отсутствия совпадений.
  7. После ключевого слова for следуют круглые скобки, в которых указывается начальное выражение, проверочное выражение и инкремент/декремент, позволяющие управлять циклом.
  8. После ключевого слова while следуют круглые скобки, в которых указывается проверочное выражение, позволяющее определить, должен ли цикл продолжаться.
  9. Перед блоком цикла while должно располагаться начальное выражение, а внутри него — инкремент/декремент.
  10. После ключевого слова do следует блок утверждений, после которого обязательно нужно добавить утверждение while, которое должно заканчиваться точкой с запятой ;.
  11. Перед циклом do...while должно находиться начальное выражение, а внутри него — инкремент/декремент.
  12. В отличие от циклов for и while утверждения цикла do...while всегда будут выполнены хотя бы один раз.
  13. Ключевое слово break может использоваться для того, чтобы завершить цикл, а ключевое слово continue — для того, чтобы пропустить одну итерацию цикла.
  14. Циклы могут быть вложенными друг в друга.
  15. Ключевое слово goto может быть использовано для того, чтобы выйти из всех циклов/блоков кода и перейти к указанной метке, однако этот подход устарел и использовать его не рекомендуется. Вместо него применяйте Условия и/или Циклы.
  16. Итак, на текущем этапе полученных знаний по программированию на С мы знаем, что любое ветвление кода в программе следует осуществлять исключительно с помощью Условий и Циклов.

Использование Функций

Изучим конструкцию функции. Научимся вызывать(выполнять) функцию по запросу утверждения в программе.

Объявление функций

До этого момента, в учебнике мы использовали обязательную главную функцию main() и стандартные функции, содержащиеся в библиотеке заголовочных файлов, такие как printf() из файла <stdio.h>. Однако в большинстве программ, написанных на языке C, содержится некоторое количество пользовательских функций, которые могут быть вызваны по требованию во время выполнения программы.

Синтаксис и общая конструкция функции.

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

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

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

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

Прототип функции иногда называют её заголовком.

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

Наши блок-схемы теперь будут следовать принципу модульности. Где отдельная функция - и есть модуль. Рассмотрим пример модульной блок-схемы, и напишем по ней программу:

Пример простейшей модульной блок-схемы главной функции и пользовательских функции в ней.
  1. Напишем новую программу firstfunc.c . Объявим три пользовательские функции:
    #include <stdio.h>
    
    void first();  /* Первая пользовательская функция */
    int square5(); /* Вторая пользовательская функция */
    int cube5();   /* Третья пользовательская функция */
    
    int main() /* Главная функция программы */
    {
       return 0;
    }
    
    Обратите внимание на подсказки от Visual Studio Code, говорящих о том, что - функции объявлены, но не определены(definition not found ) в программе.
  2. Объявим целочисленную переменную внутри главной функции
    #include <stdio.h>
    
    void first();
    int square5();
    int cube5();
    
    int main()
    {
       int num;
    
       return 0;
    }
    
  3. После главной функции определим пользовательские функции:
    #include <stdio.h>
    
    void first();
    int square5();
    int cube5();
    
    int main() 
    {
       int num;
    
       return 0;
    } /* Здесь главная функция завершается */
    
    void first()
    {
        printf("Hello from the first function\n");
    }
    
    int square5()
    {
        int square = 5 * 5;
        return square;
    }
    
    int cube5()
    {
        int cube = (5 * 5) * 5;
        return cube;
    }
    
  4. Теперь добавим вызовы пользовательских функций в блоке главной функции:
    #include <stdio.h>
    
    void first();
    int square5();
    int cube5();
    
    int main() 
    {
       int num;
       /* Вызываем пользовательские функции */
       first();
    
       num = square5();
       printf("5 x 5 = %d\n", num);
    
       printf("5 x 5 x 5 = %d\n", cube5());
    
       return 0;
    }
    
    void first()
    {
        printf("Hello from the first function\n");
    }
    
    int square5()
    {
        int square = 5 * 5;
        return square;
    }
    
    int cube5()
    {
        int cube = (5 * 5) * 5;
        return cube;
    }
    
  5. Сохраните, скомпилируйте и выполните программу:
    Простейший пример работы с функциями в коде. Обратите внимание - строку объявления функции нужно завершить точкой с запятой ;. А вот после определения функции - внизу под главной функцией - после фигурных скобок { } точка с запятой не нужна.
Определения пользовательских функций технически должны появляться перед функцией main(), но по соглашению правил хорошего тона среди программистов, там следует писать только прототипы, а функцию main() всегда размещать в начале кода.

Передача аргументов в функцию

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

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

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

Аргументы в прототипе функции называются формальными параметрами функции. Они могут иметь разные типы данных; несколько аргументов могут быть указаны для одной функции, тогда их следует разделять запятой. Например, прототип функции с аргументами, имеющими каждый из четырёх типов данных, может выглядеть так:

void action(char c, int I, float f, double d);

Компилятор проверяет, совпадают ли указанные в прототипе функции формальные параметры с параметрами в определении функции. Он сообщит об ошибке в случае несовпадения.

  1. Напишем новую программу args.c, которая демонстрирует синтаксис передачи аргументов в функцию. Объявим три пользовательские функции:
    #include <stdio.h>
    
    void display(char str[]);
    int square(int x);
    int cube(int y);
    
    int main()
    {
       return 0;
    }
    
  2. Добавьте в главную функцию, объявление целочисленной переменной и массив символьных переменных, который инициализируется текстовой строкой:
    #include <stdio.h>
    
    void display(char str[]);
    int square(int x);
    int cube(int y);
    
    int main()
    {
       int num;
       char msg[50] = "String to be passed to a function";
    
       return 0;
    }
    
  3. После главной функции определите три пользовательские функции:
    #include <stdio.h>
    
    void display(char str[]);
    int square(int x);
    int cube(int y);
    
    int main()
    {
       int num;
       char msg[50] = "String to be passed to a function";
    
       return 0;
    }
    /* Здесь пишем определения пользовательских функций */
    void display(char str[])
    {
        printf("%s\n", str);
    }
    
    int square(int x)
    {
        return x * x;
    }
    
    int cube(int y)
    {
        return (y * y) * y;
    }
    
  4. Теперь добавьте вызовы пользовательских функций в главной функции:
    #include <stdio.h>
    
    void display(char str[]);
    int square(int x);
    int cube(int y);
    
    int main()
    {
       int num;
       char msg[50] = "String to be passed to a function";
    
       /* Здесь вызываем пользовательские функции */
       display(msg);
    
       num = square(4);
       printf("4 x 4 = %d\n", num);
    
       printf("4 x 4 x 4 = %d\n", cube(4));
    
       return 0;
    }
    
    void display(char str[])
    {
        printf("%s\n", str);
    }
    
    int square(int x)
    {
        return x * x;
    }
    
    int cube(int y)
    {
        return (y * y) * y;
    }
    
  5. Сохраните файл программы, а затем скомпилируйте и выполните программу, чтобы увидеть выведенный на экран результат работы пользовательских функций, использующих переданные значения аргументов.
    Пример работы вызова пользовательских функций с передачей им параметров, которые они используют.
Функция может не возвращать значения, но попрежнему использовать слово return, рядом с которым не указано никаких значений. Это делается только для того, чтобы явно обозначить возврат управления вызывающей стороне(функции).
  1. Напишем новую программу maxint.c, которая будет определять большее число из двух целых чисел переданных пользователем в качестве параметров функции из консоли. Объявим пользовательскую функцию:
    #include <stdio.h>
    
    int myMax(int x, int y); /* Здесь объявили функцию пользователя */
    
    int main(void)
    {       
       return 0;
    }
  2. Объявим переменные для передачи их в качестве аргументов функции. Определим пользовательскую функцию после главной функции:
    #include <stdio.h>
    
    int myMax(int X, int Y);
    
    int main()
    {
       /* Здесь объявили переменные */
       printf("Enter two integers:\n");
       int A;
       int B;
       scanf("%d %d", &A, &B);
       int C = myMax(A, B);
        
       return 0;
    }
    /* Здесь определяем пользовательскую функцию */
    int myMax(int X, int Y)
    {
       if (X > Y)
       {
          return X;
       }
       else if (Y > X)
       {
          return Y;
       }
       return -1;
    }
  3. Вызовим пользовательскую функцию и возвращаемое ею значение присвоим переменной C, которую проверим на условие:
    #include <stdio.h>
    
    int myMax(int X, int Y);
    
    int main()
    {
       printf("Enter two integers:\n");
       int A;
       int B;
       scanf("%d %d", &A, &B);
    
       /* Здесь проверяем по условию возвращённое пользовательской функцией значение */
       int C = myMax(A, B);
       if (C != -1)
       {
          printf("%d is more.\n", C);
       }
       else
       {
          printf("%d is equal %d.\n", A, B);
       }
       return 0;
    }
    
    int myMax(int X, int Y)
    {
       if (X > Y)
       {
          return X;
       }
       else if (Y > X)
       {
          return Y;
       }
       return -1;
    }
  4. Сохраните, скомпилируйте и выполните программу:
    Введите различные значения, несколько раз перезапустив программу.

Виды функций

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

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

Стандартные заголовочные файлы ANSI C

  1. Напишем новую программу mathheader.c в которой используем стандартную библиотеку математических функций <math.h>. Математическая библиотека math.h
  2. Что же такое заголовочный файл или как его ещё называют файл заголовков? Вспомните:
    Прототип функции иногда называют её заголовком.
    Так что заголовочный файл - это набор заголовков функций содержащихся в нём. Т.е. эти заголовки просто напросто - объявления функций. Файл заголовков включенный в нашу программу, уже внутри себя имеет и определение объявленной в нём функции.
  3. Нам достаточно подключить заголовочный файл, использовать уже знакомый нам синтаксис записи функции, передать ей подходящие аргументы, и функция будет выполнена - вернёт значение.
  4. Изучайте стандартные заголовочные файлы стандарта ANSI C и большинство необходимых функций вам никогда не придётся писать с нуля.
  5. Язык С считается одним из самых быстрых по скорости вычислений в истории. Его математические функции доведены практически до совершенства. В нашей программе мы Возведём число e( e(число) ) в степень числа, Возведём одно число в степень другого числа, Вычислим квадратный корень из числа. Обратите внимание - функции принимают как целые числа так и натуральные(дробные).
  6. #include <stdio.h>
    #include <math.h>
    
    int main(void)
    {
       printf("%.3lf\n", exp(4.5));      /* Функция возведение экспоненты в степень */
       printf("%.3lf\n", pow(2.9, 1.3)); /* Функция возведение числа в степень числа */
       printf("%.3lf\n", sqrt(27));      /* Функция вычисления квадратного корня из числа */
    
       return 0;
    }
    
  7. Сохраните, скомпилируйте и выполните программу:
    Использование заголовочного файла <math.h> и три его функции exp(), pow(), sqrt().

Возвращение двух и более значений из функции

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

Первый - способ глобальных переменных.

Второй - способ указателей. С указателями мы ещё не знакомы, но рассмотрим его позже - в качестве введения в тему Указателей.

Итак, Глобальный способ возвращения нескольких значений из функции довольно прост:

  1. Сначала, в коде мы объявляем глобальные переменные.
  2. Объявляем прототип функции - атрибутами которой будут эти переменные.
  3. Пишем главную функцию внутри которой вызываем нашу объявленную функцию.
  4. После главной функции определяем нашу объявленную функцию.
  1. Напишем новую программу mathglobal.c, в которой передадим в пользовательскую функцию два параметра, и возвратим два результата.
    #include <stdio.h>
    #include <math.h>
    /* Глобальные переменные A, B объявляем до главной функции */
    int A;
    int B;
    
    /* Прототип функции объявляем до главной функции */
    void myFunction(int X, int Y);
    
    int main(void)
    {
        int X = 6;
        int Y = 7;
        myFunction(X, Y);
        printf("Squares of %d and %d are: %d and %d.", X, Y, A, B);
        
        return 0;
    }
    /* Определяем функцию */
    void myFunction(int X, int Y)
    {
        A = pow(X, 2);
        B = pow(Y, 2);
    }
  2. Так как переменные A и B Глобальные - область их видимости охватывает все блоки кода программы. Соответственно в строке кода printf("Squares of %d and %d are: %d and %d.", X, Y, A, B); изменённые значения переменных A и B (Глобальных), доступны в ней.
  3. Сохраним и скомпилируем программу, выполним её:
    Пример возвращения двух значений из пользовательской функции в Главную функцию - способом Глобальных переменных.
  4. Однако мы изучили правила хорошего тона работы с переменными, и помним - глобальные переменные в коде должны быть сведены к минимуму.

Рекурсивные вызовы - функция вызывает саму себя

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

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

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

  1. Напишем новую программу recursion.c. Но в начале составим блок-схему. Задача - программа должна принимать от пользователя целое число. Напечатать его в консоль. Уменьшить его на единицу(декремент). Проверить значение по условию на меньше 0. Если значение меньше 0 завершить программу. В качестве реализации цикла выполнения применить рекурсивную функцию.

    Проследим работу программы по шагам. Проиллюстрируем ключевые шаги:

    1. Выполняются все строки кода (допустим пользователь ввёл число 2), включая вызов пользовательской функции. После вызова пользовательской функции, Главная функция ожидает её выполнения:
    2. Выполняется первая итерация пользовательской функции. Условие num < 0 на первой итерации не выполняется так как (num == 2) < 0 == false . На этой ветке false происходит вызов пользовательской функции из неё же самой:
    3. Пользовательская функция вызвала саму себя - Вторая итерация. Условие num < 0 на второй итерации не выполняется так как (num == 1) < 0 == false . На этой ветке false снова происходит вызов пользовательской функции из неё же самой:
    4. Вторая итерация выполнена:
    5. Пользовательская функция вызвала саму себя - Третья итерация. Условие num < 0 на третей итерации не выполняется так как (num == 0) < 0 == false . На этой ветке false снова происходит вызов пользовательской функции из неё же самой:
    6. Третья итерация выполнена:
    7. Пользовательская функция вызвала саму себя - Четвёртая итерация. Условие num < 0 на четвёртой итерации выполняется так как (num == -1) < 0 == true . На этой ветке true происходит возврат в Главную функцию из пользовательской:
    8. Вызов Главной функции осуществлён:
    9. Главная функция ожидала завершения вызванной ею пользовательской функции, которая завершилась. Продолжается выполнение строк кода Главной функции. Печатается последняя строка printf("Lift Off!\n);, Главная функция завершается возвратом 0(как того требует объявленная главная функция) return 0;:
    10. Блок-схема изучена. Приступаем к написанию программы. Согласно изучения выполнения каждого шага и наличия внутри фигур утверждений С, напишем исходный код так:
  2. #include <stdio.h>
    /* Объявим пользовательскую функцию */
    void count_down_from(int num);
    
    int main()
    {
       int start;
       printf("Enter a positive Integer to count down from: \n");
       scanf("%d", &start);
    
       count_down_from(start);
    
       printf("Lift Off!\n");
    
       return 0;
    }
    /* Определим пользовательскую функцию */
    void count_down_from(int num)
    {
       printf("%d\n", num);
        
       --num;
       if (num < 0)
       {
       return;
       }
       else
       {
          count_down_from(num); /* Рекурсивный вызов пользовательской функции саму себя */
       }
    }
  3. Сохраните, скомпилируйте и выполните программу. Введите любое положительное число и нажмите Enter в консоли:
    Пример рекурсивного вызова пользовательской функции самой себя.
Использование рекурсивных функций может быть менее эффективно, чем использование чистых циклов. В частности из-за более высокого расхода памяти машины. Так как каждый промежуточный результат нужно сохранять в памяти для дальнейшего его использования.

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

Напишем новую программу factorial.c. Программа будет вычислять факториал введённого в консоль пользователем целого числа с помощью рекурсии:

  1. Блок-схема рекурсивного вызова пользовательской функции самой себя - Вычисления Факториала Целого Числа.
  2. Исходный код:
    #include <stdio.h>
    
    int myFactorial(int A);
    
    int main(void)
    {
       int A;
       printf("Enter an integer:\n");
       scanf("%d", &A);
        
       int result = myFactorial(A);
       printf("Factorial of %d is %d.", A, result);
        
       return 0;
    }
    
    int myFactorial(int A)
    {
       int r = 0;
       if(A > 0)
       {
          r = A * myFactorial(A-1); /* Рекурсивный вызов функции самой в себе */
          return r;
       }
       else if(A == 0)
       {
          return 1;
       }
    }
  3. Внимательно проследите утверждение за утверждением выполняемые в строках программы. Особенно пристально исследуйте что происходит внутри пользовательской функции.
  4. Мысленно "передайте" в пользовательскую функцию число 2 и "отработайте" каждую строку программы.
  5. Сохраните, скомпилируйте и выполните программу:
  1. Перепишем нашу программу вычисления факториала factorial.c - напишем компактную форму определения функции так:
    int myFactorial(int A)
    {
       return ((A == 1 || A == 0) ? 1 : A * myFactorial(A-1));
    }
    Мы применили полученные нами знания о логических булевых операциях и поместили их в качестве тернарного условия.
  2. Теперь исходный код выглядит так:
    #include <stdio.h>
    
    int myFactorial(int A);
    
    int main(void)
    {
       int A;
       printf("Enter an integer:\n");
       scanf("%d", &A);
        
       int result = myFactorial(A);
       printf("Factorial of %d is %d.", A, result);
        
       return 0;
    }
    
    int myFactorial(int A)
    {
       return ((A == 1 || A == 0) ? 1 : A * myFactorial(A-1));
    }
    
  3. Выполните эту версию программы.

Напишем новую программу fibonacci.c. Программа будет вычислять последовательность чисел Фибоначчи с помощью рекурсии. Напомним, в ряду Фибоначчи каждое число является суммой двух чисел, которые идут перед ним. Ряд начинается с 0, за которым следует 1. Обозначается ряд как T(n).

Ряд Фибоначчи начинается такими числами: 0, 1, 1, 2, 3, 5, 8, 13 ...

Математическая запись Ряда Фибоначчи записывается так:

T(n) = T(n-1) + T(n-2), для n >= 2
T(n) = n, для n < 2 для всех положительных целых чисел(базовый случай)

Таким образом для T(0) всегда получаем 0, для T(1) всегда получаем 1, а T(2) = T(2-1) + T(2-2), т.е. ряд строится так:

T(0) = 0
T(1) = 1
T(2) = T(0) + T(1) = 1
T(3) = T(2) + T(1) = 1 + 1 = 2
T(4) = T(3) + T(2) = 2 + 1 = 3
T(5) = T(4) + T(3) = 3 + 2 = 5
T(6) = T(5) + T(4) = 5 + 3 = 8
T(7) = T(6) + T(5) = 8 + 5 = 13
... и так далее.

Очевидно, что математическая природа ряда Фибоначчи простейшая рекурсия, где каждый член ряда зависит от двух предыдущих членов. Чтобы вычислить 10-тый член ряда Фибоначчи (T(10)), например, математическая запись в развёрнутом виде будет выглядеть так:

T(10) = T(9) + T(8), развернём T(9) и T(8) так:
T(10) = T(T(8)+T(7)) + T(T(7)+T(6)), развернём T(8), T(7) и T(6) так:
T(10) = T(T(T(7)+T(6))+T(T(6)+T(5))) + T(T(T(6)+T(5))+T(T(5)+T(4))), развернём T(7), T(6), T(5) и T(4) так:
T(10) = T(T(T(T(6)+T(5))+T(T(5)+T(4)))+T(T(T(5)+T(4))+T(T(4)+T(3)))) + T(T(T(T(5)+T(4))+T(T(4)+T(3)))+T(T(T(4)+T(3))+T(T(3)+T(2)))), развернём T(6), T(5), T(4), T(3) и T(2) так:
T(10) = T(T(T(T(T(5)+T(4))+T(T(4)+T(3)))+T(T(T(4)+T(3))+T(T(3)+T(2))))+T(T(T(T(4)+T(3))+T(T(3)+T(2)))+T(T(T(3)+T(2))+T(T(2)+T(1))))) + T(T(T(T(T(4)+T(3))+T(T(3)+T(2)))+T(T(T(3)+T(2))+T(T(2)+T(1))))+T(T(T(T(3)+T(2))+T(T(2)+T(1)))+T(T(T(2)+T(1))+T(T(1)+T(0))))), разворачиваем далее до базовых случаев

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

Вы можете обобщить эту рекурсивную функцию для вычисления последовательности Фибоначчи для любого члена, если вы предоставите базовые случаи для T(0) и T(1).

Итак, T(10) = 34

Составим блок-схему для алгоритма поиска чисел Фибоначчи до конкретного члена ряда:

Блок-схема работы алгоритма вывода членов ряда Фибоначчи.

Теперь напишем исходный код согласно блок-схеме алгоритма так:

#include <stdio.h>

/* Объявим прототип функции алгоритма Фибоначчи */
int fib(int n);

void main()
{
    int n;
    int i;

    printf("Enter number:\n");
    scanf("%d", &n);
    printf("The elements are:\n");

    for (i = 0; i < n; i++)
    {
        printf("%d ", fib(i));
    }
}
/* Определим функцию алгоритма Фибоначчи */
int fib(int n)
{
    if (n == 0)
    {
        return 0;
    }
    else if (n == 1)
    {
        return 1;
    }
    else
    {
        return (fib(n - 1) + fib(n - 2));
    }
}

В вашем редакторе код должен выглядеть так:

Результат работы программы реализующий рекурсивный алгоритм записи ряда чисел Фибоначчи.

Напишем программу ackermann.c. Рекурсивная функция нёмецкого математика Вильгельма Аккермана, разработанная им в 1928 году, определяет рост плотности операций сложной рекурсии. Эта функция считается очень сложным компьютерным алгоритмом, так как её сложность вычисления растёт экспоненциально (см. раздел Алгоритм).

Функция принимает два аргумента (m и n), оба являются неотрицательными целыми числами. Даже для малых значений m и n функция может выдавать чрезвычайно большие значения. Обозначим A(m, n) как функцию Аккермана. Она может быть задана следующим образом:

  1. A(m, n) = n + 1, если m == 0;
  2. A(m, n) = A(m - 1, 1), если m > 0 и n == 0;
  3. A(m, n) = A(m - 1, A(m, n - 1)), если m > 0 и n > 0.

Давайте решим A(1, 1). Здесь m == 1 и n == 1:

A(1, 1) = A(0, A(1, 0)) ……… (1) при m > 0 и n > 0

Чтобы найти A(1, 1), сначала нам нужно найти A(1, 0):

A(1, 0) = A(0, 1) ……… (2) при m > 0 и n == 0
A(0, 1) = 1 + 1 = 2 при m == 0

Теперь подставьте значение A(0, 1) в уравнение (2)

Следовательно, A(1, 0) = 2

Теперь подставьте значение A(1, 0) в уравнение (1)

Таким образом, A(1, 1) = A(0, 2) ……… (3)

A(0, 2) = 2 + 1 = 3 при m == 0

Теперь подставьте значение A(0, 2) в уравнение (3). Итак,

A(1, 1) = 3

Исходный код функции Аккермана:

#include <stdio.h>

int ack(int m, int n);

void main()
{
   int A;
   int m;
   int n;

   printf("Enter the value of m and n:\n");
   scanf("%d%d", &m, &n);
   A = ack(m, n);
   printf("The value of Ackermann Function is: %d", A);
}

int ack(int m, int n)
{
   if(m == 0)
   {
      return n+1;
   }
   else if((m > 0) && (n == 0))
   {
      return ack(m-1, 1);
   }
   else if((m > 0) && (n > 0))
   {
      return ack(m - 1, ack(m, n - 1));
   }
}
Функция Аккермана на языке С.

Сортировка слиянием

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

Функция сортировки массива Слиянием.

Опишем словами Алгоритм Сортировки Слиянием так:

  1. Объявление переменных:
    • Array[ ] - массив,
    • First - индекс первого элемента массива A[ ],
    • Last - индекс последнего элемента массива A[ ],
    • Mid - индекс среднего элемента массива A[ ].
  2. Если Last больше First, тогда:
    1. Вычислить середину Mid = (First + Last) / 2, которая делит массив на две половины.
    2. Вызвать функцию mergesort(Array, First, Mid) для левой половины.
    3. Вызвать функцию mergesort(Array, Mid + 1, Last) для правой половины.
  3. Вызвать функцию merge(Array, First, Mid, Last), чтобы рекурсивно объединить две половины в отсортированном порядке, чтобы остался только один отсортированный массив.
  4. Конец

Составим блок-схему для алгоритма Сортировки Слиянием так:

Блок-схема алгоритма сортировки массива слиянием.

Напишем программу mergesort.c на основе изученного алгоритма сортировки массива слиянием так:

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

void merge(int Array[], int First, int Mid, int Last);
void mergesort(int Array[], int First, int Last);
void print(int Array[], int size);

int main()
{
    int Array[] = {2, 1, 5, 6, 3, 4, 8, 7};
    int size = sizeof(Array) / sizeof(Array[0]);

    printf("Unsorted array: ");
    print(Array, size);

    mergesort(Array, 0, size - 1);
    printf("Sorted array: ");
    print(Array, size);

    return 0;
}

void print(int Array[], int size)
{
    int i;
    for (i = 0; i < size; i++)
    {
        printf("%d ", Array[i]);
    }
    printf("\n");
}

void mergesort(int Array[], int First, int Last)
{
    if (First < Last)
    {
        int Mid = First + (Last - First) / 2;
        mergesort(Array, First, Mid);     // Для Первой половины
        mergesort(Array, Mid + 1, First); // Для Второй половины
        merge(Array, First, Mid, Last);
    }
}

void merge(int Array[], int First, int Mid, int Last)
{
    // Объявим переменные-счётчики циклов и индексов
    int i;
    int j;
    int k;
    // Вычисляем длину Левого и Правого подмассивов
    int a1 = Mid - First + 1;
    int a2 = Last - Mid;
    // Объявим временные массивы
    int First_array[a1];
    int Last_array[a2];
    // Копируем данные во временные массивы с помощью циклов for
    for (i = 0; i < a1; i++)
    {
        First_array[i] = Array[First + i];
    }
    for (j = 0; j < a2; j++)
    {
        Last_array[j] = Array[Mid + 1 + j];
    }
    // Проведём слияние временного массива в массив Array[First..Last]
    i = 0;     // Начальный индекс для: i первого подмассива
    j = 0;     // Начальный индекс для: j второго подмассива
    k = First; // Начальный индекс для: k основного массива
    while (i < a1 && j < a2)
    {
        if (First_array[i] <= Last_array[j])
        {
            Array[k] = First_array[i];
            i++;
        }
        else
        {
            Array[k] = Last_array[j];
            j++;
        }
        k++;
    }
    // Копируем оставшиеся данные из массива First_array[] в основной с помощью цикла while
    while (i < a1)
    {
        Array[k] = First_array[i];
        i++;
        k++;
    }
    // Копируем оставшиеся данные из массива Last_array[] в основной с помощью цикла while
    while (j < a2)
    {
        Array[k] = Last_array[j];
        j++;
        k++;
    }
}

Изучите блок-схему алгоритма, прослеживая и проговаривая выполняемые шаги алгоритма в уме.

Напиши, сохраните и выполните программу:

Результат выполнения программы.

Алгоритм Быстрой Сортировки

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

Рассмотрим пример, показанный на рисунке ниже:

Не отсортированный массив.

Рассмотрим алгоритм быстрой сортировки. Первый проход по массиву:

  1. Посмотрим на предыдущее изображение - опорный элемент = 30, i и j — это два указателя. i перемещается вправо ( i++ ) и останавливается, когда находит элемент, больший опорного элемента. j перемещается влево ( j-- ) до тех пор, пока не найдёт элемент, меньший опорного элемента.
  2. Если i меньше j, поменять местами оба элемента, на которые указывают i и j.
  3. Если i больше j, поменять местами опорный элемент и элемент, на который указывает j.
  4. После выполнения шагов выше, массив делится на две части, а опорный элемент находит своё подходящее место. То есть все элементы слева от опорного элемента меньше его, а все элементы справа - больше, как показано на рисунке:
Массив после первого прохода алгоритма быстрой сортировки.

После этого те же шаги будут применяться к подмассиву слева от опорного элемента и к подмассиву справа от опорного элемента. Предыдущие шаги можно записать следующим псевдокодом:

while(i < j)
{
   while(Array[i] <= Array[pivot] && i < Last) // Первое условие
   // Last это последний элемент в массиве
   {
      i++;
      while(Array[j] > Array[pivot]) // Второе условие
      {
         j--;
         if(i < j)
         // Перемена местами i-того и j-того элементов в массиве
         {
            temp = Array[i];
            Array[i] = Arrat[j];
            Array[j] = temp;
         }
         // Иначе, Перемена местами j-того элемента и опорного элемента в массиве
         else
         {
            temp = Array[pivot];
            Array[pivot] = Array[j];
            Array[j] = temp;
         }
      }
   }
}

Выполним каждый шаг первого прохода алгоритма быстрой сортировки в виде таблицы действий:

Не отсортированный массив.
Начальные значения переменной, выполненные действия и конечные значения на основе тестовых условий
Начальное значение Условие Действие Результирующее значение
i = 0 1. 30 <= 30 && 0 < 4 i++ i = 1
j = 4 2. 20 > 3 j = 4
Array[i] = 30 Поскольку условие 1 истинно, а условие 2 ложно pivot = 30
Array[j] = 20
Array[pivot] = 30

После предыдущего шага массив выглядит, как на рисунке ниже:

Массив после первого шага.
Начальные значения переменной, выполненные действия и конечные значения на основе тестовых условий
Начальное значение Условие Действие Результирующее значение
i = 1 1. 50 <= 30 && 1 < 4 Обмен i-го и j-го элементов массива Array i = 1
j = 4 j = 4
Array[i] = 50 2. 20 > 30 Так как (i < j) pivot = 30
Array[j] = 20 Поскольку оба условия ложные
Array[pivot] = 30
Массив после второго шага.
Начальные значения переменной, выполненные действия и конечные значения на основе тестовых условий
Начальное значение Условие Действие Результирующее значение
i = 1 1. 20 <= 30 && 1 < 4 i++ i = 2
j = 4 2. 50 > 30 j-- j = 3
Array[i] = 20 pivot = 30
Array[j] = 50 Поскольку оба условия истинны
Array[pivot] = 30
Массив после третего шага.
Начальные значения переменной, выполненные действия и конечные значения на основе тестовых условий
Начальное значение Условие Действие Результирующее значение
i = 2 1. 10 <= 30 && 2 < 4 i++ i = 3
j = 3 2. 40 > 30 j-- j = 2
Array[i] = 10 Поскольку оба условия истинны pivot = 30
Array[j] = 40
Array[pivot] = 30
Массив после четвёртого шага.
Начальные значения переменной, выполненные действия и конечные значения на основе тестовых условий
Начальное значение Условие Действие Результирующее значение
i = 3 1. 40 <= 30 && 3 < 4 Обменяйте элемент Array[pivot] с j-м элементом Array i = 3
j = 2 j = 2
Array[i] = 40 2. 10 > 30 Так как (i > j) pivot = 10
Array[j] = 10
Array[pivot] = 30 Поскольку оба условия ложные
Массив после пятого шага.
Логическое разделение массива на две части, после пятого шага.

Опишем словами алгоритм быстрой сортировки так:

  1. Алгоритм Быстрой Сортировки
  2. Объявить массив Array[] и переменную Last, которая является последним элементом массива Array.
  3. Определить действия функции Quicksort(Array, 0, Last)
    1. Переместить опорный элемент в такое положение, чтобы слева от него были числа меньше его, а справа — больше.
    2. Отсортировать левую часть, то есть выполнить функцию Quicksort(Array, 0, j - 1)
    3. Отсортировать правую часть, то есть выполнить функцию Quicksort(Array, j + 1, Last)
  4. Конец

Напишем программу quicksort.c:

#include <stdio.h>

void Quicksort(int Array[50], int First, int Last);

void main()
{
    int Array[50];
    int size;
    int i;
    printf("Enter the Size of an Array up to 50 elements: ");
    scanf("%d", &size);
    printf("Enter elements of an Array: ");
    for (i = 0; i < size; i++)
    {
        scanf("%d", &Array[i]);
    }
    printf("The Unsorted Array is: \n");
    for (i = 0; i < size; i++)
    {
        printf("%d ", Array[i]);
    }
    Quicksort(Array, 0, size - 1);
    printf("\nThe Sorted Array is: \n");
    for (i = 0; i < size; i++)
    {
        printf("%d ", Array[i]);
    }
}

void Quicksort(int Array[50], int First, int Last)
{
    int i;
    int j;
    int pivot;
    int temp;
    if (First < Last)
    {
        pivot = First;
        i = First;
        j = Last;
        while (i < j)
        {
            while (Array[i] <= Array[pivot] && i < Last)
            {
                i++;
            }
            while (Array[j] > Array[pivot])
            {
                j--;
            }
            if (i < j)
            // Обмен местами i-го и j-го элементов
            {
                temp = Array[i];
                Array[i] = Array[j];
                Array[j] = temp;
            }
        }
        // Обмен местами j-го элемента и опорного элемента
        temp = Array[pivot];
        Array[pivot] = Array[j];
        Array[j] = temp;
        Quicksort(Array, 0, j - 1);    // рекурсивно отсортировать левый меньший подмассив
        Quicksort(Array, j + 1, Last); // рекурсивно отсортировать правый меньший подмассив
    }
}

Напишите, сохраните и выполните программу.

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

Заключение

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

Важные моменты

Важные вопросы

Размещение функций в заголовках

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

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

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

Функции, расположенные в пользовательском заголовочном файле, могут быть доступны программе, если записать его в начало файла, содержащего функцию main() и под директиву препроцессора #include. Имя пользовательского заголовочного файла должно размещаться в двойных кавычках " ". Символы < и >, разрешены для обрамления только стандартных заголовочных файлов. Вот так:

#include <stdio.h>                  
#include "utils.h" /* Это пользовательский файл заголовков */

int main(void)
{
   return 0;
}
  1. Напишем новую программу squre.c, и напишем пользовательский файл заголовков utils.h
  2. Пользовательский файл заголовков utils.h будет содержать в себе одну функцию - вычисление возведения значения целочисленной переменной в квадрат(во вторую степень):
    int square(int num)
    {
       return(num * num);
    }
    Создан простейший пользовательский файл заголовков. В этом файле определена простая функция - возведение целого числа в квадрат.
  3. Программа squre.c, содержащая главную функцию, будет получать от пользователя значение целого числа из консоли, возводить его в квадрат, и запрашивать пользователя о повторении этого действия:
    #include <stdio.h>
    #include "utils.h"
    
    void getnum();
    
    int main()
    {
       getnum();
    
       return 0;
    }
    
    void getnum()
    {
       int num;
       char again;
    
       printf("Enter an Integer to be Squared: ");
       scanf("%d", &num);
       printf("%d Squared is %d\n", num, square(num));
    
       printf("Square another Integer? Y or N: ");
       scanf("%1s", &again);
       if ((again == 'Y') || (again == 'y'))
       {
          getnum();
       }
       else
       {
          return;
       }
    }
  4. Сохраните, скомпилируйте и выполните программу:

Два важных замечания:

Файл программы и пользовательский заголовочный файл должны находиться в одной директории(папке), но компилируются они с помощью обычный команды — компилятор считывает заголовочный файл автоматически благодаря директиве препроцессора #include.
Обратите внимание на то, что спецификатор формата %1s в этом примере используется для того, чтобы считать один символ, введённый пользователем.

Вложение определения функции внутри другой функции

В языке С стандарта ANSI C запрещено вложение определения одной функции внутри другой функции!

Однако некоторые компиляторы(например: GCC, Dev-C++ - имеют такое расширение) поддерживают вложение определений функций. Это расширение не переносимое на другие компиляторы С, и код может не скомпилироваться, или скомпилироваться не ожидаемым образом - т.е. спровоцировать ошибку.

Вот блок-схема демонстрирующая визуально концепцию последовательного вызова функции внутри другой функции согласно стандарта ANSI C:

Визуализация концепции вызова функции другой функцией - поддерживается стандартом ANSI C.
#include <stdio.h>
 
void myFunc_1(); /* Объявление прототипа(заголовка) Первой Глобальной функции */
void myFunc_2(); /* Объявление прототипа(заголовка) Второй Глобальной функции */

int main(void)
{
   myFunc_1();   /* Вызов Первой Глобальной функции внутри Главной функции */
    
   return 0;
}

void myFunc_1()  /* Определение Первой Глобальной функции после Главной функции */
{
   printf("Inside myFunc_1\n");
   
   myFunc_2();   /* Вызов Второй Глобальной функции внутри Первой Глобальной функции */
}
void myFunc_2()  /* Определение Второй Глобальной функции после Главной функции */
{
      printf("Inside myFunc_2");
}

Сохранение, компиляция и запуск программы на основе стандарта ANSI C не вызывает никаких проблем:

Визуальное представление вложения определения функции внутри другой Глобальной функции. запрещено стандартом ANSI C.

Код будет выглядеть нёмного иначе, вот так:

#include <stdio.h>

void myFunc_1(); /* Объявление прототипа(заголовка) Первой Глобальной функции */

int main(void)
{
    myFunc_1(); /* Вызов Первой Глобальной функции внутри Главной функции */

    return 0;
}

void myFunc_1() /* Определение Первой Глобальной функции после Главной функции */
{
    printf("Inside myFunc_1\n");
    void myFunc_2() /* Определение Второй функции внутри Первой Глобальной функции */
    {
        printf("Inside myFunc_2");
    }

    myFunc_2(); /* Вызов Второй функции внутри Первой Глобальной функции */
}

Этот код сохраняется, компилируется и выполняется без видимых проблем. Так как компилятор GCC имеет своё(не стандартное) расширение поддержки вложенных определений функций:

Но обратите внимание на две подсказки в Visual Studio Code:

В них Visual Studio Code очевидно "ориентируется" на стандарт ANSI C языка, и такая конструкция для него является ошибочной.

Итак, важно соблюдать правила написания С программ согласно стандарт ANSI C по нескольким причинам:

Ограничение доступности - Область видимости

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

Этот синтаксис рекомендуется использовать в больших программах, которые располагаются в нескольких файлах , чтобы оградить их от случайного использования их функций. Например, функции square() и multiply() не могут быть вызваны непосредственно из функции main() в примере ниже:

  1. Напишем новую программу menu.c, которая предоставляет пользователю меню, в котором можно выбрать математические действия. Для начала объявим Глобальную пользовательскую функцию, вызовем её в теле главной функции, и после главной функции определим её:
    #include <stdio.h>
    
    void menu();
    
    int main()
    {
       menu();
    
       return 0;
    }
    
    void menu()
    {
       int option;
       printf("\n\tWhat would you like to do?");
       printf("\n\t1. Square a number");
       printf("\n\t2. Multiply two numbers");
       printf("\n\t3. Exit\n");
       scanf("%d", &option);
       action(option); /* Здесь мы вызываем функцию не объявленную в этом файле исходного кода */
    }
  2. Теперь напишем второй файл action.c для программы. Начнём с инструкции препроцессора, позволяющей подключить функции стандартной библиотеки ввода/вывода, и определим две простые статические функции:
    #include <stdio.h>
    
    static square(int a)
    {
        return (a * a);
    }
    
    static multiplay(int a, int b)
    {
        return (a * b);
    }
  3. В этом же файле Определим функцию, в которую будет передаваться вариант(option) меню из файла menu.c из функции main(), и на основе переданного значения будет выполняться соответствующее действие — вызовом одной из статических функций, созданных в этом файле action.c:
    #include <stdio.h>
    
    static square(int a)
    {
       return (a * a);
    }
    
    static multiplay(int a, int b)
    {
       return (a * b);
    }
    
    void action(int option) /* Определение функции 
       принимающей в качестве аргумента значение из файла menu.c */
    {
       int n1;
       int n2;
       if(option == 1)
       {
          printf("Enter an Integer to be Squared: ");
          scanf("%d", &n1);
          printf("%d x %d = %d\n", n1, n1, square(n1));
          menu();
       }
       else if(option == 2)
       {
          printf("Enter two Integers to be Multiplyed");
          printf(" separated by a SPACE: ");
          scanf("%d %d", &n1, &n2);
          printf("%d x %d = %d", n1, n2, multiply(n1, n2));
          menu();
       }
       else
       {
          return;
       }
    }
  4. Сохраните оба файла, а затем скомпилируйте и выполните программу, чтобы увидеть распечатанный в консоли результат работы статических функций. Вот команда компилятору указывающая объединить два файла: menu.c и action.c в один объектный файл:
    gcc menu.c action.c -o menu.exe
    Ошибка компиляции:
  5. Эти две ошибки легко понять:
    • menu.c: In function 'menu':
      menu.c:20:5: error: implicit declaration of function 'action' [-Wimplicit-function-declaration]
         20 |     action(option);
            |     ^~~~~~
      Читаем ошибку так: В файле menu.c в функции 'menu' строка 20 в позиции 5 ошибка - отсутствует явно объявленная функция action(option)
    • action.c: In function 'action':
      action.c:21:9: error: implicit declaration of function 'menu' [-Wimplicit-function-declaration]
         21 |         menu();
            |         ^~~~
      Читаем ошибку так: В файле action.c в функции 'action' строка 21 в позиции 9 ошибка - отсутствует явно объявленная функция menu()
    Итак, мы создали два файла .c, внутри которых каждый вызывает функцию из другого. Но область видимости функций внутри файлов ограничена их пределами, а мы не обеспечили метода взаимодействия этих функций между файлами. Исправим ошибку:
  6. Напишем новый файл заголовков функций menu.h . При компиляции файла заголовков menu.h и двух файлов menu.c и action.c компилятор на этапе Компоновки(линковки/связывания) прочитает содержимое фалов и создаст связи позволяющие функциям взаимодействовать между собой.
  7. В файле заголовков нужно просто объявить интересующие нас функции так:
    void menu(void);
    void action(int option);
                         
  8. Сохраняем файл заголовков.
  9. Теперь необходимо сослаться(подключить) в наших файлах menu.c и action.c на пользовательский файл заголовков menu.h . Мы уже знаем, что записать подключение файла заголовков нужно в качестве инструкции препроцессору, как мы это делаем со стандартными заголовочными файлами, синтаксис так же мы уже знаем - вместо угловых скобок < >, обрамляем имя заголовочного файла двойными кавычками " ":
    • В файле menu.c подключим файл заголовков menu.h
      #include "menu.h"
    • В файле action.c подключим файл заголовков menu.h
      #include "menu.h"
  10. Теперь у нас есть три файла:
    • Файл заголовков(прототипов) функций menu.h
      void menu(void);
      void action(int option);
    • Главная функция в файле menu.c
      #include <stdio.h>
      #include "menu.h"
      
      void menu();
      
      int main()
      {
         menu();
      
         return 0;
      }
      
      void menu()
      {
          int option;
          printf("\n\tWhat would you like to do?");
          printf("\n\t1. Square an Integer");
          printf("\n\t2. Multiply two Integers");
          printf("\n\t3. EXIT\n");
          scanf("%d", &option);
          action(option); 
      }
      
    • Файл action.c со статическими функциями и функцией action()
      #include <stdio.h>
      #include "menu.h"
      
      static int square(int a)
      {
          return (a * a);
      }
      static int multiply(int a, int b)
      {
          return (a * b);
      }
      
      void action(int option)
      {
          int n1;
          int n2;
          if(option == 1)
          {
              printf("Enter an Integer to be Squared: ");
              scanf("%d", &n1);
              printf("%d x %d = %d\n", n1, n1, square(n1));
              menu();
          }
          else if(option == 2)
          {
              printf("Enter two Integers to be Multiplyed");
              printf(" separated by a SPACE: ");
              scanf("%d %d", &n1, &n2);
              printf("%d x %d = %d", n1, n2, multiply(n1, n2));
              menu();
          }
          else
          {
              return;
          }
      }
  11. В Visual Studio Code у вас должны быть три файла, сохранённые и готовые к компиляции:
    Файлы готовы к компиляции.
    Все файлы, которые вы компилируете в один объектный файл, должны находится в одной папке, тогда компилятор автоматические "знает" где их "искать"!
  12. Проверьте, что все файлы сохранены. Напишем в консоль следующую команду компилятору, в которой теперь укажем три файла, которые необходимо скомпилировать в один и выполним программу:
    gcc menu.h menu.c action.c -o menu.exe
  13. Выполним несколько действий в программе:

Заключение

  1. Пользовательские функции объявляются путём указания типа данных, которые будут возвращены функцией, затем её имени, а затем парой круглых ( ) скобок для аргументов. Закончить конструкцию следует точкой с запятой ; .
  2. Объявления функций также называются прототипами функций, они должны располагаться перед функцией main() — поэтому компилятор будет знать об их существовании при чтении функции main(), и выделит для них область памяти.
  3. Определения функций, непосредственно содержащие утверждения, которые необходимо выполнить, когда вызывается функция, должны располагаться после блока функции main().
  4. Объявления функций опционально могут иметь внутри скобок разделённый запятыми список аргументов, передаваемых вызываемой стороной, для каждого из которых необходимо указать тип данных и имя.
  5. Аргументы, указанные в определении функции, должны соответствовать аргументам в описании, поскольку последние являются их формальными параметрами.
  6. В программировании на языке C аргументы передаются по значению — функция работает только с копией оригинального значения.
  7. Функция может рекурсивно вызывать саму себя, в этом случае она должна содержать утверждение, изменяющее проверочное выражение, чтобы в определённый момент завершиться. Самих же рекурсивных функций стоит избегать в исходном коде - из-за непрозрачности(сложности понимания) кода.
  8. Пользовательские заголовочные файлы должны иметь расширение .h .
  9. Если с помощью директивы препроцессора #include добавляются пользовательские заголовочные файлы, имя файла указывается в двойных кавычках " " - для визуального их отличия от стандартных заголовочных файлов.
  10. Ключевое слово static может быть использовано в объявлениях и описаниях функций, чтобы ограничить к ним доступ рамками файла, в котором они находятся.
  11. Крупные программы должны объявлять функции с помощью ключевого слова static, если только нет какой-то особенной причины, по которой функция должна быть видима за пределами файла.
  12. Указывать прототипы необязательно для функций, располагающихся за пределами файла, содержащего функцию main().
  13. Для предоставления возможности файлам взаимодействовать - позволить им вызывать функции друг друга, важно написать пользовательский файл заголовков функций, скомпилировать их вместе в один объектный файл. При чём, все эти файлы должны быть сохранены в одной папке.

10 правил программирования (от NASA) на основе языка C

Зачем нужны эти правила

В серьёзных проектах по разработке ПО используют рекомендации по стилю кода. Они помогают понять, как правильно структурировать код и какие возможности языка можно и нельзя использовать. Проблема в том, что обычно в таких рекомендациях слишком много правил (100+), и программисты не соблюдают их.

Чтобы сделать код надёжным и легче проверяемым, лучше иметь небольшой набор простых, но строгих правил, которые можно проверить автоматически. Эти правила особенно важны для разработки критически важного ПО, где ошибки могут стоить человеческих жизней.

NASA использует 10 простых правил для языка C при написании такого кода. Они помогают писать надёжный, понятный и проверяемый код.

Почему именно язык C

C — популярный язык для разработки системного и встраиваемого ПО, в том числе в NASA, благодаря:

Эти 10 правил в первую очередь ориентированы на С, но принципы полезны и для других языков.

10 правил написания надёжного и безопасного кода на C

  1. Используй только простые конструкции управления.
    • Не используй goto, setjmp/longjmp, прямую и косвенную рекурсии.
    • Почему: простой поток выполнения упрощает анализ и делает код понятнее. Отказ от рекурсии позволяет анализаторам кода доказать, что стек не переполнится, и выполнение будет завершено без ошибок.
  2. Задавай верхние границы всем циклам.
    • Проверяющие инструменты должны легко доказывать, что цикл не будет выполняться бесконечно.
    • Почему: предотвращает зависание кода и помогает анализаторам проверить выполнение программы. Если нужно пройти по списку неизвестной длины, добавляй явную границу и проверку с вызовом assert при её превышении.
  3. Не используй динамическое выделение памяти после её инициализации.
    • После старта программы не используй malloc и подобные функции.
    • Почему: динамическая память может вести себя непредсказуемо и приводить к ошибкам утечки памяти, ошибкам использования после free, выходу за границы памяти. Лучше использовать предвыделенную память и стек (без рекурсии стек можно контролировать по размеру).
  4. Функция не должна превышать одной страницы кода.
    • Не более 60 строк кода на функцию (по одной строке на инструкцию).
    • Почему: небольшие функции легче читать, тестировать и анализировать. Длинные функции обычно означают плохую структуру кода.
  5. Используй в среднём хотя бы два assert на функцию.
    • assert проверяет, что в коде не происходят невозможные ситуации.
    • assert должен:
      • быть без побочных эффектов,
      • представлять собой логическую проверку,
      • при срабатывании возвращать ошибку вызывающему коду.
    • Почему: assert помогает выявлять ошибки на ранних этапах и упрощает отладку. Их можно отключить после тестирования в коде отправляемом в официальный запуск в промышленность.
  6. Объявляй переменные в самом узком месте области видимости.
    • Почему: если переменная видна только там, где нужна, то:
      • её не могут случайно изменить другие части программы,
      • проще найти ошибку,
      • не будет переиспользования переменной для разных целей, что усложняет отладку.
  7. Проверяй значения, возвращаемые всеми функциями, и проверяй параметры.
    • Вызывающая функция должна проверять возвращаемое значение, а функция, принимающая параметры, должна проверять их корректность.
    • Почему: часто игнорирование возвратов функций приводит к ошибкам, особенно если функция возвращает ошибку, которую нужно обработать.
  8. Минимизируй использование препроцессора.
    • Разрешено:
      • подключение заголовков,
      • простые макросы.
    • Запрещено:
      • сложные макросы с подстановкой токенов,
      • макросы с переменным числом аргументов,
      • рекурсивные макросы.
    • Условная компиляция ( #ifdef ) должна использоваться по минимуму.
    • Почему: сложные макросы и условная компиляция ухудшают читаемость и усложняют анализ кода.
  9. Ограничь использование Указателей.
    • Разрешён только один уровень разыменования.
    • Запрещено:
      • скрывать разыменования в макросах и typedef,
      • использовать указатели на функции (если только нет очень веской причины).
    • Почему: указатели часто используются неправильно и усложняют анализ кода.
  10. Включай все предупреждения компилятора и анализаторов и устраняй их.
    • С самого начала проекта:
      • включай максимально строгие предупреждения компилятора,
      • устраняй все предупреждения,
      • ежедневно проверяй код статическими анализаторами.
    • Почему: современные анализаторы помогают быстро находить ошибки. Если компилятор или анализатор выдаёт предупреждение, надо переписать код так, чтобы избежать неоднозначности.
Не переживайте из-за не понятных сейчас вам концепций описанных в правилах, скоро вы узнаете о всём что они описывают.



Указатели

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

Получение доступа к данным с помощью указателей

Указатели — это очень полезный элемент языка C для эффективного программирования. Они являются переменными, хранящими адрес в памяти других переменных. Нет практически ни одной программы на C не использующей Указатели.

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

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

Операция разыменования *, также известна как косвенная операция.

Итак, символ *(астерикс, или просто звёздочка) - выполняет две функции при работе с именами указателей:

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

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

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

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

Рассмотрим пример:

  1. Напишем новую программу pointer.c:
    #include <stdio.h>
    
    int main()
    {
        int num = 8;     /* Объявляем и инициализируем переменную целочисленного типа */
        int *ptr = &num; /* Объявляем переменную-указатель,
             которую инициализируем значением адреса, по которому в памяти хранится переменная num */
    
        return 0;
    }
  2. Далее в блоке функции main() напечатаем в консоль содержимое обеих переменных, а также значение, к которому обращается указатель:
    #include <stdio.h>
    
    int main()
    {
        int num = 8;
        int *ptr = &num;
    
       printf("Regular variable contains: %d\n", num);
       printf("Pointer-variable contains: 0x%p\n", ptr); /* Спецификатор формата печати 0x%p
          отобразит адрес(%p) в шестнадцатеричном виде(0x) */
       printf("Pointer points to value: %d\n\n", *ptr);
    
        return 0;
    }
  3. Теперь в блоке функции main() присвойте новое значение обычной переменной с помощью указателя, а затем ещё раз напечатаем в консоль её содержимое и значение, к которому обращается указатель:
    #include <stdio.h>
    
    int main()
    {
        int num = 8;
        int *ptr = &num;
    
       printf("Regular variable contains: %d\n", num);
       printf("Pointer-variable contains: 0x%p\n", ptr);
       printf("Pointer points to value: %d\n\n", *ptr);
    
       *ptr = 12; /* По адресу в памяти, с целочисленным типом данных, записать значение 12 */
       printf("Regular variable contains: %d\n", num);   /* Значение переменной изменено */
       printf("Pointer-variable contains: 0x%p\n", ptr); /* Адрес тот же */
       printf("Pointer points to value: %d\n\n", *ptr);  /* Значение в памяти по адресу изменено */
    
        return 0;
    }
  4. Сохраним, скомпилируем и выполним программу:
    Пример работы с Указателями и Адресами памяти машины.
  1. Напишем новую программу l_endian.c , в которой подробнее рассмотрим адресацию памяти по принципу Little-Endian:
    #include <stdio.h>
    
    int main(void)
    {
       int A = 0x1245A79F; /* 4-байтовое целое значение */
       int *pointer = &A;  /* переменная pointer хранит адрес значения переменной A */
       printf("Value of A is = %X\n", A);
       printf("Address of A is = %X\n", &A);
       printf("Pointer value is = %p\n", pointer);
        
        return 0;
    }
  2. Для каждой машины будут отображаться свои значения адресов. Сохраним, скомпилируем и выполним программу:
    Обратите внимание: Шестнадцатиричное значение адреса и Значение Указателя внешне отличаются в консоли. Указатель отображается в размере восьми байт, а Адрес переменной в размере четырёх байт.

Размер значения Указателя

Указатель это Тип данных, представленный в языке C. Как и любой другой Тип данных, Указатель имеет размер - сколько байт в памяти он занимает.

Мы уже изучили функцию sizeof() , которой можно передать аргументом Указатель:

  1. Напишем новую программу sizepointer.c для демонстрации работы функции sizeof() с Указателем:
    #include <stdio.h>
    
    int main(void)
    {
        int A = 0xABCDEF12;
        int *pointer = &A;
        printf("\nPointer value is pointer = %p\n", pointer);
        printf("Size of pointer variable is = %zu bytes\n", sizeof(pointer));
    
        return 0;
    }
  2. Сохраним, скомпилируем и выполним программу несколько раз - Мы увидим, что Операционная Система, каждый раз при запуске программы выделяет другой адрес, но размер всегда 8 байт:

Указатель на Константу

Синтаксис указателя на константу следующий:

const <тип-данных> *<имя-константы>;

Данные на которые указывает Указатель постоянные, их значение изменить нельзя. Указатель можно изменить - т.е. на какую-то другую переменную он будет указывать - изменятся адрес значения, а не значение по адресу. Напишем простую программу constpointer.c для демонстрации концепции:

#include <stdio.h>

int main(void)
{
   int A = 45;
   int B = 67;

   const int *cip = &A; /* Указывает на адрес переменной A */
   *cip = 46; /* Ошибка компиляции -  нельзя менять константное значение */
   cip = &B;  /* А вот изменить адрес указателя - можно */
   *cip = 68; /* Ошибка компиляции -  нельзя менять константное значение */   

   return 0;
}

Сохраняем, пробуем компилировать...

Довольно просто можно понять ошибки обнаруженные компилятором:
constpointer.c: In function 'main':
constpointer.c:9:10: error: assignment of read-only location '*cip'
    9 |     *cip = 46;
      |          ^
constpointer.c:11:10: error: assignment of read-only location '*cip'
   11 |     *cip = 68;
      |          ^
error: assignment of read-only location - Ошибка: попытка назначения в область памяти со статусом "только-для-чтения"
Константные Указатели(Указатели на неизменяемые значения), широко используются при передаче параметров в функции - чтобы избежать случайного изменения параметров.

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

Перепишем код примера выше constpointer.c так:

#include <stdio.h>

int main(void)
{
    const int A = 45;
    printf("A = %d\n", A);

    int *cip = &A;

    *cip = 67; /* Нет ошибки! */
    printf("A = %d\n", A); /* Новое значение константы A !!! */

    return 0;
}
Компилятор не выдаёт ошибку но всё же предупреждает:
constpointer.c: In function 'main':
constpointer.c:8:16: warning: initialization discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
    8 |     int *cip = &A;
      |                ^

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

Проще говоря - вы инициализируете указатель, удаляя квалификатор const с типа, на который он указывает.

Итак:

Напоминаю - Объявляйте переменные до того, как в ход пойдут другие утверждения — например, в начале функции main().

Постоянный Указатель на Постоянное Значение в памяти

Синтаксис такого утверждения прост:

const <тип-данных> *const <имя-указателя> = &<имя-переменной>;

Напишем программу ccpntr.c для демонстрации работы такого утверждения:

#include <stdio.h>

int main(void)
{
   int Ru = 33;
   int En = 26;

   const int *const cip = &Ru; /* Инициализацию нужно написать здесь */
   *cip = 99; /* Ошибка компиляции */
   cip = &En; /* Ошибка компиляции */

    return 0;
}
Очевидно, компилятору "не нравится" то, что мы написали. Не возможно изменять ни постоянный адрес хранящийся в Указателе, ни постоянную переменную. Потому и две ошибки:
ccpntr.c: In function 'main':
ccpntr.c:9:10: error: assignment of read-only location '*(const int *)cip'
    9 |     *cip = 99;
      |          ^
ccpntr.c:10:9: error: assignment of read-only variable 'cip'
   10 |     cip = &En;
      |         ^

NULL Указатель

Если у Указателя отсутствует значение адреса тогда такой Указатель называют NULL-Указатель.

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

Напишем программу null_ptr.c:

#include <stdio.h>

int main(void)
{
   int *pntr_1;
   int *pntr_2 = NULL;
   printf("\nPointer #1 is %p\n", pntr_1); /* Мусорный адрес */
   printf("Pointer #2 is %p\n\n", pntr_2); /* NULL */

   return 0;
}
Сохраните, скомпилируйте и выполните программу.
Всегда инициализируйте Указатель. Если инициализировать Указатель не возможно конкретным адресом - примените NULL-Указатель.

VOID Указатель

void Указатель — это указатель общего (неопределённого) типа, который может указывать на данные любого типа. Его ещё называют:

По умолчанию размер void Указателя равен восьми байтам. Продемонстрируем это на примере программы. Напишем программу void_ptr.c:

#include <stdio.h>

int main(void)
{
   void *void_pntr = 0;
   printf("\nSize of (void *) is %zu bytes\n", sizeof(void_pntr));
   printf("void_pntr = %p\n\n", void_pntr);

   return 0;
}
Сохраните, скомпилируйте и выполните программу.

Тип данных на которые указывает void Указатель можно изменять в ходе выполнения программы. Напишем программу void_type_ptr.c:

#include <stdio.h>

int main(void)
{
   char A = 'A';
   float B = 3.141592;
   void *void_pntr = NULL;

   printf("\nBefore assignment\n");
   printf("Size of (void *) is %zu\n", sizeof(void_pntr));
   printf("void_pntr is %p\n", void_pntr);

   void_pntr = &A; /* Указывает на char */
   printf("\nAfter A assignment\n");
   printf("Size of (void *) is %zu\n", sizeof(void_pntr));
   printf("void_pntr is %p\n\n", void_pntr);

   void_pntr = &B; /* Указывает на float */
   printf("\nAfter B assignment\n");
   printf("Size of (void *) is %zu\n", sizeof(void_pntr));
   printf("void_pntr is %p\n\n", void_pntr);

   return 0;
}
Сохраните, скомпилируйте и выполните программу.
void Указатель не возможно разыменовать, так его тип неопределённый.

Приведение типа void Указателя

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

Напишем программу void_to_type.c

#include <stdio.h>

int main(void)
{
   int A = 0x87654321;
   void *void_pntr = NULL;

   printf("\nBefore assignment\n");
   printf("Size of (void *) is %zu\n", sizeof(void_pntr));
   printf("void_pntr = %p\n\n", void_pntr);

   void_pntr = &A;
   printf("After assignment\n");
   printf("Size of (void *) is %zu\n", sizeof(void_pntr));
   printf("void_pntr = %p\n\n", void_pntr);
   printf("A = %x", *void_pntr); /* Вызывает и предупреждение и ошибку компиляции */

   return 0;
}

Предупреждение:

void_to_type.c: In function 'main':
void_to_type.c:16:22: warning: dereferencing 'void *' pointer
   16 |     printf("A = %x", *void_pntr);

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

Ошибка компиляции:

void_to_type.c:16:22: error: invalid use of void expression

Не правомерное использование void выражения

Исправим ошибку так:

#include <stdio.h>

int main(void)
{
   int A = 0x87654321;
   void *void_pntr = NULL;

   printf("\nBefore assignment\n");
   printf("Size of (void *) is %zu\n", sizeof(void_pntr));
   printf("void_pntr = %p\n\n", void_pntr);

   void_pntr = &A;
   printf("After assignment\n");
   printf("Size of (void *) is %zu\n", sizeof(void_pntr));
   printf("void_pntr = %p\n\n", void_pntr);
    
   printf("Casting is used\n");
   /* Определяем размер приведённого к целочисленному типу Указателя */
   printf("Size of (int *) is %zu\n", sizeof((int *)void_pntr));

   /* Приведение Указателя к целочисленному типу */
   printf("void_pntr = %p\n", (int *)void_pntr);

   /* Разыменование приведённого к целочисленному типу Указателя */
   printf("A = *void_pntr = 0x%x\n", *((int *)void_pntr));

   return 0;
}
Нет ни предупреждений ни ошибок компиляции.

Арифметика указателей

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

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

Более длинные смещения можно осуществить с помощью операций += и -=, которые позволяют указать, на сколько позиций следует сместиться.

Арифметика указателей особенно полезна при работе с массивами, поскольку элементы массива занимают место в памяти последовательно.

Присвоение имени массива указателю автоматически присваивает ему адрес в памяти первого элемента этого массива. Увеличение значения указателя на 1 переместит его к следующему элементу массива.

  1. Начните новую программу movptr.c с инструкции препроцессора, позволяющей включить функции стандартной библиотеки ввода/вывода, объявите целочисленную переменную, и целочисленный массив с десятью элементами:
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
       return 0;
    }
  2. Далее, в главной функции объявите переменную-указатель и проинициализируйте его адресом первого элемента массива, затем выведите этот адрес и значение, которое располагается по этому адресу в памяти:
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
       int *pointer = array;
       printf("\nAt address: %p is Value: %d\n", pointer, *pointer);
    
       return 0;
    }
  3. Теперь, в главной функции увеличивайте значение указателя на 1, чтобы cмещать его к следующему элементу массива один за другим:
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
       int *pointer = array;
       printf("\nAt address: %p is Value: %d\n", pointer, *pointer);
    
       pointer++;
       printf("At address: %p is Value: %d\n", pointer, *pointer);
       pointer++;
       printf("At address: %p is Value: %d\n", pointer, *pointer);
    
       return 0;
    }
  4. Теперь сместим адрес на два элемента назад, чтобы указатель адресовал первый элемент массива:
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
       int *pointer = array;
       printf("\nAt address: %p is Value: %d\n", pointer, *pointer);
    
       pointer++;
       printf("At address: %p is Value: %d\n", pointer, *pointer);
       pointer++;
       printf("At address: %p is Value: %d\n", pointer, *pointer);
    
       pointer -= 2;
       printf("At address: %p is Value: %d\n\n", pointer, *pointer);
    
       return 0;
    }
  5. Далее добавьте цикл for() и выведите индекс каждого элемента массива, а также их значения:
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
       int *pointer = array;
       printf("\nAt address: %p is Value: %d\n", pointer, *pointer);
    
       pointer++;
       printf("At address: %p is Value: %d\n", pointer, *pointer);
       pointer++;
       printf("At address: %p is Value: %d\n", pointer, *pointer);
    
       pointer -= 2;
       printf("At address: %p is Value: %d\n\n", pointer, *pointer);
    
       for (i = 0; i < 10; i++) /* В коде появилось "магическое" число 10 !!! */
       {
          printf("Element %d Contains Value: %d", i, *pointer);
          pointer++;
       }
    
       return 0;
    }
  6. Сохраните файл программы, а затем скомпилируйте и запустите программу, чтобы увидеть значение переменной и значение, к которому обращается указатель:
    1. Обратите внимание на количество байт, на которое осуществляется смещение адреса при инкременте - 4 байта, ровно такой размер имеет тип int. При декременте на значение 2, смещение произошло на 2x4 = 8 байт.
    2. Избавимся от "магического" числа 10. Мы изучили функцию sizeof(), которая возвращает значение типа size_t(целое беззнаковое число размером unsigned long) - т.е. функция возвращает количество байт. В проверочном утверждении цикла изменим код так:

      Вместо:

      for(i=0; i < 10; i++)

      Напишем:

      for(i=0; i < (sizeof(array)/sizeof(int)); i++)

      Где:

      • sizeof(array) Возвратит нам размер массива в байтах(это 40 байт)
      • sizeof(int) Возвратит нам размер типа int в байтах на текущей машине(это 4 байта).
      • 40 / 4 = 10, Итого у нас 10 элементов в массиве. Мы избавили код от "магического" числа, и тем самым явно описали "что" проверяется в условии проверочного утверждения цикла.
    3. Вспомните, в массиве, первый его элемент имеет индекс 0. Именно по этому мы инициализируем i нулём при первой итерации цикла.
      for(i=0; i < (sizeof(array)/sizeof(int)); i++)

      В теле цикла мы инкрементируем смещение значения адреса хранимое в переменной pointer

      pointer++;

      А в этой строке кода, выводим на печать в консоль и индекс и элемент массива(с помощью разыменования переменной-указателя):

      printf("Element %d Contains Value: %d\n", i, *pointer);

      Можно внести небольшое изменение в код тела цикла, сохранив при этом прозрачность кода так:

      Было:

      for (i = 0; i < (sizeof(array)/sizeof(int)); i++)
      {
         printf("Element %d Contains Value: %d\n", i, *pointer);
         pointer++;
      }

      Стало:

      for (i = 0; i < (sizeof(array)/sizeof(int)); i++)
      {
         printf("Element %d Contains Value: %d\n", i, *pointer + i);
         
      }
      Имя массива выступает в качестве указателя на его первый элемент.
  7. Пропишите все изменения в коде, сохраните, скомпилируйте и выполните программу снова.

Смещение void Указателя

Мы изучили, что по умолчанию инкремент и декремент смещают адрес на один байт. Но, в зависимости от типа данных Указателя, смещение будет производится согласно размеру этого типа данных занимаемого в памяти.

Для Указателя неопределённого типа данных - void-Указателя, при приведении его к конкретному типу данных, смещение автоматически изменяется на размер типа данных.

Напишем новую программу incdec_void_type.c для демонстрации этого, инициализируем три типа переменных: short int, float, double, объявим NULL-Указатель и сначала инициализируем его адресом переменной I типа short int.

Напишем фрагмент кода для short int

#include <stdio.h>

int main(void)
{
   short int I = 0xA1B2; /* 2 bytes */
   float F = 324.591450; /* 4 bytes */
   double D = 45.123456789; /* 8 bytes */
   void *void_pntr = NULL;

   void_pntr = &I;
   printf("\nShort Integer is pointed. No casting:\n");
   printf("void_pntr = %p\n", void_pntr);
   printf("void_pntr + 1 = %p\n", void_pntr + 1);
   printf("(short int *) casting is used:\n");
   printf("(short int *)void_pntr = %p\n", (short int *)void_pntr);
   printf("(short int *)void_pntr + 1 = %p\n", (short int *)void_pntr + 1);
   printf("Size of Short int data type pointer is %zu bytes\n\n", sizeof(short int));

   return 0;
}

Напишем фрагмент кода для float

#include <stdio.h>

int main(void)
{
   short int I = 0xA1B2; /* 2 bytes */
   float F = 324.591450; /* 4 bytes */
   double D = 45.123456789; /* 8 bytes */
   void *void_pntr = NULL;
   
   void_pntr = &I;
   printf("\nShort Integer is pointed. No casting:\n");
   printf("void_pntr = %p\n", void_pntr);
   printf("void_pntr + 1 = %p\n", void_pntr + 1);
   printf("(short int *) casting is used:\n");
   printf("(short int *)void_pntr = %p\n", (short int *)void_pntr);
   printf("(short int *)void_pntr + 1 = %p\n", (short int *)void_pntr + 1);
   printf("Size of Short int data type pointer is %zu bytes\n\n", sizeof(short int));

   void_pntr = &F;
   printf("\nFloat is pointed. No casting:\n");
   printf("void_pntr = %p\n", void_pntr);
   printf("void_pntr + 1 = %p\n", void_pntr + 1);
   printf("(float *) casting is used:\n");
   printf("(float *)void_pntr = %p\n", (float *)void_pntr);
   printf("(float *)void_pntr + 1 = %p\n", (float *)void_pntr + 1);
   printf("Size of Float data type pointer is %zu bytes\n\n", sizeof(float));

   return 0;
}

Напишем фрагмент кода для double

#include <stdio.h>

int main(void)
{
   short int I = 0xA1B2; /* 2 bytes */
   float F = 324.591450; /* 4 bytes */
   double D = 45.123456789; /* 8 bytes */
   void *void_pntr = NULL;
   
   void_pntr = &I;
   printf("\nShort Integer is pointed. No casting:\n");
   printf("void_pntr = %p\n", void_pntr);
   printf("void_pntr + 1 = %p\n", void_pntr + 1);
   printf("(short int *) casting is used:\n");
   printf("(short int *)void_pntr = %p\n", (short int *)void_pntr);
   printf("(short int *)void_pntr + 1 = %p\n", (short int *)void_pntr + 1);
   printf("Size of Short int data type pointer is %zu bytes\n\n", sizeof(short int));

   void_pntr = &F;
   printf("\nFloat is pointed. No casting:\n");
   printf("void_pntr = %p\n", void_pntr);
   printf("void_pntr + 1 = %p\n", void_pntr + 1);
   printf("(float *) casting is used:\n");
   printf("(float *)void_pntr = %p\n", (float *)void_pntr);
   printf("(float *)void_pntr + 1 = %p\n", (float *)void_pntr + 1);
   printf("Size of Float data type pointer is %zu bytes\n\n", sizeof(float));

   void_pntr = &D;
   printf("\nDouble is pointed. No casting:\n");
   printf("void_pntr = %p\n", void_pntr);
   printf("void_pntr + 1 = %p\n", void_pntr + 1);
   printf("(double *) casting is used:\n");
   printf("(double *)void_pntr = %p\n", (double *)void_pntr);
   printf("(double *)void_pntr + 1 = %p\n", (double *)void_pntr + 1);
   printf("Size of Double data type pointer is %zu bytes\n\n", sizeof(double));

   return 0;
}

Сохраните, скомпилируйте и выполните программу.

Пример приведения типов и смещения адреса void-Указателя.
Для отсоединения окна терминала от основного окна Visual Studio Code, нажмите на значок >_ powershell, и выберите пункт в выпадающем меню - Move Terminal into New Window

Считываем любой байт в памяти

С помощью void-Указателя, приведения типа и смещения по адресу, возможно считать любой байт в памяти.

Напишем новую программу anybyte.c:

#include <stdio.h>

int main(void)
{
   int A = 0x0134756A;
   void *void_pntr = NULL;
    
   void_pntr = &A;
   printf("\nInteger is pointed. No casting.\n");
   printf("void_pntr = %p\n\n", void_pntr);

   char *cp =(char *)(void_pntr + 2);
   printf("cp = void_pntr + 2 = %p\n", cp);

   printf("The value at position void_pntr + 2 %p is\n", void_pntr + 2);
   printf("%x\n\n", *cp);

   return 0;
}
Мы объявили и инициализировали целочисленную переменную типа int(4 байта). Но после приведения типа адреса к типу char и смещения,- смогли считать третий байт целочисленной переменной.

Представим визуально память машины и с какими данными и по каким адресам мы работали:

Мы объявили и инициализировали целочисленную переменную типа int(4 байта). Но после приведения типа адреса к типу char и смещения на 2 байта,- смогли считать третий байт блока памяти выделенного для целочисленной переменной. Таким образом - задавая смещение - мы можем считывать любые данные из памяти машины и приводя их к нужному нам типу.
Типы данных в языке С
Категория Ключевые слова Размер (обычно) Описание Пример
Целое знаковое int 2 или 4 байта Целые числа (отрицательные и положительные) int a = -5;
Целое знаковое short int, short 2 байта Короткие Целые числа (отрицательные и положительные) short b = 10;
Целое знаковое long int, long 4 байта (или 8 байт) Длинные Целые числа (отрицательные и положительные) long c = 100000;
Целое знаковое long long int, long long 8 байт Очень Длинные Целые числа (отрицательные и положительные) long long d = 1*e12;
Целое беззнаковое unsigned int, unsigned 2 или 4 байта Только положительные целые числа unsigned int e = 5;
Целое беззнаковое unsigned short int, unsigned short 2 байта Короткие положительные целые числа unsigned short f = 20;
Целое беззнаковое unsigned long int, unsigned long 4 байта (или 8 байт) Длинные положительные целые числа unsigned long g = 1*e6;
Целое беззнаковое unsigned long long int, unsigned long long 8 байт Очень Длинные положительные целые числа unsigned long long h = 1*e15;
Символьное char 1 байт Символы (ASCII) или маленькие числа char ch = 'C';
Символьное unsigned char 1 байт Беззнаковый символ unsigned char uch = 255;
Символьное signed char 1 байт Знаковый символ signed char sch = -128;
С плавающей точкой float 4 байта Числа с плавающей точкой одинарной точности float f = 3.141592;
С плавающей точкой double 8 байт Числа с плавающей точкой двойной точности double d = 2.718281828;
С плавающей точкой long double 10-16 байт Числа с плавающей точкой расширенной точности long double ld = 1.234567e+30;
Специальный тип void - Отсутствие типа данных void function(void) {...}
Указатели <тип>* 4 или 8 байт Адрес в памяти, указывает на данные любого типа int *ptr = &ch;

Указатель на Указатель

Девятое правило программирования от NASA:

Хорошее правило. В языке С есть возможность создавать Указатель на Указатель и в редких случаях это применяется, знать как написать программу с таким функционалом нужно. Напишем программу ptr_to_ptr.c для демонстрации:

#include <stdio.h>

int main(void)
{
    int A= 0x98ABCDEF;
    int *pntr_A = &A;

     /* int ** - Это синтаксис, который понимает компилятор GCC для Windows.
        Такая запись позволяет создать Указатель на Указатель - т.е. 
        Указатель инициализируется значением адреса другого Указателя */

    int **pntr_pntr_A = &pntr_A;

    /* Указатель на адрес хранения значения переменной А */

    printf("\nPointer`s value of pntr_A is: %p\n", pntr_A);

    /* Указатель на Указатель адреса хранения значения переменной А */

    printf("Pointer`s value of pntr_pntr_A is: %p\n\n", pntr_pntr_A); 

    return 0;
}

Вот визуальное объяснение что происходит в памяти машины:

Указатель на Указатель на Указатель

Да, и такое встречается - но крайне редко. Так как язык C является языком среднего уровня между Ассемблером и Языками высокого уровня, он всё же отлично подходит для написания программ для работы с аппаратной архитектурой машины. Вычислительная машина это вопервых Процессор и Память - именно при работе с памятью и используются Указатели.

Продемонстрируем и этот функционал написав программу ptr_ptr_ptr.c:

#include <stdio.h>

int main(void)
{
   int A = 22;
   int *ptr_A = &A;
   int **ptr_ptr_A = &ptr_A;
   int ***ptr_ptr_ptr_A = &ptr_ptr_A;

   printf("\n%p\n", ptr_A);
   printf("%p\n", ptr_ptr_A);
   printf("%p\n\n", ptr_ptr_ptr_A);

   return 0;
}

Второй Указатель указывает на Первый Указатель

Напишем программу pntr_2_to_pntr_1.c
#include <stdio.h>

int main(void)
{
   int A = 0x123654AF; /* Значение переменной */
   int *pntr_1 = &A; /* Адрес на Значение переменной */
   int **pntr_2 = &pntr_1; /* Адрес на Адрес на Значение переменной */

   printf("\nPointer value of pntr_1 is %p\n", pntr_1); /* Адрес на Значение переменной */
   printf("Pointer value of pntr_2 is %p\n", pntr_2); /* Адрес на Адрес на Значение переменной */

   printf("*pntr_2 is %p\n\n", *pntr_2); /* Адрес на Значение переменной */

   return 0;
}

Сохраните, скомпилируйте и выполните программу.

Передача указателей в функции

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

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

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

  1. Новая программа passpntr.c продемонстрирует эту возможность. Объявите два прототипа пользовательских функций, в каждую из которых передается один целочисленный указатель.:
    #include <stdio.h>
    
    void twice(int *pntr);
    void thrice(int *pntr);
    
    int mian(void)
    {
       int num = 5;
       int *pntr = &num;
    
       return 0;
    }
  2. Далее в блоке функции main() напечатаем адрес, хранящийся в указателе, а также значение, на которое ссылается указатель:
    #include <stdio.h>
    
    void twice(int *pntr);
    void thrice(int *pntr);
    
    int mian(void)
    {
       int num = 5;
       int *pntr = &num;
    
       printf("\npntr stores address: %p\n", pntr);
       printf("pntr dereference value: %d\n\n", *pntr);
    
       return 0;
    }
  3. Теперь напечатаем в консоль оригинальное значение целочисленной переменной:
    #include <stdio.h>
    
    void twice(int *pntr);
    void thrice(int *pntr);
    
    int mian(void)
    {
       int num = 5;
       int *pntr = &num;
    
       printf("\npntr stores address: %p\n", pntr);
       printf("pntr dereference value: %d\n\n", *pntr);
       printf("The num value is %d\n\n", num);
    
       return 0;
    }
  4. После блока функции main() определите две пользовательские функции, объявленные с помощью прототипов, каждая из которых получает в качестве аргумента целочисленный указатель:
    #include <stdio.h>
    
    void twice(int *pntr);
    void thrice(int *pntr);
    
    int mian(void)
    {
       int num = 5;
       int *pntr = &num;
    
       printf("\npntr stores address: %p\n", pntr);
       printf("pntr dereference value: %d\n\n", *pntr);
       printf("The num value is %d\n\n", num);
    
       return 0;
    }
    
    void twice(int *number)
    {
       *number = (*number * 2);
    }
    
    void thrice(int *number)
    {
       *number = (*number * 3);
    }
  5. В блок функции main() добавьте вызовы пользовательских функций, передавая адрес обычной переменной как ссылку, а затем выведите изменённое значение обычной переменной.
    #include <stdio.h>
    
    void twice(int *pntr);
    void thrice(int *pntr);
    
    int mian(void)
    {
       int num = 5;
       int *pntr = &num;
    
       printf("\npntr stores address: %p\n", pntr);
       printf("pntr dereference value: %d\n\n", *pntr);
       printf("The num value is %d\n\n", num);
    
       twice(pntr);
       printf("Tne num value is now %d", num);
       thrice(pntr);
       printf("And now the num value is %d\n", num);
    
       return 0;
    }
    
    void twice(int *number)
    {
       *number = (*number * 2);
    }
    
    void thrice(int *number)
    {
       *number = (*number * 3);
    }
    Сохраните, скомпилируйте и выполните программу:
    Исправьте ошибки в программе, внесите необходимые изменения в исходный код, чтобы вывод в консоль выглядел как на снимке экрана.

Итак, вспомним - все переменные размещаются в памяти машины. Каждое такое размещение является байтами в памяти. Каждый байт в памяти имеет свой адрес.

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

Имя массива - является Указателем на этот массив, и по сути указывает на голову массива - на его первый элемент. Это и есть место размещения массива в памяти машины.

Имя функции - является Указателем на адрес в памяти, где размещаются все утверждения определения функции.

  1. Напишем программу array_func.c демонстрирующую адреса размещения массива и функции в памяти машины. Объявим прототип функции и определим её после главной функции, инициализируем целочисленные переменные и целочисленный массив:
    #include <stdio.h>
    #include <math.h>
    
    void myFunction();
    
    int main(void)
    {
       int B = 2;
       int C = 16;
       int A = pow(B, C);
       int array[3] = {11, 22, 33};
    
       return 0;
    }
    
    void myFunction()
    {
       printf("Inside myFunction\n");
    }
  2. В теле главной функции напечатаем адреса целочисленной переменной, массива и функции. И убедимся, что имена Массива и Функции и есть их Указатели:
    #include <stdio.h>
    #include <math.h>
    
    void myFunction();
    
    int main(void)
    {
       int B = 2;
       int C = 16;
       int A = pow(B, C);
       int array[3] = {11, 22, 33};
    
       printf("\nAddress of A integer is %p\n\n", &A); /* Адрес переменной */
    
       printf("Address of array is %p\n", array); /* Имя массива есть его Указатель */
       printf("Address of array is %p\n\n", &array); /* Адрес массива есть его Указатель */
    
       printf("Address of myFunction is %p\n", myFunction); /* Имя функции есть её Указатель */
       printf("Address of myFunction is %p\n\n", &myFunction); /* Адрес функции есть её Указатель */
    
       return 0;
    }
    
    void myFunction()
    {
       printf("Inside myFunction\n");
    }
  3. Выполните программу:
    Обратите ещё раз внимание на синтаксис: записи имени и адреса для массива и функции возвращают одинаковые адреса. Эта особенность нам скоро очень пригодится.
Запомните: Символ *, находясь в скобках определения функции, показывает, что аргумент является указателем, а находясь внутри утверждения, с его помощью можно получить аргумент по ссылке. В арифметическом утверждении этот символ является операцией умножения.

Подробнее о синтаксисе Указателей Функций

Синтаксис в общем виде записывается так:

<возвращаемый-тип-данных> (*<имя-Указателя>)(<аргумент-1>, <аргумент-2>, ...)

Например:

Объявление прототипа функции записывается так:

int myFunc(int, int);

Объявление Указателя Функции записывается так:

float (*funcPointer)(int, int);

Как мы уже знаем - Имя Функции и Адрес Функции есть Указатель и записывается так:

funcPointer = myFunc;

или так:

funcPointer = &myFunc;

Вызов функции можно записать так:

funcPointer(intVal1, intVal2);

или так:

(*funcPointer)(intVal1, intVal2);
  1. Напишем программу func_pointer.c для иллюстрации выше описанного:
    #include <stdio.h>
    
    int mySummation(int A, int B);
    
    int main(void)
    {
       int A = 34;
       int B = 66;
       int sum = 0;
    
       int (*funcPointer)(int, int); /* Объявляем Указатель Функции */
       funcPointer = &mySummation;   /* Инициализируем Указатель Функции значением адреса Функции,
            или вторая запись синтаксиса: funcPointer = mySummation; */
       sum = funcPointer(A, B);      /* Переменной присваивается значение
            возвращаемое функцией вызываемой по ссылке на неё */
    
       printf("\nSummation of A = %d and B = %d (A+B) is %d\n\n", A, B, sum);
    
       return 0;
    }
    
    int mySummation(int A, int B)
    {
       return A + B;
    }
  2. Выполните программу:
    Пример вызова Функции по ссылке - через инициализацию Указателя Адресом Функции.

Вот простая аналогия для понимания концепции Указателей Функций. Представь меню в ресторане:

Ты можешь:

Проясним детальнее Прототип функции типа void:

void *(*funcPointer)(int *, int *);

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

(*funcPointer)(int *, int *)

Указатель Функции принимает два аргумента, которые являются целочисленными указателями, и потом возвращается

void *

void-Указатель,- Указатель неопределённого типа.

Создание массивов указателей

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

Эта возможность особенно полезна для работы с символьными строками. Массив символов, который заканчивается символом \0 носит статус строки, поэтому его можно присвоить переменной-указателю. Имя символьного массива служит как указатель на его первый элемент, поэтому не требуется операция адресации & для того, чтобы присвоить строку переменной-указателю.

  1. Напишем новую программу array_pointer.c, объявим целочисленную переменную и целочисленный массив, массив инициализируем:
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int array[5] = {1, 2, 3, 4, 5};    
    
       return 0;
    }
  2. Далее объявите и проинициализируйте целочисленные переменные-указатели, содержащие адрес каждого элемента массива так:
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int array[5] = {1, 2, 3, 4, 5};
       
       int *elmnt_pntr_0 = &array[0];
       int *elmnt_pntr_1 = &array[1];
       int *elmnt_pntr_2 = &array[2];
       int *elmnt_pntr_3 = &array[3];
       int *elmnt_pntr_4 = &array[4];
    
       return 0;
    }
  3. Теперь объявите и проинициализируйте массив целочисленных указателей, каждый элемент которого будет содержать один из целочисленных указателей:
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int array[5] = {1, 2, 3, 4, 5};
       
       int *elmnt_pntr_0 = &array[0];
       int *elmnt_pntr_1 = &array[1];
       int *elmnt_pntr_2 = &array[2];
       int *elmnt_pntr_3 = &array[3];
       int *elmnt_pntr_4 = &array[4];
    
       int *pntrs_array[5] = {elmnt_pntr_0,
                              elmnt_pntr_1,
                              elmnt_pntr_2,
                              elmnt_pntr_3,
                              elmnt_pntr_4};
    
       return 0;
    }
  4. Далее объявите и проинициализируйте массив символов, указатель на этот массив и массив символьных указателей, содержащий строку внутри каждого элемента:
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int array[5] = {1, 2, 3, 4, 5};
       
       int *elmnt_pntr_0 = &array[0];
       int *elmnt_pntr_1 = &array[1];
       int *elmnt_pntr_2 = &array[2];
       int *elmnt_pntr_3 = &array[3];
       int *elmnt_pntr_4 = &array[4];
    
       int *pntrs_array[5] = {elmnt_pntr_0,
                              elmnt_pntr_1,
                              elmnt_pntr_2,
                              elmnt_pntr_3,
                              elmnt_pntr_4};
    
       char string[9] = {'C', ' ', 'i', 's', ' ', 'F', 'u', 'n', '\0'};
       char *str_ptnr = string;
    
       char *strings_array[3] = {"Alpha", "Bravo", "Charlie"};
    
       return 0;
    }
    Для того чтобы добавить в строку символ пробела, необходимо использовать комбинацию символов ' '(одинарная кавычка, пробел, одинарная кавычка). Две одинарные кавычки, расположенные рядом — '', рассматривается как пустой элемент и вызывает ошибку компилятора.
  5. Добавьте цикл for(), с помощью которого выводится адрес каждого элемента массива целочисленных указателей и значения, на которые они ссылаются:
    #include <stdio.h>
    
    int main(void)
    {
       int i;
       int array[5] = {1, 2, 3, 4, 5};
       
       int *elmnt_pntr_0 = &array[0];
       int *elmnt_pntr_1 = &array[1];
       int *elmnt_pntr_2 = &array[2];
       int *elmnt_pntr_3 = &array[3];
       int *elmnt_pntr_4 = &array[4];
    
       int *pntrs_array[5] = {elmnt_pntr_0,
                              elmnt_pntr_1,
                              elmnt_pntr_2,
                              elmnt_pntr_3,
                              elmnt_pntr_4};
    
       char string[9] = {'C', ' ', 'i', 's', ' ', 'F', 'u', 'n', '\0'};
       char *str_ptnr = string;
       char *strings_array[3] = {"Alpha", "Bravo", "Charlie"};
    
       for(i = 0; i < sizeof(array)/sizeof(int); i++)
       {
          printf("\nThe element [%d] have address %p and value of %d", i, pntrs_array[i], *pntrs_array[i]);
       }
    
       return 0;
    }
  6. Добавьте утверждение, печати в консоль значения, хранящегося в массиве символов string[9]:
    printf("\n\nString is: %s\n\n", string);
  7. Добавьте цикл for(), с помощью которого напечатайте каждую строку, содержащуюся в каждом элементе массива символьных указателей и адрес каждой строки:
    for (i = 0; i < 3; i++)
    {
       printf("\nString [%d] at address %p contain: %s", i, strings_array[i], strings_array[i]);
    }
    С помощью имени указателя можно обратиться ко всей строке, хранящейся в символьном массиве, не прибегая к использованию операции * разыменования.
  8. Выполните программу:
    Определите: Сколько байт занимает 1-ая и 2-я строка символьного массива, судя по адресам их размещения в памяти? Почему столько? Сколько байт занимает 3-я строка символьного массива?

Указатели и Строковые литералы

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

  1. Напишем программу pntrs_arrays.c. Приведём пример когда сама строка является Постоянным Адресом в памяти машины:
    #include <stdio.h>
    
    int main(void)
    {
        char *Ch = "Pointers"; /* Инициализация Строкового Литерала - Массива символов. 
          Ch — это указатель на char, и в него записан адрес первого символа(P) строки "Pointers". */
        printf("\n%p Ch is pointer\n", Ch); /* Печатает адрес, на который указывает Ch. 
          Это адрес первого символа(P) строки "Pointers". */
        printf("%p Constant sddress of \"Pointers\"\n", "Pointers"); /* Здесь передаётся сам строковый литерал 
          "Pointers", который неявно приводится к const char *. 
          Это будет тот же адрес, что в предыдущем printf(), 
          или может отличаться при оптимизациях компилятора, но обычно одинаков. */
        printf("%p &Constant address of &\"Pointers\"\n\n", &"Pointers"); /* Здесь &"Pointers" — 
          это адрес всего строкового литерала как объекта. 
          По сути в языке C "Pointers" является массивом char[9], состоящий из 8 символов + '\0' символ конца строки. 
          Оператор & даёт адрес этого массива целиком, но так как массив и его адрес совпадают, 
          этот адрес будет тем же, что и в предыдущих двух printf().
          Кстати, для строковых литералов ptr == &ptr[0] всегда true */
        
        printf("%p &Ch another pointer\n\n", &Ch); /* Здесь мы берём адрес переменной Ch. 
          Где Ch — это указатель на char. &Ch — это указатель на переменную Ch, т.е. char **.
          Адрес будет другим, так как это адрес локальной переменной Ch, которая хранится в стеке main(). */
    
        return 0;
    }
    Почему &Ch печатает другой адрес?
    • Адрес при &Ch, — это физический адрес переменной Ch в стеке функции main().
    • Этот адрес не имеет никакого отношения к строковому литералу "Pointers" в сегменте данных программы.
  2. Выполните программу:
    Первые три строки в консоли печатают одинаковый адрес. А четвёртая строка - уже другой адрес. Изучите комментарии в коде и объяснение под кодом для понимания что значит каждый адрес.

Указатель на строковый литерал и Строковый Литерал

В программе string_literal.c ниже, написан код в котором Указатель на строковый литерал содержит адрес размером в восемь байт, и также инициализирован массив - он же строковый литерал, содержащий восемь символов (7 букв и символ окончание строки \0):

#include <stdio.h>

int main(void)
{
    char *Ch = "Address";
    char Str[8] = "Address";

    printf("Size of Ch is %zu bytes, it is an address.\n", sizeof(Ch)); /* Ch - Указатель на символьный литерал */
    printf("Size of Str is %zu bytes, it is number of characters in array.\n", sizeof(Str)); /* Str - Массив символов - 
      символьный литерал */

    return 0;
}

Выполните программу:

Заметьте, функция sizeof() записывается одинаково как для указателя так и для имени литерала, однако возвращает значения для различных типов данных: В первом случаи - адрес, во втором - размер массива.
  1. Напишем новую программу context.c, в которой посмотрим ещё раз как придать контекст для типа данных.
    #include <stdio.h>
    
    int main(void)
    {
        char *Ch = "Hello";
        char Str[6] = "Hello";
        printf("\nSize of Ch address is %zu bytes\n", sizeof(Ch));
        /* Это контекст адреса Ch */
        printf("Size of Str (elements in array) is %zu bytes\n", sizeof(Str)); 
        /* Это контекст количества элементов в массиве Str */
        printf("Str is pointer %p\n", Str); 
        /* Это контекст адреса Str */
        printf("&Str is same pointer %p\n", &Str); 
        /* Это контекст того же адреса Str */
        printf("Size of Str is %zu bytes, and 6 characters in array\n", sizeof(Str)); 
        /* Это контекст количества элементов в массиве Str */
        printf("Size of &Str is %zu bytes, it is address of Str\n\n", sizeof(&Str)); 
        /* Это контекст адреса Str */
    
        return 0;
    }
  2. Выполните программу:
    Внимательно изучите пример, и запомните различные контексты типов данных при работе с Указателями и Размерами.

Массив Указателей с приведением типа данных

Напишем новую программу pntrs_array_type.c

  1. До главной функции запишем три пользовательских прототипа функций, после главной функции определим их:
    #include <stdio.h>
    
    int mySummation(int A, int B);
    int mySubstraction(int A, int B);
    int myMultiplication(int A, int B);
    
    int main(void)
    {
       return 0;
    }
    
    int mySummation(int A, int B)
    {
        return A + B;
    }
    
    int mySubstraction(int A, int B)
    {
        return A - B;
    }
    
    int myMultiplication(int A, int B)
    {
        return A * B;
    }
  2. Внутри главной функции Объявим и Инициализируем целочисленные переменные:
    int A = 46;
    int B = 79;
    int sum;
    int sub;
    int mult;
  3. Внутри главной функции, после целочисленных переменных, Объявим массив Указателей на Функции целочисленного типа:
    int (*func_pntr[3])(int, int);

    где:

    1. func_pntr[3] - Это массив из трёх элементов с именем func_pntr.
    2. (*func_pntr[3]) - Элементы массива являются указателями (звёздочка относится к каждому элементу массива).
    3. (*func_pntr[3])(int, int) - Каждый указатель указывает на функцию, принимающую два int.
    4. int (*func_pntr[3])(int, int); - Каждая такая функция возвращает int.
  4. Внутри главной функции, далее инициализируем каждый указатель функции из массива func_pntr адресом соответствующей ему функции:
    func_pntr[0]=mySummation;
       func_pntr[1]=mySubstraction;
       func_pntr[2]=myMultiplication;
  5. Внутри главной функции, далее инициализируем каждую переменную значением возвращаемым пользовательскими функциями по ссылке:
    sum = func_pntr[0](A, B);
    sum = func_pntr[1](A, B);
    mult = func_pntr[2](A, B);
  6. Внутри главной функции, далее напечатаем результат выполнения каждой пользовательской функции:
    printf("\nA is %d, B is %d,  A + B = %d\n", A, B, sum);
    printf("A is %d, B is %d,  A - B = %d\n", A, B, sub);
    printf("A is %d, B is %d,  A x B = %d\n\n", A, B, mult);
  7. Итоговый исходный код должен быть таким:
    #include <stdio.h>
    
    int mySummation(int A, int B);
    int mySubstraction(int A, int B);
    int myMultiplication(int A, int B);
    
    int main(void)
    {
        int A = 46;
        int B = 79;
        int sum;
        int sub;
        int mult;
    
        int (*func_pntr[3])(int, int);
    
        func_pntr[0] = mySummation;
        func_pntr[1] = mySubstraction;
        func_pntr[2] = myMultiplication;
    
        sum = func_pntr[0](A, B);
        sub = func_pntr[1](A, B);
        mult = func_pntr[2](A, B);
    
        printf("\nA is %d, B is %d,  A + B = %d\n", A, B, sum);
        printf("A is %d, B is %d,  A - B = %d\n", A, B, sub);
        printf("A is %d, B is %d,  A x B = %d\n\n", A, B, mult);
    
        return 0;
    }
    
    int mySummation(int A, int B)
    {
        return A + B;
    }
    int mySubstraction(int A, int B)
    {
        return A - B;
    }
    int myMultiplication(int A, int B)
    {
        return A * B;
    }
  8. Выполним программу:
    Пример синтаксиса создания массива Указателей на Функции.

Указатели на функции

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

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

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

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

  1. Напишем новую программу fpntr_to_func.c. Объявите прототип одной пользовательской функции, которая имеет целочисленный аргумент, и прототип другой функции, принимающей в качестве первого аргумента - указатель на функцию, в качестве второго аргумента - целое число:
    #include <stdio.h>
    
    int bounce(int a);
    int caller(int (*function)(int), int b);
    
    int main(void)
    {
       return 0;
    }
  2. Добавьте в функцию main(), в которой объявляется обычная целочисленная переменная, а также объявляется и инициализируется переменная-указатель на функцию:
    #include <stdio.h>
    
    int bounce(int a);
    int caller(int (*function)(int), int b);
    
    int main(void)
    {
       int num;
       int (*fpntr)(int) = bounce;
    
       return 0;
    }
  3. После блока функции main() определите первую пользовательскую функцию, которая выводит на экран полученное значение и возвращает целое число, и вторую - вызывающую обычную функцию из полученного указателя на функцию и передающую ей полученное целочисленное значение:
    #include <stdio.h>
    
    int bounce(int a);
    int caller(int (*function)(int), int b);
    
    int main(void)
    {
       int num;
       int (*fpntr)(int) = bounce;
    
       return 0;
    }
    
    int bounce(int a)
    {
        printf("\nReceive Value: %d\n", a);
        return ((3 * a) + 3);
    }
    int caller(int (*function)(int),  int b)
    {
       return (*function)(b);
    }
  4. В блоке функции main() присвойте значение целочисленной переменной с помощью вызова обычной функции через указатель на неё и выведите значение, которое она вернёт:
    #include <stdio.h>
    
    int bounce(int a);
    int caller(int (*function)(int), int b);
    
    int main(void)
    {
       int num;
       int (*fpntr)(int) = bounce;
    
       num = (*fpntr)(10);
       printf("Returned Value: %d\n", num);
    
       return 0;
    }
    
    int bounce(int a)
    {
        printf("\nReceive Value: %d\n", a);
        return ((3 * a) + 3);
    }
    int caller(int(*function)(int),  int b)
    {
       return (*function)(b);
    }
    Первым аргументом функции caller() в этом примере может быть указатель на любую функцию, которая получает один целочисленный аргумент и возвращает целочисленное значение, что указано в объявлении прототипа.
  5. Теперь присвойте новое значение целочисленной переменной, передав указатель на функцию и целое число другой функции, которая, в свою очередь, вызовет обычную функцию, а затем выведет значение, которое та вернёт:
    #include <stdio.h>
    
    int bounce(int a);
    int caller(int(*function)(int), int b);
    
    int main(void)
    {
       int num;
       int (*fpntr)(int) = bounce;
    
       num = (*fpntr)(10);
       printf("Returned Value: %d\n", num);
    
       num = caller(fpntr, 5);
       printf("Returned Value: %d", num);
    
       return 0;
    }
    
    int bounce(int a)
    {
        printf("\nReceive Value: %d\n", a);
        return ((3 * a) + 3);
    }
    int caller(int(*function)(int),  int b)
    {
       return (*function)(b);
    }
  6. Итоговый исходный код должен быть написан так:
    #include <stdio.h>
    
    int bounce(int a);
    int caller(int (*function)(int), int b);
    
    int main(void)
    {
       int num;
       int (*fpntr)(int) = bounce;
    
       num = (*fpntr)(10);
       printf("Returned Value: %d\n", num);
    
       num = caller(fpntr, 5);
       printf("Returned Value: %d\n", num);
    
       return 0;
    }
    
    int bounce(int a)
    {
       printf("\nReceived Value: %d\n", a);
       return ((3 * a) + 3);
    }
    
    int caller(int (*function)(int), int b)
    {
       return (*function)(b);
    }
  7. Выполните программу:
    Как и другие указатели в языке C, указатель на функцию просто хранит адрес в памяти. Когда указатель на функцию разыменовывается с помощью операции *, вызывается функция, расположенная по адресу, хранящемуся в указателе.

Указатель на Функцию как аргумент для другой Функции

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

  1. Напишем новую программу func_as_argf.c. Объявим три прототипа пользовательских функций:
    #include <stdio.h>
    
    void myFunc_1();
    void myFunc_2();
    void callFunc(void(*fp)());
    
    int main(void)
    {
       return 0;
    }
  2. В теле главной функции два раза вызовим функцию, которой в качестве аргумента передадим имена других функций. После главной функции напишем определение всех пользовательских функций:
    #include <stdio.h>
    
    int main(void)
    {
       callFunc(myFunc_1);
       callFunc(myFunc_2);
    
       return 0;
    }
    
    void myFunc_1()
    {
       printf("\nInside function #1\n");
    }
    void myFunc_2()
    {
       printf("Inside function #2\n\n");
    }
    void callFunc(void(*fp)())
    {
       fp();
    }
  3. Выполните программу:
    Функция вызывает другие функции, получив в качестве аргумента имя другой функции.
  1. Напишем новую программу func_pntr_as_arg.c, программа:
    • Создаёт указатель на функцию, которая принимает два int и возвращает int.
    • Передаёт этот указатель в другую функцию в качестве аргумента.
    • Эта функция возводит два числа в куб (³), передаёт их в функцию сложения через указатель.
    • Возвращает сумму кубов и выводит её на экран.
  2. Подключим заголовочные файлы стандартного ввода/вывода, математических функций, и заголовочный файл для явного приведения типов данных(в нашем случаи int)
    #include <stdio.h>
    #include <math.h>
    #include <stdlib.h>
                         
  3. Объявим две пользовательские функции для сложения кубов, и сложения:
    int qubeSum(int A, int B, int(*fp)(int, int));
    int mySum(int A, int B);
    
  4. Инициализируем две целочисленные переменные:
    int A = 5;
    int B = 6;
  5. Создадим Указатель на Функцию, который будет Указывать на другую Функцию:
    int result = qubeSum(A, B, fp);
    
  6. После главной функции определим пользовательскую функцию:
    int qubeSum(int A, int B, int(*fp)(int, int))
    {
        int sqSum = fp((int)pow(A, 3), (int)pow(B, 3));
        return sqSum;
    }
  7. После главной функции определим пользовательскую функцию:
    int mySum(int A, int B)
    {
        return A + B;
    }
  8. Напечатаем результат в консоль:
    printf("\nQubed sum of the Integers %d and %d is %d\n\n", A, B, result);
    
  9. Итоговый исходный код должен быть написан так:
    #include <stdio.h>
    #include <math.h>
    #include <stdlib.h>
    
    int qubeSum(int A, int B, int(*fp)(int, int));
    int mySum(int A, int B);
    
    int main(void)
    {
       int A = 5;
       int B = 6;
    
       int (*fp)(int, int);
       fp = mySum;
       int result = qubeSum(A, B, fp);
       printf("\nQubed sum of the Integers %d and %d is %d\n\n", A, B, result);
    
       return 0;
    }
    
    int qubeSum(int A, int B, int(*fp)(int, int))
    {
       int sqSum = fp((int)pow(A, 3), (int)pow(B, 3));
       return sqSum;
    }
    
    int mySum(int A, int B)
    {
       return A + B;
    }
  10. Выполните программу:

Функция возвращает Указатель

Вспомните, Указатель это тип данных, его размер - 8 байт.

Функция может возвращать Указатель как результирующее значение выполнения этой функции.

  1. Напишем простую программу func_ret_pntr.c, в которой Функция возвращает Указатель:
    #include <stdio.h>
    
    int A = 25; /* Объявляем глобальную переменную */
    
    int *myFunc(void); /* Объявляется функция myFunc, которая: Не принимает параметров (void).
       Возвращает указатель на int (int *) */
    
    int main(void)
    {
       int *pntr; /* Создаётся указатель на int с именем pntr */
       pntr = myFunc(); /* Вызывается функция myFunc(). 
          Она возвращает адрес переменной A.
          Этот адрес присваивается в pntr.
          После этого:
          pntr содержит адрес переменной A и
          *pntr будет значением переменной A, т.е. 25 */
       printf("\nPointed address is %p\n", (void *)pntr); /* Выводит адрес, на который указывает pntr в формате указателя */
       printf("Size of Pointed address is %zu bytes\n", sizeof(pntr)); /* Выводит размер указателя pntr в байтах. 
          На 64-битных системах это обычно 8 байт, на 32-битных — 4 байта */
       printf("Value at address is %d\n\n", *pntr); /* Разыменовывает указатель pntr (*pntr), 
          получая значение по адресу, который хранится в указателе. */
    
       return 0;
    }
    
    int *myFunc(void)
    {
       return (&A);
    }
    
  2. Выполните программу:

Уничтожение локальной переменной внутри функции

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

  1. Напишем новую программу local_var.c, объявим прототип пользовательской функцию, которая не принимает параметров и возвращает целочисленный указатель:
    #include <stdio.h>
    int *myFunc(void);
    
    int main(void)
    {
       return 0;
    }
    
    int *myFunc(void)
    {
       int Local = 19; /* Объявление и инициализация Локальной переменной внутри функции */
       printf("Pointed address is %p. When Inside of myFunc().\n", &Local);
       printf("Value at address is %d. When Inside of myFunc().\n", Local);
        
       return (&Local);
    }
  2. Объявим Указатель, инициализируем его адресом пользовательской функции, напечатаем адрес пользовательской функции и значение переменной расположенной внутри пользовательской функции:
    #include <stdio.h>
    int *myFunc(void);
    
    int main(void)
    {
       int *pntr;
       pntr = myFunc();
       printf("Pointed address is %p. When Outside of myFunc().\n", pntr); /* Предупреждение */
       printf("No Value at the wrong address is %d. When Outside of myFunc().\n", *pntr); /* Предупреждение */
    
       return 0;
    }
    
    int *myFunc(void)
    {
       int Local = 19; /* Объявление и инициализация Локальной переменной внутри функции. 
          Переменные, созданные внутри функции, существуют только до момента выхода из функции. */
       printf("Pointed address is %p. When Inside of myFunc().\n", &Local);
       printf("Value at address is %d. When Inside of myFunc().\n", Local);
        
       return (&Local); /* Локальные переменные хранятся в стеке, 
          и после завершения функции память под локальную переменную освобождается 
          или используется под другие нужды машины. */
    }
  3. Выполните программу:
    Часть кода не выполнилась - программа рушиться. Скомпилированный с ошибкой код не выполнился корректно.
  4. В момент возвращения адреса локальной переменной, после выхода из пользовательской функции, она уже уничтожена. Это приводит к «Dangling Pointer» (висячий указатель): Указатель pntr в main() содержит адрес, где ранее была Local, но там уже не гарантируется её корректное значение.
  5. Почему printf("No Value at the wrong address is %d. When Outside of myFunc().\n", *pntr); не выводится: Это следствие неопределённого поведения:
    • Компилятор не обязан корректно выполнять код, в котором есть доступ к освобождённой памяти.
    • Возможные варианты поведения:
      • Будет работать «нормально» на одном компиляторе и рушиться на другом. Если у тебя программа не выводит строку, значит она рушится в момент разыменования *pntr из-за доступа к освобождённой памяти.
      • Проигнорирует printf().
      • Программа аварийно завершится (Segmentation Fault).
      • Выведет мусорное значение.

Исправим ошибку в коде.

Вариант 1: Использовать static переменную внутри пользовательской функции так:

int *myFunc(void)
{
   static int Local = 19; /* Статическая локальная переменная */
   printf("Pointed address is %p. When Inside of myFunc().\n", (void*)&Local);
   printf("Value at address is %d. When Inside of myFunc().\n\n", Local);
    
   return &Local; /* Теперь это безопасно */
}

Почему это срабатывает:

Вариант 2: Передавать адрес переменной через аргумент функции:

int *myFunc(int *pntr);

int main(void)
{
   int Local;
   myFunc(&Local);
   printf("Pointed address is %p. When Outside of myFunc().\n", &Local);
   printf("Value at address is %d. When Outside of myFunc().\n\n", Local);

   return 0;
}

int *myFunc(int *pntr)
{
   *pntr = 19; /* Объявление и инициализация Указателя на значение */
   printf("\nPointed address is %p. When Inside of myFunc().\n", pntr);
   printf("Value at address is %d. When Inside of myFunc().\n\n", *pntr);    
    
}

Почему это срабатывает:

  1. Напишем новую программу pass_by_value_gcc.c, для объяснения передачи значений через их указатели. В этом примере, значения внутри функции могут быть понимаемыми как локальные переменные внутри функции. Возвращение адреса аргумента значения может создавать проблемы компиляции в некоторых компиляторах. Мы используем компилятор GCC 15.1.0 MinGW - проверим реализацию кода на нём:
    #include <stdio.h>
                            
    float *findLarger(float *, float *);
    
    int main(void)
    {
        float FLA = 45.54;
        float FLB = 67.76;
    
        float *larger_number = NULL; /* Правильная практика в программировании на С -
             инициализировать указатель нулями (NULL) */
        larger_number = findLarger(&FLA, &FLB);
        printf("\nLarger of %.2f and %.2f is %.2f\n\n", FLA, FLB, *larger_number); /* Разыменовываем указатель 
             возвращённый пользовательской функцией */
    
        return 0;
    }
    /* Здесь применяем Передачу по значению. Pfla и Pflb можно считать за локальные переменные */
    float *findLarger(float *Pfla, float *Pflb)
    {
        if (*Pfla < *Pflb) /* Сравниваем значения размещённые по адресам &FLA и &FLB */
        {
            return Pflb; /* Возвращаем адрес &FLB */
        }    
        else
        {
            return Pfla; /* Возвращаем адрес &FLA */
        }
    }
  2. Выполните программу:
    Компилятор GCC 15.1.0 MinGW - компилирует исходный код без предупреждений и ошибок. Программа печатает ожидаемый результат в консоль.

Заключение

Работа со строками

Здесь изучим принципы работы с текстовыми строками.

Чтение строк

В программировании на языке C строка является массивом символов, который содержит в последнём элементе специальный символ \0. Каждый символ, включая знаки пунктуации и непечатаемые символы, такие, как перевод каретки, имеют уникальное числовое значение кода ASCII. Это значит, что эти символы можно изменить арифметически. Например, если символ имеет значение char letter = 'S'; , то операция letter++; даст результат 'T'.

Символы с шагом 32
Код Символ Код Символ Код Символ
32 64 @ 96 `
33 ! 65 A 97 a
34 " 66 B 98 b
35 # 67 C 99 c
36 $ 68 D 100 d
37 % 69 E 101 e
38 & 70 F 102 f
39 ' 71 G 103 g
40 ( 72 H 104 h
41 ) 73 I 105 i
42 * 74 J 106 j
43 + 75 K 107 k
44 , 76 L 108 l
45 - 77 M 109 m
46 . 78 N 110 n
47 / 79 O 111 o
48 0 80 P 112 p
49 1 81 Q 113 q
50 2 82 R 114 r
51 3 83 S 115 s
52 4 84 T 116 t
53 5 85 U 117 u
54 6 86 V 118 v
55 7 87 W 119 w
56 8 88 X 120 x
57 9 89 Y 121 y
58 : 90 Z 122 z
59 ; 91 [ 123 {
60 < 92 \ 124 |
61 = 93 ] 125 }
62 > 94 ^ 126 ~
63 ? 95 _ 127 DEL
Управляющие текстом символы
Код Символ Описание Код Символ Описание
0 NUL null 16 DLE Выход из канала передачи данных
1 SOH Начало заголовка 17 DC1 Управление устройством 1
2 STX Начало текста 18 DC2 Контроль устройств 2
3 ETX Конец текста 19 DC3 Контроль устройств 3
4 EOT Конец передачи 20 DC4 Контроль устройств 4
5 ENQ Прошу подтверждения 21 NAK Не подтверждаю
6 ACK Подтверждаю 22 SYN Синхронизация
7 BEL Звонок, звуковой сигнал 23 ETB Конец блока текста
8 BS Возврат на один символ 24 CAN Отмена
9 TAB Горизонтальная табуляция 25 EM Конец носителя
10 NL Новая строка 26 SUB Подставить
11 VT Вертикальная табуляция 27 ESC Начало управляющей последовательности
12 FF Прогон страницы, новая страница 28 FS Разделитель файлов
13 CR Возврат каретки 29 GS Разделитель групп
14 SO Изменить цвет ленты 30 RS Разделитель записей
15 SI Обратно к предыдущему коду 31 US Разделитель юнитов

Значения кодов ASCII для символов нижнего регистра всегда имеют значение, превосходящее на 32 значения аналогичных символов верхнего регистра, из чего следует, что регистр любого символа может быть изменён путём добавления или вычитания числа 32. Например, если символ имеет значение char letter = 'M';, то операция letter += 32; даст результат 'm'.

Поскольку в языке C не существует специального типа данных для строки, переменные-строки должны создаваться как массив символов, в последнём элементе которого лежит символ-терминатор \0, что повышает статус данного массива до строки. Это означает, что имя массива действует как implied указатель на целую строку символов. Операция sizeof() вернёт длину строки, если передать ему в качестве аргумента имя строки. Строка может быть присвоена любому массиву или указателю как тип char, напишите и выполните программу:

char arr[6] = { 'A', 'l', 'p', 'h', 'a', '\0' } ;
char *ptr = "Beta" ; 
printf("\n%s\n", arr); /* Имя массива. Выведет слово "Alpha" */
printf("%s\n\n", ptr); /* Указатель. Выведет слово "Beta" */
Напоминаем: В языке программирования C символьные значения должны помещаться в одинарные кавычки ' ', а строки — в двойные " ".

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

Эту проблему можно решить с помощью двух альтернативных функций, также располагающихся в файле <stdio.h>. Первая из этих функций называется gets(). Она используется для чтения данных, введённых пользователем. Эта функция принимает все символы (включая пробелы), и присваивает строку массиву символов, указанному как аргумент. Она автоматически добавляет символ-терминатор \0 в конце строки, когда пользователь нажимает на клавишу Enter, чтобы гарантировать, что введённые данные будут иметь статус строки. Компаньоном этой функции является функция puts(), которая выведет строку, переданную в качестве аргумента, и автоматически в конце добавит символ перевода каретки.

  1. Напишем программу getsputs.c, для демонстрации работы ввода строк. В блоке функции main() запросите у пользователя данные, которые впоследствии будут помещены в массив:
    #include <stdio.h>
    
    int main(void)
    {
        char str[51];
    
        printf("\nEnter up to 50 characteres with Spaces:\n");
        gets(str);
    
        return 0;
    }
  2. Теперь в блоке функции main() выведите строку, сохраненную в массиве:
    #include <stdio.h>
    
    int main(void)
    {
        char str[51];
    
        printf("\nEnter up to 50 characteres with Spaces:\n");
        gets(str);
    
        printf("gets() read: ");
        puts(str);
    
        return 0;
    }
  3. Повторите процесс, чтобы увидеть ограничения функции scanf():
    #include <stdio.h>
    
    int main(void)
    {
        char str[51];
    
        printf("\nEnter up to 50 characteres with Spaces:\n");
        gets(str);
    
        printf("gets() read: ");
        puts(str);
    
        printf("\nEnter up to 50 characters with Spaces:\n");
        scanf("%s", str);
        printf("scanf() read: %s\n", str);
    
        return 0;
    }
  4. Сохраните, скомпилируйте и выполните программу:
    Во время компиляции, компилятор вывел в консоль Предупреждение:
    getsputs.c: In function 'main':
    getsputs.c:8:5: warning: call to 'gets' declared with attribute warning: 
    Using gets() is always unsafe - use fgets() instead [-Wattribute-warning]
        8 |     gets(str);
          |     ^~~~~~~~~
    Это говорит нам о том, что функция gets() потенциально небезопасная. Данная функция устарела в стандарте С11 (в 2011 году - согласно https://en.cppreference.com/w/c/io/fgets). И теперь всегда необходимо использовать функцию fgets() вместо gets(). Как мы знаем - 10 правил программирования от NASA, рекомендуют устранить все Предупреждения и Ошибки в коде, прежде чем использовать его в реальном мире...

Давайте устраним Предупреждение переписав код таким образом, чтобы применить безопасную функцию fgets(). Обращаю ваше внимание на тот факт, что в сфере программирования(на любом языке) постоянно происходят(выходят) обновления. Что-то устаревает, что-то новое внедряется, что-то принимается за стандарт, что-то из стандарта удаляется. В нашем случаи функция gets() устарела в С11, однако обратная совместимость программ с этой функцией может сохраняться довольного долго. Обратимся к статье (по ссылке выше), чтобы изучить функцию fgets() и переписать наш фрагмент кода согласно стандарту С23.

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

  1. Название функции и в какой библиотеке(заголовочном файле) она находится:
    • Название функции - fgets()
    • Заголовочный файл - <stdio.h>
  2. Прочитаем прототип функции:
    char* fgets(char* str, int count, FILE* stream); /* (До C99 (до 1999 года)) */
    char* fgets( char* restrict str, int count, FILE* restrict stream ); /* (С C99) (после 1999 года) */
    Так мы живём уже после 1999-го года, то нам нужен прототип с этой пометкой (После C99).
  3. Что делает функция (обычно 1-2 предложения описания) - Считывает не более count-1 символов из stream в str, включая \n, если он встречается. Т.о. она считывает не более count-1 символов, гарантируя, что не переполнит буфер.
  4. Аргументы функции (Parameters):
    • str: куда сохраняются данные - указатель на буфер.
    • count: максимальное количество символов (обычно sizeof(buf)).
    • stream: источник ввода (stdin для клавиатуры).
    • Т.о. При замене gets(buffer); используем fgets(buffer, sizeof(buffer), stdin);.
  5. Возвращаемое значение - Возвращает str при успехе, а NULL при ошибке или при получении символа EOF(End Of File). Необходимое проверочное утверждение пишется так:
    if (fgets(buffer, sizeof(buffer), stdin) == NULL) {
        // Код обработки ошибки
    }
    
  6. Пример - Смотрим, как функция используется в коде.
  7. Особенные ситуации и предупреждения:
    • Возвращает \n, если строка короткая.
    • Использовать проверку возврата для обработки ошибок.
  8. Так же в статье может быть получена новая информация относительно новых правил синтаксиса(из примеров например) и др. Статьи размещаемые на сайтах с высоким уровнём доверия, стоит читать периодически с целью обновления знаний по языку. Очевидно - знание и изучение английского языка всегда необходимо.
  9. Изучив статью, определяемся как перепишем исходный код.

Фрагмент кода, который нам нужно исправить был написан так:

char str[51];

   printf("\nEnter up to 50 characteres with Spaces:\n");
   gets(str);
Что конструктивно выглядит как:
char buffer[100];
gets(buffer);

Компилятор посчитал это небезопасным и вызвал предупреждение.

Согласно новой предпочитаемой конструкции кода по стандарту С23, как:

char buffer[100];
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
    // обработка buffer    
}

Перепишем наш фрагмент кода так:

printf("\nEnter up to 50 characteres with Spaces:\n");
char str[51];
if (fgets(str, sizeof(str), stdin) != NULL)
{
   printf("gets() read: ");
   puts(str);
}

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

printf("\nEnter up to 50 characteres with Spaces:\n");
char str[51];
if (fgets(str, sizeof(str), stdin) != NULL)
{
   printf("gets() read: ");
   puts(str);
} else
{
   printf("Some error description text...");
}
Перепишите программу, скомпилируйте и выполните её.
Кстати, Если вы не хотите, чтобы после вашей строки выводился символ перевода каретки, следует использовать функцию printf(), а не puts().

Размер строки - Длина строки

Стандартные библиотеки C включают в себя заголовочный файл с именем <string.h>, который содержит специальные функции работы со строками. Чтобы они были доступны, в программу необходимо подключить заголовочный файл <string.h> с помощью директивы #include, записав её в начале программы, так же как и для <stdio.h> .

Одна из функций файла <string.h> называется strlen(), она может быть использована для определения длины строки, которая ей передаётся как аргумент. Функция strlen() возвращает целое число, оно является количеством символов в строке, включая пробелы но исключая финальный символ-терминатор \0 конца строки.

Копирование строк

Заголовочный файл <string.h> предоставляет ещё две полезные функции, позволяющие копировать строки из одного массива в другой. Первая из них называется strcpy(), ей требуется передать два аргумента. Первый — это имя массива, в который требуется скопировать строку, Второй — имя массива, из которого будет выполняться копирование. Синтаксис этой функции выглядит так:

strcpy(dst, src);

Где, dst - строка приёмник, src - строка источник.

Все символы исходного массива будут скопированы в целевой, включая символ-терминатор \0. К элементам, расположенным после символа терминатора, также будет добавлен символ \0. Это гарантирует, что остатки более длинной строки окажутся удалены при копировании в массив более короткой строки.

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

strcpy(dst, src, size_t count);

Где size_t count - целое число - количество символов строки-источника начиная с первого символа.

Копируемая строка начнется с первого символа исходного массива, но закончится в позиции, указанной в третьем аргументе — финальный символ \0 не будет скопирован автоматически. После того, как символы будут скопированы, в конец целевого массива запишется символ \0, что поднимет его статус до строки. Поэтому необходимо убедиться, что целевой массив всегда будет на один элемент больше, чем количество символов, которое будет в него скопировано, включая символы пробелов, чтобы разместить символ-терминатор \0.

  1. Напишем программу strcpy.c и продемонстрируем работу выше описанных функций:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str_1[] = "Larger text string";
        printf("\n%s: %d Symbols in string", str_1, strlen(str_1)); /* Количество символов в строке */
        printf("\n%s: %d Elements in array\n", str_1, sizeof(str_1)); /* Количество элементов в строковом литерале - 
          в Массиве символов включая \0 */
    
        char str_2[] = "Smaller string";
        printf("\n%s: %d Symbols in string", str_2, strlen(str_2)); /* Количество символов в строке */
        printf("\n%s: %d Elements in array\n", str_2, sizeof(str_2)); /* Количество элементов в строковом литерале - 
          в Массиве символов включая \0 */
    
        return 0;
    }
  2. Теперь в блоке функции main() скопируйте всё содержимое второго массива в первый, напечатайте его содержимое и отобразите длину и размер:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str_1[] = "Larger text string";
        printf("\n%s: %d Symbols in string", str_1, strlen(str_1)); /* Количество символов в строке */
        printf("\n%s: %d Elements in array\n", str_1, sizeof(str_1)); /* Количество элементов в строковом литерале - 
          в Массиве символов включая \0 */
    
        char str_2[] = "Smaller string";
        printf("\n%s: %d Symbols in string", str_2, strlen(str_2)); /* Количество символов в строке */
        printf("\n%s: %d Elements in array\n", str_2, sizeof(str_2)); /* Количество элементов в строковом литерале - 
          в Массиве символов включая \0 */
       
        strcpy(str_1, str_2);
        printf("\n%s: %d Symbols in string", str_1, strlen(str_1));
        printf("\n%s: %d Elements in array\n", str_1, sizeof(str_1));
    
        return 0;
    }
  3. Скопируем в первый массив первые пять символов, содержащихся во втором массиве, далее поставьте символ-терминатор \0:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str_1[] = "Larger text string";
        printf("\n%s: %d Symbols in string", str_1, strlen(str_1)); /* Количество символов в строке */
        printf("\n%s: %d Elements in array\n", str_1, sizeof(str_1)); /* Количество элементов в строковом литерале - 
          в Массиве символов включая \0 */
    
        char str_2[] = "Smaller string";
        printf("\n%s: %d Symbols in string", str_2, strlen(str_2)); /* Количество символов в строке */
        printf("\n%s: %d Elements in array\n", str_2, sizeof(str_2)); /* Количество элементов в строковом литерале - 
          в Массиве символов включая \0 */
       
        strcpy(str_1, str_2);
        printf("\n%s: %d Symbols in string", str_1, strlen(str_1));
        printf("\n%s: %d Elements in array\n", str_1, sizeof(str_1));
    
        strncpy(str_1, str_2, 5); /* Копируем пять символов - С 1-го по 5-ый включительно */
        str_1[5] = '\0'; /* '\0' Вставляем в 6-ой элемент массива, т.е. сразу после 5-го символа */
        printf("\n%s: %d Symbols in string", str_1, strlen(str_1));
        printf("\n%s: %d Elements in array\n\n", str_1, sizeof(str_1));
    
        return 0;
    }

    Напоминаем, К пятому элементу итоговой строки str_1 можно обратиться с помощью конструкции str_1[5], поскольку нумерация индексов массива начинается с нуля, а не с единицы.

  4. Выполните программу:
    Обратите внимание на то, что размер первой строки на протяжении выполнения программы не поменялся.
  5. Чтобы увидеть все коды символов, находящиеся в первой строке, напишите цикл с помощью которого "пройдёте" по каждому элементу массива и напечатаете в консоль их значения и символы так:
    Обратите внимание на то, что размеры массивов можно не указывать если они сразу инициализируются. Для массивов, которые будут инициализированы позже, например, пользователем или при копировании из других массивов, размер необходимо указать заранее.

Объединение строк

Объединение двух строк в одну имеет более точное название — конкатенация.

Стандартный библиотечный заголовочный файл <string.h> содержит две функции, которые могут быть использованы для конкатенации строк. Чтобы эти функции были доступны, в программе необходимо добавить заголовочный файл <string.h> с помощью директивы #include, расположенной в начале программы.

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

char *strcat( char *restrict dest, const char *restrict src );

Где:

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

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

char *strncat( char *restrict dest, const char *restrict src, size_t count );

Где:

Присоединяемая часть первой dest строки по умолчанию начнется с первого символа второй src строки и закончится в позиции, указанной в третьем аргументе count. Но поскольку имя строки является указателем на первый символ, с помощью арифметики указателей можно указать другую позицию первого копируемого символа. Синтаксис будет выглядеть так:

char *strncat( char *restrict dest, const char *restrict + offset src, size_t count );

Опять же, как и в случае с функцией strcat(), важно, чтобы первый массив был достаточно большим, чтобы разместить все символы объединённой строки, что позволит избежать ошибок при использовании функции strncat().

Эти функции изменяют длину исходной строки, поскольку они добавляют символы.
  1. Напишем программу strcat.c
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str_1[100] = "A Place for Everything";
        char str_2[] = "and Everything in its Place";
        char str_3[100] = "The Truth is Rarely Pure ";
        char str_4[] = "and Neve Simple. - Oscar Wilde";
    
        return 0;
    }
  2. Далее в блоке функции main() присоедините вторую строку к первой и выведите результат в консоль:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str_1[100] = "A Place for Everything";
        char str_2[] = "and Everything in its Place";
        char str_3[100] = "The Truth is Rarely Pure ";
        char str_4[] = "and Neve Simple. - Oscar Wilde";
    
        strcat(str_1, str_2);
        printf("\n%s\n", str_1);
    
        return 0;
    }
  3. Присоедините первые 17 символов четвертой строки к третьей и выведите результат в консоль:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str_1[100] = "A Place for Everything";
        char str_2[] = "and Everything in its Place";
        char str_3[100] = "The Truth is Rarely Pure ";
        char str_4[] = "and Neve Simple. - Oscar Wilde";
    
        strcat(str_1, str_2);
        printf("\n%s\n", str_1);
    
        strncat(str_3, str_4, 17);
        printf("\n%s\n", str_3);
    
        return 0;
    }
  4. Присоедините последние 14 символов четвертой строки к третьей и выведите результат в консоль:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str_1[100] = "A Place for Everything";
        char str_2[] = "and Everything in its Place";
        char str_3[100] = "The Truth is Rarely Pure ";
        char str_4[] = "and Neve Simple. - Oscar Wilde";
    
        strcat(str_1, str_2);
        printf("\n%s\n", str_1);
    
        strncat(str_3, str_4, 17);
        printf("\n%s\n", str_3);
    
        strncat(str_3, (str_4 + 17), 14);
        printf("\n%s\n", str_3);
    
        return 0;
    }
  5. Выполните программу:
    Напоминаем, Удлиняемая строка должна иметь размер, достаточный для того, чтобы разместить все символы объединённой строки.

Поиск подстрок

Существует возможность выполнить поиск по строке, чтобы определить, содержит ли она определенную последовательность символов ( подстроку ). Это допустимо сделать с помощью функции strstr(). Она является частью стандартного заголовочного файла <string.h>, который необходимо добавить с помощью директивы #include, расположенной в начале программы.

Функция strstr() принимает два аргумента. Первый из них представляет собой строку, в которой выполняется поиск, а второй — подстроку, которую следует найти. Если подстрока не найдена, функция вернёт значение NULL. Если подстрока найдена, функция вернёт указатель на первый символ первого включения подстроки.

Внимание! Функция strstr() прекращает поиск, когда встречает первое включение подстроки. Последующие включения этой подстроки не будут найдены.

Номер элемента, содержащего первый символ подстроки, легко определить с помощью арифметики указателей. Вычтя адрес первого символа подстроки, возвращенного функцией strstr(), из адреса строки, в которой выполнялся поиск (на который указывает имя массива), можно получить целое число. Это число является индексом первого символа подстроки внутри строки, в которой выполнялся поиск.

В программировании на языке C операции сравнения == и != могут быть использованы для того, чтобы сравнить результат со значением NULL, но их нельзя использовать для сравнения самих строк. В стандартном библиотечном заголовочном файле <string.h> существует функция strcmp(), которая позволяет выполнять сравнение строк. Она принимает два аргумента, которые являются строками, подлежащими сравнению.

Сравнение выполняется на основе значений числовых кодов ASCII каждого символа и их позиции. Если строки абсолютно одинаковы, включая регистр, функция strcmp() вернёт значение 0. В противном случае она вернёт положительное или отрицательное значение в зависимости от значений строк.

  1. Напишем программу strstr.c
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str[]="no Time like Present ";
        char sub_str[]="Time";
    
        return 0;
    }
  2. Выведите сообщение в консоль в случае, если вторая строка не будет найдена в первой:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str[]="no Time like Present ";
        char sub_str[]="Time";
    
       if (strstr(str, sub_str) == NULL) /* NULL - Если подстрока не найдена */
       {
          printf("Substring \"Time\" Not Found\n");
       }  
    
        return 0;
    }
  3. Добавьте утверждения, позволяющие вывести адрес в памяти и индекс первого символа подстроки:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str[]="no Time like Present ";
        char sub_str[]="Time";
    
       if (strstr(str, sub_str) == NULL)
       {
          printf("Substring \"Time\" Not Found\n");
       }
       else /* 0 - Если подстрока найдена */
       {
          printf("Substring \"Time\" Found at %p\n", strstr(str, sub_str));
          printf("Element Index is %d\n\n", strstr(str, sub_str) - str);
    
             /* Благодаря организации адресации памяти на основе принципа Little Endian - 
             значение адреса первого символа строки str наименьший, а последнего её символа наибольший.
             Первое вхождение найденного символа имеет указатель большего значения, 
             и при вычитании из большего значения адреса меньшего значения адреса
              - получаем индекс первого символа подстроки sub_str в строке str */
       }
    
        return 0;
    }
    Обратите внимание на то, что символ \ в этом примере используется как экранирующий символ для двойных кавычек " ", что позволяет избежать преждевременного прерывания выводимых строк.
  4. Напечатаем результат трёх операций сравнения, проведённых со второй строкой:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str[]="no Time like Present ";
        char sub_str[]="Time";
    
       if (strstr(str, sub_str) == NULL)
       {
          printf("Substring \"Time\" Not Found\n");
       }
       else
       {
          printf("Substring \"Time\" Found at %p\n", strstr(str, sub_str));
          printf("Element Index is %d\n\n", strstr(str, sub_str) - str);
       }
    
       printf("%s Versus \"Time\": %d\n", sub_str, strcmp(sub_str, "Time"));
       printf("%s Versus \"time\": %d\n", sub_str, strcmp(sub_str, "time"));
       printf("%s Versus \"TIME\": %d\n", sub_str, strcmp(sub_str, "TIME"));
    
       return 0;
    }
  5. Выполните программу:

Поиск символа в строке

В заголовочном файле <string.h> имеются также две функции, позволяющие выполнять поиск символов. Функция strchr() ищет первое вхождение символа, а функция strrchr() — последнее. Обе возвращают значение NULL в случае, если символ не найден.

Обратите внимание - с помощью стандартных заголовочных файлов, используемые функции заменяют вам написание циклов работы со строками. Такие функции максимально оптимизированные для выполнения конкретной задачи и выполняются процессором очень быстро. По этому - изучайте функции стандартных заголовочных файлов с целью написания эффективного кода.
  1. Напишем программу str_ch.c
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
       char str[] = "no Time like Present !";
       char str_ch = 'T';
       char str_rch = 'e';
    
       /* Ищем первое вхождение символа 'T' */
       if (strchr(str, str_ch) == NULL)
       {
          printf("Character \'T\' Not Found\n");
       }
       else
       {
          printf("Character \'T\' Found\n at %p\n", strchr(str, str_ch));
          printf("Element Index is %d\n\n", strchr(str, str_ch) - str);
       }
       
       return 0;
    }
  2. #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
       char str[] = "no Time like Present !";
       char str_ch = 'T';
       char str_rch = 'e';
    
       if (strchr(str, str_ch) == NULL)
       {
          printf("Character \'T\' Not Found\n");
       }
       else
       {
          printf("Character \'T\' Found\n at %p\n", strchr(str, str_ch));
          printf("Element Index is %d\n\n", strchr(str, str_ch) - str);
       }
    
       /* Ищем последнее вхождение символа 'e' */
       if (strrchr(str, str_rch) == NULL)
       {
          printf("Character \'e\' Not Found\n");
       }
       else
       {
          printf("Character \'e\' Found\n at %p\n", strrchr(str, str_rch));
          printf("Element Index is %d\n\n", strrchr(str, str_rch) - str);
       }
       
       return 0;
    }
  3. Выполните программу:

Валидация строк

В стандартном библиотечном заголовочном файле <ctype.h> содержится несколько функций, позволяющих выполнять проверки символов. Чтобы эти функции были доступны, в программе необходимо добавить заголовочный файл <ctype.h> с помощью директивы #include, расположенной в начале программы.

В заголовочном файле <ctype.h> содержатся функции, которые проверяют, является ли символ:

В заголовочном файле <ctype.h> содержатся функции, позволяющие перевести символ:

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

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

  1. Напишем программу isval.c в которой продемонстрируем функции валидации строк:
    #include <stdio.h>
    #include <string.h>
    #include <ctype.h>
    
    int main(void)
    {
        char str[7]; /* Объявим массив символов, зарезервируем память */
        int i; /* Объявим целое число */
        int flag = 1; /* Объявим и инициализируем флаг единицей - true */
    
        return 0;
    }
  2. Запросите у пользователя ввести строку, затем присвойте её переменной-массиву:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char str[7]; /* Объявим массив символов, зарезервируем память */
        int i; /* Объявим целое число */
        int flag = 1; /* Объявим и инициализируем флаг единицей - true */
    
       puts("Enter six digits without any Spaces...");
       fgets(str, sizeof(str), stdin);
       if(fgets(str, sizeof(str), stdin) != NULL)
       {
            
       }
    
        return 0;
    }
  3. Теперь, если условие выполняется, добавьте цикл, позволяющий проверить каждый символ массива:
    #include <stdio.h>
    #include <string.h>
    #include <ctype.h>
    
    int main(void)
    {
        char str[7];
        int i;
        int flag = 1;
    
       puts("Enter six digits without any Spaces...");
       fgets(str, sizeof(str), stdin);
       if(fgets(str, sizeof(str), stdin) != NULL)
       {
          for (i = 0; i < sizeof(str) - 1; i++)
          {
                
          }
       }
    
        return 0;
    }
  4. Внутри блока цикла for измените значение переменной-флага на false (0), если какой-либо символ не является цифрой:
    #include <stdio.h>
    #include <string.h>
    #include <ctype.h>
    
    int main(void)
    {
        char str[7];
        int i;
        int flag = 1;
    
       puts("Enter six digits without any Spaces...");
       fgets(str, sizeof(str), stdin);
       if(fgets(str, sizeof(str), stdin) != NULL)
       {
          for (i = 0; i < sizeof(str) - 1; i++)
          {
             if (!isdigit(str[i])) /* Тоже самое что и isdigit() == 0 */
             {
                flag = 0;
             } 
          }
       }
    
        return 0;
    }
  5. В блоке if(!isdigit(str[i])) проверьте свойства символа, не являющийся цифрой и напечатайте сообщение об этом в консоль:
    #include <stdio.h>
    #include <string.h>
    #include <ctype.h>
    
    int main(void)
    {
        char str[7];
        int i;
        int flag = 1;
    
       puts("Enter six digits without any Spaces...");
       fgets(str, sizeof(str), stdin);
       if(fgets(str, sizeof(str), stdin) != NULL)
       {
          for (i = 0; i < sizeof(str) - 1; i++)
          {
             if (!isdigit(str[i]))
             {
                flag = 0;
                if (isalpha(str[i]))
                {
                   printf("Letter %s Found\n", toupper(str[i]));
                }
                else if (ispunct(str[i]))
                {
                   printf("Punctuation %c is Found\n", str[i]);
                }
                else if (isspace(str[i]))
                {
                   printf("Space is Found\n");
                }
             } 
          }
       }
    
        return 0;
    }
  6. После блока цикла for выведите сообщение, описывающее состояние переменной-флага с помощью тернарного условия:
    #include <stdio.h>
    #include <string.h>
    #include <ctype.h>
    
    int main(void)
    {
        char str[7];
        int i;
        int flag = 1;
    
        puts("Enter six digits without any Spaces...");
        if (fgets(str, sizeof(str), stdin) != NULL)
        {
            for (i = 0; i < sizeof(str) - 1; i++)
            {
                if (!isdigit(str[i]))
                {
                    flag = 0;
                    if (isalpha(str[i]))
                    {
                        printf("Letter %c Found\n", toupper(str[i]));
                    }
                    else if (ispunct(str[i]))
                    {
                        printf("Punctuation %c is Found\n", str[i]);
                    }
                    else if (isspace(str[i]))
                    {
                        printf("Space is Found\n");
                    }
                }
            }
            (flag) ? puts("Ok. Entry valid.") : puts("Error. Invalid Entry! Not a Number found in string!");
        }
    
        return 0;
    }
  7. Выполните программу:
    Почему в проверочном утверждении в цикле вычтена единица из возвращённого значения размера строки?

Полный набор функций заголовочного файла <ctype.h> и их описание здесь https://en.cppreference.com/w/c/header/ctype.html

Преобразование строк

Стандартный библиотечный файл <stdlib.h> содержит полезную функцию, которая называется atoi(). Она может быть использована для преобразования строки в число (althabet to integer, алфавит-к-целому-числу). Чтобы эта функция была доступна, в программе необходимо добавить заголовочный файл <stdlib.h> с помощью директивы #include, расположенной в начале программы.

Функция atoi() принимает в качестве своего единственного аргумента строку, которую нужно преобразовать. Если строка пустая или её первый символ не является числом или знаком «минус», функция atoi() вернёт значение 0. В противном случае строка (если в начале есть цифры) будет преобразовываться к числу, пока функция atoi() не встретит в строке не являющийся числом символ. Если функция atoi() встретит символ, не являющийся числом, она вернёт уже преобразованные в число цифры.

Также существует функция itoa() (integer-to-alpha, целое-число-к-алфавиту), которая используется для преобразования значения, имеющего тип int, к строке. Эта функция широко используется в различных компиляторах, однако она не является частью стандартной спецификации ANSI C.

Функция itoa() принимает три аргумента. Первый — преобразовываемое число, Второй — строка, которой будет присвоен сконвертированный результат, Третий — основание, которое будет использовано при преобразовании. Например, если указать основание 2, строке присвоится двоичный эквивалент указанного числа.

В стандарте ANSI C существует аналог функции itoa(), который называется sprintf() и располагается в заголовочном файле <stdio.h>. Эта функция проще, поскольку нельзя указать основание для преобразования. Функция sprintf() принимает три аргумента, Первый — строка, которой будет присвоено число, Второй - спецификатор формата и Третий - число, которое необходимо преобразовать. Эта функция возвращает число, которое является количеством символов в преобразованной строке, и возвращает строку преобразованную в формат печати сохраняя результат в первый аргумент(изменяет его).

  1. Напишем программу convers.c, для демонстрации выше описанных функций:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int num_1;
        int num_2;
        int num_3;
        char str_1[10] = "12eight";
        char str_2[10] = "-95.36";
        char str_3[10] = "X64";
    
        return 0;
    }
  2. Далее, преобразуем каждую строку к целому числу и напечатаем результат в консоль:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int num_1;
        int num_2;
        int num_3;
        char str_1[10] = "12eight";
        char str_2[10] = "-95.36";
        char str_3[10] = "X64";
    
        num_1 = atoi(str_1);
        printf("\nString %s converted to Integer: %d\n", str_1, num_1);
        num_2 = atoi(str_2);
        printf("\nString %s converted to Integer: %d\n", str_2, num_2);
        num_3 = atoi(str_3);
        printf("\nString %s converted to Integer: %d\n", str_3, num_3);
    
        return 0;
    }
  3. Далее в блоке функции main() преобразуйте первую числовую переменную к строке, переведя её в двоичную систему счисления с использованием нестандартной функции, а затем выведите результат в консоль:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int num_1;
        int num_2;
        int num_3;
        char str_1[10] = "12eight";
        char str_2[10] = "-95.36";
        char str_3[10] = "X64";
    
        num_1 = atoi(str_1);
        printf("\nString %s converted to Integer: %d\n", str_1, num_1);
        num_2 = atoi(str_2);
        printf("\nString %s converted to Integer: %d\n", str_2, num_2);
        num_3 = atoi(str_3);
        printf("\nString %s converted to Integer: %d\n", str_3, num_3);
    
        itoa(num_1, str_1, 2);
        printf("Decimal %d is Binary: %s\n", num_1, str_1);
    
        return 0;
    }
  4. Далее преобразуйте первую числовую переменную к строке, переведя её в восьмеричную и шестнадцатеричную форматы печати с использованием стандартной библиотечной функции, и сохраните длину каждой строки:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int num_1;
        int num_2;
        int num_3;
        char str_1[10] = "12eight";
        char str_2[10] = "-95.36";
        char str_3[10] = "X64";
    
        num_1 = atoi(str_1);
        printf("\nString %s converted to Integer: %d\n", str_1, num_1);
        num_2 = atoi(str_2);
        printf("\nString %s converted to Integer: %d\n", str_2, num_2);
        num_3 = atoi(str_3);
        printf("\nString %s converted to Integer: %d\n", str_3, num_3);
    
        itoa(num_1, str_1, 2);
        printf("Decimal %d is Binary: %s\n", num_1, str_1);
    
        itoa(num_1, str_1, 2);
        printf("Decimal %d is Binary: %s\n", num_1, str_1);
        num_2 = sprintf(str_3, "%o", num_1);
        printf("Decimal %d is Octal: %s cahrs: %d\n", num_1, str_3, num_2);
        num_3 = sprintf(str_3, "%x", num_1);
        printf("Decimal %d is Hexadecimal: %s cahrs: %d\n", num_1, str_3, num_3);
    
        return 0;
    }
  5. Выполните программу:
    В отличие от функции itoa() функция sprintf() не может преобразовывать числа в двоичную систему счисления, поскольку не существует спецификатора формата для двоичных чисел.
Функция itoa() очень полезна, но, поскольку она не является частью стандарта ANSI, её поддерживают не все компиляторы. Компилятор GNU GCC 15.1.0, используемый в этом учебнике, эту функцию поддерживает.

Заключение

Создание Структур

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

Группирование данных в Структуру

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

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

Переменные-члены структуры не могут быть инициализированы внутри объявления структуры.

В языке программирования C структура объявляется с помощью ключевого слова struct, после которого следует имя структуры и фигурные скобки, содержащие переменные-члены структуры. Наконец объявление структуры должно завершаться точкой с запятой после закрывающей скобки. Например: Структура, содержащая члены, описывающие координаты x и y точки, расположенной на графе, будет выглядеть так:

struct coords /* Объявлен новый тип данных coords */
{
   int x; /* Член структуры */
   int y; /* Член структуры */
}; /* Обязательная точка с запятой в конце Объявления структуры */

Опционально разрешается добавить теги перед последней точкой с запятой в формате списка, разделённого запятыми. Объявление структуры определяет новый тип данных, а эти имена ведут себя как переменные этого типа данных. Например, переменная с именем point типа данных coords может быть объявлена следующим образом:

struct coords /* Объявлен новый тип данных: coords */
{
   int x; /* Член структуры */
   int y; /* Член структуры */
} point; /* Объявлена переменная point, её тип данных coords */
Обычно в программах, написанных на языке C, объявление структур располагается перед функцией main().

К каждому члену структуры можно обратиться, добавив операцию .(точка) и имя члена и теги, например, point.x .

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

struct coords top;
  1. Начните новую программу struct.c с инструкции препроцессора, позволяющей включить функции стандартной библиотеки ввода/вывода, до главной функции объявим структуру данных, два члена в ней и один тег:
    #include <stdio.h>
    
    struct coords /* Объявили структуру данных */
    {
        int x; /* Член структуры */
        int y; /* Член структуры */
    } point; /* Тег - Экземпляр структуры с именем */
    
    int main(void)
    {
        return 0;
    }
  2. Далее создайте ещё один экземпляр структуры:
    #include <stdio.h>
    
    struct coords
    {
        int x;
        int y;
    } point;
    
    struct coords top; /* Тег - Экземпляр структуры с именем */
    
    int main(void)
    {
        return 0;
    }
  3. В главной функции, инициализируем оба члена каждой структуры:
    #include <stdio.h>
    
    struct coords
    {
        int x;
        int y;
    } point;
    
    struct coords top;
    
    int main(void)
    {
        point.x = 5; /* Доступ к члену структуры через точку */
        point.y = 8; /* Доступ к члену структуры через точку */
        top.x = 15; /* Доступ к члену структуры через точку */
        top.y = 24; /* Доступ к члену структуры через точку */
    
        return 0;
    }
  4. Теперь напечатайте значения, хранящиеся в каждом члене структуры в консоль:
    #include <stdio.h>
    
    struct coords 
    {
        int x;
        int y;
    } point;
    
    struct coords top;
    
    int main(void)
    {
        point.x = 5;
        point.y = 8;
        top.x = 15;
        top.y = 24;
        printf("\npoint.x: %d\npoint.y: %d\n", point.x, point.y); /* Доступ к члену структуры через точку */
        printf("\ntop.x: %d\ntop.y: %d\n\n", top.x, top.y); /* Доступ к члену структуры через точку */
    
        return 0;
    }
  5. Выполним программу:
    Мы объявили структуру и инициализировали два её экземпляра. Доступ к членам структуры получили через оператор (.) - точка. Создав структуру - мы объединили несколько переменных в группу, что стало представлять собой единый тип данных.
    Можно представить, что мы создали экземпляры структур описывающих некие точки point и top для каких либо геометрических расчётов или чего-то ещё:
    Точки на координатной сетке.

Разные типы данных в одной Структуре

#include <stdio.h>

int main(void)
{
    struct myStruct
    {
        int x; /* Один тип */
        double y; /* Другой тип */
    };

    return 0;
}
На блок-схеме Структура обозначается фигурой Структура

Определяем объект Структуры в программе, как переменную s

#include <stdio.h>

int main(void)
{
    struct myStruct
    {
        int x;
        double y;
    };
    struct myStruct s; /* ‘struct myStruct’ Представляет новый тип данных,
      ‘s’ - имя переменной структуры ‘struct myStruct’ нового типа данных - это Объект Структуры */

    return 0;
}
Так представляется объект Структуры в блок-схеме. В нашем случаи объект с именем s

Визуальное представление Инициализации членов Структуры

Мы уже знаем, что получение доступа к члену структуры записывается с помощью оператора точка(.):

Представим это визуально так:

В строке:
struct structureName UVW = {2, 180.0, 360.0};
Список значений членов структуры располагаются один за другим в фигурных скобках, это равнозначно записи:
{UVW.x = 2, UVW.y = 180.0, UVW.z = 360.0}
Просто компактная запись.

Инициализация членов структуры

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

Синтаксис такого вида:

struct structureName str = { .varName1 = value1, .varName2 = value2, .varNameN = valueN };

Пример кода:

#include <stdio.h>

int main(void)
{
    struct myStruct
    {
        int X;
        double Y;
    };
    
    struct myStruct S = {.X = 8, .Y = 1.75};
    
    printf("S.X = %d\n", S.X);
    printf("S.Y = %.2lf\n", S.Y);
    
    return 0;
}

Объявление сразу нескольких объектов структуры

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

#include <stdio.h>

int main(void)
{
    struct myStruct
    {
       int X;
       double Y;      
    }
   ABC = {1, 5.6},
   XYZ = { .X = 2, .Y = 7.89},
   UVW = {3, 4.75};
   
   printf("ABC.X is %d\nABC.Y is %.2lf\n\n", ABC.X, ABC.Y);
   printf("XYZ.X is %d\nXYZ.Y is %.2lf\n\n", XYZ.X, XYZ.Y);
   printf("UVW.X is %d\nUVW.Y is %.2lf\n\n", UVW.X, UVW.Y);
    
    return 0;
}

Массивы и символьные литералы в Структуре

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

#include <stdio.h>

int main(void)
{
    struct myStruct
    {
      int X;
      double Y;
      char Chrs[20]; /* Член структуры - Массив, ограниченный размером в двадцать символов */
    };
    struct myStruct S = {1, 2.34, "Some_text"};
    
    printf("S.X = %d\nS.Y = %.2lf\nS.Chrs = %s", S.X, S.Y, S.Chrs);
    
    return 0;
}

Определение типа данных с помощью Структуры

Тип данных, определяемый структурой, может быть объявлен так же, как и обычное определение типа данных с помощью ключевого слова typedef, размещённого в самом начале объявления структуры. Это определяет её как прототип, с помощью которого можно объявить другие структуры без ключевого слова struct — понадобится только тег.

Использование ключевого слова typedef часто может помочь упростить код, поскольку не придётся использовать ключевое слово struct для объявления переменных, имеющих тип данных структуры. Однако стоит начинать тег с большой буквы, чтобы можно было легко понять в тексте, что оно представляет собой тип данных, определяемый структурой.

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

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

<внешняя-структура>.<внутренняя-структура>.<член>

Определим новый тип данных с помощью ключевого слова typedef, синтаксис такой:

typedef struct [userStruct] userStType;

Где:

Теперь, зная как записать определение типа данных с помощью Структуры используя Ключевое слово typedef, напишем программу typedef.c, и продемонстрируем все выше описанные принципы:

  1. Объявим структуру, её члены и Имя пользовательской структуры:
    #include <stdio.h>
    
    typedef struct /* Объявили безымянную структуру */
    {
        int x; /* Объявили член структуры */
        int y; /* Объявили член структуры */
    } Point; /* Объявили Тег - Имя типа данных пользовательской структуры */
    
    int main(void)
    {
        return 0;
    }
    Обратите внимание на то, что структура может быть безымянной, но она обязательно должна иметь Тег - Имя типа данных пользовательской структуры.
  2. Далее создайте две переменные определённого структурой типа данных, в одной из которых будут проинициализированные оба члена структуры:
    #include <stdio.h>
    
    typedef struct 
    {
        int x;
        int y;
    } Point;
    Point top = {15, 24}; /* Объявили и Инициализировали оба члена переменной */
    Point bottom; /* Объявили переменную */
    
    int main(void)
    {
        return 0;
    }
  3. В главной функции инициализируем оба члена второй переменной bottom и напечатаем значения обоих переменных, представляющих координаты точек:
    #include <stdio.h>
    
    typedef struct 
    {
        int x;
        int y;
    } Point;
    Point top = {15, 24};
    Point bottom;
    
    int main(void)
    {
        bottom.x = 5; /* Инициализировали переменную */
        bottom.y = 8; /* Инициализировали переменную */
        printf("\nTop point X: %d, point Y: %d", top.x, top.y);
        printf("\nBottom point X: %d, point Y: %d", bottom.x, bottom.y);
    
        return 0;
    }
  4. Перед блоком функции main() добавьте вторую структуру, определяющую тип данных и имеющую в качестве членов два объекта первой структуры. Объявите переменную второго типа, определённого структурой, и инициализируйте все его переменные:
    #include <stdio.h>
    
    typedef struct 
    {
        int x;
        int y;
    } Point;
    Point top = {15, 24};
    Point bottom;
    
    typedef struct /* Объявили безымянную структуру */
    {
        Point a; /* Объявили член структуры */
        Point b; /* Объявили член структуры */
    } Box; /* Объявили Тег - Имя типа данных пользовательской структуры */
    Box rectangle = {6, 12, 30, 20}; /* Объявили и Инициализировали переменную типа Box */
    
    int main(void)
    {
        bottom.x = 5;
        bottom.y = 8;
        printf("\nTop point X: %d, point Y: %d", top.x, top.y);
        printf("\nBottom point X: %d, point Y: %d", bottom.x, bottom.y);
    
        return 0;
    }

    В Строке кода:

    Box rectangle = {6, 12, 30, 20};

    Каждый элемент списка {6, 12, 30, 20} соответствует членам структуры переменной top и bottom

  5. Вернитесь в блок функции main() и напечатайте в консоль координаты всех точек, содержащихся во вложенных членах структуры:
    #include <stdio.h>
    
    typedef struct 
    {
        int x;
        int y;
    } Point;
    Point top = {15, 24};
    Point bottom;
    
    typedef struct
    {
        Point a;
        Point b;
    } Box;
    Box rectangle = {6, 12, 30, 20}; /* Объявили и Инициализировали переменную */
    
    int main(void)
    {
        bottom.x = 5;
        bottom.y = 8;
        printf("\nTop point X: %d, point Y: %d", top.x, top.y);
        printf("\nBottom point X: %d, point Y: %d\n", bottom.x, bottom.y);
    
        printf("\nPoint x of 'a': %d", rectangle.a.x);
        printf("\nPoint y of 'a': %d", rectangle.a.y);
        printf("\nPoint x of 'b': %d", rectangle.b.x);
        printf("\nPoint y of 'b': %d\n\n", rectangle.b.y);
    
        return 0;
    }
  6. Выполните программу:
Члены структур могут быть инициализированы с помощью списка, разделённого запятыми, только при объявлении переменных — далее их можно инициализировать только индивидуально.

Использование указателей в Структурах

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

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

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

L-значения являются объектами, а R-значения — данными.

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

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

  1. Напишем программу string_member.c, объявим безымянный пользовательский тип данных структуры, в которой объявим смвольный массив размером в пять символов. Дадим название типу данных(тег):
    #include <stdio.h>
    
    typedef struct
    {
       char str[5];
    } ArrayType;
    
    int main(void)
    {
       return 0;
    }
  2. Теперь объявим безымянный пользовательский тип данных структуры, определяющую тип данных, которая имеет один член — указатель на символ, и его тег:
    #include <stdio.h>
    
    typedef struct
    {
       char str[5];
    } ArrayType;
    
    typedef struct
    {
       char *str;
    } PntrType;
    
    int main(void)
    {
       return 0;
    }
  3. Далее объявите по одной переменной каждого из объявленных типов данных и инициализируйте их члены:
    #include <stdio.h>
    
    typedef struct
    {
       char str[5];
    } ArrayType;
    
    typedef struct
    {
       char *str;
    } PntrType;
    
    ArrayType array = {'B', 'a', 'd', ' ', '\0'};
    PntrType pntr = {"Good"};
    
    int main(void)
    {
       return 0;
    }
  4. Далее добавьте в функцию main(), печать значения массива символов, являющегося членом структуры:
    #include <stdio.h>
    
    typedef struct
    {
       char str[5];
    } ArrayType;
    
    typedef struct
    {
       char *str;
    } PntrType;
    
    ArrayType array = {'B', 'a', 'd', ' ', '\0'};
    PntrType pntr = {"Good"};
    
    int main(void)
    {
       printf("\nArray string is a %s", array.str);
    
       return 0;
    }
  5. Внутри блока функции main() скрупулёзно присвойте новые значения каждому элементу массива символов и выведите получившуюся новую строку в консоль:
    #include <stdio.h>
    
    typedef struct
    {
       char str[5];
    } ArrayType;
    
    typedef struct
    {
       char *str;
    } PntrType;
    
    ArrayType array = {'B', 'a', 'd', ' ', '\0'};
    PntrType pntr = {"Good"};
    
    int main(void)
    {
       printf("\nArray string is a %s", array.str);
       array.str[0] = 'I';
       array.str[1] = 'd';
       array.str[2] = 'e';
       array.str[3] = 'a';
       array.str[4] = '\0';
       printf("%s\n", array.str);
    
       return 0;
    }
    Элементы массива символов могут быть инициализированы во время объявления массива путём присвоения ему списка, разделённого запятыми.
    Обратите внимание на то, что члены разных структур могут иметь одинаковые имена, и это не вызывает конфликтов. Почему?
  6. Далее выведите строку, к которой обращается указатель-член структуры:
    #include <stdio.h>
    
    typedef struct
    {
       char str[5];
    } ArrayType;
    
    typedef struct
    {
       char *str;
    } PntrType;
    
    ArrayType array = {'B', 'a', 'd', ' ', '\0'};
    PntrType pntr = {"Good"};
    
    int main(void)
    {
       printf("\nArray string is a %s", array.str);
       array.str[0] = 'I';
       array.str[1] = 'd';
       array.str[2] = 'e';
       array.str[3] = 'a';
       array.str[4] = '\0';
       printf("%s\n", array.str);
    
       printf("\nPointer string is a %s", pntr.str);
    
       return 0;
    }
  7. Теперь присвойте новое значение символьному указателю, это утверждение легко написать, и выведите на экран новую строку:
    #include <stdio.h>
    
    typedef struct
    {
       char str[5];
    } ArrayType;
    
    typedef struct
    {
       char *str;
    } PntrType;
    
    ArrayType array = {'B', 'a', 'd', ' ', '\0'};
    PntrType pntr = {"Good"};
    
    int main(void)
    {
       printf("\nArray string is a %s", array.str);
       array.str[0] = 'I';
       array.str[1] = 'd';
       array.str[2] = 'e';
       array.str[3] = 'a';
       array.str[4] = '\0';
       printf("%s\n", array.str);
    
       printf("\nPointer string is a %s", pntr.str);
       pntr.str = "Idea";
       printf(" %s\n\n", pntr.str);
    
       return 0;
    }
  8. Выполните программу и изучите её по-шагово:

Определение пользовательского типа данных с помощью typedef и задания Имени этому типу

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

typedef struct [userStruct] userStType;

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

  1. Напишем программу typedef_name.c, внутри главной функции объявим прототип структуры, и сразу же за ним напишем её определение:
    #include <stdio.h>
    
    int main(void)
    {
        typedef struct myStruct mySt; /* Объявляем прототип структуры, описывая пользовательский тип данных */
        struct myStruct /* Определяем пользовательский тип данных */
        {
            int X; /* Объявляем член структуры */
            double Y; /* Объявляем член структуры */
        };
        mySt S;  /* Объявляем переменную с именем S пользовательского типа данных с именем mySt */
        
        return 0;
    }
    
  2. Далее в программе мы можем создавать переменные типа mySt, также легко как мы привыкли создавать переменные стандартных типов данных языка С.
  3. Использовав ключевое слово typedef - мы сокращаем количество текста для создания переменных пользовательского типа.
  4. Без ключевого слова typedef - мы вынуждены либо объявить все используемые переменные сразу при Определении структуры, либо постоянно писать одну и ту же структуру и в ней снова объявлять переменную.
  5. Создание типов данных с помощью ключевого слова typedef широко распространено в исходном коде на языке С.

Инициализация переменных пользовательского типа данных определяемых Структурой

  1. Напишем программу typedef_var_init.c на основе программы выше инициализируем переменную пользовательского типа:
    #include <stdio.h>
    
    int main(void)
    {
       typedef struct myStruct mySt; /* Объявляем прототип структуры, описывая пользовательский тип данных */
       struct myStruct /* Определяем пользовательский тип данных */
       {
          int X; /* Объявляем член структуры */
          double Y; /* Объявляем член структуры */
       };
       mySt S = {4, 7.898}; /* Объявляем переменную с именем S
          пользовательского типа данных mySt и 
          инициализируем её значениями */
       printf("S.X is %d\nS.Y is %.2lf\n", S.X, S.Y);
    
       return 0;
    }
  2. Выполните программу:

Указатели на Структуры

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

Использование указателей на структуры настолько типично в программах, написанных на языке C, что для этого была введена специальная операция. При работе с указателями на структуру операция (.)(точка) может быть заменена операцией ->, представляющим собой дефис, за которым следует знак «больше». Эта комбинация называется «стрелкой». Например, указатель на член структуры pntr->member является эквивалентом записи (*pntr).member

  1. Напишем программу struct_pntr.c. Определим безымянную структуру пользовательского типа данных, и объявим два члена структуры:
    #include <stdio.h>
    
    typedef struct
    {
       char *name;
       char *popn;
    } City;
    
    int main(void)
    {
       return 0;
    }
    
  2. Теперь добавьте в функцию main(), объявления трёх переменных и переменную-указатель. Все они имеют вновь созданный пользовательский тип данных, определённый структурой, т.е. тип данных - City:
    #include <stdio.h>
    
    typedef struct
    {
       char *name;
       char *popn;
    } City;
    
    int main(void)
    {
       City ny;
       City la;
       City ch;
       City *pntr;
    
       return 0;
    }
    
  3. Далее, внутри блока функции main() скрупулёзно присвойте строковые значения всем членам первой переменной(ny), затем получите доступ к хранящимся в ней значениям с помощью операции (.)(точка), чтобы сначала сохранить, а затем получить значения:
    #include <stdio.h>
    
    typedef struct
    {
       char *name;
       char *popn;
    } City;
    
    int main(void)
    {
       City ny;
       City la;
       City ch;
       City *pntr;
    
       ny.name = "New York City";
       ny.popn = "9,150,000";
       printf("\n%s , Population: %s\n", ny.name, ny.popn);
    
       return 0;
    }
  4. Присвойте адрес второй переменной (la) указателю:
    #include <stdio.h>
    
    typedef struct
    {
       char *name;
       char *popn;
    } City;
    
    int main(void)
    {
       City ny;
       City la;
       City ch;
       City *pntr;
    
       ny.name = "New York City";
       ny.popn = "9,150,000";
       printf("\n%s , Population: %s\n", ny.name, ny.popn);
    
       pntr = &la;
    
       return 0;
    }
  5. Теперь присвойте значения членам второй переменной, а затем напечатайте в консоль сохранённые значения с помощью операции ->(стрелка), позволяющей сохранить значения, и операции ->(стрелка), позволяющей получить эти значения:
    #include <stdio.h>
    
    typedef struct
    {
       char *name;
       char *popn;
    } City;
    
    int main(void)
    {
       City ny;
       City la;
       City ch;
       City *pntr;
    
       ny.name = "New York City";
       ny.popn = "9,150,000";
       printf("\n%s , Population: %s\n", ny.name, ny.popn);
    
       pntr = &la;
       pntr->name = "Los Angeles";
       pntr->popn = "3,979,576";
       printf("\n%s, Population: %s\n", pntr->name, pntr->popn);
    
       return 0;
    }
  6. Теперь присвойте значения членам третей переменной(ch), а затем напечатайте в консоль сохранённые значения с помощью операции ->(стрелка), позволяющей сохранить значения, и операции ->(стрелка), позволяющей получить эти значения:
    #include <stdio.h>
    
    typedef struct
    {
       char *name;
       char *popn;
    } City;
    
    int main(void)
    {
       City ny;
       City la;
       City ch;
       City *pntr;
    
       ny.name = "New York City";
       ny.popn = "9,150,000";
       printf("\n%s , Population: %s\n", ny.name, ny.popn);
    
       pntr = &la;
       pntr->name = "Los Angeles";
       pntr->popn = "3,979,576";
       printf("\n%s, Population: %s\n", pntr->name, pntr->popn);
    
       pntr = &ch;
       pntr->name = "Chicago";
       pntr->popn = "2,695,615";
       printf("\n%s, Population: %s\n", pntr->name, pntr->popn);
    
       return 0;
    }
  7. Выполните программу:
    Значения, представляющие собой население городов, присваиваются как строки, что позволяет использовать в качестве разделителя разрядов запятые.
    Операция ->(стрелка) может помочь писать более понятный исходный код при разграничении указателей на структуры и указателей не на структуры.

Передача Структур в функции

Члены структур могут быть сохранены в массиве, как и значения любого другого типа данных. Массив объявляется как обычно, но метод присваивания значений его элементам несколько отличается. Каждый разделённый запятыми список значений-членов должен быть помещён в фигурные скобки.

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

Простое присваивание всех значений с помощью разделённого запятыми списка не сработает — каждый набор значений должен быть помещён в свою пару фигурных { } скобок.

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

  1. Напишем программу struct_to_func.c, объявите безымянную структуру, определяющую тип данных и имеющую два члена и один тег:
    #include <stdio.h>
    
    typedef struct
    {
        char *name;
        int quantity;
    } Item;
  2. Теперь объявите массив переменных, имеющих тип, определённый структурой, и инициализируйте члены каждой из трёх структур:
    #include <stdio.h>
    
    typedef struct
    {
        char *name;
        int quantity;
    } Item;
    Item fruits[3] = {{"Apple", 10}, {"Orange", 20}, {"Pear", 30}};
  3. Добавьте прототип функции, в которую в качестве аргументов будут передаваться переменная, имеющая тип структуры, и указатель на тип, определяемый структурой:
    #include <stdio.h>
    
    typedef struct
    {
        char *name;
        int quantity;
    } Item;
    Item fruits[3] = {{"Apple", 10}, {"Orange", 20}, {"Pear", 30}};
    
    void display(Item val, Item *ref);
  4. Теперь определите функцию, объявленную с помощью прототипа, в которой будут выводиться в консоль значения переданных аргументов:
    #include <stdio.h>
    
    typedef struct
    {
        char *name;
        int quantity;
    } Item;
    Item fruits[3] = {{"Apple", 10}, {"Orange", 20}, {"Pear", 30}};
    
    void display(Item val, Item *ref);
    
    int main(void)
    {
       return 0;
    }
    
    void display(Item val, Item *ref)
    {
       printf("%s: %d\n", val.name, val.quantity);
    }
  5. Далее в блоке определения функции напечатайте значения первого элемента списка, потом измените значения членов переданной структуры-копии(по значению), а затем выведите новые значения в консоль:
    #include <stdio.h>
    
    typedef struct
    {
        char *name;
        int quantity;
    } Item;
    Item fruits[3] = {{"Apple", 10}, {"Orange", 20}, {"Pear", 30}};
    
    void display(Item val, Item *ref);
    
    int main(void)
    {
       return 0;
    }
    
    void display(Item val, Item *ref)
    {
       printf("%s: %d\n", fruits[0].name, fruits[0].quantity);
    
       val.name = "Banana";
       val.quantity = 40;
       printf("%s: %d\n", val.name, val.quantity);
    }
  6. Убедитесь, что оригинальные значения не изменились - распечатайте значения:
    #include <stdio.h>
    
    typedef struct
    {
        char *name;
        int quantity;
    } Item;
    Item fruits[3] = {{"Apple", 10}, {"Orange", 20}, {"Pear", 30}};
    
    void display(Item val, Item *ref);
    
    int main(void)
    {
       return 0;
    }
    
    void display(Item val, Item *ref)
    {
       printf("%s: %d\n", fruits[0].name, fruits[0].quantity);
    
       val.name = "Banana";
       val.quantity = 40;
       printf("%s: %d\n", val.name, val.quantity);
    
       printf("%s: %d\n", fruits[0].name, fruits[0].quantity);
    }
  7. Далее измените члены оригинальной структуры(по указателю на данные в памяти машины) и выведите новые значения в консоль:
    #include <stdio.h>
    
    typedef struct
    {
        char *name;
        int quantity;
    } Item;
    Item fruits[3] = {{"Apple", 10}, {"Orange", 20}, {"Pear", 30}};
    
    void display(Item val, Item *ref);
    
    int main(void)
    {
       return 0;
    }
    
    void display(Item val, Item *ref)
    {
       printf("%s: %d\n", fruits[0].name, fruits[0].quantity); /* Проверка оригинального значения */
    
       val.name = "Banana"; /* Изменение по значению */
       val.quantity = 40; /* Изменение по значению */
       printf("%s: %d\n", val.name, val.quantity);
    
       printf("%s: %d\n", fruits[0].name, fruits[0].quantity); /* Проверка оригинального значения */
    
       ref->name = "Peach"; /* Изменение по Ссылке */
       ref->quantity = 50; /* Изменение по Ссылке */
       printf("%s: %d\n", fruits[0].name, fruits[0].quantity); /* Проверка оригинального значения */
    }
  8. Выполните программу:
Передача крупных структур по значению неэффективна, поскольку в памяти создаётся копия целой структуры, а при копировании по ссылке требуется выделить объём памяти, равный размеру одного указателя.

Группирование данных в Объединение

В программировании на языке C объединение позволяет хранить различные фрагменты любого типа данных в одном участке памяти в течение работы программы — присвоение значения объединению перезапишет данные, которые хранились там ранее. Это позволяет использовать память более эффективно.

Объединения объявляют с помощью ключевого слова union. Во время выполнения программы члены объединения могут получать значения только индивидуально.

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

  1. Напишем программу unions.c, в которой покажем разницу между инициализацией Структуры и Объединения. Объявим безымянную Структуру, определяющую пользовательский тип данных и имеющую три члена и один тег. Объявим безымянное Объединение, определяющее пользовательский тип данных и имеющее три члена и один тег:
    #include <stdio.h>
    
    typedef struct
    {
        int number;
        char letter;
        *string;
    } Distinct;
    
    typedef union
    {
        int number;
        char letter;
        *string;
    } Unified;
    
    int main(void)
    {
        return 0;
    }
  2. Теперь внутри блока функции main() объявите переменную типа данных, определяемого Структурой и инициализируйте все её члены:
    #include <stdio.h>
    
    typedef struct
    {
        int number;
        char letter;
        *string;
    } Distinct;
    
    typedef union
    {
        int number;
        char letter;
        *string;
    } Unified;
    
    int main(void)
    {
        Distinct sdata = {10, 'C', "Program"};
    
        return 0;
    }
  3. Теперь внутри блока функции main() объявите переменную типа данных, определяемого Объединением:
    #include <stdio.h>
    
    typedef struct
    {
        int number;
        char letter;
        *string;
    } Distinct;
    
    typedef union
    {
        int number;
        char letter;
        *string;
    } Unified;
    
    int main(void)
    {
        Distinct sdata = {10, 'C', "Program"};
        Unified udata;
    
        return 0;
    }
  4. После инициализации переменных Структуры выведите адрес в памяти и значение каждого члена Структуры:
    #include <stdio.h>
    
    typedef struct
    {
        int number;
        char letter;
        *string;
    } Distinct;
    
    typedef union
    {
        int number;
        char letter;
        *string;
    } Unified;
    
    int main(void)
    {
        Distinct sdata = {10, 'C', "Program"};
        Unified udata;
    
        printf("\nStructure:");
        printf("\nAt address %p: %d", &sdata.number, sdata.number);
        printf("\nAt address %p: %c", &sdata.letter, sdata.letter);
        printf("\nAt address %p: %s", &sdata.string, sdata.string);
        
        return 0;
    }
  5. Теперь, поочерёдно присваивайте значение каждого члена Объединения, а затем выводите в консоль адрес в памяти и значение этого члена Объединения:
    #include <stdio.h>
    
    typedef struct
    {
        int number;
        char letter;
        *string;
    } Distinct;
    
    typedef union
    {
        int number;
        char letter;
        *string;
    } Unified;
    
    int main(void)
    {
        Distinct sdata = {10, 'C', "Program"};
        Unified udata;
    
        printf("\nStructure:");
        printf("\nAt address %p: %d", &sdata.number, sdata.number);
        printf("\nAt address %p: %c", &sdata.letter, sdata.letter);
        printf("\nAt address %p: %s", &sdata.string, sdata.string);
    
        printf("\n\nUnion:");
        udata.number = 16;
        printf("\nAt address %p: %d", &udata.number, udata.number);
        udata.letter = 'A';
        printf("\nAt address %p: %c", &udata.letter, udata.letter);
        udata.string = "Algorithm";
        printf("\nAt address %p: %s\n\n", &udata.string, udata.string);
        
        return 0;
    }
  6. Выполните программу:
Именно программист несёт ответственность за понимание того, какие типы данных хранятся в объединении в любой момент выполнения программы.
Объединения наиболее полезны, если вам приходится работать в условиях ограниченной памяти.

Сравнительные выводы по struct и union

Перепишем код программы. Если допустить ошибку при написании кода для работы с Объединением, и инициализировать сразу каждый член Объединения, то произойдёт следующее:

#include <stdio.h>

typedef struct
{
    int number;
    char letter;
    *string;
} Distinct;

typedef union
{
    int number;
    char letter;
    *string;
} Unified;

int main(void)
{
    Distinct sdata = {10, 'C', "Program"};
    Unified udata;

    printf("\nStructure:");
    printf("\nAt address %p: %d", &sdata.number, sdata.number);
    printf("\nAt address %p: %c", &sdata.letter, sdata.letter);
    printf("\nAt address %p: %s", &sdata.string, sdata.string);
    
    udata.number = 16;
    udata.letter = 'A';
    udata.string = "Algorithm";
    printf("\n\nUnion:");
    printf("\nAt address %p: %d", &udata.number, udata.number);    
    printf("\nAt address %p: %c", &udata.letter, udata.letter);    
    printf("\nAt address %p: %s\n\n", &udata.string, udata.string);
    
    return 0;
}

Выполним программу:

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

Выделение памяти

Стандартная библиотека заголовочных файлов <stdlib.h> предоставляет функции управления памятью, с помощью которых программа может явно запросить память во время выполнения.

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

Функция calloc() требует наличия двух аргументов, которые будут перемножены между собой с целью определения объёма памяти, который следует выделить. В случае успеха возвращает указатель на начало блока памяти. В случае неудачи возвращает значение NULL. Функция calloc() заполняет всё выделенное пространство памяти нулями.

Объём памяти, выделенный с помощью функций malloc() и calloc(), может быть увеличен с помощью функции realloc(). Эта функция требует указатель на выделенный блок памяти в качестве первого аргумента и число, обозначающее размер нового блока, в качестве второго аргумента функции. Она возвращает указатель на начало увеличенного блока в случае успеха и значение NULL в случае неудачи.

Для высвобождения ранее запрошенного объёма памяти у машины с помощью функций malloc() и calloc(), используется функция free(), принимающая в качестве аргумента указатель на начало выделенного блока памяти.

Список прототипов функций, в том числе управления памятью, стандартного заголовочного файла <stdlib.h> смотрите здесь: https://en.cppreference.com/w/c/header/stdlib.html

  1. Напишем программу memory.c, подключим стандартные заголовочные файлы для ввода/вывода и управления памятью:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
       return 0;
    }
  2. Добавьте в функцию main(), объявление целочисленной переменной и целочисленный указатель:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int size;
        int *memory;
    
        return 0;
    }
  3. Запросите память в объёме, необходимом для размещения сотни целых чисел:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int size;
        int *memory;
    
        memory = malloc(100 * sizeof(int)); /* Указатель будет инициализирован адресом 
          начала области выделенной в памяти */
    
        return 0;
    }
  4. Далее выведите информацию о выделенном блоке памяти, а в случаи ошибки выделения памяти - сообщение, гласящее, что запрос не сработал:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
       int size;
       int *memory;
    
       memory = malloc(100 * sizeof(int));
       if(memory != NULL)
       {
          size = _msize(memory); /* Получаем фактический размер в байтах с помощью нестандартной функции _msize */
          printf("\nSize of block for 100 integers: %d bytes\n", size);
          printf("Beginning at address %p\n", memory);
       }
       else
       {
          printf("!!! Insufficient memory\n");
          return 1;
       }
    
       return 0;
    }

    Что делает _msize ?

    Функция используется в Windows (MSVC, также доступна в некоторых сборках MinGW) для определения реального размера блока памяти, ранее выделенного через: malloc(), calloc(), realloc().

    В нашей программе, функция возвращает размер блока выделенной памяти в количестве байт. При выполнении, Функция _msize(memory) возвращает тип данных size_t, обычно unsigned int или unsigned long, содержащий размер блока в байтах. Это возвращённое значение неявно приводится к типу int и записывается в переменную size.

    size_t — это беззнаковый целочисленный тип (unsigned integer type), используемый для представления размеров объектов в байтах и для подсчёта элементов в контейнерах.

    size_t определяется в стандартных заголовочных файлах: <stddef.h>, <stdio.h>, <stdlib.h> .

  5. Теперь попытайтесь увеличить объём выделенного блока памяти и вывести информацию о нём или же сообщение, гласящее, что запрос не сработал:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
       int size;
       int *memory;
    
       memory = malloc(100 * sizeof(int));
       if(memory != NULL)
       {
          size = _msize(memory);
          printf("\nSize of block for 100 integers: %d bytes\n", size);
          printf("Beginning at address %p\n", memory);
       }
       else
       {
          printf("!!! Insufficient memory\n");
          return 1;
       }
           memory = realloc(memory, size + (100 * sizeof(int)));
        if(memory != NULL)
        {
            size = _msize(memory);
            printf("\nSize of block for 200 integers: %d bytes\n", size);
            printf("Beginning at address %p\n", memory);
        }
        else
        {
            printf("!!! Insufficient memory\n");
            return 1;
        }
    
       return 0;
    }
  6. В конце блока функции main() освободим выделенную память:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int size;
        int *memory;
    
        memory = malloc(100 * sizeof(int)); /* Утверждение, запрашивающее память, может выглядеть и так: 
          calloc(100, sizeof(int)); */
        if (memory != NULL)
        {
            size = _msize(memory);
            printf("\nSize of block for 100 integers: %d bytes\n", size);
            printf("Beginning at address %p\n", memory);
        }
        else
        {
            printf("!!! Insufficient memory\n");
            return 1;
        }
        memory = realloc(memory, size + (100 * sizeof(int)));
        if (memory != NULL)
        {
            size = _msize(memory);
            printf("\nSize of block for 200 integers: %d bytes\n", size);
            printf("Beginning at address %p\n", memory);
        }
        else
        {
            printf("!!! Insufficient memory\n");
            return 1;
        }
    
        free(memory);
    
        return 0;
    }
  7. Выполните программу:
    Несмотря на то, что тип int, как правило, занимает 4 байта, хорошим тоном является использование операции sizeof(), чтобы определить точный размер блока памяти на случай, если переменные типа int имеют другой размер на данной машине.
  8. Добавьте в программу цикл выводящий в консоль значения каждого целого числа из области выделенной памяти - т.е. отобразите все 200 целых чисел.

Заключение

Работа с Файлами

Здесь показывается, как можно использовать создание, чтение и запись файлов, работать с: системным временем и датой, случайными числами; как создать простейшее диалоговое окно ОС Windows.

Создание файла

В заголовочном файле <stdio.h> определён специальный тип данных, предназначенный для работы с файлами. Он называется указателем на файл, и имеет тип данных определяемым ключевым словом FILE. Синтаксис следующий:

FILE *fp;

Указатели на файлы используются для их:

В программе, указатель на файл с именем file_ptr может быть создан с помощью объявления:
FILE *file_ptr;

Указатель на файл указывает на структуру, определенную в заголовочном файле <stdio.h>, которая содержит информацию о файле. Последняя включает в себя данные о текущем символе и о состоянии файла (выполняется ли его чтение или запись в данный момент).

Все стандартные функции, предназначенные для работы с файлами, содержатся в заголовочном файле <stdio.h> .

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

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

Для удобства все текстовые файлы, рассмотренные в примерах этой главы, расположены в той же директории hello, в которой также содержатся и файлы исходного кода.

В таблице ниже, перечислены все возможные режимы открытия файлов, которые могут быть переданы в качестве второго аргумента функции fopen():

Режим файла Операция
r - (read) Открытие существующего файла для чтения
w - (write) Открытие существующего файла для записи. Создание нового файла, если файла с таким именем не существует. Открытие уже существующего файла и затирание всех данных, которые там были ранее.
a - (append) Добавление текста. Открывает или создает текстовый файл для записи в конец файла.
r+ Открывает текстовый файл для чтения или записи.
w+ Открывает текстовый файл для записи или чтения.
a+ Открывает или создает текстовый файл для чтения или записи в конец файла.
Если режим включает в себя букву b (binary) после любого приведенного ранее режима, операция будет относиться к двоичному(бинарному) файлу, но не к текстовому. Например, rb или w+b.
  1. Напишем программу newfile.c, в которой изучим как файл создать:
    #include <stdio.h>
    
    int main(void)
    {
       return 0;
    }
  2. В функции main(), объявляем указатель на файл:
    #include <stdio.h>
    
    int main(void)
    {
       FILE *file_pntr; /* Объявляем Указатель на Файл */
       
       return 0;
    }
  3. Далее в блоке функции main() попробуйте создать файл с режимом Записи:
    #include <stdio.h>
    
    int main(void)
    {
       FILE *file_pntr;
       file_pntr = fopen("data.txt", "w"); /* Инициализируем Указатель именем файла, 
          ставим файл в режим Записи данных */
    
       return 0;
    }
    Обратите внимание на то, что имя файла и режим его открытия должны быть заключены в двойные " " кавычки.
  4. Теперь выведите сообщение, подтверждающее успешность попытки создания файла, а затем закройте файл:
    #include <stdio.h>
    
    int main(void)
    {
       FILE *file_pntr;
       file_pntr = fopen("data.txt", "w");
       if(file_pntr != NULL) /* Если условие выполняется - 
          Указатель не NULL - т.е. файл существует, тогда выполнить тело условия по ветке true */
       {
          printf("File created\n");
          fclose(file_pntr); /* Закрываем файл */
          return 0; /* Возвращаем 0 - Успешное выполнение */
       }
    
       return 0;
    }
  5. Выведите альтернативное сообщение, если попытка была безуспешной:
    #include <stdio.h>
    
    int main(void)
    {
       FILE *file_pntr;
       file_pntr = fopen("data.txt", "w");
       if(file_pntr != NULL)
       {
          printf("File created\n");
          fclose(file_pntr);
          return 0;
       }
       else
       {
          printf("Unable to create file.\n");
          return 1;  /* Возвращаем 1 - Безуспешно */
       }
    
       return 0;
    }
    Обратите внимание на то, что эта программа возвращает значение 1 в случае неудачной попытки открытия файла — это говорит системе, что все прошло не очень гладко.
  6. До выполнения программы, в папке текстовый файл data.txt отсутствует:
  7. После выполнения программы, в папке создан текстовый файл data.txt:
  8. Проверить содержимое файла data.txt легко - кликните на него в Visual Studio Code.
    Пустой файл с расширением *.txt

Чтение и запись символов

Стандартный ввод - Клавиатура

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

В программировании на языке C файловый поток с именем stdin представляет собой клавиатуру и является источником данных по умолчанию для функции scanf(). Вызов функции scanf() аналогичен вызову функции fscanf(stdin, …).

Стандартный вывод - Консоль

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

Файловый поток с именем stdout представляет консоль и является источником по умолчанию для функции printf(). Вызов функции printf() аналогичен вызову функции fprintf(stdout, …).

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

ещё одним стандартным файловым потоком является поток stderr, который используется для вывода сообщений об ошибках.

  1. Напишем программу writechars.c, в которой будет записывать в файл символы:
    #include <stdio.h>
    
    int main(void)
    {
        FILE *file_pntr; /* Объявляем Указатель на Файл */
        int number; /* Объявляем целое число */
        char text[50] = "Text, one character at a time."; /* Объявляем Символьный литерал и 
          инициализируем его текстовой строкой */
    
        return 0;
    }
  2. Далее в блоке функции main() попробуйте создать файл для Записи в него:
    #include <stdio.h>
    
    int main(void)
    {
        FILE *file_pntr;
        int number;
        char text[50] = "Text, one character at a time.";
        file_pntr = fopen("chars.txt", "w");  /* Инициализируем Указатель на Файл, 
          передаём имя для открытия файла, и ставим режим Записи данных для файла */
    
        return 0;
    }
  3. Теперь выведите сообщение, подтверждающее успешность попытки создания файла. С помощью цикла for запишите в файл каждый символ массива, а затем закройте файл и верните значение 0, как того требует объявление функции:
    #include <stdio.h>
    
    int main(void)
    {
        FILE *file_pntr;
        int number;
        char text[50] = "Text, one character at a time.";
    
        file_pntr = fopen("chars.txt", "w");
        if (file_pntr != NULL)
        {
            printf("File chars.txt created\n");
            for (number = 0; text[number]; number++)
            {
                fputc(text[number], file_pntr);
            }
            fclose(file_pntr);
            return 0;
        }
    
        return 0;
    }
    Функция fputc() возвращает ASCII-код текущего символа или константу EOF(End of File), которая говорит о том, что достигнут конец файла. Возвращаемый ASCII-код текущего символа, один за другим - последовательно Записывается в текстовый файл, на который указывает Указатель. Функция fputc() принимает в качестве аргументов - символ и указатель на файл.
    Обратите внимание на условное выражение в цикле, которое не содержит ни какого знака сравнение со значением, и самого значения сравнение тоже нет. В виду того, что содержимое строки по индексу массива всегда возвращает true пока значение отлично от нуля. А так как мы знаем, что символ - это целое однобайтовое число типа char всегда больше нуля, то условие будет выполняться до тех пор пока в строке не будет обнаружен символ \0 - конец строки, и тогда этот символ будет равен по условию false и цикл завершится точно пройдя все символы строки до её конца.
  4. Выведите альтернативное сообщение, если попытка создания или открытия файла была безуспешной:
    #include <stdio.h>
    
    int main(void)
    {
        FILE *file_pntr;
        int number;
        char text[50] = "Text, one character at a time.";
    
        file_pntr = fopen("chars.txt", "w");
        if (file_pntr != NULL)
        {
            printf("File chars.txt created\n");
            for (number = 0; text[number]; number++)
            {
                fputc(text[number], file_pntr);
            }
            fclose(file_pntr);
            return 0;
        }
        else
        {
            printf("Unable to create file\n");
            return 1;
        }
    
        return 0;
    }
  5. Выполните программу:
  6. Откройте текстовый файл в Visual Studio Code и просмотрите его содержимое:

Чтение и запись строк

Использование функции fgetc() — не самый эффективный способ считывания текста в программе, поскольку эту функцию необходимо вызывать большое количество раз подряд, чтобы считать все символы. Лучше использовать функцию fgets(), которая считывает текст по одной строке за раз.

Функция fgets() возвращает указатель на char (т.е. строку — массив символов). Принимает три аргумента:

char* fgets( char* restrict str, int count, FILE* restrict stream );
  1. char* restrict str
    - Первый аргумент определяет символьный указатель или массив символов, куда будет записан текст. Где:
    char*
    - указатель на char, куда будет записана строка.
    restrict
    - компилятору сообщается, что в этом участке кода доступ к этой памяти происходит только через этот указатель (для оптимизации).
  2. int count
    - Второй аргумент — Целое число, которое определяет максимальное количество символов в считываемой строке.
  3. FILE* restrict stream
    - Третий аргумент — файловый указатель, указывающий, из какого файла/потока следует производить чтение.

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

Функция fputs():

int fputs( const char* restrict str, FILE* restrict stream );

Принимает два аргумента:

  1. char* restrict str
    - Первый аргумент определяет символьный указатель или массив символов, куда будет записан текст.
  2. FILE* restrict stream
    - Второй аргумент — файловый указатель, указывающий, из какого файла/потока следует производить чтение.

Функция fputs() добавляет символ новой строки \n всякий раз, когда записывает строку. Эта функция возвращает 0 в случае успеха или константу EOF, когда происходит ошибка или функция достигает конца файла.

  1. Напишем программу text_lines.c, для обработки строк подключим заголовочный файл <string.h>:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        return 0;
    }
  2. Добавьте в функцию main(), в которой объявляются указатель на файл и неинициализированный массив символов:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        FILE *file_pntr;
        char text[50];
    
        return 0;
    }
  3. Создадим текстовый файл в папке hello, "farewell.txt", с текстом внутри:
    A thousand suns will stream on tree,
    A thousand moon will quiver;
    But not by thee my steps shall be,
    For ever and for ever
             
    Текстовый файл легко создать в Visual Studio Code также как и файлы с расширениями *.c и *.h .
  4. Далее в блоке функции main() попробуйте открыть текстовый файл "farewell.txt" для чтения и записи данных в его конец:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        FILE *file_pntr;
        char text[50];
    
        file_pntr = fopen("farewell.txt", "r+"); /* Открываем тектсовый файл в режиме Чтение+Запись-в-Конец */
    
        return 0;
    }
  5. Если попытка открытия файла Успешна - выведите сообщение:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        FILE *file_pntr;
        char text[50];
    
        file_pntr = fopen("farewell.txt", "r+");
        if(file_pntr != NULL)
        {
           printf("File \"farewell.txt\" opend to read and append.\n"); /* При успешной попытке открыть файл */
        }
    
        return 0;
    }
  6. Теперь в этом же блоке if, напишем цикл while для Чтения строк текстового файла одна за другой и напечатаем их в консоль:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        FILE *file_pntr;
        char text[50];
    
        file_pntr = fopen("farewell.txt", "r+");
        if(file_pntr != NULL)
        {
           printf("File \"farewell.txt\" opend to read and append.\n"); /* При успешной попытке открыть файл */
           while (fgets(text, 50, file_pntr))
           {
              printf("%s", text);
           }
        }
    
        return 0;
    }
    Обратите внимание на Второй аргумент Функции fgets() в проверочном утверждении цикла while имеющий значение 50. Это значение определяет максимальное количество символов которые будут Читаться функцией за раз. И до тех пор пока возвращаемое функцией fgets() значение символьного Указателя не равно Нулевому Указателю, цикл будет выполнять итерацию за итерацией. Внутри Функции fgets() скрыт механизм перехода вперёд на соответствующее количество символов (Второй её аргумент), как только Прочитаны предыдущие. При этом согласно Арифметике указателей, Символьный Указатель получает приращение и цикл повторяется. Как только будет получен EOF - Цикл завершится. Кстати, даже если строка слишком длинная, она будет прочитана в несколько вызовов Функции fgets() - это также неявный механизм работающий внутри функции.
  7. Внутри блока if, сразу после цикла while скопируйте новую строку в массив, а затем добавьте её в конец текстового файла:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        FILE *file_pntr;
        char text[50];
    
        file_pntr = fopen("farewell.txt", "r+");
        if(file_pntr != NULL)
        {
           printf("File \"farewell.txt\" opend to read and append.\n"); /* При успешной попытке открыть файл */
           while (fgets(text, 50, file_pntr))
           {
              printf("%s", text);
           }
           strcpy(text, "\n...by Lord Alfred Tennyson");
           fputs(text, file_pntr);
        }
    
        return 0;
    }
    Обратите внимание на то, как функция strcpy() используется для присвоения новой строки массиву символов.
  8. В конце блока if закройте файл и верните ноль:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        FILE *file_pntr;
        char text[50];
    
        file_pntr = fopen("farewell.txt", "r+");
        if(file_pntr != NULL)
        {
           printf("File \"farewell.txt\" opend to read and append.\n"); /* При успешной попытке открыть файл */
           while (fgets(text, 50, file_pntr))
           {
              printf("%s", text);
           }
           strcpy(text, "\n...by Lord Alfred Tennyson");
           fputs(text, file_pntr);
    
           fclose(file_pntr);
        }
    
        return 0;
    }
  9. В случаи если не удалось открыть текстовый файл "farewell.txt" для чтения и записи, напечатаем в консоль сообщение об этом и вернём единицу:
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        FILE *file_pntr;
        char text[50];
    
        file_pntr = fopen("farewell.txt", "r+");
        if(file_pntr != NULL)
        {
           printf("File \"farewell.txt\" opend to read and append.\n"); /* При успешной попытке открыть файл */
           while (fgets(text, 50, file_pntr))
           {
              printf("%s", text);
           }
           strcpy(text, "\n...by Lord Alfred Tennyson");
           fputs(text, file_pntr);
    
           fclose(file_pntr);
           return 0;
        }
        else
        {
           printf("Unable to open \"farewell.txt\" to read and write.\n");
           return 1;
        }
    
        return 0;
    }
  10. До выполнения программы:
  11. После выполнения программы:

Синтаксис записи пути к файлу

Пути бывают Абсолютные и Относительные

Абсолютный путь: Указывает полное расположение файла в файловой системе. Синтаксис отличается, например:

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

Проверка, где находится текущая рабочая папка

В Windows при запуске программы рабочей директорией будет папка, где находится .exe, если запускать двойным кликом, либо папка, из которой выполняется запуск через консоль. Можно узнать текущую папку через C (POSIX) функцией:

#include <unistd.h>
#include <stdio.h>

int main() {
    char cwd[1024]; /* Зарезервируем 1024 байта для строки */
    getcwd(cwd, sizeof(cwd)); /* Функция getcwd - get current working directory*/
    printf("Current working directory: %s\n", cwd); /* Печать в консоль текущего пути */
    return 0;
}

Например так:

Считывание и запись файлов целиком

Файл может быть прочитан целиком с помощью функции fread(). Вот её синтаксис:

size_t fread( void *restrict buffer, size_t size, size_t count, FILE *restrict stream );

Где:

Файл может быть записан целиком с помощью функции fwrite(). Вот её синтаксис:

size_t fwrite( const void* restrict buffer, size_t size, size_t count, FILE* restrict stream );

Где:

Итак, Первый из них — символьная переменная, где текст может храниться. Во втором аргументе указывается размер считываемых или записываемых за раз фрагментов текста — обычно он равен 1. Третий аргумент позволяет указать общее количество символов для чтения или записи, а четвёртый аргумент является файловым указателем, указывающим на файл, с которым нужно работать.

Функция fread() возвращает количество считанных объектов, в котором учитываются символы, пробелы переводы каретки. Аналогично, функция fwrite() возвращает количество записанных объектов. Обе функции находятся в заголовочном файле <stdio.h>:

  1. Напишем программу read_write_file.c:
    #include <stdio.h>
    
    int main(void)
    {
        FILE *orignl_pntr; /* Объявим Указатель на оригинальный файл */
        FILE *copy_pntr;   /* Объявим Указатель на  файл-копию */
        char buffer[500]; /* Объявим массив символов на 500 штук (его имя есть Указатель на него) */
        int number;        /* Объявим целочисленную переменную */
    
        return 0;
    }
  2. Создайте текстовый файл original.txt со следующим текстом в нём:
    "Sometimes it is the people no one can imagine anything of who do the things no one can imagine."
    "If a machine is expected to be infallible, it cannot also be intelligent."
    "Those who can imagine anything, can create the impossible."
    "These questions replace our original, "Can machines think?""
    Alan Turing
  3. Далее в блоке функции main() попробуйте открыть существующий файл для чтения и другой файл для записи:
    #include <stdio.h>
    
    int main(void)
    {
        FILE *orignl_pntr;
        FILE *copy_pntr;
        char buffer[500];
        int number;
    
        orignl_pntr = fopen("original.txt", "r");
        copy_pntr = fopen("copy.txt", "w");
    
        return 0;
    }
  4. Теперь проверьте, были ли оба файла успешно открыты:
    #include <stdio.h>
    
    int main(void)
    {
        FILE *orignl_pntr;
        FILE *copy_pntr;
        char buffer[500];
        int number;
    
        orignl_pntr = fopen("original.txt", "r");
        copy_pntr = fopen("copy.txt", "w");
    
        if ((orignl_pntr != NULL) && (copy_pntr != NULL))
        {        
        }
    
        return 0;
    }
  5. Внутри блока if считайте содержимое оригинального файла в массив символов, подсчитывая каждый считанный объект, а затем запишите содержимое массива во второй файл. Закройте каждый файл:
    #include <stdio.h>
    
    int main(void)
    {
        FILE *orignl_pntr;
        FILE *copy_pntr;
        char buffer[500];
        int number;
    
        orignl_pntr = fopen("original.txt", "r");
        copy_pntr = fopen("copy.txt", "w");
    
        if ((orignl_pntr != NULL) && (copy_pntr != NULL))
        {
           number = fread(buffer, 1, 500, orignl_pntr);
           fwrite(buffer, 1, number, copy_pntr);
           fclose(orignl_pntr); 
           fclose(copy_pntr);
        }
    
        return 0;
    }
    Используйте количество объектов, возвращенное функцией fread() как третий аргумент для функции fwrite(), чтобы гарантировать, что количество записанных объектов совпадет с количеством считанных объектов.
  6. В заключении блока if выведите сообщение, подтверждающее успех операции, которое содержит количество объектов, и верните значение 0:
    #include <stdio.h>
    
    int main(void)
    {
        FILE *orignl_pntr;
        FILE *copy_pntr;
        char buffer[500];
        int number;
    
        orignl_pntr = fopen("original.txt", "r");
        copy_pntr = fopen("copy.txt", "w");
    
        if ((orignl_pntr != NULL) && (copy_pntr != NULL))
        {
           number = fread(buffer, 1, 500, orignl_pntr);
           fwrite(buffer, 1, number, copy_pntr);
           fclose(orignl_pntr); 
           fclose(copy_pntr);
           printf("\nDone: \"original.txt\" copied to \"copy.txt\"");
           printf("\n%d objects were copied.\n", number);
           return 0;
        }
    
        return 0;
    }
  7. Добавьте сообщение, если попытка была без успешной:
    #include <stdio.h>
    
    int main(void)
    {
        FILE *orignl_pntr;
        FILE *copy_pntr;
        char buffer[500];
        int number;
    
        orignl_pntr = fopen("original.txt", "r");
        copy_pntr = fopen("copy.txt", "w");
    
        if ((orignl_pntr != NULL) && (copy_pntr != NULL))
        {
           number = fread(buffer, 1, 500, orignl_pntr);
           fwrite(buffer, 1, number, copy_pntr);
           fclose(orignl_pntr); 
           fclose(copy_pntr);
           printf("\nDone: \"original.txt\" copied to \"copy.txt\"");
           printf("\n%d objects were copied.\n", number);
           return 0;
        }
        else
        {
          if (orignl_pntr == NULL)
          {
             printf("Unable to open \"original.txt\"");
          }
          if (copy_pntr == NULL)
          {
             printf("Unable to open \"copy.txt\"");
          }
          return 1;
        }
    
        return 0;
    }
    Убедитесь, что третий аргумент функции fread() достаточно велик, чтобы вместить всё копируемое содержимое. Если в этом примере изменить значение данного аргумента на 100, будут скопированы первые 100 объектов. При этом все последние объекты окажутся опущены.
  8. Выполните программу:
    При выполнении программы, результ количества скопированных объектов равен 308.
  9. Результат выполнения программы:
    При выделении текста в файле, количество скопированных объектов равно 312. А при выполнении программы скопированно 308 объектов. Почему?

Сканирование файловых потоков

Функция scanf(), использовавшаяся для получения данных, введённых пользователем, является упрощенной версией функции fscanf(), которая всегда выполняет чтение из потока stdin. Функция fscanf() позволяет вам указать файловый поток для чтения в качестве её первого аргумента. Также она имеет преимущество при чтении файлов, содержащих только числа — числа в текстовом файле видны как простой набор символов, но если они будут считаны функцией fscanf(), они окажутся преобразованы к их числовому типу.

Синтаксис:

int fscanf( FILE *restrict stream, const char *restrict format, ... );

Где:

Возвращаемое значение:

  1. Количество успешно назначенных принимающих аргументов.
  2. Количество принимающих аргументов равно 0(нулю) в случае, если ошибка сопоставления произошла до назначения первого принимающего аргумента.
  3. EOF, если ошибка ввода произошла до назначения первого принимающего аргумента.
  4. То же, что и (1-3), за исключением того, что EOF также возвращается в случае нарушения ограничений времени выполнения.

Функция printf(), использованная для вывода информации на экран, является упрощенной версией функции fprintf(), которая всегда выполняет запись в поток stdout. Функция fprintf() позволяет вам указать файловый поток для записи в качестве её первого аргумента.

Синтаксис:

int fprintf( FILE* restrict stream, const char* restrict format, ... );

Где:

Возвращаемые значения:

  1. Количество символов, которые были реально выведены в поток вывода.
  2. Отрицательное значение (обычно EOF или -1) при ошибке вывода или ошибке кодировки.

Функции fscanf() и fprintf() предлагают высокий уровень гибкости, позволяя выбрать поток, с которым будет производиться чтение или запись

  1. Напишем программу fscanprint.c для демонстрации работы функций:
    #include <stdio.h>
    
    int main(void)
    {
       FILE *nums_pntr; /* Объявляем Указатель на файл */
       FILE *hint_pntr; /* Объявляем Указатель на файл */
       int nums[20];    /* Объявляем целочисленный массив */
       int i;           /* Объявляем целое число */
       int j;           /* Объявляем целое число */
    }
    
  2. Создайте текстовый файл nums.txt с таким содержимым:
    1 2 3 4 5 6 7 8 9 10
  3. #include <stdio.h>
    
    int main(void)
    {
       FILE *nums_pntr;
       FILE *hint_pntr;
       int nums[20];
       int i;
       int j;
    
       nums_pntr = fopen("nums.txt", "r"); /* Инициализируем Указатель файлом и режимом Чтение */
       nums_pntr = fopen("hint.txt", "w"); /* Инициализируем Указатель файлом и 
          режимом Записи(создавая/очищая файл) */
    
       return 0;
    }
    
  4. Применим функцию feof() проверяющую - достигнут ли конец файла, переданного ей в качестве аргумента. Функция возвращает 0(false) до тех пор пока не достигнут конец файла, если конец файла достигнут возвращает не нулевое значение(true).
    #include <stdio.h>
    
    int main(void)
    {
       FILE *nums_pntr;
       FILE *hint_pntr;
       int nums[20];
       int i;
       int j;
    
       nums_pntr = fopen("nums.txt", "r");
       hint_pntr = fopen("hint.txt", "w");
    
       if((nums_pntr != NULL) && (hint_pntr != NULL)) /* Если оба Указателя не NULL */
       {
          for(i = 0; !feof(nums_pntr); i++) /* С помощью цикла, при проверке на достижение Конца файла... */
          {
             fscanf(nums_pntr, "%d", &nums[i]); /* ...читает из файла одно число за итерацию(в десятичном формате) 
                и сохраняет его в массив(по адресу индекса) */
          }
       }
       return 0;
    }
    
  5. #include <stdio.h>
    
    int main(void)
    {
       FILE *nums_pntr;
       FILE *hint_pntr;
       int nums[20];
       int i;
       int j;
    
       nums_pntr = fopen("nums.txt", "r");
       hint_pntr = fopen("hint.txt", "w");
    
       if((nums_pntr != NULL) && (hint_pntr != NULL))
       {
          for(i = 0; !feof(nums_pntr); i++)
          {
             fscanf(nums_pntr, "%d", &nums[i]);
          }
          fprintf(stdout, "\nTotal numbers found: %d\n", i); /* Печать в консоль количества найденых чисел */
          for(j = 0; j < i; j++)
          {
             fprintf(stdout, "%d", nums[j]); /* Напечатаем каждый элемент массива в консоль */ 
          }
       }
       return 0;
    }
    
  6. #include <stdio.h>
    
    int main(void)
    {
       FILE *nums_pntr;
       FILE *hint_pntr;
       int nums[20];
       int i;
       int j;
    
       nums_pntr = fopen("nums.txt", "r");
       hint_pntr = fopen("hint.txt", "w");
    
       if((nums_pntr != NULL) && (hint_pntr != NULL))
       {
          for(i = 0; !feof(nums_pntr); i++)
          {
             fscanf(nums_pntr, "%d", &nums[i]);
          }
          fprintf(stdout, "\nTotal numbers found: %d\n", i);
          for(j = 0; j < i; j++)
          {
             fprintf(stdout, "%d", nums[j]);
          }
          fprintf(hint_pntr, "fscanf and fprintf are flexible\n"); /* Запишем строку текста в Файл */
          for(j = 0; j < i; j++)
          {
             fprintf(hint_pntr, "%d", nums[j]);  /* Запишем в Файл, в десятичном формате, каждый элемент массива */ 
          }
       }
       return 0;
    }
    
  7. #include <stdio.h>
    
    int main(void)
    {
       FILE *nums_pntr;
       FILE *hint_pntr;
       int nums[20];
       int i;
       int j;
    
       nums_pntr = fopen("nums.txt", "r");
       hint_pntr = fopen("hint.txt", "w");
    
       if((nums_pntr != NULL) && (hint_pntr != NULL))
       {
          for(i = 0; !feof(nums_pntr); i++)
          {
             fscanf(nums_pntr, "%d", &nums[i]);
          }
          fprintf(stdout, "\nTotal numbers found: %d\n", i);
          for(j = 0; j < i; j++)
          {
             fprintf(stdout, "%d", nums[j]);
          }
          fprintf(hint_pntr, "fscanf and fprintf are flexible\n");
          for(j = 0; j < i; j++)
          {
             fprintf(hint_pntr, "%d", nums[j]);
          }
          fclose(nums_pntr); /* Закроем Файл */
          fclose(hint_pntr); /* Закроем Файл */
       }
       else /* Если какой либо из файлов не открылся сообщить Ошибку */
       {
          fprintf(stderr, "Error: Unable to open files.\n");
       }
       return 0;
    }
    

    stderr (англ. standard error - стандартная ошибка) — это стандартный поток ошибок в языке С и в ОС, обычно вывод осуществляется в консоль(экран).

    Применение:

    • Вывод сообщений об ошибках и диагностики отдельно от обычного вывода данных (stdout).
    • Обеспечивает удобную фильтрацию и перенаправление потоков:
      • Можно перенаправить stdout в файл, оставив ошибки в stderr видимыми в консоли.
      • Упрощает отладку: ошибки видны даже при скрытом основном выводе.
  8. Выполните программу:

Мусорные значения сгенерированные в примере выше

В первом цикле:

for(i = 0; !feof(nums_pntr); i++) /* С помощью цикла, при проверке на достижение Конца файла... */
      {
         fscanf(nums_pntr, "%d", &nums[i]); /* ...читает из файла одно число за итерацию(в десятичном формате) 
            и сохраняет его в массив(по адресу индекса) */
      }

После того как функция feof() вернёт EOF - конец файла, переменной i будет присвоенно значение на единицу больше чем было обнаруженно чисел в файле - это потенциально опасная ситуация. Функция feof() срабатывает только после неудачной попытки чтения. В итоге последнее значение в массиве может быть мусором.

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

Исправить ситуацию можно переписав цикл так:

i = 0;
while (fscanf(nums_pntr, "%d", &nums[i]) == 1)
{
    i++;
}

Теперь:

Дополнительно можно добавить защиту от переполнения массива так:

i = 0;
while (i < 20 && fscanf(nums_pntr, "%d", &nums[i]) == 1)
{
    i++;
}

Это исключит выход за пределы массива, если в файле больше 20 чисел.

Перепишем код так:

#include <stdio.h>

int main(void)
{
    FILE *nums_pntr;
    FILE *hint_pntr;
    int nums[20];
    int i = 0; /* Инициализируем нулём переменную здесь */
    int j;

    /* Открытие файлов: nums.txt для чтения, hint.txt для записи */
    nums_pntr = fopen("nums.txt", "r");
    hint_pntr = fopen("hint.txt", "w");

    if ((nums_pntr != NULL) && (hint_pntr != NULL))
    {
        /* Корректное чтение чисел из файла */
        while ((i < 20) && (fscanf(nums_pntr, "%d", &nums[i]) == 1))
        {
            i++;
        }

        /* Вывод в консоль количества считанных чисел */
        fprintf(stdout, "\nTotal numbers found: %d\n", i);

        /* Вывод в консоль самих чисел */
        for (j = 0; j < i; j++)
        {
            fprintf(stdout, "%d ", nums[j]);
        }
        fprintf(stdout, "\n");

        /* Запись текста в hint.txt */
        fprintf(hint_pntr, "fscanf and fprintf are flexible\n");

        /* Запись чисел в hint.txt через пробелы */
        for (j = 0; j < i; j++)
        {
            fprintf(hint_pntr, "%d ", nums[j]);
        }
        fprintf(hint_pntr, "\n");

        /* Закрытие файлов */
        fclose(nums_pntr);
        fclose(hint_pntr);
    }
    else
    {
        fprintf(stderr, "Error: Unable to open files.\n");
    }

    return 0;
}

Выполним программу:

Ещё раз - на языке С можно написать невероятно быстро исполняемую программу, но в то же время программист должен досканально понимать: как ведёт себя та или иная функция, какие "подводные камни" можно не заметить в виде неочевидных побочных эффекты как этот. Изучайте язык внимательно и не спеша - пишите собственные варианты приводимых в учебнике программ, изучайте функции стандартных заголовочных файлов - подробно разбирая принимаемые ими аргументы и возвращаемые значения. Руководствуйтесь официальными источниками такими как https://en.cppreference.com/w/c/header.html
Итак, Функции fscanf() и fprintf() принимают те же аргументы, что и функции scanf() и printf() плюс дополнительный аргумент, позволяющий указать поток.

Сообщения об ошибках

Язык программирования C предоставляет функцию с именем perror(), которая находится в заголовочном файле <stdio.h> и может быть использована для вывода на экран подробных сообщений об ошибках. В качестве аргумента она принимает строку, к которой добавляет двоеточие( : ), а за ним — описание текущей ошибки.

В дополнение в заголовочном файле <errno.h> определено целочисленное выражение с именем errno, которому присваивается числовой код ошибки, когда случается ошибка. Этот код может быть передан в качестве аргумента функции strerror(), располагающейся в заголовочном файле <string.h>, которая выводит связанное с этим кодом сообщение об ошибке.

  1. Напишем программу errno.c, подключим заголовочные файлы стандартной библиотеки ввода/вывода, функции обработки сообщений об ошибках и функции работы со строками:
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    int main(void)
    {
        FILE *file_pntr;
        int i;
    
        return 0;
    }
  2. Далее в блоке функции main() попробуйте открыть несуществующий текстовый файл в режиме чтения:
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    int main(void)
    {
        FILE *file_pntr;
        int i;
    
        file_pntr = fopen("nosuch.file", "r");
    
        return 0;
    }
  3. Выведите в консоль подтверждение или сообщение об ошибке, если попытка провалится:
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    int main(void)
    {
        FILE *file_pntr;
        int i;
    
        file_pntr = fopen("nosuch.file", "r");
        if(file_pntr != NULL)
        {
          printf("File opened\n");
        }
        else
        {
          perror("Error");
        }
    
        return 0;
    }
  4. Теперь добавьте цикл, позволяющий вывести сообщение об ошибке, связанное с каждым цифровым кодом ошибки:
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    int main(void)
    {
        FILE *file_pntr;
        int i;
    
        file_pntr = fopen("nosuch.file", "r");
        if(file_pntr != NULL)
        {
          printf("File opened\n");
        }
        else
        {
          perror("Error");
        }
        for (i = 0; i < 44; i++)
        {
          printf("Error %d: %s\n", i, strerror(i));
        }    
    
        return 0;
    }
  5. Выполните программу:
    Сначала мы получили ошибку " Error: No such file or directory ", и после список стандартных ошибок(с их значениями цифровых кодов).
Диапазон значений цифровых кодов зависит от реализации. В системах на базе Linux он больше, чем на платформе Windows.

Получение даты и времени

Текущее системное время обычно определяется как количество секунд, прошедшее с момента наступления эпохи Unix (00:00:00 GMT 1 января 1970 года). Оно представляет собой текущую дату и время в соответствии с Григорианским календарем и называется «Календарное время». Специальные функции, предназначенные для работы с датой и временем, располагаются в файле <time.h> наряду со структурой tm, в которой хранятся компоненты даты и времени, как показано в таблице ниже:

Компоненты даты и времени структуры tm
Компонент Описание
int tm_sec Количество секунд, как правило, в диапазоне 0-59
int tm_min Количество минут, 0-59
int tm_hour Количество часов, 0-23
int tm_mday День месяца, 1-31
int tm_mon Количество месяцев, прошедших с января, 0-11
int tm_year Количество лет, прошедших с 1900 года
int tm_wday Количество дней, прошедших с воскресенья, 0-6
int tm_yday Количество дней, прошедших с 1 января, 0-365
int tm_isdst Признак летнего времени
Компонент tm_isdst имеет

Текущее количество прошедших секунд возвращает функция time(NULL) как тип данных time_t. Этот параметр может быть передан как аргумент в функцию localtime() для преобразования к формату компонентов структуры tm.

Полный список всех спецификаторов формата включен в разделе справочной информации в конце книги.

Подробное описание заголовочного файла <time_t> https://en.cppreference.com/w/c/header/time.html

Компоненты структуры разрешается вывести в стандартном формате даты и времени с помощью функции asctime(). В качестве альтернативы отдельные компоненты можно вывести с использованием специальных спецификаторов формата времени с помощью функции strftime(). Она принимает четыре аргумента, позволяющих указать массив символов, в котором будет храниться отформатированная строка даты, максимальная длина строки, текст и спецификаторы формата, необходимые для извлечения требуемых компонентов для структуры tm.

  1. Напишем программу datetime.c, добавим заголовочные файлы препроцессора, позволяющих подключить функции стандартной библиотеки ввода/вывода и функции работы со временем:
    #include <stdio.h>
    #include <time.h>
    
    int main(void)
    {
        char buffer[100]; /* Объявим массив символов */
        time_t elapsed;   /* Объявим переменную типа time_t */
        struct tm *now;   /* Объявим Указатель на структуру tm */
    
        return 0;
    }
  2. Далее в блоке функции main() получите текущее количество секунд, прошедшее с момента наступления эпохи Unix:
    #include <stdio.h>
    #include <time.h>
    
    int main(void)
    {
        char buffer[100];
        time_t elapsed;
        struct tm *now;
    
        elapsed = time(NULL); /* Текущее количество прошедших секунд возвращает функция time(NULL) */
    
        return 0;
    }
  3. Теперь преобразуйте это число к формату компонентов структуры tm:
    #include <stdio.h>
    #include <time.h>
    
    int main(void)
    {
        char buffer[100];
        time_t elapsed;
        struct tm *now;
    
        elapsed = time(NULL);
        now = localtime(&elapsed); /* Преобразование числа к формату компонентов структуры tm */
        printf("%s\n", asctime(now)); /* Выводим дату и время в стандартном формате */
    
        return 0;
    }
    Обратите внимание на то, что операция адресации, &, используется в этом преобразовании к местному времени для того, чтобы получить Календарное Время из типа данных time_t.
  4. Далее выведите отдельные компоненты даты и времени:
    #include <stdio.h>
    #include <time.h>
    
    int main(void)
    {
        char buffer[100];
        time_t elapsed;
        struct tm *now;
    
        elapsed = time(NULL);
        now = localtime(&elapsed);
        printf("\n%s\n", asctime(now));
    
        strftime(buffer, 100, "Todday is %A, %B %d.\n", now); /* Заполняем буфер форматированными компонентами в виде строки */
        printf("%s", buffer); /* Печать этой строки */
        strftime(buffer, 100, "The time is %I: %M %p.\n\n", now); /* Заполняем буфер форматированными компонентами в виде строки */
        printf("%s", buffer); /* Печать этой строки */
    
        return 0;
    }
  5. Выполните программу:
    Обратите внимание: Visual Studio Code в строках 14 и 16 не может корректно интерпретировать компоненты функции strftime(), однако исходный код компилируется без предупреждений и ошибок. Программа выполняется правильно.

Запуск таймера

Возможность получить текущее время до и после какого-нибудь события означает, что можно определить продолжительность события, найдя их разность. В заголовочном файле <time.h> содержится функция difftime(), отвечающая именно за это. Эта функция принимает два аргумента, они оба имеют тип данных time_t. Она вычитает второй аргумент из первого и возвращает разницу, выраженную в целом количестве секундах, которая имеет формат double.

ещё одним способом работы с временем является функция clock(), которая располагается в заголовочном файле <time.h>. Она возвращает время процессора, использованное с момента начала программы, выраженное в тиках. Эта функция может быть использована для того, чтобы приостановить выполнение программы путём запуска пустого цикла, который будет выполняться до тех пор, пока не окажется достигнут определённый момент в будущем.

Генерация случайных чисел

В заголовочном файле <stdlib.h> имеется функция rand(), которая генерирует псевдослучайное положительное целое число. По умолчанию она вернёт число в диапазоне от 0 до очень большого числа (как минимум 32767). Можно задать определенную верхнюю границу с помощью операции целочисленного деления. Например, чтобы получить число от 0 до 9, следует использовать выражение rand() % 9.

Чтобы установить минимальную нижнюю границу диапазона, нужно добавить некоторое значение к результату выражения. Например, чтобы получить число от 1 до 10, следует использовать выражение (rand() % 9) + 1.

Числа, генерируемые функцией rand(), не являются полностью случайными, поскольку она успешно создает одну и ту же последовательность чисел всякий раз, когда выполняется программа, использующая эту функцию. Для того чтобы сгенерировать другую последовательность чисел, необходимо указать зерно, с которого начинается последовательность. По умолчанию, исходное зерно равно 1, но его можно изменить, передав в качестве аргумента функции srand() другое целое число. Например, srand(8). Это приведёт к тому, что функция rand() сгенерирует другую последовательность чисел с использованием нового зерна, но последовательность всё равно будет повторяться при каждом запуске программы.

Чтобы каждый раз генерировать другую последовательность случайных чисел, аргумент, передаваемый функции srand(), должен быть чем-то другим, нежели статичным целым числом. Распространенное решение — использовать в качестве зерна текущее время в секундах: srand(time(NULL)).

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

Отображение диалогового окна

В операционной системе Windows программы, написанные на языке C, могут создавать графические компоненты с помощью специальных функций, предоставляемых интерфейсом Windows Application Programming Interface (WINAPI). Эти функции можно использовать в программе, добавив характерный для операционной системы Windows заголовочный файл <windows.h>.

В программе, написанной на языке C, для операционной системы Windows, обычная точка входа — функция main() — заменяется на специальную функцию WinMain():

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, 
int nCmdShow)

Эти аргументы всегда необходимы для того, чтобы программа осуществляла коммуникацию с операционной системой. Аргумент hInstance — это дескриптор, ссылка на программу, аргумент hPrevInstance, использовавшийся ранее в программировании для Windows, в наши дни может быть проигнорирован. Аргумент lpCmdLine — это строка, которая представляет собой все элементы, используемые в командной строке для компилирования приложения, а аргумент nCmdShow управляет способом отображения окна.

Наиболее простым в создании графическим компонентом является простое диалоговое окно, имеющее всего одну кнопку «ОК». Его можно создать, вызвав функцию MessageBox():

int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);

Аргумент hWnd — это дескриптор , ссылка на родительское окно, если таковое существует — если его нет, этот аргумент будет иметь значение NULL. Аргумент lpText — это строка с сообщением, которое будет отображено, а аргумент lpCaption представляет собой заголовок отображаемого диалогового окна. Наконец, аргумент uType может определять иконки и кнопки с помощью специальных константных значений, содержащихся в списке, разделенном вертикальной чертой. Например, константа MB_OK добавляет кнопку ОК, а иконку с восклицательным знаком можно добавить с помощью константы MB_ICONEXCLAMATION.

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

Заключение

Справочная информация

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

ASCII-коды символов

ASCII (абривиатура от American Standard Code for Information Interchange, американская стандартная кодировочная таблица) является стандартным представлением символов с помощью цифровых кодов. Коды для непечатаемых символов, первоначально разработанные для телетайпов, в наше время редко используются для первоначальных целей.

Существуют также расширенные наборы ASCII-кодов, лежащие в диапазоне 128–255 (здесь не представлены), которые содержат символы с акцентами и многие другие, но эти наборы отличаются друг от друга.

Обратите внимание на то, что символ, представленный кодом 32, на самом деле присутствует — это непечатаемый символ пробела. Символ, представленный кодом 127, — это символ delete.

Управляющие текстом символы
Код Символ Описание Код Символ Описание
0 NUL null 16 DLE Выход из канала передачи данных
1 SOH Начало заголовка 17 DC1 Управление устройством 1
2 STX Начало текста 18 DC2 Контроль устройств 2
3 ETX Конец текста 19 DC3 Контроль устройств 3
4 EOT Конец передачи 20 DC4 Контроль устройств 4
5 ENQ Прошу подтверждения 21 NAK Не подтверждаю
6 ACK Подтверждаю 22 SYN Синхронизация
7 BEL Звонок, звуковой сигнал 23 ETB Конец блока текста
8 BS Возврат на один символ 24 CAN Отмена
9 TAB Горизонтальная табуляция 25 EM Конец носителя
10 NL Новая строка 26 SUB Подставить
11 VT Вертикальная табуляция 27 ESC Начало управляющей последовательности
12 FF Прогон страницы, новая страница 28 FS Разделитель файлов
13 CR Возврат каретки 29 GS Разделитель групп
14 SO Изменить цвет ленты 30 RS Разделитель записей
15 SI Обратно к предыдущему коду 31 US Разделитель юнитов
Символы с шагом 32
Код Символ Код Символ Код Символ
32 space 64 @ 96 `
33 ! 65 A 97 a
34 " 66 B 98 b
35 # 67 C 99 c
36 $ 68 D 100 d
37 % 69 E 101 e
38 & 70 F 102 f
39 ' 71 G 103 g
40 ( 72 H 104 h
41 ) 73 I 105 i
42 * 74 J 106 j
43 + 75 K 107 k
44 , 76 L 108 l
45 - 77 M 109 m
46 . 78 N 110 n
47 / 79 O 111 o
48 0 80 P 112 p
49 1 81 Q 113 q
50 2 82 R 114 r
51 3 83 S 115 s
52 4 84 T 116 t
53 5 85 U 117 u
54 6 86 V 118 v
55 7 87 W 119 w
56 8 88 X 120 x
57 9 89 Y 121 y
58 : 90 Z 122 z
59 ; 91 [ 123 {
60 < 92 \ 124 |
61 = 93 ] 125 }
62 > 94 ^ 126 ~
63 ? 95 _ 127 delete
Термин ASCII-файл означает простой текстовый файл, похожий на те, что можно создать в приложении Блокнот (Notepad) операционной системы Windows.

Функции ввода и вывода <stdio.h>

Функции и типы, определённые в заголовочном файле <stdio.h> составляют примерно одну треть всей библиотеки языка C. Они используются для передачи данных в программу и из неё.

Поток — это источник данных, который завершается символом новой строки \n. Он может быть прочитан или записан путём открытия потока и освобожден путём его закрытия. Открытие потока возвращает указатель типа FILE, в котором хранится информация, необходимая для управления потоком.

Функции, перечисленные в ниже, выполняют операции над файлами.

Функции работы с файлами

Функции для форматирования выходных данных

Функции для форматирования входных данных

Спецификаторы формата выходных и входных данных

В языке программирования C префикс % указывает на спецификатор формата.

Перечислим все спецификаторы для функции printf():

Спецификаторы формата выходных данных
Символ Функция printf() выполняет преобразование
%d, %i Целочисленный тип данных, знаковое десятичное число
%o Целочисленный тип данных, беззнаковое восьмеричное число без нуля в начале
%x, %X Целочисленный тип данных, беззнаковое шестнадцатеричное число. для отображения в нижнем регистре (например, 0xff ), 0X для отображения в верхнем регистре (например, 0XFF )
%u Целочисленный тип данных, беззнаковое десятичное число
%c Целочисленный тип данных, один символ после преобразования к типу char
%s Символьный указатель на строку, которая закончится символом \0
%f Тип данных double в формате xxx.yyyyyy, где количество знаков после десятичной точки определяется заданной точностью. По умолчанию точность равна шести знакам
%e, %E Тип данных double в формате xx.yyyyyye±zz или xx.yyyyyyE±zz, где количество знаков после десятичной точки определяется заданной точностью. По умолчанию точность равна шести знакам
%g, %G Тип данных double, выводится как преобразование %e, %E или %f, выбирается самый короткий вариант
%p Адрес указателя в памяти
%n Не является преобразованием, сохраняет в целочисленном указателе количество символов, записанных к моменту вызова функции printf()
%% Не является преобразованием, выводит символ %

Перечислим все спецификаторы для функций scanf():

Спецификаторы формата входных данных
Символ Функция scanf() выполняет преобразование
d Целочисленный тип данных, знаковое десятичное число
i Целочисленный тип данных, число может быть восьмеричным (оно будет начинаться с 0 ) или шестнадцатеричным (оно будет начинаться с или )
o Целочисленный тип данных, восьмеричное число, которое может начинаться с 0
u Целочисленный тип данных, беззнаковое десятичное число
x Целочисленный тип данных, шестнадцатеричное число, которое может начинаться с 0
c Символы, которые будут помещены в указанный массив. Считывает количество символов, указанное как ширина строки (по умолчанию 1 ), не добавляя символ \0 к концу строки. Считывание прекратится, если встретится пробел
s Строка, не содержащая пробелов, которая будет помещена в указанный массив. Он должен быть достаточно большим, чтобы вместить все символы плюс завершающий символ \0
e, f, g Тип данных с плавающей точкой, число может иметь знак. После знака будут располагаться цифры в формате строки. Выражение может иметь десятичную точку, а также экспоненту ("е" или "Е"), после которой будет следовать число, которое может иметь знак
p Адрес в памяти, имеет тот же формат, что и выводимое функцией printf() значение под управлением преобразования %p
n Не является преобразованием. Хранит количество символов, считанных к моменту вызова функции scanf(), которое будет сохранено в целочисленном указателе
[…] Выполняет сравнение строки из потока со строкой, указанной в квадратных скобках, и добавляет символ \0
[^…] Выполняет сравнение всех символов ASCII из потока, исключая символы, указанные в квадратных скобках, и добавляет символ \0

Функции для ввода и вывода символов

Функции ввода и вывода данных напрямую из потоков

Функции fread() и fwrite(), содержащиеся в заголовочном файле <stdio.h>, наиболее эффективны для чтения и записи текстовых файлов целиком:

Функции для работы с ошибками

Множество стандартных функций библиотеки языка программирования C устанавливают индикаторы, когда происходит ошибка или когда они достигают конца файла. Эти индикаторы разрешается проверить с помощью перечисленных ниже функций. Также целочисленное выражение errno, определённое в заголовочном файле <errno.h>, может содержать код, с помощью которого можно узнать более подробную информацию об ошибке, произошедшей последней.

Функции позиционирования в файлах

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

Функции проверки символов <ctype.h>

Заголовочный файл <ctype.h> содержит функции для проверки символов. В каждом случае символ должен быть передан как аргумент функции. Она возвращает ненулевое значение (true), если символ соответствует заданным условиям, или 0 (false) — в противном случае. В дополнение, этот заголовочный файл содержит две функции, предназначенные для преобразования регистра букв. Все функции заголовочного файла <ctype.h>, а также их описание, перечислены в ниже:

Функции проверки символов <ctype.h>
Функция Описание
isalpha(c) Является ли символ буквой?
isalnum(c) Является ли символ буквой или цифрой?
iscntrl(c) Является ли символ управляющим?
isdigit(c) Является ли символ десятичной цифрой?
isgraph(c) Является ли символ печатаемым (не включая пробел)?
islower(c) Является ли символ буквой в нижнем регистре?
isprintf(c) Является ли символ печатаемым (включая пробел)?
ispunct(c) Является ли символ печатаемым (исключая пробелы, буквы и цифры)?
isspace(c) Является ли символ пробелом или символом отправки формы, новой строки, возврата каретки, горизонтальной или вертикальной табуляции?
isupper(c) Является ли символ буквой в верхнем регистре?
isxdigit(c) Является ли символ шестнадцатеричной цифрой?
int tolower(int c) Преобразует символ к нижнему регистру
int toupper(int c) Преобразует символ к верхнему регистру

Функции Арифметические <math.h>

В заголовочном файле <math.h> содержатся функции, которые выполняют математические расчеты. Все функции и их описания перечислены в таблице ниже. В этой таблице переменные x и y имеют тип double, а переменная n — тип int. Все функции возвращают значение типа double:

Функции Арифметические <math.h>
Функция Описание
sin(x) Возвращает синус переменной x
cos(x) Возвращает косинус переменной x
tan(x) Возвращает тангенс переменной x
asin(x) Возвращает арксинус переменной x
acos(x) Возвращает арккосинус переменной x
atan(x) Возвращает арктангенс переменной x
atan2(y, x) Возвращает угол (в радианах) между осью x и точкой y
sinh(x) Возвращает гиперболический синус переменной x
cosh(x) Возвращает гиперболический косинус переменной x
tanh(x) Возвращает гиперболический тангенс переменной x
exp(x) Возвращает значение е (основание натурального логарифма) в степени x
log(x) Возвращает натуральный логарифм переменной x
log10(x) Возвращает десятичный логарифм переменной x
pow(x, y) Возвращает значение x в степени y
sqrt(x) Возвращает квадратный корень из x
ceil(x) Возвращает минимальное целое число, не меньше x, имеющее тип double
floor(x) Возвращает максимальное целое число, не большее x, имеющее тип double
fabs(x) Возвращает модуль x
ldexp(x, n) Возвращает значение x, умноженное на 2 и возведенное в степень n
frexp(x, int *exp) Разбивает значение x на две части — возвращает мантиссу (лежащую в диапазоне 0.5 и 1) и сохраняет экспоненту по Указателю exp
modf(x, double *ip) Разбивает x на целую и дробную части — дробную часть возвращает, а целую сохраняет по Указателю ip
fmod(x, y) Возвращает остаток от деления x на y

Функции работы со строками <string.h>

Заголовочный файл <string.h> содержит следующие функции, которые могут быть использованы для сравнения или манипулирования текстовыми строками:

Функции работы со строками <string.h>
Функция Описание
char *strcpy(s1, s2) Копирует строку s1 в строку s2, затем возвращает (Указатель на) строку s1
char *strncpy(s1, s2, n) Копирует n символов строки s1 в строку s2, затем возвращает (Указатель на) строку s1
char *strcat(s1, s2) Добавляет строку s2 к концу строки s1, затем возвращает (Указатель на) строку s1
char *strncat(s1, s2, n) Добавляет n символов строки s2 к концу строки s1, затем возвращает (Указатель на) строку s1
char *strncmp(s1, s2) Сравнивает строки s1 и s2, затем возвращает:
  • Если s1 < s2 то отрицательное число
  • Если s1 == s2 то 0
  • Если s1 > s2 то Положительное число
char *strncmp(s1, s2, n) Сравнивает n символов строк s1 и s2, затем возвращает:
  • Если s1 < s2 то отрицательное число
  • Если s1 == s2 то 0
  • Если s1 > s2 то Положительное число
char *strchr(s, c) Возвращает указатель на первое включение символа c в строке s или значение NULL, если символ не найден
char *strrchr(s, c) Возвращает указатель на последнее включение символа c в строке s или значение NULL, если символ не найден
size_t strspn(s1, s2) Возвращает длину префикса строки s1, содержащего символы строки s2
size_t strcspn(s1, s2) Возвращает длину префикса строки s1, содержащего символы, отсутствующие в строке s2
char *strpbrk(s1, s2) Возвращает указатель на первое включение любого символа строки s2 в строке s1 или значение NULL, если ничего не найдено
char *strstr(s1, s2) Возвращает указатель на первое включение строки s2 в строке s1 или значение NULL, если ничего не найдено
size_t strlen(s) Возвращает длину строки s
char *strerror(n) Возвращает указатель на определяемую реализацией строку, связанную с кодом ошибки n
char *strok(s1, s2) Разбивает строку s1 на фрагменты, используя разделители, содержащиеся в строке s2
void *memcpy(s1, s2, n) Копирует n символов из строки s2 в строку s1, затем возвращает строку s1
void *memmove(s1, s2, n) Аналогична функции memcpy(), но она также работает в случае, если объекты пересекаются
int memcmp(s1, s2, n) Сравнивает первые n символов строк s1 и s2, возвращает значения, аналогичные значениям, которые возвращает функция strcmp()
void *memchr(s, c, n) Возвращает указатель на первое включение символа c в строке s или значение NULL, если символ не найден в первых n символах строки
void *memset(s, c, n) Заполняет первые n символов строки s указанным символом c

Функции Вспомогательные <stdlib.h>

Функции Вспомогательные <stdlib.h>
Функция Объяснение
double atof(const char *s) Преобразовывает строку s к типу double
int atoi(const char *s) Преобразовывает строку s к типу int
long atol(const char *s) Преобразовывает строку s к типу double
double strtod(const char *s, char **endp) Преобразовывает начальный фрагмент строки s к переменной типа double, игнорируя пробелы в начале этой строки. Указатель на остальную часть строки хранится в *endp
long strtol(const char *s, char **endp, int b) Преобразовывает начальный фрагмент строки s к переменной типа long с основанием b, игнорируя пробелы в начале этой строки. Указатель на остальную часть строки хранится в *endp
unsigned long strtoul(const char *s, char **endp, int b) Функция аналогична функции strtol(), за исключением того, что она выполняет преобразование к типу unsigned long
int rand(void) Возвращает псевдослучайное число, лежащее в диапазоне между 0 и зависящим от реализации максимумом (не меньше 32767)
void srand(unsigned int seed) Устанавливает зерно для новой последовательности случайных чисел, генерируемой функцией rand(). Исходное зерно равно 1
void *calloc(size_t nobj, size_t size) Возвращает указатель на фрагмент в памяти, выделенный для массива объектов nobj, имеющих размер size, или значение NULL, если требование не может быть выполнено. Выделенная память заполняется нулями
void *malloc(size_t size) Возвращает указатель на фрагмент памяти, выделенный для объекта, имеющего размер size, или значение NULL, если требование не может быть выполнено. Выделенная память никак не инициализируется
void *realloc(void *p, size_t size) Изменяет размер объекта, на который ссылается указатель p, на размер, указанный в переменной size. Эта функция возвращает указатель на новый фрагмент памяти или значение NULL, если операцию нельзя выполнить
void *free(void *p) Функция free() освобождает ранее выделенную память, но которую ссылается указатель p. Обратите внимание на то, что p должен быть указателем, содержащим результат вызова функций calloc(), malloc() или realloc().
void abort(void) Заставляет программу аварийно завершиться
void exit(int status) Заставляет программу завершиться обычным способом. Значение переменной status будет возвращено системе. Опционально переменная status может иметь значение EXIT_SUCCESS или EXIT_FAILURE
int atexit(voif (*fcn) (void)) Указывает, что функция fcn должна быть вызвана по завершении программы. Функция atexit() возвращает ненулевое значение, если произошла ошибка
int system(const char *s) Передает строку s операционной системе для обработки. Возвращаемое значение зависит от реализации
char *getenv(const char name) Возвращает строку переменной окружения с именем name или NULL, если строки, связанной с именем name не существует. Детали зависят от реализации
void *bsearch(const void *key, const void *base, size_t n, size_t size, int (*cmp), (const void *keyval, const void *datum)) Выполняет поиск элемента, соответствующего значению key, в диапазоне base[0]…base[n-1]. Эта функция возвращает указатель на найденный элемент. Если элемент не найден, будет возвращено значение NULL. Элементы в массиве base должны располагаться в порядке возрастания
void qsort(void *base, size_t n, size_t size, int (*cmp) (const void*, const void*)) Функция выполняет сортировку диапазона base[0]…base[n-1] объектов, имеющих размер size, в порядке возрастания. Функция сравнения cmp() является аналогом функции bsearch()
int abs(int n) Возвращает модуль числа n типа int
long labs(long n) Возвращает модуль числа n типа long
ldiv_t ldiv(long num, long denom) Делит число num на число denom и сохраняет результаты как члены структуры, имеющей тип ldiv_t. Член quot этой структуры содержит частное, а Член rem содержит остаток

Функции Диагностические <assert.h>

Функция assert(), расположенная в файле <assert.h>, может быть использована для диагностирования программы:

void assert(int expression);

Если значение выражения expression равно нулю во время вызова assert(expression), функция выведет сообщение об ошибке в поток stderr, например, такое:

Утверждение не выполнено: выражение, файл имяфайла, строка номерстроки

Затем функция assert() попытается завершить программу.

Функции для работы с аргументами <stdarg.h>

Заголовочный файл <stdarg.h> содержит функции, используемые для прохождения по списку аргументов функции, если вы не знаете их количество и тип. По своей природе эти функции должны быть реализованы как макросы внутри тела функции.

Список аргументов присваивается переменной, имеющей специальный тип данных va_list. Функции, перечисленные в следующей таблице, работают с переменной, имеющей имя args и тип va_list:

Функции для работы с аргументами <stdarg.h>
Функция Описание
va_start(va_list args, lastarg) Эта функция должна быть вызвана один раз, чтобы инициализировать переменную args, имеющую тип va_list, в позиции последнего известного аргумента lastarg
va_arg(va_list args, data-type) После того, как переменная va_list args была инициализирована путём вызова функции va_start, каждый успешный вызов функции va_arg() вернёт значение следующего аргумента в списке args как значение типа data-type
va_end(va_list args) Эта функция должна быть вызвана только один раз после того, как был обработан список va_list args, но до того, как функция завершит работу

Функции для работы с датой и временем <time.h>

Заголовочный файл <time.h> содержит функции для работы с датой и временем. Некоторые из них работают с «календарным временём», которое основано на Григорианском календаре. Оно выражается в секундах, прошедших с момента начала эры UNIX (00:00:00 GMT 1 января 1970 года).

Другие функции, содержащиеся в файле <time.h>, работают с местным временем, которое является преобразованием календарного времени согласно часовому поясу.

Тип данных time_t используется для описания как календарного, так и местного времени.

Структура с именем tm содержит компоненты календарного времени, которые перечислены в следующей таблице:

Компоненты календарного времени в Структуре tm
Имя Члена Структуры Описание
int tm_sec Секунды от начала минуты, 0-60
int tm_min Минуты от начала часа, 0-61
int tm_hour Часы от полуночи, 0-23
int tm_mday Число месяца, 1-31
int tm_mon Месяцы после января, 0-11
int tm_year Года с 1900
int tm_wday Дни с воскресенья, 0-6
int tm_yday Дни с 1 января, 0-365
int tm_isdst Признак летнего времени:
  • положителен, если действует летнее время
  • равен нулю, если действует зимнее время
  • отрицателен, если информация недоступна

Заголовочный файл <time.h> содержит функции, которые могут быть использованы для работы с датой и временем:

Функции работы с датой и временем в заголовочном файле <time.h>
Функция Описание
clock_t clock(void) Возвращает процессорное время, использованное программой с момента её запуска, или -1, если эти данные недоступны
time_t time(time_t *tp) Возвращает текущее календарное время или -1, если эти данные недоступны
double difftime(time_t time2, time_t time1) Возвращает разницу между time2 и time1, выраженную в секундах
time_t mktime(struct tm *tp) Преобразует местное время, содержащееся в структуре *tp, в календарное
char *asctime(const struct tm *tp) Преобразует время, содержащееся в структуре *tp, в стандартную строку
char *ctime(const time_t *tp) Преобразует календарное время в местное
struct tm *gmtime(const time_t *tp) Преобразует календарное время во всемирное координированное время (UTC или GMT)
struct tm *localtime(const time_t *tp) Преобразует календарное время, хранящееся в структуре *tp, в местное
size_t strftime(char *s, size_t smax, const char *fmt, const struct tm *tp) Форматирует время, хранящееся в структуре *tp, в выбранный формат *fmt

Функция strftime() форматирует выбранные компоненты структуры tm в соответствии с указанным спецификатором формата. Все возможные спецификаторы формата перечислены в таблице:

Спецификаторы формата времени функции strftime()
Спецификатор Описание
%a Сокращенное название дня недели
%A Полное название дня недели
%b Сокращенное название месяца
%B Полное название месяца
%c Представление местного времени
%d День месяца (01-31)
%H Часы (00-23)
%I Часы (01-12)
%j День года (001-366)
%m Месяц года (01-12)
%M Минуты (00-59)
%p Местный эквивалент времени до и после полудня
%S Секунды (00-61, с учетом секунды координации)
%U Номер недели в году (Воскресенье как первый день недели, 00-53)
%w Номер для недели (0-6, где 0 — это воскресенье)
%W Номер недели в году (Понедельник как первый день недели, 00-53)
%x Представление местной даты
%X Представление местного времени
%y Год без указания столетия (00-99)
%Y Год с указанием столетия
%Z Имя часового пояса (если доступно)

Функции переходов <setjmp.h>

Заголовочный файл <setjmp.h> используется для управления низкоуровневыми вызовами и предоставляет способы избежать обычной последовательности вызова и возврата.

Функции переходов <setjmp.h>
Функция Описание
int setjmp(jmp_buf env) Низкоуровневая функция, использующаяся в условных проверках для сохранения окружения в переменной env, и затем возвращающая 0.
void longjmp(jmp_buf env, int value) Восстанавливает окружение, которое было сохранено в переменной env с помощью функции setjmp(), как если бы функция setjmp() вернула значение value()

Функции Сигнальные <signal.h>

Заголовочный файл <signal.h> содержит функции, предназначенные для обработки исключительных состояний, которые могут возникнуть во время выполнения программы:

Функции переходов <setjmp.h>
Функция Описание
void (*signal (int sig, void (*handler) (int))) (int) Функция signal() определяет, как будут обработаны последующие вызовы. Обработчик может иметь значение SIG_DFL — значение по умолчанию, зависящее от реализации, или SIG_IGN, позволяющее проигнорировать сигнал. Переменная sig может иметь одно из следующих значений:
  • SIGABRT - аварийное прекращение работы
  • SIGFPE - арифметическая ошибка
  • SIGILL - некорректная инструкция
  • SIGINT - внешнее прерывание
  • SIGSEGV - попытка получить доступ к памяти за пределами выделен ного участка
  • SIGTERM - программе выслан запрос на завершение работы
  • SIG_ERR - Функция возвращает предыдущее значение обработчика заданного сигнала или SIG_ERR, если происходит ошибка. Когда сигнал sig срабатывает в следующий раз, сигнал восстанавливает своё первоначальное поведение, а затем вызывается обработчик сигнала. Если обработчик сигнала возвращает управление, выполнение программы продолжается с точки, где возник сигнал
int raise(int sig) Эта функция пытается отправить сигнал sig программе и возвращает ненулевое значение, если попытка не удалась

Константы пределов <limits.h>

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

Константы целочисленных пределов <limits.h>
Константа Значение переменной типа
CHAR_BIT Количество бит в char (8)
CHAR_MAX Максимальное char (UCHAR_MAX или SCHAR_MAX)
CHAR_MIN Минимальное char (0 или SCHAR_MIN)
INT_MAX Максимальное int (+32767)
INT_MIN Минимальное int (-32767)
LONG_MAX Максимальное long (+2147483647)
LONG_MIN Минимальное long (-2147483647)
SCHAR_MAX Максимальное signed char (+127)
SCHAR_MIN Минимальное signed char (-127)
SHRT_MAX Максимальное short (+32767)
SHRT_MIN Минимальное short (-32767)
UCHAR_MAX Максимальное unsigned char (+255)
UINT_MAX Максимальное unsigned int (+65535)
ULONG_MAX Максимальное unsigned long (+4294967295)
USHRT_MAX Максимальное unsigned short (+65535)

Константы с плавающей точкой <float.h>

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

Константы пределов чисел с плавающей точкой <float.h>
Константа Значение
FLT_RADIX Основание экспоненциального представления с плавающей точкой (2)
FLT_ROUNDS Округление до ближайшего числа
FLT_DIG Количество цифр точности (5)
FLT_EPSILON Наименьшее число х, где 1.0 + х != 1.0 (1Е-5)
FLT_MANT_DIG Количество цифр мантиссы FLT_RADIX
FLT_MAX Максимальное число с плавающей точкой (1Е+37)
FLT_MAX_EXP Максимальное число n, при котором выражение FLT_RADIXn - 1 корректно
FLT_MIN Минимальное число с плавающей точкой (1Е-37)
FLT_MIN_EXP Минимальное число n, при котором выражение 10n корректно
DBL_DIG Число знаков для числа типа double (10)
DBL_EPSILON Минимальное число х, при котором 1.0 + х != 1.0 (1Е-9)
DBL_MANT_DIG Количество цифр мантиссы FLT_RADIX
DBL_MAX Максимальное число типа double (1E + 37)
DBL_MAX_EXP Максимальное число n, при котором выражение FLT_RADIXn - 1 корректно
DBL_MIN Минимальное число типа double (1Е-37)
DBL_MIN_EXP Минимальное число n, при котором выражение 10n корректно

Основы программирования на языке С

Актуальный список ВСЕХ ключевых слов языка С

45 ключевых слов, перечисленные в следующей таблице, являются наиболее часто используемыми словами языка программирования C. Они имеют особое значение в программах, написанных на языке C, и не могут быть использованы для любых других целей.

Ключевые слова
alignas Задаёт выравнивание памяти для переменной, структуры или массива.
alignof Возвращает требуемое выравнивание в байтах для типа или переменной.
auto Объявление переменных с локальной областью видимости в пределах блока кода.
bool Определение типа данных Булевого значения: либо true либо false.
break Завершает цикл или оператор switch и передает управление следующему оператору, который следует за циклом или switch.
case Используется в операторе switch для указания конкретного условия(значения соответствия).
char Определение типа данных символ/ы.
const Объявление переменной как константы.
constexpr Спецификатор хранения, которым помечают объект-переменную как константное выражение.
continue Переход к следующей итерации цикла.
default Используется в операторе switch в качестве условия по умолчанию, т.е. когда ни один из других вариантов не совпадает.
do Используется с оператором while для создания цикла, называемого циклом do..while, первая итерация выполнится в любом случаи, и лишь потом будет проверено условие.
double Определение типов данных с плавающей точкой с двойной точностью.
else Указание на альтернативный блок кода, который будет выполнен, если условие оператора if ложно.
enum Определение набора именованных целых констант.
extern Объявление переменной, определенной в отдельном файле.
false Булево значение - Ложь/Нет/0.
float Определение типа данных с плавающей точкой с одинарной точностью.
for Задание цикла, который выполняет блок кода определённое количество раз.
goto Передача управления(безусловный переход) к помеченному меткой оператору в программе.
if Указание условного оператора, который выполняет блок кода, если определённое условие истинно.
inline Подсказка компилятору, что тело функции можно встроить непосредственно в место вызова, чтобы уменьшить накладные расходы на вызов функции.
int Определение целочисленного типа данных.
long Определение целочисленного типа данных с расширенным диапазоном.
nullptr Служит для обозначения константы нулевого указателя с чёткой типовой безопасностью.
register Объявление переменной как регистровой переменной, сохраняемой в регистре ЦП для более быстрого к ней доступа.
restrict Сообщает компилятору, что данный указатель является единственным способом доступа к объекту в памяти в течение его области видимости.
return Возврат значения из функции.
short Определяет целочисленный тип данных с коротким диапазоном, меньше чем int.
signed Определяет целочисленный тип данных со знаком.
sizeof Указывает размер данных в байтах: переменной или типа.
static Определяет локальную область видимости переменной, для передачи её значения между вызовами функций.
static_assert Оператор утверждения, который вызывает ошибку компиляции, если заданное условие ложно.
struct Определяет тип данных структура, которая содержит несколько переменных разных типов данных.
switch Оператор ветвления хода выполнения программы, т.е. выбор ветки программы на основе значения выражения.
thread_local Создаёт отдельный экземпляр переменной для каждого потока, так что разные потоки не делят одно и то же значение.
true Булево значение - Истина/Да/1(или не нулевое значение).
typedef Определяет новое имя для существующего типа данных.
typeof Позволяет получить тип выражения или переменной и использовать его при объявлении других объектов.
typeof_unqual Возвращает тип выражения без модификаторов const, volatile и restrict.
union Содержит разные типы данных, которые используют одно и то же адресное пространство.
unsigned Определяет целочисленный тип данных без знака.
void Указывает, что функция не возвращает значение конкретного типа данных(когда записано перед именем функции).
volatile Указывает, что переменная может быть изменена внешним источником и не должна оптимизироваться компилятором.
while Задание цикла, который выполняет блок кода заданное количество раз, но сначала должно выполнятся условие.

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

Управляющие последовательности
Последовательность Действие
\n Новая строка
\t Табуляция
\v Вертикальная табуляция
\b Возврат на шаг
\r Возврат каретки
\f Отправка формы
\a Звонок(Сигнал-тревога)
\\ Обратный слеш
\? Вопросительный знак
\' Одинарная кавычка
\" Двойная кавычка
\xhh Шестнадцатеричное число
\000 Восьмеричное число
\0 Символ-терминатор


Блок-схема как основа визуального представления алгоритма

Ранее мы уже рассмотрели фигуры с помощью которых создаются блок-схемы. Продемонстрировали множество примеров того как выглядит блок-схема на основе исходного кода программы. И даже привели несколько примеров создания кода на основе построенной блок-схемы.

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

Блок-схема сама по себе это изображение фигур соединённых линиями. Где фигуры представляют собой логические блоки внутри которых записаны утверждения на языке программирования, в нашем случаи на языке C. Линии же обозначают направление потока движения исполнения кода программы.

Зачем вообще нужны блок-схемы если в редакторе кода мы просто можем читать текст программы последовательно - строка за строкой как книгу? Дело в том, что исходный код - это сильно упрощённый формальный язык. Вспомните, в языке C всего то 60 ключевых слов, а наиболее часто применяются менее 45 слов. Много ли можно рассказать имея в лексиконе 60 слов? Более того, благодаря компилятору - невообразимо огромное количество абстрактных идей теории вычисления скрыты от программиста - так исходный код действительно будучи компактным может описывать сложнейшие алгоритмы.

Чем более высокий уровень языка программирования тем больше в нём ключевых слов, более абстрактные идей можно формализовать ключевыми словами - т.е. описать на языке программирования так называемые Объекты и их Свойства, Поведения, Взаимодействия.

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

Таким образом сложные концепции разбиваются на менее сложные, те на простые, а простые на базовые.

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

Алан Мэ́тисон Тью́ринг(23 июня 1912, Вестминстер — 7 июня 1954, Чешир) — британский математик, логик и криптограф, оказавший существенное влияние на развитие информатики. Офицер ордена Британской империи (OBE, 1945), член Лондонского королевского общества (1951)

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

  1. Назначение переменной значения и возможность переназначать его. Классическое описание объявления и инициализации переменной.
  2. Ветвление исполнения программы. Т.е. в зависимости от условия выполнять ту или иную ветку кода. (Линии в блок-схеме)
  3. Цикличность. Способность повторять блок кода заданное количество раз или делать это бесконечно. Циклы.
  4. Функции. Способность определять функцию и вызывать её. Фактически, Функции называют по разному: группа строк кода, блок кода, модуль, ветка.

Машина обладающая такими свойствами, может реализовать(как теперь принято говорить) Тьюринг-Полный Алгоритм.

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

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

Ниже языка C, как мы уже узнали из процесса компилирования, находится язык Ассемблера. Его ключевые слова ещё проще и их ещё меньше, но и он является Тьюринг-Полным.

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

Другой учёный, Джон фон Нейман, разработал как раз "железную" основу, на которой строятся большинство современных вычислительных машин.

Джон фон Не́йман (или Иоганн фон Нейман, нём. Johann von Neumann; при рождении Я́нош Ла́йош Нейман, венг. Neumann János Lajos);(28 декабря 1903, Будапешт — 8 февраля 1957, Вашингтон) — венгеро-американский математик, физик и педагог еврейского происхождения, сделавший важный вклад в квантовую физику, квантовую логику, функциональный анализ, теорию множеств, информатику, экономику и другие отрасли науки. Наиболее известен как человек, с именем которого связывают архитектуру большинства современных компьютеров (так называемая архитектура фон Неймана)

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

Наглядное изображение идей в виде рисунков и фигур уходит ещё глубже в истории развития человечества.

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

Изображение в специфической области деятельности есть письменность этой области.

Блок-схема в области информатики есть письменность этой области.

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

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

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

Всё наше медиа-пространство заполнено изображениями, видео-аудио контентом, и конечно же текстами.

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

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

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

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

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

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

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

Давайте начнём изучать такие паттерны с самых базовых.

Блок-схемное представление правил двоичной логики

Самые простые операции ветвления кода в зависимости от выполнения или не выполнения условий:

Если и А и B истинные значения, тогда выполнить действие.
if(A && B)
{
   ACTION;
}
Если и А и B истинные значения, тогда выполнить действие. Иное представление блок-схемы, того же самого алгоритма.
if(A && B)
{
   ACTION;
}
Если и А и B истинные значения, тогда выполнить действие. Ещё одно представление блок-схемы, того же самого алгоритма.
if(A && B)
{
   ACTION;
}
Обратите внимание, что визуализация взаимосвязей между фигурами, названия линий потока выполнения программы, имеют куда больше способов представления алгоритма чем на формальном языке программирования. В этом основное преимущество блок-схемы - её наглядность. В вашем распоряжении весьма гибкий инструментарий для описания любой задачи, которую нужно привести к виду рабочего алгоритма. Такая гибкость - весомое преимущество при составлении алгоритмов претендующих на визуализацию довольно сложных алгоритмов, и вскоре вы в этом неоднократно убедитесь.
Если и А и НЕ B истинные значения, тогда выполнить действие. Разные представления блок-схемы, того же самого алгоритма.
if(A && !B)
{
   ACTION;
}
Если или А или B истинно, тогда выполнить действие. Разные представления блок-схемы, того же самого алгоритма.
if(A || B)
{
   ACTION;
}
Если или А или НЕ B истинно, тогда выполнить действие. Разные представления блок-схемы, того же самого алгоритма.
if(A || !B)
{
   ACTION;
}
Если и НЕ А и НЕ B истинно, тогда выполнить действие. Разные представления блок-схемы, того же самого алгоритма.
if(!A && !B)
{
   ACTION;
}

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

Пример алгоритма, который я разработал для бизнес задач мобильного приложения для путешественников. Обратите внимание, чем краснее становится нижняя горизонтальная стрелка, тем всё более худшей становится ситуация для бизнеса - Самый крайний правый блок кода, это когда пользователь покидает приложение. А самый левый блок кода - наилучший исход для бизнеса - пользователь платит деньги в приложении. Эта блок-схема представляет этап проектирования приложения, постепенно все фигуры будут заменяться фигурами с кодом на языке программирования, их будет становится больше и больше, до тех пор пока будет написана каждая функция отвечающая за ту или иную логику работы приложения.
Если или НЕ А или НЕ B истинно, тогда выполнить действие. Разные представления блок-схемы, того же самого алгоритма.
if(!A || !B)
{
   ACTION;
}

Лестнице-образные блок-схемы алгоритмов

Имеют вид ступенек фигур условия расположенных сверху вниз и на право.

Если ((и А и B) или C) истинно, тогда выполнить действие.
if ((A && B) || C)
{
   ACTION;
}
Если ((и А и B) или НЕ C) истинно, тогда выполнить действие. Разные представления блок-схемы, того же самого алгоритма.
if ((A && B) || !C)
{
   ACTION;
}
Если или А или B или C истинно, тогда выполнить действие.
if (A || B || C)
{
   ACTION;
}
Если ((и А и B) или (НЕ C или D)) истинно, тогда выполнить действие. Разные представления блок-схемы, того же самого алгоритма.
if ((A && B) || (!C || D))
{
   ACTION;
}

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

Это возможно только потому, что мы применяем строжайшие законы бинарной логики к фигурам. В блок-схеме ниже переменная A проверяется по условию два раза в разных местах блок-схемы. Как же будет выглядеть исходный код?

Если ((и А и B) или (C и НЕ A)) истинно, тогда выполнить действие. Разные представления блок-схемы, того же самого алгоритма.
if ((A && B) || (C and !A))
{
   ACTION;
}

Исключающее ИЛИ - не всегда легко запомнить двоичную логику данной бинарной операции, с помощью блок-схемы это элементарно:

Если А не равно B истинно, тогда выполнить действие.
if (A == B)
{
   
}
else
{
   ACTION;
}

Блок-схемы Циклов

Приведём примеры блок-схем в которых визуально и подробно рассмотрим множество реализаций Циклов.

Самый простой Цикл for с инициализацией счётчика, проверкой счётчика по условию, инкрементом счётчика.
for (i = 1; i <= 3; i++)
{
   printf("%d\n", i);
}
Самый простой Цикл while с инициализацией счётчика, проверкой счётчика по условию, инкрементом счётчика.
int i = 0;               
while (i <= 3)
{
   printf("%d\n", i);
   i = i + 1;
}
Самый простой Цикл do..while с инициализацией счётчика, инкрементом счётчика, проверкой счётчика по условию.
int i = 0;               
do
{
   printf("%d\n", i);
   i++;
}while (i <= 3);
Цикл do..while с инициализацией счётчика, инкрементом счётчика, проверкой счётчика по условию. Дополнительно применяется проверочное условие переменной на кратность её двум с помощью функции условие if, если условие возвращает истинность тогда применяется оператор continue - и цикл продолжается минуя печать в консоль. Таким образом в консоль печатаются только числа не кратные двум - т.е. нечётные.
int i = 0;               
do
{   
   i++;
   if (i % 2 == 0)
   {
      continue;
   }
   printf("%d\n", i);

}while (i < 10);
Цикл do..while с инициализацией счётчика, инкрементом счётчика, проверкой счётчика по условию. Дополнительно применяется проверочное условие переменной на кратность её двум с помощью функции условие if, если условие возвращает истинность тогда применяется оператор break - и цикл досрочно завершается минуя печать в консоль. Таким образом в консоль печатаются только числа не кратные двум - точнее всего одно нечётное число 1.
int i = 0;               
do
{   
   i++;
   if (i % 2 == 0)
   {
      break;
   }
   printf("%d\n", i);

}while (i < 10);
Составим алгоритм решения последовательности 1 + 1/2 + 1/3 + 1/4 + .. + 1/N. Необходимо ввести целое число N - обозначающее количество суммируемых дробей. Если введено число 1, 0 или отрицательное - сразу вывести в консоль сообщение о результате равном 1, так как 1/1 = 1, делить на ноль нельзя, а при вводе отрицательного значения ... выясните что будет - каково будет поведение цикла?
#include <stdio.h>
int main()
{
    int n = 1;
    float accum = 0;
    printf("Enter number: \n");
    scanf("%i", &n);
    if (n >= 1)
    {
        for (; n >= 1; n--)
        {
            accum += (float)1 / n;
        }
        printf("Answer is %f\n", accum);
    }
    else
    {
        printf("Answer is 1 if you enter number 0 or negative.\n", accum);
    }
    return 0;
}
Составим алгоритм для поиска всех чисел кратных 7 в пределах от больше 100 и до 200. Каждое число кратное 7 суммируем. Напечатаем в консоль два значения: Сумму и Количество чисел кратных 7. С целью ускорения выполнения алгоритма, когда число не кратно 7 - с помощью команды continue; переходим к следующей итерации цикла.
#include <stdio.h>
int main()
{
    int counter = 0;
    int sum = 0;
    int i;
    for (i = 101; i < 200; i++)
    {
        if (i % 7 == 0)
        {
            sum += i;
            counter++;
        }
        else
        {
            continue;
        }
    }
    printf("Summation is %d.\n", sum);
    printf("There are %i numbers.", counter);
    return 0;
}
Составим алгоритм решения квадратных уравнений ax2 + bx + c = 0.
#include <stdio.h>
#include <math.h>

int main()
{
    float a, b, c, x1, x2;
    printf("Enter three constants a, b and c of the equation "
           "ax^2 + bx + c=0: \n");
    scanf("%f %f %f", &a, &b, &c);
    if (a == 0)
    {
        if (b == 0)
        {
            printf("Sorry, there is no solution to the equation.\n");
        }
        else
        {
            printf("The only root of the equation is: %0.2f\n", -(c / b));
        }
    }
    else if ((pow(b, 2) - (4 * a * c)) < 0)
    {
        x1 = -b / (2 * a);
        x2 = sqrt((4 * a * c) - pow(b, 2)) / (2 * a);
        printf("Two imaginary roots of the equation are:\n");
        printf("(l) %0.2f + i%0.2f\n(2) %0.2f - i%0.2f\n", x1, x2, x1, x2);
    }
    else if ((pow(b, 2) - (4 * a * c)) == 0)
    {
        printf("The only root of the equation is: %0.2f\n", -b / (2 * a));
    }
    else
    {
        x1 = (-b + sqrt(pow(b, 2) - (4 * a * c))) / (2 * a);
        x2 = (-b - sqrt(pow(b, 2) - (4 * a * c))) / (2 * a);
        printf("The roots of the equation are: %0.2f and %0.2f\n", x1, x2);
    }
    return 0;
}

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

Составим алгоритм для суммирования разрядов целого десятичного числа. Например: для числа 123456789 - сумма его всех разрядов равна 45.
#include <stdio.h>

int main()
{
    int num = 0;
    int sum = 0;

    printf("Enter number: ");
    scanf("%d", &num);

    while (num / 10 != 0)
    {
        sum += num % 10;
        num /= 10;
    }
    sum += num;
    printf("Summation of all digits of number is: %d\n", sum);

    return 0;
}
Составим алгоритм работы простейшего калькулятора. И напишем код для реализации алгоритма.
#include <stdio.h>

int main()
{
   float num1;
   float num2;
   char ch = 'y';
   char op;

   while (ch == 'y')
   {
      printf("Enter First Operand: \n");
      scanf("%f", &num1);
      printf("Enter Operation +, -, *, /: \n");
      scanf(" %c", &op); /* Здесь стоит пробел перед %c специально для того чтобы пропустить его после ввода предыдущего значения */
      printf("Enter Second Operand: \n");
      scanf("%f", &num2);

      switch (op)
      {
         case '+':
            printf("%.3f + %.3f = %.3f\n", num1, num2, (num1 + num2));
            break;

         case '-':
            printf("%.3f - %.3f = %.3f\n", num1, num2, (num1 - num2));
            break;

         case '*':
            printf("%.3f * %.3f = %.3f\n", num1, num2, (num1 * num2));
            break;

         case '/':
            if (num2 == 0)
            {
               printf("Cannot divide by 0\n");
            }
            else
            {
               printf("%.3f / %.3f = %.3f\n", num1, num2, (num1 / num2));
            }
      }
      printf("New statement? (y/n) \n");
      scanf(" %c", &ch);
   }

   return 0;
}
Составим алгоритм работы функции switch(). И напишем код для реализации алгоритма.
#include <stdio.h>

int main(void)
{
    int i = 2;
    switch (i)
    {
    case 1:
        printf("case 1");
        break;
    case 2:
        printf("case 2");
        break;
    default:
        printf("default");
    }
    return 0;
}
Составим алгоритм работы функций switch() внутри функции switch(). И напишем код для реализации алгоритма.
#include <stdio.h>

int main(void)
{

    enum TRANSPORT
    {
        BUS = 1,
        TRAIN,
        AIR
    };
    enum PRICE
    {
        LOW = 1,
        MEDIUM,
        LUX
    };
    enum COUNTRY
    {
        USA = 1,
        CANADA,
        MEXICA
    };
    enum CATEGORY
    {
        TRANSPORT = 1,
        PRICE,
        COUNTRY
    };
    int category = TRANSPORT;
    int type = BUS;
    switch (category)
    {
    case 1:
        printf("Transport: \n");
        switch (type)
        {
        case 1:
            printf("bus\n");
            break;
        case 2:
            printf("train\n");
            break;
        case 3:
            printf("air\n");
            break;
        default:
            printf("Transport not selected.\n");
        }
        break;
    case 2:
        printf("Price: \n");
        switch (type)
        {
        case 1:
            printf("low\n");
            break;
        case 2:
            printf("medium\n");
            break;
        case 3:
            printf("lux\n");
            break;
        default:
            printf("Price not selected.\n");
        }
        break;
    case 3:
        printf("Country: \n");
        switch (type)
        {
        case 1:
            printf("usa\n");
            break;
        case 2:
            printf("canada\n");
            break;
        case 3:
            printf("mexica\n");
            break;
        default:
            printf("Country not selected.\n");
        }
        break;
    default:
        printf("Category not selected.");
    }
    return 0;
}