Int i 0 что это значит

от admin

Int i 0 что это значит

Циклы позволяет выполнить одно действие множество раз в зависимости от определенного условия. В языке Си есть следующие типы циклов:

Цикл for

Цикл for имеет следующее формальное определение:

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

Вторая часть — условие , при соблюдении которого выполняется цикл. Зачастую в качестве условия используется операция сравнения, и если она возвращает ненулевое значение (то есть условие истинно), то выполняется тело цикла, а затем вычисляется выражение_3 .

Третья часть — приращение задает изменение параметров цикла. Обычно здесь происходит увеличение счетчиков цикла.

Рассмотрим стандартный цикл for и для этого выведем числа с 0 до 5 на консоль:

Первая часть объявления цикла — int i = 0 — создает и инициализирует счетчик i. Счетчик необязательно должен представлять тип int. Это может быть и другой числовой тип, например, float. И перед выполнением цикла его значение будет равно 0. В данном случае это то же самое, что и объявление переменной.

Вторая часть — условие, при котором будет выполняться цикл. В данном случае цикл будет выполняться, пока i не достигнет 6.

И третья часть — приращение счетчика на единицу. Опять же нам необязательно увеличивать на единицу. Можно уменьшать: i—. Можно изменять на другое значение: i+=2.

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

Каждый отдельный проход цикла называется итерацией. То есть в примере выше было 6 итераций.

Усложним цикл и выведем квадраты чисел от 0 до 8:

Здесь блок цикла сработает 9 раз, пока значение i не станет равным 9. И каждый раз это значение будет увеличиваться на 1.

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

Формально определение цикла осталось тем же, только теперь первое и третье выражения в определении цикла отсутствуют: for (; i < 9;) . Переменная-счетчик определена и инициализирована вне цикла, а ее приращение происходит в самом цикле.

Можно определять вложенные циклы. Например, выведем таблицу умножения:

Цикл do..while

В цикле do..while сначала выполняется код цикла, а потом происходит проверка условия в инструкции while . И пока это условие истинно, то есть не равно 0, то цикл повторяется.

Здесь код цикла сработает 6 раз, пока i не станет равным нулю. Но важно отметить, что цикл do гарантирует хотя бы единократное выполнение действий, даже если перед первым выполнением условие в инструкции while не будет истинно. То есть мы можем написать:

Хотя у нас переменная i меньше 0, цикл все равно один раз выполнится.

Цикл while

В отличие от цикла do цикл while сразу проверяет истинность некоторого условия, и если условие истинно, то есть не равно 0, то код цикла выполняется:

Например, выведем на консоль все числа от 6 до 1:

Здесь, пока истинно условие i > 0 , будут выполняться действия цикла — printf(«%d \n», i) и i—

Операторы continue и break

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

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

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

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

what's difference between int i=0, int i(0), int *p=new int, int *p=new int(0) in c++ [closed]

I’m going to answer a slightly different question first.

Suppose you have a class or struct (they are pretty much the same) that looks like this:

now, this is much like an int , but there are a few differences.

the above creates an int literal, then constructs Foo i with it using the Foo(int v) constructor.

the above creates an int literal, then constructs Foo i with it using the Foo(int v) constructor. Note that I just repeated myself.

the above creates an int literal, then constructs Foo i with it using the Foo(int v) constructor, then copy-constructs Foo i from it. However, the standard allows the compiler to «elide» (skip) the copy constructor, and instead just construct Foo i directly from the int literal 0 .

this goes to the free store (typically implemented and called the heap), gets ahold of enough memory to store a Foo , then default constructs it. It then returns the address of this Foo object, which is then used to initialize the pointer Foo* i . Now, note that the default constructor of Foo leaves value uninitialized. This is a flaw in my implementation of Foo above (in my opinion), and except in special cases you will rarely want to do this.

Annoyingly, integer ( char , int , long , unsigned char , etc), floating point ( double or float ) literals, and pointer literals all share this property — by default, they are not initialized to any value.

So you should make sure they are explicitly initialized. In the case of Foo , adding a line:

is enough to do it.

this goes to the free store (typically implemented and called the heap), gets ahold of enough memory to store a Foo , then constructs it with the integer literal 0. It then returns the address of this Foo object, which is then used to initialize the pointer Foo* i .

Now, memory in the free store generally stays reserved for your use once you ask for it until you get around to returning it, or your program shuts down. In order to return it, you call delete on the same pointer, which both cases the object ( Foo in this case) to have its destructor called ( Foo has no destructor, so this is skipped), and then the memory is handed back to the free store to be used by a later call to new .

Keeping track of this is a real pain, and a source of a whole bunch of errors, so you should avoid calling new . There are many ways to avoid calling new , including using std::vector to manage blocks of memory, or using shared_ptr and make_shared to create objects on the heap which manage their own lifetime via a technique known as RAII, or using unique_ptr when you want close control over the lifetime of a pointer (sadly, make_unique doesn’t exist).

Now lets go further.

This fails to compile. I said that the constructor for Foo(double) is explicit, and the above only chooses to call constructors that are non-explicit. On the other hand:

