Skip to content

Instantly share code, notes, and snippets.

@Lerbytech
Last active December 11, 2017 03:02
Show Gist options
  • Save Lerbytech/93cb6e8e2b62481860e0212932a5c0fc to your computer and use it in GitHub Desktop.
Save Lerbytech/93cb6e8e2b62481860e0212932a5c0fc to your computer and use it in GitHub Desktop.
Хромов, программа 5 - Разбиение файлов.
//
// 5
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc, char *argv[])
{
//in_file.txt -n 10 -b base_
// Check
if (argc != 6)
{
printf("ERROR! wrong number of arguments!");
}
int is_key_n = 0;
int is_key_b = 0;
int is_key_s = 0;
for (int i = 0; i < argc; i++)
{
if (strcmp(argv[i], "-n") == 0) is_key_n++;
if (strcmp(argv[i], "-b") == 0) is_key_b++;
if (strcmp(argv[i], "-s") == 0) is_key_s++;
}
if (is_key_n != 0 && is_key_s != 0) {
printf("ERROR! -s and -n are in input"); return 0;
}
if (is_key_n == 0 && is_key_s == 0)
{
printf("ERROR! -s and -n are not found");
return 0;
}
if (is_key_n > 1 || is_key_s > 1)
{
printf("ERROR! too many -s or -n");
return 0;
}
if (is_key_b != 1)
{
printf("ERROR! -b not found or something else");
return 0;
}
//---
//vars
char input_filename[100];
char out_filename[100];
char digit_buffer[12];
char base[50];
FILE *IN_ptrFile; // IN
FILE *OUT_ptrFile; //OUT
//------
strcpy(input_filename, argv[1]);
strcpy(base, argv[ argc - 1 ]);
fopen_s(&IN_ptrFile, input_filename, "rb");
// length
fseek(IN_ptrFile, 0, SEEK_END);
long lSize = ftell(IN_ptrFile);
fseek(IN_ptrFile, 0, SEEK_SET);
//bufsize calculate
long bufSize;
int N;
if (is_key_n)
{
for (int i = 0; i < argc; i++)
if (strcmp(argv[i], "-n") == 0)
{
N = atoi(argv[i + 1]);
bufSize = lSize / N;
break;
}
}
else
{
for (int i = 0; i < argc; i++)
if (strcmp(argv[i], "-s") == 0)
{
bufSize = atoi(argv[i + 1]);
N = lSize / bufSize;
break;
}
}
char *buffer = (char*)malloc(sizeof(char) * bufSize);
// PROCESS
//-------------
for (int i = 0; i < N; i++)
{
strcpy(out_filename, base); // filename1 = base;
sprintf_s(digit_buffer, "%d", i); // 2 -> "2"
strcat(out_filename, digit_buffer);
strcat(out_filename, ext);
fopen_s(&OUT_ptrFile, out_filename, "wb");
fread(buffer, 1, bufSize, IN_ptrFile);
fwrite(buffer, 1, bufSize, OUT_ptrFile);
fclose(OUT_ptrFile);
fseek(IN_ptrFile, bufSize, SEEK_CUR);
}
long finalSize = lSize - bufSize * N;
buffer = (char*)malloc(finalSize);
if (finalSize > 0)
{
strcpy(out_filename, base);
sprintf_s(digit_buffer, "%d", N + 1);
strcat(out_filename, digit_buffer);
strcat(out_filename, ext);
fopen_s(&OUT_ptrFile, out_filename, "wb");
fread(buffer, 1, finalSize, IN_ptrFile);
fwrite(buffer, 1, finalSize, OUT_ptrFile);
fclose(OUT_ptrFile);
}
free(buffer);
fclose(IN_ptrFile);
printf("FINISHED!\n");
return 0;
}
@Lerbytech
Copy link
Author

Lerbytech commented Dec 11, 2017

Содержание

  1. Условие (по Хромову)
  2. Условие (упрощенно)
  3. Запуск программы с параметрами и командная строка
  4. Общая логика работы программы
  5. Работа с файлами
  6. Разбор программы

1. Условие (по Хромову)

которое вы наверняка читали

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

  1. Имя исходного файла
  2. -s 1024, здесь значение 1024 приведено для примера, оно может быть произвольным и задает размер в байтах одного фрагмента, на которые нужно разбить исходный файл.
  3. -n 100 , здесь значение 100 приведено для примера, оно может быть произвольным и задает количество фрагментов одинаковой длины, на которые нужно разбить исходный файл.
  4. -b base, здесь base имя "основы" для имен файлов с фрагментами, то есть эти файлы будут иметь имена base_1.txt, base_2.txt и т. д.

