有关NSDictionary的内存布局,可以参看《NSDictionary 的内存布局》。

1 类图

和《NSDictionary 的内存布局》中的类图相比较,本章类图多了2个新成员:

__NSDictionaryM

__NSCFDictionary

2 __NSDictionaryM

通过下面的方式,可以创建__NSDictionaryM:

NSMutableDictionary *dictM = [NSMutableDictionary dictionary];

NSMutableDictionary  *dict = [NSMutableDictionary dictionaryWithDictionary:@{"kaaa": @"aaa"}];

Xcode的控制台输出可以看到:

(lldb) po [dictM class]
__NSDictionaryM

2.1 初始化

__NSDictionaryM的初始化流程和__NSDictionaryI类似。

当调用+[NSMutableDictionary dictionaryWithDictionary:]方法时,最终会调用到-[__NSPlaceholderDictionary initWithObjects:forKeys:count]方法。

-[__NSPlaceholderDictionary initWithObjects:forKeys:count]方法在NSDictionary部分已经介绍过。

这里重新贴出与__NSDictionaryM相关的伪代码:

// -[__NSPlaceholderDictionary initWithObjects:forKeys:count]

@interface __NSPlaceholderDictionary

...

@end

@implementation __NSPlaceholderDictionary

- (instancetype)initWithObjects:(ObjectType const[])objects forKeys:(ObjectTpye const[])keys count:(NSUInteger)count {
...
label:
if (self == ___immutablePlaceholderDictionary) {
...
} else if (self == ___mutablePlaceholderDictionary) {
// 创建 __NSDictionaryM
return __NSDictionaryM_new(keys, objecs, count, 3);
} error "创建出错"
}

从伪代码可以看到,最终会调用到__NSDictionaryM_new方法。

下面就来看看__NSDictionaryM_new的内部实现。

和创建__NSDictionaryI对象一样,__NSDictionaryM_new一开始也需要遍历__NSDictionaryCapacities数组。

遍历的目的,同样是为了找到一个index,这个index对应的capacity大于或者等于count

BOOL found = NO;
NSInteger index = 0;
for (; index < 40; index++) {
if (__NSDictionaryCapacity[i] >= count) {
found = YES;
break;
}
} if (!found) {
error "不能创建 NSDictionary";
}

从上面伪代码可以看到,创建__NSDictionaryI最多遍历64项,而这里只遍历40项。

有了index,就可以从__NSDictionarySizes数组中,得到要创建的字典的size

NSUInteger size = __NSDictionarySizes[index];

有了要创建字典的size,接下来就要创建__NSDictionaryM对象:

__NSDictionaryM *dictM = __CFAllocateObject(__NSDictionaryM.class, 0);

还记得创建__NSDictionaryI的代码吗?

__NSDictionaryI *dictI = __CFAllocateObject(__NSDictionaryM.class, size * 8 * 2);

可以看到,在创建__NSDictionaryM对象时,并没有传入size信息。

这就是说,key-value对,不是保存在__NSDictionaryM本身中。

这个很好理解。

因为__NSDictionaryM可以动态的增加key-value对,而不像__NSDictionaryI一样,创建好之后就不能再变化了。

既然__NSDictionaryMkey-value对不存储在自身,那么肯定存在堆上的另外地方。

malloc_type_calloc方法正是用来分配这块内存的。

malloc_type_calloc的方法声明如下:

void *malloc_type_calloc(size_t num_items, size_t size malloc_type_id_t type_id);

__NSDictionaryM_new内部调用malloc_type_calloc的方式为:

void *storage = malloc_type_calloc(1, size * 8 * 2, 0x8448092b);

从代码可以看到,malloc_type_calloc创建了1item,这个item的大小是size * 8 * 2

毫无疑问,创建出来的storage正是用来存储key-value对的。

storage指针存储在__NSDictionaryM对象中,内存布局如下:

从上面的内存布局图可以看到,创建的存储区域分位2个数组。

key-value对中的key存储在第1个数组中。

key-value对中的value存储在第2个数组中。

为了存储key-value对,会遍历__NSDictionaryM_new函数的keys数组参数。

针对keys数组中的每一个key,计算其hash值。

