Библиотека классов

Занялся последним проектом-упражнением к главе 13 «Многофайловые программы» учебника Лафоре (стр.639):

4. Создайте библиотеку классов, моделирующую какую-нибудь интересующую вас предметную область. Создайте main() или клиентскую программу для ее тестирования. Предложите свою библиотеку классов на рынке, станьте богатым и знаменитым.


Вроде, ничего сложного, однако в этой главе Лафоре очень обтекаемо пишет о библиотеке классов. Это понятие требует более подробного описания в упражнении, иначе не очень-то понятно, что именно требуется от ученика.

Кстати, пока гуглил, наткнулся на коллегу по запросу. Некий юрист искал четкое определение данного термина в интернете для того, чтобы включить его в судебный иск (понятно, что в суде от сторон требуется четкость в определениях):
https://softwareengineering.stackexchange.com/questions/249887/what-is-a-class-library

Лучшее, что ему смогли посоветовать, это следующее:

A class library is a library containing classes.

Как говорится, idem per idem. С другой стороны, может, это и правильно: чем проще, тем понятнее.

На самом деле, библиотекой классов называют разное в зависимости от контекста. В нашем случае, во-первых, это контекст языка C++. Библиотека классов может содержать от одного до множества классов. Одна библиотека классов может распространяться в виде одного или множества файлов.

Обычно библиотека классов состоит из интерфейса (заголовочный файл .h с определениями классов, в которых методы только объявлены) и реализации (один или несколько файлов с определениями методов классов).

Реализация может распространяться:


  • в текстовом исходном файле (файлах) .cpp;

  • в объектном файле (файлах) .obj (или с другим расширением в зависимости от операционной системы);

  • в файле .lib (или с другим расширением в зависимости от операционной системы);

  • в файле .dll (или с другим расширением в зависимости от операционной системы).

Лафоре практически не касается (по крайней мере, в главах 1-13) двух последних пунктов (.lib и .dll), возможно, потому, что, как пишут, эти форматы файлов не оговорены в стандарте языка C++ (это выходит за рамки языка программирования).

Как пишут, формат файла .lib (в операционных системах MS Windows) — это что-то вроде контейнера, который содержит один или более объектных файлов в несжатом виде.

Для создания исполняемого файла приложения программист в первом случае из текстового исходного файла библиотеки классов компилирует объектный файл, который затем компонуется в исполняемый файл. Во втором случае библиотека классов уже содержит объектный файл, поэтому для получения исполняемого файла нужно пройти только второй этап сборки — этап компоновки. В третьем случае файл .lib передается на вход компоновщику, как и во втором случае. Все эти три случая подходят под определение статической библиотеки классов. Это означает, что все классы и функции такой библиотеки будут включены компоновщиком («linker» на английском) в исполняемый файл, независимо от того, используются они в приложении или нет.

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

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

Источники:
https://ru.wikipedia.org/wiki/Библиотека_(программирование)
https://docs.microsoft.com/ru-ru/cpp/build/dlls-in-visual-cpp

Разбор упражнения с лифтами

Я уже разбирал ошибку в примере многофайловой программы elev в 13-й главе учебника Лафоре.

Эта программа является симулятором работы множества лифтов в многоэтажном здании. Лифты откликаются на запросы пассажиров с различных этажей. Каждый лифт может вместить множество пассажиров, каждый из которых может, зайдя в лифт, нажать кнопку любого из этажей. Количество лифтов и этажей в здании задается константами в файле, предоставляемом застройщиком. Я взял значения констант, которые использовались в учебнике: 4 лифта в 20-этажном здании.

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

Но алгоритм работы кабин не идеален. В третьем упражнении-проекте к 13-главе (стр.638-639) приводится пример одного из недостатков алгоритма и предлагается его исправить.

После запуска программы кабины лифтов находятся на первом этаже:


Недостаток алгоритма работы кабин. Пассажиры сначала нажали кнопку «вниз» вызова кабины с 20-го этажа, а через некоторое время кнопку «вниз» вызова кабины с 10-го этажа.


Запросы с этажей «вниз» на картинке обозначены символом «▼» (закрашенный треугольник с вершиной вниз), расположенным между соответствующим номером этажа и первой слева кабиной лифта.

