65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (33投票s)

2014年5月9日

CPOL

8分钟阅读

viewsIcon

82055

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 相同的引用计数方法,并使用 retainrelease(等于 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];
  

NSArrayNSMutableArray 是 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:首次发布。
© . All rights reserved.