【问题标题】:Why putting pointer* for Strings not for Integers in Objective-C为什么在 Objective-C 中将指针 * 用于字符串而不用于整数
【发布时间】:2020-12-05 10:14:14
【问题描述】:

我先学了 Swift,也许这应该是在学习 Obj-C 之后,所以现在我尝试完全理解 Obj-C,我遇到了以下问题:

Obj-C 变量

NSInteger score = 556;
NSString *name = @"matt";

在 Swift 中,变量的一切都是一样的

var score = 556
var name = "matt"

为什么objective-c 用于字符串 * 虽然整数没有 * 使用,为什么会有这样的不同?是相关的运行时间吗?

你能用实际的例子给我解释一下吗?谢谢

【问题讨论】:

  • 这类似于 Swift 中值类型和引用类型的区别(注意在 Swift 中数组和字符串是值类型,但在 Objective-C 中它们是引用类型)。这是关于在堆栈或堆上分配内存。坦率地说,这是一个相当大的内存管理话题。
  • Swift 的 var score = 556 是目标的 NSNumber *score = @556;
  • @Cy-4AH 不,var score = 556 在 Objective-C 中既是 NSNumber *score = @556 又是 NSInteger score = 556
  • FWIW。一般而言,指针指向 到内存中的某个位置。该内存已分配,并且是存储感兴趣的内容的地方。所以这里对象@"matt" 被分配并存储在某个地方,name 指向那个位置。如果没有 ARC,您需要在某个阶段释放或释放它,但使用 ARC 会得到照顾。
  • 另一方面,score 不占用内存中一些特别分配的位置。诸如score 之类的变量存储在堆栈中,当它超出范围时将自动丢弃 - 没有特殊的内存管理要求。此外,score 可以直接访问 - 它的值可以按原样访问。一个对象的值不能这样访问,它必须从内存中读取或写入,这就是为什么它总是被指针引用,而诸如整数之类的东西可以直接访问,或者你可以分配一些空间和也在那里存储整数。

标签: objective-c oop


【解决方案1】:

你知道,没有指针,Objective-C 只是带有分号的 Swift。

假设您需要分配一个内存区域供您自己使用。您可以在那里存储整数、图像、对象字典或声音剪辑。指针是您对该段内存的句柄。它主要告诉您您获得的内存区域的位置,但在 Objective-C 中,它还跟踪例如引用计数,这样您就不必担心分配和释放它。所以事情很快就会变得相当复杂。

让我们假设最简单的情况。您需要一些内存来存储对象。说一个字符串,但这没关系。此外,对于所有纯粹主义者来说,这是一个一般性的讨论,并不意味着在技术上是正确的,而是为了向 OP 说明这个想法。

首先你将分配那个对象

NSString * p = [NSString alloc];

这将要求操作系统提供一个大小正确的内存区域来存储字符串,并且操作系统将响应一个指向新分配区域的指针,该地址现在存储在p 中。这只是一个内存区域,你可以用它做你想做的事。你甚至不需要在那里储存一根绳子,但如果你这样走开,东西很快就会变成梨形。

现在操作系统只是给了你一个内存区域。那个内存不是空白的,它保存了一些以前使用过的剩余位。所以你的首要任务是清除内存——初始化它。所以下一步就是初始化它

p = [p init];

在此之后,内存区域已由一些初始化程序准备,例如它被归零或加载一些默认值到内存区域以用于更复杂的类。

请注意,实际上这有点不同 - 请参阅下面的编辑。

在 C 中,您有 allocmalloccalloc 来执行此操作。其中一些将仅分配内存,而其他将分配内存并将其全部设置为 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;
}

【讨论】:

  • Pascal 也有指针,它们在那里也很重要。 C 并不比 Pascal 强大。
  • @Sulthan 是的,非常正确 - 我已经更新了我的答案。
  • 我知道“C 是带有花括号的 Pascal”对您有所帮助,但是“Objective-C 是带有分号的 Swift”并没有争议,它只是错误的原因远远超出了使用显式指针的原因。 C 和 Pascal 是在几年内创建的,反映了他们成长的计算环境。Objective-C 比 Swift 早了大约 30 年,并且从 C 派生而来,它承载了很多甚至更老的包袱; Swift 是一种更现代的语言,支持多种不同的编程风格。
  • @Caleb 谢谢 - 我知道这句话很挑衅,这就是为什么我小心解释它的原因。我在“没有指针的 Obj-C 就像有/没有 x 的 Swift 中一样寻找 x,但我同意它不像 C 和 Pascal 那样工作 - 除非可能有更好的 x。我认为这里相关的是 Swift 为您屏蔽指针而 Obj-C 没有这一事实,而不是两者之间的年龄或其他任何差异。当时我遇到的原始陈述也是 C 之间的差异和它突出显示的 Pascal。
【解决方案2】:

如果您了解 Swift 中值类型和引用类型之间的区别,那么在 Objective-C 中使用指针只是表示引用类型。

在分配期间,被复制:

// swift
var score = 10
// obj-c
NSInteger score = 10;

reference 被复制,或者换句话说,我们不是将值本身分配,而是将 指针 分配给该值:

// swift
class MyObject {}

var object = MyObject();

// obj-c
@interface MyObject: NSObject
@end

MyObject *object = [[MyObject alloc] init];

还要注意,在 Objective-C 字符串 (NSString) 中,数组 (NSArray) 和类似的都是引用类型。即使是值类型(NSInteger)的数值类型也有引用类型包装器(例如NSNumber),否则您无法将它们放入数组中。甚至null 值也有对应的引用类型NSNull

当然,即使是值类型,你也可以使用指针:

NSInteger score = 10;
NSInteger *scorePointer = &score; // assign pointer to score
*scorePointer = 20; // change value of score through the pointer

NSLog(@"%@", @(score)); // 20

同样,你也可以拥有指向其他指针的指针,这在很多情况下都很有用(例如 inout 方法参数)。

【讨论】:

  • 这对来自 Swift 的人来说是一个很好的解释,但恐怕它给人的印象是 Obj-C 使用指针来实现引用类型,而这并不是真的。 Objective-C 使用指针是因为它实现了它的对象模型,并且它从 C 继承了非对象类型的值语义。Swift 的设计者认识到这两种值都是有用的,并在他们的新语言中明确区分.
  • @Caleb 我只是试图将解释简化到给定的理解水平:)
猜你喜欢
  • 2016-04-01
  • 2012-03-11
  • 2020-10-08
  • 1970-01-01
  • 2018-03-26
  • 1970-01-01
  • 1970-01-01
  • 2019-03-06
  • 1970-01-01
相关资源
最近更新 更多