Встроенные / внутренние / примитивные типы данных // 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на свободно хранящуюся/размещенную копию данных. Это полезно для полиморфных объектов, поскольку позволяет получить эффект возврата по значению, но без проблемы «нарезки». Производительность необходимо оценивать в индивидуальном порядке. - другое – этот список приведен для примера, а не в качестве исключения. Другими словами, это всего лишь начальная, а не конечная точка.
Закон Мерфи в основном гарантирует, что ваши конкретные потребности подпадут под последний пункт, а не под любой из предыдущих пунктов.
