Вы находитесь на странице: 1из 44

Функции

Функции используются для наведения порядка в хаосе алгоритмов.


Б. Страуструп
Объявление и определение функций
Функция – это именованная последовательность описаний и операторов, выполняющая
какое-либо законченное действие. Функция может принимать параметры и возвращать
значение.
Любая программа на С++ состоит из функций, одна из которых должна иметь имя
main (с нее начинается выполнение программы).
С понятием функции в С++ связано три компоненты:
- описание/определение функции;
- прототип/объявление функции;
- вызов функции.
Любая функция должна быть объявлена и определена. Как и для других величин,
объявлений может быть несколько, а определение только одно.Функция начинает
выполняться в момент вызова.
Объявление функции (прототип, заголовок, сигнатура) задает ее имя, тип возвращаемого
значения и список передаваемых параметров. Определение функции содержит, кроме
объявления, тело функции, представляющее собой последовательность операторов и
описаний в фигурных скобках:
[класс] тип имя ([список_параметров]) [throw (исключения)]
{тело функции};
Рассмотрим состояние части определения.
 С помощью необязательного модификатора класс можно явно задать область
видимости функции, используя ключевые слова extern и static:
еxtern – глобальная видимость во всех модулях программы (по умолчанию);
static – видимость только в пределах модуля, в котором определена функция.
 Тип возвращаемого функцией значения может быть любым, кроме массива и функции
(но может быть указателем на массив или функцию). Если функция не должна
возвращать значение, указывается тип void.
 Список параметров определяет величины, которые требуется передать в функцию при
ее вызове. Элементы списка параметров разделяются запятыми. Для каждого
параметра, передаваемого в функцию, указывается его тип и имя (в объявлении имена
можно опускать).
Тип возвращаемого значения и типы параметров совместно определяют тип
функции.
 Об исключениях, обрабатываемых функцией, рассказывается в разделе «Список
исключений функции».
 Тело функции представляет собой последовательность объявлений и операторов,
описывающих определенный алгоритм. Важным оператором тела функции является
оператор возврата в точку вызова:
return выражение;
Для вызова функции в простейшем случае нужно указать ее имя, за которым в
круглых скобках через запятую перечисляются имена передаваемых аргументов. Вызов
функции может находиться в любом месте программы, где по синтаксису допустимо
выражение того типа, который возвращает функция. Если тип возвращаемого функцией
значения не void, она может входить в состав выражений или, в частном случае,
располагаться в правой части оператора присваивания.
Объявление функции (прототип), может находиться в тексте раньше ее вызова для
того, чтобы компилятор мог осуществить проверку правильности вызова. Прототип по
форме такой же, как заголовок функции; в конце его ставится «;». Параметры функции в
прототипе могут иметь имена, но компилятору они не нужны. Компилятор использует
прототип функции для сравнения типов аргументов с типами параметров. Язык С++ не
предусматривает автоматического преобразования типов при их несовпадении.
При наличии прототипа вызываемые функции не обязаны размещаться в одном
файле с вызывающей функцией.
В определении, в объявлении и при вызове одной и той же функции типы и порядок
следования параметров должны совпадать. На имена параметров ограничений по
соответствию не накладывается, поскольку функцию можно вызывать с различными
аргументами, а в прототипах имена компиляторов игнорируются (они служат только для
улучшения читаемости программы).
Пример функции, возвращающей сумму двух целых величин:
#include <iostream.h>
int sum(int a, int b);
//объявление функции (прототип) можно так int sum(int, int);
int main ( )
{
int a=2, b=3, c, d;
c=sum(a, b); // вызов функции
cin >> d;
cout << sum(c, d); // вызов функции
return 0;
}
#include “sum.cpp”; //включение файла sum.cpp с функцией sum
Описание функции sum находится в файле sum.cpp и имеет следующий
вид:
int sum(int a, int b) //определение функции
{return (a+b);}
Функция может не иметь возвращаемого значения:
Пример. Функция без возвращаемого значения.
Составить программу, которая включает функцию, обеспечивающую выдачу на экран
символа, набранного на клавиатуре. При нажатии клавиши с символом С выполнение
программы завершить.
# include<iostream.h>
void pr(char); //прототип
void main ()
{
char x;
do
{cout<<"\n введите символ";
cin>>x;
pr (x); //вызов как оператор
}
while(x!= 'C');
}
void pr (char a)
{cout<<"\n введен символ:" <<a;}
Несмотря на то что функция, объявленная с типом void, результат не возвращает,
она может содержать инструкцию return, и не одну. В этом случае инструкция return
является командой завершения функции.

void InvFunc(double z)
{
if(z==0 ){cout<<"Division by zero! "<<endl; return;}
double x;
x=1/z;
cout<<"l/z = " << x << endl;
}
У функции InvFunc () один аргумент типа double, а в качестве типа
возвращаемого результата указано void (функция результат не возвращает). В результате
выполнения функции, в зависимости от значения аргумента, возможны два сценария. При
ненулевом аргументе на экран выводится значение, обратное к аргументу. Если аргумент
у функции нулевой, выводится сообщение соответствующего содержания. Проверка
аргумента на предмет равенства нулю осуществляется в условном операторе. Блок
условного оператора, выполняющийся при нулевом аргументе, состоит из двух команд:
выводится сообщение и затем завершается выполнение функции (команда return).
Если аргумент ненулевой, объявляется переменная х типа double. В качестве
значения ей присваивается величина, обратная к аргументу функции, после чего
полученное значение выводится на экран. В обеих командах вывода в функции
использована инструкция завершения строки endl. Кроме того, в данном случае
фактически инструкция return использована как альтернатива к else-блоку условного
оператора: можно было бы ту часть кода, что выполняется при ненулевом аргументе
функции, разместить в else-блоке условного оператора, отказавшись при этом от
инструкции завершения функции return. Тем не менее, в более сложных программах
использование такой инструкции часто бывает не только оправданным, но и существенно
упрощает структуру кода.
По большому счету, описывать функцию можно где угодно - главное, чтобы ее
прототип был указан до ее первого использования.
Все величины, описанные внутри функции, а также ее параметры, являются
локальными. Областью их действия является функция. При вызове функции, как и при
входе в любой блок, в стеке выделяется память под локальные автоматические
переменные. Кроме того, в стеке сохраняется содержимое регистров процессора на
момент, предшествующий вызову функции, и адрес возврата из функции для того, чтобы
при выходе из нее можно было продолжить выполнение вызывающей функции.
При выходе из функции соответствующий участок стека освобождается, поэтому
значение локальных переменных между вызовами одной и той же функции не
сохраняются. Если этого требуется избежать, при объявлении локальных переменных
используется модификатор static.
При совместной работе функции должны обмениваться информацией. Это можно
осуществлять с помощью глобальных переменных, через параметры и через возвращаемое
функцией значение.
Функцию можно определить как встроенную с помощью модификатора inline,
который рекомендует компилятору вместо обращения к функции помещать ее код
непосредственно в каждую точку вызова. Модификатор inline ставится перед типом
функции. Он применяется для коротких функций, чтобы снизить накладные расходы на
вызов (сохранение и восстановление регистров, передача управления). Директива inline
носит рекомендательный характер и выполняется компилятором по мере возможности.
Использование inline-функций может увеличить объем исполняемой программы.
Определение функции должно предшествовать ее вызовам, иначе вместо inline-
расширения компилятор сгенерирует обычный вызов.

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

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

void f(int i, int *j, int &k);


int main () {
int i = 1, j = 2, k = 3;
cout <<"i j k\n";
f (i, &j, k);
cout <<i<<' '<<j<<' '<<k;
return 0;
}
void f (int i, int*j, int& k)
{
i++; (*j)++; k++;
}

Результат роботы программы:

I j k
1 2 3
1 3 4

Первый параметр (i) передается по значению. Его изменение в функции не влияет


на исходное значение. Второй параметр (j) передается по адресу с помощью указателя,
при єтом для передачи в функецию адреса фактического параметра используется операция
взятия адреса, а для получения его значения в функции требуется операция
разыменования. Третий параметр(k) передается по адресу с помощью ссылки.
При передаче по ссылке в функцию передается адрес указанного при вызове
параметра, а внутри функции все обращения к параметру неявно разыменовываются.
Поэтому использование ссылок вместо указателей улучшает читаемость программы,
избавляя от необходимости применять операции получения адреса и разыменования.
Использование ссылок вместо передачи по значению более эффективно, поскольку не
требует копирование параметров, что имеет значение при передаче структур данных
большого объема.
Если требуется запретить изменение параметра внутри функции, используется
модификатор const:
int f (const char*);
char* t (char*a, const int *b);

Совет
Рекомендуется указывать const перед всеми параметрами, изменение которых в
функции не предусмотрено. Это облегчает отладку больших программ, так как по
заголовку функции можно сделать вывод о том, какие величины в ней изменяются, а
какие нет. Кроме того, на место параметра типа const& может передаваться константа, а
для переменной при необходимости выполняются преобразования типа.
Таким образом, исходные данные, которые не должны изменяться в функции,
предпочтительнее передавать ей с помощью константных ссылок.
По умолчанию параметры любого типа, кроме массива и функции (например,
вещественного, структурного, перечисление, объединение, указатель), передаются в
функцию по значению.
Массивы как параметры функций

Одномерные массивы

Массивы могут быть параметрами функций и функции в качестве результата могут


возвращать указатель на массив. При использовании массивов в качестве параметров
функции возникает необходимость определения в теле функции количества элементов
массива, который является аргументом при обращении к функции.
При работе со строками, т. е. с массивами типа char[], проблема решается просто,
поскольку последний элемент строки имеет значение '\0'. Поэтому при обработке массива-
аргумента каждый его элемент анализируется на наличие символа конца строки.

Пример 1. Строки как параметры функций.


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

#include <iostream.h>
#include <conio.h>
int dl(char[]); // прототип функции dl
void main ()
{
clrscr();
char c[]="kafedra ASOI";
cout << "\n kol-vo simvolov:" << dl(c);
getch();
}

int dl(char c[])


{
int i;
for (i=0; ;i++)
if (c[i]=='\0') break;
return i;
}
В результате выполнения программы на экран будет выдано сообщение:
Длина строки равна: 12.

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


использовать массивы с заранее известным числом элементов, либо передавать размер
массива с помощью дополнительного параметра.

Пример 2. Одномерный массив в качестве параметра функции.


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

//Одномерный массив в качестве параметра


#include <iostream.h>
float sum (float x[5])
{
float s=0;
for (int i=0; i<5; i++)
s=s+x[i];
return s;
}

void main()
{
float z[5],y;
for (int i=0; i<5; i++)
{
cout <<"\nВведите очередной элемент массива:";
cin >> z[i];
}
y=sum(z) ;
cout <<"\n Сумма элементов массива:"<< y;
}

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


элементов массива z.
В данном примере отсутствует прототип функции sum(), поскольку описание функции
sum() компилятор анализирует раньше, чем обращение к ней. В приведенном примере
заранее известно число элементов — 5, поэтому в функции всего один параметр — массив
х. Поскольку в функции существует возвращаемое значение, то вызов функции может
быть только выражением или частью выражения. В программе оператор присваивания
y=sum(z); содержит такое выражение в правой части. Здесь аргументом функции является
массив z.

