【问题标题】:Is it legal to treat a pointer like an array?将指针视为数组是否合法?
【发布时间】:2020-07-31 21:50:03
【问题描述】:
void uc(char* s)
{
    int i;

    for( i=0; i < strlen(s); i++ )
        if (97 <= s[i] && s[i] <= 122)
            s[i] = s[i] - 32;

    return;
}

我的教授向我们班展示了这个运算符。

char* s 复制一个数组,这没关系,因为数组名是它的第一个元素内存地址。

现在我的问题是:为什么我们将指针s 视为for 循环中的数组?
指针存储地址,但我了解到它们没有非常直观的行为......

我的问题是我将它们视为“一个 int 变量”,因为内存地址是十六进制格式的整数(对吗?),但我知道这不是那么简单。

编辑:谢谢大家的回答,我喜欢这个网站和社区

【问题讨论】:

  • 嗯。 char *s 不是运算符,而是定义。如果您有一个 char 数组 char name[] = "Giuseppe" 然后调用 uc(name),那么该数组将由指向其第一个元素的指针表示,但不会复制任何内容。如果您现在更改s[i]*s,您将修改原始数组通过指针s
  • @MOehm,我的意思是 uc 作为一个运算符(函数),无论如何,从你写的内容来看,你的意思是 *s 被定义为“地址数组”,它的数组维度相同代表? s 不是只存储一个地址吗?编译器怎么知道s[i]是什么?
  • 有些建议看起来很正常。首先 - strlen(s) 现在在每次迭代中计算。您可以在 for 循环之前将其分配给变量,例如int len = strlen(s) 然后使用len 或反转循环for ( i = strlen(s) - 1; i &gt;= 0; i-- )。第二 - 均衡比较if ( s[i] &gt;= 97 &amp;&amp; s[i] &lt;= 122 )(将变量和常量放在固定位置)。
  • 我建议您阅读 Kernighan 和 Ritchie 所著的The C Programming Language 2nd edition 一书的第 5 章(指针和数组)。

标签: c arrays pointers


【解决方案1】:

s 是一个指针,所以如果它被分配,我们可以将它用作一个数组。

以下两个选项类似:

s[i] = s[i] - 32;

*(s+i) = *(s+i) -32

因为内存地址是十六进制格式的整数(对吗?)

不,用户使用十六进制格式来显示内存地址。如果用二进制数来描述内存的地址,那就太长了。

【讨论】:

  • 那么计算器在幕后所做的就是增加存储在指针变量s中的地址的值?
  • *(s+i) 是地址s+i 的值。所以*(s+i) -32是两个值之间的减法
【解决方案2】:

char* s 复制一个数组 - 不,它没有。

这个函数的参数是一个指向char的指针。 就是这样。指针的解引用语法可以采用两种形式:*(p + n)p[n]。这两种形式是等价的。在这两种情况下,p 中的地址都是按值获取的,使用元素类型的 stride 进行调整,然后根据使用上下文取消引用结果地址以进行读取或存储。

您的函数可以用一种更明显的指针方式编写,并且作为奖励,避免每次迭代都调用strlen(这可能很昂贵)

void uc(char* s)
{
    for (; *s; ++s)
    {
        if (97 <= *s && *s <= 122)
            *s -= 32;
    }
}

这将遍历源自s 所持有的输入地址的char 序列,直到*s(在循环中使用++s 进行每次迭代时都会提前)等于终止的nullchar(零-八位字节)。因为我们在每次迭代中推进s,所以它始终位于为该迭代处理的角色上。

与 C 中的其他所有内容一样,函数参数按值传递。碰巧数组 id 的“值”在表达式上下文中使用时(几乎无处不在),是其第一个元素的基地址。因此,这产生了对从该地址引用的数据进行变异的可用性。

因此:

#include <stdio.h> // for puts

void uc(char* s)
{
    for (; *s; ++s)
    {
        if (97 <= *s && *s <= 122)
            *s -= 32;
    }
}

int main()
{
    char s[] = "lower";
    uc(s);
    puts(s);
    return 0;
}

将在兼容 ascii 的平台上打印 LOWER。我恳请您在调试器中运行上述代码,注意以下几点:

  • s[]main()中的基地址
  • 当您最初进入 uc 时,s 的参数列表中的值。
  • 循环迭代时,uc 中的 s 会发生什么变化
  • *s 的值在各种上下文中使用时出现在uc

老实说,这是我能做的最好的解释。祝你好运。

【讨论】:

  • 谢谢你的回答,所以空字符\0 在这种情况下会停止循环吗?所以\0string.h 以外的其他功能也很有用。抱歉这些愚蠢的问题,但我是编码新手。当您说记下s[] 的基地址时,您的意思是:打印地址并查看它在循环结束时的变化?我觉得自己很笨,但我是新手,我正在努力学习:p
  • #include &lt;stdio.h&gt; // for puts void uc(char* s) { for (; *s; ++s) { printf("\nWhat's inside s[i] (before): %c", *s); if (97 &lt;= *s &amp;&amp; *s &lt;= 122) *s -= 32; printf("\nWhat's inside s[i] (after): %c", *s); printf("\nAddress of s[i]: %p\n", s); } } int main() { char s[] = "lower"; printf("Address of s in main : %p\n", s); uc(s); puts(s); return 0; } ok cool,所以每次循环运行,s 的地址都会增加 1(十六进制为 a->b->c->...假设最后一个数字是a)
  • 一团糟,我尝试更好地格式化它,但我猜评论部分有点复杂......
【解决方案3】:

第一件事,并且完全直言不讳:

你的心智模式是错误的! 当务之急是,在你陷得太深之前,你现在要纠正你的误解。

char* s 复制一个数组,

这是一种误解。 s 是指向 char 的指针。它可以是单个 char 或整个数组。获取地址时会丢失底层对象的确切类型。

不过,没有任何东西被复制! 它只是一个指向“任何地方”的指针(挥舞着手臂),所有相关的人(你、编译器、其他程序员)都在一个不言而喻的不成文的协议中做个好人,不要做愚蠢的事。就像传入一个指针,该指针稍后将在函数中以无效的方式使用。

这没关系,因为数组名是它的第一个元素内存地址。

数组没有名字!符号有。数组的符号将衰减 指向一个指针,该指针指向构成数组的elementary 类型。这个衰减就是为什么你可以写char somearray[123]; char *p = somearray而不用写它的地址。

为什么我们在for循环中把指针s当作一个数组来处理?

因为我们可以。更具体地说,因为这个东西叫做“指针算术”。表达式s + 1 将产生一个指针,该指针指向指针所指向的元素地址之后的一个元素。它适用于任何数字(在ptrdiff_t 的取值范围内)。

当您在 C 中编写 a_pointer[i] 时,它会按字面意思翻译(这不是夸张,C 标准要求编译器必须这样处理它!)成 *(a_pointer + i)。所以发生的情况是,通过编写a_pointer[i],您是在告诉编译器:*“假设a_pointer 指向一个数组对象,并且a_pointer + i 仍在该数组对象的范围内:有了这个假设,取消引用定位并在那里产生价值。”

然而只有当结果指针位于对象的边界内时,才会定义指针运算的结果。

对不是从数组中取出的指针进行指针运算?未定义!

生成一个超出数组边界的指针?未定义!

我的问题是我认为它们“是一个 int 变量”,

他们不是!从技术上讲,指针可以通过独角兽灰尘和魔法来实现。在将它们与数字混合时,它们有一些非常具体的规则。在 C 编程语言中,这些规则是(简化的):

  • 指针可以转换为大小为sizeof(uintptr_t) 的整数,反之亦然。

  • 数值0转换为空指针,空指针转换为数值0。

  • Null 指针无效,因此不得取消引用。

  • 指针可以相互相减,得到一个与ptrdiff_t兼容的整数,所得整数的值是这两个指针之间元素的距离,假设两个指针都指向同一个对象。写在“类型”⟪ptrdiff_t⟫ = ⟪pointer A⟫ - ⟪pointer B⟫ 中,只有对它的算术有效的重新排列才有效。

  • 不能添加指针

  • 指针不能相乘

  • 没有强制要求指针的数字表示可用于指针算术。 IE。您不能假设 (pointer_A - pointer_B) == k*((uintptr_t)pointer_A - (uintptr_t)pointer_B)) 对应于 k 的任何值。

因为内存地址是十六进制格式的整数(对吗?),

啊?!?这不是事情的运作方式。

是的,您可以使用整数来寻址内存位置。不,您不必将它们写为十六进制。十六进制只是一个不同的数字基数,0xF == 15 = 0o17 == 0b1111。这些天我们通常用十六进制写地址,因为它很好地与我们当前的计算机架构的字长对齐,即 2 的幂。一个十六进制数字等于 4 位。但是还有其他架构使用不同的字长,并且在其他数字基础上更适合。

