一篇文章带你搞定C语言指针

目录

一、初阶了解指针

1.内存和地址

2. 指针变量和地址

2.1 取地址操作符(&)

2.2 指针变量和解引⽤操作符(*)

2.3指针变量的⼤⼩

3. 指针变量类型的意义

3.1 指针的解引用

3.2 指针+-整数

4. const修饰指针

5. 指针运算

5.1 指针+- 整数

5.2 指针-指针

5.3 指针的关系运算

6. 野指针

7. assert断言

8. 指针的使⽤和传址调用

8.1 传址调⽤

二、深⼊理解指针

1. 数组名的理解

2. 一维数组传参的本质

3.二级指针

4. 指针数组

5. 指针数组模拟二维数组

6. 字符指针变量

7. 数组指针变量

8. ⼆维数组传参的本质

9. 函数指针变量


一、初阶了解指针

在学习指针前,我们先了解一下计算机中的内存。

1.内存和地址

在计算机中为了方便管理内存,内存会被划分为以字节为单位的内存空间,也就是说一个内存单元的大小是一个字节,为了方便找到这个内存单元,我们会给每个内存单元一个编号,就像生活中每个房间都有门牌号。当我们有了内存单元的编号,就可以快速的找到内存单元。

在计算机中我们把内存单元的编号也称为地址。                                                                              C语言中给地址起 了新的名字叫:指针。

大家可以理解内存单元的编号 == 地址 == 指针

内存单元

2. 指针变量和地址

2.1 取地址操作符(&)

我们了解了内存和地址,回到C语言中。当我们创建一个变量时,变量被存放在哪儿呢?

答案是内存单元中。

因此变量创建的本质:在内存上开辟空间。

例如下面的代码

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

 我们创建了一个变量a,那么内存中就会开辟一块空间用来存放10。按下F10进入调试模式,看看a被放到了哪里,它究竟有没有编号呢?

如下图,我们看到整型变量a有四个地址,依次存放着0a  00 00  00。第一个字节内容是0a(16进制),表示10。里面确实存放着10,并且前面的一长串16进制表示对应字节的地址。四个字节是连续存放。

a的地址

 那我们在代码中怎么拿到a的地址呢?这里就要用到取地址操作符&,&的作用是取地址。但是需要注意的是,我们拿到的是a所占四个字节的第一个字节的地址(地址较小的那个字节的地址)

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

这里打印出来的是四个字节里的低位地址。

注:每次打印结果都不一样,因为每次创建地址的位置都不一样。 

2.2 指针变量和解引⽤操作符(*)

那我们通过取地址操作符(&)拿到的地址是⼀个数值,⽐如:0x006FFD70,这个数值有时候也是需要存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?

答案是:指针变量中。

int main()
{
	int a = 10;
	int* p = &a;//取出a的地址并存储到指针变量p中
	return 0;
}

这⾥p左边写的是 int*,int代表p指向对象的类型是int,*是在说明p是指针变量。

指针变量就是用来存放地址的。

存放在指针变量中的值,都会被当成地址使用。

我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?

这里我们就需要使用解引⽤操作符*,看下面代码。

int main()
{
	int a = 10;
	int* p = &a;
	*p = 0;//*解引用操作符
//*p的意思就是通过p中存放的地址,找到地址指向的空间,*p就是a,我们再给a赋值0,打印看看结果
	printf("%d\n", a);
	return 0;
}

可以看到打印结果为0,这里我们通过*P其实就得到了a的值,然后我们再给值赋为0,那么a里面的。就变成0了。大家可以仔细理解一下。

2.3 指针变量的大小

这里直接总结,不再代码举例。

•32位平台下地址是32个bit位,指针变量大小是4个字节

• 64位平台下地址是64个bit位,指针变量大小是8个字节

• 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。

3. 指针变量类型的意义

3.1 指针的解引用

上面学会了如何将一个变量的地址进行存放。那当我们拿到地址又该使用地址所指向的内容呢?

这里便需要指针的解引用。

我们看下面代码

int main()
{
	int n=0x11223344;
	int* p = &n;//取n的地址存放到指针变量p中,p为int类型。
	*p =0;//*p表示对p解引用,*p表示存储在p中的地址所指向的内容,等同于0x11223344,
          //然后我们将0赋给*p,修改p所指向的内容,因此0x11223344变为0x00000000。
	return 0;
}

这里我们可以看到,四个字节的内容全部变成了0。取n的地址存放到指针变量p中,p为int类型。
*p表示对p解引用,*p表示存储在p中的地址所指向的内容,等同于0x11223344,
  然后我们将0赋给*p,修改p所指向的内容,因此0x11223344变为0x00000000。

