从 C++ 到 Objective-C 的快速指南

移动开发 iOS
当我开始为iOS写代码的时候,我意识到,作为一个C++开发者,我必须花费更多的时间来弄清楚Objective-C中怪异的东西。这就是一个帮助C++专家的快速指南,能够使他们快速的掌握Apple的iOS语言。

当我开始为iOS写代码的时候,我意识到,作为一个C++开发者,我必须花费更多的时间来弄清楚Objective-C中怪异的东西。这就是一个帮助C++专家的快速指南,能够使他们快速的掌握Apple的iOS语言。

请注意这绝不是一个完整的指南,但是它让你避免了阅读100页的手册。除此之外,我知道你喜欢我的写作风格。

背景

需要C++的技能,我会比较C++和Objective-C的东西。此外,COM编程也是有用的,因为Objective-C有类似于IUnkown的东西,因此基础的COM编程是有帮助的(但不是必须的)

Objective C++是C++和Objective C的组合。你也能使用任何C++的东西混合到Objective C中,但是记住重新命名你的文件从.m到.mm

铛 - 铛!

我们马上就开始我们的教程. 首先我会介绍 Objective-C 的东西,然后是C++中与它对等的东西.

成员函数

  1. // Objective-C 
  2. - (int) foo : (int) a : (char) b {} 
  3. + (int) foo : (int) a : (char) b {} 
  4.  
  5. // C++ 
  6. int foo(int a,char b) {} 
  7. static int foo(int a,char b) {} 
  8.  
  9. // Objective-C 
  10. - (void) foo2 val1:(int) a; // named argument 
  11. // <span id="22_nwp" style="width: auto; height: auto; float: none;"><a id="22_nwl" href="http://cpro.baidu.com/cpro/ui/uijs.php?adclass=0&app_id=0&c=news&cf=1001&ch=0&di=128&fv=18&is_app=0&jk=b2645dbfe197c88d&k=call&k0=call&kdi0=0&luki=1&n=10&p=baidu&q=06011078_cpr&rb=0&rs=1&seller_id=1&sid=8dc897e1bf5d64b2&ssp2=1&stid=0&t=tpclicked3_hc&tu=u1922429&u=http%3A%2F%2Fwww%2Eadmin10000%2Ecom%2Fdocument%2F4400%2Ehtml&urlid=0" target="_blank" mpid="22" style="text-decoration: none;"><span style="color:#0000ff;font-size:14px;width:auto;height:auto;float:none;">call</span></a></span> 
  12. [obj foo2 val1:5]; // merely helper: You remember that 5 is assigned to param name val1. 

- 表示的是一个一般的成员函数(通过一个对象实体访问), 而 + 则表示一个静态成员函数, 不需要使用实体就能访问. 当然,像C++, 静态成员不能访问实体变量.

此外,Objective-C函数函数可以有赋予了名称的参数,这样让什么参数获得什么值会更一目了然. 理论上,被赋予了名称的参数允许程序员按任何顺序传入参数,但是Objective-C却规定要按声明的顺序传参.

通过一个指针或者一个静态成员调用一个成员

  1. // Objective-C 
  2. NSObject* ptr = ...; // some pointer 
  3. [ptr foo:5:3]; // <span id="19_nwp" style="width: auto; height: auto; float: none;"><a id="19_nwl" href="http://cpro.baidu.com/cpro/ui/uijs.php?adclass=0&app_id=0&c=news&cf=1001&ch=0&di=128&fv=18&is_app=0&jk=b2645dbfe197c88d&k=call&k0=call&kdi0=0&luki=1&n=10&p=baidu&q=06011078_cpr&rb=0&rs=1&seller_id=1&sid=8dc897e1bf5d64b2&ssp2=1&stid=0&t=tpclicked3_hc&tu=u1922429&u=http%3A%2F%2Fwww%2Eadmin10000%2Ecom%2Fdocument%2F4400%2Ehtml&urlid=0" target="_blank" mpid="19" style="text-decoration: none;"><span style="color:#0000ff;font-size:14px;width:auto;height:auto;float:none;">call</span></a></span> foo member with arguments 5 and 3 
  4. [NSObject staticfoo:5:3]; // call static function of NSOBject with arguments 4 and 3 
  5.  
  6. // C++ 
  7. CPPObject* ptr = ...; // some pointer 
  8. ptr->foo(5,3); 
  9. CPPObject::staticfoo(5,3); 

  Objective-C 使用 [ ] 来调用成员函数并传入用:分割开的参数, 其对于ObjectiveC中ptr为nil的情况非常友好,在这种情况下“调用”将会被忽略掉(而在C++中,这种情况会抛出一个指针冲突异常 ). 这使得消除对nil对象的检查成为可能.

协议VS接口

  1. // Objective-C 
  2. @protocol foo 
  3. - (void) somefunction; 
  4. @end 
  5.  
  6. @interface c1 : NSObject<foo> 
  7.  
  8. @end 
  9.  
  10. @implementation c1 
  11. - (void) somefunction { ... } 
  12. @end 
  13.  
  14. // C++ 
  15. class foo 
  16. virtual void somefunction() = 0
  17. }; 
  18.  
  19. class c1 : public NSObject, public foo 
  20. void somefunction() { ... } 

协议= 抽象类. Objective-C 和 C++ 之间的区别在于,在 Objective-C 中, 函数并不是必须要被实现的. 你可以让一个可选的方法被动的被声明,而这仅仅只是向编译器发出暗示而已,并不是编译必须的.检查一个方法是否被实现了

  1. / Objective-C 
  2. NSObject* ptr = ...; // some pointer 
  3. [ptr somefunction:5:3]; // NSObject 不必一定为其编译而实现somefunction. 如果没有被实现的话,会引发异常. 
  4.  
  5. // C++ 
  6. CPPObject* ptr = ...; // some pointer 
  7. ptr->somefunction(5,3); // CPPObject 必须实现 somefunction() 否则程序根本就不会被编译. 

  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中是2个 ::s

向下转型

  1. // Objective-C 
  2. NSObject* ptr = ...; // some pointer 
  3. if ([ptr isKindOfClass:[foo class]] 
  4. [ptr somefunction:5:3]; 
  5.  
  6. // C++ 
  7. CPPObject* ptr = ...; // some pointer 
  8. foo* f = dynamic_cast<foo*>(ptr); 
  9. if (f) 
  10. f->somefunction(5,3); 

现在只有使用NSObject的"isKindOfClass"助手——所有Object-C类的基础,才能像在C++中那样向下转型.

符合协议?

  1. // Objective-C 
  2. NSObject* ptr = ...; // some pointer 
  3. if ([ptr conformsToProtocol:@protocol(foo)] 
  4. [ptr somefunction:5:3]; 
  5.  
  6. // C++ 
  7. CPPObject* ptr = ...; // 某个也继承自foo的指针 
  8. foo* f = ptr; // 或者<span id="15_nwp" style="width: auto; height: auto; float: none;"><a id="15_nwl" href="http://cpro.baidu.com/cpro/ui/uijs.php?adclass=0&app_id=0&c=news&cf=1001&ch=0&di=128&fv=18&is_app=0&jk=b2645dbfe197c88d&k=%B1%E0%D2%EB%C6%F7&k0=%B1%E0%D2%EB%C6%F7&kdi0=0&luki=8&n=10&p=baidu&q=06011078_cpr&rb=0&rs=1&seller_id=1&sid=8dc897e1bf5d64b2&ssp2=1&stid=0&t=tpclicked3_hc&tu=u1922429&u=http%3A%2F%2Fwww%2Eadmin10000%2Ecom%2Fdocument%2F4400%2Ehtml&urlid=0" target="_blank" mpid="15" style="text-decoration: none;"><span style="color:#0000ff;font-size:14px;width:auto;height:auto;float:none;">编译器</span></a></span>会警告我们说ptr不能同foo兼容. 
  9. f->somefunction(5,3); 

现在我们要检查接收器是否 符合一个协议 (或者说,在C++就是实现一个接口), 以便我们可以发送这个协议包含的消息. 嘿嘿,它像极了Java的类和接口,而在C++中,完全被实现的类和一个“接口”之间没有技术上的差别.

  1. void* 、 id 或者 SEL? 
  2.  
  3. // Objective-C 
  4. id ptr = ...; // some pointer 
  5. if ([ptr conformsToProtocol:@protocol(foo)] 
  6. [ptr somefunction:5:3]; 
  7. SEL s = @selector(foo:); // a pointer to a function foo that takes 1 parameter 
  8.  
  9. // C++ 
  10. void* ptr = ...; // some pointer 
  11. foo* f = dynamic_cast<foo*>(ptr); 
  12. if (f) 
  13. f->somefunction(5,3); 

id 是通用的用于Objective-C类的类似于 void* 的东西. 你只能使用id而不是 void* 因为id可以通过ARC(稍后会详细介绍到它)编程一个可被管理的指针,而因此编译器需要在元指针类型和Objective-C指针之间做出区分. SEL 是一个用于选择器(C++函数指针)的通用类型,而你一般可以通过使用关键字@selector带上函数名字和:::::s的一个数字来创建一个选择器, 这取决于可以传入多少参数. 选择器实际上就是一个字符串,它会在运行时绑定到一个方法识别器.

类定义,方法,数据,继承

  1. // Objective C 
  2. @class f2; // forward declaration 
  3. @interface f1 : NSOBject // Objective-C supports only public and single inheritance 
  4. int test; // default = protected 
  5. @public 
  6. int a; 
  7. int b; 
  8. f2* f; 
  9. - (void) foo; 
  10. @end 
  11.  
  12. @implementation f1 
  13. - (void) foo 
  14. a = 5// ok 
  15. self->a = 5// ok 
  16. super.foo(); // parent <span id="11_nwp" style="width: auto; height: auto; float: none;"><a id="11_nwl" href="http://cpro.baidu.com/cpro/ui/uijs.php?adclass=0&app_id=0&c=news&cf=1001&ch=0&di=128&fv=18&is_app=0&jk=b2645dbfe197c88d&k=call&k0=call&kdi0=0&luki=1&n=10&p=baidu&q=06011078_cpr&rb=0&rs=1&seller_id=1&sid=8dc897e1bf5d64b2&ssp2=1&stid=0&t=tpclicked3_hc&tu=u1922429&u=http%3A%2F%2Fwww%2Eadmin10000%2Ecom%2Fdocument%2F4400%2Ehtml&urlid=0" target="_blank" mpid="11" style="text-decoration: none;"><span style="color:#0000ff;font-size:14px;width:auto;height:auto;float:none;">call</span></a></span> 
  17. @end 
  18.  
  19. // C++ 
  20. class f1 : public CPPObject 
  21. int test; // default = private 
  22. public
  23. class f2* f; // forward declaration 
  24. int a; 
  25. int b; 
  26. void foo(); 
  27.  
  28. void f1 :: foo() 
  29. a = 5// ok 
  30. this->a = 5// ok 
  31. CPPOBject::foo(); // parent call 

Objective-C中的实现范围在@implementation/@end 标记 (在 C++ 中我们可以将实现放在任何带有::范围操作符的地方)之中. 它使用@class关键字用于事先声明. Objective-C 默认带有 私有(private)保护, 但仅用于数据成员(方法必须是公共的). Objective-C 使用 self 而不是 this ,并且它还可以通过super关键字调用它的父类.

构造器和析构器

  1. // Objective-C 
  2. NSObject* s = [NSObject alloc] init]; // can return nil if construction failed 
  3. [s retain]; // Increment the ref count 
  4.  
  5. // C++ 
  6. CPPObject* ptr = new CPPObject(); // can throw 
  7. ptr->AddRef(); 
  8.  
  9. // Objective-C 
  10. NSObject* s = [NSObject alloc] initwitharg:4]; 
  11. [s release]; 
  12.  
  13. // C++ 
  14. CPPOBject* ptr = new CPPOBject(4); 
  15. ptr->Release(); 

Objective-C中的内存分配是通过静态成员方法alloc来实现的,所有 (做为NSObject后裔)的对象都有这个方法. self 在Objective-C中是可以被赋值的,而如果构建失败的话它就会设置成nil(而在C++中则会被抛出一个异常). 内存分配之后实际被构造器调用的是一个公共的成员函数,在Objective-C中默认的是init方法.

Objective-C 使用同COM益阳的引用计数方法, 而它也使用 retain 和 release (等同于IUnknown的 AddRef() 和 Release() 方法). 当引用计数到了0,则对象会从内存中被移除掉.

多线程

  1. // Objective C 
  2. @interface f1 : NSOBject // Objective-C supports only public and single inheritance 
  3. - (void) foo; 
  4. - (void) threadfunc :(NSInteger*) param; 
  5. - (void) mt; 
  6.  
  7. @end 
  8.  
  9. @implementation f1 
  10.  
  11. - (void) threadfunc : (NSInteger*) param 
  12. [self performSelectorOnMainThread: @selector(mt)]; 
  13.  
  14. - (void) mt 
  15.  
  16. - (void) foo 
  17. [self performSelectorInBackground: @selector(thradfunc:) withObject:1 waitUntilDone:false]; 
  18. <div></div>} 
  19. @end 

Objective-C 有一些针对NSObject的内建功能,可以在另外一个线程中操作一个选择器 (== 调用一个成员), 在主线程中,等待一次调用等等 . 在NSObject 参见更多信息.

内存和ARC

  1. // Objective-C 
  2. @interface f1 : NSObject 
  3. @property (weak) NSAnotherObject* f2; // 当没有其它强引用存在的时候,它将会被自动设置成 
@end -(void)foo { NSObject*s = [NSObject alloc]&nbsp;init]; // 如果构造失败的话会返回nil // use s // end. Hooraah! Compiler will automati<span id="9_nwp" style="width: auto; height: auto; float: none;"><a id="9_nwl" href="http://cpro.baidu.com/cpro/ui/uijs.php?adclass=0&app_id=0&c=news&cf=1001&ch=0&di=128&fv=18&is_app=0&jk=b2645dbfe197c88d&k=call&k0=call&kdi0=0&luki=1&n=10&p=baidu&q=06011078_cpr&rb=0&rs=1&seller_id=1&sid=8dc897e1bf5d64b2&ssp2=1&stid=0&t=tpclicked3_hc&tu=u1922429&u=http%3A%2F%2Fwww%2Eadmin10000%2Ecom%2Fdocument%2F4400%2Ehtml&urlid=0" target="_blank" mpid="9" style="text-decoration: none;"><span style="color:#0000ff;font-size:14px;width:auto;height:auto;float:none;">call</span></a></span>y call[s release] for us! }

  这里需要你忘记自己良好的 C++ 习惯. OK Objective-C 使用了一个垃圾收集机制,这种东西我们C++很讨厌,因为它很慢并会让我们想到Java. 但是 ARC (自动进行引用计算) 是一种 编译时间 特性,它会告诉编译器 "这里是我的对象:请帮我算算啥时候它们才要被销毁吧". 使用ARC你就不需要发送 retain/release 消息给你的对象了; 编译器会自动帮你代劳的.

为了能帮助编译器决定保留一个对象多长时间,你还要一个弱引用指向一个变量. 默认的,所有的变量都是强引用(==只要强引用还存在,被引用的对象就会存在下去) . 你也可以获取一个弱引用,它会随着每个强引用消失而失去它的值. 这在类成员从XCode的Builder Interface(像RC 编辑器)处获取它们的值时,会很有用,当类被销毁时,那些成员也会失去它们的值.

Strings

  1. // Objective-C 
  2. NSString* s1 = @"hello"
  3. NSString* s2 = [NSString stringWithUTF8String:"A C String"]; 
  4. sprintf(buff,"%s hello to %@","there",s2); 
  5. const char* s3 = [s2 UTF8String] 

;

NSString 是一个Objective-C字符串的不可变表示. 你可以使用其一个静态方法,或者是一个带有@前缀的字符串文本来创建NSString. 你也可以使用 %@ 来向printf家族函数来表示一个NSString,

数组

  1. // Objective-C 
  2. NSArray* a1 = [NSArray alloc] initWithObjects: @"hello",@"there",nil]; 
  3. NSString* first = [a1 objectAtIndex:0]; 

NSArray和NSMutableArray是在Objective-C中处理数组的 两个类(两者的差异是,NSArray元素构造时必须通过构造函数,而NSMutableArray可以在之后更改)。典型构造函数的生效,你必须通过nil去作为“结尾元素”。排序/搜索/插入函数对于NSArray和NSMutableArray来说是一样的,在第一行中的例子它返回一个新的NSArray,而在NSMutableArray的例子里,它修改的是一个存在的对象。

分类

  1. // C++ 
  2. class MyString : public string 
  3. public
  4. void printmystring() { printf("%s",c_str()); } 
  5. }; 
  6.  
  7.  
  8. // Objective-C 
  9. @interface MyString (NSString) 
  10. - (void) printmystring; 
  11. @end 
  12.  
  13. @implementation MyString (NSString) 
  14. - (void) printmystring 
  15. printf("%@",self); 
  16. @end 
  17.  
  18. // C++ 
  19. MyString s1 = "hi"
  20. s1.printmystring(); // ok 
  21. string s2 = "hello"
  22. s2.printmystring(); // error, we must change s2 from string to MyString 
  23.  
  24. // Objective-C 
  25. NSString* s2 = @"hello"
  26. [s2 printmystring]; // valid. We extended NSString without changing types. 

  C++依赖 继承机制来实现一个已知的类。这是很麻烦的,因为所有用户的实现类必须使用另外的类型名称(在例子中,MyString用来代替string)。Object-C通过使用 分类(Categories)允许扩展一个已知的类内 同型(same type )。上面链接中所有源代码在extension.h文件 (具有代表性的是像NSString+MyString.h这样的)中可以查看,上面例子中,我们立即就有可以调用新的成员函数,而不需要改变NSString类型为MyString。

块 和 Lambda

  1. // Objective-C 
  2. // member function 
  3. -(void)addButtonWithTitle:(NSString*)title <span id="4_nwp" style="width: auto; height: auto; float: none;"><a id="4_nwl" href="http://cpro.baidu.com/cpro/ui/uijs.php?adclass=0&app_id=0&c=news&cf=1001&ch=0&di=128&fv=18&is_app=0&jk=b2645dbfe197c88d&k=block&k0=block&kdi0=0&luki=3&n=10&p=baidu&q=06011078_cpr&rb=0&rs=1&seller_id=1&sid=8dc897e1bf5d64b2&ssp2=1&stid=0&t=tpclicked3_hc&tu=u1922429&u=http%3A%2F%2Fwww%2Eadmin10000%2Ecom%2Fdocument%2F4400%2Ehtml&urlid=0" target="_blank" mpid="4" style="text-decoration: none;"><span style="color:#0000ff;font-size:14px;width:auto;height:auto;float:none;">block</span></a></span>:(void(^)(AlertView*, NSInteger))block; 
  4.  
  5. // <span id="5_nwp" style="width: auto; height: auto; float: none;"><a id="5_nwl" href="http://cpro.baidu.com/cpro/ui/uijs.php?adclass=0&app_id=0&c=news&cf=1001&ch=0&di=128&fv=18&is_app=0&jk=b2645dbfe197c88d&k=call&k0=call&kdi0=0&luki=1&n=10&p=baidu&q=06011078_cpr&rb=0&rs=1&seller_id=1&sid=8dc897e1bf5d64b2&ssp2=1&stid=0&t=tpclicked3_hc&tu=u1922429&u=http%3A%2F%2Fwww%2Eadmin10000%2Ecom%2Fdocument%2F4400%2Ehtml&urlid=0" target="_blank" mpid="5" style="text-decoration: none;"><span style="color:#0000ff;font-size:14px;width:auto;height:auto;float:none;">call</span></a></span> 
  6. [object addButtonWithTitle:@"hello" block:[^(AlertView* a, NSInteger i){/*DO SOMETHING*/}]; 

块 是Objective-C 用来模拟lambda功能的一种方式. 查看Apple的文档,从AlertView的示例 (使用块的UIAlertView)可以获得更多有关块的技术.

C++ 开发者使用 Objective-C 和 ARC 的重要提示

  1. // C++ 
  2. class A 
  3. public
  4.  
  5. NSObject* s; 
  6. A(); 
  7. }; 
  8.  
  9. A :: A() 
  10. s = 0// 可能会奔溃,这是常发生在发布模式下! 

你已经知道给所有的顾客都打两折对你而言有多痛苦了,因为你bug重重的软件会在发布模式下奔溃,而在调试模式下总是妥妥的. 没有用户会理解程序员,是不是?

让我们来看看这里发生了什么. s = 0 这一行将 0 赋值给了一个变量,而因此不管这个变量之前取值是什么,首先都会被释放掉,所以编译器在赋值之前执行了一次 [s release] . 如果 s 之前已经是 0 了,假设是在调试构建的话,不会发生任何不好的事情; 如果 s 是一个nil的话,使用[s release] 是再好也不过的. 然而,在发布模式下, s可能是一个野指针,所以它可能在被“初始化”为0之前包含任何值(这很危险是不是?).

在C++中,这并不是一个问题,因为它里面是不存在ARC的. 而在Objective-C中编译器并没有办法了解这是一次"赋值" 还是一次 "初始化" (如果是后者,它就不会发送发布消息).

下面是正确的方式:

  1. // C++ 
  2. class A 
  3. public
  4.  
  5. NSObject* s; 
  6. A(); 
  7. }; 
  8.  
  9. A :: A() :s(0// 现在编译器就知道确定 it&apos;s 是一次初始化了, 一次就不存在 [s release] 

现在编译器就不会去尝试调用一个 [s release] 因为它知道它是这个对象的第一次初始化. 请小心!

从Objective-C 对象到 C++ 类型的转换

  1. // Objective-C 
  2. NSObject* a = ...; 
  3. void* b = (__bridge void*)a; // 你必须在Objective-C和C类型支架使用 __bridge 
  4. void* c = (__bridge_retained void*)a; // 现在是一个+1的保留计数,而你必须在稍后释放这个对象 
  5. NSObject* d = (__bridge_transfer NSObject*)c; // 现在ARC取得了对象c的”拥有权“, 将其装换成一个ARC管理的NSObject. 

我可以分析这一切,而我的建议是简单的. 不要 将ARC类型和非ARC类型混在一起. 如果你必须转换一些Objective-C对象的话,使用id而不是void*. 否则,你将会遇到严重的内存故障.

Objective-C 有而 C++ 没有的

分类Categories

基于NSObject的操作

YES 和 NO (等价于true和false)

NIL 和 nil (等价于0)

可命名的函数参数

self (等价于 this) 而其可以在一个构造器中被改变

C++ 有而 Objective-C 没有的

静态对象. Objective-C 中的对象不能被初始化成静态的,或者是存在于栈中. 只能是指针.

多重继承

命名空间

模板

操作符重载

STL 和算法 ;

方法可以是受保护的( protected )或者私有的( private ) (在Obj-C中,只能是公共的)

const/mutable 项

friend 方法

引用

匿名函数签名 (没有变量名称)

责任编辑:chenqingxiang 来源: 博客园
相关推荐

2014-07-01 09:22:01

SwiftObjective-CiOS

2011-05-11 13:54:08

Objective-C

2014-04-30 10:16:04

Objective-CiOS语法

2014-04-15 11:27:50

C++开发者Objective-C核心语法

2010-11-24 10:35:40

Objective-C

2011-07-18 16:36:51

Objective-C XCode

2010-11-04 16:32:00

Objective-C

2011-08-04 13:38:01

Objective-C C++

2014-10-13 09:54:08

Objective-CSwift

2016-03-30 09:56:07

c语言变量声明objectivec

2011-07-06 14:41:34

Objective-C

2011-08-10 18:07:29

Objective-C反射

2011-05-11 11:20:26

Objective-C

2013-03-27 12:54:00

iOS开发Objective-C

2013-06-20 10:40:32

Objective-C实现截图

2011-05-11 15:58:34

Objective-C

2011-06-27 15:48:09

Cocoa TouchObjective-C

2011-08-04 13:32:21

Objective-C 方法 对象

2013-04-11 13:41:30

Objective-CiOS编程

2014-07-29 09:44:35

点赞
收藏

51CTO技术栈公众号