Wie kann man mehrere Leerzeichen durch eins ersetzen in c?

2 Antworten

Etwa so:

#include <string.h>

void zipspace ( char * s )
{
 while ((s=strstr(s, "  ")))
  for (char *t=s; (t[0]=t[1]); ++t)
;
}

Die innere for-Schleife macht eigentlich nur ein strcpy(s, s+1). Letzteres hat leider wegen der Überlappung undefiniertes Verhalten.


BahaZahedah 
Fragesteller
 19.02.2016, 19:37

danke sehr ... kann man aber ohne zeiger machen ?!

1
ralphdieter  19.02.2016, 20:27
@BahaZahedah

Naja, C ohne Zeiger ist wie ... — such Dir was aus :)

In der Deklaration von zipspace() kannst Du "char*s" durch "char s[]" ersetzen. Das macht keinen Unterschied.

In der for-Schleife brauchst Du aber einen Zeiger. Notfalls könntest Du diese Schleife komplett durch "strcpy(s, s+1)" ersetzen. Das wird in 99,999% aller Fälle funktionieren (und sogar schneller sein), ist aber im Standard nicht garantiert.

0
ralphdieter  20.02.2016, 11:20
@BahaZahedah

Nächster Versuch, ohne Pointer (von TeeTier inspiriert). Ich denke, dass man das so dem Lehrer zeigen kann:

void zipspace ( char text[] )
{  const int len = strlen(text);
 int to = 0; // letzte Schreib-Pos.

// Das erste Zeichen bleibt erhalten.
// Nun geht's einmal durch den Rest:

for (int pos=1; pos<=len; ++pos)
{
 // to wandert *nur*, wenn das neue
// oder das vorige Zeichen *kein*
// Leerzeichen ist:
if (text[pos]!=' ' || text[to]!=' ')
 ++to;

text[to] = text[pos];
}
}

Anmerkungen:

Die Schleife beginnt erst ab 1, weil innerhalb das "zuletzt kopierte" Zeichen benutzt wird. Das funktioniert auch im Sonderfall "leerer Text", weil da die Schleife nichts tut. (TESTEN!)

Die Schleife ist erst nach dem abschließenden '\0' zu Ende. Damit stelle ich sicher, dass auch Mehrfach-Leerzeichen am Textende korrekt durch genau eins ersetzt werden. (TESTEN!)

Anfänger sparen sich gern die Variable len und schreiben  "pos<=strlen(text)" in die Schleifenbedingung. Dabei explodiert aber die Laufzeit, weil strlen() jedesmal den ganzen Text durchläuft.

1
TeeTier  20.02.2016, 22:32
@ralphdieter

Oh, schick! :)

Aber persönlich versuche ich immer auf strlen() zu verzichten, wenn man sowieso über die Zeichen eines C-Strings iteriert, da strlen() das ja auch sowieso erst mal selbst tut ... das hat für mich immer das Gefühl von "doppelt-gemoppelt", und selbst ein einziger Aufruf ist mir schon viel zu viel, wenn er sich denn irgendwie vermeiden lässt.

Für to, len und pos würde ich size_t anstelle von int verwenden, und den if-Zweig klammern, aber das ist ja sowieso Geschmackssache. :)

1
ralphdieter  20.02.2016, 22:51
@TeeTier

Kurzantwort: Ja-aber, Ja, Ja.

  • Ja, strlen() ist hässlich. Aber ohne len funktioniert meine Schleife nicht; da hätte ich den Fall "leerer Text" extra abbacken müssen. Das mag zwar effizient und pädagogisch wertvoll sein, aber ich hasse Sonderfälle.
  • Ja, size_t passt perfekt. Ich mache sogar Rückwärts-Schleifen damit: for (size_t i=n; i--; ) {...}. Aber leider bist Du der erste seit 20 Jahren, der deshalb nicht auf mir rumhackt. Dankeschön!
  • Und ja (schäm), ich sollte mehr Klammern setzen... Aber wenigstens habe ich ++to; in eine eigene Zeile geschrieben (war'n harter Kampf). Oft ertappe ich mich sogar bei sowas:
 to += (text[pos]!=' ' || text[to]!=' ');

     Dabei will ich nicht mal cool sein — ich denke einfach so!

1
TeeTier  20.02.2016, 23:21
@ralphdieter

Zu deinem letzten Beispiel fällt mir eine Funktion ein, die ich vor ein paar Tagen geschrieben habe (ein strcmp() welches bewusst NICHT die Locale-Einstellungen berücksichtigt, und nur Werte < 127 möglich sind die als reine Zahlen behandelt werden):

int cmpzta(const char *sa, const char *sb) {
char ca = *sa;
char cb = *sb;

while (ca == cb && ca) {
ca = *++sa;
cb = *++sb;
}

return ca < cb ? -1 : ca != cb;
}

Die return-Zeile macht genau das, was du beschrieben hast. :)

