Главная страница

Язык программирования C Пятое издание


Скачать 1.85 Mb.
НазваниеЯзык программирования C Пятое издание
Дата15.07.2019
Размер1.85 Mb.
Формат файлаpdf
Имя файла620354-www.libfox.ru.pdf
ТипДокументы
#84130
страница20 из 54
1   ...   16   17   18   19   20   21   22   23   ...   54
Упражнение 7.17. Каковы различия (если они есть) между ключевыми словами class и struct?
Упражнение 7.18. Что такое инкапсуляция? Чем она полезна?
Упражнение 7.19. Укажите, какие члены класса Person имеет смысл объявить как public, а какие как private. Объясните свой выбор.
7.2.1. Друзья
Теперь, когда переменные-члены класса Sales_data стали закрытыми, функции read(), print() и add() перестали компилироваться. Проблема в том, что хоть эти функции и являются частью интерфейса класса Sales_data, его членами они не являются.
Класс может позволить другому классу или функции получить доступ к своим не открытым членам, установив для них дружественные отношения (friend). Класс объявляет функцию дружественной, включив ее объявление с предваряющим ключевым словом friend: class Sales_data {
// добавлены объявления дружественных функций, не являющихся
// членами класса Sales_data friend Sales_data add(const Sales_data&, const Sales_data&); friend std::istream &read(std::istream&, Sales_data&);
Page 345/1103
friend std::ostream &print(std::ostream&, const Sales_data&);
// другие члены и спецификаторы доступа, как прежде public:
Sales_data() = default;
Sales data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue (p*n) { }
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(std::istream&); std::string isbn() const { return bookNo; }
Sales_data &combine(const Sales data&); private: std::string bookNo; unsigned units_sold = 0; double revenue = 0.0;
};
// объявления частей, не являющихся членами интерфейса
// класса Sales_data
Sales_data add(const Sales_data&, const Sales_data&); std::istream &read(std::istream&, Sales_data&); std::ostream &print(std::ostream&, const Sales_data&);
Объявления друзей могут располагаться только в определении класса; использоваться они могут в классе повсюду. Друзья не являются членами класса и не подчиняются спецификаторам доступа раздела, в котором они объявлены. Более подробная информация о дружественных отношениях приведена в разделе 7.3.4.
Объявления друзей имеет смысл группировать в начале или в конце определения класса.
Хотя пользовательский код не должен изменяться при изменении определения класса,
файлы исходного кода, использующие этот класс, следует перекомпилировать при каждом изменении класса. Объявление дружественных отношений
Объявление дружественных отношений устанавливает только право доступа. Это не объявление функции. Если необходимо, чтобы пользователи класса были в состоянии
Page 346/1103
вызвать дружественную функцию, ее следует также объявить.
Чтобы сделать друзей класса видимыми его пользователям, их обычно объявляют вне класса в том же заголовке, что и сам класс. Таким образом, в заголовке Sales_data следует предоставить отдельные объявления (кроме объявлений дружественными в теле класса) для функций read(), print() и add().
Многие компиляторы не выполняют правило, согласно которому дружественные функции должны быть объявлены вне класса, прежде чем они будут применены.
Некоторые компиляторы позволяют вызвать дружественную функцию, когда для нее нет обычного объявления. Даже если ваш компилятор позволяет такие вызовы, имеет смысл предоставлять отдельные объявления для дружественных функций. Так не придется переделывать весь код, если вы перейдете на компилятор, который выполняет это правило.
Упражнения раздела 7.2.1
Упражнение 7.20. Когда полезны дружественные отношения? Укажите преимущества и недостатки их использования.
Упражнение 7.21. Измените свой класс Sales_data так, чтобы скрыть его реализацию.
Написанные вами программы, которые использовали операции класса Sales_data, должны продолжить работать. Перекомпилируйте эти программы с новым определением класса,
чтобы проверить, остались ли они работоспособными.
Упражнение 7.22. Измените свой класс Person так, чтобы скрыть его реализацию.
7.3. Дополнительные средства класса
Хотя класс Sales_data довольно прост, он все же позволил исследовать немало средств поддержки классов. В этом разделе рассматриваются некоторые из дополнительных средств,
связанных с классом, которые класс Sales_data не будет использовать. К этим средствам относятся типы-члены (type member), внутриклассовые инициализаторы для типов-членов класса,
изменяемые переменные-члены, встраиваемые функции-члены, функции-члены,
возвращающие *this, а также подробности определения и использования типов класса и дружественных классов.
7.3.1. Снова о членах класса
Для исследования некоторых из дополнительных средств определим пару взаимодействующих классов по имени Screen и Window_mgr. Определение типов-членов
Класс Screen представляет окно на экране. У каждого объекта класса Screen есть переменная-член типа string, хранящая содержимое окна и три переменные-члена типа string::size_type, представляющие позицию курсора, высоту и ширину окна.
Page 347/1103

