C | 指针数组与数组指针&内存分布

·  阅读 811
C | 指针数组与数组指针&内存分布

指针类型与步长计算

如何判断指针的类型,在上一篇文章中进行了详细的讲解,现在我们来详细讲解各种不同类型指针的步长如何计算的?

  • 普通类型的指针步长计算

普通类型的指针和类型相关,如int 占4个字节(64位) char 占1个字节,所以p++内存地址加了4个字节。

	int a = 0;
	int* p = &a;
	printf("p:%p\n",p);//p:0000003DF72FF924
	p++;//步长为4个字节
	printf("p++:%p\n",p);//p++:0000003DF72FF928
复制代码
  • 多级指针的步长计算

多级指针其实就只一个指针类型,指向了另一个指针,而指针类型的变量大小是固定的8个字节,所以二级指针+1步长为8个字节内存地址加8

	int** temp = &p;//指向的类型是 int * 是一个指针 步长为8个字节(指针类型的大小固定为8个字节)
	printf("temp:%p\n",temp);//temp:0000003DF72FF948
	temp++;//步长为8个字节
	printf("temp++:%p\n",temp);//temp++:0000003DF72FF950


	//注意:二级指针可以进行类型的强转
	char** c = &p;//这样是可以的
	printf("c:%p\n",*c);//c:000000F5B5AFF5C8
	printf("c:%d\n", **c);
复制代码

如下图所示: image.png

  • 数组指针的步长计算

指针指向数组的步长其实和数组的类型相关,如果是int 数组步长就是4,如果是char数组步长就是1

	//指针指向数组
	char arr[4] = { 'a','b','c','d' };
	//指针指向了数组的首地址  指针的类型是char
	char* p_arr = &arr;//注意:p_arr是一个char类型的指针
	printf("p_arr:%p\n",p_arr);//p_arr:00000021DEB1F664
	p_arr++;//步长为char的大小就是1
	printf("p_arr++:%p,%c\n", p_arr, *p_arr);//p_arr++:00000021DEB1F665,b
复制代码
  • 二维数组指针的步长计算

