C++中的函数返回值问题
首先,强调一点,和函数传参一样,函数返回时也会做一个拷贝。
从某种角度上看,和传参一样,也分为三种:
- 返回值:返回任意类型的数据类型,会将返回数据做一个拷贝(副本)赋值给变量;由于需要拷贝,所以对于复杂对象这种方式效率比较低(调用对象的拷贝构造函数、析构函数);例如:int test(){}或者 Point test(){}
- 返回指针:返回一个指针,也叫指针类型的函数,在返回时只拷贝地址,对于对象不会调用拷贝构造函数和析构函数;例如:int *test(){} 或者 Point *test(){}
- 返回引用:返回一个引用,也叫引用类型的函数,在返回时只拷贝地址,对于对象不会调用拷贝构造函数和析构函数;例如:int &test(){}或者 Point &test(){}
一般来说,在函数内对于存在栈上的局部变量的作用域只在函数内部,在函数返回后,局部变量的内存会自动释放。因此,如果函数返回的是局部变量的值,不涉及地址,程序不会出错;但是如果返回的是局部变量的地址(指针)的话,就会造成野指针,程序运行会出错。因为函数只是把指针复制后返回了,但是指针指向的内容已经被释放,这样指针指向的内容就是不可预料,调用就会出错。
1、返回值
int test1() { int a = 1; return a; }
返回值是最简单有效的方式,他的操作主要在栈上,根据函数栈的特性局部变量a会在函数结束时被删除,为了返回a的值,需要产生a的复制。
如果a原子类型这当然也无所谓,但是如果a是大的对像,那么对a的复制将会产生比较严重的资源和性能消耗。
注:函数返回值本身因为没有名称或引用,所以是右值,是不能直接操作的。
2、指针类型的函数——返回指针
若函数的返回值是指针,该函数就是指针类型的函数。(即函数return一个指针,该指针可以是任何类型的)
1)指针类型的函数定义:
<类型> *函数名(参数)
int *test1() { int *b = new int(); *b = 3; return b; }
根据函数栈的特性也会产生复制,但是这个复制只是4(或8)字节,对于返回大型对像或数组来说可减少资源。但是返回指针资源的清理工作交给了调用者,这某种意义上违反了谁申请谁销毁的原则。
注:函数返回指针也是右值,同样无法操作。
2)说明:
- 不要将非静态局部地址用作函数返回值:因为局部地址在离开函数后就失效了。
- 可以在函数中用动态内存分配(new)的地址返回,但需要注意内存分配和释放不在同一级别,不要忘记释放,否则内存泄露;
- 可以在主调函数中定义数组,函数中对该数组进行操作,然后返回其中一个元素的地址;
3、返回引用
1)引用类型函数的定义:
<类型> &函数名(参数)
int &test3() { int *c = new int(); *c = 5; return *c; }
引用是C++中新添加的概念,所以返回引用也是C++中相对于C来说所没有的。引用是值的别名,和指针一样不存在对大对像本身的复制,只是引用别名的复制。引用是左值,返回引用可以直接操作,也就可以进行连续赋值,最经典的示例是拷贝构造函数和运算符重载一般返回引用。
test3() +=3;
2)说明:
- 和返回指针一样,不要将非静态局部变量的引用用作函数返回值:因为局部地址在离开函数后就失效了。
- 和返回指针一样,用动态内存分配(new)的局部指针可以作为引用返回,但是和返回指针一样需要调用者自己去清理内存,否则内存泄露;
总结:
在C时代函数只能返回值、指针两种,这两种返回的都是右值;前者对于返回对象时要进行拷贝,效率比较低(会执行对象的拷贝构造函数、析构函数),后者不会发生;
C++时代除了上面两种外,多了返回引用,这种返回时一种左值,特性和返回指针一样;
4、综合示例
1)返回栈内局部变量:
#include <iostream> using namespace std; int fun1() { int i = 1; cout<<"fun1 i address"<<&i<<endl; return i;//ok,返回值是i值得拷贝 } int *fun2() {//指针类型的函数 int i = 2; int *ip = &i; cout<<"fun2 i address"<<ip<<endl; return ip; // Wrong!返回值是ip指针的拷贝,但该地址在函数结束后会释放变得无效 } int main() { int r1 = fun1(); cout<<"main fun1 return i address"<<&r1<<endl; cout << r1 << endl; // 1 int *r2 = fun2(); cout<<"main fun2 return i address"<<r2<<endl; //这里有可能出错:具体看对应的内存是否被覆盖,但总之该内存已无效 cout << *r2 << endl;//0 return 0; }
输出:
fun1 i address0x7ffc49e9b69c
main fun1 return i address0x7ffc49e9b6b4
1
fun2 i address0x7ffc49e9b694
main fun2 return i address0x7ffc49e9b694
0
我们在看一个对象的例子:
#include <iostream> using namespace std; class Point { public: Point(int a,int b):x(a),y(b){} int getX(); void setX(int x); private: int x,y; }; int Point::getX(){ return x; } void Point::setX(int a) { x = a; } Point func(int x) { Point p(x,100); cout<<"func1 p address:"<<&p<<endl; return p;//ok,发生一次Point拷贝 } Point *func2(int x) {//指针函数 Point p(x,200); cout<<"func2 p address:"<<&p<<endl; return &p;//wrong,返回值是p地址的拷贝,但该地址在函数结束后会被释放变得无效 } main() { Point p = func(1); cout<<"main return p address:"<<&p<<endl; cout<<"main return p x:"<<p.getX()<<endl; Point *p2 = func2(2); cout<<"main return p address:"<<p2<<endl; cout<<"main return p x:"<<p2->getX()<<endl; }
编译的时候会有一个警告:
test88.cpp: In function ‘Point* func2(int)’:
test88.cpp:26:9: warning: address of local variable ‘p’ returned [-Wreturn-local-addr]
Point p(x,200);
^
输出:
func1 p address:0x7fff0f005270
main return p address:0x7fff0f005290
main return p x:1
func2 p address:0x7fff0f005270
main return p address:0x7fff0f005270
main return p x:6299776
结论:对于栈内局部变量,采用一般的返回值,实际上是对返回值的一次值拷贝,在内存里会有两个示例;对于指针类型函数的返回值,实际上是对地址的一次拷贝,内存只有一个示例,但该地址是一个非法的地址,在使用时会出现问题。
2)返回字符串:
通过 char* s = “Hello”; 的方式得到的是一个字符串常量 Hello,存放在只读数据段(.rodata section),把该字符串常量的只读数据段的首地址赋值给了指针 s,所以函数返回时,该字符串常量所在的内存不会被回收,所以能正确地通过指针访问。
#include <iostream> using namespace std; char *fun1() { char *s="hello"; return s;//ok } int main() { char *c1 = fun1(); cout<<c1<<endl; //常量,无法在修改 return 0; }
3)静态变量:
可以把局部变量声明为static静态变量。这样变量存储在静态存储区,程序运行过程中一直存在。
int *fun3(){ static int i = 5; cout<<"fun3 i address:"<<&i<<endl; return &i; } int main() { int *r1 = fun3(); cout<<"main return i address:"<<r1<<endl; cout<<*r1<<endl; }
输出:
fun3 i address:0x602078
main return i address:0x602078
5
4)数组:
数组是不能作为函数的返回值的。因为编译器会把数组名认为是局部变量(数组)的地址。返回一个数组,实际上是返回指向这个数组首地址的指针。函数结束后,数组作为局部变量被释放,这个指针则变成了野指针。但是声明数组是静态的,然后返回是可以的。
int *fun4() { static int a[2]={4,5}; cout<<"fun4 a[] address:"<<&a<<endl; return a; } int main() { int *r2 = fun4(); cout<<"main return a[] address:"<<r2<<endl; cout<<*r2<<endl; }
输出:
fun4 a[] address:0x60207c
main return a[] address:0x60207c
4
5)堆内变量:
函数返回指向存储在堆上的变量的指针是可以的。但是,程序员要自己负责在函数外释放(free/delete)分配。
int *fun5() { int *j = new int; *j = 99; cout<<"fun5 j address:"<<j<<endl; return j; } int main() { int *r3 = fun5(); cout<<"main return j address:"<<r3<<endl; cout<<*r3<<endl; *r3 = 100; cout<<*r3<<endl; delete r3; }
输出:
fun5 j address:0x208b010
main return j address:0x208b010
99
100
综上,C++的函数返回和函数传参有所不同,返回值和传参一样也有三种类型:
- 使用一般(传统)的函数返回,对于复杂对象会涉及到拷贝效率问题;
- 使用指针类型的函数会有很多限制和弊端(容易内存泄露);
- 引用类型的函数又是一个鸡肋;
所以一般C++函数都是用传址的方式进行双向数据绑定,而返回值仅仅是一个成功或失败的标志。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
相关文章
JetBrains CLion永久激活超详细教程(最新激活方法)
JetBrains Clion 是一款专为 C/C++ 开发所设计的跨平台 IDE,本文适用 JetBrains CLion v2019.3/3.1/3.2/3.3 永久激活,附破解补丁和激活码,可以永久激活 Windows、MAC、Linux 下的 CLion,下面给大家分享JetBrains CLion永久激活超详细教程,感兴趣的朋友一起看看吧2023-01-01
最新评论