Курс программирования на C++ для новичков и не только
Добавил(а) microsin
Прослушал курс программирования [1]. На мой взгляд, очень полезный курс, особенно для новичков. Лекции короткие и очень понятные. Конечно, многое из того, что рассказано в лекциях, я уже и так знал, но все равно прослушал курс с удовольствием. Особенно много полезного для себя узнал в уроках, посвященных лямбда-функциям и шаблонам функций.
Самым мутными, на мой взгляд, являются темы наследования (inheritance), перезагрузки (overloading, overriding, hiding), полиморфизма, урезания копии дочернего класса (slicing), абстрактные классы, виртуальные функции, лямбда-функции, шаблоны функции, вопросы доступа к элементам класса при наследовании. Слово "мутный" я применил в том смысле, что вряд ли для меня есть смысл практического использования всех этих фич для работы, потому что их применение ИМХО только запутает код и его понимание. Однако ЗНАТЬ о всех этих штуках и иметь о них хотя бы общее представление в настоящее время НЕОБХОДИМО.
Здесь выписал для себя основные тезисы, чего раньше не знал, или знал но забыл, или то, что может оказаться интересным и полезным.
1. Компиляторы, которые нужно установить в Linux: GCC, Clang llvm.
Установка на Ubuntu:
$ sudo apt install gcc
$ sudo apt install clang
Команды g++ --version и clang++ --version покажут версии компиляторов GCC и Clang:
$ g++ --version
g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ clang++ --version
Ubuntu clang version 14.0.0-1ubuntu1.1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Команда gdb --version покажет версию отладчика:
$ gdb --version
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later < http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Последнюю версию компилятора gcc также можно установить с помощью команды brew [9].
2. Фичи, поддерживаемые различными компиляторами, можно посмотреть на страничке [2].
3. Visual Studio Code (VSCode) - удобная и популярная среда разработки. Установка под Ubuntu:
$ sudo apt install code
4. Установка расширений для VSCode доступна на левой панели, по кнопке Extensions (Ctrl+Shift+X):
Расширения, которые стоит установить, если они еще не установлены:
C/C++ for Visual Studio Code
Удобный способ запустить VSCode в текущей папке:
$ code .
Полную инструкцию на все случаи жизни можно посмотреть на страничке [3].
5. Как скачать уроки:
- Перейдите в папку, где вы хотели бы сохранить уроки. Например, ~/MyProjects. - Выполните в этой папке команду git clone https://github.com/rutura/The-C-20-Masterclass-Source-Code.git
После этого в папке ~/MyProjects появится папка The-C-20-Masterclass-Source-Code с файлами уроков.
Настройка среды разработки в среде 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, находится шаблон проекта, на основе которого будут создаваться все проекты уроков (обратите внимание, что пробелы в пути папки экранируются символом обратного слеша \):
Например, чтобы начать разрабатывать новый урок (проект программы), сделайте копию папки "8.C++20 Template Project_all_compilers" под новым именем. После этого кликните правой кнопкой на копию этой папки, и выберите "Открыть с помощью -> Visual Studio Code". Запустится среда разработки VSCode, где в дереве просмотра проекта (EXPLORER) будут видны файлы шаблона c_cpp_properties.json, main.cpp и tasks.json.
После этого изменяйте код main.cpp и добавляйте новые модули исходного кода.
1. Создайте пустую папку для проекта, дайте ей понятное имя, характеризующее выполняемую задачу. Для примера назовем папку MyFirstCppProgram.
2. Кликните правой кнопкой на папке MyFirstCppProgram, и выберите "Открыть с помощью -> Visual Studio Code". Запустится среда VSCode, и откроется окно Welcome.
3. Создайте новый файл main.cpp (меню File -> New File... -> введите имя main.cpp). В редакторе файла main.cpp введите следующий текст:
#include< iostream>
intmain()
{
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:
В предыдущей врезке мы рассмотрели пример простейшей программы и её компиляции с помощью прямого вызова в командной строке компилятора g++. Однако можно настроить и автоматизировать этот процесс, сделав его более удобным, с помощью задач VSCode (Tasks).
1. Выберите в меню Terminal -> Configure Tasks..., среда VSCode найдет компиляторы, которые были установлены на шаге "Компиляторы, которые нужно установить в Linux: GCC, Clang llvm".
Чтобы создать задачу для компиляции текущего файла компилятором GCC (g++), выберите в выпавшем списке пункт "C/C++: g++ build active file". В папке проекта будет автоматически создана папка для настроек .vscode, и в ней будет создан файл настроек для задач tasks.json.
{"version":"2.0.0","tasks":[{"type":"cppbuild","label":"C/C++: g++ build active file","command":"/usr/bin/g++","args":["-fdiagnostics-color=always","-g","${file}","-o","${fileDirname}/${fileBasenameNoExtension}"],"options":{"cwd":"${fileDirname}"},"problemMatcher":["$gcc"],"group":"build","detail":"compiler: /usr/bin/g++"}] }
2. Этот файл настроек можно и нужно поправить, чтобы он стал более универсальным.
label. Пункт настроек label задает текстовую метку, которая будет отображаться в меню задач Terminal -> Run Task..., её можно поменять на любой текст по вашему усмотрению. Например, укажем в метке, что у нас используется стандарт C++ v20:
"label":"C/C++: g++ build std=c++20",
args. Этот пункт настроек задает опции, передаваемые компилятору в его командной строке. Здесь имеет смысл поменять опцию "${file}", которая будет компилировать только текущий файл, и надо будет также добавить опцию -std=c++20. Также можно поменять название результата компиляции, если изменить опцию "${fileDirname}/${fileBasenameNoExtension}". Сделанные изменения выделены жирным шрифтом:
После внесенных изменений будут компилироваться все модули исходного кода, находящиеся в папке проекта, не только текущий открытый файл ("${workspaceFolder}/*.cpp"), будут поддерживаться фичи C++ 20 ("-std=c++20"), и поменяется имя результата компиляции на myprogram.
Выполните все те же самые шаги, что были сделаны в предыдущей врезке для компилятора g++, только в меню создания новой задачи Terminal -> Configure Tasks... выберите в выпавшем списке пункт "C/C++: clang++ build active file". В результате файл настроек задач tasks.json примет примерно такой вид (внесенные изменения выделены жирным шрифтом):
На 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".
Следующие фичи требуют опции компилятора -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 {}; // инициализация переменной нулем intmyValue4(9); // инициализация переменной значением 9
В инициализаторе можно использовать выражение:
int A = 2; int B = 3; int sum {A + B};
Инициализации со скобками () и {} имеют особенности, заключающиеся в неявном преобразовании данных. Например:
// Следующее объявление создаст переменную narrowing_conversion_functional, // и неявно присвоит ей значение 2, отбросив дробную часть. Этот так называемое // "функциональное" преобразование, компилятор при этом не выдаст никаких // сообщений. Вероятно это не то, что вы хотели бы получить: intnarrowing_conversion_functional(2.9);
// Более безопасный вариант сделать то же самое, потому что в этом случае // компилятор выдаст сообщение об ошибке int.ERROR или WARNING: intnarrowing_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. Представление чисел в различных системах счисления и формах:
// Примеры декларации и инициализации переменных плавающей точки.// В примере типа float из-за точности 7 не будут сохранены все// значащие цифры инициализатора:float number1 {1.12345678901234567890f}; // Precision : 7double number2 {1.12345678901234567890}; // Precision : 15longdouble 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 Управляет, показывать или не показывать знак плюса при выводе чисел.
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 {newint}; // Выделение в куче памяти под число int// и присваивание p1 адреса этого числа. int *p2 {newint{12}}; // Выделение в куче памяти под 12 чисел int// и присваивание p2 адреса первого из них.
27. Пример определения массивов с выделением памяти из кучи:
#include< iostream>
intmain(){
constsize_t size{10};
// Различные способы, с помощью которых вы можете декларировать// массивы динамически, и как они могут быть инициализированы.// В массиве salaries будут содержаться неопределенные значения,// т. е. этот массив при создании не инициализируется:double *p_salaries { newdouble[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; return0;
}
Важное замечание: массивы, выделенные динамически, сильно отличаются от массивов, определенных статически или локально в стеке. Например по массивам, выделенным в куче, нельзя использовать итерацию по диапазону (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 = newint[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-ссылка. Ссылку можно определить так, что через неё невозможно будет изменить её переменную:
constint& const_ref_age{age};
// Теперь переменную age нельзя поменять через эту ссылку:
const_ref_age = 59; // на эту строку компилятор выдаст ошибку// Аналог const-ссылки можно сделать с помощью указателя:constint* const const_ptr_to_const_age{&age};
const_ptr_to_const_age = 59; // на эту строку компилятор выдаст ошибку
Обратите внимание, что не бывает const-ссылок на не изменяемую переменную, это не имеет смысла. Поэтому на следующее определение компилятор выдаст ошибку:
constint& 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:
intmax(int a, int b){
std::cout << "int overload called" << std::endl;
return (a>b)? a : b;
}
doublemax(double a, double b){
std::cout << "double overload called" << std::endl;
return (a>b)? a : b;
}
doublemax(int a, double b){
std::cout << "(int,double) overload called" << std::endl;
return (a>b)? a : b;
}
doublemax(double a, int b){
std::cout << "(double,int) overload called" << std::endl;
return (a>b)? a : b;
}
doublemax(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 возвращаемый тип значения. Это указывать необязательно, в этом случае компилятор сам выберет подходящий тип. ; признак завершения лямбда-функции, который делает это утверждение действительным.
Пример вызова лямбда функции с помощью присвоения имени:
Еще пример безымянного вызова лямбда-функции с передачей параметров:
[](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, которые по сути делают одно и то же.
intmax(int a, int b){
return (a > b) ? a : b;
}
doublemax(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;
}
Пример использования этого шаблона функции:
intmain(){
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;
return0;
}
Обратите внимание, что тип T в шаблоне должен поддерживать используемый в теле функции оператор >, иначе компилятор выдаст ошибку. Итак, шаблон функции это не настоящий выполняемый код, это всего лишь шаблон, по которому компилятор автоматически генерирует перегрузки функции, основываясь на типах параметров функции.
37. Дедукция компилятора при обработке типов в шаблоне, и явные аргументы. Здесь под дедукцией понимается алгоритм, который применяет компилятор при генерации реального кода на основе шаблона функции, когда он анализирует аргументы, переданные в шаблон функции.
#include< iostream> #include< string>
template < typename T> T maximum(T a, T b){
return (a > b) ? a : b;
} intmain(){
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); // генерация кода для типа intmaximum(c, d); // генерация кода для типа doublemaximum(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 < > constchar * maximum< constchar*> (constchar* a, constchar* b){
return (std::strcmp(a,b) > 0) ? a : b;
}
Назначение элементов этой специализации, по порядку:
template < >. Обозначает, что это специализация какого-то уже существующего шаблона. У специализации должно совпадать имя функции с именем функции исходного шаблона (в этом примере maximum).
const char * maximum. Обозначает, что специализация будет возвращать тип const char *.
(const char* a, const char* b). Список аргументов специализации.
Как видно, в теле специализации шаблона приведен отдельный, отличающийся алгоритм сравнения строк, который задействует библиотечную функцию std::strcmp. Пример работы шаблона без специализации и со специализацией:
#include< iostream> #include< cstring>
intmain(){
// Обычное использование шаблона функции: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() сгенерируется для типа intauto max_double = maximum(c,d); // функция maximum() сгенерируется для типа doubleauto max_str = maximum(e,f); // функция maximum() сгенерируется для типа string
// А здесь активируется отдельная специализация шаблона:constchar* g{"wild"};
constchar* h{"animal"};
constchar* result = maximum(g,h);
std::cout << "max(const char*) : " << result << std::endl;
return0;
}
Стандартные концепции шаблона функции. Концепция шаблона является одной из четырех основных нововведений стандарта C++ 20. Это специальный механизм наложения ограничений на типы параметров шаблона. Например, мы таким образом можем наложить ограничение, чтобы наша функция вызывалась только с числами типа int. И если вы вызовете её с чем-то, что не является целым числом, то компилятор сообщит вам об этом.
Следует заметить, что ограничение в виде явной проверки типов можно реализовать и альтернативными методами, например с помощью assert, однако концепция шаблона позволяет это сделать более удобно и синтаксически красиво. Ниже показан альтернативный способ ограничения на тип данных, реализованный с помощью static_assert.
template < typename T> voidprint_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> voidprint_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, указание концепции вместе с параметрами шаблона.
autoadd(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 синтаксиса. Пример требования, чтобы параметр шаблона был целочисленным:
Здесь 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;
};
Можно еще и так:
autoadd_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 синтаксиса. Здесь требуется, чтобы параметр можно было инкрементировать:
Существует 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:requiressizeof(T) < = 4;
};
[Составное требование]
Пример:
template < typename T> concept Addable = requires (T a, T b){
// Составное требование (здесь noexcept использовать необязательно):
{a + b} noexcept -> std::convertible_to< int>;
};
В этом составном требовании делается проверка, что a + b это совместимый синтаксис, с помощью noexcept отключаются исключения (это не обязательно), и еще делается проверка, что результат можно преобразовать в int (также не обязательно).
Логические комбинации концепций. Концепции также можно объединять друг с другом, с помощью операторов && и ||.
Концепции и auto. Концепции также можно использовать для объявления переменной, для параметров и для возвращаемого значения функции.
#include< iostream> #include< concepts>
// Этот синтаксис ограничивает параметры auto, которые вы передаете // в функцию, чтобы они удовлетворяли концепции std::integral: std::integral autoadd(std::integral auto a,std::integral auto b){
return a + b;
}
intmain(){
// А здесь показан пример применения концепции для объявления переменной:
std::floating_point auto x = add(5,8);
return0;
}
38. Классы. На языке C++ классы (class) это просто средство для создания пользовательских типов (кстати, структуры на C++ также неявно являются классами). Пример класса:
constdouble PI {3.1415926535897932384626433832795};
classCylinder
{
public:
// Функции (методы) класса:doublevolume(){
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 не был определен конструктор. Для такого случая компилятор автоматически сгенерирует пустой конструктор. У него не будет параметров, и в его теле не будет никаких действий.
Особенности конструкторов:
• Конструкторы не возвращают какой-либо тип. • Имя конструктора совпадает с именем класса. • У конструктора могут быть параметры. Также у них может быть пустой список параметров. • Конструкторов может быть несколько, у каждого варианта конструктора должен быть уникальный набор параметров. • Конструкторы обычно используются для инициализации свойств класса или таких специальных действий, как выделение памяти из кучи, инициализация какого-либо оборудования (например, настройка модема) и т. п.
private:
// Свойства класса:double base_radius{1};
double height{1};
};
Обратите внимание, что теперь свойства base_radius и double height стали приватными, т. е. теперь к ним снаружи нет никакого доступа (их нельзя прочитать или записать). Эти свойства могут быть установлены только при создании объекта класса: либо значениями по умолчанию, если вызвать конструктор без параметров, либо указанными значениями, если вызвать второй вариант конструктора, у которого есть параметры.
Конструктор по умолчанию. Синтаксис конструктора по умолчанию (иногда его называют пустым конструктором):
#include< iostream>
constdouble PI {3.1415926535897932384626433832795};
classCylinder
{
public :
// Конструкторы класса:Cylinder() = default; // конструктор по умолчаниюCylinder(double rad_param,double height_param)
{
base_radius = rad_param;
height = height_param;
}
// Функции (методы) класса:doublevolume(){
return PI * base_radius * base_radius * height;
}
Конструктор по умолчанию нужен в том случае, если вы хотите сохранить его наличие. Дело в том, что если пользователь создал свой конструктор, то компилятор не будет генерировать пустой конструктор. Если вы по какой-то причине хотите отменить это поведение, то с помощью следующего синтаксиса можно сохранить пустой конструктор (на примере класса 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
constdouble PI {3.1415926535897932384626433832795};
Заголовочный файл cylinder.h:
#pragma once #include"constants.h"
classCylinder
{ public:
// Конструкторы:Cylinder() = default;
Cylinder(double rad_param,double height_param);
// Публичный метод:doublevolume();
// Геттеры и сеттеры для доступа к приватным членам класса:doubleget_base_radius();
doubleget_height();
voidset_base_radius(double rad_param);
voidset_height(double height_param); private:
double base_radius{1};
double height{1};
};
Модуль cylinder.cpp, где определена реализация методов класса:
Обратите внимание, что для такой методики в заголовочном файле класса (в данном примере это cylinder.h) не должно оставаться никаких определений методов класса, все эти определения должны быть перенесены в отдельный модуль, и снабжены префиксом из имени класса и двойного двоеточия (в нашем примере это Cylinder::).
Управление объектами класса с помощью указателей. Объекты класса могут быть созданы в стеке либо в глобальной памяти, и тогда к членам экземпляра класса доступ будет осуществляться через точку. Также объекты могут быть созданы в куче с помощью оператора new, доступ к таким объектам будет осуществляться с помощью указателя и ->. Примеры:
#include< iostream> #include"cylinder.h"
// Создание объекта в глобальной памяти:
Cylinder cylinder1;
intmain(){
// Создание объекта в стеке:Cylinder cylinder2(10,10);
cylinder1.volume();
cylinder2.volume();
// Создание объекта в куче (heap) и получение указателя на него:
Cylinder* p_cylinder3 = newCylinder(11, 20);
// Обращение к объекту с помощью указателей:
Cylinder* p_cylinder1 = &cylinder1;
Cylinder* p_cylinder2 = &cylinder2;
40. Деструкторы. Это специальные методы класса, которые вызываются в момент удаления объекта класса. Эти методы могут понадобиться для выполнения дополнительных действий, связанных с удалением объекта, например для удаления выделения памяти из кучи, или других операций очистки (таких как перенастройка какого-либо оборудования). Пример:
• Когда заканчивается локальная область действия объекта (т. е. объект был выделен в стеке, например внутри тела функции). • Когда с помощью оператора delete освобождается память объекта, выделенная в куче оператором new.
Однако также существуют случаи косвенного вызова деструкторов. Деструкторы могут быть вызваны в не очевидных местах, чего следует избегать:
• Когда объект класса был передан в функцию по значению. • Когда локальный объект был возвращен из функции (поведение некоторых компиляторов).
Порядок вызова конструкторов и деструкторов. Деструкторы вызываются в обратном порядке вызову конструкторов.
41. Указатель this. Это специальное ключевое слово, которое внутри методов, конструкторов и деструкторов объекта класса указывает не память, занимаемую этим объектом. Ключевое слово this может использоваться, когда параметр метода и свойство класса имеют одинаковое имя:
dog1.set_dog_name("Pumba")->set_dog_breed("Wire Fox Terrier")->set_dog_age(4);
42. Отличия в создании класса через class и через struct. Классы можно создавать как с помощью ключевого слова class, так и с помощью ключевого слова struct:
classDog
{
std::string m_name;
};
structCat
{
std::string m_name;
};
Есть важное отличие создания класса через struct, и это отличие единственное: по умолчанию для class поля класса получают уровень доступа private, а для класса, созданного через struct, по умолчанию поля класса получают уровень доступа public.
Мы можем изменить это поведение по умолчанию, используя для класса class модификатор доступа public, а для класса struct модификатор доступа private.
43. Размер объекта класса. Попробуем проанализировать размер объекта следующего класса:
#include< stdint.h> #include< iostream>
classSomeClass
{ public:
voiddo_some(){
}
voidprint_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"};
};
Если запустить эту программу, то будет выведено следующее:
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:
С public-наследованием производные классы могут получать доступ к public членам базового класса, но не может обращаться к private членам базового класса. То же самое относится и к friend производного класса. Они могут обращаться к private членам производного класса, но не могут обращаться к private членам базового класса.
Существует также кроме модификаторов доступа public и private существует также и модификатор protected. Он дает доступ на чтение и запись к членам базового класса из производного класса, но не дает доступ к ним извне.
Спецификатор наследования protected. В примере выше для определения производного класса использовался модификатор доступа public:
classPlayer : public Person
{
...
Со спецификатором наследования public для производного класса будет точно такой же доступ к членам класса, как и доступ снаружи к членам для базового класса. Это максимально облегченный способ доступа к элементам базового класса из производного класса:
[Базовый класс] --> [Производный класс]
public --> public protected --> protected private --> private
Со спецификатором наследования protected для производного класса доступ к членам базового класса будет установлен следующим образом:
classPlayer : protected Person
{
...
[Базовый класс] --> [Производный класс]
public --> protected protected --> protected private --> private
Также возможно еще и private наследование. С этим типом наследования доступ к членам класса со стороны производного класса будет закрыт. Это самый защищенный вариант для базового класса:
classPlayer : 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. Создавать ссылки на объекты производного класса через эти указатели.
3. Вызывать через указатели (или ссылки) методы как базового, так и производного класса.
4. Можно создать функцию, общую для всех производных классов, которая будет в параметре принимать указатель на базовый класс. В зависимости от того, что передано в этом указателе (адрес какого производного класса объект) могут быть предприняты разные действия.
voiddtaw_shape(const Shape& shape){
// Будет вызван правильный метод производного класса,// в зависимости от того, адрес какого объекта производного// класса был передан в параметре shape:
shape->draw();
}
5. Еще одно преимущество полиморфизма - можно хранить указатели на объекты различных производных типов в одной коллекции (массиве), и обрабатывать их однообразно, в одном цикле. Для этого нужно использовать массив указателей на объекты базового класса.
Внимание: если заново присвоить указатель на объект базового класса адресом другого производного класса, то все равно будет вызван метод предыдущего производного класса. Причина в том, что была сделана статическая привязка в момент компиляции программы. Избежать такого нежелательного поведения можно с помощью ключевого слова virtual, представляющего динамическую привязку методов. Ключевое слово virtual добавляется перед определением и декларацией методов класса.
При наследовании: без ключевого слова virtual перед определением метода происходит его статическое связывание, а с ключевым словом virtial - динамическое. Т. е. с использовании virtual размеры объектов будут больше.
Статическое связывание:
Динамическое связывание:
При статическом связывании компилятор будет вызывать методы базового класса по указателю базового класса, которому присвоен адрес объекта дочернего класса. При динамическом связывании компилятор умнее - он может в этом случае вызывать методы объекта дочернего класса.
46. Размер полиморфных объектов, или объектов, использующих динамическое связывание.
Динамическое связывание не является бесплатным, это достигается ценой дополнительного расхода памяти.
47. Слайсинг, срез (slicing). Если присвоить объекту базового класса объект дочернего класса, то из дочернего класса будет взято только то, что относится к базовому классу.
48. Override (переопределение). Ключевое слово override вставляется после имени в определении метода, перед открывающей фигурной скобкой:
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;". Пример:
public :
virtual ~Shape() = default; // Если деструктор не public, то нельзя удалять// объекты по указателю на базовый класс.// Pure virtual функции:virtualdoubleperimeter()const= 0;
virtualdoublesurface()const= 0;
private :
std::string m_description;
};
Получается следующее:
• Если в классе есть хотя бы одна чисто виртуальная (pure virtual) функция, то он становится абстрактным классом. • Вы не можете создавать объекты абстрактного класса. Если сделать такую попытку, то будет ошибка компилятора. • Дочерние классы для абстрактного класса должны явно переназначить (override) чисто виртуальные функции базового абстрактного класса. Если они этого не сделали, то тогда эти классы сами станут абстрактными. • Чисто виртуальные функции не содержат реализации в своем абстрактном классе. Эти функции предназначены для реализации только в дочерних классах. • Вы не можете вызывать чисто виртуальные функции из конструктора абстрактного класса. • Конструктор абстрактного класса используется дочерним классом для построения базовой части объекта. Но конструктор абстрактного класса не предназначен для использования извне, потому что мы не можем создавать объекты абстрактного класса.
56. Моделирование интерфейсов с помощью абстрактных классов.
• Абстрактный класс, где есть только чисто виртуальные функции, и отсутствуют переменные члены класса, может использоваться как модель, которая в ООП называется интерфейсом. • Интерфейс это некая спецификация, которая будет полностью реализована в дочернем классе, но сама спецификация будет находиться в абстрактном классе.