上一篇整理了一下指针的基础知识
可以点这里进行查看:↓↓↓↓↓↓
「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));
}
上一篇里,总结了指针前面的“*”可以理解为一种“脱衣服”操作,上面三个函数,就是实现对多级指针变量的“脱衣服”操作
直接调用第三个函数,打印出三级指针一层层脱下衣服后最终所指的内容,代码如下:
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]的内存大小。