Пример 3. Одномерный массив с произвольным числом элементов в качестве


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

//Поиск максимального элемента


#include <iostream.h>

float max(int n, float a[])


{
float m=a[0];
for (int i=1;i<n;i++)
if (m<a[i]) m=a[i];
return m;
}

void main()
{
float z[6];
for (int i=0;i<6;i++)
{
cout <<"\nВведите очередной элемент массива:";
cin >> z[i];
}
cout <<"\nМаксимальный элемент массива:"<< max(6,z);
}
В результате выполнения программы на экран монитора будет выдано сообщение со
значением максимального элемента массива z, который в данном примере состоит из
шести элементов.
В приведенной программе прототип отсутствует, поскольку описание функции следует
раньше ее вызова. В функции тах() используется два параметра: первый указывает число
элементов в массиве, а второй — имя и размерность массива. Аргументы указаны при
вызове функции max(6, z). Здесь 6 — размер массива, a z - имя массива.

Имя массива представляет собой константный указатель адреса нулевого элемента


массива. Поэтому, имя массива является константным указателем адреса элемента
массива с нулевым индексом. Отсюда следует, что при использовании имени массива в
качестве параметра функции допускается изменять значения элементов массива.
Пример 4. Массив как параметр функции.
Пусть требуется составить программу, содержащую обращение к функции der( ),
которая уменьшает на 1 значения всех элементов массива-параметра. Возможный вариант
решения имеет вид:
#include <iostream.h>
void der(int, float[]); // прототип функции
void main () {
const int k=10;
float a[k];
int i;
for (i=0; i<k; i++)
{
cout << "\nВведите элемент массива ";
cin >> a[i];
}
der(k,a); // обращение к функции
for (i=0; i<k; i++)
cout << "\ta[" << i << "]=" << a[i];
}
void der (int k1, float a1[])
{
int j;
for (j=0; j<k1;j++)
a1[j]--;
}

Пример 5. Массивы с произвольным числом элементов как параметры функции.


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

//Указатели на массив в качестве параметров


#include <iostream.h>
#include <conio.h>

void maxl(int,int*,int*,int*); //прототип

void main ()
{
clrscr();
int a[]={0,1,2,3,4};
int b[]={5,6,0,7,1};
int d[5];
maxl (5,a,b,d) ;
cout << "\n";
for (int i=0;i<5;i++)
cout << "\t" << d[i] ;
getch();
}

void maxl (int n, int *x, int *y, int *z)


{
for (int i=0;i<n;i++)
z[i]=x[i]>y[i] ? x[i] :y[i];
}

В результате выполнения программы на экран монитора будут выданы элементы


результирующего массива d:
5 6 2 7 4.
В приведенной программе у функции имеется четыре параметра: число п элементов в
массивах и три указателя х, у, z. При вызове функции max1(5,a,b,d);, (с помощью
оператора, поскольку отсутствует возвращаемое значение) в качестве аргументов
используются имена исходных массивов а и b и результирующего d.

Многомерные массивы
Особенностью языка C++ является несамоопределенность массивов, т. е. по имени
массива невозможно узнать его размерность и размеры по каждому измерению. Кроме
того, в C++ многомерные массивы не определены. Например, если объявлен массив float
d[3][4][5], то это не трехмерный, а одномерный массив d, включающий три элемента,
каждый из которых имеет тип float [4][5]. В свою очередь, каждый из четырех элементов
типа float [5]. И, соответственно, каждый из этих элементов является массивом из пяти
элементов типа float. Эти особенности затрудняют использование массивов в качестве
параметров функций.
При передаче массивов в качестве параметров через заголовок функции следует
учитывать, что передавать массивы можно только с одной неопределенной границей
мерности (эта мерность должна быть самой левой).

Пример 6. Двумерный массив как параметр функции.


Пусть требуется составить программу с функцией, которая подсчитывает сумму
элементов матрицы.
#include <iostream.h>
float summa(int n,float a[][3])
{
float s=0;
for (int i=0;i<n;i++)
for (int j=0;j<3;j++)
s=s+a[i][j];
return s;
}

void main()
{
float z[4][3] = {0,1,2,3,4,5,6,7,7,6,5,4};
cout << "\n Сумма элементов матрицы равна " << summa(4,z);
}

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


Сумма элементов матрицы равна 50.
В приведенной программе параметром функции summa() является матрица a[][3], у
которой не определено число строк. Число столбцов должно быть заранее определено, оно
равно 3. Таким образом, при использовании данной функции вводится существенное
ограничение — фиксированное число столбцов. Указанное ограничение можно обойти с
помощью использования вспомогательных массивов указателей на массивы.

Пример 7. Вспомогательный массив указателей на массив как параметр функции.


Требуется составить программу с функцией, которая возвращает в качестве результата
минимальный элемент матрицы d размером mxn.

#include <iostream.h>

float min(int m,int n,float *p[]); //прототип

void main()
{
float d[3][4]={1,2,-2,4,5,0,-3,18,-9,6,7,9};
float *r[]={(float*)&d[0], (float*)&d[1], (float*)&d[2]};
int m=3;
int n=4;
cout<<"\n Минимальный элемент матрицы равен "<<min(m,n,r);
}

float min(int m,int n,float *p[])


{
float x=p[0][0];
for (int i=0;i<m;i++)
for (int j=0;j<n;j++)
if (x>p[i][j]) x=p[i][j];
return x;
}
В результате выполнения программы на экран будет выведено сообщение:
Минимальный элемент матрицы равен -9.
В функции min() в качестве параметров используются int т — число строк, int n —
число столбцов и float *p[] — массив указателей на одномерные массивы элементов типа
float, причем размеры двумерного массива заранее неизвестны.
В теле функции обращение к элементам матрицы осуществляется с помощью двойного
индексирования. Здесь p[i][j] — значение элемента двумерного массива.
В функции main() объявлена и инициализирована матрица d[3][4], имеющая
фиксированные размеры. Такой размер нельзя использовать непосредственно в качестве
аргумента функции, поэтому объявлен дополнительный вспомогательный массив
указателей float *r[]. В качестве значений элементам этого массива присваиваются адреса
первых элементов строк матрицы, т. е. &d[0], &d[1], &d[2], преобразованные к типу float
* (указатель на тип flоаt).

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

Пример 8. Использование одномерного динамического массива. Нахождение суммы


элементов массива.

#include <iostream.h>
int sum (const int *mas, const int n); //прототип
int const n=10;
int main()
{
int marks[n] = {3, 4, 5, 4, 4};
cout << "Сумма элементов массива: " << sum(marks, n);
return 0;
}

int sum(const int *mas, const int n)


// варианты: int sum(int mas[], int n)
// или int sum(int mas[n], int n)
// (величина n должна быть константой)
{
int s = 0;
for (int i=0; i<n; i++)
s += mas[i];
return s;
}

Пример 9. Динамические массивы как параметры функций.


Требуется составить программу с использованием функции, которая заполняет
элементы матрицы размером тхп последовательно значениями целых чисел: 0, 1,2, 3,....
Для формирования матрицы в основной программе использованы динамические массивы,
так как размеры матрицы заранее не известны.

// Матрица как набор одномерных динамических массивов


#include <iostream.h>
void fun(int m, int n, int **uc)
{
int k=0;
for (int i=0;i<m;i++)
for (int j=0;j<n;j++)
uc[i][j]=k++;
}

void main()
{
int **pi; // указатель на массив указателей
int m1; // число строк в матрице
cout << "\n Введите число строк матрицы:";
cin >> m1;
int n1; // число столбцов в матрице
cout << "\n Введите число столбцов матрицы:";
cin >> n1;
int i,j;
// выделение вспомогательного массива указателей
pi=new int* [m1];
for (i=0;i<m1;i++)
// формирование i-й строки матрицы
pi[i]=new int [n1];
fun(m1,n1,pi); // обращение к функции
for (i=0;i<m1;i++) // цикл перебора строк
{
cout << "\n строка " << i+1 << ":";
for (j=0;j<n1;j++)
cout << "\t" << pi[i][j];
}
//
for (i=0;i<m1;i++)
delete []pi[i];
delete []pi;
}

Если при вводе задать размерность матрицы 3x4, то в результате выполнения


программы на экран будет выдано три следующие строки:
строка 1: 0 1 2 3
строка 2: 4 5 6 7
строка 3: 8 9 10 11

В функции fun() есть три следующих параметра: int т — число строк, int n — число
столбцов матрицы и int * *uc — указатель на массив указателей.
В функции main для определенности значение числа строк т1 задано 3, а число
столбцов матрицы n1 задано 4. При выполнении оператора присваивания pi=new int*[m1];
операцией new выделяется память для вспомогательного массива указателей. В результате
выполнения этой операции для массива указателей выделяется требуемое количество
памяти, а адрес первого элемента выделенной памяти присваивается указателю pi. При
выполнении в теле цикла второй операции new формируются т1 строк матрицы.
При обращении к функции происходит присваивание элементам матрицы
последовательно значений целых чисел от 0 до 11. В конце программы с помощью delete
выделенная память освобождается.

Многомерный массив с переменными размерами, который формируется в теле


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

Пример 10. Формирование многомерного динамического массива в функции.


Пусть требуется составить программу с функцией, формирующую квадратную матрицу
порядка п с единичными диагональными элементами и остальными нулевыми.
// Единичная диагональная матрица порядка n
#include <iostream.h>
#include <process.h> //для exit ()
// функция формирования матрицы
int **matr(int n)
{
int **p; //вспомогательный указатель
p=new int* [n]; //массив указателей на строки
if (p==NULL)
{
cout << "\n He создан динамический массив!!";
exit(1);
}
//цикл создания строк (внешний)
for (int i=0;i<n;i++)
{
p[i]=new int [n]; //выделение памяти для i-й строки
if (p[i]==NULL)
{
cout << "\n He создан динамический массив!";
exit(1);
}

//цикл заполнения строк (вложенный)


for (int j=0;j<n;j++)
if (j!=i) p[i][j]=0;
else p[i][j]=1;
}
return p;
}

void main()
{
int n1; //порядок матрицы
cout << "\n Введите порядок матрицы:";
cin >> n1;
int **ma; //указатель для формирования матрицы
ma=matr(n1); //обращение к функции
//печать элементов матрицы
for (int i=0;i<n1;i++)
{
cout << "\n строка " << i+1 << ":";
for (int j=0;j<n1;j++)
cout << "\t" << ma[i][j];
}
//освобождение памяти
for (i=0;i<n1;i++)
delete []ma[i]; //удаляем содержимое строк
delete []ma; //удаляем массив указателей на
строки
}
При выполнении программы на экране появится подсказка:
Введите порядок матрицы:
Если в ответ на подсказку пользователь введет с клавиатуры порядок матрицы, равный
5, то на экране дисплея он получит сообщение из пяти строк вида:
строка 1: 1 0 0 0 0
строка 2: 0 1 0 0 0
строка 3: 0 0 1 0 0
строка 4: 0 0 0 1 0
строка 5: 0 0 0 0 1
В данной программе функция matr() имеет один параметр int n — порядок матрицы. В
теле функции формируется квадратная матрица размером nxn (создаются n одномерных
массивов с элементами типа int и массив указателей на эти одномерные массивы) и ее
элементы заполняются необходимыми значениями. Возвращаемым значением функции
matr() является значение указателя на сформированную матрицу.

