Вход и изход от програма

Input/Output

Четене и писане на информация. C и C++ варианти. Конзола. Файлове.
Пренасочване на входа и изхода. Сравняване на файлове.
Автор: Александър Георгиев

Много важна тема, както в стандартното програмиране, така и в състезателното такова, е как нашата програма да получи данните за обработка и да върне резултата.

Вход и изход

Обикновено четенето на данни от нашата програма се нарича вход (като информацията се нарича входни данни), докато връщането на резултат се нарича изход (като върнатата информация се казва изходни данни).
?Реално, когато вашата програма бива тествана, тя получава входните данни от файлове и печата изходните данни също във файлове, но това се извършва скрито от вас чрез пренасочване на входа и изхода.
В най-честия случай и входът и изходът се четат и пишат в конзолата. Въпреки, че тя изглежда като едно нещо за потребителя, реално четенето и писането се извършват на различни места - в така наречените стандартен вход и стандартен изход. Но тъй като текстът и от двете се показва на потребителя на едно и също място, се създава илюзията, че те са едно. Примери за състезания, в които се ползва конзолата за вход и изход, са българските ученически състезания, IOI, повечето ACM-ски състезания и много други.

По-рядко ще срещнете състезания, в които входът и изходът се задават във файлове. При тях входът се чете от един файл, докато изходът се пише в друг такъв. Примери за такива състезания са по-стари ученически състезания, USACO, някои ACM-ски състезания, Google Code Jam, Facebook HackerCup и други.

Сравнително отскоро (но набиращо популярност) е четенето и писането на входа и изхода изобщо да не се извършва от нас, а директно ни се подава като аргументи на функция. Примери за такива състезания са TopCoder и някои частни конкурси като квалификационния кръг на Code of Duty.

Предимства и недостатъци

Предимства

Когато се налага ние да четем входа, можем да приложим редица трикове, които други (по-неопитни) състезатели не знаят, като така спечелим време за останалата част от задачата. Нещо повече, съществуват цели задачи, в които сложното е да се обработи правилно входът. Примери за такива задачи са Very Short Problem и Language Ocean. Когато сами четете входа, можете да го четете в променливи и структури от какъвто вид желаете, като не се налага да го конвертирате впоследствие. Например ако са ви дадени точки с целочислени координати в равнината, но на вас по-късно ще ви се наложи да ги обработвате като нецелочислени, то можете директно да ги прочетете в double променливи.

Недостатъци

Като цяло това ние да четем входа се счита за лошо нещо. То няма никаква алгоритмична насоченост и не изисква почти никакво мислене. В различни програмни езици е направено по различен начин, което дава предимство на някои езици пред други. Когато входът е голям, самото четене може да отнеме толкова много време, че да е сравнимо със самото изпълнение на алгоритъма ни (понякога даже може да бъде много повече). Така трудно може да се оцени колко ефективен е самият алгоритъм, докато на състезателите се налага да отделят време за оптимизиране на четенето и писането.

Печатането на изхода също може да бъде проблем. Като състезатели сме срещали немалко задачи, в които има разлика с колко знака след десетичната точка печатаме отговора (ако той е реално число). В по-добре направените задачи това е ясно специфицирано, но в други това не е така. В следствие на неточностите на типа double се получават дори по-големи абсурди - например 7 знака и по-надолу дават грешен резултат; 8, 9 и 10 дават верен такъв; а 11 и нагоре - отново грешен.

Файлове и конзола

?Съществува и трети специален файл, който можем да ползваме. Той се нарича "стандартен изход за грешка" и името му за достъп е stderr. В него обикновено пишем информация за изпълнението на програмата ни (грешки, които са възникнали и т.н.) като така лесно можем да разграничим debugging информацията от нормалния изход (резултатните данни).
До края на темата ще покажем как можем да четем и пишем данни в конзолата и във файлове. Много улеснени сме от факта, че конзолата всъщност е файл.
По-точно "стандартния вход", откъдето четем, е специален файл с име stdin, а "стандартният изход", където пишем, е специален файл с име stdout. Така няма нужда да учим отделно как да пишем в конзолата и във файлове - и двете стават по един и същ начин.

