четверг, 1 октября 2009 г.

Функция без явного определения

Убойный для меня пример.
typedef int (*pf)(int, int);
char c[] = {85,-119,-27,-117,69,12,3,69,8,93,-61,-112};
pf sum = (pf)c; //reinterpret_cast отказывается, поэтому так.
cout << sum(2,3); //Вывод 5.


* This source code was highlighted with Source Code Highlighter.
Теперь sum - функция сложения, являющаяся аналогом этой:
int sum(int a, int b){return a+b;}

Сначала я написал нормальную функцию sum, затем байт за байтом просмотрел ее содержимое в памяти. К сожалению sizeof к функциям неприменим, поэтому я, после sum, определил еще одну функцию, и предположил, что в памяти они тоже рядом. Их адреса различались на 12 байт.

UPD: Одной строкой:
cout << ((int (*)(int, int))"\x55\x89\xE5\x8B\x45\x0C\x03\x45\x08\x5D\xC3\x90")(2,3);

Функции

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

Повторюсь, что я веду этот блог не как учебное пособие, а пытаюсь объяснить самому себе сложные моменты (написание топика заставляет достаточно подробно вникнуть в суть), а так же необычные вещи удивившие меня. Еще я люблю очень маленькие(особенно в одну строчку :), но показательные примеры.

Вопрос которым я задался: можно ли вызвать функцию, имея только ее адрес хранящийся в long. Разумеется просто ради интереса, без практической пользы. Без reinterpret_cast обойтись явно не смогу. Еще понадобится тип функции.


char f(){return 'f';}

typedef char (*PF)(); //именно такой тип у f()

long a = reinterpret_cast<long>(f); //получаем число...
cout << (reinterpret_cast<PF>(a))(); //... говорим, что это число - указатель на функцию. Скобки () приводят вызову функции.
 

суббота, 26 сентября 2009 г.

Константы

У Эккеля не зря целая глава выделена под константы. В этой теме много интересного. Сейчас я хочу создать константу и подкопаться к ней через указатели.

const int a = 1;
int b;
cout << &a << &b; //У меня, при запусках, b всегда имеет адрес меньше(на 4 байта), чем a.
int* p = (&b+1); //Это адрес константы a
cout << *p //1
*p = 2; //Меняем
cout << a << endl << *p; //1 2


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

понедельник, 21 сентября 2009 г.

reinterpret_cast

Мне нравится, когда в книге, сначала приводится пример программы с комментарием типа: «Ну что, видали как круто? Подбирайте челюсть со стола и читайте дальше, как это работает». Обожаю если примеры маленькие, например:
long a = 2829155;
cout << reinterpret_cast< char* >(&a);
Выведется строка «c++». Не отдам руку на отсечение, что у всех она будет именно такой, это требует дополнительного исследования.
В первой строке записали четырехбайтовое число. Затем взяли адрес переменной и нагло наврали компилятору, что этот адрес совсем не указатель на long. Это указатель на char. Как компилятор интерпретирует указатель на char? Как строку.
Почему именно «с++»? 2829155 == 0x002B2B63. То есть число 2829155 хранится в виде последовательности байт: 0x63(==99), 0x2B(==43), 0x2B(==43), 0x0(==0). Если посмотреть таблицу символов, то убедимся, что 99 это символ «c», а 43 – «+». Байт с нулем интерпретируется как конец строки.
Совсем нетрудно написать программу обратного преобразования, из строки в число

Я не смогу толково объяснить все тонкости reinterpret_cast, для этого есть учебники. Однако, точно могу сказать, что он запросто превращает указатель одного типа в указатель другого. А это влечет совершенно другую интерпретацию байтов памяти, в чем можно убедиться выше. Об опасностях нетрудно догадаться. Если в примере взять число побольше, то 4-й байт уже не будет равен нулю. Значит, приведенная к char* строка будет продолжаться до первого нуля, но уже неизвестно где он будет.
С классами тоже интересно.
class A {
  public:
  int i;
};
void main(){
  int b = 1;
  cout << (*reinterpret_cast< A* >(&b)).i; /*1*/
}
Тут говорится, что по адресу &b вовсе не число, а переменная класса A. Нужно ли говорить насколько это все опасно.

пятница, 18 сентября 2009 г.

Массивы

В C++ очень многие понятия связаны с указателями. Не знаю нравится мне это или очень нравится, но я все чаще говорю "ух ты, прикольно!". Если написать в Паскале

var z : array[1..3] of Integer;

то переменная z будет массивом и все тут. Словно обертка над более низкоуровневыми типами. Хотя, может быть, это не совсем верно. Массив в C++:

int z[3];

В памяти отводится место(непрерывное) для трех int, а переменная z - указатель на первый элемент. Квадратные скобки просто делают удобнее работу с указателями. А раз так, то для получения значения, можно писать не только z[i], а еще и *(z+i).

z[i] == *(z+i)
&z[i] == z+i


Иными словами: Берем адрес первого элемента массива (адрес это z). Следующий элемент находится на sizeof(int) байт (у меня 4) дальше. Но количество байт нам знать не нужно, так как компилятор в курсе, что z - указатель на int, поэтому z+1 указывает на sizeof(int) дальше чем z, т.е на следующий элемент массива (на z[1]). Аналогично и z+i.

Еще пример
int* p = z+1;
int* p = &z[1];
//Обе записи эквивалентны.
cout << p[2]; //p - не массив, просто квадратные скобки смещают указатель и сразу разыменовывают.

В этом примере p[1]==z[2].

четверг, 17 сентября 2009 г.

Каждая строка занимает память. Операция определения адреса применима не только к переменным.

cout << (long)&"Hello" << (long)&"World"; //4464640 4464646 (опять эти числа, почему всегда они)

Любопытно, если записать одинаковые слова:

cout << (long)&"Hello" << (long)&"Hello"; //4464640 4464640

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

понедельник, 14 сентября 2009 г.

char* и строковые литералы

Изучаю дальше.

char c = 'x';
char* pc = &c;

Все понятно, pc – указатель на переменную char. И тут внезапно! Оказывается можно так:

char* pc = ”Hello”;
cout << pc //Hello;

pc же вроде должна хранить адрес! Страуструп сказал, что так можно типа потому, что в языке C строковые литералы (это оказывается так по-научному называется) хранились именно так, через char*. А C++ должен поддерживать C. И Страуструпу тут все понятно и незачем останавливаться на этом подробно. Жаль я не он, и мне ничего не понятно. Мне не нравится, что char* ведет двойную жизнь. Сейчас будем разбираться.

Попробую посмотреть адрес, и разыменовать pc.

cout << pc << (long)pc << *pc; //Hello 4464640 H

Ага, pc выдает Hello, а когда я преобразовал ее к long, то она все-таки вернула адрес.
Отлично, *pc выдает символ H. Т.е. pc по-прежнему работает как указатель на char.
Интересно, где остальные символы слова.

Итак, я взял pc (а он все-таки хранит адрес, но при удобной возможности прикидывается строкой), прибавил к хранящемуся (слово было выговорено с только нескольких попыток) там адресу 1. В ячейке с адресом pc лежит ‘H’, очевидно предположить, что в следующей ячейке лежит 'e'. Чтобы узнать адрес следующей ячейки, нужно просто прибавить 1 (так как ячейка char равна байту). А чтобы извлечь ее значение, нужно сделать разыменование:

cout << *pc << *(pc+1) << *(pc+2) << *(pc+3) << *(pc+4); //Hello

Сейчас нарисую, память после:
char* pc = ”Hello”;
char* pc2 = ”World”;


Так получилось, что слова идут друг за другом. В следующий раз такого может не быть. Для этого случая верно выражение pc + 6 == pc2. Еще можно вытащить слово “World” не используя pc2

cout << *(pc+6) << *(pc+7) << *(pc+8) << *(pc+9) << *(pc+10); //World

Разумеется, в другой раз это может не прокатить.
Определить конец слова помогает ‘\0’. Если бы я был составителем задачника, то добавил задачу печати слова, используя *(pc+i) в цикле, пока не встретится ‘\0’.

А еще минздрав предупреждает: строковые литералы изменять нельзя. *(pc) = 'z'; вывалится с ошибкой во время выполнения.

Вообще говоря это устаревший способ, лучше использовать char c[] = “Hello”. Потом об этом напишу.

воскресенье, 13 сентября 2009 г.

Указатели

В двух словах. 

int i = 7;
int* pi = &i; //Указатель на int. Проинициализирован адресом i.
int** ppi = &pi //Указатель на int*, т.е. указатель на указатель на int. Проинициализирован адресом pi.

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

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

Переменная i может хранить любое значение типа int. pi - адрес любой переменной int. ppi - адрес любой переменной типа int*(например адрес pi).

Все это легко проверяется. В моем примере:

cout << i << (long)&i; //7 2293572
cout << pi << (long)&pi; //2293572 2293568
cout << ppi << (long)&ppi; //22968 2293564
cout << *pi << **ppi << (long)*ppi; //7 7 2293572

(long) - преобразование перед выводом. Без него адреса выводятся в шеснадцатеричном виде. * - операция разыменования. То есть *pi возвращает значение переменной, находящуюся по адресу pi. *ppi == pi == &i

Был еще сложный для меня момент: указатели на int и на int* (и вообще все указатели) так похожи, раз хранят только адрес. Почему же нельзя сделать просто указатель на переменную любого типа. Вообще говоря можно. Но в таком случае можно легко ошибиться. Например случайно указать на переменную int, и думать про себя, что это объект. А благодаря типизации, компилятор будет следить за этим, а среда разработки будет помогать автозавершением кода для более сложных типов.

Уф, пока писал и проверял, сам все понял :)

Перечислимые типы

С этими типами посложнее, чем в Delphi. Самый простой пример:

enum colors{RED, GREEN, BLUE};

RED, GREEN, BLUE - константы, равные числам 0, 1, 2 соответственно. Их значения можно задавать самостоятельно:

enum colors{RED=0, GREEN=1, BLUE=9}; //Должны быть целыми

Использование достаточно очевидно:

colors x = GREEN; //z == 1 
colors z = 9; //Ошибка компиляции.
colors z = colors(9); //А вот так все хорошо. z == BLUE
if (z = BLUE) {/* */};

Любопытно (по крайней мере для меня), что переменной z можно присвоить другое  значение кроме этих трех:

colors z = 2; //Как мы уже выяснили - это ошибка компиляции
colors z = colors(2); // Так можно. z == 2

Интересно, в каких пределах можно задавать значения? У меня не сошлось со Страуструпом.

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

enum e2{a=3, b=9} //диапазон 0:15

Поэтому, по его словам, e2 f2 = e2(20) не определено.

Границы диапазонов связаны со степенями двойки, но я не буду переписывать точное правило. Размер sizeof не может превышать sizeof(int).

Не знаю почему, но как я не крутил, sizeof всегда был равен 4. По всей вероятности sizeof(enum) = sizeof(int).

Вывод, который я сделал: Перечислимый тип - это фактически тот же int, но с личными именами некоторых чисел

суббота, 12 сентября 2009 г.

Консольный сапер

Чтобы практиковаться мне был нужен компилятор и среда разработки. Microsoft Visual Studio не устраивал платностью. Есть бесплатный Visual C++ Express Edition, но без MFC. Остановился на распространенном варианте Code::Blocks + MinGW.

Для тренировки сделал сапера. Пишем столбец и строку, а в ответ получаем количество мин вокруг.

четверг, 10 сентября 2009 г.

Страуструп. Размеры целочисленных типов.

Я был в курсе, что книга для продолжающих. Во 2-й главе я занервничал. В 3-й начал паниковать. Эти главы с высоты птичьего полета охватывали процентов 70 из того, что я узнал из предыдущей книги. Что же будет дальше. Но все закончилось хэппи эндом, это был просто обзор возможностей языка :) 

