Классы и объекты / FAQ C++
Что такое класс?
Это базовый строительный блок объектно-ориентированного программирования.
Класс определяет тип данных, как структура делала это в C. С точки зрения информатики, тип состоит из набора состояний и набора операций, которые выполняют переход между этими состояниями. Таким образом, int
является типом, потому что он имеет как набор состояний, так и операции типа i + j
или i++
и т.д. Точно так же класс предоставляет набор (обычно общедоступных) операций и набор (обычно не общедоступных) битов данных, представляющих абстрактные значения, которые могут иметь экземпляры типа.
Вы можете представить себе, что int
– это класс, в котором есть функции-члены с именами operator++
, и т.п. (int
на самом деле не класс, но основная аналогия такова: класс – это тип, во многом как и int
является типом).
Примечание: программист на C может думать о классе как о структуре C, члены которой по умолчанию являются закрытыми. Но если это всё, что вы думаете о классе, вам, вероятно, нужно сменить парадигму мышления.
Что такое объект?
Область в хранилище со связанной семантикой.
После объявления int i;
мы говорим, что «i
– объект типа int
». В объектно-ориентированном программировании / C ++ «объект» обычно означает «экземпляр класса». Таким образом, класс определяет поведение, возможно, многих объектов (экземпляров).
Когда интерфейс является «хорошим»?
Когда он обеспечивает упрощенное представление фрагмента программного обеспечения и отражается в словаре пользователя (где «фрагмент» обычно представляет собой класс или узкую группу классов, а «пользователь» – это другой разработчик, а не конечный заказчик).
- «Упрощенное представление» означает, что ненужные детали намеренно скрыты. Это снижает уровень ошибок пользователя.
- «Словарь пользователей» означает, что пользователям не нужно учить новый набор слов и понятий. Это сокращает время обучения пользователя.
Что такое инкапсуляция?
Предотвращение несанкционированного доступа к какой-либо информации или функциям.
Ключевой способ сэкономить деньги – отделить изменчивую часть какой-либо части программного обеспечения от его стабильной части. Инкапсуляция устанавливает вокруг блока кода файрвол, который предотвращает доступ других блоков кода к изменчивым частям; другие фрагменты могут получить доступ только к стабильным частям. Это предотвращает поломку других блоков, если (когда!) изменчивые части изменяются. В контексте объектно-ориентированного программного обеспечения «блок» обычно представляет собой класс или узкую группу классов.
«Изменчивые части» – это детали реализации. Если блок кода является одним классом, изменчивая часть обычно инкапсулируется с помощью ключевых слов private
и/или protected
. Если блок кода представляет собой узкую группу классов, можно использовать инкапсуляцию, чтобы запретить доступ к целым классам в этой группе. Наследование также может использоваться как форма инкапсуляции.
«Стабильные части» – это интерфейсы. Хороший интерфейс обеспечивает упрощенное представление в словаре пользователя и разработан «снаружи вовнутрь» (здесь «пользователь» означает другого разработчика, а не конечного пользователя, который покупает законченное приложение). Если блок кода является одним классом, интерфейс – это просто общедоступные функции-члены и дружественные функции класса. Если блок представляет собой узкую группу классов, интерфейс может включать в себя несколько классов в блоке.
Разработка чистого интерфейса и отделение этого интерфейса от его реализации просто позволяет пользователям использовать интерфейс. А инкапсуляция (помещение «в капсулу») вынуждает пользователей использовать интерфейс.
Как C++ помогает найти компромисс между безопасностью и удобством использования?
В C инкапсуляция достигалась за счет статичности (с помощью ключевого слова static
) объекта или модуля компиляции. Она предотвращала доступ другого модуля к статическим штукам (кстати, статические данные в области видимости файла теперь в C++ считаются устаревшими: не делайте так.)
К сожалению, этот подход не поддерживает несколько экземпляров данных, поскольку нет прямой поддержки создания нескольких экземпляров статических данных модуля. Если в C требовалось несколько экземпляров, программисты обычно использовали структуру. Но, к сожалению, C-структуры не поддерживают инкапсуляцию. Это ухудшает компромисс между безопасностью (сокрытием информации) и удобством использования (несколько экземпляров).
В C++ вы можете иметь и несколько экземпляров, и инкапсуляцию в классе. Открытая (public
) часть класса содержит интерфейс класса, который обычно состоит из общедоступных функций-членов класса и его дружественных функций. Закрытые (private
) и/или защищенные (protected
) части класса содержат реализацию класса, в которой обычно находятся данные.
Конечный результат похож на «инкапсулированную структуру». Это уменьшает компромисс между безопасностью (скрытие информации) и удобством использования (несколько экземпляров).
Как я могу предотвратить нарушение инкапсуляции другими программистами, которые видят закрытые части моего класса?
Это не стоит усилий – инкапсуляция предназначена для кода, а не для людей.
Инкапсуляция не нарушается, когда программист увидел закрытые и/или защищенные части вашего класса, если он не пишет код, который так или иначе зависит от того, что он видел. Другими словами, инкапсуляция не мешает людям узнать о внутренней части класса; она предотвращает зависимость кода, который они пишут, от внутренней части класса. Вашей компании не нужно оплачивать «расходы на поддержку», чтобы поддерживать серое вещество между вашими ушами; но приходится платить за обслуживание поддержки кода, который исходит из кончиков ваших пальцев. То, что вы знаете как личность, не увеличивает стоимость обслуживания, при условии, что код, который вы пишете, зависит от интерфейса, а не от реализации.
Кроме того, это редко вообще является проблемой. Я не знаю программистов, которые намеренно пытались получить доступ к закрытым частям класса. «В таких случаях я бы порекомендовал сменить программиста, а не код» [Джеймс Канце].
Может ли метод напрямую обращаться к необщедоступным членам другого экземпляра своего класса?
Да.
Имя this
не является особенным. Доступ предоставляется или запрещается на основе класса ссылки/указателя/объекта, а не на основе имени ссылки/указателя/объекта (подробности смотрите ниже).
Тот факт, что C++ позволяет методам и друзьям класса получать доступ к закрытым частям всех его объектов, а не только к объекту this
, на первый взгляд, кажется, ослабляет инкапсуляцию. Однако верно и обратное: это правило сохраняет инкапсуляцию. Вот почему.
Без этого правила большинству непубличных членов потребовался бы общедоступный get-метод, потому что у многих классов есть, по крайней мере, один метод или друг, который принимает явный аргумент (то есть аргумент, который не называется this
) своего собственного класса.
Что? Давайте уберем эту болтовню и рассмотрим пример:
Рассмотрим оператор присваивания Foo::operator=(const Foo& x)
. Этот оператор присваивания, вероятно, изменит элементы данных в левом аргументе, *this
, основываясь на элементах данных в правом аргументе, x
. Без обсуждаемого здесь правила C++ единственный способ для этого оператора присваивания получить доступ к непубличным членам x
– это предоставить классу Foo
общедоступный метод получения для всех непубличных данных. Это было бы отстойно («отстойно» – это точный, сложный технический термин).
Оператор присваивания – не единственный, кто ослабил бы инкапсуляцию, если бы не это правило. Вот неполный (!) список других:
- конструктор копирования;
- операторы сравнения:
==
,!=
,<=
,<
,>=
,>
; - бинарные арифметические операторы:
x+y
,x-y
,x*y
,x/y
,x%y
; - бинарные побитовые операторы:
x^y
,x&y
,x|y
; - статические методы, которые принимают экземпляр класса в качестве параметра;
- статические методы, которые создают/манипулируют экземпляром класса;
- и т.п.
Вывод. Инкапсуляция была бы уничтожена без этого полезного правила: большинство непубличных членов большинства классов в конечном итоге имели бы общедоступный get-метод.
Мелкий шрифт. Есть еще одно правило, связанное с вышеизложенным: методы и друзья производного класса могут получить доступ к защищенным членам базового класса любого из их собственных объектов (любых объектов его класса или любого производного класса его класса), но не к другим. Поскольку это безнадежно непрозрачно, вот пример: предположим, что классы D1
и D2
наследуются напрямую от класса B
, а базовый класс B
имеет защищенный член x
. Компилятор позволит членам и друзьям D1
напрямую обращаться к члену x
любого объекта, который он знает как минимум D1
, например, через указатель D1*
, ссылку D1&
, объект D1
и т.д. Однако компилятор выдаст ошибку времени компиляции, если член или друг D1
попытается напрямую получить доступ к члену x
того, чего он не знает, как минимум, D1
, например, через указатель B*
, ссылку B&
, объект B
, указатель D2*
, ссылка D2&
, объект D2
и т.д. По аналогии (неидеальной!) вам разрешено шарить по своим собственным карманам, но вам не разрешено шарить по карманам вашего отца или вашего брата.
Инкапсуляция – это средство безопасности?
Нет.
Инкапсуляция! = Безопасность.
Инкапсуляция предотвращает ошибки, а не шпионаж.
В чем разница между ключевыми словами struct
и class
?
Члены и базовые классы структуры по умолчанию являются общедоступными (public
), тогда как в классе они по умолчанию являются закрытыми (private
). Примечание: вы должны делать свои базовые классы явноpublic
, private
или protected
, а не полагаться на значения по умолчанию.
В остальном struct
и class
функционально эквивалентны.
Хватит чисто технических разговоров. С эмоциональной точки зрения большинство разработчиков четко различают класс и структуру. Структура просто ощущается как открытая куча битов с очень небольшой величиной инкапсуляции или функциональности. Класс ощущается как живой и ответственный член общества с интеллектуальными службами, сильным барьером инкапсуляции и четко определенным интерфейсом. Поскольку этот подтекст уже есть у большинства людей, вам, вероятно, следует использовать ключевое слово struct
, если у вас есть класс, который имеет очень мало методов и общедоступные данные (такое существует в хорошо спроектированных системах!), а в противном случае вам, вероятно, следует использовать ключевое слово class
.
Как в классе определить константу?
Если вам нужна константа, которую вы можете использовать в выражении константы времени компиляции, например, как размер массива, используйте constexpr
, если ваш компилятор поддерживает эту функцию C++11, в противном случае у вас есть два других варианта:
class X {
constexpr int c1 = 42; // предпочтительно
static const int c2 = 7;
enum { c3 = 19 };
array<char,c1> v1;
array<char,c2> v2;
array<char,c3> v3;
// ...
};
У вас будет больше гибкости, если эта константа не нужна для использования в выражении константы времени компиляции:
class Z {
static char* p; // инициализируется в определении
const int i; // инициализируется в конструкторе
public:
Z(int ii) :i(ii) { }
};
char* Z::p = "hello, there";
К статическому элементу данных вы можете привязать ссылку или взять его адрес, если (и только если) он имеет определение вне класса:
class AE {
// ...
public:
static const int c6 = 7;
static const int c7 = 31;
};
const int AE::c7; // определение
void byref(const int&);
int f()
{
byref(AE::c6); // ошибка: c6 - не lvalue
byref(AE::c7); // ok
const int* p1 = &AE::c6; // ошибка: c6 - не lvalue
const int* p2 = &AE::c7; // ok
// ...
}
Почему я должен помещать данные в объявления моего класса?
Вы не должны. Если вам не нужны данные в интерфейсе, не помещайте их в класс, определяющий интерфейс. Вместо этого поместите их в производные классы. Смотрите «Почему компиляция моего кода занимает так много времени?» (ссылка скоро будет).
Иногда вам нужно иметь данные представления в классе. Рассмотрим класс complex
:
template<class Scalar> class complex {
public:
complex() : re(0), im(0) { }
complex(Scalar r) : re(r), im(0) { }
complex(Scalar r, Scalar i) : re(r), im(i) { }
// ...
complex& operator+=(const complex& a)
{ re+=a.re; im+=a.im; return *this; }
// ...
private:
Scalar re, im;
};
Этот тип предназначен для использования во многом как встроенный тип, и представление необходимо в объявлении, чтобы сделать возможным создание по настоящему локальных объектов (т.е. объектов, которые размещены в стеке, а не в куче), и для обеспечения надлежащего встраивания (inline
) простых операций. Подлинно локальные объекты и встраивание необходимы для получения производительности complex
, близкой к той, что предоставляется в языках со встроенным типом комплексных чисел.
Как объекты C++ размещаются в памяти?
Как и C, C++ определяет не компоновку, а только семантические ограничения, которые необходимо соблюдать. Поэтому разные реализации работают по-разному. Одно хорошее объяснение содержится в книге, которая в остальном устарела и не описывает никаких текущих реализаций C++: «Справочное руководство по C++ с аннотациями» («The Annotated C++ Reference Manual», обычно называемое ARM). В нем есть схемы основных примеров компоновки. В главе 2 TC++PL3 есть очень краткое объяснение.
По сути, C++ создает объекты просто путем объединения подобъектов. Таким образом,
struct A { int a,b; };
представлена двумя int
рядом друг с другом, и
struct B : A { int c; };
представлена A
, за которой следует int
; то есть три int
рядом друг с другом.
Виртуальные функции обычно реализуются путем добавления указателя («vptr») к каждому объекту класса с виртуальными функциями. Этот указатель указывает на соответствующую таблицу функций («vtbl»). У каждого класса есть свой собственный vtbl, общий для всех объектов этого класса.
Почему размер пустого класса не равен нулю?
Чтобы адреса двух разных объектов были разными. По той же причине new
всегда возвращает указатели на отдельные объекты. Рассмотрим:
class Empty { };
void f()
{
Empty a, b;
if (&a == &b) cout << "impossible: report error to compiler supplier";
Empty* p1 = new Empty;
Empty* p2 = new Empty;
if (p1 == p2) cout << "impossible: report error to compiler supplier";
}
Есть интересное правило, согласно которому пустой базовый класс не обязательно должен быть представлен отдельным байтом:
struct X : Empty {
int a;
// ...
};
void f(X* p)
{
void* p1 = p;
void* p2 = &p->a;
if (p1 == p2) cout << "nice: good optimizer";
}
Эта оптимизация безопасна и может быть очень полезной. Она позволяет программисту использовать пустые классы для представления очень простых концепций без накладных расходов. Некоторые современные компиляторы предоставляют такую «оптимизацию пустого базового класса».
Более того, «оптимизация пустого базового класса» больше не является необязательной оптимизацией, а является обязательным требованием к компоновке классов, начиная с C++11. Обвиняйте разработчика компилятора, если он не реализует ее должным образом.