No Image

Программирование на чистом си

СОДЕРЖАНИЕ
292 просмотров
10 марта 2020

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

Это не учебник по Си, а скорее краткая справка , где перечислены основные моменты, которые необходимо знать для программирования на этом языке. Что касается базовых основ, то их читайте у классиков – Шилдт, Страуструп и т.п.

Пример простой программы на Си

Функция main – это точка входа в программу, с которой компьютер начинает выполнение программы.

Допускается из main возвращать void, хотя это не по стандарту, так что лучше int.

В функцию main можно передавать аргументы командной строки:

Структура памяти программы:

– куча – для динамического выделения памяти

– стек – локальные переменные класса памяти auto (включая аргументы функций)

– CODE – исполняемый код, инструкции процессора

Некоторые элементы синтаксис языка:

– команда продолжается на следующей строке

"go" "to" – воспринимается как одна строка "goto"

Типы данных в Си

-Базовые типы данных: char, int, float, double.

-Модификаторы знака: signed, unsigned.

-Модификаторы знака: long, short.

void – тип без значения

В Си логический тип реализован неявно (с помощью int): false = нуль, true = не нуль.

Введение псевдонимов для ранее описанных типов данных:

typedef тип имя

где тип – любой существующий тип данных, имя – новое имя для этого типа.

Пример: typedef unsigned char byte;

Преобразование типов:

Если операнды операции имеют разные типы, то происходит неявное приведение типов:

(чтобы здесь получить 0.4 нужно было бы написать x=2.0/5 или 2/5.0)

Явное приведение типов:

Принудительное преобразование типов:

(желательно вообще избегать преобразования типов)

Переменные и константы

Переменная представляет собой блок памяти, на который мы ссылаемся по её имени (идентификатору).

Декларация переменных (вместе с инициализацией):

[класс памяти] [квалификаторы] [модификаторы] тип идентификатор = инициатор;

Здесь ";" – составляющая часть конструкции, завершающая часть.

Допустима (хотя и редко используется) запись: const x = 100; (по умолчанию int).

Квалификаторы (или "модификаторы доступа"): const, volatile.

const – означает, что переменные не могут изменяться во время выполнения программы; инициалиировать можно только при декларации;

volatile – содержимое переменной может измениться само собой (используется в многопоточных программах при взаимодействии процессов)

Возможен вариант const volatile, когда писать могут только снаружи.

Спецификторы хранения (описатель класса памяти): auto, register, extern, static.

auto – локальные переменных (по умолчанию) – программный стек.

register – просьба компилятору положить переменную в регистр ЦПУ (но он эту просьбу редко выполняет);

extern – объявление (declaration) переменных, но не определение (definition) (определение где-то в другом месте); определение может идти ниже по файлу (но как глобальная) или в другом файле.

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

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

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

Описание области действия идентификаторов (имен):

– внутреннее (локальное) – внутри блока

– внешнее (глобальное) – вне всех блоков

Идентификатор, описанный внутри блока, известен только в этом блоке (локальный идентификатор).

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

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

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

В языке C/C++ предусмотрено три категории связей: внешние, внутренние связи и их отсутствие.

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

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

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

Глобальные переменные по умолчанию имеют класс памяти extern (но ключевое слово ставить не надо) и располагаются в сегменте данных (Data). Такие переменные по умолчанию инициализируются нулем при запуске программы (т. е. один раз только). Область видимости идентификатора extern – от точки появления до конца файла.

Однако ключевое словое extern ставится только для объявления, но при определении переменной слово extern не ставится:

Определение должно быть только одним. Объявлений может быть много в пределах одного файла.

Спецификатор extern сообщает компилятору, что следующие за ним типы и имена переменных объявляются где-то в другом месте.

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

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

Переменные с классом памяти static видны только в пределах текущего блока (для локальных) или в пределах файла (для объявленных глобально).

Статические переменные хранятся в сегменте данных (data) и по умолчанию инициализируются нулем. Т.е. память под static-переменные выделяется при старте программы и существует до конца программы.

Замечание: Инициализация выполняется одни раз при выделении памяти!

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

Читайте также:  Запрос в access это

Следует различать присваивание и инициализацию:

– Присваивание: имя_переменной = выражение;

– Многочисленное присваивание: x = y = z = 0;

– Инициализация переменных: тип имя_переменной = константа;

Константы

Константы являются частью машинных команд и под них память не выделяется.

10-я система: 127; -127; +127;

8-я система: 0127; (начинается с нуля – значит 8-ричная!)

16-я система: 0x7F; (x или X, f или F – регистр не влияет)

– вещественные: 3.14; 2. ; .25 (0 можно опускать); 2E3; 2e3; 2E-3; 2.0E+3;

