Пост

Типы данных в языке C

Представление чисел

Порядок байт

Оперативная память компьютера разбивается на ячейки, каждая ячейка хранит некоторую порцию информации размером в один байт. Следовательно каждая ячейка в оперативной памяти равна одному байту. У каждой ячейки также есть свой адрес, благодаря которому в программе можно ссылаться на эту ячейку по адресу. Адресация в памяти обычно происходит слева направо, т.е адреса в памяти увеличиваются от младших байтов к старшим(от меньших адресов к большим)

В оперативной памяти для хранения многобайтовых значений используется порядок байт. Выбор порядка байт зависит от архитектуры процессора, и программы, написанные для одной архитектуры, могут работать некорректно на другой, если предполагается определенный порядок байт.

Данный порядок(endianness) бывает двух типов

Big-endian

big-endian(прямой порядок байт) - это привычный порядок записи чисел.

Возьмем число в шестнадцатеричном формате 0x1234. Данное число в памяти будет занимать 2 байта и если архитектура процессора подразумевает прямой порядок хранения байт, тогда в памяти данное число будет сохранено в том же порядке что и при записи 0x1234.

Если представить что адреса ячеек нумеруются от 0, то выглядело бы это следующим образом:

Aдресбайт 0байт 1
a1234

Старший байт (0x12) занимает младший адрес (a0), младший байт (0x34) занимает старший адрес (a1)

Little-endian

little-endian(обратный порядок байт) - это противоположность привычному порядку записи.

При данном порядке байт младшие байты хранятся в младших адресах, а старшие в старших. Если представить число 0x1234 в этом порядке в памяти, тогда получим следующую картину:

Aдресбайт 0байт 1
a3412

Старший байт (0x12) занимает старший адрес (a1), младший байт (0x34) занимает младщий адрес (a0)

Биты внутри байта нумеруется справа налево, в то время как сами байты в памяти нумеруются слева направо

Типы данных

Язык C является статически типизированным языком программирования.

Переменная - это поименованная область памяти. Хранилище каких-то данных, которое может изменять своё значение в процессе выполнения программы. Каждая переменная имеет свое имя (идентификатор), которое позволяет обращаться к ней в коде программы. Переменные могут содержать данные различных типов.

Тип данных определяет какие значения могут принимать переменные и какие операции можно выполнять с этими значениями.

Знаковые целочисленные типы

Всего стандарт определяет пять знаковых целочисленных типов:

ТипСиноним
signed char-
intsigned, signed int
shortshort int, signed short, signed short int
longlong int, signed long, signed long int
long long(C99)long long int, signed long long, signed long long int

Для каждого из пяти знаковых целочисленных типов из таблицы, имеется соответствующий беззнаковый тип, который занимает такое же количество памяти, с тем же выравниванием. Другими словами, если компилятор выравнивает объекты signed int по четным адресам байтов, то по четным адресам байтов будут выровнены и объекты unsigned int

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

Тип char также является одним из стандартных целочисленных типов. Однако однословное имя типа char
ассоциируется, в зависимости от компилятора, с signed char или unsigned char. Поскольку этот выбор
отдан на усмотрение реализации, char, signed char, unsigned char формально являются тремя различными типами.

Беззнаковые целочисленные типы

ТипСиноним
_Boolbool (определен в stdbool.h)
unsigned char-
unsigned intunsigned
unsigned shortunsigned short int
unsigned longunsigned long int
unsigned long longunsigned long long int

C99 вводит целочисленный тип _Bool для представления булевых значений. Истинное логическое значение кодируется как 1, а ложное как 0. Если включить в программу заголовочный файл stdbool.h, можно использовать идентификаторы bool, true, false.

Тип char всегда занимает один байт. С определяет только минимальные размеры стандартных типов:
short - имеет по крайней мере два байта.
long - имеет по крайней четыре байта.
long long по крайней мере восемь байт.

Хотя целочисленные типы могут быть больше указанных минимальных размеров, их размеры должны подчиняться следующему условию:
sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)

Оператор sizeof

Типы с плавающей точкой

ТипСиноним
float-
double-
long double 

Типы с плавающей точкой или вещественные(действительные) - это числа вида \(2.5, 4.1, {5\over2}\) которые могут содержать дробную часть. Хранятся данные числа в компьютере с ограниченной точностью

Для понимания ограниченной точности и также откуда появилось понятие “числа с плавающей точкой” нужно рассмотнреть пример того, как вещесвенные числа хранятся в памяти.

В качестве примера возьмем число 14.94 в десятичном формате и преобразуем его в двоичный.
Для преобразования нам нужно делить целую часть на 2, записывая остаток от деления, а дробную часть умножать на 2, записывая целую часть

