2.9. Преобразование типов

Раньше мы использовали для преобразования типов выражения типа

rez=(double) i;

В С++ можно использовать эквивалентное выражение

rez=double(i);

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

(double *) p; допускается

double *(p);  не допускается

2.10. Новые операции

Одну из них- :: мы уже немного изучили.

Рассмотрим следующие

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

- new-     для распределения памяти

- delete – для освобождения памяти

Используя new, можно распределить память для одного объекта или массива объектов любого типа, в том числе и типа, определенного пользователем. Результатом выполнения операции будет указатель на отведенную память или нулевой указатель в случае ошибки

Распределение одного элемента типа int

int *ip= new int;

Распределение массива из 20 элементов типа str

struct str {char name[40];

int m;

};

str * pp= new str[20];

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

Она имеет формат

delete указатель

С помощью delete может быть освобождена только память, ранее распределенная операцией new. «указатель » – это указатель на освобождаемую память.

int * fi=new int[5];

int mas[5];

delete fi;

delete mas;- ошибка: mas  не распределялась операцией new.

С помощью операции new можно определить ссылку на динамическую память. Для этого можно использовать конструкцию вида:

тип & идентификатор = * new тип;

Знак операции * перед new необходим, поскольку результатом операции new является указатель, а ссылка должна инициализироваться переменной

double & num=* new double;

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

delete & num;

Операция & нужна, т.к. операндом операции delete должен быть указатель.

2.11. Объявление функций

В языке С++ все функции перед использованием должны быть объявлены с полным списком типов формальных параметров и с указанием типа возвращаемого значения. Это требование введено потому, что компилятор С++ при вызове функции производит контроль на соответствие типов фактических и формальных аргументов, а также преобразование аргументов к требуемому типу, если это возможно. Например char ff(long, double, char *); int a1; ff(a1,a1,a1); В этом случае первый аргумент а1 автоматически преобразуется к типу long, второй а1 – к типу double. Однако при попытке преобразовать а1 к типу char будет сообщение об ошибке. В С++ объявление типа float g( ) означает, что функция g не принимает никаких параметров. Можно сделать и объявление типа float g(void) эквивалентное первому. В С++ объявления функций имеют ту же область видимости, что и переменные. Это означает, что если функция объявлена в блоке, то это объявление локально в данном блоке. При попытке вызвать эту функцию вне блока, компилятор выдаст сообщение, что для функции нет прототипа. #include<stdio.h> int  i=56; main() { printf(«i=%d\n»,i); { char fun(int); char c; c=fun(i); } void fun(i);- ошибка – в этом блоке нет прототипа для функции fun(); } char fun(int a) { char rez=(char)a; printf(«fun=%c\n»,rez); return rez; }

2.12. Передача аргументов функции по ссылке

Ссылки часто используются как формальные параметры в функциях.

Это может с пользой для дела быть использовано в двух случаях:

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

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

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

#include<stdio.h>

void in1(int &x) {x+=1;}

void in2(int *x) {*x=*x-1;}

main() {int a=3,b=5;in1(a); in2(&b);

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

}

Результат a=4, b=6

Первая из функций использует ссылочный параметр х. Для формального параметра int &x  инициализирующего выражения не требуется. Ранее подчеркивалось, что с правой стороны от объявления ссылочной переменной должно присутствовать инициализирующее выражение. Последнее для int &x подставляется при вызове функции in1. В частности, при вычислении выражения in1(a) инициализирующее выражение принимает вид int &x=a.

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

Тонкости. Рассмотрим пример.

#include<stdio.h>

void chv(int &p)

{ p+=20;

printf(«p внутри chv=%d\n»,p);

}

main() { int x=50;

chv(x); // p указывает на переменную x

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

int y=100;

chv(x+y);    // p указывает на копию выражения x+y

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

float r=-1.5;

chv(r);

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

}

Результат

p внутри chv=70

x=70

p внутри chv=190

x=70 y=100

p внутри chv=19

r=-1.5

Первый вызов функции не несет сложностей: значение х изменилось с 50 на 70.

При следующем вызове функции chv передается выражение x+y. Внутри функции создается локальная копия этого выражения, значение p изменяется со 170 на 190, x  и y  при этом не меняются.

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

Компилятор не может связать ссылку на тип int с переменной типа float. Вместо этого создается временная переменная, которая связывается с p. Таким образом, значение r, переданное в нашу функцию, останется незатронутым.

Этот код ведет себя следующим образом

float r=-1.5;

int temp=r;

chv(temp);

Недостатки:

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

void sw(int & a, int & b)

{ int t=a; a=b; b=t;}

main() { void sw(int &, int &);

int m1=5;

unsigned int m2=500;

sw(m1,m2);

}

