Полезное для программистов:

Фриланс
Новости
Статьи
   
Рубрики:

Error. Page cannot be displayed. Please contact your service provider for more details. (24)


СИ строки и немного теории

Поиск:
ВВЕДЕНИЕ

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

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

    Готовы?..

    СИ СТРОКИ ИЛИ НЕМНОГО ТЕОРИИ.

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

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

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

Код

    int main() {
        int x;

        {
            int y;
        }
        // К этому времени переменной y уже нет и память от неё оставшуюся
        // можно задействовать для z
        int z;

        return 0;
    }


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

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

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

Код

    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;// Результат не определён, зависит от конкретного компилятора или ОС


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

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

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

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

Код

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


    Знаете, что сделают эти две строчки, если пустить их сразу после инициализации Биос? Выведут вам на экран строку "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// Число символов до любого символа не из строки


Ну вот, на этом с Си строками можно закончить. Переходим к Си++.
Автор: ManiaK
Сайт: http://www.qpp.ru/






Просмотров: 24625

 

 

Новые статьи:


Популярные:
  1. Как сделать цикличным проигрывание MIDI-файла?
  2. Создание AVI файла из рисунков
  3. Как устройство "отключить в данной конфигурации"?
  4. Kто в данный момент присоединен через Сеть?
  5. Как узнать количество доступной памяти?
  6. Как реализовать в RichEdit разноцветный текст?
  7. Как скрыть свое приложение от ProcessViewer
  8. Как программно нажать/скрыть/показ кнопку "Start"?
  9. Модуль работы с ресурсами в PE файлах
10. Функции вызова диалоговых окон выбора
11. Проверка граматики средствами Word'а из Delphi.
12. Модуль для упрощенного вызова сообщений
13. Функции для записи и чтение своих данных в, ЕХЕ- файле
14. Рекурсивный просмотр директорий
15. Network Traffic Monitor
16. Разные модули
17. Универсальная функция для обращения к любым экспортируем функциям DLL
18. Библиотека от VladS
19. Протектор для UPX'а
20. Еще об ICQ, сообщения по контакт листу?
21. Использование открытых интерфейсов
22. Теория и практика использования RTTI
23. Работа с TApplication
24. Примеры использования Drag and Drop для различных визуальных компонентов
25. Что такое порт? Правила для работы с портами
26. Симфония на клавиатуре
27. Загрузка DLL
28. Исправление автоинкремента
29. Взаимодействие с чужими окнами
30. Проверить дубляжи в столбце


 

 

 
 
На главную