these both are willing to call explicit constructors, and work fine.

Next, C++11 brings us uniform initialization. Instead of putting what you want to initialize something with in () , you put it in <> — a squiggly brace.

etc. <> has a few differences compared to () based syntax — most importantly it avoids most vexing parse problems. Other differences include initializer list behavior, dealing with explicitly trivially constructing something ( int x() does not construct an int named x , but int x<> does).

Speaking of which, time to get back to your actual question.

int is different than my struct Foo in a few ways.

First, it is not a class or a struct . So its behavior instead of being determined by some code you write, is instead described extensively by the standard. As it happens, C++ tries to have primitive types like int behave a lot like a simple user defined type like Foo , which is useful.

So while no «copy constructor» is called, and int has no constructors nor destructors at all, int almost exactly behaves «as if» there where such constructors.

creates the integer literal 0, then initializes int i with it. The compiler may elide this, and simply create the integer int i with the value 0 directly. For int , there is no way to observe the difference directly.

is identical to int i = 0 , because int has no non-explicit constructors. It is just a different syntax.

Now, there is the problem of the most vexing parse. If you typed

you’d get the same as int i = 0 , but if you typed

what would happen is that the compiler would say «that could be a function named i with takes zero arguments and returns int «, and for various annoying reasons prefers that interpretation to a default-initialized int . So you get a forward-declared function named i instead of an integer.

As mentioned earlier, the way to avoid this is to always use this syntax:

in C++11 compilers.

which creates a default constructed (uninitialized) int on the free store, and assigns a pointer to it to the variable int *p . A copy has occurred, but it is a copy of a pointer, not the int .

is much the same, but the free store int created has the value 0 instead of being uninitialized.

In both these cases, you are responsible for calling delete once and only once on the value returned by new . Failure to do so is known as a memory leak. Using the pointer value after you have done so results in undefined behavior, and typically memory corruption. You almost certainly won’t be warned by the program that what you did is dangerous, but your program can now do utterly random things that make no sense and still be a valid C++ program. Aligning each new with exactly one delete , and making sure that nobody uses pointers after the delete , and calling delete as early as possible, is an annoying enough program that entire categories of programming languages have been developed whose main selling feature is that they free developers from having to deal with it.

So avoid calling new .

Oh, and because this isn’t long enough, note that my use of the «most vexing parse» above is not quite right. The «most vexing parse» is actually:

because the first could be either a constructor call or a function, while the second cannot be a constructor call because the standard doesn’t allow it.

However, I and most other people find the parsing rules that make int x(); , so call it the penultimate vexing parse, and you won’t be far wrong. It still doesn’t do what you would naively think it should do (create a default constructed x ), so it is plenty vexing.

в чем разница между int i = 0, int i (0), int * p = new int, int * p = new int (0) в переполнении стека

является int *p=new int все еще копировать первоначальный стиль?

когда использовать int i=0; не new int(0) ?

Решение

Сначала я собираюсь ответить на немного другой вопрос.

Предположим, у вас есть class или же struct (они в значительной степени одинаковы), который выглядит так:

Теперь это очень похоже на int , но есть несколько отличий.

вышесказанное создает int буквальный, то конструирует Foo i с помощью использования Foo(int v) конструктор.

вышесказанное создает int буквальный, то конструирует Foo i с помощью использования Foo(int v) конструктор. Обратите внимание, что я только что повторил.

вышесказанное создает int буквальный, то конструирует Foo i с помощью использования Foo(int v) конструктор, затем копи-конструкции Foo i от него. Однако стандарт позволяет компилятору «исключать» (пропускать) конструктор копирования, а вместо этого просто создавать Foo i прямо из int буквальный 0 ,

это идет в бесплатное хранилище (обычно реализуемое и называемое кучей), получает достаточно памяти для хранения Foo , то по умолчанию конструирует его. Затем он возвращает адрес этого Foo объект, который затем используется для инициализации указателя Foo* i , Теперь обратите внимание, что конструктор по умолчанию Foo листья value неинициализированным. Это недостаток в моей реализации Foo выше (на мой взгляд), и за исключением особых случаев вы редко захотите это сделать.

Досадно, целое число ( char , int , long , unsigned char и т. д.), с плавающей точкой ( double или же float ) литералы и литералы-указатели все разделяют это свойство — по умолчанию они не инициализируются каким-либо значением.

Поэтому вы должны убедиться, что они явно инициализированы. В случае Foo , добавив строку:

достаточно сделать это.

это идет в бесплатное хранилище (обычно реализуемое и называемое кучей), получает достаточно памяти для хранения Foo , затем создает его с целочисленным литералом 0. Затем он возвращает адрес этого Foo объект, который затем используется для инициализации указателя Foo* i ,

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

Отслеживать это — настоящая боль и источник множества ошибок, поэтому вам следует избегать звонков new , Есть много способов избежать звонка new в том числе с использованием std::vector управлять блоками памяти, или используя shared_ptr а также make_shared создавать объекты в куче, которые управляют своим временем жизни с помощью техники, известной как RAII, или используя unique_ptr когда вам нужен строгий контроль над временем жизни указателя (к сожалению, make_unique не существует)

