专业编程基础技术教程

网站首页 > 基础教程 正文

C++|深入理解函数参数传递与返回 c++函数参数的传递方式有

ccvgpt 2024-10-19 03:24:51 基础教程 7 ℃

1 函数的封装与代回

为什么需要函数?对于一些重复性的功能实现,为避免复制、粘贴带来的麻烦,而提炼出函数(并通过参数实现一般化),是分治算法的一种实现,代码整体上也更加模块化。所以要理解函数的种种参数传递与返回机制,要能考虑如何提炼出函数和如何将函数打散后再代回原调用处。

2 普通变量、指针变量、引用变量之间的地址语义和值语义

变量的二重属性:变量首先是一个内存单元地址的名称化,这是变量的地址语义,内存单元的比特值根据类型实现其值语义;

C++|深入理解函数参数传递与返回 c++函数参数的传递方式有

2.1 普通变量:显式使用其值语义,隐式使用其地址语义;

2.2 指针变量:显式使用其地址语义,隐式使用其值语义;

2.3 引用变量:声明、定义、初始化时显式使用其地址语义(使用上类似指针变量),此外的其他应用场合显式使用其值语义(使用上类似普通变量);所以说,引用变量其实质是一个有常量属性的实现了自动解引用的特殊指针。所以引用相对于指针,有其使用上的简洁性、安全性,但也有其模糊性。

	int i=5;
	int* p = &i;
	int& r = i;
	i=++*p;
	r++;
	cout<<i;//7

以上的赋值方式,同样也适用于实参与形参的结合,以及函数返回值的语法机制。

应该使用指针的情况: 可能存在不指向任何对象的可能性,需要在不同的时刻指向不同的对象(此时,你能够改变指针的指向) 。返回函数体中new出的内存空间的地址。多态中使用指针。

应该使用引用的情况: 如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,使用此时应使用引用。

数组用作函数形式参数时会丢失数组元素个数的信息,即退化为指针;

函数返回数组名,实际返回的是指向数组首元素的指针。

3 主调函数caller与被调函数callee之间的关系

主调函数caller与被调函数callee之间的关系主要有两种:

一是两者相互独立;

二是两者通过实参和形参的结合来建立联系,实现内存单元的共享,可读也可写共享的内存单元。

实现的机制是通过副本机制(包括传参和返回)来实现的。

3.1 值传递:副本保存的是值(不是地址和引用),主调函数和被函数之间相互独立;

3.2 指针传递和返回:副本保存的是地址(不是值),主调函数和被函数之间通过传递的参数变量相互影响;

3.3 引用传递和返回:副本保存的是地址(不是值),主调函数和被函数之间通过传递的参数变量相互影响;但引用在初始化时有指针语义,在使用时有普通变量语义(自动解引用);

函数的调用与嵌套调用通过栈来实现回溯,函数内可以用{}来嵌套块作用域。

4 指针和引用做参数及返回时,对应数据处理的方式

当指针做参数时,函数要处理的往往不是指针本身,而是指针所指向的数据,所以通常会以解引用的形式做为左值或右值出现在函数体中,而当指针做为左值出现时,通常是用于指针的修改或移动(更多的情形是将形参指针赋给一个临时变量的指针,以避免形参指针的变更)。

当引用做参数时,函数体中对引用所指向的变量的处理,因为其特殊的语义,使用上就像普通变量。

5 实参与形参结合及函数返回值的理解

5.1 实参与形参结合:局部变量的声明、定义、初始化;

5.2 函数返回值:可以理解为函数名做为一个全局变量,类型就是函数的类型,值就是其返回值;

#include <iostream>
using namespace std;
int& fr(int b[],int i)
{
	return b[i];
}
//int& fr = b[i];
int* fp(int b[],int i)
{
	return &b[i];
}
//int* fp = &b[i]
int main()
{
	//调用时
	int a[] = {1,2,3,4,5};
	int n = fr(a,3);// 实参与形参结构相当于:int b[]; b = a,int i = 3;
	fr(a,3) = 14;//相当于a[3];
	cout<<*fp(a,2)<<endl;// 实参与形参结构相当于:int b[]; b = a,int i = 3;
	system("pause");
	return 0;
}

5.3 两者都可能会有隐式类型转换,当是类类型和对象时,会自动调用拷贝构造函数。

6 指针做为参数时,注意区分函数体对指针本身或指针所指向的对象的处理

且两种情形的作用域不同,指针参数的作用域是本函数体,指针所指向的对象的作用域在本函数体以外。

