「C++学习笔记」指针篇2:指针及数组作为函数参数

上一篇整理了一下指针的基础知识

可以点这里进行查看:↓↓↓↓↓↓

「C++学习笔记」指针的理解


下面整理一下 指针如何作为函数参数吧

一、传入参数

废话不多说,先看代码:

//传入字符串,将传入的字符串打印
void printstr(const char* str)
{
	cout << str << endl;
}

int main()
{
	const char* str = "hello world!";
	printstr(str);
	cin.get();
	return 0;
}

上面实现了一个非常简单的功能:将字符串打印。

虽然简单,但也清楚表现了指针作为变量传入的方式:

在声明&定义函数时,将参数定义为指针形式,在调用该函数时,传入对应的指针变量即可

下面写一个稍微复杂一点的:

//传入三级指针,返回它所指向的二级指针
int **pppint(int ***pIn)
{
	return *pIn;//三层 减 一层 = 两层
}
//传入二级指针,返回它所指向的一级指针
int *ppint(int **pIn)
{
	return *pIn;//两层 减 一层 = 一层
}
//传入三级指针,返回最终所指的对象
int pint(int ***pIn)
{	
	return *ppint(pppint(pIn));
}

上一篇里,总结了指针前面的“*”可以理解为一种“脱衣服”操作,上面三个函数,就是实现对多级指针变量的“脱衣服”操作

直接调用第三个函数,打印出三级指针一层层脱下衣服后最终所指的内容,代码如下:

扫描二维码关注公众号,回复: 10111465 查看本文章
void main()
{
	int ***ppp = new int**;
	int **pp = new int*;
	int *p = new int;
	*p = 300;
	*pp = p;
	*ppp = pp;

	cout << "pint(ppp)=" << pint(ppp) << endl;
	
	if (ppp != NULL){ delete(ppp); }//可以颠倒顺序的删除
	if (pp != NULL) { delete(pp); }
	if (p != NULL) { delete(p); }
	cin.get();
}

定义三个指针,并按等级建立起连接,将三级指针 ppp 直接作为参数传入到pint()函数中

运行结果是:

pint(ppp)=300

以上,是一个多级指针作为函数参数,调用该函数的一个示例。如果你有看我的上一篇博客,应该很好理解的

这里还有一点,就是这里定义的函数,不仅在参数中用到了指针,返回值也出现了指针

也就是说:

指针还可作为函数返回值,返回值为指针的函数在声明时,其返回值类型应按照指针声明的方式写

-----------------------------------------------------------------

我们知道,在调用普通参数的函数时,所谓的变量传入,实际上是被调用的的函数将所传入的参数进行了拷贝

例如:

void big(int a)
{
	a = a + 5;
}

调用该函数

void main()
{
	int a = 5;
	big(a);
	cout << "a=" << a << endl;
}

其打印结果依然时  a=5。

那么当指针作为函数参数时,又会是什么情况呢

编写如下代码:

void big(int *p)
{
	*p = *p + 5;
}

void main()
{
	int *a = new int;
	*a = 5;
	big(a);
	cout << "*a=" << *a << endl;
}

以上代码会打印出什么内容呢?


二、传出参数

上面的代码,运行后输出的结果如下:

*a=10

显然,big()函数确实对原有数据进行了加5操作。

在调用指针参数的函数时,函数形参将传入的指针实参中存储的地址进行了复制,在函数内对指针所指向的内存进行直接操作,自然会修改原 实参指针 所指向的内存区域,因为这两个指针指向的内存区域一样

将上面示例中,指针a和p的地址以及他们所存储的地址分别打印出来

void big(int *p)
{
	cout << "p =" << p << endl;//p中存储的地址
	cout << "p add=" << &p << endl;//p自身的地址
	*p = *p + 5;
}

void main()
{
	int *a = new int;
	*a = 5;
	cout << "a =" << a << endl;//a中存储的地址
	cout << "a add=" << &a << endl;//a自身的地址
	big(a);
	cout << "*a=" << *a << endl;
}

输出结果如下:

a =    0000021D48D95CE0
a add= 000000E90ADCF7D8
p =    0000021D48D95CE0
p add= 000000E90ADCF7B0
*a=10

可以从结果看出,其符合上面的总结:a和p是两个不同的指针,但它们存储着相同的地址。

至此,也就说明了指针用于传出参数时的原理了:

函数并不是真的传出了某个参数,而是在函数内,以传入指针中的地址,直接对该地址对应的内存数据做处理使得函数实现对实参的修改。

其实任何函数参数中,只要有输出参数,其原理都与此相似,因为你总是要先声明用于接纳这些输出参数的变量,才能调用该函数。

基于以上,C++中定义函数时的输出参数经常使用 取地址符“&”,其原因也就很好理解了。

三、数组做参数

前面有提到,函数形参是将实参进行了复制,并不能直接对实参进行操作,这里做一个这样的验证

