C++ Bit-Editor

Der Bit-Editor

in der Konsole


Ich möchte hier jedem einen leichten Einstieg in die Programmierung ermöglichen. Jedes gängige Tutorial ist entweder "staubtrocken" oder "schwer geschrieben" (für Studierende). Außerdem befasst sich so gut wie jedes Tutorial nur mit theoretischer Programmierung, die man aber niemals so umsetzen würde. Hier möchte ich einhaken und jedem Projekte nahebringen, die nicht einfach anzueignen sind, aber schon recht schnell Erfolge zeigen.

Trotzdem ist dieses Tutorial an alle gerichtet die zumindest die klassischen Kontrollstrukturen beherrschen.
https://www.w3schools.com/cpp/ -englisch
http://www.online-tutorials.net/c-c++-c/c++-tutorial-teil-1/tutorials-t-1-58.html


Ich gehe hier aber trotzdem nicht auf alles ein und möchte hier schonmal weiterführende Thematik verlinken.

Dualsystem:
https://de.wikipedia.org/wiki/Dualsystem
https://www.inf-schule.de/informati...on/binaerdarstellungzahlen/konzept_dualsystem
Hexadezimalsystem:
https://de.wikipedia.org/wiki/Hexadezimalsystem
https://www.inf-schule.de/informati...erdarstellungzahlen/konzept_hexadezimalsystem
Oktalsystem:
https://de.wikipedia.org/wiki/Oktalsystem
Bitmanipulation:
https://www.c-howto.de/tutorial/variablen/bitmanipulation/ C
https://www.monsterli.ch/blog/programmierung/cpp/c-c-bitmanipulation-mit-bitweise-operatoren/ C++


Warum sollte ich mich überhaupt damit befassen?

Zum einen ist das absolutes Grundwissen und wer den Bit-Editor oder die Bitoperationen verstanden hat, kann daran aufbauen. Weiterführende Projekte wären z.B.:
  • Kryptographie für Dateien oder Netzwerktechnik
  • Ein Hex-Editor

Eine Einführung in die Stellenwertsysteme

Das Binär- oder Dualsystem hat die Basis 2 und 2 Zustände: 0 und 1. Es hat nur 1 Bit.

Das Hexadezimalsystem wird im Stellenwertsystem mit der Basis 16 dargestellt. Das heißt ein Hexadezimalsystem besteht aus 4 Bits. Binär:1111=15. Mit 0000 sind das 16 Zustände. Da wir in der Programmierung aber nur mit Byte-Datentypen arbeiten, müssen wir diese immer mindestens in einem Paar darstellen. Ein 1Byte großer Datentyp wäre zum Beispiel ein char. Daher seht ihr normalerweise immer die Darstellung A1(10100001) oder bei Farben z.B. #FF0000 (rot) [3Bytes = 11111111 00000000 00000000]. Wie ihr vielleicht schon seht hat das Hexadezimalsystem eine weitere Besonderheit. Es stellt außer Zahlen auch Buchstaben dar. Das liegt daran, dass das Hexadezimalsystem alle Zahlen nach 9 mit Buchstaben darstellt. Also besteht das Hexadezimalsystem aus folgenden Ziffern: 0123456789ABCDEF.

Das Oktalsystem hat die Basis 8. Ich kann also ein Oktal in 3 Bits darstellen. Binär:111=7. Wie oben schon genannt haben wir also mit 000 wieder 8 Zustände.

Das Dezimalsystem hat die Basis 10 und ist soweit jedem bekannt, darauf muss ich hier jetzt nicht weiter eingehen.


Zur Veranschaulichung nochmal eine Tabelle mit einer 4-Bit-Darstellung zum besseren Verständnis.

DezimalOktalHexadezimalDual
0000000
1110001
2220010
3330011
4440100
5550101
6660110
7770111
81081000
91191001
1012A1010
1113B1011
1214C1100
1315D1101
1416E1110
1517F1111


Programmierparadigma

Es gibt verschiedene "Wege" zu programmieren. Wie z.B. die prozedurale, modulare oder objektorientierte (OOP) Programmierung. Ich gehe hier zwar auf alle ein, werde mich aber auf die objektorientierte und modulare konzentrieren, da sie eine bessere Wartbarkeit gewährleisten. Wenn wir später unseren Code bearbeiten möchten, können wir beispielsweise direkt in das Modul "springen" und diesen Teil verändern.

Weiterführende Literatur:
https://de.wikipedia.org/wiki/Programmierparadigma
https://www.it-talents.de/blog/it-talents/prozedurale-vs-objektorientierte-programmierung

