文章目录
一、参数传递
1.1 值传递
值传递过程中,被调函数的形参作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本。值传递的特点是,被调函数对形参的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值。
指针参数传递本质上是值传递,它所传递的是一个地址值,将实参指针的值传递给了形参指针,所以形参指针和实参指针指向的是同一个对象,但是形参指针和实参指针是两个指针(改变形参的值,实参不会变)。
下面实例向函数传递参数的指针调用方法,把参数的地址复制给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
#include <iostream>
using namespace std;
// 函数定义
void swap(int *x, int *y) {
int temp;
temp = *x; /* 保存地址 x 的值 */
*x = *y; /* 把 y 赋值给 x */
*y = temp; /* 把 x 赋值给 y */
return;
}
int main() {
// 局部变量声明
int a = 100;
int b = 200;
cout << "交换前,a 的值:" << a << endl; // 100
cout << "交换前,b 的值:" << b << endl; // 200
/* 调用函数来交换值
* &a 表示指向 a 的指针,即变量 a 的地址
* &b 表示指向 b 的指针,即变量 b 的地址
*/
swap(&a, &b);
cout << "交换后,a 的值:" << a << endl; // 200
cout << "交换后,b 的值:" << b << endl; // 100
return 0;
}
1.2 引用传递
引用传递过程中,被调函数的形参也作为局部变量在栈中开辟了内存空间,但此时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
使用引用传递可以有效避免函数调用时,因为实参和形参之间拷贝带来的程序低效问题,实例入下:
#include <iostream>
using namespace std;
void swap(int &x, int &y);
int main() {
int a = 100;
int b = 200;
cout << "交换前,a 的值:" << a << endl; // 100
cout << "交换前,b 的值:" << b << endl; // 200
swap(a, b);
cout << "交换后,a 的值:" << a << endl; // 200
cout << "交换后,b 的值:" << b << endl; // 100
return 0;
}
void swap(int &x, int &y) {
int temp;
temp = x; /* 保存地址 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 x 赋值给 y */
return;
}
指针传递与引用传递的区别:
-
引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。
-
从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。
1.3 const形参和实参
作用:在函数的参数中使用const,可以让编译器知道在函数调用过程中,对于某个参数不会修改参数的数据,从而可以提供给编译器更多的优化机会。
比如标准函数
char *strcpy(char *dst, const char *src);
这里,第二个输入参数使用const char *src
,而不是char *src
. 这个表示函数strcpy
不会修改 src
指向的内容。
还有在某些C++类成员函数中,我们会标明一个成员函数是const
,这个表示这个成员函数不会修改这个类对象的任何数据,比如下面例子中,函数display不会修改对象的数据成员x和y,所以我们可以给函数标上const
属性
class complex {
double x, y;
public:
void diplay() const { std::cout << x << " + " << y << std::endl; }
};
对于一个函数,如果其某个指针参数指向的内容不会被修改,就应该加上const
属性。同样,一个类成员函数,如果不会修改类对象中的数据,同样也应该加上const
属性。
bool Bar(const int x[], const int y[], int z[], int n) {
int i;
for (i = 0; i < n; i++) {
if (x[i] < y[i])
z[i] = x[i];
else
z[i] = y[i];
}
return true;
}
给指针x
,y
标上const
属性后(对于函数参数,数组和指针实质上是相同的),那么编译器就可以知道,这个函数中的循环每个不同的迭代(Iteration)中的内容都是不相关的,可以并行执行,所以编译器就可以采用并行指令来处理它们了。
不然, 由于指针z
有可能同x
,y
指向的内存(全部或部分重叠),那么对任何一个z[i]
的修改可能会改变x[.]
或y[.]
中部分的数据,从而循环的不同迭代之间就会有数据依赖关系了;这样,编译器就无法采用并行指令来优化上面的代码了。
此外,对于常数变量,同样也要在能够添加const
时就要添加const
,这些都可以增加编译器对代码进行优化的机会。
还有一点需要注意,当用实参初始化形参是,会忽略掉顶层的const
,所以调用时传常量或非常量对象都是OK的。
int main() {
int a = 100;
const int ca = 200;
printNum(a); // 非常量对象
printNum(ca); // 常量对象
return 0;
}
void printNum(const int i) { // 该const是顶层const
cout << i << endl;
return;
}
1.4 数组形参
因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数,会被替换为指针,所以我们以数组作为参数传递时,实际上传递的都是数组首元素的指针。但是可以用数组的形式来传递:
/*
* 下面数字10没啥用,不能在函数里面用这个长度来遍历数组
* 也不能在函数内部算数组长度,size_t size = sizeof(a) / sizeof(int);
*/
function(int a[10]);
function(int a[]);
function(int *a);
// 两者等价
void function(int arraySize, char **arr);
void function(int arraySize, char *arr[]);
上面三种方式都是将数组的首元素地址作为值拷贝过来。形参是实参的一份拷贝,是局部变量,但是数组是个例外,因为数组的数据太多了,将其一一赋值既麻烦又浪费空间,所以数组作为参数传递给函数的只是数组首元素的地址,数据还是在内存里的,函数在需要用到后面元素时再按照这个地址和数组下标去内存查找。
如何防止函数中访问形参数组越界?
-
使用标记指定数组长度,即数组本身包含一个结束标记,如C风格的字符串数组最后一个字符是
'\0'
-
使用标准库规范,传递指向数组首元素和尾元素的下一个指针
-
显式传递数组长度作为形参
/* *只有确切知道会改变数组元素的内容的时候,才会传递非常量指针,负责最好选用常量指针 */ void fun_test1(const char *cp) { if (cp) while (*cp) cout << *cp++; return; } void fun_test2(const int *beg, const int *end) { if (beg != end) cout << *beg++; return; } void fun_test3(const int ia[], size_t size) { for (auto i = 0; i < size; i++) cout << ia[i]; return; }
数组引用形参和多维数组形参
形参可以用数组引用,将其绑定到实参数组上面,下面int (&arr)[10]
中的()
不能少,即这样是错的int &arr[10]
,使用形参数组引用就可以采用for ... auto
的形式遍历数组。
void fun_test4(int (&arr)[10]) {
for (auto elem : arr)
cout << elem;
return;
}
最后一点,多维数组传递。多维数组传递的指针也是指向数组首元素的指针,只不过这个指针是数组类型,而数组的长度也是数组类型的一部分,所以多维数组传递只需要传递第一维的长度,第二维以及后面所有的维度,都可以在函数内部获得。
void fun_test5(int (*arr)[5], int size) {
for (auto i = 0; i < size; i++)
for (auto elem : arr[i])
cout << elem;
}
int main(void) {
int a[2][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
fun_test5(a, 2);
}
二、类型和return语句
2.1 不要返回局部对象的引用或指针
函数完成后,他所占用的存储空间也会随之释放。下面用例返回了局部对象的引用,局部对象的指针也不能返回。
const string &mianip() {
string ret("OK");
if (!ret.empty())
return ret;
return "Empty";
}
2.2 引用返回左值
调用一个返回引用的函数返回左值,其他返回类型返回右值。
char &get_val(string &str, string::size_type ix) {
return str[ix];
}
int main() {
string s("a value");
cout << s << endl; // a value
get_val(s, 0) = 'A';
cout << s << endl; // A value
return 0;
}
2.3 返回数组指针
由于数组不能拷贝,所以函数不能返回数组,但是可以返回数组的引用或指针。
// 声明一个返回数组指针的函数
int (*func1(int i))[10];
// 使用类型别名简化
typedef int arrT[10];
using arrT = int[10]; // 同typedef定义的留一个就行
arrT *func2(int i);
// 使用未置返回类型
auto func3(int i) -> int (*)[10];
三、函数指针和指针函数
3.1 使用函数指针
函数指针指向的是函数而非对象,函数指针funPt1
必须同指向的函数ret_max
类型完全匹配。函数名其实也就是函数的指针,注意funPt3
的用法。
int ret_max(int x, int y) {
int ret = x > y ? x : y;
cout << ret << endl;
return ret;
}
int main() {
// 函数指针的定义,funPt1与funPt2等价
int (*funPt1)(int, int) = nullptr;
using funType = int (*)(int, int);
funType funPt2 = 0;
// 两者赋值方式等价
funPt1 = ret_max;
funPt2 = &ret_max;
// 三者调用方式等价
funPt1(10, 100);
(*funPt2)(10, 100);
ret_max(10, 100);
// funPt_i64是函数ret_max的地址值
int64_t funPt_i64 = reinterpret_cast<int64_t>(ret_max);
funType funPt3 = reinterpret_cast<funType>(funPt_i64);
funPt3(10, 100);
}
上面代码的输出:
3.2 函数指针作为形参
同数组一样,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,此时形参看上去是个函数类型,实际上是当做指针来使用:
//第二个形参为函数类型,会自动转换为指向此类函数的指针
void fuc(int nValue, int pf(int, int));
//等价的声明,显示的将形参定义为指向函数的指针
void fuc(int nValue, int (*pf)(int, int));
// 使用类型别名
using PF = int (*)(int, int);
void fuc(int nValue, PF);
3.3 返回指向函数的指针
同数组一样,虽然不能返回一个函数,但是能返回指向函数类型的指针,必须将返回类型写成指针类型,编译器不会自动将其转成指针类型。
直接定义函数指针作为返回参数,这样看上去会很麻烦。
int (*fuc2(int))(int,int);//显示定义
最好还是使用typedef
或using
定义的函数指针类型作为返回参数
using PF = int (*)(int, int);
PF fuc2(int);//PF为函数指针类型