那如果我们将代码改一改

int main()
{
	int n=0x11223344;
	char* p = &n;
	*p =0;
	return 0;
}

我们可以看到只改变了一个字节。这是因为int类型地址存放时是四个字节,char是一个字节,&n地址时只拿到了四个字节里面的低位地址,就是最小的地址。所以对*p修改只修改一个字节所指向的内容,其余三个字节的地址没有收到影响。

因此我们可以得到结论, 指针类型决定了指针在解引用操作时的权限,也就是一次解引用访问几个字节,char*类型的指针解引用访问一个字节,int*类型的指针解引用访问四个字节。

3.2 指针+-整数

直接看代码

int main()
{
	int n=0x11223344;
	char* pc = &n;
	int * p = &n;
	printf("p     =%p\n", p);//打印p所存放的地址
	printf("p+1   =%p\n", p+1);//打印地址p+1后的地址

	printf("pc    =%p\n", pc);
	printf("pc+1  =%p\n", pc+1);

	return 0;
}

 我们可以看到p+1的地址跳过了四个字节,pc+1的地址跳过了一个字节。

这是为什么呢?

不妨可以大胆猜一下,int类型占用内存四个字节,因此地址+1跳过一个整形,所以地址向后加上4个字节,cha类型占用内存一个字节,地址+1时向后跳过一个char,地址加一个字节。

这里我们可以得出结论,指针类型决定了,指针进行+1/-1操作的时候,一次跳过几个字节。

下面我们可以用上面所学的知识,用指针代码打印数组。大家可以自己去尝试。不懂可以注释。

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = &arr[0];//p存放数组首元素的地址
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));//因为数组在内存中存放时,
                                //每一个元素的地址都是连续的,
                                //因此拿到第一个元素的地址进行+i,便可得到对应i下标的元素的地址
	}
	return 0;
}

4. const修饰指针

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。 但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。

int n=100;
n=20;

这里我们有一个变量n,我们可以轻易的修改n的值。

当我们加上const时,此时再进行修改,代码会报错无法修改。

const int n=100;
n=10;//这里会报错,不能修改

我们再看一段代码

const int n=100;
int *p=&n;
*p=20;

这里我们通过指针变量p取地址n,然后解引用修改n的值,发现可以修改成功。

这里const从语法上限制直接更改n无法更改,但是我们将n的地址给p,通过p可以更改。

显然这里有点不合理,既然想要n不能更改,那就应该无论什么方法都不可以更改。

所以这里我们有一种方法,那就是const修饰指针。

int main()
{
	int m = 100;
	int n = 10;

	const int* p = &n;
//	*p = 0;   这句报错,const 在左边限制*p,p所指向的内容不可以变,可以用p = &m;
    int* const p = &n; 
//	p = &m; 这句报错 const在*右边,限制的是指针变量本身,不能修改指针变量的指向,可以用*p = 0;

	return 0;
}

总结:

• const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。

• const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。

5. 指针运算

指针的基本运算可以分为三种

指针+-整数

指针-指针

指针的关系运算

5.1 指针+- 整数

指针加减整数,与指针类型有关系。比如int类型,加1就是跳过四个字节,char类型加1,跳过一个字节。比如,数组在内存中存放的地址是连续的,我们只需要知道数组首元素地址,便可得到每一个元素的地址。这里可以通过指针+-整数实现。

代码举例

#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;
}

下面还有一个代码应用,大家可以自行理解。

int main()
{
	char arr[] = "abcedef";
	printf("%s\n", arr);
	char* pc = &arr[0];//取字符串首元素的地址
	while (*pc != '\0')//字符串的结尾是\0,如果指针所指向的元素不是\0,那么循环继续。
	{
		printf("%c", *pc);
		pc++;
	}
	return 0;
}

同样都是打印字符串,用了两种不同的方法,有助于理解指针的+-。

5.2 指针-指针

这里先看代码。

int main()
{
	int arr[10] = { 0 };
	int  ret = &arr[9] - &arr[0];//取数组中下标为9元素的地址减去下标为0元素的地址
	printf("%d\n", ret);
	return 0;
}

大家想一想,连个指针相减得到的是什么?

结果ret=9. 解释:指针减指针得到的是之前的元素个数。两个指针相减前提的条件是,两个指针指向通一块空间,得到的值得绝对值是指针和指针之间的元素个数。

5.3 指针的关系运算

指针的关系运算就是比较指针大小。

//指针的关系运算
#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;
}

我们打印数组这里便可用到指针的关系运算。

创建一个10个元素的地址,将首元素的地址给p,然后只要p的地址小于第十个元素后面的地址就进行打印,然后p+1.使用这种方法也可以完成数组的打印,涉及到了指针的关系运算,需要比较p的地址和第11个元素的地址。

6. 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

野指针有以下几个原因。

1. 指针未初始化

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

在内存中一块空间,你想要使用,你得申请拿到这块空间才能使用!

2. 指针越界访问

假如一个数组arr,里面有10个元素,1,2,3,4,5,6,7,8,9,10。

那我们for循环打印数组元素,如果我们打印完十个元素后继续循环打印,这里就是越界访问了。程序就会出现问题。第十个后面元素的地址就是野指针。

3. 指针指向的空间释放

#include <stdio.h>
int* test()
{
 int n = 100;
 return &n;
}
int main()
{
 int*p = test();
 printf("%d\n", *p);
 return 0;

这里p得到的是n地址,但是n在函数结束时已经不存在了,但是p里面还存在n那块空间的地址,这就属于野指针。

那么如何避免野指针?

1、指针初始化

最有效的就是指针初始化。如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.

NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。本质是0。

int *a=NULL;

这里就是空指针,防止出现野指针。空指针不可以直接访问。

2、小心指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。

3、指针变量不再使⽤时,及时置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;
}

内存是一块区域,里面划分为栈区、堆区、静态区。局部变量一般存放在栈区。

7. assert断言

我们使用指针的过程中,经常能遇到assert。

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。

 assert(p != NULL);

这样如果p是空指针程序就会报错,直接告诉你哪里出问题了。相比if语句更好。

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG 。

#define NDEBUG
#include <assert.h>

然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移 除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。

8. 指针的使⽤和传址调用

8.1 传址调⽤

写⼀个函数,交换两个整型变量的值。

#include <stdio.h>
void Swap1(int x, int y)
{
 int tmp = x;
 x = y;
 y = tmp;
}

int main()
{
 int a = 0;
 int b = 0;
 scanf("%d %d", &a, &b);
 printf("交换前:a=%d b=%d\n", a, b);
 Swap1(a, b);
 printf("交换后:a=%d b=%d\n", a, b);
 return 0;
}

 我们发现其实没产⽣交换的效果,这是为什么呢?a和b传参时调用的是a和b的本身,叫做传值调用,函数实参传递给形参后,形参是实参的一份临时拷贝,所以对形参的修改不影响实参。形参有自己的独立空间。

总结:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。所以Swap是失败的了。

修改代码

#include <stdio.h>
void Swap2(int*px, int*py)
{
 int tmp = 0;
 tmp = *px;
 *px = *py;
 *py = tmp;
}
int main()
{
 int a = 0;
 int b = 0;
 scanf("%d %d", &a, &b);
 printf("交换前:a=%d b=%d\n", a, b);
 Swap1(&a, &b);
 printf("交换后:a=%d b=%d\n", a, b);
 return 0;
}

 我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传 递给了函数,这种函数调⽤⽅式叫:传址调⽤。

大家可以用上面的知识写一个函数,模拟实现strlen函数。这里就不在演示了。

二、深⼊理解指针

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 = %p\n", arr);
 return 0;
}

 我们可以看到打印结果一样。

数组名是数组首元素的地址,但是有两个例外:

1、sizeof(数组名),这里的数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小,单位是字节

2、&数组名,这里的数组名表示整个数组,&数组名:取出的是整个数组的地址

除此之外,遇到的所有的数组名都是数组首元素的地址。

2. 一维数组传参的本质

数组名是数组⾸元素的地址,那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组⾸元素的地址。

⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

3.二级指针

int a=10;

int *p=&a;

a是整形变量,占用4个字节的空间,a是有自己的地址,&a拿到的就是a所占4个字节的第一个字节的地址。

p是指针变量,占用4/8个字节的空间,p也是有自己的地址,&p就拿到了p的地址。

那如果我们想存放p的地址呢?

int * * pp=&p;

pp也是指针变量,这里*pp表示是指针,int*表示的是p的类型。因此前面是int**。

一颗*叫做一级指针,**表示二级指针。

int ***ppp=&pp;//ppp是三级指针

4. 指针数组

 什么是指针数组呢?我们可以类比一下之前学过的。

整形数组-存放整形的数组 int arr[10];

字符数组-存放字符的数组 char ch[5];

指针数组-存放指针的数组        int * parr[5];  char *pc[10]; ......