Кроме переменных и функций-членов, класс может определять собственные локальные имена таких типов. Определенные классом имена типов подчиняются тем же правилам доступа, что и любой другой его член, и могут быть открытыми или закрытыми: class Screen { public: typedef std::string::size_type pos; private: pos cursor = 0; pos height = 0, width = 0; std::string contents;
};
Тип pos определен в части public класса Screen, поскольку пользователи должны использовать это имя. Пользователи класса Screen не обязаны знать, что он использует класс string для хранения своих данных. Определив тип pos как открытый член, эту подробность реализации класса Screen можно скрыть.
В объявлении типа pos есть два интересных момента. Во-первых, хоть здесь и был использован оператор typedef (см. раздел 2.5.1), с таким же успехом можно использовать псевдоним типа (см. раздел 2.5.1): class Screen { public:
// альтернативный способ объявления типа-члена с использованием
// псевдонима типа using pos = std::string::size_type;
// другие члены как прежде
};
Во-вторых, по причинам, которые будут описаны в разделе 7.3.4, в отличие от обычных членов, типы-члены определяются прежде, чем используются. В результате типы-члены обычно располагают в начале класса. Функции-члены класса Screen
Чтобы сделать наш класс полезней, добавим в него конструктор, позволяющий пользователям задавать размер и содержимое экрана, наряду с членами, позволяющими переместить курсор и получить символ в указанной позиции: class Screen {
Page 348/1103
public: typedef std::string::size_type pos;
Screen() = default; // необходим, поскольку у класса Screen есть
// другой конструктор
// внутриклассовый инициализатор инициализирует курсор значением 0
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c) { } char get() const // получить символ в курсоре
{ return contents [cursor]; } // неявно встраиваемая inline char get(pos ht, pos wd) const; // явно встраиваемая
Screen &move(pos r, pos с); // может быть сделана встраиваемой позже private: pos cursor = 0; pos height = 0, width = 0; std::string contents;
};
Поскольку мы предоставляем конструктор, компилятор не будет автоматически создавать стандартный конструктор сам. Если у нашего класса должен быть стандартный конструктор,
то придется создать его явно. В данном случае используется синтаксис = default, чтобы попросить компилятор самому создать определение стандартного конструктора (см. раздел
7.1.4).
Стоит также обратить внимание на то, что второй конструктор (получающий три аргумента)
неявно использует внутриклассовый инициализатор для переменной-члена cursor (см. раздел
7.1.4). Если бы у класса не было внутриклассового инициализатора для переменной-члена cursor, то мы явно инициализировали бы ее наряду с другими переменными-членами.
Встраиваемые члены класса
У классов зачастую бывают небольшие функции, которые выгодно сделать встраиваемыми.
Page 349/1103

Как уже упоминалось, определенные в классе функции-члены автоматически являются встраиваемыми (inline) (см. раздел 6.5.2). Таким образом, конструкторы класса Screen и версия функции get(), возвращающей обозначенный курсором символ, являются встраиваемыми по умолчанию.
Функцию-член можно объявить встраиваемой явно в ее объявлении в теле класса. В качестве альтернативы функцию можно указать встраиваемой в определении, расположенном вне тела класса: inline // функцию можно указать встраиваемой в определении
Screen &Screen::move(pos r, pos с) { pos row = r * width; // вычислить положение ряда cursor = row + с; // переместить курсор к столбцу этого ряда return *this; // возвратить этот объект как l-значение
} char Screen::get(pos r, pos с) const // объявить встраиваемый в классе
{ pos row = r * width; // вычислить положение ряда return contents[row + с]; // возвратить символ в данном столбце
}
Хоть и не обязательно делать это, вполне допустимо указать ключевое слово inline и в объявлении, и в определении. Однако указание ключевого слова inline в определении только вне класса может облегчить чтение класса.
По тем же причинам, по которым встраиваемые функции определяют в заголовках (см.
раздел 6.5.2), встраиваемые функции-члены следует определить в том же заголовке, что и определение соответствующего класса. Перегрузка функций-членов
Подобно функциям, которые не являются членами класса, функции-члены могут быть перегружены (см. раздел 6.4), если они отличаются количеством и/или типами параметров.
При вызове функции-члена используется тот же процесс подбора функции (см. раздел 6.4),
что и у функций, не являющихся членом класса.
Page 350/1103

