Pointer

2022-03-21
11分钟阅读时长

概念

计算机程序在存储数据时需要跟踪三种属性:

  1. 信息存储在何处
  2. 存储的值是多少
  3. 存储的信息是什么类型

声明变量时:

  1. 通过声明语句指明的符号名跟踪内存单元;
  2. 程序为该变量分配内存来存储值;
  3. 声明语句指出值的类型和符号名;

变量的地址称为该变量的指针,保存变量地址的变量称为指针变量;

通过变量A存储变量B的地址,并可以通过A访问B,则称A为指向B的指针变量,一般简称A为指向B的指针。

指针

声明

定义指针变量:dataType * p;

  1. *在与数据类型结合时,表示这是一个指向该数据类型的指针类型;

  2. *与变量名结合时,作为间接值/间接访问/解除引用运算符,表示将通过该指针变量间接找到所指向的变量的值;或表示p是一个引用,通过解除引用该指针变量从而找到它指向变量的值;

综上,称pdataType*类型,而*pdataType类型。

连续定义时每个变量前都需要带*,否则只有第一个变量会被解析为指针变量。

int *a,*b,*c;

特别地,数组名可以被认为是指针常量

赋值与取值

由于指针保存变量的地址,因此用取地址运算符对变量取地址,而后将地址赋给指针。

int *p=&a;
//这种声明即赋值的方法等效于下面两行:
int *p;
p=&a;

注意到pint*类型,aint类型,&aint*类型。

对指针作解除引用运算,得到指针所指向的变量的值。

int c=*p;

对指针变量A所指向的变量B的值的操作等效于直接对B进行操作。

#include<stdio.h>
int main(){
    int a=10;
    int *p=&a;
    *p=20;
    printf("%d",a);
}

下面的操作等价:

int a[2]={1,2};
int *p1=&a[0];
int *p2=&*a;
int *p3=a;

地址

指针大小

指针变量保存的是地址,地址本质上是整数。

一般地,指针变量大小由当前CPU运行模式的寻址位数决定。

寻址位数(bit)

指针大小(字节)

16

2

32

4

64

8

指针移动

#include<stdio.h>
int main(){
	char a='A',*pa=&a,*paa = &a;
    int b=20,*pb=&b;
    double c=99.9,*pc=&c;
    printf("&a=%#xc, &b=%#x, &c=%#x\n", &a, &b, &c);
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    pa++; pb++; pc++;
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    pa -= 2; pb -= 2; pc -= 2;
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //比较运算
    if(pa == paa){
        printf("%d\n", *paa);
    }else{
        printf("%d\n", *pa);
    }
    return 0;
}

注意到pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 int、double、char 类型所占字节的长度;减 2 时,地址分别减少 8、16、2,正好是 int、double、char 类型所占字节的长度的 2 倍。

对于指向数据类型为T的指针p:

  1. 表达式p+1
    1. 实际上地址值变化sizeof(T)
    2. 当p保存数组首地址时,具体含义是指向下一个元素
  2. 表达式&p+1
    1. 实际上地址值变化sizeof(p)
    2. 不一定有具体含义。

特别注意:

  1. (*p)++代表指向的元素自增,去掉括号则代表指向下一个元素!因为单目运算符结合顺序是右到左
  2. 不能对指针变量进行乘、除、取余等运算。

数组

数组概念

数组是一种数据格式,用来存储多个同类型的值。

  1. 声明数组应指出:存储的元素的数据类型数组名、数组元素个数

  2. 访问数组则应从0开始,一直到n-1结束。

    特别地,编译器不会检查下标是否有效。因此保证下标的有效性尤为重要。

数组与指针

数组名可以认为是指针常量,指向数组的第0个元素。 数组第 0 个元素的地址也称为数组的首地址。

数组名的本意是表示整个数组,且数组名和数组首地址并不总是等价。 其特点如下:

  1. 编译时编译器知道数组的长度;
  2. 数组名在其作用域内编译器认为它是数组;
  3. 在作用域外数组名退化为指向数组第 0 个元素的指针;

下面的例子说明数组名可以认为是数组首地址的情况:

#include <stdio.h>
int main(){
    int arr[] = { 1, 2, 3, 4, 5 };
    int len = sizeof(arr) / sizeof(int);  //求数组长度
    int i;
    for(i=0; i<len; i++){
        printf("%d  ", *(arr+i) );  //*(arr+i)等价于arr[i]
    }
    printf("\n");
    return 0;
}

下面的例子说明数组名在作用域内代表整个数组,在作用域外退化为指针常量:

#include <stdio.h>
int* test(int arr[]){
    return arr;
}
int main(){
    int arr[] = { 1, 2, 3, 4, 5 };
    int len1=sizeof(arr)/sizeof(int);
    int *p=test(arr);
    int len2=sizeof(test(arr))/sizeof(int);
    int len3=sizeof(p)/sizeof(int);
    printf("%d,%d,%d",len1,len2,len3);
    return 0;
}

注意:

  1. sizeof是运算符,它的值(或它代表的表达式)在编译时已经确定。

  2. C标准规定sizeof不能用于函数,但是GNU C扩展了,使之对void类型返回1,对函数类型返回函数指针的长度(在64位寻址能力的CPU上是8)。(注意划线部分也可能输出1)

  3. 上述程序中函数test的返回值类型不要写错了。

    由于指针可以由整型变量保存,因此如果写int,编译器报“从返回类型为int的函数返回int * ,将指针转换为整型而不进行类型转换”,而后返回整型,执行时会抛出异常,非常“贴心”。

指针引用数组元素

对于一维数组,引用方法如下:定义int a[10];

  1. 下标法a[i]
  2. 指针法*(a+i)

