410 Stimmen

Wie kann ich den Inhalt eines Vektors ausdrucken?

Wie drucke ich den Inhalt einer std::vector auf dem Bildschirm?


Eine Lösung, die Folgendes implementiert operator<< wäre auch schön:

template<container C, class T, String delim = ", ", String open = "[", String close = "]">
std::ostream & operator<<(std::ostream & o, const C<T> & x)
{
  // ... What can I write here?
}

Hier ist, was ich bis jetzt habe, ohne eine separate Funktion:

#include <iostream>
#include <fstream>
#include <string>
#include <cmath>
#include <vector>
#include <sstream>
#include <cstdio>
using namespace std;

int main()
{
    ifstream file("maze.txt");
    if (file) {
        vector<char> vec(istreambuf_iterator<char>(file), (istreambuf_iterator<char>()));
        vector<char> path;
        int x = 17;
        char entrance = vec.at(16);
        char firstsquare = vec.at(x);
        if (entrance == 'S') { 
            path.push_back(entrance); 
        }
        for (x = 17; isalpha(firstsquare); x++) {
            path.push_back(firstsquare);
        }
        for (int i = 0; i < path.size(); i++) {
            cout << path[i] << " ";
        }
        cout << endl;
        return 0;
    }
}

525voto

Zorawar Punkte 5997

Wenn Sie einen C++11-Compiler haben, würde ich vorschlagen, eine bereichsbasierte for-Schleife zu verwenden (siehe unten); oder verwenden Sie einen Iterator. Sie haben jedoch mehrere Möglichkeiten, die ich im Folgenden alle erläutern werde.

Bereichsbezogene for-Schleife (C++11)

In C++11 (und später) können Sie die neue bereichsbasierte for-Schleife verwenden, die wie folgt aussieht:

std::vector<char> path;
// ...
for (char i: path)
    std::cout << i << ' ';

Der Typ char in der for-Schleife sollte der Typ der Elemente des Vektors sein path und nicht ein Integer-Indexierungstyp. Mit anderen Worten, da path ist vom Typ std::vector<char> ist der Typ, der in der bereichsbasierten for-Schleife erscheinen sollte char . Allerdings werden Sie wahrscheinlich oft sehen, dass der explizite Typ durch das auto Platzhaltertyp:

for (auto i: path)
    std::cout << i << ' ';

Unabhängig davon, ob Sie den expliziten Typ oder die auto Schlüsselwort, das Objekt i hat einen Wert, der eine Kopie des tatsächlichen Elements in der path Objekt. Daher werden alle Änderungen an i in der Schleife werden nicht in path selbst:

std::vector<char> path{'a', 'b', 'c'};

for (auto i: path) {
    i = '_'; // 'i' is a copy of the element in 'path', so although
             // we can change 'i' here perfectly fine, the elements
             // of 'path' have not changed
    std::cout << i << ' '; // will print: "_ _ _"
}

for (auto i: path) {
    std::cout << i << ' '; // will print: "a b c"
}

Wenn Sie verbieten möchten, dass dieser kopierte Wert von i in der for-Schleife können Sie auch den Typ der i zu sein const char wie diese:

for (const auto i: path) {
    i = '_'; // this will now produce a compiler error
    std::cout << i << ' ';
}

Wenn Sie die Einträge in path so dass diese Änderungen in der Zukunft bestehen bleiben path außerhalb der for-Schleife, dann können Sie einen Verweis wie folgt verwenden:

for (auto& i: path) {
    i = '_'; // changes to 'i' will now also change the
             // element in 'path' itself to that value
    std::cout << i << ' ';
}

und auch wenn Sie nicht ändern wollen path Wenn das Kopieren von Objekten teuer ist, sollten Sie eine Konst-Referenz verwenden, anstatt nach Wert zu kopieren:

for (const auto& i: path)
    std::cout << i << ' ';

Iteratoren

Vor C++11 wäre die kanonische Lösung die Verwendung eines Iterators gewesen, und das ist immer noch vollkommen akzeptabel. Sie werden wie folgt verwendet:

std::vector<char> path;
// ...
for (std::vector<char>::const_iterator i = path.begin(); i != path.end(); ++i)
    std::cout << *i << ' ';

Wenn Sie den Inhalt des Vektors in der for-Schleife ändern wollen, dann verwenden Sie iterator statt const_iterator .

