Pull to refresh

Сравнительный анализ языков C# и C++

Reading time 9 min
Views 17K
Давайте взглянем на эти два языка внимательнее. Что важно для прилежного программиста? Чтобы его код был удобочитамым, дабы в любой момент, спустя любое время можно было изменить этот код.

C++


Чему учит нас C++, что он нам говорит, какие парадигмы он воздвигает?

1. Что можно именовать типы, методы и переменные маленькими буквами

        std::list<std::string> L;
	L.push_back(50);
	L.push_front(-50);

Вы спросите, что же плохого в именовании элементов маленькими буквами?

А то, что, во-первых, C++ не различает контекст типа и контекст переменной и функции! И вы не можете никак взять и объявить поле, скажем, size size или point point! Поэтому, имеет смысл именовать функции и переменные маленькими буквами, но только если тип именуется в стиле C#, а именование типа маленькими буквами портит весь смысл такого действия! А смысл заключается в невозможности пересечения имени типа и его членов!

Ну, а во-вторых, именование как C#, выглядит солиднее, красивее.

Хотя, конечно, вы можете именовать именно так как сейчас предложено, но не факт, что другие будут следовать этому правилу.

2. Что можно сокращать так, что будет непонятно тому, кто использует ваш код, но зато «быстро печатается». Или сокращать, но все же понятно.

Вот лишь некоторые методы из стандартной библиотеки:

	std::string s; s.rfind(); s.c_str();
	std::fstream fs; fs.copyfmt(); fs.rdbuf();

Вы что-нибудь поняли? Что есть «rfind» и почему не «find», что это за r такая? Что такое «c_str»? Нужно посмотреть документацию, чтобы понять, что это превращение строки в «строку как в С» — массив-указатель на символы…

3. Что можно использовать макросы, как универсальное средство решения проблем. При этом намеренно забывается, или умалчивается, что эти макросы можно легко заменить, и что они глобальны, не входят в какое-либо пространство имён.

Вот например:

	#define foreach(x,c) for(auto x = c.begin(); x != c.end(); x++)
	#define min(x,y) ((x)<(y) ? (x) : (y))
	#define true false
	#define NULL 1

Обратите также внимание на то, что если параметры параметризованного макроса не обставлять скобками, то все вычисления с этим макросом легко испортятся! А дело в том, что параметры раскрываемого макроса воспринимаются как текст (!), и если не поставить скобки, то мы получим примерно такие варианты:

	a = b * min(x,y);  //==>  a = b*x < y ? x : y;
	a = min(5,3*k)*100; //==>  a = 5 < 3*k ? 5 : 3*k*100;

Согласитесь, это не то, что мы ожидали! И только познавшие эту хитрость макросов с параметрами на своем горьком опыте, долгих мытарствах и восклицаниях: «боже, что я не так сделал!» (или прочитавшие документацию к языку), пишут такие макросы правильно (и то, нужно быть очень внимательным!).

4. Что вы сами, сами! ‒ должны делать многие вещи, которые в C++ поленились сделать.

  • Не реализована нормальная работа стека. Я до сих пор не понимаю, как он работает! А где взять привычный нам хеш (словарь/карту)?
  • Выделение памяти (new/malloc) жутко тормозит! Вы сами, сами! должны делать управлятель памятью (ничего себе задача!), либо довольствоваться медленной работой
  • Не реализованы элементарные классы — класс «Массив», класс «Двумерный массив» и класс «Трехмерный массив».
  • Не созданы преобразования базовых типов: строковых и числовых (в том числе и логических), их надо делать самому (хотя, есть для этого возможности)

И таких вещей много еще. Конечно, можно найти и готовые решения. Но неизвестно, какого они качества будут, найдете ли вы их вообще; к тому же, их нужно будет подстроить под свой стиль.

5. Что можно свободно баловаться с указателями и даже с константными указателями, надо лишь правильно преобразовать типы. Взгляните:

	void Func(const int* x) {
		((int*)x)[0] = 5;
	}

	int main() {
		int m = 500;
		Func(&m);
		std::cout << m <<'\n';
		return 0;
	}

