Функции setjmp и longjmp и обработка ошибок

Последнее обновление: 09.03.2025

В языке программирования C функции setjmp и longjmp предоставляют механизм для нелокальных переходов управления, что позволяет "прыгать" из одной точки программы в другую, минуя обычный порядок выполнения функций. Эти функции определены в заголовочном файле setjmp.h и часто используются для обработки ошибок или исключительных ситуаций в программах, где традиционные механизмы возврата значений оказываются недостаточными, а также для выхода из глубоко вложенного стека вызовов функций без необходимости выполнять стандартный путь возврата, например, при обработке критических ошибок. Эти функции позволяют сохранить состояние программы в определённой точке (с помощью setjmp) и затем восстановить его позже (с помощью longjmp), обходя нормальный возврат через стек вызовов. Это может быть полезно, например, в системном программировании или при реализации простых систем обработки исключений в C, где отсутствует встроенная поддержка try-catch, как в более высокоуровневых языках.

Эти функции полезны в следующих сценариях:

  • Обработка ошибок: когда функция обнаруживает ошибку, она может вызвать longjmp, чтобы сразу выйти из сложной структуры вызовов и перейти к обработке ошибки, минуя оставшуюся часть выполнения.

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

  • Реализация блокировки/выхода: в случаях, когда требуется выполнить "выход" из многократно вложенных функций, setjmp и longjmp могут использоваться для быстрой отмены операций и возврата в точку, где программа была начата.

setjmp

Функция setjmp Эта функция устанавливает точку возврата. Она сохраняет контекст выполнения программы (состояние стека, регистры и т.д.) в структуре типа jmp_buf. Функция имеет следующий прототип:

int setjmp(jmp_buf env);

Эта функция принимает структуру типа jmp_buf, в которое и сохраняется состояние программы.

Возвращаемое значение этой функции зависит от контекста. Если функция setjmp вызывается впервые, она возвращает 0. Если управление передается обратно в setjmp из longjmp, то возвращается ненулевое значение - значение второго аргумента функции longjmp.

longjmp

Функция longjmp функция выполняет "долгий переход" в точку, заданную ранее с помощью setjmp. При этом восстанавливается сохраненный контекст, и выполнение продолжается с места, где был сделан вызов setjmp. Функция longjmp передает управление обратно в функцию, которая вызывала setjmp. Она имеет следующий прототип:

void longjmp(jmp_buf env, int val);

Эта функция восстанавливает состояние программы, сохранённое в jmp_buf в функции setjmp. В результате выполнения longjmp программа немедленно переходит к месту, где была вызвана setjmp, и с этого места продолжается выполнение программы.

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

Параметр val представляет значение, которое будет возвращено из функции setjmp при восстановлении состояния программы - своего рода статус выполнения. Оно должно быть отличным от нуля, чтобы отличить этот случай от первого вызова setjmp. Если же это значение равно 0, то setjmp возвращает 1.

Пример использования

Рассмотрим простой пример работы setjmp и longjmp:

#include <stdio.h>
#include <setjmp.h>

jmp_buf env;

void testFunction() {
    puts("В функции testFunction перед longjmp.");
    longjmp(env, 1);  // Возвращаемся к точке, где был вызван setjmp
    puts("Эта строка не будет выведена.");
}

int main() {
    if (setjmp(env) == 0) {   // Точка возврата из longjmp
        // Первый вызов setjmp возвращает 0 при нормальном вызове
        puts("Выполняем код до вызова longjmp.");
        testFunction();  // В этой функции будет вызван longjmp
    } else {
        // Второй вызов - longjmp возвращает ненулевое значение
        puts("Возвращение из longjmp");
    }
    return 0;
}

Процесс работы программы:

  1. В функции main вызывается setjmp(env). Это сохраняет текущий контекст (состояние программы) в переменной env. В первый раз функция setjmp вернет 0, и будет выполнен код перед вызовом testFunction().

  2. В testFunction() вызывается longjmp(env, 1), что приводит к возвращению программы в точку, где была вызвана setjmp в функции main. Однако теперь функция setjmp возвращает значение 1, так как это значение передано в вызов longjmp().

  3. После возврата из `longjmp`, выполнение продолжается в блоке, где происходит обработка ненулевого возвращаемого значения, что выводит сообщение "Возвращение из longjmp".