Теперь пойдем дальше.

это не компилируется. Я сказал, что конструктор для Foo(double) является явным, и вышеупомянутое только выбирает конструкторы, которые не являются явными. С другой стороны:

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

Далее, C ++ 11 приносит нам равномерную инициализацию. Вместо того, чтобы положить то, что вы хотите инициализировать с () Вы положили это в <> — волнистая скобка.

и т.п. <> имеет несколько отличий по сравнению с () основанный синтаксис — самое главное, он избегает самый неприятный разбор проблемы. Другие различия включают в себя поведение списка инициализаторов, которые имеют дело с явным тривиальным построением чего-либо ( int x() не создает int названный x , но int x<> делает).

Говоря об этом, пришло время вернуться к вашему актуальному вопросу.

int отличается от моего struct Foo несколькими способами.

Во-первых, это не class или struct , Таким образом, его поведение вместо того, чтобы определяться каким-то кодом, который вы пишете, вместо этого широко описывается стандартом. Как это происходит, C ++ пытается иметь примитивные типы, такие как int ведут себя во многом как простой пользовательский тип, такой как Foo , что полезно.

Так что пока не вызывается «конструктор копирования», а int не имеет ни конструкторов, ни деструкторов вообще, int почти точно ведет себя «как будто» там, где такие конструкторы.

создает целочисленный литерал 0, затем инициализирует int i с этим. Компилятор может исключить это и просто создать целое число int i со значением 0 напрямую. За int , нет никакого способа наблюдать разницу напрямую.

идентично int i = 0 , так как int не имеет неявных конструкторов. Это просто другой синтаксис.

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

вы получите то же самое, что и int i = 0 , но если вы набрали

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

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

в С ++ 11 компиляторов.

Далее у нас есть

который создает конструкцию по умолчанию (неинициализированную) int в свободном магазине, и присваивает указатель на него переменной int *p , Копия произошла, но это копия указателя, а не int ,

почти такой же, но бесплатный магазин int созданный имеет значение 0 вместо того, чтобы быть неинициализированным.

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

Так что избегайте звонить new ,

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

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

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

Другие решения

они одинаковы, с двумя различными синтаксисами инициализации (C-подобный первому, стиль конструктора второй); в принципе они немного отличаются при работе с классами (первый подразумевает — потенциально исключенный — вызов конструктора копирования, второй — нет), но для int с они фактически одинаковы.

В первом int не инициализируется (оно будет иметь любое значение в той области памяти); во втором случае int инициализируется до 0.

Но самое главное, это два совершенно разных зверя по отношению к первым. Вы не объявляете автоматические переменные типа int , но указатели в int , которые указывают на два динамически распределяется int s.

Разница глубокая: с int i=0 память управляется автоматически (она имеет продолжительность автоматического хранения), и переменная уничтожается, когда выходит из области видимости; с new Вы выделяете память из freestore (так называемой кучи), которая не имеет метода автоматического освобождения — вы должны освободить ее явно (с помощью delete хотя в современном C ++ интеллектуальные указатели обычно используются для автоматического управления временем жизни динамических объектов).

new обычно используется именно в тех случаях, когда продолжительность автоматического хранения не является хорошим выбором (например, вы хотите, чтобы те, int s для того, чтобы пережить текущую область, или быть разделенным между несколькими объектами или чем-то еще) или когда вы выделяете слишком большой материал, чтобы оставаться в локальной переменной (в типичной реализации локальные переменные идут в стек, который ограничен по размеру; , для двоих int это не действительная проблема).

Для «обычных», местных int переменные, которые должны «умереть» с текущей областью действия new обычно это не очень хорошая идея.

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

в C и ранних C ++ вы могли использовать только int i = 0;

int i(0); шаблон такой же, как конструктор для общего типа

Так что это было добавлено в качестве альтернативы int i=0; который не похож на общий шаблон конструктора. Это полезно, когда используются шаблоны. Таким образом, шаблон может использовать как int, так и классы.

new подразумевает выделение кучи памяти (которая может просочиться), поэтому нет, вы не хотите делать это все время.

int i=0; а также int i(0); эквивалентны, но в зависимости от реализации первый может использовать оператор присваивания, в то время как второй может быть создан со значением. Это может позволить компилятору оптимизировать для целевой архитектуры. В случае с классом метод присваивания, вероятно, медленнее, поскольку он создаст класс (обычно со значениями по умолчанию), а затем выполнит присваивание и уничтожит все значения по умолчанию, которые он только что потратил на присвоение времени.

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

Глава 2. Типы, операторы и выражения

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

Стандартом ANSI было утверждено значительное число небольших изменений и добавлений к основным типам и выражениям. Любой целый тип теперь может быть со знаком, signed , и без знака, unsigned . Предусмотрен способ записи беззнаковых констант и шестнадцатиричных литерных констант. Операции с плавающей точкой допускаются теперь и с одинарной точностью. Введен тип long double , обеспечивающий повышенную точность. Стринговые константы конкатенируются («склеиваются») теперь во время компиляции. Перечислимый тип стал частью языка, формализующей установку диапазона значений типа. Объекты разрешено помечать как const для защиты их от каких-либо изменений. В связи с введением новых типов расширены правила автоматического преобразования из одного арифметического типа в другой.

