[一篇读懂]C语言五讲:指针
1. 指针的本质(间接访问原理)
重要
1 指针的定义
- 内存区域中的每字节都对应一个编号,这个编号就是“地址”。
- 按变量地址存取变量值的方式称为“直接访问”,如:
printf("%d",i);
scanf("%d",&i);
- 另一种存取变量值的方式称为“间接访问”,即将变量 i i i的地址存放到另一个变量中。
- 在C语言中,指针变量是一种特殊的变量,它用来存放变量地址。
说某个变量的地址时,讲的都是它的
起始地址
直接访问:
访问地址拿到宝藏
间接访问:
把地址
看作藏宝图
通过指针变量 - 找到地址
(藏宝图) - 再拿到宝藏
即多了一次跳转
- 指针变量的定义格式如下:
/*基类型 *指针变量名;*/
int *i_pointer;//定义了一个指针变量,i_pointer是指针变量名
- 指针与指针变量是两个概念
一个变量的地址称为该变量的
“指针”
。
如果有一个变量专门用来存放另一变量的地址(即指针),那么称它为“指针变量”
。
- 指针变量占用的内存空间
本章中编写的程序都是64位应用程序,寻址范围为64位即8字节,所以对于本章来说sizeof(i_pointer)=8。如果编写的程序是32位,那么寻址范围就是4字节(考研中往往会强调程序是32位的程序)。
2 取地址操作符与取值操作符,指针本质
- 取地址操作符为&,也称引用。
通过该操作符我们可以获取一个变量的地址值。
- 取值操作符为*,也称解引用。
通过该操作符我们可以得到一个地址对应的数据。
- 例:
#include <stdio.h>
int main()
{
//&符号是取地址,指针变量的初始化一定是某个变量取地址
int i = 5;
//指针变量的初始化是某个变量取地址来赋值,不能随机写个数
//指针变量的基类型与所指向的变量的基类型相同
int* p = &i;
printf("i = %d\n", i);//直接访问
printf("p = %d\n", *p);//间接访问
return 0;
}
注意!!
(1)指针变量前面的“*”表示该变量为指针型变量。
例如,float *pointer_1;
注意指针变量名是pointer_1,而不是*pointer_1。
(2)在定义指针变量时必须指定其类型。
需要注意的是,指针变量的基类型与所指向的变量的基类型相同。例如,下面的赋值是错误的:float a; int *pointer_1; pointer_1 = &a; //毫无意义而且会出错
(3)如果已执行了语句pointer_1 = &a;
那么
&*pointer_1
的含义是什么呢?(犯傻)
“&”和“*”两个运算符的优先级别相同,但要按自右向左的方向结合。因
此,&*pointer_1与*&a相同,都表示变量a的地址
,也就是pointer_1。
*&a
的含义是什么呢?
首先进行&a运算,得到a的地址,再进行*运算。*&a和*pointer_1的作用是一样的,它们都等价于变量a
,即*&a 与a等价。
(4)为什么要让*和指针变量连着
C语言本质上是一种自由形式的语言,这很容易诱使我们把“*”
写在靠近类型的一侧,如int* a
这个声明与int *a
具有相同的意思,而且看上去更清晰,a被声明成类型为int*的指针。
但是,这并不是一个好习惯,因为类似int* a,b,c的语句会使人们很自然地认为这条语句把所有三个变量声明为指向整型的指针,但事实上并非如此,“*”
实际上是*a
的一部分,只对a标识符起作用,但其余两个变量只是普通的整型变量。
要声明三个指针变量,正确的语句如下:int *a, *b, *c;
2. 指针的传递使用场景
指针的使用场景通常只有两个,即传递与偏移
1 指针的传递
程序启动起来就是进程
【例1.1】指针的传递使用场景。
#include <stdio.h>
//在子函数中去改变主函数中某个变量的值
void change(int j)//j是形参
{
j = 5;
}
int main()
{
int i = 10;
printf("before change i = %d\n", i);
change(i);//C语言的函数调用是值传递,实参赋值给形参,i是实参
//j = i - 把i赋值给了j
printf("after change i = %d\n", i);
return 0;
}
运行结果:
change后
i
i
i的值没有改变。
C语言的函数调用是值传递,实参赋值给形参, i i i是实参, j j j是形参。
运行change -j = i
-把i赋值给了j
然后j = 5
- j的值改变了
所以 i i i的值不会改变!
- 通过调试查看:
i i i的地址:61fe1c
j j j的地址:61fdf0
运行change,先把 i i i赋值给 j j j -j = i
,然后执行j = 5
改变的都是61fdf0地址的值 - 即 j j j;
不会影响到61fe1c地址的值 - 即 i i i
【例1.1】的原理图:
变量
i
i
i在
m
a
i
n
main
main函数中;
变量
j
j
j在
c
h
a
n
g
e
change
change函数中。
任何时候都是实参赋值给形参
【例1.2】在子函数中修改main函数中某个变量的值。
#include <stdio.h>
//在子函数中去改变主函数中某个变量的值
void change(int *j)//j是形参
{
*j = 5;//*j等价于变量i,只是间接访问
//间接访问得到变量i
}
//指针的传递
int main()
{
int i = 10;
printf("before change i = %d\n", i);
change(&i);//传递变量i的地址 - j = &i
printf("after change i = %d\n", i);
return 0;
}
运行结果:
程序执行后,
i
i
i的值变成了5。
将变量
i
i
i的地址传递给change函数时,实际效果是j=&i
,依然是值传递,只是这时
j
j
j是一个指针变量,内部存储的是变量
i
i
i的地址,所以通过*j
就间接访问到了与变量
i
i
i相同的区域,通过*j=5
就实现了对变量
i
i
i的值的改变。
3. 指针的偏移使用场景
1 指针的偏移
指针即地址,就像是找到了一栋楼,这栋楼的楼号是B,那么往前就是A,往后就是C,所以应用指针的另一个场景就是对其进行加减,但对指针进行乘除是没有意义的,就像家庭地址乘以5没有意义那样。在工作中,我们把对指针的加减称为指针的偏移,加就是向后偏移,减就是向前偏移。
【例1.1】指针的偏移使用场景。
实现数组的正序输出和逆序输出:
#include <stdio.h>
//指针的偏移使用场景,也就是对指针的加减
#define N 5//定义一个符号常量
int main()
{
int a[N] = { 1,2,3,4,5 };//数组名内存储了数组的起始地址,a中存储的就是一个地址值
int *p;//定义指针变量p
p = a;//保证等号两边的数值类型一致 - p中存储数组起始地址
int i;
for (i = 0; i < N; i++)//正序输出
{
//printf("%3d", a[i]);//等价下一句
printf("%3d", *(p + i));//*(p + 0)拿到a[0];*(p + 1)拿到a[1]……
}
printf("\n-----------------\n");//分隔
p = &a[4];//对整型变量取地址 - 指针变量p指向了数组的最后一个元素
for (i = 0; i < N; i++)//逆序输出
{
printf("%3d", *(p - i));
}
printf("\n");
return 0;
}
数组名内存储了数组的起始地址,a中存储的就是一个地址值
运行结果:
- 数组名中存储着数组的起始地址0x61fdf0,其类型为整形指针,可以赋值给整型指针变量p,通过
*p
可以得到元素a[0]。 - p + 1的值为0x61fdf4,因为指针变量加1后,偏移的长度是其基类型的长度,也就是偏移sizeof(int)长度,这样通过
*(p+1)
就可以得到元素a[1]。
2 指针与一维数组
- 数组名作为实参传递给子函数是,是弱化为指针的
直接写
*d
void change(char *d)
【例】数组传递给子函数的实战练习
#include <stdio.h>
//指针与一维数组的传递
//数组名作为实参传递给子函数是,是弱化为指针的
void change(char *d)//数组名存储的是一个指针值,形参写指针变量,不写数组变量
{
*d = 'H';
d[1] = 'E';//等价于*(d+1) = 'E';
*(d + 2) = 'L';
}
int main()
{
char c[10] = "hello";
change(c);
puts(c);
return 0;
}
运行结果:
4. 指针与malloc动态内存申请,栈空间与堆空间的差异
1 指针与动态内存申请
- int a[10]数组长度固定 - 很不方便
- 数组长度固定是因为其定义的整型、浮点型、字符型变量、数组变量都在栈空间中,而栈空间的大小在编译时是确定的。
- 如果使用的空间大小不确定,那么就要使用堆空间。
【例1.1】动态内存申请。
#include <stdio.h>
#include <stdlib.h>//malloc需要使用的头文件
#include <string.h>
int main()
{
int size;//size代表我们要申请多大字节的空间
char* p;//void*类型的指针是不能偏移的,因此自己不会去定义无类型指针
scanf("%d", &size);//输入要申请的空间大小
//注意指针本身大小,和其指向的空间大小,是两码事,不能和前面的变量类比去理解!
//malloc返回的void*代表无类型指针
//使用malloc动态申请堆空间
p = (char*)malloc(size); //强制类型转换与p的类型一致
//p[0] = 'H';
//p[1] = 'O';
//p[2] = 'W';
//p[3] = '\0';
strcpy(p, "malloc success");
puts(p);
//不用时要free!释放申请的空间!
//释放申请的空间时,给的地址,必须是最初malloc返回给我们的地址
free(p); //free时必须使用malloc申请时返回的指针值,不能进行任何偏移
printf("free success\n");
return 0;
}
- 向malloc申请一片空间,malloc返回对应空间的首地址,分配接下来一部分空间。
- malloc返回值为void*,即无类型指针,其只能用来存储一个地址且不能偏移。
- 堆空间是借来的,不用的时候要free掉,释放申请的空间。
- 释放申请的空间时,给的地址必须是最初使用malloc申请时返回给我们的地址。
- 注意指针本身大小,和其指向的空间大小,是两码事,不能和前面的变量类比去理解。
堆是动态的,但是堆的效率比栈低得多。
2 栈空间与堆空间的差异(了解)
记住即可
【例2.1】堆空间与栈空间的差异。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//堆和栈的差异
//函数栈空间释放后,函数内的所有局部变量消失
//低级错误
char* print_stack()//栈空间stack
{
char c[100] = "I am print_stack func";//栈空间在子函数结束后被释放了
char *p;
p = c;
puts(p);//可以打印出I am print_stack func
return p;
}
//堆空间不会因为函数执行结束而释放
char* print_malloc()//堆空间heap
{
char* p = (char*)malloc(100);//堆空间在整个进程中一直有效,不因为函数结束而消亡
strcpy(p, "I am print malloc func");
puts(p);
return p;
}
int main()
{
char *p;
p = print_stack();//数据放在栈空间
puts(p);//子函数中造的数据到主函数中打印出乱码
//是因为栈空间已经被释放掉了
p = print_malloc();//数据放在堆空间
puts(p);
free(p);//只有free时,堆空间才会释放
return 0;
}
- 栈空间在子函数结束后被释放了,函数栈空间释放后,函数内的所有局部变量消失。
- 堆空间在整个进程中一直有效,不会因为函数执行结束而释放,只有free时,堆空间才会释放。
总结
1.1
- 按变量地址存取变量值的方式称为“直接访问”,如:
printf("%d",i);
scanf("%d",&i);
- 另一种存取变量值的方式称为“间接访问”,即将变量 i i i的地址存放到另一个变量中。
/*基类型 *指针变量名;*/
int *i_pointer;//定义了一个指针变量,i_pointer是指针变量名
1.2
- 取地址操作符为&,也称引用。
通过该操作符我们可以获取一个变量的地址值。
- 取值操作符为*,也称解引用。
通过该操作符我们可以得到一个地址对应的数据。
- 指针变量前面的“*”表示该变量为指针型变量。
- 在定义指针变量时必须指定其类型。
2.1
- C语言的函数调用是值传递,实参赋值给形参。
- 通过指针变量可以访问变量所在的区域。
3.1
- 对指针的加减称为指针的偏移,加就是向后偏移,减就是向前偏移。
- 偏移的长度是其基类型的长度,也就是偏移sizeof(int)长度。
3.2
- 数组名作为实参传递给子函数是,是弱化为指针的。
4.1
- 数组长度固定是因为其定义的整型、浮点型、字符型变量、数组变量都在栈空间中,而栈空间的大小在编译时是确定的。
- 如果使用的空间大小不确定,那么就要使用堆空间。
- malloc需要使用的头文件#include <stdlib.h>
- 向malloc申请一片空间,malloc返回对应空间的首地址,分配接下来一部分空间。
- malloc返回值为void*,即无类型指针,其只能用来存储一个地址且不能偏移。
- 堆空间是借来的,不用的时候要free掉,释放申请的空间。
- 释放申请的空间时,给的地址必须是最初使用malloc申请时返回给我们的地址。
- 注意指针本身大小,和其指向的空间大小,是两码事,不能和前面的变量类比去理解。
4.2
- 堆是动态的,但是堆的效率比栈低得多。
- 栈空间在子函数结束后被释放了,函数栈空间释放后,函数内的所有局部变量消失。
- 堆空间在整个进程中一直有效,不会因为函数执行结束而释放,只有free时,堆空间才会释放。