Программирование PC Курс программирования на C++ для новичков и не только Sun, September 08 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Курс программирования на 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):

CPP course fig01

Расширения, которые стоит установить, если они еще не установлены:

C/C++ for Visual Studio Code

Удобный способ запустить VSCode в текущей папке:

$ code .

Полную инструкцию на все случаи жизни можно посмотреть на страничке [3].

5. Как скачать уроки:

- Перейдите в папку, где вы хотели бы сохранить уроки. Например, ~/MyProjects.
- Выполните в этой папке команду git clone https://github.com/rutura/The-C-20-Masterclass-Source-Code.git

После этого в папке ~/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, простейший пример программы вывода в консоль.
   3.3Comments: как в коде создаются комментарии (// комментарий, /* комментарий */).
   3.4ErrorsWarnings: виды ошибок (ошибка времени компиляции, ошибка времени выполнения, предупреждение).
   3.5StatementsAndFunctions: что такое операторы и функции.
   3.6DataInputAndOutput: ввод и вывод данных в программе. Описание операторов std::cout, std::cin, std::cerr, std::clog, << , >>. Первое появление строкового типа std::string, функции std::getline.

04.VariablesAndDatatypes

   4.2NumberSystems: представление чисел в программе в различных системах счисления: двоичная, десятичная, восьмеричная, шестнадцатеричная, символьная.
   4.3IntegerTypes: объяснение различных вариантов объявления инициализируемых переменных, присваивание с неявным преобразованием, sizeof().
   4.4IntegerModifiers: что такое модификаторы целых чисел, как они влияют на представление и размер данных (signed, unsigned, short, long, signed long, signed long long, signed long int и т. п.). Неявное указание типа.
   4.5FractionalNumbers: что такое дробные числа и как с ними работать. Понятие о точности дробного числа.
   4.6Booleans: работа с логическими (двоичными) значениями true и false.
   4.7CharactersAndText: работа с одиночными символами (char) и текстом.
   4.8Auto: автоматический подбор типа для переменной с помощью компилятора.
   4.9Assignments: эксперименты с присваиванием переменным значений.

05.OperationsOnData

   5.2.BasicOperations: обзор базовых математических операций.
   5.3.PrecedenceAndAssociativity: порядок выполнения вычислений, приоритеты операций в выражениях.
   5.4.PrefixPostfixIncrementDecrement: смысл префиксных и постфиксных операторов инкремента и декремента.
   5.5.CompoundAssignmentOperators: комбинированные операторы (с присваиванием).
   5.6.RelationalOperators: операторы сравнения чисел.
   5.7.LogicalOperators: логические операторы.
   5.8.OutputFormatting: форматирование при выводе std::cout (#include < ios>, #include < iomanip>).
   5.9.NumericLimits: функции определения пределов целых чисел (#include < limits>), min(), max(), lowest().
   5.10.MathFunctions: математические функции.
   5.11.WeirdIntegralTypes: "странные" целочисленные типы.

06.LiteralsAndConstants

07.ConversionsOverflowAndUnderflow

08.BitwiseOperators

09.VariableLifetimeAndScope

10.FlowControl, управление потоком вычислений.

   10.2IfStatements: операторы условного ветвления потока выполнения.
   10.3ElseIf: операторы условного ветвления потока выполнения.
   10.4Switch: оператор управления потоком, работающий в стиле "переключения". Особенность этого оператора в том, что его проверяемое значение может быть только целочисленным или перечислением (enum).
   10.5ShortCircuitEvaluation
   10.6IntegralLogicConditions
   10.7TernaryOperators: троичный оператор ветвления (условие?оператор1:оператор2).
   10.8IfConstexpr
   10.9IfWithInitializer
   10.10SwitchWithInitializer
   10.11VariableScopeRevisited
   10.12SwitchScope

11.Loops, циклы.

   11.2ForLoop: цикл for.
   11.3ForLoopMultipleDeclarations
   11.4CommaOperator
   11.5RangeBasedForLoop
   11.6WhileLoop: цикл while.
   11.7HugeLoopsWithOutput
   11.8DoWhileLoop: цикл do/while.
   11.9InfiniteLoops
   11.10InfiniteLoopPractice
   11.11DecrementingLoops
   11.12NestedLoops
   11.13BreakAndContinue
   11.14FixCalculator
   11.15ForLoopWithInitCondition

12.Arrays, массивы.

   12.2DeclaringAndUsingArrays: декларирование и использование массивов.
   12.3SizeOfAnArray: способ определения размера массива с помощью std::size.
   12.4ArraysOfCharacters: массивы символов.
   12.5ArrayBounds: описание проблем, которые могут произойти из-за попытки доступа к данным вне массива (см. [13] для диагностики этих проблем).

13.Pointers, указатели.

   13.2DeclaringAndUsingPointers: декларирование и использование указателей.
   13.3PointerToChar: указатель на символы.
   13.15DynamicMemoryAllocation: динамическое выделение памяти.
   13.16DanglingPointers: неправильные указатели (см. [13] для диагностики этих проблем).
   13.17WhenNewFails: техника перехвата ошибок в программе (try/catch/std::exeption) на примере оператора new.
   13.18NullPointerSafety: обсуждение проблемы нулевых указателей.
   13.19MemoryLeaks: обсуждение проблемы утечек памяти (см. [13] для диагностики этих проблем).
   13.20DynamicallyAllocatedArrays: примеры инициализации массивов с выделением памяти из кучи.

14.References, работа со ссылками.

   14.2.DeclaringAndUsingReferences: примеры декларирования и использования ссылок.
   14.3.ComparingPointersAndReferences: сходство и отличие указателей и ссылок.
   14.4.ReferencesAndConst: как ключевое слово const можно использовать со ссылками.

15.CharacterManipulationAndStrings, строки, операции над строками.

   15.2CharacterManipulation: проверки символов на регистр, является ли они алфавитно-цифровыми, подсчет пробелов в строке и т. п.
   15.3CStringManipulation: определение длины строк, сравнение строк, поиск символа в строке.
   15.4CStringConcatenationAndCopy: слияние, копирование строк.
   15.6DeclaringAndUsingStdString: декларирование и использование строк типа std::string.

16.Functions, работа с функциями.

   16.2FirstHandOnCppFunctions: первое знакомство с функциями C++.
   16.3FunctionDeclarationsAndDefinitions: объяснение понятий декларации и определения функции.
   16.4MultipleFiles_CompilationModelRevisited: обзор модели компиляции, объяснение понятий препроцессора, компиляции, линковки, единицы трансляции.
   16.5PassByValue: передача параметра в функцию по значению.
   16.7PassByPointer: передача параметра в функцию по указателю.
   16.10PassByReference: передача параметра по ссылке.

17.EnumsAndTypeAliases

18.ArgumentsToTheMainFunction

19.GettingThingsOutOfFuntions, методы работы с функциями.

   19.2InputAndOutputParameters: способы вывода данных из функции с помощью ссылок и указателей.
   19.3ReturningFromFunctionsByValue: объяснение особенностей возврата по значению из функции в контексте работы оптимизатора компилятора.

20.FunctionOverloading, обзор перегрузки функции.

   20.2OverloadingWithDifferentParameters: демонстрация перегрузки функции путем различий в параметрах.

21.LambdaFunctions, знакомство с лямбда-функциями.

   21.2DeclaringAndUsingLambdas: простые примеры декларирования и использования лямбда-функций.
   21.3CaptureLists: использование списка захвата.
   21.4CaptureAllLists: демонстрация захвата всего внешнего контекста.

22.FunctionsTheMisfits

23.FunctionCallStackD_ebugging

24.FunctionTemplates, введение в шаблоны функции.

   24.2TryingOutFunctionTemplates: первое знакомство с шаблоном функции. В уроке обсуждаются проблемы, связанные с несоответствием типов передаваемых переменных типам шаблона, а также случаям не поддерживаемых в теле шаблона операторов для переданных в шаблон типов параметров. Показана отладка кода шаблона в среде VSCode.
   24.3TemplateTypeDeductionAndExplicitArguments: как работает дедукция компилятора при обработке аргументов шаблона функции, и как указываются явные аргументы для параметров шаблона.
   24.4TemplateTypeParametersByReference: передача параметров по ссылке в шаблон функции.
   24.5TemplateSpecialization: объяснение специализаций шаблона функции.

25.Concepts, введение в концепции шаблона функции.

   25.02UsingConcepts: использование концепций.
   25.03BuildingYourOwnConcepts: применение пользовательских концепций.
   25.04ZoomingInOnRequiresClause: рассмотрение других вариантов концепций.
   25.05CombiningConcepts: объединение концепций логическими операторами && и ||.
   25.06ConceptsAndAuto: концепции и auto.

26.Classes, знакомство с классами.

   26.2YourFirstClass: ваш первый класс на C++.
   26.3Constructors: знакомство с конструкторами.
   26.4DefaultedConstructors: конструкторы по умолчанию.
   26.5SettersAndGetters: методы для манипулирования переменными - членами класса.
   26.6ClassAcrossMultipleFiles: пример вынесения класса в отдельные файлы.
   26.8ManagingClassObjectsThroughPointers: создание объектов класса в стеке и в куче, и управление ими с помощью указателей.
   26.9Destructors: введение в понятие деструктора класса.
   26.10OrderOfConstructorDestructorCalls: в каком порядке вызываются конструкторы и деструкторы.
   26.11ThisPointer: использование указателя на память объекта класса this.
   26.12Struct: особенности создания класса с помощью struct.
   26.13.SizeOfClassObjects: обсуждение, сколько памяти может занимать объект класса.

27.ZoomingInOnClassObjects

28.DivingDeepIntoConstructorsAndInitialization

29.Friends

30.ConstAndStaticMembers

31.Namespaces

32.ProgramsWithMultipleFiles

   32.5OneDefinitionRule: объяснение работы "правила одного определения".

33.SmartPointers

34.OperatorOverloading

35.LogicalOperatorsAndThreeWayComparison

36.Inheritance, наследование.

   36.2FirstTryOnInheritance: первое знакомство с наследованием.
   36.3ProtectedMembers: демонстрация действия модификатора доступа protected.
   36.6ClosingInOnPrivateInheritance): объяснение невозможности наследования private членов из производного класса, который получил private наследование от базового класса.
   36.8DefaultArgConstructorsWithInheritance: особенности использования конструкторов по умолчанию при наследовании.
   36.9ConstructorsWithInheritance: использование пользовательских конструкторов при наследовании.
   36.10CopyConstructorsWithInheritance: пользовательские конструкторы копирования и иерархия наследования.
   36.11InheritingBaseConstructors: использование конструкторов базового класса.
   36.12InheritanceAndDestructors: как деструкторы работают с наследованием.
   36.13ReusedSymbolsInInheritance: повторное использование имен при наследовании.

37.Polymorphism, объяснение полиморфизма.

   37.2StaticBindingWithInheritance: демонстрация статической привязки одноименных методов к производному классу в полиморфизме.
   37.3PolymorphismWithVirtualFunctions: полиморфизм (динамическая привязка) с помощью виртуальных функций в иерархии наследования.

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.

CPP course fig02

После этого изменяйте код main.cpp и добавляйте новые модули исходного кода.

1. Создайте пустую папку для проекта, дайте ей понятное имя, характеризующее выполняемую задачу. Для примера назовем папку MyFirstCppProgram.

2. Кликните правой кнопкой на папке MyFirstCppProgram, и выберите "Открыть с помощью -> Visual Studio Code". Запустится среда VSCode, и откроется окно Welcome.

3. Создайте новый файл main.cpp (меню File -> New File... -> введите имя main.cpp). В редакторе файла main.cpp введите следующий текст:

#include < iostream>

int main() { auto result = (10 < => 20) > 0; std::cout << result << std::endl; }

В этом коде мы проверяем, что реально поддерживаются фичи 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:

CPP course fig03

В предыдущей врезке мы рассмотрели пример простейшей программы и её компиляции с помощью прямого вызова в командной строке компилятора g++. Однако можно настроить и автоматизировать этот процесс, сделав его более удобным, с помощью задач VSCode (Tasks).

1. Выберите в меню Terminal -> Configure Tasks..., среда VSCode найдет компиляторы, которые были установлены на шаге "Компиляторы, которые нужно установить в Linux: GCC, Clang llvm".

CPP course fig04

Чтобы создать задачу для компиляции текущего файла компилятором GCC (g++), выберите в выпавшем списке пункт "C/C++: g++ build active file". В папке проекта будет автоматически создана папка для настроек .vscode, и в ней будет создан файл настроек для задач tasks.json.

CPP course fig05

{
   "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
• Prograniz
• cpp.sh
• OneCompiler
• Wandbox
• Compiler Explorer
• Coliru
• Online C++ Compiler

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

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, инициализация с помощью фигурных скобок {}.
Functional Initialization, инициализация с помощью круглых скобок ().
Assignment Initialization, инициализация с помощью оператора присваивания =.

Примеры:

int myValue1 = 7;     // инициализация переменной значением 7
int myValue2 {8}; // инициализация переменной значением 8
int myValue3 {}; // инициализация переменной нулем
int myValue4 (9); // инициализация переменной значением 9

В инициализаторе можно использовать выражение:

int A = 2;
int B = 3;
int sum {A + B};

Инициализации со скобками () и {} имеют особенности, заключающиеся в неявном преобразовании данных. Например:

// Следующее объявление создаст переменную narrowing_conversion_functional,
// и неявно присвоит ей значение 2, отбросив дробную часть. Этот так называемое
// "функциональное" преобразование, компилятор при этом не выдаст никаких
// сообщений. Вероятно это не то, что вы хотели бы получить:
int narrowing_conversion_functional(2.9);

// Более безопасный вариант сделать то же самое, потому что в этом случае
// компилятор выдаст сообщение об ошибке int.ERROR или WARNING:
int narrowing_conversion(2.9);

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 4 7  
double 8 15 Рекомендуемый для использования тип.
long double 12 > double  
    // Примеры декларации и инициализации переменных плавающей точки.
    // В примере типа 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';
int intA = (int)chA; // так на языке C
int intB = static_cast< int>(chA); // так на языке C++

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 адреса этого числа.
int *p2 {new int{12}}; // Выделение в куче памяти под 12 чисел int // и присваивание p2 адреса первого из них.

27. Пример определения массивов с выделением памяти из кучи:

#include < iostream>

int main() { const size_t size{10}; // Различные способы, с помощью которых вы можете декларировать // массивы динамически, и как они могут быть инициализированы. // В массиве salaries будут содержаться неопределенные значения, // т. е. этот массив при создании не инициализируется: double *p_salaries { new double[size]}; // В этом примере все значения массива инициализируются в 0. // Исключения при работе с массивом отключаются: int *p_students { new(std::nothrow) int[size]{} }; // Выделение памяти под массив из size элементов (т. е. 10). // Первые пять элементов массива инициализируется значениями // 1, 2, 3, 4, 5, а остальные элементы инициализируются // нулями: double *p_scores { new(std::nothrow) double[size]{1,2,3,4,5}}; // Проверка на nullptr: корректно ли была выделена память под массив. if(p_scores) { std::cout << "size of scores (it's a regular pointer) : " \ << sizeof(p_scores) << std::endl; std::cout << "Successfully allocated memory for scores." \ << std::endl; // Печать элементов массива. Можно использовать как традиционную // нотацию обращения к элементам массива, так и арифметику указателей: for( size_t i{}; i < size ; ++i) { std::cout << "value : " << p_scores[i] << " : " << \ *(p_scores + i) << std::endl; } }
delete [] p_salaries; p_salaries = nullptr;
delete [] p_students; p_students = nullptr;
delete [] p_scores; p_scores = nullptr;
return 0; }

Важное замечание: массивы, выделенные динамически, сильно отличаются от массивов, определенных статически или локально в стеке. Например по массивам, выделенным в куче, нельзя использовать итерацию по диапазону (for(auto i: имя_массива){}), и для них не работает оператор std::size.

   // Сравнение динамических массивов с массивами в стеке:
   std::cout << "=====================================" << std::endl;

// Этот массив выделяется из стека, и на нем будет нормально работать код // с использованием std::size и for( auto s : scores): int scores[10] {1,2,3,4,5,6,7,8,9,10}; std::cout << "scores size : " << std::size(scores) << std::endl; for( auto s : scores) { std::cout << "value : " << s << std::endl; }

// Этот массив выделяется из кучи, и на нем нельзя будет использовать // std::size и for( auto s : p_scores1): int* p_scores1 = new int[10] {1,2,3,4,5,6,7,8,9,10}; // std::cout << "p_scores1 size : " << std::size(p_scores1) << std::endl; /* for( auto s : p_scores1) { std::cout << "value : " << s << std::endl; } */

28. Определение ссылок на C++ может быть сделано по-другому:

   // Определение переменных int_data и double_data:
   int int_data{33};
   double double_data{55};

// Определение указателей ref_int_data и ref_double_data, // и инициализация их адресами переменных int_data и double_data: int& ref_int_data{int_data}; double& ref_double_data{double_data};

Стоит отметить, что только так же, как и на языке C, ссылки на переменные работают точно так же, как и имена переменных. Т. е. для предыдущего примера &int_data и &ref_int_data дадут один и тот же адрес.

29. Ссылки и указатели. В принципе и ссылки, и указатели делают одно и то же, но все же их поведение и работа с ними отличается.

Ссылки (references):

- Не используется разименование (*) для чтения и записи по ссылке. Ссылка указывается в коде точно так же, как исходная переменная.
- Ссылка не может быть изменена, чтобы она ссылалась на другую переменную. Этим ссылки соответствуют постоянным (const) указателям, которые также не могут быть изменены.
- Ссылка при декларации обязательно должна быть инициализирована.

Указатели (pointers):

- При доступе к значению для чтения и записи по указателю используется разименование (*).
- Указатель может быть изменен, чтобы через него можно было обращаться к другому значению.
- Указатель может быть декларирован без его инициализации, т. е. он может иметь недостоверный адрес (указывающий на недопустимое место в памяти), или может иметь значение nullptr.

Таким образом, код с использованием ссылок более безопасен и прост. Указатели позволяют произвольно манипулировать данными.

   int age {58};                // переменная 
   int& ref_age{age};           // обычная ссылка на age,
   ref_age++;                   // позволяющая её изменять

// Ссылка не может быть перенастроена на другую переменную. // То же самое можно проделать с указателем, если объявить // его с модификатором const: int* const pnt_age{&age}; pnt_age = nullptr; // на эту строку компилятор выдаст ошибку

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. Проверка, является ли символ алфавитно-цифровым.
isalpha. Проверка, является ли символ алфавитным.
isdigit. Проверка, является ли символ цифрой (0 .. 9).
isxdigit. Проверка, является ли символ шестнадцатеричной цифрой (0 .. 9, a .. f, A .. F).
isblank. Проверка, является ли символ пустым, т. е. пробелом (начиная с C++11).
islower, isupper. Проверка, находится ли символ в нижнем регистре или в верхнем регистре.
std::tolower, std::toupper. Преобразование символов к нижнему или верхнему регистру.

cstring. Также существуют std::-аналоги C-функций < string.h>, определенные в заголовочном файле < cstring>.

31. Примеры декларации строк типа std::string:

#include < iostream>
#include < string>

// Пустая строка: std::string full_name; // Инициализация строки литералом: std::string planet {"Earth. Where the sky is blue"}; // Инициализация строки существующей строкой: std::string prefered_planet{planet}; // Инициализация строки частью строкового литерала ("Hello"): std::string message {"Hello there",5}; // Инициализация несколькими копиями символа (будет содержать "eeee"): std::string weird_message(4,'e');

std::string greeting{"Hello World"}; // Инициализация частью существующей строки, начиная с индекса 6 // в количестве 5 символов (будет содержать "World"): std::string saying_hello{ greeting,6,5};

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; 
}

double max(double a, double b) { std::cout << "double overload called" << std::endl; return (a>b)? a : b; }

double max(int a, double b) { std::cout << "(int,double) overload called" << std::endl; return (a>b)? a : b; }

double max(double a, int b) { std::cout << "(double,int) overload called" << std::endl; return (a>b)? a : b; }

double max(double a, int b,int c) { std::cout << "(double,int,int) overload called" << std::endl; return a; }

std::string_view max(std::string_view a, std::string_view b) { std::cout << "(string_view,string_view) overload called" << std::endl; return (a>b)? a : b; }

Компилятор автоматически выберет правильную перегрузку функции в зависимости от типа, количества и порядка следования типов передаваемых параметров.

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

35. Лямбда-функции. Это механизм настройки анонимных функций (безымянных функций). Как только они настроены, мы можем либо дать им имена, чтобы потом вызывать их, либо мы даже можем заставить их делать что-то напрямую, не используя имени.

Особенность лямбда-функции в том, что в её теле нет возможности доступа к внешнему контексту (внешним переменным и функциям), если не использовать специально предусмотренный для этого список захвата (capture list). Вероятно это еще один шаг в сторону усиления безопасности программирования путем разделения доступа.

Сигнатура лямбда-функции:

[capture list] (parameters)->return type
{
   Тело функции
};

[capture list] список захвата лямбда-функции.
(parameters) параметры лямбда-функции.
-> return type возвращаемый тип значения. Это указывать необязательно, в этом случае компилятор сам выберет подходящий тип.
; признак завершения лямбда-функции, который делает это утверждение действительным.

Пример вызова лямбда функции с помощью присвоения имени:

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 : " << result << std:endl;

Также лямбда-функцию можно поместить непосредственно в оператор вывода:

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);

std::cout << "result : " << result << std:endl;

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 = [c](){ std::cout << "Внутреннее значение : " << c << " &inner : " << &c << std::endl; }; for(size_t i{} ; i < 5 ;++i){ std::cout << "Внешнее значение : " << c << " &outer : " << &c << std::endl; func(); ++c; }
// Демонстрация захвата по ссылке. Изменение внешней переменной отражается // внутри лямбда-функции, адреса переменных совпадают. int c{42}; auto func = [&c](){ std::cout << "Inner value : " << c << " &inner : " << &c << std::endl; }; for(size_t i{} ; i < 5 ;++i){ std::cout << "Outer value : " << c << " &outer : " << &c << std::endl; func(); ++c; }

Полный захват внешнего контекста. Можно получить доступ ко всем внешним переменным и функциям внутри тела лямбда-функции:

[=] Захват по значению. В тело лямбда-функции будет передана копия внешних объектов.

[&] Захват по ссылке. В тело лямбда-функции будет передана ссылка на внешний объект. Довольно опасный выбор, потому что все действия над объектами внутри лямбда функции будут отражаться снаружи, и наоборот, все действия снаружи над объектами будут отражаться и внутри лямбда-функции.

   // Захват по значению, изменения снаружи не отражаются
   // внутри лямбда-функции:
   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;
}

