C++ Frage bezüglich Speicheradressierung bei Funktionen

Also zum Thema delete.
Die richtige Stelle dafür wäre nach dem Aufruf der Funktion ziffern_ausgeben().
Wenn du "delete[] pziffern;" innerhalb von ziffern_ausgeben() aufrufen würdest wäre das unschön.
Wenn du z.B. in ein paar Wochen denkst "hey ich habe da noch eine Idee was ich mit den pziffern anfangen kann". Du schreibst also eine neue Methode machst dessen Aufruf nach ziffern_ausgeben() und wunderst dich das das ganze abstürzt weil du vergessen hast das ziffern_ausgeben() den Speicher ja schon freigegeben hat.
Das gleiche gilt eben auch wenn du diese Methoden z.B. jemand anderem geben würdest.

Versuche den Code immer so zu schreiben das
1. Du selber auch nach Woche noch nachvollziehen kannst was dort passiert.
2. Auch andere nachvollziehen können was dort passiert.

Bzgl. der for Schleife.
Mit C++11 wurde die Range-Based for Loop eingeführt. Diese ist grade bei der Nutzung von Container Klassen der STL empfehlenswert. Wenn du eh alle Elemente durchlaufen musst, sparst du hier ganz einfach mögliche Fehlerquellen.
http://en.cppreference.com/w/cpp/language/range-for
 
Ach so...
Im array sind die Ziffern in der umgekehrten Reihenfolge gespeichert. Das stimmt.

Schau ich mir auch gleich an und poste dann entweder einen funktionieren Code mit Rekursion, oder aber einen Hilferuf.

Edit:
Danke Fonce, das ist ein sehr guter Einwand bezüglich der Positionierung des delete[];
Das mit der Range-based Schleife werde ich mir auch noch anschauen. Bisher hab ich nur nur (do) while und for Schleifen kennengelernt.
 
Zuletzt bearbeitet:
Muss ins WE ;-) Hier meine rekursive Lösung
Code:
#include <iostream>
#include <array>
#include <vector>
#include <algorithm>


void getDigit(int number, std::vector<int>& digit_vector){
    
    int last_digit = number % 10;
    //digit_vector.push_back(last_digit); // rückwärts einsortieren
    if(last_digit != number){
        getDigit(number / 10, digit_vector);
    }
    digit_vector.push_back(last_digit); // vorwärts einsortieren
}

int main()
{
    std::vector<int> digit_vector;
    int number = 1234567890;
    getDigit(number, digit_vector);
    //std::reverse(digit_vector.begin(), digit_vector.end()); // reihenfolge umdrehen
    for(auto digit : digit_vector){
        std::cout << digit << std::endl;   
    }
}
 
So hier ist auch meine Lösung:

Code:
#include <iostream>
#include <vector>
#include <algorithm>


int zahl_einlesen();
std::vector<int> ziffern_rekursiv(int &zahl, std::vector<int> &ziffern);
void ziffern_ausgeben(std::vector<int> &ziffern1);


int main()
{
	int zahl = zahl_einlesen();
	std::vector<int> ziffern;
	std::vector<int> ziffern1 = ziffern_rekursiv(zahl,ziffern);
	ziffern_ausgeben(ziffern1);
	std::cin.sync();
	std::cin.get();
	return 0;
}

int zahl_einlesen()
{
	std::cout << "Geben Sie eine Zahl ein: ";
	int eingabe;
	std::cin >> eingabe;
	return eingabe;
}

std::vector<int> ziffern_rekursiv(int &zahl,std::vector<int> &ziffern)
{	
	if (zahl <= 0)
	{
		std::reverse(ziffern.begin(), ziffern.end());
		return ziffern;
	}
	else
	{
		ziffern.push_back(zahl % 10);
		zahl /= 10;
		return ziffern_rekursiv(zahl,ziffern);
	}
}

void ziffern_ausgeben(std::vector<int> &ziffern1)
{
	//Alle Ziffern untereinander ausgeben	
	for (int i : ziffern1)
	{
		std::cout << i << "\n";
	}
}


Ich wollte in Zeile 41 eigentlich

Code:
return ziffern_rekursiv(zahl/10,ziffern);

schreiben, um mir Zeile 40 zu ersparen.
Aber das klappt nicht.

Ich glaube ich habe auch schon die Antwort warum es nicht klappt.

Ich führe bei ziffern_rekursiv ja einen call by reference durch und keinen call by value. Insofern übergebe ich ja im Prinzip die Speicheradresse (Pointer) in der der Wert steht und keine Kopie des Wertes.

Insofern würde ich mit

