指针与指针变量是C或C 的一把利刃,但也是一把双刃剑。指针变量是变量之间关系的一种数据表达,在实现函数的副作用(指针变量,包括函数指针做函数参数)、构造链式存储(数据元素的存储位置可以不连续,元素之间通过指针建立联系)、获取动态内存时,不可或缺。但指针变量与函数、数组等混杂在一起,有一定的复杂性。

0 从“存储程序控制”概念谈起

话说第一台电子计算机Eniac创建于1945年。当时的程序并不存储于内存memory,也没有控制器。程序如何运行呢?其程序也不是现代意义上的程序,而是一份操作清单,提供一个如何连线和插接线路板的操作指南,来组合不同的硬件模块。这种方式可以称为硬连接或硬编程,自然,不够自动化。为此,堪称全才的美国人冯诺依曼提出了“存储程序控制”的概念,增加存储器和控制器,将数据和代码存储到存储器,由控制器依次(或按状态控制器跳转)从存储器中读取代码,翻译指令,产生控制信号来控制计算机的其它部件操作。

数据或代码是一块块地存在于内存中的,通常我们称其为一个段。而且代码和数据是分开存放的,即不储存于同于一个段中,而且各种数据也是分开存放在不同的段中的。部分数据(如全局数据)在程序加载时加载,程序从main函数开始执行,部分数据在需要时分配存储空间。

存储到内存中的数据或代码能够被随机访问(读或写),是因为内存是按字节编址的,内存就像一排长长的开关(有多长呢?如果是32位系统,则是2^32=0xFFFFFFFF),以8个为一组提供一个地址(值域就是0x00000000~0xFFFFFFFF)。代码和内存被编译成二进制码后,加载到内存时,如同需要噼噼啪啪地不同按开关。

c语言关于指针知识点(可以说基本上弄懂C)(1)

1 指针及指针变量内涵

程序和数据存储到内存后,其所在内存的位置(地址)由标识符,如变量、常量、函数名称来标识。由此,标识符有两重含义,地址和其本身的值,或称己址和己值。通常,显示使用其值,隐式使用其址。隐式使用某个地址的变量称为指针变量,指针变量存储一个指针(地址),这个指针可以是变量、常量或函数名称对应的地址(存储函数地址的变量称为函数指针或函数指针变量)。

看思维导图:

c语言关于指针知识点(可以说基本上弄懂C)(2)

指针的算术运算:

c语言关于指针知识点(可以说基本上弄懂C)(3)

指针的移动:

c语言关于指针知识点(可以说基本上弄懂C)(4)

STL的迭代器通常是容器的内部模板类,称为迭代器类,迭代器类内含一个指向容器元素的指针,重载上述的一些运算符,实现指针的移动,用于容器内元素的遍历。

2 指针变量与const

指针变量可以用const修饰,可以是修饰自身或指针变量指向的类型。

2.1 const修饰指针变量自身

int n = 0; int m = 0; int * const p = &n; p = &m; // error,p是const,不能再更新

2.2 const修饰指针变量指向的对象

int n = 0; const int * p = &n; // int const* p = &n; 是效果一致的写法 n = 5; *p = 2; // error,p指向一个常量,不能用p去更新

规则:const写在指针类型符号*右边表示修饰其自身,写在左边,表示修饰其指向的类型。

3 数组类型、声明与指向数组的指针变量的类型、声明

int arr[5]; // 标识符arr→往右看→是数组符号→arr是一个数组,有5个元素→往左看,数组元素的类型,类型是int。其类型可以理解为int[5],也就是挖掉标识符以后剩下的内容。

数组元素的类型当然可以是指针变量,如int *,则写成

int* arr[5]; // 其类型可以理解为int*[5]

数组的声明是分裂式的,右边部分是数组符号及元素数量,左边部分是元素的类型。核心是右边的[],所以先往右看。

指向数组指针的声明,将标识符用(*p)填充:

int (*p)[5]; // 标识符p→往右看→是一个右括号(括号的中止)→往左看,是一个指针声明符号,所以p是一个指针,括号内的内容看完了,往右看,是数组符号→p指向一个数组,有5个元素→往左看,数组元素的类型,类型是int。其类型可以理解为int(*)[5],也就是挖掉标识符以后剩下的内容。

同样的int*(*p)[5]; // 也是按上述思路去理解,p是指针,指向有5个元素的数组,元素类型是int*。

英文的表达是先干先枝,好像更准确:

int vari[10];

vari is array of int

int vari[10][3];

vari is array of array(3 elements) of int

