let’s dev

Blog

Top!

let’s dev | Scroll to top
let’s dev | Scroll to next content item

Hello

let’s dev | Scroll to previous content item
let’s dev Blog | Smart Pointer in C++
by Matthias
01. April 2018

Smart Pointer in C++

Mit diesem Blogbeitrag soll ein Einblick in die Verwendung von Smart Pointern (Shared Pointern in C++) und die wichtigsten Grundlagen gegeben werden. Die nachfolgenden Erläuterungen zielen darauf ab, einen schnellen Einstieg in diese Thematik zu ermöglichen.

Einleitung

Im Einführungskapitel wird zunächst die Notwendigkeit von Shared Pointern verdeutlicht und zu deren Verwendung motiviert. Nicht ohne Grund sind Shared Pointer bei einigen höheren Programmiersprachen wie z.B. das ARC (Automatic Reference Counting) bei Objective-C ein Standard Sprach-Feature der Programmiersprache.

Smart Pointer in C++

Lesern, die bereits Grundkenntnisse besitzen und sich für ein lauffähiges Code Snippet interessieren, empfehlen wir, direkt bei Kapitel 2 Grundlagen der shared_ptr einzusteigen. Darin wird anhand von Beispielen die Verwendung der Shared Pointer erklärt.

Einführung - Das Mysterium der Shared Pointer in C++

“Warum kann man nicht einfach Shared Pointer verwenden? Shared Pointer sind eine sehr schlaue Methode zur Verwaltung von Pointern und des Speichers.”

Diese Aussage hört man sehr häufig. Aber werden Shared Pointer bereits wirklich im größeren Stil und konsequent in Projekten eingesetzt? Im Austausch mit Entwicklern gewinnt man schnell den Eindruck, dass Shared Pointer nicht besonders weit verbreitet oder populär sind. Woran liegt das? Gibt es Probleme, die bei der Verwendung häufiger auftreten? Sind Shared Pointer zu kompliziert zu verstehen oder ist es zu aufwendig, diese zu benutzen?

Wir hoffen, dass dieser Beitrag dabei helfen wird, einige der Fragen um das Mysterium Shared Pointer zu klären und ein gutes Beispiel dafür geben zu können, wie man diese korrekt verwendet.

Einführung - Eine Analogie zu Shared Pointern

Anstatt direkt in technische Details zu springen, wird die Verwendung von Shared Pointern zunächst anhand eines Beispiels aus dem echten Leben erläutert:

Stellen Sie sich vor, Sie arbeiten in einer Firma und es gibt eine einfache Regelung für das Bürogebäude: der letzte Mitarbeiter, welcher das Gebäude verlässt, muss die Eingangstür zum Gebäude verriegeln und die Alarmanlage aktivieren. Falls jemand das Gebäude verlässt und die Alarmanlage ist bereits aktiviert, dann wird der Alarm ausgelöst. Deswegen sollten Sie hier genau prüfen, ob sich noch jemand anderes im Gebäude befindet, bevor Sie die Alarmanlage aktiv schalten.

Angenommen, das Bürogebäude besteht nur aus einem kleinen Zimmer und es gibt nur fünf Mitarbeiter, dann ist es für Sie sehr einfach festzustellen, ob Sie der letzte Mitarbeiter sind, der sich noch im Gebäude befindet. Stellen Sie sich jedoch vor, Sie arbeiten in einem riesigen Bürogebäude mit mehreren Stockwerken und vielen hunderten Mitarbeitern. Um hier noch zu wissen, ob Sie die letzte Person im Gebäude sind, müssten Sie alle Stockwerke und alle Zimmer absuchen. Nun überlegen Sie sich, jemand anderes sucht zur gleichen Zeit das Gebäude ab, um nachzusehen, ob noch jemand anderes im Gebäude ist und sie beide verfehlen sich dadurch. Am Ende schließen Sie aus Versehen die andere Person im Gebäude ein, mit aktivierter Alarmanlage. Was für ein Chaos!

