函数的值的传递、返回值、const问题

2015/07/28 C和C++基础

C++函数参数和返回值三种传递方式:值传递、指针传递和引用传递。

函数的值的传递

函数调用的机制:

函数的调用过程实际是对栈空间的操作过程(先进后出)。因为调用函数是用栈空间来保存信息的。函数调用过程大致描述如下:

1)建立被调用函数的栈空间;

2)保存调用函数的运行状态和返回地址;

3)传递函数参给形参;

4)执行被调用函数的函数体内语句;

5)将控制权或返回值交给调用函数。

1、值传递

#include <iostream>
using namespace std;
void(int a,int b);

void main(){
	int x=5,y=10;
	cout<<"x="<<x<<"y="<<y<<endl;
	swap(x,y);
	cout<<"x="<<x<<"y="<<y<<endl;
}

void swap(int a,int b){
	int t;
	t=a;
	a=b;
	b=t;
}

swap()和main()的栈区是相互独立的。在main()中不能访问swap()中的局部变量a,b,和t在被调用函数中,改变了形参的值,而不能改变调用函数实参值。这就是C++函数的参数传值调用的特性。

2、函数的传址调用

传址调用:是将实参的地址值传递给形参,这时形参应是指针,即让形参指针指向实参地址,这里不再是将实参拷贝一个副本给形参,而是让形参直接指向实参。于是,这就提供了一种可以改变实参变量值的方法:在被调用函数中改变形参所指向的变量值。这便是传址调用的特点,它与传值调用的特点。

#include <iostream>
using namespace std;
void(int *a,int *b);
void main(){
	int x=5,y=10;
	cout<<"x="<<x<<"y="<<y<<endl;
	swap(&x,&y);
	cout<<"x="<<x<<"y="<<y<<endl;
}

void swap(int *a,int *b){
	int t;
	t=*a;
	*a=*b;
	*b=t;
}

3、引用传递

使用引用作函数参数时,要求实参变量名,将实参变量名赋给形参引用,即形参实际上成了实参的别名。引用起到了传了传址调用的作用,即不仅可以不传递实参的副本,还可以在被调用函数中参通过形参改变实参值。在引用调用中,实参直接用变量名,形参用引用名,免去使用指针带来的麻烦。因此,使用引用调用比传址调 用更为简单,并且可读性好。所以,在C++语言中,较多地使用引用调用来替代传址调用,以简捷的方式达到了传址调用的效果。

#include <iostream>
using namespace std;
void(int &a,int &b);
void main(){
	int x=5,y=10;
	cout<<"x="<<x<<"y="<<y<<endl;
	swap(x,y);
	cout<<"x="<<x<<"y="<<y<<endl;
}

void swap(int &a,int &b){
	int t;
	t=a;
	a=b;
	b=t;
}

4、函数参数的压栈顺序

函数参数顺序一般是按照从右到左的顺序进行压栈,可能不同的编译器有所不同。

下面程序在VS2010环境中编译:

#include <iostream>
using namespace std;

void func1(int a,int b,int c){
	cout<<a<<" "<<b<<" "<<c<<endl;
}

int main(){
	int i=0;
	func1(i++,i++,i++);     //从右到左进行计算后入栈
	int j=0;
	func1(++j,++j,++j);     //前置++先都计算完成 再入栈
	int x=0;
	func1(x,x++,x);
	int y=0;
	func1(y++,y,++y);       //从右到左进行计算 y++是计算完成以后立马入栈  ++y或y等等所有计算完成再统一入栈
	int z=0;
	func1(z,++z,z++);
	system("pause");
}

函数返回值

1、值返回

在带有返回值的函数中,需要使用return语句返回一个表达式的值,如:return 表达式;一般函数返回值时都要建立临时变量,即用来拷贝副本。

(1)函数的返回值用于初始化在调用函数时创建的临时对象(temporary object),如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象。

(2)在求解表达式的时候,如果需要一个地方存储其运算结果,编译器会创建一个没命名的对象,这就是临时对象。C++程序员通常用temporary这个术语来代替temporary object。

(3)用函数返回值初始化临时对象与用实参初始化形参的方法是一样的。

(4)当函数返回非引用类型时,其返回值既可以是局部对象,也可以是求解表达式的结果。

调用完成以后,局部变量和临时变量都会被销毁。

2、引用返回类型

引用返回值时,不产生值的副本,而是将其返回值直接传递给接收函数返回值的变量或对象。

