Как правильно освободить динамически выделенную память, если программа может завершиться досрочно из-за ошибок? - PullRequest
0 голосов
/ 22 мая 2018

Я всегда знал, что хорошо быть щедрым с проверками ошибок в вашем коде и предоставлять сообщение об ошибке в случае выполнения определенных условий.Рассмотрим программу следующей структуры:

int main(const int argc, const char *argv[]) {

// read two integers M and N from input file 1 provided in argv[]
// read two integers K and L from input file 2 provided in argv[]

if (M*N < 1000000) {
    allocate array A
} else {
    printf("With your input values, the matrix will be too large!");
    return 1;
}

if (K*L < 1000000) {
    allocate array B
} else {
    printf("With your input values, the matrix will be too large!");
    return 1;
}

// multiply arrays elementwise
// free memory
return 0;
}

На данный момент не обращайте внимания на то, что этот код может быть реорганизован таким образом, что проверка правильности входных аргументов проверяется до того, как произойдет распределение (что было бы простым решением вэто простой пример).Моя актуальная проблема будет подробно рассмотрена ниже для тех, кто интересуется.

Если в псевдокоде выше A успешно выделен, но B не потому, что K * L превышает предел, операторы освобождения памяти в концепрограммы не достигаются, и утечка памяти связана с A. Каков наилучший способ избежать этой проблемы, если код не может быть реорганизован, как описано выше?Удаление return 1; может вызвать другие ошибки в дальнейшем.Два других варианта, о которых я мог подумать, - это использование пресловутого оператора goto (я не осмелился) или включение всех бесплатных вызовов в условные выражения как;

if (K*L < 1000000) {
    allocate array B
} else {
    printf("With your input values, the matrix will be too large!");
    free(A);
    return 1;
}

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

Мой реальный случай: Я работаю с CUDA, который (для тех, кто с ней не знаком)) позволяет писать код, который будет выполняться на GPU.Весь (или большая часть) кода, связанного с этим API, возвращает флаг ошибки, который рекомендуется проверить перед продолжением программы.В моем случае, в какой-то момент в моей программе я выделяю память на GPU следующим образом (должен быть читаем для тех, кто знаком с C):

double *dev_I;
cudaError_t err;
err = cudaMalloc(&dev_I, N*M, sizeof(*dev_I)); // note: cudaMalloc takes a double pointer
if (err != cudaSuccess) { printf("Error allocating dev_I.\n"); return 1; }

Вот важная часть: следующий шаг -скопируйте память с хоста в место, где GPU может получить к нему доступ.Это выглядит примерно так:

err = cudaMemcpy(dev_I, host_I, M*N * sizeof(*host_I), cudaMemcpyHostToDevice);
if (err != cudaSuccess) { printf("Error copying dev_I"); return 1; }

И это та часть, которая меня беспокоит.Если выделение будет успешным, но копирование памяти не удастся, программа будет (должна) завершиться, и у меня останется утечка памяти.Отсюда мой вопрос, как с этим лучше всего справляться.

Ответы [ 2 ]

0 голосов
/ 22 мая 2018

В дополнение к @ Kerrek SB хороший ответ, другой подход состоит в том, чтобы присвоить / инициализировать все объекты в каком-либо допустимом состоянии, проверить достоверность всех ресурсов, использовать, если все в порядке, затем освободить.

Ключевой идеей является объявление объекта (например, bar *A = NULL), объекту A немедленно присваивается действительное значение - некоторое действительное значение.

int foo(void) {
  bar *A = NULL;
  bar *B = NULL;
  int error = 0;

  if (M*N < 1000000) {
    A = allocate(M, N);
  } else {
    printf("With your input values, the matrix will be too large!");
    error = 1;
  }

  if (K*L < 1000000) {
    B = allocate(K, L);
  } else {
    printf("With your input values, the matrix will be too large!");
    error = 1;
  }

  // Was resource allocation successful?
  if (A && B) {
   // multiply arrays
   mul(A,B);
  }

  // free resources
  free(A);
  free(B);  
  return error;
}

Схема псевдокода

Get resources for A
Get resources for B
Get resources for C
If (OK(A) && OK(B) && OK(C)) {
  Perform the operation(A,B,C)
}
Return resources(C)
Return resources(B)
Return resources(A)

Мне кажется, что этот подход легче проверить.

0 голосов
/ 22 мая 2018

Общее структурированное программирование на C требует goto, как показано ниже:

int f() {
  void *p = alloc_resource();
  if (p == NULL) goto error_1;

  void* q = alloc_resource();
  if (q == NULL) goto error_2;

  return g(p, q);  // g takes ownership

error_2:
  dealloc_resource(p);
error_1:
  return -1;  // some error code
}

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

Обычно можно попытаться избежать чрезмерно сложных функций, оставив каждую функцию маленькой.так что очистка может быть выполнена более специальным способом без goto s, но иногда вам нужно что-то структурированное, как это (например, при составлении нескольких битов владения).

(другие языки предлагаютподдержка нескольких выходов на уровне языка, например, в C ++ эта логика обеспечивается так называемыми «деструкторами». Они в значительной степени влияют на одно и то же только в более компостируемом и повторно используемом виде со всеми переходами, сгенерированными компилятором.)

...