10 отличий C от C++

Почитав Б.Страуструпа, Г.Буча и вообще книги об объектно-ориентированном программировании, некоторые приверженцы C делают вывод о том, что C++ - это нечто запредельно сложное и гиганский шаг вперед. Это заблуждение. На самом деле C++ - это тот же C, но с некоторыми удобными упрощениями. Если вы - хороший С-программист и воспринимаете C++ как нечто на порядок более крутое, то это - диагноз. Который называется "перечитал заумных книжек". Это излечимо J

Часто можно слышать споры на тему: писать на C или на C++? При этом существует расхожее мнение о том, что есть два стиля написания программ: стиль С и стиль C++. Они противопоставляются друг другу. C++ ассоциируется с ООП (объектно-ориентированноым программированием), а чистый C - с ПОП (процедурно-ориентированным программированием). ООП и ПОП также противопоставляются.

На самом деле, все, что есть нового в C++ - уже было в C и вы этим пользовались (если действительно много программировали на C). Только в C++ это записывается чуть по-другому. Однажды у меня вышел спор с одним из приверженцев C на эту тему, в результате чего был написан список из 10 различий между двумя языками.

Различие 1. Объекты

В C++ появились классы и объекты. Технически класс C++ - это тип структуры в C, а объект - переменная такого типа. Разница только в том, что в C++ есть еще модификаторы доступа и полями могут быть не только данные, но и функции (функции-методы).

Функция-метод - это обычная функция C, у которой первый параметр - это указатель на структуру, данные которой она обрабатывает: this. Если сравнить, как выглядят функции-методы в C++ и функции с параметром-указателем на структуру в C, то мы обнаружим, что всего лишь изменилась форма записи. В C++ получается короче, так как this и имя типа во многих случаях писать не обязательно (подразумевается по умолчанию).

Модификаторы доступа - это слова public, private и protected. В C вместо них была внимательность программиста: public - значит с этими полями делаю, что хочу; private - значит к этим полям обращаюсь только с помощью методов этой структуры; protected - то же, что public, но еще можно обращаться из методов унаследованных структур (см. следующий пункт).

Различие 2. Наследование

То, что в C++ - наследование, в C - это просто структура в структуре. При программировании в стиле C++ применяются такие красивые и звучные слова, как "класс Circle порожден от класса Point" или "класс Point наследуется от класса Circle и является производным от него". На практике все это словоблудие заключается в том, что структура Point - это первое поле структуры Circle.

При этом реальных усовершенствований два. Первое - поля Point считаются так же и полями Circle, в результате доступ к ним записывается короче, чем в C. Второе - в обоих структурах можно иметь функции-методы, у которых имена совпадают с точностью до имени структуры. Например, Point::paint и Circle::paint . Следствие - не надо изобретать имена вроде Point_paint и Circle_paint, как это было в C, а префиксы Point:: и Circle:: в большинстве случаев можно опускать.

Различие 3. new и delete

В C++ появились две новые операции: new и delete. В первую очередь это - сокращения для распространенных вызовов функций malloc и free:

В стиле C:

Point *p = (Point*) malloc(sizeof(Point));
free(p);

В стиле C++:

Point *p = new Point;
delete p;

При вызове new автоматически вызывается конструктор, а при вызове delete - деструктор (см. следующий пункт). Так что нововведение можно описать формулой: new = malloc + конструктор, delete = free + деструктор.

Различие 4. Конструкторы и деструкторы

Когда программируешь в стиле C, после того, как завел новую переменную типа структуры, часто надо ее инициализировать и об этом легко забыть. А перед тем как такая структура закончит свое существование, надо ее почистить, если там внутри есть ссылки на какие-то ресурсы. Опять-таки легко забыть.

В C++ появились функции, которые вызываются автоматически после создания переменной структуры (конструкторы) и перед ее уничтожением (деструкторы). Во всех остальных отношениях это - обычные функции, на которые наложен ряд ограничений. Некоторые из этих ограничений ничем не оправданы и мешают: например, конструктор нельзя вызвать напрямую (дестркутор, к счастью, можно). Нельзя вернуть из конструктора или деструктора значение. Что особенно неприятно для конструктора. А деструктору нельзя задать параметры.