И что же выведется у нас? 5! Константный параметр изменил свое значение! Так можно любые константные указатели менять, даже строковые или числовые (правда их изменение влечет ошибку обращения с памятью). Или вот:

	struct T {
	int M,N;
	T(int m, int n) {
		M = m; N = n;
	}
	};

	int main() {
		static const T t = T{5,3};
		((T*)(&t))[0] = T(-1,-5);
		std::cout << t.M <<" "<< t.N << "\n";
		return 0;
	}

Здесь изменяется статическая константа структурного типа! То есть, та, в чьей неизменяемости мы уверены на все 100%! «А вот нет!» — говорит на C++. И выводится именно изменённое значение константы.

6. Что можно вываливать содержимое перечислений в общий контекст! При чем, без включения стандарта C++11, мы не сможем сделать иначе.

Это ужасно! Взгляните на это:

	enum Month {
		JANUARY,FEBRUARY,
		MARCH,APRIL,MAY,
		JUNE,JULY,AUGUST,
		SEPTEMBER,NOVEMBER, DECEMBER
	};
	...
	Month m = JANUARY;
	...
	switch(x) {
	case JANUARY:
	case FEBRUARY:
	}

А если элементы пересекаются — присутствуют и в одном, и в другом перечислении (например, такие вполне могущие повторяться «NORMAL», «DEFAULT»)? Что тогда?.. C++ попускает вызов перечислений именно таким образом! Ленивые распущенные программисты C++ скажут: «А что, также быстрее пишется, также можно!», им невдомёк, что это распущенность, как невдомёк тем, кто ходит на современные российские комедии («А что, это прикольно, как в жызни!»)!

Даже если вы хотите правильно вызвать (допустим, вы порядочный программист C++, использующий чей-то код), то получается и вовсе некрасиво, уродливо ‒ получается сочетание константного стиля (элемент) и стиля C# (имя перечисления), и вы вынуждены писать без имени перечисления, дабы было хотя бы красиво.

Хотя, конечно, вы можете быть порядочным и именовать элементы перечисления правильно (как сами перечисления (здесь, вообще, врятли будет пересечение имени типа и его элемента, потому может быть одинаковый стиль, что предпочтительнее, т.к. красивее)) и не вываливать элементы в контекст, даже запрещая это добавлением слова «class» после ”enum" (стандарт C++11). Но не факт и здесь, что остальные будут такими же порядочными и соблюдать правила!

Правильно делать так:

	enum class Month {
		January, February,
		March,April,May,
		June,July,August,
		September,October,November, December
	};

7. Что вовсе не являются ошибкой такие вещи как:

  • не возврат функцией значения, или возврат не во всех условных ветках
  • использование не определенной переменной

Зато об этом есть хотя бы предупреждения, но порою забывают прочесть, и они образуются только после компиляции, пока мы их прочтем, может произойти ошибка, ведь мы обычно компилируем с дальнейшим запуском.

И, самая страшная вещь ‒ чтобы подставить на место параметра-ссылки переменную при вызове, не надо никаких дополнительных слов. Это ужасно, так как ведет к тому, что вызов может быть воспринят как вызов без каких-либо изменений параметров. Взгляните:

	void F(int& x, int& y);
	. . .
	F(x,y);
	void FF(int x, int y);
	FF(x,y);

Что мы видим? Мы видим, что ЕСЛИ УБРАТЬ ЗНАК ССЫЛКА, ТО ВЫЗОВ НЕ ИЗМЕНИТСЯ. Мы можем не знать, где ссылочный параметр, где параметр-значение! Лишь по смыслу функции или при активации подсказки в IDE мы можем это определить!

Подведём итоги


С++ нам говорит: у меня нет некоторых важных вещей, ты должен достать или написать их сам, а еще в нашем «демократическом» языке нет четких правил, живите как хотите!

С++ нас учит: пишите безобразно, развратно, используя макросы, двойные-тройные-четверные подчеркивания, сокращая имена до малопонятных, но быстропечатаемых, вываливая элементы перечислений в общий контекст, свободно обращаясь с указателями, даже изменяя содержимое констант, да и вообще на некоторые вещи мы не будем ругаться прерывая компиляцию, а лишь предупредим вас… Как бы говоря: вам никто не запрещает этого, все ограничения надуманы!