double max (double a, double b) { return (a > b) ? a : b; }

std::string_view max (std::string_view a, std::string_view 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};

int* p_x {&x}; int* p_y {&y};

auto result = maximum(p_x,p_y); std::cout << "result : " << *result << std::endl; return 0; }

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

37. Дедукция компилятора при обработке типов в шаблоне, и явные аргументы. Здесь под дедукцией понимается алгоритм, который применяет компилятор при генерации реального кода на основе шаблона функции, когда он анализирует аргументы, переданные в шаблон функции.

#include < iostream>
#include < string>

template < typename T> T maximum(T a, T b){ return (a > b) ? a : b; }

int
main()
{ int a{10}; int b{23}; double c{34.7}; double d{23.4}; std::string e{"hello"}; std::string f{"world"};

// Генерация кода компилятором с применением дедукции: maximum(a, b); // генерация кода для типа int maximum(c, d); // генерация кода для типа double maximum(e, f); // генерация кода для типа string

Однако дедукция в некоторых случаях, может вызвать проблемы, когда в шаблон функции необходимо передать разные, не одинаковые типы, поскольку без явных аргументов будет требоваться, чтобы у параметров и возвращаемого значения был одинаковый тип 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 ; 
}

// Специализация этого шаблона для типа const char *:
template < > const char * maximum< const char*> (const char* a, const char* b){ return (std::strcmp(a,b) > 0) ? a : b; }

Назначение элементов этой специализации, по порядку:

template < >. Обозначает, что это специализация какого-то уже существующего шаблона. У специализации должно совпадать имя функции с именем функции исходного шаблона (в этом примере maximum).

const char * maximum. Обозначает, что специализация будет возвращать тип const char *.

< const char*>. Обозначает типы аргументов специализации.

(const char* a, const char* b). Список аргументов специализации.

Как видно, в теле специализации шаблона приведен отдельный, отличающийся алгоритм сравнения строк, который задействует библиотечную функцию std::strcmp. Пример работы шаблона без специализации и со специализацией:

#include < iostream>
#include < cstring>

int main() { // Обычное использование шаблона функции: int a{10}; int b{23}; double c{34.7}; double d{23.4}; std::string e{"hello"}; std::string f{"world"};

auto max_int = maximum(a,b); // функция maximum() сгенерируется для типа int auto max_double = maximum(c,d); // функция maximum() сгенерируется для типа double auto max_str = maximum(e,f); // функция maximum() сгенерируется для типа string

std::cout << "max_int : " << max_int << std::endl; std::cout << "max_double : " << max_double << std::endl; std::cout << "max_str : " << max_str << std::endl;

// А здесь активируется отдельная специализация шаблона: const char* g{"wild"}; const char* h{"animal"};

const char* result = maximum(g,h); std::cout << "max(const char*) : " << result << std::endl; return 0; }

Стандартные концепции шаблона функции. Концепция шаблона является одной из четырех основных нововведений стандарта 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. Указывает, что тип такой же, как и другой тип.
derived_from. Указывает, что тип является производным от другого типа.
convertible_to. Указывает, что у этого типа существует возможность неявного преобразования в другой тип.
common_reference_with. Указывает, что два типа разделяют общий ссылочный тип.
common_with. Указывает, что два типа разделяют общий тип.
integral. Указывает, что этот тип целочисленный.
signed_integral. Указывает, что этот тип целочисленный, и имеет знак.
unsigned_integral. Указывает, что этот тип целочисленный и беззнаковый.
floating_point. Указывает, что этот тип плавающей точки.
assignable_from. Указывает, что этот тип можно назначить значением другого типа.
swappable, swappable_with. Указывает, что этот тип может быть заменен, или что два типа можно поменять местами.
destructible. Указывает, что объект этого типа может быть уничтожен.
constructible_from. Указывает, что переменная этого типа может быть построена из набора типов аргументов.
default_initializable. Указывает, что объект этого типа может иметь конструктор по умолчанию.
move_constructible. Указывает, что объект этого типа может быть сконструирован по перемещению.
copy_constructible. Указывает, что объект этого типа может быть сконструирован по копированию и по перемещению.