Aber was wäre, wenn es eine Art “Smart Door”, also eine Art “schlaue Eingangstür” zum Gebäude gäbe, welche +1 für jede Person addiert, die das Gebäude betritt und -1 subtrahiert für jede Person, die das Gebäude verlässt. Sobald der Zählerstand also den Wert 0 erreicht, wird die Alarmanlage automatisch aktiviert.

Wäre das nicht eine großartige Lösung für dieses Problem? Die Idee, welche sich hinter den Shared Pointern verbirgt, ähnelt der “Smart Door” aus dem erläuterten Beispiel.

Einführung - Die Verwendung von C++ shared_ptr

Bei der Programmierung in C++ ist es oft nicht einfach, den Überblick über die Speicherverwaltung des gesamten Source Codes eines Projektes zu behalten. Sobald ein C++ Pointer erzeugt wurde, kommen häufig folgende Fragen auf:

Als simple Antwort auf diese Fragen folgt dann meist folgende Aussage:

“Der Pointer wird an der Stelle im Programm gelöscht, in der er als letztes verwendet wurde.”

Aber wie in dem Beispiel aus 1.2, in dem nicht eindeutig bestimmt werden kann, wieviele Leute sich in einem großen Gebäude befinden, und wo genau diese sich aufhalten, so ist es keineswegs einfach, den Überblick darüber zu behalten, wohin alle Pointer in einem Programm weitergereicht wurden.

"Woher weiß man, welches die letzte Stelle ist, an der ein Pointer verwendet wurde?"

Ein Pointer könnte unter Umständen durch mehrere Konstruktoren oder Methoden Parameter weitergereicht worden sein. Außerdem könnte es sein, dass man mit mehreren Teamkollegen zusammen am selben Source Code arbeitet.

Vergleicht man die Behandlung von Pointern in C++ mit dem Beispiel aus 1.2, dann wäre ein Aufruf des delete Befehls auf einen Pointer, für welchen der Speicher bereits freigegeben wurde, gleichbedeutend mit dem Versuch, das Gebäude zu verlassen, in welchem bereits die Alarmanlage aktiviert wurde. Sobald man versucht, die Tür von innen zu öffnen, würde die Alarmanlage ausgelöst und bei den Pointern das Programm abstürzen.

Welches die letzte Stelle ist, an der ein Pointer verwendet wurde und zu wissen zu welchem Zeitpunkt man delete aufrufen sollte, ist keine Frage die sich so einfach beantworten lässt, sofern es eine dritte Instanz gibt, welche sich um diese Aufgabe kümmert. Und mit dieser dritten Instanz kommen Shared Pointer, wie der std::shared_ptr ins Spiel.

Der Shared Pointer weiß genau, welches die letzte Stelle ist, an der ein Pointer verwendet wurde und sobald ein Pointer zum letzten Mal im gesamten Programm verwendet wurde, wird er automatisch deleted. Hierfür addiert er +1. Dies geschieht jedes Mal, wenn jemand eine Kopie erzeugt, also z.B. wenn der Shared Pointer in einer Methode oder in einem Konstruktor als Parameter übergeben wird. Des Weiteren wird immer -1 subtrahiert, wenn das Objekt, welches den Shared Pointer verwendet hat, gelöscht oder die Methode, welche den Shared Pointer verwendet hat, beendet wurde. Sobald der Wert 0 erreicht wird, wird der Pointer automatisch gelöscht und der belegte Speicher wieder freigegeben.

Grundlagen der shared_ptr

Nachfolgend werden nun einige Code Beispiele präsentiert. Aufgrund der Anschaulichkeit wird auch hier nochmal das Büro Beispiel aus Kapitel 1.2 aufgegriffen:

Es wird zunächst eine Klasse OfficeBuilding definiert, also ein Gebäude, welches von Mitarbeitern betreten werden kann. Das bedeutet, ein OfficeBuilding kann mehrere Instanzen eines Mitarbeiter Objektes (Employee) referenzieren. Und als kleines, zusätzliches Feature gibt es in dem Gebäude ein WC (Washroom), welches von genau einem Mitarbeiter betreten werden kann.

Datenbankrelationen

Grundlagen der shared_ptr - Instanziierung eines shared_ptr

