ВВЕДЕНИЕ

В течение последних двух десятилетий языки программирова­ния развивались очень интенсивно как в количественном отно­шении (появление множества новых языков, особенно специали­зированных, рост возможностей языков высокого уровня, вклю­чая даже бейсик), так и в качественном отношении. Эти каче­ственные изменения отразились в изменении самой методоло­гии программирования, в появлении парадигмы объектно-ори­ентированного программирования.

Вещь эта – весьма новая и весьма сложная, к тому же эффек­тивная лишь при профессиональной работе и решении больших задач, то есть тех задач, за которые платят. Поэтому вокруг проблем ООП очень много разговоров, однако очень мало книг, еще меньше хороших книг, а специалистов в этой области еще меньше.

Попробуем вкратце рассказать, что такое ООП, почему для освоения ООП выбран язык С++, а не иной.

Языки – ветераны, такие как  Алгол, Фортран, Паскаль и др. яв­ляются процедурными языками. При программировании на этих языках основное внимание уделяется разработке процедур (функций) и подбору наилучших алгоритмов. Процедурное программирование – вещь хорошая, мы с вами в этом убедились, разбивая программы на автономные функции.

Со временем внимание при проектировании программ стало перемещаться с разработки процедур  к более рациональной организации данных (включая и процедуры). Причины очевидны – рост объемов программ и сложности обрабатываемых данных.

Это привело к появлению модульного программирования, при котором множество связанных процедур и обрабатываемых ими данных оформляется в единый модуль.

При модульном программировании акцент делается на разбиении программы на модули таким образом, чтобы данные, обрабатываемые модулем, были спрятаны в нем. К языкам модульного программирования можно отнести Модула-2, базовый Си и некоторые другие.

Модульное программирование было шагом вперед, так как вело к более структурированным программам. Мы видели, что в рамках Си можно было вводить некоторые собственные типы данных (модули), которые однако значительно отличались от типов, встроенных в язык.

Следующим шагом в развитии языков были языки, предоставляющие возможность определения типов данных, которые вели бы себя почти так же, как встроенные типы (Ада, С++). Подобные типы обычно называют абстрактными типами данных (типами, определяемыми пользователем). Они объединяют данные и функции, манипулирующие этими данными, в один тип.

При использовании абстрактных типов данных важно определить, какие типы данных требуются для решения задачи, и обеспечить полный набор операций для каждого типа. Абстракции данных значительно облегчили обработку сложных структур данных. Но при разработке программ, требующих несколько пользовательских типов, которые имеют много общих свойств, оказалось, что часто приходится дублировать большие фрагменты программы. Избежать этого позволили языки ООП (Симула-67, Smalltalk, С++  и др.)

1. Основные свойства ООП

Можно сказать, что  ООП – результат эволюции методологии программирования. Оно в большей степени, чем модульное программирование, предоставляет возможность  создавать программы, обладающие структурированностью, модульностью и абстракциями данных.

Три основные свойства языка ООП:

  • Инкапсуляция (сокрытие информации)- определение пользователем новых типов данных. Каждый такой тип содержит определение набора значений и операций, которые могут быть выполнены над этими значениями, и образуют так называемый абстрактный тип данных. При этом никакие другие функции, кроме набора операций,  не должны иметь доступа  к значениям типа.

Например, новый тип «предприятие торговли» может содержать значения, характеризующие арендуемую площадь, количество работающих, объем товарооборота, а также действия: расчеты по кредитам, выплата налогов, арендной платы и пр.

  • Наследование – это механизм, позволяющий строить иерархию типов. Механизм наследования предполагает  определение базового типа (прародительского), а затем  использование этого типа для построения производных типов (потомков). Причем каждый производный тип наследует все свойства базового типа, включая характеристики (данные) и набор операций (функции).

Например, из базового типа «Предприятие торговли » могут быть порождены типы: «гастроном», «ларек», «оптовая база» и пр. Все эти типы будут содержать характеристики типа «Предприятие торговли» и иметь свои, отличные от других, характеристики.

  • Полиморфизм  заключается в обозначении одного общего действия одним общим именем (функцией), которое используется во всей иерархии типов. Причем каждый тип в этой иерархии реализует это действие своим собственным, пригодным для него способом. Например, все типы иерархии «предприятие торговли » содержат операцию «продажа», однако в ларьке и гастрономе продают за наличные, на оптовой базе – путем безналичных рассветов, которые сами по себе осуществляются по-разному.

