LINUX.ORG.RU

Оператор read в Pascal отказывается читать пробельные символы

 , ,


0

1

Добрый день!

Решаю задачи из книжки А.В. Столярова «Программирование. Введение в профессию. Задачи и этюды». Там, в частности, есть задача 2.42:

«Напишите программу, которая читает из стандартного потока ввода строки, состоящие из слов (слова могут разделяться произвольными группами пробельных символов), и в ответ на каждую прочитанную строку печатает слова из этой строки в обратном порядке; например, в ответ на фразу «Humpty Dumpty sat on a wall» должно быть напечатано «wall a on sat Dumpty Humpty». Вводить ограничния на длины строк, слов и др. нельзя; в частности, недопустимо использовать тип string для хранения читаемых строк и/или отдельных слов. Вся выделенная динамическая память должна быть корректно освобождена сразу после обработки очередной строки.»

Я написал текст программы:

program ReverseStringsOfWords;

type
    ListOfChrPtr = ^ListOfChr;                  {list of chars}
    ListOfChr = record
        ch: char;
        next: ListOfChrPtr;
        end;

    ListOfWrdPtr = ^ListOfWrd;                  {list of words}
    ListOfWrd = record
        wr: ListOfChrPtr;
        next: ListOfWrdPtr;
        end;

procedure AddWord(var first: ListOfWrdPtr; var LOCfirst: ListOfChrPtr);
var
    tmp: ListOfWrdPtr;
begin
    new(tmp);
    tmp^.wr := nil;
    LOCfirst := tmp^.wr;
    tmp^.next := first;
    first := tmp;
end;

procedure AddToWord(
    var first, last: ListOfChrPtr; var LOW: ListOfWrdPtr; c: char
    );
begin
    if first = nil then
        begin
        new(LOW^.wr);
        last := LOW^.wr;
        first := last;
        end
    else
        begin
        new(last^.next);
        last := last^.next;
        end;
    last^.ch := c;
    last^.next := nil;
end;

procedure TypeAWord(first: ListOfChrPtr);
begin
    while first <> nil do
        begin
        write(first^.ch);
        first := first^.next;
        end;
end;

procedure TypeWordsInReverseOrder(first: ListOfWrdPtr);
begin
    while first <> nil do
        begin
        TypeAWord(first^.wr);
        write(' ');
        first := first^.next;
        end;
end;

var
    c: char;
    SpcPressed: boolean;
    LOW: ListOfWrdPtr;
    LOCfirst, LOClast: ListOfChrPtr;
