Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

articles:c-tricks-10h [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== c-tricks-10h ======
 +<​sub>​{{c-tricks-10h.odt|Original file}}</​sub>​
 +
 +====== сишные трюки\\ (10h выпуск) ======
 +
 +крис касперски ака мыщъх, a.k.a. souriz, a.k.a. nezumi, a.k.a. elraton, no-email
 +
 +**этот выпуск трюков в некотором смысле особенный,​ а особенный он, потому что юбилейный (в шестнадцатеричной нотации). мыщъх долго готовился к такому знаменательному событию,​ отбирая самые вкусные трюки, но… в конце концов трюков оказалось столько (и один вкуснее другого),​ что пришлось просто подкинуть монетку,​ выбрав четыре трюка наугад.**
 +
 +===== трюк 1 — обход префикса "​_"​ =====
 +
 +Си-соглашение о передаче параметров (обычно обозначаемое как cdecl от C Declaration),​ которому подчиняются все Си-функции,​ если только их тип не специфицирован явно, заставляет компилятор помешать префикс "​_"​ перед именем каждой функции,​ чтобы линкер мог определить,​ что он имеет дело именно с "​cdecl",​ а не, скажем,​ stdcall.
 +
 +Поэтому,​ категорически не рекомендуется использовать перед функциями знак подчеркивания,​ особенно при смешанном стиле программирования (то есть когда cdecl функции используются наряду с stdcall), в противном случае линкер может запутаться,​ вызывав совсем не ту функции или выдать ошибку,​ дескать нет такой функции и ничего линковать я не буду, хотя такая функция на самом деле есть. Обычно это случается при портиировании программы,​ написанной в одной среде разработке,​ под другие платформы.
 +
 +Хорошо,​ а как быть, если текст программы уже кишит функциями с префиксами знака подчеркивания,​ что в частности любит делать Microsoft, отмечая таким образом нестандартные функции,​ отсутствующие в ANSI C. Переделывать программу,​ заменяя знаки подчеркивания на что-нибудь другое — себе обойдется дороже. Хорошо,​ если она вообще потом соберется,​ а если даже и соберется,​ нет гарантий,​ что не появится кучи ошибок в самых разных местах.
 +
 +И вот тут на помощь нам приходит трюкачество. А именно — макросы. Допустим,​ мы имеем функцию _f() и хотим избабится от знака подчеркивания. Как это мы делаем?​ Да очень просто:​
 +
 +**#define _f() x_f()**
 +
 +**x_f();**
 +
 +Листинг 1 избавляемся от префиксов знака подчеркивания через макросы
 +
 +Фокус в том, что макросы "​разворачиваются"​ препроцессором в Си-код,​ в котором зловредных префиксов уже не оказывается и риск развалить программу — минимален (однако,​ не стоит забывать,​ что макросы вносят множество побочных эффектов и обращаться с ними следует _крайне_ осторожно).
 +
 +===== трюк 2 — динамические массивы =====
 +
 +Известно,​ что язык Си не поддерживает динамических массивов. Ну не поддерживает и все тут. Хоть тресни. Хоть убейся о Газель. Хоть грызи зубами лед. А динамические массивы все равно нужны. Функции семейства malloc не в счет, поскольку они выделяют именно блок памяти,​ а не массив,​ что совсем не одного и тоже.
 +
 +И вот на этот случай есть один хитрый древний трюк. Когда-то это широко известный но потом позабытый,​ что очень странно,​ поскольку это не простой трюк, а очень даже нужный и важный. Короче,​ рассмотрим следующую структуру:​
 +
 +**struct string**
 +
 +**{**
 +
 +**intlength;​****//​ ****длина строки**
 +
 +**chardata [1];****// ****память зарезервированная для строки**
 +
 +**};**
 +
 +Листинг 2 структура,​ реализующая динамический массив
 +
 +Элемент "​length"​ хранит длину строки,​ а "char data [1]" это не сама строка (как это можно подумать поначалу),​ а место _зарезервированное_ под нее. Осталось только научиться как с этой структурой обращаться.
 +
 +Рассмотрим следующий фрагмент кода, реализующий настоящий динамический массив:​
 +
 +**// ****некая строка с динамическим массивом внутри**
 +
 +**string* p2 = ...**
 +
 +**...**
 +
 +****
 +
 +******// ****выделение памяти,​ необходимой для строки размеров ****p2->​length**
 +
 +**// ****минус один заранее зарезервированный байт**
 +
 +**struct string s = malloc (sizeof (struct string) + p2->​length - 1);**
 +
 +****
 +
 +******// ****инициализация элемента структуры ****length**
 +
 +**s->​length = p2->​length;​**
 +
 +****
 +
 +**// ****копирование строки из ****p2 ****в ****s**
 +
 +**strncpy (s->​data,​ p2->​data,​ p2->​length);​**
 +
 +**...**
 +
 +**// ****освобождение ****s**
 +
 +**free (s);**
 +
 +**...**
 +
 +Листинг 3 практический пример использования динамических массивов
 +
 +Ну и в чем здесь прикол?​ А в том, что язык Си с его вольностями в трактовке типов позволяет нам выделить блок памяти произвольной длины и "​натянуть"​ на него структуру string. При этом первые ячейки займет элемент length типа int, а остальное — данные строки,​ длина которой может и не совпадать с data[1]. Действуя таким образом,​ мы можем, например,​ имитировать PASCAL-строки (однако,​ следует сказать,​ что с С++ данный трюк не работает,​ точнее работает,​ но дает непредсказуемый результат и потому применять его крайне опасно,​ это может позволить себе только опытный программист).
 +
 +===== трюк 3 — экономия памяти =====
 +
 +Допустим,​ нам потребовалось выделить три локальные переменные типа char и еще один массив типа char[5]. Ну, потребовалось,​ ну что тут такого?​ Хорошо,​ тогда попробуйте ответить на вопрос:​ сколько байт мы при этом израсходовали?​ Голос из толпы: восемь! Всего восемь байт?! Это же за компилятор такой у вас, ась?! Берем MS VC (впрочем,​ с тем же успехом можно брать и любой другой) и компилируем следующий код:
 +
 +foo()
 +
 +{
 +
 +char a;
 +
 +char b;
 +
 +char c;
 +
 +char d[5];
 +
 +}
 +
 +Листинг 4 функция с тремя переменными типа char и одной char[5]
 +
 +Сморим на откомпилированный код, дизассемблированный IDA Pro (крепко держать за стул):
 +
 +.text:​00000000 _fooproc near
 +
 +.text:​00000000pushebp
 +
 +.text:​00000001movebp,​ esp
 +
 +**.text:​00000003********sub********esp,​ 14h**
 +
 +.text:​00000006movesp,​ ebp
 +
 +.text:​00000008popebp
 +
 +.text:​00000009 retn
 +
 +.text:​00000009 _fooendp
 +
 +Листинг 5 откомпилированный результат листинга 4
 +
 +Откуда тут взялось 14h (20) байт локальной памяти?​! Все очень просто. Компилятор в угоду производительности самопроизвольно выравнивает все переменные по границе двойного слова. Итого мы получаем 3*max(1,4) + max(5,8) = 12 + 8 = 20. Вот они наши "​оптимизированные"​ 20 байт вместо ожидаемых 5ти.
 +
 +А что делать,​ если нам не нужна такая оптимизация?​! Все просто — гоним переменные в структуру,​ предварительно отключив выравнивание соответствующей прагмой компилятора (в частности,​ у MS VC за это отвечает ключевое слово "#​pragma pack( [ n] )", где n – желаемая кратность выравнивания в данном случае равная единице,​ то есть выравнивание производится по границе одного байта или, говоря иными словами,​ не производится вовсе).
 +
 +Переписанный код будет выглядеть приблизительно так:
 +
 +#pragma pack( 1 )
 +
 +struct bar
 +
 +{
 +
 +char a;
 +
 +char b;
 +
 +char c;
 +
 +char d[5];
 +
 +};
 +
 +foo()
 +
 +{
 +
 +struct bar baz;
 +
 +}
 +
 +Листинг 6 оптимизированный вариант с отключенным выравниванием
 +
 +Смотрим на откомпилированный код, дизассемблированный все той же IDA Pro.
 +
 +.text:​00000000 _fooprocnear
 +
 +.text:​00000000pushebp
 +
 +.text:​00000001movebp,​ esp
 +
 +**.text:​00000003subesp,​ 8**
 +
 +.text:​00000006movesp,​ ebp
 +
 +.text:​00000008popebp
 +
 +.text:​00000009retn
 +
 +.text:​00000009 _fooendp
 +
 +Листинг 7 дизассемблированный код с отключенным выравниванием
 +
 +Вот оно! Вот они наши 8 ожидаемых байт вместо непредвиденных 20'​ти! Правда,​ скорость доступа к переменным за счет отключения выравнивания слегка упала, но… с не выровненными данными процессоры научились эффективно бороться еще со времен Pentium-II, а вот если данные не влезут в кэш первого уровня,​ тогда падения производительности действительно не избежать.
 +
 +===== трюк 4 —загадка чистых виртуальных методов =====
 +
 +В предыдущих выпусках этой рубрики мы не касались вопросов приплюснутого си, но на случай юбилея сделаем исключение. Как известно,​ в любом учебнике по Си++ черным по белому написано,​ что невозможно создать экземпляр (instantiate) класса,​ имеющего чистый виртуальный метод (pure virtual method), при условии,​ что он никогда не вызывается. В этом, собственно говоря,​ и заключается суть концепции абстрактных классов.
 +
 +На самом деле, не всему написанному можно верить и приплюснутый си открывает достаточно большие возможности для трюкачества. Поставленную задачу можно решить например,​ так:
 +
 +class base
 +
 +{
 +
 +public:
 +
 +base();
 +
 +virtual void f() = 0;
 +
 +};
 +
 +class derived : public base
 +
 +{
 +
 +public:
 +
 +virtual void f() {}
 +
 +};
 +
 +void G(base& b){}
 +
 +base::​base() {G(*this);}
 +
 +
 +
 +main()
 +
 +{
 +
 +derived d;
 +
 +}
 +
 +Листинг 8 трюковый код, создающий экземпляр объекта с чистой виртуальной функций,​ которая никогда не вызывается
 +
 +После компиляции (в данном случае использовался компилятор Microsoft Visual C++) мы увидим (см. листинг 7),​ что когда создается экземпляр d, то конструктор base::base будет вызывать функцию G, передавая ей в качестве указателя this указатель на base, но не на derived, что, собственно говоря,​ и требовалось доказать.
 +
 +.text:​00000005 public: __thiscall Base::​Base(void) proc near
 +
 +.text:​00000005;​ CODE XREF: Derived::​Derived(void)+A↓p
 +
 +.text:​00000005
 +
 +.text:​00000005 var_4= dword ptr -4
 +
 +.text:​00000005
 +
 +.text:​00000005pushebp
 +
 +.text:​00000006movebp,​ esp
 +
 +.text:​00000008pushecx
 +
 +**.text:​00000009mov[ebp+var_4],​ ecx********;​ this (base::​base)**
 +
 +.text:​0000000Cmoveax,​ [ebp+var_4]
 +
 +.text:​0000000Fmovdword ptr [eax], offset const Base::​`vftable'​
 +
 +**.text:​00000015movecx,​ [ebp+var_4]**
 +
 +**.text:​00000018pushecx**
 +
 +**.text:​00000019callG(Base &)**
 +
 +.text:​0000001Eaddesp,​ 4
 +
 +.text:​00000021moveax,​ [ebp+var_4]
 +
 +.text:​00000024movesp,​ ebp
 +
 +.text:​00000026popebp
 +
 +.text:​00000027retn
 +
 +.text:​00000027 public: __thiscall Base::​Base(void) endp
 +
 +Листинг 9 результат компиляции "​трюкового"​ кода компилятором MS Visual C++ 6.0
 +
 +