– символьные: 8-битные ASCII: ‘A’, ‘=’, ‘
‘, ‘ ‘, ‘370’, ‘xF8’ (символ градуса);

– строковые литералы (в двойных кавычках): "Hello, world!
". Строки заканчиваются нулевым байтом – ‘’.

Операции и операторы

Оператор (инструкция, англ. statement) – это единица выполнения программы.

В языке Си любое выражение, заканчивающееся символом "точка с запятой" (;), является оператором.

Фигурные скобки – это составной оператор.

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

Операции:

– Инкрименты и декрименты: ++a, –a, a++, a– (могут выполняться быстрее)

– Операторы сравнения (отн-ний): > >=

– Логические операторы: && || ! (возвращают 1 или 0)

– Оператор ?: x ? y : z, напр.: r = 10>9 ? 100 : 200

sizeof – унарный оператор для вычисления размера переменной или типа

, – оператор запятая (последовательное вычисление): a = (b=3, b+2);

Приоритеты операций:

1) ::->. [] () – разрешение контекста, извлечение; индекс массива, вызов ф-ии и преобр-ие типа;

8) == != равно, не равно;

10) ^ XOR (исключающее ИЛИ);

11) | побитовое ИЛИ;

13) || ИЛИ логическое;

14) ?: тернарная операция (x ? y : z);

15) = *= /= %= += и т.д. – операция присвоения [Справа-налево];

Порядок выполнения операторов:

– Унарные операторы выполняются справа-налево.

– Бинарные выполняются слева-направо.

– Присваивание выполняется справа-налево.

Порядок можно менять с помощью скобок!

Выражение а + b + c интерпретируется как (а + b) + с.

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

Например, результат y = (x=10)*2 + x*y будет зависеть от компилятора.

4 исключения: && и || (всегда вычисляет 1-ый оператор; причем если он равен true(false), то 2-ой не вычисляется); условие (x ? y : z); оператор запятая (,).

Поэтому условие if-else можно реализовать так:

sizeof() – возвращает длину в байтах переменной или типа; sizeof(int); sizeof(a);

sizeof выражение; – само выражение не вычисляется.

Оператор запятая:

левая сторона оператора вычисляется как void и не выдаёт значения, переменной x присвается значение выражения в правой стороне, т.е. y+1.

Указатели и ссылки в Си

* – доступ к значению объекта по указанному адресу;

Указатели:

Указатель — такая переменная, которая хранить адрес некоторого объекта и связана с типом этого объекта.

класс_памяти квалификатор тип * квалификатор идентификатор = инициатор;

Основные операции над указателями:

p = q; – копирование адреса. Обычно одно типа. Если разного типа, то это не безопасно!

Указатель p может ссылаться на тип void (используется в C для обобщенных алгоритмов).

p = NULL; – нулевой указатель – это признак отсутствия значения у указателя. Такой указатель нельзя использовать для доступа к памяти, т. к. это приведет к сбою программы во время выполнения.

Арифметика указателей отличается от обычной и зависит от типа:

Унарные операции * и ++ имеют одинаковый приоритет и выполняются справа налево, т. е.

    Переводы, 1 октября 2016 в 0:02

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

1. Указатели на функцию

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

Этот приём заключается в следующем. Сперва нужно задать тип “указатель на функцию, возвращающую что-то” и использовать его для объявления переменной. Рассмотрим простой пример. Сначала я задаю тип PFC (Pointer to a Function returning a Character):

Затем использую его для объявления переменной z :

Определяю функцию a() :

Адрес функции теперь хранится в z :

Заметим, что вам не нужен оператор & ("address-of") ; компилятор знает, что a должна быть адресом функции. Так происходит из-за того, что с функцией можно произвести лишь две операции: 1) вызвать её или 2) взять её адрес. Поскольку вызова функции не происходит (отсутствуют скобки), остаётся лишь вариант с получением адреса, который помещается в z .

Чтобы вызвать функцию, адрес которой находится в z , просто добавьте скобки:

2. Списки аргументов переменной длины

Обычно вы объявляете функцию, которая принимает фиксированное число аргументов. Тем не менее, можно написать функцию, которая принимает любое их количество. Стандартная функция printf() тому доказательство. Разумеется, вы можете сами написать подобную функцию. Вот пример:

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

С переменными аргументами работают несколько встроенных функций и макросов: va_list , va_start , va_arg и va_end (они определены в stdarg.h ).

Сперва вам нужно объявить указатель на список аргументов:

Затем установите argp в первый аргумент переменной части. Это первый аргумент после последнего фиксированного (в нашем случае arg_count ):

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

Заметим, что вам нужно знать тип аргумента заранее (в нашем случае int ) и число аргументов (у нас задаётся фиксированным arg_count ).

Наконец, нужно убраться при помощи va_end :

3. Проверка и установка отдельных битов

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