指针运算方法:定义int *p=a; 注意对指针访问进行是否越界检查。

  1. 下一个元素*(p+1)*(p+=1)*(++p)

  2. 上一个元素*(p-1)*(p-=1)或`*(++p);

  3. 定义int *p1,*p2都指向同一数组的元素

    p1-p2代表表示两个元素之间差了多少个元素。

注意为什么不写a++,因为a的值是常量。

对于二维数组,见二维数组指针节。

指针数组

数组中所有元素保存的元素是指针,称为指针数组。类型上可以将它当做二级指针使用。

dataType *arrayName[length]//[]优先级高于*,故应该理解为 dataType *(arrayName[length])

在访问各个指针元素时使用*(*arrayName+j)*arrayName[j]

下面是整型指针数组的例子:

#include<stdio.h>
int main(){
    int a=1,b=2,c=3;
    int *arr[3]={&a,&b,&c};
    int **parr=arr;
    //指针和二级指针的值输出
    printf("%d, %d, %d, %d\n",arr,parr,*arr,*parr);
    //二级指针指向的值的输出
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
    return 0;
}

可以发现指针数组中一级指针和二级指针输出的地址不同。

数组指针的应用:处理多个字符串,利用函数数组指针实现if-else功能。

下面是字符串指针数组的例子:

#include<stdio.h>
int main(){
	char *str[3] = {
        "String Test.",
        "Using Char Data Type.",
        "It is in C Language."
    };
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    printf("%d,%d,%s",str,*str,*str);
    return 0;
}

字符数组str中存放的是字符串的首地址,不是字符串本身。

字符串存储的位置在其他的内存区域,和字符数组是分开的。

一维数组和数组指针

  1. 数组名是常量,代表数组首元素的地址;

  2. 对数组名进行取地址操作,其类型为整个数组

    int a[10];
    int *p1=a;//相当于 int *p1=&a[0];
    int (*p2)[10]=&a;
    

    注意,在面对a+1和&a+1的区别时:

    1. a表示数组首元素首地址,其类型为int *,因此a+1相当于数组首地址值+sizeof(int)

      称p1是指向数组a的指针,通过p1[i]能够访问a的每一个元素。

    2. &a表示整个数组的首地址,其类型为int (*)[10],因此&a+1相当于数组首地址值+sizeof(a)

      称p2是指向数组a的首地址的指针,

      1. 通过(*p2)[i]能够访问a的每一个元素;
      2. 通过(*p2)+i能够访问a的每一个元素的地址。

多维数组

二维数组

  1. 二维数组在逻辑上是二维的,在内存中所有的数组元素都是连续排列的;
  2. 二维数组中的各个元素存放顺序是按行优先排列的;
  3. 允许把二维数组分解成多个一维数组来处理;
#include<stdio.h>
int main(){
    int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
    //打印第一维数组的数组名,它保存的是a[0]的首地址;打印第二维数组的数组名,它保存着a[0][0]的首地址
    printf("%d %d\n",a,*a);
    //均打印第二维数组的数组名,它保存它第一个元素a[0][0]的首地址
    printf("%d %d\n",a[0],*(a+0));
    //打印第一维数组的数组名,它保存的是a[0]的首地址;打印第一个元素a[0][0]的首地址
    printf("%d %d\n",&a[0],&a[0][0]);
    printf("%d %d\n",a[1],a+1);
    printf("%d %d\n",&a[1][0],*(a+1)+0);
    printf("%d %d\n",a[2],*(a+2));
    printf("%d %d\n",&a[2],a+2);
    printf("%d %d\n",a[1][0],*(*(a+1)+0));
    return 0;
}

设二维数组的数组名为a,则访问的元素的数据类型:

  1. 形如*(*(a+i)+j)a[i][j]*(a[i]+j)*(a+i)[j],程序员需要注意检查是否越界;
  2. 形如a,*a,a[0],*(a+0),&a[0],&a[0][0]的均为首地址,具体代表内容需要根据情况判断;
  3. 其他指针类型的地址值具体计算方法见下:

设二维数组中第一级M个指针,第二级N个指针。数据类型为dataType

  1. 一级指针每变化1,地址值变化sizeof(N*sizeof(dataType)),即到上一行或下一行
  2. 二级指针每变化1,地址值变化sizeof(dataType),即到上一个元素或下一个元素;

二维数组与数组指针

首先明确:二级指针和二维数组不是一个概念!多维同理!

int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};//定义二维数组a
int (*p1)[3][4]=&a;//&a为整个数组的首地址,类型为int (*)[3][4],定义指向&a的指针变量p1;
int (*p2)[4]=a;//a为第一维数组的数组名,类型为int (*)[4],定义指向a的指针变量p2
int *p3 =a[0];//a[0]为第二维数组的数组名,类型为int *,定义指向a[0]的指针变量p3

注意,在面对a+1、&a+1的区别时:

  1. a[0]表示数组(第一行的)第一个元素的首地址,其类型为int *,因此a[0]+1相当于数组首地址值+sizeof(int)

    称p3是指向数组a第一行的指针,能够访问a的第一行的每一个元素。

  2. a表示数组第一行的首地址,其类型为int (*)[4],因此a+1相当于数组首地址值+sizeof(int[4])

    称p2是指向数组a的指针,能够访问a的每一个元素

  3. &a表示整个数组的首地址,其类型为int (*)[3][4],因此&a+1相当于数组首地址值+sizeof(a)

    称p1是指向数组a的首地址的指针,能够访问a的每一个元素的地址。

特别地,数组名 a 在表达式中会被转换为和 p2 等价的指针!

注意:写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是数组指针。

指针数组和数组指针的区别

指针数组和数组指针在定义时非常相似,只是括号的位置不同:

int *(p1[5]);  //指针数组,可以去掉括号直接写作 int *p1[5];
int (*p2)[5];  //数组指针,不能去掉括号

指针数组和二维数组指针有着本质上的区别:

  1. 指针数组是一个数组,只是每个元素保存的都是指针;

    它用于访问一系列指针。

  2. 数组指针是一个指针,它指向一个数组;

    它用于访问一维数组中每个元素的地址*(*p)+n或二维数组中的每个元素*(*p+i)+j

实参

形参

类型名

类型

类型名

类型

数组的数组(二维数组)

int c[8][10]

数组指针

int (*)[10]

指针数组

int *c[10]

指针的指针(二级指针)

int ** c

数组指针

int (*)[10]

不改变

int (*)[10]

指针的指针(二级指针)

char **

不改变

char **

作为函数参数的多维数组

本节摘自《C与指针》中的指针节。

int matrix[3][10];
func(matrix);

函数原型应为:

void func(int (*mat)[10]);
void func(int mat[][10]);

该函数中,mat的第一个下标根据包含10个元素的整型数组长度进行调整,第二个下标根据整型的长度进行调整。

关键在于编译器必须知道第二个及以后的维度的长度才能够对各下标求值,故原型中必须声明这些维的长度。而第一维的长度并不需要,计算下标值时用不到它(可被自动计算)。

因此编写一维数组的形参的函数原型时,既可以写成数组形式,也可以写成指针的形式。但是对于多维数组,只有第一维可以如此选择。特别地,下面的原型是错误的:

void func(int **mat);

语法检查的时候不会报错,但是运行的时候会出现段错误。

这是因为该原型把mat声明为一个指向整型指针的指针,但是需要的是指向整型数组的指针

下面的例子更详细地讨论多维数组作参数时的情况:

#include <stdio.h>
int test1(int (*a)[2][3])
{
    printf("%d\n", ***a);//a[0][0][0]=1
    printf("%d\n", *(*(*a + 1) + 1));//a[0][1][1]=5
    return 0;
}
int test2(int (*a)[3])
{
    printf("%d\n", *(*a));//a[1][0][0]=7
    printf("%d\n", *(*a + 1));//a[1][0][1]=8
    printf("%d\n", *(*a + 2));//a[1][0][2]=9
    return 0;
}
int test3(int *a)
{
    printf("%d\n", *a);//a[0][1][0]=4
    printf("%d\n", *(a + 1));//a[0][1][1]=5
    printf("%d\n", *(a + 2));//a[0][1][2]=6
    return 0;
}
int main(int argc, char const *argv[])
{
    int a[2][2][3] = {{{1, 2, 3},{4, 5, 6}},{{7, 8, 9}, {10, 11, 12}}};
    test1(a);
    test2(*(a + 1));
    test3(*(*a + 1));
    return 0;
}

函数指针

函数指针是指向函数的指针,保存函数的入口地址。通过函数指针可以调用该函数。

函数指针声明:

returnType (*pointerName)(param list);

注意()的优先级高于*,第一个括号不能省略,如果写作returnType *pointerName(param list);就成了函数原型,它表明函数的返回值类型为returnType *

函数指针调用时可以直接用指针名pointerName(param list),也可以用(*pointerName)(param list)

#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b){
    return a>b ? a : b;
}
int main(){
    int x, y, maxval;
    //定义函数指针
    int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
    printf("Input two numbers:");
    scanf("%d %d", &x, &y);
    maxval = (*pmax)(x, y);//也可以写作maxval=pmax(x,y);
    printf("Max value: %d\n", maxval);
    return 0;
}

总结

定义

含义

int *p;

p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。

int **p;

p 为二级指针,指向 int * 类型的数据。

int *p[n];

p 为指针数组。[]的优先级高于 *,所以应该理解为 int *(p[n]);

int (*p)[n];

p 为二维数组指针。

int *p();

p 是一个函数,它的返回值类型为 int *。

int (*p)();

p 是一个函数指针,指向原型为 int func() 的函数。

特别注意:

  1. 指针变量可以进行加减运算,具体跟指针指向的数据类型有关。

  2. 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给数据;

  3. 使用指针变量之前一定要初始化,否则指针就成了野指针,可能造成崩溃。

    对于暂时没有指向的指针,建议赋值NULL

  4. 若两个指针变量指向同一个数组中的某个元素,那么两个指针变量可以相减,相减的结果就是两个指针之间相差的元素个数。

  5. 数组也是有类型的,数组名的本意是表示一组类型相同的数据。

    1. 在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组;
    2. 表达式或形参列表中的数组名会被转换为一个指向数组的指针。
Avatar

坐忘道琼峰 Sitting Oblivion Tao EndlessPeak

瞽者无以与乎文章之观,聋者无以与乎钟鼓之声。岂唯形骸有聋盲哉?
上一页 Scope
下一页 Custom Data Type