Какие строковые типы существуют в Delphi, и чем они отличаются друг от друга. 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Какие строковые типы существуют в Delphi, и чем они отличаются друг от друга.



В Delphi 1.0 существовал лишь единственный строковый тип String, полностью эквивалентный одноименному типу в Turbo Pascal и Borland Pascal. Однако, этот тип имеет существенные ограничения. Для обхода этих ограничений, в Delphi 2, разработчики из Borland устроили небольшую революцию. Теперь, начиная с Delphi 2, имеются три фундаментальных строковых типа: ShortString, AnsiString, и WideString. Кроме того, тип String теперь стал логическим. Т.е., в зависимости от настройки соответствующего режима компилятора (режим больших строк), он приравнивается либо к типу ShortString (для совместимости со старыми программами), либо к типу AnsiString (по умолчанию). Управлять режимом, можно используя директиву компиляции {$LONGSTRINGS ON/OFF} (короткая форма {$H+/-}) или из окна настроек проекта – вкладка "Compiler" -> галочка "Huge strings". Если режим включен, то String приравнивается к AnsiString, иначе String приравнивается ShortString. Из этого правила есть исключение: если в определении типа String указан максимальный размер строки, например String[25], то, вне зависимости от режима компилятора, этот тип будет приравнен к ShortString соответствующего размера.

Поскольку по умолчанию, после установки Delphi, режим больших строк включен, большинство молодых программистов даже и не подозревают, что String может представлять что-то отличное от AnsiString. Поэтому, дальше в этом пункте, любое упоминание типа String без указания размера, подразумевает, что он равен типу AnsiString, если не будет явно указано иное. Т.е., считается что настройка компилятора соответствует настройке по умолчанию.

Существуют различия между типами AnsiString и WideString. Эти типы имеют практически одинаковую реализацию, и отличаются лишь тем, что WideString используется для представления строк в кодировке UNICODE использующей 16-ти битное представление каждого символа (WideChar). Эта кодировка используется в тех случаях когда необходима возможность одновременного присутствия в одной строке символов из двух и более языков (помимо английского). Например, строк содержащих одновременно символы английского, русского и европейских языков. За эту возможность приходится платить – размер памяти, занимаемый такими строками в два раза больше размера, занимаемого обычными строками. Использование WideString встречается не часто, поэтому, я буду в основном рассказывать о строках типа AnsiString. Но, поскольку они имеют одинаковую реализацию, почти все сказанное относительно AnsiString будет действительно и для WideString, естественно с учетом разницы в размере каждого символа.

Тоже самое касается и разницы между pChar и pWideChar.

Строковый тип AnsiString, обычно используется для представления строк в кодировке ANSI, или других (например OEM) в которых для кодирования одного символа используется один байт (8 бит). Такой способ кодирования называется single-byte character set, или SBCS. Но, очень многие не знают о существовании еще одного способа кодирования многоязычных строк (помимо UNICODE) используемого в системах Windows и Linux. Этот способ называется multibyte character sets, или MBCS. При этом способе, некоторые символы представляются одним байтом, а некоторые, двумя и более. В отличие от UNICODE, строки, закодированные таким способом, требуют меньше памяти для своего хранения, но требуют более сложной обработки. Так вот, строковый тип AnsiString может использоваться для хранения таких строк.

Также существуют еще типы pChar (pWideChar) и array [...] of Char, они очень часто используются в сочетании со строковыми типами.

Итак, основные характеристики строковых типов:

Тип Максимальный размер строки Размер переменной Объем памяти, требуемый для хранения строки
String[n] где 0 <= n <= 255 n символов n+1 байт n+1 байт
ShortString 255 символов 256 байт 256 байт
AnsiString ~2^31 символов 4 байта 4 байта + (0.. 2 Гбайт)
WideString ~2^30 символов 4 байта 4 байта + (0.. 2 Гбайт)
pChar не ограничено 4 байта 4 байта + размер строки +1
pWideChar не ограничено 4 байта 4 байта + (размер строки+1)*2
array [0..n] of Char n n+1 байт n+1 байт
array [0..n] of WideChar n (n+1)*2 байт (n+1)*2 байт

Теперь, остановимся подробнее на каждом из этих типов. Начнём с более простых.

array [0..n] of Char

Формально, этот тип не являются строковыми. Однако, в Delphi, он несколько отличаются от остальных типов массивов. А именно, если массив символов имеет нижнюю границу индекса равной 0, то Delphi считает такой тип совместимым по присваиванию со строковыми константами. Например, если переменная a будет описана как a:array[0..20] of Char, то оператор a:= 'abc' будет допустим. Причём, значения элементов начиная с a[3] и до a[20] будут установлены в #0.

