IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C语言指针的那些事:第二篇(字符指针,数组指针,指针数组,指针传参,数组传参设计思考方式) -> 正文阅读

[C++知识库]C语言指针的那些事:第二篇(字符指针,数组指针,指针数组,指针传参,数组传参设计思考方式)

1 字符指针

字符指针就是指向字符的指针,指针的类型为 char*;

来看看一种用法:指向一个字符

int main()
{
  char ch = 'a';
  char *pc = &ch;
  return 0;
}

分析一下:pc是一个指针,指向类型为char的变量 char;


还有一种用法是用字符指针,指向字符串常量

int main()
{
  char* pstr = "hello world.";//这里是把一个字符串放到pstr指针变量里了吗?
  printf("%s\n", pstr); //打印为字符串内容,注意这里是指针 pstr;
  return 0;
}

上面有个问题,是否将“ hello world.”放入指针变量 pstr中?

答:不是,即使你想放也放不下,pstr是指针,32为机器是四个字节,而 hello world. 这个字符串早就超过四个字节啦。
其实是把 hello world. 的字符串首地址放入到指针变量 pstr中,然后指针变量pstr就可以找到该字符串的首地址,打印时候,就会顺着首地址一直打印,直到结束。


但是上面的指针写法并不完美,我们知道“hello world.”是常量字符串,常量字符串存放在常量区,这个常量字符串是不可以修改的,一旦你通过*pstr去修改字符串的字符时候,编译器就会报错
调试看看如下:
在这里插入图片描述


所以说,我们有一种写法就是

  char* pstr = "hello world.";
  修改为:
 const char* pstr = "hello world.";
 加个const 假如你要修改,就可以在编译阶段报错,不至于运行时候报错了。
  

来看看一道面试题:帮助你理解字符指针

int main()
{
	char str1[] = "hello world.";
	char str2[] = "hello world.";
	char *str3 = "hello world.";
	char *str4 = "hello world.";
	if (str1 == str2)
		printf("str1 and str2 are same\n");
	else
		printf("str1 and str2 are not same\n");

	if (str3 == str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");

	return 0;
}

测试结果:
在这里插入图片描述
又结果很容易分析到:指针 str3 和 str4 指向同一块内存,所以 str3 == str4;才会成立;
而 数组名 str1 和 str2 指向不同的内存,虽然他们的值相同,但是地址空间不同,所以 str1 和 str2不一样。
原因很简单:使用字符指针 会指向字符串所在常量区的地址,而使用数组名,只会将字符串拷贝一份给数组去存储。很明显拷贝时候要开辟栈空间,很明显对数组名来说地址是不一样滴。


在这里插入图片描述


2 指针数组和数组指针

很多人都搞混这两个名词,其实有个记忆方式
指针数组:数组在后面,所以说指针数组是个数组,数组里存放的是指针;
数组指针:指针在后面,所以说数组指针是个指针,指向数组的指针;

1)指针数组

指针数组
int a = 10;
int b = 20;
int c = 30;
int *arr[3] = {&a,&b,&c};

在这里插入图片描述

指针数组就是数组,数组里面的元素是指针,即地址;

那我要访问 arr数组的元素就是 arr[ i ];比如得到arr[0]就是 &a;也就是a 的地址,我想得到
a的值,则需要解引用*arr[i];比如:*arr[0] ,这就是a 的值;
所以,有一种打印方式,得到指针数组里边变量地址的值

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int *arr[3] = { &a, &b, &c };
	for (int i = 0; i < 3; i++)
	{
		printf("%d ", *(arr[i])); //arr[i]表示数组元素,该元素为地址,通过解引用地址得到变量值。
	}	
	return 0;
}

还有另一种用法,look:

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[] = { arr1, arr2, arr3 };
	
	return 0;
}

分析一下:*这个parr;首先parr是一个数组,该数组的每个元素是 int,每个元素是其他数组的首地址;**画出内存布局图看看,大概如下。

在这里插入图片描述
假如我想通过数组指针 parr访问 里面数组的值,该如何访问?
首先,我们直到解引用指针,得到该指针指向的值,所以*parr== arr1; 但是 arr1还是地址,我们再解引用 即 *(*parr) == arr1[0];得到了arr1[0]的元素,那如何获得 arr1[1]的元素呢?我们可以通过* (arr1 +1)得到 arr[1],那么也就是说,还可以通过 *(*parr+ 1)得到 arr1[1];
以此类推就有:

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[] = { arr1, arr2, arr3 };

	for (int i = 0; i < 3; i++)
	{
		
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", *(*(parr + i) + j));
			// *(parr+i)  =parr[i]; 
			//*(parr[i] + j) = parr[i][j];

		}
		printf("\n");
	}
	return 0;
}

这就类似二维数组了。

在这里插入图片描述


2)数组指针

数组指针,就是指针,指向的是数组;
数组指针的初始化:

 int arr[3] = {0};
 int (*p)[3] = &arr; //数组指针

这是怎么定义出来的呢?或者说怎么书写的? 我上面(指针第一篇文章中有概述)是不是说过一种方法呀:
我是这么想的:

  • 首先你要书写数组指针,就要知道*等号左边需要写 p,即*p =的模样;
  • 其次,你数组的类型(去掉数组名剩下的就是数组的类型): int [3];
  • 然后在 *p = ;星号的左边补上 int[3]; 即 int [3] *p =
  • 最后,在等号右边写上 &arr,即int [3] *p = &arr;
  • 最后的最后还没完,需要调整位置 即把int [3] *p = &arr 调整为 int (*p)[3] = &arr;
  • 这就是书写数组指针的过程。

当我们阅读这句代码,即 int (*p)[3] = &arr时候,应该这么阅读
首先 p是一个指针,指针指向数组,数组有3个元素,每个元素的类型为int 类型

在这里插入图片描述
举个例子:请写出指向该数组的数组指针 char* arr[5];
分析:首先数组指针是个指针,所以等号左边=先写 (*p),其次,看数组的类型,是 char* [5],等号=的左边再写上 char*(*p)[5];最后书写格式:char*(*p)[5] = &arr;


3)数组指针的常见用法

  1. 和二维数组一起用;

普通二维数组的遍历

# include<stdio.h>
int main()
{
	int arr[3][4] = { { 1, 2, 3, 4 }, { 2, 3, 4, 5 }, { 3, 4, 5, 6 } };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}

	return 0;
}

结果:
在这里插入图片描述
用数组指针去遍历二维数组

void print(int (*p)[4], int x, int y)
{
	for (int i = 0; i < x; i++)
	{
		for (int j = 0; j < y; j++)
		{
			printf("%d ", *(*(p + i) + j) );
		}//*(*(p + i) + j) == p[i][j];
		printf("\n");
	}
}
# include<stdio.h>
int main()
{
	int arr[3][4] = { { 1, 2, 3, 4 }, { 2, 3, 4, 5 }, { 3, 4, 5, 6 } };
	print(arr, 3, 4);//数组名就是首元素的地址,	
	return 0;
}

在这里插入图片描述
首先我们知道数组名就是首元素的地址,在二维数组中,我们把它看成为一维数组;怎么理解呢?
在这里插入图片描述

那么arr 表示 &arr[0][0],那么arr + 1;表示 arr[1][0]的地址,那么*(arr + 1)表示 arr[1][0]的元素值,那么*(*(arr + 1)+1)表示 arr[1][1]的元素的值
那么数组指针,指向数组的指针,int (*p)[4] = arr; 就可以这么理解:*表示p是个数组指针p指向的数组有4个元素,每个元素的类型为 int;


3 指针和数组结合的复杂类型的阅读例题

int arr[5];
分析:arr是一个数组,数组有5个元素,每个元素的类型为int;
可以这么说:arr是一个包含5int类型的数组。

/

int *parr1[10];
分析:parr1是一个数组,数组有10个元素,每个元素的类型为 int*;
可以这么说:parr是一个包含10int*类型元素的数组。

/

int (*parr2)[10];
分析:parr2是一个指针,指向了一个数组,该数组有10个元素,每个元素的类型为int;
可以这么说: parr2指向了含有10int类型元素的数组,parr2是数组指针。

/

int (*parr3[10])[5];
分析:parr3是一个数组,该数组有10个元素,每个元素的类型为 int(*)[5],即每个元素的类型为数组指针,该数组指针指向了含有5int类型元素的数组。
可以这么说:parr3是一个包含了 10个数组指针类型的数组,该数组指针指向的数组含有5int类型的数组。

4 关于指针传参,数组传参的设计思考方式

许多朋友,从设计者的角度来说不知道怎么设计一个函数,参数类型是 一级指针,还是二级指针,不知道一级指针可以接受什么参数,二级指针可以接受什么参数。
从调用者的角度,当你传参的时候,也不知道传的参数,是否能改变外界实参,也不知道,传地址过去还是传变量过去。

这里我将带你扫一扫这个问题的疑惑,看看能不能帮助你们解决这个问题。


首先我们得有个认知,当我是调用者时候,即我要调用函数时候,传入实参,其实就是给形参初始化,我打个比方,看下面函数。

int Add (int x, int y)
{
	return x+y;
}
int main()
{
	int a = 10;
	int b = 10;
	int ret = 0;
	ret = Add(a,b); //调用函数
	
	return 0;
}
_______________________________________________________
Add(a,b);对于这个函数调用,当你在传实参时候,就相当于给形参初始化,
从形参得角度来看:就是这种效果:
int x = a,int y = b;对于形参来说,等价于这种方式。
_______________________________________________________

为什么说这个呢?因为,这有帮助我们在设计实参,形参时候有理解帮助,还记得我们在平时普通初始化一个变量是怎么初始化得嘛?是不是也是通过 = 等号初始化一个变量。
那这么说, = 等号两边得类型是需要相同的,那也就是说,当我看 = 等号,左边时候,也就是从函数设计者的角度区思考,这个类型必须是需要与 等号 = 右边的类型相同,那么等号 = 右边的类型从哪里来呢?就是从我们的调用者角度去看,就在调用函数里面的实参,可以得到实参类型。

这个思想很重要,你需要有这个思想才可以知道,函数形参,实参到底如何设计合理。


1)从设计的函数角度,思考形参的设计

所以说,当我是一个函数设计人员,当我在设计形参 为 int* x时候,我是在想什么?

我是想,我这个形参到底能够接受什么参数?

1. 接收比形参 x 低一个等级级变量的地址,即 int a,
   a是int类型,比x 的类型 int*低一个等级,接受后相当于:int* x = &a;
   你发现等号 = 左右两边的变量类型是一致的。

2. 接收同等级的变量,即 int* a;
   a 是int* 类型,和 x 的类型 int* 同等级,接受后相当于: int* x = a;
   你发现等号 = 左右两边的变量类型是一致的。
   
3. 接收一位数组名,由于一位数组名本质是首元素地址,对于 int arr[2] = {1,2};
   一位数组名的类型为int*,接收后相当于 int * x = arr; 
   你发现等号=左右两边的变量类型相等。

其次我又想我到底需不需要改变外界传入的实参呢?

假如需要改变外部传入的实参,那么我需要 接收的是比形参 x 的类型低一个等级变量的地址;
如 int a = 10; 传入 &a;
假如我不需要改变外部传入的实参,那么我就接收一个和形参 x 的类型一致的变量;
如: int *p = &a; 传入 p;

这里有个重点:无论你是什么类型的变量,如一级指针变量还是二级指针变量还是多级指针变量,只要你传的不是该指针变量的地址,就无法改变该指针的内容;无论你是什么类型的变量,只要你传入一个地址,前提这个变量是需要比形参低一个等级,那么你就可以修改该变量的内容。


2)从调用者角度思考实参形参设计

自然而然的假如你是调用者,你要是传变量的地址,那么就需要设计函数为比变量高一个等级(就是多一个*星号)的形参类型,你要是传变量,那么设计函数就需要和实参同类型的形参类型;
那你是想不想通过形参修改实参的数据呢?想的话,你就传实参的地址,不想的话,你就传实参的变量。


假如我想设计一个交换数的函数,首先你就得清楚,你需不需要修改实参,需要就传地址,不需要就传变量。
很明显,交换数的函数,需要修改实参。所以传实参时候,你就传地址。
如:
int a = 10;
int b = 20;
swap(&a,&b);
形参设计时候,由于需要修改实参,所以用一个比实参高一级的形参变量去接收实参,
int swap(int*x,int*y);这个时候 形参 x 和 y 是int*类型,都是 比实参 a和b 都是 int 类型,高一个等级类型的。

假如你不需要修改外部实参,那么你就不需要传地址,
就传同等级的变量过去,无论你的实参变量是否为指针,形参接收时候都是相同的类型。


3)一些参数传递的练习

问:对于main函数中的四条语句是否传参成功。

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

答:对于 test(arr); 从调用者的角度思考,这是一个数组名,数组首元素地址&a[0],该类型为 int* ,既然传的是地址,那么说明,调用者希望在函数里面是可以修改实参的。
并且由于 需要实参和形参的类型对应,那么我的设计函数的形参,就必须类型为 int*或者int [ ](对于数组来说,这是一种特别的形参);

所以 前面4个函数都是可以传参成功,唯独第五个函数void test2(int **arr){ },不行,因为 它的形参类型为 int ,从设计函数的角度来说,它希望接收的参数**

  1. 接收比形参 arr 低一个等级级变量的地址,即 int* arr,
  2. 接收一个同等级的实参 即,int** arr;
  3. 接收一个二维数组的数组名,int arr[][3] = {{0},{0},{0};
    至于要不要改变实参,还是那句话,看你传入的是否为 取地址变量,还是同等级的变量。

对于 test2(arr2),从调用者的角度思考,数组名是首元素的地址,该首元素地址的类型 为 int**,所以设计时候,对于上面的函数,只有第五个函数可以接收这个参数啦。


这次分享到这里,下一篇我们继续指针,讲的是函数指针,指针函数,指向函数指针的数组,回调函数等。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-15 15:19:21  更:2021-08-15 15:19:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/26 17:30:58-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计