Шилдт c++_базовый_курс издание 3. Герберт Шилдт С базовый курс
Скачать 9.37 Mb.
|
Глава 20: Пространства имен и другие темы В этой главе описаны пространства имен и такие эффективные средства, как explicit- конструкторы, указатели на функции, static-члены, const-функции-члены, альтернативный синтаксис инициализации членов класса, операторы указания на члены, ключевое слово asm, спецификация компоновки и функции преобразования. Пространства имен Пространство имен определяет некоторую декларативную область. Пространства имен мы кратко рассмотрели в главе 2. Они позволяют локализовать имена идентификаторов, чтобы избежать конфликтных ситуаций с ними. В С++-среде программирования используется огромное количество имен переменных, функций и имен классов. До введения пространств имен все эти имена конкурировали за память в глобальном пространстве имен, что и было причиной возникновения многих конфликтов. Например, если бы в вашей программе была определена функция toupper(), она могла бы (в зависимости от списка параметров) переопределить стандартную библиотечную функцию toupper(), поскольку оба имени должны были бы храниться в глобальном пространстве имен. Конфликты с именами возникали также при использовании одной программой нескольких библиотек сторонних производителей. В этом случае имя, определенное в одной библиотеке, конфликтовало с таким же именем из другой библиотеки. Подобная ситуация особенно неприятна при использовании одноименных классов. Например, если в вашей программе определен класс VideoMode, и в библиотеке, используемой вашей программой, определен класс с таким же именем, конфликта не избежать. Для решения описанной проблемы было создано ключевое слово namespace. Поскольку оно локализует видимость объявленных в нем имен, это значит, что пространство имен позволяет использовать одно и то же имя в различных контекстах, не вызывая при этом конфликта имен. Возможно, больше всего от нововведения "повезло" С++-библиотеке стандартных функций. До появления ключевого слова namespace вся С++-библиотека была определена в глобальном пространстве имен (которое было, конечно же, единственным). С наступлением namespace-"эры" С++-библиотека определяется в собственном пространстве имен, именуемом std, которое значительно понизило вероятность возникновения конфликтов имен. В своей программе программист волен создавать собственные пространства имен, чтобы локализовать видимость тех имен, которые, по его мнению, могут стать причиной конфликта. Это особенно важно, если вы занимаетесь созданием библиотек классов или функций. Понятие пространства имен Ключевое слово namespace позволяет разделить глобальное пространство имен путем создания некоторой декларативной области. По сути, пространство имен определяет область видимости. Общий формат задания пространства имен таков. namespace name { // объявления } Все, что определено в границах инструкции namespace, находится в области видимости этого пространства имен. В следующей программе приведен пример использования namespace-инструкции. Она локализует имена, используемые для реализации простого класса счета в обратном направлении. В созданном здесь пространстве имен определяется класс counter, который реализует счетчик, и переменные upperbound и lowerbound, содержащие значения верхней и нижней границ, применяемых для всех счетчиков. namespace CounterNameSpace { int upperbound; int lowerbound; class counter { int count; public: counter(int n) { if(n <= upperbound) count = n; else count = upperbound; } void reset(int n) { if(n <= upperbound) count = n; } int run() { if(count > lowerbound) return count--; else return lowerbound; } }; } Здесь переменные upperbound и lowerbound, а также класс counter являются частью области видимости, определенной пространством имен CounterNameSpace. В любом пространстве имен к идентификаторам, которые в нем объявлены, можно обращаться напрямую, т.е. без указания этого пространства имен. Например, в функции run(), которая находится в пространстве имен CounterNameSpace, можно напрямую обращаться к переменной lowerbound: if(count > lowerbound) return count--; Но поскольку инструкция namespace определяет область видимости, то при обращении к объектам, объявленным в пространстве имен, извне этого пространства необходимо использовать оператор разрешения области видимости. Например, чтобы присвоить значение 10 переменной upperbound из кода, который является внешним по отношению к пространству имен CounterNameSpace, нужно использовать такую инструкцию. CounterNameSpace::upperbound = 10; Чтобы объявить объект типа counter вне пространства имен CounterNameSpace, используйте инструкцию, подобную следующей. CounterNameSpace::counter ob; В общем случае, чтобы получить доступ к некоторому члену пространства имен извне этого пространства, необходимо предварить имя этого члена именем пространства и разделить эти имена оператором разрешения области видимости. Рассмотрим программу, в которой демонстрируется использование пространства имен CounterNameSpace. // Демонстрация использования пространства имен. #include using namespace std; namespace CounterNameSpace { int upperbound; int lowerbound; class counter { int count; public: counter (int n) { if(n <= upperbound) count = n; else count = upperbound; } void reset (int n) { if(n <= upperbound) count = n; } int run() { if(count > lowerbound) return count--; else return lowerbound; } }; } int main() { CounterNameSpace::upperbound = 100; CounterNameSpace::lowerbound = 0; CounterNameSpace::counter ob1(10); int i; do { i = ob1.run(); cout << i << " "; }while(i > CounterNameSpace :: lowerbound); cout << endl; CounterNameSpace::counter ob2(20); do { i = ob2.run(); cout << i << " "; }while(i > CounterNameSpace::lowerbound); cout << endl; ob2.reset(100); CounterNameSpace::lowerbound = 90; do { i = ob2.run(); cout << i << " "; }while(i > CounterNameSpace::lowerbound); return 0; } Обратите внимание на то, что при объявлении объекта класса counter и обращении к переменным upperbound и lowerbound используется имя пространства имен CounterNameSpace. Но после объявления объекта типа counter уже нет необходимости в полной квалификации его самого или его членов. Поскольку пространство имен однозначно определено, функцию run() объекта ob1 можно вызывать напрямую, т.е. без указания (в качестве префикса) пространства имен (ob1.run()). Программа может содержать несколько объявлений пространств имен с одинаковыми именами. Это означает, что пространство имен можно разбить на несколько файлов или на несколько частей в рамках одного файла. Вот пример. namespace NS { int i; } // . . . namespace NS { int j; } Здесь пространство имен NS разделено на две части. Однако содержимое каждой части относится к одному и тому же пространству имен NS. Любое пространство имен должно быть объявлено вне всех остальных областей видимости. Это означает, что нельзя объявлять пространства имен, которые локализованы, например, в рамках функции. При этом одно пространство имен может быть вложено в другое. Инструкция using Инструкция using делает заданное пространство имен "видимым", т.е. действующим. Если программа включает множество ссылок на члены некоторого пространства имен, то нетрудно представить, что необходимость указывать имя этого пространства имен при каждом к ним обращении, очень скоро утомит вас. Эту проблему позволяет решить инструкция using, которая применяется в двух форматах. using namespace имя; using name::член; В первой форме элемент имя задает название пространства имен, к которому вы хотите получить доступ. Все члены, определенные внутри заданного пространства имен, попадают в "поле видимости", т.е. становятся частью текущего пространства имен и их можно затем использовать без квалификации (уточнения пространства имен). Во второй форме делается "видимым" только указанный член пространства имен. Например, предполагая, что пространство имен CounterNameSpace определено (как показано выше), следующие инструкции using и присваивания будут вполне законными. using CounterNameSpace::lowerbound; /* Видимым стал только член lowerbound */ lowerbound = 10; /* Все в порядке, поскольку член lowerbound находится в области видимости. */ using namespace CounterNameSpace; // Все члены видимы. upperbound = 100; // Все в порядке, поскольку все члены видимы. Использование инструкции using демонстрируется в следующей программе (которая представляет собой новый вариант счетчика из предыдущего раздела). // Использование инструкции using. #include using namespace std; namespace CounterNameSpace { int upperbound; int lowerbound; class counter { int count; public: counter (int n) { if(n <= upperbound) count = n; else count = upperbound; } void reset(int n) { if(n <= upperbound) count = n; } int run() { if(count > lowerbound) return count--; else return lowerbound; } }; } int main() { /* Используется только член upperbound из пространства имен CounterNameSpace. */ using CounterNameSpace::upperbound; /* Теперь для установки значения переменной upperbound не нужно указывать пространство имен. */ upperbound = 100; /* Но при обращении к переменной lowerbound и другим объектам по-прежнему необходимо указывать пространство имен. */ CounterNameSpace::lowerbound = 0; CounterNameSpace::counter ob1(10); int i; do { i = ob1.run(); cout << i << " "; }while(i > CounterNameSpace::lowerbound); cout. << endl; /* Теперь используем все пространство имен CounterNameSpace. */ using namespace CounterNameSpace; counter ob2(20); do { i = ob2.run(); cout << i << " "; }while(i > lowerbound); cout << endl; ob2.reset(100); lowerbound = 90; do { i = ob2.run(); cout << i << " "; }while(i > lowerbound); return 0; } Эта программа иллюстрирует еще один важный момент. Использование одного пространства имен не переопределяет другое. Если некоторое пространство имен становится "видимым", это значит, что оно просто добавляет свои имена к именам других, уже действующих пространств. Поэтому к концу этой программы к глобальному пространству имен добавились и std, и CounterNameSpace. Неименованные пространства имен Неименованное пространство имен ограничивает идентификаторы рамками файла, в котором они объявлены. Существует неименованное пространство имен специального типа, которое позволяет создавать идентификаторы, уникальные для данного файла. Общий формат его объявления выглядит так. namespace { // объявления } Неименованные пространства имен позволяют устанавливать уникальные идентификаторы, которые известны только в области видимости одного файла. Другими словами, члены файла, который содержит неименованное пространство имен, можно использовать напрямую, без уточняющего префикса. Но вне файла эти идентификаторы неизвестны. Как упоминалось выше в этой книге, использование модификатора типа static также позволяет ограничить область видимости глобального пространства имен файлом, в котором он объявлен. Например, рассмотрим следующие два файла, которые являются частью одной и той же программы. Поскольку переменная k определена в файле One, ее и можно использовать в файле One. В файле Two переменная k определена как внешняя (extern-переменная), а это значит, что ее имя и тип известны, но сама переменная k в действительности не определена. Когда эти два файла будут скомпонованы, попытка использовать переменную k в файле Two приведет к возникновению ошибки, поскольку в нем нет определения для переменной k. Тот факт, что k объявлена static-переменной в файле One, означает, что ее область видимости ограничивается этим файлом, и поэтому она недоступна для файла Two. Несмотря на то что использование глобальных static-объявлений все еще разрешено в C++, для локализации идентификатора в рамках одного файла лучше использовать неименованное пространство имен. Рассмотрим пример. Здесь переменная k также ограничена рамками файла One. Для новых программ рекомендуется использовать вместо модификатора static неименованное пространство имен. Обычно для большинства коротких программ и программ среднего размера нет необходимости в создании пространств имен. Но, формируя библиотеки многократно используемых функций или классов, имеет смысл заключить свой код (если хотите обеспечить его максимальную переносимость) в собственное пространство имен. Пространство имен std Пространство имен std используется библиотекой C++. Стандарт C++ определяет всю свою библиотеку в собственном пространстве имен, именуемом std. Именно по этой причине большинство программ в этой книге включает следующую инструкцию: using namespace std; При выполнении этой инструкции пространство имен std становится текущим, что открывает прямой доступ к именам функций и классов, определенных в этой библиотеке, т.е. при обращении к ним отпадает необходимость в использовании префикса std::. Конечно, при желании можно явным образом квалифицировать каждое библиотечное имя префиксом std::. Например, следующая программа не привносит библиотеку в глобальное пространство имен. // Использование явно заданной квалификации std::. #include int main() { double val; std::cout << "Введите число: "; std::cin >> val; std::cout << "Вы ввели число "; std::cout << val; return 0; } Здесь имена cout и cin явно дополнены именами своих пространств имен. Итак, чтобы записать данные в стандартный выходной поток, следует использовать не просто имя потока cout, а имя с префиксом std::cout, а чтобы считать данные из стандартного входного потока, нужно применить "префиксное" имя std::cin. Если ваша программа использует стандартную библиотеку только в ограниченных пределах, то, возможно, ее и не стоит вносить в глобальное пространство имен. Но если ваша программа содержит сотни ссылок на библиотечные имена, то гораздо проще сделать пространство имен std текущим, чем полностью квалифицировать каждое имя в отдельности. Если вы используете только несколько имен из стандартной библиотеки, то, вероятно, имеет смысл использовать инструкцию using для каждого из них в отдельности. Преимущество этого подхода состоит в том, что эти имена можно по-прежнему использовать без префикса std::, не внося при этом всю библиотеку стандартных функций в глобальное пространство имен. Рассмотрим пример. /* Внесение в глобальное пространство имен лишь нескольких имен. */ #include // Получаем доступ к именам потоков cout и cin. using std::cout; using std::cin; int main() { double val; cout << "Введите число: "; cin >> val; cout << "Вы ввели число "; cout << val; return 0; } Здесь имена потоков cin и cout можно использовать напрямую, но остальная часть пространства имен std не внесена в область видимости. Как упоминалось выше, исходная библиотека C++ была определена в глобальном пространстве имен. Если вам придется модернизировать старые С++-программы, то вы должны либо включить в них инструкцию using namespace std, либо дополнить каждое обращение к члену библиотеки префиксом std::. Это особенно важно, если вам придется заменять старые заголовочные *.h-файлы современными заголовками. Помните, что старые заголовочные *.h-файлы помещают свое содержимое в глобальное пространство имен, а современные заголовки — в пространство имен std. Указатели на функции Указатель на функцию ссылается на входную точку этой функции. Указатель на функцию— это довольное сложное, но очень мощное средство C++. Несмотря на то что функция не является переменной, она, тем не менее, занимает физическую область памяти, некоторый адрес которой можно присвоить указателю. Адрес, присваиваемый указателю, является входной точкой функции. (Именно этот адрес используется при вызове функции.) Если некоторый указатель ссылается на функцию, то ее (функцию) можно вызвать с помощью этого указателя. Указатели на функции также позволяют передавать функции в качестве аргументов другим функциям. Адрес функции можно получить, используя имя функции без круглых скобок и аргументов. (Этот процесс подобен получению адреса массива, когда также используется только его имя без индекса.) Если присвоить адрес функции указателю, то эту функцию можно вызвать через указатель. Например, рассмотрим следующую программу. Она содержит две функции, vline() и hline(), которые рисуют на экране вертикальные и горизонтальные линии заданной длины. #include using namespace std; void vline(int i), hline(int i); int main() { void (*p)(int i); p = vline; // указатель на функцию vline() (*p)(4); // вызов функции vline() p = hline; // указатель на функцию hline() (*p)(3); // вызов функции hline() return 0; } void hline(int i) { for( ; i; i--) cout << "-"; cout << "\n"; } void vline(int i) { for( ; i; i--) cout << "|\n"; } Вот как выглядят результаты выполнения этой программы. I I I I - - - Рассмотрим эту программу в деталях. В первой строке тела функции main() объявляется переменная р как указатель на функцию, которая принимает один целочисленный аргумент и не возвращает никакого значения. Это объявление не определяет, какая функция имеется в виду. Оно лишь создает указатель, который можно использовать для адресации любой функции этого типа. Необходимость круглых скобок, в которые заключен указатель *р, следует из С++-правил предшествования. В следующей строке указателю р присваивается адрес функции vline(). Затем выполняется вызов функции vline() с аргументом 4. После этого указателю р присваивается адрес функции hline(), и с помощью этого указателя реализуется ее вызов. В этой программе при вызове функций посредством указателя используется следующий формат: (*p) (4); Однако функцию, адресуемую указателем р, можно вызвать с использованием более простого синтаксиса: p (4); Единственная причина, по которой чаще используется первый вариант вызова функции, состоит в том, что всем, кто станет разбирать вашу программу, станет ясно, что здесь реализован вызов функции через указатель р, а не вызов функции с именем р. Во всем остальном эти варианты эквивалентны. Несмотря на то что в предыдущем примере указатель на функцию используется только ради иллюстрации, зачастую такое его применение играет очень важную роль. Указатель на функцию позволяет передавать ее адрес другой функции. В качестве показательного примера можно привести функцию qsort() из стандартной С++-библиотеки. Функция qsort() — это функция быстрой сортировки, основанная на алгоритме Quicksort, который упорядочивает содержимое массива. Вот как выглядит ее прототип. void qsort(void * start, size_t length, size_t size, int (*compare) (const void *, const void *)); Функция qsort() — это функция сортировки из стандартной С++-библиотеки. Прототип функции qsort() "прописан" в заголовке тип size_t (как тип unsigned int). Чтобы использовать функцию qsort(), необходимо передать ей указатель на начало массива объектов, который вы хотите отсортировать (параметр start), длину этого массива (параметр length), размер в байтах каждого элемента (параметр size) и указатель на функцию сравнения элементов массива (параметр *compare). Функция сравнения, используемая функцией qsort(), сопоставляя два элемента массива, должна возвратить отрицательное значение, если ее первый аргумент указывает на значение, которое меньше второго, нуль, если эти аргументы равны, и положительное значение, если первый аргумент указывает на значение, которое больше второго. Чтобы понять, как можно использовать функцию qsort(), рассмотрим следующую программу. #include #include #include using namespace std; int comp(const void *a, const void *b); int main() { char str[] = "Указатели на функции дают гибкость."; qsort(str, strlen(str), 1, comp); cout << "Отсортированная строка: " << str; return 0; } int comp(const void *a, const void *b) { return * (char *) a - * (char *) b; } Вот как выглядят результаты выполнения этой программы. Отсортированная строка: Уаааабгдезииииккклнностттуфцью Эта программа сортирует строку str в возрастающем порядке. Поскольку функции qsort() передается вся необходимая ей информация, включая указатель на функцию сравнения, ее можно использовать для сортировки данных любого типа. Например, следующая программа сортирует массив целых чисел. Для гарантии переносимости при определении размера целочисленного значения в ней используется оператор sizeof. #include #include using namespace std; int comp(const void *a, const void *b); int main() { int num[] = {10, 4, 3, 6, 5, 7, 8}; int i; qsort(num, 7, sizeof(int), comp); for(i=0; i<7; i++) cout << num[i] << ' '; return 0; } int comp(const void *a, const void *b) { return * (int *) a - * (int *) b; } He стану утверждать, что указатели на функции не так просты для понимания, но практика поможет и "с ними найти общий язык". В отношении указателей на функции необходимо рассмотреть еще один аспект, связанный с перегруженными функциями. Как найти адрес перегруженной функции Получить адрес перегруженной функции немного сложнее, чем найти адрес обычной "одиночной" функции. Если же существует несколько версий перегруженной функции, то должен существовать механизм, который бы определял, адрес какой именно версии мы получаем. При получении адреса перегруженной функции именно способ объявления указателя определяет, адрес какой ее версии будет получен. По сути, объявление указателя в этом случае сравнивается с соответствующими объявлениями указателей перегруженных функций. Функция, объявление которой обнаружит совпадение, и будет той функцией, адрес которой мы получили. В следующем примере программы содержится две версии функции space(). Первая версия выводит на экран count пробелов, а вторая— count символов, переданных в качестве аргумента ch. В функции main() объявляются два указателя на функции. Первый задан как указатель на функцию с одним целочисленным параметром, а второй — как указатель на функцию с двумя параметрами. /* Использование указателей на перегруженные функции. */ #include using namespace std; // Вывод на экран count пробелов. void space(int count) { for( ; count; count--) cout << ' '; } // Вывод на экран count символов, переданных в ch. void space(int count, char ch) { for( ; count; count--) cout << ch; } int main() { /* Создание указателя на void-функцию с одним int-параметром. */ void (*fp1) (int); /* Создание указателя на void-функцию с одним int-параметром и одним параметром типа char. */ void (*fp2)(int, char); fp1 = space; // получаем адрес функции space(int) fp2 = space; // получаем адрес функции space(int, char) fp1(22); // Выводим 22 пробела (этот вызов аналогичен вызову (* fp1) (22)) . cout << "|\n"; fp2(30, 'х'); // Выводим 30 символов "х" (этот вызов аналогичен вызову (*fp2) (30, 'x'). cout << "|\n"; return 0; } Вот как выглядят результаты выполнения этой программы. I ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ I Как отмечено в комментариях, компилятор способен определить, адрес какой перегруженной функции он получает, на основе того, как объявлены указатели fp1 и fp2. Итак, когда адрес перегруженной функции присваивается указателю на функцию, то именно это объявление указателя служит основой для определения того, адрес какой функции был присвоен. При этом объявление указателя на функцию должно соответствовать одной (и только одной) из перегруженных функций. В противном случае в программу вносится неоднозначность, которая вызовет ошибку компиляции. Статические члены класса Один статический член класса разделяется всеми объектами класса. Ключевое слово static можно применять и к членам класса. Объявляя член класса статическим, мы тем самым уведомляем компилятор о том, что независимо от того, сколько объектов этого класса будет создано, существует только одна копия этого static-члена. Другими словами, static-член разделяется всеми объектами класса. Все статические данные при первом создании объекта инициализируются нулевыми значениями, если не представлено других значений инициализации. При объявлении статического члена данных в классе программист не должен его определять. Необходимо обеспечить его глобальное определение вне этого класса. Это реализуется путем повторного объявления этой статической переменной с помощью оператора разрешения области видимости, который позволяет идентифицировать, к какому классу она принадлежит. Только в этом случае для этой статической переменной будет выделена память. Рассмотрим пример использования static-члена класса. Изучите код этой программы и постарайтесь понять, как она работает. #include using namespace std; class ShareVar { static int num; public: void setnum(int i) { num = i; }; void shownum() { cout << num << " "; } }; int ShareVar::num; // определяем static-член num int main() { ShareVar a, b; a.shownum(); // выводится 0 b.shownum(); // выводится 0 a.setnum(10); // устанавливаем static-член num равным 10 a.shownum(); // выводится 10 b.shownum(); // также выводится 10 return 0; } Обратите внимание на то, что статический целочисленный член num объявлен и в классе ShareVar, и определен в качестве глобальной переменной. Как было заявлено выше, необходимость такого двойного объявления вызвана тем, что при объявлении члена num в классе ShareVar память для него не выделяется. C++ инициализирует переменную num значением 0, поскольку никакой другой инициализации в программе нет. Поэтому в результате двух первых вызовов функции shownum() для объектов а и b отображается значение 0. Затем объект а устанавливает член num равным 10, после чего объекты а и b снова выводят на экран его значение с помощью функции shownum(). Но так как существует только одна копия переменной num, разделяемая объектами а и b, значение 10 будет выведено при вызове функции shownum() для обоих объектов. Узелок на память. При объявлении члена класса статическим вы обеспечиваете создание только одной его копии, которая будет совместно использоваться всеми объектами этого класса. Если static-переменная является открытой (т.е. public-переменной), к ней можно обращаться напрямую через имя ее класса, без ссылки на какой-либо конкретный объект. (Безусловно, обращаться можно также и через имя объекта.) Рассмотрим, например, эту версию класса ShareVar. class ShareVar { public: static int num; void setnum(int i) { num = i; }; void shownum() { cout << num << " "; } }; В данной версии переменная num является public-членом данных. Это позволяет нам обращаться к ней напрямую, как показано в следующий инструкции. ShareVar::num = 100; Здесь значение переменной num устанавливается независимо от объекта, а для обращения к ней достаточно использовать имя класса и оператор разрешения области видимости. Более того, эта инструкция законна даже до создания каких-либо объектов типа ShareVar! Таким образом, получить или установить значение static-члена класса можно до того, как будут созданы какие-либо объекты. И хотя вы, возможно, пока не почувствовали необходимости в static-членах класса, по мере программирования на C++ вам придется столкнуться с ситуациями, когда они окажутся весьма полезными, позволив избежать применения глобальных переменных. Можно также объявить статической и функцию-член, но это — нераспространенная практика. К статической функции-члену могут получить доступ только другие static-члены этого класса. (Конечно же, статическая функция-член может получать доступ к нестатическим глобальным данным и функциям.) Статическая функция-член не имеет указателя this. Создание виртуальных статических функций-членов не разрешено. Кроме того, их нельзя объявлять с модификаторами const или volatile. Статическую функцию-член можно вызвать для объекта ее класса или независимо от какого бы то ни было объекта, а для обращения к ней достаточно использовать имя класса и оператор разрешения области видимости. Применение к функциям-членам модификаторов const и mutable Константная (const-) функция-член не может модифицировать объект, который ее вызвал. Функции-члены класса могут быть объявлены с использованием модификатора const. Это означает, что с указателем this в этом случае необходимо обращаться как с const- указателем. Другими словами, const-функция не может модифицировать объект, для которого она вызвана. Кроме того, const-объект не может вызвать не const-функцию-член. Но const-функцию-член могут вызывать как const-, так и не const-объекты. Чтобы определить функцию как const-член класса, используйте формат, представленный в следующем примере. class X { int some_var; public: int f1() const; // const-функция-член }; Как видите, модификатор const располагается после объявления списка параметров функции. Цель объявления функции как const-члена — не допустить модификацию объекта, который ее вызывает. Например, рассмотрим следующую программу. /* Демонстрация использования const-функций-членов. Эта программа не скомпилируется. */ #include using namespace std; class Demo { int i; public: int geti() const { return i; // все в порядке } void seti (int x) const { i = x; // ошибка! } }; int main() { Demo ob; ob.seti(1900); cout << ob.geti(); return 0; } Эта программа не скомпилируется, поскольку функция seti() объявлена как const-член. Это означает, что ей не разрешено модифицировать вызывающий объект. Ее попытка изменить содержимое переменной i приводит к возникновению ошибки. В отличие от функции seti(), функция geti() не пытается модифицировать переменную i, и потому она совершенно приемлема. Возможны ситуации, когда нужно, чтобы const-функция могла изменить один или несколько членов класса, но никак не могла повлиять на остальные. Это можно реализовать с помощью модификатора mutable, который переопределяет атрибут функции const. Другими словами, mutable-член может быть модифицирован const-функцией-членом. Рассмотрим пример. /* Демонстрация использования модификатора mutable. */ #include using namespace std; class Demo { mutable int i; int j; public: int geti() const { return i; // все в порядке } void seti(int x) const { i = x; // теперь все в порядке } /* Следующая функция не скомпилируется, void setj (int х) const { j = x; // Это по-прежнему неверно! } */ }; int main() { Demo ob; ob.seti(1900); cout << ob.geti(); return 0; } Здесь член i определен с использованием модификатора mutable, поэтому его можно изменить с помощью функции seti(). Однако переменная j не является mutable-членом, поэтому функции setj() не разрешено модифицировать его значение. Использование explicit-конструкторов Для создания "неконвертирующего" конструктора используйте спецификатор explicit. В C++ определено ключевое слово explicit, которое применяется для обработки специальных ситуаций, когда используются конструкторы определенных типов. Чтобы понять назначение спецификатора explicit, рассмотрим следующую программу. #include using namespace std; class myclass { int a; public: myclass(int x) { a = x; } int geta() { return a; } }; int main() { myclass ob(4); cout << ob.geta(); return 0; } Здесь конструктор класса myclass принимает один параметр. Обратите внимание на то, как объявлен объект ob в функции main(). Значение 4, заданное в круглых скобках после имени ob, представляет собой аргумент, который передается параметру x конструктора myclass(), а параметр x в свою очередь используется для инициализации члена a. Именно таким способом мы инициализируем члены класса с начала этой книги. Однако существует и альтернативный вариант инициализации. Например, при выполнении следующей инструкции член a также получит значение 4. myclass ob = 4; /* Этот формат инициализации автоматически преобразуется в формат myclass(4). */ Как отмечено в комментарии, этот формат инициализации автоматически преобразуется в вызов конструктора класса myclass, а число 4 используется в качестве аргумента. Другими словами, предыдущая инструкция обрабатывается компилятором так, как если бы она была записана: myclass ob(4); В общем случае всегда, когда у вас есть конструктор, который принимает только один аргумент, для инициализации объекта можно использовать любой из форматов: либо ob(х), либо ob=х. Дело в том, что при создании конструктора класса с одним аргументом вами неявно создается преобразование из типа аргумента в тип этого класса. Если вам не нужно, чтобы такое неявное преобразование имело место, можно предотвратить его с помощью спецификатора explicit. Ключевое слово explicit применяется только к конструкторам. Конструктор, определенный с помощью спецификатора explicit, будет задействован только в том случае, если для инициализации членов класса используется обычный синтаксис конструктора. Никаких автоматических преобразований выполнено не будет. Например, объявляя конструктор класса myclass с использованием спецификатора explicit, мы тем самым отменяем поддержку автоматического преобразования типов. В этом варианте определения класса функция myclass() объявляется как explicit-конструктор. #include using namespace std; class myclass { int a; public: explicit myclass(int x) { a = x; } int geta() { return a; } }; Теперь будут разрешены к применению только конструкторы, заданные в таком формате. myclass ob(110); Чем интересно неявное преобразование конструктора Автоматическое преобразование из типа аргумента конструктора в вызов конструктора само по себе имеет интересные последствия. Рассмотрим, например, следующий код. #include using namespace std; class myclass { int num; public: myclass(int i) { num = i; } int getnum() { return num; } }; int main() { myclass о(10); cout << o.getnum() << endl; // отображает 10 /* Теперь используем неявное преобразование для присвоения нового значения. */ о = 1000; cout << o.getnum() << endl; // отображает 1000 return 0; } Обратите внимание на то, что новое значение присваивается объекту о с помощью такой инструкции: о = 1000; Использование данного формата возможно благодаря неявному преобразованию из типа int в тип myclass, которое создается конструктором myclass(). Конечно же, если бы конструктор myclass() был объявлен с помощью спецификатора explicit, то предыдущая инструкция не могла бы выполниться. Синтаксис инициализации членов класса В примерах программ из предыдущих глав члены данных получали начальные значения в конструкторах своих классов. Например, следующая программа содержит класс myclass, который включает два члена данных numA и numB. Эти члены инициализируются в конструкторе myclass(). #include using namespace std; class myclass { int numA; int numB; public: /* Инициализируем члены numA и numB в конструкторе myclass(), используя обычный синтаксис. */ myclass(int х, int у) { numA = х; numA = y;} int getNumA() { return numA; } int getNumB() { return numB; } }; int main() { myclass ob1(7, 9), ob2 (5, 2); cout << "Значения членов данных объекта ob1 равны " << ob1.getNumB() << " и " << ob1.getNumA() << endl; cout << "Значения членов данных объекта ob2 равны " << ob2.getNumB() << " и " << ob2.getNumA() << endl; return 0; } Результаты выполнения этой программы таковы. Значения членов данных объекта ob1 равны 9 и 7 Значения членов данных объекта ob2 равны 2 и 5 Присвоение начальных значений членам данных numA и numB в конструкторе, как это делается в конструкторе myclass(), — обычная практика, которая применяется для многих классов. Но этот метод годится не для всех случаев. Например, если бы члены numA и numB были заданы как const-переменные, т.е. таким образом: class myclass { const int numA; // const-член const int numB; // const-член }; то им нельзя было бы присвоить значения с помощью конструктора класса myclass, поскольку const-переменные должны быть инициализированы однократно, после чего им уже нельзя придать другие значения. Подобные проблемы возникают при использовании ссылочных членов, которые должны быть инициализированы, и при использовании членов класса, которые не имеют конструкторов по умолчанию. Для решения проблем такого рода в C++ предусмотрена поддержка альтернативного синтаксиса инициализации членов класса, который позволяет присваивать им начальные значения при создании объекта класса. Синтаксис инициализации членов класса аналогичен тому, который используется для вызова конструктора базового класса. Вот как выглядит общий формат такой инициализации. constructor(список_аргументов): член1(инициализатор), член2(инициализатор), // ... членN (инициализатор) { // тело конструктора } Члены, подлежащие инициализации, указываются после конструктора класса, и отделяются от имени конструктора и списка его аргументов двоеточием. При этом в одном и том же списке можно смешивать обращения к конструкторам базового класса с инициализацией членов. Ниже представлена предыдущая программа, но переделанная так, чтобы члены numA и numB были объявлены с использованием модификатора const, и получали свои начальные значения с помощью альтернативного синтаксиса инициализации членов класса. #include using namespace std; class myclass { const int numA; // const-член const int numB; // const-член public: /* Инициализируем члены numA и numB с использованием альтернативного синтаксиса инициализации. */ myclass(int х, int у) : numA(x), numB(y) { } int getNumA() { return numA; } int getNumB() { return numB; } }; int main() { myclass ob1 (7, 9), ob2(5, 2); cout << "Значения членов данных объекта ob1 равны " << ob1.getNumB() << " и " << ob1.getNumA()<< endl; cout << "Значения членов данных объекта ob2 равны " << ob2.getNumB() << " и " << ob2.getNumA()<< endl; return 0; } Эта программа генерирует такие же результаты, как и ее предыдущая версия. Однако обратите внимание на то, как инициализированы члены numA и numB. myclass(int х, int у) : numA(x), numB(у) { } Здесь член numA инициализируется значением, переданным в аргументе х, а член numB — значением, переданным в аргументе у. И хотя члены numA и numB сейчас определены как const-переменные, они могут получить свои начальные значения при создании объекта класса myclass, поскольку здесь используется альтернативный синтаксис инициализации членов класса. Использование ключевого слова asm С помощью ключевого слова asm в С++-программу встраивается код, написанный на языке ассемблера. Несмотря на то что C++ — всеобъемлющий и мощный язык программирования, возможны ситуации, обработка которых для него оказывается весьма затруднительной. (Например, в C++ не предусмотрена инструкция, которая могла бы запретить прерывания.) Чтобы справиться с подобными специальными ситуациями, C++ предоставляет средство, которое позволяет войти в код, написанный на языке ассемблера, совершенно игнорируя С ++-компилятор. Этим средством и является инструкция asm, используя которую можно встроить ассемблерный код непосредственно в С++-программу. Этот код скомпилируется без каких-либо изменений и станет частью кода вашей программы, начиная с места нахождения инструкции asm. Общий формат использования ключевого слова asm имеет следующий вид. asm ("код"); Здесь элемент код означает инструкцию, написанную на языке ассемблера, которая будет встроена в программу. При этом некоторые компиляторы также позволяют использовать и другие форматы записи инструкции asm. asm инструкция; asm инструкция newline asm { последовательность инструкций } Здесь элемент инструкция означает любую допустимую инструкцию языка ассемблера. Поскольку использование инструкции asm зависит от конкретной реализации среды программирования, то за подробностями обратитесь к документации, прилагаемой к вашему компилятору. На момент написания этой книги в среде Visual C++ (Microsoft) для встраивания кода, написанного на языке ассемблера, предлагалось использовать инструкцию _ _asm. Во всём остальном этот формат аналогичен описанию инструкции asm. Осторожно! Для использования инструкции asm необходимо обладать доскональными знаниями языка ассемблера. Если вы не считаете себя специалистом по этому языку, то лучше пока избегать использования инструкции asm, поскольку неосторожное ее применение может вызвать тяжелые последствия для вашей системы. Спецификация компоновки Спецификатор компоновки позволяет определить способ компоновки функции. В C++ можно определить, как функция связывается с вашей программой. По умолчанию функции компонуются как С++-функции. Но, используя спецификацию компоновки, можно обеспечить компоновку функций, написанных на других языках программирования. Общий формат спецификатора компоновки выглядит так: extern "язык" прототип_функции Здесь элемент язык означает нужный язык программирования. Все С++-компиляторы поддерживают как С-, так и С++-компоновку. Некоторые компиляторы также позволяют использовать спецификаторы компоновки для таких языков, как Fortran, Pascal или BASIC. (Эту информацию необходимо уточнить в документации, прилагаемой к вашему компилятору.) Следующая программа позволяет скомпоновать функцию myCfunc() как С-функцию. #include using namespace std; extern "C" void myCfunc(); int main() { myCfunc(); return 0; } // Эта функция будет скомпонована как С-функция. void myCfunc() { cout << "Эта функция скомпонована как С-функция.\n"; } На заметку. Ключевое слово extern — необходимая составляющая спецификации компоновки. Более того, спецификация компоновки должна быть глобальной; ее нельзя использовать в теле какой-либо функции. Используя следующий формат спецификации компоновки, можно задать не одну, а сразу несколько функций. extern "язык" { прототипы_функций } Спецификации компоновки используются довольно редко, и вам, возможно, никогда не придется их применять. Основное их назначение — позволить применение в С++- программах кода, написанного сторонними организациями на языках, отличных от C++. Операторы указания на члены ".*" и "->*" Операторы указания на член позволяют получить доступ к члену класса через указатель на этот член. В C++ предусмотрена возможность сгенерировать указатель специального типа, который "ссылается" не на конкретный экземпляр члена в объекте, а на член класса вообще. Указатель такого типа называется указателем на член класса (pointer-to-member). Это — не обычный С++-указатель. Этот специальный указатель обеспечивает только соответствующее смещение в объекте, которое позволяет обнаружить нужный член класса. Поскольку указатели на члены — не настоящие указатели, к ним нельзя применять операторы "." и "- >". Для получения доступа к члену класса через указатель на член необходимо использовать специальные операторы ".*" и "->*". Если идея, изложенная в предыдущем абзаце, вам показалась немного "туманной", то следующий пример поможет ее прояснить. При выполнении этой программы отображается сумма чисел от 1 до 7. Здесь доступ к членам класса myclass (функции sum_it() и переменной sum) реализуется путем использования указателей на члены. // Пример использования указателей на члены класса. #include using namespace std; class myclass { public: int sum; void myclass::sum_it(int x); }; void myclass::sum_it(int x) { int i; sum = 0; for(i=x; i; i--) sum += i; } int main() { int myclass::*dp; // указатель на int-член класса void (myclass::*fp)(int x); // указатель на функцию-член myclass с; dp = &myclass::sum; // получаем адрес члена данных fp = &myclass::sum_it; // получаем адрес функции-члена (c.*fp)(7); // вычисляем сумму чисел от 1 до 7 cout << "Сумма чисел от 1 до 7 равна " << с.*dp; return 0; } Результат выполнения этой программы таков. Сумма чисел от 1 до 7 равна 28 В функции main() создается два члена-указателя: dp (для указания на переменную sum) и fp (для указания на функцию sum_it()). Обратите внимание на синтаксис каждого объявления. Для уточнения класса используется оператор разрешения контекста (оператор разрешения области видимости). Программа также создает объект типа myclass с именем с. Затем программа получает адреса переменной sum и функции sum_it() и присваивает их указателям dp и fp соответственно. Как упоминалось выше, эти адреса в действительности представляют собой лишь смещения в объекте типа myclass, по которым можно найти переменную sum и функцию sum_it(). Затем программа использует указатель на функцию fp, чтобы вызвать функцию sum_it() для объекта с. Наличие дополнительных круглых скобок объясняется необходимостью корректно применить оператор ".*". Наконец, программа отображает значение суммы чисел, получая доступ к переменной sum объекта с через указатель dp. При доступе к члену объекта с помощью объекта или ссылки на него необходимо использовать оператор ".*". Но если для этого используется указатель на объект, нужно использовать оператор "->*", как показано в этой версии предыдущей программы. #include using namespace std; class myclass { public: int sum; void myclass::sum_it(int x); }; void myclass::sum_it(int x) { int i; sum = 0; for(i=x; i; i--) sum += i; } int main() { int myclass::*dp; // указатель на int-член класса void (myclass::*fp)(int x); // указатель на функцию-член myclass *c, d; // член с сейчас -- указатель на объект с = &d; // присваиваем указателю с адрес объекта dp = &myclass::sum; // получаем адрес члена данных sum fp = &myclass::sum_it; // получаем адрес функции sum_it() (c->*fp) (7); // Теперь используем оператор для вызова функции sum_it(). cout << "Сумма чисел от 1 до 7 равна " << c->*dp; // ->* return 0; } В этой версии переменная с объявляется как указатель на объект типа myclass, а для доступа к члену данных sum и функции-члену sum_it() используется оператор "->*". Помните, что операторы указания на члены класса предназначены для специальных случаев, и их не стоит использовать для решения обычных повседневных задач программирования. Создание функций преобразования Функция преобразования автоматически преобразует тип класса в другой тип. Иногда возникает необходимость в одном выражении объединить созданный программистом класс с данными других типов. Несмотря на то что перегруженные операторные функции могут обеспечить использование смешанных типов данных, в некоторых случаях все же можно обойтись простым преобразованием типов. И тогда, чтобы преобразовать класс в тип, совместимый с типом остальной части выражения, можно использовать функцию преобразования типа. Общий формат функции преобразования типа имеет следующий вид. operator type() {return value;} Здесь элемент type — новый тип, который является целью нашего преобразования, а элемент value— значение после преобразования. Функция преобразования должна быть членом класса, для которого она определяется. Чтобы проиллюстрировать создание функции преобразования, воспользуемся классом three_d еще раз. Предположим, что нам нужно иметь средство преобразования объекта типа three_d в целочисленное значение, которое можно использовать в целочисленном выражении. Более того, такое преобразование должно происходить с использованием произведения значений трех координат. Для реализации этого мы будем использовать функцию преобразования, которая выглядит следующим образом, operator int() { return х * у * z; } Теперь рассмотрим программу, которая иллюстрирует работу функции преобразования. #include using namespace std; class three_d { int x, y, z; // 3-мерные координаты public: three_d(int a, int b, int с) { x = a; у = b; z = c; } three_d operator+(three_d op2); friend ostream &operator<<(ostream &stream, three_d &obj); operator int() {return x * у * z; } }; /* Отображение координат X, Y, Z - функция вывода данных для класса three_d. */ ostream &operator<<(ostream &stream, three_d &obj) { stream << obj.x << ", "; stream << obj.у << ", "; stream << obj.z << "\n"; return stream; } three_d three_d::operator+(three_d op2) { three_d temp(0, 0, 0); temp.x = x+op2.x; temp.у = y+op2.y; temp.z = z+op2.z; return temp; } int main() { three_d a(1, 2, 3), b(2, 3, 4); cout << a << b; cout << b+100; /* Отображает число 124, поскольку здесь выполняется преобразование объекта класса в значение типа int. */ cout << "\n"; а = а + b; // Сложение двух объектов класса three_d выполняется без преобразования типа. cout << а; // Отображает координаты 3, 5, 7 return 0; } Эта программа генерирует такие результаты. 1, 2, 3 2, 3, 4 124 3, 5, 7 Как подтверждают результаты выполнения этой программы, если в таком выражении целочисленного типа, как cout<, используется объект типа three_d, к этому объекту применяется функция преобразования. В данном случае функция преобразования возвращает значение 24, которое затем участвует в операции сложения с числом 100. Но когда в преобразовании нет необходимости, как при вычислении выражения а=а+b, функция преобразования не вызывается. Если функция преобразования создана, то она будет вызываться везде, где требуется преобразование, включая ситуации, когда объект передается функции в качестве аргумента. Например, если объект класса three_d передать стандартной функции abs(), также будет вызвана функция, выполняющая преобразование объекта типа three_d в значение типа int, поскольку функция abs() должна принимать аргумент целочисленного типа. Узелок на память. Для различных ситуаций можно создавать различные функции преобразования. Например, можно определить функции, которые преобразуют объекты типа three_d в значения типа double или long, при этом созданные функции будут применяться автоматически. Функции преобразования позволяют интегрировать новые типы классов, создаваемые программистом, в С++-среду программирования. |