Möchte man z.B. eine Instanz eines Objektes Employee erzeugen, dann würde man normalerweise einen Pointer mit Hilfe des Schlüsselworts new instanziieren, um Heap-Speicher zu allokieren. Das Schlüsselwort delete würde man dazu verwenden, um den Speicher wieder freizugeben:

Employee  *employee1 = new Employee( 1, “Peter Parker”);
delete employee1;
Diese beiden Zeilen Code werden nun ersetzt durch die Folgende:
shared_ptr<Employee> employee1 = make_shared<Employee>(1, "Peter Parker");

Die linke Seite der Zuweisung shared_ptr<Employee> employee1 hält sich eine Instanz der Klasse Employee und anstatt das Schlüsselwort new zu verwenden, verwendet man das Schlüsselwort make_shared, um diese Instanz zu erzeugen.

Die Parameter, welche man normalerweise dem Konstruktor der Klasse Employee übergeben würde, werden stattdessen dem Ausdruck shared_ptr<Employee> (parameter1, parameter2, ...) übergeben.

Sobald der aktuelle Kontext verlassen wird, also z.B. der Kontext der momentan aufgerufenen Methode, wird der Speicher, welcher von der employee1 Instanz belegt wurde, automatisch freigegeben. Der Aufruf des delete-Befehls ist somit nicht notwendig.

Grundlagen der shared_ptr - Zugriff auf Class Members eines Shared Pointers

Angenommen, man möchte unter Verwendung der Getter Methode auf den Namen der Variablen employee1 zugreifen, dann könnte man dies tun, indem man die Variable wie einen ganz normalen C++ Pointer behandelt und den Pfeil Operator “->” verwendet:

string employeeName = employee1->getName();

Die Verwendung des Punkt Operators “.” gewährt im Gegensatz dazu den Zugriff auf hilfreiche Informationen über die shared reference wie zum Beispiel die Anzahl an Reference Counts:

long employee1UseCount = employee1.use_count();

Theoretisch betrachtet, ist es möglich, über den Shared Pointer auch noch auf den Raw Pointer zuzugreifen, welcher vom Shared Pointer verwaltet wird:

Employee *pointer = employee1.get();

Jedoch sollte ein Zugriff auf den Raw Pointer, wie in diesem Beispiel um jeden Preis vermieden werden, weil dies wieder nur zu altbekannten Problemen führt: jemand versucht, Speicher freizugeben, welcher bereits freigegeben wurde. Diese Möglichkeit soll hier nur aus Gründen der Vollständigkeit erwähnt werden.

Grundlagen der shared_ptr - Kopieren eines shared_ptr

Wie kann man eine Kopie eines shared_ptr machen, welcher auf allokierten Speicher verweist? Vergleicht man es mit normalen Pointern, dann würde man normalerweise Folgendes schreiben:

Employee  *employee1 = new Employee( 1, “Peter Parker”);
Employee  *employee1Copy = employee1;

Die erste Zeile des Codes allokiert ein Employee Objekt und lässt den Pointer employee1 auf den neu allokierten Speicher zeigen. Anschließend wird in der zweiten Zeile eine Kopie dieser Variable gemacht und die neue Variable employee1Copy verweist dann auf denselben Speicherbereich wie employee1. Wenn man etwas vergleichbares mit einem shared_ptr machen möchte, dann gibt es zwei Möglichkeiten:


shared_ptr<Employee> employee1 = make_shared<Employee>(1, "Peter Parker");

1) shared_ptr<Employee> employee1Copy = employee1;
2) auto employee1Copy2 = employee1;

Wie verhält sich das Reference Counting?

Nachdem nun die Grundlagen über shared_ptr vermittelt wurden, wäre es als nächstes interessant, zu betrachten, wie sich das Reference Counting in einem richtigen Code Beispiel verhält.

Zunächst soll die Klasse Employee betrachtet werden. Ein Mitarbeiter (Employee) hat ein Attribut Name und eine eindeutige Id, welche dem Konstruktor übergeben werden kann. Diese Klasse enthält, um das Beispiel möglichst einfach zu halten, keine Pointer oder Shared Pointer.

#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_EMPLOYEE_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_EMPLOYEE_H

#import <vector>
#import <string>
#import <iostream>
using namespace std;