На рисунке видно, что первая кабина лифта (они нумеруются слева направо) двинулась вверх. По алгоритму в данном случае она движется к дальнему запросу на 20-м этаже. Вторая и следующие кабины проводят анализ: между ближайшим запросом с этажа (10-й этаж) и их текущим расположением (1-й этаж) уже есть кабина, движущаяся к запросу с этажа. Значит, следует оставаться на месте.

Только после того, как первая кабина пройдет 10-й этаж и продолжит движение к запросу на 20-м этаже, вторая кабина начнет движение к запросу на 10-м этаже.

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

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

Побился пару дней, пока не сообразил, что кабина должна двигаться, только если количество кабин сверху меньше количества запросов с этажей. Тогда первая кабина начнёт двигаться вверх, потому что сверху нет кабин, а запросов два. Вторая кабина начнёт двигаться вверх, потому что сверху одна кабина, а запросов два. Третья и четвертая кабины останутся на месте, потому что сверху две кабины и два запроса с этажей.

Тот же принцип должен соблюдаться в противоположном направлении. Например, если все лифты находятся на 20-м этаже, а запросы поступили с 1-го и 10-го этажей.

Файлы проекта: elev4.

Жарю минтай с овощами

Я думал, у меня в плане кулинарии — руки из ж... Оказалось, нужен хороший учитель. Приготовил первый рецепт отсюда:

https://www.youtube.com/watch?v=uOV9TPcKX0w

С первого же раза получилась офигенная вкуснятина! Фото моей тарелки:



Канал называется «Кухня наизнанку», автора зовут Оксаной. Больше двух миллионов подписчиков.

Рецепт объясняется за 2 минуты. Ничего лишнего, люблю таких специалистов.

Всё делается на глаз. Если рыба разморожена заранее, а овощи порезаны, сама готовка занимает около 15 минут. Не положил лавровый лист, так как у меня его нет.

Забыл положить соль — она и не нужна, и так вкусно! Любителям ЗОЖ на заметку: рыба и овощи — то, что доктор прописал!

Добавление переменной к пространству имен

Цитата из учебника Лафоре (стр.611 перевода на русский, стр.649 оригинала):

You can place declarations outside a namespace that behave as if they were inside it. All you need is the scope resolution operator and the namespace name:

namespace beta
{
    int uno;
}

int beta::dos;

Here, both uno and dos are declared in the namespace beta.


Интересно, откуда Лафоре это взял?

У меня (компилятор среды «Visual Studio Community 2017») такое не работает. Выдает ошибку:
error C2039: dos: не является членом "beta"

В интернетах пишут, что такого в стандарте C++ нет:
https://www.linux.org.ru/forum/development/11941687
https://www.cyberforum.ru/cpp-beginners/thread1278918.html

Множественное включение заголовочного файла

Перечитываю раздел «Ошибка повторения включений» в учебнике Лафоре (стр.607-608).

Сначала автор напоминает о своей же рекомендации не вписывать в заголовочные файлы определения функций и глобальных переменных (рекомендуется держать в заголовочных файлах только объявления функций и глобальных переменных во избежание нарушения правила одного определения, описанного в предыдущем посте на эту тему; это нарушение возникнет, если заголовочный файл с определениями указанных программных элементов включить в несколько исходных файлов многофайловой программы). Цитата:

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


И тут же, на следующей странице, автор приводит пример (иллюстрирующий ошибку при включении в исходный файл одного и того же заголовочного файла несколько раз), в котором вписывает в заголовочный файл определение глобальной переменной. Цитата:
// файл headtwo.h
int globalVar;

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

Например, в статье википедии, посвященной «include guard» (примерный перевод на русский: «защитное условие во включаемом файле») в этом случае для примера использовано определение структуры:
https://ru.wikipedia.org/wiki/Include_guard

Цитата:
// файл grandfather.h
struct foo {
    int member;
};

В добрый час! (1956), кинофильм

Посмотрел сейчас советский черно-белый художественный фильм «В добрый час!» 1956 года (продолжительность чуть больше полутора часов). Фильм произвел сильное впечатление. По-моему, это кино гораздо лучше тоже хорошего фильма «Москва слезам не верит» 1979 года. Хотя последний имеет гораздо большую популярность. (Сравниваю эти два фильма потому, что сюжет во многом сходный — тоже про приезжих на учебу в Москву студентов, и про местных тоже.)


