从C++到Objective-C:面向实用程序员的快速指南






4.75/5 (33投票s)
Objective-C初学者的教程。
引言
当我开始为 iOS 编程时,我意识到作为一个 C++ 开发者,我将花费更多时间来弄清楚 Objective-C 的怪异之处。这是一份快速指南,帮助 C++ 专家快速掌握 Apple 的 iOS 语言。
请注意,这绝不是一份完整的指南,但它能让你避免阅读一本 100 页的手册。此外,我知道你无论如何都喜欢我的写作风格。
背景
需要 C++ 技能,我将比较 C++ 的内容和 Objective-C 的内容。此外,COM 编程很有用,因为 Objective-C 对象与 IUnknown 相似,所以基本的 COM 知识会很有帮助(但不是必需的)。
Objective C++ 是 C++ 和 Objective-C 的结合。你可以使用任何 C++ 的东西并将其与任何 Objective-C 的东西混合,只需记住将文件名从 .m 重命名为 .mm。
塔拉姆-塔拉姆!
我们立即开始教程。首先我给出 Objective-C 的内容,然后是 C++ 的等价物。
成员函数
// Objective-C - (int) foo : (int) a : (char) b {} + (int) foo : (int) a : (char) b {} // C++ int foo(int a,char b) {} static int foo(int a,char b) {} // Objective-C - (void) foo2 val1:(int) a; // named argument // call [obj foo2 val1:5]; // merely helper: You remember that 5 is assigned to param name val1.
- 表示一个普通成员函数(可通过对象实例访问),而 + 表示一个静态成员函数,无需实例即可访问。当然,与 C++ 一样,静态成员不能访问实例变量。
此外,Objective-C 函数可以有命名参数,这使得参数值的传递更加清晰。理论上,命名参数还允许程序员以任何顺序传递参数,然而 Objective-C 要求与声明中相同的顺序。
通过指针或静态成员调用成员
// Objective-C NSObject* ptr = ...; // some pointer [ptr foo:5:3]; // call foo member with arguments 5 and 3 [NSObject staticfoo:5:3]; // call static function of NSOBject with arguments 4 and 3 // C++ CPPObject* ptr = ...; // some pointer ptr->foo(5,3); CPPObject::staticfoo(5,3);
Objective-C 使用 [] 来调用成员函数,并以 : 分隔参数。与 C++ 不同,Objective-C 中指针为 nil 是完全可以接受的,在这种情况下,“调用”会被忽略(而在 C++ 中,这会引发指针违例异常)。这使得可以消除对 nil 对象的检查。
Protocols vs Interfaces
// Objective-C @protocol foo - (void) somefunction; @end @interface c1 : NSObject<foo> @end @implementation c1 - (void) somefunction { ... } @end // C++ class foo { virtual void somefunction() = 0; }; class c1 : public NSObject, public foo { void somefunction() { ... } }
Protocol = 抽象类。Objective-C 和 C++ 之间的区别在于,在 Objective-C 中,函数不强制要求实现。你可以强制实现可选方法,但这仅仅是对编译器的提示,而不是编译的要求。
检查方法是否已实现
// Objective-C NSObject* ptr = ...; // some pointer [ptr somefunction:5:3]; // NSObject does not need to implement somefunction for this to compile. If it does not, an exception is raised. // C++ CPPObject* ptr = ...; // some pointer ptr->somefunction(5,3); // CPPObject must implement somefunction() or the program does not compile.
Objective-C 成员函数是“消息”(Smalltalk),当在 Objective-C 中我们说接收者(指针)响应某个选择器时,这意味着它实现了我们试图调用的虚拟函数。当存在接口时,C++ 对象必须实现其所有成员函数。在 Objective-C 中则不是必需的,所以我们可以向一个不一定实现了它的对象发送“消息”(从而引发异常)。
// Objective-C NSObject* ptr = ...; // some pointer if ([ptr respondsToSelector:@selector(somefunction::)] [ptr somefunction:5:3];
现在我们确定接收者响应了选择器,所以我们可以调用它。在 C++ 中不需要这种检查,因为实现必须始终“响应选择器”,否则源代码就不会编译。请注意,我们必须知道选择器接收多少个参数(因此 @selector : 中有两个 ::)。
下转型
// Objective-C NSObject* ptr = ...; // some pointer if ([ptr isKindOfClass:[foo class]] [ptr somefunction:5:3]; // C++ CPPObject* ptr = ...; // some pointer foo* f = dynamic_cast<foo*>(ptr); if (f) f->somefunction(5,3);
这就像 C++ 中的下转型一样,只是使用了 NSObject(所有 Objective-C 类的基类)的“isKindOfClass”辅助方法。
符合协议?
// Objective-C NSObject* ptr = ...; // some pointer if ([ptr conformsToProtocol:@protocol(foo)] [ptr somefunction:5:3]; // C++ CPPObject* ptr = ...; // some pointer that also inherits from foo foo* f = ptr; // or the compiler warns us that ptr isn't compatible with foo. f->somefunction(5,3);
现在我们检查接收者是否符合协议(或者,用 C++ 的话说,实现了接口),这样我们就可以发送该协议包含的消息。嘿,这在很大程度上类似于 Java 类和接口,而在 C++ 中,完全实现的类和“接口”之间没有技术区别。
void* 或 id 或 SEL?
// Objective-C id ptr = ...; // some pointer if ([ptr conformsToProtocol:@protocol(foo)] [ptr somefunction:5:3]; SEL s = @selector(foo:); // a pointer to a function foo that takes 1 parameter // C++ void* ptr = ...; // some pointer foo* f = dynamic_cast<foo*>(ptr); if (f) f->somefunction(5,3);
id 是 Objective-C 类中通用的 void*-like 类型。你必须使用 id 而不是 void*,因为 id 可以是 ARC(稍后将详细介绍)管理的指针,因此编译器需要区分原始指针类型和 Objective-C 指针。SEL 是选择器的通用类型(C++ 的函数指针),通常通过关键字 @selector 结合函数名和若干个 :::::(取决于可以传递的参数数量)来创建选择器。选择器实际上是一个字符串,它在运行时绑定到方法标识符。
类声明、方法、数据、继承。
// Objective C @class f2; // forward declaration @interface f1 : NSOBject // Objective-C supports only public and single inheritance { int test; // default = protected @public int a; int b; f2* f; } - (void) foo; @end @implementation f1 - (void) foo { a = 5; // ok self->a = 5; // ok super.foo(); // parent call } @end // C++ class f1 : public CPPObject { int test; // default = private public: class f2* f; // forward declaration int a; int b; void foo(); } void f1 :: foo() { a = 5; // ok this->a = 5; // ok CPPOBject::foo(); // parent call }
Objective-C 中的实现作用域在 @implementation/@end 标签内(而在 C++ 中,我们可以使用 :: 作用域运算符在任何地方实现它)。它使用 @class 关键字进行前向声明。Objective-C 默认具有私有保护,但仅限于数据成员(方法必须是公共的)。Objective-C 使用 self 而不是 this,并通过 super 关键字调用其父类。
构造函数和析构函数
// Objective-C NSObject* s = [NSObject alloc] init]; // can return nil if construction failed [s retain]; // Increment the ref count // C++ CPPObject* ptr = new CPPObject(); // can throw ptr->AddRef(); // Objective-C NSObject* s = [NSObject alloc] initwitharg:4]; [s release]; // C++ CPPOBject* ptr = new CPPOBject(4); ptr->Release();
Objective-C 中的内存分配是通过调用静态成员函数 alloc 来完成的,该函数所有对象(NSObject 的子类)都有。self 在 Objective-C 中是可赋值的,并且在构造失败时设置为 nil(而在 C++ 中,会抛出异常)。内存分配后的实际构造函数调用是 Objective-C 中的一个普通成员函数,默认情况下是 init。
Objective-C 使用与 COM 相同的引用计数方法,并使用 retain 和 release(等于 IUnknown 的 AddRef() 和 Release() 方法)。当引用计数达到零时,dealloc(析构函数)会被自动调用,然后对象从内存中移除。
多线程
// Objective C @interface f1 : NSOBject // Objective-C supports only public and single inheritance { } - (void) foo; - (void) threadfunc :(NSInteger*) param; - (void) mt; @end @implementation f1 - (void) threadfunc : (NSInteger*) param { [self performSelectorOnMainThread: @selector(mt)]; } - (void) mt { } - (void) foo { [self performSelectorInBackground: @selector(thradfunc:) withObject:1 waitUntilDone:false]; <div>} @end
Objective-C 具有基于 NSObject 的内置函数,可以在另一个线程、主线程中执行选择器(== 调用成员)、等待调用等。有关更多信息,请参阅 NSObject。
内存和 ARC
// Objective-C @interface f1 : NSObject { } @property (weak) NSAnotherObject* f2; // This will be automatically set to nil when no other strong references exist. @end - (void) foo { NSObject* s = [NSObject alloc] init]; // can return nil if construction failed // use s // end. Hooraah! Compiler will automatically call [s release] for us! }
这是你将忘记你良好的 C++ 习惯的地方。好的,Objective-C 曾经有过垃圾回收,我们 C++ 程序员讨厌它,因为它很慢,让我们想起 Java。但是 ARC(自动引用计数)是一个编译时功能,它告诉编译器“这是我的对象:请找出它们何时需要被销毁”。使用 ARC,你不需要向你的对象发送 retain/release 消息;编译器会自动完成。
为了帮助编译器确定一个对象的保留期限,你还可以使用弱引用。默认情况下,所有变量都是强引用(== 只要它们存在,对象就存在)。你还可以使用弱引用,只要没有其他强引用存在,它就会失去其值。这对于从 XCode Builder Interface(如 RC 编辑器)获取值的类成员很有用,当类被销毁时,这些成员也会失去其值。
你可以用 C++ 的 shared_ptr 和 weak_ptr 的概念来理解这一切。
字符串
// Objective-C NSString* s1 = @"hello"; NSString* s2 = [NSString stringWithUTF8String:"A C String"]; sprintf(buff,"%s hello to %@","there",s2); const char* s3 = [s2 UTF8String];
NSString 是 Objective-C 字符串的不可变表示。你可以使用它的一个静态方法或带 @ 前缀的字符串字面量来创建它。你还可以使用 %@ 将 NSString 表示为 printf 系列函数。
数组
// Objective-C NSArray* a1 = [NSArray alloc] initWithObjects: @"hello",@"there",nil]; NSString* first = [a1 objectAtIndex:0];
NSArray 和 NSMutableArray 是 Objective-C 中处理数组的两个类(区别在于 NSArray 的元素必须在构造时通过其构造函数放入,而 NSMutableArray 可以在之后修改)。构造函数具有典型的 printf 签名形式,你必须传递 nil 来“结束项”。NSArray 和 NSMutableArray 也有 sort/search/insert 函数,在第一种情况下返回一个新的 NSArray,而在 NSMutableArray 的情况下,则修改现有对象。
分类
// C++ class MyString : public string { public: void printmystring() { printf("%s",c_str()); } }; // Objective-C @interface MyString (NSString) - (void) printmystring; @end @implementation MyString (NSString) - (void) printmystring { printf("%@",self); } @end // C++ MyString s1 = "hi"; s1.printmystring(); // ok string s2 = "hello"; s2.printmystring(); // error, we must change s2 from string to MyString // Objective-C NSString* s2 = @"hello"; [s2 printmystring]; // valid. We extended NSString without changing types.
C++ 依赖于继承来扩展已知类。这可能很麻烦,因为扩展类的所有用户都必须使用另一个类型(在这种情况下,是 MyString 而不是 string)。Objective-C 允许在同一类型内扩展已知类,方法是使用 Categories。这允许所有查看扩展 .h 文件(通常是类似 NSString+MyString.h 的文件)的源文件能够立即调用新的成员函数,而无需将 NSString 类型更改为 MyString。
Blocks and Lambdas
// Objective-C // member function -(void)addButtonWithTitle:(NSString*)title block:(void(^)(AlertView*, NSInteger))block; // call [object addButtonWithTitle:@"hello" block:[^(AlertView* a, NSInteger i){/*DO SOMETHING*/}];
Block 是 Objective-C 模拟 lambda 函数的方式。请查看 Apple 的文档以及 AlertView(使用 blocks 的 UIAlertView)的示例以获取更多信息。
使用 Objective-C 和 ARC 的 C++ 开发人员的重要提示
// C++ class A { public: NSObject* s; A(); }; A :: A() { s = 0; // Might boom, usually in release mode! }
你已经知道,当所有客户给出 1/5 的评分,因为你的 bugware 在发布模式下崩溃但在调试时没有崩溃时的痛苦。没有用户能理解程序员,不是吗?
让我们看看这里发生了什么。s = 0 这行代码将 0 赋给变量,因此,该变量之前存储的任何内容都必须首先被释放,因此编译器在赋值前执行 [s release]。如果 s 已经是 0,就像在调试构建中一样,什么也不会发生;对 nil 使用 [s release] 是完全有效的。然而,在发布构建中,s 可能是一个悬空指针,因此在其“初始化”为 0 之前,它可能包含任何值。
在 C++ 中,这不是问题,因为没有 ARC。然而在 Objective-C 中,编译器无法判断这是一个“赋值”还是“初始化”(如果是后者,它将不发送 release 消息)。
这是正确的方法
// C++ class A { public: NSObject* s; A(); }; A :: A() :s(0) // now the compiler knows for sure it's an initialization, so no [s release] { }
现在编译器不会尝试 [s release],因为它知道这是对象的第一次初始化。请小心!
从 Objective-C 对象到 C++ 类型的强制类型转换
// Objective-C NSObject* a = ...; void* b = (__bridge void*)a; // you must use __bridge between Objective-C types and C types void* c = (__bridge_retained void*)a; // Now a has +1 retain count and you must release the object later on NSObject* d = (__bridge_transfer NSObject*)c; // Now ARC gets "ownership" of the object c, converting it to an ARC-managed NSObject.
我可以分析所有这些,但我的建议很简单。不要混用 ARC 类型和非 ARC 类型。如果你必须转换某个 Objective-C 对象,请使用 id 而不是 void*。否则,你将遇到严重的内存问题。
Objective-C 有而 C++ 没有的
- 分类
- 基于 NSObject 的操作
- YES 和 NO(等于 true 和 false)
- NIL 和 nil(等于 0)
- 命名函数参数
- self(等于 this),但它可以在构造函数中更改
C++ 有而 Objective-C 没有的
- 静态对象。Objective-C 中的对象不能静态实例化或在栈上实例化。只能是指针。
- 多重继承
- 命名空间
- 模板
- 运算符重载
- STL 和算法;
- 方法可以是 protected 或 private(在 Obj-C 中,只有 public)
- const/mutable 项
- 友元方法
- 参考文献
- 匿名函数签名(没有变量名)
延伸阅读
从 C++ 到 Objective-C 的指南,在此。
历史
- 2014/10/05:首次发布。