Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

OOP_C++ / 07

.htm
Скачиваний:
20
Добавлен:
02.02.2015
Размер:
30.77 Кб
Скачать

07 - Преобразование типов. Указатели на функции. Cтруктура программы Содержание     Предыдущее занятие     Следующее занятие

Занятие 07 Преобразование типов. Указатели на функции. Cтруктура программы Использование описания typedef Описания typedef позволяет вводить синонимы для определенных ранее типов. Имена, определенные с помощью typedef, можно использовать так же, как спецификаторы встроенных типов и производных типов, а также типов, определенных пользователем. В описании после ключевого слова typedef следует спецификатор типа и идентификатор. Этот идентификатор и есть синоним для указанного типа.

typedef int *PArray[10]; // Тип массива из 10 указателей на целое PArray p; // Переменная описанного типа Синонимы типов, определенные с помощью typedef, могут участвовать в другом определении typedef. Описание typedef не определяет нового (пользовательского) типа, а только определяет синоним для существующего.

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

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

В арифметическом выражение с операндами разных типов все операнды приводятся к наибольшему типу из встретившихся. Это называется арифметическим преобразованием.

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

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

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

В приведенном фрагменте программы

int i = 3.14; i = i + 3.14; произойдут три преобразования типа. Константа 3.14 преобразуется к типу int (i равно 3). Во второй строке значение i расширяется до double. Результат 6.14 затем сужается до int. Это суженное значение присваивается i. Переменная i теперь содержит значение 6.

Явное преобразование типов производится при помощи следующих операторов: static_cast, dynamic_cast, const_cast и reinterpret_cast. Хотя иногда явное преобразование необходимо, оно служит потенциальным источником ошибок, поскольку подавляет проверку типов, выполняемую компилятором.

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

char byte_value = 32; cout << static_cast<int>(byte_value); Оператор dynamic_cast применяется при идентификации типа во время выполнения (run-time type identification) и будет рассмотрен при изучении классов.

Преобразование const_cast используется для добавления или удаления из типа переменной модификаторов const и volatile (последний модификатор используется для отображения того факта, что переменная может быть изменена фоновым процессом, подпрограммой-обработчиком прерываний и т.д.). В выражении

const_cast< T > (arg) T и arg должны быть одного и того же типа, за исключением модификаторов const и volatile. Преобразование осуществляется во время выполнения. Результат имеет тип T. Чаще всего используется для преобразования типов указателей и ссылок:

int i = 2; const int *p1 = &i; int* p2 = const_cast<int*>(p1); // Без const_cast будет ошибка компиляции *p2 = 3; // Можно модифицировать i через p2 Преобразование reinterpret_cast работает с внутренними представлениями объектов (re-interpret – другая интерпретация того же внутреннего представления), причем правильность этой операции целиком зависит от программиста. В выражении

reinterpret_cast< T > (arg) тип T должен быть типом указателя, ссылки, арифметическим типом, указателем на функцию или член класса.

Устаревшая форма явного преобразования имеет два вида:

// появившийся в C++ вид type (expr); // вид, существовавший в C (type) expr; и может применяться вместо операторов static_cast, const_cast и reinterpret_cast.

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

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

int (*pf)(int); Скобки вокруг *pf обязательны. В противном случае мы бы имели объявление функции с целым параметром, возвращающей указатель на int. Теперь переменной pf можно присвоить адрес реальной функции соответствующего типа.

int f(int k) { return k; } int main() { int (*pf)(int); pf = f; ... } Имя функции без следующих за ним скобок интерпретируется как указатель на функцию. Указатель на функцию применяется для вызова функции, которую он адресует. Включать оператор разыменования при этом необязательно. И прямой вызов функции по имени, и косвенный вызов по указателю записываются одинаково.

cout << f(3); // Вызов функции f cout << pf(3); // То же самое Часто для удобства определяют синоним типа указателя на функцию через typedef:

typedef int (*FuncType)(int); FuncType pf; Указатели на функцию чаще всего используются в качестве формальных параметров других функций.

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

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

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

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

typedef double (*FuncType)(double); Определим функцию нахождения корня методом деления отрезка пополам с параметрами, задающими начало и конец интервала, указателем на функцию и необязательным параметром "точность" (eps):

double getRoot(double a, double b, FuncType f, double eps = 0.001) { В теле функции определяется средняя точка и реализуется цикл в соответствии с описанным выше алгоритмом:

double c; do { c = (a + b) / 2; if (f(a) * f(c) > 0) a = c; else b = c; } while (b - a > eps); return (a + b) / 2; } Для тестирования функции нахождения корня можно предложить небольшую функцию:

double g(double x) { return x * x - 2; } В функции вызывается функция нахождения корня с различными значениями точности:

int main(int argc, char* argv[]) { cout << getRoot(0, 2, g) << endl; // Точность по умолчанию cout << getRoot(0, 2, g, 0.000001) << endl; return 0; } 5 Области видимости и время жизни объектов Имя идентификатора может использоваться многократно в различных областях видимости. Каждая переменная видима в программе только внутри ее области видимости. Язык С++ поддерживает три разновидности областей видимости.

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

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

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

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

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

Регистровый объект - это автоматический объект, для которого поддерживается быстрое считывание и запись его значения. Такие переменные объявляются с ключевым словом register.

register int i = 0; Локальный статический объект описывается с указанием служебного слова static. Он располагается в области памяти, существующей на протяжении всего времени выполнения программы. Статический локальный объект инициализируется во время первого выполнения инструкции, где он объявлен. Статический локальный объект используется тогда, когда его значение должно сохраняться между вызовами функции. Неинициализированные статические локальные объекты получают значение по умолчанию, принятое для их типа (для числовых типов - 0).

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

Накопление суммы можно реализовать с помощью функции add(), имеющей статический член sum:

int add(int i) { static int sum = 0; sum += i; return sum; } В функции main() определяется локальная переменная i и организуется цикл, в котором вводится значение i и выводится текущая сумма:

int main(int argc, char* argv[]) { int i; do { cin >> i; cout << add(i) << endl; } while (i); return 0; } Примечание Иногда для наглядности в случаях проверки переменной х на неравенство нулю пишут if(х != 0)или while(х != 0), хотя это полностью эквивалентно if(х) и while(х) соответственно.

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

namespace MySpace { int k = 10; void f(int n) { k = n; } } Определения объектов, объявленных внутри пространства имен, могут находиться и вне пространства имен:

namespace MySpace { int k = 10; void f(int n); } void MySpace::f(int n) { k = n; } Пространства могут быть вложены одно в другое, но при этом не могут находиться внутри функций или классов. Пространства имен могут быть разбиты на части в пределах одной единицы трансляции.

namespace MySpace { int k = 10; void f(int n); } // namespace MySpace { void f(int n) { k = n; } } Имена, объявленные и определенные внутри пространства имен, без каких-либо ограничений доступны в пределах этого пространства. Чтобы получить доступ к именам пространства из других частей программы, необходимо использовать синтаксис имя-пространства::имя:

int x = MySpace::k; Имеется возможность задания псевдонимов для пространств имен, например:

namespace YourSpace = MySpace; Существуют так называемые безымянные пространства имен, в который помещаются объекты, видимые только в своем файле (единице компиляции). Такие имена имеют так называемое внутреннее связывание.

// файл f1.cpp namespace { int x = 100; // х известно только в f1.cpp } // файл f2.cpp namespace { int x = 200; // x известно только в f2.cpp } Для того, чтобы все имена пространства можно было использовать без квалификатора, используется using-директива:

using namespace MySpace; // Свободное использование всех имен пространства Директива using делает все имена пространства глобальными в пределах данного файла или данного блока.

В отличие от using-директивы, using-объявление применяется к отдельным именам.

using MySpace::k; using MySpace::f; // использование f и k без квалификатора С помощью using-директивы можно объединять пространства имен:

namespace NewSpace { using namespace FirstSpace; // using-директива using namespace SecondSpace; // using-директива } Если требуется доступ только к нескольким именам из пространства, можно произвести явный отбор при помощи создания нового пространства и использования using-объявления:

namespace NewSpace { using OtherSpace::name1; // using-объявление using OtherSpace::name2; // using-объявление } Имена, указанные с помощью using-объявлений, имеют приоритет по отношению к именам, сделанным доступными при помощи using-директив. Механизм перегрузки функций работает сквозь границы пространств имен. Пространства имен открыты. К ним можно добавлять новые имена в нескольких объявлениях:

namespace FirstSpace { // начало } namespace SecondSpace { // прерывает } namespace FirstSpace { // продолжение } 8 Заголовочные файлы Организация программы в виде набора исходных файлов называется физической структурой программы.

Заголовочные файлы включаются в исходный текст с помощью директивы #include, после которой в угловых скобках обычно помещают имена стандартных заголовочных файлов, а в кавычках - имена заголовочных файлов разработанных пользователем. Например:

// Стандартный заголовочный файл: #include <iostream> // Заголовочный файл пользователя #include "MyFile.h" Различие заключается в порядке поиска файлов. Кавычки предполагают начало поиска с текущего каталога, а угловые скобки - поиск, начиная со специального каталога, определенного в конкретной среде программирования для стандартных заголовочных файлов. Заголовочные файлы могут содержать:

именованные пространства имен

определения типов

объявления и определения шаблонов

объявления имен

определения встроенных функций и констант

перечисления

директивы включения

макроопределения

директивы условной компиляции

комментарии.

Заголовочный файл не должен содержать

определений обычных функций и данных

неименованных пространств имен

экспортируемых определений шаблонов.

Для указания заголовочных файлов стандартной библиотеки не требуется расширение.

Каждый конкретный класс, перечисление, шаблон и т.д. должны быть определены в программе ровно один раз. "Правило одного определения" (One-Definition Rule) допускает наличие двух определений класса, шаблона или встроенной функции для определения одной и той же сущности, если они находятся в различных единицах трансляции, идентичны лексема за лексемой и значение лексем идентично в обеих единицах трансляции.

Для предотвращения повторной перекомпиляции заголовочного файла используются "стражи включения" (include guards):

// some_file.h #ifndef Some_file_h #define Some_file_h // текст заголовочного файла #endif 9 Подходы к разработке структуры программы Программа является набором отдельных единиц компиляции, объединяемых компоновщиком. Нелокальные переменные инициализируются до вызова функции main(). Завершение программы происходит при выходе из main(), при вызове exit() или abort(). Определенный порядок инициализации глобальных переменных, расположенных в разных единицах трансляции, не гарантируется.

Есть два подхода к разработке структуры программы на языке С++.

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

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

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

10 Создание в MS Visual C++.NET программ, состоящих из нескольких единиц трансляции Для добавления нового заголовочного файла в ранее созданный проект необходимо:

в подменю "Project" главного меню выбрать "Add New Item";

на панели "Templates" окна "Add New Item" выбрать тип файла Header File (.h); нужно указать имя нового файла (без расширения) в строке "Name";

нажать "OK", после чего открывается новый пустой файл.

Добавление нового файла с исходным текстом происходит аналогично: выбирается тип файла C++ File на закладке Files. В созданном новом файле целесообразно разместить директиву #include для подключения созданного ранее заголовочного файла.

Заголовочный файл и файл реализации целесообразно добавлять парами и давать им одинаковые имена (и различные расширения - .h и .cpp). Допустим, создается модуль состоящий из заголовочного файла SomeFile.h и файла реализации SomeFile.cpp.

В исходный текст заголовочного файла SomeFile.h нужно добавить "стражей включения"

#ifndef SomeFile_h #define SomeFile_h #endif В заголовочном файле (перед #endif) следует разместить объявление функции (например функции sum()):

#ifndef SomeFile_h #define SomeFile_h int sum(int a, int b); #endif В файле реализации после #include "SomeFile.h" размещается определение функции.

#include "SomeFile.h" int sum(int a, int b) { return a + b; } В главном модуле проекта необходимо подключить модуль SomeFile.h, а в функции main() осуществить ввод данных, вызов функции sum() и вывод результата:

// Программа вычисления суммы двух чисел #include <iostream> #include "SomeFile.h" using namespace std; int main(int argc, char* argv[]) { int x, y; cout << "Enter two integer values:" << endl; cin >> x >> y; int z = sum(x, y); cout << "Sum is " << z << endl; return 0; } 11 Задания на самостоятельную работу Задание 1 Реализовать программу, в которой с клавиатуры вводятся целые значения и по мере их ввода в двух отдельных функциях вычисляются максимум и минимум и выводятся на экран до тех пор, пока пользователь не введет значение 0. Использовать статические переменные в функциях.

Задание 2 Реализовать программу, которая позволяет найти минимум некоторой функции на заданном интервале. Алгоритм нахождения минимума заключается в последовательном переборе с заданным шагом точек интервала и сравнении значений функции в текущей точке с ранее найденным минимумом. Нахождение минимума реализовать отдельной функцией. Параметры функции - интервал, указатель на функцию, для которой ищется минимум, и шаг (по умолчанию шаг равен 0.00001). Разместить все функции, кроме main(), в отдельной единице трансляции (заголовочный файл и файл реализации).

 

Содержание     Предыдущее занятие     Следующее занятие

 

© 2001 - 2006 Иванов Л.В.

Соседние файлы в папке OOP_C++