Андрей (актер Леонид Харитонов) и Алексей (актер Леонид Давыдов-Субоч)

В комментариях на ютубе (ссылка на фильм) зрители, описывая фильм, часто употребляют слова «светлый» и «добрый». Если решите смотреть это кино, не надейтесь на такое содержание. На самом деле, фильм очень злой и недобрый. Возможно, я не прав, потому что пристрастен. У меня в жизни была очень похожая история с поступлением в институт. Моя история была в 90-х, а неприятные впечатления отпечатались в памяти и не поблекли до сих пор. Хоть я не считаю фильм добрым, но оцениваю его очень высоко.

Фильм снят по пьесе Виктора Сергеевича Розова (1913-2004) «В добрый час!» 1955 года. Он же написал сценарий к фильму.

Прочитать пьесу можно здесь:
https://bookscafe.net/read/rozov_viktor-v_dobryy_chas-236514.html#p1

Актеры все сыграли — как на подбор. Причем даже сложно решить, чья роль главная. Вроде бы второстепенные роли — а стоят перед глазами, как живые. Но Леонид Харитонов, конечно, всех переиграл. Резанул неприятный диссонанс при сравнении с современными фильмами. Нынешние и актеры, и сюжеты (хоть российские, хоть западные) находятся где-то далеко внизу, вроде тараканов на дороге у слона.

Сюжет вкратце такой. Деревенский парень Алексей со своими друзьями, только что окончившими школу, едет в Москву поступать на учебу в институт. Хочет выучиться на агронома. Его мать письмом просит своего брата-профессора приютить племянника в своей новенькой квартире. Алексей едет второпях, не дождавшись согласия родственников из Москвы.

И чуть не оказывается на улице, потому что жена родственника не очень хочет принимать у себя плохо знакомого человека, за которым, возможно, потребуется присмотр. А ведь у нее уже есть два сына, которые еще живут с родителями — старший Аркадий 28 лет и младший Андрей 17 лет.

Андрей и его школьные друзья тоже только что закончили школу и собираются поступать в институт. Благодаря заступничеству Андрея Алексей всё же поселяется в квартире у своего дяди.

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

Честно говоря, очень хотелось бы второй серии. Хочется узнать, что же всё-таки сталось с Алексеем и Андреем дальше. Но, насколько я знаю, продолжения этого фильма не было.

C++: правило одного определения, extern

Продолжение, начало тут:
1. 13 глава Лафоре, обязательность main, С++;
2. С++: объявление и определение переменной.

В C++ существует правило одного определения (по-английски «One Definition Rule», сокращенно «ODR»):
https://ru.wikipedia.org/wiki/Правило_одного_определения

Это правило говорит о том, что в программе не должно быть двух и более определений одного и того же программного элемента (переменной, функции, класса). Например, следующий код (компилятор среды «Visual Studio Community 2017»):
int someVar;
int someVar; // ошибка
послужит причиной ошибки при компиляции:
error C2086: int someVar: переопределение

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

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

Попытаемся наладить межфайловое взаимодействие через переменную на примере. У нас есть проект из двух файлов. В одном файле определим и инициализируем переменную:
// file_A.cpp
int someVar = 3;
В другом файле попытаемся ее использовать:
// file_B.cpp
#include <iostream>
using namespace std;
int main()
{
    someVar = someVar + 7;   // ошибка
    cout << someVar << endl; // ошибка
}
При сборке этого проекта получим ошибку:
error C2065: someVar: необъявленный идентификатор

Попробуем определить нашу переменную и во втором файле, чтобы ее можно было использовать:
// file_A.cpp
int someVar = 3;
// file_B.cpp
#include <iostream>
using namespace std;

int someVar; // ошибка

int main()
{
    someVar = someVar + 7;
    cout << someVar << endl;
}
и при сборке проекта снова получим ошибку, но теперь другую:
error LNK2005: "int someVar" уже определен в file_A.obj

Это и есть нарушение рассмотренного выше правила одного определения для случая многофайловой программы.

