C 语言中的指针(补充)

本文是对之前文章的一个补充。

首先我们知道:

  • & 运算符后加变量名就变成了变量名的地址,& 可以称为取地址运算符
  • 变量地址前加 * 运算符就能够访问到变量,* 可以成为取内容运算符
  • 地址一般是用存储空间的第一个字节的地址表示变量地址,即低字节地址
  • 地址也是有大小的

指针和指针变量

指针

最纯粹的指针可能就是 (void *),也就是用 malloc 函数在堆上申请内存空间的返回值,此时只表示申请内存空间的首地址。但其实指针存在很多类型。归总来说,指针就是有类型的地址,该类型表示一次性能够读取到的内存空间范围。

该部分内容在之前文章中也有提到,不熟悉的话可以再翻翻。

指针变量

指针变量的形式为:

datatype *var;
  • datatype 表示一次性能够读取到的内存空间范围
  • * 表示该变量是个指针
  • var 表示该指针变量的变量名

变量大小

而不管是什么类型的指针变量,sizeof(var) 的值都是与计算机本身相关的常量值。

#include <stdio.h>

int main(void)
{
    char *pc;
    int *pi;
    int (*pii)[3];
    int arr[10];
    printf("sizeof(pc) = %d\n",sizeof(pc));
    printf("sizeof(pi) = %d\n",sizeof(pi));
    printf("sizeof(pii) = %d\n",sizeof(pii));
    printf("sizeof(arr) = %d\n",sizeof(arr));

    return 0;
}

结果为:

sizeof(pc) = 4
sizeof(pi) = 4
sizeof(pii) = 4
sizeof(arr) = 40

注意最后的 arr 并不符合指针变量的形式,是数组名。而数组指针的形式也并不符合指针变量的形式,但却仍旧是指针。

变量类型

变量的值通过不同类型的指针变量进行解释,会得到不同的结果。还是之前那句话,指针变量的类型决定了该指针的寻址能力。

#include <stdio.h>

int main(void)
{
    int a = 0x12345678;
    char *ch = &a;
    short *un = &a;
    int *in = &a;
    printf("a = %x\n",a);
    printf("*ch = %x\n",*ch);
    printf("*un = %x\n",*un);
    printf("*in = %x\n",*in);

    return 0;
}

结果为:

a = 12345678
*ch = 78
*un = 5678
*in = 12345678

指针运算

指针运算和数值运算可能有点不太一样,主要是与指针类型有关。

#include <stdio.h>

int main(void)
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};

    printf("%p\n",arr);
    printf("%p\n",arr+5);

    printf("%d\n",(arr+5)-arr);
    printf("%d\n",(char *)(arr+5)-(char *)arr);


    return 0;
}

结果为:

0060FE88
0060FE9C
5
20

从上边的结果可以看出:

  • arr 和 arr+5 的地址间隔为 20
  • 但是 (arr+5)-arr 却不是 20,而是 5,这是因为在指针运算中,会默认将其结果按照寻址能力进行换算
  • 因此在将 (arr+5) 和 (arr) 转化为 (char *) 之后,计算的结果就是按照 (char *) 的寻址能力进行换算的

二级指针

其实我们之前使用过二级指针,在堆上返回一维数组的第二种形式中,我们没有使用返回值,而是使用二级指针作形参,改变了一级指针指向的内容。简单说来,二级指针是指向指针的指针。

定义和初始化

datatype **var = &p;

上次的 p 是一个一级指针。

#include <stdio.h>

int main(void)
{
    char c = 'h';
    char *pc = &c;
    char **pp = &pc;

    printf("&c = %p\n",&c);
    printf("pc = %p\n",pc);
    printf("*pp = %p\n",*pp);

    printf("c = %c\n",c);
    printf("*pc = %c\n",*pc);
    printf("**pp = %c\n",**pp);

    return 0;
}

结果是:

&c = 0060FEAB
pc = 0060FEAB
*pp = 0060FEAB
c = h
*pc = h
**pp = h

从上边的例子中可以看出,利用二级指针能够改变原始变量的值。

间接数据访问

改变二级指针的指向

可以改变二级指针的指向,而不改变原始变量的内容。

#include <stdio.h>

int main(void)
{
    char c = 'h';
    char d = 'h';
    char *pc = &c;
    char **pp = &pc;

    printf("c = %c\n",c);
    printf("*pc = %c\n",*pc);
    printf("**pp = %c\n",**pp);

    printf("&c = %p\n",&c);
    printf("pc = %p\n",pc);
    printf("*pp = %p\n",*pp);

    *pp = &d;

    printf("d = %c\n",d);
    printf("*pc = %c\n",*pc);
    printf("**pp = %c\n",**pp);

    printf("&d = %p\n",&d);
    printf("pc = %p\n",pc);
    printf("*pp = %p\n",*pp);

    return 0;
}