C vs C++

В C++ съществуват два различни (основни) начина за четене и писане на данни. Първият от тях е по-архаичен и е наследен от оригиналното C. Той става с викане на функции (може да сте срещали printf() и scanf()) и често бива наричан "форматирано четене" и "форматирано писане", тъй като чрез него се задава формат как точно се очаква да изглеждат четените или писаните от нас данни. Вторият от тях, на който най-вероятно са ви учили, е възникнал при създаване на езика C++. Той става чрез ползване на оператори (може да сте срещали cin и cout) и се нарича "потоково четене" и "потоково писане".

Както в училище, така и в университета, обикновено се преподава потоковият вход и изход, тъй като те са по-нови и по-лесни за ползване.
!Ако ползвате компилатора GCC можете да забързате значително потоковото четене и писане (cin и cout) като ползвате следния ред в програмата си:
std::ios::sync_with_stdio(false);
Повече информация можете да намерите тук.
Тук, обаче, ние няма да направим това. Нещо, което рядко се споменава, е, че понякога потоковото четене и писане са с пъти по-бавни, отколкото форматираните такива. А е крайно нежелателно да загубите точки на задача, само защото четете входа бавно. Също така, в много случаи форматираното писане е далеч по-удобно, отколкото потоковото такова - особено ако трябва да изпечатате изхода форматиран по специфичен начин (с еди колко си знака, с водещи нули, ако се налага, в еди колко си позиции и т.н.).

Нужна библиотека

За да работим с файлове и форматиран вход и изход ни е нужна стандартната библиотека . Името ѝ идва от "C standard input/output", което е доста логично и я прави лесна за запомняне. В нея се намират всички функции, константи и структури, които ще разгледаме по-долу.

Да се отървем от printf() и scanf()

?За тези от вас, които не са срещали функция с "..." за аргумент преди: трите точки означават, че може да има нула или повече други аргументи на тяхно място. Следователно, функцията scanf() приема 1 или повече аргумента.
Както казахме, стандартният вход и изход също са файлове. Тъй като те се ползват много често, освен стандартните функции в C, са добавени и такива, които "скриват" ползването на файловете при работа с тях. Това са именно scanf() и printf(), които може да сте виждали. Техните оригинали са fscanf() и fprintf(), които приемат по един допълнителен аргумент - файл, от който да четат или пишат. Както казахме, файловете за вход и изход са съответно stdin и stdout. Така стигаме до извода, че:
  • scanf(const char*, ...) е еквивалентно на fscanf(stdin, const char*, ...)
  • printf(const char*, ...) е еквивалентно на fprintf(stdout, const char*, ...)
До края на темата ще работим само с fscanf() и fprintf(), но, разбира се, когато пишете програма и четете/пишете в конзолата, ако желаете можете да ползвате съкратените версии.

Работа с файлове

Тъй като от сега нататък ще работим само с файлове, трябва да видим как са представени те в езика C. Той съдържа специална структура FILE, която съдържа различна информация, нужна на програмата ви да борави с файла (кой е файлът, къде се намира, докъде сме стигнали с четенето му и т.н.).

Ние ще ползваме само указатели към файловете (също наричани хендъли, което идва от английското "handle"), като ще ползваме функция, която да попълва тези данни вместо нас. По-точно, тази функция е:
FILE
*
fopen(
const
char
*
fileName
,
const
char
*
mode)
Първият аргумент е пълното име на файла (заедно с пътя до него). Вторият аргумент е какъв тип е файлът - дали текстов или двоичен - както и за какво го отваряме - четене, писане или и двете.

Пълно име на файла

Първият аргумент (fileName) е пълното име на файла (с разширението му), заедно с пътя до него. Ако искате да четете или да пишете във файл, който е в директорията на изпълнимия ви файл, то това е просто името на файла. Той доста често е и там, където се намира сорсът ви, с изключение на някои по-advanced IDE-та, като Visual Studio и Eclipse. Ако сами компилирате файла или ползвате CodeBlocks/DevC++ то можете да разчитате, че изпълнимият файл ще е при сорса. В същата директория ви предлагаме да държите и файловете с входните и изходните данни, тъй като така четенето е най-лесно.