#include <iostream>
using namespace std;
void f(char* p)
{
	*p = 'A'; //对指针指向的内存单元的操作(解引用)
	p++;		//对指针本身的操作,p=p+1,指针值发生了变化,相当于指针移到了下一个位置,
	//这里是演示用,通常是用一个临时指针来做指针移动
	*p = 'B';
	cout<<p<<endl;//Bc
}
int main()
{	
	char arr[] = "abc";
	f(arr);
 system("pause");
	return 0;
}

7 可以返回指针或引用的数据结构

因为函数调用是通过栈机制来实现的,函数实现了一个独立的作用域机制,所以函数不能返回函数体内定义的非静态局部变量的指针或引用。

可以返回指针或引用的数据结构包括:

7.1 引用或指针参数指向内容(包括数组);

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int *func(int *p) 
{
 (*p)++;
	printf("%p\n",p);
 return p; //p本身虽是局部变量,但其值却是从主调函数传过来的地址值
}
/*
int& func(int& p) 
{
 p++;
 return p; 
}
*/
int main()
{
 int i=10;
	int *p=&i;
	printf("%p\n",p);
 printf("%d\n", *(func(p)));
	system("pause");
 return 0;
}
/*
0012FF44
0012FF44
11
*/

数组也是如此:

int* f2(int a[],int i)
{
	return &a[i];
int& f2(int a[],int i)
{
	return a[i];
 } 

7.2 静态局部变量;

int& func()
{
 static int i=10; //因为是全局的静态区
 int& p=i;
 p++;
 return p; //OK:可以做为返回值
}

7.3 全局变量;

7.4 堆上数据;

避免返回函数内部new分配的内存的引用,但可以返回new的指针(也违背“谁申请,谁释放”的原则,存在内存泄漏的安全隐患)。

7.5 成员函数对私有数据成员的引用

#include<iostream>
using namespace std;
 
class C
{
	public:
		//返回一个引用,也就是n的引用 
		int& getReFN()
		{
			return n;
		}
		int getN()
		{
			return n;
		}
	private:
		int n;
}c;
 
int main()
{
	//将返回的引用赋值给k,k和n是一样的
	//尽管n声明为私有,但是执行下条语句时候,就可以在外界通过k随意访问该变量 
	int& k = c.getReFN();
	k = 7;
	cout<<c.getN()<<endl;//7
	c.getReFN() = 9;
	cout<<c.getN()<<endl;//9
	system("pause");
	return 0;
 }

8 二级指针或指针引用做函数参数

8.1 二级指针或指针引用做函数参数:

#include <iostream>
using namespace std;
void GetMemory(char* *pp,int n)
{
 *pp = new char[n];//pp的解引用是*pp
}
int main()
{
 char* p = NULL;
 GetMemory(&p,100);//注意二级指针的赋值char**p = &p;
 strcpy(p,"Hello!");
 cout<<p;
 delete[]p;
 p=NULL;
 system("pause");
 return 0;
}

以上为什么不能用一级指针作为参数?还是要回到前面讨论的在函数体内当用指针做形参时,在函数体操作指针本身与指针指向的对象的区别。也就是指针也其解引用的区别。

8.2 指针引用做函数参数

相对于二级指针更简洁:

#include <iostream>
using namespace std;
void GetMemory(char* &pp,int n)
{
	pp = new char[n];
}
int main()
{
	char* p = NULL;
	GetMemory(p,100);//注意指针引用的赋值char*&p = p;
	strcpy(p,"Hello!");
	cout<<p;
	delete[]p;
	p=NULL;
 system("pause");
	return 0;
}

8.3 当然也可以用指针函数

#include <iostream>
using namespace std;
char* GetMemory(int n)
{
	char* pp = new char[n];
	return pp;
}
int main()
{
	char* p = NULL;
	p=GetMemory(100);//注意指针引用的赋值char*&p = p;
	strcpy(p,"Hello!");
	cout<<p;
	delete[]p;
	p=NULL;
 system("pause");
	return 0;
}

以上美中不足的是,违背了“谁创建、谁释放”的原则,容易造成内存泄漏。

但有时却不得已为之:

const int MAX = 55;
typedef struct Heap
{
	int sizeHeap;
	int* heapData;
}HEAP,*LPHEAP;
LPHEAP createHeap()
{
	LPHEAP heap=(LPHEAP)malloc(sizeof(HEAP));
	heap->sizeHeap=0;
	heap->heapData=(int*)malloc(sizeof(int)*MAX);
	return heap;
}

8.4 最常见的是链表操作:

struct node
{
 char data;
 node * next;
}
void Insert(node * &head,char keyWord,char newdata)	//keyWord是查找关键字符
{
	node *newnode=new node;	//新建结点
	newnode->data=newdata;	//newdata是新结点的数据
	node *pGuard=Search(head,keyWord);	//pGuard是插入位置前的结点指针
	if (head==NULL || pGuard==NULL)	//如果链表没有结点或找不到关键字结点
	{				//则插入表头位置
		newnode->next=head;	//先连
		head=newnode;	//后断
	}
	else	//否则
	{				//插入在pGuard之后
		newnode->next=pGuard->next;	//先连
		pGuard->next=newnode;	//后断
	}
}

9 三种传递方式的效率比较

如果是单个的基本数据类型,值传递、指针传递、引用传递三者的区别不是很大,但当传递的是复杂的数组、结构体、类对象时,区别就很大了。

#include <stdio.h>
#include <string>
#include <iostream>
#include <sstream>
#include <time.h>
using namespace std;
string test = "origion" ;
string changed = "changed" ;
string getTime()
{
	time_t tt =time(NULL);
	tm* t =localtime(&tt);
	string mon;
	stringstream ss;
	ss<<t->tm_mon;
	ss>>mon;
	ss.clear();
	string day;
	ss<<(t->tm_mday);
	ss>>day;
	string hour;
	ss.clear();
	ss<<(t->tm_hour);
	ss>>hour;
	string min;
	ss.clear();
	ss<<(t->tm_min);
	ss>>min;
	string sec;
	ss.clear();
	ss<<(t->tm_sec);
	ss>>sec;
	string times = mon+"-"+day+":"+hour+":"+min+":"+sec ;
	return times ;
}
void func1(string s)//值传递
{
	s = changed ;
}
void func2(string& s)//引用传递
{
	s = changed ;
}
void func3(string* s)//指针传递
{
	*s = changed;
}
int main()
{
	cout<<getTime()<<endl;
	for (int i = 0; i < 3000000; i++)
	{
		func1(test);
	}
	cout<<getTime()<<endl;
	for (i = 0; i < 3000000; i++)
	{
		func2(test);
	}
	cout<<getTime()<<endl;
	for (i = 0; i < 3000000; i++)
	{
		func3(&test);
	}
	cout<<getTime()<<endl;
	//func3(&test);
	system("pause");
	return 0 ;
}
/*
9-26:18:29:50
9-26:18:29:54
9-26:18:29:56
9-26:18:29:58
*/

如果参数是一个较复杂类对象,较效果更明显。

10 赋值运算符重载与引用返回

赋值运算符重载一般使用引用作为返回,简洁,高效,还可以实现链式表达式操作:

String 的赋值函数 operator = 的实现如下: 
	String & String::operator=(const String &other)
	{
	if (this == &other) //自己赋值给自己会形成循环调用构造函数的错误
	return *this; 
	delete m_data; //因为赋值相当于更新,原来的空间释放,使用新的堆空间
	m_data = new char[strlen(other.data)+1]; 
	strcpy(m_data, other.data);
	return *this; // 返回的是 *this 的引用,无需拷贝过程
	}

this是隐含的指向函数调用对象的指针,一般是隐式使用,在特殊场合下可以显式使用。

为什么是return *this而不是return this?要注意引用在地址语义和值语义上的的语法特性,引用是用一个变量名(或对象名)而不是指针来初始化的。

看下面实例:

int& at()
{
 return m_data_; //副本机制是变量地址
}
int at()
{
 return m_data_; //副本机制是变量值
}

11 其它

11.1 复制构造函数的参数为什么一定要用引用传递,而不能用值传递?

值传递的参数在参数传递时有一个构造过程,即用实际参数的值构造形式参数,这个构造过程是由复制构造函数完成的。如果将复制构造函数的参数设计成值传递,会引起复制构造函数的递归调用。

11.2 下标运算符重载函数为什么要用引用返回?

下标运算符的第一个运算数是数组名,即当前类的对象。将下标运算符重载成成员函数时,编译器会将程序中诸如a[i]的下标变量的引用改为a.operator[](i)。如果a不是当前类的对象,编译器就会报错。下标变量是左值,所以必须用引用返回

11.3 链式表达式的实现要用可以做为左值的引用或指针做为函数返回值,使用引用更为简洁。重载操作符>>或<<时,返回的类型应该是一个流类型,而且返回的类型必须是引用,也是为了链式表达式实现的需要。

11.4 一般用指针参数做为输出参数,放在参数列表的右边(输入参数放在左边),而函数的参数有时用于输出逻辑值。

11.5 当为了避免副本机制而使用址传递或返回而又不需修改共享内存时,可以附加const修饰,以增强安全性。

-End-

Tags:

最近发表
标签列表