跳到主要内容

第二章 变量和基本类型

阅读提示

  • 本文中的代码分为三类:
    • OK 可编译:可直接作为单文件(或放入 main 函数体)编译通过。
    • 片段:只展示语法要点,非完整程序;如需编译,请放入函数体或补齐所需声明/头文件。
    • NG 错误示例:为阐释语义而故意写出的编译错误,请勿整块复制编译。

引用

片段

int val = 1024;
int &refVal = val;
const int &refVal5 = val + 1;

double dval = 3.14;
const int &refVal7 = dval;

NG 错误示例(不可编译,演示语义):

// NG 错误示例(不可编译)
int val = 1024;
double dval = 3.14;

int &refVal2 = 10; // 错:引用的初始值必须是对象(左值)
int &refVal3; // 错:引用必须初始化
int &refVal4 = val + 1; // 错:非常量左值引用不能绑定到右值
int &refVal6 = dval; // 错:非常量左值引用不能绑定到临时(由 double 转 int 产生)

指针

  • 指针的值应该属于下列4种状态之一:

    • 指向一个对象
    • 指向紧邻对象所占空间的下一个位置
    • 空指针,意味着指针没有指向任何对象
    • 无效指针,也就是上述情况之外的其他值
  • 注意:对 one-past-the-end 指针(指向“尾后”位置)只能做比较或指针算术(如减法),不可解引用。

  • 给指针赋值,就是令它存放一个对象的地址,或者令它为空。

  • 通过*的个数可以区分指针的级别。也就是说,**表示指向指针的指针,***表示指向指针的指针的指针。

*&运算符

片段

int i = 42;

int &r = i; // &紧随类型名出现,是声明的一部分,r是一个引用。
int *p; // *紧随类型名出现,是声明的一部分,p是一个指针。
p = &i; // &出现在表达式中,是取地址运算符。
*p = i; // *出现在表达式中,是解引用运算符,用于访问/修改p所指向的对象;这里将i的值写入p所指向的对象。
int &r2 = *p; // &是声明的一部分,r2是一个引用。*是解引用运算符,r2被绑定到p所指向的对象i上。

怎么读懂复杂的指针声明

面对复杂的指针或引用声明时,可以使用"从右向左阅读"的方法来理解变量的类型。

基本规则

核心原则:从变量名开始,按照"离变量名最近的符号优先"的原则来理解类型。

阅读顺序

  1. 找到变量名
  2. 看变量名右边的符号(如果有的话)
  3. 看变量名左边最近的符号
  4. 继续向左,直到基本类型

示例1:指向指针的引用

int *p = nullptr;   // 已有一个指向 int 的指针变量
int *&r = p; // r 是“指向 int 指针”的引用,必须绑定到一个现存的指针对象

分析步骤

  1. 找到变量名:r
  2. 变量名右边没有符号
  3. 变量名左边最近的符号是 &,所以 r 是一个引用
  4. 继续向左看到 *,说明 r 引用的是一个指针
  5. 最左边是基本类型 int,说明这个指针指向的是 int 类型

结论r 是一个指向 int 指针的引用(reference to a pointer to int)

示例2:指向指针的指针

int **p;

分析步骤

  1. 找到变量名:p
  2. 变量名左边第一个 *,所以 p 是一个指针
  3. 继续向左第二个 *,说明 p 指向的也是一个指针
  4. 基本类型是 int

结论p 是一个指向 int 指针的指针(pointer to a pointer to int)

示例3:指针数组

int *arr[10];

分析步骤

  1. 找到变量名:arr
  2. 变量名右边是 [10],说明 arr 是一个数组,有10个元素
  3. 向左看到 *,说明数组的每个元素是指针
  4. 基本类型是 int

结论arr 是一个包含10个 int 指针的数组(array of 10 pointers to int)

示例4:指向数组的指针

int (*p)[10];

分析步骤

  1. 找到变量名:p
  2. p 被括号包围,优先处理括号内的内容
  3. 括号内,p 左边是 *,所以 p 是一个指针
  4. 括号右边是 [10],说明 p 指向的是一个数组,有10个元素
  5. 基本类型是 int

结论p 是一个指向包含10个 int 元素的数组的指针(pointer to an array of 10 ints)

注意:括号 () 改变了运算优先级,[]() 的优先级高于 *

示例5:返回指针的函数指针

int *(*func)(double);

分析步骤

  1. 找到变量名:func
  2. func 被括号包围,优先处理括号内的内容
  3. 括号内,func 左边是 *,所以 func 是一个指针
  4. 括号外右边是 (double),说明 func 指向的是一个函数,该函数接受一个 double 参数
  5. 最左边是 int *,说明这个函数返回一个指向 int 的指针

结论func 是一个函数指针,该函数接受 double 参数并返回指向 int 的指针(pointer to a function that takes a double and returns a pointer to int)

快速记忆口诀

从变量名出发,右左右左读,括号改优先,最后看类型

  • :先看右边,处理 [](数组)或 ()(函数)
  • :再看左边,处理 *(指针)或 &(引用)
  • 括号:括号改变优先级,优先处理括号内的内容
  • 类型:最后得到基本数据类型

指向指针的引用

指针本身是一个对象,所以可以被引用。 下面来做一个指针,和指针的引用的例子。

  • 指针的引用

片段

int i = 1024;
int newI = 2048;

// p是一个int指针,指向i
int *p = &i;
// r是一个对int指针的引用,它是p的别名
int *&r = p;

// 修改r就是修改p。这里让p指向newI
r = &newI;

// 因为p已经指向newI,所以解引用p得到的是newI的值
std::cout << *p << std::endl; // 输出 2048
  • 单纯的指针

片段

int i = 1024;
int newI = 2048;

// p是一个int指针,指向i
int *p = &i;
// r是另一个int指针,它的初始值是p的值(即i的地址)
int *r = p; // 这里r和上面的例子不一样,它不是引用,而是指针。

// 修改r,让r指向newI。这个操作不影响p
r = &newI;

// p仍然指向i
std::cout << *p << std::endl; // 输出 1024