2.1. Имена переменных

Хотя мы ничего не говорили об этом в гл. 1, но существуют некоторые ограничения на задание имен переменных и именованных констант. Имена составляются из букв и цифр; первой литерой должна быть буква. Знак подчеркивания ‘_’ считается буквой; его иногда удобно использовать, чтобы улучшить восприятие длинных имен переменных. Не начинайте имена переменных с подчеркивания, так как многие переменные библиотечных программ начинаются именно с этого знака. Большие (прописные) и малые (строчные) буквы различаются, так что x и X — два разных имени. Обычно в программах на Си малыми буквами набирают переменные, а большими — именованные константы.

Для внутренних имен значимыми являются первые 31 литера. Для имен функций и внешних переменных число значимых литер может быть меньше 31, так как эти имена обрабатываются ассемблерами и загрузчиками и языком не контролируются. Уникальность внешних имен гарантируется только в пределах 6 литер, набранных безразлично в каком регистре. Ключевые слова if , else , int , float и т.д. зарезервированы, и их нельзя использовать в качестве имен переменных. Все они набираются на нижнем регистре (т.е. малыми буквами).

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

2.2. Типы и размеры данных

В Си существует всего лишь несколько базовых типов:

char единичный байт, который может содержать одну литеру из допустимого набора литер
int целое, обычно отображаемое на естественное представление целых в машине
float число с плавающей точкой одинарной точности
double число с плавающей точкой двойной точности

Имеется также несколько квалификаторов, которые можно использовать вместе с указанными базовыми типами. Например, квалификаторы short (короткий) и long (длинный) применяются к целым:

В таких описаниях слово int можно опускать, что обычно и делается.

Если только не возникает противоречий со здравым смыслом, целое short и целое long должны быть разной длины, а int соответствовать естественному размеру целых на данной машине. Чаще всего для представления целого, описанного с квалификатором short , отводится 16 бит, с квалификатором long — 32 бита, а значению типа int — или 16, или 32 бита. Разработчики компилятора вправе сами выбирать подходящие размеры, сообразуясь с характеристиками своего компьютера и соблюдая только следующие ограничения: значения типов short и int представляются по крайней мере 16 битами, типа long — по крайней мере 32 битами, размер short не больше размера int , который в свою очередь не больше размера long .

Квалификаторы signed (со знаком) или unsigned (без знака) можно применять к типу char и любому целому типу. Значения unsigned всегда положительны или равны нулю и подчиняются законам арифметики по модулю 2 n , где n — количество бит в представлении типа. Так, например, если значению char отводится 8 бит, то unsigned char имеет значения в диапазоне от 0 до 255, а signed char — от -128 до 127 (в машине с двоичным дополнительным кодом). Являются ли значения типа просто char знаковыми или беззнаковыми, зависит от машины, но в любом случае коды печатаемых литер положительны.

Тип long double предназначен для арифметики с плавающей точкой повышенной точности. Как и в случае целых, размеры объектов с плавающей точкой зависят от реализации; float , double и long double могут представляться одним размером, а могут — двумя или тремя разными размерами.

Именованные константы для всех размеров вместе с другими характеристиками машины и компилятора содержатся в стандартных головных файлах <limits.h> и <float.h> . (См. приложение B.)

Упражнение 2.1.

Напишите программу, которая будет выдавать диапазоны значений типов char , short , int и long , описанных как signed и как unsigned , с помощью печати соответствующих значений из стандартных головных файлов и путем прямого вычисления. Определите диапазоны чисел с плавающей точкой различных типов. Вычислить эти диапазоны сложнее.

2.3. Константы

Целая константа, например, 1234, имеет тип int . Константа типа long завершается буквой l или L , например 123456789L; слишком большое целое, которое невозможно представить как int , будет представлено как long . Беззнаковые константы заканчиваются буквой u или U , а окончание ul или UL говорит о том, что тип константы — unsigned long .