При вызове sw для переменной m2  будет произведено преобразование следующим образом:

int temp=(int)m2;

int & b=temp;

В результате выполнения  sw поменяются местами значения m1 и временной переменной temp, т.е. m1=500, temp=5. После выхода из функции переменная temp удалится (она временная), но m2 не изменится.

Такие ошибки очень трудно вылавливаются, хотя некоторые компиляторы и сообщают, что создается временная переменная.

В С++ функции могут не только принимать, но и  возвращать ссылку на переменную.

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

char & bb(char *);

char *ma= «Я Вася»;

main()

{ printf(«%s\n»,ma);

bb(ma)=’\t’;

printf(«%s\n»,ma);

}

char & bb(char *z)

{for(int i=0; z[i] !=’ ‘; i++0);

return(z[i]);

}

Результат

Я Вася

Я       Вася

Дело в том, что функция bb возвращает ссылку на пробельный символ строки «Я Вася», после чего этой ссылке присваивается знак табуляции. В итоге на экране видим сдвиг в строке.

Следует помнить, что

  • если возвращаемое значение – указатель, нельзя в операторе

return функции указывать адрес локальной переменной;

  • если  возвращаемое значение – ссылка, нельзя оператором

return возвращать локальную переменную.

Пример:

int & fu()

{int kk;

….

return kk;

}

После выхода из fu переменная kk не существует и получаем повисшую ссылку, т.е. возвращаемое значение будет ссылаться на область памяти, которая ничего  интересного для нас не содержит.

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

2.13. Передача аргументов по умолчанию

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

Значения по умолчанию указываются при объявлении функции в списке параметров следующим образом:

