
- •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
Глава 1. Обзор C++
Язык программирования C++ является объектно-ориентированным. Различные аспекты объектно-ориентированного программирования в языке C++ переплетены между собой. Поэтому, прежде чем изучать нюансы, важно получить общее представление об особенностях объектно-ориентированного программирования. Задачей этой главы является обзор ключевых концепций, включенных в C++. Остальные главы посвящены подробному изучению специфических особенностей C++.
1.1 Происхождение C++
C++ является расширением языка С. С представляет собой гибкий и мощный язык программирования, использовавшийся для разработки наиболее важных программных продуктов в течение прошедших нескольких лет. Однако, как только проект превышает определенные размеры, возможности применения языка С достигают своих границ. В зависимости от проекта, программы размером от 25000 до 100000 строк оказываются трудными для разработки и управления потому, что их трудно охватить целиком. Работая в Bell Laboratories в Murray Hill, штат Нью-Джерси, Бьярн Страуструп (Bjarne Stroustrup) добавил к языку С несколько расширений с целью решить эту проблему. Первоначально язык назывался “С с классами”. Это название было заменено на C++ в 1983 году.
Большинство сделанных Страуструпом добавлений к С поддерживают объектно-ориентированное программирование, которое сокращенно называют ООП. Как отмечает Страуструп, целый ряд объектно-ориентированных концепций был добавлен в C++, основываясь на языке Симула-6. Поэтому C++ представляет собой смесь двух мощных программных методов.
С момента своего возникновения C++ подвергался серьезным ревизиям трижды, первый раз в 1985 году, второй — в 1989 году. Третий пересмотр языка произошел в связи с работой над стандартом ANSI для C++. Первая версия предложенного стандарта была создана к 25 января 1994 года. Комитет ANSI по языку C++ практически сохранил все черты языка, определенные Страуструпом, и добавил несколько новых. Процесс стандартизации обычно является достаточно медленным, и стандартизация C++ не является исключением. Работа над C++ продолжается и некоторые его характеристики могут быть модифицированы.
Изобретая C++ путем добавления к языку С поддержки объектно-ориентированного программирования, Страуструп представлял всю важность сохранения философии языка С, включая его эффективность, гибкость и то, что именно программист, а не язык отвечает за разрабатываемое программное обеспечение. C++ обеспечивает всю свободу языка С одновременно с мощью объектов. Как отмечал Страуструп, C++ позволяет добиться ясности, расширяемости и легкости сопровождения за счет структуризации, причем без потери эффективности.
Хотя первоначально C++ был нацелен на работу с очень большими программами, это никоим образом не ограничивает его применение. Фактически объектно-ориентированные атрибуты языка C++ могут быть эффективно применены практически к любой задаче программирования.
1.2 Философские замечания
Язык программирования решает две взаимосвязанные задачи: позволяет программисту записать подлежащие выполнению действия и формирует понятия, которыми программист оперирует, размышляя о своей задаче. Первой цели идеально отвечает язык, который очень "близок машине". Тогда со всеми ее основными "сущностями" можно просто и эффективно работать на этом языке, причем делая это очевидным для программиста способом. Именно это имели в виду создатели С. Второй цели идеально отвечает язык, который настолько "близок к поставленной задаче", что на нем непосредственно и точно выражаются понятия, используемые в решении задачи. Именно это имелось в виду, когда первоначально определялись средства, добавляемые к С.
Язык программирования С++ задумывался как язык, который будет:
- лучше языка С;
- поддерживать абстракцию данных;
- поддерживать объектно-ориентированное программирование.
В этой главе объясняется смысл этих фраз без подробного описания конструкций языка.
1.3 Процедурное программирование
Первоначальной (и, возможно, наиболее используемой) парадигмой программирования было:
Определите, какие процедуры вам нужны; используйте лучшие из известных вам алгоритмов.
Ударение делалось на обработку данных с помощью алгоритма, производящего нужные вычисления. Для поддержки этой парадигмы языки предоставляли механизм передачи параметров и получения результатов функций. Первым процедурным языком был Фортран, а Алгол60, Алгол68, Паскаль и С продолжили это направление.
Типичным примером хорошего стиля в таком понимании может служить функция извлечения квадратного корня. Для заданного параметра она выдает результат, который получается с помощью понятных математических операций:
double sqrt(double arg)
{
// программа для вычисления квадратного корня
}
voide some_function()
{
double root = sqrt(2);
// ..
}
При такой организации программы функции вносят определенный порядок в хаос различных алгоритмов.
1.4 Модульное программирование
Со временем при проектировании программ акцент сместился с организации процедур на организацию структур данных. Помимо всего прочего это вызвано и ростом размеров программ. Модулем обычно называют совокупность связанных процедур и тех данных, которыми они управляют. Парадигма программирования приобрела вид:
Определите, какие модули нужны; поделите программу так, чтобы данные были скрыты в этих модулях.
Эта парадигма известна также как "принцип сокрытия данных". Если в языке нет возможности сгруппировать связанные процедуры вместе с данными, то он плохо поддерживает модульный стиль программирования. Теперь метод написания "хороших" процедур применяется для отдельных процедур модуля. Типичный пример модуля - определение стека. Здесь необходимо решить такие задачи:
1) Предоставить пользователю интерфейс для стека (например, функции push() и pop()).
2) Гарантировать, что представление стека (например, в виде массива элементов) будет доступно лишь через интерфейс пользователя.
3) Обеспечивать инициализацию стека перед первым его использованием.
Язык Модула-2 прямо поддерживает эту парадигму, тогда как С только допускает такой стиль. Ниже представлен на С возможный внешний интерфейс модуля, реализующего стек:
void push(char);
char pop();
const int stack_size = 100;
Допустим, что описание интерфейса находится в файле stack.h, тогда реализацию стека можно определить следующим образом:
#include "stack.h"
static char v[stack_size]; // “static” означает: локальный в данном файле/модуле
static char *p = v; // стек вначале пуст
void push(char c)
{
//проверить на переполнение и поместить в стек
}
char pop()
{
//проверить, не пуст ли стек, и считать из него
}
Вполне возможно, что реализация стека может измениться, например, если использовать для хранения связанный список. Пользователь в любом случае не имеет непосредственного доступа к реализации: v и p - статические переменные, т.е. переменные, локальные в том модуле (файле), в котором они описаны. Использовать стек можно так:
#include "stack.h" // используем интерфейс стека
void some_function()
{
push('c');
char c = pop();
if (c != 'c') error("невозможно");
}
Поскольку данные есть единственная вещь, которую хотят скрывать, понятие упрятывания данных тривиально расширяется до понятия упрятывания информации, т.е. имен переменных, констант, функций и типов, которые тоже могут быть локальными в модуле. Хотя С++ и не предназначался специально для поддержки модульного программирования, классы поддерживают концепцию модульности. Помимо этого С++, естественно, имеет уже продемонстрированные возможности модульности, которые есть в С, т.е. представление модуля как отдельной единицы трансляции.