Меж тем, такие учения и парадигмы лишь портят прилежных программистов, к тому же, заставляя их создавать/искать некоторые базовые, необходимые, решения, плохо сказывась на мировоззрении и нервах программиста и ведущие к ухудшении удобочитаемости кода…

C#


Но взглянем на C#! Что же он нам говорит, какие парадигмы воздвигает, чему же учит?

1. Что слова в именах типов, функций, переменных начинаются с большой буквы и соединяются без разделителей (благодаря большим буквам слова видны). Это называется стилем C# (другое название ‒ стиль Pascal), представитель верблюжей группы стилей. Что первое слово имени каждой закрытой переменной начинается с маленькой, дабы не путать с открытыми ‒ стиль Java, второй представитель верблюжей группы стилей.

Контекст имен типов отличается от контекста имен функций, переменных и констант. Вы можете объявить поле Size Size или Point Point, и это скомпилируется и можно будет даже правильно использовать (даже сочетать имя поля и имя типа без использования this!)!

	struct Rect {
		Point Point;
		Size Size;
		// Функции...
		Rect(int x,int y,int w,int h) {
			Size = new Size(w,h);
			Point = new Point(x,y);
		}
		...
	}
	Rect r = new Rect(new Point(5,5), new Size(300,200));

2. Что никаких сокращений вне контекста функции! Своим стилем именования в стд. библиотеках C# показывает пример остальным ‒ как правильно именовать элементы кода, стиль этот красив и вдохновляет на подвиги. Никаких сокращений, верблюжий стиль с заглавной буквы, никаких подчеркиваний, вы их вообще не найдете! Каждое слово полно, либо является аббревиатурой (что, естественно допускается), также начинающейся с большой, а далее ‒ маленькими.

	XmlDocument document = new XmlDocument();
	Rectangle rect = new Rectangle(300,500,1000,500);

3. Возможности макросов урезаны до предела. Можно лишь объявлять и проверять, есть ли такой-то — такой-то макрос, а некоторые макросы объявляются через настройки компилятора. Всё!

	#define M
	...
	#if M
		double M = 53;
	#endif

Причём имя макроса и имя элемента (тип/функция/переменная/константа) параллельны, никак не связаны, можно одинаково называть и макрос и переменную (или другой элемент).

Вот какими безопасными могут быть макросы.

4. Что многие, многие вещи уже качественно сделаны в C#, а нормальный, солидный стиль делает их еще и самодокументирующимися, понятными, особенно после прочтения документации.

Полно еще классов и пространств имен в основных сборках, которые еще не изучены вами. И это еще только основные сборки, а есть еще множество других!

В основных сборках сделаны:
  • все базовые преобразования между строковым, числовым и логическим типами, преобразования находятся в классе Convert (System)
  • Вменяемые коллекции (System.Collections*)
  • Классы Xml (System.Xml*)
  • Сетевые классы (System.Net*)
  • Многопоточность (System.Threading*)
  • Классы по работе с процессами, диагностикой производительности, отладке (System.Diagnostics*)
  • и даже компилятор CodeDom для языков .NET! (System.CodeDom)
  • и многое другое

А есть еще много дополнительных, среди них можно выделить 2 основные, используемые в оконных приложениях: System.Windows.Forms* (по работе с GUI) и System.Drawing* (по работе с графикой)

Также, есть некоторые полезные встроенные вещи в языке, которых нет в C++:
  • встроенная конструкция foreach ‒ перечисление элементов коллекции
  • автоматический сборщик мусора, нам не нужно вручную освобождать память, а выделяется очень быстро, и не надо делать собственное управление памятью
  • свойства ‒ вместо того, чтобы писать методы доступа, можно писать свойства, которые объединяют в себе два метода доступа и не имеют параметров, все более лаконично, особенно сказывается на красоте интерфейса класса
  • всё наследует от объекта и может быть превращено в строку переопределяемым методом ToString()

	class Example {
		int X{get; set;}
		int Y{get; set;}
		object Object{
			get{return object;}
			set{object = value==null ? new object() : value;}
		}
	}
	...
	foreach(String arg in args) {
		Console.WriteLine( arg );
	}
	...


