Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]
.pdfС++ для начинающих |
632 |
class Token { public:
TokenKind tok;
// анонимное объединение union {
char _cval; int _ival; char *_sval; double _dval;
};
};
К данным-членам анонимного объединения можно напрямую обращаться в той области видимости, в которой оно определено. Перепишем функцию lex(), используя
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;
// ... и т.д.
предыдущее определение:
}
Анонимное объединение позволяет убрать один уровень доступа, поскольку обращение к его членам идет как к членам класса Token. У него не может быть закрытых или защищенных членов, а также функций-членов. Такое объединение, определенное в глобальной области видимости, должно быть объявлено в безымянном пространстве имен или иметь модификатор static.
13.8. Битовое поле – член, экономящий память
Для хранения заданного числа битов можно объявить член класса специального вида, называемый битовым полем. Он должен иметь целый тип данных, со знаком или без
class File { // ...
unsigned int modified : 1; // битовое поле
знака:
};
С++ для начинающих |
633 |
После идентификатора битового поля следует двоеточие, а за ним – константное выражение, задающее число битов. К примеру, modified – это поле из одного бита.
Битовые поля, определенные в теле класса подряд, по возможности упаковываются в соседние биты одного целого числа, делая хранение объекта более компактным. Так, в
следующем объявлении пять битовых полей будут содержаться в одном числе типа
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; // ...
unsigned int, ассоциированном с первым полем mode:
};
Доступ к битовому полю осуществляется так же, как к прочим членам класса. Скажем, к битовому полю, являющемуся закрытым членом класса, можно обратиться лишь из
void File::write()
{
modified = 1; // ...
}
void File::close()
{
if ( modified )
// ... сохранить содержимое
функций-членов и друзей этого класса:
}
Вот простой пример использования битового поля длиной больше 1 (примененные здесь
enum { READ = 01, WRITE = 02 }; // режимы открытия файла
int main() { File myFile;
myFile.mode |= READ;
if ( myFile.mode & READ )
cout << "myFile.mode is set to READ\n";
побитовые операции рассматривались в разделе 4.11):
}
Обычно для проверки значения битового поля-члена определяются встроенные функции- члены. Допустим, в классе File можно ввести члены isRead() и isWrite():
С++ для начинающих |
634 |
inline int File::isRead() { return mode & READ; } inline int File::isWrite() { return mode & WRITE; }
if ( myFile.isRead() ) /* ... */
С помощью таких функций-членов битовые поля можно сделать закрытыми членами класса File.
К битовому полю нельзя применять оператор взятия адреса (&), поэтому не может быть и указателя на подобные поля-члены. Кроме того, полю запрещено быть статическим членом.
В стандартной библиотеке C++ имеется шаблон класса bitset, который облегчает манипуляции с битовыми множествами. Мы рекомендуем использовать его вместо битовых полей. (Шаблон класса bitset и определенные в нем операции рассматривались в разделе 4.12.)
Упражнение 13.17
Перепишите примеры из этого подраздела так, чтобы в классе File вместо объявления и
прямого манипулирования битовыми полями использовался класс bitset и его операторы.
13.9. Область видимости класса A
Тело класса определяет область видимости. Объявления членов класса внутри тела вводят их имена в область видимости класса.
Для обращения к ним применяются операторы доступа (точка и стрелка) и оператор разрешения области видимости (::). Когда употребляется оператор доступа, то предшествующее ему имя обозначает объект или указатель на объект типа класса, а следующее за ним имя должно находиться в области видимости этого класса. Аналогично при использовании оператора разрешения области видимости поиск имени, следующего за ним, идет в области видимости класса, имя которого стоит перед оператором. (В главах 17 и 18 мы увидим, что производный класс может обращаться к членам своих базовых.)
Однако применение операторов доступа или оператора разрешения области видимости нужно не всегда. Некоторые части программы сами по себе находятся в области видимости класса, и в них к членам класса можно обращаться напрямую. Одной из таких частей является само определение класса. Имя его члена можно использовать в теле после
class String { public:
typedef int index_type;
// тип параметра - это на самом деле String::index_type char& operator[]( index_type )
объявления:
};
Порядок объявления членов класса в его теле важен: нельзя ссылаться на члены, которые будут объявлены позже. Например, если объявление оператора operator[]() находится
С++ для начинающих |
635 |
раньше объявления typedef index_type, то приведенное ниже объявление operator[]() оказывается ошибочным, поскольку в нем используется еще неизвестное
class String { public:
// ошибка: имя index_type не объявлено char &operator[]( index_type );
typedef int index_type;
имя index_type:
};
Однако из этого правила есть два исключения. Первое касается имен, использованных в определениях встроенных функций-членов, второе – имен, применяемых как аргументы по умолчанию. Рассмотрим обе ситуации.
Разрешение имен в определениях встроенных функций-членов происходит в два этапа. Сначала объявление функции (т.е. тип возвращаемого значения и список параметров) обрабатывается в том месте, где оно встретилось в определении класса. Затем тело функции обрабатывается во всей области видимости, сразу после того, как были просмотрены объявления всех членов. Посмотрим на наш пример, в котором оператор
class String { public:
typedef int index_type;
char &operator[]( index_type elem ) { return _string[ elem ]; }
private:
char *_string;
operator[]() определен как встроенный внутри тела класса:
};
На первом этапе просматриваются имена, использованные в объявлении operator[](), чтобы найти имя типа параметра index_type. Поскольку первый шаг выполняется тогда, когда в теле класса встретилось определение функции-члена, то имя index_type должно быть объявлено до определения operator[]().
Обратите внимание, что член _string объявлен в теле класса после определения operator[](). Это правильно, и _string не является в теле operator[]()
необъявленным именем. Имена в телах функций-членов просматриваются на втором шаге разрешения имен в определениях встроенных функций-членов. Этот этап выполняется во всей области видимости класса, как если бы тела функций-членов обрабатывались последними, прямо перед закрытием тела класса, когда все его члены уже объявлены.
Аргументы по умолчанию также разрешаются на втором шаге. Например, в объявлении функции-члена clear() используется имя статического члена bkground, который определен позже:
С++ для начинающих |
636 |
class Screen { public:
//bkground относится к статическому члену,
//объявленному позже в определении класса
Screen& clear( char = bkground ); private:
static const char bkground = '#';
};
Хотя такие аргументы в объявлениях функций-членов разрешаются во всей области видимости класса, программа будет считаться ошибочной, если он ссылается на нестатический член. Нестатический член должен быть привязан к объекту своего класса или к указателю на такой объект, иначе использовать его нельзя. Употребление подобных членов в качестве аргументов по умолчанию нарушает это ограничение. Если переписать
class Screen { public:
//...
//ошибка: bkground - нестатический член
Screen& clear( char = bkground );
private:
const char bkground = '#';
предыдущий пример так:
};
то имя аргумента по умолчанию разрешается нестатическим членом bkground, а это считается ошибкой.
Определения членов класса, появляющиеся вне его тела, – это еще один пример части программы, которая находится в области видимости класса. В ней имена членов распознаются несмотря на то, что оператор доступа или оператор разрешения области видимости при обращении к ним не применяется. Как же разрешаются имена в определениях членов?
Как правило, если такое определение появляется вне тела, то часть программы, следующая за именем определяемого члена, считается находящейся в области видимости класса вплоть до конца определения члена. Вынесем определение оператора
class String { public:
typedef int index_type;
char& operator[]( index_type ); private:
char *_string;
};
// в operator[]() есть обращения к index_type и _string inline char& operator[]( index_type elem )
{
return _string[ elem ];
operator[]() из класса String:
}
С++ для начинающих |
637 |
Обратите внимание, что в списке параметров встречается typedef index_type без квалифицирующего имени класса String::.Текст, следующий за именем члена String::operator[] и до конца определения функции, находится в области видимости класса. Объявленные в этой области типы рассматриваются при разрешении имен типов, использованных в списке параметров функции-члена.
Определения статических данных-членов также появляются вне определения класса. В них часть программы, следующая за именем статического члена вплоть до конца определения, считается находящейся в области видимости класса. Например, инициализатор статического члена может непосредственно, без соответствующих
class Account: // ...
private:
static double _interestRate; static double initInterestRate();
};
// ссылается на Account::initInterest()
операторов, ссылаться на члены класса:
double Account::_interestRate = initInterest();
Инициализатор _interestRate вызывает статическую функцию-член Account::initInterest() несмотря на то, что ее имя не квалифицировано именем класса.
Не только инициализатор, но и все, что следует за именем статического члена _interestRate до завершающей точки с запятой, находится в области видимости класса Account. Поэтому в определении статического члена name может быть обращение к
class Account: // ...
private:
static const int nameSize = 16; static const char name[nameSize];
// nameSize не квалифицировано именем класса Account
члену класса nameSize:
const char Account::name[nameSize] = "Savins Account";
Хотя член nameSize не квалифицирован именем класса Account, определение name не является ошибкой, так как оно находится в области видимости своего класса и может ссылаться на его члены после того, как компилятор прочитал Account::name.
В определении члена, которое появляется вне тела, часть программы перед определяемым именем не находится в области видимости класса. При обращении к члену в этой части следует пользоваться оператором разрешения области видимости. Например, если типом статического члена является typedef Money, определенный в классе Account, то имя Money должно быть квалифицировано, когда статический член данных определяется вне тела класса:
С++ для начинающих |
638 |
class Account {
typedef double Money; //...
private:
static Money _interestRate; static Money initInterest();
};
// Money должно быть квалифицировано именем класса Account::
Account::Money Account::_interestRate = initInterest();
С каждым классом ассоциируется отдельная область видимости, причем у разных классов эти области различны. К членам одного класса нельзя напрямую обращаться в определениях членов другого класса, если только один из них не является для второго базовым. (Наследование и базовые классы рассматриваются в главах 17 и 18.)
13.9.1. Разрешение имен в области видимости класса
Конечно, имена, используемые в области видимости класса, не обязаны быть именами членов класса. В процессе разрешения в этой области ведется поиск имен, объявленных и в других областях. Если имя, употребленное в области видимости класса, не разрешается именем члена класса, то компилятор ищет его в областях, включающих определение класса или члена. В этом подразделе мы покажем, как разрешаются имена, встречающиеся в области видимости класса.
Имя, использованное внутри определения класса (за исключением определений встроенных функций-членов и аргументов по умолчанию), разрешается следующим образом:
1.Просматриваются объявления членов класса, появляющиеся перед употреблением имени.
2.Если на шаге 1 разрешение не привело к успеху, то просматриваются объявления в пространстве имен перед определением класса. Напомним, что глобальная область видимости – это тоже область видимости пространства имен. (О пространствах имен речь шла в разделе 8.5.)
typedef double Money; class Account {
//...
private:
static Money _interestRate; static Money initInterest();
//...
Например:
};
Сначала компилятор ищет объявление Money в области видимости класса Account. При этом учитываются только те объявления, которые встречаются перед использованием Money. Поскольку таких объявлений нет, далее поиск ведется в глобальной области видимости. Объявление глобального typedef Money найдено, именно этот тип и используется в объявлениях _interestRate и initInterest().
С++ для начинающих |
639 |
Имя, встретившееся в определении функции-члена класса, разрешается следующим образом:
1.Сначала просматриваются объявления в локальных областях видимости функции- члена. (О локальных областях видимости и локальных объявлениях говорилось в разделе 8.1.)
2.Если шаг 1 не привел к успеху, то просматриваются объявления для всех членов класса.
3.Если и этого оказалось недостаточно, просматриваются объявления в пространстве имен перед определением функции-члена.
int _height;
class Screen { public:
Screen( int _height ) {
_height = 0; // к чему относится _height? К параметру
}
private:
short _height;
Имена, встречающиеся в теле встроенной функции-члена, разрешаются так:
};
В поисках объявления имени _height, которое встретилось в определении конструктора Screen, компилятор просматривает локальную область видимости функции и находит его там. Следовательно, это имя относится к объявлению параметра.
Если бы такое объявление не было найдено, компилятор начал бы поиск в области видимости класса Screen, просматривая все объявления его членов, пока не встретится объявление члена _height. Говорят, что имя члена _height скрыто объявлением параметра конструктора, но его можно использовать в теле конструктора, если
int _height;
class Screen { public:
Screen( long _height ) {
this->_height = 0; // относится к Screen::_height
//тоже правильно:
//Screen::_height = 0;
}
private:
short _height;
квалифицировать имя члена именем его класса или явно использовать указатель this:
};
Если бы не были найдены ни объявление параметра, ни объявление члена, компилятор стал бы искать их в объемлющих областях видимости пространств имен. В нашем примере в глобальной области видимости просматриваются объявления, которые расположены перед определением класса Screen. В результате было бы найдено объявление глобального объекта _height. Говорят, что такой объект скрыт за
С++ для начинающих |
640 |
объявлением члена класса, однако его можно использовать в теле конструктора, если
int _height;
class Screen { public:
Screen( long _height ) {
::_height = 0; // относится к глобальному объекту
}
private:
short _height;
квалифицировать оператором разрешения глобальной области видимости:
};
Если конструктор объявлен вне определения класса, то на третьем шаге разрешения имени просматриваются объявления в глобальной области видимости, которые встретились перед определением класса Screen, а также перед определением функции-
class Screen { public:
// ...
void setHeight( int ); private:
short _height;
};
int verify(int);
void Screen::setHeight( int var ) {
//var: относится к параметру
//_height: относится к члену класса
//verify: относится к глобальной функции
_height = verify( var );
члена:
}
Обратите внимание, что объявление глобальной функции verify() невидимо до определения класса Screen. Однако на третьем шаге разрешения имени просматриваются объявления в областях видимости пространств имен, видимые перед определением члена, поэтому нужное объявление обнаруживается.
Имя, встретившееся в определении статического члена класса, разрешается следующим образом:
1.Просматриваются объявления всех членов класса.
2.Если шаг 1 не привел к успеху, то просматриваются объявления, расположенные в областях видимости пространств имен перед определением статического члена, а не только предшествующие определению класса.
Упражнение 13.18
Назовите те части программы, которые находятся в области видимости класса.
С++ для начинающих |
641 |
Упражнение 13.19
Назовите те части программы, которые находятся в области видимости класса и для которых при разрешении имен просматривается полная область (т.е. принимаются во внимание все члены, объявленные в теле класса).
Упражнение 13.20
К каким объявлениям относится имя Type при использовании в теле класса Exersise и в определении его функции-члена setVal()? (Напоминаем, что разные вхождения могут относиться к разным объявлениям.) К каким объявлениям относится имя initVal при
typedef int Type; Type initVal();
class Exercise { public:
// ...
typedef double Type; Type setVal( Type ); Type initVal();
private: int val;
};
Type Exercise::setVal( Type parm ) { val = parm + initVal();
употреблении в определении функции-члена setVal()?
}
Определение функции-члена setVal() ошибочно. Можете ли вы сказать, почему? Внесите необходимые изменения, чтобы в классе Exercise использовался глобальный typedef Type и глобальная функция initVal().
13.10. Вложенные классы A
Класс, объявленный внутри другого класса, называется вложенным. Он является членом объемлющего класса, а его определение может находиться в любой из секций public, private или protected объемлющего класса.
Имя вложенного класса известно в области видимости объемлющего класса, но ни в каких других областях. Это означает, что оно не конфликтует с таким же именем, объявленным в объемлющей области видимости. Например: