- •Кетков ю.Л.
- •Раздел 5. Системные данные текстового типа 33
- •Раздел 6. Основные синтаксические конструкции языка c 46
- •Раздел 7. Указатели и ссылки 59
- •Раздел 8. Функции и их аргументы 62
- •Раздел 9. Работа с массивами. 74
- •Раздел 10. Пользовательские типы данных. 95
- •Раздел 11. Работа с файлами 104
- •Раздел 12. Библиотеки стандартных и нестандартных функций 118
- •Раздел 15. Классы. Создание новых типов данных 131
- •Раздел 16. Классы как средство создания больших программных комплексов 150
- •Раздел 17. Прерывания, события, обработка исключений 167
- •Введение
- •Раздел 1. Немного истории
- •Раздел 2. Структура программы на языке c
- •Раздел 3. Среда программирования
- •Раздел 4. Системные данные числового типа
- •4.1. Типы числовых данных и их представление в памяти эвм
- •4.1.1. Внутреннее представление целочисленных данных
- •4.1.2. Однобайтовые целочисленные данные
- •4.1.3. Двухбайтовые целочисленные данные
- •4.1.4. Четырехбайтовые целочисленные данные
- •4.1.5. Восьмибайтовые целочисленные данные
- •4.2. Внутреннее представление данных вещественного типа
- •4.3. Внешнее представление числовых констант
- •4.4. Объявление и инициализация числовых переменных
- •4.5. Ввод числовых данных по запросу программы
- •4.5.1. Потоковый ввод данных числового типа
- •4.5.2. Форматный ввод
- •4.6. Вывод числовых результатов
- •4.6.1. Форматный вывод
- •4.6.2. Потоковый вывод
- •4.7. Примеры программ вывода числовых данных
- •4.8. Операции над числовыми данными целого типа
- •4.9. Операции над числовыми данными вещественного типа
- •Раздел 5. Системные данные текстового типа
- •5.1. Символьные данные и их представление в памяти эвм
- •5.2. Строковые данные и их представление в памяти эвм
- •5.3. Ввод текстовых данных во время работы программы
- •5.3.1. Форматный ввод
- •5.3.3. Потоковый ввод
- •5.3.4. Специальные функции ввода текстовых данных
- •5.4. Вывод текстовых данных
- •5.4.1. Форматный вывод
- •5.5.2. Операции над строковыми данными
- •5.6. Управление дисплеем в текстовом режиме
- •Раздел 6. Основные синтаксические конструкции языка c
- •6.1. Заголовок функции и прототип функции
- •6.2. Объявление локальных и внешних данных
- •6.3. Оператор присваивания
- •6.4. Специальные формы оператора присваивания
- •6.5. Условный оператор
- •6.6. Оператор безусловного перехода
- •6.7. Операторы цикла
- •6.8. Дополнительные операторы управления циклом
- •6.9. Оператор выбора (переключатель)
- •6.10. Обращения к функциям
- •6.11. Комментарии в программах
- •Раздел 7. Указатели и ссылки
- •7.1. Объявление указателей
- •7.2. Операции над указателями
- •7.3. Ссылки
- •Раздел 8. Функции и их аргументы
- •8.1. Параметры-значения
- •8.2. Параметры-указатели
- •8.3. Параметры-ссылки
- •8.4. Параметры-константы
- •8.5. Параметры по умолчанию
- •8.6. Функции с переменным количеством аргументов
- •8.7. Локальные, глобальные и статические переменные
- •8.8. Возврат значения функции
- •8.9. Рекурсивные функции
- •8.10. Указатели на функцию и передача их в качестве параметров
- •8.11. "Левые" функции
- •Раздел 9. Работа с массивами.
- •9.1. Объявление и инициализация массивов.
- •9.2. Некоторые приемы обработки числовых массивов
- •9.2. Программирование задач линейной алгебры
- •9.2.1. Работа с векторами
- •9.2.2.Работа с матрицами
- •9.3. Поиск
- •9.3.1. Последовательный поиск
- •9.3.2. Двоичный поиск
- •9.4. Сортировка массивов.
- •9.4.1. Сортировка методом пузырька
- •9.4.2. Сортировка методом отбора
- •9.4.3. Сортировка методом вставки
- •9.4.4. Сортировка методом Шелла
- •9.4.5.Быстрая сортировка
- •9.5. Слияние отсортированных массивов
- •9.6. Динамические массивы.
- •Раздел 10. Пользовательские типы данных.
- •10.1. Структуры
- •10.1.1. Объявление и инициализация структур
- •10.1.2. Структуры – параметры функций
- •10.1.3.Функции, возвращающие структуры
- •10.2. Перечисления
- •10.3. Объединения
- •Раздел 11. Работа с файлами
- •11.1.Файлы в операционной системе
- •11.1. Текстовые (строковые) файлы
- •11.2. Двоичные файлы
- •11.3. Структурированные файлы
- •11.4. Форматные преобразования в оперативной памяти
- •11.5. Файловые процедуры в системе bcb
- •11.5.1. Проверка существования файла
- •11.5.2. Создание нового файла
- •11.5.3. Открытие существующего файла
- •11.5.4. Чтение из открытого файла
- •11.5.5. Запись в открытый файл
- •11.5.6. Перемещение указателя файла
- •11.5.7. Закрытие файла
- •11.5.8. Расчленение полной спецификации файла
- •11.5.9. Удаление файлов и пустых каталогов
- •11.5.10. Создание каталога
- •11.5.11. Переименование файла
- •11.5.12. Изменение расширения
- •11.5.13. Опрос атрибутов файла
- •11.5.14. Установка атрибутов файла
- •11.5.15. Опрос и изменение текущего каталога
- •11.6. Поиск файлов в каталогах
- •Раздел 12. Библиотеки стандартных и нестандартных функций
- •12.2. Организация пользовательских библиотек
- •12.3. Динамически загружаемые библиотеки
- •13.1. Препроцессор и условная компиляция
- •13.2. Компилятор bcc.Exe
- •13.3. Утилита grep.Com поиска в текстовых файлах
- •14.1. Переопределение (перегрузка) функций
- •14.2. Шаблоны функций
- •Раздел 15. Классы. Создание новых типов данных
- •15.1. Школьные дроби на базе структур
- •15.2. Школьные дроби на базе классов
- •15.3. Класс на базе объединения
- •15.4. Новые типы данных на базе перечисления
- •15.5. Встраиваемые функции
- •15.6. Переопределение операций (резюме)
- •15.8. Конструкторы и деструкторы (резюме)
- •Раздел 16. Классы как средство создания больших программных комплексов
- •16.1. Базовый и производный классы
- •16.1.1.Простое наследование
- •16.1.2. Вызов конструкторов и деструкторов при наследовании
- •16.1.3. Динамическое создание и удаление объектов
- •16.1.4. Виртуальные функции
- •16.1.5. Виртуальные деструкторы
- •16.1.6. Чистые виртуальные функции и абстрактные классы
- •16.2. Множественное наследование и виртуальные классы
- •16.3. Объектно-ориентированный подход к созданию графической системы
- •Раздел 17. Прерывания, события, обработка исключений
- •17.1. Аппаратные и программные прерывания
- •17.2. Исключения
Раздел 15. Классы. Создание новых типов данных
Одно из важнейших достижений языка C++ – возможность объявления нового типа данных и описания тех операций, которые компилятор должен научиться делать с новыми данными. Именно таким образом состав входного языка пополнился классами данных String (строки), Set (множества), Complex (комплексные переменные) и др.
15.1. Школьные дроби на базе структур
Мы продемонстрируем технику создания таких данных на примере дробно-рациональных чисел, представленных в виде пары целых чисел – числителя (num) и знаменателя (denum). Этот пример подробно исследован в книгах В. Лаптева (С++. Экспресс-курс. СПб.: БХВ-Петербург, 2004. – 512 с), У. Торпа и У. Форда ("Структуры данных в С++",....).
В качестве первого инструмента воспользуемся механизмом структур:
struct Rational {unsigned num,denum;};
Теперь имя структуры Rational может использоваться для объявления переменных или массивов нового типа:
Rational x,y,z[20];
x.num=1; x.denum=3;
Одна из первых операций, которую нам предстоит определить, связана с упрощением дроби за счет сокращения. Для этой цели нам потребуется вспомогательная функция определения наибольшего общего делителя (подобного рода пример приводился в разделе "Рекурсивные функции"):
unsigned gcd(unsigned x,unsigned y)
{//Поиск наибольшего общего делителя
if(y==0) return x;
return gcd(y,x%y);
}
void reduce(Rational &c)
{//Сокращение дроби
unsigned t=((c.num>c.denum)?gcd(c.num,c.denum):gcd(c.denum,c.num));
c.num /= t; c.denum /= t;
}
Теперь начинается самое интересное – надо научить компилятор выполнять простейшие операции над дробями. По существу, мы должны переопределить некоторые действия, которые, будучи записаны в естественном для человека виде, должны правильно интерпретироваться и компилятором. В терминологии C++ такое "волшебство" называется перегрузкой операций. Сначала мы перегрузим операцию +=, которая знакома нам по школе:
Rational& operator+=(Rational &a, const Rational &b)
{ a.num = a.num*b.denum + b.num*a.denum;
a.denum *= b.denum;
reduce(a); return a;
}
Как видите, вся хитрость переопределения операции заключается в написании функции с определенным именем. Теперь мы воспользуемся новой операцией для переопределения обычного сложения:
Rational operator+(Rational &a,const Rational &b)
{ Rational t=a; t += b; reduce(t); return t; }
Немного сложнее выглядит переопределение операций потокового ввода/вывода. Во-первых, среди аргументов функции мы должны предусмотреть ссылку на входной (istream &) или выходной (ostream &) поток. Во-вторых, мы должны организовать ввод или вывод по указанной ссылке и возвратить ее в качестве значения функции. Поэтому тип возвращаемого значения тоже должен быть ссылкой на входной или выходной поток:
istream& operator>>(istream &t, Rational &a)
{ char s;
t >> a.num; t>>s; t>> a.denum;
reduce(a); return t;
}
Вспомогательная переменная s, использованная в этой функции, предназначена для ввода символа "/", которым мы будем отделять числитель дроби от ее знаменателя.
ostream& operator<<(ostream &t, const Rational &a)
{ t << a.num << '/'<<a.denum;
return t;
}
На базе построенных функций уже можно организовать простейшую программу:
void main()
{ Rational A,B,C;
A.num=1; A.denum=2;
B.num=1; B.denum=3;
C=A+B;
cout << C << endl;
getch();
}
//===Результат работы ===
5/6
Операцию "=" мы не перегружали, хотя и использовали в программе. Но дело в том, что структуры одинакового типа можно присваивать. Точно также, не перегружая операцию индексирования ("[]") можно работать с элементами массивов:
void main()
{ Rational d[5],c;
int i;
cout<<"Enter 5 rational number:"
cout<<" num / denum <Enter>"<<endl;
for(i=0; i<5;i++) cin>>d[i];
for(i=0; i<5;i++) cout<<d[i]<<' ';
c=d[0]+d[1];
c += d[2]; c += d[3]; c+=d[4];
cout<<endl<<c<<endl;
getch();
}
//=== Результат работы ===
Enter 5 rational number: num / denum <Enter>
1/1
2/2
3/3
4/4
5/5
1/1 1/1 1/1 1/1 1/1
5/1
К уже переопределенным операциям можно добавить еще несколько операций умножения, когда оба сомножителя дробно-рациональные или один из них целочисленный:
Rational operator*(const Rational &a, const Rational &b)
{ Rational c;
c.num=a.num*b.num;
c.denum=a.denum*b.denum;
reduce(c); return c;
}
Rational operator*(const Rational &a, const unsigned &b)
{ Rational c;
c.num=a.num*b;
c.denum=a.denum;
reduce(c); return c;
}
Rational operator*(const unsigned &a, const Rational &b)
{ Rational c;
c.num=a*b.num;
c.denum=b.denum;
reduce(c); return c;
}
Для последующей перегрузки инкрементных операций нам потребуется еще одна процедура прибавления к дробно-рациональному числу целого числа:
Rational operator+=(Rational &a, const unsigned &b)
{ a.num=a.num+b*a.denum;
reduce(a); return a;
}
С операциями x++ и ++x дело обстоит не так просто. Дело в том, что обозначения этих операций одинаковы (operator++), но выполняются они по-разному. В первом случае сначала используется старое значение переменной x, а уже потом ее значение увеличивается на 1. А во втором случае сначала увеличивается x, а уже потом используется новое значение переменной. Игра строится на том, что если формула заканчивается знаком + или –, то компилятор добавляет несуществующее слагаемое, равное 0. Поэтому мы в одной операции (++x) оставим один аргумент, а во второй (x++) добавим неиспользуемый аргумент типа int. Хотя он не влияет на результат операции, но по его фиктивному присутствию компилятор правильно сориентируется между похожими функциями:
Rational operator++(Rational &a)
{//Переопределение операции ++a
a += 1; return a;
}
Rational operator++(Rational &a, int)
{//Переопределение операции a++
Rational t=a; a += 1; return t;
}
Оглядываясь на все наши перегруженные функции, невольно задаешься вопросом, а стоило ли городить весь этот огород. Не проще ли было непосредственно в программе расписывать операции над числителями и знаменателями. Если все это делается только один раз с целью демонстрации новых технологий, то, наверное, затея выеденного яйца не стоила. Но теперь мы спрячем все наши описания и новые функции в файл, который назовем rational.h. После этого программа, использующая наши навороты, выглядит совсем неплохо:
#include <stdio.h>
#include <iostream.h>
#include <conio.h>
#include "rational.h"
void main()
{ Rational A,B,C;
A.num=1; A.denum=2;
B.num=1; B.denum=3;
C=A+B;
cout << C << endl;
getch();
}
Надо иметь в виду, что новые типы данных и обслуживающие их операции тщательно планируются и разрабатываются не ради одноразового применения. Впоследствии ими может пользоваться любой программист. Зато операции над вновь созданными объектами выглядят очень естественно, вероятность появления ошибок в результате неверного использования операндов резко уменьшается. Все это положительно сказывается на качестве программ и времени их создания.
Построенный нами набор операций над дробно-рациональными числами далеко не полон. При работе с дробями потребуется их сравнивать, мы ничего не предусмотрели для обслуживания отрицательных дробей. Одним словом, предстоит еще большая работа по созданию законченного пакета для новых данных. Но мы показали, как конструируются некоторые компоненты такого пакета.
Надо отметить, что объявление нового типа данных с использованием структур и внешних функций не приветствуется современными технологиями. В нашем варианте функции не защищены от внешнего вмешательства, они не объединены в некую целостную структуру, в которой не все предназначено для внешнего использования. Т.е. то, что демонстрирует разбираемый пример – это только объяснение некоторых идей, получивших в C++ существенно более серьезное развитие.
Следует заметить, что в приведенном выше варианте программы имеется довольно непривлекательный фрагмент, связанный с объявлением типа данных и их отдельной инициализацией. В языке C для данных стандартного типа эти две процедуры совмещены, что гораздо удобнее в использовании. Для преодоления такого неудобства с новыми типами данных были придуманы специальные функции – конструкторы. Имена конструкторов совпадают с именами новых типов данных. В отличие от обычных функций конструкторы не возвращают значений (даже значений типа void). Они преследуют две цели – выделить необходимые ресурсы памяти для хранения объявляемого объекта и произвести начальную инициализацию всех его полей.
К типовым конструкторам относятся конструктор по умолчанию, конструктор инициализации и конструктор копирования. Конструктор по умолчанию не имеет параметров и, выделяя поля под хранение компонент объекта, обычно производит их очистку. Конструктору инициализации сообщают начальные значения определенных полей. Конструктор копирования выделяет ресурсы новому объекту и копирует в его поля содержимое полей другого объекта, полученного в качестве параметра. В примере с дробно-рациональными числами могли быть объявлены следующие конструкторы:
//Конструктор по умолчанию
Rational() { num=0; denum=1; }
//Конструктор инициализации
Rational(unsigned n) { num=n; denum=1; }
//Еще один конструктор инициализации
Rational(unsigned n,unsigned d) {if(d!=0){num=n; denum=d;}}
//Конструктор копирования
Rational(const Rational &r) { num=r.num; denum=r.denum; }
В языке C++ более распространен другой способ объявления inline-конструкторов, использующий так называемые списки инициализации:
Rational():num(0),denum(1){}
Rational(unsigned n):num(n),denum(1){}
Rational(unsigned n,unsigned d):num(n),denum((d!=0)?d:1){}
Rational(const Rational &r):num(r.num),denum(r.denum){}
Включение таких конструкторов в файл rational.h сделает процедуру объявления и инициализации новых данных более цивилизованной:
Rational A(1,2),B(1,3),C;
Вы, наверное, помните, что компилятор языка C берет на себя преобразования типов аргумента при обращении к математическим функциям. По прототипам, как правило, их аргументы имеют тип double, но мы можем обращаться к ним и с данными типа float, и с целочисленными аргументами. Необходимое преобразование аргумента компилятор выполняет сам. Аналогичные преобразования следовало бы предусмотреть и в нашем пакете обработки дробно-рациональных данных. Если бы в нашем распоряжении оказались средства по прямому и обратному преобразованию данных типов Rational и unsigned, то не пришлось бы писать по три варианта операций умножения (Rational* Rational, Rational*unsigned, unsigned* Rational).
Роль преобразования данных могут выполнять конструкторы и специальные функции. Например, для преобразования типа unsignedRational можно было бы написать следующий конструктор:
Rational(unsigned n=0,unsigned d=1)
{ if(d!=0){num=n; denum=d; }}
Наличие в конструкторе параметров по умолчанию позволяет теперь объявлять переменные типа Rational следующим образом:
Rational x(5,1); //по-старому
Rational x(5); //по-новому, с преобразованием unsignedRational
Rational x=5; //по-новому
Для обратного преобразования Rationalunsigned необходимо написать специальную функцию, которую надо объявить внутри структуры:
operator unsigned(){return num/denum;}
После этого целочисленным данным типа unsigned можно присваивать значения дробно-рациональных данных.