Ако, например, искаме да отворим файла "EllysIO.in" за четене и "EllysIO.out" за писане, бихме ползвали (за сега игнорирайте втория аргумент):
FILE
*
in
=
fopen(
"EllysIO.in"
,
"rt"
)
;
FILE
*
out
=
fopen(
"EllysIO.out"
,
"wt"
)
;
Ако файловете, които искаме да отворим, не са в директорията, където се изпълнява програмата, то трябва да дадем или пълния или релативния път до тях.
Нека, например, изпълнимият файл се намира в директорията ~/Data/espr1t/myTasks/, a входните файлове са в ~/Data/espr1t/myTasks/Trivial/EllysIO/. Тогава щеше да трябва да ги отворим по някой от следните два начина:
// Вариант 1: с релативен път
FILE
*
in1
=
fopen(
"Trivial/EllysIO/EllysIO.in"
,
"rt"
)
;
FILE
*
out1
=
fopen(
"Trivial/EllysIO/EllysIO.out"
,
"wt"
)
;
// Вариант 2: с пълен път
FILE
*
in2
=
fopen(
"~/Data/espr1t/myTasks/Trivial/EllysIO/EllysIO.in"
,
"rt"
)
;
FILE
*
out2
=
fopen(
"~/Data/espr1t/myTasks/Trivial/EllysIO/EllysIO.out"
,
"wt"
)
;

Начин на четене/писане

Вторият аргумент е начинът, по който отваряме файла. Какво се има предвид под начин?

Най-вече това е дали искаме да отворим файла за да го четем, за да пишем в него, или да правим и двете. Тъй като в състезания много рядко (ако изобщо) ще ви се наложи да правите и двете в един файл, то тук ще разгледаме само двата най-основни и често ползвани мода 'r' (за четене, идва от "read") и 'w' (за писане, идва от "write"). Всъщност това е всичко, което трябва да запомните. Ако искате да отворите файл за четене, то първата буква в mode трябва да е 'r'. Ако искате да го отворите за писане, то тя трябва да е 'w'.

Както виждате по-горе, сме отворили ".in" (input) файла за четене (тъй като той съдържа информацията, която бива подадена на програмата ни), докато ".out" (output) файла - за писане (тъй като той пък е мястото, където ще пишем резултата от изпълнението на програмата ни).

Вторият символ в mode указва какъв тип е файлът. Тъй като, ако не подадем втори символ, типът по подразбиране е точно такъв, какъвто ще ни трябва в 99% от случаите, то много състезатели го пропускат. Все пак мислим, че няма да ви навреди да го знаете.

Съществуват два основни типа файлове - двоични (binary) и текстови (text). Двоичните файлове съдържат някакъв тип информация (примерно изображение, музика, видео, архив и т.н.), докато текстовите файлове съдържат (surprise, surprise) текст. По подразбиране (ако не се подаде втори символ) файлът се отваря за четене на текст. Ако искате сами да го укажете (както направихме ние по-горе), то вторият символ в mode трябва да е 't' (за текст, идва от "text"). В редките случаи, когато ще има по-специална задача, в която трябва да обработите картинка или нещо подобно, то трябва да укажете, че файлът е двоичен, с буквата 'b' (идва от "binary").

Обобщение

Ползвайте "rt" или "wt", когато искате да четете или пишете текстови файлове, и "rb" или "wb", когато искате да четете или пишете двоични файлове.

Затваряне на файл

int fclose(FILE* file)
!Често по време на състезания не е нужно да затваряте файловете, които ползвате, тъй като те се затварят автоматично при излизане от програмата.
След като свършим с работата с даден файл, можем да го затворим използвайки функцията fclose(). Забележете, че аргументът е указателят към файла, а не името му. За да затворим двата файла, които отворихме по-рано, бихме ползвали:
fclose(in)
;
// Затваря EllysIO.in
fclose(out)
;
// Затваря EllysIO.out

Текстови файлове

Вече научихме как да отворим файл. Сега ще разгледаме как можем да четем и пишем от текстови файлове.

Спецификатори