结果为:

c = h
*pc = h
**pp = h
&c = 0060FEAB
pc = 0060FEAB
*pp = 0060FEAB
d = e
*pc = e
**pp = e
&d = 0060FEAA
pc = 0060FEAA
*pp = 0060FEAA

改变二级指针指向的内容

还可以改变二级指针指向的内容,而不改变指针本身。

#include <stdio.h>

int main(void)
{
    char c = 'h';
    char *pc = &c;
    char **pp = &pc;

    printf("c = %c\n",c);
    printf("*pc = %c\n",*pc);
    printf("**pp = %c\n",**pp);

    printf("&c = %p\n",&c);
    printf("pc = %p\n",pc);
    printf("*pp = %p\n",*pp);

    c = 'e';

    printf("c = %c\n",c);
    printf("*pc = %c\n",*pc);
    printf("**pp = %c\n",**pp);

    printf("&c = %p\n",&c);
    printf("pc = %p\n",pc);
    printf("*pp = %p\n",*pp);

    return 0;
}

结果是:

c = h
*pc = h
**pp = h
&c = 0060FEAB
pc = 0060FEAB
*pp = 0060FEAB
c = e
*pc = e
**pp = e
&c = 0060FEAB
pc = 0060FEAB
*pp = 0060FEAB

除此之外,还可以利用二级指针改变指向的一级指针的指向。同样利用 N 级指针改变指向的 N-1 级指针,N-2 级指针......的指向。

二级指针的步长

由于二级指针指向的是一级指针,而一级指针跟平台有关(此处为 4),因此二级指针的步长也是 4。

#include <stdio.h>

int main(void)
{
    char c;
    char *pc = &c;
    char **ppc = &pc;

    printf("ppc = %p\n",ppc);
    printf("ppc+1 = %p\n",ppc+1);

    int **ppi = NULL;
    printf("ppi = %p\n",ppi);
    printf("ppi+1 = %p\n",ppi+1);

    double **ppd = NULL;
    printf("ppd = %p\n",ppd);
    printf("ppd+1 = %p\n",ppd+1);

    return 0;
}

结果为:

ppc = 0060FE9C
ppc+1 = 0060FEA0
ppi = 00000000
ppi+1 = 00000004
ppd = 00000000
ppd+1 = 00000004

指针数组

定义

之前我们说到的是数组指针,而这里的是指针数组,准确地说应该是字符指针数组。

指针数组的本质是数组,数组中每一个成员是一个指针。定义形式为:

char *p[10];

而数组指针的形式为:

char (*p)[10];

可以看出两者的不同在于 * 和 p 的结合方式不同。指针数组中,是 char * 修饰的 p[10],而数组指针中是 char [10] 修饰的 (*p)。

也就是说,指针数组中的每个元素都是 char *。

初始化

既然是数组,就会有初始化:

#include <stdio.h>

int main(void)
{
    char *p[10] = {"apple","banana","orange","strawberry"};
    for(unsigned i=0; i<sizeof(p)/sizeof(*p); i++)
        printf("%s\n",p[i]);

    return 0;
}

结果为:

apple
banana
orange
strawberry

二级指针访问指针数组

指针数组名与二级指针的关系

  • char **p 是二级指针
  • char *arr[10] 是指针数组,arr[0] 本身是 char * 类型
  • 因此就可将两者联系起来:char **p = arr;
#include <stdio.h>

int main(void)
{
    char *p[10] = {"apple","banana","orange","strawberry"};
    for(unsigned i=0; i<sizeof(p)/sizeof(*p); i++)
        printf("%s\n",p[i]);

    printf("*****************\n");

    char **pp =p;
    for(unsigned i=0; i<sizeof(p)/sizeof(*p); i++)
        printf("%s\n",pp[i]);

    return 0;
}

结果为:

apple
banana
orange
strawberry
*****************
apple
banana
orange
strawberry

最后的 NULL

在进程空间中我们提到过 argv 的参数个数会比 argc 的数值多 1,而多出来的那个参数是 NULL,为什么会这样子呢?

在上边的程序中,我们用到了下边的程序段:

char **pp =p;
for(unsigned i=0; i<sizeof(p)/sizeof(*p); i++)
    printf("%s\n",pp[i]);

但是在日常的习惯中,如果我们使用 char **pp = p 之后,在 for 循环中就可以丢弃掉 p,但是实际上 pp 只是一个指针,因此在 for 中单单使用 pp 是不能遍历所有的数组元素的。因此,就需要在指针数组的最后边加上一个 NULL 来作为结束遍历的条件。

#include <stdio.h>

