浅谈C++三种传参方式
浅谈C++三种传参方式
C++给函数传参中,主要有三种方式:分别是值传递、指针传递和引用传递。
下面通过讲解和实例来说明三种方式的区别。
值传递
我们都知道,在函数定义括号中的参数是形参,是给函数内专用的局部变量,意味着函数接收到的是实参的副本,如果形参的值在函数内部被改变,对实参是没有影响的。
#include <iostream>
using namespace std;
void change(int formalNum) {
formalNum = 0;
cout << "formalNum address: " << &formalNum << endl;
}
int main() {
int realNum = 10;
cout << "Before Change: " << realNum << endl;
cout << "realNum address: " << &realNum << endl;
change(realNum);
cout << "After Change: " << realNum ;
return 0;
}
// 执行结果
Before Change: 10
realNum address: 008FFDA0
formalNum address: 008FFCCC
After Change: 10
可以看见,实参和形参的地址完全不一样,而且函数完全没有办法改变实参的值。值传递的作用更多是让函数内部了解外部参数的值。值传递是单向的,只能由实参传向形参。
指针传递
指针传递很好理解,形参为指向实参地址的指针,当对形参操作时,等同于直接通过地址操作实参。
#include <iostream>
using namespace std;
void change(int *ptr) {
*ptr = 0;
}
int main() {
int realNum = 10;
int* ptr = &realNum;
cout << "Before Change: " << realNum << endl;
change(ptr);
cout << "After Change: " << realNum ;
return 0;
}
// 执行结果
Before Change: 10
After Change: 0
可以很明显地看见,我们在函数内部成功地修改了实参的值。是C++很常见的一种传参方式。
引用传递
引用传递其实是最难理解的一种传参方式。在详细剖析它之前,我们先说他的功能。
向函数传递参数的引用调用方法,把引用的地址复制给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
那么肯定有人问了,既然都是直接影响,指针和引用有啥区别呢???那区别可大了去了。
- 指针从本质上是一个变量,是一个整形变量,存放的是另一个变量的地址。指针在逻辑上是独立的,它可以被改变,甚至能改变它的值(指向其他地址),并且可以取出对应内存中的数据。
- 引用可以理解为外号,是另一个变量的同义词,它在逻辑上具有依附性,所以C++也规定引用的在创立的时候就必须被初始化(现有一个变量,然后创建对该变量的引用)。而且其引用的对象在其整个生命周期中不能被改变,即自始至终只能依附于同一个变量(初始化的时候代表的是谁的别名,就一直是谁的别名,不能变)。
- 在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
引用的规则:
- 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
- 不能有NULL引用,引用必须与合法的存储单元关联(指针可以有野指针,可以指向NULL)。
- 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
看了这么多,指针传递和引用传递的用处是什么呢?
- 函数内部修改参数并且希望改动影响调用函数。对比指针/引用传递可以将改变由形参“传给”实参(实际上就是直接在实参的内存上修改);
- 当一个函数实际需要返回多个值,而只能显式返回一个值时,可以将另外需要返回的变量以指针/引用传递。
下面看具体操作:
引用变量的定义方法和常规变量类似,但是其数据类型和名称之间有一个 & 符号。例如,以下函数定义使形参 refNum 成为引用变量:
#include <iostream>
using namespace std;
void change(int& refNum) {
refNum = 0;
cout << "reference address: " << &refNum << endl;
}
int main() {
int realNum = 10;
cout << "Before Change: " << realNum << endl;
cout << "realNum address: " << &realNum << endl;
change(realNum);
cout << "After Change: " << realNum ;
return 0;
}
// 执行结果
Before Change: 10
realNum address: 00A4F9F4
reference address: 00A4F9F4
After Change: 0
可以看见,引用传递成功地改变了参数的值,同时形参的地址和实参的地址其实是一模一样的。
在学完数据结构和算法后,我对其又有新的认识。
void func(nodeList* &Node){
// 这里对Node进行了操作
}
上面这个传参,又有*又有&,第一眼有点懵,后来细想一下其实很简单。
nodeList是一个整体,代表传进来的是nodeList这个类的指针。我们之前已经学到了,指针其实是一个变量,它的基本性质和变量没有区别。那么我们要在函数体内改变其值,最安全的办法就是传入其引用(也可以创建指针的指针)。所以这里的&Node表示引用Node的实参,是Node的别名,操作引用变量就相当于操作实参变量。*
CEx08aApp::CEx08aApp() { // TODO: add construction code here, // Place all significant initialization in InitInstance int a =0; int b =1; testEBP(a,a, b,b); 68: int a =0; 00402BBC C7 45 EC 00 00 00 00 mov dword ptr [ebp-14h],0 69: int b =1; 00402BC3 C7 45 E8 01 00 00 00 mov dword ptr [ebp-18h],1 70: testEBP(a,a, b,b); 00402BCA 8B 4D E8 mov ecx,dword ptr [ebp-18h] 00402BCD 51 push ecx 00402BCE 8B 55 E8 mov edx,dword ptr [ebp-18h] 00402BD1 52 push edx 00402BD2 8B 45 EC mov eax,dword ptr [ebp-14h] 00402BD5 50 push eax 00402BD6 8B 4D EC mov ecx,dword ptr [ebp-14h] 00402BD9 51 push ecx //copy 00402BDA 8B 4D F0 mov ecx,dword ptr [ebp-10h] 00402BDD E8 EE 00 00 00 call CEx08aApp::testEBP (00402cd0) --- F:\C++资料\yangl_test\ex08a\ex08a.cpp ---------------------------------------------------------------------------------------- 92: 93: int CEx08aApp::testEBP(int a, int b, int c, int d) //未写类前缀 94: { 00402CD0 55 push ebp 00402CD1 8B EC mov ebp,esp 00402CD3 83 EC 48 sub esp,48h 00402CD6 53 push ebx 00402CD7 56 push esi 00402CD8 57 push edi 00402CD9 51 push ecx 00402CDA 8D 7D B8 lea edi,[ebp-48h] 00402CDD B9 12 00 00 00 mov ecx,12h 00402CE2 B8 CC CC CC CC mov eax,0CCCCCCCCh 00402CE7 F3 AB rep stos dword ptr [edi] 00402CE9 59 pop ecx 00402CEA 89 4D FC mov dword ptr [ebp-4],ecx 95: int r = a+b+c+d; 00402CED 8B 45 08 mov eax,dword ptr [ebp+8] 00402CF0 03 45 0C add eax,dword ptr [ebp+0Ch] 00402CF3 03 45 10 add eax,dword ptr [ebp+10h] 00402CF6 03 45 14 add eax,dword ptr [ebp+14h] 00402CF9 89 45 F8 mov dword ptr [ebp-8],eax 96: d = 0; 00402CFC C7 45 14 00 00 00 00 mov dword ptr [ebp+14h],0 //修改的本地栈内存 97: return 0; 00402D03 33 C0 xor eax,eax 98: } 00402D05 5F pop edi 00402D06 5E pop esi 00402D07 5B pop ebx 00402D08 8B E5 mov esp,ebp 00402D0A 5D pop ebp 00402D0B C2 10 00 ret 10h
类对象的拷贝构造函数:
testClass::testClass(const testClass & {...}) address 0x00402d20
CEx08aApp::CEx08aApp() line 71 + 23 bytes
CEx08aApp::CEx08aApp() { // TODO: add construction code here, // Place all significant initialization in InitInstance testClass b; testEBP(b,1,2,3); ================================ 70: testClass b; 00402BBC 8D 4D D8 lea ecx,[ebp-28h] 00402BBF E8 5C 00 00 00 call testClass::testClass (00402c20) 00402BC4 C6 45 FC 01 mov byte ptr [ebp-4],1 71: testEBP(b,1,2,3); 00402BC8 6A 03 push 3 00402BCA 6A 02 push 2 00402BCC 6A 01 push 1 00402BCE 83 EC 18 sub esp,18h 00402BD1 8B CC mov ecx,esp 00402BD3 89 65 D4 mov dword ptr [ebp-2Ch],esp 00402BD6 8D 55 D8 lea edx,[ebp-28h] 00402BD9 52 push edx 00402BDA E8 41 01 00 00 call testClass::testClass (00402d20) 拷贝构造函数 00402BDF 89 45 D0 mov dword ptr [ebp-30h],eax 00402BE2 8B 4D F0 mov ecx,dword ptr [ebp-10h] 00402BE5 E8 16 02 00 00 call CEx08aApp::testEBP (00402e00) ================================== testClass::testClass: 00402D20 55 push ebp 00402D21 8B EC mov ebp,esp 00402D23 83 EC 44 sub esp,44h 00402D26 53 push ebx 00402D27 56 push esi 00402D28 57 push edi 00402D29 51 push ecx 00402D2A 8D 7D BC lea edi,[ebp-44h] 00402D2D B9 11 00 00 00 mov ecx,11h 00402D32 B8 CC CC CC CC mov eax,0CCCCCCCCh 00402D37 F3 AB rep stos dword ptr [edi] 00402D39 59 pop ecx 00402D3A 89 4D FC mov dword ptr [ebp-4],ecx 00402D3D 8B 45 FC mov eax,dword ptr [ebp-4] 00402D40 8B 4D 08 mov ecx,dword ptr [ebp+8] 00402D43 8B 11 mov edx,dword ptr [ecx] 00402D45 89 10 mov dword ptr [eax],edx 00402D47 8B 45 FC mov eax,dword ptr [ebp-4] 00402D4A 8B 4D 08 mov ecx,dword ptr [ebp+8] 00402D4D 8B 51 04 mov edx,dword ptr [ecx+4] 00402D50 89 50 04 mov dword ptr [eax+4],edx 00402D53 8B 45 08 mov eax,dword ptr [ebp+8] 00402D56 83 C0 08 add eax,8 00402D59 8B F4 mov esi,esp 00402D5B 50 push eax 00402D5C 8B 4D FC mov ecx,dword ptr [ebp-4] 00402D5F 83 C1 08 add ecx,8 00402D62 FF 15 6C BA 40 00 call dword ptr [__imp_??0?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAE@ABV 00402D68 3B F4 cmp esi,esp 00402D6A E8 25 3D 00 00 call _chkesp (00406a94) 00402D6F 8B 45 FC mov eax,dword ptr [ebp-4] 00402D72 5F pop edi 00402D73 5E pop esi 00402D74 5B pop ebx 00402D75 83 C4 44 add esp,44h 00402D78 3B EC cmp ebp,esp 00402D7A E8 15 3D 00 00 call _chkesp (00406a94) 00402D7F 8B E5 mov esp,ebp 00402D81 5D pop ebp 00402D82 C2 04 00 ret 4 ============================== 94: int CEx08aApp::testEBP(testClass a, int b, int c, int d) //δдÀàǰ׺ 95: { 00402E00 55 push ebp 00402E01 8B EC mov ebp,esp 00402E03 83 EC 48 sub esp,48h 00402E06 53 push ebx 00402E07 56 push esi 00402E08 57 push edi 00402E09 51 push ecx 00402E0A 8D 7D B8 lea edi,[ebp-48h] 00402E0D B9 12 00 00 00 mov ecx,12h 00402E12 B8 CC CC CC CC mov eax,0CCCCCCCCh 00402E17 F3 AB rep stos dword ptr [edi] 00402E19 59 pop ecx 00402E1A 89 4D FC mov dword ptr [ebp-4],ecx 96: a.fieldA = 1; 00402E1D C7 45 08 01 00 00 00 mov dword ptr [ebp+8],1 97: return 0; 00402E24 C7 45 F8 00 00 00 00 mov dword ptr [ebp-8],0 00402E2B 8D 4D 08 lea ecx,[ebp+8] 00402E2E E8 9D FE FF FF call testClass::~testClass (00402cd0) 00402E33 8B 45 F8 mov eax,dword ptr [ebp-8] 98: } 00402E36 5F pop edi 00402E37 5E pop esi 00402E38 5B pop ebx 00402E39 83 C4 48 add esp,48h 00402E3C 3B EC cmp ebp,esp 00402E3E E8 51 3C 00 00 call _chkesp (00406a94) 00402E43 8B E5 mov esp,ebp 00402E45 5D pop ebp 00402E46 C2 24 00 ret 24h
拷贝构造函数何时调用?
背景知识
之前写的几篇文章就派上用场了,在接着往下阅读之前,我们需要首先掌握以下知识:
正文部分
典型案例
/*虚构有这么一个类,如有雷同,纯属巧合*/
class Student
{
public:
Student(const char* name, int size)
{
_name = new char[size];
strncpy_s(_name, size, name, MaxNameSize);
}
~Student()
{
delete _name;
_name = nullptr;
}
private:
char* _name;
static constexpr int MaxNameSize = 64;
};
// 实际工程中用值类型传递一个对象
// 也是存在的,很有可能还不在少数
static void Save(Student s)
{
}
int main()
{
constexpr char Jim[] {"Jim"};
constexpr size_t SizeOfJim = sizeof (Jim) / sizeof (char);
Student jim{Jim, SizeOfJim};
Save(jim);
return 0;
}
我们先分析下:
- 类
Student
没有提供拷贝构造函数 Save(Student s)
函数中的形参是个值类型
因为Student
没提供拷贝构造函数,而又有Save(Student s)
这样的函数要用,所以编译器就会助人为乐给你一个,但编译器又不知道Student
类中存在像char* _name;
这样的需要在堆上分配内存的成员变量,这样就会导致在调用Save(jim);
这个函数的时候,复制出来的临时对象s
和作为实参传进来的jim
对象指向同一块堆内存_name
。注:这里比较拗口,不妨多读几遍。
这样可能会因为对象生命期的不一致或者处于不同的线程,从而使得程序访问已经被释放掉的_name
引发程序崩溃。
要解决这个问题,一种办法是提供一个拷贝构造函数,管理好堆内存,这样不管Save
函数的参数是值类型的还是引用类型的,至少不会引发程序崩溃。
另一种办法就是我们要着重讨论的:抑制类的拷贝构造,通常情况下,在程序员不能明确表达自己的设计意图的时候,笔者是推荐这么做的。
抑制类的拷贝构造
- 只声明不实现法,C++11之前流行的做法
class Student
{
public:
Student(const char* name, int size)
{
_name = new char[size];
strncpy_s(_name, size, name, MaxNameSize);
}
~Student()
{
delete _name;
_name = nullptr;
}
private:
/* 看这里,只声明不实现 */
/*
* 至于访问权限,不推荐用public,
* protected还是private的取决于实际场景
*/
Student(const Student&);
private:
char* _name;
static constexpr int MaxNameSize = 64;
};
- 使用delete告诉编译器别没事找事干,C++11之后流行的做法
class Student
{
public:
Student(const char* name, int size)
{
_name = new char[size];
strncpy_s(_name, size, name, MaxNameSize);
}
/* 看这里,直接delete掉 */
/* 这里的访问权限为public可以明确你的设计意图 */
Student(const Student&) = delete;
~Student()
{
delete _name;
_name = nullptr;
}
private:
char* _name;
static constexpr int MaxNameSize = 64;
};
别忘了抑制赋值函数
int main()
{
constexpr char Jim[] {"Jim"};
constexpr size_t SizeOfJim = sizeof (Jim) / sizeof (char);
Student jim{Jim, SizeOfJim};
constexpr char Mike[] {"Mike"};
constexpr size_t SizeOfMike = sizeof (Mike) / sizeof (char);
Student mike{Mike, SizeOfMike};
/* 这样编译能通过吗? */
mike = jim;
}
形如上面的mike = jim;
是要发生灾难的,同样因为编译器会乐于助人地提供了一个默认赋值函数,所以同时也要抑制赋值函数,风格尽量保持跟抑制拷贝构造函数的方式一致。
总结
通过明确是否抑制构造函数和赋值函数,清晰地表达出设计意图,渐渐地提升我们的设计思维。这里是比较常见的抑制手法,对于非常熟悉C++类的机制的开发人员来说我们可以这么做,但当团队中存在人员流动并且类的数目和变动比较频繁的情况下,这种做法显得有点单薄了,难以形成体系。有没有其他的方案呢?
因为这篇篇幅有点长了,知友们可能已经累了,找时间再进一步讨论。
清华大学 c++语言程序设计
拷贝构造函数 在以下三种情况下会被调用:
当用类的一个对象初始化该类的另一个对象时
int main(void)
{
Point A(1,2);
Point B(A);
}
如果函数形参是类的对象,调用函数时,进行形参和实参结合时, void fun(Point p);
如果函数的返回值是类的对象,函数执行完成返回调用者时
Point getP()
{
Point A(1,2);
return A;
}
int main(void)
{
Point B = getP();
}
南来地,北往的,上班的,下岗的,走过路过不要错过!
======================个性签名=====================
之前认为Apple 的iOS 设计的要比 Android 稳定,我错了吗?
下载的许多客户端程序/游戏程序,经常会Crash,是程序写的不好(内存泄漏?刚启动也会吗?)还是iOS本身的不稳定!!!
如果在Android手机中可以简单联接到ddms,就可以查看系统log,很容易看到程序为什么出错,在iPhone中如何得知呢?试试Organizer吧,分析一下Device logs,也许有用.