Вступ

На семінарі №4 розглянуто:

  • Перенаправлення у Linux
  • Файлове введення/виведення
  • Покажчики та динамічне виділення пам'яті

Перенаправлення

За допомогою перенаправлення у Linux можна змусити програму друкувати у файл, а не на екран консолі, вводити дані з текстового файлу або передати результати роботи однієї програми на вхід іншої програми.

  • >
    Перенаправляє виведення програми у файл замість консолі. Наприклад,

    ./hello > output.txt
    
    • >>
      Перенаправляє виведення програми у файл, доповнюючи цей файл (а не перезаписуючи його).
    • 2>
      Виводить у файл лише повідомлення про помилки.

  • <
    Перенаправляє дані з файлу на вхід програми, наприклад,
    ./hello > input.txt
    			
  • |
    Перенаправляє виведення однієї програми на вхід іншої. Наприклад,
    .\hello | .\program_1
    			

Файлове введення/виведення

Раніше ми зчитували дані тільки з терміналу та писали дані лише у термінал (читали з stdin, записували в stdout). Але дані можна також зчитувати з файлу або записувати у файл. Запис у файл та зчитування з файлу називаються відповідно файловим введенням та файловим виведенням.

Функції для роботи з файлами

Для зчитування або запису у файл треба:

  1. Створити покажчик на файл.

    FILE* file;

  2. Відкрити файл

    Файл можна відкрити за допомогою функціїї fopen. Ця функція повертає покажчик на відкритий файл, або NULL, якщо не вдалось відкрити файл. Треба завжди перевіряти, чи був відкритий файл, перед тим як щось із ним робити.

    file = fopen("file.txt", "r");

    • Перший аргумент: шлях до файлу
    • Другий аргумент - режим
      • "r" - читання з файлу
      • "w" - запис у файл. Створить файл, якщо він ще не існує.
      • "a" - дописування в кінець файлу. Створить файл, якщо він ще не існує.

  3. Зчитати або записати щось у файл:

    1. Функції зчитування з файлу:

      • fgetc - повертає наступний символ
      • fgets - повертає текстовий рядок
      • fread - зчитує певну кількість байтів та записує їх у масив
      • fseek - переходить на визначену позицію у файлі

    2. Функції запису у файл:

      • fputc - записує символ
      • fputs - записує текстовий рядок
      • fprintf - записує відформатований рядок у файл
      • fwrite - записує масив байтів у файл

  4. Закрити файл:
    fclose(file);

Приклади

Приклад 1. Запис у файл

#include <stdio.h>

#define STUDENTS 3

int main(void) { 
  int scores [] = { 96, 90, 83 }; 
  FILE* file = fopen("database", "w"); //Відкриваємо файл
  if (file != NULL) //Якщо файл відкрито
  {  
    for (int i ; i < STUDENTS; i++) 
    { 
      fprintf(file, "%i\n", scores[i]); //Записуємо дані
    }
    fclose(file); //Закриваємо файл
  }  
}

Приклад 2. Читання з файлу

Програма, наведена нижче, приймає як аргумент командного рядка назву файлу, який потрібно зчитати, і виводить на екран вміст цього файлу, або помилку, якщо файл не існує.

#include <stdio.h> 

int main(int argc, char* argv[]) 
{ 
  if (argc < 2) 
  { 
    printf("Usage: cat file [file ...]\n"); 
    return 1; 
  } 
  for (int i = 1; i < argc; i++) 
  { 
    FILE* file = fopen(argyfil, "r"); 
    if(file = NULL) //Перевіряємо чи відкрито файл
    { 
      printf("cat: %s: No such file or directory\n", argv[i]); 
      return 1; //Якщо файл не відкрито, отже він не існує - помилка
    } 
    //Цикл, у якому зчитуємо символи з файлу
    for(int c = fgetc(file); c != EOF; c = fgetc(file)) 
    { 
      putchar(c); //Виведення символу на екран
    } 
    fclose(file); 
  }
  return 0; 
}

Вправа

Доповніть код, щоб програма виводила "Hello, world" у файл:

#include <stdio.h>
int main(void)
{
  //Ваш код
}

Розв'язок

Покажчики

Пам'ять комп'ютера

Мова C дає програмісту можливість прямого доступу до пам'яті комп'ютера. Для використання покажчиків необхідно розуміти, як влаштовано пам'ять.

Нижче схематично зображено, як саме виглядає пам'ять комп'ютера.

Загалом, пам'ять - це величезний масив комірок, розміром 1 байт. У кожного блока пам'яті є своя шістнадцяткова адреса (на рисунку вище префікс показує, що адреса є саме шістнадцятковим числом).

Поняття покажчика

Ми маємо змінні різних типів - int, float тощо. З їх допомогою ми зберігаємо у пам'яті різні дані - цілі, дробові числа. Покажчик - це також певного роду тип даних, але покажчики зберігають адреси в пам'яті.

Варто зазначити, що у 32-розрядній системі, усі адреси в пам'яті мають розмір 4 байти, тому й покажчики теж мають розмір 3 байти.

Основні види роботи з покажчиками

Створення покажчиків

Покажчики створюються майже так само як і інші змінні. Загальний синтаксис оголошення покажчика:

<тип>* <ім'я змінної>

За адресою, на яку вказує покажчик, можна зберігати лише дані того ж типу, що має покажчик.

Приклади:

int* x;
char* y;
float* z;

Розіменування покажчика

Розіменування - це отримання доступу до значення, що зберігається у комірці пам'яті, адреса якої зберігається в покажчику. Простіше кажучи, розіменування дозволяє отримати значення, на яке посилається покажчик. Синтаксис:

*<ім'я покажчика>

Взяття адреси

Взяття адреси - це операція, за допомогою якої можна дізнатися, за якою адресою зберігається змінна у пам'яті. Синтаксис:

&<ім'я покажчика>

Приклади

Приклад 1

У цьому прикладі можна побачити, як працюють операції з покажчиками.

У першому рядку ми створюємо змінну x, із значенням 5. Ця змінна розміщується за адресою 0х04

У другому рядку, ми створюємо покажчик, що посилається на змінну х. Значення покажчика - це адреса змінної у пам'яті.

У третьому рядку ми створюємо змінну copy, в яку записуємо значення, що зберігається за адресою, на яку вказує покажчик ptr.

Приклад 2

У цьому прикладі, ми створюємо покажчик, який вказує на змінну x. Потім, у рядку 3, ми використовуємо розіменування покажчика і таким чином змінюємо значення змінної x.

Вправа

Необхідно заповнити таблицю, вписавши значення змінних у комірки.

Розв'язок

Арифметика покажчиків

Додавання або віднімання числа n до покажчика зсуває адресу, на яку вказує покажчик, на певну кількість байтів. Кількість байтів вираховується за формулою:

n * sizeof(<тип покажчика>)

У нижченаведеному коді ми використовуємо покажчики, щоб дізнатись довжину рядка. У циклі ми створюємо покажчик, і на кожній ітерації циклу зсуваємо покажчик на наступний символ у рядку.

int main(void) 
{ 
  char* str = "happy cat"; 
  int counter = 0; 
  for (char* ptr = str; *ptr != '\0'; ptr++) 
  { 
    counter++; 
  } 
  printf("%d\n", counter); 
}

Покажчики і масиви

Масиви - це неперервні блоки пам'яті. Змінна-масив - це просто покажчик на першу комірку пам'яті, яку займає масив. Тому можна використовувати арифметику покажчиків для роботи з масивами

Наприклад, можна заповнити масив чисел таким чином:

int array[3]
*array = 1; //Записуємо 1 в першу комірку
*(array + 1) = 2; //Записуємо 2 в другу комірку
*(array + 2) = 3; //Записуємо 3 в третю комірку

Динамічна робота з пам'яттю

Відомо, що пам'ять під змінні, які ми оголошуємо, виділяється у стеку. Ця пам'ять автоматично очищається при виході з області видимості змінних, які там зберігаються.

У мові C можна також виділити пам'ять для даних у купі. Дані, записані туди, будуть зберігатись поки ви не вивільните пам'ять вручну.

Виділення пам'яті

Пам'ять виділяють за допомогою функції malloc. Ця функція виділяє певну кількість байтів пам'яті та повертає покажчик на цю область пам'яті.

void* malloc(розмір в байтах);

Приклад:

int* ptr = malloc*(sizeof(int)*10);

У випадку, якщо пам'ять неможливо виділити, malloc повертає NULL. Необхідно завжди робити перевірку на NULL, щоб уникнути помилок роботи з пам'яттю:

int* ptr = malloc(sizeof(int) * 10);	
if (ptr == NULL)	
{	
  printf("Error -- out of memory.\n");
  return 1;	
}

Вивільнення пам'яті

Пам'ять, виділену за допомогою функції malloc, завжди необхідно вивільняти за допомогою функції free, оскільки інакше область пам'яті залишиться недоступною для використання.

void free(покажчик на пам'ять в купі);

Наприклад:

free(ptr);

Приклад динамічної роботи з пам'яттю

#include <stdio.h>
#include <cs50.h> 
int main(void) 
{ 
  //Виділяємо пам'ять під одне ціле число
  int* ptr = malloc(sizeof(int));  
  if (ptr == NULL) //Якщо пам'ять не виділено
  { 
    printf("Error -- out of memory.\n");
    return 1;
  }
  *ptr = GetInt();
  printf("You entered %d.\n", *ptr); 
  //Вивільняємо пам'ять
  free(ptr); 
}