prozedural:
https://de.wikipedia.org/wiki/Prozedurale_Programmierung
https://www.itwissen.info/Prozedurale-Programmierung-procedural-programming.html
https://softech.cs.uni-kl.de/homepage/teaching/SeIWS0910/SeIWS0910Material/SE4_4.pdf -code

OOP:
https://de.wikipedia.org/wiki/Objektorientierte_Programmierung
https://programmieren-starten.de/blog/objektorientierte-programmierung/
https://www.kompf.de/cplus/tutor.html -code

modular:
https://de.wikipedia.org/wiki/Modulare_Programmierung



Coding

Anmerkung: Die hier gezeigten Codebeispiele entsprechen nicht "gutem" Programmierstil. Ich habe einen eigenen Programmierstil, der mir persönlich gefällt. Bitte nehmt euch kein Beispiel daran!

Programmierstil:
https://medium.com/@tristan.lins/programmierstil-34a12c1b7820
http://users.informatik.uni-halle.de/~abgnr/stuff/guterprogrammstil.pdf



Prozeduale Programmierung​




Die Standardbibliothek - Teil 1​


Jetzt beschäftigen wir uns mit dem eigentlichen Code. Doch wie realisieren wir dies? Die einfachste und vielleicht beste Methode ist einfach die Standardbibliothek zu verwenden. Die Vorteile von bestehenden Bibliotheken sind:
-sie sind stabil, da sie mehrfach durchgetestet sind
-sie sind schnell, da sie daraufhin programmiert und getestet sind

https://de.wikipedia.org/wiki/C++-Standardbibliothek



bitset


Um unsere Variable binär darzustellen gibt es die bitset-Funktion. Diese benötigt den bitset-Header. Diesen bindet ihr über #include <bitset> ein. Wir können die Binärdarstellung also einfach über folgenden Code realisieren:
C++:
#include <bitset>
#include <iostream>

int main()
{
int i = 254;
std::cout << std::bitset<32>(i) << std::endl;
return 0;
}
Die bitset-Funkion verlangt eine konstante Größe vor der Laufzeit. 32-Bit haben inzwischen alle Computer, also können wir damit am besten arbeiten.

Output:
Code:
00000000000000000000000011111110

bitset:
https://docs.microsoft.com/de-de/cpp/standard-library/bitset-class?view=msvc-160
https://en.cppreference.com/w/cpp/utility/bitset -englisch



Ausgabe-Manipulatoren​


Die Standard-Bibliothek #include <iostream> hat Ausgabe-Manipulatoren um das Dezimalsystem std::dec, Hexadezimalsystemstd::hex und Oktalsystem std::oct darzustellen. Wir erweitern unser vorherigen Code mit der Binärdarstellung also wie folgt:
C++:
#include <bitset>
#include <iostream>

int main()
{
int i= 254;
std::cout << "bin:" << std::bitset<32>(i) << std::endl;
std::cout << "dec:" << std::dec << i << std::endl;
std::cout << "hex:" << std::hex << i << std::endl;
std::cout << "oct:" << std::oct << i << std::endl;
return 0;
}
Ihr müsst nur die entsprechende Formatierung vor die Variable setzen.

Output:
Code:
bin:00000000000000000000000011111110
dec:254
hex:fe
oct:376

Stream-Manipulatoren:
https://www.kompf.de/cplus/artikel/stream2.html
https://en.cppreference.com/w/cpp/io/manip -englisch



Bitoperationen

des bitset-Containers


bitset<4> bs;
Bit setzen: bs.set(2,1); -setzt das 2. Bit auf 1
Bit löschen: bs.reset(1); -löscht das 1. Bit
Bit invertieren: bs.flip(2); -invertiert das 2. Bit

Es wird dabei von rechts nach links und von 0 (0-sequenziert) gezählt.


Die klassischen Bitoperationen:
http://www.willemer.de/informatik/cpp/sysprog.htm
https://cpluspluspedia.com/de/tutorial/2572/bitoperatoren
https://de.wikibooks.org/wiki/C++-Programmierung/_Nützliches/_Logische_Bitoperatoren



Der erste Bit-Editor

C++:
#include <bitset>
#include <iostream>

int main() {
  int i, iPos;
  char c;
  std::bitset<32> bs;

  while(i!=1) {
   std::cout << "input:";
   std::cin >> i >> c >> iPos;

   bs = i;
   if(c == '+') { bs.set(iPos, 1); }
   else if (c == '-') { bs.reset(iPos); }
   else if (c == '~') { bs.flip(iPos); }

   i = bs.to_ulong();
   std::cout << "bin:" << bs << std::endl;
   std::cout << "dec:" << std::dec << i << std::endl;
   std::cout << "hex:" << std::hex << i << std::endl;
   std::cout << "oct:" << std::oct << i << std::endl;
   std::cout << std::endl; }

  return 0;
}
to_ulong konvertiert den Inhalt des Bitsets in ein unsigned long.