Читайте также:  Launch csm в биосе что это

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

Если они как-то связаны, как в примере выше (они все описывают состояние движения), то зачастую бывает удобнее хранить их в одной “переменной состояния” и использовать каждый бит для задания того или иного состояния:

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

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

В архиве с кодом, который будет дан в конце статьи, есть пример работы с битами.

Чтобы установить заданный бит переменной value (в диапазоне от 0 до 31), используйте такое выражение:

Для очистки бита используйте:

А для получения значения бита:

4. Ленивые логические операторы

Логические операторы Си, && (“и”) и || (“или”), позволяют вам составлять цепочки условий в тех случаях, когда действие должно выполняться при выполнении всех условий ( && ) или только одного ( || ). Но в Си также есть операторы & и | . Очень важно понимать разницу между ними. Если вкратце, двухсимвольные операторы ( && и || ) называются “ленивыми” операторами. Если они используются между двумя выражениями, то второе будет выполнено, только если первое оказалось верным, иначе оно пропускается. Рассмотрим пример:

Тест (int)f && feof(f) должен вернуть истинный результат, когда будет достигнут конец файла f . Сперва тест вычисляет f ; она будет равна нулю (ложное значение), если файл не был открыт. Это ошибка, поэтому попытка чтения файла не увенчается успехом. Однако, поскольку первая часть теста провалена, вторая не будет обработана, и feof() не будет запущена. Этот пример демонстрирует корректное использование ленивого оператора. Теперь посмотрите на этот код:

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

5. Тернарные операторы

Операция называется тернарной. когда принимает три операнда. В Си тернарный оператор ? : можно использовать для сокращённой записи тестов if..else . Общий синтаксис выглядит так:

Пусть у нас есть две целых переменных, t and items . Мы можем использовать if..else для проверки значения items и присваивания её значения переменной t таким образом:

Используя тернарный оператор, эту запись можно сократить до одной строки:

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

Рассмотрим ещё один пример. Этот код выводит первую строку, когда у нас один предмет, и вторую, когда их несколько:

Это можно переписать так:

6. Стек

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

Код ниже задаёт очень маленький стек: массив _stack из двух целых. Помните, что при тестировании всегда лучше использовать небольшие числа. Если код содержит ошибки, найти их при работе с массивом из 2 элементов будет проще, чем если их будет 100. Также объявляется указатель на стек _sp и устанавливается в основание стека _stack :

Теперь определим функцию push() , которая помещает целое в стек. Она возвращает новое число элементов в стеке или -1, если стек полон:

Для получения элементов стека нужна функция pop() . Она возвращает новое число элементов в стеке или -1, если он пуст:

А вот пример, демонстрирующий работу со стеком:

7. Копирование данных

Вот три способа копирования данных. Первый использует стандартную функцию memcpy() , которая копирует n байт из src в dst :

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

И наконец, функция, использующая 32-битные целые для ускорения копирования. Помните, что скорость в конечном итоге зависит от оптимизации компилятора. В этом примере предполагается, что счётчик данных n кратен 4 из-за работы с 4-байтовыми указателями:

Примеры можно найти в архиве ниже.

8. Использование заголовочных файлов

Си использует заголовочные файлы ( .h ), которые могут содержать объявления функций или констант. Заголовочный файл можно импортировать в код двумя способами: если файл предоставляется компилятором, используйте #include , а если файл написан вами — #include "mystring.h" . В сложных программах есть риск того, что вы можете подключить один и тот же заголовочный файл несколько раз.

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

Затем создадим другой файл header2.h , содержащий это:

Добавим в нашу программу, main.c , это:

При компиляции программы мы получим ошибку компиляции, потому что T_SIZE будет объявлена дважды (её определение в header1 подключено к двум разным файлам). Мы должны подключить header1 к header2 для того, чтобы header2 компилировался в тех случаях, когда header1 не используется. И как это исправить? Можно написать макрос для header1 :

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

9. Скобки: нужны ли они?

Вот несколько простых правил:

  1. Скобки нужно использовать для изменения порядка выполнения операторов. Например, 3 * (4 + 3) — не то же самое, что 3 * 4 + 3 .
  2. Скобки можно использовать для улучшения читаемости. Здесь они, очевидно, не нужны:

Приоритет оператора || ниже, чем и > . Однако, в этом случае скобки точно не будут лишними:

Скобки стоит использовать в макросах:

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

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

Но в одном месте скобки точно не нужны: в выражении после return. Например, это…

Читайте также:  Red square eclipse обзор

…выполнится так же, как это:

10. Массивы как адреса

Программисты, которые учат Си после какого-то другого языка, часто удивляются, когда Си работает с массивами как с адресами и наоборот. Массив — это контейнер фиксированного размера, а адрес — это число, связанное с местом в памяти; разве они связаны?