begin
    while not SeekEof do
        begin
        LOW := nil;
        LOCfirst := nil;
        LOClast := nil;
        SpcPressed := true;
        while not SeekEoln do
            begin
            read(c);
            if (SpcPressed = true) and (c <> ' ') and (c <> #9) then
                begin
                AddWord(LOW, LOCfirst);
                AddToWord(LOCfirst, LOClast, LOW, c);
                {just create a new word}
                {start filling in a new word}
                SpcPressed := false;
                end
            else
            if (c = ' ') or (c = #9) then
                begin
                {end of word}
                SpcPressed := true;
                end
            else
            if (SpcPressed = false) and (c <> ' ') and (c <> #9) then
                begin
                AddToWord(LOCfirst, LOClast, LOW, c);
                {continuing typing a word (adding elements to old list)}
                end;
            end;
        TypeWordsInReverseOrder(LOW);
        writeln;
        {cleaning up heap}
        end;
end.

Но по какой-то причине программа «не видит» вводимые пробельные символы. Например, если напечатать «pi po» выводом будет «pipo».

Что я упустил?

write(first^.ch);
first := first^.next;

при печати не вставляется никакой разделитель или не определён формат.

Хотя это не тот фрагмент, а насчёт не видит, там ж есть условие (c <> ' '), так что пробелы никуда и не записываются, если я правильно понял.

grem ★★★★★
()
Последнее исправление: grem (всего исправлений: 1)
Ответ на: комментарий от grem

Прогонял программу через отладчик gdb - программа именно что не видит пробельные символы, попадающие к ней из стандартного потока ввода. Поэтому создаёт не два элемента списка «pi» и «po», а один - «pipo».

KOMMUNIST90
() автор топика
Ответ на: комментарий от grem

Участок кода, призванный обрабатывать случаи, когда повстречался пробел, у меня есть:

            if (c = ' ') or (c = #9) then
                begin
                {end of word}
                SpcPressed := true;
                end

(строка 89)

KOMMUNIST90
() автор топика

Внимательно прочитай описание функции SeekEof https://www.freepascal.org/docs-html/rtl/system/seekeof.html

SeekEof returns True is the file-pointer is at the end of the file. It ignores all whitespace. Calling this function has the effect that the file-position is advanced until the first non-whitespace character or the end-of-file marker is reached.

ignores all whitespace...

Там же и пример приведен

Program Example57;

{ Program to demonstrate the SeekEof function. }
Var C : Char;

begin
  { this will print all characters from standard input except
    Whitespace characters. }
  While Not SeekEof do
    begin
    Read (C);
    Write (C);
    end;
end.

this will print all characters from standard input except Whitespace characters.

SeekEoln тоже игнорирует пробелы

praseodim ★★★★★
()
Последнее исправление: praseodim (всего исправлений: 1)
Ответ на: комментарий от grem

Выше уже ответили, SeekEoLn и SeekEoF удаляют пробелы из потока ввода.
Надо отметить, что это надлежащее поведение (см. Turbo Pascal, где они появились изначально, Delphi).

Однако, в FPC оно когда-то было иным: SeekEoLn и SeekEoF «смотрели вперёд», есть ли за пробельными символами что-то непробельное, но сами пробельные символы из потока ввода не убирали. Вот для такого поведения код выше был бы корректным.

PS.
https://wiki.freepascal.org/User_Changes_3.2.2#SeekEof
SeekEof
Old behaviour: If SeekEof did not find the end of the file, it tried to restore the input to its original state/position.
New behaviour: Data consumed by SeekEof while looking for the end of the file (such as spaces) will remain consumed.
Reason: improved TP/BP/Delphi compatibility
svn: 46946

bormant ★★★★★
()
Последнее исправление: bormant (всего исправлений: 1)

Оффтопик: класть в дерево по одному символу — дичайший оверхэд, никогда так не делайте, если только не в целях обучения работе со списками/деревьями.

bormant ★★★★★
()
Ответ на: комментарий от bormant

Спасибо большое! :D

SeekEoLn и SeekEoF «смотрели вперёд», есть ли за пробельными символами что-то непробельное, но сами пробельные символы из потока ввода не убирали. Вот для такого поведения код выше был бы корректным.

Да, так я и предполагал что это работает. Если честно, смысл «нововведения» от меня ускользает :/

KOMMUNIST90
() автор топика
Ответ на: комментарий от bormant

Понял, спасибо!

Ну это условие задачки

недопустимо использовать тип string для хранения читаемых строк и/или отдельных слов

по-моему не даёт сделать никак иначе.

KOMMUNIST90
() автор топика
Ответ на: комментарий от KOMMUNIST90

Это не нововведение, это исправление ошибки.

Есть EoLn и EoF, возвращающие текущий статус потока ввода/вывода.

Смысл существования SeekEoLn и SeekEoF при имеющихся EoLn и EoF — скипнуть пробельные символы и посмотреть, не кончился ли ввод. Необходимость их добавления довольно проста: для файла с пробельными символами в конце легко прочитать лишний 0.
Файл («_» — пробельные символы, ^Z — конец файла):

_12_^Z
Читаем:
while not EoF do begin[br]  Read(n); Write(n);[br]end;

Первый Read(n) пропустит пробельные символы (поведение для чисел, для символьных данных поведение отличается), прочитает число (12), остановится на пробеле после 2; EoF = False; второй Read(n) пропустит пробельные символы и увидит конец файла, в n возвращается 0. Падать в такой ситуации смешно/нерационально — слишком частая ситуация (да, #10, #13#10 — тоже пробельные символы ;) ).
В той же ситуации после первого Read, SeekEoF и SeekEoLn вернут True, лишнего Read не будет.

Если не нужно игнорировать пробельные символы, EoF и EoLn никто не отнимал ;)

bormant ★★★★★
()
Ответ на: комментарий от bormant

Спасибо за подробное объяснение!

Этот пример упоминался в учебнике А.В. Столярова - но у меня в памяти он отложился в упрощённом виде - «лучше всегда использовать SeekEof и SeekEoln, а то оператор read может работать с ошибками».

Теперь на своём опыте прочувствовал разницу :)

KOMMUNIST90
() автор топика
Ответ на: комментарий от KOMMUNIST90

... но, например, кусочек в несколько килобайт, скажем, array [0..8*1024-1] of Char, тоже не запрещает.
Но согласен, на данном этапе обучения это неоправданное усложнение.

Кстати, по условию ограничивать длину ввода типом String нельзя, но типом String можно читать длинные строки ;)
Строки «короткие» в состоянии {$H-} String=String[255], т.е. ShortString. В состоянии {$H+} этого ограничения нет, но и String уже не совсем тот ;)
Прочитать короткой строкой длинный ввод несложно:

while not EoLn do begin
  Read(s); Write(s);
end; ReadLn;
Тут нужно помнить, что Read(s) не пересечет конца строки, если параметр s строковый (String или PChar, включая array [0..Size-1] of Char).

Правда, для
type
  PNode = ^TNode;
  TNode = record
    data: String[255];
    next: PNode;
  end;
пришлось бы писать слишком длинные примеры входных данных. Правда, в учебных целях, можно было бы ограничить длину строк data меньшим числом.
В любом случае, пришлось бы решать дополнительную задачу — был ли пробельный символ между парой таких буферов ;)
var s: String[5];
Read(s);
из текста на входе «123451234512345» вычитает только первые 5 символов. Следующий — еще 5. Если последний возьмет 4 или меньше, будет место добавить признак в конец строки, а если там будет ровно 5, то тоже не проблема, новый узел с единственным #10 (или ' ') в data...
Ну или еще какое решение на это счет, вариантов всяко больше одного ;)