当函数返回引用类型时,没有复制返回值创建临时变量,相反,返回的是对象本身。

千万不要返回局部对象的引用!千万不要返回指向局部对象的指针!

当函数执行完毕时,将释放分配给局部对象的存储空间。此时对局部对象的引用就会指向不确定的内存!返回指向局部对象的指针也是一样的,当函数结束时,局部对象被释放,返回的指针就变成了不再存在的对象的悬垂指针。

返回引用时,要求在函数的参数中,包含有以引用方式或指针方式存在的、需要被返回的参数。

如:

int& abc(int a, int b, int c, int& result){
     result = a + b + c;
     return result;
}

下面通过程序来说明这个问题:

1
#include <iostream>
using namespace std;

int func(int a, int b, int c){
	int d=a+b+c;
	cout<<d<<"的地址为:"<<&d<<endl;
	return d;
}

int main(){
	int d=func(1,2,3);
	cout<<d<<"的地址为:"<<&d<<endl;
	system("pause");
}

程序解析:

可以看出func函数中产生局部变量d,在返回的过程中产生临时变量tmp,把变量d的值拷贝到tmp,在main函数中创建变量d,把临时变量赋值给main中的变量d。调用完成以后把局部变量d和临时变量tmp销毁。

2
#include <iostream>
using namespace std;

int& func(int a, int b, int c){
	int d=a+b+c;
	cout<<d<<"的地址为:"<<&d<<endl;
	return d;
}

int main(){
	int d=func(1,2,3);
	cout<<d<<"的地址为:"<<&d<<endl;
	system("pause");
}

程序解析:

可以看出func函数中产生局部变量d,在返回的过程中不产生产生临时变量,而直接把变量d的值本身返回,在main函数中创建变量d,把局部变量d赋值给main中的变量d。调用完成以后局部变量d销毁。

3
#include <iostream>
using namespace std;

int& func(int a, int b, int c){
	int d=a+b+c;
	cout<<d<<"的地址为:"<<&d<<endl;
	return d;
}

int main(){
	int &d=func(1,2,3);
	cout<<d<<"的地址为:"<<&d<<endl;
	system("pause");
}

程序解析:

可以看出func函数中产生局部变量d,在返回的过程中不产生产生临时变量,而直接把变量d的值本身返回,在main函数中创建变量引用d,引用变量d直接指向局部变量d。因此打印的地址是完全相同的。理论上这样写是不可取的,因为局部变量在函数调用完成就已经销毁了,但是在VS2010环境下任然可以使用,VS2010编译器应该做过某些优化吧。

4
#include <iostream>
using namespace std;

int func(int a, int b, int c){
	int d=a+b+c;
	cout<<d<<"的地址为:"<<&d<<endl;
	return d;
}

int main(){
	int &d=func(1,2,3);
	cout<<d<<"的地址为:"<<&d<<endl;
	system("pause");
}

程序解析:

程序无法编译通过,func函数中产生局部变量d,在返回的过程中产生临时变量tmp,把变量d的值拷贝到tmp,在main函数中创建变量引用d,引用变量d直接指向临时变量tmp。而临时变量在函数调用完成就会销毁,因此在main中用引用变量指向临时变量tmp是不可取的。

#include <iostream>
using namespace std;
int& func(int p){
	p=p+2;
	cout<<&p<<endl;
	return p;           //返回参数引用 正确
}
int main(){
	int a=0;
	cout<<&func(1)<<endl;
	system("pause");
}

因此返回引用时,要求在函数的参数中,包含有以引用方式需要被返回的参数。

比如:

#include <iostream>
using namespace std;

int*& func(){
	int *d=new int;
	*d=3;
	cout<<d<<endl;
	return d;                //返回局部指针变量 不可取
}

int main(){
	int* &d=func();
	cout<<*d<<" "<<d<<endl;
	*d=5;                    //运行报错  因为指针变量d在func中是一个局部变量 调用完成会销毁
	cout<<*d<<endl;
	system("pause");
}
#include <iostream>
using namespace std;

int*& func(int* &d){         
	d=new int;
	*d=3;
	cout<<d<<endl;
	return d;                //返回参数引用 正确
}

int main(){
	int *a;
	int* &d=func(a);
	cout<<*d<<" "<<d<<" "<<a<<endl;
	*d=5;                             
	cout<<*d<<endl;
	system("pause");
}

什么时候用引用返回:

当返回结果需要做为左值时,就要用引用返回。即重载函数的返回结果需要出现在=左边时,必须用引用返回。如果不用引用返回,那么重载函数的返回结果会是一个临时变量,临时变量是不能放在=左边的。

比如:

#include <iostream>
using namespace std;

int func(int p){
	p=p+2;
	return p;           //返回参数引用 正确
}

int main(){
	int a=0;
	func(1)=10;        //临时对象作为左值 会报错
	system("pause");
}
#include <iostream>
using namespace std;

int& func(int p){
	p=p+2;
	return p;           //返回参数引用 正确
}

int main(){
	int a=0;
	func(1)=10;         //加上引用作为左值 编译通过
	system("pause");
}
#include <iostream>
using namespace std;

class A{
public:
	int a;
	A(int n):a(n){}
};

A func(){
	return A(2);
}

int main(){
	A b(3);
	func()=b;         //如果是对象  也可以编译通过  在main中创建一个对象 但是没有名字
	system("pause");  //b赋值给这个没有名字的对象
}
#include <iostream>
using namespace std;

int& func(int &p){
	p=p+2;
	return p;           //返回参数引用 正确
}

int main(){
	int a=0;
	func(a)+=3;         //相当于a的引用作为左值
	cout<<a<<endl;      //打印5
	system("pause");
}

如果是返回类对象:

解释:首先Play(5)隐式调用有参构造函数生成函数参数形参b对象,然后在执行return b语句过程中调用拷贝构造函数生成一个临时对象tmp,最后退出Play函数,销毁临时对象tmp以及b,先析构tmp,再析构b

解释:首先Play(5)隐式调用有参构造函数生成函数参数形参b对象。 在执行return b语句过程中调用拷贝构造函数生成一个临时对象tmp,理论上会再调用一次拷贝构造函数用临时对象初始化t1对象,但是实际上并非如此,编译器会做一个优化,离开Play函数时不撤销临时对象tmp,而是让t1对象和tmp临时对象关联起来,t1和tmp应该是一致的。因此在离开Play函数时会析构b对象,在离开excute函数时再析构t1对象。

解释:首先Play(5)隐式调用有参构造函数生成函数参数形参b对象。 由于函数Play返回值类型时引用,因此在return的过程不再调用拷贝构造函数生成临时变量,而直接返回b对象的本身,在离开Play函数时析构b对象。

解析:首先Play(5)隐式调用有参构造函数生成函数参数形参b对象。 由于函数Play返回值类型时引用,因此在return的过程不再调用拷贝构造函数。 生成临时变量,而直接返回b对象的本身,在离开Play函数时析构b对象。然后使用b对象通过拷贝构造函数初始化t2对象,在离开excute函数时析构t2对象。

例题:下面是一个段有错误的代码,找出其中的错误。

错误代码
#include <iostream>
using namespace std;
int val() 
{ 

     int i = 1; 
     return i; 
} 
int & ref() 
{ 
     int &i = j; 
     return i;
} 
  
int main() 
{ 

     int   vv = val(); 
     int & rv = val(); 
     int   vr = ref(); 
     int & rr = ref(); 
     return 0;
}
正确代码
#include <iostream>
using namespace std;
int j=3;            //j是全局变量
int val() 
{ 

     int i = 1; 
     return i; 
} 
int & ref() 
{ 
//   int j=3;j不能是局部变量!
    int &i = j; 
      return i; //不能返回局部对象的引用
} 
  
int main() 
{ 
      int   vv = val(); 
      int   rv = val();//int   &rv = val()错误!val()返回的是一个int型的数,而给引用&rv 赋值的必须是一个同类型的变量。
      int   vr = ref(); 
      int & rr = ref(); 
      cout<<vv<<endl;
      cout<<rv<<endl;
      cout<<vr<<endl;
      cout<<rr<<endl;
      return 0;
}

const返回值

1、普通函数返回值加const类型

#include <iostream>
using namespace std;

const int func(){
	int p=3;
	return p;
}

int main(){
	int s=2;
	const int n=s;    //普通变量可以初始化const
	int t=n;          //const也可以初始化普通变量
	cout<<s<<" "<<n<<" "<<t<<endl;

	int &y=s;         //引用指向普通变量
	int x=y;          //引用也可以初始化普通变量
	cout<<x<<" "<<y<<endl;

	int num=func();   //这里加const和普通函数并无区别
	func()=3;         //编译不通过
	cout<<num<<endl;
	system("pause");
}

