Press "Enter" to skip to content

Operator new – kilka słów na jego temat

Operator new – kilka słów na jego temat

Operator new, jak zapewne wszyscy wiedzą służy w językach obiektowych/ zorientowanych obiektowo do kreacji nowych obiektów. W przypadku języka C++ używamy go również do tworzenia zmiennych dynamicznych typów wbudowanych, na które pokazuje wskaźnik. Zmienna dynamiczna bowiem sama w sobie nazwy niestety nie ma. Ale ma adres który można przypisać do wskaźnika, a to nam w zupełności wystarczy do posługiwania się nią. Zmienna dynamiczna nie podlega prawom zakresu ważności (co innego wskaźnik, który na tą zmienną pokazuje). Jeżeli w danym zakresie ważności mamy dostęp do przynajmniej jednego wskaźnika na zaalokowany obszar pamięci, będzie od dostępny. Tracimy zmienną dopiero po użyciu bliźniaczego operatora delete (którym nie będziemy się zajmować w tym artykule).  Wybaczcie mi to masło maślane.
Zmienne tworzone dynamicznie w języku c++ nie są zerowane, należy o tym pamiętać.
Artykuł ten stanowi vademecum posługiwania się tym operatorem na (prawie) wszystkie możliwe sposoby. Ograniczymy się przy tym do języka c++. Ma on także za zadanie rozbudzić waszą ciekawość do zbadania tego, co nieznane.

Tak ten operator wygląda w deklaracji visual studio (skopiowałam z msdn)

void* operator new(
   std::size_t _Count
) throw(bad_alloc);
void* operator new(
   std::size_t _Count,
   const std::nothrow_t&
) throw( );
void* operator new(
   std::size_t _Count,
   void* _Ptr
) throw( );

TIP: Aby korzystać z innej formy niż podstawowa (coś* wsk = new coś) potrzebujemy dodać do naszego pliku #include <new> .
Jak nietrudno zauważyć, funkcja zwraca wskaźnik na void. Nie nie nie, nie wskaźnik na nic, ale wskaźnik, jaki można przypisać do dowolnego typu.

Przyjmowane parametry:
_Count: ile bajtów ma zaalokować (pobierze go z typu prawego operandu. Jak dasz new int, weźmie sizeof(int). Przecież to zwraca size_t, nie?)
_Ptr: wskaźnik, jaki ma zwrócić (przy realokacji)
Może też nie przyjmować żadnych parametrów (w nawiasie) a wtedy domyślnie zaalokuje tyle bajtów, ile ma typ zmiennej, oraz umieści ją w wybranym przez siebie miejscu pamięci.

Przykład użycia tych form

1 forma:

JakasKlasa* wskObj = new Jakas Klasa;

Jest to forma, jaką znamy od dawna i używamy często. Do właśnie zadeklarowanego wskaźnika przypisany jest adres nowego, właśnie przypisanego obiektu. No ok, ale weźmy sobie taką sytuację. Umieszczamy tą instrukcję w jakiejś pętli nieskończonej (lub skończonej, ale obiekty klasy JakasKlasa są spore) i w którymś miejscu zabrakło pamięci. Oczywiście, do wskaźnika przypisuje się wartość null, ALE to nie wszystko co się stanie. Został również wyrzucony wyjątek bad_alloc, a co za tym idzie, aplikacja się wykrzaczyła, jak to zwykle bywa, gdy zostanie wyrzucony wyjątek, którego później nie obsłużyliśmy. Co zrobić, jeśli chcemy, by ten wyjątek nie został wyrzucony? Ano używamy drugiej formy operatora. W naszym przypadku będzie to

JakasKlasa* wskObj = new( nothrow ) Jakas Klasa;

(Jeżeli nie daliśmy

using namespace std;

to musimy napisać

std::nothrow

) I już żaden wyjątek nie zostanie wyrzucony. W przypadku błędu alokacji po prostu wskaźnik będzie wskazywał na nulla. I nic więcej się nie stanie. Należy jednak w kodzie dalej sprawdzić, czy wskaźnik wskazuje na null i odpowiednio zareagować na tą informację.
A gdzie się podział realloc?
Pewnego dnia mój znajomy (Tak Mariusz, Ty z półtora roku temu) powiedział, że trójca święta malloc, free, realloc znana z języka C jest lepsza, niż para new, delete. Uargumentował to:
a) brakiem możliwości alokacji pamięci o rozmiarze jakim on sobie wymyśli (można oszukać tworząc tablicę bajtów o liczbie elementów odpowiadającej ilości pamięci i zrzutować)
b) brakiem możliwości realokacji pamięci.
Co prawda nie ma trzeciej funkcji, jak realloc, ale nie jest ona potrzebna. Tutaj możemy również użyć operatora new. Przykład takiej realokacji zaczerpnięty z MSDNa:

char x[sizeof( MyClass )];
   MyClass* fPtr2 = new( &x[0] ) MyClass;

Czyli po skrócie wskaźnik = new(gdzie) typ;

Oczywiście gdzie to adres gdzie ma alokować;) Jako void*, bo takimi adresami operuje new. Jeśli używamy operatora pobrania adresu, zwróci nam jak trzeba. Jak widać realokowany zasób musi być wcześniej zaalokowany jakkolwiek. W przeciwnym wypadku program się wysypie z błędem segmentacji lub co gorsza, będzie pisał po innych danych programu. Także używać tego na prawdę z głową. Kiedy to jest przydatne? Gdy chcemy zaalokować raz kawał pamięci, a potem realokować jego fragment.

TIP: jeżeli nie używamy referencji do jakiejś zmiennej, a wskaźnik chcemy podać żywcem (czy np. stałą dosłowną adres podajemy) a program nie przyjmuje, zrzutujmy go na void* i przyjmie.

Nadawanie początkowych wartości tworzonym obiektom

Tak, jest to możliwe, ale wyłącznie w przypadku prostych obiektów (np. nie przejdzie w tablicach, bo to typ pochodny). Ale także dla np. naszych klas. Używamy tego tak:

int *wskliczba = new int(32);

Ktoś mógłby zadać pytanie, czy nie pomyli z argumentem realokacji. A niby czemu miałby tak zrobić? Przecież liczba w nawiasie jest przy int, a nie przy new. Czy to…. Tak, wywołanie yyy nazwijmy to sobie „konstruktora zmiennej typu wbudowanego”, jakkolwiek by to nie zabrzmiało.

Oczywiście działa bajer również dla pojedynczych, niestablicowanych obiektów naszych własnych klas. Weźmy sobie jakąś prostą klasę

class rekord{
	public:
		int lp;
		string imie;
		string nazwisko;
		rekord(int numer, string im, string naz){  //konstruktor
			lp = numer; imie = im; nazwisko = naz;
	}
};

Klasa ta jest prymitywna, (taaa, łamie wszystkie zasady enkapsulacji i w ogóle) ale wystarczy do pokazania o co mi chodzi. Żeby to zadziałało, musimy mieć ofc publiczny konstruktor.
Obiekt dynamiczny tworzymy sobie w taki oto sposób

rekord *wskrekord = new rekord(1,"Jan","Kowalski");

podając sobie pola po kolei, jak w konstruktorze zadeklarowaliśmy.
Co do ominięcia problemów z tablicami, możemy przecież po new cośtam dać ={konstruktor(), konstruktor(),…} i będzie ok, bo na liście inicjalizatorów będą już tworzone pojedyncze elementy tablicy.
New a tablice
W literaturze często znajduje się oznaczenie, że operatorem do tworzenia dynamicznych tablic jest

new[]

. Mimo że zapisuje się nadal jako new. Jednak w parze za nim idzie już jawnie zapisany

delete[] zmienna;

i pewnie by ułatwić ludziom pamiętanie o [] tak piszą. (jakbyście deletnęli bez nawiasu kwadratowego, usuniecie tylko 0 element tablicy i będziecie mieli leaka wielkości sizeof(typ) * ilosc_elementow-1).

Przykład stworzenia dynamicznej tablicy typu double

double *wsk = new double[1024];

co jest oczywiście równoznaczne parze instrukcji:

double *wsk;
wsk = new double[1024];

Rozmiaru nie musimy podawać w ten sposób. Nie musi to być literał. Może to być równie dobrze stała (const, czy nawet #define).  Jeżeli alokujemy wiele wymiarów, to pierwszy z prawej może być zmienną, lub może nie istnieć.

Wyłapanie wyjątku

Wróćmy, do sytuacji, gdzie tworzyliśmy sobie sobie coś sporego w bloku try

float* wsk;
int licznik;
try{
	//jakiś kod
	while(true){
		wsk = new float[10000000000];
		licznik++;
		//może robić jeszcze cośtam
	}
}

To nam wykrzaczy aplikację, prędzej czy później. Żeby jednak błąd alokacji nie przerwał nam działania programu i wyświetlenia okienka, (w przypadku windows), że „system windows napotkał błąd” a w szczegółach dał informację o nieobsłużonym wyjątku, musimy go złapać i obsłużyć. Na przykład pisząc gdzieś dalej w kodzie tak:

catch (std::bad_alloc){

 cout<<"\nStworzyłem ci już "<<licznik<<"tablic. Więcej nie upchnę. Dokup ramu.";
    }

No cóż, nic nie wykombinujemy więcej:)

Przeładowanie operatora new- czyli tworzymy obiekty po swojemu

Pierwsza rzecz: operator new musi być statycznym składnikiem klasy. Wiadomo, jest używany, gdy obiektu jeszcze nie ma:). Nie może się kumplować. Musi być składnikiem. Jeżeli przeładowujemy globalny operator new (można), musimy pamiętać, że nie odwołamy się do globalnego new, który był wcześniej. Nie ma tu jak operatora zakresu użyć. Strumieni standardowych też sobie nie poużywamy, bo by newa użyły (żegnajcie cin, cout, cerr, strumienie plikowe itd ;( ). Osobiście oprócz laborek z c++ i egzaminu nigdy nie musiałam ich używać (podobno używają ich przy optymalizacji alokowania), więc przepiszę kod z symfonii c++

#include <stdio.h>
void* operator new(size_t  rozmiar){
	void *wsk = malloc(rozmiar);
	printf("Globalnym kreuję pojedynczy obiekt %x\n", wsk);
	return wsk;
}

Pamiętać należy, że jak daliśmy new, musimy też stworzyć new[], delete oraz delete[].
Dla klasy jak chcemy operator przeładować, to tak samo jeśli jesteśmy jeszcze w klasie, a jeżeli już poza klasą, piszemy

void* klasa::operator new(size_t rozmiar){
	//jakiś kod, w tym użycie new dla pól lub malloca
	return wsk; //lub return (new obiekt_jakiśtam);
}

Czyli ogólnie rzecz biorąc, pamiętać należy, że new ZAWSZE musi zwracać void*, a pobierać zmienną typu size_t.

Podsumowanie

Wow, myślałam że będzie trochę krótszy:). Zebrałam do kupy wszystko, co wiem o tym operatorze i co dokumentacja o nim mówi w jednym miejscu, by nie trzeba było skakać. Mam nadzieję że udało mi się zaciekawić czytelników tym operatorem, oraz, że zachęciłam tych bardziej ciekawskich do poeksperymentowania z kodem.

Dla tych, którym już świeżbią ręce do kombinowania:

  • Sprawdźcie jak się zachowa, jak spróbujecie reallocować zerowy adres, adres poza osiągiem aplikacji, adres stały używany przez OS, nulla etc i podzielcie się spostrzeżeniami.
  • Pamiętając o tym, że zmienne, które tworzycie operatorem new nie czyszczą pamięci, zastanówcie się, czy można tym sposobem odczytywać fragmenty nieczyszczonej pamięci RAM (nie nadając początkowych wartości) i np. zrzucać taką zawartość do pliku, który potem podejrzycie hexdumpem :p <evil> TIP: zaalokuj bardzo dużą tablicę charów i zrzuć ją do pliku tekstowego, lub binarnego jak wolisz:))
  • Jak się zachowa kompilator, w momencie w którym spróbujecie stworzyć tablicę o indeksie ze zwykłej stałej (const), ale pomiędzy jej deklaracją (z przypisaniem wartości ofc) a utworzeniem tablicy zmieńcie wartość tego, co użyjecie w indeksie na większą o kilka/kilkanaście oczek(rzutując metodą const cast). Po czym spróbujcie odwołać się do indeksu poprzednia_wartość+2
    TIP: nie próbujcie robić tego w 1 (od lewej) wymiarze tablicy wielowymiarowej, tam może stać zarówno zmienna, jak i nawet może nic nie stać

 

Bibliografia:

  • ms-help://MS.MSDNQTR.v90.en/dv_vcstdlib/html/2476d0f9-59df-485c-981e-ba9f7ee83507.htm (wersja offline MSDN Library for Visual Studio 2008 SP1)
  • Symfonia c++ standard, Jerzy Grębosz, Kraków 2008
  • Własne eksperymenty:)

2 komentarze

  1. Niezły artykuł :). Wiedza o operatorze new jest niestety bardzo niska.

    To, co opisujesz jako odpowiednik dla realloca – placement new – wbrew pozorom się przydaje :). Udało mi się go użyć budując kiedyś memory managera na potrzeby gry, oraz chyba też jak własny allokator dla STLa (przydzielał pamięć wewnątrz preallokowanej statycznie tablicy; ot dla wydajności ;)).

  2. (ad. wiedzy – oczywiście mam na myśli wiedzę w społeczeństwie, nie Twoją; Twoja jest – jak widać po artykule – bardzo wysoka ;))

Comments are closed.