* 和 & 是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 =  &plus;
    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 ;
}

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