Table of Contents

6 - Dynamická alokace 1/2

* pro vyučující: 06

Cíle cvičení

  1. Dekompozice programu na funkce.
  2. Implementace funkčního a správného programu s ošetřením možných chybových stavů.
  3. Dynamická alokace a načítání vstupu (souboru).
Cvičení na dynamickou alokaci pokračuje 7 - Dynamická alokace 2/2. Hlavním cílém 6. cvičení je seznámit se se základním použitím malloc a realloc ve funkci read(). Rozšíření na načítání více řádků je možné dokončit na 7. cvičení.

Teoretická příprava

Úkoly

Vytvoření permutace

  1. Implementujte tři funkce init, print, permute, např.

void init(unsigned int n, unsigned int a[n]);
void print(unsigned int n, unsigned int a[n]);
void permute(unsigned int n, unsigned int a[n]);

  1. Funkce použijte v např. v jednoúčelovém programu pro inicializaci pole celých čísel, jeho vytištění, permutování a znovu vytištění na stdout.

#include <stdio.h>
#include <stdlib.h>
 
#ifndef MAX_NUM
#define MAX_NUM 10
#endif
 
void init(unsigned int n, unsigned int a[n]);
void print(unsigned int n, unsigned int a[n]);
void permute(unsigned int n, unsigned int a[n]);
 
int main(void)
{
   int ret = EXIT_SUCCESS;
   const unsigned int n = MAX_NUM;
   unsigned int a[MAX_NUM];
 
   init(n, a);
   print(n, a);
   permute(n, a);
   printf("Randomized array\n");
   print(n, a);
   return 0;
}

S výstupem např.

clang -DMAX_NUM=5 perm.c -o perm && ./perm
0
1
2
3
4
Randomized array
3
0
4
2
1

Načtení řádku s dynamickou alokací

void print(char *str)
{
   size_t i = 0;
   while (str && str[i] != '\0') {
      putchar(str[i++]);
   }
   // if (str && str[i] == '\0') putchar('\n'); //not needed if new line character '\n' is a part of the string str.
}

#include <stdlib.h>
#include <stdio.h>
 
#ifndef INIT_SIZE
#define INIT_SIZE 128
#endif
 
void print(char *str);
char* read(void);
 
int main(void)
{
   int ret = EXIT_SUCCESS;
   char *str = NULL;
   unsigned int count = 1;
   while ((str = read())) {
      printf("%3d ", count++);
      print(str);
      free(str);
   }
   return ret; 
}

Načtení řádků do pole ukazatelů na textové řetězce

int print_lines(size_t n, char **str);
char** read_lines(size_t *n);
void free_lines(size_t n, char ***str);
 
int main(void)
{
   int ret = EXIT_SUCCESS;
   size_t n = 0;
   char **lines = read_lines(&n);
   print_lines(n, lines);
   free_lines(n, &lines);
   return ret; 
}
 
 
void print_lines(size_t n, char **str)
{
   if (str) {
      for (size_t i = 0; i < n; ++i) {
	 print(str[i]);
      }
   }
}
 
void free_lines(size_t n, char ***str)
{
   if (str && *str) {
      for (int i = 0; i < n; ++i) {
	 free((*str)[i]);
      }
      free(*str);
   }
   *str = NULL;
}

Výpis řádků v náhodné permutaci

int main(void)
{
   int ret = EXIT_SUCCESS;
   size_t n = 0;
   char **lines = read_lines(&n);
   unsigned int a[n]; // Počet řádku se musí vejít do paměti (zásobníku)
   init(n, a);
   permute(n, a);
   for (int i = 0; i < n; ++i) {
      print(lines[a[i]]);
   }
   free_lines(n, &lines);
   return ret; 
}

Přidání ošestření chybových stavů a reportování chyby návratovou hodnotou

enum {
   ERROR_OK = EXIT_SUCCESS,
   ERROR_IN = 100,
   ERROR_MEM = 101,
   ERROR_OUT = 102,
};

char* read(int *error);
char** read_lines(size_t *n, int *error);

int print(char *str); //return error value
char* read(int *error);

int print_lines(size_t n, char **str)
{
   int ret = ERROR_OK;
   if (str) {
      for (size_t i = 0; i < n && ret == ERROR_OK; ++i) {
         ret = print(str[i]);
      }
   }
   return ret;
}

int print(char *str)
{
   int ret = ERROR_OK;
   size_t i = 0;
   while (str && str[i] != '\0') {
      if (putchar(str[i++]) == EOF) {
         ret = ERROR_OUT;
         break;
      }
   }
   return ret;
}

int print(char *str)
{
   int ret = ERROR_OK;
   size_t i = 0;
   while (str && ret == ERROR_OK && str[i] != '\0') {
      ret = putchar(str[i++]) == EOF) ? ERROR_OUT : ret;
   }
   return ret;
}