Пример 11. Нахождение суммы элементов двух двумерных массивов.


Внутри функции массив интерпретируется как одномерный, а его индекс
пересчитывается в программе. Размерность массива b известна на этапе компиляции, под
массив а память выделяется динамически (программа написана в стиле С).
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <malloc.h>

int sum(const int *a, const int nstr, const int nstb);

int main()
{
clrscr();
int b[2][2]= {{2, 2},{4,3}};
printf ("Cумма элементов b: %d\n", sum(&b[0][0], 2, 2));
//имя массива передавать в sum нельзя из-за несоответствия типов
int i,j, nstr, nstb, *a;
printf ("Введите количество строк и столбцов: \n");
scanf("%d%d", &nstr, &nstb);
a=(int *)malloc(nstr* nstb* sizeof(int)); //1
printf ("Введите элементы массива: \n");
for (i=0; i<nstr; i++)
for (j=0; j<nstb; j++)
scanf("%d", &a[i * nstb + j]); //2
printf("Сумма элементов a: %d\n", sum(a, nstr, nstb));
free(a);
return 0;
}
int sum(const int *a, const int nstr, const int nstb)
{
int i,j,s=0;
for (i=0; i<nstr; i++)
for (j = 0; j<nstb; j++)
s += a[I * nstb + j];
return s;
}
Память выделяется сразу под все элементы массива (оператор 1), то есть, по сути
двумерный динамический массив занимает сплошной участок памяти. При заполнении
массива используется формула для определния индекса соответствующего элемента (2). В
конце работы программы массив удаляется.
Можно решить эту задачу по-другому выделяя память (программа написана в стиле
С++).
#include <iostream.h>
int sum(int **a, const int nstr, const int nstb);
int main()
{
int nstr, nstb;
cout<<"Введите количество строк и столбцов: \n";
cin >> nstr >> nstb;
int i,j, **a;
a=new int *[nstr];
for (i=0; i<nstr; i++)
a[i]=new int [nstb];
cout<<"Введите элементы массива: \n";
for (i=0; i<nstr; i++)
for (j=0; j<nstb; j++)
cin>>a[i][j];
cout<<"Сумма элементов a:"<<sum(a, nstr, nstb);
//освобождение памяти
for (i=0;i<nstr;i++)
delete []ma[i]; //удаляем содержимое строк
delete []ma; //удаляем массив указателей на
строки
return 0;
}

int sum(int **a, const int nstr, const int nstb)


{int i,j,s=0;
for (i=0; i<nstr; i++)
for (j = 0; j<nstb; j++)
s += a[i][j];
return s;
}

В этом случае память сначала выделяется под столбец указателей на строки матрицы, а
затем в цикле под каждую строку. Освобождение памяти должно выполняться в обратном
порядке.
Параметры со значениями по умолчанию
Чтобы упростить вызов функции, в ее заголовке можно указать значения
параметров по умолчанию. Эти параметры должны быть последними в списке и могут
опускаться при вызове функции. Если при вызове параметр опущен, должны быть
опущены и все параметры, стоящие за ним. В качестве значений параметров по
умолчанию могут использоваться константы, глобальные переменные и выражения:
int f(int a, int b=0);

void f1(int, int=100, char* =0);


/* обратите внимание на пробел между * и = (без него получилась
бы операция сложного присваивания *=) */