void change(int a[2][2])
{
	cout << "a=" << a << endl;
	a[0][0] = 9;
	a[0][1] = 9;
	a[1][0] = 9;
	a[1][1] = 9;
}

int main()
{
	int x[2][2] = { {0,0}, {0,0} };	
	cout << "x=" << x << endl;
	change(x);
	for (int i = 0; i < 2; i++)
	{
		for (int j = 0; j < 2; j++)
		{
			cout << x[i][j] << ",";
		}
		cout << endl;
	}
	cin.get();
	return 0;
}

输出结果如下:

x=00000036B16FFC18
a=00000036B16FFC18
9,9,
9,9,

可见,change()函数确实对main()函数中的x数组进行了修改

结合上一篇中所总结的,“[]”在一定程度是和“*”有相同作用的,可总结为如下:

在数组做函数参数时,并不是对原有数组做复制,而是对原有数组的指针进行复制,这样在函数内,按传入的地址进行数据处理,是会直接改变原有数组数据的。这一点与普通参数不同也很好理解,因为数组占用内存大,如果每传入函数一次都完整复制一次,是对内存的极大浪费。

补充:函数参数中其他数组做参数传出/传入的形式

比如将矩阵x[2][3]转置为矩阵y[3][2];

最简单粗暴的方法就是声明y[3][2]并赋初始值,按照上面的方法直接传入

除此之外,列举其他几种方法:

第一种方法:

//参数二是一个指向3x2维数组的指针
void trans(int a[2][3], int (*b)[3][2])
{
	for (int i = 0; i < 2; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			(*b)[j][i] = a[i][j];		
		}	
	}
}

int main()
{
	int x[2][3] = { {1,2,3}, {4,5,6} };
	int y[3][2] = { {0,0},{0,0},{0,0} };
    //这里传入矩阵y的地址
	trans(x, &y);
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 2; j++)
		{
			cout << y[i][j]<<",";
		}
		cout << endl;
	}
	cin.get();
	return 0;
}

输出结果如下:

1,4,
2,5,
3,6,

结果是正确的。这里trans()函数,第二个参数是一个指向3x2维数组的指针,调用时,传入y自身的地址,那么传入后b中存储的就是y的地址,*b就是数组y。

方法二:

void trans(int a[2][3], int b[][2])
{
	...
}

int main()
{
	int x[2][3] = { {1,2,3}, {4,5,6} };
	int y[][2] = { {},{},{} };	

	trans(x, y);
	...
}

经过验证,该方法也成立

方法三:

//形参2 是一个二级指针
void trans(int a[2][3], int**b)
{
	/*...*/
}

int main()
{
	int x[2][3] = { {1,2,3}, {4,5,6} };
	//首先 y指向存有三个指针的一维指针数组
	int **y = new int*[3];
	//三个一级指针分别指向三个不同的二位数组
	for (int m = 0; m < 3; m++)
	{
		y[m] = new int[2];
	}

	trans(x, y);
	/*...*/
	for (int m = 0; m < 3; m++)
	{
		delete(y[m]);
	}
	delete(y);
}

这是用二级指针作为形参的,验证也是可行的

当然,形式不止这三种,各位读者可以尽自己所想去写其他形式。

下面看一些错误形式:

错误一:

void func(int **a)
{
	a[1][1] = 2;
}
void main()
{
	int **p = new int*[3];
	func(p);
}

错误是因为p是一个二级指针,它指向存有三个一级指针的一维数组,但一级指针没有明确的指向,所以编译会通过,运行就会抛异常,指针越界

错误二:

void func(int **a)
{
	a[0][1] = 2;
}
void main()
{
	int **p = new int*[3];
	int *r = new int;
	*p = r;
	func(p);
}

这个错误就很明显,一方面一级指针r只指向单一数据,虽然概率性可以运行成功,但它已经相当于在主函数中使用*(r+1)=2,这样显然是不对的;另外函数只对p[0]进行了赋值r,若func()中使用a[1][x]、a[2][x],就会直接抛异常

基于以上,总结一下

1、数组在作为函数参数时,都只是数组的地址被形参拷贝,在函数体内按照传入的地址对原始数组进行直接操作

2、作为函数形参的数组、实际上就是指针,但该指针必须指向相应大小的数组存储空间

关于第二点,解释 一下,就是当你的函数参数中,要使用数组,无论你是下面哪种形式:

int **p;
int (*p)[3][4]
int p[][4]
... ...

都要确保在调用时,对应的实参指针,即所传入的地址,所指向的内存范围,决不能小于函数内所要操作的内存范围,否则运行时就会溢出。

指针在没有被  new T[x] 声明指向区域大小的时候,都只默认指向T[1]的内存大小。

发布了21 篇原创文章 · 获赞 5 · 访问量 3677

猜你喜欢

转载自blog.csdn.net/Raink_LH/article/details/88919401