Константы с плавающей точкой имеют десятичную точку (123.4) или экспоненциальную часть (1e-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’ , а не какое-нибудь значение (например, 48), которое следует из способа кодировки, мы тем самым делаем программу независимой от частного значения кода, к тому же она и легче читается. Литерные константы могут участвовать в операциях над числами точно так же, как и любые другие целые, хотя чаще они используются для сравнения с другими литерами.

Некоторые литеры в литерных и стринговых константах записываются с помощью эскейп-последовательностей, например \n (новая_строка); такие последовательности изображаются двумя литерами, но обозначают одну. Кроме того, произвольный восьмеричный код можно задать в виде

Читать:
Как сравнить два документа pdf на различия

где ooo — одна, две или три восьмеричные цифры (0. 7) или

где hh — одна, две или более шестнадцатиричные цифры (0. 9, a. f, A. F). Таким образом, мы могли бы написать

или в шестнадцатиричном виде:

Полный набор эскейп-последовательностей следующий:

\a сигнал_звонок
\b возврат_на_шаг
\f перевод_страницы
\n новая_строка
\r возврат_каретки
\t гор_табуляция
\v верт_табуляция
\\ обратная_наклонная_черта
\? знак_вопроса
\’ одиночная_кавычка
\" двойная_кавычка
\ ooo восьмеричный_код
\x hh шестнадцатиричный_код

Литерная константа ‘\0’ — это литера с нулевым значением — так называемая литера null . Вместо просто 0 часто используют запись ‘\0’ , чтобы подчеркнуть литерную природу выражения, хотя и в том и другом случае запись обозначает нуль.

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

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

Кавычки не входят в стринг, а служат только ее ограничителями. Так же, как и в литерные константы, в стринги можно включать эскейп-последовательности; \" , например, представляет собой двойную кавычку. Стринговые константы можно конкатенировать («склеивать») во время компиляции; например, запись двух стрингов

эквивалентна записи одного следующего стринга:

Указанное свойство позволяет разбивать длинные стринги на части и располагать эти части на отдельных строчках.

Фактически стринговая константа — это массив литер. Во внутреннем представлении стринга в конце обязательно присутствует null -литера ‘\0’ , поэтому памяти для стринга требуется на один байт больше, чем число литер, расположенных между двойными кавычками. Это означает, что нет ограничения на длину задаваемого стринга, но чтобы определить его длину, требуется просмотреть весь стринг. Функция strlen(s) вычисляет длину стринга s без учета завершающей его литеры ‘\0’ . Ниже приводится наша версия этой функции:

Функция strlen и некоторые другие, применяемые к стрингам, описаны в стандартном головном файле <string.h> .

Будьте внимательны и помните, что литерная константа и стринг, содержащий одну литеру, не одно и то же: ‘x’ не то же самое, что "x" . Запись ‘x’ обозначает целое значение, равное коду буквы x из стандартного набора литер, а запись "x" — массив литер, который содержит одну литеру (букву x) и ‘\0’ .

В Си имеется еще один вид константы, константа перечисления. Перечисление — это список целых констант, как, например, в

Первое имя в enum (от английского слова enumeration — перечисление. — Примеч-ред.) имеет значение 0, следующее — 1 и т.д. (если не было явных спецификаций значений констант). Если не все значения специфицированы, то они продолжают прогрессию, начиная от последнего специфицированного значения, как в следующих двух примерах:

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

Средство enum обеспечивает удобный способ присвоить константам имена, причем в отличие от #define при этом способе значения констант могут генерироваться автоматически. Перечислимый тип разрешено использовать для определения переменных, однако компилятор не обязан контролировать, входят ли присваиваемые этим переменным значения в их тип. Но сама возможность такой проверки часто делает enum лучше, чем #define . Кроме того, отладчик получает возможность печатать значения перечислимых переменных в символьном виде.

2.4. Декларации

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

Переменные можно распределять по декларациям произвольным образом, так что указанные выше списки можно записать и в следующем виде:

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

В своей декларации переменная может быть инициализирована, как, например:

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

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

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

Реакция на попытку изменить переменную, помеченную квалификаторам const , оставлена на усмотрение компилятора.

2.5. Арифметические операторы

Бинарными арифметическими операторами являются + , — , * , / , а также оператор взятия модуля % . Деление целых сопровождается отбрасыванием дробной части, какой бы она ни была. Выражение

дает остаток от деления x на y и, следовательно, нуль, если x делится на y нацело. Например, год является високосным, если он делится на 4 (но не на 100). Кроме того, високосным считается год, если он делится на 400. Следовательно,

Оператор % к операндам типов float и double не применяется. В какую сторону, (в сторону увеличения или уменьшения числа) будет усечена дробная часть при выполнении / и каким будет знак результата операции % с отрицательными операндами, это зависит от машины.

Бинарные операторы + и — имеют одинаковый приоритет, который ниже приоритета операторов * , / и % , который в свою очередь ниже приоритета унарных операторов + и — . Арифметические операции одного приоритетного уровня выполняются слева направо.

В конце этой главы приводится табл. 2.1, в которой показаны приоритеты всех операторов и порядок их выполнения.

2.6. Операторы отношения и логические операторы

Операторами отношения являются

Все они имеют одинаковый приоритет. Ровно на одну ступень ниже приоритет операторов сравнения на равенство:

Операторы отношения имеют более низкий приоритет, чем арифметические, поэтому выражение типа i < lim-1 будет выполняться так же, как i < (lim-1) , т.е. как мы и ожидаем.

Более интересны логические операторы && и || . Выражения, между которыми стоят операторы && или || , вычисляются слева направо, и вычисление прекращается, как только становится известна истинность или ложность результата. Многие Си-программы опираются на это свойство, как, например, цикл из функции getline , которую мы приводили в гл. 1:

Прежде чем читать очередную литеру, нужно проверить, есть ли место для нее в массиве s . иначе говоря, сначала необходимо проверить условие i < lim-1 . Если это условие не выполняется, мы не должны продолжать вычисление, в частности читать следующую литеру. Так же было бы неправильным сравнивать c с EOF до обращения к getchar ; следовательно, и вызов getchar , и присваивание должны выполняться перед указанной проверкой.

Приоритет оператора && выше, чем оператора || , однако их приоритеты ниже, чем приоритет операторов отношения и равенства; поэтому выражение вида

не нуждается в дополнительных скобках. Но, так как приоритет != выше, чем приоритет присваивания, в

скобки необходимы, чтобы сначала выполнить присваивание, а затем сравнение с ‘\n’ .

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

Унарный оператор ! преобразует ненулевой операнд в 0, а нуль в 1. Обычно оператор ! используют в конструкциях вида

Трудно сказать, какая из форм записи лучше. Конструкция вида !valid хорошо читается («если не valid »), но в случае более сложных выражений может оказаться, что ее не так-то легко понять.

Упражнение 2.2.

Напишите цикл, эквивалентный приведенному выше for -циклу, не пользуясь операторами && и || .

2.7. Преобразования типов

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

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

Как мы уже говорили в гл. 1, выражение

дает числовое значение литеры, хранящейся в s[i] , так как значения ‘0’ , ‘1’ и т.д. образуют непрерывную возрастающую последовательность.

Другой пример приведения char к int связан с функцией lower , которая одиночную литеру из набора ASCII, если она является заглавной буквой, превращает в прописную. Если же литера не является заглавной буквой, lower ее не изменяет.

В случае ASCII эта программа будет правильно работать потому, что между одноименными буквами верхнего и нижнего регистров — одинаковое расстояние (если их рассматривать как числовые значения), и, кроме того, латинский алфавит — плотный в том смысле, что между буквами A и Z ничего кроме букв не существует. Для набора EBCDIC последнее условие не выполняется, и поэтому наша программа в этом случае будет преобразовывать не только буквы.

Стандартный головной файл <ctype.h> , описанный в приложении B, определяет семейство функций, которые позволяют проверять и преобразовывать литеры независимо от набора литер. Например, функция tolower(c) возвращает букву c в коде нижнего регистра, если она была в коде верхнего регистра, поэтому tolower — универсальная замена функции lower , рассмотренной выше. Аналогично проверку

можно заменить на

Далее мы будем пользоваться функциями из <ctype.h> .

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

Гарантируется, что любая литера из стандартного набора печатаемых литер никогда не будет отрицательным числом, поэтому в выражениях такие литеры всегда являются положительными операндами. Но произвольный восьмибитовый код в переменной типа char на одних машинах может быть отрицательным числом, а на других — положительным. Для совместимости переменные типа char , в которых хранятся нелитерные данные, следует специфицировать явно как signed или unsigned .

Отношения типа i > j и логические выражения, перемежаемые операторами && и || , определяют выражение — условие, которое имеет значение 1, если оно истинно, и 0, если ложно. Так, присваивание

установит в d значение 1, если c есть цифра, и 0 в противном случае. Однако функции, подобные isdigit , в случае истины могут выдавать любое ненулевое значение. В местах проверок внутри if , while , for и т.д. «истина» просто означает «не нуль».

Неявные арифметические преобразования, как правило, осуществляются естественным образом. В общем случае, когда оператор типа + или * с двумя операндами (бинарный оператор) имеет разнотипные операнды, прежде чем операция начнет выполняться, «младший» тип подтягивается к «старшему». Результат будет иметь старший тип. В разд. 6 приложения A правила преобразования сформулированы точно. Если же в выражении нет беззнаковых операндов, можно удовлетвориться следующим набором неформальных правил:

  • Если какой-либо из операндов принадлежит типу 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 принадлежит типу int и подтягивается к типу signed long . Но -1L > 1UL , так как -1L подтягивается к типу unsigned long и воспринимается как большое положительное число.

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

Литера превращается в целое посредством размножения знака или другим описанным выше способом.

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

значение c не изменится. Это справедливо независимо от того, размножается знак при переводе char в int или нет. Однако, если изменить порядок присваиваний, возможна потеря информации.

Если x принадлежит типу float , а i типу int , то и x = i , и i = x вызовут преобразования, причем перевод float в int сопровождается отбрасыванием дробной части. Если double переводится в float , то значение либо округляется, либо обрезается; это зависит от реализации.

Так как аргумент в вызове функции есть выражение, при передаче его функции также возможно преобразование типа. При отсутствии прототипа функции аргументы типа char и short переводятся в int , а float — в double . Вот почему мы объявляли аргументы типа int или double даже тогда, когда в вызове функции использовали аргументы типа char или float .

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

приводит выражение к указанному в скобках типу по перечисленным выше правилам. Смысл операции приведения можно представить себе так: выражение как бы присваивается некоторой переменной указанного типа, и эта переменная используется вместо всей конструкции. Например, библиотечная программа sqrt рассчитана на аргумент типа double и выдает чепуху, если ей подсунуть что-нибудь другое. ( sqrt описана в <math.h> .) Поэтому, если n есть целое, мы можем написать

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

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

перед обращением к sqrt в присваивании

целое 2 будет переведено в значение double 2.0 автоматически без явного указания операции приведения.

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

Упражнение 2.3.

Напишите функцию htoi(s) , которая преобразует последовательность шестнадцатиричных цифр, начинающуюся с 0x или 0X в соответствующее целое. Шестнадцатиричными цифрами являются литеры 0. 9 , a. f , A. F .

2.8. Инкрементные и декрементные операторы

В Си есть два необычных оператора, предназначенных для увеличения и уменьшения переменных. Инкрементный оператор ++ добавляет 1 к своему операнду, а декрементный оператор — вычитает 1. Мы уже неоднократно использовали ++ для наращивания значения переменных, как, например, в

Необычность ++ и — в том, что их можно использовать и как префиксные операторы (помещая перед переменной, например, ++n ), и как постфиксные операторы (помещая после переменной: n++ ). В обоих случаях значение n увеличивается на 1. Но выражение ++n увеличивает n до того, как его значение будет использовано, а n++ — после того. Предположим, что n содержит 5, тогда

установит в x значение 5, а

установит в x значение 6. И в том и другом случае значение n станет равным 6. Инкрементные и декрементные операторы можно применять только к переменным. Например, запись (i+j)++ не верна. В контексте, где требуется только увеличить (или уменьшить) значение переменной, как в

безразлично, какой выбрать оператор — префиксный или постфиксный. Но существуют ситуации, когда требуется оператор вполне определенного типа. Например, рассмотрим функцию squeeze(s, c) , которая удаляет из стринга s все литеры, совпадающие с c :

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

Другой пример — функция getline , которая нам известна по гл. 1. Приведенную там запись

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

В качестве третьего примера рассмотрим стандартную функцию strcat(s,t) , которая стринг t помещает в конец стринга s . Предполагается, что в s достаточно пространства, чтобы в нем разместить суммарный стринг. Мы написали strcat так, что она не возвращает никакого результата. На самом деле библиотечная strcat возвращает ссылку на результирующий стринг.

При копировании очередной литеры из t в s постфиксный оператор ++ применяется и к i , и к j , чтобы на каждом шаге цикла переменные i и j правильно отслеживали позиции перемещаемой литеры.

Упражнение 2.4.

Напишите версию функции squeese(s1, s2) , которая удаляет из s1 все литеры, встречающиеся в стринге s2 .

Упражнение 2.5.

Напишите функцию any(s1, s2) , которая возвращает либо ту позицию в s1 , где стоит первая литера, совпавшая с любой из литер в s2 , либо -1 (если ни одна литера s1 не совпадает с литерами из s2 ). (Стандартная библиотечная функция strpbrk делает то же самое, но выдает указатель на литеру, а не номер ее позиции.)

2.9. Побитовые операторы

В Си имеются шесть операторов для манипулирования с битами. Их можно применять только к целочисленным операндам, т.е. к операндам типов char , short , int и long , знаковым и беззнаковым.

Оператор & (побитовое И) часто используется для обнуления некоторой группы разрядов. Например,

очищает в n все разряды, кроме младших семи.

Оператор | (побитовое ИЛИ) применяют для установки разрядов; так,

устанавливает единицы в тех разрядах x , которым соответствуют единицы в SET_ON .

Оператор ^ (побитовое исключающее ИЛИ) в каждом разряде установит 1, если соответствующие разряды операндов имеют различные значения, и 0, когда они совпадают.

Поразрядные операторы & и | следует отличать от логических операторов && и || , которые при вычислении слева направо дают значение истинности. Например, если x есть 1, а y равно 2, то x & y даст нуль, а x && y — единицу.

Операторы << и >> выполняют сдвиг, влево или вправо, своего левого операнда на число битовых позиций, задаваемое правым операндом, которое должно быть положительным. Так, x << 2 сдвигает значение x влево на 2 позиции, заполняя освобождающиеся биты нулями, что эквивалентно умножению x на 4. Сдвиг вправо беззнаковой величины всегда сопровождается заполнением освобождающихся разрядов нулями. Сдвиг вправо знаковой величины на одних машинах происходит с размножением знака («арифметический сдвиг»), на других — с заполнением освобождающихся разрядов нулями («логический сдвиг»).

производит дополнение целого до единиц по всем разрядам, т. е. превращает единичные биты в нулевые и наоборот. Например,

обнуляет в 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 прижимая их к правому краю. Вот эта функция:

Выражение x >> (p+1-n) сдвигает нужное нам поле к правому краю. Константа

0 состоит только из единиц, и ее сдвиг влево на n бит (

0 << n) приведет к тому, что правый край этой константы займут n нулевых разрядов. Еще одна операция побитового отрицания,

0 << n) , позволяет получить справа n единиц.