Първо ще изброим поредица спецификатори, като след малко ще разберете къде, как и за какво се ползват те:
  • %c - за char
  • %hd - за short
  • %d - за int
  • %u - за unsigned
  • %f - за float
  • %lf - за double
  • %s - за char*
  • %Ld или %lld - за long long
  • %% - за знака '%'

Форматиран Вход

Има три основни функции, които можем да ползваме за четене: fscanf(), fgetc(), и fgets().

fscanf()

int fscanf(FILE* file, const char* format, ...)
Това е функцията, която най-често ще ползвате, тъй като е най-мощна от изброените.

fscanf() :: първи аргумент

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

fscanf() :: втори аргумент

Вторият аргумент format указва какво точно искате да прочетете - какви променливи и какви други символи има между тях. Най-лесният вариант е ако просто искате да прочетете число или стринг или който и да е друг от стандартните типове. За целта се ползват изброените по-горе спецификатори. Например, ако искате да прочетете едно единствено int число, то format ще бъде "%d". Ако искате да прочетете стринг, то форматът би бил "%s". Какъв би бил форматът, ако искате да прочетете double?

Това, обаче, е далеч от пълния потенциал на форматираното четене. Спокойно можете да четете повече от една променлива наведнъж, като няма проблем те да са от различни типове. Например, ако на входа ви е дадено име на ученик (char*), години (int) и среден успех (double), разделени с по един или повече интервала, то форматът, който ви върши работа, е: "%s %d %lf" или "%s%d%lf".

Това, което прави форматът, е да укаже какви променливи се очакват на входа, а също така и как са разделени те. fscanf() игнорира whitespace (шпации, табулации, нови редове), в следствие на което няма значение дали ползваме "%s %d %lf" или "%s%d%lf". Нещо повече, обаче, можем да задаваме дори по-специфичен формат (различни от whitespace символи между променливите). Например, нека ни е казано, че входът ще съдържа точки с реални координати в тримерното пространство, зададени като (X, Y, Z). Можем спокойно да ги четем с "(%lf, %lf, %lf)". Тук "%lf" ни указва, че четем double числа, докато скобите и запетаите са просто част от формата и fscanf() ги очаква.
?В състезателни задачи рядко ще ви се налага да ползвате по-сложно форматиране от чисти променливи (тоест, примерно, "%s %d %lf").
Така можем да четем по-общ текст. Ако информацията за учениците (име, години и среден успех) беше зададена с цели изречения от типа "My name is Alexander, I'm 25 years old and my GPA is 5.92", форматът би бил: "My name is %s, I'm %d years old and my GPA is %lf.".

fscanf() :: останали аргументи

Всички аргументи от трети нататък (ако има такива), са указатели към променливите, в които искаме да прочетем входа. За горния пример с името, годините и средния успех ще ползваме три променливи: char name[32], int age и double gpa.
char
name[
32
]
;
int
age
;
double
gpa
;
fscanf(
stdin
,
"%s %d %lf"
,
name
,
&
age
,
&
gpa)
;
Забележете, че аргументите са указатели към променливите, а не самите променливи! Това е много важно и много често в началото води до грешки. След като веднъж свикнете с това, обаче, повече няма да имате проблеми.
!Интересен факт е, че можете да игнорирате този частен случай. Поради специфичност в езика при работа с масиви, дори да сложите амперсанд пред името на масив, то върнатият адрес ще е отново същия и кодът ви пак ще работи.
За да вземем указател (адреса) на променлива, ползваме амперсанд ('&') пред името й. Защо нямаме амперсанд пред name тогава? Защото той е стринг, тоест реално е от тип char*, което вече е указател, и не се налага да го конвертираме.

Аналогично можем да четем входа и в елементите на масив(и). Например, ако имаме даден на първия ред брой точки N (по-малък или равен на 100) и N реда с по три цели числа, разделени с интервали - координатите на всяка от точките - то можем да ги прочетем със следния код:
int
n
;
double
points[
100
][
3
]
;
fscanf(
stdin
,
"%d"
,
&
n)
;
for
(
int
i
=
0
;
i
<
n
;
i
+
+
) fscanf(in
,
"%lf %lf %lf"
,
&
points[i][
0
]
,
&
points[i][
1
]
,
&
points[i][
2
])
;
!Забележете, че обратното - да четем нецелочислени числа в целочислени променливи не би работело!
Тук, за удобство, четем целочислени координати в нецелочислени променливи. Това не е проблем, стига спецификаторите да отговарят на типа на променливите (в случая "%lf" за double).