Это действие во всех типах (базовом и производных) будет иметь имя, допустим, sale( ), но по существу выполнять различные действия. Впоследствии в программе конкретные действия по функции sale( ) будут зависеть от того, для какого объекта функция sale( ) была вызвана.

Новизна ООП в том, что оно заставляет отбросить стандартные способы мышления и привычки, которые в течение многих лет использовались в традиционном программировании.

Поиск общих свойств типов – задача далеко не тривиальная. От того, как спроектирована система, зависит, насколько эффективно можно будет использовать общность свойств.

При структурированном программировании каждый новый проект трактуется, как новая задача и разработка начинается с чистого листа. При ООП анализ проекта начинается с попытки трактовать новый проект как расширение. То есть предполагается, что новый проект-это усовершенствование чего-либо уже разработанного.

Поэтому  ставится проблема: какие новые подклассы надо создать и какие атрибуты можно унаследовать.

2. Обзор новых возможностей С++ 2.1.Комментарии

Одна из распространенных ошибок в С – незакрытые комментарии типа   /*  Поэтому в С++ введен новый тип комментариев //. Этот символ указывает компилятору, что вся информация на данной строке после // должна рассматриваться как комментарий. Комментарии типа /* также в языке сохранены.

Возможные неприятности: если комментарий используется в середине строки в качестве разделителя, то может, например, иметь место ситуация

a=b//* Операция деления */ c;

a++;

Результат: a=ba++; так как знак операции деления и первый символ ограничителя комментария проинтерпретируются как символ  //  и весь остаток строки – как комментарий.

2.2. Константы

Представление и тип числовых констант в С++ такие же, как и в С.

Плавающие числовые константы  в С++ всегда имеют тип double, а тип целых числовых констант зависит от размера константы.

Для изменения типа целой константы можно в С использовать  модификатор I или L. Например,  45L – константа имеющая тип signed long.

В С++ введено два новых модификатора : u или U для целых констант и f или F для нецелых:

35ul- имеет тип unsigned long;

4.9f- float;

5.98- double

Модификатор const был введен еще в С.

В С++ этот модификатор рекомендован для использования вместо директив препроцессора #define при объявлении символических констант.

#define nn  100  рекомендуется заменить на const int nn=100

Такая переменная (nn) не может быть изменена и может быть использована везде, где требуется литерал (строка символов), например, при объявлении массива

double mass[nn];

Важно отметить, что символическая константа, объявленная через ключевое слово const, подчиняется тем же правилам видимости, что и обычные переменные (т.е. может быть определена локальная символическая константа)

С помощью  ключевого слова const можно объявить указатель на константу, чего также нельзя сделать через директиву  #define .

const int *аа; аа- указатель на константу типа int;

Это нельзя путать с объявлением вида:

int * const aa; аа – константный указатель  на величину типа int, вообще говоря, переменную.

Кроме того, можно определить константный указатель на константную величину

const double *  const dd;

Примеры:

const double pi=3.1416;

const double e=3.72;

const double *pconst=π

double mi;

double *const con=&mi;

const double *const cpt=π

*pconst=6.7; – нельзя изменять константу

pconst=&e;   – верно: указателю присваивается адрес другой константы

con=&e;     – неверно: нельзя изменить константный указатель

*con+=4;    – верно: значение переменной, на которую указывает константный  указатель, можно изменить

cpt=&e;     – оба последних оператора неверны, т.к. нельзя изменить ни сам

*cpt= 7.4;    константный указатель, ни значение константы, на которую он указывает.

2.3. Встраиваемые функции

Вспомним, как мы ранее использовали директиву препроцессора для построения макроопределений. Макроопределения обеспечивают удобную запись вызова функции для часто используемых небольших конструкций без уменьшения скорости выполнения  программы, связанного с накладными расходами на действительный вызов функции. Однако работать с макроопределениями не так то просто. Рассмотрим пример:

#define SUMMA(a,b) a+b

