Это обширный учебник и пользоваться им довольно просто - продвигайтесь последовательно по материалу, все
программы вводите
исключительно самостоятельно печатая их руками, не копируйте и не вставляйте примеры исходного кода в редактор.
Чем больше вы печатаете руками, тем быстрее в вашем мозге начнут образовываться связи между Концепциями,
Понятиями, Абстракциями языка С, его ключевыми словами, синтаксисом и вашими памятью и мышлением.
Как говорила моя преподавательница по промышленной электронике: Набивайте руки...
Для поиска любого текста в Учебнике,
нажмите комбинацию горячих клавиш Ctrl+F, и вверху над
страницей откроется поле ввода текста для поиска, введите текст и нажмите ENTER:
Находите любой текст в Учебнике. С помощью комбинаций горячих клавиш Ctrl+G перейдите
к следующему результату поиска, а с помощью Ctrl+Shift+G к предыдущему
результату поиска. Или просто нажмите курсором на соответствующие кнопки (стрелка вверх и стрелка вниз)
возле поля ввода текста.
Воспользуйтесь фильтром (если он есть в вашем обозревателе)
для более точного поиска текста:
Уточните поиск с помощью фильтра.
Учебник рассчитан на начинающих изучение языка С с полного нуля и является исчерпывающим руководством
для того чтобы научится программировать на С, понять и изучить устройство любой современной электронной
вычислительной машины. Ни какие предварительные знания не требуются.
По мере изучения материала, темы будут становиться сложнее. Без хорошего усваивания предыдущих тем, не
переходите к изучению последующих - не спешите.
В учебнике с помощью примеров программ и иллюстраций, показывающих результаты работы исходного кода,
разбираются все ключевые аспекты языка С - от базовых до сложных. Описано как установить Компилятор и Редактор
Кода для языка С и работать в нём.
Учебник подходит для:
программистов решивших получить достаточно полное представление о языке С.
студентов и энтузиастов решивших начать изучать написание программ на языке С.
изучающих процедурное, структурное и логическое программирование для:
полноценных настольных компьютеров.
встраиваемых систем на основе микроконтроллеров.
тех кто хочет изучать программирование как интеллектуальное и практическое хобби позволяющее держать мозги
в тонусе.
Добро пожаловать в мир языка C. В этой главе
показывается, как создать вашу первую программу на
языке C, затем скомпилировать
её в исполняемый файл и в итоге выполнить.
— это компактный компьютерный язык программирования
общего назначения, часто называемый процедурным,
созданный Деннисом Макалистером
Ритчи (Dennis MacAlistair Ritchie) для операционной системы Unix на компьютере
Digital Equipment Corporartion PDP-11 в 1972-м году.
Де́ннис Макалистэйр Ри́тчи (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++, Java и
Python, заимствовали свой синтаксис из C. Поэтому его считают их базовым языком.
Его достаточно просто учить, так как основан на английском, широко используемом в мире и простом для
понимания языке.
Программы, которые вы пишете на C, компилируются и выполняются гораздо быстрее, чем на других языках. Это
связано с тем, что он не записывает мусорные значения, не проверяет границы индексов массивов, не выполняет
проверки типов во время выполнения, не обрабатывает исключений и других дополнительных накладных расходов.
С компилируемый язык, а компилятор значительно быстрее интерпретатора.
Он портативен, поскольку его программы можно легко перенести на компьютеры с другими архитектурами. Он
является
аппаратно-независимым по своей природе.
Не смотря на то, что современные Микроконтроллеры управляются встроенными программами, написанными на
разных языках,
в профессиональной среде и в промышленности: Роботы, компьютеры и автоматизация используют эти
микроконтроллеры с программами на С,
так как язык тесно взаимодействует с аппаратным обеспечением.
Благодаря своей скорости многие операционные системы и системные программы написаны на C, такие как UNIX.
Также он используется для разработки игр благодаря своей хорошей скорости.
Существует сходство между изучением языка C и английского языка. Когда мы учим английский, мы сначала изучаем
символы или алфавит, а затем комбинируем алфавит, чтобы сформировать слово. Кроме того, сочетание слов образует
предложение, а затем набор предложений формирует абзац, следуя грамматическим правилам. Аналогично, в C сначала
мы изучаем символы, а затем эти символы образуют токены (переменные, константы, ключевые слова и так далее).
Кроме того, эти токены образуют инструкции, операторы или команды, и эти операторы объединяются вместе, чтобы
создать утверждения, а из них строятся программы.
Зачем изучать программирование с помощью языка C?
Программы, написанные на языке C двадцать лет назад,
в наши дни выполняются так же успешно, как и в те времена. - Майк МакГрат
Язык C существует уже долгое время, он застал создание новых языков
программирования вроде Java, C++ и C#. Многие из них основаны на
языке C, по крайней мере, отчасти, и при этом более громоздкие. C, как более компактный язык, лучше подходит
для того, чтобы начать программировать, поскольку изучить его проще.
Как только вы освоите основные принципы программирования на языке C, вы сможете переключиться на изучение
более
новых языков программирования. Например, язык C++ является расширением языка C.
C++ может быть труден в изучении с нуля, если вы сперва не освоите программирование на C.
Однако не стоит обольщаться, С невероятно мощный и развитый язык, и чтобы стать экспертом программистом на С
потребуется постоянная практика.
На С можно писать быстрые графические приложения, обработку многоканального аудио и работу с цифровыми
музыкальными
инструментами с помощью таких библиотек как SDL, SFML, RayLib и др. На
современных компьютерах возможно генерировать тысячи кадров в секунду в графических приложениях и сотни
кадров
в секунду в видео-играх.
Несмотря на дополнительные возможности, доступные в новых языках, C остаётся популярным, поскольку он гибок
и эффективен. Сегодня он используется на большом количестве платформ, начиная с микроконтроллеров и
заканчивая продвинутыми научными системами.
Программисты по всему миру используют язык C, поскольку он позволяет им получить максимальный контроль над
выполнением программ, а также повышает их эффективность. Многие современные языки программирования, с целью
ускорения вычислений сложных математических вычислений, поддерживают встраивание кода на С внутрь их кода.
С даже можно применять для обработки алгоритмов внутри Веб-страниц. С помощью проекта Web-Assembly, код на C
преобразуется в быстрый вэб-ассемблер, вызывается командами языка JavaScript и запускается на машине
пользователя.
Естественно, скорость работы таких приложений высочайшая и сопоставима с настольными приложениями
написанными
на С/С++, но работающих в окне вашего обозревателя интернета.
С - главный язык программирования микроконтроллеров, а благодаря мощным компиляторам - размер программы на С
сжимается многократно, что критически важно при ограниченных объёмах памяти в микроконтроллере.
Практически все драйверы устройств вычислительных систем пишут на С/С++.
У С есть и своя специфика:
Алгоритмический язык
В чистом виде, С не поддерживает Объектно-ориентированную парадигму программирования(ООПП). Это
значит,
что программы написанные на С это императивные инструкции(инструкции написанные программистом
указывают
что конкретно
делать процессору шаг за шагом - строка за строкой исходного кода) и функции состоящие из таких
инструкций - чётко
и напрямую манипулирующие данными в памяти машины. С это логический, структурный язык. См. далее.
Типы Данных
Программист должен уделять особое внимание размеру данных. См. далее.
Язык среднего уровня
На С пишут программы аппаратного уровня, т.е. код программы по ходу её выполнения, манипулирует
процессором и памятью. С занимает место между Ассемблером и языками высокого уровня(С++, Java и др.).
Вся ответственность по контролю за выполнением операций с памятью и данными полностью лежит на
программисте. См. далее.
Язык очень простой
Простые вещи - мало кода. Сложные вещи - много кода.
Сначала изучите С
Перед тем как изучать другой язык Высокого уровня, крайне
желательно понять как работает "железо" вычислительной машины на уровне языка С.
Стандартные библиотеки языка С
Функция — это фрагмент кода (набор инструкций), который может быть
повторно использован в программе на языке C. Наборы функций объединяют в библиотеки. Каждая функция имеет
свой заголовок, по этому такие библиотеки часто называют заголовочными файлами.
Для ANSI C определены несколько стандартных библиотек, содержащие опробованные и протестированные
функции, которые могут быть использованы в ваших собственных программах, написанных на
языке C.
Библиотеки содержатся в заголовочных файлах (англ. header - заголовок), каждый из которых
имеет расширение .h Названия стандартных заголовочных файлов
и их краткие описания приведены ниже. Интерфейс стандартной библиотеки C определяется следующей коллекцией
заголовочных файлов:
Которые могут быть использованы для диагностики программы
<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.
После установки MSYS2 введите команду pacman -S mingw-w64-ucrt-x86_64-gcc и нажмите
ENTER
Откройте Visual Studio Code
Установите расширения C/C++ для VS Code от Корпорации Microsoft. Вы можете установить расширения
C/C++, выполнив поиск по
запросу "C/C++" в представлении "Расширения"(англ. EXTENSIONS) .
C/C++ IntelliSense, debugging, and code browsing.
C/C++ Extension Pack
C/C++ Themes
Расширения для редактора кода позволяют писать программисту исходный код в более комфортных
условиях.
Проверьте установку MinGW, откройте командную строку и введите по очереди команды, нажимая
Enter после каждой:
gcc --version
g++ --version
gdb --version
gcc -v
Ваша версия может отличаться, обычно в большую сторону версии. На изображении версия 15.1.0
Если никаких ошибок и проблем не возникло, напишем первую программу:
Написание первой программы
В языке программирования C утверждения(инструкции), которые должны быть выполнены, располагаются
внутри функций, определяемых с использованием следующего синтаксиса:
После того, как функция вызывается с целью выполнения утверждений, которые в ней содержатся, она
может
вернуть значение вызывающей стороне. Это значение должно иметь тип, указанный перед именем
функции.
Программа может содержать одну или несколько функций, при этом
в ней должна присутствовать функция, которая называется main().
Функция main()
— это стартовая точка всех программ, написанных на языке C, и компилятор не будет компилировать код,
если не найдет внутри программы функцию main().
Другим функциям программы можно давать любые имена, содержащие
буквы, цифры и символы подчеркивания, но следует иметь в виду, что
имя функции не должно начинаться с цифры. Также следует избегать
использования в качестве имён функций ключевых слов языка C, приведенных в конце книги.
Круглые скобки (), следующие за именем функции, могут содержать
значения, которые используются функцией. Эти значения имеют вид
разделённого запятыми списка и называются аргументами функции.
Фигурные скобки {} содержат утверждения, которые должны быть выполнены при вызове функции. Каждое
утверждение обязано заканчиваться точкой с запятой ; .
Традиционно при изучении языка программирования в первую очередь пишут программу, выводящую на экран
сообщение Hello World :
Откройте Проводник Windows и Создайте Папку с именем Learn_C на любом диске
Внутри Папки с именем Learn_C создайте папку с именем hello
Откройте Visual Studio Code
Нажмите вверху слева File
Нажмите Open Folder
Укажите местоположение папки с именем hello
Нажмите кнопку снизу справа в окне проводника "Выбор Папки"
Может появится предупреждение о доверии автору содержимого внутри папки с именем
hello
Так как Автор - вы, то смело ставим галочку и жмём синюю кнопку "Yes, I trust the authors", тем
самым подтверждаем доверие к содержимому в папке.
Вверху слева видно, что вы находитесь внутри папки с именем hello
Создадим наш текстовый файл исходного кода на языке С с именем hello.c . Файл
автоматически будет создан внутри папки с именем hello :
Нажмите на иконку "New File"
Появилось поле ввода: введите имя файла и через точку его расширение
Введём hello.c
Жмём ENTER
В текстовое поле ввода исходного кода справа напечатаем первую строку кода:
#include <stdio.h>
Программа начинается с инструкции (препроцессора) для компилятора C, которая
указывает подключить (англ. include - включить в состав) файл стандартной библиотеки функций
ввода и вывода данных <stdio.h> (англ. standardin and out).
Это
сделает доступными в программе все
функции, описанные внутри этого файла. Подходящее название
данной инструкции — инструкция препроцессора или директива препроцессора, она всегда
должна быть написана в начале страницы, до того, как будет обработан сам код программы ниже её.
Функции внутри заголовочного файла
На примере заголовочного файла <stdio.h> заглянём в часть его содержимого, любые другие
заголовочные файлы устроены похожим образом:
Фрагмент внутренности файла заголовков функций <stdio.h>. Выделенная функция
printf() как раз и вызывается в нашей первой программе из файла
заголовков функций
<stdio.h>, и при её вызове запустится код реализующий печать символов в консоль.
Пропустите две строки после инструкции препроцессора и добавьте пустую(без аргументов в круглых скобках)
функцию main() :
intmain(){
}
Такое объявление функции определяет, что после её выполнения
функция должна будет вернуть значение типа int (подробнее о типах
данных далее).
Внутри фигурных скобок вставьте строку кода, которая вызывает одну из
функций, определённых в стандартной библиотеке ввода-вывода <stdio.h> ,
ставшую теперь доступной после написания инструкции препроцессора #include :
printf("Hello World!\n");
Внутри круглых скобок функции printf() определяется один строковой
аргумент. В языке
программирования C строки должны быть заключены в двойные кавычки "". Эта строка содержит текст
Hello World! и управляющую последовательность \n, которая автоматически
переводит каретку печатания символов в консоли к левому краю следующей(ниже) строки.
Внутри скобок вставьте последнюю строку кода, возвращающую число 0, это требуется для того, чтобы
сообщить Операционной Системе (которая вызвала к выполнению нашу программу), что программа завершена без
ошибок:
return 0;
По традиции возвращение значения 0 после выполнения программы указывает Операционной Системе(в
нашем
случаи - Windows), что программа выполнилась корректно.
Проверьте, что код программы выглядит в точности так же, как
и в листинге, приведенном внизу, а затем добавьте последний символ новой строки (нажмите клавишу
ENTER после закрывающей фигурной
скобки) и сохраните программу под именем hello.c :
Для сохранения можно нажать комбинацию горячих клавиш 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 процесс компилирования проходит через четыре отдельных этапа,
каждый из которых генерирует новый файл.
Предварительная обработка
препроцессор заменяет все директивы препроцессору в
оригинальном коде на языке C на код библиотек,
которые реализуют эти директивы. Например, подобная
замена выполняется для директив #include. Сгенерированный файл,
содержащий замены, имеет текстовый формат, и как правило расширение .i .
Преобразование (трансляция)
компилятор транслирует высокоуровневые инструкции файла с расширением .i
в низкоуровневые инструкции языка Ассемблера. Сгенерированный файл,
содержащий транслированные инструкции, имеет текстовый формат и,
как правило, расширение .s .
Сборка
сборщик преобразует текстовые инструкции языка Ассемблера в файле с расширением .s
в машинный код. Сгенерированный файл объекта имеет двоичный формат
и, как правило, расширение .o .
Связывание
компоновщик связывает полученные двоичные
объектные файлы с расширением .o в единый исполняемый файл.
Сгенерированный файл имеет двоичный формат и, как правило,
расширение .exe .
Вкратце, компилирование представляет собой первые три вышеописанных этапа
работы с одним файлом исходного кода, из которого создаётся один двоичный
объектный файл.
Если исходный код программы содержит синтаксические ошибки,
например, отсутствует или лишняя в нужном месте точка с запятой или круглые
скобки, компилятор сообщит о них в консоль и компиляция не завершится.
Компоновщик, с другой стороны, способен работать с несколькими
объектами и по окончании своей работы сгенерирует один исполняемый файл.
Это позволяет создавать большие программы из отдельных файлов,
и каждый из них может содержать повторно используемые функции.
Если компоновщик находит функцию, имя которой уже
встречалось в другом файле, он сообщит об ошибке и исполняемый
файл не будет создан.
Компоновщик, иногда называют линковщик/линкер
от англ. to link - соединять, связывать воедино.
Этапы процесса компиляции исходного кода.
Обычно временные файлы, созданные во время промежуточных
шагов процесса компилирования, удаляются автоматически, но их можно
восстановить с помощью дополнительной опции -save-temps, переданной в команде
компилятору.
В консоли введите команду gcc hello.c -save-temps -o hello.exe ,
а затем нажмите клавишу ENTER, чтобы повторно
скомпилировать программу и сохранить временные файлы.
Так они выглядят в Visual Studio CodeА так в папке hello
Откройте файл hello.i здесь в Visual Studio Code - кликните на него.
Ваш исходный код окажется в самом конце файла,
а перед ним будет располагаться код библиотеки <stdio.h>.
Содержимое файла hello.i, более тысячи строк кода занимает содержимое
библиотеки
<stdio.h>
Теперь откройте файл hello.s , чтобы
увидеть ваш исходный код на С, преобразованный к низкоуровневым
инструкциям Ассемблера. Обратите внимание, насколько менее
дружелюбным он кажется по сравнению с кодом на языке C.Это Ассемблер, самый низкоуровневый язык программирования, на котором всё ещё может
программировать человек. Так выглядит ваша программа hello.c на Ассемблере.
Программы, написанные на
языке Ассемблера, потенциально могут исполняться быстрее,
чем те, которые написаны на языке C, но их гораздо сложнее писать
и обслуживать(вы должны в совершенстве знать архитектуру вычислительной машины:
процессор, память и множество других электронных компонентов и т.п.).
Для традиционного программирования - язык C приоритетнее. Всю оптимизацию вашего исходного
кода С,
вместо вас сделает компилятор.
По мере совершенствования знания языка С,
вы сможете напрямую в коде С указывать вычислительной машине как использовать
архитектурные особенности процессора и памяти и компилятор сделает это для вас.
Заключения к Введению
Национальный Американский Институт Стандартов создал стандарт для языка программирования C - ANSI C.
Другие языки программирования, например C++ и C#, как
минимум частично созданы на базе языка C.
Язык программирования C имеет несколько стандартных библиотек,
содержащих проверенные годами функции, которые могут
быть использованы в любой программе.
Библиотеки языка C содержатся в заголовочных файлах, чьи имена
имеют расширение .h .
Программы на языке C создаются как простые текстовые файлы,
чьи имена имеют расширение .с, их можно написать даже в простом Блокноте, в специальном же
редакторе
кода - процесс написания и отладки кода происходят гораздо комфортнее.
Популярный компилятор GNU C Complier (GCC)( Minimalist GNU for Windows (MinGW)) можно
установить с помощью пакета MSYS2.
Добавление к системному пути каталога, где располагается
компилятор, позволит последнему запускаться из любой директории.
Программы могут иметь одну или более функций, содержащих
утверждения, которые должны быть выполнены при вызове
функции.
Каждая программа, написанная на языке C, обязана иметь
функцию main().
Объявление функций начинается с указания типа данных значения,
которое должно быть возвращено после выполнения функции. Если мы указали тип
int, то и с
помощью команды return 0; этот
int и возвращаем.
Утверждения, которые следует выполнить, содержатся в фигурных
скобках { }, каждое утверждение обязано заканчиваться точкой
с запятой ; .
Инструкции препроцессора располагаются в начале исходного кода листинга
программы и, как правило, будут заменены кодом библиотек.
Компилятор GNU C Compiler запускается с помощью команды gcc,
могущую включать в себя опцию -o, вместе с которой передается
имя исполняемого файла.
Временные файлы, созданные в процессе компилирования(компиляции),
могут быть получены с помощью опции команды компилятора -save-temps.
По умолчанию промежуточные файлы удаляются.
В этой главе мы изучим основные аспекты решения задач в контексте компьютерного программирования. Мы рассмотрим
различные техники решения проблем и поймем роль языков программирования в выражении наших решений. Более того,
мы узнаем о Трансляторах или Обработчиках языка, которые облегчают преобразование кода, удобного для человека,
в инструкции, выполняемые машиной. Кроме того, мы исследуем процесс компиляции и выполнения программ и получим
представления о том, как справляться с синтаксическими и логическими ошибками во время компиляции. К концу этой
главы у вас будет прочная основа для начала вашего пути в программировании и уверенности в решении различных
задач.
Цель главы заключается в том, чтобы обеспечить понимание концепций решения проблем и программирования. Она
охватывает подходы и техники решения проблем, языки программирования, трансляторы языков, процессы компиляции и
выполнения, обработку синтаксических и логических ошибок, а также файлы, создаваемые при написании, компиляции
и выполнении программ на C. Цель состоит в том, чтобы предоставить читателям навыки, позволяющие уверенно
подходить к задачам программирования и писать эффективные, безошибочные программы на C.
Подход к решению проблем
Решение проблемы — это процесс выяснения того, что необходимо сделать чтобы решить проблему. Он начинается с
описания проблемы и заканчивается нахождением наилучшего способа её исправления. Прежде чем можно будет найти
решение, проблему необходимо правильно определить, а затем превратить в чёткие, выполнимые шаги, которые будут
понятны всем. В этих шагах используются различные операторы для получения необходимого решения.
Логическое и численное решение задач включает в себя следующие основные шаги для решения любой проблемы:
Проанализируйте проблему: это процесс понимания проблемы. Чтобы лучше понять проблему, вам нужно
знать, какие входные данные, а какие будут их возможные результаты. Это просто означает знание требований
для решения проблемы и её возможного решения. Также проверяется осуществимость решения проблемы, чтобы
выяснить, можно ли решить проблему с существующими технологиями или нет.
Разделите сложную проблему на небольшие, простые задачи: если проблема
сложная и большая, то сначала разделите её на разные модули. Решите отдельные модули и объедините их для
окончательного решения. (Это необязательный шаг.)
Разработайте план решения проблемы: на этом этапе
разрабатывается план решения проблемы. Он включает в себя запись проблемы в виде псевдокода, блок-схемы или
в виде алгоритма. Псевдокод, блок-схемы и алгоритм представляют собой вашу пошаговую стратегию для
разрешения проблемы. Этот шаг даёт логику для решения проблемы.
Реализуйте план: он преобразует алгоритм или псевдокод в программу
(актуальный код) с использованием подходящего языка программирования, такого как C, C++, Python и т.д.
Введите программу в компьютер. Компьютер генерирует результат в выходных данных на основе программы и
входных данных. Эти выходные данные можно использовать для принятия правильных решений.
Обратное отслеживание: как только вы
получите результат в выходных данных, вы можете вернуться назад и внести некоторые изменения в программу или
план для дальнейшего улучшения результата. Вы можете подумать о том, чтобы решить проблему другим способом
для получения лучших результатов. Этот шаг часто называется этапом тестирования и отладки.
Решение логических проблем
Давайте решим классическую логическую задачу: задачу о ёмкостях с водой. Допустим, вам поручено отмерять 4-ре
литра воды с помощью двух кувшинов. Объём первого кувшина 3-и литра, а второго - 5-ть литров. Необходимо
выполнить следующие шаги:
Анализ проблемы: Сначала проверим, доступны ли нам все необходимые ингредиенты, такие как
источник воды, кувшины и так далее, которые будут служить исходными данными. Решением же является
возможность получения 4-ёх литров воды - это выходные данные.
Разделение сложной проблемы на небольшие, простые задачи: Наша проблема довольно простая, поэтому
нет необходимости делить её на простые задачи(пропустим этот шаг.)
Разработать план: Псевдокод ниже представляет пошаговый план достижения решения проблемы с
помощью описания её простыми словами:
Наполните 5-литровый кувшин до краев.
Перелейте воду из 5-литрового кувшина в 3-литровый, в 5-литровом кувшине останется 2-а литра.
Опустошите 3-литровый кувшин.
Перелейте оставшиеся 2-а литра воды из 5-литрового кувшина в 3-литровый.
Снова наполните 5-литровый кувшин до краев.
Осторожно перелейте воду из 5-литрового кувшина в 3-литровый до заполнения 3-литрового кувшина.
Вылили 1 литр.
В 5-литровом кувшине останется ровно 4 литра воды.
Реализовать план: Преобразуйте алгоритм или псевдокод в программу
(реальный код) с использованием подходящего языка программирования, например C.
Введите программу в компьютер, и вы получите желаемый результат.
Обратное отслеживание: Предположим, что после нескольких шагов вы обнаружите,
что распределение воды не приводит к ожидаемому результату в 4-е литра в 5-литровом кувшине. Вместо
того, чтобы сдаваться, вы возвращаетесь к предыдущему шагу, где у вас были другие варианты выбора (например,
какой из кувшинов наполнять, когда наливать воду и так далее). Затем вы пробуете другой вариант алгоритма,
чтобы увидеть, приведёт ли это к желаемому результату.
Решение численных проблем
Чтобы было легче понять концепцию решения, давайте рассмотрим пример с числами. Необходимо сложить два числа.
Будем
следовать логическим шагам решения численных задач:
Проанализируем проблему: здесь мы должны взять три переменные: две для входных данных и одну для
сохранения выходного результата. Пусть числа A и B являются входными данными, а C — выходными переменными.
Итак, мы можем записать это следующим образом: Входные числа: Целые числа A и B. Выходное число: Целое число
C.
Разделим сложную задачу: Эта задача простая, поэтому нет необходимости делить её на мелкие
модули.
Разработаем план: Псевдокод - представление плана решения простым языком.
Псевдокод для сложения двух чисел, A и B, и сохранение результата(суммы) в третье число, C, записывается
следующим образом:
Сумма двух чисел. Это Название(суть решения) задачи.
Ввод: целые числа A и B.
Вывод: целое число C.
C = A + B.
Печать C.
Конец.
Выполните план: теперь этот псевдокод может быть преобразован в язык программирования C:
#include <stdio.h>
intmain(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;
}
Обратное отслеживание: Как только вы получили результат в качестве выхода, вы можете попробовать
альтернативные методы решения. Обычно, в первую очередь альтернативные методы решения ищут если
вывод(результат)
не соответствует ожидаемому. Тогда вернитесь к плану и проверьте
код: возможно допущена какая-то ошибка, устраните её, чтобы получить правильный вывод. Только после того как
выполнение программы гарантированно выдаёт ожидаемый результат, можно заниматься следующими этапами
улучшения исходного кода, а именно: переписывать исходный код с целью увеличения его скорости выполнения,
или с целью уменьшения требуемого объёма памяти машины(подробнее читайте далее в учебнике).
Техники решения проблем
Алгоритмы, псевдокод и блок-схемы являются основными методами или инструментами для решения проблемы с
использованием компьютера. Более того, они преобразуются в реальные компьютерные программы с использованием
языков программирования.
Алгоритм
План, как решать проблему шаг за шагом без использования аппаратного и программного обеспечения, называется
алгоритмом. Затем эти шаги записываются на языке программирования в виде программы.
Так создаётся программа. Другими словами, алгоритм — это план для компьютерной программы.
Опишем Характеристики алгоритма:
Конечность: После определённого количества шагов алгоритм должен всегда завершаться.
Однозначность или чёткость:
Каждый из его шагов, а также входные и выходные данные должны быть легко понятны и приводить лишь к
одному значению.
Ввод: Он должен иметь чёткий способ предоставления входных данных.
Вывод: Алгоритм должен чётко обозначать, каков будет результат.
Эффективность: Каждый шаг должен быть необходимым.
Независимость от языка: Он может быть написан на любом языке программирования.
Некоторые простые примеры алгоритмов:
Алгоритм для сложения двух чисел, A и B, и сохранение суммы в число C:
Объявите три целых числа A, B и C.
Введите значение A и B.
Сложите значения A и B.
Сохраните результат Шага 4 в C.
Выведите C.
Конец
Алгоритм для нахождения большего из двух чисел:
Объявите два целых числа A и B.
Введите значения A и B.
Если A > B, выведите "A больше".
Иначе, если B > A, выведите "B больше".
В противном случае выведите "Числа равны".
Конец
Алгоритм для определения, является ли число чётным или нечётным:
Объявите целое число A.
Введите значение A.
Если A % 2 == 0, тогда "выведите число ЧЁТНОЕ".
Иначе "выведите число НЕ ЧЁТНОЕ".
Конец
Алгоритм для обмена значений двух чисел:
Объявите три целых числа A, B и T.
Введите значения A и B.
Установите T = A.
Установите A = B.
Установите B = T.
Выведите A и B.
Конец
Блок-схема
Блок-схема — это диаграмма, которая показывает шаги для решения проблемы в виде алгоритма. Она используется для
визуальной демонстрации того, как работает алгоритм. Это графическое представление алгоритма. Иными словами,
это ещё одна техника представления проблемы, как алгоритма но в виде диаграммы или рисунка.
В ней используются различные фигуры, которые связаны друг с другом линиями. Линии связей показывают, как, от
куда и куда
протекают информация и управление. Ниже представлены фигуры, которые используются в этом учебнике при
изображении блок-схем:
Все фигуры для построения блок-схем
Фигура(Изображение)
Название
Применение
Пример
Старт
Обычно так обозначается точка входа в главную функцию
main() или Название алгоритма - какую задачу он решает.
Завершение программы
Обычно так обозначается выход из главной функции main()
Утверждение
Любое утверждение на языке С
Условный оператор
Ветвление программы по условию либо в ветку 1(true/истина/да) либо в
ветку 0(false/ложь/нет). Для простоты понимания - рекомендуется применять слова "ДА" и "НЕТ", как в
ответах на вопросы в обычной жизни.
Переключатель варианта
Выбирает один из вариантов выражения
Вариант переключателя
Вариант, соответствующий значению утверждения в Переключателе
Объявление Функции
Объявление Функции. Точка входа в функцию
Вызов функции
Вызов функции. Перейти в функцию по её имени/адресу
Комментарий
Добавление комментария. Обычно справа/сверху от фигуры, но может
быть и вертикально по
линии.
Процедура
Асинхронная внешняя процедура, блок кода, модуль и т.п. - без
возможности управления
ею.
Пока процедура не вернёт "завершение", "значение" или "ошибку" - Главная
функция ждёт.
Параметры
Формальные параметры необходимые для запуска кода. Инструкции
препроцессора и т.п. данные необходимые до Главной функции.
Структура
Своего рода "полка", у которой сверху указывается её название, а снизу список элементов данных
содержащиеся в ней.
Начало цикла
Сжатое визуальное представление начальной конструкции циклов(for,
while, do-while)
Конец цикла
Сжатое визуальное представление конечной конструкции циклов(for,
while, do-while)
Отправить параметр
Обычно обозначает отправку параметра/ов(снизу) какому либо получателю(сверху)
Получить параметр/ры
Обычно обозначает получение параметра/ов(снизу) от какого либо получателя(сверху)
Отправить
Чаще всего обозначают печать в консоль
Получить
Чаще всего обозначают ввод данных пользователем из консоли
Часы
Инициирование выполнения утверждения в заданный момент реальной даты/времени
Длительность
Выполнение утверждения/ий в течении заданного промежутка времени
Таймер
Подождать заданное время, прежде чем выполнить утверждение
Параллелизм
Запустить два или более параллельных потока(веток) кода одновременно
Управление Параллельным фоновым процессом
Запустить/Приостановить/Продолжить/Завершить независимый
Параллельный фоновый процесс. Главная функция продолжает выполняться далее - не ждёт пока
Выполняющийся фоновый процесс вернёт своё "завершение" или "ошибку"
Соединитель
Буквенно-Цифровой индекс соединителя линии/ий алгоритма указывает на
такой же соединитель на другом листе/странице
Блок-схемы условно делят на три типа по степени сложности:
Простая линейная - Состоит из нескольких базовых фигур, с помощью которых изображают простейшие
алгоритмы. Обычно не имеет ветвления. Каждое действие идёт строго вертикально сверху вниз - шаг за шагом.
Обычно с такой блок-схемы начинается проектирование программы.
Средняя линейная - Содержит в себе не большое количество фигур ветвления потока выполнения
программы и один-два цикла. Как правило с помощью такой блок-схемы изображают детально работу конкретного
алгоритма.
Крупная не линейная - Может размещаться на одном большом или нескольких листах/экранах/страницах
посредством разбиения схемы на модули, блоки, процедуры и т.п. Содержит множество фигур ветвлений потока,
циклов, функций и т.д. Состоит из Простых и Средних блок-схем.
Полная - Может размещаться на десятках и сотнях больших листах/экранах/страницах
посредством разбиения схемы на модули, блоки, процедуры и т.п. Содержит множество фигур ветвлений потока,
циклов, функций и т.д. Состоит из Простых и Средних и Крупных блок-схем.
Пример Простой линейной блок-схемы:
Блок-схема алгоритма сложения двух чисел и вывода значения суммы на печать в консоль.
Пример Средней линейной блок-схемы:
Блок-схема алгоритма сравнения двух чисел и вывода на печать в консоль сообщения о том какое число больше
или что числа равны.
Пример Крупной не линейной блок-схемы:
Блок-схема алгоритма работы мобильного приложения. Подготовительная стадия проектирования.
Псевдокод
Псевдокод используется для написания шагов решения конкретной задачи. Это краткая форма алгоритма. Она убирает
ненужные слова из алгоритма. Псевдокод - это неформальный способ написания программы на структурированном
родном языке программиста. Он концентрируется на логике алгоритма и не углубляется в детали синтаксиса какого
либо
языка программирования.
Позже его можно напрямую преобразовать в программу на конкретном языке программирования. Тем не менее, сам по
себе псевдокод не является компьютерной программой. Он только представляет алгоритм программы на обычном языке
и математических обозначениях.
Они обычно используются в учебниках и научных статьях для демонстрации сути алгоритма. Некоторые примеры
псевдокода приведены далее.
Пример псевдокода на английском языке для сложения двух чисел:
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. Конец
В таблице объясняется разница между Блок-Схемой, Алгоритмом и Псевдокодом:
Разница между Блок-Схемой, Алгоритмом и Псевдокодом
Алгоритм
Блок-схема
Псевдокод
Это план о том, как шаг за шагом решить проблему.
Блок-схема — это графическое представление алгоритма или шагов для решения проблемы.
Это неформальный способ написания программы.
Сложно понять.
Легко понять.
Труднее понять по сравнению с алгоритмом.
Текст используется для написания алгоритма.
Для рисования блок-схем используются различные фигуры, линии показывают как выполняется поток
программы.
Текст и математические символы используются для написания алгоритма.
Легко отлаживать.
Трудно отлаживать.
Труднее отлаживать по сравнению с алгоритмом.
Языки программирования
Чтобы проинструктировать компьютер как выполнять задачу, требуется язык программирования.
Он используется для общения с компьютером путём написания компьютерной программы (набор
инструкций/команд/утверждений). Существует три основных типа языков программирования:
Машинный уровень или низкоуровневый язык (НУЯП): Этот язык имеет только два символа, 0 и 1
(бинарные числа). Все данные и инструкции (программа) должны быть написаны в двоичном коде.
Компьютер может непосредственно понимать код, написанный на этом языке, так как он может понимать только
двоичный язык. Он также известен как машинный язык.
Он не требует транслятора (компилятора, интерпретатора) для преобразования в другую форму, так как он
непосредственно понятен компьютеру. Он очень близок к компьютеру, так как компьютер понимает только 0 или 1.
Каждый компьютер имеет свой собственный машинный язык, поэтому он зависит от машины и запустить на другой не
такой же машине его не получится. Писать программу на этом языке сложно, и ещё сложнее найти ошибки в ней.
Вот пример того как такие низкоуровневые программы выглядят для программиста:
Гипотетическая программа для сложения двух чисел в двоичном формате. Следующая программа состоит
только из трёх команд, записанных в двоичном виде:
Чаще всего, программы на машинном уровне пишутся в восьмеричной или шестнадцатеричной системе
счисления:
57 D5
D5 AB
AA AB
Язык ассемблера: он использует символы для написания программы. Эти символы называются
мнёмониками, например такие ADD, SUB, MUL и DIV, которые гораздо легче запомнить чем
шестнадцатеричные коды.
Эти мнёмоники написаны на основе сокращения английских слов. Его также называют языком среднего
уровня, потому что он использует как естественный язык, так и символы. Он обладает характеристиками
языков высокого уровня (ВУЯП) и языков низкого уровня (НУЯП). Компьютер не понимает их напрямую.
Программе, написанной на Ассемблере, для выполнения требуется транслятор, который так и называется
ассемблер (англ. Assembler - Сборщик), для преобразования из языка ассемблера
в язык низкого уровня (НУЯП).
Все инструкции, написанные на языке Ассемблера, различаются в зависимости от машины, поэтому они
зависят от машины(машино-зависимые, аппаратно-зависимые, архитектурно-зависимые); то есть программа,
написанная на одном типе компьютера, не будет выполняться на компьютере другого типа.
Этот язык очень близок к компьютеру, так как он зависит от машины. Писать программу на этом языке легче,
и отладка (удаление ошибок) значительно проще по сравнению с языком низкого уровня (НУЯП), но сложнее по
сравнению с каким либо языком высокого уровня (ВУЯП). Вот пример кода на языке низкого уровня (НУЯП) для
сложения двух чисел:
LOAD A
ADD B
STORE C
Как вы уже знаете язык C скорее ближе к языкам Среднего Уровня чем к Высокоуровневым. Так что условно
можно определять язык С, как значительно более удобная версия низкоуровневого языка но на основе
синтаксиса как у
высокоуровневых языков. Так, выполнение сложения двух чисел на C записывается крайне просто:
C = A + B;
Язык высокого уровня (ЯПВУ): Он упрощает программирование, используя естественный язык, такой как
английский, для написания программы. Его легко изучить, так как он использует естественный язык. Компьютеры
не могут напрямую понять код, написанный на этом языке, так как компьютер может понимать только двоичный
язык.
Поэтому требуется переводчик, чтобы преобразовать ЯВУ в ЯНУ, например, компилятор, интерпретатор и так
далее. Он очень близок к человеку, потому что человек может легко его понять. Он не зависим от машины и
портативен, поэтому его программу можно запустить на любом компьютере. Написать программу на этом языке
очень просто, и отладка (удаление ошибок) также очень проста. Примеры ЯВУ: C, C++, JAVA, FORTRAN, PYTHON,
PROLOG и так далее.
Следующая программа на ЯПВУ. Пример: Программа на C для
сложения двух чисел, которые машина запрашивает у пользователя через консоль(терминал):
#include <stdio.h>
intmain()
{
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;
}
Трансляторы или обработчики языка
Языковые трансляторы или обработчики переводят или конвертируют программы, написанные на одном языке, на
другой, например, из высокого уровня в низкий уровень. Программа, написанная на языке программирования,
известна как исходный код; когда она транслируется на машинный язык, она становится объектным
кодом. Простой механизм языковой трансляции показан на рисунке:
Существует в целом три типа языковых трансляторов:
Компилятор: Это программа, которая преобразует высокоуровневый язык в низкоуровневый. Она
принимает полную программу в качестве входных данных и превращает её в низкоуровневый код.
Исходный код — это программа, написанная на высокоуровневом языке.
Когда она преобразована в низкоуровневый код, она становится объектным кодом. Таким образом, компилятор
превращает исходный код во что-то, что называется объектным кодом. Как только объектный код был получен, он
выполняется для получения результата. Это преобразование называется компиляцией.
Функции компилятора
Основная задача заключается в преобразовании языков высокого уровня в языки
низкого уровня. Найти ошибки в исходном коде в соответствии с синтаксисом (грамматикой) языка.
Отобразить ошибки, чтобы программист мог их устранить. Выделяет пространство для программы в основной
памяти. Генерирует объектный код.
Интерпретатор: он преобразует программу на высокоуровневом языке в программу на низкоуровневом
языке построчно; то есть одно выражение за раз переводится и сразу же выполняется.
Программные выражения напрямую переводятся построчно в машинный язык без генерации объектного кода.
В отличие от компилятора, интерпретатор - это простая программа, занимающая меньше места в памяти.
Интерпретация программы занимает больше времени по сравнению с компиляцией, потому что в
интерпретаторе
каждое выражение должно быть переведено по отдельности. Таким образом,
процесс компиляции занимает меньше времени, чем интерпретация.
Ассемблер: Программное обеспечение, которое преобразует программу на языке ассемблера в
машинный или
низкоуровневый язык, называется ассемблером(сборщиком). Программа, написанная на языке ассемблера,
известна
как исходный код, а выходные данные ассемблера известны как объектный код. После получения объектного
кода
он выполняется, чтобы получить результат. Функции ассемблера:
Основная задача заключается в том, чтобы преобразовать ассемблер в низкоуровневый код.
Найти ошибку в исходном коде в соответствии с синтаксисом (грамматикой) языка.
Вывести сообщение ошибки, чтобы программист мог её устранить.
Выделить место для программы в основной памяти.
Сгенерировать объектный код.
Кроме выше упомянутых программ, следующие программы также поддерживают написание и выполнение
программы:
Редактор: программа, которая предоставляет платформу для написания, изменения и
редактирования исходного кода или текста. Этот текстовый редактор имеет свои собственные команды
для
написания, изменения и редактирования исходного кода или текста.
Компоновщик: Он создает исполняемый файл с
расширением .exe, комбинируя объектные файлы, сгенерированные компилятором или
ассемблером, и
дополнительные
куски кода. Компоновщик ищет и добавляет все библиотеки, необходимые для создания исполняемого
файла,
в
объектный файл. Он объединяет два или более файлов в один файл, например, объектный код нашей
программы и
объектный код библиотечных функций, в один исполняемый файл.
Загрузчик: Это программа, которая загружает
исполняемый код от компоновщика, помещает его в основную память и подготавливает его к
выполнению на
компьютере. Она выделяет программному коду память в основной памяти.
В настоящее время все ранее упомянутые инструменты объединены в один программный пакет, который
называется средой разработки (IDE). Некоторые примеры IDE включают Turbo C, CodeBlocks, Visual
Studio,
Microsoft Visual C++, Eclipse и так далее.
Процесс компиляции и выполнения
Существует несколько основных этапов, связанных с переводом программы на языке C(исходный код) в исполняемый
файл, вот они:
Напишите программу в редакторе, которая является исходным кодом с расширением .c.
Исходный код передаётся компилятору, и компилятор генерирует объектный код с расширением .o.
Если возникает какая-либо ошибка(синтаксическая ошибка), программист снова открывает исходный код в
редакторе, находит
ошибку, устраняет её и компилирует код снова.
Компоновщик связывает объектный код с некоторыми дополнительными файлами, такими как библиотечные файлы и
другие куски кода, необходимые для выполнения программы. Компоновщик генерирует бинарный исполняемый файл с
расширением .exe. Если возникает ошибка при компоновке, скажем, компоновщик не может связать файлы с
объектным кодом, это называется ошибкой компоновки.
Этот исполняемый файл загружается в основную память машины загрузчиком.
После этого программа выполняется, и создается вывод в форме результата. Если возникает какая-либо ошибка
(ошибка времени выполнения), программист снова открывает исходный код в редакторе, находит ошибку и
устраняет её.
Изобразим визуально процесс Компиляции и Выполнения исходного кода в виде блок-схемы:
Процесс Компиляции и Выполнения программы.
Синтаксические и логические ошибки в компиляции
Язык программирования имеет набор правил для написания утвердительных предложений(утверждений).
Этот набор правил известен как грамматика языка. Грамматика имеет два типа ошибок:
Синтаксические ошибки: возникают, если программист нарушает правила грамматики при написании
операторов. Например, отсутствие точки с запятой в конце оператора, использование не объявленных переменных
в программе, неправильное написанных ключевых слов и так далее. Компилятор выявляет это во время компиляции.
Изучите следующую программу:
#include <stdio.h>
void main()
{
int A = 2, B = 3, C;
C = A + B
printf("Sum is %d", C);
getcha();
}
Напишите этот код, сохраните его и выполните. Какие ошибки компиляции сообщил Компилятор?
Логические или семантические ошибки: они возникают, когда логика программы неверна.
Это связано со смысловым значением утверждения. Например, если программист ставит знак минус вместо знака
плюс, то такая ошибка не выявляется на этапе компиляции(так как синтаксис утверждения верный).
Но она выявится во время выполнения, когда программа выдаст неверные результаты. Например:
A + B = C;
A = B / 0;
Напишите и выполните программу ниже, где допущена логическая ошибка?:
#include <stdio.h>
intmain()
{
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
Итак, повторим основные три типа файлов создаваемые при написании программ на языке С и запомним чем они
отличаются:
Исходный код: Когда мы пишем программу, её необходимо сохранить с расширением .c.
Процесс сохранения программы генерирует файл. Например, создаётся и сохраняется файл с именем sum.c.
Этот файл и есть исходный код.
Объектный код: Исходный код передаётся компилятору, а компилятор генерирует другой файл, известный
как
объектный код. Он имеет расширение .o, например, sum.o.
Входными данными для компилятора является исходный код, а выводом компилятора является объектный код.
Исполняемый код: с помощью компоновщика объектный код связывается с некоторыми дополнительными
файлами, такими как библиотечные файлы и другие куски кода, необходимые для выполнения программы.
Компоновщик генерирует бинарный исполняемый файл с расширением .exe, известный как исполняемый код.
Например, объектный код sum.o преобразуется в sum.exe.
После успешного выполнения все три файла хранятся в рабочем каталоге C(в папке). Полный процесс показан на
блок-схеме выше.
Заключение
В этой главе мы сосредоточились на методах решения проблем с помощью программирования, а также на различных
подходах, используемых в решении
проблем, таких как блок-схемы, алгоритмы и псевдокод.
Кроме того, обсудили несколько уровней компьютерных языков, таких как высокоуровневые, низкоуровневые и язык
ассемблера, а также инструменты, используемые для
преобразования между этими уровнями, такие как компилятор, интерпретатор и ассемблер. Также изучили процесс,
который будет использоваться для превращения алгоритма решения проблемы в настоящую программу.
Следующая глава будет посвящена введению и истории языка программирования C. Это необходимо,
для понимания стратегии создания программ, т.е. как написать компьютерную программу имея лишь стратегию решения
проблемы.
Кроме того, мы обсудим основные компоненты программы на языке C и метод её выполнения.
Важные моменты
Решение проблем — это процесс определения того, что необходимо сделать для решения проблемы.
Шаги в решении логических и численных задач включают в себя анализ проблемы, разделение сложной задачи на
небольшие простые задачи, разработку плана для решения проблемы, выполнение плана и обратный подход.
Тремя основными инструментами для решения задач являются алгоритмы, блок-схемы и псевдокоды.
Алгоритм — это план того, как шаг за шагом решить проблему без компьютерного оборудования и программного
обеспечения.
Блок-схема — это диаграмма, которая показывает шаги для решения проблемы, на основе алгоритма.
Псевдокод — это неформальный способ написания программы на структурированном обычном человеческом языке(на
вашем родном).
Он сосредоточен на логике алгоритма и не углубляется в детали синтаксиса конкретного языка программирования.
Чтобы задать компьютеру выполнение задачи, требуется язык, который называется языком программирования.
Самый Низко-Уровневый Язык программирования (LLL) имеет только два символа, 0 и 1. Это машинный код.
Высоко-Уровневый ЯП (HLL) использует естественный язык, такой как английский, для написания программы.
Ассемблерный язык использует символы для написания программ. Эти символы называются мнёмониками, которые
легко запомнить, такие как ADD, SUB, MUL и DIV.
Трансляторы(Переводчики, обработчики) языка транслируют(переводят или конвертируют) программу, написанную
на
одном языке, на другой, например, с высокоуровневого языка на низкоуровневый.
Компилятор — это программа, которая преобразует языки высокого уровня (HLL) в языки низкого уровня (LLL).
Она принимает полный HLL программный код на вход и преобразует его в LLL.
Ассемблер - это программа, которая преобразует программу на ассемблере в машинный код или LLL.
Редактор кода — это программа, которая предоставляет платформу для написания, изменения и редактирования
исходного кода или текста.
Компоновщик ищет и добавляет все библиотеки, необходимые для того, чтобы сделать файл исполняемым в
объектном файле.
Загрузчик - это программа, которая загружает исполняемый код из компоновщика, помещая его в основную
память и подготавливает к выполнению на компьютере.
Синтаксическая ошибка возникает, если программист нарушает грамматические правила при написании
утверждений.
Логические или семантические ошибки возникают, когда логика(смысл) программы неверна.
Когда мы пишем программу, её необходимо сохранить с расширением .c. Процесс сохранения программы
генерирует файл исходного кода.
Исходный код передаётся компилятору, и компилятор генерирует другой файл, известный как объектный код.
Компоновщик генерирует бинарный исполняемый файл с расширением .exe, известный как исполняемый код,
связывая необходимые файлы с объектным кодом.
Важные вопросы
Что вы имеете в виду под решением проблем? Напишите различные техники для этого с их преимуществами и
недостатками.
Опишите различные шаги, используемые в решении логических и числовых задач.
Опишите различные этапы, используемые для решения реальной проблемы. Объясните их, взяв подходящий пример.
Напишите алгоритм, блок-схему и псевдокод для задачи нахождения наибольшего из трех чисел.
Напишите алгоритм, блок-схему и псевдокод для нахождения корней квадратного уравнения.
Что такое язык программирования? Объясните его разные типы с их преимуществами и ограничениями.
Что вы имеете в виду под языковыми трансляторами(переводчиками или обработчиками)? Объясните их различные
типы.
Опишите и обсудите процесс компиляции и выполнения.
Опишите разницу между синтаксическими и логическими ошибками.
Опишите разницу между компилятором и интерпретатором.
Объясните разные файлы, генерируемые при написании, компиляции и выполнении программы на языке C.
Эта глава исследует основные аспекты компьютеров. Понимание ключевых компонентов и концепций компьютеров
является важным в цифровую эпоху. В этой главе будут рассмотрены строительные блоки компьютера, отличия
между различными типами компьютеров и ключевые характеристики, которые делают компьютеры незаменимыми в нашей
жизни. Мы углубимся в преимущества и ограничения разнообразных применений этих электронных устройств и подведём
итоги ключевых моментов главы и важных вопросов для дальнейшего изучения. Давайте начнём путешествие в мир
компьютеров.
Глава этой книги направлена на предоставление всестороннего понимания компьютеров и их неотъемлемых
компонентов. Мы исследуем данные, классификацию компьютеров, их основные характеристики, а также
преимущества и ограничения этих машин. Рассмотрим их разнообразные области применения, подведём итоги и
предоставим ключевые выводы и зададим важные вопросы для более глубокого понимания темы.
Термин компьютер происходит от слова вычисление(англ. compute - высчитывать,
вычислять, считать). Компьютер — это электронное устройство, которое принимает
данные и набор инструкций в качестве входных данных от пользователя, обрабатывает данные и выдает информацию
в качестве результата. Этот полный цикл известен как цикл ввода-обработки-вывода, как показано на
схеме:
Набор Инструкций, Утверждений, Команд это исходный код на языке С, которые программист пишет для
выполнения задачи. Набор инструкций известен как программа. Набор программ известен как
программное обеспечение(ПО). Электронное вычислительное устройство известно как аппаратное
обеспечение(АО). Таким
образом, компьютер — это совокупность аппаратного и программного обеспечения.
Схема ниже показывает базовые компоненты любой вычислительной машины:
ЭВМ состоит из Программного и Аппаратного обеспечения.
В общем базовом случае, ЭВМ состоит из четырех функциональных блоков, которые соединены между собой, образуя
компьютер:
Четыре базовые составные части любого компьютера - Процессор, Память, Устройства Ввода и Вывода.
Все Блоки соединены друг с другом через системные шины(провода) и в целом определяют архитектуру машины. На
сегодня количество различных архитектур ЭВМ огромно. Существует три основных шины:
Шина Данных - осуществляет передачу данных от одного Блока к другому;
Шина Управления - передает управляющие сигналы, сгенерированные Блоком Управления;
Шина Инструкций - передает инструкции(команды).
Блок Ввода
Получает данные или программу от пользователя или других носителей (устройств).
Отвечает за считывание ввода. Функции устройств ввода выполняются различными устройствами,
такими как:
клавиатура
мышь
джойстик
сенсорный экран
сканер
и т. д.
Устройство Ввода обычно выполняет три основные функции, которые заключаются в следующем:
принимает ввод от пользователей в читаемой для человека форме, которая написана на английском языке(чаще
всего);
преобразует ввод от пользователей в формат, читаемый компьютером (двоичный язык);
предоставляет преобразованные данные компьютерной системе для дальнейшей обработки.
Блок Вывода
Устройство Вывода принимает обработанный результат от компьютера и делает его доступным для конечного
пользователя.
Функции устройств вывода выполняются различными устройствами, такими как:
монитор
принтер
динамики
плоттер
и т. д.
Устройство Вывода как правило, выполняет три основных функции:
принимает обработанные результаты от компьютера в формате, читаемом компьютером (двоичные данные - это
форма сигнала);
преобразует формат, читаемый компьютером, в формат, читаемый человеком (английский, русский, аудио,
видео и
т. д.);
поставляет преобразованный результат конечным пользователям.
Устройства Вывода условно можно разделить на три типа:
Генерирующих сенсорную информацию воспринимаемую человеком через органы чувств - Эти устройства создают
электронную версию вывода. Например, монитор генерирует изображение на экране, динамик генерирует
звуковые
сигналы, вибро-моторы генерируют вибрации в джойстике, и т.п.
Выполняющие физическую запись данных в файлы, которые хранятся на жестком диске, флэш-накопителях и
т.п.,
Создающие или изменяющие физические предметы - Печать на бумаге, 3-Д принтеры, прожиг оптических дисков
и
т.п.
Три основных типа Устройств Вывода.
Блок Центрального Процессора
Обычно говорят просто Процессор или Центральный Процессор. Это блок обработки
процесса выполнения операций над данными согласно командам и данным от пользователя.
Центральный процессор (ЦП) работает как единый блок обработки в компьютере.
Он выполняет вычисления и операции обработки данных, введённых через устройства
ввода. Его также называют мозгом компьютера. Основные компоненты ЦП следующие:
Арифметико-логическое устройство (АЛУ) - отвечает за выполнение арифметических и логических функций.
Например арифметических:
сложение,
вычитание,
умножение,
деление,
и так далее.
Пример логических функций:
И,
ИЛИ,
ИНВЕРСИЯ,
и др.
Управляющее устройство (УУ), (тоже самое - Устройство Управления, Блок Управления) - действует как
центральная нервная система компьютера. Генерирует управляющие сигналы, которые управляют и контролируют
все
компоненты компьютера.
Набор Регистров - это самая быстрая память, которая только доступная Процессору. Состоит из минимального
объёма памяти. Размер Регистров измеряется количеством бит, которые они могут хранить; например, 8-битный
регистр, 16-битный регистр, 32-битный регистр, 64-битный регистр и так далее.
Блок Памяти
Его задача - хранить данные и инструкции/программы. ЦП обращается к данным и программам в памяти и
обрабатывает их. Если генерируется какой-либо промежуточный результат, он сохраняется в памяти, и окончательный
результат также сохраняется в памяти. Память разбита на ячейки. Каждая ячейка имеет собственный адрес. ЦП
обращается к памяти, генерируя адрес ячейки. Работа памяти с ЦП показана на рисунке:
Связи между ЦП и Памятью.
Компьютерная память условно делится на три типа:
Первичная/Основная/Внутренняя Память. Основная память является временным хранилищем и также известна как
рабочая память. Процессор напрямую обращается к этой памяти. Она хранит все временные данные/инструкции,
пока компьютер работает. Примеры основной памяти:
Память Случайного Доступа, она же RAM - Random Access Memory, она же ОЗУ -
Оперативное Запоминающее Устройство(Оперативная память),
Память только для чтения, она же ROM - Read Only Memory, она же ПЗУ - Постоянное
Запоминающее Устройство.
Вторичная/Вспомогательная/Внешняя Память. Вторичная память не подключена напрямую к ЦП. Сначала ЦП
обращается к ОЗУ(поэтому её и называют первичной памятью). Затем осуществляется доступ ко
вторичной памяти. Вторичная память является постоянной.
Постоянная - означает, что память хранит данные, даже когда питание машины отключено.
Она сохраняет данные постоянно.
Обычно её используют для резервного (постоянного) хранения данных, чтобы их можно было
использовать в будущем.
Вторичная память значительно медленнее первичной памяти; данные, хранящиеся в основной памяти,
доступны за наносекунды (1/1 000 000 000 сек - одна миллиардная секунды), а данные из вторичной памяти
могут
быть доступны за миллисекунды
(1/1000 сек - одна тысячная секунды).
Примеры вторичной памяти:
жесткий диск (HDD)
флеш-накопитель
компакт-диск (CD)
цифровой универсальный диск (DVD)
твердотельный накопитель (SSD)
и др.
Стоит упомянуть, что кроме Регистров, внутри ЦП ещё есть и так называемая КЭШ-память. Это своего рода
личное
ОЗУ Процессора. В современных процессорах и уже в микроконтроллерах, КЭШ (англ. cash - наличные
деньги
при себе) встраивают несколько уровней КЭШ-памяти. И дают им определения как L1, L2, L3 - ( англ.
Level
- уровень)
. L3 - самая медленная, но она быстрее ОЗУ. L2 - быстрее L3, а L1 - быстрее L2. Регистры быстрее L1.
Доступ к КЭШу и Регистрам возможен за десятые наносекунды (1/10 000 000 000 сек - одна десяти-миллиардная
секунды).
На рисунке показано подключение первичной и вторичной памяти к ЦП:
Вторичная, Первичная, КЭШ и Регистры - основные виды памяти машины.
Стоит уточнить, что сами по себе Первичная и Вторичная носители
памяти не умеют изменять, отправлять, получать данные - всеми этими процедурами управляет Процессор.
Данные и информация
Слово данные происходит от слова данное, которое является единственным числом. Данные
состоят
из
сырых фактов и цифр, предоставленных компьютеру в качестве входных данных. Компьютер обрабатывает
данные и
преобразует их в информацию. Следовательно, информация определяется как обработанные данные.
Например,
оценки
студентов – это данные; когда они обрабатываются, они становятся результатом, который является информацией.
Ещё
один пример: имена студентов в классе могут рассматриваться как данные, когда они обрабатываются и
упорядочиваются в алфавитном порядке - тогда они становятся информацией. На рисунке показаны
взаимосвязи между данными и информацией:
Компьютер преобразует Данные в Информацию.
Данные состоят из чисел, букв и символов.
Например, следующие символы используются при создании данных:
1, 2, 3...
A, B, C...
a, b, c...
+, -, @...
Основная цель преобразования данных в информацию заключается в получении полезной информации, используемой
для
принятия
решений. Очень сложно принимать решения, основываясь на сырых данных. Мы можем различать данные и
информацию, как
показано в таблице:
Отличия между данными и информацией
Данные
Информация
Это сырые факты и числа, состоящие из символов, таких как цифры и буквы.
Обрабатывается на основе данных.
Неорганизованны.
Организованны по своей природе.
Нельзя использовать напрямую в принятии решений. Сначала их необходимо обработать в форму
информации.
Можно использовать напрямую для принятия решений.
Необходимо провести некоторую обработку, чтобы принимать решения.
Не требуется никаких обработок для принятия решений.
Это входные данные в компьютер.
Это вывод из компьютера.
Например, результаты теста каждого студента являются данными.
Например, средний балл класса — это информация.
Классификация компьютеров
Компьютеры можно классифицировать по различным параметрам, таким как время, назначение, количество
пользователей, используемые технологии, размер(объём) памяти.
Классификация по поколениям или историческому развитию
Поколения компьютеров основаны на временной шкале, начиная с того момента, когда компьютеры
были
разработаны или построены. Каждое поколение имеет промежуток времени существования: от 10 до 20 лет, и
включает
различные
технологии, используемые для разработки компьютеров, за исключением нулевого поколения. На текущий момент можно
говорить о шести
поколениях:
Классификация компьютеров по поколениям
Номер поколения
Года существования
Описание
0
До 1945
Механические компьютеры. Все компьютеры или другие инструменты, использованные для
вычислений в то время, попадают в эту категорию. Примеры компьютеров нулевого поколения:
Счёты(известно упоминание в 1387 году до нашей эры)
Преимущества: Они были самыми быстрыми вычислительными машинами своего времени.
Недостатки: У них были ограничения применения, и они использовались для решения специфических
задач.
С ними было трудно работать. Они были чисто механическими устройствами. Сегодня они устарели и
имеют ограниченное использование.
1
1946 - 1956
Вакуумные лампы: это поколение стало началом
эры цифровых компьютеров. Они использовали множество вакуумных ламп для вычислений. Для установки
они обычно требовали огромное пространство: комнаты, залы или здания. Для ввода данных
использовались
перфокарты, а для хранения информации - магнитные барабаны. Они программировались на низком уровне,
то
есть в двоичной системе, которая состоит из 0 и 1. Вот несколько машин первого поколения:
Electronic Numerical Integrator and Calculator (ENIAC),
Преимущества: Они были самыми быстрыми машинами своего времени. Быстрее, чем компьютеры нулевого
поколения.
Недостатки: Они были громоздкими по размеру, трудными в эксплуатации, крайне ненадежными и
сложными
в управлении. Слишком перегревались, требовали сложных охлаждающих устройств.
Потребляли много электроэнергии.
2
1957 - 1963
Они использовали транзисторы для своих вычислений. Они были быстрыми и компактными
компьютерами по
сравнению с предыдущим поколением. Они использовали магнитные ленты и диски для вторичного
хранения.
Они использовали перфокарты для ввода информации, полученной в виде распечаток. Их программировали
с
помощью символов или языка Ассемблера, который состоял из специальных символов, таких как SUM,
MUL, LOAD и так далее. Примеры компьютеров второго поколения:
HONEYWELL,
IBM-7030,
CDC1604,
UNIVAC LARC
Преимущества:
Они были самыми быстрыми машинами своего времени.
Размер был меньше по сравнению с компьютерами первого поколения.
Скорость и устойчивость к неисправностям были высокими.
Потребляли меньше энергии и генерировали меньше тепла, чем компьютеры первого поколения.
Программировались на Ассемблере.
Недостатки: Они использовали транзисторы, которые приходилось производить вручную, поэтому
стоимость производства была огромной и очень дорогой.
3
1964 - 1971
Они использовали интегральные схемы (ИС) для своих вычислений. Одна ИС способна выполнять
задачи
множества транзисторов и вакуумных ламп. Это были чипы на основе материала кремния, которые
значительно повысили быстродействие и эффективность компьютеров. ИС были меньше, дешевле,
быстрее и
надежнее, чем транзисторы.
Они использовали ИС с маломасштабной интеграцией (SSI), среднёмасштабной интеграцией (MSI) и
крупномасштабной интеграцией (LSI). Чипы LSI, MSI и LSI могут содержать до десяти, ста и тысячи
электронных компонентов соответственно(транзисторов, резисторов, диодов, конденсаторов и т.д.).
Для первичной памяти использовалось магнитное ядро, а для вторичной памяти - магнитный диск. Для
ввода данных использовались клавиатура и мышь, а для вывода - мониторы. Примеры компьютеров
третьего поколения:
Они были меньше, более надежными, дешевле и потребляли меньше энергии.
У них была большая первичная и вторичная память.
Недостатки:
Они быстро перегревались, поэтому требовалось кондиционирование воздуха для поддержания
температуры.
Для производства интегральных схем требуется очень сложная технология.
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. Хотя пользователь
вводит данные в виде десятичных чисел или в виде символов, внутри компьютера они всегда преобразованы в
цифровую форму нулей и единиц.
Примеры цифровых компьютеров - персональные компьютеры, ноутбуки, смарфоны и так далее.
Аналоговый сигнал отличается от Дискретного(цифрового).
Гибридные - Это сочетание цифровых и аналоговых компьютеров. Они обрабатывают как
аналоговые, так и цифровые сигналы. Они используют аналого-цифровые преобразователи(АЦП - преобразует
аналоговый сигнал в цифровой) и цифро-аналоговые
преобразователи (ЦАП - преобразует цифровой сигнал в аналоговый).
Типичными примерами аналого-цифровых преобразователей являются всевозможные датчки: пожарные датчики дыма,
датчики объёма топлива в баке автомобиля,
датчики давления воды в насосе, и т.п. - т.е. когда параметры окружающей среды или свойства(характеристики)
предмета переводятся из
аналогового вида в цифровой(дискретный).
Типичными примерами цифро-аналоговых преобразователей являются: Аудио-карта в вашем компьютере, Монитор
компьютерный или Экран ноутбука,- каждый пиксель которых состоит из трёх точек: красного, синего и зелёного
цвета. Цифро-аналоговые преобразователи в них преобразуют цифровые данные о каждом цвете в их
непосредственную
интенсивность свечения, и на выходе мы видим разноцветное аналоговое изображение.
преобразователи частоты заданной в цифровом виде преобразуют в скорость вращения электро-мотора,
преобразователи угла заданного в цифровом виде преобразуют в положение заслонки подачи газовой смеси в
камеру,
и т.п. - т.е. когда цифровое значение(числовое) сохранённое внутри компьютера преобразуется в
свойства(характеристики) предмета или окружающей среды в аналоговом виде.
Кроме того, условно, цифровые компьютеры можно поделить на следующие четыре типа:
Микрокомпьютеры: они используют микропроцессор (чип(микросхема), содержащий все компоненты
процессора) в
качестве своего процессора. Эти компьютеры предназначены для одного пользователя, малы по размеру, имеют
небольшую
память и выполняют простые задачи. Примеры: ПК и ноутбуки в базовой комплектации.
Мини-компьютеры: Мини-компьютеры быстрее и мощнее, чем микрокомпьютеры. Эти компьютеры
предназначены
для работы как с одиночными пользователями, так и с многопользовательскими системами. Однако количество
пользователей ограничено. Эти компьютеры являются компьютерами среднего класса и стоят дороже, чем
микрокомпьютеры. У этих компьютеров больше памяти, чем у микрокомпьютеров, и они выполняют сложные
задачи.
Примеры: ПК и ноутбуки многокомпонентной сборки - несколько процессоров, сотни гигабайт ОЗУ, несколько
видеокарт, специальное сетевое и серверное оборудование, специализированное ПО и т.п.
Мэйнфреймы: Мэйнфреймы мощнее мини-компьютеров. Как правило, их используют в качестве серверов.
Эти
многопользовательские компьютеры обладают большей мощностью обработки и памятью, чем мини-компьютеры. Их
используют в крупных организациях, таких как университеты, банки, железнодорожные и авиакомпании, где
большое количество пользователей часто получает доступ к их базам данных. Примеры: серверы, IBM S/390,
Amdahl 58 и так далее.
Суперкомпьютеры: Эти компьютеры являются самыми быстрыми, мощными и дорогими среди всех
компьютеров.
Суперкомпьютеры обрабатывают большое количество данных и используются для решения научных задач, где
требуются быстрые вычисления. Они используются в области мониторинга климата и прогнозирования погоды, и
т.п.
Классификация по количеству пользователей
В зависимости от количества пользователей, взаимодействующих с компьютером одновременно, компьютеры
классифицируются на две категории:
Однопользовательские машины: В этих компьютерах один пользователь может
взаимодействовать с компьютером в одно время и выполнять свои задачи. Примеры включают ноутбуки,
настольные
компьютеры, карманные компьютеры, смартфоны и т.д.
Мультипользовательские компьютеры: они способны одновременно выполнять задачи многих
пользователей. Примеры: серверы, мэйнфреймы и суперкомпьютеры.
Характеристики компьютера
Сегодня современные компьютеры обладают следующими характеристиками:
Автоматичность
Компьютеры по своей сути автоматические, поскольку могут выполнять задачи без
вмешательства пользователя. Пользователям нужно только задать задачу компьютеру, после чего он
автоматически
завершает её в соответствии с инструкциями программы, хранящимися в нём, и выдает окончательный
результат.
Быстродействие
Компьютер – это очень быстрая вычислительная машина. Он может выполнять миллионы,
сотни миллионов и миллиарды операций в секунду. Скорость компьютера обычно измеряют в:
миллионах инструкций в секунду (MIPS - Million Instructions Per Second)
операциях с плавающей запятой в секунду (FLOPS - FLoating-point Operations Per Second)
циклах на инструкцию (CPI - Cycles Per Instruction)
инструкциях в секунду (IPS - Instructions Per Second)
и т.п.
Точность
Компьютеры обеспечивают 100% правильные результаты, при условии, что правильные данные и
программы вводятся в систему. Это работает по принципу "если мусор на входе - то мусор и на выходе"
(GIGO - Garbage In Garbage Out),
что означает, что если вы вводите мусор(неправильные данные или программу) в компьютер, вы получите
мусор(неправильный результат) на выходе в виде информации. Из принципа "если мусор на входе - то мусор и на
выходе"(GIGO) ясно, что выходные данные,
производимые компьютером, полностью зависят от вводимых данных и программы.
Многофункциональность
Роль компьютеров в нынешнюю эпоху многообразна(многофункциональна). Они
могут выполнять множество задач, например, в расчётах, развлекательной деятельности, бизнесе, играх,
обучении и симмуляциях. Таким образом, это многофункциональная машина.
Усердие
Машина может выполнять одну и ту же задачу неоднократно, не уставая от этого как человек, с такой же
степенью точности и надёжности, как и в первый раз. Поэтому, в отличие от человека, она никогда не
утомляется, не теряет концентрацию и свободна от эмоций.
Нулевой уровень интеллекта
IQ компьютера равен нулю, потому что он не может выполнять никакие задачи
самостоятельно и не обладает способностью сопоставления решений. Тем не менее, люди пытаются сделать
компьютеры умными с
помощью концепций искусственного интеллекта.
Память
Компьютер может хранить данные без их потери в течение нескольких лет во вторичной памяти.
Сохранённые данные могут извлекаться и использоваться по мере необходимости. Он не теряет данные, пока
его
не попросят это сделать. Те же данные можно получить через несколько лет в том же виде и содержании, как
и в
день их ввода в память.
Экономичность
Компьютеры могут приносить большую ценность, в отношении к своей стоимости как изделия, если их
правильно использовать для экономии времени, денег и энергии. Например, если мы хотим отправить письмо
или документ обычной почтой, это займет больше времени, ресурсов и энергии, чем электронное письмо, которое
отправляется мгновенно по сети.
Преимущества компьютера
Отметим некоторые преимущества компьютеров, компьютеры могут:
Простое копирование данных в цифровом виде
Выполнять сложные вычисления очень быстро
Хранить полезную информацию продолжительное время
Выполнять задачи самостоятельно, то есть автоматически, по предоставленным данным и программам
Выдавать пользователям информацию в понятном им виде
Выполнять однотипную работу очень быстро
Сортировать, организовывать и искать большие объёмы данных и информации
Экономить много времени, выполняя задачи невероятно быстро по сравнению с людьми
Увеличивает количество способов связи и каналов взаимодействий, то есть посредством интернета, телефона,
сотовой сети и т.п.
Ограничения компьютера
Однако, есть у электронных вычислительных машин и недостатки:
Нет здравого смысла, так как у него нулевой IQ
Не может принимать решения самостоятельно
Не может исправить неправильно написанную программу
Полностью зависит от человеческих программ
Постоянная угроза неавторизованного доступа к информации с какой-либо незаконной целью
Безопасность данных и информации является серьезной проблемой
Вирусы и хакерские атаки позволяют красть или повредить вашу информацию без вашего ведома
Увеличивает уровень рисков киберпреступности внутри организаций
Долгое(более 5-ти часов в день) сидение за компьютером вызывает проблемы со здоровьем, такие как
ухудшение зрения, боли в шее, спине, головные боли, защемление нервов в руках, позвоночнике, ногах, стресс,
низкое давление, инсульты и т.п.
В виду того, что Информационные Технологии постоянно развиваются - программист должен непрерывно учиться
и переучиваться, что бы обладать актуальными знаниями в своей сфере деятельности и в смежных областях.
Однако с возрастом у человека интеллектуальные способности ухудшаются из-за старения организма и
поддерживать непрерывность самообразование становится сложнее.
Непрерывное потребление электрической энергии и как следствие уничтожение ископаемых(извлекаемых из недр
планеты)
природных ресурсов-энергоносителей таких как: нефть, уголь, газ, ядерное топливо, вода и т.п.
Непрерывное развитие и инновации в электронной промышленности ведут к необходимости добывать ископаемые
и не ископаемые материалы природных ресурсов таких как: вода, редкие металлы, полимеры и пластик, стекло,
газы и т.п.
После срока службы, электронный мусор - все компоненты вычислительных устройств выбрасывают в
окружающую среду - на свалку. Но так как внутри этих компонентов находятся выше описанные материалы, а
извлечь их обратно для применения в электронной промышленности крайне тяжело, то встал вопрос о развитии
технологий переработки электронного мусора. На текущий момент, полноценно работающих перерабатывающих циклов
не существует, поэтому накопление электронного мусора на планете растёт, а количество ископаемых материалов
заканчивается. Многие материалы вредны для человека, животных, растений, воды и грунта - и вызывают болезни.
Такие материалы как металлы, стекло, пластики практически не разлагаются и будут лежать в грунте
тысячелетиями или дольше.
Применения компьютера
В настоящее время компьютер является универсальной машиной. Вот некоторые из областей человеческой
деятельности, где используются
компьютеры постоянно:
Редактирование и обработка текстов - Одним из основных приложений компьютеров является текстовый
процессор.
Он создает, организует, читает и пишет документы Word. Некоторые программы для обработки текста - это MS
Office, PDF-ридер и так далее.
Банковские услуги - Компьютер является основой банковской системы, поскольку мир движется к
безналичному
обществу. Человек может выполнять все виды транзакций, не посещая банк физически, через электронное
банкинг
в любое время и в любом месте. Ваши деньги всегда остаются с вами круглосуточно.
Развлечения - Сегодня компьютер не ограничивается расчетами. Он также используется для
развлечений.
Некоторые примеры включают компьютерные игры, просмотр фильмов и прослушивание песен и так далее.
Наука и технология - Компьютер очень полезен для выполнения научных расчетов с быстротой и
точностью. Он
используется для научных экспериментов, сбора и анализа данных и так далее. Компьютер также используется
в
симуляции для обучения и тестирования.
Образование и обучение - Сегодня компьютер является необходимостью как для учителя, так и для
студента.
Компьютер помогает студентам как в классе (умных классах), так и вне его эффективно усваивать концепции.
Некоторые из применений компьютеров в образовании — это онлайн-библиотеки, электронные книги,
образовательные сайты, электронное обучение, видео-лекции, видеодемонстрации и так далее.
Электронная коммерция - Электронная коммерция также известна как электронная торговля. Это
процесс
покупки
и продажи товаров через интернет (онлайн). Покупатель может приобрести товары онлайн, не выходя на рынок
физически, а продавец может продавать свой товар, не открывая магазин/рынок. Средства переводятся с
помощью
электронного банкинга, то есть онлайн-банкинга/кредитной карты/дебетовой карты и так далее.
Автоматизация предприятий - Робот — это машина, управляемая компьютером, которая используется
для автоматического выполнения задач в экстремальных условиях, таких как высокая температура и высокое
давление, без вмешательства человека. Логистика и Склады хранения, учёта, сортировки и отправки товаров.
Научные исследования - Для проведения исследований компьютер является обязательным устройством.
Он используется для всех исследовательских действий, таких как определение проблемы, обзор литературы, сбор
образцов, проектирование исследования, анализ данных и проведение экспериментов.
Коммуникационные технологии и связь - В современном мире компьютер используется для быстрой
передачи наших данных с помощью различных сетей. Некоторые из способов общения включают: электронную почту,
голосовые и видео звонки, в социальных сетях происходит обмен текстами, изображениями и видео, и так далее.
Хранение данных и доступ к информации через Интернет - Видео-хостинги, хостинги изображений,
трансляции видео и аудио информацией.
Автоматизация окружающей среды - Интернет Вещей, Интернет Всего - эти технологии строятся на
микро- и нано-компьютерах(на основе микроконтроллеров), позволяющих собирать данные от датчиков размещаемых
в окружающей среде и на устройствах и предметах. Полученные данные обрабатываются и выдаётся информация о
необходимости выполнения определённых действий. Например: системы противопожарной защиты зданий(пожарные
сигнализации), охранные сигнализации, системы охранного видео наблюдения, мониторинг состояния влажности
почвы, радио-сети обмена сообщениями без интернета.
Искусственный Интеллект - Развивающаяся область информационных технологий вобравшая в себя
практически все выше описанные применения компьютеров.
Заключение
Эта глава кратко разъяснила вам основные компоненты компьютеров, которые включают в себя фундаментальные
составляющие компьютера, такие как блок ввода, блок вывода, устройства памяти и процессор. Также было
удалённо
внимание работе компьютеров и их компонентов, компьютерным приложениям и ограничениям. Рассмотрена детальная
классификация компьютеров по времени, историческому развитию, назначению, используемой технологии и
количеству
пользователей. Также обсуждены различные характеристики компьютера, такие как быстродействие,
многофункциональность, автоматичность и др.
Теперь вы знаете что такое центральный процессор и его компоненты, память и её типы. Узнали о том как
инструкции/команды/выражения извлекаются из памяти и выполняются процессором. Получили представление об
иерархии памяти, её размерах и скорости работы.
Важные моменты
Вспомните следующие важные моменты, и подумайте о них:
Компьютер — это электронное устройство, которое принимает данные и команды (инструкции) в качестве
входных
данных от пользователя, обрабатывает данные и производит информацию в качестве выхода.
Компьютер следует циклу «вход-обработка-выход». Компьютерные блоки соединены через системные шины
(провода).
Существует три основные шины:
шина данных, шина управления и шина инструкций.
ЦП известен как мозг компьютера.
Устройство Управления действует как центральная нервная система компьютера.
Регистр — это мгновенно доступная память внутри ЦП.
Основная память хранит все текущие временные данные/инструкции во время работы компьютера.
Вторичная память используется для резервного хранения (постоянного хранения) данных, чтобы они могли
быть
использованы в будущем.
Данные — это сырые факты и цифры, состоящие из символов, таких как числа и буквы алфавита. Они
неорганизованны по своей природе.
Информация производится из данных.
Компьютеры нулевого поколения были механическими компьютерами.
Компьютеры
первого поколения использовали много вакуумных трубок для вычислений.
Компьютеры второго поколения использовали
транзисторы для вычислений.
Компьютеры третьего поколения использовали интегральные схемы для вычислений.
Аналоговые компьютеры используются для измерения аналоговых сигналов.
Цифровые компьютеры используются для
обработки цифровых сигналов, то есть 0 и 1.
Гибридные компьютеры являются комбинацией цифровых и аналоговых
компьютеров.
Микрокомпьютеры используют микропроцессоры (чип, содержащий все компоненты ЦП) в качестве своего
ЦП.
Мини-компьютеры быстрее и мощнее микрокомпьютеров.
Мейнфреймы мощнее мини-компьютеров.
Суперкомпьютеры самые
быстрые, самые мощные и самые дорогие среди всех компьютеров.
В однопользовательских компьютерах один пользователь
может взаимодействовать с компьютером в одно время.
Многопользовательские компьютеры способны
выполнять/обрабатывать задачи многих пользователей одновременно.
Быстродействие компьютера можно измерять в терминах
MIPS, FLOPS, CPI, IPS и так далее.
Компьютеры предоставляют 100% правильные результаты, при условии, что
корректные данные и программы введены в систему.
Компьютеры универсальны. Вот почему они могут выполнять
разноплановые задачи.
IQ компьютера равен нулю.
Компьютеры следуют принципу "мусор на вводе - мусор на выводе"(GIGO).
Важные вопросы
Дайте ответы на вопросы ниже, если затрудняетесь - прочитайте ещё раз текст выше:
Что такое компьютер? Объясните из чего он состоит. Приведите области его применения.
В чём различие между данными и информацией. Что более полезно для человека?
Чем отличаются первичная и вторичная память.
Что такое шина? Сколько типов шин вы уже знаете. Объясните назначение каждой шины.
Какие различные вычислительные устройства использовались в разных поколениях? Объясните главные их
отличия.
Что такое ИС - Интегральная Схема? Укажите различные их типы.
Объясните поколения компьютеров. Сравните поколения между собой. Какое из них лучше и почему?
Классифицируйте компьютеры на основе их исторического развития, назначения, использованной
технологии, числа пользователей и размера.
Обсудите разницу между микро-компьютерами и мини-компьютерами.
Сравните супер-компьютеры и мейнфреймы. Также опишите их области применения.
Опишите основные характеристики компьютера.
Что такое концепция "мусор на вводе - мусор на выводе"(GIGO)? Объясните её.
Эта глава углубится в суть компьютерных систем — Центрального процессора (ЦП) и его ключевую роль в выполнении
инструкций. Мы раскроем сложные механизмы работы ЦП, шаги, которые он предпринимает для выполнения одной
инструкции, и важность скорости ЦП. Кроме того, мы исследуем единицы памяти, от быстрой основной памяти до
объёмной вторичной памяти, и как они формируют основу иерархии памяти компьютера.
Цель этой главы - предоставить полное понимание основных компонентов аппаратного обеспечения компьютера и их
взаимодействия. Читатели получат представление о Центральном Процессорном Устройстве (ЦП) и процессе
выполнения его инструкций, поймут значение скорости ЦП, изучат различные единицы памяти, осознают иерархию
памяти и научатся эффективно измерять и управлять компьютерной памятью.
Процессор
Процесс обработки данных в соответствии с командой, заданной пользователем, называется обработкой. Центральный
процессор (CPU) работает как единица обработки в компьютере. Он выполняет вычисления и операции обработки
данных на данных, введённых с помощью устройства ввода. Его также называют мозгом компьютера.
Из чего состоит Процессор
Основные компоненты ЦП следующие:
Арифметико-логическое устройство (АЛУ)
Блок Управления
Регистры Процессора
КЭШ-память процессора
Арифметико-Логический Блок
Арифметико-логическое устройство отвечает как за арифметические, так и за логические функции. Арифметические
функции включают сложение, вычитание, умножение, деление и т.д., а логические операции - И, ИЛИ, НЕ и т.д.
Блок Управления
Управляющее устройство служит нервной системой компьютера. Оно генерирует управляющие сигналы, которые
управляют и контролируют все компоненты компьютера. Например, чтобы сложить два числа, оно выполняет следующую
операцию:
Извлекает (получает) числа для сложения из памяти.
Извлекает инструкцию из памяти (в данном случае инструкция – сложение).
Декодирует инструкцию.
Запрашивает АЛУ выполнить операцию согласно инструкции.
Сохраняет итоговый результат в памяти.
Регистры Процессора
Регистры - это малогабаритная, быстро доступная память внутри ЦП. Они состоят из небольшого количества быстрой
памяти. Размер регистров измеряется количеством бит, которые они могут содержать, например, 8-битный регистр,
16-битный регистр, 32-битный регистр, 64-битный регистр и так далее. Компьютер с большим количеством регистров
может обрабатывать больше информации и наоборот. Регистры могут быть сгруппированы на две группы, которые
следующие:
Регистры Общего Назначения: могут хранить данные и адреса. Регистр Данных может хранить только
данные, а Регистр Адреса может хранить только адрес. Регистры Общего Назначения – это регистры, которые
можут хранить и то, и другое.
Регистры Специального Назначения: - это регистры, предназначенные для некоторых специальных
целей. Обычно они хранят состояние программы, например:
Регистры счетчика команд: Адрес следующей инструкции, которая должна быть выполнена,
хранится в этом Регистре Счётчика.
Регистры адресов памяти: Эти регистры хранят адреса области памяти, из которой
извлекаются или в которую сохраняются данные.
Регистры данных памяти: Они работают как буфер между памятью и ЦП.
Аккумулятор: он взаимодействует с АЛУ и хранит результаты ввода/вывода.
Регистр инструкций: он содержит текущую выполняемую инструкцию.
Инструкции внутри Процессора: Работа Процессора
Основная функция компьютера — выполнять программу (набор инструкций). Формат инструкции показан ниже.
Согласно архитектуре компьютера по модели фон Неймана или концепции хранения программы, данные и инструкции
(программа) хранятся в памяти. ЦП должен выполнить следующие два шага для обработки инструкции:
В цикле выборки процессор читает (извлекает) инструкцию из памяти.
Инструкция выполняется процессором в цикле исполнения.
Инструкция состоит из двух частей:
Код операции(Опкод)
Операнды
<Код_операции> <Операнд>
Опкод указывает, что должно быть сделано, например, сложение, умножение, деление и так далее. Он обозначает код
операции. Операнды указывают данные/адрес, к которым будет применен опкод. Рассмотрим следующее арифметическое
выражение:
a = b + c
Здесь a, b, c являются операндами, а +, = кодами операций. Инструкция выполняется процессором с
помощью
цикла инструкций или машинного цикла. Машинный цикл состоит из следующих двух циклов:
Цикл выборки
Цикл выполнения
Рассмотрим работу машинного цикла на блок-схеме:
Пошаговое выполнение одной инструкции
Когда ЦП выполняет инструкцию, он должен следовать ряду шагов, которые представлены на рисунке. Давайте
рассмотрим инструкцию сложения двух чисел. Здесь инструкция - ADD, а данные - 50 и 60. ЦП извлекает и выполняет
одну инструкцию с каждым тактом тактового генератора. Если ЦП завершает один машинный цикл за одну секунду, это
называется
одним герцем. Для выполнения одной инструкции выполняются следующие шаги:
Извлечь инструкции и данные из основной памяти
Декодировать инструкцию.
Выполнить команду, то есть ADD(Сложение двух операндов).
Сохранить результат в основной памяти.
Быстродействие Процессора
Скорость ЦП измеряется в IPS( англ. instructions per second ), что означает "инструкций в секунду". Это
указывает на то, сколько инструкций выполняется процессором за одну секунду.
Существует три основных фактора, влияющих на скорость выполнения инструкций ЦП:
Тактовая частота,
Количество используемых ядер,
КЭШ-память.
Частота тактового сигнала
Тактовая частота относится к скорости, с которой ЦП может выполнять инструкции.
Она измеряется в герцах(Гц).
Тактовый генератор управляет работой ЦП.
ЦП извлекает и выполняет одну инструкцию с каждым тактом тактового генератора.
Циклы в секунду являются единицей измерения скорости тактовой частоты, и один герц равен одному
циклу в одну секунду.
Когда тактовая частота ЦП выше, он может обрабатывать инструкции с более высокой скоростью.
Процессор с тактовой частотой 5,0 ГигаГерц(ГГц) выполняет 5 миллиардов циклов в секунду.
Общее количество ядер процессора
ЦП состоит из элемента, известного как ядро.
Обычно ЦП состоит из одноядерного процессора.
Большинство современных центральных процессоров имеют два, четыре и больше ядер.
Например, двухъядерный ЦП содержит два ядра, в то время как четырёхъядерный ЦП содержит четыре ядра.
Одноядерный процессор может извлекать и выполнять только одну инструкцию за раз, тогда как двухъядерный
процессор может извлекать и выполнять две
инструкции за раз.
ЦП с четырьмя ядрами может выполнять ещё больше инструкций за то же время, чем процессор только с двумя
ядрами.
КЭШ-память процессора
Кэш - это память небольшого размера, которая работает на очень высокой скорости и расположена в ЦП. Он
содержит данные и инструкции, которые используются снова и снова.
Чем больше кэш, тем быстрее часто используемые инструкции и данные могут быть переданы в процессор и
использованы.
Это связано с тем, что для их хранения в кэше выделяется больше места.
Блок Памяти
Ячейки памяти отвечают не только за хранение данных, но и за хранение инструкций(программы). Данные и программа
извлекаются из памяти центральным процессором (ЦП), который затем выполняет над ними операции (обрабатывает
их). Если есть какие-либо промежуточные результаты, они также сохраняются в памяти. Финальный результат,
который генерируется ЦП, сохраняется в памяти.
Оперативная память является временным хранилищем, также известным как рабочая память или основная память. Эта
память напрямую (в первую очередь) доступна ЦП, и поэтому она известна как основная память. Она хранит все
текущие временные данные/инструкции во время работы компьютера. Оперативная память дополнительно делится на
следующие три части:
ОЗУ(RAM)
ПЗУ(ROM)
КЭШ(Cache)
В общем, ОЗУ считается основной памятью.
Она является энергозависимой - данные стираются, когда компьютер выключен.
Вторичная память не подключена напрямую к ЦП. Она также называется вспомогательной памятью или третичной
памятью.
Оперативное Запоминающее
Устройство
ОЗУ - это сокращение от Оперативное Запоминающее Устройство.
Оно хранит данные и инструкции перед обработкой их процессором
и также хранит результат этой обработки.
Оно известно как память с произвольным доступ (сокр.англ. - RAM - Random Accesses Memory - Память со случайным
доступом), потому что сохраненное
содержимое может быть напрямую получено из любого места произвольно или в любом порядке.
Данные, которые в данный момент используются процессором, находятся в нём.
Оно временно хранит данные или инструкции, так как данные стираются, когда отключается питание.
Оно требует постоянно подключенного источника питания для хранения данных.
Чтобы запустить данные или программу на компьютере, они сначала должны быть загружены в ОЗУ.
Если памяти ОЗУ слишком мало, она не может удержать все необходимые данные
или программы, которые нужны процессору. Когда это происходит, процессору приходится получать необходимые
данные из вторичной памяти, что очень медленно и замедляет работу компьютера.
Чтобы решить эту проблему, достаточно увеличить размер ОЗУ в компьютере. Существует два типа ОЗУ:
Статическая оперативная память (Static RAM): Основными компонентами, используемыми для хранения
данных в SRAM,
являются транзисторы. Чипы SRAM используются в качестве кэш-памяти в компьютере. SRAM быстрее, чем DRAM.
SRAM стоит дороже, чем DRAM. SRAM занимает больше места, чем DRAM. Следовательно, плотность данных в SRAM
ниже по сравнению с DRAM. Генерирует больше тепла по сравнению с ROM и DRAM. Утечка заряда не происходит в
SRAM; следовательно, обновление не требуется, как в DRAM.
Динамическая оперативная память (Dynamic RAM): основными компонентами, используемыми для хранения
данных в DRAM, являются конденсаторы.
Поскольку конденсатор со временем теряет заряд(разряжается), его нужно постоянно подзаряжать;
эта концепция известна как обновление(и это процесс динамический).
Таким образом, необходимо постоянное обновление ячеек памяти в DRAM.
Обновление делает DRAM медленнее, чем SRAM.
DRAM используется в качестве оперативной памяти в компьютере.
DRAM дешевле, чем SRAM.
Плотность хранения данных в DRAM больше, чем в SRAM.
Следовательно, она занимает меньше места. Обычно выделяет меньше тепла по сравнению с SRAM.
Постоянное Запоминающее
Устройство
ПЗУ означает энергонезависимую память и относится к основной памяти(англ. - Read Only Memory -
Постоянное Запоминающее Устройство). Это энергонезависимая память, поскольку
она сохраняет данные даже при отключении питания. Она не требует постоянного источника питания для хранения
данных. Это постоянная память. ПЗУ сохраняет фиксированные инструкции загрузки, используемые для процесса
загрузки (для включения компьютера при подаче питания). ПЗУ можно только читать, его нельзя записывать или
стирать пользователем компьютера. Запись осуществляется только один раз. Производитель компьютера хранит
инструкции загрузки в ПЗУ. Компьютеры содержат небольшое количество ПЗУ, которое хранит такие программы, как
базовая система ввода-вывода (англ. BIOS - Base Input Output System), используемая для загрузки
компьютера при его включении.
В процессе загрузки операционная система копируется из вторичной памяти(например с SSD(англ. Solid State
Drive - Накопитель Твёрдого Состояния(твердотельный(не механический) накопитель))) в основную
память(ОЗУ).
Изначально, ПЗУ (память только для чтения) была действительно памятью только для чтения. Поэтому, чтобы
обновить программу, хранящуюся в них, чипы ПЗУ приходилось извлекать и заменять другими обновленными ПЗУ с
новыми программами. Но сегодня чипы ПЗУ уже не только для чтения. Их можно стирать или обновлять,
доступны Программируемые ПЗУ(ППЗУ), они бывают следующих типов:
Программируемое ROM (PROM - Programmable Read-Only Memory). Также известно как одноразовая
программируемая ПЗУ. ПЗУ записывается/программируется с помощью специального устройства, известного как
программатор ПЗУ. В ПЗУ данные программируются сразу и не могут быть обновлены или удалены. Работа ПЗУ
похожа на CD-ROM, где данные записываются на CD-ROM с помощью CD-привода. Записанные данные могут
читаться множество раз, но записаны только один раз. Поэтому программирование ПЗУ также называется
'прожигом', так же как CD-R. PROM широко используется в ПК для запуска программ и настроек BIOS.
Программы хранимые в PROM известны как встроенное ПО или прошивки(прошивка - прожигается элемент
высоким напряжением, на его месте образуется отверстие, которое не проводит ток - и при опросе этого участка
памяти в ответ выдаётся логический 0, так же если элемент остался целый - не прожжённый, проводник остаётся
целым и при опросе - выдаёт логическую 1).
Стираемое Программируемое ПЗУ (EPROM - Erasable Programmable Read-Only Memory):
Она может быть стерта и перепрограммирована/перезаписана. Чип(микросхему) EPROM можно
стереть, поместив его под ультрафиолетовый свет, обычно на десять или более минут, а затем её можно
записать с помощью процесса прожига, который требует высокого напряжения. EPROM более полезен, чем PROM.
Его можно сравнить с CD-RW, который является многоразово-перезаписываемой памятью.
EPROM широко используется в встроенных и специальных системах.
EPROM также используется для хранения данных в научных инструментах.
EPROM также известен как Ультрафиолетовое EPROM (UV-EPROM - Ultraviolet EPROM).
Электрически Стираемое Программируемое ПЗУ (EEPROM) (EEPROM - Electrically Erasable Read-Only
Memory): Его можно стереть и переписать с помощью электричества.
Процесс записи во встроенную программируемую память EEPROM известен как "флеширование"*.
Также иногда обозначается как E2PROM.
Это также форма энергонезависимой памяти.
*Флэш-память - это тип EEPROM, в котором данные стираются под контролем программного обеспечения. Это
наиболее гибкая память среди ПЗУ, так как данные можно легко стирать и записывать. Она используется как
память флеш-накопителей в MP3-плеерах, внутренней памяти смартфонов, цифровых фото-камерах, картах
памяти, SSD и во множестве
устройств переносной электроники.
Несмотря на то, что современная флэш-память надёжна, перезапись в ней осуществляется с
помощью
электричества большего напряжения чем чтения, и по этому возможен выход из строя(поломка) ячейки
хранения.
Для компенсации вышедших из строя ячеек используются специальные контроллеры внутри накопителей
флеш-памяти, которые реализуют алгоритмы отключения повреждённых ячеек. Т.е. со временем флеш-память
может
дойти до такого состояния когда количество повреждённых ячеек может стать критичным, и тогда скорость
её
работы либо сильно замедляется или и вовсе становится не возможной.
Вторичная (Внешняя) Память
Вторичная память — это постоянная память, которая хранит данные и программы, которые в текущий момент не
используются. Она обеспечивает долгосрочное хранение. Она хранит данные или программы, которые могут быть
использованы в будущем. Она используется для хранения данных и программ с целью их резервного копирования.
Она хранит данные даже когда компьютер выключен, так как она является энергонезависимой.
Вторичная память не подключена напрямую к центральному процессору (ЦП).
Сначала процессор обращается к первичной памяти(поэтому её и называют первичной памятью).
Если данные не найдены в первичной памяти, ЦА обращается к вторичной памяти.
Вторичная память значительно медленнее первичной памяти, т.о. данные, хранящиеся в основной памяти, доступны
за наносекунду, а данные из вторичной памяти могут быть получены за миллисекунду(в тысячу раз медленнее).
Вторичную память можно классифицировать двумя типами:
Память с Последовательным доступом — На основе различных магнитных лент, проволок; оптических плёнок.
Память с Произвольным доступом — На основе различных магнитных дисков; оптических дисков; твердотельных
накопителей и микросхем DRAM/SRAM.
Некоторые вторичные запоминающие устройства являются переносными-устройствами — это устройства, на которых
данные
записываются и физически удаляются или отключаются от компьютера, например, CD, DVD, магнитная лента и так
далее. Встроенная-память не может быть физически удалена или отключена от компьютера, например, ОЗУ, ПЗУ и т.п.
Широкая классификация компьютерной памяти может быть представлена на схеме ниже:
Последовательный доступ к памяти: магнитная лента
Предоставляет собой последовательный доступ к данным из носителя памяти; например, если нам нужно получить
доступ к N-му
месту данных, то нам сначала нужно пройти через все предыдущие N–1 места. Магнитная лента является хорошим
примером носителя памяти с последовательным доступом. Магнитная лента состоит из барабана и собственно ленты,
которая покрыта
слоем магнитного материала. Магнитные ленты доступны в виде кассет, катушек и картриджей.
Магнитные накопители до сих пор используются в информационных технологиях.
Информационная ёмкость магнитной ленты = плотность записи данных * длина ленты
Количество данных, которые могут быть сохранены на определенной длине ленты, называется плотностью записи
данных. Этот параметр измеряется в bpi (англ. bits per inch - бит на дюйм). Магнитный ленточный
накопитель читает и записывает данные на магнитную ленту. Механизм чтения и записи состоит из головки
чтения/записи и
головки стирания.
Предварительно записанные данные стираются головкой стирания и записываются/читаются головкой записи/чтения.
Магнитная проволока и Оптическая Лента, принципиально по принципу работы ничем не отличаются. Разница лишь в
конструкции механизмов чтения/записи/стирания.
Полу-свободный доступ к памяти
Используется как последовательный доступ, так и метод произвольного доступа к данным.
Поэтому магнитные (жесткий диск, дискета) и оптические (CD и DVD) устройства являются носителями с
полу-произвольным доступом к памяти.
Диск разделён на дорожки и сектора. Концентрические круги это дорожки. Секторы представляют собой области в
форме дуги, отделённые условными линиями. Данные хранятся на поверхности диска и строго позиционируются в
дорожках и
секторах.
Оптический диск: Оптические диски используют луч лазерного света для чтения и записи данных на плоском
круглом
диске, покрытом специальным материалом (часто алюминием). Запись данных на диск называется прожигом
диска,
потому что лазерный свет сжигает материал, покрывающий диск, во время записи данных на диск. Данные хранятся в
виде канавок (двоичной единицы или «включено» (из-за отражения)) и впадинок (двоичного нуля
или «выключено»
(отсутствие отражения)) на поверхности диска, формируемой лазерным лучом. Рисунок показывает поверхность
оптического диска с канавками и впадинками.
Плотность записи на различные оптические диски. Характеристики размера впадинок(питов) и длинны
волны лазеров.
Иерархия памяти
В современном компьютере взаимодействие памяти с ЦП основано на так называемой иерархии памяти,
для повышения производительности и снижения стоимости компьютера.
Иерархия памяти означает размещение её на разных уровнях в компьютере в зависимости от дальности размещения
от Центрального Процессора.
Иерархию памяти можно представить в виде пирамиды, где скорость и
стоимость памяти постепенно уменьшаются к основанию пирамиды, но увеличивается её объём.
Так например 10 ГБ основной памяти дороже, чем 10 ГБ вторичной памяти.
Время доступа ЦП к КЭШу гораздо меньше, чем к основной памяти, а размер
увеличивается, т.о. размер КЭШа несоизмеримо мал по сравнению с размером
основной памяти.
Память более высокого уровня (на вершине пирамиды) - ближе к ЦП(находится прямо внутри ЦП), она быстрее и
дороже, однако её объём крайне
мал.
В то время как на более низком уровне (максимально далеко от ЦП) память очень медленная и
дешёвая, а вот её объём огромный.
Память наивысшего уровня в иерархии включает в себя РЕГИСТРЫ Центрального Процессора, потом
КЭШ-память, потом основную память и так далее(вниз к основанию пирамиды).
Память более низкого уровня включает в себя Вторичную память, то есть магнитные и оптические диски,
магнитные ленты, "флешки" и так далее.
Триггеры или Защёлки отвечают за хранение одного бита данных, и могут хранить его состояние либо в 1-це, либо в
0-ле.
Триггеры являются ключевым компонентом для создания регистров ЦП.
Регистры ЦП расположены внутри ЦП и, следовательно, непосредственно
доступны Процессору.
Регистры ЦП хранят данные в виде так называемых битовых слов данных размером в 8, 16, 32, 64 бита и
так далее.
КЭШ расположен между ультра-быстрыми регистрами и основной памятью. Он хранит часто используемые
данные, которые нужно использовать Процессору снова и снова. КЭШ состоит из чипов SRAM.
Хранение повторно необходимых данных позволяет избегать запроса доступа к данным в более медленной памяти
(DRAM — основной памяти) со стороны Процессора, что увеличивает производительность компьютера,
так как SRAM быстрее, чем чипы DRAM. Кэш-память обычно делится на уровни:
L1 КЭШ: всегда находится на чипе процессора (внутренний кэш).
L2 КЭШ: обычно находится на чипе процессора (внутренний кэш). Его размер больше, чем у L1.
L3 КЭШ: обычно находится на чипе процессора (внутренний кэш). L3 больше, чем L1 и L2 кэш, но быстрее
основной памяти.
Размеры КЭШа в современных Процессорах суммарно уже измеряются в МегаБайтах или десятках МегаБайт.
Размеры ОЗУ в современных компьютерах уже измеряются в десятках ГигаБайт.
Размеры Вторичной памяти в современных компьютерах суммарно уже измеряются в ТераБайтах или десятках ТераБайт.
Измерение памяти
Наименьшая единица измерения памяти — это бит. Один бит означает либо 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-битные компьютеры и т.п.
Сколько байт в 8 МБ? Объяснение: (8) × (1024) × (1024) байт = 8,388,608 байт.
Сколько бит в 1 гигабайте? Объяснение: (1,024) × (1,024) × (1,024) × 8 бит = 8,589,934,592 бит
Сколько бит в 8 гигабайтах? Объяснение: ((8) × (1,024) × (1,024) × (1,024) байт) × 8 бит = 68,719,476,736
бит
20 гигабайт = (20) × (1,024) Мегабайт
Один миллиард символов = 1 Гигабайт. Объяснение: 1 гигабайт = 109 байт = один миллиард байт, где
один
байт эквивалентен одному символу.
Если один гигабайт равен 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 бит.
Если у вас есть устройство хранения объёмом 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.
В памяти мы имеем 22,000 килобайт свободного пространства — примерно, сколько это мегабайт? Объяснение:
Достаточно поделить 22,000 килобайт на 1024(т.е. один килобайт) = 21.484375 мегабайт, т.е. около 21
мегабайта.
Какой степени двойки эквивалентно 8 гигабайт в байтах? Объяснение: 23 × 230 =
233 байт.
У одного человека 1700 мегабайт данных на флешке, а у другого человека на флешке 1500 мегабайт данных.
Поместятся ли на флешку третьего человека эти файлы, если размер флешки 4 гигабайта? Объяснение:
Суммируем файлы первого и второго человека так: 1700 + 1500 = 3300 мегабайт.
Разделим суммарный объём на 1024 и получим 3.22 гигабайта. 3.22 меньше 4 - значит все файлы поместятся.
Сколько 3 мегабайтных фотографий поместится на флешке объёмом 30 гигабайт? Объяснение: Переведём 30
гигабайт в мегабайты и разделим на размер фотографии в 3 мегабайта так: (30 × 230) / (3 ×
220) = 10 × 210 = 10 × 1024 = 10,240 фотографий.
Ранжируйте объёмы памяти от наибольшего к наименьшему:
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
килобайт
Заключение
Мы всесторонне осветили Центральный Процессор и Память компьютерной системы. Мы исследовали внутренние
процессы ЦП, включая его инструкции и поэтапное выполнение одной инструкции. Понимание того, как работает ЦП,
имеет решающее значение для понимания общей работы компьютерной системы. Мы также обсудили концепцию скорости
ЦП, подчеркивая её значимость в определении эффективности и производительности компьютера. Мы рассмотрели
основные и вторичные блоки памяти, которые играют важные роли в хранении и извлечении данных.
Кроме того, мы изучили концепцию иерархии памяти, которая заключается в организации памяти на разных уровнях в
зависимости от её скорости, размера и ёмкости. Наконец, мы рассмотрели методы измерения памяти, которые
позволяют нам количественно оценивать и анализировать ёмкость хранения компьютерных систем. Эта глава создала
прочную основу для понимания основных компонентов компьютерной системы и их взаимодействия, проложив путь
для дальнейшего изучения в следующих главах.
В следующей главе будет обсуждаться компьютерное программное обеспечение и его классификация по различным
параметрам. Также будет дано основное введение в операционные системы и их классификации по таким параметрам,
как выполнение программы, количество пользователей и интерфейс. Кроме того, мы исследуем понятие вредоносных
программ.
Важные моменты
ЦП обрабатывает данные от устройств ввода. Это мозг компьютера.
Арифметико-логическое Устройство(или Блок) выполняет арифметические и логические
операции.
Блок Управления(или Устройство Управления) выступает в роли центральной нервной системы компьютера,
генерируя управляющие сигналы для управления
и контроля всех компонентов.
Регистры — это быстрая, доступная память внутри ЦП, хранящая небольшое количество
данных, измеряемое количеством бит, которые она может сохранить.
Регистры Счётчика Команд хранят адрес следующей инструкции.
Регистр Адреса Памяти хранит адрес данных или инструкций, которые необходимо извлечь из основной памяти или
записать в нее.
Регистр Данных работает как буфер между памятью и ЦП.
Аккумулятор взаимодействует с АЛУ и хранит результаты
ввода/вывода.
Текущая выполняемая инструкция хранится в Регистре Инструкций.
Машинный цикл = Цикл выборки + Цикл выполнения.
Тактовая частота относится к скорости выполнения инструкций ЦП и измеряется в Гц (герцах).
Двухядерный процессор содержит два ядра (элемента обработки).
КЭШ — это быстрая память, расположенная между ЦП и основной памятью.
Первичная память — это быстрая и быстро изменяемая память, к которой ЦП
имеет прямой доступ.
Основные компоненты, используемые для хранения данных в SRAM, — это транзисторы.
Основные компоненты, используемые для хранения данных в DRAM, — это конденсаторы.
ПЗУ(ROM) — это акроним для постоянной памяти.
ППЗУ(PROM) — это акроним для программируемой постоянной памяти.
СППЗУ(EPROM) — это акроним для стираемой программируемой
постоянной памяти.
ЭСППЗУ(EEPROM) означает электрически стираемую PROM и часто используется во флэш-памяти, картах памяти,
флеш-накопителях, смартфонах, цифровых камерах и многом другом.
Вторичная память является постоянной.
Магнитный диск состоит из круглой пластины (диска), покрытой магнитным материалом.
Жёсткий диск — это набор магнитных дисков, сгруппированных на вращающемся шпинделе.
Оптические диски используют лазерный луч для чтения и записи данных на плоском круглом диске, покрытом
светочувствительным материалом, обычно алюминием.
Распределение памяти с ЦП для улучшения производительности и снижения затрат называется иерархией памяти.
Триггеры или Защёлки отвечают за хранение одного бита данных, который может быть либо 1, либо 0.
Наименьшая единица измерения памяти — это бит.
Важные вопросы
Что такое процессор? Опишите его компоненты - из чего он состоит.
Какова цель наличия регистров в ЦП? Обсудите регистры разного назначения, которые обычно имеются в
процессоре.
Что такое тактовая частота? Каковы её единицы измерения?
Что такое одноядерные, двухядерные и четырёхядерные и т.д. процессоры?
Опишите цикл инструкций. Также подробно опишите циклы выборки и выполнения.
Запишите части форматов инструкций.
Что именно вы имеете в виду под компьютерной памятью? В чём разница между первичной и вторичной памятью.
Что такое Иерархия памяти? Почему она существует?
Что такое BIOS? Какой тип памяти в нём предпочтителен и почему?
Что такое оперативная память (ОЗУ - RAM)? В чём разница между статической оперативной памятью (SRAM) и
динамической оперативной памятью (DRAM).
Как Процессор обращается к ячейке в памяти? Объясните на примере циклов выборки и выполнения.
Объясните, как работает магнитный диск.
Как данные хранятся на оптических дисках?
Что такое флеш-накопитель? Напишите о его применении.
Кратко объясните важность КЭШа Процессора.
Что такое ПЗУ? Объясните его типы.
Обсудите разные типы оптических запоминающих устройств.
Как бы вы рассчитали объём информации хранимой на магнитной ленте?
Изучим компьютерное программное обеспечение и операционные системы, которые являются основой
современного вычисления. Обсудим классификации программного обеспечения и роль операционной системы
как менеджера ресурсов. Рассмотрим различные типы операционных систем, трансляторы языков
программирования и угрозы вредоносного ПО. Понимание этих тем поможет нам более эффективно
ориентироваться в мире Информационных Технологий.
Эта глава направлена на предоставление всестороннего понимания компьютерного программного обеспечения и
операционных систем. Она охватывает примеры системного программного обеспечения, его классификации и
критическую роль операционной системы как менеджера ресурсов. Кроме того, рассматривается угроза вредоносного
ПО и его потенциальные риски для компьютерных систем.
Программное Обеспечение (ПО)
Программное обеспечение — это группа компьютерных программ, которые указывают компьютеру, что, когда
и как делать для выполнения поставленной задачи.
Программа — это набор компьютерных инструкций или команд, написанных на
языке программирования, таком как C, C++, Java и так далее.
Программное обеспечение в широком смысле делится на две категории:
Прикладное программное обеспечение: разработано для решения
конкретной проблемы или задачи. Оно берет задачу пользователя и программирует её с помощью аппаратного
обеспечения и системного программного обеспечения. Примеры прикладного программного обеспечения: MS Office,
WordPad, блокнот, калькулятор, веб-браузеры и так далее. Каждое из них предназначено для выполнения
конкретной задачи.
Системное программное обеспечение: предназначено для работы в
качестве интерфейса между прикладным программным обеспечением и аппаратным обеспечением. Оно предоставляет
платформу для запуска прикладного программного обеспечения. Оно управляет и контролирует все ресурсы
компьютера, такие как ЦП, память, устройства ввода-вывода и так далее. Системное программное обеспечение
включает в себя драйверы устройств, компиляторы, интерпретаторы, линковщики и загрузчики, BIOS компьютера и
операционные системы, такие как Windows, Linux, UNIX и так далее. На рисунке ниже показана взаимосвязь между
прикладным и системным программным обеспечением:
Взаимоотношение между аппаратным обеспечением и задачами пользователя посредством прикладного и системного
программного
обеспечения.
Примеры системного ПО
Вот некоторые примеры системного программного обеспечения: драйверы устройств, компиляторы, интерпретаторы,
компоновщики, загрузчики, BIOS и Операционные Системы(ОС). Наиболее известные ОС: Windows, Linux, UNIX и др.
Опишем назначение различного Системного ПО:
BIOS:(в разговорной речи - биос ) базовая система ввода-вывода позволяет компьютеру
управлять аппаратным обеспечением, которое
либо подключено к нему в данный момент, либо встроено в него. Он хранится в микросхеме ПЗУ.
Также он известен как встроенное ПО(в разговорной речи - прошивка, микропрограмма,
файл конфигурации).
Это небольшие программы, отвечающие за запуск компьютера (загрузку) и выполнение самотестирования при
включении (англ. POST - Power-On Self-Test - Тестирование при включении). В тесте POST компьютер
проверяет все подключенные устройства.
Драйвер устройства: работает как переводчик между подключенным аппаратным
устройством и компьютером. Он управляет конкретным устройством, для которого предназначен. Драйверы
устройств встроены в операционную систему для многих типов устройств. Например, мышь, клавиатура, джойстик,
видеокарта и так далее. Когда мы подключаем устройство к компьютеру, операционная система начинает искать
его драйверы,
устанавливает их и использует. Такие устройства называются подключи и работай(англ.
plug-and-play). Для некоторых типов устройств нам нужно вручную установить драйверы, такие как
принтеры, сканеры и так далее.
Утилитное программное обеспечение: предназначено для анализа, мониторинга, настройки и
оптимизации параметров компьютера. Это программное обеспечение может использоваться прикладным программным
обеспечением. Некоторые утилиты приведены ниже:
Дефрагментаторы дисков
Антивирусы
ПО резервного копирования
Очистка диска
Мониторинг обновления ПО
Контроль температуры ЦП
и множество других утилит
Прошивки: Они встроены в аппаратное обеспечение (англ. firmware ). Это также известно как
программное обеспечение для
аппаратного обеспечения. Они хранятся в устройствах с энергонезависимой памятью, таких как ПЗУ, СППЗУ или
флеш-память. Примеры прошивок: загрузчики, команды BIOS и т.п.
Классификация программного
обеспечения в зависимости от прав собственности
Открытое программное обеспечение: позволяет пользователю копировать, изменять, использовать или
удалять код программного обеспечения без разрешения разработчика (англ. Open-source software ).
Примеры: Linux, Firefox и Open Office.
Закрытое программное обеспечение: не позволяет пользователю копировать, изменять, использовать
или удалять код программного обеспечения (исходный код) без разрешения разработчика. Также известно как
проприетарное программное обеспечение(англ. Closed source software ). Примеры: MS Windows,
UNIX, Internet Explorer, Opera и Safari.
Бесплатное программное обеспечение: доступно бесплатно для всех пользователей, но не позволяет
пользователю копировать, изменять, использовать
или удалять код программного обеспечения (исходный код) без разрешения разработчика(англ. Freeware ).
Пример: Adobe
Acrobat Reader. Большинство разработчиков программного обеспечения предлагают бесплатное ПО в модели
freemium или shareware, чтобы побудить пользователей купить
более мощную или полную версию.
Условно бесплатное программное обеспечение: это бесплатное программное обеспечение, но не позволяет
пользователю копировать, изменять, использовать или удалять код программного обеспечения (исходный код) без
разрешения разработчика(англ. Freemium). За дополнительные, более мощные функции этого ПО нужно
платить.
Временно бесплатное программное обеспечение: Вначале он доступен бесплатно но не позволяет
пользователю копировать, изменять, использовать или удалять код программного обеспечения (исходный код) без
разрешения разработчика. Пользователям рекомендуется делиться с другими пользователями его копией в течение
ограниченного времени(англ. Shareware). После этого пользователь должен заплатить
за дальнейшее использование. Примеры принципов ограничения пользования(монетизация):
Рекламно-бесплатное программное обеспечение: Это программное обеспечение, поддерживаемое
рекламой. Оно распечатывает рекламу
пользователям, отображает её и генерирует доход от этого(англ. Adware).
Демо-версия программное обеспечение: Программное обеспечение, которое используется для
демонстрации
функции продукта, но имеет ограничения. Оно либо истечёт через установленное время, либо будет иметь
ограниченную функциональность(англ. Demoware).
Испытательный срок программное обеспечение: она может быть запущена в течение ограниченного
времени до истечения срока
действия. Например, 30-дневная пробная версия программного обеспечения истекает через 30 дней. После
этого вам необходимо купить полную версию(англ. Trialware).
Пожертвования-версия программное обеспечение: Это полностью функционирующее программное
обеспечение для пользователей,
которое предлагает пользователям делать добровольные пожертвования программисту или любому третьему
лицу
бенефициару по желанию. Это один из типов бесплатного программного обеспечения(англ.
Donationware).
Урезанный функционал программное обеспечение: программное обеспечение, функциональность
которого была ограничена с единственной
целью — побудить пользователя купить его. Оно повреждает документы, вставляя логотип или водяной знак.
Это программное обеспечение может быть не полностью функциональным, но позволяет пользователю понять,
как оно работает(англ. Crippleware).
Чаще всего используются комбинации выше описанных методов
Операционные Системы (ОС)
Это системное программное обеспечение, которое работает как интерфейс между аппаратным обеспечением и
пользователем и позволяет запускать другое прикладное программное обеспечение. Оно также выполняет функцию
менеджера ресурсов, поскольку управляет ресурсами компьютера, такими как ЦП, память, файлы, устройства
ввода-вывода и так далее. Примеры операционных систем (ОС): Linux, UNIX, Windows, Android и другие.
Рисунок ниже показывает положение операционной системы в компьютере:
Позиция операционной системы в компьютере.
Структура ОС UNIX
Это многослойная структура. Структура операционной системы состоит из ядра(англ. Kernel) и командного
интерпретатора(англ. Shell - оболочка):
Ядро: это самый важный компонент операционной системы. Обычно ядро отвечает за управление памятью,
управление процессами, управление ЦП и так далее. Оно принимает команды от оболочки и сообщает
аппаратному
обеспечению, что нужно выполнять.
Оболочка: она принимает команду от пользователя и передает её ядру. Это интерфейс между ядром и
пользователями. Диаграмма компонентов операционной системы представлена ниже, и показывает
компоненты ОС UNIX:
Компоненты ОС UNIX.
Операционная система как менеджер ресурсов
Операционная система управляет всеми аппаратными ресурсами, такими как ЦП, память, устройства ввода-вывода и
тому подобное, а также программными ресурсами, такими как программы, файлы, процессы, потоки и т. д.
Именно поэтому её называют менеджером ресурсов. Рассмотрим, как операционная система управляет
ресурсами:
Управление процессором(ЦП): Операционная система отвечает за следующие виды управления ЦП:
отслеживает состояние процессора, свободен он или занят;
распределяет и освобождает задания для ЦП;
решает, какое задание должно использовать процессор и сколько времени выделить для задачи.
Управление памятью: Операционная система отвечает за следующие действия по управлению памятью:
выделение и освобождение памяти;
отслеживание свободных и используемых пространств(адресов) памяти.
Управление файлами: Операционная система отвечает за следующие действия по управлению файлами:
Создание и удаление файлов;
Защита файла;
Установка разрешений для файла на чтение-запись и выполнение.
Управление процессами: Операционная система отвечает за следующие виды деятельности по управлению
процессами:
Создание, удаление, приостановка и возобновление процесса;
Обработка синхронизации процессов и взаимных блокировок.
Управление устройствами ввода-вывода: Операционная система отвечает за следующие действия по
управлению устройствами ввода-вывода:
Когда компьютерная система запускается, операционная система идентифицирует и распознает все
подключенные устройства ввода-вывода.
Скрывает от пользователя все внутренние механизмы работы устройств ввода и вывода.
Операционная система общается с устройством с помощью драйвера устройства, который представляет собой
программный модуль, который действует как посредник, переводя команды операционной системы высокого
уровня в специфические для устройства команды на уровне аппаратной реализации.
Операционная система выделяет и освобождает задачи для устройств ввода-вывода.
Классификация операционных систем
Классификация операционных систем может быть выполнена на основе различных критериев, включая пользовательский
интерфейс, количество поддерживаемых пользователей, их функции, назначение, способ выполнения программ и среду,
в которой они работают. Вот некоторые распространенные классификации:
На основе пользовательского интерфейса: В зависимости от интерфейса, с которым пользователь
взаимодействует с компьютером, его классифицировали на две категории:
Символьный пользовательский интерфейс или командный пользовательский интерфейс (не
интерактивный): В этом типе операционной системы пользователь взаимодействует с
компьютером с помощью клавиатуры, вводя различные команды для выполнения некоторых задач или операций.
Сегодня большинство компьютеров используют графический пользовательский интерфейс GUI (англ.
GUI - graphical user interface), а не CUI (англ. CUI - command user interface или
character user interface). Примером
операционной системы с CUI является MS-DOS.
Графический пользовательский интерфейс (GUI) (интерактивный): В этом типе
операционной системы пользователь взаимодействует с компьютером, используя графику, которая включает
иконки, значки, панель навигации, изображения и так далее. Мышь может использоваться для щелчка или
взаимодействия с этими графическими элементами интерфейса. GUI использует как мышь, так и клавиатуру
для
выполнения операций. Он очень прост в использовании по сравнению с CUI. Примеры операционных систем с
GUI: Windows, Android и Linux.
На основе количества пользователей: В зависимости от числа пользователей, операционная система имеет
следующие категории:
Операционная система для одного пользователя: только один пользователь может взаимодействовать
с компьютером одновременно.
Много-пользовательская операционная система: в этой операционной системе более одного
пользователя могут взаимодействовать одновременно через несколько терминалов или сеть.
На основе выполнения программы: Он выполняет программы последовательно. Операционная система в
однопользовательской среде или в базовой системе пакетной обработки выполняет программы последовательно,
причём каждое задание завершается перед началом следующего. Многопрограммная среда позволяет нескольким
программам или процессам работать одновременно. Многопрограммное выполнение используется в современных
операционных системах, таких как Windows, macOS и Linux, для эффективного управления множеством задач. На
основе способа выполнения программ операционная система имеет категории, обсуждаемые в далее.
Последовательное выполнение программы(серийная обработка)
Эта операционная система выполняет одну программу или задачу за раз. После завершения одной программы она
берется за другую. Она последовательно выполняет программы, одну за другой, и по одной за раз. Она выполняет
программу по принципу "первый пришел — первый вышел" (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) бывают двух типов:
Жёсткие операционные системы реального времени: В жёсткой операционной системе реального времени все
процессы должны быть завершены в рамках заданных временных ограничений; в противном случае система потерпит
сбой. Например, если пользователь ожидает результат через 5 секунд, система должна обработать и выдать
результат именно через 5 секунд, а не через 6 или 4 секунды. В жёсткой системе реального времени соблюдение
временного ограничения является обязательным. Если оно не будет соблюдено, эффективность работы системы
потерпит неудачу, и полезность результата станет нулевой после истечения срока. Примерами жестких
операционных систем реального времени являются системы запуска ракет, игры, системы ядерных реакторов,
авиационная промышленность, оборона, робототехника и так далее.
Мягкая операционная система реального времени: В мягкой операционной системе реального времени, даже
если система не успевает уложиться в срок, это не считается неудачей. Конечный результат не является
бесполезным. Значение результата не рассматривается как ноль, даже если срок прошёл. Например, если
некоторые биты теряются во время аудиовизуальной трансляции, это не будет значительной проблемой, но если
потеряно слишком много, пострадает качество трансляции, но трансляция всё равно будет продолжаться.
Трансляторы языков
Это Переводчики или Обработчики языков, они переводят или конвертируют программу с одного языка
программирования, на другой. Например, с языка высокого уровня (англ. HLL - high-level language) на язык
низкого уровня (англ. LLL - low-level language). На рисунке ниже показан процесс работы переводчика языка:
Процесс трансляции одного ЯП(языка программирования) в другой ЯП.
Программа, написанная на языке программирования, известна как исходный код; когда она переводится
на машинный язык, она становится объектным кодом. Языковой транслятор преобразует исходный код в
объектный код.
В общем виде существуют три типа языковых переводчиков, а именно:
Компилятор: это программа, которая преобразует язык высокого уровня (HLL) в язык низкого уровня
(LLL). Она принимает полный HLL-программный код на вход, преобразует его в язык низкого уровня и
уведомляет об ошибках и предупреждениях, если таковые имеются. (англ. Compiler)
Интерпретатор: Он преобразует программу из высокоуровневого языка (HLL) в низкоуровневый язык
(LLL) построчно; одно выражение переводится и выполняется нёмедленно. (англ. Interpreter)
Ассемблер: Ассемблер - это программа, которая переводит программу из ассемблера в машинный код
или LLL. (англ. Assembler)
Дополнительно существуют ещё программы помогающие в написании и выполнении программ:
Редактор: это программа, которая предоставляет платформу для написания, изменения и
редактирования исходного кода или текста программы. (англ. Editor)
Компоновщик: В объектном файле компоновщик ищет и добавляет все библиотеки, необходимые для
создания исполняемого файла. Он объединяет два или более файлов в один файл, например, объектный код от
компилятора и другие библиотечные файлы, в один исполняемый файл.(англ. Linker)
Загрузчик: Он берёт исполняемый код от компоновщика, загружает его в основную память и
подготавливает для выполнения компьютером. Он выделяет в основной памяти пространство для
программы.(англ.
Loader)
Вредоносное ПО
Слово "вредоносное ПО" происходит от двух слов: "вредоносный" и "программное обеспечение". Это вредоносное
программное обеспечение. Оно предназначено для повреждения и уничтожения компьютеров и компьютерных систем. Это
не легитимное программное обеспечение. Основная концепция различных видов вредоносного ПО приведена ниже:
Вирус: это вредоносное программное обеспечение, которое добавляется в файл или документ. Как только
вирус загружен, он остаётся в неактивном состоянии, пока файл не будет открыт и использован. Он может делать
копии самого себя. Вирус использует ресурсы компьютера, такие как ОЗУ, жесткий диск и процессор. (англ.
Virus)
Черви: это куски вредоносного программного обеспечения, которые быстро копируют себя и
распространяются на каждое устройство в сети. Червям не нужен заражённый файл для распространения, как это
требуется вирусам. (англ. Worms)
Троян: Троянские вирусы скрыты в программах, которые призваны помочь. После загрузки
вирус-троян может получить доступ к конфиденциальным данным и редактировать, блокировать или уничтожать их.
В отличие от большинства вирусов и червей, троянские вирусы не созданы для саморепликации. (англ. Trojan
horse virus)
Шпионское ПО: это вредоносное программное обеспечение, которое работает в фоновом
режиме и отправляет информацию удалённому пользователю. Люди часто используют шпионское ПО для кражи личной
или финансовой информации, такой как пароли и т.п. (англ. Spyware)
Вымогательское ПО: это программное обеспечение, которое проникает в
компьютерную систему, находит конфиденциальную информацию, шифрует её так, чтобы пользователь не мог
получить к ней доступ, а затем требует деньги за возврат данных. Когда атакующий оплачивает сумму, данные
могут быть снова доступны. (англ. Ransomware)
Наглое ПО: его также могут называть "выклянчивающим ПО" или "назойливым ПО". Оно напоминает
пользователю о
том, что ему нужно купить лицензию на программное обеспечение. (англ. Naggware)
Логическая бомба: Вредоносное программное обеспечение, которое срабатывает или активируется, когда
выполняется определённое логическое условие, называется логической бомбой, например, любое нажатие мыши,
любое нажатие на клавиатуру и так далее. (англ. Logic bomb)
Временная бомба: Бомба замедленного времени - это тип логической бомбы, которая взрывается или
активируется в определённое время или день. (англ. Time bomb)
Запугивающее ПО: его также называют мошенническим программным обеспечением. Это тип вредоносного ПО,
которое использует социальную инженерию, чтобы заставить пользователей чувствовать себя напуганными, как
будто они в опасности, чтобы они купили нежелательное программное обеспечение. (англ. Scareware)
Заключение
Мы обсудили компьютерное программное обеспечение и то, как его можно классифицировать по нескольким
различным критериям. В дополнение к этому, описали основное введение в операционную систему, а также ее
классификации на основе нескольких факторов, таких как выполнение программы, количество пользователей и
интерфейс. Вы узнали о различных инструментах для трансляции языков программирования.
Также объяснили основы вредоносных программ и различные их виды.
Важные моменты
Программное обеспечение - это группа компьютерных программ, которые указывают компьютеру, что, когда и как
это делать.
Прикладное программное обеспечение предназначено для решения конкретной проблемы или задачи.
Системное программное обеспечение разработано для работы в качестве интерфейса между прикладным программным
обеспечением и аппаратным обеспечением.
Операционная система также известна как менеджер ресурсов.
Драйвер устройства работает как переводчик между аппаратным устройством и компьютером.
BIOS предоставляет функциональность для управления и контроля аппаратного обеспечения, подключенного или
встроенного в компьютер.
Утилитарное программное обеспечение предназначено для помощи в анализе, мониторинге, настройке и
оптимизации параметров компьютера.
Прошивка встроена в аппаратное обеспечение. Она также известна как ПО для аппаратного обеспечения.
Программное обеспечение с открытым исходным кодом позволяет пользователям вносить изменения в исходный код,
тогда как программное обеспечение с закрытым исходным кодом этого не позволяет.
Операционная система является интерфейсом между аппаратным обеспечением и пользователем.
При последовательном выполнении программы(серийной обработке) выполняется одна программа за раз.
Пакет - это коллекция похожих программ или задач. В системе пакетной обработки разные пакеты обрабатываются
последовательно.
Многопрограммность использует концепцию переключения контекста, которая заключается в сохранении состояния
одного процесса и загрузке состояния другого процесса.
Многозадачная операционная система является логическим продолжением многопрограммности.
В одном процессе может быть несколько независимых подпроцессов, работающих одновременно. Эти подпроцессы
известны как потоки.
Многопоточность - это техника, схожая с многозадачностью, которая позволяет одновременно выполнять
множество потоков.
Мультипроцессорная операционная система означает, что компьютер имеет два или более ЦП.
В распределённой операционной системе данные хранятся и обрабатываются в различных местах, которые
соединены через сеть.
В сетевой операционной системе компьютеры в разных местоположениях соединены через сеть.
Системы реального времени должны завершать обработку в заданные временные рамки; иначе система рухнет.
Языковые переводчики(трансляторы) переводят один язык программирования на другой.
Важные вопросы
Опишите разницу между аппаратным и программным обеспечение компьютера.
Что такое программное обеспечение? Напишите его виды.
Что такое операционная система? Приведите её пример.
Объясните роль операционной системы. Почему её называют менеджером ресурсов?
Приведите несколько примеров компьютерного программного обеспечения.
Опишите разницу между исходным кодом и объектным кодом.
Опишите разницу между компиляторами и интерпретаторами.
Что такое прикладное программное обеспечение? Приведите примеры.
Что такое BIOS?
Можете объяснить, что такое утилитарное программное обеспечение? Является ли оно необходимым программным
обеспечением для установки пользователями в их компьютеры?
Напишите короткую заметку о различных типах операционных системах.
Напишите о различиях между прикладным и системным программным обеспечением.
Что такое вредоносное ПО? Объясните его виды.
Что такое языковой транслятор? Напишите краткое объяснение об этих понятиях:
Компилятор
Интерпретатор
Ассемблер
Редактор
Компоновщик
Загрузчик
Напишите о различиях между сетевыми операционными системами и распределенными операционными системами.
Что такое многозадачная операционная система?
Напишите о различиях между многопрограммностью, многозадачностью и многопроцессорностью в ОС.
Что вы понимаете под операционной системой пакетной обработки?
Какова разница между Символьным Интерфейсом и Графическим Интерфейсом пользователя.
Напишите о различиях между открытым и закрытым программным обеспечением.
В чем разница между вирусами, червями и троянскими программами?
Теперь мы приступим к изучению различных типов систем счисления, таких как десятичная,
двоичная, восьмеричная и шестнадцатеричная. Также будет уделено внимание преобразованию одной системы счисления
в другую. Будут рассмотрены и другие известные двоичные кодирования на основе 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
В программе С мы можем записывать шестнадцатиричные числа так:
AB04, FF0A, 456FEA09. Без префикса.
0XAB04, 0XFF0A, 0X456FEA09. С префиксом 0X.
0xAB04, 0xFF0A, 0x456FEA09. С префиксом 0x.
Это предпочитаемый метод записи шестнадцатиричных чисел в языке С.
Так как человеку привычнее использовать десятичные числа,
а машина оперирует только двоичными числами, очевидно,
что для написания программ без ошибок, важно понимать как
преобразовывать числа из одной системы счисления(с одним
основанием)
в числа другой системы счисления(с другим основанием).
Предположим, что мы имеем некое число с определённым
основанием b.
Это число состоит из нескольких разрядов:
Число из N-разрядов с основанием b
b
b
...
b
b
b
Значение разряда числа
n-1
n-2
...
2
1
0
Номер разряда числа
Допустим что это Двоичное число. Преобразование данного
числа в десятичное число делается по формуле:
Практический пример: Необходимо двоичное число 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
Преобразование Десятичных
чисел в Двоичные
Данное преобразование строится на принципе остатка от деления.
Метод такой:
Делим десятичное целое число на 2 - записываем остаток.
Делимое снова делим на 2 - записываем остаток.
Повторяем до тех пор пока делимое более не делится на два.
Например возьмём десятичное число 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 в двоичное:
Эти числа также имеют название беззнаковые целые числа(англ.
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.
Вы возможно задаётесь вопросом, а как компьютер отличает
целые отрицательные числа от целых положительных чисел.
Ведь, получается, что любое двоичное число может
представлять как положительные так и отрицательные числа.
Но какое именно?
Всё довольно просто - Типы данных.
Во время написания программы, программист заранее
определяет Типы данных с которыми будет работать функция или утверждение.
Все Типы данных в языке С имеют свои идентификаторы типа - однозначно
указывающие компилятору - с каким типом данных и какие
действия будет выполнять машина. Но об этом чуть позже.
Преобразование Десятичных Дробных чисел в Двоичные Дробные
Десятичное дробное число состоит из двух частей, например, 45.54. Где, 45 является целым числом перед
десятичной точкой, а 0.54 является дробной частью после десятичной точки.
Выполним следующие шаги:
Преобразуйте целую часть десятичного числа в двоичное представление, как обычно.
Следуйте данным шагам, чтобы преобразовать дробную часть десятичного числа в её двоичный эквивалент:
Умножьте её на 2
Соберите целую часть результата, то есть цифру перед точкой.
Напишите цифру сверху вниз (смотрите пример).
Сложите целую и дробную части двоичного числа, чтобы получить окончательный результат.
Пример преобразования 45.54 в двоичное дробное число:
Преобразуйте целую часть десятичного числа в двоичный вид, как обычно. Число перед точкой(целое
число) = 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.
Преобразуем дробную часть в двоичное представление. Здесь сразу обозначим, чтобы преобразовать десятичную
дробную часть в двоичную, необходимы выбрать предел точности преобразования.
Так если мы будем брать предел точности в пять знаков после точки, то и количество шагов преобразования
будет не более пяти. Число после точки(дробная часть) = .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
Сложите целую и дробную части двоичного числа числа:
(1) 101101 + .10001
(2) 101101.10001
Следовательно, 45.54 приблизительно равно 101101.10001. Если вы преобразуете это двоичное дробное число в
десятичное дробное вы увидите результат 45.53125, что близко к 45.54, а если увеличить точность
преобразования дробной части до семи знаков так 101101.1000101, то мы получим число 45.5390625, которое
значительно ближе к 45.54. Именно по этому, в цифровой технике и вычислениях так важен предел точности
преобразования, чтобы результаты вычислений всегда были в допустимых пределах погрешности.
Восьмеричные дробные числа и Шестнадцатеричные дробные числа преобразуются в десятичные дробные
числа также как и двоичные дробные числа, просто изменяем основание на 8 для восьмеричных и 16 для
шестнадцатеричных.
Пример: Преобразуйте десятичное 139.29 в восьмеричное число.
Преобразуйте целую часть десятичного числа в восьмеричное, как обычно (см. преобразование из десятичной
системы в восьмеричную). Число перед дробной частью (целое число) = 139:
(1) 139/8 = 3 в остатке
(2) 17/8 = 1 в остатке
(3) 2 = 2 в остатке
Следовательно 139 в десятичной = 213 в восьмеричной.
Преобразуйте дробную часть в восьмеричный эквивалент. Число после дроби (дробная часть) = .29:
(1) .29×8 = 2.32 - Перед точкой мы получили 2, записываем её, и далее не используем.
(2) .32×8 = 2.56 - Перед точкой мы получили 2, записываем его, и далее не используем.
(3) .56×8 = 4.48 - Перед точкой мы получили 4, записываем его, и далее не используем.
Следовательно .29 в десятичной = .224 в восьмеричной. С целью увеличения точности мы можем продолжать эти
шаги.
Соединяем целую и дробную части восьмеричного числа:
(1) 213 + .224
(2) 213.224
Итак десятичное дробное число 139.29 = 213.224 в восьмеричной системе исчисления.
Пример: Преобразуйте восьмеричное 1047.1365 в десятичное число.
Преобразуйте целую часть восьмеричного числа в десятичное, как обычно (см. преобразование из восьмеричную
системы в десятичную). Число перед дробной частью (целое число) = 1047:
Следовательно 1047.1365 в восьмеричной = 551.1847 в десятичной.
Пример: Преобразуйте десятичное 2063.4799 в шестнадцатеричное число.
Преобразуйте целую часть десятичного числа в шестнадцатеричное, как обычно. Число перед дробной частью
(целое число) = 2063:
(1) 2048/16 = 15 в остатке (15 = F)
(2) 128/16 = 0 в остатке
(3) 8 = 8 в остатке
Следовательно 2063 в десятичной = 80F в шестнадцатеричной.
Преобразуйте дробную часть в шестнадцатеричный эквивалент. Число после дробной части (дробная часть) =
.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 в шестнадцатеричной.
Соединяем целую и дробную части шестнадцатеричного числа:
(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, объявление переменной должно заканчиваться точкой с
запятой ; . Несколько
переменных, имеющих одинаковый тип данных, могут быть объявлены одновременно в
виде списка через запятую:
В языке 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(одиночный символ) должны быть заключены в одинарные
кавычки.
Использование двойных кавычек предназначено только для строк(два и более
символов).
Объявление переменных должно происходить до того, как в программе
появится код, который при выполнении будет использовать переменную. Когда
значение назначается
переменной говорят, что переменная была инициализирована. Иногда
переменная
инициализируется
сразу в момент её объявления. Так как при объявлении переменной, к имени
переменной сразу
привязывается адрес ячейки хранения значения(числа) в памяти, а в этот момент
по этому адресу
уже могли быть какие-то значения(называемые мусорными), то в
сообществе программистов
на С есть негласное правило: Инициализировать переменную сразу же после её
объявления, всегда когда
это возможно! Тем самым устраняя из кода мусорные данные сразу.
В примере кода, ниже, различные переменные
объявляются, а затем и инициализируются подходящими значениями, что
описано в комментариях к коду — описательных текстах, заключенных
между символами /* и */, которые компилятор игнорирует:
Кстати, Спецификация языка явно не определяет длину имени переменной. Однако большинство
компиляторов 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-литерал.
Константа бывает следующих четырёх основных видов:
Целого числа - Может принимать только положительные или отрицательные целые значения. Не может
принимать вещественные или десятичные значения с дробной частью. Примеры:
256
-999
Вещественного числа - Cодержит десятичные (действительные) или числа с плавающей точкой. Эти числа
состоят из двух частей: перед десятичной точкой и после десятичной части, например, 58.4. Они могут быть как
положительными, так и отрицательными. Примеры:
-156.01
9.99
94.00
Символьная - Константа символа — т.е. один символ (знак), заключенный в одинарные кавычки (' ').
Примеры:
'B'
'8'
'='
Строковая - Это группа символов. Строка - это коллекция(набор) символов, заключённых в двойные
кавычки(" "). Примеры:
"C language"
"89"
"B"
Примечание: 'B' и "B" - не одно и тоже. Так как строка(в двойных кавычках) содержит скрытый
символ-терминатор конца строки \0. Писать это символ-терминатор нужно в определённых случаях,
подробности далее.
Идентификаторы
Идентификаторы — это слова, определяемые пользователем. Это имя, обозначающее конкретную область памяти. Эти
слова используются для именования переменных, функций, массивов, структур и т.п.. Правила для построения
идентификаторов
в языке C следующие:
Имя идентификатора может содержать буквы, цифры и символы подчеркивания.
Идентификаторы могут начинаться только с буквы или подчеркивания. Они не могут начинаться с цифры.
Не могут содержать пробелов и специальных символов, таких как @, !, #, %, и так далее, кроме подчеркивания.
Не могут содержать зарезервированные(ключевые) слова, такие как
int, break и так
далее.
Имена идентификаторов чувствительны к регистру, потому что C различает заглавные и строчные буквы по их
кодам ASCII.
Имена идентификаторов, по длине не должны превышать ограничения компилятора(обычно это 31 и до 63
символов).
Идентификаторы должны быть выражены в содержательном, кратком и понятном стиле. Когда имена имеют смысл -
чтение кода становится более простым и прозрачным.
Операторы
Операторы — это символы, которые выполняют операции над операндами. Например, в выражении
a = b + c;, 'a', 'b' и 'c' — это
операнды, где '=' и '+' — это операторы. Ниже приведены примеры некоторых операторов языка С:
Арифметические - '+', '-', '*', '/'.
Отношения - '<', '>', '<=', '>=', '==', '!='.
Логические - '&&', '||', '!'.
Присваивания - '=', '+=', '-='.
и другие...
Специальные символы
Специальные символы разделяют различные части программы или попросту обеспечивают её структурную организацию.
Примеры специальных символов:
Скобки - '()', '[]', '{}'.
Точки и запятые - ';', ':', '.', ','.
и другие...
Опишем основные символы языка C в виде таблицы:
Название символов в языке С
Символ
Название
Символ
Название
Символ
Название
~
Тильда
=
Равно
;
Точка с запятой
%
Процент
&
Амперсанд
]
Правая(Закрывающая) Прямоугольная Скобка
|
Вертикальная линия
$
Доллар
!
Восклицательный знак
@
Эт. Символ Вставки
/
Прямой Слэш
,
Запятая
+
Плюс
(
Левая(Открывающая) Круглая Скобка
{
Левая(Открывающая) Фигурная Скобка
<
Меньше чем
*
Звёздочка
?
Вопросительный знак
>
Больше чем
\
Обратный Слэш
.
Точка
_
Нижнее подчёркивание
)
Правая(Закрывающая) Круглая Скобка
}
Правая(Закрывающая) Фигурная Скобка
-
Минус
`
Апостроф
#
Знак Числа
^
Курсор
:
Двоеточие
'
Одинарная Кавычка
[
Левая(Открывающая) Прямоугольная Скобка
"
Двойная Кавычка
Структурная организация кода на языке С
Следующие строки кода показывают общую структуру программы на C. Программа на C обычно состоит из заголовочных
файлов, комментариев, типов данных, функций, операторов ввода, операторов обработки и операторов вывода.
Общий синтаксис самой простой программы на C выглядит следующим образом:
Комментарий является неотъемлемой частью исходного кода любой профессиональной программы написанной на любом
языке программирования. Комментарии пишет программист в первую очередь для себя и своих коллег - поясняя
особенности фрагментов кода.
Комментарий в С бывают однострочные и многострочные. Однострочные начинаются с символов "//".
Многострочные обрамляют текст в символы "/* */":
// Комментарий в одной строке
/* Комментарий в одной строке, который может быть очень длинным, поясняющий особенности конкретного фрагмента кода программы, функции или чего-то что так сразу и не понять... */
/* Комментарий в несколько строк, начинается на этой строке,
и продолжается дальше. Такой комментарий иногда называют
комментарий-параграф, лучше так оформлять длинные тексты,
чтобы было легче их читать */
Отображение
значений переменных в консоли и не
только
Значение переменной может быть отображено с помощью функции
printf(), которая уже была использована нами ранее для
отображения сообщения Hello
World. Формат отображения значения переменной должен быть определён как аргумент функции
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 можно модифицировать, используя следующие флаги:
%5d - Отображает целочисленное значение в поле шириной пять символов
%-5d - Отображает целочисленное значение в поле шириной пять символов, выровненное по левому краю
Таким образом, если мы хотим напечатать несколько значений разных переменных
внутри одной строки, нам достаточно последовательно внутри строки разместить спецификаторы формата
печати для каждой переменной, а после
закрывающий двойной кавычки(после запятой) в той же последовательности записать через запятую
список имён переменных. Переменные
будут подставлены на места спецификаторов форматов печати в строке. Пример:
#include <stdio.h>
intmain()
{
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
Замечания:
Для типа данных size_t рекомендуется использовать
%zu(т.е. специальное беззнаковое значение), а не %lu(произвольное беззнаковое long),
для
полной
переносимости между 32/64-битными платформами.
Для long long используйте %lld, %llu.
Для указателей всегда используйте %p(от англ. pointer - указатель).
Для вывода ptrdiff_t используйте %td, а для intmax_t
— %jd.
%i используется так же, как %d в printf(), но
различается в
scanf()
(распознаёт систему счисления).
Не переживайте если что-то в таблицах вам сейчас не понятно. По
мере обучения будут
описаны все аспекты.
Количество разрядов числа
Спецификатор формата позволяет убедиться, что выходные данные
займут определённый объём места при печати, если указать число сразу же после
символа %. Например, чтобы убедиться, что целое число всегда будет
иметь как минимум семь разрядов, следует использовать спецификатор
%7d. Если необходимо заполнить всё свободное пространство нулями,
между спецификатором и числом следует приписать 0. Например, чтобы
убедиться, что целое число всегда будет как минимум семь разрядов,
и при этом свободные разряды окажутся заполнены нулями, следует
использовать спецификатор %07d .
Спецификатор точности, который представляет собой точку и
число, может быть использован со спецификатором %f для указания
отображаемого количества знаков после запятой. Например, чтобы
отобразить только два разряда, следует использовать спецификатор %.2f .
Спецификатор
точности
может быть использован вместе со
спецификатором минимального пустого пространства, чтобы управлять как
минимальным объёмом пустого места, так и количеством разрядов
числа. Например, чтобы отобразить семь разрядов числа, включая два
после запятой, и заполнить пустые разряды нулями, используйте
спецификатор вида %07.2f .
По умолчанию пустые разряды следуют перед самим числом,
соответственно, число имеет выравнивание по правому краю. Однако пустые
разряды могут быть добавлены и слева. Это достигается путём
добавления символа к спецификатору.
Вот пример исходного кода:
#include <stdio.h>
intmain()
{
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;
}
Так программа напечатает в консоль значения переменных согласно указанным
модификаторам длины и спецификаторам
формата в строке, подставив
вместо них значения переменных:
Начните новую программу с названием vars.c с инструкции
препроцессора, чтобы
включить в код стандартные библиотечные функции ввода/вывода :
#include <stdio.h>
Добавьте функцию main(), в которой объявляются и
инициализируются две
переменные :
intmain()
{
int num = 100;
double pi = 3.1415926536;
}
В функции 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(),
добавьте перед ним ещё
один символ %, как это показано в примере.
В конце функции main() добавьте финальное утверждение,
которое возвращает 0, чего требует объявление функции:
return 0;
Теперь сохраните файл vars.c и затем в командной строке
запишите команду компилятору скомпилировать код и затем запустите
программу, чтобы увидеть, как значения переменных будут выведены в разных форматах.
Обратите внимание, что в случае, если указано
меньше десятичных разрядов, чем того требует число,
значение с плавающей точкой будет округлено, а не
обрезано.
Ввод
значений переменных в программу,
пользователем из консоли
Строки — это особый
случай. Работа с ними
продемонстрирована в разделе,
посвящённом работе с массивами, далее.
Стандартная библиотека функций ввода-вывода <stdio.h>
предоставляет
функцию scanf(), которая может использоваться для получения
данных
от пользователя. Функция scanf() требует, чтобы в неё передавали
два аргумента, определяющие тип данных и адрес значения переменной в памяти,
куда оно должно
быть сохранено.
Первый аргумент функции scanf() должен быть одним из
спецификаторов формата из таблицы, приведённой выше, он
помещается в двойные кавычки. Например так, "%d", если вводится
целочисленное
значение. Вторым аргументом функции scanf() должно быть имя
переменной, перед которым стоит символ &, если только вводится не
строка.
Символ & имеет несколько применений в программировании на
языке C, но в этом контексте он используется как операция адресации,
что
значит, что вводимые данные должны храниться в том участке памяти,
который был зарезервирован для этой переменной.
При объявлении переменной в памяти компьютера резервируется
место(регистры) для хранения данных, записанных в эту переменную. Количество
байт зависит от типа переменной. К выделенной памяти можно
обратиться, используя уникальное имя этой переменной и по указателю
адреса(об этом позже).
Представьте, что память компьютера — это очень длинный ряд
коробок, которые приставлены одна к другой. Каждая коробка имеет уникальный
номер - адрес,
который записывается в шестнадцатеричном формате. Внутри каждой коробки может
лежать определённый тип
предмета(данные
определённого типа), адрес каждого такого предмета и есть уникальный номер
коробки.
В программах, написанных на языке C, коробки — это адресованные ячейки, а
предметы внутри —
это данные, расположенные по этим адресам.
Визуальная аналогия. Коробки - ячейки памяти, Номера на коробках -
Адреса ячеек, Предметы внутри коробок - Данные определённого типа.
Функция scanf() может назначать значения нескольким переменным
одновременно.
Для этого в первый аргумент помещается несколько спецификаторов формата,
разделённых пробелами, а весь список
должен располагаться в двойных кавычках. Второй аргумент должен содержать
разделённый запятыми список имён переменных, перед каждым из которых следует
поставить символ & - операция адресации. Имена переменных следует располагать
последовательно указанным спецификаторам форматов.
Обратите внимание на то,
что функция scanf()
прекращает считывание введённых в консоль
значений, если встретит
пробел.
Операция адресации & также может использоваться для того, чтобы
возвращать шестнадцатеричный адрес, по которому хранится переменная.
Функции printf() и scanf() позволяют программам
взаимодействовать с
пользователем посредством консоли. Часто такие программы так и определяют, как
консольные.
Начните новую программу setvars.c с инструкции препроцессора,
включающей стандартную
библиотеку функций ввода/вывода:
#include <stdio.h>
Добавьте функцию main(), в которой объявляются три
переменные:
intmain()
{
char letter;
int num1, num2;
}
Далее в блоке функции main() после объявления переменных
добавьте
утверждения, позволяющие пользователю ввести данные:
printf("Enter any one keyboard character: ");
scanf("%c", &letter);
printf("Enter two integers separated by a Space: ");
scanf("%d %d", &num1, &num2);
Теперь добавьте утверждения, печатающие в консоль сохранённые
данные:
В конце блока функции main() верните значение
0, чего
требует объявление функции:
return 0;
Сохраните файл программы 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 int —
LONG_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 байта. Эта рекомендация особенно важна
при программировании
встраиваемых систем на основе микроконтроллеров - объём ОЗУ которых часто
ограничен
десятками или сотнями килобайт.
Добавьте финальное утверждение, чтобы вернуть значение
0, чего требует
объявление
функции.
return 0;
Visual Studio Code выдаёт предупреждение о том, что константы для него не
известны. Это
значит,
что
предварительно читается содержимое заголовочных файлов, если это содержимое
содержит
необходимую
информацию используемую в исходном коде ниже, то компиляция может пройти
успешно. В противном
случаи
необходимо подключить недостающие библиотеки. Итак, проверьте исходный код
на наличие ошибок
и
предупреждений :
Исходный код sizeof.c с предупреждением об отсутствии
необходимого
заголовочного
файла <float.h>. Если навести курсор мыши на
подчёркнутые константы, то нам
подсветится
подсказка.
Так как константы для типов данных float, double
и
long double,
с
которыми мы работаем в нашей программе, не содержатся в заголовочном файле
<limits.h> , а содержаться в заголовочном файле
<float.h> ,
то
его необходимо добавить как и предыдущие заголовочные файлы, как инструкцию
препроцессору :
#include <float.h>
Сохраните файл программы, а затем скомпилируйте и выполните
программу, чтобы увидеть размеры и диапазоны типов данных.
Исправленный исходный код sizeof.c
Для оформления вывода данных в консоль в более презентабельном виде,
можно переписать код
с
помощью
управляющих символов, пробелов и текста :
Обратите внимание на то, как в данном примере используется
управляющая
последовательность
символов табуляции \t.
#include <stdio.h>
#include <limits.h>
#include <float.h>
intmain(){
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
Сигнал тревоги (Звонок)
Генерирует звуковой сигнал
Понятие и Использование
Глобальных переменных
Фрагменты программы, в которых доступны переменные, называются
областью видимости.
Локальной называется переменная, объявленная внутри тела функции,
Глобальной называется переменная, объявленная за пределами тела функций.
Тело функции это утверждения внутри её фигурных скобок:
/* Переменная объявленная вне функции и до неё, видна внутри функции - она ГЛОБАЛЬНАЯ */
<data-type> <function-name> ()
{/* Тело функции *//* Переменная объявленная здесь, не видна за пределами функции - она ЛОКАЛЬНАЯ */}
Локальные переменные могут быть использованы только внутри той
функции, в которой они объявлены. Это поведение применяется по
умолчанию, однако его можно использовать намеренно с помощью
редко используемого ключевого слова auto в объявлении
переменной.
Такие переменные создаются в памяти во время вызова функции и, как
правило, удаляются из памяти, как только функция отработает.
Глобальные переменные, с другой стороны, могут использоваться
внутри любой функции программы. Они создаются в памяти во время запуска
программы и существуют в
течение
всего времени выполнения программы.
Внешние Глобальные переменные должны быть объявлены всего один
раз в начале программы. Они также должны быть объявлены внутри
каждой функции, которая будет их использовать. Такое объявление
должно начинаться с ключевого слова extern, что указывает,
что переменная является
внешней,
а не локальной. Подобные объявления не следует использовать для объявления
глобальных
переменных.
Повсеместное использование глобальных переменных в коде может вызвать конфликты имён.
Крупные программы на языке C часто состоят из нескольких файлов
исходного кода, которые компилируются вместе для создания одного
исполняемого файла. Глобальные переменные обычно доступны из
любой функции любого файла программы. Все функции, как правило,
также доступны глобально. Но область действия функций и, что более типично,
глобальных переменных может быть ограничена только тем файлом, в котором они располагаются, с помощью
дописывания в объявление переменной ключевого слова static.
Постарайтесь использовать только локальные
переменные. Глобальные
переменные удобно использовать для получения их
значений из любой функции
программы, но рекомендуется избегать глобальных
переменных и передавать
значения между функциями
как аргументы.
Обычно программа не способна использовать несколько переменных
с одинаковым именем, но это становится возможным, если каждая из
таких переменных объявлена с помощью ключевого слова
static и
является уникальной(единственной) в текущем файле исходного кода.
Создание
программы, использующей несколько файлов, может привести к тому, что
имя одной глобальной переменной будет использоваться в двух
разных файлах. Использование ключевого слова static
позволяет избежать
переименования одной из этих переменных в исходном коде.
Внутренние глобальные переменные также могут быть объявлены
с использованием ключевого слова static. Они станут
доступны только внутри функции, в
которой
они
были объявлены, но не пропадут по завершении работы функции. Это
позволяет создать перманентное(постоянное, вечное)
частное хранилище внутри функции, которое будет существовать до
конца работы программы.
Напишем новую программу global_1.c . Подключим стандартную
библиотеку функций
ввода/вывода.
#include <stdio.h>
Объявите и инициализируйте глобальную статическую
переменную, получить доступ к которой можно только из текущего файла
исходного кода:
static int sum = 100;
Добавьте функцию main(), в которой объявляется необходимость
использовать
глобальную
статическую переменную, а также выводится её значение:
intmain()
{
extern int sum;
printf("Sum is %d\n", sum);
}
Далее добавьте объявление второй глобальной переменной вне функции
main(), а
внутри функции main(), выведите её значение. Теперь исходный
код выглядит так:
#include <stdio.h>
static int sum = 100;
static int num;
intmain()
{
extern int sum;
printf("Sum is %d\n", sum);
extern int num;
printf("Num is %d\n", num);
}
Добавьте финальное утверждение, чтобы вернуть значение
0, чего требует
объявление функции, а затем сохраните файл:
return 0;
Исходный код программы global_1.c
Создайте новый файл global_2.c. Внутри него объявите вторую
глобальную переменную,
затем
сохраните этот файл:
static int num = 200;
Исходный код программы global_2.c
Скомпилируйте оба файла исходного кода в один исполняемый
файл, передав имя каждого из них в команде компилирования: gcc global_1.c global_2.c -o global.exe Ошибка компиляции возникла потому, что область видимости
переменной
num
в
файле global_2.c определена как глобальная только для файла в
котором она
объявлена и
инициализирована.
Ключевое слово static, добавленное к объявлению
переменной num, ограничило
её область видимости рамками только этого файла, что
привело к ошибке компиляции.
Исправим ошибку. Удалим ключевое слово static, добавленное к
объявлению
переменной num и сохраним файл global_2.c .
Исправленный исходный код программы global_2.c
Снова скомпилируйте оба файла исходного кода в один исполняемый файл,
передав имя каждого из
них
команде компилирования
gcc global_1.c global_2.c -o global.exe
, а затем запустите программу, чтобы увидеть на экране значения глобальных
переменных.
Результат работы программы global.exe
Заключение
Область видимости переменных один из ключевых моментов работы с
переменными не только
при
написании единичного файла исходного кода, но в особенности при написании
программы содержащей
несколько
файлов, которые в последствии будут объединены в один исполняемый файл.
Считается, что объявление переменной, которое включает в себя ключевое слово
register (англ. register - регистр, реестр,
подручный журнал учёта), указывает
компилятору, что эта переменная будет
часто использоваться в программе. Это делается для того, чтобы
компилятор разместил переменные, зарегистрированные таким способом,
в регистрах процессора, чтобы ускорить доступ к ним. Полезность этого
действия довольно сомнительная, поскольку компиляторы могут
свободно проигнорировать это указание.
С использованием ключевого слова register могут быть
объявлены
только внутренние локальные переменные. В любом случае, таким
способом могут быть зарегистрированы только несколько переменных,
и они могут иметь только определённые типы. Точные ограничения
могут варьироваться от процессора к процессору.
Несмотря на возможные недостатки, использование ключевого слова
register безвредно, поскольку это ключевое слово будет
проигнорировано, если компилятор не сможет поместить переменные в регистры.
Вместо этого переменные создаются обычным образом, как если бы
ключевое слово register не было указано.
Ключевое слово volatile
Полной противоположностью ключевого слова register
является ключевое слово volatile (англ. volatile -
непостоянный,
шаткий, неустойчивый). Это значит, что переменная не должна
помещаться в регистры, поскольку её значение способно измениться в
любой момент даже без участия кода, окружающего её. Это бывает важно для
глобальных переменных в крупных программах, которые могут
изменять переменную сразу несколькими потоками вычислений.
Использование ключевого слова register может оказаться
полезным для локальных переменных, применяющихся для хранения
управляющего значения в цикле. На каждой итерации(повторении)
цикла происходит обращение к переменной, которая хранит
управляющее значение. Хранение этого значения в регистре способно
ускорить выполнение цикла. Компилятор распознаёт структуру цикла и
оптимизирует его в эффективный код Ассемблера.
С другой стороны, если для хранения такого значения используется
глобальная переменная, которая может быть изменена за пределами
цикла, рекомендуется использовать ключевое слово volatile.
Изучение структур циклов будет дано позже, а пример, приведённый далее,
показан для того, чтобы продемонстрировать повторяющиеся
обращения к переменной, объявленной с помощью ключевого слова
register.
Ключевые слова register
и volatile описаны здесь
лишь для полноты картины — в действительности
они редко используются,
поскольку в большинстве
программ для хранения данных используются обычные
переменные.
Напишем новую программу register.c с подключенным заголовочным
файлом стандартной библиотеки функций ввода/вывода :
#include <stdio.h>
Добавьте функцию main(), в которой объявляется и
инициализируется переменная с использованием ключевого слова
register. Эта переменная содержит управляющее значение
цикла,
равное 0:
intmain()
{
register int num = 0;
}
Теперь добавьте условие, позволяющее проверить, не превысило ли
управляющее значение число 5, а затем пару фигурных скобок {}:
#include <stdio.h>
intmain()
{
register int num = 0;
while(num < 5)
{
}
}
Если для переменной,
объявленной с помощью
ключевого слова register,
использовать операцию &,
компилятор разместит
переменную в ОЗУ, а не в регистре процессора.
Между скобками цикла while (англ. while - до
тех пор пока)
добавьте утверждения, позволяющие в каждой
итерации цикла увеличить управляющее значение и печатать
его в консоль. Теперь код выглядит так:
#include <stdio.h>
intmain()
{
register int num = 0;
while(num < 5)
{
++num;
printf("Pass %d\n", num);
}
}
Возвратим 0 при завершении программы:
return 0;
Сохраните, компилируйте и запустите файл программы,
чтобы увидеть в консоли значение переменной, размещённой в
регистре, на каждой итерации цикла. Теперь код выглядит так:
#include <stdio.h>
intmain()
{
register int num = 0;
while(num < 5)
{
++num; /* тоже самое что и num = num + 1; */printf("Pass %d\n", num);
}
return 0;
}
Результат выполнения программы register.c
Возможно понятия: итерация цикла и условие вам не
совсем понятны? Чтобы понять их давайте представим себе процесс работы
программы визуально -
в виде блок-схемы. На блок-схеме отметим фигуры и линии связывающие фигуры.
Линии показывают нам какой шаг алгоритма выполнятся следующим. Линии идут с
верху вниз,
слева на права, и только итерация цикла обозначена линией со стрелкой
идущей вверх и на лево
-
в начало условия.
Структура алгоритма функции цикла while()
А вот как выглядит алгоритм согласно утверждениям на языке С:
Структура алгоритма функции цикла while() согласно утверждениям на
языке С.
При первой итерации цикла, процессор проверяет условие (num < 5)
, переданное как аргумент функции while() на истинность.
Если (num < 5), тогда условие истинно и мы движемся по линии от
условия(жёлтый ромб) влево-вниз - по линии true. Так как num =
0,
значит 0 < 5. Добавляем к значению переменной единицу - ++num;
и печатаем результат первой итерации в консоль. Возвращаемся к проверке
условия.
На второй итерации проверяем условие - Если (num < 5), т.е.
теперь уже 1 < 5. Условие по прежнему истинно. Добавляем единицу к
переменной
и печатаем это значение в консоль.
Тоже самое на третей и четвёртой итерациях цикла.
Но что будет после четвёртой итерации - когда 4 < 5, и добавляется
единица к значению переменной и она становится равна 5.
На пятой итерации проверяем условие 5 < 5. Очевидно что условие ложно -
пять не меньше пяти. Тогда говорят, что условие цикла не
выполняется
и управление программой передаётся на линию false, т.е. выполняется
утверждение
return 0;
и программа завершается.
В дальнейшем мы изучим детально циклы и условия ветвления потока выполнения
утверждений в
программе.
Заключение
Сохранение переменных в Регистры или в Оперативную память зависит от назначения
переменной.
Если переменная нужна для итерации в Цикле - предпочтительнее записать её в
Регистр,
Если переменная нужна для общих вычислений - достаточно её сохранять в
оперативную память.
Циклы и условия ветвления выполнения программы являются ключевым аспектом
построения сложных
программ.
Преобразование
одних типов данных в другие типы
данных
Преобразование (англ. cast - переплавлять) буквально означает - из
одной формы предмет
сделать
предмет другой формы, но из того же самого материала. Наиболее удачная
аналогия - конечно же
переплавка
металлических деталей в другие металлические детали. Типы данных - это формы
металлических
деталей, а
Металл - двоичные числа в компьютере.
Денис Ритчи плавит металл.
Компилятор будет заменять один тип данных другим без явных указаний
в коде со стороны программиста, если это кажется логичным.
Например, если переменной типа float назначено
целочисленное значение, компилятор преобразует это целое число к типу
float, пример:
#include <stdio.h>
intmain()
{
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 .
Начните новую программу cast.c с инструкции препроцессора,
подключающую стандартную библиотеку функций ввода/вывода:
#include <stdio.h>
Добавьте функцию main(), в ней объявите и инициализируйте
переменные, имеющие
различные
типы данных:
intmain()
{
float num = 5.75;
char letter = 'C';
int zee = 90;
int x = 7, y = 5;
double decimal = 0.1234569;
}
Теперь добавьте утверждения, печатающие результат
преобразования каждой переменной в консоль и завершите программу
возвращением
0,
как того требует объявление функции. Теперь код выглядит так:
#include <stdio.h>
intmain()
{
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;
}
Сохраните файл программы, а затем скомпилируйте и запустите
её, чтобы увидеть в консоли результат каждого приведения типа:
Пример синтаксиса приведения типов.
Значения типа 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);
Начните новую программу array.c с инструкции препроцессора,
подключающую стандартную библиотеку функций ввода/вывода:
#include <stdio.h>
Добавьте главную функцию, в которой объявляется массив из трёх
целочисленных элементов:
#include <stdio.h>
intmain(){
int arr[3];
}
Теперь добавьте утверждения, инициализирующие массив
целочисленными элементами, индивидуально назначая значение каждого
элемента:
Далее добавьте утверждения, позволяющие вывести все элементы
целочисленного массива и строки из массива символов в консоль. Завершите
программу
возвращением нуля:
#include <stdio.h>
intmain(){
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;
}
Сохраните файл программы, скомпилируйте и запустите
программу, чтобы увидеть на экране значения, сохранённые в массивах
переменных:
Пример синтаксиса работы с массивами.
При создании массива, для
каждого элемента, память
выделяется соответственно
его типу — например, один байт для
каждого элемента типа 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]
Использование большего числа индексов позволит создавать массивы
с большим количеством измерений, но в действительности массивы
более чем с тремя измерениями,
применяются редко, поскольку их трудно визуализировать. Чем
большей многомерностью обладает массив тем более сложный абстрактный
объект можно создать.
Напишем программу matrix.c для работы с массивами.
Подключите стандартную библиотеку ввода/вывода
#include <stdio.h>
Добавьте в код главную функцию и объявим двухмерный массив
элементов целочисленного типа
int. Указав два индекса - [2][3], мы тем самым
создали двухмерный массив. Первый индекс
указывает на количество строк, второй индекс указывает на
количество элементов в каждой строке.
Т.е. у
нас две строки по три элемента в каждой.
#include <stdio.h>
intmain()
{
int matrix[2][3] = { {'A', 'B', 'C'}, /*Это строка с индексом [0]*/
{1, 2, 3} }; /*Это строка с индексом [1]*/
}
Теперь добавьте утверждения, позволяющие отобразить
содержимое всех элементов строки с индексом 0 в консоль.
Сохраните файл программы, скомпилируйте и запустите
программу, чтобы увидеть все значения элементов, сохранённые в двухмерном
массиве .
Пример синтаксиса программы для работы с Двухмерным массивом
Подумайте, почему последние три символа отобразились как смайлики и
сердечко?
Заключения
Переменная является контейнером в программе, написанной на языке C,
с помощью которого данные могут быть сохранены в памяти компьютера.
Имена переменных должны соответствовать соглашениям языка C об
именовании.
Четыре базовых типа данных в языке C это
char, int, float, double.
Спецификаторы формата %d, %f, %c, %s, %p могут использоваться
в функции
printf(), чтобы отобразить на экране значения переменных
разных типов.
Данные, введённые пользователем, разрешается помещать в переменные
с помощью функции scanf().
Допустимый диапазон значений целочисленных переменных может быть
явно указан с помощью ключевых слов short и long.
Переменные, в которые никогда не будет записано отрицательное
значение, разрешается пометить с помощью ключевого слова
unsigned,
чтобы расширить диапазон возможных положительных значений.
Количество байт памяти, резервируемое для каждой переменной, можно
узнать с помощью функции sizeof().
Область действия описывает доступность переменных — они могут быть
локальными и глобальными.
Ключевое слово extern указывает, что переменная
объявлена вне блока кода, где она используется,
а ключевое слово static ограничивает доступность
переменной тем файлом исходного кода, где она объявлена.
Производительность вычислений может быть повышена с помощью
ключевого слова register указывающая компилятору
поместить переменную итерации цикла в регистр процессора.
Переменные, которые не следует помещать в регистры, могут быть
отмечены ключевым словом volatile.
Компилятор способен самостоятельно неявно изменять типы данных там,
где это логично,
или же тип данных может быть изменён программистом явно путём выполнения
операции приведения типов вручную.
Массивы переменных могут хранить несколько элементов данных внутри
своих ячеек, которые пронумерованы последовательно начиная с нуля.
Доступ к значениям массивов можно получить, если после имени массива
в квадратных скобках указать номер требуемой ячейки - её индекс.
Массивы могут иметь несколько индексов, что позволяет создавать
многомерные массивы. Массивы размерностью более 3, очень специфические
и редко используются в простых программах.
Установка значений переменных
Подробно рассмотрим, как создать и использовать
константные значения и типы внутри программы.
В случае, когда в программе требуется использовать константное
значение, которое никогда не изменится, оно должно быть
объявлено точно так же, как и обычная переменная, но с
использованием в объявлении ключевого слова const.
Например, константа, представляющая неизменяемое число
«миллион», может быть объявлена и инициализирована следующим
образом:
const int MILLION = 1000000;
В объявлениях констант объект всегда должен инициализироваться.
Программа не способна изменить исходное значение константы. Это
охраняет её от случайностей — компилятор сообщит об ошибке, если
программа попытается изменить исходное значение константы.
Имена констант должны соответствовать соглашениям именования
переменных, приведенных выше, но традиционно в именах
констант используются символы в верхнем регистре(CAPS LOCK), что позволяет
с легкостью отличить их от имён переменных при чтении исходного кода.
Ключевое слово const также может быть использовано
при объявлении массива, если все его элементы не будут изменяться
во время работы программы.
Напишем новую программу constant.c. Подключаем заголовочный файл
стандартных функций ввода/вывода, главную функцию
в которой объявляем константу и возвращаем
0
#include <stdio.h>
intmain(){
const float PI = 3.141593; /* по умолчанию у типа float всегда шесть знаков после точки */return 0;
}
Далее в блоке функции main() добавьте утверждения,
объявляющие
четыре переменные типа float.
Теперь добавьте утверждения, требующие от пользователя ввести
значение переменной.
#include <stdio.h>
intmain(){
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;
}
Добавьте утверждения, вычисляющие значения трёх остальных
переменных с использованием константного значения и значения,
введенного пользователем.
#include <stdio.h>
intmain(){
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(),
предназначенной для получения значений переменных из консоли.
Теперь вставьте утверждения, позволяющие вывести рассчитанные
значения с округлением до двух десятичных знаков.
#include <stdio.h>
intmain(){
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;
}
Этот знак *(звёздочка) в языке С является
арифметической операцией
умножения. Другие арифметические операции
рассматриваются подробнее в следующей главе.
Сохранить, скомпилировать и выполнить программу:
Скомпилированная программа.Выполняем программу. Вводим диаметр круга в миллиметрах и
жмём 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.
В следующем примере перечисление представляет собой очки,
начисляемые за шары при игре в бильярд. Оно содержит необязательные
имена переменных, записанные ЗАГЛАВНЫМИ буквами, поскольку они
являются константами.
Напишем новую программу enum.c. Добавьте утверждение,
которое объявляет перечисление констант,
чьи значения начинаются с единицы:
Добавьте утверждения, предназначенные для печати в консоль
значений некоторых перечисленных констант:
#include <stdio.h>
intmain()
{
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;
}
Добавьте следующее утверждение, позволяющее рассчитать
общую сумму константных значений, отображенных на предыдущем
шаге:
#include <stdio.h>
intmain()
{
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;
}
Теперь добавьте утверждение, предназначенное для вывода
рассчитанной суммы значений:
#include <stdio.h>
intmain()
{
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;
}
Сохраните, скомпилируйте и выполните программу,
чтобы увидеть значения констант перечисления
и вычисленную их сумму в консоли:
Пример синтаксиса работы с перечислением.
Инициализируйте значением 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.
Напишем новую программу consttype.c.
Добавьте заголовочный файл ввода/вывода, главную функцию,
объявим и инициализируем перечисление констант,
чьи значения начинаются с единицы, и возвратим 0 в завершении
программы:
#include <stdio.h>
intmain()
{
enum SNOOKER
{
RED = 1,
YELLOW,
GREEN,
BROWN,
BLUE,
PINK,
BLACK
};
return 0;
}
Далее объявите и проинициализируйте
переменную определённого типа enum,
а затем напечатайте её значение в консоль:
Теперь добавьте утверждение, предназначенное для создания
пользовательского типа данных:
typedef unsigned short int USINT;
Далее объявите и проинициализируйте переменную
пользовательского типа данных и напечатайте её значение:
USINT num = 16;
printf("Unsigned short int value: %d\n", num);
Сохраните, скомпилируйте и выполните
программу, чтобы увидеть значение, назначенное переменной
перечисляемого типа, и значение, назначенное переменной
пользовательского типа данных:
Синтаксис определения пользовательских типов данных.
Пользовательские типы данных должны быть
определены в программе до того, как переменные этого типа
будут созданы(объявленые и инициализированные).
Предопределение констант
Директива препроцессора #define может быть использована для
указания текстовых значений констант, которые впоследствии могут быть
использованы в программе, с помощью следующего синтаксиса:
#define <ИМЯ-КОНСТАНТЫ> "<текстовая-строка>"
Как и #include, эта директива должна размещаться в самом начале
файла с кодом программы. Все включения константы с указанным именем
в коде программы перед компиляцией будут заменены
препроцессором на соответствующую текстовую строку.
#ifdef #define #endif
Директива препроцессора #ifdef является условием и может быть
использована для проверки того, существует ли Предопределение константы.
В зависимости от результата проверки, далее, может идти директива
#define,
позволяющая указать значение константы. #ifdef инструкция
препроцессора
также называется макросом. Каждый макрос должен оканчиваться
директивой #endif .
К счастью, макросы препроцессора могут проверять заранее определённые
компилятором константы, чтобы определить текущую операционную
систему. Значения этих констант будут варьироваться на разных
компьютерах, но на платформе Windows значением константы обычно
является _WIN32, а на платформе Linux — linux.
Директива препроцессора #ifdef может применить
соответствующую текстовую строку для определения текущей платформы.
Напишем новую программу define.c , начнём стандартно:
#include <stdio.h>
intmain()
{
return 0;
}
Добавьте ещё три директивы препроцессора, которые предопределяют
заменяемые текстовые строки в исходном коде:
#include <stdio.h>
#define LINE "_________________________________" /* Просто знаки подчёркивания */
#define TITLE "C Programming form Zero to Hero"
#define AUTOR "You are - programmer"
intmain()
{
return 0;
}
Далее добавьте условный макрос, который определяет текстовую
строку, позволяющую идентифицировать платформу 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
intmain()
{
return 0;
}
Далее добавьте условный макрос, который определяет текстовую
строку, позволяющую идентифицировать платформу 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
intmain()
{
return 0;
}
Далее заполним функцию 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
intmain()
{
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;
}
Сохраните файл программы, а затем скомпилируйте и выполните
программу, чтобы увидеть напечатанные строки в консоли:
Пример применения #define #ifdef #endif -
Предопределения строковых
Констант.
Отладка с помощью Предопределений
В качестве альтернативы можно использовать директивы
препроцессора #if, #else и #elif (else if).
Они позволяют использовать условное
ветвление в соответствии с результатом оценки.
Константа, предопределенная директивой препроцессора #define,
может
быть разопределена с помощью директивы #undef. Оценка также
рассматривается в случае, если константа не определена, это делается с
помощью директивы препроцессора #ifndef .
Все оценки могут быть выполнены внутри блока функции, смешавшись
с обычными утверждениями языка C, они могут быть вложены друг
в друга. Подобные макросы довольно полезны для отладки исходного
кода, поскольку целые разделы могут быть спрятаны или показаны
путём простого изменения состояния макроса DEBUG.
Напишем новую программу debug.c:
#include <stdio.h>
intmain()
{
return 0;
}
Добавьте ещё одну директиву препроцессора, предназначенную для
создания макроса:
Далее в главную функцию добавьте директивы препроцессора,
предназначенные для оценки и отчёта о состоянии макросов:
#include <stdio.h>
#define DEBUG 1
intmain()
{
#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;
}
Сохраните файл программы, а затем скомпилируйте и выполните
программу, чтобы увидеть сообщение о состоянии макроса:
Состояние макроса №1. Обратите внимание как Visual Studio Code уже
заранее
затеняет строки кода, которые не будут выполняться,
так как уже проверено значение условия.
Измените значение макроса, а затем сохраните, перекомпилируйте
и выполните программу снова, чтобы увидеть, что состояние макроса
изменилось:
#define DEBUG 2
Состояние макроса №2. Обратите внимание как Visual Studio Code уже
заранее
затеняет строки кода, которые не будут выполняться,
так как уже проверено значение условия.
Измените значение макроса, а затем сохраните, перекомпилируйте
и выполните программу снова, чтобы увидеть, что состояние макроса
изменилось:
#define DEBUG 3
Состояние макроса №3. Обратите внимание как Visual Studio Code уже
заранее
затеняет строки кода, которые не будут выполняться,
так как уже проверено значение условия.
Будьте внимательны, Каждая проверка условий
с помощью препроцессора
должна оканчиваться директивой #endif
Добавьте ещё одну директиву препроцессора в начале блока
функции, а затем сохраните, перекомпилируйте и выполните программу
снова, чтобы увидеть изменение:
#undef DEBUG
Состояние макроса №4. Обратите внимание как Visual Studio Code уже
заранее
затеняет строки кода, которые не будут выполняться,
так как уже проверено значение условия.
Заключение
Фиксированное значение, которое не будет изменяться, должно
быть сохранено как константа, объявленная с помощью ключевого
слова const.
При объявлении константы всегда необходимо инициализировать
её фиксированным значением.
Ключевое слово enum создает последовательность констант, чьи
значения по умолчанию начинаются с нуля.
Любой константе в последовательности может быть назначено
числовое значение, которое будет увеличиваться в последующих
константах.
Последовательность констант допустимо рассматривать как новый
тип данных. Переменные такого типа могут быть созданы для
хранения перечислений, определённых этим типом.
Пользовательский тип данных может быть определён с помощью
ключевого слова typedef.
Переменные такого типа могут быть созданы с помощью синтаксиса,
используемого для создания обычных переменных.
Директива препроцессора #define может быть
использована для указания значения константы, которое перед
компиляцией будет заменено.
Директива условия препроцессора #ifdef проверяет, существует
ли
заданное определение.
Макрос — это процедура препроцессора, которая должна
оканчиваться директивой #endif.
определённые компилятором константы, например _WIN32
и linux, могут помочь в определении операционной системы.
Выполнить проверку макросов на разные значения внутри главной функции
можно с
помощью директив препроцессора #if, #else, #elif
Предопределения макросов-констант могут быть «разопределены» с
помощью директивы #undef.
С помощью директивы #ifndef можно проверить, определён ли
заданный макрос.
Макросы могут быть полезны при отладке исходного кода,
позволяя с легкостью спрятать или показать разделы кода.
Выполнение вычислений
Изучим, как использовать операции языка С,
чтобы манипулировать данными внутри программы.
В зависимости от количества операндов, операции бывают трёх типов:
Унарные - операция проводится только над одним операндом.
Бинарные - операция проводится только с двумя операндами.
Тернарные - операция проводится только при наличии трёх операндов.
Арифметические операторы, повсеместно используемые в программах,
приведены в таблице ниже:
Арифметические Операции
Символ Операции
Операция
Тип операции
+
Сложение
Бинарная
-
Вычитание
Бинарная/Унарная
*
Умножение
Бинарная
/
Деление
Бинарная
%
Остаток после деления
Бинарная
++
Инкремент
Унарная
--
Декремент
Унарная
Операции сложения, вычитания, умножения и деления ведут себя в
соответствии с вашими ожиданиями, когда у вас в операции всего два операнда.
Числа, используемые при работе с операциями для
формирования выражений, называются операндами —
в выражении 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.
Операции инкремента и декремента могут быть размещены перед
операндом или после него, эффект от них будет разным.
Если операция размещена перед операндом (префикс), значение операнда изменится
мгновенно, в противном случае (постфикс) его значение сначала
записывается, а затем изменяется.
Напишем новую программу, стандартно, объявим и инициализируем переменные
типа
int:
#include <stdio.h>
intmain()
{
int a = 4;
int b = 8;
int c = 1;
int d = 1;
return 0;
}
Далее в главной функции выведите на печать в консоль результат
арифметических
операций, произведённых над значениями переменных:
#include <stdio.h>
intmain()
{
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;
}
Теперь в главной функции выведите результат постфиксных
и префиксных операций инкремента:
#include <stdio.h>
intmain()
{
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;
}
Сохраните файл, а затем запустите программу, чтобы увидеть
результат выполнения арифметических операций:
Синтаксис простых арифметических действий
Обратите внимание на то,
что значение переменной
мгновенно увеличивается
только при постфиксных операциях. При использовании
префиксных операций операнд оказывается увеличенным только при
следующем
обращении к нему.
Присваивание значений переменной
Операции, для присваивания значений, перечислены в таблице ниже.
Все они, помимо простой операции присваивания, =, являются краткой
формой
более длинного выражения, поэтому для ясности каждому из них
приведён эквивалент.
Операции присваивания значений
Операция
Короткая запись
Развёрнутая запись
Читается
=
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.
Стандартно напишем новую программу assign.c. Объявим две
переменные:
#include <stdio.h>
intmain()
{
int a;
int b;
return 0;
}
Далее в главной функции указываем напечатать результаты работы операций
присваивания, произведённых над значениями переменных:
#include <stdio.h>
intmain()
{
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() следует
использовать комбинацию
символов %%.
Запустите программу, чтобы увидеть результат выполнения операций
присваивания:
Операции присваивания совмещённые с математическими операциями.
Сравнение значений
Операции для сравнения двух числовых значений, перечислены в таблице ниже.
Операции сравнения значений
Операция
Способ сравнения значений
==
Равенство
!=
Неравенство
<
Меньше
>
Больше
<=
Меньше или Равно
>=
Больше или Равно
Операция равенства, ==, сравнивает два операнда и возвращает 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).
Эти операции часто используется для проверки счетчика цикла.
Операции сравнения полезны при проверке состояния
двух переменных с целью выполнения условного ветвления в программе.
Стандартно напишем новую программу comparison.c, и в главной
функции объявим и
инициализируем переменные
целочисленного типа и типа символьного:
#include <stdio.h>
intmain()
{
int zero = 0;
int nil = 0;
int one = 1;
char upr = 'A';
char lwr = 'a';
return 0;
}
Далее в главной функции выведите результат выполнения
операций сравнения над переменными:
#include <stdio.h>
intmain()
{
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 -
ложь).
Сохраните файл программы, а затем скомпилируйте и запустите
программу, чтобы увидеть результат выполнения операций сравнения:
Работа операций сравнения значений переменных - возвращение 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.
Начните стандартно новую программу logic.c, и в главной функции
сразу объявите и
инициализируйте переменные типа int:
#include <stdio.h>
intmain()
{
int yes = 1;
int no = 0;
return 0;
}
Далее в главной функции выведите результат выполнения логических операций
над переменными:
#include <stdio.h>
intmain()
{
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;
}
Сохраните файл программы, а затем скомпилируйте и выполните программу,
чтобы увидеть результат выполнения логических операций:
Результат работы Логических операций с булевыми значениями
Проверка условий
Возможно, самой любимой операцией программистов, пишущих на C,
является условная операция ?: , также известная как
тернарная(тройная) операция. Она
сначала
оценивает результат выполнения выражения (true или false), а
затем выполняет одно
из
двух заданных утверждений в зависимости от результата оценки.
Условная тернарная операция имеет следующий синтаксис:
Эта операция может быть использована, например, для оценки того, является ли
заданное число
чётным или нечётным путём проверки наличия остатка от деления на 2, а затем
вывода соответствующей
строки:
В этом примере деление числа 7 на число 2 оставляет ненулевой остаток,
поэтому выражение имеет
значение true, а значит, будет выполнено первое утверждение, правильно
описывая число как
Нечётное.
Условная тернарная операция также может быть полезна для контроля за
грамматикой в тексте,
выводимом
на экран, когда речь идёт о единственном и множественном числе, избегая
неловких фраз наподобие
«Имеем
пять ножницы». Такую проверку легко выполнить внутри утверждения
printf():
В этом примере в случае, если значение выражения равно true, значение
переменной num
равно 1 и будет использован вариант «ножницы», в противном случае будет
использован вариант «ножниц».
Условная операция также может быть использована для присвоения
соответствующих значений переменной в зависимости от результата проверки.
Синтаксис выражения
будет выглядеть так:
Эта операция может использоваться, например, для того, чтобы проверить,
превосходит ли одно
число другое, а затем присвоить большее из них переменной a,
например так:
int num;
int a = 5;
int b = 2;
num = (a > b) ? a : b;
В данном примере значение переменной a больше значения переменной
b,
поэтому
переменной num присваивается большее значение 5.
Начните стандартно новую программу conditional.c, объявим и
инициализируем
целочисленную переменную:
#include <stdio.h>
intmain()
{
int num = 7;
return 0;
}
Добавьте условное утверждение, позволяющее вывести чётность значения
переменной:
#include <stdio.h>
intmain()
{
int num = 7;
(num % 2 != 0) ? printf("\n%d is add\n", num) : printf("%d is even\n", num);
return 0;
}
Сохраните файл, а затем запустите программу, чтобы увидеть результат
выполнения условных
операций:
Пример работы тернарного условного оператора.
Условную тернарную операцию лучше
всего использовать для
утверждений, имеющих
всего две простые альтернативы.
Но всё же, несмотря на то, что условная
тернарная операция проста в применении, её использование может снизить
читабельность
кода.
Итак, мы изучили три типа операций:
Унарные - операция проводится только над одним операндом.
Бинарные - операция проводится только с двумя операндами.
Тернарные - операция проводится только при наличии трёх операндов.
Существует пять видов операций:
Арифметические - Математика.
Логические - Проверка Условий.
Битовые - Булева алгебра.
Присвоения - Краткая форма записи комбинации операции присвоения и дополнительной операции.
Адресации - Работа с адресами памяти.
Основные Операции языка 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 или 0,
ДА(истина) или
НЕТ(ложь),
true или false.
Предпочтительнее, На линиях ветвления по условию указывать их назначения
как ДА или НЕТ.
Пересечение линий Запрещено!
Слияние линий в одну ветку Разрешено.
Линии со стрелками разрешены только при возврате кода в исходные точки.
Такие точки всегда сверху слева или
сверху справа.
Слева блок-схемы всегда Маршрут Приемлемой Ситуации исхода выполнения
алгоритма.
Справа блок-схемы всегда Маршрут Худшего исхода выполнения алгоритма.
Алгоритм строим исключительно на основе правил бинарной логики!
Одинаковые Фигуры можно сливать в одну на основе правил бинарной логики!
В Фигуры одного типа запрещено вписывать утверждения языка С другого типа.
Добавляйте Фигуры "Комментарий" если считаете что блок-схема в этом месте
не очевидна.
По мере составления блок-схем будем уточнять и расширять правила и особенности
их создания.
Повторим изученное ранее и добавим новые знания
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.
Внутри тернарного условия можно записывать обычное условие:
Пример блок-схемы и её код, реализующий тернарное условие. Переменная А = 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
Широкое распространение битовые операции получили при программировании
встроенных систем на
основе
микроконтроллеров, где электронные устройства имеют ограниченные аппаратные
вычислительные
возможности. И не смотря на то, что современные микроконтроллеры приблизились
по функциональным
возможностям к процессором нижнего и среднего ценового сегмента, битовые
операции в них по
прежнему
являются важными.
Программируя на С для написания программ для компьютеров, битовые операции
можно применять
весьма
оригинальными способами.
Начнём писать стандартно новую программу xor.c, объявим и
инициализируем
целочисленные переменные и распечатаем их значения в консоли:
#include <stdio.h>
intmain()
{
int x = 10;
int y = 5;
printf("\nX = %d\nY = %d\n", x, y);
return 0;
}
Добавьте три утверждения ИСКЛЮЧАЮЩИЕ ИЛИ (^), чтобы
поменять местами значения переменных путём битовых манипуляций,
распечатайте в консоль
результат битовых операций над переменными:
#include <stdio.h>
intmain()
{
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;
}
Сохраните файл программы, а затем скомпилируйте и выполните программу,
чтобы увидеть
результат
битовой операции ИСКЛЮЧАЮЩИЕ ИЛИ (^):
Результат работы битовой операции ИСКЛЮЧАЮЩИЕ ИЛИ (^).
Не путайте битовые операции с логическими. Битовые операции
оперируют двоичными значениями
бит чисел, а логические - проверяют булевы значения.
Измерение размера данных
определённого типа
В программировании на языке C функция sizeof() возвращает
целочисленное значение,
представляющее количество байт, необходимое для
хранения содержимого данного операнда в памяти.
Когда операнд, переданный параметром в функцию sizeof(), является
именем типа
данных,
например, int, он должен быть помещён в скобки () — как в
примерах, выше.
В качестве альтернативы в случае, если переданный операнд является именем
объекта данных,
например
именем переменной, скобки опционально можно опустить. Однако, это плохой тон
написания программ
на С.
На практике многие программисты всегда помещают имя переменной параметром в
функцию
sizeof() внутри скобок, чтобы избежать необходимости запоминать
подобное
разграничение.
Простейшим хранилищем в языке C является тип данных char, который
хранит один
символ в одном байте памяти. Это значит, что функция вида
sizeof(char) вернёт 1 (один байт).
#include <stdio.h>
intmain(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. Тем самым выравнивая блок памяти, и сделав его оптимальным для
максимально быстрой
обработки процессором.
Стандартно начнём писать новую программу size.c, объявим и
инициализируем переменную
типа
int
#include <stdio.h>
intmain()
{
int num = 123456789;
return 0;
}
В главной функции добавьте утверждения, позволяющие
узнать объём памяти, выделенной для типа данных int (в вашей
реализации), с помощью имени типа данных и имени переменной.
Объявим и инициализируем переменную типа int:
#include <stdio.h>
intmain()
{
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;
}
Теперь добавьте утверждение, позволяющее вывести на экран объём памяти,
который выделяется
для каждого элемента массива:
#include <stdio.h>
intmain()
{
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;
}
Определите структуру, содержащую одну переменную типа
char и одну
переменную
типа int, а затем выведите на экран объём памяти, выделенный
структуре, включая
прослойку(байты) выравнивания:
#include <stdio.h>
intmain()
{
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().
Сохраните файл программы, а затем скомпилируйте и выполните её:
Результат работы программы печатающей в консоль размеры: типов
данных, массива и
структуры -
с помощью функции 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, на определённое количество бит с
помощью операций побитового сдвига влево (<<) и вправо (>>).
Напишем новую программу bitflag.c . Объявим и инициализируем
переменную
типа int :
Добавьте утверждения, накладывающие маску на первые
четыре бита байта, а затем инвертируйте все значения битовых флагов.
Обратите внимание -
приоритет логической битовой операции И(&) выше, и следовательно сначала
выполнится она,
а потом результат будет ИНВЕРТИРОВАН:
Добавьте утверждения, позволяющие распечатать в консоль
измененные значения всех битовых флагов, а также десятичное
значение, представляющее этот шаблон:
Более крупные битовые
поля могут быть созданы с использованием
переменной, резервирующей больший объём памяти.
Например, переменная типа int, для которой
резервируется 4 байта, может вместить 32 флага.
Знакомство с приоритетами
Приоритет операций определяет порядок их выполнения в утверждениях языка C.
Например, в выражении a = 6 + b * 3 приоритет
операций определяет, в каком порядке будут выполнены операции
сложения и умножения.
Операция умножения, *, находится выше, чем операция
сложения, +, поэтому в выражении a = 6 + b * 3 сначала
выполняется операция умножения.
В следующей таблице перечислен приоритет операций в убывающем
порядке. Операции, расположенные выше, имеют больший приоритет,
чем операции, стоящие ниже, по этому они будут выполнены раньше.
Операции из одной ячейки имеют одинаковый приоритет, поэтому по
рядок их выполнения зависит только от направления ассоциативности
операций. Например, при ассоциативности слева направо, операции,
расположенные слева, будут выполнены раньше.
Приоритет выполнения операций
Приоритет операций
Операция
Ассоциативность
Постфиксные
++ Суффиксный/Постфиксный Инкремент
-- Суффиксный/Постфиксный Декремент
( ) Вызов функции
[ ] Подстрочные индексы массива
. Доступ к члену структуры и объединения
-> Доступ к члену структуры и объединения посредством Указателя
(type){list} Составной литерал
Слева направо
Унарные
++ Префиксный Инкремент
-- Префиксный Декремент
+ Унарный Знак «плюс»
- Унарный Знак «минус»
! Логическое НЕ
~ Логическое Битовое НЕ(ИНВЕРСИЯ)
(type) Приведение Типа
* Разыменование
& Знак адреса
sizeof() Размер
_Alignof() Требование Выравнивания
Справа налево
Мультипликативные
* Умножение
/ Деление
% Деление с остатком
Слева направо
Аддитивные
+ Cложение
- Вычитание
Слева направо
Сдвига
<< Cдвиг влево
>> Cдвиг вправо
Слева направо
Отношения
< Меньше
<= Меньше или равно
> Больше
>= Больше или равно
== Равенство
!= Неравенство
Слева направо
Битовое И
& Битовое И
Слева направо
Битовое ИСКЛЮЧАЮЩЕЕ ИЛИ
^ Битовое ИСКЛЮЧАЮЩЕЕ ИЛИ
Слева направо
Битовое ИЛИ
| Битовое ИЛИ
Слева направо
Логическое И
&& Логическое И
Слева направо
Логическое ИЛИ
|| Логическое ИЛИ
Слева направо
Тернарная Условная операция
?: Тернарная Условная операция
Справа налево
Присваивания
=
+=
-=
*=
/=
%=
&=
^=
|=
<<=
>>=
Справа налево
Запятая
, (Запятая)
Слева направо
Напишем новую программу precedence.c, в которой проверим
приоритет выполнения
операций по умолчанию, и с принудительным назначением приоритета с помощью
круглых скобок:
В главной функции выведите число-результат вычисления
выражения, следующего правилам приоритета для ассоциативности
слева направо, а затем результат вычисления выражения с явно
определённым приоритетом:
Теперь выведите число-результат вычисления выражения,
следующего правилам приоритета для ассоциативности
справа налево, а затем результат вычисления выражения
с явно определённым приоритетом:
Сохраните файл программы, а затем скомпилируйте и выполните
программу, чтобы увидеть результаты, следующие правилам приоритета.
Для того чтобы напечатать в консоль
символ % в функции
printf(), следует использовать
комбинацию %%.
Заключение
Арифметические операции, которые могут образовывать
выражения с двумя операндами — сложение, +, вычитание, -,
умножение, *,
деление, /, и деление с остатком, %.
Операции инкремента, ++, и декремента, --, изменяют значение
их
единственного операнда на 1.
Операция присваивания = может быть объединена с
арифметической операцией, чтобы выполнить расчёт выражения, а затем сразу
присвоить результат.
Операции сравнения могут образовывать выражения,
сравнивающие два операнда с точки зрения равенства == или
неравенства !=,
а также проверяющие, является ли одно число больше >, меньше
<,
больше или равно >= или меньше или равно <= другому.
Операции Логическое И(&&) и Логическое ИЛИ(||) образуют
выражения, оценивающие два операнда и возвращающие булево
значение true или false,
а логическая операция НЕ(!) возвращает
инвертированное булево значение одного операнда.
Тернарная Условная операция ?: оценивает заданное булево выражение,
а
затем возвращает один из двух операндов в зависимости от
результата.
Функция sizeof() возвращает размер выделяемой памяти в байтах
для заданного типа данных или объекта данных.
Один байт памяти состоит из восьми бит, которые могут содержать
значение 0 или 1.
Битовые операции ИЛИ(|), И(&), НЕ(~) или ИСКЛЮЧАЮЩЕЕ
ИЛИ(^)
возвращают значение после выполнения сравнения значений двух
битов, а операции Побитового Сдвига Влево(<<)
и Побитового Сдвига Вправо(>>) смещают значение переданных
битов на заданное число позиций.
Наиболее частое использование битовых операций заключается
в том, чтобы изменить компактное поле бит, содержащее набор
булевых флагов.
В выражениях, содержащих несколько операций, операции будут
выполняться в соответствии со стандартным приоритетом
операций, если только порядок выполнения не определён явно путём
добавления круглых скобок.
Различные Проверочные утверждения
Здесь показывается, как с помощью утверждений
можно оценивать выражения, чтобы определить направление,
по которому последует выполнение программы.
Если код, который должен быть выполнен, содержит
только одно утверждение, фигурные скобки можно опустить.
Ключевое слово 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 /* ... Это */
}
Этот приём программирования является фундаментальным, он предлагает
программе пойти по одному из двух направлений в зависимости от
результатов проверки. Этот приём также известен как условное
ветвление.
С помощью создания блок-схем легко увидеть - какая ветка кода будет
выполняться,
в том или ином случаи - в зависимости от результата условия.
Напишем программу ifelse.c и изобразим её блок-схему. Прочитайте
исходный код и на блок-схеме проследите какая ветка кода исполнится в том
или ином случаи? Что будет напечатано в консоль?
#include <stdio.h>
intmain()
{
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;
}
Составим блок-схему алгоритма:
Проследите взглядом выполнение кода, указывая курсором на одну за
другой фигуру
и читая исполняемые внутри этой фигуры утверждение. Укажите, что
будет напечатано в
консоль.
Сохраните, скомпилируйте и запустите программу.
Напоминаю, значение 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
применяется следующая
визуальная конструкция:
Напишите новую программу switch.c и изучите её визуальное
представление в виде
блок-схемы:
Сохраните, скомпилируйте и выполните программу. Введите различные
значения, чтобы
увидеть соответствующее поведение функции switch():
В утверждениях switch ключевое слово case и
значение, указанное рядом и символ двоеточия за ним, рассматриваются
компилятором
как уникальная метка.
Поставим задачу для решения которой необходимо написать С исходный код.
Программа должна напечатать строку в консоль, соответствующую значению
целочисленной
переменой
int.
Программа должна напечатать строку в консоль, соответствующую символу
символьной переменой
char.
Используем функцию ветвления кода switch() для обеих задач.
Составим блок-схему для явного визуального представления программы.
Отобразим все интересующие нас ветки case для функции
ветвления
switch()
Блок-схема двух функций ветвления кода switch() идущих
одна за другой.
Напишем, сохраним и выполним новую программу switchchar.c на основе
блок-схемы выше:
#include <stdio.h>
intmain()
{
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;
}
Выполним программу:
Пример исходного кода синтаксиса функции условного ветвления
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).
Запустите программу и проверьте что печатается в консоль.
Заключение
Мы изучили один из самых широко употребляемых видов циклов - for.
Циклы могут быть бесконечные и конечные, которые завершаются по условию.
Область видимости переменной внутри инициализации цикла(внутри круглых
скобок) - ограничена
исключительно телом цикла(внутри фигурных скобок).
Область видимости переменной можно изменить на локальную - вынеся
её из
инициализации
цикла и
перенеся её в исходном тексте до цикла в котором она будет использоваться.
Внутри инициализации цикла можно инициализировать несколько циклов. Однако
нужно
сформулировать и правильно записать условия для выхода из цикла, понимая -
что мы ожидаем получить в
результате работы программы.
Для создания условий применяются условные операторы изученные ранее: И,
ИЛИ, НЕ(ИНВЕРСИЯ),
ИСКЛЮЧАЮЩЕЕ ИЛИ, и их комбинации.
Переменные внутри инициализации цикла могут быть любых типов: int,
float, double, char...
Циклы могут вкладываться один внутрь другого.
Конструкция циклов полностью строится на основе правил бинарной логики.
Зацикливание с помощью условия
Цикл 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. По
аналогии,
называются
такие циклы один по отношению к другому: внешний и внутренний.
Запустите программу из примера выше.
Заключение
Цикл while() требует наличия начального
выражения, проверочного выражения и инкремента/декремента, но объявление и
инициализация
значения
переменной записывается вне круглых скобок цикла while().
Все способы записи условных выражений внутри цикла while()
работают.
Инкремент/декремент переменной проверочного выражения записывается внутри
тела цикла
while()
в фигурных скобках{}.
Чаще всего цикл while() создаёт внутри главной функции некий
основной рабочий цикл.
Цикл while() специфический, и менее популярный чем цикл for.
Цикл с условием вида
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(). Напишите и
выполните
программу из этого примера.
Измените значения объявленных переменных для понимания реализации цикла.
Напишите новую программу dowhilearray.c для работы с одномерным
массивом.
Объявим целочисленную переменную-счётчик, целочисленный массива с пятью
элементами в нём. Сначала добавим
цикл while():
#include <stdio.h>
intmain(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;
}
Теперь добавим цикл do...while():
#include <stdio.h>
intmain(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;
}
Сохраните, скомпилируйте , исправьте ошибки в программе(изучите вывод
напечатанного в
консоли) и
выполните программу - добившись идентичного вывода печати в вашей консоли.
Измените значение в проверочном выражении на 0
(while(i < 0)) в обоих циклах, а затем перекомпилируйте программу,
чтобы увидеть,
что
будет
выполнена только первая итерация цикла do...while.
Заключение
Циклы while() и do...while() будут выполнять свои итерации
до тех пор, пока
значение проверочного выражения не станет равно false — в этот
момент цикл завершается. Поэтому необходимо, чтобы тело цикла
содержало код, который в определённый момент изменит значение
проверочного выражения, в противном случае создастся бесконечный цикл,
который заблокирует систему из-за зависшей программы или произойдёт сбой
программы.
Значительное различие между циклами while() и do...while()
заключается в том,
что первый не сделает и единой итерации, если при стартовой оценке
значение проверочного выражения равно false. Цикл
do...while() напротив
всегда будет делать хотя бы одну итерацию, поскольку утверждения
выполняются до оценки выражения. Если такое поведение является
необходимым, цикл do...while() очевидно является лучшим выбором,
в противном случае выбор между циклами for() и while(),
и является скорее делом стиля, вкуса или договорённостями/правилами команды
разработчиков.
Как правило, циклами for() пользуются, когда нужно выполнить
определённое количество итераций, а циклами while() и
do...while() — когда
нужно
выполнять итерации до тех пор, пока не будет соблюдено некоторое условие.
Циклы идеально подходят для работы с массивами, поскольку каждая
итерация поможет легко считать или записать последовательные
элементы массива.
Если так случилось, что запустился бесконечный цикл,
в операционных системах Windows и Linux нажмите
сочетание клавиш Ctrl+C,
чтобы прекратить выполнение цикла.
Досрочный выход из циклов
Ключевое слово break может быть использовано для того, чтобы
преждевременно прекратить выполнение цикла при соблюдении
определённого условия. Утверждение break находится внутри блока
утверждений цикла, перед ним должно располагаться проверочное выражение.
Когда проверочное выражение возвращает значение true, цикл
нёмедленно заканчивается и программа переходит к выполнению следующей
задачи. Например, при завершении внутреннего цикла таким способом
начнется новая итерация внешнего цикла.
Напишем новую программу nobreak.c без досрочного выхода из цикла на
основе блок-схемы
задачи:
Задача визуализированная в виде блок-схемы следующая: Вывести в
консоль распечатку
значений двух целочисленных переменных i и j в пределах
их значений от 1 до 3
включительно. Должно быть реализовано решение на основе вложенных
циклов.(Напоминаю, левая
блок-схема подробного вида, правая - компактная). Пока мы изучаем
основы языка С - для нас важно
детальное понимание принципов работы алгоритмов.
Изучив блок-схему реализации задачи, напишем исходный код:
Внесите изменения в программу что бы получить группы по 4-ре значения
- от 0 до 3
включительно.
Измените код. Создайте вывод в консоль распечатку данного вида.
Измените код. Создайте вывод в консоль распечатку данного вида.
Перепишите код - добавьте утверждение break в самом начале
блока
внутреннего цикла срабатывающем при истинности условия, затем сохраните, скомпилируйте и запустите программу
ещё
раз.
Блок-схема визуализации задачи выглядит так:
Измените код. Создайте вывод в консоль распечатку данного вида.
#include <stdio.h>
intmain(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>
intmain(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 так:
#include <stdio.h>
intmain(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;
}
Сохраним, скомпилируем и выполним программу:
В этом примере утверждение continue просто пропускает первую
итерацию
внутреннего цикла, когда внешний пытается запустить его в первый раз.
Визуализация программы в подробной и компактной блок-схемах.
Проследите выполнение всей программы от фигуры к фигуре.
Переход к меткам
Ключевое слово goto в соответствии со своим названием позволяет
потоку программы переходить к меткам , расположенным в других частях
программы, примерно как гиперссылка на веб-странице. Однако в
реальности это может привести к ошибкам и считается плохим приёмом
программирования.
Переход с помощью ключевого слова goto — это мощная
функциональность, которая существовала в компьютерных программах
десятилетиями, но её мощью злоупотребляли многие первые программисты,
создававшие программы, в которых переходы выполнялись
непостижимым образом. Это приводило к написанию трудно читаемого
программного кода, поэтому использование ключевого слова goto
стало весьма непопулярным.
Одним из возможных корректных применений ключевого слова goto
может быть выход из внутреннего цикла путём перехода к метке,
расположенной после блока внешнего цикла. Это мгновенно завершит оба
цикла, ни одна из итераций любого цикла не будет выполнена.
Напишем новую программу jump.c для ознакомления с работой оператора
goto
#include <stdio.h>
intmain(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;
}
Теперь добавьте end метку после закрывающей скобки
внешнего цикла
#include <stdio.h>
intmain(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;
}
Сохраните, скомпилируйте и выполните программу.
Применение устаревшего в программировании метода - Переход к метке.
Блок-схемы(подробная и компактная) устаревшего в программировании
метода - Переход к
метке.
Как уже было отмечено - переход к меткам с помощью оператора goto
является устаревшей практикой и не рекомендуется к применению. Однако в старых программах вы
можете его встретить. В своих программах никогда не применяйте переход к меткам.
Современный код на С предполагает применение ветвлений кода по условию,
циклов, и других более эффективных методов, чем Переход к метке.
Перепишем наш пример - избавимся от перехода к метке. Применим Условие
проверки состояния Флага. Объявим и инициализируем целочисленную переменную flag:
#include <stdio.h>
intmain(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;
}
Сохраните, скомпилируйте и выполните переписанную программу.
Современный подход к управлению выполнением кода по Условию с помощью
Флага.
Блок-схемы(подробная и компактная) современного метода в
программировании - Переход по
Условию с помощью Флага.
Заключение
Ключевое слово if выполняет простую условную проверку, чтобы
оценить значение заданного выражения — оно может возвратить либо
true либо
false.
Ключевое слово else разрешается использовать для
предоставления
альтернативного набора утверждений, которые должны быть
выполнены в случае, если утверждение if возвращает значение
false.
Предоставление программе альтернативных направлений выполнения
после Условия называется условным ветвлением.
Условное ветвление, выполняемое с помощью нескольких утверждений
if...else, может быть выполнено более эффективно с помощью
утверждения switch.
Обычно утверждения case внутри блока switch
должны заканчиваться
утверждением break. Если необходимо выполнить одно утверждение
для нескольких
значений
case - то утверждение ставится в последнём case,
в предыдущих
case слово break не записывается.
Опционально блок switch может содержать утверждение
default,
с помощью которого указываются утверждения, которые должны быть
выполнены в случае отсутствия совпадений.
После ключевого слова for следуют круглые скобки,
в которых указывается начальное выражение, проверочное выражение и
инкремент/декремент,
позволяющие управлять циклом.
После ключевого слова while следуют круглые скобки,
в которых указывается проверочное выражение, позволяющее определить,
должен ли цикл продолжаться.
Перед блоком цикла while должно располагаться начальное
выражение,
а внутри него — инкремент/декремент.
После ключевого слова do следует блок утверждений, после
которого
обязательно нужно добавить утверждение while, которое должно
заканчиваться точкой с запятой ;.
Перед циклом do...while должно находиться начальное
выражение,
а внутри него — инкремент/декремент.
В отличие от циклов for и while утверждения
цикла
do...while всегда будут
выполнены хотя бы один раз.
Ключевое слово break может использоваться для того, чтобы
завершить
цикл, а ключевое слово continue — для того, чтобы пропустить
одну
итерацию цикла.
Циклы могут быть вложенными друг в друга.
Ключевое слово goto может быть использовано для того, чтобы
выйти из всех циклов/блоков кода и перейти к указанной метке, однако этот подход устарел и использовать его
не
рекомендуется. Вместо него применяйте Условия и/или Циклы.
Итак, на текущем этапе полученных знаний по программированию на С мы знаем, что
любое ветвление кода в программе следует осуществлять исключительно с помощью Условий и
Циклов.
Использование Функций
Изучим конструкцию
функции. Научимся вызывать(выполнять) функцию по запросу утверждения в
программе.
До этого момента, в учебнике мы использовали обязательную
главную функцию main() и стандартные функции, содержащиеся в
библиотеке заголовочных файлов, такие как printf() из файла
<stdio.h>.
Однако в большинстве программ, написанных на языке C, содержится некоторое
количество пользовательских функций, которые могут быть вызваны
по требованию во время выполнения программы.
Синтаксис и общая конструкция функции.
Блок функции содержит только набор утверждений, который
выполняется при вызове функции. Как только утверждения функции
выполняются полностью, поток программы продолжает свою работу с точки,
следующей непосредственно после вызова функции. Такой подход называется
модульностью. Модульность очень полезна при программировании на языке
C,
поскольку она позволяет изолировать набор процедур, чтобы впоследствии их
можно
было вызывать многократно.
Для того чтобы ознакомить программу с пользовательской функцией,
последнюю необходимо объявить, точно так же, как и переменные,
которые сначала необходимо объявить, а лишь затем использовать.
Объявления функций должны быть добавлены перед блоком функции
main().
Как и функция main(), пользовательские функции могут возвращать
значения. Тип данных возвращаемого значения должен быть включен в
объявление функции. Если функция не возвращает никакого значения,
она должна быть объявлена с помощью ключевого слова void.
Имя функции следует выбирать исходя из соглашений именования переменных.
Объявление функции часто более корректно называют
прототипом функции, с его помощью мы можем информировать
компилятор о том, что функция существует. Определение же функции, включающее
в себя утверждения, которые необходимо выполнить, располагается
после функции main().
Пользовательские функции могут быть вызваны из функции main()
для того, чтобы выполнить их утверждения.
Прототип функции иногда
называют её заголовком.
Значение, возвращаемое пользовательской функцией, может быть
присвоено переменной подходящего типа данных или просто
отображено с использованием подходящего спецификатора формата.
Наши блок-схемы теперь будут следовать принципу модульности. Где отдельная
функция - и есть
модуль.
Рассмотрим пример модульной блок-схемы, и напишем по ней программу:
Пример простейшей модульной блок-схемы главной функции и пользовательских
функции в ней.
Напишем новую программу firstfunc.c . Объявим три пользовательские
функции:
#include <stdio.h>
void first(); /* Первая пользовательская функция */int square5(); /* Вторая пользовательская функция */int cube5(); /* Третья пользовательская функция */intmain() /* Главная функция программы */
{
return 0;
}
Обратите внимание на подсказки от Visual Studio Code, говорящих о том, что
- функции
объявлены, но
не определены(definition not found ) в программе.
Объявим целочисленную переменную внутри главной функции
#include <stdio.h>
void first();
int square5();
int cube5();
intmain()
{
int num;
return 0;
}
После главной функции определим пользовательские функции:
#include <stdio.h>
void first();
int square5();
int cube5();
intmain()
{
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;
}
Теперь добавим вызовы пользовательских функций в блоке главной функции:
#include <stdio.h>
void first();
int square5();
int cube5();
intmain()
{
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;
}
Сохраните, скомпилируйте и выполните программу:
Простейший пример работы с функциями в коде. Обратите внимание -
строку объявления
функции
нужно завершить точкой с запятой ;. А вот после определения
функции - внизу под
главной
функцией - после фигурных скобок { } точка с запятой не нужна.
Определения пользовательских функций технически
должны появляться перед функцией main(),
но по соглашению правил хорошего тона среди программистов,
там следует писать только прототипы, а
функцию main() всегда размещать в начале кода.
Передача аргументов в функцию
В пользовательские функции данные могут быть переданы как
аргументы. Там они могут использоваться для выполнения их
утверждений. Как мы уже знаем - Прототип функции состоит из её имени и типа
данных которые она
возвратит, также каждый аргумент функции записывается с указанием его имени и
типа данных
этого аргумента.
Важно понимать - данные в языке C передаются по значению (временная
локальная копия) в
переменную, указанную как аргумент функции. Это отличает язык
программирования C от других языков программирования, таких как Pascal, где
аргументы передаются по ссылке — когда функция работает с
оригинальным
значением, а не локальной копией.
Передача данных по значению присваивает значение
переменной в вызываемой
функции. Далее функция может работать только с этой копией,
но оригинальные данные она не затронет.
Аргументы в прототипе функции называются формальными параметрами
функции.
Они могут иметь разные типы данных; несколько аргументов могут быть указаны
для одной функции, тогда их следует разделять запятой.
Например, прототип функции с аргументами, имеющими
каждый из четырёх типов данных, может выглядеть так:
void action(char c, int I, float f, double d);
Компилятор проверяет, совпадают ли указанные в прототипе функции
формальные параметры с параметрами в определении функции. Он
сообщит об ошибке в случае несовпадения.
Напишем новую программу args.c, которая демонстрирует синтаксис
передачи аргументов в
функцию. Объявим три пользовательские функции:
#include <stdio.h>
void display(char str[]);
int square(int x);
int cube(int y);
intmain()
{
return 0;
}
Добавьте в главную функцию, объявление целочисленной
переменной и массив символьных переменных, который
инициализируется текстовой строкой:
#include <stdio.h>
void display(char str[]);
int square(int x);
int cube(int y);
intmain()
{
int num;
char msg[50] = "String to be passed to a function";
return 0;
}
После главной функции определите три пользовательские функции:
#include <stdio.h>
void display(char str[]);
int square(int x);
int cube(int y);
intmain()
{
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;
}
Теперь добавьте вызовы пользовательских функций в главной
функции:
#include <stdio.h>
void display(char str[]);
int square(int x);
int cube(int y);
intmain()
{
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;
}
Сохраните файл программы, а затем скомпилируйте и выполните
программу, чтобы увидеть выведенный на экран результат работы
пользовательских функций, использующих переданные значения
аргументов.
Пример работы вызова пользовательских функций с передачей им
параметров, которые они
используют.
Функция может не
возвращать значения, но
попрежнему использовать
слово return, рядом с которым не
указано никаких значений.
Это делается только для
того, чтобы явно обозначить
возврат управления
вызывающей стороне(функции).
Напишем новую программу maxint.c, которая будет определять большее
число из двух
целых
чисел переданных пользователем в качестве параметров функции из консоли.
Объявим
пользовательскую
функцию:
#include <stdio.h>
int myMax(int x, int y); /* Здесь объявили функцию пользователя */intmain(void)
{
return 0;
}
Объявим переменные для передачи их в качестве аргументов функции.
Определим пользовательскую
функцию после главной функции:
#include <stdio.h>
int myMax(int X, int Y);
intmain()
{
/* Здесь объявили переменные */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;
}
Вызовим пользовательскую функцию и возвращаемое ею значение присвоим
переменной C,
которую
проверим на условие:
#include <stdio.h>
int myMax(int X, int Y);
intmain()
{
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;
}
Сохраните, скомпилируйте и выполните программу:
Введите различные значения, несколько раз перезапустив программу.
Виды функций
За десятилетия развития языка С, были созданы сотни и тысячи библиотек
заголовочных файлов
специально
для применения специализированных функций.
Если вы подробно изучите заголовочные файлы входящие в стандарт ANSI C - вы
обнаружите, что
массу
полезнейших
функций уже написали, протестировали и усовершенствовали для вас. Нет нужды
постоянно писать
собственные функции -
воспользуйтесь готовыми и надёжными функциями стандарта ANSI C или крупных
сообществ
программистов
определённого направления.
Напишем новую программу mathheader.c в которой используем
стандартную библиотеку
математических функций <math.h>. Математическая
библиотека math.h
Что же такое заголовочный файл или как его ещё называют файл
заголовков?
Вспомните:
Прототип функции иногда называют её
заголовком.
Так что заголовочный файл - это набор заголовков функций содержащихся в
нём. Т.е. эти
заголовки
просто напросто - объявления функций. Файл заголовков включенный в нашу
программу, уже внутри
себя
имеет и определение объявленной в нём функции.
Нам достаточно подключить заголовочный файл, использовать уже знакомый нам
синтаксис записи
функции, передать ей подходящие аргументы, и функция будет выполнена -
вернёт значение.
Изучайте стандартные заголовочные файлы стандарта ANSI C и большинство
необходимых функций
вам
никогда не придётся писать с нуля.
Язык С считается одним из самых быстрых по скорости вычислений в истории.
Его математические
функции доведены практически до совершенства. В нашей программе мы Возведём
число e(
e(число) )
в
степень числа, Возведём одно число в степень другого числа, Вычислим
квадратный корень из
числа.
Обратите внимание - функции принимают как целые числа так и
натуральные(дробные).
#include <stdio.h>
#include <math.h>
intmain(void)
{
printf("%.3lf\n", exp(4.5)); /* Функция возведение экспоненты в степень */printf("%.3lf\n", pow(2.9, 1.3)); /* Функция возведение числа в степень числа */printf("%.3lf\n", sqrt(27)); /* Функция вычисления квадратного корня из числа */return 0;
}
Сохраните, скомпилируйте и выполните программу:
Использование заголовочного файла <math.h> и три
его функции
exp(), pow(), sqrt().
Возвращение двух и более
значений из функции
Обычно функция возвращает одно значение. Однако на языке С можно применить два
способа
возвращения
нескольких значений.
Первый - способ глобальных переменных.
Второй - способ указателей. С указателями мы ещё не знакомы, но
рассмотрим его позже - в качестве введения в тему Указателей.
Итак, Глобальный способ возвращения нескольких значений из функции довольно
прост:
Сначала, в коде мы объявляем глобальные переменные.
Объявляем прототип функции - атрибутами которой будут эти переменные.
Пишем главную функцию внутри которой вызываем нашу объявленную функцию.
После главной функции определяем нашу объявленную функцию.
Напишем новую программу mathglobal.c, в которой передадим в
пользовательскую функцию
два
параметра, и возвратим два результата.
#include <stdio.h>
#include <math.h>
/* Глобальные переменные A, B объявляем до главной функции */int A;
int B;
/* Прототип функции объявляем до главной функции */
void myFunction(int X, int Y);
intmain(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);
}
Так как переменные A и B Глобальные - область их видимости охватывает все
блоки кода
программы.
Соответственно в строке кода
printf("Squares of %d and %d are: %d and %d.", X, Y, A, B);
изменённые значения переменных A и B (Глобальных), доступны в ней.
Сохраним и скомпилируем программу, выполним её:
Пример возвращения двух значений из пользовательской функции в
Главную функцию -
способом
Глобальных переменных.
Однако мы изучили правила хорошего тона работы с переменными, и помним -
глобальные переменные в коде должны быть сведены к минимуму.
Рекурсивные вызовы -
функция вызывает саму себя
Согласно "десяти главных правил написания кода" (от
NASA), рекурсию нужно избегать - в виду того что код с
рекурсиями трудно
читаемый, не прозрачный, потребляет много памяти.
Утверждения, находящиеся внутри пользовательских функций, могут
свободно вызывать другие пользовательские функции точно так же, как они
могут вызывать стандартные библиотечные функции наподобие
printf().
Также функции могут вызывать сами себя, это и называется рекурсивным вызовом.
Как и в случае с циклами, очень важно, чтобы рекурсивные функции
изменяли проверочное выражение, чтобы функция в конечном итоге
завершилась.
Напишем новую программу recursion.c. Но в начале составим
блок-схему. Задача -
программа
должна принимать от пользователя целое число. Напечатать его в консоль.
Уменьшить его на единицу(декремент). Проверить значение по условию на
меньше 0. Если значение
меньше 0
завершить программу. В качестве реализации цикла выполнения применить
рекурсивную функцию.
Проследим работу программы по шагам. Проиллюстрируем ключевые шаги:
Выполняются все строки кода (допустим пользователь ввёл число 2),
включая вызов
пользовательской функции. После вызова пользовательской функции,
Главная функция
ожидает её
выполнения:
Выполняется первая итерация пользовательской функции.
Условие num < 0 на первой итерации не выполняется так
как
(num == 2) < 0 == false .
На этой ветке false происходит вызов
пользовательской функции из
неё же
самой:
Пользовательская функция вызвала саму себя - Вторая итерация.
Условие num < 0 на второй итерации не выполняется так
как
(num == 1) < 0 == false .
На этой ветке false снова происходит вызов
пользовательской функции
из
неё же
самой:
Вторая итерация выполнена:
Пользовательская функция вызвала саму себя - Третья итерация.
Условие num < 0 на третей итерации не выполняется так
как
(num == 0) < 0 == false .
На этой ветке false снова происходит вызов
пользовательской функции
из
неё же
самой:
Третья итерация выполнена:
Пользовательская функция вызвала саму себя - Четвёртая итерация.
Условие num < 0 на четвёртой итерации выполняется так
как
(num == -1) < 0 == true .
На этой ветке true происходит возврат в Главную
функцию из
пользовательской:
Вызов Главной функции осуществлён:
Главная функция ожидала завершения вызванной ею пользовательской
функции, которая
завершилась. Продолжается выполнение строк кода Главной функции.
Печатается последняя
строка
printf("Lift Off!\n);, Главная функция завершается
возвратом 0(как того
требует
объявленная главная функция) return 0;:
Блок-схема изучена. Приступаем к написанию программы. Согласно
изучения выполнения
каждого
шага и наличия внутри фигур утверждений С, напишем исходный код так:
#include <stdio.h>
/* Объявим пользовательскую функцию */
void count_down_from(int num);
intmain()
{
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); /* Рекурсивный вызов пользовательской функции саму себя */
}
}
Сохраните, скомпилируйте и выполните программу. Введите любое
положительное число и нажмите
Enter в консоли:
Пример рекурсивного вызова пользовательской функции самой себя.
Использование рекурсивных функций может быть менее эффективно, чем
использование чистых циклов. В частности из-за более высокого расхода
памяти машины. Так как каждый промежуточный результат нужно сохранять в памяти для дальнейшего его
использования.
Знать как написать и прочитать код рекурсивный функции всё равно нужно, потому что они могут встретиться в
чужих
программах. А во вновь написанных вами программах всячески избегайте рекурсии.
Напишем новую программу factorial.c. Программа будет вычислять
факториал введённого в
консоль
пользователем целого числа с помощью рекурсии:
Блок-схема рекурсивного вызова пользовательской функции самой себя -
Вычисления
Факториала
Целого Числа.
Исходный код:
#include <stdio.h>
int myFactorial(int A);
intmain(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;
}
}
Внимательно проследите утверждение за утверждением выполняемые в строках
программы.
Особенно пристально исследуйте что происходит внутри пользовательской
функции.
Мысленно "передайте" в пользовательскую функцию число 2 и "отработайте"
каждую строку
программы.
Сохраните, скомпилируйте и выполните программу:
Перепишем нашу программу вычисления факториала factorial.c -
напишем компактную форму
определения функции так:
int myFactorial(int A)
{
return ((A == 1 || A == 0) ? 1 : A * myFactorial(A-1));
}
Мы применили полученные нами знания о логических булевых операциях и поместили их в качестве тернарного
условия.
Теперь исходный код выглядит так:
#include <stdio.h>
int myFactorial(int A);
intmain(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));
}
Выполните эту версию программы.
Напишем новую программу 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), т.е. ряд строится так:
Очевидно, что математическая природа ряда Фибоначчи простейшая рекурсия, где каждый член ряда зависит от двух
предыдущих членов. Чтобы вычислить 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) как функцию Аккермана.
Она может быть задана следующим образом:
A(m, n) = n + 1, если m == 0;
A(m, n) = A(m - 1, 1), если m > 0 и n == 0;
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));
}
}
Функция Аккермана на языке С.
Сортировка слиянием
Она рекурсивно делит массив на более мелкие массивы, пока размер каждого массива станет равным одному элементу.
Она сортирует их по отдельности, а затем объединяет их обратно, чтобы получить отсортированный массив.
Таким образом, она следует принципу разделяй и властвуй. На рисунке показан алгоритм процесса
сортировки:
Функция сортировки массива Слиянием.
Разделение: первый шаг состоит в том, чтобы разделить массив на два подмассива, найдя средний индекс
массива (Mid) следующим образом:
Mid(A) = (F + L) / 2, Где: A - массив, F - начальный индекс массива, L - последний индекс массива.
Сортировка с помощью Рекурсии: после деления массива мы рекурсивно применяем алгоритм сортировки
слиянием к двум подмассивам. Этот шаг продолжается до тех пор, пока мы не достигнём подмассивов размером в 1
элемент,
которые уже считаются отсортированными(на рисунке выше "Разделение массива").
Слияние: Как только два подмассива отсортированы, мы объединяем их обратно, чтобы получить один
отсортированный массив. Процесс слияния включает в себя сравнение элементов из двух подмассивов и выбор
меньшего (или большего) элемента для размещения их в окончательно отсортированном массиве. Этот шаг
повторяется, пока все элементы из обоих отсортированных подмассивов будут объединены в один отсортированный
массив(на рисунке выше "Слияние массива").
Опишем словами Алгоритм Сортировки Слиянием так:
Объявление переменных:
Array[ ] - массив,
First - индекс первого элемента массива A[ ],
Last - индекс последнего элемента массива A[ ],
Mid - индекс среднего элемента массива A[ ].
Если Last больше First, тогда:
Вычислить середину Mid = (First + Last) / 2, которая делит массив на две половины.
Вызвать функцию mergesort(Array, First, Mid) для левой половины.
Вызвать функцию mergesort(Array, Mid + 1, Last) для правой половины.
Вызвать функцию merge(Array, First, Mid, Last), чтобы рекурсивно объединить две половины в
отсортированном порядке, чтобы остался только один отсортированный массив.
Конец
Составим блок-схему для алгоритма Сортировки Слиянием так:
Блок-схема алгоритма сортировки массива слиянием.
Напишем программу 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);
intmain()
{
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++;
}
}
Изучите блок-схему алгоритма, прослеживая и проговаривая выполняемые
шаги алгоритма в уме.
Напиши, сохраните и выполните программу:
Результат выполнения программы.
Алгоритм Быстрой Сортировки
Он выбирает элемент в качестве опорного (первый элемент) и затем делит массив на две части на основе этого
опорного. Опорный элемент разделяет массив на два подмассива, то есть на левый и правый массивы, таким образом,
что элементы в левом массиве меньше значения опорного элемента, а элементы в правом массиве больше опорного.
Это также алгоритм принципа разделяй и властвуй, как и сортировка слиянием.
Рассмотрим пример, показанный на рисунке ниже:
Не отсортированный массив.
Рассмотрим алгоритм быстрой сортировки. Первый проход по массиву:
Посмотрим на предыдущее изображение - опорный элемент = 30, i и j — это два указателя.
i перемещается вправо ( i++ ) и останавливается, когда находит элемент, больший опорного
элемента.
j перемещается влево ( j-- ) до тех пор, пока не найдёт элемент, меньший опорного элемента.
Если i меньше j, поменять местами оба элемента, на которые указывают i и j.
Если i больше j, поменять местами опорный элемент и элемент, на который указывает j.
После выполнения шагов выше, массив делится на две части, а опорный элемент находит своё подходящее
место. То есть все элементы слева от опорного элемента меньше его, а все элементы справа - больше, как
показано на рисунке:
Массив после первого прохода алгоритма быстрой сортировки.
После этого те же шаги будут применяться к подмассиву слева от опорного элемента и к подмассиву справа от
опорного элемента. Предыдущие шаги можно записать следующим псевдокодом:
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
Поскольку оба условия ложные
Массив после пятого шага.
Логическое разделение массива на две части, после пятого шага.
Опишем словами алгоритм быстрой сортировки так:
Алгоритм Быстрой Сортировки
Объявить массив Array[] и переменную Last, которая является последним элементом массива
Array.
Определить действия функции Quicksort(Array, 0, Last)
Переместить опорный элемент в такое положение, чтобы слева от него были числа меньше его, а справа —
больше.
Отсортировать левую часть, то есть выполнить функцию Quicksort(Array, 0, j - 1)
Отсортировать правую часть, то есть выполнить функцию Quicksort(Array, j + 1, Last)
Конец
Напишем программу 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); // рекурсивно отсортировать правый меньший подмассив
}
}
Напишите, сохраните и выполните программу.
Результат выполнения программы для массива целых чисел, размером в десять элементов.
Заключение
Итак, мы обсудили важную концепцию программирования, известную как Рекурсия.
Рекурсивная функция — это функция, которая решает задачи, разбивая их на более мелкие подзадачи через вызов
самой себя. С помощью рекурсии, Мы решили такие задачи:
Вычисление Факториала числа — Факториал числа определяется как произведение всех
положительных целых чисел до этого числа.
Построили Последовательность Фибоначчи — это последовательность, в которой
каждое число является суммой двух предыдущих.
Описали Функцию Аккермана — это математическая функция, используемая для
иллюстрации вычислительной сложности.
Провели Сортировку массива Слиянием — это алгоритм сортировки, который разбивает массив,
рекурсивно сортирует подмассивы, а затем объединяет их.
Провели Быструю Сортировку массива — это ещё один алгоритм сортировки,
который выбирает опорный элемент, разбивает элементы на части и рекурсивно сортирует подмассивы.
Важные моменты
Рекурсивная функция вызывает саму себя многократно; такие вызовы функций известны как рекурсивные вызовы.
Этот процесс называется рекурсией.
Условие остановки или базовый случай используется для прекращения процесса рекурсии. Таким образом, оно
работает как терминатор процесса рекурсии.
Факториал числа получается умножением всех положительных целых чисел от 1 до этого числа.
В ряде Фибоначчи каждое слагаемое является суммой двух предыдущих. Он начинается с 0, за которым следует 1.
нёмецкий математик Вильгельм Аккерман разработал функцию Аккермана в 1928 году. Это рекурсивная
математическая функция.
Функция Аккермана принимает два входа (m и n), оба являются неотрицательными целыми числами.
Слияние рекурсивно разбивает массив на более мелкие массивы, пока размер массива не станет равным одному.
Затем оно сортирует их индивидуально, а затем объединяет их обратно, чтобы получить отсортированный массив.
Быстрая сортировка выбирает элемент в качестве опорного (первый элемент), а затем делит массив на две части
на основе этого опорного элемента.
Оперативный элемент делит массив на два подмассива, то есть на левый и правый массивы, так что элементы в
левом массиве меньше значения оперативного элемента, а элементы в правом массиве больше, чем опорный.
Алгоритмы слияния и быстрой сортировки являются алгоритмами «разделяй и властвуй», подобно алгоритму
сортировки слиянием.
Важные вопросы
Что такое рекурсия и как она работает?
Что такое базовый случай в рекурсивной функции и почему он важен?
В чем разница между рекурсией и итерацией?
Как можно предотвратить бесконечную рекурсию в рекурсивной функции?
Объясните концепцию рекурсивного стека и как она относится к рекурсивным вызовам функций.
Напишите рекурсивную функцию для вычисления факториала числа.
Как вы бы реализовали рекурсивную функцию для вычисления n-го числа Фибоначчи?
Опишите преимущества и недостатки использования рекурсии в программировании.
Напишите рекурсивную функцию для вычисления суммы всех элементов в массиве.
Как можно преобразовать рекурсивную функцию в итеративную?
Подробно объясните функцию Аккермана, взяв подходящий пример.
Напишите программу для расчета результата функции Аккермана.
Объясните ряд Фибоначчи и напишите программу для генерации ряда Фибоначчи до заданного числа.
Объясните сортировку слиянием с использованием рекурсии.
Программы в примерах, приведенных в этом учебнике, маленькие - и фактически
показывают наглядно реализацию одной-двух концепций языка С в одном примере, но в действительности
большинство
программ, написанных на языке C, будут
содержать гораздо больше кода - сотни, тысячи, и даже сотни тысяч строк.
При разработке крупных программ следует продумать структуру
программы. Поддержка программного кода, размещённого в одном файле,
может стать неудобной по мере развития программы.
Для того чтобы упростить структуру программы, разрешается создать
пользовательский заголовочный файл, содержащий в себе функции,
которые могут быть использованы много раз. Такой файл должен иметь
расширение .h,(как и обычные заголовочные файлы библиотеки C). Так
программисты придерживаются
одной из фундаментальных основ - модульность.
Функции, расположенные в пользовательском заголовочном файле,
могут быть доступны программе, если записать его в начало файла,
содержащего функцию main() и под директиву препроцессора
#include. Имя
пользовательского заголовочного файла должно размещаться в двойных
кавычках " ". Символы < и >, разрешены для
обрамления только стандартных заголовочных файлов. Вот так:
Напишем новую программу squre.c, и напишем пользовательский файл
заголовков
utils.h
Пользовательский файл заголовков utils.h будет содержать в себе
одну функцию -
вычисление
возведения значения целочисленной переменной в квадрат(во вторую степень):
int square(int num)
{
return(num * num);
}
Создан простейший пользовательский файл заголовков. В этом файле
определена простая
функция -
возведение целого числа в квадрат.
Программа squre.c, содержащая главную функцию, будет получать от
пользователя
значение
целого числа из консоли, возводить его в квадрат, и запрашивать
пользователя о повторении
этого
действия:
#include <stdio.h>
#include "utils.h"
void getnum();
intmain()
{
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;
}
}
Сохраните, скомпилируйте и выполните программу:
Два важных замечания:
Файл программы и пользовательский заголовочный
файл должны находиться
в одной директории(папке), но компилируются они с помощью
обычный команды — компилятор считывает заголовочный файл автоматически
благодаря директиве препроцессора #include.
Обратите внимание на то, что спецификатор формата
%1s в этом примере используется для того, чтобы считать
один символ, введённый пользователем.
Вложение определения
функции внутри другой функции
В языке С стандарта ANSI C запрещено вложение определения одной функции
внутри другой
функции!
Однако некоторые компиляторы(например: GCC, Dev-C++ - имеют такое расширение)
поддерживают
вложение
определений функций. Это расширение не
переносимое на другие компиляторы С, и код может не скомпилироваться,
или
скомпилироваться не
ожидаемым образом - т.е. спровоцировать ошибку.
Вот блок-схема демонстрирующая визуально концепцию последовательного вызова
функции внутри
другой
функции согласно стандарта ANSI C:
Визуализация концепции вызова функции другой функцией - поддерживается
стандартом ANSI C.
#include <stdio.h>
void myFunc_1(); /* Объявление прототипа(заголовка) Первой Глобальной функции */
void myFunc_2(); /* Объявление прототипа(заголовка) Второй Глобальной функции */intmain(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(); /* Объявление прототипа(заголовка) Первой Глобальной функции */intmain(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 по
нескольким
причинам:
Поддержка переносимости кода на любые платформы и компиляторы.
Упрощение анализа, отладки и сопровождения кода.
Код для практической работы в реальной индустрии, где код должен
соответствовать
стандарту ANSI C.
Ограничение доступности -
Область видимости
Мы уже говорили об области видимости переменных, теперь раскроем эту тему для
функций.
Ключевое слово static может быть использовано для того, чтобы
ограничить
доступность функций в пределах файла, в котором они были созданы, точно так
же,
как и для переменных.
Этот синтаксис рекомендуется использовать в больших программах, которые
располагаются в нескольких файлах .с, чтобы оградить их от
случайного использования их функций.
Например, функции square() и multiply() не
могут быть вызваны непосредственно из функции main() в примере
ниже:
Напишем новую программу menu.c, которая предоставляет пользователю
меню, в котором
можно выбрать математические действия. Для начала объявим Глобальную
пользовательскую функцию, вызовем её в
теле
главной
функции, и после главной функции определим её:
#include <stdio.h>
void menu();
intmain()
{
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); /* Здесь мы вызываем функцию не объявленную в этом файле исходного кода */
}
Теперь напишем второй файл action.c для программы. Начнём с
инструкции
препроцессора, позволяющей подключить функции стандартной библиотеки
ввода/вывода, и определим две простые статические функции:
#include <stdio.h>
static square(int a)
{
return (a * a);
}
static multiplay(int a, int b)
{
return (a * b);
}
В этом же файле Определим функцию, в которую будет передаваться
вариант(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;
}
}
Сохраните оба файла, а затем скомпилируйте и выполните
программу, чтобы увидеть распечатанный в консоли результат работы
статических функций. Вот команда компилятору указывающая объединить два
файла:
menu.c и action.c в один объектный файл:
gcc menu.c action.c -o menu.exe
Ошибка компиляции:
Эти две ошибки легко понять:
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, внутри которых каждый вызывает
функцию из другого.
Но область видимости функций внутри файлов ограничена их пределами, а мы не
обеспечили метода
взаимодействия этих функций между файлами. Исправим ошибку:
Напишем новый файл заголовков функций menu.h . При компиляции файла
заголовков
menu.h и двух файлов menu.c и action.c компилятор на
этапе
Компоновки(линковки/связывания)
прочитает содержимое фалов и создаст связи позволяющие функциям
взаимодействовать между
собой.
В файле заголовков нужно просто объявить интересующие нас функции так:
void menu(void);
void action(int option);
Сохраняем файл заголовков.
Теперь необходимо сослаться(подключить) в наших файлах menu.c и
action.c на
пользовательский файл заголовков menu.h . Мы уже знаем, что записать
подключение файла
заголовков нужно в качестве инструкции препроцессору, как мы это делаем со
стандартными
заголовочными файлами, синтаксис так же мы уже знаем - вместо угловых
скобок <
>,
обрамляем
имя заголовочного файла двойными кавычками " ":
В файле menu.c подключим файл заголовков menu.h
#include "menu.h"
В файле action.c подключим файл заголовков menu.h
#include "menu.h"
Теперь у нас есть три файла:
Файл заголовков(прототипов) функций menu.h
void menu(void);
void action(int option);
Главная функция в файле menu.c
#include <stdio.h>
#include "menu.h"
void menu();
intmain()
{
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;
}
}
В Visual Studio Code у вас должны быть три файла, сохранённые и готовые к
компиляции:
Файлы готовы к компиляции.
Все файлы, которые вы компилируете в один объектный файл,
должны находится в
одной
папке,
тогда компилятор автоматические "знает" где их "искать"!
Проверьте, что все файлы сохранены. Напишем в консоль следующую команду
компилятору, в
которой
теперь укажем три файла, которые необходимо скомпилировать в один и
выполним программу:
gcc menu.h menu.c action.c -o menu.exe
Выполним несколько действий в программе:
Заключение
Пользовательские функции объявляются путём указания типа
данных, которые будут возвращены функцией, затем её имени, а затем
парой круглых ( ) скобок для аргументов.
Закончить конструкцию следует точкой с запятой ; .
Объявления функций также называются прототипами функций,
они должны располагаться перед функцией main() — поэтому
компилятор будет знать об их существовании при чтении функции
main(), и выделит для них область памяти.
Определения функций, непосредственно содержащие утверждения,
которые необходимо выполнить, когда вызывается функция,
должны располагаться после блока функции main().
Объявления функций опционально могут иметь внутри скобок
разделённый запятыми список аргументов, передаваемых вызываемой
стороной, для каждого из которых необходимо указать тип данных
и имя.
Аргументы, указанные в определении функции, должны
соответствовать аргументам в описании, поскольку последние являются
их формальными параметрами.
В программировании на языке C аргументы передаются по
значению — функция работает только с копией оригинального
значения.
Функция может рекурсивно вызывать саму себя, в этом случае
она должна содержать утверждение, изменяющее проверочное
выражение, чтобы в определённый момент завершиться.
Самих же рекурсивных функций стоит избегать
в исходном коде - из-за непрозрачности(сложности понимания) кода.
Пользовательские заголовочные файлы должны иметь
расширение .h .
Если с помощью директивы препроцессора #include добавляются
пользовательские заголовочные файлы, имя файла указывается
в двойных кавычках " " - для визуального их отличия от стандартных
заголовочных
файлов.
Ключевое слово static может быть использовано в объявлениях
и описаниях функций, чтобы ограничить к ним доступ рамками
файла, в котором они находятся.
Крупные программы должны объявлять функции с помощью
ключевого слова static, если только нет какой-то особенной
причины,
по которой функция должна быть видима за пределами файла.
Указывать прототипы необязательно для функций,
располагающихся за пределами файла, содержащего функцию
main().
Для предоставления возможности файлам взаимодействовать - позволить им
вызывать функции друг
друга, важно написать пользовательский файл заголовков функций,
скомпилировать их вместе в
один
объектный файл. При чём, все эти файлы должны быть сохранены в одной папке.
10 правил
программирования (от NASA) на основе
языка C
В серьёзных проектах по разработке ПО используют рекомендации по стилю кода.
Они помогают
понять, как
правильно структурировать код и какие возможности языка можно и нельзя
использовать. Проблема в
том,
что обычно в таких рекомендациях слишком много правил (100+), и программисты
не соблюдают их.
Чтобы сделать код надёжным и легче проверяемым, лучше иметь небольшой набор
простых, но строгих
правил, которые можно проверить автоматически. Эти правила особенно важны для
разработки
критически
важного ПО, где ошибки могут стоить человеческих жизней.
NASA использует 10 простых правил для языка C при написании такого кода. Они
помогают писать
надёжный,
понятный и проверяемый код.
Почему именно язык C
C — популярный язык для разработки системного и встраиваемого ПО, в том числе
в NASA, благодаря:
большому количеству инструментов анализа кода,
отладчиков,
надёжных компиляторов,
анализаторов кода
Эти 10 правил в первую очередь ориентированы на С, но принципы полезны и для
других языков.
10 правил
написания надёжного и безопасного кода на
C
Используй только простые конструкции управления.
Не используй goto, setjmp/longjmp, прямую и косвенную
рекурсии.
Почему: простой поток выполнения упрощает анализ и делает код
понятнее.
Отказ от рекурсии позволяет анализаторам кода доказать, что стек не
переполнится,
и выполнение будет завершено без ошибок.
Задавай верхние границы всем циклам.
Проверяющие инструменты должны легко доказывать, что цикл не
будет выполняться
бесконечно.
Почему: предотвращает зависание кода и помогает анализаторам
проверить выполнение
программы.
Если нужно пройти по списку неизвестной длины, добавляй явную границу
и проверку с
вызовом
assert при её превышении.
Не используй динамическое выделение памяти после её инициализации.
После старта программы не используй malloc и подобные
функции.
Почему: динамическая память может вести себя непредсказуемо и
приводить к ошибкам
утечки
памяти, ошибкам использования после free, выходу за
границы памяти. Лучше
использовать
предвыделенную память и стек (без рекурсии стек можно контролировать
по размеру).
Функция не должна превышать одной страницы кода.
Не более 60 строк кода на функцию (по одной строке на инструкцию).
Почему: небольшие функции легче читать, тестировать и анализировать.
Длинные функции
обычно
означают плохую структуру кода.
Используй в среднём хотя бы два assert на функцию.
assert проверяет, что в коде не происходят
невозможные ситуации.
assert должен:
быть без побочных эффектов,
представлять собой логическую проверку,
при срабатывании возвращать ошибку вызывающему коду.
Почему: assert помогает выявлять ошибки на ранних
этапах и упрощает
отладку. Их
можно
отключить после тестирования в коде отправляемом в официальный запуск
в промышленность.
Объявляй переменные в самом узком месте области видимости.
Почему: если переменная видна только там, где нужна, то:
её не могут случайно изменить другие части программы,
проще найти ошибку,
не будет переиспользования переменной для разных целей, что
усложняет отладку.
Проверяй значения, возвращаемые всеми функциями, и проверяй параметры.
Вызывающая функция должна проверять возвращаемое значение, а
функция,
принимающая
параметры,
должна проверять их корректность.
Почему: часто игнорирование возвратов функций приводит к ошибкам,
особенно если
функция
возвращает ошибку, которую нужно обработать.
Минимизируй использование препроцессора.
Разрешено:
подключение заголовков,
простые макросы.
Запрещено:
сложные макросы с подстановкой токенов,
макросы с переменным числом аргументов,
рекурсивные макросы.
Условная компиляция ( #ifdef ) должна использоваться по
минимуму.
Почему: сложные макросы и условная компиляция ухудшают читаемость и
усложняют анализ
кода.
Ограничь использование Указателей.
Разрешён только один уровень разыменования.
Запрещено:
скрывать разыменования в макросах и typedef,
использовать указатели на функции (если только нет очень
веской причины).
Почему: указатели часто используются неправильно и усложняют анализ
кода.
Включай все предупреждения компилятора и анализаторов и устраняй их.
С самого начала проекта:
включай максимально строгие предупреждения компилятора,
устраняй все предупреждения,
ежедневно проверяй код статическими анализаторами.
Почему: современные анализаторы помогают быстро находить ошибки.
Если компилятор или
анализатор выдаёт предупреждение, надо переписать код так, чтобы
избежать неоднозначности.
Не переживайте из-за не понятных сейчас вам концепций описанных в
правилах, скоро вы
узнаете о всём что они описывают.
Указатели
Изучим, как можно обратиться к данным путём использования их
адреса хранения
в памяти.
Указатели — это очень полезный элемент языка C для эффективного
программирования. Они являются переменными, хранящими адрес
в памяти других переменных. Нет практически ни одной программы на C не
использующей Указатели.
Когда объявляется обычная переменная, для неё выделяется некоторый
объём памяти(диапазон байтов) в соответствии с её типом данных в свободном
участке памяти. После этого, когда программа встречает имя переменной,
она обращается к данным по её адресу. Аналогично, когда программа
встречает имя переменной-указателя, она обращается к данным,
хранящимся по адресу, хранящемуся в ней. Но переменную-указатель
разрешается разыменовать, чтобы можно было обратиться к
данным,
хранящимся по адресу, который лежит в указателе, а не к самому адресу.
Переменные-указатели объявляются точно так же, как и другие
переменные, но к имени указателя добавляется символ *. В этом случае
символ * представляет собой операцию разыменования и означает,
что объявленная переменная является указателем(хранит в себе адрес блока
памяти).
Операция разыменования *, также известна как косвенная
операция.
Итак, символ *(астерикс, или просто звёздочка) - выполняет две функции
при работе с
именами
указателей:
Создаёт переменную-указатель, в которой будет хранится адрес данных.
Синтаксис такой:
<тип-данных> *<имя-переменной-указателя>;
,
например:
float *pi; /* Переменная pi типа float объявлена как
Указатель на адрес в памяти, где хранится значение */
Разыменовывает переменную-указатель, чтобы получить данные, которые
хранятся по адресу.
Синтаксис
такой:
*<имя-переменной-указателя>;
*pi; /* Вернёт значение, которое хранится по адресу в памяти */
Вот пример полного синтаксиса объявления, инициализации и работы с
Указателем:
float *pi; /* Указание типа данных перед звёздочкой есть Объявление переменной-указателя,
в нашем случаи - с именем pi, типа данных float, хранимый по этому адресу */
*pi = 3.141592; /* Разыменование(работа со значением хранимым по адресу в памяти)
указателя записывается без указания типа данных.
По адресу, который хранит переменная pi типа float записать в память число 3.141592 */
После того, как переменная-указатель была объявлена, ей можно
назначить адрес другой переменной с помощью операции адресации &(символ
амперсанд).
Перед переменной-указателем при присвоении ей значения операцию
разыменования, *, помещать не нужно, если только инициализация не
происходит непосредственно после объявления переменной-указателя.
Имя переменной-указателя само по себе представляет адрес в
памяти, записываемый в шестнадцатеричном формате.
Когда операция разыменования, *, используется при объявлении
переменной, он указывает, что переменная является указателем, но
использование этой операции в другом месте программы вызовет обращение
к данным, хранящимся по адресу, лежащему в переменной-указателе.
С помощью имени переменной-указателя можно обратиться к
данным, хранящимся по адресу, присвоенному этой переменной, если
перед ним стоит операция разыменования, *.
Это значит, что программе будет возвращён адрес, присвоенный
переменной-указателю, с помощью её имени. Также она может получить
доступ к данным, лежащим по адресу, который хранится в
переменной-указателе, если поместить операцию разыменования, *,
перед именем этой переменной.
Убедитесь, что вы удалили указатель после удаления
объекта, к которому он обращается, чтобы не оставлять в программе висящие
указатели.
Рассмотрим пример:
Напишем новую программу pointer.c:
#include <stdio.h>
intmain()
{
int num = 8; /* Объявляем и инициализируем переменную целочисленного типа */int *ptr = # /* Объявляем переменную-указатель,
которую инициализируем значением адреса, по которому в памяти хранится переменная num */return 0;
}
Далее в блоке функции main() напечатаем в консоль содержимое
обеих
переменных, а также значение, к которому обращается указатель:
#include <stdio.h>
intmain()
{
int num = 8;
int *ptr = #
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;
}
Теперь в блоке функции main() присвойте новое значение
обычной
переменной с помощью указателя, а затем ещё раз напечатаем в консоль её
содержимое и значение, к которому обращается указатель:
#include <stdio.h>
intmain()
{
int num = 8;
int *ptr = #
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;
}
Сохраним, скомпилируем и выполним программу:
Пример работы с Указателями и Адресами памяти машины.
Напишем новую программу l_endian.c , в которой подробнее рассмотрим
адресацию памяти
по
принципу Little-Endian:
#include <stdio.h>
intmain(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;
}
Для каждой машины будут отображаться свои значения адресов. Сохраним,
скомпилируем и
выполним
программу:
Обратите внимание: Шестнадцатиричное значение адреса и Значение
Указателя внешне
отличаются в
консоли. Указатель отображается в размере восьми байт,
а Адрес переменной в размере четырёх байт.
Размер значения Указателя
Указатель это Тип данных, представленный в языке C. Как и любой другой Тип
данных, Указатель
имеет
размер - сколько байт в памяти он занимает.
Мы уже изучили функцию sizeof() , которой можно передать
аргументом Указатель:
Напишем новую программу sizepointer.c для демонстрации работы
функции
sizeof()
с Указателем:
#include <stdio.h>
intmain(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;
}
Сохраним, скомпилируем и выполним программу несколько раз - Мы увидим, что
Операционная
Система,
каждый раз при запуске программы выделяет другой адрес, но размер всегда 8
байт:
Указатель на Константу
Синтаксис указателя на константу следующий:
const <тип-данных> *<имя-константы>;
Данные на которые указывает Указатель постоянные, их значение изменить нельзя.
Указатель можно изменить - т.е. на какую-то другую переменную он будет
указывать - изменятся
адрес значения, а не значение по адресу.
Напишем простую программу constpointer.c для демонстрации концепции:
#include <stdio.h>
intmain(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>
intmain(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с
типа, на который он
указывает.
Итак:
Предупреждение -Wdiscarded-qualifiers защищает от ошибок
записи в данные,
которые
объявлены как
const.
Допускать такую программу в реальный мир с предупреждениями,(вспоминаем 10
правил
программирования
от NASA) нельзя. Чтобы убрать предупреждение:
Используйте const при объявлении указателя.
Не снимайте const квалификатор без крайней
необходимости и понимания
последствий.
Прививайте себе привычку использовать const для неизменяемых
данных для
безопасного и
чистого кода на C.
Напоминаю - Объявляйте переменные до того, как в ход пойдут другие
утверждения — например, в начале функции main().
Постоянный
Указатель на Постоянное Значение в памяти
Напишем программу ccpntr.c для демонстрации работы такого утверждения:
#include <stdio.h>
intmain(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>
intmain(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 Указатель — это указатель общего (неопределённого) типа,
который может
указывать на
данные любого
типа. Его ещё называют:
универсальным указателем (generic pointer)
указателем на ничто
По умолчанию размер void Указателя равен восьми байтам.
Продемонстрируем это на
примере
программы.
Напишем программу void_ptr.c:
Тип данных на которые указывает void Указатель можно изменять в
ходе выполнения
программы.
Напишем программу void_type_ptr.c:
#include <stdio.h>
intmain(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>
intmain(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>
intmain(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 переместит его к следующему элементу массива.
Начните новую программу movptr.c с инструкции препроцессора,
позволяющей включить функции стандартной библиотеки ввода/вывода, объявите
целочисленную
переменную,
и целочисленный массив с десятью элементами:
Далее, в главной функции объявите переменную-указатель и
проинициализируйте его адресом первого элемента массива, затем
выведите этот адрес и значение, которое располагается по этому адресу
в памяти:
#include <stdio.h>
intmain(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;
}
Теперь, в главной функции увеличивайте значение указателя на 1,
чтобы cмещать его к следующему элементу массива один за другим:
#include <stdio.h>
intmain(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;
}
Теперь сместим адрес на два элемента назад, чтобы указатель адресовал
первый элемент массива:
#include <stdio.h>
intmain(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;
}
Далее добавьте цикл for() и выведите индекс каждого элемента
массива,
а также их значения:
#include <stdio.h>
intmain(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;
}
Сохраните файл программы, а затем скомпилируйте и запустите
программу, чтобы увидеть значение переменной и значение, к
которому обращается указатель:
Обратите внимание на количество байт, на которое
осуществляется смещение адреса
при
инкременте - 4 байта,
ровно такой размер имеет тип int. При декременте
на значение 2,
смещение
произошло на 2x4 = 8 байт.
Избавимся от "магического" числа 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 элементов в массиве. Мы
избавили код от
"магического" числа, и тем самым явно описали "что"
проверяется в условии
проверочного утверждения цикла.
Вспомните, в массиве, первый его элемент имеет индекс
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);
}
Имя массива выступает в качестве
указателя на его первый
элемент.
Пропишите все изменения в коде, сохраните, скомпилируйте и выполните
программу снова.
Смещение void Указателя
Мы изучили, что по умолчанию инкремент и декремент смещают адрес на один байт.
Но, в зависимости
от
типа данных Указателя, смещение будет производится согласно размеру этого типа
данных
занимаемого в
памяти.
Для Указателя неопределённого типа данных - void-Указателя, при
приведении его к
конкретному типу данных, смещение автоматически изменяется на размер типа
данных.
Напишем новую программу incdec_void_type.c для демонстрации этого,
инициализируем три
типа
переменных: short int, float, double,
объявим
NULL-Указатель и
сначала инициализируем его адресом переменной I типа
short int.
Напишем фрагмент кода для short int
#include <stdio.h>
intmain(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>
intmain(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>
intmain(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>
intmain(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:
Ограничь использование указателей
Разрешён только один уровень разыменования.
Запрещено:
скрывать разыменования в макросах и typedef,
использовать указатели на функции (если только нет очень
веской причины).
Почему: указатели часто используются неправильно и усложняют анализ
кода.
Хорошее правило. В языке С есть возможность создавать Указатель на Указатель и
в редких случаях
это
применяется, знать как написать программу с таким функционалом нужно. Напишем
программу
ptr_to_ptr.c для демонстрации:
#include <stdio.h>
intmain(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>
intmain(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>
intmain(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, аргументы функций передаются
по значению в локальную переменную внутри вызываемой функции.
Это значит, что функция работает не с оригинальным значением, а только с
его копией.
Однако, как мы уже знаем - с помощью Указателя можно изменить даже объявленной
константой
значение
переменной. Указатель позволяет работать непосредственно с ячейкой памяти
машины напрямую.
Передача указателя на оригинальное значение позволяет функции
работать с оригинальным значением по ссылке и является основным
преимуществом использования указателей.
Новая программа passpntr.c продемонстрирует эту возможность.
Объявите два прототипа
пользовательских функций, в каждую из которых передается один целочисленный
указатель.:
#include <stdio.h>
void twice(int *pntr);
void thrice(int *pntr);
int mian(void)
{
int num = 5;
int *pntr = #
return 0;
}
Далее в блоке функции main() напечатаем адрес, хранящийся в
указателе, а также значение, на которое ссылается указатель:
#include <stdio.h>
void twice(int *pntr);
void thrice(int *pntr);
int mian(void)
{
int num = 5;
int *pntr = #
printf("\npntr stores address: %p\n", pntr);
printf("pntr dereference value: %d\n\n", *pntr);
return 0;
}
Теперь напечатаем в консоль оригинальное значение целочисленной
переменной:
#include <stdio.h>
void twice(int *pntr);
void thrice(int *pntr);
int mian(void)
{
int num = 5;
int *pntr = #
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;
}
После блока функции main() определите две пользовательские
функции, объявленные с помощью прототипов, каждая из которых
получает в качестве аргумента целочисленный указатель:
#include <stdio.h>
void twice(int *pntr);
void thrice(int *pntr);
int mian(void)
{
int num = 5;
int *pntr = #
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);
}
В блок функции main() добавьте вызовы пользовательских функций,
передавая адрес обычной переменной как ссылку, а затем выведите
изменённое значение обычной переменной.
#include <stdio.h>
void twice(int *pntr);
void thrice(int *pntr);
int mian(void)
{
int num = 5;
int *pntr = #
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);
}
Сохраните, скомпилируйте и выполните программу:
Исправьте ошибки в программе, внесите необходимые изменения в
исходный код, чтобы вывод
в
консоль выглядел как на снимке экрана.
Итак, вспомним - все переменные размещаются в памяти машины.
Каждое такое размещение является байтами в памяти.
Каждый байт в памяти имеет свой адрес.
Схожий принцип используется и для функций - каждая функция размещена по
собственному адресу в
памяти
машины.
Имя массива - является Указателем на этот массив, и по сути указывает на
голову массива
- на
его первый элемент. Это и есть место размещения массива в памяти машины.
Имя функции - является Указателем на адрес в памяти, где размещаются все
утверждения определения
функции.
Напишем программу array_func.c демонстрирующую адреса размещения
массива и функции в
памяти
машины. Объявим прототип функции и определим её после главной функции,
инициализируем
целочисленные
переменные и целочисленный массив:
#include <stdio.h>
#include <math.h>
void myFunction();
intmain(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");
}
В теле главной функции напечатаем адреса целочисленной переменной, массива
и функции. И
убедимся,
что имена Массива и Функции и есть их Указатели:
#include <stdio.h>
#include <math.h>
void myFunction();
intmain(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");
}
Выполните программу:
Обратите ещё раз внимание на синтаксис: записи имени и адреса для
массива и функции
возвращают одинаковые адреса. Эта особенность нам скоро очень
пригодится.
Запомните: Символ *, находясь в скобках определения
функции,
показывает, что аргумент является указателем, а находясь внутри
утверждения,
с его помощью можно получить аргумент по ссылке.
В арифметическом утверждении этот символ является операцией умножения.
Как мы уже знаем - Имя Функции и Адрес Функции есть Указатель и записывается
так:
funcPointer = myFunc;
или так:
funcPointer = &myFunc;
Вызов функции можно записать так:
funcPointer(intVal1, intVal2);
или так:
(*funcPointer)(intVal1, intVal2);
Напишем программу func_pointer.c для иллюстрации выше описанного:
#include <stdio.h>
int mySummation(int A, int B);
intmain(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;
}
Выполните программу:
Пример вызова Функции по ссылке - через инициализацию Указателя
Адресом Функции.
Вот простая аналогия для понимания концепции Указателей Функций. Представь
меню в
ресторане:
Ресторан - Главная Функция main().
Названия блюд в меню ресторана - это Имена Функций.
Номера телефонов поваров - Указатели на Функции.
Ты можешь:
Лично позвать повара по имени в ресторане - обычный вызов функции.
Позвонить по номеру телефона и повар приготовит блюдо удалённо - Указатель
на Функцию.
Проясним детальнее
Прототип функции типа void:
void *(*funcPointer)(int *, int *);
Мы должны проследить такое утверждение изнутри наружу, так:
(*funcPointer)(int *, int *)
Указатель Функции принимает два аргумента, которые являются целочисленными
указателями, и потом
возвращается
void *
void-Указатель,- Указатель неопределённого типа.
Создание массивов указателей
Программа, способна содержать массивы указателей, в каждом элементе которого
хранятся адреса
других
переменных.
Эта возможность особенно полезна для работы с символьными
строками. Массив символов, который заканчивается символом \0 носит
статус строки, поэтому его можно присвоить переменной-указателю.
Имя символьного массива служит как указатель на его первый элемент,
поэтому не требуется операция адресации & для того, чтобы присвоить
строку переменной-указателю.
Напишем новую программу array_pointer.c, объявим целочисленную
переменную и
целочисленный
массив, массив инициализируем:
#include <stdio.h>
intmain(void)
{
int i;
int array[5] = {1, 2, 3, 4, 5};
return 0;
}
Далее объявите и проинициализируйте целочисленные переменные-указатели,
содержащие адрес каждого элемента массива так:
#include <stdio.h>
intmain(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;
}
Теперь объявите и проинициализируйте массив целочисленных
указателей, каждый элемент которого будет содержать один из целочисленных
указателей:
#include <stdio.h>
intmain(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;
}
Далее объявите и проинициализируйте массив символов, указатель
на этот массив и массив символьных указателей, содержащий строку внутри
каждого элемента:
#include <stdio.h>
intmain(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;
}
Для того чтобы добавить в строку символ пробела,
необходимо использовать комбинацию символов ' '(одинарная
кавычка, пробел,
одинарная
кавычка).
Две одинарные кавычки, расположенные рядом — '',
рассматривается как пустой элемент и вызывает ошибку
компилятора.
Добавьте цикл for(), с помощью которого выводится адрес каждого
элемента массива целочисленных указателей и значения, на которые
они ссылаются:
#include <stdio.h>
intmain(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;
}
Добавьте утверждение, печати в консоль значения, хранящегося в массиве
символов
string[9]:
printf("\n\nString is: %s\n\n", string);
Добавьте цикл for(), с помощью которого напечатайте каждую строку,
содержащуюся в каждом элементе массива символьных указателей и адрес каждой
строки:
for (i = 0; i < 3; i++)
{
printf("\nString [%d] at address %p contain: %s", i, strings_array[i], strings_array[i]);
}
С помощью имени указателя можно обратиться ко всей строке,
хранящейся
в символьном массиве, не прибегая к использованию операции *
разыменования.
Выполните программу:
Определите: Сколько байт занимает 1-ая и 2-я строка символьного
массива, судя по
адресам их размещения в памяти? Почему столько? Сколько байт занимает
3-я строка
символьного
массива?
Указатели и Строковые литералы
Указатели и Строковые литералы(массивы символов) очень тесно связанны между
собой. Имена
массивов
могут пониматься как Постоянные
Указатели на первый элемент массива.
Напишем программу pntrs_arrays.c. Приведём пример когда сама
строка
является
Постоянным Адресом в памяти машины:
#include <stdio.h>
intmain(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" в сегменте
данных
программы.
Выполните программу:
Первые три строки в консоли печатают одинаковый адрес. А четвёртая
строка - уже другой
адрес.
Изучите комментарии в коде и объяснение под кодом для понимания что
значит каждый
адрес.
Указатель на
строковый литерал и Строковый Литерал
В программе string_literal.c ниже, написан код в котором Указатель на
строковый литерал
содержит адрес размером в
восемь байт, и также инициализирован массив - он же строковый литерал,
содержащий восемь
символов (7
букв и символ окончание строки \0):
#include <stdio.h>
intmain(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() записывается одинаково как для
указателя так и для
имени
литерала, однако возвращает значения для различных типов данных: В первом
случаи - адрес, во
втором
- размер массива.
Напишем новую программу context.c, в которой посмотрим ещё раз как
придать контекст
для
типа
данных.
#include <stdio.h>
intmain(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;
}
Выполните программу:
Внимательно изучите пример, и запомните различные контексты типов
данных при работе с
Указателями и Размерами.
Массив Указателей с
приведением типа данных
Напишем новую программу pntrs_array_type.c
До главной функции запишем три пользовательских прототипа функций, после
главной функции
определим
их:
#include <stdio.h>
int mySummation(int A, int B);
int mySubstraction(int A, int B);
int myMultiplication(int A, int B);
intmain(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;
}
Внутри главной функции Объявим и Инициализируем целочисленные переменные:
int A = 46;
int B = 79;
int sum;
int sub;
int mult;
Внутри главной функции, после целочисленных переменных,
Объявим массив Указателей на Функции целочисленного типа:
int (*func_pntr[3])(int, int);
где:
func_pntr[3] - Это массив из трёх элементов с именем
func_pntr.
(*func_pntr[3]) - Элементы массива являются указателями
(звёздочка
относится к
каждому элементу массива).
(*func_pntr[3])(int, int) -
Каждый указатель указывает
на функцию,
принимающую
два int.
int (*func_pntr[3])(int, int);
- Каждая такая функция
возвращает
int.
Внутри главной функции, далее инициализируем каждый указатель функции из
массива
func_pntr
адресом соответствующей ему функции:
Внутри главной функции, далее инициализируем каждую переменную значением
возвращаемым
пользовательскими функциями по ссылке:
sum = func_pntr[0](A, B);
sum = 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);
Итоговый исходный код должен быть таким:
#include <stdio.h>
int mySummation(int A, int B);
int mySubstraction(int A, int B);
int myMultiplication(int A, int B);
intmain(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;
}
Выполним программу:
Пример синтаксиса создания массива Указателей на Функции.
Указатели на функции
Мы уже познакомились с Указателями на Функции, хотя эта
возможность в исходном коде используется реже, чем создание обычных
указателей.
Указатель на функцию похож на указатель на данные, но он всегда
должен помещаться в скобки при использовании операции
разыменования, *, чтобы избежать возникновения ошибок
компилятора.
После этой конструкции также должны следовать скобки, содержащие
аргументы, которые должны быть переданы в функцию, на которую
ссылается указатель.
Указатель на функцию содержит адрес в памяти, по которому размещается
начало функции.
Когда указатель на функцию разыменовывается, вызывается функция,
на которую он ссылается, и аргументы передаются в вызываемую функцию.
Указатель на функцию может быть передан как аргумент в другую
функцию. Функция, получившая такой аргумент(функцию), способна вызвать
функцию, на которую ссылается указатель.
Напишем новую программу fpntr_to_func.c.
Объявите прототип одной пользовательской функции, которая
имеет целочисленный аргумент, и прототип другой функции,
принимающей в качестве первого аргумента - указатель на функцию, в качестве
второго аргумента
-
целое число:
#include <stdio.h>
int bounce(int a);
int caller(int (*function)(int), int b);
intmain(void)
{
return 0;
}
Добавьте в функцию main(), в которой объявляется обычная
целочисленная переменная, а также объявляется и инициализируется
переменная-указатель на функцию:
#include <stdio.h>
int bounce(int a);
int caller(int (*function)(int), int b);
intmain(void)
{
int num;
int (*fpntr)(int) = bounce;
return 0;
}
После блока функции main() определите первую пользовательскую
функцию, которая выводит на экран полученное значение и возвращает целое
число,
и вторую - вызывающую обычную функцию из полученного указателя на функцию
и передающую ей полученное целочисленное значение:
#include <stdio.h>
int bounce(int a);
int caller(int (*function)(int), int b);
intmain(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);
}
В блоке функции main() присвойте значение целочисленной
переменной с помощью вызова обычной функции через указатель на неё
и выведите значение, которое она вернёт:
#include <stdio.h>
int bounce(int a);
int caller(int (*function)(int), int b);
intmain(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() в этом
примере
может быть указатель на любую функцию, которая
получает один целочисленный аргумент и возвращает
целочисленное значение, что указано в объявлении
прототипа.
Теперь присвойте новое значение целочисленной переменной,
передав указатель на функцию и целое число другой функции, которая,
в свою очередь, вызовет обычную функцию, а затем выведет значение,
которое та вернёт:
#include <stdio.h>
int bounce(int a);
int caller(int(*function)(int), int b);
intmain(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);
}
Итоговый исходный код должен быть написан так:
#include <stdio.h>
int bounce(int a);
int caller(int (*function)(int), int b);
intmain(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);
}
Выполните программу:
Как и другие указатели
в языке C, указатель на функцию просто хранит адрес
в памяти. Когда указатель на
функцию разыменовывается
с помощью операции *, вызывается функция, расположенная по
адресу,
хранящемуся в указателе.
Указатель на
Функцию как аргумент для другой Функции
Функции могут принимать Указатель на Функцию в качестве аргумента, а имена
функций могут быть
переданы
как Указатели на Функции.
Напишем новую программу func_as_argf.c. Объявим три прототипа
пользовательских
функций:
В теле главной функции два раза вызовим функцию, которой в качестве
аргумента передадим
имена
других функций. После главной функции напишем определение всех
пользовательских функций:
Выполните программу:
Функция вызывает другие функции, получив в качестве аргумента имя
другой функции.
Напишем новую программу func_pntr_as_arg.c, программа:
Создаёт указатель на функцию, которая принимает два int
и возвращает
int.
Передаёт этот указатель в другую функцию в качестве аргумента.
Эта функция возводит два числа в куб (³), передаёт их в функцию
сложения через
указатель.
Возвращает сумму кубов и выводит её на экран.
Подключим заголовочные файлы стандартного ввода/вывода, математических
функций, и
заголовочный
файл для явного приведения типов данных(в нашем случаи int)
Объявим две пользовательские функции для сложения кубов, и сложения:
int qubeSum(int A, int B, int(*fp)(int, int));
int mySum(int A, int B);
Инициализируем две целочисленные переменные:
int A = 5;
int B = 6;
Создадим Указатель на Функцию, который будет Указывать на другую Функцию:
int result = qubeSum(A, B, fp);
После главной функции определим пользовательскую функцию:
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;
}
Напечатаем результат в консоль:
printf("\nQubed sum of the Integers %d and %d is %d\n\n", A, B, result);
Итоговый исходный код должен быть написан так:
#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);
intmain(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;
}
Выполните программу:
Функция возвращает Указатель
Вспомните, Указатель это тип данных, его размер - 8 байт.
Функция может возвращать Указатель как результирующее значение выполнения этой
функции.
Напишем простую программу func_ret_pntr.c, в которой Функция
возвращает Указатель:
#include <stdio.h>
int A = 25; /* Объявляем глобальную переменную */int *myFunc(void); /* Объявляется функция myFunc, которая: Не принимает параметров (void).
Возвращает указатель на int (int *) */intmain(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);
}
Выполните программу:
Уничтожение локальной
переменной внутри функции
Локальная переменная имеющая свою область видимости внутри функции, после
завершения работы
функции
становится не доступной(уничтожается). Вернуть локальный адрес такой
переменной уже не возможно.
Напишем новую программу local_var.c, объявим прототип
пользовательской функцию,
которая не принимает параметров и возвращает целочисленный указатель:
#include <stdio.h>
int *myFunc(void);
intmain(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);
}
Объявим Указатель, инициализируем его адресом пользовательской функции,
напечатаем
адрес пользовательской функции и значение переменной расположенной внутри
пользовательской
функции:
#include <stdio.h>
int *myFunc(void);
intmain(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); /* Локальные переменные хранятся в стеке,
и после завершения функции память под локальную переменную освобождается
или используется под другие нужды машины. */
}
Выполните программу:
Часть кода не выполнилась - программа рушиться.
Скомпилированный с ошибкой код
не
выполнился корректно.
В момент возвращения адреса локальной переменной, после выхода из
пользовательской функции,
она
уже уничтожена. Это приводит к «Dangling Pointer» (висячий указатель):
Указатель pntr в main() содержит адрес, где ранее
была
Local, но там уже не гарантируется её корректное
значение.
Почему
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; /* Теперь это безопасно */
}
Почему это срабатывает:
static переменная хранится в статической области памяти, а не в стеке, и
живёт до завершения
программы.
Возвращать её адрес теперь безопасно - он не исчезнет по завершении работы
функции.
Можно разыменовывать указатель в main() и получать корректное
значение.
Вариант 2: Передавать адрес переменной через аргумент функции:
int *myFunc(int *pntr);
intmain(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);
}
Почему это срабатывает:
Переменная созданная в main() существует до завершения
main().
Передаём её адрес в myFunc() в качестве аргумента.
Функция записывает в неё значение.
Нет проблем с временем жизни памяти.
Напишем новую программу pass_by_value_gcc.c, для объяснения
передачи значений через
их
указатели. В этом примере, значения внутри функции могут быть понимаемыми
как локальные
переменные
внутри функции. Возвращение адреса аргумента значения может создавать
проблемы компиляции в
некоторых компиляторах.
Мы используем компилятор GCC 15.1.0 MinGW - проверим реализацию кода на
нём:
#include <stdio.h>
float *findLarger(float *, float *);
intmain(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 */
}
}
Выполните программу:
Компилятор 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, напишите и выполните программу:
Напоминаем: В языке программирования C символьные значения
должны помещаться в одинарные кавычки ' ', а строки — в двойные "
".
Пользователь может ввести строковое значение в программу,
написанную на языке C, с помощью функции scanf() -
примеры кода с ней мы приводили выше.
Эта функция хорошо работает как с отдельными символами, так
и с их последовательностями, формируя из отдельных символов целое слово.
Однако, функция scanf() имеет одно важное ограничение —
считывание прекращается, когда функция встречает пробел.
Это значит, что пользователь с помощью функции scanf()
не может ввести предложение,
поскольку строка будет обрезана после первого пробела.
Эту проблему можно решить с помощью двух альтернативных функций,
также располагающихся в файле <stdio.h>.
Первая из этих функций называется gets().
Она используется для чтения данных, введённых
пользователем. Эта функция принимает все символы (включая пробелы),
и присваивает строку массиву символов, указанному как аргумент. Она
автоматически добавляет символ-терминатор \0 в конце строки,
когда пользователь нажимает на клавишу Enter,
чтобы гарантировать, что введённые данные будут иметь статус строки.
Компаньоном этой функции является функция puts(),
которая выведет строку, переданную в качестве аргумента,
и автоматически в конце добавит символ перевода каретки.
Напишем программу getsputs.c, для демонстрации работы ввода строк.
В блоке функции main() запросите у пользователя данные,
которые впоследствии будут помещены в массив:
#include <stdio.h>
intmain(void)
{
char str[51];
printf("\nEnter up to 50 characteres with Spaces:\n");
gets(str);
return 0;
}
Теперь в блоке функции main() выведите строку, сохраненную в
массиве:
#include <stdio.h>
intmain(void)
{
char str[51];
printf("\nEnter up to 50 characteres with Spaces:\n");
gets(str);
printf("gets() read: ");
puts(str);
return 0;
}
Повторите процесс, чтобы увидеть ограничения функции scanf():
#include <stdio.h>
intmain(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;
}
Сохраните, скомпилируйте и выполните программу:
Во время компиляции, компилятор вывел в консоль Предупреждение:
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.
Читать такие статьи - частая деятельность программистов. Структура таких
статей обычно типовая - т.е. они
составлены так, чтобы быстро понять как работает функция. Сделаем и мы это.
Название функции и в какой библиотеке(заголовочном файле) она находится:
Название функции - fgets()
Заголовочный файл - <stdio.h>
Прочитаем прототип функции:
char* fgets(char* str, int count, FILE* stream); /* (До C99 (до 1999 года)) */
char* fgets( char* restrict str, int count, FILE* restrict stream ); /* (С C99) (после 1999 года) */
Так мы живём уже после 1999-го года, то нам нужен прототип с этой пометкой
(После C99).
Что делает функция (обычно 1-2 предложения описания) - Считывает не более
count-1 символов из
stream в str, включая \n, если он
встречается. Т.о. она считывает не
более count-1 символов, гарантируя, что не переполнит буфер.
Аргументы функции (Parameters):
str: куда сохраняются данные - указатель на буфер.
count: максимальное количество символов (обычно
sizeof(buf)).
stream: источник ввода (stdin для
клавиатуры).
Т.о. При замене gets(buffer); используем
fgets(buffer, sizeof(buffer), stdin);.
Возвращаемое значение - Возвращает str при успехе,
а NULL при ошибке или при получении символа EOF(End
Of File). Необходимое проверочное утверждение пишется так:
if (fgets(buffer, sizeof(buffer), stdin) == NULL) {
// Код обработки ошибки
}
Пример - Смотрим, как функция используется в коде.
Особенные ситуации и предупреждения:
Возвращает \n, если строка короткая.
Использовать проверку возврата для обработки ошибок.
Так же в статье может быть получена новая информация относительно новых
правил синтаксиса(из примеров
например) и др. Статьи размещаемые на сайтах с высоким уровнём доверия,
стоит читать периодически с целью
обновления знаний по языку. Очевидно - знание и изучение английского языка
всегда необходимо.
Изучив статью, определяемся как перепишем исходный код.
Фрагмент кода, который нам нужно исправить был написан так:
char str[51];
printf("\nEnter up to 50 characteres with Spaces:\n");
gets(str);
Что конструктивно выглядит как:
char buffer[100];
gets(buffer);
Компилятор посчитал это небезопасным и вызвал предупреждение.
Согласно новой предпочитаемой конструкции кода по стандарту С23, как:
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(), ей требуется передать два
аргумента.
Первый — это имя массива, в который требуется скопировать строку,
Второй — имя массива, из которого будет выполняться копирование.
Синтаксис этой функции выглядит так:
Все символы исходного массива будут скопированы в целевой, включая
символ-терминатор \0. К элементам, расположенным после символа
терминатора, также будет добавлен символ \0. Это гарантирует, что
остатки более длинной строки окажутся удалены при копировании
в массив более короткой строки.
Вторая функция копирования строк имеет похожее название —
strncpy().
Она используется точно так же, как и функция strcpy(), но
принимает
третий аргумент, позволяющий указать, сколько символов следует
скопировать. Её синтаксис выглядит так:
strcpy(dst, src, size_t count);
Где size_t count - целое число - количество символов
строки-источника
начиная с первого символа.
Копируемая строка начнется с первого символа исходного массива, но
закончится в позиции, указанной в третьем аргументе — финальный
символ \0 не будет скопирован автоматически.
После того, как символы будут скопированы, в конец целевого массива
запишется символ \0, что поднимет его статус до строки.
Поэтому необходимо убедиться, что целевой массив всегда будет на один
элемент больше, чем количество символов, которое будет в него скопировано,
включая символы пробелов, чтобы разместить символ-терминатор \0.
Напишем программу strcpy.c и продемонстрируем работу выше описанных
функций:
#include <stdio.h>
#include <string.h>
intmain(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;
}
Теперь в блоке функции main() скопируйте всё содержимое
второго
массива в первый, напечатайте его содержимое и отобразите длину и размер:
#include <stdio.h>
#include <string.h>
intmain(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;
}
Скопируем в первый массив первые пять символов,
содержащихся во втором массиве, далее поставьте символ-терминатор
\0:
#include <stdio.h>
#include <string.h>
intmain(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], поскольку нумерация
индексов массива начинается с нуля, а не с единицы.
Выполните программу:
Обратите внимание на то, что размер первой строки на протяжении
выполнения программы не поменялся.
Чтобы увидеть все коды символов, находящиеся в первой строке, напишите
цикл с помощью которого "пройдёте"
по
каждому элементу массива и напечатаете в консоль их значения и символы так:
Обратите внимание на то, что размеры массивов
можно не указывать если они сразу инициализируются. Для массивов,
которые будут инициализированы позже, например,
пользователем или при копировании из других
массивов, размер необходимо указать заранее.
Объединение строк
Объединение двух строк в одну имеет более точное название —
конкатенация.
Стандартный библиотечный заголовочный файл <string.h>
содержит две
функции, которые могут быть использованы для конкатенации строк.
Чтобы эти функции были доступны, в программе необходимо добавить
заголовочный файл <string.h> с помощью директивы
#include,
расположенной в начале программы.
Первая функция конкатенации строк называется strcat(), в
качестве
аргументов она требует имена двух строк, которые требуется объединить.
Строка, идущая второй в списке аргументов, будет добавлена в конец
строки, идущей первой, затем функция вернёт сконкатенированную
первую строку. Синтаксис этой функции выглядит так:
restrict - квалификатор указателя в C (начиная с C99),
с помощью которого программист говорит компилятору: “В этом участке кода я
обещаю, что через этот указатель
доступ к памяти будет происходить эксклюзивно, и
нет других указателей, через которые будет производиться доступ к этой же
памяти.”
Это подсказка компилятору для оптимизации кода.
dest - Строка к которой присоединяется строка
src.
src - Строка которую присоединяют к строке dest.
Необходимо помнить, что массив, хранящий первую строку, должен
быть достаточного размера, чтобы разместить все символы объединённой
строки, что позволит избежать ошибок конкатенации.
Вторая функция конкатенации строк имеет похожее название —
strncat(). Она используется точно так же, как и функция
strcat(),
но принимает третий аргумент, позволяющий указать, сколько символов
второй строки будет добавлено к первой. Синтаксис выглядит так:
count - количество символов второй строки src,
которое будет добавлено к первой dest.
Присоединяемая часть первой dest строки по умолчанию начнется с
первого
символа второй src строки и закончится в позиции,
указанной в третьем аргументе count.
Но поскольку имя строки является указателем на первый
символ, с помощью арифметики указателей можно указать другую
позицию первого копируемого символа. Синтаксис будет выглядеть так:
Опять же, как и в случае с функцией strcat(), важно, чтобы первый
массив был достаточно большим, чтобы разместить все символы
объединённой строки, что позволит избежать ошибок при использовании
функции strncat().
Эти функции изменяют длину исходной строки,
поскольку они добавляют символы.
Напишем программу strcat.c
#include <stdio.h>
#include <string.h>
intmain(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;
}
Далее в блоке функции main() присоедините вторую строку к
первой
и выведите результат в консоль:
#include <stdio.h>
#include <string.h>
intmain(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;
}
Присоедините первые 17 символов четвертой строки к третьей
и выведите результат в консоль:
#include <stdio.h>
#include <string.h>
intmain(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;
}
Присоедините последние 14 символов четвертой строки к третьей
и выведите результат в консоль:
#include <stdio.h>
#include <string.h>
intmain(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;
}
Выполните программу:
Напоминаем, Удлиняемая строка должна иметь размер, достаточный
для того, чтобы разместить все символы объединённой строки.
Поиск подстрок
Существует возможность выполнить поиск по строке, чтобы
определить, содержит ли она определенную последовательность символов
( подстроку ). Это допустимо сделать с помощью функции
strstr().
Она является частью стандартного заголовочного файла
<string.h>, который
необходимо добавить с помощью директивы #include,
расположенной в начале программы.
Функция strstr() принимает два аргумента. Первый из них
представляет собой строку, в которой выполняется поиск, а второй — подстроку,
которую следует найти. Если подстрока не найдена, функция вернёт
значение NULL. Если подстрока найдена, функция вернёт указатель на
первый символ первого включения подстроки.
Внимание!
Функция strstr() прекращает поиск, когда встречает
первое включение подстроки. Последующие включения
этой подстроки не будут найдены.
Номер элемента, содержащего первый символ подстроки, легко
определить с помощью арифметики указателей. Вычтя адрес первого символа
подстроки, возвращенного функцией strstr(), из адреса строки, в
которой выполнялся поиск (на который указывает имя массива), можно
получить целое число. Это число является индексом первого
символа подстроки внутри строки, в которой выполнялся поиск.
В программировании на языке C операции сравнения == и != могут
быть
использованы для того, чтобы сравнить результат со значением NULL,
но их нельзя использовать для сравнения самих строк. В стандартном
библиотечном заголовочном файле <string.h>
существует функция strcmp(), которая позволяет выполнять
сравнение строк.
Она принимает два аргумента, которые являются строками, подлежащими сравнению.
Сравнение выполняется на основе значений числовых кодов ASCII каждого символа
и их
позиции. Если строки абсолютно одинаковы, включая регистр, функция
strcmp() вернёт значение 0. В противном случае она вернёт
положительное
или отрицательное значение в зависимости от значений строк.
Напишем программу strstr.c
#include <stdio.h>
#include <string.h>
intmain(void)
{
char str[]="no Time like Present ";
char sub_str[]="Time";
return 0;
}
Выведите сообщение в консоль в случае, если вторая строка не будет найдена
в первой:
#include <stdio.h>
#include <string.h>
intmain(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;
}
Добавьте утверждения, позволяющие вывести адрес в
памяти и индекс первого символа подстроки:
#include <stdio.h>
#include <string.h>
intmain(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;
}
Обратите внимание на то,
что символ \ в этом примере
используется как экранирующий символ для двойных кавычек " ",
что позволяет избежать преждевременного прерывания выводимых
строк.
Напечатаем результат трёх операций сравнения, проведённых со второй
строкой:
#include <stdio.h>
#include <string.h>
intmain(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;
}
Выполните программу:
Поиск символа в строке
В заголовочном файле <string.h> имеются также две
функции, позволяющие выполнять поиск символов.
Функция strchr() ищет первое вхождение символа,
а функция strrchr() — последнее.
Обе возвращают значение NULL в случае, если символ не найден.
Обратите внимание - с помощью стандартных заголовочных файлов,
используемые функции заменяют вам написание циклов работы со строками.
Такие функции максимально оптимизированные для выполнения конкретной задачи
и выполняются
процессором очень быстро. По этому - изучайте функции стандартных
заголовочных файлов
с целью написания эффективного кода.
Напишем программу str_ch.c
#include <stdio.h>
#include <string.h>
intmain(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;
}
#include <stdio.h>
#include <string.h>
intmain(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;
}
Выполните программу:
Валидация строк
В стандартном библиотечном заголовочном файле <ctype.h>
содержится несколько функций, позволяющих выполнять проверки символов.
Чтобы эти функции были доступны, в программе необходимо
добавить заголовочный файл <ctype.h> с помощью директивы
#include, расположенной в начале программы.
В заголовочном файле <ctype.h> содержатся функции,
которые проверяют, является ли символ:
isalnum() - буквой алфавита или десятичной цифрой
isalpha() - буквой алфавита
islower() - в нижнем регистре
isupper() - в верхнем регистре
isdigit() - десятичной цифрой
isxdigit() - шестнадцатеричной цифрой
iscntrl() - управляющим
isgraph() - графическим
isspace() - пробелом
isblank() - пустым
isprint() - печатаемым
ispunct() - пунктуационным
В заголовочном файле <ctype.h> содержатся функции,
позволяющие перевести символ:
toupper() - в верхний регистр
tolower() - в нижний регистр
Каждая проверочная функция возвращает ненулевое значение (необязательно 1),
когда символ успешно проходит проверку, но всегда
возвращает 0, если символ проверку не прошёл.
Эти проверочные функции могут быть использованы для валидации
строк, введённых пользователем, путём прохода по их символам в цикле
и проверкой каждого из них. Если один из символов не подходит
требуемым условиям, устанавливается значение переменной-флага, на основе
которой пользователю рекомендуется исправить введённые данные.
Напишем программу isval.c в которой продемонстрируем функции
валидации строк:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
intmain(void)
{
char str[7]; /* Объявим массив символов, зарезервируем память */int i; /* Объявим целое число */int flag = 1; /* Объявим и инициализируем флаг единицей - true */return 0;
}
Запросите у пользователя ввести строку, затем присвойте её
переменной-массиву:
#include <stdio.h>
#include <string.h>
intmain(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;
}
Теперь, если условие выполняется, добавьте цикл, позволяющий
проверить каждый символ массива:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
intmain(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;
}
Внутри блока цикла for измените значение переменной-флага на
false
(0), если какой-либо символ не является цифрой:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
intmain(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;
}
В блоке if(!isdigit(str[i])) проверьте свойства
символа,
не являющийся цифрой и напечатайте сообщение об этом в консоль:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
intmain(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;
}
После блока цикла for выведите сообщение, описывающее состояние
переменной-флага
с помощью тернарного условия:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
intmain(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;
}
Выполните программу:
Почему в проверочном утверждении в цикле вычтена единица из
возвращённого значения размера строки?
Стандартный библиотечный файл <stdlib.h> содержит
полезную функцию, которая называется atoi().
Она может быть использована для преобразования строки в число (althabet
tointeger,
алфавит-к-целому-числу).
Чтобы эта функция была доступна, в программе необходимо добавить
заголовочный файл <stdlib.h> с помощью директивы
#include,
расположенной в начале программы.
Функция atoi()
принимает в качестве своего единственного аргумента строку, которую
нужно преобразовать. Если строка пустая или её первый символ не
является числом или знаком «минус», функция atoi() вернёт
значение 0.
В противном случае строка (если в начале есть цифры) будет преобразовываться к
числу, пока функция atoi() не встретит в строке не являющийся
числом символ. Если функция atoi() встретит символ,
не являющийся числом, она вернёт уже преобразованные в число цифры.
Также существует функция itoa() (integer-to-alpha,
целое-число-к-алфавиту), которая используется для преобразования
значения, имеющего тип int, к строке.
Эта функция широко используется в различных компиляторах,
однако она не является частью стандартной спецификации ANSI C.
Функция itoa() принимает три аргумента.
Первый — преобразовываемое число, Второй — строка,
которой будет присвоен сконвертированный результат,
Третий — основание, которое будет использовано
при преобразовании. Например, если указать основание 2, строке
присвоится двоичный эквивалент указанного числа.
В стандарте ANSI C существует аналог функции itoa(), который
называется sprintf() и располагается в заголовочном файле
<stdio.h>.
Эта функция проще, поскольку нельзя указать основание
для преобразования. Функция sprintf() принимает три
аргумента, Первый — строка, которой будет присвоено число, Второй -
спецификатор
формата и Третий - число, которое необходимо преобразовать. Эта функция
возвращает число, которое является количеством символов в преобразованной
строке, и возвращает строку преобразованную в формат печати сохраняя результат
в первый аргумент(изменяет
его).
Напишем программу convers.c, для демонстрации выше описанных
функций:
#include <stdio.h>
#include <stdlib.h>
intmain(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;
}
Далее, преобразуем каждую строку к целому числу и напечатаем результат
в консоль:
#include <stdio.h>
#include <stdlib.h>
intmain(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;
}
Далее в блоке функции main() преобразуйте первую числовую
переменную к строке, переведя её в двоичную систему счисления с
использованием нестандартной функции, а затем выведите результат в консоль:
#include <stdio.h>
#include <stdlib.h>
intmain(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;
}
Далее преобразуйте первую числовую переменную к строке,
переведя её в восьмеричную и шестнадцатеричную форматы печати
с использованием стандартной библиотечной функции, и
сохраните длину каждой строки:
Выполните программу:
В отличие от функции itoa()
функция sprintf() не может
преобразовывать числа в двоичную систему
счисления, поскольку не существует спецификатора формата
для двоичных чисел.
Функция itoa() очень полезна, но, поскольку она не
является частью стандарта
ANSI, её поддерживают не
все компиляторы. Компилятор GNU GCC 15.1.0,
используемый в этом учебнике, эту функцию поддерживает.
Заключение
Программируя на языке C, строка является массивом символов,
в конце которого размещается специальный символ-терминатор \0.
Каждый символ имеет свой числовой код ASCII.
Имя массива символов действует как указатель на всю строку.
Функция scanf() прекращает считывать пользовательский ввод
после того, как встретит первый пробел, но функция fgets()
может
работать с пробелами и добавляет символ-терминатор \0 автоматически.
Функция puts() выводит на экран строку, переданную ей в
качестве
аргумента, и автоматически добавляет символ перевода каретки \n.
В стандартном заголовочном библиотечном файле
<string.h>
содержатся специальные функции работы со строками, такие как
strlen(),
которая возвращает длину заданной строки, или strcpy() и
strncpy(),
которые копируют строки.
В заголовочном файле <string.h> также содержатся
функции strcat() и strncat(),
которые могут использоваться для конкатенации строк.
В дополнение, в файле <string.h> имеется
функция strstr(), которая ищет в строке заданную подстроку,
и функция strcmp(), которая выполняет сравнение двух заданных
строк.
Если функция strstr() не может найти заданную подстроку,
она возвращает значение NULL.
В стандартном библиотечном файле <ctype.h> содержатся
функции,
позволяющие проверять типы символов, например
isalpha(), isdigit()
и ispunct().
В заголовочном файле <ctype.h> также имеются
функции islower(), isupper() и
tolower(), toupper(),
которые проверяют и изменяют регистр символов.
В стандартном библиотечном заголовочном файле
<stdlib.h>
существует функция atoi(), которая преобразует строку к числу.
В стандартном библиотечном заголовочном файле
<stdlib.h>
существует функция sprintf(), которая преобразует число к
строке.
Более могущественная нестандартная функция itoa().
Создание Структур
Здесь вы познакомитесь со структурами
и абстрактными типами данных 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;
Начните новую программу struct.c с инструкции препроцессора,
позволяющей включить функции стандартной библиотеки ввода/вывода,
до главной функции объявим структуру данных, два члена в ней и один тег:
#include <stdio.h>
struct coords /* Объявили структуру данных */
{
int x; /* Член структуры */int y; /* Член структуры */
} point; /* Тег - Экземпляр структуры с именем */intmain(void)
{
return 0;
}
Далее создайте ещё один экземпляр структуры:
#include <stdio.h>
struct coords
{
int x;
int y;
} point;
struct coords top; /* Тег - Экземпляр структуры с именем */intmain(void)
{
return 0;
}
В главной функции, инициализируем оба члена каждой структуры:
#include <stdio.h>
struct coords
{
int x;
int y;
} point;
struct coords top;
intmain(void)
{
point.x = 5; /* Доступ к члену структуры через точку */point.y = 8; /* Доступ к члену структуры через точку */top.x = 15; /* Доступ к члену структуры через точку */top.y = 24; /* Доступ к члену структуры через точку */return 0;
}
Теперь напечатайте значения, хранящиеся в каждом члене структуры в
консоль:
#include <stdio.h>
struct coords
{
int x;
int y;
} point;
struct coords top;
intmain(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;
}
Выполним программу:
Мы объявили структуру и инициализировали два её экземпляра. Доступ к
членам структуры получили через
оператор (.) - точка. Создав структуру - мы объединили несколько
переменных в группу, что стало
представлять собой единый тип данных.
Можно представить, что мы создали экземпляры структур описывающих некие
точки point и top
для каких либо геометрических расчётов или чего-то ещё:
Точки на координатной сетке.
Разные типы данных в одной
Структуре
#include <stdio.h>
intmain(void)
{
struct myStruct
{
int x; /* Один тип */double y; /* Другой тип */
};
return 0;
}
На блок-схеме Структура обозначается фигурой Структура
Определяем
объект Структуры в программе, как
переменную s
#include <stdio.h>
intmain(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}
Просто компактная запись.
Инициализация членов структуры
С целью увеличить читаемость, прозрачность, понятность исходного кода(свойство
исходного кода называемое
само-документируемый код), можно инициализировать члены структуры
записью, при которой
мы явно указываем, какие члены инициализируем, через их имена.
Удобно и правильно объявить несколько объектов структуры сразу при объявлении
самой структуры в едином блоке
кода через запятую:
#include <stdio.h>
intmain(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>
intmain(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 - Ключевое слово языка С
"типа"(type)"определителя"(define)
struct - Ключевое слово языка С Объявления
Структуры(structure)
userStruct - Пользовательское Имя Структуры, если его не
записать - Такая структура называется
безымянной
userStType - Пользовательское Имя Типа данных определённое
Структурой
Теперь, зная как записать определение типа данных с помощью Структуры
используя Ключевое слово
typedef, напишем программу typedef.c, и продемонстрируем
все выше описанные принципы:
Объявим структуру, её члены и Имя пользовательской структуры:
#include <stdio.h>
typedef struct /* Объявили безымянную структуру */
{
int x; /* Объявили член структуры */int y; /* Объявили член структуры */
} Point; /* Объявили Тег - Имя типа данных пользовательской структуры */intmain(void)
{
return 0;
}
Обратите внимание на то, что
структура может быть безымянной, но она обязательно должна
иметь Тег - Имя типа данных
пользовательской структуры.
Далее создайте две переменные определённого структурой типа
данных, в одной из которых будут проинициализированные оба
члена структуры:
#include <stdio.h>
typedef struct
{
int x;
int y;
} Point;
Point top = {15, 24}; /* Объявили и Инициализировали оба члена переменной */
Point bottom; /* Объявили переменную */intmain(void)
{
return 0;
}
В главной функции инициализируем оба члена второй переменной
bottom и напечатаем значения
обоих переменных,
представляющих координаты точек:
#include <stdio.h>
typedef struct
{
int x;
int y;
} Point;
Point top = {15, 24};
Point bottom;
intmain(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;
}
Перед блоком функции 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 */intmain(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
Вернитесь в блок функции 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}; /* Объявили и Инициализировали переменную */intmain(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;
}
Выполните программу:
Члены структур могут быть
инициализированы с помощью списка, разделённого
запятыми, только при объявлении переменных — далее
их можно инициализировать только индивидуально.
Использование указателей в
Структурах
Использовать указатель на символ, находящийся в структуре, в
качестве контейнера для строки порой более выгодно, чем использовать для
этих же целей массив символов.
Целая строка может быть присвоена массиву символов только при его
объявлении. Далее в программе массиву символов разрешается
присвоить строки только последовательно, символ за символом, с помощью
операции =.
При каждом присваивании значение, расположенное слева от операции
=, называется L-значением, оно представляет фрагмент памяти.
Значение, расположенное справа от операции =, называется
R-значением,
оно представляет собой данные, которые следует поместить в этот фрагмент.
L-значения являются объектами, а R-значения —
данными.
В программировании на языке C существует важное правило,
согласно которому R-значение не может располагаться слева от операции =
присваивания.
В то время как , L-значение способно располагаться с любой стороны операции
= присваивания.
Т.о. объект может находится с любой стороны, а данные могут
находится только справа
от операции = присваивания.
Каждый отдельный элемент массива символов является L-значением,
ему может быть присвоен отдельный символ. Указатель на символ
также является L-значением, которому после объявления может быть
присвоена целая строка.
Напишем программу string_member.c, объявим безымянный
пользовательский тип данных структуры, в
которой
объявим смвольный массив размером в пять символов. Дадим название типу
данных(тег):
Определение
пользовательского типа данных с помощью typedef и задания Имени
этому типу
Ранее мы изучили синтаксис записи объявления пользовательского типа данных
определяемых структурой:
typedef struct [userStruct] userStType;
В квадратных скобках как раз показано, что имя структуры опционально - можно
записать объявление и без него,
тогда такое определение является безымянным. При его наличии объявление
становится именованным.
Напишем программу typedef_name.c, внутри главной функции объявим
прототип структуры, и сразу же за
ним напишем её определение:
#include <stdio.h>
intmain(void)
{
typedef struct myStruct mySt; /* Объявляем прототип структуры, описывая пользовательский тип данных */
struct myStruct /* Определяем пользовательский тип данных */
{
int X; /* Объявляем член структуры */
double Y; /* Объявляем член структуры */
};
mySt S; /* Объявляем переменную с именем S пользовательского типа данных с именем mySt */return 0;
}
Далее в программе мы можем создавать переменные типа mySt,
также легко как мы привыкли
создавать переменные стандартных типов данных языка С.
Использовав ключевое слово typedef - мы сокращаем количество
текста для создания переменных
пользовательского типа.
Без ключевого слова typedef - мы вынуждены либо объявить все
используемые переменные сразу при
Определении структуры,
либо постоянно писать одну и ту же структуру и в ней снова объявлять
переменную.
Создание типов данных с помощью ключевого слова typedef
широко распространено в исходном коде
на языке С.
Инициализация переменных
пользовательского типа данных определяемых Структурой
Напишем программу typedef_var_init.c на основе программы выше
инициализируем переменную
пользовательского типа:
#include <stdio.h>
intmain(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;
}
Выполните программу:
Указатели на Структуры
Указатели на типы данных, объявленные с помощью структур, могут
быть созданы точно так же, как и указатели на стандартные типы данных.
В этом случае указатель хранит адрес начала фрагмента памяти,
используемого для хранения данных членов структуры.
Использование указателей на структуры настолько типично в
программах, написанных на языке C, что для этого была введена специальная
операция. При работе с указателями на структуру операция (.)(точка)
может
быть заменена операцией ->, представляющим собой дефис, за которым
следует знак «больше». Эта комбинация называется «стрелкой».
Например, указатель на член структуры pntr->member
является эквивалентом
записи (*pntr).member
Напишем программу struct_pntr.c. Определим безымянную структуру
пользовательского типа данных, и
объявим два члена структуры:
Теперь добавьте в функцию main(), объявления трёх переменных
и переменную-указатель.
Все они имеют вновь созданный пользовательский тип данных,
определённый структурой, т.е. тип данных - City:
#include <stdio.h>
typedef struct
{
char *name;
char *popn;
} City;
intmain(void)
{
City ny;
City la;
City ch;
City *pntr;
return 0;
}
Далее, внутри блока функции main() скрупулёзно присвойте
строковые значения
всем членам первой переменной(ny), затем получите доступ к
хранящимся в ней
значениям с помощью операции (.)(точка), чтобы сначала сохранить, а затем
получить значения:
#include <stdio.h>
typedef struct
{
char *name;
char *popn;
} City;
intmain(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;
}
Присвойте адрес второй переменной (la) указателю:
#include <stdio.h>
typedef struct
{
char *name;
char *popn;
} City;
intmain(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;
}
Теперь присвойте значения членам второй переменной, а затем
напечатайте в консоль сохранённые значения с помощью операции
->(стрелка),
позволяющей сохранить значения, и операции ->(стрелка),
позволяющей получить эти значения:
#include <stdio.h>
typedef struct
{
char *name;
char *popn;
} City;
intmain(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;
}
Теперь присвойте значения членам третей переменной(ch), а
затем
напечатайте в консоль сохранённые значения с помощью операции
->(стрелка),
позволяющей сохранить значения, и операции ->(стрелка),
позволяющей получить эти значения:
Выполните программу:
Значения, представляющие собой население городов,
присваиваются как строки, что позволяет использовать
в качестве разделителя разрядов запятые.
Операция ->(стрелка) может помочь писать более
понятный исходный код при разграничении
указателей на структуры
и указателей не на структуры.
Передача Структур в функции
Члены структур могут быть сохранены в массиве, как и значения
любого другого типа данных. Массив объявляется как обычно, но метод
присваивания значений его элементам несколько отличается. Каждый
разделённый запятыми список значений-членов должен быть помещён
в фигурные скобки.
Аналогично, структура может быть передана в функцию в качестве
аргумента, как и любая другая переменная. Тип данных структуры
должен быть указан как в прототипе функции, так и в её описании, наряду
с именем переменной, с помощью которого можно адресовать
значения структуры.
Простое присваивание всех
значений с помощью разделённого запятыми списка не
сработает — каждый набор
значений должен быть помещён в свою пару фигурных { } скобок.
Но помните, что если вы передадите данные по значению с помощью
обычной переменной, функция будет работать с копией структуры,
а её оригинальные значения останутся неизменными.
C другой стороны, передача структуры по ссылке с помощью переменной-указателя
означает, что функция будет работать с исходными членами
структуры, что изменит их по завершении её работы.
Напишем программу struct_to_func.c, объявите безымянную структуру,
определяющую тип данных
и имеющую два члена и один тег:
Добавьте прототип функции, в которую в качестве аргументов
будут передаваться переменная, имеющая тип структуры, и указатель
на тип, определяемый структурой:
Далее в блоке определения функции напечатайте значения первого элемента
списка,
потом измените значения членов переданной
структуры-копии(по значению), а затем выведите новые значения в консоль:
Далее измените члены оригинальной структуры(по указателю на данные в
памяти машины)
и выведите новые значения в консоль:
#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);
intmain(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); /* Проверка оригинального значения */
}
Выполните программу:
Передача крупных структур по значению неэффективна,
поскольку в памяти создаётся копия целой структуры,
а при копировании по ссылке требуется выделить
объём памяти, равный размеру одного указателя.
Группирование данных в Объединение
В программировании на языке C объединение позволяет хранить
различные фрагменты любого типа данных в одном участке памяти
в течение работы программы — присвоение значения объединению
перезапишет данные, которые хранились там ранее.
Это позволяет использовать память более эффективно.
Объединения объявляют с помощью ключевого слова union.
Во время выполнения
программы члены объединения могут получать значения только
индивидуально.
Массив объединений можно создать точно так же, как и массив
структур в предыдущем примере, но члены объединений могут быть
инициализированы только при объявлении, если все они имеют
одинаковый тип данных. Указатель на объединение создаётся точно так же, как
и указатель на структуру. Также объединение можно передать в
функцию в качестве аргумента.
Напишем программу unions.c, в которой покажем разницу между
инициализацией Структуры и Объединения.
Объявим безымянную Структуру, определяющую пользовательский тип данных и
имеющую три члена и один тег.
Объявим безымянное Объединение, определяющее пользовательский тип данных и
имеющее три члена и один тег:
#include <stdio.h>
typedef struct
{
int number;
char letter;
*string;
} Distinct;
typedef union
{
int number;
char letter;
*string;
} Unified;
intmain(void)
{
return 0;
}
Теперь внутри блока функции main() объявите переменную типа
данных, определяемого Структурой и инициализируйте все её члены:
Именно программист несёт ответственность за
понимание того, какие типы данных хранятся в объединении
в любой момент выполнения программы.
Объединения наиболее полезны, если вам приходится
работать в условиях ограниченной памяти.
Сравнительные выводы по
struct и union
struct(структура) — это составной тип данных.
Хранит несколько переменных разного типа одновременно.
Каждая переменная (член структуры) занимает свою память.
Размер структуры равен сумме размеров всех её членов(с учётом
выравнивания).
Все члены структуры хранятся одновременно и независимо.
union(объединение) — это составной тип данных.
Все члены объединения используют одну и ту же область памяти.
Размер union равен размеру самого большого члена.
В один момент времени корректно использовать только один член
union.
Используется, когда нужно хранить одно из нескольких возможных значений,
значительно экономя память.
Перепишем код программы. Если допустить ошибку при написании кода для работы с
Объединением, и инициализировать
сразу каждый член Объединения, то произойдёт следующее:
В консоли выведены значения Объединения не соответствующие нашим ожиданиям.
Потому что последнее значение,
которое мы инициализировали, изменило содержимое Объединения.
Выделение памяти
Стандартная библиотека заголовочных файлов <stdlib.h>
предоставляет функции управления памятью, с помощью которых программа может
явно запросить память во время выполнения.
Функция malloc() принимает один аргумент,
позволяющий определить, сколько байт памяти требуется выделить.
Функция malloc() сохраняет значения, лежащие в памяти
до вызова этой функции. В случае успеха
возвращает указатель на начало блока памяти. В случае неудачи
возвращает значение NULL.
Функция calloc() требует наличия двух аргументов,
которые будут перемножены между собой с целью определения объёма
памяти, который следует выделить. В случае успеха
возвращает указатель на начало блока памяти. В случае неудачи
возвращает значение NULL. Функция calloc() заполняет
всё выделенное пространство памяти нулями.
Объём памяти, выделенный с помощью функций malloc() и
calloc(), может быть увеличен с помощью функции
realloc(). Эта функция требует указатель на выделенный
блок памяти в качестве первого аргумента и число,
обозначающее размер нового блока, в качестве второго аргумента функции.
Она возвращает указатель на начало увеличенного блока в случае успеха
и значение NULL в случае неудачи.
Для высвобождения ранее запрошенного объёма памяти у машины
с помощью функций malloc() и calloc(), используется
функция free(), принимающая в качестве аргумента указатель
на начало выделенного блока памяти.
Добавьте в функцию main(), объявление целочисленной
переменной и целочисленный указатель:
#include <stdio.h>
#include <stdlib.h>
intmain(void)
{
int size;
int *memory;
return 0;
}
Запросите память в объёме, необходимом для размещения сотни целых чисел:
#include <stdio.h>
#include <stdlib.h>
intmain(void)
{
int size;
int *memory;
memory = malloc(100 * sizeof(int)); /* Указатель будет инициализирован адресом
начала области выделенной в памяти */return 0;
}
Далее выведите информацию о выделенном блоке памяти, а в случаи
ошибки выделения памяти - сообщение, гласящее, что запрос не сработал:
#include <stdio.h>
#include <stdlib.h>
intmain(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> .
Теперь попытайтесь увеличить объём выделенного блока памяти
и вывести информацию о нём или же сообщение, гласящее, что
запрос не сработал:
В конце блока функции main() освободим выделенную
память:
#include <stdio.h>
#include <stdlib.h>
intmain(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;
}
Выполните программу:
Несмотря на то, что тип int,
как правило, занимает 4 байта, хорошим тоном является
использование операции
sizeof(), чтобы определить
точный размер блока памяти
на случай, если переменные типа int имеют другой
размер на данной машине.
Добавьте в программу цикл выводящий в консоль значения
каждого целого числа из области выделенной памяти - т.е. отобразите все 200
целых чисел.
Заключение
Структура способна содержать одну или несколько переменных
любого типа данных, которые называются членами структуры.
К каждому члену структуры можно обратиться, приписав его имя
к тегу с помощью операции(.) - точка.
Последующие экземпляры структуры унаследуют свойства-члены
оригинальной структуры, от которой они образованы.
Перед ключевым словом struct может располагаться ключевое
слово
typedef, указывающее, что структура является типом данных.
Хорошим приёмом программирования является написание Тегов
с большой буквы, что позволит проще разграничивать в тексте исходного кода
определённые структурами типы данных.
При объявлении переменной, имеющей тип, определяемый
структурой, можно инициализировать каждый член структуры,
присвоив переменной разделённый запятыми список значений внутри фигурных
{ } скобок.
Указатель на структуру хранит адрес в памяти начала фрагмента
памяти, задействованного для хранения данных членов структуры.
При работе с указателем на структуру операция . - точка, может быть
заменена операцией -> - стрелка.
Члены массива структур могут быть опционально
инициализированы при объявлении, но каждый список значений должен
размещаться в фигурных { } скобках.
Структура может быть передана в функцию в качестве аргумента,
как и любая другая переменная.
Передача структуры в функцию через переменную означает, что
функция будет работать с копией членов структуры. Передача
структуры в функцию с помощью указателя означает, что
последняя будет работать с оригинальными значениями.
Объединение очень похоже на структуру — в нём так же хранятся
разные данные, однако они лежат в одном фрагменте памяти во время
работы программы - фрагмент имеет размер наибольшего из членов объединения.
Стандартный библиотечный заголовочный файл <stdlib.h>
предоставляет заголовочные функции malloc(), calloc() и
realloc(), которые позволяют выделить память,
и функцию free(), предназначенную для освобождения
выделенной памяти.
Работа с Файлами
Здесь показывается, как можно использовать создание, чтение и
запись
файлов, работать с: системным временем и датой, случайными числами; как
создать простейшее диалоговое окно
ОС 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.
Напишем программу newfile.c, в которой изучим как файл создать:
Далее в блоке функции main() попробуйте создать файл с
режимом Записи:
#include <stdio.h>
intmain(void)
{
FILE *file_pntr;
file_pntr = fopen("data.txt", "w"); /* Инициализируем Указатель именем файла,
ставим файл в режим Записи данных */return 0;
}
Обратите внимание на то,
что имя файла и режим
его открытия должны быть
заключены в двойные " " кавычки.
Теперь выведите сообщение, подтверждающее успешность
попытки создания файла, а затем закройте файл:
#include <stdio.h>
intmain(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;
}
Выведите альтернативное сообщение, если попытка была безуспешной:
Обратите внимание на то,
что эта программа возвращает значение 1 в случае
неудачной попытки открытия файла — это говорит
системе, что все прошло не
очень гладко.
До выполнения программы, в папке текстовый файл data.txt
отсутствует:
После выполнения программы, в папке создан текстовый файл
data.txt:
Проверить содержимое файла 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,
который
используется для вывода сообщений об ошибках.
Функция fputc() может использоваться для записи в файловый
поток одного символа за раз — обычно путём прохода в цикле по
массиву символов. её компаньон fgetc() может использоваться
для
чтения из файлового потока по одному символу за раз.
Функция fputs() может использоваться для записи в файловый
поток одной строки за раз, а её компаньон fgets() — для чтения
одной
строки за раз.
Функция fread() может использоваться для чтения файлового
потока полностью, а функция fwrite() — для записи файлового
потока
полностью.
Функции fscanf() и fprintf() могут
использоваться
для чтения и записи в файловые потоки строк и чисел.
Напишем программу writechars.c, в которой будет записывать в файл
символы:
#include <stdio.h>
intmain(void)
{
FILE *file_pntr; /* Объявляем Указатель на Файл */int number; /* Объявляем целое число */
char text[50] = "Text, one character at a time."; /* Объявляем Символьный литерал и
инициализируем его текстовой строкой */return 0;
}
Далее в блоке функции main() попробуйте создать файл для
Записи в него:
#include <stdio.h>
intmain(void)
{
FILE *file_pntr;
int number;
char text[50] = "Text, one character at a time.";
file_pntr = fopen("chars.txt", "w"); /* Инициализируем Указатель на Файл,
передаём имя для открытия файла, и ставим режим Записи данных для файла */return 0;
}
Теперь выведите сообщение, подтверждающее успешность попытки
создания файла. С помощью цикла for запишите в файл каждый символ
массива, а затем
закройте файл и верните значение 0, как того требует объявление
функции:
#include <stdio.h>
intmain(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 и цикл завершится точно пройдя все символы строки до
её конца.
Выведите альтернативное сообщение, если попытка создания или открытия
файла была безуспешной:
#include <stdio.h>
intmain(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;
}
Выполните программу:
Откройте текстовый файл в Visual Studio Code и просмотрите его содержимое:
Чтение и запись строк
Использование функции fgetc() — не самый эффективный способ
считывания текста в программе, поскольку эту функцию необходимо
вызывать большое количество раз подряд, чтобы считать все
символы. Лучше использовать функцию fgets(), которая считывает
текст по
одной строке за раз.
Функция fgets() возвращает указатель на char (т.е.
строку — массив символов).
Принимает три аргумента:
- Первый аргумент определяет
символьный указатель или массив символов, куда будет записан текст. Где:
char*
- указатель на char, куда будет
записана строка.
restrict
- компилятору сообщается, что в этом
участке кода доступ к этой памяти
происходит только через этот указатель (для оптимизации).
int count
- Второй аргумент — Целое число, которое
определяет максимальное
количество символов в считываемой строке.
FILE* restrict stream
- Третий аргумент — файловый
указатель, указывающий, из какого файла/потока следует производить чтение.
Аналогично, функция fputc(), которая записывает текст в файл по
одному символу за раз, менее эффективна, чем функция fputs(),
которая
записывает текст в файл по одной строке за раз.
Функция fputs():
int fputs( const char* restrict str, FILE* restrict stream );
Принимает два аргумента:
char* restrict str
- Первый аргумент определяет
символьный указатель или массив символов, куда будет записан текст.
FILE* restrict stream
- Второй аргумент — файловый
указатель, указывающий, из какого файла/потока следует производить чтение.
Функция fputs() добавляет символ новой строки \n всякий
раз,
когда записывает строку. Эта функция возвращает 0 в случае успеха или
константу EOF, когда происходит ошибка или функция достигает конца
файла.
Напишем программу text_lines.c, для обработки строк подключим
заголовочный файл
<string.h>:
Обратите внимание на Второй аргумент Функции fgets() в проверочном
утверждении цикла while
имеющий значение 50. Это значение определяет максимальное количество
символов которые будут Читаться
функцией за раз. И до тех пор пока возвращаемое функцией fgets()
значение символьного Указателя не
равно Нулевому Указателю, цикл будет выполнять итерацию за итерацией.
Внутри Функции fgets() скрыт
механизм перехода вперёд на соответствующее количество символов (Второй её
аргумент), как только Прочитаны
предыдущие. При этом согласно Арифметике указателей, Символьный Указатель
получает приращение и цикл
повторяется. Как только будет получен EOF - Цикл завершится. Кстати,
даже если строка слишком
длинная, она будет прочитана в несколько вызовов Функции fgets() -
это также неявный механизм
работающий внутри функции.
Внутри блока if, сразу после цикла while скопируйте новую
строку в массив, а затем добавьте
её в конец текстового файла:
#include <stdio.h>
#include <string.h>
intmain(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()
используется для присвоения новой строки массиву
символов.
В конце блока if закройте файл и верните ноль:
#include <stdio.h>
#include <string.h>
intmain(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;
}
В случаи если не удалось открыть текстовый файл
"farewell.txt" для чтения и записи, напечатаем
в консоль сообщение об этом и вернём единицу:
#include <stdio.h>
#include <string.h>
intmain(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;
}
До выполнения программы:
После выполнения программы:
Синтаксис записи пути к файлу
Пути бывают Абсолютные и Относительные
Абсолютный путь: Указывает полное расположение файла в файловой системе.
Синтаксис отличается, например:
Windows. Экранирование обратного слэша в Windows: В яззыке С обратный слэш
\ используется для управляющих
последовательностей (\n, \t, \\). Поэтому в путях нужно писать \\ вместо
\, например так : "C:\\Users\\Имяпользователя\\Documents\\farewell.txt".
Windows. Или использовать прямой слэш /, который также понимает Windows:
"C:/Users/Имяпользователя/Documents/farewell.txt"
Linux / macOS: "/home/username/Documents/farewell.txt"
Относительный путь: Указывается относительно папки, где запускается программа
(рабочей
директории). Синтаксис отличается, например:
"farewell.txt" // в той же папке, что exe
"data\\input.txt" // в подпапке data
"data/input.txt" // в подпапке data
"../config/settings.txt" // в папке на уровень выше
Проверка, где находится
текущая рабочая папка
В Windows при запуске программы рабочей директорией будет папка, где находится
.exe, если запускать
двойным
кликом, либо папка, из которой выполняется запуск через консоль. Можно узнать
текущую папку через C (POSIX)
функцией:
#include <unistd.h>
#include <stdio.h>
intmain() {
char cwd[1024]; /* Зарезервируем 1024 байта для строки */
getcwd(cwd, sizeof(cwd)); /* Функция getcwd - get current working directory*/printf("Current working directory: %s\n", cwd); /* Печать в консоль текущего пути */return 0;
}
Например так:
Считывание и запись файлов целиком
Файл может быть прочитан целиком с помощью функции fread(). Вот её
синтаксис:
buffer - Указатель на первый объект массива куда будет
производиться запись
size - Размер каждого объекта в байтах
count - Количество объектов для записи
stream - Указатель на выходной поток для записи в него
Итак, Первый из них — символьная переменная,
где текст может храниться. Во втором аргументе указывается размер
считываемых или записываемых за раз фрагментов текста — обычно
он равен 1. Третий аргумент позволяет указать общее количество
символов для чтения или записи, а четвёртый аргумент является
файловым указателем, указывающим на файл, с которым нужно работать.
Функция fread() возвращает количество считанных объектов,
в котором учитываются символы, пробелы переводы каретки.
Аналогично, функция fwrite() возвращает количество записанных
объектов.
Обе функции находятся в заголовочном файле <stdio.h>:
Напишем программу read_write_file.c:
#include <stdio.h>
intmain(void)
{
FILE *orignl_pntr; /* Объявим Указатель на оригинальный файл */
FILE *copy_pntr; /* Объявим Указатель на файл-копию */
char buffer[500]; /* Объявим массив символов на 500 штук (его имя есть Указатель на него) */int number; /* Объявим целочисленную переменную */return 0;
}
Создайте текстовый файл 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
Далее в блоке функции main() попробуйте открыть существующий
файл для чтения и другой файл для записи:
Внутри блока if считайте содержимое оригинального файла в
массив символов, подсчитывая каждый считанный объект,
а затем запишите содержимое массива во второй файл. Закройте каждый файл:
Используйте количество
объектов, возвращенное функцией fread() как
третий аргумент для функции
fwrite(), чтобы гарантировать,
что количество записанных
объектов совпадет с количеством считанных объектов.
В заключении блока if выведите сообщение, подтверждающее
успех операции, которое содержит количество объектов, и верните
значение 0:
Добавьте сообщение, если попытка была без успешной:
#include <stdio.h>
intmain(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 объектов. При этом все
последние объекты окажутся опущены.
Выполните программу:
При выполнении программы, результ количества скопированных объектов
равен 308.
Результат выполнения программы:
При выделении текста в файле, количество скопированных объектов равно
312.
А при выполнении программы скопированно 308 объектов. Почему?
Сканирование файловых потоков
Функция scanf(), использовавшаяся для получения данных, введённых
пользователем, является упрощенной версией функции fscanf(),
которая всегда выполняет чтение из потока stdin.
Функция fscanf() позволяет вам указать файловый поток для чтения
в качестве её первого
аргумента.
Также она имеет преимущество при чтении файлов, содержащих
только числа — числа в текстовом файле видны как простой набор
символов, но если они будут считаны функцией fscanf(), они
окажутся
преобразованы к их числовому типу.
Синтаксис:
int fscanf( FILE *restrict stream, const char *restrict format, ... );
Где:
stream - Файл входного потока для Чтения из него
buffer - Указатель на символ \0 конца строки, которую
Читают
format - Указатель на символ \0 конца строки, которая
определяет как Читать ввод
... - дополнительные аргументы функции
Возвращаемое значение:
Количество успешно назначенных принимающих аргументов.
Количество принимающих аргументов равно 0(нулю)
в случае, если ошибка сопоставления произошла до назначения первого
принимающего аргумента.
EOF, если ошибка ввода произошла до назначения первого принимающего
аргумента.
То же, что и (1-3), за исключением
того, что EOF также возвращается в случае нарушения ограничений
времени выполнения.
Функция printf(), использованная для вывода
информации на экран, является упрощенной версией функции
fprintf(),
которая всегда выполняет запись в поток stdout.
Функция fprintf() позволяет вам
указать файловый поток для записи в качестве её первого аргумента.
Синтаксис:
int fprintf( FILE* restrict stream, const char* restrict format, ... );
Где:
stream - Выходной Файл потока для Записи в него
buffer - Указатель на символьную строку, в которую Записывают
bufsz - до bufsz - 1 символов может быть
записано, и символ конца строки
\0
format - Указатель на символ \0 конца строки, которая
определяет как Интерпретировать
Данные
... - дополнительные аргументы функции
Возвращаемые значения:
Количество символов, которые были реально выведены в поток вывода.
Отрицательное значение (обычно EOF или -1) при ошибке вывода
или ошибке кодировки.
Функции fscanf() и fprintf() предлагают высокий
уровень гибкости,
позволяя выбрать поток, с которым будет производиться чтение или запись
Напишем программу fscanprint.c для демонстрации работы функций:
#include <stdio.h>
intmain(void)
{
FILE *nums_pntr; /* Объявляем Указатель на файл */
FILE *hint_pntr; /* Объявляем Указатель на файл */int nums[20]; /* Объявляем целочисленный массив */int i; /* Объявляем целое число */int j; /* Объявляем целое число */
}
Создайте текстовый файл nums.txt с таким содержимым:
1 2 3 4 5 6 7 8 9 10
#include <stdio.h>
intmain(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;
}
Применим функцию feof() проверяющую - достигнут ли конец
файла, переданного ей в качестве
аргумента. Функция возвращает 0(false) до тех пор пока не достигнут
конец файла, если конец файла
достигнут
возвращает не нулевое значение(true).
#include <stdio.h>
intmain(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;
}
#include <stdio.h>
intmain(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;
}
#include <stdio.h>
intmain(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;
}
#include <stdio.h>
intmain(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 (англ. standarderror -
стандартная ошибка) — это стандартный
поток ошибок в языке С и в ОС, обычно вывод осуществляется в
консоль(экран).
Применение:
Вывод сообщений об ошибках и диагностики отдельно от обычного вывода
данных (stdout).
Обеспечивает удобную фильтрацию и перенаправление потоков:
Можно перенаправить stdout в файл, оставив ошибки
в stderr видимыми в
консоли.
Упрощает отладку: ошибки видны даже при скрытом основном
выводе.
Выполните программу:
Мусорные значения
сгенерированные в примере выше
В первом цикле:
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++;
}
Теперь:
fscanf(nums_pntr, "%d", &nums[i]) возвращает:
1, если успешно прочитало одно число.
EOF или 0, если не удалось прочитать число (конец
файла или ошибка).
Цикл продолжается только пока успешно читаются числа:
Чтение → сохранение в nums[i] → увеличение i.
По завершении:
i будет равно количеству реально считанных чисел.
В массиве будут только корректные значения.
Дополнительно можно добавить защиту от переполнения массива так:
i = 0;
while (i < 20 && fscanf(nums_pntr, "%d", &nums[i]) == 1)
{
i++;
}
Это исключит выход за пределы массива, если в файле больше 20 чисел.
Перепишем код так:
#include <stdio.h>
intmain(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>, которая выводит связанное с этим кодом
сообщение об
ошибке.
Напишем программу errno.c, подключим заголовочные файлы стандартной
библиотеки ввода/вывода, функции обработки сообщений об ошибках
и функции работы со строками:
Теперь добавьте цикл, позволяющий вывести сообщение об
ошибке, связанное с каждым цифровым кодом ошибки:
#include <stdio.h>
#include <errno.h>
#include <string.h>
intmain(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;
}
Выполните программу:
Сначала мы получили ошибку "
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 имеет
положительное значение, если действует летнее время,
0 — если летнее время не действует ,
отрицательное значение — если информация недоступна.
Текущее количество прошедших секунд возвращает функция time(NULL)
как тип данных time_t.
Этот параметр может быть передан как аргумент в функцию
localtime()
для преобразования к формату компонентов структуры tm.
Полный список всех спецификаторов формата включен в разделе
справочной
информации в конце книги.
Компоненты структуры разрешается вывести в стандартном формате
даты и времени с помощью функции asctime(). В качестве
альтернативы
отдельные компоненты можно вывести с использованием специальных
спецификаторов формата времени с помощью функции strftime().
Она принимает четыре аргумента, позволяющих указать массив символов,
в котором будет храниться отформатированная строка даты,
максимальная длина строки, текст и спецификаторы формата, необходимые
для извлечения требуемых компонентов для структуры tm.
Напишем программу datetime.c, добавим заголовочные файлы
препроцессора,
позволяющих подключить функции стандартной библиотеки ввода/вывода
и функции работы со временем:
#include <stdio.h>
#include <time.h>
intmain(void)
{
char buffer[100]; /* Объявим массив символов */
time_t elapsed; /* Объявим переменную типа time_t */
struct tm *now; /* Объявим Указатель на структуру tm */return 0;
}
Далее в блоке функции main() получите текущее количество
секунд,
прошедшее с момента наступления эпохи Unix:
Теперь преобразуйте это число к формату компонентов структуры
tm:
#include <stdio.h>
#include <time.h>
intmain(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.
Далее выведите отдельные компоненты даты и времени:
#include <stdio.h>
#include <time.h>
intmain(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;
}
Выполните программу:
Обратите внимание: Visual Studio Code в строках 14 и 16 не может
корректно интерпретировать
компоненты функции strftime(), однако исходный код
компилируется без предупреждений и
ошибок.
Программа выполняется правильно.
Запуск таймера
Возможность получить текущее время до и после какого-нибудь
события означает, что можно определить продолжительность события,
найдя их разность. В заголовочном файле <time.h> содержится
функция
difftime(), отвечающая именно за это. Эта функция принимает два
аргумента, они оба имеют тип данных time_t.
Она вычитает второй аргумент из первого и возвращает разницу,
выраженную в целом количестве секундах, которая имеет формат
double.
ещё одним способом работы с временем является функция clock(),
которая располагается в заголовочном файле <time.h>.
Она возвращает время процессора, использованное с момента начала программы,
выраженное в тиках. Эта функция может быть использована для того,
чтобы приостановить выполнение программы путём запуска пустого
цикла, который будет выполняться до тех пор, пока не окажется
достигнут определённый момент в будущем.
Напишем программу timer.c, добавим заголовочные файлы позволяющие
включить функции стандартной библиотеки ввода/вывода и функции работы с
временем:
#include <stdio.h>
#include <time.h>
void wait(int seconds); /* Объявим прототип пользовательской функции */intmain(void)
{
time_t start; /* Объявим переменную типа time_t */
time_t stop; /* Объявим переменную типа time_t */int i; /* Объявим целочисленную переменную типа int */return 0;
}
Далее в блоке функции main() получите текущее время и
выведите
сообщение:
#include <stdio.h>
#include <time.h>
void wait(int seconds);
intmain(void)
{
time_t start;
time_t stop;
int i;
start = time(NULL); /* Текущее количество прошедших секунд возвращает функция time(NULL) */printf("\nStarting countdown...\n"); /* Печать сообщения в консоль */return 0;
}
Добавьте цикл for, позволяющий вывести значение счетчика, на каждой
итерации
вызывайте пользовательскую функцию wait():
#include <stdio.h>
#include <time.h>
void wait(int seconds);
intmain(void)
{
time_t start;
time_t stop;
int i;
start = time(NULL);
printf("\nStarting countdown...\n");
for (i = 10; i > -1; i--) /* Цикл с декрементом значения итератора */
{
printf(" - %d\n", i); /* Печать сообщения в консоль */
wait(1); /* Вызов пользовательской функции */
}
return 0;
}
Далее в блоке функции main() ещё раз получите текущее время,
а затем
выведите время, потребовавшееся для работы цикла:
#include <stdio.h>
#include <time.h>
void wait(int seconds);
intmain(void)
{
time_t start;
time_t stop;
int i;
start = time(NULL);
printf("\nStarting countdown...\n");
for (i = 10; i > -1; i--)
{
printf(" - %d\n", i);
wait(1);
}
stop = time(NULL); /* Текущее количество прошедших секунд возвращает функция time(NULL) */printf("\nRuntime: %.0f seconds\n\n", difftime(stop, start)); /* Печать сообщения в консоль -
Разница между значением переменной stop и start */return 0;
}
После блока функции main() напишем определение
пользовательской функции -
инициализируем переменную типа clock_t как момента в будущем:
Количество clock ticks зависит от реализации, оно
определено в файле <time.h> как константа
CLOCKS_PER_SEC.
В заключение в блоке функции wait() добавьте пустой цикл,
который прекращает свою работу, когда текущее время достигает заданной
точки в будущем, и верните управление в цикл функции main():
Вы можете найти время в секундах, прошедшее с начала
работы программы, разделив значение, возвращенное
функцией clock() на значение CLOCKS_PER_SEC.
Выполните программу:
Внимательно проследите каждый шаг программы.
Генерация случайных чисел
В заголовочном файле <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().
Напишем программу lotto.c, подключим стандартные заголовочные файлы
позволяющих включить функции стандартной библиотеки ввода/вывода,
функции работы со случайными числами, временем и строками:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
intmain(void)
{
int i;
int r;
int nums[50]; /* Объявим целочисленный массив */
char buffer[4]; /* Объявим массив символов */
char string[50] = {"Your Six Lucky Numbers Are: "}; /* Объявим строковый литерал */return 0;
}
В блоке функции main() с помощью количества прошедших секунд
зададим зерно генератору случайных чисел:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
intmain(void)
{
int i;
int r;
int nums[50];
char buffer[4];
char string[50] = {"Your Six Lucky Numbers Are: "};
srand(time(NULL)); /* Зерно генератора псевдо-случайных чисел на основе времени */return 0;
}
Далее, спомощью цикла for заполним целочисленный массив числами от
0...49 по порядку:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
intmain(void)
{
int i;
int r;
int nums[50];
char buffer[4];
char string[50] = {"Your Six Lucky Numbers Are: "};
srand(time(NULL));
for (i = 0; i < sizeof(nums) / sizeof(int); i++) /* Проверочное утверждение основано на размере масива делённого на размер типа данных */
{
nums[i] = i;
}
return 0;
}
Теперь напишем цикл перемешивания последовательности числе в массиве:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
intmain(void)
{
int i;
int r;
int nums[50];
char buffer[4];
char string[50] = {"Your Six Lucky Numbers Are: "};
srand(time(NULL));
for (i = 0; i < sizeof(nums) / sizeof(int); i++)
{
nums[i] = i;
}
int temp;
for (i = 0; i < sizeof(nums) / sizeof(int); i++) /* цикл перемешивания последовательности числе в массиве */
{
r = (rand() % 49) + 1; /* Генерируется случайное число 1...49 */
temp = nums[i]; /* Во временную переменную сохраним число по текущему индексу i массива */
nums[i] = nums[r]; /* По текущему индексу i массива разместим число по индексу слачайного числа */
nums[r] = temp; /* По индексу случайного числа в массиве разместим число хранимое во временной переменной */
}
return 0;
}
Элементу массива nums[0]
присваивается значение 0,
но в перемешивании он не участвует, поскольку для
выбора доступны только элементы в диапазоне 1...49.
Добавьте числа из шести последовательных элементов массива в строку.
Нулевой индекс не участвует.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
intmain(void)
{
int i;
int r;
int nums[50];
char buffer[4];
char string[50] = {"Your Six Lucky Numbers Are: "};
srand(time(NULL));
for (i = 0; i < sizeof(nums) / sizeof(int); i++)
{
nums[i] = i;
}
int temp;
for (i = 0; i < sizeof(nums) / sizeof(int); i++)
{
r = (rand() % 49) + 1;
temp = nums[i];
nums[i] = nums[r];
nums[r] = temp;
}
for (i = 1; i < 7; i++) /* Цикл записи чисел в строку. Работа с целыми строками */
{
sprintf(buffer, "%d", nums[i]);
strcat(buffer, " ");
strcat(string, buffer);
}
printf("\n%s\n\n", string); /* Печать в консоль результата: Строка и шесть чисел */return 0;
}
Выполните программу:
Обратите внимание на то, как функция sprintf()
используется для преобразования
целочисленных значений
к символам, которые впоследствии могут быть
сконкатенированы в более крупную строку.
Отображение диалогового окна
В операционной системе 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,
которая может быть добавлена в самом конце команды компиляции,
чтобы предотвратить появление окна командной строки в случае, если
программа запускается по двойному щелчку.
Создайте копию предыдущего примера, lotto.c, и переименуйте её
в winlotto.c.
В самом начале переименованного файла программы добавьте
ещё одну инструкцию препроцессора, чтобы сделать доступными
функции WINAPI:
#include <windows.h>
Далее замените строку
intmain(void) на другую, которая
осуществляет
коммуникацию с операционной системой Windows:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
Теперь замените вызов функции printf(), который выводит на
экран
выбранные числа, на вызов, который отображает их в диалоговом
окне:
MessageBox(NULL, str, "Lotto Number Picker", MB_OK | MB_ICONEXCLAMATION);
Сохраните, скомпилируйте и выполните программу двумя
способами — из командной строки и по двойному щелчку:
Запуск программы wlotto.exe по двойному щелчку. Отобразит
окно.
Запуск программы wlotto.exe из консоли. Отобразит окно.
Заключение
Указатель на файл имеет синтаксис FILE *fp и может быть
использован для открытия, чтения, записи и закрытия файлов.
Функция fopen(), которая открывает файлы, должна знать о
расположении файла и о режиме его открытия.
После завершения операций с файлом этот файл должен быть
закрыт с помощью функции fclose().
Отдельные символы могут быть считаны и записаны с помощью
функций fgetc() и fputs(), а функции
fgets() и fputs() могут считывать целые строки.
Функции fread() и fwrite() могут считывать и
записывать файловые
потоки целиком в символьные буферы и из них.
Уровень гибкости, предоставляемый функциями fscanf() и
fprintf(),
позволяет считывать и записывать данные в файловые потоки,
stdin или stdout.
Сообщения об ошибках могут быть выведены на экран с помощью
функции perror(), функция strerror() способна
вывести сообщение
по связанному с ним коду errno.
Функция time(NULL) возвращает количество секунд, прошедшее с
момента Эры UNIX в полночь 1 января 1970 года.
Члены структуры tm struct инициализируются путём
преобразования количества прошедших секунд с помощью функции
localtime().
Дата и время в стандартном формате предоставляются функцией
asctime(), но отдельные компоненты могут быть отформатированы
с помощью спецификаторов формата и функции strftime().
Временные отсечки во время выполнения программы можно
получить, вызвав функции difftime() и clock().
Для последовательности псевдослучайных чисел, генерируемой
функцией rand(), следует предварительно задать зерно с помощью
функций srand() и передать ей агрумент
time(NULL).
Включение заголовочного файла <windows.h> в программу
делает
доступным Windows API, позволяющий создавать графические
компоненты.
Точкой входа в программу Windows является функция WinMain(),
а функция MessageBox() позволяет создать простейшее диалоговое
окно.
Справочная информация
В этой главе перечислены стандартные ASCII
коды и все функции, содержащиеся в стандартной
библиотеке языка C, сгруппированные по
файлам, в которых они находятся, а также описания
всех стандартных констант.
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, в котором хранится информация, необходимая
для
управления потоком.
Функции, перечисленные в ниже, выполняют операции над файлами.
Функция fopen() возвращает указатель типа FILE
или значение NULL,
если файл не может быть открыт. При вызове функции следует
указать один из следующих режимов:
Режимы работы с открытыми файлами
Режим открытия файла - mode
Описание режима
r
открыть файл только для чтения
w
создать текстовый файл для записи и стереть всё его предыдущее
содержимое
a
открыть или создать текстовый файл для записи в конец файла
r+
открыть текстовый файл для обновления данных (чтение
и запись)
w+
открыть текстовый файл для обновления данных (чтение
и запись, стереть всё предыдущее содержимое)
a+
открыть или создать текстовый файл для обновления, запись
будет производиться в конец файла.
Если нужно открыть бинарный файл, к режиму следует добавить
символ b(слева от плюса). Например, wb+.
Функция freopen() открывает файл в заданном режиме и
привязывает его к потоку. Она возвращает поток или значение NULL,
если происходит ошибка. Обычно эта функция используется для изменения
файлов, связанных с потоками stdin, stdout или
stderr.
int fflush(FILE *stream);
Вызов этой функции приводит к тому, что буферизованные
данные, находящиеся в выходном потоке, нёмедленно записываются.
При ошибке записи функция возвращает константу EOF,
в противном случае — значение NULL.
Вызов fflush(NULL) очищает все выходные потоки.
int fclose(FILE *stream);
Вызов функции fclose() очищает любые незаписанные данные из
потока, а затем закрывает поток. Эта функция возвращает
константу EOF в случае ошибки или, в противном случае, 0.
int remove(const char *filename);
Эта функция удаляет указанный файл — все последующие
попытки открыть его обернутся неудачей. Функция возвращает
ненулевое значение, если файл удалить не удается.
int rename(const char *old-name, const char *new-name);
Функция rename() изменяет имя указанного файла или
возвращает
ненулевое значение, если файл переименовать не удается.
FILE *tmpfile(void);
Вызов функции tmpfile() создает временный файл, открытый в
режиме wb+, который будет удален по окончании работы
программы. Эта функция возвращает поток или значение NULL,
если файл создать не удается.
char *tmpnam(char arr[L_tmpnam]);
Эта функция хранит строку, располагающуюся в массиве, и
возвращает указатель с уникальным именем, указывающим на этот
массив. Массив arr должен содержать как минимум
L-tmpnam
символов. Функция tmpnam() генерирует новое имя при каждом
вызове.
int setvbuff(FILE *stream, char *buffer, int mode, size_t size);
Вызов функции setvbuff() начинает буферизацию указанного
потока. Эта функция должна быть вызвана после того, как поток был
открыт, но перед тем, как с ним будет выполнена хотя бы одна
операция. Корректными режимами для этой функции являются:
_IOFBF - указывает проводить полную буферизацию,
_IOLBF - указывает проводить буферизацию строки,
_IONBF - отключает буферизацию.
Значение переменной size указывает размер
буфера.
Функция возвращает ненулевое значение, если происходит ошибка.
void setbuf(FILE *stream, char *buffer);
Функция setbuf() определяет, как будет буферизован поток.
Эта функция должна быть вызвана после того, как поток был открыт,
но перед тем, как с ним будет выполнена хотя бы одна операция.
Аргумент buffer указывает на массив,
который будет использован
в качестве буфера.
Функции для форматирования
выходных данных
int fprintf(FILE *stream, const char *format, …);
Функция fprintf() преобразует и записывает данные в
указанный
файловый поток под управлением спецификатора формата. Эта
функция возвращает количество записанных символов или
отрицательное значение, если происходит ошибка.
intprintf(const char *format, …);
Функция printf() преобразует и записывает данные в поток
stdout.
её вызов эквивалентен вызову
fprintf(stdout, const char *format).
int sprintf(char *s, const char *format);
Функция sprintf() аналогична функции printf()
за исключением того,
что данные записываются в строку, которая завершается символом
\0.
Эти три функции эквивалентны соответствующим функциям
printf()
за исключением того, что их переменное число параметров
заменяется аргументом типа va_list type. Обратитесь к
списку функций
<stdarg.h> для получения более подробной информации.
Функции для форматирования
входных данных
int fscanf(FILE *stream, const char *format, …);
Функция fscanf() считывает данные из указанного потока под
управлением спецификатора формата и присваивает
преобразованные значения указанным аргументам. Эта функция возвращает
количество преобразованных символов или константу EOF если
достигнут конец файла или произошла ошибка.
int scanf(const char *format, …);
Функция scanf() преобразовывает и считывает данные из
потока
stdin. Её вызов эквивалентен вызову
fscanf(stdin, const char *format)
int sscanf(char *s, const char *format, …);
Функция sscanf() аналогична функции scanf() за
исключением того,
что входные данные считываются из указанной строки.
Спецификаторы формата
выходных и входных данных
В языке программирования C префикс % указывает на спецификатор
формата.
Перечислим все спецификаторы для функции printf():
Спецификаторы формата выходных данных
Символ
Функция printf() выполняет преобразование
%d, %i
Целочисленный тип данных, знаковое десятичное число
%o
Целочисленный тип данных, беззнаковое восьмеричное число без нуля в
начале
%x, %X
Целочисленный тип данных, беззнаковое шестнадцатеричное число.
0х для отображения в нижнем регистре (например, 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 ) или шестнадцатеричным
(оно будет начинаться с 0х или 0Х )
o
Целочисленный тип данных, восьмеричное число, которое может
начинаться с 0
u
Целочисленный тип данных, беззнаковое десятичное число
x
Целочисленный тип данных, шестнадцатеричное число, которое может
начинаться с 0
c
Символы, которые будут помещены в указанный массив.
Считывает количество символов, указанное как ширина строки (по
умолчанию 1 ),
не добавляя символ \0 к концу строки.
Считывание прекратится, если встретится пробел
s
Строка, не содержащая пробелов, которая будет помещена
в указанный массив. Он должен быть достаточно большим,
чтобы вместить все символы плюс завершающий символ \0
e, f, g
Тип данных с плавающей точкой, число может иметь
знак. После знака будут располагаться цифры в
формате строки. Выражение может иметь десятичную точку,
а также экспоненту ("е" или "Е"), после которой будет
следовать число, которое может иметь знак
p
Адрес в памяти, имеет тот же формат, что и выводимое
функцией printf() значение под управлением
преобразования %p
n
Не является преобразованием. Хранит количество
символов, считанных к моменту вызова функции scanf(),
которое будет сохранено в целочисленном указателе
[…]
Выполняет сравнение строки из потока со строкой,
указанной в квадратных скобках, и добавляет символ \0
[^…]
Выполняет сравнение всех символов ASCII из потока,
исключая символы, указанные в квадратных скобках,
и добавляет символ \0
Функции для ввода и вывода символов
int fgetc(FILE *stream);
Возвращает следующий символ указанного потока как
переменную типа char или константу EOF,
если достигнут конец файла или произошла ошибка.
char *fgets(char *s, intn, FILE *stream);
Считывает следующие n-1 символов указанного потока, затем
добавляет символ \0 в конец массива. Эта функция возвращает
указатель s или значение NULL, если достигнут конец
файла или
произошла ошибка.
int fputc(intc, FILE *stream);
Записывает символ c в указанный поток и возвращает
записанный
символ или константу EOF, если произошла ошибка.
int fputs(const char *s, FILE *stream);
Записывает строку s в указанный поток и возвращает неотрица
тельное значение или константу EOF, если произошла ошибка.
int getc(FILE *stream);
Функция getc() эквивалентна функции fgetc().
int getchar(void);
Функция getchar() эквивалентна функции
getc(stdin).
char *gets(char *s);
Считывает следующую введенную строку в массив, заменяя её
завершающий символ перевода каретки символом \0. Эта функция
возвращает указатель s или значение NULL, если
достигнут конец
файла или произошла ошибка.
int putc(intc, FILE *stream);
Функция putc() эквивалентна функции fputc().
int putchar(intc);
Функция putchar() эквивалентна функции
putc(c, stdout).
int puts(const char *s);
Записывает строку s и символ новой строки \n в поток
stdout.
Эта функция возвращает неотрицательное значение или константу
EOF,
если произошла ошибка.
int ungetc(intc, FILE *stream);
Помещает символ c обратно в указанный поток, он будет
прочитан
при следующем обращении. Гарантированно можно выполнить
только один возврат символа, константу EOF вернуть нельзя. Эта
функция возвращает отправленный назад символ или константу
EOF, если произошла ошибка.
Функции ввода и вывода
данных напрямую из потоков
Функции fread() и fwrite(),
содержащиеся в заголовочном файле <stdio.h>,
наиболее эффективны для чтения и записи текстовых файлов целиком:
Функция fread() считывает данные из указанного потока в
указанный массив ptr. Может быть прочитано максимум
nobj объектов
размера size. Эта функция возвращает количество считанных
объектов,
которое может быть меньше запрошенного количества объектов.
Состояние функции fread() можно узнать во время её
выполнения
с помощью функций feof() и ferror() —
информацию о них вы
найдете ниже.
Функция fwrite() записывает в поток nobj
объектов размера size из указателя ptr.
Эта функция возвращает количество записанных
объектов. Если произойдет ошибка, возвращенное значение будет
меньше, чем количество запрошенных объектов nobj.
Функции для работы с ошибками
Множество стандартных функций библиотеки языка программирования
C устанавливают индикаторы, когда происходит ошибка или когда
они достигают конца файла. Эти индикаторы разрешается проверить
с помощью перечисленных ниже функций. Также целочисленное выражение
errno, определённое в заголовочном файле
<errno.h>, может содержать
код, с помощью которого можно узнать более подробную информацию
об ошибке, произошедшей последней.
void clearer(FILE *stream);
Функция clearer() очищает индикаторы конца файла и ошибок
для
заданного потока.
int feof(FILE *stream);
Функция foef() возвращает ненулевое значение, если для
заданного потока был установлен индикатор конца файла.
int ferror(FILE *stream);
Функция ferror() возвращает ненулевое значение, если для
заданного потока был установлен индикатор ошибки.
void perror(const char *s);
Функция perror() выводит определяемое реализацией сообщение
об ошибке, соответствующее целочисленному значению,
содержащемуся в выражении errno. Смотрите также информацию
о функции
strerror(), которая располагается в
заголовочном файле <string.h>.
Функции позиционирования в файлах
Файловый поток обрабатывается по одному символу за раз. Функции,
приведенные в следующей таблице, могут использоваться для
управления позицией в файловом потоке.
int fseek(FILE *stream, long offset, intoriginal);
Функция fseek() устанавливает позицию файла в заданном
потоке.
Все последующие операции чтения и записи будут производиться
в новой позиции. Новая позиция определяется путём указания,
насколько следует сместиться (offset) от оригинальной
позиции
(original). Опционально третий аргумент может иметь
значение:
SEEK_SET (начало файла),
SEEK_CUR (текущая позиция),
SEEK_END (конец файла).
Для текстового потока смещение может быть равно либо
нулю, либо значению, возвращенному функцией ftell(), — при
этом значение оригинальной позиции должно быть равно
SEEK_SET.
Функция fseek() возвращает ненулевое значение, если
происходит
ошибка.
long ftell(FILE *stream);
Функция ftell() возвращает позицию в текущем файле
указанного
потока или значение -1, если происходит ошибка.
int fgetpost(FILE *stream, fpos_t *ptr);
Функция fgetpos() записывает текущую позицию файла
указанного
потока в заданный указатель ptr, который имеет специальный
тип
fpos_t. Эта функция возвращает ненулевое значение, если
происходит ошибка.
int fsetpos(FILE *stream, const fpos_t *ptr);
Функция fsetpos() позиционирует файловый указатель в
заданном
потоке в позиции, записанной функцией fgetpos() в указатель
ptr.
Эта функция возвращает ненулевое значение, если происходит ошибка.
Функции проверки символов
<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(intc)
Преобразует символ к нижнему регистру
int toupper(intc)
Преобразует символ к верхнему регистру
Функции Арифметические
<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, intb)
Преобразовывает начальный фрагмент строки s к
переменной
типа long с основанием b, игнорируя пробелы в
начале этой строки.
Указатель на остальную часть строки хранится в *endp
unsigned long strtoul(const char *s, char **endp, intb)
Функция аналогична функции strtol(), за исключением
того, что она
выполняет преобразование к типу unsigned long
int rand(void)
Возвращает псевдослучайное число, лежащее в диапазоне между
0 и зависящим от реализации максимумом (не меньше
32767)
void srand(unsigned intseed)
Устанавливает зерно для новой последовательности случайных
чисел, генерируемой функцией 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(intstatus)
Заставляет программу завершиться обычным способом. Значение
переменной 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(intn)
Возвращает модуль числа 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(intexpression);
Если значение выражения 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,
в местное
Форматирует время, хранящееся в структуре *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, intvalue)
Восстанавливает окружение, которое было сохранено в переменной
env с помощью функции setjmp(), как если
бы функция
setjmp() вернула значение value()
Функции Сигнальные
<signal.h>
Заголовочный файл <signal.h> содержит функции,
предназначенные для
обработки исключительных состояний, которые могут возникнуть во
время выполнения программы:
Функция signal() определяет, как будут обработаны
последующие
вызовы. Обработчик может иметь значение SIG_DFL —
значение по
умолчанию, зависящее от реализации, или SIG_IGN,
позволяющее
проигнорировать сигнал. Переменная sig может иметь одно
из
следующих значений:
SIGABRT - аварийное прекращение работы
SIGFPE - арифметическая ошибка
SIGILL - некорректная инструкция
SIGINT - внешнее прерывание
SIGSEGV - попытка получить доступ к памяти за
пределами выделен
ного участка
SIGTERM - программе выслан запрос на завершение
работы
SIG_ERR - Функция возвращает предыдущее значение
обработчика
заданного сигнала или SIG_ERR, если происходит
ошибка.
Когда сигнал sig срабатывает в следующий раз,
сигнал восстанавливает своё первоначальное поведение,
а затем вызывается обработчик сигнала. Если обработчик сигнала
возвращает управление, выполнение
программы продолжается с точки, где возник сигнал
int raise(intsig)
Эта функция пытается отправить сигнал 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)
Его исследования показали, что фактически машина должна обладать всего лишь
четырьмя основными свойствами, вот
они:
Назначение переменной значения и возможность переназначать его.
Классическое описание объявления и инициализации переменной.
Ветвление исполнения программы. Т.е. в зависимости от условия
выполнять ту или иную ветку кода.
(Линии в
блок-схеме)
Цикличность. Способность повторять блок кода заданное количество
раз или делать это бесконечно.
Циклы.
Функции. Способность определять функцию и вызывать её. Фактически,
Функции называют по разному:
группа
строк кода, блок кода, модуль, ветка.
Машина обладающая такими свойствами, может реализовать(как теперь принято
говорить) Тьюринг-Полный
Алгоритм.
Со всеми этими понятиями вы уже познакомились и это самые базовые концепции,
которых достаточно для написания
программы любой
степени сложности.
Язык 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>
intmain()
{
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>
intmain()
{
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>
intmain()
{
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>
intmain()
{
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>
intmain()
{
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>
intmain(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>
intmain(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;
}