Code:
return ziffern_rekursiv(zahl/10,ziffern);

versuchen die Speicheradresse durch 10 zu teilen und gar nicht den Wert.

Wenn ich die Funktion ziffern_rekursiv so geschrieben hätte, dass ein call by value vorliegt, dann wäre

Code:
return ziffern_rekursiv(zahl/10,ziffern);

möglich.


Sind meine Überlegungen diesbezüglich korrekt?


PS: schönes Wochenende allen :)

Gruß
thesi
 
Sind meine Überlegungen diesbezüglich korrekt?
Ja. Und ich denke, das solltest du auch entsprechend anpassen, denn so hast du einen sich verändernden Zustand an einer Stelle, wo du ihn nicht brauchst (Main-Funktion), was schnell mal zu Verwirrung und Fehlern führt.

Gleiches gilt übrigens für den Vector - entweder, du machst Call by Reference und gibst der Funktion void als Rückgabetypen, oder du änderst auch die Portion auf Call by Value. Aber nicht beides, das ist redundant.

Von mir auch noch mal eine alternative rekursive Lösung:

Code:
#include <utility>
std::list<int>&& ziffern_rekursiv(int zahl, std::list<int>&& ziffern) {
  if (zahl <= 0)
    return std::move(ziffern);
  ziffern.push_front(zahl % 10);
  return ziffern_rekursiv(zahl / 10, std::move(ziffern));
}

Und jetzt machen wir uns noch einen schönen Overload, damit wir nicht immer eine leere Liste übergeben müssen:
Code:
std::list<int> ziffern_rekursiv(int zahl) {
  return ziffern_rekursiv(zahl, std::list<int>());
}

Das ist jetzt vielleicht ein bisschen viel auf einmal, aber ich versuche mal, es zu erklären.

1. aus std::vector habe ich mal std::list gemacht, weil die uns erlaubt, vorne etwas einzufügen. Ich weiß nicht, inwieweit du dich mit fundamentalen Datenstrukturen schon auskennst.
Alternativ könnte man auch den Vector belassen und bei der Ausgabe einfach von hinten nach vorne durchlaufen, das Ergebnis ist dasselbe.

2. wir haben jetzt zwei Funktionen mit demselben Namen. Das funktioniert nur, weil diese unterschiedliche Parameter haben - der Compiler kann trotzdem entscheiden, welche Funktion genau aufgerufen werden soll.
Dasselbe wird übrigens auch genutzt, wenn du std::cout << verwendest. Wie du sicherlich schon gemerkt hast, kannst du da sowohl Zahlen als auch Zeichenketten einfach so hineingeben - das funktioniert, weil dieselbe Funktion einmal für Zahlen implementiert ist und an einer anderen Stelle für Strings.

3. Move-Semantik, also das Zeug mit && und std::move. Das zu verstehen braucht aber tieferes Verständnis der Sprache und wird auch erst interessant, wenn du mit Klassen arbeitest. In diesem Fall ist das im Grunde ein Weg, effizientes Call-by-Reference zu verwenden, ohne eine Variable definieren zu müssen, die referenziert wird - man kann das gleiche auch mit "einfachem" Call-by-Reference erledigen, so ähnlich es dein aktueller Code im Moment tut, allerdings geht dadurch so ein bisschen der Rekursionsgedanke verloren.
 
Zuletzt bearbeitet:
Also nur mal so dazu dies hier recursiv zu machen. Man spart in diesem Fall absolut nichts. Man macht hier aus einer simplen Sache etwas kompliziertes. Ohne nachgemessen zu haben würde ich darauf tippen das man weder Speicher noch CPU Zeit spart sondern eher das Gegenteil der Fall sein wird.
 
Ist natürlich richtig, aber um ihm Rekursion nahezulegen, taugt sowas eben auch.

Wobei man natürlich auch Klassiker wie die Berechnung einer Fakultät oder von Fibonacchi-Zahlen implementieren könnte. Da ist die Rekursion zwar genau so wenig nötig, ist dort aber intuitiver und sinnvoller.
 
Ja das war weil es zuvor ja um Rekursion gegangen ist. Ich finde das ist auch ein gutes Beispiel um Richtung Objektorientierung weiter zu bauen.

@Thesi Strukte kennst Du ja mit Datentypen. Jetzt fangen wir mal einfach an, und ich sage, man kann auch Funktionen in Strukte reinbauen. Schau Dir mal den folgenden Code an und überlege mal welche Vorteile es bietet. Der Code ist natürlich noch keine echte Objektorientierung.
Code:
#include <iostream>
#include <array>
#include <vector>
#include <algorithm>

struct Digitizer{
    