Упражнение 2.6.

Напишите функцию setbits(x, p, n, y) , возвращающую значение x , в котором n бит, начиная с p -й позиции, заменены на n правых разрядов из y (остальные биты не изменяются).

Упражнение 2.7.

Напишите функцию invert(x, p, n) , возвращающую значение x с инвертированными n битами, начиная с позиции p (остальные биты не изменяются).

Упражнение 2.8.

Напишите функцию rightrot(x, n) , которая циклически сдвигает («вращает») вправо x на n разрядов.

2.10. Операторы присваивания и выражения

в котором стоящая слева переменная повторяется и справа, можно написать в сжатом виде:

Оператор += называется оператором присваивания.

Большинству бинарных операторов (аналогичных + и имеющих левый и правый операнды) соответствуют операторы присваивания op= , где op — один из операторов

Если выр1 и выр2 — выражения, то запись

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

В качестве примера приведем функцию bitcount , подсчитывающую число единичных битов в своем аргументе целого типа.

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

Помимо краткости операторы присваивания обладают тем преимуществом, что они более соответствуют тому, как человек мыслит. Мы говорим «прибавить 2 к i » или «увеличить i на 2», а не «взять i , добавить 2 и затем вернуть результат в i », так что выражение i += 2 лучше, чем i = i + 2 . Кроме того, в сложных выражениях вроде

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

