指针与引用

指针与引用

指针类型,指针变量,指针。有时候我们把三个都叫做指针于是乎如果对于一个不是很懂指针的人来说,当说到指针时头就很大,因为他很难区分此时说的指针到底是以上那个东西。其实这三个的存在是有顺序的:先有类型,然后才有变量,有了变量然后我们可以往里面存东西。变量的值要存储在内存中,那我们如何取这个值呢?第一种:用变量名;第二种:用内存单元的地址(编号)。

基本概念

  • 类型
    类型决定了数据的意义(应该以何种方式来解读它),以及可以在数据上执行的操作。类型也说明了以这种类型为蓝图创建的对象所占的存储空间的大小。
    指针与引用

  • 地址
    内存单元的编号,可以看作是门牌号。每8bit称作一个byte,内存单元以字节为单位进行编号。
    指针与引用

  • 变量(对象)
    变量可以看作是一段连续内存空间的别名。把内存的每一个byte当作是一个一个的小的储物箱,有时若是我们的东西太大了以至于一个储物箱放不下,于是我们就需要使用多个储物箱(一般都是连续的)来存放我们的东西,那么我们这个由几个小储物箱组成的大储物箱的编号应该是什么呢?
    答案是:addr=min{box1,box2,box3....}addr = min\lbrace box1, box2, box3....\rbrace就是所用的所有存储物箱的地址(编号)集合中最小的那一个。
    即采用多字节存储的对象,它的地址是他所用地址集合中最小的那个地址。下图中:变量a跨越了地址为:0xFF00-0xFF03的4个存储存储单元。那变量a的地址其实是:0xFF00
    指针与引用
    我们可以将变量理解为在储物箱上贴了一个标号,方便我们自己使用。比如说,占用了8byte的编号(地址)为:0xFFEE01的储物箱,我们将其称为——Mike。这样显然方便了我们人类使用,但是对于计算机而言,它还是使用编号去寻找我们的储物箱,因为这样更不容易出错也没有歧义。
    上面我们说了跨域多个连续字节的变量的地址问题,那么还有一个问题就是它的值该如何放进去呢?
    指针与引用
    测试你的硬件所支持的的大端(MSB)还是小端(LSB),一般而言intel桌面级处理器所使用的是LSB(下面给出了测试代码)。对于一台机器采用是MSB还是LSB在两个进行通信的时候一定要协商好,否则通信格式不一致出现意外。

//也可采用union来测试
#include <iostream>
using namespace std;

int main()
{
	//测试得到sizeof(int) = 4
	//标准并未规定int占用多大的字节,只规定了它的最小尺寸
    int x = 0x12345678;
    char *res = (char*)&x;
    if(*res == 0x78)
        cout << "LSB" << endl;
    else
        cout << "MSB" << endl;
   
    return 0;
}
  • 值(内容)
    值或者内容,就是我们存储在内存单元中(储物箱)东西。

有了前面的铺垫,下面我们来开始正式介绍指针。

指针

  • 指针类型
    指针类型是指向(point to)另外一种类型的复合类型。
  • 指针变量
    指针变量是用来存放变量地址的变量。通过指针我们可以间接的访问对象。

1:对于指针而言非常重要的一点就是:一定要分清楚指针本身和它所指向对象的区别
2:一定要熟悉内存,如果不懂内存,乱用指针访问,要么会导致程序崩溃要么就是内存泄漏

例1——指针的用法

//指针的用法
{
    int x = 10;
    int *p = &x;//p存放变量x的地址,或者说p指向x
}
//使用指针间接访问对象:
{
   
    int ival = 88;
    int *p = &ival;
    cout << *p;//由*得到指针p所指的对象,即ival

    *p = 66;//此时ival也是66.
}

指针与引用
例2——空指针、泛型指针和2级指针

//空指针——即不指向任何对象
{
    //二者等价
    int *p1 = nullptr;
    int *p2 = 0;
}

//void* 指针——来者不拒(泛型指针)
{
    //它可以接收任何类型的指针
    int a = 1;
    double b = 3.166;
    void *obj = &a;
    obj = &b;
}

//指针的指针(2级指针,根据*的个数判断)
{
    int ival = 77;
    int *p1 = &ival;//p1指向一个int类型的变量
    int **p2 = &p1;//p2指向一个int* (int类型指针)
}

指针与引用

引用

引用(reference)就是为一个对象起一个别名。引用类型引用(refers to)另外一种类型。(这里介绍的都是左值引用,右值引用会在后续介绍)
引用的用法

int ival = 10;
int &ref = ival;//ref为ival的别名

指针与引用

引用就是为一个已经存在的对象起一个另外的名字,引用必须和对象一直绑定在一起,无法令引用重新绑定到另外一个对象身上,因此引用必须初始化
引用本身不是对象,所以不能定义引用的引用(有例外,引用折叠规则)。
引用只能绑定到一个对象上,不可以绑定到某个字面值或者表达式的计算结果上。
在C++语言中,引用在底层就是通过指针来实现的。

const的引用

int &r = 10;//error,r是不是对常量的引用。所以r不可能绑定到字面量10,
//因为字面量是read-only的,如果可以绑定,那么r岂不是可以修改它了!!!
const int &r2 = 10;//OK
r2 = 30;//error, r2是对常量的引用,不可修改
const int &r3 = r2 * 10;//r3绑定到表达式的计算结果上

看了上面的代码这到底发生了什么有趣的事情呢?不是说引用不可以绑定到字面值和表达式计算结果上面吗?
我们来看一个小的案例:

double dval = 3.14;
const int &r = dval;
//由于引用的而类型与要绑定的类型不一致,编译器版我们做了如下的工作:
const int tmp = dval
const int &r = tmp;
//即编译器将r绑定了一个所谓的临时对象上面。
//所以说上面的例子其实就是编译器让我们的引用绑定了一个临时对象身上。
const int &ref = 10;//做了以下两个动作
//1.生成临时对象
const int tmp = 10;
//2.绑定到临时对象身上
const int &ref = tmp;

简化接口

例1

#include <iostream>
using namespace std;

void swap(int &a, int &b)
{
     int tmp = a;
     a = b;
     b = tmp;
}

void swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main()
{
	int x = 1;
	int y = 2;
	swap(&x, &y);//指针版
	swap(x, y);  //引用版
	//很明显,引用版的接口更加简洁,隐藏了形参细节,便于用户使用。
    return 0;
}

例2

//如果不需要改变传入对象的值,那么使用常量引用,可以使得接口更加的强大,
//接收多种类型对象。
#include <iostream>
using namespace std;
//普通左值引用版
void fun1(int &r)
{
    cout << "fun1:r = " << r << endl;
}
//常量引用版
void fun2(const int &r)
{
    cout << "fun2:r = " << r << endl;
}
//右值引用版
void fun3(int &&r)
{   
    cout << "fun3" << endl;
}

int test()
{
    return 555;
}

int main()
{    
   int x = 888; 
   fun1(x);//OK
   fun2(x);//OK
   
   const int y = 777;
   //fun1(y);//error
   fun2(y);
   
   //fun1(666);//error,普通左值引用无法绑定到字面量上面
   fun2(666);
   
   //fun1(test());//error, 普通左值引用无法绑定到临时对象上面
   fun2(test());//OK
   fun3(test());//OK, 右值引用可绑定到临时对象上面
   
   return 0;
}

参考文献

[1]《C++ primer》
[2]《计算机科学导论》

Email:[email protected]