Различие 5. Виртуальные функции

Из всех усовершенствований это вызывает наибольшую "щенячью радость". Как обычно, придуманы и звучно-научно-рекламные названия: "полиморфизм", "виртуальный", "абстрактный". Если отбросить разницу в терминологии, то что получим в сухом остатке? А получим мы очередное сокращение записи. И очень большое сокращение.

При программировании на C часто бывает так, что имеется несколько вариантов одной и той же структуры, для которых есть аналогичные функции. Например, есть структура, описывающая точку (Point) и структура, описывающая окружность (Circle). Для них обоих часто приходится выполнять операцию рисования (point). Так что, если у нас есть блок данных, где перемешаны точки, окружности и прочие графические примитивы, то перед нами стоит задача быстро вызвать для каждого из них свою функцию рисования.

Обычное решение - построить таблицу соответствия "вариант структуры - фукция". Затем берется очередной примитив, определяется его тип, и по таблице вызывается нужная функция. В C этот метод применять довольно нудно из-за того, что во-первых, надо строить эту таблицу, а во-вторых, внутри структур заводить поле, сигнализирующее о том, какого она типа, и следить за тем, чтобы это поле содержало правильное значение.

В C++ всем этим занимается компилятор: достаточно обозначить функцию-метод как virtual, и для всех одноименных функций будет создана таблица и поле типа, за которыми следить будет опять-таки компилятор. Вам останется только пользоваться ими: при попыке вызвать функцию с таким именем, будет вызвана одна из серии одноименных функций в зависимости от типа структуры.

Различие 6. Исключения

Исключение по своей сути - это просто последовательность goto и return. Основан на обычной C-технологии setjmp/longjmp. try и catch - это setjmp с проверкой. throw - это longjmp. Когда вызывается throw, то проверяется: если он окажется внутри блока try, то выполняется goto на парный блок catch. Если нет, то делается return и ищется catch на уровень выше и так далее.

Наличие в throw/catch параметра ничего принципиально не меняет: и в обычном C можно было заполнить какие-то переменные перед вызовом longjmp и потом их проанализировать.

Различие 7. Перегруженные операторы

Относитесь к ним как к уродливым функциям и все станет ясно. a + b, где a и b - типа Point это функция от двух аргументов a и b, возвращающая Point:

Point operator+(Point a, Point b)

Написать a+b равносильно вызову такой функции: operator+(a,b). Иногда эта технология удобна, а иногда вносит путаницу.

Различие 8. Ссылка

Многие программисты изучали C на основе языка Pascal. В Pascal есть возможность возвращать из функции больше одного параметра. Для этого применялось магическое слово "var". В C для того, чтобы сделать то же самое, приходилось расставлять в тексте уйму символов "*".

Разработчики C++ вняли стонам несчастных программистов и ввели слово var. А чтобы все это выглядел ооригинально, "var" они переименовали в "&" и назвали "ссылкой". Это вызвало большую путаницу, так как в C уже были понятия "указатель" (та самая звездочка) и "адрес" (обозначался тем же символом &), а понятие "ссылка" звучит тоже как что-то указующе-адресующее. Вот если бы, не мудрствуя лукаво, добавили слово var…

С одной стороны, использование ссылок намного сокращает текст программы. Но есть и неприятности. Во-первых, вызов функции, в которой параметр является ссылкой, выглядит так же, как вызов с обычным параметром. В результате "на глаз" незаметно, что параметр может измениться. А в C это заметно по значку &. Во-вторых, многочисленные звездочки в C напоминают программисту о том, что каждый раз выполняется дополнительная операция * разыменования указателя. Что побуждает сделать разумную оптимизацию. В C++ эти операции остаются незамеченными.

Различие 9. Inline, template и default-аргумент

Аргумент по-умолчанию - это то, о чем мечтали программисты C: чтобы иногда не надо было при вызове задавать некоторые параметры, которые в этом случае должны иметь некоторое "обычное" значение.

Желание программистов C контролировать типы параметров в define-ах породило в C++ inline-функции. Такая функция - это обычный define с параметрами, только не надо мучиться с символами "\" и проверяются типы.

