Категории
Самые читаемые
Лучшие книги » Компьютеры и Интернет » Программирование » Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Читать онлайн Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 20 21 22 23 24 25 26 27 28 ... 73
Перейти на страницу:

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

Каковы ограничения на допустимые значения вашего нового типа? Обычно только некоторые комбинации значений данных-членов класса являются правильными. Эти комбинации определяют инварианты, которые должен поддерживать класс. А инварианты уже диктуют, как следует контролировать ошибки в функциях-членах, в особенности в конструкторах, операторах присваивания и функциях установки значений («setter» functions). Могут быть также затронуты исключения, которые возбуждают ваши функции, и спецификации этих исключений.

Укладывается ли ваш новый тип в граф наследования? Наследуя свои классы от других, вы должны следовать ограничениям, налагаемым базовыми классами. В частности, нужно учитывать, как объявлены в них функции-члены: виртуальными или нет (см. правила 34 и 36). Если вы хотите, чтобы вашему классу могли наследовать другие, то нужно тщательно продумать, какие функции объявить виртуальными; в особенности это относится к деструктору (см. правило 7).

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

Какие операторы и функции имеют смысл для нового типа? Ответ на этот вопрос определяет набор функций, которые вы объявляете в вашем классе. Некоторые из них будут функциями-членами, другие – нет (см. правила 23, 24 и 46).

• Какие стандартные функции должны стать недоступными? Их надо будет объявить закрытыми (см. правило 6).

Кто должен получить доступ к членам вашего нового типа? Ответ на этот вопрос помогает определить, какие члены должны быть открытыми (public), какие – защищенными (protected) и какие – закрытыми (private). Также вам предстоит решить, какие классы и/или функции должны быть друзьями класса, а также когда имеет смысл вложить один класс внутрь другого.

Что такое «необъявленный интерфейс» вашего нового типа? Какого рода гарантии могут быть предоставлены относительно производительности, безопасности относительно исключений (см. правило 29) и использования ресурсов (например, блокировок и динамической памяти)? Такого рода гарантии определяют ограничения на реализацию вашего класса.

Насколько общий ваш новый тип? Возможно, в действительности вы не определяете новый тип. Возможно, вы определяете целое семейство типов. Если так, то вам нужно определять не новый класс, а новый шаблон класса.

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

На эти вопросы нелегко ответить, поэтому определение эффективных классов – непростая задача. Но при ее должном выполнении определенные пользователями классы C++ дают типы, которые ничем не уступают встроенным и уже оправдывают все ваши усилия.

Что следует помнить

• Проектирование класса – это проектирование типа. Прежде чем определять новый тип, убедитесь, что рассмотрены все вопросы, которые обсуждаются в настоящем правиле.

Правило 20: Предпочитайте передачу по ссылке на const передаче по значению

По умолчанию в C++ объекты передаются в функции и возвращаются функциями по значению (свойство, унаследованное от C). Если не указано противное, параметры функции инициализируются копиями реальных аргументов, а после вызова функции программа получает копию возвращаемой функцией величины. Копии вырабатываются конструкторами копирования. Поэтому передача по значению может оказаться накладной операцией. Например, рассмотрим следующую иерархию классов:

class Person {

public:

Person(); // параметры опущены для простоты

virtual ~Person(); // см. в правиле 7 – почему виртуальный

...

private:

std::string name;

std::string address;

};

class Student: public Person {

public:

Student(); // и здесь параметры опущены

~ Student();

...

private:

std::string schoolName;

std::string schoolAddress;

};

Теперь взгляните на следующий код, где вызывается функция validateStudent, которая принимает аргумент Student (по значению) и возвращает признак его корректности:

bool validateStudent(Student s); // функция принимает параметр

// Student по значению

Student plato; // Платон учился у Сократа

bool platoIsOk = validateStudent(plato); // вызов функции

Что происходит при вызове этой функции?

Ясно, что вызывается конструктор копирования Student для инициализации параметра plato. Также ясно, что s уничтожается при возврате из validate-Student. Поэтому передача параметра по значению этой функции обходится в один вызов конструктора копирования Student и один вызов деструктора Student.

Но это еще не все. Объект Student содержит внутри себя два объекта string, поэтому каждый раз, когда вы конструируете объект Student, вы должны также конструировать и эти два объекта. Класс Student наследует класу Person, поэтому каждый раз, конструируя объект Student, вы должны сконструировать и объект Person. Но объект Person содержит еще два объекта string, поэтому каждое конструирование Person влечет за собой два вызова конструктора string. Итак, передача объекта Student по значению приводит к одному вызову конструктора копирования Student, одному вызову конструктора копирования Person и четырем вызовам конструкторов копирования string. Когда копия объекта Student разрушается, каждому вызову конструктора соответствует вызов деструктора, поэтому общая стоимость передачи Student по значению составляет шесть конструкторов и шесть деструкторов!

Что ж, это корректное и желательное поведение. В конец концов, вы хотите, чтобы все ваши объекты были надежно инициализированы и уничтожены. И все же было бы неплохо найти способ пропустить все эти вызовы конструкторов и деструкторов. Способ есть! Это – передача по ссылке на константу:

bool validateStudent(const Student& s);

Этот способ гораздо эффективнее: не вызываются никакие конструкторы и деструкторы, поскольку не создаются никакие новые объекты. Квалификатор const в измененном объявлении параметра важен. Исходная версия validateStudent принимала параметр Student по значению, вызвавший ее знает о том, что он защищен от любых изменений, которые функция может внести в переданный ей объект; validateStudent сможет модифицировать только его копию. Теперь же, когда Student передается по ссылке, необходимо объявить его const, поскольку в противном случае вызывающая программа должна побеспокоиться о том, чтобы validateStudent не вносила изменений в переданный ей объект.

Передача параметров по ссылке также позволяет избежать проблемы « срезки » (slicing). Когда объект производного класса передается (по значению) как объект базового класса, вызывается конструктор копирования базового класса, а те части, которые принадлежат производному, «срезаются». У вас остается только простой объект базового класса – что вполне естественно, так как его создал конструктор базового класса. Это почти всегда не то, что вам нужно. Например, предположим, что вы работаете с набором классов для реализации графической оконной системы:

class Window {

public

...

std::string name() const; // возвращает имя окна

virtual void display() const; // рисует окно и его содержимое

};

class WindwoWithScrollBars: public Window {

public:

...

virtual void display() const;

};

Все объекты класса Window имеют имя, которое вы можете получить посредством функции name, и все окна могут быть отображены, на что указывает наличие функции display. Тот факт, что display – функция виртуальная, говорит о том, что способ отображения простых объектов базового класса Window может отличаться от способа отображения объектов WindowWithScrollBar (см. правила 34 и 36).

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

1 ... 20 21 22 23 24 25 26 27 28 ... 73
Перейти на страницу:
На этой странице вы можете бесплатно скачать Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс торрент бесплатно.
Комментарии