Модераторы: bsa

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Указатели, строки, классы и пр. Си++ новичкам посвящается... 
V
    Опции темы
ManiaK
  Дата 9.8.2005, 14:35 (ссылка) |    (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


Homo Sapience
***


Профиль
Группа: Комодератор
Сообщений: 1145
Регистрация: 3.8.2004
Где: ИУ5-93

Репутация: 2
Всего: 29



 ВВЕДЕНИЕ

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

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

    Ну, готовы?..

    ЧАСТЬ 1. СИ СТРОКИ ИЛИ НЕМНОГО ТЕОРИИ.

    Наверное треть всех вопросов, задаваемых новичками на различных форумах интернета, посвящённых Си или Си++, так или иначе касаются строк. Кстати, в названии параграфа я не ошибся - именно Си строки. Почему? потому что в Си++ есть классы и использование указателей бессмысленно (Си является практически идеальным подмножеством Си++ и потому почти всё, что справедливо для Си, справедливо и для Си++). В сущности, все проблемы со строками сводятся к проблемам указателей. Если человек не понимает, как работают указатели и что они из себя представляют, он никогда не поймёт, чем char отличается от char*. Если ещё более углубиться в проблему, то можно увидеть, что непонимание указателей связано зачастую с непонимаением работы самого компьютера. Действительно, а знаете ли вы как ваши программы выполняются компьютером? В Си, как и в Си++, этот вопрос очень важен, так как эти языки очень тесно взаимодействуют с аппаратным уровнем.

    Обычно ваша программа (бинарный объектный код, понятный процессору без трансляции) загружается в оперативную память и там исполняется. То есть, предположим, что ваша программа занимает 512 байт. Загрузчик программы определяет, куда лучше всего впихнуть её; предположим, он выбрал адрес 0x1000. Тогда последний байт вашей программы будет по адресу 0x1200 (0x1000 + 512). Некоторые преполагают, что в том же месте хранятся все переменные программы. Спешу вас разуверить, это не так. В сегменте кода (так правильно называется эта часть оперативной памяти) хранятся только машинные инструкции самой программы и (не обязательно) константные объекты. Проще говоря, только то, что не меняется в процессе выполнения программы. При попытке из вашей же программы поменять код ("поработать над собой") хорошая операционная система даст оплеуху и выкинет из списка задач, почистив за вами память. Где же хранятся переменные?..

Необходимо сначала разделить переменные на два рода - "динамические" и стековые. В чём разница? во-первых, в скорости выделения памяти под них: под стековые переменные память выделяется с гиганской скоростью и ещё быстрее высвобождается, в то время как при создании динамической переменной система ещё подумает: а где взять эту память?.. Данный вопрос очень важен, часто программисты "обманывают" компилятор, заключая динамический объект в статическую оболочку.

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

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

В теории всё просто. При непосредственном выполнении ваша программа выясняет размер файла и запрашивает у операционной системы необходимый объём памяти. Но как нам "дадут" эту память? Просто! Операционная система выделяет в оперативной памяти область, размер которой равен запрошенному, и даёт вам адрес первого байта этой области (далее просто - адрес области) - этакий жетончик, по которому вы можете получить свои данные. Помимо этого она заносит в свои списки вашу программу, адрес области и её размер. Теперь вы полноправный владелец данной области памяти! Смотрите, как выглядит последний абзац в Си-коде:

Код

    int *i = alloc(1000);// Хотим блок памяти в тысячу байт


    В Си++ есть ещё вариант:

Код

    int *i = new int[1000];// Здесь сложнее - блок памяти, размером в тысячу int'ов


    В обоих примерах переменная i - статическая, а область памяти, адрес которой эта переменная содержит, - динамическая. Сложно? только первое время, на самом деле всё логично. Можно даже обнаглеть и написать такое:

Код

    unsigned long ic = (unsigned long)alloc(1000);// Ха-ха, в Си такое прокатывает
    unsigned long icpp = reinterpret_cast<unsigned long>(new int[1000]);// А так - в Си++


    В Си++ значительно более уважительно относятся к типам объектов и на любые преобразования, подобные этим, грустно улыбаются; от себя хочу добавить, что подобными фокусами по возможности лучше не заниматься. Размер unsigned long гарантированно совпадает с размером указателя. С указателями пользователь может выполнять некоторые арифметические операции; рассмотрим на примере:

Код

    int *p = new int;// Если скобок нет - объект один
    int *p2 = p;// Присвоение адреса из одного указателя другому
    p2 += 10;// Сдвиг указателя на десять позиций
    p2 = 1001;// Ошибка! слева тип - указатель, справа - числовая константа!


    Почему предпоследняя строчка работает, а последняя нет, хотя и в той и другой справа - числа? Есть одна маленькая хитрость. В предпоследней операции при сдвиге литера 10 имеет не числовой тип, а специальный тип ptr_diff. Это тип разницы между двумя адресами; переменные этого типа можно прибавлять/вычитать из указателей, но их нельзя присваивать. Иногда этот тип обозначают как difference_typе.

    Да, для любителей поглумиться над компилятором скажу сразу: операции умножения/деления над указателями запрещены.

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

    1. Использовать функцию операционной системы для увеличения размера блока (обычно - realloc).
    2. Удалить старый блок ("вернуть" его системе) и запросить новый нужного размера.

    В Си это выглядит примерно так:

Код

    free(p);// Возвращаем память системе
    // p теперь указывает на нулевой адрес; гарантируется, что там ничего нет
    p = alloc(500);// Берём блок по-меньше


    В Си++ это может выглядеть ещё и так:

Код

    delete [] p2;// Квадратные скобки указывают на то, что удаляемый объект - массив
    // если их нет, считается, что объект одиночный
    p2 = new int[125];// Во так


    При выделении памяти оператором new под массив обычно помечается также и размер выделенного массива. Тогда при освобождении памяти оператором delete [] программа будет знать, сколько памяти вернуть системе. Вот такие ситуации не желательны:

Код

    int* pi = new int[12];
    delete pi;// Результат не определён, зависит от конкретного компилятора или ОС


    Лучше не делать этого. Чаще всего разработчики компиляторов перестраховываются и разницы между удалением массива и удалением одиночного объекта нет (и то - при использовании встроенных типов). Но по закону подлости, если сделать такую пакость, как в последнем примере, через несколько месяцев (лет?) вашу программу перекомпилируют на другом компиляторе или перегрузят операторы delete и.. лучше просто следовать известному в России принципу "заплати налоги - спи спокойно" под налогом подразумевая дополнительную проверку кода на такие вот тонкости.

    И последнее замечание по указателям: в Си динамически созданные объекты сами не удаляются. То есть, если вы создали какую-то переменную через alloc-функцию или new, она будет висеть в RAM до самого завершения работы программы. Такие маленькие недоработки называют утечками памяти и иногда они выливаются в полное исчерпание ресурсов системы и её зависание (в том случае если программа работает достаточно продолжительное время и регулярно "теряет" память).

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

    Первое и самое главное, что вам необходимо понять - это что в Си++, равно как и в Си, нет типа данных "строка". Поняли? ну я и не сомневался, разберём по-подробней...

    Си - самый логичный с точки зрения архитектуры ЭВМ язык программирования. Практически всё, что можно сделать на ассемблере, можно сделать и на Си. Не верите? Смотрите:

Код

    unsigned char* pch = (unsigned char*)0xBC00;
    memcpy(pch, _T("Hello, World!"), 26);


    Знаете, что сделают эти две строчки, если пустить их сразу после инициализации Биос? Выведут вам на экран строку "Hello, World!". Мы можем откомпилировать этот код, записать в первый сектор дискеты и загрузиться с неё. Здорово? Если попытаться проделать тоже под ОС, очень вероятно, что ваша программа просто "грохнется", т.к. любая уважающая себя ОС не даёт кому ни попадя лазить в системную область RAM.

    В Си++ всё выглядит почти также. Отличие только в том, что преобразовывать числовую константу в указатель приходится через reinterpret_cast. Ну не любят этого плюсники! Разумеется, всё это сказано только для общего развития, подобные вещи принято и правильно писать на Ассемблере, так как только в нём можно наиболее эффективно работать с аппаратурой напрямую. На данном примере вы должны были уже понять, что строка в Си/Си++ - это последовательность кодов из заданной таблицы символов. Не правда ли логично? Загляните в текстовый файл hex-редактором и вы увидите нечто вроде этого:

    
Цитата
cf ff f2 b8 f0 ea e0 20 e7 e0 20 eb fe e1 ee e7
    ed e0 f2 e5 eb fc ed ee f1 f2 fc 21 3a 2d 29 20
    d1 e8 2b 2b 20 f0 f3 eb e8 f2 21 20 cd e8 ea ee
    ec f3 20 ed e5 20 e3 ee e2 ee f0 e8 21 3a 29 29


    Проще говоря, вы увидите набор кодов символов. Точно также строки хранятся в Си/Си++ переменных. Придумайте, как можно текст представить иначе и очень может быть, что вам дадут нобелевскую премию. Шутка!..

    Любая строка в Си - это массив символов. Всё остальное вытекает из этого постулата.

Код

    char str[] = {'S', 't', 'r', 'i', 'n', 'g'};// Статический массив (статическая строка)
    str[3] = 'o';// Нормально. Теперь str содержит "Strong"
    // Внимание - ВАЖНО!
    char* pStr = "Constant String";// Указатель на константную (!!!) строку!
    pStr[3] = 'A';// Ошибка! результат зависит от реализации...
    char* pStr2 = new char[15];// Вот так нужно создавать динамические строки
    strcpy(pStr2, "Dinamic String");// После этого pStr2 будет указывать на
    // динамическую область памяти, содержащую строку "Dinamic String"


    Как известно, строки в Си заканчиваются нулевым символом (символом, имеющим код 0). Любая строка, взятая в двойные кавычки по определению заканчивается этим символом и потому размер "Dinamic String" не 14, а 15. Это нужно для того, чтобы программа, использующая эту строку, знала, где она кончается. Можно, конечно, для каждой строки таскать свою переменную, в которой содержится число символов, но это очень неудобно в тех случаях, когда приходится оперировать десятками различных строк. В Pascal'е другой принцип - первый символ строки содержит не код первого символа, а число символов в строке. Но тогда для стандартного байтного набора символов максимально возможной длинной строки будет 255 символов, а этого бывает мало.

    Рассмотрим ещё пару примеров работы со строками в Си стиле и закончим эту главу.

Код

    char* pStr = new char[255];// Берём с запасом

    strcpy(pStr, "Тестовая строка");

    strcat(pStr, " #1");// Добавление к строке, pStr = pStr + " #1"
    strncpy(pStr, "String", 3);// Копирование n символов, pStr = "Str"
    strncat(pStr, "ings", 3);// Добавление n символов, pStr = pStr + "ing"
    size_t i = strlen(pStr);// Получение длины строки, i = 6
    strcmp(pStr, "string");// Сравнение строк, возвратит не 0 - разные строки
    stricmp(pStr, "string");// Сравнение без учёта регистра, возвратит 0
    // Сравнение первых n символов, также возвратит 0
    strncmp(pStr, "Strings", 6);
    char* pc = strchr(pStr, 'i');// Поиск первого вхождения, pc = pStr+3 = "ing"
    strcpy(pStr, "development");

    // Аналогично функции:
    strrchr// Поиск последнего вхождения
    strstr// Поиск первого вхождения строки
    strpbrk// Поиск первого вхождения одного из символов
    strspn// Число символов до любого символа из строки
    strcspn// Число символов до любого символа не из строки


Ну вот, на этом с Си строками можно закончить. Переходим к Си++. 

ЧАСТЬ 2. ПРОЦЕДУРНОЕ ПРОГРАММИРОВАНИЕ. ФУНКЦИИ, СТРУКТУРЫ, МАССИВЫ.

Вспоминая себя во времена моего изучения Си++, я часто удивляюсь, насколько логично проходило это изучение. На первых порах я брал чужие программы и пытался их модифицировать; классическая схема: сам пока ничего не можешь, но что-то творить хочется. Вскоре мне это, разумеется, надоело, я залез в книги и начал пробовать писать самостоятельно. Но как я писал - это заслуживает отдельного внимания! Я думаю, многие так начинали свой путь программиста...

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

Для чего нужны программы? - вот самый простой по формулировке и самый сложный по своему смыслу вопрос программирования. Отвлечёмся от метафизических прений и опустимся на землю. Компьютеры необходимы для выполнения различных операций без участия человека. Когда-то, на заре информационных технологий, существовали машины, которые были созданны для определённых операций, причём функциональность таких машин определялась их строением. Например: строение лопаты определяет её назначение - копку, строение чайника - хранение вязких веществ и т.д. Конечно, с дуру можно в чайник засунуть голову, а лопатой попробовать писать вместо ручки.. если у вас возникают подобные мысли, то, как ни странно, программирование для вас идеально подходит! Почему - выяснится позже.

Итак, строение машин долгое время определяло их функции. Однако, за человеком давно замечена одна глупость: он всё пытается унифицировать. Иными словами, в один прекрасный день захотелось одному человеку объединить лопату и чайник в один предмет, причём так, чтобы можно было переключать их функциональность. Нажал кнопочку - чайник, нажал другую - лопата. Удобно!..

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

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

Теперь посмотрим более современно. В любом компьютере существует исполнительное устройство (и часто - не одно), которое может выполнять фиксированный набор каких-то простых операций. Разберём это на примере нашего вездесущего чайника. Предположим, у нашей машины "автоматического чайника" есть следующий набор элементарных операций: открыть крышку, залить воду, включить нагреватель, выключить нагреватель, заорать "идиот, опять включил - и заснул!!!". Нам нужно составить несколько программ: кипичение воды без криков при окончании работы и с ними. Очевидно, что обе программы будут содержать блоки с одинаковыми наборами операций (открыть крышку, залить воду, включиться, выключиться) и только в конце второй программы будет приписано включение ора на всю ивановскую. "И что?" - скажем мы, "Не обломаемся продублировать эти одинаковые блоки!". Подождите, я же не договорил: у нас памяти на перфокарте только на шесть элементарных операций. Что делать будете?

Вот тут в дело вступают процедуры: мы обзываем наши первые четыре операции как "кипячение чайника" и прописываем отдельно, а в двух наших программах пишем только вызов этого блока: "закипятить чайник", "поорать". Функции отличаются от процедур только тем, что возвращают результат своих действий; например, после выполнения функции "закипятить чайник", можно куда-то записать, сколько времени происходило кипячение. Также, и функции и процедуры могут принимать параметры, например: "закипятить чайник водой с начальной температурой 20 градусов" - здесь параметром будет начальная температура; от начальных параметров, мы видим, будет меняться и возвращаемое значение - чем выше начальная температура, тем меньше времени потребуется на кипячение. А если мы пишем просто "закипятить чайник", то фактически мы предоставляем выбор начальной тепературы самой функции кипячения, мы не передаём параметра. Если функция способна сама выбрать это начальное значение при необходимости, то такой параметр называют параметром по умолчанию. Кстати, само разграничение понятий "процедура" и "функция" довольно условно и никто вас не имеет права щёлкнуть по носу, если вы вдруг употребить одно понятие взамен другого (хотя принято по возможности пользоваться вторым).

Программирование, основанное на функциях и процедурах и называют процедурным. Однако довольно часто в это понятие включают ещё кое-что. О том, что такое переменная и указатель говорилось в предыдущей статье, так что не имеет смысла повторяться. Переменные предоставляют возможность хранить информацию, но предоставляют очень примитивно: нам доступны лишь целые числа, строки и дробные числа. А как мы сможем описать параметры чайника? Для этого нам потребуется несколько переменных, но передавать их все отдельно в функции неудобно. Да и создавать придётся для каждого экземпляра чайника сразу несколько переменных.. но опять эти люди!..

Придумали обзывать набор переменных одним именем и предоставлять возможность создавать сразу несколько переменных по заранее сформированному шаблону. Такой шаблон назвали структурой. Структура подобна штампу, который заметно облегчает жизнь бухгалтерам и юристам: им не приходится каждый раз писать, им необходимо просто припечатать штамп к листку и на последнем сразу появится нужная надпись, да ещё и в рамочке! Структуры - продвинутые штампы, они содержат в себе нечто, что можно изменять (некоторые штампы тоже содержат пустые поля, куда можно что-нибудь вписать). Это заранее заранее "непропечатанное" нечто и есть переменные, как вы должны были уже понять. Вы указываете только тип и набор переменных. Вот пример:
Код

struct Chaynik
{
    char model[255];// Модель
    int gab_x, gab_y, gab_z;// Габаритные размеры
    // Бла-бла-бла...
};

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

struct Chaynik ch1;// "Штампуем".. :-)

Переменная ch1 уже содержит в себе переменные model, gab_x и пр. Нам нужно только использовать специальный оператор, чтобы обратиться к ним:
Код

strcpy(ch1.model, "Bosch CH3000");
ch1.gab_x = 90, ch1.gab_y = 90, ch1.gab_z = 200;

Структуры также могут быть указываемы; и тогда используется другой оператор для доступа к содержимому структуры:
Код

struct Chaynik* pch1 = malloc(sizeof(struct Chaynik));
pch1->gab_x = 90, cph1->gab_y = 90, pch1->gab_z = 200;

struct Chaynik практически полноценный тип, такой же как int, double и char. Он не умеет только того, что не определено явно. К примеру, компилятор не знает, как ему прибавить к чайнику что-либо. Поэтому данная операция в процедурном программировании существовать не может:
Код

ch1 += 5;
ch1 += *pch1;// Тоже самое - каким образом это сделать?

Задавая себе вопрос "как?" можно довольно точно определить, будет ли компилятор воспринимать вашу запись или нет, хотя иногда бывают неожиданности. В любом случае знайте: если после компиляции вашей гениальной программы компилятор покатывается со смеху, указывая на строчку в вашей программе, где производится какая-либо операция над структурой - значит компилятор такую операцию не понимает (заметим, что над тем, что они что-то не понимают, смеются только люди довольно глупые, но это не значит, что разработчики вашего компилятора одни из них).

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

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

int func(int* a, char* b)
{
    // ...
}

int func1(int a[], struct Chaynik b)
{
    // ...
}

int func2(int a[13], double b)
{
    // ...
}

Первые две функции с точки зрения ассемблера будут совершенно идеинтичным, различными у них будут только имена. Почему? Потому, что всё, что больше размера указателя (sizeof(int*)) обычно передаётся посредством указателя: сначала создаётся копия b, а потом в функцию отправляется только указатель на эту копию. int a[] и int* в Си же вообще идеинтичны, потому копия a создаваться не будет: вместо этого просто произойдёт передача указателя на первый элемент a. Казалось бы: где логика? На самом деле логика есть: когда мы объединили несколько переменных структурой в одну, мы тем самым сказали компилятору, чтобы он распространил на эту группу переменных теже правила, что действуют на обычные переменные (насколько это возможно, конечно). Одиночные переменные передаются "по значению" (то есть, передаётся не сам объект, а его копия - значение). Разделение же на int[] и int* в Си и Си++ чисто лексическое, компилятор интерпретирует и то однозначно за исключением случая создания переменной: тут указание константы в квадратных скобках явно свидетельствует об том, что будет создан статический массив. Также, было введено ещё одно удобное ограничение; взгляните на func2 - параметр a здесь будет передаваться точно также, как и в func и func1, но компилятор должен будет проверить, что вы передаёте ему массив именно из 13 элементов (разумеется, проверить это на этапе компиляции можно лишь при передаче статического объекта). Обычно правда компилятор языка Си разрешает нарушение этого правила и максимум - погрозит вам пальчиком при передаче указателя или статического объекта иного размера. В Си++ всё может быть значительно строже.

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


 ! 
archimed7592
Любые просьбы решить задачку, помочь разобраться в проблеме и прочий оффтоп в этой теме будут неумолимо удаляться.
Для решения своих проблем создавайте отедльную тему.


Это сообщение отредактировал(а) archimed7592 - 29.12.2007, 13:38
PM MAIL WWW   Вверх
509harbore
Дата 21.9.2005, 20:24 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 2
Регистрация: 4.9.2005

Репутация: нет
Всего: нет



ManiaK Объясните мне разницу между указателеми и ссылками. smile
Насколько я понял из прочтения вашей статьи. Что указатель это переменная, которая содержит адрес другой переменой.
PM MAIL   Вверх
Гость_ManiaK
Дата 21.9.2005, 23:12 (ссылка)    |    (голосов: 0) Загрузка ... Загрузка ... Быстрая цитата Цитата


Unregistered











Цитата(509harbore @ 21.9.2005, 20:24)
Насколько я понял из прочтения вашей статьи. Что указатель это переменная, которая содержит адрес другой переменой.

Именно так.

Цитата(509harbore @ 21.9.2005, 20:24)
ManiaK Объясните мне разницу между указателеми и ссылками.

Ссылка - это просто синоним для переменной.

Код

int a = 13;
int& sa = a;
sa = 15;// Тоже самое, что и a = 15


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

Код

void func(int i)
{
    i = 15;
}

void main(void)
{
    int a = 13;
    func(a);// a в этом примере не изменяется!!!
}


В данном примере при передаче а в функцию func сначала создаётся копия a и она уже отдаётся на растерзание, в то время как сам исходный объект не меняется. А вот так можно явно указать компилятору, что нужно использовать сам объект (точнее - ссылку, хотя это одно и тоже), а не его копию:
Код

void func(int& i)
{
    i = 15;// Вот теперь a примет значение 15
}

}
  Вверх
maxim1000
Дата 22.9.2005, 00:56 (ссылка) |    (голосов:2) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Участник
Сообщений: 3334
Регистрация: 11.1.2003
Где: Киев

Репутация: 1
Всего: 110



ссылка ближе к указателю, чем к пременной
в общем-то именно так она и реализована на уровне компилятора
просто везде, где она используется, компилятор дорисовывает звездочку smile
Код

int x;
int &y=x;
y++;
x=y;

при компиляции будет сначала переделано в такое:
Код

int x;
int *z=&x;
(*z)++;
x=(*z);

поэтому, когда мы пишем
Код

y=x;

на самом деле получается
Код

(*z)=x;

получается, что изменить значение адреса невозможно

единственное, чем отличается ссылка от указателя - инициализация:
Код

int &y=x;

означает занесение адреса x в y
и это навсегда (пока не выйдем из этого блока)...


--------------------
qqq
PM WWW   Вверх
509harbore
Дата 22.9.2005, 20:32 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 2
Регистрация: 4.9.2005

Репутация: нет
Всего: нет



ManiaK ,maxim1000 ,благодарю вас за столь хорошие отвтеты. smile
А для чего служит необходимость разыменования указателей? smile
PM MAIL   Вверх
Void
Дата 22.9.2005, 20:55 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


λcat.lolcat
****


Профиль
Группа: Участник Клуба
Сообщений: 2206
Регистрация: 16.11.2004
Где: Zürich

Репутация: 2
Всего: 173



Цитата(509harbore @ 22.9.2005, 22:32)
А для чего служит необходимость разыменования указателей

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


--------------------
“Coming back to where you started is not the same as never leaving.” — Terry Pratchett
PM MAIL WWW GTalk   Вверх
BlHol
Дата 3.5.2006, 12:37 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 126
Регистрация: 26.11.2005

Репутация: нет
Всего: нет



Маленький вопрос:

Вот это понятно:

Код

TComponent *Obj;


Указатель на объект класса.


А вот это как понимать?:

Код

(TComponent *)Sender;



Заранее благодарен.
С уважением. 
PM MAIL   Вверх
ManiaK
Дата 30.5.2006, 16:49 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Homo Sapience
***


Профиль
Группа: Комодератор
Сообщений: 1145
Регистрация: 3.8.2004
Где: ИУ5-93

Репутация: 2
Всего: 29



BlHol, абсолютно никакой разницы нет. 
PM MAIL WWW   Вверх
champion
Дата 22.6.2006, 17:37 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 272
Регистрация: 26.1.2005

Репутация: нет
Всего: 2



Я правильно понял, что указатель это переменная, которая ссылается на адрес, по которому находится содержание переменной,  другой переменной?  И каков смысл применения указателей? Приведите пожалуйста в пример код, из которого было бы видно, где их используют. 
 В статье я увидел int *i=alloc(1000); что есть alloc?
 И все остальное из этой кухни, что приводится в пример? 


--------------------
user posted image
PM MAIL   Вверх
slava72
Дата 26.6.2006, 12:11 (ссылка) |    (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 22
Регистрация: 23.6.2006

Репутация: нет
Всего: 1



2ManiaK: продолжения похоже так и не дождутся новички ))

Тем не менее тема, почему не стоит пользоваться C-style строками в С++ ИМХО очень важна.
Итак попытка продолжения - разрешается пользоватся без ограничений, в т.ч. копировать, редактировать, etc. (например вставить после редактирвания в начальную статью).

Строки в стиле С++

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

В С у нас есть только базовый набор функций над участками памяти, исходя из предположения,
что строка - это набор символов (char или wchar_t), заканчивающийся нулевым символом ('\0').

Основные недостатки этого подхода:
Мы должны заранее распределять память (неважно каким образом), достаточную для помещения в нее строки (+1 символ) при всех манипуляциях над ней.
Если память ваделяется динамически - не забыть ее вернуть системе.
Использовать в коде strcpy(a,b) вместо a+b, strcmp(a,b) < 0  вместо a < b и т.д., что затрудняет читабельность программы (и время написания ;)