Da "char" laut Spezifikation entweder "signed char" oder auch "unsigned char" sein kann, und ein Integer-Überlauf Undefined Behavior ist, konnte ich am Ende nicht einfach "ca - cb" schreiben, was man in solchen Fällen ja auch oft sieht.

Außerdem habe ich versucht, die Derefenzierungen der Zeiger auf ein Minimum zu reduzieren. Leider optimiert mir jeder Compiler die while-Bedingung so, dass sie für den konkreten Anwendungsfall meiner Eingangsdaten ineffizient wird, was man am Disassemblat sehen kann. (Aus "ca == cb && ca" wird "ca && ca == cb", obwohl in 99% aller Fälle ersteres zutreffen wird.)

Naja egal ... wollte nur darauf hinweisen, dass du nicht der einzige bist, der so komisch denkt. ;)

0
TeeTier  20.02.2016, 23:40
@ralphdieter

Ganz ehrlich: Ich denke ein Einsteiger kann da durchaus viel mitnehmen, auch wenn er dabei auf Anhieb nicht alles behält. Als Anfänger habe ich auch immer gerne Threads von erfahrenen Programmierern gelesen, und besonders "clevere" Funktionen fand ich sehr interessant. (Auch wenn ich jetzt im Nachhinein weiß, dass "clever" nicht immer gleich "gut" heißen muss.)

Also dann, ich denke wir sind mit dem Thema jetzt erst mal durch. Bis zum nächsten mal! Schönen Abend noch! :)

1

Also wenn du eine naive Lösung ohne Zeiger suchst, vielleicht so:

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>

void zipws(char *s) {
char c;

size_t from = 0;
size_t to = 0;

for (int ws, last = 0; (c = s[from++]); last = ws) {
ws = isspace(c);

if (!ws || !last) {
s[to++] = ws ? ' ' : c;
}
}

s[to] = '\0';
}


int main(void) {
char s[] = "foo bar baz\tqux\t \r\nquuux";
printf("before: <%s>\n", s);

zipws(s);
printf("after: <%s>\n", s);

return EXIT_SUCCESS;
}

Das ersetzt alle Whitespace-Folgen (also ' ', '\t', '\r', '\n', usw.; auch gemischt) durch ein einziges normales Leerzeichen, sodass obiger Test folgendes ausgibt:

before: <foo bar  baz        qux      
quuux>
after: <foo bar baz qux quuux>

Allerdings gefällt mir die Lösung persönlich aus verschiedenen Gründen überhaupt nicht, und ich würde auf jeden Fall Zeiger bevorzugen:

void zipws(char *s) {
char c, *p = s;

for (int ws, last = 0; (c = *s++); last = ws) {
if (!(ws = isspace(c)) || !last) {
*p++ = ws ? ' ' : c;
}
}

*p = '\0';
}

Das ist zwar immer noch lange nicht perfekt, aber ich habe jetzt keine Lust Kleinigkeiten durch zu kauen. Da du aber um eine Lösung ohne Zeiger gebeten hast, nehme ich im Hauptbeispiel ganz oben am Anfang Array-Indizes zur Hilfe. :)

Außerdem musst du immer abwägen, ob du eine "clevere" Lösung bevorzugst, oder lieber etwas "Ordentliches" haben willst (im Sinne von Wartbarkeit und Verständlichkeit).

