• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Miejscowa wersja new - nadpisywanie pamięci

Object Storage Arubacloud
+2 głosów
601 wizyt
pytanie zadane 3 kwietnia 2017 w C i C++ przez Evelek Nałogowiec (28,960 p.)
edycja 3 kwietnia 2017 przez Evelek

Moje pytanie odnosi się do pojęcia "miejscowej wersji new" (placement new).

Przedstawię to na rysunku (adresy pamięci oczywiście ustawione specjalnie w tej kolejności): http://i.imgur.com/AgPJgBU.png

Na początku alokuje dynamicznie pamięć dla pierwszego wskaźnika: int *wsk = new int[10];

Następnie alokuje dynamicznie pamięć dla drugiego wskaźnika: int *wsk_2 = new int[10];

"Szczęście" tak chciało, że obszary pamięci ustawiły się jeden za drugim.

Następnie korzystam z miejscowej wersji new dla trzeciego wskaźnika, wskazując obszar pamięci wskaźnika wsk: int *new_wsk = new (wsk) int[11];

Okazuje się, że 11 element wskaźnika new_wsk[10], czyli ten ostatni, wskazuje na obszar pamięci wskaźnika wsk_2. W moim rozumieniu nadpisuje go.

Pytanie więc: Czy tak właśnie poprawnie działa miejscowa wersja new i to do programisty należy obowiązek dopilnowania, aby obszary pamięci się nie nadpisały? Czy może jest sposób, aby gdy dochodzi do sytuacji jak w wyżej opisanej, aby wskaźnik new_wsk zaalokował pamięć w nowym miejscu za pomocą wbudowanej funkcji w standard języka C++ zamiast nadpisywać inny obszar pamięci? Moim pomysłem byłoby porównywanie adresów pamięci i gdyby okazało się, że są identyczne to użyłbym wtedy zwykłego new zamiast miejscowego. Jeszcze inny pomysł to może inteligentne wskaźniki (smart pointers) są lepiej przystosowane do takich zachowań?

Taki krótki kod w C++, w którym jeden wskaźnik nadpisuje drugi:

#include <iostream>
#include <new>
#include <fstream>
using namespace std;

int main()
{
	ofstream plik_wypis_adresow;
	plik_wypis_adresow.open("adresy.txt");

	int *wsk = new int[10];
	int *wsk_2 = new int[10];

	plik_wypis_adresow << "Pamiec wsk: " << '\n';
	for (int i = 0; i < 10; ++i)
		plik_wypis_adresow << &wsk[i] << endl;

	plik_wypis_adresow << "\nPamiec wsk_2: " << '\n';
	for (int i = 0; i < 10; ++i)
		plik_wypis_adresow << &wsk_2[i] << endl;

	int *new_wsk = new (wsk) int[11750]; //miejscowa wersja new i nadpisanie pamieci
	plik_wypis_adresow << "\nPamiec new_wsk: " << '\n';
	for (int i = 0; i < 11750; ++i)
		plik_wypis_adresow << &new_wsk[i] << endl;

	plik_wypis_adresow.close();

	delete[] wsk_2;
	delete[] new_wsk; //nie musimy czyscic pamieci wskaznika wsk, robi to new_wsk
}

W moim przypadku nadpisywanie pamięci zachodzi w tym przypadku po zaalokowaniu pamięci dla 11750 zmiennych typu int.

2 odpowiedzi

+1 głos
odpowiedź 3 kwietnia 2017 przez mokrowski Mędrzec (155,460 p.)
wybrane 3 kwietnia 2017 przez Evelek
 
Najlepsza

Czy tak właśnie poprawnie działa miejscowa wersja new i to do programisty należy obowiązek dopilnowania, aby obszary pamięci się nie nadpisały?

Tak. Tak właśnie działa. Dobrze także dedukujesz że miałeś "szczęście" że wskaźniki ułożyły się jeden za drugim bo mogły ułożyć się jakkolwiek.

Moim pomysłem byłoby porównywanie adresów pamięci i gdyby okazało się, że są identyczne to użyłbym wtedy zwykłego new zamiast miejscowego.

Oczywiście jeśli bardzo chcesz, możesz tak robić. W praktyce jednak takie problemy rozwiązuje się operatorem new implementowanym w ramach klasy. Ten operator wie najlepiej jak wykonywać alokację i dealokację danych. Jest nawet biblioteka która to wspiera http://www.boost.org/doc/libs/1_63_0/libs/pool/doc/html/index.html tu masz dokładny opis jednego z możliwych algorytmów (w tym przypadku stosowanego przez bibliotekę): http://www.boost.org/doc/libs/1_63_0/libs/pool/doc/html/boost_pool/pool/pooling.html#boost_pool.pool.pooling.simple

Jeszcze inny pomysł to może inteligentne wskaźniki (smart pointers) są lepiej przystosowane do takich zachowań?

Nie. One kompletnie nie od tego są. One zarządzają cyklem życia, zliczają referencje, chronią przed dostępem do zerwanej referencji. Żaden z nich nie zajmuje się bezpośrednio opieką nad pamięcią. Pamięć się im przekazuje w formie wskaźnika na obszar. Mają możliwość podania własnego "deletera" jeśli pamięć była przydzielana nietypowo (np. przez new miejscowe).

A co do wskaźników. C++ gwarantuje przy arytmetyce wskaźników że poprawne wyniki będą dla zakresu operacji od 1 wskaźnika na tablicę do wskaźnika 1 element za tablicą. Standard nie gwarantuje układania danych dla różnych (nic nie wiedzących o sobie) typów. Nie wiadomo czy następny trafi w niższy adres czy wyższy. Nie gwarantuje nawet kolejności adresów przy danych na stosie!

komentarz 3 kwietnia 2017 przez Evelek Nałogowiec (28,960 p.)

Dzięki mokrowski, rozwiałeś moje wątpliwości. wink

Co do tej arytmetyki wskaźników to czytałem o tym i w praktyce używałem niejednokrotnie. Z angielskiego jest to "one past the end". Trochę spędziłem jakiś czas temu na SO czytając o tym, np. http://stackoverflow.com/questions/988158/take-the-address-of-a-one-past-the-end-array-element-via-subscript-legal-by-the .

W takim razie jeszcze jedno pytanie co do miejscowej wersji new. Odnosić się ono będzie do metod z mojego szablonu Vector w C++. Intuicja mi podpowiada, aby korzystać z miejscowej wersji new tylko i wyłącznie w przypadku usuwania elementu z Vectora. Wtedy mamy pewność, że żaden obszar pamięci nie zostanie nadpisany. Zaś w przypadku dodawania nowego elementu bezpieczniej jest na nowo zaalokować pamięć w nowym miejscu. Czy jest to poprawne? (Oczywiście wykluczam jeszcze na chwilę obecną używanie smart pointerów, skupiam się na new/delete).

1
komentarz 3 kwietnia 2017 przez mokrowski Mędrzec (155,460 p.)

Jak zakładam, chcesz ten swój Vector upodobnić do std::vector? Jeśli tak, to działa to inaczej. std::vector alokuje automatycznie bufor o wielkości większej niż wymaganie dane. W momencie gdy dochodzisz do granicy tego bufora, następuje próba alokowania nowego obszaru o jeszcze większego. O ile? To standard zostawia producentom kompilatorów. Stawia tylko wymagania np. by std::vector był w ciągłej przestrzeni pamięci. Jest jeszcze szereg innych wymagań ale nie będę pisał tu elaboratu. Ogólnie wywołanie .capacity() na vector zwraca Ci informacje ile std::vector ma alokowanej pamięci a .size() ile "na teraz" jest w niej elementów. 

Zakładając że te nowe alokowanie się udało, następuje przepisanie elementów ze starej przestrzeni pamięci do nowej oraz zniszczenie starych obiektów. Jeśli obiekty posiadają konstruktory kopiujące, zostaną one użyte (mogą to być konstruktory domyślne). I tak było do C++11 :-) 

Po C++11, vector szuka w obiekcie konstruktora przenoszącego/operatora przypisania przenoszącego i te preferuje. Stąd koszt ewentualnej realokacji istotnie się zmniejsza.

Z faktu realokacji danych w std::vector, płynie jeszcze jeden wniosek. Jeśli używałeś iteratorów i/lub wskaźników na elementy w std::vector, po realokacji są one nieważne i nie powinieneś przez nie sięgać do danych. Pokazują w stare miejsca. Standard określa że to jest UB ale.... większość implementacji pamięci nie czyści i jeśli wywołałeś stary wskaźnik, to ma to "nieszczęśliwą tendencję że zadziała". A najlepiej by było by nie działało :-/

Dobra wystarczy.. długie wyszło :-)

komentarz 3 kwietnia 2017 przez Evelek Nałogowiec (28,960 p.)

Ślicznie dziękuję za wyjaśnienie. Różnicę między size() a capacity() znam, kiedyś używałem w obiektach string, jak sprawdziłem teraz to w moim przypadku pojemność jest wielokrotnością liczby 16. Jeszcze o konstruktorach przenoszących muszę sobie powtórzyć. Idę w dobrą stronę tylko jeszcze daleko do ideału. wink

+1 głos
odpowiedź 3 kwietnia 2017 przez j23 Mędrzec (194,920 p.)

Trochę nie rozumiem Twojej rozkminki, bo tak naprawdę placement new służy jedynie do wywoływania konstruktorów obiektów w uprzednio zaalokowanej pamięci.  Więc siła rzeczy musisz zadbać o to, żeby nie tworzyć obiektu poza przydzielonym regionem pamięci.

Podobne pytania

0 głosów
1 odpowiedź 472 wizyt
pytanie zadane 28 marca 2018 w C i C++ przez Jakub 0 Pasjonat (23,120 p.)
0 głosów
2 odpowiedzi 250 wizyt
pytanie zadane 9 lutego 2019 w C i C++ przez jankustosz1 Nałogowiec (35,880 p.)
0 głosów
1 odpowiedź 632 wizyt

92,568 zapytań

141,420 odpowiedzi

319,622 komentarzy

61,954 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Kolejna edycja największej imprezy hakerskiej w Polsce, czyli Mega Sekurak Hacking Party odbędzie się już 20 maja 2024r. Z tej okazji mamy dla Was kod: pasjamshp - jeżeli wpiszecie go w koszyku, to wówczas otrzymacie 40% zniżki na bilet w wersji standard!

Więcej informacji na temat imprezy znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...