Сравнение hashmap C/C++ с dict Python (часть 3)
Данная статья продолжит рассмотрение темы отличия hash table C/C++ от dict Python и способов их реализации.
Дополнительные материалы
Для скачивания материалов необходимо войти или зарегистрироваться
Файлы также можно получить в Telegram-боте по коду: 646514
Реклама
Вступление
Всем доброго дня! 24 октября - как я обещал в предыдущей статье Вхожу в IT - telegram bot (часть 1), мы снова вернемся к сравнению hash map С и C++ с dict Python.
В рамках этой статьи я напишу реализацию телефонной книги на C++, а также небольшую историческую справку по C++. Открывайте свои IDE и приготовьтесь продолжить писать код по теме hash map, а также окунуться в мир языка С++.
Для чего мне нужен C++
Моё знакомство с C++ началось с увлечения платами Arduino.
Arduino - это простая плата для создания простеньких DIY проектов. Тактовая частота микроконтроллера слабых Arduino составляет 8 МГц и 16 МГц. Этого хватит чтобы запустить машинку на управлении с телефона, светодиодную ленту с разными режимами свечения и многое другое.
Проекты на данной плате, увы, не могут раскрыть все возможности языка C++, поэтому его возможности для Arduino существенно ограничены. Также мне, как веб-разработчику на Python, может пригодиться использование полноценного С++ для написания более быстрого бэкенда после прототипирования на Python. Это нужно далеко не всегда, но в случаях когда нужна большая скорость и производительность C++ может работать во много раз быстрее Python. Поэтому в своих новых статьях перейду с языка С на более продвинутый C++, на котором можно написать практически что угодно. Я буду углубляться в изучение С++ одновременно с написанием новых статей.
Для меня, как и для многих из вас: я думаю будет много нового. Самое важное из нового в C++ для меня это классы, так как в Arduino я использовал одни лишь функции. Тема "Сравнение классов C++ от классов Python" обязательно будет в одной из будущих статей. Теперь давайте перейдём к исторической справке.
Историческая справка о языке С++
Язык С++ возник в начале 1980-х годов, когда сотрудник компании Bell Labshttps Бьёрн Страуструп решил усовершенствовать обычный язык C. Бьёрн решил добавить к языку C возможность работы с классами, объектами и многое другое.
Его первоначальное название было C with classes (С с классами). В последующем язык развивался дальше и появлялись его новые версии. Название C++ появилось в 1983 году, когда язык стал менее похож на C. Последняя реализация C++ вышла в 2024 году - стандарт C++26. Язык стал более удобным для разработки большого программного обеспечения и неожиданно стал более популярным чем язык C. Давайте разберёмся в его преимуществах и причинах такой популярности.
Поддержка ООП позволяет лучше структурировать код в виде классов и объектов. Для больших проектов это улучшает модульность и переиспользование кода:
- Абстракция данных позволяет создавать пользовательские типы данных, что упрощает работу со сложными структурами данных.
- Инкапсуляция помогает скрывать информацию внутри классов и защищать от доступа из вне.
- Наследование помогает наследовать атрибуты и методы класса родителя классом потомков для их использования.
- Полиморфизм позволяет разным сущностям выполнять одни и те же действия.
Пример из реальной жизни: птицы и пчёлы умеют летать. - Более эффективная обработка ошибок.
- Более продвинутые механизмы управления памятью, которые включают умные указатели, что снижает риск утечек памяти.
- Перегрузка функций и операторов позволяет использовать одно и то же имя для схожих операций над разными типами данных.
А теперь давайте вернёмся к теме hash map и нашей телефонной книге. В прошлых статьях Сравнение hash map С/C++ с dict Python (часть 1) и Сравнение hash map С и C++ с dict Python (часть 2) я реализовал две версии телефонной книги на языках Python и C. Сейчас давайте попробуем реализовать похожий функционал на языке C++.
#include <iostream>
#include <unordered_map>
#include <string>
#include <sstream>
// Переменная для хранения map телефонной книги
std::unordered_map<std::string, std::string> phoneBook = {
{"Николай Николаев", "+72423535354"},
{"Виталий Краснов", "84424434345"}
};
// Функция для получения названия контакта (фи)
std::string getTitle(const std::string& firstName, const std::string& lastName){
std::ostringstream oss;
oss << firstName << " " << lastName;
std::string newTitle = oss.str();
return newTitle;
}
// Функция для добавления контакта
void addContact(const std::string& title, const std::string& phoneNumber) {
phoneBook[title] = phoneNumber;
}
// Функция для получения номера по имени
std::string getNumber(const std::string& title) {
auto number = phoneBook.find(title);
if (number != phoneBook.end()) {
return number->second;
}
return "Контакт не найден!";
}
// Функция для вывода всех контактов
void getAllContacts() {
size_t count = phoneBook.size();
if (count <= 0) {
std::cout << "Hash map пуста!" << std::endl;
}
else {
for (const auto& contact : phoneBook) {
std::cout << contact.first << " - " << contact.second << std::endl;
}
}
}
// Функция для удаления контакта
void delContact(const std::string& title) {
phoneBook.erase(title);
}
// Функция для удаления всех контактов
void delAllContacts() {
phoneBook.clear();
}
// Главная функция
int main() {
// Добавляем контакт
std::string newTitle = getTitle("Сергей", "Сергеев");
addContact(newTitle, "83201345024");
// Получаем количество контактов
size_t count = phoneBook.size();
std::cout << "Количество контактов: " << count << std::endl;
std::cout << "--------------------------------" << std::endl;
// Получаем номер по имени и фамилии
std::string title = getTitle("Николай", "Николаев");
std::cout << getNumber(title) << std::endl;
std::cout << "--------------------------------" << std::endl;
// Выводим все контакты
getAllContacts();
std::cout << "--------------------------------" << std::endl;
// Удаляем контакт
title = getTitle("Виталий", "Краснов");
delContact(title);
// Выводим все контакты (для проверки после удаления)
getAllContacts();
std::cout << "--------------------------------" << std::endl;
// Удаляем все контакты
delAllContacts();
// Выводим все контакты (когда hash map пуста)
getAllContacts();
return 0;
}
Разберёмся, что за код мы написали и как он устроен. Также посмотрим где С++ похож на дедушку C, а где он существенно отличается от него. Давайте начнём с верхних строк с заголовочными файлами.
Заголовочный файл <iostream>
из стандартной библиотеки отвечает за функциональность ввода-вывода на экран.
Следом идёт <unordered_map>
- он отвечает за работу hash map.
Далее идёт <string>
, отвечающий за работу со строками.
Последним идёт <sstream>
, который отвечает за работу строк с потоками ввода-вывода. Абстрактным языком, поток - это последовательность символов, к которым можно получить доступ. Если сравнить с C языком синтаксически, то работа с заголовочными файлами не слишком сильно изменилась. Взгляните на пример из языка С #include <string.h>
и увидите что синтаксис почти тот же самый. Данная синтаксическая схожесть внешне очень обманчива, так как внутри язык С++ был довольно сильно дополнен новым. Главным отличием является то что в языке С++ появляется возможность работать с классами и их объектами, в то время как в простом С языке мы имеем дело с функциями.
Заголовочный файл <string.h>
предоставляет функции для работы со строками, в то время как <string>
предоставляет методы класса. При этом вы можете использовать <string.h>
в C++, так как С совместим с C++. Использовать <string>
в C у вас не получится, так как он не умеет работать с классами в принципе.
Далее у нас идёт переменная для хранения телефонной книги. Строка std::unordered_map<std::string, std::string> phoneBook
создаёт hash map для телефонной книги. Строка std::unordered_map
это наш hash map, который хранит пары ключ и значение. Тут <std::string, std::string>
мы определяем типы данных для ключа и значения. В данном случае оба значения имеют тип string
строка. Символ <
открывает определение шаблона, а символ >
закрывает.
Простым языком шаблон в C++ позволяет создавать функции и классы с параметрами. В конце строки идёт название переменной phoneBook
, которая записана в стиле Camel case. Этот же стиль используется в языке JS, но не используется в Python. В нём используется Snake case для имён переменных и там бы мы написали phone_book
.
Что же означает std::
? Это значит, что мы используем разрешение области видимости. Если разобрать строку std::unordered_map
полностью, то получится, что unordered_map
является частью пространства имён std
, в которых стандартные классы и функции. Также это позволяет избежать конфликтов имён. Далее мы создаём данные для {"Николай Николаев", "+72423535354"}
ключа и значения. В телефонной книге на Python я тоже создал несколько данных сразу прямо в dict 'Николай Николаев': '+72423535354'
.
Спустимся ниже к функции для получения строки контакта по фамилии и имени. Тут std::string getTitle(const std::string& firstName, const std::string& lastName){
мы создаём функцию, которая вернёт нам строку фи. Она принимает два аргумента в виде строк firstName
имя и lastName
фамилию. Для понимания &
в std::string& firstName
помогает создать ссылку на объект. Если написать без символа &
, то будут созданы копии. Создание копий может быть затратным по времени и памяти, особенно если речь идёт о больших объектах. В языке Python любая переменная является ссылкой на объект, а если присвоить одну переменную другой, то они обе будут ссылаться на один и тот же объект. Далее в std::ostringstream oss;
мы создаём объект oss
для класса std::ostringstream
, который мы будем использовать для создания строки с использованием потокового вывода. Далее мы собираем фи в одну строку в oss << firstName << " " << lastName;
с помощью объекта потока. По сути мы в поток записываем фи и пробел. После этого мы получаем строку в std::string newTitle = oss.str();
с помощью метода str()
объекта oss
. Ну и в конце функции return newTitle;
мы возвращаем строку фи.
Перейдём к следующей функции addContact()
, которая нужна для добавления нового контакта в телефонную книгу. В объявлении функции void addContact(const std::string& title, const std::string& phoneNumber) {
видим, что она имеет тип void
. Этот тип используется, когда функция ничего не возвращает, как и в языке C. Функция имеет два параметра title
для фи контакта и phoneNumber
для номера телефона. Далее в строке phoneBook[title] = phoneNumber;
мы присваиваем значение по ключу. В языке Python это выглядит похожим образом phone_book[contact] = number
. В языке C всё выглядело гораздо массивнее с использованием библиотеки hashmap.h
. Более подробно об этом можно почитать в первой статье Сравнение hash map С/C++ с dict Python (часть 1). Там используется встроенная функция hashmap_set
библиотеки hashmap.h
. Вернёмся к нашему коду на C++.
Следующей идёт функция getNumber
, которая получает телефонный номер по ключу фи в виде строки. Она имеет параметр title
, который принимает аргумент, представляющий собой строку с фи контакта. Тут у нас auto contact = phoneBook.find(title);
выполняется поиск по ключу в hash map телефонной книги. Ключевое слово auto
нужно для автоматического определения типа данных, что может быть полезно в случаях когда в hash map присутствуют разные типы данных. Я поставил auto
не просто так. А для чего я это сделал - узнаете в следующих статьях =)
В следующей строке if (contact != phoneBook.end()) {
у нас идёт проверка условия "найден ли наш контакт в hash map". Метод end()
используется для возврата итератора, который является некой маркировкой конца hash map. Иными словами это нечто, что идёт после последнего элемента. Синтаксисом это довольно сильно отличается от реализации на Python if contact in phone_book.keys():
, где мы просто проверяли оператором if
есть ли фи в list
с ключами из dict
. Далее мы возвращаем значение в строке return contact->second;
- наш телефонный номер. Для получения ключа мы бы использовали return contact->first;
. В случае если мы получаем маркер конца, то возвращаем строку return "Контакт не найден!";
, которая нам говорит о том, что мы не нашли контакт по фи.
Далее у нас идёт функция getAllContacts()
для вывода на экран всех контактов из телефонной книги. В строке size_t count = phoneBook.size();
мы смотрим размер книги. Если размер равен нулю if (count <= 0) {
, то выводим на экран сообщение std::cout << "Hash map пуста!" << std::endl;
что наш hash map пуст. Давайте разберём данную строку и узнаем как работает код. Строку std::cout
мы используем для работы с потоком вывода. Оператор <<
передаёт строку "Hash map пуста!"
в поток вывода. В конце << std::endl
выполняется вставка символа новой строки и сброс буфера вывода. Далее, если у нас телефонная книга не пуста (в данном случае у нас просто else
условие), мы проходим по книге в цикле for
для вывода каждого контакта. Разберём синтаксис цикла for (const auto& contact : phoneBook) {
поподробнее. Сначала мы создаём константу с автоматическим типом данных const auto& contact
, ссылкой на которую выступает переменная contact
. Далее у нас идёт запись : phoneBook
двоеточие в которой играет роль разделителя переменной для итерации от переменной для hash map, по которому проходит итерация. Она играет такую же роль как in
в Python, когда мы проходим по dict или другому итерируемому объекту. Для меня двоеточие выглядит довольно непривычно в этой роли. В конце у нас идёт строка вывода контакта на экран std::cout << contact.first << " - " << contact.second << std::endl;
тут есть важный синтаксический момент: к ключу мы обращаемся contact.first
(фи), а к значению обращаемся contact.second
. В языке Python мы просто проходили по phone_book.items()
в цикле for
, который представляет из себя объект, визуально похожий на список кортежей, где первый элемент ключ, а второй значение.
Следом идёт пара простых функций delContact()
удаляет контакт и delAllContacts()
очищает телефонную книгу. Первая функция принимает фи в качестве аргумента и, используя метод erase()
, удаляет пару ключ значение из hash map. Вторая функция очищает телефонную книгу с помощью метода clear()
. Далее идёт главная функция main()
, в которой мы вызываем каждую функцию, передаём аргументы и получаем результат их работы. Вывод строки "--------------------------------"
играет роль разделителя между результатами выполнения функций. В конце главной функции мы возвращаем return 0;
как и в языке C. Нужно ли возвращать 0 в С++? Главная функция всегда вернёт 0 при успешной работе программы даже если вы не пропишите это руками. Но лично я люблю явный подход, поэтому всегда пишу руками возврат 0. Поэтому функция main()
должна иметь тип int
. Код мы написали и теперь нужно проверить его работу.
Но, прежде чем мы проверим работу кода нам нужно проверить наш компилятор. Давайте посмотрим нашу версию компилятора и проверим какие стандарты C++ он поддерживает. Откройте terminal и пропишите следующую команду:
g++ --version
Вывод в terminal
g++ (Debian 12.2.0-14) 12.2.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Версия моего компилятора 12.2.0, которая была создана в 2022 году. Скорее всего, у вас версия будет отличаться от моей. Можно проверить, какие стандарты поддерживает компилятор моей версии. Попробуем три современных стандарта - 20, 23 и 26.
g++ --std=c++20 -E - </dev/null
Необходимо ввести три команды для каждого стандарта. Просто меняйте число, которое отвечает за номер стандарта C++. Разберем команду что мы ввели - g++ --std=c++20 -E - </dev/null
. Тут g++ --std=c++20
мы используем компилятор и задаём ему флагом стандарт, с которым он будет работать. Тут -E - </dev/null
мы просим компилятор выполнить только preprocessing - обработку исходного кода без всяких компиляций в файл, и задаём что у нас нет исходного кода для компиляции. Посмотрим, что выдаст компилятор в terminal.
‘-std=c++20’ is valid for C++/ObjC++
‘-std=c++23’ is valid for C++/ObjC++
g++: error: unrecognized command-line option ‘--std=c++26’; did you mean ‘--std=c++20’?
Вывод терминала будет гораздо объёмнее, так что я показал лишь те строки, которые нам нужны. Из строки ‘-std=c++20’ is valid for C++/ObjC++
следует, что стандарт 20 для С++ поддерживается нашим компилятором. Со стандартом 23 то же самое. Третья строка g++: error: unrecognized command-line option ‘--std=c++26’
показывает, что 26 стандарт не поддерживается. Если мы перейдём на страницу с релизами релизы gcc, то увидим что моя версия 12.2 появилась в 2022 году, а самая последняя 14.2 не так и давно - в текущем 2024 году. Вам придётся обновить компилятор чтобы использовать самые новые стандарты. Я пока остановлюсь на 23 стандарте. В будущих статьях я, скорее всего, обновлю компилятор и проверю работу новейшего стандарта. Пусть вас не смущает ссылка ибо оба компилятора g++
и gcc
являются частью GNU Compiler Collection (GCC).
Также я рекомендую установить библиотеку для работы со строками, которая введёт больше удобства для работы с С++ и позволит использовать удобное форматирование строк, похожее на Python и многое другое.
sudo apt-get install libfmt-dev
Теперь давайте скомпилируем и запустим код через эмулятор terminal в Linux.
g++ -o phone_book_cpp phone_book.cpp
./phone_book_cpp
Обратите внимание, что мы используем компилятор g++
для языка C++. В остальном команда такая же как и была для языка C.
Количество контактов: 3
--------------------------------
+72423535354
--------------------------------
Сергей Сергеев - 83201345024
Виталий Краснов - 84424434345
Николай Николаев - +72423535354
--------------------------------
Сергей Сергеев - 83201345024
Николай Николаев - +72423535354
--------------------------------
Hash map пуста!
Мы видим, что наша программа отработала успешно и показала нам в точности такой же вывод как и в двух других версиях телефонной книги. Поздравляю, вот мы и закончили предварительные версии телефонных книг на трёх разных языках!
Анонс на следующие статьи
Для удобства я решил выделить информацию о следующих статьях в отдельный раздел. Статьи выходят раз в три недели. Следующая статья "Вхожу в IT - telegram bot (часть 2)" выйдет 14.11.24, в которой я напишу про первую версию бота продакт уровня на языке Python с библиотекой Aiogram3.
Следующая часть статьи по теме сравнения Python и C выйдет уже, получается, в декабре. Я продолжу развивать код телефонных книг, но уже только на двух языках - Python и C++, потому что C++ более современный в сравнении с C. Также C не поддерживает работу с классами, а я буду переходить на тему программирования на классах.
В последующих статьях я коснусь темы улучшения кода и обработки ошибок на языках Python и C++. Дальше будет ещё интереснее :)
Заключение
- Поговорили о том, для чего мне нужен C++;
- Узнали об истории C++;
- Написали основу телефонной книги на C++ с использованием hash map;
- Посмотрели стандарты языка C++ на своём ПК;
- Увидели анонс будущих статей;
Все статьи