Just nu i M3-nätverket
Jump to content

Lite enkla/svåra frågor? #2


Jonas Innala

Recommended Posts

Jonas Innala

Jag ställer frågor än en gång till några som kan lite mer än jag. Jag programerar i C++.

 

1. Hur gör jag en 3D vector(STL)?

2. Går de att sätta inte en struct i en vector?

T.ex.:

 

struct blob {

int x;

int y;

int z;

bool used;

}

 

vector <blob> blub;

 

och sedan använda dem såhär:

blub[0].x

blub[2].x

blub[0].used

blub[6].y

 

3. Jag har en dator med 2 CPU så jag hade tänkt om jag kunde göra mitt program anpassat för en och två CPU. Hur gör jag program för 2 CPU?

 

4. Jag har upptäckt att i c++ standard biblotek(vissa) finns de speciella kommandon som inte är funktioner.

T.ex.:

 

fstream filen;

filen.open(filnamn,ios::binary);

filen<<heltal //här sätter jag in en variabel i filen med << går de att göra egna << på sina egna klasser?

 

Hoppas de var intresanta frågor och att de hjälper fler än jag.

 

Link to comment
Share on other sites

jerker olofsson

1) Du får skapa en vector med en vector:

std::vector<std::vector<int> > Veector3d;
Vector3d[0][0] = 10;

 

Observera att det måste vara ett mellanrum mellan int> och >. Då det misstolkas av C++ operators regler som en "right shift" >> annars.

 

2) Visst går det. Borde fungera precis som du skrivit. Se bara till att du har fyllt vector'n med objekt.

 

struct blob { int x,y,z; bool used; };
std::vector<blob> blub;
blob b1;
blob b2;
blob b3;
blub.push_back(b1);
blub.push_back(b2);
blub.push_back(b3);

blub[0].x = 10;

 

Observera att vectors kopierar dina objekt, och att b1, b2 och b3 inte längre är samma som i listan.

 

Lite överkurs som kan vara bra att läst, då du kan reflektera över detta någon gång då du stöter på problem:

std::vector använder sig av copy-contructorn och assignment (=) operatorn. I alla klasser kommer dessa att genereras av kompilatorn som default, och är i detta fall en memcpy. Det fungerar bra för så kallade POD-klasser (Pure-Old-Data). Men har du klasser i din klass så är det väl värt att skapa egna då alla objekt kanske inte fungerar med memcpy.

 

Ett annat exempel är en klassiskt enkel buffer-klass (eller alla med en pekare).

 

class Buffer
{
public:
Buffer( int Length )
{
Length_ = Length;
Ptr_ = new char[Length];
}

~Buffer()
{
delete [] Ptr_;
}

char* Ptr_;
int Length__;
};

 

Om du skapar en sådan här klass och stoppar in den i en vector så kommer det med högsta sannolikhet inte att gå så bra. Händelse förloppet blir följande:

 

1) Ett Buffer objekt allokeras.

2) Buffer::Length_ sätts till korrekt längd på buffern

3) Buffer::Ptr_ allokeras.

 

Sen när du stoppar in den i vector'n:

4) Ett nytt Buffer objekt allokeras

5) Det Buffer objektet du stoppade in kopieras (med default konstruktorn som är memcpy.) DVS att en ny buffer (Buffer::Ptr_) kommer inte att allokeras. De kommer att peka på samma minnesyta.

 

Minnet kommer att avallokeras två gånger (i destruktorn).

 

I detta fallet måste du skapa din egen kopiering för din klass:

 

class Buffer {

// Samma som förut..

 

// En copy-konstruktor

Buffer( const Buffer& rhs )

{

// Kopiera längden vanligt.

Length_ = rhs.Length_;

 

// Allokera en egen buffer.

Ptr_ = new char[Length_];

 

// Gör en kopia av buffern

memcpy( Ptr_, rhs.Ptr_, Length_ );

}

 

// Samt en assignment operator (Då denna används ner element flyttas i vektorn.)

const Buffer& operator = (const Buffer& rhs )

{

delete [] Buffer_;

// Samma kod som i copy-konstruktorn

return *this;

}

};

 

Nu kanske du undrar varför du måste skriva samma kod 2 gånger? Anledningen till att det finns både en copy-konstruktor och en assignment operator är följande:

Låt oss säga att du har en klass som heter A som har dessa egenskaper:

class A {

A()

{

// Default allokeras en 10 MB stor buffer

Ptr_ = new char[1024*1024*10];

}

 

A( const A& rhs )

{

// Samma som i Buffer::

}

 

const &A operator = (const &A rhs)

{

// Samma som i Buffer::

}

char* Ptr_;

int Length_;

};

 

Skapar du sedan klass B:

class B

{

public:

A a_;

};

 

Och bara använder dig av = operatorn, så kommer det att när du kopierar ett objekt:

B b1;

//:.

B b2 = b1;

 

b2 kommer först att allokera en 10MB stor buffer. Sedan direkt kommer b1's buffer att kopieras till b2.

 

En onödig allokering kommer att göras här.

 

Om du skriver kod (som den ovan) så kommer (även om du skriver = ), så kommer copy-konstruktorn att användas.

 

Skriver du däremot

B b2; // Default konstruktor

b2 = b1; // Assignment operator

 

Du kan även deklarera din konstruktor som "explicit", och då kommer den att använda sig av = operatorn om du inte anropar copy-konstruktorn "explicit", dvs:

B b2( b1 );

 

3) Det som du måste göra är att använda dig av trådar. Sök lite på google eller MSDN för exempel. Har du två trådar så kan varje tråd köras på en CPU var. Det finns inga egentliga genvägar, utan det är det enda sättet.

 

4) Det går att göra. Likadant som med assignment operatorn som jag skrev ovan.

 

class A {

public:
 int v;

 // Skapa en << operator som tar en int in, och sedan returnerar A objektet.
 const& A operator << ( const int MinInt )
 {
   v = MinInt;
   return *this;
 }

 // Likadant kan du göra för alla operatorer.
 bool operator == ( const A& rhs ) 
 {
   if( rhs.v == this->v ) return true;
   return false;
 }

 void operator += ( const A& rhs )
 {
   this->v += rhs.v;
 }

 void operator += ( const int VardeOkning )
 {
   this->v += VardeOkning;
 }

 void operator ~ ()
 {
   this->v = ~this->v;
 }

 // Sen finns det vissa special fall. Du kan också skapa en conversion-operator. Som omvandlar ditt objekt till ett annat objekt.
 operator std::string() 
 {
   // Skapa en sträng som innehåller talet "v".
   std::stringstream ss;
   ss << v;
   return ss.str();
 }
};

 

Med conversion-operatorn så kommer du att kunna använda din klass såhär:

 

void fun( std::string a )
{
 std::cout << "roligt: " << a;
}

int main()
{
 std::string b( "Detta är väl " );
 A a;
 a << 10;
 a += 10;
 fun( b );
 fun( a );
 std::cout << "\r\n";
}

 

main() kommer att skriva ut:

"roligt: Detta är väl roligt: 20"

 

Lycka till!

 

Link to comment
Share on other sites

jerker olofsson

För punkt 2) kan jag tillägga att:

 

Du även kan initiera din vector med flera objekt genom följande:

 

std::vector<blob> blub( 20 );

 

blub[0].x = 10;

blub[19].z = -2;

 

osv..

 

 

 

Link to comment
Share on other sites

jerker olofsson

3)

För att utveckla trådar lite..

 

Om du kodar win32, kolla in

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dllproc/base/createthread.asp

 

Annars fungerar posix-trådar för de flesta operativsystem (*nix).

 

Men koncepten är mycket lika.

 

Det du bör studera är hur man startar och stoppar trådar, samt säkerhet för trådar, då många problem som är mycket svåra att förstå kan uppstå.

 

Om du har två trådar som kör på 1 CPU (På de flesta "vanliga" operativsystem kan du inte bestämma själv om trådarna skall köra eller "vänta" på att köra).

 

Operativsystemets tråd/process scheduling kommer att bestämma vilken tråd det är som får köra nästa gång. Har du fler än en CPU så kan båda köra samtidigt, och i båda fallen så är det viktigt att du inte är inne och pillar i samma data samtidigt.

 

Ett enkelt exempel är följande:

static int Counter = 0;

while( 1 )

{

++Counter;

std::cout << Counter << std::flush;

--Counter;

}

 

Kör du detta exempel med en tråd, så kommer det självklart bara skrivas ut ettor hela tiden. Men om du kör det med två trådar, så kan det hända att operativsystemet byter tråd, en sk ContextSwitch efter att ++Counter, men innan --Counter. Då kommer den aktiva tråden att skriva ut en tvåa istället.

 

Tänk också på att ContextSwitch's inte görs i C++ nivå, utan i kompileradkod. Så efter varje instruktion skulle en contextswitch kunna inträffa.

 

Hade exemplet bara ökat ++Counter, så hade exemplet varit relativt säkert, eftersom ++Counter med största sannolikhet bara är 1 CPU instruktion. Däremot skulle den kunna byta tråd efter att "Counter" skrivits ut, men innan radbrytningen.

 

En annan viktig sak att skydda är alla CTL containers. Ofta när du lägger till objekt, eller tar bort, så kommer alla iteratorer bli ogiltliga. Därför måste du skydda dessa också. Och i vissa fall när ett objekt tar bort sig själv ur en lista kan det hjälpa att kopiera containern innan man itererar den. Ett exempel på detta kan vara ett klassiskt Observer-Pattern.

 

Exempel:

class Subject;

class Observer {

virtual void Notify( Subject* s ) = 0;

};

 

class Subject {

public:

void NotifyObservers()

{

for( std::set<Observers*>::iterator it = Observers_.begin(); it != Observers_.end() ++it )

{

(*it)->Notify( this );

}

}

 

void AddObserver( Observer *s )

{

Observers_.insert( s );

}

 

void RemoveObserver( Observer *s )

{

Observers_.erase( s );

}

 

private:

std::set<Observers*> Observers_;

};

 

// Och här kommer din klass, som kommer att ta bort sig själv från listan när den blir meddelad av "Subject"

class ObserverImpl : public Observer

{

public:

ObserverImpl( Subject& s )

{

s.AddObserver( this );

}

void Notify( Subject* s )

{

s->RemoveObserver( this );

}

};

 

int main()

{

// Skapa ett subject

Subject s;

 

// Skapa flera av dina klasser.

// Dessa klasser kommer att lägga in sig i Subject's std::set container i sin c'tor.

ObserverImpl i1( s );

ObserverImpl i2( s );

ObserverImpl i3( s );

ObserverImpl i4( s );

 

// Meddela alla Observers.

// Dessa kommer nu enligt implementation av Observern att direkt ta bort sig ur listan, vilket kommer att medföra att det kommer att krasha för att iteratorn "it" i NotifyObservers() metoden kommer att bli invaliderad inuti loopen. För att lösa detta så blir du tvungen att skapa en kopia av containern, innan du itererar den. Se nedan.

s.NotifyObservers();

}

 

// Korrekt implementation

std::set<Observers*> Copy( Observers_ );

for( std::set<Observers*>::iterator it = Copy.begin(); it != Copy.end() ++it )

{

(*it)->Notify( this );

}

 

 

Men även detta är inte trådsäkert! Då flera trådar kan ropa på NotifyObservers() samtidigt, och fel då kan uppstå någonstans i STL-containern.

 

För att skydda sk delad-data mellan dina trådar så finns det ett klassiskt hjälpmedel kallat "Mutual Exclusive".. MutEx.

 

Ett mutex har egenskaperna att det kan låsas och låsas upp. Om man försöker låsa det 2 gånger så kommer den tråden att vänta tills det är upplåst.

 

Implementationen av mutex finns både för posix och win32, och är relativt lika. (win32: CreateMutex, pthread: pthread_mutex_t) I mitt exempel har du redan skrivit en klass som kapslar in den koden. (class Mutex). Mutex klassen har 2 metoder, lock() och unlock().

 

För att ta det första exemplet igen.

static int Counter = 0;

static Mutex m;

while( 1 )

{

m.Lock();

++Counter;

std::cout << Counter << std::flush;

--Counter;

m.Unlock();

}

 

Inga konstigheter här. Koden mellan m.Lock() och m.Unlock() är skyddad, så att bara en tråd kan köra den samtidigt.

 

Nu så är det bara ett problem kvar. Counter variabeln kan ha lagt sig i ett register efter kod-optimering. I just detta exempel är det ingen fara, eftersom räknaren alltid är 0 när en tråd kör loopen. Men säg att du tar bort --Counter.

 

Därför bör du också deklarera all känslig data med typen "volatile". Volatile säger till C++ kompilatorn att den inte får stoppa just den variablen i ett register, utan det måste ligga i minnet hela tiden.

 

Nu får du en sådan här snygg typ:

static volatile unsigned int Counter = 0;

 

Med det andra exemplet med Observer'n. Så måste du skydda listan. "Copy" behöver du inte skydda då den inte är delad mellan trådarna (den är lokal och inte static).

 

Så du behöver ha ett mutex, som alla trådar kan dela.

Det kan du lägga till i Subject klassen som en member.

 

Sen behöver du låsa/låsa upp det runt:

- Kopiering av containern sker, dvs när Copy skapas.

- erase()

- insert()

 

Sen kan i alla fall inte jag hitta några icke-trådsäkra fall med det exemplet. Men det finns säkert någon här som vill tillägga något.

 

Trådprogrammering ser enkelt ut till att börja med, men sedan blir det lite mer komplicerat när "konstiga" problem uppstår. Det är svårare att debugga. Kanske uppstår inte ens problemen när man debuggar. Så man måste tänka sig för extra noga när man skriver trådsäkert kod.

 

Jag avslutar med en trådimplementation för win32


#include <windows.h>
#include <iostream>

DWORD WINAPI MyThread( LPVOID lpParameter )
{
 int Counter = 0;
 for(;
 {
   std::cout << Counter++ << std::endl;
   Sleep( 1000 );
 }
}

int main()
{
 DWORD ThreadID = 0;
 HANDLE hThread = CreateThread( NULL, 0, MyThread, 0, 0, &ThreadID );

 // nu är tråden startad, så vi måste vänta här och inte avsluta, för då kommer tråden att stängas.

 // När vi matat in ett tecken kommer applikationen att avslutas.
 char c;
 std::cin >> c;

return 0;
}

 

Och jag lägger till en enkel Mutex klass för win32.

class Mutex
{
public:
Mutex()
{
  Mutex_ = CreateMutex( NULL, FALSE, NULL );
}
void Lock()
{
  WaitForSingleObject( Mutex_, INIFINITE );
}
void Unlock()
{
  ReleaseMutex( Mutex_ );
}
private:
HANDLE Mutex_;
};

 

 

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...