Например, в классе Screen определены две версии функции get(). Одна версия возвращает символ, обозначенный в настоящее время курсором; другая возвращает символ в указанной позиции, определенной ее рядом и столбцом. Чтобы определить применяемую версию,
компилятор использует количество аргументов:
Screen myscreen; char ch = myscreen.get(); // вызов Screen::get() ch = myscreen.get(0,0); // вызов Screen::get(pos, pos) Изменяемые переменные-члены
Иногда (но не очень часто) у класса есть переменная-член, которую следует сделать изменяемой даже в константной функции-члене. Для обозначения таких членов в их объявление включают ключевое слово mutable.
Изменяемая переменная-член (mutable data member) никогда не бывает константой, даже когда это член константного объекта. Соответственно константная функция-член может изменить изменяемую переменную-член. В качестве примера добавим в класс Screen изменяемую переменную-член access_ctr, используемую для отслеживания частоты вызова каждой функции-члена класса Screen: class Screen { public: void some_member() const; private: mutable size_t access_ctr; // может измениться даже в константном
// объекте
// другие члены как прежде
}; void Screen::some_member() const {
++access_ctr; // сохранить количество вызовов любой функции-члена
// безотносительно других выполняемых ею действий
}
Page 351/1103

Несмотря на то что функция-член some_member() константная, она может изменить значение переменной-члена access_сtr. Этот член класса является изменяемым, поэтому любая функция-член, включая константные, может изменить это значение. Инициализаторы переменных-членов класса
Кроме класса Screen, определим также класс диспетчера окон, который представляет коллекцию окон на данном экране. У этого класса будет вектор объектов класса Screen,
каждый элемент которого представляет отдельное окно. По умолчанию класс Window_mgr должен изначально содержать один объект класса Screen, инициализированный значением по умолчанию. По новому стандарту наилучшим способом определения такого значения по умолчанию является внутриклассовый инициализатор (см. раздел 2.6.1): class Window_mgr { private:
// по умолчанию отслеживающий окна объект класса Window_mgr
// содержит одно пустое окно стандартного размера std::vector<Screen> screens{Screen(24, 80, ' ')};
};
При инициализации переменных-членов типа класса их конструктору следует предоставить аргументы. В этом случае применяется список инициализации переменной-члена типа vector
(см. раздел 3.3.1) с инициализатором для одного элемента. Этот инициализатор содержит значение типа Screen, передаваемое конструктору vector<Screen> для создания вектора с одним элементом. Это значение создается конструктором класса Screen, получающим параметры в виде двух размерностей и заполняющего символа, чтобы создать пустое окно заданного размера.
Как уже упоминалось, для внутриклассовой инициализации может использоваться форма инициализации = (как при инициализации переменных-членов класса Screen) или прямая форма инициализации с использованием фигурных скобок (как у вектора screens).
При предоставлении внутриклассового инициализатора это следует сделать после знака =
или в фигурных скобках. Упражнения раздела 7.3.1
Упражнение 7.23. Напишите собственную версию класса Screen.
Упражнение 7.24. Добавьте в свой класс Screen три конструктора: стандартный; получающий высоту, ширину и заполняющий содержимое соответствующим количеством пробелов;
получающий высоту, ширину и заполняющий символ для содержимого экрана.
Упражнение 7.25. Может ли класс Screen безопасно полагаться на заданные по умолчанию версии операторов копирования и присвоения? Если да, то почему? Если нет, то почему?
Упражнение 7.26. Определите функцию Sales data::avg_price как встраиваемую.
7.3.2. Функции, возвращающие указатель *this
Page 352/1103

Теперь добавим функции, устанавливающие символ в курсоре или в заданной области: class Screen { public:
Screen &set(char);
Screen &set(pos, pos, char);
// другие члены, как прежде
}; inline Screen &Screen::set(char c) { contents[cursor] = с; // установите новое значение в текущей позиции
// курсора return *this; // возвратить этот объект как l-значение
} inline Screen &Screen::set(pos r, pos col, char ch) { contents[r * width + col] = ch; // установить позицию по данному
// значению return *this; // возвратить этот объект как l-значение
}
Как и функция move(), функция-член set() возвращает ссылку на объект, из которого они вызваны (см. раздел 7.1.2). Возвращающие ссылку функции являются l-значениями (см.
раздел 6.3.2), а это означает, что они возвращают сам объект, а не его копию. Это позволяет связать несколько их вызовов в одно выражение:
// переместить курсор в указанную позицию и присвоить
Page 353/1103

