如何理解C语言中的指针

一、什么是指针

指针其实就是地址,地址就是内存单元的编号。为了方便高效的管理内存,计算机中的内存被划分为一个个的内存单元,每个内存单元的大小是一个字节。每个内存单元都有⼀个编号,有了这个内存单元的编号,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 字符,直到找到为⽌,所以可能存在越界查找。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值