Архив рубрики: C Plus Plus

Знакомство с Boost.Spirit.Qi

Столкнувшись с необходимостью написания парсера для небольшого встроенного DSL, я решил использовать Boost.Spirit. Но оказалось, что использовать библиотеку человеку, плохо знакомому с boost, весьма не просто. В этой статье я попытаюсь познакомить Вас с основными аспектами работы с Boost.Spirit, чтобы помочь быстрее начать пользоваться этой замечательной библиотекой.

Библиотека Boost.Spirit состоит из нескольких частей: Classic, Qi, Lex и Karma. Classic -- это старая реализация (1.x) Boost.Spirit, оставленная для совместимости. Lex -- это токенайзер, позволяющий разбить входящий текст на токены. Karma служит для форматирования аутпута. В этой же статье речь пойдёт о Qi -- второй версии парсера Boost.Spirit.

Boost.Spirit.Qi использует расширенную форму Бэкуса-Наура (EBNF), но, в отличие от yacc, не генерирует код парсера, а позволяет писать EBNF прямо в коде.

Для того, чтобы познакомиться с библиотекой, нам понадобится простой формальный язык. Пусть это будет арифметическое выражение со скобками и знаками "+" и "-", например:
1
(1)
1 - 2
1 - 2 + 3
(1 + 2)
3 + (1 + 2)
(1 + 2) + 3
(1 + 2 + 3) + 4
(1 + 2) + (3 + 4)
(1 + (2 + 3)) + 4 + (5 + 6)
(1 + (2 + 3)) + (4 + 5) + 6
1 + 2 + (3 - 4 + (5 + 6 - (7 - 8 + 9) - 10) - 11 + 12) + 13 + 14
Этого примера достаточно для понимания того, что нужно делать для написания собственного парсера. Вместе с этим, этот пример достаточно прост, чтобы не загромождать эту статью кодом.

Первое и самое сложное, что необходимо для создания парсера -- продумать EBNF языка. Для представленного выше языка EBNF будет следующей:
Оператор ::= "+" | "-"
Операнд ::= число | <Выражение в скобках>
<Выражение в скобках> ::= "(" Выражение ")"
<Правая часть выражения> ::= Оператор Операнд
Выражение ::= Операнд { <Правая часть выражения> }
Далее нужно описать структуры данных для хранения данных каждого правила. Для того, чтобы одно и то же поле могло хранить данные разных типов (что необходимо, например, для реализации правила Операнд), следует использовать библиотеку Boost.Variant, которая поддерживается парсерами Spirit. Эта же библиотека позволяет разрешить проблему с рекурсивным вхождением правил друг в друга.
Итак, давайте посмотрим на заголовочный файл нашего парсера.
#include <memory>
#include <string>
#include <ostream>
#include <vector>
#include <boost/variant.hpp>

namespace Parser {

enum class Operator
{
plus,
minus
};

typedef int Number;

struct Expression;

typedef boost::variant<Number, boost::recursive_wrapper<Expression>> Operand;

struct RightExpression
{
Operator operator_;
Operand operand;
};

struct Expression
{
Operand operand;
std::vector<RightExpression> right;
};

std::shared_ptr<Expression> parse(const std::string & _input);

} // namespace Parser

std::ostream & operator << (std::ostream & _stream, const Parser::Operator & _operator);
std::ostream & operator << (std::ostream & _stream, const Parser::Operand & _operand);
std::ostream & operator << (std::ostream & _stream, const Parser::Expression & _expression);
Как видно, структуры данных в точности повторяют описания в EBNF. Структура для правила <Выражение в скобках> отсутствует, так как это правило идентично правилу Выражение, за исключением скобок, которые не являются данными. Повторяющиеся вхождения правил может быть записано с помощью стандартного вектора, что мы и сделали для правила Выражение при записи вхождения <Правая часть выражения>.
Наибольший интерес вызывает строка
typedef boost::variant<Number, boost::recursive_wrapper<Expression>> Operand;
Здесь применяется библиотека Boost.Variant. Это выражение декларирует тип Operand, который может хранить либо число, либо Expression. Класс boost::recursive_wrapper позволяет избежать ошибки компиляции из-за использования незавершённых типов. Такая ошибка происходит, потому что для типа Operand требуется тип Expression, а для типа Expression требуется тип Operand.

Описав все необходимые типы, мы сталкиваемся с новой проблемой -- Boost.Spirit не умеет с ними работать. Чтобы Boost.Spirit научился работать с нашими типами, необходимо сгенерировать дополнительную информацию с помощью макроса BOOST_FUSION_ADAPT_STRUCT, входящего в библиотеку Boost.Fusion.
BOOST_FUSION_ADAPT_STRUCT(
RightExpression,
(Operator, operator_)
(Operand, operand)
)

BOOST_FUSION_ADAPT_STRUCT(
Expression,
(Operand, operand)
(std::vector<RightExpression>, right)
)
Теперь мы можем приступить к написанию парсера.
namespace qi = boost::spirit::qi;

class OperatorSymbol : public qi::symbols<char, Operator>
{
public:
OperatorSymbol()
{
add
("+", Operator::plus)
("-", Operator::minus);
}
};

class ExpressionParser :
public qi::grammar<typename std::string::const_iterator, Expression(), qi::space_type>
{
private:
template<typename T>
using Rule = qi::rule<typename std::string::const_iterator, T, qi::space_type>;

public:
ExpressionParser();

private:
OperatorSymbol m_operator;
Rule<Operand()> m_operand;
Rule<RightExpression()> m_right_exp;
Rule<Expression()> m_bracketed_expression;
Rule<Expression()> m_expression;
};

ExpressionParser::ExpressionParser() :
ExpressionParser::base_type(m_expression)
{
m_operand %= qi::int_ | m_bracketed_expression;
m_right_exp %= m_operator >> m_operand;
m_bracketed_expression %= '(' >> m_expression >> ')';
m_expression %= m_operand >> *m_right_exp;
}
Здесь класс OperatorSymbol описывает правила преобразования символа в элемент перечисления. Подобные классы необходимо наследовать от класса boost::spirit::qi::symbols с указанием в шаблоне типа символа и результирующего типа. Экземпляры наследников класса symbols можно использовать в качестве правила. В данном случае, экземпляр класса OperatorSymbol используется для реализации правила Оператор.
Далее, класс парсера, ExpressionParser, наследуется от класса boost::spirit::qi::grammar. Шаблон этого класса первым параметром принимает тип итератора входящих данных. Второй, третий и четвёртый параметры опциональны, но почти всегда в них нужно передавать сигнатуру создания результирующей структуры данных (обычно это сигнатура конструктора по умолчанию) и тип символов, которые будут пропускаться парсером (обычно это пробелы). Более подробно с параметрами шаблона grammar можно ознакомиться в документации.
Для построения EBNF используются стандартные парсеры для чисел, символов и прочего, коих очень много, поэтому я отсылаю Вас за ними в документацию. В данном примере мы можем видеть использование персера boost::spirit::qi::int_. Необходимо отметить, что некоторые  парсеры, такие как char_ лежат внутри неймспейсов с именем, совпадающим с именем кодировки символов, например boost::spirit::qi::ascii::char_. Кроме стандартных парсеров, можно создавать свои правила, используя класс boost::spirit::qi::rule. Шаблон этого класса совпадает с шаблоном grammar.
Конструктор нашего ExpressionParser должен передать в конструктор базового класса правило, которое будет использовано для разбора всего входящего текста. Это правило должно иметь тип, совпадающий с типом, переданным в шаблон grammar. Базовый конструктор проще всего вызывать через typedef base_type, так как имя базового класса весьма длинное.
EBNF записывается в достаточно простом виде. Естественно, я не буду перечислять здесь все доступные операторы, это займёт много места и времени. Вместо этого снова отсылаю Вас в документацию.

Пришло время применить наш парсер.
std::shared_ptr<Expression> Parser::parse(const std::string & _input)
{
ExpressionParser parser;
Expression * expression = new Expression();
std::shared_ptr<Expression> result(expression);
if(qi::phrase_parse(_input.begin(), _input.end(), parser, qi::space, *expression))
return result;
return nullptr;
}
Есть несколько способов применения парсера, наиболее распространённым из них является вызов функции boost::spirit::qi::phrase_parse. Функция возвращает true, если строка распаршена удачно. Для того, чтобы получить место падения парсера, отсылаю Вас к статье на официальном сайте.

Для тестирования парсера нам понадобится выводить содержимое структур данных в консоль. Для этого реализуем операторы <<.
class DefaultPrintVisitor : public boost::static_visitor<>
{
public:
explicit DefaultPrintVisitor(std::ostream & _stream) :
mr_stream(_stream)
{
}

template<typename T>
void operator ()(const T & _value)
{
mr_stream << _value;
}

private:
std::ostream & mr_stream;
};

std::ostream & operator << (std::ostream & _stream, const Operator & _operator)
{
switch(_operator)
{
case Operator::plus:
_stream << "+";
break;
case Operator::minus:
_stream << "-";
break;
}
return _stream;
}

std::ostream & operator << (std::ostream & _stream, const Operand & _operand)
{
DefaultPrintVisitor visitor(_stream);
boost::apply_visitor(visitor, _operand);
return _stream;
}

std::ostream & operator << (std::ostream & _stream, const Expression & _expression)
{
_stream << '(' << _expression.operand;
for(const RightExpression & rexp : _expression.right)
_stream << ' ' << rexp.operator_ << ' ' << rexp.operand;
_stream << ')';
return _stream;
}
Для того, чтобы работать с boost::variant библиотекой предлагается использовать паттерн visitor. Для этого Вам нужно унаследовать Ваш Visitor от boost::static_visitor<>, и определить оператор () для каждого из типов, входящих в шаблон boost::variant. Экземпляр этого класса нужно передать функции boost::apply_visitor вместе со ссылкой на экземпляр boost::variant, что и продемонстрировано в operator << для Operand.

Протестируем то, что получилось на нескольких примерах. Уберём в некоторых местах пробелы и наставим побольше скобок.

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

using namespace Parser;

int main()
{
const std::string sources[] = {
"1",
"(1)",
"1 - 2",
"1 - 2 + 3",
"1-2+3",
"(1 + 2)",
"3 + (1 + 2)",
"(1 + 2) + 3",
"(1-2)+3",
"(1 + 2 + 3) + 4",
"(1 + 2) + (3 + 4)",
"(1 + (2 + 3)) + 4 + (5 + 6)",
"(1 + (2 + 3)) + (4 + 5) + 6",
"1 + 2 + (3 - 4 + (5 + 6 - (7 - 8 + 9) - 10) - 11 + 12) + 13 + 14"
};
for(const std::string & src : sources)
{
std::cout << "Parsing string: " << src << ": ";
std::shared_ptr<Expression> expression = parse(src);
if(expression)
std::cout << "succeeded\nResult: " << *expression;
else
std::cout << "failed";
std::cout << std::endl << std::endl;
}
return 0;
}
Parsing string: 1: succeeded
Result: (1)

Parsing string: (1): succeeded
Result: ((1))

Parsing string: 1 - 2: succeeded
Result: (1 - 2)

Parsing string: 1 - 2 + 3: succeeded
Result: (1 - 2 + 3)

Parsing string: 1-2+3: succeeded
Result: (1 - 2 + 3)

Parsing string: (1 + 2): succeeded
Result: ((1 + 2))

Parsing string: 3 + (1 + 2): succeeded
Result: (3 + (1 + 2))

Parsing string: (1 + 2) + 3: succeeded
Result: ((1 + 2) + 3)

Parsing string: (1-2)+3: succeeded
Result: ((1 - 2) + 3)

Parsing string: (1 + 2 + 3) + 4: succeeded
Result: ((1 + 2 + 3) + 4)

Parsing string: (1 + 2) + (3 + 4): succeeded
Result: ((1 + 2) + (3 + 4))

Parsing string: (1 + (2 + 3)) + 4 + (5 + 6): succeeded
Result: ((1 + (2 + 3)) + 4 + (5 + 6))

Parsing string: (1 + (2 + 3)) + (4 + 5) + 6: succeeded
Result: ((1 + (2 + 3)) + (4 + 5) + 6)

Parsing string: 1 + 2 + (3 - 4 + (5 + 6 - (7 - 8 + 9) - 10) - 11 + 12) + 13 + 14: succeeded
Result: (1 + 2 + (3 - 4 + (5 + 6 - (7 - 8 + 9) - 10) - 11 + 12) + 13 + 14)

Все исходники примера лежат тут.

Virtual inline методы C++

C++ даёт возможность определять встраиваемые (inline) функции и методы. Реализация таких функций будет встраиваться в другие функции по месту вызова (за исключением некоторых случаев, когда это невозможно). Как правило, в конечном исполняемом файле или динамической библиотеке отдельной реализации таких функций не генерируется.
Другой замечательный механизм, предоставляемый языком C++ — это виртуальные методы. Эти методы участвуют в полиморфизме. Вы может вызывать такие функции из производного класс по указателю или ссылке на базовый. Достигается это путём помещения указателя на конкретную реализацию в таблицу виртуальных функций, которая помещается компилятором в известное ему место относительно полиморфного указателя или ссылке (конкретная реализация не стандартизирована и зависит от компилятора).

Стандарт C++ позволяет применять модификатор virtual к inline функциям. Тут мы наталкиваемся на неоднозначность. Как в таблицу виртуальных функций попадёт указатель, если тело функции не генерируется? Ответ на этот вопрос также не стандартизирован и зависит от компилятора. Давайте попробуем разобраться.
Итак, допустим у нас есть такой код
extern "C" void exFunction(int);

class Base
{
public:
virtual int doSomething(int x, int y)
{
return x + y;
}
};

class Child : public Base
{
public:
virtual int doSomething(int x, int y)
{
return x * y;
}
};

extern "C" void run()
{
Base base;
int result = base.doSomething(1, 2);
exFunction(result);
Child child;
result = child.doSomething(2, 2);
exFunction(result);
}
Функция exFunction нужна лишь для того, чтобы компилятор не соптимизировал наши вычисления.
Я буду использовать компилятор g++ 4.7.2 и операционную систему Debian GNU/Linux 7.0 (wheezy) 64 bit.
Сохраним исходный текст в файл test.cpp соберём динамическую библиотеку с помощью команды
g++ -g -O2 -shared -fPIC  test.cpp
Я собираю динамическую библиотеку, чтобы было меньше мусора в дампе. Кроме того, я включил оптимизацию и отладочные символы. Посмотрим результат компиляции с помощью программы objdump.
objdump -d a.out
00000000000006e0 <run>:
6e0: 48 83 ec 08 sub $0x8,%rsp
6e4: bf 03 00 00 00 mov $0x3,%edi
6e9: e8 e2 fe ff ff callq 5d0 <exFunction@plt>
6ee: bf 04 00 00 00 mov $0x4,%edi
6f3: 48 83 c4 08 add $0x8,%rsp
6f7: e9 d4 fe ff ff jmpq 5d0 <exFunction@plt>
Как видим, компилятор не обратил внимание на модификатор virtual и просто встроил inline методы в код, соптимизировав сложение и умножение.
Теперь добавим в код полиморфизма.
extern "C" void exFunction(int);

class Base
{
public:
virtual int doSomething(int x, int y)
{
return x + y;
}
};

class Child : public Base
{
public:
virtual int doSomething(int x, int y)
{
return x * y;
}
};

extern "C" void run()
{
Base base;
int result = base.doSomething(1, 2);
exFunction(result);
Child child;
result = child.doSomething(2, 2);
exFunction(result);
Base * polymorphic = &child;
result = polymorphic->doSomething(3, 3);
exFunction(result);
}
Скомпилируем и посмотрим на дизассемблированный код.
0000000000000a50 <run>:
a50: 48 83 ec 18 sub $0x18,%rsp
a54: bf 03 00 00 00 mov $0x3,%edi
a59: e8 e2 fe ff ff callq 940 <exFunction@plt>
a5e: 48 8b 05 e3 03 20 00 mov 0x2003e3(%rip),%rax # 200e48 <_DYNAMIC+0x228>
a65: bf 04 00 00 00 mov $0x4,%edi
a6a: 48 83 c0 10 add $0x10,%rax
a6e: 48 89 04 24 mov %rax,(%rsp)
a72: e8 c9 fe ff ff callq 940 <exFunction@plt>
a77: 48 8b 04 24 mov (%rsp),%rax
a7b: 48 89 e7 mov %rsp,%rdi
a7e: ba 03 00 00 00 mov $0x3,%edx
a83: be 03 00 00 00 mov $0x3,%esi
a88: ff 10 callq *(%rax)
a8a: 89 c7 mov %eax,%edi
a8c: e8 af fe ff ff callq 940 <exFunction@plt>
a91: 48 83 c4 18 add $0x18,%rsp
a95: c3 retq
a96: 90 nop
a97: 90 nop
a98: 90 nop
a99: 90 nop
a9a: 90 nop
a9b: 90 nop
a9c: 90 nop
a9d: 90 nop
a9e: 90 nop
a9f: 90 nop

0000000000000aa0 <_ZN4Base11doSomethingEii>:
aa0: 8d 04 16 lea (%rsi,%rdx,1),%eax
aa3: c3 retq
aa4: 90 nop
aa5: 90 nop
aa6: 90 nop
aa7: 90 nop
aa8: 90 nop
aa9: 90 nop
aaa: 90 nop
aab: 90 nop
aac: 90 nop
aad: 90 nop
aae: 90 nop
aaf: 90 nop

0000000000000ab0 <_ZN5Child11doSomethingEii>:
ab0: 89 f0 mov %esi,%eax
ab2: 0f af c2 imul %edx,%eax
ab5: c3 retq
ab6: 90 nop
ab7: 90 nop
Теперь мы видим, что компилятор сгенерировал реализации для виртуальных методов и использовал виртуальную таблицу для третьего вызова. При этом первые два вызова остались встроенными.

Вообще говоря, компилятор всегда будет генерировать тело inline функции, если где-либо в программе требуется её адрес (явно или неявно). При этом, Вы можете расчитывать на встраивание virtual inline функции, если не используете для её вызова указатель или ссылку.

Я провёл подобное исследование с Visual C++, и оказалось, что этот компилятор ведёт себя аналогичным образом.

Почему C# хуже C++?

С момента моего ознакомительного исследования C# прошло полтора года. Поработав с языком, я убедился в превосходстве C++ над ним. Когда я говорю об этом, мои оппоненты часто начинают приводить доводы, касательно того, как хорош .NET или, что архитектура CLR накладывает ограничения на язык. Ни тот, ни другой аргументы не могут оправдать C#, так как язык -- это то, что программисты видят каждый день, а CLR скрыт от глаз и не должен оказывать влияния на инструмент разработки, как архитектура процессора не оказывает влияния на C++. В этой статье я не буду обсуждать стандартную библиотеку и рантайм, речь пойдёт только о языке. Не затягивая со вступлением, начну с самых основ.
Якщо вам потрібен якісний сайт або портал, то рекомендуємо звернутися у компанію  web-ukraine, вони допоможуть усе зробити по вашим бажанням, створення сайтів, підтримка сайтів, реклама сайтів все це входить у вартість послуг.

ООП ради ООП

Программируя долгое время на C++, я не задумывался о минусах ОО подхода. Один мой знакомый постоянно спорил со мной, утверждая, что ООП -- это зло. Тогда я не понимал истинных причин его выводов. Лишь начав программировать на C#, я осознал всю несостоятельность объектно-ориентированной парадигмы. Дело в том, что пока я писал на C++, я активно пользовался процедурной парадигмой и ООП лишь помогало там, где оно имело место. В C# (как впрочем и в Java) объектно-ориентированная парадигма вытеснила все другие. В итоге в C# мы имеем множество статических классов-Helper`ов и Utility. Гипертрофия ООП в C# приводит к тому, что классы содержат невероятно избыточное количество данных для настройки своего поведения и поведения своих потомков. Подробнее о недостатках ООП подхода можно почитать здесь. Приведу одну цитату:

Единственный способ сделать повторно используемые бизнес компоненты на этом уровне — сделать их сверх-настраиваемыми путём добавления таких штук как движки правил и встраиваемые языки. Такую модель я бы не стал называть моделью компонента. Скорее моделью очередного экземпляра bloatware. Обещание о повторном использовании ушло, люди либо покупали огромные bloatware системы(неудачники), или разрабатывали свои специальные бизнес объекты в своих собственных проектах.

Кроме того, Вы можете ознакомиться со знаменитой статьёй Ричарда Гэбриэла "Объектная парадигма провалилась" и её триумфальное возрождением "Почему объектно-ориентированное программирование провалилось?". Советую обратить внимание ещё и на эту статью. Подводя итог вышеперечисленным статьям, можно сказать, что концепция ООП не справилась с возложенными на неё ожиданиями, вместо этого она усложнила разработку и архитектуру приложений. C++ же не настаивает на использовании ООП и позволяет получить от него максимум выгоды, используя там, где это оправдано. Доклад Джека Дидриха довольно точно описывает моё отношение к ООП: не нужно писать класс, если можно написать функцию. Исходя из того, что C# ничего, кроме ООП не предлагает, в дальнейшем я буду рассматривать C# и C++ с точки зрения объектно-ориентированной парадигмы.

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

Наследование в C++ -- это одна из самых широких возможностей. C++ позволяет использовать один из нижеприведённых вариантов или любую их комбинацию:

  • Наследование интерфейса;
  • Наследование реализации;
  • Множественное наследование;
  • Виртуальное наследование.

C# изо всего этого поддерживает лишь наследование интерфейса, наследование интерфейса вместе с реализацией и множественное наследование интерфейсов без реализации. Вообще говоря, множественное наследование интерфейсов с реализациями используется довольно редко, а виртуальное ещё реже. Но при возникновении такой необходимости в C# приходится применять сложные обходные пути. К сожалению, реальные примеры слишком сложны, а книжные мало убедительны, тем не менее, я постараюсь показать полезность множественного наследования на небольшом примере. Предположим, что в программе имеется класс Clock (часы) и класс Phone (телефон).

class Clock
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone
{
public:
    PhoneStream * call(const std::string & number);
};

Естественно, у методов должна быть реализация и методов может быть гораздо больше. Теперь Вам необходимо написать класс CellPhone (мобильный телефон). В C++ Вы могли бы использовать множественное наследование для получения функционала часов и телефона в одном устройстве.

class CellPhone : public Clock, public Phone { };

Вместо одной строки кода, на C# придётся написать

class CellPhone {
    Phone phone = new Phone();
    Clock clock = new Clock();

    public Phone Phone {
        get { return phone; }
    }

    public Clock Clock {
        get { return clock; }
    }
}

При такой реализации клиенты этого класса будут вынуждены углубить связность с его внутренней реализацией, что противоречит закону Деметры. Чтобы избежать связности можно реализовать все методы классов Clock и Phone внутри класса CellPhone, но это имеет смысл только при условии, что указанные классы имеют интерфейсы и используются только через них. Теперь усложним задачу. Каждый класс, Clock и Phone, будут унаследованы от класса Device (устройство).

class Device
{
public:
    void powerOn();
    void powerOff();
};

class Clock : public Device
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone: public Device
{
public:
    PhoneStream * call(const std::string & number);
};

class CellPhone : public Clock, public Phone { };

В этом коде есть одна, не слишком очевидная, проблема. Объекты класса CellPhone будут иметь два экземпляра класса Device: один от Clock, другой от Phone. Для таких случаев (которые получили название ромбовидное наследование) в C++ введён специальный тип наследования -- виртуальное наследование.

class Device
{
public:
    void powerOn();
    void powerOff();
};

class Clock : public virtual Device
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone: public virtual Device
{
public:
    PhoneStream * call(const std::string & number);
};

class CellPhone : public Clock, public Phone { };

Теперь объекты CellPhone будут иметь один экземпляр класса Device. C# не предоставляет подобной возможности. В агрегированных объектах у Вас всегда будут храниться два разных объекта класса Device, бездарно расходуя память. Подробно о пользе и вреде множественного наследования  Вы можете прочитать в книге Скотта Майерса "Эффективное использование C++. 50 рекомендаций по улучшению Ваших программ и проектов" в правиле 42: "Продумывайте подход к использованию множественного наследования". Более реальный пример множественного наследования  -- реализация паттерна проектирования Observer (наблюдатель).

#include <iostream>
#include <set>

template<typename T>
class Observable
{
protected:
    Observable() { }
    virtual ~Observable() { }

public:
    bool addObserver(T * observer)
    {
        return m_observers.insert(observer).second;
    }

    bool removeObserver(T * observer)
    {
        return m_observers.erase(observer);
    }

protected:
    std::set<T *> m_observers;
};

class Worker
{
public:
    virtual ~Worker() { }
    virtual void start() = 0;
    virtual void stop() = 0;
};

class WorkerObserver
{
public:
    virtual ~WorkerObserver() { }
    virtual void onStart() = 0;
    virtual void onStop() = 0;
};

class ObservableWorker : public Observable<WorkerObserver>, public Worker
{
public:
    virtual void start()
    {
        for(std::set<WorkerObserver *>::iterator it = m_observers.begin();
            m_observers.end() != it; ++it)
        {
            (*it)->onStart();
        }
    }

    virtual void stop()
    {
        for(std::set<WorkerObserver *>::iterator it = m_observers.begin();
            m_observers.end() != it; ++it)
        {
            (*it)->onStop();
        }
    }
};

class WorkerLogger : public WorkerObserver
{
public:
    virtual void onStart()
    {
        std::cout << "Started" << std::endl;
    }

    virtual void onStop()
    {
        std::cout << "Stoped" << std::endl;
    }
};


int main()
{
    ObservableWorker worker;
    WorkerLogger logger;
    worker.addObserver(&logger);
    worker.start();
    worker.stop();
    std::cin.get();
    return 0;
}

В данном подходе класс ObservableWorker является одновременно и Worker`ом и Observable`ом. Без множественного наследования пришлось бы выбирать между родителями, и выбор был бы в пользу Worker`а, а методы addObserver и removeObserver пришлось бы реализовавать в каждом классе. Кроме упомянутых методов, класс Observable мог бы содержать дополнительные методы работы с коллекцией наблюдателей, но для краткости я не стал раздувать код примера.

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

Инкапсуляция

Один из самых любимых мною, да и всеми C++ программистами, паттерн Private Implementation (например, библиотека Qt очень активно использует этот паттерн) не применим в C# вообще. Причиной этому является открытость всех модулей. В C++, желая скрыть детали имплементации класса, мы можем предварительно объявить в заголовочном файле приватный внутренний класс и указатель на него, а сам интерфейс и реализацию перенести в *.cpp модуль. Тем самым мы скрываем все переменные и дополнительные, вспомогательные методы, которые мы используем, от посторонних глаз. Интерфейс остаётся чистым и понятным при реализации любой сложности. В описанной стратегии очень сложно достучаться до деталей приватной имплементации. В C# при помощи reflection можно вызвать любой приватный метод и изменить приватное состояние объекта всего в одну-две строки кода. Но этот соблазн ещё выше с учётом того, что все внутренности класса находятся в одном модуле.

Объявление синонимов типа

Создатели C# посчитали, что оператор typedef им не нужен. В C++ оператор typedef служит для объявления синонимов типов. Для чего это нужно? К примеру, Вы используете в качестве параметра метода такой тип

Tuple<Dictionary<string, List<MyObject>>, bool>

Вместо того, чтобы всякий раз писать эту длинную последовательность, оператор typedef позволяет сократить запись. В C# это могло бы выглядеть так

typedef Tuple<Dictionary<string, List<MyObject>>, bool> MyType;

И далее, везде, где использовался изначальный длинный тип, можно было бы писать просто MyType. Кроме удобочитаемого имени, оператор typedef используется в C++ для получения типов из других классов или шаблонов. Приведу простой пример использования.

#include <iostream>
#include <vector>

struct Letter
{
    typedef char ValueType;
    char value;
};

struct Number
{
    typedef int ValueType;
    int value;
};

template<typename Element>
class Container
{
public:
    typedef typename Element::ValueType  ElementValueType;

    void add(ElementValueType value)
    {
        Element element;
        element.value = value;
        m_elements.push_back(element);
    }

    void print()
    {
        for(typename std::vector<Element>::const_iterator it = m_elements.begin();
            m_elements.end() != it; ++it)
        {
            std::cout << it->value << " ";
        }
    }
    
private:
    std::vector<Element> m_elements;
};


void testLetter()
{
    Container<Letter> container;
    container.add('a');
    container.add('b');
    container.add('c');
    std::cout << "Letters: ";
    container.print();
    std::cout << std::endl;
}

void testNumber()
{
    Container<Number> container;
    container.add(1);
    container.add(2);
    container.add(3);
    std::cout << "Numbers: ";
    container.print();
    std::cout << std::endl;
}

int main()
{
    testLetter();
    testNumber();
    std::cin.get();
}
Letters: a b c
Numbers: 1 2 3

Класс Container получает из типа шаблона тип значения, которое хранится в некотором другом классе и которое доступно из переменной value. Далее класс Container может оперировать этим типом, в том числе и в параметрах методов. Typedef`ы типов могут быть вложены и тогда классы Number и Letter могут стать одним шаблоном. Работают такие конструкции во время компиляции, что обеспечивает почти полную защиту от ошибок. Более сложные примеры с большой вложенностью Вы сможете найти в заголовочных файлах контейнеров (std::vector, std::map итд) стандартной библиотеки шаблонов (STL) С++.

Обобщения

Одним из самых мощных инструментов в C++ являются шаблоны. Сразу приведу пример, с которым я недавно столкнулся. Представим, что у нас есть абстрактный класс Connection

class Connection
{
public:
    virtual ~Connection() { }
    virtual bool connect(const std::string & address, short port) = 0;
    virtual void disconnect() = 0;
    virtual bool isConnected() const = 0;
    virtual void send(char * buffer, size_t size) = 0;
    virtual size_t recv(char * buffer, size_t size) = 0;
};

И от него унаследованы два (или более) конкретных класса: TcpConnection и SshConnection

class TcpConnection : public Connection
{
public:
    inline TcpConnection();
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
    virtual bool isConnected() const;
    virtual void send(char * buffer, size_t size);
    virtual size_t recv(char * buffer, size_t size);

private:
    bool m_is_connected;
};

TcpConnection::TcpConnection() :
    m_is_connected(false)
{
}

bool TcpConnection::connect(const std::string & address, short port)
{
    std::cout << "TcpConnection::connect" << std::endl;
    m_is_connected = ::rand() % 2;
    return m_is_connected;
}

void TcpConnection::disconnect()
{
    std::cout << "TcpConnection::disconnect" << std::endl;
    m_is_connected = false;
}

bool TcpConnection::isConnected() const
{
    std::cout << "TcpConnection::isConnected" << std::endl;
    return m_is_connected;
}

void TcpConnection::send(char * buffer, size_t size)
{
    std::cout << "TcpConnection::send" << std::endl;
}

size_t TcpConnection::recv(char * buffer, size_t size)
{
    std::cout << "TcpConnection::recv" << std::endl;
    return size;
}
class SshConnection : public Connection
{
public:
    inline SshConnection();
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
    virtual bool isConnected() const;
    virtual void send(char * buffer, size_t size);
    virtual size_t recv(char * buffer, size_t size);

private:
    bool m_is_connected;
};

SshConnection::SshConnection() :
    m_is_connected(false)
{
}

bool SshConnection::connect(const std::string & address, short port)
{
    std::cout << "SshConnection::connect" << std::endl;
    m_is_connected = ::rand() % 2;
    return m_is_connected;
}

void SshConnection::disconnect()
{
    std::cout << "SshConnection::disconnect" << std::endl;
    m_is_connected = false;
}

bool SshConnection::isConnected() const
{
    std::cout << "SshConnection::isConnected" << std::endl;
    return m_is_connected;
}

void SshConnection::send(char * buffer, size_t size)
{
    std::cout << "SshConnection::send" << std::endl;
}

size_t SshConnection::recv(char * buffer, size_t size)
{
    std::cout << "SshConnection::recv" << std::endl;
    return size;
}

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

template<typename T>
class ConnectionAdapter : public T
{
public:
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
};

template<typename T>
bool ConnectionAdapter<T>::connect(const std::string & address, short port)
{
    bool connected = T::connect(address, port);
    std::cout << "Log: " << (connected ? "Connected" : "Connection failed") << std::endl;
    return connected;
}

template<typename T>
void ConnectionAdapter<T>::disconnect()
{
    bool connected = T::isConnected();
    T::disconnect();
    std::cout << "Log: " << (connected ? "Disconnected" :
        "Doesn't disconnected because connection hasn't been established") << std::endl;
}

Теперь объекты класса ConnectionAdapter можно использовать точно так же, как и объекты типа его параметра.

void tcpFunction(TcpConnection & connection)
{
    connection.connect("example.com", 80);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}

void sshFunction(SshConnection & connection)
{
    connection.connect("example.com", 22);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}

void commonFunction(Connection & connection)
{
    connection.connect("example.com", 123);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}


int main()
{
    ::srand(::time(NULL));

    ConnectionAdapter<TcpConnection> tcp_connection;
    ConnectionAdapter<SshConnection> ssh_connection;
    tcpFunction(tcp_connection);
    std::cout << std::endl;
    sshFunction(ssh_connection);
    std::cout << std::endl;
    commonFunction(tcp_connection);
    std::cout << std::endl;
    commonFunction(ssh_connection);
    std::cin.get();
    return 0;
}

Вы не сможете сделать нечто подобное на C#. C# не позволяет наследоваться от параметра обобщения. Таким образом, единственное, что Вам оставляет этот язык -- унаследовать базовый класс, Connection, реализовать все его абстрактные методы при помощи агрегированного объекта конкретного класса, и дописать в двух нужных методах по паре строк кода. И даже сделав это, Вы не сможете использовать объекты данного класса как объекты конкретного класса. Кроме приведённого примера, есть ещё одна очень удобная возможность использовать подобный механизм в купе с приватным наследованием. Передавая в шаблон некоторый тип Вы изменяете реализацию для шаблонного класса, хотя, как уже было сказано, можно заменить это простой агрегацией. Другим серьёзным недостатком обобщений C# является невозможность специализации. Если Вы точно знаете, что для определённого типа данных Ваш шаблон будет работать крайне не оптимально, то Вы можете создать для этого типа отдельную реализацию. C++ позволяет специализировать как все, так и любое количество аргументов шаблона.

#include <iostream>
#include <string>

template<typename T1, typename T2>
class Tuple
{
public:
    Tuple(const T1 & p1, const T2 & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    T1 m_p1;
    T2 m_p2;
};

template<typename T>
class Tuple<T, int>
{
public:
    Tuple(const T & p1, const int & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << "T, int specialization: " << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    T m_p1;
    int m_p2;
};

template<>
class Tuple<std::string, int>
{
public:
    Tuple(const std::string & p1, const int & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << "std::string, int specialization: " << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    std::string m_p1;
    int m_p2;
};


int main()
{
    Tuple<std::string, std::string>("Hello", "World").print();
    Tuple<int, int>(100, 42).print();
    Tuple<std::string, int>("Hello", 42).print();
    std::cin.get();
    return 0;
}

Этот код даст на выходе такой результат

Hello, World
T, int specialization: 100, 42
std::string, int specialization: Hello, 42

Как видно, компилятор выбирает наиболее подходящую реализацию исходя из параметров шаблона. К сожалению, для специализации шаблонов необходимо полностью повторить интерфейс и реализацию шаблона. Но, имея такой мощный инструмент, как препроцессор, в C++ это не является серьёзной проблемой. Справедливости ради, стоит отметить, что обобщения C# строго типизированы и поддерживают ковариантность (out) и контравариантность (in) аргументов, что обеспечивает поддержку наследования. Для того, чтобы C++ поддерживал эти механизмы необходимо вручную писать операторы преобразования типов.

Препроцессор

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

class Exception
{
public:
    Exception() :
        m_id(0)
    {
    }
    
    explicit Exception(int id) :
        m_id(id)
    {
    }

    Exception(int id, const std::string & message) :
        m_id(id),
        m_message(message)
    {
    }

private:
    int m_id;
    std::string m_message;
};

И вместо того, чтобы каждый раз, когда необходимо создать наследника этого класса, писать реализацию всех конструкторов, просто вызывающих базовый конструктор (думаю, что все сталкивались с этой задачей в C#), С++ позволяет написать один макрос.

#define DECLARE_EXCEPTION(name)                     \
    class name : public Exception                   \
    {                                               \
    public:                                         \
        name() { }                                  \
        name(int id) :                              \
            Exception(id)                           \
        {                                           \
        }                                           \
        name(int id, const std::string & message) : \
            Exception(id, message)                  \
        {                                           \
        }                                           \
    };

Всё, что нужно написать для определения двух классов -- две строки.

DECLARE_EXCEPTION(NotFoundException)
DECLARE_EXCEPTION(InvalidArgumentException)

Современные IDE, включая Visual Studio, способны выдавать подсказки по сгенерированному таким образом коду.

Неизменяемость

C# ограничивает понятие константности только лишь статическими ссылками на примитивные типы (POD),  дополнительно вводит ключевое слово readonly для того, чтобы ссылке нельзя было переприсвоить объект. Этих механизмов не достаточно, чтобы обеспечить безопасную манипуляцию объектами и избавиться от side-эффектов. C++ предлагает механизм неизменяемых объектов (равно как и переменных POD типов). Если Вы объявляете переменную или параметр функции с модификатором const, то в дальнейшем Вы не можете ни переприсвоить этой переменной значение, ни вызвать из такого объекта метод, который может изменить этот объект. Кроме того, модификатор const может быть применён к типу возвращаемого значения функции. Таким образом, можно реализовать безопасный возврат ссылки или указателя с ограничением только на чтение. Методы, которые не меняют состояние объекта помечаются модификатором const в конце сигнатуры. Внутри константных методов все члены-данные класса неявным образом становятся константами, за исключением тех, что помечены специальным модификатором mutable. Модификатор mutable говорит, что данная переменная не оказывает влияние на состояние объекта и может быть изменена в константных методах. Примером такой переменной может быть счётчик обращений к какому-либо методу. Следующий код вполне валиден  для C#.

class Person {
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public override string ToString() {
        return String.Format("{0}, {1}", LastName, FirstName);
    }
}

class Account {
    readonly Person person;

    public Account(Person person) {
        this.person = person;
    }

    public Person Person {
        get { return person; }
    }
}

class Program {
    static void Main(string[] args) {
        Account account = new Account(new Person {
            FirstName = "John",
            LastName = "Connor"
        });
        account.Person.FirstName = "Isaac";
        account.Person.LastName = "Asimov";
        Console.Out.WriteLine(account.Person);
        Console.ReadKey();
    }
}

Этот пример демонстрирует изменение данных, которые, очевидно, не должны меняться. Следующий пример демонстрирует, как C++ легко обходит указанную проблему.

#include <string>

class Person
{
public:
    Person(const std::string & first_name, const std::string & last_name) :
        m_first_name(first_name),
        m_last_name(last_name)
    {
    }

    void setFirstName(const std::string & first_name)
    {
        m_first_name = first_name;
    }

    const std::string & firstName() const
    {
        return m_first_name;
    }

    void setLastName(const std::string & last_name)
    {
        m_last_name = last_name;
    }

    const std::string & lastName() const
    {
        return m_last_name;
    }

private:
    std::string m_first_name;
    std::string m_last_name;
};

class Account
{
public:
    Account(const Person & person) :
        m_person(person)
    {
    }

    const Person & person() const
    {
        return m_person;
    }

private:
    Person m_person;
};

int main()
{
    Account account(Person("John", "Connor"));
    account.person().setFirstName("Isaac"); // Ошибка
    account.person().setLastName("Asimov"); // Ошибка
    std::cin.get();
    return 0;
}

Строки, помеченные комментарием "Ошибка", к примеру в Visual Studio, приведут к такому сообщению компилятора:

...consoleapplication1.cpp(58): error C2662: 'Person::setFirstName' : cannot convert 'this' pointer from 'const Person' to 'Person &'

На моей практике встречались случаи, когда getter`ы свойств меняли кучу переменных класса, создавая side-эффекты, которые приводили к длительной отладке. Если бы в C# поддерживались константные методы (и getter`ы свойств), то таких ситуаций практически не возникало.

Ссылки

В C++ существует два способа указывания на объект: классические указатели и ссылки. Указатели являются адресом переменной в памяти, а ссылки -- это механизм, введённый в язык C++ с целью повышения удобства и безопасности. Между ссылками и указателями есть две принципиальных разницы:

  1. ссылке нельзя переприсвоить значение;
  2. ссылка не может быть нулевой.

Первое ограничение касается, скорее, только безопасности, а вот второе очень сильно помогает в работе. В C++ принято принимать в функцию указатель, если параметр может быть равным NULL, в противном случае (чаще всего) принимают  ссылку. В C# ссылки могут быть равными null, что приводит к постоянной проверке всех аргументов и частому возникновению NullReferenceException`ов. Да, в C# параметр можно пометить ключевым словом ref, тогда нельзя будет передать null явно, но следующий код будет вполне валидным.

class Program {
    static void PrintString(ref string str) {
        Console.WriteLine(str);
    }

    static void Main(string[] args) {
        string str = null;
        PrintString(ref str);
        Console.ReadKey();
    }
}

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

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

Паттерн проектирования prototype (прототип) предполагает наличие одного объекта (прототипа) и его копирования для создания новых объектов. Такой подход часто обусловлен дороговизной инициализации новых объектов. В языке C++ предусмотрены мощные средства для создания копий объектов -- это конструкторы копирования и операторы присваивания. Конструкторы копирования создают новый объект по переданному образцу, а операторы присваивания заполняют существующий объект из образца. C++ никак не ограничивает программиста в реализации упомянутых методов. Если конструктор копирования или оператор присваивания не определены, то компилятор сгенерирует их автоматически. Копирование по умолчанию подразумевает полное побайтовое копирование объектов.

#include <iostream>
#include <cstring>

class Data
{
public:
    Data(void * data, size_t size);
    Data(const Data & data);
    ~Data();
    Data & operator = (const Data & data);
    inline const void * data() const;

private:
    void * mp_data;
    size_t m_size;
};

Data::Data(void * data, size_t size) :
    m_size(size)
{
    mp_data = ::malloc(size);
    ::memcpy(mp_data, data, size);
    std::cout << "Constructor" << std::endl;
}

Data::Data(const Data & data) :
    m_size(data.m_size)
{
    mp_data = ::malloc(m_size);
    ::memcpy(mp_data, data.mp_data, m_size);
    std::cout << "Copy constructor" << std::endl;
}

Data::~Data()
{
    ::free(mp_data);
    std::cout << "Destructor" << std::endl;
}

Data & Data::operator = (const Data & data)
{
    if(this ==  &data) return *this;
    ::free(mp_data);
    m_size = data.m_size;
    mp_data = ::malloc(m_size);
    ::memcpy(mp_data, data.mp_data, m_size);
    std::cout << "Assignment operator" << std::endl;
    return *this;
}

const void * Data::data() const
{
    return mp_data;
}

void test()
{
    char * content = "Hello, World!";

    Data data1(content, strlen(content) + 1); // Конструктор
    Data data2(data1); // Конструктор копирования
    Data data3(NULL, 0); // Конструктор
    data3 = data2; // Оператор присваивания

    std::cout << data1.data() << ": " << static_cast<const char *>(data1.data()) << std::endl;
    std::cout << data2.data() << ": " << static_cast<const char *>(data2.data()) << std::endl;
    std::cout << data3.data() << ": " << static_cast<const char *>(data3.data()) << std::endl;
}

int main()
{
    test();
    std::cin.get();
    return 0;
}
Constructor
Copy constructor
Constructor
Assignment operator
00489448: Hello, World!
0048A5D0: Hello, World!
0048A620: Hello, World!
Destructor
Destructor
Destructor

Из вывода программы видно, что данные действительно скопированы, а конструкторы и оператор присваивания вызваны так, как и ожидалось. Чаще всего, конструкторы копирования и операторы присваивания реализуются для копирования данных, которые хранятся в объекте по указателю, но существуют и другие полезные применения, среди которых:

  • Запрет копирования объектов класса. Реализуется путём объявления конструктора копирования и оператора присваивания в приватной секции. При этом реализацию можно опустить, тогда при попытке использования вы получите ошибку от линкера.
  • Модификация копий объектов или статических переменных. Таким образом, к примеру, реализуются "умные" указатели с подсчётом ссылок.

C# при применении оператора копирования копирует лишь ссылку на объект (за исключением значимых типов данных).  Для копирования объектов предназначен интерфейс ICloneable. Этот интерфейс содержит лишь один метод Clone, который возвращает объект типа object. Из этого следует, что клонированный объект всегда нужно приводить к нужному типу (и надеяться на порядочность разработчиков класса).

class Data : ICloneable {
    private byte[] data;

    public Data(byte[] data) {
        this.data = new byte[data.Length];
        data.CopyTo(this.data, 0);
    }

    public byte[] Buffer {
        get { return data; }
    }

    public object Clone() {
        return new Data(data);
    }
}

unsafe class Program {
    public static void Main(string[] args) {
        byte[] content = Encoding.ASCII.GetBytes("Hello, World");
        Data data1 = new Data(content);
        Data data2 = data1.Clone() as Data;
        PrintData(data1);
        PrintData(data2);
        Console.ReadKey();
    }

    static void PrintData(Data data) {
        fixed(byte* ptr = data.Buffer) {
            Console.WriteLine("{0:x}: {1}", (int)ptr, Encoding.ASCII.GetString(data.Buffer));
        }
    }
}
232f454: Hello, World
232f478: Hello, World

Тем не менее, из примера видно, что подход C# справился с поставленной задачей. Но что будет, если необходимо унаследовать один из классов, к исходникам которого у Вас нет доступа и который не реализует ICloneable?

class Data {
    private byte[] data;

    public void Load(string url) {
        using(WebClient client = new WebClient()) {
            data = client.DownloadData(url);
        }
    }

    public byte[] Buffer {
        get { return data; }
    }
}

class DataChild : Data, ICloneable {
    public object Clone() {
        return new DataChild(); // Не полная копия.
    }
}

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

Заключение

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

Нюхаем сеть через Linux

В этой статье речь пойдёт не столько о написании сниффера для Linux, сколько о структуре пакетов стека протоколов TCP/IP. Идея статьи взята из статьи на сайте xakep.ru.
Мы будем рассматривать пакеты, которые попадают к нам на сетевую карту. Эти пакеты относятся к протоколу Ethernet. Примерная структура этого пакета приведена в таблице ниже.
Ethernet заголовок
Ethernet данные (фрейм сетевого протокола)
IP заголовок
IP данные (фрейм транспортного протокола)
TCP или UDP заголовок
Данные приклодного протокола (HTTP, FTP, SMB итд).

Содержание статьи

Sniffer

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


#include <set>
#include <string.h>
#include <stdexcept>
#include "Analyzer.h"


namespace LinuxSniffer {

class SnifferError :
public std::runtime_error
{
public:
SnifferError(const std::string & message) throw() :
std::runtime_error(message)
{
}

virtual ~SnifferError() throw()
{
}
}; // class SnifferError


class Sniffer
{
public:
explicit Sniffer(const std::string & device_name, bool sniff_all = false)
throw(SnifferError);
virtual ~Sniffer();
bool addAnalyzer(Analyzer * analyzer);
bool removeAnalyzer(Analyzer * analyzer);
void start() throw(SnifferError);
void stop();

private:
void deinit();
void makeSocket() throw(SnifferError);
void bindSocketToDevice() throw(SnifferError);
void setPromiscuousMode() throw(SnifferError);
void unsetPromiscuousMode();
void runAnalyzers(const unsigned char * frame, size_t frame_size);

private:
Sniffer(const Sniffer &);
Sniffer & operator = (const Sniffer &);

private:
const std::string m_device;
const bool m_sniff_all;
std::set<Analyzer *> m_analyzers;
int m_socket;
bool m_is_promiscuouse_mode_set;
bool m_is_stopping;
unsigned char * mp_frame_buffer;
static const size_t m_frame_buffer_size = 65536;
}; // class Sniffer


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_SHIFFER_H__
Когда сниффер получает пакет, он запускает всех зарегистрированных анализаторов. Для их регистрации и служат методы addAnalyzer и removeAnalyzer.
Каждый анализатор должен реализовывать интерфейс Analyzer
class Analyzer
{
public:
virtual ~Analyzer() { }
virtual void analyze(const uint8_t * frame, size_t frame_size) = 0;
}; // class Analyzer
Именно этим механизмом мы и будем пользоваться при анализе структуры пакетов стека TCP/IP.
Приведу полную реализацию класса Sniffer
#include <errno.h>
#include <netpacket/packet.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <linux/if.h>
#include <linux/if_ether.h>
#include <linux/sockios.h>
#include "Sniffer.h"


using namespace LinuxSniffer;


Sniffer::Sniffer(const std::string & device_name, bool sniff_all)
throw(SnifferError) :
m_device(device_name),
m_sniff_all(sniff_all),
m_is_promiscuouse_mode_set(false),
m_is_stopping(false),
mp_frame_buffer(0)
{
makeSocket();
mp_frame_buffer = new unsigned char[m_frame_buffer_size];
}

Sniffer::~Sniffer()
{
deinit();
}

void Sniffer::deinit()
{
::close(m_socket);
unsetPromiscuousMode();
delete [] mp_frame_buffer;
}

bool Sniffer::addAnalyzer(Analyzer * analyzer)
{
return m_analyzers.insert(analyzer).second;
}

bool Sniffer::removeAnalyzer(Analyzer * analyzer)
{
return m_analyzers.erase(analyzer) > 0;
}

void Sniffer::makeSocket()
throw(SnifferError)
{
m_socket = ::socket(AF_PACKET, SOCK_RAW, ::htons(ETH_P_ALL));
if(-1 == m_socket)
throw SnifferError(::strerror(errno));
try
{
bindSocketToDevice();
if(m_sniff_all)
setPromiscuousMode();
}
catch(...)
{
deinit();
throw;
}
}

void Sniffer::bindSocketToDevice()
throw(SnifferError)
{
const size_t device_name_len = m_device.length() + 1;
char * device = new char[device_name_len];
::strcpy(device, m_device.c_str());
device[m_device.length()] = '';
int setopt_result = ::setsockopt(m_socket, SOL_SOCKET,
SO_BINDTODEVICE, device, device_name_len);
delete [] device;
if(-1 == setopt_result)
throw SnifferError(::strerror(errno));
}

void Sniffer::setPromiscuousMode() throw(SnifferError)
{
ifreq iface;
::strcpy(iface.ifr_name, m_device.c_str());
if(::ioctl(m_socket, SIOCGIFFLAGS, &iface) < 0)
throw SnifferError(::strerror(errno));
iface.ifr_flags |= IFF_PROMISC;
if(::ioctl(m_socket, SIOCSIFFLAGS, &iface) < 0)
throw SnifferError(::strerror(errno));
m_is_promiscuouse_mode_set = true;
}

void Sniffer::unsetPromiscuousMode()
{
if(!m_is_promiscuouse_mode_set)
return;
ifreq iface;
::strcpy(iface.ifr_name, m_device.c_str());
if(::ioctl(m_socket, SIOCGIFFLAGS, &iface) >= 0)
{
iface.ifr_flags &= ~IFF_PROMISC;
if(::ioctl(m_socket, SIOCSIFFLAGS, &iface) >= 0)
m_is_promiscuouse_mode_set = false;
}
}

void Sniffer::start() throw(SnifferError)
{
while(!m_is_stopping)
{
ssize_t length = ::recvfrom(m_socket, mp_frame_buffer,
m_frame_buffer_size, 0, 0, 0);
if(-1 == length)
throw SnifferError(::strerror(errno));
runAnalyzers(mp_frame_buffer, length);
}
}

void Sniffer::runAnalyzers(const unsigned char * frame, size_t frame_size)
{
for(std::set<Analyzer *>::iterator it = m_analyzers.begin();
m_analyzers.end() != it; ++it)
{
Analyzer * analyzer = *it;
if(0 != analyzer)
analyzer->analyze(frame, frame_size);
}
}

void Sniffer::stop()
{
m_is_stopping = true;
}
Конструктор принимает два параметра: имя устройства для прослушивания, например eth0 и флаг, включающий неразборчивое прослушивание. Для тех сетей, которые работают через хабы, этот режим будет прослушивать все пакеты, даже те, которые не адресованы Вашей машине.
Конструктор вызывает метод makeSocket, который создаёт сокет для прослушивания устройства. Делается это вызовом
m_socket = ::socket(AF_PACKET, SOCK_RAW, ::htons(ETH_P_ALL));
Я отошёл от того вызова, что был показан в статье на xakep.ru по причине того, что в man 2 socket написано
SOCK_PACKET
Устарело и не должно использоваться в новых программах; см. packet(7).
А страница man 7 packet говорит, что нужно использовать SOCK_RAW.
Далее метод bindSocketToDevice привязывает созданный сокет к устройству вызовом
::setsockopt(m_socket, SOL_SOCKET, SO_BINDTODEVICE, device, device_name_len); 
Последним шагом в подготовке сокета является необязательная установка опции неразборчивого прослушивания методом setPromiscuousMode. Для её установки мы должны получить структуру ifreq из сокета вызовом
::ioctl(m_socket, SIOCGIFFLAGS, &iface)
добавить флаг IFF_PROMISC в поле ifr_flags
iface.ifr_flags |= IFF_PROMISC;
и записать сруктуру обратно
::ioctl(m_socket, SIOCSIFFLAGS, &iface)
Всё готово к запуску сниффера. Метод start вызывает в цикле функцию recvfrom и передаёт полученный буфер анализаторам.
ssize_t length = ::recvfrom(m_socket, mp_frame_buffer, m_frame_buffer_size, 0, 0, 0);

Стек протоколов TCP/IP

Прежде чем перейти к описанию анализатора, давайте рассмотрим фреймы протоколов стека TCP/IP.

Ethernet

Пакеты этого протокола являются низшими из тех, что мы можем получить. Существует несколько версий пакетов ethernet, мы рассмотрим самую популярную - вторую версию. В таблице ниже показана структура фрейма Ethernet 2
Смещение Размер Описание
0 байт 6 байт MAC адрес назначения
6 байт 6 байт MAC адрес источника
12 байт 2 байта Тип Etherner
14 байт 46 - 1500 байт Данные
Последние 4 байта 4 байта CRC контрольная сумма
Для разбора фреймов всех протоколов я написал базовый класс
#ifndef __LINUX_SNIFFER_PROTOCOL_FRAME_H__
#define __LINUX_SNIFFER_PROTOCOL_FRAME_H__

#include <string>
#include <sys/types.h>
#include <stdint.h>

namespace LinuxSniffer {


class ProtocolFrame
{
public:
ProtocolFrame(const std::string & protocol_name) :
m_protocol_name(protocol_name),
m_data_offset(0)
{
}

virtual ~ProtocolFrame()
{
}

const std::string & getName() const
{
return m_protocol_name;
}

virtual bool init(const uint8_t * buffer, size_t buffer_size) = 0;

size_t getDataOffset() const
{
return m_data_offset;
}

protected:
void setDataOffset(size_t offset)
{
m_data_offset = offset;
}

private:
const std::string m_protocol_name;
size_t m_data_offset;
}; // class ProtocolFrame


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_PROTOCOL_FRAME_H__
Этот класс хранит и возвращает имя протокола описываемого фрейма и смещение данных от начала фрейма. Также класс передоставляет абстрактный метод инициализации из буфера фрейма и его размера.
Класс EthernetFrameV2 наследуется от ProtocolFrame и добавляет специфическую информацию.
#ifndef __LINUX_SNIFFER_ETHERNET_FRAME_V2_H__
#define __LINUX_SNIFFER_ETHERNET_FRAME_V2_H__

#include <cstring>
#include "ProtocolFrame.h"
#include "MacAddress.h"

namespace LinuxSniffer {

class EthernetFrameV2 :
public ProtocolFrame
{
public:
EthernetFrameV2() :
ProtocolFrame("Ethernet Version 2")
{
}

virtual ~EthernetFrameV2()
{
}

virtual bool init(const uint8_t * buffer, size_t buffer_size);

const MacAddress & getSourceMacAddress() const
{
return m_src_mac_addr;
}

const MacAddress & getDestinationMacAddress() const
{
return m_dest_mac_addr;
}

static const uint8_t (& getEthernetType())[2]
{
static bool is_init = false;
static uint8_t type[2];
if(!is_init)
{
::memcpy(type, "x08x00", 2);
is_init = true;
}
return type;
}

private:
MacAddress m_src_mac_addr;
MacAddress m_dest_mac_addr;
}; // class EthernetFrameV2

} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_ETHERNET_FRAME_V2_H__
Дополнительные поля являются MAC адресами отправителя и получателя. Эти поля имеют тип MacAddress, который описан простым классом
#ifndef __LINUX_SNIFFER_MAC_ADDRESS_H__
#define __LINUX_SNIFFER_MAC_ADDRESS_H__

#include <stdint.h>
#include <string>
#include <cstdio>

namespace LinuxSniffer {


class MacAddress
{
public:
MacAddress() :
b0(0),
b1(0),
b2(0),
b3(0),
b4(0),
b5(0)
{
}

explicit MacAddress(const uint8_t address[6]) :
b0(address[0]),
b1(address[1]),
b2(address[2]),
b3(address[3]),
b4(address[4]),
b5(address[5])
{
}

std::string toString() const
{
char str[16];
::sprintf(str, "%02x:%02x:%02x:%02x:%02x:%02x", b0, b1, b2, b3, b4, b5);
return str;
}

public:
uint8_t b0;
uint8_t b1;
uint8_t b2;
uint8_t b3;
uint8_t b4;
uint8_t b5;
}; // class MacAddress


} // namespace LinuxSniffer



#endif // __LINUX_SNIFFER_MAC_ADDRESS_H__
Реализацтя метода init класса EthernetFrameV2 сводится к проверке версии протокола Ethernet и получению MAC адресов отправителя и получателя. Для простоты, получение контрольной суммы опустим. Для того, чтобы получить все необходимые поля, достаточно привести буфер фрейма к типу ether_header, объявленному в netinet/ether.h.
#include <cstring>
#include <netinet/ether.h>
#include "EthernetFrameV2.h"

using namespace LinuxSniffer;


bool EthernetFrameV2::init(const uint8_t * buffer, size_t buffer_size)
{
if(buffer_size < sizeof(ether_header))
return false;
const ether_header * hdr = reinterpret_cast<const ether_header *>(buffer);
if(::memcmp(&hdr->ether_type, getEthernetType(), sizeof(getEthernetType())))
return false;
setDataOffset(sizeof(ether_header));
m_src_mac_addr = MacAddress(hdr->ether_shost);
m_dest_mac_addr = MacAddress(hdr->ether_shost);
return true;
}

Internet Protocol

В секции данных фрейма протокола ethernet хранится фрейм протокола IP. Этот протокол добавляет к пакету информацию об IP адресах отправителя и получателя и множество служебных данных. В следующей таблице показана структура фрейма IP версии 4.
Байты Смещение в битах Размер в битах Описание
0 0 4 Версия
4 4 Размер заголовка
1 8 6 Точка кода дифференцированных услуг (Differentiated services code point)
14 2 Явное уведомление о перегруженности (Explicit congestion notification)
2-3 16 16 Размер пакета
4-5 32 16 Идентификатор
6-7 48 3 Флаги
51 13 Смещение фрагмента
8 64 8 Время жизни
9 72 8 Протокол
10 80 16 Контрольная сумма заголовка
11-15 96 32 IP адрес источника
16-20 128 32 IP адрес назначения
20-24 160 32 Опции (если размер заголовка > 5)
20+ или 24+ 160 или 192 - Данные
В ранних версиях спецификации протокола "Точка кода дифференцированных услуг" и "Явное уведомление о перегруженности" были объединены в одно поле "Тип сервиса". Для того, чтобы использовать новое деление участники обмена должны договориться об этом. Я не буду разделять эти поля и буду использовать тип сервиса.
В поле "Флаги" биты от старшего к младшему означают:
0: Зарезервирован, должен быть равен 0;
1: Не фрагментировать;
2: У пакета еще есть фрагменты.
"Идентификатор" используется для предоставления информации о фрагментации пакета. Поле "Протокол" сообщает идентификатор протокола, фрейм которого находится в данных.
Думаю, остальные поля не нуждаются в комментариях.
Класс, описывающий IP фрейм приведён ниже
#ifndef __LINUX_SNIFFER_IP_FRAME_V4_H__
#define __LINUX_SNIFFER_IP_FRAME_V4_H__

#include <string>
#include "ProtocolFrame.h"

namespace LinuxSniffer {


class IpFrameV4 :
public ProtocolFrame
{
public:
IpFrameV4() :
ProtocolFrame("Internet Protocol Version 4"),
m_header_length(0),
m_tos(0),
m_package_size(0),
m_id(0),
m_flags(0),
m_frag_offset(0),
m_time_to_life(0),
m_protocol(0),
m_checksum(0)
{
}

virtual ~IpFrameV4()
{
}

virtual bool init(const uint8_t * buffer, size_t buffer_size);

const std::string & getSourceAddress() const
{
return m_src_ip_addr;
}

const std::string & getDestinationAddress() const
{
return m_dest_ip_addr;
}

uint8_t getHeaderLength() const
{
return m_header_length;
}

uint8_t getTypeOfService() const
{
return m_tos;
}

uint16_t getPackageSize() const
{
return m_package_size;
}

uint16_t getId() const
{
return m_id;
}

uint8_t getFlags() const
{
return m_flags;
}

uint16_t getFragmintationOffset() const
{
return m_frag_offset;
}

uint8_t getTimeToLife() const
{
return m_time_to_life;
}

uint8_t getProtocolId() const
{
return m_protocol;
}

uint16_t getHeaderCheckSum() const
{
return m_checksum;
}

public:
static const uint8_t m_ipv4_version = 4;

private:
void splitFragmentOffsetAndFlags();

private:
std::string m_src_ip_addr;
std::string m_dest_ip_addr;
uint8_t m_header_length;
uint8_t m_tos;
uint16_t m_package_size;
uint16_t m_id;
uint8_t m_flags;
uint16_t m_frag_offset;
uint8_t m_time_to_life;
uint8_t m_protocol;
uint16_t m_checksum;
}; // class IpFrameV4


} // namespace LinuxSniffer



#endif // __LINUX_SNIFFER_IP_FRAME_V4_H__
Для того, чтобы получить все поля из буфера фрейма достаточно привести указатель на него к указателю на структуру iphdr, объявленной в файле netinet/ip.h. В реализации метода init класса IpFrameV4 именно так и сделано
#include <netinet/ip.h>
#include <arpa/inet.h>
#include "IpFrameV4.h"



using namespace LinuxSniffer;

bool IpFrameV4::init(const uint8_t * buffer, size_t buffer_size)
{
if(buffer_size < sizeof(iphdr) || 0 == buffer)
return false;
const iphdr * ip_header = reinterpret_cast<const iphdr *>(buffer);
if(m_ipv4_version != ip_header->version)
return false;
in_addr addr;
addr.s_addr = ip_header->saddr;
m_src_ip_addr = ::inet_ntoa(addr);
addr.s_addr = ip_header->daddr;
m_dest_ip_addr = ::inet_ntoa(addr);
m_header_length = ip_header->ihl;
m_tos = ip_header->tos;
m_package_size = ip_header->tot_len;
m_id = ip_header->id;
m_frag_offset = ip_header->frag_off;
m_time_to_life = ip_header->ttl;
m_protocol = ip_header->protocol;
m_checksum = ip_header->check;
setDataOffset(m_header_length);
splitFragmentOffsetAndFlags();
return true;
}

void IpFrameV4::splitFragmentOffsetAndFlags()
{
union
{
struct
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
uint16_t flags : 3;
uint16_t frag_offset : 13;
#elif __BYTE_ORDER == __BIG_ENDIAN
uint16_t frag_offset : 13;
uint16_t flags : 3;
#endif
} spl;
uint16_t num;
} splitter;

splitter.num = m_frag_offset;
m_flags = splitter.spl.flags;
m_frag_offset = splitter.spl.frag_offset;
}
Смещение фрагмента и флаги в структуре iphdr объединены. Для их разделения я написал метод splitFragmentOffsetAndFlags.

Transmission Control Protocol

Протоколы траспортного уровня добавляют сведения о портах источника и назначения. Для описания общих свойств всех фреймов транспортных протоколов я ввёл класс TransportProtocolFrame, унаследованный от ProtocolFrame.
#ifndef __LINUX_SNIFFER_TRANSPORT_PROTOCOL_FRAME_H__
#define __LINUX_SNIFFER_TRANSPORT_PROTOCOL_FRAME_H__

#include "ProtocolFrame.h"


namespace LinuxSniffer {


class TransportProtocolFrame :
public ProtocolFrame
{
public:
TransportProtocolFrame(const std::string & protocol_name) :
ProtocolFrame(protocol_name),
m_src_port(0),
m_dest_port(0)
{
}

virtual ~TransportProtocolFrame()
{
}

uint16_t getSourcePort() const
{
return m_src_port;
}

uint16_t getDestinationPort() const
{
return m_dest_port;
}

protected:
void setSourcePort(uint16_t port)
{
m_src_port = port;
}

void setDestinationPort(uint16_t port)
{
m_dest_port = port;
}

private:
uint16_t m_src_port;
uint16_t m_dest_port;
}; // class TransportProtocolFrame


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_TRANSPORT_PROTOCOL_FRAME_H__
Структура фрейма протокола TCP, являющегося одним из протоколов транспортного уровня, показана в следующей таблице.
Байты Смещение в битах Размер в битах Описание
0-1 0 16 Порт источника
2-3 16 16 Порт назначения
4-8 32 32 Номер последовательности
8-12 64 32 Номер подтверждения последовательности
13-14 96 4 Смещение данных
100 6 Зарезервировано
106 6 Флаги
15-16 112 16 Размер окна
17-18 128 16 Контрольная сумма
19-20 144 16 Указатель важности
21+ 160 - Необязательные опции
За опциями или 21+ За опциями или 160 - Данные
Протокол TCP контролирует последовательность пакетов. Именно для этого нужны поля "Номер последовательности" и "Номер подтверждения последовательности".
"Флаги":
URG - Поле "Указатель важности" задействовано;
ACK - Поле "Номер подтверждения" задействовано;
PSH - Инструктирует получателя протолкнуть данные, накопившиеся в приемном буфере, в приложение пользователя;
RST - Оборвать соединения;
SYN - Синхронизация номеров последовательности;
FIN - флаг указывает на завершение соединения.
"Размер окна" - это число байтов, которые получатель готов принять.
"Указатель важности" указывает на номер октета, которым заканчиваются важные данные. Это поле игнорируется, если флаг URG не установлен.
Для чтения фрейма протокола TCP я расширил класс TransportProtocolFrame классом TcpFrame.
#ifndef __LINUX_SNIFFER_TCP_FRAME_H__
#define __LINUX_SNIFFER_TCP_FRAME_H__

#include "TransportProtocolFrame.h"

namespace LinuxSniffer {


class TcpFrame :
public TransportProtocolFrame
{
public:
TcpFrame() :
TransportProtocolFrame("Transmission Control Protocol"),
m_sequence_number(0),
m_ascknowledgment_number(0),
m_flag_fin(false),
m_flag_syn(false),
m_flag_rst(false),
m_flag_psh(false),
m_flag_ack(false),
m_flag_urg(false),
m_window_size(0),
m_checksum(0),
m_urgent_ptr(0)
{
}

virtual ~TcpFrame()
{
}

virtual bool init(const uint8_t * buffer, size_t buffer_size);

uint32_t getSequenceNumber() const
{
return m_sequence_number;
}

uint32_t getAscknowledgmentNumber() const
{
return m_ascknowledgment_number;
}

bool isFinFlagSet() const
{
return m_flag_fin;
}

bool isSynFlagSet() const
{
return m_flag_syn;
}

bool isRstFlagSet() const
{
return m_flag_rst;
}

bool isPshFlagSet() const
{
return m_flag_psh;
}

bool isAckFlagSet() const
{
return m_flag_ack;
}

bool isUrgFlagSet() const
{
return m_flag_urg;
}

uint16_t getWindowSize() const
{
return m_window_size;
}

uint16_t getCheckSum() const
{
return m_checksum;
}

uint16_t getUrgentPtr() const
{
return m_urgent_ptr;
}

public:
static const uint8_t m_protocol_id = 6;

private:
uint32_t m_sequence_number;
uint32_t m_ascknowledgment_number;
bool m_flag_fin;
bool m_flag_syn;
bool m_flag_rst;
bool m_flag_psh;
bool m_flag_ack;
bool m_flag_urg;
uint16_t m_window_size;
uint16_t m_checksum;
uint16_t m_urgent_ptr;
}; // class TcpFrame


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_TCP_FRAME_H__
Как и в случае с IP, для фрейма TCP в заголовочных файлах linux припасена структура tcphdr. Находится она в файле netinet/tcp.h. Реализуем метод init с использованием этой структуры.
#include <netinet/tcp.h>
#include "TcpFrame.h"

using namespace LinuxSniffer;


bool TcpFrame::init(const uint8_t * buffer, size_t buffer_size)
{
if(buffer_size < sizeof(tcphdr))
return false;
const tcphdr * tcp_header = reinterpret_cast<const tcphdr *>(buffer);
setSourcePort(tcp_header->source);
setDestinationPort(tcp_header->dest);
setDataOffset(tcp_header->doff);
m_sequence_number = tcp_header->seq;
m_ascknowledgment_number = tcp_header->ack_seq;
m_flag_fin = tcp_header->fin != 0;
m_flag_syn = tcp_header->syn != 0;
m_flag_rst = tcp_header->rst != 0;
m_flag_psh = tcp_header->psh != 0;
m_flag_ack = tcp_header->ack != 0;
m_flag_urg = tcp_header->urg != 0;
m_window_size = tcp_header->window;
m_checksum = tcp_header->check;
m_urgent_ptr = tcp_header->urg_ptr;
return true;
}

User Datagram Protocol

Последний протокол, который мы рассмотрим в этой статье - UDP. Это простой протокол транспортного уровня, который ни как не контролирует доставку пакетов и не устанавливает соединения. По этим причинам, фрейм этого протокола весьма прост.
Байты Смещение в битах Размер в битах Описание
0-1 0 16 Порт источника
2-3 16 16 Порт назначения
4-5 32 16 Длина дейтаграммы
6-7 48 16 Контрольная сумма
8+ 64 - Данные
В этой таблице всё понятно без коментариев.
Второй класс, унаследованный от TransportProtocolFrame - UdpFrame.
#ifndef __LINUX_SNIFFER_UDP_FRAME_H__
#define __LINUX_SNIFFER_UDP_FRAME_H__

#include "TransportProtocolFrame.h"

namespace LinuxSniffer {


class UdpFrame :
public TransportProtocolFrame
{
public:
UdpFrame() :
TransportProtocolFrame("User Datagram Protocol")
{
}

virtual ~UdpFrame()
{
}

virtual bool init(const uint8_t * buffer, size_t buffer_size);

uint16_t getDatagramLength() const
{
return m_datagram_length;
}

uint16_t getCheckSum() const
{
return m_checksum;
}

public:
static const uint8_t m_protocol_id = 17;

private:
uint16_t m_datagram_length;
uint16_t m_checksum;
}; // class UdpFrame


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_UDP_FRAME_H__
Аналогично фреймам протоколов IP и TCP, реализуем метод init с использованием структуры udphdr, объявленной в файле netinet/udp.h.
#include <netinet/udp.h>
#include "UdpFrame.h"

using namespace LinuxSniffer;


bool UdpFrame::init(const uint8_t * buffer, size_t buffer_size)
{
if(buffer_size < sizeof(udphdr))
return false;
const udphdr * udp_header = reinterpret_cast<const udphdr *>(buffer);
setSourcePort(udp_header->source);
setDestinationPort(udp_header->dest);
setDataOffset(sizeof(udphdr));
m_datagram_length = udp_header->len;
m_checksum = udp_header->check;
return true;
}

Анализатор

Теперь, когда мы знаем структуру основных протоколов, ничего не стоит проанализировать данные, которые мы получаем от сниффера. Для этого реализуем интерфейс Analyzer.
#ifndef __LINUX_SNIFFER_IP_ANALYZER_H__
#define __LINUX_SNIFFER_IP_ANALYZER_H__

#include <utility>
#include "Analyzer.h"


namespace LinuxSniffer {

class TransportProtocolFrame;

class IpAnalyzer :
public Analyzer
{
public:
virtual ~IpAnalyzer() { }
virtual void analyze(const uint8_t * frame, size_t frame_size);

private:
size_t tryEthV2Analyze(const uint8_t * frame, size_t frame_size);
std::pair<size_t, uint8_t> tryIpV4Analyze(const uint8_t * frame, ize_t frame_size);
size_t tryTcpAnalyze(const uint8_t * frame, size_t frame_size);
size_t tryUdpAnalyze(const uint8_t * frame, size_t frame_size);
void printTransportProtocolFrame(const TransportProtocolFrame & frame) const;
}; // class IpAnalyzer


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_IP_ANALYZER_H__
#include <iostream>
#include "IpAnalyzer.h"
#include "EthernetFrameV2.h"
#include "IpFrameV4.h"
#include "TcpFrame.h"
#include "UdpFrame.h"

using namespace LinuxSniffer;

void IpAnalyzer::analyze(const uint8_t * frame, size_t frame_size)
{
const size_t ip_offset = tryEthV2Analyze(frame, frame_size);
if(0 == ip_offset)
return;

const uint8_t * ip_frame = &frame[ip_offset];
const size_t ip_frame_size = frame_size - ip_offset;
std::pair<size_t, uint8_t> ip_analyze_result =
tryIpV4Analyze(ip_frame, ip_frame_size);
if(0 == ip_analyze_result.first)
return;


const uint8_t * transport_frame = ip_frame + ip_analyze_result.first;
const size_t transport_frame_size = ip_frame_size - ip_analyze_result.first;
size_t application_protocol_data_offset;
switch(ip_analyze_result.second)
{
case TcpFrame::m_protocol_id:
application_protocol_data_offset = tryTcpAnalyze(
transport_frame, transport_frame_size);
break;
case UdpFrame::m_protocol_id:
application_protocol_data_offset = tryUdpAnalyze(
transport_frame, transport_frame_size);
break;
default:
std::cout << "======= Unsupported transport protocol ======n";
}

std::cout << std::endl;
}

size_t IpAnalyzer::tryEthV2Analyze(const uint8_t * frame, size_t frame_size)
{
EthernetFrameV2 eth_frame;
if(!eth_frame.init(frame, frame_size))
return 0;

std::cout << "====== " << eth_frame.getName() << " ======n" <<
"Source MAC Address: " << eth_frame.getSourceMacAddress().toString() <<
std::endl << "Destination MAC Address: " <<
eth_frame.getDestinationMacAddress().toString() << std::endl;

return eth_frame.getDataOffset();
}

std::pair<size_t, uint8_t> IpAnalyzer::tryIpV4Analyze(const uint8_t * frame,
size_t frame_size)
{
IpFrameV4 ip_frame;
if(!ip_frame.init(frame, frame_size))
return std::make_pair(0, 0);

std::cout << "====== " << ip_frame.getName() << " ======n" <<
"Source IP Address: " << ip_frame.getSourceAddress() << std::endl <<
"Destination IP Address: " << ip_frame.getDestinationAddress() <<
std::endl <<
"Header Length: " << std::dec <<
static_cast<uint32_t>(ip_frame.getHeaderLength()) << std::endl <<
"Type Of Service: " <<
static_cast<uint32_t>(ip_frame.getTypeOfService())<< std::endl <<
"Package Size: "<< ip_frame.getPackageSize() << std::endl <<
"Identification: " << ip_frame.getId() << std::endl <<
"Flags: " << static_cast<uint32_t>(ip_frame.getFlags())<< std::endl <<
"Fragmentation Offset: " << ip_frame.getFragmintationOffset() <<
std::endl <<
"Time To Live: " << static_cast<uint32_t>(ip_frame.getTimeToLife()) <<
std::endl <<
"Transport Protocol ID: " <<
static_cast<uint32_t>(ip_frame.getProtocolId()) << std::endl <<
"CRC-16 Header CheckSum:" << std::hex << ip_frame.getHeaderCheckSum() <<
std::endl;
return std::make_pair(ip_frame.getDataOffset(), ip_frame.getProtocolId());
}

size_t IpAnalyzer::tryTcpAnalyze(const uint8_t * frame, size_t frame_size)
{
TcpFrame tcp_frame;
if(!tcp_frame.init(frame, frame_size))
return 0;

printTransportProtocolFrame(tcp_frame);

std::cout << std::dec <<
"Sequence Number: " << tcp_frame.getSequenceNumber() << std::endl <<
"Ascknowledgment Number: " << tcp_frame.getAscknowledgmentNumber() <<
std::endl <<
"Window Size: " << tcp_frame.getWindowSize() << std::endl <<
"Urgent Pointer: " << tcp_frame.getUrgentPtr() << std::endl <<
"Flags:n" <<
" FIN: " << std::boolalpha << tcp_frame.isFinFlagSet() << std::endl <<
" SYN: " << tcp_frame.isSynFlagSet() << std::endl <<
" RST: " << tcp_frame.isRstFlagSet() << std::endl <<
" PSH: " << tcp_frame.isPshFlagSet() << std::endl <<
" ACK: " << tcp_frame.isAckFlagSet() << std::endl <<
" URG: " << tcp_frame.isUrgFlagSet() << std::endl <<
"CheckSum: " << std::hex << tcp_frame.getCheckSum() << std::endl;

return tcp_frame.getDataOffset();
}

size_t IpAnalyzer::tryUdpAnalyze(const uint8_t * frame, size_t frame_size)
{
UdpFrame udp_frame;
if(!udp_frame.init(frame, frame_size))
return 0;

printTransportProtocolFrame(udp_frame);
std::cout << std::dec <<
"Datagram Length: " << std::dec << udp_frame.getDatagramLength() <<
std::endl <<
"Datagram CheckSum: " << std::hex << udp_frame.getCheckSum() <<
std::endl;
return udp_frame.getDataOffset();
}

void IpAnalyzer::printTransportProtocolFrame(
const TransportProtocolFrame & frame) const
{
std::cout << "====== " << frame.getName() << " ======n" <<
std::dec << "Source Port: " << frame.getSourcePort() << std::endl <<
"Destination Port: " << frame.getDestinationPort() <<
std::endl;
}
Всё очень просто. Пытаемся преобразовать полученный фрейм в фрейм протокола Ethernet версии 2. Затем, используя полученное смещение данных, получаем фрейм IP. Проанализировав номер протокола, пытаемся получить данные либо о TCP, либо о UDP фрейме. Попутно выводим всю полученную информацию на консоль. В конце анализа у нас остаётся переменная, хранящая смещение данных прикладного протокола, которую Вы можете применить для дальнейшего разворачивания стека.

Заключение

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

Спецификация исключений: друг или враг?

Исторически сложилось, что разные языки программирования, поддерживающие работу с исключениями, по-разному относятся к спецификации исключений. В Java спецификации обязательны и контролируются статически, в C# и Python их вообще нет, а в C++ этот вопрос является одним из самых "сырых" мест.
В двух словах о том, что такое спецификация исключений, для тех, кто не сталкивался с этим понятием. Спецификация исключений - это явное описание тех типов исключений, которые могут быть сгенерированы некой функцией. В java, языке, который поддерживает наилучшим образом спецификацию исключений, это выглядит так
void funct() throw MyException
{
throw new MyException();
}
MyException - это единственно возможный тип исключений, который может покинуть метод. При спецификации можно указать несколько типов исключений.

C++

С самого начала, идея спецификации исключений была чужда языку C++. Язык унаследовал миллиарды строк кода на языке C и, к тому времени, уже были написаны миллионы строк кода на C++, в которых не было ничего о спецификации исключений. Тем не менее, в C++ была добавлена возможность спецификаций. В своей книге "Дизайн и эволюция C++" Бьёрн Страуструп пишет о том, что спецификации, изначально могли контролироваться только во время исполнения, но, позже, были добавлены некоторые возможности для статического контроля, но, как мы увидим, этого не достаточно. В том же параграфе, Бьёрн приводит пример, который я сейчас Вам продемонстрирую. Итак, предположим, что у нас есть библиотека, написанная на языке C++, вот заголовок этой библиотеки
#ifndef SHAREDCPP_H
#define SHAREDCPP_H

#ifdef __cplusplus
extern "C" {
#endif

void foo();

#ifdef __cplusplus
} // extern "C"
#endif

#endif // SHAREDCPP_H
И её реализация
#include <iostream>
#include <string>

extern "C" {

void foo()
{
std::cout << "Throw exceptionn";
throw std::string("test exception");
}

}
Я назвал эту библиотеку libsharedcpp. Я использую ОС Linux, поэтому я не писал конструкции типа __declspec(dllexport). Если Вы используете ОС MS Windows, то для компиляции примеров, Вам нужно будет добавить эти конструкции (думаю, не нужно Вас учить это делать). Для компиляции, я буду использовать компиляторы из набора GCC. Собираем эту библиотеку командой
g++ -shared -fPIC sharedcpp.cpp -o libsharedcpp.so
Теперь создадим библиотеку libsharedc на языке C со следующим кодом
#ifndef SHAREDC_H
#define SHAREDC_H

#ifdef __cplusplus
extern "C" {
#endif

void bar();

#ifdef __cplusplus
} // extern "C"
#endif

#endif // SHAREDC_H
#include "sharedc.h"
#include "sharedcpp.h"

void bar()
{
foo();
}
Как видно, библиотека libsharedc использует функцию из библиотеки libsharedcpp, поэтому, при компиляции, надо указать этот факт
gcc -shared -fPIC sharedc.c -lsharedcpp -L. -o libsharedc.so
Теперь создадим исполняемый модуль на языке C++.
#include <string>
#include <iostream>
#include "sharedc.h"

int main()
{
try
{
bar();
}
catch(const std::string & str)
{
std::cout << str << std::endl;
}
return 0;
}
Если Вы используете UNIX-like ОС, то, перед компиляцией программы, Вам следует либо поместить собранные библиотеки в место, где Ваша ОС их найдёт, например в /usr/lib, либо добавить в путь для поиска текущий каталог командой
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:`pwd`
Далее собираем программу командой
g++ program.cpp -lsharedc -L. -o program
Запустим программу
$ ./program 
Throw exception
test exception
Итак, что же я хотел продемонстрировать этой программой. Прежде всего, обратите внимание, на то, что исключение прошло через два скомпилированных модуля. Мало того, один из скомпилированных модулей написан на языке C, который знать не знает ни о каких исключениях. Из этого уже можно сделать вывод о том, что указать спецификацию исключений для функции bar невозможно.
Теперь, давайте дополним программу таким образом
#include <string>
#include <exception>
#include <iostream>
#include "sharedc.h"

void test() throw(std::exception)
{
bar();
}

int main()
{
try
{
test();
}
catch(const std::exception & err)
{
}
return 0;
}
Я добавил функцию test, которая специфицирует исключения только стандартного типа - std::exception. Статические анализаторы не смогут определить, что функция bar генерирует исключение типа std::string, и компиляция пройдёт успешно. Но при выполнении нас ждёт ошибка.
$ ./program 
Throw exception
terminate called after throwing an instance of 'std::string'
Аварийный останов
И даже добавление catch блока, обрабатывающего std::string, не устранит проблему , так как она возникает до момента обработки исключения, а именно - в момент выхода исключения из функции.
try
{
test();
}
catch(const std::exception & err)
{
}
catch(const std::string & err)
{
}
Таким образом, я продемонстрировал ситуацию, когда пользы от спецификации исключений меньше, чем вреда.

Java

По-другому дела обстоят с Java. В этом языке спецификация исключений контролируется статически, а компиляция в байт-код позволяет выявить все спецификации на этапе компиляции или раньше. В отличие от C++, спецификация исключений в Java носит обязательный характер, что тоже сопряжено с некоторыми трудностями.
Предположим, что у нас в программе, есть базовый класс
package test;

public class MyBase
{

private int value;

public MyBase(int value)
{
this.value = value;
}
}
И от него наследуются множество наследников. Например
public class MyFirst
extends MyBase
{

public MyFirst()
{
super(1);
}
}
И, некоторый наследник содержит в себе данные, исходный код которых, не доступен
package test;

import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.SparseMultigraph;

public class MySecons
extends MyFirst
{

private Graph<Integer, String> graph;

public MySecons()
{
graph = new SparseMultigraph<Integer, String>();
}
}
Внезапно Вам становится необходимо сделать Ваш базовый класс клонируемым и Вы добавляете переопределение метода clone и наследование от интерфейса Cloneable
package test;

public class MyBase
implements Cloneable
{

private int value;

public MyBase(int value)
{
this.value = value;
}

@Override
public Object clone()
{
try
{
MyBase base = (MyBase)super.clone();
base.value = value;
return base;
} catch(CloneNotSupportedException ex)
{
// Этого не должно произойти.
return null;
}
}
}
Я задушил исключение, так как оно не может возникнуть, если класс реализует интерфейс Cloneable. Теперь я должен реализовать метод clone в потомках базового класса. С классом MyFirst проблем нет; он достаточно примитивен, чтобы не писать эту реализацию вообще. Но, вот, класс MySecond принесёт нам не мало хлопот. Склонировать граф из библиотеки JUNG мы не можем, так как он не предоставляет нам такой возможности. После некоторых раздумий, мы можем прийти к выводу, что клонировать граф - действительно не лучшая затея, и мы решаем запретить клонировать объекты класса MySecond. Но мы уже разрешили клонировать базовый класс и всех его детей, поэтому единственным верным решением остаётся выкинуть исключение при попытке вызова метода clone. Но не тут-то было. В java запрещается специфицировать исключения у переопределённых методов таким образом, чтобы это расходилось со спецификацией, определённой в родительском классе (убирать исключения из спецификации можно). И теперь, как ни старайся, ничего с методом clone, дельного не выйдет, либо возвращаем задушенное исключение, либо придумываем другой способ копирования. Я, к слову, смог ввести некое подобие конструкторов копирования из C++. Другой вариант решения проблемы описан в статье "Фабрика клонов".

Итого

Я не могу говорить за большинство языков, которые существуют, но о некоторых сказать кое-что могу. В частности, в языках C# и Python от спецификации исключений отказались вовсе, и правильно сделали, на мой взгляд. Как видно из предыдущих частей статьи: спецификации исключений зачастую оказываются вредными, а в случае с C++, ещё и бесполезными.

Анализ и оптимизация кода на 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 показана окружность радиуса 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.

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

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