Ergänzung: typedef / type alias (C++11) / auto (C++11)

Dies ist keine weitere Lösung, sondern eine Ergänzung zu den oben genannten iterator Lösung. Wenn Sie den C++11-Standard (oder höher) verwenden, können Sie die auto Schlüsselwort, um die Lesbarkeit zu verbessern:

for (auto i = path.begin(); i != path.end(); ++i)
    std::cout << *i << ' ';

Hier wird die Art der i wird nicht konstant sein (d.h. der Compiler verwendet std::vector<char>::iterator als die Art der i ). Dies liegt daran, dass wir die begin Methode, so dass der Compiler den Typ für i davon. Wenn wir die cbegin Methode stattdessen ("c" für const), dann i wird ein std::vector<char>::const_iterator :

for (auto i = path.cbegin(); i != path.cend(); ++i) {
    *i = '_'; // will produce a compiler error
    std::cout << *i << ' ';
}

Wenn Sie nicht damit einverstanden sind, dass der Compiler Typen ableitet, können Sie in C++11 einen Typ-Alias verwenden, um zu vermeiden, dass Sie den Vektor ständig abtippen müssen (eine gute Angewohnheit, die man sich angewöhnen sollte):

using Path = std::vector<char>; // C++11 onwards only
Path path; // 'Path' is an alias for std::vector<char>
// ...
for (Path::const_iterator i = path.begin(); i != path.end(); ++i)
    std::cout << *i << ' ';

Wenn Sie keinen Zugang zu einem C++11-Compiler haben (oder die Typ-Alias-Syntax aus irgendeinem Grund nicht mögen), dann können Sie die traditionellere typedef :

typedef std::vector<char> Path; // 'Path' now a synonym for std::vector<char>
Path path;
// ...
for (Path::const_iterator i = path.begin(); i != path.end(); ++i)
    std::cout << *i << ' ';

Nebenbei bemerkt:

An dieser Stelle sind Sie vielleicht schon einmal auf Iteratoren gestoßen, und Sie haben vielleicht schon einmal gehört, dass man Iteratoren verwenden "soll", und fragen sich vielleicht, warum. Die Antwort ist nicht leicht zu verstehen, aber kurz gesagt geht es darum, dass Iteratoren eine Abstraktion sind, die Sie von den Details der Operation abschirmen.

Es ist praktisch, ein Objekt (den Iterator) zu haben, das die gewünschte Operation durchführt (z. B. sequentieller Zugriff), anstatt die Details selbst zu schreiben (die "Details" sind der Code, der den tatsächlichen Zugriff auf die Elemente des Vektors durchführt). Sie sollten beachten, dass Sie in der for-Schleife den Iterator immer nur auffordern, Ihnen einen Wert zurückzugeben ( *i , wobei i ist der Iterator) - Sie interagieren nie mit path direkt selbst. Die Logik geht wie folgt: Sie erstellen einen Iterator und geben ihm das Objekt, über das Sie eine Schleife ziehen wollen ( iterator i = path.begin() ), und dann bitten Sie den Iterator lediglich, den nächsten Wert für Sie zu ermitteln ( *i ); Sie mussten sich nie darum kümmern, wie der Iterator das genau macht - das ist seine Sache, nicht Ihre.

Gut, aber was soll das bringen? Stellen Sie sich vor, es wäre nicht einfach, einen Wert zu ermitteln. Was, wenn es ein bisschen Arbeit bedeutet? Keine Sorge, denn der Iterator hat das für Sie erledigt - er kümmert sich um die Details, und Sie müssen ihn nur noch nach einem Wert fragen. Und was ist, wenn Sie den Container von std::vector zu etwas anderem? Theoretisch ändert sich Ihr Code nicht, auch wenn sich die Details des Zugriffs auf Elemente im neuen Container ändern: Denken Sie daran, dass der Iterator alle Details hinter den Kulissen für Sie erledigt, so dass Sie Ihren Code überhaupt nicht ändern müssen - Sie fragen den Iterator einfach nach dem nächsten Wert im Container, genauso wie vorher.

Auch wenn dies wie ein verwirrender Overkill für die Schleifenbildung durch einen Vektor erscheinen mag, gibt es gute Gründe für das Konzept der Iteratoren, so dass man sich durchaus an ihre Verwendung gewöhnen kann.