int main(void)
{
    char *p[10] = {"apple","banana","orange","strawberry",NULL};
    for(unsigned i=0; i<sizeof(p)/sizeof(*p); i++)
        printf("%s\n",p[i]);

    printf("*****************\n");

    char **pp =p;
    while(*pp)
        printf("%s\n",*pp++);

    return 0;
}

结果为:

apple
banana
orange
strawberry
*****************
apple
banana
orange
strawberry

上边的结果比之前的 for 循环的形式要简洁的多了。

堆上二维空间

之前我们说过堆上的一维空间,那么堆上的二维空间又是什么意思呢?

我们平常见到的二维数组是一种二维空间,而二维空间却不一定都是二维数组,但二维空间却具有数组的访问形式。比如我们可以将一维数组转化为数组指针的形式,从而得到类似二维空间的访问形式。

一级指针作返回值

#include <stdio.h>
#include <stdlib.h>

int *memapp(int base, int row, int col)
{
    int *p = (int *)malloc(base*row*col);
    return p;
}

int main(void)
{
    int base = sizeof(int), row = 3, col = 3;

    int (*p)[3] = memapp(base, row, col);

    for(int i=0; i<row; i++)
        for(int j=0; j<col; j++)
            p[i][j] = i*col+j+1;

    for(int i=0; i<row; i++)
        for(int j=0; j<col; j++)
            printf("%d\n",p[i][j]);
    
    free(p);

    return 0;
}

结果为:

1
2
3
4
5
6
7
8
9

上边的程序中,在堆上申请了连续一段空间,但在 main 函数中却将之解释成了二维空间。

二级指针作返回值

上边的程序中,函数 memapp 返回的是一段连续的空间,更像是一维空间。我们同样可以直接申请不连续的空间并利用二级指针返回。

#include <stdio.h>
#include <stdlib.h>

int **memapp(int base, int row, int col)
{
    int **p = malloc(sizeof(void *)*row);
    for(int i=0; i<row; i++)
    {
        p[i] = malloc(base*col);
    }
    return p;
}

int main(void)
{
    int base = sizeof(int), row = 3, col = 4;

    int **p = memapp(base, row, col);

    for(int i=0; i<row; i++)
        for(int j=0; j<col; j++)
            p[i][j] = i*col+j+1;

    for(int i=0; i<row; i++)
        for(int j=0; j<col; j++)
            printf("%d\n",p[i][j]);

    for(int i=0; i<row; i++)
        free(p[i]);
    free(p);

    return 0;
}

结果为:

1
2
3
4
5
6
7
8
9
10
11
12

比较需要注意的是最后堆上变量的释放形式,需要从里到外先释放一级指针的指向内容,再释放二级指针的指向内容。

const

因为多级指针的能够从次级指针对原始变量进行更改,因此有时候为了防止对数据或次级指针的更改,增强程序的健壮性,需要使用 const 关键字进行变量进行限制。

const 修饰变量

此时首先需要对变量进行初始化,并且经过 const 修饰符修饰过的变量不能再次发生更改。

#include <stdio.h>

int main(void)
{
    const int a = 1;
    const int b;

    printf("%d\n",a);
    printf("%d",b);
    return 0;
}

结果为:

1
89

此时的 b 只能一直保持随机值。

const 修饰指针

此时表示指针的指向是恒定的,但是指向的内容却是可以更改的。

#include <stdio.h>

int main(void)
{
    int a = 1;
    int * const p = &a;

    printf("%d\n",a);
    printf("%d\n",*p);

    a = 2;
    printf("%d\n",a);
    printf("%d\n",*p);
    return 0;
}

结果为:

1
1
2
2

const 修饰指针指向内容

此时表示指针的指向的内容是恒定的,但是指向本身却是可以更改的。

#include <stdio.h>

int main(void)
{
    int a = 1;
    int b = 2;
    const int *p = &a;

    printf("%d\n",a);
    printf("%d\n",*p);

    p = &b;
    printf("%d\n",a);
    printf("%d\n",*p);
    return 0;
}

结果为:

1
1
1
2

但是下边的修改却是可行的:

#include <stdio.h>

int main(void)
{
    int a = 1;
    const int *p = &a;

    printf("%d\n",a);
    printf("%d\n",*p);

    a = 2;
    printf("%d\n",a);
    printf("%d\n",*p);
    return 0;
}

结果为:

1
1
2
2

因为 a 并没有被限制,因此显然是可以修改的,所限制的只是 *p = 2 这种形式。

同时限制

此时不管是指针的指向还是指向的内容都不能修改,相当于该指针被锁定。

#include <stdio.h>

int main(void)
{
    int a = 1;
    int b = 2;
    const int * const p = &a;

    printf("%d\n",a);
    printf("%d\n",*p);
    
    return 0;
}

结果为:

1
1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值