double result, X1=5.2, X2=0.8;

result=SUMMA(X1,X2)*10;

после работы препроцессора получим подстановку вида:

result= X1+X2*10;

Поэтому директива препроцессора должна быть заменена на

#define SUMMA(a,b) ((a)+(b))

Чтобы избежать подобных ошибок можно было бы просто определить настоящую функцию SUMMA(a,b), но это увеличило бы время выполнения программы при частом использовании этой функции и удлинило бы объектный код, так как команды вызова функции значительно длиннее самого тела функции.

Другим недостатком макроопределений является то, что они не являются функциями и поэтому в них нельзя определить локальные переменные. По этой же причине при обработке макровызовов не осуществляется проверка и преобразование типов аргументов.

В С++ для определения функции, которая должна встраиваться как макрорасширение (встраиваемой функции) используется ключевое слово inline

Наш пример: inline double SUMMA(double a, double b) {return (a+b);}

Правила использования встраиваемых функций:

  • определение и объявление функции inline должны быть совмещены и располагаться перед первым вызовом этой функции (т.е. если появится объявление функции с ключевым словом inline и без тела функции, компилятор выдаст соответствующее сообщение об ошибке.)
  • имеет смысл определять с ключевым словом inline только очень маленькие функции, так как большие функции значительно удлиняют объектный код и замедляют компиляцию (хотя при этом увеличивают скорость выполнения)
  • следует помнить, что ключевое слово inline является лишь рекомендацией компилятору о том, что данную функцию следует сделать встраиваемой. Компилятор сам решает, будет ли функция встраиваемой или нет. При этом он руководствуется, во-первых, размером функции (некоторые компиляторы позволяют делать встраиваемые функции до 1000 строк и более). Во-вторых, если функция содержит рекурсивный вызов, то встраиваемым может быть только первый вызов. В третьих, в некоторых реализациях в теле встраиваемой функции нельзя использовать операторы цикла for, while, do, break, continue. Соответствующее предупреждение, что функция будет обычной, а не встраиваемой, выдается не во всех реализациях, поэтому программисту во многих случаях остается только догадываться о том, что объявленная им функция действительно работает как встраиваемая.

2.4. Объявления структур, объединений и перечислений

В С++ имена типов структур, объединений и перечислений рассматриваются как полноценные типы, определенные пользователем. Таким образом, при объявлении переменных данного типа не требуется указывать ключевые слова struct , union, enum

Отступление. Что такое enum? Это перечисления.

Ключевое слово enum позволяет создавать новый тип и определять значения, которые он может иметь.

enum spectrum (red,orange,yellow,green,blue,violet);

enum spectrume color;

Первый оператор объявляет новый тип: spectrum . Он перечисляет также возможные значения переменных типа spectrum: red, orange и т. д. Они являются константами типа spectrum так же, как 4 является константой типа int, а ‘g’- константой типа char.

Второй оператор объявляет color переменной типа spectrum. Вы можете присвоить переменной  color любую константу типа spectrum:

color=green;

Как компьютер запоминает что-либо подобное red? Он может рассматривать это как символьную строку, но у нее нет кавычек. И действительно такие константы запоминаются как целые числа.

Пример

printf(«red=%d, orange=%d\n»,red,orange);

результат: red=0, orange=1

по существу константа red и ее «сестры» действуют как синонимы целых чисел от 0 до 5.

Результат подобен использованию

#define red 0

за исключением того, что соответствие, установленное с помощью оператора enum, более ограничено. Например, если ind имеет тип int, то недопустимы следующие действия

ind=green; – несоответствие типа

color=3; – несоответствие типа

Таким образом, константам, появляющимся в описании enum, присваиваются  целые числа 0,1,2 и т.д. в порядке их расположения по умолчанию.

Можно выбирать значения, которые Вы хотите присвоить константам, но они должны быть целого типа (включая char)

enum levels {low=100, medium=500, high=2000};

Если вы присваиваете какое-либо значение одной константе и не присваиваете ничего следующим за ней, то им будут присвоены последовательные значения, идущие за явно присвоенным значением.

enum spectrum {red=30,orange,yellow};

В результате orange  присвоится 31, yellow-32.