// символу значение myScreen.move(4,0).set('#');
Эти операции выполнятся для того же объекта. В этом выражении сначала перемещается курсор (move()) в окно (myScreen), а затем устанавливается (set()) заданный символ. Таким образом, этот оператор эквивалентен следующему: myScreen.move(4,0); myScreen.set('#');
Если бы функции move() и set() возвращали тип Screen, а не Screen&, этот оператор выполнялся бы совсем по-другому. В данном случае он был бы эквивалентен следующему:
// если move возвращает Screen, а не Screen&
Screen temp = myScreen.move(4,0); // возвращаемое значение было
// бы скопировано temp.set('#'); // содержимое myScreen осталось бы неизменно
Если бы функция move() имела возвращаемое значение не ссылочного типа, то оно было бы копией *this (см. раздел 6.3.2). Вызов функции set() изменил бы лишь временную копию, а не сам объект myScreen. Возвращение *this из константной функции-члена
Теперь добавим функцию display(), выводящую содержимое окна. Необходима возможность включать эту операцию в последовательность операций set() и move(). Поэтому, подобно функциям set() и move(), функция display() возвратит ссылку на объект, для которого она выполняется.
Логически отображение объекта класса Screen (окна) не изменяет его, поэтому функцию display() следует сделать константным членом. Но если функция display() будет константной,
то this будет указателем на константу, а значение *this — константным объектом.
Следовательно, типом возвращаемого значения функции display() будет const Screen&.
Однако, если функция display() возвратит ссылку на константу, мы не сможем вставить вызов функции display() в последовательность действий:
Screen myScreen;
// если display возвращает константную ссылку,
// вызов в последовательности будет ошибкой
Page 354/1103
myScreen.display(cout).set('*');
Хотя объект myScreen неконстантный, вызов функции set() не будет компилироваться.
Проблема в том, что константная версия функции display() возвращает ссылку на константу, и мы не можем вызвать функцию set() для константного объекта.
Тип возвращаемого значения константной функции-члена, возвращающей *this как ссылку,
должен быть ссылкой на константу. Перегрузка на основании константности
Функции-члены вполне можно перегружать исходя из того, являются ли они константными или нет, причем по тем же причинам, по которым функцию можно перегружать исходя из того,
является ли ее параметр указателем на константу (см. раздел 6.4). Неконстантная версия неприменима для константных объектов; она применима только для константных объектов.
Для неконстантного объекта можно вызвать любую версию, но неконстантная версия будет лучшим соответствием.
В этом примере определим закрытую функцию-член do_display() для фактического вывода окна. Каждая из функций display() вызовет эту функцию, а затем возвратит объект, для которого она выполняется: class Screen { public:
// display перегружена на основании того, является ли
// объект константой или нет
Screen &display(std::ostream &os)
{ do_display(os); return *this; } const Screen &display(std::ostream &os) const
{ do_display(os); return *this; } private:
// функция отображения окна void do_display(std::ostream &os) const {os << contents;}
// другие члены как прежде
};
Как и в любом другом случае, при вызове одной функции-члена другой неявно передается указатель this. Таким образом, когда функция display() вызывает функцию-член do_display(),
ей неявно передается собственный указатель this. Когда неконстантная версия функции display() вызывает функцию do_display(), ее указатель this неявно преобразуется из указателя
Page 355/1103
на неконстанту в указатель на константу (см. раздел 4.11.2).
Когда функция do_display() завершает работу, функция display() возвращает объект, с которым они работают, обращаясь к значению указателя this. В неконстантной версии указатель this указывает на неконстантный объект, так что эта версия функции display()
возвращает обычную, неконстантную ссылку; константная версия возвращает ссылку на константу.
Когда происходит вызов функции display() для объекта, вызываемую версию определяет его константность:
Screen myScreen(5, 3); const Screen blank(5, 3); myScreen.set('#').display(cout); // вызов неконстантной версии blank.display(cout); // вызов константной версии Совет. Используйте закрытые вспомогательные функции
Некоторые читатели могут удивиться: зачем дополнительно создавать отдельную функцию do_display()? В конце концов, обращение к функции do_display() не намного проще, чем осуществляемое в ней действие.
Зачем же она нужна? Причин здесь несколько.
• Всегда желательно избегать нескольких экземпляров одного кода.
• По мере развития класса функция display() может стать значительно более сложной, а следовательно, преимущества одной, а не нескольких копий кода станут более очевидными.
• Во время разработки в тело функции display(), вероятно, придется добавить отладочный код, который в финальной версии будет удален. Это будет проще сделать в случае, когда весь отладочный код находится в одной функции do_display().
• Поскольку функция do_display() объявлена встраиваемой (inline), при создании исполняемого кода компилятор и так вставит ее содержимое по месту вызова, поэтому вызов функции не повлечет за собой никаких потерь времени и ресурсов.
Обычно в хорошо спроектированных программах на языке С++ присутствует множество маленьких функций, таких как do_display(), которые выполняют всю основную работу, когда их использует набор других функций. Упражнения раздела 7.3.2
Упражнение 7.27. Добавьте функции move(), set() и display() в свою версию класса Screen.
Проверьте свой класс, выполнив следующий код:
Screen myScreen(5, 5, 'X'); myScreen.move(4,0).set('#').display(cout); cout << "\n"; myScreen.display(cout); cout << "\n";
Page 356/1103

