Язык Си
История
Кен Томпсон и Деннис Ричи у PDP-11
Википедия про историю языка Си
Мы используем стандарт С17 (прошлые стандарты: C89, C99, C11; грядёт стандарт C23).
Черновик стандарта (сам стандарт можно купить у ISO за большие деньги, там написано то же самое.)
Что регулирует стандарт
- Implementation-defined behavior
- Пример: представление целых чисел (two's complement, sign-magnitude)
- Unspecified behavior
- Пример: порядок вычисления аргументов при вызове функции (
f(g(), h())
)
- Пример: порядок вычисления аргументов при вызове функции (
- Undefined behavior (UB)
- Пример: выход за границы массива
Статья про то, зачем нам нужен UB
*p = 1;
if (!p) {
fprintf(stderr, “NULL pointer\n”);
return;
}
Целые типы
char signed char unsigned char
short = signed short unsigned short
int = signed int unsigned int
long = signed char unsigned char
long long = signed long long unsigned long long
- CHAR_BIT >= 8
- sizeof(char)== 1
- sizeof(signed T) == sizeof(unsigned T) == sizeof(T)
- short как минимум 16 бит
- int как минимум 16 бит
- long как минимум 32 бита
- long long как минимум 64 бита
sizeof для типов:
Тип Atmel AVR 32-bit Win64 64-bit
char 1 1 1 1
short 2 2 2 2
int 2 4 4 4
long 4 4 4 8
long long - 8 8 8
__int128 - - - 16
#include <limits.h>
CHAR_MIN, CHAR_MAX, SCHAR_MIN, SCHAR_MAX,
UCHAR_MIN, ..., INT_MIN, INT_MAX, UINT_MAX,
LONG_MIN, LONG_MAX, ULONG_MAX, LLONG_MIN,
LLONG_MAX, ULLONG_MAX
Заголовочный файл <stdint.h>
:
- Знаковые типы:
int8_t, int16_t, int32_t, int64_t
- Беззнаковые типы:
uint8_t, uint16_t, uint32_t, uint64_t
- Типы размера, достаточного для хранения
адреса в памяти:
intptr_t, uintptr_t
int8_min_t
(минимум 8 бит, но может быть больше, если платформой не поддерживается вариант с 8 битами),int8_fast_t
(минимум 8 бит, но больше, если вычисления так производить быстрее)
Литералы:
0 // int
0U // unsigned
0UL // unsigned long
0LL // long long
Знаковая и беззнаковая арифметика
Unsigned – арифметика по модулю \(2^N\).
Signed – операции с неправильным результатом, как правило, UB.
UINT_MAX + 1; // OK, always 0
INT_MAX + 1; // UB
int i;
i < i + 1; // the compiler may assume this is always true
Опции gcc:
-ftrapv
– abort() при знаковом целочисленном переполнении-fwrapv
– выполнять знаковые операции по модулю 2^N, но компилятор не делает никаких оптимизационных предположений-fsanitize=undefined
– частичная проверка на undefined behavior при работе программы
Для обнаружения переполнения в знаковой арифметике можем воспользоваться GCC built-in overflow detection
Implicit conversions
Integer promotion: числа короче, чем int (то есть char, short) при использовании их в выражениях превратятся в int или unsigned int
cppreference on implicit conversions
Hello World
#include <stdio.h>
#define GREETING "Hello, world!\n"
// макрос будет раскрыт в ходе препроцессирования
int main(void) {
printf("Hello, world!\n");
return 0;
}
int main(){} //объявление функции с неизвестными параметрами
// запуск
gcc hello.c -o hello
./hello
gcc -c hello.c - делает объектный файл
gcc -S hello.c - делает ассемблерный файл
gcc -E hello.c - остановка на стадии препроцессирования
gcc -Wall -Werror // show all warnings, warnings will be treated as errors
Исполнение начинается с нахождения функции main: если она не нашлась - ошибка компоновки. Main - единственная функция, которая может ничего не возвращать, и это не приведет к UB. В таком случае будет считаться, что main возвращает 0
#include <stdio.h>
#define GREETING "Hello, world\n"
int foobar() {}
int main(void) {
printf(GREETING);
int x = foobar(); // UB
return 0;
}
Классы хранения переменных
#include <stdio.h>
#define GREETING "Hello, world\n"
int x; // статический класс хранения
int main(void) {
int y; // автоматический класс хранения
printf(GREETING);
return 0;
}
- Автоматический : переменная объявлена внутри функции (создается при своем объявлении, перестает существовать при завершении функции, то есть живет на стеке). Если не инициализировать, то остается не инициализированной - при использовании будет ub.
- Статический: существует все время, пока существует программа (попадает в секцию data/bss). Автоматически инициализируются 0 (по стандарту), если их не инициализировать.
- Регистровый - нет адреса в памяти
register int r;
Extern, static
Объявить переменную, определенную в другой единице трансляции
extern int x;
Ключевое слово static
Для переменной, объявленной вне функции, static
сообщает компилятору, что
переменная не должна быть доступна из других единиц трансляции
(то есть переменная
получит скрытый класс линковки — перестанет быть .global
в терминах
языка ассемблера).
Для переменной, объявленной внутри функции, ключевое слово static означает,
что у нее будет статический класс хранения — она попадёт в секцию .data
или .bss
соответствующего объектного файла, и её значение будет сохраняться
между вызовами функции. Имя такой переменной всё ещё доступно только внутри
функции.
Запись заголовочных файлов: пользовательских и системных
//incr.h
int incr(void);
#include <stdio.h> // системный
#include "incr.h" // пользовательский
#define GREETING "Hello, world\n"
int main(void) {
printf(printf("x is %d\n", incr()));
return 0;
}
Declarations and definitions
Объявления функций сколько угодно раз, определение только 1.
Заголовочный файл может быть включен в код несколько раз, но может все сломать. Чтобы избежать, используем макрос:
//foo.h
#ifndef FOO_H
#define FOO_H
struct foo {
int x;
};
#endif
Почти для всех компиляторов вместо этого можно писать #pragma once
Структуры
Без ключевого слова struct
тэг структуры не является именем типа.
#include <stdio.h>
#include "incr.h"
struct example {
char b;
int i;
long long l;
};
int main(void) {
struct pair p1 = {1, 2, 3}; // инициализация 1
struct pair p = {.b = 1, .l = 2}; // инициализация 2
// кого явно не инициализировали - тот 0
printf("x is %d\n", p.i);
}
Выравнивание типов
В си происходит автоматически. Обычно выравнивание равно размеру типа, но не
больше размера машинного слова (int) на данной платформе. Например, на 32-битной
платформе выравнивание long long
будет 32 бита.
Чтобы элементы структуры выровнялись, компилятор вставляет между ними padding - пустые байты. У структуры в целом выравнивание максимальное из всех ее членов.
struct example { // 32 bit platform
char b;
// 3 padding bytes
int i;
// no padding bytes
long long l;
};
Как попросить компилятор не добавлять паддинг
struct example {
char b;
int i;
long long l;
} __attribute__((packed));
Массивы
// пустой инициализатор невалиден
int arr[10] = {0} // остальные инициализируются автоматически
// можно инициализировать произвольный элемент массива
int arr[10] = {[5] = 4};
// кол-во элементов = кол-во инициалированных
int arr[] = {1,2,3}
Выход за границы массива - ub. Узнать размеры массива можно через макрос.
#define ARRLEN(a) (sizeof(a) / sizeof((a)[0]))
int main() {
int arr[10] = {[5] = 4};
for (int i = 0; i < ARRLEN(arr); ++i) {
printf("%d\n", arr[i]);
}
}
Указатели
&p; // взять адрес переменной
int* p = &arr[0]; // указатель
-
Разыменование указателя
*p;
-
Арифметика
Прибавить целое число - отступить столько эл-ов в массиве (к адресу указателя прибавляется это число * sizeof(type))
int *ptr = &arr[0]; for (int i = 0; i < ARRLEN(arr); ++i) { printf("%d\n", *(ptr + i)); }
равнозначно
int *ptr = &arr[0]; for (int i = 0; i < ARRLEN(arr); ++i) { printf("%d\n", ptr[i]); }
Строки
0-терминированная строка - массив байт, заканчивающийся на \0
char[] c = "hello world\n"; // лежит на стеке
c[0]="H"; //можно менять, но длину строки менять нельзя
//такую строку не получится поменять, она лежит в rodata
char* str = "hello world\n";
Длина строки char* str = "hello world\n"
:
int strlen(char* s){
int i;
for (i = 0; s[i] != '\0'; ++i);
return i;
}
int main(void){
char* str = "hello world\n";
printf("%d %s", strlen(str), str);
}
Printf
int printf(const char *restrict format, ...);
Как работает printf:
- Принимает строчку с форматом
- Читает строку, печатает обычные символы, пока не дойдет до символа %
- Смотрит спецификатор формата
"%d" - int
"%s" - char*
"%x" - int в 16ричной системе счисления, "%o" - в 8ричной
Спецификатор размера
h - short
l - long int
short s = 7;
printf("%d %hd", strlen(str), s); // integer promotion of s
Scanf
int scanf(const char *restrict format, ...);
- Тут очень важен спецификатор размера числа
- Возвращает кол-во считанных элементов; если scanf ничего не считал, возвращает eof
Ввод числа
int main(void) {
short shint = 7;
int result = scanf("%hd", &shint);
if (result == EOF) {
printf("empty input\n");
return 1;
}
if (result != 1) {
printf("error\n");
return 1;
}
}
Ввод строки - читает до 1 пробела
int main(void) {
char buf[100];
int result = scanf("%s", buf);
if (result != 1) {
printf("error\n");
return 1;
}
printf("buf : %s\n", buf);
}
Чтобы не начать запись за границами буфера:
int main(void) {
char buf[3] = {0};
int result = scanf("%2s", buf);
if (result != 1) {
printf("error\n");
return 1;
}
printf("buf : %s\n", buf);
}
Символы вне кодировки ASCII - Unicode
В ASCII 127 символов, помещаются в 1 байт. Unicode - кодировка, где каждый символ имеет номер code point.
# python
ord('f') # 102
chr(1100) # ь
В качестве кодировки code points используется теперь в основном utf-8. Она запихивает все символы в байтовую строку. Символы ascii записываются теми же кодами. Вне ascii — распилим число на отдельные участки бит, старшими битами будем кодировать их последовательность
𝄞 ← U+1D11E MUSICAL SYMBOL G CLEF
0x1d11e = 0b11101_000100_011110
UTF-8: 0b11110_000 0b10_011101 0b10_000100 0b10_011110
In [1]: [bin(b) for b in 'q'.encode('utf-8')]
Out[1]: ['0b01110001']
In [2]: [bin(b) for b in 'я'.encode('utf-8')]
Out[2]: ['0b11010001', '0b10001111']
In [3]: [bin(b) for b in 'ツ'.encode('utf-8')]
Out[3]: ['0b11100011', '0b10000011', '0b10000100']
In [4]: [bin(b) for b in '𝄞'.encode('utf-8')]
Out[4]: ['0b11110000', '0b10011101', '0b10000100', '0b10011110']
Overlong encoding — использование большего числа байт, чем необходимо:
0b110_00000 0b10_100001
→ символ chr(0b100001) == '!'
.
Структура программы
Declarations and definitions
Translation unit
Extern, static
Агрегатные типы
Arrays and structures
Aggregate initializers
Array dimensions
Pointer arithmetic
Typedef
Alignment
Другие темы
Hosted vs freestanding
Stdio
Argc/argv
Strings
Restrict
Noreturn etc
VLA
Function pointers