Программа должна проверять корректность данных, введенных пользователем в командной строке, выводить сообщение об ошибке и завершать работу, если данные некорректны.
Параметры -s и -n являются взаимоисключающими, допускается наличие в командной строке только одного из них.

Примеры запуска программы:
split file_ini.txt -s 2048 -b name
Файл file_ini.txt должен быть разбит на части длиной 2048 байтов, которые будут называться name_1.txt, name_2.txt и т. д.

split myfile.dat -n 200 -b base
Файл myfile.dat должен быть разбит на 200 равных частей, которые будут называться base_1.txt, base_2.txt и т. д.

Для успешной работы программы под операционными системами семейства MS WINDOWS, необходимо открывать файлы для чтения и записи в "двоичном режиме", т. е. использовать в качестве второго аргумента функции fopen "rb" или "wb".

2. Условие (упрощенно)

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

Немножко(нет) объяснений: В условии выше упоминаются: командная строка, параметры и общие требования к программе. Разберем подробнее.
Раньше вы запускали программу прямо из CodeBlocks либо Visual Studio. Появлялось окно, куда выводился результат и куда вводилось что-либо если того требовало условие задачи. В данной лабе впервые требуется сделать так, чтобы её можно было вызывать из командной строки. Командная строка - это отдельная программа, которая встроена в операционную систему (на Unix-подобных системах её так же называют терминалом или консолью).
Это может звучать сложно, но вы увидите что заводится подобное очень легко - нужно лишь исправить функцию main и вписать нужную логику в начале программы. Просто запомните: нет никакой разницы вводите ли вы параметры работы программы в командной строке или в самой вашей программе.

Чем отличается командная строка от CodeBlocks (и почему так сложно всё?)
Прежде чем читать дальше, запомните одну вещь: программы могут вызывать другие программы. Когда вы включаете Excel или кликаете на иконку браузера, то программа отвечающая за рабочий стол вызывает ту программу, на которую указывает иконка. Операционную систему можно рассматривать как сложную совокупность программ которая может управлять другими программами. Так, командная строка Windows является очень мощным инструментом, встроенным в операционную систему. Она специально заточена под управление ОС - воспринимает команды вводимые пользователем и выполняет их. Львиную долю манипуляций с компьютером можно выполнять через неё (и у опытных спецов это выходит много быстрее, чем через графический интерфейс).
CodeBlocks же - это IDE, то есть интегрированная среда разработки. IDE можно представить как продвинутый текстовый редактор, совмещенный с полезными инструментами: компилятор, дебаггеры, различные профилировщики, навигацией по коду и тому подобное.
Приведем пример для понимания. CodeBlocks при запуске компиляции вызывает отдельную программу. Однако её можно было бы вызвать и вручную из командной строки:
gcc hromov5.c -o filesplitter
Строчка выше вызывает компилятор GCC и передает ему параметры: hromov5.c - название файла с функцией main и filesplitter - название исполняемого файла (.exe) который получим на выходе. Ключ -o управляет настройками компилятора. В нашей лабе ближайшей аналогией ему будут ключи -n, -s и -b
Для каждого созданного проекта IDE создает папку и складирует в неё исходные файлы с вашим кодом, множество своих служебных файлов и, главное, откомпилированную программу в виде исполняемого файла с расширением .exe.
Когда вы жмете кнопку "скомпилировать" (имеет рисунок шестеренки в codeblocks) IDE берет все текстовые (и иные) файлы вашей программы с исходными кодами и скармливает их компилятору. Компилятор формально является отдельной программой, которая вызывается IDE. Компилятор принимает команды от IDE, отрабатывает и создает в папке проекта исполняемый файл. Затем IDE вызывает этот файл и перед вами появляется окно программы, куда вы можете что-либо ввести. Если вы не будете перекомпилировать ваш проект, то при запуске программы IDE будет вызывать созданный ранее .exe файл. Если же перекомпилируете, то он будет переписан.
Созданный исполняемый файл можно вызывать и вручную. Просто кликните на него вне IDE и готово (так что если захотите однажды написать что-нибудь для себя знайте - не обязательно для запуска пользоваться CodeBlocks и прочим).
Существует альтернативный способ запуска вашей программы - через командную строку. Командная строка принимает имя файла, дополнительные параметры его работы, если таковые нужны, и вызывает .exe файл, передавая ему пользовательский ввод. Ниже приведена схема для лучшего понимания:
image
Таким образом, в данной лабораторной работе от вас просят написать программу так, чтобы ей можно было пользоваться вне CodeBlocks, а именно - через интерфейс командной строки.

3. Запуск программы с параметрами и командная строка

