iOS中容易混淆的知识点(持续更新中)

时间:2024-01-13 13:50:02

1、成员变量和属性的区别

@interface Person : NSObject
{
NSString *_sex;
}
@property (nonatomic, copy) NSString *name;
@end
//定义一个Person类,在类里面分别定一个成员变量_sex和属性name

成员变量:

1. 成员变量的默认修饰是@protected。
2. 成员变量不会自动生成set和get方法,需要自己手动实现。
3. 成员变量不能用点语法调用,因为没有set和get方法,只能使用->调用。

属性:

 1.     属性的默认修饰是@protected。

 2. 属性会自动生成set和get方法。

 3. 属性用点语法调用,点语法实际上调用的是set和get方法。

2、结构体和类的区别

//结构体
struct Worker{
NSString *postOfDuty;
NSString *department;
} Worker1,Worker2; //类
@interface Progremer :NSObject{
@public
NSString *_name;
}
@end
@implementation Progremer
@end

  1、类是引用类型,结构体是值类型,值类型的传递和赋值时是复制操作(不会改变原始对象),而引用类型则只会使用引用对象的一个指向,即对象的地址(会改变原始对象);(详见下面分析)

   2、结构体只能封装属性,类却不仅可以封装属性也可以封装方法。如果一个封装的数据有属性也有行为,就只能用类了;

    3、结构体变量分配在栈,而OC对象分配在栈的空间相对于堆来说是比较小的,但是存储在栈中的数据访问效率相对于堆而言是比较高。所以,我们使用结构体的时候最好是属性比较少的结构体对象,如果属性较多的话就要使用类了。

  4、类可以集成,这样子类可以使用父类的特性和方法,而结构体不可以;

3、nil 、NULL、 Nil 、NSNull的区别

  nil:指向Objective C语言中对象的空指针,其定义值为(id)0,指向OC中对象的空指针.    

示例代码:
NSString *someString = nil;
NSURL *someURL = nil;
id someObject = nil;
if (anotherObject == nil) // do something NSArray * array = [NSArray arrayWithObjects:@"test",@"test1" ,nil];
 [array release];
  
if (array){
   //仅仅对数组release,并没有赋值为nil,在程序某个地方如果继续对数组操作,程序直接崩溃
   NSString * string = [array objectAtIndex:];
   NSLog(@"%@",string);
 }
// 用法讲解:当对某个对象release 的同时最好把他们赋值为nil,这样可以确保安全性,如果不赋值nil,可能导致程序崩溃.

  Nil:指向Objective C语言中类(Class)的空指针,其定义值为(Class)0。

 示例代码:  
Class someClass = Nil; 
Class anotherClass = [NSString class];

  NULL:指向C语言中的标准空指针,其定义值为(void *)0,可以用在C语言的各种指针上。

 示例代码:
int *pointerToInt = NULL;    
char *pointerToChar = NULL;  
struct TreeNode *rootNode = NULL; //用法讲解:在Objective-C里,nil对象被设计来跟NULL空指针关联的。他们的区别就是nil是一个对象,而NULL只是一个值。而且我们对于nil调用方法,不会产生crash或者抛出异常。

  NSNull:在Objective C语言的集合对象中,表示空值的对象,NSNull是一个类,其定义值为[NSNull null]。[NSNull null]是一个对象,用在不能使用nil的场合。

  因为在NSArray和NSDictionary中nil有特殊的含义(表示列表结束),所以不能在集合中放入nil值。如要确实需要存储一个表示“什么都没有”的值,可以使用NSNull类。例:[NSMutableArrayObj addObject:[NSNull null]];

/*
集合对象无法包含nil作为其具体值,如NSArray、NSSet和NSDictionary,这个时候可以用[NSNull null]来代替。
*/
我们对于nil调用方法,不会产生crash或者抛出异常。

  若obj为nil:[obj message]将返回NO,而不是NSException。

  若obj为NSNull:[obj message]将抛出异常NSException。

参考资料

4、一维指针\二维指针……

指针值存放某一个变量地址的一个容器,我们经常用到的是一维指针,即 int i= 6; int *p = &i;这里的p就是指向变量i地址的指针(p是指针  *p不是    *是间接取值  也就是*p是通过p指针指向的地址读取该变量的值)

 int i = ;
int *p = &i;
//用&p来输出地址
//打印结果:6--6---0x7ffeefbff58c---0x7ffeefbff58c
//由打印结果可以得知 指针p 是指向变量i的地址的 *是间接取值 也就是*p是通过p指针指向的地址读取该变量的值
//(注意p才是指针 *p表示读取p指针p所指向的值)
NSLog(@"%d--%d---%p---%p",i,*p,p,&i); //打印结果:6--0x7ffeefbff58c--0x7ffeefbff58c--0x7ffeefbff580--0x7ffeefbff580--0x7ffeefbff578
//**q是个二维指针 也就是指向一个(指向int变量的指针[p])的指针[q]
//*q 是读取q指向地址的值 也就是指针p的值 指针p是存储i的地址值 所以*p输出的是&i
//**q 则是读取*q指向地址的值 也就是读取&i的值 所以输出为 6
//指针p是指向i的地址值的,但它自身也有一个地址值,所以&p就是他自身的地址值 这个值和&不一样
//q就是&p 也就是p的地址值
int **q = &p;
NSLog(@"%d--%p--%p--%p--%p--%p",**q,*q,&i,&p,q,&q); //q是一个二维数组 指向一个(指向int变量的指针[p])的指针[q]
//w是一个三维数组 w是指针q的自身地址值 *w则是读取w指向地址的值 也就是q存储的值 q存储的优势p的地址值 也就是*w实际上读取的是p的地址值 **w是读取*w的值 *w读取的是p的地址值 那么**w则是根据p的地址值读取p的存储值 也就是i的地址值 同理***w则是根据p存储的值也就是i的地址值读取i存储的值 6
//&w是其自身的地址值
int ***w = &q;
//输出结果:0x7ffeefbff578---0x7ffeefbff580---0x7ffeefbff58c---6---0x7ffeefbff570
NSLog(@"%p---%p---%p---%d---%p",w,*w,**w,***w,&w);

具体的指向图:

iOS中容易混淆的知识点(持续更新中)

这个时候可能有疑问了,比如说我们定义一个字符串:NSString  *str2 = @"sddd";

我们在输出的时候,总是写 NSLog(@"%@",str2);

按照指针的理解,不应该是str是指向某个值的地址吗?*str2才是根据这个地址取得的值,按理说应该是NSLog(@"%@",*str2);才对呀,但是这个写会报错,

这是因为当输出时,程序是按流输出 遇到'/0'结束 也就是说字符串输出时只要给出首地址 但遇到\0是就自动结束,所以我们需要传的正是地址也就是str2

当我们对指针进行赋值操作时,实际上是将指针指向了一个新的区域

str2 = @"test";//s1指向了新的内存,不再指向原来的内存,跟原来的内存没有关系了
总结:当我们用字面量语法对一个变量赋值时相当于对他重新开辟了内存块,也就是原本的内存块和他是已经没有关系了。原来的内存因为没有指针指向,会被释放回收。

这边我们可以通过打印数据来验证:

iOS中容易混淆的知识点(持续更新中)

我们发现,str2指针的位置并没有变,还是0x7ffeefbff520  但是它指向的地址却由0x100003328变为了0x100003368

iOS中容易混淆的知识点(持续更新中)

函数指针与指针函数的区别:

1、指针函数是指带指针的函数,即本质是一个函数,函数返回类型是某一类型的指针

2、函数指针是指向函数的指针变量本质是一个指针变量。

注意指针函数与函数指针表示方法的不同,千万不要混淆。最简单的辨别方式就是看函数名前面的指针*号有没有被括号()包含,如果被包含就是函数指针,反之则是指针函数。

指针函数:当一个函数声明其返回值为一个指针时,实际上就是返回一个地址给调用函数,以用于需要指针或地址的表达式中。

格式: 
类型说明符 * 函数名(参数) 
当然了,由于返回的是一个地址,所以类型说明符一般都是int。 
例如:int *GetDate(); 
     int * aaa(int,int); 
函数返回的是一个地址值,经常使用在返回数组的某一元素地址上。

   //函数声明
int * GetDate(int wk,int dy);
//main函数主体
main()
{
int wk,dy;
do
{
printf(Enter week(-)day(-)\n);
scanf(%d%d,&wk,&dy);
}
while(wk<||wk>||dy<||dy>);
printf(%d\n,*GetDate(wk,dy));
}
//声明函数的实现 程序应该是很好理解的,子函数返回的是数组某元素的地址。输出的是这个地址里的值。
int * GetDate(int wk,int dy)
{
static int calendar[][]=
{
{,,,,,,},
{,,,,,,},
{,,,,,,},
{,,,,,,},
{,,,-}
};
return &calendar[wk-][dy-];
} // int * GetDate(int wk,int dy);就是一个指针函数 函数返回数据是一个指针(变量的地址)

函数指针是指向函数的指针变量,即本质是一个指针变量,指向函数的指针包含了函数的地址,可以通过它来调用函数。

声明格式如下:类型说明符 (*函数名)(参数)

其实这里不能称为函数名,应该叫做指针的变量名。这个特殊的指针指向一个返回整型值的函数。指针的声明笔削和它指向函数的声明保持一致。 
指针名和指针运算符外面的括号改变了默认的运算符优先级。如果没有圆括号,就变成了一个返回整型指针的函数的原型声明。

    void (*funcp)();
void FileFunc(),EditFunc(); main()
{
funcp=FileFunc;
(*funcp)();
funcp=EditFunc;
(*funcp)();
} void FileFunc()
{
printf(FileFunc\n);
} void EditFunc()
{
printf(EditFunc\n);
} 程序输出为:
FileFunc
EditFunc
//这里的funcp就是一个函数指针 他指向了一个函数的地址
参考资料

5.逻辑运算符与++运算符注意点

iOS中容易混淆的知识点(持续更新中)

6、objc_getClass和object_getClass

Class objc_getClass(const chat *aClassName)

Class objc_getClass(const chat *aClassName)
> 传入一个类名的字符串
> 返回对应的类对象

Class object_getClass(id obj)

Class object_getClass(id obj)
> 传入的obj可能是instance对象,class对象、meta-class对象
> 返回值
  a:如果是instance对象,返回class对象
  b:如果是class对象,返回meta-class对象
  c:如果是meta-class对象,返回NSObject(基类)的meta-class对象

- (class)class、+(class)class

返回的是类对象

7、查看数据分配内存大小的方法

class_getInstanceSize函数:

查看对象的实例变量实际占用的内存大小

malloc_size函数:

系统实际分配给对象的内存大小(关于实际占用与实际分配不一致的原因 前面讲到过)

iOS中容易混淆的知识点(持续更新中)

sizeof运算符:

sizeof是运算符,求指定变量或者变量类型等所占内存大小,其返回结果类型是size_t,它在头文件中typedef为unsigned int类型。在头文件中typedef为unsigned int,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。

它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:

数组——编译时分配的数组空间大小;

指针——存储该指针所用的空间大小;

strlen函数:

strlen是函数,要在运行时才能计算。参数必须是字符型指针。当数组名作为参数传入时,实际上数组就退化成指针了。

它的功能是:返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符NULL。返回的长度大小不包括NULL。

iOS中容易混淆的知识点(持续更新中)

8、():<>

@interface NSObject (A) : B <C>   //←这个关系是不可能存在的 只为了演示

其中A是表示该类是NSObject的分类 那么文件名就是NSObject+A.h/.m

B表示该类继承自B(这个只是为了演示  NSObject本来就是基类了 不可能会继承自某个类)

C表示该类需要遵守的协议 比如<NSCoding>

另外需要注意的一点就是在oc中分类中是不可能在继承其他类的,也就是A和B不可能在一起出现(即使A=B),因为oc不支持多继承,分类只是某个类的衍生品,本身并不是一个独立的类也就不存在继承自谁的说法(所以分类都不能写继承[:])比如分类结构是PersonClass(student) 这虽然是个分类 但它的类名还是PersonClass  而PersonClass是一个继承自NSObject的独立的类  所以PersonClass(student)这个分类就不可能再继承自哪个类了

9、全局变量、局部变量、静态变量、自动变量、常量、static、extern、const

对于程序中的数据,我们按照其值能否修改,可以划分为变量和常量两大类。

常量:值无法修改,因为常量是只读属性,无法修改,所以常量创建的时候就要赋值。常量常见的创建方式有以下三种:

宏定义:(宏只是字符串替换,当项目中用到TestKey时,系统会将其自动替换成@“test”)

 //直接使用#define定义函数、字符串和数字,
#define TestKey @“test”
#define Max_time 100.0
#define degresssToRadian(x) (M_PT*(X)/180.0)

② 使用枚举定义常量,在这里kTagSeriesView等于101,在程序中直接使用kTagSeriesView来表示这个常量

typedef enum{
kTagLanguageView = ,
kTagSeriesView,
kTagSeriesDetailView,
kTagThumbView,
kTagVideoView,
kTagFullPhotoView,
}TagSystemViews;

const修饰 被const修饰的变量是只读的,不可以修改,将变量变成常量。

NSString * const test = @"abc";

变量:值可以修改,创建的时候可以赋初值,也可以以后再赋值。

所有变量,按定义位置划分的话可以分类全局变量和局部变量两类:

定义在函数(方法)以外的变量是全局变量,定义在函数以内的变量是局部变量;

全局变量有以下特点:

1.声明在函数外部;

2.可以在同项目中跨文件访问(通过extern);

3.可以在声明时赋上初值,如果不赋初值的话,系统会自动赋值为0;

4.全局变量存放在全局存储区(既不是栈区也不是堆区);

5.全局变量名称不能重复  也就是定义了一个stee的变量后不能在项目中再定义stee这个名称的变量 即使是不同类型也是不可以的 否则会crash

局部变量的特点:

1.声明在函数内部

2.只能在声明的函数内部访问,仅当函数执行时存在,函数结束后变量就会释放;

3.可以在声明时赋上初值,如果不赋初值的话,系统会自动赋值为0;

4.存储在栈区。【自动保存在函数的每次执行的【栈帧】中,并随着函数结束后自动释放,另外,函数每次执行则保存在【栈】中】

5.局部变量默认就是自动变量  也就是在函数内声明的变量,如果不加修饰符的话默认就是自动变量

#import <Foundation/Foundation.h>

//全局变量
NSString *name = @"dsa"; int main(int argc, const char * argv[]) {
@autoreleasepool {
//局部变量(自动变量)
NSString *nickName = @"test";
//局部变量默认就是自动变量 有auto修饰符的 但是auto修饰符可以不写
auto NSString *nickName = @"test"; }
return ;
}

extern访问外部变量(也就是其他文件的变量)

用extern修饰的变量,会在整个项目中查找这个变量 进行访问  (如果这个变量是个静态变量(也就是被static修饰)则无法访问)

比如我们有个全局变量定义在A类的实现文件中,如果在B类中想访问这个变量就可以通过extern

比如我们在personClass.m中定义两个全局变量

#import "PersonClass.h"

NSString *stsssee = @"ssdsdsd";
@interface PersonClass()
@end @implementation PersonClass
NSString *stee = @"dsdsd"; - (void)test
{ }

那么在其他文件中想要访问时 ,可以通过extern

  extern NSString *stee;
extern NSString *stsssee; NSLog(@"%@--%@--",stee,stsssee);

但是如果我们访问一个全局都不存在或者这个变量被static修饰的变量时  会crash    :-1: Undefined symbol: _steeaa

 extern NSString *steeaa; 

static:如果变量用static修饰的话 那就变成了静态变量

静态变量有以下特点:

1.无论是全局变量 还是局部变量  如果用static修饰的话 那就变成了静态变量;

2.静态变量的访问范围只能在当前文件内访问,无法在其他文件中访问 通过extern也不可以;

3.可以在声明时赋上初值,如果不赋初值的话,系统会自动赋值为0;

4.静态变量存储在全局存储区

const:如果变量用const修饰的话,那么这个变量就成为了常量,只可以读取不能修改  分为以下几种情况

//①const修饰的是str1 也就是指针中存储的内存地址不能变,也就是指针的指向不能变
NSString * const str1 = @"sddd";
//报错:Cannot assign to variable 'str1' with const-qualified type 'NSString *const __strong'
//相当于给str1换一块存储空间
str1 = @""; //②const修饰的是*str1 也就是指针指向的存储内容不能变,但是指针的指向可以变 也就是这块内容不变 但是指针指向另一块内容
NSString const *str2 = @"sddd";
str2 = @""; //③这个和 NSString const *str3 = @"sddd";没有区别
const NSString *str3 = @"sddd";
str3 = @"";

②和③是一样的,也就是看const在*的左边还是右边了,

const在*左边说明 是*str不能变,也就是指针指向的原内存中的内容不可以变,但指针可以指向其他的内存

const在*右边说明 是str不能变,也就是指针的指向不可以变,但指针指向的内容可以变

iOS中容易混淆的知识点(持续更新中)

所以有两道面试题可以很轻松的回答:

1.全局变量和局部变量的区别?

1.声明位置不同,全局变量声明在函数外,局部函数声明在函数内;

2.访问范围不同,全局变量可以整个项目中都可以访问,而局部变量只能在声明的函数内部访问;

3.存储在内存中的位置不同,全局变量存储在全局存储区,而静态变量存储在栈中。【全局变量保存在内存的全局存储区中,占用静态的存储单元;局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。】

2.全局变量和静态变量都纯在全局存储区,两者有什么区别?

①全局变量只能声明在函数外,而静态变量既可以声明在函数外也可以声明在函数内,只要有static修饰即可;

②访问范围不同,即使全局变量和局部变量声明位置一样,两者的访问范围也是不同的,全局变量可以通过extern被同项目中所有文件访问 而静态变量只能在声明的当前文件中使用;

这里大概讲一下 内存的分区:

程序对内存的使用可以分为以下几个区:(关于堆和栈的记忆:一堆程序员,所以堆是由程序员管理和控制的)

iOS中容易混淆的知识点(持续更新中)

栈区(stack):是一种用来存储函数调用时的临时信息的结构,由编译器自动分配和释放,比如存放函数的参数值,局部变量的值等
        /*
          栈的特性: 最后一个放入栈中的物体总是被最先拿出来,这个特性通常称为先进后出(FILO)队列。
          栈的基本操作:
             PUSH操作:向栈中添加数据,称为压栈,数据将放置在栈顶;
             POP操作:POP操作相反,在栈顶部移去一个元素,并将栈的大小减一,称为弹栈。
        */ 堆区(heap)用来存储程序运行时分配的变量,一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。
        /*
           堆的大小并不固定,可动态扩张或缩减。其分配由malloc()、new()等这类实时内存分配函数来实现。
           当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张)(对象都存储在堆上);
           当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
        */ 全局区(数据段):全局变量和静态变量存放在此。包括已初始化的数据段(.data)和未初始化的数据段(.bss),前者用来存放保存全局的和静态的已初始化变量,后者用来保存全局         的和静态的未初始化变量。数据段在编译时分配。 文字常量区:常量字符串放在此,程序结束后由系统释放。 程序代码区(.text):存放函数体的二进制代码。
栈:像是装数据的桶或者箱子
栈是大家比较熟悉的一种数据结构,它是一种具有后进先出的数据结构,也就是说后存放的先取,先存放的后取,这就类似于我们要在取放在箱子底部的东西(放进去比较早的物体),我们首先要移开压在它上面的物体(放入比较晚的物体)。 堆:像是一颗倒立的大树
堆是一种经过排序的树形数据结构,每个节点都有一个值。通常我们所说的堆的数据结构是指二叉树。堆的特点是根节点的值最小(或最大),且根节点的两个树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。

我们可以打印几个常用的数据,可以发现一些有意思的事情

iOS中容易混淆的知识点(持续更新中)

比如说,3、6中我们可以看出,栈存放的内存地址应该比堆高,也就是指针存放在栈中,而指针指向的内容却存放在堆中呢?

在Objective-C 中,对象通常是指一块有特定布局的连续内存区域。我们通常这样创建一个对象:

NSObject *obj = [[NSObject alloc] init];
这行代码创建了一个 NSObject 类型的指针 obj 和一个 NSObject 类型的对象,obj 指针存储在栈上,而其指向的对象则存储在堆上(简称为堆对象

所以我们看说对象存放在堆区,是因为对象指针指向的内容(本体)存放在堆区而不是看他的指针存放在哪?

所以从上面我们可以看出来:

1、全局变量和静态变量的指针和指针指向的内容都存放在全局存储区;(1.4)

2、局部变量存储在栈区;(2)

3、对象存储在堆区;(6)

4、blcok存放在栈区,但是当被调用时,会将block移动到堆区,同时将blcok内部引用的数据也移动到堆区(5.7.8.9)

5、字符常量存放在文字常量区(3)【这里是因为我们查看nickName的底层是__NSCFConstantString 】

__NSCFConstantString显然是常量字符串,自然就是存储在常量区。
__NSCFString表示为oc对象,NSString就是封装的CFString,0x6000000315c0地址显示这个字符串对象存储在堆中。

iOS中容易混淆的知识点(持续更新中)

关于NSString存放在哪?可以看下面几篇资料:

NSString参考资料

NSString在内存中的存储

Objective-C对象的TaggedPointer特性 (这篇文章中  关于储存区域的说法 我个人觉得是不正确的  但除此之外的讲解还是不错的)

深入理解Tagged Pointer

关于栈对象堆对象参考资料