Есть ещё – оператор @ (получение указателя) для переменной такого типа возвращает значение типа pChar. Это очень удобно, поскольку переменные этого типа очень часто используются как буфер при работе с функциями Windows API. Например:

var a:array[0..20] of Char;... GetModuleFileName(GetModuleFileName(HInstance,@a,SizeOf(a));

Здесь, функция GetModuleFileName возвращает результат в массив a.

PChar

Этот тип широко используется в языках C и C++. В Delphi, это не фундаментальный тип, а производный. Его определение выглядит так:

pChar = ^Char

Т.е. переменные этого типа являются указателем, поэтому и имеют размер 4 байта. Формально, значение pChar может указывать как на один символ, так и на строку символов. Однако, общепринято что значения pChar указывают на строки, завершающиеся символом с кодом 0 (#0). В DOSе, такие строки назывались ASCIIZ, но чаще можно встретить название null-terminated string. Наличие такого «концевика» позволяет легко определить реальный размер строки на которую указывает значение pChar.

Не смотря на то, что формально pChar это указатель на Char (^Char), как это часто бывает в Delphi, тип pChar имеет несколько особенностей по сравнению с другими указателями. Таких особенностей несколько.

Первая, заключается в том, что константы, и глобальные переменные этого типа могут быть проинициализированы строковой константой. Вот пример:

const pc:pChar ='abc';var pv:pChar ='abc';

Эти строки, определяют константу pc и переменную pv типа pChar. При этом, и pc и pv указывают на разные области памяти, но содержащие одинаковые значения, состоящие из трех символов: 'a', 'b', 'c', и символа #0. Завершающий символ с кодом 0 компилятор добавил автоматически.

Вторая особенность в том, что к переменным типа pChar применимо обращение как к массиву символов. Например, если есть приведенные выше определения, тогда:

C:= pv^; // C будет присвоен символ 'a'. Это обычное обращениеC:= pv[0]; // необычно, но С станет равным 'a'C:= pv[1]; // С станет равным 'b'C:= pv[2]; // С станет равным 'c'C:= pv[3]; // С станет равным #0C:= pv[4]; // ОШИБКА!

Символ с индексом 3 отсутствует в строке, однако, там есть завершающий ее символ с кодом 0. Именно он будет результатом pv[3]. О pv[4] тоже стоит сказать особо. Дело в том, что компилятор не даст ошибки при компиляции, поскольку на этапе компиляции он, в общем случае, не известен реальный размер строки, на которую указывает переменная pv. Однако, на этапе выполнения программы, такое обращение может вызвать ошибку нарушения доступа к памяти (Access Violation). А может и не вызвать, но результатом будет неопределённое значение. Все зависит от «расклада» в памяти. Поэтому, при таком способе обращения необходимо быть внимательным, и выполнять все необходимые проверки, исключающие выход за размеры строки.

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

function StringLength (p:pChar):Cardinal;begin Result:= 0; if p = nil then Exit; while p^ <> #0 do begin Inc(Result); Inc(p); end;end;

Здесь важно обратить внимание на два нюанса.

Первый, это проверка переданного указателя на nil. Такая проверка необходима, поскольку очень часто, для обозначения пустой строки используется значение nil.

Второй, это оператор Inc(p) – он «продвигает» указатель на следующий символ. Можно было бы записать его и так: p:= p + 1.

Что бы продемонстрировать вычитание указателей pChar, приведем еще один вариант реализации той же функции:

function StringLength (p:pChar):Cardinal;var pp:pChar;begin Result:= 0; pp:= p; if pp <> nil then while pp^ <> #0 do Inc(pp); Result:= (pp-p);end;

Здесь, выражение pp-p дает «расстояние» между указателями, т.е. число символов между символом, на который указывает указатель p (начало строки) и символом, на который указывает указатель pp (завершающий строку #0).

ShortString, и String[n]

ShortString является частным случаем String[n], а если быть более точным, он полностью эквивалентен String[255].

Если посмотреть на таблицу, где приведены характеристики переменных этого типа, то мы увидим что их размер на один байт больше чем размер хранимой в них строки. Но в данном случае, это не для завершающего символа, как в pChar. Здесь в дополнительном байте хранится текущий размер строки. Кроме того, этот байт располагается не в конце строки, а наоборот, в ее начале. К примеру, если имеется следующее определение:

var s:String[4];

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

Теперь, выполнение оператора s:= 'abc', приведёт к тому, что содержимое этих пяти байт станет следующим: байт 1 = 3, байт 2 = 'a', байт 3 = 'b', байт 4 = 'c', а значение байта 5 будет неопределённо – оно будет зависеть от «расклада» в памяти. Т.е., первый символ строки будет находиться во втором байте. Это неудобно, поэтому к символам строк ShortString принято индексироваться, начиная с 1. Следовательно:

s[1] = 'a' – первый символ строкиs[2] = 'b' – второй символ строкиs[3] = 'c' – третий символ строкиs[4] = все что угодно:).

А как же байт длины? Да все очень просто, к нему можно обратиться как s[0]. Только вот есть маленькая проблемка. Поскольку элементами строки являются символы, то и тип значения s[0] тоже будет символ. Т.е., если Вы хотите получить длину строки в виде целого числа, как это принято у нормальных людей, то надо выполнить соответствующее преобразование типа: Ord(s[0]) = 3 – размер строки.

Почему для типа String[n] существует ограничение 0<=n<=255. Это весь набор значений, которые могут быть представлены в одном байте. На всякий случай, отмечу, что размерность переменной (n) определяет размер выделяемой под эту переменную памяти, и он не зависит от размера строки, которая там хранится. Т.е., если, например переменная описана как String[30], то даже если присвоить ей значение 'abcd', то размер этой переменной все равно останется 31 байт. Это иногда бывает очень удобно, например, при записи (чтении) таких переменных в файл (из файла).

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

Пример:

function StringLength (s:ShortString):Cardinal;begin Result:= Ord(s[0]);end;

Как видите, это код существенно компактнее, и быстрее чем соответствующий код для pChar. Именно поэтому, он и применялся в языке Pascal.

Однако ясно, что ограничения переменных этого типа – максимальный размер строки 255 символов, и всегда максимальный размер занимаемой переменной памяти, вне зависимости от реально помещенной в нее строки, заставили разработчиков Delphi вести новые строковые типы. Вот к ним мы сейчас и перейдем.

AnsiString и WideString

Строки этого типа объединили в себе ряд качеств как строк ShortString и их байтом длины, так и строк завершающихся нулем (pChar). Последнее было необходимо, поскольку к моменту их появления, засилье C-ишников было уже так велико:), что большинство системных функций Windows API оперировало строками именно такого формата. А если серьезно, то такой формат строк хоть и более трудоемок в обработке, зато в принципе лишен ограниченности на максимальный размер строки. Ведь хранение размера строки всегда ограниченно какими-либо рамками: если хранить в байте, то ограничение 255 байт; если хранить в слове, то ограничение 65535 символов; и т.д. Однако, совсем отказываться от хранения текущей длинны строки в Borland не стали.

Еще одним из недостатков статически размещаемых строк ShortString было "расточительство". Т.е. определив переменную такого типа, мы заранее резервировали под нее 256 байт памяти, поэтому, если мы один раз, во всей программе, присвоили ей значение 'abcd', то 251 байт памяти мы просто «пустили на ветер». Казалось бы, а зачем так определили, написали бы String[4], и ничего не потеряли бы. Но, когда мы пишем программу, мы же чаще всего не знаем что мы будем «класть» в эту переменную. Вот и определяем с запасом. Решением этой проблемы стало использование динамически размещаемых строк.

Как они устроены?

Если вы обратили внимание, в таблице характеристик строковых типов, размер переменных AnsiString равен четырем байтам, как и у pChar. Это говорит нам о том, что переменные этого типа тоже являются указателями. Но, в отличие от pChar, в данном случае, это скрыто реализацией. Т.е. программист, работает с ними как с обычными строками: не надо выделять под них память, не надо заботиться о наличии завершающего нуля, не надо волноваться об изменении размеров строки и т.п. Всю эту работу берет на себя компилятор Delphi. Более того, он ещё и занимается оптимизациями. В частности, если например выполнить следующий код:

var s1, s2:AnsiString;...s1:= 'abc';s2:= s1;

то в памяти будет храниться лишь ОДИН экземпляр строки 'abc'!

Как же это происходит?

При выполнении первого оператора (присваивание s1), указатель, хранящийся в переменной s1 настраивается на область памяти в которой размещена строка 'abc'. При выполнении второго оператора, в s2 попадает тот же адрес! Т.е. в памяти при это присутствует лишь один экземпляр строки 'abc'. Да и зачем нам нужны дублирующие себя строки.

Не нужно программисту заботиться и об освобождении памяти занятой ненужными уже строками. Например, есть такая процедура:

procedure ShowInteger;var s:AnsiString; n:Integer;begin n:= 123; s:= 'Значение переменной равно '+IntToStr(n); ShowMessage(s);end;

Здесь, при выполнении присваивания, Delphi создаст в памяти экземпляр строки 'Значение переменной равно 123', и присвоит адрес этой строки переменной s. Однако, при завершении процедуры, переменная s перестанет существовать – она же локальная. Значит, и экземпляр строки тоже уже не нужен – на него некому будет указывать. Вот поэтому, Delphi автоматически освободит память, выделенную под строку, как только, выполнение процедуры достигнет строки end. Более того, даже если во время выполнения процедуры возникнет исключительная ситуация, при которой "хвост" процедуры может и не выполниться, Delphi всё равно корректно освободит память для всех строк, распределенных в этой процедуре. Достигается это неявным использованием механизма подобного try - finally.

Казалось бы, всё замечательно. Но попробуем усложнить ситуацию.

var gs:AnsiString; // глобальная переменнаяprocedure ShowInteger;var s:AnsiString; n:Integer;begin n:= 123; s:= 'Значение переменной равно '+IntToStr(n); gs:= s; ShowMessage(s);end;

Теперь, к моменту завершения процедуры, на экземпляр строки 'Значение переменной равно 123' уже ссылаются две переменные s и gs. И, несмотря на то, что область существования переменной s заканчивается, освобождать память, выделенную под строку на которую она указывает нельзя! Ведь позже, возможны обращения к переменной gs.

Для того, чтобы корректно обрабатывать такие ситуации, Delphi для каждой динамически распределенной строки ведет так называемый "счётчик ссылок". Т.е., как только он присваивает какой-либо из строковых (AnsiString) переменных ссылку на распределенную в памяти строку, то он увеличивает этот счетчик на единицу. Первоначально, при присваивании динамически распределённой строки, первой переменной (в примере s), значение этого счётчика устанавливается равным единице. В последствии, при прекращении жизни каждой строковой переменной, он уменьшает на 1 этот счетчик для той строки на которую она указывает. Если счётчик становится равным 0, то значит более нет строковых переменных, указывающих на данную строку. Значит, ее можно освобождать. Благодаря такому алгоритму, после присваивания в примере значения переменной gs, у строки 'Значение переменной равно 123' счетчик ссылок становится равным 2. Следовательно, при "умирании" переменной s, он декрементируется, и становится равным 1. Т.е. >0, поэтому то Delphi и не освобождает память, занятую строкой.

Еще, этот счётчик используется и для разрешения проблем, связанных со следующей ситуацией:

procedure ShowInteger;var s1:AnsiString; s2: AnsiString; n:Integer;begin n:= 123; s1:= 'abc'+IntToStr(n); s2:= s1; s2[1]:= 'X';end;

Здесь, как мы уже знаем, после выполнения оператора s2:= s1, обе переменные указывают на один и тот же экземпляр строки 'abc123'. Однако, что же произойдёт когда выполниться оператор s2[1]:= 'X'? Казалось бы, в единственном имеющимся в нашем распоряжении экземпляре строки первая буква будет заменена на 'X'. И как следствие, обе строки станут равными 'Xbc123'. s1 то за что "страдает"? Но, к счастью это не так. Здесь на помощь Delphi вновь приходит счетчик ссылок. Delphi, при выполнении этого оператора понимает, что строка на которую указывает s2 будет изменена, а это может повлиять на других. Поэтому, перед изменением строки, проверяется ее счётчик ссылок. Обнаружив, что на нее ссылается более одной строковой переменной, делается следующее: создается копия этой строки со счётчиком ссылок равным 1, и адрес этой копии, присваивается s2; У исходного экземпляра строки, счетчик ссылок уменьшается на 1 – ведь s2 на неё теперь не ссылается. И лишь после этого, происходит изменение первой буквы, теперь уже собственного экземпляра строки. Т.е., по окончанию выполнения этого оператора, в памяти будут находиться две строки: 'abc123' и 'Xbc123'. Причем, s1 будет ссылаться на первую, а s2 на вторую.

При работе со строками определенными как константы, алгоритм работы несколько отличается.

Пример:

procedure ShowInteger;var s:AnsiString;begin s:= 'Вася'; ShowMessage(s);end;

Казалось бы, при завершении работы процедуры, экземпляр строки 'Вася' должен быть уничтожен. Но в данном случае это не так. Ведь, при следующем входе в процедуру, для выполнения присваивания нужно будет вновь где-то взять строку 'Вася'. Для этого, ещё при компиляции, Delphi размещает экземпляр строки 'Вася' в области констант программы, где её даже невозможно изменить, по крайней мере, простыми методами. Но как же при завершении процедуры определить что строка 'Вася' – константная строка, и ее нельзя уничтожать? Все очень просто. Для константных строк, счётчик ссылок устанавливается равным -1. Это значение, "выключает" нормальный алгоритм работы со "счётчиком ссылок". Он не увеличивается при присваивании, и не уменьшается при уничтожении переменной. Однако, при попытке изменения переменной (помните s2[1]:='X'), значение счётчика равное -1 будет всегда считаться признаком того, что на строку ссылается более одной переменной (ведь он не равен 1). Поэтому, в такой ситуации всегда будет создаваться уникальный экземпляр строки, естественно, без декремента счётчика ссылок старой. Это защитит от изменений экземпляр строки-константы.

К сожалению, этот алгоритм срабатывает не всегда.

Где же Delphi хранит "счётчик ссылок"? Причем, для каждой строки свой! Естественно, вместе с самой строкой. Вот что представляет собой эта область памяти, хранящая экземпляр строки 'abc':

Байты с 1 по 4 Счётчик ссылок равный -1
Байты с 5 по 8 Длина строки равная 3
Байт 9 Символ 'a'
Байт 10 Символ 'b'
Байт 11 Символ 'c'
Байт 12 Символ с кодом 0 (#0)

Для удобства работы с такой структурой, когда строковой переменной присваивается ссылка на эту строку, в переменную заносится адрес не начала этой структуры, а адрес её девятого байта. Т.е. адрес начала реальной строки (прямо как pChar). Для того, что бы приблизиться к реальной жизни, перепишем приведённую структуру:

Смещение Размер Значение Назначение
-8   -1 Счётчик ссылок
-4     Длина строки
    'a'  
    'b'  
    'c'  
    #0  

С полем по смещению -8, нам уже должно быть все ясно. Это значение, хранящееся в двойном слове (4 байта), тот самый счетчик, который позволяет оптимизировать хранение одинаковых строк. Значение этого счетчика имеет тип Integer, т.е. может быть отрицательным. На самом деле, используется лишь одно отрицательное значение – "-1", и положительные значения. 0 не используется.

Теперь, обратим внимание на поле, лежащее по смещению -4. Это, четырёхбайтовое значение длинны строки (почти как в ShortString). Думаю, Вы заметили, что размер памяти выделенной под эту строку не имеет избыточности. Т.е. компилятор выделяет под строку минимально необходимое число байт памяти. Это конечно хорошо, но, при попытке "нарастить" строку: s1:= s1 + 'd', компилятору, точнее библиотеке времени исполнения (RTL) придется перераспределить память. Ведь теперь строке требуется больше памяти, аж на целый байт. Для перераспределения памяти нужно знать текущий размер строки. Вероятно, именно для того, что бы не приходилось каждый раз сканировать строку, определяя её размер, разработчики Delphi и включили поле длины, строки в эту структуру. Длина строки, хранится как значение Integer, отсюда и ограничение на максимальный размер таких строк – 2 Гбайт. Кстати, именно потому, что память под эти строки выделяется динамически, они и получили ещё одно свое название: динамические строки.

Ещё немного о нескольких особенностях переменных AnsiString. Важнейшей особенностью значений этого типа является возможность приведения их к типу Pointer. Это впрочем, естественно, ведь в "душе" они и есть указатели, как бы они этого не скрывали. Например, если описаны переменные: s:AnsiString и p:Pointer. То выполнение оператора p:= Pointer(s) приведет к тому, что переменная p станет указывать на экземпляр строки. Однако, при этом, очень важно знать: счетчик ссылок этой строки не будет увеличен.

Поскольку, переменные этого типа реально являются указателями, то для них и реально такое значение как Nil – указатель в "никуда". Это значение в переменной типа AnsiString по смыслу приравнивается пустой строке. Более того, чтобы не тратить память и время на ведение счётчика ссылок, и поля размера строки всегда равного 0, при присваивании пустой строке переменной этого типа, реально, присваивается значение Nil. Это не очевидно, поскольку обычно не заметно, но как мы увидим позже, очень важная особенность.

Преобразование строк из одного типа в другой

Преобразование между "настоящими" строковыми типами String[n], ShortString, и AnsiString выполняются легко, и прозрачно. Никаких явных действий делать не надо, Delphi все сделает за Вас. Надо лишь понимать, что в маленькое большое не влезает. Например:

var s3:String[3]; s:AnsiString;... s:= 'abcdef'; s3:= s;

В результате выполнения этого кода, в переменной s3 окажется строка 'abc', а не 'abcdef'. С преобразованием из pChar в String[n], ShortString, и AnsiString, тоже всё очень не плохо. Просто присваивайте, и все будет нормально.

Сложности начинаются тогда, когда мы начинаем преобразовывать "настоящие" строковые типы в pChar. Непосредственное присваивание переменным типа pChar значений строк не допускается компилятором. На оператор p:= s где p имеет тип pChar, а s:AnsiString, компилятор выдаст сообщение: "Incompatible types: 'String' and 'PChar'" - несовместимые типы 'String' и 'PChar'. Чтобы избежать такой ошибки, надо применять явное приведение типа: p:= pChar(s). Так рекомендуют разработчики Delphi. В общем, они правы. Но, если вспомнить, как хранятся динамические строки - с нулем в конце, как и pChar. А еще и то, что к AnsiString применимо преобразование в тип Pointer. Станет очевидным, что всего, возможно целых три способа преобразования строки в pChar:

var s:AnsiString; p1,p2,p3:PChar;... p1:= pChar(s); p2:= Pointer(s); p3:= @(s[1]);

Все они, синтаксически правильны. И кажется, что все три указателя (p1, p2 и p3) будут в результате иметь одно и то же значение. Но это не так. Всё зависит от того, что находится в s. Если быть более точным, равно ли значение s пустой строке, или нет:

s <> ''p1 = p2 <> p3s = ''p1 <> p2 = p3

Чтобы была понятна причина такого явления, опишем, как Delphi выполняет каждое из этих преобразований. Переменные AnsiString представляющие пустые строки, реально имеют значение Nil. Так вот:

pChar(s)

Для выполнения преобразования pChar(s), компилятор генерит вызов специальной внутренней функции @LstrToPChar. Эта функция проверяет – если строковая переменная имеет значение Nil, то вместо него, она возвращает указатель на реально размещенную в памяти пустую строку. Т.е. pChar(s) никогда не вернет указатель равный Nil.

Pointer(s)

Тут все просто, такое преобразование просто возвращает содержимое строковой переменной. Т.е. если она при пустой строке содержит Nil, то и результатом преобразования будет Nil. Если же строка не пуста, то результатом будет адрес экземпляра строки.

@(s[1])

Здесь, все совсем по-другому. Перед выполнением такого преобразования, компилятор вставляет код, обеспечивающий ситуацию, когда указатель, хранящийся в s, будет единственным указателем на экземпляр строки в памяти. В нашем примере, если строка не пуста, то будет создана копия исходной строки. Вот ее-то адрес и будет возвращен как результат такого преобразования. Но, если строка пуста, то результатом будет Nil, как и во втором случае.

Теперь, интересно отметить, что если в приведенном примере, преобразование p3:= @(s[1]) выполнить первым, то при не пустой строке в s, все указатели (p1, p2, и p3), будут равны. И содержать они будут адрес "персонального" экземпляра строки.

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

Приведем пример. В нем, преобразование, рекомендуемое разработчиками, приводит к "странному" поведению программы:

procedure X1;var s:AnsiString; p:PChar;begin s:= 'abcd'; p:= PChar(s); p^:= 'X'; // <- ShowMessage(s);end;

вызывает ошибку доступа к памяти при выполнении строки, помеченной <=. Почему - предлагаю Вам разобраться самостоятельно. После прочтения данной статьи Ваших знаний для этого достаточно. В тоже время, код:

procedure X1;var s:AnsiString; p:PChar;begin s:= 'abcd'; p:= @(s[1]); p^:= 'X'; ShowMessage(s);end;

будет выполнен без ошибок, выведя строку 'Xabcd'. Также как и код:

procedure X1;var s:AnsiString; p:PChar;begin s:= 'abcd'; s[2]:= 'b'; p:= PChar(s); p^:= 'X'; ShowMessage(s);end;

Рассматривая преобразование AnsiString в pChar (получение адреса строки) нельзя не упомянуть ещё одну серьезную проблему – область действия для полученного таким путем указателя.

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

function IntToPChar (n:Integer):pChar;var s:AnsiString;begin s:= IntToStr(n); Result:= PChar(s);end;

Казалось бы, всё написано правильно. Однако если выполнить такой оператор:

ShowMessage(IntToPChar(100));

То, вместо ожидаемого окна со строкой '100', мы либо получим абракадабру, либо и того хуже – ошибку AV. А все почему? Да, просто, единственным учтённым указателем на экземпляр строки, полученный от IntToStr, будет s. Поэтому, когда его область действия прекращается по выходу из процедуры, экземпляр строки будет уничтожен. А не учтённый указатель, возвращаемый функцией IntToPChar, после этого станет указывать "куда бог пошлёт". Точнее, на то место в памяти, где недавно была строка. Если же переменная s будет объявлена на глобальном уровне, то будет нормально, но всё равно возможны ошибки. Например:

var s:AnsiString; // глобальная переменная...function IntToPChar (n:Integer):pChar;begin s:= IntToStr(n); Result:= PChar(s);end;...var p100, p200:pChar;begin p100:= IntToPChar(100); p200:= IntToPChar(200);...

После второго выполнения функции IntToPChar, указатель p100, опять будет указывать "куда бог пошлет", Почему, разберитесь сами:).

Длинные строки

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

Если происходит присваивание локальной переменной: procedure E2; var S: string; begin S:= 'String'; // refCnt = -1; end;

Переменная S получит счетчик равный -1 и будет ссылаться прямо на литерал. При присваивании другой локальной переменной или передаче в процедуру счетчик никогда не меняется. Естественно, ни о каком управлении памятью в данном случае речи не идет.

В остальных случаях (то есть когда строка помещается в глобальную переменную, поле, элемент массива и т.п.) Delphi создает обычную строку в динамической памяти, в которую копирует присваиваемую константу.

Любое изменение строки заменяется на вызов системных функций. Если счетчик строки в этот момент не равен 1, создается новая строка. Исключением из этого правила является доступ к содержимому строки по указателю (приведение к типу PChar). В этом случае уже ничего не контролируется. Ниже приведены два примера, иллюстрирующих такое поведение.

procedure E3;var S1, S2, S3: string;begin S1:= 'q'; // refCnt = -1 S2:= S1 + 'w'; // refCnt = 1 S3:= S2; // Сейчас в памяти располагаются 2 строки // 'q' refCnt = -1 (ссылка на которую содержится в S1) // 'qw' refCnt = 2 (ссылка на которую содержится в S2, S3) S3[1]:= '1'; // А сейчас уже три разных строки // 'q'; refCnt = -1 (ссылка на которую содержится в S1) // 'qw' refCnt = 1 (ссылка на которую содержится в S2) // '1w' refCnt = 1 (ссылка на которую содержится в S3)end; procedure E4;var S1, S2, S3: string;begin S1:= 'q'; // refCnt = -1 S2:= S1 + 'w'; // refCnt = 1 S3:= S2; // Сейчас в памяти располагаются 2 строки // S1 = 'q' refCnt = -1 // S2 = S3 = 'qw' refCnt = 2 PChar(S3)[0]:= '1'; // Сейчас также две строки

 

К изменению строки через PChar нужно относится с осторожностью. Рассмотрим код:

procedure E5;var S: string;begin S:= 'qqqqqqq'; // refCnt = -1 PChar(S)[0]:= '1';

end;

Это код правильно скомпилируется, но при выполнении выдаст ошибку нарушения доступа. Причина в том, что строка S (refCnt = -1) находится в сегменте памяти, защищенном от записи.

Поэтому казалось бы, безобидная процедура:

procedure E6(S: string)begin if length(S) > 0 then PChar(S)[0]:= 'q'; //...

end;

вызовет ошибку нарушения доступа при передаче в нее строки сrefCnt = -1.

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

Примеры

Пример 1.

В символьной строке подсчитать количество цифр, предшествующих первому символу «!»

 

-
+
+
-
Начало
s
K:=0; I:=1;  
I<=length(s) and s(i)<>’!’  
S(i)>=’0’ and s(i)<=’9’  
K:=k+1; I:=i+1;  
k
конец

 

Program C;

Var S: String;

K, I: Byte;

Begin

Writeln («введите строку»);

Readln (S);

K:=0;

I:=1;

While (I<=Length (S)) And (S[I]<>’!’) Do

Begin

If (S[I]>=’0’) And (S[i]<=’9’)

Then K:=K+1;

I:=I+1

End;

Writeln (‘Количество цифр до символа «! «равно ’,K)

End.

 

В этой программе переменная K играет роль счетчика цифр, а переменная I-роль параметра чикла. Цикл закончит выполнение при первом же выходе на конец строки. Символ S[I] является цифрой, если истинно отношение: 0<S[I]<9.

4.3. Контрольные вопросы

1. Что появится на экране дисплея в результате работы следующего блока программы:

str_1:=’ Привет друз ’;

str_2:=’ья’;

writeln (str_1+str_2);

writeln (concate(str_1, str_2));

2. Какой может быть длина строки?

3. Какие операции можно производить над строками? Опишите их.

4. С помощью каких функций можно работать со строками?

5. Составьте программу получения из слова «СТРОКА» слово «СЕТКА».

6. Какие строковые типы существуют в Delphi и чем они отличаются друг от друга?

7. К какому типу будет приравнен тип, если в определении типа String указан максимальный размер строки?

8. Чем отличаются типы AnsiString и WideString?

9. Сколько байт используется для кодирования одного символа в кодировке ANSI?

10. Каков максимальный размер строки типа ShortString?

11. Переменные какого типа являются указателем и имеют размер 4 байта?

12. В чем особенности типа pChar?

13. Перечислите недостатки строк ShortString.

14. Как устроены динамически размещенные строки?

15. Каковы особенности переменных AnsiString?

16. Назовите, в чем особенность длинных строк?

Задания к лабораторной работе № 4

Строки

Вариант 1.

1.Дана строка, заканчивающаяся точкой. Подсчитать, сколько слов в строке.

2. Дана строка. Указать те слова, которые содержат хотя бы одну букву с.

 

Вариант 2.

1.Дана строка, содержащая английский текст. Найти количество слов, начинающихся с буквы b.

2. Дана строка. Найти в ней те слова, которые начинаются и оканчиваются одной и той же буквой.

 

Вариант 3.

1. Дана строка. Подсчитать, сколько в ней букв r,k,t.

2. В строке заменить все двоеточия (:) точкой с запятой (;). Подсчитать количество замен.

 

Вариант 4.

1. Дана строка. Определить, сколько в ней символов *,;,:.

2. В строке удалить символ «двоеточие» (:) и подсчитать количество замен

 

Вариант 5.

1. Дана строка, содержащая текст. Найти длину самого короткого слова и самого длинного слова.

2. В строке вставить вместо пробела запятую и пробел.

 

Вариант 6.

1. Дана строка символов, среди которых есть двоеточие (:). Определить, сколько символов ему предшествует.

2. Удалить часть символьной строки, заключенной в скобки (вместе со скобками)

 

Вариант 7.

1. Дана строка, содержащая текст, заканчивающийся точкой. Вывести на экран слова, содержащие три буквы.

2. Определить сколько раз в строке встречается заданное слово.

 

Вариант 8.

1. Дана строка. Преобразовать ее, удалив каждый символ * и повторив каждый символ, отличный от *.

2. В строке имеется точка с запятой (;). Подсчитать количество символов до точки с запятой и после нее.

 

Вариант 9.

1. Дана строка. Определить, сколько раз входит в нее группа букв abc.

2. Дана строка. Преобразовать ее, заменив точками все двоеточия, встречающиеся среди первой половины символов строки, и заменив точками все восклицательные.

 

Вариант 10.

1. Дана строка. Подсчитать количество букв k в последнем ее слове.

2. Строка содержит одно слово. Проверить будет ли она одинаково читаться справа налево и слева направо (т.е. является ли оно палиндромом.)

 

Вариант 11.

1. Дана строка. Подсчитать, сколько различных символов встречается в ней. Вывести их на экран.

2. В записке слова зашифрованы – каждое из них записано наоборот. Расшифровать сообщение.

 

Вариант 12.

1. Дана строка. Подсчитать самую длинную последовательность подряд идущих букв a.

2. Проверить, одинаковое ли число открывающихся и закрывающихся скобок в данной строке.

 

Вариант 13.

1. Дана строка. Указать те слова, которые содержат хотя бы одну букву k.

2. Дана строка. Заменить в ней все парные символы на одиночные (например, аа -> а, тт -> т).

 

Вариант 14.

1.Имеется строка, содержащая буквы латинского алфавита и цифры. Вывести на экран длину наибольшей последовательности цифр, идущих подряд.

2. Дана строка, содержащая текст, заканчивающийся точкой. Вывести на экран слова, содержащие хотя бы одну букву о.

 

Вариант 15.

1. Дан набор слов, разделенных точкой с запятой (;). Набор заканчивается двоеточием (:). Определить, сколько в нем слов, заканчивающихся буквой а.

2. Дана строка, заканчивающаяся точкой. Подсчитать, сколько запятых в строке.

 



Поделиться:


Последнее изменение этой страницы: 2016-07-16; просмотров: 1221; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 34.206.64.143 (0.195 с.)