for (NSInteger i = 0; i < count; i++) {
ObjectType key = keys[i];
NSUInteger hashValue = [key hash];
}

计算出hash值之后,对其进行取余计算,取余的结果作为storage.keys数组中的索引:

for (NSInteger i = 0; i < count; i++) {
ObjectType key = keys[i];
NSUInteger hashValue = [key hash];
NSInteger index = hashValue % size;
}

有了这个索引index,就可以读取storage.keys数组中的值:

for (NSInteger i = 0; i < count; i++) {
ObjectType key = keys[i];
NSUInteger hashValue = [key hash];
NSInteger index = hashValue % size;
ObjectType oldKey = storage.keys[index];
}

oldKey的值会有3种情形。

1种情形,是oldKey的值为nil,说明这个位置之前没有值,可以放心将key-value对存入:

for (NSInteger i = 0; i < count; i++) {
ObjectType key = keys[i];
Objecttype value = values[i];
NSUInteger hashValue = [key hash];
NSInteger index = hashValue % size;
ObjectType oldKey = storage.keys[index];
if (oldKey == nil) {
storage.keys[index] = [key copyWithZone:nil];
storage.values[index] = value;
}
}

上面伪代码需要注意的时,存储key是,调用了copyWithZone:方法。

因此,要做字典的Key,必须遵循copy协议。

__NSDictionaryM对象上,有25 bit记录存储的key-value对个数。

在这种情形下,这个值会加1

2种情形,是oldKey的值为___NSDictionaryM_DeletedMarker

___NSDictionaryM_DeletedMarker是一个特殊的对象,它是一个NSObject

 0x18052c7ac <+280>: add    x21, x21, #0x420 ; ___NSDictionaryM_DeletedMarker
