一文讲解C语言指针

计算机内存中的每个位置都有一个地址表示,邻近内存位置合成一组,就允许存储更大范围的值。而 C 语言提供了指针类型,以符号形式使用地址的方法。因为计算机的硬件指令非常依赖地址,指针在某种程度上把程序员想要传达的指令以更接近机器的方式表达。

一文讲解C语言指针

指针变量

指针使用 type * pointer-name 声明,并用其他变量的地址予以初始化。声明后并未初始化是不会自动分配任何内存的。

int *p;

上述声明中,int 表明了指针所指向对象的类型,*表明声明的变量是指针。对未初始化的指针变量执行间接访问操作是非法的,而且错误常常难以检测。

*p = 12;

未对指针变量初始化,所以没法预测 12 这个值将存储在哪里。因此要对指针执行间接访问前,指针必须进行初始化,可以指向现有内存,或者分配动态内存。

int a = 12;
p = &a;
// 或者
p = malloc(sizeof(int));
*p = 12;

声明并初始化后,看一下指针所指向的内存地址。

int    a = 112, b = -1;
float  c = 3.14;
int   *d = &a;
float *e = &c;

指针的初始化是用 & 操作符完成的,它用于产生操作数的内存地址。

一文讲解C语言指针

de 的值是地址而不是整型或浮点型数值。从上面可以看出,d 存储的内容是 a 的地址,e 存储的内容是 c 的地址。区分变量 d 的地址 112 和内容 100 是非常重要的。

间接访问

通过一个指针访问它所指向的地址的过程称为间接访问 indirection 或解引用指针 dereferencing the pointer。这个用于执行间接访问的操作符是单目操作符*。下表是上面例子中的一些声明。

表达式 右值 类型
a 112 int
b -1 int
c 3.14 float
d 100 int *
e 108 float *
*d 112 int
*e 3.14 float

d 的值是 100,当使用间接访问操作符时,*d 访问位置 100 的值是 112,左值是位置 100 本身。

一文讲解C语言指针

上述箭头指向的黑框代表的是变量的地址,因此,除了 de 指针变量中存在间接引用操作符,否则 de 所存储的值就是 ac 变量的地址。

间接访问操作符所需要的操作数是个右值,但这个操作符所产生的结果是个左值。

int a;
int *d = &a;

对指针变量进行间接访问表示的是访问指针所指向的位置。间接访问指定了一个特定的内存位置,这样可以把间接访问表达式的结果作为左值使用。

*d = 10 - *d;

上述语句包含两个间接访问操作。右边的间接访问作为右值使用,所以它的值是 d 所指向的位置所存储的值,即 a 的值。左边的间接访问作为左值使用,所以 d 所指向的位置把赋值符右侧的表达式的计算结果作为它的新值。

而下述语句就是非法的。

d = 10 - *d;   <-???

上述表示的是把一个整型数量 10-*d 存储于一个指针变量中,变量类型不一致,编译器会报错 warning: assignment makes pointer from integer without a cast

指针与整型值混用属于无意识的错误

了解一下如下表达式的情况。

*&a = 25;

该表达式是用 & 操作符产生变量 a 的地址,它是一个指针常量,然后使用 * 操作符访问其操作数所表示的地址。在这个表达式中,操作数是 a 的地址,所以值 25 就存储于 a 中。与 a=25 的功能是相同的,但除非编译器或优化器能做优化,否则上述表达式所产生的目标代码将会更大、更慢,并使源代码可读性变差。

指针常量

*100 = 25;

该表达式看上去是把 25 赋值给 a,因为 a 是位置 100 所存储的变量。但是 100 的类型是整型,而间接访问操作只能用作于指针类型表达式。如果想把 25 存储于位置 100,可以使用强制类型转换。

*(int *)100 = 25;

如果 a 存储于位置 100,上述表达式使用强制类型转换把值 100整型转换为指向整型的指针,就是把值 25 存储于 a

NULL指针

NULL 指针作为一个特殊的指针变量,表示不指向任何东西。要使一个指针变量为 NULL,你可以给它赋一个零值。

int *np = NULL;

为了测试一个指针变量是否为 NULL,你可以将它与零值进行比较。

printf("%dn", np == NULL);    // 结果: 1
printf("%dn", np == 0);       // 结果: 1

选择零这个值是因为一种源代码约定。就机器内部而言,NULL 指针的实际值可能与此不同。在这种情况下,编译器将负责零值和内部值之间的翻译转换。

对指针进行解引用操作可以获得它所指向的值。但从定义上看,NULL 指针并未指向任何东西。因此,对一个 NULL 指针进行间接访问操作是非法的,后果因编译器而异,两个常见的后果分别是返回内存位置零的值以及终止程序。

指针的指针

下面声明指针指向 int 类型变量的地址:

int a = 12;
int *b = &a;

内存分配如下图所示:

一文讲解C语言指针

当要声明指向 b 的类型,并进行初始化,即声明一个指针的指针。

int **c = &b;

它们在内存中大致如下:

一文讲解C语言指针

上述表达式中,**c 相当于 *(*c)*c 访问 c 所指向的位置,即变量 b*(*c) 访问变量 b 所指向的地址,即变量 a

表达式 相当的表达式
a 12
b &a
*b a,12
c &b
*c b, &a
**c *b, a, 12

指针运算

指针可以执行一些有限的算术运算,并非所有运算都合法。如指针运算作用于数组中,其结果可以预测。对任何非指向数组元素的指针执行算术运算是非法的。

指针可以加上一个整形值,也可以减去一个整形值,根据情况进行调整,原值将乘以指针目标类型的长度。这样,对一个指针加 1 便使它指向下一个变量,至于该变量在内存中占几个字节的大小则与此无关,如下表所示。

