欢迎光临
我们一直在努力

一篇文章讲清楚数组(数组名、&数组、字符串等)与指针

在讲数组和指针之前,我想先讲讲关于变量的内存分配。

int a = 10;

变量在分配内存时发生了什么。

1.分配4个连续的内存地址(一个字节一个地址),

2.将10的二进制(32位由0或1写成的数字)

  • 分成 4 个字节:00000000 | 00000000 | 00000000 | 00001010

  • 每个字节占 8 位,存储在连续的 4 个地址中

但是C语言里:int 的地址是它的起始地址,通过这个地址可以访问到完整的 4 个字节。

所以对于地址为起始地址的标识符,利用指针变量,存储的都是其起始地址。

一、指针和数组的官方定义

        一、什么是指针

指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

type *var_name;

在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。

c

int a = 10;
int *p = &a; // 把 a 的地址赋值给 p

p存的是a变量的地址,*p 是“解引用”——通过 p 里存的地址,去找到那个地址上存的值10;

那么用%p打印p会得到变量a的起始地址,一个十六位进制的数。

        二、指针的运算

1.核心运算:加减法和自增自减

这是最常见的运算,步长完全取决于指针类型。

  • int *p; p + 1;:偏移 sizeof(int) 字节(通常4字节),指向下一个整数。

  • char *p; p + 1;:偏移 sizeof(char) 字节(固定1字节),指向下一个字符。

  • double *p; p + 1;:偏移 sizeof(double) 字节(通常8字节)。

代码直观对比(假设地址从 1000 开始):

int arr[5] = {10, 20, 30, 40, 50};
int *p_int = arr; // p_int = 1000
char *p_char = (char*)arr; // p_char = 1000

printf("%p\\n", p_int + 1); // 输出 1004 (跳过了4个字节)
printf("%p\\n", p_char + 1); // 输出 1001 (跳过了1个字节)

这完美解释了你上问中 &array + 1 为何跳过整个数组——因为 &array 的类型是“整个数组的指针”,sizeof(整个数组) 就是步长。

2. 指针相减(求间隔)

当两个指针指向同一块连续内存(比如同一个数组)时,它们可以相减,结果是一个 ptrdiff_t 类型的整数,代表中间隔了多少个元素,而不是字节数。

int arr[10];
int *p1 = &arr[0]; // 指向第0个
int *p2 = &arr[5]; // 指向第5个

printf("%ld\\n", p2 – p1); // 输出 5 (隔了5个元素)
printf("%ld\\n", p1 – p2); // 输出 -5 (顺序相反)

注意:如果两个指针指向毫无关联的独立变量(如 &a – &b),这种运算是未定义行为,虽然能编译但结果毫无意义,绝不要这样写。

3. 指针比较(判断先后)

指向同一数组的两个指针,可以使用关系运算符(>, <, >=, <=, ==)进行比较。

for (int *p = arr; p < arr + 5; p++) { // 合法且优雅
printf("%d ", *p); // 遍历数组
}

比较结果代表地址在内存空间中的先后次序。指向高地址的指针更大。

二、数组

        一、数组的定义

定义: 数组是一种由相同类型元素构成的、非空的、在内存中连续存储的数据类型。

语法: int arr[n]; 定义了一个名为 arr 的数组,其包含 n个类型为 int类型 的元素。

特点:

  • 它的类型由“元素类型”和“元素个数”共同决定。例如 int[10] 和 float[10] 是不同类型;int[5] 和 int[10] 也是不同类型。

  • 它的总大小(字节数)被严格锁定为 元素个数 × 单个元素大小(即 sizeof(数组) = 元素个数 * sizeof(元素类型))。

  • 它的内存地址是连续的。所有元素在内存中连续排列,地址依次递增。

        二、数组名的定义(细讲)

在 C 语言中,数组名表示数组的地址,即数组首元素的地址。当我们在声明和定义一个数组时,该数组名就代表着该数组的地址。

例如,在以下代码中:

int myArray[5] = {10, 20, 30, 40, 50};

在这里,myArray 是数组名,它表示整数类型的数组,包含 5 个元素。myArray 也代表着数组的地址,即第一个元素的地址。

               三、&数组名和&数组名[下标]

数组名本身是一个常量指针,意味着它的值是不能被改变的,一旦确定,就不能再指向其他地方。

我们可以使用&运算符来获取数组的地址,如下所示:

int myArray[5] = {10, 20, 30, 40, 50};
int *ptr = &myArray[0]; // 或者直接写作 int *ptr = myArray;

