
- •1.2 Философские замечания
- •1.3 Процедурное программирование
- •1.4 Модульное программирование
- •1.5 Абстракция данных
- •1.6 Пределы абстракции данных
- •1.7 Объектно-ориентированное программирование
- •1.8 Концепции объектно-ориентированного программирования
- •1.8.1 Инкапсуляция
- •1.8.2 Полиморфизм
- •1.8.3 Наследование
- •1.10 Несколько полезных советов
- •2.2 Перегрузка функций
- •2.3 Перегрузка операторов
- •2.4 Наследование
- •2.5 Конструкторы и деструкторы
- •2.7 Два новых типа данных
- •Глава 3. Классы и объекты
- •3.1 Параметризованные конструкторы
- •3.2 Дружественные функции
- •3.3 Значения аргументов функции по умолчанию
- •3.3.1 Корректное использование аргументов по умолчанию
- •3.4 Взаимосвязь классов и структур
- •3.5 Связь объединений и классов
- •3.6 Анонимные объединения
- •3.7 Inline-функции
- •3.7.1 Создание inline-функций внутри класса
- •3.8 Передача объектов в функции
- •3.9 Возвращение объектов функциями
- •3.10 Присваивание объектов
- •3.11 Конструктор копирования
- •3.12 Массивы объектов
- •3.12.1 Инициализация массивов объектов
- •3.12.2 Создание инициализированных и неинициализированных массивов
- •3.13 Указатели на объекты
- •3.14 Статические члены класса
- •Глава 4. Перегрузка функций и операторов
- •4.1 Перегрузка конструкторов
- •4.2 Локализация переменных
- •4.3 Локализация создания объектов
- •4.4 Перегрузка функций и неопределенность
- •4.5 Определение адреса перегруженной функции
- •4.6 Указатель this
- •4.7 Перегрузка операторов
- •4.8 Дружественная функция-оператор
- •4.9 Ссылки
- •4.9.1 Параметры-ссылки
- •4.9.2 Передача ссылок на объекты
- •4.9.3 Возврат ссылок
- •4.9.4 Независимые ссылки
- •4.9.5 Использование ссылок для перегрузки унарных операторов
- •4.10 Перегрузка оператора []
- •4.11 Создание функций преобразования типов
- •Глава 5. Наследование, виртуальные функции и полиморфизм
- •5.1 Наследование и спецификаторы доступа
- •5.1.1 Спецификаторы доступа
- •5.1.2 Спецификатор доступа при наследовании базового класса
- •5.1.3 Дополнительная спецификация доступа при наследовании
- •5.2 Конструкторы и деструкторы производных классов
- •5.3 Множественное наследование
- •5.4 Передача параметров в базовый класс
- •5.5 Указатели и ссылки на производные типы
- •5.6 Ссылки на производные классы
- •5.7 Виртуальные функции
- •5.8 Для чего нужны виртуальные функции?
- •5.9 Чисто виртуальные функции и абстрактные типы
- •5.10 Виртуальный базовый класс
- •5.11 Раннее и позднее связывание
- •Глава 6. Подсистема динамического выделения памяти
- •6.1 Введение в обработку исключений
- •6.1.1 Перехват всех исключений
- •6.2 Работа с памятью с помощью new и delete
- •6.3 Размещение объектов
- •6.4 Перегрузка new u delete
- •7.1.1 Потоки
- •7.3 Создание собственных операторов вставки и извлечения
- •7.3.1 Создание операторов вставки
- •7.3.2 Перегрузка операторов извлечения
- •7.4 Форматирование ввода/вывода
- •7.4.1 Форматирование с помощью функций-членов класса ios
- •7.4.2 Использование манипуляторов
- •7.5 Создание собственных функций-манипуляторов
- •7.5.1 Создание манипуляторов без параметров
- •7.5.2 Создание манипуляторов с параметрами
- •7.6 Файловый ввод/вывод
- •7.6.1 Открытие и закрытие файлов
- •7.6.2 Чтение и запись в текстовые файлы
- •7.6.3 Двоичный ввод/вывод
- •7.6.4 Определение конца файла
- •7.6.5 Произвольный доступ
- •Глава 8. Ввод/вывод в массивы
- •8.1 Классы ввода/вывода в массивы
- •8.2 Создание потока вывода
- •8.3 Ввод из массива
- •8.4 Использование функций-членов класса ios
- •8.5 Потоки ввода/вывода в массивы
- •8.6 Произвольный доступ в массив
- •8.7 Использование динамических массивов
- •8.8 Манипуляторы и ввод/вывод в массив
- •8.9 Собственные операторы извлечения и вставки
- •8.10 Форматирование на основе массивов
- •Глава 9. Шаблоны и библиотека stl
- •9.1 Функции-шаблоны
- •9.2 Функции с двумя типами-шаблонами
- •9.3 Ограничения на функции-шаблоны
- •9.4 Классы-шаблоны
- •9.5 Пример с двумя типами-шаблонами
- •9.6 Обзор библиотеки stl
- •9.7 Класс vector
- •9.7 Класс string
- •9.8 Класс list
4.3 Локализация создания объектов
Тот факт, что локальные переменные могут объявляться в любом месте блока кода, имеет большое значение для создания объектов. В реальных программах часто необходимо создавать объекты, инициализирующиеся значениями, появляющимися только в процессе выполнения программы. Имея возможность создавать объект после того, как эти величины становятся известными, можно избежать усложнения программы, которое возникает, когда сначала создается неинициализированный объект и лишь затем ему присваивается значение.
Для того чтобы увидеть преимущества, предоставляемые объявлением локальных объектов вблизи точки их первого использования, рассмотрим следующую версию программы timer. В ней два объекта b и c создаются, используя информацию, получаемую во время выполнения программы как раз перед первым использованием этих объектов. Программа также иллюстрирует преимущества перегрузки конструкторов, что позволяет реализовать различные способы инициализации.
#include <iostream.h>
#include <stdlib.h>
#include <time.h>
class timer
{
//аналогично прошлому примеру
};
int main()
{
timer a(10);
a.run();
cout << "Enter number of seconds: ";
char str[80];
cin >> str;
timer b(str); // инициализация во время выполнения с помощью строки
b.run();
cout << "Enter minutes and seconds: ";
int min, sec;
cin >> min >> sec;
timer с(min, sec); /* инициализация во время выполнения с помощью минут и секунд */
c.run();
return 0;
}
Как можно видеть, объект а создан с использованием целой константы. В то же время объекты b и с созданы с использованием информации, введенной пользователем. Поэтому они не объявлялись до тех пор, пока эта информация не была известна. Таким образом, объекты b и с созданы с использованием данных, которые были сформированы как раз перед точкой их создания. Для объекта b в качестве таких данных выступает строка, в которой записаны секунды. Для объекта с данными служат два целых числа, с помощью которых вводятся минуты и секунды. Поскольку допустимы различные форматы инициализации, то нет необходимости выполнять преобразования данных от одного типа к другому при инициализации объекта. Из сказанного можно заключить, что целесообразно создавать объекты перед точкой их первого использования.
4.4 Перегрузка функций и неопределенность
При перегрузке функций возможно появление ошибок такого типа, какие нам раньше не встречались. Можно создать ситуацию, в которой компилятор не сможет выбрать между двумя или более перегруженными функциями. Когда такое происходит, говорят, что ситуация неопределенна, двусмысленна (ambiguous). Неопределенные инструкции являются ошибками, и программа, содержащая неопределенности, не будет откомпилирована.
Основная причина, которая может вызывать неопределенность, связана с автоматическим преобразованием типов в C++. C++ автоматически пытается преобразовать аргументы, использованные при вызове функции, к типу аргументов интерфейса функции. В качестве примера рассмотрим следующий фрагмент:
int F(double d);
cout << F('с'); // ошибки не происходит из-за преобразования
Как указано в комментарии, данный код не является ошибочным, потому что С автоматически конвертирует символ с в его эквивалент типа double. В C++ только немногие из подобных преобразований запрещены. Хотя автоматическое преобразование типов является очень удобным, оно служит наиболее распространенной причиной неопределенности. Например, рассмотрим следующую программу:
#include <iostream.h>
float F(float i) { return i; }
double F(double i) { return -i; }
int main()
{
cout << F(10.1) << " "; // неопределенности нет, вызов F(double)
cout << F(10); // неопределенность
return 0;
}
Здесь функция F() перегружена, так что она может иметь в качестве аргументов переменные типа float или double. Первая строка в функции main() не вызывает неопределенности, поскольку число 10.1 автоматически преобразуется в C++ к типу double и вызывается функция F() с аргументом типа double. Однако когда функция F() вызывается с использованием числа 10, возникает неопределенность, поскольку компилятор не может определить, приводить это число к типу float или типу double. Это вызывает сообщение об ошибке, и программа не компилируется.
Как показывает предыдущий пример, неопределенность вызвана не перегрузкой функции F, а вызовом этой функции с неопределенным типом аргумента, т.е. особенностями ее вызова.
Ниже приведен другой пример неопределенности, вызванной автоматическим преобразованием типа в языке C++:
#include <iostream.h>
char F(unsigned char ch) { return ch-1; }
char F(char ch) { return ch+1; }
int main()
{
cout << F('c'); // вызов F(char)
cout << F(88) << " "; // неопределенность
return 0;
}
В C++ unsigned char и char не являются двусмысленными. Тем не менее, когда функция F() вызывается с числом 88, компилятор не знает, какую из функций вызывать. Иными словами, следует ли 88 преобразовывать к char или к unsigned char?
Другой способ возникновения неопределенности связан с использованием в перегруженных функциях аргументов по умолчанию. Чтобы увидеть, как это происходит, рассмотрим следующую программу:
#include <iostream.h>
int F(int i) { return i; }
int F(int i, int j = 1) { return i*j; }
int main()
{
cout << F(4, 5) << " "; // неопределенности нет
cout << F(10); // неопределенность
return 0;
}
Здесь при первом вызове функции F() указываются два аргумента, так что неопределенность не возникает и вызывается функция F(int i, int j). Однако при втором вызове функции F() возникает двусмысленность, поскольку компилятор не знает, вызывать ли функцию F() с одним аргументом, или же использовать функцию с двумя аргументами, у которой второй аргумент принимает значение по умолчанию.