Testování vstupu a chybových stavů

limits  -v 2621440 ./rand <in-long.txt; echo $?
ld-elf.so.1: /lib/libc.so.7: mmap of entire address space failed: Cannot allocate memory
1

limits -v 5242880 ./rand <in-long.txt; echo $?
101

V Linuxu spíše použijete příkaz ulimit, který nastavuje aktuální sezení příkazového interpretu, což samo o sobě vyžaduje paměti více. Mimoto je nastavení paměti v kB a simulace malé paměti tak může být náročnější. Nicméně princip ošetření zůstává stále validní, ikdyž zrovna teď máme paměti dost, zásadní je princip, že uvažujeme takovou možnost a jsme na ni připraveni. Alternativně můžeme přijmout fakt, že chyba se může stát, ale když se stane, tak s tím nic neuděláme, což je typicky příklad vypisování zpráv na stderr, případně stdout, kde běžně nekontrolujeme, že se vypsalo vše co mělo. V takovém případě indikaci špatného chování máme, v případě přístupu do nepřidělené paměti může program stále běžet, ale nebude mít definované chování a uživatel o tom nebude informován.

int main(void)
{
  ...
  print_error(ret);
  return ret;
}

V případě jediného místa ukončení programu, return v hlavní funkci main, je to relativně snadné. Samotné rozšíření programu o testování chybových stavů je spíše pracné, než náročné. Zdrojový kód se stane relativně méně přehledný. Nicméně při krátkých funkcí je rozšíření relativně rychle a pokud jste implementovali vlastní řešení vhodně, mělo by se pohybovat mezi 5 až 10 minutami.

limits -v 5242880 ./rand <in-long.txt; echo $?
101

limits -v 12582912 ./rand <in-long.txt; echo $?
...
DEBUG: n: 5921 size: 8192
DEBUG: n: 5922 size: 8192
101

head -n 5922 in-long.txt |wc
  5922   95927  742533

Omezení paměti v Linuxu

Protože v Linuxu můžeme ještě narazit na chování OS a volání malloc, které spíše vrátí přidělenou paměť (z virtuálního adreseního prostoru, který je v případě 64-bit dostatečně veliký) nicméně k fyzickému přidělení paměti dojde až při zápisu do paměti, které v případě nedostatku paměti skončí ukončením programu. Toto tzv. overcommit chování je možné nastavit globálně sysctl vm.overcommit_memory = 2 a následně vm.overcommit_ratio a vm.overcommit_kbytes, což však může mít katastrofální důsledky pro běžící OS.

char** read_lines(size_t *n, int *error)
{
   size_t size = INIT_SIZE;
   char **lines = malloc(size * sizeof(char*)); //Assume INIT_SIZE would always be > 0
   *n = 0;
   *error = ERROR_OK; //assume everything would be fine
   if (lines) {
      char *str;
      while ((str = read(error))) { //read would return NULL and set the error
         if (*n == size) {
	    fprintf(stderr, "DEBUG: realloc from %lu -> %lu\n", size, size * 2);
	    char **t = realloc(lines, sizeof(char*) * size * 2);
...

 ulimit -v 26624 
 ./rand < in-long.txt
DEBUG: realloc from 128 -> 256
DEBUG: realloc from 256 -> 512
DEBUG: realloc from 512 -> 1024
DEBUG: realloc from 1024 -> 2048
DEBUG: realloc from 2048 -> 4096
DEBUG: realloc from 4096 -> 8192
DEBUG: realloc from 8192 -> 16384
DEBUG: realloc from 16384 -> 32768
DEBUG: realloc from 32768 -> 65536
DEBUG: realloc from 65536 -> 131072
zsh: segmentation fault (core dumped)  ./rand < in-long.txt

 ulimit -v 32768     
 ./rand < in-long.txt
DEBUG: realloc from 128 -> 256
DEBUG: realloc from 256 -> 512
DEBUG: realloc from 512 -> 1024
DEBUG: realloc from 1024 -> 2048
DEBUG: realloc from 2048 -> 4096
DEBUG: realloc from 4096 -> 8192
DEBUG: realloc from 8192 -> 16384
DEBUG: realloc from 16384 -> 32768
DEBUG: realloc from 32768 -> 65536
DEBUG: realloc from 65536 -> 131072
DEBUG: realloc from 131072 -> 262144
zsh: segmentation fault (core dumped)  ./rand < in-long.txt

Přidání uvedeného ladícího výstupu je příkladem ladění, které není tak přímočaré realizovat krokováním, neboť nás zajímá poslední hodnota, kdy program selže a krokování je v takovém případě neefektivní.

Ověření chování při chybě vstupu nebo výstupu

Další úkoly