数组指针类型就是:type (*p)[ n ] 指针会指向一个数组,那么步长就和数组的长度和数组的类型相关,也就是数组的length * sizeof(type数组类型)。

	//数组指针 +1 的操作 类似于二维数组如下
	//二维数组
	int arr2[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
	//1 2 3
	//4 5 6
	//7 8 9
	//10 11 12
	int(*p_arr2)[3] = arr2;//数组指针 指针的类型int()[4]
	printf("p_arr2:%p,%d\n", p_arr2, *p_arr2[0]);//p_arr2:00000008B04FFBC8,1
	p_arr2++;//步长就是 int * 4
	printf("p_arr2++:%p,%d\n", p_arr2, *p_arr2[0]);//p_arr2++:00000008B04FFBD4,4
复制代码

根据如下图理解: image.png

  • 指针数组的步长计算

指针数组就是数组中的类型是指针类型 {int *,int *,int *.....} 所以步长和指针指向的类型相关。这个可以很好的理解看下面的代码

	//指针数组
	int arr3[2] = {1,2};
	int arr4[2] = {3,4};
	int arr5[2] = {5,6};
	int arr6[2] = {7,8};
	int* p_arr3[4] = {arr3,arr4,arr5,arr6};//数组中是 int * 类型  int [2]
	printf("p_arr3:%p\n", p_arr3[0]);//p_arr3:0000006BC02FFC38

	p_arr3[0]++;//int * 而指针类型是int类型 所以步长为4

	printf("p_arr3++:%p\n",p_arr3[0]);//p_arr3++:0000006BC02FFC3C
复制代码

内存分布

C 语言的内存分布:代码区、常量去、全局数据区、栈区、堆区、动态链接库。在C语言中的内存分布明显和Java中的不同在JVM中分为:堆区、栈区、方法区。

内核空间和用户空间:一般在32位环境,理论上程序可以拥有4GB的虚拟地址空间,C语言中的变量、函数、字符串都会对应内存中一块区域。 在这4GB的内存地址空间,需要拿出一部分给操作系统内核使用,应用程序无法直接访问这一段内存,这一部分内存地址称为内核空间(Kernel Space). 在window系统默认情况会将高地址的2GB空间分配给内核(也可以配置1GB),而Linux默认情况会将高地址的1GB空间分配给内核。也就是说应用程序4GB的地址空间只剩下2GB或者3GB的地址空间,称为用户空间(User Space).

如下图片所示:0xffffffff 是高地址,0x00000000是低地址。从内存模型可以看出:

  1. 栈区是从高地址向低地址,向下增长
  2. 堆区是从低地址向高地址,向上增长

104P51I1-0.jpg 对于栈区,如下代码:

a的地址 > b > c 高地址 -> 低地址,显然在main函数栈中线执行的变量的地址高于后执行变量的地址。

int main() {
    int a = 1;
    int b = 2;
    int c = 3;
    //栈区:高地址位于栈顶  低地址位于栈底,也就是说先入栈的地址要大于后入栈的
    //a的地址 > b > c 高地址 -> 低地址
    /*
    a:000000000061FE0C
    b:000000000061FE08
    c:000000000061FE04
    */
    printf("a:%p\n", &a);
    printf("b:%p\n", &b);
    printf("c:%p\n", &c);
    return 0;
}
复制代码

函数执行时,会进入栈区执行,每个函数都相当于栈区的一个栈帧,函数执行完毕会移除栈区。 ​

如下堆区,如下代码:

堆区:地址是从低到高和栈区是相反的。 通过malloc向堆区申请一块内存空间,int占有4个字节

    //堆区:地址是从低到高和栈区是相反的
    int *mp = malloc(16);
    for (size_t i = 0; i < 4; i++) {
        printf("mp %p\n", (mp + i));
    }
    /*
    mp 0000023891D455F0
    mp 0000023891D455F4
    mp 0000023891D455F8
    mp 0000023891D455FC
    */
复制代码

执行过程如下图所示: image.png

在C语言中执行函数,都会将函数放到栈区入栈,执行完毕后出栈,如下面这一段代码,test函数在栈区执行完毕后返回结果,然后被移除栈区

int test() {
    int a = 1;
    int b = 2;
    return a + b;
}
int main(){
	int d = test();//test执行:先入栈main得到结果 test执行完出栈
    return 0;
}
复制代码

思考:当函数非常多时,会频繁的入栈和出栈操作,多少会影响性能,都知道C的性能比所有的语言都要好,那么为什么呢?

C 语言性能强大,一个来源于指针直接操作内存,另一个还有很多优化点,比如上述的函数频繁入栈和出栈,C语言有宏定义的方式,预编译在编译阶段将方法替换为源码,而直接省去了入栈和出栈的操作。

宏定义的使用如下:

#define 是宏定义的关键字,可以定义成类似java中的static final变量,也可以定义方法,当调用这个ADD(1,2) 就会在编译阶段直接替换为 1 + 2. 调用Test在编译阶段就会被直接替换为99

#define Test 99;//宏变量 类似java中的static final
#define ADD(x, y) x+y //宏定义 加法操作   编译时替换 预处理

int main(){
 	printf("add:%d\n", ADD(1,2));//ADD() 直接替换成对应的源码 1+2 不用频繁的入栈和出栈
    return 0;
}
复制代码

注意:宏定义看起来非常好用,但是存在着一个问题:宏定义不考虑优先级

下面代码,预期的输出是 30,但是输出结果确是 21

	//宏定义的问题
    printf("add:%d\n", ADD(1,2)*10);//输出:21  执行:ADD(1,2) -> 1+2;1+2*10  只会帮你替换为对应的源码,不考虑优先级
复制代码

所以在使用宏定义的时候一定要注意加上括号保证执行的优先级,这样就可以得到预期的结果。

#define ADD(x, y) (x+y) //宏定义 加法操作   编译时替换 预处理
printf("add:%d\n", ADD(1,2)*10);//输出:30  执行:ADD(1,2) -> (1+2);-> (1+2)*10
复制代码
  • 字符串指针越界的问题,注意在C语言中字符串遇到"\0"才表示结束
//指针越界的问题
    char buf[3] = "abc";//字符串遇到 \0,才会结束
    printf("buf:%s\n",buf);//abc??
    char buf2[3] = "ab\0";//字符串遇到 \0,才会结束
    printf("buf2:%s\n",buf2);//ab
复制代码
  • 函数返回指针的问题,注意堆区和栈区的区别,当函数执行完毕会被移除栈区(变量虽然被移除,但是内存区域不一定会立刻清零),而堆区需要手动free释放才可以。
 /**
  * getArr 执行完毕栈就被移除了,当去查找这个内存区域是查不到值
  * @return
  */
 int * getArr(){
     int arr[4] = {1,2,3,4};
     return arr;
 }

 /**
  * arr 指向了堆区的一块内存区域,当getArr2执行完毕,移除栈但是堆区的并没有移除
  * @return
  */
int * getArr2(){
    int *arr = malloc(16);
    return arr;
}

	int * s1 = getArr();
    printf("s1:%p\n", s1);//s1:0000000000000000   找不到内存地址了 因为arr在执行完毕已经被栈区移除了

    int * s2 = getArr2();
    printf("s2:%p\n", s2);//s2:00000000007B1460
复制代码
  • 二级指针的复杂使用方式

二级指针是常用的指针方式,必须要进行掌握,二级指针的值指向一个指针的地址

常见的二级指针:

此处的二级指针是在栈区进行声明

    //二级指针
    int b = 10;
    int *p = &b;
    int **p2 = &p;
复制代码

那么如何在堆区声明一个二级指针呢?如下代码

void print_array(int **arr,int n){
    for (int i = 0; i < n; i++) {
        printf("arr:%d\n",*arr[i]);//arr[i] === **(arr+i)
        printf("arr:%d\n",**(arr+i));//arr[i] === **(arr+i)
    }
}

void test() {
    int a1 = 10;
    int a2 = 20;
    int a3 = 30;
    int a4 = 40;
    int a5 = 50;
    int n = 5;
    //二级指针更加复杂的使用
    int **arr = malloc(sizeof(int *) * n);
//    arr[0] = &a1;//arr[0] === *arr
//    arr[1] = &a2;//arr[0] === *(arr+1)
//    arr[2] = &a3;//arr[0] === *(arr+2)
//    arr[3] = &a4;//arr[0] === *(arr+3)
//    arr[4] = &a5;//arr[0] === *(arr+4)

    *arr = &a1;//arr[0] === *arr
    *(arr+1) = &a2;//arr[0] === *(arr+1)
    *(arr+2) = &a3;//arr[0] === *(arr+2)
    *(arr+3) = &a4;//arr[0] === *(arr+3)
    *(arr+4) = &a5;//arr[0] === *(arr+4)
    print_array(arr,n);
    //释放空间
    free(arr);
}
复制代码

上述代码,通过malloc开辟了一个大小为 指针的大小(64位8个字节) * n的内存空间,用来存储n个指针类型。

如下图所示: 内存运行的模型图

在图中注意:栈区从高地址向低地址,堆区从低地址向高地址

image.png

  • 二级指针常用的方式

如下代码:allocateSpace函数中的参数接收一个二级指针,去改变一级指针的值 注意:如果一个函数要想改变外部指针的值,就需要传递**指针的地址,而不是指针指向的地址(int p = &a,传递&p)*

void allocateSpace(int **addr){
    //p是一个二级指针
    int *temp = malloc(sizeof(int)*16);
    for (int i = 0; i < 16; ++i) {
        temp[i] = 100+i;
    }
    *addr = temp;
}
void print_array(int **arr,int n){
    for (int i = 0; i < n; i++) {
        //(*arr) -> *p -> temp[]
        printf("arr:%d\n",(*arr)[i]);
    }
}

int main(){
    //一级指针
    int * p = NULL;
    //二级指针 要改变指针需要传递指针的地址
    allocateSpace(&p);
    print_array(&p,16);
}
复制代码

可能看代码有一些懵逼,来看下面的画图所示,一遍就懂: image.png

思考:通过上图可以明白代码的运行流程,思考一个问题在allocateSpace函数中申请了堆内存,堆内存必须要手动释放,但是allocateSpace执行完毕就从栈区移除栈帧,那么如何释放这一块的内存区域呢?

其实经过上述流程 ptemp指向的地址相同,那么通过free(p)就可以释放申请的内存空间。 还可以通过二级指针的应用写一个通用的释放函数:

void freeAddr(int ** addr){
    free(*addr);
    *addr = NULL;
}

freeAddr(&p);
复制代码

二级指针在实际应用中会使用非常多,所以必须要掌握二级指针的应用。搞懂上述两个内存模型拆分图二级指针会变的非常简单。

收藏成功!
已添加到「」, 点击更改