Кажется, что тут есть некий логический парадокс: в первый раз при сборке проекта из двух файлов среда программирования заявляет, что, вроде как, не видит переменную someVar. А при второй сборке среда сообщает, что эта переменная уже определена в другом файле!

На самом деле, логика тут есть. Сборка состоит из двух этапов: 1) компиляция (трансляция) в объектный файл каждого исходного файла; 2) компоновка из объектных файлов окончательного исполняемого файла. На первом этапе компилятор «видит» в пределах лишь одного файла, того, который транслируется в данный момент. На втором этапе компилятор «видит» содержимое всех файлов. У нас так получилось, что в первый раз ошибка была найдена на первом этапе сборки, а во второй раз — на втором этапе.

Что же делать, чтобы межфайловое взаимодействие начало работать? Нужно, чтобы не было нарушения правила одного определения. Для этого необходимо во втором файле нашу переменную не определить, а объявить, так как множественные объявления в C++ не запрещены. В предыдущем посте упоминалось, что в большинстве случаев объявление переменной одновременно является и ее определением. Чтобы сделать только объявление ранее определенной переменной, применяется служебное слово extern:
// file_A.cpp
int someVar = 3; // объявление, определение, инициализация
// file_B.cpp
#include <iostream>
using namespace std;

extern int someVar; // только объявление

int main()
{
    someVar = someVar + 7;
    cout << someVar << endl;
}
Этот код у меня собирается и работает без ошибок. На экран выводится число 10.

extern переводится на русский как «внешний». Для нашего примера имеется в виду, что определение переменной размещено в другом (внешнем по отношению к текущему) файле (единице трансляции), а здесь только ее объявление.

Вообще это служебное слово применяется и для других целей:
https://docs.microsoft.com/en-us/cpp/cpp/extern-cpp
https://en.cppreference.com/w/cpp/keyword/extern

С++: объявление и определение переменной

Продолжение, начало тут: 13 глава Лафоре, обязательность main, С++.

Объявлением (по-английски «declaration») переменной называется указание ее типа и имени. Определение (по-английски «definition») переменной — это выделение под нее необходимой памяти.

int someVar;              // не только объявление, но и определение
cout << &someVar << endl; // выведем на экран для проверки адрес переменной
Этот код у меня работает без ошибок (компилятор среды «Visual Studio Community 2017»).

Почему? Казалось бы, я только объявил переменную. Однако, компилятор еще и выделил под эту переменную память, то есть произошло и определение этой переменной. Большинство объявлений переменных является одновременно и определениями.

Интересно, что если переменная глобальная (то есть определена вне тела какой-либо функции и не является членом класса или структуры), то при ее определении она еще считается и инициализированной нулем. Для локальной переменной это не так. Например:
#include <iostream>
using namespace std;

int someVar1;                 // глобальная переменная

int main()
{
    int someVar2;             // локальная переменная

    cout << someVar1 << endl; // выведет на экран число 0
    cout << someVar2 << endl; // ошибка компиляции
}
Текст ошибки будет следующий:
error C4700: использована неинициализированная локальная переменная "someVar2"

Это, видимо, не совсем ошибка, а, скорее, предупреждение. Его можно обойти. Например, так (следующий код у меня собирается без ошибок и работает):
#include <iostream>
using namespace std;

int someVar1;                  // глобальная переменная

int main()
{
    int someVar2;              // локальная переменная

    cout << someVar1 << endl;  // выведет на экран число 0
    cout << &someVar2 << endl;
    cout << someVar2 << endl;  // выведет на экран какое-то значение
}

Но какой в этом смысл? Хорошей практикой в программировании считается всегда инициализировать переменные перед их использованием. Это точно уменьшит число ошибок.

13 глава Лафоре, обязательность main, С++

В 13-й главе учебника Лафоре в разделе «Межфайловое взаимодействие» много мелких кусков кода, иллюстрирующих текст учебника, но они не оформлены в какие-то небольшие примеры, как это было в предыдущих главах. Это, мне кажется, недостаток. Возможно, не хватало места, ведь бумажный учебник к тому моменту, видимо, уже получался слишком толстым. Я решил немного покрутить эти куски кода в компиляторе.