Упражнение 7.28. Что если бы в предыдущем упражнении типом возвращаемого значения функций move(), set() и display() был Screen, а не Screen&?
Упражнение 7.29. Пересмотрите свой класс Screen так, чтобы функции move(), set() и display()
возвращали тип Screen, а затем проверьте свое предположение из предыдущего упражнения.
Упражнение 7.30. Обращение к членам класса при помощи указателя this вполне допустимо,
но избыточно. Обсудите преимущества и недостатки явного использования указателя this для доступа к членам.
7.3.3. Типы классов
Каждый класс определяет уникальный тип. Два различных класса определяют два разных типа, даже если их члены совпадают. Например: struct First { int memi; int getMem();
}; struct Second { int memi; int getMem();
};
First obj1;
Second obj2 = obj1; // ошибка: obj1 и obj2 имеют разные типы
Даже если у двух классов полностью совпадает список членов, они являются разными типами. Члены каждого класса отличны от членов любого другого класса (или любой другой области видимости).
К типу класса можно обратиться непосредственно, используя имя класса как имя типа. В
качестве альтернативы можно использовать имя класса после ключевого слова class или struct:
Sales_data item1; // инициализация значением по умолчанию объекта
// типа Sales_data class Sales_data item1; //
Page 357/1103
эквивалентное объявление
Оба способа обращения к типу класса эквивалентны. Второй метод унаследован от языка С и все еще допустим в С++. Объявления класса
Подобно тому, как можно объявить функцию без ее определения (см. раздел 6.1.2), можно объявить (class declaration) класс, не определяя его: class Screen; // объявление класса Screen
Такое объявление иногда называют предварительным объявлением (forward declaration), оно вводит имя Screen в программу и указывает, что оно относится к типу класса. После объявления, но до определения, тип Screen считается незавершенным типом (incomplete type), т.е. известно, что Screen — это тип класса, но не известно, какие члены он содержит.
Использование незавершенного типа весьма ограниченно. Его можно использовать только для определения указателей или ссылок, а также для объявления (но не определения)
функций, которые используют этот тип как параметр или тип возвращаемого значения.
Прежде чем можно будет писать код, создающий объекты некого класса, его следует определить, а не только объявить. В противном случае компилятор не будет знать, в каком объеме памяти нуждаются его объекты. Аналогично класс должен быть уже определен перед использованием ссылки или указателя для доступа к члену класса. В конце концов, если класс не был определен, компилятор не сможет узнать, какие члены имеет класс.
За одним исключением, рассматриваемым в разделе 7.6, переменные-члены могут быть определены как имеющие тип класса, только если класс был определен. Тип следует завершить, поскольку компилятор должен знать объем памяти, необходимый для хранения переменных-членов. Пока класс не определен, пока его тело не создано, у класса не может быть переменных-членов его собственного типа. Однако класс считается объявленным (но еще не определенным), как только его имя стало видимо. Поэтому у класса могут быть переменные-члены, являющиеся указателями или ссылками на ее собственный тип: class Link_screen {
Screen window;
Link_screen *next;
Link_screen *prev;
}; Упражнения раздела 7.3.3
Упражнение 7.31. Определите два класса, X и Y, у которых класс X имеет указатель на класс
Y, a Y содержит объект типа X.
7.3.4. Снова о дружественных отношениях
Наш класс Sales_data определил три обычных функции, не являющиеся членом класса, как
Page 358/1103
дружественные (см. раздел 7.2.1). Класс может также сделать дружественным другой класс или объявить дружественными определенные функции-члены другого (определенного ранее)
класса. Кроме того, дружественная функция может быть определена в теле класса. Такие функции неявно являются встраиваемыми. Дружественные отношения между классами
В качестве примера дружественных классов рассмотрим класс Window_mgr (см. раздел
7.3.1), его членам понадобится доступ к внутренним данным объектов класса Screen,
которыми они управляют. Предположим, например, что в класс Window_mgr необходимо добавить функцию-член clear(), заполняющую содержимое определенного окна пробелами.
Для этого функции clear() нужен доступ к закрытым переменным-членам класса Screen. Для этого класс Screen должен объявить класс Window_mgr дружественным: class Screen {
// члены класса Window_Mgr смогут обращаться к закрытым
// членам класса Screen friend class Window_mgr;
//
... остальное, как раньше в классе Screen
};
Функции-члены дружественного класса могут обращаться ко всем членам класса,
объявившего его другом, включая не открытые члены. Теперь, когда класс Window_mgr является другом класса Screen, функцию-член clear() класса Window_mgr можно переписать следующим образом: class Window_mgr { public:
// идентификатор области для каждого окна на экране using ScreenIndex = std::vector<Screen>::size_type;
// сбросить данное окно, заполнив его пробелами void clear(ScreenIndex); private: std::vector<Screen> screens{Screen(24, 80, ' ')};
}; void Window_mgr::clear(ScreenIndex i) {
Page 359/1103

// s - ссылка на окно, которое предстоит очистить
Screen &s = screens[i];
// сбросить данное окно, заполнив его пробелами s.contents = string(s.height * s.width, ' ');
}
Сначала определим s как ссылку на класс Screen в позиции i вектора окон. Затем переменные-члены height и width данного объекта класса Screen используются для вычисления количества символов новой строки, содержащей пробелы. Эта заполненная пробелами строка присваивается переменной-члену contents.
Если бы функция clear() не была дружественной классу Screen, то этот код не компилировался бы. Функция clear() не смогла бы использовать переменные-члены height,
width или contents класса Screen. Поскольку класс Screen установил дружественные отношения с классом Window_mgr, для его функций доступны все члены класса Screen.
Важно понимать, что дружественные отношения не передаются. Таким образом, если у класса Window_mgr есть собственные друзья, то у них нет привилегий доступа к членам класса Screen.
Каждый класс сам контролирует, какие классы или функции будут его друзьями. Как сделать функцию-член дружественной
Вместо того чтобы делать весь класс Window_mgr дружественным классу Screen, можно предоставить доступ только функции-члену clear(). Когда функция-член объявляется дружественной, следует указать класс, которому она принадлежит: class Screen {
// класс Window_mgr::clear должен быть объявлен перед классом Screen friend void Window_mgr::clear(ScreenIndex);
//
... остальное как раньше в классе Screen
};
Создание дружественных функций-членов требует тщательного структурирования программ в соответствии с взаимозависимостями объявлений и определений. В данном случае программу следует упорядочить следующим образом.
• Сначала определите класс Window_mgr, который объявляет, но не может определить функцию clear(). Класс Screen должен быть объявлен до того, как функция clear() сможет использовать члены класса Screen.
• Затем определите класс Screen, включая объявление функции clear() дружественной.
Page 360/1103

