在上 C++ 课的时候老师会要求一般类对象作为参数传递时要把形参写成 const 引用的形式,也就是这样的
void fun(const string &a,const string &b)
{
}
那么写成这样有什么好处?如果不这么写会怎样?
我们先一个个来,讲一下引用的作用
引用,其实就是给绑定的对象起了个别名,之后对这个别名的所有操作都可以看出对绑定对象的直接操作,那么当引用作为形参,函数调用时也可以看成将传递的实参绑定给它,这样我们在函数体内对这个引用做的一切操作都有可能影响到函数传递的实参,所以在这里我们就无意间引出了 const 的第一个作用,即我们希望参数在函数体内是只读的,所以当我们加了引用有希望参数是只读的就必须加 const ,那有人可能会问了,别加引用不就行了,值传递就不会影响到实参了。确实是这样,对于 C++ 内置的数据类型来说我们完全可以通过值传递或者引用来控制是否要更改原先的值,但是当参数是类对象时值传递就有了一个问题,那就是性能可能会大受影响。我们知道值传递实际就是向函数拷贝一份副本来使用,那么对于一些复杂的类,尤其是 string 这样每一次拷贝可能消耗很多的时间,那么通过引用传参就很有必要了,所以这样写的一个基本逻辑就出来了,即因为我想提高类对象传参时的性能,所以要用引用,因为用了引用我又希望它只读所以我用了const。
接下来是重点讲讲const
我们知道 const 修饰的对象就是告诉编译器这个对象是只读的,是不能改的,正是因为有这样的性质所以 const 修饰的对象必须初始化,否则这个对象没有任何意义。那么什么样的类型可以给 const 修饰的对象赋值呢?答案是不管是不是常量,都可以给它赋值
const int a=1;
int b;
const int c=b;
const int d=c;
const string str="234";
string str1="234";
const string str2=str1;
这些都是正确的。
那么如果我们将 const 和引用或者指针放在一起呢?在这里我可能有必要解释下常量指针和指针常量的问题,也就是以下这两种形式表达的意思是不一样的
int *q;
const int *a;
int *const b=q;
为什么我要在这里加个 q ,因为不让 b 初始化指向一个地址这个程序就不能编译了。所以 a 和 b 的区别是一个是指向常量的指针,一个是指针本身是常量,也就是说我们无法通过 a 更改它所指向的值,也不能让 b 换一个对象去指。这两种常量还有另外的名字,叫底层常量和顶层常量,为什么要引出这样的概念,因为C++中几乎所有的赋值都是会忽略等号两边对象的顶层常量,但是底层常量则有一些规则来限制它,这样就不难理解为什么上面的程序中身为非常量的 q 可以赋给常量的 b ,而他们反过来也是合法的,因为 b 中的const实际是修饰顶层常量的,所以在赋值时 int *const b
和int *b
没有任何区别!为了试验这个结论我们可以将赋值双方反过来跟底层常量对比一下来试试效果。
int a;
int *const b=&a;
int *c=b;//以上都合法,因为顶层常量可以直接忽略
int d;
const int *e=&d;
int *f=e;//这一句报错,因为底层常量不能忽略,所以底层常量只能赋值给同样是常量的对象
那么引用呢,其实跟指针是一个道理的,因为引用本身就是不能解绑再去换一个变量绑定的,所以对于引用来说,只有底层常量是有意义的,所以和指针一样,底层常量无法赋值给非常量。
int b=1;
const int &a=b;
int &c=a;//报错,理由和指针一样
所以有了这些结论我们再回头看传参时为什么要const string&
这样传,除去说的第一个理由还有一个很重要的原因是,常量引用必然是底层常量,而底层常量可以接受非常量的变量,但反过来却不行!通俗的来讲,如果我们没有加 const,意味着我们传参是无法传常量的。
void fun(string &a)
{
}
int main()
{
fun("123");//这里报错
}
所以加上 const 一个极为重要的原因是希望在调用这个函数时我既可以传常量也可以传非常量。
另外关于顶层常量在赋值时会被忽略,还可以用以下例子做实验
void fun1(const int a)
{
}
void fun1(int a)
{
}
int main()
{
}
这个是不能编译的,因为 const int a和 int a 被编译器看作是同一种类型了,所以函数重复定义了。