0x18052c7b0 <+284>: ldr x8, [sp, #0x30]

Xcodelldb控制台上输出:

(lldb) po $x21
<NSObject: 0x1e3db2420>

有关__NSDictionaryM_DeletedMarker在介绍removeObjectForKey:方法时会继续介绍。

此时,如果oldKey是一个__NSDictionaryM_DeletedMarker,那么就顺着storage.keys数组当前的位置往前继续查找,直到查找完storage.keys数组中的所有位置。

如果查找过程中找到了一个oldKeynil的位置,那么就将key-value对放到这个位置。

同时,__NSDictionaryM对象中,记录存储key-value对个数的值加1

如果遍历的过程中,找到了一个oldKey是一个普通对象,那么就是情形3了。

3种情形,如果oldKey是一个普通的对象,那么就检测keyoldKey是否是同一个对象,或者它们的isEqual方法是否相等:

key == oldKey || [oldKey isEqual:key]

如果它们是同一个对象,或者isEqual方法相等,那么将value直接覆盖oldKey对应的oldValue值。

注意,此时__NSDictionaryM对象中,记录存储key-value对个数的值不会有变化。

如果keyoldKey既不是同一个对象,它们的isEqual方法也不相等,那么就顺着当前storage.keys数组的位置往前找,直到遍历所有storage.keys数组的位置。

此时的情形和遇到__NSDictionaryM_DeletedMarker完全一样。

2.2 内存布局

cowCopy On Write的缩写,再字典拷贝操作中有用,这里先不用关心。

2.3 objectForKey:

有了上面的内存布局,objectForKey:方法就很容易理解了。

首先根据参数key计算其hash值,并对hash值进行取余计算:

NSUInteger hashValue = [key hash];
NSIndex index = hashValue % size;

size是从哪里获取的呢?

从上面内存布局图可以知道,__NSDictionaryM对象有6 bit记录size的索引。

有了这个索引,就可以轻松的从__NSDictionarySizes数组中获取到对应的size值了。

通过hash值计算出index后,将这个index作为storage.keys数组的索引,

读取一个值candidateKey

此时也有3种情形。

情形1,如果candidateKey的值是nil,说明这个key在字典中没有对应的value,直接返回nil

情形2,如果candidateKey是一个___NSDictionaryM_DeletedMarker对象,那么就从storage.keys数组的当前位置顺序向前找,直到遍历完所有storage.keys中的位置。

如果遍历的过程中,找到了一个candidateKeynil,那么就直接返回nil

如果遍历的过程中,找到了一个普通对象,那么就是情形3了。

情形3,如果candidateKey是一个普通对象,那么就检测它们是否是同一个对象,或者isEqual方法是否相等:

candidateKey == key || [candidateKey isEqual:key]

如果满足上面的条件,就直接将candidateKey对应的value返回。

如果不满足上面的条件,那么就从storage.keys数组的当前位置顺序向前找,直到遍历完所有storage.keys中的位置。

如果遍历完所有位置,都没有找到合适的candidateKey,那么就返回nil

2.4 setObject:forKey:

setObject:forKey方法首先根据参数key,计算其hash值。

根据hash值可以得到storage.keys数组中的索引,然后读取这个索引对应的值oldKey

此时会有3种情形。

情形1,如果运气不错,oldKeynil,那么说明这个位置没有被占用,直接将key-value对添加进去。

同时,__NSDictionaryM对象中记录存储key-value对个数的值会加1

情形2,如果运气太差,oldKey是一个___NSDictionaryM_DeletedMarker,那么就从storage.keys数组的当前位置顺序向前找,直到遍历完所有storage.keys中的位置。

如果再查找的过程中,找到了一个没有被占用的位置,并不能直接将key-value对添加进去。

此时,需要判断查找的次数是否大于16次。

如果查找次数不大于16次,那么就直接添加key-value对:

如果大于16次,需要对整个storage数组进行重新哈希,避免频繁遇到___NSDictionaryM_DeletedMarker,造成频繁查找。

重新进行哈希,会创建新的storage数组,旧storage数组中的___NSDictionaryM_DeletedMarker不会存到新storage数组中。

从图中可以看到,重新哈希之后,新storage数组中的key-value对顺序,可能和旧storage数组中不一样。

重新哈希之后,需要重新计算参数keyhash值,重复上面的步骤。

如果查找过程中,oldKey是一个普通对象,那么就会遇到情形3

情形3,如果oldKey是一个普通对象,那么就检测oldKeykey是否是同一个对象,或者它们的isEqual方法是否相等:

oldKey == key || [oldKey isEqual:key]

如果满足条件,直接将oldKey对应的的值覆盖成参数value

此时,__NSDictionaryM对象中记录存储key-value对的值不会变化。

如果不满足条件,也就是oldKey与参数key既不是同一个对象,它们的isEqual方法也不相等。

那么,就从storage.keys数组的当前位置顺序向前找,直到遍历完所有storage.keys中的位置。

整个流程和情形2完全一样。

需要注意的是,判断是否重新哈希的查找次数,是累计情形2和情形3的。

比如查找过程中遇到了一个___NSDictionaryM_DeletedMarker对象,那么查找计数加1

紧接着查找,遇到了一个普通对象不满足:

oldKey == key || [oldKey isEqual:key]

那么查找次数也要加1

最后,如果遍历了当前storage.keys的所有位置,都没有找到合适的位置,那么将当前字典的size索引加1作为新的索引,从__NSDictionarySizes数组中得到一个新的size

获取到新size之后,使用这个新size创建一个新的storage数组,然后将旧storage数组中的key-value对重新哈希到新storage数组中。

重新哈希之后,重头计算参数key的哈希值以及在新storage数组中的索引,重复上面步骤。

由于新storage数组发生了变化,根据参数key计算的索引值也可能会发生变化。

需要注意的是,只要set操作成功,就会触发根据__NSDictionaryM对象中的KVO标志,触发KVO:

[self willChangeValueForKey:key];
// set key-value 对
[self didChangeValueForKey:key];

2.5 removeObjectForKey:

要进行删除操作,首先要看storage数组中,是否存在需要被删除的目标targetKey

要成为targetKey,需要满足下面的条件:

targetKey == key || [targetKey isEqual:key]

也就是说,目标targetKey要么和参数key是同一个对象,要么它们的isEqual方法相等。

要找到targetKey,会有一个查找过程。

查找过程和setObject:forKey:方法中的一样。

查找过程中也会记录查找的次数。

如果找到了targetKey,那么就使用___NSDictionaryM_DeletedMarker对象覆盖targetKey的值。

也就是说,___NSDictionaryM_DeletedMarker对象是删除操作产生的。

同时,需要将targetKey对应的valuenil

但是,事情远远还没有结束。

删除完之后,还得看查找次数是否大于16次

如果查找大于16次,需要将删除后的storage数组重新进行哈希操作。

重新哈希会产生新的storage数组,并且新的storage数组里面不会有___NSDictionaryM_DeletedMarker对象。

如果查找次数不超过16次,还需要检测被覆盖的targetKey所处位置的前一个位置的值。

如果前一个位置的值既不是一个___NSDictionaryM_DeletedMarker,也不是一个普通对象,而是nil,那么就会有一个清除___NSDictionaryM_DeletedMarker对象的操作。

清除过程从当前targetKey所处位置开始,向后遍历storage.keys数组,将碰到的___NSDictionaryM_DeletedMarker对象全部置成nil,直到遇到一个非___NSDictionaryM_DeletedMarker对象。

这个对象可以是nil,也可以是普通对象。

如果删除操作发生了,就会根据__NSDictionaryM对象中的KVO标志,触发KVO:

[self willChangeValueForKey:key];
// 删除操作
[self didChangeValueForKey:key];

为什么删除的时候,需要一个___NSDictionaryM_DeletedMarker对象来进行占位呢?

因为有可能有2key:key1key2

2keyhash值一样,但是isEqual方法不相等:

[key1 hash] == [key2 hash] && ![key1 isEqual:key2]

那么根据前面的分析,这2key都可以通过setObject:forKey:的方法添加到字典中。

如果此时删除key1,直接将它在storage.keys数组中的所在位置置成nil,那么当在key2上调用objectForKey:就会出问题。

因为key2key1hash值一样,计算出来的storage.keys数组索引也一样。

此时由于这个索引对应的值为nil,就会错误的返回nil给用户,而不是正确的值。

3 __NSCFDictionary

__NSCFDictionary字典是一个很奇怪的可变字典。

虽然它是可变的,但是如果使用不正确,就会造成崩溃。

通过下面的方式可以创建一个__NSCFDictionary字典:

// 创建一个可变字典
CFMutableDictionaryRef mutableDict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, NULL, NULL);