    void convert(){
        
        int last_digit = number % 10;
        number /= 10;
        
        if(last_digit != number){
            convert();
        }
        digit_vector.push_back(last_digit); // vorwärts einsortieren
    }
    
    void print_digits(){
        for(auto digit : digit_vector){
            std::cout << digit << std::endl;   
        }    
    }
    
    void reverse_digits(){
        std::reverse(digit_vector.begin(), digit_vector.end()); // reihenfolge umdrehen
    }
    
    void zahl_einlesen()
    {
        if(digit_vector.empty()){
            digit_vector.clear();
        }
        std::cout << "Geben Sie eine Zahl ein: ";
    	//std::cin >> number;
        number = 1234567890; // muss ich so machen, da coliru keine Consoleneingaben unterstützt
    }    
    
    // Member Variablen, Zugriff aus allen Memberfunktionen möglich!
    int number;
    std::vector<int> digit_vector;
};


int main()
{
    Digitizer myDigitizer;
    myDigitizer.zahl_einlesen();
    myDigitizer.convert();
    myDigitizer.print_digits();
    myDigitizer.reverse_digits();
    myDigitizer.print_digits();
}
 
@Del Torres
Der Code wäre eigentlich ein perfektes Beispiel dafür wie man es eben NICHT machen sollte.
Warum kommt später noch, bin grade noch unterwegs.

EDIT:
Solche Beispiele sind einfach Kontraproduktiv um jemanden zu verdeutlichen welche Vorteile Objektorientierung bietet. Es erzeugt auch den Eindruck das man alles so machen sollte, was definitiv falsch ist im Falle von C++.
Es wird Dateneingabe und Ausgabe gemischt, auf Member number und digit_vector besteht Zugriff, wodurch beide in einen asynchronen Zustand gebracht werden können. Man kann also im Grunde "Dumme Dinge" damit machen. :p

Ich würde für so einen Fall eigentlich überhaupt keine Klasse schreiben. Ich würde das eher mit sowas erledigen. Das geht zwar jetzt ein wenig weiter, aber soll ja nur mal als Beispiel dienen :Freak

Zum einen die Methode get_digits() welche alle Integralen Typen wie int, long, long long, usw. entgegen nehmen kann und nicht nur int.(Mit ein paar Zeilen mehr könnte man so auch Floating Point Typen integrieren) Zum anderen operator<<() welcher den von mir definierten Typ digits_t entgegen nimmt und die ausgabe in jedem std::ostream durchführen kann. also nicht nur in std::cout sondern auch in std::stringstream oder std::fstream.

Code:
#include <iostream>
#include <vector>
#include <algorithm>

int zahl_einlesen();

namespace mynamespace
{
	template<typename _Ty>
	using digits_t = std::vector<_Ty>;

	template<typename _Ty>
	digits_t<int> get_digits(_Ty number)
	{
		static_assert(std::is_integral<_Ty>::value, "_Ty must be integral");
		digits_t<int> digits;
		while (number > 0)
		{
			digits.push_back(number % 10);
			number /= 10;
		}
		std::reverse(std::begin(digits), std::end(digits));
		return digits;
	}
}

template<typename _Ty>
std::ostream & operator<<(std::ostream & os, mynamespace::digits_t<_Ty>& op)
{
	for (auto digit : op)
	{
		os << digit << "\n";
	}
	return os;
}

int main()
{
	auto zahl = zahl_einlesen();
	std::cout << mynamespace::get_digits<int>(zahl);
	return 0;
}

int zahl_einlesen()
{
	std::cout << "Geben Sie eine Zahl ein: ";
	int eingabe;
	std::cin >> eingabe;
	return eingabe;
}


EDIT_2:

Und hier mit Erweiterung für die Floating Point Typen :D

Code:
namespace mynamespace
{
	template<typename _Ty>
	using digits_t = std::vector<_Ty>;


	template<typename _Ty, 
		typename = std::enable_if_t<std::is_floating_point<_Ty>::value>>
	digits_t<char> get_digits(_Ty number)
	{
		auto _digits = std::to_string(number);
		return digits_t<char>(std::cbegin(_digits), std::cend(_digits));
	}

	template<typename _Ty, 
		typename = std::enable_if_t<std::is_integral<_Ty>::value>>
	digits_t<int> get_digits(_Ty number)
	{
		digits_t<int> digits;
		while (number > 0)
		{
			digits.push_back(number % 10);
			number /= 10;
		}
		std::reverse(std::begin(digits), std::end(digits));
		return digits;
	}
}

