1. ОБЗОР ЯЗЫКА ПРОГРАММИРОВАНИЯ С Разработан в 1972 году Д.Ритчи в фирме Bell Laboratories. Его предшественниками явились язык BCPL (Ричардсон, Кембридже, Англия) и язык В (1970 год, Томпсон) для ранней версии ОС UNIX для компьютера РDP 11. У языка В не было типов данных, его единственным объектом было машинное слово. Для получения доступа к машинным словам использовались переменные, содержащие указатели. Язык Си устранил эти недостатки. Сохранились указатели, но появились и более сложные структуры данных: массивы, структуры. Си - современный язык. Его структура позволяет программно использовать нисходящее проектирование, структурное программирование и пошаговую разработку модулей. Си - мобильный язык. Легко переносится на другие типы машин с небольшими изменениями. Си - эффективный язык. Программы на Си компактны и быстродействующие. Си - мощный и гибкий язык. Большая часть UNIX написана на Си, компиляторы и интерпретаторы Fortran, Pascal, Basic, Лисп, Лого. Используются для решения физических, технических проблем, производства мультфильмов. Си - удобный язык. Сняты многие ограничения. С++ - язык программирования, используемый в задачах от системного программирования до проблем искусственного интеллекта. Разработал С++ сотрудник Bell Lab’s в 1983 году Брайен Страуструп. В 1989 году фирма Borland создала Turbo C++ 1.0. А в 90г. фирам Borland C++ 2.0. Появилась возможность программировать в среде Windows Borland C++ 3.0 имеет библиотеку классов Objekt Windows. Язык Turbo Pascal 6.0 заимствовал часть идей из С++. В 1991 году в Москве, на выставке программных продуктов 80% программ было написаны на Си и Pascal. Си - олицетворяет гибкость и богатство возможностей, часто в ущерб безопасности и простоте, надёжность, в ущерб универсальности и изяществу Паскаль. Borland С++ является интегрированной средой программирования имеющей текстовый редактор, компилятор, редактор связей, отладчик, мощные средства настройки среды. Си - это язык "компилирующего" типа. Компилятор - это системная программа, которая преобразует "язык высокого уровня" в язык компьютера - машинный язык. Компиляторы удобны при разработке больших программ, многократного использования. Дальнейшим продолжением развития Си++ и объектно-ориентированного стиля программирования явилась разработка среды программирования Borland C++ Builder, позволяющая быстро создавать программы, работающие в среде Windows 95, 98. 2. ЭТАПЫ СОЗДАНИЯ ПРОГРАММЫ 1) Программа готовится с помощью любого текстового редактора и запоминается в исходном файле с расширением *.С, *.СРР. 2) Преобразуется компилятором в объектный файл *.obj. 3) Вместе с другими объектными файлами преобразуется в исполняемый файл программой, называемой загрузчиком или редактором связей *.EXE. Этот файл уже может быть исполнен компьютером. 3. СТРУКТУРА ПРОГРАММЫ НА ЯЗЫКЕ СИ Программа на языке Си определяется как совокупность одного или нескольких модулей. Модулем является самостоятельно компилируемый файл. Модуль содержит один или несколько функций. Функция состоит из операторов языка Си. Рис.1. 2 Программа на Си Модуль 1 Функция 1 Операторы Функция 2 Операторы Модуль 2 Функция 1 Операторы Функция 2 Операторы Рис.1 Структура программы на языке Си. 3.1. Внутренняя структура программы Исполняемая программа на Си состоит из 4 частей: область команд, область статических данных, область динамических данных, область стека см. Рис.2. 1. Область команд содержит машинные команды; 2. Стек используется для временного хранения данных и адресов возврата при вызовах подпрограмм; 3. Область статических данных для хранения переменных программы; 4. Область динамических данных для размещения дополнительных данных, которые появляются в процессе работы программы. 3.2. Пример программы на СИ заголовок # include < stdio.h > //включение файла void main (void) /*пример*/- имя функции и комментарии { тело функции int num; num = 1; printf("пример программы"); // вывод на экран printf("на Си"); } // конец тела функции Программа Си всегда имеет функцию main(). С нее начинается выполнение программы. Область стека расширяется Старшие адреса 3 Рис.2. Внутренняя структура программы на Си. 4. БАЗОВЫЕ ЭЛЕМЕНТЫ ЯЗЫКА СИ Комментарии – используются для документирования программы. // - далее все игнорируется до конца строки. /* Текст */ - в любом месте программы /* Строка 1 - для комментария любой длины строка 2 строка 3 */ 2. Идентификатор - это имя, которое присваивается какому-либо объекту (переменной). Используются строчные и прописные английские буквы, цифры и знак подчёркивания. Строчные и прописные буквы различаются. Начинаются с буквы или знака подчеркивания. Длина не ограничена. В современном программировании часто используется для создания идентификаторов Венгерская нотация. Например: WordParametrFormat или Word_Parametr_Format. 3. Служебные слова – это слова, с которыми в языке жестко сопоставлены определённые смысловые значения и которые не могут быть использованы для других целей. Это имена операторов, команды препроцессора и так далее. 1. 5. ДАННЫЕ В ПРОГРАММЕ НА СИ Каждая программа оперирует в программе с данными. Они присутствуют в программе в виде переменных и констант. Данные, которые могут изменяться или которым может присваиваться значения во время выполнения программы, называются переменными. Данные, которым устанавливаются определенные значения и они сохраняют свои значения на всем протяжении работы программы, называются константами. 5.1. Константы Константы - это фиксированные значения. Значение, будучи установлено, больше не меняется. Типы констант: a) Целые и длинные целые. Записываются в десятичной, восьмеричной и шестнадцатеричной системе счисления. Десятичная система: 4 Размер: 16 битов Диапазон чисел: 32768 знаковое 0 – 65535u беззнаковое - (unsigned) 32 бита 2147483648l знаковое длинное – (long) 0 – 4294967295ul беззнаковое длинное - (unsigned long ) Пример: 12, 12567, 24u, 135l, 4567ul Восьмеричная система: Если число начинается с цифры 0, оно интерпретируется как восьмиричное число 16 битов 0 077777 0100000 0177777u 32 бита 0200000 01777777777l 020000000000 037777777777ul Пример: 0674 Шестнадцатеричная система: Если число начинается с символа 0х, то оно интерпретируется как шестнадцатиричное 16 битов 0x0000 0x7FFF 0x8000 0xEFFFu 32 бита 0x10000 0x7FFFFFFFl 0x80000000 0xFFFFFFFFul Пример: 0х3D4 b) Вещественные константы. Это числа с плавающей точкой. Значение имеет дробную часть. По умолчанию все вещественные константы имеют тип двойной точности double. Занимают в памяти 8 байт. Диапазон значений 1 e 308. Принудительно можно задать формат одинарной точности. Число будет занимать 4 байта. (5.75 f) А также расширенной точности – 10 байт. (3.14L) Знак + можно не писать. Разрешается опускать либо десятичную точку, либо экспоненциальную часть, но не одновременно (.2; 4е16). Можно не писать дробную либо целую часть, но не одновременно (100.; .8е-5) c) Символьные константы. Это набор символов, используемых в ЭВМ. Делятся на 2 группы: печатные и непечатные (управляющие коды). Символьная константа включает в себя только 1 символ, который необходимо заключить в апострофы и занимает 1 байт памяти. Любой символ имеет своё двойное представление в таблице ASCII Символ 'А' 'a' ' ' '\n' Его код 65 97 32 10 Как целый тип данных 'A'=01018, 010000012, 4116, 6510. Управляющие коды начинаются с символа \ и тоже заключаются в апострофы. Наиболее распространенные управляющие коды: \n – переход на новую строку \t – табуляция (сдвиг курсора на некоторое фиксированное значение) \b – шаг назад (сдвиг на одну позицию назад) \r – возврат каретки (возврат к началу строки) \f – подача бланка (протяжка бумаги на 1 страницу) \\ - слеш \’ - апостроф \” - кавычки Последние три знака могут выступать символьными константами, а также применяться в функции вывода на экран printf() , поэтому применение их в качестве символов может привести 5 к ошибке. Например, если мы хотим вывести строку «Символ \ называется слеш», то оператор должен выглядеть так: рrintf( «Символ \\ называется слеш» d) Строковые константы - содержат последовательность из 1 и более символов, заключённых в " ". Расходуется по 1 байту на любой символ + 1байт на так называемый нольсимвол - признак конца строки. Ноль-символ – не цифра ноль, он означает, что количество символов в строке (N) должно быть на 1 байт больше (N+1), чтобы обозначать конец строки. 5.2. Базовые стандартные типы переменных Если величина является константой, компилятор сам может распознать ее тип по тому виду, в котором она введена. В случае с переменными, обязательно должен быть объявлен ее ТИП. Программа на СИ не будет выполняться, если не описать все используемые переменные. Тип является характеристикой данных. Под типом понимается совокупность информации о данном: сколько ему нужно выделить памяти, какой вид имеет его представление, какие над ним определены операции. Для выполнения вычислений в программе задаются переменные различных типов. Переменная – это именованный объект, который может принимать различные значения в процессе выполнения программы. В Си определены следующие стандартные типы данных: а) для целых чисел Тип объем диапазон чисел int 2 байта 32768 signed int 32768 знаковое unsigned int 0…65535 беззнаковое chort int короткое целое – тождественно int long int 4 байта 2147483648 длинное целое signed long int 2147483648 знаковое unsigned long int 0…4294967295 беззнаковое б) с плавающей точкой float 4 байта double 8 байт long double 10 байт 1038 10308 104932 двойной точности расширенной точности Существует еще один тип данных - char. Он в основном используется для символов, но может также использоваться для целых чисел char 1 байт 128 signed char 128 unsigned char 0 ... 255 При описании данных, необходимо ввести тип, за которым должно идти имя переменной (описание). Можно в один оператор объединять несколько имен переменных одного типа, разделенных запятой. Операторы должны заканчиваться точкой с запятой. Пр.: int num; int cows, hogs; Переменным можно присваивать некоторое значение перед началом обработки (инициализировать). В качестве инициализации переменной часто применяются константы. Пр.: int num; num=1024; Можно инициализировать переменную в операторе описания: int var = 72; 6 int num=1024, sum=45; Если присваивается символьное значение переменной типа char , то необходимо не забывать брать символ в апострофы: char isma=’S’; т.к. если записать char isma=S, компилятор будет считать, что используется переменная с именем S, которая не описана. В СИ имеется встроенная операция sizeof, которая позволяет определить размер объектов в байтах. Пример: main() { printf(“данные типа int занимают %d байта.\n”, sizeof(int)); printf(“данные типа char занимают %d байта.\n”, sizeof(char)); printf(“данные типа long занимают %d байта.\n”, sizeof(long)); } В результате будет выведена информация: данные типа int занимают 2 байта данные типа char занимают 1 байта данные типа long занимают 4 байта Символ %d указывает куда нужно вставить значение переменной. % - означает, что необходимо напечатать число, а d – что число необходимо печатать в десятичном формате. 6. ОПЕРАЦИИ ЯЗЫКА СИ Операции в языке Си применяются для представления арифметических выражений. Насчитывается около 40 операций и 16 приоритетов. Величина, над которой выполняется операция, называется операндом. Операции могут быть унарные (один операнд), бинарные (два операнда) и тернарные. 6.1. Арифметические операции Можно выполнять действия над переменными, переменными и константами, константами. Самый высокий приоритет у скобок () 1) Изменение знака - r = -12; -r (2) 2) Умножения * сm = 2,54 *in; (3) 3) Деления / var = 12.0 / 3.0; (3) У целых чисел при делении дробная часть отбрасывается (происходит усечение) x = 5 / 3 (х примет значение 1) 4) % - деление по модулю (3) ( используется только для целых чисел) х = 5%3 ( в результате получается остаток от деления х примет значение 2) 5) Сложение + sum = 20+10; sum = 20 + x ; sum = hou + sec; (4) 6) Вычитание – (4) 7) Операции автоуменьшения -- и автоувеличения ++ на 1 (2) Различают 2 формы записи операции ++i - префиксная форма (увеличение переменной i происходит до следующей операции) и i++ - постфиксная форма (после) Примеры: ++size < 18,5 сначала произойдет увеличение переменной, а затем сравнение с числом 18,5 size++ <18,5 сначала сравнение, а затем увеличение на 1. Пусть переменным присвоены следующие значения: y=2; n=3; 7 В результате операции y=n++ сначала переменной y присвоится значение переменной n, а затем n увеличится на 1. y станет равным 3, а n - 4 next=(y+n++)*6; в этом случае сначала произойдет сложение, а затем n увеличится на 1 ( на результате это не отразится) (2+3)*6=30; n=4; next=(y+(++n))*6; в этом случае сначала n увеличится на 1. А затем уже числа будут складываться (2+4)*6=36; n=4; 8) Операция определения размера sizeof (2) Можно определять размерность (количество байт) типа sizeof (int); а также размер переменных sizeof x; 9) Операция присваивания = (15) Знак = не означает в этом случае равенство. Это присваивание некоторого значения. val = 3,75; PI = 3,14; i = i+1; математически это неверно, но поскольку = это не знак равенства, то эта запись в СИ верна. Она означает – к значению переменной i прибавить 1 и новое значение присвоить переменной с именем i. Можно одно значение присваивать нескольким переменным (присваивается справа налево) y = x = z = 1; Нельзя присваивать значения константам. Поэтому при присваивании слева от знака = не может быть константы: 3,75 = val не верно 10) Операция следования , (запятая) х=5, у=24; (16) Выражения, разделенные запятой, будут выполняться слева направо. Символ запятая может также использоваться как разделитель (в операторах). 6.2. Операции отношения Меньше <, (7) Больше или равно > =, (7) Меньше или равно <=, (7) Не равно != (8) Больше > (7) Равно = = . (8) Используются для сравнения в условных выражениях. Вырабатывают значения «истина» и «ложь». Не следует путать знаки = и = =. С помощью операции присваивания (=) некоторой переменной слева от этого знака присваивается значение. А с помощью операции отношений (= =) проверяется равенство выражений, стоящих слева и справа от этого знака. Все операции отношений возвращают результат «истина» или «ложь» (0 или 1). Значение переменных при этом не изменяется. При сравнении float ( вещественных величин) лучше пользоваться только операциями < и >, т.к. ошибки округления могут привести к тому, что числа окажутся неравными, хотя по логике они должны быть равны. (например 3*1/3 равно 1,0, но 1/3 в вещественном формате будет представлена как 0,999999…, и произведение не будет равно 1) Лучше всего сравнивать модуль их разности с некоторым малым числом. Это связано с погрешностью представления вещественных значений в памяти: float a, b; if ( a==b ) printf(“равны”); else printf(“не равны”); //рекомендуеся if ( fabs(a-b)<1e-6) printf(“равны “); else printf(“не равны”); Приоритет больше чем у операции присваивания, но меньше чем у +, -. 8 y > x+2; - сначала сложение, затем сравнение. 6.3. Логические операции Используются для проверки условия, вырабатывая значение истина или ложь. 1) && "И" (операция логического умножения.) Выражение истинно только в том случае, если истинны выражения, стоящие до и после &&. Если первое – ложь, то дальше не проверяется. (12) 2) || "ИЛИ" (логическое сложение.) Выражение истинно, если одно из выражений истинно. Если первое – истина, дальше не проверяется (13) 3) ! "НЕ" (отрицание) (2) Булева логика: x y && || ! 0 0 0 0 0 0 1 0 1 1 1 0 0 1 0 1 1 1 1 1 Пр.: if(x>0 && x<10) действие 5>2 && 4>7 - ложь 5>2 || 4>7 - истина !(4>7) – истина У операции ! (НЕ) очень высокий приоритет (выше только у скобок). && и || выше присваивания, но ниже чем отношения. a>b && b>c || b>d ((a>b) && (b>c) || (b>d)) 6.4. Операции с разрядами Поразрядные логические операции. Приводят к изменению значения переменной. Действия производятся над данными класса целых и char. Они называются поразрядными, потому что они выполняются отдельно над каждым разрядом независимо от разряда, находящегося слева или справа. 1) ~ Дополнение до 1 или поразрядное отрицание. Это унарная операция изменяет каждую 1 на 0, а 0 на 1. ~(11010) получим (00101) 2) & Поразрядное И служит для сбрасывания битов. Эта бинарная операция сравнивает последовательно разряд за разрядом два операнда. Результат равен 1, если оба соответствующих разряда операндов равны 1 (10010011) & (00111101) => (00010001) 3) | Поразрядное ИЛИ служит для установки битов. Эта бинарная операция сравнивает последовательно разряд за разрядом два операнда. Результат равен 1, если один (или оба) из соответствующих разряда операндов равен 1. (10010011) | (00111101) => (10111111) 4) ^ Исключающее ИЛИ. Результат равен 1, если один из разрядов равен 1 (но не оба) (10010011) ^ (00111101) => (10101110) 9 6.5. Операции сдвига Операции сдвига осуществляют поразрядный сдвиг операнда. Величина сдвига определяется значением правого операнда. Сдвигаемые разряды теряются. При сдвиге вправо знаковый разряд размножается. 1) << сдвиг влево Разряды левого операнда сдвигаются влево на число позиций, указанное правым операндом. Освобождающиеся позиции заполняются нулями, а разряды, сдвигаемые за левый предел левого операнда, теряются. (10001010)<<2 = = 00101000 2) >> сдвиг вправо Разряды левого операнда сдвигаются вправо на число позиций, указанное правым операндом. Разряды, сдвигаемые за правый предел левого операнда, теряются. Для беззнаковых чисел освобожденные слева разряды заполняются нулями. Для чисел со знаком левый разряд принимает значение знака. (10001010)>>2 = = 00100010 Эти операции выполняют эффективное умножение и деление на степени 2: number<<n – умножает number на 2 в n-й степени number>>n – делит number на 2 в n-й степени 6.6. Операция условия ?: Операция состоит из двух частей (? и :) и содержит три операнда (операнд1 ? операнд2 : операнд 3). Это более короткий способ записи оператора if else и называется «условным выражением». Например: условное выражение x = (y<0)? –y : y; означает, что если у меньше 0, то х = -у, в противном случае х = у. В терминах оператора if else это выглядело бы так: if(y<0) x = -y; else x = y; Условные выражения более компактны и приводят к получению более компактного машинного кода. Т.о. если условие операнда 1 истинно, то значением условного выражения является величина операнда 2, если условие операнда 1 ложно – величина операнда 2. Условное выражение удобно использовать, когда имеется некоторая переменная, которой можно присвоить одно из двух возможных значений. Пр.: max = (а>b) ? a : b; 6.7. Преобразование типов В операторах и выражениях должны быть данные одного и того же типа. Но на Си возможно это нарушить (в отличии от Паскаля). Си компилятор автоматически преобразует типы, но следует соблюдать определенные правила: 1.Если производится операция над данными 2-х разных типов, то обе величины приводятся к высшему типу (происходит "повышение" типа). 2. Типы от высшего к низшему: double, float, long, int, short, char. Применение слова unsigned повышает ранг соответствующего типа со знаком. 3. В операторе присваивания конечный результат вычисления выражения в правой части приводится к типу переменной, которой должно быть присвоено это значение ( при этом может быть как повышение так и понижение типа). "Понижение" типа приводит к отбрасыванию разрядов. 10 4. При вычислениях величин типа float они автоматически преобразуются в тип double (для сохранения точности вычислений, это уменьшает ошибку округления). Конечный результат преобразуется обратно в float, если это диктуется оператором описания. 6.8. Операции приведения Хотя в СИ и возможно преобразование типов, лучше избегать этого и указывать точно тип данных. Это называется привидением типов. int num; num = 1,6+1,7; 3,3 => 3. Сначала числа складываются, затем результат приводится к указанному типу. num = (int)1,6+(int)1,7; 1+1 = 2. В это случае, числа сначала приведены к данному типу, а затем складываются. 6.9. Дополнительные операции присваивания + = (величина, стоящая справа, прибавляется к величине, расположенной слева) n + = 13 тоже, что и n=n+13 - = (от величины, стоящей слева, отнимается величина, расположенная справа) den - = 20 den = den-20 * = (величина слева умножается на величину справа) n*=2 n = n*2 / = делит переменную л.ч. на величину п.ч. % = дает остаток от деления переменной л.ч. на величину п.ч. <<=; >>=; | =; & =; ^ =; ~=; 7. ОПЕРАТОРЫ ЯЗЫКА СИ Основу программы на Си составляют выражения, а не операторы. Большинство операторов в программе являются выражениями с ‘;’. Это позволяет создавать эффективные программы. Оператор является законченной конструкцией языка Си. Операторы служат основными конструкциями при построении программы. Выражение состоит из операций и операндов (операнд – то, над чем выполняется операция, простейшее выражение может состоять из одного операнда). Оператор служит командой компьютеру. Операторы бывают простые и составные. Простые операторы оканчиваются ‘;’ . Простые операторы: 1. Пустой оператор ‘;’ 2. Оператор описания int x, y; 3. Оператор присвоения count = 0.0; 4. Оператор выражение (управляющий оператор) sum = sum+count; var = (var + 10)/4; 5. Оператор вызова функции printf("Привет \n"); 6. Оператор следоаания ‘,’ x=7, y=10; 7. Составные операторы или блоки Это группа операторов, заключенных в фигурные скобки {...}. Оператор ветвления if ... else. 11 Оператор может быть простым и составным. Позволяет пропустить оператор или блок операторов, если условие ложно. if(условное выражение) или if(условное выражение) { оператор; операторы; } 1.Схема выполнения оператора Тело программы If Проверка Условия if(условное выражение) оператор1; else оператор2; Истина Операторы if(условное выражение) { операторы; } else { операторы; } 2. Схема выполнения оператора If или Проверка Условия Истина else Операторы тело программы Операторы С помощью оператора ветвления можно организовать множественный выбор, если составить сложную конструкцию. if(условное выражение1) оператор1; else if(условное выражение2) оператор2; else if(условное выражение3) оператор3; else оператор4; Слово else всегда относится к ближайшему if, иначе нужно ставить скобки. if(условное выражение1) { if(условное выражение2) оператор1; } else оператор2; // Пример задачи на использование оператора #include <stdio.h> #define LIMIT 12600 #define MAX 25200 #define NORMA 60 #define PEOPLE 20 void main(void) 12 { float kwh; //количество киловат float bill; // плата int p,house; printf("Укажите количество израсходованных кВт/ч.\n); scanf("%f",&kwh); printf("Укажите количество человек в семье.\n); scanf("%d",&p); house=NORMA+PEOPLE*p; if(kwh<=house) bill=kwh*LIMIT; else bill= house*LIMIT + (kwh-house)*MAX; printf("Плата за %f составляет %f.\n",kwh,bill); } 8. Оператор множественного выбора. Оператор if...else осуществляет выбор между двумя (условиями) вариантами. Но иногда нужно сделать выбор из нескольких вариантов. Это можно сделать с помощью if-else if-else. Иногда удобнее применить оператор switch. switch (целое выражение) { case конст1: оператор; case конст2: оператор; default: оператор; } В начале вычисляется выражение в скобках за ключевым словом. Затем просматривается список меток case 'a': пока не будет найдена, соответствующая этому значению. Если нет такой метки, то будет выполнен default. Метка default может отсутствовать, тогда оператор switch ничего не выполнит и программа перейдет на следующий оператор. Оператор break служит для выхода из оператора switch и перехода к следующему оператору. Метки в операторе switch должны быть константами целого типа, включая chаr. Могут следовать несколько меток подряд. //пример программы на множественный выбор #include <stdio.h> void main(void) { int x, y; char c; printf("Введите 2 целых числа и знак операции"); scanf(“ %d %d %c”, &x, &y, &c); switch (c) { case '+': printf("x + y = %3d\n”, x+y); break; case '-': printf("x - y = %3d\n”, x-y); break; case '*': printf("x * y = %3d\n”, x*y); break; case '/': printf("x / y = %3d\n”, x/y); 13 break; default: printf("Такой операции нет!\n); } } 9. Операторы цикла while. Цикл – это многократно повторяющиеся действия или группа действий. Операторы цикла могут быть трех видов: с предусловием, с постусловием, со счетчиком. а) с предусловием: Оператор while состоит из трех частей: это ключевое слово while, затем в круглых скобках указывается проверяемое условие, и тело цикла - оператор, который выполняется, если условие истинно (таких операторов может быть несколько, тогда они заключаются в {} и получается составной оператор). Если оператор один, то действие оператора while распространяется от ключевого слова while до ‘;’. while (условное выражение) или while (условное выражение){ оператор; операторы; } Схема выполнения цикла while Истина Проверка Проверка условия условия Операторы Операторы Ложь (переход к следующей операции) Тело цикла может состоять из нескольких операторов. В этом случае оно заключается в фигурные скобки. Пример 1: Пример 2: index = 0; index = 0; while (index++ < 10) while (index++ < 10) { sum = 10*index + 2; sum = 10*index + 2; printf ("sum = % d \n", sum); printf ("sum = % d \n", sum); } В примере 1 в цикл включен только один оператор присваивания. Печать данных будет производиться только 1 раз – после завершения цикла (будет выведено – sum=102). В примере 2 в цикл включено два оператора, поэтому печать результатов будет выводиться на каждом шаге работы цикла. В операторе цикла с предусловием, решение о прохождении цикла принимается до прохождения цикла. Поэтому возможно, что цикл никогда не будет пройден. Последовательность действий, состоящая из проверки выражения и выполнения оператора, периодически повторяется до тех пор, пока выражение не станет ложным. Каждый такой шаг называется «итерацией». //Пример программы с циклом while #include <stdio.h> void main(void) { int guess=1; char res; 14 printf("Задумайте число от 1 до 100. Я попробую его угадать"); printf("\n Отвечайте д, если моя догадка верна и”); printf( «\n н, если я ошибаюсь.\n»); printf("Итак ваше число %d?\n", guess); while((res=getchar()) != 'д') if(res!='\n') printf("Тогда оно равно %d?\n", ++guess); printf("Ура, я угадала!!!"); } б) Оператор цикла с постусловием (истинность условия проверяется после выполнения каждой итерации цикла. Решение, выполнять или нет очередной раз тело цикла, принимается после выполнения всех итераций). Этот цикл всегда выполняется хотя бы 1 раз. do или do { оператор; операторы; while (условное выражение); } while (условное выражение); Проверка истинности осуществляется после выполнения оператора или блока операторов. Хотя бы один раз этот цикл будет выполнен. Схема выполнения цикла Do Операторы while Проверка условия Истина Ложь Рассмотрим два примера: Пример 1 Пример 2 do { while((ch = getchar()) ! = '\n') ch = getchar(); putchar (ch); putchar (ch); } while(ch ! = '\n') Функция getchar получает один символ, поступающий с клавиатуры и передает его программе, аргументов она не имеет. Функция putchar получает один символ, поступающий из программы, и пересылает его для вывода на экран, эта функция имеет один аргумент, т.е. в скобках необходимо указывать символ, который будет выведен на печать. Можно использовать эту функцию в следующем виде: putchar(getchar()) – эта запись компактна и не требует введения вспомогательных переменных.) Во втором примере на экран будут выводиться все символы до появления символа «новая строка». А в первом примере будут выводиться все символы, включительно и символ «новая строка» (только после печати этого символа, если он введен, в цикле производится проверка символа «новая строка» и действие цикла завершается). Цикл выполняется хотя бы один раз, так как проверка осуществляется только после его завершения. в) Оператор цикла со счетчиком for (выр1; выр2; выр3) или for (выр1; выр2; выр3) { оператор; операторы; } Выр1 – инициализация, проводится только 1 раз в начале цикла. Выр2 - проверка условия окончания цикла (производится перед каждым возможным выполнением тела цикла. Когда выражение становится ложным, цикл завершается). 15 Выр3 – наращивание счетчика цикла (выражение вычисляется в конце каждого выполнения тела цикла). Схема выполнения цикла Увеличение счетчика Истина For Инициализация счетчика Проверка условия Выполняемые операторы Ложь Если какое-либо из выражений отсутствует, то ‘;’ все равно ставится. Могут отсутствовать все выражения, и тогда цикл бесконечен. Цикл for очень многообразен. Примеры: 1). for (n = 1; n < 1000; n++) ; счет в порядке возрастания 2). for ( i = 10; i > 0; i--) ; счет в порядке убывания 3). for (n = 2; n < 60; n = n + 12) ; значение переменной n будет увеличиваться на 12 при каждом выполнении тела цикла (после этой команды можно ввести printf(“%d\n”,n) и тогда на экран будут выводиться числа 2,14,26,38,50,62) 4). Можно вести подсчет с помощью символов: for (ch = 'a'; ch <= 'z'; ch++) ; printf(«Величина кода ASCII для %c равна %d.\n», ch, ch); на печать будут выведены все буквы от a до z вместе с их кодами ASCII. 5). Наращивание может происходить и после выполнения действий в выражениях (значение переменных будет меняться при каждой итерации): for (x = 1; y <= 75; y = 5*(x++)+10) ; for (i = 1, cost = 20; i <= 16; i++, cost+= 17) ; 6). Можно опустить одно или более выражений (но нельзя опустить символы ;). Необходимо только включить в тело цикла несколько операторов, которые приведут к завершению его работы. ans = 2; for (n = 3; ans <=25;) ans=*n; Значение переменной asn сначала будет равно2, затем на первой итерации цикла примет значение 6, затем 18 и 54. 7). Бесконечный цикл for (;;) ; пустое условие всегда считается истинным. 8). Первое выражение не обязательно должно инициализировать переменную. Там может стоять оператор некоторого специального вида, например printf. Необходимо только помнить, что первое выражение выполняется только один раз, до начала выполнения остальных частей цикла. for (printf ("Запомните введение числа!\n"); num = =6;) scanf (" % d, &num); г) Вложенные циклы 16 Если внутри одного цикла находится другой цикл, то эта конструкция называется вложенный цикл. Внутренний цикл выполняется столько раз, сколько задано во внешнем цикле. // Пример программы на вложенные циклы // Вывести на экран числа от 0 до 99 по 10 в каждой строке #include <stdio.h> void main(void) { int num=0; int i, j; for(i=0; i<10; i++) { for(j=0; j<10; j++) printf("%4d",num++); printf("\n"); } } 8. Операторы безусловных переходов а) Оператор goto метка. Осуществляет переход на любую точку программы вверх и вниз. Имя метки задается согласно правилам создания идентификатора. goto m1; ... m1: оператор; б) Оператор break. Служит для досрочного выхода из цикла. Применяется в любых циклах. В случае вложенных циклов осуществляет выход только из внутреннего цикла. while (условное выражение1) { ... if(условное выражение2) break; ... } в) Оператор continue. Служит для пропуска группы операторов и выхода на начало цикла. for(i=0; i<10; i++) { ... if(условное выражение2) continue; ... } while((ch = getchar())!=EOF){ if (ch=='/n') break; (continue;) putchar (ch); } 8. СТАНДАРТНЫЕ ФУНКЦИИ ВВОДА И ВЫВОДА Функции ввода вывода служат для ввода данных в программу с клавиатуры и вывода их на экран. 8.1. printf (). Служит для вывода данных на экран printf ("Управляющая строка", список параметров); 17 Параметрами могут быть переменные, константы, выражения. Управляющая строка - строка символов, показывающая, как должны быть напечатаны параметры. Обязательно берётся в кавычки. Состоит из литерных констант и спецификаций преобразования: %d – целое %f – плавающий формат %c – символ %lf – двойной точности %s – строка %e – экспоненциальная форма %i – целое %o – восьмиричная с.с. %g – плавающий формат %x – шестнадцатеричная с.с. %u – беззнаковое целое Каждому аргументу должна соответствовать cвоя спецификация преобразования. %% чтобы печатать символ %. printf("Число pi равно % f/n," PI); 8.2. Модификаторы спецификаций преобразования Общий вид модификаторов спецификаций преобразования %[выравнивание][ширина][доп.признаки]символ преобразования %[-][ширина]d – выравнивание по влевой границе %[-][ширина][.точность]f %[-][+][ширина]d – целое со знаком %[-][#][ширина][l]u(o,x) – целое без знака, выводится 0 или 0х для восьмиричной или шестнадцатиричной системы счисления %[-][+][#][ширина][.точночть]f(e, g) - # вывод дес.точки даже при нулевой точности. По умолчанию точность 6 цифр. Цифры - ширина поля. Если число больше, то оно выходит за ширину поля. %4.2f - количество печатаемых цифр после запятой 2, поле шириной 4 позиции. /%d/ /3.36/ - "выбор по умолчанию" %2d /336/ %10d / 336/ %-10d /336 / %f /1234.560059/ 1234.56 %e /1.234560E+03/ %4.2f /1234.56/ %3.1f /1234.6/ %10.3f / 1234.560/ %10.3e / 1.234E+0.3/ /%s/ /'Выдающееся исполнение'/ 22 c. /%25.s / Выдающееся исполнение'/ % 25.5s / Выдаю/ %-25.5 s /Выдаю / %d 336 %o 520 %x 150 %d -336 %u 65200 от 32768-65536 - отрицательные числа 65535- -1 65536 - 336 = 65200 printf (" %d %c ", 'A', 'A'); //На экране 65 A 8.3. sсanf(). Функция ввода данных с клавиатуры. 18 scanf ("Управляющая строка", список адресов параметров); Используются те же спецификаторы, что и printf(). В качестве параметров указываются адреса переменных. scanf("%d",&num); 8.4. Функции ввода/вывода одного символа getchar(), putchar(). Объявлены в заголовочном файле stdio.h. # include <stdio.h> void main() { char ch; ch = getchar(); тоже самое putchar(ch); putchar (getchar()); } 8.5. Функции небуфиризированного ввода с клавиатуры (прямой ввод на экран без нажатия клавиши ENTER). Объявлены в заголовочном файле <conio.h>. char ch=getch(); // ввод без эхоотображения. char ch=getche(); //с эхоотображением на экране. 8.6. Ввод/вывод в поток в С++ Система ввода/вывода – неотъемлемая часть среды программирования С++, и она занимает в языке особое место В заголовочном файле iostream.h содержаться следующие объявления одного потока ввода и трех потоков вывода: extern istream_withassign _Cdecl cin; //Объект потока ввода extern ostream_withassign _Cdecl cout; //Объект потока вывода extern ostream_withassign _Cdecl cerr; //Объект потока вывода ошибок extern ostream_withassign _Cdecl clog; //Объект буферизованного потока вывода ошибок Поток является промежуточным звеном между программой и устройством ввода и вывода. Поток может быть присоединён к различным устройствам: консоли, принтеру, фиктивному устройству, а так же к файлам на диске. cin >> v; // Прочитать из стандартного потока ввода cout << v; // Записать в стандартный поток вывода Обычно стандартный поток вывода cout присоединен к экрану терминала Простейшая программа, использующая операцию вывода, имеет следующий вид: #include <iostream.h> void main(void) { cout << “Hello, world!”; } Приоритет операций << и >> позволяет помещать в оператор вывода арифметические выражения, не прибегая к использованию скобок, зато при использовании логических выражений или операций присваивания скобки использовать необходимо: cout << “x+y =” << x+y <<”; x&y =” << (x&y) << “\n”; Для ввода данных в программу используется стандартный поток ввода Он имеет много общего с выводом данных Обычно стандартный поток ввода соединен с клавиатурой cin. int i; float y; cin >> i >> f ; 19 8.7. Форматирование вывода Для форматирования вывода можно установить несколько флагов, для этого используются функции-члены setf, unsetf. unsigned v =12345; cout << "Before: " << v << endl; cout.setf(cout.hex); //Модификация потока cout << "After: " << v << endl; Для форматирования можно подключить заголовочный файл Iomanip.h, тогда используем манипуляторы cout << "In hexadecimal v == " << hex << v << endl; cout << "In decimal v == " << dec << v << endl; ends вставить нулевой завершающий символ в строку endl начать новую строку oct 8-ричная система счисления Для выравнивания по правому краю целочисленных переменных можно задать: cout.width(8); но он не оказывает влияние на следующее выводимое значение. Заключительная программа Задача. Три бригады собирают в саду яблоки. Написать программу учета сбора яблок каждой бригадой. Определить, сколько яблок было собрано за день и средний заработок в каждой бригаде. # include <stdio.h> # include <conio.h> void main (void) { int worker1, worker2, worker3; //Кол-во человек в бригадах int weight1, weight2, weight3; //Вес яблок char ch; float cost; //Стоимость яблок int num, w; //Номер бригады и вес яблок clrscr(); weight1 = weight2 = weight3 = 0; printf("Введите количество студентов в каждой бригаде\n"); scanf ("%d %d %d",&worker1,&worker2,&worker3); printf("Введите стоимость 1 кг. яблок \n"); scantf("%f",&cost); for(;;) { printf("В. номер бригады и количество собранных яблок\n"); scanf("%d %d",&num,&w); switch (num) { case 1: weight1 += w; break; case 2: weight2 += w; break; case 3: weight3 += w; break; default: 20 printf("Вы неверно ввели номер бригады\n"); } printf("Для окончания нажмите клавишу q\n"); ch = getche(); if (ch == 'q') break; } printf("Собрано яблок \n"); printf("1 б-й = %d, 2 б-й = %d, 3 б-й = %d\n”, weight1,weight2,weight3); printf("Всего за день собрано %d яблок\n ",weight1+weight2+ weight3); printf("Заработок в 1-й бригаде=%f\n", (float)weight1*cost/worker1); printf("Заработок в 2-й бригаде=%f\n", (float)weight2*cost/worker2); printf("Заработок в 3-й бригаде=%f\n", (float)weight3*cost/worker3); } 9. МАССИВЫ 9.1. Одномерные массивы Массив - это набор переменных, расположенных последовательно в памяти, имеющих одно имя и отличающихся друг от друга числовым признаком. float mas [20]; - объявляет массив mas, состоящий из 20 членов или элементов. 1 элемент mas [0], последний mas [19]. mas [5] = 32.54; присваивание значения элементу массива float y = mas[5]; Массивы могут быть образованы из данных любого типа. Одномерные массивы называются вектором, двумерные – матрицей. Mas[0] Mas[1] Mas[2] 4б 4б 4б Обращение к элементу массива осуществляется с помощью индекса. Индекс изменяются от 0 до n-1, где n размерность массива. Для работы с массивами часто используются циклы. #include <stdio.h> void main (void) { int i, a[10]; for (i=0; i<10; i++) scanf (" %d",&a[i]); //ввод с клавиатуры printf ("Вывести следующие результаты \n"); for (i=0; i<10; i++) printf (" %5d", a[i]); // вывод на экран. } 9.1.1. Стандартные алгоритмы работы с одномерными массивами //Пример: Нахождения max (min) числа в массиве. #include <stdio.h> void main (void) { int i, num, a[10]; for (num=a[0], i=1; i<10; i++) if (num<a[i]) num=a[i]; printf("Max число в массиве %d\n",num); } 21 //Пример: Сортировка по возрастанию методом отбора #include <stdio.h> void main (void) { int i, j, num, k, flag, a[10]; for (i=0; i<9; ++i) { num=a[i]; k=i; flag=0; for (j=i+1; j<10; ++j){ if (a[j]<num) { // ищем min, его индекс. num=a[j]; k=j; flag=1; } } if(flag==1){ a[k]=a[i]; a[i]=num; } } } //Пример: Сортировка по возрастанию методом «Пузырька» #include <stdio.h> void main (void) { int i, j, a[10], num; for (i=1; i<10; i++) for (j=8; j>=i; --j) if (a[j-1]<a[j]) { // ищем min, его индекс. num=a[j-1]; a[j-1]=a[j]; a[j]=num; } } //Пример: поиск заданного значения #include <stdio.h> void main (void) { int i, k=0, num=10, a[10]; for (i=0; i<10; i++) if(a[i]==num) printf(«%d»,i); } 9.1.2. Инициализация одномерных массивов Для хранения данных часто используются массивы. Их можно инициализировать. int days[12]={31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; Если данных меньше, чем 12, то остальные будут инициализированы 0, если массив глобальный, иначе значения неопределенны. Можно не задавать количество элементов в массиве. Оно будет определяться автоматически. int days []={31, 28, 31, 30, 31, 30, 31, 31, 30, 31}; void main (void){ int i; for (i=0; i<sizeof days/(sizeof(unt)); i++) 22 printf("Месяц %d имеет %d дней\n",i+1, i++); } 9.2. Многомерные массивы Определяются в программе также как и одномерные с указанием размерности каждого индекса в квадратных скобках. float rain[5][12]; Количество выделяемой памяти рассчитывается как 4*5*12 байт. Каждый индекс изменяется от 0 до n-1. В памяти все элементы располагаются последовательно, но индексы меняются следующим образом: Mas[0][0] Mas[0][1] Mas[0] [2] Mas[1][0] Mas[1][1] Mas[1] [2] Mas[2][0] Mas[2][1] Mas[2] [2] //Пример программы ввода кол-ва осадков помесячно за 5 лет и расчета среднего за каждый год и за 5 лет void main(void){ int i,j; float mas[5][12], sumyear, sum=0; for (i=0; i<5; i++){ printf("Ввести кол-во осадков за %d год помесячно\n", i+1); sumyear=0; for (j=0; j<12; j++){ scanf ("%f",&mas[i][j]); sumyear+=mas[i][j]; } sum+=sumyear/12; printf(Ср. за %d год = %.2f\n”,sumyear/12); } printf(Ср. за 5 лет = %.2f\n”,sum/5); } 9.2.1. Инициализация многомерных массивов 1. Инициализируем как одномерный массив. float rain[2][3]={10.2, 8.1, 6.8, 9.2, 7.8, 4.4}; 2. Инициализируем каждую строку. float rain[2][3]={{10.2, 8.1, 6.8}, {9.2, 7.8, 4.4}}; 3. Можно инициализировать в строке не все элементы, а только первые указанные. int array[2][3]={{2, 8}, {9}}; 4. Можно не задавать количество строк. Оно будет определяться автоматически по заданным инициализаторам. int mas[][3]={{10, 8}, {9, 7, 4}}; 23 9.2.2. Стандартные алгоритмы работы с двумерными массивами К стандартным алгоритмам работы с двумерными массивами (матрицами) относятся: 1. Сложение (вычитание) двух матриц; 2. Умножение матрицы на скаляр; 3. Умножение матрицы на вектор; 4. Умножение матрицы на матрицу; 5. Транспонирование матрицы; 6. Вставка (удаление) строк и столбцов матрицы; 7. Вычисление обратной матрицы. 8. Перестановка строк и столбцов. Пример 1. Сложение двух матриц. void main(void){ int i,j; float mas1[3][4], mas2[3][4]; for (i=0; i<3; i++) for (j=0; j<4; j++){ mas1[i][j]+=mas2[i][j]; printf(mas1[%d][%d]=%.2f\n”, i, j, mas1[i][j]); } } Пример 2. Умножение матрицы на вектор void main(void){ int i,j; float mas1[3][4], vec[4], var, mas2[3]; for (i=0; i<3; i++) { var = 0; for (j=0; j<4; j++) var += mas1[i][j] * vec[j]; mas2[i] = var; printf(mas2[%d] = %.2f\n”, i, mas2[i]); } } Пример 3. Умножение матрицы на матрицу void main(void){ int i, j, k; float mas1[3][4], var, mas2[4][5], mas3[3][5]; for (i=0; i<3; i++) { for (j=0; j<5; j++) { var = 0; for (k=0; k<4; k++) var += mas1[i][k] * mas2[k][j]; mas3[i][j] = var; printf(mas3[%d][%d] = %.2f\n”, i, j, mas3[i][j]); } } } Пример 4. Вставить строку void main(void){ int i, j; 24 float mas1[4][4], var=1, vec[4]; for (i=0; i<4; i++) for (j=3; j<var; j--) mas1[j][i] = mas1[j-1][i]; for (i=0; i<4; i++) mas1[var][i] = vec[i]; //сдвиг строк } 10. ФУНКЦИИ Функция – это фрагмент программы со своим именем, к которому можно обратиться для выполнения необходимых действий. Функция содержит как данные, так и операции над этими данными. Если для объекта имя ссылается на область памяти, где он хранится, то и имя функции ссылается на начало кода этой функции. Функции в Си играют ту же роль что и функции, программы, процедуры в других языках. - они освобождают от повторного программирования, если конкретную задачу нужно решать в программе несколько раз. - повышают уровень модульности программы, облегчают её чтение, внесение изменений, коррекцию ошибок. - cозданные функции можно использовать и в других программах. Функции бывают библиотечные и пользовательские. Для использования библиотечных функций нужно указать заголовочный файл, в которых они объявлены (директива #include). 10.1. Cоздание и использование пользовательских функций Весь принцип программирования на Си основан на понятии функции. Выполнение программы начинается с команд, содержащихся в функции main(), затем из неё вызываются другие функции: printf, scanf(), getchar(), putchar(). Это библиотечные функции языка Си. Как же создать свои функции? void main (void){ float list[50]; read list (list); // ввод набора чисел sort list (list); // сортировка average (list); // поиск среднего bargaph (list); // печать графика } До определенного момента функция рассматривается как "чёрный ящик", пока не нужно писать программу, реализующую данную функцию. Нужно знать как определить функцию и как к ней обратиться. Различают 3 момента работы с функцией: 1. Объявление функции или прототип функции. 2. Вызов функции. 3. Определение функции. //Задача. Напечатать титульный бланк организации. #inсlude <stdio.h> void starbar (void); //объявление функции void main (void) { starbar (); // вызов функции printf ("ПГУ"); printf ("им.Т.Г.Шевченко"); printf ("25 октября, 200"); 25 starbar(); - вызов функции } void starbar (void) { //определение функции int count; for (count=1; count<=65; count++) putchar ('*'); putchar ('\n'); } При написании функции starbar() используются те же правила, что и при создании main(): имя, фигурные скобки. Объявление функции должно быть обязательно, если функция определена ниже ее вызова. Объявлять можно в начале модуля в заголовке программы, тогда функция доступна в любой другой функции, или в функции, где она вызывается, тогда область ее видимости ограничена этой функцией. Определять функцию можно в любом месте программы. Единственное ограничение – нельзя определить функцию внутри определения другой функции. 10.2. Параметры функции Вернёмся к рассмотренной задаче. Пусть функция starbar() печатает любой символ, указанный в вызывающей программе. Он передается в функцию как аргумент. #inсlude <stdio.h> void starbar (char); // объявление функции void main (void) { starbar ('_'); // вызов функции printf ("ПГУ"); printf ("им.Т.Г.Шевченко"); printf ("25 октября, 200"); starbar('#'); // вызов функции } void starbar (char x) { // определение функции int count; for (count=1; count<=65; count++) putchar (x); putchar ('\n'); } Определение функции начинается с заголовка void starbar (char x) { Переменная x - является формальным параметром. Это новая переменная и под нее должна быть выделена отдельная ячейка памяти. При вызове функции мы присваиваем формальному аргументу значение фактического аргумента. starbar (‘-‘); x = '-'; Фактический аргумент может быть константой, переменной или сложным выражением. Можно передавать в функцию несколько фактических аргументов, разделенных запятыми. printnum (int i, int j) { printf ("Первый аргумент %d, вторй аргумент %d\n", i, j); } 10.3. Возвращение значения функцией Создадим функцию определяющую максимальное из двух чисел. На входе в эту функцию будут передаваться два числа, а на выходе возвращаться одно, то которое больше. Для возврата используется ключевое слово return. 26 void main (void){ int a=5, b=10, c=15, d=-10; int e,f; e=max(a, b); f=max(c, d); } int max (int i, int j) { int y; y=i>j ? i:j; return(y); } Оператор return прекращает работу функции и передает управление следующему оператору в вызывающей функции, даже если он не является последним оператором в функции. Количество операторов return не ограничено. Возвращаемое значение присваивается переменной по выходу из функции. 10.4. Inline-функции Перед определением функции может быть использован спецификатор inline для того, чтобы компилятор помещал код функции непосредственно в место вызова функции. #include <stdio.h> #include <conio.h> #include <dos.h> inline float Circle(float r) { return 2*3.14*r; }; void main(void) { float y; y = Circle(2); printf(“%.2f\n”,y); } Это увеличивает код программы, но ведет к увеличению быстродействия работы программы. Inline-функции не должны содержать сложные программные конструкции. 10.5. Значение формальных параметров функции по умолчанию Формальный параметр может иметь значение по умолчанию. Все параметры, стоящие справа от него тоже должны иметь значения по умолчанию. Эти значения передаются в функцию, если при вызове данные параметры не указаны. #include <stdio.h> void noName1 (float x, int y, char z='b') { printf("x = %0.1f y = %d, z = %d \n", x,y, (int)z); } void noName2 (float x, int y=16, char z='a') { printf("x = %0.1f y = %d, z = %d \n", x,y, (int)z); } void noName3 (float x=1.3, int y=4, char z='c') { printf("x = %0.1f y = %d, z = %d \n", x,y, (int)z); } void main(void) { 27 noName1(1.0,2); noName2(100.0); noName3(); } 10.6. Перегрузка функций Имена функций могут быть перегружены в пределах одной области видимости. Компилятор отличает одну функцию от другой по сигнатуре. Сигнатура задается числом, порядком следования и типами ее параметров. #include <stdio.h> #include <string.h> int noName (int first) { return first*first; } int noName (unsigned first) { return first*first; } char noName (char first) { return first*first; } int noName (int first,char *second) { return first*strlen(second); } float noName (float r) { return r*r; } double noName (double r) { return r*r; } void main(void) { printf("%d\n", noName(4)); printf("%d\n", noName((unsigned)4)); printf("%c\n", noName('c')); printf("%d\n", noName(4,"cлово")); printf("%0.2f\n", noName((float)1.2)); printf("%0.2lf\n", noName((double)1.2)); } 11. КЛАССЫ ПАМЯТИ И ОБЛАСТЬ ДЕЙСТВИЯ До сих пор мы имели дело с локальными переменными, которые доступны только в одной функции. В данном случае переменные a, b в main() и myfunc() являются разными переменными. void myfunc(int, int); void main (void){ int a=5, b=10; printf ("%d %d\n", a, b); myfun (a, b); printf ("%d %d\n", a, b); } void myfun (int a, int b){ a++; b++; 28 printf ("%d %d\n", a, b); } Каждая существует только в своей функции и доступна только в ней. Физически это разные ячейки памяти. Такие переменные называются "локальными". Но иногда требуются переменные доступные из любой функции и даже модуля программы. Такие переменные называются "глобальными". Любая переменная обладает 2-мя основными характеристиками: временем жизни и областью действия. Они зависят от места и способа описания переменной. Если переменная описана вне любого блока(в частности функции), онпа нпазывается глобальной, размещается в сегменте данных и изначальнл обнуляется, если вы не предусмотрели ее инициализацию другим значением. Время жизни глобальной переменной-с начала выполнения программы и до ее окончаения, а область действия (область , в которой эту переменную можно использовать, обратившись к ней по имени) –весь файл, в котором она описана, начиная с точки описания. Переменная, описанная внутри блока ( в частности, внутри функции main) является локальной, память под нее выделяется в сегменте стека в момент выполнения оператора описания. Для локальных переменных автоматическое обнуление не пролизводиттся. Областью действия локальной переменной является блок, в которм она описана, начиная с точки описания. Время ее жизни также ограничено этим блоком. Сколько раз выплняется блок, столько раз “ рождается” и “умирает “ локальная переменная. Су- ществуют понятия: область видимости и область существования. Область видимости не может выходить за область существования, но область существования может превышать область видимости. Каждая переменная в Си принадлежит некоторому классу памяти. Всего существует 5 классов памяти. Для их описания используются ключевые слова: extern - внешний, auto - автоматический, static - статический register - регистровые. 11.1. Глобальные переменные 1. Класс Extern - внешние переменные. Хранятся в области данных программы. Определяются до main() и доступны в любой функции программы. Время жизни - программа. При определении инициализируются по умолчанию 0 на стадии компиляции. Область видимости вся программа. int count; void main (void){ count ++; ......... } fun (){ printf ("%d\n", count); } Переменная count является внешней и доступна в обеих функциях. Если в программе есть одноименная локальная переменная, она закрывает глобальную переменную. Для расширения видимости операция расширения доступа :: int count; void main (void){ int count; //автоматическая переменная ::count++; .......... printf(“%d”,::count); 29 } void fun (void){ count ++; printf ("%d\n",count); } Если переменная определяется в одном модуле, но к ней есть обращение в другом, то нужно обязательно это указать, объявив ее как внешнюю, иначе будет создана новая переменная с этим именем. I модуль. int cost; void main (void){ ............ } II модуль. void func (void){ extern int cost; ................ } 2. Статистические внешние переменные – static Естьт один вид локальных переменных, которые подобно глобальным, размещаются в сегменте данных, существуют на всем протяжении выполнения программы и инициализируются однократно. Это статические переменные. Область видимости ограничена одним модулем. Доступны всем функциям в одном модуле. Время жизни работа всей программы. Инициализируются по умолчанию 0 на стадии компиляции. Создаются в области данных программы. static int num; void main (void){ int count; for (count=1; count<5; count ++){ printf ("%d\n", count); fun (); } } void fun (void){ int i=1; num ++; printf ("%d %d\n", i, num); } 11.2. Локальные переменные 3. Внутренняя статическая переменная. Ключевое слово static. Объявляется и определяется внутри одной функции, блока. Инициализируют 0 на стадии компиляции. Создаётся в области данных. Время жизни - работа всей программы, но область видимости только эта функция. При многократном вызове сохраняет своё значение. Инициализирует только 1 раз. Они как локальные переменные, видны только в своем блоке. Пример 1. int a; // глобальная переменная int main( ){ static int b = 1; // локальная статическая переменная int c; // локальная переменная } Пример 2. 30 void main (void){ int count; for (count=1; count<5; count ++){ printf ("%d\n", count); fun (); } } void fun (void){ static int num; int i=1; printf ("%d %d\n", i, num++); } 4. Регистровая переменная. Ключевое слово register int i . Помещаются в регистры микропроцессора для увеличения скорости вычисления. Если нет свободного регистра, рассматриваются как автоматические. Область видимости блок, в котором были определены. По умолчанию не инициализируются. 5. Автоматические переменные Ключевое слово auto. Определяются внутри любого блока. Время жизни работа блока. Создаются в области стека. Область видимости блок, в котором были определены. По умолчанию не инициализируются. Пример 1. if (i==1){ int j=0; int k=1; i++; } printf(“%d %d”,j,k); //сообщение об ошибке Пример 2. int i; ...... { int i; Внешнее i не видимо ...... } ...... Здесь опять ее видно Программа Внешний Статический static Модуль Таблица 1. Классы памяти и инициализация Аргумен. АвтоматиРегистро- Внурен. СтаФункции ческие auto вые тические Register Static Функция Блок Блок Блок Программа Программа Функция Сегмент Данных Все Сегмент данных Все Сегмент Сегмент стека стека Не раз- Все в С++ решена в Си Класс Хранения Внешний Extern Область действия Время Жизни Область Хранения Инициалемость объектов Блок Блок Программа Регистры МП Все Сегмент данных Все 31 Момент На стадии На стадии инициали- Компил. компил. зации Инициали- Инициали- Инициализация по зируются 0 зируются 0 умолчан. Память под все эти переменные При входе При входе в При входе На стадии в блок блок в блок компил. Значение Значение не Значение Инициализиуказываопред. не опред. руется 0 ется выделяет компилятор. Кроме перечисленных, динами- ческие переменные, память под под которые резервируется во время выполнения программы с помощью операции new в динамической области памяти, или хипе(heap). Доступ к таким переменным осуществляется не по имени, а через указатели. 12. ПРЕПРОЦЕССОР ЯЗЫКА СИ Компиляция выполняет еще один шаг перед собственно компиляцией исходного файла, называемый предварительно обработкой. Препроцессор просматривает программу и заменяет символьные аббревиатуры в программе на соответствующие директивы. Он расширяет возможности языка 3 функциями. 1. Подстановкой имен; 2. Включением файлов; 3. Условной компиляцией. Это позволяет создавать мобильные, более удобочитаемые и более удобные для сопровождения программы. # - первый символ в левой позиции, в любом месте исходного файла сообщает препроцессору, что далее следует директива. Она имеет действие до конца файла. 12.1. Подстановка имен Директивы: #define – создать макроопределение #undef - удалить макроопределение Директива #define служит для: а) определения символических констант #define TWO 2 #define MSG “Приднестровский Университет” #define PX printf(“X равен %d\n”,x) #define FMT “X равен %d” void main (void) { int x=TWO; PX; Printf(FMT,x); Printf(“%s\n”,MSG); Printf(“TWO:MSG\n”); } Макроопределение не должно содержать пробелы. Процесс прохождения от макроопределения до заключительной строки называется макрорасширением. Препроцессор делает подстановки. б) Макроопределение с аргументом. Очень похоже на функцию (макро-функция). #define SQUARE(x) x*x #define PR(x) printf(“X равен %d\n”,x) 32 void main(void) { int x=4; int z; z=SQUARE(x); //16 PR(z); Z=SQUARE(2); //4 PR(z); PR(SQUARE(X)); PR(100/SQUARE(2));//100 PR(SQUARE(X+2)); //14 PR(SQUARE(++X)); //30 } Всюду, где в вашей программе появляется макроопределение SQUARE(x) оно заменяяется на х*х. В макроопределении ‘x’ замещается символом, использованным в макровызове программы. Макроопределение SQUARE(2) замещается на 2*2. Х действует как аргумент, однако аргумент макроопределения не работает так же, как аргумент функции. Первые 2 строки программы еще предсказуемы. Заметим, что даже внутри двойных кавычек в определении PR переменная замещается соответствующим аргументом. Все аргументы в зтом определении замещаются . 3 строка представляет интерес - она становится следующей строкой: printf (“SQUARE(x) равен %d\n”, SQUARE(x)); после 1-го этапа макрорасширения 2-е SQUARE(x) расширяется, превращаясь в х*х, а 1е остается без изменения, потому что теперь оно находится внутри двойных кавычек в операторе прграммы, и таким образом защищено от дальнейшего расширения. Окончательно строка содержит printf (“SQUARE(x) равен %d\n”, х*х)); и выводит на печать SQUARE(x) равно16 при работе программы. Если ваше макроопределение включает аргумент с двойными кавычками, то аргумент будет замещаться строкой из макровызова. Но после этого он в дальнейшем не расширяется, даже если строка является еще одним макроопределением. В нашем примере переменная х стала макроопределением SQUARE(x) и осталась им. Вспомним , что х имеет значение 4. Это позволяет предположить, что SQUARE(x+2) будет 6*6 или 36 .Но получится 14. Причиной является,что препроцессор не делает вычислений, он только замещает строку. Всюду, где наше определение указывает на х , препроцессор подставит строку х+2. Т.О. х*х становится х+2*х+2 Единственное умнажение здесь 2*х. Если х = 4 Макрофункция ведет к увеличению программы. Функция к увеличению времени работы программы. Макроопределение создает строчный код, не нужно заботится о типах переменных. 12.2. Включение файлов Директива #include позволяет включить в текст содержимое файла, имя которого является параметром директивы. 1. #include <stdio.h> - ищет в системном каталоге 2. #include “stdio.h” – ищет в текущем рабочем каталоге. 3. Можно создавать свои файлы с функциями и подключать. #include “my_file.cpp” 4. Можно подключать макроопределение. Макрорасширение макроса должно приводить к одной из первых двух форм директивы. #define MY_FILE “d:\\bc\work\\my_file.h” #include MY_FILE 33 12.3. Условная компиляция Позволяет компилировать не все части программы. Директивы условной компиляции исползуются в больших программах. #if константное_выражение [фрагмент текста] [#elif константное_выражение фрагмент текста] ... [#else фрагмент текста] #endif Результатом вычисления константного_выражения является целое число. Если оно не 0, то выполняется фрагмент текста программы от директивы #if до одной из директив #else, #elif или #endif. 1. #ifdef CI #include <stdio.h> #define MAX 80 #else #include <iostream.h> #define MAX 132 #endif 2. #ifndef – если макроопределение не определено #ifndef MY_FILE // файл будет компилироваться только один раз #define MY_FILE //когда макрос не определен #include “my_fyle” #endif 3. #if SYS == ”IBM” //похоже на оператор, за ним следует константное выражение //которое считается истинным, если оно не равно 0 #endif 4. Можно исключить блок программы #ifdef любое имя ***** #endif #if defined(__LARGE__)||defined(__HUGE__) typedef INT long #else typedef INT int #endif 13. УКАЗАТЕЛИ Память состоит из байтов, каждый из которых пронумерован, начиная с 0, 1, 2 ... Номер – это адрес. В Си есть переменные, которые могут содержать этот адрес – указатели и операция взятия адреса - &. int var=1; - определение и инициализация переменной. var – её имя. printf ("%d %d\n",var, &var); 1 12136 34 Машинный код команды можно выразить словами. "Выделить 2 байта памяти, присвоить им имя var. Поместить в ячейку с адресом 12136 число". Фактический адрес этой переменной 12136, а его символическое представление &var. Значением переменной типа указатель служит адрес некоторой величины. Дадим имя этой переменной ptr; тогда можно написать ptr=&var; В этом случае говорим "ptr указывает на var", где ptr-переменная, &var-константа. ptr=&num; - теперь указывает на num. 13.1. Операция косвенной адресации * Для доступа к переменной, адрес которой помещен в ptr, используется операция косвенной адресации. val=*ptr; //val==num *ptr = 10; //num==10 13.2. Описание указателей Мы уже знаем как описываются переменные, массивы. Как же описать указатель! Сложность в том, что переменные разных типов содержат разное число ячеек, но операции с указателями требуют знания отведенной им памяти. Поэтому, при определении указателя, мы описываем, на какой тип переменной она будет указывать, и что это указатель символ *. int* ptr; float* pmas; char* pc; 13.3. Использование указателей для связи функций В функцию можно передавать не только значения переменных, но и их адреса. В этом случае в вызываемой функции можно изменять значение локальных переменных, определенных в вызывающей функции. void swap(int*,int*); void main(void){ int x=5, y=10; printf ("Прервичные значения х=%d, y=%d\n",x, y); swap (&x, &y); printf ("Новые значения х=%d, y=%d\n", x, y); } void swap (int*v, int*z){ int u; u=*v; *v=*z; //x=y *z=u; } Мы передали в функцию адреса переменных, поэтому при выходе х, у имееют новые значения. При передаче значений этого не происходило. Вызов swap (x, y); swap (&x, &y); Определение функции swap (int v, int z); swap (int*v, int*z); Переменные х, y являются локальными в функции main, но мы, таким образом, можем на них воздействовать в другой функции. 13.4. Указатели на одномерные массивы Указатели позволяют эффективно работать с массивами. Имя массива представляет собой скрытую форму указателя. 35 Mas -> &mas[0]; - определяется адрес 1-го элемента массива. Оба выражения являются константными выражениями и не меняются на протяжении работы программы. Их можно присваивать переменной типа указатель. void main(void){ int dates [4], *pti, i; float bills [4], *ptf; pti=dates; ptf=bills; for (i=0; i<4; i++) printf ("указатель +%d: %10u и %10u\n", i, pti+i; ptf+i); } Указатель +0: 56014 56026 - начало адреса массивов. +1: 56016 56030 +2: 56018 56034 +3: 56020 56038 Прибавляя 1 к указателю, переходим к следующему элементу массива, а не к следующему байту, т.е. смещаемся на длину типа элемента массива. рt pt+1 pt+2 pt+3 dates[1] dates[2] dates[3] dates[0] dates+2 <=> &dates[2] *(dates+2) <=> dates[2] *dates+2 <=> *(dates)+2 //Суммирование элементов массива с использованием указателя. for(i=0;i<10; i++) sum+=*(ptm+i); или for(i=0;i<10; i++){ sum+=*ptm; ptm++; } или for(i=0;i<10; i++) sum+=*ptm++; 13.5. Указатели на многомерные массивы Указателю можно присвоить адрес на массив int mas[2][2], *pti; pti=mas[0]; //pti=&mas[0][0] pti+1<=>&mas[0][1]; //так как элементы массива расположены в pti+2<=>&mas[1][0]; //памяти последовательно и сначала //меняется второй индекс. Для двумерного массива: mas[0]<=> &mas[0][0]; mas[1] <=>&mas[1][0]; //Это важное свойство, потому что можно //работать с 2-м массивом как с одномерным. int a[3][5]; // целочисленная матрица из 3-х строк и 5 столбцов. 36 Массив хранится по строкам в непрерывной области памяти. В памяти сначала располагается одномерный массив а[0], представляющий собой нулевую строку массива, а затем- массив а[1], представляющий первую строку массива а, и т.д.. Для доступа к отдельному элементу массива применяется конструкция вида а[i][j], где i- номер строки, j-номер столбца:выражения целочисленного типа. Можно обратиться к элементу массива и другими способами: *(*(а+i)+j) или *(а[i]+j).Допустим , требуется обратиться к элементу, расположенному на пересечении 2-й строки и 3-го столбца – а[2][3]. Имя массива а представляет собой константный указатель на начало массива. Сначала требуется обратиться ко 2-й строке массива, т.е. одномерному массиву а[2]. Для этого надо прибавить к адресу начала массива смещение, равнон номеру строки, и выполнить разадресацию:*(a+2).Напомним, что при сложении указателя с константой учитывается длина адресуемого элемента, поэтому на самом деле прибавляется число 2 , умноженное на длину элемента, т.е. 2*(5*sizeof(int)), поскольку элементом является строка, состоящая из 5 элементов типа int. Далее требуется обратиться к 3-му элементу полученного массива. Для получения его адреса опять применяется сложение указателя с константой 3 (на самом деле прибавляется 3*sizeof(int) ), а затем применяется операция разименования для получения значения элемента :*(*(а+2)+3). 13.6. Операции над указателями 1. Присвоить ему значение адреса int*px, x=2; px=&x; 2. Можно присвоить константу - адрес ячейки с описанием состояния аппаратных средств - абсолютный адрес. 3. После присвоения адреса можно применять операцию взятия косвенного адреса. px=&x; y=*px; //y==x; Приоритет выше, чем y операции «присвоение» *px=10; x=10; 4. *px+2 рх++; ++рх; //к значению переменной, адрес которой в px, прибавить 2. // рх+1 увеличение адреса на длину типа. 4. сравнение указателей ==, !=, >=, <=, >, < //Пример программы копирования двух массивов void main(void) { char ar1[100], ar2[100]; char *pa1, *pa2; pa1=&ar1; pa2=&ar2; while(pa2<(ar2+sizeof(ar2))) *pa1++=*pa2++; } Внутренние арифметические операции с указателями зависят от действующей модели памяти и наличия переопределяющих модификаторов указателя. Разность между двумя значениями указателей имеет смысл только в том случае, если оба они указывают на один массив. Арифметические операции с указателями ограничены сложением, вычитанием и сравнением. Арифметические операции с указателями объектов типа "указатель на тип type" автоматически учитывают размер этого типа, то есть число байт, необходимое для хранения в памяти объекта данного типа. 37 При выполнении арифметических операций с указателями предполагается, что указатель указывает на массив объектов. Таким образом, если указатель объявлен как указатель на type, то прибавление к нему целочисленного значения перемещает указатель на соответствующее количество объектов type. Если type имеет размер 10 байтов, то прибавление целого числа 5 к указателю этого типа перемещает указатель в памяти на 50 байт. Разность представляет собой число элементов массива, разделяющих два значения указателей. Например, если ptr1 указывает на третий элемент массива, а ptr2 на десятый, то результатом выполнения вычитания ptr2 - ptr1 будет 7. Когда с "указателем на тип type" выполняется операция сложения или вычитания целого числа, то результат также будет "указателем на тип type". Если type не является массивом, то операнд указателя будет рассматриваться как указатель на первый элемент "массива типа type" длиной sizeof(type). Конечно, такого элемента, как "указатель на следующий за последним элементом" не существут, однако указатель может принимать это значение. Если P указывает на последний элемент массива, то значение P+1 допустимо, но неопределено. Использование указателя на элемент вне массива ведет к непредсказуемым результатам работы программы. Контроль за допустимыми значениями указателей возлагается на программиста. 13.7. Передача массива в качестве параметра в функцию Указатель можно передавать в функцию в качестве параметра: void main(void){ int age[10]; ............ sum (age); .......... } void sum (int year[]){} Описатель int year[] - создает указатель на массив age. Можно по другому. void sum (int*pm){}. pm+3 <=> year[3] <=> age[3] одно и тоже pm[3]. int* pm и int pm[]; - одно и тоже Пример: Создадим функцию для определения среднего значения в массиве. float var (int*pm, int i){ //передаем адрес массива и количество //элементов. float sum=0; int k; for (k=0; k<i; k++) sum+=*pm++; //*(pm+k)или pm[k] returm (sum/i); //возвращаем среднее значение в } //вызывающую функцию. void main(void){ int mas[3][4]={ {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int i; for (i=0; i<3; i++) printf ("Среднее значение строки %d равно %4.2f.\n", i+1, 38 var(mas[i],4)); } Работаем с двумерным массивом как одномерным, передавая в функцию его адрес и количество элементов. Каждая строка – это одномерный массив. Если нужно передать массив в функцию как двумерный. float var(int mas[][4]),т.е. разбивает массив на строки по 4 столбца. 13.8. Указатель на void * Объявление void *vptr; объявляет, что vptr - это родовой указатель, которому может быть присвоено любое значение "указатель на тип type" без выдачи компилятором сообщений. Без правильного приведения типов между "указателем на тип type1" и "указателем на тип type2", где type1 и type2 это различные типы, присвоение может вызвать предупреждение или ошибку компилятора. Если type1 это указатель на void, приведения типов не требуется. int max(void *, void *); void main(void) { int x=5, y=10; int k=max(&x,&y); (k==0)?puts(«x>y»): puts(«x<y»); } int max(void *a,void *b) { return((*a>*b)?0:1); } 14. СИМВОЛЬНЫЕ СТРОКИ И ФУНКЦИИ НАД СТРОКАМИ Всякий раз, когда компилятор встречается с чем-то заключённым в кавычки, он определяет это как строковую константу. Символы+'\0' записыватся в последовательные ячейки памяти. Если необходимо включить кавычки в символьную строку, нужно поставить впереди \". Строковые константы размещаются в области данных. Вся фраза в кавычках является указателем на место в памяти, где записана строка. Это аналогично имени массива, служащего указателем на адрес расположения массива. Для вывода символьной константы служит идентификатор %s. char mas[] = "Это одна строка"; mas - адрес строки -> &m[0] *mas=='Э'. Можно использовать указатель для создания строки. char *str = "Таблица результатов"; char str[]= "Таблица результатов". Сама строка размещается в области данных, а указатель инициализируется адресом. void main(void){ char* mesg="Ошибка чтения"; char* copy; copy = mesg; создается второй указатель на строку. printf ("%S", copy); printf ("%S", mesg); } char* name; scanf ("%S", name); //Так нельзя !!! Указателю не присвоен адрес. char name [81]; //Нужно в начале определить массив 14.1. Массивы символьных строк 1. Строки в символьный массив можно вводить с клавиатуры. 39 char mas[80]; scanf("%S",mas); 2. Если требуется несколько строк, то организуем цикл ввода char mas[4][81]; for (i=0; i<4; i++) sсanf("%S", mas[i]); // &mas[i][0] 3. Можно сразу инициализировать в программе. char m1[] = "Только одна строка"; // автоматически определяется // длина строки + 1 байт на '\0'. 4. Размер массива можно задать явно. char m2[50] = "Только одна строка"; //18+1 5. char m3[]={'c', 'm', 'p', 'o', 'k', 'a', '\o'}; 6. Инициализация массива строк: char mastr[3][16]={"Первая строка", "Вторая строка", "Третья строка" }; *mastr[0]=='П'; *mastr[1]=='B'; *mastr[2]=='Т'; for (int i=0;i<3;i++) printf(“%s”,mastr[i]); //распечатать эти 3 строки printf(“%u”,mastr[i]); // распечатать адреса букв printf(“%c”,*mastr[i]); // распечатать первые символы строки printf(“%s”,*(mastr+i)); // распечатать все строки 7. «Рваный массив» – это массив указателей на строки. static char *masstr[3]= {"Первая строка", "Вторая строка", "Третья строка" }; В случае «рваного массива» длина строк разная и зря не расходуется память. 14.2. Массивы указателей Можно определять массивы указателей int* parray[5]; //5 указателей на целые значения. *parray[3] //3-й элемент массива. char *keywords[5]={"ADD", "CHANGE", "DELETE", "LIST", "QUIT"}; В памяти keyword[0] – адрес 10000 строка ADD\0 4б keyword[1] 10004 CHANGE\0 7б keyword[2] 10011 DELETE\0 7б for (i=0; i<5; i++) printf("%S", keywords[i]); char *key[3],**pt; //определение указателя на указатель pt=key; printf(«%s %d\n»,*pt,**pt); //распечатывается первая строка и код //первой буквы printf("%c", **(pt+i)); // напечатает первые буквы слов(ивлечение символа по адресу) 40 printf("%s", *(pt+i)); // распечатается вся строка for (i=0; i<5; i++) printf("%c", *(*pt+i)); // распечатать все слово по буквам *(*(pt+1)+i) // 2-е слово 14.3. 14.4. Указатель как возвращаемое значение функции Передача указателя как параметра функции Указатель можно передавать в функцию в качестве аргумента и возвращать из функции. #include<string.h> char* strcopy(char*, char*); void main(void){ char target [24]; char* cptr; cptr=strcpy (target, "Персональная"); //возврвщает адрес target+12 cptr=strcpy (cptr, "IBM"); // возврвщает адрес target+16 } char* strcpy (char*tptr, char*cptr){ while (*tptr++=*cptr++); //while (*cptr!='\0') // *tptr++=*cptr++; return (tptr); } 14.5. Функции, работающие со строками Функции, определеные в заголовочном файле stdio.h. 1. Функция gets(char *) - вводит строку в массив с клавиатуры char name[81]; gets(name); // Берёт все символы до конца строки символ '\n' // отбрасывает и записывает '\0' и передаёт в вызов программы. 2. Функция puts(char *) - выводит строку на экран puts ("Я функция puts()"); char str1[]="Только одна строка"; puts(str1); //Читает строку до встречи '\0'. Все строки //выводятся с новой строки. 3. Функция int getc(stdin) – вводит символ в переменную с клавиатуры. 4. Функция int putc(int c, stdout) – выводит символ на экран. 14.6. Стандартные библиотечные функции Функции, определеные в заголовочном файле string.h. 1. int strlen(char *) - опредляет длинну строки без'\0'; int k = strlen(str1); 2. char * strcat(char *, char *) - объединяет две строки в одну. strcat(str1, str2); //результат в первом массиве. void main (void){ 41 char str1[80]="Мой любимый цветок"; char str2[10]="ромашка"; if (strlen(str1)+strlen(str2)< 80-1) strcat(str1, str2); //Чтобы копировать - необходимо проверить, //чтобы длины массива было достаточно на //две строки + ноль-байт. } 3. char * strncat(char *, char *,int) - объединяет 1-ю строку и n-байтов второй строки в одну. 4. int strcmp(char *, char *) - сравнение строк. # define ANSWER "YES" void main(void){ char try[10]; gets(try); puts ("Вы студенты 203 группы?"); while (strcmp(try, ANSWER)!=0){ puts ("Попытайтесь ещё раз") gets (try); } puts("Верно"); } Функция возвращает 0, если строки одинаковы. Сравнение идёт до признака конца строки - '\0', а не до конца массива, или до первого несравнения: В-А возвращает 1, А-В возвращает -1. 5. int strncmp(char *, char *,int) - сравнение n байт у 2-х строк . 6. char * strcpy(char *, char *) - копирование строк #define WORD "Таблица результатов" void main (void){ char str1[30]; //Длина массива не проверяется strcpy(str1, WORD) puts(str1); } 7. char * strтcpy(char *, char *,int) - копирование n байт строки 8. char *strdup(char *) – выделяет память и копирует строку. 9. char *strupr(char *) – преобразует строчные буквы в прописные. 10. char *strlwr(char *) – преобразует прописные буквы в строчные. 11. char *strrev(char *) – реверсирует строку. 12. char *strchr(char *, int ) – устанавливает позицию первого вхождения символа 14. char *strrchr(char *, int c) – устанавливает позицию последнего вхождения символа с. 15. char *strstr(char *, char *) - устанавливает позицию первого вхождения подстроки str2 в строку str1. 16. int stricmp(char *, char *) – сравнивает не различая строчные и прописные буквы. 42 14.7. Преобразование символьных строк Функции, определеные в заголовочном файле stdlib.h. 1. int atoi() - строку в целое. double atof() - строку в число с плавающей точкой. void main(void){ char num[10]; int val; puts("Введите число"); gets(num); val=atoi(num); // обрабатывает до 1-го символа не являющегося } // цифрой 2. Существуют функции обратного преобразования числа в строку. itoa(int val, char *str, int radix) - целое в строку, где int val – число; char *str – строка; int radix – система счисления. ltoa(long val, char *str, int radix) - с плавающей точкой в строку. Функции, определеные в заголовочном файле ctype.h. Выполняют преобразования только с буквами английского алфавита. 1. Преобразование строчной буквы в прописную - int toupper(int c) 2. Проверка буква прописная или нет - int isupper(int c) 3. Преобразование прописной буквы в строчную - int tolower(int c) 4. Проверка буква строчная или нет - int islower(int c) #include <ctype.h> void main(void){ int ch; crit=0; //Признак прописные или строчные буквы while ((ch=getche())!='\n'){ if(crit==0){ ch=isupper(ch) ? tolower(ch): ch; putchar(ch); } else{ ch=islower(ch)? toupper(ch):ch; putchar (ch); } } 15. ССЫЛКИ Ссылка - это псевдоним для другой переменной. Они объявляются при помощи символа &. Ссылки должны быть проинициализированы при объявлении, причем только один раз. Тип "ссылка на type" определяется следующим образом: type& имя_перем. Ссылка при определении сразу же инициализируется. Инициализация ссылки производится следующим образом: int i = 0; int& iref = i; Физически iref представляет собой постоянный указатель на int - переменную типа int* const. Ее значение не может быть изменено после ее инициализации. Ссылка отличается от 43 указателя тем, что используется не как указатель, а как переменная, адресом которой она была инициализирована: iref++; //то же самое, что i++ int *ip = &iref; //то же самое, что ip = &i; Таким образом, iref становится синонимом переменной i. При передаче больших объектов в функции с помощью ссылки он не копируется в стек и, следовательно, повышается производительность. #include <iostream.h> void incr (int&); void main(void){ int i = 5; incr(i); cout<< "i= " << i << "\n"; } void incr (int& k){ k++; } Поскольку ссылка это псевдоним, то при передаче объекта в функцию по ссылке внутри нее объект можно изменять. Ссылки не могут ссылаться на другие ссылки или на поле битов. Не может быть массивов ссылок или указателей на ссылку. Ссылка может использоваться для возврата результата из функции. Возвратить результат по ссылке - значит возвратить не указатель на объект и не его значение, а сам этот объект. 16. ПАРАМЕТРЫ КОМАНДНОЙ СТРОКИ У функции main() могут быть свои формальные аргументы. В них возникает необходимость, если нужно передать какие-либо значения в программу из командной строки. int main(int argc, char *argv[]) {} argc – определяет кол-во передаваемых параметров в командной строке, включая имя самой программ. *argv[] – это массив указателей на строки. argv[0] – имя самой программы; argv[1] – первый параметр и т.д. Функция main() может возвращать значение, если необходимо. Любая программа должна возвращать в DOS код возврата. При нормальном завершении он равен 0. В командных файлах можно анализировать этот код командой IF ERRORLEVEL 0 echo «Ok!». Если нет необходимости возвращать значение, то оператор return не нужен. #include <iostream.h> int main(int argc, char* argv[]) { if(argc<2) { puts(“Нет аргументов в строке”); exit(1); } else { cout<<”Имя выполняемой программы”<<argv[0]<<endl; cout<<”Аргумент командной строки”<<argv[1]<<endl; } return 0; } Если программа завершилась с ошибкой, то можно передать код ошибки в DOS функцией exit(1). 44 17. ПРОИЗВОДНЫЕ ТИПЫ ДАННЫХ 17.1. Структуры Рассмотрим новый тип данных - структуру. Он не только гибок для представления разнообразных данных, но и позволяет создавать новые типы данных. Пример использования - создание каталога книг. Каждая книга имеет следующие атрибуты: шифр, название, автора, издательство, год издания, число страниц, тираж, цену. Это несколько массивов. Очень сложно организовать одновременную работу с каталогом, если нужно их упорядочить по названиям, авторам, цене и так далее. Лучше иметь один массив, в котором каждый элемент содержит всю информацию о книге. Структура – это объект, состоящий из последовательностей поименнованных элементов. Каждый элемент имеет свой тип. Для определения нового типа данных нужно его описать: struct book { char title [81]; char author[30]; float value; }; book – это имя нового типа данных. Сруктурный шаблон является основной схемой, описывающей, как образуется новый тип. struct - ключевое слово, имя типа структуры book - необязателен, если сразу определить имя переменной, то его можно не вводить. struct { char title [81]; char author[30]; float value; }libry; Каждый элемент структуры определяется своим собственным описанием. Это переменные и массивы стандартных типов данных. Шаблон является схемой без содержания. Он сообщает компилятору, как сделать что-то, но ничего не делает в программе, а вот создание структурной переменной, это и есть смысл слова «структура». Согласно шаблону под эту переменную выделяется память, равная сумме всех элементов (81). struct book играет ту же роль, что и int, float перед именем переменной. struct book doyle, panshin; Для доступа к элементам структурной переменной используется операция точка. Имя переменной, точка, имя элемента структуры. void main(void) { struct book libry; //описание перем-й типа book puts("Введите название книги"); gets(libry.title); puts("Введите фамилию автора"); gets(libry.author); puts("Введите цену книги"); scanf("%f",&libry.value); printf("%s, %s, %p.2f",libry.title,libry.author,libry.value); } Структурную переменную можно инициализировать: struct book libry={"Руслан и Людмила", "А.С.Пушкин", 1.50}; 45 17.1.1. Массивы структур Если переменных типа структура много, то определяется массив структур. void main(void){ struct book libry[100]; int i; for(i=0; i<100; i++){ puts("Введите название книги"); gets(libry[i].title); puts("Введите автора книги"); gets(libry[i], author); puts("Введите цену книги"); scanf("%f",&libry[i].value); } } Индекс применяется к имени массива структур libry[i]. Если libry[2].title[3] – это 4-й элемент в title в 3-й структуре типа book. 17.1.2. Вложенные структуры Если одна структура содержится или "вложена" в другую, то говорят, что это вложенные структуры. struct names{ char name[20]; char fio[20];}; struct worker{ struct names people; char job[20]; float money;}; void main(void){ struct worker driver = {{"Иван", "Иванов"}, "водитель", 1234.1}; Для обращения к элементу вложенной структуры применяется две операции «точка». puts(driver .people.name); 17.1.3. Указатели на структуры Указателями на структуры легче пользоваться, чем самими структурами. Структура не может передаваться в качестве аргумента функции, а указатель на структуру может. struct worker *pdrv; pdrv = &driver; struct worker driver[2]; //массив структур а) pdrv = driver; // pdrv <=> &driver[0]; pdrv+1 <=> &driver[1]. Доступ к элементу структуры осуществляется через операцию ->. pdrv->job -> driver[0].job ->(*prdv).job б) pdrv->people.name 17.1.4. Операции над структурами 1) Операция получения элемента. driver.money=1234; 2) Операция косвенного получения элемента. pdrv->money=3456; 46 17.1.5. Передача структуры в функцию 1. Можно передавать элемент структуры в качестве параметра в функцию. Тогда функция не знает, что это структура. struct funds{ char* bank; float fonds; char* name; float savef; }stan={"ПРБ", 1023.87, «Иванов И.И.», 123,45}; float sum (float, float); void main(void){ printf ("У Иванова И.И.всего %.2f рубл.\n", sum(stan.fonds,stan.savef)); } float sum(float x, float y){ return(x+y); } Функция sum() не знает, что ей передается элементы структуры, важно, что они имеют тип float. 2. Если нужно, чтобы она воздействовала на элемент структуры, то нужно передвать адрес элемента и далее работать через указатель определенного типа. modify(&stan.savef); 3. Сообщение функции, что она имеет дело со структурой. Для этого нужно передать адрес структуры в качестве параметра. struct funds {...} stan={...}; void main(void){ float sum (struct funds*); printf ("У Иванова И.И. %.2f рублей\n", sum(&stan)); } float sum (stuct funds* money){ return(money->fonds+money->savef); } Указатель money ссылается на структуру funds. В отличие от массива имя структуры не является её адресом, поэтому указываем адрес &stan. 4. Имеется массив структур. В этом случае имя массива является его адресом. struct funds {...}stans[2]={{...},{...}}; void main(void){ float sum (struct funds*); printf ("Всего капитала %.2f рублей\n", sum(stans)); } float sum(struct funds* money){ float summ; int i; for (i=0, summ=0;i<2; i++, money++) summ+=money->fonds+money->savef; return (summ); } money <=> &stan[0]; увеличивается money++, ссылаемся на stan[1]. 47 Пример 1. Определить номер дня в году. struct date { int day; int month; int year; int yearday;}d={25,3,1999}; int date_tab[2][13]={{ 0,31,28,31,30,31,30,31,31,30,31,30,31}, { 0,31,29,31,30,31,30,31,31,30,31,30,31}}; int day_of_year(struct date *pd) { int i, day, l=0; day = pd->day; if(pd->year%4==0&&pd->year%100!=0||pd->year%400==0) //год высок. l=1; for(i=0; i< pd->month; i++) day+=date_tab[l][i]; return(day); } void main(void) { d.yearday=day_of_year(&d); - вызов функции. printf(“%d“, d.yearday); } 17.2. Объединения Объединения - это средство, позволяющее запоминать данные различных типов в одном и том же месте памяти. Объединение позволяет создавать массив, состоящий из элементов одинакового размера, каждый из которых может содержать различные типы данных. Определяется также как и структура. Кючевое слово union. Есть шаблон определения и переменные этого типа. union simbl { int digit; double bigfl; char letter; }; union simbl fit, save[10], *pu. Компилятор выделяет память по наибольшему из элементов объединения bigfl (double 8 байт), для массива структур save[10] будет выделено (10 x 8) байт. Как обращаться к элементу объеденения? fit.digit=23; (использ. 2байта) fit.bigfl=2.0 (23 стирается и записывается 2.0) fit.letter='a' (2.0 стирается и записывается'a'в 1байт) pu=&fit; x=pu->digit; Все типы объединения начинаются с одного и того же адреса и в данный момент можно обратиться только к одному данному объединения. В С++ можно создавать «безымянные» объединения. В объявлении безымянного объединения отсутствует как тег объединения, так и список объектов этого типа: union { int digit; double bigfl; char letter; }; После появления такого объявления имена членов объединения могут использоваться подобно именам обычных переменных; при этом они все расположены в памяти, начиная с одного адреса. 48 digit=23; bigfl=2.0 Безымянные объединения очень удобно использовать в качестве членов структур. 17.3. Синоним имени типа Встречаются ситуации, когда удобно ввести синоним для имени некоторого типа. Строится синоним имени с помощью ключевого слова typedef. Примеры: typedef int INT //INT-синоним типа int INT x, y; typedef unsigned size_t; size_t x, y; //переменная типа unsigned typedef char string[225]; string array; //char array[225]; 1. Функция typedef даёт имена типам данных. 2. Выполняется компилятором. 3. Более гибка, чем #define. Испоьзование типа real вместо float: typedef float real; real x, y[5], *px; если определение расположено внутри функции, то область действия локальна, вне функции глобальна. typedef char* STRING //STRING-идинтификатор указателя на тип char. STRING name, sign; //char*name,*sign; 17.4. Определение именнованных констант Существуют 3 вида именнованных констант: - имя любого массива или функции; - имена членов перечисления; - любое имя любого типа, в определении которого присутствует модификатор const. const i = 5; const char *ip = &i; Поскольку модификация такого объекта-константы запрещена, он должен быть инициализирован. const int *ip; //константой является объект, на который указывает указатель; int* const ip; //сам указатель является константой const char *pc = "Это строка"; pc[2] = 'a'; //ошибка pc = "Это другая строка"; //верно char* const pc = "Это строка"; pc[2] = 'a'; //верно pc = "Это другая строка"; //ошибка Использование const предпочтительнее по сравнению с #define, так как использование константы контролирует компилятор. 49 17.5. Перечисления Спецификатор enum позволяет программисту создавать собственные типы. enum weekDays {Monday, Tuesday, Wensday, Thursday, Friday}; Идентификаторы перечисления представляют собой целочисленные переменные, которые по умолчанию имеют значения 0,1,..., если не указаны другие значения. weekDays days; Переменная days теперь может принимать одно из 5 значений. days = Wensday; Пример 2. enum colors {Red=2, Green=3, Grey}; Если задано значение впереди стоящему члену перечисления, то Grey по умолчанию будет равен 4. Пример 3. enum VIDEO_BASE_ADDRES { VGA_EGA=0xA000000, CGA=0xB800000, MONO=0xB000000}; 17.6. Битовые поля В некоторыя задачах для экономии памяти необходимо упаковывать несколько объектов в одно машинное слово. В Си для этого определяются поля и доступ к ним. Поле – это последовательность битов внутри одного целого значения. struct { unsigned a:8; unsigned b:6; unsigned c:2;}d; Определяем структуру d, содержащую поле а – 8 битов, поле b – 6 битов, с – 2 бита. Поля описываются как unsigned, чтобы подчеркнуть, что это величины без знака. Отдельные поля теперь обозначаются как d.a, d.b, d.c. С полями можно выполнять различные операции. d.a= d.b=( d.c<<2)+6; Поля не могут переходить за границу слова в ЭВМ. Если же очередное поле не помещается в частично заполненное слово, то под него выделяется новое слово. Поля могут быть безымянными. Используются как заполнители. Для принудительного перехода на новое слово используется специальный размер 0. struct {unsigned a:8; :2; unsigned b:6; :0; unsigned c:12;} d; Битовые поля и объединения можно применять для неявного преобразования типов. Пример 1. struct DOS_DATE { unsigned int day:5; unsigned int month:4; unsigned int year:7;}; union DATE_CONV { unsigned int packed_date; struct DOS_DATE unpacked_date;}; typedef union DATE_CONV DATE void main(void) { struct ffblk ff; //структура в которую читается информация о 50 //файле из каталога, описана в <dir.h> int done=findfirst(“*.*”, &ff,0); //ищет первый файл в каталоге if(!done) { DATE d; d.packed_date=ff.ff_date; printf(%2d/%2d/%4d”, d.unpacked_date.day, d.unpacked_date.month, d.unpacked_ date .year+1980); } } 18. ДИНАМИЧЕСКОЕ ВЫДЕЛЕНИЕ ПАМЯТИ До сих пор в программе использовались переменные и массивы, создаваемые компилятором языка. Однако при этом не рационально расходуется память. В отличие от статических и автоматических данных, память под которые распределяется компилятором, динамически распределяемая память выделяется программой самостоятельно. Время жизни таких объектов также определяется программой. Память выделяется по мере необходимости и должна освобождаться, как только данные, содержащиеся в ней больше не нужны. Доступ к ней осуществляется при помощи указателей. Функции создания динамических переменных и массивов объявлены в заголовочных файлах <alloс.h>, <stdlib.h>. 1. Функция void* malloc(размер) - выделяет в «куче» n байтов и возвращает указатель на 1-й байт, иначе возвращает 0. Необходимо делать преобразование типов. void main(void){ char *original=”Исходная строка”; char *copy; copy=(char*)malloc(strlen(original)+1); if(copy==NULL) { puts(“Ошибка выделения памяти”); exit(1); } strcpy(copy,original); cout<<copy<<endl; cout<<original<<endl; free(copy); } При выделении памяти она не очищается. Размер указывается в байтах. 2. Функция void* calloc(n,size type); long* newmas = (long*)calloc(100,sizeof(long)); первый параметр - количество требуемых ячеек памяти; второй параметр - размеp каждой ячейки. Функция calloc() обнуляет выделенную память. #define SIZE 128 void main(void) { char *str=(char *)calloc(1,SIZE); if(str==NULL) { puts(“Ошибка выделения памяти”); exit(1); } else { cout<<”Введите строку”; 51 gets(str); cout<<str; free(s); } } 3. void* realloc(void *, n) - изменяет размер ранее выделенного блока памяти. Если дополнительная память выделяется в новом месте оперативной памяти, то уже введенная информация переписывается в новое место. ptr = realloc(nptr, size); где nptr - указатель на ранее выделенный блок; size размер выделяемой памяти. void main(void) { char *p1, *p2; puts(«Выделяем 128 байт»); p1=malloc(128); if(p1) { puts(“Изменяем размер блока на 256 байтов”); p2=realloc(p1,256); //блок теперь равен 256 байтам } if(p2) free(p2); else free(p1); } 4. void free(void *ptr) - освобождение выделенной памяти. Количество выделенной динамически памяти не может превышать 64 Кбайта - размера одного сектора. 18.1. Операция new и delete в С++ В С++ появились операции выделения динамической памяти и удаления (освобождения) динамической памяти. Операции new и delete выполняют динамическое распределение и отмену распределения памяти, аналогично, но с более высоким приоритетом, нежели стандартные библиотечные функции семейства malloc и free. Упрощенный синтаксис: указатель-имени = new имя <инициализатор-имени>; ... delete указатель-имени; Имя может быть любого типа. new пытается создать объект с типом "имени", распределив (при возможности) sizeof(имя) байт в свободной области памяти (которую также называют "кучей"). Продолжительность существования в памяти данного объекта - от точки его создания и до тех пор, пока операция delete не отменит распределенную для него память. В случае успешного завершения new возвращает указатель нового объекта. Пустой указатель означает неудачное завершение операции (например, недостаточный объем или слишком большая фрагментированность кучи). Как и в случае malloc, прежде чем пытаться обращаться к новому объекту, следует проверить указатель на наличие пустого значения. Возвращаемый указатель будет иметь правильный тип, "указатель имени", без необходимости явного приведения типов. name *nameptr // name может иметь любой тип, кроме функции 52 ... if (!(nameptr = new name)) { errmsg("Недостаточно памяти для name"); exit (1); } ... delete nameptr; //удаление name и отмена выделения //sizeof(name) байтов памяти new, будучи ключевым словом, не нуждается в прототипе. 18.2. Операция new с массивами Если "имя" это массив, то возвращаемый new указатель указывает на первый элемент массива. При создании с помощью new многомерных массивов следует указывать все размерности массива: mat_ptr = new int[3][10][12]; // так можно mat_ptr = new int[3][][12]; // нельзя ... delete [] mat_ptr; //освободить память, занятую массивом, //на который указывает mat_ptr 18.3. Инициализаторы с операцией new Другим преимуществом операции new по сравнению с malloc является возможность инициализации. При отсутствии явных инициализаторов объект, создаваемый new, содержит непредсказуемые данные ("мусор"). Объекты, распределяемые new, за исключением массивов, могут инициализироваться соответствующим выражением в скобках: int_ptr = new int(3); Для очистки выделенной памяти операцией new можно использовать функцию meset(), объявленную в <mem.h>. Ей передаются 3 параметра: адрес очищаемой памяти, символ для очистки, количество байт. #include <string.h> #include <stdio.h> #include <mem.h> int main(void) { char buffer[] = "Hello world\n"; printf("Буфер до memset: %s\n", buffer); memset(buffer, '*', strlen(buffer) - 1); printf("Буфер после memset: %s\n", buffer); return 0; } 18.4. Проблемы, возникающие при использовании динамичски распределяемой памяти 1). Недоступные блоки - блоки памяти, указатели на которые потеряны. 2). Висящие ссылки - указатели на освобожденные блоки памяти. 3). Повторное освобождение динамической памяти. Недоступные блоки возникают после выделения памяти при присваивании указателю какого- либо другого значения или при уничтожении локального указателя после выхода из области видимости. Пример 1: int *a1, *a2; a1 = new int[1000]; //выделили память 53 ... a1 = a2; //что-то делаем //ошибка - присвоение а1 другого //значения - память недоступна Пример 2: void func(void) { int * a1; a1 = new int[1000]; ... } //ошибка - при выходе из функции автоматически //уничтожен a1,а память, тем не менее, осталась //занята и недоступна. Необходимо следить за указателями на выделенную память: int * c; void func1(void) { int * a1; a1 = new int[1000]; c = a1; //если данные по адресу a1 необходимы вне func1 ... //иначе освободить перед выходом из функции } void func2(void) { ... delete []c; } Висящие ссылки возникают при освобождении памяти, на которую указывает более чем 1 указатель: int *a = new int[1000]; int *a1 = a; ... delete []a; d = *(a1+50); //опасно - a1 уже нельзя использовать для обращения к //массиву! ... Если нет освобождения памяти, программист в зависимости от конкретной ситуации может посчитать это ошибкой, а может и нет (хотя это в любом случае уменьшает доступные ресурсы памяти), повторное освобождение безусловно ошибочно и скорее всего приведет к зависанию системы. Как правило, подобные ошибки не проявляются немедленно после появления, что затрудняет процесс отладки. Пример 1. Написать программу учета успеваемости группы. Программа должна считать ср. бал группы и выводить на экран. #include <stdio.h> #include <conio.h> #include <iostream.h> void main(void) { int i,j,n; float varsum=0; struct stud { char fio[15]; char name[10]; struct exam { int val1; int val2; int val3; int val4;}estimate; 54 float midle; }; puts(«введите кол-во студентов”); cin>>n; struct stud *pgroup=new struct stud[n]; for(i=0; i<n; i++) { printf(“Введите данные по %d студенту\n”,i+1); cout<< “Фамилия:”; gets(pgroup->fio); cout<< “Имя:”; gets(pgroup->name); cout<< “Оценка за 1-й экзамен:”; gets(pgroup->estimate.val1); cout<< “Оценка за 2-й экзамен:”; gets(pgroup->estimate.val2); cout<< “Оценка за 3-й экзамен:”; gets(pgroup->estimate.val3); cout<< “Оценка за 4-й экзамен:”; gets(pgroup->estimate.val4); pgroup->middle = ( pgroup->estimate.val1+pgroup->estimate.val2+ pgroup->estimate.val3+ pgroup->estimate.val4)/4; cout<<”Средний бал ”<< pgroup->fio<< pgroup->name<< pgroup->midle<<endl; varsum+= pgroup->midle; } cout<<»Средний балл по группе”<<varsum/n<<endl; delete[] pgroup; } 19. ФАЙЛ Язык Си "рассматривает" файл как структуру. В stadio.h содержится определение структуры файла. Определен шаблон и директива препроцессора #defile FILE struct iobuf FILE краткое наименование шаблона файла. Некоторые системы используют директиву typedef для установления этого соответствия. typedef struct iobuf FILE 19.1. Открытие файла fopen() Функцией управляют три параметра. FILE *in; //указатель потока Для связывания указателя с файлом служит функция открытия файла fopen(), которая объявлена в заголовочном файле <stdio.h>. in = fopen("test", "r"); 1 параметр - имя открываемого файла 2 параметр "r"-по чтению "r+"-чтение и запись "w"-по записи "w+"-запись и чтение, если файл уже был, он перезаписываетя "a"-дозапись "a+"-чтение и дозапись, если файла еще не было, он создается "b"-двоичный файл "t"-текстовый файл in является указателем на файл "test". Теперь будем обращаться к файлу через этот указатель. 55 Если файл не был открыт (его нет, нет места на диске), то возвращается в указатель 0. if((in=fopen("test", "r"))==0) puts("Ошибка открытия файла"); Можно по другому in=fopen("test", "r"); if (!in) puts("Ошибка открытия файла"); 19.2. fclose(FILE *stream); 19.3. Закрытие файла fclose() //Если файл закрыт успешно, то возвращается 0 иначе -1. Функции ввода/вывода одного символа fgetc(), fputc() 1. Чтение из файла посимвольно int fgetc(FILE *stream); 2. Запись в файл посимвольно int fputc(int c, FILE *stream); Код этого же символа и возвращается. ch=fgetc(in); fputc(ch,out); # include <stadio.h> void main(void){ FILE *in,*out; char ch; if((in=fopen("prog1", "r"))==0) fputs("Ошибка открытия prog1"); if((out=fopen("prog2", "w"))==0) fputs("Ошибка открытия prog2); while((ch=getc(in))!=EOF) //”End Оf File” константа определенная в dos.h fputc(ch, out); fclose(in); fclose(out); } 19.4. Функции форматированного ввода/вывода в файл 1. Форматированный вывод в текстовый файл int fprintf(FILE *stream,”управл.cтрока”,arg1,…) Возвращает количество записанных байтов. 2. Форматированный ввод из текстового файла int fscanf(FILE *stream,”управл.cтрока”,&arg1,…) Возвращает количество прочитанных байтов. # include<stadio.h> void main(void){ FILE*in; int age; in=fopen("prog1", "r"); fscanf(in,"%d",&age); fclose(in); 56 in=fopen("prog2", "w"); fprintf(in,"число %d\n",age); fclose(in); } Модификаторы и спецификаторы, те же, что и в printf(). 19.5. Функции ввода/вывода строки символов в файл 1. Чтение текстовой строки из файла. char* fgets(char *str, int n, FILE *stream); Читает до перевода строки или n-1 байт и к концу строки присоединяет 0 байт, если прочитан \n. void main(void){ FILE*in; char string[80]; in=fopen("story", "r"); while(fgets(string,80, in)!=0) puts(string); } Считывается до конца строки '\n' или 80-1 байт. При встрече EOF возвращает 0. 2. Запись текстовой строки в файл int fputs(char *str,FILE *stream); y=fputs(“Это строка”,in); y-целое число, которое устанавливается в EOF, если fputs() встре-чает EOF или ошибку. fputs не добавляет '\n' в конeц строки. Все эти функции работают с текстовыми файлами и называются функциями последовательного доступа к файлу. Указатель внутри файла перемещается автоматически при чтении или записи. Существуют функции прямого доступа к файлу. 19.6. Функции управления указателем в файле Функция позволяет работать с файлом как с массивом. Достигать любого байта. int fseek(FILE *stream, смещение, start) Возвращает число типа int: 0 - если все хорошо; -1 - ошибка. Смещение – это количество байт на которое нужно сместить указатель по файлу с +(вперед), -(назад); start - код начальной точки: SEEK_SET или 0 – от начала файла; SEEK_END или 2 – от конца файла; SEEK_CUR или 1 – от текущего положения курсора. fseek(in,0,0) - установить курсор на начало файла. long int ftell(FILE *stream)- возвращает текущее положение курсора в файле. 19.7. Ввод/вывод записей фиксированной длины Под записью фиксированной длины можно понимать размер элемента массива или структуры. 57 1. Чтение данных из двоичного файла. int fread(void *ptr, size type, size n, FILE *stream) void *ptr – адрес массива, куда записываются данные; size type – размер типа в байтах; size n – количество данных; FILE *stream – указатель на файл. void main(void){ struct STOK record; FILE *in; in=fopen("data", "r"); int n=fread(&record, sizeof(record), 1, in); } Возвращает число считанных записей или EOF. void main(void){ float mas[100]; FILE *in; In=fopen("data", "rb"); fread(mas, sizeof(mas), 1, in); } //можно так - fread(mas, sizeof(float), 100, in); 2. запись данных в двоичный файл. int fwrite(void *ptr, size type, size n, FILE *stream) Возвращает число записанных байт. void *ptr – адрес массива, куда записываются данные; size type – размер типа в байтах; size n – количество данных; FILE *stream – указатель на файл. fwrite(mas, sizeof(mas), 1, in); Пример 1. Запись во временный файл и чтение из него в массив. #include <stdio.h> #include <stdlib.h> void main(void) { int array[100]; //создать временный файл FILE *tempf=tmpfile(); if(!tempf) { puts(“нельзя открыть временный файл”); exit(1); } for(int index=0; index<100; index++)//пишем в файл fwrite(array,sizeof(int),1,tempf); rewind(tempf); //указатель вернуть на начало fread(array,sizeof(int),100,tempf); rmtmp(); //закрыть и уничтожить временный файл } Пример 2. Проверить конец файлового потока void main(void) { int buff[100]; FILE *fp; 58 fp=fopen(“prog.txt”,”r”); if(!fp) { puts(“нельзя открыть файл”); } else { while(!feof(fp)) if(fgets(buff,100,fp)!=NULL) fputs(buff,stdout); fclose(fp); } } 20. ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ Многие задачи программирования используют динамические структуры данных. Например, организация каталога книг в библиотеке. Нельзя заранее определить количество книг, числящихся в библиотечном фонде, так как идет постоянное поступление новых книг и списание старых. Для реализации таких задач существуют различные связные списки: однонаправленные, двунаправленные; бинарные деревья и т.д. 20.1. Однонаправленные связные списки Элементы списка называются узлами. Узел представляет собой объект, содержащий в себе указатель на другой объект того же типа и данные. Очевидным способом реализации узла является структура: struct TelNum { Указатель TelNum * next; //указатель на следующий элемент long telephon; // данные Данное char name[30]; // данные Данное }; TelNum *temp = new TelNum; - создается новый узел. Список представляет собой последовательность узлов, связанных указателями, содержащимися внутри узла. Узлы списка создаются динамически в программе по мере необходимости с помощью соответсвующих функций и располагаются в различных местах динамической памити. При уничтожении узла память обязательно освобождается. Простейшим списком является линейный или однонаправленный список. Признаком конца списка является значение указателя на следующий элемент равное NULL. Для работы со списком должен существовать указатель на первый элемент - заголовок списка. Иногда удобно иметь и указатель на конец списка. Указатель заголовок Указатель Указатель Указатель Данное Данное Данное Данное Данное Данное Данное Данное NULL Основными операциями, производимыми со списками, являются обход, вставка и удаление узлов. Можно производить эти операции в начале списка, в середине и в конце. 59 Вставка узла 20.1.1. a) в начало списка start temp next next next next next temp->next = start; start = temp; b) в середину списка start current next next temp temp->next = current->next; current->next = temp; a) в конец списка end next next temp next end->next = temp; end = temp; end->next = NULL; Удаление узла из списка 20.1.2. a) первого узла start next TelNum *del = start; start = start->next; delete del; b) В середине писка next next 60 current del TelNum *del = current->next; current->next = del->next; delete del; c) в конце списка current end next next TelNum *del = end; current->next=NULL; delete del; end = current; Алгоритмы, приведенные выше, обладают существенным недостатком - если необходимо произвести вставку или удаление ПЕРЕД заданным узлом, то так как неизвестен адрес предыдущего узла, невозможно получить доступ к указателю на удаляемый (вставляемый) узел и для его поиска надо произвести обход списка, что для больших списков неэффективно. Избежать этого позволяет двунаправленный список, который имеет два указателя: один - на последующий узел, другой - на предыдущий. start NULL Указатель NULL Указатель Указатель Данное Указатель Указатель Указатель NULL Указатель Данное Данное Данное Указатель Указатель // Пример программы работы с односвязным списком #include <stdio.h> #include <conio.h> struct TelNum; int printAllData(TelNum * start); int inputData(TelNum * n); struct TelNum { TelNum * next; long number; char name[30]; }; // Описание: печать списка int printAllData(TelNum * start){ int c=0; Указатель 61 for(TelNum * t=start;t; t= t->next) printf("#%3.3i %7li %s\n",++c,t->number,t->name); return 0; } // Описание: ввод данных в структуру n конец ввода - ввод нуля // в первое поле. Возвращает 1 если конец ввода int inputData(TelNum * n){ printf("Номер?"); scanf("%7li",&n->number); if(!n->number) return 1; printf("Имя? "); scanf("%30s",&n->name); return 0; } void main(void){ TelNum * start = NULL; //сторож TelNum * end = start; do{ //блок добавления записей TelNum * temp = new TelNum; if(inputData(temp)) { delete temp; break; } else { if(start==NULL) { temp->next = start; start = temp; end = temp; } else { end->next=temp; end=temp; end->next=NULL; } } }while(1); printf("\nЗаписи после добавления новых:\n"); printAllData(start); getch(); do{ //блок удаления записей TelNum * deleted = start; start = start->next; delete deleted; printAllData(start); getch(); }while(start!=NULL); } 20.2. Бинарные деревья В бинарном дереве каждый узел содержит 2 указателя. Начальная точка бинарного дерева называется корневым узлом. 62 Корневой узел Е указывает на В и Н. Узел В является корневым узлом для левого поддерева Е, узел Н – для правого поддерева Е. За исключением самого нижнего яруса каждый узел бинарного дерева имеет одно или два поддерева. Обычно левое поддерево содержит меньшие данные, правое большие, что ускоряет поиск информации. Дерево такого типа называется деревом бинарного поиска. Поиск обычно начинается с корня. В зависимости от результата сравнения двигаются либо влево (если данные узла больше образца поиска), либо вправо (если меньше). Новые узлы всегда присоединяются к нижним вершинам, так что дерево всегда растет вниз. Чтобы распечатать бинарное дерево можно использовать алгоритм, называемый обратным ходом. Простейший алгоритм - рекурсивный. При заданном корне, программа совершает 3 шага: а) выполняет обход левого поддерева; б) печать корня; в) выполняет обход правого поддерева. Если обнаружен указатель NULL, то продвижение в направлении обхода в данном поддереве прекращается. При прямом обходе содержимое корня печатается до обхода поддеревьев. При концевом обходе содержимое корня печатается после совершения обхода двух поддеревьев. // Пример программы работы с бинарным деревом #include <stdio.h> #include <conio.h> #include <string.h> struct TelNum; void printtreepre(TelNum * ); //обход с корня дерева, левое поддерево, правое поддерево void printtreein(TelNum * ); //обход с вершины правое поддерево,корень, левое поддерево void printtreepost(TelNum * ); //обход с вершины левое поддерево, правое поддерево,корень int inputData(TelNum * ); TelNum * addtree(TelNum *, TelNum *); struct TelNum { TelNum * left, *right; long number; char name[30]; }; // Описание: печать списка void printtreepre(TelNum * root) { if(!root) return; if(root->number) printf("номер %7li фамилия %s\n",root->number,root->name); printtreepre(root->left); printtreepre(root->right); 63 } void printtreein(TelNum * root) { if(!root) return; if(root->number) printtreein(root->left); printf("номер %7li фамилия %s\n",root->number,root->name); printtreein(root->right); } void printtreepost(TelNum * root) { if(!root) return; if(root->number) printtreepost(root->left); printtreepost(root->right); printf("номер %7li фамилия %s\n",root->number,root->name); } // Описание: ввод данных int inputData(TelNum * n) { printf("Номер?"); scanf("%7li",&n->number); if(!n->number) return 1; printf("Имя? "); scanf("%30s",&n->name); return 0; } // Добавление узла к дереву TelNum * addtree(TelNum *root, TelNum *temp) { if(root==NULL) { //добавление узла в вершину дерева TelNum * root = new TelNum; root->number=temp->number; strcpy(root->name,temp->name); root->left=NULL; root->right=NULL; return root; } else { if(root->number>temp->number) root->left=addtree(root->left,temp); else root->right=addtree(root->right,temp); } return root; } // Поиск значения в бинарном дереве //по номеру телефона void searchtree(TelNum *root, int num) { while (root!=NULL) { if(root->number==num) { printf("номер %7li фамилия %s\n",root->number,root->name); return ; } 64 else{ if(root->number>num) root=root->left; else root=root->right; } } puts("Телефон не найден"); return ; } void main(void) { TelNum * start = NULL; //сторож TelNum * temp = new TelNum; do{ //блок добавления записей if(inputData(temp)) {delete temp; break; } else start=addtree(start,temp); }while(1); printtreepre(start); //ebcahgi getch(); printtreein(start); //ighebca getch(); printtreepost(start); //acbighe getch(); int num; puts("Введите номер телефона"); scanf("%d",&num); searchtree(begin,num); } 21. РАЗМЕЩЕНИЕ ДАННЫХ В ПАМЯТИ Данные могут храниться в регистрах процессора, в области статической памяти, в области организованной как стек и в динамической памяти. Статическая память - данные размещаются в ней после компиляции и хранятся до конца. В стеке - временно. Динамическая память "куча" используется в зависимости от выбранной модели памяти. Различают "ближнюю кучу" - неиспользованную часть сегмента стека и "дальнюю кучу" оставшаяся свободная память машины. В начале блока выделяемой памяти записывается его размер. Он затем используется при удалении. Вся оперативная память логически разбита на сегменты. Для обращения к сегментам используются 4 специальных регистра микропроцессора для хранения адресов сегментов: CS – сегмент кода программы, DS – сегмент статических данных программы, SS – сегмент стека для временных переменных, ES - дополнительный сегмент статических данных. 65 Для оптимизации управления памятью имеется 7 моделей памяти. Размещением данных в памяти управляет программист. Адрес любого участка памяти состоит из смещения и сегмента. Полный (физический) адрес для чтения и записи данных в память получается: адрес сегмента * 16 + смещение. 22. МОДЕЛИ ПАМЯТИ C++ поддерживает 7 моделей памяти: tiny, small, medium, compact, large, huge, flat. Для каждой модели различается количество сегментов отведенных под код программы и данных. Рассмотрим эти модели. 1. Крошечная модель Tiny - 64 Кбайта код программы и данные CS,DS,SS Сегмент кода Сегмент стат. данных Куча Свободная динамич. память 64 Кбайта SP Сегмент стека 2. Малая модель Small – 64Кбайта код программы и 64 Кбайта данные CS DS,SS Сегмент кода 64 Кбайта Сегмент стат. данных «Ближняя» куча Свободная динамич. память 64 Кбайта Сегмент стека SP «Дальняя» куча Свободная динамическая память 3. Средняя модель Medium – 1 Мбайт код программы и 64 Кбайта данные CS DS,SS Сегмент кода А 64 Кбайта Сегмент кода В 64 Кбайта Сегмент кода С 64 Кбайта Сегмент стат. данных Куча Свободная динамич. память Сегмент стека «Дальняя» куча 64 Кбайта 66 SP 4. Компактная модель Compact - 64Кбайта код программы и 1 Мбайт данные CS Сегмент кода DS Сегмент стат. данных SS Свободная динамич. память SP Сегмент стека 64 Кбайта 64 Кбайта 64 Кбайта Куча Свободная динамическая память 5.Большая модель Large – 1 Мбайт код программы и 1 Мбайт данные CS DS SS SP Сегмент кода А 64 Кбайта Сегмент кода В 64 Кбайта Сегмент кода С 64 Кбайта Сегмент стат. данных 64 Кбайта Свободная динамич. память 64 Кбайта Сегмент стека Куча 6. Гигантская модель Huge - 1 Мбайт код программы и 1 Мбайт данные CS DS Сегмент кода А 64 Кбайта Сегмент кода В 64 Кбайта Сегмент кода С 64 Кбайта Сегмент стат. данных Сегмент стат. данных SS Сегмент стат. данных Свободная динамич. память Сегмент стека 64 Кбайта 64 Кбайта 64 Кбайта 64 Кбайта 67 SP 7.Плоская модель Flat. Модель Flat соответствует варианту модели Small, но используется 32 разрядные смещения (суммарная длина адреса 6 байт). Эта модель используется только для МП 386 и выше. В этом случае с помощью одного регистра обеспечивается доступ ко всей физической памяти. Хотя регистры DS, SS, ES отличаются, они фактически указывают на один физический адрес начала сегмента (дескрипторы имеют одинаковый адрес и длину сегмента, но разные права доступа к элементам памяти внутри сегмента). Эта модель используется для программ для Windows. Организация процессора I8086 накладывает ограничения на размер статистической памяти программы - размер кодов функций и размер статических данных. Размер данных не более 64 Кб в одном сегменте, т.к. размер адресуемой памяти ПЭВМ равен 1 Мб. Существует 2 варианта построения программы: А) весь исходный текст компилируется сразу; Б) программа собирается из нескольких фрагментов (модулей), которые компилируются отдельно. В любом таком модуле свои сегмент данных и сегмент кода. Объединение сегментов может происходить по-разному – в зависимости от используемого метода настройки сегментных регистров CS и DS. Может быть так, что независимо от количества модулей настройка CS, DS выполняется только однажды - тогда размер кода должен быть меньше 64 Кбайт. Размер кода или данных ограничен адресной памятью (1 Мбайт). tiny 64 Кб всего small 64 Кб кода и 64 Кб данные medium 1Мб код, 64 Кб данные compact 64 Кб код, 1Мб данные large 1Мб код, 1Мб данные huge тоже что large, но размер статических данных может превышать 64 Кб. В huge для статистических данных выделяют более 1 сегмента. int far array [30000]; char far a [70000]; - ошибка более 64кб. char huge b[70000]; - верно. Распределение данных по сегментам и управление перехода от сегмента к сегменту берет на себя компилятор. Для каждого модуля можно выделить более одного сегмента статических данных и кода. Для "малых" моделей все указатели типа near, для больших - far. Указатели на функции для моделей tiny, small,compact - near, в остальных - far. Если все функции в одном файле, то все указатели типа near. Если несколько модулей, но они не обращаются друг к другу, тоже самое. Но если есть обращения функций одного модуля к функциям другого, они должны быть описаны как far функции. void near fn (int arg); fn (1); 68 При вызове функции внутри одного сегмента адрес возврата состоит только из смещения SP: записывется смещение адреса SP - 2: записывается параметр arg. При вызове функции из другого сегмента адрес возврата состоит из адреса сегмента и смещения void far ff(int arg); ff(2); SP: записывается смещение адреса SP - 2: записывается адрес сегмента SP - 4: записывается параметр arg. huge fn - настройка регистра DS на сегмент статических данных того модуля, которому принадлежит функция. Для far функции значения DS меняется. Для совместной компляции нескольких модулей создается файл-проект. Проект создается через пункт меню Project - проект, где указываются все компилируемые файлы. Для этого используется подпункт меню: Open Project -> Insert - добавить модуль Delete - удалить модуль 23. СПИСОК ЛИТЕРАТУРЫ 1. М. Уэйт, С. Прайт, Д. Мартин. Язык СИ. - М.: "Мир", 1988 г. 2. Кеpниган Б., Ритчи Д. Язык пpогpаммиpования Си. - М.: Финансы и статистика, 1992г. 3. Кузнецов С.Д. Турбо Си. - М.: Малип, 1992г. 4. П. Киммел Borland C++ 5.0. – C-П.: «BHV-Cанкт Питербург», 1997 год. 5. Цимбал А.А.,Майоров А.Г., Козодаев М.А. Turbo C++: язык и его применение. – М.: «Джен Ай Лтд», 1993 г. СОДЕРЖАНИЕ ОБЗОР ЯЗЫКА ПРОГРАММИРОВАНИЯ С ............................................................................................. 1 ЭТАПЫ СОЗДАНИЯ ПРОГРАММЫ .......................................................................................................... 1 СТРУКТУРА ПРОГРАММЫ НА ЯЗЫКЕ СИ ............................................................................................. 1 3.1. Внутренняя структура программы ..................................................................................................... 2 3.2. Пример программы на СИ ................................................................................................................. 2 4. БАЗОВЫЕ ЭЛЕМЕНТЫ ЯЗЫКА СИ.......................................................................................................... 3 5. ДАННЫЕ В ПРОГРАММЕ НА СИ.............................................................................................................. 3 5.1. Константы ............................................................................................................................................ 3 5.2. Базовые стандартные типы переменных ......................................................................................... 5 6. ОПЕРАЦИИ ЯЗЫКА СИ............................................................................................................................. 6 6.1. Арифметические операции ................................................................................................................ 6 6.2. Операции отношения ......................................................................................................................... 7 6.3. Логические операции.......................................................................................................................... 8 6.4. Операции с разрядами....................................................................................................................... 8 6.5. Операции сдвига ................................................................................................................................ 9 6.6. Операция условия ?: .......................................................................................................................... 9 6.7. Преобразование типов ....................................................................................................................... 9 6.8. Операции приведения ...................................................................................................................... 10 6.9. Дополнительные операции присваивания ..................................................................................... 10 7. ОПЕРАТОРЫ ЯЗЫКА СИ ........................................................................................................................ 10 8. СТАНДАРТНЫЕ ФУНКЦИИ ВВОДА И ВЫВОДА ................................................................................... 16 8.1. printf (). Служит для вывода данных на экран ............................................................................... 16 8.2. Модификаторы спецификаций преобразования............................................................................ 17 8.3. sсanf(). Функция ввода данных с клавиатуры................................................................................. 17 1. 2. 3. 69 8.4. Функции ввода/вывода одного символа getchar(), putchar(). ........................................................ 18 8.5. Функции небуфиризированного ввода с клавиатуры..................................................................... 18 8.6. Ввод/вывод в поток в С++ ................................................................................................................ 18 8.7. Форматирование вывода ................................................................................................................. 19 9. МАССИВЫ ................................................................................................................................................ 20 9.1. Одномерные массивы ...................................................................................................................... 20 9.2. Многомерные массивы .................................................................................................................... 22 10. ФУНКЦИИ ............................................................................................................................................. 24 10.1. Cоздание и использование пользовательских функций............................................................ 24 10.2. Параметры функции ..................................................................................................................... 25 10.3. Возвращение значения функцией ............................................................................................... 25 10.4. Inline-функции ................................................................................................................................ 26 10.5. Значение формальных параметров функции по умолчанию .................................................... 26 10.6. Перегрузка функций ...................................................................................................................... 27 11. КЛАССЫ ПАМЯТИ И ОБЛАСТЬ ДЕЙСТВИЯ ..................................................................................... 27 11.1. Глобальные переменные ............................................................................................................. 28 11.2. Локальные переменные ............................................................................................................... 29 12. ПРЕПРОЦЕССОР ЯЗЫКА СИ ............................................................................................................. 31 12.1. Подстановка имен ......................................................................................................................... 31 12.2. Включение файлов ....................................................................................................................... 32 12.3. Условная компиляция ................................................................................................................... 33 13. УКАЗАТЕЛИ .......................................................................................................................................... 33 13.1. Операция косвенной адресации * ............................................................................................... 34 13.2. Описание указателей.................................................................................................................... 34 13.3. Использование указателей для связи функций ......................................................................... 34 13.4. Указатели на одномерные массивы ............................................................................................ 34 13.5. Указатели на многомерные массивы .......................................................................................... 35 13.6. Операции над указателями .......................................................................................................... 36 13.7. Передача массива в качестве параметра в функцию ............................................................... 37 13.8. Указатель на void * ........................................................................................................................ 38 14. СИМВОЛЬНЫЕ СТРОКИ И ФУНКЦИИ НАД СТРОКАМИ ................................................................. 38 14.1. Массивы символьных строк ......................................................................................................... 38 14.2. Массивы указателей ..................................................................................................................... 39 14.3. Указатель как возвращаемое значение функции ....................................................................... 40 14.4. Передача указателя как параметра функции ............................................................................. 40 14.5. Функции, работающие со строками ............................................................................................. 40 14.6. Стандартные библиотечные функции......................................................................................... 40 14.7. Преобразование символьных строк ............................................................................................ 42 15. ССЫЛКИ ................................................................................................................................................ 42 16. ПАРАМЕТРЫ КОМАНДНОЙ СТРОКИ ................................................................................................ 43 17. ПРОИЗВОДНЫЕ ТИПЫ ДАННЫХ ...................................................................................................... 44 17.1. Структуры ...................................................................................................................................... 44 17.2. Объединения................................................................................................................................. 47 17.3. Синоним имени типа ..................................................................................................................... 48 17.4. Определение именнованных констант ....................................................................................... 48 17.5. Перечисления ............................................................................................................................... 49 17.6. Битовые поля ................................................................................................................................ 49 18. ДИНАМИЧЕСКОЕ ВЫДЕЛЕНИЕ ПАМЯТИ......................................................................................... 50 18.1. Операция new и delete в С++ ....................................................................................................... 51 18.2. Операция new с массивами ......................................................................................................... 52 18.3. Инициализаторы с операцией new .............................................................................................. 52 18.4. Проблемы, возникающие при использовании динамичски распределяемой памяти ............. 52 19. ФАЙЛ ..................................................................................................................................................... 54 19.1. Открытие файла fopen() ............................................................................................................... 54 19.2. Закрытие файла fclose() ............................................................................................................... 55 19.3. Функции ввода/вывода одного символа fgetc(), fputc() .............................................................. 55 19.4. Функции форматированного ввода/вывода в файл ................................................................... 55 19.5. Функции ввода/вывода строки символов в файл ....................................................................... 56 19.6. Функции управления указателем в файле .................................................................................. 56 19.7. Ввод/вывод записей фиксированной длины .............................................................................. 56 20. ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ.......................................................................................... 58 20.1. Однонаправленные связные списки ........................................................................................... 58 20.2. Бинарные деревья ........................................................................................................................ 61 70 21. 22. 23. РАЗМЕЩЕНИЕ ДАННЫХ В ПАМЯТИ ................................................................................................. 64 МОДЕЛИ ПАМЯТИ ............................................................................................................................... 65 СПИСОК ЛИТЕРАТУРЫ ...................................................................................................................... 68