Курс программирования на C++ для новичков и не только |
![]() |
Добавил(а) microsin | ||||||||||||||||
Прослушал курс программирования [1]. На мой взгляд, очень полезный курс, особенно для новичков. Лекции короткие и очень понятные. Конечно, многое из того, что рассказано в лекциях, я уже и так знал, но все равно прослушал курс с удовольствием. Особенно много полезного для себя узнал в уроках, посвященных лямбда-функциям и шаблонам функций. Самым мутными, на мой взгляд, являются темы наследования (inheritance), перезагрузки (overloading, overriding, hiding), полиморфизма, урезания копии дочернего класса (slicing), абстрактные классы, виртуальные функции, лямбда-функции, шаблоны функции, вопросы доступа к элементам класса при наследовании. Слово "мутный" я применил в том смысле, что вряд ли для меня есть смысл практического использования всех этих фич для работы, потому что их применение ИМХО только запутает код и его понимание. Однако ЗНАТЬ о всех этих штуках и иметь о них хотя бы общее представление в настоящее время НЕОБХОДИМО. Здесь выписал для себя основные тезисы, чего раньше не знал, или знал но забыл, или то, что может оказаться интересным и полезным. 1. Компиляторы, которые нужно установить в Linux: GCC, Clang llvm. Установка на Ubuntu: $ sudo apt install gcc $ sudo apt install clang Команды g++ --version и clang++ --version покажут версии компиляторов GCC и Clang: $ g++ --version g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 Copyright (C) 2021 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. $ clang++ --version Ubuntu clang version 14.0.0-1ubuntu1.1 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin Команда gdb --version покажет версию отладчика: $ gdb --version GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1 Copyright (C) 2022 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later < http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Последнюю версию компилятора gcc также можно установить с помощью команды brew [9]. 2. Фичи, поддерживаемые различными компиляторами, можно посмотреть на страничке [2]. 3. Visual Studio Code (VSCode) - удобная и популярная среда разработки. Установка под Ubuntu: $ sudo apt install code 4. Установка расширений для VSCode доступна на левой панели, по кнопке Extensions (Ctrl+Shift+X): Расширения, которые стоит установить, если они еще не установлены: C/C++ for Visual Studio Code Удобный способ запустить VSCode в текущей папке: $ code . Полную инструкцию на все случаи жизни можно посмотреть на страничке [3]. 5. Как скачать уроки: - Перейдите в папку, где вы хотели бы сохранить уроки. Например, ~/MyProjects. После этого в папке ~/MyProjects появится папка The-C-20-Masterclass-Source-Code с файлами уроков. 02.EnvironmentSetup Настройка среды разработки в среде Windows и Linux для компиляторов GCC, MSVC, Clang. Готовые шаблоны задач для среды разработки VScode (task.json). 03.FirstSteps 3.2FirstCppProgram: что такое #include < iostream>, std::endl, простейший пример программы вывода в консоль. 04.VariablesAndDatatypes 4.2NumberSystems: представление чисел в программе в различных системах счисления: двоичная, десятичная, восьмеричная, шестнадцатеричная, символьная. 05.OperationsOnData 5.2.BasicOperations: обзор базовых математических операций. 06.LiteralsAndConstants 07.ConversionsOverflowAndUnderflow 08.BitwiseOperators 09.VariableLifetimeAndScope 10.FlowControl, управление потоком вычислений. 10.2IfStatements: операторы условного ветвления потока выполнения. 11.Loops, циклы. 11.2ForLoop: цикл for. 12.Arrays, массивы. 12.2DeclaringAndUsingArrays: декларирование и использование массивов. 13.Pointers, указатели. 13.2DeclaringAndUsingPointers: декларирование и использование указателей. 14.References, работа со ссылками. 14.2.DeclaringAndUsingReferences: примеры декларирования и использования ссылок. 15.CharacterManipulationAndStrings, строки, операции над строками. 15.2CharacterManipulation: проверки символов на регистр, является ли они алфавитно-цифровыми, подсчет пробелов в строке и т. п. 16.Functions, работа с функциями. 16.2FirstHandOnCppFunctions: первое знакомство с функциями C++. 17.EnumsAndTypeAliases 18.ArgumentsToTheMainFunction 19.GettingThingsOutOfFuntions, методы работы с функциями. 19.2InputAndOutputParameters: способы вывода данных из функции с помощью ссылок и указателей. 20.FunctionOverloading, обзор перегрузки функции. 20.2OverloadingWithDifferentParameters: демонстрация перегрузки функции путем различий в параметрах. 21.LambdaFunctions, знакомство с лямбда-функциями. 21.2DeclaringAndUsingLambdas: простые примеры декларирования и использования лямбда-функций. 22.FunctionsTheMisfits 23.FunctionCallStackD_ebugging 24.FunctionTemplates, введение в шаблоны функции. 24.2TryingOutFunctionTemplates: первое знакомство с шаблоном функции. В уроке обсуждаются проблемы, связанные с несоответствием типов передаваемых переменных типам шаблона, а также случаям не поддерживаемых в теле шаблона операторов для переданных в шаблон типов параметров. Показана отладка кода шаблона в среде VSCode. 25.Concepts, введение в концепции шаблона функции. 25.02UsingConcepts: использование концепций. 26.Classes, знакомство с классами. 26.2YourFirstClass: ваш первый класс на C++. 27.ZoomingInOnClassObjects 28.DivingDeepIntoConstructorsAndInitialization 29.Friends 30.ConstAndStaticMembers 31.Namespaces 32.ProgramsWithMultipleFiles 32.5OneDefinitionRule: объяснение работы "правила одного определения". 33.SmartPointers 34.OperatorOverloading 35.LogicalOperatorsAndThreeWayComparison 36.Inheritance, наследование. 36.2FirstTryOnInheritance: первое знакомство с наследованием. 37.Polymorphism, объяснение полиморфизма. 37.2StaticBindingWithInheritance: демонстрация статической привязки одноименных методов к производному классу в полиморфизме. 38.Exceptions 39.Practice-BoxContainerType 40.ClassTemplates 41.MoveSemantics 42.FunctionLikeEntities 43.StlContainersAndIterators 44.ZoomingOnSTLContainers 45.StlAlgorithms 46.RangesLibraryInCpp20 47.BuildingIteratorsForCustomContainers 48.Coroutines 49.Modules 6. Интеграция компилятора в среду разработки VSCode. В папках, которые были скачаны на шаге 5, находится шаблон проекта, на основе которого будут создаваться все проекты уроков (обратите внимание, что пробелы в пути папки экранируются символом обратного слеша \): ~/The-C-20-Masterclass-Source-Code/02.EnvironmentSetup/1.Windows/8.C++20\ Template\ Project_all_compilers/ Например, чтобы начать разрабатывать новый урок (проект программы), сделайте копию папки "8.C++20 Template Project_all_compilers" под новым именем. После этого кликните правой кнопкой на копию этой папки, и выберите "Открыть с помощью -> Visual Studio Code". Запустится среда разработки VSCode, где в дереве просмотра проекта (EXPLORER) будут видны файлы шаблона c_cpp_properties.json, main.cpp и tasks.json. После этого изменяйте код main.cpp и добавляйте новые модули исходного кода. 1. Создайте пустую папку для проекта, дайте ей понятное имя, характеризующее выполняемую задачу. Для примера назовем папку MyFirstCppProgram. 2. Кликните правой кнопкой на папке MyFirstCppProgram, и выберите "Открыть с помощью -> Visual Studio Code". Запустится среда VSCode, и откроется окно Welcome. 3. Создайте новый файл main.cpp (меню File -> New File... -> введите имя main.cpp). В редакторе файла main.cpp введите следующий текст: #include < iostream> В этом коде мы проверяем, что реально поддерживаются фичи C++ версии 2.0. Для этого применяется оператор '< =>' [11], который не поддерживается старыми вариантами стандарта C++. Для его успешной компиляции необходимо применить опцию компиляции -std=c++20. Например, если запустить компиляцию командой g++ main.cpp, то произойдет ошибка: $ g++ main.cpp main.cpp: In function ‘int main()’: main.cpp:5:25: error: expected primary-expression before ‘>’ token 5 | auto result = (10 < => 20) > 0; Но если добавить опцию -std=c++20, то компиляция завершится успешно: $ g++ -std=c++20 main.cpp Получится исполняемый файл a.out, который можно запустить и получить ожидаемый результат 0: $ ./a.out 0 Примечание: запускать команды для компиляции можно в отдельном окне терминала, открыв текущую папку проекта командами cd, либо можно открыть панель терминала прямо у среде VSCode выбором в меню Terminal -> New Terminal: В предыдущей врезке мы рассмотрели пример простейшей программы и её компиляции с помощью прямого вызова в командной строке компилятора g++. Однако можно настроить и автоматизировать этот процесс, сделав его более удобным, с помощью задач VSCode (Tasks). 1. Выберите в меню Terminal -> Configure Tasks..., среда VSCode найдет компиляторы, которые были установлены на шаге "Компиляторы, которые нужно установить в Linux: GCC, Clang llvm". Чтобы создать задачу для компиляции текущего файла компилятором GCC (g++), выберите в выпавшем списке пункт "C/C++: g++ build active file". В папке проекта будет автоматически создана папка для настроек .vscode, и в ней будет создан файл настроек для задач tasks.json. { "version": "2.0.0", "tasks": [ { "type": "cppbuild", "label": "C/C++: g++ build active file", "command": "/usr/bin/g++", "args": [ "-fdiagnostics-color=always", "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": [ "$gcc" ], "group": "build", "detail": "compiler: /usr/bin/g++" } ] 2. Этот файл настроек можно и нужно поправить, чтобы он стал более универсальным. label. Пункт настроек label задает текстовую метку, которая будет отображаться в меню задач Terminal -> Run Task..., её можно поменять на любой текст по вашему усмотрению. Например, укажем в метке, что у нас используется стандарт C++ v20: "label": "C/C++: g++ build std=c++20", args. Этот пункт настроек задает опции, передаваемые компилятору в его командной строке. Здесь имеет смысл поменять опцию "${file}", которая будет компилировать только текущий файл, и надо будет также добавить опцию -std=c++20. Также можно поменять название результата компиляции, если изменить опцию "${fileDirname}/${fileBasenameNoExtension}". Сделанные изменения выделены жирным шрифтом: "args": [ "-fdiagnostics-color=always", "-g", "${workspaceFolder}/*.cpp", "-std=c++20", "-o", "${fileDirname}/myprogram" ], После внесенных изменений будут компилироваться все модули исходного кода, находящиеся в папке проекта, не только текущий открытый файл ("${workspaceFolder}/*.cpp"), будут поддерживаться фичи C++ 20 ("-std=c++20"), и поменяется имя результата компиляции на myprogram. Выполните все те же самые шаги, что были сделаны в предыдущей врезке для компилятора g++, только в меню создания новой задачи Terminal -> Configure Tasks... выберите в выпавшем списке пункт "C/C++: clang++ build active file". В результате файл настроек задач tasks.json примет примерно такой вид (внесенные изменения выделены жирным шрифтом): { "version": "2.0.0", "tasks": [ { "type": "cppbuild", "label": "C/C++: g++ build std=c++20", "command": "/usr/bin/g++", "args": [ "-fdiagnostics-color=always", "-g", "${workspaceFolder}/*.cpp", "-std=c++20", "-o", "${fileDirname}/myprogram" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": [ "$gcc" ], "group": "build", "detail": "compiler: /usr/bin/g++" }, { "type": "cppbuild", "label": "C/C++: clang++ build std=c++20", "command": "/usr/bin/clang++", "args": [ "-fcolor-diagnostics", "-fansi-escape-codes", "-g", "${workspaceFolder}/*.cpp", "-std=c++20", "-o", "${fileDirname}/myprogram" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": [ "$gcc" ], "group": "build", "detail": "compiler: /usr/bin/clang++" } ] На Windows интеграция компиляторов g++.exe и clang++.exe аналогична тому, как это делается на Linux (см. предыдущие две врезки), есть только незначительные отличия в стиле указания пути к файлу (разделитель пути / заменяется на \\). Готовый файл настроек tasks.json для Windows можно найти в папке "8.C++20 Template Project_all_compilers" среди скачанных уроков [2] (см. выше "Как скачать уроки"). Вот его содержимое: { "version": "2.0.0", "tasks": [ { "type": "cppbuild", "label": "Build GCC", "command": "C:\\mingw64\\bin\\g++.exe", "args": [ "-g", "-std=c++20", "${workspaceFolder}\\*.cpp", "-o", "${fileDirname}\\rooster.exe" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": [ "$gcc" ], "group": "build", "detail": "compiler: C:\\mingw64\\bin\\g++.exe" }, { "type": "cppbuild", "label": "Build with MSVC", "command": "cl.exe", "args": [ "/Zi", "/std:c++latest", "/EHsc", "/Fe:", "${fileDirname}\\rooster.exe", "${workspaceFolder}\\*.cpp" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": [ "$msCompile" ], "group": "build", "detail": "compiler: cl.exe" }, { "type": "cppbuild", "label": "Build with Clang", "command": "C:\\mingw64\\bin\\clang++.exe", "args": [ "-g", "-std=c++20", "${workspaceFolder}\\*.cpp", "-o", "${fileDirname}\\rooster.exe" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": [ "$gcc" ], "group": "build", "detail": "compiler: C:\\mingw64\\bin\\clang++.exe" } ] Этот файл можно взять как базовый пример для настроек, нужно в нем просто поправить абсолютные пути до компиляторов "command". Существует несколько популярных онлайн компиляторов: • OnlineGDB Они могут быть полезны для быстрого тестирования каких-нибудь алгоритмов, когда нет возможности воспользоваться полноценной оффлайн-средой разработки. 7. Оператор std::endl означает то же самое, что и '\n', обе означают символ новой строки. Например, следующие две строки эквивалентны: std::cout << "Number1" << std::endl; std::cout << "Number1" << '\n'; Следующие фичи требуют опции компилятора -std=c++20 (см. выше описание файла настроек tasks.json для VSCode): 1. Оператор < =>, см. [11]. 2. Спецификатор consteval (см. consteval specifier site:cppreference.com). Он декларирует функцию или шаблон функции, чтобы она была "непосредственной" (immediate function). Т. е. каждый потенциальный вызов этой функции должен (напрямую или косвенно) генерировать во время компиляции константное выражение. Непосредственная (immediate) функция это constexpr функция, с учетом её требований и в зависимости от обстоятельств. Так же, как и спецификатор constexpr, спецификатор consteval подразумевает inline. Однако consteval не может быть применен к деструкторам, функциям выделения или освобождения (allocation, deallocation functions). Объявление функции или шаблона функции, указывающее consteval, может также не указывать constexpr, и любые повторные объявления этой функции или шаблона функции также должны указывать consteval. Потенциально вычисляемый вызов immediate-функции, самая внутренняя не блочная область действия которой не является областью действия параметра функции immediate-функции или истинной ветвью оператора consteval if (начиная от C++ 23), должен создавать константное выражение; такой вызов известен как немедленный вызов (immediate invocation). 8. Код возврата из функции main. Общепринятое устоявшееся соглашение для программ, как в Linux, так и в Windows, состоит в том, что возвращаемое значение 0 из функции main означает успешное завершение программы. Любое другое возвращенное значение означает ошибку, и оно различные определенные значения могут кодировать тип этой ошибки. 9. Инициализированные переменные можно создавать разными способами: Braced Initialization, инициализация с помощью фигурных скобок {}. Примеры: int myValue1 = 7; // инициализация переменной значением 7 В инициализаторе можно использовать выражение: int A = 2; Инициализации со скобками () и {} имеют особенности, заключающиеся в неявном преобразовании данных. Например: // Следующее объявление создаст переменную narrowing_conversion_functional, 10. Для вывода в консоль используется std::cout, а для ввода std::cin. Также существуют потоки std::cerr для вывода в консоль сообщений об ошибках и std::clog для вывода в консоль сообщений лога. 11. Немного основной терминологии. Core features. Это основные фичи, к которым относятся правила синтаксиса языка C++, базовые типы и операторы. Standard library. Набор готовых к использованию специализированных компонентов, которые можно использовать в программе на C++. Стандартные библиотечные функции подключаются к программе операторами наподобие #include < iostream>, #include < string>. STL. Standard Template Library, часть стандартной библиотеки C++, но представляет собой набор типов контейнеров. Их можно представить себе как наборы каких-то сущностей (объектов), из которых можно составлять коллекции. Существуют специальные итераторы, которые могут просматривать эти коллекции сущностей, с выполнением задаваемого программистом алгоритма для каждого элемента коллекции. int main() {}. Основная функция, где описывается весь алгоритм программы C++. Entry Point. Точка входа в программу, соответствующая началу тела функции main. Statement. Оператор языка, минимальная единица программы, определяющая какое-то действие. Function. Функция, объединенный набор действий в программе. Функция на входе может принимать входные данные (параметры функции) определенных типов, и на выходе может возвращать какое-либо значение определенного типа. Error, Warning. Ошибка, предупреждение. Разделяют ошибки времени компиляции (compile time error, сообщение об этой ошибке выводит компилятор, если он обнаружит неправильный синтаксис) и ошибки времени выполнения (runtime error, эту ошибку компилятор не заметит, но она может привести к неправильной работе программы или даже к зависанию или аварийному завершению программы). Также бывают предупреждения компилятора (warning), когда компилятор обнаружил подозрительное место, где возможно содержится ошибка. Предупреждения обычно не приводят к ошибке компиляции, и программа все равно будет успешно скомпилирована. Input, Output. Ввод и вывод - система, с помощью которой программа может общаться с внешним миром (пользователем программы). Comment. Комментарий: поясняющий текст в программе, который не компилируется в двоичный код. Dev Workflow. Рабочий процесс разработки программы. Memory Model. Модель организации памяти, используемый в программе. Execution Model. Модель выполнения кода программы. Base Types. Предопределенные базовые типы данных, например int, double, float, char, bool, void, auto и т. д. 12. Представление чисел в различных системах счисления и формах: int number1 = 15; // Decimal (десятичная) int number2 = 017; // Octal (восьмеричная) int number3 = 0x0F; // Hexadecimal (шестнадцатеричная) int number4 = 0b00001111; // Binary (двоичная) char number5 = 'A'; // Symbolic (символьная) 13. Числа с плавающей точкой:
// Примеры декларации и инициализации переменных плавающей точки. // В примере типа float из-за точности 7 не будут сохранены все // значащие цифры инициализатора: float number1 {1.12345678901234567890f}; // Precision : 7 double number2 {1.12345678901234567890}; // Precision : 15 long double number3 {1.12345678901234567890L}; Обратите внимание на суффиксы f и L в конце значений инициализаторов. Если они указаны, то будут явно определять точность чисел с плавающей запятой для указанных констант. Имейте в виду, что если суффикс не указан, то подразумевается double. Управление отображением максимальной точности при выводе чисел с плавающей точкой (определено в заголовочном файле < iomanip>): std::cout << std::setprecision(20); Числа с плавающей запятой можно делить на 0, в отличие от целых чисел. Например: n (число с плавающей точкой) / 0 = Infinity(+/-) (бесконечность) 0.0 / 0.0 = NaN (Not A Number, не число. Число с плавающей точкой без возможности представления) 14. Значения типа bool занимают в памяти 8 бит. По умолчанию при выводе на печать значение true будет выведено как 1, false как 0. Это можно поменять с помощью оператора std::boolalpha, тогда при выводе на печать они будут выводиться как true и false. 15. На языке C явное преобразование типа производится с помощью префикса в круглых скобках (тип). На C++ для той же цели принято использовать оператор static_cast< тип>, хотя вариант языка C тоже работает. Например: char chA = 'A'; 16. Тип auto. При инициализации переменной типа auto компилятор сам задаст подходящий встроенный тип для переменной, ориентируясь на значение инициализатора. 17. Форматирование вывода std::cout. В стандартной библиотеке C++ (#include < ios>, #include < iomanip>) существуют богатые возможности по управлению выводом информации. Вот их список (неполный) с кратким описанием: std::endl Конец строки. std::flush Сброс накопленного символьного буфера на систему вывода (экран терминала). std::left, std::right, std::internal Управление позиционированием с символами заполнения [12]. std::boolalpha Активация текстового представления булевых значений (true, false). std::showpoint, std::noshowpoint Управление разрешением или запретом безусловного включения десятичной точки при выводе чисел с плавающей запятой. Работает только для вывода. std::setfill() Установит символ заполнения для потока вывода [12]. std::setw() Установит ширину вывода при заполнении для потока вывода. std::dec, std::hex, std::oct Модифицирует основание по умолчанию для чисел при вводе/выводе. std::fixed, std::scientific, std::hexfloat, std::defaultfloat Модифицирует форматирование по умолчанию для вывода чисел с плавающей запятой. std::setprecision() Устанавливает количество символов после десятичной точки (точность) при выводе чисел с плавающей точкой. std::showbase, std::noshowbase Управляет, показывать или не показывать при выводе идентификатор базы числа (такой как 0x). std::uppercase, std::nouppercase Управляет переводом в верхний или нижний регистр символов при выводе шестнадцатеричного или научного формата чисел. std::showpos, std::noshowpos Управляет, показывать или не показывать знак плюса при выводе чисел. 18. Математические функции. Подключаются строчкой #include < cmath>. std::floor(), std::floorf(), std::floorl() Округление числа вниз (отбрасыванием дробной части). std::ceil(), std::ceilf(), std::ceill() Округление числа вверх. std::abs() Абсолютное значение числа. std::sin(), std::cos(), std::tan() Тригонометрические функции: синус, косинус, тангенс. std::exp() Экспонента. std::log() Логарифм. std::pow() Возведение в степень. std::sqrt() Квадратный корень. std::round Округление числа. 19. Для целочисленных типов, размер которых меньше 4 байт (char, short int), не поддерживаются аппаратно арифметические операции. Таким образом, когда в программе над ними производятся арифметические вычисления, компилятор автоматически переводит их в целочисленные 4-байтные значения. 20. size_t. Это целочисленный тип без знака, у которого может быть различный размер в зависимости от используемой системы. Например на 64-битных системах его размер составляет 8 байт. 21. std:size(имя_массива). Оператор std:size позволяет запросить размер массива в элементах, не в байтах, во время выполнения. 22. Цикл на основе диапазона: for(auto i: имя_массива) { тело цикла } 23. nullptr. Это аналог NULL на языке C, может использоваться для инициализаторов указателей: int *pNum{nullptr}; // то же самое, что и int *pNum = NULL; 24. Виртуальная память. Каждой запущенной программе операционная система предоставляет виртуальное пространство памяти размером в 2^N байт (с адресами от 0 до (2^N)-1). Здесь N=32 для 32-битных систем, и N=64 для 64-битных систем. Это происходит благодаря программно-аппаратному взаимодействию между процессором (CPU) и блоком управления памятью (Memory Management Unut, MMU), который преобразует физическую память (оперативная память и дисковый файл подкачки) в виртуальную память для каждой программы. 25. Стек и куча. Понятия стека (stack) и куча (heap) относятся к динамической памяти. Динамическая память отличается от статической памяти (оперативная память data и память программ text) тем, во время выполнения программы эта память может появляться и исчезать, т. е. будет выделяться и освобождаться. Однако и у стека и кучи есть отличия в поведении и использовании. В стеке хранится информация о локальных переменных функции и о передаваемых в неё параметрах. Также локальные переменные быть внутри блока из фигурных скобок. Таким образом, использование локальных переменных, выделяемых в стеке, полностью ограничивается областью действия локальных переменных, и выделение и освобождение областей стека происходит автоматически, без прямого участия программиста. При использовании кучи ситуация обратная, здесь именно программист отвечает за выделение (оператор new, вызов конструкторов класса) и освобождение памяти (оператор delete, вызов деструкторов класса). 26. Указатели можно инициализировать с помощью оператора new вот так: int *p1 {new int}; // Выделение в куче памяти под число int // и присваивание p1 адреса этого числа. 27. Пример определения массивов с выделением памяти из кучи: #include < iostream> Важное замечание: массивы, выделенные динамически, сильно отличаются от массивов, определенных статически или локально в стеке. Например по массивам, выделенным в куче, нельзя использовать итерацию по диапазону (for(auto i: имя_массива){}), и для них не работает оператор std::size. // Сравнение динамических массивов с массивами в стеке: std::cout << "=====================================" << std::endl; 28. Определение ссылок на C++ может быть сделано по-другому: // Определение переменных int_data и double_data: int int_data{33}; double double_data{55}; Стоит отметить, что только так же, как и на языке C, ссылки на переменные работают точно так же, как и имена переменных. Т. е. для предыдущего примера &int_data и &ref_int_data дадут один и тот же адрес. 29. Ссылки и указатели. В принципе и ссылки, и указатели делают одно и то же, но все же их поведение и работа с ними отличается. Ссылки (references): - Не используется разименование (*) для чтения и записи по ссылке. Ссылка указывается в коде точно так же, как исходная переменная. Указатели (pointers): - При доступе к значению для чтения и записи по указателю используется разименование (*). Таким образом, код с использованием ссылок более безопасен и прост. Указатели позволяют произвольно манипулировать данными. int age {58}; // переменная int& ref_age{age}; // обычная ссылка на age, ref_age++; // позволяющая её изменять const-ссылка. Ссылку можно определить так, что через неё невозможно будет изменить её переменную: const int& const_ref_age{age}; // Теперь переменную age нельзя поменять через эту ссылку: const_ref_age = 59; // на эту строку компилятор выдаст ошибку // Аналог const-ссылки можно сделать с помощью указателя: const int* const const_ptr_to_const_age{&age}; const_ptr_to_const_age = 59; // на эту строку компилятор выдаст ошибку Обратите внимание, что не бывает const-ссылок на не изменяемую переменную, это не имеет смысла. Поэтому на следующее определение компилятор выдаст ошибку: const int& const weird_ref_age{age}; // ошибка, это определение бессмысленное! 30. На языке C++ можно работать с традиционными строками языка C, т. е. ASCIIZ-строками (массив символов char, заканчивающийся 0). Однако работать с такими строками не очень удобно, поскольку нужно следить за буфером в памяти, где помещается такая строка, т. е. нужно следить за размером строки в памяти. По этой причине в C++ появился еще один тип std::string (подключается #include < string>), который дает более простой интерфейс для работы со строками, чем стандартная библиотека < string.h> языка C. С типом std::string можно абстрагироваться от размеров строк, необходимости завершать их нулем ('\0') и отслеживать границы массива строки. cctype. Основные возможности манипуляции над символами С++ библиотеки < cctype> [14] (соответствуют C-библиотеке < ctype.h>): isalnum. Проверка, является ли символ алфавитно-цифровым. cstring. Также существуют std::-аналоги C-функций < string.h>, определенные в заголовочном файле < cstring>. 31. Примеры декларации строк типа std::string: #include < iostream> 32. One Definition Rule (ODR). Правило одного определения гласит: "определения не могут появляться в программе или единицах трансляции программы больше одного раза". Это относится к глобальным переменным, именам функций. Под "единицей трансляции" подразумевается компилируемый файл исходного кода C++. Исключение из этого правила относится только к классам: определение того или иного класса может содержаться в нескольких единицах трансляции, но в одной единице трансляции не может быть несколько одноименных классов. Причина этого исключения в том, что нам необходимо создавать объекты этих классов, так что каждая единица трансляции должна видеть определение своего класса. Определение своего класса подключается к исходному коду C++ файлами *.h (или *.hpp) с помощью директивы #include. Сигнатура функции. Обратите внимание, что "определение" для функции на языке C++ означает не только имя функции, но также и уникальный по типу набор параметров. Это называют сигнатурой функции. Как ни странно, на C++ возвращаемый тип к сигнатуре функции не относится. Таким образом, в глобальном контексте именно сигнатура функции должна быть уникальной. Т. е. не могут быть два одинаковых определения функции, однако могут быть функции с одинаковыми именами, но с разными сигнатурами: с разными наборами параметров (так называемая перегрузка функции, function overloading). Определение (definition) и декларация (declaration) функции. Иногда более гибким будет разместить заголовок функции (декларацию) в отдельном файле. Такие файлы называют заголовочными (header files), и они получают расширение *.h или *.hpp. Сам код тела функции, т. е. её реализация (определение) будет при этом размещаться в модуле исходного кода, в файле с расширением *.cpp. Декларации функций также могут размещаться и в начале модуля исходного кода, не обязательно в заголовочных файлах. Декларацию функции также иногда называют прототипом функции. 33. Из функции можно выводить данные следующими способами, в порядке убывания предпочтения: - Через возвращаемое значение. При возврате по значению следует иметь в виду, что в современных компиляторах возврат по значению обычно оптимизируется компилятором, когда это возможно, и функция модифицируется за вашей спиной для возврата по ссылке, избегая ненужных копий! 34. Перегрузка функции. В программе C++ можно определить несколько совершенно разных функций с одинаковыми именами, но с разными сигнатурами (т. е. разными наборами параметров). Это называется перегрузка функции. Чтобы перегрузка работала правильно, параметры для каждой перегрузки могут отличаться следующим: - Типами параметров. Пример перегрузки функции max: int max(int a, int b) { std::cout << "int overload called" << std::endl; return (a>b)? a : b; } Компилятор автоматически выберет правильную перегрузку функции в зависимости от типа, количества и порядка следования типов передаваемых параметров. Следует помнить, что тип возвращаемого значения не относится к сигнатуре функции, поэтому нельзя на основе этого определять перегрузку. Компилятор сообщит об ошибке, если вы попытаетесь сделать что-то подобное. 35. Лямбда-функции. Это механизм настройки анонимных функций (безымянных функций). Как только они настроены, мы можем либо дать им имена, чтобы потом вызывать их, либо мы даже можем заставить их делать что-то напрямую, не используя имени. Особенность лямбда-функции в том, что в её теле нет возможности доступа к внешнему контексту (внешним переменным и функциям), если не использовать специально предусмотренный для этого список захвата (capture list). Вероятно это еще один шаг в сторону усиления безопасности программирования путем разделения доступа. Сигнатура лямбда-функции: [capture list] (parameters)->return type
{
Тело функции
};
[capture list] список захвата лямбда-функции. Пример вызова лямбда функции с помощью присвоения имени: auto mylambda = []() { std::cout << "Hello World!" << std:endl; }; Вызов лямбда-функции по присвоенному имени: mylambda();
Также лямбда-функцию можно вызвать без указания имени: []()
{
std::cout << "Hello World!" << std:endl;
}();
Еще пример безымянного вызова лямбда-функции с передачей параметров: [](double a, double b) { std::cout << "a + b : " << (a + b) << std:endl; }(12.1, 5.7); Пример лямбда-функции, которая возвратит значение: auto result = [](double a, double b) { return (a + b); }(12.1, 5.7); Также лямбда-функцию можно поместить непосредственно в оператор вывода: std::cout << "result : " << [](double a, double b) { return (a + b); }(12.1, 5.7) << std:endl; Тип возвращаемого значения можно указать явно следующим способом: auto result = [](double a, double b)->double { return (a + b); }(12.1, 5.7); Capture list. Список захвата - это то, что у лямбда-функции находится в прямоугольных скобках. Список захвата позволяет получить внутри тела лямбда-функции доступ к внешним переменным и функциям. Для этого их имена перечисляются внутри квадратных скобок через запятую. Пример: double a{10}; double b{20}; // Список захвата из двух внешних переменных, передача по значению: auto func = [a,b](){ std::cout << "a + b : " << a + b << std::endl; }; func(); Полный захват внешнего контекста. Можно получить доступ ко всем внешним переменным и функциям внутри тела лямбда-функции: [=] Захват по значению. В тело лямбда-функции будет передана копия внешних объектов. [&] Захват по ссылке. В тело лямбда-функции будет передана ссылка на внешний объект. Довольно опасный выбор, потому что все действия над объектами внутри лямбда функции будут отражаться снаружи, и наоборот, все действия снаружи над объектами будут отражаться и внутри лямбда-функции. // Захват по значению, изменения снаружи не отражаются // внутри лямбда-функции: int c{42}; auto func = [=](){ std::cout << "Внутреннее значение : " << c << std::endl; }; for(size_t i{} ; i < 5 ;++i){ std::cout << "Внешнее значение : " << c << std::endl; func(); ++c; } // Захват по ссылке. Изменения снаружи отражаются внутри // лямбда-функции. Лучше так никогда не делать. int c{42}; int d{5}; auto func = [&](){ std::cout << "Внутреннее значение : " << c << std::endl; std::cout << "Внутреннее значение (d) : " << d << std::endl; }; for(size_t i{} ; i < 5 ;++i){ std::cout << "Внешнее значение : " << c << std::endl; func(); ++c; } 36. Шаблоны функции. Простыми словами это можно определить как общую модель для набора согласованных друг с другом функций, управляемых сразу в одной точке - в определении шаблона. Шаблон функции предназначен для устранения дублирования кода, когда один и тот же алгоритм обработки нужно применять к различным типам данных. Назначение шаблонов проще понять, если сравнить их поведение и использование с макросами [15]. Шаблоны позволяют сделать код более лаконичным и решить проблемы, связанные с перегрузкой функции. В следующем примере мы видим три различные перегрузки функции max, которые по сути делают одно и то же. int max (int a, int b) { return (a > b) ? a : b; } Если предположить, что понадобятся еще штук 10 перегрузок, то становится очевидно, что код становится чрезмерно громоздким. Такое повторение кода это очень плохо, потому что при необходимости сделать изменение алгоритма придется вносить правки в каждую из перегрузок. Это неудобно и чревато трудно обнаруживаемыми ошибками. Для преодоления этой проблемы как раз и предназначены шаблоны функции. Следующий пример шаблона заменит все перезагрузки функции max, показанные выше. template < typename T> T maximum(T a , T b) { return (a > b)? a : b; } Пример использования этого шаблона функции: int main() { int x{5}; int y{7}; Обратите внимание, что тип T в шаблоне должен поддерживать используемый в теле функции оператор >, иначе компилятор выдаст ошибку. Итак, шаблон функции это не настоящий выполняемый код, это всего лишь шаблон, по которому компилятор автоматически генерирует перегрузки функции, основываясь на типах параметров функции. 37. Дедукция компилятора при обработке типов в шаблоне, и явные аргументы. Здесь под дедукцией понимается алгоритм, который применяет компилятор при генерации реального кода на основе шаблона функции, когда он анализирует аргументы, переданные в шаблон функции. #include < iostream> Однако дедукция в некоторых случаях, может вызвать проблемы, когда в шаблон функции необходимо передать разные, не одинаковые типы, поскольку без явных аргументов будет требоваться, чтобы у параметров и возвращаемого значения был одинаковый тип T. В следующем примере показано применения явных аргументов, которое в этой ситуации позволяет передавать в шаблон аргументы разного типа. // Принудительное указание типа double для аргументов // шаблона функции (явные аргументы): maximum< double>(c, d); // Явное указание, что мы хотим double. // Если соответствующий экземпляр функции // пока не существует, то он будет создан. maximum< double>(a, c); // Сработает, даже если у аргументов разные типы. // Для первого параметра произойдет неявное // преобразование типа к указанному double. maximum< double>(a, e); // Произойдет ошибка компиляции из-за невозможности // преобразования типа std::string в тип double. Передача параметров в шаблон по ссылке. С помощью символа & можно указать, что значения параметров типов шаблона функции будут передаваться в его тело по ссылке, например: template < typename T> const T& maximum(const T& a, const T& b){ return (a > b) ? a : b; } При этом изменения переданных по ссылке переменных внутри сгенерированной функции будут отражаться снаружи. Ключевое слово const здесь применено только для каноничности, чтобы показать, что в данном примере шаблона функции нет никаких намерений изменять переданные параметры в теле функции. Специализация шаблона функции. Специализация шаблона это дополнительная его реализация для частного случая типов аргументов, когда для этого случая необходимо реализовать отдельное поведение функции. Специализация реализуется с помощью повторения имени функции шаблона и пустых угловых скобок. Пример: // Исходный шаблон функции:template < typename T> T maximum(T a,T b){ return (a > b) ? a : b ; } Назначение элементов этой специализации, по порядку: template < >. Обозначает, что это специализация какого-то уже существующего шаблона. У специализации должно совпадать имя функции с именем функции исходного шаблона (в этом примере maximum). const char * maximum. Обозначает, что специализация будет возвращать тип const char *. < const char*>. Обозначает типы аргументов специализации. (const char* a, const char* b). Список аргументов специализации. Как видно, в теле специализации шаблона приведен отдельный, отличающийся алгоритм сравнения строк, который задействует библиотечную функцию std::strcmp. Пример работы шаблона без специализации и со специализацией: #include < iostream> Стандартные концепции шаблона функции. Концепция шаблона является одной из четырех основных нововведений стандарта C++ 20. Это специальный механизм наложения ограничений на типы параметров шаблона. Например, мы таким образом можем наложить ограничение, чтобы наша функция вызывалась только с числами типа int. И если вы вызовете её с чем-то, что не является целым числом, то компилятор сообщит вам об этом. Следует заметить, что ограничение в виде явной проверки типов можно реализовать и альтернативными методами, например с помощью assert, однако концепция шаблона позволяет это сделать более удобно и синтаксически красиво. Ниже показан альтернативный способ ограничения на тип данных, реализованный с помощью static_assert. template < typename T> void print_number(T n) { static_assert(std::is_integral_v< T> ,"print_number() можно вызвать \ только с целочисленными типами"); std::cout << "number : " << n << std::endl; } Существуют 2 варианта концепции шаблона функции. Первый это встроенные стандартные концепции, которые поставляются вместе с языком программирования C++ в виде готовой библиотеки (подключаются заголовком < concepts>). Если по какой-то причине этого окажется недостаточно, то можно создать свою собственную концепцию, и это второй вариант - пользовательская концепция шаблона функции. При попытке вызова шаблона функции, который нарушает концепцию, произойдет ошибка компиляции. Вот основной список концепций шаблона функции C++20, которые вы можете использовать в C++ (определены в заголовочном файле < concepts>, см. также [16]): same_as. Указывает, что тип такой же, как и другой тип. Вариант 1, когда синтаксис концепции шаблона подразумевает использование ключевого слова requires, например: template < typename T> void print_number(T n) { static_assert(std::is_integral_v< T> ,"print_number() можно вызвать \ только с целочисленными типами"); std::cout << "number : " << n << std::endl; } Здесь концепцией является конструкция requires std::integral< T>. С такой концепцией шаблон будет работать только в том случае, если при вызове шаблона концепция будет удовлетворена. Для этого примера концепция требует, чтобы типы параметров были целочисленные (std::integral). Вариант 2, концепция шаблона без requires. Можно также установить концепцию шаблона, если вместо typename указать конкретный тип. Например: template < std::integral T> T add (T a, T b){ return a + b; } Вариант 3, указание концепции вместе с параметрами шаблона. auto add (std::integral auto a, std::integral auto b){ return a + b; } Вариант 4. template < typename T> T add (T a, T b) requires std::integral< T>{ return a + b; } Пользовательские концепции шаблона функции. Можно создавать свои концепции, ограничивающие применяемые в шаблоне типы параметров. Это делается с помощью ключевого слова concept. Вариант 1 синтаксиса. Пример требования, чтобы параметр шаблона был целочисленным: template < typename T> concept MyIntegral = std::is_integral_v< T>; Здесь MyIntegral это имя пользовательской концепции. Вот так эта концепция применяется: template < typename T> requires MyIntegral < T> T add_1 (T a, T b){ return a + b; }; Либо можно вот так: template < MyIntegral T> T add_2 (T a, T b){ return a + b; }; Можно еще и так: auto add_3 (MyIntegral auto a, MyIntegral auto b){ return a + b; }; Вариант 2 синтаксиса. Здесь требуется, чтобы оба параметра поддерживали умножение: template < typename T> concept Multipliable = requires (T a, T b){ a * b; // Код, которому должна удовлетворять концепция Multipliable }; Например, если вместо a и b передать строки, то произойдет ошибка компиляции, потому что тип строки не поддерживает умножение (не имеет смысла умножать строки). Вариант 3 синтаксиса. Здесь требуется, чтобы параметр можно было инкрементировать: template < typename T> concept Incrementable = requires (T a){ a += 1; ++a; a++; }; Существует 4 вида требований в концепциях шаблона функции: • Простые требования (simple requirements). [Простые требования] В принципе это все то, что уже было рассмотрено выше. Еще пример: template < typename T> concept TinyType = requires (T t){ // Простое требование: выражение только лишь проверяется на допустимый синтаксис: sizeof(T) < = 4; }; [Вложенные требования] Пример: template < typename T> concept TinyType = requires (T t){ // Простое требование: выражение только лишь проверяется на допустимый синтаксис: sizeof(T) < = 4; // Вложенное требование: проверка на то, чтобы выражение было true: requires sizeof(T) < = 4; }; [Составное требование] Пример: template < typename T> concept Addable = requires (T a, T b){ // Составное требование (здесь noexcept использовать необязательно): {a + b} noexcept -> std::convertible_to< int>; }; В этом составном требовании делается проверка, что a + b это совместимый синтаксис, с помощью noexcept отключаются исключения (это не обязательно), и еще делается проверка, что результат можно преобразовать в int (также не обязательно). Логические комбинации концепций. Концепции также можно объединять друг с другом, с помощью операторов && и ||. #include < iostream> Концепции и auto. Концепции также можно использовать для объявления переменной, для параметров и для возвращаемого значения функции. #include < iostream> 38. Классы. На языке C++ классы (class) это просто средство для создания пользовательских типов (кстати, структуры на C++ также неявно являются классами). Пример класса: const double PI {3.1415926535897932384626433832795}; В этом примере члены класса (свойства и методы) помечены спецификатором доступа public. Это значит, что эти члены класса общедоступны. Т. е. метод volume можно вызвать на экземпляре класса, и свойства base_radius и будут доступны для чтения и изменения. По умолчанию (если не указать public_ члены класса получают доступ private, т. е. они будут недоступны за пределами класса. Методы класса всегда имеют доступ к свойствам класса независимо от того, являются ли они private или public. Свойства класса могут быть либо переменными, либо указателями, но не могут быть ссылками. Причина в том, что ссылки всегда должны быть инициализированы, а для свойств класса важно иметь возможность быть не инициализированными. 39. Конструкторы. Это специальный метод класса, который предназначен выполнять какие-то инициализационные действия. Они вызываются в момент создания экземпляра класса. В предыдущем примере у класса Cylinder не был определен конструктор. Для такого случая компилятор автоматически сгенерирует пустой конструктор. У него не будет параметров, и в его теле не будет никаких действий. Особенности конструкторов: • Конструкторы не возвращают какой-либо тип. Пример класса Cylinder с конструкторами: class Cylinder { public: // Конструкторы класса: Cylinder(){ base_radius = 2.0; height = 2.0; } Обратите внимание, что теперь свойства base_radius и double height стали приватными, т. е. теперь к ним снаружи нет никакого доступа (их нельзя прочитать или записать). Эти свойства могут быть установлены только при создании объекта класса: либо значениями по умолчанию, если вызвать конструктор без параметров, либо указанными значениями, если вызвать второй вариант конструктора, у которого есть параметры. Конструктор по умолчанию. Синтаксис конструктора по умолчанию (иногда его называют пустым конструктором): #include < iostream> Конструктор по умолчанию нужен в том случае, если вы хотите сохранить его наличие. Дело в том, что если пользователь создал свой конструктор, то компилятор не будет генерировать пустой конструктор. Если вы по какой-то причине хотите отменить это поведение, то с помощью следующего синтаксиса можно сохранить пустой конструктор (на примере класса Cylinder): Cylinder() = default; Сеттеры (set) и геттеры (get). Когда мы делаем переменные - члены класса приватными (private), то для доступа к ним нужно создать публичные функции, которые называют сеттерами (от слова set, т. е. "установить значение") и геттерами (от слова get, т. е. "получить значение"). Сеттеры и геттеры должны определяться со спецификатором доступа public. Распределение класса по нескольким файлам. В предыдущих примерах использования класса Cylinder присутствовала константа PI. Её целесообразно вынести в отдельный файл, например constants.h, что позволит подключить определение этой константы в других разных модулях исходного кода вашей программы. Это делается с помощью директивы #include: #include "constants"
Обратите внимание, что имя подключаемого файла допускается указывать без расширения ".h". Определение класса Cylinder также можно переместить в отдельный заголовочный файл, например cylinder.h. Но можно пойти еще дальше: определить отдельный модуль cylinder.cpp, чтобы в нем находилась реализация методов класса. Такая методика позволяет разделить логику программы на простые, легко поддерживаемые уровни, где каждая часть четко отвечает за какие-то конкретные действия. Ниже на примере класса Cylinder показан принцип реализации модульности программы. Заголовочный файл constants.h: #pragma once Заголовочный файл cylinder.h: #pragma once Модуль cylinder.cpp, где определена реализация методов класса: #include "cylinder.h" Модуль main.cpp, где находится логика верхнего уровня программы: #include < iostream> Обратите внимание, что для такой методики в заголовочном файле класса (в данном примере это cylinder.h) не должно оставаться никаких определений методов класса, все эти определения должны быть перенесены в отдельный модуль, и снабжены префиксом из имени класса и двойного двоеточия (в нашем примере это Cylinder::). Управление объектами класса с помощью указателей. Объекты класса могут быть созданы в стеке либо в глобальной памяти, и тогда к членам экземпляра класса доступ будет осуществляться через точку. Также объекты могут быть созданы в куче с помощью оператора new, доступ к таким объектам будет осуществляться с помощью указателя и ->. Примеры: #include < iostream> 40. Деструкторы. Это специальные методы класса, которые вызываются в момент удаления объекта класса. Эти методы могут понадобиться для выполнения дополнительных действий, связанных с удалением объекта, например для удаления выделения памяти из кучи, или других операций очистки (таких как перенастройка какого-либо оборудования). Пример: #include < iostream> Очевидные случаи вызова деструктора: • Когда заканчивается локальная область действия объекта (т. е. объект был выделен в стеке, например внутри тела функции). Однако также существуют случаи косвенного вызова деструкторов. Деструкторы могут быть вызваны в не очевидных местах, чего следует избегать: • Когда объект класса был передан в функцию по значению. Порядок вызова конструкторов и деструкторов. Деструкторы вызываются в обратном порядке вызову конструкторов. 41. Указатель this. Это специальное ключевое слово, которое внутри методов, конструкторов и деструкторов объекта класса указывает не память, занимаемую этим объектом. Ключевое слово this может использоваться, когда параметр метода и свойство класса имеют одинаковое имя: class Dog { Если метод будет возвращать указатель на объект, то появится возможность вызывать методы цепочкой в одной строке: dog1.set_dog_name("Pumba")->set_dog_breed("Wire Fox Terrier")->set_dog_age(4); Также можно использовать this вместе со ссылками, но синтаксис будет несколько другой. Dog& set_dog_name(std::string_view name) { this->name = name; return *this; } Методы объекта теперь вызываются через точку: dog1.set_dog_name("Pumba")->set_dog_breed("Wire Fox Terrier")->set_dog_age(4); 42. Отличия в создании класса через class и через struct. Классы можно создавать как с помощью ключевого слова class, так и с помощью ключевого слова struct: class Dog { std::string m_name; }; Есть важное отличие создания класса через struct, и это отличие единственное: по умолчанию для class поля класса получают уровень доступа private, а для класса, созданного через struct, по умолчанию поля класса получают уровень доступа public. Мы можем изменить это поведение по умолчанию, используя для класса class модификатор доступа public, а для класса struct модификатор доступа private. 43. Размер объекта класса. Попробуем проанализировать размер объекта следующего класса: #include < stdint.h> Если запустить эту программу, то будет выведено следующее: sizeof(char): 1 sizeof(short): 2 sizeof(int): 4 sizeof(size_t): 8 sizeof(std::string): 32 sizeof(SomeClass): 48 sizeof(sc): 48 name: I am center of Universe size_of(m_name): 32 size_of(this): 8 Функции do_some и print_info занимают какое-то место в памяти программ, и являются общими для всех экземпляров класса. В размере экземпляра класса они никак не учитываются. Переменные m8, m16, m32 и m64 будут занимать в памяти 1, 2, 4 и 8 байт соответственно. Может показаться, что размер экземпляра объекта будет равен сумме размеров его членов (указателей на методы, переменных, указателей на дочерние объекты). Однако это не верно, поскольку существует еще и такая вещь, как выравнивание размещения объектов в памяти (boundary alignment). Переменные будут храниться в байтовых группах памяти, начальные адреса которых нацело делятся на 4. Таким образом, если у нас есть переменные размером меньше 4 байт, то в памяти могут образовываться пустые места. Поэтому размер объекта класса может быть несколько больше, чем можно было бы ожидать при суммировании размеров членов класса. Таким образом, эти переменные будут занимать в памяти 20 байт: 4 + 4 + 4 + 8. Переменная m_name внутренне реализована как класс, и здесь хранится указатель на экземпляр этого класса. Т. е. размер переменной m_name будет равен размеру экземпляра класса, а строка, которая хранится в ней, будет находиться в отдельно выделенной памяти. Ключевое слово this представляет собой указатель на объект класса, поэтому его размер равен 8 байт. 44. Наследование. Это способ создания иерархии классов, когда новый класс создается на основе родительского класса. Новые типы данных создаются на основе других типов, и новые типы могут использовать методы и свойства родительских типов, а также смогут добавлять свои собственные методы и свойства. В результате создается дерево наследования, которое может быть многоуровневым, начиная от самого базового класса, кончая конечными производными классами. Это вроде как позволяет улучшить повторное использование кода. Пример наследования, класс Player наследуется от Person: Базовый класс Person, файл person.h: #ifndef PERSON_H Реализация базового класса, файл person.cpp: #include "person.h" Определение производного класса, файл player.h: #ifndef PLAYER_H Реализация производного класса, файл player.cpp: #include "person.h" Пример использования, файл main.cpp: #include < iostream> С public-наследованием производные классы могут получать доступ к public членам базового класса, но не может обращаться к private членам базового класса. То же самое относится и к friend производного класса. Они могут обращаться к private членам производного класса, но не могут обращаться к private членам базового класса. Существует также кроме модификаторов доступа public и private существует также и модификатор protected. Он дает доступ на чтение и запись к членам базового класса из производного класса, но не дает доступ к ним извне. Спецификатор наследования protected. В примере выше для определения производного класса использовался модификатор доступа public: class Player : public Person { ... Со спецификатором наследования public для производного класса будет точно такой же доступ к членам класса, как и доступ снаружи к членам для базового класса. Это максимально облегченный способ доступа к элементам базового класса из производного класса: [Базовый класс] --> [Производный класс] public --> public Со спецификатором наследования protected для производного класса доступ к членам базового класса будет установлен следующим образом: class Player : protected Person { ... [Базовый класс] --> [Производный класс] public --> protected Также возможно еще и private наследование. С этим типом наследования доступ к членам класса со стороны производного класса будет закрыт. Это самый защищенный вариант для базового класса: class Player : private Person { ... [Базовый класс] --> [Производный класс] public --> private Закрытие доступа после private-наследования. Частное (private) наследование не позволит членам этого класса быть унаследованными в следующем дочернем классе, независимо от его типа наследования (объяснение этого с демонстрационным примером см. в части 3 уроков [1], тема "Closing in on private inheritance" с позиции 30 минут 55 секунд. Пример находится в папке 36.Inheritance/36.6ClosingInOnPrivateInheritance). Восстановление доступа после private-наследования. Это можно сделать с помощью ключевого слова using (объяснение этого с демонстрационным примером см. в части 3 уроков [1], тема "Resurrecting members back in scope" с позиции 49 минут 47 секунд. Пример находится в папке 36.Inheritance/36.7ResurectingMembersBackInContext). 45. Полиморфизм. Сущность полиморфизма заключается в том, что можно управлять объектами производных классов через указатели на объект базового класса, и в результате получать правильный метод, вызываемый по указателю базового класса. То же самое относится и ссылкам на объект базового класса. - Указатель на базовый класс может принимать адрес на объект любого из производных классов. То же самое относится и к ссылкам на базовый класс. Полиморфизм позволяет: 1. Присвоить указателю на базовый класс адрес объекта производного класса (созданного с помощью new). Shape * shape1 = new Circle; Shape * shape2 = new Rectangle; Shape * shape3 = new Oval; 2. Создавать ссылки на объекты производного класса через эти указатели. Shape& ref1 {&shape1}; Shape& ref2 {&shape2}; Shape& ref3 {&shape3}; 3. Вызывать через указатели (или ссылки) методы как базового, так и производного класса. 4. Можно создать функцию, общую для всех производных классов, которая будет в параметре принимать указатель на базовый класс. В зависимости от того, что передано в этом указателе (адрес какого производного класса объект) могут быть предприняты разные действия. void dtaw_shape (const Shape& shape) { // Будет вызван правильный метод производного класса, // в зависимости от того, адрес какого объекта производного // класса был передан в параметре shape: shape->draw(); } 5. Еще одно преимущество полиморфизма - можно хранить указатели на объекты различных производных типов в одной коллекции (массиве), и обрабатывать их однообразно, в одном цикле. Для этого нужно использовать массив указателей на объекты базового класса. Внимание: если заново присвоить указатель на объект базового класса адресом другого производного класса, то все равно будет вызван метод предыдущего производного класса. Причина в том, что была сделана статическая привязка в момент компиляции программы. Избежать такого нежелательного поведения можно с помощью ключевого слова virtual, представляющего динамическую привязку методов. Ключевое слово virtual добавляется перед определением и декларацией методов класса. При наследовании: без ключевого слова virtual перед определением метода происходит его статическое связывание, а с ключевым словом virtial - динамическое. Т. е. с использовании virtual размеры объектов будут больше. Статическое связывание: Динамическое связывание: При статическом связывании компилятор будет вызывать методы базового класса по указателю базового класса, которому присвоен адрес объекта дочернего класса. При динамическом связывании компилятор умнее - он может в этом случае вызывать методы объекта дочернего класса. 46. Размер полиморфных объектов, или объектов, использующих динамическое связывание. Динамическое связывание не является бесплатным, это достигается ценой дополнительного расхода памяти. 47. Слайсинг, срез (slicing). Если присвоить объекту базового класса объект дочернего класса, то из дочернего класса будет взято только то, что относится к базовому классу. 48. Override (переопределение). Ключевое слово override вставляется после имени в определении метода, перед открывающей фигурной скобкой: virtual void draw() const override { std::cout << "Oval::draw() called. Drawing " << m_description << " with m_x_radius : " << m_x_radius << " and m_y_radius : " << m_y_radius << std::endl; } Override указывает, что здесь происходит переопределение существующего метода в базовом классе. Используется для специфичной настройки методов в производных классах. 49. Overloading, overriding, hiding. Overloading: это когда существует 2 функции (метода) с одинаковым именем, но разным набором параметров. Hiding: если сделать перезагрузку в дочернем классе с ключевым словом override, то тогда все методы базового класса с таким же именем (перезагрузки) станут недоступны. 50. Final. Метод с добавленной спецификацией final не может быть переопределен в следующем дочернем классе. Если применить final на уровне определения класса, то это полностью запрещает его дальнейшее наследование. 51. Аргументы по умолчанию и полиморфизм virtual. Следует иметь в виду поведение C++, касающееся аргументов по умолчанию метода в базовом классе и динамического переопределения этого метода в дочернем классе с помощью virtual и override: • Аргументы по умолчанию обрабатываются в момент компиляции, т. е. статически. Таким образом, в полиморфной функции дочернего класса (неожиданно) будут использоваться аргументы по умолчанию из базового класса. Т. е. при использовании полиморфизма следует избегать аргументов по умолчанию для функций. 52. Виртуальный деструктор. Если определить деструктор через virtual, то это позволит избежать проблемы удаления дочерних объектов по указателю, у которого тип указателя на базовый объект. 53. dynamic_cast < >(). Это динамическое преобразование типа объекта, которое позволяет сделать следующее: • Выполнить runtime-трансформацию данных из указателя или ссылки на объект базового класса в объект дочернего класса. 54. Конструкторы, деструкторы и виртуальные функции. Ни в коем случае не следует пытаться вызывать виртуальные функции из тела конструкторов или деструкторов: • Вызов полиморфной функции из конструктора или деструктора не даст эффекта полиморфизма. 55. Чисто виртуальные методы, абстрактный класс. Абстрактный класс это такой класс, где нет реализации виртуальных методов, т. е. они должны быть реализованы в дочерних классах. Побочный эффект - компилятор запретит создавать объекты такого класса. Определение чисто виртуального класса выглядит довольно непривычно. У такого класса есть pure virtual (чисто виртуальные) методы, тело которых определено как "const = 0;". Пример: class Shape { Получается следующее: • Если в классе есть хотя бы одна чисто виртуальная (pure virtual) функция, то он становится абстрактным классом. 56. Моделирование интерфейсов с помощью абстрактных классов. • Абстрактный класс, где есть только чисто виртуальные функции, и отсутствуют переменные члены класса, может использоваться как модель, которая в ООП называется интерфейсом. [Ссылки] 1. Курс программирования на C++ от новичка до продвинутого / машинный перевод site:youtube.com. |