Indizierung

Sie können auch einen Integer-Typ verwenden, um explizit durch die Elemente des Vektors in der for-Schleife zu indizieren:

for (int i=0; i<path.size(); ++i)
    std::cout << path[i] << ' ';

Wenn Sie dies tun, ist es besser, die Mitgliedstypen des Containers zu verwenden, wenn sie verfügbar und geeignet sind. std::vector hat einen Membertyp namens size_type für diesen Auftrag: Es ist der Typ, der von der size Methode.

typedef std::vector<char> Path; // 'Path' now a synonym for std::vector<char>
for (Path::size_type i=0; i<path.size(); ++i)
    std::cout << path[i] << ' ';

Warum nicht diese anstelle der iterator Lösung? Für einfache Fälle können Sie das tun, aber mit einer iterator bringt mehrere Vorteile mit sich, die ich oben kurz skizziert habe. Daher würde ich Ihnen raten, diese Methode zu vermeiden, es sei denn, Sie haben gute Gründe dafür.

std::copy (C++11)

Voir Josuas Antwort . Sie können den STL-Algorithmus verwenden std::copy um den Inhalt des Vektors in den Ausgabestrom zu kopieren. Ich habe dem nichts hinzuzufügen, außer dass ich diese Methode nicht verwende, aber dafür gibt es außer der Gewohnheit keinen guten Grund.

std::ranges::copy (C++20)

Der Vollständigkeit halber wurden mit C++20 Bereiche eingeführt, die auf den gesamten Bereich einer std::vector also keine Notwendigkeit für begin y end :

#include <iterator> // for std::ostream_iterator
#include <algorithm> // for std::ranges::copy depending on lib support

std::vector<char> path;
// ...
std::ranges::copy(path, std::ostream_iterator<char>(std::cout, " "));

Es sei denn, Sie haben einen aktuellen Compiler (auf GCC offenbar mindestens Version 10.1 ), werden Sie wahrscheinlich keine Ranges-Unterstützung haben, auch wenn Sie vielleicht einige C++20-Funktionen zur Verfügung haben.

Überladung std::ostream::operator<<

Siehe auch Die Antwort von Chris unten . Dies ist eher eine Ergänzung zu den anderen Antworten, da Sie immer noch eine der oben genannten Lösungen in der Überladung implementieren müssen, aber der Vorteil ist ein viel saubererer Code. So könnten Sie die std::ranges::copy Lösung oben:

#include <iostream>
#include <vector>
#include <iterator> // for std::ostream_iterator
#include <algorithm> // for std::ranges::copy depending on lib support

using Path = std::vector<char>; // type alias for std::vector<char>

std::ostream& operator<< (std::ostream& out, const Path& v) {
    if ( !v.empty() ) {
        out << '[';
        std::ranges::copy(v, std::ostream_iterator<char>(out, ", "));
        out << "\b\b]"; // use two ANSI backspace characters '\b' to overwrite final ", "
    }
    return out;
}

int main() {
    Path path{'/', 'f', 'o', 'o'};

    // will output: "path: [/, f, o, o]"
    std::cout << "path: " << path << std::endl;

    return 0;
}

Jetzt können Sie Ihre Path Objekte in den Ausgabestrom, genau wie die Grundtypen. Die Verwendung einer der anderen oben genannten Lösungen sollte ebenso einfach sein.

Schlussfolgerung

Jede der hier vorgestellten Lösungen wird funktionieren. Es liegt an Ihnen (und dem Kontext oder Ihren Codierungsstandards), welche die "beste" ist. Alles, was detaillierter ist als dies ist wahrscheinlich am besten für eine andere Frage verlassen, wo die Vor-und Nachteile richtig bewertet werden können, aber wie immer Benutzerpräferenz wird immer eine Rolle spielen: keine der vorgestellten Lösungen sind objektiv falsch, aber einige werden schöner zu jedem Programmierer aussehen.

Nachtrag

Dies ist eine erweiterte Lösung einer früheren Lösung, die ich veröffentlicht habe. Da dieser Beitrag immer wieder Beachtung fand, beschloss ich, ihn zu erweitern und auf die anderen hervorragenden Lösungen zu verweisen, die hier gepostet wurden, zumindest auf diejenigen, die ich persönlich in der Vergangenheit mindestens einmal verwendet habe. Ich möchte den Leser jedoch ermutigen, sich die Antworten weiter unten anzusehen, da es wahrscheinlich gute Vorschläge gibt, die ich vergessen habe oder nicht kenne.

