3.1. Общая информация про язык C++

(Этот раздел не то чтобы необходим для базового изучения C++, но тем не менее очень полезен. Если не хотите читать длинный текст, переходите к следующему разделу, про среды разработки. Но лучше все-таки прочитайте, вам потом будет проще понимать дальнейший текст.)

3.1.1. Про языки С и C++

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

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

Тем не менее, во многих учебниках и источниках информации вы можете столкнуться именно с конструкциями в стиле C, не удивляйтесь. Эти конструкции не надо использовать, а такие учебники не надо читать; благо учебников и хороших книг и вообще источников информации по C++ очень много. Я ниже буду приводить конструкции в стиле C чисто «для сведения», не разъясняя, как они работают, но просто чтобы вы не удивлялись, если их увидите.

(А еще есть язык C#, который на самом деле имеет очень мало отношения к C и C++; про него мы не будем говорить.)

3.1.2. Статическая типизация и компилируемость

В отличие от питона, C++ — компилируемый и статически типизированный язык. Это обозначает следующее.

3.1.2.1. Компилируемость

Для начала, компилируемость. Если питоновскую программу после написания вы просто сразу запускаете, а точнее (и это важно!), запускаете питон, чтобы он выполнил вашу программу, то на C++ запуск написанной программы состоит из двух шагов. Сначала вы должны скомпилировать ваш код — запустить специальную программу, называемую компилятор C++ (который собственно и умеет понимать язык C++), в результате у вас получится исполняемый файл (.exe под Windows), который вы можете уже запускать напрямую, без C++. И собственно вторым шагом вы уже запускаете этот исполняемый файл.

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

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

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

Простейший пример. Рассмотрим следующий питоновский код:

a = int(input())
if a == 2:
    prnt("Yes")

Тут опечатка в названии функции print. Но она будет обнаружена, только если вы введете a==2. Если вы введете любое другое число, то питон не пойдет в if, и не заметит, что вы вызываете несуществующую функцию.

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

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

3.1.2.2. Статическая типизация

Второе радикальное отличие C++ от питона — это статическая типизация. Она тесно связана с компилируемостью. В питоне вы могли в одной и той же переменной хранить то число, то строку, то массив и т.д. Следующий код на питоне совершенно корректен:

a = 20  # записали в переменную a число
a = "Test"  # в ту же переменную записали строку вместо числа
a = []  # или массив

В C++ такое недопустимо. В C++ каждая переменная имеет конкретный тип, который указывается заранее (при объявлении переменной, см. ниже), и не может изменяться во время работы программы. Вы сразу, когда пишете код, указываете, какого типа данные будут храниться в какой переменной, и компилятор это проверяет сразу в процессе компиляции.

Частный эффект статической типизации — это то, что в любом выражении компилятор может и будет осуществлять проверку того, что там не перепутаны переменные разных типов. Для примера, рассмотрим следующий код на питоне:

a = input()
if a == 2:
    print(a // 2)

Тут забыто int, поэтому в переменной a оказывается строка. Поэтому когда вы сравниваете a с числом 2, сравнение всегда окажется ложным. Но питон это проглотит без проблем: питон вполне разрешает сравнивать строку с числом (просто всегда получается False). C++ же в аналогичной ситуации выдал бы ошибку компиляции. (Естественно, для этого вы должны заранее сказать C++, какого типа будет переменная a, но как раз про это я и буду писать ниже.)

Дальше в этом же коде написано a//2. Но строки нельзя делить пополам. На питоне это будет ошибкой, но она будет обнаружена, только когда исполнение кода дойдет до этой строчки (аналогично примеру с prnt выше). В данной конкретной программе исполнение никогда не дойдет до этой строчки (потому что условие if никогда не выполнится), но даже если вы перепишете условие на if a=="2", то ошибка с делением в программе останется, но будет проявляться только когда a=="2". При невнимательном тестировании вы легко можете это не заметить. (И это, конечно, очень простой пример; в реальных программах, конечно, все бывает намного запутаннее). C++ же в аналогичной программе еще при компиляции заметит, что вы пытаетесь поделить строку на число, и выдаст ошибку.

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

(Ну и, конечно, код со статической типизацией работает быстрее — потому что не надо на каждом действии смотреть, какой сейчас тип у какой переменной.)

Примечание

Есть известная шутка, что если программа на C++ скомпилировалась, то в ней нет ошибок, она будет работать ровно так, как и ожидалось. Это, конечно, шутка; есть очень много ошибок, которые компилятор C++ не заметит, и в наших алгоритмических задачах вы, конечно, это прочувствуете сполна.

Но, как и в любой шутке, в ней есть доля правды. Ошибки вида использования неизвестной функции или ошибки с типами, конечно, очень распространены — в тех языках, в которых нет средств контроля за этим. А если компилятор (или кто-нибудь еще) тщательно следит за типами, то все такие ошибки становятся невозможны. Это особенно проявляется в больших программах, где вы работаете вовсе не с простейшими типами — числами, строками и т.д. — а со сложными структурами и классами, когда вы используете объектно-ориентированное программирование по максимуму (ну или другие аналогичные парадигмы программирования).

Представьте себе, что вы пишете программу, управляющую беспилотным автомобилем. У вас в программе, скорее всего, будет объект, отвечающий за работу с двигателем, у него будет отдельный тип Engine и например будет метод accelerate (ускориться). И будет тип SteeringWheel, отвечающий за работу с рулем, и у него будут методы turnLeft (повернуть налево) и turnRight (направо). Соответственно, вы можете по ошибке у объекта, отвечающего за работу с двигателем, вызвать метод turnLeft, и наоборот. Если бы вы писали бы на питоне, это не было бы обнаружено до тех пор, пока соответствующий код не будет выполняться. А на C++ программа просто не скомпилировалась бы.

Поэтому если программа, особенно большая, на C++ скомпилировалась, то как минимум вы вызываете корректные методы у корректных объектов. Конечно, ошибки вида «поворачиваем налево там, где надо было направо», останутся, но все-таки многие ошибки, которые на питоне были бы пропущены, на C++ будут замечены.

Собственно, поэтому многие современные языки с динамической типизацией (т.е. не требующие указывать типы переменных заранее) двигаются в сторону проверки типов тем или иным способом (type hints в последних версиях питона, или движение от Javascript к TypeScript).

(Все вышесказанное не значит, что языки с динамической типизацией плохие. У каждого языка есть свои достоинства и недостатки; и более того, та же динамическая типизация во многих случаях наоборот может быть достоинством, а статическая типизация как в C++ может наоборот мешать. Все зависит от ситуации, от конкретной задачи и т.д.)

Примечание

Помимо «статической» и «динамической» типизации существует перпендикулярная классификация: «слабая» и «сильная». Языки со «слабой» типизацией позволяют больше смешивать переменные разных типов в одном выражении, языки с «сильной» — меньше. Например, Python можно назвать языком с более сильной типизацией чем C++: в языке C++ можно написать и "hello" + 2, и '0' + 4, и это скомпилируется во что-то (более или менее неожиданное). В Python же сложить строчку с числом нельзя никак. (Зато, правда, Python позволяет сравнивать строку с числом, а C++ нет.)

Чёткого определения этим видам типизации, тем не менее, нет. Если вам интересно, вы можете почитать хорошую вводную статью с примерами: https://habr.com/ru/post/161205/

3.1.3. Стандарты и компиляторы

В отличие от многих других языков программирования, где зачастую есть «официальный», «эталонный» компилятор/интерпретатор/… и возможно пара альтернативных (например, в питоне есть «официальный» питон — так называемой Cpython, и еще есть PyPy и пара совсем малоизвестных реализаций), в C++ есть довольно много разных компиляторов. Наиболее известные из них — это GNU C++ Compiler (сокращенно G++ или GCC), Clang, Microsoft Visual Studio (сокращенно MSVS, это и среда разработки, и компилятор), и Intel C Compiler (ICC); но также есть еще и многие другие, и среди них нет какого-то «эталонного».

Сам язык C++, поддерживаемый этими компиляторами, практически одинаковый. Компиляторы различаются, например, разными оптимизациями или поддерживаемыми платформами (операционными системами, процессорами и т.д.); кроме того, в разных компиляторах могут быть различные небольшие дополнения к языку, или какие-то редкие фичи могут не поддерживаться, иногда также бывают и более сильные отличия (в первую очередь MSVS известен тем, что у них несколько свое мнение по поводу смысла некоторых конструкций C++, хотя последнее время они вроде двигаются к унификации с другими компиляторами). Но на начальном уровне можно считать, что особой разницы между разными компиляторами нет. Поначалу вы столкнетесь, скорее всего, с GCC и/или MSVS.

Кроме того, у языка C++ есть несколько разных «версий», именуемых «стандартами». Они обозначаются C++XY, где XY — две цифры, обозначающие год, когда был принят этот стандарт. Стандарты существуют следующие: C++98, очень старый стандарт; C++03, который в каком-то смысле является «классическим» C++; C++11, в котором было добавлено много новых фич, некоторые из которых вам будут довольно удобны; C++14, который не особо отличается от C++11, но там тоже есть пара удобных новых вещей; C++17 и наконец C++20, который на данный момент (ноябрь 2020) уже почти готов, но еще не совсем. Основные вещи, которые вам понадобятся поначалу, относятся к C++03, также я буду упоминать (и явно это указывать) фичи, добавленные в C++11.

Понятие стандарта языка в некотором плане ортогонально компиляторам: каждый компилятор обычно поддерживает несколько стандартов (какой-то один — по умолчанию, на остальные надо переключаться указанием определенных параметров при компиляции), и каждый стандарт поддерживается многими компиляторами. (При этом, конечно, разные компиляторы и разные версии компиляторов различаются по тому, какие фичи из каких стандартов они поддерживают.) В принципе, сейчас (2020 г.) практически все компиляторы, которые вы встретите, поддерживают C++11 по умолчанию, более новые версии нередко надо запрашивать явно. Например, в тестирующих системах вы нередко можете выбирать, под каким стандартом вы хотите отправить вашу программу (например, вам могут предлагать варианты «GNU C++/C++11» и «GNU C++/C++14»). Как правило, имеет смысл выбирать наиболее свежий стандарт из доступных, но в целом поначалу вряд ли вам понадобятся фичи из C++17, да и скорее всего из C++14 тоже ничего вам не понадобится (хотя там есть пара удобных вещей). А вот C++11 действительно нужен.

Примечание

Не случайно версии языка называются «стандартами». Существуют официальные документы, которые так и называются «стандарт C++», в которых подробно и формально описан язык C++. Вот, к примеру, черновик текущего стандарта (C++20). Не надо его читать при начальном изучении языка, он написан очень сложно и формально, но знать о существовании такого документа полезно. Это по сути справочник даже не для программистов, пишущих на C++ (хотя и для них тоже), а для программистов, пишущих сами компиляторы C++. Именно за счет существования стандарта C++ достигается такое единообразие в поведении разных компиляторов.

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

Собственно, версии стандарта (C++98, C++03, C++11 и т.д.) — это как раз разные версии этого текста, официально утвержденные Международной организацей по стандартизации, ISO (которая утверждает стандарты на что угодно, начиная от форматов бумаги, например, A4, и заканчивая условными обозначениями по уходу за одеждой и тканями).

Соответственно, процесс подготовки нового стандарта состоит в длительных обсуждениях (в том числе, конечно, авторами компиляторов) насчет того, что и как надо добавить или поменять в текущем стандарте, и потом текст финализируется и утверждается ISO. Естественно, это не значит, что все компиляторы сразу будут поддерживать новый стандарт, до полной поддержки может пройти еще несколько лет. Хотя, конечно, многие предложения и изменения в стандарт сначала проходят отработку и тестовую реализацию в существующих компиляторах, и многие компиляторы поддерживают наиболее популярные фичи новых стандартов еще до официального утверждения стандарта.