Для избавления от этих недостатков в C++ введен стандартный класс std::string, с которым можно работать почти также как с обычным типом (например int), и не задумываться о распределении памяти - все за вас сделает компилятор.
Код

  typedef std::string MyStr; // для любителей

  MyStr str_list[100]; // обявляем массив строк
  MyStr prefix = "эта строка содержит\""; // объявляем строку и инициализируем ее
  MyStr suffix = "\";\n";
  str_list[0] = "А это 1-я строка массива";
  
  if(str_list[0] < suffix) { ... } // операция сравнения 2-х строк
  str_list[1] = prefix + str_list[0]; // конкатенация (объединение) 2-х строк
  str_list[1] += suffix; // то же самое, что str_list[1] = str_list[1] + suffix;
  ...
  str_list[99] = "the last";
  for(int i = 0; str_list[i] != "the last" ; ++i) {
    ...
  }
  const char* pc1 = str_list[3].c_str(); // получаем неизменяемую С-строку
  const char* pc2 = str_list[5].c_str();
  if((stricmp(pc1, pc2) == 0) {// проверяем равенство строк без учета регистра
  }

....
bool Find_stri(
  std::string dets&,      // изменяемый аргумент, прередается по ссылке
  const std::string src1,  // не изменяемый аргумент, прередается по ссылке
  std::string src2         // передача по значению - так не стоит делать
)
  


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

Хотя внешне обект строка похож на обычный int, тем не менее для каждого нового объекта распределяется память, вызываются конструкторы/деструкторы и т.д.

Поэтому при написании своих функций в списоке аргументов желательно:
- Для входных параметров объявлять аргумент как const string param&
- для выходных (изменяемых) - как string param&

Использование строк в качестве char* (C-строк) - можно для функций, который _не_ модифицируют
строки (с использованием c_str() см. код).
Если же требуется буфер обмена с изменяемой памятью - его придется создавать самостоятельно,
но зато потом можно будет присваивать строке.

Зы: навеяно вопросом 
PM MAIL   Вверх
ManiaK
Дата 24.7.2006, 10:01 (ссылка) |    (голосов:2) Загрузка ... Загрузка ... Быстрая цитата Цитата


Homo Sapience
***


Профиль
Группа: Комодератор
Сообщений: 1145
Регистрация: 3.8.2004
Где: ИУ5-93

Репутация: 2
Всего: 29



Цитата(champion @  22.6.2006,  17:37 Найти цитируемый пост)
В статье я увидел int *i=alloc(1000); что есть alloc?

Читаем внимательней, коментарии не зря ставил:
Код

int *i = alloc(1000);// [b]Хотим блок памяти в тысячу байт[/b]

Т.е.: данной функцией мы можем "попросить" у операционной системы дать нам память.

Цитата(champion @  22.6.2006,  17:37 Найти цитируемый пост)
Я правильно понял, что указатель это переменная, которая ссылается на адрес, по которому находится содержание переменной,  другой переменной?

Да. Причём не обязательно другой переменной. У вас в программе может существовать указатель на память, которой не соответствует ни одна переменная. Каким образом мы получим доступ к этой памяти? Только с помощью указателя на неё. Это ответ на следующий вопрос:
Цитата(champion @  22.6.2006,  17:37 Найти цитируемый пост)
И каков смысл применения указателей?


Цитата(champion @  22.6.2006,  17:37 Найти цитируемый пост)
Приведите пожалуйста в пример код, из которого было бы видно, где их используют. 

Пожалуйста! - 
Код

char* i = alloc(1000); // Получаем 1000 байт
FILE* file = fopen("myfile.txt", "rb");// Получаем указатель на данные файла
fread(i, 1000, 1, file);// Читаем из файла, на который указывает file 1 блок данных размером в 1000 байт


[оффтоп]
Подписался на тему, теперь буду отвечать во-время smile
[/оффтоп]

Добавлено @ 10:11 
Цитата(BlHol @  3.5.2006,  12:37 Найти цитируемый пост)
А вот это как понимать?:

Пардон, если вот так:

Код

int* Sender = alloc(1000);
TComponent * p = (TComponent *)Sender;


То это т.н. преобразование типов. Название страшное, но, как обычно, идея элементарная: в первой строке мы создаём переменную-указатель типом int, во второй строке - типом TComponent. Си++ отличается от Си тем, что довольно строго относится ко взаимодействиям объектов разного типа. Если бы мы написали вот так вторую строку:
Код

TComponent * p = Sender;

То компилятор заявил бы нам, мол, парень, очко протри: у тебя слева помидор, а справа огурец: ты их потом не перепутаешь? А то может у тебя алергия на помидоры, а ты съешь указатель на огурец, который на самом деле указывает на помидор.

Когда мы пишем перед объектом "(Type*)", мы фактически говорим компилятору: думай шо хошь, но тип этого объекта - указатель на Type. У меня всё под контролем, мол... 
PM MAIL WWW   Вверх
NightmareZ
Дата 2.11.2006, 13:28 (ссылка) |    (голосов:2) Загрузка ... Загрузка ... Быстрая цитата Цитата


[хакер]
**


Профиль
Группа: Участник
Сообщений: 699
Регистрация: 10.8.2006

Репутация: 0
Всего: 13



Цитата(Lokky @  31.8.2006,  00:51 Найти цитируемый пост)
1. enum (пречисления);

Код

enum MyEnum
{
   One, Two, Three
};

Таким образом определяем тип данных MyEnum, который может принимать три значения: OneTwo и Three.
При этом OneTwo и Three - это целочисленные константы.
Далее MyEnum можно использовать, например, так:
Код

MyEnum hello = One;

switch (hello)
{
  case One:
    cout << "One";
  break;
  case Two:
    cout << "Two";
  break;
  case Three:
    cout << "Three";
}


Цитата(Lokky @  31.8.2006,  00:51 Найти цитируемый пост)
2. struct (структуры) и чем они отличаются от enum;

Структуры же - это совокупность переменных, объединённых под одним именем. Если мы определим структуру вот так:
Код

struct MyStruct
{
  int One, Two, Three;
};

то сможем создавать переменные, содержащие в себе три значения (OneTwo и Three).
Обращаться к отдельным переменным внутри структуры нужно через точку:
Код

MyStruct hello;

hello.One = 10;


Цитата(Lokky @  31.8.2006,  00:51 Найти цитируемый пост)
3. union (объединения).

Объединения позволяют хранить переменные разного типа в одной области памяти.
Код

union MyUnion
{
    struct {unsigned char One, Two;} Four;
    unsigned short int Three;
};

MyUnion hello;
Three = 256;

Если unsigned short int занимает два байта, а unsigned char - один, то, после данного присваивания, переменная One будет содержать ноль, а Two - единицу.


--------------------
NightmareZ.net - мой блог и сайт, мои проекты и прочий трэш
Ely-Art.ru - наша маленькая домашняя арт-студия
mugcraft.ru - кружки на любой вкус
PM WWW ICQ Skype GTalk AOL YIM   Вверх
Frekenbok
Дата 10.11.2006, 06:05 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 51
Регистрация: 29.10.2006

Репутация: нет
Всего: 1



Со всеми этими обсуждениями вторая глава как-то осталась незамеченной. Или всем все понятно? Мне вот не все.
ManiaK, батенька, да вы философ! (я имею в виду чайники)  smile 
Вот мои вопросы:
  •  В структуре поля хранятся в соседних ячейках памяти наподобие массивов?
  •  
    Цитата(ManiaK @  7.11.2006,  15:29 Найти цитируемый пост)
    int func(int* a, char* b)

    Почему char* b? Потому что первое поле структуры типа char? 
    Почему тогда...
    Цитата(ManiaK @  7.11.2006,  15:29 Найти цитируемый пост)
    int func2(int a[13], double b)

    Или здесь во второй параметр передается уже не структура?
  •  Объясни, плиз, поподробнее разницу между "точками" и "стрелками" для доступа к полям классов (кстати, только к полям или к методам тоже?). В Delphi вот точка да и все. А здесь как? Или можно просто ссылочку полезную дать...



PM MAIL   Вверх
archimed7592
Дата 10.11.2006, 07:21 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Архимед
****


Профиль
Группа: Завсегдатай
Сообщений: 2531
Регистрация: 12.6.2004
Где: Moscow

Репутация: 6
Всего: 93



Цитата(Frekenbok @  10.11.2006,  07:05 Найти цитируемый пост)
 В структуре поля хранятся в соседних ячейках памяти наподобие массивов?
да в соседних ячейках, но только при выполнении определенных условий. точнее говоря, в общем случае между полями структуры могут быть "дырки"(маленькие - 1-7 байт).
но это не массивы. массив - это N элементов одного заданного типа. структура же может иметь много полей и все могут быть разных типов, в том числе и массивами. структура - это как мусорный контейнер (совсем не в плохом слысле, просто в голову больше ниче не приходит), а массив - как корзина для яиц.
кстати, помимо того, что структура может содержать массивы ещё не редко встречаются массивы из структур.

Цитата(Frekenbok @  10.11.2006,  07:05 Найти цитируемый пост)
Почему char* b? Потому что первое поле структуры типа char? 
вступление: передача параметров как правило производится через регистры (размер регистра 4 байта). если регистров не хватает, то передача параметров производится через стек.
ещё одно вступление: указатель - это почти в любом случае 4 байта - просто номер первого (нулевого) байта в памяти (для int - это номер младшего байта).
ответ: нет. ManiaK сказал, что с точки зрения ассемблера - это одинаковые ф-ции (лучше было сказать сигнатуры)...ф-ции то разные, но вот способ их вызова одинаковый. т. е. способ их вызова при хорошо сложившихся звёздах будет одинаковый. т. е., когда мы вызываем любую из этих ф-ций происходит следующее: первый аргумент кладётся в регистр1, второй - в регистр2. и в том и в другом случае второй аргумент - это указатель. в первом случае - на char, во втором - на Chaynik. но! с точки зрения ассемблера разницы между указателем на char и указателем на Chaynik нету. в первом случае всё понятно, а вот во втором: почему указатель, мы ж структуру передаем? вот собственно говоря та мысль, которую пытался донести ManiaK: при передаче малых объектов (до 4 байт) они передаются как есть т. е. их копия лежит в регистре, а для больших объектов делается следующее: создается копия на стеке и в регистре передается уже указатель, а не сам объект.

Цитата(Frekenbok @  10.11.2006,  07:05 Найти цитируемый пост)
Или здесь во второй параметр передается уже не структура?
во второй параметр передается не структура, но некий объект, размер которого превышает 4 байта (8 байт) и потому разницы между сигнатурами всех трех ф-ций не будет - в первой укзатель передается явно, в друих двух компилятор делает так, что ты не замечаешь как делается копия и неявно передается указатель на неё.

Цитата(Frekenbok @  10.11.2006,  07:05 Найти цитируемый пост)
Объясни, плиз, поподробнее разницу между "точками" и "стрелками" для доступа к полям классов. В Delphi вот точка да и все.
просто в делфи нет указателей на классы smile в делфи class - это и есть указатель на object (есть там такой, но про него мало кто знает smile) т. е. в делфи на уровне типов принимается решение каким образом получать доступ к объекту. в си это немного иначе. в си компилятор "ленится для твоего же блага" и не принимает решений на уровне типов. полагаю, что ты усвоил, что указатель - это номер байта в памяти, и чтобы получить эту самую память (на которую указывает указатель) нужно его прежде разыменовать: 
Код
int x = 5;
int *px = &x;
int y = *px; //<< звездочка перед px разыменовывает указатель и получает само значение x
дык вот. стрелочка - это просто упрощение синтаксиса: стрелочка - это два в одном
1. разыменовать
2. взять поле.
т. е.
Код
struct S
{
    int x;
    int y;
};

S s;

s.x = 0;
s.y = 0;

S *ps = &s;

ps->x = 5; // изменяем s.x - так проще
(*ps).y = 10; // изменяем s.y - так тоже просто, но смотрится не оч. красиво...

int result = s.x + s.y; // result = 15


Цитата(Frekenbok @  10.11.2006,  07:05 Найти цитируемый пост)
(кстати, только к полям или к методам тоже?)
к методам тоже. к любым членам.


--------------------
If you have an apple and I have an apple and we exchange apples then you and I will still each have one apple. But if you have an idea and I have an idea and we exchange these ideas, then each of us will have two ideas.
© George Bernard Shaw
PM Jabber   Вверх
Frekenbok
Дата 12.11.2006, 08:51 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 51
Регистрация: 29.10.2006

Репутация: нет
Всего: 1



archimed7592, еще раз, для особо одаренных...  smile 

Цитата(archimed7592 @  10.11.2006,  07:21 Найти цитируемый пост)
int x = 5;
int *px = &x;
int y = *px; //<< звездочка перед px разыменовывает указатель и получает само значение x


Насколько я понимаю, указатель должен указывать на уже существующий объект (переменную и т.п.) или на NULL. Возьмем для примера класс TButton (С++ Builder). Я получаю доступ к полям и методам кнопки через указатель? А "где" сам объект? Компилятор создает объект и указатель на него, а я работаю только с указателем?
PM MAIL   Вверх
Ответ в темуСоздание новой темы Создание опроса
Правила форума "C/C++: Для новичков"
JackYF
bsa

Запрещается!

1. Публиковать ссылки на вскрытые компоненты

2. Обсуждать взлом компонентов и делиться вскрытыми компонентами

  • Действия модераторов можно обсудить здесь
  • С просьбами о написании курсовой, реферата и т.п. обращаться сюда
  • Вопросы по реализации алгоритмов рассматриваются здесь


Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, JackYF, bsa.

 
0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | C/C++: Для новичков | Следующая тема »


 




[ Время генерации скрипта: 0.2092 ]   [ Использовано запросов: 23 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.