Beide Beispiele aus meiner Antwort erachte ich nicht gerade als schick, habe jetzt um 03:00 Uhr morgens aber auch nicht die Muße, mich näher damit beschäftigen zu wollen. Ich habe es nicht mal getestet ... sollte aber eigentlich in dieser Form problemlos kompilieren. ><

Gute Nacht! :)


ralphdieter  20.02.2016, 11:39

Ich habe es nicht mal getestet ... sollte aber eigentlich in dieser Form problemlos kompilieren

Ganz ohne Syntax-Highlightning und ohne Autokorrektur??? Du bist ganz schön arrogant — und ich dachte immer, ich wär der einzige :-)

Trotzdem etwas Kritik:

  • Du mischst in der for-Schleife Äpfel (ws, last) und Birnen (c, s). Es hat ein Weilchen gedauert, bis ich das verstanden habe. Ich habe die Erfahrung gemacht, dass das nicht nur den Leser, sondern auch den Compiler verwirren kann; der optimiert dann nur noch halbherzig.
  • Ich glaube, dass Dein zipws() alle Leerzeichen am String-Ende ersatzlos löscht (ganz sicher bin ich mir aber nicht).
  • Du hast die Aufgabe von "Leerzeichen" auf "Whitespace" erweitert. Kann man machen, aber ich hasse es, wenn mir jemand ungefragt meine Tabulatoren und Zeilentrenner weglöscht — sei froh, dass Du nicht mein Kollege bist :-P
1
TeeTier  20.02.2016, 22:21
@ralphdieter

Ich gebe dir in jedem der Punkte absolut recht!

Vor allem beim letzten habe ich mir vorher schon gedacht, dass irgendein "Erbsenzähler" darauf anspringen wird. ;)

Aber du hattest strstr() verwendet, also wollte ich auch wenigstens EINE Funktion der Standardbibliothek benutzen. (Ehrlich, kein Scherz! Das war der ausschlaggebende Grund ... im Nachhinein frage ich mich, was ich mir dabei gedacht habe.)

Aber es war mitten in der Nacht, und ich befand mich im Halbschlaf ... versteht das denn keiner?!?!? hahaha ><

Mir gefällt übrigens auch die Deklaration von c und *p in ein und der selben Zeile überhaupt nicht, und die Nicht-Initialisierung von "ws" im Schleifenkopf deutet auch schon auf ein Knickei hin. Naja, und dann gibt es neben der Form der if-Bedingung noch weitere Punkte ... aber wir wollen mal nicht so kleinlich sein. :)

Schönen Abend noch! :)

1
TeeTier  20.02.2016, 23:35
An den Fragensteller:

Ich denke, die letzte Version aus ralphdieter's Antwort ist die Schönste. Wie bereits erwähnt, muss man (fast) immer abwägen zwischen effizient und elegant, und wie du an unserer Diskussion gesehen hast, gibt es selbst bei so einer einfachen Aufgabe noch massig viel Optimierungspotential.

Um zu viele Zeiger-Dereferenzierungen zu vermeiden, könnte man noch wesentlich mehr mit "register" Variablen arbeiten, aber erstens bist du vermutlich noch nicht so weit, und zweitens ist es sicherlich nicht das, was dein Lehrer sehen will.

Auf jeden Fall hast du hier schon mal Folgendes gelernt:

- Niemals eigenständig die Anforderungen erweitern (z. B. aus "Leerzeichen" einfach "White Space" machen)

- Alle drei Teile im Kopf von for-Schleifen konsistent halten

- Dir einen einheitlichen Stil der Codeformatierung angewöhnen

- Für Optimierungen ist später Zeit; Hauptsache dein Code ist erst mal korrekt lauffähig und leicht verständlich

- Wenn du irgendwann mal fortgeschritten bist, kannst du dir mit einem Disassembler angucken, ob der Compiler deinen Code korrekt übersetzt hat

- Der Input von anderen Programmierern ist bei Veröffentlichung von eigenem Code sehr wertvoll, und durch ein paar kritische Sätze Dritter in einer Diskussion lernt man oft mehr, als beim Lesen einiger Kapitel Theorie

Also dann ... wenn ich du wäre, würde ich die letzte Lösung von ralphdieter wählen. Die benutzt zwar strlen(), ist aber sehr schön schlank.

Viel Erfolg damit! :)

0