* 和 & 是c++中非常常见的一对符号。但实际使用中有极其多的变化需要注意。本文就从两个符号的作用入手,逐渐深入,将函数指针,左值右值一起一起说清楚
先说* ,一般来说都把* 当成取值的符号,但是这在实际的应用中很容易出现歧义
例如
int a = 20;
int *b = &a;
std::cout << *b << std::endl;
从第二行来看,我们把a的地址赋给了*b,但是明显第三行打印出来的*b并不是a的地址,而是a的值。这明显是有歧义的。
所以严格地说,符号*其实有2个作用:
1. 当* 用于声明变量或者函数的时候,意为声明该变量或者函数为一个指针
2. 其他场景* 意为对变量取值
第一个怎么理解呢?其实*本质上还是一个运算符号,就像加减乘除、++、--等。运算符号必然要有运算的目标,例如我们用++ 这个符号的时候一定要有一个变量,而不能只写一个++
int a = 1;
a ++; 合法
++; 不合法
而* 出现在定义变量中的时候,*运算符的运算目标就是变量类型,所以
int *a 的本质其实是 (int*) a
而
int a;
int* a;
是两个完全不同的定义方法,只是书写比较相似而已
当* 当做定义指针的运算符的时候,其作用目标可以是* 的左边,也可以是* 的右边,就像我们可以写 a++, 也可以写 ++a。当没有括号的时候,* 默认作用于左边的目标。
例如
Class Dog{
Dog *born();
}
Dog * Dog::born(){
Dog dog_baby{}
return &dog_baby
}
上面的代码中,born这个成员函数中,* 的左边是变量类型,右边是函数名称。* 运算符优先作用于左边的变量类型Dog上,而不是作用于右边的函数名born上。因此born是一个返回 Dog类型指针的函数。
当然,如果有括号的话,就另当别论了。例如
int plus(int a,int b);
int (* plus2)(int a, int b);
int main() {
plus2 = +
std::cout << (*plus2)(2,3) << std::endl;
std::cout << plus2(2,3) << std::endl;
}
int plus(int a, int b){
return a + b;
}
在这段代码中,括号把plus2和* 括起来,代表plus2 是一个函数指针。所以&plus可以传给plus2。 应该注意到的是在main函数中,用*plus2和plus2 都可以正确的输出结果。这是为什么呢?对于函数来说,函数名就是函数的地址。这2这是等价的,而*plus2只是一个易于理解的写法而已。
而接下来就很容易想到一个问题,那么是不是可以直接把plus赋给plus2呢?
int plus(int a,int b);
int (* plus2)(int a, int b);
int main() {
plus2 = plus; // 这一行做了修改
std::cout << (*plus2)(2,3) << std::endl;
std::cout << plus2(2,3) << std::endl;
}
int plus(int a, int b){
return a + b;
}
事实证明,这样写确实是没有问题的。
最后总结一下:
1. 当 * 用作定义变量,表示定义的变量是指针类型。其他场景表示取指针的值。
2. 函数名就是指针
再来说说&:
& 有3个作用
1. 位与,这个是最简单的用法
2. 当&符号和变量一起使用是,表示取内存地址
2. 当 & 用在定义变量的时候表示引用变量,一个简单地例子用法:
int main() {
int a = 10;
int & b = a;
std::cout << a << std::endl;
std::cout << b << std::endl;
a ++;
std::cout << a << std::endl;
std::cout << b << std::endl;
return 0;
}
这里需要明确引用的使用和引用的本质,引用可以理解成对内存地址的起的另外一个名字。我们都知道变量名在编译的时候是不保存的,而是变成一个内存地址,以后每次用到变量名都会用内存地址中的数据来代替,而引用就是给这个内存地址起了另外一个名字。
了解到这个本质以后我们就能理解引用的很多性质,比如
1 引用为什么不能不初始化,例如
int & b; // error
因为我们每次创建一个变量例如 int a会开辟一个内存空间并且在编译时用内存地址代替变量名a
而引用是不会开辟新的内存空间的,int &b只能把现有的内存空间增加一个名字 b。所以如果我们不初始化,就不知道b这个名字是给哪个内存空间起的。
2 引用不能修改,因为引用就是给内存空间起的名字,一旦起好了,就终身绑定。
3 引用指向的最终开辟的内存地址,例如
int main() {
int i = 10;
int *a = &i;
int & b = *a;
std::cout << *a << std::endl;
std::cout << b << std::endl;
// 输出 10 10
i ++;
std::cout << *a << std::endl;
std::cout << b << std::endl;
// 输出 11 11
int c = 20;
a = &c;
std::cout << *a << std::endl;
std::cout << b << std::endl;
// 输出20 11
return 0;
}
引用变量虽然是通过指针a赋值的,但本质上引用变量表示的是i的那个内存地址。所以引用变量是和i绑定的,而不是a。当然这个写法极度的不提倡,大家千万不要模仿。
以上是引用的理解,但实际上引用的实现和指针是一样的,大家可以看这篇文章的解释 https://www.zhihu.com/question/37608201/answer/1601079930
左值与右值
引用这里有一个非常复杂的用法:左值右值。这里给出一个比较容易的理解:左值表示在内存中有地址的值,右值则代表没有地址的值。
例如int i= 10 中。执行时,会在内存中开辟一个空间叫i ,而10 则是cpu在产生的值,10这个数字在内存中根本不会存储。因此i是左值, 10 是右值
再比如:
int a = i * 10;
上面a同样开辟了一个内存空间,是左值。这里应该注意的是,i* 10 虽然会读取内存中的地址,但i * 10仍旧是cpu读取内存后计算得到的一个数字,这个过程会在cpu寄存器中短暂存储,但是不会有内存存储,所以 i * 10 也是右值。
所以是不是右值,要看这个值被从cpu里面出来的时候有没有内存地址,而不是进cpu之前有没有地址。这里有一个非常容易混淆的地方,函数的返回值是左值还是右值?函数的返回值应该属于右值,虽然在函数中,返回值可能有一个内存地址,但是这个内存地址最终会被cpu读取,然后以数值的形式返回给主程序中去。也就是说正常情况下,主程序只能拿到函数的返回结果。所以函数返回值应该是右值。
左值引用
理解了左值右值以后,就能明白左值引用和右值引用了。
左值引用就是上面讲的最常规的引用,给左值一个引用就叫左值引用。例如
int main() {
int a = 10;
int & b = a;
std::cout << a << std::endl;
std::cout << b << std::endl;
a ++;
std::cout << a << std::endl;
std::cout << b << std::endl;
return 0;
}
在上面的例子中,我们已经讲了,int & b = a 就是把a的内存空间又起了一个名字叫b。所以这个式子成立的前提下就是等号的右边,必须有自己的内存空间,如果没有,则必然报错,而右值是没有自己的内存空间的,所以左值引用的等号右边,必须是左值。
例如 int & b = a * 10。上面讲过了 a *10 是cpu读取a然后计算得到的,不会开辟内存空间,所以没办法重新起名。
所以右值没有开辟内存空间,不能进行左值引用,这个是非常好理解的。
不过这里有一个例外情况: const变量的左值引用等号右边可以是常量左值,左值,常量右值,右值,例如
const int &a2 = i * 6; // 正确,可以将一个const引用绑定到一个右值
这是因为全局const是一个常量,存储在文字常量区。当然,也可以把一个左值赋给常量左值,即
const int &a2 = i; // 正确,可以将一个const引用绑定到一个左值
右值引用,顾名思义,就是给右值一个引用。
右值引用的写法是:
int num = 10;
int && a = 10;
int && a = num; //右值引用不能初始化为左值
应该注意的是,右值引用并不能初始化为左值。而且右值引用后的值,本身是左值,例如上面的参数a,是一个左值。
看着右值引用和直接赋值的作用是一样的,例如
int && a = 10;
int a = 10;
那右值引用有什么用呢?
引用变量作为入参
引用变量是可以作为作为函数的入参的,当引用变量作为入参的时候,作用于指针入参类似,可以在被调用函数中修改调用函数的变量值。
int previous_number(int a);
int previous_number2(int *a);
int previous_number3(int &a);
int main() {
int i = 10;
int *a = &i;
int b = previous_number(i);
std::cout << i << std::endl;
std::cout << *a << std::endl;
std::cout << b << std::endl;
b = previous_number2(a);
std::cout << i << std::endl;
std::cout << *a << std::endl;
std::cout << b << std::endl;
b = previous_number3(i);
std::cout << i << std::endl;
std::cout << *a << std::endl;
std::cout << b << std::endl;
return 0;
}
int previous_number(int a){
return --a;
}
int previous_number2(int *a){
return --(*a);
}
int previous_number3(int &a){
return --a ;
}