Du allokierst 6*32bit. du schreibst aber nur 15*5 bits zurrück.
(Deswegen funktioniert dein Programm.)
Das sehe ich nicht so.
Er alloziert Platz für 6 Pointer. (Im prinzip egal ob dort *char, *int oder *void steht)
Auf einer einer 32-Bit Maschine wären das also 6*32Bit = 24 Byte.
Ab test[0] wären also linear 24 Byte reserviert zum beschreiben. Platz für 6 Pointer.
test[2] wäre beispielsweise aber schon *(test + sizeof(*void) * 2).
Und das wäre test[0] + 8 chars (Byte).
test[5] wäre die letzte Zelle für einen Pointer, die man beschreiben darf, also
*(test + sizeof(*void) * 5). => test[0] + 20 chars (Byte). Ab da an bleiben also genau 4 Byte für den letzten Pointer übrig, den man schreiben darf.
Das schreiben auf test[6] ist schon ein Buffer-Overflow.
Warum es trotzdem bis 15 funktioniert? Wenn man Speicherplatz beim Betriebsystem anfordert, dann haben die zurückgegebenen Speicherseiten eine bestimmte Granularität. Man bekommt notwendigerweise dann mehr zurück als man braucht.
Allerdings merkt sich der Speichermanager das, in etwa: "
Ich habe 2000 Speicherzellen Platz bekommen. Angefordert wurden aber nur 24. Beim nächsten mal benutze ich wieder Zellen von diesen 2000, aber erst NACH den reservierten 24."
Angenommen du würdest jetzt noch einmal Speicherplatz reservieren. Dann würdest wahrscheinlich (kommt auf die Logik des Speichermanagers und die Umstände an) Platz ab test[6] bekommen, und würdest deine alten char*-Pointer die du in der Schleife geschrieben hast wieder überschreiben mit neuen Daten.
Du hast also Glück, dass es nicht abstürzt, weil mehr kongruent nacheinanderliegener Platz da ist, und du danach nicht noch einmal Speicher anforderst, der ja ab test[6] liegen könnte.
C hat kein Range-Checking wie zum Beispiel Delphi es hat. du kannst test[53453] probieren und es würde freudig ausgewertet werden und nach der Adress-Rechnung wird auch tatsächlich versucht dahin zu schreiben, ohne zu gucken ob das geht. (Eine Range-Checking Logik gibt es in C nicht und wäre auch viel zu Overkill für simples leichtes C, das möglichst portabel sein sollte)
Zu deinem Problem:
Du könntest Zeilen, wie du es schon tatest, als Liste von String-Pointern allozieren:
int alloc_size = 5;
test = (char**)malloc(sizeof(char*) * (5));
Kommen zu den Zeilen wieder welche dazu, kannst du ja wieder mit realloc um 5 erhöhen:
alloc_size += 5;
test = (char**)realloc(sizeof(char*) * (alloc_size));
Dann müsstest du die Länge der Zeile herausfinden und für jede Zeile muss wieder ein malloc her!
Und zwar dieser Art:
test[0] = (char*)malloc(len+1); //wobei len die Länge der Zeile ist
Dann kanst du auch mit strcpy(test[0], sourcestring) deine Strings dortreinkopieren.