理解C语言指针

数据、指令和内存

  • 程序的数据和指令存放在同一内存空间
  • 虚拟存储空间的两个关键要素:内存地址+类型
  • 指针是对内存区域的抽象:指针变量存放目标对象的内存地址

    定义和使用指针

    指针的定义

  • 使用* 标记指针,定义方式与定义变量一样
  • 在同一个变量定义语句中,基本数据类型只能有一个,但是可以有多个形式相同或不同的声明符。这也就是说,同一个语句可以定义出不同类型的变量。

    1
    2
    int *ip1, *ip2;         // ip1 和 ip2 都是指向 int 类型变量的指针变量
    double d, *dp; // d 是 double 类型变量,dp 是指向 double 类型变量的指针变量
  • 可以定义一个指向这一指针的指针

    1
    2
    3
    int val = 1024
    int *p = &val
    int **pp = &p

pp 是一个指向指向 int 类型变量的指针的指针。

获取对象的地址

  • 用&取地址符号来获取对象地址
  • 指针的类型和对象的类型需要严格匹配
    1
    2
    int val = 42;
    int *p = &val; // &val 返回变量 val 的地址,记录在指向 int 类型变量的指针里

访问指针指向的对象

  • 通过解引用指针 p 来访问变量 val
    1
    2
    3
    int val = 1004
    int *p = &val
    cout << *p << endl

空指针和空类型的指针

  • 空指针是不指向任何对象的指针,字面值是 NULL,它定义在 stdlib 当中
  • 空类型的指针,指的是形如 void *pv 的指针。这是一类特殊的指针;这里的空类型,不是说没有类型,而是说空类型的指针,可以用于存储任意类型对象的地址。
  • 由于空类型的指针可以接受任意类型对象的地址,所以,当编译器拿到一个空类型的指针的时候,它无法知道应该按照何种方式解释和使用指针中记录地址中的内容。因此,空类型指针能够做的事情非常有限:做指针之间的比较、作为函数的输入或输出、赋值给另外一个空类型指针。
1
2
3
4
5
6
7
int *p1 = NULL;         // C 风格的空指针初始化
int *p2 = nullptr; // C++ 风格的空指针初始化
int *p3 = 0; // 使用字面值常量 0 初始化空指针

if (nullptr == p1) { // 思考一下为什么不是 p1 == nullptr
; // do something
}
1
2
3
4
5
6
7
double pi = 3.14;
void *pv = &pi; // 使用 void * 存放了一个 double 类型对象的地址
double *pd = &pi;
pd = pv; // 错误:不能将空类型的指针赋值给其他类型的指针
pv = pd; // 正确:空类型的指针可以接受任意类型的指针赋值
pd = (double *)pv; // 正确:C 风格的强制类型转换
pd = reinterpret_cast<double *>(pv); // 正确:C++ 风格的强制类型转换

const与指针

  • 定义常量,只需要在基本类型前,加上 const 关键字即可
  • 常量的值在生存期内不允许改变
  • const与指针连用有4种情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int val = 0;                // int 型变量
const int cnst = 1; // int 型常量

int *pi = &val; // pi 本身是变量,通过 pi 访问的也是变量
// 正确:将变量地址赋值给变量的指针
pi = &cnst; // 错误:不允许将常量的地址赋值给变量的指针

const int *pci = &cnst; // pci 本身是变量,通过 pci 访问的是常量 (point to const)
// 正确:将常量地址赋值给常量的指针
pci = &val; // 正确:允许将变量地址赋值给常量的指针

int *const cpi = &val; // cpi 本身是常量,通过 cpi 访问的是变量
// 正确:允许将变量地址赋值给变量的指针
int fake = 2; // int 型变量
cpi = &fake; // 错误:cpi 本身是常量,不能在定义之外赋值

const int *const cpci = &val;
// cpci 本身是常量,通过 cpci 访问的也是常量
// 正确:允许将变量地址赋值给常量的指针
cpci = &fake; // 错误:cpci 本身是常量,不能在定义之外赋值
cpci = &cnst; // 错误:cpci 本身是常量,不能在定义之外赋值,哪怕是常量的地址
  • 变量可以是常量,而指针本身也可以是常量。因此在变量和指针两个维度,都可以选择是否为常量
  • 为了区分这两个维度,我们引入顶层 const 和底层 const 的概念:

    顶层 const:指针本身是常量。此时,指针在定义初始化之外,不能被赋值修改。称指针为指针常量。
    层 const:指针指向的变量是常量。此时,不能通过解引用指针的方式,修改变量的值。称指针为常量的指针。

