数组与指针

  1. 数组和指针的关系:在 C 语言中,数组名其实是一个指针,指向数组的第一个元素。例如,如果你有一个数组 int arr[10];,那么 arr 实际上是指向 arr[0] 的指针。这意味着你可以用指针来遍历数组,也可以通过指针操作数组元素。

  2. 通过指针访问数组:当你理解了数组名是一个指针后,你就能明白为什么 *(arr + 1) 等价于 arr[1]。这个表达式的意思是“通过指针 arr,向后移动一个位置,然后取该位置的值”。

  3. 指针的灵活性:数组在定义时大小是固定的,而指针则更为灵活,可以指向任何内存位置。这使得你在处理数据时,能够更灵活地进行操作,例如动态分配内存或处理不同类型的数据结构。

  4. 内存理解:通过理解指针如何操作数组,你会对内存管理有更深入的认识。这在 C 语言中尤为重要,因为你需要手动管理内存,避免错误操作。

把数组和指针放在一起学习,有助于更深入地理解两者的工作原理,可以更好地理解内存和数据访问的底层机制,让编程更加高效和可靠。

数组

一维数组

数组初始化中的常量表达式:数组名 [常量表达式](通常为常量,或宏定义的符号常量)表示数组的大小,这是因为一个函数的函数栈空间大小是固定的

数组元素的引用方式是 数组名 [下标](从 0 开始)访问数组中的数据

数组在内存中的存放

数组存放在一块连续的内存中,元素间的地址根据其数据类型所占字节数决定。

访问越界 (Out-of-Bounds Access)

C 语言的内存管理:先定义的变量在高地址,后定义的变量在低地址。

#include <stdio.h>

int main() {
    int i = 10; // 定义一个整数变量 i,并赋值为 10
    int arr[3] = {1, 2, 3}; // 定义一个大小为 3 的整数数组

    printf("Before out-of-bounds access:\n");
    printf("i = %d\n", i);
    printf("arr[0] = %d, arr[1] = %d, arr[2] = %d\n", arr[0], arr[1], arr[2]);

    // 访问数组的第 4 个元素(越界访问)
    arr[3] = 20;

    printf("\nAfter out-of-bounds access:\n");
    printf("i = %d\n", i); // 查看 i 的值是否被改变
    printf("arr[0] = %d, arr[1] = %d, arr[2] = %d\n", arr[0], arr[1], arr[2]);

    return 0;
}

当访问越界发生时,新定义的数组元素可能覆盖了先定义的变量i的地址(高地址),i的值(该地址中的值)发生改变。

可能的输出:

Before out-of-bounds access:
i = 10
arr[0] = 1, arr[1] = 2, arr[2] = 3

After out-of-bounds access:
i = 20 // i的值可能被改变
arr[0] = 1, arr[1] = 2, arr[2] = 3

在不同的环境中,越界行为是未定义的,结果可能会有所不同,甚至可能导致程序崩溃。为避免这种错误,始终要确保在使用数组时不越界访问。

二维数组

二维数组可以看作是一种特殊的一维数组,按行连续存放在内存中。

注意:二维数组的数组名 —— 指向一个一维数组的指针

函数的值传递:二维数组打印