Мы уже видели, что присваивание вырабатывает значение и может применяться внутри выражения; во многих рядовых программах мы видим

В выражениях встречаются и другие операторы присваивания ( += , -= и т.д.), хотя и реже.

Типом и значением любого выражения присваивания являются тип и значение его левого операнда после завершения присваивания.

Упражнение 2.9.

Применительно к числам, в представлении которых использован дополнительный код, выражение x &= (x-1) уничтожает самую правую 1 в x . Объясните, почему. Используйте это наблюдение при написании более быстрого варианта функции bitcount .

2.11. Условные выражения

пересылают в z максимальное из двух значений, a и b . Условное выражение, написанное с помощью тернарного оператора « ?: », представляет собой другой способ записи этой и подобных ей конструкций. В выражении

первым вычисляется выражение выр1. Если его значение не нуль (истина), то вычисляется выражение выр2, и значение этого выражения становится значением всего условного выражения. В противном случае вычисляется выражение выр3, и его значение становится значением условного выражения. Следует отметить, что из выражений выр2 и выр3 вычисляется только одно из них. Таким образом, чтобы установить в z наибольшее из a и b , можно написать

Следует заметить, что условное выражение и в самом деле является выражением, и его можно использовать в любом месте, где допускается выражение. Если выр2 и выр3 принадлежат разным типам, то тип результата определяется правилами преобразования, о которых шла речь в этой главе ранее. Например, если f имеет тип float , а n — тип int , то типом выражения

