При определении переменной её тип может дополняться так называемыми квалификаторами (qualifiers).
Язык Си определяет четыре квалификатора const
, volatile
, restrict
и _Atomic
.
Квалификаторы volatile
, restrict
и _Atomic
будут рассмотрены позднее.
Квалификаторы могут стоять на любом месте в спецификации типа: до имени типа, после имени типа, и даже в середине имён типов,
состоящих из нескольких ключевых слов, например:
const unsigned long
unsigned long const
unsigned const long
— все правильные спецификации типа и описывают один и тот же тип. Однако, предпочтительнее использовать const unsigned long
.
Квалификатор const
означает, что определяемый объект не может быть модифицирован, потому что например, находится в ПЗУ
или в памяти, запись в которую запрещена операционной системой.
const int nproc = 30;
определяет переменную nproc типа int, которая не может быть модифицирована в данной
единице компиляции. Переменная, описанная с квалификатором const
, не может быть использована в константных выражениях,
которые, в частности, задают количество элементов массива. Следующий фрагмент не является правильным в языке Си:
const int N = 10;
double arr[N];
Правильный, хотя и не очень эстетичный способ определения констант периода компиляции в Си — ключевое слово enum
:
enum { N = 10 };
Если квалификатор const
используется для определения переменной-указателя,
его семантика меняется в зависимости от того, где он расположен: до символа * или после него.
Определение
char *ptr;
вводит указатель ptr, который и сам может изменяться, и память по адресу, на который указывает данный указатель, также может быть изменена.
char const *ptr;
определяет указатель ptr на неизменяемую область памяти. Сам указатель может изменяться.
char * const ptr;
определяет неизменяемый указатель на изменяемую область памяти.
char const * const ptr;
определяет неизменяемый указатель на неизменяемую область памяти.
Из всех вышеперечисленных комбинаций чаще всего используется const char *
.
Такой тип, например, имеют параметры многих стандартных функций. Например, функция
strcmp
определена следующим образом:
int strcmp(const char *sl, const char *s2);
Ключевое слово const
здесь является частью контракта между функцией и ее окружением, оно показывает всем пользователям
функции strcmp
, что области памяти, на которые указывают s1
и s2
не будут модифицированы. Компилятор может расчитывать
на это для выполнения оптимизаций, а программист может предполагать, что строки s1
и s2
не изменятся.
При передаче параметров и возврате значения не имеет смысла писать const char * const
или const int
, например,
const int process(const int len, const char *const str);
Лишние const
ничего не дают с точки зрения контракта функции, так как параметры при передаче в функцию копируются,
как и возвращаемые значения при выходе из функции. Поэтому правильно писать:
int process(int len, const char *str);
Объявление переменных в Си/Си++ состоит из базового типа, идентификатора объявляемой переменной и разных модификаторов. Например,
int a; // базовый тип -- int
const unsigned long long *ptr; // базовый тип -- const unsigned long long
double *ptr[10]; // базовый тип -- double
На самом деле конструкции, модифицирующие тип, такие как указатели, массивы, функции комбинируются в синтаксической конструкции, называемой «декларатором». Таким образом, полное определение переменной выглядит следующим образом:
<базовый тип> <декларатор> [ = <инициализатор> ];
Декларатор содержит имя определяемого объекта, но в некоторых местах может быть «анонимным», то есть не содержащим имя определяемого объекта. Анонимные деклараторы допускаются в операции приведения типа и при описании формальных параметров в прототипах функций. Пример декларатора:
char (*(*x[3])())[5];
Анонимный декларатор может выглядеть следующим образом:
char (*(*[3])())[5];
Такая (на первый взгляд «странная») форма определения производных типов на самом деле введена по аналогии с выражениями. Декларатор можно рассматривать как некоторое выражение над типом. В таком выражении есть три операции:
OP | приоритет | описание |
---|---|---|
[] |
постфиксная | массив из заданного количества элементов |
() |
постфиксная | функция с заданными параметрами |
* |
префиксная | указатель |
() |
группировка членов в выражении |
Постфиксные операции имеют самый высокий приоритет и читаются слева направо от определяемого имени. Префиксная операция имеет более низкий приоритет и читается справа налево. Скобки могут использоваться для изменения порядка чтения.
Таким образом, декларатор читается, начиная от имени определяемого объекта следуя правилам приоритетов операций. Имя определяемого объекта — это первое имя (идентификатор) после базового типа. Примеры:
decl | описание |
---|---|
int a[3][4]; |
массив из 3 элементов типа массива из 4 элементов типа int (матрица 3 × 4 целых) |
char **b; |
указатель на указатель на char |
char *c[]; |
массив из неопределённого количества элементов типа указатель на тип char |
int *d[10]; |
массив из 10 элементов типа указатель на тип int |
int (*e)[10]; |
указатель на массив из 10 элементов типа int |
int *f(); |
функция, возвращающая указатель на int |
int (*g)(); |
указатель на функцию, возвращающую int |
int *(*g)(); |
указатель на функцию, возвращающую указатель на int |
Чтобы не нагромождать деклараторы и облегчить их чтение, введено специальное ключевое слово typedef
.
Оно записывается перед именем базового типа в декларации, например
typedef int *pint;
В этом случае имя pint
определяется как синоним для типа int *
,
то есть далее в определениях переменных это имя можно использовать наравне с именем базового типа, например
pint a[10], f(), *p;
Конструкция typedef
не вводит новый тип, а задаёт ещё одно имя для типа, которое может
использоваться наравне со старым. Поэтому переменная pint a;
и переменная int *b;
имеют один и тот же тип int *
.
Если есть typedef
-имя и декларация, использующая это имя, то от typedef
-имени
можно избавиться, подставив декларируемое имя вместо typedef
-имени в typedef
-декларацию
и добавив при необходимости скобки для того, чтобы порядок чтения не изменился. Например,
typedef void (*pfunc)(int);
pfunc signal(int, pfunc);
после преобразования получаем
void (*signal(int, void (*)(int)))(int);
Декларатор вида
int (*pfunc)(int a, int b);
читается следующим образом: pfunc
— это указатель на функцию, принимающую два параметра типа int
и возвращающую значение типа int
.
То есть pfunc
— это переменная указатель на функцию. С помощью typedef
переменная может быть объявлена следующим образом:
typedef int (*func_t)(int a, int b); // func_t - тип указателя на функцию
func_t pfunc;
Любая переменная-указатель на функцию может принимать значение NULL
(или 0, или nullptr в Си++).
Если переменная-указатель на функцию не равна NULL, она должна указывать на функцию,
совместимую по передаваемым параметрам и возвращаемому значению.
Операция взятия адреса &
, примененная к имени функции, дает значение типа указателя на функцию
с соответствующим количеством и типом параметров и типом возвращаемого значения, например,
int handler(int value, int mask);
&handler // даст значение типа int (*)(int, int)
Однако, для имен функций автоматически выполняется неявное преобразование имени функции в указатель на функцию. То есть,
pfunc = handler;
это то же самое, что
pfunc = &handler;
Поэтому, как правило, явную операцию взятия адреса перед именами функций не пишут.
Операция разыменования *
, примененная к указателю NULL или к указателю, не указывающему на функцию,
совместимую по передаваемым параметрам и возвращаемому значению, дает неопределенное поведение
(undefined behavior). Как обычно, компилятор имеет право исходить из предположения, что в программе
при ее выполнении никогда не возникает неопределённое поведение, и оптимизировать программу соответственно.
Если указатель на функцию корректен, то операция разыменования дает в результате значение (точнее, function designator), к которому применима операция вызова функции. Например,
int z = (*pfunc)(10, 20); // будет вызвана функция, на которую в данный момент указывает pfunc
Если указатель на функцию используется там, где требуется function designator, он неявно разыменовывается. Таким образом,
явная операция *
для вызова функции по указателю не нужна и часто опускается.
int z = pfunc(10, 20); // то же, что и выше
Поэтому по виду операции вызова функции бывает сложно определить, вызывается ли функция напрямую или через указатель.
Указатели на функции требуются в реализациях обобщенных алгоритмов. Например, рассмотрим функцию сортировки стандартной библиотеки языка Си:
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
Функция qsort
сортирует массив данных, расположенный в памяти по указателю base
и содержащий nmemb
элементов.
Размер одного элемента задается параметром size
. При выполнении сортировки для сравнения элементов массива будет
вызываться функция сортировки, указатель на которую передается в параметре compar
. Этой функции сортировки
будут передаваться указатели на два элемента сортируемого массива, и функция сортировки должна вернуть 0, если элементы равны,
отрицательное значение, если первый элемент массива меньше второго, и положительное значение, если первый элемент массива
больше второго. Функция qsort
может реализовывать нестабильную сортировку, то есть в отсортированном массиве не гарантируется,
что равные элементы сохранят свой относительный порядок.
Предположим, что требуется отсортировать массив целых чисел в порядке неубывания.
void sort_ints(int *data, int count);
Функция принимает параметр data
, указывающий на начало сортируемого массива, и параметр count
, содержащий число сортируемых элементов массива.
Тогда вызов функции qsort
может быть записан следующим образом:
void sort_ints(int *data, int count)
{
qsort(data, // указатель на начало сортируемых данных, к void * преобразуется автоматически
count, // количество элементов в сортируемом массиве
sizeof(data[0]), // размер одного элемента массива
sort_func); // функция сортировки, см. ниже
}
Предположим, что в процессе сортировки потребовалось выполнить сравнение элементов data[i] и data[j], где i и j — некоторые индексы.
Тогда будет вызвана функция сравнения, указатель на которую был передан в параметре sort_func
, и ей в качестве аргументов будут
переданы указатели на сравниваемые значения, то есть &data[i]
и &data[j]
. Функция сравнения может быть написана
следующим образом:
int sort_func(const void *arg1, const void *arg2)
{
int val1 = *(const int*) arg1; // требуется явное преобразование из const void * в const int *
int val2 = *(const int*) arg2;
// помним, что int может переполняться при вычитании!
if (val1 < val2) {
return -1;
} else {
return val1 > val2;
}
}
При написании функции сравнения может возникнуть идея написать нужные типы аргументов в функции sort_func
, а в вызов qsort
добавить
явное преобразование типа:
int sort_func(const int *arg1, const int *arg2)
{
if (*arg1 < *arg2) {
return -1;
} else {
return *arg1 > *arg2;
}
}
void sort_ints(int *data, int count)
{
qsort(data, // указатель на начало сортируемых данных, к void * преобразуется автоматически
count, // количество элементов в сортируемом массиве
sizeof(data[0]), // размер одного элемента массива
(int (*)(const void *, const void *)) sort_func);
}
Это — плохая, нет, очень плохая идея!
В этом случае явное преобразование типа в вызове qsort
скроет возможное рассогласование между фактическими параметрами
функции sort_func
и параметрами, передаваемыми ей функцией qsort
. Не делайте так!
Функция, которая передается в качестве аргумента в другую функцию для того, чтобы быть вызванной позже при наступлении какого-либо условия, называется функцией обратного вызова (callback function). Например, гипотетическая функция установки таймера может требовать функции, которая будет вызвана при истечении указанного интервала времени.
void timer_handler(void)
{
}
// использование:
set_timer(1000, timer_handler); // таймер устанавливается на 1000 миллисекунд, затем вызывается timer_handler
Сложности, возникающие из-за того, что функция timer_handler
может быть вызвана асинхронно, то есть
в непредсказуемый для основной программы момент, мы рассмотрим в дальнейшем, пока же остановимся на передаче дополнительной
информации в функцию обратного вызова.
Функция обратного вызова timer_handler
может требовать для своей работы дополнительной информации, например, о том,
как обрабатывать тайм-аут. Поскольку наш вариант timer_handler
не принимает никаких параметров, дополнительная
информация может быть передана этой функции только в глобальных переменных, что не является хорошим решением.
Использование глобальных переменных делает сложным ее распараллеливание на несколько нитей, затрудняет ее поддержку и модификацию.
Поэтому, как правило, во всех функциях, которые принимают в качестве параметра указатель на функцию обратного вызова,
принимается и дополнительный параметр user
типа void *
. Этот параметр без изменений передается в функцию обратного вызова.
Например,
struct TimerContext
{
// здесь необходимые данные для обработки тайм-аута
};
void timer_handler(void *user)
{
struct TimerContext *cntx = user;
// обрабатываем тайм-аут с данными в cntx
}
// основная программа
struct TimerContext cntx = { /* задаем доп. информацию для обработчика тайм-аута */ };
set_timer(1000, timer_handler, &cntx); // передаем ее в обработчик, когда он будет вызван