КАТЕГОРИИ: Архитектура-(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) |
Параметризованные типы и функции.(2 час.)
Заголовочные файлы. Предопределенные объекты и потоки. Операции помещения и извлечения. Форматирование. Флаги форматирования. Манипуляторы. Ошибки потоков. Файловый ввод-вывод с применением потоков С++. Конструкторы файловых потоков. Открытие файлов в разных режимах. Ввод-вывод в файлы. Заголовочные файлы. В Си имеется общая технология, которая касается как организации модульных программ, так и библиотек. Любой модуль, который претендует на дальнейшее использование через обращение к собственным внешним переменным и вызов собственных внешних функций, должен иметь некоторое описание своего интерфейса. Оно заключается в составлении заголовочного файла (файла с расширением - ".h"), который используется другими модулями. Заголовочный файл должен включать в себя: · определение используемых типов данных в формальных параметрах и результатах функций с использованием оператора typedef; · объявления внешних переменных и функций модуля, к которым возможно обращение. При помощи директивы #include текст заголовочного файла включается в текст транслируемого модуля, таким образом транслятор получает необходимые определения типов и объявления переменных и функций модуля, к которым будут производиться обращения. Директива #include возможна в двух вариантах: #include <alloc.h> - заголовочный файл из системного каталога #include "myhead.h" - заголовочный файл из текущего (явно указанного) каталога Процесс подготовки библиотеки включает в себя следующие шаги · создание заголовочного файла, содержащего определения используемых типов данных и объявления внешних функций и переменных библиотеки; · создание модуля, включающего определение функций и переменных библиотеки и трансляция его в объектный модуль; · включение объектного модуля в библиотеку. Предопределенные объекты и потоки. В стандартной библиотеке ввода/вывода стандартного Си (заголовочный файл библиотеки - <stdio.h>) имеются внешние переменные-указатели на дескрипторы файлов - стандартных устройств ввода-вывода. extern FILE *stdin, *stdout, *stderr, *stdaux, *stdprn; стандартный ввод стандартный вывод регистрация ошибок дополнительное устройство устройство печати Эти файлы открываются библиотекой автоматически перед выполнением функции main и по умолчанию назначаются на терминал (stdin - клавиатура, stdout, stderr - экран), последовательный порт (stdaux) и принтер (stdprn). stdin и stdout могут быть переназначены в командой строке запуска программы на любые другие файлы >test.exe <a.dat >c:\xxx\b.dat файл stdout файл stdin В Си++ существуют классы потоков ввода-вывода, которые являются объектно-ориентированным эквивалентом (stream.h) стандартной библиотеки ввода-вывода (stdio.h): ios базовый потоковый класс streambuf буферизация потоков istream потоки ввода ostream потоки вывода iostream двунаправленные потоки iostream_withassign поток с переопределенной операцией присваивания istrstream строковые потоки ввода ostrstream строковые потоки вывода strstream двунаправленные строковые потоки ifstream файловые потоки ввода ofstream файловые потоки вывода fstream двунаправленные файловые потоки Стандартные потоки (istream,ostream,iostream) служат для работы с терминалом. Строковые потоки (istrstream, ostrstream, strstream) служат для ввода-вывода из строковых буферов, размещенных в памяти. Файловые потоки (ifstream, ofstream, fstream) служат для работы с файлами. Следующие объекты-потоки заранее определены и открыты в программе перед вызовом функции main: extern istream cin; // Стандартный поток ввода с клавиатуры extern ostream cout; // Стандартный поток вывода на экран extern ostream cerr; // Стандартный поток вывода сообщений об ошибках (экран) extern ostream cerr;// Стандартный буферизованный поток вывода сообщений об ошибках (экран). Операции помещения и извлечения. Для начала рассмотрим пример: #include <stream.h> main() { cout << "Hello, world\n"; } Строка #include <stream.h> сообщает компилятору, чтобы он включил стандартные возможности потока ввода и вывода, находящиеся в файле stream.h. Без этих описаний выражение cout << "Hello, world\n" не имело бы смысла. Операция << ("поместить в") пишет свой первый аргумент во второй (в данном случае, строку "Hello, world\n" в стандартный поток вывода cout). Программирующим на C << известно как операция сдвига влево для целых. Такое использование << не утеряно, просто в дальнейшем << было определено для случая, когда его левый операнд является потоком вывода. Ввод производится с помощью операции >> ("извлечь из") над стандартным потоком ввода cin. Описания cin и >>, конечно же, находятся в <stream.h>. Операцию вывода << можно применять к ее собственному результату, так что несколько команд вывода можно записать одним оператором: cout << inch << " in = " << inch*2.54 << " cm\n"; Операция вывода используется, чтобы избежать той многословности, которую дало бы использование функции вывода. Но почему <<? Возможности изобрести новый лексический символ нет. Операция присваивания была кандидатом одновременно и на ввод, и на вывод, но оказывается, большинство людей предпочитают, чтобы операция ввода отличалась от операции вывода. Кроме того, = не в ту сторону связывается (ассоциируется), то есть cout=a=b означает cout=(a=b). Делались попытки использовать операции < и >, но значения "меньше" и "больше" настолько прочно вросли в сознание людей, что новые операции ввода/вывода во всех реальных случаях оказались нечитаемыми. Помимо этого, "<" находится на большинстве клавиатур как раз на ",", и у людей получаются операторы вроде такого: cout < x, y, z; Для таких операторов непросто выдавать хорошие сообщения об ошибках. Операции << и >> к такого рода проблемам не приводят, они асимметричны в том смысле, что их можно проассоциировать с "в" и "из", а приоритет << достаточно низок, чтобы можно было не использовать скобки для арифметических выражений в роли операндов. Например: cout << "a*b+c=" << a*b+c << "\n"; Естественно, при написании выражений, которые содержат операции с более низкими приоритетами, скобки использовать надо. Например: cout << "a^b|c=" << (a^b|c) << "\n"; Операцию левого сдвига тоже можно применять в операторе вывода: cout << "a<<b=" << (a<<b) << "\n"; В С++ нет выражений с символьными значениями, в частности, '\n' является целым (со значением 10, если используется набор символов ASCII), поэтому cout << "x = " << x << '\n'; напечатает число 10, а не ожидаемый символ новой строки. Эту и аналогичные проблемы можно сгладить, определив несколько макросов (в которых используются стандартные имена символов ASCII): #define sp << " " #define ht << "\t" #define nl << "\n" Теперь предыдущий пример запишется в виде: cout << "x = " << x nl; Для печати символов предоставляются функции ostream::put(char) и chr(int). Рассмотрим примеры: cout << x << " " << y << " " << z << "\n"; cout << "x = " << x << ", y = " << y << "\n"; Люди находят их трудно читаемыми из-за большого числа кавычек и того, что операция вывода внешне выглядит слишком непривычно. Здесь могут помочь приведенные выше макросы и несколько отступов: cout << x sp << y sp << z nl; cout << "x = " << x << ", y = " << y nl; Форматирование. Флаги форматирования. Пока << применялась только для неформатированного вывода, и в реальных программах она именно для этого главным образом и применяется. В то же время << легко справляется с тремя стандартными типами данных: char, int, float.В резудьтате переопределения операция помещения в поток определяет тип посланных данных и сама выбирает подходящий формат. То же самое происходит и с операцией извлечения из потока >>, которая вводит данные. Сравним пример на Си: scanf("%d%f%c", &int_data, &float_data, &char_data); с его эквивалентом на Си++: cin >> int_data >> float_data >> char_data; В Си++ не нужно заботиться о том, чтобы ставить перед переменными знак адреса &. Операция извлечения из потока сама вычисляет адреса переменных, определяет их типы и формат ввода. Помимо этого существует также несколько форматирующих функций, создающих представление своего параметра в виде строки, которая используется для вывода. Их второй (необязательный) параметр указывает, сколько символьных позиций должно использоваться. char* oct(long, int=0); // восьмеричное представление char* dec(long, int=0); // десятичное представление char* hex(long, int=0); // шестнадцатеричное представление char* chr(int, int=0); // символ char* str(char*, int=0); // строка Если не задано поле нулевой длины, то будет производиться усечение или дополнение; иначе будет использоваться столько символов (ровно), сколько нужно. Например: cout << "dec(" << x << ") = oct(" << oct(x,6) << ") = hex(" << hex(x,4) << ")"; Если x==15, то в результате получится: dec(15) = oct(17) = hex(f); Можно также использовать строку в общем формате: char* form(char* format...); cout<<form() эквивалентно применению стандартной функции вывода языка C printf()*. form() возвращает строку, получаемую в результате преобразования и форматирования ее параметров, которые стоят после первого управляющего параметра - строки формата format. Строка формата состоит из объектов двух типов: обычных символов, которые просто копируются в поток вывода, и спецификаций преобразования, каждая из которых влечет преобразование и печать следующего из параметров. Каждая спецификация преобразования начинается с символа %. Например: cout<<form("there were %d members present",no_of_members); Здесь %d указывает, что no_of_members должно рассматриваться как int и печататься в виде соответствующей последовательности десятичных цифр. Если no_of_members==127, вывод будет такой: there were 127 members present Множество спецификаций преобразования довольно велико и обеспечивает высокую степень гибкости. После % может стоять: · необязательный знак минус, который задает выравнивание преобразованного значения влево в указанном поле; · d необязательная строка цифр, задающая ширину поля. Если преобразованное значение имеет меньше цифр, чем ширина поля, оно будет дополняться пробелами слева (или справа, если был задан индикатор выравнивания влево) до заполнения всей ширины поля; если ширина поля начинается с нуля, то вместо дополнения пробелами будет делаться дополнение нулями; · необязательная точка, для отделения ширины поля от следующей строки цифр; · d необязательная строка цифр, специфицирующая точность, которая задает число цифр после десятичной точки для преобразований e и f или печатаемых символов для строки; · * в ширине поля или точности вместо строки цифр может стоять *. В этом случае ширина поля и точность задается целым параметром; · h необязательный символ h; указывает на то, что идущие за ним d, o, x или y соответствуют параметру короткое целое; · l необязательный символ l; указывает на то, что идущие за ним d, o, x или y соответствуют параметру длинное целое; · % указывает, что должен быть напечатан символ %, никакие параметры при этом не затрагиваются; · c символ, указывающий, какой тип преобразования должен применяться. Символы преобразования и их значения таковы: § d целый параметр преобразуется в десятичную запись; § o целый параметр преобразуется в восьмеричную запись; § x целый параметр преобразуется в шестнадцатиричную запись; § f параметр float или double преобразуется в десятичную запись вида [-]ddd.ddd, где число, задаваемое цифрами d после десятичной точки, эквивалентно спецификации точности для параметра. Если точность опущена, дается шесть цифр; § если точность явно задана как 0, то не печатается десятичная точка и не печатается ни одной цифры; § e параметр float или double преобразуется в десятичную запись вида [-]d.ddde+dd, где перед десятичной точкой стоит одна цифра, а число, задаваемое цифрами после десятичной точки, эквивалентно спецификации точности для параметра; § когда точность опущена, выдается шесть цифр; § g параметр float или double печатается в том из видов d,f или e, который обеспечивает полную точность при минимальной затрате места; § c печатается символьный параметр, пустые символы игнорируются; § s параметр воспринимается как строка (указатель на символ), и печатаются символы из строки до пустого символа или до тех пор, пока не будет достигнуто число символов, указанное спецификацией точности; но если точность равна нулю, печатаются все символы до пустого; § u беззнаковый целый параметр преобразуется в десятичную запись. Несуществующая или недостаточная ширина поля никогда не приводит к обрезанию поля; дополнение поля записи имеет место только в том случае, если указанная ширина поля превышает фактическую ширину. Вот более сложный пример: char* src_file_name; int line; char* line_format = "\n#line %d \"%s\"\n"; //... cout << "int a;\n"; cout << form(line_format,line,src_file_name); cout << "int b;\n"; который печатает int a;
#line 13 "С++/main.c" int b;
Применение form() небезопасно в смысле того, что не выполняется проверка типа. Вот, например, хорошо хорошо известный способ получить непредсказуемый вывод и/или дамп (core dump): char x; //... cout<<form("bad input char: %s",x); Правда, она дает большую гибкость в том виде, который хорошо знаком программистам на C. Потоковый вывод можно смешивать с выводом в стиле printf(). В настоящее время нет полностью удовлетворительных средств, обеспечивающих форматированный вывод типов, определяемых пользователем. Вполне осуществимый, но не идеальный подход состоит в том, чтобы снабжать определяемый пользователем тип функциями, которые порождают соответствующее строковое представление объекта, для которого они вызываются, аналогично форматирующим функциям oct(), hex() и т.д. Например: class complex { float re,im; public: //... char* string(char* format) { return form(format,re,im); } }; //... cout << z.string("(%.3f,%.3f)"); Память для хранения строк, которые возвращают form(), hex() и т.п., берется из одного статически размещаемого циклического буфера, поэтому не имеет смысла сохранять указатель, возвращаемый любой из этих функций, для дальнейшего использования. Указываемые символы будут меняться. Манипуляторы. Манипуляторы - функции потока, которые можно включать в операции помещения и извлечения в потоки (<<, >>). Имеются следующие манипуляторы: endl // Помещение в выходной поток символа конца строки '\n' и вызов функции flush ends // Помещение в выходной поток символа '\0' flush // Вызов функции вывода буферизованных данных в выходной поток dec // Установка основания 10 системы счисления hex // Установка основания 16 системы счисления oct // Установка основания 8 системы счисления ws // Установка игнорирования при вводе пробелов setbase(int) // Установка основания системы счисления (0 - 10 - по умолчанию, также 8,10,16) resetiosflasg(long) // Сброс флагов форматирования по маске setiosflags(long) // Установка флагов форматирования по маске setfill(int) // Установка заполняющего символа setprecision(int) // Установка точности вывода вещественных чисел setw(int) // Установка ширины поля ввода-вывода Пример вызова манипулятора: cout << 15 << hex << 15 << setbase(8) << 15; Ошибки потоков. Каждый поток (istream или ostream) имеет ассоциированное с ним состояние, и обработка ошибок и нестандартных условий осуществляется с помощью соответствующей установки и проверки этого состояния. Поток может находиться в одном из следующих состояний: enum stream_state { _good, _eof, _fail, _bad }; Если состояние _good или _eof, значит последняя операция ввода прошла успешно. Если состояние _good, то следующая операция ввода может пройти успешно, в противном случае она закончится неудачей. Другими словами, применение операции ввода к потоку, который не находится в состоянии _good, является пустой операцией. Если делается попытка читать в переменную v, и операция оканчивается неудачей, значение v должно остаться неизменным (оно будет неизменным, если v имеет один из тех типов, которые обрабатываются функциями членами istream или ostream). Отличие между состояниями _fail и _bad очень незначительно и представляет интерес только для разработчиков операций ввода. В состоянии _fail предполагается, что поток не испорчен и никакие символы не потеряны. В состоянии _bad может быть все что угодно. Состояние потока можно проверять например так: switch (cin.rdstate()) { case _good: // последняя операция над cin прошла успешно break; case _eof: // конец файла break; case _fail: // некоего рода ошибка форматирования, возможно, не слишком плохая break; case _bad: // возможно, символы cin потеряны break; } Для любой переменной z типа, для которого определены операции << и >>, копирующий цикл можно написать так: while (cin>>z) cout << z << "\n"; Например, если z - вектор символов, этот цикл будет брать стандартный ввод и помещать его в стандартный вывод по одному слову (то есть, последовательности символов без пробела) на строку. Когда в качестве условия используется поток, происходит проверка состояния потока, и эта проверка проходит успешно (то есть, значение условия не ноль) только если состояние _good. В частности, в предыдущем цикле проверялось состояние istream, которое возвращает cin>>z. Чтобы обнаружить, почему цикл или проверка закончились неудачно, можно исследовать состояние. Делать проверку на наличие ошибок после каждого ввода или вывода действительно не очень удобно, и обычно источником ошибок служит программист, не сделавший этого в том месте, где это существенно. Например, операции вывода обычно не проверяются, но они могут случайно не сработать. Парадигма потока ввода/вывода построена так, чтобы когда в С++ появится (если это произойдет) механизм обработки исключительных ситуаций (как средство языка или как стандартная библиотека), его будет легко применить для упрощения и стандартизации обработки ошибок в потоках ввода/вывода. Файловый ввод-вывод с применением потоков С++. Конструкторы файловых потоков. Потоки обычно связаны с файлами. Библиотека потоков создает стандартный поток ввода cin, стандартный поток вывода cout и стандартный поток ошибок cerr. Программист может открывать другие файлы и создавать для них потоки. Для инициализации потоков вывода ostream имеет конструкторы: class ostream { //... ostream(streambuf* s); // связывает с буфером потока ostream(int fd); // связывание для файла ostream(int size, char* p); // связывает с вектором }; Главная работа этих конструкторов - связывать с потоком буфер. streambuf - класс, управляющий буферами, как и класс filebuf, управляющий streambuf для файла. Класс filebuf является производным от класса streambuf. Естественно, тип istream, так же как и ostream, снабжен конструктором: class istream { //... istream(streambuf* s, int sk =1, ostream* t =0); istream(int size, char* p, int sk =1); istream(int fd, int sk =1, ostream* t =0); }; Параметр sk задает, должны пропускаться пропуски или нет. Параметр t (необязательный) задает указатель на ostream, к которому прикреплен istream. Например, cin прикреплен к cout; это значит, что перед тем, как попытаться читать символы из своего файла, cin выполняет cout.flush(); - пишет буфер вывода С помощью функции istream::tie() можно прикрепить (или открепить, с помощью tie(0)) любой ostream к любому istream. Например: int y_or_n(ostream& to, istream& from) /*"to", получает отклик из "from" */ { ostream* old = from.tie(&to); for (;;) { cout << "наберите Y или N: "; char ch = 0; if (!cin.get(ch)) return 0; if (ch!= '\n') { // пропускает остаток строки char ch2 = 0; while (cin.get(ch2) && ch2!= '\n'); } switch (ch) { case 'Y': case 'y': case '\n': from.tie(old); // восстанавливает старый tie return 1; case 'N': case 'n': from.tie(old); // восстанавливает старый tie return 0; default: cout << "извините, попробуйте еще раз: "; } } } Когда используется буферизованный ввод (как это происходит по умолчанию), пользователь не может набрав только одну букву ожидать отклика. Система ждет появления символа новой строки. y_or_n() смотрит на первый символ строки, а остальные игнорирует. Символ можно вернуть в поток с помощью функции istream::putback(char). Это позволяет программе "заглядыватьвперед" в поток ввода. Деструктор для ostream сбрасывает буфер с помощью открытого члена функции ostream::flush(): ostream::~ostream() { flush(); // сброс } Сбросить буфер можно также и явно. Например: cout.flush(); Открытие файлов в разных режимах. Точные детали того, как открываются и закрываются файлы, различаются в разных операционных системах. Поскольку после включения <stream.h> становятся доступны cin, cout и cerr, во многих (если не во всех) программах не нужно держать код для открытия файлов. Вот, однако, программа, которая открывает два файла, заданные как параметры командной строки, и копирует первый во второй: #include <stream.h> void error(char* s, char* s2) { cerr << s << " " << s2 << "\n"; exit(1); } main(int argc, char* argv[]) { if (argc!= 3) error("неверное число параметров",""); filebuf f1; if (f1.open(argv[1],input) == 0) error("не могу открыть входной файл",argv[1]); istream from(&f1); filebuf f2; if (f2.open(argv[2],output) == 0) error("не могу создать выходной файл",argv[2]); ostream to(&f2); char ch; while (from.get(ch)) to.put(ch); if (!from.eof()!! to.bad()) error("случилось нечто странное",""); } Последовательность действий при создании ostream для именованного файла та же, что используется для стандартных потоков: (1) сначала создается буфер (здесь это делается посредством описания filebuf); (2) затем к нему подсоединяется файл (здесь это делается посредством открытия файла с помощью функции filebuf::open()); (3) создается сам ostream с filebuf в качестве параметра. Потоки ввода обрабатываются аналогично. Файл может открываться в одной из двух мод: enum open_mode { input, output }; Действие filebuf::open() возвращает 0, если не может открыть файл в соответствие с требованием. Если пользователь пытается открыть файл, которого не существует для output, он будет создан. Используя классы ifstream и ofstream - производные от istream и ostreasm, описанные в fstream.h, можно открывать файловые потоки в разных модах с помощью флагов конструктора потока: ofstream object (filename, flag) где flag может иметь следующие значения: ios::app запись в конец существующего файла ios::ate после открытия файла перейти в его конец ios::binary открыть файл в двоичном режиме (по умолчанию - текстовый) ios::in открыть для чтения ios::nocreate сообщать о невозможности открытия, если файл не существует ios::noreplace сообщать о невозможности открытия, если файл существует ios::out открыть для вывода ios::trunc если файл существует, стереть содержимое. При необходимости изменить способ открытия или применения файла можно при создании файлового потока использовать два флага или более флага: ios::app|ios::noreplace Для открытия файла одновременно на чтение и запись можно использовать объекты класса fstream: fstream object(filename, ios::in|ios::app); Перед завершением программа проверяет, находятся ли потоки в приемлемом состоянии. При завершении программы открытые файлы неявно закрываются. Для явного закрытия объектов файловых потоков применяется метод close(): object.close(); Ввод-вывод в файлы. Для ввода/вывода в потоковые объекты можно применять методы put(), get(), для связывания объекта с различными файлами служат методы open(), close(), для позиционирования в файле имеются методы seekg(), seekp(), tellp(). При этом seekg() назначает или возвращает текущую позицию указателя чтения, а seekp() назначает или возвращает текущую позицию указателя записи. Обе функции могут иметь один или два аргумента. При вызове с одним аргументом функции перемещают указатель на заданное место, а при вызове с двумя аргументами вычисляется относительная позиция от начала файла (ios::beg), текущей позиции (ios::cur) или от конца файла (ios::end). Текущщая позиция определяется методом tellp(). Для объектов файловых потоков контроль состояния также производится с помощью методов, манипулирующих флагами ошибок: bad() возвращает ненулевое значение, если обнаружена ошибка clear() сбрасывает сообщения об ошибках eof() возвращает ненулевое значение, если обнаружен конец файла fail() возвращает ненулевое значение, если операция завершилась неудачно good() возвращает ненулевое значение, если флаги ошибок не выставлены rdstate() возвращает текущее состояние флагов ошибок. Если флаги показывают наличие ошибки, все попытки поместить в поток новые объекты будут игнорироваться, то есть состояние потока не изменится. Шаблоны функций. Параметры шаблонов. Шаблоны классов. Наследование и шаблоны. Шаблоны функций. Шаблоны, которые называют иногда родовыми или параметризоавнными типами, позволяют создавать (конструировать) семейства родственных функций и классов. Цель введения шаблонов функций - автоматизация создания функций, которые могут обрабатывать разнотипные данные. В отличие от механизма перегрузки, когда для каждого набора формальных параметров определяется своя функция, шаблон семейства функций определяется один раз, но это определение параметризуется. Параметризовать в шаблоне функций можно тип возвращаемого функцией значения и типы любых параметров, количество и порядок размещения которых должны быть фиксированы. Для параметризации используется список параметров шаблона. В определении шаблона семейства функций используется служебное слово template. Для параметризации используется список формальных параметров шаблона, который заключается в угловые скобки <>. Каждый формальный параметр шаблона обозначается служебным словом class, за которым следует имя параметра (идентификатор). Пример определения шаблона функций, вычисляющих абсолютные значения числовых величин разных типов: template <class type> type abs (type x) { return x > 0? x: -x;} Шаблон семейства фукций состоит из двух частей - заголовка шаблона: template <список_параметров_шаблона> и из обыкновенного определения функции, в котором тип возвращаемого значения и типы любых параметров обозначаются именами параметров шаблона, введенных в его заголовке. Те же имена параметров шаблона могут использоваться и в теле определения функции для обозначения типов локальных объектов. В качестве еще одного примера рассмотрим шаблон семейства функций для обмена значений двух передаваемых им параметров. template <class T> void swap (T* x, T* y) { T z = *x; *x = *y; *y = x; } Здесь параметр T шаблона функций используется не только в заголовке для спецификации формальных параметров, но и в теле определения функции, где он задает тип вспомогательной переменной z. Шаблон семейства функций служит для автоматического формирования конкретных определений функций по тем вызовам, которые транслятор обнаруживает в теле программы. Например, если программист употребляет обращение abs(-10.3), то на основе приведенного ранее шаблона компилятор сформирует такое определение функции: double abs (double x) {return x > 0? x: -x;} Далее будет организовано выполнение именно этой функции и в точку вызова в качестве результата вернется числовое значение 10.3. Если в программе присутствует приведенный ранее шаблон семейства функций swap() и появится последовательность операторов: long k = 4, d = 8, swap (&k, &d); то компилятор сформирует определение функции: void swap (long* x, long* y) { long x = *x; *x = *y; *y = x; } Затем будет выполнено обращение именно к этой функции и значения переменных k, d поменяются местами. Если в той же программе присутствуют операторы: double a = 2.44, b = 66.3; swap (&a, &b); то сформируется и выполнится функция void swap (double* x, double* y) { double x = *x; *x = *y; *y = x; } Проиллюстрируем сказанное о шаблонах на более конкретном примере. Рассмотрим программу, используем некоторые возможности функций, возвращающих значение типа «ссылка». Но тип ссылки будет определяться параметром шаблона: #include <iostream.h> //Функция определяет ссылку на элемент с максимальным значением templat <class type> type& rmax (int n, type d[]) { int im = 0; for (int i = 1; i < n; i++) im = d[im] > d[i]? im: i; return d[im]; } void main () { int n = 4; int x[] = { 10, 20, 30, 14}; //Массив целых чисел cout << "\nrmax(n,x) = " << rmax (n,x); // rmax(n,x) = 30 rmax(n,x) = 0; for (int i = 0; i < n; i++) cout << "\tx[" << i << "] =" << x[i]; // x[0] = 10 x[1]... float arx[] = { 10.3, 20.4, 10.5}; //Массив вещественных чисел cout << "\nrmax(3,arx) = " << rmax (3,arx); //rmax(3,arx) = 20.4 rmax(3, arx) = 0; for (int i = 0; i < 3; i++) cout << "\tarx[" << i << "] =" << arx[i]; //arx[0] = 10.3... } В программе используются два разных обращения к функции rmax(). В одном случае параметр - целочисленный массив и возвращаемое значение - ссылка типа int. Во втором случае фактический параметр - имя массива типа float и возвращаемое значение имеет тип ссылки на float. По существу механизм шаблонов функций позволяет автоматизировать подготовку переопределений перегруженных функций. При использовании шаблонов уже нет необходимости готовить заранее все варианты функций с перегруженным именем. Компилятор автоматически,анализируя вызовы функций в тексте программы, формирует необходимые определения именно для таких типов параметров, которые использованы в обращениях. Дальнейшая обработка выполняется так же, как и для перегруженных функций. Параметры шаблонов. Можно считать, что параметры шаблона являются его формальными параметрами, а типы тех параметров, которые используются в конкретных обращениях к функции, служат фактическими параметрами шаблона. Именно по ним выполняется параметрическая настройка и с учетом этих типов генерируется конкретный текст определения функции. Однако, говоря о шаблоне семейства функций, обычно употребляют термин «список параметров шаблона», не добавляя определения «формальных». Перечислим основные свойства параметров шаблона: 1. Имена параметров шаблона должны быть уникальными во всем определении шаблона. 2. Список параметров шаблона функции не может быть пустым, так как при этом теряется возможность параметризации и шаблон функций становится обычным определением конкретной функции. 3. В списке параметров шаблона функций может быть несколько параметров. Каждый из них должен начинаться со служебного слова class. Например, допустим такой заголовок шаблона: template <class type1, class type2> Соответственно, неверен заголовок: template <class type1, class type2> 4. Недопустимо использовать в заголовке шаблона параметры с одинаковыми именами, то есть ошибочен такой заголовок: 5. Имя параметра шаблона (в примерах - type1, type2) имеет в определяемой шаблоном функции все права имени типа, то есть с его помощью могут специализироваться формальные параметры, определяться тип возвращаемого функцией значения и типы любых объектов, локализованных в теле функции. Имя параметра шаблона видно во всем определении и скрывает другие использования того же идентификатора в области, глобальной по отношению к данному шаблону функций. Если внутри тела определяемой функции необходим доступ к внешним объектам с тем же именем, нужно применять операцию изменения области видимости. Следующая программа иллюстрирует указанную особенность имени параметра шаблона функций: #include <iostream.h> int N; //статическая, инициализирована нулем template <class N> N max (N x, N y) { N a = x; cout << "\nСчетчик обращений N = " << ++::N; if (a < y) a = y; return a: } void main () { int a = 12, b = 42; max (a,b); //Счетчик обращений N = 1 float z = 66.3, f = 222.4; max (z,f); //Счетчик обращений N = 2 } Итак, одно имя нельзя использовать для обозначения нескольких параметров одного шаблона, но в разных шаблонах функций могут быть одинаковые имена у параметров шаблонов. Ситуация здесь такая же, как и у формальных параметров при определении обычных функций, и на ней можно не останавливаться подробнее. Действительно, раз действие параметра шаблона заканчивается в конце определения шаблона, то соответствующий идентификатор свободен для последующего использования, в том числе и в качестве имени параметра другого шаблона. Все параметры шаблона функций должны быть обязательно использованы в спецификациях параметров определения функции. Таким образом, будет ошибочным такой шаблон: template <class A, class B, class C> B func (A n, C m) {B valu;... } В данном неверном примере остался неиспользованныйм параметр шаблона с именем B. Его применение в качестве типа возвращаемого функцией значения и для определения объекта valu в теле функции недостаточно. Определяемая с помощью шаблона функция может иметь любое количество непараметризованных формальных параметров. Может быть не параметризовано и возвращаемое функцией значение. Например, в следующей программе шаблон определяет семейство функций, каждая из которых подсчитывает количество нулевых элементов одномерного массива параметризованного типа: #include <iostream.h> template <class D> long count0 (int, D *); //Прототип шаблона viod main () { int A[] = {1,0,6,0,4,10}; int n = sizeof(A)/sizeof A[0]; cout << "\ncount0(n,A) = " << count0(n,A); float X[] = {10.0, 0.0, 3.3, 0.0, 2.1}; n = sizeof(X)/sizeof X[]; cout << "\ncount0(n,X) = " << count0(n,X); } template <class T> long count0 (int size, T* array) { long k = 0; for (int i = 0; i < size; i++) if (int(array[i]) == 0) k++; return k; } В шаблоне функций count0 параметр T используется только в спецификации одного формльного параметра array. Параметр size и возвращаемое функцией значение имеют явно заданные непараметризованные типы. Как и при работе с обычными функциями, для шаблонов функций существуют определения и описания. В качестве описания шаблона функций используется прототип шаблона: template < список_ параметров_ шаблона > В списке параметров прототипа шаблона имена параметров не обязаны совпадать с именами тех же параметров в определении шаблона. Это и продемонстрировано в программе. При конкретизации шаблонного определения функции необходимо, чтобы при вызове функции типы фактических параметров, соответствующие одинаково параметризованным формальным параметрам, были одинаковыми. Для определенного выше шаблона функций с прототипом template < class E > void swap (E,E); недопустимо использовать такое обращение к функции: int n = 4; double d = 4.3; swap (n,d); // Ошибка в типах параметров Для правильного обращения к такой функции требуется явное приведение типа одного из параметров. Например, вызов swap (double (n), d); // Правильные типы параметров приведет к кокретизации шаблонного определения функций с параметром типа double. При использовании шаблонов функций возможна перегрузка как шаблонов, так и функций. Могут быть шаблоны с одинаковыми именами, но разными параметрами. Или с помощью шаблона может содаваться функция с таким же именем, что и явно определенная функция. В обоих случаях «распознавание» конкретного вызова выполняеся по сигнатуре, т.е. по типам, порядку и количеству фактических параметров. Шаблоны классов. Аналогично шаблонам функций. определяется шаблон семейства классов: template <список_параметров_шаблона> определение_класса Шаблон семейства классов определяет способ построения отдельных классов подобно тому, как класс определяет правила построения и формат отдельных объектов. В определении класса, входящего в шаблон, особую роль играет имя класса. Оно является не именем отдельного класса, а параметризованным именем семейства классов. Как и для шаблонов функций, определение шаблона класса может быть только глобальным. Следуя авторам языка и компилятора Си++, рассмотрим векторный класс (в число данных входит одномерный массив). Какой бы тип ни имели элементы массива (целый, вещественный, с двойной точностью и т.д.), в этом классе должны быть определены одни и те же базовые операции, например доступ к элементу по индексу и т.д. Если тип элементов вектора задавать как параметр шаблона класса, то система будет формировать вектор нужного типа (и соответствующий класс) при каждом определении конкретного объекта. Следующий шаблон автоматически формирует классы векторов с указанными свойствами: // TEMPLATE.VEC - шаблон векторов template <class T> // T - параметр шаблона ckass Vector { T *data; // Начало одномерного массива int size; // Количество элементов в иассиве public: Vector (int); // Конструктор класса vector Vector () { delete [] data; } // Деструктор // Расширение действия (перегрузка) операции «[]»: T& operator [] (int i) { return data [i];} }; // Внешнее определение конструктора класса: template <class T> Vector <T>:: Vector (int n) { data = new T [n]; size = n; }; Когда шаблон введен, у программиста появляется возможность определять конкретные объекты конкретных классов, каждый из которых параметрически порожден из шаблона. Формат определения объекта одного из классов, порождаемых шаблоном классов: имя_параметризованного_класса <фактические_параметры_шаблона> имя_объекта (параметры_конструктора); В нашем случае определить вектор, имеющий восемь вещественных координат типа double, можно следующим образом: Vector <double> z (8); Проиллюстрируем сказанное следующей программой: // формирование классов с помощью шаблона # inclub «template.vec» // Шаблон класса «вектор» # inclub <iostream.h> main () {Vector <int> X(5); //Создаем объект класса «целочисленный вектор» Vector <char> C(5); // Создаем объект класса «символьный вектор» for (int i = 0; i < 5; i++)// Определяем компоненты векторов { X [i] = i; C [i] = ‘A’ + i;} for (i = 0; i < 5; i++) cout << ² ² << X[i] << ² ² << C[i];}// 0 A 1 B 2 C 3 D 4 E В программе шаблон семейства классов с общим именем Vector используется для формирования двух классов с массивами целого и символьного типов. В соответствии с требованием синтаксиса имя параметризованного класса, определенное в шаблоне (в примере vector), испольуется в программе только с последующим конкретным фактическим параметром (аргументом), заключенным в угловые скобки. Параметром может быть имя стандартного или определенного пользователем типа. В данном примере использованы стандартные типы int и char. Использовать имя Vector без указания фактического параметра шаблона нельзя - никакое умалчиваемое значение при этом не предусматривается. В списке параметров шаблона могут присутствовать формальные параметры, не определяющие тип, точнее - это параметры, для которых тип фиксирован: # include <iostream.h> template <class T, int size = 64> class row { T *data; int length; public: row () { length = size; data = new T [size]; } ~row () { delete [] data; } T& operator [] (int i) { return data [i]; } }; void main () { row <float,8> rf; row <int,8> ri; for (int i = 0; i < 8; i++) { rf [i] = i; ri [i] = i * i; } for (i = 0; i < 8, i++) cout << ² ² << rf[i] << ² ² << ri[i]; } //0 0 1 1 2 4 3 9 4 16 5 25 6 36 7 49 В качестве аргумента, заменяющего при обращении к шаблону параметр size, взята константа. В общем случае может быть использовано константное выражение, однако выражения, содержащие переменные, использовать в качестве фактических параметров шаблонов нельзя.
Дата добавления: 2014-01-03; Просмотров: 721; Нарушение авторских прав?; Мы поможем в написании вашей работы! Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет |