Автомонтирование флешек в Linux

Случилось так, что я отказался от полноценных рабочих столов в пользу оконного менеджера fluxbox. Всё меня устраивает, кроме одного - автоматического монтирования флешек в оконном менеджере, естественно, не предусмотрено. Один мой приятель, на своём сайте разместил статью об автомонтировании флешек через правила демона udev. В этой заметке я напишу о том, как я немного расширил правила, приведённые в указной статье.
Всем Вам известно, что накопители в Linux монтируются в существующий каталог. И каждый, кто монтировал флешки вручную, знает, что перед монтированием нужно создать каталог, затем смонтировать, а после размонтирования - удалить.  Альтернативой может служить заранее созданная куча каталогов, но это засоряет файловую систему и, к тому же, не красиво, с эстетической точки зрения.
Для автоматизации указанных действий, мною был написан следующий скрипт на bash.
#!/bin/bash

# $1 - action: mount or umount
# $2 - device name
# $3 - device file

ACTION=$1
MOUNT_ACTION=mount
UMOUNT_ACTION=umount
DEVICE_NAME=$2
DEVICE_FILE=$3
MOUNT_DIR=/mnt/
MOUNT_CMD=mount
UMOUNT_CMD=umount
MOUNT_OPTS="-o rw,umask=0"
SCRIPT_NAME=devmount


function printUsage() {
echo Usage:
echo "$SCRIPT_NAME $MOUNT_ACTION <device name> <device file>"
echo or
echo "$SCRIPT_NAME $UMOUNT_ACTION <device name>"
}

# $1 - error message
function errorMessage() {
echo -e 'E[31m'<$SCRIPT_NAME> Error: $1
tput sgr0
}

# $1 - warning message
function warningMessage() {
echo -e 'E[33m'<$SCRIPT_NAME> Warning: $1
tput sgr0
}

# $1 - message
function logMessage() {
echo -e 'E[32m'<$SCRIPT_NAME>: $1
tput sgr0
}

# $1 - error message
function errorExit() {
errorMessage "$1"
echo
printUsage
echo
exit 1
}

# $1 - error message
function crashExit() {
errorMessage "$1"
echo
exit 2
}

# $1 - directory path
# returns 1 - true, 0 - false
function isDirectoryEmpty() {
DIR_CONTENT=`ls -A $1 3> /dev/null`
NOT_EMPTY=X$DIR_CONTENT
if [ "$NOT_EMPTY" == "X" ]; then
return 1
else
return 0
fi
}

# $1 - directory name
function deleteDirectory() {
rmdir $1
if [ ! 0 == $? ]; then
warningMessage "couldn't delete $1"
return 1
fi
return 0
}

function mountDevice() {
if [ -d $MOUNT_DIR$DEVICE_NAME ]; then
warningMessage "$MOUNT_DIR$DEVICE_NAME already exist"
isDirectoryEmpty $MOUNT_DIR$DEVICE_NAME
if [ $? == 0 ]; then
crashExit "$MOUNT_DIR$DEVICE_NAME is not empty"
fi
else
mkdir $MOUNT_DIR$DEVICE_NAME
if [ ! 0 == $? ]; then
crashExit "couldn't create $MOUNT_DIR$DEVICE_NAME"
fi
fi
$MOUNT_CMD $DEVICE_FILE $MOUNT_DIR$DEVICE_NAME $MOUNT_OPTS
if [ ! 0 == $? ]; then
deleteDirectory $MOUNT_DIR$DEVICE_NAME
crashExit "couldn't mount device"
else
logMessage "mount success"
fi
}

function umountDevice() {
$UMOUNT_CMD $MOUNT_DIR$DEVICE_NAME
if [ $? == 0 ]; then
logMessage "unmount success"
deleteDirectory $MOUNT_DIR$DEVICE_NAME
else
crashExit "unmount failure"
fi
}

case $ACTION in
$MOUNT_ACTION)
mountDevice
;;

$UMOUNT_ACTION)
umountDevice
;;

*)
errorExit "unknown parametr "$ACTION
;;
esac
Этот скрипт создаёт в каталоге /mnt каталог с переданным именем и монтирует в него указанное устройство. Если каталог существует, скрипт выдаст предупреждение, а если существующий каталог не пуст - ошибку.
Этот скрипт я и скормил udev`у, как сказано в статье.
SUBSYSTEM=="block", KERNEL=="sd[c-z][0-9]", ACTION=="add", RUN+="/usr/local/bin/devmount mount flash-%k /dev/%k"
SUBSYSTEM=="block", KERNEL=="sd[c-z][0-9]", ACTION=="remove", RUN+="/usr/local/bin/devmount umount flash-%k"
Теперь при вставке флешки, она у меня автоматически монтируется, а при вытаскивании - размонтируется. Кроме того, скрипт позволяет проводить эти операции вручную, что необходимо, если Вы не уверены в безопасности извлечения флешки в данный момент.

C#, кто он?

Не так давно я начал осваивать нынче сверхпопулярный, среди windows программистов, язык программирования C#. Осваивать я его начал, естественно, не просто так, а с корыстными целями, но об этом я рассказывать не буду. Нельзя сказать, что до этого я его в глаза не видел, писал я маленький тестик. Теперь же, после недельного знакомства с этим языком, я хочу поделиться своими впечатлениями.Сразу хочу предупредить, что всё, написанное в этой статье, является моим субъективным мнением.

Платформа .NET

Для работы программ, созданных на платформе .NET, то есть всех программ на языке C#, требуется виртуальная машина .NET. Платформа .NET должна была создать конкуренцию платформе Java, но разработчики Microsoft остановились на поддержке только операционных систем семейства Windows, похоронив тем самым кроссплатформенность. Однако компания Novell разрабатывает альтернативную кроссплатформенную реализацию .NET и C#. Этот проект получил название Mono.
Даже несмотря на то, что Microsoft предоставила Novell все права, проект Mono остаётся догоняющим. По этой причине проектирование Mono сильно отстаёт. Например, WPF в проекте Mono даже и не собираются реализовывать.Исходя из вышесказанного, нетрудно предположить, что мы можем либо писать только под Windows, либо принять ограничения Mono и жить в их рамках.
Но не всё так плохо, того, что реализовано проектом Mono достаточно для создания полноценных приложений. Открытым остаётся вопрос, а согласятся ли компании, создающие ПО на C#, на такие ограничения?
Проект .NET создавался сразу с несколькими целями, давайте разберёмся, что это за цели и каков результат.

Замена устаревшим технологиям COM и ActiveX

Позволю себе напомнить о том, что такое COM и ActiveX. Технология COM служит для межпроцессного, внутрепроцессного и сетевого взаимодействия объектно-ориентированных модулей. Взаимодействие строится на понятиях виртуальной таблицы (VTBL) и интерфейса класса. Приложение, которое хочет воспользоваться объектом другого модуля, просит этот модуль создать ему экземпляр какого-либо класса и, получив его, инициализирует специальным образом, заранее известный, интерфейс. Системой маршалинга COM вызовы виртуальных методов интерфейса преобразуются в корректные вызовы и все остаются довольны. Технология ActiveX - это надстройка над COM, которая служит для работы с графическим интерфейсом, обеспечивая такие понятия как компонент, узел компонента и обмен сообщениями.
Итак, как же технология .NET решает те же проблемы, что и COM. Во-первых .NET вводит понятие сборки (assembly), которая является её функциональной частью. Сборка представляет из себя бинарный модуль, хранящий полную информацию о том, что он содержит: пространства имён, зависимости, типы, методы итд. Любая сущность, объявленная в сборке как public автоматически становится доступной из других сборок. При том все события и делегаты остаются в рабочем состоянии, а виртуальная машина .NET обеспечивает, как и в случае с COM, все межпроцессные, внутрипроцессные и сетевые взаимодействия. Таким образом, отпадает необходимость в виртуальных таблицах, а значит и работа с графическими компонентами становится неотличимой от простого взаимодействия.
С первой задачей .NET справился, но на этом создатели не остановились. Раз уж нам известно всё о типах, находящихся внутри сборки, то пользователь этой сборки может расширять все типы, унаследовав их. Технология COM не позволяет наследоваться от классов из скомпилированного модуля.

Мультиязычная поддержка

В рамках платформы .NET реализована технология под названием CLR (Common Language Runtime). Это технология, позволяющая работать с платформой .NET из других языков программирования. Также, предоставляется возможность взаимодействия сборок, написанных на разных языках.
В рамках технологии CLR реализовано множество языков, как самой корпорацией Microsoft, так и другими компаниями. Среди этих языков, такие языки как C#, Visual Basic.NET, C++.NET, F#, IronPython, Delphi, Nemerle и многие другие. Так как я не работал со многими из тех языков, что значатся в списке, поддерживающих CLR, я скажу только о C++.NET. Достаточно справедливо будет отметить, что разработчики компании Microsoft внесли такие изменения в язык C++, что работать с ним стало просто не приятно. Попробовав поработать с C++.NET, я понял, что не выдержу издевательств над своим любимым языком и поспешно удалил тестовый проект. Больше я к нему не возвращался. Получается, что не CLR поддерживает язык, а язык изменили для CLR (то есть подвинули гору к Магомету). Из этого можно сделать вывод, что некоторые языки, как минимум C++ и Visual Basic подгонялись под платформу .NET только для того, чтобы создать изначальный пул языков CLR. Надеюсь, что с другими языками, которые создавались не в такой спешке, подобной истории не случилось.
Подводя итоги для этой цели, можно сказать, что платформа .NET не полностью справилась с поставленной задачей, так как я вынужден использовать не C++, а его модификацию только потому, что разработчикам .NET так захотелось. Но нельзя отрицать тот факт, что поддерживаемых языков, действительно, много, и их количество возрастает.

Язык C#

Давайте же перейдём к виновнику сей статьи - к языку C#. Язык C# - это первый из языков CLR, который создавался с нуля и специально для платформы .NET, поэтому он должен отражать все намерения разработчиков. Язык разрабатывался под сильным влиянием языка Java и должен был стать его конкурентом. В этой статье я не буду писать учебник по этому языку, о нём написано много книг и на msdn есть руководство. Здесь я расскажу о тех конструкциях языка, которые мне понравились и о тех, которые мне не понравились.

Наследование

Как и в Java, в C# запрещено множественное наследование классов, но разрешено множественное наследование интерфейсов. Хорошо это или плохо? Споры по этому поводу не умолкают с тех пор, как придумали наследование. Моё мнение - плохо. Ведь у каждого программиста есть своя собственная голова на плечах и он ею может подумать и сам решить все проблемы, которые несёт неверное использование множественного наследования, а современные компиляторы способны подсказать ему, что в коде возникла путаница. А вот пользы от множественного наследования гораздо больше, чем вреда. Если вспомнить истоки ООП, то основной причиной создания этого подхода было определение типов, соответствующих реальным объектам и предметам. А коли это так, то "диван-кровать" и "компьютер моноблок" являются примерами таких реальных объектов, которые в ООП было бы правильно описывать множественным наследованием (от "дивана" и "кровати" в первом случае и от "системный блок" и "монитор" - во втором).

Упаковка объектов

В C# все типы неявно наследуются от типа System.Object или просто object. Тип object используется для, так называемой, упаковки объектов. Упаковка в синтаксическом виде - это ни что иное, как приведение объекта к типу object
string str = "Hello";
object obj = (object)str;
Надо отметить, что не смотря на все советы Скотта Мэйерса, приведение типов в C# осталось в стиле языка Си.
Итак, что же особенного в данной ситуации. Упаковка объектов в платформе .NET используется повсеместно, что может привести к путанице. Конечно, при неверном приведении среда исполнения выдаст исключение, но мне представляется, что runtime - это уже поздно. Ловить исключения при каждом присвоении смешно, согласитесь.
В качестве примера использования упаковки можно привести операцию клонирования. В интерфейсе IClonable объявлен метод Clone, который возвращает тип object. Служит этот метод только одной цели - создание копии объекта. Давайте посмотрим, на то, как выглядит определение и использование клонирования.
class Sheep : ICloneable
{
private string theName;
private int theAge;

public Sheep(string aName, int anAge)
{
theName = aName;
theAge = anAge;
}

public object Clone()
{
return new Sheep(theName, theAge);
}
}

class Program
{
static void Main(string[] args)
{
Sheep dolly = new Sheep("Dolly", 1);
Sheep dolly2 = (Sheep)dolly.Clone();
}
}
Представляется мне это всё, мягко говоря, не удобным и плохо читабельным.

Копирование объектов

Раз уж я заговорил о клонировании, то добью эту тему до конца. В C# все типы данных поделены на два вида: структурные и ссылочные. Структурные - это те типы, которые мы, C++ программисты, привыкли называть POD (Primitive Old Data) типами (int, float, bool, ...), перчисления и структуры - которые в C# обрели новую жизнь. Все структурные типы являются наследниками типа System.ValueType и память для их объектов всегда выделяется в стеке. Все остальные типы, то бишь классы и интерфейсы, называются ссылочными и наследуются, как уже было сказано, от System.Object или любого производного, кроме System.ValueType. Память для объектов ссылочных типов выделяется в управляемой куче.
До сих пор всё выглядит сносно, но теперь давайте представим всё на деле. Допустим, у нас есть некая структура, содержащая какие-то данные.
struct Data
{
public int theData;

public Data(int aData)
{
theData = aData;
}
}

class Program
{
static void PrintIncrementedData(Data data)
{
Console.Write("Incremental data: {0}n", ++data.theData);
}

static void Main(string[] args)
{
Data data = new Data(50);
PrintIncrementedData(data);
PrintIncrementedData(data);
}
}
Данная программа не выглядит реально, но тем не менее, ведёт себя так, как мне хочется, а именно, выводит такой результат
Incremental data: 51
Incremental data: 51
Теперь давайте представим, что тот, кто сопровождает эту структуру неожиданно переименует её в класс, ну, допустим, ему понадобится конструктор по умолчанию, который запрещено создавать в структурах C#. Тогда вывод нашей программы внезапно приобретает другой вид
Incremental data: 51
Incremental data: 52
Одно слово и наша программа перестала работать так, как положено, но продолжила компилироваться без ошибок и предупреждений. Всё это произошло по одной причине: структурные типы при присваивании и передачи в качестве параметров копируются по значению, а ссылочные, как видно из названия, - копируют только ссылку на объект в памяти.
Не знаю, какой тайный смысл разработчики C# вложили в структуры, но трудно обнаруживаемые ошибки, связанные с их использованием представляются более, чем вероятными.

Константность

Здесь я хотел бы поговорить о таких понятиях, которые с C++ называются константными методами и константными возвращаемыми значениями. Если быть точнее, то возвращаемое значение просто имеет константный тип. Но формулировки не столь важны, в C# нет ни того, ни другого. Методы в C# всегда возвращают ссылку на объет ссылочного типа или копию объекта структурного типа. Чем это чревато. Давайте посмотрим на пример из книги "C# и платформа .NET" Эндрю Троелсена, описанный в третьей главе, в разделе, посвящённом инкапсуляции. Так как этот пример там размазан по нескольким параграфам, я соберу его и немного дополню, смысл от этого не изменится.
class FullName
{
public string firstName;
public string secondName;

public FullName(string aFirstName, string aSecondName)
{
firstName = aFirstName;
secondName = aSecondName;
}
}

class Emploee
{
private FullName theFullName;

public Emploee(FullName aFullName)
{
theFullName = aFullName;
}

public FullName Name
{
get { return theFullName; }
}
}


class Program
{
static void Main(string[] args)
{
Emploee emploee = new Emploee(new FullName("William", "Gates"));
FullName name = emploee.Name;
name.firstName = "Linus";
name.secondName = "Torvalds";
Console.Write("1: {0} {1}n", emploee.Name.firstName,
emploee.Name.secondName);
Console.Write("2: {0} {1}n", name.firstName, name.secondName);
}
}
Не смотря на то, что автор утверждает, что свойство Name доступно только для чтения, я не применяя никаких усилий изменил значение приватной переменной, в чём легко убедиться, скомпилировав и запустив программу.
1: Linus Torvalds
2: Linus Torvalds
Стоит ли упоминать, что подобное поведение может привести к, довольно плачевным, последствиям. Для того, чтобы избежать такой ситуации не остаётся ничего другого, кроме как скопировать объект FullName перед возвратом из свойства.
public FullName Name
{
get { return new FullName(theFullName.firstName,
theFullName.secondName); }
}
Естественно, такой подход гораздо более затратный, нежели возврат константной ссылки в C++.

Делегаты

Увидев в первый раз концепцию делегатов, я пришёл в восторг. Это, действительно, мощная и простая технология позволяет создавать такие конструкции, для которых в C++ применялись колбеки, функторы и обсерверы. Если вкратце, то делегат - это особый вид функции, который объявляется в одном классе, а реализован может быть в другом. Кроме того, один делегат может быть реализован много раз и, при вызове, такого делегата, будут вызваны все его реализации. Давайте посмотрим на примере.
class People
{
public delegate void SpeakDelegate();
public SpeakDelegate Speak;
}

class Man
{
private string theName;

public Man(string aName)
{
theName = aName;
}

public void Speak()
{
Console.Write("Hello, my name is {0}n", theName);
}
}

class Program
{
static void Main(string[] args)
{
Man jhon = new Man("Jhon");
Man alice = new Man("Alice");
People people = new People();
people.Speak +=
new People.SpeakDelegate(jhon.Speak) +
new People.SpeakDelegate(alice.Speak);
people.Speak();
}
}
При вызове people.Speak будут вызваны методы Speak двух объектов - jhon и alice, а результат будет таким
Hello, my name is Jhon
Hello, my name is Alice
Этот же механизм (с небольшими дополнениями) используется при отправке сообщений. Но об этом я говорить не буду.
В общем-то, всё это выглядит очень красиво, но стоит помнить одну деталь - вызов делегата, у которого нет подписчиков приведёт к генерации исключения и крешу программы, если его не обработать. Следовательно, при вызове делегата мы должны каждый раз проверять, инициализирован ли он, благо не инициализированные делегаты всегда равны null. На первый взгляд, может показаться, что разработчики могли бы гарантировать безопасный вызов делегатов. Но нет, делегаты могут возвращать значение, а какое значение может вернуть то, что не было вызвано? Раз уж я об этом заговорил, то возвращаемым значением делегата - это значение, которое вернёт последняя вызванная делегатом функция.

Оператор foreach

Конечно же, каждый программист на C++ мечтает увидеть в своём любимом языке оператор обхода коллекций foreach. В языке C# он существует. При том синтаксис этого оператора весьма удобен, а работать с ним одно удовольствие.
foreach(ТипЭлементаКоллекции ссылка in коллекция)
{
}
"ссылка" принимает значение каждого из элементов и является именно ссылкой, а не копией. Чтобы Ваш класс мог работать как коллекция в операторе foreach, он должен реализовать интерфейс IEnumerable, в котором всего один метод - GetEnumerator. Вот если Вы используете не встроенные коллекции, то Вам придётся реализовать и интерфейс IEnumerator.

Коллекции

Список всевозможных коллекций в .NET достаточно большой, но что более всего меня вводит в недоумение - это то, что большинство из них не являются обобщёнными. Например, класс ArrayList хранит объекты типа object. Как я уже говорил в разделе об упаковке объектов, это может вызвать путаницу. По какой причине часть контейнеров сделали обобщёнными, а часть - хранящими объекты типа object для меня остаётся загадкой.

Разные мелочи

Из того, что я видел только в C#, стоит отметить статические конструкторы. Это особый вид конструкторов, в котором можно инициализировать статические члены класса. Кроме того, статические члены можно инициализировать прямо при объявлении, даже вызовом какой-либо функции.
Не удалось мне найти достойной замены оператору typedef из C++. Можно использовать ключевое слово using, но распространятся такой "typedef" будет только внутри одного модуля.

Итоги

Подводя итоги, могу сказать следующее: C# - довольно неплохой язык, но в некоторых местах, явно, плохо продуманный. Обойти все изъяны можно, но лучше бы разработчики подумали о них во время проектирования.

Автоматизация монтирования NFS каталогов в Linux

Есть у меня сетевое хранилище Thecus N299. Из путей доступа к нему есть протоколы SMB, NFS, FTP и web-интерфейс через HTTP. Так как я пользуюсь, чаще всего, ОС Linux, то протокол SMB является чужеродным и дико тормозящим, но зато он поддерживается любым файловым менеджером. Монтировать каталоги по NFS было очень лениво, поэтому я пользовался, в основном, SMB в ущерб скорости. Надо ли говорить, что работать с файлами через SMB напрямую не умеют почти все программы UNIX. Конечно же, решением (через одно место) могло бы быть монтирование через smbfs, но тут я вспомнил о NFS.
Писать постоянно что-то вроде
sudo mkdir /mnt/video
sudo mount -t nfs 192.168.1.100:/web/raid/video /mnt/video
лениво. И я решил автоматизировать себе этот процесс. После написания скрипта на bash, я подумал, что, может быть, кому-то он тоже будет полезен и решил выложить его сюда. Итак, встерчайте
#!/bin/bash

# Корневой каталог для монтирования.
DIRECTORY="/mnt/thecus/"
# Адрес сервера.
SERVER="192.168.1.100"
# Корневой каталог сервера.
SERVER_ROOT="/web/raid/"
# Каталоги на сервере, теже имена имеют каталоги, в которые они монтируются.
SERVER_DIRECTORIES=("video" "documents" "development" "projects" "soft")
# Параметр для монтирования всех каталогов сервера.
ALL="all"
# Имя команды скрипта.
COMMAND="thecus"
# Параметр скрипта для монтирования.
MOUNT="mount"
# Параметр скрипта для размонтирования.
UMOUNT="umount"

#####################################################################
#####################################################################


# $1 - имя каталога на nfs сервере.
# Возвращает 0 в случае успеха.
function doMount() {
if [ ! -d $DIRECTORY ]; then
mkdir $DIRECTORY
if [ ! 0 == $? ]; then
echo $COMMAND: Не удалось создать каталог $DIRECTORY
return 1
fi
fi

if [ ! -d $DIRECTORY$1 ]; then
mkdir $DIRECTORY$1
if [ ! 0 == $? ]; then
echo $COMMAND: Не удалось создать каталог $DIRECTORY$1
return 1
fi
fi

mount -t nfs $SERVER:$SERVER_ROOT$1 $DIRECTORY$1
return $?
}

# $1 - имя каталога в $DIRECTORY для размонтирования.
# Возвращает 0 в случае успеха.
function doUmount() {
umount $DIRECTORY$1
return $?
}

# Монтирует все каталоги, игнорируя ошибки.
function mountAll() {
for DIR in ${SERVER_DIRECTORIES[@]}; do
doMount $DIR
done
}

# Размонтирует все каталоги, игнорируя ошибки.
function umountAll() {
for DIR in ${SERVER_DIRECTORIES[@]}; do
doUmount $DIR
done
}

# Проверяет $1 на валидность, в случае успеха возвращает 0.
function checkTargetName() {
for DIR in ${SERVER_DIRECTORIES[@]}; do
if [ $1 == $DIR ]; then
return 0
fi
done
return 1
}

# Печатает справку.
function printHelp() {
echo Использование:
echo -e " $COMMAND $MOUNT имя_каталога"
echo -e " $COMMAND $UMOUNT имя_каталога"
echo -e " $COMMAND $MOUNT $ALL"
echo -e " $COMMAND $UMOUNT $ALL"
echo
echo -e "$MOUNT - смонтировать каталог"
echo -e "$UMOUNT - размонтировать каталог"
echo
echo Допустимые имена каталогов:
for DIR in ${SERVER_DIRECTORIES[@]}; do
echo -e " $DIR"
done
echo
echo -e "$ALL - смонтировать/размонтировать все каталоги"
echo
echo Все каталоги монтируются в $DIRECTORY
}

# Выход с ошибкой
function errorExit() {
echo
printHelp
echo
exit 1
}

#####################################################################
#####################################################################


# Если параметров меньше 2-х, то напечатать справку и выйти.
if [ $# -lt 2 ]; then
errorExit
fi

# Если вторым параметром выбрали $ALL, то действие относится ко всем каталогам.
if [ $2 == $ALL ]; then
case $1 in
$MOUNT)
mountAll
;;
$UMOUNT)
umountAll
;;
*)
errorExit
;;
esac
else
# Проверяем второй параметр на валидность.
checkTargetName $2
if [ ! $? == 0 ]; then
echo $COMMAND: Неверный каталог: $2
errorExit
fi

case $1 in
$MOUNT)
doMount $2
;;
$UMOUNT)
doUmount $2
;;
*)
errorExit
;;
esac
fi
Я сохранил этот скрипт под именем thecus в каталог /usr/local/bin и назначил права на выполнение. Теперь, для монтирования всех моих каталогов на NFS сервере, мне достаточно дать команду
sudo thecus mount all
Из комментов в тексте скрипта ясно,что он делает.

Анализ и оптимизация кода на C++ для Linux

Какие проблемы могут нас подстерегать при разработке программ? Неэффективные алгоритмы, утечки памяти, работа с невалидными указателями и не инициализированными переменными. Даже несмотря на, казалось бы, тщательное написание кода, мы порой делаем ошибки. Человеку свойственно ошибаться, поэтому эта статья посвящена контролю машин над человеком -- машинной проверки кода и исполняемых файлов. Я поделю весь процесс на три части:
  1. Статический анализ исходных текстов;
  2. Проверка утечек памяти;
  3. Нахождение участков кода, требующих неприлично много машинного времени.
В этой статье, говоря о компиляторе, я буду подразумевать g++, входящий в состав GCC (GNU Compiler Collection).

Статический анализ исходных текстов

Этот раздел опирается, во многом, на этот пост на хабре.

g++

Наряду со статическими анализаторами, компилятор g++ может выдать очень много полезной информации. Для того, чтобы добиться от компилятора максимум возмущений, следует добавит несколько опций.
-Wall - включает почти все стандартные предупреждения. Эту опцию нужно ставить всегда и везде, это должно стать Вашим правилом.
-Wextra - сообщит об ошибках в условных операторах, пустые if'ы и сравнение signed с unsigned.
-pedantic - по приказу этой опции компилятор начнёт следовать стандарту ISO C++. Например, запретит тип long long.
-Weffc++ -  эта опция напомнит Вам о некоторых правилах Скотта Майерса, которые Вы все, надеюсь, читали. В частности, это следующие правила из книги "Эффективное использование C++. 50 рекомендаций по улучшению ваших программ и проектов":
  • Правило 11. "Для классов с динамическим выделением памяти объявляйте копирующий конструктор и оператор присваивания".
  • Правило 12. "Предпочитайте инициализацию присваиванию в конструкторах".
  • Правило 14. "Убедитесь, что базовые классы имеют виртуальные деструкторы".
  • Правило 15. "operator= должен возвращать ссылку на *this".
  • Правило 23. "Никогда не пытайтесь вернуть ссылку, когда вы должны вернуть объект".
И несколько правил из книги "Наиболее эффективное использование C++. 35 новых рекомендаций по улучшению ваших программ и проектов":
  • Правило 6. "Различайте префиксную и постфиксную формы операторов инкремента и декремента".
  • Правило 7. "Никогда не перегружайте операторы &&, || и ,".
-Woverloaded-virtual - сообщит о перегрузке виртуальных функций.
-Wctor-dtor-privacy - возмутится, если найдёт у Вас в коде класс с закрытыми конструкторами и деструктором, который нигде не используется.
-Wnon-virtual-dtor - этой опции не нравятся не виртуальные деструкторы.
-Wold-style-cast - поможет избавится от приведений типов в стиле языка C.
-Wconversion -Wsign-conversion - заставят компилятор сказать пару ласковых о неверных приведениях типов, при которых Вы можете лишиться  части своих значений.
-Wunreachable-code - укажет на участки кода, которые никогда не будут выполнены. Эта опция может выдать очень много ворнингов даже в стандартной библиотеке, полезно её включать только при проверке.

Давайте проверим всё вышесказанное на практике. Для этой цели я написал следующий код полный ошибок
#include <iostream>

class CBicycle
{
public:
CBicycle(unsigned short max_speed)
{
m_max_speed = max_speed;
}

unsigned short GetMaxSpeed() const
{
return m_max_speed;
}

private:
short m_max_speed;
};

class CBicyclist
{
public:
CBicyclist(const CBicycle & bicycle)
{
mp_bicycle = new CBicycle(bicycle);
m_speed = 0;
}

unsigned short GetSpeed() const
{
return m_speed;
}

bool Move(short speed)
{
bool result = true;
if(speed <= mp_bicycle->GetMaxSpeed())
{
m_speed = speed;
std::cout << "I'm move with speed of " << m_speed << " km/hn";
}
else
{
result = false;
std::cout << "Sorry, this bicycle can't move with speed of " <<
speed << " km/hn";
}
return result;
}

private:
CBicycle * mp_bicycle;
unsigned short m_speed;
};

int TestMaxSpeed(CBicyclist & bicyclist)
{
int speed = bicyclist.GetSpeed();
int max_speed = speed;
bool loop = true;
do
{
if(bicyclist.Move(max_speed))
{
++max_speed;
}
else
{
loop = false;
--max_speed;
bicyclist.Move(speed);
}
} while(loop);
return max_speed;
}

int main()
{
CBicycle * bicycle = new CBicycle(75);
CBicyclist * bicyclist = new CBicyclist(*bicycle);
bicyclist->Move(600);
std::cout << "Begin max speed testn";
int max_speed = TestMaxSpeed(*bicyclist);
std::cout << "End max speed test. Max speed = " << max_speed << std::endl;
return 0;
}
У нас есть некое подобие классов, класс велосипеда и класс велосипедиста. Велосипед принимает в конструкторе максимальную скорость, а велосипедист - велосипед. Велосипедист может разогнаться на столько быстро, на сколько позволит велосипед или тип unsigned short, с которым у меня в коде связано много ошибок. Класс велосипедиста "забывает" удалить свой велосипед, а функция main "забывает" не только про велосипед, но и про велосипедиста. Задача функции TestMaxSpeed проверить, какая же скорость у велосипеда максимальная, используя для этой цели только велосипедиста. Но и у этой функции не всё в порядке с типами.
Итак, скомпилируем код с вышеуказанными опциями.
$ g++ -Wall -Wextra -pedantic -Weffc++ -Woverloaded-virtual -Wctor-dtor-privacy -Wnon-virtual-dtor -Wold-style-cast -Wconversion -Wsign-conversion -Wunreachable-code -g -O0  -c main.cpp
main.cpp: In constructor ‘CBicycle::CBicycle(short unsigned int)’:
main.cpp:6: warning: ‘CBicycle::m_max_speed’ should be initialized in the member initialization list
main.cpp:8: warning: conversion to ‘short int’ from ‘short unsigned int’ may change the sign of the result
main.cpp: In member function ‘short unsigned int CBicycle::GetMaxSpeed() const’:
main.cpp:13: warning: conversion to ‘short unsigned int’ from ‘short int’ may change the sign of the result
main.cpp: In constructor ‘CBicyclist::CBicyclist(const CBicycle&)’:
main.cpp:23: warning: ‘CBicyclist::mp_bicycle’ should be initialized in the member initialization list
main.cpp:23: warning: ‘CBicyclist::m_speed’ should be initialized in the member initialization list
main.cpp: In member function ‘bool CBicyclist::Move(short int)’:
main.cpp:39: warning: conversion to ‘short unsigned int’ from ‘short int’ may change the sign of the result
main.cpp: In function ‘int TestMaxSpeed(CBicyclist&)’:
main.cpp:63: warning: conversion to ‘short int’ from ‘int’ may alter its value
main.cpp:71: warning: conversion to ‘short int’ from ‘int’ may alter its value
main.cpp: In constructor ‘CBicyclist::CBicyclist(const CBicycle&)’:
main.cpp:25: warning: will never be executed
g++ -Wall -Wextra -pedantic -Weffc++ -Woverloaded-virtual -Wctor-dtor-privacy -Wnon-virtual-dtor -Wold-style-cast -Wconversion -Wsign-conversion -Wunreachable-code -g -O0 *.o -o test
Теперь давайте проанализируем вывод компилятора.
main.cpp: In constructor ‘CBicycle::CBicycle(short unsigned int)’:
main.cpp:6: warning: ‘CBicycle::m_max_speed’ should be initialized in the member initialization list
Нам сообщают, что в конструкторе класса CBicycle мы должны инициализировать член m_speed в списке инициализации, а не в теле конструктора.
main.cpp:8: warning: conversion to ‘short int’ from ‘short unsigned int’ may change the sign of the result
Конструктор CBicycle принимает значение unsigned short и присваивает его, почему-то, переменной типа short. (Это мы забыли написать unsigned в типе переменной-члена m_max_speed).
main.cpp: In member function ‘short unsigned int CBicycle::GetMaxSpeed() const’:
main.cpp:13: warning: conversion to ‘short unsigned int’ from ‘short int’ may change the sign of the result
Теперь мы ещё и возвращаем short от туда, от куда должны вернуть unsigned short.
main.cpp:23: warning: ‘CBicyclist::mp_bicycle’ should be initialized in the member initialization list
main.cpp:23: warning: ‘CBicyclist::m_speed’ should be initialized in the member initialization list
Класс CBicyclist повторил "подвиг" класса CBicycle и инициализировал все свои переменные в теле, а не в списке инициализации.
main.cpp: In member function ‘bool CBicyclist::Move(short int)’:
main.cpp:39: warning: conversion to ‘short unsigned int’ from ‘short int’ may change the sign of the result
Опять забыли про unsigned, какие же мы невнимательные.
main.cpp: In function ‘int TestMaxSpeed(CBicyclist&)’:
main.cpp:63: warning: conversion to ‘short int’ from ‘int’ may alter its value
main.cpp:71: warning: conversion to ‘short int’ from ‘int’ may alter its value
А это уже претенденты на потерю данных, конверсия из int в short.
main.cpp: In constructor ‘CBicyclist::CBicyclist(const CBicycle&)’:
main.cpp:25: warning: will never be executed
Этот ворнинг выдаёт опция -Wunreachable-code, его нужно проанализировать, и если Вы уверены, что всё в порядке -- можно проигнорировать.

cppcheck

Существуют специальные программы для анализа исходных текстов на C++, которые позволяют выявить потенциальные ошибки ещё до сборки (или использования) программ. Одна из таких программ -- cppcheck. В man руководстве к этой программе говорится, что она предназначена для выявления тех ошибок, которые не находит компилятор. Это такие ошибки как некоторые утечки памяти, выход за границу массива, исключения в деструкторах, разыменование нулевых и освобождённых указателей, виртуальность деструктора базовых классов и другое.
Работать с этой программой очень просто. Для демонстрации возьмём следующий небольшой пример.
int main()
{
int * x = new int[10];
x = 0;
return 0;
}
После запуска cppcheck в этом малюсеньком коде обнаруживается сразу две ошибки.
$ cppcheck --enable=all -v .
Checking ./main.cpp...
[./main.cpp:6]: (style) Variable 'x' is assigned a value that is never used
[./main.cpp:7]: (error) Memory leak: x
Checking usage of global functions..
Во-первых, мы объявили переменную x и никогда её не используем, а во-вторых, у нас зафиксирована утечка памяти.

Проверка утечек памяти

Практика показывает, что не всегда можно найти утечки памяти статическими анализаторами. Первый приведённый в этой статье листинг -- тому подтверждение. Если запустить cppcheck для этой программы, то мы ничего не найдём. Но отчаиваться ещё рано, нам может помочь утилита valgrind, предназначенная специально для этого.  Эта утилита находит утечки памяти не статически, а при работе программы. Давайте познокомимся с выводом этой программы
$ valgrind --leak-check=full -v ./test > /dev/null
==31174== Memcheck, a memory error detector
==31174== Copyright (C) 2002-2010, and GNU GPL'd, by Julian Seward et al.
==31174== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
==31174== Command: ./test
==31174==
--31174-- Valgrind options:
--31174-- --suppressions=/usr/lib/valgrind/debian-libc6-dbg.supp
--31174-- --leak-check=full
--31174-- -v
--31174-- Contents of /proc/version:
--31174-- Linux version 2.6.32-5-amd64 (Debian 2.6.32-29) (ben@decadent.org.uk) (gcc version 4.3.5 (Debian 4.3.5-4) ) #1 SMP Fri Dec 10 15:35:08 UTC 2010
--31174-- Arch and hwcaps: AMD64, amd64-sse3-cx16
--31174-- Page sizes: currently 4096, max supported 4096
--31174-- Valgrind library directory: /usr/lib/valgrind
--31174-- Reading syms from /mnt/data/projects/blog/CodeOptimize/code/test (0x400000)
--31174-- Reading syms from /lib/ld-2.11.2.so (0x4000000)
--31174-- Considering /lib/ld-2.11.2.so ..
--31174-- .. CRC mismatch (computed 91367345 wanted f148be98)
--31174-- Considering /usr/lib/debug/lib/ld-2.11.2.so ..
--31174-- .. CRC is valid
--31174-- Reading syms from /usr/lib/valgrind/memcheck-amd64-linux (0x38000000)
--31174-- object doesn't have a dynamic symbol table
--31174-- Reading suppressions file: /usr/lib/valgrind/debian-libc6-dbg.supp
--31174-- Reading suppressions file: /usr/lib/valgrind/default.supp
--31174-- REDIR: 0x40162d0 (strlen) redirected to 0x380408a7 (vgPlain_amd64_linux_REDIR_FOR_strlen)
--31174-- Reading syms from /usr/lib/valgrind/vgpreload_core-amd64-linux.so (0x4a20000)
--31174-- Reading syms from /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so (0x4c21000)
==31174== WARNING: new redirection conflicts with existing -- ignoring it
--31174-- new: 0x040162d0 (strlen ) R-> 0x04c25850 strlen
--31174-- REDIR: 0x4016140 (index) redirected to 0x4c25460 (index)
--31174-- REDIR: 0x40161c0 (strcmp) redirected to 0x4c25e30 (strcmp)
--31174-- Reading syms from /usr/lib/libstdc++.so.6.0.13 (0x4e29000)
--31174-- object doesn't have a symbol table
--31174-- Reading syms from /lib/libm-2.11.2.so (0x513d000)
--31174-- Considering /lib/libm-2.11.2.so ..
--31174-- .. CRC mismatch (computed b0a7ab6b wanted 907fac55)
--31174-- Considering /usr/lib/debug/lib/libm-2.11.2.so ..
--31174-- .. CRC is valid
--31174-- Reading syms from /lib/libgcc_s.so.1 (0x53bf000)
--31174-- Considering /lib/libgcc_s.so.1 ..
--31174-- .. CRC mismatch (computed 07151771 wanted 3f9779e8)
--31174-- object doesn't have a symbol table
--31174-- Reading syms from /lib/libc-2.11.2.so (0x55d5000)
--31174-- Considering /lib/libc-2.11.2.so ..
--31174-- .. CRC mismatch (computed 21e032ea wanted d5c67601)
--31174-- Considering /usr/lib/debug/lib/libc-2.11.2.so ..
--31174-- .. CRC is valid
--31174-- REDIR: 0x5652600 (__GI_strrchr) redirected to 0x4c25280 (__GI_strrchr)
--31174-- REDIR: 0x5650b40 (__GI_strlen) redirected to 0x4c25810 (__GI_strlen)
--31174-- REDIR: 0x4ef46a0 (operator new(unsigned long)) redirected to 0x4c24d78 (operator new(unsigned long))
--31174-- REDIR: 0x5650b10 (strlen) redirected to 0x4a205ac (_vgnU_ifunc_wrapper)
==31174== WARNING: new redirection conflicts with existing -- ignoring it
--31174-- new: 0x05650b40 (__GI_strlen ) R-> 0x04c257f0 strlen
--31174-- REDIR: 0x5653e70 (mempcpy) redirected to 0x4c26bc0 (mempcpy)
--31174-- REDIR: 0x5654750 (memcpy) redirected to 0x4c25f00 (memcpy)
--31174-- REDIR: 0x564b7e0 (free) redirected to 0x4c24076 (free)
==31174==
==31174== HEAP SUMMARY:
==31174== in use at exit: 20 bytes in 3 blocks
==31174== total heap usage: 3 allocs, 0 frees, 20 bytes allocated
==31174==
==31174== Searching for pointers to 3 not-freed blocks
==31174== Checked 180,104 bytes
==31174==
==31174== 2 bytes in 1 blocks are definitely lost in loss record 2 of 3
==31174== at 0x4C24DFA: operator new(unsigned long) (vg_replace_malloc.c:261)
==31174== by 0x400B17: main (main.cpp:79)
==31174==
==31174== 18 (16 direct, 2 indirect) bytes in 1 blocks are definitely lost in loss record 3 of 3
==31174== at 0x4C24DFA: operator new(unsigned long) (vg_replace_malloc.c:261)
==31174== by 0x400B38: main (main.cpp:80)
==31174==
==31174== LEAK SUMMARY:
==31174== definitely lost: 18 bytes in 2 blocks
==31174== indirectly lost: 2 bytes in 1 blocks
==31174== possibly lost: 0 bytes in 0 blocks
==31174== still reachable: 0 bytes in 0 blocks
==31174== suppressed: 0 bytes in 0 blocks
==31174==
==31174== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 4 from 4)
--31174--
--31174-- used_suppression: 2 dl-hack3-cond-1
--31174-- used_suppression: 2 glibc-2.5.x-on-SUSE-10.2-(PPC)-2a
==31174==
==31174== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 4 from 4)

Здесь мы можем увидеть статистику выделения и высвобождения памяти
==31174== HEAP SUMMARY:
==31174== in use at exit: 20 bytes in 3 blocks
==31174== total heap usage: 3 allocs, 0 frees, 20 bytes allocated
Три раза память была выделена, но ни разу не освобождалась. Чуть далее в выводе программы valgrind мы можем найти информацию немного подробнее.
==31174== 2 bytes in 1 blocks are definitely lost in loss record 2 of 3
==31174== at 0x4C24DFA: operator new(unsigned long) (vg_replace_malloc.c:261)
==31174== by 0x400B17: main (main.cpp:79)
==31174==
==31174== 18 (16 direct, 2 indirect) bytes in 1 blocks are definitely lost in loss record 3 of 3
==31174== at 0x4C24DFA: operator new(unsigned long) (vg_replace_malloc.c:261)
==31174== by 0x400B38: main (main.cpp:80)
Вот тут-то valgrind и сдаёт нас с потрохами, выдавая все наши грехи.

Оптимизация кода

Теперь пришло время задуматься о производительности. Пусть у нас есть такой код
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <cstdlib>
#include <sys/time.h>

template<typename Type>
std::vector<Type> & Sort(std::vector<Type> & array)
{
size_t arr_size = array.size();
for(size_t i = 0; i < arr_size; ++i)
{
bool swaped = false;
for(size_t j = 1; j < arr_size; ++j)
{
if(array[j - 1] > array[j])
{
std::swap(array[j - 1], array[j]);
swaped = true;
}
}
if(!swaped)
break;
}
return array;
}

template<typename Type>
class CWriter
{
public:
CWriter(std::ostream & stream) :
mr_stream(stream)
{
}

void operator () (const Type & value)
{
mr_stream << value << std::endl;
}

private:
std::ostream & mr_stream;
};

template<typename Type>
bool WriteArrayToFile(const std::string & filename,
const std::vector<Type> & array)
{
bool result = false;
std::ofstream out(filename.c_str());
if(out.is_open())
{
std::for_each(array.begin(), array.end(), CWriter<Type>(out));
result = true;
out.close();
}
return result;
}

int main(int argc, char ** argv)
{
if(argc < 3)
{
std::cerr << "To few argumentsn";
return 1;
}
size_t array_size = static_cast<size_t>(::atoi(argv[1]));
std::string filename = argv[2];
::srand(static_cast<unsigned int>(::time(0)));
std::vector<int> array;
array.reserve(array_size);

for(size_t i = 0; i < array_size; ++i)
{
array.push_back(::rand());
}

if(! ::WriteArrayToFile(filename, ::Sort(array)))
{
std::cerr << "Error writing the file with name "" <<
filename << ""n";
return 2;
}

return 0;
}
Здесь генерируется массив из n элементов, сортируется и записывается в файл. Если мы его запустим, то увидим картину, не очень-то удовлетваряющую нас -- большие затраты времени. В этом можно убедиться, запустив такую команду
$ time ./test 100000 test.txt

real 0m24.913s
user 0m24.566s
sys 0m0.344s
25 секунд -- это, явно, слишком много. Естественным желанием будет оптимизировать работу программы так, чтобы предельно сократить время её работы. Из данного примера очевидно, что пузырьковая сортировка и является причиной нашего недовольства, но в больших проектах причина может быть не столь очевидной. Давайте сделаем вид, что мы не знаем причину столь затяжного выполнения и попробуем найти её методом профилирования программы. А поможет нам в этом утилита с именем gprof. Подробно о работе с этой утилитой можно прочитать на man-странице проекта opennet.
Для того, чтобы можно было работать с профилировщиком gprof, нужно добавить опцию -pg к команде сборки проекта, а затем собрать проект с отладочными символами. После сборки нужно запустить программу с тем же параметрами, с которыми мы получили неудовлетворительный результат, что приведёт к созданию файла gmon.conf в последнем текущем каталоге программы, а у нас этот каталог не менялся. Запустить программу gprof можно следующим образом
$ gprof ./test > gprof.log
На выходе программа gprof выдаёт очень много информации, поэтому её лучше перенаправить в файл. Теперь нужно открыть этот файл и проанализировать его. Я советую открывать этот файл в текстовом редакторе, поддерживающем подсветку синтаксиса и выбрать режим подсветки C++, так как профиль и граф вызовов содержат много текста на C++. Кроме того, лучше отключить динамический перенос строк. Я приведу только часть простого профиля файла, который у меня получился.
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
55.50 98.49 98.49 1 98.49 154.05 std::vector<int, std::allocator<int> >& Sort<int>(std::vector<int, std::allocator<int> >&)
24.22 141.47 42.99 7769644628 0.00 0.00 std::vector<int, std::allocator<int> >::operator[](unsigned long)
10.77 160.58 19.11 73 0.26 0.26 std::vector<int, std::allocator<int> >::size() const
6.94 172.89 12.31 2507056584 0.00 0.00 void std::swap<int>(int&, int&)
Из приведённых выше строк простого профиля видно, что 55,5% времени программа тратит на сортировку и ещё 24,22% на обращение к элементам вектора. Скорее всего, нам удастся решить обе проблемы, подставив более эффективный алгоритм сортировки. Давайте проверим, заменим сортировку пузырьком сортировкой слияниями.
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <cstdlib>
#include <sys/time.h>


template<typename Type>
std::vector<Type> & MergeSort(std::vector<Type> & array)
{
struct CMergeSort
{
void Sort(std::vector<Type> & array)
{
size_t array_size = array.size();
if(array_size <= 1)
return;

typename std::vector<Type>::iterator middle =
array.begin() + static_cast<ptrdiff_t>(array_size) / 2;
std::vector<Type> left(array.begin(), middle);
std::vector<Type> right(middle, array.end());
Sort(left);
Sort(right);
array = Merge(left, right);
}

std::vector<Type> Merge(const std::vector<Type> & left,
const std::vector<Type> & right)
{
std::vector<Type> result;
result.reserve(left.size() + right.size());
for(typename std::vector<Type>::const_iterator l =
left.begin(), r = right.begin();;)
{
if(left.end() == l)
{
typename std::vector<Type>::iterator it =
result.end();
result.insert(it, r, right.end());
break;
}
if(right.end() == r)
{
typename std::vector<Type>::iterator it =
result.end();
result.insert(it, l, left.end());
break;
}
if(*l < *r)
{
result.push_back(*l);
++l;
}
else
{
result.push_back(*r);
++r;
}
}
return result;
}
}; // struct CMergeSort

CMergeSort().Sort(array);
return array;
}

template<typename Type>
class CWriter
{
public:
CWriter(std::ostream & stream) :
mr_stream(stream)
{
}

void operator () (const Type & value)
{
mr_stream << value << std::endl;
}

private:
std::ostream & mr_stream;
};

template<typename Type>
bool WriteArrayToFile(const std::string & filename,
const std::vector<Type> & array)
{
bool result = false;
std::ofstream out(filename.c_str());
if(out.is_open())
{
std::for_each(array.begin(), array.end(), CWriter<Type>(out));
result = true;
out.close();
}
return result;
}

int main(int argc, char ** argv)
{
if(argc < 3)
{
std::cerr << "To few argumentsn";
return 1;
}
size_t array_size = static_cast<size_t>(::atoi(argv[1]));
std::string filename = argv[2];
::srand(static_cast<unsigned int>(::time(0)));
std::vector<int> array;
array.reserve(array_size);

for(size_t i = 0; i < array_size; ++i)
{
array.push_back(::rand());
}

if(! ::WriteArrayToFile(filename, ::MergeSort(array)))
{
std::cerr << "Error writing the file with name "" <<
filename << ""n";
return 2;
}

return 0;
}
Если мы откомпилируем этот код и запустим программу, то получим следующее
$ time ./test 100000 test.txt

real 0m0.437s
user 0m0.076s
sys 0m0.360s
Очевидно, что такие результаты нас устраивают.

---------------
Источники:
Статический анализ кода C++
Профилятор gprof

Работа с COM и ActiveX в Visual C++

Сразу внесу некоторую поправку к обозначенной в заголовке теме. В этой статье речь пойдёт не просто о COM и ActiveX, а о COM и ActiveX библиотеках, содержащих библиотеки типов (type library).
Поводом для написания этой статьи стали мои собственные изыскания в этой теме. На просторах Интернета тут и там разбросана информация о работе с COM и ActiveX из Visual C++ (отмечу, что речь идёт не о .NET, а обычном C++), но хорошего, структурированного материала мне найти не удалось. В этой статье я разберу два примера: в первом я расскажу о работе с простыми COM библиотеками; во втором будет показана работа с объектом ActiveX. Я не буду рассказывать, что такое COM, ActiveX и Type libraries, об этом Вы сможете прочитать, например, здесь.

Директива препроцессора #import

Компилятор Microsoft Visual C++ определяет директиву препроцессора #import. Основное её предназначение – загружать информацию из библиотеки типов и представлять её в виде кода C++. В простейшем случае директива #import используется так
#import "libname.tlb"
После компиляции в каталоге сборки программы появляются два файла: libname.tlh и libname.tli. Первый из них является заголовочным и автоматически подключается к программе, а второй содержит код реализации и компилируется вместе с проектом.
У директивы #import есть множество дополнительных опций, о которых Вы сможете прочитать тут или в справочной системе к Visual Studio.

Где взять библиотеку типов

Хотя директива #import и позволяет подключать сами COM серверы, я советую подключать именно библиотеки типов, это бинарные файлы, обычно имеющие расширение TLB. Это позволит добавить такой файл в Ваш проект и не заботиться о расположении библиотеки при сборке проекта на другом компьютере.
Но вот тут, на первый взгляд, может возникнуть проблема. Где взять библиотеку типов, если большинство серверов распространяется одним файлом (чаще всего это либо *.dll, либо *.ocx)? Ответ прост; библиотека типов является ни чем иным как ресурсом это сервера. А если это ресурс, то получить его можно, к примеру, с помощью Resource Hacker. На скриншоте ниже показано, как это сделать.

Простой COM сервер

Начнём работу с простого примера, в котором не нужно отображать визуальные компоненты. Для демонстрации я взял библиотеку msxml6.dll. Стоит отметить, что в Windows API существует привязка к этой библиотеке. Ниже приведён листинг программы, которая создаёт файл "test.xml" с одним лишь тегом "Example".
#include <iostream>
#import "msxml6.tlb"

class CComInitializer
{
public:
CComInitializer() { ::CoInitialize(NULL); }
~CComInitializer() { ::CoUninitialize(); }
} gComInitializer;

int main()
{
MSXML2::IXMLDOMDocument3Ptr xml_document;
HRESULT hr = ::CoCreateInstance(__uuidof(MSXML2::DOMDocument60),
NULL, CLSCTX_INPROC_SERVER, __uuidof(MSXML2::IXMLDOMDocument3),
reinterpret_cast<void **>(&xml_document));
if(SUCCEEDED(hr))
{
MSXML2::IXMLDOMElementPtr xml_elem =
xml_document->createElement(L"Example");
xml_document->appendChild(xml_elem);
xml_document->save(L"test.xml");
}
else
std::cerr << "Error creating instancen";

return 0;
}
Назначение класса CComInitializer только в том, чтобы инициализировать модель COM в приложении. Для этого создаётся один глобальный объект этого класса. При разрушении объекта модель COM деинициализируется.
Директива #import помещает весь сгенерированный код в пространство имён, соответствующее имени библиотеки, в нашем случае – это MSXML2. В сгенерированный код, по умолчанию, добавляются объявления "умных" указателей для каждого интерфейса. Имена их соответствуют именам интерфейсов с постфиксом Ptr. Объявив такой объект, мы можем передать указатель на него в функцию CoCreateInstance.
Каждый интерфейс и класс объявляется в файлах *.tlh специальным образом, например
struct __declspec(uuid("2933bf96-7b36-11d2-b20e-00c04f983e60"))
IXMLDOMDocument3 : IXMLDOMDocument2
Это позволяет получить их GUID’ы с помощью оператора (специфичного для Visual C++) __uuidof. Для того, чтобы быть точно уверенными, что Вы получаете GUID именно того класса, который Вам нужен, следует обратиться к текстам библиотеки типов. Чтобы получить текст библиотеки из двоичного файла можно воспользоваться утилитой OleView или просмоторщиком из Total Commander. В IDL коде библиотеки мы должны найти coclass, который реализует нужный интерфейс.
[
uuid(88D96A05-F192-11D4-A65F-0040963251E5),
helpstring("W3C-DOM XML Document 6.0 (Apartment)")
]
coclass DOMDocument60 {
[default] interface IXMLDOMDocument3;
[default, source] dispinterface XMLDOMDocumentEvents;
};
Все последующие действия специфичны для библиотеки MSXML и приведены только для примера.
Обратите внимание, я не вызываю метод Release из xml_document и xml_elem, это за меня делают "умные" указатели.

Элемент управления ActiveX

Для того, чтобы разместить элемент ActiveX в окне требуется выполнить очень много скучной рутинной работы, поэтому мы воспользуемся готовым решением, предоставляемым библиотекой MFC. Те, кому интересно узнать всю подноготную, могут пройти по этой и этой ссылкам.
Для демонстрации встраивания элемента управления, я воспользовался библиотекой MSFLXGRD.OCX, которая входит в стандартную поставку Visual Basic 6.0. То, что у меня получилось, видно на следующем скриншоте.

По традиции, сразу приведу листинг программы, а затем прокомментирую.
#define WINVER 0x0501

#include <afxwin.h>
#include <afxole.h>
#import "msflexgrid.tlb"


template<typename InterfaceT>
class CComWindow :
public CWnd
{
public:
CComWindow(const IID & iid, const CLSID & class_id) :
mp_ax(NULL),
m_class_id(class_id),
m_iid(iid)
{
}

virtual ~CComWindow()
{
if(NULL != mp_ax)
mp_ax->Release();
}

virtual BOOL Create(LPCTSTR class_name, LPCTSTR window_name,
DWORD style, const RECT & rect, CWnd * parent, UINT id,
CCreateContext * context = NULL)
{
UNREFERENCED_PARAMETER(class_name);
UNREFERENCED_PARAMETER(context);
BOOL result = CreateControl(m_class_id, window_name,
style, rect, parent, id);
if(result)
result = InitComponent();
return result;
}

virtual BOOL Create(LPCTSTR window_name, DWORD style,
const RECT & rect, CWnd * parent, UINT id, CFile * persist = NULL,
BOOL storage = FALSE, BSTR lic_key = NULL)
{
BOOL result = CreateControl(m_class_id, window_name, style,
rect, parent, id, persist, storage, lic_key);
if(result)
result = InitComponent();
return result;
}

protected:
BOOL InitComponent()
{
BOOL result = FALSE;
COleControlSite * control_site = GetControlSite();
if(NULL != control_site)
{
HRESULT hr =
control_site->m_pInPlaceObject->QueryInterface(
m_iid, reinterpret_cast<void **>(&mp_ax));

if(SUCCEEDED(hr))
result = TRUE;
else
mp_ax = NULL;
}
return result;
}

public:
InterfaceT * mp_ax;

protected:
CLSID m_class_id;
IID m_iid;
}; // class CComWindow





class CMainWindow :
public CWnd
{
DECLARE_MESSAGE_MAP()
DECLARE_EVENTSINK_MAP()
public:
CMainWindow();
virtual ~CMainWindow();
int OnCreate(LPCREATESTRUCT cs);

HRESULT OnFlexGridClick();

private:
CComWindow<MSFlexGridLib::IMSFlexGrid> * mp_flex_grid;
static const UINT m_grid_id = 1800;
}; // class CMainWindow

BEGIN_MESSAGE_MAP(CMainWindow, CWnd)
ON_WM_CREATE()
END_MESSAGE_MAP()

BEGIN_EVENTSINK_MAP(CMainWindow, CWnd)
ON_EVENT(CMainWindow, m_grid_id, DISPID_CLICK,
CMainWindow::OnFlexGridClick, VTS_NONE)
END_EVENTSINK_MAP()

CMainWindow::CMainWindow() :
mp_flex_grid(NULL)
{
}

CMainWindow::~CMainWindow()
{
delete mp_flex_grid;
}

int CMainWindow::OnCreate(LPCREATESTRUCT cs)
{
int result = CWnd::OnCreate(cs);

if(0 == result)
{
mp_flex_grid = new CComWindow<MSFlexGridLib::IMSFlexGrid>(
__uuidof(MSFlexGridLib::IMSFlexGrid),
__uuidof(MSFlexGridLib::MSFlexGrid));
BOOL created = mp_flex_grid->Create(L"",
WS_CHILD | WS_VISIBLE, CRect(CPoint(10, 10), CSize(520, 450)),
this, m_grid_id);
if(created)
{
const long cols = 8;
const long rows = 25;
mp_flex_grid->mp_ax->Cols = cols;
mp_flex_grid->mp_ax->Rows = rows;
mp_flex_grid->mp_ax->ColWidth[0] = 350;

mp_flex_grid->mp_ax->Col = 0;
wchar_t text[8];
for(long i = 1; i < rows; ++i)
{
_itow(i, text, 10);
mp_flex_grid->mp_ax->Row = i;
mp_flex_grid->mp_ax->Text = text;
}
mp_flex_grid->mp_ax->Row = 0;
mp_flex_grid->mp_ax->Col = 1;
mp_flex_grid->mp_ax->Text = L"Пн";
mp_flex_grid->mp_ax->Col = 2;
mp_flex_grid->mp_ax->Text = L"Вт";
mp_flex_grid->mp_ax->Col = 3;
mp_flex_grid->mp_ax->Text = L"Ср";
mp_flex_grid->mp_ax->Col = 4;
mp_flex_grid->mp_ax->Text = L"Чт";
mp_flex_grid->mp_ax->Col = 5;
mp_flex_grid->mp_ax->Text = L"Пт";
mp_flex_grid->mp_ax->Col = 6;
mp_flex_grid->mp_ax->Text = L"Сб";
mp_flex_grid->mp_ax->Col = 7;
mp_flex_grid->mp_ax->Text = L"Вс";>
}
else
result = 1;
}
return result;
}

HRESULT CMainWindow::OnFlexGridClick()
{
static int i_text = 1;
wchar_t text[16];
::_itow(i_text++, text, 10);
mp_flex_grid->mp_ax->Text = text;
return S_OK;
}




class CApplication :
public CWinApp
{
public:
CApplication();
virtual ~CApplication();
virtual BOOL InitInstance();
} gApplication; // class CApplication


CApplication::CApplication()
{
}

CApplication::~CApplication()
{
delete m_pMainWnd;
}

BOOL CApplication::InitInstance()
{
BOOL result = CWinApp::InitInstance();
if(FALSE != result)
{
::AfxOleInit();
::AfxEnableControlContainer();

const wchar_t * wnd_class = ::AfxRegisterWndClass(
0, LoadStandardCursor(IDC_ARROW),
reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1));
CMainWindow * main_window = new CMainWindow();
main_window->CreateEx(0, wnd_class, L"Flex Grid Example",
WS_OVERLAPPEDWINDOW, 200, 100, 800, 600,
GetDesktopWindow(), NULL);
m_pMainWnd = main_window;
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
}
return result;
}
Теперь, нам требуется не просто получить указатель на интерфейс, но и связать его с окном, поэтому создаём класс CComWindow, сделав его шаблонным для универсальности. В качестве параметра шаблона требуется указывать тип интерфейса COM объекта.
Операции Create замещают операции класса CWnd и создают контрол из переданных в конструктор GUID’ов интерфейса и кокласса. Член mp_ax инициализируется указателем из элемента управления и является мостом между клиентом и интерфейсом COM.
Далее окно ActiveX создаётся как и все другие окна, с той лишь разницей, что в конструктор мы передаём GUID’ы класса и интерфейса.
Следует заметить, что директива #import генерирует специальные поля классов – свойства. Такие поля также являются расширением Visual C++ и выглядят следующим образом
__declspec(property(get=GetRows,put=PutRows))
long Rows;
где значениями параметров get и put являются функции, которые неявно вызываются при соответствующих обращениях к данному полю.
Чтобы окно ActiveX могло общаться с окружающим миром посредствам сообщений, библиотекой MFC предусмотрена специальная карта сообщений. Используется она практически идентично карте основных сообщений. Для её объявления добавляем в класс родительского окна макроопределение
DECLARE_EVENTSINK_MAP()
А определение этой карты должно находиться между макросами
BEGIN_EVENTSINK_MAP(CMainWindow, CWnd)
и
END_EVENTSINK_MAP()
Далее, нам нужно в IDL тексте TLB файла найти, какой класс реализует события интересующего интерфейса
[
uuid(6262D3A0-531B-11CF-91F6-C2863C385E30),
helpstring("Microsoft FlexGrid Control 6.0"),
helpcontext(0x00059621),
licensed,
control
]
coclass MSFlexGrid {
[default] interface IMSFlexGrid;
[default, source] dispinterface DMSFlexGridEvents;
};
В нашем случае – это DMSFlexGridEvents. В *.tlh файле Вы найдёте объявление этого класса со всеми доступными событиями, а в файле *.tli – все реализации.
Чтобы добавить обработку события в карту EVENTSINK_MAP следует добавить в класс окна функцию с идентичной сигнатурой (за исключением имени). После этого в карту событий помещается элемент с именем ON_EVENT. Первым параметром этого макроса является класс, принимающей событие, второй - идентификатор ActiveX объекта, третий - это номер события, его можно подсмотреть в реализации методов-событий в файле *.tli. Стандартные события имеют макроимена. Следующим параметром мы передаём метод, который будет вызван в ответ на событие. Последний параметр - это набор аргументов, которым соответствуют макроопределения. Набор этих параметров пишется без разделения запятой. Все возможные значения объявлены в файле afxdisp.h.
Для примера, я определил обработчик события Click. Теперь, при щелчке мышью, в соответствующую ячейку будут устанавливаться числовые значения, каждый раз на единицу больше предыдущего.
Следует отметить, что для работы с COM в приложении MFC необходимо вызвать функцию AfxOleInit, а для работы с компонентами ActiveX - функцию AfxEnableControlContainer. Все они находятся в файле afxdisp.h, но подключать их следует из afxole.h.

Положение двух точек относительно прямой

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

Где может понадобится решение подобной задачи? Представьте, вы пишите стратегическую игру. Два юнита находятся по разные стороны реки. Пользователь выделяет их обоих, и заставляет куда-то идти. И тут нужно определить, смогут ли оба юнита пройти туда, куда велит пользователь. Стало интереснее?
Итак, набросаем небольшой рисунок.

У нас есть точки A, B и C. B и C лежат по одну сторону от прямой, а A по другую.
Для того, чтобы проверить, лежат ли две точки по одну сторону от прямой или нет нужно спроецировать эти точки на прямую линией, параллельной оси OY. Затем сравнить ординаты определяемых точек с ординатами полученных. Если оба отношения будут идентичными, то точки лежат с одной стороны, иначе - с разных.
Поробуем это запрограммировать.
#include <iostream>

class CFunction
{
public:
CFunction(double a, double b) :
m_a(a),
m_b(b)
{
}

double Run(double x)
{
return m_a * x + m_b;
}

private:
double m_a;
double m_b;
};

struct CPoint
{
double m_x;
double m_y;
};

int main()
{
CPoint point_a;
CPoint point_b;
double a;
double b;

std::cout << "y = ax + b.nEnter the a value: ";
std::cin >> a;
std::cout << "Enter the b value: ";
std::cin >> b;

std::cout << "Point A.nEnter the X coord: ";
std::cin >> point_a.m_x;
std::cout << "Enter the Y coord: ";
std::cin >> point_a.m_y;

std::cout << "Point B.nEnter the X coord: ";
std::cin >> point_b.m_x;
std::cout << "Enter the Y coord: ";
std::cin >> point_b.m_y;

CFunction function(a, b);

bool a_up = function.Run(point_a.m_x) > point_a.m_y;
bool b_up = function.Run(point_b.m_x) > point_b.m_y;

if(a_up == b_up)
{
std::cout << "Points lie on one siden";
}
else
{
std::cout << "Points lie on different sidesn";
}
}
Здесь я ввёл класс, соответствующий линейной функции и структуру для описания точки. Ничего сложного. Далее, как уже говорилось, берём ординаты из функции, соответствующие абсциссам точкек и сравниваем их с ординатами точек. Проверяем результаты. Если они идентичны, выдаём сообщение о том, что точки лежат с одной стороны прямой, иначе сообщаем о том, что точки находятся по разные стороны прямой.

Универсальный Makefile

Довольно часто приходится писать небольшие проекты для тестирования того или иного участка кода. Компиляция этих проектов не всегда тривиальна, и запуск компиляции из командной строки утомляет. К тому же не хочется создавать проекты в IDE. И тут на помощь программистам приходят Makefile'ы. Пользователям UNIX-like систем хорошо знакомы файлы для сборки приложений, имеющие названия Makefile. Но и писать всякий раз Makefile'ы тоже не удобно. Поэтому я решил написать универсальный Makefile. На столько универсальный, на сколько это возможно.

К примеру, мы хотим написать такой код:
#include <iostream>
#include <string>
#include <boost/thread.hpp>

boost::mutex gMutex1;
boost::mutex gMutex2;


void MyThread(const std::string & name, size_t iter_count,
size_t step_size)
{
for(size_t i = 0; i < iter_count;)
{
gMutex1.lock();
gMutex2.lock();

for(size_t j = 0; j < step_size && i < iter_count; ++j, ++i)
{
if(0 == j)
gMutex1.unlock();
std::cout << name << ": " << i + 1 << " from " << iter_count <<
std::endl;
}
std::cout << std::endl;
gMutex2.unlock();
}
}

int main()
{
boost::thread thread_one(MyThread, "one", 12, 3);
boost::thread thread_two(MyThread, "two", 8, 4);
thread_one.join();
thread_two.join();
return 0;
}
Сразу же видно, что без подключения библиотеки boost_thread тут не обойтись. Не буду утомлять Вас и приведу полный код Makefile'а. Пусть наша программа записана в файл main.cpp
TARGET_NAME = test
DEFAULT_BUILD_TYPE = debug
#DEFAULT_BUILD_TYPE = release
COMPILER = g++
HEADERS =
SOURCES = main.cpp
OBJECTS = *.o

# Common options
COMMON_COMPILER_OPTS =
COMMON_LINKER_OPTS = -lboost_thread
COMMON_COMMON_OPTS = -Wall

# Debug options
DEBUG_COMPILER_OPTS = $(COMMON_COMPILER_OPTS)
DEBUG_LINKER_OPTS = $(COMMON_LINKER_OPTS)
DEBUG_COMMON_OPTS = $(COMMON_COMMON_OPTS) -g -O0

# Release options
RELEASE_COMPILER_OPTS = $(COMMON_COMPILER_OPTS)
RELEASE_LINKER_OPTS = $(COMMON_LINKER_OPTS)
RELEASE_COMMON_OPTS = $(COMMON_COMMON_OPTS) -O4

# Debug build steps
DEBUG_BUILD = $(COMPILER) $(DEBUG_COMMON_OPTS) $(DEBUG_COMPILER_OPTS) -c $(SOURCES)
DEBUG_LINK = $(COMPILER) $(DEBUG_COMMON_OPTS) $(DEBUG_LINKER_OPTS) $(OBJECTS) -o $(TARGET_NAME)

# Release build steps
RELEASE_BUILD = $(COMPILER) $(RELEASE_COMMON_OPTS) $(RELEASE_COMPILER_OPTS) -c $(SOURCES)
RELEASE_LINK = $(COMPILER) $(RELEASE_COMMON_OPTS) $(RELEASE_LINKER_OPTS) $(OBJECTS) -o $(TARGET_NAME)

all: $(DEFAULT_BUILD_TYPE)

debug: debug_build debug_link

release: release_build release_link

debug_build: $(HEADERS) $(SOURCES)
$(DEBUG_BUILD)

debug_link: debug_build
$(DEBUG_LINK)

release_build: $(HEADERS) $(SOURCES)
$(RELEASE_BUILD)

release_link: release_build
$(RELEASE_LINK)

clean:
rm -rf $(TARGET_NAME)
rm -rf $(OBJECTS)
Так как при вставке в этот блог строк, начинающихся со знака табуляции они (знаки табуляции) заменяются на пробелы, прошу Вас заменить четыре пробела на один знак табуляции. Утилита make требует именно табуляцию.
Итак, что мы здесь имеем. В начале файла большое количество переменных. По порядку:
TARGET_NAME - имя исполняемого файла;
DEFAULT_BUILD_TYPE - тип компиляции по умолчанию. Может принемать два значения: debug и release;
COMPILER - команда, вызывающая компилятор;
HEADERS - перечисляем здесь все заголовочные файлы через пробел.
SOURCES - а здесь перечисляем все компилируемые модули (*.cpp);
OBJECTS - тут мы вписываем *.o,- это маска для всех объектных файлов.
Далее идёт секция общих опций для отладочной и релизной компиляций:
COMMON_COMPILER_OPTS - общие опции компилятора;
COMMON_LINKER_OPTS - общие опции линковщика;
COMMON_COMMON_OPTS - общие опции общие для компилятора и для линковщика, чтобы два раза не писать;
Опции отладочного режима:
DEBUG_COMPILER_OPTS - отладочные опции компилятора;
DEBUG_LINKER_OPTS - отладочные опции линковщика;
DEBUG_COMMON_OPTS - отладочные опции общие для компилятора и линковщика;
Опции релиза:
RELEASE_COMPILER_OPTS - релизные опции компилятора;
RELEASE_LINKER_OPTS - релизные опции линковщика;
RELEASE_COMMON_OPTS - релизные опции общие для компилятора и линковщика.
Следующие переменные DEBUG_BUILD, DEBUG_LINK, RELEASE_BUILD и RELEASE_LINK складывают команды на компиляцию и линковку из предыдущих переменных. Их редактировать не нужно.
Итак, первая цель, цель по умолчанию, по традиции называется all. Цель all скомпилирует и слинкует программу в режиме, заданном переменной DEFAULT_BUILD_TYPE.
Отдельные цели debug и release компилируют и линкуют соответствующие конфигурации программы.
Цели debug_build и release_build только компилируют, но не линкуют приложение, а цели debug_link и release_link, напротив, только линкуют программу.
Цель clean очищает каталог от объектных файлов и файла приложения. Обратите внимание на то, что использованы команды rm. Для успешной работы в ОС Windows их следует заменить на команды del или установить в систему соответствующую программу, например с пакетом MSYS.

В опциях линкера мы указали библиотеку boost_thread. Но я не указал каталог, в котором она находится. Я этого не сделал потому, что в ОС Linux, которую я использую, эта библиотека находится в стандартной директории, путь до которой известен компилятору. Пользователям ОС Window придётся указать путь в опции -L (например -LC:boostlib). Аналогичным образом стоит поступить и с каталогом, содержащим заголовочный файл boost/thread.hpp. Для этого в опциях компилятора указываем путь до этого каталога в параметре -I.
Указываем имя программы: test. Для ОС Windows не обязательно указывать расширение .exe, компилятор g++ допишет его сам.
Дописываем имя нашего файла с исходными текстами: main.cpp. Теперь всё готово для компиляции. Откройте консоль и перейдите в каталог с Makefile'ом и файлом main.cpp и запустите команду make. Пользователям ОС Windows придётся установить комплект программ MinGW и поместить путь до подкаталога bin в переменную окружения PATH. Кроме того, команда для ОС Windows будет не make, а mingw32-make. После успешной компиляции проекта (для компиляции моего примера Вам понадобится библиотека boost), можно запускать программу.
sergey@debian:~/temp/cpp$ make
g++ -Wall -g -O0 -c main.cpp
g++ -Wall -g -O0 -lboost_thread *.o -o test
sergey@debian:~/temp/cpp$ ./test
one: 1 from 12
one: 2 from 12
one: 3 from 12

two: 1 from 8
two: 2 from 8
two: 3 from 8
two: 4 from 8

one: 4 from 12
one: 5 from 12
one: 6 from 12

two: 5 from 8
two: 6 from 8
two: 7 from 8
two: 8 from 8

one: 7 from 12
one: 8 from 12
one: 9 from 12

one: 10 from 12
one: 11 from 12
one: 12 from 12
Первым управление может захватить второй поток, тогда результат будет немного отличаться.

Определяем, лежит ли точка внутри полигона

Эта статья является продолжением цикла "Математика в программировании". На этот раз я хочу показать, как можно по координатам определить, лежит ли точка внутри замкнутого многоугольника или нет. Для решения этой задачи существует несколько способов. Оптимальный, для вычислительной машины, способ заключается в подсчёте количества пересечённых сторон лучём проведённым из проверяемой точки.


Как видно из рисунка 1, если точка принадлежит полигону, то луч пересечёт нечётное количество сторон. Если же луч пересечёт чётное количество сторон, или ниодной, то это значит, что точка лежит вне полигона.
У данного способа есть недостаток, связанный с случаем, когда точка проходит через вершину, но в большинстве случаев такой вероятностью можно пренебречь. Кроме того, можно выполнить дополнительные действия для изменения условия, например изменить направление луча, если последний проходит через одну из вершин.
Недавно я начать изучать язык программирования Java и в связи с этим решил реализовать пример к этой статье именно на нём. Это мой первый опус на Java, так что не обессудьте.
Итак, что должно получится в итоге:
В окне можно "натыкать" левой кнопкой мыши точек, которые будут являться вершинами полигона. Во время рисования все линии будут отображаться. Закончить рисовать полигон нужно нажатием правой кнопки, после чего все щелчки левой кнопкой будут приводить к установке точки, вхождение которой нужно проверить. Для того, чтобы начать всё сначала нужно нажать Esc.
Итак, исходники:


Файл CPoints.java - это мой вспомогательный класс, который я использовал, для хранения массивов точек. Он динамически выделяет память под массивы блоками.
package polygon;


public class CPoints
{

// Массив абсцисс.
private int[] m_x;
// Массив ординат.
private int[] m_y;
// Количество точек.
private int m_count;
// Вместимость.
private int m_capacity;
// Количество элементов, добавляемых при расширении.
private final int m_block_size;
// Минимальный размер m_block_size.
private final int m_block_size_minimal = 10;

// Устанавливает размер блока в значение по умолчанию.
public CPoints()
{
m_block_size = m_block_size_minimal;
m_count = 0;
m_capacity = 0;
increase();
}

// default_block_size - размер блока, если меньше чем
// m_block_size_minimal,то игнорируется.
public CPoints(int default_block_size)
{
if(default_block_size < m_block_size_minimal)
default_block_size = m_block_size_minimal;
m_block_size = default_block_size;
m_count = 0;
m_capacity = 0;
increase();
}

// Добавляет точку в конец массива.
public void push(final int x, final int y)
{
// Если добавлять некуда, то увеличиваем размер массивов.
if(m_count == m_capacity)
increase();

m_x[m_count] = x;
m_y[m_count] = y;
m_count++;
}

// Удаляет последнюю точку.
public void pop()
{
if(m_count > 0)
m_count--;
}

/// Возвращает размер массива.
public int count()
{
return m_count;
}

// Возвращает массив X'ов.
public final int[] getXArray()
{
return m_x;
}

// Возвращает массив Y'ов.
public final int[] getYArray()
{
return m_y;
}

// Увеличевает размер массивов на m_block_size.
private void increase()
{
int new_capasity = m_capacity + m_block_size;
if(m_capacity != 0)
{
int[] tempx = new int[new_capasity];
int[] tempy = new int[new_capasity];

for(int i = 0; i < m_capacity; i++)
{
tempx[i] = m_x[i];
tempy[i] = m_y[i];
}

m_x = tempx;
m_y = tempy;
}
else
{
m_x = new int[new_capasity];
m_y = new int[new_capasity];
}
m_capacity = new_capasity;
}
}

Непосредственно сам определитель, модуль CDeterminant.java
package polygon;


public class CDeterminant
{

public static boolean determine(final CPoints points, int x, int y)
{

boolean result = false;
int count = points.count();
int[] xp = points.getXArray();
int[] yp = points.getYArray();


for (int i = 0, j = count - 1; i < count; j = i++)
{
if(xp[j] == xp[i] || yp[j] == yp[i]) // Деление на ноль.
continue;

if(((yp[i] <= y && y < yp[j]) || (yp[j] <= y && y < yp[i])))
{
float x1 = xp[i];
float y1 = yp[i];
float x2 = xp[j];
float y2 = yp[j];

float k = (y2 - y1) / (x2 - x1);
float b = y1 - k * x1;
float cross_x = (y - b) / k;

if((float)x > cross_x)
result = !result;
}
}
return result;
}
}

Здесь я нарочно не стал оптимизировать, даже наоборот, написал всё более подробно для того, чтобы было проще понять. Мы в цикле берём каждую пару смежных точек полигона и находим точку пересечения с лучём f(x) = y, где y - ордината определяемой точки. Луч проводим в левую сторону. Сначала определяем, пересекутся ли вообще отрезки. Затем находим абсциссу пересечения (ордината известна исходя из того, что наш луч параллелен оси абсцисс). Так как все вычисления выполняются в числах с плавающей точкой, то я привёл сразу всё к типу float. Далее восстанавливаем уравнение прямой стороны полигона, находя тангенс угла наклона - коэффициент при x и точку пересечения с осью ординат. Затем вычисляем точку пересечения с лучём. Если точка пересечения оказывается левее точки, то мы получили необходимое пересечение и отмечаем его инвентированием результирующего флага.
Оптимизированную версию этого алгоритма можно найти здесь.

Ну и далее я приведу остальные части программы.
Файл CMainWindow.java с главным окном
package polygon;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JPanel;
import javax.swing.JFrame;
import javax.swing.JOptionPane;

class CPanel
extends JPanel
{

// Флаг того, что полигон был замкнут.
private boolean m_polygon_end;
// Вершины полигона.
private CPoints m_points;
// Абсциса определяемой точки.
private int m_x;
// Ордината определяемой точки.
private int m_y;
// Флаг того, что точка была установлена.
private boolean m_point_end;

public CPanel()
{
m_points = new CPoints();
m_polygon_end = false;
m_point_end = false;

CMouseHandler mouse_handler = new CMouseHandler();
addMouseListener(mouse_handler);
addMouseMotionListener(mouse_handler);
addKeyListener(new CKeyHandler());
setFocusable(true); // Для обработки клавиатурных сообщений.
setBackground(Color.white);
}

@Override public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D graphics = (Graphics2D)g;

if(m_polygon_end)
{
graphics.drawPolygon(m_points.getXArray(),
m_points.getYArray(), m_points.count());
if(m_point_end)
graphics.fillOval(m_x - 5, m_y - 5, 10, 10);
}
else
{
graphics.drawPolyline(m_points.getXArray(),
m_points.getYArray(), m_points.count());
}
}


private class CMouseHandler
extends MouseAdapter
{

@Override public void mouseClicked(MouseEvent event)
{
if(!m_polygon_end)
{
if(event.getButton() == MouseEvent.BUTTON1)
{
m_points.push(event.getX(), event.getY());
repaint();
}
else if(event.getButton() == MouseEvent.BUTTON3)
{
// В полигоне не может быть меньше трёх точек.
if(m_points.count() >= 4)
{
m_points.pop(); // Убираем хвост.
m_polygon_end = true;
repaint();
}
}
}
else if(event.getButton() == MouseEvent.BUTTON1)
{
m_x = event.getX();
m_y = event.getY();
m_point_end = true;
repaint();
String msg = CDeterminant.determine(m_points, m_x, m_y) ?
"Точка лежит внутри полигона" :
"Точка лежит вне полигона";
JOptionPane.showMessageDialog(null, msg, "Положение точки",
JOptionPane.INFORMATION_MESSAGE);
}
}

@Override public void mouseMoved(MouseEvent event)
{
if(!m_polygon_end)
{
m_points.pop();
m_points.push(event.getX(), event.getY());
repaint();
}
}
} // private class CMouseHandler

private class CKeyHandler
extends KeyAdapter
{

@Override public void keyPressed(KeyEvent event)
{
if(event.getKeyCode() == KeyEvent.VK_ESCAPE)
{
m_points = new CPoints();
m_point_end = false;
m_polygon_end = false;
repaint();
}
}
} // private class CKeyHandler
} // class CPanel

public class CMainWindow
extends JFrame
{

public CMainWindow()
{
setTitle("Определение принадлежности точки полигону");
setSize(800, 600);
setBackground(Color.white);

getContentPane().add(new CPanel());
}
}
И файл, запускающий всё это добро - CMain.java:

package polygon;

public class CMain
{

public static void main(String params[])
{
CMainWindow wnd = new CMainWindow();
wnd.setDefaultCloseOperation(CMainWindow.EXIT_ON_CLOSE);
wnd.setVisible(true);
}
}

Пишем часы со стрелками

Цель этой статьи - показать практическое использование некоторых тригонометрических функций в программировании.

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

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



На рисунке 1 показана окружность радиуса r = 1 с центром в точке O. Произвольная точка A заданная на окружности образует дугу AB и угол α, численно (в радианах) равных друг другу.
Проекция точки A на ось абсцисс (x) называется косинусом угла α или дуги AB.
Проекция точки A на ось ординат (y) называется синусом угла α или дуги AB.
Если рассматривать полученные треугольники AxOA или AyOA, то можно говорить о следующих отношениях.
Косинусом острого угла называется отношение прилежащего катета к гипотенузе.
Синусом острого угла называется отношение противолежащего катета к гипотенузе.
Углы в системе координат откладываются против часовой стрелки.
Так как длина всей окружности равна 2πr, а радиус нашей окружности равен единице, то длина окружности в нашем случае равна . То есть, угол в 180 градусов соответствует π радиан. Из этого мы можем вывести формулу: 1 радиан = 180 / π.
Более подробную информацию Вы можете получить, например, здесь.


Результатом нашего проекта должны стать часы, показанные на рисунке 2.




Итак, далее я приведу полный исходный текст программы, а затем прокомментирую непонятные моменты. Программа написана с использованием библиотеки Qt.

Файл Clock.h
#ifndef CLOCK_H
#define CLOCK_H

#include <QtGui/QWidget>
#include <QtCore/QPoint>

class CClock : public QWidget
{
Q_OBJECT
public:
explicit CClock(QWidget *parent = 0);

public slots:
void onTimer();

protected:
virtual void paintEvent(QPaintEvent * event);

private:
QPoint rotatePoint(const QPoint & point, int degree, double radius);
};

#endif // CLOCK_H

Файл Clock.cpp
#include "Clock.h"
#include <QtGui/QPainter>
#include <QtCore/QTime>
#include <QtCore/QTimer>
#include <cmath>
//===================================================================

CClock::CClock(QWidget *parent) :
QWidget(parent)
{
setMinimumSize(500, 500);
QTimer * timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(onTimer()));
timer->start(1000);
}
//===================================================================

void CClock::onTimer()
{
QWidget::update();
}
//===================================================================

void CClock::paintEvent(QPaintEvent * /*event*/)
{
//
// константы для отрисовки.
//

// цвет окружности.
static const QColor circle_color(88, 88, 88, 255);
// цвет засечек часов.
static const QColor stroke_hour_color(19, 66, 91, 255);
// цвет засечек минут.
static const QColor stroke_min_color(100, 120, 120, 255);
// цвет часовой стрелки.
static const QColor hour_hand_color(60, 65, 65, 255);
// цвет минутной стрелки.
static const QColor min_hand_color(90, 105, 105, 255);
// цвет секундной стрелки.
static const QColor sec_hand_color(125, 150, 150, 255);

//
// константы метрик и координат.
//

// ширина линии для отрисовки окружности.
static const int circle_line_width = 5;
// ширина линии штриха (деление).
static const int stroke_line_width = 3;
// длина штриха часов.
static const int stroke_hour_length = 10;
// длина штриха минут.
static const int stroke_min_length = 5;

// зазор меду окружностью и засечками.
static const int spacing = 10;
// абсцисса центра окружности в оконных координатах.
const double circle_center_x = width() / 2;
// ордината центра окружности в оконных координатах.
const double circle_center_y = height() / 2;
// радиус окружности.
const double circle_radius = (circle_center_x < circle_center_y ?
circle_center_x : circle_center_y) - spacing - circle_line_width;

// радиус, описываемый часовой стрелкой.
const double hour_hand_radius = circle_radius / 2;
// радиус, описываемый минутной стрелкой.
const double min_hand_radius = hour_hand_radius + circle_radius / 6;
// радиус, описываемый секундной стрелкой.
const double sec_hand_radius = hour_hand_radius + circle_radius / 4;

static const double hour_hand_tail = 20.0; // длина хвоста часовой стрелки.
static const double min_hand_tail = 30.0; // длина хвоста минутной стрелки.
static const double sec_hand_tail = 40.0; // длина хвоста секундной стрелки.

// половина основания часовой стрелки.
static const double hour_hand_half_found = 15.0;
// половина основания минутной стрелки.
static const double min_hand_half_found = 10.0;
// половина основания секундной стрелки.
static const double sec_hand_half_found = 5.0;

// радиус, описываемый крайними точками хвоста часовой стрелки.
const double hour_hand_tail_radius = ::sqrt(::pow(hour_hand_tail, 2) +
::pow(hour_hand_half_found, 2));
// радиус, описываемый крайними точками хвоста минутной стрелки.
const double min_hand_tail_radius = ::sqrt(::pow(min_hand_tail, 2) +
::pow(min_hand_half_found, 2));
// радиус, описываемый крайними точками хвоста секундной стрелки.
const double sec_hand_tail_radius = ::sqrt(::pow(sec_hand_tail, 2) +
::pow(sec_hand_half_found, 2));


// координаты часовой стрелки в начальном состоянии (указывает на 3).
//
// конец стрелки.
const QPoint hour_hand_a0(hour_hand_radius, 0);
// координаты первой точки основания часовой стрелки.
const QPoint hour_hand_b0(-hour_hand_tail, hour_hand_half_found);
// координаты второй точки основания часовой стрелки.
const QPoint hour_hand_c0(-hour_hand_tail, -hour_hand_half_found);

// координаты минутной стрелки в начальном состоянии (указывает на 3).
//
// конец стрелки.
const QPoint min_hand_a0(min_hand_radius, 0);
// координаты первой точки основания минутной стрелки.
const QPoint min_hand_b0(-min_hand_tail, min_hand_half_found);
// координаты второй точки основания минутной стрелки.
const QPoint min_hand_c0(-min_hand_tail, -min_hand_half_found);

// координаты секундной стрелки в начальном состоянии (указывает на 3).
//
// конец стрелки.
const QPoint sec_hand_a0(sec_hand_radius, 0);
// координаты первой точки основания секундной стрелки.
const QPoint sec_hand_b0(-sec_hand_tail, sec_hand_half_found);
// координаты второй точки основания секундной стрелки.
const QPoint sec_hand_c0(-sec_hand_tail, -sec_hand_half_found);


//
// рисуем.
//

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);

// устанавливаем новое начало координат.
painter.translate(circle_center_x, circle_center_y);

// копируем перо из устройства рисования.
QPen pen = painter.pen();

// рисуем окружность.
//
// устанавливаем ширину и цвет линии пера.
pen.setWidth(circle_line_width);
pen.setColor(circle_color);
// устанавливаем перо в устройство рисования.
painter.setPen(pen);
// рисуем.
painter.drawEllipse(QPoint(0, 0), static_cast<int>(circle_radius),
static_cast<int>(circle_radius));

// рисуем 60 засечек.
//
// крайняя к окружности точка.
const QPoint p1(circle_radius - circle_line_width - spacing, 0);
// вторая точка для штрихов часов.
const QPoint p2(p1.x() - stroke_min_length, 0);
// вторая точка для штрихов минут.
const QPoint p3(p1.x() - stroke_hour_length, 0);
pen.setWidth(stroke_line_width);
pen.setColor(stroke_min_color);
painter.setPen(pen);
for(int i = 0; i < 60; i++)
{
if(i % 5 == 0)
{
pen.setColor(stroke_hour_color);
painter.setPen(pen);
painter.drawLine(p1, p3);
pen.setColor(stroke_min_color);
painter.setPen(pen);
}
else
{
painter.drawLine(p1, p2);
}
painter.rotate(6.0);
}

// рисуем стрелки.
//
QPoint points[3]; // точки для рисования.
// узнаём текущее время.
QTime cur_time = QTime::currentTime();
// часовая стрелка.
//
// угол часовой стрелки от начального состояния (3 часа).
const int hour_beta = 90 - (cur_time.hour() * 30
+ cur_time.minute() / 2);
points[0] = rotatePoint(hour_hand_a0, hour_beta, hour_hand_radius);
points[1] = rotatePoint(hour_hand_b0, hour_beta, -hour_hand_tail_radius);
points[2] = rotatePoint(hour_hand_c0, hour_beta, -hour_hand_tail_radius);
painter.setPen(Qt::NoPen);
painter.setBrush(hour_hand_color);
painter.drawConvexPolygon(points, 3);
// минутная стрелка.
//
// угол минутной стрелки от начального состояния (3 часа).
const int min_beta = 90 - cur_time.minute() * 6;
points[0] = rotatePoint(min_hand_a0, min_beta, min_hand_radius);
points[1] = rotatePoint(min_hand_b0, min_beta, -min_hand_tail_radius);
points[2] = rotatePoint(min_hand_c0, min_beta, -min_hand_tail_radius);
painter.setBrush(min_hand_color);
painter.drawConvexPolygon(points, 3);
// секундная стрелка.
//
// угол секундной стрелки от начального состояния (3 часа).
const int sec_beta = 90 - cur_time.second() * 6;
points[0] = rotatePoint(sec_hand_a0, sec_beta, sec_hand_radius);
points[1] = rotatePoint(sec_hand_b0, sec_beta, -sec_hand_tail_radius);
points[2] = rotatePoint(sec_hand_c0, sec_beta, -sec_hand_tail_radius);
painter.setBrush(sec_hand_color);
painter.drawConvexPolygon(points, 3);

// переварачиваем всё вверх ногами, так как на экране направление
// оси ординат сверху вниз.
painter.rotate(180);
}
//===================================================================

QPoint CClock::rotatePoint(const QPoint & point, int degree, double radius)
{
static const double pi = 3.14159265359; // число Пи.

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

// старый угол.
double old_degree_rad = ::asin(point.y() / radius);
// переводим угол в радианы.
double degree_rad = degree * pi / 180.0;
// новый угол.
double new_degree_rad = old_degree_rad - degree_rad;

return QPoint(::cos(new_degree_rad) * radius, ::sin(new_degree_rad) * radius);
}
//===================================================================

Ну, и использование класса для отображения в простейшем случае выглядит так
#include <QtGui/QApplication>
#include "Clock.h"

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
CClock w;
w.show();
return a.exec();
}





Весь необходимый код помещён в методе paintEvent класса CClock. В начале мы определяем множество констант, которые обуславливают цвет и размеры наших часов. О них я говорить не буду, так как они достаточно подробно закомментированны.

Мы определяем координаты для всех стрелок в начальном состоянии. За начальное состояние
я принял угол равный 0 градусов. На рисунке 3 показано начальное состояние стрелки, соответствующее треугольнику AoBoCo. Стрелку мы условно разделяем на две части: собственно стрелку и хвост. Хвост - это та часть, которая находится с обратной стороны точки вращения стрелки. Более подробно хвостовая часть показана на рисунке 4.

На рисунке 4 мы видим треугольники OBoDo и OCoDo. Из любого из них мы можем легко найти радиус окружности по теореме Пифагора.

Итак, координаты стрелки в начальном состоянии таковы:
Ao: x равен длине отрезка OAo, у равен нулю.
Bo: x равен длине отрезка ODo, взятой с отрицательным знаком, y равен длине отрезка DoBo, взятой с отрицательным знаком.
Co: x равен длине отрезка ODo, взятой с отрицательным знаком, y равен длине отрезка DoCo.

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

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

Обёртка для вызова функций из dll при динамическом связывании

Как-то раз мне пришлось загружать очень много функций из dll. После десятого вызова GetProcAddress и добавление переменной в класс (к тому же функции вызывались не из одной библиотеки, что приводило ещё к вызовам LoadLibrary и FreeLibrary) я решил автоматизировать этот процесс раз и навсегда.

#ifndef DYNAMIC_LIB_PROC
#define DYNAMIC_LIB_PROC

class CDynamicLibProcException
{
// Здесь какая-то начинка исключения.
};

struct CUnloadLibOnExeption
{
void operator () (HMODULE hlib)
{
if(hlib)
::FreeLibrary(hlib);
}
};

struct CNotUnloadLibOnExeption { void operator () (HMODULE) { } };

template<typename ProcT, typename ExceptionActionT = CUnloadLibOnExeption>
class CDynamicLibProc
{
public:
CDynamicLibProc(LPCTSTR libname, LPCSTR procname,
bool unload_lib_on_destroy = true) :
m_hlib(::LoadLibrary(libname)),
m_unload_lib_on_destroy(unload_lib_on_destroy)
{
LoadProc(procname);
}

CDynamicLibProc(const HMODULE hlib, LPCSTR procname,
bool unload_lib_on_destroy = false) :
m_hlib(hlib),
m_unload_lib_on_destroy(unload_lib_on_destroy)
{
LoadProc(procname);
}

~CDynamicLibProc()
{
if(m_unload_lib_on_destroy && m_hlib)
::FreeLibrary(m_hlib);
}

HMODULE GetLibHandle() const
{
return m_hlib;
}

public:
ProcT Execute;

private:

void LoadProc(LPCSTR procname)
{
if(!m_hlib)
throw CDynamicLibProcException(); // Тут Вы дополняете своё исключение.
Execute = (ProcT)::GetProcAddress(m_hlib, procname);
if(!Execute)
{
ExceptionActionT ea;
ea(m_hlib);
throw CDynamicLibProcException(); // Тут Вы дополняете своё исключение.
}
}

private:
HMODULE m_hlib;
bool m_unload_lib_on_destroy;
};

#endif // #ifndef DYNAMIC_LIB_PROC

Ну, а использовать этот класс можно так
#include <windows.h>
#include <tchar.h>
#include "DynamicLibProc.h"

int main()
{
CDynamicLibProc<DWORD (WINAPI *)(HWND, LPCWSTR, DWORD, DWORD)>
message_box(_T("user32.dll"), "MessageBoxW");
message_box.Execute(NULL, L"Hello from MessageBox", NULL, 0);
return 0;
}