Что можно и чего нельзя делать с величинами типа enum?

Можно присвоить константу типа enum переменной того же типа.

enum spectrum aaa;

aaa=red;

Нельзя использовать другие операции присваивания

aaa+=red – недопустимо

Можно провести сравнение с целью выявления равенства или неравенства

if (aaa==red)

if (aaa!=orange)

Нельзя использовать другие операции и отношения

if (red> yellow)- недопустимо

Можно применять к константам типа enum арифметические операции.

color=red+blue; color= red*yellow; и пр.

Имеют ли такие выражения какой-то смысл-это другой вопрос.

Нельзя использовать арифметические операции для переменных типа enum (включая операции увеличения и уменьшения):

color=color+red; color++;  -  недопустимо

Нельзя использовать константу типа enum для индекса массива:

ddd[red]=4;-  недопустимо

Символической константе, определенной через перечисление не отводится память во время исполнения программы, поэтому к ней нельзя применять операцию взятия адреса -&.

Применение типа enum повышает удобочитаемость программ. Например, если вы имеете дело с некоторым видом цветовых кодов, то можно использовать напрямую названия red, orange и т. д., а не числа, которые им соответствуют.

Итак, в  С++ имена типов структур, объединений и перечислений рассматриваются как полноценные типы, определенные пользователем. Таким образом, при объявлении переменных данного типа не требуется указывать ключевые слова struct , union, enum

Рассмотрим в качестве примера фрагмент программы на С

enum day {sun,mon,tue,wen,…}

struct path {

char str[40];

enum day week;

};

struct path list;

на языке С++ примет вид

enum day {sun,mon,tue,wen,…}

struct path {

char str[40];

day week;

};

path list;

Исключение составляет случай, когда имена типов структур, перечислений, объединений совпадают с именами переменных. При объявлении переменных этих типов ключевые слова  struct , union, enum опускать нельзя.

Рассмотрим фрагмент программы

struct str {

char s[5];

int n;

};

main(){

char * str;

struct str Sring;

}

Во внутреннем блоке локальная переменная str перекрыла имя типа структуры str, объявленной на глобальном уровне. В этом случае ключевое слово struct в нужном месте указывает компилятору, что str- имя типа.

В С++ введена возможность использовать неименованные перечисления  и объединения. Неименованные объединения удобно использовать для определения переменных частей структуры.

В С мы бы имели

struct anketa {

char * name;

char * adress;

int switch;

union {

char * wife;

int  age;

} fam;

} men1;

доступ к полю wife осуществлялся бы так: men1.fam.wife

В С++ мы имеем

struct anketa {

char * name;

char * adress;

int switch;

union {

char * wife;

int  age;

};

} men1;

доступ к полю wife осуществлялся бы так: men1.wife , что короче.

В отличие от С в С++ элементом структуры может быть статическая переменная.  В этом случае для всех объектов такого типа будет храниться одна копия этого элемента.

struct sklad{

char type;

double netto;

static int count;

}

sklad sk1;  sklad sk2;

Для каждой переменной типа sklad будет отводиться память достаточная для хранения значений типа char и double. И для всех объектов типа  sklad будет создан один элемент count типа int. Причем этот элемент будет создан вне зависимости от того, объявляется ли хоть один объект типа sklad. В нем может храниться информация доступная для всех объектов.

Доступ к статическому элементу структуры можно организовать двумя способами:

  • уточняя элемент именем объекта sk1.count;
  • уточняя объект именем структуры с помощью операции области видимости,

например, sklad :: count.

2.5. Операция расширения области видимости :

Она позволяет осуществлять доступ к переменным, которые в данной области видимости замаскированы (невидимы)

#include <stdio.h>

float r=2.6;

int inc(int k)

{ int r=k+15;

printf(«В функции inc: значение r внутри нее =%d»,r);

printf(«В функции inc: значение r вне ее =%f»,::r);

return r;

}

main() { int r=inc(20);

printf(«В функции main: значение r внутри нее =%d»,r);

printf(«В функции main: значение r вне нее =%f»,::r);

}

Результат

В функции inc: значение r внутри нее =35

В функции inc: значение r вне нее =2.6

В функции main: значение r внутри нее =35