表达式 p指向…的指针 *p的大小是… 增加到指针的值
p+1
char 1 1
short 2 2
int 4 4
double 8 8
p+2 char 1 2
short 2 4
int 4 8
double 8 16

算术运算

指针运算的第一种形式是 。这种形式只能用于指向数组中某个元素的指针,如下图所示。

一文讲解C语言指针

这类表达式的结果类型也是指针,也适用于使用 malloc 函数动态分配获得的内存。

数组中的元素存储于连续的内存空间中,后面元素的地址大于前面元素的地址。因此,对指针加 3 使它向右移动 3 个元素的位置,减 3 使它向左移动 3 个元素的位置。

int a[5] = {1,3579};
int *p1 = a;        // 把一个地址赋给指针
int *p2 = &a[2];    // 把一个地址赋给指针

int *p3;
p3 = p1+3;    // 指针加法
printf("%dn", *p3);    // 输出结果: 7
p3 = p2-2;    // 指针减法
printf("%dn", *p3);    // 输出结果: 1

当对指针执行加法或减法运算后如果指针所指的位置在数组第 1 个元素的前面或在数组最后一个元素的后面,那么其效果就是未定义的。让指针指向数组最后一个元素后面的那个位置是合法的,但对这个指针执行间访问可能会失败,一般不允许对指向这个位置的指针执行间接访问操作。

指针算术运算也可以使用递增和递减,代码如下所示。

*++p1;

因为 p1 指向数组的起始地址,间接访问操作符作用于增值后的指针的拷贝上,所以上述操作访问的是数组中第二个元素的地址,在使用间接访问操作符,可以获取值 3

*p1++;

但使用后缀操作符所产生的结果是指向数组的起始地址,获得值 1。执行时的步骤是先产生一份指针的拷贝;然后++操作符增加数组起始地址的值,会指向第二个元素的地址;最后,在拷贝的指针上执行间接访问操作。

一文讲解C语言指针

指针运算的第二种形式是 。只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针,如下图所示:

一文讲解C语言指针

两个指针相减的结果的类型是 ptrdiff_t,它是一种有符号整型类型。减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而非字节),因为减法运算的结果将除以数组元素类型的长度。

假定上图中数组元素的类型为 float,每个元素占 4 个字节的内存你空间。如果数组的起始位置为 1000p1 的值是 1004p2 的值是 1024,但表达式 p2-p1 的结果值将是 5,因为两个指针的差值 20 将除以每个元素的长度 4 。这种差值的调整使指针的运算结果与数据的类型无关。无论数组包含的元素类型如何,这个指针减法运算的值总是 5

如果两个指针所指向的不是同一个数组中的元素,那么它们之间相减的结果是未定义的。

绝大多数编译器都不会检查指针表达式的结果是否位于合法的边界之内。因此,当使用指针运算时,必须小心,确信运算的结果将指向有意义。

关系运算

指针执行关系运算可以用下列关系操作符。

==    !=    <    <=    >    >=

任何指针之间都可以进行比较,测试它们相等或不相等。两个指针都指向同一个数组中的元素时,还可以执行 <<=>>= 等关系运算,用于判断它们在数组中的相对位置。对两个不相关的指针执行关系运算,其结果是未定义的。

根据你所使用的操作符,比较表达式将告诉你哪个指针指向数组中更前或更后的元素。

int a[5] = {1,3579};
int *p1 = a;        // 把一个地址赋给指针
int *p2 = &a[2];    // 把一个地址赋给指针
printf("%d", p2 > p1);    // 输出结果: 1

指针也可以在循环中使用关系操作符,如下所示,用于清除一个数组中所有的元素。

int a[5] = {1,3579};
int *p;

for (p=&a[0]; p < &a[5];) {
    *p++ = 0;
}

void指针

void类型为C99新增的类型,用于指定没有可用的值,当void为指针时,表示为无类型指针,是一种特殊的指针,任何类型的指针都可以直接转为 void 指针,而无需进行强制类型转换。

int a = 12;
int *p = &a;
void *vp = p;

如上所述,int * 类型可以直接赋值给 void * 类型,而无需进行强制类型转换。但是,当 void * 类型赋值给 int * 类型时,需进行强制转换。

int a = 12;
void *vp = &a;
int *p = (int *)vp;

使用 void 指针,就不要做间接访问算术运算,因为不知道其间接访问操作的内存大小,以及算术运算操作的大小,因此它的结果是未知的。

int a = 12;
int *p = &a;
void *vp = p;
(*vp)++;    // 报错

上述代码或警告和报错,如下所示:

warning: dereferencing 'void *' pointer
error: invalid use of void expression

void指针通常是用在函数参数中,用于表示可接收处理任何类型,如C头文件 string.h中的 memset 函数接收的就是 void指针 。

void *memset(void *dst, int val, size_t n);

该函数是内存赋值函数,用来给某一块内存空间进行赋值。

  • dis 目标起始地址。
  • val 要赋的值。
  • n 要赋值的字节数。
char str[10];
char *cp = str;
memset(cp, 1sizeof(str) - 5);
for (int i = 0; i < 10; ++i) {
    printf("%dx20", *(cp + i));
}
// 输出结果:    1 1 1 1 1 0 25 21 64 0

可以手动模拟以下 memset 函数,了解 void * 作为参数如何使用,如下所示。

void *analog_memset(void *dst, int val, size_t size) {
    // char就是一个字节,计算机是以字节为单位存储的
    char *p = dst;
    if (p == NULLreturn NULL;
    while (size--) *p++ = (char)val;
    return p;
}



一文讲解C语言指针


原文始发于微信公众号(海人为记):一文讲解C语言指针

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/27509.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!