Ознакомимся поближе с командной строкой. Варианты вызова:

  1. Пуск->Выполнить... -> cmd.exe
  2. Нажмите на клавиатуре Win+R. В появившемся окне введите cmd.exe, нажмите ОК.
  3. Найдите файл по адресу: C:\Windows\System32\cmd.exe
    Должно появиться такое окно:
    image

Обратите внимание на подчеркнутый красным текст. Он может отличаться от того, что будет видно на вашем компьютере, но по умолчанию должна быть папка: C:\Users\<название вашей учетной записи>. В силу некоторых причин, командная строка запускается "из определенной папки", то есть она видит файлы только из той папки которая сейчас для неё является активной. Между папками можно переходить (для этого есть команда cd), по умолчанию командная строка запускается из папки пользователя. Эта папка называется "рабочей директорией". Механика ничем не отличается от проводника Windows.
Название рабочей директории дополняется значком >, после которого вы можете вводить команды. Это приглашение пользователя к вводу.
Покажем пример работы в командной строке и чуть лучше ознакомим вас с ней чтобы вы не пугались. Команда dir выводит содержимое текущей директории:
image
Вы можете сопоставить названия файлов выведенных в командной строке с содержимым папки справа и увидеть что всё совпадает. Командная строка может использоваться для навигации по файловой системе компьютера и работе с ней.
Перейдем в папку Anaconda3, для чего введём: cd Anaconda3. Как можете видеть, название рабочей директории изменилось. Снова отобразим содержимое папки и сопоставим с проводником Windows:
image
Через командную строку можно напрямую перейти в любую папку на компьютере. Перейдем сразу в папку нашей лабы:
image

У командной строки есть свой набор встроенных команд. К ним относятся и команды dir и cd из примеров выше. Если первое слово не находится в этом списке, то командная строка ищет в текущей рабочей директории исполняемый файл с совпадающим названием. Если он будет найден, то командная строка вызовет данную программу и передаст ей на вход набор введенных параметров. Если же подходящего файла нет, то будет выдана ошибка command not found, так как перебирать все файлы в компьютере в потугах найти подходящий файл долго, дорого и глупо.
Из этих условий следует, что либо командную строку необходимо вызывать сразу из нужной папки (как именно будет показано позже), либо следует передавать полные пути к исполняемому файлу C:\Users\Admin\Documents\Visual Studio 2015\Projects\Hromov_random\Debug\filesplitter.exe (командная строка сама разберется с адресом файла представленного в таком виде). Для нашей лабы аналогично вам придется передавать полные пути и к остальным файлам. Очевидно что было бы проще не вводить полный путь. Для этого нужно либо через команду cd сменить рабочую директорию, либо сразу запустить командную строку из нужной папки.
Для запуска из нужной папки перейдите в проводнике в нужную директорию, зажмите shift и кликните правой кнопкой мыши по свободному пространству. В выпадающем списке выберите "открыть окно команд"
image