template<typename _Ty>
std::ostream & operator<<(std::ostream & os, mynamespace::digits_t<_Ty>& op)
{
	for (auto digit : op)
	{
		os << digit << "\n";
	}
	return os;
}

int main()
{
	auto zahl = zahl_einlesen();
	std::cout << mynamespace::get_digits<int>(zahl);
	std::cout << mynamespace::get_digits<double>(3234.1232);
	return 0;
}

int zahl_einlesen()
{
	std::cout << "Geben Sie eine Zahl ein: ";
	int eingabe;
	std::cin >> eingabe;
	return eingabe;
}
 
Zuletzt bearbeitet:
Aso es geht um eine perfekte Lösung für das Problem und nicht um jemand an OOP heranzuführen, sry. Das hab ich also falsch verstanden.

Ich hab doch geschrieben das der Code so keinen Sinn macht. :freak:
 
@Del Torres:
Was mir bei dem Code auffällt:
+ Er ist übersichtlicher/strukturierter, da die einzelnen Funktionen noch einmal zusammengefasst/gruppiert werden und nicht alle untereinander unter main stehen. Wobei dieser Aspekt erst wirklich zum Tragen kommen wird, wenn man mehrere Klassen verwendet.
+ Ich nehme an, dass er auch weniger Angriffspunkte für Fehler bietet, da man die Funktionen über "Objekt.Funktion()" aufruft und nicht nur über "funktion()". Hierdurch hat man womöglich eine Gedankenstütze um nicht fälschlicherweise eine falsche Funktion aufzurufen.
+ Bei den Funktionen werden keinerlei Variablen übergeben. Dadurch dürfte man auch einige Fehler vermeiden können.
+ Es ist unwahrscheinlicher, dass man versehentlich eine Variable durch eine andere Funktion/Klasse überschreibt, da man durch die Schreibweise "Objekt.Variable" mehr Informationen über die Variablen hat/benötigt, als wenn die Variable nur int zahl wäre.

Das wären jetzt so die Punkte, die mir mit meinem Anfängerwissen in den Sinn kommen :)


@Fonce:
Sicher interessant, nur leider verstehe ich fast gar nichts von dem was du da schreibst...Trotzdem danke.

Das fängt schon damit an:
[...]auf Member number und digit_vector besteht Zugriff, wodurch beide in einen asynchronen Zustand gebracht werden können.

Was ist ein asynchroner Zustand?

Und bei dem Code habe ich auch gar keine Chance :o


Gruß
thesi
 
Moin Thesi,
viele Punkte hast Du richtig erkannt.

"Objekt.Funktion()" bietet auch den Vorteil, das man Code completion nutzen kann. Wenn Du nicht gerade in einem Text Editor programmierst, sollte Dir nach der Eingabe von Objekt. die List der Variablen und Funktionen angezeigt werden. Vorteil in komplexen Projekten, man muss nicht alles kennen und kann sich Anhand von (hoffentlich) gut gewählten Funktionsnamen "entlanghangeln".
Das führt aber auch zu einem Problem! Ich habe ja bewusst die Syntax mit struct gewählt, um die Syntax einfach zu halten. Allerdings sind damit alle Funktionen von außen sichtbar (Stichwort zum merken: "public"). Oft will man das nicht. Bei Membervariablen ist es sogar schlechter Programmierstil - das hat Fonce richtig angemerkt.

In meinem Beispiel kann man objekt.zahl_einlesen(); objekt.convert(); objekt.number = 5; machen. Nun passt ja das Vector-Array nicht mehr zur zuletzt eingegeben Zahl. Deswegen muss man dem Programmierer verbieten, solche inkonsistenten Zustände zu erzeugen.
Also kommen wir mal zur echten Klassensyntax. Statt "struct" einfach "class" schreiben. Nun wird der Code nicht mehr kompilieren. Es kommt die Fehlermeldung, das zahl_einlesen() "private" ist. Im Gegensatz zu public, sind private Variablen oder Funktionen nicht von außen mit objekt.xyz aufzurufen.
Ein Programmierer sollte sich immer Gedanken machen, was die Schnittstelle nach außen für den Anwender der Klasse ist, und was Interna sind, die der Anwender nicht kennen muss (bis es ein Problem gibt^^).

Die Membervariablen number und digit_vector sind von außen nicht von Interesse. Sie dienen ja nur dem "zwischenspeichern". Die Funktion convert() ist auch nicht von Interesse. Ich setze mal den Anwendungsfall voraus, das nach jeder neuen Zahl zwangsläufig die Konvertierung stattfinden sollte.

Noch nen Bugfix für die Abbruchbedingung meiner Rekursion... und der Code mal umgestellt auf etwas das schon mehr eine Klasse ist.
Code:
#include <iostream>
#include <array>
#include <vector>
#include <algorithm>