252voto

Joshua Kravitz Punkte 2775

Eine viel einfachere Möglichkeit, dies zu tun, ist die Verwendung der Standard Kopieralgorithmus :

#include <iostream>
#include <algorithm> // for copy
#include <iterator> // for ostream_iterator
#include <vector>

int main() {
    /* Set up vector to hold chars a-z */
    std::vector<char> path;
    for (int ch = 'a'; ch <= 'z'; ++ch)
        path.push_back(ch);

    /* Print path vector to console */
    std::copy(path.begin(), path.end(), std::ostream_iterator<char>(std::cout, " "));

    return 0;
}

Der ostream_iterator ist ein so genannter Iterator-Adapter . Es wird eine Vorlage über die Art des Ausdrucks in den Stream erstellt (in diesem Fall, char ). cout (auch Konsolenausgabe genannt) ist der Stream, in den wir schreiben wollen, und das Leerzeichen ( " " ) ist das, was zwischen den einzelnen Elementen des Vektors gedruckt werden soll.

Dieser Standardalgorithmus ist sehr leistungsfähig, wie auch viele andere. Die Leistungsfähigkeit und Flexibilität, die Ihnen die Standardbibliothek bietet, machen sie so großartig. Stellen Sie sich vor: Sie können einen Vektor auf der Konsole ausgeben mit nur eine Code-Zeile. Sie müssen sich nicht mit den Sonderfällen des Trennzeichens befassen. Sie müssen sich keine Gedanken über for-Schleifen machen. Die Standardbibliothek erledigt das alles für Sie.

84voto

Sven Punkte 20703

Diese Lösung wurde von Marcelos Lösung inspiriert, mit ein paar Änderungen:

#include <iostream>
#include <iterator>
#include <type_traits>
#include <vector>
#include <algorithm>

// This works similar to ostream_iterator, but doesn't print a delimiter after the final item
template<typename T, typename TChar = char, typename TCharTraits = std::char_traits<TChar> >
class pretty_ostream_iterator : public std::iterator<std::output_iterator_tag, void, void, void, void>
{
public:
    typedef TChar char_type;
    typedef TCharTraits traits_type;
    typedef std::basic_ostream<TChar, TCharTraits> ostream_type;

    pretty_ostream_iterator(ostream_type &stream, const char_type *delim = NULL)
        : _stream(&stream), _delim(delim), _insertDelim(false)
    {
    }

    pretty_ostream_iterator<T, TChar, TCharTraits>& operator=(const T &value)
    {
        if( _delim != NULL )
        {
            // Don't insert a delimiter if this is the first time the function is called
            if( _insertDelim )
                (*_stream) << _delim;
            else
                _insertDelim = true;
        }
        (*_stream) << value;
        return *this;
    }

    pretty_ostream_iterator<T, TChar, TCharTraits>& operator*()
    {
        return *this;
    }

    pretty_ostream_iterator<T, TChar, TCharTraits>& operator++()
    {
        return *this;
    }

    pretty_ostream_iterator<T, TChar, TCharTraits>& operator++(int)
    {
        return *this;
    }
private:
    ostream_type *_stream;
    const char_type *_delim;
    bool _insertDelim;
};

#if _MSC_VER >= 1400

// Declare pretty_ostream_iterator as checked
template<typename T, typename TChar, typename TCharTraits>
struct std::_Is_checked_helper<pretty_ostream_iterator<T, TChar, TCharTraits> > : public std::tr1::true_type
{
};

#endif // _MSC_VER >= 1400

namespace std
{
    // Pre-declarations of container types so we don't actually have to include the relevant headers if not needed, speeding up compilation time.
    // These aren't necessary if you do actually include the headers.
    template<typename T, typename TAllocator> class vector;
    template<typename T, typename TAllocator> class list;
    template<typename T, typename TTraits, typename TAllocator> class set;
    template<typename TKey, typename TValue, typename TTraits, typename TAllocator> class map;
}

// Basic is_container template; specialize to derive from std::true_type for all desired container types
template<typename T> struct is_container : public std::false_type { };