通过Xcodelldb控制台输出可以看到:

(lldb) po [mutableDict class]
__NSCFDictionary
(lldb) p (BOOL)[mutableDict isKindOfClass:NSMutableDictionary.class]
(BOOL) YES
(lldb) p (BOOL)[mutableDict respondsToSelector:@selector(setObject:forKey:)]
(BOOL) YES

从控制台的输出可以看到,__NSCFDictionary字典是一个可变字典。

同时,这个可变字典也有setObject:forKey:方法。

下面我们对这个字典进行copy操作:

NSDictionary *dict = [(__bridge NSMutableDictionary *)mutableDict copy];

按照道理,调用copy方法之后,应该返回的是一个非可变字典,但是如果打印dict的类型,发现仍然是__NSCFDictionary:

(lldb) po [dict class]
__NSCFDictionary

如果我们使用isKindOfClass:方法对其进行判断,然后强转成NSMutableDictionary执行setObject:forKey:方法,就会发生崩溃:

 if ([dict isKindOfClass:NSMutableDictionary.class]) {
[(NSMutableDictionary *)dict setObject:@"hh" forKey:@"cc"];
}

崩溃信息为:

Thread 1: "-[__NSCFDictionary setObject:forKey:]: mutating method sent to immutable object"

为了搞清楚原因,我们首先得从CFDictionaryCreateMutable函数入手。

CFDictionaryCreateMutable函数的汇编代码如下:

CoreFoundation`CFDictionaryCreateMutable:
...
// 1. 调用 __NSCFDictionaryCreateMutable 方法
0x1803d53c4 <+36>: bl 0x180529394 ; __NSCFDictionaryCreateMutable
0x1803d53c8 <+40>: mov x19, x0
0x1803d53cc <+44>: cbnz x0, 0x1803d5430 ; <+144>
...
// 2. 调用 __CFDictionaryCreateGeneric
0x1803d53dc <+60>: bl 0x1803d52e8 ; __CFDictionaryCreateGeneric
...
// 3. 设置 isa 为 __NSCFDictionary
0x1803d540c <+108>: bl 0x18041e80c ; _CFRuntimeSetInstanceTypeIDAndIsa

从汇编代码可以知道,CFDictionaryCreateMutable内部会调用2个函数创建字典。

首先调用__NSCFDictionaryCreateMutable方法,调用的方式为:

__NSCFDictionaryCreateMutable(kCFAllocatorDefault, 0, NULL, NULL);

这个方法的汇编代码如下:

CoreFoundation`__NSCFDictionaryCreateMutable:
...
// 1. 检测第 3 个参数
0x180529418 <+132>: add x8, x8, #0x948 ; kCFTypeDictionaryValueCallBacks
0x18052941c <+136>: cmp x21, x9
0x180529420 <+140>: b.ne 0x180529434 ; <+160>
...
// 2. 检测第 4 个参数
0x180529434 <+160>: adrp x9, 407675
0x180529438 <+164>: add x9, x9, #0x918 ; kCFCopyStringDictionaryKeyCallBacks
...
// 3. 熟悉的 __NSDictionaryM_new 方法
0x18052946c <+216>: b 0x18052c694 ; __NSDictionaryM_new
// 4. 返回 nil
0x180529470 <+220>: mov x0, #0x0 ; =0
0x180529474 <+224>: ldp x29, x30, [sp, #0x30]
0x180529478 <+228>: ldp x20, x19, [sp, #0x20]
0x18052947c <+232>: ldp x22, x21, [sp, #0x10]
0x180529480 <+236>: ldp x24, x23, [sp], #0x40
0x180529484 <+240>: ret
...

由于调用__NSCFDictionaryCreateMutable时,第3个参数和第4个参数传的都是NULL,因此程序直接跳转到代码注释4处执行。

也就是跳过了我们熟悉的__NSDictionaryM_new方法,失去了创建OC可变字典的机会,直接返回nil

由于__NSCFDictionaryCreateMutable方法返回nil__CFDictionaryCreateGeneric方法得到执行。

__CFDictionaryGeneric方法的汇编代码如下:

CoreFoundation`__CFDictionaryCreateGeneric:
...
// 1. 调用 CFBasicHashCreate 方法
0x1803d5374 <+140>: bl 0x1804ebe30 ; CFBasicHashCreate

可以看到__CFDictionaryGeneric方法直接调用了CFBasicHashCreate方法。

这个方法会创建一个CFBasichash对象,是一个CF类型:

(lldb) po $x0
<CFBasicHash 0x60000174c680 [0x1e3b3b680]>{type = mutable dict, count = 0,
entries =>
}

创建完毕之后,CFDictionaryCreateMutable方法在代码注释3处调用了_CFRuntimeSetInstanceTypeIDAndIsa方法。

_CFRuntimeSetInstanceTypeIDAndIsa方法将CFBasicHashisa设置成__NSCFDictionary

这样这个CF对象就能桥接成OC对象了,但它本质上还是一个CF对象。

3.1 copy

那为什么调用copy方法,返回的字典还是一个可变的呢?

原因是__NSCFDictionary重写了copyWithZone:方法。

__NSCFDictionarycopyWithZone:方法汇编代码如下:

CoreFoundation`-[__NSCFDictionary copyWithZone:]:
// 1. 检测当前对象是不是 OC 里面的可变字典
0x1803e3d14 <+32>: bl 0x1803d5e88 ; _CFDictionaryIsMutable
0x1803e3d18 <+36>: cbz w0, 0x1803e3d30 ; <+60>
...
// 2. 调用 CFDictionaryCreateCopy
0x1803e3d2c <+56>: b 0x1803d5448 ; CFDictionaryCreateCopy

代码注释1,检测当前对象是否是一个OC的可变字典。

很明显,当前对象是一个CF对象,只是能桥接为OC对象,因此检测不成立。

代码注释2,调用CFDictionaryCreateCopy方法进行拷贝。

这个方法拷贝出来的仍是一个__NSCFDictionary对象,其汇编代码如下:

CoreFoundation`CFDictionaryCreateCopy:
...
// 1. 拷贝当前对象
0x1803d5484 <+60>: bl 0x1804ec1b8 ; CFBasicHashCreateCopy
...
0x1803d54b0 <+104>: mov x0, x19
0x1803d54b4 <+108>: mov w1, #0x12 ; =18
// 2. 设置拷贝出来的对象的 isa 为 __NSCFDictionary
0x1803d54b8 <+112>: bl 0x18041e80c ; _CFRuntimeSetInstanceTypeIDAndIsa

3.3 setObject:forKey:

那为什么强转成可变字典,调用setObject:forKey:方法会发生崩溃呢?

下面就来看下setObject:forKey:方法的汇编代码:

CoreFoundation`-[__NSCFDictionary setObject:forKey:]:
...
// 1. 检测当前对象是否是 OC 的可变字典
0x1803e3968 <+44>: bl 0x1803d5e88 ; _CFDictionaryIsMutable
0x1803e396c <+48>: tbz w0, #0x0, 0x1803e39ec ; <+176>
...
0x1803e39ec <+176>: mov x0, x19
0x1803e39f0 <+180>: mov x1, x21
// 2. 检测失败会执行到这里
0x1803e39f4 <+184>: bl 0x18053d8ac ; -[__NSCFDictionary setObject:forKey:].cold.1
...

代码注释1,检测当前对象是否是OC的可变字典。

很明显,当前对象是一个CF类型,不是一个OC对象,检测失败。

代码注释2,检测失败后,会指向到这里。

-[__NSCFDictionary setObject:forKey:].cold.1看名字就知道不简单。

它的汇编代码如下:

CoreFoundation`-[__NSCFDictionary setObject:forKey:].cold.1:
...
0x18053d8c0 <+20>: add x8, x8, #0xeb8 ; NSInternalInconsistencyException
...
0x18053d8d4 <+40>: add x1, x1, #0x700 ; @"%@: mutating method sent to immutable object"
...

从代码上看,正是这个函数抛出了异常。

3.4 isKindOfClass:

__NSCFDictionary字典虽然是一个可变字典,通过了isKindOfClass:方法检测,但是确不能强转着使用。

苹果文档中,关于isKindOfClass:对类簇的讨论,到这里,才变得十分具体:

Be careful when using this method on objects represented by a class cluster. Because of the nature of class clusters, the object you get back may not always be the type you expected. If you call a method that returns a class cluster, the exact type returned by the method is the best indicator of what you can do with that object

NSMutableDictionary 的内存布局的更多相关文章

  1. 图说C++对象模型:对象内存布局详解

    0.前言 文章较长,而且内容相对来说比较枯燥,希望对C++对象的内存布局.虚表指针.虚基类指针等有深入了解的朋友可以慢慢看. 本文的结论都在VS2013上得到验证.不同的编译器在内存布局的细节上可能有 ...

  2. C++ 系列:内存布局

    转载自http://www.cnblogs.com/skynet/archive/2011/03/07/1975479.html 为什么需要知道C/C++的内存布局和在哪可以可以找到想要的数据?知道内 ...

  3. C++类内存布局图(成员函数和成员变量分开讨论)

    一.成员函数 成员函数可以被看作是类作用域的全局函数,不在对象分配的空间里,只有虚函数才会在类对象里有一个指针,存放虚函数的地址等相关信息. 成员函数的地址,编译期就已确定,并静态绑定或动态的绑定在对 ...

  4. 根据内存布局定位的一个fastdfs坑

    在使用fastdfs时,编写数据上传代码时,遇到一个坑.最终根据指针对应的内存布局定位到一个其client API的一个坑,值得记录一下.具体是在 tracker_connect_server() 这 ...

  5. c++ 对象的内存布局

    之前介绍过了普通对象比如系统自带的int等对象的对齐方式,在学习类型转换的时候遇到了自定义类型的继承体系中的downcast与upcast. 于是顺藤摸瓜,摸到了这里.发现还是 陈皓的博客里面写的最早 ...

  6. C++使用继承时子对象的内存布局

    C++使用继承时子对象的内存布局 // */ // ]]>   C++使用继承时子对象的内存布局 Table of Contents 1 示例程序 2 对象的内存布局 1 示例程序 class ...

  7. .NET对象的内存布局

    每个虚拟机都有它自己的对象布局,本文我们将针对sscli源码和windbg调试器来查看不同类型的.net对象布局. 在.net虚拟机里,每个对象都需要保存这些信息: 对象的类型: 对象实例的成员属性( ...

  8. c/c++ 对象内存布局

    一.对象内存查看工具 VS 编译器 CL 的一个编译选项可以查看 C++ 类的内存布局,非常有用.使用如下,从开始程序菜单找到 Visual Stdio 2012. 选择 VS 的命令行工具,按如下格 ...

  9. C++ Data Member内存布局

    如果一个类只定义了类名,没定义任何方法和字段,如class A{};那么class A的每个实例占用1个字节的内存,编译器会会在这个其实例中安插一个char,以保证每个A实例在内存中有唯一的地址,如A ...

  10. C++中派生类对象的内存布局

    主要从三个方面来讲: 1 单一继承 2 多重继承 3 虚拟继承 1 单一继承 (1)派生类完全拥有基类的内存布局,并保证其完整性. 派生类可以看作是完整的基类的Object再加上派生类自己的Objec ...

随机推荐

  1. [tldr]windows使用scoop安装make工具辅助程序编译

    make是一个好用的GNU工具,用来辅助我们进行自动化的程序编译,只需要一个Makefile文件,即可实现一行指令自动编译 scoop是windows的一个包管理工具 安装 scoop bucket ...

  2. bs4库爬取天气预报

    Python不仅用于网站开发,数据分析,图像处理,也常用于爬虫技术方向,最近学习了解下,爬虫技术入门一般先使用bs4库,爬取天气预报简单尝试下. 第一步:首先选定目标网站地址 网上查询,天气预报准确率 ...

  3. BGP四大属性

    **公认必遵**:Origin.AS_Path.Next_hop(所有BGP路由都必须识别这类属性,且必须在Update报文中传递,如果缺少就报错) Origin:指示路由信息的来源(如IGP.EGP ...

  4. HTTP/1.1 优化

    避免发送 HTTP 请求 对于一些具有重复性的 HTTP 请求,比如每次请求得到的数据都一样的,我们可以把这对「请求-响应」的数据都缓存在本地,通过缓存技术减少请求次数. 客户端会把第一次请求以及响应 ...

  5. pandas -- 处理非数值型数据 -- 数据分析三剑客(核心)

    博客地址:https://www.cnblogs.com/zylyehuo/ 开发环境 anaconda 集成环境:集成好了数据分析和机器学习中所需要的全部环境 安装目录不可以有中文和特殊符号 jup ...

  6. 【Linux】5.6 Shell打印输出指令

    Shell打印输出命令 1. echo命令 Shell 的 echo 指令与 PHP 的 echo 指令类似,都是用于字符串的输出.命令格式:echo string 您可以使用echo实现更复杂的输出 ...

  7. study Python3 【1】

    用VSCode来编辑Python代码,作为IDE使用,有点头晕. https://www.runoob.com/python3/python-vscode-setup.html有介绍.还有更好的博客介 ...

  8. 从源码解析 QGraphicsItem 旋转、缩放、平移、transform等变换操作,利用QGraphicsTransform实现变形动画

    QGraphicsItem 有3种方式进行变换:1. 最简单方便的是使用 setRotation() .setScale():2. 使用 setTransform() 进行复杂变换:3. 还可以使用 ...

  9. Unity性能优化-降低功耗,发热量,耗电量之OnDemandRendering篇

    公司游戏项目,手机运行严重发烫,耗电量飞快.在暂时无法做其他美术性和技术性优化的情况下,我写了这个公司内部文档,并做了个实验,今天干脆公布出来,希望对大家有用. --官方文档: Unity - Scr ...

  10. 剑气纵横千行码:AI写就的设计模式侠客行助您仗剑走天涯

    烟火里的江湖旧忆 暮色里,代码侠的电动车在巷口急刹,外卖箱里的热汤晃出细响,恍惚间竟像当年工厂堡锻造炉的轰鸣.难得休息之余,他抹了把额头的汗,扶了扶常开网约车的腰,摸了摸自己晒黑的脸,偶感那颠炒粉的手 ...