2016-09-07 17 views
10

Prawdopodobnie jest to proste pytanie, ale muszę to template class:niebezpieczne konstruktor szablon tablica

template<typename Type> 
class Array { 
    size_t n; 
    Type* buff; 
public: 
    Array(size_t n_): n(n_), buff(new Type[n]) {} 
}; 

Kod jest z pliku pdf kurs gdzie mówi buff(new Type[n]) jest niebezpieczne. Nie rozumiem, dlaczego jest to niebezpieczne, czy ogólnie size_t nie jest unsigned? Czy mogę podać przykład, w którym może wystąpić błąd kompilacji i/lub wykonywania?

+1

Na początek nie masz destruktora, a to może prowadzić do wycieku pamięci –

+0

@ArnavBorborah, ale czy to jedyna * niebezpieczna * rzecz? – Loay

Odpowiedz

12

Kod jest "niebezpieczny", ponieważ opiera się na n budowanym przed buff. Ta zależność dodaje kruchość do kodu.

Podczas konstruowania członków klasy są zbudowane w kolejności są zadeklarowane w klasie, a nie jak są one nazywane w liście inicjującej członkiem, więc jeśli kod został zmieniony na

template<typename Type> 
class Array { 
    Type* buff; 
    size_t n; 
public: 
    Array(size_t n_): n(n_), buff(new Type[n]) {} 
}; 

Następnie, gdy robisz buff(new Type[n]), n jest niezainicjowany i masz niezdefiniowane zachowanie.

+1

Należy pamiętać, że zarówno [gcc i clang] (http://coliru.stacked-crooked.com/a/08d42bdb5e31d8ed) ostrzeże Cię, jeśli umieścisz członków na liście inicjatora członków w innej kolejności, niż będą one tworzone, a ty "Gotowe" (włączone, a dokładniej "W porządku"). – jaggedSpire

+0

@jaggedSpire, i to jest ostrzeżenie, które najbardziej mnie denerwuje! – SergeyA

+0

@jaggedSpire To miło, nie wiedziałem, że to zrobił. Na złe zbyt wielu ludzi tego nie używa. – NathanOliver

-1

Nie jest bezpiecznie wywoływać nowego operatora na liście inicjalizacji. jeśli nowa się nie powiedzie, destruktor Array nie zostanie wywołany. Oto podobne pytanie. Are there any issues with allocating memory within constructor initialization lists?

+0

To nie ma tu zastosowania, choć byłoby tak, gdyby istniał jakiś kod "niezerowy", który byłby wykonywany po "nowym", a nawet innym "nowym". nowe zawiedzie, a następnie kończy się niepowodzeniem Konstrukcja nic więcej w klasie jest przydzielana dynamicznie, więc po sobie się sprząta – jaggedSpire

3

Przede wszystkim porządku, inicjujące konstruktora są wykonywane nie zależy od kolejności, w jakiej są zapisane, ale w kolejności te inicjowane pola występują w kodzie:

class Array { 
    size_t n; 
    Type* buff; 
public: 
    Array(size_t n_): n(n_), buff(new Type[n]) {} 
}; 

Tutaj pierwsze n zostanie zainicjowane, a następnie buff.

class Array { 
    Type* buff; 
    size_t n; 
public: 
    Array(size_t n_): n(n_), buff(new Type[n]) {} 
}; 

Teraz pierwszy buff zostanie zainicjowany, a następnie n, więc n nie ma zdefiniowanej wartości w tym przypadku.

Użycie list inicjalizacyjnych dla konstruktorów jest dobrą praktyką, ale należy uważać, aby nie tworzyć żadnych założeń dotyczących zamówienia.

Generalnie dobrze jest powstrzymać się od posiadania surowych wskazówek. Jeśli zamiast tego używasz inteligentnych wskaźników, nie możesz zapomnieć o zwolnieniu danych.

W konkretnym przypadku możesz również chcieć użyć std :: vector zamiast tablicy typu C. Zajmuje się dla ciebie całą alokacją, realokacją, wydaniami itp. W sposób bezpieczny dla wątków. Wygląda na to, że próbujesz napisać coś w rodzaju własnego std :: vector. Zrób to tylko w celach edukacyjnych. Zawsze preferuj standardową implementację w kodzie produkcyjnym. Prawdopodobnie nie osiągniesz tego lepiej przez dłuższy czas. Jeśli tak, zadaj różne pytania tutaj ;-)

+0

ten przykład kodu służy do celów edukacyjnych – Loay

3

Po pierwsze, masz wyciek pamięci. Ale prawdopodobnie nie o to chodzi. Załóżmy więc, że masz destruktor, który deallocuje tablicę.

template<typename Type> 
class Array { 
    size_t n; 
    Type* buff; 
public: 
    Array(size_t n_): n(n_), buff(new Type[n]) {} 
    ~Array() { delete[] buff; } 
}; 

Teraz to szczególności kod jest całkowicie bezpieczne. Nie można zgłaszać wyjątków podczas przypisywania wartości n_, kolejność inicjowania jest poprawna, a buff jest jedynym nieprzetworzonym wskaźnikiem w klasie. Jednak wraz z rozpoczęciem rozszerzania klasy i pisania kolejnych zajęć wzrasta ryzyko wycieku pamięci.

Wyobraźmy sobie, że trzeba dodać jeden więcej członków do class Array:

template<typename Type> 
class Array { 
    size_t n; 
    Type* buff; 
    SomethingElse xyz; 
public: 
    Array(size_t n_): n(n_), buff(new Type[n_]), xyz(n_) {} 
    ~Array() { delete[] buff; } 
}; 

Jeżeli konstruktor SomethingElse rzuca, pamięć przydzielona dla buff będzie przeciekać, ponieważ destruktor ~Array() nigdy nie zostanie wywołana.

Nowoczesne C++ nazywa wskaźniki takie jak Type* buffsurowych wskaźników ponieważ jesteś odpowiedzialny za dealokując przechowywanie siebie (biorąc pod uwagę wyjątki), a także wprowadza narzędzia, takie jak std::unique_ptr i std::shared_ptr że może zająć dealokacji pamięci automatycznie.

We współczesnym C++ można napisać swoją klasę tak:

template<typename Type> 
class Array { 
    size_t n; 
    std::unique_ptr<Type[]> buff; 
public: 
    Array(size_t n_): n(n_), buff(new Type[n_]) {} 
}; 

Zawiadomienie braku destructor. unique_ptr zajmie się dla ciebie dzwonieniem pod numer delete.

Zauważ też nie zależność od członków klasy wewnątrz listy inicjatora (po prostu pisząc new Type[n_] zamiast new Type[n] czyni kod bardziej wydajny)

0

C++ 98 Standard 12.6.2.5.4 (nie spodziewam się nowe wersje aby to rozluźnić).

- Następnie nonstatic członkowie danych zostanie zainicjowany w kolejności ich zostały zadeklarowane w definicji klasy (znowu niezależnie od kolejności z MEM-inicjalizatorów).

Tak więc kolejność inicjalizacji jest zdefiniowana zgodnie z tym.

Jeśli chcesz podać przykład awarii, po prostu zmień całkowitą pamięć w swoim systemie na sizeof(Type)*n.

Powiązane problemy