Arbeiten mit Objektdateien

Manch einer, der schonmal Quelltexte eines Programms heruntergeladen und kompilert hat, mag sich gefragt haben, warum das so massig viele Dateien sind. Von Übersichtlichkeit kann ja keine Rede sein, wenn man sich als Fremder damit befasst. Ich suche erstmal ewig nach der Hauptdatei, hüpfe von einem #include zum nächsten, finde die gesuchte Funktion nicht und habe keine Ahnung, was das ganze soll.
Beim Kompileren merkt man, daß ewig lange Compileraufrufe gemacht werden, von denen ich gerade die Hälfte verstehe. Hab ich doch bisher ganz gut damit gelebt, eine Hauptdatei zu haben, in der ich Code schreibe, ferner vielleicht andere Funktionen in Headerdateien ausgelagert, damit ich sie in anderen Programmen verwenden kann.

Programmierer vor allem größerer Projekte bauen zwar komplexe Strukturen auf, die aber auf den zweiten Blick sehr sinnvoll erscheinen:
Sie lagern ihre Funktionen nicht komplett in Headerfiles aus sondern in Object-Dateien. Objectdateien sind bereits kompilierte Dateien, die aber nicht direkt ausführbar sind, sondern ins Hauptprogramm eingebunden werden. Dies erledigt der Linker. (Nebenbei: printf ist z.B. eine solche Funktion, nur kriegt man davon nix mit.)
Durch dieses Vorgehen wird die Übersichtlichkeit größer, und man muss nicht jedesmal wenn ein Änderung im Hauptprogramm gemacht wurde, unveränderten Code mitübersetzten. Bei größeren Projekten bringt dies einen Geschwindigkeitsvorteil.
Ausserdem kann man diese Objectdateien (enden meist auf *.o) ohne Neukompilation in jedem anderen Programm (auf dem selben Computer!) mitbenutzen.
Theoretisch könnte man die Funktion ja auch in ein headerfile schreiben, aber dann wird eben statt dem #include "headerfile.h" der Inhalt der Headerdatei geschreiben. Folge: Eine einzige riesige Datei, die übersetzt werden muss. Jedesmal. Wer will, kann sich diese mal anschauen, wenn er folgendes kleines Programm schreibt (hello.c):

#include <stdio.h> 
#include "testlib.h" 
 
int main(){   
  printf("Hello World"); 
  return 0; 
} 

und mittels

user@sonne> gcc -E hello.c -o hello.txt 

den Quellcode dieser Datei erzeugt (gcc -E wendet nur den Präprozessor an). Jetzt schaut Euch in der Datei hello.txt an, wieviele Sachen über dem eigentlichen Code aus diversen Headerfiles da drinstehen. Und bisher habe ich nur einen Header angegeben. Was auffällt ist, das von irgendwelchen Funktionen immer nur der Rumpf vorhanden ist, kein Code. Die Antwort darauf kommt aber später, jetzt machen wir ein Beipiel mit Objects:

Unser Hauptprogramm (test.c) sieht so aus:

#include <stdio.h> 
 
int main(){   
  printf("Addition von 4 und 6 ergibt: %i\n", addition(4,5)); 
  return 0; 
} 

Ich weiß, dass es keinen sinn macht, eine Funkion zu schreiben, die sich "addition" nennt, aber es ist ja nur ein Beispiel.

Jetzt zu unserem Object (testlib.c):

int addition(int a, int b){   
  return (a+b); 
} 

Und unser Headerfile (testlib.h) sieht so aus:

int addition(int,int); 

Zum Vorgehen:


mit gcc -c testlib.c wird testlib compilert,und als testlib.o abgespeichert. Wichtig ist die Optio n -c, damit nur der Compiler aktiv wird, nicht der Linker. Der Linker hat später nämlich die Aufgabe, nach dem Compilieren des Hauptprogramms die Libraries/Objects, in denen die benötigten Funktionen stehen, an das Programm zu binden.

Jetzt könnte man probieren, mit gcc test.c -o test das Programm zu kompilieren, was aber fehlschlagen wird. Man m uss hier nämlich aufpassen, dass man den Namen des Objects als Argument mitübergibt:
 

user@sonne> gcc test.c testlib.o -o test

muss der Aufruf also heißen. Das funktioniert übrigens auch mit mehreren Objektdateien.

Jetzt mag man sich fragen, wozu eigentlich noch Headerfiles gut sind. Wenn man die Funktion schon in einer separaten Datei hat, steht doch die Funktion ohnehin nur mit dem Rumpf im Headerfile. Der Grund ist folgender: Dem Compiler ist das (nur bei Funktionen!) egal. Der übersetzt weiterhin wie gehabt und nimmt für unbekannte Funktionen den Rückgabewert int an.
Allerdings kann der Compiler hier keine Warnungen über typeninkompatiblitäten ausgeben, man kann sich also stundenlang damit beschäftigen den (logischen, nicht syntaktischen) Fehler zu suchen. Deshalb werden dem Compiler mittels Prototypen die Funktionen mit ihren Datentypen mitgeteilt.
Diese Prototypen lagert man der Übersichtlichkeit halber in Headerfiles aus, da ein object üblicherweise nicht nur aus einer Funktion besteht. Hier sollte auch nicht mit Kommentaren gespart werden. Die meisten Headerfiles sind selbstdokumentierend gehalten und sind auch das, was ein anderer Programmierer anschaut, wenn er wissen will, welche Funktionen vorhanden sind, bzw welche Parameter erwartet werden.
Wenn man dementsprchend viele objectdateien zu übersetzten hat, können sehr lange Compileraufrufe vonnöten sein. Um sich bei etwaigen Änderungen Tipparbeit zu sparen, kann man sich auch näher mit makefiles befassen, die einem hier zur Hand gehen. Makefiles sind auch das, was man benutzt, wenn man irgendwo ein Programm im Quellcode runterlädt und installiert. Man könnte auch jede Datei von Hand kompileren. Der Grund, wieso die Objecte hier auch übersetzt werden müssen, ist der, daß man ein Object, das auf einem System kompiliert ist, nicht einfach auf ein anderes System übertragen kann. (Das kann im einzelnen Fall aber funktionieren).
 


Anmerkend bleibt noch zu sagen, dass man nicht wegen jeder Kleinigkeit eine neue Objectdatei planen sollte, man sollte diese vielmehr zu sinnvollen Einheiten zusammenzufassen, z.B. eigene Stringoperationen in eine Datei. Sonst verzettelt man sich leicht, und findet sich in seinem eigenen Programm nicht mehr zurecht.
Weiterführende Literatur: http://www.fh-hamburg.de/rzbt/dankert/c_tutor.html

Autor: Baitronic