namespace cpp

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


kennen:module

Module

Divide et impera.
— Römisches Sprichwort

Hinweis: Diese Seite beschreibt C++98 bis C++17, nicht das neue Modulkonzept, welches eventuell mit C++20 eingeführt werden soll.

Organisation

Modularisierung ist eine Technik zum Aufbrechen und Beherrschen der Komplexität von Programmieraufgaben. Die Abgrenzung von Programmteilen kann

Inhaltlich zusammengehörige Datentypen und Funktionen lassen sich als Modul in einer Quelltextdatei bündeln. Vorspanndateien (Header) enthalten Deklarationen und Definitionen, z.B. Funktionsprototypen, die zur Nutzung der Moduls notwendig sind. Die Kommunikation mit dem Modul sollte nur über die dadurch festgelegte Schnittstelle erfolgen (Geheimnisprinzip).

Bei der Übersetzung entstehende Objektdateien können mit Bibliotheken zum ausführbaren Programm (Binärdatei) zusammengeführt werden. Das Modul kann als Objektdatei oder als Bibliothek weitergegeben und in mehrere Programme eingebunden werden, ohne geändert oder neu übersetzt werden zu müssen. Bibliotheken sind Sammlungen von Objektdateien, die mit einem Bibliothekar oder Archivar genannten Programm zusammengestellt und gepflegt werden. Auf der Ebene der Objektdateien und Bibliotheken sind Programmteile aus unterschiedlichen Programmiersprachen kombinierbar.

Die Dateinamen besitzen je nach System typische Endungen:

Headerdatei *.h, *.hpp, *.hxx
Quelltext *.cc, *.cpp, *.cxx
Objektdatei *.o, *.obj
Bibliothek lib*.a, lib*.so, *.lib, *.dll
Programm a.out, *.exe

Namensräume

Namenskonflikte können die Integration von Modulen vereiteln, wenn derselbe Name in ihnen mehrfach definiert wird. Namensräume (Schlüsselwort namespace) begrenzen die Verschmutzung des globalen Namensraumes.

Syntax:

namespace Namensraumbezeichner
{
Deklarationen
Definitionen

}

Namensbereiche sind schachtelbar. Ein Namensraum darf mehrfach, auch in verschiedenen Dateien, geöffnet, erweitert und geschlossen werden. Der Namensraum std ist jedoch als abgeschlossen zu betrachten. Unbenannte Namensräume kapseln globale Namen, die nur innerhalb eines Moduls sichtbar sein sollen (ohne externe Bindung). Lange Namensraumbezeichner können durch kürzere ersetzt werden:

namespace fs = std::filesystem;

Namen sind außerhalb ihres Namensraumes qualifiziert mit dem Namensraumbezeichner anzugeben. Einzelne Namen und ganze Namensräume können mit der using-Anweisung in einen Block oder global importiert werden.

std::cout << "Hallo" << std::endl;
 
using std::cout;
using namespace std;
cout << "Hallo" << endl;  // nun ohne std:: nutzbar

Modulschnittstellen werden vor der Nutzung durch Einbinden von Deklarationsdateien (Header) bekannt gemacht. Standard-Header werden in spitzen Klammern angegeben. Eigene Header in Gänsefüßchen werden zuerst bei den Quelltexten (im aktuellen Projekt-Verzeichnis) gesucht, danach in den Standard-Include-Verzeichnissen.

#include <Standardheader>
#include "mein_header.h"

Syntaktisch korrekte Header können in beliebiger Folge angegeben werden. Abhängigkeiten zwischen den Headern sollten in den Headern geregelt werden. Erfordern Deklarationen eines Headers die Kenntnis anderer Schnittstellen, muss dieser die anderen Header einbinden. Eventuelles mehrfaches Einbinden soll nicht zu Übersetzungsfehlern führen. Header werden deshalb mit "Include-Wächtern" versehen, deren eindeutige Namen dies durch bedingte Übersetzung verhindern:

#ifndef MEIN_HEADER_H
#define MEIN_HEADER_H
// ... Abhängigkeiten 
// ... Deklarationen
#endif // MEIN_HEADER

Viele Compiler erlauben als Ersatz für den Include-Wächter auch die Nutzung der nicht standardisierten Präpozessoranweisung #pragma once.

Bindung

In einer Quelltextdatei definierte Funktionen und globale Variablen haben interne Bindung, wenn sie mit dem Spezifizierer static oder in einem namenlosen Namensraum deklariert wurden. Auf Bezeichner mit interner Bindung kann nur innerhalb dieses Quelltextes Bezug genommen werden. Definierte gleichnamige Bezeichner mit interner Bindung existieren in verschiedenen Übersetzungseinheiten unabhängig voneinander.

Alle anderen Funktionen und globalen Variablen haben externe Bindung. Sie dürfen im Programm nur ein einziges Mal definiert sein (one definition rule). Funktionen mit externer Bindung können aus anderen Quelltexten heraus aufgerufen werden. Lediglich ihr Prototyp muss zum Aufruf bekannt sein. In anderen Dateien definierte globale Variablen mit externer Bindung sind nutzbar, nachdem sie mit dem Spezifizierer extern angemeldet wurden.

extern int global;      // modul1.h
int minimum(int, int);
int global;             // modul1.cpp
 
int minimum(int x, int y) 
{
  return x<y ? x : y;
}
#include "modul1.h"
 
int main()              // main.cpp
{
  global = minimum(0, 1);
  return global;
}

Anmerkung: Im allgemeinen sollten globale Variablen vermieden werden. Sie stellen ein Problem bei der Wartung und bei der Parallelisierung von Programmen dar.

