指针
1. 什么是指针?
指针:是一个32位数的一个数。只不过这个数是二进制表示的。例如0000 0000 0000 0000 … 0001(32个bit位)。为了方便表示,因此转换成16进制所表示的一个数。
0000 0000 0000 0000 0000 0000 0000 0000 32bit
--------- ========= --------- =========
1 2 3 4 字节数
内存区:最小的存储单元是一个字节,每一个存储单元都有其对应且唯一的编号。这个编号我们称为地址,也叫指针。通过地址/指针就能找到某一块内存的首地址/某一个内存单元。
拿32位平台举例,一个计算机的某一个内存区有2^32个存储单元。
指针即地址,地址即指针。指针具体来讲就是一个值,不过这个值是内存区的一个地址。
2. 如何编址(即如何给地址分配空间呢)
我们知道,如果给每一个地址分配一个字节的空间。从32位二进制的第一个地址(全是0)开始到最后一个地址(全是1)一共有2^32个字节。
2^32 byte == 4GB (2^32/1024/1024/1024 == 4GB)
3. 概念和基本术语
3.1指针的值==指针所指向的地址/内存区
指针的值是指针本身存储的数值,而不是一个一般的数值,这个值将被编译器当作一个地址。在32 位程序里,所有类型的指针的值都是一个32 位整数,因为32 位程序里内存地址全都是32 位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。
以后,我们说一个指针的值是0xFFFF,就相当于说该指针指向了以0xFFFF 为首地址的一片内存区域;
反过来,一个指针指向了某块内存区域,则该指针的值是这块内存区域的首地址。指针所指向的内存区和指针所指向的类型是两个完全不同的概念。
以32位举例,一个内存有32字节,一共有2^32个内存单元,一个字节是一个内存单元。
1 byte = 8 bit
3.2 指针的类型(指针本身的类型)
去掉指针名字
(1)int*ptr;//指针的类型是int*
(2)char*ptr;//指针的类型是char*
(3)int**ptr;//指针的类型是int**
(4)int(*ptr)[3];//指针的类型是int(*)[3]
(5)int*(*ptr)[4];//指针的类型是int*(*)[4]
思考:
为什么要搞不同类型的指针呢?,
为什么不能用一个通用的指针访问任何类型的变量呢?
定义指针类型的意义是什么?
1.指针类型决定了指针解引用的权限有多大,不同类型的指针解引用的权限不同。比如int*类型的指针,可以访问4个字节,double *可以访问8个字节。而char *类型只能访问1个字节。
2.指针类型决定了指针的步长有多大。不同类型的指针步长不同,比如int *类型的指针,步长为4,double *步长为8。而char *类型步长为1。
3.3 指针所指向的类型
去掉*和指针名字就是指针所指向的类型
(1)int*ptr; //指针所指向的类型是int
(2)char*ptr; //指针所指向的的类型是char
(3)int**ptr; //指针所指向的的类型是int*
(4)int(*ptr)[3]; //指针所指向的的类型是int()[3]
(5)int*(*ptr)[4]; //指针所指向的的类型是int*()[4]
指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
3.4 指针本身所占据的内存区
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。指针是用来存放地址的,所以存储地址有多大,指针就有多大。在32 位平台里,指针本身占据了4 个字节的长度。(在64位平台里,指针占据8个字节)。指针本身占据的内存这个概念在判断一个指针表达式(后面会解释)是否是左值时很有用。
3.5 指针数组和数组指针(注意区分)
Ex1.int * p //p只是一个指针
Ex2.int *p[3] //p是存放整型指针的数组
Ex3.int (*p)[3] //p是指向整型数组的指针
原理:如Ex3.*优先级小于[],因此若加括号的话,表明是p优先与*结合,因此p是指针,再与[]结合,表明指针所指向的是一个数组。Ex2可举一反三。
//数组名本身就是一个地址,而地址即指针
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;//arr <==> &arr[0]
printf("%d %d %d %d", arr[0], 1[arr], p[2], 3[p]); //arr[2] <==> *(p+2) <==> *(2+p) <==> 2[p] <==> 2[arr]
}
由此可见,只要p指向数组名,其实本质上就是数组的首地址赋值给了p这个"指针变量",所以p和arr是等价的。那么结合[]就可以玩出“新花样”。重点是arr、如同p本质上都是指针(地址)。而[1]、[2]相当于这个指针向后移动了1位、2位。
下面这个内存变化图你看不懂?你要是看不懂,把上面文字重新读一遍吧。
3.6 野指针
定义:不知道指向哪里的指针,随机的地址。
什么样会导致野指针?
- 指针未初始化若指针未初始化,那么该指针会随机找一块内存区分配地址,这个地址大概率不是计算机允许访问的区段。因此,如果不知道指针一开始要访问的空间,则置为NULL。
- 指针越界访问
int main() {
int arr[10] = {0};
int *p = arr;
for (int i = 0; i <= 10; i++) {//当i=10时,p访问的不是给数组分配的地址空间
*p = i;
p++;
}
return 0;
}
- **指针指向的空间已经被提前释放了。**比如前一秒指针在使用的时候申请了内存空间,但是你用完之后把它释放了,当你想要再去使用该指针访问这个空间时,已经没有权限了。
int* test(){
int a = 10;//分配四个字节内存空间
return &a;//返回a的地址
}
int main(){
int *p = test();//当test函数调用完,a的地址对应的空间已经被释放了,此时你再用p去访问这个空间,就是非法访问。
return 0;
}
注意:以下写法是错误的。
int main(){
int *p = NULL;
*p = 10;//空指针,也叫空地址,是允许读,不允许写的,会抛出写入访问权限异常
return 0;
}
思考一下怎么解决?其实很简单,加个判空条件即可。
4. 指针运算
int main() {
int arr[10] = {1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int *p1 = &arr[0];
int *p2 = &arr[9];
printf("p1 = %p,p2 = %p\n", p1, p2);
printf("%d", p2 - p1);
return 0;
}
打印出来的结果是9,这是因为,首指针到尾指针需要4*9个字节才能到达尾指针(变量)的首地址。以%d输出即9;故指针-指针==指针需要走多少步==两个指针之间的元素个数;
假设我们要自己写一个统计字符串长度的函数。
例1:统计字符串长度;
int main() {
char str[] = "abcde";
int len = my_strlen(str);
printf("%d", len);
return 0;
}
没学指针前的你可能会这么写:
int my_strlen(char str[]) {
int count = 0;
for (int i = 0; str[i] != '\0'; i++) {
count++;
}
return count;
}
学了指针后:
int my_strlen(char *p) {
int count = 0;
while (*p != '\0') {
count++;
p++;
}
return count;
}
int my_strlen(char *p) {
char *start = p;
while (*p != '\0')
p++;
return p - start;
}
这里传过去的str实际上是一个str中a的首地址,本质上还是指针,所以我们能通过一个*p来指向它。
5. 指针关系运算
例2:将某数组元素全部置为1;
#define N_VALUES 5
int main() {
int values[N_VALUES] = {0};
int *p = values;
for (p = &values[0]; p < &values[N_VALUES]; p++)
*p = 1;
for (int i = 0; i < 5; i++)
printf("%d ", values[i]);
return 0;
}
你会发现p = &values[0]、p++可以省略;因为指针一开始就初始化了,p++可以放到for里面。其实就是一个while循环。
int *p = values;
while(p < &values[N_VALUES])
*p++ = 1;
或者:
for (int *p = values; p < &values[N_VALUES]; p++)
*p = 1;
6. 二级指针
int main() {
int a = 10;
int *pa = &a;
int * *ppa = &pa;
printf("%d\n", *pa);
printf("%d", **ppa); //*ppa <==>pa
return 0;
}
上述代码内存表示图
7. 字符串与指针的应用
int main() {
// char *p = "hello world!";//存放字符串的指针p
char str[] = "hello world!";
char *p = "hello world!"//本质上是将字符串的首字符(h)的地址存放到p里面
printf("%c\n", *p);//h
printf("%s\n", str);//hello world!
printf("%s\n", p);//hello world!
return 0;
}
char *p1 = "hello";
char *p2 = "hello";
char str1[] = "hello";
char str2[] = "hello";
printf("%d",str1==str2);//0
printf("%d",p1==p2);//1
为什么会这样呢? 这是因为,当我们用指针来声明一个字符串时:
第一,其实就是把字符串的首字符的地址存放到指针变量中(因为一个字符占一个字节),所以首字符的地址就是整个字符串的地址。
第二,如果我们用一个指针去声明某个字符串时,该字符串就是一个常量字符串。我们无法对这个常量字符串进行写操作。
上述代码内存表示图
char *p1 = "hello";
p1 = "world";//非法操作,因为p1是一个常量字符串
char str[] = "hello";
char *p2 = str;
p2 = "world";//p2是一个指向str的一个指针变量,修改p2就相当于修改str,因此可行
现在能理解为什么打印p是全部字符串,而*p是首字符了吧?
因为*p是取出字符串的首地址的内容,即字符串的首字符的地址的内容。在例1中即h
而p,无论是指向一个字符串,还是自己声明一个常量字符串。它本质上都是将字符串赋值给这个指针变量p而已。
8. 指针数组的应用
如何玩转指针数组?假设你已经知道什么是指针数组。我们尝试:
例1
int main(){
int a = 1,b = 2,c = 3;
int* ptr[] = {&a,&b,&c};
for(int i=0;i<3;i++){
int* temp = ptr[i];//拿到指针数组
printf("%d ",*temp);//解引用每一个指针
}
return 0;
}
例2
int main(){
int arr1[] = {1,2,3,4,5};
int arr2[] = {1,2,3,4,5};
int arr3[] = {1,2,3,4,5};
int* ptr[] = {arr1,arr2,arr3};//数组名本身就是地址/指针
for(int i=0;i<3;i++){
int* temp_arr = ptr[i];
for(int j=0;j<5;j++){
printf("%d ",*(temp_arr+j));//temp_arr(数组名)是地址,数组名+j就是当前数组第j个地址
//temp_arr[j] <==> *(temp_arr+j) <==> *(ptr[i]+j) <==> ptr[i][j]
}
printf("\n");
}
return 0;
}
temp_arr[j] <==> *(temp_arr+j) <==> *(ptr[i]+j) <==> ptr[i][j]//变形为二维数组
由此,不难发现其实一个指针数组可以模拟二维数组。
只不过,对于以上,我个人更喜欢这种方式:
int main() {
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {1, 2, 3, 4, 0};
int arr3[] = {1, 2, 3, 4, 3};
int *ptr[] = {arr1, arr2, arr3}; //数组名本身就是地址/指针
for (int i = 0; i < 3; i++) {
int *temp_arr = ptr[i];
for (int j = 0; j < 5; j++) {
printf("%d ", *(temp_arr++));//temp_arr <==> ptr[i]可简化
}
printf("\n");
}
return 0;
}
9. 数组指针的应用
一定要复习前面的指针所指向的类型,再来学以下内容。
例1
//数组指针
int main() {
int arr[] = {1, 2, 3};
//arr <==> &arr[0] 是数组的首元素地址 思考:&arr是什么?答:数组地址(数组指针)
//顾名思义:数组指针,就是存放数组地址的指针。
printf("%p %p %p", &arr, arr, &arr[0]);//地址相同,但含义不同
int (*p)[3] = &arr;//(*p)代表这是一个指针,[3]代表p指向的是一个长度为3的数组,int代表该数组的元素是int类型的。
printf("%p ", *p);
return 0;
}
ps: 数组名是数组首元素地址这个公式有两个例外:
1.sizeof(数组名)2. &数组名 这两种情况的数组名都是代表的整个数组
例2:
void print_arr(int (*parr)[3][5], int r, int l) {
//*parr//解引用拿到的是首元素地址:即第一行
for (int i = 0; i < r; i++) {
int (*temp)[5] = *parr + i; //每一行
for (int j = 0; j < l; j++) {
printf("%d ", *(*temp + j));
}
printf("\n");
}
}
int main() {
int arr[3][5] = {1, 2, 3, 4, 5, 2, 3, 4, 5, 6, 3, 4, 5, 6, 7};
print_arr(&arr, 3, 5);//&arr代表是整个数组:传参给数组指针
return 0;
}
思考:
- int (*p)[5] // 数组指针,指向的数组是int [5]
- int (*p[10])[5]是什么?//数组指针的数组
即把原来那个数组指针复制十份。得到:p是一个长度为10的数组,该数组里面存放的是数组指针,每一个数组指针指向的数组类型是int[5]。
10. 指针的传参
实参写什么?
一级指针作为函数参数时,它能接受的参数类型有:只要是地址就可以。
二级指针作为函数参数时,它能接受的参数类型有:1.二级指针2.一级指针的地址 3.存放一级指针的指针数组
11. 函数指针
函数指针是什么?函数指针就是存放函数地址的指针。
讲到&的用法时我们回顾一下数组名加&的区别,数组名就是首元素地址,相当于&数组名[0],而&数组名是取出整个数组的地址,它代表的是整个数组的地址。
不同于数组名,而函数名 、&函数名 是完全等价的。也就是说,无论加不加&,他们都表示取出函数的地址。
int Add(int a, int b) {
return a + b;
}
int main() {
int (*pf)(int, int) = &Add; //&加不加都是代表Add的地址
int x = (*pf)(1, 3);//4
int y = pf(1, 4);//5 //Add <==> pf
printf("%d %d", x, y);
return 0;
}
函数指针的定义很类似于数组指针。比如上例,取出函数Add的地址,给到我们的指针pf,因此(*pf)表示这是一个指针(也是为了防止与后面的括号结合),(*pf)(int, int)表示该指针指向的是函数,最后前面的int 代表该函数的返回类型是int。
(*( void(*)())0) ();
将0强转为函数指针类型,解引用拿到这个函数,再调用函数。
void(* signal(int,void(*)(int)) )(int);
signal是一个函数指针,这个函数指针内,将函数指针作为参数,即函数指针套函数指针。
12. 函数指针数组
void (*p[5]) (int,int)//*p[5]去掉就是它的类型,可见它是一个函数,不加括号会报错
//计算器模拟
int cal_add(int x, int y) {
return x + y;
}
int cal_sub(int x, int y) {
return x - y;
}
int cal_mul(int x, int y) {
return x * y;
}
int cal_div(int x, int y) {
return x / y;
}
int main() {
int (*cal[5]) (int, int) = {0, cal_add, cal_sub, cal_mul, cal_div};//将函数指针存放到函数指针数组中
int op = 999;
while (op != 0) {
printf("请输入两个数\n");
int x, y = 0;
scanf("%d", &x);
scanf("%d", &y);
printf("请选择操作符:exit:0 +:1 -:2 *:3 /:4 \n");
scanf("%d", &op);
int res = cal[op](x, y);//取出函数指针进行调用
printf("%d\n", res);
}
return 0;
}
int (*f_arr[5])(int,int) //函数指针数组
int (*(*p)[5])(int,int) = &f_arr;//要加&表示整个数组,去掉*p就是它的类型
去掉*p后,表示它的类型为函数指针数组,因此叫指向函数指针数组的指针。
13. 回调函数
回调函数就是,把函数作为形参进行处理,最后返回这个形参函数,这样的函数就叫回调函数。
int f1(int x) {
x++;
return x;
}
int f2(int (*pf)(int), int z) {//回调函数
return pf(z);
}
int main() {
int y = f2(f1, 2);
printf("%d", y);
return 0;
}
在例1中,pf作为函数指针充当形参,最后再返回它自己。
首先由于f2(f1,2)是一个回调函数,它把pf充当f1最后其实调用的就是f1(2),得到3;
那么它的实际意义是什么呢?我们再来看一个例子。
假如我们把之前学习函数指针数组用到的计算器程序进行改装一下:
int cal_random(int (*cal)(int,int),int x,int y){
return cal(x,y);
}
int main(){
cal_random(cal_div,3,4);//3/4
cal_random(cal_mul,1,8);//1*8
}
由此可见,我们可以把通过回调函数,来解决多个重复类型函数的定义所带来的代码冗余。此外,这样的代码,更加具备可读性。