class Employee {

private:
    int id;
    string name;
public:
    Employee(int id, const string &name) : id(id), name(name){
    }

    int getId() const{
        return id;
    }

    const string &getName() const{
        return name;
    }

    void printCurrentUseCount(shared_ptr<Employee> employee){
        if(employee.use_count() > 0){

            printf("%s, use count: %li n", getName().c_str(), (employee.use_count() -1));

        } else{
            printf("use count: %li n", employee.use_count());
        }
    }
};

#endif //SHAREDPOINTEREXAMPLEAPPLICATION_EMPLOYEE_H

Um einen besseren Überblick über die Reference Counts zu haben, gibt es noch eine Methode printCurrentUseCount. Da bei jedem Aufruf der Methode ein neuer Kontext betreten wird und damit das Ergebnis um den Wert +1 “verfälscht” würde, wird hier wieder das auszugebende Ergebnis immer um -1 heruntergesetzt.

Das OfficeBuilding, in welchem die Mitarbeiter arbeiten, kann durch den Aufruf der Method enterBuilding und leaveBuilding von Mitarbeitern betreten bzw. verlassen werden. Bei genauerem Hinsehen fällt auf, dass die Klasse OfficeBuilding eine Klassenvariable vector<shared_ptr<Employee>> smartDoorPeopleCounter hat, welche sich merkt, welche Mitarbeiter sich zur Zeit im Gebäude befinden.

Über die beiden Methoden useWashRoom und leaveWashRoom kann immer genau ein Mitarbeiter das WC betreten bzw. wieder verlassen.

#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEBUILDING_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEBUILDING_H

#include "Employee.h"
#include "WashRoom.h"
#import <vector>
#import <string>
#include <thread>
#include <cstdlib>

class OfficeBuilding {

private:
    vector<shared_ptr<Employee>> smartDoorPeopleCounter;
    vector<WashRoom> washRoom;

public:
    void enterBuilding(shared_ptr<Employee> employeeWhoWantsToEnter) {
        smartDoorPeopleCounter.push_back(employeeWhoWantsToEnter);
        employeeWhoWantsToEnter->printCurrentUseCount(employeeWhoWantsToEnter);
    }

    void leaveBuilding(shared_ptr<Employee> employeeWhoWantsToLeave) {
        int indexToRemove = -1;
        for (int i = 0; i < smartDoorPeopleCounter.size(); i++) {
            auto currentEmployee = smartDoorPeopleCounter[i];
            if (currentEmployee->getId() == employeeWhoWantsToLeave->getId()) {
                indexToRemove = i;
            }
        }
        if (indexToRemove != -1) {
            smartDoorPeopleCounter.erase(smartDoorPeopleCounter.begin() + indexToRemove);
        }
        if (smartDoorPeopleCounter.size() == 0) {
            // activate alarm system
        }
    }

    void useWashRoom(shared_ptr<Employee> employee) {
        if (washRoom.size() == 0) {
            WashRoom emptyWashRoom;
            emptyWashRoom.setEmployee(employee);
            washRoom.push_back(emptyWashRoom);
        } else {
            //washroom is currently in use, please wait!
        }
    }

    void leaveWashRoom() {
        if (washRoom.size() == 1) {
            washRoom.clear();
        }
    }
};

#endif //SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEBUILDING_H

Das Objekt Washroom hält sich immer einen shared_ptr auf den aktuellen Mitarbeiter, welcher gerade das WC benutzt.

#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEROOM_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEROOM_H

#include "Employee.h"

class WashRoom {
    shared_ptr<Employee> employee;
public:
    void setEmployee(const shared_ptr<Employee> &employee) {
        WashRoom::employee = employee;
    }

    const shared_ptr<Employee> &getEmployee() const {
        return employee;
    }
};

#endif //SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEROOM_H

Der folgende Beispielcode allokiert einige Mitarbeiter Objekte (Employee) und lässt diese das OfficeBuilding betreten und wieder verlassen.


OfficeBuilding officeBuilding;