Теперь когда вы в совершенстве освоили командную строку, разберемся как она распознает команды пользователя и как они передаются в вашу программу.
У командной строки есть своя логика работы и взаимодействия с пользователем. Когда вы вводите команду, она разбивается на отдельные слова по пробелам. Общепринято первым словом писать название выполняемой команды, а всё остальное рассматривается как её аргументы. Можете рассматривать командную строку как калькулятор, который принимает массив слов, где первое слово - это название функции F которую нужно посчитать, а остальное - её аргументы х1, х2 и т.д.
Программа на языке Cи должна содержать функцию main. На функцию main распространяются все те же требования, что и для любой другой функции на языке Си. Следовательно, функция main может принимать аргументы, в таком случае перепишем её как:
int main(int argc, char* argv[]).
Такой предикат является общепринятым и отступать от него нежелательно. (но если решите поставить эксперименты обязательно расскажите мне о результатах). Приведенная строка — заголовок главной функции main(), в скобочках объявлены параметры argс и argv. При запуске через командную строку, введенная пользователем команда будет преобразована и передана через эти параметры argc и argv[].
Параметр argc имеет тип данных int, и содержит количество параметров, передаваемых в функцию main. argc всегда не меньше 1, даже когда мы не передаем никакой информации, так как первым параметром считается имя функции. Параметр argv[] это массив указателей на строки так как через командную строку можно передать только данные строкового типа.
По условию лабораторной работы пользователь может ввести такую команду:
filesplitter file_ini.txt -s 2048 -b name
Командная строка получив ввод разобьет его на отдельные слова. Получится массив из 6 слов. Затем она попытается найти встроенную команду совпадающую с первым словом. Когда ожидаемо ничего не выйдет, будет предпринята попытка найти файл исполняемый файл filesplitter.exe в текущей рабочей директории. Если он присутствует, то будет вызвана данная программа и ей на вход переданы значения argc равный 6 и argv в виде массива строк{ 'filesplitter", "file_ini.txt" "-s" "2048" "-b" "name" }.
На этапе передачи параметров вызываемой программе filesplitter роль командной строки будет исполнена. Так как мы работаем в оконном режиме, то filesplitter.exe откроется в отдельном окне и результаты работы (если таковые нужно выводить) будут выведены в нём же. Вся логика обработки команд введенных пользователем прописывается в самой вызываемой программе и командную строку не колышет как это будет реализовано. Её задача сводится лишь к превращению строки в набор argc //argv и поиск и вызов нужной программы.
В завершение рассмотрим понятие ключей. Мы рассмотрели командную строку по аналогии с калькулятором. Общепринято называть слова начинающиеся с знака тире ключами. Ключи рассматриваются как опции программы, от выбора и настроек которых зависит порядок работы программы. Следующее после ключа слово обычно является аргументом ключа. В редких случаях после ключа требуется ввести несколько слов. В таких случаях лучше почитать документацию на программу. В нашей лабе ключ -n означает, что исходный файл следует разбивать по количеству файлов, а аргумент 200 указывает их количество. Ответственность за грамотную реализацию лежит на вас как на авторах программы. Замечу, что хорошая программа всегда проверяет правильность набора ключей и уведомляет об этом пользователя в понятным для него образом, а так же индиффирентна к порядку следования ключей.

4. Общая логика работы программы

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

Процитируем Википедию:

Двоичный (бинарный) файл — в широком смысле: последовательность произвольных байтов. Название связано с тем, что байты состоят из бит, то есть двоичных (англ. binary) цифр. В узком смысле слова двоичные файлы противопоставляются текстовым файлам. При этом с точки зрения технической реализации на уровне аппаратуры, текстовые файлы являются частным случаем двоичных файлов, и, таким образом, в широком значении слова под определение «двоичный файл» подходит любой файл. В целом данный термин представляет собой меру отношения потребителя бинарного файла и самого файла. Если потребитель знает структуру и правила, по которым он способен преобразовать данный файл к более высокоуровневому, то он не является для него бинарным. Например, исполняемые файлы являются бинарными для пользователя компьютера, но при этом не являются таковыми для операционной системы.

Если требуется разбить файл на N файлов, то необходимо вычислить размер каждого файла. Если же требуется разбить файл на множество файлов заданного размера, то полезно, но не обязательно, знать количество конечных файлов. Необходимость работы с бинарными файлами наравне с текстовыми ставит нас перед дилеммой: либо писать отдельные функции для работы с текстовыми и бинарными файлами, либо писать универсальный алгоритм для обоих видов файлов. Вызвано это следующими причинами.
Для представления символов в текстовых файлах используется таблица ASCII. Для чтения текстовых файлов обычно используют методы, работающие с ними как с строками: fgets и fputs или группу fprintf и fscanf. Однако, данные функции не являются удобными по ряду причинам.
Функция fgets воспринимает последовательность символов как строку до тех пор, пока не встретит нуль-символ. По таблице ASCII значение нуль-символа равно 0. Бинарный файл как последовательность байт может содержать сколько угодно нулей, так как его устройство соответствует иным правилам в отличие от текстовых файлов. Так же, символ конца текстового файла может быть в середине потока бинарного файла, так как для него[потока] он не будет иметь смысла символа EOF. Альтернативным вариантом являются fprintf/fscanf. Они предоставляют возможности форматированного чтения файлов, но есть нюанс: если файл разбивается побайтово, то функция приобретает такой вид:
fscanf(input_file, "%с", symbol);
В таком виде технически она ничем не отличается от fgetc, так как та тоже считывает только по одному символу за раз. Выходит, что для переноса информации из одного файла в другой мы вынуждены совершить множество операций копирования, каждая из которых переносит лишь один байт.

Для чтения бинарных файлов используются функции fread и fwrite, способные сразу работать с блоками данных произвольного размера. Данные функции позволяют копировать информацию из файла в файл как побайтово, так поблочно. При этом, размер блока можно указать равным размеру конечного файла: мы за один шаг скопируем всё, что нужно из исходного файла и перенесем это в конечный файл. Если же рассматривать текстовый файл как последовательность байт, то разница между бинарными файлами и текстовыми в рамках нашей задачи размывается.
Следовательно, для решения ранее упомянутой дилеммы мы имеем выбор между написанием отдельных функций для посимвольного разбиения текстовых файлов и поблочного бинарных, либо можем написать универсальный алгоритм если рассматривать текстовые файлы как бинарные. Очевидно, что второе проще.

По условию задачи, пользователь вводит либо количество конечных файлов, либо их размеры. Размер конечного файла очевидным образом совпадает с размером блока который переносится из одного файла в другой. Для количества файлов ситуация несколько отличается. Однако мы можем найти длину исходного файла, поделить её на желаемое количество конечных файлов и найдем размер конечного файла, он же - размер блока. То есть, операция разбиения файла на N файлов является частным случаем разбиения исходного файла на файлы фиксированного размера. Впрочем, если найти кол-во конечных файлов, то мы упростим себе жизнь и при генерации их названий.

Из приведенных выше соображения опишем общие шаги работы программы:

  1. Получить и обработать пользовательский ввод (argc и argv[]), проверить его на корректность.
  2. Рассчитать размер блоков и кол-во конечных файлов в зависимости от пользовательского ввода.
  3. Выполнить разбиение файлов
  4. Обработать остаток (опционально).

5. Работа с файлами

Файл в языке Си рассматривается как неструктурированная последовательность байтов. С этой точки зрения в языке программирования C файлом может быть как собственно файл на жестком диске, так и принтер, дисплей и другие подключаемые устройства ввода-вывода.
Как правило, взаимодействие между приложением и файлами производится посредством обмена блоков байт фиксированной длины (обычно длина представляет степень двойки - 256 или 512 байт). При чтении из файла данные помещаются в буфер операционной системы, а затем побайтно передаются приложению. При записи в файл данные накапливаются в буфере, а при заполнении буфера записываются на диск в виде единого блока байт.
Буферы представляют участки памяти, поэтому передача данных между приложением и буфером происходит довольно быстро в отличие от взаимодействия с физическими устройствами типа принтера. Файл вместе с предоставляемыми средствами буферизации представляет поток.
Язык программирования Си содержит необходимый функционал для работы с файлами и устройствами ввода-вывода. Для применения его применения в программе необходимо подключить заголовочный файл stdio.h.
Чтобы работать с потоком, его необходимо открыть. Для открытия потока применяется функция fopen(), которая имеет следующий прототип:
FILE * fopen(имя_файла, режим_открытия);
Первый параметр представляет имя открываемого файла, а второй задает режим открытия, от которого зависит, как файл может быть обработан.
Функция возвращает указатель на структуру, которая имеет тип FILE, определенный в том же файле stdio.h. Этот указатель идентифицирует поток в программе и через него мы сможем обращаться к открытому файлу.
При открытии поток связывается со структурой
Режимы открытия
Каждый режим задается в виде набора символов. В частности, мы можем использовать следующие режимы:

  • "w": текстовый файл открывается для записи. Если файл ранее существовал, то он пересоздается и записывается заново
  • "r": текстовый файл открывается для чтения
  • "a": текстовый файл открывается для добавления в него новых данных. Если файл существовал ранее, то данные просто добавляются
  • "w+": текстовый файл создается для записи/записи. Если файл ранее существовал, то при первой записи после открытия он пересоздается и записывается заново. А при последующих записях после открытия данные добавляются в него без перезаписи.
  • "r+": текстовый файл открывается для чтения/записи. Запись допустима в любом месте файла, кроме конца файла, то есть недопустимо увеличение размеров файла.
  • "a+": текстовый файл открывается или создается (при его отсутствии) для чтения/записи. В отличие от режима w+ файл при открытии не пересоздается заново, а в отличии от режима r+ можно записывать данные в конец файла
  • "wb": бинарный файл открывается для записи
  • "rb": бинарный файл открывается для чтения
  • "ab": бинарный файл открывается для дозаписи
  • "w+b": бинарный файл создается для записи/чтения
  • "r+b": бинарный файл открывается для чтения/записи
  • "a+b": бинарный файл открывается или создается (при его отсутствии) для чтения/дозаписи

Режимы позволяют разграничить доступ к файлу и открыть его только для чтения или только для записи или совместить оба варианта. Кроме того, на уровне режимов происходит разделение файлов на текстовые и бинарные. И программа будет обрабатывать файлы определенным образом, в зависимости какой режим будет выбран - для текстовых или бинарных файлов. Неправильно заданный режим может привести к некорректной интерпретации файла.

Закрытие файла
После завершения работы с файлом его следует закрыть. Для этого применяется функция fclose():
int fclose(указатель_на_поток);
Единственный параметр функции представляет ранее полученный при открытии файла указатель на структуру FILE, связанный с файлом.
Функция возвращает число: 0 - в случае успешного выполнения и встроенное значение EOF в случае ошибки.
Например, откроем и закроем файл "C:\data.txt":

#include <stdio.h>
 
int main(void)
{
    FILE * fp = fopen("D:\data.txt", "w");
    fclose(fp);
    return 0;
}

В процессе открытия или создания файла мы можем столкнуться с рядом ошибок, например, при открытии в режиме чтения не окажется подобного файла, недостаточно памяти и т.д. И в случае возникновения ошибки функция fopen() возвращает значение NULL. Мы можем обработать возникновение ошибки с помощью проверки результата функции:

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE * fp;
    if((fp= fopen("C:\data.txt", "r"))==NULL)
    {
        printf("Error occured while opening data.txt");
        exit(0);
    }
    fclose(fp);
    return 0;
}