int *vari[10]

vari is array(10 elements) of pointer to int

int(*vari)[3];

vari is pointer to array(3 elements) of int

int vari[10][3]; vari往右看,是一个数组,有10个元素,元素的类型是int[10]。

4 函数类型、声明与指向函数的指针变量的类型、声明

将上述数据的符号[]改成(),数组元素的个数改成参数类型,则成了函数的声明,其理解规则基本一致。

int arr(int); // 标识符arr→往右看→是函数符号→arr是一个函数,其参数类型是int→往左看,函数返回值的类型,类型是int。其类型可以理解为int(int),也就是挖掉标识符以后剩下的内容。

函数元素的类型当然可以是指针变量,如int *,则写成

int* arr(int); // 其类型可以理解为int*(int)

函数的声明也是分裂式的,右边部分是函数符号及参数类型,左边部分是返回值类型。核心是右边的(),所以先往右看。

指向函数指针的声明,也是标识符用(*p)填充:

int (*p)(int); // 标识符p→往右看→是一个右括号(括号的中止)→往左看,是一个指针声明符号,所以p是一个指针,括号内的内容看完了,往右看,是函数符号→p指向一个函数,参数类型是int→往左看,函数元素的类型,类型是int。其类型可以理解为int(*)(int),也就是挖掉标识符以后剩下的内容。

同样的int*(*p)(int); // 也是按上述思路去理解,p是指针,指向参数类型是int的函数,函数返回值类型是是int*。

英文的表达是先干先枝,好像更准确:

int func(int vari);

func is function(pararmeter is int vari) returning int

int (*func)(int vari);

func is pointer to function(pararmeter is int vari) returning int

5 与数组、函数相关的类型与声明的理解的规则

其类型和声明的写法是分裂式的。

对于数组,右边是数组符号、数组元素数量,数组元素的类型写在左边。

对于函数,右边是函数符号、函数参数类型,函数返回值类型写在左边。

核心是右边,以理解的规则是,从标识符开始,往右看,看是数组还是函数,及元素数量或函数参数类型,然后再往左看,对应元素类型或返回值类型。如果有括号,则先理解括号内的部分,规则也是先右后左(也可以理解为符号[]和()相对于*,有较高的优先级)。(在按上述规则理解时,如果先看到的是右括号,自然是优先级的括号,如果先看到的是左括号,则一般是函数标识符括号)。

c语言关于指针知识点(可以说基本上弄懂C)(5)

二级指针和二维数组也可以应用于上述规则:

int n = 5; int *p = &n; int **pp = &p; // p先右后左,右边没有,左边首先是*,表示p是一个指针,指向的类型是int* int vari[10][3]; // vari往右看,是一个数组,有10个元素,元素的类型是int[10]。

6 指针变量与数组名

数组名的实质是一个指针变量,指向一块内存的基地址,数组元素的分量以其地址为基准进行偏移,如:

int arr[5]; arr[3] = 6; *(arr 3) = 6; // []写法是指针写法的语法糖

注意arr 3运算时,指针的移动是按arr元素的尺寸进行移动的,移动的字节数是3*sizeof(int),为了方便指针的算术运算,在C或C 编译器中,要求数组名要蜕变为指向数组首元素的指针。因为这里数组本身的尺寸是5*sizeof(int),按数组自身的尺寸移动指针没有任何意义。

int flag = 999; int arr[5] = {1,2,3,4,5}; arr[3] = 6; *(arr 3) = 6; // []写法是指针写法的语法糖 //int *p = &arr; // cannot convert from 'int (*)[5]' to 'int *' int (*pp)[5] = &arr; pp ; // pp指向了整个数组的后一个元素flag(栈地址是递减分配的) int a = **pp; // a = 999 int *p = arr; p ; //p指向了数组元素的下一个元素 int b = *p; // b = 2 printf("%d %d\n",a,b); // 999 2

数组名的上下文中,只有三种情况表示数组本身,其它情形都蜕变为指向数组首元素的指针。

int arr[3][4] = {{1},{2},{3,10,11,12}}; // 情形一,声明时 int n = sizeof(arr); // 情形二,使用sizeof时 int (*parr)[3][4] = & arr; // 情形三,取址时 int (*parr)[4] = arr; // 其它情况都蜕变为指向数组首元素的指针 int a = *(*(arr 2) 3); // arr[2][3]是指针写法的语法糖 //int **pp = arr; // cannot convert from 'int [3][4]' to 'int ** ' // pp和arr类型完全不一致,这的类型是int**,arr的类型是int[3][4],蕴含有长度信息。 printf("%d\n",arr[2][3]); // 12 printf("%d\n",a); // 12

数据与指针的等价关系:

c语言关于指针知识点(可以说基本上弄懂C)(6)

比较一下指针和数组:

c语言关于指针知识点(可以说基本上弄懂C)(7)

7 指向数组或函数的指针变量的使用情形

主要用做函数参数。

7.1 数组指针用作函数参数

#include <stdio.h> #include <stdlib.h> void func(int(*p)[4]){} void func2(int**pp){} int main() { int arr[3][4] = {{1},{2},{3,10,11,12}}; int (*p)[4] = arr; func(p); func(arr); int **pp = (int**)malloc(sizeof(int*)*3); func2(pp); return 0; }

一元数组和二元数组用做函数参数:

#include <stdio.h> #include <iostream> using namespace std; void func0(int arr[10]){} void func2(int arr[]){} void func3(int *arr){} void func4(int *arr[20]){} void func5(int **arr){} void oneDimension(){ int arr[10] = {0}; int *arr2[20] = {0}; func0(arr); func2(arr); func3(arr); func4(arr2); func5(arr2); } void test0(int arr[3][5]){} void test2(int arr[][5]){} void test3(int (*parr)[5]){} void twoDimension(){ int arr[3][5]={0}; test0(arr); test2(arr); test3(arr); } int main() { oneDimension(); twoDimension(); return 0; }

7.2 函数指针用作函数参数

#include <stdio.h> bool comp(int a,int b){return false;} void sort(int(*p)[4],bool(*pf)(int,int)){} int main() { bool(*pf)(int,int) = comp; int arr[3][4] = {{1},{2},{3,10,11,12}}; int (*p)[4] = arr; sort(p,pf); sort(arr,pf); return 0; }

使用总结:

c语言关于指针知识点(可以说基本上弄懂C)(8)

8 数组与函数指针变量其元素或返回值混合声明的理解

直接看代码和备注:

int(*fp)(int); // 函数指针 int * arr[5]; // 指针数组 int(*fa[5])(int); // 函数指针数组,(*fa[5])要用括号,返回类型int(int)要分裂 int(* fpp())(int); // 函数指针函数,(* fpp())要用括号,返回类型int(int)要分裂 int(* fpa())[5]; // 数组指针函数,(* fpa())要用括号,返回类型int[int]要分裂 int(* fppp(int(*fp2)(int a)))(int b); //函数fppp有一个函数指针做参数,返回一个函数指针 // 函数可以返回函数指针或数组指针,不能返回函数或数组 // 当先从fp2开始理解时,发现其包括在一个()中,所以是一个函数参数 // 当先从fppp开始时,发现其是一个指针,所以当一个声明中有多个标识符时,从最左边的一个开始 int(*parr[10])[5]; // 数组指针数组

以上代码可以看到,写函数指针函数(一个函数返回一个函数指针)或数组指针函数(一个函数返回一个数组指针)时,需要用到括号来标明优先级,然后将函数的返回类型与参数(或数组的元素类型与数组的元素个数)分开写到括号的前后。

9 二级指针应用的场合

当需要一个主调函数调用一个被调函数来修改主调函数内或全局的指针变量时,被调函数需要使用一个二级指针或一个指针引用

int n = 5; int m = 6; int *p = &n; int *p2 = &m; int **pp = &p; // p先右后左,右边没有,左边首先是*,表示p是一个指针,指向的类型是int* int **pp2 = &p2; int *&r = p; void func1(int *p){ p = &m; // 如果指向的不是动态内存,函数调用结束后p本身(局部变量)将不复存在 *p = 55; // 函数内解引用指针变量参数,产生副作用,更新p指向的变量的值为55 } void func2(int** pp){ pp = pp2; // 如果指向的不是动态内存,函数调用结束后p本身(局部变量)将不复存在 *pp = p2; // 函数内解引用指针变量参数,产生副作用,更新pp指向的变量的值为p2(一级指针) } void func3(int*&r){ r = p2; // 引用自动解引用 }

另外,二级指针也用于动态内存分配场合:

int **arr = (int**)malloc(sizeof(int*)*n); for(int i=0;i<n;i ) arr[i] = (int*)malloc(sizeof(int)*n);

ref

达尔教育 陈宗权 https://www.bilibili.com/video/BV1XT4y1Z7GK?p=20

鹏哥C语言 https://www.bilibili.com/video/BV1TT4y1F7Z9?p=123

-End-

,