Язык программирования решает две взаимосвязанные задачи: позволяет программисту записать подлежащие выполнению действия и формирует понятия, которыми программист оперирует, размышляя о своей задаче. Первой цели идеально отвечает язык, который очень "близок машине". Тогда со всеми ее основными "сущностями" можно просто и эффективно работать на этом языке, причем делая это очевидным для программиста способом. Именно это имели в виду создатели С. Второй цели идеально отвечает язык, который настолько "близок к поставленной задаче", что на нем непосредственно и точно выражаются понятия, используемые в решении задачи. Именно это имелось в виду, когда первоначально определялись средства, добавляемые к С.
Язык программирования С++ задумывался как язык, который будет:
- лучше языка С;
- поддерживать абстракцию данных;
- поддерживать объектно-ориентированное программирование.
В этой главе объясняется смысл этих фраз без подробного описания конструкций языка.
Первоначальной (и, возможно, наиболее используемой) парадигмой программирования было:
Определите, какие процедуры вам нужны; используйте лучшие из известных вам алгоритмов.
Ударение делалось на обработку данных с помощью алгоритма, производящего нужные вычисления. Для поддержки этой парадигмы языки предоставляли механизм передачи параметров и получения результатов функций. Первым процедурным языком был Фортран, а Алгол60, Алгол68, Паскаль и С продолжили это направление.
Типичным примером хорошего стиля в таком понимании может служить функция извлечения квадратного корня. Для заданного параметра она выдает результат, который получается с помощью понятных математических операций:
double sqrt(double arg)
{
// программа для вычисления квадратного корня
}
voide some_function()
{
double root = sqrt(2);
// ..
}
При такой организации программы функции вносят определенный порядок в хаос различных алгоритмов.
Со временем при проектировании программ акцент сместился с организации процедур на организацию структур данных. Помимо всего прочего это вызвано и ростом размеров программ. Модулем обычно называют совокупность связанных процедур и тех данных, которыми они управляют. Парадигма программирования приобрела вид:
Определите, какие модули нужны; поделите программу так, чтобы данные были скрыты в этих модулях.
Эта парадигма известна также как "принцип сокрытия данных". Если в языке нет возможности сгруппировать связанные процедуры вместе с данными, то он плохо поддерживает модульный стиль программирования. Теперь метод написания "хороших" процедур применяется для отдельных процедур модуля. Типичный пример модуля - определение стека. Здесь необходимо решить такие задачи:
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("невозможно");
}
Поскольку данные есть единственная вещь, которую хотят скрывать, понятие упрятывания данных тривиально расширяется до понятия упрятывания информации, т.е. имен переменных, констант, функций и типов, которые тоже могут быть локальными в модуле. Хотя С++ и не предназначался специально для поддержки модульного программирования, классы поддерживают концепцию модульности. Помимо этого С++, естественно, имеет уже продемонстрированные возможности модульности, которые есть в С, т.е. представление модуля как отдельной единицы трансляции.