void ff(double m, char ch=’*',int i=2);

Важно помнить, что параметры, которым назначаются значения по умолчанию, должны быть последними в списке аргументов. Это связано с тем, что разрешение аргументов при вызове функций выполняется позиционно. Для функции ff, будут допустимы следующие вызовы:

ff(2.5,’\',10),

ff(2.5,’\'),

ff(2.5,)

Вызов вида ff(2.5,20) – ошибочен, ff(2.5,’*',20) – верный.

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

  • значение по умолчанию должно быть определено лишь в одном месте: либо в объявлении функции, либо в определении
  • к моменту вызова функции с неполным списком аргументов значение по умолчанию уже должно быть определено и видимо в области видимости функции. Например,  будут работать оба варианта следующего примера:
    • (значение по умолчанию в объявлении функции)

int f(char,double=3.14);

void f1();

main()

{ f(‘*’); f1();

}

void f1()

{…

f(‘\’);

}

int f(char c, double pi) {…..}

  • (значение по умолчанию в определении функции)

int f(char c,double pi=3.14) {…….}

int f(char, double);

void f1();

main()

{ f(‘*’); f1();

}

void f1()

{…

f(‘\’);

}

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

void save(char *, char * = «привет»)

между символами  * и = обязательно  нужно вставить пробел, иначе пара символов  *= проинтерпретируется как составной знак операции, и будет выдано сообщение об ошибке. То же самое касается объявления ссылки в качестве параметра со значением по умолчанию.

2.14. Перегрузка функций

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

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

void ss(int &,int&);

void ss(double &,double &);

void ss(char &,char &);

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

Определим в программе все три функции как

void ss(int & a, int b)

{int temp=a; a=b; b=temp; }

void ss(double & a, double b)

{double temp=a; a=b; b=temp; }

void ss(char & a, char b)

{char temp=a; a=b; b=temp; }

Теперь для пользователя достаточно знать, что есть функция ss, которая может менять местами два данных либо целых, либо символьных, либо double. Компилятор при вызове функции по типу и количеству фактических аргументов, определит, вызов какой функции нужно осуществлять.

Замечание: в ранних версиях С++ имена всех функций, которые должны были перегружаться, предварительно должны были быть перечислены с ключевым словом  overload

3. С++ как Си с классами

Мы уже неплохо знакомы со структурами и знаем, что данные структурного типа в Си можно использовать куда более ограниченно, чем данные встроенных типов (int, float ..). Структурные данные можно передавать как параметры функций из функции как возвращаемые значения. Используя операции выбора элемента  и -> можно обрабатывать  отдельные элементы. Однако в Си нельзя использовать структурное данное как операнд в различных операциях.  Для манипуляции с подобными данными надо писать набор функций, выполняющих различные действия, и вместо операций вызывать эти функции. При этом в языке нет естественных средств для связи этих функций с обрабатываемыми ими данными. Вторым недостатком структур языка Си является то, что элементы структуры никак не защищены от случайной модификации, то есть  любая функция, даже не из набора средств манипуляции подобными данными, может обратиться к любому элементу структурного типа. Это противоречит основному принципу создания абстрактных типов данных: никакие другие функции, кроме специальных функций манипуляции этим типом данных, не   должны иметь доступа к элементам данного.

В С++ для реализации механизма создания абстрактных типов данных введено новое понятие- класс. Это понятие было настолько фундаментальным и революционным, что первое название языка даже звучало как «Си с классами».

Класс в С++- это тип структуры, расширенный таким образом, что позволяет включать  в описание типа не только элементы, содержащие значения (член-данные), но и функции, оперирующие этими значениями (член – функции).

3.1. Определение класса

Для примера объявления класса рассмотрим реализацию типа String (строка символов и работа в ней) .

const int MAX= 256;

class String

{

private:                  // необязательное ключевое слово

unsigned char len;       // определение внутренних

char line[MAX];          // данных

public:                    // операции

void fill(char *);       // заполнение строки

int length()

{return len;}        // длина строки

void write()

{cout << line;}      // вывод строки

void writeln() { write();

{cout << ‘\n’;}

char & index (int i);    //получение элемента строки

// по индексу

};

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

String s1;

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

В нашем примере используются ключевые слова private и public. Кроме них может использоваться ключевое слово protected. Эти слова управляют доступом  к членам класса. Если члены класса объявлены после ключевого слова public, то они считаются общими, т.е. открытыми для доступа из любой точки программы в области видимости объекта типа String.

Ключевые слова private и protected говорят о том, что следующие за ними члены доступны только для член-функций данного класса. Подобное ограничение доступа носит название «сокрытие информации». Различие между доступом к членам, объявленным как private и как protected будет разъяснено много позже. В нашем случае эти слова эквивалентны.

Хорошим стилем считается объявление член-данных в части  private, а член-функций в части public. Это обеспечивает следующие преимущества:

- при необходимости корректировки член-данных в определении класса изменения надо проводить только в функциях, являющихся членами данного класса;

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

Если при объявлении класса ключевые слова   public,  private, protected  опущены, то подразумевается доступ private.

Член-данные len и line  в нашем примере – скрытые, т. е. они доступны только для член-функций. Все член-функции  у нас объявлены в части public и поэтому доступны из любой точки программы в области видимости объекта типа String. Доступ к членам public  осуществляется с помощью операций выбора «.» и ->. :

String s1,* ps2;

s1.fill(«Привет»);

ps2->length();

Член-функции могут быть также внешними и встраиваемыми. Если тело функции приведено внутри описания класса, такая функция будет неявно встраиваемой.

В классе String встраиваемыми член-функциями являются length(), write(), writeln(). Если определение член-функции вынесено за  пределы описания класса и этому описанию не предшествует ключевое слово inline, то функция является внешней.

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

Имя класса :: имя функции

Например, определение член-функции fill, выполняющей заполнение объекта String, будет выглядеть так:

void String :: fill(char *str)

{for(len=0;line[lean]=str[len];len++);

}

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

//Выбор символа строки по заданному индексу

inline char & String::index(int i)

{ if (i==len)

{cout << «Индекс за границей строки\n»;

return line[0];

}

else return line[i];

}

Важно отметить, что при определении член-функций класса доступ к другим членам этого же класса (как и public, так и private) осуществляется просто по имени без дополнительной2 операции уточнения. Это вызвано тем, что всем член-функциям передается неявный аргумент – указатель на объект класса, для которого данная функция  вызывается (указатель this). Все неуточненные члены в теле член-функции неявно уточняются этим указателем.

При определении класса, так же как и при определении структуры, память не выделяется. Для выделения памяти под объект класса должна быть выделена переменная  (String ss).

Неудобством при работе с объектами нашего класса String является то, что нельзя проинициализировать переменную этого класса при объявлении.

На оператор объявления

String ss= «Вася»

будет выдано сообщение об ошибке.

Заполнить переменную информацией можно с помощью функции fill:

ss.fill(«Вася + Маша»);

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

3.2. Конструкторы и деструкторы

В С++ есть специальное средство, позволяющее обеспечить возможность начальной инициализации при объявлении объекта. Этим средством являются специальные член-функции, имя которых совпадает с именем класса  и которые называются конструкторами.

Эти функции вызываются автоматически при объявлении переменной соответствующего класса.

Например, если

вместо член-функции fill класса String мы в определении класса объявим функцию

String(char *);

и определим ее следующим образом:

String :: String(char * str)

{ for(len=0; line[len]=str[len]; len++);}

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

String text1(«ИМЯ»);  String text2=»ФАМИЛИЯ»;

Здесь text1, text2- объекты типа String, инициализируемые при объявлении строками ИМЯ и ФАМИЛИЯ.

Так как член-функции представляют собой функции, но с ограниченной областью действия (только над элементами своего класса), на них распространяются все правила, определенные для функций. Они могут быть перегружаемыми, т.е. в определении класса может быть объявлено несколько функций с одним именем, но разной сигнатурой. Это относится и к конструкторам. Например, для класса String наряду с уже определенным конструктором могут быть использованы также следующие:

String::String(int i=255) {len=i; line[0]=’\0′;}

Рассмотренные выше конструкторы выполняют только одну функцию- инициализацию объявленного объекта.

Но конструкторы могут выполнять и еще одну, очень важную функцию-построение объекта в динамической памяти. Наш класс String плох тем, что для каждого объекта класса выделяется 256 байт памяти. Более рационально было бы выделить столько байтов, сколько надо для размещения строки. Перепишем определение класса String с учетом данного пожелания.

class String

{

unsigned char len;       // определение внутренних

char * line;          // данных

public:                    // операции, методы

String(int i=255);

String(char *);

String(const String &);

int length() {return len;}        // длина строки

void write()

{cout << line;}      // вывод строки

char & index (int i)    //получение элемента строки

{ if (i==len)          // по индексу

{cout << «Индекс за границей строки\n»;

return line[0];

}

else return line[i];

}

};

String :: String(char * str)

{ //подсчет длины строки

for(len=0;line[len];len++);

//получение динамической памяти для строки

line= new char [len+1];

//инициализация полученной памяти

for(int l=0;line[l]=str[l];l++);

}

String :: String(int l)

{ len =l;

line= new char [len+1];

for(int i=0;i<=len;i++) line[i]=’\0′;

}

String :: String(const String & st)

{len=st.len;

line= new char [len+1];

for(int l=0;line[l]=st.line[l];l++);

}

Имея такие конструкторы для класса String, можно использовать следующие объявления переменных типа String;

String text3,  text4(100), text5(«СТРОКА»);

String text5=text4;

При объявлении переменных типа String, определенного подобным образом, статически (т.е. во время компиляции) выделяется память под член-данные, т.е. 1 байт для len и 2 или 4 байта для указателя line в зависимости от модели памяти. Затем динамически, т.е. во время выполнения, будет автоматически вызываться соответствующий конструктор, который в динамической области распределит необходимое количество памяти и всю выделенную объекту область (как статически, так и динамически) инициализирует.

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

В нашем классе определим деструктор:

~String(){delete line;}

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

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

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

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

3. Деструктор для каждого класса может быть только один и не должен иметь аргументов.

3.3. Перегрузка операций

Итак, мы определили класс String таким образом, что имеем возможность работать с переменными типа String, которые будут построены во время объявления (т.е. для них будет отведена и проинициализирована память). Выполнять действия над объектами типа String мы можем только используя член-функции, определенные в классе. Но использование переменных типа String в арифметических и логических выражениях все еще невозможно.  Например, нельзя использовать следующие операторы:

String s1(10), s2(«Вася»);

….

if (s1= =s2)

….

Для обеспечения возможности использовать подобные конструкции в С++ имеется механизм перегрузки операций. Для перегрузки операций в определении класса должна быть объявлена функция с ключевым словом operator и следующим за ним знаком операции. Например, для перегрузки операции == в определении нашего класса следует включить объявление:

boolean  operator = = (const String &)

Определение этой функции будет выглядеть так:

boolean String :: operator = = (const String & st)

{ if (len !=st.len) return FALSE;

for (int i= 0;i<len; i++)

if (line[i] !=st.line[i]) return FALSE;

return TRUE;

}

Тип boolean  определен следующим образом:

enum boolean {FALSE,TRUE};

Подобным образом можно организовать перегрузку большинства операций С++.

Член функцию index можно заменить перегрузкой операции []:

char & operator [ ] (int);

char & String :: operator [ ] (int ind)

{ if (ind>=len)

{ cout<< «Индекс за границей строки\n»;

return line[0];

}

else  return line[i];

}

Введем еще опреацию присваивания для класса String:

String & String :: operator= (const String &);

String & String :: operator= (const String & text)

{ len = text.len;

delete line;

line= new char[len+1];

for(int i=0;line[i]=text.line[i];i++);

return * this;

}

Так как в результате операции присваивания нужно получить ссылку на объект типа String, в операторе return возвращается разадресованный оператор this.

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

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

Например, чтобы работала следующая программа

char * text =»12345″;

String str(«12345″);

if (text= =str)

….

уже определенной нами операции  = = мало.

Если мы определим также операцию :

boolean operator = =(char *); ,

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

str= =text, но не text= =str.

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

boolean operator = = (char * s, String & st);

не являющуюся член-функцией. Но проблема заключается в том, что для функции, не являющейся членом класса, нет доступа к членам private.