Вариант 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).
• Вложенные требования (nested requirements).
• Составные требования (compound requirements).
• Требования типа (type 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>
#include < concepts>

template < typename T> concept TinyType = requires ( T t){ sizeof(T) < =4; // простое требование requires sizeof(T) < = 4; // вложенное требование };

template
< typename T>
//requires std::integral< T> || std::floating_point< T> // оператор OR
//requires std::integral< T> && TinyType< T> // оператора AND
requires std::integral< T> && requires (T t){ sizeof(T) < =4; // простое требование requires sizeof(T) < = 4; // вложенное требование }
T add(T a, T b){ return a + b; }

int
main()
{ long long int x{7}; long long int y{5};

add(x,y); return 0; }

Концепции и auto. Концепции также можно использовать для объявления переменной, для параметров и для возвращаемого значения функции.

#include < iostream>
#include < concepts>

// Этот синтаксис ограничивает параметры auto, которые вы передаете
// в функцию, чтобы они удовлетворяли концепции std::integral:
std::integral auto add (std::integral auto a,std::integral auto b){ return a + b; }

int main(){ // А здесь показан пример применения концепции для объявления переменной: std::floating_point auto x = add(5,8); return 0; }

38. Классы. На языке C++ классы (class) это просто средство для создания пользовательских типов (кстати, структуры на C++ также неявно являются классами). Пример класса:

const double PI {3.1415926535897932384626433832795};

class Cylinder { public: // Функции (методы) класса: double volume() { return PI * base_radius * base_radius * height; }

public: // Переменные члены класса (свойства): double base_radius{1}; double height{1}; };

В этом примере члены класса (свойства и методы) помечены спецификатором доступа public. Это значит, что эти члены класса общедоступны. Т. е. метод volume можно вызвать на экземпляре класса, и свойства base_radius и будут доступны для чтения и изменения. По умолчанию (если не указать public_ члены класса получают доступ private, т. е. они будут недоступны за пределами класса. Методы класса всегда имеют доступ к свойствам класса независимо от того, являются ли они private или public.

Свойства класса могут быть либо переменными, либо указателями, но не могут быть ссылками. Причина в том, что ссылки всегда должны быть инициализированы, а для свойств класса важно иметь возможность быть не инициализированными.

39. Конструкторы. Это специальный метод класса, который предназначен выполнять какие-то инициализационные действия. Они вызываются в момент создания экземпляра класса. В предыдущем примере у класса Cylinder не был определен конструктор. Для такого случая компилятор автоматически сгенерирует пустой конструктор. У него не будет параметров, и в его теле не будет никаких действий.

Особенности конструкторов:

• Конструкторы не возвращают какой-либо тип.
• Имя конструктора совпадает с именем класса.
• У конструктора могут быть параметры. Также у них может быть пустой список параметров.
• Конструкторов может быть несколько, у каждого варианта конструктора должен быть уникальный набор параметров.
• Конструкторы обычно используются для инициализации свойств класса или таких специальных действий, как выделение памяти из кучи, инициализация какого-либо оборудования (например, настройка модема) и т. п.

Пример класса Cylinder с конструкторами:

class Cylinder
{
   public: 
      // Конструкторы класса:
      Cylinder(){
         base_radius = 2.0;
         height = 2.0;
      }

Cylinder(double rad_param, double height_param){ base_radius = rad_param; height = height_param; } // Функции (методы) класса: double volume(){ return PI * base_radius * base_radius * height; }

private: // Свойства класса: double base_radius{1}; double height{1}; };

Обратите внимание, что теперь свойства base_radius и double height стали приватными, т. е. теперь к ним снаружи нет никакого доступа (их нельзя прочитать или записать). Эти свойства могут быть установлены только при создании объекта класса: либо значениями по умолчанию, если вызвать конструктор без параметров, либо указанными значениями, если вызвать второй вариант конструктора, у которого есть параметры.

Конструктор по умолчанию. Синтаксис конструктора по умолчанию (иногда его называют пустым конструктором):

#include < iostream>

const double PI {3.1415926535897932384626433832795};

class Cylinder { public : // Конструкторы класса: Cylinder() = default; // конструктор по умолчанию Cylinder(double rad_param,double height_param) { base_radius = rad_param; height = height_param; } // Функции (методы) класса: double volume(){ return PI * base_radius * base_radius * height; }

private : // Переменные - члены класса: double base_radius{1}; double height{1}; };

int
main()
{ Cylinder cylinder1; std::cout << "volume : " << cylinder1.volume() << std::endl; return 0; }

Конструктор по умолчанию нужен в том случае, если вы хотите сохранить его наличие. Дело в том, что если пользователь создал свой конструктор, то компилятор не будет генерировать пустой конструктор. Если вы по какой-то причине хотите отменить это поведение, то с помощью следующего синтаксиса можно сохранить пустой конструктор (на примере класса 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

const double PI {3.1415926535897932384626433832795};

Заголовочный файл cylinder.h:

#pragma once
#include "constants.h"

class Cylinder {
public: // Конструкторы: Cylinder() = default; Cylinder(double rad_param,double height_param); // Публичный метод: double volume(); // Геттеры и сеттеры для доступа к приватным членам класса: double get_base_radius(); double get_height(); void set_base_radius(double rad_param); void set_height(double height_param);

private
: double base_radius{1}; double height{1}; };

Модуль cylinder.cpp, где определена реализация методов класса:

#include "cylinder.h"
Cylinder::Cylinder(double rad_param, double height_param) { base_radius = rad_param; height = height_param; }

double
Cylinder::volume()
{ return PI * base_radius * base_radius * height; }

double
Cylinder::get_base_radius()
{ return base_radius; }

double Cylinder::get_height() { return height; }

void Cylinder::set_base_radius(double rad_param) { base_radius = rad_param; }

void
Cylinder::set_height(double height_param)
{ height = height_param; }

Модуль main.cpp, где находится логика верхнего уровня программы:

#include < iostream>
#include "cylinder.h"

int main() { Cylinder cylinder1(10,10); std::cout << "volume : " << cylinder1.volume() << std::endl; return 0; }

Обратите внимание, что для такой методики в заголовочном файле класса (в данном примере это cylinder.h) не должно оставаться никаких определений методов класса, все эти определения должны быть перенесены в отдельный модуль, и снабжены префиксом из имени класса и двойного двоеточия (в нашем примере это Cylinder::).

Управление объектами класса с помощью указателей. Объекты класса могут быть созданы в стеке либо в глобальной памяти, и тогда к членам экземпляра класса доступ будет осуществляться через точку. Также объекты могут быть созданы в куче с помощью оператора new, доступ к таким объектам будет осуществляться с помощью указателя и ->. Примеры:

#include < iostream>
#include "cylinder.h"

// Создание объекта в глобальной памяти: Cylinder cylinder1;

int main() { // Создание объекта в стеке: Cylinder cylinder2(10,10);

cylinder1.volume(); cylinder2.volume();

// Создание объекта в куче (heap) и получение указателя на него: Cylinder* p_cylinder3 = new Cylinder(11, 20);

// Обращение к объекту с помощью указателей: Cylinder* p_cylinder1 = &cylinder1; Cylinder* p_cylinder2 = &cylinder2;

std::cout << "volume 1: " << (*p_cylinder1).volume() << std::endl; std::cout << "volume 2: " << p_cylinder2->volume() << std::endl; std::cout << "volume 3: " << p_cylinder3->volume() << std::endl; std::cout << "base_rad(cylinder2): " << p_cylinder2->get_base_radius() << std::endl;

delete p_cylinder3; return 0; }

40. Деструкторы. Это специальные методы класса, которые вызываются в момент удаления объекта класса. Эти методы могут понадобиться для выполнения дополнительных действий, связанных с удалением объекта, например для удаления выделения памяти из кучи, или других операций очистки (таких как перенастройка какого-либо оборудования). Пример:

#include < iostream>
#include < string_view>

class Dog {
public: // Декларация конструкторов: Dog() = default; Dog(std::string_view name_param, std::string_view breed_param, int age_param); // Декларация деструктора: ~Dog();

private: std::string name; std::string breed; int * p_age{nullptr}; };

// Реализация пользовательского конструктора:
Dog::Dog(std::string_view name_param, std::string_view breed_param, int age_param) { name = name_param; breed = breed_param; p_age = new int; *p_age = age_param; std::cout << "Dog constructor called for " << name << std::endl; }

// Реализация пользовательского деструктора: Dog::~Dog() { delete p_age; std::cout << "Dog destructor called for : " << name << std::endl; }

void
some_func()
{ Dog* p_dog = new Dog("Fluffy", "Shepherd", 2); delete p_dog; // это вызовет деструктор класса Dog }

int main() { some_func(); std::cout << "Done!" << std::endl; return 0; }

Очевидные случаи вызова деструктора:

• Когда заканчивается локальная область действия объекта (т. е. объект был выделен в стеке, например внутри тела функции).
• Когда с помощью оператора delete освобождается память объекта, выделенная в куче оператором new.

Однако также существуют случаи косвенного вызова деструкторов. Деструкторы могут быть вызваны в не очевидных местах, чего следует избегать:

• Когда объект класса был передан в функцию по значению.
• Когда локальный объект был возвращен из функции (поведение некоторых компиляторов).

Порядок вызова конструкторов и деструкторов. Деструкторы вызываются в обратном порядке вызову конструкторов.

41. Указатель this. Это специальное ключевое слово, которое внутри методов, конструкторов и деструкторов объекта класса указывает не память, занимаемую этим объектом. Ключевое слово this может использоваться, когда параметр метода и свойство класса имеют одинаковое имя:

class Dog
{
public : ... Dog* set_dog_name(std::string_view name) { this->name = name; return this; }

private : std::string name; std::string breed; int * p_age{nullptr}; };

Если метод будет возвращать указатель на объект, то появится возможность вызывать методы цепочкой в одной строке:

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 Cat { std::string m_name; };

Есть важное отличие создания класса через struct, и это отличие единственное: по умолчанию для class поля класса получают уровень доступа private, а для класса, созданного через struct, по умолчанию поля класса получают уровень доступа public.

Мы можем изменить это поведение по умолчанию, используя для класса class модификатор доступа public, а для класса struct модификатор доступа private.

43. Размер объекта класса. Попробуем проанализировать размер объекта следующего класса:

#include < stdint.h>
#include < iostream>

class SomeClass {
public: void do_some() { } void print_info() { std::cout << "name: " << m_name << std::endl; std::cout << "size_of(m_name): " << sizeof(m_name) << std::endl; std::cout << "size_of(this): " << sizeof(this) << std::endl; } char m8{}; short m16{}; int m32{}; size_t m64{}; std::string m_name{"I am center of Universe"}; };

int main() { SomeClass sc;
std::cout << "sizeof(char): " << sizeof(sc.m8) << std::endl; std::cout << "sizeof(short): " << sizeof(sc.m16) << std::endl; std::cout << "sizeof(int): " << sizeof(sc.m32) << std::endl; std::cout << "sizeof(size_t): " << sizeof(sc.m64) << std::endl; std::cout << "sizeof(std::string): " << sizeof(std::string) << std::endl; std::cout << "sizeof(SomeClass): " << sizeof(SomeClass) << std::endl; std::cout << "sizeof(sc): " << sizeof(sc) << std::endl; sc.print_info(); return 0; }

Если запустить эту программу, то будет выведено следующее:

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
#define PERSON_H

#include < string>
#include < iostream>

class Person { friend std::ostream& operator<< (std::ostream& out, const Person& person);
public: Person(); Person(std::string& first_name_param, std::string& last_name_param); ~Person(); // Getters std::string get_first_name() const{ return first_name; } std::string get_last_name() const{ return last_name; }
// Setters void set_first_name(std::string_view fn){ first_name = fn; } void set_last_name(std::string_view ln){ last_name = ln; }

private : std::string first_name{"Mysterious"}; std::string last_name{"Person"}; };

#endif // PERSON_H

Реализация базового класса, файл person.cpp:

#include "person.h"

Person::Person(){ }

Person::Person(std::string& first_name_param, std::string& last_name_param) : first_name(first_name_param), last_name(last_name_param) { }

std::ostream& operator<< (std::ostream& out, const Person& person) { out << "Person [" << person.first_name << " " << person.last_name << "]"; return out; }

Person::~Person() { }

Определение производного класса, файл player.h:

#ifndef PLAYER_H
#define PLAYER_H

#include < string>
#include < iostream>
#include < string_view>
#include "person.h"

class Player : public Person { friend std::ostream& operator<< (std::ostream& out, const Player& player);
public: Player() = default; Player(std::string_view game_param);
private : std::string m_game{"None"}; };

#endif // PLAYER_H

Реализация производного класса, файл player.cpp:

#include "person.h"
#include "player.h"

Player::Player(std::string_view game_param) : m_game(game_param) { //first_name = "John"; Compiler errors //last_name = "Snow"; }

std::ostream& operator<< (std::ostream& out, const Player& player) { out << "Player : [ game : " << player.m_game << " names : " << player.get_first_name() << " " << player.get_last_name() << "]"; return out; }

Пример использования, файл main.cpp:

#include < iostream>
#include "player.h"

int main() { Player p1("Basketball"); p1.set_first_name("John"); p1.set_last_name("Snow"); std::cout << "player : " << p1 << std::endl; return 0; }

С public-наследованием производные классы могут получать доступ к public членам базового класса, но не может обращаться к private членам базового класса. То же самое относится и к friend производного класса. Они могут обращаться к private членам производного класса, но не могут обращаться к private членам базового класса.

Существует также кроме модификаторов доступа public и private существует также и модификатор protected. Он дает доступ на чтение и запись к членам базового класса из производного класса, но не дает доступ к ним извне.

Спецификатор наследования protected. В примере выше для определения производного класса использовался модификатор доступа public:

class Player : public Person
{
   ...

Со спецификатором наследования public для производного класса будет точно такой же доступ к членам класса, как и доступ снаружи к членам для базового класса. Это максимально облегченный способ доступа к элементам базового класса из производного класса:

[Базовый класс]   -->   [Производный класс]

public            -->   public
protected         -->   protected
private           -->   private

Со спецификатором наследования protected для производного класса доступ к членам базового класса будет установлен следующим образом:

class Player : protected Person
{
   ...

[Базовый класс]   -->   [Производный класс]

public            -->   protected
protected         -->   protected
private           -->   private

Также возможно еще и private наследование. С этим типом наследования доступ к членам класса со стороны производного класса будет закрыт. Это самый защищенный вариант для базового класса:

class Player : private Person
{
   ...

[Базовый класс]   -->   [Производный класс]

public            -->   private
protected         -->   private
private           -->   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 размеры объектов будут больше.

Статическое связывание:

CPP course fig06

Динамическое связывание:

CPP course fig07

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

46. Размер полиморфных объектов, или объектов, использующих динамическое связывание.

Динамическое связывание не является бесплатным, это достигается ценой дополнительного расхода памяти.

47. Слайсинг, срез (slicing). Если присвоить объекту базового класса объект дочернего класса, то из дочернего класса будет взято только то, что относится к базовому классу.

CPP course fig08

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:

• Аргументы по умолчанию обрабатываются в момент компиляции, т. е. статически.
• Привязка вызова виртуальных функций (со спецификатором virtual) с помощью полиморфизма осуществляется runtime.
• Если вы используете аргументы по умолчанию с виртуальными функциями, то можете получить странное (ошибочное) поведение полиморфизма.

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

52. Виртуальный деструктор. Если определить деструктор через virtual, то это позволит избежать проблемы удаления дочерних объектов по указателю, у которого тип указателя на базовый объект.

53. dynamic_cast < >(). Это динамическое преобразование типа объекта, которое позволяет сделать следующее:

• Выполнить runtime-трансформацию данных из указателя или ссылки на объект базового класса в объект дочернего класса.
• Выполнить вызов не полиморфных методов объекта дочернего класса.

54. Конструкторы, деструкторы и виртуальные функции. Ни в коем случае не следует пытаться вызывать виртуальные функции из тела конструкторов или деструкторов:

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

55. Чисто виртуальные методы, абстрактный класс. Абстрактный класс это такой класс, где нет реализации виртуальных методов, т. е. они должны быть реализованы в дочерних классах. Побочный эффект - компилятор запретит создавать объекты такого класса.

Определение чисто виртуального класса выглядит довольно непривычно. У такого класса есть pure virtual (чисто виртуальные) методы, тело которых определено как "const = 0;". Пример:

class Shape
{
protected: Shape() = default; Shape(std::string_view description);

public : virtual ~Shape() = default; // Если деструктор не public, то нельзя удалять // объекты по указателю на базовый класс. // Pure virtual функции: virtual double perimeter() const = 0; virtual double surface() const = 0;

private : std::string m_description; };

Получается следующее:

• Если в классе есть хотя бы одна чисто виртуальная (pure virtual) функция, то он становится абстрактным классом.
• Вы не можете создавать объекты абстрактного класса. Если сделать такую попытку, то будет ошибка компилятора.
• Дочерние классы для абстрактного класса должны явно переназначить (override) чисто виртуальные функции базового абстрактного класса. Если они этого не сделали, то тогда эти классы сами станут абстрактными.
• Чисто виртуальные функции не содержат реализации в своем абстрактном классе. Эти функции предназначены для реализации только в дочерних классах.
• Вы не можете вызывать чисто виртуальные функции из конструктора абстрактного класса.
• Конструктор абстрактного класса используется дочерним классом для построения базовой части объекта. Но конструктор абстрактного класса не предназначен для использования извне, потому что мы не можем создавать объекты абстрактного класса.

56. Моделирование интерфейсов с помощью абстрактных классов.

• Абстрактный класс, где есть только чисто виртуальные функции, и отсутствуют переменные члены класса, может использоваться как модель, которая в ООП называется интерфейсом.
• Интерфейс это некая спецификация, которая будет полностью реализована в дочернем классе, но сама спецификация будет находиться в абстрактном классе.

[Ссылки]

1. Курс программирования на C++ от новичка до продвинутого / машинный перевод site:youtube.com.
2. rutura / The-C-20-Masterclass-Source-Code site:github.com.
3. C++ compiler support site:cppreference.com.
4. Visual Studio Code on Linux site:code.visualstudio.com.
5. VSCode FAQ.
6. VSCode: использование файла настроек c_cpp_properties.json.
7. Интерфейс командной строки Visual Studio Code.
8. VScode: как добавить путь поиска подключаемых файлов.
9. Homebrew on Linux site:docs.brew.sh.
10. C++ Operator Precedence site:cppreference.com.
11. Three-way comparison site:cppreference.com.
12. std::left, std::right, std::internal site:cppreference.com.
13. Отладка консольных приложений Linux.
14. Standard library header cctype site:cppreference.com.
15. Программирование шаблонов C++ для идиотов, часть 1.
16. Concepts library Core language concepts site:cppreference.com.

 

Добавить комментарий


Защитный код
Обновить

Top of Page