f(100);
f(a, 1); //варианты вызова функции
f
f1(a); f1(a,10); f(a,10,"Vasia"); //варианты вызова функции f1
fl(a, ,”Vasia") // неверно!
В случае, когда прототип функции указывается до ее определени, значения по
умолчанию указываются только в прототипе.

Передача имен функций в качестве параметров


Указатель может ссылаться на функцию, так как имя функции (без круглых скобок
и аргументов) является указателем на функцию. Значение этого указателя есть адрес, по
которому записана функция. Указатель на функцию объявляется следующим образом.
тип (*имя) (список_ типов_ аргументов);
Например, объявление:
int (*fun) (double, double);
задает указатель с именем fun на функцию, возвращающую значение типа int и
имеющую два аргумента типа double. В качестве значения такому указателю
присваивается имя функции, на которую должен ссылаться указатель и функцию можно
вызвать через указатель на нее. Для этого объявляется указатель соответствующего типа и
ему с помощью операции взятия адреса присваивается адрес функции:
void f (int а){...} //определение функции
void (*pf)(int); //указатель на функцию

pf =&f; //указателю присваивается адрес функции
//(можно написать pf = f;)
pf(10): //функция f вызывается через указатель pf
//(можно написать (*pf)(10)
Тип указателя и тип функции, которая вызывается посредством этого указателя,
должны совпадать в точности.
//Функция возведения в квадрат:
double sqr(double x){return x*x;}
//Функция возведения в куб:
double cube(double x) {return x*x*x;}
//Функция со вторым аргументом-указателем на функцию:
void myfunc(double x,double (*f)(double))
{cout<<f(x) <<endl;}
int main()
{
double z;
double (*p)(double); //Указатель на функцию:
cout << ”z = ”;
cin>>z;
p=cube; //Указателю присваивается значение, можно p=&cube;
myfunc(z,sqr); //Использование имени функции
myfunc(z, p); //Использование указателя
cout << p(z) << endl;
//Адрес функции:
cout<<sqr << endl;
cout << cube << endl;
cout << p << endl;
return 0;
}
В программе объявляются три функции. У функции sqr() аргумент типа doub1е,
результатом является квадрат аргумента - число типа doub1е. Функцией cube() в
качестве значения возвращается значение типа double – куб аргумента, который также
имеет тип double. У функции myfunc() два аргумента. Первый аргумент имеет тип
double, а второй аргумент является указателем на функцию. Он объявлен как double
(*f) (double). В данном случае f - формальное название аргумента. Оператор *
означает, что это указатель. Ключевое слово double в круглых скобках - тип аргумента
функции, a double перед именем указателя - тип результата функции. Таким образом,
функции myfunc() первым аргументом передается число, а вторым – имя функции.
В результате вызова функции отображается значение f(z), то есть значение
функции, имя которой указано вторым аргументом, от аргумента – первого параметра
функции myfunc().
В главном методе программы командой double (*р) (double) объявляется
указатель на функцию. Значение указателю присваивается командой p=cube. После этого
указатель р ссылается на функцию cube(). Далее этот указатель и имена функций
используются в командах myfunc(z,sqr) (квадрат числа z), myfunc(z,p) (куб числа
z) и р(z) (куб,числа z). В конце программы разными способами отображаются адреса
функций с использованием указателя р и имен функций. Результат выполнения
программы может иметь вид:
z = 5
25
125
125
00401195
0040128F
0040128F
Для того чтобы сделать программу легко читаемой, при описании указателей на
функции используют переименование типов (typedef). Можно объявлять массивы
указателей на функции (это может быть полезно, например, при реализации меню):
//Описание типа PF как указатель на функцию
//с одним параметром типа int:
typedef void (*PF)(int);
//Описание и инициализация массива указателей:
PF menu[]={&new, &open, &save};
menu[1](10); // Вызов функции open
Здесь new, open и save — имена функций, которые должны быть объявлены ранее.

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


В языке C++ можно применять функции, у которых до выполнения программы число
параметров заранее не известно. Количество и, соответственно, типы параметров
становятся известными при вызове функции. Такие функции называются функциями с
переменным числом параметров. Формат описания функции с переменным числом
параметров следующий:
тип имя (список_явных_ параметров, ...)
{тело_функции }
Здесь: тип — тип возвращаемого значения; список_явных_параметров —
список параметров, которые известны до вызова функции.
После списка явных параметров ставится необязательная запятая и многоточие,
которое сообщает компилятору о том, что контроль типов и количества параметров при
вызове функции проводить не следует.
При программировании функций с переменным числом параметров необходимо
предусмотреть механизм определения количества параметров и их типов. При реализации
этого механизма используется два следующих подхода:
• один из параметров определяет число дополнительных параметров функции;
• в список явных параметров последним задается параметр-индикатор,
указывающий на окончание списка параметров.
Переход от одного параметра к другому в обоих случаях осуществляется с
помощью указателей.
Пример 1. Задание числа дополнительных параметров функции с помощью одного
из параметров.
Пусть требуется составить программу, содержащую функцию с переменным
числом параметров. Функция должна вычислять сумму значений дополнительных
параметров. Список явных параметров должен состоять из одного параметра,
используемого для задания числа дополнительных параметров.
/* Функции с переменным числом параметров */
#include <iostream.h>
int sum (int, ...); // прототип функции
void main () {
cout << "\n 4+6=" << sum(2,4,6);
cout << "\n 1+2+3+4+5+6=" << sum(6,1,2,3,4,5,6);
cout << "\n Параметры отсутствуют. Сумма равна:" << sum(0);
}
int sum(int n, ...) // n - число суммируемых параметров
{
int *p=&n; // выход на начало списка дополнительных
параметров
int s=0;
for (int i=l; i<=n; i++)
{
if (n==0) break;
s+=*(++p);
}
return s;
}
Результат выполнения программы:
4+6=10
1+2+3+4+5+6=21
Параметры отсутствуют. Сумма равна:0
В приведенном примере для организации анализа аргументов используется значение
первого параметра. Для доступа к нему используется указатель р. Ему присваивается
значение адреса первого аргумента, который соответствует явно заданному параметру n.
Таким образом указатель устанавливается на начало списка аргументов в памяти. Затем в
цикле указатель р перемещается по адресам следующих аргументов, соответствующим
дополнительным параметрам. С помощью операции разыменования *р выполняется
выборка и суммирование значений аргументов.
Замечание.
Особенность рассмотренного подхода заключается в том, что все параметры (явный и
дополнительные) должны иметь одинаковый тип int или double.
Пример 2. Определение конца списка дополнительных параметров функции с
помощью параметра-индикатора.
Пусть требуется составить программу, содержащую функцию с переменным числом
параметров. Функция должна вычислять произведение значений дополнительных
параметров. Для определения конца списка параметров используется параметр-индикатор.
#include <iostream.h>
double pr(double a, ...)
{
double b=1.0; // b - произведение
double *p=&a;
if(*p==0.0) return 0.0;
for(; *p; p++) b* =*p;
return b;
}
void main()
{
cout << "\n pr(6e0,5e0,3e0,0e0)=" << pr(6e0,5e0,3e0,0e0);
cout << "\n pr(l.5,2.0,0.0,3.0,0.0)=" <<
pr(1.5,2.0,0.0,3.0,0.0);
cout << "\n pr(0.0)=" << pr(0.0);
}
Результат выполнения программы:
pr(6е0,5е0,3е0,0е0)=90
pr(1.5,2.0,0.0,3.0,0.0)=3
pr(0.0)=0
В функции рr() перемещение по списку аргументов осуществляется за счет
изменения значения указателя р. При втором обращении к функции в середине списка
аргументов находится аргумент с нулевым значением. Он воспринят функцией как
индикатор, который сигнализирует об окончании списка аргументов. В приведенной
программе, так же как и в первом примере, аргументы должны иметь один тип, но не
обязательно int или double.
Для использования в программе функций с переменным числом параметров, при
вызове которых можно использовать аргументы разных типов, необходимо включать в
текст программы специальный набор макроопределений. Эти макроопределения
находятся заголовочном файле stdarg.h.

Функция main()
Функция, которой передается управление после запуска программы, должна иметь
имя main. Она может возвращать значение в вызвавшую систему и принимать параметры
из внешнего окружения. Возвращаемое значение должно быть целого типа. Стандарт
предусматривает два формата функции:
// без параметров:
тип main (){ /*-*/}
// с двумя параметрами:
тип main (int argc, char* argv[]){ /*-*/}
При запуске программы параметры разделяются пробелами. Имена параметров в
программе могут быть любыми, но принято использовать argc u argv. Первый параметр
(argc) определят количество параметров, передаваемых функции, включая имя самой
программы, второй параметр (argv) является указателем на массив указателей типа char*.
Каждый элемент массива содержит указатель на отдельный параметр командной строки,
хранящийся а виде С-строки, оканчивающейся нуль-символом. Первый элемент массива
(argv[0]) ссылается на полное имя запускаемого на выполнение файла, следующий
(argv[l]) указывает на первый параметр, argv[2] — на второй параметр, и так далее.
Параметр argv[argc] должен быть равен 0.
//программа выводит на экран все переданные в командной строке
//параметры функции
#include <iostream.h>
void main (argc, char* argv[]) {
for (int i = 0; i<argc; i++)
cout << argv[i] << '\n';
}
Первый целочисленный argc аргумент особых вопросов не вызывает. Второй
аргумент, объявленный как char* argv[ ] - массив, элементами которого являются
текстовые строки, реализованные в виде символьных массивов. индексная переменная i
пробегает значения в соответствии с размером массива (от 0 до size-1 включительно).
Для каждого значения этого индекса на экран выводится соответствующий параметр
командной строки, для чего используется ссылка argv[i].
Если функция main() ничего не возвращает, вызвавшая система получит
значение, означающее успешное завершение. Ненулевое значение означает аварийное
завершение. Оператор возврата из main() можно опускать. Если параметры в командную
строку передавать не планируется, аргументы для метода main()можно не указывать (а
можно указывать).

Возвращаемое значение
Механизм возврата из функции в вызывавшую ее функцию реализуется
оператором
Return [выражение];
Значение выражения, если оно задано, возвращается в вызывающую функцию в
качестве значения вызываемой функции. Если выражение опущено, то возвращаемое
значение не определено. Выражение может быть заключено в (). Функция может
содержать несколько операторов return (это определяется потребностями алгоритма).
Выражение, указанное после return, неявно преобразуется к типу возвращаемого
функцией значения и передается в точку вызова функции. Если в функции отсутсвует
оператор , то передача управления в вызывающую функцию происходит после последнего
оператора вызываемой функции. При этом возвращаемое значение не определено.
Примеры:
int f1() {return 1;} //правильно

Void f2() {return 1;} //неправильно f2 не должна возвращать


значение

Double f3() {return 1;} //правильно 1 преобразуется к типу


double

int max ( int a, int b ) {return ( a > b ? a : b ); }

Возвращение функцией указателя


Функция в качестве значения может возвращать указатель. Главное, что для этого
нужно сделать, - предусмотреть соответствующие инструкции в программном коде
функции. В прототипе функции, возвращающей в качестве значения указатель, перед
именем функции указывают оператор *.
//функция, возвращающая указатель
int *mpoint(int &n,int &m)
{
if(n>m) return &n; //return(n>m?n:m);
else return &m;
}
int main ()
{int n=3,m=5;
int *p;
p=mpoint(n,m);
(*p) ++;
cout << “n = " << n << endl;
cout << "m =” << m << endl;
return 0;
}

В программе объявляется функция mpoint (), возвращающая в качестве


результата указатель на максимальный из двух ее аргументов. Сразу отметим, что оба
аргумента передаются по ссылке - если бы они передавались по значению, не было бы
смысла возвращать указатель на один из аргументов, поскольку в этом случае указатель
ссылался бы на временную переменную-копию аргумента.
При возвращении в качестве значения указателя не следует забывать, что
возвращается адрес большего из аргументов, а не сам аргумент. Адрес переменной
(аргумента) получаем, указав перед именем соответствующей переменной оператор &, что
и было сделано в командах return &n и return &m.
В главном методе программы объявляются две целочисленные переменные n и m
со значениями 3 и 5 соответственно. Именно они передаются аргументами функции
mpoint(). Результат записывается в переменную-указатель р. В результате переменная
р в качестве значения получает адрес переменной m. Поэтому после выполнения команды
(*р) ++ значение переменной m увеличивается на единицу. Результатом выполнения
программы станет пара строк
n =3
m = 6
В конечном счете указатель - это всего лишь переменная, значением которой
является адрес памяти. Нет ничего удивительного в том, что адрес возвращается
функцией.
Внимание
Нельзя возвращать из функции указатель на локальную переменную, поскольку память
выделенная локальным переменным при входе в функцию, освобождается после возврата
из нее.
Пример:
int* f()
{
int a = 5;
return &a; //нельзя!
}

Возвращение функцией ссылки


В C++ функции могут возвращать в качестве результата ссылку на значение. Для
того чтобы функция возвращала ссылку на значение, перед именем функции в ее
прототипе (и при описании) необходимо использовать оператор &.
//Функция, результатом которой является ссылка
#include <iostream>
using namespace std;
int &mpoint ( int &n , int &m)
{
if(n>m) return n;
else return m;
}
int main ()
{
int n=3,m=5;
int р;
mpoint(n,m)=2 ;
p=mpoint(n, m) ;
cout << "n="<<n<<endl;
cout << "m="<<m<<endl;
cout << "p="<<p<<endl;
return 0;
}
Приведенный программный код представляет собой модификацию предыдущего
примера из, только в данном случае функцией mpoint () возвращается не указатель, а
ссылка на больший из своих аргументов. При объявлении функции перед названием
указан оператор & (признак того, что функцией возвращается ссылка). В качестве
значения, как отмечалось, возвращается больший из аргументов n и m (инструкции
return n и return m в условном операторе). Однако в силу того, что функция
объявлена как ссылка, в действительности возвращается не значение соответствующей
переменной, а ссылка на нее! Драматичность этого утверждения легко подтвердить с
помощью кода, представленного в главном методе программы.
В методе main(), как и в предыдущем примере, инициализируются две
целочисленные переменные n=3 и m=5, а также объявляется целочисленная переменная р.
Далее следует на первый взгляд довольно странная команда mpoint(n,m)=2, после чего
командой p=mpoint(n,m) присваивается значение переменной р. Но самое интересное -
это результат, который получаем при выполнении этой программы:
n =3
m =2
Р =3
По крайней мере, странно выглядят значения переменных n и p . Разумеется, это не
совсем так, а точнее, совсем не так. Начнем с команды mpoint(n,m)=2. Чтобы понять
смысл этой команды, вспомним, что результатом инструкции mpoint(n,m) является
ссылка на больший из аргументов - при текущих значениях аргументов n и m это есть
ссылка на переменную m. Поэтому команда mpoint(n,m)=2 эквивалентна
присваиванию переменной m значения 2. Далее, командой p=mpoint(n,m) переменной
р присваивается значение максимального из аргументов n и m - теперь это переменная n
(поскольку на момент вызова означенной команды значение n равно 3, а значение m равно
2). Таким образом, переменная n остается со значением 3, такое же значение имеет
переменная р, а значение переменной m равно 2. Отметим также, что аргументы функции
mpoint() передаются по ссылке по тем же причинам, что и в предыдущем примере.

Получениe нескольких результатов


Во многих практических задачах ответных значений бывает несколько. Поскольку
имя функции является носителем одного результирующего значения, для решения
поставленной задачи напрямую его использовать нельзя. Рассмотрим, каким образом в
таких случаях описываются и вызываются функции.
Если аргументы функции передаются по значению, прямым путем значения
соответствующих переменных в функции изменять нельзя (чтобы вернуть ответные
значения в качестве результатов вычисления). Однако соответствующей цели можно
достичь, если в качестве параметров и аргументов функции использовать адреса
переменных, а не их значения.
Пример 1. Получение нескольких результатов функцией.
Пусть требуется составить программу, содержащую обращение к функции, которая
обменивает переменные их значениями. Возможный вариант решения имеет вид:
void change(int*, int *); // Прототип функции
void main()
{int a,b;
cout << "\nВведите а и b";
cin >> a;
cin >> b;
change(&a, &b);
cout << "\nНовые значения: а=" << a << "\tb=" << b);
}
void change(int *x, int *y) //заголовок функции
{int z; // x и у — указатели,
z=*x; *x=*y; *y=z; //*x и *у — значения, на которые они
указывают
}
В приведенной программе указатели &a и &b используются в качестве аргументов
функции change(). При обращении к функции значения адресов-аргументов
передаются по значению указателям-параметрам х и у. При выполнении функции
адресуемые указателями значения меняются местами и управление передается обратно в
точку вызова функции.
Также можно передавать параметры посылке:
void change(int &x, int &y) //заголовок функции
{int z; // x и у переменные, переданные по
ссылке,
z=x; x=y; y=z; //
}
Вызов функции:
change(a, b);
Рекурсивные функции
Мой соперник не будет избран, если дела не пойдут хуже. А дела
не пойдут хуже, если его не выберут.
Дж. Буш (старший)
Для реализации рекурсивных алгоритмов в C++ предусмотрена возможность создания
рекурсивных функций. Рекурсивная функция представляет собой функцию, в теле
которой осуществляется вызов этой же функции.
Пример 1. Использование рекурсивной функции для вычисления факториала.
Пусть требуется составить программу вычисления факториала произвольного
положительного числа.
#include <iostream.h>
int fact(int n)
{ if (n<0) return 0;
if (n==0) return 1;
else /*надо ли?*/ return n * fact(n-l);
}
void main()
{ int m;
cout << "\nВведите целое число:";
cin >> m;
cout << "\n Фактoриал числа " << m << " равен " << fact(m);
}
Для отрицательного аргумента факториала не существует, поэтому функция в этом
случае возвращает нулевое значение. Так как факториал 0 равен 1 по определению, то в
теле функции предусмотрен и этот вариант. В случае когда аргумент функции fact()
отличен от 0 и 1, то вызывается функция fact() с уменьшенным на единицу значением
параметра и результат умножается на значение текущего параметра. Таким образом, в
результате встроенных вызовов функций будет возвращен следующий результат:
n * (n-1) * (n-2) * . . . * 2 * 1 * 1
Последняя единица при формировании результата принадлежит вызову fact(0).
Поскольку в языке C++ отсутствует операция возведения в степень, то для выполнения
возведения в степень вещественного ненулевого числа можно использовать рекурсивную
функцию.
Пример 2. Использование рекурсивной функции для возведения в степень.
Пусть требуется составить программу возведения в положительную целую степень
вещественного ненулевого числа.
// Возведение основания х в степень n
float st (float x, int n)
{ if (n==0) return 1;
if (x==0) return 0;
if (n>0) return x*st(x,n-l);
if (n<0) return st(x,n+l)/x;
}
void main()
{ int m;
float a,y;
cout << "\nВведите основание степени:";
cin >> a;
cout << "\nВведите степень числа:";
cin >> m;
y=st(a,m);
cout << "\n Число " << a << " в степени " << m << " равно:"
<< у;
}
В функции предусмотрены четыре ветви выполнения возведения в степень
вещественного числа:
• степень числа равна нулю;
• основание равно нулю;
• степень больше нуля;
• степень меньше нуля.
При вызове функции, когда основание степени больше нуля, например, если при вводе
заданы значения переменных а=2.0 и m=4, тогда обращение st(a,m) приводит к
следующему вычислению:
2.0 * 2.0 * 2.0 * 2.0 * 1
Вызов функции для отрицательной степени, например, когда значения аргументов
равны а=3.0 и m=-2 приводит к вычислению следующего выражения:
1/3.0/3.0