Трудностите при форматираното четене идват от това, че:
  • Трябва да запомните спецификаторите за различните типове
  • Трябва да запомните да подавате указатели към променливите, а не самите променливи
  • Трябва да внимавате при подаването на допълнителни аргументи - да са толкова на брой, колкото са спецификаторите във format, а също и да отговарят на тях по тип

fscanf() :: върнат резултат

Функцията връща броя правилно прочетени неща. Така лесно можем да следим дали четем правилно входа, както и да четем елементи, чиито брой не знаем предварително. Например, ако ни е казано, че на входа ще има не повече от 100 точки с целочислени координати в 3D пространството, но не е казано колко на брой ще са те, можем да ги прочетем със следния програмен фрагмент:
Point points[
128
]
;
int
n
=
0
;
while
(fscanf(in
,
"%d %d %d"
,
&
points[n].x
,
&
points[n].y
,
&
points[n].z)
=
=
3
) n
+
+
;

fgetc

int fgetc(FILE* file)
Много, много по-лесна функция - тя просто чете следващия символ на входа. Връща int стойността на следващия символ (независимо дали той е printable или не, тоест тя не игнорира whitespace). Ако такъв символ няма (тоест сме стигнали до края на файла), вместо това връща специалната стойност - константата EOF (end of file). Това е много удобно ако искате да четете докато стигнете специален символ (например първа следваща цифра, първи следващ пунктуационен знак и т.н.). За да покажем как работи, ще напишем програма, която чете от файл "PrintMe.in" текст и го печата на стандартния изход (подобно на командата cat в Bash).
FILE
*
in
=
fopen(
"PrintMe.in"
,
"rt"
)
;
while
(
true
) {
int
ch
=
fgetc(in)
;
if
(ch
=
=
EOF)
break
;
fprintf(
stdout
,
"%c"
,
(
char
)ch)
;
}

fgets

char* fgets(char* dest, int maxChars, FILE* file)
Ако %c е спецификаторът за символ, а %s е спецификаторът за стринг, като fgetc() чете символ, можете ли да се досетите какво върши fgets()?

Мислите си, че чете стринг? Ама се лъжете.

Реално чете входа, докато прочете едно от:
  • Край на ред
  • Край на файл
  • Определен брой символи
Тази функция е удобна ако, например, в задачата се иска да обработите всеки ред по отделно, но не сте много сигурни какво точно има на всеки ред.

Прочетената информация се записва в dest, maxChars указва колко най-много символа да бъдат прочетени, а file e, очевидно, файла, от който се чете.

Главната причина за съществуването на maxChars е да пишете по-сигурен код. Например ако dest ви е масив с 1024 елемента, логично е maxChars да е 1024 (или по-малко, за да имате място за терминираща нула примерно). В противен случай, ако редът е по-дълъг от 1024 символа, следващите ще излязат извън масива dest и с голяма вероятност нещо ще се омаже.

Примерна задача би била да преброим празните редове в даден файл.
FILE
*
in
=
fopen(
"EmptyLines.in"
,
"rt"
)
;
int
ans
=
0
;
while
(
true
) {
char
buff[
1024
]
;
if
(fgets(buff
,
1023
,
in)
=
=
NULL
)
break
;
// Забележете, че знакът за нов ред *се* прочита и пази в резултата.
// Следователно празен ред може да изглежда по два начина:
// -- ред без абсолютно нищо (последният ред на файла може да бъде такъв)
// -- ред само със знак за нов ред (по средата на файла)
if
(buff[
0
]
=
=
'\0'
) ans
+
+
;
else
if
(buff[
0
]
=
=
'\n'
&
&
buff[
1
]
=
=
'\0'
) ans
+
+
;
} fprintf(
stdout
,
"Number of empty lines: %d\n"
,
ans)
;
Даденото решение не работи за особено специфични случаи. Сещате ли се какви?

Форматиран Изход