• И наконец, определите функцию clear(), способную теперь обращаться к членам класса
Screen. Перегруженные функции и дружественные отношения
Хотя у перегруженных функций одинаковое имя, это все же разные функции. Поэтому класс должен объявить дружественной каждую из перегруженных функций:
// перегруженные функции storeOn extern std::ostream& storeOn(std::ostream &, Screen &); extern BitMap& storeOn(BitMap &, Screen &); class Screen {
// версия ostream функции storeOn может обращаться к закрытым членам
// объектов класса Screen friend std::ostream& storeOn(std::ostream &, Screen &); // ...
};
Класс Screen объявляет другом версию функции storeOn, получающей поток ostream&.
Версия, получающая параметр BitMap&, особых прав доступа к объектам класса Screen не имеет. Объявление дружественных отношений и область видимости
Классы и функции, не являющиеся членами класса, не следует объявлять прежде, чем они будут использованы в объявлении дружественными. Когда имя впервые появляется в объявлении дружественной, оно неявно подразумевается принадлежащей окружающей области видимости. Однако сам друг фактически не объявлен в этой области видимости (см. раздел 7.2.1).
Даже если мы определим функцию в классе, ее все равно придется объявить за пределами класса, чтобы сделать видимой. Объявление должно уже существовать, даже если вызывается дружественная функция: struct X { friend void f() { /* дружественная функция может быть определена в теле класса */ }
X() { f(); } // ошибка: нет объявления для f void g(); void h();
Page 361/1103

}; void X::g() { return f(); } // ошибка: f не была объявлена void f(); // объявляет функцию, определенную в X void X::h() { return f(); } // ok: объявление f теперь в области
// видимости
Важно понимать, что объявление дружественной затрагивает доступ, но не является объявлением в обычном смысле.
Помните: некоторые компиляторы не выполняют правил поиска имен друзей (см. раздел
7.2.1). Упражнения раздела 7.3.4
Упражнение 7.32. Определите собственные версии классов Screen и Window_mgr, в которых функция clear() является членом класса Window_mgr и другом класса Screen.
7.4. Область видимости класса
Каждый класс определяет собственную область видимости. Вне области видимости класса (class scope) к обычным данным и функциям его члены могут обращаться только через объект, ссылку или указатель, используя оператор доступа к члену
(см. раздел 4.6). Для доступа к членам типа из класса используется оператор области видимости. В любом случае следующее за оператором имя должно быть членом соответствующего класса.
Screen::pos ht = 24, wd = 80; // использование типа pos, определенного
// в классе Screen
Screen scr(ht, wd, ' ');
Screen *p = &scr; char c = scr.get(); // доступ к члену get() объекта scr c = p->get(); //
Page 362/1103
доступ к члену get() из объекта, на который
// указывает p Область видимости и члены, определенные вне класса
Тот факт, что класс определяет область видимости, объясняет, почему следует предоставить имя класса наравне с именем функции, при определении функции-члена вне ее класса (см.
раздел 7.1.2). За пределами класса имена ее членов скрыты.
Как только имя класса становится видимо, остальная часть определения, включая список параметров и тело функции, находится в области видимости класса. В результате мы можем обращаться к другим членам класса без уточнения.
Вернемся, например, к функции-члену clear() класса Window_mgr (см. раздел 7.3.4). Параметр этой функции имеет тип, определенный в классе Window_mgr: void Window_mgr::clear(ScreenIndex i) {
Screen &s = screens[i]; s.contents = string(s.height * s.width, ' ');
}
Поскольку компилятор видит последующий список параметров и ничего подобного в области видимости класса WindowMgr, нет никакой необходимости определять, что требуется тип
ScreenIndex, определенный в классе WindowMgr. По той же причине использование объекта screens в теле функции относится к имени, объявленному в классе Window_mgr.
С другой стороны, тип возвращаемого значения функции обычно располагается перед именем функции. Когда функция-член определяется вне тела класса, любое имя,
используемое в типе возвращаемого значения, находится вне области видимости класса. В
результате тип возвращаемого значения должен определять класс, членом которого он является. Например, мы могли бы добавить в класс Window_mgr функцию addScreen(),
добавляющую еще одно окно на экран. Этот член класса возвратит значение типа
ScreenIndex, которое пользователь впоследствии сможет использовать для поиска этого окна:
class Window_mgr { public:
// добавить окно на экран и возвратить его индекс
ScreenIndex addScreen(const Screen&);
// другие члены, как прежде
};
// тип возвращаемого значения видим прежде, чем начинается область
Page 363/1103