用来存放指针的数组叫做指针数组

 如图,每一个元素都是int*类型的,因此数组的类型为int*。叫做指针数组。

5. 指针数组模拟二维数组

#include <stdio.h>
int main()
{
 int arr1[] = {1,2,3,4,5};
 int arr2[] = {2,3,4,5,6};
 int arr3[] = {3,4,5,6,7};
 //数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
 int* parr[3] = {arr1, arr2, arr3};
 int i = 0;
 int j = 0;
 for(i=0; i<3; i++)
 {
 for(j=0; j<5; j++)
 {
 printf("%d ", parr[i][j]);
 }
 printf("\n");
 }
return 0;
}

这里用三个一维数组,用指针数组将三个数组联系起来,模拟实现二维数组。大家可以自行实现加深理解。

这里如果不理解parr [ i ] [ j ],可以这样理解*(parr[i]+j),得到的值是一样的。[ ]就想当于解引用了。

6. 字符指针变量

int main()
{
	char* ch = 'a';
	char* pc = &ch;//pc就是字符指针变量
	//字符指针变量时用来存放地址的

	char* p = "abcdefe";
	//不是将abcdefe\0 字符串存放到p中,而是将首字符a的地址存储到p中
	//"abcdefe"这是一个常量字符串,是不能被修改的
	return 0;
}

如何理解字符串存放到p中,是将a的地址存储到p中呢?因为字符串的地址是连续存放的,所以得到首字符地址也就能得到整个字符串元素。注意的是"abcdefe"这是一个常量字符串,是不能被修改的。

7. 数组指针变量

我们之前血虚指针数组,指针数组是数组,是存放指针的数组。那数组指针是什么呢?

我们依然类比来看:

字符指针-指向字符的指针,存放的是字符的地址   char ch='w'; char* pc=&ch;

正定型指针-指向整形的指针,存放的是整形的地址   int n=100;  int *p=&n;

数组指针-指向数组的地址,存放的是数组的地址   int arr[10];  int (*p)[10]=&arr;

 

int main()
{
  int arr[6];
  int *p=arr;
  int (*ptr)[6]=&arr;//数组的地址
   // ptr 是数组指针
  return 0;

}

8. ⼆维数组传参的本质

如下图,二维数组的每一行是一维数组,这个一维数组可以看做是二维数组的一个元素,所以二维数组也可以认为是一维数组的数组。那么二维数组的数组名表示首元素的地址,就是第一行的地址!也就是一个一维数组的地址。

直接上代码,结合数组指针打印二维数组,理解二维数组传参本质。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>


test(int(*p)[5], int x, int y)//这里得到的是一维数组的地址,因此形参定义数组指针接收。
{
	int i = 0;
	for (i = 0; i < x; i++)
	{
		int j = 0;
		for (j = 0; j < y; j++)
		{
			printf("%d ", *(*(p + i) + j));//arr[i][j]这样写更好理解
		}
		//这里p存放的是二维数组第一行元素的地址,理解成一个一维数组,
		//我们将二维数组看成是一维数组构成的,每一行相当于一维数组, 
		// 那么一维数组地址p+i,就得到第i行的一维数组的地址
		// *(p+i)得到第i行首元素的地址,然后再+j,整体解引用得到的是第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;
}

 这里注释讲解的很详细了,不理解可以看看注释。

9. 函数指针变量

数组指针-是指针-是指向数组的指针,是存放数组地址的指针

函数指针-是指针-是指向函数的指针-是存放函数地址的指针!!

那么函数是否有地址呢? 我们做个测试:

#include <stdio.h>
void test()
{
 printf("hehe\n");
}
int main()
{
 printf("test: %p\n", test);
 printf("&test: %p\n", &test);
 return 0;
}

输出结果如下:

test: 005913CA

&test: 005913CA

可以看到,函数也是有地址的,并且两个打印结果是一样的。因此对于函数,&函数名和函数名都是函数的地址。

函数既然有地址,那么怎么存放呢?

#include <stdio.h>
int Add(int x, int y)
{
 return x+y;
}
int main()
{
 int(*pf3)(int, int) = Add;// pf3函数指针变量,形参的xy可以不写,只需要交代类型
 
 printf("%d\n", (*pf3)(2, 3)); // 函数指针变量的使用,输出结果5
 printf("%d\n", pf3(3, 5)); // 这里两种写法都可以,直接pf3和(*pf3)都可以。输出结果8
 return 0;
}

 到此,C语言中指针的知识已经讲解完成了。感谢大家的观看,喜欢的可以点赞收藏多多支持一下。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值