Экономное вычисление логических выражений.
Результат вычисления логического выражения часто можно получить, вычислив только его часть. Пусть в следующем операторе цикла i равно 10 while(i<5 && a> b) {…} Тогда, чтобы определить значение логического выражения в заголовке цикла, достаточно выполнить первую операцию сравнения i<5. Полученный при этом ноль, явится, независимо от значений a и b, результатом вычисления выражения в целом. Если несколько подвыражений связаны операцией ||, ненулевой результат вычисления любого подвыражения также определяет результат вычисления всего выражения. В таких случаях С++ прерывает вычисления, чтобы не тратить лишнего времени на получение уже известного результата. Экономное вычисление логических выражений может порождать скрытые ошибки, связанные с тем, что могут не выполняться подвыражения, в которых изменяются значения переменных. Пусть, например, нужно выводить на экран последовательность не более, чем 10 значений a/b, a/(b2), a/(b3), a/(b4), до тех пор, пока выводимые числа больше единицы, но не менее, чем 10 чисел. При a< b cледующий фрагмент решает эту задачу правильно I=9; while (a>1 || I-->0) { a=a/b; printf(“ %f ”, a); } . Но если исходные данные будут равны, например, a=3, и b=0.9, мы получим бесконечный цикл. Подвыражения, связанные операциями && или ||, вычисляются строго слева направо. Вычисления a>1 достаточно для определения значения выражения в целом. Определив, что результат выражения a>1 || I-->0 равен единице, подвыражение I-->0 пропускается и из счетчика циклов I не вычитается единица. 1.5.11. Условная операция Операции, имеющие один операнд, называются унарными, два – бинарными. В С++ есть операция, имеющая три операнда – тернарная операция.
Записывается она в виде трех выражений, разделенных символами ‘? ’ и ’: ’: <выражение1>? <выражение2>: <выражение3>. Выполнение совпадает с логикой работы условного оператора: – если выражение 1 не равно нулю, результат операции получается вычислением выражения 2; – если выражение 1 равно нулю, вычисляется выражение 3. Рассмотрим несколько типичных примеров Оператор B= A>0? A:- A; запишет в переменную В модуль числа A, а оператор С= A> B? A: B; запишет в С большее из чисел A и B. Отличие этой операции от условного оператора заключается в том, что она может быть частью выражения. Например, сумму модулей переменных A и B можно вычислить следующим образом: С= A>0? A:- A+ B>0? B:- B В операторах цикла часто приходится выполнять разные действия в четных и нечетных проходах. Рассмотрим вычисление суммы первых ста слагаемых последовательности 1, -1/2, 1/3, -1/4, 1/5, …, 1/i*(-1i+1) Писать для решения такой задачи отдельную функцию возведения целого числа в степень i очень неэкономно в вычислительном отношении. Удобнее ввести переменную «маятник», которая при каждом повторении тела цикла изменяет свое значение на противоположное: int a=-1; float S=0; for (int i =1;i<=100;i++) S=S+(a=a>0?-1:1)*1.0/i; Чему будет равна сумма, если оператор цикла будет иметь вид for (int i =1;i<=100;i++) S=S+(a=a>0?-1:1)*1/i; Если Вы не видите отличий от предыдущего оператора, попробуйте скопировать через буфер обмена оба оператора в программу и выполнить. Каждое из выражений, входящих в условную операцию, в cвою очередь, может быть построено при помощи условной операции. Пусть, например, надо ограничить переменную R диапазоном 10<= R <=127. Это можно делать оператором R= R<127? R>10? R:10:127; Чтобы результат вычислений стал понятнее, можно, хотя и не обязательно, разделить выражения, входящие в тернарную операцию, скобками: R=( R<127 )?( R>10?R:10 ):( 127 ); Если переменная R>=127 в нее заносится 127, а
если R<127 выполняется операция R>10?R:10,которая при R > 10 оставляет переменную неизменной, а при R <=10, заносит в нее 10. 1.5.12. Составные присваивания С простым присваиванием = мы уже знакомы. При написании различных программ можно заметить, как часто встречаются операторы, в которых читается значение переменной, с ней выполняется одна математическая операция и результат заносится в ту же переменную. Замена операций типа A=A+1 или A=A-1 на A++, A-- заметно сокращает текст программы. Учитывая, что хороший стиль программирования требует давать переменным длинные осмысленные имена, в языке введена краткая нотация и для других подобных операций. Если Variable1, Var2 – это имена переменных, то следующие пары выражений эквивалентны: Variable1+=Var2 Variable1= Variiable1+Var2 Variable1-=Var2 Variable1= Variable1-Var2 Variable1*=Var2 Variable1= Variable1*Var2 Variable1/= Var2 Variable1= Variable1/ Var2 Variable1&= Var2 Variable1= Variable1&Var2 Variable1%=Var2 Variable1= Variable1%Var2 Variable1^= Var2 Variable1= Variable1^ Var2. 1.5.13. Операция соединения (запятая) Операция соединения имеет самый низкий приоритет и используется для того, чтобы записать последовательность разделенных запятой выражений там, где согласно синтаксису должно быть записано только одно выражение. Результатом операции считаются значение и тип последнего из соединенных выражений. Таким путем можно, например, всю задачу вывода степеней числа два поместить в заголовок цикла, а тело цикла оставить пустым – там будет только точка с запятой, означающая пустой оператор. float st2; int I; Void main(void) { for(I=1,st2=1;I<5;I++,st2*=2,printf(“\n%g”,st2)); } Замечание. Если объявить переменную I внутри цикла – появится ошибка. В объявлении одной строкой двух переменных, как заголовке цикла (int I=1, st2=1; I<5;I++,st2*=2,printf(“\n%g”,st2)) запятая понимается компилятором какобъявлениеи инициализация двух переменных целого типа. В результате в программе будет две переменных с именем st2: вещественная глобальная, объявленная первой строкой программы, и целочисленная локальная, объявленная в заголовке и применяемая только внутри фигурных скобок. Потом во время выполнения степени после 15-й вычисляются неправильно, кроме того, возникает ошибка в printf() –- переменная st2 стала целой и неправильно выводится по формату g.
1.6. Точки последовательности Вернемся к сделанному в начале изучения выражений замечанию о порядке вычисления подвыражений, записанных в скобках. Когда эти подвыражения связаны операциями одинакового приоритета (например, операциями умножения и деления): A=i=10; C=B=5; K=(A+I)*(C+B)/(I+=10);, содержимое скобок вычисляется в произвольном порядке. Если третья скобка (I+=10)вычислится первой, первый сомножитель (A+I) будет равен тридцати, а если последней, то (A+I) равно двадцати. Поэтому переменная K может получить значение 3.5 или 4. Для устранения неоднозначности не следует изменять внутри выражения значение переменной, которая включена одновременно в несколько подвыражений одинакового приоритета. Язык С++ гарантирует вычисление содержимого скобок слева направо только если скобки связаны операциями && - логическое и; || логическое или;, (запятая) объединение; ?: - условная операция. Говорят, что эти операции задают в выражении точки последовательности (или являются точками последовательности), потому что они устраняют неопределенность в последовательности вычисления выражения. Все операции части выражения, которая находится слева от точки последовательности и не ниже ее по приоритету, обязательно вычисляются раньше той части выражения, которая находится справа от точки последовательности. Наличие в выражении точек последовательности влияет и на операции инкремента и декремента: к моменту достижения точки последовательности должны выполниться как префиксные, так и постфиксные операции. В следующем примере i=1; j=1; i= i++&&(j= i); переменная i значение 1, переменная j получит значение 2 (а не 1), потому что операция i++ выполняется не после вычисления выражения в целом, а после достижения точки последовательности – операции &&. Таким образом, в момент выполнения j= i, значение i уже равнo двум. В результате его выполнения следующих операторов
j=5; i=1; i=(i++-1)&&(i=-- j); переменная i получит значение 0, а j – 5. При вычислении выражения сначала вычислится i-1 по результату 0 дальнейшие вычисления прекратятся и в i запишется общий результат ноль. Ни до инкремента ни до присваивания i =-- j дело просто не дойдет. (Но если начальное значение i задать отличным от единицы, i-1не будет равно нулю, инкремент выполнится до присваивания i=-- j). Рассмотрим два оператора i= i--&& j= i i=-- i&& j= i;, В них и постфиксная операция i— и префиксная – i должны выполнится ло точки последовательности &&. Но различия в результатах вычисления выражений остаются: – в операторе i= i-- &&(j=i); i-- - это тоже выражение. Сначала оно вычисляется (без декремента, это просто чтение i), потом проверяется возможность сокращенного умножения, а уже потом декремент; – в операторе i= -- i &&j=i; сначала выполняется декремент, а потом проверяется возможность сокращенного вычисления. Если место декремента использовать инкремент, операторы, естественно, тоже работают по-разному. Чтобы обнаружить различия, достаточно перед выполнением следующих операторов присвоить i значение минус единица: i=-1; j=1; i=i++&&(j=i); 1.1.1. и фрагмент, использующий префиксную форму: i=-1; j=1; i=++ i&&(j= i); Объясните, что запишется в переменные j, i и почему. Кому интересно, может посмотреть, как компилируется выражение i=i++&&(j=i) компилятором Microsoft: mov eax, i - это мы вычислили и запомнили выражение до точки последовательности mov ecx, i add ecx,1 выполнили инкремент i – как видим, обычным сложением без inc i mov i,ecx test eax,eax проверили то значение i, что было до прибавления единицы je M1 при ноле отрабатываем вариант сокращенного вычисления выражения mov edx, i во втором подвыражении, присваивании, используем уже увеличенный i mov j, edx cmp j, 0 je M1 mov [ebp-4Ch],1 jmp M2 M1: [ebp-4Ch],0 M2 mov eax, [ebp-4Ch] mov, eax Для выполнения задания наиболее интересным в этом примере является то, что вычисленная перед точкой последовательности часть выражения (а это просто переменная i) сохраняется в регистре, потом выполняется инкремент i, а далее для решения вопроса о возможности сокращенного вычисления выражения проверяется не увеличенное i, а запомненное в регистре значение. В различных приложениях очень часто приходится циклически изменять значение целочисленной переменной: – увеличивать в цикле значение переменной i на единицу, после достижения максимального значения iMax устанавливать i в ноль, и в следующем проходе продолжать увеличение; – уменьшать значение переменной i на единицу, и каждый раз после достижения нуля заносить в нее максимальное значение iMax.
Такая задача возникает при реализации меню: нажимая стрелку влево мы перемещаем курсор (цветовое выделение пункта меню), но после достижения первого пункта выделение перескакивает на последний. При вводе данных с клавиатуры или выводе на экран бегущей строки часто организуют кольцевой буфер символов (это массив символов и целочисленная переменная, содержащая номер текущего элемента массива). Буфер называется кольцевым, потому что после ввода (вывода) символа в последний элемент массива, текущая позиция переставляется на его начало. В таких ситуациях полезна условная операция: i = i?-- i: iMax, которая при i не равном нулю, выполняет второе подвыражение --i и уменьшает i на единицу. При достижении переменной i нуля, выполняется третье подвыражение и в переменную i заносится значение iMax. Заметим, что похожее выражение i=i? i--:iMax при i не равном нулю решает эту задачу неправильно – значение i не изменяется. Если, например, i=4, сколько ни повторяй оператор i = i? i --: iMax, в переменной i останется 4. Этот результат на первый взгляд кажется не соответствующим смыслу операции. Вот пример убедительных, но неверных рассуждений: – пусть i=4. Вычисление первого выражения i = i дает не ноль, поэтому вычисляется выражение 2 - i --; – учитываем, что операция i-- уменьшает i после вычисления выражения, поэтому значение выражения 2 без декремента (число 4) заносится в левую часть, то есть, в i; – декремент записан после i, поэтому после вычисления выражения переменная i уменьшится на единицу. Там получается 3. Однако отладчик показывает, что этого не происходит – в i остается 4. Поскольку наши рассуждения дают не то, что вычисляет программа, считаем, что встретились с редким, но реальным случаем – компилятор неправильно преобразует выражение в машинные коды (вспоминаем реализацию функции getch() в консольных приложениях). Чтобы в этом убедиться, проверяем выполнение оператора на компиляторах фирм Borland и Microsoft – видим, что оба работают одинаково. Убедившись, что выхода нет, анализируем выражение тщательнее и видим причины ошибки, их две: – второе выражение условной операции i -- содержит декремент в постфиксной форме. Это значит, что сначала оно вычисляется без декремента и результат заносится в регистровую переменную R. (Наше второе выражение – это просто переменная i, ее значение 4 является результатом R вычисления второго выражения); – мы считали, что декремент i-- выполняется после вычисления выражения i=i? i--:iMax, но не учитывали, что операция i? i--:iMax является точкой последовательности, поэтому единица вычитается из переменной i до присваивания (i становится равным 3); – присваивание выполняется после декремента. Число 4 из регистровой переменной R заносится в i и стирает только что записанную туда тройку. Разумеется, реально столкнувшись с тем, что не понимаю, как работает оператор, я не стал задумываться над его синтаксисом и семантикой, а просто посмотрел ассемблерную реализацию оператора. Анализ формируемого компилятором фирмы Borland ассемблерного кода показывает, что в принципе он обрабатывает точки последовательности так же, как Microsoft: cmp DGROUP:_i,0 je short @1@58 mov ax, DGROUP:_i вот запоминание выражения в регистровой переменной ax dec DGROUP:_i а вот декремент i, который уменьшает i до трех jmp short @1@86 @1@58:mov ax,10 @1@86:mov DGROUP:_i, ax операция присваивания заносит в i четверку из ax
Авторы учебников по языку ассемблера, обычно пишут, что его изучение необходимо для того, чтобы потом хорошо понимать языки высокого уровня. Эти утверждения выглядят достаточно голословно, поэтому мы так подробно рассмотрели, как анализ семи строчек ассемблерного кода помогает понять реализованную в языке высокого уровня Си обработку точек последовательности.
1.7. Обмен с файлами 1.7.1. Последовательный доступ к файлу Файл – это размещенный во внешней памяти и имеющий имя массив данных. Работа с файлами требует выполнения следующих этапов: – открытие файла функцией fopen(), после которого файл становится доступным для обмена данными; – выполнения функциями fscanf(),fread(), fprintf(),fwrite() и др. операций чтения данных из файла в объявленные в программе переменные или записи из переменных в файл; закрытие файла функцией fclose(), после которого обмен с ним невозможен. Содержимое файла, как и основной оперативной памяти – это последовательность байтов. Но при обмене с файлом данными, можно считать, что эта последовательность структурирована и состоит из строк текста, однотипных структур данных, вещественных чисел и т.п. В языке Паскаль тип элементов, из которых состоит файл, задавалась при его открытии: если файл открывается как файл, состоящий из записей, то в него уже нельзя записать, например, целое число. Одной операцией обмена можно было прочитать из файла или занести в файл одну запись заданного при открытии типа. Чтобы записывать в файл целые числа, можно было его же открыть как файл, состоящий из целых чисел. В языке Си, один раз открыв файл, можно выбором функций обмена с файлом изменять трактовку его элементов (как строк текста, чисел, блоков, состоящих из нескольких байтов). В оперативной памяти ЭВМ каждый байт имеет свой индивидуальный номер – адрес, и процессор обращается к нужному байту, задавая его адрес. В операциях обмена с файлами адрес порции данных не указывается – данные читаются из «текущей позиции» в файле. Можно считать, что где-то хранится невидимый программе (то есть не объявленный явно как переменная) указатель текущей позиции, начиная с которой операции ввода-вывода читают или записывают данные. Данные из файла читаются последовательно: при открытии файла указатель текущей позиции обычно устанавливается на его первый байт. После считывания данных указатель автоматически перемещается дальше – на первый еще не прочитанный байт. До распространения накопителей на магнитных дисках основным устройствами внешней памяти были работающие в старт-стопном режиме ленточные магнитофоны. Магнитофон протягивал ленту и текущая позиция соответствовала участку ленты, проходящему под считывающей головкой. По той же причине использовался последовательный обмен с файлами – в силу физического устройства накопителя следующий участок ленты оказывался готов к чтению или записи после чтения предыдущего элемента. В результате, при последовательном доступе к файлу для того, чтобы после открытия файла получить содержимое N-го элемента, необходимо предварительно прочитать N-1 предыдущих элементов. Рассмотрим функции, необходимые для последовательного обмена с файлами. Прототипы функций обмена и необходимые типы данных описаны в уже известном нам заголовочном файле stdio.h, поэтому в исходном тексте программы должна быть строка: #include <stdio.h> Если будет использоваться функция завершения программы exit(), необходимо также подключить заголовочный файл process.h или stdlib.h. 1. Открытие файла. При открытии файла путь к файлу (имя диска, каталог, имя файла) связывается с файловой переменной, которая объявляется как указатель на структуру типа FILE: FILE *f;. По терминологии С++ (и не только его) файловая переменная дает доступ к потоку входных или выходных данных. Из входного потока функции ввода выбирают байты и заносят их в объявленные в программе переменные. В выходной поток функции вывода помещают данные из переменных, чтобы поток унес их внешнему устройству. Директивами операционной системы можно связывать поток с разными внешними устройствами и, не изменяя операторов программы, перенаправить вывод из файла на дисплей, или ввод с клавиатуры подменить вводом из текстового файла. Функцией открытия потока fopen() программа связывает поток с конкретным источником данных (обычно для этого указывают путь к файлу, но можно указать и закрепленное за устройством имя: “PRN” – принтер и т.д.). Пример открытия: FILE *FileName = fopen(“C:\temp\abc.txt”, “r”). Функция возвращает указатель на потоккоторыйзаписывается в переменную(у нас это FileName) и идентифицирует файл во всех последующих операциях. Один указатель на структуру FILE позволяет открыть один поток. Но можно, объявив несколько переменных, открыть одновременно несколько потоков и даже связать их с одним файлом. Второй параметр функции открытия файла – строка, задающая режим открытия. Файл, связанный с потоком, можно открыть в одном из следующих режимов: “w” – создается новый файл, открытый для записи. Если файл с таким именем уже существовал, то его предыдущее содержимое стирается. “r”- существующий файл открывается только для чтения. “a”- открывается (или создается, если файла нет) для добавления в него новой порции информации. В отличие от режима “w”, режим “a” позволяет открывать уже существующий файл, не уничтожая его содержимого. При таком открытии текущая позиция сразу устанавливается в конец файла (после его последнего байта). Строка режима может содержать также буквы t или b, который задают текстовый или двоичный режим доступа к файлу. В текстовом режиме прочитанная из потока комбинация символов CR (13) и LF(10) преобразуются в один символ с кодом 13. При выводе символа CR в файл записывается два байта с кодами 13 и 10. В двоичном режиме эти преобразования не выполняются. По умолчанию (при отсутствии букв t,b) установлен текстовый режим. Но установку по умолчанию можно изменить, функцией fmode(), объявленной в файле fcntl.h. Если в строке режима присутствует символ “+”, то разрешены как вывод в поток, так и чтение из него, т.е. режим “r+” открывает существующий, а “w+” создает новый файл для чтения и записи. При открытии потока могут возникать ошибки: файл не найден, диск заполнен или защищен от записи. В этом случае функция fopen() возвратит значение NULL. Номер ошибки при этом заносится в переменную int errno, определенную в заголовочном файле stdio.h. Для вывода на экран дисплея сообщения об возникшей при открытии потока ошибке можно использовать стандартную функцию printf() или специальную функцию perror(). Функция void perror (char *s) выводит строку символов, адресуемую указателем s. Текст выводится также на экран, но через специальный поток, предназначенный для вывода сообщений об ошибках. Поэтому пользователь может перенаправить эти сообщения, связав этот поток с файлом этот поток с файлом или пустым “NUL” устройством. (Детальнее стандартные потоки мы обсудим позже). В следующем фрагменте, где выполнение операции открытия совмещено с проверкой ее выполнения,
# include < stdio. h > Начало примера из файла f read. cpp. #include <process.h> FILE *f; v oid main (void) { if ((f =fopen (“DATA.txt”,”rt+”)) = = NULL) { perror (“ошибка при открытии файла DATA. txt \ n”); exit (0); // Или сделать запрос другого имени файла } perror() выводит сообщение в открытый по умолчанию поток stderr. 2. Обмен данными с файлом. После открытия файла данные считываются из него функциями ввода fread(), fscanf(), fgets(), fgetw(),fgets() или записываются функциями вывода fwrite(), fprintf(), fputs() и другими. В функциях ввода-вывода указывается, из какого открытого потока и в какие переменную читают данные из файла, и другие необходимые данные. Например, функция fread(), имеет четыре параметра: long fread (ptr, - указатель на переменную, в которую будут прочитаны данные, int r, - размер читаемого с диска блока байтов, int с, - количество блоков, читаемых одним оператором fread();, FILE * f, - указатель на открытый поток. );. Функция fwrite() имеет такие же параметры: fwrite (адрес буфера, размер блока, количество блоков, указатель на поток), но первый параметр содержит адрес переменной, из которой данные записываются на диск. Заметим, что выполнение функций fread(), fwrite() не зависит от установки при открытии файла текстового или двоичного режима – функции всегда передают указанное в них количество байтов безо всяких преобразований. Рассмотренные функции удобно применять для обмена с файлом данными (или массивами данных) типа структура: Struct abc {int p1; float p2; } s1, s2; объявили переменные.
fread (& s1, sizeof (abc), 1, f); прочитали данныеиз файла в структуру s1. fread (& s2, sizeof (abc), 1, f); прочитали данныеиз файла в структуру s2. При таком применении мы считаем, что элементами файла являются структуры типа abc. Но ничто не мешает нам объявить переменную целого типа или массив слов short buf[300]; и прочитать в него следующие после первых двух структур байты оператором fread (buf,2,300, f); Это удобно, когда формат хранящихся в файле данных состоит из разнотипных элементов. Например, картинка, хранящаяся в файле с расширением bmp, содержит заголовок типа tagBITMAPFILEHEADER, второй заголовок типа tagBITMAPINFOHEADER, после которых может следовать последовательность слов, каждое из которых кодирует цвет одной точки изображения. Напомним, что считывание данных производится из текущей позиции, значение которой равно номеру читаемого из файла байта. При открытии файла текущая позиция устанавливается на первый байт файла и равна нулю. После считывания каждого следующего байта значение текущей позиции увеличивается на единицу. Если требуется рассматривать файл как последовательность строк текста, каждая из которых заканчивается символом с кодом 13 (или парой символов с кодами 13,10), применяют функции форматного ввода-вывода fprint(), fscanf(). Они отличаются от соответствующих функций printf(), scanf() только тем, что имеют дополнительный первый параметр, которым задается используемый для обмена открытый поток: int fprint (указатель на поток, форматная строка, список переменных); int fscanf (указатель на поток, форматная строка, список адресов переменных);. Ниже приведен пример записи в файл целых чисел от 10 до 20 и их квадратов. Выше файл был открыт строкой f=fopen (“Data.txt”,”r+”), для чтения и записи, поэтому в тот же файл записываем строки текста: for (int n=10; n<21; n++) fprintf (f,” Число % d Квадрат числа % d \ n”, n, n* n); fclose (f); } Конец примера из файла fread. cpp.
3. После завершения обмена поток f закрывают функцией fclose(f). В качестве примера работы с файлом, составим программу, которая добавляет данные в состоящий из однотипных элементов файл City.dat,. Каждый элемент содержит с следующие данные о городах мира: название; численность населения; территория. . # include < stdio. h> Начало примера из файла structst. cpp # include < conio. h> Описываем структуру с полями для названия, численности и территории. Struct metropol { char name[20]; int population; float territory; } work; Переменную work будем использовать для записи в файл. У нас не было примера функции, возвращающей в качестве результата структуру, поэтому заполнение полей структуры данными с клавиатуры реализуем функцией:
Воспользуйтесь поиском по сайту: ©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...
|