class Digitizer{
    private:
        void convert(){
            
            int last_digit = number % 10;
            number /= 10;
            if(number != 0){
                convert();
            }
            digit_vector.push_back(last_digit); // vorwärts einsortieren
        }
        
        void reverse_digits(){
            std::reverse(digit_vector.begin(), digit_vector.end()); // reihenfolge umdrehen
        }        
        
    public:
        void setNumber(int new_number){
            
            if(!digit_vector.empty()){
                digit_vector.clear();
            }
            
            number = new_number;
            convert();
        }
        
        void print_digits(bool forwards = true){
            
            if(!forwards){
                reverse_digits();    
            }
            
            for(auto digit : digit_vector){
                std::cout << digit << std::endl;   
            }    
        }
        
        void zahl_einlesen()
        {
            int new_number = 0;

            std::cout << "Geben Sie eine Zahl ein: ";
        	//std::cin >> new_number;
            new_number = 1234567890; // muss ich so machen, da coliru keine Consoleneingaben unterstützt
            setNumber(new_number);
        }    
    
    // Member Variablen, Zugriff aus allen Memberfunktionen möglich!
    private:
        int number;
        std::vector<int> digit_vector;
};


int main()
{
    Digitizer myDigitizer;

    myDigitizer.zahl_einlesen();     // einlesen
    myDigitizer.print_digits();      // std fall vorwärts
    
    myDigitizer.setNumber(7331);     // direktes setzen
    myDigitizer.print_digits(false); // sonderfall rückwärts
}

Schau Dir mal den neuen Code an, ich hoffe mit meiner Erklärung von oben, kannst Du nachvollziehen was hier abläuft.
Kleine Anmerkung. setNumber kann man von außen aufrufen um eine neue Zahl zu setzen. Diese Funktion wird auch verwendet, wenn zahl_einlesen aufgerufen wird. Für mich ist das die zentrale Stelle um auch das leeren des Vektors (war auch nen bug drin, sorry^^) und das neue convert zu machen.

Auch hier gilt, ich habe noch viele wichtige Dinge weggelassen. Wichtig ist einfach das mit der Sichtbarkeit zu verstehen. private kann man nur innerhalb der Klasse verwenden, public ist die Schnittstelle nach außen. Variablen sind eigentlich immer als private zu wählen und mit get() und set() Methoden zu versehen. Das wäre das Lernziel für diese Unterrichtseinheit :D
Bonusziel ist zu verstehen, wie man das private und public nutzen kann, um die Verwendung der Klasse einfach zu halten.
Bonusziel 2: struct und class ist in C++ dasselbe. Lediglich ist in struct alles public per default, bei class alles private.

Ich denke, langsam wird es aber sehr komplex. Folgende Punkte kommen zum Thema Klassen noch dazu. Konstruktoren/Destruktoren habe ich noch komplett weggelassen. Zudem ist eigentlich das wichtigste Thema, die Ableitung von Klassen, auch noch zwingend nötig, um von Objektorientierung zu sprechen.
 
Wow, vielen vielen Dank für diese Erläuterung!!!!

Da konnte ich auf jeden Fall sehr viel mitnehmen.

Ich muss mir jetzt nur noch ein etwas größeres Projekt suchen, wo ich das mit den Klassen selbst mal versuchen kann.

Eventuell baue ich den Glücksspiel-Code mal neu auf.
Mit Klassen für die einzelnen Akteure und die zugehörigen Variablen.

Nochmal danke für die ganze bisherige Hilfe :)


Gruß
thesi
 
Jepp, den Kreis zum Glücksspiel zu ziehen, wäre sicher Sinnvoll.

Da wäre dann noch das Thema mit der Ableitung vielleicht interessant. So kann man "leicht" und schnell verschiedene Varianten an Spielen implementieren. Vielleicht schaust Du Dir das mal in einem Tutorial an im Netz. Bei Fragen stehe ich gerne weiter zur Verfügung.
 
Del Torres schrieb:
Zudem ist eigentlich das wichtigste Thema, die Ableitung von Klassen, auch noch zwingend nötig, um von Objektorientierung zu sprechen.
Wobei ich da mal eben einwerfen möchte, dass man nicht dadurch ein besserer Programmierer wird, dass man eine popelige Linked List mit einer Vererbungstiefe von 5 implementiert, sondern dass man verstehen muss, welche Relation Vererbung impliziert, was diese bedeutet und wann man diese benötigt. Falsch angewedet sorgt das ganze nur für schlechten Code.
 
Zurück
Oben