Встроенные / внутренние / примитивные типы данных // FAQ C++

Добавлено 18 сентября 2020 в 22:28

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

Закон Мерфи в основном гарантирует, что ваши конкретные потребности подпадут под последний пункт, а не под любой из предыдущих пунктов.

Теги

C++ / CPPFAQВысокоуровневые языки программированияПрограммированиеЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.