/* create employee 1 */
shared_ptr<Employee> employee1 = make_shared<Employee>(1, "Peter Parker");
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 1
string employeeName = employee1->getName(); // "Peter Parker"
Employee *pointer = employee1.get(); // attention: raw pointer should not be used!!
shared_ptr<Employee>  employee1Copy = employee1;
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 2
auto employee1Copy2 = employee1;
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 3

/* create employee 2 */
shared_ptr<Employee> employee2;
employee2->printCurrentUseCount(employee2); //use count: 0
employee2 = make_shared<Employee>(2, "Bruce Wayne");
employee2->printCurrentUseCount(employee2); //Bruce Wayne, use count: 1

/* create employee 3 */
auto employee3 = make_shared<Employee>(3, "Clark Kent");
employee3->printCurrentUseCount(employee3); //Clark Kent, use count: 1

/* employees entering building */
officeBuilding.enterBuilding(employee1); //Peter Parker, use count: 5
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 4
officeBuilding.enterBuilding(employee2); //Bruce Wayne, use count: 3
employee2->printCurrentUseCount(employee2); //Bruce Wayne, use count: 2
officeBuilding.enterBuilding(employee3);//Clark Kent, use count: 3
employee3->printCurrentUseCount(employee3);//Clark Kent, use count: 2

/* employee 1 entering using washroom */
officeBuilding.useWashRoom(employee1);
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 5
officeBuilding.leaveWashRoom();
employee1->printCurrentUseCount(employee1);//Peter Parker, use count: 4

/* employee 1 leaving building */
officeBuilding.leaveBuilding(employee1);
employee1->printCurrentUseCount(employee1);//Peter Parker, use count: 3

/* employee 1 visiting coffee house */
CoffeeHouse coffeeHouse;
coffeeHouse.visitCoffeeHouse(employee1);
employee1->printCurrentUseCount(employee1);//Peter Parker, use count: 4

bool finish = true;

Die Print-Ausgaben sollen hier verdeutlichen, wie jeweils der aktuelle Reference Count ist:

Peter Parker, use count: 1
Peter Parker, use count: 2
Peter Parker, use count: 3
use count: 0
Bruce Wayne, use count: 1
Clark Kent, use count: 1
Peter Parker, use count: 5
Peter Parker, use count: 4
Bruce Wayne, use count: 3
Bruce Wayne, use count: 2
Clark Kent, use count: 3
Clark Kent, use count: 2
Peter Parker, use count: 5
Peter Parker, use count: 4
Peter Parker, use count: 3
use count inside context: 5
use count outside context: 4
use count from thread: 6
use count after thread finished : 5
Peter Parker, use count: 4

Die Verwendung von shared_ptr innerhalb von Threads

Es wird vermutet, dass es bei der Verwendung von C++ Shared Pointern zu Problemen im Zusammenhang mit Threads kommen könnte. Nachfolgend auch ein Beispiel für diesen speziellen Fall: es gibt ein Cafe, welches sich außerhalb des Gebäudes OfficeBuilding befindet und welches ein Mitarbeiter (Employee) besuchen kann.

Das Cafe kann mittels der Methode visitCoffeeHouse betreten werden. In dieser Methode wird zunächst der Shared Pointer auf den Mitarbeiter einer auto Variable zugewiesen und anschließend einem Shared Pointer struct, welcher als Container für die Parameter des Threads dient.


#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_COFEEHOUSE_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_COFEEHOUSE_H

#include "Employee.h"
#import <vector>
#import <string>
#include <thread>
#include <cstdlib>
#include <unistd.h>

using namespace std;
static pthread_t threadID = pthread_t();

class CoffeeHouse {

private:
    shared_ptr<vector<shared_ptr<Employee>>> cafeteriaCounter;
    struct threadParams {
        shared_ptr<Employee> employeeCopy;
        shared_ptr<vector<shared_ptr<Employee>>> cafeteriaCounterCopy;
    };

    static void *visitCoffeeHouseAsync(void *context) {

        unsigned int microseconds = 5000000; // sleep for 5 seconds
        usleep(microseconds);
        auto params = *(shared_ptr<threadParams> *) context;
        auto employee =  params->employeeCopy;
        string employeeName = employee->getName();

        if (employeeName.length() > 0) {
            params->cafeteriaCounterCopy->push_back(employee);
            printf("use count from thread: %li n", (employee.use_count()));
        }

        pthread_detach(pthread_self());
        return nullptr;
    }

public:

    CoffeeHouse(){
        cafeteriaCounter = make_shared<vector<shared_ptr<Employee>>>();
    }

    void visitCoffeeHouse(shared_ptr<Employee> employee) {
        {
            shared_ptr<threadParams> params = make_shared<threadParams>();
            params->employeeCopy = employee;
            params->cafeteriaCounterCopy = cafeteriaCounter;
            printf("use count inside context: %li n", (employee.use_count())); //use count inside context: 5
            pthread_create(&threadID, NULL, visitCoffeeHouseAsync, &params);
        }

        printf("use count outside context: %li n", (employee.use_count())); //use count outside context: 4
        pthread_join(threadID, NULL);
        printf("use count after thread finished : %li n", (employee.use_count()));//use count after thread finished : 5
    }
};

#endif //SHAREDPOINTEREXAMPLEAPPLICATION_COFEEHOUSE_H

Der Thread wird innerhalb eines speziellen Kontextes gestartet, wobei der Kontext durch die geschweiften Klammern { } markiert ist. Innerhalb des Kontextes wird der useCount den Wert 5 haben, kurz bevor der Thread gestartet wird. Nachdem der Thread gestartet wurde, wird der Kontext verlassen, während der Thread für 5 Sekunden schläft und in der Zwischenzeit wird der Reference Count durch den Main Thread auf den Wert 4 verringert.

Sobald der Thread wieder erwacht, wird dieser eine lokale und eine globale Kopie des Shared Pointer machen, was zu der Ausgabe des Wertes 6 führt.

Nachdem der Thread beendet wurde, verbleibt lediglich die globale Kopie, was zu einer Ausgabe des Wertes 5 führt. Das Endergebnis nach dem Verlassen der Methode visitCoffeeHouse für den Mitarbeiter “Peter Parker” ist der Wert 4.

Auf den ersten Blick sieht mit den Threads alles relativ normal aus. Das heißt, die reference counts scheinen korrekt erhöht und verringert zu werden. Jedoch können Probleme nie völlig ausgeschlossen werden. Möchte man ganz sicher gehen, empfiehlt es sich, nur auf einer Kopie der Daten zu arbeiten.

Zusammenfassung

Das Konzept der C++ shared_ptr bietet eine Alternative zu den traditionellen C++ Pointern. Es ist jedoch entscheidend, niemals normale C++ Pointer und Shared Pointer zu vermischen, weil dies zu ernsthaften Problemen führen kann.

Der große Vorteil der Shared Pointer, die automatische Verwaltung von Speicher-Referenzen, ersetzt außerdem die mühselige, manuelle Speicherfreigabe. Des Weiteren wurde ein Einblick gegeben, wie sich das Reference Counting verhält. Einfach gesagt: Wann immer ein neuer Kontext betreten wird z.B. eine Methode oder eine Kopie eines Shared Pointers, wird der Reference Count um +1 erhöht. Sobald ein Kontext wieder verlassen wird, wird der Reference Count automatisch verringert, abhängig von der Anzahl der Kopien in diesem Kontext. Nach intensiver Einarbeitung in die Thematik, lässt sich festhalten, dass Shared Pointer den Entwicklungsprozess deutlich komfortabler und einfacher machen können. Auch die Kollaboration mit Teamkollegen wird durch diese deutlich erleichtert. Es dürfte spannend werden, Shared Pointer zukünftig auch in größeren Projekten einzusetzen.

Einen kleinen Nachteil könnte es dabei jedoch geben: sofern man Shared Pointer in einem Projekt verwendet und im selben Projekt auch eine externe Bibliothek wie z.B. OpenCV verwenden möchte, welche vielleicht in den Interfaces keine Shared Pointer anbietet, sollte man sehr vorsichtig damit umgehen. Möglicherweise bietet es sich an, hier dann eine “tiefe Kopie” der Daten zu verwenden, welche man als Parameter den Schnittstellen der Bibliothek übergeben möchte.

Bildnachweis:

  • iStock: kasto80

Weitere Artikel aus unserem Blog