BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Bezpośrednie operacje na pamięci w C++/CLI cz.1

29-12-2006 19:19 | uzytkownik usuniety
W początkowym okresie, kiedy język C++ był dostosowywany do platformy .NET mieliśmy do czynienia z trzema rodzajami odwołania się do pamięci komputera. Były to zarządzane referencje, wskaźniki natywne oraz tzw. interior pointers. Problem w tym, że gdy programista zobaczył zapis T*, to bez deklaracji nie mógł rozróżnić wyżej wymienionych zapisów. Jest to kolejna rzecz którą poprawiono w C++/CLI. Od czasu opracowania standardu języka, zarządzane referencje używają punktatora T^, wskaźniki natywne


Czym są interior pointers?


Interior pointer to specjalny mechanizm opracowany przez twórców języka, który pozwala na bezpośrednie odniesienie się do zarządzanej sterty. Zazwyczaj mamy do czynienia właśnie z zarządzanymi obiektami, lub też ich składnikami, jednak wcale nie musi tak być bo interior pointers nie muszą się zawsze odnosić do sterty CLI.

Po co powstały? Właściwie, to głównym problemem w używaniu natywnych wskaźników w odniesieniu do sterty CLI, było to, że Garbage Collector mógł przesunąć wskazywany obiekt w inne miejsce podczas cyku odśmiecania zarządzanej pamięci. Znaczy to, że adres mógł się zmienić np. z 0x00BB0010 na 0x00BBC010 . Można sobie teraz wyobrazić co się stanie gdy na obiekt pokazywałby wskaźnik natywny. Przy próbie odniesienia się do miejsca, na które by wskazywał, dostalibyśmy coś co zostało przesunięte na miejsce naszego obiektu. Doprowadziłoby to do zniszczenia danych!

I w tym właśnie miejscu do gry wchodzą interior pointers. Dzięki opracowanemu mechanizmowi, GC za każdym razem uaktualnia adresy wskaźników, przez co nie można popełnić błędu, takiego jak opisany powyżej.

Oto przykładowy kod:

[Kod C++]

#include <iostream> //użyjemy std::cout, wolno nam :P

using namespace System;
using namespace std;

// deklaracja bardzo prostej klasy
ref class Test
{
public :
    Test( double f, double s, double t): m_i(f), m_j(s), m_k(t)

    {}
    // trzy pola, na których adresach będziemy operować (oczywiście na konkretnej instancji)
    double m_i, m_j, m_k;
};

// deklaracja funkcji, która zaalokuje tablicę o count elementach
array <Test^>^ DoLotsOfAllocs( unsigned count)
{
    array <Test^>^ a = gcnew array <Test^>(count); // alokacja tablicy
   
    for ( int i=0; i<count; i++) // wypełnienie tablicy instancjami klasy Test
        a[i] = gcnew Test(77, 88, 99);

return a;
}

int main()
{
    // ustalamy ilość elementów oraz alokujemy tablicę
    unsigned count = 1000000;
    array <Test^>^ a = DoLotsOfAllocs(count);

    // tworzymy kolejną instancję
    Test^ t = gcnew Test(99, 88, 77);

    // tworzymy wskaźnik żeby udowodnić, że adres obiektu się zmienia
    interior_ptr < double > p( &t->m_j );

    // wypisujemy pierwszy adres
    cout << "Adres: " << &p << ", wartosc: " << *p << endl;


    // alokujemy drugą bardzo dużą tablicę
        array<Test^>^ b = DoLotsOfAllocs(count);
        // wypisujemy adres naszego obiektu, który powinien być różny od pierwszego
    cout << "Adres: " << &p << ", wartosc: " << *p << endl;

return 0;
}


Przykładowy kod powinien wypisać dwa różne adresy wskaźnika pokazującego na ten sam obiekt, jednak nie zawsze to robi (zależy od bieżącego zużycia pamięci). W przypadku natywnego wskaźnika mielibyśmy duży problem ze znalezieniem naszego obiektu (właściwie to byłoby to niemożliwe). Nie ma jednak czym się martwić, bo kompilator nie pozwoli nam na pokazanie tego rodzaju wskaźnikiem na część zarządzanego obiektu.

Prześledźmy kolejny przykład, który powinien rozwiać jeszcze kilka wątpliwości:

[Kod C++]

value struct V
{
    int data;
};

V v;

interior_ptr <V> pv = &v;

V** p = &pv; // błąd

interior_ptr <V>* pi = &pv; // OK, pv jest na stosie więc jest l-wartością

int * p2 = &(pv->data); // bąłd

int * p3 = &(v.data); // OK, v jest na stosie, więc v.data jest l-wartością



Rzutowanie na interior pointer

Interior pointer staje się użyteczny gdy musimy wysłać do funkcji argument przez referencję.

Oto przykładowy kod:

[Kod C++]

#include <iostream>

using namespace System;
using namespace std;

ref class Test
{
public :
    Test( int _i): m_i(_i)
{}
    int m_i;
};

// deklaracja funkcji, do której przekazujemy parametr przez wskaźnik
void Square( interior_ptr < int > pNum)
{
    *pNum *= *pNum; // prosta zmiana wartości wskaźnika
}

int main()
{
    Test^ t = gcnew Test(20);

    interior_ptr < int > p = &t->m_i; // wskaźnik pokazuje na pole m_i obiektu t

    cout << *p << endl; // wartość przed

    Square(p); // zmiana wartości przez funkcję

    cout << *p << endl; // wartość po

    int a = 10;

    Square(&a); // to samo dla zwykłej zmiennej

    cout << a << endl;

return 0;
}

------ekran------
20
400
100


Na postawie przykładu widzimy jak łatwo przesłać oba wskaźniki do funkcji. Warto zauważyć, że wskaźniki natywne są automatycznie konwertowane do interior pointers. (W druga stronę nie jest to wykonalne).



Śledzące referencje


Oprócz sposobu pokazanego wyżej można równie dobrze użyć śledzącej referencji (ang. tracking reference):

[Kod C++]

// deklaracja funkcji, parametr przekazujemy przez referencję
void Square2(int% pNum)
{
    pNum *= pNum;
}

int main()
{
    Test^ t = gcnew Test(20); // tworzymy instancję klasy z poprzedniego przykładu

    cout << t->m_i << endl; // wartość przed wywołanie funkcji

    Square2(t->m_i);

    cout << t->m_i << endl; // wartość po

    int a = 10;

    Square2(a); // to samo dla zwykłej zmiennej

   
    cout << a << endl;

return 0;
}

Efekt działania tej funkcji będzie identyczny.

Śledzące referencje są bardzo podobne do interior pointers, jednak ustępują im w kilku kwestiach takich jak arytmetyka oraz porównywanie [wskaźników]. Cały mechanizm jest identyczny jak w standardowym C++. Wykorzystywane są tylko dodatkow operatory [unarne].

N* hn = new N; // alokacja na natywnej stercie
N& rn = *hn; // powiązanie referencji z obiektem

R^ hr = gcnew R; // alokacja na stercie CLI
R% rr = *hr; // powiązanie śledzącej referencji z uchwytem

R^ h = gcnew R; // alokacja na stercie CLI
R% r = *h; // powiązanie śledzącej referencji z uchwytem

void F(V% r); // deklaracja jedno parametrowej funkcji
F(*gcnew V); // powiązanie śledzącej referencji z uchwytem

Ogólnie rzecz biorąc punktatora % używamy z ^, natomiast & z *. Dodatkowo śledząca referencja różni się od zwykłej tym, że nie można zmieniać tego na co wskazuje.


Arytmetyka interior pointers


Właściwie to jeśli ktoś zna dobrze ISO C++ to może ten rozdział opuścić bo interior pointers zachowują się niemal identycznie jak zwykłe wskaźniki.

Zsumujmy teraz wszystkie elementy tablicy:

[Kod C++]

void ArrayStuff()
{
    // alokujemy nową tablicę – zwróćmy uwagę na bezpośrednią inicjalizację, przez tzw. agregację
    array<int>^ arr = gcnew array<int> {2,4,6,8,3,5,7};

    /* tworzymy wskaźnik na pierwszy element tablicy – niestety tu już nie można wykorzystać zapisu takiego jak     w standardowym C++, że identyfikator tablicy jest adresem pierwszego jej elementu, dlatego stosujemy         poniższy zapis*/
    interior_ptr<int> p = &arr[0];

    int s = 0;

    // korzystamy z operacji na wskaźnikach – przesuwania wskaźników
    while(p != &arr[0] + arr->Length)
    {
        s += *p; // sumujemy

        p++; // przeskakujemy do następnego elementu
    }

    cout << "Suma: " << s << endl;
}


Możemy także bezpośrednio manipulować stringami (należy dołączyć bibliotekę vcclr.h):

[Kod C++]

String^ str = "hello";

/*
PtrToStringChars zwraca interior pointer na Char. Muszimy to zrzutować na interior_ptr<wchar_t>. Równie dobrze możnaby było napisać:

interior_ptr<const Char> ppchar = PtrToStringChars( mystring );

jednak wtedy nie możnaby było zmieniać zawartości stringa
*/

interior_ptr<wchar_t> ptxt = interior_ptr<wchar_t>( PtrToStringChars(str) );

// przechodzimy po wszystkich elementach
for(int i=0; i<str->Length; i++)
    *(ptxt+i) = *(ptxt+i) + 1; // 'zwiększamy' każdy znak o 1 wykorzystując arytmetkę wskaźników

Console::WriteLine(str);


// Wyjście: ifmmp


Pętlę for można przerobić tak, żeby wyglądała w ten sposób:

[Kod C++]

/* zapis zrobi to samo co powyższy. Przechodzimy po kolejnych elementach przy okazji je zwiększając. Pętla skończy się gdy osiągniemy koniec stringa, czyli znak pusty */

for(; (*ptxt++)++;);


co skróci zapis, ale statnie się niestety mniej czytelny.


Kilka ważnych kwestii


Interior pointers nie możemy zadeklarować bezpośrednio w klasie. Poniższy zapis da nam błąd przy kompilacji:

[Kod C++]

ref class A
{
public:
    interior_ptr<A> ptr;

};


... niezależnie od tego na jaki typ zmiennych będzie pokazywał. Możemy natomiast używać wskaźników w zadeklarowanych metodach.

Dla typów bezpośrednich wskaźnik this to interior pointer. Można przypuszczać, że jest tak gdyż typy (klasy) bezpośrednie mogą być składnikami zarządzanych obiektów i używają wskaźnika this. Gdyby nie był to interior pointer, takie kombinacje mogłyby być fatalne w skutkach.

[Kod C++]

value class V
{
    void A()
    {
        interior_ptr<V> pV1 = this;

        V* pV2 = this; //tu dostaniemy błąd
    }
};


Nie można także użyć wskaźnika do obiektu referencyjnego:

interior_ptr<System::String>

Można natomiast zadeklarować wskaźnik do uchwytu do takiego obiektu:

interior_ptr<System::String^>

Wszystkie wskaźniki są niejawnie inicjalizowane wartością nullptr, która reprezentuje literał typu null. nullptr jest odniesieniem do tzw. null value constant, co oznacza że nie może powstać żadna instancja do tego obiektu, a jedyny sposób odniesienia się do wartości null to właśnie przez nullptr. Wartość tą można zrzutować na każdy wskaźnik bądź uchwyt.

Teraz trochę przykładów pokazujących co w C++/CLI wolno a czego nie:

Object^ obj1 = nullptr; // uchwyt obj1 ma wartość null

String^ str1 = nullptr; // uchwyt obj1 ma wartość null


if (obj1 == 0); // fałsz (0 -> boxing -> typ referencyjny. Uchwyty będą różne)

if (obj1 == nullptr); // prawda


char* pc1 = nullptr; // pc1 wskazuje na wartość null

if (pc1 == 0); // prawda, zero dla wskaźników natywnych jest równoważne z null

if (pc1 == nullptr); // prawda


int n1 = 0;

n1 = nullptr; // błąd, brak niejawnej konwersji do int

if (n1 == 0); // prawda, oczywiste

if (n1 == nullptr); // błąd, brak niejawnej konwersji do int

if (nullptr); // błąd

if (nullptr == 0); // błąd, brak niejawnej konwersji do int


nullptr = 0; // błąd, nullptr nie jest l-wartością

nullptr + 2; // błąd, nullptr nie może występować w działaniach arytmetycznych

Object^ obj2 = 0; // obj2 będzie uchwytem do 0 (przekształconego do typu referencyjnego)

String^ str2 = 0; // błąd, brak konwersji z int na String^

char* pc2 = 0; // pc2 wskazuje na wartość null


Zakończenie


Jak pewnie zauważyliście, temat wskaźników wewnętrznych nie jest trudny, gdyż podobne są one do zwykłych znanych z C++. Jedyne co potrzeba do swobodnego nimi manipulowania, to trochę doświadczenia, żeby wiedzieć w którym miejscu można ich używać. Mam nadzieję, że artykuł wyjaśnił trochę w tym kierunku.



Referencje

http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-372.pdf

http://www.codeproject.com/managedcpp/interiorpointers.asp


tagi: C++/CLI

Komentarze 1

n-j
n-j
87 pkt.
Poczatkujacy
n-j
21-01-2010
oceń pozytywnie 0
Semafory omówione poprzez wzorce projektowe, czy rozwiązania ogólnych problemów, do jakich można sprowadzić wiele konkretnych zagadnień programowania wielowątkowego. Zasada działania semaforów opisana jako logicznie spójna treść, uzupełniona jest przez źródła obfite w komentarze. Polecam i miłego multithreadingu!
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.