// Mark vector as a container
template<typename T, typename TAllocator> struct is_container<std::vector<T, TAllocator> > : public std::true_type { };

// Mark list as a container
template<typename T, typename TAllocator> struct is_container<std::list<T, TAllocator> > : public std::true_type { };

// Mark set as a container
template<typename T, typename TTraits, typename TAllocator> struct is_container<std::set<T, TTraits, TAllocator> > : public std::true_type { };

// Mark map as a container
template<typename TKey, typename TValue, typename TTraits, typename TAllocator> struct is_container<std::map<TKey, TValue, TTraits, TAllocator> > : public std::true_type { };

// Holds the delimiter values for a specific character type
template<typename TChar>
struct delimiters_values
{
    typedef TChar char_type;
    const TChar *prefix;
    const TChar *delimiter;
    const TChar *postfix;
};

// Defines the delimiter values for a specific container and character type
template<typename T, typename TChar>
struct delimiters
{
    static const delimiters_values<TChar> values; 
};

// Default delimiters
template<typename T> struct delimiters<T, char> { static const delimiters_values<char> values; };
template<typename T> const delimiters_values<char> delimiters<T, char>::values = { "{ ", ", ", " }" };
template<typename T> struct delimiters<T, wchar_t> { static const delimiters_values<wchar_t> values; };
template<typename T> const delimiters_values<wchar_t> delimiters<T, wchar_t>::values = { L"{ ", L", ", L" }" };

// Delimiters for set
template<typename T, typename TTraits, typename TAllocator> struct delimiters<std::set<T, TTraits, TAllocator>, char> { static const delimiters_values<char> values; };
template<typename T, typename TTraits, typename TAllocator> const delimiters_values<char> delimiters<std::set<T, TTraits, TAllocator>, char>::values = { "[ ", ", ", " ]" };
template<typename T, typename TTraits, typename TAllocator> struct delimiters<std::set<T, TTraits, TAllocator>, wchar_t> { static const delimiters_values<wchar_t> values; };
template<typename T, typename TTraits, typename TAllocator> const delimiters_values<wchar_t> delimiters<std::set<T, TTraits, TAllocator>, wchar_t>::values = { L"[ ", L", ", L" ]" };

// Delimiters for pair
template<typename T1, typename T2> struct delimiters<std::pair<T1, T2>, char> { static const delimiters_values<char> values; };
template<typename T1, typename T2> const delimiters_values<char> delimiters<std::pair<T1, T2>, char>::values = { "(", ", ", ")" };
template<typename T1, typename T2> struct delimiters<std::pair<T1, T2>, wchar_t> { static const delimiters_values<wchar_t> values; };
template<typename T1, typename T2> const delimiters_values<wchar_t> delimiters<std::pair<T1, T2>, wchar_t>::values = { L"(", L", ", L")" };

// Functor to print containers. You can use this directly if you want to specificy a non-default delimiters type.
template<typename T, typename TChar = char, typename TCharTraits = std::char_traits<TChar>, typename TDelimiters = delimiters<T, TChar> >
struct print_container_helper
{
    typedef TChar char_type;
    typedef TDelimiters delimiters_type;
    typedef std::basic_ostream<TChar, TCharTraits>& ostream_type;

    print_container_helper(const T &container)
        : _container(&container)
    {
    }

    void operator()(ostream_type &stream) const
    {
        if( delimiters_type::values.prefix != NULL )
            stream << delimiters_type::values.prefix;
        std::copy(_container->begin(), _container->end(), pretty_ostream_iterator<typename T::value_type, TChar, TCharTraits>(stream, delimiters_type::values.delimiter));
        if( delimiters_type::values.postfix != NULL )
            stream << delimiters_type::values.postfix;
    }
private:
    const T *_container;
};

// Prints a print_container_helper to the specified stream.
template<typename T, typename TChar, typename TCharTraits, typename TDelimiters>
std::basic_ostream<TChar, TCharTraits>& operator<<(std::basic_ostream<TChar, TCharTraits> &stream, const print_container_helper<T, TChar, TDelimiters> &helper)
{
    helper(stream);
    return stream;
}

