Система типов C++
Терминология
Переменная: символическое имя количества данных, чтобы имя можно было использовать для доступа к данным, на которые он ссылается в области кода, где он определен. В C++ переменная обычно используется для ссылки на экземпляры скалярных типов данных, тогда как экземпляры других типов обычно называются объектами.
Объект. для простоты и согласованности в этой статье используется объект term для ссылки на любой экземпляр класса или структуры, и когда он используется в общем смысле, включает все типы, даже скалярные переменные.
Тип POD (обычные старые данные): Эта неофициальная Категория типов данных в C++ относится к скалярным типам (см. раздел фундаментальные типы) или к классам Pod. Класс POD не содержит статических данных-членов, которые не являются типами POD, а также не содержит пользовательских конструкторов, пользовательских деструкторов или пользовательских операторов присваивания. Кроме того, класс POD не имеет виртуальных функций, базового класса и ни закрытых, ни защищенных нестатических данных-членов. Типы POD часто используются для внешнего обмена данными, например с модулем, написанным на языке С (в котором имеются только типы POD).
Указание типов переменных и функций
C++ — это строго типизированный язык, который также является статически типизированным; Каждый объект имеет тип, и этот тип никогда не изменяется (не следует путать с статическими объектами данных). При объявлении переменной в коде необходимо либо явно указать ее тип, либо использовать auto ключевое слово, чтобы указать компилятору вывести тип из инициализатора. При объявлении функции в коде необходимо указать тип каждого аргумента и его возвращаемое значение или void значение, если функция не возвращает никакого значения. Исключением является использование шаблонов функции, которые допускают аргументы произвольных типов.
После объявления переменной изменить ее тип впоследствии уже невозможно. Однако можно скопировать значения переменной или возвращаемое значение функции в другую переменную другого типа. Такие операции называются преобразованиями типов, которые иногда являются обязательными, но также являются потенциальными источниками потери или неправильности данных.
При объявлении переменной типа POD настоятельно рекомендуется инициализировать ее, т. е. указать начальное значение. Пока переменная не инициализирована, она имеет «мусорное» значение, определяемое значениями битов, которые ранее были установлены в этом месте памяти. Необходимо учитывать эту особенность языка C++, особенно при переходе с другого языка, который обрабатывает инициализацию автоматически. При объявлении переменной типа, не являющегося классом POD, инициализация обрабатывается конструктором.
В следующем примере показано несколько простых объявлений переменных с небольшим описанием для каждого объявления. В примере также показано, как компилятор использует сведения о типе, чтобы разрешить или запретить некоторые последующие операции с переменной.
Базовые (встроенные) типы
Базовые типы распознаются компилятором, в котором предусмотрены встроенные правила, управляющие операциями, выполняемыми с такими типами, а также преобразованием в другие базовые типы. Полный список встроенных типов, а также их размер и числовые ограничения см. в разделе Встроенные типы.
На следующем рисунке показаны относительные размеры встроенных типов в реализации Microsoft C++:
В следующей таблице перечислены наиболее часто используемые фундаментальные типы и их размеры в реализации Microsoft C++:
Другие реализации C++ могут использовать разные размеры для определенных числовых типов. Дополнительные сведения о размерах и отношениях размеров, необходимых стандарту C++, см. в разделе Встроенные типы.
Тип void
Квалификатор типа const
Любой встроенный или пользовательский тип может квалифицироваться ключевым словом const. Кроме того, функции-члены могут быть const полными и даже const перегруженными. Значение const типа не может быть изменено после инициализации.
Строковые типы
Определяемые пользователем типы
Компилятор не имеет встроенных сведений о пользовательском типе. Он узнает о типе при первом обнаружении определения во время процесса компиляции.
типы указателей
Датировано назад к самой ранней версии языка C, C++ позволяет объявить переменную типа указателя с помощью специального декларатора * (звездочка). Тип указателя хранит адрес расположения в памяти, в котором хранится фактическое значение данных. В современных C++ они называются необработанными указателями и доступны в коде с помощью специальных операторов * (звездочки) или -> (тире с символом «больше»). Это называется разыменованием, и какой из используемых объектов зависит от того, выполняется ли разыменование указателя на скаляр или указатель на член в объекте. Работа с типами указателя долгое время была одним из наиболее трудных и непонятных аспектов разработки программ на языках C и C++. В этом разделе приводятся некоторые факты и рекомендации по использованию необработанных указателей, если вы хотите, но в современной версии C++ больше не требуется (или рекомендуется) использовать необработанные указатели для владения объектами, так как при развитии интеллектуального указателя (см. Дополнительные сведения в конце этого раздела). Все еще полезно и безопасно использовать необработанные указатели для отслеживания объектов, но если требуется использовать их для владения объектом, необходимо делать это с осторожностью и после тщательного анализа процедуры создания и уничтожения объектов, которые им принадлежат.
Первое, что необходимо знать, — это то, что при объявлении переменной необработанного указателя выделяется только память, необходимая для хранения адреса расположения памяти, на который будет ссылаться указатель при разыменовывании. Выделение памяти для самого значения данных (также называемое резервным хранилищем) еще не выделено. Другими словами, объявив переменную необработанного указателя, вы создаете переменную адреса памяти, а не фактическую переменную данных. Разыменовывание переменной указателя до проверки того, что она содержит действительный адрес в резервном хранилище, приведет к неопределенному поведению (обычно неустранимой ошибке) программы. В следующем примере демонстрируется подобная ошибка:
Пример разыменовывает тип указателя без выделения памяти для хранения фактических целочисленных данных или без выделенного допустимого адреса памяти. В следующем коде исправлены эти ошибки:
Однако можно легко забыть удалить динамически выделенный объект, особенно в сложном коде, который вызывает ошибку ресурса, называемую утечкой памяти. По этой причине в современном С++ настоятельно не рекомендуется использовать необработанные указатели. Почти всегда лучше обернуть необработанный указатель в Интеллектуальный указатель, который автоматически освобождает память при вызове его деструктора (когда код выходит за пределы области для смарт-указателя); с помощью смарт-указателей вы практически устраняете целый класс ошибок в программах на C++. В следующем примере предположим, что MyClass — это пользовательский тип, который имеет открытый метод DoSomeWork();
Дополнительные сведения о смарт-указателях см. в разделе интеллектуальные указатели.
Дополнительные сведения о преобразовании указателей см. в разделе преобразования типов и типизация.
Дополнительные сведения об указателях в целом см. в разделе указатели.
Типы данных Windows
Дополнительные сведения
Дополнительные сведения о системе типов C++ см. в следующих разделах.
Преобразования типов и безопасность типов
Описание типовых проблем преобразования типов и способов их избежать.
Переменные
Переменные
П еременные используются для хранения значений (sic!). Переменная характеризуется типом и именем. Начнём с имени. В си переменная может начинаться с подчерка или буквы, но не с числа. Переменная может включать в себя символы английского алфавита, цифры и знак подчёркивания. Переменная не должна совпадать с ключевыми словами (это специальные слова, которые используются в качестве управляющих конструкций, для определения типов и т.п.)
| auto | double | int | struct |
| break | else | long | switch |
| register | typedef | char | extern |
| return | void | case | float |
| unsigned | default | for | signed |
| union | do | if | sizeof |
| volatile | continue | enum | short | while | inline |
А также ряд других слов, специфичных для данной версии компилятора, например far, near, tiny, huge, asm, asm_ и пр.
Типы переменных
Целые
Указанные выше значения характерны для компилятора VC2012 на 32-разрядной машине. Так что, если ваша программа зависит от размера переменной, не поленитесь узнать её размер.
| Тип | Размер, байт | Минимальное значение | Максимальное значение |
|---|---|---|---|
| unsigned char | 1 | 0 | 255 |
| signed char ( char ) | 1 | -128 | 127 |
| unsigned short | 2 | 0 | 65535 |
| signed short ( short ) | 2 | -32768 | 32767 |
| unsigned int ( unsigned ) | 4 | 0 | 4294967296 |
| signed int ( int ) | 4 | -2147483648 | 2147483647 |
| unsigned long | 4 | 0 | 4294967296 |
| signed long ( long ) | 4 | -2147483648 | 2147483647 |
| unsigned long long | 8 | 0 | 18446744073709551615 |
| signed long long ( long long ) | 8 | -9223372036854775808 | 9223372036854775807 |
sizeof
В си есть оператор, который позволяет получить размер переменной в байтах. sizeof переменная, или sizeof(переменная) или sizeof(тип). Это именно оператор, потому что функция не имеет возможности получить информацию о размере типов во время выполнения приложения. Напишем небольшую программу чтобы удостовериться в размерах переменных.
(Я думаю ясно, что переменные могут иметь любое валидное имя). Эту программу можно было написать и проще
В си один и тот же тип может иметь несколько названий
short === short int
long === long int
long long === long long int
unsigned int === unsigned
Типы с плавающей точкой
Переполнение переменных
Си не следит за переполнением переменных. Это значит, что постоянно увеличивая значение, скажем, переменной типа int в конце концов мы «сбросим значение»
Вообще, поведение при переполнении переменной определено только для типа unsigned: Беззнаковое целое сбросит значение. Для остальных типов может произойти что угодно, и если вам необходимо следить за переполнением, делайте это вручную, проверяя аргументы, либо используйте иные способы, зависящие от компилятора и архитектуры процессора.
Постфиксное обозначение типа
Следующий код, однако, не будет приводить к ошибкам, потому что происходит неявное преобразование типа
Шестнадцатеричный и восьмеричный формат
В о время работы с числами можно использовать шестнадцатеричный и восьмеричный формат представления. Числа в шестнадцатиричной системе счисления начинаются с 0x, в восьмеричной системе с нуля. Соответственно, если число начинается с нуля, то в нём не должно быть цифр выше 7:
Экспоненциальная форма представления чисел
Объявление переменных
При объявлении переменной пишется её тип и имя.
Можно объявить несколько переменных одного типа, разделив имена запятой
Здесь объявлены переменные a и b внутри функции main, и переменная z внутри тела цикла. Следующий код вызовет ошибку компиляции
Это связано с тем, что объявление переменной стоит после оператора присваивания. При объявлении переменных можно их сразу инициализировать.
int i = 0;
При этом инициализация при объявлении переменной не считается за отдельный оператор, поэтому следующий код будет работать
Начальное значение переменной
Область видимости переменной
П еременные бывают локальными (объявленными внутри какой-нибудь функции) и глобальными. Глобальная переменная видна всем функциям, объявленным в данном файле. Локальная переменная ограничена своей областью видимости. Когда я говорю, что переменная «видна в каком-то месте», это означает, что в этом месте она определена и её можно использовать. Например, рассмотрим программу, в которой есть глобальная переменная
Будет выведено
foo: 100
bar: 333
Здесь глобальная переменная global видна всем функциям. Но аргумент функции затирает глобальную переменную, поэтому при передаче аргумента 333 выводится локальное значение 333.
Вот другой пример
Программа выведет 555. Также, как и в прошлом случае, локальная переменная «важнее». Переменная, объявленная в некоторой области видимости не видна вне её, например
Этот пример не скомпилируется, потому что переменная y существует только внутри своего блока.
Вот ещё пример, когда переменные, объявленные внутри блока перекрывают друг друга
Программа выведет
30
20
10
Глобальных переменных необходимо избегать. Очень часто можно услышать такое. Давайте попытаемся разобраться, почему. В ваших простых проектах глобальные переменные выглядят вполне нормально. Но представьте, что у вас приложение, которое
Во-первых, глобальная переменная, если она видна всем, может быть изменена любой частью программы. Вы изменили глобальную переменную, хотите её записать, а другая часть программы уже перезаписала в неё другое значение (на самом деле это целый класс проблем, которые возникают в многопоточной среде). Во-вторых, при больших размерах проекта не уследить, кто и когда насоздавал глобальных переменных. В приведённых выше примерах видно, как переменные могут перекрывать друг друга, то же произойдёт и в крупном проекте.
Безусловно, есть ситуации, когда глобальные переменные упрощают программу, но такие ситуации случаются не часто и не в ваших домашних заданиях, так что НЕ СОЗДАВАЙТЕ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ!
Переменные могут быть не только целочисленными и с плавающей точкой. Существует множество других типов, которые мы будем изучать в дальнейшем.
4.5 – Целочисленные типы данных без знака, и почему их следует избегать
Целочисленные типы данных без знака
В предыдущем уроке (4.4 – Целочисленные типы данных со знаком) мы рассмотрели целочисленные типы со знаком, которые представляют собой набор типов, которые могут содержать положительные и отрицательные целые числа, включая 0.
C++ также поддерживает беззнаковые целочисленные типы. Беззнаковые целочисленные значения – это целые числа, которые могут содержать только неотрицательные значения.
Определение беззнаковых целочисленных значений
Диапазоны значений целочисленных типов без знака
Ниже показана таблица, показывающая диапазоны беззнаковых целочисленных типов:
| Размер / Тип | Диапазон |
|---|---|
| 1 байт без знака | от 0 до 255 |
| 2 байт без знака | от 0 до 65 535 |
| 4 байт без знака | от 0 до 4 294 967 295 |
| 8 байт без знака | от 0 до 18 446 744 073 709 551 615 |
Диапазон беззнаковой переменной размером n бит составляет от 0 до (2 n )-1.
Целочисленные типы без знака хорошо подходят для сетей и систем с небольшим объемом памяти, когда нет необходимости в отрицательных числах, поскольку беззнаковые целочисленные значения могут хранить бо́льшие положительные числа, не занимая дополнительной памяти.
Напоминание относительно значений со знаком и без знака
Начинающие программисты иногда путаются между значениями со знаком и без знака. Простой способ запомнить разницу: чтобы отличить отрицательные числа от положительных, мы используем знак минус. Если знак не указан, мы предполагаем, что число положительное. Следовательно, целочисленное значение со знаком (целочисленный тип со знаком) может различать положительные и отрицательные числа. Целочисленное значение без знака (целочисленный тип без знака) предполагает, что все значения положительны.
Переполнение целочисленных значений без знака
Что произойдет, если мы попытаемся сохранить число 280 (для представления которого требуется 9 бит) в 1-байтовом (8-битном) целочисленном значении без знака? Ответ – переполнение.
Примечание автора
Как ни странно, в стандарте C++ прямо говорится, что «вычисление с использованием беззнаковых операндов никогда не может переполниться». Это противоречит общему мнению программистов о том, что целочисленное переполнение охватывает случаи как со знаком, так и без знака. Учитывая, что большинство программистов рассмотрело бы этот случай как переполнение, мы будем называть его переполнением, несмотря на утверждения C++ об обратном.
Если значение без знака выходит за пределы допустимого диапазона, оно делится на число, превышающее на единицу наибольшее число, допустимое для заданного типа данных, и сохраняется только остаток.
Число 280 слишком велико, чтобы поместиться в наш 1-байтовый диапазон от 0 до 255. Значение, которое на 1 больше максимального числа для этого типа данных, равно 256. Следовательно, мы делим 280 на 256, получая остаток 24. Остаток 24 – это то, что в итоге сохранится.
Вот еще один способ размышления о том же. Любое число, превышающее наибольшее число, которое может быть представлено заданным типом данных, просто «оборачивается» (или совершает циклический переход). Число 255 находится в диапазоне 1-байтового целочисленного типа, поэтому 255 сохранится нормально. А число 256 находится за пределами диапазона, поэтому оно оборачивается до значения 0. 257 оборачивается до значения 1. 280 оборачивается до значения 24.
Давайте посмотрим на это, используя 2-байтовые целые числа:
Как вы думаете, каким будет результат работы этой программы?
Приведенный выше код в некоторых компиляторах вызывает предупреждение, поскольку компилятор обнаруживает, что целочисленный литерал выходит за пределы допустимого диапазона для данного типа. Если вы всё равно хотите скомпилировать этот код, временно отключите параметр «Treat warnings as errors» (обрабатывать предупреждения как ошибки).
В качестве отступления.
Многие заметные ошибки в истории видеоигр произошли из-за поведения циклического перехода целочисленных значений без знака. В аркадной игре Donkey Kong невозможно пройти уровень 22 из-за бага переполнения, из-за которого у пользователя недостаточно бонусного времени для завершения уровня.
Споры по поводу чисел без знака
Многие разработчики (и некоторые крупные компании, например, Google) считают, что разработчикам следует избегать целочисленных типов без знака.
Во многом это связано с двумя типами поведения, которые могут вызывать проблемы.
На машине автора эта, казалось бы, невинно выглядящая программа дает следующий результат:
Во-вторых, непредвиденное поведение может возникнуть при смешивании целочисленных значений со знаком и без знака. В приведенном выше примере, даже если один из операндов ( x или y ) будет со знаком, другой операнд (беззнаковый) приведет к тому, что операнд со знаком будет преобразован в целочисленное значение без знака, и будет получено такое же поведение!
Рассмотрим следующий фрагмент:
Если вам нужно защитить функцию от отрицательных входных данных, используйте вместо этого утверждение или исключение. Они оба будут рассмотрены позже.
Начинающие программисты часто используют целочисленные типы без знака для представления неотрицательных данных или для того, чтобы воспользоваться дополнительно расширенным диапазоном. Бьярн Страуструп, разработчик C++, сказал: «Использование unsigned int вместо int для получения еще одного бита для представления положительных целых чисел почти никогда не бывает хорошей идеей».
Предупреждение
Избегайте использования числовых типов без знака, за исключением особых случаев или, когда это необходимо.
Не избегайте отрицательных чисел с помощью беззнаковых типов. Если вам нужен больший диапазон, чем предлагает числовой тип со знаком, используйте один из целочисленных типов гарантированной ширины, показанных в следующем уроке (4.6 – Целочисленные типы фиксированной ширины и size_t ).
Если вы всё же используете беззнаковые числа, по возможности избегайте смешивания чисел со знаком и без знака.
Так где же стоит использовать беззнаковые числа?
В C++ всё же есть несколько случаев, когда можно (или необходимо) использовать беззнаковые числа.
Во-первых, числа без знака предпочтительнее при работе с битами (рассматривается в главе O (это заглавная буква «о», а не «0»).
Во-вторых, использование беззнаковых чисел всё еще неизбежно в некоторых случаях, в основном тех, которые связаны с индексацией массивов. Подробнее об этом мы поговорим в уроках по массивам и индексированию массивов.
Также обратите внимание, что, если вы разрабатываете для встраиваемой системы (например, Arduino) или какого-либо другой системы с ограничениями процессора/памяти, использование чисел без знака более распространено и приемлемо (а в некоторых случаях неизбежно) по соображениям производительности.
Свод правил по работе с целыми числами в C/C++
В основу статьи легли мои собственные выработанные нелегким путем знания о принципах работы и правильном использовании целых чисел в C/C++. Помимо самих правил, я решил привести список распространенных заблуждений и сделать небольшое сравнение системы целочисленных типов в нескольких передовых языках. Все изложение строилось вокруг баланса между краткостью и полноценностью, чтобы не усложнять восприятие и при этом отчетливо передать важные детали.
Всякий раз, когда я читаю или пишу код на C/C++, мне приходится вспоминать и применять эти правила в тех или иных ситуациях, например при выборе подходящего типа для локальной переменной/элемента массива/поля структуры, при преобразовании типов, а также в любых арифметических операциях или сравнениях. Обратите внимание, что типы чисел с плавающей запятой мы затрагивать не будем, так как это большей частью относится к анализу и обработке ошибок аппроксимации, вызванных округлением. В противоположность этому, математика целых чисел лежит в основе как программирования, так и компьютерной науки в целом, и в теории вычисления здесь всегда точны (не считая проблем реализации вроде переполнения).
Типы данных
Базовые целочисленные типы
Целочисленные типы устанавливаются с помощью допустимой последовательности ключевых слов, взятых из набора
Несмотря на то, что битовая ширина каждого базового целочисленного типа определяется реализацией (т.е. зависит от компилятора и платформы), стандартом закреплены следующие их свойства:
Наличие знака
Дополнительные правила
Типы из стандартных библиотек
Преобразования
Представим, что значение исходного целочисленного типа нужно преобразовать в значение целевого целочисленного типа. Такая ситуация может возникнуть при явном приведении, неявном приведении в процессе присваивания или при продвижении типов.
Как происходит преобразование?
Главный принцип в том, что, если целевой тип может содержать значение исходного типа, то это значение семантически сохраняется.
Арифметика
Продвижение/преобразование
Неопределенное поведение
Счетчик цикла
Выбор типа
Отсчет вниз
Для циклов, ведущих отсчет вниз, более естественным будет использовать счетчик со знаком, потому что тогда можно написать:
При этом для беззнакового счетчика код будет таким:
Заблуждения
Все пункты приведенного ниже списка являются мифами. Не опирайтесь на эти ложные убеждения, если хотите писать корректный и портируемый код.