在上面的例子中,ptr 指针变量被初始化为 myArray 的地址,即数组的第一个元素的地址。


int arr[5]={1,2,3,4,5};
printf("%p",&arr);

&是取地址符,&arr,代表整个数组的起始内存地址。当数组名作为 & 运算符的操作数时,它不会退化为指向首元素的指针。这是C标准(C11)中一个明确的“豁免”规则。

其结果是一个指向整个数组的指针。

但是根据数组的定义,数组的元素地址分配是一个连续的,而计算机中规定,每一块连续区域的“起始地址”,指的就是它的第一个字节物理编号,即第一个元素的地址。

所以&arr和&arr[0]打印的值相同。

但是二者的内存长度不相同,sizeof &arr是整个内存的长度20个字节,而sizeof &arr[0]是4个字节(int型是4个字节)。

        四、数组名的退化

 在绝大多数表达式中,数组名会隐式转换为一个指向其首元素的指针,类型为 int*,且值为 &arr[0]。这个过程称为“数组退化”。

规则例外: 数组名不发生退化的两种特殊情况:

  • 作为 sizeof 的操作数(sizeof(arr) 计算的是整个数组的大小)。

  • 作为取地址运算符 & 的操作数(&arr 的类型是 int (*)[n])。

  • 原因:& 的作用是“取地址”,它的产出是一个“指针”。如果arr进行退化,那么&arr相当于时**arr,进行两次“解引”。

    数组名的退化现象

    int arr[5] = {1, 2, 3, 4, 5};

    // 情况1:表达式中的 arr 退化为了 int*
    int *p = arr; // 编译通过,arr 退化为 &arr[0]

    // 情况2:sizeof(arr) 中 arr 未退化,保留完整数组类型
    printf("sizeof(arr) = %zu\\n", sizeof(arr)); // 输出 20

    // 情况3:&arr 中 arr 未退化,得到的是整个数组的地址,类型 int(*)[5]
    int (*q)[5] = &arr; // 编译通过,&arr 的类型为 int(*)[5]


       五、arr 与 &arr 的深度对比

    1. 值相同,类型不同

    虽然 arr(退化为指针)和 &arr 打印出的地址数值相同,但它们属于完全不同的类型,这直接导致指针运算的步长截然不同。

    • arr 的类型:int*(指向单个 int 的指针)。

    • &arr 的类型:int (*)[N](指向“包含 N 个 int 的数组”的指针)。

    int arr[5] = {1, 2, 3, 4, 5};

    printf("arr = %p\\n", arr); // 假设输出 0x1000
    printf("&arr = %p\\n", &arr); // 输出 0x1000 (值相同)

    printf("arr + 1 = %p\\n", arr + 1); // 输出 0x1004 (跳过 1 个 int,4 字节)
    printf("&arr + 1 = %p\\n", &arr + 1); // 输出 0x1014 (跳过整个数组,20 字节)

    分析:

    • arr + 1:因为类型为 int*,指针加 1 增加 sizeof(int) 即 4 字节。

    • &arr + 1:因为类型为 int (*)[5],指针加 1 增加 5 * sizeof(int) 即 20 字节,越过整个数组。&会产生一个指针,所以&arr此刻是指针类型,sizeof &arr即8字节。


    三、指针和数组之间的联系

            一、相同点

    相同点1:在绝大多数表达式中,数组名退化为首元素地址,其数值与指针变量保存的数值相同。

    int arr[5] = {0};
    int *p = arr;
    printf("%p\\n", arr); // 输出:0x7ffc12345670
    printf("%p\\n", &arr[0]); // 输出:0x7ffc12345670
    printf("%p\\n", p); // 输出:0x7ffc12345670

    //arr、&arr[0]、p 三个打印值完全相同。


    相同点 2:都支持下标访问 []、解引用 *、与整数加减 +/-,且底层统一转换为地址运算。

    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;

    // 下标访问
    printf("%d\\n", arr[2]); // 输出:30
    printf("%d\\n", p[2]); // 输出:30

    // 解引用
    printf("%d\\n", *arr); // 输出:10
    printf("%d\\n", *p); // 输出:10

    // 与整数加减
    printf("%d\\n", *(arr + 3)); // 输出:40
    printf("%d\\n", *(p + 3)); // 输出:40

    // 事实:arr 和 p 在 []、*、+/- 三种运算上行为完全一致。


    相同点 3:都支持关系比较运算符(==、!=、<、>、<=、>=),用于判断地址先后或是否相等。

    int arr[5] = {0};
    int *p = arr;

    // 比较地址是否相等
    if (arr == p) {
    printf("arr == p 成立\\n"); // 输出:arr == p 成立
    }

    // 比较地址先后(指向同一数组时有效)
    if (p < arr + 5) {
    printf("p 在数组范围内\\n"); // 输出:p 在数组范围内
    }

    if (arr + 5 > p) {
    printf("arr+5 在 p 之后\\n"); // 输出:arr+5 在 p 之后
    }

    // arr(退化后)和 p 均可以作为关系比较的操作数。


    相同点 4:都可以作为逻辑条件(如 if、while)直接判断,非空(非NULL)为真,空(NULL)为假。

    int arr[5] = {0};
    int *p = NULL;

    if (arr) {
    printf("arr 为真\\n"); // 输出:arr 为真(数组地址恒非0)
    }

    if (p) {
    printf("p 为真\\n"); // 不输出,因为 p 为 NULL
    }

    p = arr;
    if (p) {
    printf("p 重新赋值后为真\\n"); // 输出:p 重新赋值后为真
    }

    // 数组名退化的地址和指针变量都可以直接参与布尔判断。


    相同点 5:作为函数实参传递时,传递的都是地址值(按值传递地址)。

    void func(int *ptr) {
    printf("接收到的地址:%p\\n", (void*)ptr);
    }

    int main() {
    int arr[5] = {0};
    int *p = arr;

    func(arr); // 传递数组名,实际传入首元素地址
    func(p); // 传递指针变量,传入其保存的地址值

    // 输出两次地址完全相同。
    // 无论传 arr 还是传 p,函数接收到的都是同一个地址数值。


    二、不同点

    不同点 1:sizeof 的结果不同。对数组名返回整个数组字节数;对指针变量返回指针自身字节数(64位系统为8)。

    int arr[5] = {0};
    int *p = arr;

    printf("%zu\\n", sizeof(arr)); // 输出:20 (5个int × 4字节)
    printf("%zu\\n", sizeof(p)); // 输出:8 (64位系统指针大小为8)

    //若两者等价,sizeof 应该相同,但实际 20 != 8。

    不同点 2:数组名不可作为左值被赋值;指针变量可作为左值被修改。

    int arr[5] = {0};
    int *p = arr;
    int other[5] = {0};

    p = other; // 合法,编译通过
    // arr = other; // 编译报错(error: assignment to expression with array type)

    // p 可以指向新地址,arr 不能指向新地址。

    不同点 3:取地址 & 的结果类型不同。&arr 类型是 int (*)[5](数组指针);&p 类型是 int **(二级指针)。类型不同导致 +1 的跳跃步长不同。

    int arr[5] = {0};
    int *p = arr;

    // &arr 的类型是 int (*)[5],+1 跳跃整个数组(20字节)
    printf("%p\\n", &arr); // 假设输出:0x1000
    printf("%p\\n", &arr + 1); // 输出:0x1014 (0x1000 + 20 字节)

    // &p 的类型是 int **,+1 跳跃一个指针变量的大小(8字节)
    printf("%p\\n", &p); // 假设输出:0x2000
    printf("%p\\n", &p + 1); // 输出:0x2008 (0x2000 + 8 字节)

    // &arr 和 &arr+1 相差20字节;&p 和 &p+1 相差8字节。数值变化证明类型不同。

    不同点 4:作为函数形参时,数组声明会退化为指针声明。函数内部对形参使用 sizeof 得到的是指针大小,而非数组大小。

    // 形参写为 int arr[5] 或 int arr[],编译器强制改写为 int *arr
    void test(int arr[5]) {
    printf("%zu\\n", sizeof(arr)); // 输出:8 (指针大小)
    }

    int main() {
    int arr[5] = {0};
    test(arr);
    return 0;
    }
    // 若形参真的是数组,sizeof 应为20,但实际输出8,证明形参已退化成了指针变量。


    四、arr、&arr、&arr[0]、指针p一张表带你总结

    表达式值(地址)类型+1 跳过的字节数能否自增
    arr 0x1000 int* 4(1 个 int) ❌ 不能(arr++ 非法)
    &arr[0] 0x1000 int* 4 ❌ 不能(&arr[0]++ 非法)
    &arr 0x1000 int (*)[5] 20(整个数组) ❌ 不能(&arr++ 非法)
    p = arr 0x1000 int* 4 ✅ 能(p++ 合法)

    五、指针数组和数组指针

    一、什么是指针数组

    int *arr[n]

    定义: 一个数组,其元素均为指向int 类型的指针。

    运算优先级: [] 的优先级高于 *,因此 int *arr[n] 被解析为 (int *) arr[n],即 arr 是一个数组,元素类型为 int*。

    内存占用: sizeof(arr) = n * sizeof(int*)。

                    int*是8个字节,因为是指针类型。

    指针数组的创建与使用

    // 指针数组
    int num1=100,num2=200,num3=300;
    printf("看一下变量地址%p\\n", &num1);
    printf("看一下变量地址%p\\n",&num2);
    printf("看一下变量地址%p\\n", &num3);
    // 定义指针数组:
    // 指针数组:p 是数组,里面有 3 个int*类型元素(3个地址)
    // 一个存储三个地址的指针数组;
    int *prt[3] = {&num1, &num2, &num3};

    printf("那么prt占多少字节:%zu\\n",sizeof prt); // 3 * 8 = 24 (64位系统)

    for (int i = 0; i < 3; i++)
    {//尝试打印出里面的元素
    printf("里面存储的是:%p\\n", prt[i]);//打印的一串16进制数,地址。
    printf("数组的地址是:%p\\n", &prt[i]);//所以数组prt元素的地址是8个字节一个
    printf("里面指针解引的值是:%d\\n", *prt[i]);//解引用—获取地址存储的值
    printf("里面类型是:%zu\\n", sizeof prt[i]); // 8 指针类型
    printf("里面类型是:%zu\\n", sizeof &prt); // 此时&prt是指针
    //所以prt是数组,里面存储的是指针,各变量的地址
    }

    总结: 指针数组的每个元素都是一个独立的指针变量,可以指向不同的变量或内存。


    二、什么是数组指针:int (*p)[n]

    定义: 一个指针,指向一个包含 n 个 int类型元素的数组。

    运算优先级: 使用括号 () 强制 * 先与变量名结合,即 (*p) 是指针,指向的类型是 int[n]。

    内存占用: sizeof(p) 为一个指针的大小(64位系统下为 8 字节),无论数组元素多少。

    案例6:数组指针的赋值与解引用

    //创建数组指针
    int arr[5] = {1, 2, 3, 4, 5};

    // p 是一个指针,指向一个包含 5 个 int 的数组
    int (*p)[5] = &arr;

    // 解引用 p,得到整个数组实体(类型退化为 int*)
    //&arr表示整个数组的地址,&运算符会产生一个指针。

    // 注意:这里的解引用是将 p 指向的“整体数组”还原,而非访问元素。
    printf("p 指向的数组首元素: %d\\n", **p);
    // *p 得到数组首地址,再 * 得到第一个元素

    // 使用下标访问数组指针指向的元素
    for (int i = 0; i < 5; i++) {
    printf("(*p)[%d] = %d\\n", i, (*p)[i]); // (*p)[i] 等价于 arr[i]
    }

    // 验证 p 的移动步长
    printf("p 的值: %p\\n", p);
    printf("p + 1 的值: %p\\n", p + 1); // 地址增加 20 字节,跳过整个数组

    关于数组指针的一些知识点

    1.p*的一些特性

    int (*p)[5] = &arr;

    *p 的类型是数组,但数组在 C 语言中有一个特性:在大多数表达式中,它会“退化”为指向其首元素的指针。在 C 语言的类型系统中,*p 此刻(作为表达式的结果)的类型是 int[5](数组类型),它不是一个指针。

    • 当你写 *p 时,它代表那块 20 字节的内存,整个数组实体。

    • 当你写 *p + 1 时,*p 遇到了“表达式运算”,它临时退化为 int*,指向第一个元素,然后加 1 指向第二个元素。

    • 利用下标访问元素(语法糖 ——指针[下标],为*(指针+下标)),所以(*p)[n]===*((*p)+n),那么n=0时,**p是表示访问首元素。

    2.*p和p打印地址值相同,但类型不同

    • p 是一个指针变量(有存储空间,存地址)。

    • *p 是一个数组实体(没有独立的存储空间,它是 p 指向的那块内存的别名)。

           *p 代表的是 p 所指向的那个完整的数组对象。

    3.sizeof *p和sizeof arr是相同的;sizeof p和sizeof &arr是相同的;

    总结: 数组指针 p 存储的是数组整体的起始地址,*p 代表该数组实体,(*p)[i] 用于访问第 i 个元素。

    赞(0)
    未经允许不得转载:171主机测评 » 一篇文章讲清楚数组(数组名、&数组、字符串等)与指针
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址