Язык С: типы данных
Типы массивов и типы структур называются агрегированными типами. Тип-функция определяет тип возвращаемого значения функции. Ниже рассмотрим основные типы. Описание остальных типов будет дано в последующих разделах.
Целочисленные типы
В следующей таблице представлены стандартные целочисленные типы с их размерами хранения и диапазонами значений:
Чтобы получить точный размер типа или переменной на конкретной платформе, можно использовать оператор sizeof . Выражение sizeof(type) выдает размер хранения объекта или типа в байтах. Ниже приведен пример получения размера различных типов на компьютере с помощью различных констант, определенных в заголовочном файле limits.h :
Если скомпилировать и выполнить приведенную выше программу в Linux, получим следующий результат:
Типы с плавающей точкой
В следующей таблице представлены стандартные типы с плавающей точкой с размерами хранения, диапазонами значений и их точностью:
В заголовочном файле float.h определяются макросы, позволяющие использовать в программах эти значения и другие определения двоичного представления действительных значений. В следующем примере выводится объем памяти, занимаемый типом с плавающей точкой и диапазоном его значений.
Если скомпилировать и выполнить вышеприведенную программу в Linux, получим следующий результат:
Пустые типы ( void )
Пустой тип ( void ) указывает на отсутствие значения. Он используется в трех случаях:
- Возврат из функции —void
В языке C существуют различные функции, которые не возвращают никакого значения, или, можно сказать, возвращают void . Функция без возвращаемого значения имеет тип результата void . Например, void exit (int status);
- Аргумент функции —void
В языке C существуют различные функции, которые не принимают никаких параметров. Функция без параметров может принимать void . Например, int rand(void);
- Указательvoid
Указатель типа void * представляет адрес объекта, но не его тип. Например, функция выделения памяти void *malloc( size_t size ); возвращает указатель void , который можно привести к любому типу данных.
Свод правил по работе с целыми числами в C/C++
В основу статьи легли мои собственные выработанные нелегким путем знания о принципах работы и правильном использовании целых чисел в C/C++. Помимо самих правил, я решил привести список распространенных заблуждений и сделать небольшое сравнение системы целочисленных типов в нескольких передовых языках. Все изложение строилось вокруг баланса между краткостью и полноценностью, чтобы не усложнять восприятие и при этом отчетливо передать важные детали.
Всякий раз, когда я читаю или пишу код на C/C++, мне приходится вспоминать и применять эти правила в тех или иных ситуациях, например при выборе подходящего типа для локальной переменной/элемента массива/поля структуры, при преобразовании типов, а также в любых арифметических операциях или сравнениях. Обратите внимание, что типы чисел с плавающей запятой мы затрагивать не будем, так как это большей частью относится к анализу и обработке ошибок аппроксимации, вызванных округлением. В противоположность этому, математика целых чисел лежит в основе как программирования, так и компьютерной науки в целом, и в теории вычисления здесь всегда точны (не считая проблем реализации вроде переполнения).
Типы данных
Базовые целочисленные типы
Целочисленные типы устанавливаются с помощью допустимой последовательности ключевых слов, взятых из набора
Несмотря на то, что битовая ширина каждого базового целочисленного типа определяется реализацией (т.е. зависит от компилятора и платформы), стандартом закреплены следующие их свойства:
- char : минимум 8 бит в ширину;
- short : минимум 16 бит и при этом не меньше char ;
- int : минимум 16 бит и при этом не меньше short ;
- long : минимум 32 бит и при этом не меньше int ;
- long long : минимум 64 бит и при этом не меньше long .
Наличие знака
- Стандартный сhar может иметь знак или быть беззнаковым, что зависит от реализации.
- Стандартные short , int , long и long long идут со знаком. Беззнаковыми их можно сделать, добавив ключевое слово unsigned .
- Числа со знаком можно кодировать в двоичном формате в виде дополнительного кода, обратного или как величину со знаком. Это определяется реализацией. Заметьте, что обратный код и величина со знаком имеют различные шаблоны битов для отрицательного нуля и положительного, в то время как дополнительный код имеет уникальный нуль.
- Символьные литералы (в одинарных кавычках) имеют тип ( signed ) int в C, но ( signed или unsigned ) char в C++.
Дополнительные правила
- sizeof(char) всегда равен 1, независимо от битовой ширины char .
- Битовая ширина не обязательно должна отличаться. Например, допустимо использовать char , short и int , каждый шириной в 32 бита.
- Битовая ширина должна быть кратна 2. Например, int может иметь ширину 36 бит.
- Есть разные способы написания целочисленного типа. К примеру, в каждой следующей строке перечислен набор синонимов:
- int , signed , signed int , int signed ;
- short , short int , short signed , short signed int ;
- unsigned long long , long unsigned int long , int long long unsigned .
Типы из стандартных библиотек
- size_t (определен в stddef.h) является беззнаковым и содержит не менее 16 бит. При этом не гарантируется, что его ширина будет как минимум равна int .
- ptrdiff_t (определен в stddef.h) является целочисленным типом со знаком. Вычитание двух указателей будет давать этот тип. При этом не стоит ожидать, что вычитание двух указателей даст int .
- В stdint.h определена конкретная ширина типов: uint8_t , int8_t , 16 , 32 и 64 . Будьте внимательны к операциям, подразумевающим продвижение типов. Например, uint8_t + uint8_t даст int (со знаком и шириной не менее 16 бит), а не uint8_t , как можно было предположить.
Преобразования
Представим, что значение исходного целочисленного типа нужно преобразовать в значение целевого целочисленного типа. Такая ситуация может возникнуть при явном приведении, неявном приведении в процессе присваивания или при продвижении типов.
Как происходит преобразование?
Главный принцип в том, что, если целевой тип может содержать значение исходного типа, то это значение семантически сохраняется.
- Когда исходный тип расширяется до целевого типа с аналогичной знаковой характеристикой (например, signed char -> int или unsigned short -> unsigned long ), каждое исходное значение после преобразования сохраняется.
- Даже если исходный и целевой типы имеют разные диапазоны, все значения в их пересекающейся части будут сохранены. Например, int , содержащий значение в диапазоне [0, 255] , будет без потерь преобразован в unsigned char .
- При преобразовании в беззнаковый тип новое значение равняется старому значению по модулю 2 целевая ширина в битах . Объяснение:
- Если исходный тип беззнаковый и шире целевого, тогда старшие биты отбрасываются.
- Если исходный тип имеет знак, тогда в процессе преобразования берется исходное значение, и из него/к нему вычитается/прибавляется 2 целевая ширина в битах до тех пор, пока новое значение не впишется в диапазон целевого типа. Более того, если число со знаком представлено в дополнительном коде, то в процессе преобразования старшие биты отбрасываются, как и в случае с беззнаковыми числами.
- Если исходное значение вписывается в диапазон целевого типа, тогда процесс преобразования (например, расширение знака) производит целевое значение, семантически равное исходному.
- Если же оно не вписывается, тогда поведение будет определяться реализацией и может вызвать исключение (к примеру, прерывание из-за переполнения).
Арифметика
Продвижение/преобразование
-
Унарный арифметический оператор применяется только к одному операнду. Примеры: — ,
- В реализации присутствуют 16-битный short и 24-битный int . Если переменные x и y имеют тип unsigned short , то операция x & y продвигает оба операнда до signed int .
- В реализации присутствуют 32-битный char и 32-битный int . Если переменные x и y имеют тип unsigned char , то операция x – y продвигает оба операнда до unsigned int .
- (long) + (long) → (long) ;
- (unsigned int) * (int) → (unsigned int) ;
- (unsigned long) / (int) → (unsigned long) ;
- если int является 32-битным, а long 64-битным: (unsigned int) % (long) → (long) ;
- если int и long оба являются 32-битными: (unsigned int) % (long) → (unsigned long) .
Неопределенное поведение
Знаковое переполнение:
- При выполнении арифметических операций над целочисленным типом переполнение считается неопределенным поведением (UB). Такое поведение может вызывать верные, несогласованные и/или неверные действия как сразу, так и в дальнейшем.
- При выполнении арифметики над беззнаковым целым (после продвижений и преобразований) любое переполнение гарантированно вызовет оборот значения. Например, UINT_MAX + 1 == 0 .
- Выполнение арифметики над беззнаковыми целыми фиксированного размера может привести к едва уловимым ошибкам. Например:
- Пусть uint16_t = unsigned short , и int равен 32-битам. Тогда uint16_t x=0xFFFF , y=0xFFFF , z=x*y ; x и y будут продвинуты до int , и x * y приведет к переполнению int , вызвав неопределенное поведение.
- Пусть uint32_t = unsigned char , и int равен 33-битам. Тогда uint32_t x=0xFFFFFFFF , y=0xFFFFFFFF , z=x+y ; x и y будут продвинуты до int , и x + y приведет к переполнению int , то есть неопределенному поведению.
- Чтобы обеспечить безопасную арифметику с беззнаковыми целыми, нужно либо прибавить 0U , либо умножить на 1U в качестве пустой операции. Например: 0U + x + y или 1U * x * y . Это гарантирует, что операнды будут продвинуты как минимум до ранга int и при этом останутся без знаков.
- Деление на нуль и остаток с делителем нуля также относятся к неопределенному поведению.
- Беззнаковое деление/остаток не имеют других особых случаев.
- Деление со знаком может вызывать переполнение, например INT_MIN / -1 .
- Остаток со знаком при отрицательных операндах может вызывать сложности, так как некоторые части являются однообразными, в то время как другие определяются реализацией.
- Неопределенным поведением считается битовый сдвиг (< < и >>) на размер, который либо отрицателен, либо равен или больше битовой ширины.
- Левый сдвиг беззнакового операнда (после продвижения/преобразования) считается определенным правильно и отклонений в поведении не вызывает.
- Левый сдвиг операнда со знаком, содержащего неотрицательное значение, вследствие которого 1 бит переходит в знаковый бит, является неопределенным поведением.
- Левый сдвиг отрицательного значения относится к неопределенному поведению.
- Правый сдвиг неотрицательного значения (в типе операнда без знака или со знаком) считается определенным правильно и отклонений в поведении не вызывает.
- Правый сдвиг отрицательного значения определяется реализацией.
Счетчик цикла
Выбор типа
Предположим, что у нас есть массив, в котором нужно обработать каждый элемент последовательно. Длина массива хранится в переменной len типа T0 . Как нужно объявить переменную счетчика цикла i типа T1 ?
- Самым простым решением будет использовать тот же тип, что и у переменной длины. Например:
- Говоря обобщенно, переменная счетчика типа T1 будет работать верно, если диапазон T1 будет являться (не строго) надмножетсвом диапазона T0 . Например, если len имеет тип uint16_t , тогда отсчет с использованием signed long (не менее 32 бит) сработает.
- Говоря же более конкретно, счетчик цикла должен просто покрывать всю фактическую длину. Например, если len типа int гарантированно будет иметь значение в диапазоне [3,50] (обусловленное логикой приложения), тогда допустимо отсчитывать цикл, используя char без знака или со знаком (в котором однозначно можно представить диапазон [0,127] ).
- Нежелательно использовать переменную длины и переменную счетчика с разной знаковостью. В этом случае сравнение вызовет неявное сложное преобразование, сопровождаемое характерными для платформы проблемами. К примеру, не стоит писать такой код:
Отсчет вниз
Для циклов, ведущих отсчет вниз, более естественным будет использовать счетчик со знаком, потому что тогда можно написать:
При этом для беззнакового счетчика код будет таким:
Примечание: сравнение i >= 0 имеет смысл только, когда i является числом со знаком, но всегда будет давать true , если оно будет беззнаковым. Поэтому, когда это выражение встречается в беззнаковом контексте, значит, автор кода скорее всего допустил ошибку в логике.
Заблуждения
Все пункты приведенного ниже списка являются мифами. Не опирайтесь на эти ложные убеждения, если хотите писать корректный и портируемый код.
What does int & mean
It returns a reference to an int. References are similar to pointers but with some important distinctions. I’d recommend you read up on the differences between pointers, references, objects and primitive data types.
«Effective C++» and «More Effective C++» (both by Scott Meyers) have some good descriptions of the differences and when to use pointers vs references.
EDIT: There are a number of answers saying things along the lines of «references are just syntactic sugar for easier handling of pointers». They most certainly are not.
Consider the following code:
The line p = pointerToB changes the value of p , i.e. it now points to a different piece of memory.
r = referenceToB does something completely different: it assigns the value of b to where the value of a used to be. It does not change r at all. r is still a reference to the same piece of memory.
The difference is subtle but very important.
If you still think that references are just syntactic sugar for pointer handling then please read Scott Meyers’ books. He can explain the difference much better than I can.
Be careful here. you’re walking the C/C++ line. There’s a quite clear distinction but it doesn’t always appear that way:
C++: this often means a reference. For example, consider:
As such, C++ can pass by value or pass by reference.
C however has no such pass by reference functionality. & means "addressof" and is a way to formulate a pointer from a variable. However, consider this:
Deceptively similar, yet fundamentally different. What you are doing in C is passing a copy of the pointer. Now these things still point to the same area of memory, so the effect is like a pass by reference in terms of the pointed-to memory, but it is not a reference being passed in. It is a reference to a point in memory.
Try this (Compile as C):
If C had pass by reference, the incoming pointer address, when changed by addptr should be reflected in main , but it isn’t. Pointers are still values.
So, C does not have any pass by reference mechanism. In C++, this exists, and that is what & means in function arguments etc.
Edit: You might be wondering why I can’t do this demonstration in C++ easily. It’s because I can’t change the address of the reference. At all. From this quite good guide to references:
How can you reseat a reference to make it refer to a different object?
No way.
You can’t separate the reference from the referent.
Unlike a pointer, once a reference is bound to an object, it can not be "reseated" to another object. The reference itself isn’t an object (it has no identity; taking the address of a reference gives you the address of the referent; remember: the reference is its referent).
In that sense, a reference is similar to a const pointer such as int* const p (as opposed to a pointer to const such as int const* p). But please don’t confuse references with pointers; they’re very different from the programmer’s standpoint.
By request, on returning references:
Any good compiler ought to give you this warning message in some form:
exp.cpp:7:11: warning: reference to stack memory associated with local variable ‘f’ returned
What does this mean? Well, we know function arguments are pushed onto the stack (note: not actually on x64, they go into registers then the stack, but they are on the stack literally on x86) and what this warning is saying is that creating a reference to such an object is not a good idea, because it’s not guaranteed to be left in place. The fact it is is just luck.
So what gives? Try this modified version:
Run this, and you’ll see both values get updated. What? Well they both refer to the same thing and that thing is being edited.
C#: числовые типы (Numeric Types)
Среди целочисленных (integral) типов int и long используются наиболее часто. Остальные применяются для функциональной совместимости или когда действительно необходимы большие числа. Из реальных чисел, float и double , также называемые числами с плавающей точкой (floatingpoint types), обычно используются для научных вычислений. Тип decimal обычно используется в финансовых вычислениях, где необходима десятичная арифметика и высокая точность. Различия между double и decimal приведены в следующей таблице:
В связи с тем что типы float и double в своем внутреннем представлении являются двоичными, литералы с дробной частью (десятичные) не являются совсем точными:
По этой причине float и double лучше не использовать для финансовых вычислений, для которых лучше подходит тип decimal , десятичный в своей основе и поэтому более точный для дробных десятичных чисел.
Числовые литералы (Numeric Literals)
Целочисленные литералы (Integral literals) могут использовать десятичную (decimal) или шестнадцатеричную (hexadecimal) запись; шестнадцатеричная обозначается префиксом 0x (например, 0x7f = 127). Реально-числовые литералы также используют десятичную и шестнадцатеричную запись (например, 1E06 ).
По умолчанию, компилятор определяет тип числовых литералов по следующим критериям: если литерал содержит десятичную точку или знак экспоненты ( E ) — это double , в противном случае — это int , uint , long , или ulong .
Тип литерала также можно указать с помощью суффиксов, перечисленных в таблице выше. Суффикс ставиться сразу после литерала, например:
Необходимость в суффиксах U и L возникает крайне редко, т.к. почти всегда типы uint , long , и ulong могут быть либо выведены либо неявно преобразованы из int :
Суффикс D технически излишен, т.к. все литералы с десятичной точкой приводятся к double . Суффиксы F и M более полезны: они необходимы при написании дробных литералов типа float или decimal . Без суффикса подобные литералы будут считаться типом double , который не преобразуется скрыто к float или decimal .
Преобразование чисел
Скрытое преобразование целых чисел возможно только когда конечный тип вмещает все возможные значения исходного, в противном случае необходимо явное преобразование:
Преобразование реальных чисел. Число типа float может быть скрыто преобразовано к double , поскольку double способно вместить все возможные значения float . Обратное преобразование должно быть явным. Преобразование между decimal и другими реальными типами должно быть явным.
Преобразование между реальными и целыми числами. Преобразование целых чисел в реальные может быть скрытым, а вот обратное должно быть явным. Преобразование реальных чисел в целые происходит путем отсечения дробной части (без округления). Если необходимо преобразование с округлением, нужно использовать System.Convert класс. Важно помнить, что преобразование больших целых чисел в числа с плавающей точкой сохраняет их величину (magnitude, колличество цифр), но может привести к потере точности (precision):
Числовые операторы
Арифметические операторы
Арифметические операторы: + — * / % . Они применимы ко всем числовым типам, корме 8- и 16-битных целочисленных типов.
При делении целых чисел остаток всегда отсекается. При делении на переменную равную нулю возникает ошибка времени исполнения, а при делении на литерал или константу 0 — ошибка времени компиляции.
Оператор % возвращает остаток после деления.
Инкремент и декремент
Операторы инкремента ++ (Incremantal) и декремента — (Decremental) увеличивают и уменьшают (соответственно) числа на 1. Они могут идти до или после переменной, в зависимости от того, когда необходимо изменить переменную: до или после вычисления выражения:
Переполнение и оператор check
При выполнении арифметических операций с целыми числами возможно переполнение (Integral overflow) — ситуация, когда полученный результат превышает арифметические пределы типы. Как правило оно происходит тихо: никакие исключения не выбрасываются.
Оператор checked заставляет генерировать ошибку при переполнении (OverflowException). Он воздействует на выражения с операторами ++, —, — (унарный, обозначающий отрицательное число), +, -, *, / и операторами явного приведения между целочисленными типами. Применять его можно либо к одному выражению, либо к блоку:
В последнем случае, если для какого-либо выражения или блока необходимо отключить проверку переполнения, аналогичным способом необходимо использовать оператор unchecked .
Побитовые (поразрядные, bitwise) операторы
Каждый целочисленный тип может быть представлен в виде последовательности бит:
Битовые операторы применяются последовательно к каждой паре соответствующих битов своих операндов. Результатом будет значение того же типа, что и операнды, каждый бит которого есть результат применения соответствующего побитового оператора.
К побитовым операторам относятся:
- И — & — устанавливает (в значение 1) только те биты, которые установлены (имеют значение 1) в обоих операндах, остальные биты выключаются (устанавливаются в 0)
- ИЛИ — | — устанавливает те биты которые установлены либо в одном, либо в другом операнде
- ИСКЛЮЧАЮЩЕЕ ИЛИ — ^ — устанавливает только те биты, которые установлены либо в одном, либо в другом операнде, но не в обоих одновременно
- ОТРИЦАНИЕ —
a ) ; // 11111010 == 250
Операции с 8-битные и 16-битные целыми числами
8-битные и 16-битные целые числа ( byte , sbyte , short , ushort ) не имеют собственных операторов, поэтому C# неявно преобразовывает их в большие типы при необходимости. В связи с этим при компиляции может возникнуть ошибка, если результату попытаться снова назначить младший тип:
В этом примере x и y скрыто преобразуются в int , чтоб над ними можно было выполнить сложение. Результат в связи с этим тоже будет иметь тип int , который не может быть скрыто преобразован обратно в short (т.к. это может привести к потере данных). Чтобы избежать ошибки, необходимо использовать явное приведение к типу:
Операции над числами с плавающей точкой
Типы с плавающей точкой (в отличие от целочисленных) имеют ряд специальных значений, которые ведут себя по особенному в ряде случаев. Этими специальными значениями являются:
- NaN (Not a Number)
- +∞
- –∞
- –0
Деление ненулевого значения на ноль дает бесконечность:
Деление ноль на ноль и вычитание бесконечности из бесконечности дает NaN :
При использовании оператора сравнения == , значение NaN никогда не будет равно никакому другому значению, в том числе другому значению NaN . Для проверки значения на равенство NaN нужно использовать методы float.IsNaN и double.IsNaN :
Однако, при использовании метода object.Equals два NaN значения будут равны:
Преобразование чисел в строку, форматные строки
Числовые типы определяют экземплярный метод ToString , позволяющий преобразовать число в строку. В качестве дополнительного параметра метод может принимать форматные строки. Подробно этот метод и форматные строки рассмотрены в разделе, посвященном форматированию и преобразованию строк. Ниже будут перечислены стандартные и специальные форматные строки для чисел.
Стандартные форматные строки для чисел
Символ Значение Пример Результат Примечание G или g Общий (general) формат 1.2345, «G»
0.00001, «G»
0.00001, «g»
1.2345, «G3»
12345, «G3»1.2345
1E-05
1e-05
1.23
1.23E04Перключается на экспоненциальную запись для очень маленьких и больших чисел. Цифра ограничивает точность (количество цифр) F Формат с фиксированной точкой 2345.678, «F2»
2345.6, «F2»2345.68
2345.60Цифра указывает сколько знаков оставить после запятой N Формат с фиксированной точкой и разделителем групп 2345.678, «N2»
2345.6, «N2»2,345.68
2,345.60Тоже самое что и F, но с разделением групп (тысяч) D Заполнение ведущими нулями 123, «D5»
123, «D1»00123
123Только для целых типов. Цифра указывает до какой длины дополнять, усечение не происходит E или e Экспоненциальная запись 56789, «E»
56789, «e»
56789, «E2»5.678900E+004
5.678900e+004
5.68E+004Цифра указывает точность (по умолчанию — 6) C Денежное значение 1.2, «C»
1.2, «C4»$1.20
$1.2000Цифра указывает количество знаков после запятой P Процент .503, «P»
.503, «P0»50.30 %
50 %Цифра указывает количество знаков после запятой X или x Шестнадцатеричный формат 47, «X»
47, «x»
47, «X4»2F
2f
002FX — верхний регистр, x — нижний регистр R Округление 1f / 3f, «R» 0.333333343 Указание иной форматной строки, пустой строки или null эквивалентно «G».
Специальные форматные строки для чисел
Символ Значение Пример Результат Примечание # Заполнитель для цифр 12.345, «.##»
12.345, «.####»12.35
12.3450 Заполнитель для нуля 12.345, «.00»
12.345, «.0000»
99, «000.00»12.35
12.3500
099.00. Десятичная точка , Разделитель групп 1234, «#,###,###»
1234, «0,000,000»1,234
0,001,234, Коэффициент 1000000, «#,»
1000000, «#,,»1000
1% Процентная запись 0.6, «00%» 60% E0, e0,
E+0, e+0 E-0,
e-0Экспоненциальная запись 1234, «0E0»
1234, «0E+0»
1234, «0.00E00»
1234, «0.00e00»1E3
1E+3
1.23E03
1.23e03/ Скобка для литерального символа 50, @»\#0″ #50 Используется в сочетании с префиксом @ в строках или можно применять // ‘xx»xx’ Скобка для литеральной строки 50, «0 ‘…’» 50 … ; Разделитель секций 15, «#;(#);zero»
−5, «#;(#);zero»
0, «#;(#);zero»15
(5)
zeroдругой символ Литерал 35.2, «$0 . 00c» $35 . 20c Преобразование строки в число, NumberStyles
Преобразование строки в число можно осуществить с помощью статических методов Parse и TryParse . Подробно эти методы рассмотрены в разделе, посвященном форматированию и преобразованию строк.
Оба метода могут принимать enum NumberStyles , определяющий как строка читается при преобразовании в числовой тип (они позволяют указывать такие аспекты, как могут ли встречаться во входной строке круглые скобки или символ валюты):