Zwei Char* zusammen fügen

Hallo ich möchte in einer Funktion zwei Char* zur eine Zusammen fügen. Dabei habe ich die strcat-Funktion eingesetzt. Jedoch habe ich gemerkt, dass immer wieder aufrufen der Funktion den RAM des Arduinos weiter voll macht.

Ich habe mal ein die Funktionalität mal im folgenden Script gekapselt ohne restlichen Code, daher bitte nicht nach Sinn diesen Cods fragen, dieser soll lediglich das Problem demonstrieren.

void loop()
{
 ConCat((char*)"/A/B/");
}

void ConCat(char* Dir) {
char Name[100];
 char* DirName = Dir; //Dir = Pfad wird übergeben z.B: "/A/B/"
file.getName(Name,sizeof(Name)); //Eine Funktion um Datenamen auszulesen z.B: "test.pm3"
 strcat(DirName,Name); // Soll Pfad und Datei zusammen fügen 
Serial.println( DirName );
}

Wass ich feststellen konnte, dass die Seriale Ausgabe nach jeden Schleifen durchlauf einfach das String immer weiter mit Anhängen der Dateinamen durchgeführt wurde. z.B:

  1. "A/B/test.mp3"
  2. "A/B/test.mp3/test.pm3"
    und so weiter bei jedem Durchlauf.

Wieso wird immer weiter der Dateinamen an eine bereits zusammen gefügten char* angehängt? Ich hätte gedacht dass sobald die "ConCat"-Funktion abgearbeitet wird, alle Variablen diese innerhalb dieser Funktion vereinbart wurden, wieder aus dem Speicher gelöscht?

Der Durchlauf läuft paar mal bis vermutlich der RAM Speicher von Arduino voll läuft.
Wie kann ich es verhindern dieses Phänomen. An Sich ist es genügend RAM für einen Pfad+Datei vorhanden, jedoch muss dieser bei dem nächsten wieder frei gemacht werden.

es gibt keinen Speicher, um das Ergebnis davon zu speichern

strcat(DirName,Name);

DirName ist nur ein statisch zugewiesener Pointer (auf das "/A/B/"), den Sie nicht ändern sollten

void loop()
{
  const char directory[] = "/A/B/";
  ConCat(directory);
}

void ConCat(const char *Dir)
{
  char DirName[126] = {"\0"};
  char Name[] = "test.pm3";
  strcpy (DirName, Dir);
  strcat(DirName, Name); // Soll Pfad und Datei zusammen fügen
  Serial.println( DirName );
}

@J-M-L war schneller :wink:

:wink:

um sicherer (strlcpy & strlcat) und schneller (keine Initialisierung) zu sein:

void setup() {
  Serial.begin(115200);
}

void loop()
{
  const char directory[] = "/A/B/";
  ConCat(directory);
}

void ConCat(const char *Dir)
{
  char DirName[126]; //keine Initialisierung erforderlich, da strlcpy() dies tut.
  const char * Name = "test.pm3";
  strlcpy (DirName, Dir, sizeof DirName);
  strlcat(DirName, Name, sizeof DirName); // Soll Pfad und Datei zusammen fügen
  Serial.println( DirName );
}
2 Likes

Wo liegt im Grunde der Unterschied bei folgenden Deklarationen? Es wird alle drei Variationen wohl akzeptiert.

const char[ ] a = "123";
const char* a = "123";
char* a = (char*)"123";

Diese ist irgendwie unsinnig... bis problematisch.
.... schon fast fahrlässig.

Naja...
Gemeint ist sicher
const char* const a = "123";

Die beiden teilen sich eine Repräsentation des Strings

Dieses Array beinhaltet eine Kopie der Zeichenkette.
Also u.a. mehr Speicherverbrauch.

"u.a" kann höchstens "eventuell" bedeuten. Dem aktuellen Compiler für einen Uno ist es jedenfalls egal, er macht das gleiche draus. Ob "123" als Kopie von a den Compilierungsvorgang überlebt und, falls irgendwo im Sketch nochmal "123" vorkommt, nochmal verwendet wird, ist dem Compiler freigestellt.

Nein, nicht unter allen Umständen!
Die "123" muss min einmal im Flash stehen, damit überhaupt irgendwelche Variablen mit dem Inhalt erzeugt werden können.

Die "123" muss mindestens einmal im RAM stehen, und sei es auch nur als anonyme konstante Zeichenkette. Sonst könnte man keinen gültigen Zeiger darauf richten.
const char[ ] b = "123"; die Zeile ist falsch

sketch_jul10c:5:11: error: structured binding declaration cannot have type 'const char'
    5 | const char[ ] b = "123";
      |           ^~~
E:\Programme\arduino\portable\sketchbook\sketch_jul10c\sketch_jul10c.ino:5:11: note: type must be cv-qualified 'auto' or reference to cv-qualified 'auto'
sketch_jul10c:5:11: error: empty structured binding declaration
sketch_jul10c:5:15: error: expected initializer before 'b'
    5 | const char[ ] b = "123";
      |               ^

Gemeint ist also:
const char b[] = "123";
Hier könnte der Compiler optimieren und auf die eben genannte anonyme Variable zurückgreifen. Wird er aber nur bei hoher Optimierungsstufe tun, wenn überhaupt

char b[] = "123";
Spätestens jetzt ist mit Optimierungen Schluss, und es wird eine Kopie angelegt.

Hier dann noch der Beweis für die Kopie:

#include <Streaming.h> // die Lib findest du selber ;-)
Stream &cout {Serial}; // cout Emulation für AVR Arduinos