5. Что нельзя просто так взять и побаловаться указателями. Вам нужно для этого разрешить небезопасный код в настройках компиляции и обозначить класс как небезопасный, добавив «unsafe» в список его модификаторов. И только после этого можно будет с ними работать.
Кроме того, невозможно, невозможно! ‒ получить адрес константы или переменной только для чтения, что делает невозможным её изменение! ‒ и мы можем спокойно спать сладким сном, зная, что C# не даст константы и неизменяемые переменные в обиду всяким садистам, ой, то есть, изменять!

	unsafe class C {
		int* n;
		static void Main() {
			byte* n = (byte*)(5);
			n = (byte*)(n+3);
		}
	}

6. Что элементы перечислений остаются в контексте перечислений и не вываливаются в глобальный контекст. И они пишутся (по соглашению) в таком же стиле ‒ стиле C# ‒ как и имя содержащего их перечисления.

	enum DayOfWeek {
		Monday = 1, Tuesday, Wednesday,
		Thursday, Friday, Saturday,  Sunday
	}

В C# вообще ни одна функция/переменная/константа не может находиться вне класса.

7. Что многие ошибки, которые программист по невнимательности пропускает, на выявление которых можно определить алгоритм, C# обнаруживает и говорит, что это ошибка, прерывая компиляцию, если эта ошибка влияет на работу, а некоторые остаются как предупреждение, если не влияют. То есть, ошибки по невнимательности.

Среди ошибок:

  • возврат значения не во всех условных ветвях, либо отсутствие возврата значения
  • использование не определенной переменной
  • неявное преобразование большего по размеру в ОЗУ числа в меньшее по размеру в ОЗУ ‒ double==>float / long==>int==>short==>byte и т.п., а также знакового и беззнакового друг в друга: int<=>uint, short<=>ushort и т.п.; а также bool<=>число
  • неявное преобразование bool<=>число
  • перепрыгивание с одной метки case на другую, если данная не закрыта командой прерывания break, при том, что-то содержит или последняя.

Среди предупреждений:

  • не использованная переменная, функция, событие (приватная или локальная)
  • не достижимый код ‒ значит, перед ним стоит команда перехода (return/goto/break/continue)


Кроме того, параметры функции могут иметь один из двух модификаторов:
1. ref ‒ передача ссылки, в объявлении и передаче ссылке переменную нужно явно указать это слово впереди.
2. out ‒ то же самое, но переменная еще и может быть не объявлена

	void F(ref int x,ref int y){ ... }
	void O(out float j,out float k){ ... }
	...
	int x = 0, y = 0;
	F(ref x,ref y);
	int j,k;
	O(out j,out k);

И мы при вызове функции можем видеть, где мы передаем ссылку, а где ‒ значение. Это здорово, это хорошее правило языка.

Есть и другие полезные ограничения:

  • порядок с операторными функциями ‒ они все обязаны быть статичными
  • нельзя создать функцию/переменную/константу вне класса (ведь на любые действия или данные можно найти класс)

Подведём итоги


C# нам говорит: программируйте в удовольствие, у меня для этого всё есть: замечательные правила языка, правила именования, а также разные полезные библиотеки.

C# нас учит: используйте правильный стиль именования, забудьте про всякие подчеркивания, они излишни; забудьте про свободно гуляющие глобальные переменные и функции, у нас все находится в классах; не считайте ограничения надуманными, а считайте их правильными и упорядывающими ваше мышление, пишите по правилам и вы обретете нирвану!

Весьма полезные и нужные порядочному программисту учения и парадигмы, отлично сказывающиеся на настроении и мировоззрении и ведущие к порядку в голове и коде.

Вывод


Если вы хотите стать приверженцем развращающей демократии C++, то туда вам и дорога, вы вправе сделать этот выбор. Но если вы готовы постичь нирвану в программировании, сохранить свои нервы, здоровье и здравомыслие, то ваш выбор ‒ C#.
Tags:
Hubs:
-62
Comments 77
Comments Comments 77

Articles