будет float вне зависимости от того, положительно значение n или нет.

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

Условное выражение часто позволяет сократить программу. В качестве примера приведем цикл, обеспечивающий печать n элементов массива по 10 на каждой строке с одним пробелом между колонками; каждая строка цикла, включая последнюю, заканчивается литерой новая_строка:

Литера новая_строка посылается после каждого десятого и после n -го элемента. За всеми другими элементами следует пробел. Эта программа выглядит довольно замысловато, зато она более компактна, чем эквивалентная программа с использованием if-else . Вот еще один пример:

Упражнение 2.10.

Напишите функцию lower , которая переводит большие буквы в малые, используя условное выражение (вместо if-else ).

2.12. Приоритет и порядок вычислений

В табл. 2.1 показаны приоритеты и порядок вычислений всех операторов, включая и те, которые мы еще не рассматривали. Операторы, перечисленные на одной строке, имеют одинаковый приоритет; строки упорядочены по убыванию приоритетов; так, например, * , / и % имеют одинаковый приоритет, который выше, чем приоритет бинарных + и — . «Оператор» () обозначает вызов функции. Операторы -> и . (точка) обеспечивают доступ к элементам структур; о них пойдет речь в гл. 6, там же будет рассмотрен и оператор sizeof (размер объекта). Операторы * (адресация по указателю) и & (получение адреса объекта) обсуждаются в гл. 5. Оператор «запятая» будет рассмотрен в гл. 3.

Таблица 2.1. Приоритеты и порядок вычислений операторов
Операторы Выполняются
слева направо
справа налево
слева направо
слева направо
слева направо
слева направо
слева направо
слева направо
слева направо
слева направо
слева направо
слева направо
справа налево
справа налево
слева направо
Унарные операторы + , — и * имеют более высокий приоритет,
чем те же операторы в бинарном варианте

Заметим, что приоритеты побитовых операторов & , ^ и | ниже, чем приоритет == и != , из-за чего в побитовых проверках типа

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

Си подобно многим языкам не фиксирует порядок вычисления операндов оператора (за исключением && , || , ?: и « , »). Например, в инструкции вида

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

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

может давать несовпадающие результаты. Результат вызова функции зависит от того, когда компилятор сгенерирует команды увеличения n — до или после обращения к power . Чтобы обезопасить себя от возможного побочного эффекта, достаточно написать

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

возникает вопрос: массив a индексируется старым или измененным значением i ? Компиляторы могут по-разному генерировать программу, что проявится в интерпретации данной записи. Стандарт сознательно устроен так, что большинство подобных вопросов оставлено на усмотрение компиляторов, так как лучший порядок вычислений определяется архитектурой машины. Стандартом только гарантируется, что все побочные эффекты при вычислении аргументов проявятся перед входом в функцию. Правда, в примере с printf это нам не поможет.

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

Похожие статьи