#include <stdio.h>
void print(int a[][4], int row) 
{
	int i,j;
	for (i = 0; i < row; i++) 
	{
		for(j=0; j<sizeof(a[0])/sizeof(int); j++)
		{
			printf("%3d", a[i][j]);
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
	print(arr, 3);
	system("pause");
}

像一维数组的大小一样,二维数组的“行”不能直接传递到子函数中,二维数组在传递时是弱化成的,因此需要把“列”写入到形参中才能通过编译。

指针

编译时,系统给变量分配内存地址。

指针用于指向内存地址,并可以通过直接或间接的方式访问和修改地址内的变量。

指针也是一个变量,称为指针变量,

指针的使用场景:传递和偏移

一级指针

一级指针的传递

例:使用子函数改变主函数中变量的值。

void change(int *j)
{
	*j = 5;
}

int main()
{
	int i = 10;
	change(&i);
    //把变量i的地址传递给子函数中的指针变量j
    printf("%d\n, i);
}

若使用简单的值传递,由于子函数与主函数的变量所在的栈空间不同,当 change 函数执行结束时,子函数占用的栈空间得到释放。对子函数中局部变量的操作不影响主函数中的变量。

指针的偏移与自增

指针的偏移是为连续的数组元素服务的。

#include <stdio.h>

int main()
{
    int a[5] = {1, 3, 5, 7, 9}; // 定义一个包含5个元素的数组a
    int *p;                      // 定义一个指向整数的指针p
    int i;

    p = a; // p指向数组a的第一个元素,即p = &a[0],a[0]的地址

    i = *p++; 
    // *p 取当前指针p指向的值,也就是从地址&a[0]找到a[0]的值1;
    // 然后p++,指针p自增,指向数组的下一个元素a[1];
    // 最后将a[0]的值1赋给变量i,所以此时i = 1。

    i = p[0]++;
    // p[0]表示当前p指向的元素的值,指针p在上一步之后自增指向数组a的第二个元素,即a[1]的值3;
    // 先将p[0]的当前值3赋给i,所以i = 3;
    // 然后执行自增操作p[0]++,也就是p[0]=p[0]+1,将p[0]的值3增加1,即a[1]的值变为4。
}

指针偏移:p++ 将指针 p 从指向 a[0] 变为指向 a[1]。这种操作使得你可以遍历数组元素,而不需要直接使用数组下标。

p++p[0]++ 是两种不同的自增操作。

p++ 是指针自增,使指针指向下一个元素。

p[0]++ 是元素自增,先取当前元素的值,再将该元素值增加 1。

数组指针与二维数组

数组指针是一个指向行数组的指针变量,服务于二维数组的传递与偏移。

数组指针本质上仍是一级指针。

int main()
{
    int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
    int b[4]={1,2,3,4};
    int (*p)[4];
    //指向长度为4的行数组
    int i,j;
    p = a;
    for(i = 0; i < 3; i++)
    {
        for(j = 0; j < 4; j++)
        {
            printf("%3d", *(*(p+i)+j));
        }
    }
}

二维数组的偏移

表示形式 含义 地址值
a 二维数组名,指向行数组a[0] 0x2000
a[0], *(a+0), *a 0行0列元素地址 0x2000
a+1, &a[1] 1行的首地址 0x2010
a[1], *(a+1) 1行0列元素a[1][0]的地址 0x2010
a[1]+2, *(a+1)+2, &a[1][2] 1行2列元素a[1][0]的地址 0x2018
*(a[1]+2), *( *(a+1)+2), a[1][2] 1行2列元素a[1][0]的值 元素的值为13

&a+1 对数组名取地址后 +1,指针偏移到数组末尾

二级指针

二级指针服务于一级指针的传递与偏移。

使用场景:在子函数(定义形参为二级指针)中改变主函数的一级指针指向(值)。

void change(int** p1, int* p2)
{
	*p1 = p2;
}

int main()
{
	int i=10;
	int j=5;
	int *pi = &i;
	int *pj = &j;
	printf("i=%d,j=%d,*pi=%d,*pj=%d\n",i,j,*pi,*pj);
	change(&pi,pj);
    //把变量j的值赋给变量i
	printf("i=%d,j=%d,*pi=%d,*pj=%d\n",i,j,*pi,*pj);
}

指针数组

二级指针的偏移服务于指针数组。

例,冒泡排序(索引式排序)

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

#define N 5
int main()
{
	char b[N][10]={"Google","Facebook","Microsoft","HUAWEI","Apple"};
	char *p[N];
	char **p2;
	int i,j;
	char *temp;
	p2 = p;
	for (i = 0; i < N; i++)
	{
		p2[i]=b[i];
	}
	for (i = 0; i < N; i++)
	{
		printf("%s\n",p2[i]);
	}
	printf("After sort:\n");
	for (i = N; i > 0; i--)
	{
		for (j = 0; j < i - 1; j++)
		{
			if (strcmp(p[j], p[j + 1])>0)
			{
				temp = p[j+1];
				p[j+1] = p[j];
				p[j] = temp;
			}
		}
	}
	for (i = 0; i < N; i++) 
	{
		printf("%s\n",p[i]);
	}
	printf("-----------------\n");
	for (i = 0; i < N; i++)
	{
		printf("%s\n", b[i]);
	}
	system("pause");
}

通过交换指针(指向的地址值)的顺序,对来数据量较大的结构体进行排序操作,以减小交换成本。

指针数组与其他数组一样,无法将长读传递给子函数。

实际工作中,排序数据较多时,需要为指针数组申请动态内存空间。

p = (char **)malloc(sizeof(char *) *5);