Желание узаконить в параметрах define имя типа породило template. Главный плюс template - то, что #define с одинаковыми параметрами породит два одинаковых куска кода. А template в компиляторе скорее всего будет соптимизирован: одинаковые куски кода будут соединены в один. Имеется небольшой контроль типов по сравнению с #define, но не очень удобный.

В то же время template имеют ряд серьезных недостатков. Первый - ужасный, неудобный синтаксис. Чтобы это ощутить, достаточно попробовать. Уж лучше бы разрешили не контролировать типы некоторых параметров inline-функций. Второй недостаток - template остался так же неудобен при работе с отладчиком, как и #define.

Ну и последнее нововведение, продиктованное, видимо, все тем же стремлением избавиться от #define. Это - тип "имя поля" (pointer to member). В C удобно было применять имена полей структур в define. В C++ тоже самое можно сделать с помощью операторов ::*, .* и ->*. Например &Circle::radius - это имя поля radius структуры Circle, а Circle::*radius - соответствующий тип. Такую величину можно передать, как параметр. Фактически речь идет о смещении этого поля относительно начала структуры. Бывает полезно. Примерно так же можно передать адрес функции-метода.

Различие 10. Язык более высокого уровня?

Когда появились все эти нововведения, то многим стало видно то, что раньше было видно не столь многим. Это нормально: код упростился, а значит те его свойства, которые раньше были замаскированы лишними символами, стали заметны при меньшем напряжении мозгов. В этом и только в этом заключается более высокий уровень C++. Чуть больше абстракции, чуть меньше деталей - можно сосредоточится на более крупных блоках.

Существует мнение, что писать в стиле C на C++ - дурной стиль. Это мнение - всего лишь дань моде. Если в стиле C++ получается короче, лучше, надежнее, то глупо писать в стиле C. Это так, но верно и обратное!

Простой пример: у вас есть большой массив из 100 тысяч структур Point, который инициализируется один раз (все поля на 0) и много раз копируется в такие же массивы. Писать для элемента такого массива конструктор оказывается накладно. При его создании будет вызван конструктор для каждого элемента. Потом вы создадите массив, куда его надо копировать - и снова вызовы конструкторов. Затем вы выполняете копирование и затираете результаты второй инициализации. Мало того, что 100 тысяч вызовов конструктора просто не сопоставимы с одним вызовом memset, но эта серия вызовов будет повторяться не один раз, а много.

Такие примеры можно привести для каждого нововведения C++. Каждый плюс неизбежно тянет за собой минусы. Для хорошего программиста главным законом должна быть не мода, а конечный результат и трезвый расчет: что в данном конкретном случае выгоднее с точки зрения эффективности программы и времени, затраченного на ее разработку.

Что касается объектно-ориентированного программирования, то на самом деле оно не имеет никакого отношения к разнице между C и C++. Благодаря ряду усовершенствований, код на C++ компактнее и надежнее, чем на C. Часть этих усовершенствований связана с ООП, а часть - нет. Например, аргументы функций по-умолчанию и inline-функции к ООП не имеют никакого отношения. Они имеют отношение к ужесточению контроля типов.

ООП - это просто идея: "в зависимости от данных, выполнить процедуру". А ПОП (процедурно ориентированное программирование) - "в зависимости от процедуры изменить данные". Глупо молиться на ООП или на ПОП или отвергать что-то из них и тем более ужасаться при их смешивании. Разумно использовать тот и другое, смотря как будет точнее, проще, быстрее, компактнее.

Смешон консерватор, который говорит: "Я назло не буду использовать ООП, так как это - глупая новомодная штучка." Такой консерватор обычно упрямо применяет только C и при этом не замечает, что давно пишет в стиле ООП, но на чистом C! Он думает, что раз он использует C, его никто не заподозрит в излишнем умничаньи.

Смешон модник, который говорит: "Я буду использовать ООП везде, так как хочу прослыть прогрессивным человеком, который быстро осваивает все новое!" Такой "передовик" упрямо применяет классы и template где надо и где не надо. Он громогласно вопит об ООП, но сколько-нибудь сложная часть его кода обычно написана в стиле ПОП: потому, что он ценит ООП только как признак прогрессивности, но не понимает простого смысла, заключенного в нем.

"Будьте проще и люди к вам потянутся!" J