一、什么是指针
指针其实就是地址,地址就是内存单元的编号。为了方便高效的管理内存,计算机中的内存被划分为一个个的内存单元,每个内存单元的大小是一个字节。每个内存单元都有⼀个编号,有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起了一个名字叫:指针。
所以我们可以理解为:内存单元的编号==地址==指针
1.指针变量和地址
通过取地址操作符(&)可以拿到一个变量的地址,地址一般以16进制显示,比如:0x006FFD70
用(*) 可以定义指针变量,比如:int *p; int * 是类型,p 是变量名,用(&)拿到的地址就可以存到这个指针变量里面,即 int a = 7; int *p = &a; 此时 p 里面存放的就是整形变量 a 的地址,用解引用操作符 * 就可以通过地址找到对应的变量,*p == a ,*p 就是顺着p 里面存放的地址找到这个地址里面存放的内容。*p = 4 就等于 a = 4;
int *p 中的 int 指的是 p 指向的对象的类型。比如要存char 类型变量的地址的指针变量:char c = 'a'; char *p = &c;
2.指针变量类型的意义
指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。
char* 的指针+/-n(整数),就是向前或者向后移动n个字节,int* 的指针+/-n(整数),就是向前或者向后移动n*4个字节
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
3.指针变量的大小
• 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
• 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
• 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。
4.const修饰指针
前面已经讲了变量可以通过指针变量来修改,如果不想让人通过指针修改这个变量,就需要用 const 限制这个指针变量。比如:int a = 5; int const *p = &a; 如果再 *p=3; 就会报错,const 也可以放在 int 左边 const int *p 。如果把const 放在 * 右边就限制了 p 使 p 的指向不能修改,即不能p=&b;
总结:
const 放在*左边限制的是*p ,保证不能通过指针来修改p 指向的对象,但是可以改变指针的指向
const 放在*右边限制的是p,保证不能修改p 的指向,但是指针向的内容,可以通过指针改变。
5.指针运算
指针的基本运算有三种,分别是:
• 指针+-整数
• 指针-指针
• 指针的关系运算
5.1指针+-整数
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。
int arr[10]={1,2,3,4,5,6,7,8,9,10}; int *p=&arr[0]; p+1 就指向arr[1],p+2 就指向arr[2]
5.2指针-指针
指针减指针表示两个指针指向的内存位置之间相隔多少个元素,而不是字节数。
这种操作有两个前提条件:
1.两个指针指向同一块空间,意味着这两个指针的类型是一致的。
2.大的地址减去小的地址是一个正数,按照常理,小的地址减去大的地址是一个负数。
5.3指针的关系运算
数组在内存中存放时,随着下标的增长地址从低到高
&arr[0] 拿到的就是0x000000d98bcffc18
比如用指针的关系运算作判断条件
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int sz = sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
6.野指针
6.1 野指针成因
1. 指针未初始化
2. 指针越界访问
3. 指针指向的空间释放
6.2 如何规避野指针
1 指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
2 ⼩⼼指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。
4 避免返回局部变量的地址
局部变量只在局部变量所在函数运行时存在,函数运行完局部变量所占的内存就被释放了。再访问这个变量就是非法访问了。
7.指针的使⽤和传址调⽤
7.1指针的使⽤
7.2 传值调⽤和传址调⽤
传值调用,传递的是实参的值,是把实参的值给形参,形参是实参的一份临时拷贝,对形参的修改不影响实参。
传址调⽤,传递的是参数的地址,在函数内部可以通过指针间接修改主调函数中的变量
函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。
二、指针和数组
1. 数组名的理解
数组名其实就是数组首元素的地址,但有两个例外,sizeof(数组名) 和 &数组名 。
sizeof(数组名)中的数组名表示的是整个数组,sizeof(数组名)计算的是整个数组的大小。
&数组名,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
输出结果
我们使⽤ &arr[0] 的⽅式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,⽽且是数组⾸元素的地址,所以&arr[0] 可以换成 arr
补充&arr与arr的区别(arr是数组)
arr+1跳过的是一个元素,&arr+1跳过的是一个数组
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0]+1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr+1);
return 0;
}
运行结果是
1 &arr[0] = 0077F820
2 &arr[0]+1 = 0077F824
3 arr = 0077F820
4 arr+1 = 0077F824
5 &arr = 0077F820
6 &arr+1 = 0077F848
2. 使⽤指针访问数组
include <stdio.h>
int main()
{
int arr[10] = {0};
//输⼊
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
//输⼊
int* p = arr;
for(i=0; i<sz; i++)
{
scanf("%d", p+i);
//scanf("%d", arr+i);//也可以这样写
}
//输出
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
数组名arr是数组⾸元素的地址,可以赋值给p,其实数组名arr和p在这⾥是等价的,那我们可以使⽤arr[i]可以访问数组的元素, 理论上也可以用 p[i] 访问数组,实际上也是如此。所以本质上p[i]是等价于*(p+i)。同理arr[i]应该等价于*(arr+i),数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。
3. ⼀维数组传参的本质
数组名是数组⾸元素的地址,那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组⾸元素的地址。
所以函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。那么在函数内部我们写sizeof(arr) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是数组的⼤⼩(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
总结:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
4. ⼆级指针
指针变量也是变量,是变量就有地址,存放指针变量的指针变量就是二级指针
• *ppa 通过对ppa中的地址进⾏解引⽤,这样找到的是 pa , *ppa 其实访问的就是 pa .
1 int b = 20;
2 *ppa = &b;//等价于 pa = &b;
• **ppa 先通过 *ppa 找到 pa ,然后对 pa 进⾏解引⽤操作: *pa ,那找到的是 a .
1 **ppa = 30;
2 //等价于*pa = 30;
3 //等价于a = 30;
*ppa 就是通过ppa里面的地址找到这个地址里面存放的内容,这里就是 0x0012ff50,即pa
再对*ppa 解引用**ppa 即 *(*ppa) 就等价于*pa ,*pa 就是10,所以**ppa 访问的就是a
5. 指针数组
整型数组,是存放整型的数组,字符数组是存放字符的数组。指针数组就是存放指针的数组。
指针数组的每个元素都是⽤来存放地址(指针)的。
指针数组的每个元素是地址,⼜可以指向⼀块区域。
6. 数组指针变量
数组指针变量是:存放的是数组的地址,能够指向数组的指针变量 .
int (*p)[10];
p先和*结合,说明p是⼀个指针变量,然后指针指向的是⼀个⼤⼩为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫数组指针。
要注意:[ ]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。
数组指针变量是⽤来存放数组地址的,如果要存放个数组的地址,就得存放在数组指针变量中。
例如:
int arr[10] = {0};
int(*p)[10] = &arr;
int(*p)[10] = &arr; 中 int 是p 指向的数组的元素的类型,p 是变量名,[10] 指p 指向的数组中元素的个数
7. ⼆维数组传参的本质
⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。
int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};
所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。
#include <stdio.h>
void test(int (*p)[5], int r, int c)
{
int i = 0;
int j = 0;
for(i=0; i<r; i++)
{
for(j=0; j<c; j++)
{
printf("%d ", *(*(p+i)+j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};
test(arr, 3, 5);
return 0;
}
总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。
三、字符指针变量
字符指针变量就是指向存放字符变量的指针
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
如果写成char* pstr = "hello" 是把字符串 hello ⾸字符的地址放到了pstr中。
四、函数指针
1. 函数指针变量
函数指针变量就是存放函数地址的指针变量
函数指针类型解析:
int (*pf3) (int x, int y)
| | |
| | |
| | pf3指向函数的参数类型和个数的交代
| 函数指针变量名
pf3指向函数的返回类型
函数指针变量的使⽤:
#include <stdio.h>
int Add(int x, int y)
{
return x+y;
}
int main()
{
int(*pf3)(int, int) = Add;
printf("%d\n", (*pf3)(2, 3));
printf("%d\n", pf3(3, 5));
return 0;
}
结果:5
8
2. 函数指针数组
数组是⼀个存放相同类型数据的存储空间,函数指针数组就是每个元素都是函数指针的数组。
函数指针数组的定义: int (*parr[3])();
parr1 先和 [ ] 结合,说明parr是数组, 是int (*)() 类型的函数指针。
3. 转移表
函数指针数组的⽤途:转移表
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void menu()
{
printf("********************\n");
printf("*** 1.add 2.sub ****\n");
printf("*** 3.mul 4.div ****\n");
printf("*** 0.exit *********\n");
printf("********************\n");
}
void com(int(*p)(int, int))
{
int x = 0;
int y = 0;
printf("请输入操作数\n");
scanf("%d %d", &x, &y);
printf("%d\n", p(x, y));
}
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
//转移表代码实践
int (*p[5])(int, int) = { 0,add,sub,mul,div };
int input = 0;
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
com(p[input]);
break;
case 2:
com(p[input]);
break;
case 3:
com(p[input]);
break;
case 4:
com(p[input]);
break;
case 0:
break;
default:
printf("输入错误,请重新选择\n");
break;
}
} while (input);
return 0;
}
五、回调函数
回调函数就是⼀个通过函数指针调⽤的函数。
如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
void calc(int(*pf)(int, int))
{
int ret = 0;
int x, y;
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 1;
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
printf("退出程序\n"); break;
default:
printf("选择错误\n"); break;
}
} while (input);
return 0;
}
六、sizeof和strlen的对⽐
1.sizeof
sizeof 是操作符 ,用来计算变量所占内存内存空间的⼤⼩,单位是字节,如果操作数是类型的话,计算的是使⽤类型创建的变量所占内存空间的⼤⼩。
sizeof 只关注占⽤内存空间的⼤⼩,不在乎内存中存放什么数据。
#inculde <stdio.h>
int main()
{
int a = 10;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof a);
printf("%d\n", sizeof(int));
return 0;
}
2.strlen
strlen 是C语⾔库函数,功能是求字符串⻓度。函数原型如下:
size_t strlen ( const char * str );
统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。strlen 函数会⼀直向后找 \0 字符,直到找到为⽌,所以可能存在越界查找。