Где живут переменные, как функция возвращает несколько результатов и как передать одну функцию в другую
static-локальными переменными и понимать, чем опасны глобальныеconstqsort и bsearch со своей функцией сравненияОбласть видимости имени — часть текста программы, где к нему можно обратиться. В C их три вида:
| Где объявлено | Где видно | Пример |
|---|---|---|
в блоке { … }, в том числе в теле функции | от объявления до конца блока | локальная переменная, переменная цикла for (int i …) |
| в списке параметров | во всём теле функции | int n в int sum(int n) |
| вне всех функций | от объявления до конца файла | глобальная переменная, функция, typedef, enum |
Имя во внутреннем блоке скрывает такое же имя внешнего: до конца блока x означает внутреннюю переменную, внешняя недоступна, но не меняется (examples/15/scope.c):
int x = 1;
{
int x = 2; /* скрывает внешнюю x до конца блока */
printf("inner x = %d\n", x); /* inner x = 2 */
}
printf("outer x = %d\n", x); /* outer x = 1 */
Компилятор сокрытие не запрещает и по умолчанию о нём молчит (предупреждение — ключ -Wshadow). Это частый источник ошибок «присвоил, а значение не изменилось»: присвоено внутренней переменной. Правило курса — не давать внутренним переменным имена внешних.
Время жизни — когда переменная существует в памяти. Видимость — про текст программы, время жизни — про её выполнение.
static-локальная видна только в своей функции, но живёт всю программу: создаётся и инициализируется один раз до запуска main и хранит значение между вызовами. Без инициализации равна нулю./* Номер вызова: static-локальная создаётся один раз и хранит значение между вызовами */
int next_id(void)
{
static int id = 100; /* инициализация — один раз, до запуска программы */
calls++;
return id++;
}
/* Обычная локальная: новый кадр — новая переменная */
int fresh(void)
{
int n = 100;
calls++;
return n++;
}
next_id 100, fresh 100
next_id 101, fresh 100
next_id 102, fresh 100
calls = 6
Статические и глобальные переменные лежат не в стеке, а в отдельной области памяти программы, размер которой не ограничен стеком. Поэтому массив на миллион элементов из раздела 12 объявляют static bool composite[1000001]; внутри функции или вне функций — в стеке (1 МБ в Windows) он не поместится.
NULL, чтобы ошибка проявилась сразу:
int *make_value(void)
{
int value = 42;
return &value; /* value исчезнет вместе с кадром функции */
}
warning: function returns address of local variable [-Wreturn-local-addr]; проверка в main показывает, что указатель равен NULL. Результат возвращают значением, через параметр-указатель (пункт 5) или в динамической памяти (раздел 17).Глобальную переменную может изменить любая функция файла. Поэтому:
Данные передают параметрами, результат возвращают. Глобальными оставляют константы (const double PI = 3.14159265358979;, enum), большие таблицы, которые программа только читает, и счётчики для отладки, как calls в примере выше. Если значение нужно хранить между вызовами одной функции — static-локальная: её никто, кроме этой функции, не видит.
В C параметры передаются только по значению: функция получает копию аргумента. Изменить переменную вызывающего функция может, только получив её адрес (раздел 10). Отсюда три вида параметров:
| Параметр | Что получает функция | Когда |
|---|---|---|
int n, double x | копию значения | входные числа, символы, небольшие структуры |
int *result | адрес переменной вызывающего | функция должна её изменить: результат, счётчик |
const Student *s, const int a[] | адрес, но менять по нему нельзя | большая структура или массив только для чтения |
Массив передаётся адресом первого элемента, даже если параметр записан как int a[10]: внутри функции a — указатель, и число 10 компилятор игнорирует. Поэтому размер массива передают отдельным параметром, а sizeof параметра-массива — ошибка:
int count(int a[10])
{
return (int)(sizeof a / sizeof a[0]); /* 8 / 4 = 2, а не 10 */
}
warning: ‘sizeof’ on array function parameter ‘a’ will return size of ‘int *’ [-Wsizeof-array-argument]
in main 10, in function 2
const в параметре — обещание функции не менять данные. Компилятор держит обещание: присваивание a[i] = 0 в функции с const int a[] — ошибка сборки. А вызывающий видит по заголовку, что его массив останется целым. У указателя const бывает в двух местах:
| Запись | Нельзя | Можно |
|---|---|---|
const int *p | *p = 5 — менять данные | p++ — двигать указатель |
int *const p | p++ — двигать указатель | *p = 5 |
const int *const p | ни то, ни другое | только читать |
Правило чтения — справа налево от имени: const int *p — «p — указатель на int, который const». В параметрах почти всегда нужен первый вид: функция читает чужие данные через собственную копию указателя.
return возвращает одно значение. Когда результатов несколько, есть три способа (examples/15/results.c):
1. Параметры-указатели — функция пишет результаты по адресам, которые ей дали:
/* Минимум и максимум массива — через указатели; n >= 1 */
void min_max(const int a[], int n, int *min, int *max)
{
*min = *max = a[0];
for (int i = 1; i < n; i++) {
if (a[i] < *min) {
*min = a[i];
}
if (a[i] > *max) {
*max = a[i];
}
}
}
…
int lo, hi;
min_max(a, 5, &lo, &hi); /* min -3, max 12 */
2. Признак успеха — результатом, значение — через указатель. Так устроены scanf и fread: результат говорит, получилось ли, а данные лежат там, куда указали. Значение по указателю меняется только при успехе:
/* Строка -> int: результат — признак успеха, число — через указатель */
bool parse_int(const char *text, int *value)
{
char *end = NULL;
errno = 0;
long v = strtol(text, &end, 10);
if (end == text || *end != '\0' || errno == ERANGE || v < INT_MIN || v > INT_MAX) {
return false;
}
*value = (int)v;
return true;
}
"42" -> 42
"-17" -> -17
"12abc" -> error
"" -> error
"99999999999" -> error
strtol при выходе за диапазон long возвращает предельное значение и ставит errno в ERANGE; поэтому errno обнуляют перед вызовом. long в Windows — 32 бита, в Linux — 64, проверка v > INT_MAX нужна для Linux.
3. Структура — когда результаты связаны по смыслу и их удобно передавать дальше вместе. Структура возвращается целиком, копированием:
typedef struct {
int count;
double mean;
} Stats;
Stats stats(const int a[], int n); /* Stats s = stats(a, 5); — count 5, mean 4.20 */
Код функции тоже лежит в памяти, и у него есть адрес. Имя функции без скобок — этот адрес. Его можно сохранить в переменную, положить в массив и передать в другую функцию, а потом вызвать функцию через указатель. Тип указателя записывает, какие параметры функция принимает и что возвращает; его удобно назвать через typedef:
typedef double (*Operation)(double, double); /* указатель на функцию (double, double) -> double */
double add(double a, double b) { return a + b; } /* так же sub и mul */
/* Применить f к каждому элементу: a[i] = f(a[i], arg) */
void apply(double a[], int n, Operation f, double arg)
{
for (int i = 0; i < n; i++) {
a[i] = f(a[i], arg);
}
}
double a[] = {1, 2, 3};
apply(a, 3, mul, 10); /* имя функции без скобок — её адрес */
apply(a, 3, add, 0.5); /* 10.5 20.5 30.5 */
(*Operation) обязательны: double *Operation(double, double) — это объявление функции, возвращающей double *.apply не знает, какую операцию выполняет: «что делать» передаётся параметром, как данные. Так одна функция обхода служит для любого действия над элементами.if: {{"add", add}, {"sub", sub}, {"mul", mul}} — найти строку и вызвать table[i].f(7, 2). Добавить команду — добавить строку таблицы.Главное применение указателя на функцию — библиотечная сортировка qsort из stdlib.h. Она сортирует массив любого типа: ей передают адрес массива, число элементов, размер элемента и функцию сравнения, которая знает, как сравнить два элемента:
void qsort(void *base, size_t n, size_t size, int (*compare)(const void *, const void *));
Функция сравнения получает адреса двух элементов как const void * — «указатель неизвестно на что» — и возвращает отрицательное число, если первый должен стоять раньше, ноль, если равны, и положительное, если позже. Внутри указатели приводят к настоящему типу:
/* Сравнение int без вычитания: a - b переполняется на больших числах */
int compare_int(const void *a, const void *b)
{
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y);
}
/* По убыванию балла, при равенстве — по имени */
int by_score_desc(const void *a, const void *b)
{
const Student *s = a;
const Student *t = b;
if (s->score != t->score) {
return s->score < t->score ? 1 : -1;
}
return strcmp(s->name, t->name);
}
qsort(numbers, n, sizeof numbers[0], compare_int);
qsort(group, m, sizeof group[0], by_score_desc);
-2000000000 -7 0 13 42 2000000000
Kuznetsova 5.00
Antonov 4.75
Ivanov 4.75
Sidorov 4.50
Petrova 3.80
return x - y; ломается на больших числах. 2 000 000 000 − (−2 000 000 000) не помещается в int: переполнение — неопределённое поведение (раздел 3), на практике получается отрицательное число, и qsort «узнаёт», что 2·10⁹ меньше −2·10⁹. Массив {2000000000, -2000000000, 0} после такой сортировки остаётся 2000000000 -2000000000 0. Выражение (x > y) - (x < y) даёт −1, 0 или 1 без переполнения.qsort обычно реализована быстрой сортировкой или её разновидностью (раздел 16): O(n log n) в среднем. Она неустойчива: равные элементы могут поменяться местами. Нужен определённый порядок равных — сравнивайте по второму ключу, как имя в by_score_desc.bsearch(&key, base, n, size, compare) — двоичный поиск (раздел 8) в массиве, отсортированном той же функцией сравнения. Возвращает адрес найденного элемента или NULL. Ключ — элемент того же типа, у которого заполнено поле сравнения: Student key = {"Sidorov", 0};qsort не определён.static-локальная видна в своей функции, живёт всю программу и инициализируется один разconst в параметре-указателе обещает не менять данные: const int *ptypedefqsort/bsearch — с функцией сравнения (const void *, const void *), без вычитания; qsort неустойчива->. Нужен для: раздела 16 — рекурсия и сортировки за O(n log n), сравнение с qsort; раздела 17 — функции над динамическим массивом и списком (ПЗ №15–17); раздела 18 — static и extern для функций и переменных в разных файлах; раздела 23 — функции сравнения и лямбды в std::sort.