1、函数返回值不想其立即修改的

函数返回值为const,只有用在函数返回为引用的情况。函数返回值引用常量表示不能将函数调用表达式作为左值使用。

比如:

const int & abc(int a,int b,int &re)
{
    re = a+b;
    return re;
}

int main()
{
    int a = 1,b =2,c;
    abc(a,b,c)++;  /////////////////////错误
    c++;           /////////////////////正确
    cout<<c<<endl;
    return 0;
}

2、重载运算符符合逻辑

class A
{
public:
    int a;
    A(int b):a(b){}
    friend const A operator +(const A& lft,const A& rgt)
    {
        return A(lft.a + rgt.a);
    }
};

int main()
{
    A a(1),b(3),c(7);
    a+b = c;                           //错误
    return 0;
}

3、通过函数创建指向常量的指针

常量指针可以指向常变量,也可以指向普通变量,但是不能用普通指针赋值常量指针。

#include <iostream>
using namespace std;

const char *helpFun()
{
    char * p =new char[3];
    p[0]='a';
    p[1]='b';
    p[2]='\0';
    return p;
}

int main()
{
    const char * p = helpFun();
    cout<<p<<endl;
    delete p;
    system("pause");
}

const参数,const返回值与const函数总结

在C++程序中,经常用const 来限制对一个对象的操作,例如,将一个变量定义为const 的:

 const  int  n=3;

则这个变量的值不能被修改,即不能对变量赋值。const 这个关键字经常出现在函数的定义中,而且会出现在不同的位置,比如:

int strcmp(const char *str1,const char *str2);
const int &min (int &,int &);
void printMessage(char *msg) const;
1.const 参数

出现在函数参数中的const表示在函数体中不能对这个参数做修改。比如上面的例子中strcmp()函数用来比较两个字符串的大小,在函数体中不应该改变两个参数的值,所以将它定义为是const的。const通常用来限制函数的指针参数,引用和数组参数,而一般形式的参数因为形参和实参本来就不在同一内存空间,所以对形参的修改不会影响实参,因此也没有必要限制函数体不能对参数进行修改。

下面是一些使用函数const参数的例子:

(1)、函数strcpy()将src字符串的内容复制到targ字符串中,为保证src字符串不被修改,将它定义为const参数:

void  strcpy(const char *src,char *targ);

(2)、函数max()从数组array中找出具有最大值的数组元素并返回这个最大元素的值,为保证数组元素不会在函数中被修改, 将它定义为const参数:

int max(const int array[],int size);

(3)、函数outputObject()将类Myclass的对象obj的内容输出。对象定义为const引用,即可以保证对象不会在函数体中有所改变,又可以节省对象传递的开销:

void outputObject(const Myclass &obj);

PS:const指针可以接受const和非const地址,但是非const指针只能接受非const地址。所以const 指针的能力更强一些,所以尽量多用const指针,这是一种习惯。

2.const 返回值

函数返回值为const只有用在函数返回为引用的情况。函数返回值引用常量表示不能将函数调用表达式作为左值使用。例如前面讲的返回引用的函数min()。

int &min(int &i,int &j); 

可以对函数调用进行赋值,因为它返回的是左值:min(a,b)=4;

但是,如果对函数的返回值限定为const的:const int &min(int &i, int &j);

那么,就不能对min(a,b)调用进行赋值了。

3.const 函数

在类中,可以为类的成员函数进行如下形式的定义:

class  classname{
	int member ;
public:
    int getMember() const; 
};

这里,在函数定义头后面加上的cons 表示这个函数是一个“只读函数”,函数不能改变类对象的状态,不能改变对象的成员变量的值。如在函数体中不能这么写:

classname::getmember(){
	member = 4;            //修改了变量的值
	return member;
}

const成员函数也不能在函数中调用其他非const的函数。

例如:

#include <iostream>
#include <string>
using namespace std;

class Student { 
	string name; 
	int score; 
public: 
    Student(){}  
    Student(const string& nm,int sc=0):name(nm),score(sc){} 
    void set_student(const string& nm, int sc=0){              //后面不能有const 因为要改变变量
		name = nm; 
		score = sc;        
	} 
    const string& get_name() const{
    	return name;   
	} 
	int get_score() const{     
		return score; 
	} 
}; 

void output_student(const Student& student){                    //输出学生名字和分数 
	cout<<student.get_name()<<" "; 
	cout<<student.get_score()<<endl; 
} 

int main() { 
	Student stu("Wang",85); 
	output_student(stu); 
}

1、为什么get_name()前面也加const。如果没有前后两个const的话,get_name()返回的是对私有数据成员name的引用,所以通过这个引用可以改变私有成员name的值,如:

Student stu("Wang",85);
stu.get_name() = "Li";        //引用可以作为左值

即把name由原来的”Wang”变成了”Li”,而这不是我们希望的发生的。所以在get_name()前面加const避免这种情况的发生。

2、get_name()和get_score()这两个后面应该加const的成员函数,如果没有const修饰的话可不可以呢?回答是可以!但是这样做的代价是:const对象将不能再调用这两个非const成员函数了。如:

const string& get_name(); 
int get_score();                                   //这两个函数都应该设成const型

void output_student(const Student& student){
	cout<<student.get_name()<<" ";
  	cout<<student.get_score()<<endl; 
	//如果get_name()和get_score()是非const成员函数,这两句调用都是错误的
}

由于参数student表示的是一个对const Student型对象的引用,所以student不能调用非const成员函数如set_student()。如果get_name()和get_score()成员函数也变成非const型,那么上面的student.get_name()和student.get_score()的使用就是非法的,这样就会给我们处理问题造成困难。

因此,我们没有理由反对使用const,该加const时就应该加上const,这样使成员函数除了非const的对象之外,const 对象也能够调用它。

#include <iostream>
using namespace std;
class A{
public:
	A(int i){
		n=i;
	}
	void getNum() const{
		cout<<n<<endl;
	}
	void setNum(int j){
		n=j;
	}
	int n;
};
int main(){
	const A a(2);
	//cout<<a.n<<endl;
	a.getNum();              //打印2
	//a.setNum(4);
	system("pause");
}

满足对const成员函数的调用:const类型的对象,不能调用自身的非const成员函数,但是可以调用自己的const成员函数。

class A {
public:
    A():num(2){}
    void setnum(){}
    void getnum() const{}
private:
    int num;
};
int main(){
    const A b;
    b.getnum();
    b.setnum();				//错误
    return 0;
}
class A{
public:
    A():num(2){}
    void setnum(){num = 10;}
    void getnum() const{
        printf("%d\\n",num);
    }
private:
    int num;
};

class B
{
public:
	//A类是B类运算中产生的隐藏变量,为了调用A类中的const函数,B类生产A类的函数返回类型需要加const。
    const A* get(){
        A *p = new A();
        return p;
    }
};

int main(){
    B b;
    b.get()->getnum();
    b.get()->setnum();
    return 0;
}

const成员函数的返回类型是引用时候,需要加const约束:

const int& fun() const;
class Test
{
public :
    Test(int a):value(a){}
    const int & GetValue()const
    {
        return value;
    }
private:
    const int value;
};

int main()
{
    Test t(3);
    const int &a = t.GetValue();  //const返回值引用只能用const引用指向
    cout<<a<<endl;
    return 0;
}

如果const成员函数返回的值指针呢?这与返回int类型是一样是赋值给接受对象,而引用因为没有了复制所以必须有const,例如下面代码可以编译运行,同时发现绕开了类的private约束。

class Test
{
public :
    Test(int a):value(a){ p = &value; }
    void setValue(){value =10;}
    int * GetValue()const
    {
        return p;
    }
    int getv(){return value;}
private:
    int value;
    int * p;
};

int main()
{
    Test t(3);
    int *a = t.GetValue();
    cout<<*a<<endl;
    *a = 5;
    cout<<t.getv()<<endl;
    delete a;
    return 0;
}
#include <iostream>
using namespace std;

class A{
public:
	A(int i):n(i){}
	int& get(){
		return n;
	}
	void print(){
		cout<<n<<endl;
	}
private:
	int n;
};

int main(){
	A a(1);
	a.print();            //打印1
	int &b=a.get();       //获取私有变量的引用
	b=2;                  //不通过公有接口修改私有变量
	a.print();            //打印2
	system("pause");
}
#include <iostream>
using namespace std;

class A{
public:
	A(int i):n(i){}
	int* get(){
		return &n;
	}
	void print(){
		cout<<n<<endl;
	}
private:
	int n;
};

int main(){
	A a(1);
	a.print();            //打印1
	int *b=a.get();       //获取私有变量的地址
	*b=2;                 //不通过公有接口修改私有变量
	a.print();            //打印2
	system("pause");
}

Search

    Post Directory