След като знаем как можем да четем от файл, нека видим как можем да пишем в такъв. Като цяло всичко е почти същото, само че файлът трябва да е отворен за писане и още някакви дребни разлики.

fprintf()

int fprintf(FILE* file, const char* format, ...)
Забележете, че сигнатурата на функцията е същата като при fscanf(). Тук, обаче, аргументите се изпечатват вместо да се четат, както и тук не трябва да подаваме указатели, ами самите аргументи. Тоест никъде нямаме '&'. Върнатият резултат е колко символа са били изпечатани, но ние ще го игнорираме.

Например за да изпечатаме информация за студентите с формат "Alexander is 25 years old and has average GPA of 5.500000.", то бихме ползвали:
struct
Student {
char
name[
32
]
;
int
age
;
double
gpa
;
}
;
void
printStudentInfo(Student
*
students
,
int
numStudents) {
for
(
int
i
=
0
;
i
<
numStudents
;
i
+
+
) fprintf(
stdout
,
"%s is %d years old and has average GPA of %lf.\n"
,
students[i].name
,
students[i].age
,
students[i].gpa)
;
}

fputc()

int fputc(int character, FILE* file)
Печата символа character във файла file.

fputs()

int fputs(const char* str, FILE* file)
Печата стринга str във файла file.

Модификатори

Както казахме, това е форматиран вход и изход. Може би най-голямата негова сила е това, колко лесно е да печатаме числа по специфични начини.

Водещи шпации

Искаме да изпечатаме таблица от цели числа с N реда и M колони, като всяка колона е с широчина 10 символа. Няма проблем! Ползвайки "%<число>d" указваме, че искаме да изпечатаме int в поне <число> позиции. Например "%5d" би изпечатало 42 като "___42", където '_' ще бъдат шпации.
void
printNumTable(
int
nums[
32
][
32
]
,
int
N
,
int
M) {
for
(
int
i
=
0
;
i
<
N
;
i
+
+
) {
for
(
int
c
=
0
;
c
<
M
;
c
+
+
) fprintf(
stdout
,
"%10d"
,
nums[i][c])
;
fprintf(
stdout
,
"\n"
)
;
} }

Водещи нули

Искаме да изпечатаме число с точно определен брой цифри, като печатаме водещи нули ако се налага? И това е лесно - трябва да ползваме "%0<число>d". Например fprintf(stdout, "%05d", 42); би изпечатало "00042".

Брой цифри след десетичната точка

Много често, когато печатаме числа с плаваща запетая (floating point), искаме да ги печатаме с определена точност. "%lf" по подразбиране ползва 6 цифри след десетичната точка. Ние, обаче, лесно можем да променяме това, използвайки "%.<число>lf". Тук <число> показва колко цифри да има след десетичната точка. Ако числото съдържа различен брой значещи цифри, отколкото броя, с които го печатаме, то автоматично се закръгля към най-близкото число с толкова цифри. Закръглянето (при по-малък брой) се извършва по стандартния начин - ако най-лявата премахната цифра е 5 или нагоре, то се закръгля нагоре, докато ако е 4 и надолу, се закръгля надолу.

Ето няколко примера, от които това ще ви стане ясно:
  • fprintf(stdout, "%lf", 42.1337); печата "42.133700"
  • fprintf(stdout, "%.3lf", 42.1337); печата "42.134"
  • fprintf(stdout, "%.9lf", 42.1337); печата "42.133700000"
  • fprintf(stdout, "%.1lf", 42.1337); печата "42.1"
  • fprintf(stdout, "%.0lf", 42.1337); печата "42"
  • fprintf(stdout, "%.3lf", 1.2345); печата "1.235"
  • fprintf(stdout, "%.3lf", 1.23449999); печата "1.234"
  • fprintf(stdout, "%.3lf", 3.999999); печата "4.000"

Двоични файлове

Четене и писане на двоични файлове скоро едва ли ще ви трябва, затова можете да го пропуснете за сега. Все пак, тук има някои от най-често ползваните функции за референция.

fread()

size_t fread(void* dest, size_t elementSize, size_t elementCount, FILE* file)
Чете elementCount на брой елемента с размер elementSize байта (тоест общо elementCount * elementSize байта) от файла file и ги записва в паметта, сочена от dest.