const char* const a = "123";

const char b[] = "123";
//const char[ ] b = "123";




void setup() 
{
  Serial.begin(9600);
  cout << F("Start: ") << F(__FILE__) << endl;
  cout << a << endl;
  cout << b << endl;
  char *c = (char *)b;  // const modifizierer entfernen
  *c ='c'; 
  cout << a << endl;
  cout << b << endl;
}

void loop() 
{

}

Ausgabe:

Start: E:\Programme\arduino\portable\sketchbook\sketch_jul10c\sketch_jul10c.ino
123
123
123
c23

Damit ist bewiesen, dass die "123" beim Programmstart 1 mal im Flash und 2 mal im Ram steht,

Außer dieser "Kopie" für b im RAM und natürlich deren Initialwert im Flash gibt es die "123" nicht nochmal, wenn sie sonst nicht gebraucht wird. Das erfordert keinen allzu hohen Optimierungsgrad.
a) char b[] = "123";
b) char b[] {"123"};
sind in der Praxis dasselbe, auch wenn das erste fast wie ein Zuweisungsoperator aussieht.
Das kannst du bemerken, wenn du den Zuweisungsoperator überlädst (eine deiner Lieblingstätigkeiten, danke dafür übrigens)

Du hast übrigens bewiesen, dass "123" zweimal im RAM steht, einmal an der Stelle a und einmal an der Stelle b. Kein Widerspruch. Wie der RAM initialisiert wurde, kann man einem C++ Programm natürlich nicht ansehen.

Guck die Sketch-Größe im Flash an, ändere den zweiten Text nach "124" und sieh, dass selbst im Flash wohl zwei Versionen von "123" gelegen haben müssen. (Für alle initialisierten Variablen legt der Compiler ein gemeinsames Abbild an, vermute ich)

(Mit char [] b hast du natürlich völlig recht, wo ich das nur her hatte? :slight_smile: )

Es geht sogar noch weiter:

char *a = "123";
char *b = "3";

Wenn man jetzt Zeiger vergleicht:
(a+2) == b

Dann ist bei hoher Optimierung das Resultat oftmals true und bei niedriger meist false.
Da auf unspezifiziertem Verhalten basierend.
Wobei die Spezifikation in einem Punkt recht klar ist: Zeigervergleiche sind nur spezifiziert, wenn sie auf die Identische Speicherstruktur zugreifen. Und eben das ist hier nicht gewährleistet.

Das ist mir mehr als klar. Darum verwende ich meist die {} Initialisierung.
Eben um Verwechselungen auszuschließen. Und {} Initialisierungen sind Typesicherer.

int a = 3.14; // geht ohne Murren durch
int b {3.14}; // erregt Aufmerksamkeit

Sowas habe ich schon lange aufgegeben!
Da kommen zu viele Randbedingungen ins Spiel. Da lohnt es sich eher den generierten Code zu analysieren.

Wie Zeichenketten Literale im Speicher angeordnet, wer auf was Verweist, ist in C und C++ nicht spezifiziert. Das schafft Raum für Optimierungen der Compilerersteller. Es ist also eigentlich völlig Sinnfrei, über das "Wie?" nachzudenken, wenn man selber keinen Compiler bauen will.
Jeglicher Code, der sich auf eine solcherart Annahme stützt ist per Definition fehlerhaft.
Denn er kann mit jeder Optimierungsstufe, Compiler Version oder Ersteller, Zielprozessor etwas anderes produzieren.

So auch dieser:

#include <Streaming.h> // die Lib findest du selber ;-)
Stream &cout = Serial; // cout Emulation für Arme

const char* const a = "123";
const char b[]      = "123";
const char *c       = "123";
const char d[]      = "123";



void setup() 
{
  Serial.begin(9600);
  cout << F("Start: ") << F(__FILE__) << endl;
  cout << a << endl;
  cout << b << endl;
  cout << c << endl;
  cout << d << endl;
  cout << "123" << endl;
  cout << "---------------" << endl;


  // mogeln
  char *ch1 = (char *)b; // const modifizierer entfernen
  *ch1 = 'X';
  
  char *ch2 = (char *)c; // const modifizierer entfernen
  *ch2 = 'Y';

   
  cout << a << endl;
  cout << b << endl;
  cout << c << endl;
  cout << d << endl;
  cout << "123" << endl;
}

void loop() 
{

}

Resultat:

Start: E:\Programme\arduino\portable\sketchbook\sketch_jul10c\sketch_jul10c.ino
123
123
123
123
123
---------------
Y23
X23
Y23
123
Y23

Hier muss man streng unterscheiden:

Undefiniertes Verhalten:
Wenn man in sein Programm ein undefiniertes Verhalten einbaut, kann es passieren, dass der Arduino einem das Wohnzimmer neu tapeziert.

Unspezifiziertes Verhalten:
Der Compiler tut es, er tut es auch so, dass es funktioniert. Das ist spezifiziert. Aber wie er es tut/erreicht, bleibt ihm überlassen. Vollständig.

Implementation definiertes Verhalten:
Der Compiler tut, was er soll. Die Compiler Doku liefert die Spezifikation für das Verhalten. Die Compiler der verschiedensten Ersteller unterscheiden sich in dem Punkt.

Spezifiziertes Verhalten:
Das Verhalten ist in der Sprache selber festgeschrieben. Alle Compiler verhalten sich gleich.

Ich habe das mal auf 4 verschiedene Verhaltensmuster runter gebrochen.
Man sollte sich schon tunlichst im spezifizierten Bereich bewegen, wenn man keine Wackelkandidaten bauen will.