- •Формальные свойства алгоритмов
- •Основные определения языка, структура программы на языке Си.
- •1. Алфавит
- •2. Литералы
- •3. Комментарии
- •Основные математические операции
- •Увеличение значения переменной на 1
- •Представление о префиксной (до) и постфиксной (после) операциях увеличения
- •Перегрузка операции присваивания копированием
- •Притянутые за уши примеры использования
- •Перегрузка операции запятая
- •1.4. Операторы
- •1.4.1. Оператор выражение
- •1.4.2. Пустой оператор
- •1.4.3. Составной оператор
- •1.4.4. Оператор if
- •1.4.5. Оператор switch
- •1.4.6. Оператор break
- •1.4.7. Оператор for
- •1.4.8. Оператор while
- •1.4.9. Оператор do while
- •1.4.10. Оператор continue
- •1.4.11. Оператор return
- •1.4.12. Оператор goto
- •Условный оператор
- •13. Массивы
- •14. Указатели
- •16. Операторы ввода-вывода в языке си (c)
- •Препроцессор Си
- •Директивы
- •Функции Включение
- •Условная компиляция
- •Функции возвращающие значение.
- •Функции с параметрами.
- •Статические переменные
- •Передача аргументов по ссылке
- •23. Урок 12. Локальные переменные и область видимости
- •Объявление локальных переменных
- •Глобальные переменные
- •24. Пространство имен
- •25. Стандартная библиотека языка Си
- •Структура
- •[Править]Библиотечные заголовочные файлы ansi Си
- •[Править]Стандартная библиотека Си в других языках
- •[Править]Общая поддержка библиотек
- •[Править]Встроенные функции компилятора
Притянутые за уши примеры использования
Рассмотрим несколько примеров. Пусть у нас описаны 4 функции и 3 переменных:
//Определяем функции нахождения минимума для разного числа аргументов:int min(int a) { return a; }//Запятая разделяет аргументы функции. Это не операция:int min(int a, int b) { return a < b ? a : b; }int min(int a, int b, int c) { return min(min(a, b), c); }//Вывод аргумента на экран (побочный эффект) и возврат значения аргумента:int out(int x) { cout << x; return x; }int a, b, c; //Это не операция запятая. Это перечисление переменных
Поиграемся с операцией запятая. Прежде всего, проверим её приоритет:
a = (1, 2, 3); //В итоге a == 3, так как запятая возвращает // правый аргументb = 1, 2, 3; //Операция запятая имеет самый низкий приоритет, // поэтому строка эквивалентна (b = 1), 2, 3; // В итоге b == 1, а числа 2 и 3 не используются.
Обратите внимание на то, как скобки изменили смысл выражения. В следующем примере изменение смысла ещё более неожиданно, так как кажется, что ((выражение)) и (выражение) — это одно и то же:
a = min(1, 2, 3); //Запятые разделяют аргументы функции. В итоге a == 1b = min((1, 2, 3)); //Операции запятая. В итоге b = min(3) == 3.
Частая ошибка «паскалистов» — написание через запятую индексов многомерного массива. Допустим, у нас есть два двумерных массива, каждый из который сделан, как массив указателей на одномерные массивы:
int const width = 10, height = 10;int **a = new int*[height];int **b = new int*[height];for(int y=0; y<height; ++y){ a[y] = new int[width]; b[y] = new int[width]; for(int x=0; x<width; ++x) { a[y][x] = 1; //Массив a заполнен единицами b[y][x] = 0; //Массив b заполнен нулями }}
А теперь мы хотим скопировать массив a в b поэлементно, но случайно написали индексы массива через запятую:
for(int y=0; y<height; ++y){ for(int x=0; x<width; ++x) b[y,x] = a[y,x]; //Какой ужас!}
Компилятор в этом случае не выдаст ни ошибок, ни предупреждений. Интересно то, что при просмотре содержимого массива b он будет состоять из единиц, как мы и ожидаем. Вот только при модификации массива b «сам собой» теперь будет модифицироваться и массив a, что может привести к странным глюкам в других местах программы:
b[0][0] = 3;cout << a[0][0] <<endl; //Выведет 3
Для тех, кто не понял, отчего так происходит, поясню. В выражении b[y,x] = a[y,x]; у нас написаны операции запятая, которые «игнорируют» левый аргумент. Поэтому выражение эквивалентно выражению b[x] = a[x];. То есть указатели на стро́ки массива b мы заменяем указателями на стро́ки массива a; после этого строками обоих массивов являются одни и те же буферы памяти. Незаметности ошибки поспособствовало также то, что высота массива совпадает с шириной (в выражении b[x] = a[x] мы индексируем массив, имеющий размер height, индексом, меняющимся от 0 до width-1).
Запятая как точка следования
Важно, что операция запятая определяет в программе точку следования. Это означает, что сначала полностью вычисляется выражение слева от запятой, а затем — выражение справа. Для сравнения, операция сложения точку следования не определяет, поэтому:
a = out(1), out(2), out(3); //В итоге a == 1, а на экран выведется 123, так как // аргументы операции запятая вычисляются слева- // направо (значит, в этом же порядке происходят // вызовы функций)a = ( out(1), out(2), out(3) ); //В результате a == 3, на экране по прежнему 123b = out(1) + out(2) + out(3); //На экране выведутся цифры 123 в неопределённом // порядке. (123 или 321 или ещё как-нибудь)c = min( out(1), out(2), out(3) ); //Аргументы функции. Порядок не определён
В моём случае (Visual C++ 2010, Release) программа вывела 123123123321 при выполнении предыдущего кода.
Если вам интересно, то в программах на C++ есть ещё несколько мест, в которых определены точки следования: это логические операции &&, || и ?:, разделитель операторов ;, точки входа и выхода из функций.
Кроме того, точки следования определены на месте запятых, разделяющих переменные при их описании. Поэтому можете смело писать int x = 1, y = x + 1; не боясь, что y = x + 1 вычислится до того, как иксу присвоится единица. Будьте внимательны, не спутайте эти запятые с теми, что разделяют аргументы функции, — там точек следования нет.
Запятая в операторах циклов
Теперь рассмотрим возможности применения запятой в операторе цикла for. Вначале пример, где запятая разделяет объявления переменных (не является операцией):
for(int x=0, y=3; x<y; ++x) out(x); //Выведет 012
Если в первом выражении цикла for описываются переменные (как в приведённом примере), то они вынуждены иметь одинаковый тип. Кроме выбора инициализирующих значений переменных никакой свободы у нас нет. Единственный способ впихнуть в это выражение какое-либо дополнительное действие — это добавить вызов функции и/или операцию запятая справа от операции инициализации:
for(int x=out(0), y=3; x<y; ++x) out(x); //Выведет 0012for(int x=( out(9), 0 ), y=3; x<y; ++x) out(x); //Выведет 9012
Последний пример — совершенно бесполезная вещь, ибо out(9) можно написать и перед циклом. Ещё малополезные применения:
for(int x=0, y=9; x<y; ++x, --y) out(x), out(y); //Выведет 0918273645
Запятая позволила нам модифицировать два индекса в операторе цикла, а также обойтись без фигурных скобок при выводе двух чисел. Последний пример можно переписать так:
for(int x=0, y=9; x<y; ) { out(x); out(y); ++x; --y; }
Возникает впечатление, что польза есть только от разделительных запятых, а запятая-операция лишь усложняет понимание программы, не давая никаких новых возможностей программисту. Зачем эта операция вообще была введена в C и C++?
Мне всё же удалось найти два полезных применения операции запятая. Первое — это выполнение дополнительных действий в условных выражениях циклов for и while. Второе — это перегрузка операции запятая для того, чтобы она служила совершенно иным целям.
Вначале рассмотрим использование в условных выражениях циклов. Допустим, перед проверкой условия цикла требуется выполнить некоторые действия (например, вычислить значение функции), и вычисленное значение требуется нам внутри цикла. Тогда есть смысл написать такой код:
//Перебираем значения x. Условие продолжения работы цикла --// положительность некоторой функции f(x)for(float x=0.0, y; y=f(x), y>0.0; x+=0.1) cout << x << ", " << y << endl;
Аналогично для цикла while:
float x = 0.0, y;while(y=f(x), y>0.0) { cout << x << ", " << y << endl; x+=0.1; }
Обратите внимание, что описание переменных мы можем поместить перед циклом, наращивание значений этих переменных можем поместить в конце тела цикла, а вот вызов функции f(x) мы не можем никуда перенести, иначе нам придётся написать его дважды: перед циклом и в конце тела цикла.
Приведённые примеры можно реализовать и без запятых, используя возвращаемое значение операции присваивания:
for(float x=0.0, y; (y=f(x)) > 0.0; x+=0.1) cout << x << ", " << y << endl;float x = 0.0, y;while( (y=f(x)) > 0.0 ) { cout << x << ", " << y << endl; x+=0.1; }
Но мы смогли обойтись без запятой лишь потому, что задача простая. Было бы у нас, например, две функции вместо одной — и запятая бы очень пригодилась.
Творческое применение операции запятая описанным выше способом можно найти в следующей программе, вычисляющей 800 десятичных цифр числа (подробности здесь):
#include<stdio.h>int a=10000,b,c=2800,d,e,f[2801],g;int main(){for(;b-c;)f[b++]=a/5;for(;d=0,g=c*2;c-=14,printf("%.4d",e+d/a),e=d%a)for(b=c;d+=f[b]*a,f[b]=d%--g,d/=g--,--b;d*=b);}