Функции, модули, классы
Прототип функции, он же называется объявлением функции состоит из трех частей
[тип возвращаемого значения] [имя функции] ([аргументы функции]);
Назначение прототипа функции - предварительное описании функции для компилятора. Оно должно быть размещено до определения самой функции(тело функции). Обычно его размещают в заголовочном файле, если предполагается его использование в других модулях компиляции или в начале модуля компиляции, если функция будет использоваться только в нем.
Пример:
int foo(float a, float b);
int
тип возвращаемого значения
foo
имя функции
a
и b
- аргументы
Если функция не подразумевает возвращаемого значения, то тип возвращаемого значения указывается void. Прототип функции отличается от ее определения отсутствием тела.
#include <iostream>
int sum(int a, int b); // Объявление
int main() {
std::cout << sum(4,5) << std::endl;
return 0;
}
int sum(int a, int b) { // Определение
int result = 0;
result = a + b;
return result;
}
Класс памяти отвечает за размещение и дальнейшее использования переменной. По-умолчанию, переменная имеет класс auto
,
то есть размещена в области видимости, в которой объявлена. Другими словами, это обычная локальная переменная,
существующая только внутри области видимости - внутри функции, модуля, класса. Этот класс не надо указывать, все
переменные без дополнительных модификаторов являются локальными. Важно не путать описание класса памяти auto
и
типа переменной auto
!
extern
указывает на то, что переменная внешняя, то есть ее объявление находится в другом файле.
static
указывает на то, что переменная сугубо внутренняя, доступна только внутри файла. Важной особенностью является то,
что если переменная объявлена static
внутри функции, она просуществует все время выполнения программы.
#include <iostream>
int inc() {
static int x = 0;
x++;
return x;
}
int main() {
std::cout << inc();
std::cout << inc();
std::cout << inc();
return 0;
}
Эта программа выведет 1,2,3;
#include <iostream>
int inc() {
int x = 0;
x++;
return x;
}
int main() {
std::cout << inc() << ",";
std::cout << inc() << ",";
std::cout << inc();
return 0;
}
а если убрать слово static на экран будет выведено 1,1,1
register
устаревший класс памяти, подсказывающий компилятору, что эту переменную следует разместить в отдельном
регистре процессора. Используется для ускорения работы программы, т.к. доступ к регистрам процессора быстрее, чем
доступ к памяти. В настоящее время компиляторы достаточно умны, чтобы разобраться с такими переменными самостоятельно.
Является только подсказкой и ни к чему в настоящее время не обязывает.
thread_local
указывает на то, что переменная относится к потоку, в котором создана, может комбинироваться с static
и extern
Тип возвращаемого значения не обязательно должен быть одним из встроенных. Это может быть структура, класс, в том числе и собственные. Тип возвращаемого значения определяется при ее объявлении.
Аргументы могут передаваться по значению (by value) и по ссылке (by reference). При передаче аргументов по значению внешний объект, который передается в качестве аргумента в функцию, не может быть изменен в этой функции. В функцию передается само значение этого объекта.
#include <iostream>
int inc(int x) {
x++;
return x;
}
int main() {
int x = 0;
std::cout << inc(x) << ",";
std::cout << x;
return 0;
}
Выведет 1,0, т.к. в функцию переменная была педерана по значению, другими словами была передана копия переменной, а не сама переменная. Стоит немного изменить функцию, добавив указание, чтобы компилятор передал переменную по ссылке:
#include <iostream>
int inc(int &x) {
x++;
return x;
}
int main() {
int x = 0;
std::cout << inc(x) << ",";
std::cout << x;
return 0;
}
и программ выведет 1,1, так как операцию внутри фунции мы проводим не с копией переменной, а непосредственно с ней самой. Важный практический смысл заключается в том, что если тип переменной сложный - например, класс или структура - то при передаче переменной по значению будет создаваться ее копия, а это может быть связано с существенными накладными расходами.
Полезно использовать const
при передаче переменной в функцию, чтобы показать, что внутри функции переменную изменять
нельзя.
int inc(const int x) {
x++;
return x;
}
и компилятор выдаст ошибку error: increment of read-only parameter ‘x’
Если функция принимает в качестве агрумента массив, то фактически в эту функцию передается указатель на первый элемент массива, остальное делает за нас компилятор. Как и в случае с указателями нам доступен адрес, по которому мы можем менять значения. Например, записи :
int sum(int x[], int len) {
int sum = 0;
for(int i = 0;i<len;i++) {
sum += x[i];
}
return sum;
}
и
int sum(int *x, int len) {
int sum = 0;
for(int i = 0;i<len;i++) {
sum += x[i];
}
return sum;
}
равнозначны.
И не забываем про слово const
:
int sum(const int x[], const int len) {
int sum = 0;
for(int i = 0;i<len;i++) {
sum += x[i];
x[i] += sum;
}
return sum;
}
выдаст ошибку при компиляции.
В С++ есть возможность не указывать некоторые агрументы функции, если указано какое значение поставить на их место в таком случае. Например:
#include <iostream>
int sum(int x[], int len = 4) {
int sum = 0;
for(int i = 0;i<len;i++) {
sum += x[i];
}
return sum;
}
int main() {
int x[] = {1,2,3,4};
std::cout << sum(x) << std::endl;
return 0;
}
Если не указать значение аргумента len, будет подставлено значение 4. Стоит ли говорить, что если массив будет размера отличного от 4, функция отработает не правильно. Поэтому этот механизм надо использовать с осторожностью. Вот безопасный пример:
#include <iostream>
int sum(const int x[], int len, bool print = false) {
int sum = 0;
for(int i = 0;i<len;i++) {
sum += x[i];
}
if(print) {
std::cout << sum << std::endl;
}
return sum;
}
int main() {
int x[] = {1,2,3,4};
sum(x, 4);
sum(x, 4, true);
return 0;
}
Если для одного из параметров указано значение по-умолчанию, то следующие тоже должны иметь такие значения:
#include <iostream>
int sum(const int x[], bool print = false, int len) {
int sum = 0;
for(int i = 0;i<len;i++) {
sum += x[i];
}
if(print) {
std::cout << sum << std::endl;
}
return sum;
}
int main() {
int x[] = {1,2,3,4};
sum(x, 4);
return 0;
}
Такая запись уже не корректна, о чем компилятор сообщит.
Полиморфизм позволяет объявлять функции с одинаковым именем, но разными аргументами. Это может относится как к типам аргументов, так и к их количеству. В процессе компиляции компилятор сам выберет функцию, которую надо вызывать на основании типов ее аргументов.
#include <iostream>
int add(int a, int b) {
return a+b;
}
double add(double a, double b) {
return a+b;
}
int main() {
int x = 3, y = 5;
int z = add(x,y);
std::cout << z << std::endl;
double alpha = 7.0, beta = 11.0;
double delta = add(alpha, beta);
std::cout << delta << std::endl;
return 0;
}
Стоит обратить внимение на то, что тип возвращаемого значения не влияет на выбор функции. Вот код, на который компилятор выдаст ошибку:
#include <iostream>
int add(int a, int b) {
return a+b;
}
double add(int a, int b) {
return a+b;
}
int main() {
int x = 3, y = 5;
int z = add(x,y);
std::cout << z << std::endl;
return 0;
}
Структуры и классы предназначены для определения пользовательских типов. Классы и структуры могут включать данные-члены и функции-члены, позволяющие описывать состояние и поведение данного типа.
В C++ классы и структуры идентичны, за исключением того факта, что структуры по умолчанию открыты для доступа, а классы — закрыты.
Объявим структуру AStruct и воспользуемся ней:
#include <iostream>
struct AStruct {
AStruct() { x = 0; y = 0; }
int x;
int y;
};
int main() {
AStruct a;
a.x = 4;
a.y = 5;
std::cout << a.x << "," << a.y << std::endl;
return 0;
}
Как видно, у структуры есть конструктор и два члена x
и y
. Мы можем обратиться к ним, изменить их значение. Однако,
стоит поменять struct
на class
:
#include <iostream>
class AStruct {
AStruct() { x = 0; y = 0; }
int x;
int y;
};
int main() {
AStruct a;
a.x = 4;
a.y = 5;
std::cout << a.x << "," << a.y << std::endl;
return 0;
}
И компилятор уже выдаст ошибку о попытке доступа к полю типа private
. Добавление модификатора public
решает проблему:
class AStruct {
public:
AStruct() { x = 0; y = 0; }
int x;
int y;
};
Классы это концепция из объектно-ориентированного программирования, позволяющая пользователю вводить
свои тип данных, объединяя данные и методы работы с ними(инкапсуляция). Это повышает переиспользование кода,
модульность и проч. проч. Смысл классов в объединение данных с методами их обработки и хранения их взаимного состояния.
Важнейшим свойством классов является возможность их наследования. Другими словами класс – это описание множества
объектов программирования (объектов) и выполняемых над ними действий. Например, опишем два класса Point
и Vector
.
#include <iostream>
#include <cmath>
class Point {
protected:
int x;
int y;
private:
void _reset() {
x = 0;
y = 0;
}
public:
void Move(int newX, int newY) {
if(newX >= 0 && newY >= 0) {
x = newX;
y = newY;
}
}
Point() {
_reset();
}
};
class Vector : public Point {
public:
double Length() {
return sqrt(x*x+y*y);
}
Vector() {
std::cout << "Vector created" << std::endl;
}
~Vector() {
std::cout << "Vector destroyed" << std::endl;
}
};
int main() {
Vector v;
v.Move(1,1);
std::cout << v.Length() << std::endl;
return 0;
}
У класса Point
есть два поля x
и y
, которые выражают координаты точки в пространстве. Метод _reset()
устаналвивает
значение координат в начало системы координат, метод Move()
, изменяет координаты точки в пространстве.
Метод _reset()
является приватным, т.к. находится в области private
. Это делает обращение к нему недоступным
из других классов, другими словами это чисто внутренний метод.
Метод Move()
наоборот, объявлен в области public
, которая делает его доступным к использованию снаружи. Это позволяет
обращатся к нему из любой части программы.
Координаты x
и y
объявлены protected
. Он защищённый, внутренний член иерархии классов) — обращения к члену
допускаются из методов того класса, в котором этот член определён, а также из любых методов его классов-наследников.
Обратите внимание: private
поля запрещены к обращению и из наследников!
Наследование по типу protected делает все public-члены родительского класса protected-членами класса-наследника;
Класс Vector
, предназначен для демонстрации наследования. Он наследует от своего родителя Point
понятия
координат и метод Move()
. Однако, у вектора есть понятие длины, которую возвращает метод Lenght()
.
Для классов есть два специальных метода: конструктор - Vector()
и деструктор ~Vector()
.
Конструктор вызывается при создании экземпляра класса, который называется объект(об этом ниже).
Деструктор вызывается при уничтожении объекта.
Понятие инкапсуляции означает объединение данных и методов, которые работают с ними. Объединение изолирует их в некой капсуле, остюда и название.
Инкапсуляция позволяет изолировать данные, хранящиеся в объекте от прямых внешних изменений. Например, в предыдущем
параграфе мы убираем x
и y
в область private
, чтобы изменения координат происходили только с ведома объекта,
методом Move()
, В нем условный оператор if
, не позволяет задать координаты меньше 0, если бы внешний код
имел доступ к полям x
и y
, это было бы возможно. В том числе это избавляет внешний код, от необходимости проверки
корректности координат(если внешний код является "добрым и пушистым" и у него нет цели нам все поломать).
Так же инкапсуляция экономит место:
#include <iostream>
#include <cmath>
class Point {
protected:
int x;
int y;
public:
void Move(int newX, int newY) {
x = newX;
y = newY;
}
Point() {
x = 0;
y = 0;
}
};
class Vector : public Point {
public:
double Length() {
return sqrt(x*x+y*y);
}
Vector() {
std::cout << "Vector created" << std::endl;
}
~Vector() {
std::cout << "Vector destroyed" << std::endl;
}
};
int main() {
Vector v;
int X = 1, Y = 1;
if(X >= 0 && Y >= 0) {
v.Move(X, Y);
}
std::cout << v.Length() << std::endl;
X = 17; Y = 12;
if(X >= 0 && Y >= 0) {
v.Move(X, Y);
}
std::cout << v.Length() << std::endl;
return 0;
}
То есть, возложив проверку на внешний код, мы вынудили его каждый раз перед Move()
проверять корректность новых
координат. Код ниже - компактнее:
#include <iostream>
#include <cmath>
class Point {
protected:
int x;
int y;
public:
void Move(int newX, int newY) {
if (newX >= 0 && newY >= 0) {
x = newX;
y = newY;
}
}
Point() {
x = 0;
y = 0;
}
};
class Vector : public Point {
public:
double Length() {
return sqrt(x*x+y*y);
}
Vector() {
std::cout << "Vector created" << std::endl;
}
~Vector() {
std::cout << "Vector destroyed" << std::endl;
}
};
int main() {
Vector v;
int X = 1, Y = 1;
v.Move(X, Y);
std::cout << v.Length() << std::endl;
X = 17; Y = 12;
v.Move(X, Y);
std::cout << v.Length() << std::endl;
return 0;
}
Такой код проще поддерживать и расширять. Например, если в нашем виртаульном мире точек и векторов все-таки станут
возможными кооридинаты меньшие 0, то проверку надо будет убрать только в одном месте - в методе Move()
, а не
в двух(тысячах ;)) мест во внешнем коде.
Объект это экземпляр класса. То есть это переменная имеющая тип, соотвествующий некоторому классу. В примере с точками
и векторами, переменная v
это объект класса Vector
, которая позволяет нам работать с методами и данными, которые
инкапсулированны в класс Vector
. В данном случае объект создается при объявлении переменной Vector v;
, но это
не всегда удобно, поэтому существует способ динамического создания объекта с помощью вызова new
.
Вызов new
вызывает конктруктор класса и возвращает указатель на созданый объект для дальнейшего использования.
#include <iostream>
class Point {
int x;
int y;
public:
Point() {
std::cout << "Creating point" << std::endl;
x = 0;
y = 0;
}
};
int main() {
Point *p = new Point();
std::cout << p << std::endl;
return 0;
}
В этом коде мы объявляем класс Point
, затем в функции main()
создаем переменную типа
"указатель на объект класса Point" и присваиваем ему значение, которое возвращает вызов new
. Указатель на объект
может существовать сам по себе и не обязательно указывать на какой либо объект. По-умолчанию, переменная типа указатель
в С++ вообще никак не инициализируется, поэтому при объявлении ее надо либо сразу же заполнить указателем на объект,
либо прировнять к nullptr, чтобы в дальнейшем можно было понять, что она ни на что не указывает.
У С++ есть неприятная особенность при работе с объектами, обращение к членом объекта различается в зависимости от того
работаем мы через указатель на объект или через объект. При работе с переменной типа "объект" обращение происходит с
помощью символа .
, а если работа идет с указателем на объект, то обращение осуществляется через ->
. Пример, ниже:
#include <iostream>
class Point {
int x;
int y;
public:
void print(){
std::cout << x << ":" << y << std::endl;
}
Point() {
std::cout << "Creating point" << std::endl;
x = 0;
y = 0;
}
};
int main() {
Point *pPoint = nullptr;
pPoint = new Point();
pPoint->print();
Point point;
point.print();
return 0;
}
Указатели на объекты это обычные переменные, которые можно приравнивать друг другу и передавать в функции.
Конструктор предназначен для инициализации объекта и вызывается автоматически при его создании. Ниже перечислены основные свойства конструкторов.
- Конструктор не возвращает значение, даже типа void.
- Нельзя получить указатель на конструктор.
- Класс может иметь несколько конструкторов с разными параметрами для разных видов инициализации (при этом используется механизм перегрузки).
- Конструктор, вызываемый без параметров, называется конструктором по-умолчанию.
- Параметры конструктора могут иметь любой тип, кроме этого же класса. Можно задавать значения параметров по-умолчанию. Их может содержать только один из конструкторов.
- Если программист не указал ни одного конструктора, компилятор создает его автоматически. Такой конструктор вызывает конструкторы по умолчанию для полей класса и конструкторы по умолчанию базовых классов. В случае, когда класс содержит константы или ссылки, при попытке создания объекта класса будет выдана ошибка, поскольку их необходимо инициализировать конкретными значениями, а конструктор по-умолчанию этого делать не умеет.
- Конструкторы не наследуются.
- Конструкторы нельзя описывать с модификаторами const, virtual и static.
- Конструкторы глобальных объектов вызываются до вызова функции main. Локальные объекты создаются, как только становится активной область их действия. Конструктор запускается и при создании временного объекта (например, при передаче объекта из функции).
Конструктор копирования — это специальный вид конструктора, получающий в качестве единственного параметра указатель
на объект этого же класса: T::T(const Т&) { ... /* Тело конструктора V }
, где Т - имя класса.
Деструктор - это особый вид метода, применяющийся для освобождения памяти, занимаемой объектом. Деструктор вызывается автоматически, когда объект выходит из области видимости:
- для локальных объектов - при выходе из блока, в котором они объявлены;
- для глобальных - как часть процедуры выхода из main;
- для объектов, заданных через указатели деструктор вызывается при использовании операции delete.
Если деструктор явным образом не определен, компилятор автоматически создает пустой деструктор.
Имя деструктора начинается с тильды ~
, непосредственно за которой следует имя класса.
Деструктор:
- не имеет аргументов и возвращаемого значения;
- не может быть объявлен как const или static;
- не наследуется;
- может быть виртуальным
Стандартная библиотека содержит три класса для работы с файлами: ifstream - класс входных файловых потоков; ofstream - класс выходных файловых потоков; fstream - класс двунаправленных файловых потоков.
Эти классы являются производными от классов istream, ostream и iostream соответственно, поэтому они наследуют перегруженные операции << и >>, флаги форматирования, манипуляторы, методы, состояние потоков и т. д. Использование файлов в программе предгюлагает следующие операции:
- создание потока;
- открытие потока и связывание его с файлом;
- обмен (ввод/вывод);
- уничтожение потока;
- закрытие файла.
Каждый класс файловых потоков содержит конструкторы, с помощью которых можно создавать объекты этих
классов различными способами.
- Конструкторы без параметров создают объект соответствующего класса, не связывая его с файлом:
ifstream();
ofstream();
fstream();
- Конструкторы с параметрами создают объект соответствующего класса, открывают файл с указагнхым именем
и связывают файл с объектом:
ifstream(const char *name, ios_base::openmode mode = ios_base::in);
ofstream(const char *name, ios_base::openmode mode = ios_base::out | ios_base::trunc);
fstream(const char *name, ios_base::openmode mode = ios_base::in | ios_base::out);
Вторым параметром конструктора является режим открытия файла.
Так же для открытия файла можно использовать функцию open()
, которая есть у потоков. Выглядят они аналогично конструкторам:
void open (const char* filename, ios_base::openmode mode = ios_base::in);
void open (const string& filename, ios_base::openmode mode = ios_base::in);
Чтение и запись выполняются либо с помощью операций чтения и извлечения, аналогичных потоковым классам, либо с помощью методов классов.
Закрытие файлов осуществляется методом close() у соответствующуего объекта.
#include <iostream>
#include <fstream>
int main() {
char text[81], buf[81];
std::cout << "Введите имя файла:";
std::cin >> text;
// Открываем с помощью конструктора
std::ifstream f(text, std::ios_base::in);
if (!f.is_open()) {
std::cout << "Ошибка открытия файла";
return 1;
}
while (!f.eof()) {
// Читаем построчно методом getline()
f.getline(buf, 81);
std::cout << buf << std::endl;
}
// Закрытие файла
f.close();
// Открываем с помощью метода open
f.open(text, std::ios_base::in);
if (!f.is_open()) {
std::cout << "Ошибка открытия файла";
return 1;
}
while (!f.eof()) {
// Читаем оператором извлечения
f >> buf;
std::cout << buf << std::endl;
}
f.close();
return 0;
}
???
Самый простой способ связать множество элементов - сделать так, чтобы каждый элемент содержал ссылку на следующий. Такой список называется однонаправленным (односвязным). Недостатоком односвязного списка является то, что скнирование по списку возможно только в одном направлении - от начала до конца, т.к. в каждом элементе храниться указатель только на следующий.
Простейший элемент однонаправленного списка:
struct Node
{
int d; // данные
Node *р; // Указатель на следующий элемент
}
Здесь в качестве данных, хранящихся в элементе используется int d
. Node *p
указывает на следущий элемент списка.
Обычно в программе обязательно храниться указатель на первый элемент списка, например: struct Node *head
. Благодаря
этому программа всегда знает с какого элемента начинать при сканировании списка. У последнего элемента указатель на
следующий элемент, хоть и присутствует, равен nullptr. При сканировании это указывает на последний элемент.
#include <iostream>
struct Node
{
int d; // данные
Node *next; // Указатель на следующий элемент
};
int main() {
Node *head = nullptr; // Пока ничего нет. Список пуст
head = new Node; // Создадим структуру
head->d = 1; // Зададим некую полезную нагрузку
head->next = nullptr; // Следующий элемент null, т.к. больше в списке ничего нет
// Теперь head указывает на первый элемент списка. Он же и последний.
Node *e = head; // Подготовка к сканированию. Создадим переменную, указывающую на начало списка.
while(e != nullptr) { // сканируем до тех пор, пока элемент списка не nullptr
std::cout << e->d << std::endl;
e = e->next; // Присвоим переменной e, значение указателя на следующий элемент.
}
return 0;
}
21. Базовые операции с однонаправленным линейным списком: создание пустого списка, удаление всего списка
#include <iostream>
struct Node
{
int d; // данные
Node *next; // Указатель на следующий элемент
};
struct List { // Структура для списка
Node *head; // Указатель на первый элемент списка. nullptr если список пуст
Node *tail; // Указатель на последний элемент списка. Используется для быстрого добавления в конец.
};
// Создание пустого списка
List *create_list() {
List *result = new List; // Создадим указатель на структуру списка
result->head = nullptr; // Покажем, что он пустой
return result;
}
// Добавление элемента в списк
void add_node(List *list, int data) {
Node *newNode = new Node;
newNode->d = data;
newNode->next = nullptr;
if(list->head == nullptr) { // Список пустой
list->head = newNode; // Новый элемент будет первым
} else {
list->tail->next = newNode; // В текущем последнем элементе списка указываем, что теперь последний - вновьсозданый.
}
list->tail = newNode; // Присваиваем указатель на новый элемент сохраненному в структуре списка.
}
// Удаление элемента из списка
void del_node(List *list, Node *node) {
if( node == list->head ) { // Наш элемент оказался первым в списке
list->head = node->next; // Теперь первым стал следущий элемент.
delete node; // Удалили объект по указателю node
return;
}
// Придется просканировать весь список в поисках нужного элемента
Node *e; // Создадим временную переменную
e = list->head; // Присвоим ей значение указателя на первый элемент списка
while(e->next != node) { // Перемещаемся по списку пока указатель на следующий элемент не будет указывать на наш
e = e->next; // Присвоим временной переменной значение указателя на следующий элемент
}
// Если желаемый элемент последний, надо присовить tail значение предпоследнего узла
if(node == list->tail) {
list->tail = e;
}
// В элементе для которого следующий элемент искомый заменим значение указателя на следующий элемент на
// элемент, являющийся следующим для искомого(до этого этот указатель показывал на искомый элемент
e->next = node->next;
delete node; // Собственно уничтожим объект
}
// Поиск узла, с некоторыми данными
Node *find_node(List *list, int value) {
Node *e = list->head; // Создадим временную переменную и присвоим ей значение указателя на первый элемент
while(e != nullptr) { // Будем сканировать пока элемент не nullptr
if( e->d == value) { // Если значение элемента равно искомому - прерываем сканирование
break;
}
e = e->next; // Присвоим временной переменной значение следующего элемента относительно текущего
}
// Если выход из цикла произведен по if(e->d==value) - значит мы нашли нужный элемент и указатель на него
// находится в e.
// Если искомый элемент не найден e будет содержать nullptr. Это нормальный способ показать, что элемента нет
return e;
}
// Распечатаем элементы списка
void print_list(List *list) {
Node *e = list->head;
do {
std::cout << e->d << " ";
e = e->next;
} while(e!= nullptr);
std::cout << std::endl;
}
void destroy_list(List *list) {
Node *e = list->head; // Создадим временную переменную и присвоем ей указатель на первый элемент списка
do { // Сканируем до тех пор, пока e не последний элемент
Node *node_to_del = e; // Сохраним указатель на текущий элемент в node_to_del
e = e->next; // Сохраним в e указатель на следующий элемент
delete node_to_del; // Удалим объект
} while(e != nullptr);
delete list; // Освободим память, выделенную под структуру списка
}
int main() {
List *list = create_list(); // Создадим структуру для списка
add_node(list, 1); // Добавляем элемент со значением 1
add_node(list, 2); // 2
add_node(list, 3); // 3
add_node(list, 4); // 4
add_node(list, 5); // 5
print_list(list);
Node *n = find_node(list, 8); // Найдем узел со значением 8
if(n != nullptr) { // Если из функции вернулось nullptr, значит узел не найден. Его там нет, мы его не создавали.
del_node(list, n);
}
print_list(list);
n = find_node(list, 4); // Найдем узел со значением 4
if(n != nullptr) { // В этот раз найдется и в n, будет указатель на искомый элемент
del_node(list, n); // Удалим этот элемент из списка
}
print_list(list);
n = find_node(list, 1); // Найдем узел со значением 1
if(n != nullptr) { // Он тут есть. И мы праверим, как у нас удаляется "головной" элемент
del_node(list, n); // Удаляется, конечно, как и любой другой. Теперь "голова" списка показывает на элемент 2
}
print_list(list);
n = find_node(list, 5); // Проверим, что будет если мы захотим удалить последний элемент
if(n != nullptr) {
del_node(list, n); // Мы это предусмотрели, так что все будет в порядке
}
print_list(list);
add_node(list, 88); // На всякий случай добавим в конце еще один элемент
print_list(list);
destroy_list(list); // Убьем остатки списка
print_list(list); // Ай ай ай, не надо так. Списка уже нетЪ. Выведет мусор или упадет
return 0;
}