Так как дальнейшие действия в программе в случае ошибки при открытии файла смысла не имеют, то с помощью вызова exit(0) завершаем работу приложения.
И если при попытке открытия файла по указанному пути его не окажется, то консоль выведет следующую ошибку:

Error occured while opening data28.txt: No such file or directory
Чтение файлов
Рассмотрим работу с файлами с помощью fread и fwrite.

fread (char *ptr, size_t size, size_t count, FILE *stream )
Функция fread() считывает count объектов — каждый объект по size символов в длину — из потока, указанного stream, и помещает их в символьный массив, указанный в buf. Указатель позиции в файле продвигается вперед на количество считанных символов.
Если поток открыт для текстовых операций, то возврат каретки и последовательности перевода строки автоматически транслируются в символы новых строк. Но так как мы работаем с бинарными, то это неважно.

size_t fwrite(const void *buf, size_t size, size_t count, FILE *stream)
Аналогично: функция fwrite() записывает count объектов — каждый объект по size символов в длину — в поток, указанный stream, из символьного массива, указанного buf. Указатель позиции в файле продвигается вперед на количество записанных символов.

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

Проблема работы с fread в том, что указатель на позицию потока не перемещается после выполнения операции считывания. Это основное отличие от fscanf / fgetc / fgets. Решается это принудительным применением команды fseek.
Команда fseek перемещает указатель файлового потока на определенную позицию. Функция имеет следующий вид:
fseek (FILE *stream, long offset, int origin)
Функция fseek() устанавливает указатель положения в файле, связанном с потоком stream, в соответ­ствии со значениями offset и origin. Аргумент offset — это выраженный в байтах сдвиг от позиции, определяемой origin, до новой позиции. Для аргументов origin объявлены следующие макросы: SEEK_SET для начала файла, SEEK_CUR для текущей позиции, SEEK_END - для конца файла. Их значения соответствуют 0, 1 и 2.
Для понимания представьте, что значения макросов следующие: SEEK_SET = 0, SEEK_CUR = <текущее положение потока>, SEEK_END = <конец потока, он же конец файла>. Тогда fseek() аналогичен следующему выражению: stream = SEEK_*** + offset, где SEEK_*** - любой из макросов.
Всё вместе собирается в следующий пример - откроем файл, прочитаем в нем 20 байт, отступим 10, снова прочитаем 20 и закроем его:

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE * fp;
    if((fp= fopen("C:\data.txt", "r"))==NULL)
    {
        printf("Error occured while opening data.txt");
        exit(0);
    }
    int bufSize = 20;
    char *buffer = (char*)malloc(sizeof(char) * bufSize);
    fread(buffer, 1, bufSize, fp);
    fseek(fp, 10, SEEK_CUR);
    fread(buffer, 1, bufSize, fp);
    fclose(fp);
    return 0;
}
  1. Разбор программы
    Вспомним общую логику работы программы:
  2. Получить и обработать пользовательский ввод (argc и argv[]), проверить его на корректность.
  3. Рассчитать размер блоков и кол-во конечных файлов в зависимости от пользовательского ввода.
  4. Выполнить разбиение файлов
  5. Обработать остаток (опционально).

