【C++】谈谈对值传递、指针传递、引用传递的理解

【C++】谈谈对值传递、指针传递、引用传递的理解



一、值传递

1.1 值传参

以值的方式给函数传递参数,形参本质上是调用实参类型的拷贝构造函数对实参拷贝的一个副本。

举一个简单的例子——定义一个Person类:

class Person {
public:	
	Person(string name, int* id) {
		m_name = name;
		m_id = id;
		cout << "正在调用有参构造函数" << endl;
	}

	Person(const Person& p) {
		m_name = p.m_name;
		m_id = new int(*(p.m_id));
		cout << "正在调用拷贝构造函数" << endl;
	}

	~Person() {
		if (this->m_id != NULL) {
			delete m_id;
			m_id = NULL;
		}
		cout << "正在调用析构函数" << endl;
	}

public:
	string m_name;
	int* m_id;
};

先以有参构造的方式创建一个值p1,再调用func1(Person p)将p1以值传递的方式传递给形参p

void func1(Person p) {
	cout << &p << endl;
	cout << "function1 is running!" << endl;
}

void test1() {
	Person p1("Chandler", new int(1));
	cout << &p1 << endl;
	func1(p1);
}

int main() {
	test1();
	system("pause");
	return 0;
}

输出结果:

正在调用有参构造函数====》Person p1("Chandler", new int(1))以有参构造的方式创建p1
0056FBB4====》打印了p1的地址
正在调用拷贝构造函数====》将p1值传递给形参时调用拷贝构造函数创建了一个副本p
0056FA94====》形参(副本)p的地址与实参的地址并不相同
function1 is running!
正在调用析构函数====》func1函数执行结束,拷贝的副本p调用析构函数进行释放
正在调用析构函数====》test1函数执行结束,p1调用析构函数进行释放

分析:

通过输出结果可以发现,实参p1的地址与形参p的地址不相同,且p1以值的方式传入函数时调用了拷贝构造函数来构造p,说明形参p实际上是调用拷贝构造函数对实参拷贝的一个副本。
注意:当没有重写拷贝构造函数时,调用的是默认的拷贝构造函数,可能存在一些浅拷贝带来的问题。

1.2 值作为函数返回

以值的方式作为函数的返回值,return p实质上返回的是调用p的拷贝构造函数对p拷贝的一个副本。

Person func2(Person p) {
	cout << &p << endl;
	cout << "function is running!" << endl;
	return p;
}

void test2() {
	Person p1("Chandler", new int(1));
	Person p2 = func2(p1);
	cout << &p1 << endl;
	cout << &p2 << endl;
}

int main() {
	test2();
	system("pause");
	return 0;
}

输出结果:

正在调用有参构造函数======>有参构造p1
正在调用拷贝构造函数======>以值传递方式给func2传入p1,拷贝构造形参p
0086FC80=====>形参p的地址
function is running!
正在调用拷贝构造函数======>注意!!!以值的方式返回p,实质上对p进行拷贝构造返回了一个副本
正在调用析构函数======>func2函数结束,形参p被释放
0086FDC8======>p1的地址
0086FDA0======>p2的地址
正在调用析构函数
正在调用析构函数

分析:

通过对比可以发现,func2中形参p的地址与接收函数返回值p2对象的地址并不相同,且func2函数return时调用了拷贝构造函数,说明以值的方式作为函数返回值实际上返回的是拷贝构造的一个副本。

二、指针传递

指针/地址传递实际上传递的是一个值为地址的指针,指针可以看成是一种“地址类型”的变量,在32位系统下无论什么类型的指针都是占用4个字节的内存空间。
一种理解方式:指针传递其实是一种特殊的值传递,因为指针是一种保存地址的变量。在使用指针进行传参或函数返回时,也会由类似值传递调用拷贝构造来创建指针变量的副本,当不管是原本(被传递的指针)还是副本,它们的内容都是对象的地址,而拿到这个地址就可以对对象本身进行修改等操作。

2.1 指针传参

以指针/地址的方式对函数传递参数,传递的是实参的地址,函数可以根据实参的地址直接对实参进行相应的操作,而不需要创建副本。
通过以下一段代码来解释地址传参的原理:
首先通过创建指向一个Person对象的指针p1,以p1作为实参传入函数func3(以指针的方式传参)

void func3(Person* p) {
	cout << "形参指针p的值为:" <<  p << endl;
	p->m_name = "Monica";	
}

void test3() {
	Person *p1 = new Person("Chandler", new int(1));
	cout << "指针p1的值为:" << p1 << endl;
	cout << p1->m_name << endl;
	func3(p1);
	cout << p1->m_name << endl;
}

int main() {
	test3();
	system("pause");
	return 0;
}

输出结果:

正在调用有参构造函数
形参指针p的值为:01183848
Chandler
指针p1的值为:01183848
Monica

分析:

通过对比可以发现,传入的实参指针p1的值与func3中形参指针p的值一致,实参p1与形参p指向同一对象,函数内部对形参所指对象进行操作等同于对传入的实参所指对象进行操作。
函数内部对形参p所指对象的属性m_name进行修改,可以发现外部实参p1所指对象的m_name由"Chandler"变为了“Monica”,说明以地址/指针传参可以对实参进行相应操作

2.2 指针作为函数返回值

Person* func4(Person* p) {
	cout << "形参指针p的值为:" << p << endl;
	return p;
}

void test4() {
	Person* p1 = new Person("Chandler", new int(1));
	cout << "指针p1的值为:" << p1 << endl;
	Person* p2 = func4(p1);
	cout << "函数返回值p2的值为:" << p2 << endl;
}

int main() {
	test4();
	system("pause");
	return 0;
}

输出结果:

正在调用有参构造函数
指针p1的值为:010D36B8
形参指针p的值为:010D36B8
函数返回值p2的值为:010D36B8

分析:

传入func4中实参指针p1的值等于func4中形参指针p的值,等于函数返回值指针p2的值。

三、引用传递

引用实际上是一种指针常量(指针的指向不可改变,指针指向的值可以改变,例如:int* const ref)引用必须初始化且初始化将和初始值对象一直绑定在一起。注意:程序把引用和初始值绑定在一起,而不是将初始值拷贝给引用。
在引用的实际使用过程中,编译器会自动识别出变量ref是引用类型,然后自动将这个指针常量所指的对象( *ref)返回给我们进行使用。

举个例子:

int a = 1;
int &ref = a;//编译器识别ref为引用类型,将代码自动翻译为int* const ref = &a;
ref = 10;//编译器识别ref是引用,将代码自动翻译为*ref = 10;

3.1 引用传参

一种直观的理解方式:函数以引用的方式传入一个实参,函数内部对形参的操作就是对实参进行相应的操作。
稍稍深入:函数形参是引用类型时,当函数被调用即传入实参时,此时创建这个引用并将其绑定在实参对象上——引用是其初始化对象的别名,此后函数内部对引用的操作就是对这个初始化对象的操作。

void swapFunc(int &num1, int &num2) {
	int temp = num1;
	num1 = num2;
	num2 = temp;
}

int main() {
	int a = 5;
	int b = 3;
	swapFunc(a, b);
	cout << a << endl;
	cout << b << endl;
	return 0;
}

分析:

函数通过引用进行传参,在函数内部对形参进行修改操作,可以发现实参a,b的值也发生交换,说明引用传参可以对实参进行修改等操作。

以指针常量的角度理解swapFunc(int &num1, int &num2)的调用过程:

//swapFunc(a, b);
int * const num1 = &a;
int * const num2 = &b;
int temp = *num1;
*num1 = *num2;
*num2 = temp;

可以发现引用传递和指针传递很类似,也是通过拿到对象的地址再对对象进行相应操作。

3.2 引用作为函数返回值

如果函数的返回值是引用类型,函数中return obj,实际上是执行(以指针常量的方式理解):

  1. Obj * const ref = &obj;
  2. return *ref;

编译器可以自动识别出ref是引用类型,我们关于ref的代码实质上是“* ref”(把ref作为指针常量)
简单理解:引用作为函数返回值时,函数返回的就是return的那个对象本身

int& myPlus(int& num) {
	num++;
	cout << &num << endl;
	return num;
}

int main() {
	int a = 5;
	cout << &a << endl;
	cout << &(myPlus(a)) << endl;
	return 0;
}

输出结果:

010FF828====>&a
010FF828====>函数myPlus中&num
010FF828====>&(myPlus(a))

注意:一个潜在的风险——引用返回局部变量:引用传递时如果函数返回的是一个局部变量,当函数执行结束后局部变量被释放了,函数返回的引用会出现问题——即引用接收到的地址值还是之前那个对象的地址,但是那个地址对应的对象的值(即和引用绑定的初始化对象的值)因为被释放掉就没办法保证了。

3.3 小trick

通过引用传参传递可以对实参本身进行操作,但有时为了防止实参被修改,可以利用常量引用来修饰形参,防止误操作。

void show(const int& num) {
	//num++;//如果尝试对常量引用的值进行修改会报错
	cout << num << endl;
}

int main() {
	int a = 5;
	show(a);
	return 0;
}

总结

  1. 值传递:需要建立一个副本,当对象“很大”时,去拷贝构造一个该对象的副本开销较大,对副本对象的操作并不会影响“原本”对象。
  2. 指针/地址传递:也需要建立一个副本,但是这个副本是一个只占4个字节的指针变量,相比值传递开销明显减少。并且由于副本和原本保存的都是同一个对象的地址,二者指向的是同一个对象。
  3. 引用传递:引用ref实质上是一个指针常量,不过编译器可以自动识别引用类型,使用引用时编译器自动把这个指针常量所指的对象返回给我们。因此通过引用进行传递,传递的就是这个对象本身。

参考资料

  1. 《C++ Primer》第五版
  2. C++引用的本质
  3. 细谈 C++ 返回传值的三种方式:按值返回、按常量引用返回以及按引用返回
  4. 黑马程序员C++系列课程
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值