在C++中,函数形参一般带“&”和“*”,例如:int*、char*、int&等。这是为什么呢?此处以常用的string字符串数据类型来引入今天要讲的话题;
在C语言中是没有字符串这个数据类型的,C++在C语言的基础上升级了string类。如果很多从CSharp、Java等语言转C++的开发人员在用字符串做函数参数的时候都会很自然的用string类型,但是作为正宗的C++开发者都会建议你不要用string类型作为函数参数。
CSharp的数据类型
CSharp里面把数据类型分为两大类,值类型和引用类型;值类型包括基本数据类型(int ,double等),结构和枚举;引用类型包括接口,数组,Object类型,类,委托,字符串,null类型等;一般来说,结构体类型的数据就是值类型,而类构造的数据就是引用类型;
引用类型和值类型都继承自System.Object类。不同的是,几乎所有的引用类型都直接从System.Object继承,而值类型则继承其子类,即直接继承System.ValueType。
请看下面这段代码:
public class People
{
public string Name { get; set; }
public int Age { get; set; }
public void reset(string name, int age)
{
name = "xiaoli";
age = 200;
}
public void reset(People p)
{
p.Name = "xiaoli";
p.Age = 200;
}
}
class Program
{
static void Main(string[] args)
{
string name = "xiaoming";
int age = 100;
People p = new People();
p.Name = name;
p.Age = age;
People p2 = new People();
p2.Name = "xiaohua";
p2.Age = 90;
Console.WriteLine(p.Name);
Console.WriteLine(p.Age);
p.reset(name,age);
Console.WriteLine(name);
Console.WriteLine(age);
p.reset(p);
Console.WriteLine(p.Name);
Console.WriteLine(p.Age);
Console.Read();
}
运行结果为:
xiaoming
100
xiaoming
100
xiaoli
200
int类型为值类型,形参是不能改变实参的值的,但是People是引用类型,形参可以改变实参的值。引用类型指向的其实是一个内存地址,string 虽然是引用类型 不过是不可变的。
但是CSharp提供ref关键字来使得值类型的实参在传递时也可以被修改
public class People
{
public string Name { get; set; }
public int Age { get; set; }
public void reset(ref string name, ref int age)
{
name = "xiaoli";
age = 200;
}
public void reset(ref People p)
{
p.Name = "xiaoli";
p.Age = 200;
}
}
class Program
{
static void Main(string[] args)
{
string name = "xiaoming";
int age = 100;
People p = new People();
p.Name = name;
p.Age = age;
People p2 = new People();
p2.Name = "xiaohua";
p2.Age = 90;
Console.WriteLine(p.Name);
Console.WriteLine(p.Age);
p.reset(ref name,ref age);
Console.WriteLine(name);
Console.WriteLine(age);
p.reset(ref p);
Console.WriteLine(p.Name);
Console.WriteLine(p.Age);
Console.Read();
}
}
为何C++函数参数都带*或者&
前面通过CSharp讲到数据分值类型和引用类型,在C++中,引用类型是要显示定义的。
int n;
int& r = n;
如果在函数参数传递时,形参不声明为引用类型,参数传递方式是传值的。传引用的方式要求函数的形参是引用。
那究竟形参带&与不带有什么区别呢?
void func(int& i){i = 110 ;}
void func2(int i){i = 110 ;}
void main()
{
int n = 200 ;
int n2 = 300 ;
func(n);
func2(n2);
cout<<n<<endl;
cout<<n2<<endl;
system("pause");
}
首先看运行结果:
110
300
再看汇编的区别:
func(n);
00C4443C lea eax,[n]
00C4443F push eax
00C44440 call func (0C41271h)
00C44445 add esp,4
func2(n2);
00C44448 mov eax,dword ptr [n2]
00C4444B push eax
00C4444C call func2 (0C4143Dh)
00C44451 add esp,4
先说一下几个指令:
lea:load effective address, 加载有效地址,可以将有效地址传送到指定的的寄存器;
mov:在CPU内或CPU和存储器之间传送字或字节
push:入栈
从上面运行结果和汇编代码来看,当采用引用类型作为形参时,它将变为实参列表中相应变量的别名,对形参进行的任何更改都将真正更改正在调用它的函数中的变量。引用变量本身并不需要分配内存,而是直接对被引用对象取的别名;
引用变量示意
当采用值传递方式时:当函数被调用时,在“栈”中就会分配出一块新的存储空间,用来存放形参和函数中定义的变量(局部变量)。实参的值会被复制到栈中存放对应形参的地方,所以形参的值才等于实参。函数执行过程中对形参的修改,其实只是修改了实参的一个拷贝,因此不会影响实参。
好处
从汇编代码可以看出,普通值传递的传参方式,是需要复制实参的,然后将实参的赋本传递给形参。而引用传参方式直接取实参的有效地址,所以在运行效率上引用传参速度更快更节省内存开销。实参对象所占内存越大,开销越大,对效率的影响也越大;
指针传递(地址调用)
还有一种方式也能避免普通值传参方式的大开销和低效率问题,那就是直接将对象的地址传递给形参。
void func(int& i){i = 110 ;}
void func2(int i){i = 110 ;}
void func3(int* i){*i = 110 ;}
void main()
{
int n = 200 ;
int n2 = 300 ;
int n3 = 400 ;
int* n4 = &n3 ;
func(n);
func2(n2);
func3(&n3);
func3(n4);
cout<<n<<endl;
cout<<n2<<endl;
cout<<n3<<endl;
system("pause");
}
查看汇编代码:
func(n);
00327009 lea eax,[n]
0032700C push eax
0032700D call func (0321271h)
00327012 add esp,4
func2(n2);
00327015 mov eax,dword ptr [n2]
00327018 push eax
00327019 call func2 (032143Dh)
0032701E add esp,4
func3(&n3);
00327021 lea eax,[n3]
00327024 push eax
00327025 call func3 (0321442h)
0032702A add esp,4
func3(n4);
0032702D mov eax,dword ptr [n4]
00327030 push eax
00327031 call func3 (0321442h)
00327036 add esp,4
从上述汇编代码可以看出func3(n4);本质上是值传递,它所传递的是一个地址值,不管其所指向的对象如何,其在函数调用时复制的只是一个四字节的地址而已,对内存开销和效率影响不大。而func3(&n3);却又是引用传参方式。
如何避免实参被修改
有没有什么办法既保证效率又保证实参的安全性?既然不想实参在函数调用过程中被修改,那么C++有一个关键字--const可以达到目的。
void func(const int& i){i = 110 ;}
void func3(const int* i){*i = 110 ;}