В функции main: значение r вне нее =2.

Фактически, операция :: позволяет обратиться к глобальной переменной, если видима локальная  переменная с тем же именем.

Следует отметить, что эта операция нарушает модульность кода, поэтому ее использование является плохой практикой программирования и редко может быть оправдано. Однако она необходима для организации классов.

Вернемся опять к нашему статическому элементу для объектов типа sklad.

Второй вариант доступа к статическому элементу (через sklad::count) возможен потому, что элемент count- единственный для всех объектов типа sklad.

Если в структуре объявлен статический элемент, он обязательно должен быть проинициализирован. Для явной инициализации статического элемента структуры на глобальном уровне вне описания структуры должно появиться сообщение типа

int sklad :: count=5;

Если программа, использующая объекты структурного типа со статическими элементами, разбита на несколько отдельно компилируемых файлов, инициализация статических элементов должна быть выполнена только в одном из файлов.

2.6. Объявление переменных

В С требовалось,  чтобы все объявления переменных были проведены в начале блока до первого исполняемого оператора. В С++ на объявление переменных накладывается лишь одно требование: переменная должна быть объявлена к моменту ее использования, т.е. не обязательно в начале блока. Считается, что это удобнее при программировании, так как более естественно думать о переменной в момент, когда она понадобится, а не пытаться предвидеть все необходимые переменные в начале блока.

for(int i=0;i<max;i++) s[i]=’\0′;

2.7. Ссылки

Операция & означает ссылку, если она используется в следующем контексте:

тип & идентификатор1= идентификатор2;

Такое объявление фактически назначает переменной с именем идентификатор1 второе имя идентификатор2.

Ссылка существенно отличается от указателя.

Пример

int y=16;

int &x=y;

printf(«x=%d\n»,x);

y=12;

printf(«x=%d\n»,x)

Результат

x=16

x=12

Ссылочная переменная x определяет местоположение переменной y в памяти в инициализирующем выражении int &x=y. Переменная x обрабатывается как «нормальная» переменная типа int. При обращении к такой переменной нет необходимости в операции снятия ссылки.   Когда переменной y присваивается значение 12, то переменная х принимает это новое значение, поскольку она определяет местоположение переменной y в памяти.

Функциональный эквивалент приведенного выше примера выглядит следующим образом:

int y=16;

int *x=&y;

printf(«x=%d\n»,x);

y=12;

printf(«x=%d\n»,x)

Вопрос. Что, если в первый из приведенных примеров добавить строку x=10? Что произойдет со значением переменной y?

Значение переменной y при этом станет равным 10. Поскольку x-это ссылка на переменную y , то любое изменение x приводит к изменению y. Тот же результат даст присвоение значения выражению *x во втором примере.

Ссылочная переменная должна быть при объявлении инициализирована. Объявление типа int &x; недопустимо, поскольку неизвестно на что указывает переменная х.

Простой пример инициализации

inline void P(float b) {printf(«ref=%f\n»,b);}

main()