fwrite()

size_t fwrite(const void* source, size_t elementSize, size_t elementCount, FILE* file)
Записва elementCount на брой елемента с размер elementSize байта (тоест общо elementCount * elementSize байта) във файла file, като блокът данни започва от source.

fseek()

int fseek(FILE* file, long int offset, int origin)
Мести текущата позиция във файла file в позиция origin + offset, като origin е някоя от трите константи SEEK_SET (за начало на файла), SEEK_CUR (за текуща позиция във файла), или SEEK_END (за край на файла). Забележете, че offset може да е отрицателно число, тоест ако искате да отидете 20 байта преди края на файла, можете да ползвате fseek(file, -20, SEEK_END).

ftell()

long int ftell(FILE* file)
Връща текущата позиция във файла. Например ако сте прочели до сега 421337 байта от файла, то ще върне числото 421337.

rewind()

void rewind(FILE* file)
Премества текущата позиция на файла в неговото начало. Аналогично е на това да направим fseek(file, 0, SEEK_SET).

Четене и писане в стрингове

Освен във файлове, можем да четем и печатаме информация и от/в стрингове. Функциите за това са sprintf() и sscanf(), като, логично, първата буква 's' е заменила 'f', тъй като четем от "strings", вместо от "files". Всичко е както при fscanf() и fprintf(), с тази разлика, че първият аргумент е char*, вместо FILE*.

sscanf()

int sscanf(const char* inputString, const char* format, ...)

sprintf()

int sprintf(char* outputString, const char* format, ...)

Четене до край на файла

Понякога ще искате да четете до края на файла. Единият от начините да правите това, е чрез fgetc(), като следите за EOF. Има и друг, по-лесен начин - за да проверите дали сте стигнали до края на файла, можете да ползвате функцията int feof(FILE* file). Тя връща нула, ако все още не е стигнат края на файла, или друго число, ако е.

Изчистване на буферите

Работата с файлове изисква системно прекъсване, което е (що-годе) бавно. Затова компилаторът събира известен брой такива команди, и после ги изпълнява наведнъж, като така печели значително време. Това нещо се нарича буфериране, и е много разпространено при почти всички части на компютрите.

Понякога, обаче, това чакане да се събере повече информация, може да е проблем. При състезателното програмиране това се случва единствено ако решавате интерактивна задача - тоест такава задача, при която "комуникирате" с друга програма - изписвате нещо на конзолата, другата програма го чете, тя изписва нещо, вие го четете и т.н. При такива задачи трябва да карате буферите да се изчистват след всяко печатане, защото в противен случай може да се получи обратния ефект и да загубите време в комуникацията, в следствие на буферирането. За целта е предвидена много проста функция, която изчиства буферите, асоциирани с даден файл.

fflush()

int fflush(FILE* file)Викайте я след всеки fprintf() при комуникацията, за да сте сигурни, че той е бил изпечатан веднага.

Пренасочване между файлове и конзола

Понякога е по-удобно да не въвеждаме входа всеки път, когато искаме да тестваме решението си. Това е неудобно и отнема време. Вместо това ще въведем входа във файл и ще го четем оттам. Това ще ни спести много време, особено ако входът е голям или пускаме решението си много пъти.

Тези от вас, които програмират на Линукс, могат лесно да пренасочват входа и изхода от файлове в Bash. Под Windows, обаче, нямаме толкова мощна конзола. Това, не е кой знае какъв проблем, тъй като съществуват няколко начина, по които лесно можем да преминаваме от конзола към файлове и обратно в самия сорс на програмата ни.

in = stdin, out = stdout

Първият начин е моят фаворит, като промяната от файлове към конзола или обратно изисква точно два символа (коментиране на един ред). Забележете, че той не е начинът, който ще срещнете най-често. Него сме показали малко по-надолу.
Нужни действия:
  1. Обявяваме два указателя към файлове FILE* in и FILE* out някъде в глобалното пространство (примерно след include-ите и using namespace std).
  2. През цялата програма четем от in и пишем в out.
  3. Съвсем в началото на main() функцията ги караме да сочат към, съответно, stdin и stdout.
  4. Веднага след това отваряме (с fopen()) входния файл в in, и изходния файл в out.
  5. В момента, в който искаме да четем и пишем в конзолата, а не от файлове, коментираме отварянето на файловете
