C语言进阶学习笔记二指针的进阶(重点必看+代码图解+练习)

Posted 大家好我叫张同学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言进阶学习笔记二指针的进阶(重点必看+代码图解+练习)相关的知识,希望对你有一定的参考价值。

在C语言基础阶段,我们学习过指针相关的一些基础内容,比如说:

1.指针是一个变量,用来存放地址,地址是唯一标识一块内存空间
2.指针的大小是固定的4 / 8个字节(32位平台 / 64位平台)
3.指针是由类型,指针的类型决定了指针的 + -整数的步长,指针解引用操作时候的权限
4.指针的运算

本篇文章及后面的几篇文章将会更加详细的去介绍和学习指针的进阶部分。(指针的内容在数据结构中会经常用到,所以一定要好好学习,打好基础~)
在这里插入图片描述



1、字符指针

在指针的类型中我们知道有一种指针类型为字符指针char*;
一般使用方式:
在这里插入图片描述

这种方式通过字符指针解引用改变原字符变量的内容。
实际上,字符指针也可以这样使用:
在这里插入图片描述

这种方式是将字符串的首地址放到指针中,通过指针可以找到该字符串(千万不要理解成将字符串放到指针里面去,这是不可能的)。(类似与数组名就是首元素地址,但是跟数组还是有所区别的,这个字符串是一个常量字符串,无法被改变,如下图:)

在这里插入图片描述


如果说我们想修改这个字符串,需要将其放入数组中,然后再去修改:
在这里插入图片描述
扩展:在C语言中,内存可以被划分为栈区、堆区、静态区、常量区。
在这里插入图片描述

栈区:局部变量,函数形参,函数调用
堆区:动态内存如malloc等申请使用
静态区:全局变量,static修饰的局部变量
常量区:常量字符串
常量区中的内容在整个程序的执行期间是不允许被修改的,且同一份常量字符串只会创建一份,不会重复创建存储。


请看以下代码,猜猜输出结果是什么?

#include<stdio.h>
int main()
{
	char arr1[] = "abcdef";
	char arr2[] = "abcdef";
	char* p1 = "abcdef";
	char* p2 = "abcdef";
	if (arr1 == arr2)
	{
		printf("arr1 == arr2\\n");
	}
	else
	{
		printf("arr1 != arr2\\n");
	}
	if (p1 == p2)
	{
		printf("p1 == p2\\n");
	}
	else
	{
		printf("p1 != p2\\n");
	}
	return 0;
}

在这里插入图片描述

分析:

创建数组需要开辟空间,数组arr1和arr2在内存空间所在位置是不同的,所以arr1 != arr2; char* p1 = “abcdef”; char* p2 = “abcdef”; "abcdef"是常量字符串,不能被修改,在内存空间所占位置固定,char * p1 = “abcdef”; 是将该常量字符串的首地址放到字符指针p1中,char* p2 = “abcdef”;
是将该常量字符串的首地址放到字符指针p2中。也就是说p1和p2存放都是常量字符串"abcdef"的首地址,所以 p1 ==p2。(注意:同样的常量字符串只会存一份,不会同时存两份,所以不会开辟不同的空间来存储。)

在这里插入图片描述

char* p2 = “abcdef”; 这种写法本身也不严谨,严谨的写法应该是:
const char* p2 = “abcdef”; (指向常量字符串的指针最好加上const!)

总结:这里arr1和arr2指向的是一个同一个常量字符串。
C /C++会把常量字符串存储到单独的一个内存区域当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以arr1和arr2不同,p1和p2相同。


2、指针数组

指针数组是一个存放指针的数组

int* arr1[5];
char* arr2[5];
double* arr3[5];

整型数组-- - 存放整型的数组

int arr4[10];

字符数组-- - 存放字符的数组

char arr5[10];

看以下代码,猜猜结果是什么?

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int* arr[3] = { &a, &b, &c };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", *arr[i]);
	}
	return 0;
}

在这里插入图片描述


我们知道数组名可以代表首元素的地址,请看下面这段代码:

#include<stdio.h>
int main()
{
	int arr1[] = { 1,2,3 };//arr1--int*
	int arr2[] = { 4,5,6 };//arr2--int*
	int arr3[] = { 7,8,9 };//arr3--int*
	int* arr[3] = { arr1,arr2,arr3 };
	//通过arr数组打印arr1,arr2,arr3三个数组中的元素
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 3; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\\n");
	}
	return 0;
}

在这里插入图片描述


上面举例都是用整型指针数组,接下来我们来看一个字符指针数组的例子:

#include<stdio.h>
int main()
{
	char* p1 = "student_zhang";
	char* p2 = "guofucheng";
	char* p3 = "liudehua";
	char* ch[3] = { p1,p2,p3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%s\\n", ch[i]);
	}
	return 0;
}

在这里插入图片描述
上面的代码可以进行简化:
在这里插入图片描述
这里我们再复习一下,下面指针数组是什么意思 ?

int* arr1[10];//整型指针的数组
char* arr2[4]; // 一级字符指针的数组
char** arr3[5];//二级字符指针的数组

3、数组指针

1.数组指针的定义

数组指针是指针还是数组 ?
答案是∶指针

我们已经熟悉︰
整形指针 : int* i pint; 能够指向整形数据的指针。
浮点型指针 : float* pf; 能够指向浮点型数据的指针。
那数组指针应该是︰能够指向数组的指针。
数组指针和指针数组要区分开来。

整型指针-- - 指向整型的指针

int a = 10;
int* pa = &a;

字符指针-- - 指向字符的指针

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

数组指针-- - 指向数组的指针

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//int* p = arr;// 数组名是首元素地址
	//数组指针 存放数组的指针变量
	int(*p)[10] = &arr;//(*p)代表p是指针变量
	//该指针指向了一个数组,数组10个元素,每个元素的类型是int
	//如果不用括号将*p括起来,写成int* p[10],这是指针数组
	return 0;
}

为了更好理解数组指针,再看下面几个例子;

//1:
char arr[5];
char(*pa)[5] = &arr;
//2:
char* ch[8];
char* (*pc)[8] = &ch;
//3:
int(*p)[10];
//解释: p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

在这里插入图片描述


2. & 数组名 和 数组名

在这里插入图片描述
我们看到打印的结果都是一样的,那么数组名arr和数组的地址 & arr是一样的吗?
从地址值来看,两者是一样的,但是两者的含义和使用是不同的:
在这里插入图片描述

int* p1; //p1+1 表示跳过一个int类型的长度,也就是4个字节
char* p2;//p2+1表示跳过一个char类型的长度,也就是1个字节
int(*p3)[10];//p3+1表示跳过一个具有10个整型长度的数组,也就是4*10=40个字节

在这里插入图片描述


3.数组指针的使用

我们先看这个例子:
在这里插入图片描述

通过数组指针解引用找到数组,再用方括号[ ],去找到数组中的每个元素。
这种并非数组指针的常用方式,因为用起来很“别扭”。
这种方式不如首元素地址 + i 流畅:

在这里插入图片描述
数组指针的使用,一般常见于二维数组及其以上

当我们在谈首元素的时候,一维数组的首元素就是第一个元素,二维数组的首元素要先将二维数组看作一维数组(该数组中每一个元素都是一个一维数组),那二维数组的首元素就是第一个一维数组。那么二维数组的首元素地址就是第一个一维数组的地址!(不是第一个一维数组中第一个元素的地址,虽然值相同,但含义和使用不同)

#include<stdio.h>
void print_arr1(int arr[3][5], int x, int y)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < x; i++)
	{
		for (j = 0; j < y; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\\n");
	}
}
void print_arr2(int(*p)[5], int x, int y)//数组指针
{
	int i = 0;
	int j = 0;
	for (i = 0; i < x; i++)
	{
		for (j = 0; j < y; 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} };
	//打印这个二维数组
	//print_arr1(arr,3,5);
	print_arr2(arr, 3, 5);
	return 0;
}

在这里插入图片描述

注意:对一个存放数组地址的指针进行解引用操作,找到的是这个数组,也就是这个数组的数组名,数组名这时候又表示数组首元素地址!

* (p + i):相当于拿到了一行
相当于这一行的数组名
(p + i)[j] <===> ((*p + i) + j)


为了更好的理解这一点,我们来看这个例子:

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
		//方式一:通过指针找到与首元素偏移i个元素的地址,
		//再对齐解引用操作,找到这个元素
		printf("%d ", *(arr + i));
		//方式二:既然可以将arr赋值给p,说明arr与p等价
		//那么就可以直接用arr替代p进行相应的解引用操作
		printf("%d ", arr[i]);
		//方式三:通过数组名+[下标]访问数组元素
		//即arr+[下标i]访问下标为i的元素,也就是第i+1个元素
		printf("%d ", p[i]);
		//方式四:既然arr与p等价,
		//那么也可以直接用p+[下标]的方式访问数组的元素

		//上述四种方式实际结果完全相同,实际上也可以互相转换使用
	}
	return 0;
}

在这里插入图片描述

总结:我们对一个数组指针变量进行解引用操作,比如int(*p)[10],得到的是一个数组,或者说是这个数组的数组名,而数组名又可以表示该数组首元素的地址。如果要找到该数组中的每一个元素,就需要对这个数组元素的地址进行解引用操作。
简单点来说就是,对一个数组指针类型进行解引用操作,得到的还是地址,对这个地址在进行相应的解引用操作,才能得到数组中的具体的元素。


练习:

下面这些代码的含义是什么?

int arr[5];
int* parr1[10];
int(*parr2)[10];
int(*parr3[10])[5];

解析:

int arr[5];
//arr是一个数组,数组有5个元素,每个元素类型是int
//arr类型是  int [5]  --- 去掉变量名,剩下的就是变量的类型

int* parr1[10];
//parr1是一个数组,数组有10个元素,每个元素类型是int*
//parr1是指针数组,类型是 int* [10]

int(*parr2)[10];
//parr2是一个指针,指针指向一个数组,数组有10个元素,每个元素的类型是int
//parr2是数组指针,类型是 int(*)[10]

int(*parr3[10])[5];
//parr3是一个数组,数组有10个元素,每个元素都是一个指 针
//指针指向一个数组,数组有5个元素,每个元素类型是int
//parr3是一个指向数组指针的数组,本质上还是数组
//parr3类型是 int(*[10])[5]

int(parr3[10])[5]; 拿掉数组名后,剩下 int()[5]就是这个数组的类型

在这里插入图片描述

问题1:parr2 = &parr1;//能否将数组parr1的地址放到parr2中呢?
答:不能,因为类型不匹配,parr2指向的类型应该是 int[10] parr1是 int* [10];


4、数组参数、指针参数

1.一维数组传参

#include <stdio.h>
void test(int arr[])//ok? 
{}
void test(int arr[10]) //ok?
{}
void test(int* arr)//ok?
{}
void test2(int* arr[20])//ok?
{}
void test2(int** arr) // ok ? 
{}
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
}

答案:以上五种传参方式均ok

注意:一维数组传参可以传数组形式,也可以传指针形式,传数组形式的时候数组元素的个数可以不写,也可以写,传指针的时候要注意指针的类型,也就是指针指向什么类型的元素,比如说指针指向int类型元素,那么指针的类型就是
int* 。


2.二维数组传参

void test(int arr[3][5])//ok ?
{}
//可以
void test(int arr[][])//ok ?
{}
//不可以,行可以省略,列不可以,第一个[ ]内容可以不写,第二个[ ]要写
void test(int arr[][5])//ok ?
{}
//可以
void test(int* arr)// ok ? 
{}
//不可以
void test(int* arr[5])//ok ?
{}
//不可以
void test(int(*arr)[5])//ok ?
{}
//可以
void test(int** arr)// ok ? 
{}
//不可以
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

总结:二维数组传参,函数形参的设计只能省略第一个[ ]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
这样才方便运算。
二维数组传参也能写成指针的形式,指针的类型应该是数组指针。


3.一级指针传参

在这里插入图片描述

思考1:这里的指针传参可以用数组去接收吗?
在这里插入图片描述

经过实验我们可以看到这样做是没问题的,指针传参可以用数组去接收!

思考2:当一个函数的参数部分为一级指针的时候,函数能接收什么参数 ?
例如:int * p

void test1(int* p)
{
}
int main()
{
	int a = 10;
	int* pa = &a;
	int arr[10] = { 0 };
	test1(&a);
	test1(pa);
	test1(arr);
	return 0;
}

再例如:char* p

void test2(char以上是关于C语言进阶学习笔记二指针的进阶(重点必看+代码图解+练习)的主要内容,如果未能解决你的问题,请参考以下文章

C语言进阶学习笔记二指针的进阶(练习篇)

C语言基础学习笔记六初始指针(重点必看)(详细讲解+代码举例+练习巩固)

C语言指针进阶(下)

C语言学习笔记(12)指针进阶

豆瓣9.0,百万程序员的宝藏书,C语言进阶必看

C语言进阶笔记深入了解进阶指针