Joel Spolsky scharakteryzował C ++ jako „wystarczającą ilość liny do powieszenia się” . W rzeczywistości podsumował „Effective C ++” Scotta Meyersa:
Jest to książka, która w zasadzie mówi, że C ++ jest wystarczającą liną do zawieszenia się, a następnie kilka dodatkowych mil liny, a następnie kilka tabletek samobójczych, które są przebrane za M & Ms ...
Nie mam kopii książki, ale istnieją oznaki, że znaczna część książki dotyczy pułapek zarządzania pamięcią, które wydają się być wykonane w C #, ponieważ środowisko wykonawcze zarządza tymi problemami.
Oto moje pytania:
- Czy C # pozwala uniknąć pułapek, których można uniknąć w C ++ tylko poprzez staranne programowanie? Jeśli tak, to w jakim stopniu i jak można ich uniknąć?
- Czy są nowe, różne pułapki w C #, o których powinien wiedzieć nowy programista C #? Jeśli tak, dlaczego nie można ich uniknąć dzięki projektowi C #?
c#
programming-languages
c++
alx9r
źródło
źródło
Your questions should be reasonably scoped. If you can imagine an entire book that answers your question, you’re asking too much.
. Wierzę, że to kwalifikuje się jako takie pytanie ...Odpowiedzi:
Podstawowa różnica między C ++ i C # wynika z nieokreślonego zachowania .
Nie ma to nic wspólnego z ręcznym zarządzaniem pamięcią. W obu przypadkach jest to rozwiązany problem.
C / C ++:
W C ++, gdy popełnisz błąd, wynik jest niezdefiniowany.
Lub, jeśli spróbujesz poczynić pewne założenia dotyczące systemu (np. Przepełnienie liczby całkowitej ze znakiem), istnieje prawdopodobieństwo, że Twój program nie zostanie zdefiniowany.
Może przeczytaj tę 3-częściową serię o nieokreślonym zachowaniu.
To sprawia, że C ++ jest tak szybki - kompilator nie musi się martwić o to, co się stanie, gdy coś pójdzie nie tak, więc może uniknąć sprawdzania poprawności.
C #, Java itp.
W języku C # masz gwarancję, że wiele błędów pojawi się na twojej twarzy jako wyjątki, i masz gwarancję znacznie więcej na temat podstawowego systemu.
Jest to podstawowa bariera w tworzeniu C # tak szybko, jak C ++, ale jest to także podstawowa bariera w zapewnianiu bezpieczeństwa C ++, a także ułatwia pracę z C # i debugowanie.
Cała reszta to po prostu sos.
źródło
Większość to robi, a niektóre nie. I oczywiście tworzy kilka nowych.
Niezdefiniowane zachowanie - Największą pułapką w C ++ jest to, że jest wiele niezdefiniowanych języków. Kompilator może dosłownie wysadzić wszechświat, kiedy robisz te rzeczy, i będzie dobrze. Naturalnie, jest to niczym niezwykłym, ale to jest dość powszechne dla programu działać na jednej maszynie i naprawdę nie ma dobrego powodu nie działać na innym. Lub gorzej, subtelnie zachowuj się inaczej. C # ma kilka przypadków nieokreślonego zachowania w specyfikacji, ale są one rzadkie i w obszarach języka, które są rzadko podróżowane. C ++ ma możliwość napotkania niezdefiniowanego zachowania za każdym razem, gdy wypowiadasz instrukcję.
Wycieki pamięci - jest to mniejszy problem dla współczesnego C ++, ale dla początkujących i przez około połowę swojego życia, C ++ sprawiło, że bardzo łatwo wyciek pamięci. Skuteczne C ++ pojawiło się tuż przy ewolucji praktyk w celu wyeliminowania tego problemu. To powiedziawszy, C # może nadal wyciekać pamięć. Najczęstszym przypadkiem, na który wpadają ludzie, jest przechwytywanie zdarzeń. Jeśli masz obiekt i umieściłeś jedną z jego metod jako procedurę obsługi zdarzenia, właściciel tego zdarzenia musi zostać GC, aby obiekt umarł. Większość początkujących nie zdaje sobie sprawy, że moduł obsługi zdarzeń liczy się jako odniesienie. Występują również problemy z brakiem rozporządzalnych zasobów, które mogą przeciekać pamięć, ale nie są one tak powszechne, jak wskaźniki we wstępnym C ++.
Kompilacja - C ++ ma opóźniony model kompilacji. Prowadzi to do wielu sztuczek, z którymi można się dobrze bawić i skrócić czas kompilacji.
Ciągi - Modern C ++ sprawia, że jest to trochę lepsze, ale
char*
odpowiada za ~ 95% wszystkich naruszeń bezpieczeństwa przed rokiem 2000. Dla doświadczonych programistów skupią sięstd::string
, ale wciąż jest coś, czego należy unikać i problem w starszych / gorszych bibliotekach . I to modli się, abyś nie potrzebował obsługi Unicode.I tak naprawdę to wierzchołek góry lodowej. Głównym problemem jest to, że C ++ jest bardzo słabym językiem dla początkujących. Jest to dość niespójne, a wiele starych, naprawdę złych pułapek rozwiązano poprzez zmianę idiomów. Problem polega na tym, że początkujący muszą nauczyć się idiomów z czegoś takiego jak Effective C ++. C # całkowicie eliminuje wiele z tych problemów i sprawia, że reszta jest mniej ważna, dopóki nie przejdziesz dalej ścieżką uczenia się.
Wspomniałem o problemie z „wyciekiem pamięci”. To nie jest problem językowy tak bardzo, jak programista oczekujący czegoś, czego język nie może zrobić.
Innym jest to, że finalizator dla obiektu C # nie jest technicznie gwarantowany do uruchomienia przez środowisko wykonawcze. To zwykle nie ma znaczenia, ale powoduje, że niektóre rzeczy są projektowane inaczej, niż można się spodziewać.
Innym półpułapkiem, na który wpadli programiści, jest semantyka przechwytywania anonimowych funkcji. Kiedy przechwytujesz zmienną, przechwytujesz zmienną . Przykład:
Nie robi tego, co naiwnie się myśli. Drukuje się
10
10 razy.Jestem pewien, że zapominam o wielu innych, ale głównym problemem jest to, że są mniej rozpowszechnione.
źródło
char*
. Nie wspominając o tym, że nadal możesz przeciekać pamięć w C # w porządku.enable_if
Moim zdaniem niebezpieczeństwa związane z C ++ są nieco przesadzone.
Istotne niebezpieczeństwo jest następujące: podczas gdy C # pozwala wykonywać „niebezpieczne” operacje na wskaźnikach przy użyciu
unsafe
słowa kluczowego, C ++ (będący przeważnie nadzbiorem C) pozwala używać wskaźników, kiedy tylko masz na to ochotę. Oprócz zwykłych zagrożeń związanych ze stosowaniem wskaźników (które są takie same w przypadku C), takich jak wycieki pamięci, przepełnienie bufora, zwisające wskaźniki itp., C ++ wprowadza nowe sposoby poważnego zepsucia.Ta „dodatkowa lina”, że tak powiem, o której mówił Joel Spolsky , w zasadzie sprowadza się do jednej rzeczy: pisania zajęć, które wewnętrznie zarządzają własną pamięcią, znaną również jako „ Reguła 3 ” (którą można teraz nazwać Regułą 4 lub Reguła 5 w C ++ 11). Oznacza to, że jeśli kiedykolwiek chcesz napisać klasę, która wewnętrznie zarządza przydziałami pamięci, musisz wiedzieć, co robisz, w przeciwnym razie program prawdopodobnie się zawiesi. Musisz ostrożnie utworzyć konstruktor, skopiować konstruktor, destruktor i operator przypisania, co jest zaskakująco łatwe do popełnienia błędu, często powodując dziwne awarie w czasie wykonywania.
JEDNAK , w codziennym programowaniu w C ++, bardzo rzadko jest pisanie klasy, która zarządza własną pamięcią, więc mylące jest twierdzenie, że programiści C ++ zawsze muszą być „ostrożni”, aby uniknąć tych pułapek. Zwykle będziesz robić coś więcej:
Ta klasa wygląda dość blisko tego, co robisz w Javie lub C # - nie wymaga jawnego zarządzania pamięcią (ponieważ klasa biblioteki
std::string
zajmuje się tym wszystkim automatycznie) i nie jest wymagana żadna funkcja „Reguły 3”, ponieważ domyślna Konstruktor kopiowania i operator przypisania są w porządku.Tylko wtedy, gdy próbujesz zrobić coś takiego:
W takim przypadku nowicjuszom może być trudne uzyskanie poprawnego przypisania, destruktora i konstruktora kopiowania. Ale w większości przypadków nie ma powodu, aby to robić. C ++ sprawia, że bardzo łatwo jest uniknąć ręcznego zarządzania pamięcią przez 99% czasu, używając klas bibliotek takich jak
std::string
istd::vector
.Innym powiązanym problemem jest ręczne zarządzanie pamięcią w sposób, który nie bierze pod uwagę możliwości zgłoszenia wyjątku. Lubić:
Jeśli
some_function_which_may_throw()
faktycznie nie wyjątek, jesteś w lewo z wyciekiem pamięci, ponieważ pamięć przeznaczona nas
nigdy nie zostaną odzyskane. Ale znowu, w praktyce nie jest to już problemem z tego samego powodu, dla którego „Reguła 3” nie jest już tak naprawdę dużym problemem. Bardzo rzadko (i zwykle nie jest to konieczne) zarządzanie własną pamięcią za pomocą surowych wskaźników. Aby uniknąć powyższego problemu, wystarczy użyćstd::string
lubstd::vector
, a destruktor zostanie automatycznie wywołany podczas rozwijania stosu po zgłoszeniu wyjątku.Tak więc ogólnym tematem jest to, że wiele funkcji C ++, które nie zostały odziedziczone po C, takich jak automatyczna inicjalizacja / niszczenie, konstruktory kopiowania i wyjątki, zmusza programistę do zachowania szczególnej ostrożności podczas ręcznego zarządzania pamięcią w C ++. Ale znowu jest to problem tylko wtedy, gdy zamierzasz ręcznie zarządzać pamięcią, co nie jest już prawie konieczne, gdy masz standardowe pojemniki i inteligentne wskaźniki.
Tak więc, moim zdaniem, podczas gdy C ++ daje ci dużo dodatkowej liny, prawie nigdy nie trzeba jej używać do powieszenia się, a pułapek, o których mówił Joel, są banalnie łatwe do uniknięcia we współczesnym C ++.
źródło
Does C# avoid pitfalls that are avoided in C++ only by careful programming?
. Odpowiedź brzmi: „nie bardzo, bo banalnie łatwo jest uniknąć pułapek, o których mówił Joel we współczesnym C ++”Naprawdę nie zgodziłbym się. Może mniej pułapek niż C ++, jakie istniały w 1985 roku.
Nie całkiem. Reguły takie jak Reguła Trzech straciły ogromne znaczenie w C ++ 11 dzięki
unique_ptr
ishared_ptr
są znormalizowane. Używanie klas standardowych w dość rozsądny sposób nie jest „ostrożnym kodowaniem”, lecz „podstawowym kodowaniem”. Ponadto odsetek populacji C ++, którzy są nadal wystarczająco głupi, niedoinformowani lub oboje, aby wykonywać takie czynności, jak ręczne zarządzanie pamięcią, jest znacznie niższy niż wcześniej. Rzeczywistość jest taka, że wykładowcy, którzy chcą wykazać takie zasady, muszą spędzić tygodnie, próbując znaleźć przykłady, w których nadal mają zastosowanie, ponieważ zajęcia standardowe obejmują praktycznie każdy możliwy do wyobrażenia przypadek użycia. Wiele skutecznych technik C ++ poszło tą samą drogą - drogą dodo. Wiele innych nie jest tak specyficznych dla C ++. Daj mi zobaczyć. Pomijając pierwszy element, kolejne dziesięć to:final
ioverride
pomogliśmy zmienić tę konkretną grę na lepsze. Zrób swój destruktor,override
a zagwarantujesz niezły błąd kompilatora, jeśli odziedziczysz po kimś, kto nie zrobił jego destruktoravirtual
. Uczyń swoją klasę, abyfinal
żaden słaby peeling nie mógł przyjść i odziedziczyć po nim przypadkowo bez wirtualnego destruktora.Oczywiście nie będę przechodził przez każdy element Effective C ++, ale większość z nich po prostu stosuje podstawowe pojęcia do C ++. Znajdziesz tę samą radę w dowolnym zorientowanym obiektowo języku operatora przeciążalnego operatora. Wirtualne niszczyciele są prawie jedyną pułapką w C ++ i nadal są ważne - chociaż, prawdopodobnie, z
final
klasą C ++ 11, nie jest tak ważne, jak było. Pamiętaj, że Effective C ++ został napisany, kiedy pomysł zastosowania OOP i specyficzne cechy C ++ były wciąż bardzo nowe. Te elementy nie dotyczą pułapek w C ++, a więcej o tym, jak radzić sobie ze zmianą z C i jak poprawnie używać OOP.Edycja: Pułapki w C ++ nie obejmują takich pułapek
malloc
. Po pierwsze, każdą pułapkę, którą można znaleźć w kodzie C, można również znaleźć w niebezpiecznym kodzie C #, więc nie jest to szczególnie istotne, a po drugie, tylko dlatego, że Standard definiuje go dla współdziałania, nie oznacza, że użycie go jest uważane za C ++ kod. Norma również określagoto
, ale jeśli miałbyś napisać z niego gigantyczną kupkę spaghetti, uznałbym, że to twój problem, a nie język. Istnieje duża różnica między „ostrożnym kodowaniem” a „przestrzeganiem podstawowych idiomów języka”.using
do bani. To naprawdę działa. I nie mam pojęcia, dlaczego nie zrobiono czegoś lepszego. Ponadto,Base[] = Derived[]
i prawie każde użycie Object, które istnieje, ponieważ oryginalni projektanci nie zauważyli ogromnego sukcesu, jakim były szablony w C ++, i zdecydowali, że „Po prostu odziedziczmy wszystko od wszystkiego i stracimy całe nasze bezpieczeństwo typu” był mądrzejszym wyborem . Wierzę również, że można znaleźć paskudne niespodzianki w takich warunkach jak wyścig z delegatami i inne tego rodzaju zabawy. Są też inne ogólne rzeczy, takie jak przerażające ssanie leków generycznych w porównaniu do szablonów, naprawdę bardzo niepotrzebne wymuszone umieszczanie wszystkiego wclass
itd. I takie rzeczy.źródło
malloc
nie oznacza, że powinieneś to zrobić, podobnie jak fakt, że kurwagoto
jak suka oznacza, że jest to lina, z którą możesz się powiesić.unsafe
w języku C #, co jest równie złe. Mógłbym wymienić każdą pułapkę kodowania C # jak również C, jeśli chcesz.C # ma zalety:
char
,string
itp jest realizacja zdefiniowane. Rozłam między podejściem Windows do Unicode (wchar_t
dla UTF-16,char
dla przestarzałych „stron kodowych”) i podejściem * nix (UTF-8) powoduje duże trudności w kodzie międzyplatformowym. C #, OTOH, gwarantuje, że astring
to UTF-16.Tak:
IDisposable
Istnieje książka o nazwie Effective C #, która ma strukturę podobną do Effective C ++ .
źródło
Nie, C # (i Java) są mniej bezpieczne niż C ++
C ++ jest weryfikowalny lokalnie . Mogę sprawdzić pojedynczą klasę w C ++ i stwierdzić, że klasa nie przecieka pamięci lub innych zasobów, zakładając, że wszystkie klasy, do których istnieją odniesienia, są poprawne. W Javie lub C # konieczne jest sprawdzenie każdej klasy, do której istnieje odniesienie, aby ustalić, czy wymaga ona pewnego rodzaju finalizacji.
C ++:
DO#:
C ++:
DO#:
źródło
auto_ptr
(lub kilku jego krewnych). To jest przysłowiowa lina.auto_ptr
jest tak prosta, jak znajomość obsługiIEnumerable
lub znajomość obsługi interfejsów lub nie używaj liczb zmiennoprzecinkowych dla waluty lub tym podobnych. Jest to podstawowa aplikacja DRY. Nikt, kto zna podstawy programowania, nie popełniłby tego błędu. W przeciwieństwieusing
. Problemusing
polega na tym, że musisz wiedzieć dla każdej klasy, czy jest ona jednorazowa (i mam nadzieję, że nigdy się nie zmieni), a jeśli nie jest jednorazowa, automatycznie banujesz wszystkie pochodne klasy, które mogą być jednorazowe.Dispose
metodą, musisz je zaimplementowaćIDisposable
(„właściwy” sposób). Jeśli twoja klasa to robi (co jest równoważne z implementacją RAII dla twojej klasy w C ++) i używaszusing
(co jest jak inteligentne wskaźniki w C ++), wszystko działa idealnie. Finalizator ma przede wszystkim zapobiegać wypadkom -Dispose
jest odpowiedzialny za poprawność, a jeśli go nie używasz, cóż, to twoja wina, a nie C #.Tak 100% tak, ponieważ uważam, że nie można zwolnić pamięci i użyć jej w C # (zakładając, że jest zarządzana i nie przejdziesz w niebezpieczny tryb).
Ale jeśli wiesz, jak programować w C ++, czego niewiarygodna liczba ludzi nie. Nic ci nie jest. Podobnie jak lekcje Charlesa Salvii tak naprawdę nie zarządzają swoimi wspomnieniami, ponieważ wszystko odbywa się w istniejących klasach STL. Rzadko używam wskaźników. W rzeczywistości realizowałem projekty bez użycia jednego wskaźnika. (C ++ 11 ułatwia to).
Jeśli chodzi o pisanie liter, głupie pomyłki itp. (
if (i=0)
Np. Klucz bc utknął po bardzo szybkim naciśnięciu ==) kompilator narzeka, co jest dobre, ponieważ poprawia jakość kodu. Innym przykładem jest zapominaniebreak
w instrukcjach switch i niedopuszczanie do deklarowania zmiennych statycznych w funkcji (co czasem mi się nie podoba, ale jest dobrym pomysłem imo).źródło
=
/,==
wykorzystując==
do równości referencyjnej i wprowadzając.equals
do równości wartości. Słaby programista musi teraz śledzić, czy zmienna jest „podwójna” czy „podwójna” i koniecznie wybrać odpowiedni wariant.struct
możesz zrobić,==
co działa niewiarygodnie dobrze, ponieważ przez większość czasu można było mieć tylko łańcuchy, liczby całkowite i zmiennoprzecinkowe (tj. tylko elementy struktury). W moim własnym kodzie nigdy nie mam tego problemu, chyba że chcę porównać tablice. Nie sądzę, żebym kiedykolwiek porównywał listy lub typy niestrukturalne (string, int, float, DateTime, KeyValuePair i wiele innych)==
równości wartości iis
równości odniesienia.