你知道,没有指针,Objective-C 只是带有分号的 Swift。
假设您需要分配一个内存区域供您自己使用。您可以在那里存储整数、图像、对象字典或声音剪辑。指针是您对该段内存的句柄。它主要告诉您您获得的内存区域的位置,但在 Objective-C 中,它还跟踪例如引用计数,这样您就不必担心分配和释放它。所以事情很快就会变得相当复杂。
让我们假设最简单的情况。您需要一些内存来存储对象。说一个字符串,但这没关系。此外,对于所有纯粹主义者来说,这是一个一般性的讨论,并不意味着在技术上是正确的,而是为了向 OP 说明这个想法。
首先你将分配那个对象
NSString * p = [NSString alloc];
这将要求操作系统提供一个大小正确的内存区域来存储字符串,并且操作系统将响应一个指向新分配区域的指针,该地址现在存储在p 中。这只是一个内存区域,你可以用它做你想做的事。你甚至不需要在那里储存一根绳子,但如果你这样走开,东西很快就会变成梨形。
现在操作系统只是给了你一个内存区域。那个内存不是空白的,它保存了一些以前使用过的剩余位。所以你的首要任务是清除内存——初始化它。所以下一步就是初始化它
p = [p init];
在此之后,内存区域已由一些初始化程序准备,例如它被归零或加载一些默认值到内存区域以用于更复杂的类。
请注意,实际上这有点不同 - 请参阅下面的编辑。
在 C 中,您有 alloc 和 malloc 和 calloc 来执行此操作。其中一些将仅分配内存,而其他将分配内存并将其全部设置为 0 - 清除所有位。在 Objective-C 中,您通常将其作为一个步骤来完成
p = [[NSString alloc] init];
由于操作系统分配了一个内存区域,您需要在完成释放它时告诉它,例如
[p release];
或者在 C 语言中你会在完成后使用 free。
现在这只是学术性的,因为 ARC 会自动为您执行此操作,方法是为对象保留一个内部引用计数器以确定它何时超出范围然后释放它,但我离题了。现在让我们假设您不必担心将内存块释放回操作系统,ARC 会为您处理。
现在让我们假设一个更复杂的类,比如说
p = [[Invoice alloc] init];
然后当你例如做
p.items = 100;
编译器会知道它需要将100 的整数值复制到该内存块内的正确位置,它会为您这样做。这个主题有很多变体,但基本上,当您为 ivars 分配值时,这些值会被复制到分配的内存区域中,当您读取它们时,它们也会从正确的位置复制回局部变量中。
另一方面,如果你给一个简单的 int 变量赋值
int items = 100;
这不会复制到您预先分配的区域。相反,消息或函数有一个特殊的本地工作区,称为堆栈,在其中创建这些变量,然后动态丢弃。因此上面的语句会将值100 加载到堆栈中并记录位置,以便稍后如果您引用items,它将知道在哪里找到并读取或写入堆栈中的值。
同样
Invoice * p = [[Invoice alloc] init];
被加载到堆栈上,但这里指针的值或内存地址被加载到堆栈上,而不是它指向的内容。它是一个指针这一事实向您表明它指向其他地方的某个感兴趣区域。
如果以后你这样做
Invoice * q = p;
您将 p 的相同指针值加载(在堆栈上)到 q 中,现在 p 和 q 都指向存储实际对象的同一内存区域。因此,指针在可以解释为指向内存地址的指针值与存储在该地址的数据之间添加了这一层分离。这可以继续,例如你可以并且确实得到指向指针的指针等等。
由于你不能自己在动态栈上分配内存,所以你不能在栈上存储对象,但是你可以在那里存储指向对象的指针。
当您直接访问内存时,指针会给您带来不可思议的力量。你可以例如编写一个通用函数来分配内存或将内存写入磁盘或清除内存或通过网络传输一块内存或将一块内存加载到屏幕区域。现代编译器为我们处理了这些事情,所以听起来没什么大不了的,但强大的是您对所有对象使用相同的例程。如果您的数组存储指针,那么您可以在数组中存储任何内容作为另一个示例。
指针的寿命也比堆栈长。你放在堆栈上的东西,例如
int items = 100;
只有在你在函数内部时才能生存。当函数返回时堆栈被丢弃,因此您需要一个专门分配的内存区域以超越函数甚至应用程序的方式进行通信。
这里有一个更深层次的例子,只使用整数指针,而不是 Objective-C 对象的指针,来进一步说明堆栈上的指针和值之间的区别。
在 C 中你会这样做
int * p = malloc ( 10 * sizeof( int ) );
这将分配足够的空间来存储 10 个整数并返回指向p 中分配空间的指针。 p 现在在堆栈中,包含刚刚返回的内存区域的地址。
如果你这样做了,下一步
int q = 123;
值 123 存储在堆栈中并在名字对象 q 下,当您使用 q 时,编译器会将其转换为堆栈上的特定位置,例如给它赋值。
接下来,如果你这样做了
* p = q;
* ( p + 1 ) = q / 2;
这会将q (123) 的值从堆栈加载到p 指向的内存区域中,该区域指向10 个整数中的第一个。接下来计算q / 2 并将该值存储到下一个整数的位置,即第一个整数之后的1,由p 指向并由* ( p + 1 ) 引用。编译器可以很容易地做到这一点,因为它知道整数的大小,所以它知道如何翻译
* ( p + 1 )
进入正确的区域,该区域指向距离基数 p 偏移 1 的整数。
这是另一个关于指针的主题,它允许非常高效的指针算法,您可以使用它来遍历内存区域。另外,这就是为什么sizeof 在 C 中是一个如此重要的关键字,因为它与语言通过指针处理内存的方式直接相关。
在 C 中,完成后,您需要将该区域释放回操作系统,以便您这样做
free( p );
完成后。
这看起来很简单,但实际上很难跟踪所有已分配的内存区域。很容易发生您不发布或将数据加载到已发布的区域。前者会泄漏内存,而后者可能会产生灾难性的后果。
这就是为什么 Objective-C 的 ARC 代表了语言发展中的一大步,因为它会为您解决这个问题。在 ARC 世界中,内存的分配和释放(指针中的一个大主题)不再那么重要或可见 - 因此它还可以屏蔽用户对指针的一些理解。
在例如Swift 这种分配和释放完全在幕后进行,您不必真正担心直接分配或释放内存。这使得很难理解指向内存和被指向的内存之间的区别,这是指针的核心。
你知道,没有指针,Objective-C 只是带有分号的 Swift。
我以这个颇有争议的说法开始和结束。
为什么?
当我还是一名为指针概念而苦苦挣扎的程序员时,我曾想过要完全放弃它。这是在对象之前,您可以(尝试)以这种方式在堆栈和侧步指针上做所有您需要做的事情。
我刚从 Pascal (那里你不经常使用指针)和 C 中挣扎,尤其是指针,然后在某个地方读到,没有指针,C 只是带有花括号的 Pascal!我知道 C 比 Pascal 更强大,但要掌握它,我必须掌握指针。
注意 - 正如 Sulthan 所提到的,Pascal 也有指针,如果一个比另一个更强大,这是非常值得商榷的。
这里有一个很好的示例来说明这些概念,以及如何使用指针遍历内存。
int main(int argc, const char * argv[]) {
@autoreleasepool
{
// insert code here...
int a = 10; // a loaded on stack
int * b = & a; // b now points to a
NSLog ( @"a pointer %p value %d", & a, a );
NSLog ( @"b value %p points to %d", b, * b );
int * p = malloc ( 10 * sizeof ( int ) );
int * q = p;
// Show p on the stack and * p allocated somewhere
// Note the values, & p agrees with & a of earlier e.g.
NSLog ( @"p address %p on stack but points to %p", & p, p );
for ( int i = 0; i < 10; i ++ )
{
* q = i; // loads value i into area pointed to by q
NSLog ( @"i now %d", i );
// Note the huge difference between addresses on the stack and addresses alloc'd
NSLog ( @"\tp %p p + i %p value pointed to is %d", p, p + i, * ( p + i ) );
NSLog ( @"\tq %p value pointed to is %d", q, * q );
q ++; // Increment pointer, not value, now points to next int
}
// Of course ...
free ( p );
}
return 0;
}
编辑 Objective-C 分配
我在文中提到内存需要在 Objective-C 分配之后初始化。我现在找不到任何文档,但我很确定 Objective-C 的 alloc,例如像
NSString * s = [NSString alloc];
实际上会两者分配和清除或归零内存。 init 并不是真正的归零,而是让类正确初始化其成员。
但是,既然我们正在讨论指针,并且您正在寻找编程示例,那么有什么比编写一段代码来测试它更好的了。
下面的代码将创建两个您选择的 Objective-C 类。一个只会被分配,一个会被分配和初始化。然后它使用指针逐字节比较这两个类,看看是否有区别。另一个使用指针的好例子。
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
// The object we will test
#define TEST_OBJECT NSFileManager
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Allocate some complex class
TEST_OBJECT * obja = [TEST_OBJECT alloc];
TEST_OBJECT * obji = [[TEST_OBJECT alloc] init];
// Size of this class
unsigned long n = class_getInstanceSize ( TEST_OBJECT.class );
NSLog ( @"Object size is %lu", n );
NSLog ( @"\tLocation in memory %p", obja );
NSLog ( @"\t %p", obji );
// Get pointers to the two instances
void * p = ( __bridge void * ) obja;
void * q = ( __bridge void * ) obji;
// Compare the two
int cmp = memcmp( p, q, n );
NSLog ( @"Result is %d - %@", cmp, cmp ? @"different" : @"same" );
// Dump the actual bytes side by side
for ( int i = 0; i < n; i ++ )
{
int l = ( unsigned ) ( * ( ( unsigned char * ) p + i ) ); // Left
int r = ( unsigned ) ( * ( ( unsigned char * ) q + i ) ); // Right
NSLog ( @"Byte %3d | %3d | %3d | %@", i, l, r, l != r ? @"***" : @"" );
}
}
return 0;
}