{ float data[5]={0, 1, 2, 3 ,4};

float &ref=data[3];

P(ref);

for(int i=0;i<5; i++) data[i]=(float)-i;

P(ref);

ref=10;

P(ref);

printf(«data[3]=%f\n»,data[3]);

// Восстановим массив

for(i=0;i<5; i++) data[i]=(float)i;

float r=data[3];

for(i=0;i<5; i++) data[i]=(float)-i;

printf(«r=%f\n»,r);

Результат:

ref=3.0

ref=-3.0

ref=10.0

data[3]=10.0

r=3.0 – это поскольку r-обычная переменная, а не ссылочная.

Если одновременно объявляется несколько переменных типа ссылка, то перед каждым идентификатором должен стоять знак адреса.

float &fl1=f1, &fl2=f2;

Подчеркнем, что ссылка не создает копии объекта, а является другим именем объекта . Поэтому в обычных условиях при

int a;

int & alt=a;

условное выражение &alt==&a всегда будет истинным.

Однако есть два исключения, когда при объявлении ссылки создается копия объекта во временной памяти.

  • Объявление ссылки на константу. В этом случае генерируется внутренняя рабочая переменная, ей присваивается данная константа, а затем ссылка инициализируется этой переменной, то есть по объявлению

char &rch =’\0′

компилятор построит :

char temp=’\0′; char &rch=temp;

Такое решение можно объяснить соображениями безопасности.

Дело в том, что многие компиляторы проводят оптимизацию констант и размещают одинаковые константы в одной области памяти. Поэтому, если бы ссылка на константу rch в нашем примере осуществлялась без использования временной памяти (внутренней рабочей переменной) то оператор rch=’\n’ изменил бы константу ‘\0′, т.е. записал бы на ее место ‘\n’ и дальнейшее использование ‘\0′ в программе было бы невозможным.

  • При инициализации ссылки переменной другого типа.

Например, последовательность операторов

float ui=20;

int &rui=ui;

будет интерпретироваться как

int temp=(int)ui;

int &rui=temp;

В случае, когда при объявлении ссылки создается копия объекта во временной памяти, компилятор, как правило, выдает соответствующее предупреждение.

Основной причиной введения в С++ нового типа данных – ссылки явилась необходимость передачи параметров в функции через ссылку и получение возвращаемого значения в виде ссылки при реализации перегружаемых операций. Но об этом речь пойдет несколько позже.

2.8. Использование void

В С ключевое слово void использовалось для указания того, что функция не возвращает значений и не принимает аргументов.

void fff(void);

В более поздних версиях языка введены еще два способа использования этого ключевого слова.

  • Ключевое слово void может быть использовано в операциях приведения типа для указания компилятору, что значение вычисляемого выражения игнорируется.

#include<stdio.h>

int ff(int i) { printf(» В ff имеем i=%d\n», i);

return(i);}

main(){ int k=5;   (void)ff(k); }

В этом случае мы, в сущности, изменили там где нужно тип функции с возвращающей на не возвращающую.

Наиболее интересно и практически полезно использование void для объявления указателя на неопределенный тип.

Объявление вида

void *dd

объявляет переменную dd указателем на неопределенный тип.

Такому указателю может быть присвоен указатель на любой тип, но не наоборот

void *dd;

int i; double cos;

int *intp=&i; double *floatp=&cos;

dd=intp;   – верно

floatp=dd; – неверно

Для того чтобы присвоить указатель на void какому-либо другому указателю, должна быть выполнена операция явного приведения типа:

floatp=(double *)dd;

Над указателем типа void* нельзя выполнить операцию разадресации без явного приведения типа. Это вызвано тем, что компилятор не знает, как интерпретировать значение, расположенное по указанному адресу:

cos+=*(int *)dd;

Перед использованием указателя на void в адресной арифметике он также должен быть приведен к нужному типу операций приведения типа.

#include<stdio.h>

main(){ void * dd;

float cos[3]={0.15,0.5,1.0}, rez;

dd=&cos;

printf(«cos=%f\n», *(float *) dd);

(float *)dd+=1;

((float *)dd)++;

(float *)dd++; – ошибка: размер выражения

неизвестен или нуль.

rez=*(float *)dd;

printf(«rez=%f\n», rez);

}

Результат:

cos=0.15

rez=1

Примечание. Выражение  (float *)dd++; вызывает сообщение компилятора об ошибке, так как операции приведения типа и увеличения являются унарными операциями и вычисляются справа налево. Поэтому при попытке увеличить указатель на void (еще не приведенный) будет выдано сообщение об ошибке.

Ключевое применение указателей на неопределенный тип заключается в использовании   таких указателей при описании формальных параметров в функции, например,

void mmm(void *s1, const void * s2, int n);

Эта функция копирует  n байтов из области, на которую указывает s2, в область, на которую указывает s1. Действительные аргументы могут быть указателями на любой тип.

В дополнение к правилам приведения типов в языке С (обычным арифметическим преобразованиям) в С++ добавлены следующие неявные  преобразования указателей:

  • константа 0 преобразуется к нулевому указателю.

При этом гарантируется, что он совместим с указателем на любой тип и уникален (т.е. никаких объектов, расположенных по нулевому адресу, быть не может).

Пусть, например

int *ip;

Тогда ip!=0 эквивалентно выражению ip!=NULL в Си программе

  • указатель на любой тип преобразуется к указателю на тип  void.