结构体内存分配对齐深入理解
问题描述:
定义好一个结构体之后,结构体所占的内存大小是多少?
struct stu {
int a;
double b;
char c[9];
};
按照结构体内存大小的计算方式,结构体的内存大小,就是各成员所占大小的总和。但其实不仅仅是这样的,因为结构体分配内存时候,编译器有一个内存分配对齐的机制。——目的是为了使每个字节步长都一致,系统执行起来速度会加快,从而提高了程序的运行效率。
那具体是如何对齐呢?
首先要知道出于效率的考虑,C语言引入了字节对齐机制,一般来说,不同的编译器字节对齐机制有所不同,但还是有以下3条通用准则:
(1)结构体变量的大小能够被其最宽基本类型成员的大小所整除;
(2)结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;
(3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
看完这个我来再看上面那段代码
struct a
{
int b;/*本身占用4个字节,但是因为下面最宽数据类型是double,要让它整除,所以占用8个字节,编译器自动对齐(参考第一点)*/
double c;/*这个就是最宽数据类型,占用字节就是它本身8个字节*/
char d[9];/*本身每个char占用1个字节,定义了9个char所以大家认为一共用9个字节,但是其实是16个字节,因为要被double整除(参照第二点)*/
};
void main()
{
prinf("%d",sizeof(struct a));//所以最后输出为32
}
动态分配内存和回收
内存四区模型
流程说明
1、操作系统把物理硬盘中的代码加载(load)到内存中
2、操作系统把C代码分成4个区
3、操作系统找到main()函数入口
一个右C/C++编译的程序占用的内存分四个部分:
1、栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方
式类似于数据结构中的栈。
2、堆区(heap: 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回 收 。
注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、数据据区:主要包括静态全局区和常量区,如果要站在汇编角度细分的话还可以分为很多小的区。
(1)全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变
量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
程序结束后有系统释放
(2)常量区 :常量字符串就是放在这里的。 程序结束后由系统释放
5、代码区:存放函数体的二进制代码。
动态内存分配
malloc分配的内存是在堆区
程序员自己分配,自己释放,如果没有释放由操作系统释放
操作系统对分配的内存进行权限管理,已经分配的内存但没有被释放,不能再进行分配使用,这块内存也就被浪费掉了,造成了内存泄露
重启系统后,内存中的数据全部清零,全部重新规划,就不存在这个内存不足的问题了。但是如果还运行这些不完善的程序,还会内存泄露。堆的作用主要就是为程序提供动态内存分配,这样就能满足程序运行时增加内存的要求。
回调函数
简单来说,如果把一个函数的入口地址传递给一个函数指针,通过函数指针去调用这个函数,那么称这个函数为回调函数。回调函数的一般用法为:A进行结构体的定义(成员中包括回调函数的指针),B进行声明结构体变量,并向A注册回调函数。A在某个特定事件触发的时候,遍历并执行所有注册的回调函数。
下面代码是一个简单回调函数的例程
#include <stdio.h>
struct operation {
void (*callback)();
};
void trigger(struct operation* ops, int ops_length) {
int i = 0;
while(i++ < ops_length) {
ops->callback();
ops++;
}
}
void a_callback() {
printf("I'm A\n");
}
void b_callback() {
printf("I'm B\n");
}
int main(int argc, char* argv[]) {
struct operation A,B;
A.callback = &a_callback;
B.callback = &b_callback;
struct operation ops[2];
ops[0] = A;
ops[1] = B;
trigger(ops, 2);
return 1;
}
A、B分别向operation注册了各自的回调函数。
main()函数的参数问题
main函数的参数
前面介绍的main函数都是不带参数的。因此main 后的括号都是空括号。实际上,main函数可以带参数,这个参数可以认为是 main函数的形式参数。C语言规定main函数的参数只能有两个, 习惯上这两个参数写为argc和argv。因此,main函数的函数头可写为: main (argc,argv)C语言还规定argc(第一个形参)必须是整型变量,argv( 第二个形参)必须是指向字符串的指针数组。加上形参说明后,main函数的函数头应写为:
main (argc,argv)
int argv;
char *argv[];或写成:
main (int argc,char *argv[])
由于main函数不能被其它函数调用, 因此不可能在程序内部取得实际值。那么,在何处把实参值赋予main函数的形参呢? 实际上,main函数的参数值是从操作系统命令行上获得的。当我们要运行一个可执行文件时,在DOS提示符下键入文件名,再输入实际参数即可把这些实参传送到main的形参中去。
DOS提示符下命令行的一般形式为: C:>可执行文件名 参数 参数……; 但是应该特别注意的是,main 的两个形参和命令行中的参数在
位置上不是一一对应的。因为,main的形参只有二个,而命令行中的参数个数原则上未加限制。argc参数表示了命令行中参数的个数(注意:文件名本身也算一个参数),argc的值是在输入命令行时由系统按实际参数的个数自动赋予的。例如有命令行为: C:>E6 24 BASIC dbase FORTRAN由于文件名E6 24本身也算一个参数,所以共有4个参数,因此argc取得的值为4。argv参数是字符串指针数组,其各元素值为命令行中各字符串(参数均按字符串处理)的首地址。 指针数组的长度即为参数个数。数组元素初值由系统自动赋予。其表示如图6.8所示:
main(int argc,char *argv){
while(argc-->1)
printf("%s\n",*++argv);
}
本例
是显示命令行中输入的参数如果上例的可执行文件名为e24.exe,存放在A驱动器的盘内。
因此输入的命令行为: C:>a:e24 BASIC dBASE FORTRAN
则运行结果为:
BASIC
dBASE
FORTRAN
该行共有4个参数,执行main时,argc的初值即为4。argv的4个元素分为4个字符串的首地址。执行while语句,每循环一次 argv值减1,当argv等于1时停止循环,共循环三次, 因此共可输出三个参数。在printf函数中,由于打印项*++argv是先加1再打印, 故第一次打印的是argv[1]所指的字符串BASIC。第二、 三次循环分别打印后二个字符串。而参数e24是文件名,不必输出。
下例的命令行中有两个参数,第二个参数20即为输入的n值。在程序中*++argv的值为字符串“20”,然后用函数”atoi”把它换为整型作为while语句中的循环控制变量,输出20个偶数。
#include"stdlib.h"
main(int argc,char*argv[]){
int a=0,n;
n=atoi(*++argv);
while(n--) printf("%d ",a++*2);
}
本程序是从0开始输出n个偶数。指向指针的指针变量如果一个指针变量存放的又是另一个指针变量的地址, 则称这个指针变量为指向指针的指针变量。