【C语言】指针理解(一)

本文详细介绍了指针的概念,包括内存地址的分配、直接访问与间接访问的方式,以及指针变量的定义、取地址操作符和解引用操作符的使用。此外,还探讨了void指针、const修饰指针、指针运算、野指针和指针作为函数参数的传值调用与传址调用的区别。
摘要由CSDN通过智能技术生成

前言

什么是指针

为了说清楚什么是指针,必须先弄清楚数据在内存中是如何存放的,又是如何读取的

内存与地址

如果在程序中定义了一个变量,在对程序进行编译时,系统就会给这个变量分配内存单元。编译系统根据程序中定义的变量类型,分配一定长度的空间.例如,Visual C++ 为整型变量分配分配4个字节,为单精度浮点型变量分配4个字节,为字符型变量分配1个字节。内存区的每一个字节都有一个编号,这就是“地址”,它相当于旅馆中的房间号。在地址所标志的内存单元中存放的数据则相当于旅馆房间中居住的旅客
每个内存单元也都有⼀个编号(这个编号就相当于宿舍房间的⻔牌号),有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。
所以我们可以理解为:
内存单元的编号 == 地址 == 指针

对地址的访问方式

直接访问

假如在代码中有一段语句

scanf("%d",&i);

在执行时,把键盘输入的值送到地址为某一内容开始的整形存储单元中。
如果有语句

k = i + j;

就从地址中取出 ij的值,将他们相加后的和送到k所占的字节单元中,这种直接按变量名进行访问的,成为直接访问

间接访问

即将变量i的地址存放在另一变量中,然后通过该变量来找到变量i的地址,从而访问i变量,我们可以通过一段代码来理解;

#include<stdio.h>

int main()
{
	int i = 10;
	int* a = i;
	int* p = a;
	printf("%d\n", a);
	printf("%d\n", p);

	return 0;
}

最终的结果输出为
在这里插入图片描述
用文字来理解就是打开一个抽屉A,为了安全起见,将钥匙A放入B抽屉锁起来,如果要打开抽屉A,就需要先找出钥匙B,打开抽屉B,取出钥匙A,再打开A抽屉

1.指针变量

存放地址的变量是指针变量,它用来指向另一个对象(如变量、数组、函数等)指针变量就是地址变量,指针变量的值就是地址(即指针)
在简单知道了指针变量的定义后,我们还需要知道两个相关的操作符;

取地址操作符(&)

当我们在代码中创建了一个变量是,系统就在内存中申请了一块空

int main()
{
	int a = 10;
	return 0;
}

上述的代码就是创建了整型变量a,内存中
申请4个字节,⽤于存放整数10,其中每个字节都
有地址,上图中4个字节的地址分别是:
0x0019FAB0
0x0019FAB1
0x0019FAB2
0x0019FAB3
在这里插入图片描述
那我们如果想打印出来a的地址,就需要借助(&

int main()
{
	int a = 10;
	&a;
	printf("%p\n", &a);
	return 0;
}

&a取出的是a所占4个字节中地址较⼩的字节的地
址。按照上面所写的就是0x0019FAB0

解引用操作符(*)

我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?
在现实⽣活中,我们使⽤地址要找到⼀个房间,在房间⾥可以拿去或者存放物品。
C语⾔中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)
指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符(*)。

1#include <stdio.h>
2int main()
3{
4	 int a = 100;
5 	 int* pa = &a;
6 	 *pa = 0;
7	 return 0;
8}

上⾯代码中第6⾏就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间,
pa其实就是a变量了;所以pa = 0,这个操作符是把a改成了0.
有同学肯定在想,这⾥如果⽬的就是把a改成0的话,写成 a = 0; 不就完了,为啥⾮要使⽤指针呢?
其实这⾥是把a的修改交给了pa来操作,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活,
后期慢慢就能理解了。

定义指针变量

一般形式为:类型名 * 指针变量名

int main()
{
	int a = 10;
	int *pa = &a
	return 0;
}

这⾥pa左边写的是 int* , * 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型(int)
类型的对象。
那如果有⼀个char类型的变量ch,ch的地址,要放在什么类型的指针变量中呢?

char ch = 'w';
pc = &ch;

一个变量的指针的含义包括两个方面,一是以存储单元编号表示的纯地址(如编号为2024的字节),一是它指向的存储单元的数据类型(如int、char、float等)