Так-то все немного сложнее, чем кажется на первый взгляд ;)

bormant ★★★★★
()
Ответ на: комментарий от bormant

Если можно, ещё вопрос по той же задачке.

Последним условием значится: «Вся выделенная динамическая память должна быть корректно освобождена сразу после обработки очередной строки.»

Дописал текст программы, добавил в конце главной части процедуру CleanUpString, а также кусок кода для проверки, всё ли я сделал правильно.

Получилось следующее:

program ReverseStringsOfWords;

type
    ListOfChrPtr = ^ListOfChr;                  {list of chars}
    ListOfChr = record
        ch: char;
        next: ListOfChrPtr;
        end;

    ListOfWrdPtr = ^ListOfWrd;                  {list of words}
    ListOfWrd = record
        wr: ListOfChrPtr;
        next: ListOfWrdPtr;
        end;

procedure AddWord(var first: ListOfWrdPtr; var LOCfirst: ListOfChrPtr);
var
    tmp: ListOfWrdPtr;
begin
    new(tmp);
    tmp^.wr := nil;
    LOCfirst := tmp^.wr;
    tmp^.next := first;
    first := tmp;
end;

procedure AddToWord(
    var first, last: ListOfChrPtr; var LOW: ListOfWrdPtr; c: char
    );
begin
    if first = nil then
        begin
        new(LOW^.wr);
        last := LOW^.wr;
        first := last;
        end
    else
        begin
        new(last^.next);
        last := last^.next;
        end;
    last^.ch := c;
    last^.next := nil;
end;

procedure TypeAWord(first: ListOfChrPtr);
begin
    while first <> nil do
        begin
        write(first^.ch);
        first := first^.next;
        end;
end;

procedure TypeWordsInReverseOrder(first: ListOfWrdPtr);
begin
    while first <> nil do
        begin
        TypeAWord(first^.wr);
        write(' ');
        first := first^.next;
        end;
end;

procedure CleanUpWord(var first: ListOfChrPtr);
var
    tmp: ListOfChrPtr;
begin
    while first <> nil do
        begin
        tmp := first;
        first := first^.next;
        dispose(tmp);
        end;
end;

procedure CleanUpString(var first: ListOfWrdPtr);
var
    tmp: ListOfWrdPtr;
begin
    while first <> nil do
        begin
        tmp := first;
        CleanUpWord(tmp^.wr);
        first := first^.next;
        dispose(tmp);
        end;
end;

var
    c: char;
    SpcPressed: boolean;
    LOW: ListOfWrdPtr;
    LOCfirst, LOClast: ListOfChrPtr;
