Повторение основного курса Си
Побитовые операции
Побитовые операции работают над двоичным представлением чисел.
Операции бывают:
-
одномерные:
~
— побитовая инверсия
-
двумерные:
&
— побитовое И|
— побитовое ИЛИ<<
— побитовый сдвиг влево>>
— побитовый сдвиг вправо^
— побитовое исключающее ИЛИ (XOR)
Поразрядные и логические операторы
Поразрядные операторы не следует использовать вместо логических. Результатом логических операторов (&&
, ||
и !
) является либо 0, либо 1 но побитовые операторы возвращают целочисленное значение.
Логические операторы рассматривают любой ненулевой операнд как 1.
Рассмотрим следующую программу:
int32_t x = 3, y = 8; //0b0011 = 3, 0b1000 = 8
(x & y) ? printf(" True ") : printf(" False ");
(x && y) ? printf(" True ") : printf(" False ");
Первая строка выведет False вторая - True.
Побитовый сдвиг
Необходимо помнить:
-
сдвиг на величину, превышающую размер правого операнда, или сдвиг на отрицательное число разрядов не определён.
-
При сдвиге вправо результат операции зависит от знаковости операнда. Если операнд имеет беззнаковый тип, то слева выдвигаются нулевые биты. Такой сдвиг иногда называют арифметическим. Когда операнд знаковый, то слева выдвигается самый старший бит — бит знака. В том случае, когда этот бит равен 1, то выдвигается единица, а если бит знака равен 0, то выдвигается ноль. Такой вид сдвига называют логическим.
Сдвиг влево и вправо на 1 эквивалентен умножению и делению на 2
Примеры побитового сдвига:
uint8_t u = 0xF5; //беззнаковый тип
u >>= 1; // сдвиг вправо на 1 бит
printf("u = %" PRIx8 "\n", u); // u = 7a
int8_t u = 0xF5; //знаковый тип
u >>= 1; // сдвиг вправо на 1 бит
printf("u = %" PRIx8 "\n", u); // u = fa
Операция исключающее ИЛИ - XOR
Побитовый оператор XOR — весьма полезный оператор, он используется во многих задачах. Простым примером может быть следующая задача: дан массив чисел, где все элементы встречаются четное количество раз, кроме одного числа, найдите нечётное встречающееся число. Эту проблему можно эффективно решить, просто выполнив операцию XOR для всех чисел.
int find_odd_element(int32_t arr[], size_t n) {
int32_t res = 0;
for (size_t i = 0; i < n; i++)
res ^= arr[i];
return res;
}
int main(void) {
int32_t arr[] = { 17, 17, 24, 97, 24, 24, 24 };
size_t n = sizeof(arr) / sizeof(arr[0]);
printf("The element is %" PRId32, find_odd_element(arr, n));
return 0;
}
Побитовое И
Операция побитовое И может быть использована для быстрой проверки чётности числа.
int32_t x = 17;
(x & 1) ? printf("Odd") : printf("Even")
Примеры побитовых операций
uint32_t a = 60; /* 60 = 0011 1100 */
uint32_t b = 13; /* 13 = 0000 1101 */
int32_t c = 0;
c = a & b; /* 12 = 0000 1100 */
printf("Line 1 c = %d\n", c );
c = a | b; /* 61 = 0011 1101 */
printf("Line 2 c = %d\n", c );
c = a ^ b; /* 49 = 0011 0001 */
printf("Line 3 c = %d\n", c );
c = ~a; /*-61 = 1100 0011 */
printf("Line 4 c = %d\n", c );
c = a << 2; /* 240 = 1111 0000 */
printf("Line 5 c = %d\n", c );
c = a >> 2; /* 15 = 0000 1111 */
printf("Line 6 c = %d\n", c );
Структуры с битовыми полями
Пример:
Предположим есть некоторое количество переменных, упакованных в структуру для хранения значений True/False. Такая структура будет занимать в памяти 8 байт, хотя на самом деле использоваться будет только два бита для хранения 0 и 1.
/* Определение структуры */
struct {
uint32_t width;
uint32_t height;
} st1;
При использовании полей внутри структуры, можно задать их размер. Такая структура будет занимать в памяти только 4 байта, но только 2 бита будет фактически использовано. Можно разместить в ней до 32-ух однобитовых полей, и это никак не скажется на размере выделенной для неё памяти
/* Определение структуры с побитовыми полями */
struct {
uint32_t width : 1;
uint32_t height: 1;
} st2;
Пример оптимизации структуры для хранения даты
struct date {
uint32_t day : 5; // значение от 0 до 31
uint32_t month : 4; // значение 0 до 15
uint32_t year;
};
int main()
{
struct date dt = { 31, 12, 2021 };
printf("Size is %lu\n", sizeof(dt));
printf("Date is %u/%u/%u\n", dt.day, dt.month, dt.year);
return 0;
}
Если вы зададите значение, которое не помещается в поле с данным размером, то оно будет сохранено с ошибкой.
struct date dt = { 31, 12, 2021 };
dt.month = 16;
printf("Date is %u/%u/%u", dt.day, dt.month, dt.year);
::: Внимание! В языке Си нет возможности создать структуру вида побитовый массив. :::
Как в памяти хранится вещественное число?
#include <stdio.h>
#include <inttypes.h>
union floatbit {
float value;
struct {
uint32_t mant : 23;
uint32_t exp : 8;
uint32_t sign : 1;
} bit;
} f;
int main()
{
f.value = 4.0;
printf("Memory size is %lu\n", sizeof(f));
printf("f.value = %f\n", f.value );
printf("sign = %x\n", f.bit.sign);
printf("exp = %x\n", f.bit.exp);
printf("mantissa = %x\n", f.bit.mant);
return 0;
}
Вывод на консоль Memory size is 4, f.value = 4.000000, sign = 0, exp = 81, mantissa = 0
Массивы, структуры и функции
Указателями в Си являются переменные, которые хранят адрес. Указатель всегда имеет одинаковый размер. Указатель может хранить адрес:
- Переменной, в том числе другого указателя
- Массива или строки
- Структуры
- Функции
Примеры:
int i=123;
int *pi; //указатель на переменную
pi = &i;
int **ppi;// указатель на указатель
ppi = π
printf("**ppi = %d\n",**ppi);
int ar[5];
int *pa; //указатель на массив
pa = &ar[0]; // pa = ar;
struct s {
int i;
float f;
} st;
struct s *ps;
ps = &st; //указатель на структуру
printf("ps -> i = %d\n",ps->i);
int ar[3][5];
int (*pa)[5]; //указатель массив из 5-и элементов
pa = ar+1; //адрес 1-ой строки
Структуры
При обращении к полю структуры по указателю на структуру можно использовать оператор '→'
.
ps→i эквивалент (*ps).i
;
Структуры можно передавать в функции и возвращать из функции как по значению, так и по ссылке. При передаче по значению происходит копирование всей структуры на стек. В случае, если структура занимает много места в памяти, то оптимально передавать в функцию её адрес, чтобы избежать дополнительного расхода памяти.
Пример:
#include <stdio.h>
struct student{
int id;
char name[20];
int group;
};
void func(struct student record){
printf(" Id is: %d \n", record.id);
printf(" Name is: %s \n", record.name);
printf(" Group is: %d \n", record.group);
}
void pfunc(struct student *record)
{
printf(" Id is: %d \n", record->id);
printf(" Name is: %s \n", record->name);
printf(" Group is: %d \n", record->group);
}
int main(){
struct student record = {1, "Vasiliy", 102};
func(record);
pfunc(&record);
return 0;
}
Вывод на консоль:
Id is: 1
Name is: Vasiliy
Group is: 102
Id is: 1
Name is: Vasiliy
Group is: 102
При описании структурного типа память не выделяется. Выделение памяти происходит только после объявления переменных.
Пример:
struct student // Описание нового типа. Память не выделяется.
{
int id;
char name[20];
int group;
};
struct student st; // Описание переменной. Выделяется память
под нее.
Пример описания структурного типа:
При описании структурного типа делать инициализацию нельзя.
// ТАК НЕЛЬЗЯ
struct student
{
char name[20] = “Ivan”;
int group;
};
// Так можно
struct student
{
char name[20];
int group;
};
struct student st = {“Ivan”, 104};
Передача функции в функцию
Указатель на функцию задаётся следующим образом:
#include <stdio.h>
int func(int n) {
printf("Hello func %d\n",n);
return n+1;
}
int main()
{
int (*fp)(int);
fp = func;
fp(5);
return 0;
}
Вывод на консоль:
Hello func 5