Си прав: массив — это просто адрес базы в блоке памяти, а форма записи массива, например, в Java или JavaScript — просто синтаксический сахар.

Присмотритесь к этому коду:

Первый цикл здесь копирует адрес каждого элемента массива в сам массив:

На каждой итерации адрес увеличивается на i . Поэтому адрес переменной _x будет первым элементом, а каждый следующий адрес — адресом _x плюс 1. Когда мы прибавляем 1 к адресу массива, компилятор Си вычисляет подходящий сдвиг в зависимости от типа данных (в нашем случае 4 байта для массива целых).

Второй цикл выводит значения, хранящиеся в массиве, сперва выводя адрес элемента _x + i , затем значение элемента через привычный вид массива _x[i] , а потом содержимое массива с использованием адресной нотации (где оператор * возвращает содержимое памяти по адресу в скобках): *(_x + i) . Во всех случаях значения будут одинаковыми. Это наглядно демонстрирует, что массив и адрес — это одно и то же.

Обратите внимание, что для получения адреса массива вам не нужен оператор & , поскольку компилятор считает, что массив и есть адрес.

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

Содержание

Вариант «простой» [ править ]

Первая программа, которую мы рассмотрим, — это «Hello World» — программа, которая выведет на экран строку текста «Hello, World!» («Здравствуй, мир!») и закончит своё выполнение.

Посмотрим на неё внимательно. Первая строка — #include — означает «включи заголовок stdio.h ». В этом заголовке объявляются функции, связанные с вводом и выводом данных. [1] [2]

Аббревиатуру stdio можно перевести как стандартный ввод-вывод (англ. standard input/output ). Буква «h» после точки означает заголовок (англ. header ). В заголовках (которые как правило представлены отдельными заголовочными файлами) обычно объявляются предоставляемые соответствующими им библиотеками функции, типы данных, константы и определения препроцессора. [3]

Далее идёт определение функции main . Оно начинается с объявления:

что значит: «функция с именем main , которая возвращает целое число (число типа int от англ. integer ) и у которой нет аргументов (void) »

В качестве варианта, стандарт допускает определение функции main как функции двух аргументов ( int argc и char *argv[] — имена, разумеется, могут быть произвольными), что используется для получения доступа к аргументам командной строки из программы. В данном случае, эта возможность не требуется, поэтому функция определена как безаргументная (что также явно разрешено стандартом.) [4]

Английское слово void можно перевести как «пустота». Далее открываются фигурные скобки и идёт тело функции, в конце фигурные скобки закрываются. Функция main — главная функция программы, именно с нее начинается выполнение программы.

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

Это действие, в свою очередь, есть вызов функции puts стандартной библиотеки. [5] В результате выполнения этого вызова, на стандартный вывод (которым, скорее всего, окажется экран или окно на экране) печатается строка Hello, world! .

Затем идёт команда return 0; , которая завершает выполнение функции с возвратом значения 0, определяемого стандартом (для функции main ) как код успешного завершения. [6] [7]

Вариант «классический» [ править ]

Этот вариант отличается использованием функций printf (вместо puts ) и getchar .

В отличие от функции puts , выводящей переданную в качестве аргумента символьную строку, первый и обязательный аргумент функции printf определяет формат вывода. [8]

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

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

Действующая редакция стандарта определяет семь таких комбинаций, причем все они записываются с помощью символа обратной косой черты (см. ASCII коды символов). [9]

Обратим внимание и на следующее новшество:

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

Однако, при запуске непосредственно из графического окружения, отведенное программе окно может закрыться сразу же после завершения программы. Функция getchar [10] ожидает ввод пользователя, тем самым «откладывая» завершение программы ( return ). Какие именно действия могут прервать это ожидание — зависит от системы, однако можно надеяться, что нажатие клавиши ⏎ Enter завершит эту функцию в любой системе.

В некоторых руководствах для этой же цели предлагается функция getch . Однако, эта функция (в отличие от getchar ) не является стандартной и, к тому же, зависима от платформы. Так, в некоторых системах использование getch требует включения файла curses.h и предшествующего вызова функции initscr . В других системах, однако, getch может быть объявлена в conio.h , и выполнение initscr — не требуется.

Вариант «экзотический» [ править ]

Наконец, рассмотрим следующий, выходящий за рамки стандарта, вариант этой программы.

Использование заголовка windows.h может произвести впечатление приемлемости этого варианта кода в рамках только лишь одной конкретной системы. Однако, для использованного здесь интерфейса существует и независимая свободная реализация — Wine, — позволяющая собрать и выполнить данный вариант на таких системах, как, например, GNU/Linux, FreeBSD, Solaris и Mac OS X.

Комментировать
292 просмотров
Комментариев нет, будьте первым кто его оставит

Это интересно
Adblock
detector