Перегрузка функций
В C++ допускается наличие нескольких одноименных функций, выполняющих
аналогичные действия над данными разных типов, и отличающиеся типом
возвращаемого результата. Причем вполне достаточно хотя бы одного отличия. При
вызове презагруженной функции в программе выбор нужного варианта функции
осуществляется исходя из использованного синтаксиса вызова функции.
Например, пусть в программе определены две функции с прототипами:
int max(int,int);
float max(float,float);
В этом случае в программе допустимо указание следующих операторов:
float х,у;
cout<<"max="<<max (5,8) <<endl;
х=12.5; у=24.8;
cout<<"max="<<max (х, у) ;
В процессе компиляции программы при обращении к функциям тах() в зависимости
от типа и числа аргументов будет осуществляться загрузка требуемого экземпляра
функции. Описанный механизм называется перегрузкой функций.
Если в программе описано несколько одноименных функций, то при компиляции
возможны следующие ситуации:
1. Если возвращаемые значения и сигнатуры (тип и число параметров) нескольких
функций совпадают, то второе и последующие объявления трактуются как
переобъявления первого. Например:
extern double max(double a,double b);
double max(double c,double d);
2. Если сигнатуры нескольких одноименных функций совпадают, но возвращаемые
значения различны, то второе и последующие объявления при компиляции
рассматриваются как ошибочные. Например, при наличии двух одноименных функций со
следующими прототипами
unsigned int max(unsigned,unsigned);
extern long max (unsigned,unsigned);
при компиляции будет выдано сообщение об ошибке. Причина его в том, что при
определении возможности перезагрузки функции тип возвращаемого значения не
принимается во внимание, а сигнатуры в данном случае совпадают.
3. Если сигнатуры нескольких одноименных функций различны (параметры имеют
различия по типам или количеству), тогда функция считается перегружаемой при
условии объявления ее экземпляров в одной области видимости. Например:
int max(int, int);
float max(float, float);
float max (float c,float d)
{ if (c<d) return(c);
return(d)
}
int max(int a,int b)
{ if (a>b) return(a);
if (a<=b) return(b);
}
В данном случае мы имеем два экземпляра перегружаемой функции тах().

Выбор экземпляра функции


При выборе требуемого экземпляра функции осуществляется сравнение типов и числа
параметров и аргументов. При этом возможны три следующие варианта:
1. Имеет место точное соответствие типов аргументов параметрам одного из
экземпляров функции. В этом случае осуществляется вызов этого экземпляра
функции. Точное соответствие имеет место в случае, когда аргументы точно
соответствуют параметрам одного из объявленных экземпляров перегружаемой
функции.
Замечания.
o Аргументы типа char и short и константа 0 считаются как точно соответствующие
параметру типа int.
o Аргумент типа float считается точно соответствующим параметру типа double.
2. Соответствие аргументов параметрам может быть достигнуто путем
преобразования типов аргументов к типам параметров только для одного
экземпляра функции. В этом случае компилятор пытается сначала выполнить
подходящие стандартные преобразования типов данных, а затем преобразования,
определенные пользователем.
Если точное соответствие не имеет места, то компилятор пытается выполнить
подходящие стандартные преобразования, при этом:
 •Аргумент любого числового типа можно преобразовать стандартным путем
к любому числовому типу.
 Константа 0 может быть преобразована стандартным путем к указателю.
 Любой указатель может быть преобразован к указателю на void.
