Статья расчитана на новичков в Objective-C и рассказывает об одном способе выстрелить себе в ногу. Мы попытаемся создать два различных объекта NSString с одинаковым текстом, исследуем реакцию на это различных компиляторов, а также узнаем, при каких условиях NSLog(@"%@", @«123456789») выведет совсем не «123456789».
Объекты NSString и указатели
Как вы думаете, что выведет следующий код?
#import "Foundation/Foundation.h"
int main(){
@autoreleasepool {
NSString *a = @"123456789";
NSString *b = a;
NSLog(@"%p %p", a, b);
}
return 0;
}
Естественно, указатели будут равны («объекты присваиваются по ссылке»), так что NSLog() напечатает два одинаковых адреса памяти. Никакой магии:
2015-01-30 14:39:27.662 1-nsstring[13574] 0x602ea0 0x602ea0
Здесь и далее адреса объектов приводятся в качестве примера; при попытке воспроизведения фактические значения, разумеется, будут другими.
Давайте попробуем добиться того, чтобы у нас было два различных NSString с одинаковым текстом. В случае других стандартных классов, например, NSArray, мы могли бы написать так:
#import "Foundation/Foundation.h"
int main(){
@autoreleasepool {
NSArray *a = @[@"123456789"];
NSArray *b = @[@"123456789"];
NSLog(@"%p %p", a, b);
}
return 0;
}
Поскольку мы инициализировали NSArray по отдельности, то они были помещены в различные участки памяти и в консоли высветятся два разных адреса:
2015-01-30 14:40:45.799 2-nsarray[13634] 0xa9e1b8 0xaa34e8
Однако применение такого же подхода к NSString не приведет к желаемому эффекту:
#import "Foundation/Foundation.h"
int main(){
@autoreleasepool {
NSString *a = @"123456789";
NSString *b = @"123456789";
NSLog(@"%p %p", a, b);
}
return 0;
}
2015-01-30 14:41:41.898 3-nsstring[13678] 0x602ea0 0x602ea0
Как видим, несмотря на раздельную инициализацию, оба указателя по-прежнему ссылаются на одну и ту же область памяти.
Использование stringWithString
Немного покопавшись в NSString, мы обнаруживаем метод stringWithString, который «returns a string created by copying the characters from another given string». Так это же то, что нам надо! Попробуем следующий код:
#import "Foundation/Foundation.h"
int main(){
@autoreleasepool {
NSString *a = @"123456789";
NSString *b = [NSString stringWithString:@"123456789"];
NSString *с = [NSString stringWithString:b];
NSLog(@"%p %p %p", a, b, с);
}
return 0;
}
Оказывается, что вывод этой программы зависит от используемой версии компилятора. Так clang под Ubuntu на LLVM 3.4 действительно создаст три различных объекта, расположенных в различных ячейках памяти. Но компиляция указанного кода в Xcode при помощи clang под Mac на LLVM 3.5 сгенерирует всего один объект и три пойнтера на него:
2015-01-30 17:59:02.206 4-nsstring[670:21855] 0x100001048 0x100001048 0x100001048
Сеанс магии с разоблачением
Вышеуказанные странности объясняются попытками компилятора оптимизировать строковые ресурсы. Встречая в исходном коде строковые объекты с одинаковым содержанием, он для экономии затрат на хранение и сравнение создает их только один раз. Эта оптимизация выполняется также и на этапе линковки: даже если строки с одинаковым текстом находятся в различных модулях, скорее всего они будут созданы только один раз.
Поскольку тип NSString является неизменяемым (для изменяемых строк используется NSMutableString), то такая оптимизация является безопасной. До тех пор, пока мы манипулируем со строками только методами класса NSString.
Компилятор, впрочем, не всемогущ. Один из самых простых способов запутать его и действительно создать два различных NSString c одинаковым текстом — таков:
#import "Foundation/Foundation.h"
int main(){
@autoreleasepool {
NSString *a = @"123456789";
NSString *b = [NSString stringWithFormat:@"%@", a];
NSLog(@"%p %p", a, b);
}
return 0;
}
GCC
Аналогичную оптимизацию строковых констант выполняет и gcc при компиляции кода на Си. Например,
#include <stdio.h>
void main(){
char *a = "123456789";
char *b = "123456789";
printf("%p %pn", a, b);
}
выведет 0x4005f4 0x4005f4.
Однако есть существенное различие c clang: gcc размещает такие строковые константы в read-only сегменте — попытки изменить их в рантайме (например, a[0]='0') приведут к segmentation fault. Чтобы разместить строки в heap, где они могут быть изменены, нужно заменить char *a на char a[], однако в таком случае gcc не будет применять оптимизацию. Следующий код создаст уже две различных строки:
#include <stdio.h>
void main(){
char a[] = "123456789";
char b[] = "123456789";
printf("%p %pn", a, b);
}
0x7fff17ed0020 0x7fff17ed0030
Стрельба в ногу
Итак, мы знаем, что встречая в исходном коде одинаковые строковые объекты, компилятор оптимизирует их и создает NSString только один раз. При этом он создает ее в heap, где она может быть изменена при помощи ручных манипуляций с указателем. (В plain C, как обсуждалось выше, такое невозможно.)
Отгадайте, что печатает следующий код?
#import <Foundation/Foundation.h>
void bad(){
NSString* a = @"123456789";
char* aa = (__bridge void *)(a);
aa[8] = 92;
}
int main(){
@autoreleasepool {
bad();
NSLog(@"%@", @"123456789");
}
return 0;
}
В зависимости от компилятора результат может быть разным: мой Xcode под Маком печатает набор кракозябр «㈱㐳㘵㠷9䀥», а clang в Убунту выводит фрагмент из служебной информации «red:pars». В любом случае, это никак не ожидаемое «123456789». Эксперименты с другими значениями aa[8], а также aa[16], предлагаю читателю проделать самостоятельно.
Хуже всего то, что функция bad() из последнего примера может находиться за хедером, например, в подключаемой библиотеке другого автора, который по своим нуждам изменял свой личный (как ему казалось) NSString. Умный компилятор все равно найдет совпадающие строковые константы и замкнет их на один пойнтер, после чего порча переменной внутри bad() повлечет превращение строки в контексте main() в иероглифы.
Автор: Bodigrim