В итоге вывод программы будет таким:

Выполняем код до вызова longjmp.
В функции testFunction перед longjmp
Возвращение из longjmp

Выход из вложенных функций

Другой пример с выходом из многократно вложенных функций:

#include <stdio.h>
#include <setjmp.h>

jmp_buf env; // Переменная для хранения состояния

void second_function() {
    puts("Внутри second_function, вызываем longjmp");
    longjmp(env, 42); // Возвращаемся обратно в место вызова setjmp с кодом 42
    puts("Эта строка никогда не выполнится");
}

void first_function() {
    second_function();
    puts("Эта строка тоже не выполнится");
}

int main() {
    int status = setjmp(env); // Сохраняем точку возврата
    if (status == 0) {
        puts("setjmp вернул 0, вызываем first_function");
        first_function();
    } else {
        printf("Вернулись через longjmp с кодом %d\n", status);
    }
    return 0;
}

В данном случае получаем код возврата в переменную status. ПРи первом вызове setjmp возвращает 0. Соответственно в условной конструкции if выполняется функция first_function(), в которой, в свою очередь, вызывается другая функция - second_function. И в second_function идет обратный переход с помощью вызова longjmp(env, 42) в место вызова setjmp:

int status = setjmp(env);

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

else {
    printf("Вернулись через longjmp с кодом %d\n", status);
}

Консольный вывод:

setjmp вернул 0, вызываем first_function
Внутри second_function, вызываем longjmp
Вернулись через longjmp с кодом 42

Обработка ошибок

Еще один пример - с простейшей обработкой ошибок:

#include <stdio.h>
#include <setjmp.h>

static jmp_buf onError;

static void divide(int a, int b)
{
    // сохраняем контекст в onError
    // после if идут действия, которые могут привести к ошибке
    if (!setjmp(onError))
    {
        if(!b) longjmp(onError, 1); // если что-то не так, сигнализируем об ошибке
        int c = a / b;
        printf("%d / %d = %d\n", a, b, c);
    }
    else    // после else идет обработка ошибки
    {
        puts("Division by zero");
    }
}
int main (void)
{
    divide(8,2);
    divide(8,0);
    divide(6,2);
   
    return 0;
}

Для демонстрации здесь определена функция divide, которая делит два числа. В конструкции if проверяем результат setjmp. После if идут действия, которые могут привести к ошибке:

if (!setjmp(onError))
{
    if(!b) longjmp(onError, 1); // если что-то не так, сигнализируем об ошибке
    int c = a / b;
    printf("%d / %d = %d\n", a, b, c);
}

Если второе число равно 0, то переходим к обработчику ошибок с помощью вызова longjmp(onError, 1). И тогда setjmp повторно выполняется и возвращает 1, в результате чего будет возвращаться блок else:

else    // после else идет обработка ошибки
{
    puts("Division by zero");
}

Консольный вывод:

8 / 2 = 4
Division by zero
6 / 2 = 3

В примере выше нам ничего не мешает просто проверять второе число на ноль без всяких вызовов setjmp/longjmp. Но рассмотрим чуть более сложный пример, где нам надо разделить число на определенный элемент массива:

#include <stdio.h>
#include <setjmp.h>

#define OutOfRangeError 1
#define DivideByZeroError 2

#define COUNT 4  // количество элементов в массиве

jmp_buf onError;

#define throwError(err) longjmp(onError, err)