// Prints a container to the stream using default delimiters
template<typename T, typename TChar, typename TCharTraits>
typename std::enable_if<is_container<T>::value, std::basic_ostream<TChar, TCharTraits>&>::type
    operator<<(std::basic_ostream<TChar, TCharTraits> &stream, const T &container)
{
    stream << print_container_helper<T, TChar, TCharTraits>(container);
    return stream;
}

// Prints a pair to the stream using delimiters from delimiters<std::pair<T1, T2>>.
template<typename T1, typename T2, typename TChar, typename TCharTraits>
std::basic_ostream<TChar, TCharTraits>& operator<<(std::basic_ostream<TChar, TCharTraits> &stream, const std::pair<T1, T2> &value)
{
    if( delimiters<std::pair<T1, T2>, TChar>::values.prefix != NULL )
        stream << delimiters<std::pair<T1, T2>, TChar>::values.prefix;

    stream << value.first;

    if( delimiters<std::pair<T1, T2>, TChar>::values.delimiter != NULL )
        stream << delimiters<std::pair<T1, T2>, TChar>::values.delimiter;

    stream << value.second;

    if( delimiters<std::pair<T1, T2>, TChar>::values.postfix != NULL )
        stream << delimiters<std::pair<T1, T2>, TChar>::values.postfix;
    return stream;    
}

// Used by the sample below to generate some values
struct fibonacci
{
    fibonacci() : f1(0), f2(1) { }
    int operator()()
    {
        int r = f1 + f2;
        f1 = f2;
        f2 = r;
        return f1;
    }
private:
    int f1;
    int f2;
};

int main()
{
    std::vector<int> v;
    std::generate_n(std::back_inserter(v), 10, fibonacci());

    std::cout << v << std::endl;

    // Example of using pretty_ostream_iterator directly
    std::generate_n(pretty_ostream_iterator<int>(std::cout, ";"), 20, fibonacci());
    std::cout << std::endl;
}

Wie die Version von Marcelo verwendet sie einen is_container-Typ-Trait, der für alle zu unterstützenden Container spezialisiert werden muss. Es könnte möglich sein, einen Trait zu verwenden, um zu prüfen, ob value_type , const_iterator , begin() / end() aber ich bin mir nicht sicher, ob ich das empfehlen würde, da es Dinge finden könnte, die diesen Kriterien entsprechen, aber eigentlich keine Container sind, wie std::basic_string . Ebenso wie Marcelos Version verwendet es Vorlagen, die spezialisiert werden können, um die zu verwendenden Begrenzungszeichen anzugeben.

Der Hauptunterschied besteht darin, dass ich meine Version um eine pretty_ostream_iterator die ähnlich funktioniert wie die std::ostream_iterator aber gibt nach dem letzten Element kein Trennzeichen aus. Die Formatierung der Container erfolgt durch die Funktion print_container_helper die direkt verwendet werden kann, um Container ohne eine is_container-Eigenschaft zu drucken oder um einen anderen Begrenzertyp anzugeben.

Ich habe auch is_container und delimiters definiert, so dass es für Container mit Nicht-Standard-Prädikaten oder Allokatoren und sowohl für char als auch für wchar_t funktionieren wird. Die Funktion operator<< selbst ist ebenfalls so definiert, dass sie sowohl mit char- als auch mit wchar_t-Streams funktioniert.

Schließlich habe ich verwendet std::enable_if die als Teil von C++0x verfügbar ist und in Visual C++ 2010 und g++ 4.3 (benötigt das Flag -std=c++0x) und höher funktioniert. Auf diese Weise gibt es keine Abhängigkeit von Boost.

79voto

Shoe Punkte 72596

In C++11 können Sie jetzt eine Bereichsbezogene for-Schleife :

for (auto const& c : path)
    std::cout << c << ' ';

72voto

vitaut Punkte 42467

Sie können sowohl Container als auch Bereiche und Tupel drucken, indem Sie die {fmt}-Bibliothek . Zum Beispiel:

#include <vector>
#include <fmt/ranges.h>

int main() {
  auto v = std::vector<int>{1, 2, 3};
  fmt::print("{}", v);
}

druckt

[1, 2, 3]

zu stdout ( godbolt ).

Ich würde nicht empfehlen, zu viel zu laden. operator<< für Standardcontainer, da dies zu ODR-Verletzungen führen kann.

Haftungsausschluss : Ich bin der Autor von {fmt}.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X