А. Григорьев - О чём не пишут в книгах по Delphi
Начнем со сравнения двух строк типа PChar (листинг. 3.19).
Листинг 3.19. Сравнение строк типа PCharprocedure TForm1.Button1Click(Sender: TObject);
var
P1, P2: PChar;
begin
P1 := StrNew('Test');
P2 := StrNew('Test');
if P1 = P2 then Label1.Caption := 'Равно';
else Label1.Caption := 'Не равно';
StrDispose(P1);
StrDispose(P2);
end;
В данном примере мы увидим надпись Не равно. Это происходит потому, что в этом случае сравниваются указатели, а не содержимое строк, а указатели здесь будут разные. Попытка сравнить строки с помощью оператора сравнения — весьма распространенная ошибка у начинающих. Для сравнения таких строк следует применять специальную функцию — StrComp. Следующий пример, на первый взгляд, в плане сравнения ничем не отличается от только что рассмотренного (листинг 3.20).
Листинг 3.20. Сравнение строк типа PChar, заданных одинаковыми литераламиprocedure TForm1.Button2Click(Sender: TObject);
var
P1, P2: PChar;
begin
P1 := 'Test';
P2 := 'Test';
if P1 = P2 then Label1.Caption := 'Равно'
else Label1.Caption := 'Не равно';
end;
Разница только в том, что строки хранятся не в динамической памяти, a в сегменте кода. Тем не менее на экране появится надпись Равно. Это происходит, разумеется, не потому, что сравнивается содержимое строк, а потому, что в данном случае два указателя оказываются равными. Компилятор поступает достаточно интеллектуально: видя, что в разных местах указаны литералы с одинаковым значением, он выделяет для такого литерала место только один раз, а потом помещает в разные указатели один адрес. Поэтому сравнение дает правильный (с интуитивной точки зрения) результат.
Такое положение дел только запутывает ситуацию со сравнением PChar: написав подобный тест, человек может сделать вывод, что строки PChar сравниваются не по указателю, а по значению, и действовать под руководством этого заблуждения.
Раз уж мы столкнулись с такой особенностью компилятора, немного отвлечемся от сравнения строк и "копнем" этот вопрос немного глубже. В частности, выясним, распространяется ли "интеллект" компилятора на литералы типа AnsiString (листинг 3.21).
Листинг 3.21. Сравнение переменных типа AnsiString как указателейprocedure TForm1.Button3Click(Sender: TObject);
var
S1, S2: string;
begin
S1 := 'Test';
S2 := 'Test';
if Pointer(S1) = Pointer(S2) then Label1.Caption := 'Равно'
else Label1.Caption := 'He равно';
end;
В этом примере на экран будет выведено Равно. Как мы видим, указатели равны, т.е. и здесь компилятор проявил "интеллект".
Рассмотрим чуть более сложный случай (листинг 3.22).
Листинг 3.22. Сравнение переменных AnsiString и PChar как указателейprocedure TForm1.Button4Click(Sender: TObject);
var
P: PChar;
S: string;
var
S := 'Test';
P := 'Test';
if Pointer(S) = P then Label1.Caption := 'Равно'
else Label1.Caption := 'He равно';
end;
В этом случае указатели окажутся не равны. Действительно, с формальной точки зрения литерал типа AnsiString отличается от литерала типа PChar: в нем есть счетчик ссылок (равный -1) и длина. Однако если забыть с существовании этой добавки, эти два литерала одинаковы: четыре значащих символа и один #0, т.е. компилятор, в принципе, мог бы обойтись одним литералом. Тем не менее на это ему "интеллекта" уже не хватило. Рассмотрим еще один пример: сравнение строк по указателям (листинг 3.23).
Листинг 3.23. Сравнение глобальных переменных типа AnsiString как указателейvar
GS1, GS2: string;
procedure TForm1.Button5Click(Sender: TObject);
begin
GS1 := 'Test';
GS2 := 'Test';
if Pointer(GS1) = Pointer(GS2) then Label1.Caption := 'Равно';
else Label1.Caption := 'Не равно';
end;
Этот пример отличается от приведенного в листинге 3.21 только тем, что теперь переменные глобальные, а не локальные. Однако этого достаточно, чтобы результат оказался другим — на экране мы увидим надпись Не равно. Для глобальных переменных компилятор всегда создаст уникальный литерал, на обнаружение одинаковых литералов ему "интеллекта" не хватает. Более того, если поставить точки останова в методах Button3Click и Button4Click, легко убедиться, что указатель, который будет помещен в переменную S в методе Button4Click, отличается от того, который будет помещен в переменные S1 и S2 в методе Button3Click, хотя литералы в обоих случаях одинаковые. Компилятор умеет обнаруживать равенство литералов типа AnsiString только в пределах одной функции.
Теперь посмотрим, что будет с глобальными переменными типа PChar при присваивании им одинакового литерала (листинг 3.24).
Листинг 3.24. Сравнение глобальных переменных типа PCharvar
GP1, GP2: PChar;
procedure TForm1.Button6Click(Sender: TObject);
begin
GP1 := 'Test';
GP2 := 'Test';
if GP1 = GP2 then Label1.Caption := 'Равно'
else Label1.Caption := 'He равно';
end;
После выполнения этого кода мы увидим надпись Равно, т.е. здесь компилятор смог обнаружить равенство литералов, несмотря на то, что переменные глобальные. Однако переменные типа PChar, которым присваиваются одинаковые литералы в разных функциях, как и переменные типа AnsiString, получат разные значения.
Но вернемся к сравнению строк. Как мы знаем, строки AnsiString сравниваются по значению, а PChar — по указателю. А что будет, если сравнить AnsiString с PChar? Ответ на этот вопрос даёт листинг 3.25.
Листинг 3.25. Сравнение переменных типа AnsiString и PCharprocedure TForm1.Button7Click(Sender: TObject);
var
P: PChar;
S: string;
begin
S := 'Test';
P := 'Тest';
it S = Р then Label1.Caption := 'Равно'
else Label1.Caption := 'Не равно';
end;
Этот код выдаст Равно. Как мы знаем из предыдущих примеров (см. листинг 3.22), значения указателей не будут равны, следовательно, производится сравнение по содержанию, т.е. именно то, что к требуется. Если исследовать код, который генерирует компилятор, то можно увидеть, что сначала неявно создается строка AnsiString, в которую копируется содержимое строки PChar, а потом сравниваются две строки AnsiString. Сравниваются, естественно, по значению.
Для строк ShortString сравнение указателей невозможно, две таких строки всегда сравниваются по значению. Правила хранения литералов и сравнения с другими типами следующие:
1. Литералы типа ShortString размещаются в сегменте кода только один раз на одну функцию, сколько бы раз они ни повторялись в ее тексте.
2. При сравнении строк ShortString и AnsiString первая сначала конвертируется в тип AnsiString, а потом выполняется сравнение.
3. При сравнении строк ShortString и PChar строка PChar конвертируется в ShortString, затем эти строки сравниваются.
Последнее правило таит в себе «подводный камень», который иллюстрируется следующим примером (листинг 3.26).
Листинг 3.26. Ошибка при сравнении переменных типа ShortString и PCharprocedure TForm1.Button8Click(Sender: TObject);
var
P: PChar;
S: ShortString
begin
P := StrAlloc(300);
FillChar(P^, 299, 'A');
P[299] := #0;
S[0] := #255;
FillChar(S[1], 255, 'A');
if S = P then Label1.Caption := 'Равно'
else Label1.Caption := 'Не равно';
StrDispose(Р);
end;
Здесь формируется строка типа PChar, состоящая из 299 символов "A". Затем формируется строка ShortString, состоящая из 255 символов "А". Очевидно, что эти строки не равны, потому что имеют разную длину. Тем не менее на экране появится надпись Равно.
Происходит это вот почему: строка PChar оказывается больше, чем максимально допустимый размер строки ShortString. Поэтому при конвертировании лишние символы просто отбрасываются. Получается строка длиной 255 символов, совпадающая со строкой ShortString, с которой мы ее сравниваем. Отсюда вывод: если строка ShortString содержит 255 символов, а строка PChar — более 255 символов, и ее первые 255 символов совпадают с символами строки ShortString, операция сравнения ошибочно даст положительный результат, хотя эти строки не равны.
Избежать этой ошибки поможет либо явное сравнение длины перед сравнением строк, либо приведение одной из сравниваемых строк к типу AnsiString (второй аргумент при этом также будет приведен к этому типу). Следующий пример (листинг 3.27) дает правильный результат Не равно.