Output:
Code:
input:254 + 0
bin:00000000000000000000000011111111
dec:255
hex:ff
oct:377

input:1022 + 0
bin:00000000000000000000001111111111
dec:1023
hex:3ff
oct:1777

input:24 + 8
bin:00000000000000000000000100011000
dec:280
hex:118
oct:430

input:0 + 0
bin:00000000000000000000000000000001
dec:1
hex:1
oct:1
Wie in diesem Beispiel gezeigt, könnt ihr das ganze gerne mal ausprobieren.


Jetzt könnten wir das Tutorial hier beenden, alle Funktionen sind erklärt worden...


Das Problem dabei ist, wir haben hier einen prozeduralen Ansatz verfolgt. Mit einer Menge Fallstricke wie z.B.
  • Der Bitset-Container verlangt eine konstante Größe vor der Laufzeit. Was aber, wenn wir diese selbst nicht kennen?
    • er sollte also je nach Variablengröße angepasst werden.
  • Es fehlen wichtige Routineprüfungen - z.B. prüfen auf richtige Usereingaben.
Die genannten Punkte und mehr möchte ich hier noch abarbeiten. Genauso wie modulare und objektorientierte Programmierung.




Modulare Programmierung​

Jetzt versuchen wir das ganze mal mit Funktionen.
C++:
#include <bitset>
#include <iostream>

void binDisplay(int var) { std::cout << "bin:" << std::bitset<32>(var) << std::endl; }
void decDisplay(int var) { std::cout << "dec:" << std::dec << var << std::endl; }
void hexDisplay(int var) { std::cout << "hex:" << std::hex << var << std::endl; }
void octDisplay(int var) { std::cout << "oct:" << std::oct << var << std::endl; }

std::bitset<32> bitMan(int var, int iPos, char c) {
  std::bitset<32> bs(var);

  switch(c) {
   case '+':bs.set(iPos,1); break;
   case '-':bs.reset(iPos); break;
   case '~':bs.flip(iPos); break; }

  return bs;
}

int main() {
  int i, iPos;
  char c;

  while(i!=1) {
   std::cout << std::endl << "input:";
   std::cin >> i >> c >> iPos;
   i = bitMan(i,iPos,c).to_ulong();
   binDisplay(i);
   decDisplay(i);
   hexDisplay(i);
   octDisplay(i); }

  return 0;
}

Output:
Code:
input:5 + 5
bin:00000000000000000000000000100101
dec:37
hex:25
oct:45

input:97 + 0
bin:00000000000000000000000001100001
dec:97
hex:61
oct:141

input:6 + 13
bin:00000000000000000010000000000110
dec:8198
hex:2006
oct:20006

input:0 + 0
bin:00000000000000000000000000000001
dec:1
hex:1
oct:1

Funktionen besitzen 3 Bestandteile. Den Rückgabewert, die Funktionsbezeichnung und die Parameter.

Der Rückgabewert [->]void[<-] binDisplay(int var)(void heißt in diesem Fall das es keinen Rückgabewert hat, da wir nur Text in der Konsole ausgeben). Der Rückgabewert kann dabei jeder Datentyp oder Container sein (theoretisch auch Zeiger oder Referenzen, diese werden dann aber als Parameter übergeben).

Die Funktionsbezeichnung.void [->]binDisplay[<-](int var)

Parametervoid binDisplay([->]int var[<-]). Diesen haben wir hier mit dem copy-constructor übergeben. Das heißt alle Variablen werden beim übergeben nur kopiert. Die eigentlich Variable in der main-Funktion ändert sich nicht, außer wir übergeben den Rückgabewert direkt der ursprünglichen Variable. Wenn wir Variablen per Referenzen übergeben(dabei wird die Speicheradresse der Variable übergeben), können wir die ursprüngliche Variable direkt verändern.

copy-constructor:
https://de.wikipedia.org/wiki/Kopierkonstruktor
http://www.willemer.de/informatik/cpp/copyconst.htm

Referenzen:
https://de.wikipedia.org/wiki/Referenzen
https://de.wikibooks.org/wiki/C++-Programmierung/_Weitere_Grundelemente/_Referenzen