引用指针变量

在引用指针时,有三种情况:
(1)给指针变量赋值。如:

p = &a;   //把a的地址赋给指针变量p

指针变量p的值是变量a的地址,p指向a。
(2)引用指针变量指向的变量。
如果已执行“p = &a;”,即指针变量p指向了整型变量a,则

printf(“%d\n”,*p);

其作用是以整数形式输出指针变量p所指向的变量的值,即变量a的值。
如果有语句

*p = 1

(3)引用指针变量的值。如:

printf("%o",p);

作用是以八进制数形式输出指针变量p的值,如果p指向了a,就是输出了a的地址,即&a
举一个运用指针变量的例子,交换两个指针变量的值,来从大到小输出a和b;

#include<stdio.h>

int main()
{
	int a, b;
	int* p1, * p2,* p;
	scanf("%d,%d", &a, &b);
	p1 = &a;
	p2 = &b;
	if (a < b)
	{
		p = &a;
		p1 = p2;
		p2 = p;
	}
	pritnf("a = %d,b = %d\n", a, b);
	printf("max = %d,min = %d\n", *p1, *p2);
	return 0;
}

void指针

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指
针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的±整数和解引⽤的运算
void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算。
那么 void* 类型的指针到底有什么⽤呢?
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以
实现泛型编程的效果。使得⼀个函数来处理多种类型的数据,

const修饰指针

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。
但是如果我们希望⼀个变量加上⼀些限制,不能被修改,这就是const的作⽤。
当在变量中加入一个const,就在语法上对指针变量做出了限制。使得它在某种情况下不能被修改;
• const如果放在的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
但是指针变量本⾝的内容可变。
• const如果放在
的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指
向的内容,可以通过指针改变。

#include <stdio.h>
//代码1
void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}
void test2()
{
	//代码2
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;//ok?// 报错
	p = &m; //ok?
}
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20; //ok?
	p = &m; //ok?// 报错
}
void test4()
{
	int n = 10;
	int m = 20;
	int const* const p = &n;
	*p = 20; //ok? 报错
	p = &m; //ok? 报错
}
int main()
{
	//测试⽆const修饰的情况
	test1();
	//测试const放在*的左边情况
	test2();
	//测试const放在*的右边情况
	test3();
	//测试*的左右两边都有const
	test4();
	return 0;
}

指针运算

指针 ± 整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。

#include <stdio.h>
//指针+- 整数
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0];
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 for(i=0; i<sz; i++)
 {
 printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
 }
 return 0;
}

&arr[0]在这里代表首元素地址,*(p+ i)会按顺序打印出数组的元素,当i= 1是打印出的就是数组第二个元素

指针 ± 指针

//指针-指针
#include <stdio.h>
int my_strlen(char *s)
{
 char *p = s;
 while(*p != '\0' )
 p++;
 return p-s;
}
int main()
{
 printf("%d\n", my_strlen("abc"));
 return 0;
}

指针之间相减,最终得到的是两个指针之间相差的元素数,结果的数字表示两个地址在内存中间隔多少个指针类型的字节倍数。(但两个指针之间不能进行加法运算,这是非法操作,也没有意义,且进行减法运算时,指针的类型必须要相等)这种减法一般用在数组方面,来表示两个数组之间相隔多少
虽然不能进行加法,却能进行递增和递减,在上述代码中就用到了递增来打印数组全部的元素,可见每一次递增,就会指向下一个元素的存储单元。递减则是指向了当前元素上一个元素,而每次递增递减跳过的字节数则由指针类型来决定;

指针之间的关系运算

本质是比较指针的大小;允许指向数组元素的指针与指向数组元素的最后一个元素的指针进行比较,但不允许与指向数组元素的第一个元素之前的指针进行比较;

//指针的关系运算
#include <stdio.h>
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0];
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 while(p<arr+sz) //指针的⼤⼩⽐较
 {
 printf("%d ", *p);
 p++;
 }
 return 0;
}

野指针

  1. 指针未初始化
#include <stdio.h>
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
 *p = 20;
 return 0;
}
  1. 指针越界访问
#include <stdio.h>
int main()
{
 int arr[10] = {0};

 int *p = &arr[0];
 int i = 0;
 for(i=0; i<=11; i++)
 {
 //当指针指向的范围超出数组arr的范围时,p就是野指针
 *(p++) = i;
 }
 return 0;
}
  1. 指针指向的空间释放
#include <stdio.h>
int* test()
{
 int n = 100;
 return &n;
}
int main()
{
 int*p = test();
 printf("%d\n", *p);
 return 0;
}

如何去规避错误
.1 指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址
会报错。

初始化如下:

#include <stdio.h>
int main()
{
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL;
 
 return 0;
}

2 ⼩⼼指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是
越界访问。
3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的
时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,
同时使⽤指针之前可以判断指针是否为NULL。
我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来,
就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起
来。
不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我
们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去
使⽤。

int main()
{
 int arr[10] = {1,2,3,4,5,67,7,8,9,10};
 int *p = &arr[0];
 for(i=0; i<10; i++)
 {
 *(p++) = i;
 }
 //此时p已经越界了,可以把p置为NULL

 p = NULL;
 //下次使⽤的时候,判断p不为NULL的时候再使⽤
 //...
 p = &arr[0];//重新让p获得地址
 if(p != NULL) //判断
 {
 //...
 }
 return 0;
}

6.2.4 避免返回局部变量的地址
如造成野指针的第3个例⼦,不要返回局部变量的地址。

指针变量作为函数参数

函数的参数不仅可以是整形、浮点型、字符型等数据、还可以是指针类型。它的作用是将一个变量过的地址传送到另一个函数中。
举个例子来说明,要求对输入的两个整数按大小顺序输出。

int main()
{
	void swap(int* p1, int* p2);     //声明
	int a, b;
	int* pointer_1, * pointer_2;
	scanf("%d,%d", &a, &b);
	pointer_1 = &a;
	pointer_2 = &b;
	if (a < b)
	{
		swap(pointer_1, pointer_2);
		printf("max = %d,miin = %d\n", a, b);
		
	}
	return 0;
}

void swap(int* p1, int* p2)
{
	int temp;
	temp = *p1;                //使两个指针互换;
	*p1 = *p2;
	*p2 = temp;
}

swap是用户自定义函数,它的作用是交换两个变量(a和b)的值。swap函数的两个形参p1和p2是指针变量。程序运行时,先执行main函数,输入a和b的值(现输人5和9)。然后将a和b的地址分别赋给intx变量pointer_1和 pointer_2,使 pointer_指向 a,pointer_2指向b,见图8.5(a)。接着执行i语句,由于a<b,因此执行swap 函数。注意实参pointer_1和pointer_2是指针变量,在函数调用时,将实参变量的值传送给形参变量,采取的依然是“值传递”方式。因此虚实结合后形参p1的值为&a,p2的值为&b.见图8.5(b)。这时pl和pointer_l都指向变量a,p2和pointer_2都指向b。接着执行 swap函数的函数体,使pl和p2的值互换,也就是使a和b的值互换。互换后的情况见图8.5©。函数调用结束后,形参p1和p2不复存在(已释放),情况如图8.5(d)所示。最后在main函数中输出的a和b的值已是经过交换的值(a=9,b=5)。

传值调用和传址调用

在上面的代码中就用到了传址调用,将地址传到了函数中实现了数之间的互换;
1 . 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
实际参数(实参): 真实传给函数的参数,叫实参。 实参可以是:常量、变量、表达式、函数等。 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数(形参): 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
2 . 传址调用
传址调用就是把函数外部创建变量的内存地址传递给函数的一种调用函数的方式

int main()
{
	void swap(int* p1, int* p2);     //声明
	int a, b;
	int* pointer_1, * pointer_2;
	scanf("%d,%d", &a, &b);
	pointer_1 = &a;
	pointer_2 = &b;
	if (a < b)
	{
		swap(pointer_1, pointer_2);
		printf("max = %d,miin = %d\n", a, b);
		
	}
	return 0;
}

void swap(int* p1, int* p2)//p1接收a的地址,p2接收b的地址;
{
	int temp;
	temp = *p1;                //使两个指针互换;
	*p1 = *p2;
	*p2 = temp;
}

在该代码中,我们将变量a,b的地址传给了swap函数,那么p1,p2复制了a,b的地址并存储了起来之后,我们再通过解引用操作访问到a,b进行交换的操作,那么即使p1,p2会被销毁,但是我们通过地址已经达到了交换a,b的值的目的
当你想要对传递进函数的实参经过函数内的操作发生改变,那么就用传址调用。

  • 35
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值