这仍然假设线性地址空间。然而,也有支持分段地址空间的计算机架构。事实上,您正在阅读本文的机器很可能就是这样的计算机。如果用的是Intel或者AMD的CPU,这玩意其实可以理解分段地址https://en.wikipedia.org/wiki/X86_memory_segmentation

在 x86 分段地址空间中,地址实际上由 两个 数字组成,即它形成一个向量。这意味着如果您正在编译 C 程序以在分段地址空间环境中运行,则指针类型不再是简单的奇异值数字。不过,C 仍然要求它们可翻译为 uintptr_t,请考虑一下!

【讨论】:

  • 非常感谢,现在我对所有这些意味着什么有了“更好”(因为我认为甚至不接近实际情况)的图片。很好很清楚的解释
【解决方案4】:

除非它是sizeof 或一元&amp; 运算符的操作数,或者是用于在声明中初始化字符数组的字符串文字,否则表达式 类型为“N- T" (T [N]) 的元素数组被转换 ("decays") 为 "pointer to T" (T *) 类型的表达式,表达式的值是第一个元素的地址数组。

Array objects 不是指针。如果你声明一个数组像

char foo[] = "hello";

它在内存中看起来像这样(地址仅用于说明):

        +–––+
0x1000: |'h'|
        +–––+
0x1001: |'e'|
        +–––+
0x1002: |'l'|
        +–––+
0x1003: |'l'|
        +–––+
0x1004: |'o'|
        +–––+
0x1005: | 0 |          
        +–––+

对象 foo 不是指针;它没有为指针留出任何空间。 表达式 foo 在大多数情况下都会转换为指针,包括作为函数参数传递时:

uc( foo );

uc 收到的是第一个元素的地址,因此是声明

void uc( char *s ) { ... }

至于下标[]操作符,也是一样的——数组表达式被转换为指向第一个元素的指针,下标操作应用于那个指针。下标操作定义

a[i] == *(a + i)

给定一个起始地址a,计算该地址之后的第i'个指向类型对象的地址(不是i'第一个字节)结果。

所以结果是肯定的,您可以在指针表达式和数组表达式上使用[] 下标运算符。

指针必须表示为整数 - 在一些较旧的分段架构中,它们表示为一对值(页码和偏移量)。此外,指向不同类型的指针可能具有不同的表示形式 - 例如,char * 可能看起来不像 int *,它可能看起来不像 double *,等等。在 x86 等桌面系统上它们会这样做,但不能保证.

编辑

来自评论:

当像这样初始化一个 int 向量时:for( int i=0; i &lt; size; ++i); scanf("%d", &amp;vector[i]) 计算器是否使用这个指针“机制”来循环低谷?

是的,没错。 scanf 期望与%d 转换说明符对应的参数是int 对象的地址,这意味着int * 类型的表达式。一元&amp;操作符返回一个对象的地址,所以假设vector已经被声明了

int vector[N]; // for some value of N

那么表达式&amp;vector[i]的计算结果为数组中第i'个元素的地址,表达式的类型为int *

请记住,C 传递所有函数参数按值 - 函数定义中的形参与函数调用中的实参在内存中是不同的对象。例如,给定

void foo( T x ) // for any type T
{ 
  x = new_value;
}

void bar( void )
{
  T var;
  foo( var );
}

foo 中的形参xvar 是不同的内存对象,因此更改为x 不会影响var。如果我们希望foo 能够写入var,那么我们必须传递一个指向它的指针:

void foo( T *ptr )
{
  *ptr = new_value; // write a new value to the thing ptr *points to*
}

void bar( void )
{
  T var;
  foo( &var ); writes a new value to var
}

*ptr = new_value 中的一元 * 运算符 取消引用 ptr,因此foo 中的表达式 *ptr 等价于var

*ptr ==  var  // T   == T
 ptr == &var  // T * == T *

声明中,* 仅表示 object ptr 具有指针类型 - 它不会取消引用,因此您可以编写类似

int x;
int *ptr = &x; // ptr is *not* being dereferenced
int y = 5;
*ptr = y;      // ptr *is* being dereferenced

【讨论】:

  • 谢谢你的回答,我不应该在 cmets 中问“子问题”,但是在初始化这样的 int 向量时:for( int i=0; i &lt; size; ++i); scanf("%d", &amp;vector[i]) 计算器是否使用这个指针“机制”来循环槽?
猜你喜欢
  • 2021-07-03
  • 2020-11-04
  • 2017-01-19
  • 1970-01-01
  • 2016-11-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多