Zeiger:
https://de.wikipedia.org/wiki/Zeiger_(Informatik)
https://de.wikibooks.org/wiki/C++-Programmierung/_Weitere_Grundelemente/_Zeiger
http://www.willemer.de/informatik/cpp/pointer.htm




Referenzparameter​


C++:
#include <bitset>
#include <iostream>

void binDisplay(int &var) { std::cout << "bin:" << std::bitset<32>(var) << std::endl; }
void decDisplay(int &var) { std::cout << "dec:" << std::dec << var << std::endl; }
void hexDisplay(int &var) { std::cout << "hex:" << std::hex << var << std::endl; }
void octDisplay(int &var) { std::cout << "oct:" << std::oct << var << std::endl; }

std::bitset<32> bitMan(int &var, int &iPos, char &c) {
  std::bitset<32> bs(var);

  switch(c) {
   case '+':bs.set(iPos,1); break;
   case '-':bs.reset(iPos); break;
   case '~':bs.flip(iPos); break; }

  return bs;
}

int main() {
  int i, iPos;
  char c;

  while(i!=1) {
   std::cout << std::endl << "input:";
   std::cin >> i >> c >> iPos;

   i = bitMan(i,iPos,c).to_ulong();
   binDisplay(i);
   decDisplay(i);
   hexDisplay(i);
   octDisplay(i); }

  return 0;
}
Der Code unterscheidet sich nicht sonderlich von dem davor.
Kopierkonstruktor: std::bitset<32> bitMan(int var, int iPos, char c)
Referenzparameter: std::bitset<32> bitMan(int &var, int &iPos, char &c)
Bis auf einen Unterschied: die Paramater haben ein &. Das ist ein Referenzparameter.

Bei Referenzen wird die Speicheradresse der Variable übergeben. So wird der Code im Speicher quasi reduziert, dadurch dass die Variable nicht kopiert, sondern direkt verändert wird.

TIPP: Jeder (C++-)Programmierer sollte sich es angewöhnen Parameter per Referenz zu übergeben.

Referenzen:
https://de.wikibooks.org/wiki/C++-Programmierung/_Weitere_Grundelemente/_Referenzen



Die Header​

Programmcode der in Funktionen oder Klassen steht, kann genauso ausgelagert werden (Klassen müssen sogar ausgelagert werden, das werde ich aber im nächsten Kapitel behandeln). Das heißt dieser wird in eine separate Datei geschrieben und im Hauptprogramm wieder eingebunden. Das kann sehr vorteilhaft sein, wenn man große Projekte mit verschiedenen Funktionen hat.

Ein Beispiel:
C++:
// bit.h
#ifndef BIT_H
#define BIT_H

#include <iostream>

void binDisplay(int &var) { std::cout << "bin:" << std::bitset<32>(var) << std::endl; }
void decDisplay(int &var) { std::cout << "dec:" << std::dec << var << std::endl; }
void hexDisplay(int &var) { std::cout << "hex:" << std::hex << var << std::endl; }
void octDisplay(int &var) { std::cout << "oct:" << std::oct << var << std::endl; }

std::bitset<32> bitMan(int &var, int &iPos, char &c) {
  std::bitset<32> bs(var);

  switch(c) {
   case '+':bs.set(iPos,1); break;
   case '-':bs.reset(iPos); break;
   case '~':bs.flip(iPos); break; }

  return bs;
}

#endif
Header-Dateien in C++ haben die Dateiendungen: .h oder .hpp .

C++:
//bit.cpp
#include <iostream>
#include "bit.h"

int main() {
  int i, iPos;
  char c;

  while(i!=1) {
   std::cout << std::endl << "input:";
   std::cin >> i >> c >> iPos;

   i = bitMan(i,iPos,c).to_ulong();
   binDisplay(i);
   decDisplay(i);
   hexDisplay(i);
   octDisplay(i); }

  return 0;
}
Header-Dateien im Compiler-Verzeichnis werden z.B. mit #include <iostream> aufgerufen. Wird eine Header-Datei aufgerufen die man selbst geschrieben hat, wird diese z.B. mit include "bit.h" aufgerufen(sofern sich diese im selben Ordner befindet, wie die Hauptdatei). Eigene Header-Dateien können auch über relative Pfadangaben aufgerufen werden, wenn sich diese in einem anderen Ordner befinden z.B.: #include "../h/bit.h".

Header:
https://docs.microsoft.com/de-de/cpp/cpp/header-files-cpp