Наконец-то я понял в каких пределах могут отличаться фундаментальные типы в зависимосимости от платформы. Авторы любят предупреждать: "Внимание, размер типов может отличаться, обязательно это помните". А откуда я знаю какой размер может принять, скажем, тип int на другой платформе. 2 байта? 4?. А может, например 2 бита? Могу ли я быть уверен, что число 9000 влезет в int?

Но тут конкретно сказано: независимо от погоды типу char гарантируется 8 бит, для short - 16, long - 32. Хм, где же int. Cудя по sizeof(short) <= sizeof(int) <= sizeof(long) ему дадут 16 бит точно. У меня на 32-битной Висте и компилятором minGW для них выдало: 2, 4, 4. Ну да ладно, просто учту на будущее.

среда, 9 сентября 2009 г.

C++

Так получилось, что я плохо знаю C++. Скажем так, чуть лучше уровня "Hello, World!". Если я хочу(а я хочу) писать прикладные программы, то просто обязан постигнуть его.

Это осознание пришло около 2-х недель назад. Я начал с поиска подходящей книги. В рунете первым делом вспоминают книгу Брюса Эккеля "Философия Java" (Thinking in Java). Вот и отлично, тем более она была куплена мною около полугода назад и аккуратно положена на полку. Я часто смотрел на ее красивую оранжевую обложку. Обложка мне нравилась. Вероятно потому, что оранжевый цвет считается самым антидепрессивным :) Иногда я прочитывал введение, через месяц забывал, и снова прочитывал. Начало введения было очень вдохновляющим, но к концу оно меня утомляло.

Но однажды я сел и стал читать. И за пару недель почти полностью ее прочитал. Мне понравилось, но новичку в программировании не рекомендую. Автор предполагает(и подчеркивает), что читатель знаком с языком C, и разъясняет только новое, появившееся в C++.

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

Стиль интересный, хочу перечитать. Но только после Страуструпа "Язык программирования С++", в котором я прочитал кучу введений и предисловий, а также главу "от редакции русского издания". Ходят легенды, что каждый программист должен ее прочитать.

вторник, 8 сентября 2009 г.

Итак, надо вспомнить, что я вообще знаю.

Первым в списке QBasic - на нем написаны моя первая, а также вагон других программ. Интересно, что самым трудным для меня был цикл for, сложнее чем do-loop с массивами. Но я был тогда в 5-м классе. Не могу не вспомнить одну из самых крупных, созданную в 8 классе - тетрис с 29 различными фигурками (в каждой от одного до пяти квадратиков). Впрочем с возможностью выбора набора. 

В университете начал учить Pascal. Долго не мог врубиться в указатели, а особенно в реализацию списков с помощью них. Хотя у меня тогда не было интернета.

Там же изучил HTML, CSS, но средненько. Очень поверхностно ASP, 1С, PHP

Курсовики и диплом писал на Delphi и достаточно хорошо усвоил принципы ООП. 

Универ дал не очень много практики, зато теперь я относительно хорошо подкован в теории :)

На работе продолжил писать на Delphi, поэтому сам язык знаю достаточно хорошо. Но для меня остаются тайной дельфовые вкладки компонентов с названиями типа SOAP, Corba и т. п.

На среднем уровне владею SQL, PL/SQL

Не буду упоминать то, с чем мало работал.

Наверняка о чем-нибудь забыл написать.

Самая большая дыра в моем образовании это плохое знание C++. Про это в другом топике.

Начало не знаю чего

Читая ИТ- форумы, блоги, книги, статьи, слушая разговоры коллег, все чаще стал понимать, что ничего не понимаю. Завертелась мысль, что неплохо было бы изучить все хотя бы поверхностно как можно больше популярных технологий, языков программирования, и всего остального, что связано с ИТ. Может быть я не представляю всей сложности, но по крайней мере есть большое желание. К тому же очень не хотелось бы отставать в своем развитии. Поэтому, для начала, завел блог в котором буду писать о своих трудностях, лени и т.п. успехах :)