Работа с бинарными файлами на Си

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

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <string.h>
#include <limits.h>

/* Изменение файла в соответствии с заданием */
void edit_file(char *filename);
/* вывод содержимого бинарного файла на экран */
void out_file(char *filename);

int main(int n_arg, char *arg[])
{
    
    system("chcp 65001"); // смена кодировки, если файл в кодировке utf-8
    system("cls");

    char *file_name;
    FILE *fin, *fout;

    //Чтение аргументов программы
    if (n_arg > 1){
        file_name = (char*)malloc(sizeof(arg[1]));
        file_name = arg[1];
    } else {
        file_name = (char*)malloc(sizeof(char)*20);
        //Если аргументы не заданы ввод названия файла пользователем.
        //Тут ограничение на название файла 20 символов. 
        printf("Введите имя файла: ");
        if (!fgets(file_name, 20, stdin)){
            printf("\nИмя файла введено неверно");
            return 1;
        };
        file_name[strlen(file_name)-1] = '\0';
    }
    printf("Исходный файл:\n");
    out_file(file_name);
    edit_file(file_name);
    printf("Измененный файл:\n");
    out_file(file_name);
    printf("\n");
    system("pause");

    return 0;
}

void out_file(char *filename)
{
    // вывод содержимого файла на экран
    int n = 0;
    FILE *fin;
    fin = fopen(filename, "rb");
    if(!fin)
    {
        printf("Невозможно открыть бинарный файл\n");
        system("pause");
        exit(0);
    }

    while(fread(&n, sizeof(n), 1, fin))
    {
        printf("%5d", n); 
    }
    fclose(fin);
}

void edit_file(char *filename)
{
// изменение содержимого файла
    int n = 0, max=INT_MIN, min=INT_MAX;
    FILE *fin;
    fin = fopen(filename, "r+b");
    if(!fin)
    {
        printf("Невозможно открыть бинарный файл\n");
        system("pause");
        exit(0);
    }
    rewind(fin);

    //сдвигаем все элементы на 1
    while(fread(&n, sizeof(n), 1, fin))
    {
        if (n > max) max=n; //проверяем максимум
        if (n < min) min=n; //проверяем минимум
    }

    //записываем максимум
    fwrite(&max, sizeof(n), 1, fin);
    //записываем минимум
    fwrite(&min, sizeof(n), 1, fin);
    fclose(fin); 
    printf("\n\nФайл изменен\n");
} 

Ответы (1 шт):

Автор решения: nick_n_a

Функции работы с файлами не предусматривают "удаление" куска. Файл представлен как "неразрывная лента". В ленту можно писать, можно перемещаться по массиву влево вправо. Можно делать его короче. С файлом нужно работать где-то так же как и с одним обычным массивом типа char array[] без дополнительных удобств, только длинну можно менять.

Так как явно вырезать кусок файла нельзя, то делается "косвенное" вырезание. Для файла применимы следующие операции. Open открытие. Close - закрытие. Seek - перемещение. Read чтение. Write - запись. Trunc - обрезание. Я бы выделил три алгоритма перезаписи файла.

Trunc в разных системах запысывается по-разному. Я использовал _chsize из io.h. Но что бы она работала, пришлось взять файловый дескриптор используя fileno, и текущую позицию файла, используя ftell. Итого получилось _chsize(fileno(f),ftell(f));. В WINAPI можно использовать SetEndOfFile, у unix ф-ция ftruncate cкорее всего.

  1. Простейший алгоритм будет такой: Open, Read, обработка, Seek(0), Write(весь файл), Truncate. Т.е.

    #include <io.h>
    //...
    FILE *fin;
    fin = fopen(filename, "r+b");
    // ....
    fseek(fin,0,0);
    fwrite(новый файл); // Записываем весь буфер с новыми данными
    _chsize(fileno(f),ftell(f)); // truncate установим размер файла 
    fclose(f);
    

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

  1. Что бы не лопатить весь файл, можно например удалить строку 2 из файла. Создаём файл, записываем в него 1 2 3 4 через возврат каретки. Удалить строку номер 2 можно так (код не идеальный, для примера как выкинуть кусок файла с from по to)

    #include <stdio.h>
    #include <io.h>
    
    int main(int n_arg, char *arg[])
    {
      FILE* f = fopen("1.txt","r+b"); // Нет проверки
      char myfile[4096];
      int size = fread(myfile,1,4096,f); // Читаем весь файл
      int from=0, to=0, i;
      // Поиск первого символа 13
      for (i=0;i<size;i++) if (myfile[i]==13) { from = ++i; break; } 
      // Поиск второго символа 13
      for (i=from;i<size;i++) if (myfile[i]==13) { to = ++i; break; }
    
      if ((from !=0) && (to !=0)) {
        fseek(f,from,0); // Оставляем начало файла по from без изменений
        fwrite(myfile + to,size - to, 1, f); // Записывам изменившуюся часть файла
        _chsize(fileno(f),ftell(f));   // Обрезаем старый "хвост"
        }
     fclose(f);
    
  2. Если мы не хотим читать весь файл в память, прийдётся брать буфер, и "жанглировать" файлом, делая операции seek-read-обработка-seek-write. Таким образом можно "перемещать" куски файла с место на место. Что то вроде memcpy...

    bool MoveInFile(FILE * f, long from, long to, int size) {
      char buffer[256]; // Или задать нужный размер отличный от 256, или делать динамический буфер
      if (size > sizeof(buffer)) return false; 
      fseek(f,from,0); 
      fread(buffer,1,size); // Прочли кусок в буфер
      fseek(f,to,0);
      fwrite(buffer,1,size); // Записали прочитаный кусок в другую позицию
      return true;
      }
    

Алгоритм сложный - приводить полностью не буду. Тем более что при значительно больших кусках эту ф-цию прийдётся сильно усложнить - разбить один MoveInFile на несколько таких, что бы каждый кусок поместился в буфер. Это делают в крайних случаях.

Но это не всё.

  1. Нужно помнить, что такие операции пригодны при размере файла где-то до 10...100 МБайт, и то при размере выше 1Мб будут проблемы. Если куски мелкие - могут быть проблемы с быстродействием. Так же может возникнуть проблема - где и как получить нужное колличество ОЗУ. В x86 приёдется брать память мелкими кусками. При работе с большими файлами - нужно писать кусками не менее 4096 байт, желательно кусками не меньше чем размер кластера файловой системы, и желательно выравнивать обращения по этим границам. Если раньше 4096 было стандартно, думаю скоро перейдём на 8192 байта. Поэтому при работе с файлами более 1 Мб часто используют классы/библиотеки кеширования.
  2. При работе с файлами свыше 4Гб прийдётся использовать seek64 или что-то подобное.
  3. Желательно учитывать littel-indian/big-indian иначе можно поймать UB.
  4. Иногда желательно делать fflush перед закрытием, или в случае если вы делаете логирование.

P.S. Ваш алгоритм не подходит для работы с файлами соовсем. Потому что нужно заранее знать какой вид будет иметь выходной файл. Теоретически можно вычислить позицию первого и второго удаляемого куска, но, т.к. вы не храние файл в памяти - можно сделать перемещение по алгоритму 3 через MoveInFile. Но это будет сильно сложно для вас, но и к тому же сильно нерационально, т.к. ради одного-двух удалений прийдётся сделать несколько перезаписей файла. Этот алгоритм выгодно использовать только если соовсем не хватает памяти для метода 1,2 и файл более 1Мб (на глаз). Я думаю лучше вам лучше вычитать всё, и сделать по алгоритму 1.

→ Ссылка