Funktionen und Unterprogramme in C definieren
Hallo !
Ich versuche gerade die Programmiersprache C zu lernen.
Ich habe zu diesem Zweck auch ein paar Tutorials auf Youtube angeschaut.
Da ich schon Programmiererfahrung in einer anderen Programmiersprache habe komme ich auch gut voran.
Leider hat sich die Video-Tutorial-Reihe die ich zur Zeit schaue ausgerechnet um das Thema Funktionen und Unterprogramme herumgemogelt und das nur äußerst oberflächlich abgehandelt.
- Kann mir jemand kurze Beispielprogramme für Funktionen und Unterprogramme posten oder mir die ausführliche komplette Syntax nennen mit denen man Funktionen und Unterprogramme einleitet ?
- Insbesondere interessiert mich wie man in C Variablen in Funktionen und Unterprogrammen lokal statisch, also STATIC, machen kann.
- Müssen die Variablen im Hauptprogramm int nain () { ... return 0; } definiert werden, die später in Funktionen und Unterprogramen lokal verwendet werden, oder in der jeweiligen Funktion oder Unterprogramm selber oder kann man es sich aussuchen ?
LG Spielkamerad
4 Antworten
Funktionen werden deklariert mit:
[Rückgabedatentyp] Funktionsname (Parameterliste)
{
Code
}
Wobei die Parameterliste sich weiter gliedert in:
(Datentyp Variablenname, Datentyp Variablenname, Datentyp Variablenname, ...)
Beispiel: Funktion, die zwei Integer addiert und einen Integer (die Summe) zurückgibt.
#include <stdio.h>
int summe (int a, int b)
{
return a + b;
}
int main ()
{
printf("Die Summe lautet: %i", summe(3, 4));
return 0;
}
Der Datentyp vor dem Funktionsnamen ist der Rückgabedatentyp. Die Daten werden von der aufrufenden Funktion an die aufgerufene Funktion per Parameter übergeben. Hierfür gibt die aufrufende Funktion die Daten in der Argumentenliste (das, was in Klammern steht) an die aufgerufene Funktion über.
Für die aufgerufene Funktion sind die Parameter lokale Variablen, die lediglich mit dem übergebenen Wert "vorbelegt" sind. Falls Du innerhalb der Funktion weitere Variablen brauchst, etwa um Zwischenergebnisse zu speichern oder als Zählvariable für eine Schleife, deklarierst Du sie innerhalb der entsprechenden Funktion per "Datentyp Variablenname", also z. B. "int count;". Deklarierte Variablen sind nicht initialisiert und können beliebige Werte enthalten (sie enthalten einfach das, was "zufällig" vorher an dieser Speicheradresse stand, an der sie hinterlegt wurden). Um sie zu initialisieren, kannst Du die Deklaration und Wertzuweisung in einer Zeile vornehmen, also z. B. "int count = 0;". Die Variablennamen gelten nur innerhalb der Funktion, in der sie deklariert wurden. Außerhalb sind die Variablen "nicht sichtbar".
Der Speicher für diese (lokalen) Variablen wird auch erst beim Aufruf der Funktion auf dem Stack allokiert und nach dem Zurückkehren der Funktion wieder freigegeben.
Wenn eine Funktion sich selbst aufruft (Rekursion), können (und werden) verschiedene Aufrufe derselben Funktion unterschiedliche Belegungen der lokalen Variablen haben. Beispiel Fakultätsfunktion.
int fakultaet (int a)
{
if (a <= 0)
return 1;
else
return a * fakultaet(a - 1);
}
Wenn Du diese Funktion aufrufst, z. B. mit "fakultaet(3)", rechnet sie "3 * fakultaet(3 - 2) = 3 * fakultaet(2)". Der Aufruf von "fakultaet(2)" rechnet "2 * fakultaet(2 - 1) = 2 * fakultaet(1)". Der Aufruf von "fakultaet(1)" rechnet "1 * fakultaet(1 - 1) = 1 * fakultaet(0)". Der Aufruf von "fakultaet(0)" ergibt (per Definition) 0 und kehrt zurück. Damit ist das Ergebnis von "fakultaet(0)" bekannt und der Aufruf von "fakultaet(1)" rechnet "1 * fakultaet(0) = 1 * 1 = 1" und kehrt mit dem Rückgabewert 1 zurück. Damit ist das Ergebnis von "fakultaet(1)" bekannt und der Aufruf von "fakultaet(2)" rechnet "2 * fakultaet(1) = 2 * 1 = 2" und kehrt mit dem Rückgabewert 2 zurück. Damit ist das Ergebnis von "fakultaet(2)" bekannt und der Aufruf von "fakultaet(3)" rechnet "3 * fakultaet(2) = 3 * 2 = 6". Damit ist das Ergebnis von "fakultaet(3)" bekannt und der Aufruf kehrt mit dem Ergebnis 6 zurück.
Hier lief die Funktion "fakultaet" gewissermaßen viermal "ineinander", nämlich als "fakultaet(3)", "fakultaet(2)", "fakultaet(1)", "fakultaet(0)". Die einzelnen Durchläufe entscheiden sich in der Belegung der lokalen Variablen a. Diese wird auf dem Stack hinterlegt. Jeder Aufruf fügt einen neuen "Stackframe" (frame = Rahmen) hinzu, in dem die Werte der lokalen Variablen dieses "Durchgangs" abgelegt werden (und die Speicheradresse, von der aus die Funktion aufgerufen wurde, damit nach "Rückkehr" bekannt ist, wo im Code die Ausführung fortgesetzt werden muss, schließlich können Funktionen von unterschiedlichen Stellen im Programm aus aufgerufen werden). Sobald ein "Durchgang" der Funktion "zurückkehrt", wird der entsprechende "stack frame" wieder "abgeräumt". Da das ganze wie ein "Stapel" (daher auch die Bezeichnung "stack") funktioniert, wird dadurch automatisch der "darunterliegende Frame" sichtbar und die "alte Belegung" der lokalen Variablen (vor dem Funktionsaufruf) ist "wiederhergestellt".
Der neue Code-Editor ist wirklich grauenvoll und entstellt hier jegliche Art von Quelltext.
Ich denke, deine Antwort ist für den Fragensteller die eindeutig gehaltvollere, und du hast zufällig so ziemlich genau das gleiche Beispiel wie ich gewählt! (naja, ist ja auch naheliegend)
Aber guck dir bitte mal auch meine Antwort an, ich glaube die ist evtl. ganz interessant für dich!
Ansonsten gute Antwort! Schönen Abend noch! :)
Das ist sogar in der Tat äußerst interessant für mich, weil ich Informatiker/Entwickler bin und auch recht viel in C entwickle und mir nie wirklich Gedanken darüber gemacht habe. ;-)
Also ich verwende in der Regel die Datentypen aus "cstdint" (oder früher "stdint.h"), wie "uint32_t", "int8_t", etc. anstatt "unsigned int", "char", etc. weil ich weiß, dass die "Breite" der letzteren architekturabhängig ist (und man das ja in der Regel nicht will). Gibt es das Problem dort auch?
Wenn du die Möglichkeit hast, und dein Compiler unterstützt typedefs für uint32_t & Co, dann ist das natürlich prima, aber leider hat man nicht immer die Wahl, und wenn man Pech hat, wird C89 nur zur Hälfte unterstützt. :)
Allerdings tritt ein möglicher Programmabbruch auch bei der Benutzung von "stdint.h" bzw. "cstdint" auf! Man muss also VOR der eigentlich Rechenoperation prüfen, ob es zu einem Überlauf kommen kann.
Deine summe()-Funktion würde mit uint32_t und Überprüfung so aussehen:
uint32_t summe(uint32_t a, uint32_t b) {
if (a > UINT32_MAX - b) {
abort(); /* Überlauf */
}
return a + b;
}
Für Subtraktion (a - b) wäre es noch einfacher:
if (b > a) { ...
Für Multiplikation - vor allem bei signed-Typen - ist die Überprüfung hingegen nochmal deutlich komplexer und sogar geschachtelt.
Man muss also aus Effizienzgründen gut abwägen, wann ein Wert überlaufen könnte und überprüft werden muss, oder ob man gleich ganz auf eine Überprüfung verzichten kann. :)
Ich möchte dir jetzt deine Antwort bewusst nicht beantworten, da dir offensichtlich noch zu viele Grundlagen fehlen, aber einen gut gemeinten Rat will ich dir dennoch mal geben:
Auch wenn du als Anfänger den Eindruck hast, dass du mit Youtube-Videos relativ gut C lernen kannst, so ist mir persönlich noch nie ein einziges solcher Lehrvideos untergekommen, was in meinen Augen auch nur annähernd akzeptabel war.
Die meisten dieser Videos werden von Leuten gemacht, die selber noch Anfänger sind, bzw. erst seit ein paar Wochen programmieren können. Das merkt man dann auch am Code und and den Erklärungen.
Da du ja offensichtlich motiviert bist, würde ich dir dringend empfehlen, dir ein C-Buch zu kaufen. (eher zu dick, als zu dünn, aber möglichst nicht vom Galileo-Verlag)
Kleines Beispiel, das gleich zwei schwere Fehler enthält, welche sogar viele erfahrene C-Programmierer nicht entdecken werden:
#include <stdio.h>
char add(char a, char b) {
return a + b;
}
void main(void) {
printf("Result: %d\n", add(100, 30));
}
(Ich habe mal den Rückgabewert und die Argumente von main() als void, und die Argumente von add() nicht als const definiert, um das Beispiel so simpel wie möglich zu halten!)
Die add()-Funktion enthält zwei grobe Fehler, die beim Aufruf von add(100, 30) zu VIER verschiedenen Ergebnissen führen kann! Folgendes ist möglich:
Result: -126
Result: 127
Result: 130
[Ein plötzlicher Programm Absturz]
Die Fehler bestehen darin, dass alleine in dieser super simplen add()-Funktion an zwei Stellen undefiniertes Verhalten auftreten wird, weshalb das Ergebnis stark vom verwendeten Compiler, und der Plattform abhängen wird. Einer der Fehler ist übrigens für den Absturz einer Ariane 5 Rakete verantwortlich.
Wo genau die Fehler liegen, lasse ich jetzt aber mal offen als Rätsel! :)Falls jedoch der Fragensteller oder ein anderer Mitleser partout nicht auf die Lösung kommt, bitte einfach einen Kommentar schreiben! Ich werde dann eine Lösung mit Erklärung hinzufügen! :)
Viel Spaß beim Knobeln an alle Mitleser, und viel Erfolg beim C-Lernen an den Fragensteller! :)
PS: Egal was man lernt, die Lehrmaterialen sind entscheidend! Sogar die grausamen C-Bücher vom Galileo-Verlag, oder "C für Dummies" ist meiner Erfahrung nach immer noch besser als jedes Youtube Video! Wer anderer Meinung ist, kann mich ja gerne mal mit einem Youtube-Link überzeugen! Würde mich echt mal interessieren, ob es dort auch gute Tutorials gibt, die über die Basics hinausgehen, und nicht in jeder zweiten Code-Zeile Bad-Practices oder gar Fehler enthalten. :)
Die add()-Funktion enthält zwei grobe Fehler
Nope, die Funktion ist völlig ok, denn:
- Die Addition zweier char liefert int (ISO/IEC 9899:TC3 6.3.1.8 Usual arithmetic conversions), also sicher keinen Überlauf.
- Bei der Umwandlung von int nach char hat der Compiler im Fall eines Überlaufs etwas Freiheit (ebd. 6.3.1.3 listet zwei "implementation-defined" Alternativen).
Wenn ein Programmierer glaubt, sein Code würde sich anders verhalten als im Compiler-Handbuch dokumentiert, ist das nicht UB, sondern PEBKAC. Der Rakete ist das aber letztendlich egal :-(
OK, war ein schlechtes Beispiel. Habe versucht, zwei Fliegen mit einer Klappe zu schlagen, und "int add(int a, int b);" in das char-Äquivalent umgeschrieben.
Nach Abschnitt 6.3.1.3 Absatz 3 bleiben aber mehrere Möglichkeiten:
Otherwise, the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised.
Allein daraus ergeben sich mindestens 3 mögliche Ergebnisse für die Char-add-Funktion. (aber keine 4, so wie von mir behauptet)
Wenn ein Programmierer glaubt, sein Code würde sich anders verhalten als im Compiler-Handbuch dokumentiert, ist das nicht UB, sondern PEBKAC.
Du hast wirklich Recht, dass mein Beispiel Verwirrung stiftet, Ich hätte die int-summe-Funktion von NoHumanBeing nicht "verschlimmbessern" sollen. Trotzdem kann diese auch zwei verschiedene Ergebnisse liefern und ist somit nicht eindeutig. (allerdings sind so kleine Werte wie 100 und 30 natürlich absolut sicher)
Sowohl auf x86- als auch auf ARM- und PowerPC-CPUs (mit letzteren muss ich mich z.Z. beruflich beschäftigen) wird man die Fälle "Programmabsturz" und 127 nicht hinbekommen, sondern nur 130 oder -126.
CPUs, in denen die Fälle 127 oder Programmabsturz auftreten (wobei der Programmabsturz sogar noch zwei mögliche Ursachen haben kann) gibt es nur selten.
Den Fall 127 bekommst du mit Clang und Compilerflags auf den von dir genannten Systemen hin!
-126 ist der Standard bei den meisten Compilern auf den aktuellen und großen Desktop-Betreibssystemen, und 130 ist auch kein Problem.
127 und Programmabsturz sind sehr verbreitet im embedded Bereich, und den Programmabsturz sollte man eigentlich auch mit GCC und -ftrapv hinbekommen können, wenn diese Funktion nicht schon seit vielen Jahren aufgrund eines Bugs außer Funktion sein würde. :)
PPS: Das ist ja lustig! :)
Ich sehe gerade, dass jemand anderes in seiner Antwort auch eine summe() Methode als Beispiel verwendet hat. Und diese enthält den gleichen Fehler, den ich absichtlich in meiner Antwort angesprochen habe.
Durch die Verwendung von int statt char wurde zwar ein Fehler ausgemerzt, aber die summe()-Funktion von NoHumanBeing ist immer noch undefiniertes Verhalten, und die Ariane-Rakete wäre auch damit abgestürzt. :)
Wahnsinn! Ich bin Informatik-Masterstudent und habe schon einige Dinge (unter anderem eine GPU-gestützte Fluiddynamiksimulation) in C entwickelt. ;-)
130 kommt heraus, wenn ein "char" vorzeichenlos oder mehr als 8 Bit breit (Unicode?) ist?
127 kommt heraus, wenn ein "char" vorzeichenbehaftet und 8 Bit breit ist und "Sättigungsarithmetik" verwendet wird?
-124 kommt heraus, wenn ein "char" vorzeichenbehaftet und 8 Bit breit ist und "überlaufen kann"?
Den letzten werde ich mal etwas detaillierter erläutern.
100 dezimal ist in Binärdarstellung "01100100", 30 dezimal ist in Binärdarstellung "00011110". Also lautet die Addition "01100100" + "00011110" = "10000010" und das ist -126 wegen Zweikomplementdarstellung, denn alle Bits invertieren ergibt "01111101", inkrementieren ergibt "01111110" und das ist 126.
Zumindest eine richtig? Oder sogar alle? ;-)
Was kann abstürzen? Exception beim Überlauf? Schließlich werden ja keine Pointer oder so etwas verwendet, die "absturzkritisch" (oder gar sicherheitkritisch - "signedness bugs" bei Pointern werden gerne exploited) wären.
Nicht schlecht, (fast) alles richtig! :)
130 kommt heraus, wenn ein "char" vorzeichenlos oder mehr als 8 Bit breit (Unicode?) ist?
Das hast du selber zwar erkannt, aber ich fasse es nochmal kurz für die Mitleser zusammen: Und zwar ist es nicht definiert ob der Datentyp "char" vorzeichenbehaftet ist, oder nicht!
char => signed char ODER unsigned char
short => signed short
int => signed int
long => singed long
Ausschließlich bei "char" ist es dem Compiler überlassen, ob dieser "signed" oder "unsigned" ist. Bei allen anderen Integer-Typen bedeutet eine fehlende Angabe einfach nur "signed". Die meisten Compiler behandeln "char" zwar wie "signed char", unterscheiden diese Typen aber.
Im C-Standard steht auch seit C89 direkt drin, dass man zur Sicherheit "char", "signed char" und "unsigned char" als drei unterschiedliche Datentypen behandeln sollte, und auf gar keinen Fall davon ausgehen darf, dass "char" gleich "signed char" ist! :)
127 kommt heraus, wenn ein "char" vorzeichenbehaftet und 8 Bit breit ist und "Sättigungsarithmetik" verwendet wird?
Genau, für die Mitleser: Es gibt keinen Überlauf, sondern der Wert bleibt am Maximalwert des Datentyps stehen.
-124 kommt heraus, wenn ein "char" vorzeichenbehaftet und 8 Bit breit ist und "überlaufen kann"?
Genau, und das ist vermutlich auch das Ergebnis, mit dem ein Programmierer am ehesten rechnen würde.
Zumindest eine richtig? Oder sogar alle? ;-)
Bis jetzt alle. Zum letzten Punkt komme ich gleich ... :)
Was kann abstürzen? Exception beim Überlauf? Schließlich werden ja keine Pointer oder so etwas verwendet, die "absturzkritisch" (oder gar sicherheitkritisch - "signedness bugs" bei Pointern werden gerne exploited) wären.
Bei Additionen, Subtraktionen, etc. von vorzeichenbehafteten Integerwerten, die den Maximal-/Minimalwert überschreiten soll laut Standard der Compiler zwischen normalem Überlauf, Sättigung und Programmabbruch entscheiden.
Auf Desktops ist meistens ein einfacher Überlauf (mit Carry-Flag) der Fall, bei embedded Systemen und Mikrocontrollern hingegen passiert oft eine der letzten beiden Möglichkeiten.
Die Anekdote mit der Ariane-5 war übrigens, dass der Prozessor einen Überlauf festgestellt hat, und sich dann "zur Sicherheit" selbst abgeschaltet hat (sprich Programmabbruch).
Aber nicht schlecht! Eigentlich alles richtig! Respekt! :)
Wie "laufen eigentlich andere Operationen über", z. B. Multiplikationen? Auch so, dass "im Grunde ganz normal gerechnet würde", die "hochwertigsten Bits", die "nicht mehr hereinpassen" aber einfach "fallen gelassen" werden?
Zumindest würde das Sinn machen. Beim Shiften "fallen ja die höchstwertigen Stellen auch heraus", wobei "vorzeichenbehafteter Shift" natürlich auch wieder so eine Sache ist, weil negative Zahlen von links mit Einsen aufgefüllt werden. ;-)
Und Multiplikation mit einer Zweierpotenz sollte ja eigentlich das gleiche Ergebnis, wie ein Shift bringen. ;-)
Ich glaube Modulo-Operation bei negativen Zahlen ist auch so eine Sache. Wird zwar (soweit ich weiß) nicht "compilerabhängig" behandelt, ist aber in unterschiedlichen Sprachen unterschiedlich.
... weil negative Zahlen von links mit Einsen aufgefüllt werden. ;-)
Das ist ebenfalls undefiniertes Verhalten und darauf solltest du dich nicht verlassen! :)
Das hängt stark vom verwendeten Compiler und der Plattform ab.
Es kann also durchaus passieren, dass ...
uint32_t a = -1;
uint32_t b = a >> 16;
printf("%d\n", b);
... dir 65535 ausgibt! :)
Der Standard betont ausdrücklich, dass man niemals nie auf gar keinen Fall "signed" Werte shiften sollte, aber wenn doch, dann vorher in das "unsigned" Äquivalent casten! :)
Da C auch nicht nur das Zweierkomplement vorgibt, sondern Einerkomplement und "Sign and Magnitude" ebenfalls für die Darstellung von negativen Ganzzahlen erlaubt sind, ist also auch gar nicht klar, welchen Wert du letzendlich erhältst, selbst wenn von links lauter Einsen eingeschoben werden würden. :)
Ich glaube Modulo-Operation bei negativen Zahlen ist auch so eine Sache. Wird zwar (soweit ich weiß) nicht "compilerabhängig" behandelt, ist aber in unterschiedlichen Sprachen unterschiedlich.
Gerade bei Modulo und Division muss man auch auf einen Überlauf achten, was viele Leute vergessen, die nur auf "Division durch Null" prüfen.
Probier mal folgendes:
INT_MIN / -1
Da im Zweierkomplement der negative Bereich um eins größer ist, als der Positive, und "negativ durch negativ" etwas positives ergibt, gibt es bei obiger Berechnung eine Exception, und das Programm stürzt ab, als würde man durch 0 dividieren. :)
Also zur Sicherheit folgendes implementieren (wenn a, und b "signed int" sind):
if (b == 0 || (a == INT_MIN && b == -1)) {
abort(); /* Fehler */
} else {
c = a / b;
}
Wie gesagt, den zweiten Teil der if-Bedingung vergessen viele Leute, obwohl das regelmäßig in Exploits ausgenutzt wird.
OK, ... ich habe dir jetzt nicht alles beantworten können, habe jetzt aber auch leider keine Zeit mehr! Vielleicht kann dir jemand anderes noch die offenen Fragen beantworten!
Schönen Abend noch, und viel Erfolg bei deinem Studium! :)
test
INT_MIN / -1
Also ich habe schon genügend Plattformen/Compiler gesehen, auf denen ...
(INT_MIN) / -1 == INT_MIN
... wahr war.
Das ist auch wieder das, was man als Informatiker am ehesten erwarten würde. Alles invertieren ergibt INT_MAX. Eins addieren ergibt INT_MIN. Und es ist meines Erachtens auch "das wenig schlimmste, was er machen kann", weil ja so immerhin der Betrag stimmt und "nur" das Vorzeichen "geschluckt" wurde.
Eine Frage hätte ich noch.
Der Standard betont ausdrücklich, dass man niemals nie auf gar keinen Fall "signed" Werte shiften sollte, aber wenn doch, dann vorher in das "unsigned" Äquivalent casten! :)
Warum definiert man denn in einem "Standard", dass bestimmte Operationen undefiniertes Verhalten haben können und daher nicht genutzt werden sollen, anstatt einfach das Verhalten zu definieren?
Es sollte doch ein leichtes sein, festzulegen, wie "signed"-Werte geshiftet werden sollen und den Compiler (der sich an die Spezifikation hält) dann ein Stück Maschinencode "ausspucken lassen", das äquivalent zu "Cast auf unsigned, Shift, Cast zurück" ist. Warum lässt man Verhalten undefiniert, anstatt zu sagen "signed shift ist immer exakt so zu implementieren"?
Du solltest dich unbedingt (!!!) mit Cert-C und MISRA auseinander setzen, falls du das nicht schon hast.
Das sind Guidelines für "gutes Programmieren" und Fehlervermeidung. Diese sind in Kategorien unterteilt (Präprozessor, Floatingpoint, Speicheralloziierung, etc.) und enthalten zu jedem Punkt mindestens ein schlechtes Codebeispiel, und meistens mehrere gute Workarounds.
Wie auch immer, eine Beschreibung von MISRA und Cert-C würde hier den Rahmen sprengen, aber guck dir bitte mal folgenden Anhang CC über "Undefined Behavior" von Cert-C an:
https://www.securecoding.cert.org/confluence/display/c/CC.+Undefined+Behavior
Anhang DD bezieht sich auf "Unspecified Behavior" und an anderer Stelle wird auf "Implementation Defined Behavior" eingegangen. Das sind alles Listen mit Punkten, die in C und C++ - aus welchen Gründen auch immer - undefiniert sind.
C und C++ sind beide sehr schön, haben ihre Stärken und Schwächen, aber sind natürlich nicht perfekt. Auch Standards wie POSIX haben Funktionen wie realpath() komplett verhauen und trotz allen mir bekannten Bibliotheksfunktionen für die Erstellung von Temporären Dateien, können trotzdem Race-Conditions auftreten! (tmpfile, mkstemp, oder open mit O_CREAT und O_EXCL)
Cert-C und MISRA sind Empfehlungen, mit denen man nicht einverstanden sein muss. Vor allem MISRA schreibt eindeutig in der Einleitung, dass es sich nur um "Empfehlungen" handelt, und nichts blindlinks angewendet werden sollte. Das gleiche gilt für Cert-C. Zu letzterem würde ich dir das Buch empfehlen, auch wenn das logischerweise nicht ganz so aktuell und auch nicht so vollständig wie die Online-Ausgabe ist.
Ich wette, in sehr sehr vielen Punkten von Cert-C wirst du ein Aha-Erlebnis haben, und etwas verächtlich auf deinen bisherigen Code blicken. Einige Empfehlungen öffnen einem wirklich die Augen!
MISRA-C und MISRA-C++ kannst du dir als PDF für 15 britische Pfund online kaufen.
Ich würde dir hier gerne weitere Links posten, aber GF hat einen wohl durchdachten Spamschutz, und deshalb geht das leider nicht. Also googel einfach mal nach "Cert-C" und "MISRA" oder guck ggf. bei Wikipedia und Amazon nach. Sorry!
Also ... lies dir am besten Cert-C und MISRA bzgl. C und C++ komplett durch. Das ist eine Menge, und wird nicht von Heute auf Morgen gehen, aber wenn du rein haust, bist du in ein bis zwei Wochen damit fertig, und hoffentlich ein sehr viel besserer Programmierer geworden. :)
Dazu kann ich dir noch den ANSI/ISO/IEC Standard für C und C++ empfehlen, und zwar in allen Versionen seit C89 inklusive den ganzen Updates (vor allem Annex K). Dann erhältst du einen guten Überblick über die Evolution von C, und weist genau, wann welcher Compiler für welche Version auf welcher Plattform was genau unterstützt und was nicht. (siehe undefiniertes Verhalten)
Also dann ... viel Spaß beim Lesen! :)
Variablen in Funktionen und Unterprogrammen lokal statisch
static ist wohl das meistmissbrauchte Schlüsselwort in C: Eine damit definierte lokale Variable existiert nur einmal. Sie wird beim ersten Funktionsaufruf initialisiert und behält ihren Wert am Funktionsende für den nächsten Aufruf bei:
int func ( void )
{
static int i = 42;
return ++i;
}
void main ( void )
{
print( "%d ", func() );
print( "%d ", func() );
}
produziert die Ausgabe 42 43. Dasselbe erhält man effizienter, wenn i außerhalb von func() definiert wird. Die obige Konstruktion ist eigentlich nur interessant, wenn der Zeitpunkt der Initialisierung wichtig ist.
Beispiele für Funktionen:
Die Grammatik einer Funktionsdefinition will ich jetzt nicht korrekt darstellen (müsste mich erst einlesen wie man es korrekt formuliert), aber im Wesentlichen (als pseudo-regex-wasauchimmer-Hybrid):
Type FunctionName "(" Argument+ ")
"{"
"return" Type ";"
"}"
"void" FunctionName "(" Argument+ ")"
"{"
"}"
wobei Argument wie folgt definiert werden kann:
"void" || (Type Identifier)
Wie gesagt...mein EBNF ist etwas eingerostet (und das hier ist weit von EBNF entfernt - teilweise sogar offensichtlich falsch weil ich das Komma in der Argumentliste weggelaasen habe, sollte aber die Idee klar machen), für korrekte Details kann man ja in Google nach der C-Grammar nachschlagen.
void func1(void)
{
printf("func1\n");
}
Das void in der ArgumentListe ist im ANSI-C vorgegeben, kommt halt auf den Compiler an ob er klar kommt das void wegzulassen (für C++ bitte kein void an der Stelle, halte ich für schlechten Stil)
void func2(int arg1, int arg2)
{
printf("Arg1=%d, Arg2=%d\n", arg1, arg2);
}
int func3(int arg1, int arg2)
{
return arg1+arg2;
}
Aufruf dann ganz trivial:
int x;
func1();
func2(1, 5);
x = func3(3, 7);
Statische Variablen kannst du mit dem Schlüsselwort 'static' definieren:
void func4(void)
{
static val=1;
printf("%d\n", val++);
}
Das würde dann bei folgendem Code:
func4();
func4();
folgende Ausgabe geben:
1
2
Die letzte Frage verstehe ich ehrlich gesagt nicht so ganz.
Ich nehme an du beziehst dich auf Werte wie val in func oder arg1/arg2 in func3. Dafür müssen keine Variablen schon im Hauptprogramm angegeben werden, bzw es ist sogar so, dass es keinen Einfluss hat. Der Gültigkeitsbereich einer Variable ist immer auf den Block beschränkt (der idR durch { } eingegrenzt wird (und seine "Child-Blocks")
Beispiel:
void func5(int arg)
{
int x1 = 1;
printf("%d\n" x1+arg); //arg und x1 sind im Scope
{
int x2 = 2;
printf("%d", x1+x2+arg); //passt auch alles
}
printf("%d", x1+x2+arg); //x2 ist in dem Scope nicht bekannt
int val = 6;
func4(); //der vorherige befehl hat keinerlei Auswirkung
printf("%d", val); //Ausgabe: val, egal wie oft func4 aufgerufen wurde
}
Ich hoffe das klärt alle Fragen.
Wäre vllt noch interessant zu kommen aus welcher Sprache du kommst, um weitere Details zu geben (ANSI-C hat so einige Eigenheiten)
PS: Gut formatierte und konkret gestellte Frage, das wünsche ich mir öfters auf GF (abgesehen vom letzten Punkt, der mir nicht 100% klar war, kann aber an mir liegen)
Ups, grad gesehen...func5 ist nicht ANSI-C. Die Zeile
int val = 6;
ist an der Stelle nicht erlaubt. Variablendeklarationen müssen an den Anfang von einen Block...eine der Eigenheiten von ANSI-C (die ich auch gerne mal vergesse)
Das wusste ich gar nicht. Faktisch nimmt das aber jeder moderne C-Compiler an.
Ich wusste nur, dass die in C++ bestehende Möglichkeit, Variablen im Kopf einer Schleife zu deklarieren, in ANSI-C nicht möglich ist (und von C-Compilern in der Regel auch zurückgewiesen wird).
Also ...
int i;
for (i = 0; i < 10; i++)
printf("%i", i);
... statt ...
for (int i = 0; i < 10; i++)
printf("%i", i);
Warum auch immer da plötzlich Leerzeilen zwischen den einzelnen Codezeilen sind. Die hab ich nicht eingegeben. Naja, wie auch immer. Whitespace hat ja in C zum Glück keine semantische Bedeutung.