Was als erstes in dem Beispiel auffällt ist in Zeile 2 #ifndef BIT_H und in Zeile 3 #define BIT_H. Abgeschlossen wird das Ganze in Zeile 24 mit #endif. Das sind Präprozessor-Anweisungen, die verhindern das Header-Dateien mehrfach eingebunden werden.
Diese Programmiertechnik nennt man einen Include-Guard. Wird diese Präprozessor-Direktive nicht eingesetzt, kann es passieren, dass die Header mehrfach kompiliert und das Programm größer ist als es sein sollte. Das geschieht, wenn man z.B. mehrere Header-Dateien(Module) hat, die aufeinander aufbauen, aber im Hauptprogramm genauso benötigt werden.

Präprozessor:
https://de.wikipedia.org/wiki/Präprozessor
https://docs.microsoft.com/de-de/cpp/preprocessor/preprocessor-directives
http://openbook.rheinwerk-verlag.de/c_von_a_bis_z/010_c_praeprozessor_002.htm -C

Include-Guard:

https://de.wikipedia.org/wiki/Include-Guard

Ersetzen kann man das auch mit der Anweisung #pragma once. Wird weitestgehend auch von allen allen Compilern unterstützt ist aber kein Standard.

#pragma once:
https://en.wikipedia.org/wiki/Pragma_once

TIPP: Den Include-Guard in jeder Header-Datei einsetzen.

-----------------------------------------------------------------------------------------------------------------------------------------------
Ich werde hier das Projekt weiter vorantreiben. Allerdings bin ich zeitlich gerade eingeschränkt.
Ihr könnt mir aber gerne schonmal Kritik dalassen. Ich freue mich auf jeden dem ich helfen konnte oder der etwas hierzu beitragen kann. Hoffe das ganze ist nicht zu ausführlich gehalten :)

Nur zum sichergehen. Ich habe bereits einen funktionierenden Bit-Editor mit einer modularen, objektorientierten Herangehensweise. Außerdem berechnet dieser die verschiedenen Darstellungen selbst. Den werde ich natürlich ganz zum Schluss vorstellen.

Ich möchte nur zeigen, wie man mit Bibliotheken und wie quasi "selbst" programmiert.

Ich will hier nur eine bestimmte Herangehensweise verdeutlichen, wie man als Anfänger an so was herangehen kann(sollte?).

Außerdem werde ich, wenn das hier gut ankommen sollte mich weiteren Projekten widmen und hier vorstellen, wie z.B.
-Kryptographie
-Android-Apps
-Netzwerkprogramme
...
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: merlin123

jaredgerald

Newbie
Ersteller dieses Themas
Dabei seit
Jan. 2021
Beiträge
3
Zitat von deineMudda:
Warum verwendest du nicht
using namespace std
Habe ich als Anfänger auch gemacht. Erspart ne Menge Code. Aber wird schnell sinnlos bei verschiedenen Headern mit denselben Funktionsbezeichnungen. Wie @BAGZZlash auch schon verlinkt hat ist das inzwischen auch "bad practice". Sieh dir das mal an->https://www.geeksforgeeks.org/cons-using-whole-namespace-c/

Eines der Dinge die man sich als C++ Programmierer angewöhnen sollte! Immer den Namespace vor der Funktion: z.B. std::cout, std::cin, std::string etc.
 
Zuletzt bearbeitet:
D

deineMudda

Gast
Zitat von jaredgerald:
Habe ich als Anfänger auch gemacht. Erspart ne Menge Code. Aber wird schnell sinnlos bei verschiedenen Headern mit selben Funktionsbezeichnungen. Wie @BAGZZlash auch schon verlinkt hat ist das inzwischen auch "bad practice". Sieh dir das mal an->https://www.geeksforgeeks.org/cons-using-whole-namespace-c/

Eines der Dinge die man sich als C++ Programmierer angewöhnen sollte! Immer den Namespace vor der Funktion: z.B. std::cout, std::cin, std::string etc.
Man muss die namespaces nicht global verwenden. Da in dem Beispiel außer dem namespace std nichts mehr vorkommt, spricht in dem Fall auch nichts dagegen.
 
  • Gefällt mir
Reaktionen: jaredgerald

jaredgerald

Newbie
Ersteller dieses Themas
Dabei seit
Jan. 2021
Beiträge
3
Absolut richtig! Es ist nur so, dass wir alle Gewohnheitstiere sind und ich lieber weiterhin den namespace vor der Funktion schreibe, als es irgendwann bei großen Projekten zu vergessen. Außerdem finde ich es gut, dass man so weiß welche Funktionen direkt zur Standardbibliothek gehören und welche zur boost-library zum Beispiel.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: BAGZZlash

Ähnliche Themen

Top