static int divide(int a, int b)
{
    if(!b) throwError(DivideByZeroError); // если делим на 0, сигнализируем об ошибке
    return a / b;
}
// Функция получает индекс элемента массива
static int takeFormArray(size_t index)
{
    int nums[COUNT] = {10, 20, 30, 40};
    if(index >= COUNT) throwError(OutOfRangeError); // если недопустимый индекс
    return nums[index];  // либо возвращаем число
}
// Функция получает индекс элемента массива и число, на которое надо разделить элемент массива
static void work(size_t index, int b){

    int a, c;
    switch (setjmp(onError)){

        case 0: 
            a = takeFormArray(index); 
            c = divide(a, b);
            printf("%d / %d = %d\n", a, b, c);
            break;
        case DivideByZeroError:
            puts("Division by zero");
            break;
        case OutOfRangeError:
            puts("Invalid index");
            break;
    }
}
int main (void)
{
    
    work(2, 5); // делим 2-й элемент массива на 5
    work(2, 0); // делим 2-й элемент массива на 0
    work(8, 5); // делим 8-й элемент массива на 5

    return 0;
}

Прежде всего здесь вызов longjmp вынесен в макрос throwError:

#define throwError(err) longjmp(onError, err)

Макрос принимает код ошибки и передает в longjmp. Используем два типа ошибок:

#define OutOfRangeError 1     // выход за границы массива
#define DivideByZeroError 2   // деление на ноль

По-прежнему у нас есть функция divide, которая делит два числа:

static int divide(int a, int b)
{
    if(!b) throwError(DivideByZeroError); // если делим на 0, сигнализируем об ошибке
    return a / b;
}

Если происходит деление на ноль, то в макрос throwError передаем код DivideByZeroError. Иначе возвращаем результат деления.

И есть вторая функция - takeFormArray, которая по индексу выбирает элемент массива:

static int takeFormArray(size_t index)
{
    int nums[COUNT] = {10, 20, 30, 40};
    if(index >= COUNT) throwError(OutOfRangeError); // если недопустимый индекс
    return nums[index];  // либо возвращаем число
}

Перед возвращением элемента проверяем, что индекс является допустимым. Если индекс вне границ массива, аналогично вызываем макрос, передавая ему код OutOfRangeError.

Основные действия происходят в функции work:

static void work(size_t index, int b){

    int a, c;
    switch (setjmp(onError)){

        case 0: 
            a = takeFormArray(index); 
            c = divide(a, b);
            printf("%d / %d = %d\n", a, b, c);
            break;
        case DivideByZeroError:
            puts("Division by zero");
            break;
        case OutOfRangeError:
            puts("Invalid index");
            break;
    }
}

Функция получает индекс элемента массива и число, на которое надо разделить элемент массива. Если setjmp возвращает 0, то получаем элемент массива по индексу и делим его на переданное число:

case 0: 
    a = takeFormArray(index); 
    c = divide(a, b);
    printf("%d / %d = %d\n", a, b, c);

Однако в процессе выполнения функций takeFormArray и divide могут произойти ошибки, и тогда setjmp возвратит код ошибки. С помощью дополнительных выражений case мы можем обработать эти ошибки:

case DivideByZeroError:
    puts("Division by zero");
    break;
case OutOfRangeError:
    puts("Invalid index");
    break;

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

В функции main для демонстрации несколько раз вызываем функцию work. В итоге мы получим следующий консольный вывод:

30 / 5 = 6
Division by zero
Invalid index

Ограничения

  • Переносимость: работа jmp_buf зависит от платформы, поэтому его содержимое не следует интерпретировать напрямую.

  • longjmp корректно работает только если точка вызова setjmp всё ещё находится в активном стеке вызовов. Если функция, вызвавшая setjmp, уже завершилась, результат будет неопределённым.

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

  • Проблемы с ресурсами: если ресурсы (например, память или файлы) были выделены в функции, которая завершила выполнение из-за longjmp, может потребоваться дополнительная работа для освобождения этих ресурсов.

  • Переносимость: работа jmp_buf зависит от платформы, поэтому его содержимое не следует интерпретировать напрямую.

Таким образом, функции setjmp и longjmp предоставляют мощные, низкоуровневые инструменты в ANSI C, которые позволяют реализовать нестандартные схемы управления потоком и предоставляют мощную возможность для быстрого перемещения между разными частями программы, минуя обычные механизмы возврата через return, и являются особенно полезными в тех ситуациях, когда требуется быстрый выход из сложных вычислений.

Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850