Преобразования, определенные пользователем, применяются компилятором при
отсутствии точного соответствия и подходящих стандартных преобразований.
3. Соответствие может быть достигнуто более чем для одного экземпляра
функции. В этой ситуации следует иметь в виду, что преобразования типов,
определенные пользователем, имеют меньший приоритет в сравнении со
стандартными преобразованиями. Если соответствие типов достигается путем
равноприоритетных преобразований, то компилятором выдается сообщение об
ошибке (двусмысленность). Это связано с тем, что здесь невозможно определить
однозначно требуемый экземпляр функции.
// перегрузка функции.
//Первый вариант функции:
void showArgs(double x)
{cout << "Double-number ”<< x << endl; }
//Второй вариант функции:
void showArgs(double x,double y)
{cout<<"Double-numbers " << x << " and ” << y << endl;
}
//Третий вариант функции:
void showArgs(char s)
{cout<<"Symbol "<<s<<endl;}
//Четвертый вариант функции:
int showArgs (int n)
{return n;}
int main ()
{ int n=3;
double x=2.5,y=5.1;
char s=’w’;
//Первый вариант функции:
showArgs(x);
//Второй вариант функции:
showArgs(х,у);
//Третий вариант функции:
showArgs(s);
//Четвертый вариант функции:
cout<<"Int-number "<<showArgs (n)<<endl;
}
В программе описано четыре варианта функции showArgs(), назначение
которой состоит в выводе на экран информации о ее аргументах. Однако в зависимости от
количества и типа аргументов выполняются немного разные действия. В частности,
предусмотрены такие варианты передачи аргументов:
1. Один аргумент типа double
2. Два аргумента типа double
3. Один аргумент типа char
4. Один аргумент типа int
В первых трех случаях результат функцией не возвращается (тип функции void), а
на экран выводится сообщение о типе и значении аргумента (или аргументов). В
четвертом случае функцией в качестве значения возвращается аргумент.
В главном методе программы функция showArgs() вызывается в разном
контексте: вызов showArgs(х) соответствует варианту функции для одного аргумента
типа double, вызов showArgs(х,у) соответствует варианту функции для двух
аргументов типа double, вызов showArgs(s) соответствует варианту функции для
одного аргумента типа char, вызов cout<<"Int-number "<<showArgs(n) <<
endl соответствует варианту функции с одним аргументом типа int. Результат
выполнения программы будет следующим:
Double-number 2.5
Double-numbers 2.5 and 5.1
Symbol w
Int-number 3
При работе с перегруженными функциями не следует забывать об автоматическом
приведении типов. Эти два механизма в симбиозе могут давать очень интересные, а
иногда и странные результаты. Рассмотрим пример.
//Автоматическое приведение без перегрузки
void ndiv(double x, double y)
{cout<<"x/y= " << x/y << endl; }
int main ()
{ double x=l0 ,y=3;
int n=10,m=3;
ndiv(x,y);
ndiv(n,m);
}
Результат выполнения такой программы будет следующим:
х/у= 3.33333
х/у= 3.33333
Первая строка, которая является результатом выполнения команды ndiv(х,у),
думается, вопросов не вызывает. Результатом вызова функции является частное двух
чисел - аргументов функции. Хотя в команде ndiv(n,m) аргументами функции указаны
целые числа, благодаря автоматическому приведению типов целочисленные значения
расширяются до типа double, после чего вычисляется нужный результат.
Однако стоит изменить программный код, перегрузив функцию ndiv(), и
результат изменится кардинально.
//Перегрузка и автоматическое приведение типов
void ndiv(double x, double y)
{ cout<<"x/y= "<<x/y<<endl;}
void ndiv(int n, int m)
{ cout<<"n/m= "<<n/m<<endl;}
int main ()
{ double x=10,y=3;
int n=10,m=3;
ndiv(x,y);
ndiv(n,m);
}
В частности, на экране в результате выполнения программы появится сообщение:
х/у= 3.33333
n/m= 3
Дело в том, что измененный программный код содержит перегруженную функцию
ndiv(), для которой предусмотрен вызов как с двумя аргументами типа double, так и с
двумя аргументами типа int. В последнем случае, хотя результат функции формально
вычисляется так же, как и в исходном варианте, для целых чисел оператор / означает
целочисленное деление с отбрасыванием остатка. Поэтому в результате получаем число
3, а не 3.33333, как для вызова функции с double -аргументами. Более того, нередко
случаются ситуации, когда с учетом автоматического приведения типов невозможно
однозначно определить, какой из вариантов перегруженной функции необходимо
вызывать. Например, если для перегруженной функции предусмотрены два варианта
передачи аргумента для типа данных float и для типа данных double, то передача
функции в качестве аргумента int-переменной приведет к ошибке компиляции,
поскольку непонятно, к какому формату данных (float или double) нужно
преобразовывать тип int. Такие ситуации относятся к разряду логических ошибок, и их
нужно внимательно отслеживать.
Как и обычные функции, перегруженные функции могут иметь аргументы со
значениями, используемыми по умолчанию. Причем для каждого варианта
перегружаемой функции эти значения по умолчанию могут быть разными. Единственное,
за чем необходимо постоянно следить, чтобы наличие значений по умолчанию у
аргументов не приводило к неоднозначным ситуациям при вызове перегруженной
функции.
//Перегрузка и значения по умолчанию
void hello ()
{cout<<"Hello, my friend! \n";}
void hello (char str[], char name[] = “Alex")
{ cout << str << " , "<<name<<" ! "<<endl; }
int main ()
{
hello ();
hello("Hello","Peter");
hello("Hi");
return 0 ;
}
У перегруженной функции hello() два варианта: без аргументов и с двумя
аргументами-массивами типа char. Причем в последнем случае второй аргумент имеет
значение по умолчанию.
Если функция вызывается без аргументов, в результате ее выполнения на экран
выводится сообщение Hello, my friend!. При вызове функции с двумя
аргументами (каждый аргумент - текстовая строка, реализованная в виде массива
символов) на экран последовательно выводятся текстовые значения аргументов функции.
Поскольку второй аргумент имеет значение по умолчанию Alex, то формально функцию
можно вызывать и с одним текстовым аргументом. В главном методе функция hello()
последовательно вызывается без аргументов (команда hello() ), с двумя аргументами
(команда hello("Hello", "Peter") ) и с одним аргументом (команда
hello("Hi") ). Результат выполнения программы выглядит следующим образом:
Hello, my friend!
Hello, Peter!
Hi, Alex!
Но если попытаться для второго варианта функции (с двумя аргументами) указать
значение по умолчанию и для первого аргумента, возникнет ошибка. Причина в том, что
наличие у каждого из аргументов функции hello() значения по умолчанию приводит к
неоднозначной ситуации: если функция при вызове указана без аргументов, невозможно
определить, вызывается ли это вариант функции без аргументов, или нужно использовать
вариант функции с двумя аргументами со значениями по умолчанию. Как уже
отмечалось, на подобные ситуации следует постоянно обращать внимание при создании
программных кодов в процессе перегрузкй функций.
Структуры(struct)
В отличие от массива, все элементы которого однотипны, структура может содержать
элементы разных типов. В языке С++ структура является видом класса и обладает всеми
его свойствами, но во многих случаях достаточно использовать структуры так, как они
определены в языке С:
Struct [имя типа]
{ Тип1 элемент1;
Тип2 элемент2;
Типn элементn;
} [список описателей];
Элементы структуры называются полями структуры и могут иметь любой тип, кроме типа
этой же структуры, но могут быть указателями на него. Если отсутствует имя типа,
должен быть указан список описателей переменных, указателей или массивов. В этом
случае описание структуры служит определением элементов этого списка: Размер
структуры не обязательно равен сумме размеров ее элементов, поскольку они могут быть
выровнены по границам слова.
После фигурных скобок, заканчивающих непосредственно описание структуры, можно
указать (а можно и не указывать) список переменных структуры. Дело в том, что само по
себе описание структуры эту структуру не создает. Описание структуры - это всего лишь
некий шаблон, по которому впоследствии создаются переменные. Процесс создания
переменных структуры можно не откладывать в долгий ящик, а создать их сразу, указав
список с названиями после описания структуры.
// Определение массива структур и указателя на структуру:
struct One
{ char fio[30]
int date, code;
double salary;
}staff [100], * ps;
Если список отсутствует, описание структуры определяет новый тип, имя которого можно
использовать в дальнейшем наряду со стандартными типами, например:
Struct Worker
{char fio[30];
int date, code;
double salary
}; // Описание заканчивается точкой с запятой
Worker a; //перменнная а типа Worker
С точки зрения использования в программе имеет смысл говорить лишь о переменных
структуры. Фактически переменная структуры - это объект, который имеет имя (имя
переменной структуры) и поля, тип и названия которых определяются описанием
структуры. Чтобы в программе создать переменную структуры, необходимо указать имя
структуры, в соответствии с описанием которой создается переменная, и имя этой
переменной. Другими словами, переменная структуры в программе создается точно так
же, как и переменная любого базового типа, только вместо названия типа переменной
указывается название структуры.
Сама по себе переменная структуры интерес представляет больше спортивный, чем
практический. Все данные, которые могут понадобиться в процессе выполнения
программы, записаны в поля переменной. Обращение к полю переменной структуры
осуществляется через так называемый точечный синтаксис (стандартный синтаксис для
объектно-ориентированного программирования) - указывается имя переменной
структуры, и через точку имя поля, к которому выполняется обращение, т.е. в формате
структура.поле.
Доступ к полям структуры выполняется с помощью операций выбора (точка) при
обращении к полю через имя структуры и через -> при обращении через указатель,
например:
Worker worker, staff[100], * ps;

worker.fio = ”Страусенко”;
staff[8].code = 215;
ps->salary = 0.12;
Если элементом структуры является другая структура, то доступ к ее элементам
выполняется через две операции выбора:
struct A { int a; double x;};
struct B { int a; double x;} x[2];
x[0].a.a=1;
x[1].x=0.1;
Как видно из примера поля разных структур могут иметь одинаковые имена, поскольку у
них разная область видимости. Более того, можно объявлять в одной области видимости
структуру и другой объект (например, переменную или массив) с одинаковыми именами,
если при определении структурной переменной использовать слово struct, но не
советую это делать - запутать компилятор труднее, чем себя.
// Объявление и использованиеструктуры
struct Marks
{ char name[80];
int phys;
int chem;
int maths;
} ivanov,petrov,sidorov;
struct Exams
{ double phys;
double chem;
double maths;
};
int main()
{ strcpy (ivanov.name, "Sergei Ivanov") ;
ivanov.phys=4;
ivanov.chem=3;
ivanov.maths=3;
strcpy (petrov.name, "Igor Petrov");
petrov.phys=5;
petrov.chem=4;
petrov.maths=4;
strcpy(sidorov.name,"Ivan Sidorov");
sidorov.phys=5;
sidorov.chem=4;
sidorov.maths=3;
Exams LastYear,ThisYear;
LastYear.chem=4.33333;
LastYear.phys=3.66667;
LastYear.maths=3.33333;
ThisYear.chem=(double)(ivanov.chem + petrov.chem + sidorov.chem)/3;
ThisYear.phys=(double)(ivanov.phys + petrov.phys + sidorov.phys)/3;
ThisYear.maths=(double)(ivanov.maths + petrov.maths +
sidorov.maths)/3;
cout<<"Last year marks:" << endl;
cout<<"Physics" << LastYear.phys<<endl;
cout<<"Chemistry"<<LastYear.chem<<endl;
cout<<"Mathematics"<<LastYear.maths<<endl;
cout<<"This year marks:" << endl;
cout<<"Physics"<<ThisYear.phys<<endl;
cout<<"Chemistry"<<ThisYear.chem<<endl;
cout<<"Mathematics "<<ThisYear.maths<<endl;
return 0;
}
В программе объявлены две структуры: Marks и Exams. В структуру Marks входят четыре
поля: символьный массив name типа char и целочисленные поля (тип int) phys, chem и
maths. Одновременно с объявлением структуры выполняется объявление и переменных
структуры с именами ivanov, petrov и sidorov.
Структура Marks создается в программе для хранения информации об успеваемости
учащихся. Оценки по трем предметам (физика, химия и математика) заносятся в
целочисленные поля phys, chem и maths соответственно. Поле-массив name
предназначено для записи имени и фамилии учащегося.
Описание структуры является лишь общим шаблоном, в соответствии с которым
формируются переменные структуры. Чтобы было куда записать оценки, а также имя и
фамилию, необходимо создать переменную структуры. В данном случае переменные
структуры созданы путем перечисления названий в конце описания структуры.
Назначение структуры Exams состоит в хранении средних оценок по каждому из
предметов в разные годы. Каждому году соответствует переменная структуры.
Переменные структуры создаются непосредственно в главном методе программы. У
структуры три поля, которые имеют такие же названия (phys, chem и maths), однако
теперь поля объявлены как имеющие тип double. Дело в том, что средняя оценка, в
отличие от оценки учащегося по предмету, в общем случае не является целым числом.
В главном методе программы сначала выполняется инициализация полей переменных
структуры Marks. В частности, заполнение массива name для переменной ivanov
выполняется командой strcpy (ivanov.name, "Sergei Ivanov") (копирование
строки "Sergei Ivanov" в поле-массив name).
ivanov.phys=4;
ivanov.chem=3;
ivanov.maths=3 ;
Точно так же заполняются поля двух других переменных (petrov и sidorov) структуры
Marks.
Командой Exams LastYear, ThisYear в главном методе программы объявляются две
переменные LastYear и ThisYear структуры Exams. Переменная LastYear
предназначена для записи средних оценок прошлого года.
Оценки (средние) за текущий год записываются в переменную ThisYear структуры
Exams. Значения полям переменной структуры LastYear присваиваются командами
LastYear.chem=4.33333;
LastYear.phys=3.66667;
LastYear.maths=3.33333;
Соответствующие поля переменной структуры определяются как средние арифметические
по набору учащихся (их в данном случае всего три):
ThisYear.chem=(double)(ivanov.chem+petrov.chem+sidorov.chem)/3;
ThisYear.phys=(double)(ivanov.phys+petrov.phys+sidorov.phys)/3;
ThisYear.maths=(double)(ivanov.maths+petrov.maths+sidorov.maths)/3;
Инструкция (double) использована для явного приведения типов, поскольку по
умолчанию для целочисленных операндов операция деления выполняется как деление с
отбрасыванием дробной части (целочисленное деление). Далее полученные значения для
полей переменных LastYear и ThisYear структуры Exams выводятся на экран.
Результат имеет следующий вид:
Last year marks:
Physics 3.66667
Chemistry 4.33333
Mathematics 3.33333
This year marks:
Physics 4.66667
Chemistry 3.66667
Mathematics 3.33333.
Для переменных одного и того же структурного типа определена операция присваивания,
при этом происходит поэлементное копирование. В результате такого присваивания из
одной переменной в другую копируются значения всех полей структуры:
ivanov=petrov;
Для инициализации структуры значения ее элементов перечисляют в фигурных скобках в
порядке их описания:
Struct Worker
{
char fio[30];
int date, code;
double salary
} worker = { “Страусенкo“, 31,215,3400.55};

Массивы структур
Как и обычные переменные, структуры могут быть элементами массивов.
//Массивы структур
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
struct Marks
{
char name[80];
int phys;
int chem;
int maths;
};
int main()
{
const int n=3;
bool state;
char s [80];
Marks students[n];
for (int i=0 ;i<n;i++)
{
cout << ("Student name:");
gets(students[i].name);
students[i].phys=3+rand()%3;
students[i].chem=3+rand()%3;
students[i].maths=3+rand()%3;
}
do
{
cout<<"What is the student name?";
gets (s);
if (!strcmp(s,"exit")) return 0 ;
state=true;
for(int i=0 ;i<n;i++)
{
if (!strcmp(students[i].name,s))
{ state=false;
cout<<"Physiscs:"<<students[i].phys<<endl;
cout<<"Chemistry:"<<students[i].chem<<endl;
cout<<"Mathematics:"<<students[i].maths<<endl;
break;
}
}
if (state) cout<<"There is no student with such name\n";
}while(true);
}
Пример выполнения программы может выглядеть следующим образом:
Student name: Sergei Ivanov
Student name: Igor Petrov
Student name: Ivan Sidorov
What is the student name? Sergei Ivanov
Physics: 5
Chemistry: 5
Mathematics: 4
What is the student name? Ivan Sidorov
Physics: 3
Chemistry: 3
Mathematics: 4
What is the student name? Igor Petrovski
There is no student with such name
What is the student name? Igor Petrov
Physics: 4
Chemistry: 5
Mathematics: 4
What is the student name? exit
В программе командой Marks student s[n] определяется массив students
переменных структуры Marks из n элементов (предварительно объявлена целочисленная
константа const int n=3). Заполнение элементов массива выполняется в рамках
оператора цикла. Индексная переменная i пробегает значения от 0 до n-1 включительно.
Каждый раз выводится запрос на ввод имени учащегося и это имя командой
gets(students[i].name) записывается в поле name переменной структуры
students[i]-элемента массива students. Оценки по трем предметам для каждого
учащегося определяются как случайные числа командами students[i].
phys=3+rand()%3, students[i].chem=3 + rand( )%3 и
students[i].maths=3+rand () %3. К базовой оценке 3 прибавляется целое случайное
число в диапазоне от 0 до 2 включительно (результат команды rand()%3 - остаток от
деления случайного числа на 3).
Командой char s [80] объявляется символьный массив, в который будет выполняться
считывание вводимого пользователем имени учащегося для отображения его оценок. Этот
массив используется в операторе цикла do{ . . . }while (true). Причем стоит
обратить внимание, что в качестве проверяемого условия указано логическое значение
true, что означает, что формально цикл бесконечный. Возможность выхода из этого цикла
предусмотрена в самом цикле. В начале цикла командой cout<<"What is the student
name?" выводится запрос на ввод имени учащегося. Введенное пользователем имя с
помощью команды gets(s) считывается и записывается в массив s. После того, как имя
учащегося введено, выполняется условный оператор
if (!strcmp(s, "exit")) return 0.
Этой командой предусмотрена возможность не только выхода из оператора цикла, но и
завершения работы программы: если пользователь в качестве имени введет exit, работа
программы завершается (инструкция return 0). Там же командой state=true
логической переменной state (переменная используется для индикации того, найдено
совпадение имен или нет) присваивается значение true. После этого посредством
оператора цикла выполняется последовательный перебор элементов массива students и
производится сравнивание значений строк, записанных в массив s и в поле-массив
students[i].name. Для сравнения строк используется функция strcmp(). Напомним,
что если строки совпадают, то в качестве значения функцией возвращается 0, поэтому в
условном операторе указано условие !strcmp(students[i].name, s). Если условие
истинно, командой state=false меняется состояние логической переменной state,
после чего выводится информация об оценках соответствующего учащегося. В конце
условного оператора размещена команда break для преждевременного выхода из
оператора цикла - если совпадение найдено, продолжать поиск не имеет смысла. Если
совпадения нет (т.е. введенное пользователем имя не найдено при просмотре полей
элементов массива students), переменная state имеет значение true. На этот случай
предусмотрена команда
if (state) cout << "There is no student with such name\n".

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


элемент массива:
struct Complex
{
float real, im;
}compl[2][3]={{{1.1},{1.1},{1,1}},/*Строка1*/{{2.2},{2.2},{2.2}}};
//Строка 2, //то есть массив compl[1]

Структуры как аргументы функций


Структуры могут передаваться в качестве аргументов функциям. В этом случае в качестве
типа аргумента функции указывается название структуры, переменная которой будет
передана в функцию.
//структур иргушштами функрй
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
struct Marks
{ char name[80];
int phys;
int сhem;
int maths;
};
void set_one(Marks &str)
{ cout << ("Student name:");
gets(str.name);
str.phys=3+rand()%3;
str.chem=3+rand()%3;
str.maths=3+rand()%3;
}
void set_all(Marks *str, int m)
{ for(int i=0; i<m; i++) set_one(str[i]); }
void get(Marks *str,int m )
{ bool state;
char s [80];
do
{ cout<<"What is the student name? ";
gets (s);
if (!strcmp(s,"exit")) return;
state=true;
for (int i=0; i<m; i++)
{ if (!strcmp(str[i].name,s))
{ state=false;
cout<<"Physiscs:"<<str[i].phys<<endl;
cout<<"Chemistry:"<<str[i].chem<<endl;
cout<<"Mathematics:"<<str[i].maths<<endl;
break;
}
}
if (state) cout<<"There is no student with such name\n";
} while(true);
}
int main ()
{
const int n=3;
Marks students[n];
set_all(students,n);
get (students,n);
return 0 ;
}
Основной программный код реализован в виде нескольких функций. Функция set_one()
предназначена для заполнения полей структуры, переданной аргументом функции.
Функция объявлена как void set_one (Marks &str). Аргумент функции указан как
такой, что имеет тип Marks - тип объявленной ранее структуры. Кроме того, аргумент
(переменная структуры) передается по ссылке, поэтому перед формальным именем str
аргумента функции указывается оператор &. Причина передачи аргумента функции по
ссылке, а не по значению (как обычно) состоит в том, что в результате выполнения
функции необходимо изменить аргумент, переданный этой функции.
При выполнении кода функции выводится приглашение ввести имя учащегося и с
помощью генерирования случайных чисел определяются оценки по трем предметам.
В функции set_all() в рамках оператора цикла вызывается функция set_one() для
заполнения элементов массива, указанного первым аргументом функции set_all().
Прототип этой функции имеет вид void set_all (Marks *str, int m). Первый
аргумент - указатель на переменную структуры Marks. В данном случае это имя
заполняемого массива. Второй аргумент - размер массива. Поскольку имя массива
является ссылкой на его первый элемент, проблемы с передачей аргументов, как в
предыдущем случае, не возникает - при заполнении массива будет изменяться именно тот
массив, имя которого передано аргументом функции.
Функция get() с прототипом void get (Marks *str, int m) предназначена для
считывания имени учащегося и вывода его оценок. Первый аргумент функции - массив, в
котором выполняется поиск на предмет совпадения имен учащихся, второй аргумент -
размер массива. Использована инструкция if (!strcmp(s, "exit")) return для
завершения работы функции (но не программы!). Непосредственно после инструкции
return ничего не указывается, поскольку функция определена как такая, что не
возвращает результат.
Как следствие использования описанных функций существенно упрощается код главного
метода программы. Кроме инициализации константы, определяющей размер массива, и
объявления массива с элементами-переменными структуры, основной код главного
метода состоит из вызова двух функций (set_all() и get()) и стандартной
инструкции return 0.
Указатели на структуры
При объявлении указателя на структуру, как и в случае создания указателей на значения
базовых типов, указывается тип структуры, а перед именем переменной-указателя
ставится оператор *. Этот же оператор используется для получения доступа к переменной
структуры по указателю на эту переменную.
Кроме того, через указатель на структуру можно обращаться непосредственно к полям
структуры, для чего используют оператор -> (стрелка, состоит из двух символов - и >).
Например, если в программе определен указатель на переменную структуры, у которой
есть поле, то доступ к этому полю можно получить с помощью инструкции указатель-
>поле.
//Указатели на структуру >
#include <iostream>
using namespace std;
struct Numbers
{ int integer;
double real;
char symbol;
};
void show(Numbers x )
{ cout << "Integer:"<<x.integer<<endl;
cout << “Real:"<<x.real<<endl;
cout<<"Symbol:" << x.symbol<<endl;
}
int main ()
{ Numbers a,b;
Numbers *p,*q;
p=&a;
q=&b;
p->integer=1;
p->real=2.5;
p->symbol=’a’;
(*q).integer=2;
(*q).real=5.1;
(*q).symbol='b';
show(a);
show(*q);
return 0;
}
В программе объявлена структура Numbers, имеющая три поля: типа int, типа double и
типа char. Кроме этого, описана функция show(), аргументом которой является
переменная структуры Numbers, а в результате выполнения функции выводятся значения
всех полей аргумента.
В главном методе программы командой Numbers а, b объявляются переменные а и b
структуры Numbers, а командой Numbers *р, *q объявлены указатели р и q на
переменные структуры Numbers. Значения указателям (адреса) присваиваются командами
р=&а и q=&b соответственно. При этом использован, как и в случае переменных базовых
типов, оператор получения адреса &. Поля переменной а заполняются посредством
команд p->integer=1, p->real=2.5 и p->symbol=’а’. В этом случае ссылка на поле
реализуется через указатель р и оператор ->. Поля переменной b заполняются более
консервативным способом. Для этого использованы команды (*q).integer=2,
(*q).real=5.1 и (*q).symbol=’b’ соответственно. Здесь принято во внимание, что
инструкция *q является ничем иным, как той переменной, на которую ссылается
указатель q (т.е. переменная b). Аналогично при вызове функции show() ее аргументом
можно указать как имя переменной (а или b) структуры Numbers, так и инструкцию вида
*р или *q. В результате выполнения программы получим следующее:
Integer: 1
Real: 2.5
Symbol: а
Integer: 2
Real: 5.1
Symbol: b
Имя структуры можно использовать сразу после его объявления в тех случаях, когда
компилятору не требуется знать размер структуры, например:
struct List; // Объявление структуры List
struct Link
{
List * p; // Указатель на структуру List
Link * prev, * succ; // Указатели на структуру Link
};
struct List { / * определение структуры List */ };
Это позволяет создавать связные списки структур.
Указатели на структуры находят широкое применение, и в первую очередь при
составлении динамических списков.

Битовые поля
Битовые поля - это особый вид полей структуры. Они используются для плотной
упаковки данных, например, флажков типа «да/нет». Минимальная адресуемая ячейка
памяти – 1 байт, а для хранения флажка достаточно одного бита. При описании полей
структуры в явном виде можно указывать размер полей. Минимальный размер поля
структуры - один бит. Общий синтаксис объявления структуры с явным указанием
размеров полей имеет вид:
struct имя
{
тип_поля1 имя_поля1: размер1;
тип_поля2 имя_поля2: размер2;
тип_поляЫ имя_поляЫ: размеры;
};
При описании битового поля после имени через двоеточие указывается длина поля в
битах (целая положительная константа):
Struct Options
{
Bool cеnterX: 1;
Bool centerY: 1;
Unsigned int shadow : 2;
Unsigned int pаlette : 4;
};
Битовые поля могут быть любого целого типа. При этом для части полей можно указывать
размер, а для других - нет. Если поле имеет размер в один бит, в качестве его типа
указывается unsigned (у числа, реализованного с помощью одного бита, не может быть
знака). Имя поля может отсутствовать, такие поля служат для выравнивания на
аппаратную границу. Доступ к полю осуществляется обычным способом - по имени.
Адрес поля получить нельзя, однако в остальном битовые поля можно использовать точно
так же, как обычные поля структуры. Следует учитывать, что операции с отдельными
битами реализуются гораздо менее эффективно, чем с байтами и словами, так как
компилятор должен генерировать специальные коды, и экономия памяти под переменные
оборачивается увеличением объема кода программы. Размещение битовых полей в памяти
зависит от компилятора и аппаратуры.
//Явное указание размеров полейструктуры
#include <iostream>
using namespace std;
struct BitFields
{ unsigned int state:1;
int n:2;
int m;
} str;
int main ()
{ cout<<"Enter a number:";
cin>>str.m;
str.state=str.m%2;
str.n=str.m%4-2;
cout<<"state=" << str.state<<endl;
cout<<"n=”<< str.n<<endl;
return 0;
}
У структуры BitFields три поля: однобитовое целочисленное поле state, двухбитовое
целочисленное поле n и целочисленное поле m (размер поля не указан).
Поле state может принимать всего два значения: 0 или 1. Поле n принимает
целочисленные значения в диапазоне от -2 до 1 включительно, т.е. всего 4 возможных
значения: напомним, что старший бит определяет знак числа, поэтому двухбитовое число
11 соответствует числу -2, двухбитовое число 10 соответствует числу -1, число 00
соответствует нулю и число 01 соответствует числу 1.
В главном методе программы выводится запрос на ввод пользователем целого числа. Это
число записывается в поле m переменной str структуры BitFields. В переменную
state заносится остаток от деления этого числа на 2, а в переменную n записывается
остаток от деления введенного пользователем числа на 4 минус 2. Значения полей
переменной str структуры BitFields выводятся на экран. В результате можем получить
нечто наподобие следующего:
Enter a number: 9
state = 1
n = -1
Таким образом, путем явного указания размеров полей структуры удалось добиться
экономии системных ресурсов: для записи значений используется минимально
необходимое количество бит. Возникает естественный вопрос: а что будет, если
присваиваемое битовому полю (т.е. полю, для которого явно указан размер) значение,
выходит за допустимые для этого поля пределы? Например, что будет, если полю п
значение присваивать с помощью команды str.n=str.m%4-4? Ответ такой: в указанных
случаях происходит автоматическое отбрасывание лишних бит. В частности, если поле m
равно 9, а поле n определяется как str.n=str.m%4-4, то этому полю будет присвоено
значение 1. Поясним это. Так, остаток от деления 9 на 4 есть 1. Если отнять 4, получим -
3. В двоичном представлении число -3 имеет вид (последние три бита, все старшие биты
равны 1) 101. Старшие биты отбрасываются, и в результате в двухбитовом представлении
получаем 01, что соответствует числу 1.

Объединение (union)
Объединение (union) представляет собой частный случай структуры, все поля которой
располагаются по одному и тому же адресу. Формат описания такой же, как у структуры,
только вместо ключевого слова struct используется слово union. Длина объединения
равна наибольшей из длин его полей. В каждый момент времени в переменной типа
объединение хранится только одно значение, и ответственность за его правильное
использование лежит на программисте.
Объединение применяют для экономии памяти в тех случаях, когда известно, что больше
одного поля одновременно не требуется:
#include <iostream.h>
int main()
{
enum Paytype {CARD, CHECK};
Paytype ptype;
union payment
{
char card[25];
long check;
} info;
/*присваивание значений info и ptype*/
Switch (ptype)
{
case CARD: cout <<Оплата по карте: “<<info.card; break;
case CHECK: cout <<Оплата чеком: “<<info.check; break;
}
return 0;
}
Объединение часто используют в качестве поля структуры, при этом в структуру удобно
включить дополнительное поле, определяющее, какой именно элемент объединения
используется в каждый момент. Имя объединения можно не указывать, что позволяет
обращаться к его полям непосредственно:
#include <iostream.h>
int main()
{
enum Paytype {CARD, CHECK};
struct
{
Paytype ptype;
Union
{
char card[25];
long check;
};
} info;
... /*присваивание значений info*/
switch (info.ptype)
{
case CARD: cout <<Оплата по карте: “<<info.card: break;
case CHECK: cout <<Оплата чеком: “<<info.check; break;
}
return 0;
}
//Использование объединений
#include <iostream>
using namespace std;
union nums
{ unsigned short int n;
short int m;
};
void show(nums a )
{ cout << "n =”<< a.n << endl;
cout << "m = "<<a.m<<endl;
cout<<endl ;
};
int main()
{ nums un;
un.m=1;
show(un);
un.m=32767;
show(un);
un.m=65535;
show(un);
un.m=-1;
show(un);
un.m=-65536;
show(un);
return 0;
}
Объявленное в программе объединение nums содержит два члена: целочисленную
беззнаковую переменную n типа unsigned short int и целочисленную переменную m
типа short int. Подчеркнем, что для записи обеих переменных используется общая
область памяти. Обычно объем памяти для хранения данных объединения выбирается
исходя из размера самой большой (по типу) переменной. В данном случае размер памяти
для хранения переменных одинаков, разница только в интерпретации записанных в эту
память значений.
Ситуация следующая: при обращении к переменной n или m, являющихся членами
объединения, просматривается одна и та же область памяти. Поэтому изменяя
переменную n мы изменяем переменную m и наоборот. На все происходящее можно
посмотреть и с другой точки зрения: есть область памяти, к которой можно обращаться
через разные переменные, и в зависимости от типа переменной по-разному
интерпретировать записанное в память значение. Нечто подобное иллюстрирует и
приведенный в листинге программный код: в главном методе программы создается
экземпляр un объединения nums, одному из членов экземпляра объединения
присваивается значение, после чего проверяется значение другого члена. Для удобства в
программе описана функция show(), которой выводятся на экран значения членов
экземпляра объединения (имя экземпляра объединения указывается аргументом
функции).
Проследим, каков будет результат выполнения программного кода. Для этого необходимо
вспомнить некоторые особенности форматов unsigned short int и short int. Для
используемого компилятора диапазон изменения чисел типа short int составляет от -
32768 до 32767. Значения для чисел типа unsigned short int лежат в пределах от 0 до
65535. И в том, и в другом случае числа реализуются в виде 16-битовых двоичных
последовательностей. Другими словами, в область памяти, выделенную под экземпляр
объединения, записывается последовательность из 16-ти нулей и единиц. Если обращение
к области памяти осуществляется через переменную m, то этот двоичный код
интерпретируется как число типа short int, а при обращении к памяти через
переменную n код интерпретируется как число типа unsigned short int. Но сам код
один и тот же! Сначала выполняется присваивание m=1. В этом случае как член n, так и
член m получают одинаковые значения (точнее, одно и то же значение интерпретируется
одинаково), поэтому результатом выполнения команды show(un) будет
n = 1
m = 1
Аналогичный результат получаем в результате присваивания un.m=32767 (оба члена
имеют одинаковое значение):
n = 32767
m = 32767
Ситуация принципиально меняется после выполнения команды un.m=65535. В
результате получим:
n = 65535
m = -1
Чтобы понять, почему так происходит, более детально рассмотрим процедуру
интерпретации чисел в двоичном коде для разных типов данных. Значение 65535 выходит
за допустимые пределы диапазона данных short int, поэтому при записи значения в
переменную m старшие биты в двоичном представлении числа отбрасываются. Само по
себе значение 65535 в двоичном коде представляется в виде 0...00111...11, т.е. 16-ть
единиц с нулевыми старшими битами (общее количество бит зависит от специфики
используемого компилятора). После отбрасывания “лишних” битов остаются последние
16 единиц.
Таким образом, в память, выделенную под экземпляр объединения, записано значение
111... 11. Если это значение интерпретируется как unsigned short int (переменная n),
получаем в десятичной системе значение 65535. Если же значение интерпретируется как
такое, что относится к типу short int, ситуация несколько иная. В этом случае старший
бит отвечает за знак числа, и единичный старший бит означает, что число отрицательное.
Для получения соответствующего десятичного значения необходимо выполнить
побитовую инверсию, прибавить единицу, перевести число в десятичную систему
счисления и добавить “минус”. После побитовой инверсии получаем 000. ..00. Прибавив
единицу, получаем число вида 000. ..01, что соответствует числу 1, а со знаком “минус”
это -1 (значение переменной m).
Аналогичная ситуация складывается после выполнения команды un.m=-1.
Описанные выше преобразования необходимо проделать в обратном порядке, после чего
станет ясно, что значением переменной n будет 65535. Кстати, такой же результат
получим, если присвоим un.n=65535. А вот если выполнить команду un.m=-65536 и
затем вывести значения переменных экземпляра объединения, получим
n = 0
m = 0
Чтобы перевести число -65536 в двоичное представление, необходимо модуль этого
числа (т.е. 65536) перевести в двоичный код, инвертировать каждый бит и прибавить
единицу. Число 65536 в двоичном представлении имеет вид 000...001000...00 (в данном
случае количество N старших незначащих нулевых битов значения не имеет). После
инвертирования получаем 111...110 111...11. Прибавив единицу, получим
1000...001000...00.
В переменную m записываются последние 16 бит, т.е. число 000...00, или 0. Такое же
значение и у переменной n.
Объединения применяются также для разной интерпретации одного и того же битового
представления (но, как правило, в этом случае лучше использовать явные операции
преобразования типов). В качестве примера рассмотрим работу со структурой,
содержащей битовые поля:
struct Options
{
bool centerX:1;
bool centerY:2;
unsigned int shadow:2;
unsigned int palette:4;
};
union
{
unsigned char ch;
options bit:
} option = {0xC4};
cout << option.bit.palette;
option.ch= 0xF0; // наложение маски
По сравнению со структурами на объединения налагаются некоторые ограничения. Смысл
некоторых из них станет понятен позже:
1. Объединение может инициализироваться только значением его первого
элемента;
2. Объединение не может содержать битовые поля;
3. Объединение не может содержать виртуальные методы, конструкторы,
деструкторы и операцию присваивания;
4. Объединение не может вxодить в иерархию классов.

Перечисления и определение типов


Под перечислением подразумевают набор именованных целочисленных констант,
которые используются для определения возможных значений переменной типа
перечисления. Другими словами, перечисление определяет специальный тип данных,
переменные которого могут принимать целочисленные значения, причем каждое
целочисленное значение имеет имя. Объявляется перечисление с помощью ключевого
слова enum, после которого следует имя перечисления и список (в фигурных скобках)
именованных констант:
enum тип_перечисления {константа]., константа2 , . . . , константаn};
После списка именованных констант можно указывать список переменных типа
перечисления.
enum color {red, green, blue, yellow, black} car;
color bus;
car=green;
bus=yellow;
cout<<"Car is”<< car << "and bus is "<<bus<<endl;
Командой enum color {red, green, blue, yellow, black} car создается
перечисление color. Переменные, относящиеся к этому типу (т.е. относящиеся к
перечислению color), могут принимать значения red, green, blue, yellow и
black. Это не текстовые значения, а числовые. Другими словами, ключевые слова red,
green, blue, yellow и black - это названия целочисленных констант. По умолчанию
первая в списке значений константа равна 0, а каждая следующая на единицу больше
предыдущей. Таким образом, red соответствует значению 0, green соответствует
значению 1, blue соответствует значению 2, yellow соответствует значению 3 и black
соответствует значению 4.
В программе объявляются две переменные перечисления color. Переменная саr указана
при объявлении перечисления сразу после фигурной скобки списка возможных значений.
Переменная bus объявлена командой color bus. Значения этим переменным
присваиваются командами car=green и bus=yellow соответственно. Тем не менее
следует помнить, что переменные, на самом деле, получают целочисленные значения. В
этом легко убедиться по результату выполнения программы. В частности, на экране
появится сообщение
Car is 1 and bus is 3
Легко догадаться, что в данном случае вместо идентификаторов green и yellow при
выводе на экран использовались соответствующие числовые значения.
Используемые по умолчанию значения для целочисленных констант в списке возможных
значений могут быть изменены. В этом случае после соответствующего имени через знак
равенства указывается значение соответствующей константы.
//В списке перечисления явно указаны значения констант
enum color {red=10, green, blue=100, yellow, black};
color car,bus,van;
car=green; bus=blue; van=black;
cout<<”Car is “<<car<<", bus is " << bus << “and van is “<<van<<endl;
В результате выполнения программы появится строка:
Car is 11, bus is 100 and van is 102
Константе red явно присвоено значение 10, поэтому константа green имеет значение 11
(на единицу больше). Для константы blue явно указано значение 100, поэтому константы
yellow и black имеют значения 101 и 102 соответственно.
Воспользовавшись объявлением вида typedef тип новое_имя_типа, в программе
создают новое имя для типа данных.
typedef int* IntPointer;
int n=100;
IntPointer p;
p=&n;
(*p)++;
cout << "n="<<n<<endl;
Командой typedef int * IntPointer объявлено новоеимя IntPointer для данных
“указатель на целое число”. Впоследствии идентификатор IntPointer используется при
объявлении новых переменных, причем результат, например, команды IntPointer * р,
как если бы это было объявление int * р. В результате выполнения команды появится
сообщение n = 101.