|
|
Эта простая программа, появившаяся в первом издании книги «Язык программирования Си» Кернигана и Ритчи, обычно является первой программой большинства учебников Си. Она печатает сообщение «Hello World!» на стандартном устройстве вывода (которым, как правило, является монитор (дисплей), но может быть и файл, какое-либо устройство или область в памяти, в зависимости от того, как отражается стандартное устройство вывода на данной платформе). main() { printf("Hello, World!\n"); } Несмотря на то, что на большинстве современных компиляторов эта программа может быть корректно скомпилирована, она порождает несколько предупреждений на компиляторах стандарта ANSI C. Кроме того, этот код не будет компилироваться, если компилятор жёстко следует стандарту C99, так как в этом случае по умолчанию больше не принимается тип int в качестве возвращаемого значения. Эти сообщения можно убрать, если внести в эту программу несколько небольших изменений: #include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; } В первой строке программы расположена директива препроцессора #include, встретив которую, компилятор заменяет её на полный текст файла, на который она ссылается. В данном случае эта строка будет заменена стандартным заголовочным файлом stdio.h. Угловые скобки указывают компилятору искать файл stdio.h в каталоге стандартных заголовочных файлов. Следующая строка является объявлением функции с именем main. Эта функция в программе Си является особенной, так как выполняется первой при запуске программы, то есть служит так называемой точкой входа в программу. Фигурные скобки после функции main обозначают её определение. Слово int говорит, что функция main возвращает (вычисляет) целое число. Слово void показывает, что функция main не требует от вызывающего ни параметров, ни аргументов. Следующая строка «вызывает» или исполняет функцию printf. Включаемый заголовочный файл stdio.h содержит информацию, описывающую то, как нужно вызывать эту функцию. В данном примере этой функции передаётся единственный аргумент, содержащий текстовую строку «Hello, World!\n». Последовательность \n транслируется в символ «новая строка», который при отображении соответственно обозначает разрыв строки. Функция printf вообще возвращает значение типа int, которое в этом примере полностью отбрасывается. Выражение return заставляет программу прекратить выполнение данной функции (main в этом случае), возвращая вызвавшей функции значение, указанное после ключевого слова return (0 в этом случае). Так как текущая функция — это main, то вызывающим является то, что запустило программу. Последняя фигурная скобка обозначает конец определения функции main.
Целая константа, например 1234, имеет тип int. Константа типа long завершается буквой l или L, например 123456789L: слишком большое целое, которое невозможно представить как int, будет представлено как long. Беззнаковые константы заканчиваются буквой u или U, а окончание ul или UL говорит о том, что тип константы - unsigned long. Константы с плавающей точкой имеют десятичную точку (123.4), или экспоненциальную часть (1е-2), или же и то и другое. Если у них нет окончания, считается, что они принадлежат к типу double. Окончание f или F указывает на тип float, а l или L - на тип long double. Целое значение помимо десятичного может иметь восьмеричное или шестнадцатеричное представление. Если константа начинается с нуля, то она представлена в восьмеричном виде, если с 0x или с 0X, то - в шестнадцатеричном. Например, десятичное целое 31 можно записать как 037 или как 0X1F. Записи восьмеричной и шестнадцатеричной констант могут завершаться буквой L (для указания на тип long) и U (если нужно показать, что константа беззнаковая). Например, константа 0XFUL имеет значение 15 и тип unsigned long.
Символьная константа есть целое, записанное в виде символа, обрамленного одиночными кавычками, например 'x'. Значением символьной константы является числовой код символа из набора символов на данной машине. Например, символьная константа '0' в кодировке ASCII имеет значение 48, которое никакого отношения к числовому значению 0 не имеет. Когда мы пишем '0' , а не какое-то значение (например 46), зависящее от способа кодировки, мы делаем программу независимой от частного значения кода, к тому же она и легче читается. Символьные константы могут участвовать в операциях над числами точно так же, как и любые другие целые, хотя чаще они используются для сравнения с другими символами. Некоторые символы в символьных и строковых константах записываются с помощью эскейп-последовательностей, например \n (символ новой строки); такие последовательности изображаются двумя символами, но обозначают один. Кроме того, произвольный восьмеричный код можно задать в виде '\ooo' где ооо - одна, две или три восьмеричные цифры (0 … 7) или '\xhh' где hh - одна, две или более шестнадцатеричные цифры (0...9, а...f, A...F). Таким образом, мы могли бы написать #define VTAB '013' /* вертикальная табуляция в ASCII */ #define BELL '\007' /* звонок в ASCII */ или в шестнадцатеричном виде: #define VTAB '\xb' /* вертикальная табуляций в ASCII */ #define BELL '\x7' /* звонок в ASCII */ Полный набор эскейп-последовательностей таков: \а сигнал-звонок \b возврат-на-шаг (забой) \f перевод-страницы \n новая-строка \r возврат-каретки \t горизонтальная-табуляция \v вертикальная-табуляция \\ обратная наклонная черта \? знак вопроса \' одиночная кавычка \" двойная кавычка \ooo восьмеричный код \xhh шестнадцатеричный код Символьная константа '\0' - это символ с нулевым значением, так называемый символ null. Вместо просто 0 часто используют запись '\0', чтобы подчеркнуть символьную природу выражения, хотя и в том и другом случае запись обозначает нуль. Константные выражения - это выражения, оперирующие только с константами. Такие выражения вычисляются во время компиляции, а не во время выполнения, и поэтому их можно использовать в любом месте, где допустимы константы, как, например, в #define MAXLINE 1000 char line[MAXLINE+1]; или в #define LEAP 1 /* in leap years - в високосные годы */ int days[31+28+LEAP+31+30+31+30+31+31+30+31+30+31];
Строковая константа, или строковый литерал, - это нуль или более символов, заключенных в двойные кавычки, как, например, "Я строковая константа” или "" /* пустая строка */ Кавычки не входят в строку, а служат только ее ограничителями. Так же, как и в символьные константы, в строки можно включать эскейп-последовательности; \", например, представляет собой двойную кавычку. Строковые константы можно конкатенировать ("склеивать”) во время компиляции; например, запись двух строк "Здравствуй," " мир!" эквивалентна записи одной следующей строки: "Здравствуй, мир!" Указанное свойство позволяет разбивать длинные строки на части и располагать эти части на отдельных строчках. Фактически строковая константа — это массив символов. Во внутреннем представлении строки в конце обязательно присутствует нулевой символ '\0' , поэтому памяти для строки требуется на один байт больше, чем число символов, расположенных между двойными кавычками. Это означает, что на длину задаваемой строки нет ограничения, но чтобы определить ее длину, требуется просмотреть всю строку. Функция strlen(s) вычисляет длину строки s без учета завершающего ее символа '\0' . Ниже приводится наша версия этой функции: /* strlen: возвращает длину строки s */ int strlen(char s[]) { int i = 0; while (s[i] != '\0') ++i; return i; } Функция strlen и некоторые другие, применяемые к строкам, описаны в стандартном заголовочном файле <string.h>. Будьте внимательны и помните, что символьная константа и строка, содержащая один символ, не одно и то же: 'x' не то же самое, что "x". Запись 'x' обозначает целое значение, равное коду буквы x из стандартного символьного набора, а запись "x" - массив символов, который содержит один символ (букву x) и '\0'.
Си имеет ту же систему типов, что и другие потомки Алгола, такие как Паскаль. Существуют типы для целых чисел различных размеров, имеющих знак и не имеющих его, чисел с плавающей запятой, символов, перечисляемых типов (enum) и структур (struct). Кроме того, язык Си имеет тип объединения (union), позволяющий программисту создавать структуры, способные хранить данные разных типов, но только одного типа единовременно. Массивы позволяют объединить несколько элементов вышеуказанных типы данных под одним именем и предоставить к ним доступ по целочисленному интексу
В Си существует всего лишь несколько базовых типов: char - единичный байт, который может содержать один символ из допустимого символьного набора; int - целое, обычно отображающее естественное представление целых в машине; float - число с плавающей точкой одинарной точности; double - число с плавающей точкой двойной точности. Имеется также несколько квалификаторов, которые можно использовать вместе с указанными базовыми типами. Например, квалификаторы short (короткий) и long (длинный) применяются к целым: short int sh; long int counter; В таких объявлениях слово int можно опускать, что обычно и делается. Если только не возникает противоречий со здравым смыслом, short int и long int должны быть разной длины, а int соответствовать естественному размеру целых на данной машине. Чаще всего для представления целого, описанного с квалификатором short, отводится 16 бит, с квалификатором long - 32 бита, а значению типа int - или 16, или 32 бита. Разработчики компилятора вправе сами выбирать подходящие размеры, сообразуясь с характеристиками своего компьютера и соблюдая следующие ограничения: значения типов short и int представляются по крайней мере 16 битами; типа long - по крайней мере 32 битами; размер short не больше размера int, который в свою очередь не больше размера long. Квалификаторы signed (со знаком) или unsigned (без знака) можно применять к типу char и любому целочисленному типу. Значения unsigned всегда положительны или равны нулю и подчиняются законам арифметики по модулю 2n, где n - количество бит в представлении типа. Так, если значению char отводится 8 битов, то unsigned char имеет значения в диапазоне от 0 до 255, a signed char – от -128 до 127 (в машине с двоичным дополнительным кодом). Являются ли значения типа просто char знаковыми или беззнаковыми, зависит от реализации, но в любом случае коды печатаемых символов положительны. Тип long double предназначен для арифметики с плавающей точкой повышенной точности. Как и в случае целых, размеры объектов с плавающей точкой зависят от реализации; float, double и long double могут представляться одним размером, а могут - двумя или тремя разными размерами. Именованные константы для всех размеров вместе с другими характеристиками машины и компилятора содержатся в стандартных заголовочных файлах <limits.h> и <float.h>
В Си имеется еще один вид константы - константа перечисления. Перечисление - это список целых констант, как, например, в enum boolean {NO, YES}; Первое имя в enum имеет значение 0, следующее - 1 и т.д. (если для значений констант не было явных спецификаций). Если не все значения специфицированы, то они продолжают прогрессию, начиная от последнего специфицированного значения, как в следующих двух примерах: enum escapes { BELL = '\a', BACKSPACE = '\b', TAB = '\t', NEWLINE = '\n', VTAB = '\v', RETURN = '\r'}; enum months { JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC}; /* FEB есть 2, MAR есть 3 и т.д. */ Имена в различных перечислениях должны отличаться друг от друга. Значения внутри одного перечисления могут совпадать. Средство enum обеспечивает удобный способ присвоить константам имена, причем в отличие от #define значения констант при этом способе могут генерироваться автоматически. Хотя разрешается объявлять переменные типа enum, однако компилятор не обязан контролировать, входят ли присваиваемые этим переменным значения в их тип. Но сама возможность такой проверки часто делает enum лучше, чем #define. Кроме того, отладчик получает возможность печатать значения переменных типа enum в символьном виде.
Все переменные должны быть объявлены раньше, чем будут использоваться, при этом некоторые объявления могут быть получены неявно - из контекста. Объявление специфицирует тип и содержит список из одной или нескольких переменных этого типа, как, например, в int lower, upper, step; char с, line[1000]; Переменные можно распределять по объявлениям произвольным образом, так что указанные выше списки можно записать и в следующем виде: int lower; int upper; int step; char c; char line[1000]; Последняя форма записи занимает больше места, тем не менее она лучше, поскольку позволяет добавлять к каждому объявлению комментарий. Кроме того, она более удобна для последующих модификаций. В своем объявлении переменная может быть инициализирована, как, например: char esc = '\\'; int i = 0; int limit = MAXLINE+1; float eps = 1.0e-5; Инициализация неавтоматической переменной осуществляется только один раз - перед тем, как программа начнет выполняться, при этом начальное значение должно быть константным выражением. Явно инициализируемая автоматическая переменная получает начальное значение каждый раз при входе в функцию или блок, ее начальным значением может быть любое выражение. Внешние и статические переменные по умолчанию получают нулевые значения. Автоматические переменные, явным образом не инициализированные, содержат неопределенные значения ("мусор”). К любой переменной в объявлении может быть применен квалификатор const для указания того, что ее значение далее не будет изменяться. const double е = 2.71828182845905; const char msg[] = "предупреждение: "; Применительно к массиву квалификатор const указывает на то, что ни один из его элементов не будет меняться. Указание const можно также применять к аргументу- массиву, чтобы сообщить, что функция не изменяет этот массив: int strlen(const char[]); Реакция на попытку изменить переменную, помеченную квалификатором const зависит от реализации компилятора.
Помимо просто эстетических соображений, для применения typedef существуют две важные причины. Первая - параметризация программы, связанная с проблемой переносимости. Если с помощью typedef объявить типы данных, которые, возможно, являются машинно-зависимыми, то при переносе программы на другую машину потребуется внести изменения только в определения typedef. Одна из распространенных ситуаций - использование typedef-имен для варьирования целыми величинами. Для каждой конкретной машины это предполагает соответствующие установки short, int или long, которые делаются аналогично установкам стандартных типов, например size_t и ptrdiff_t. Вторая причина, побуждающая к применению typedef,- желание сделать более ясным текст программы. Тип, названный Тreeptr (от английских слов tree - дерево и pointer - указатель), более понятен, чем тот же тип, записанный как указатель на некоторую сложную структуру.
На платформе x86/win32 целочисленные типы данных имеют следующие размеры в байтах: char – 1 short – 2 int – 4 long – 4
На платформе x86 размеры вещественных типов данных (в байтах) следующие: float - 4 double - 8 long double - 10 (в реализациях компилятора Си от Microsoft размер long double равен 8 байтам)
Структура - это одна или несколько переменных (возможно, различных типов), которые для удобства работы с ними сгруппированы под одним именем. (В некоторых языках, в частности в Паскале, структуры называются записями.) Структуры помогают в организации сложных данных (особенно в больших программах), поскольку позволяют группу связанных между собой переменных трактовать не как множество отдельных элементов, а как единое целое. Традиционный пример структуры - строка платежной ведомости. Она содержит такие сведения о служащем, как его полное имя, адрес, номер карточки социального страхования, зарплата и т. д. Некоторые из этих характеристик сами могут быть структурами: например, полное имя состоит из нескольких компонент (фамилии, имени и отчества); аналогично адрес, и даже зарплата. Другой пример (более типичный для Си) - из области графики: точка есть пара координат, прямоугольник есть пара точек и т. д. Главные изменения, внесенные стандартом ANSI в отношении структур, - это введение для них операции присваивания. Структуры могут копироваться, над ними могут выполняться операции присваивания, их можно передавать функциям в качестве аргументов, а функции могут возвращать их в качестве результатов. В большинстве компиляторов уже давно реализованы эти возможности, но теперь они точно оговорены стандартом. Для автоматических структур и массивов теперь также допускается инициализация. Объявление структуры начинается с ключевого слова struct и содержит список объявлений, заключенный в фигурные скобки. За словом struct может следовать имя, называемое тегом структуры Тег дает название структуре данного вида и далее может служить кратким обозначением той части объявления, которая заключена в фигурные скобки. Перечисленные в структуре переменные называются элементами. Имена элементов и тегов без каких-либо коллизий могут совпадать с именами обычных переменных (т. е. не элементов), так как они всегда различимы по контексту. Более того, одни и те же имена элементов могут встречаться в разных структурах, хотя, если следовать хорошему стилю программирования, лучше одинаковые имена давать только близким по смыслу объектам. Объявление структуры определяет тип. За правой фигурной скобкой, закрывающей список элементов, могут следовать переменные точно так же, как они могут быть указаны после названия любого базового типа. Таким образом, выражение struct {...} x, y, z; с точки зрения синтаксиса аналогично выражению int х, у, z; в том смысле, что и то и другое объявляет x, y и z переменными указанного типа; и то и другое приведет к выделению памяти соответствующего размера. Объявление структуры, не содержащей списка переменных, не резервирует памяти; оно просто описывает шаблон, или образец структуры. Однако если структура имеет тег, то этим тегом далее можно пользоваться при определении структурных объектов. Например, с помощью заданного выше описания структуры point строка struct point pt; определяет структурную переменную pt типа struct point. Структурную переменную при ее определении можно инициализировать, формируя список инициализаторов ее элементов в виде константных выражений: struct point maxpt = {320, 200}; Инициализировать автоматические структуры можно также присваиванием или обращением к функции, возвращающей структуру соответствующего типа. Доступ к отдельному элементу структуры осуществляется посредством конструкции вида: имя-структуры.элемент Оператор доступа к элементу структуры . соединяет имя структуры и имя элемента. Чтобы напечатать, например, координаты точки pt, годится следующее обращение к printf: printf("%d, %d", pt.x, pt.y); Структуры могут быть вложены друг в друга. Одно из возможных представлений прямоугольника - это пара точек на углах одной из его диагоналей: struct rect { struct point pt1; struct point pt2; }; Структура rect содержит две структуры point. Если мы объявим screen как struct rect screen; то screen.pt1.x обращается к координате x точки pt1 из screen.
Объединение - это переменная, которая может содержать (в разные моменты времени) объекты различных типов и размеров. Все требования относительно размеров и выравнивания выполняет компилятор. Объединения позволяют хранить разнородные данные в одной и той же области памяти без включения в программу машинно-зависимой информации. Эти средства аналогичны вариантным записям в Паскале. Примером использования объединений мог бы послужить сам компилятор, заведующий таблицей символов, если предположить, что константа может иметь тип int, float или являться указателем на символ и иметь тип char *. Значение каждой конкретной константы должно храниться в переменной соответствующего этой константе типа. Работать с таблицей символов всегда удобнее, если значения занимают одинаковую по объёму память и запоминаются в одном и том же месте независимо от своего типа. Цель введения в программу объединения - иметь переменную, которая бы на законных основаниях хранила в себе значения нескольких типов. Синтаксис объединений аналогичен синтаксису структур.
Количество используемых индексов массива может быть различным. Массивы с одним индексом называют одномерными, с двумя — двумерными и т. д. Одномерный массив нестрого соответствует вектору в математике, двумерный -- матрице. Чаще всего применяются массивы с одним или двумя индексами, реже — с тремя, еще большее количество индексов встречается крайне редко.
Указатель (англ. pointer) — переменная, диапазон значений которой состоит из адресов ячеек памяти и специального значения — нулевого адреса. Значение нулевого адреса не является реальным адресом и используется только для обозначения того, что указатель в данный момент не может использоваться для обращения ни к какой ячейке памяти. Указатели применяются в двух различных сферах. Во-первых, они позволяют использовать некоторые выгоды косвенной адресации, широко применяемой в программировании на языках ассемблера. Во-вторых, указатели предлагают метод динамического управления памятью: их можно использовать для доступа к области с динамическим размещением памяти, обычно называемой кучей, или динамической памятью. Переменные, размещаемые в куче, называются динамическими. Часто они не содержат связанных с ними идентификаторов, и ссылаться на них можно только с помощью указателей и ссылок.
Хранение данных Одной из самых важных функций любого языка программирования является предоставление возможностей для управления памятью и объектами, хранящимися в ней. В Си есть три разных способа выделения памяти для объектов: Статическое выделение памяти: пространство для объектов создаётся в области хранения данных кода программы в момент компиляции; время жизни таких объектов совпадает со временем жизни этого кода. Автоматическое выделение памяти: объекты можно временно хранить в стеке; эта память затем автоматически освобождается и может быть использована снова, после того, как программа выходит из блока, использующего её. Динамическое выделение памяти: блоки памяти нужного размера могут запрашиваться во время выполнения программы с помощью библиотечных функций malloc, realloc и free из области памяти, называемой кучей. Эти блоки освобождаются и могут быть использованы снова после вызова для них функции free. Все три этих способа хранения данных пригодны в различных ситуациях и имеют свои преимущества и недостатки. Например, статическое выделение памяти не имеет накладных расходов по выделению, автоматическое выделение — лишь малые расходы при выделении, а вот динамическое выделение потенциально требует больших расходов и на выделение, и на освобождение памяти. С другой стороны, память стека гораздо больше ограничена, чем статическая, или память в куче. Только динамическая память может использоваться в случаях, когда размер используемых объектов заранее неизвестен. Большинство программ на Си интенсивно используют все три этих способа. Там, где это возможно, предпочтительным является автоматическое или статическое выделение памяти, потому что такой способ хранения объектов управляется компилятором, что освобождает программиста от трудностей ручного выделения и освобождения памяти, как правило, служащего источником трудно отыскиваемых ошибок в программе. К сожалению, многие структуры данных имеют переменный размер во время выполнения программы, поэтому из-за того, что автоматически и статически выделенные области должны иметь известный фиксированный размер во время компиляции, очень часто требуется использовать динамическое выделение. Массивы переменного размера — самый распространённый пример такого использования памяти.
Язык Си был создан уже после внедрения стандарта ASCII, поэтому использует почти все его графические символы (нет только $ @ `). Более старые языки вынуждены были обходиться более скромным набором — так, Фортран, Лисп и Кобол использовали только круглые скобки ( ), а в Си есть и круглые ( ), и квадратные [ ], и фигурные { }. Кроме того, в Си различаются заглавные и строчные буквы, а более старые языки использовали только заглавные.
Бинарными (т. е. с двумя операндами) арифметическими операторами являются +, -, *, /, а также оператор деления по модулю %. Деление целых сопровождается отбрасыванием дробной части, какой бы она ни была. Выражение x % y дает остаток от деления x на y и, следовательно, нуль, если x делится на y нацело. Например, год является високосным, если он делится на 4, но не делится на 100. Кроме того, год является високосным, если он делится на 400. Следовательно, if ( ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0) ) printf("%d високосный год\n", year); else printf("%d невисокосный год\n", year); Оператор % к операндам типов float и double не применяется. В какую сторону (в сторону увеличения или уменьшения числа) будет усечена дробная часть при выполнении / и каким будет знак результата операции % с отрицательными операндами, зависит от машины. Бинарные операторы + и - имеют одинаковый приоритет, который ниже приоритета операторов *, / и %, который в свою очередь ниже приоритета унарных операторов + и -. Арифметические операции одного приоритетного уровня выполняются слева направо.
Операторами отношения являются > >= < <= Все они имеют одинаковый приоритет. Сразу за ними идет приоритет операторов сравнения на равенство: == != Операторы отношения имеют более низкий приоритет, чем арифметические, поэтому выражение вроде i < lim-1 будет выполняться так же, как i < (lim-1), т.е. как мы и ожидаем. Более интересны логические операторы && и ||. Выражения, между которыми стоят операторы && или ||, вычисляются слева направо. Вычисление прекращается, как только становится известна истинность или ложность результата. for (i = 0; i < lim-1 && (с = getchar()) != EOF && с != '\n'; ++i) s[i] = c; Прежде чем читать очередной символ, нужно проверить, есть ли для него место в массиве s, иначе говоря, сначала необходимо проверить соблюдение условия i < lim-1. Если это условие не выполняется, мы не должны продолжать вычисление, в частности читать следующий символ. Так же было бы неправильным сравнивать c и EOF до обращения к getchar; следовательно, и вызов getchar, и присваивание должны выполняться перед указанной проверкой. Приоритет оператора && выше, чем таковой оператора ||, однако их приоритеты ниже, чем приоритет операторов отношения и равенства. Из сказанного следует, что выражение вида i < lim-1 && (с = getchar()) != '\n' && с != EOF не нуждается в дополнительных скобках. Но, так как приоритет != выше, чем приоритет присваивания, в (с = getchar()) != '\n' скобки необходимы, чтобы сначала выполнить присваивание, а затем сравнение с '\n'. По определению численным результатом вычисления выражения отношения или логического выражения является 1, если оно истинно, и 0, если оно ложно. Унарный оператор ! преобразует ненулевой операнд в 0, а нуль в 1. Обычно оператор ! используют в конструкциях вида if (!valid) что эквивалентно if (valid == 0) Трудно сказать, какая из форм записи лучше. Конструкция вида !valid хорошо читается ("если не правильно”), но в более сложных выражениях может оказаться, что ее не так-то легко понять.
Каждый раз, когда встречается символ, отличный от c, он копируется в текущую j-ю позицию, и только после этого переменная j увеличивается на 1, подготавливаясь таким образом к приему следующего символа. Это в точности совпадает со следующими действиями: if (s[i] != с) { s[j] = s[i]; j++; }
В Си имеются шесть операторов для манипулирования с битами. Их можно применять только к целочисленным операндам, т. е. к операндам типов char, short, int и long, знаковым и беззнаковым. & - побитовое И | - побитовое ИЛИ ^ - побитовое исключающее ИЛИ. << - сдвиг влево. >> - сдвиг вправо. ~ - побитовое отрицание (унарный). Оператор & (побитовое И) часто используется для обнуления некоторой группы разрядов. Например n = n & 0177; обнуляет в n все разряды, кроме младших семи. Оператор | (побитовое ИЛИ) применяют для установки разрядов; так, x = x | SET_ON; устанавливает единицы в тех разрядах x, которым соответствуют единицы в SET_ON. Оператор ^ (побитовое исключающее ИЛИ) в каждом разряде установит 1, если соответствующие разряды операндов имеют различные значения, и 0, когда они совпадают. Поразрядные операторы & и | следует отличать от логических операторов && и ||, которые при вычислении слева направо дают значение истинности. Например, если x равно 1, а y равно 2, то x & y даст нуль, а x && y - единицу. Операторы << и >> сдвигают влево или вправо свой левый операнд на число битовых позиций, задаваемое правым операндом, который должен быть неотрицательным. Так, x << 2 сдвигает значение x влево на 2 позиции, заполняя освобождающиеся биты нулями, что эквивалентно умножению x на 4. Сдвиг вправо беззнаковой величины всегда сопровождается заполнением освобождающихся разрядов нулями. Сдвиг вправо знаковой величины на одних машинах происходит с распространением знака ("арифметический сдвиг”), на других - с заполнением освобождающихся разрядов нулями ("логический сдвиг”). Унарный оператор ~ поразрядно "обращает” целое т. е. превращает каждый единичный бит в нулевой и наоборот. Например x = x & ~077 обнуляет в x последние 6 разрядов. Заметим, что запись x & ~077 не зависит от длины слова, и, следовательно, она лучше, чем x & 0177700, поскольку последняя подразумевает, что x занимает 16 битов. Не зависимая от машины форма записи ~077 не потребует дополнительных затрат при счете, так как ~077 - константное выражение, которое будет вычислено во время компиляции.
Для иллюстрации некоторых побитовых операций рассмотрим функцию getbits(x, p, n), которая формирует поле в n битов, вырезанных из x, начиная с позиции p, прижимая его к правому краю. Предполагается, что 0-й бит - крайний правый бит, а n и p- осмысленные положительные числа. Например, getbits(x,4,3) вернет в качестве результата 4, 3 и 2-й биты значения x, прижимая их к правому краю. Вот эта функция: /* getbits: получает n бит, начиная с p-й позиции */ unsigned getbits(unsigned x, int p, int n) { return (x >> (p+1-n)) & ~(~0 << n); } Выражение x >> (р+1-n) сдвигает нужное нам поле к правому краю. Константа ~0 состоит из одних единиц, и ее сдвиг влево на n бит (~0 << n) приведет к тому, что правый край этой константы займут n нулевых разрядов. Еще одна операция побитовой инверсии ~ позволяет получить справа n единиц.
Выражение i = i + 2; в котором стоящая слева переменная повторяется и справа, можно написать в сжатом виде: i += 2; Оператор +=, как и =, называется оператором присваивания. Большинству бинарных операторов (аналогичных + и имеющих левый и правый операнды) соответствуют операторы присваивания op=, где op - один из операторов + - * / % << >> & ^ | Если выр1 и выр2 - выражения, то выр1 op= выр2 Эквивалентно выр1 = (выр1) op (выр2) с той лишь разницей, что выр1 вычисляется только один раз. Обратите внимание на скобки вокруг выр2: x *= y + 1 эквивалентно x = x * (y + 1) но не x=x*y+1 Помимо краткости операторы присваивания обладают тем преимуществом, что они более соответствуют тому, как человек мыслит. Мы говорим "прибавить 2 к i" или "увеличить i на 2", а не "взять i, добавить 2 и затем вернуть результат в i", так что выражение i+=2 лучше, чем i=i+2. Кроме того, в сложных выражениях вроде yyval[yypv[p3+p4] + yypv[p1+p2]]+= 2 благодаря оператору присваивания += запись становится более легкой для понимания, так как читателю при такой записи не потребуется старательно сравнивать два длинных выражения, совпадают ли они, или выяснять, почему они не совпадают. Следует иметь в виду и то, что подобные операторы присваивания могут помочь компилятору сгенерировать более эффективный код. Мы уже видели, что присваивание вырабатывает значение и может применяться внутри выражения: вот самый расхожий пример: while ((с = getchar()) != EOF) В выражениях встречаются и другие операторы присваивания (+=, -= и т. д.), хотя и реже. Типом и значением любого выражения присваивания являются тип и значение его левого операнда после завершения присваивания.
В качестве примера приведем функцию bitcount, подсчитывающую число единичных битов в своем аргументе целочисленного типа. Независимо от машины, на которой будет работать эта программа, объявление аргумента x как unsigned гарантирует, что при правом сдвиге освобождающиеся биты будут заполняться нулями, а не знаковым битом.
Если операнды оператора принадлежат к разным типам, то они приводятся к некоторому общему типу. Приведение выполняется в соответствии с небольшим числом правил. Обычно автоматически производятся лишь те преобразования, которые без какой-либо потери информации превращают операнды с меньшим диапазоном значений в операнды с большим диапазоном, как, например, преобразование целого в число с плавающей точкой в выражении вроде f+i. Выражения, не имеющие смысла, например число с плавающей точкой в роли индекса, не допускаются. Выражения, в которых могла бы теряться информация (скажем, при присваивании длинных целых переменным более коротких типов или при присваивании значений с плавающей точкой целым переменным), могут повлечь за собой предупреждение, но они допустимы. Значения типа char - это просто малые целые, и их можно свободно использовать в арифметических выражениях, что значительно облегчает всевозможные манипуляции с символами. В качестве примера приведем простенькую реализацию функции atoi, преобразующей последовательность цифр в ее числовой эквивалент. /* atoi: преобразование s в целое */ int atoi(char s[]) { int i, n; n = 0; for (i = 0; s[i] >= '0' && s[i] <= '9';++i) n = 10 * n + (s[i] - '0'); return n; } Выражение s[i] -'0' дает числовое значение символа, хранящегося в s[i], так как значения '0', '1' и пр. образуют непрерывную возрастающую последовательность. Другой пример приведения char к int связан с функцией lower, которая одиночный символ из набора ASCII, если он является заглавной буквой, превращает в строчную. Если же символ не является заглавной буквой, lower его не изменяет. /* lower: преобразование c в строчную, только для ASCII */ int lower(int c) { if (c >= 'A' && c <='Z') return c +'a'-'A'; else return c; } В случае ASCII эта программа будет работать правильно, потому что между одноименными буквами верхнего и нижнего регистров - одинаковое расстояние (если их рассматривать как числовые значения). Кроме того, латинский алфавит - плотный, т. е. между буквами A и Z расположены только буквы. Для набора EBCDIC последнее условие не выполняется, и поэтому наша программа в этом случае будет преобразовывать не только буквы. Стандартный заголовочный файл <ctype.h> определяет семейство функций, которые позволяют проверять и преобразовывать символы независимо от символьного набора. Например, функция tolower(c) возвращает букву c в коде нижнего регистра, если она была в коде верхнего регистра, поэтому tolower - универсальная замена функции lower, рассмотренной выше. Аналогично проверку c >= '0' && c <= '9' можно заменить на isdigit(c) Существует одна тонкость, касающаяся преобразования символов в целые числа: язык не определяет, являются ли переменные типа char знаковыми или беззнаковыми. При преобразовании char в int может ли когда- нибудь получиться отрицательное целое? На машинах с разной архитектурой ответы могут отличаться. На некоторых машинах значение типа char с единичным старшим битом будет превращено в отрицательное целое (посредством "распространения знака”). На других - преобразование char в int осуществляется добавлением нулей слева, и, таким образом, получаемое значение всегда положительно. Гарантируется, что любой символ из стандартного набора печатаемых символов никогда не будет отрицательным числом, поэтому в выражениях такие символы всегда являются положительными операндами. Но произвольный восьмибитовый код в переменной типа char на одних машинах может быть отрицательным числом, а на других - положительным. Для совместимости переменные типа char, в которых хранятся несимвольные данные, следует специфицировать явно как signed или unsigned. Отношения вроде i > j и логические выражения, перемежаемые операторами && и ||, определяют выражение-условие, которое имеет значение 1, если оно истинно, и 0, если ложно. Так, присваивание d = c >= '0' && c <= '9' установит d в значение 1, если c есть цифра, и 0 в противном случае. Однако функции, подобные isdigit, в качестве истины могут выдавать любое ненулевое значение. В местах проверок внутри if, while, for и пр. "истина” просто означает "не нуль”. Неявные арифметические преобразования, как правило, осуществляются естественным образом. В общем случае, когда оператор вроде + или * с двумя операндами (бинарный оператор) имеет разнотипные операнды, прежде чем операция начнет выполняться, "низший” тип повышается до "высшего”. Результат будет иметь высший тип. Если же в выражении нет беззнаковых операндов, можно удовлетвориться следующим набором неформальных правил: Если какой-либо из операндов принадлежит типу long double, то и другой приводится к long double. В противном случае, если какой-либо из операндов принадлежит типу double, то и другой приводится к double. В противном случае, если какой-либо из операндов принадлежит типу float, то и другой приводится к float. В противном случае операнды типов char и short приводятся к int. И наконец, если один из операндов типа long, то и другой приводится к long. Заметим, что операнды типа float не приводятся автоматически к типу double; в этом данная версия языка отличается от первоначальной. Вообще говоря, математические функции, аналогичные собранным в библиотеке <math.h>, базируются на вычислениях с двойной точностью. В основном float используется для экономии памяти на больших массивах и не так часто - для ускорения счета на тех машинах, где арифметика с двойной точностью слишком дорога с точки зрения расхода времени и памяти. Правила преобразования усложняются с появлением операндов типа unsigned. Проблема в том, что сравнения знаковых и беззнаковых значений зависят от размеров целочисленных типов, которые на разных машинах могут отличаться. Предположим, что значение типа int занимает 16 битов, а значение типа long - 32 бита. Тогда -1L < 1U, поскольку 1U принадлежит типу unsigned int и повышается до типа signed long. Но –1L >1UL, так как -1L повышается до типа unsigned long и воспринимается как большое положительное число. Преобразования имеют место и при присвоениях: значение правой части присвоения приводится к типу левой части, который и является типом результата. Тип char превращается в int путем распространения знака или другим описанным выше способом. Тип long int преобразуются в short int или в значения типа char путем отбрасывания старших разрядов. Так, в int i; char c; i = c; c = i; значение c не изменится. Это справедливо независимо от того, распространяется знак при переводе char в int или нет. Однако, если изменить очередность присваиваний, возможна потеря информации. Если x принадлежит типу float, а i - типу int, то и x=i, и i=z вызовут преобразования, причем перевод float в int сопровождается отбрасыванием дробной части. Если double переводится во float, то значение либо округляется, либо обрезается; это зависит от реализации. Так как аргумент в вызове функции есть выражение, при передаче его функции также возможно преобразование типа. При отсутствии прототипа (функции аргументы тина char и short переводятся в int, a float - в double. Вот почему мы объявляли аргументы типа int или double даже тогда, когда в вызове функции использовали аргументы типа char или float. И наконец, для любого выражения можно явно ("насильно”) указать преобразование его типа, используя унарный оператор, называемый приведением. Конструкция вида (имя-типа) выражение приводит выражение к указанному в скобках типу по перечисленным выше правилам. Смысл операции приведения можно представить себе так: выражение как бы присваивается некоторой переменной указанного типа, и эта переменная используется вместо всей конструкции. Например, библиотечная функция sqrt рассчитана на аргумент типа double и выдает чепуху, если ей подсунуть что-нибудь другое (sqrt описана в ). Поэтому, если n имеет целочисленный тип, мы можем написать sqrt((double) n) и перед тем, как значение n будет передано функции, оно будет переведено в double. Заметим, что операция приведения всего лишь вырабатывает значение n указанного типа, но саму переменную n не затрагивает. Приоритет оператора приведения столь же высок, как и любого унарного оператора, что зафиксировано в таблице, помещенной в конце этой главы. В том случае, когда аргументы описаны в прототипе функции, как тому и следует быть, при вызове функции нужное преобразование выполняется автоматически. Так, при наличии прототипа функции sqrt: double sqrt(double); перед обращением к sqrt в присваивании root2 = sqrt(2); целое 2 будет переведено в значение double 2.0 автоматически без явного указания операции приведения. Операцию приведения проиллюстрируем на переносимой версии генератора псевдослучайных чисел и функции, инициализирующей "семя”. И генератор, и функция входят в стандартную библиотеку. unsigned long int next = 1; /* rand: возвращает псевдослучайное целое 0...32767 */ int rand(void) { next = next * 1103515245 + 12345; return (unsigned int)(next/65536) % 32768; } /* srand: устанавливает "семя” для rand() */ void srand(unsigned int seed) { next = seed; }
Инструкции if (a > b) z = a; else z = b; пересылают в z большее из двух значений a и b. Условное выражение, написанное с помощью тернарного (т. е. имеющего три операнда) оператора "? : ", представляет собой другой способ записи этой и подобных ей конструкций. В выражении выр1 ? выр2 : выр3 первым вычисляется выражение выр1. Если его значение не нуль (истина), то вычисляется выражение выр2, и значение этого выражения становится значением всего условного выражения. В противном случае вычисляется выражение выр3 и его значение становится значением условного выражения. Следует отметить, что из выражений выр2 и выр3 вычисляется только одно из них. Таким образом, чтобы установить в z большее из a и b, можно написать z = (a > b) ? a : b; /* z = max(a, b) */ Следует заметить, что условное выражение и в самом деле является выражением, и его можно использовать в любом месте, где допускается выражение. Если выр2 и выр3 принадлежат разным типам, то тип результата определяется правилами преобразования. Например, если f имеет тип float, а n - тип int, то типом выражения (n > 0) ? f : n будет float вне зависимости от того, положительно значение n или нет. Заключать в скобки первое выражение в условном выражении не обязательно, так как приоритет ?: очень низкий (более низкий приоритет имеет только присваивание), однако мы рекомендуем всегда это делать, поскольку благодаря обрамляющим скобкам условие в выражении лучше воспринимается.
Примечание. Унарные операторы +, -, * и & имеют более высокий приоритет, чем те же бинарные операторы. Заметим, что приоритеты побитовых операторов &, ^ и | ниже, чем приоритет == и != , из-за чего в побитовых проверках, таких как if ((x & MASK) == 0) ... чтобы получить правильный результат, приходится использовать скобки. Си подобно многим языкам не фиксирует очередность вычисления операндов оператора (за исключением &&, ||, ?: и ,). Например, в инструкции вида x = f() + g(); f может быть вычислена раньше g или наоборот. Из этого следует, что если одна из функций изменяет значение переменной, от которой зависит другая функция, то помещаемый в x результат может зависеть от очередности вычислений. Чтобы обеспечить нужную последовательность вычислений, промежуточные результаты можно запоминать во временных переменных. Очередность вычисления аргументов функции также не определена, поэтому на разных компиляторах printf("%d %d\n", ++n, power(2, n)); /* НЕВЕРНО*/ может давать несовпадающие результаты. Результат вызова функции зависит от того, когда компилятор сгенерирует команды увеличения n - до или после обращения к power. Чтобы обезопасить себя от возможного побочного эффекта, достаточно написать ++n; printf("%d %d\n", n, power(2, n)); Обращения к функциям, вложенные присвоения, инкрементные и декрементные операторы дают "побочный эффект”, проявляющийся в том, что при вычислении выражения значения некоторых переменных изменяются. В любом выражении с побочным эффектом может быть скрыта трудно просматриваемая зависимость результата выражения от очередности изменения значений переменных, входящих в выражение. В такой, например, типично неприятной ситуации a[i] = i++; /* I.B.: doubtful example */ возникает вопрос: массив a индексируется старым или измененным значением i? Компиляторы могут по-разному генерировать программу, что проявится в интерпретации данной записи. Стандарт сознательно устроен так, что большинство подобных вопросов оставлено на усмотрение компиляторов, так как лучший порядок вычислений определяется архитектурой машины. Стандартом только гарантируется, что все побочные эффекты при вычислении аргументов проявятся перед входом в функцию. Правда, в примере с printf это нам не поможет. Мораль такова: писать программы, зависящие от очередности вычислений, - плохая практика, какой бы язык вы ни использовали. Естественно, надо знать, чего следует избегать, но если вы не знаете, как образуются побочные эффекты на разных машинах, то лучше и не рассчитывать выиграть на особенностях частной реализации.
Выражение, скажем x = 0, или i++, или printf(…), становится инструкцией, если в конце его поставить точку с запятой, например: x = 0; i++; printf(...); В Си точка с запятой является заключающим символом инструкции, а не разделителем, как в языке Паскаль. Фигурные скобки { и } используются для объединения объявлений и инструкций в составную инструкцию, или блок, чтобы с точки зрения синтаксиса эта новая конструкция воспринималась как одна инструкция. Фигурные скобки, обрамляющие группу инструкций, образующих тело функции, - это один пример; второй пример - это скобки, объединяющие инструкции, помещенные после if, else, while или for.После правой закрывающей фигурной скобки в конце блока точка с запятой не ставится.
Инструкция if-else используется для принятия решения. Формально ее синтаксисом является: if (выражение) инструкция1 else инструкция2 причем else-часть может и отсутствовать. Сначала вычисляется выражение, и, если оно истинно (т. е. отлично от нуля), выполняется инструкция1. Если выражение ложно (т. е. его значение равно нулю) и существует else-часть, то выполняется инструкция2. Так как if просто проверяет числовое значение выражения, условие иногда можно записывать в сокращенном виде. Так, запись if (выражение) короче, чем if ( выражение != 0 ) Иногда такие сокращения естественны и ясны, в других случаях, наоборот, затрудняют понимание программы. Отсутствие else-части в одной из вложенных друг в друга if-конструкций может привести к неоднозначному толкованию записи. Эту неоднозначность разрешают тем, что else связывают с ближайшим if, у которого нет своего else. Например, в if (n > 0) if (а > b) z = a; else z = b; else относится к внутреннему if, что мы и показали с помощью отступов. Если нам требуется иная интерпретация, необходимо должным образом расставить фигурные скобки: if (n > 0) { if (а > b) z = a; } else z = b; Ниже приводится пример ситуации, когда неоднозначность особенно опасна: if (n >= 0) for (i=0; i < n; i++) if (s[i] > 0) { printf ("…"); return i; } else /* НЕВЕРНО */ printf("ошибка – отрицательное n\n"); С помощью отступов мы недвусмысленно показали, что нам нужно, однако компилятор не воспримет эту информацию и отнесет else к внутреннему if. Искать такого рода ошибки особенно тяжело. Здесь уместен следующий совет: вложенные if обрамляйте фигурными скобками. Кстати, обратите внимание на точку с запятой после z = a в if (а > b) z = а; else z = b; Здесь она обязательна, поскольку по правилам грамматики за if должна следовать инструкция, а выражение-инструкция вроде z = a; всегда заканчивается точкой с запятой.
Конструкция if (выражение) инструкция else if (выражение) инструкция else if (выражение) инструкция else if (выражение) инструкция else инструкция встречается так часто, что о ней стоит поговорить особо. Приведенная последовательность инструкций if - самый общий способ описания многоступенчатого принятия решения. Выражения вычисляются по порядку; как только встречается выражение со значением "истина", выполняется соответствующая ему инструкция, на этом последовательность проверок завершается. Здесь под словом инструкция имеется в виду либо одна инструкция, либо группа инструкций в фигурных скобках. Последняя else-часть срабатывает, если не выполняются все предыдущие условия. Иногда в последней части не требуется производить никаких действий, в этом случае фрагмент else инструкция можно опустить или использовать для фиксации ошибочной ("невозможной") ситуации.
При бинарном поиске значение x сначала сравнивается с элементом, занимающим серединное положение в массиве v. Если x меньше, чем это значение, то областью поиска становится "верхняя" половина массива v, в противном случае - "нижняя". В любом случае следующий шаг - это сравнение с серединным элементом отобранной половины. Процесс "уполовинивания" диапазона продолжается до тех пор, пока либо не будет найдено значение, либо не станет пустым диапазон поиска. Основное действие, выполняемое на каждой шаге поиска, - сравнение значения x (меньше, больше или равно) с элементом v[mid]; это сравнение естественно поручить конструкции else-if.
Инструкция switch используется для выбора одного из многих путей. Она проверяет, совпадает ли значение выражения с одним из значений, входящих в некоторое множество целых констант, и выполняет соответствующую этому значению ветвь программы: switch (выражение) { case конст-выр: инструкции case конст-выр: инструкции default: инструкции } Каждая ветвь case помечена одной или несколькими целочисленными константами или же константными выражениями. Вычисления начинаются с той ветви case, в которой константа совпадает со значением выражения . Константы всех ветвей case должны отличаться друг от друга. Если выяснилось, что ни одна из констант не подходит, то выполняется ветвь, помеченная словом default, если таковая имеется, в противном случае ничего не делается. Ветви case и default можно располагать в любом порядке. Инструкция break вызывает немедленный выход из переключателя switch. Поскольку выбор ветви case реализуется как переход на метку, то после выполнения одной ветви case, если ничего не предпринять, программа провалится вниз на следующую ветвь. Инструкции break и return — наиболее распространенные средства выхода из переключателя. Инструкция break используется также для принудительного выхода из циклов while, for и do-while (мы еще поговорим об этом чуть позже). "Сквозное" выполнение ветвей case вызывает смешанные чувства. С одной стороны, это хорошо, поскольку позволяет несколько ветвей case объединить в одну, как мы и поступили с цифрами в нашем примере. Но с другой - это означает, что в конце почти каждой ветви придется ставить break, чтобы избежать перехода к следующей. Последовательный проход по ветвям - вещь ненадежная, это чревато ошибками, особенно при изменении программы. За исключением случая с несколькими метками для одного вычисления, старайтесь по возможности реже пользоваться сквозным проходом, но если уж вы его применяете, обязательно комментируйте эти особые места. Добрый вам совет: даже в конце последней ветви (после default в нашем примере) помещайте инструкцию break, хотя с точки зрения логики в ней нет никакой необходимости. Но эта маленькая предосторожность спасет вас, когда однажды вам потребуется добавить в конец еще одну ветвь case.
Цикл — последовательность из нескольких (0 и больше) инструкций, которая указывается в тексте программы один раз, но может выполняться несколько (0 и более) раз подряд, от первого до последнего, после последнего снова выполняется первый.
В данном примере в памяти находятся 2 переменные: int i char a На данной машине тип int занимает четыре байта, а тип char – один Порядок следования байт в целых числах на данной машине – т.н. little endian – младшие байты располагаются по младшему адресу На других машинах порядок байт, а также размеры типов данных могут различаться
Язык программирования C++
Язык С++ (Си++) Компилируемый статически типизированный язык программирования общего назначения Является продолжателем языка программирования C Многие программы языка C исправно работают и с компилятором C++ Поддержка объектно-ориентированного и обобщенного программирования
Программа Hello World #include <stdio.h> int main(int argc, char * argv[]) { printf("Hello, World!\n"); return 0; } #include <iostream> int main(int argc, char * argv[]) { std::cout << "Hello, World!\n"; return 0; } Вывод Hello world в C Вывод Hello world в C++
Комментарии в языке C++ Текст, заключённый в служебные символы /* и */ в этом порядке, полностью игнорируется компилятором. Компиляторы, совместимые со стандартом C99, также позволяют использовать комментарии, начинающиеся с символов // и заканчивающиеся переводом строки #include <stdio.h> /* Это простейшая программа на языке Си, которая выводит фразу “Hello World” на устройство стандартного вывода */ int main() { // Выводим строку Hello World printf("Hello World!\n"); return 0; }
Числовые константы Десятичные 12345, -34021 999999L, 99983UL Шестнадцатеричные 0xFeedBeef, 0x328aadb Восьмеричные 003, 0723 Вещественные 1.35, 8.45f 2e+10f, -3.835e-6L
Логические константы Логическая константа true служит для обозначения логического значения «Истина», а константа false – значения «Ложь» int i = 5; bool isEven = ((i % 2) == 0); bool everythingIsOK = false; if (isEven) { everythingIsOK = true; }
Символьные константы Записывается в виде символа, обрамленного одиночными кавычками 'A', '1' Значение символьной константы – числовой код символа из набора символов на данной машине Некоторые символы записываются в виде escape-последовательностей, начинающихся с символа \ '\'', '\0', ‘\n', '\177', '\xff'
Строковые константы (строковые литералы) Нуль или более символов, заключенных в двойные кавычки "Hello, world\n" "" "Hello " "world\n" эквивалентно "Hello world\n” Во внутреннем представлении строковая константа – массив символов, завершающийся символом с кодом 0 (‘\0’)
Представление строковой константы в памяти
Типы данных языка C++ Целые числа различных размеров со знаком или без int, short, char Числа с плавающей запятой различной размерности float, double, long double Логический тип bool Перечисляемые типы (enum) Структуры (struct) Объединения (union) Массивы
Базовые типы данных Типы данных целых чисел char int квалификаторы short/long unsigned/signed Логический тип bool Типы данных вещественных чисел float double
Пример - функция strlen /* strlen: возвращает длину строки s */ int strlen(char s[]) { int i = 0; while (s[i] != '\0') ++i; return i; }
Константы перечисления (enumeration) Задают список целых констант enum WeekDay {Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday}; Особенности Имена в различных перечислениях должны отличаться друг от друга Значения внутри одного перечисления могут совпадать: enum Status {Ok, Failure, Success = Ok}
Объявления переменных Переменные объявляются раньше их использоваться int lower, upper, step; char c, line[1000]; bool success; При объявлении переменные могут быть инициализированы char esc = '\\'; int i = 0; int limit = MAXLINE + 1; float eps = 1.0e-5f; bool success = true; Квалификатор const указывает, что значение переменной не будет далее изменяться const double e = 2.71828182845905; const char msg[] = "предупреждение: "; int strlen(const char str[]);
Ключевое слово typedef Язык Си++ предоставляет оператор typedef, позволяющий давать типам данных новые имена После этого новое имя типа может использоваться в качестве синонима оригинала Причины использования typedef Решение проблемы переносимости На разных платформах/компиляторах один и тот же тип может иметь различный размер Желание сделать текст программы более ясным
Пример использования оператора typedef typedef int Length; Length len, maxlen; len = 1; typedef double real; typedef int int32; typedef short int16; typedef char int8; int32 counter = 0; real x = 0.3;
Целочисленные типы данных Служат для хранения целых чисел различного размера char short (short int) int long (long int) Целые числа могут быть как со знаком, так и без него signed unsigned Гарантируется следующее соотношение размеров целочисленных типов: sizeof(char) <= sizeof(short) sizeof(short) <= sizeof(int) sizeof(int) <= sizeof(long)
Знаковые и беззнаковые целые числа Типы int и short (без квалификатора) знаковые int = signed int short = signed short Тип char, как правило, тоже знаковый char = signed char Это поведение может изменяться при помощи настроек некоторых компиляторов
char a = 'A'; unsigned char b = 150; signed char c = -15; short d = 10000; unsigned short e = 49320; signed short f = -25000; int g = -1000 * 1000; unsigned int h = 0xffffffff; signed int i = -999999999; long j = -123456789; unsigned long k = 0x9c9d9e9f; signed long k = j; Примеры объявления целочисленных переменных
Представление целых чисел в памяти компьютера Тип char занимает одну ячейку памяти (байт) размером 8 бит Возможны системы, в которых разрядность байта не равна 8 битам Типы short и int, занимают размер, кратный размеру типа char Размер типа short <= Размер типа int При этом число записывается в позиционной системе счисления с основанием 2разрядность байта Порядок записи байтов, представляющих число в памяти, зависит от архитектуры системы
Пример представления числа 666 в виде типа short и int Дано: разрядность типа char = 8 бит разрядность типа short = 16 бит разрядность типа int = 32 бита 666256 = 2 * 256 + 154 * 1 154 2 0 0 0 0 2 154 Little-endian Big-endian int short 154 2 2 154
Типы данных с плавающей запятой Позволяют задавать вещественные числа различного размера и точности float double long double Гарантированы следующие соотношения размеров вещественных типов данных sizeof(float) <= sizeof(double) sizeof(double) <= sizeof (long double)
const float PI = 3.1415927f; double sin60 = 0.86602540378443864676372317075294; double FahrengeitToCelsius(double fahr) { return (fahr – 32) * 5.0 / 9.0; } float DegreesToRadian(float degrees) { return degrees * PI / 180.0f; } Пример использования вещественных чисел
Перечисляемые типы данных (перечисления) Позволяет задать ограниченный набор именованных целочисленных значений День недели Состояние конечного автомата Тип файла Модель компьютера и т.д
#include <stdio.h> enum WeekDay { SUNDAY = 0, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, }; int main() { WeekDay today = SUNDAY; printf("Today is %d\n", today); today++; printf("Tomorrow will be %d\n", today); return 0; } Today is 0 Tomorrow will be 1 Пример использования перечислимых типов
Пример использования логического типа данных double CalculateCircleRadius(double area) { bool argumentIsValid = (area >= 0); if (argumentIsValid) { return sqrt(area / 3.14159265); } else { return -1; } }
Структуры Структура - это одна или несколько переменных (возможно, различных типов), которые для удобства работы с ними сгруппированы под одним именем. Структуры помогают в организации сложных данных, позволяя группу связанных между собой переменных трактовать не как множество отдельных элементов, а как единое целое
struct Point { int x; int y; }; struct Rectangle { Point leftTop; Point rightBottom; }; int main() { Point p0 = {0, 3}; Point p1 = {30, 20}; Rectangle r1 = {{1,1}, {20, 30}}; Rectangle r2; r2.leftTop = p0; r2.rightBottom = r1.rightBottom; return 0; } Пример использования структур
Объединения Объединение - это тип данных, который может содержать (в разные моменты времени) объекты различных типов и размеров Объединения позволяют хранить разнородные данные в одной и той же области памяти без включения в программу машинно-зависимой информации
#include <stdio.h> enum NumericType { TYPE_INTEGER, TYPE_REAL, }; struct Numeric { NumericType type; union { int intValue; double realValue; }value; }; void PrintNumeric(Numeric n) { if (n.type == TYPE_INTEGER) printf("n = %d\n", n.value.intValue); else printf("n = %f\n", n.value.realValue); } int main() { Numeric a, b; a.type = TYPE_INTEGER; a.value.intValue = 5; b.type = TYPE_REAL; b.value.realValue = 3.8; PrintNumeric(a); PrintNumeric(b); return 0; } n = 5 n = 3.80000
Массивы Простая статическая структура данных, предназначенная для хранения набора единиц данных, каждая из которых идентифицируется индексом или набором индексов Индекс —целое число, либо значение типа, приводимого к целому, указывающее на конкретный элемент массива Количество используемых индексов определяет размерность массива Индекс самого первого элемента массива равен 0, индекс последнего элемента равен N - 1
#include <stdio.h> int main() { int studentRatings[3] = {5, 4, 4}; char student0[] = "Bill"; char student1[] = {'J', 'o', 'h', 'n', '\0'}; char student2[6] = "Peter"; printf("Students' marks:\n"); printf("1. %s - %d\n", student0, studentRatings[0]); printf("2. %s - %d\n", student1, studentRatings[1]); printf("3. %s - %d\n", student2, studentRatings[2]); return 0; } Students' marks: 1. Bill - 5 2. John - 4 3. Peter - 4
Многомерные массивы double matrix1[3][2]; double matrix2[3][2] = { {3.0, 8.5}, {2.3, 6.2}, {9.2, -1.5}, }; double k = matrix2[0][1]; // k = 8.5
Указатели Указатель – используются для хранения адресов переменных в памяти Основные области применения Работа с динамической памятью Работа с массивами Передача параметров в функцию по ссылке Организация связанных структур данных (списки, деревья)
#include <stdio.h> typedef struct tagPoint { int x, y; }Point; void PrintPoint(Point *pPoint) { printf("point is (%d, %d)\n", pPoint->x, (*pPoint).y); } void Swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } int main() { int value = 0; int one = 1, two = 2; int *pValue = &value; Point pnt = {10, 20}; printf("value is %d\n", value); *pValue = 1; printf("now value is %d\n\n", value); printf("one=%d, two=%d\n", one, two); Swap(&one, &two); printf("now one=%d, two=%d\n\n", one, two); PrintPoint(&pnt); return 0; } value is 0 now value is 1 one=1, two=2 now one=2, two=1 point is (10, 20)
Хранение данных В C++ есть три разных способа выделения памяти для объектов Статическое: пространство для объектов создаётся в области хранения данных кода программы в момент компиляции; Автоматическое: объекты можно временно хранить в стеке; эта память затем автоматически освобождается и может быть использована снова, после того, как программа выходит из блока, использующего её. Динамическое: блоки памяти нужного размера могут запрашиваться во время выполнения программы с помощью оператора new в области памяти, называемой кучей. Эти блоки освобождаются и могут быть использованы снова после вызова для них оператора delete.
Набор используемых символов Используются почти все графические символы ASCII таблицы (кроме @ и $) Язык является чувствительным к регистру символов Для записи операторов используются строчные буквы Для записи идентификаторов – цифры, заглавные и строчные буквы и символ подчеркивания Идентификатор не может начинаться с цифры
Основные операторы языка Си Общие Арифметические операторы и оператор присванивания Логические операторы и операторы сравнения Оператор sizeof Управление ходом выполнения программы Условные операторы Операторы циклов Оператор множественного выбора Операторы для работы с массивами, структурами и объединениями Операторы для работы с указателями
Арифметические операторы Бинарные + - * / % (остаток от деления – применяется только к целым) int i = 10 % 3; /* i = 1; */ Деление целых сопровождается отбрасыванием дробной части float f = 8 / 3; /* f = 2.0 */ Унарные (ставятся перед операндом) + int i = +1; - int j = -8;
Пример if ( ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0) ) printf("%d високосный год\n", year); else printf("%d невисокосный год\n", year);
Операторы отношения Операторы отношения > >= < <= Операторы сравнения на равенство == != Логические операторы && - логическое И char ch = getchar(); int isDigit = (ch >= ‘0’) && (ch <= ‘9’); || - логическое ИЛИ char ch = getchar(); if ((ch == ‘ ‘) || (ch == ‘\n’) || (ch == ‘\t’)) printf(“Разделитель”); ! – логическое НЕ if (!valid) эквивалентно if (valid == 0) Вычисления операторов && и || прекращаются как только станет известна истинность или ложность результата
Пример #include <stdio.h> int main(int argc, char * argv[]) { int ch; char buffer[10]; const int MAX_SIZE = sizeof(buffer) - 1; int len = 0; while ((len < MAX_SIZE) && ((ch = getchar()) != EOF)) { buffer[len++] = ch; } buffer[len] = '\0'; return 0; }
Операторы инкремента и декремента Увеличивают или уменьшают значение операнда на 1 ++ -- Имеют две формы Префиксная форма int i = 0; int j = ++i; /* i = 1; j = 1; */ Постфиксная форма int i = 0; int j = i--; /* i = -1; j = 0; */ Операторы инкремента и декремента можно применять только к переменным int i = (j + y)++; /* ошибка */
Пример – функция squeeze /* squeeze: удаляет все символы c из строки s*/ void squeeze(char s[], char c) { int i, j; for (i = j = 0; s[i] != '\0'; ++i) { if (s[i] != c) s[j++] = s[i]; } s[j] = '\0'; }
Побитовые операторы Данные операторы позволяют осуществлять операции над отдельными битами целочисленных операндов & - побитовое И int i = 0xde & 0xf0; /* i = 0xd0 */ | - побитовое ИЛИ int i = 0xf0 | 0x03; /* i = 0xf3 */ ^ - побитовое исключающее ИЛИ int i = 0x03 ^ 0x02; /* i = 0x01 */ << - сдвиг влево int i = 1 << 3; /* i = 8 */ >> - сдвиг вправо int i = 0xd0 >> 4; /* i = 0x0d */ ~ - побитовое отрицание (унарный оператор). char i = ~0x1; /* i = 0xfe (0xfe = 11111110b) */
Пример: функция getbits /* getbits: получает n бит, начиная с p-й позиции */ unsigned getbits(unsigned x, int p, int n) { return (x >> (p+1-n)) & ~(~0 << n); } (x >> (9 + 1 – 7)) = ~0 = ~0 << 7 = ~(~0 << 7) = (x >> (9 + 1 – 7)) & ~(~0 << 7) =
Операторы и выражения присваивания Служат для присваивания переменным значения некоторого выражения i = 3; i += 8; i <<= 1; j %= 3; Типом и значением выражения присваивания является тип и значение левого операнда после завершения присваивания while ((c = getchar()) != EOF) { // do something }
Пример: функция bitcount /* bitcount: подсчет количества единичных битов в числе x */ int bitcount(unsigned х) { int b; for (b = 0; х != 0; x >>= 1) { if (x & 0x01) b++; } return b; }
Преобразование типов в стиле С Происходит, когда операнды оператора принадлежат к разным типам Неявное преобразование int i = 7.0 + 3 – 2.0f; Явное преобразование int i = (int)(7.0 + 3 – 2.0f); Если один из аргументов является знаковым целым, а второй беззнаковым, результатом будет целое число без знака
Опасность неявного приведения типов int CenterPictureOnTheScreen( int pictureWidth, unsigned screenWidth) { return (screenWidth - pictureWidth) / 2; } int main(int argc, char * argv) { unsigned screenWidth = 100; unsigned pic1Width = 50; int pic1X = CenterPictureOnTheScreen(pic1Width, screenWidth); // pic1x = 25: ok unsigned pic2Width = 150; int pic2X = CenterPictureOnTheScreen(pic2Width, screenWidth); // pic2x = 2147483623: error return 0; }
Решение проблемы – явное приведение типов int CenterPictureOnTheScreen( int pictureWidth, unsigned screenWidth) { return ((int)screenWidth - pictureWidth) / 2; } int main(int argc, char * argv) { unsigned screenWidth = 100; unsigned pic1Width = 50; int pic1X = CenterPictureOnTheScreen(pic1Width, screenWidth); // pic1x = 25: ok unsigned pic2Width = 150; int pic2X = CenterPictureOnTheScreen(pic2Width, screenWidth); // pic2x = -25:ok return 0; }
Недостатки оператора преобразования типов в стиле C Несмотря на свою простоту данный способ преобразования типов обладает рядом недостатков Допускаются потенциально некорректные преобразования типов, зачастую без информирования разработчика Сложно найти в тексте программы
Пример void Test(double doubleValue) { int intValue = (int)&doubleValue; … } struct Point { double x; double y; }; void Test1(const Point * p) { /* программист отвлекся и вместо int x = (int)p->x; написал: */ int x = (int)p; }
Преобразование типов в стиле C++ В языке C++ введены 4 оператора приведения типов static_cast<Type>(arg) dynamic_cast<Type>(arg) const_cast<Type>(arg) reinterpret_cast<Type>(arg) Каждый из данных операторов применяется для определенного преобразования типов в конкретной ситуации В программах на C++ следует отдавать предпочтение данным операторам
Оператор static_cast Применяется для статического преобразования одного типа к другому Также может применяться для статического преобразования типов указателей в пределах иерархии классов
Пример void Test(double doubleValue) { // Ошибка компиляции int intValue = static_cast<int>(&doubleValue); } struct Point { double x; double y; }; void Test1(const Point * p) { int x = static_cast<int>(p->x); // ok int y = static_cast<int>(p); // ошибка компиляции }
Оператор dynamic_cast Применяется для динамического преобразования типов в пределах иерархии классов (об этом позже)
Оператор const_cast Применяется для снятия константности с константного выражения int k = 0; const int * pConstK = &k; int * pK = const_cast<int*>(pConstK); const int & constRefK = k; int & refK = const_cast<int&>(constRefK);
Оператор reinterpret_cast Может применяться для преобразования между целочисленными типами и указателями, а также между указателями на несвязанные друг с другом типа данных int main(int argc, char * argv) { float x = 30.3; unsigned y = *reinterpret_cast<int*>(&x); // берем четвертый бит двоичной записи числа 30.3 unsigned z = y & (1 << 4); return 0; }
Условное выражение Условное выражение имеет вид: выр1 ? выр2 : выр3 Сначала вычисляется выражение 1 Если оно истинно (не равно нулю), то вычисляется выражение 2 и его значение становится значением всего условного выражения В противном случае вычисляется выражение 3 и становится значением всего условного выражения Пример z = (a > b) ? a : b; /* z = max(a, b)*/
Приоритет и очередность выполнения операторов
Инструкции и блоки Выражение (например, x = 0) становится инструкцией, если в конце поставить точку с запятой x = 0; printf(“Hello”); В Си точка с запятой является заключающим символом инструкции, а не разделителем, как в языке Паскаль. Фигурные скобки { и } используются для объединения объявлений и инструкций в составную инструкцию, или блок с т.з. синтаксиса языка блок воспринимается как одна инструкция
Блоки и область видимости Переменные видимы внутри того блока, где она объявлена При покидании своего блока видимости переменная уничтожается, а занимаемая ею область памяти – освобождается (автоматическое управление памятью) int main(int argc, char * argv) { int a = 0; if (argc > 1) { int b = argc - 1; } return 0; }
Конструкция if-else Оператор if позволяет выполнить тот или иной участок кода в зависимости от значения некоторого выражения if (<выражение>) <инстр.1> else <инстр.2> if (<выражение>) <инстр>
Конструкция else-if Позволяет осуществлять многоступенчатое решение if (выражение) инструкция else if (выражение) инструкция else if (выражение) инструкция else if (выражение) инструкция else инструкция
Пример, бинарный поиск /* binsearch: найти x в v[0] <= v[1] <= ... <= v[n-1] */ int binsearch(int x, const int v[], int n) { int low, high, mid; low = 0; high = n - 1; while (low <= high) { mid = (low + high) / 2; if (x < v[mid]) high = mid - 1; else if (x > v[mid]) low = mid + 1; else /* совпадение найдено */ return mid; } return -1; /* совпадения нет */ }
Оператор switch Используется для выбора одного из нескольких путей Осуществляется проверка на совпадение значения выражения с одной из некоторого набора целых констант, и выполняет соответствующую ветвь программы switch (выражение) { case конст-выр: инструкции case конст-выр: инструкции default: инструкции }
#include <stdio.h> int main() /* подсчет цифр, символов-разделителей и прочих символов */ { int c, i, nwhite, nother, ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; i++) ndigit[i] = 0; while ((c = getchar()) != EOF) { switch (c) { case '0' : case '1' : case '2' : case '3' : case '4' : case '5' : case '6' : case '7' : case '8' : case '9' : ndigit[c - '0']++; break; case ' ': case '\n': case '\t': nwhite++; break; default: nother++; break; } } printf ("цифр ="); for (i = 0; i < 10; i++) printf (" %d", ndigit[i]); printf(", символов-разделителей = %d, прочих = %d\n", nwhite, nother); return 0; }
Циклическое выполнение
Что такое циклическое выполнение Цикл – последовательность из нескольких операторов, указываемая в программе один раз, которая выполняется несколько раз подряд Допускается существование бесконечного цикла Тело цикла - последовательность операторов, предназначенная для многократного выполнения в цикле
Циклическое выполнение в языке Си Циклическое выполнение в языке Си осуществляется при использовании следующих операторов цикла: while for do..while Внутри циклов могут использоваться операторы управления работой цикла: break для досрочного выхода из цикла continue для пропуска текущей итерации
Оператор while Оператор while служит для организации циклов с предусловием цикл, который выполняется, пока истинно некоторое условие, указанное перед его началом Синтаксис while (выражение) инструкция Инструкция (тело цикла) выполняется до тех пор, пока выражение принимает ненулевое значение
Примеры #include <stdio.h> int main() { int ch; while ((ch = getchar()) != EOF) // пока не конец файла { putchar(ch); } return 0; } #include <stdio.h> int main() { while (1) // бесконечный цикл while { putchar(‘#’); } return 0; }
Оператор for Оператор for служит для организации циклов со счетчиком Синтаксис for (выр1; выр2; выр3) инструкция Выражение1 выполняется один раз перед началом цикла Например, оператор инициализации счетчика цикла Выполнение инструкции (тело цикла) продолжается до тех пор, пока выражение2 имеет ненулевое значение если выражение2 отсутствует, то выполнение цикла продолжается бесконечно После каждой итерации цикла выполняется выражение3 Например, изменение счетчика цикла
Пример #include <stdio.h> int main() { int i; // цикл от 0 до 10 включительно с шагом 1 for (i = 0; i <= 10; ++i) { printf("%d^2 = %d\n", i, i*i); } return 0; }
Оператор do-while Оператор do-while служит для организации циклов с постусловием цикл, в котором условие проверяется после выполнения тела цикла тело всегда выполняется хотя бы один раз Синтаксис do инструкция while (выражение); Инструкция выполняется до тех пор, пока выражение принимает ненулевое значение
#include <stdio.h> void PrintInteger(int n) { char buffer[20]; int sign, i = 0; if ((sign = n) < 0) n = -n; do // записываем в буфер десятичные разряды числа, начиная с самого правого { buffer[i++] = (char)((n % 10) + ‘0’); } while((n /= 10) != 0); // пока число не обнулится if (sign < 0) buffer[i++] = '-'; for (--i; i >= 0; --i) putchar(buffer[i]); } int main() { PrintInteger(12345); putchar('\n'); PrintInteger(-12345); putchar('\n'); PrintInteger(0); putchar('\n'); return 0; }
Вложенные циклы Один цикл может быть вложен в другой При этом выполнение внутреннего цикла выполняется как часть оператора внешнего цикла
#include <stdio.h> void PrintArray(int v[], int n) { int i; printf("{"); for (i = 0; i < n; ++i) printf((i != n - 1) ? "%d, " : "%d", v[i]); printf("}\n"); } void ShellSort(int v[], int n) { int gap, i, j, temp, unordered = 1; for (gap = n/2; (gap > 0) && unordered; gap /= 2) { for (unordered = 0, i = gap; i < n; i++) for (j = i- gap; (j >= 0) && (v[j] > v[j+gap]); j -= gap) { unordered = 1; temp = v[j]; v[j] = v[j + gap]; v[j + gap] = temp; } PrintArray(v, n); } } int main() { int array[] = {76, 1, 9, 3, 15, 4, 10, 9, 13, 20}; const int ARRAY_LENGTH = sizeof(array) / sizeof(array[0]); PrintArray(array, ARRAY_LENGTH); printf("Sorting:\n"); ShellSort(array, ARRAY_LENGTH); return 0; } {76, 1, 9, 3, 15, 4, 10, 9, 13, 20} Sorting: {4, 1, 9, 3, 15, 76, 10, 9, 13, 20} {4, 1, 9, 3, 10, 9, 13, 20, 15, 76} {1, 3, 4, 9, 9, 10, 13, 15, 20, 76}
Инструкции break и continue Инструкция break осуществляет немедленный выход из тела цикла, внутри которого она находится Также инструкция break осуществляет выход из оператора switch Инструкция continue осуществляет пропуск оставшихся операторов тела цикла, внутри которого она находится, и переход на следующую итерацию цикла В циклах while и do-while осуществляется переход к проверке условия В цикле for осуществляется переход к приращению переменной цикла
#include <stdio.h> #include <string.h> /* trim: удаляет завершающие пробелы, табуляции и новые строки */ int trim(char s[]) { int n; for (n = strlen(s) - 1; n >= 0; --n) { if ((s[n] != ' ') && (s[n] != '\t') && (s[n] != '\n')) break; // выходим из цикла, встретив печатаемый символ } s[n+1] = '\0'; return n; } int main() { char str[] = "Hello \t\n"; printf("Before trim: \"%s\"\n", str); trim(str); printf("After trim: \"%s\"\n", str); return 0; } Before trim: "Hello " After trim: "Hello"
#include <stdio.h> void PrintPositives(int v[], int n) { int i; for (i = 0; i < n; i++) { if (v[i] < 0) /* пропуск отрицательных элементов */ continue; printf("%d ", v[i]); } } int main() { int array[] = {-76, 1, 9, -3, 15, -4, 10, 9, 13, 20}; const int ARRAY_LENGTH = sizeof(array) / sizeof(array[0]); PrintPositives(array, ARRAY_LENGTH); return 0; } 1 9 15 10 9 13 20
Инструкция goto Инструкция goto позволяет осуществить переход на заданную метку внутри текущей функции Синтаксис: goto метка; Как правило, использование инструкции goto усложняет структуру программы и без крайней необходимости ею пользоваться не стоит Если Вы все еще думаете об использовании этого оператора – использовать его все равно не стоит
Пример /* поиск совпадающих элементов в массивах */ for (i = 0; i < n; ++i) { for (j = 0; j < m; ++j) { if (a[i] == b[j]) goto found; } } /* нет одинаковых элементов */ ... found: /* обнаружено совпадение: a[i] == b[j] */ ...
Функции
Объявление функции int Foo(float bar); // объявление функции Foo // Определение функции Foo int Foo(float bar) { // тело функции return 0; // оператор выхода из функции } // функция, не возвращающая никакого значения (процедура) void Moo() { … return; }
Перегрузка функций Перегрузкой имени функции является его использование для обозначения разных операций над разными типами Если несколько функций выполняют одно и то же действие с объектами различных типов, имеет смысл дать им одинаковые имена
Пример #include <stdio.h> void Print(int number) { printf("%d", number); } void Print(double number) { printf("%.15f", number); } void Print(const char* str) { printf("%s", str); } int main() { Print(1); printf("\n"); Print(8.0/3); printf("\n"); Print("Hello World\n"); return 0; }
Функции с различным количеством аргументов Функции с одним именем могут иметь различное число аргументов В этом случае, будет вызвана та функция, количество формальных аргументов которой совпадает с количеством переданных параметров
Пример void Print(char ch) { putchar(ch); } void Print(char ch, int count) { while (count--) { putchar(ch); } } int main() { Print('!'); putchar('\n'); Print('!', 5); putchar('\n'); return 0; }
Выбор нужной функции Для выбора функции из имеющихся вариантов компилятор сравнивает типы фактических аргументов, указанные в вызове, с типами формальных аргументов всех описаний функций с данным именем В результате вызывается та функция, у которой типы формальных аргументов наилучшим образом сопоставились с параметрами вызова Типы возвращаемых значений функций не учитываются
Правила сопоставления параметров Правила сопоставления имеют следующие приоритеты (в порядке убывания): Точное сопоставление сопоставление произошло без всяких преобразований типа или только с неизбежными преобразованиями Сопоставление с использованием стандартных целочисленных преобразований и преобразований с плавающей запятой char в int, short в int, float в double Сопоставление с использованием стандартных преобразований, определенных в §R.4 int в double, derived * в base*, unsigned в int Сопоставление с использованием пользовательских преобразований §R.12.3 Сопоставление с использованием эллипсиса ... в описании функции Выбирается та функция, у которой произошло сопоставление по наиболее приоритетному правилу
Возможные проблемы Если две или более функций с данным именем были сопоставлены по самому приоритетному правилу, компилятор выдаст сообщение об ошибке В этом случае требуется явно приводить аргументы к требуемым типам
Пример void Print(int number) { printf("%d", number); } void Print(double number) { printf("%.15f", number); } int main() { unsigned int arg = 2; Print(arg); // ошибка Print(static_cast<int>(arg)); // OK return 0; }
Стандартные значения параметров Некоторые функции могут принимать больше параметров, чем в самых простых и часто используемых случаях Для гибкого использования этих функций могут применяться необязательные параметры
#include <stdio.h> void ClearArray(int *array, unsigned size, int fillValue = 0) { for (unsigned index = 0; index < size; ++index) array[index] = fillValue; } void PrintArray(const int * array, unsigned size) { printf("{"); for (unsigned index = 0; index < size; ++index) { printf("%d", array[index]); if (index != (size - 1)) printf(" "); } printf("}\n"); } int main() { int arr[10]; unsigned const NUMBER_OF_ELEMENTS = sizeof(arr) / sizeof(*arr); ClearArray(arr, NUMBER_OF_ELEMENTS); PrintArray(arr, NUMBER_OF_ELEMENTS); ClearArray(arr, NUMBER_OF_ELEMENTS, 1); PrintArray(arr, NUMBER_OF_ELEMENTS); return 0; } OUTPUT: {0, 0, 0, 0, 0, 0, 0, 0, 0, 0} {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
Ссылки
Ссылки Ссылку можно рассматривать как еще одно имя объекта Синтаксис <Тип> & означает ссылку на <Тип> Применение Задание параметров функций Перегрузка операций
Ссылки в качестве параметров функций При передаче параметра в функцию по ссылке, функция принимает не копию аргумента, а ссылку на него При сложных типах аргументов это может дать прирост в скорости вызова функции Измененное внутри функции значение формального параметра приведет к изменению значения переданного аргумента Использование ссылок может быть альтернативным способом возврата значения из функции Если функция не изменяет значение аргумента, будет иметь смысл передать его по константной ссылке
Пример 1 #include <stdio.h> void Swap(int & a, int & b) { int tmp = a; a = b; b = tmp; } int main() { int a = 1, b = 3; pritnf(“a=%d, b=%d\n”, a, b); Swap(a, b); pritnf(“a=%d, b=%d\n”, a, b); } OUTPUT a=1, b=3 a=3, b=1
Пример 2 struct Point { int x, y; }; void Print(Point const& pnt) { printf("(x:%d, y:%d)\n", pnt.x, pnt.y); } int main() { Point pnt = {10, 20}; Print(pnt); return 0; }
Инициализация ссылки Ссылка должна быть обязательно проинициализирована Должен существовать объект на который она ссылается Синтаксис Тип & идентификатор = значение; Инициализация ссылки совершенно отличается от операции присваивания Будучи проинициализированной, присваивание ссылке нового значения изменяет значение ссылаемого объекта, а не значение ссылки
Пример #include <stdio.h> int main() { int i = 1; int j = 3; // инициализация ссылки int & ri = i; printf("i=%d, j=%d\n", i, j); // присваивание значения объекту, на который ссылается ri ri = j; printf("i=%d, j=%d\n", i, j); } OUTPUT i=1, j=3 i=3, j=3
Ссылки на временные объекты При инициализации ссылки объектом другого типа компилятор создает временный объект нужного типа и использует его для инициализации ссылки На данный временный объект может ссылаться только константная ссылка То же самое происходит при инициализации ссылки значением константы Изменение значения объекта в данном случае не отражается на значении временного объекта
Пример int a = 1; int & refA = a; printf("a = %d\n", a); ++refA; printf("Now a = %d\n\n", a); const double & refDoubleA = a; printf("refDoubleA = %f\n", refDoubleA); ++a; printf("Now a = %d, refDoubleA = %f\n", a, refDoubleA); OUTPUT: a = 1 Now a = 2 refDoubleA = 2.00000 Now a = 3, refDoubleA = 2.00000
Пространства имен Пространства имен позволяют логически сгруппировать классы, переменные и функции в некоторые именованные области Позволяют избежать конфликта имен идентификаторов в различных модулях проекта Разбивают программу на функциональные единицы
#include <stdio.h> namespace math { int calculateX2(int x) { return x * x; } } namespace graphics { namespace shapes { struct rectangle { int x, y, w, h; }; struct circle { int x, y, r; }; } } namespace sound_player { void PlaySound() { // sound playing code is placed here } } using namespace sound_player; int main() { int x = 5; int x2 = math::calculateX2(x); graphics::shapes::rectangle rect = {0, 0, 40, 30}; PlaySound(); return 0; }
Стандартная библиотека шаблонов STL
Стандартная библиотека шаблонов (STL) Программная библиотека, содержащая большое количество готового к использованию обобщенного кода Контейнеры Итераторы Алгоритмы Все контейнеры, алгоритмы и итераторы в STL объявлены в пространстве имен std Стандарт запрещает программисту объявлять свои типы в данном пространстве имен
Контейнеры Классы, предназначенные для хранения элементов определенного типа STL содержит классы обобщенных реализаций различных контейнеров, которые можно использовать с элементами различных типов В STL поддерживаются 2 вида контейнеров Последовательные Ассоциативные
Контейнеры в STL Последовательные контейнеры Строка (basic_string, string, wstring) Вектор (vector) Двусвязный список (list) Двусторонняя очередь (deque) Ассоциативные контейнеры карта (map, multimap) множество (set, multiset) Контейнеры-адаптеры Стек (stack) Очередь (queue) Очередь с приоритетом (priority_queue)
Строка std::string Контейнер, предназначенный для хранения строк произвольной длины В качестве элементов строк могут выступать элементы типа char (string), wchar_t (wstring) или определяемые пользователем типы (basic_string) Данный контейнер автоматизирует задачу управления памятью, занимаемой символами строки и предоставляет набор методов для осуществления операций над строками Для работы с данным классом строк необходимо подключить заголовочный файл <string>
Пример #include <string> #include <stdio.h> using namespace std; int main(int argc, char *argv[]) { string hello = “Hello”; string world(“world”); string helloWorld = hello + “ “ + world; size_t len = helloWorld.length(); printf(“%s\n”, helloWorld.c_str()); return 0; }
Вектор std::vector Контейнер для хранения динамического массива элементов произвольного типа Как и строка, вектор автоматизирует процесс управления памятью, занимаемой элементами массива Везде, где возможно, рекомендуется использовать класс vector как альтернативу динамическому выделению массивов объектов при помощи оператора new К элементам массива предоставляется индексированный доступ Для использования данного класса необходимо подключить заголовочный файл <vector>
Пример #include <vector> #include <string> using namespace std; int main(int argc, char *argv[]) { // создаем массив целых чисел, содержащий 100 элементов vector<int> vectorOfInt(100); vector<string> vectorOfString; vectorOfInt.push_back(10); vectorOfString.push_back(“Hello”); std::string hello = vectorOfString[0]; size_t numberOfItems = vectorOfString.size(); return 0; }
Двусвязный список std::list Реализовывает двусвязный список элементов произвольного типа К элементам списка осуществляется последовательный доступ при помощи итераторов Вставка и удаление элементов из произвольного места списка осуществляется за постоянное время Для начала работы с данным контейнером необходимо подключить заголовочный файл <list>
Пример #include <list> #include <string> #include <iostream> using namespace std; int main(int argc, char *argv[]) { list<string> listOfStrings; listOfStrings.push_back(“One”); listOfStrings.push_back(“Two”); listOfStrings.push_back(“Three”); for (list<string>::iterator it = listOfStrings.begin(); it != listOfStrings.end(); ++it) { std::string const& item = *it; cout << item << “, “; } return 0; }
Двусторонняя очередь std::deque Аналогична вектору, но обеспечивает эффективную вставку и удаление элементов не только в конце, но и в начале очереди Для использования необходимо подключить заголовочный файл <deque>
Классы std::map и std::multimap Ассоциативный контейнер, хранящий пары «ключ» - «значение» Позволяет отображать элементы одного типа в элементы другого или того же самого типа map – все ключи уникальные multimap – допускается дублирование ключей Для подключения данных классов необходимо подключить заголовочный файл <map>
Пример #include <map> #include <string> #include <iostream> using namespace std; int main(int argc, char *argv[]) { map<string, string> dictionary; dictionary.insert(pair<string, string>(“Cat”, “Кошка”)); dictionary[“Dog”] = “Собака”; dictionary [“Mouse”] = “Мышь”; cout << dictionary[“Dog”] << “\n”; return 0; }
Классы множеств std::set и std::multiset Ассоциативный контейнер, хранящий множество элементов определенного типа set – дублирование элементов не допускается multiset – дублирование элементов допускается Для использования данных классов необходимо подключить заголовочный файл <set>
Пример #include <set> #include <string> #include <iostream> using namespace std; int main(int argc, char *argv[]) { set<int> primeNumbers; primeNumbers.insert(2); primeNumbers.insert(3); primeNumbers.insert(5); if (primeNumbers.find(3) != primeNumbers.end()) { cout << “3 is a prime number\n”; } set<string> maleNames; maleNames.insert(“John”); maleNames.insert(“Peter”); return 0; }
Итераторы Итератор – объект, позволяющий программисту осуществлять перебор элементов контейнера вне зависимости от деталей его реализации Например, осуществлять вставку диапазона элементов одного контейнера в другой Итераторы используются в STL для доступа к элементам контейнеров Обобщенные реализации алгоритмов используют итераторы для обработки элементов контейнеров Итератор – связующее звено между контейнером и алгоритмом
Алгоритмы Обобщенные функции, реализующие типичные алгоритмы над элементами контейнеров Сортировка, поиск, поэлементная обработка Алгоритмы в STL не работают с контейнерами напрямую Вместо этого алгоритмы используют итераторы, задающие определенные элементы или диапазоны элементов контейнера Для работы с алгоритмами STL необходимо подключить заголовочный файл <algorithm>
Пример #include <string> #include <vector> #include <list> #include <iterator> using namespace std; int main(int argc, char *argv[]) { vector<string> names; names.push_back(“Peter”); names.push_back(“Ivan”); names.push_back(“John”); list<string> namesList; sort(names.begin(), names.end()); copy(names.begin(), names.end(), namesList.end()); return 0; }
Указатели
Организация памяти в языке C++ С точки зрения языка С++ память представляет собой массив последовательно пронумерованных ячеек памяти, с которыми можно работать по отдельности или связными кусками Порядковый номер ячейки называется ее адресом Эта память используется для хранения значений переменных. Переменные различных типов могут занимать различное количество ячеек памяти, и иметь различные способы представления в памяти
Пример int i = 1000; char a = 15;
Что такое указатель? Указатель – это переменная, которая может хранить адрес другой переменной в памяти заданного типа Указатели – мощное средство языка С++, позволяющее эффективно решать различные задачи Использование указателей открывает доступ к памяти машины, поэтому пользоваться ими следует аккуратно
Объявление указателя Указатель на переменную определенного типа объявляется следующим образом: <тип> *<идентификатор>; Например: int *pointerToInt; Указатель, способный хранить адрес переменной любого типа имеет тип void*: void * pointerToAnyType; Как и к обычным переменным, к указателям можно применять модификатор const: const int * pointerToConstInt; char * const constPointerToChar = &ch; const double * const constPointerToConstDouble = &x; float * const constPointerToFloat = &y; const void * pointerToConstData;
Получение адреса переменной Для взятия адреса переменной в памяти служит унарный оператор & Этот оператор возвращает адрес переменной, который может быть присвоен указателю совместимого типа Оператор взятия адреса применим только к переменным. Его нельзя применять к константам, выражениям или регистровым переменным
Оператор косвенного доступа Для доступа к значению, на которое ссылается указатель, необходимо его разыменование (dereferencing), осуществляемое при помощи унарного оператора * int * p = &i; *p = 5;
Пример p c char c = ‘A’; char *p = &c; *p = ‘B’; ‘A’ ‘B’
Инициализация указателей Значение неинициализированного указателя не определено Разыменование такого указателя приводит к неопределенному поведению Лучше присвоить указателю нулевое значение (или символическую константу NULL), чтобы подчеркнуть, что он не ссылается ни на какую переменную: char * p1 = 0; char * p2 = NULL; Разыменование нулевого указателя также приводит к неопределенному поведению, однако появляется возможность проверки значения указателя: if (p != NULL) // или просто if (p)
Копирование указателей Как и в случае обычных переменных, значение одного указателя может быть присвоено другому при помощи оператора = Следует помнить, что в этому случае копируется адрес переменной, а не ее значение Для копирования значения переменной, на которую ссылается указатель, необходимо применить оператор разыменования * char a = ‘A’; char b = ‘B’; char c = ‘C’; char *pa = &a; char *pb = &b; char *pc = &c; pa = pb; // pa и pb теперь хранят адрес b *pa = *pc; // b теперь хранит значение ‘C’
Указатели и аргументы функций В языке Си параметры в функцию передаются по значению. Указатели – единственный способ изменить значение параметра изнутри функции В языке Си++ появилась возможность передачи параметров по ссылке void swap(int *pa, int *pb) { int tmp = *pa; *pa = *pb; *pb = tmp; } void swap(int &pa, int &pb) { int tmp = pa; pa = pb; pb = tmp; }
Указатели на функции В Си можно объявить указатель на функцию и работать с ним как с обычной переменной, сохраняя возможность вызова функции по указателю на нее Данная возможность позволяет иметь несколько реализаций алгоритма, имеющих общий интерфейс
#include <stdio.h> typedef int (*OrderedFunction)(int a, int b); void BubbleSort(int array[], int size, OrderedFunction fn) { int sorted, i; do { sorted = 1; for (i = 0; i < size - 1; ++i) { if (!fn(array[i], array[i + 1])) { int tmp = array[i]; array[i] = array[i + 1]; array[i + 1] = tmp; sorted = 0; } } --size; } while(!sorted && (size > 1)); } int IsOrdered(int a, int b) { return a <= b; } int main() { int arr[5] = {3, 5, 1, 7, 9}; BubbleSort(arr, 5, IsOrdered); return 0; }
Массивы в Си и Си++ Массивы позволяют объявить несколько (один и более) последовательных объектов, объединенных под одним именем, и осуществлять к ним индексированный доступ В качестве индексов используются целые числа, или типы, приводимые к целым Размер массива задается статически на этапе компиляции и не может быть изменен в ходе работы программы Индекс начального элемента массива равен нулю Есть возможность объявления многомерных массивов
// неинициализированный массив из 10 элементов с индексами от 0 до 9 int a[10]; a[0] = 5; // присвоить начальному элементу массива значение 5 a[9] = 10;// присвоить конечному элементу массива значение 10 a[10] = 0;// ошибка, индекс должен быть от 0 до 9 // объявляем и инициализируем массив из 3 элементов, int b[3] = {1, 2, 3}; // объявляем и инициализируем массив из 4 элементов int c[] = {100, 200, 300, 400}; // двумерный массив из 4 строк и 7 столбцов int matrix[4][7]; matrix[0][5] = 11; // доступ к 0 строке и 5 столбцу // заполнение элементов массива заданным значениемж void FillArray(int arr[], int size, int init) { int i; for (i = 0; i < size; ++i) arr[i] = init; }
Указатели и массивы Указатели и массивы в Си тесно связаны Имя массива является синонимом расположения его начального элемента int arr[10]; int *p = arr; // эквивалентно int *p = &arr[0]; Индексация элементов массива возможна с помощью указателей и адресной арифметики
Адресная арифметика Если p – указатель на некоторый элемент массива, то p+1 – указатель на следующий элемент p-1 – указатель на предыдущий элемент p+j – указатель на j-й элемент после p p[j] разыменовывает j-й элемент относительно p Если p и q – указатели на некоторые элементы одного массива, то p–q - равно количеству элементов после q, которое необходимо добавить, чтобы получить p p<q принимает значение 1, если p указывает на элемент, предшествующий q, в противном случае - 0 p==q, принимает значение 1 если p и q указывают на один и тот же элемент, в противном случае - 0
Адресная арифметика в действии a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] p p+1 p-1 q q – p = 2 q - 2 = p p + 2 = q p < q p + 1 = q - 1 p[3] = a[7] p[-2] = a[2] p = &a[4] q = &a[6] &a[8] - &a[0] = 8
Примеры int arr[10]; // получаем указатель на начальный элемент массива int *p = arr; // эквивалентно int *p = &arr[0]; // следующие две строки эквивалентны *(p + 4) = 5; arr[4] = 5; /* несмотря на то, что в массиве всего 10 элементов, допускается получать указатель на ячейку, следующую за последним элементом массива */ p = &a[10]; *(p – 1) = 3; // эквивалентно arr[9] = 3;
Указатели на char Строковые константы в Си – массивы символов с завершающим нулем Передача строковой константы в функцию (напр. printf) осуществляется путем передачи указателя на ее начальный элемент
Особенности Присваивание символьных указателей, не копирует строки char * p = “Hello”; char * p1 = p; // p и p1 указывают на одно и то же место в памяти Символьный массив и символьный указатель – различные понятия char msg[] = “Hello”; // массив Символы внутри массива могут изменяться msg всегда указывает на одно и то же место в памяти char *pmsg = “Hello”; // указатель Попытка изменить символы через pmsg приведет к неопределенному поведению pmsg – указатель, можно присвоить ему другое значение в ходе работы программы
Массивы указателей Указатели, как и другие переменные можно группировать в массивы int main(int argc, char* argv[]) const char * a[] = {“Hello”, “World!”}; printf(“%s %s\n”, a[0], a[1]); a[0] = “Goodbye”; Массивы указателей могут использоваться как альтернатива двумерных массивов
Указатели на указатели В C и C++ возможны указатели, ссылающиеся на другие указатели char arr[] = “Hello”; char *parr = arr; char **pparr = &parr; // pparr – хранит адрес указателя parr (*pparr)[0] = ‘h’; // arr = “hello” pparr[0][1] = ‘E’; // arr = “hEllo”;
Инкремент и декремент указателя Когда указатель ссылается на определенный элемент массива, имеют смысл операции инкремента и декремента указателя char str[] = “Hello, world!”; char *p = str;// p указывает на символ H p++; // p указывает на символ e *p = ‘E’; // заменяем символ e на E
#include "stdio.h" // возвращаем адрес найденного символа в строке или NULL в случае отсутствия const char* FindChar(const char str[], char ch) { const char * p = str; while (*p != ‘\0') { if (*p == ch) return p; ++p; } return NULL; } int main() { const char str[] = "Hello, world!\n"; const char *pw = FindChar(str, 'w'); if (pw != NULL) printf("%s", pw); return 0; } Output: world!
Указатели и динамическая память Часто возможны ситуации, когда размер и количество блоков памяти, необходимых программе, не известны заранее В этом случае прибегают к использованию динамически распределяемой памяти Приложение может запрашивать блоки памяти необходимого размера из области, называемой кучей (heap) Как только блок памяти становится не нужен, его освобождают, возвращая память в кучу
Операторы new и delete В состав языка C++ вошли операторы new и delete, осуществляющие работу с динамической памятью на уровне языка Оператор new выделяет память под элемент или массив элементов Тип *p = new Тип() Тип *p = new Тип(инициализатор,...) Тип *p = new Тип[кол-во элементов] Оператор delete освобождает память, выделенную ранее оператором new delete pObject; delete [] pArray; В программах на C++ следует использовать данные операторы при любой удобной возможности
Прочие средства работы с динамической памятью В стандартной библиотеке языка C для работы с динамической памятью служат функции: malloc calloc realloc free Существуют средства работы с динамической памятью, зависящие от используемой ОС или используемых компонентов
Функции memcpy, memset и memmove Функция memcpy осуществляет копирование блока памяти из одного адреса в другой void memcpy(void *dst, const void *src, unsigned count) Функция memmove аналогична memcpy, но корректно работает, если блоки перекрываются void memmove(void *dst, const void *src, unsigned count) Функция memset заполняет область памяти определенным значением типа char void memset(void *dst, int c, unsigned count)
Пример int n = 30; // выделяем память под n элементов типа int int * arr = (int*)malloc(sizeof(int) * n); memset(arr, 1, sizeof(int) * n); arr[0] = 5; free(arr); arr = NULL;
Указатели на структуры и объединения Указатели на структуры объявляются аналогично указателям на другие типы Для доступа к элементам структуры может применяться оператор -> struct Point { int x, y; }; Point p = {10, 20}; Point *pPoint = &p; (*pPoint).x = 1; pPoint->y = 2;
Контейнеры STL и умные указатели Контейнеры STL автоматически освобождают занимаемую своими элементами память std::vector – рекомендуемая альтернатива динамическому массиву std::auto_ptr – простейший умный указатель boost::shared_ptr – умный указатель с подсчетом ссылок std::tr1::shared_ptr – он же, включенный в новый стандарт языка C++ Прочее boost:scoped_ptr boost::intrusive_ptr boost::scoped_array boost::shared_array
Summary: Лекция знакомит с синтаксисом языка C++
| URL: |
No comments posted yet
Comments