Этот код не выполняет какой-либо работы, так как он нужен лишь для иллюстрации правил объявления и определения программных элементов (переменных, функций, классов). Но нужно помнить, что определение функции main обязательно (в многофайловой программе — в одном из файлов), иначе компилятор (у меня — компилятор среды «Visual Studio Community 2017») выдаст ошибку компоновки:
error LNK2001: неразрешенный внешний символ "_main"
которая автоматически влечет за собой другую ошибку компоновки:
fatal error LNK1120: неразрешенных внешних элементов: 1

Одного объявления функции main тоже недостаточно:
int main();
необходимо определение этой функции (наличие тела функции), хотя бы так:
int main() {}

Тут нужно заметить, что инструкция return для функции main по стандарту не является обязательной. Ее отсутствие эквивалентно return 0;. Подробнее об этом можно прочитать тут:
https://en.cppreference.com/w/cpp/language/main_function

Ошибка в программе симуляции работы лифтов

В 13-й главе учебника Лафоре в качестве примера многофайловой программы приведена программа симуляции автоматической работы четырех лифтов здания в 20 этажей.

Вот, собственно, проблемный код (стр.626) в файле elev.cpp:
void building::record_floor_reqs() // получение запросов от пассажиров с этажей
{
    char ch = 'x';                 // символ для ввода
    
    // ...
    
    cout << "Нажмите [Enter] для вызова лифта: ";
    if( !kbhit() )                 // ожидание нажатия (должен быть CR,
                                   // возврат каретки)
        return;
    cin.ignore(10, '\n');
    if(ch=='\x1B')                 // при нажатии Esc — выход из программы
        exit(0);

    // ...

}
(Я заменил многоточиями код, который не нужен для понимания ошибки.)

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

Но, как оказалось, цикл там есть. Сам этот метод вызывается в бесконечном цикле в другом файле (elev_app.cpp, стр.633):
#include "elev.h" // описатели классов building и elevator

int main()
{
    building theBuilding;

    while(true)
    {
        theBuilding.master_tick();       // запустить такты всех лифтов
        wait(1000);                      // пауза
        theBuilding.record_floor_reqs(); // получить запросы с этажей
    }
    
    return 0;
}
В этом бесконечном цикле программа постоянно вызывает метод building::record_floor_reqs(), который отображает на экране надпись «Нажмите [Enter] для вызова лифта» и проверяет в ветвлении if, не нажал ли пользователь какую-нибудь клавишу на клавиатуре (программа запрашивает нажатие клавиши Enter, но на самом деле можно нажать любую клавишу).

Если пользователь ничего не нажал, работа метода building::record_floor_reqs() завершается и программа возвращается в бесконечный цикл. Затем всё повторяется.

Если пользователь нажал какую-нибудь клавишу, продолжается работа указанного метода и программа переходит к следующим инструкциям:
    cin.ignore(10, '\n');
    if(ch=='\x1B')                 // при нажатии Esc — выход из программы
        exit(0);

Здесь уже видно ошибку. Очевидно, что автор программы предполагал, что в переменной ch окажется значение клавиши, нажатой на клавиатуре. При этом, если бы это оказалась клавиша Esc, то программа должна была завершить свою работу. При нажатии Enter или любой другой клавиши (кроме Esc) метод должен продолжить свою работу.

Однако, в программе отсутствует присвоение переменной ch значения (кроме ее инициализации). Поэтому выхода из такой программы не произойдет вообще.

Вместо совершенно лишней здесь инструкции cin.ignore(10, '\n'); в программу следует добавить инструкцию ch = _getch(); (подробнее про эту функцию). Наверное, автор скопировал откуда-то неправильную инструкцию в спешке. В итоге должно получиться следующее:
void building::record_floor_reqs() // получение запросов от пассажиров с этажей
{
    char ch = 'x';                 // символ для ввода
    
    // ...
    
    cout << "Нажмите [Enter] для вызова лифта: ";
    if( !kbhit() )                 // ожидание нажатия (должен быть CR,
                                   // возврат каретки)
        return;
    ch = _getch();
    if(ch=='\x1B')                 // при нажатии Esc — выход из программы
        exit(0);

    // ...

}

Полный текст данной программы (она состоит из 4-х файлов).