Това, като код, би изглеждало:
#include <cstdio>
using
namespace
std
;
FILE
*
in
;
FILE
*
out
;
int
main(
void
) { in
=
stdin
;
out
=
stdout
;
in
=
fopen(
"EllysIO.in"
,
"rt"
)
;
out
=
fopen(
"EllysIO.out"
,
"wt"
)
;
int
numTests
;
fscanf(in
,
"%d"
,
&
numTests)
;
fprintf(out
,
"Hello, world!\n"
)
;
return
0
;
}
Ако коментираме реда, в който отваряме файловете, то in и out ще сочат към, съответно, stdin и stdout, като така ще четем и пишем в конзолата.

freopen()

FILE* freopen(const char* fileName, const char* mode, FILE* file)Това е другият начин за пренасочване на входа и/или изхода между файл и конзолата. Ако забелязвате, сигнатурата на функцията е почти същата като на fopen(), с тази разлика, че има трети аргумент, който е от тип FILE*. Той указва къде да бъде отворен файла. Почти винаги той ще е stdin или stdout, като така ще "заменим" стандартния вход или стандартния изход с файла с име fileName. Горният пример с редиректването на файловете би изглеждал по следния начин:
#include <cstdio>
using
namespace
std
;
int
main(
void
) { freopen(
"EllysIO.in"
,
"rt"
,
stdin
)
;
freopen(
"EllysIO.out"
,
"wt"
,
stdout
)
;
int
numTests
;
fscanf(
stdin
,
"%d"
,
&
numTests)
;
fprintf(
stdout
,
"Hello, world!\n"
)
;
return
0
;
}

diff/kdiff

Този параграф е само частично свързан с четене и писане, но по наше мнение е адски полезен като цяло. Става дума за tool, с който можете да сравнявате файлове. Тези от вас, които са на Linux, са (отново) леко привилегировани, тъй като могат да го ползват наготово - той е вграден в линукската конзола и е познат като diff. За хората, които са на Windows, има програма, която е подобна (даже отчасти по-удобна, тъй като има графичен интерфейс): kdiff3.

Какво прави тази програма? Давате ѝ два файла и тя ги сравнява, като ви показва разликите (ако има такива). Много полезно, когато сравнявате изходите от вашето и друго решение. В следствие на това е полезно за състезания от типа на Google Code Jam, където пращате изходен файл, а не решение. Аз лично го ползвам когато пиша различни решения за малкия и големия вход - след като знам, че отговорът за малкия вход ми е верен, то изпълнявам решението ми за големия вход върху малкия такъв и сравнявам двата резултата (току-що получения и този, за който знам, че е верен). Ако има разлики, то най-вероятно умното ми решение е грешно (освен ако задачата не е с чекер).

Освен за GCJ, това ще ви е доста полезно когато решавате задачи, които не са качени в online judge (като повечето, които даваме тук). Те (в най-добрия случай) имат .sol файлове, които показват верните резултати. Един от начините да проверите дали решението ви е вярно, е като изпечатате вашите отговори в (примерно) .out файлове и за всеки тест сравните (ползвайки тази програма) вашия изход с изхода на журито.

Задача

Две тривиални задачки, с които можете да тренирате четене и писане от файлове, са A * B Problem и InputOutput. Пробвайте да ги напишете, като се водите по тази лекция. За първата ще ви трябва да изпечатате отговора с достатъчно голяма точност (поне 9 знака след десетичната точка), докато за втората ще трябва да чете и пишете стрингове и числа.

Допълнителни материали

  1. Документация на (cplusplus.com)
  2. Как да направим cin/cout по-бърз?


За да предложите корекция, селектирайте думата или текста, който искате да бъде променен,
натиснете Enter и изпратете Вашето предложение.
Страницата е посетена 6130 пъти.

Предложете корекция

Selected text (if you see this, there is something wrong)

(Незадължително) E-mail за обратна връзка: