Джоэл Спольски охарактеризовал C ++ как «достаточно веревки, чтобы повеситься» . На самом деле он резюмировал «Эффективный C ++» Скотта Мейерса:
Это книга, которая в основном гласит: C ++ - это достаточно веревки, чтобы повеситься, а затем пара лишних миль веревки, а затем пара таблеток-самоубийц, которые замаскированы под M & Ms ...
У меня нет экземпляра книги, но есть признаки того, что большая часть книги связана с ловушками управления памятью, которые, как кажется, были бы спорными в C #, потому что среда выполнения решает эти проблемы за вас.
Вот мои вопросы:
- C # избегает ловушек, которых избегают в C ++ только при тщательном программировании? Если да, то в какой степени и как их избежать?
- Есть ли в C # новые подводные камни, о которых должен знать новый программист C #? Если так, то почему их нельзя было избежать с помощью дизайна C #?
c#
programming-languages
c++
alx9r
источник
источник
Your questions should be reasonably scoped. If you can imagine an entire book that answers your question, you’re asking too much.
. Я считаю, что это квалифицируется как такой вопрос ...Ответы:
Принципиальное отличие C ++ от C # заключается в неопределенном поведении .
Это не имеет ничего общего с ручным управлением памятью. В обоих случаях это решенная проблема.
C / C ++:
В C ++, когда вы делаете ошибку, результат не определен.
Или, если вы попытаетесь сделать определенные предположения о системе (например, целочисленное переполнение со знаком), есть вероятность, что ваша программа будет неопределенной.
Возможно, прочитайте эту серию из трех частей о неопределенном поведении.
Это то, что делает C ++ таким быстрым - компилятору не нужно беспокоиться о том, что происходит, когда что-то идет не так, поэтому он может избежать проверки на правильность.
C #, Java и т. Д.
В C # вам гарантировано, что многие ошибки будут взорваны вашим лицом как исключения, и вам гарантировано гораздо больше о базовой системе.
Это фундаментальный барьер для того, чтобы сделать C # таким же быстрым, как C ++, но это также фундаментальный барьер для того, чтобы сделать C ++ безопасным, и это облегчает работу и отладку C #.
Все остальное просто соус.
источник
Большинство это делает, некоторые нет. И, конечно, это делает некоторые новые.
Неопределенное поведение. Самая большая ошибка в C ++ состоит в том, что существует множество неопределенных языков. Компилятор может буквально взорвать вселенную, когда вы делаете эти вещи, и все будет в порядке. Естественно, это редко, но является довольно распространенным для вашей программы работы штрафа на одной машине и на самом деле нет уважительной причины не работать на другом. Или, что еще хуже, тонко действуют иначе. C # имеет несколько случаев неопределенного поведения в своей спецификации, но они редки, и в тех областях языка, которые путешествуют нечасто. C ++ имеет возможность наталкиваться на неопределенное поведение каждый раз, когда вы делаете заявление.
Утечки памяти - это не так важно для современного C ++, но для начинающих и в течение примерно половины своего срока службы C ++ значительно упростил утечку памяти. Эффективный C ++ полностью соответствовал эволюции практик для устранения этой проблемы. Тем не менее, C # все еще может утечка памяти. Наиболее распространенный случай, с которым сталкиваются люди, - это захват событий. Если у вас есть объект, и вы поместили один из его методов в качестве обработчика события, владелец этого события должен быть GC'd, чтобы объект умер. Большинство новичков не понимают, что обработчик событий считается ссылкой. Существуют также проблемы с неиспользованием одноразовых ресурсов, которые могут привести к утечке памяти, но они встречаются не так часто, как указатели в до-эффективном C ++.
Компиляция - C ++ имеет модель компиляции с задержкой. Это приводит к целому ряду хитростей, позволяющих с ним хорошо играть, и сокращает время компиляции.
Строки - Современный C ++ делает это немного лучше, но
char*
он ответственен за ~ 95% всех нарушений безопасности до 2000 года. Для опытных программистов, они сосредоточатся на этомstd::string
, но это все же что-то, чтобы избежать и проблема в старых / худших библиотеках , И это молитва, что вам не нужна поддержка Unicode.И действительно, это верхушка айсберга. Основная проблема в том, что C ++ - очень плохой язык для начинающих. Это довольно противоречиво, и многие старые, действительно, очень плохие ловушки были исправлены путем изменения идиом. Проблема в том, что начинающим нужно учить идиомы чему-то вроде Effective C ++. C # устраняет многие из этих проблем в целом, а остальное меньше заботит, пока вы не продвинетесь дальше по пути обучения.
Я упомянул проблему «утечки памяти». Это не столько проблема языка, сколько программист, ожидающий чего-то, чего не может сделать язык.
Другое - то, что финализатор для объекта C # технически не гарантированно будет выполняться во время выполнения. Это обычно не значения, но это приводит к тому, что некоторые вещи оформляются не так, как вы ожидаете.
Еще одна подводная лодка, с которой сталкиваются программисты, это семантика захвата анонимных функций. Когда вы захватываете переменную, вы захватываете переменную . Пример:
Не делает то, что наивно думают. Это печатает
10
10 раз.Я уверен, что есть ряд других, которые я забыл, но главная проблема в том, что они менее распространены.
источник
char*
. Не говоря уже о том, что вы все еще можете утечь память в C # просто отличноenable_if
На мой взгляд, опасность C ++ несколько преувеличена.
Существенная опасность заключается в следующем: хотя C # позволяет вам выполнять «небезопасные» операции с указателями с помощью
unsafe
ключевого слова, C ++ (в основном расширенный набор C) позволяет вам использовать указатели всякий раз, когда вам это нравится. Помимо обычных опасностей, связанных с использованием указателей (которые одинаковы с C), таких как утечки памяти, переполнения буфера, висячие указатели и т. Д., C ++ представляет новые способы для вас, чтобы серьезно испортить вещи.Эта, так сказать, «лишняя веревка», о которой говорил Джоэл Спольски , сводится к одному: написание классов, которые внутренне управляют своей собственной памятью, также известной как « правило 3 » (которое теперь можно назвать правилом 4 или Правило 5 в C ++ 11). Это означает, что если вы когда-нибудь захотите написать класс, который управляет своими собственными распределениями памяти внутри, вы должны знать, что вы делаете, иначе ваша программа, скорее всего, потерпит крах. Вы должны тщательно создать конструктор, конструктор копирования, деструктор и оператор присваивания, что на удивление легко ошибиться, что часто приводит к причудливым сбоям во время выполнения.
ОДНАКО в реальном повседневном программировании на C ++ действительно очень редко пишут класс, который управляет собственной памятью, поэтому вводить в заблуждение то, что программисты на C ++ всегда должны быть «осторожными», чтобы избежать этих ловушек. Обычно вы будете делать что-то более похожее на:
Этот класс выглядит очень похоже на то, что вы делаете в Java или C # - он не требует явного управления памятью (потому что библиотечный класс
std::string
позаботится обо всем этом автоматически), и никаких «Правил 3» вообще не требуется, так как по умолчанию Конструктор копирования и оператор присваивания в порядке.Это только когда вы пытаетесь сделать что-то вроде:
В этом случае новичкам может быть сложно получить правильное назначение, деструктор и конструктор копирования. Но в большинстве случаев нет причин когда-либо делать это. C ++ позволяет очень легко избежать ручного управления памятью в 99% случаев с помощью библиотечных классов, таких как
std::string
иstd::vector
.Другая связанная с этим проблема заключается в ручном управлении памятью таким образом, чтобы не учитывать возможность возникновения исключения. Подобно:
Если на
some_function_which_may_throw()
самом деле это сгенерирует исключение, вы остаетесь с утечкой памяти , потому что память , выделенная дляs
никогда не будет утилизирована. Но опять же, на практике это вряд ли проблема больше по той же причине, по которой «правило 3» больше не является проблемой. Очень редко (и обычно не нужно) фактически управлять своей собственной памятью с помощью сырых указателей. Чтобы избежать вышеуказанной проблемы, все, что вам нужно сделать, это использоватьstd::string
илиstd::vector
, и деструктор будет автоматически вызываться при разматывании стека после возникновения исключения.Итак, общая тема здесь заключается в том, что многие функции C ++, которые не были унаследованы от C, такие как автоматическая инициализация / уничтожение, конструкторы копирования и исключения, вынуждают программиста быть особенно осторожным при ручном управлении памятью в C ++. Но опять же, это проблема только в том случае, если вы собираетесь в первую очередь выполнять ручное управление памятью, что вряд ли когда-либо понадобится, когда у вас есть стандартные контейнеры и умные указатели.
Так что, на мой взгляд, хотя C ++ дает вам много лишней веревки, вряд ли когда-либо понадобится использовать ее, чтобы повеситься, и ловушки, о которых говорил Джоэл, тривиально легко избежать в современном C ++.
источник
Does C# avoid pitfalls that are avoided in C++ only by careful programming?
. Ответ «не совсем, потому что банально легко избежать ловушек, о которых говорил Джоэл в современном C ++»Я бы не очень согласился. Возможно, меньше подводных камней, чем в C ++, как это было в 1985 году.
На самом деле, нет. Правила , как правило три потеряли массивное значение в C ++ 11 , благодаря
unique_ptr
иshared_ptr
быть стандартизированы. Использование стандартных классов в некоторой степени разумным способом - это не «тщательное кодирование», а «базовое кодирование». Кроме того, доля населения C ++, который все еще достаточно глуп, не информирован или выполняет обе функции, такие как ручное управление памятью, намного ниже, чем раньше. Реальность такова, что лекторам, которые хотят продемонстрировать подобные правила, приходится тратить недели, пытаясь найти примеры, где они все еще применяются, потому что стандартные классы охватывают практически все мыслимые варианты использования. Многие эффективные техники С ++ пошли тем же путем - путем додо. Многие другие не настолько специфичны для C ++. Дайте-ка подумать. Пропустив первый пункт, следующие десять:final
иoverride
помогли изменить эту игру к лучшему. Сделайте свой деструктор,override
и вы гарантированно получите хорошую ошибку компилятора, если унаследуете от того, кто не сделал его деструкторvirtual
. Создайте свой класс,final
и ни один плохой скраб не сможет прийти и наследовать его случайно без виртуального деструктора.Очевидно, я не собираюсь проходить каждый элемент Effective C ++, но большинство из них просто применяют базовые концепции к C ++. Тот же совет вы найдете в любом объектно-ориентированном языке перегрузочных операторов с типизированным значением. Виртуальные деструкторы - это единственная ловушка в C ++, и она по-прежнему действительна, хотя, возможно, с
final
классом C ++ 11 она не так верна, как была. Помните, что Effective C ++ был написан, когда идея применения ООП и специфических особенностей C ++ была еще очень новой. Эти пункты вряд ли о подводных камнях C ++ и больше о том, как справиться с изменением от C и как правильно использовать ООП.Изменить: Подводные камни C ++ не включают в себя такие вещи, как подводные камни
malloc
. Я имею в виду, что, во-первых, каждую ловушку, которую вы можете найти в C-коде, вы также можете найти в небезопасном C # -коде, так что это не особенно актуально, и, во-вторых, только потому, что Стандарт определяет его для взаимодействия, не означает, что его использование считается C ++. код. Стандарт также определяетgoto
, но если бы вы написали огромную кучу беспорядка спагетти, используя его, я бы подумал, что ваша проблема, а не язык. Существует большая разница между «тщательным кодированием» и «следованием основным идиомам языка».using
отстой. Это действительно так. И я понятия не имею, почему что-то лучшее не было сделано. Кроме того,Base[] = Derived[]
и почти каждое использование Object, которое существует потому, что первоначальные дизайнеры не заметили огромного успеха шаблонов в C ++, и решили, что «Давайте просто все унаследуем от всего и потеря всей нашей безопасности типов» было более разумным выбором. , Я также считаю, что вы можете найти некоторые неприятные сюрпризы в таких вещах, как условия гонки с делегатами и другие подобные забавы. Есть и другие общие вещи, например, как дженерики ужасно сосут по сравнению с шаблонами, действительно действительно ненужное принудительное размещение всего в aclass
и тому подобное.источник
malloc
вовсе не означает, что вы должны это делать, и даже не потому, что вы можете быть шлюхой,goto
как сукой, это означает, что веревкой вы можете повеситься.unsafe
на C #, что так же плохо. Я мог бы перечислить каждую ловушку кодирования C # как C, если хотите.C # имеет следующие преимущества:
char
,string
и т.д. зависит от реализации. Раскол между подходом Windows к Unicode (wchar_t
для UTF-16,char
для устаревших «кодовых страниц») и подходом * nix (UTF-8) вызывает большие трудности в кроссплатформенном коде. C #, OTOH, гарантирует, что этоstring
UTF-16.Да:
IDisposable
Есть книга под названием Effective C #, которая похожа по структуре на Effective C ++ .
источник
Нет, C # (и Java) менее безопасны, чем C ++
C ++ проверяется локально . Я могу проверить один класс в C ++ и определить, что класс не пропускает память или другие ресурсы, предполагая, что все указанные классы являются правильными. В Java или C # необходимо проверить каждый ссылочный класс, чтобы определить, требует ли он какой-либо финализации.
C ++:
C #:
C ++:
C #:
источник
auto_ptr
(или несколько его родственников). Это общеизвестная веревка.auto_ptr
просто знатьIEnumerable
или использовать интерфейсы, или не использовать плавающую точку для валюты или чего-то подобного. Это базовое приложение DRY. Никто, кто знает основы программирования, не допустит этой ошибки. В отличие отusing
. Проблема вusing
том, что вам нужно знать для каждого класса, является ли он Одноразовым (и я надеюсь, что он никогда не изменится), и если он не Одноразовый, вы автоматически запрещаете все производные классы, которые могут быть Одноразовыми.Dispose
методом, вы должны реализовать егоIDisposable
(«правильный» способ). Если ваш класс делает это (что эквивалентно реализации RAII для вашего класса в C ++), и вы используетеusing
(что похоже на интеллектуальные указатели в C ++), все это прекрасно работает. Финализатор в основном предназначен для предотвращения несчастных случаев -Dispose
отвечает за правильность, и если вы его не используете, то это ваша вина, а не C #.Да, 100%, да, так как я думаю, что невозможно освободить память и использовать ее в C # (при условии, что она управляемая, и вы не переходите в небезопасный режим).
Но если вы знаете, как программировать на C ++, чего не знает невероятное количество людей. Вы в значительной степени в порядке. Как Чарльз Сальвия, классы на самом деле не управляют своей памятью, поскольку все это обрабатывается в уже существующих классах STL. Я редко использую указатели. На самом деле я работал над проектами без единого указателя. (C ++ 11 делает это проще).
Что касается опечаток, глупых ошибок и т. Д. (Например:
if (i=0)
ключ застрял, когда вы нажали == очень быстро), компилятор жалуется, что приятно, так как улучшает качество кода. Другой пример - забыватьbreak
в операторах switch и не разрешать вам объявлять статические переменные в функции (что мне иногда не нравится, но это хорошая идея imo).источник
=
/==
, использовав==
для сравнения ссылок и введя.equals
для равенства значений. Плохой программист теперь должен отслеживать, является ли переменная «double» или «Double», и обязательно вызывать правильный вариант.struct
вы можете сделать это,==
что работает невероятно хорошо, так как в большинстве случаев будут только строки, целые и плавающие числа (то есть только члены структуры). В моем собственном коде я никогда не получаю эту проблему, за исключением случаев, когда я хочу сравнить массивы. Я не думаю, что когда-либо сравнивал список или неструктурные типы (string, int, float, DateTime, KeyValuePair и многие другие)==
для равенства значений иis
для ссылочного равенства.