Рассмотрим пункт 1. Из задания следует, что параметры -s и -n являются взаимоисключающими, допускается наличие в командной строке только одного из них. Должен присутствовать параметр -b. Программа так же должна быть безразлична к порядку введения ключей. Будем считать, что после каждого ключа обязательно следует значение параметра (в условии это не прописано, но давайте немного упростим себе жизнь). Опишем наши проверки:

  • Введенная строка разбивается на 6 слов, следовательно размерность массива argv равна 6.
if (argc != 6)
{
	printf("ERROR! wrong number of arguments!"); 
        return 0;
}
  • Далее нам необходимо проверить правильно расстановки параметров -n, -s и -b. Каждый ключ может быть не более чем в одном экземпляре, причем ключи -nи -s` являются взаимоисключающими. Объявим переменные счётчики количества повторов каждого параметра и пропишем логику проверки этих значений. Допустимые наборы значений: 0,1,1 или 1,0,1. Так как мы хотим выводить пользователю указание как именно он ошибся, то нам придется написать больше условий.
int is_key_n = 0; // счётчик -n
int is_key_b = 0;// счётчик -b
int is_key_s = 0;// счётчик -s

Пройдемся по всему списку аргументов и найдем кол-во параметров.

for (int i = 0; i < argc; i++)
{
     if (strcmp(argv[i], "-n") == 0) is_key_n++;
     if (strcmp(argv[i], "-b") == 0) is_key_b++;
     if (strcmp(argv[i], "-s") == 0) is_key_s++;
}

Теперь введём проверки. Пропишем их так, чтобы было понятно как именно пользователь ошибся.
Например, можно ввести оба параметра -n и -s

if (is_key_n != 0 && is_key_s != 0) {
      printf("ERROR! -s and -n are in input"); return 0;
}

Или их вообще может не быть

if (is_key_n == 0 && is_key_s == 0)
{
	printf("ERROR! -s and -n are not found");
	return 0;
}

Можно ввести по ошибке дважды

if (is_key_n > 1 || is_key_s > 1)
{
       printf("ERROR! too many -s or -n");
       return 0;
}

Или ошибиться с ключем -b

if (is_key_b != 1)
{
	printf("ERROR! -b not found or something else");
	return 0;
}

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

Для работы нам понадобятся следующие переменные:

char input_filename[100]; // массив с именем входящего файла
char out_filename[100]; // массив с именем исходящего файла
char digit_biffer[12]; // массив для текстового представления порядкового номера исходящего файла
char base[50]; // массив для префикса файла (base_ из примера)
FILE *IN_ptrFile; // входящий файл
FILE *OUT_ptrFile; // исходящий файл

Что хранится в argc и ячейках argv ?
Строка split file_ini.txt -s 2048 -b name из командной строки будет преобразована в массив argv из 6 элементов (argc очевидно равно 6)
Значения argv:

  • argv[0] split
  • argv[1] file_ini.txt
  • argv[2] -s
  • argv[3] 2048
  • argv[4] -b
  • argv[5] name

Получим название входного файла:

strcpy(input_filename, argv[1]);
strcpy(base, argv[ argc - 1 ]);
fopen_s(&IN_ptrFile, input_filename, "rb");

Ранее мы обсуждали квантовой-волновой дуализм схожесть текстовых и бинарных файлов и решили, что будем писать универсальный код. Так же мы рассмотрели опции -n и -s и поняли, что -n является частным случаем -s. Для алгоритма нам нужно найти размер исходного файла. Для этого используем волшебную функцию fseek. Формально, файловый поток показывает текущую позицию в исходном файле. Если мы переместить указатель потока в конец файла и найти расстояние между ним и началом файла, то это расстояние и будет размером. С помощью fseek перенесем указатель в конец файла и найдем расстояние с помощью функции ftell. Вообще, она возвращает текущую позицию указателя в потоке, но для бинарных файлов эта позиция равна количеству байт от начала потока, то есть от начала файла. Мы работаем с бинарными файлами, так что "дайте две!". Единственный нюанс - функция возвращает значения типа long вместо int, но так даже правильнее: размер файла может превышать максимальное значение, что хранится в int. Для этого достаточен файл в ~1.9Гб. Довольно мало, согласитесь) Тип long спасёт нас и отца русской демократии. В конце остается лишь перемотать поток обратно: поставим значение указателя в начало файла.

fseek(IN_ptrFile, 0, SEEK_END);
long lSize = ftell(IN_ptrFile);
fseek(IN_ptrFile, 0, SEEK_SET)

Почему мы ищем размер файла так, а не иначе? Ведь можно было бы воспользоваться методами операционной системы. Метод fseek и ftell это лучшее что есть не потому, что они отлично решают проблему, а потому, что остальное - ещё хуже. Подробнее здесь и здесь

Следующий шаг: найти кол-во конечных файлов и размер каждого конечного файла. Методика подсчета зависит от опции: либо -s либо -n. Следующее слово после ключа является значением параметра. Оно представлено в текстовом виде, придется преобразовать в число с помощью atoi. Так как по условию порядок параметров не важен, то найдем снова параметр с помощью цикла. Ранее мы убедились что все ключи стоят верно, так что искать можно в лоб:

long bufSize;
int N;
if (is_key_n)
{
	for (int i = 0; i < argc; i++)
		if (strcmp(argv[i], "-n") == 0)
		{
			N = atoi(argv[i + 1]);
			bufSize = lSize / N;
			break;
		}
}
else
{
	for (int i = 0; i < argc; i++)
		if (strcmp(argv[i], "-s") == 0)
		{
			bufSize = atoi(argv[i + 1]);
			N = lSize / bufSize;
			break;
		}
}

Обратите внимание что деление - целочисленное.

Для работы fread и fwrite нужен буффер. Мы нашли его размер, объявим переменную и выделим под неё память:
char *buffer = (char*)malloc(sizeof(char) * bufSize);
Наконец-то переходим к основной задаче - разбиению файла.
Процедура следующая: в цикле

  1. составить название выходного файла
  2. открыть его на запись
  3. считать блок из исходного файла
  4. записать блок в исходящий файл
  5. переместить позицию указателя в входящем файле

Определенные проблемы может вызвать составление названия исходящего файла. По идее оно следующее: <префикс> + <номер_файла_в_виде_строки> + ".txt"
В названии файла ничего нет - скопируем туда base. При пересоздании файла мы должны и перезаписать название out_filename, strcpy отлично подходит.
strcpy(out_filename, base);
Для хранения строкового представления числа мы раньше объявили digit_buffer. Функция sprintf - это аналог printf который выводит значение не на экран, а в другую текстовую переменную.
sprintf_s(digit_buffer, "%d", i);
Остается добавить к названию файла это число и расширение:
strcat(out_filename, digit_buffer);
strcat(out_filename, ".txt");

И, наконец-то, сам кусок работы с файлами. Функция fopen открывает файл на запись. Если его не существует, то файл будет создан. Если уже существует файл с таким названием, то он будет перезаписан.
fopen_s(&OUT_ptrFile, out_filename, "wb");
Считаем блок и сразу запишем его в исходящий файл (ну а что с ним ещё делать помимо этого?)
fread(buffer, 1, bufSize, IN_ptrFile);
fwrite(buffer, 1, bufSize, OUT_ptrFile);
Исходящий файл больше не нужен - можно закрыть
fclose(OUT_ptrFile);
Переместим указатель в исходном файле на размер буфера вправо, чтобы считывать на следующей итерации новый кусок.
fseek(IN_ptrFile, bufSize, SEEK_CUR);

Всё! На этом основной блок программы закончен. Нам остается лишь обработать остаток файла если таковой будет. Ранее мы высчитали количество исходящих файлов N. Их суммарный размер меньше либо равен размеру исходного файла.
Найдем разницу.
long finalSize = lSize - bufSize * N;
Эта разница меньше, чем bufSize и, следовательно, меньше чем размера буфера. Нам нужно пересчитать размер buffer:
buffer = (char*)malloc(finalSize);
Теперь мы можем повторить процедуру и скинуть в конечный файл оставшийся кусок:

if (finalSize > 0)
	{
		strcpy(out_filename, base);
		sprintf_s(digit_buffer, "%d", N + 1);
		strcat(out_filename, digit_buffer);
		strcat(out_filename, ext);

		fopen_s(&OUT_ptrFile, out_filename, "wb");
		fread(buffer, 1, finalSize, IN_ptrFile);
		fwrite(buffer, 1, finalSize, OUT_ptrFile);

		fclose(OUT_ptrFile);
	}

В конце остается лишь прибраться за собой: очистим буффер и закроем входящий файл:
free(buffer);
fclose(IN_ptrFile);
Дополнительно можно вывести в консоль сообщение пользователю:
printf("FINISHED!\n");

7. Вызов

Пара замечаний как это всё дело запускать.
Вариантов два:

  1. Найдите папку вашего проекта, откройте её, найдите исполняемый файл, поместите туда ваши входящие файлы (текстовый и бинарный), вызовите командную строку и введите в неё нужные команды.
  2. Пусть среда разработки всё сделает за вас! В Visual Studio зайдите в Проект-> Свойства->Отладка->Аргументы команды и введите ваш набор команд БЕЗ ПЕРВОГО СЛОВА, то есть без названия вашей программы.

Для CodeBlocks проследуйте в Project -> Set programs arguments, далее аналогично.
Теперь при запуске IDE вызываемому исполняемому файлу автоматически будут передаваться пользовательские аргументы, то есть IDE будет имитировать ввод из командной строки. В случае затруднений с CodeBlocks гуглите по запросам "CodeBlocks run with arguments"

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

int main(int argc, char *argv[])
{
      FILE *temp_ptrFile;
      fopen_s(&temp_ptrFile, "TEMPFILE_FINDME.txt", "wb");
      fclose(temp_ptrFile);

Этот код создает файл с названием TEMPFILE_FINDME.txt. Теперь вам нужно найти в папке проекта этот файл и поместить входящие файлы в содержащую временный файл папку. Исходящие файлы будут сохраняться в эту же папку.
Не забудьте удалить временный файл и сам временный код.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment