一般编译器的预编译指令,分为以下4类:
条件编译
#if 格式
功能:当表达式的值为真时,编译代码段1,否则编译代码段2。其中,#else 和 代码段2 可有可无
#if 表达式
代码段1
#else
代码段2
#endif
#ifdef 格式
功能:当标识符已被定义时(用 #define 定义),编译代码段1,否则编译代码段2。其中 #else 和 代码段2 可有可无
#ifdef 标识符
代码段1
#else
代码段2
#endif
#ifndef 格式
功能:当标识符没被定义时(用 #define 定义),编译代码段1,否则编译代码段2。其中 #else 和 代码段2 可有可无
#ifndef 标识符
代码段1
#else
代码段2
#endif
注意:
预编译器根据条件编译指令有选择的删除代码,编译器编译时不知道代码分支的存在
#if、#ifdef、#ifndef、#endif 被预编译器处理,而 if … else… 语句被编译器处理
条件编译指令在预编译期进行分支判断,而 if … else … 语句在程序运行期间进行分支判断
#include 和 条件编译的应用
#include 的本质是将已经存在的文件内容嵌入到当前文件中,#include 的间接包含同样会产生嵌入文件内容的操作。可以理解为:预编译时, #include 会将指定文件的内容复制到 #include 所在的位置。
如下代码会在 main.c 中产生重复定义问题:
// ---------------- Calc00.h ----------------
#include
int add(int num0, int num1);
int sub(int num0, int num1);
// ---------------- Calc00.c ----------------
#include "Calc00.h"
int add(int num0, int num1) {
return num0 + num1;
}
int sub(int num0, int num1) {
return num0 - num1;
}
// ---------------- Calc01.h ----------------
#include
#include "Calc00.h"
int multipl(int num0, int num1);
int divis(int num0, int num1);
int mix(int num0, int num1);
// ---------------- Calc01.c ----------------
#include "Calc01.h"
int multipl(int num0, int num1) {
return num0 * num1;
}
int divis(int num0, int num1) {
if (0 == num1) {
return 0;
}
return num0 / num1;
}
int mix(int num0, int num1) {
return multipl(add(num0, num1), sub(num0, num1));
}
// ---------------- main.c ----------------
#include
#include "Calc00.h"
#include "Calc01.h"
int main(int argc, const char * argv[]) {
int result = add(1, 2);
return 0;
}
在 XCode 中编译 C 或者 Objective-C 代码时,其实参与编译的只有 .c 文件和 .m 文件,.h文件是不参与编译的。我们可以在 Build Phases - Compile Sources 中看到,参与编译的只有 .c 文件和 .m 文件:
但是因为我们在 .c 文件中,使用了 #include 导入头文件;在 .m 文件中使用了 #import 导入头文件,所以(C 和 Objective-C)头文件 .h 里面的内容,实际上也会参与编译。
在 XCode 中,创建 C 代码的时候,.h 文件里面默认会生成如下预编译指令,这些预编译指令的作用就是防止重复导入(相当于 Objective-C 中 #improt 的功能):
Test.h 里面的内容如下:
#ifndef Test_h
#define Test_h
#include
// 这里编写代码
#endif /* Test_h */
Test.c 里面的内容如下:
#include "Test.h"
// 这里编写代码
__bridge 只做类型转换,但是不修改对象(内存)管理权
__bridge_retained(也可以使用 CFBridgingRetain)将 Objective-C 的对象转换为 Core Foundation 的对象,同时将对象(内存)的管理权交给程序员,后续需要使用 CFRelease 或者相关方法来释放对象
__bridge_transfer(也可以使用 CFBridgingRelease)将 Core Foundation 的对象转换为 Objective-C 的对象,同时将对象(内存)的管理权交给ARC
// 读取从 list 地址开始的,16 个 Byte 的内存数据,并以 16 进制显式
(lldb) x/16bx list
0x100534f10: 0x0a 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x100534f18: 0x60 0x56 0x53 0x00 0x01 0x00 0x00 0x00
// 读取从 list 地址开始的,16 个 Word 的内存数据,并以 16 进制显式
// 注意,在 64bit 的 CPU 里面,1Word == 4Byte
(lldb) x/16wx list
0x100534f10: 0x0000000a 0x00000000 0x00535660 0x00000001
0x100534f20: 0x534e5b2d 0x63756f54 0x72614268 0x6f6c6f43
0x100534f30: 0x73694c72 0x63695074 0x4272656b 0x4372756c
0x100534f40: 0x61746e6f 0x72656e69 0x77656956 0x696e6920
上一篇中虽然完成了 HCGLinearList 的功能,但没有对其进行封装和优化,HCGLinearList 还存在以下缺陷:
基于以上分析,对 HCGLinearList 做如下修改:
将 HCGLinearList.h 中 对 LinearList 结构体的定义放到 HCGLinearList.c,对外只暴露一个 void 类型的 LinearList,这样外界无法知道 LinearList 的具体定义,也就修改不了 LinearList 结构体的成员了。同时,使用条件编译,防止在 HCGLinearList.h 和 HCGLinearList.c 中对结构体 LinearList 和指针 LinearListNodeValue 进行重复定义。
将 LinearListNodeValue 的类型由 int 改为 void*,即表明线性表存储的节点数据为指针类型。那么此时 LinearList.value 为指向指针数据的指针。
HCGLinearList.h 如下:
// -------------------------------- HCGLinearList.h --------------------------------
#ifndef LinearList_h
#define LinearList_h
#include
#pragma mark - 宏定义(条件编译)
#ifndef LINEARLIST_STRUCT
// 线性表节点数据
typedef void* LinearListNodeValue;
// 线性表
typedef void LinearList;
#endif
#pragma mark - 创建 销毁 清空
// 创建线性表
// param0.线性表容量
// return.线性表指针
LinearList* listCreat(int capacity);
// 销毁线性表
// param0.线性表指针
void listRelease(LinearList* list);
// 清空线性表
// param0.线性表指针
void listClear(LinearList* list);
#pragma mark - 属性获取
// 获取线性表的长度
// param0.线性表指针
// return.线性表长度
int listLength(LinearList* list);
// 获取线性表容量
// param0.线性表指针
// return.线性表容量
int listCapacity(LinearList* list);
#pragma mark - 增
// 往线性表中插入数据
// param0.线性表指针
// param1.要插入的位置的索引
// param2.要插入的值
void listInsert(LinearList* list, int index, LinearListNodeValue value);
// 往线性表中添加数据(添加在表尾)
void listAdd(LinearList* list, LinearListNodeValue value);
#pragma mark - 删
// 删除线性表中指定索引位置的元素
// param0.线性表指针
// param1.索引
void listRemove(LinearList* list, int index);
#pragma mark - 改
// 修改线性表中指定位置的元素为指定的值
// param0.线性表指针
// param1.索引
// param2.值
void listSet(LinearList* list, int index, LinearListNodeValue value);
#pragma mark - 查
// 获取线性表指定索引处元素的值
// param0.线性表指针
// param1.索引
// return.元素的值
LinearListNodeValue listGet(LinearList* list, int index);
#pragma mark - 特殊功能
// 删除线性表中具有指定值的所有元素
// param0.线性表指针
// param1.要删除的值
void listRemoveValue_1(LinearList* list, LinearListNodeValue value);
// 删除线性表中具有指定值的所有元素
// param0.线性表指针
// param1.要删除的值
void listRemoveValue_2(LinearList* list, LinearListNodeValue value);
// 打印线性表
// param0.线性表指针
void listPrint(LinearList* list);
#endif /* LinearList_h */
HCGLinearList.c 如下:
// -------------------------------- HCGLinearList.c --------------------------------
#pragma mark - 定义线性表结构体
// 数据节点指针
typedef void* LinearListNodeValue;
// 线性表结构体
typedef struct {
int capacity; // 容量
int length; // 长度
LinearListNodeValue* value; // 节点数据的指针(相当于 void** value,指向指针的指针)
} LinearList;
/*
注意:宏 LINEARLIST_STRUCT 的定义,一定要在导入 HCGLinearList.h 之前
这是为了防止上面的 LinearListNodeValue 指针和 LinearList 结构体被重复定义
**/
//
#define LINEARLIST_STRUCT
#pragma mark - 导入头文件
#include "HCGLinearList.h"
// 使用 mallc函数 需要导入
#include
#pragma mark - 创建 销毁 清空
// 创建线性表
LinearList* listCreat(int capacity) {
if (capacity < 0) {
return NULL;
}
// 分配线性表结构体的内存空间(在堆区)
// malloc函数如果遇到内存资源紧张,给不了这么多字节,可能会返回空
LinearList* list = malloc(sizeof(LinearList));
if (list) {
list->capacity = capacity;
list->length = 0;
// 分配存储线性表元素的内存空间
list->value = malloc(capacity * sizeof(LinearListNodeValue));
if (!list->value) {
return NULL;
}
}
return list;;
}
// 销毁线性表
void listRelease(LinearList* list) {
if (NULL == list) {
return;
}
if (list->value) {
free(list->value);
}
free(list);
}
// 清空线性表
void listClear(LinearList* list) {
if (NULL == list) {
return;
}
// 这里不需要对线性表中的元素都置0
// 只要将线性表的长度置为0,下次使用线性表的时候,就会对之前的数据进行覆盖了
list->length = 0;
}
#pragma mark - 属性获取
// 获取线性表的长度
int listLength(LinearList* list) {
if (NULL == list) {
return 0;
}
return list->length;
}
// 获取线性表容量
int listCapacity(LinearList* list) {
if (NULL == list) {
return 0;
}
return list->capacity;
}
#pragma mark - 增
// 往线性表中插入数据
void listInsert(LinearList* list, int index, LinearListNodeValue value) {
if (NULL == list) {
return;
}
// 可以在表尾进行插入,因此这里的条件是 index > list->length,而不是 index >= list->length
if (index < 0 || index > list->length) {
return;
}
if (list->length == list->capacity) {
return;
}
// 反向for循环挪动数据:从表尾数据开始挪动直到index标识的位置,每个数据依次向后挪动一个步长
for (int i = list->length - 1; i >= index; i--) {
list->value[i + 1] = list->value[i];
}
// 将新值value插入到index的位置
list->value[index] = value;
// 线性表的长度 + 1
list->length++;
}
// 往线性表中添加数据(添加在表尾)
void listAdd(LinearList* list, LinearListNodeValue value) {
if (NULL == list) {
return;
}
listInsert(list, list->length, value);
}
#pragma mark - 删
// 删除线性表中指定索引位置的元素
void listRemove(LinearList* list, int index) {
if (NULL == list) {
return;
}
if (index < 0 || index > list->length - 1) {
return;
}
// 反向for循环挪动数据:从(index + 1)标识的位置开始直到表尾,每个数据依次向前挪动一个步长
for (int i = index + 1; i < list->length; i++) {
list->value[i - 1] = list->value[i];
}
/*
// 等效写法
for (int i = index; i < list->length - 1; i++) {
list->value[i] = list->value[i + 1];
}
*/
// 线性表的长度 - 1
list->length--;
}
#pragma mark - 改
// 修改线性表中指定位置的元素为指定的值
void listSet(LinearList* list, int index, LinearListNodeValue value) {
if (NULL == list) {
return;
}
if (index < 0 || index > list->length - 1) {
return;
}
list->value[index] = value;
}
#pragma mark - 查
// 获取线性表指定索引处元素的值
LinearListNodeValue listGet(LinearList* list, int index) {
if (NULL == list) {
return 0;
}
if (index < 0 || index > list->length - 1) {
return 0;
}
return list->value[index];
}
#pragma mark - 特殊功能
// 删除线性表中具有指定值的所有元素 - 代码简单,但是效率低(时间复杂度高)
void listRemoveValue_1(LinearList* list, LinearListNodeValue value) {
if (NULL == list) {
return;
}
// 遍历所有元素
for (int i = 0; i <= list->length - 1; i++) {
while (list->value[i] == value && i <= list->length - 1) {
listRemove(list, i);
}
}
}
// 删除线性表中具有指定值的所有元素 - 效率较高
void listRemoveValue_2(LinearList* list, LinearListNodeValue value) {
if (NULL == list) {
return;
}
// 遍历所有元素
int removeCount = 0;
for (int i = 0; i <= list->length - 1; i++) {
if (list->value[i] == value) {
removeCount++;
} else {
list->value[i - removeCount] = list->value[i];
}
}
// 将长度减去删除的个数
list->length -= removeCount;
}
// 打印线性表
void listPrint(LinearList* list) {
if (NULL == list) {
return;
}
printf("list{\n");
printf("\tlength = %d;\n", list->length);
printf("\tcapacity = %d;\n", list->capacity);
printf("\tvalue = [");
for (int i = 0; i <= list->length - 1; i++) {
printf("%p", list->value[i]);
if (i <= list->length - 2) {
printf(",");
}
}
printf("];\n\t}\n\n");
}
Person.h 和 Person.m 如下:
// -------------------------------- Person.h --------------------------------
#import
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
#pragma mark - 属性
// 姓名
@property (nonatomic, copy) NSString* name;
// 年龄
@property (nonatomic, assign) int age;
#pragma mark - 构造方法
+(instancetype)personWithName:(NSString *)name
age:(int)age;
-(instancetype)initWithName:(NSString *)name
age:(int)age;
#pragma mark - 对象方法
-(void)introduce;
@end
NS_ASSUME_NONNULL_END
// -------------------------------- Person.m --------------------------------
#import "Person.h"
@implementation Person
#pragma mark - 构造方法
+(instancetype)personWithName:(NSString *)name age:(int)age {
return [[self alloc] initWithName:name age:age];
}
-(instancetype)initWithName:(NSString *)name age:(int)age {
if (self = [super init]) {
self.name = name;
self.age = age;
}
return self;
}
#pragma mark - 对象方法
-(void)introduce {
NSLog(@"Hello, I am %@, I am %d year old!", self.name, self.age);
}
@end
main.m 如下:
#import
#import "HCGLinearList.h"
#import "Person.h"
// 线性表存储基本数据类型
void saveElementaryDemo() {
LinearList* list = listCreat(10);
// 基本数据类型,需要强转为 LinearListNodeValue(即 void*)类型后再保存到线性表里面
listAdd(list, (LinearListNodeValue)1);
listAdd(list, (LinearListNodeValue)2);
listAdd(list, (LinearListNodeValue)3);
listAdd(list, (LinearListNodeValue)4);
listPrint(list);
/* 打印结果:value[] 里面以 16进制保持着 0 1 2 3 4
list{
length = 5;
capacity = 10;
value = [0x0,0x1,0x2,0x3,0x4];
}
*/
listRelease(list);
list = NULL;
}
// 线性表存储指针类型
void savePointerDemo() {
LinearList* list = listCreat(10);
// Objective-C 的对象本质上(在底层)就是一个结构体的指针
Person* person0 = [Person personWithName:@"P0" age:0];
Person* person1 = [Person personWithName:@"P1" age:1];
Person* person2 = [Person personWithName:@"P2" age:2];
NSLog(@"%p,%p,%p", person0, person1, person2);
// 输出结果
// 0x1004b1620,0x1004b1bf0,0x1004b1c10
// __bridge 是给编译器看的,告诉编译器将 Objective-C 环境下的对象,变成 C 环境下的指针类型。
// 通过 __bridge 将 Objective-C 环境下的 Person 对象,转换为 C 环境下的 LinearListNodeValue(void*)类型
listAdd(list, (__bridge LinearListNodeValue)(person0));
listAdd(list, (__bridge LinearListNodeValue)(person1));
listAdd(list, (__bridge LinearListNodeValue)(person2));
listPrint(list);
/* 打印结果(value[] 里面保存的值,为 person0, person1, person2 的地址,即 value[] 里面保存的是指针):
list{
length = 3;
capacity = 10;
value = [0x1004b1620,0x1004b1bf0,0x1004b1c10];
}
*/
// 取出节点数据
Person* p = (__bridge Person *)(listGet(list, 0));
[p introduce];
// 打印结果:
// Hello, I am P0, I am 0 year old!
listRelease(list);
list = NULL;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// saveElementaryDemo();
// savePointerDemo();
}
return 0;
}
这里有个有趣的地方:
Objective-C 的 NSArray、NSMutableArray,可以直接存放对象,但是如果要存放基本数据类型,则需要对基本数据类型进行包装。
C 的 LinearList,可以直接存放指针类型,但是存放基本数据类型需要强转,存放 Objective-C 对象需要桥接。
int main(int argc, const char * argv[]) {
@autoreleasepool {
LinearList* list = listCreat(10);
listAdd(list, (LinearListNodeValue)0);
listAdd(list, (LinearListNodeValue)1);
listAdd(list, (LinearListNodeValue)2);
listAdd(list, (LinearListNodeValue)3);
listAdd(list, (LinearListNodeValue)4);
listPrint(list);
listRelease(list);
list = NULL;
}
return 0;
}
通过 LLDB 读取 list 的内存进行分析,我们可以看到:(lldb) x/32xb list
0x10059d8b0: 0x0a 0x00 0x00 0x00 0x05 0x00 0x00 0x00
0x10059d8b8: 0x90 0x6f 0x5a 0x00 0x01 0x00 0x00 0x00
0x10059d8c0: 0x2d 0x5b 0x4e 0x53 0x54 0x61 0x62 0x50
0x10059d8c8: 0x69 0x63 0x6b 0x65 0x72 0x56 0x69 0x65
(lldb) x/96xb 0x01005a6f90
0x1005a6f90: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x1005a6f98: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x1005a6fa0: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x1005a6fa8: 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x1005a6fb0: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x1005a6fb8: 0x74 0xa3 0x05 0x10 0x00 0x00 0x00 0x20
0x1005a6fc0: 0x66 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x1005a6fc8: 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00
0x1005a6fd0: 0x28 0x65 0x78 0x70 0x65 0x72 0x69 0x6d
0x1005a6fd8: 0x65 0x6e 0x74 0x61 0x6c 0x5f 0x6d 0x61
0x1005a6fe0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x1005a6fe8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
通过 LLDB 对 NSArray 的内存进行分析
main 函数中的代码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 注意:
// 1.字符串常量,在编译的那一刻,地址就确定了,不会再发生改变
// 2.根据字符串的这一特性,可以利用字符串的地址,反推 NSArray 存储数组元素的位置
NSString* name0 = @"jack";
NSString* name1 = @"jones";
NSString* name2 = @"hcg";
NSString* name3 = @"hzp";
NSArray* array = @[name0, name1, name2, name3];
NSLog(@"name0 = %p, name1 = %p, name2 = %p, name3 = %p", name0, name1, name2, name3);
NSLog(@"array = %@", array);
NSLog(@"array.count = %ld", array.count);
/* 输出结果如下:
name0 = 0x100002048, name1 = 0x100002068, name2 = 0x100002088, name3 = 0x1000020a8
array = (
jack,
jones,
hcg,
hzp
)
array.count = 4
*/
}
return 0;
}
通过 LLDB 读取 array 的内存进行分析,我们可以看到:
NSArray 存储数组对象的属性与元素的内存空间是连续的
我们可以猜测,NSArray 底层可能只通过一次 malloc 就一次性开辟了存储 数组对象的属性和元素的内存空间
而不是像 HCGLinearList 那样使用两次 malloc,一次用于开辟存储数组属性的内存空间,一次用于开辟存储数组元素的内存空间
这样做的好处是:少调用了一次 malloc 和 free,代码简洁,提高了性能
属性和元素放在连续的内存空间里,根据局部性原理,也可以提高性能
注意:可以通过反复修改不可变数组的长度再并运行,来确定:不可变数组在内存中的第二个8字节,是用来存储数组的 count 属性。
(lldb) x/64xb array
0x10060d5f0: 0x59 0xf4 0xbd 0x8c 0xff 0xff 0x1d 0x01
0x10060d5f8: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10060d600: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x10060d608: 0x68 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x10060d610: 0x88 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x10060d618: 0xa8 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x10060d620: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10060d628: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
(lldb) po 0x0100002048
jack
(lldb) po 0x0100002068
jones
(lldb) po 0x0100002088
hcg
(lldb) po 0x01000020a8
hzp
拓展
原本 NSArray 的 count 为只读属性,但是我们可以通过定位到 array 对象在内存中的地址,通过直接操作内存,来修改 array.count
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 注意:
// 1.字符串常量,在编译的那一刻,地址就确定了,不会再发生改变
// 2.根据字符串的这一特性,可以利用字符串的地址,反推 NSArray 存储数组元素的位置
NSString* name0 = @"jack";
NSString* name1 = @"jones";
NSString* name2 = @"hcg";
NSString* name3 = @"hzp";
NSArray* array = @[name0, name1, name2, name3];
NSLog(@"name0 = %p, name1 = %p, name2 = %p, name3 = %p", name0, name1, name2, name3);
NSLog(@"array = %@", array);
NSLog(@"array.count = %ld", array.count);
/* 输出结果:
name0 = 0x100002048, name1 = 0x100002068, name2 = 0x100002088, name3 = 0x1000020a8
array = (
jack,
jones,
hcg,
hzp
)
array.count = 4
**/
// 直接操作内存,修改 NSArray 的只读属性 count
// 注意:iOS 底层有防御性编程,不可变数组 array 初始化时的容量为4,即系统为不可变数组分配的内存空间大小为 4 * 8Byte = 32Byte
// 如果将 array.count 修改为小于等于 4 的数值,程序可以运行通过。
// 但是如果将 array.count 修改为 大于 4 的数值,由于运行时发生内存越界,程序会奔溃。
void* address = (__bridge void*)array;
*((long *)address + 1) = 2;
// 打印输出
NSLog(@"array.count = %ld", array.count);
NSLog(@"array = %@", array);
/* 输出结果:
array.count = 2
array = (
jack,
jones
)
**/
}
return 0;
}
修改后,通过 LLDB 读取 array 的内存地址,可以看到,存储 count 属性值的内存地址,已经被修改为 2(原先的值为 4)
(lldb) x/64xb array
0x10060beb0: 0x59 0xf4 0xbd 0x8c 0xff 0xff 0x1d 0x01
0x10060beb8: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10060bec0: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x10060bec8: 0x68 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x10060bed0: 0x88 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x10060bed8: 0xa8 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x10060bee0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10060bee8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Objective-C 中的数组
Objective-C 中的数组本身是一个对象,数组中存放的元素是对象的引用
Objective-C 中的数组分为:不可变数组(NSArray)和可变数组(NSMutableArray)
不可变数组(NSArray)初始化之后不能修改数组的内容
可变数组(NSMutableArray)初始化之后可以随时修改数组的内容(增加元素,删除元素,修改元素)
可变数组的容量:
通过 LLDB 对 NSMutableArray 的内存进行分析
main 函数中的代码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 打印字符串常量的地址
NSString* str = @"jack";
NSLog(@"str addr = %p", str);
// 创建可变数组
int capacity = 0;
NSMutableArray* mArr = [NSMutableArray arrayWithCapacity:capacity];
// 往数组里面添加元素
int total = 10;
for (int i = 0; i < total; i++) {
[mArr addObject:str];
}
}
return 0;
}
通过 LLDB 查看 mArr 的内存情况:
str addr = 0x100002048
(lldb) x/96xb mArr
0x100535720: 0x89 0xdd 0xbd 0x8c 0xff 0xff 0x1d 0x01
0x100535728: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x100535730: 0xc0 0x5a 0x53 0x00 0x01 0x00 0x00 0x00
0x100535738: 0x00 0x00 0x00 0x00 0x0a 0x00 0x00 0x00
0x100535740: 0x0b 0x00 0x00 0x00 0x0a 0x00 0x00 0x00
0x100535748: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x100535750: 0x2d 0x5b 0x4e 0x53 0x54 0x69 0x74 0x6c
0x100535758: 0x65 0x62 0x61 0x72 0x43 0x6f 0x6e 0x74
0x100535760: 0x61 0x69 0x6e 0x65 0x72 0x56 0x69 0x65
0x100535768: 0x77 0x20 0x74 0x69 0x74 0x6c 0x65 0x48
0x100535770: 0x65 0x69 0x67 0x68 0x74 0x54 0x6f 0x48
0x100535778: 0x69 0x64 0x65 0x49 0x6e 0x46 0x75 0x6c
发现:
字符串常量 str 的地址,并没有保存在可变数组 mArr 的堆区
我们推测,可变数组 mArr 应该是在堆区另外开辟了一段内存空间用于存储 mArr 的元素
在可变 mArr 的堆区,应该会有 8Byte 的内存空间,记录着指向 mArr 存储数组元素的堆区的首地址
观察可变 mArr 堆区的内存数据,发现:第 3 行的数值,非常像一个堆区的内存地址
因此,将第 3 行当成内存地址,并进行打印:
(lldb) x/96xb 0x0100535ac0
0x100535ac0: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x100535ac8: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x100535ad0: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x100535ad8: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x100535ae0: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x100535ae8: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x100535af0: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x100535af8: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x100535b00: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x100535b08: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00
0x100535b10: 0x03 0x3c 0x14 0x79 0xff 0x7f 0x00 0x00
0x100535b18: 0x9a 0x20 0x52 0xb8 0x00 0x00 0x00 0x00
我们看到了在 0x0100535ac0 这段内存地址中,存储着 10 个字符串常量 str 的地址
由此可以确定,可变数组 mArr 堆区的第 3 行(8Byte)记录着指向 存储数组元素的堆区的首地址
修改 main 函数中的变量 capacity 和 total,反复进行单步调试并通过 LLDB 打印 mArr 每次添加元素后堆区的内存变化
我们发现:
为了更好地分析 NSMutableArray 的扩容机制,编写 NSMutableArray 的分类 MemoryAnalyse 如下:
// -------------------------------- NSMutableArray+MemoryAnalyse.h --------------------------------
#import
NS_ASSUME_NONNULL_BEGIN
@interface NSMutableArray (MemoryAnalyse)
// 返回存储数组的元素的堆区的首地址
-(void *)elementHeapAddress;
// 获取当前数组的容量
-(int)getCurrentCapacity;
// 设置当前数组的容量
-(void)setCurrentCapacity:(int)capacity;
// 获取当前数组的元素个数
-(int)getCurrentCount;
// 设置当前数组的元素个数
-(void)setCurrentCount:(int)count;
@end
NS_ASSUME_NONNULL_END
// -------------------------------- NSMutableArray+MemoryAnalyse.m --------------------------------
#import "NSMutableArray+MemoryAnalyse.h"
@implementation NSMutableArray (MemoryAnalyse)
// 返回存储数组的元素的堆区的首地址
-(void *)elementHeapAddress {
// 获取数组对象的首地址
void* arrAddress = (__bridge void*)self;
// void* 为万能指针,可以指向任意数据类型,即 void*指针 的步长是不确定
// 因此,void* 类型的指针,不能直接通过 + 或 - 来移动指针
// void** 为 指向 void*类型 的指针,其步长为确定的 8Byte(因为 void* 占8个字节 )(因为在 64bit CPU 下,一个指针占 8Byte)。
// 根据之前的观察,NSMutableArray 的第 3 行是 存储数组的元素的堆区的首地址
// 偏移量为 3 * 8Byte = 24Byte,步数从 0 开始计算,void** 的步长为 8Byte,则有 24 / 8 - 1 = 2
void* elementHeapAddress = *((void **)arrAddress + 2);
return elementHeapAddress;
}
// 获取当前数组的容量
-(int)getCurrentCapacity {
// 获取数组对象的首地址
void* arrAddress = (__bridge void*)self;
// 根据之前的观察,NSMutableArray 的第 4 行的后 4 个 Byte 为 当前数组的容量
// 偏移量为 4 * 8Byte = 32Byte,步数从 0 开始计算,int* 步长为 4Byte,则有 32 / 4 - 1 = 7
return *((int *)arrAddress + 7);
}
// 设置当前数组的容量
-(void)setCurrentCapacity:(int)capacity {
// 获取数组对象的首地址
void* arrAddress = (__bridge void*)self;
// 根据之前的观察,NSMutableArray 的第 4 行的后 4 个 Byte 为 当前数组的容量
// 偏移量为 4 * 8Byte = 32Byte,步数从 0 开始计算,int* 步长为 4Byte,则有 32 / 4 - 1 = 7
*((int *)arrAddress + 7) = capacity;
}
// 获取当前数组的元素个数
-(int)getCurrentCount {
// 获取数组对象的首地址
void* arrAddress = (__bridge void*)self;
// 根据之前的观察,NSMutableArray 的第 5 行的后 4 个 Byte 为 当前数组的元素个数
// 偏移量为 5 * 8Byte = 40Byte,步数从 0 开始计算,int* 步长为 4Byte,则有 40 / 4 - 1 = 9
return *((int *)arrAddress + 9);
}
// 设置当前数组的元素个数
-(void)setCurrentCount:(int)count {
// 获取数组对象的首地址
void* arrAddress = (__bridge void*)self;
// 根据之前的观察,NSMutableArray 的第 5 行的后 4 个 Byte 为 当前数组的元素个数
// 偏移量为 5 * 8Byte = 40Byte,步数从 0 开始计算,int* 步长为 4Byte,则有 40 / 4 - 1 = 9
*((int *)arrAddress + 9) = count;
}
@end
在 main 函数中,编写代码,分析 NSMutableArray 的扩容机制:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 打印字符串常量的地址
NSString* str = @"jack";
NSLog(@"str addr = %p", str);
// 创建可变数组
int capacity = 10;
NSMutableArray* mArr = [NSMutableArray arrayWithCapacity:capacity];
// 打印可变数组扩容时机,扩容容量等
int total = 512;
int currentTimes = 0;
int preTimes = currentTimes;
void* currentElementHeapAddr = mArr.elementHeapAddress;
// 可变数组的容量,随着数组元素的增加而动态地增加
NSLog(@"Arr存储了[%03d]个元素时 currentElementHeapAddr = %p, currentCapacity = %d", 0, mArr.elementHeapAddress, [mArr getCurrentCapacity]);
for (currentTimes = 0; currentTimes < total; currentTimes++) {
[mArr addObject:str];
// 存储数组的元素的堆区的首地址发生改变 - 可变数组进行扩容
if (currentElementHeapAddr != mArr.elementHeapAddress) {
NSLog(@"currentElementHeapAddr = %p, currentTimes = %03d, capacity = %03d", mArr.elementHeapAddress, currentTimes, [mArr getCurrentCapacity]);
currentElementHeapAddr = mArr.elementHeapAddress;
preTimes = currentTimes;
}
}
// 可变数组的容量,随着数组元素的减少而动态地减少
NSLog(@"Arr存储了[%03d]个元素时 currentElementHeapAddr = %p, currentCapacity = %d", total, mArr.elementHeapAddress, [mArr getCurrentCapacity]);
for (currentTimes = 0; currentTimes < total; currentTimes++) {
[mArr removeLastObject];
// 存储数组的元素的堆区的首地址发生改变 - 可变数组进行扩容
if (currentElementHeapAddr != mArr.elementHeapAddress) {
NSLog(@"currentElementHeapAddr = %p, currentTimes = %03d, capacity = %03d", mArr.elementHeapAddress, currentTimes, [mArr getCurrentCapacity]);
currentElementHeapAddr = mArr.elementHeapAddress;
preTimes = currentTimes;
}
}
/* 打印输入如下:
str addr = 0x100003048
Arr存储了[000]个元素时 currentElementHeapAddr = 0x100751170, currentCapacity = 10
currentElementHeapAddr = 0x100405730, currentTimes = 010, capacity = 016
currentElementHeapAddr = 0x100751200, currentTimes = 016, capacity = 026
currentElementHeapAddr = 0x1007512d0, currentTimes = 026, capacity = 042
currentElementHeapAddr = 0x100507470, currentTimes = 042, capacity = 068
currentElementHeapAddr = 0x100507700, currentTimes = 068, capacity = 110
currentElementHeapAddr = 0x102808200, currentTimes = 110, capacity = 192
currentElementHeapAddr = 0x102808800, currentTimes = 192, capacity = 320
currentElementHeapAddr = 0x102809200, currentTimes = 320, capacity = 576
Arr存储了[512]个元素时 currentElementHeapAddr = 0x102809200, currentCapacity = 576
currentElementHeapAddr = 0x101009200, currentTimes = 291, capacity = 256
currentElementHeapAddr = 0x10066f0c0, currentTimes = 414, capacity = 098
currentElementHeapAddr = 0x10066eb50, currentTimes = 474, capacity = 038
currentElementHeapAddr = 0x10066efe0, currentTimes = 497, capacity = 014
currentElementHeapAddr = 0x10066f050, currentTimes = 506, capacity = 006
currentElementHeapAddr = 0x100661ec0, currentTimes = 509, capacity = 002
**/
}
return 0;
}
注意:
mArr[0] 是获取数组第一个元素的值,我们无法通过 mArr[0] 来获取 可变数组存储元素的堆区的首地址
并且我们不能通过常规手段,获取到可变数组存储元素的堆区的首地址
通过上面对 NSMutableArray 的内存进行分析,得到 Objective-C 对于 NSMutableArray 动态变更容量的思路:
按照 NSMutableArray 扩容的思路,优化 HCGLinearList 代码:
HCGLinearList.h 文件,相对于上面的代码,没有做任何改变:
#ifndef LinearList_h
#define LinearList_h
#include
#pragma mark - 宏定义(条件编译)
#ifndef LINEARLIST_STRUCT
// 线性表节点数据
typedef void* LinearListNodeValue;
// 线性表
typedef void LinearList;
#endif
#pragma mark - 创建 销毁 清空
// 创建线性表
// param0.线性表容量
// return.线性表指针
LinearList* listCreat(int capacity);
// 销毁线性表
// param0.线性表指针
void listRelease(LinearList* list);
// 清空线性表
// param0.线性表指针
void listClear(LinearList* list);
#pragma mark - 属性获取
// 获取线性表的长度
// param0.线性表指针
// return.线性表长度
int listLength(LinearList* list);
// 获取线性表容量
// param0.线性表指针
// return.线性表容量
int listCapacity(LinearList* list);
#pragma mark - 增
// 往线性表中插入数据
// param0.线性表指针
// param1.要插入的位置的索引
// param2.要插入的值
void listInsert(LinearList* list, int index, LinearListNodeValue value);
// 往线性表中添加数据(添加在表尾)
void listAdd(LinearList* list, LinearListNodeValue value);
#pragma mark - 删
// 删除线性表中指定索引位置的元素
// param0.线性表指针
// param1.索引
void listRemove(LinearList* list, int index);
#pragma mark - 改
// 修改线性表中指定位置的元素为指定的值
// param0.线性表指针
// param1.索引
// param2.值
void listSet(LinearList* list, int index, LinearListNodeValue value);
#pragma mark - 查
// 获取线性表指定索引处元素的值
// param0.线性表指针
// param1.索引
// return.元素的值
LinearListNodeValue listGet(LinearList* list, int index);
#pragma mark - 特殊功能
// 删除线性表中具有指定值的所有元素
// param0.线性表指针
// param1.要删除的值
void listRemoveValue_1(LinearList* list, LinearListNodeValue value);
// 删除线性表中具有指定值的所有元素
// param0.线性表指针
// param1.要删除的值
void listRemoveValue_2(LinearList* list, LinearListNodeValue value);
// 打印线性表
// param0.线性表指针
void listPrint(LinearList* list);
#endif /* LinearList_h */
HCGLinearList.c 文件,相对于上面的代码,做了三处更改:
#pragma mark - 定义线性表结构体
// 数据节点指针
typedef void* LinearListNodeValue;
// 线性表结构体
typedef struct {
int capacity; // 容量
int length; // 长度
LinearListNodeValue* value; // 节点数据的指针(相当于 void** value,指向指针的指针)
} LinearList;
/*
注意:宏 LINEARLIST_STRUCT 的定义,一定要在导入 HCGLinearList.h 之前
这是为了防止上面的 LinearListNodeValue 指针和 LinearList 结构体被重复定义
**/
//
#define LINEARLIST_STRUCT
#pragma mark - 导入头文件
#include "HCGLinearList.h"
// 使用 mallc函数 需要导入
#include
// 使用内存拷贝函数 memcpy 需要导入
#include
#pragma mark - 创建 销毁 清空
// 创建线性表
LinearList* listCreat(int capacity) {
if (capacity < 0) {
return NULL;
}
// 分配线性表结构体的内存空间(在堆区)
// malloc函数如果遇到内存资源紧张,给不了这么多字节,可能会返回空
LinearList* list = malloc(sizeof(LinearList));
if (list) {
list->capacity = capacity;
list->length = 0;
// 分配存储线性表元素的内存空间
list->value = (list->capacity == 0 ? NULL : malloc(capacity * sizeof(LinearListNodeValue)));
if (!list->value) {
return NULL;
}
}
return list;;
}
// 销毁线性表
void listRelease(LinearList* list) {
if (NULL == list) {
return;
}
if (list->value) {
free(list->value);
}
free(list);
}
// 清空线性表
void listClear(LinearList* list) {
if (NULL == list) {
return;
}
// 这里不需要对线性表中的元素都置0
// 只要将线性表的长度置为0,下次使用线性表的时候,就会对之前的数据进行覆盖了
list->length = 0;
}
#pragma mark - 属性获取
// 获取线性表的长度
int listLength(LinearList* list) {
if (NULL == list) {
return 0;
}
return list->length;
}
// 获取线性表容量
int listCapacity(LinearList* list) {
if (NULL == list) {
return 0;
}
return list->capacity;
}
#pragma mark - 增
// 往线性表中插入数据
void listInsert(LinearList* list, int index, LinearListNodeValue value) {
if (NULL == list) {
return;
}
// 可以在表尾进行插入,因此这里的条件是 index > list->length,而不是 index >= list->length
if (index < 0 || index > list->length) {
return;
}
// 判断是否需要扩容
if (list->length == list->capacity) {
// 1.申请新的堆空间
int tempCapacity = list->capacity + 10;
LinearListNodeValue* tempValue = malloc(sizeof(LinearListNodeValue) * tempCapacity);
if (NULL == tempValue) {
return;
}
// 2.将旧的堆空间的数据 复制到 新的堆空间
/*
for (int i = 0; i < list->capacity; i++) {
tempValue[i] = list->value[i];
}
*/
// param0.目标地址
// param1.来源地址
// param2.要拷贝的字节数
memcpy(tempValue, list->value, sizeof(LinearListNodeValue) * list->capacity);
// 3.释放旧的堆空间
free(list->value);
list->value = tempValue;
// 4.修改capacity
list->capacity = tempCapacity;
}
// 反向for循环挪动数据:从表尾数据开始挪动直到index标识的位置,每个数据依次向后挪动一个步长
for (int i = list->length - 1; i >= index; i--) {
list->value[i + 1] = list->value[i];
}
// 将新值value插入到index的位置
list->value[index] = value;
// 线性表的长度 + 1
list->length++;
}
// 往线性表中添加数据(添加在表尾)
void listAdd(LinearList* list, LinearListNodeValue value) {
if (NULL == list) {
return;
}
listInsert(list, list->length, value);
}
#pragma mark - 删
// 删除线性表中指定索引位置的元素
void listRemove(LinearList* list, int index) {
if (NULL == list) {
return;
}
if (index < 0 || index > list->length - 1) {
return;
}
// 反向for循环挪动数据:从(index + 1)标识的位置开始直到表尾,每个数据依次向前挪动一个步长
for (int i = index + 1; i < list->length; i++) {
list->value[i - 1] = list->value[i];
}
/*
// 等效写法
for (int i = index; i < list->length - 1; i++) {
list->value[i] = list->value[i + 1];
}
*/
// 线性表的长度 - 1
list->length--;
}
#pragma mark - 改
// 修改线性表中指定位置的元素为指定的值
void listSet(LinearList* list, int index, LinearListNodeValue value) {
if (NULL == list) {
return;
}
if (index < 0 || index > list->length - 1) {
return;
}
list->value[index] = value;
}
#pragma mark - 查
// 获取线性表指定索引处元素的值
LinearListNodeValue listGet(LinearList* list, int index) {
if (NULL == list) {
return 0;
}
if (index < 0 || index > list->length - 1) {
return 0;
}
return list->value[index];
}
#pragma mark - 特殊功能
// 删除线性表中具有指定值的所有元素 - 代码简单,但是效率低(时间复杂度高)
void listRemoveValue_1(LinearList* list, LinearListNodeValue value) {
if (NULL == list) {
return;
}
// 遍历所有元素
for (int i = 0; i <= list->length - 1; i++) {
while (list->value[i] == value && i <= list->length - 1) {
listRemove(list, i);
}
}
}
// 删除线性表中具有指定值的所有元素 - 效率较高
void listRemoveValue_2(LinearList* list, LinearListNodeValue value) {
if (NULL == list) {
return;
}
// 遍历所有元素
int removeCount = 0;
for (int i = 0; i <= list->length - 1; i++) {
if (list->value[i] == value) {
removeCount++;
} else {
list->value[i - removeCount] = list->value[i];
}
}
// 将长度减去删除的个数
list->length -= removeCount;
}
// 打印线性表
void listPrint(LinearList* list) {
if (NULL == list) {
return;
}
printf("list{\n");
printf("\tlength = %d;\n", list->length);
printf("\tcapacity = %d;\n", list->capacity);
printf("\tvalue = [");
for (int i = 0; i <= list->length - 1; i++) {
printf("%p", list->value[i]);
if (i <= list->length - 2) {
printf(",");
}
}
printf("];\n\t}\n\n");
}
main.c 文件,测试 HCGLinearList 的扩容功能:
#import
#import "HCGLinearList.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建线性表
LinearList* list = listCreat(5);
// 添加数据
int total = 20;
for (int i = 0; i < total; i++) {
listAdd(list, (LinearListNodeValue)i);
NSLog(@"list->length = %02d, list->value = %p", listLength(list), *((void**)list + 1));
}
// 打印
listPrint(list);
// 销毁线性表
listRelease(list);
list = NULL;
/** 输出结果如下:
list->length = 01, list->value = 0x10064b520
list->length = 02, list->value = 0x10064b520
list->length = 03, list->value = 0x10064b520
list->length = 04, list->value = 0x10064b520
list->length = 05, list->value = 0x10064b520
list->length = 06, list->value = 0x100407b40
list->length = 07, list->value = 0x100407b40
list->length = 08, list->value = 0x100407b40
list->length = 09, list->value = 0x100407b40
list->length = 10, list->value = 0x100407b40
list->length = 11, list->value = 0x100407b40
list->length = 12, list->value = 0x100407b40
list->length = 13, list->value = 0x100407b40
list->length = 14, list->value = 0x100407b40
list->length = 15, list->value = 0x100407b40
list->length = 16, list->value = 0x100408d40
list->length = 17, list->value = 0x100408d40
list->length = 18, list->value = 0x100408d40
list->length = 19, list->value = 0x100408d40
list->length = 20, list->value = 0x100408d40
list{
length = 20;
capacity = 25;
value = [0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0x9,0xa,0xb,0xc,0xd,0xe,0xf,0x10,0x11,0x12,0x13];
}
*/
}
return 0;
}