begin
    while not SeekEof do
        begin
        LOW := nil;
        LOCfirst := nil;
        LOClast := nil;
        SpcPressed := true;
        while not eoln do
            begin
            read(c);
            if (SpcPressed = true) and (c <> ' ') and (c <> #9) then
                begin
                AddWord(LOW, LOCfirst);
                AddToWord(LOCfirst, LOClast, LOW, c);
                {just create a new word}
                {start filling in a new word}
                SpcPressed := false;
                end
            else
            if (c = ' ') or (c = #9) then
                begin
                {end of word}
                SpcPressed := true;
                end
            else
            if (SpcPressed = false) and (c <> ' ') and (c <> #9) then
                begin
                AddToWord(LOCfirst, LOClast, LOW, c);
                {continuing typing a word (adding elements to old list)}
                end;
            end;
        TypeWordsInReverseOrder(LOW);
        writeln;
        CleanUpString(LOW);

        writeln('---');
        TypeWordsInReverseOrder(LOW);
        TypeAWord(LOCfirst);
        TypeAWord(LOClast);
        if LOW = nil then
            writeln('LOW is nil');
        if LOCfirst = nil then
            writeln('LOCfirst is nil');
        if LOClast = nil then
            writeln('LOClast is nil');
        writeln('---');

        end;
end.

В результате получилось, что переменная LOW (list of words) равна nil, а в переменных LOCfirst и LOClast (list of chars, соответственно) оказались мусорные значения.

Прогонял через отладчик:

72	        first := first^.next;
1: LOW^.wr^.ch = 112 'p'
2: LOW^.wr^.next^.ch = 105 'i'
3: LOW^.next^.wr^.ch = 112 'p'
4: LOW^.next^.wr^.next^.ch = 111 'o'
5: LOCfirst^.ch = 112 'p'
6: LOClast^.ch = 105 'i'
(gdb) 
73	        dispose(tmp);
1: LOW^.wr^.ch = 105 'i'
2: LOW^.wr^.next^.ch = <error: Cannot access memory at address 0x0>
3: LOW^.next^.wr^.ch = 112 'p'
4: LOW^.next^.wr^.next^.ch = 111 'o'
5: LOCfirst^.ch = 112 'p'
6: LOClast^.ch = 105 'i'
(gdb) 
69	    while first <> nil do
1: LOW^.wr^.ch = 105 'i'
2: LOW^.wr^.next^.ch = <error: Cannot access memory at address 0x0>
3: LOW^.next^.wr^.ch = 112 'p'
4: LOW^.next^.wr^.next^.ch = 111 'o'
5: LOCfirst^.ch = 248 '�'
6: LOClast^.ch = 105 'i'
(gdb)

Как такое возможно? Почему указатели LOW^.wr^.next^.ch и LOCfirst^.ch содержат адрес одной и той же области памяти, но один из них пустой, а второй заполнен «мусором».

Как это исправить?

KOMMUNIST90
() автор топика
Ответ на: комментарий от KOMMUNIST90

Задумайтесь о концепции владеющего указателя.

И еще: если SomePtr=nil, его разыменовывать нельзя - нельзя обращаться к SomePtr^.

Почему указатели LOW^.wr^.next^.ch и LOCfirst^.ch содержат адрес одной и той же области памяти

Это не указатели, тип обоих Char, а адрес области памяти каждого @(...).

bormant ★★★★★
()
Последнее исправление: bormant (всего исправлений: 3)
Ответ на: комментарий от KOMMUNIST90

Когда делают так

if (SpcPressed = true) ...
if (SpcPressed = false) ...

вместо очевидного
if SpcPressed ...
if not SpcPressed ...

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

bormant ★★★★★
()
Ответ на: комментарий от KOMMUNIST90

Можно было пойти таким путём:

{$H+}
program RevWords;

type
  PWordNode = ^TWordNode;                  {list of words}
  TWordNode = record
    Chars: String;
    Next: PWordNode;
  end;

  procedure AddWord(var First: PWordNode);
  var
    tmp: PWordNode;
  begin
    New(tmp);
    tmp^.Chars := '';
    tmp^.Next := First;
    First := tmp;
  end;

  procedure AddToWord(Words: PWordNode; c: char);
  begin
    with Words^ do Chars := Chars + c;
  end;

  procedure WriteWord(Words: PWordNode);
  begin
    Write(Words^.Chars);
  end;

  procedure WriteWords(First: PWordNode);
  begin
    if First <> nil then begin
      WriteWord(First);
      First := First^.Next;
    end;
    while First <> nil do begin
      Write(' ');
      WriteWord(First);
      First := First^.Next;
    end;
  end;

  procedure CleanUpWords(var First: PWordNode);
  var
    tmp: PWordNode;
  begin
    while First <> nil do begin
      tmp := First;
      First := First^.Next;
      Dispose(tmp);
    end;
  end;

var
  c: Char;
  SpcPressed: boolean;
  Words: PWordNode;
begin
  while not SeekEof do begin
    //Words := nil;
    SpcPressed := True;
    while not EoLn do begin
      Read(c);
      if c > ' ' then
        if SpcPressed then begin
          AddWord(Words);
          AddToWord(Words, c);
          SpcPressed := False;
        end
        else
          AddToWord(Words, c);
      else if c in [#9, ' '] then
        SpcPressed := True;
    end;
    WriteWords(Words);
    WriteLn;
    CleanUpWords(Words);
  end;
end.

bormant ★★★★★
()
Последнее исправление: bormant (всего исправлений: 2)
Ответ на: комментарий от hobbit

Ну тут, с одной стороны, нарушение явного запрета в условии на тип String, а с другой, в $H+ длинные строки совсем не тот стандартный String[255], что может в разумных пределах ввести «ограничния на длины строк, слов и др.».

Понятно, что изначально цель упражнения была в обучении и понимании работы со списками, с добавлением в начало списка и в конец, в т.ч. с очевидной оптимизацией в виде хранения указателя конца списка в невладеющем указателе (что вызвало странные вопросы у ТС - выходит, цель достигнута пока не полностью). Скажем, в таких целях можно вернуться к списку из String для «слов», что длиннее High(SizeInt), правда практической ценности не добавит, если только как упражнение, да и протестировать такое (добавление второй строки в список по реальному ограничению длины) будет непросто, придётся искусственно ограничивать ;)

Ну и стандартно для задачи достаточно тупого списка блоков, чтобы вычитать строку целиком (возможно, с удалением дублирующихся разделителей на случай «слово 4 ГБ пробелов слово»), и пробежки по ним с конца с выводом по разделителям. Списки «слов» - оверхэд.

bormant ★★★★★
()
Последнее исправление: bormant (всего исправлений: 2)
Ответ на: комментарий от KOMMUNIST90

Еще пара моментов...

//procedure AddWord ...
    tmp^.wr := nil;
    LOCfirst := tmp^.wr; // ??? почему не просто nil?
Никакой магической связи между LOCfirst и tmp^.wr не появится, LOCfirst получит значение, которе хранится в tmp^.wr, а туда только что положили nil.

//begin -- основная программа
  CleanUpString(LOW);
  //...
  if LOCfirst = nil then
    WriteLn('LOCfirst is nil');
  if LOClast = nil then
    WriteLn('LOClast is nil');
Это бессмысленные проверки — у вас выше нет кода, который присвоил бы LOCfirst и LOClast значение nil, следовательно они сохранят свое прежнее значение. Только нужно понимать, что после зачистки списка теперь они указывают в уже удаленную память. Dispose(t) не «очищает» ни сам переданный указатель t (его значение не меняется), ни t^ — содержимое памяти по указателю и, очевидно, не может чистить вообще все указатели на этот блок, поскольку ничего о них не знает. Dispose(t) только помечает во внутренних структурах менеджера памяти ранее выделенный блок по адресу t свободным и ничего больше (ну почти ;) ).

bormant ★★★★★
()

Ну, на вопрос тут уже ответили — хотя, конечно, странно сие наблюдать, я про этот SeekEof и его отличия от простого Eof целый параграф написал, но ТСу, по ходу, всё до фени.

На правах автора книжки отмечу, что если ТС продолжит форматировать свои программы в том же духе, что мы наблюдаем здесь, то вряд ли чего-то серьёзного добьётся. Как должны в нормально оформленном коде располагаться begin и end, в книге написано вполне подробно.

Croco ★★★
()
Ответ на: комментарий от bormant

Когда делают так

Кстати да. Ещё один параграф, который ТС предпочёл пройти мимо. См. 2.2.9, в конце, жирным шрифтом.

Croco ★★★
()
Ответ на: комментарий от Croco

если ТС продолжит форматировать свои программы в том же духе

Прямо пайтон-стайл.
Хотя, пусть безобразно, лишь бы однообразно...
Но зато после такого своего об чужой код глазоньки поломаются с непривычки ;)

bormant ★★★★★
()
Ответ на: комментарий от bormant

Прямо пайтон-стайл.

Ну не совсем, у такого форматирования вроде даже название есть, только я его не помню и искать лень. Если мне склероз не изменяет, весь OpenSSL так отформатирован.

Естественно, всё это не делает сей стиль сколько-нибудь допустимым, поскольку когда закрывающая операторная скобка ставится в той же колонке, что и закрываемое тело, скобку становится тупо не видно. Ну а авторы OpenSSL у меня особого уважения не вызывают (мягко говоря).

Croco ★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.