Встроенные / внутренние / примитивные типы данных // FAQ C++
Может ли на каких-либо машинах sizeof(char)
быть равен 2? Например, как насчет двухбайтовых символов?
Нет, sizeof(char)
всегда равен 1. Всегда. Он никогда не бывает равен 2. Никогда, никогда, никогда.
Даже если вы думаете о «символе» как о многобайтовой штуковине, с char
это не так. sizeof(char)
всегда равен ровно 1. Никаких исключений, никогда.
Послушайте, я знаю, что это вызовет у вас головную боль, поэтому, пожалуйста, просто прочтите следующие несколько часто задаваемых вопросов по порядку, и, надеюсь, боль пройдет где-то на следующей неделе.
Какие единицы измерения у sizeof
?
Байты.
Например, если sizeof(Fred)
равно 8, расстояние между двумя объектами Fred
в массиве Freds
будет ровно 8 байтов.
Или другой пример, это означает, что sizeof(char)
– это один байт. Именно один байт. Один, один, один, ровно один байт, всегда один байт. Никогда не два байта. Без исключений.
А как насчет машин или компиляторов, поддерживающих многобайтовые символы? Вы хотите сказать, что «символ» и char
могут отличаться?!
Да, именно так: то, что обычно называют «символом», может отличаться от того, что C++ называет char
.
Мне очень жаль, если это больно; но, поверьте, лучше сразу же избавиться от боли. Сделайте глубокий вдох и повторяйте за мной: «символ и char
могут отличаться». Вот так, разве не лучше? Нет? Что ж, читайте дальше – станет еще хуже.
Но, но, а как насчет машин, у которых char
имеет более 8 бит? Вы ведь не утверждаете, что байт в C++ может иметь более 8 бит, не так ли?!
Да, именно так: байт в C++ может иметь более 8 бит.
Язык C++ гарантирует, что байт всегда должен иметь не менее 8 бит. Но есть реализации C++, в которых более 8 бит на байт.
Хорошо, я мог представить машину с 9-битными байтами. Но уж точно не 16-битными или 32-битными байтами. Верно?
Неправильно.
Я слышал об одной реализации C++ с 64-битными «байтами». Вы прочитали всё правильно: байт в этой реализации имеет 64 бита. 64 бита на байт. 64. Как 8 умножить на 8.
И да, вы правы, объединение со сказанном ранее означало бы, что char
в этой реализации будет иметь 64 бита.
Я совсем запутался. Не могли бы вы еще раз повторить правила, касающиеся байтов, char
и символов?
Вот правила:
- Язык C++ создает у программиста впечатление, что память представлена как последовательность того, что C++ называет «байтами».
- Каждая из этих штук, которые язык C++ называет байтами, имеет не менее 8 бит, но может иметь и более 8 бит.
- Язык C++ гарантирует, что
char*
(указатель наchar
) может указывать на отдельные байты. - Язык C++ гарантирует, что между двумя байтами нет битов. Это означает, что каждый бит в памяти является частью байта. Если вы пройдетесь по памяти с помощью
char*
, вы сможете увидеть каждый бит. - Язык C++ гарантирует отсутствие битов, которые являются частью двух разных байтов. Это означает, что изменение одного байта никогда не вызовет изменения другого байта.
- Язык C++ дает вам способ узнать, сколько бит находится в байте в вашей конкретной реализации: включите заголовок
<climits>
, тогда фактическое количество бит на байт будет задано макросомCHAR_BIT
.
Давайте проиллюстрируем эти правила на примере. PDP-10 имеет 36-битные слова без каких-либо аппаратных средств для адресации чего-либо в одном из этих слов. Это означает, что указатель может указывать только на объекты на 36-битной границе: указатель не может указывать на 8 бит справа от того места, куда указывает другой указатель.
Один из способов соблюдать все вышеперечисленные правила состоит в том, чтобы компилятор C++ для PDP-10 определял «байт» как 36 бит. Другой допустимый подход – определить «байт» как 9 бит и имитировать char*
двумя словами памяти: первое может указывать на 36-битное слово, второе может быть битовым смещением внутри этого слова. В этом случае компилятору C++ потребуется добавить дополнительные инструкции при компиляции кода, использующего указатели char*
. Например, код, сгенерированный для *p = 'x'
, может считывать слово в регистр, а затем использовать битовые маски и битовые сдвиги для изменения соответствующего 9-битного байта в этом слове. Тип int*
всё еще может быть реализован как один аппаратный указатель, поскольку C++ допускает sizeof(char*) != sizeof(int*)
.
Используя ту же логику, можно было бы определить «байт» в C++ для PDP-10 12-битным или 18-битным. Однако описанный выше метод не позволяет нам определить «байт» в C++ для PDP-10 8-битным, поскольку 8×4 равно 32, что означает, что после каждого 4-го байта мы пропустим 4 бита. Для этих 4 битов можно использовать более сложный подход, например, упаковав девять байтов (по 8 бит каждый) в два соседних 36-битных слова. Важным моментом здесь является то, что memcpy()
должна иметь возможность видеть каждый бит памяти: между двумя соседними байтами не может быть никаких битов.
Примечание: один из популярных подходов, не связанных с C/C++, в PDP-10 заключался в упаковке 5 байтов (по 7 бит каждый) в каждое 36-битное слово. Однако это не сработает в C или C++, поскольку 5×7 равно 35, что означает, что использование указателей char*
для обхода памяти будет «пропускать» бит после каждого пятого байта (а также потому, что C++ требует, чтобы байты имели не менее 8 бит).
Что такое тип POD / простая структура данных?
Тип, состоящий только из «простых старых данных» (Plain Old Data, POD, или же простая структура данных).
POD – это тип C++, имеющий эквивалент в C и использующий те же правила, что и C для инициализации, копирования, компоновки и адресации.
Например, объявление C struct Fred x;
не инициализирует элементы переменной x
типа Fred
. Чтобы сделать то же самое в C++, Fred
должен не иметь никаких конструкторов. Точно так же, чтобы сделать C++ версию копирования такой же, как C-версия, Fred
в C++ должен не перегружать оператор присваивания. Чтобы убедиться, что другие правила совпадают, у версии в C++ должно не быть ни виртуальных функций, ни базовых классов, ни нестатических членов, которые являются private
или protected
, ни деструктора. Однако он может иметь статические члены данных, статические функции-члены и нестатические невиртуальные функции-члены.
Фактическое определение типа POD является рекурсивным и немного корявым. Вот несколько упрощенное определение POD: нестатические члены данных типа POD должны быть public
и могут относиться к любому из следующих типов: bool
, любой числовой тип, включая различные варианты char
, любой тип перечисления, любой тип указателя на данные (то есть, любой тип, конвертируемый в void*
), любой тип указателя на функцию или любой тип POD, включая массивы из любого из них. Примечание: указатели на данные и указатели на функцию допустимы, но указатели на члены – нет. Также обратите внимание, что не допускаются ссылки. Кроме того, тип POD не может иметь конструкторов, виртуальных функций, базовых классов или перегруженного оператора присваивания.
При инициализации нестатических членов данных встроенных / внутренних / примитивных типов, следует использовать «список инициализации» или присваивание?
Для симметрии обычно лучше инициализировать все нестатические члены данных в «списке инициализации» конструктора, даже те, у которых встроенный / внутренний / примитивный тип. Далее в FAQ будет показано, почему и как.
Должен ли я при инициализации статических членов данных встроенных / встроенных / примитивных типов беспокоиться о «фиаско с порядком инициализации static
»?
Да, если вы инициализируете встроенную / внутреннюю / примитивную переменную с помощью выражения, которое компилятор не оценивает исключительно во время компиляции. В FAQ есть несколько решений этой (незаметной!) проблемы.
Могу ли я определить перегрузку оператора, который работает со встроенными / внутренними / примитивными типами?
Нет, язык C++ требует, чтобы перегрузки операторов принимали хотя бы один операнд «типа класса» или типа перечисления. Язык C++ не позволяет вам определять оператор, все операнды/параметры которого относятся к примитивным типам.
Например, вы не можете определить operator==
, который принимает два char*
и использует сравнение строк. Это хорошая новость, потому что если s1
и s2
имеют тип char*
, выражение s1 == s2
уже имеет четко определенное значение: оно сравнивает два указателя, а не две строки, на которые они указывают. В любом случае вам не следует использовать указатели. Используйте std::string
вместо char*
.
Если C++ позволит вам переопределить значение операторов во встроенных типах, вы никогда не узнаете, что такое 1 + 1: это будет зависеть от того, какие заголовочные файлы включены, и переопределил ли один из этих заголовочных файлов сложение, например, как вычитание.
Когда я удаляю массив какого-либо встроенного / внутреннего / примитивного типа, почему я не могу просто сказать delete a
вместо delete[] a
?
Потому что не можете.
Послушайте, пожалуйста, не пишите мне, спрашивая, почему C++ такой. Он просто такой. Если вам действительно нужно обоснование, купите отличную книгу Бьярна Страуструпа «Дизайн и эволюция C++». Но если ваша настоящая цель – написать код, не тратьте слишком много времени на выяснение того, почему в C++ есть эти правила, а вместо этого, просто соблюдайте их.
Итак, вот правило: если a
указывает на массив штуковин, который был выделен с помощью new T[n]
, то вы должны, должны, должны удалять его с помощью delete[] a
. Даже если элементы в массиве встроенного типа. Даже если они имеют тип char
, int
или void*
. Даже если вы не понимаете почему.
Как без цикла определить, является ли целое число степенью двойки?
inline bool isPowerOf2(int i)
{
return i > 0 && (i & (i - 1)) == 0;
}
Что необходимо возвращать из функции?
На практике случаев очень много. Вот несколько из них в случайном порядке:
void
– если вам не нужно возвращаемое значение, не возвращайте его.- локальная переменная по значению - это самый простой вариант, и при небольшой осторожности NRVO (named return value optimization, оптимизация именованного возращаемого значения) максимизирует производительность.
- локальная переменная по указателю или ссылке – НЕТ! Пожалуйста, не делайте так.
- член данных по значению – отличный выбор, если функция является нестатической функцией-членом, и если член данных можно скопировать относительно быстро, например,
int
. Если член данных является чем-то, что медленно копируется, это приведет к снижению производительности, если вы вызовете эту функцию-член во внутреннем цикле приложения, ограниченном CPU. - член данных по указателю – хорошо, но убедитесь, что вы не хотите возвращать его по ссылке, и убедитесь, что вы используете
const Foo*
илиFoo const*
, если не хотите, чтобы вызывающий объект изменял этот член данных. Поскольку вызывающие могут хранить указатель, а не копировать член данных, вы должны предупредить их в «контракте» функции-члена о том, что они не должны использовать возвращенный указатель после смерти данного объекта. - член данных по ссылке на неконстанту – хорошо, но это позволяет вызывающей стороне вносить изменения в член данных вашего объекта без того, чтобы ваш класс «увидел» это изменение. Если у вас есть set-метод, который изменяет этот член данных, то используйте вместо возврата по ссылке на неконстанту либо возврат по ссылке на константу, либо возврат по значению. Еще одна вещь: поскольку вызывающие могут хранить ссылку, а не копировать член данных, вы должны предупредить их в «контракте» функции-члена о том, что они не должны использовать возвращенную ссылку после смерти данного объекта.
- член данных по ссылке на константу – хорошо, но это позволяет вашим пользователям видеть тип данных ваших переменных-членов. Это означает, что если вам когда-либо понадобится изменить тип переменной-члена, это изменение может нарушить код, использующий ваш класс, а это один из основных моментов инкапсуляции. Вы можете уменьшить этот риск, предоставив определение типа
public typedef
для типа этой переменной-члена (и, следовательно, типа возвращаемого значения ссылки на константу), и предупредив своих пользователей, что они должны использовать это определение типаtypedef
, а не исходный базовый тип. Другое дело заключается в том, что если вызывающий объект перехватывает эту ссылку, а не копирует объект, то базовый объект ссылки может измениться «под носом у вызывающего», даже если тип является ссылкой на константу. Поскольку это удивляет многих программистов, разумно предупреждать вызывающих об этом в «контракте» функции-члена. Вы также должны предупредить вызывающих об отказе от возвращенной ссылки после смерти данного объекта. shared_ptr
на член, который был выделен с помощьюnew
– здесь есть компромиссы, которые очень похожи на те, что при возврате члена по указателю или по ссылке; смотрите соответствующие пункты в данном списке. Преимущество состоит в том, что вызывающие могут законно удерживать и использовать возвращенный указатель после того, как данный объект умирает.- локальный
unique_ptr
илиshared_ptr
на свободно хранящуюся/размещенную копию данных. Это полезно для полиморфных объектов, поскольку позволяет получить эффект возврата по значению, но без проблемы «нарезки». Производительность необходимо оценивать в индивидуальном порядке. - другое – этот список приведен для примера, а не в качестве исключения. Другими словами, это всего лишь начальная, а не конечная точка.
Закон Мерфи в основном гарантирует, что ваши конкретные потребности подпадут под последний пункт, а не под любой из предыдущих пунктов.