Главная страница
Навигация по странице:

  • 13.6.3. Указатели на статические члены класса

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница60 из 93
    1   ...   56   57   58   59   60   61   62   63   ...   93
    624
    }
    Параметр op – это указатель на функцию-член, которая должна вызываться times раз.
    Если бы нужно было задать значения аргументов по умолчанию, то объявление repeat() выглядело бы следующим образом:
    };
    А ее вызовы так: myScreen.repeat( &Screen::down, 20 );
    Определим таблицу указателей. В следующем примере Menu – это таблица указателей на функции-члены класса
    Screen
    , которые реализуют перемещение курсора.
    CursorMovements
    – перечисление, элементами которого являются номера в таблице
    Menu
    };
    Можно определить перегруженную функцию-член move(), которая принимает параметр
    CursorMovements и использует таблицу Menu для вызова указанной функции-члена. Вот ее реализация: typedef Screen& (Screen::Action)();
    Screen& Screen::repeat( Action op, int times )
    { for ( int i = 0; i < times; ++i )
    (this->*op)(); return *this; class Screen { public:
    Screen &repeat( Action = &Screen::forward, int = 1 );
    // ...
    Screen myScreen; myScreen.repeat(); // repeat( &Screen::forward, 1 );
    Action::Menu() = {
    &Screen::home,
    &Screen::forward,
    &Screen::back,
    &Screen::up,
    &Screen::down,
    &Screen::end
    }; enum CursorMovements {
    HOME, FORWARD, BACK, UP, DOWN, END

    С++ для начинающих
    625
    }
    У оператора взятия индекса ([]) приоритет выше, чем у оператора указателя на функцию-член (->*). Первая инструкция в move() сначала по индексу выбирает из таблицы Menu нужную функцию-член, которая и вызывается с помощью указателя this и оператора указателя на функцию-член. move() можно применять в интерактивной программе, где пользователь выбирает вид перемещения курсора из отображаемого на экране меню.
    13.6.3.
    Указатели на статические члены класса
    Между указателями на статические и нестатические члены класса есть разница.
    Синтаксис указателя на член класса не используется для обращения к статическому члену. Статические члены – это глобальные объекты и функции, принадлежащие классу.
    Указатели на них – это обычные указатели. (Напомним, что статической функции-члену не передается указатель this.)
    Объявление указателя на статический член класса выглядит так же, как и для указателя на объект, не являющийся членом класса. Для разыменования указателя никакой объект не требуется. Рассмотрим класс Account:
    }
    Тип &_interestRate – это double*: double Account::*
    Определение указателя на &_interestRate имеет вид:
    Screen& Screen::move( CursorMovements cm )
    {
    ( this->*Menu[ cm ] )(); return *this; class Account { public: static void raiseInterest( double incr ); static double interest() { return _interestRate ; } double amount() { return _amount; } private: static double _interestRate; double _amount; string _owner;
    }; inline void Account::raiseInterest( double incr )
    {
    _interestRate += incr;
    // это неправильный тип для &_interestRate

    С++ для начинающих
    626
    double *pd = &Account::_interestRate;
    Этот указатель разыменовывается так же, как и обычный, объект класса для этого не требуется: double daily = *pd / 365 * unit._amount;
    Однако, поскольку _interestRate и _amount – закрытые члены, необходимо иметь статическую функцию-член interest() и нестатическую amount().
    Указатель на interest() – это обычный указатель на функцию: double (*)() а не на функцию-член класса Account: double (Account::*)()
    Определение указателя и косвенный вызов interest() реализуются так же, как и для обычных указателей: double daily = pf() / 365 * unit.amount();
    Упражнение 13.11
    К какому типу принадлежат члены _screen и _cursor класса Screen?
    Упражнение 13.12
    Определите указатель на член и инициализируйте его значением Screen::_screen; присвойте ему значение Screen::_cursor.
    Упражнение 13.13
    Определите typedef для каждой из функций-членов класса Screen.
    Упражнение 13.14
    // правильно: double*, а не double Account::*
    Account unit;
    // используется обычный оператор разыменования
    // правильно
    // неправильно
    // правильно: double(*pf)(), а не double(Account::*pf)() double(*pf)() = &Account::interest;

    С++ для начинающих
    627
    Указатели на члены можно также объявлять как данные-члены класса. Модифицируйте определение класса Screen так, чтобы оно содержало указатель на его функцию-член того же типа, что home() и end().
    Упражнение 13.15
    Модифицируйте имеющийся конструктор класса Screen (или напишите новый) так, чтобы он принимал параметр типа указателя на функцию-член класса Screen, для которой список формальных параметров и тип возвращаемого значения такие же, как у home()
    и end(). Реализуйте для этого параметра значение по умолчанию и используйте параметр для инициализации члена класса, описанного в упражнении 13.14. Напишите функцию-член Screen, позволяющую пользователю задать ее значение.
    Упражнение 13.16
    Определите перегруженный вариант repeat(), который принимает параметр типа cursorMovements
    13.7.
    Объединение – класс, экономящий память
    Объединение – это специальный вид класса. Данные-члены хранятся в нем таким образом, что перекрывают друг друга. Все члены размещаются, начиная с одного и того же адреса. Для объединения отводится столько памяти, сколько необходимо для хранения самого большого его члена. В любой момент времени можно присвоить значение лишь одному такому члену.
    Рассмотрим пример, иллюстрирующий использование объединения. Лексический анализатор, входящий в состав компилятора, разбивает программу на последовательность лексем. Так, инструкция int i = 0; преобразуется в последовательность из пяти лексем:
    1. Ключевое слово int.
    2. Идентификатор i.
    3. Оператор =
    4. Константа 0 типа int.
    5. Точка с запятой.
    Лексический анализатор передает эти лексемы синтаксическому анализатору, парсеру, который идентифицирует полученную последовательность. Полученная информация должна дать парсеру возможность распознать эту последовательность лексем как объявление. Для этого с каждой лексемой ассоциируется информация, позволяющая парсеру увидеть следующее:
    (
    Тип ИД Присваивание Константа Точка с запятой)
    Далее парсер анализирует значения каждой лексемы. В данном случае он видит:
    Type ID Assign Constant Semicolon

    С++ для начинающих
    628
    Constant <==> 0
    Для Assign и Semicolon дополнительной информации не нужно, так как у них может быть только одно значение: соответственно := и ;.
    Таким образом, в представлении лексемы могло бы быть два члена – token и value. token
    – это уникальный код, показывающий, что лексема имеет тип Type, ID, Assign,
    Constant или Semicolon, например 85 для ID и 72 для Semicolon.value содержит конкретное значение лексемы. Так, для лексемы ID в предыдущем объявлении value будет содержать строку "i", а для лексемы Type – некоторое представление типа int.
    Представление члена value несколько проблематично. Хотя для любой отдельной лексемы в нем хранится всего одно значение, их типы для разных лексем могут различаться. Для лексемы ID в value хранится строка символов, а для Constant – целое число.
    Конечно, для хранения данных нескольких типов можно использовать класс. Разработчик компилятора может объявить, что value принадлежит к типу класса, в котором для каждого типа данных есть отдельный член.
    Применение класса решает проблему представления value. Однако для любой данной лексемы value имеет лишь один из множества возможных типов и, следовательно, будет задействован только один член класса, хотя памяти выделяется столько, сколько нужно для хранения всех членов. Чтобы память резервировалась только для нужного в данный момент члена, применяется объединение. Вот как оно определяется:
    };
    Если самым большим типом среди всех членов TokenValue является dval, то размер
    TokenValue будет равен размеру объекта типа double. По умолчанию члены объединения открыты. Имя объединения можно использовать в программе всюду, где допустимо имя класса:
    TokenValue *pt = new TokenValue;
    Обращение к членам объединения, как и к членам класса, производится с помощью операторов доступа:
    Type <==> int
    ID <==> i union TokenValue { char _cval; int _ival; char *_sval; double _dval;
    // объект типа TokenValue
    TokenValue last_token;
    // указатель на объект типа TokenValue

    С++ для начинающих
    629
    char ch = pt->_cval;
    Члены объединения можно объявлять открытыми, закрытыми или защищенными:
    }
    У объединения не бывает статических членов или членов, являющихся ссылками. Его членом не может быть класс, имеющий конструктор, деструктор или копирующий оператор присваивания. Например:
    };
    Для объединения разрешается определять функции-члены, включая конструкторы и деструкторы: last_token._ival = 97; union TokenValue { public: char _cval;
    // ... private: int priv;
    } int main() {
    TokenValue tp; tp._cval = '\n'; // правильно
    // ошибка: main() не может обращаться к закрытому члену
    // TokenValue::priv tp.priv = 1024; union illegal_members {
    Screen s; // ошибка: есть конструктор
    Screen *ps; // правильно static int is; // ошибка: статический член int &rfi; // ошибка: член-ссылка

    С++ для начинающих
    630
    }
    Вот пример работы объединения TokenValue:
    };
    Объект типа Token можно использовать так:
    }
    Опасность, связанная с применением объединения, заключается в том, что можно случайно извлечь хранящееся в нем значение, пользуясь не тем членом. Например, если в последний раз значение присваивалось _ival, то вряд ли понадобится значение, оказавшееся в _sval. Это, по всей вероятности, приведет к ошибке в программе. union TokenValue { public:
    TokenValue(int ix) : _ival(ix) { }
    TokenValue(char ch) : _cval(ch) { }
    // ... int ival() { return _ival; } char cval() { return _cval; } private: int _ival; char _cval;
    // ...
    }; int main() {
    TokenValue tp(10); int ix = tp.ival();
    //... enum TokenKind ( ID, Constant /* и другие типы лексем */ } class Token { public:
    TokenKind tok;
    TokenValue val; int lex() {
    Token curToken; char *curString; int curIval;
    // ... case ID: // идентификатор curToken.tok = ID; curToken.val._sval = curString; break; case Constant: // целая константа curToken.tok = Constant; curToken.val._ival = curIval; break;
    // ... и т.д.

    С++ для начинающих
    631
    Чтобы защититься от подобного рода ошибок, следует создать дополнительный объект,
    дискриминант объединения, определяющий тип значения, которое в данный момент хранится в объединении. В классе Token роль такого объекта играет член tok: idVal = curToken.val._sval;
    При работе с объединением, являющимся членом класса, полезно иметь набор функций для каждого хранящегося в объединении типа данных:
    }
    Имя в определении объединения задавать необязательно. Если оно не используется в программе как имя типа для объявления других объектов, его можно опустить.
    Например, следующее определение объединения Token эквивалентно приведенному выше, но без указания имени:
    };
    Существует анонимное объединениеобъединение без имени, за которым не следует определение объекта. Вот, например, определение класса Token, содержащее анонимное объединение: char *idVal;
    // проверить значение дискриминанта перед тем, как обращаться к sval if ( curToken.tok == ID )
    #include
    // функции доступа к члену объединения sval string Token::sval() { assert( tok==ID ); return val._sval; class Token { public:
    TokenKind tok;
    // имя типа объединения опущено union { char _cval; int _ival; char *_sval; double _dval;
    } val;

    С++ для начинающих
    632
    };
    К данным-членам анонимного объединения можно напрямую обращаться в той области видимости, в которой оно определено. Перепишем функцию lex(), используя предыдущее определение:
    }
    Анонимное объединение позволяет убрать один уровень доступа, поскольку обращение к его членам идет как к членам класса Token. У него не может быть закрытых или защищенных членов, а также функций-членов. Такое объединение, определенное в глобальной области видимости, должно быть объявлено в безымянном пространстве имен или иметь модификатор static.
    13.8.
    Битовое поле – член, экономящий память
    Для хранения заданного числа битов можно объявить член класса специального вида, называемый битовым полем. Он должен иметь целый тип данных, со знаком или без знака:
    }; class Token { public:
    TokenKind tok;
    // анонимное объединение union { char _cval; int _ival; char *_sval; double _dval;
    }; int lex() {
    Token curToken; char *curString; int curIval;
    // ... выяснить, что находится в лексеме
    // ... затем установить curToken case ID: curToken.tok = ID; curToken._sval = curString; break; case Constant: // целая константа curToken.tok = Constant; curToken._ival = curIval; break;
    // ... и т.д. class File {
    // ... unsigned int modified : 1; // битовое поле

    С++ для начинающих
    633
    После идентификатора битового поля следует двоеточие, а за ним – константное выражение, задающее число битов. К примеру, modified – это поле из одного бита.
    Битовые поля, определенные в теле класса подряд, по возможности упаковываются в соседние биты одного целого числа, делая хранение объекта более компактным. Так, в следующем объявлении пять битовых полей будут содержаться в одном числе типа unsigned int
    , ассоциированном с первым полем mode:
    };
    Доступ к битовому полю осуществляется так же, как к прочим членам класса. Скажем, к битовому полю, являющемуся закрытым членом класса, можно обратиться лишь из функций-членов и друзей этого класса:
    }
    Вот простой пример использования битового поля длиной больше 1 (примененные здесь побитовые операции рассматривались в разделе 4.11):
    }
    Обычно для проверки значения битового поля-члена определяются встроенные функции- члены. Допустим, в классе File можно ввести члены isRead() и isWrite(): typedef unsigned int Bit; class File { public:
    Bit mode: 2;
    Bit modified: 1;
    Bit prot_owner: 3;
    Bit prot_group: 3;
    Bit prot_world: 3;
    // ... void File::write()
    { modified = 1;
    // ...
    } void File::close()
    { if ( modified )
    // ... сохранить содержимое enum { READ = 01, WRITE = 02 }; // режимы открытия файла int main() {
    File myFile; myFile.mode |= READ; if ( myFile.mode & READ ) cout << "myFile.mode is set to READ\n";

    С++ для начинающих
    634
    if ( myFile.isRead() ) /* ... */
    С помощью таких функций-членов битовые поля можно сделать закрытыми членами класса File.
    К битовому полю нельзя применять оператор взятия адреса (&), поэтому не может быть и указателя на подобные поля-члены. Кроме того, полю запрещено быть статическим членом.
    В стандартной библиотеке C++ имеется шаблон класса bitset, который облегчает манипуляции с битовыми множествами. Мы рекомендуем использовать его вместо битовых полей. (Шаблон класса bitset и определенные в нем операции рассматривались в разделе 4.12.)
    Упражнение 13.17
    Перепишите примеры из этого подраздела так, чтобы в классе File вместо объявления и прямого манипулирования битовыми полями использовался класс bitset и его операторы.
    13.9.
    Область видимости класса A
    Тело класса определяет область видимости. Объявления членов класса внутри тела вводят их имена в область видимости класса.
    Для обращения к ним применяются операторы доступа (точка и стрелка) и оператор разрешения области видимости (::). Когда употребляется оператор доступа, то предшествующее ему имя обозначает объект или указатель на объект типа класса, а следующее за ним имя должно находиться в области видимости этого класса.
    Аналогично при использовании оператора разрешения области видимости поиск имени, следующего за ним, идет в области видимости класса, имя которого стоит перед оператором. (В главах 17 и 18 мы увидим, что производный класс может обращаться к членам своих базовых.)
    Однако применение операторов доступа или оператора разрешения области видимости нужно не всегда. Некоторые части программы сами по себе находятся в области видимости класса, и в них к членам класса можно обращаться напрямую. Одной из таких частей является само определение класса. Имя его члена можно использовать в теле после объявления:
    };
    Порядок объявления членов класса в его теле важен: нельзя ссылаться на члены, которые будут объявлены позже. Например, если объявление оператора operator[]() находится inline int File::isRead() { return mode & READ; } inline int File::isWrite() { return mode & WRITE; } class String { public: typedef int index_type;
    // тип параметра - это на самом деле String::index_type char& operator[]( index_type )

    С++ для начинающих
    635
    раньше объявления typedef index_type, то приведенное ниже объявление operator[]()
    оказывается ошибочным, поскольку в нем используется еще неизвестное имя index_type:
    };
    Однако из этого правила есть два исключения. Первое касается имен, использованных в определениях встроенных функций-членов, второе – имен, применяемых как аргументы по умолчанию. Рассмотрим обе ситуации.
    Разрешение имен в определениях встроенных функций-членов происходит в два этапа.
    Сначала объявление функции (т.е. тип возвращаемого значения и список параметров) обрабатывается в том месте, где оно встретилось в определении класса. Затем тело функции обрабатывается во всей области видимости, сразу после того, как были просмотрены объявления всех членов. Посмотрим на наш пример, в котором оператор operator[]()
    определен как встроенный внутри тела класса:
    };
    На первом этапе просматриваются имена, использованные в объявлении operator[](), чтобы найти имя типа параметра index_type. Поскольку первый шаг выполняется тогда, когда в теле класса встретилось определение функции-члена, то имя index_type должно быть объявлено до определения operator[]().
    Обратите внимание, что член _string объявлен в теле класса после определения operator[]()
    . Это правильно, и _string не является в теле operator[]() необъявленным именем. Имена в телах функций-членов просматриваются на втором шаге разрешения имен в определениях встроенных функций-членов. Этот этап выполняется во всей области видимости класса, как если бы тела функций-членов обрабатывались последними, прямо перед закрытием тела класса, когда все его члены уже объявлены.
    Аргументы по умолчанию также разрешаются на втором шаге. Например, в объявлении функции-члена clear() используется имя статического члена bkground, который определен позже: class String { public:
    // ошибка: имя index_type не объявлено char &operator[]( index_type ); typedef int index_type; class String { public: typedef int index_type; char &operator[]( index_type elem )
    { return _string[ elem ]; } private: char *_string;

    С++ для начинающих
    1   ...   56   57   58   59   60   61   62   63   ...   93


    написать администратору сайта