1
2
3
4
14 / 2 = 7, остаток 0
7  / 2 = 3, остаток 1
3  / 2 = 1, остаток 1
1  / 2 = 0, остаток 1

После деления числа, в двоичном виде получаем \(1110_2\). Далее умножаем дробную часть

1
2
3
4
5
6
7
8
9
10
0.94 * 2 = 1.88, целая часть 1, остаток 0.88
0.88 * 2 = 1.76  целая часть 1, остаток 0.76
0.76 * 2 = 1.52, целая часть 1, остаток 0.52
0.52 * 2 = 1.04, целая часть 1, остаток 0.04
0.04 * 2 = 0.08, целая часть 0, остаток 0.08
0.08 * 2 = 0.16, целая часть 0, остаток 0.16
0.16 * 2 = 0.32, целая часть 0, остаток 0.32
0.32 * 2 = 0.64, целая часть 0, остаток 0.64
0.64 * 2 = 1.28, целая часть 1, остаток 0.28
0.28 * 2 = 0.56, целая часть 0, остаток 0.56

После умножения числа, в двоичном виде получаем \(1111000010_2\).

Умножение можно продолжать бесконечно, увеличивая тем самым точность числа (знаков после запятой), но в компьютерных системах используется конечное количество битов для представления дробной части числа. Когда вы представляете десятичную дробь в двоичной форме с конечным количеством бит, результат может быть только приблизительным. В реальности, длина представления ограничена, и точность зависит от числа бит, выделенных для представления дробной части.

\[14.94 = 1110.1111000010_2\]

Для представления формата чисел с плавающей точкой существует стандарт IEEE 754, в котором описано, что двоичное число представляется в виде формулы: $$(-1)^s \times c \times b^q$$

Где \(s\) - знак числа, \(c\) - мантисса, \(b\) - основание, \(q\) - экспонента.
Так как мы работаем с двоичными числами, основание будет равно 2 и формула принимает вид: $$(-1)^s \times c \times 2^q$$

Чтобы привести двоичное число \(1110.1111000010_2\) к формуле, нужно сдвинуть запятую влево настолько, чтобы в целой части была единица, при этом каждый сдвиг увеличивает степень основания $$1.1101111000010_2$$

Количество сдвигов получилось равным 3, следовательно формула принимает вид: $$(-1)^0 \times 1.1101111000010_2 \times 2^3$$

Знак \(s\) равен нулю так как число положительное
Экспонента \(q\) равна \(3\), так как число имеет \(3\) цифры до запятой (мантисса нормализована)
Мантисса \(c\) равна \(1.1101111000010_2\) она нормализована ( то есть первая цифра всегда \(1\))

Нормализованный вид представления позволяет экономить бит памяти увеличивая точность представления числа. Передняя часть мантиссы всегда является \(1\) (при условии, что число не является нулем). Это позволяет не хранить этот ведущий бит в формате, так как он всегда известен и всегда равен \(1\).

Для завершения представления числа в памяти отсалось прибавить число \(127\) к экспоненте.
Процесс прибавления \(127\) к экспоненте связан с тем, что формат чисел с плавающей точкой IEEE 754 использует смещенную (biased) экспоненту. Смещение на \(127\) позволяет представить отрицательные экспоненты как положительные числа, что упрощает сравнение и операции с ними. $$ 3 + 127 = 130 (10000010_2) $$

Таким образом мы получаем представление нашего числа по формуле IEEE 754 $$0\ 10000010\ 11011111000010000000000_2 $$

Тип void

Тип void указывает, что никакого значения нет.
Объявлять переменные такого типа нельзя, он предназначен для других операций.

Объявление указателей

Спомощью типа void можно объявлять указатели. Такой указатель будет хранить адрес объекта в памяти, но не его тип. Объявление таких указателей дает некоторую универсальность при работе с памятью разных типов, например указатель на тип void можно приводить к указателям на другие типы.

К примеру функции выделения динамической памяти возвращают указатель на void *, который в дальнейшем может быть приведен к указателю нужного типа:

1
2
/*выделяем память для хранения 1024 целых типов*/
int *ptr = (int *)malloc(sizeof(int) * 1024);

Конструкция вида (int *) выполняет приведение возвращаемого значения к типу указателя на int.

Возвращаемое значение функций

При объявлении функции в качестве возвращаемого значения можно указать данный тип.
Такая функция не будет возвращать никаких значений:

1
2
3
void sayHello() {
    printf("Hello");
}

Параметры функций

Если у функции нет параметров, данный тип также можно прописать в списке параметров функции:

1
2
3
void sayHello(void) {
    printf("Hello");
}

Если попытаться вызвать такую функцию с каким-нибудь параметром, то компилятор выдаст ошибку:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

void sayHello(void) {
    printf("Hello");
}

int main() {
    sayHello("Hello?");
    return 0;
}
1
2
3
4
5
6
7
test.c: In function ‘main’:
test.c:9:9: error: too many arguments to function ‘sayHello’
    9 |         sayHello("Hello?");
      |         ^~~~~~~~
test.c:3:6: note: declared here
    3 | void sayHello(void) {
      |      ^~~~~~~~

Если бы мы явно не указали void в параметрах функции, то никаких ошибок мы бы не получили:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

void sayHello() {
    printf("Hello");
}

int main() {
    sayHello("Hello?");
    return 0;
}
1
2
3
root@fedora-develop:~# gcc test.c 
root@fedora-develop:~# ./a.out 
Hello

Отбрасывание возвращаемых значений

1
(void) printf("Hello");

Такая конструкция используется редко, чаще всего возращаемое значение просто игнорируется, если его не нужно сохранять в переменной, достаточно просто написать вызов функции:

1
printf("Hello");

Размеры памяти и диапазоны значений типов

Таблица размеров типов и их диапазонов

Тип данныхРазмер в байтахДиапазон значений
char1от -128 до 127 или от 0 до 255 (в зависимости от знака)
unsigned char1от 0 до 255
signed char1от -128 до 127
int2 или 4от -32 768 или -2 147 483 648 до 32 767 или 2 147 483 647
unsigned int2 или 4от 0 до 65 535 или 4 294 967 295
short2от -32 768 до 32 767
unsigned short2от 0 до 65 535
long4от -2 147 483 648 до 2 147 483 647
unsigned long4от 0 до 4 294 967 295
long long (C99)8от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807
unsigned long long (C99)8от 0 до 18 446 744 073 709 551 615
float4от ~1.2e-38 до ~3.4e38 (6 значащих цифр)
double8от ~2.3e-308 до ~1.7e308 (15 значащих цифр)
long double10от ~1.1e-4932 до ~3.4e-4932 (19 значащих цифр)

Проверка размерности через оператор sizeof

Из таблицы видно что тип int может занимать 2 или 4 байта памяти, но от чего это зависит?
Размер типа зависит от архитектуры и от компилятора, который используется для компиляции программы. Для того чтобы самостоятельно проверить какой объем занимает тот или иной тип, можно воспользоваться оператором sizeof

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main() {
    printf("sizeof char\t\t\t = %lu bytes\n", sizeof(char));
    printf("sizeof unsigned char\t\t = %lu bytes\n", sizeof(unsigned char));
    printf("sizeof signed char\t\t = %lu bytes\n", sizeof(signed char));
    printf("sizeof unsigned int\t\t = %lu bytes\n", sizeof(unsigned int));
    printf("sizeof short\t\t\t = %lu bytes\n", sizeof(short));
    printf("sizeof unsigned short\t\t = %lu bytes\n", sizeof(unsigned short));
    printf("sizeof long\t\t\t = %lu bytes\n", sizeof(long));
    printf("sizeof unsigned long\t\t = %lu bytes\n", sizeof(unsigned long));
    printf("sizeof long long\t\t = %lu bytes\n", sizeof(long long));
    printf("sizeof unsigned long long\t = %lu bytes\n", sizeof(unsigned long long));
    printf("sizeof float\t\t\t = %lu bytes\n", sizeof(float));
    printf("sizeof double\t\t\t = %lu bytes\n", sizeof(double));
    printf("sizeof long double\t\t = %lu bytes\n", sizeof(long double));
    return 0;
}

Вывод на моей системе получился следующий:

1
2
3
4
5
6
7
8
9
10
11
12
13
sizeof char			 = 1 bytes
sizeof unsigned char		 = 1 bytes
sizeof signed char		 = 1 bytes
sizeof unsigned int		 = 4 bytes
sizeof short			 = 2 bytes
sizeof unsigned short		 = 2 bytes
sizeof long			 = 8 bytes
sizeof unsigned long		 = 8 bytes
sizeof long long		 = 8 bytes
sizeof unsigned long long	 = 8 bytes
sizeof float			 = 4 bytes
sizeof double			 = 8 bytes
sizeof long double		 = 16 bytes
Авторский пост защищен лицензией CC BY 4.0 .

© spybull. Некоторые права защищены.

Использует тему Chirpy для Jekyll

Популярные теги