指针类型与步长计算
如何判断指针的类型,在上一篇文章中进行了详细的讲解,现在我们来详细讲解各种不同类型指针的步长如何计算的?
- 普通类型的指针步长计算
普通类型的指针和类型相关,如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);
复制代码
如下图所示:
- 数组指针的步长计算
指针指向数组的步长其实和数组的类型相关,如果是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
复制代码
根据如下图理解:
- 指针数组的步长计算
指针数组就是数组中的类型是指针类型 {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是低地址。从内存模型可以看出:
- 栈区是从高地址向低地址,向下增长
- 堆区是从低地址向高地址,向上增长
对于栈区,如下代码:
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
*/
复制代码
执行过程如下图所示:
在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个指针类型。
如下图所示: 内存运行的模型图
在图中注意:栈区从高地址向低地址,堆区从低地址向高地址
- 二级指针常用的方式
如下代码: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);
}
复制代码
可能看代码有一些懵逼,来看下面的画图所示,一遍就懂:
思考:通过上图可以明白代码的运行流程,思考一个问题在allocateSpace函数中申请了堆内存,堆内存必须要手动释放,但是allocateSpace执行完毕就从栈区移除栈帧,那么如何释放这一块的内存区域呢?
其实经过上述流程 p
和 temp
指向的地址相同,那么通过free(p)
就可以释放申请的内存空间。
还可以通过二级指针的应用写一个通用的释放函数:
void freeAddr(int ** addr){
free(*addr);
*addr = NULL;
}
freeAddr(&p);
复制代码
二级指针在实际应用中会使用非常多,所以必须要掌握二级指针的应用。搞懂上述两个内存模型拆分图二级指针会变的非常简单。