介绍
我们必须明白一件事情,如果你是从大学开始学C语言的话,你不可能学的非常精通,比如我们学校就是如此,所以在毕业之后的简历上也千万不要写自己会C语言,因为难度会很大,他可能会面试你一些关于GCC底层的逻辑包括GUNC的执行,代码编译和解释的过程等等,这些问题的难度很大,当然,这就是本博文的主要对象,那就是GCC处理一种比C99更牛的准则,GUNC。
因为GUNC所有内容都是前后双下划线,为了避免与markdown语法重叠而加粗影响理解,这里只画前面的
__attribute
__attribute是一个GNUC对齐的最基本属性,此属性包含很多校验方案,比如函数是否被使用,或者对齐模式,参数校验等等。
aligned(“size”)
首先默认情况下gcc会对结构体进行对齐,如果一个结构体不进行对齐,那么他所占的空间就是所有属性内存空间的和,所以也就意味着如果内部包含一个可变长度的数据类型,那么就必然会发生内存溢出。这样是不安全的,需要对内部元素进行转移扩容,也需要实时判断。
如果使用内存对齐,那么在分配内存空间时就会按照结构体内空间最大的属性进行对齐,我们需要传递一个内存单元的大小,然后依次放入元素,如果元素可以正好放入n个单元内,则结构体的大小是n*一个单元的大小,注意这里内部属性的内存空间并没有改变。如果分配的单元小于最大的,那么更改为最大的单元进行对齐。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <stdio.h>
struct Aim{ int a; char b; short c; } __attribute__((aligned(4))) aa;
int main() { printf("int=%llu\n",sizeof(int)); printf("char=%llu\n",sizeof(aa.b)); printf("short=%llu\n",sizeof(aa.c)); printf("struct=%llu\n",sizeof(aa)); }
int=4 char=1 short=2 struct=8
|
其中的char和short因为不足4字节,所以共用一块内存单元。最后空余一个1字节的大小
如果我们没有指定内存单元的大小,GCC会使用结构体内某个内存空间最大的属性当作2的指数,然后进行分配。当然这个数字随着内部最大对象的增加而增加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <stdio.h> struct Aim{ int a; short b; short c; short d; short e; short f; short g; } __attribute__((aligned())) aa;
int main() { printf("int=%llu\n",sizeof(int)); printf("struct=%llu\n",sizeof(aa)); }
int=4 struct=16
|
原因是struct最大的对象是int,占4字节,所以最后大小为2^4=16,如果有两个int就会变成32,如果增加属性使他超过内存大小,则进行翻2倍。
当然,这种对齐方式与你结构体内属性的先后顺序有关系,我们对他稍作修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <stdio.h>
struct Aim{ char b; int a; short c; } __attribute__((aligned(4))) aa;
int main() { printf("int=%llu\n",sizeof(int)); printf("char=%llu\n",sizeof(aa.b)); printf("short=%llu\n",sizeof(aa.c)); printf("struct=%llu\n",sizeof(aa)); }
int=4 char=1 short=2 struct=12
|
我们看到仅仅交换了char和int的位置,内存空间就已经变化了,因为int类型已经占满了一个4字节的区域,不可能和char共用一个区域了,所以三者依次占4字节。
packed
这个功能是取消内存对齐,如果是追求高效率的话就必须做好内存管理,非常容易溢出。不进行对齐后内存大小就是所有属性的和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <stdio.h>
struct Aim{ char b; int a; short c; } __attribute__((packed)) aa;
int main() { printf("int=%llu\n",sizeof(int)); printf("char=%llu\n",sizeof(aa.b)); printf("short=%llu\n",sizeof(aa.c)); printf("struct=%llu\n",sizeof(aa)); }
int=4 char=1 short=2 struct=7
|
使用这种方法对齐和使用1字节对齐是一样的,但是因为指派1字节会导致int类型无法放入,在后面会解释为什么packed是1字节对齐。
constructor&destructor
使用这两个属性声明的函数,可以在main函数执行前后执行,可以脱离main的摆弄。使用constructor可以在main函数之前执行,使用destructor可以在main函数之后执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <stdio.h>
__attribute__((destructor())) void onDisable(){ printf("onDisable\n"); } __attribute__((constructor())) void onLoad(){ printf("onLoad\n"); }
int main() { printf("main\n"); }
onLoad main onDisable
|
如果有很多这样的需要提前执行的函数,就需要在参数的括号内加入优先级吗,提前执行的对象,数字越小越先执行,拖后执行的对象,数字越大越先执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <stdio.h>
__attribute__((destructor(1))) void onDisable1(){ printf("onDisable1\n"); } __attribute__((constructor(1))) void onLoad1(){ printf("onLoad1\n"); } __attribute__((destructor(2))) void onDisable2(){ printf("onDisable2\n"); } __attribute__((constructor(2))) void onLoad2(){ printf("onLoad2\n"); }
int main() { printf("main\n"); }
onLoad1 onLoad2 main onDisable2 onDisable1
|
__alignof
这是与__attribute相反的关键字,前者是使用某种方法对齐,这个是获取对齐的字节数,可以作为aligned()
的参数,我们这时就可以证明一下上文所述对齐的具体内容了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <stdio.h> struct A{ int a; char b; short c; } __attribute__((aligned(4))) a;
int main() { printf(" __alignof__(int)=%llu\n", __alignof__(int)); printf(" __alignof__(char)=%llu\n", __alignof__(char)); printf(" __alignof__(short)=%llu\n", __alignof__(short)); printf(" __alignof__(A)=%llu\n", __alignof__(a)); }
__alignof__(int)=4 __alignof__(char)=1 __alignof__(short)=2 __alignof__(A)=4
|
我们可以看到A的对齐方法是4个字节,和我们设置的一样,如果我们不加入参数,对齐的字节数就是最大内存字节数的以2为底的指数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <stdio.h> struct A{ int a; char b; short c; } __attribute__((aligned())) a;
int main() { printf(" __alignof__(int)=%llu\n", __alignof__(int)); printf(" __alignof__(char)=%llu\n", __alignof__(char)); printf(" __alignof__(short)=%llu\n", __alignof__(short)); printf(" __alignof__(A)=%llu\n", __alignof__(a)); }
__alignof__(int)=4 __alignof__(char)=1 __alignof__(short)=2 __alignof__(A)=16
|
最后我们看一下不使用对齐会是什么结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <stdio.h> struct A{ int a; char b; short c; } __attribute__((packed)) a;
int main() { printf(" __alignof__(int)=%llu\n", __alignof__(int)); printf(" __alignof__(char)=%llu\n", __alignof__(char)); printf(" __alignof__(short)=%llu\n", __alignof__(short)); printf(" __alignof__(A)=%llu\n", __alignof__(a)); }
__alignof__(int)=4 __alignof__(char)=1 __alignof__(short)=2 __alignof__(A)=1
|
我们看到所谓不使用对齐就是按1个字节进行对齐,这样我们非常清晰明了的知道了__attribute的对齐逻辑。
__asm
这种方法可以在C语言执行时插入汇编指令,比如声明内存进行了改动,让gcc进行注意
1 2 3 4 5 6
| #include <stdio.h>
int main() { __asm__ ("":::"memory"); int a = 1; }
|
1 2 3 4 5 6 7 8 9 10
| main: pushq %rbp movq %rsp, %rbp subq $48, %rsp call __main movl $1, -4(%rbp) movl $0, %eax addq $48, %rsp popq %rbp ret
|
当然我可以使用关键字volatile进行声明内存易变。
1 2 3 4 5
| #include <stdio.h>
int main() { volatile int a = 1; }
|
1 2 3 4 5 6 7 8 9 10
| main: pushq %rbp movq %rsp, %rbp subq $48, %rsp call __main movl $1, -4(%rbp) movl $0, %eax addq $48, %rsp popq %rbp ret
|
我们从汇编上可以看到两者完全相同,没错volatile的功能和使用__asm__("":::"memory")
一样,不过__asm可以插入更丰富的汇编指令,这里不做过多演示
__extension
此关键字后面的表达式,如果出先非标准的警告,不过一般人无法刻意写出bug,这里我就不举例了。
__cplusplus
这个关键字就是__cplusplus不是双下划线,这个关键字是为了方便CPP和C之间互通的,在预编译器内可以判断这是否为一个C++文件。这里我新建一个C文件,然后写入一下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <stdio.h> void show() { printf("Hello, C!"); }
#ifdef __cplusplus void show() { printf("Hello, C++!"); } #endif
int main() { show(); }
|
使用这种方法可以让C和C++使用同名函数,提高代码的复用性
其他写法
零长数组:注意,内存大小是0哦
case:可以提供一个范围,下面表示0到9
语句表达式:({各种执行的操作………;}),在所有操作的最后就是返回的值。
1 2 3 4 5 6 7
| #include <stdio.h>
#define min_t(typename,x,y) ({ typename __x = (x); typename __y = (y); __x < __y ? __x: __y; })
int main() { printf("%d",min_t(int,2,3)); }
|
typeof: 把一个对象的类型定义给另一个对象
1 2
| int x = 2; typeof(x) _x = 3;
|
可变参数:同折叠表达式,使用“…”表示一个粘连表达式,也就是##对象
当然还有很多的函数,比如可以获取当前函数名的__FUNCTION等等
总结
本文着重描写了内存对齐的具体过程以及如何进行优化,了解到内存的分配机制,可以帮助基础程序员变成大佬程序员,无论是在C的结构体的内存分配,还是针对不同架构相同数据类型的数据对齐都是使用的这种方法,所以C语言是一个可以以小见大的好语言。