指针与数组

1
2
3
4
5
6
7
8
9
int nums[] = {1, 2, 3};
int *p = &(nums[0]);
if (p == nums) {
printf("true!\n");
}
size_t i = 0;
for (i = 0; i != 3; ++i) {
printf("%d\n", p[i]);
}
  • 数组指针可自增,可加减运算

    1
    2
    3
    4
    5
    6
    int nums[] = {0,1,2,3,4,5};
    size_t len = sizeof(nums) / sizeof(nums[0]);
    int *iter, end = nums[len]; // end 是尾后指针
    for (iter = nums; iter != end; ++iter) {
    printf("%d\n", *iter);
    }
  • 两个指针如果指向同一个数组中的元素,那么它们可以做差

  • 数组指针与整数的加减,实际是将指针沿着数组进行移动,得到的结果还是一个指针。既然结果是指针,那么就可以解引用,访问数组中的元素

函数与指针

让函数返回一个数组的指针

  • 函数在返回时会对返回值拷贝
  • 数组不能被拷贝,函数无法直接返回数组,但可返回数组的指针
  • 如何定义一个返回数组指针的函数:element_type

(*func(param_list))[dimension]

1
2
3
int arr[10]
int *parr[10] // parr 是一个数组,长度是 10,元素类型是 int *,也就是数组中存的是指针
int (*p)[10]=&arr
1
2
3
4
int (*func(param_list))[10];
// 正确:func 是一个函数,param_list 是它的参数
// 它返回的是一个指针
// 这个指针指向了一个长度为 10 元素类型是 int 型的数组

函数的指针

  • 一个函数的类型,取决于它的输入和输出。这也就是说,一个函数的类型,应当包含它的返回值类型和参数列表
  • 定义一个指向某类型的函数指针

    1
    2
    3
    bool isEqual(int, int);
    bool (*pfunc)(int, int) = &isEqual; // 定义了一个函数指针,指向 isEqual
    bool (*pfunc)(int, int) = isEqual; // 一个等价定义
  • pfunc 就是一个函数指针,它指向一个 bool (int, int) 类型的函数。也就是说,这类函数接收两个 int 型的参数,并返回一个 bool 类型的值。

  • 当函数名字作为值使用时,它会自动地转换成指针(有点像数组名字,不是吗)。因此,在函数指针的初始化或者复制的过程中,取值运算符是可选的。于是,上述两个定义语句是等价的。另一方面,函数指针作为函数调用使用时,它会自动转换成函数名(有点像数组指针,不是吗)
1
2
3
4
5
6
bool isEqual(int, int);
bool (*pfunc)(int, int) = isEqual;

bool res1 = isEqual(1, 2); // 通过原函数名调用
bool res2 = (*pfunc)(1, 2); // 一个等价调用:通过函数指针,解引用调用
bool res3 = pfunc(1, 2); // 另一个等价调用:函数指针自动转换成函数名

将函数指针作为参数传入另一个函数

  • 函数不能拷贝,但函数指针可以
    1
    2
    3
    4
    5
    6
    void addIfEqual(int lhs, int rhs, bool pfunc(int, int));
    // addIfEqual 的第三个参数是一个函数定义
    // 它会自动地转换成一个函数指针的参数
    void addIfEqual(int lhs, int rhs, bool (*pfunc)(int, int));
    // 一个等价定义:显式地注明第三个参数是函数指针
    >>addIfEqual(1, 1, isEqual);

让函数返回一个函数的指针

  • outer_return_type (*func(param_list))(outer_param_list)
  • func(param_list) 是当前需要定义的函数;outer_return_type 和 outer_param_list 分别是当前定义的函数返回的函数指针对应函数的返回值类型和参数列表。

其他

  • ((void()())0)():访问内存地址 0,将它作为一个参数列表和返回类型均为空的函数,并执行函数调用
  • void(*pfunc)(); 定义了一个函数指针 pfunc,它指向的函数参数列表为空、返回值类型也为空
  • 类型强制转换符:
    1
    2
    (double) c;     // 将变量 c 强制转换为 double 类型
    (double *) d; // 将变量 d 强制转换为 double * 类型
Thanks!