// видимости класса Window_mgr
Window_mgr::ScreenIndex
Window_mgr::addScreen(const Screen &s) { screens.push_back(s); return screens.size() - 1;
}
Поскольку тип возвращаемого значения встречается прежде имени класса, оно находится вне области видимости класса Window_mgr. Чтобы использовать тип ScreenIndex для возвращаемого значения, следует определить класс, в котором определяется этот тип.
Упражнения раздела 7.4
Упражнение 7.33. Что будет, если добавить в класс Screen переменную-член size(),
определенную следующим образом? Исправьте все обнаруженные ошибки. pos Screen::size() const { return height * width;
}
7.4.1. Поиск имен в области видимости класса
В рассмотренных до сих пор программах поиск имен (name lookup) (процесс поиска объявления, соответствующего данному имени)
был относительно прост.
• Сначала поиск объявления осуществляется в том блоке кода, в котором используется имя.
Причем рассматриваются только те имена, объявления которых расположены перед местом применения.
• Если имя не найдено, поиск продолжается в иерархии областей видимости, начиная с текущей.
• Если объявление так и не найдено, происходит ошибка.
Когда поиск имен осуществляется в функциях-членах, определенных в классе, может показаться, что он происходит не по правилам поиска. Но в данном случае внешний вид обманчив. Обработка определений классов осуществляется в два этапа.
• Сначала компилируются объявления членов класса.
• Тела функции компилируются только после того, как виден весь класс.
Определения функций-членов обрабатываются после того , как компилятор обработает все объявления в классе.
Page 364/1103

Классы обрабатываются в два этапа, чтобы облегчить организацию кода класса. Поскольку тела функций-членов не обрабатываются, пока весь класс не станет видимым, они смогут использовать любое имя, определенное в классе. Если бы определения функций обрабатывались одновременно с объявлениями переменных-членов, то пришлось бы располагать функции-члены так, чтобы они обращались только к тем именам, которые уже видимы. Поиск имен для объявлений членов класса
Этот двухэтапный процесс применяется только к именам, используемым в теле функции-члена. Имена, используемые в объявлениях, включая имя типа возвращаемого значения и типов списка параметров, должны стать видимы прежде, чем они будут использованы. Если объявление переменной-члена будет использовать имя, объявление которого еще не видимо в классе, то компилятор будет искать то имя в той области
(областях) видимости, в которой определяется класс. Рассмотрим пример. typedef double Money; string bal; class Account { public:
Money balance() { return bal; } private:
Money bal;
// ...
};
Когда компилятор видит объявление функции balance(), он ищет объявление имени Money в классе Account. Компилятор рассматривает только те объявления в классе Account, которые расположены перед использованием имени Money. Поскольку его объявление как члена класса не найдено, компилятор ищет имя в окружающей области видимости. В этом примере компилятор найдет определение типа (typedef) Money. Этот тип будет использоваться и для типа возвращаемого значения функции balance(), и как тип переменной-члена bal. С другой стороны, тело функции balance() обрабатывается только после того, как видимым становится весь класс. Таким образом, оператор return в этой функции возвращает переменную-член по имени bal, а не строку из внешней области видимости. Имена типов имеют особенности
Обычно внутренняя область видимости может переопределить имя из внешней области видимости, даже если это имя уже использовалось во внутренней области видимости. Но если член класса использует имя из внешней области видимости и это имя типа, то класс не сможет впоследствии переопределить это имя: typedef double Money; class Account { public:
Money balance() { return bal; } // используется имя Money из внешней
Page 365/1103

// область видимости private: typedef double Money; // ошибка: нельзя переопределить Money
Money bal;
// ...
};
Следует заметить, что хотя определение типа Money в классе Account использует тот же тип,
что и определение во внешней области видимости, этот код все же ошибочен.
Хотя переопределение имени типа является ошибкой, не все компиляторы обнаружат эту ошибку. Некоторые спокойно примут такой код, даже если программа ошибочна.
Определения имен типов обычно располагаются в начале класса. Так, любой член класса,
который использует этот тип, будет расположен после определения его имени. При поиске имен в областях видимости члены класса следуют обычным правилам
Поиск имени, используемого в теле функции-члена, осуществляется следующим образом.
• Сначала поиск объявления имени осуществляется в функции-члене. Как обычно,
рассматриваются объявления в теле функции, только предшествующие месту использования имени.
• Если в функции-члене объявление не найдено, поиск продолжается в классе.
Просматриваются все члены класса.
• Если объявление имени в классе не найдено, поиск продолжится в области видимости перед определением функции-члена.
Обычно не стоит использовать имя другого члена класса как имя параметра в функции-члене.
Но для демонстрации поиска имени нарушим это правило в функции dummy_fcn():
// обратите внимание: это сугубо демонстрационный код, отражающий
// плохую практику программирования. Обычно не стоит использовать
// одинаковое имя для параметра и функции-члена int height; // определяет имя, впоследствии используемое в Screen class Screen {
Page 366/1103
public: typedef std::string::size_type pos; void dummy_fcn(pos height) { cursor = width * height; // какое имя height имеется в виду?
} private: pos cursor = 0; pos height = 0, width = 0;
};
Когда компилятор обрабатывает выражение умножения в функции dummy_fcn(), он ищет имена сначала в пределах данной функции. Параметры функции находятся в области видимости функции. Таким образом, имя height, используемое в теле функции dummy_fcn(),
принадлежит ее параметру.
В данном случае имя height параметра скрывает имя height переменной-члена класса. Если необходимо переопределить обычные правила поиска, то это можно сделать так:
// плохой подход: имена, локальные для функций-членов, не должны
// скрывать имена переменных-членов класса void Screen::dummy_fcn(pos height) { cursor = width * this->height; // переменная-член height
// альтернативный способ указания переменной-члена cursor = width * Screen::height; // переменная-член height
}
Несмотря на то что член класса скрыт, его все равно можно использовать. Достаточно указать его полное имя, включающее имя класса, либо явно применить указатель this.
Значительно проще обеспечить доступ к переменной-члену height, присвоив параметру другое имя:
//
Page 367/1103
хороший подход: не используйте имена переменных-членов для
// параметров или других локальных переменных void Screen::dummy_fcn(pos ht) { cursor = width * height; // переменная-член height
}
Теперь, когда компилятор будет искать имя height, в функции dummy_fcn() он его не найдет.
Затем компилятор просмотрит класс Screen. Поскольку имя height используется в функции-члене dummy_fcn(), компилятор просмотрит все объявления членов класса.
Несмотря на то что объявление имени height расположено после места его использования в функции dummy_fcn(), компилятор решает, что оно относится к переменной-члену height.
После поиска в области видимости класса продолжается поиск в окружающей области видимости
Если компилятор не находит имя в функции или в области видимости класса, он ищет его в окружающей области видимости. В данном случае имя height объявлено во внешней области видимости, перед определением класса Screen. Однако объект во внешней области видимости скрывается переменной-членом класса по имени height. Если необходимо имя из внешней области видимости, к нему можно обратиться явно, используя оператор области видимости:
// плохой подход: не скрывайте необходимые имена, которые
// определены в окружающих областях видимости void Screen::dummy_fcn(pos height) { cursor = width * ::height; // который height? Глобальный
}
Несмотря на то что глобальный объект был скрыт, используя оператор области видимости,
доступ к нему вполне можно получить. Поиск имен распространяется по всему файлу, где они были применены
Когда член класса определен вне определения класса, третий этап поиска его имени происходит не только в объявлениях глобальной области видимости, которые расположены непосредственно перед определением класса Screen, но и распространяется на остальные объявления в глобальной области видимости. Рассмотрим пример. int height; // определяет имя, впоследствии используемое в Screen class Screen {
Page 368/1103
public: typedef std::string::size_type pos; void setHeight(pos); pos height = 0; // скрывает объявление height из внешней
// области видимости
};
Screen::pos verify(Screen::pos); void Screen::setHeight(pos var) {
// var: относится к параметру
// height: относится к члену класса
// verify: относится к глобальной функции height = verify(var);
}
Обратите внимание, что объявление глобальной функции verify() не видимо перед определением класса Screen. Но третий этап поиска имени включает область видимости, в которой присутствует определение члена класса. В данном примере объявление функции verify() расположено перед определением функции setHeight(), a потому может использоваться. Упражнения раздела 7.4.1

1   ...   16   17   18   19   20   21   22   23   ...   54


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