Студопедия

КАТЕГОРИИ:


Архитектура-(3434)Астрономия-(809)Биология-(7483)Биотехнологии-(1457)Военное дело-(14632)Высокие технологии-(1363)География-(913)Геология-(1438)Государство-(451)Демография-(1065)Дом-(47672)Журналистика и СМИ-(912)Изобретательство-(14524)Иностранные языки-(4268)Информатика-(17799)Искусство-(1338)История-(13644)Компьютеры-(11121)Косметика-(55)Кулинария-(373)Культура-(8427)Лингвистика-(374)Литература-(1642)Маркетинг-(23702)Математика-(16968)Машиностроение-(1700)Медицина-(12668)Менеджмент-(24684)Механика-(15423)Науковедение-(506)Образование-(11852)Охрана труда-(3308)Педагогика-(5571)Полиграфия-(1312)Политика-(7869)Право-(5454)Приборостроение-(1369)Программирование-(2801)Производство-(97182)Промышленность-(8706)Психология-(18388)Религия-(3217)Связь-(10668)Сельское хозяйство-(299)Социология-(6455)Спорт-(42831)Строительство-(4793)Торговля-(5050)Транспорт-(2929)Туризм-(1568)Физика-(3942)Философия-(17015)Финансы-(26596)Химия-(22929)Экология-(12095)Экономика-(9961)Электроника-(8441)Электротехника-(4623)Энергетика-(12629)Юриспруденция-(1492)Ядерная техника-(1748)

Лекция 8. Если описатели переменных отсутствуют, то структура определяет новый тип, например:




Struct

Struct

Struct

{

char fio[30].

int date, code;

double salary;

} stuff[100], *ps;

Если описатели переменных отсутствуют, то структура определяет новый тип, например:

struct Worker

{

char fio[30];

int date, code;

double salary;

};

Worker stuff[100], *ps;

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

struct List; // объявление структуры List

struct Link

{

List *p; // указатель на структуру List

Link *prev, *succ; // указатели на структуру Link

};

struct List { /* определение структуры List */};

Это позволяет создавать связные списки структур.

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

{

char fio[30];

int date, code;

double salary;

} worker = {“Страусенко”, 31, 215, 3400.55};

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

Доступ к полям структурывыполняется с помощью операций выбора «.» (точка, применяется к имени структуры) и –> (применяется к указателю на структуру). Например:

Worker worker, stuff[100], *ps;

ps = &worker;

strcpy (worker.fio, “Страусенко”); stuff[8].code = 215; ps->salary = 0.12;

 

4.7.4. Битовые поля

 

Битовые поля – это особый вид полей структуры. Они используются для плот­ной упаковки данных, например, флажков типа «да/нет». Минимальная адресуе­мая ячейка памяти – 1 байт, а для хранения флажка достаточно одного бита. При описании битового поля после имени через двоеточие указывается длина поля в битах (целая положительная константа):

struct Options

{

bool centerX: l:

bool centerY: l;

unsigned int shadow: 2;

unsigned int palette: 4;

};

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

 

4.7.5. Объединения

 

Объединение представляет собой специальный вид структуры, все поля которой располагаются по одному и тому же адресу. Формат его описания такой же, только вместо ключевого слова struct используется union. Длина объединения равна наибольшей из длин его полей. В переменной типа объединение хранится только одно значение, и ответст­венность за его правильное использование лежит на программисте.

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

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

#include <iostream.h>

int main()

{

enum Paytype {CARD, CHECK};

{

Paytype ptype;

union { char card[25]; long check; };

} info;

………………………………………………………

switch (info.ptype)

{

case CARD: cout << “Оплата по карте: “ << info.card; break;

case CHECK: cout << “Оплата чеком: “ << info.check; break;

}

return 0;

}

По сравнению со структурами на объединения налагаются некоторые ограниче­ния:

· объединение может инициализироваться только значением его первого элемента;

· объединение не может содержать битовые поля;

· объединение не может содержать виртуальные методы, конструкторы, деструкторы и операцию присваивания;

· объединение не может входить в иерархию классов.

Смысл двух последних из них разъяснится позднее.

 

4.8. Модульное программирование

 

Естественный путь решения сложной задачи – ее разбиение на относительно независимые части, которые можно решать отдельно и даже при участии разных специалистов. В C++ программа может быть разделена на простые и обозримые части с помощью функций, после чего программу можно рассмат­ривать в укрупненном виде – на уровне их взаимодействия. Разделение на функции позволяет также избежать избыточности ко­да, поскольку функцию записывают один раз, а обращаться к ней мож­но многократно. Часто используемые функции можно помещать в библиотеки.

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

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

Разделение сложной программы на максимально обособленные части – слож­ная задача, решаемая на этапе проектирования программы.

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

Модульность в языке C++ поддерживается с помощью функций, директив препроцессора, пространств имен, классов памяти, исключений и раздельной компиляции.

 

4.8.1. Функции

 

4.8.1.1. Объявление и определение

 

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

Любая программа на C++ состоит из функций, одна из которых называется main (с нее начинается выполнение программы). Функция начинает выпол­няться в момент вызова. Любая функция должна быть объявлена и определена. Как и для других элементов программы, объявлений функции может быть несколько, а определение только одно. Объявление должно находиться в тексте раньше вызова функции.

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

[класс] [inline] тип имя ([список_параметров]) [throw(исключения)] {тело функции}

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

· extern – глобальная видимость во всех модулях (по умолча­нию);

· static – видимость в пределах модуля, в котором определена функция.

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

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

Функцию можно определить как встроенную с помощью модификатора inline. Он рекомендует компилятору все обращения к функции замещать непосредственно ее кодом. Это обычно применяется для коротких функций, для снижения на­кладных расходов на вызов. Директива inline носит рекомендательный характер и выполняется компилятором по мере возможности. Определение inline-функции должно предше­ствовать ее вызовам, иначе компилятор генерирует ее обычные вызовы.

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

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

# include <iostream.h>

int sum (int a, int b); // Объявление функции

int main ()

{

int a = 2, b = 3, c, d;

c = sum (a, b); // Вызов функции

cin >> d; cout << sum (c, d);

return 0;

}

int sum (int a, int b) { return (a + b); } // Определение функции

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

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

Механизм возврата из функции реализуется опера­тором

return [ выражение ];

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

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

 

4.8.1.2. Передача параметров

 

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

# include <iostream.h>

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

int main ()

{

int i = l, j = 2, k=3;

cout << “i j k\n”:

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 – по адресу с по­мощью указателя, к – по адресу с помощью ссылки.

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

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

int f (const char *);

char * t (char *a, const int *b);

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

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

# include <iostream.h>

int sum (const int *mas, const int n); // mas – указатель на неизменяемые данные

const int 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 s = 0;

for (int i = 0; i < n; i++) s += mas[i];

return s;

}

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

# include <iostream.h>

typedef void (*PF) (int); // PF – тип указателя на void-функцию c целым параметром

void fl (PF pf) { pf (5); } // pf – формальный параметр этого типа

void f (int i) {cout << i;}

int main ()

{

fl (f); return 0; // Можно указать fl (&f) – работает аналогично

}

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

int f (int a, int b = 0);

void f1 (int, int = 100, char * = 0); // Важен пробел между * и =

void err (int errValue = errno); // errno - глобальная переменная

f (100); f (a, 1); // варианты вызова функции f

f1(a); f1(a, 10); f1(a, 10, "Vasia"); // варианты вызова функции f1

 

4.8.1.3. Рекурсия

 

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

long fact (long n)

{

if (n == 0 || n == l) return 1;

else return (n * fact(n – 1));

}

Любую функцию C++ можно реализовать без применения рекурсии. Досто­инством рекурсии является компактная запись, а недостатками – расход време­ни и памяти на повторные вызовы функции и передачу ей копий параметров, и соответственно опасность переполнения стека.

 

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

 

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

int max (int, int);

char * max (char *, char *);

Если соответствие может быть получено более чем одним способом, вызов считается неоднозначным и выдается сообщение об ошибке. Функции не могут быть перегруженными, если описание их параметров отлича­ется только модификатором const или использованием ссылки.

 

4.8.1.5. Шаблоны функций

 

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

Формат простейшей функции-шаблона таков:

template < typename Type> заголовок { тело функции }

Слово typename без изменения смысла может быть заменено на class. Вместо слова Туре может использоваться произвольное имя.

В общем случае шаблон функции содержит несколько параметров, каж­дый из которых может быть как типом, так и просто переменной, напри­мер:

template < class A, class В, int i> void f () { }

Например, функция сортировки массива n элементов любого типа в виде шаблона может выглядеть так:

template < typename Type>

void sort (Type *b, int n)

{

Type а; // Промежуточная переменная для обмена элементов

for (int i = 0; i < n - 1; i++)

{

int kMin = i;

for (int j = i + 1; j < n; j++) if (b[j] < b[kMin]) kMin = j;

а = b[i]; b[i] = b[kMin]; b[kMin] = a;

}

}

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

# include <iostream.h>

template < typename Type> void sort (Type *b, int n);

int main ()

{

const int n = 20;

int i, b[n];

for (i = 0; i < n; i++) cin >> b[i];

sort (b, n); // Сортировка целочисленного массива, можно sort < int > (b, n);

for (i = 0; i < n; i++) cout << b[i] << ' ';

cout << ‘\n’;

double a [] = {0.22, 117, -0.08, 0.21, 42.5};

sort (a, 5); // Сортировка массива вещественных чисел, можно sort < double > (a, 5);

for (i = 0; i < 5; i++) cout << a[i] << ' ';

return 0;

}

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

template < class X, class Y, class Z> void f (Y, Z);

void g ()

{

f < int, char *, double > ("Vasia", 3.0);

f < int, char *> ("Vasia", 3.0); // Z определяется как double, т.к. 3.0 - double

f < int > ("Vasia", 3.0); // Y определяется как char*, a Z - как double

// f (“Vasia”, 3.0); Ошибка: Х определить невозможно

}

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

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

void sort < int > (int *b, int n)

{... // Тело специализированного варианта функции }




Поделиться с друзьями:


Дата добавления: 2014-01-06; Просмотров: 384; Нарушение авторских прав?; Мы поможем в написании вашей работы!


Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет



studopedia.su - Студопедия (2013 - 2024) год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! Последнее добавление




Генерация страницы за: 0.09 сек.