Sollen Funktionen aus / in anderen Programmiersprachen aufgerufen werden, muss der Compiler darauf vorbereitet werden, weil unterschiedliche Sprachen unterschiedliche Konventionen der Parameterübergabe, Wertrückgabe und Stackbereinigung haben. Da andere Programmiersprachen bestimmte Merkmale von C++ nicht unterstützen, sind Überladen u.ä. für solche Funktionen nicht nutzbar.

extern "C" int minimum(int, int);

Übersetzung

<...> \
*.h    => Präprozessor => Compiler => *.o  => Linker => Programm
*.cpp /                                |        ^
                                 Archivar <=> Bibliotheken

Ein Programm entsteht in einem mehrstufiger Prozess, auch wenn dieser mit einem einzigen Befehl angestoßen wird.

  1. Für jede Quelltextdatei (Übersetzungseinheit) einzeln (getrennte Übersetzung):
    1. Vorverarbeiten mit dem Präprozessor cpp:
    2. Übersetzen: Mit dem Ergebnis der Vorverarbeitung wird der Compiler gefüttert.
      Für jeden erfolgreich übersetzten Quelltext entsteht eine gleichnamige Objektdatei.
  2. Verbinden: Objektdateien und Bibliotheken werden vom Linker zum ausführbaren Programm zusammengeführt.

Projektverwaltung

Die Übersetzung und Projektverwaltung erfolgt systemabhängig

  • über Kommandozeilenwerkzeuge und Steuerdateien oder
  • in integrierten Entwicklungsumgebungen (z.B. Code::Blocks).

Integrierte Entwicklungsumgebungen sind Arbeitswerkzeuge, die den Programmierer bei der Erstellung von Quelltexten und Programmen, bei der Fehlersuche und Dokumentation unterstützen (können), wenn man weiß, wie. Jede Entwicklungsumgebung wird anders bedient. Die Beschreibung einzelner Entwicklungsumgebungen erfolgt hier nicht. Zudem erfordern Entwicklungsumgebungen zumeist die Anwesenheit eines Tastatur und Maus bedienenden Programmierers. Mausklicks sind schwer automatisierbar. Dagegen können Kommandozeilenbefehle leicht in Skripte gefasst und automatisiert werden (GNU-Werkzeuge g++ und make als Beispiel).

Übersetzen an der Kommandozeile

Ein Programm aus 2 Quelltexten wird erstellt:

g++ -o progname main.cpp modul1.cpp

Fehlt die Angabe -o progname (o wie output), findet sich die ausführbare Datei als a.out ("Assembler Output").

Die Übersetzung einzelner Quelltexte erfolgt mit dem Schalter -c (compile):

g++ -c main.cpp
g++ -c modul1.cpp

Die entstehenden Objektdateien werden zum Programm zusammengeführt (gelinkt):

g++ -o progname main.o modul1.o

Erfordert ein Programm bestimmte Bibliotheken (lib*.a oder lib*.so), ist beim Linken -l name anzugeben, Der name ist dabei der nach code|lib| stehende Teil des Bibliotheksnamens, z.B. ist libm.a die Mathematik-Bibliothek.

g++ prog.cpp -lm

Die Kombination unterschiedlichster Übersetzungsstufen ist erlaubt:

g++ -o progname main.cpp modul1.o -lm

Archivieren in Bibliotheken

Mit dem Archivar ar lassen sich Bibliotheken aus Objektdateien zusammenstellen, pflegen und bereinigen. Die Symboltabelle für den Linker wird mit dem Befehl ranlib erzeugt. Zur Dokumentation siehe man ar bzw. man ranlib.

Automatisieren mit make

Das Werkzeug make unterstützt die Automatisierung. Mit einem einfachen Kommando

make

kann ein Projekt aktualisiert werden. Der Aufruf

make test

könnte einen Regressionstest (Fehlerprüfung nach Wartung) veranlassen. Mit der Zielangabe

make clean

könnten Zwischendateien beräumt werden. Voraussetzung ist eine Datei Makefile im (aktuellen) Projektverzeichnis. Beim Anlegen der Datei ist darauf zu achten, dass Tabulatoren auch wirklich als Tabs in der Datei gespeichert werden. Im Makefile werden die verschiedenen Ziele definiert.

Syntax:

Ziel : Tabulator Voraussetzung
Tabulator Befehl

Zu jedem Ziel wird angegeben, welche Voraussetzungen oder Abhängigkeiten erfüllt sein müssen, um die nachfolgenden Befehle ausführen zu können.

Zuerst versucht make, alle Voraussetzungen zu erfüllen. Voraussetzungen sind Teilziele oder Dateien. Ist eine Ausgangsdatei jünger als die Zieldatei, müssen die zu dem Ziel angegebenen Befehle ausgeführt werden. Bei aktuellen Dateien unterbleibt die Ausführung der Befehle, und das Ziel gilt als erfüllt. Tritt irgendwo ein Fehler auf, bricht make den Aktualisierungsprozess ab.

# Kommentar - Beispiel Makefile
 
all :   progname
 
progname :      main.o modul1.o
        g++ -o progname main.o modul1.o
 
main.o :        main.cpp
        g++ -c main.cpp
 
modul1.o :      modul1.cpp
        g++ -c modul1.cpp
 
# Regressionstest
test :  progname input.txt soll.txt
        progname < input.txt > output.txt
        diff output.txt soll.txt
 
# Saubermachen
clean :  
        rm *.o output.txt

Der make-Aufruf ohne Parameter führt das oberste Ziel aus. Bei unbekannten Zielen gibt sich make spröde:

make love
make: *** No rule to make target 'love'. Stop.
kennen/module.txt · Zuletzt geändert: 2019-01-13 14:45 von rrichter