深入理解C语言指针和数组

深入理解C语言指针和数组


一. 指针的本质

1. 指针是什么

  • 指针就是地址
  • 地址的本质是什么?地址是数据
  • 那么数据可不可以被放在空间内保存?当然可以
(1)如何看待下面代码中的a变量
#include<stdio.h>

int main() {
    int a = 0;
    a = 10;    // (1)
    int b = a;  // (2)
    return 0;
}
  • (1)处是将a用作左值,也就是使用了a的地址空间,将10存储到a的内存空间中

  • (2)处是将a用作右值,也就是使用了a的值,将其赋给变量b

重新理解变量:定义一个变量,本质是在内存中根据类型来进行开辟空间。有了空间,就必须有地址来标识空间,来方便CPU寻址。

(2)指针变量的概念
  • 保存指针(地址)数据的变量就是指针变量
(3)指针和指针变量有何不同
  • 严格意义上说,指针和指针变量是不同的
  • 指针就是地址值
  • 指针变量是变量,要在特定区域开辟空间,来存放数据,可以被取地址(&)
(4)口语中的"定义一个指针"定义的是什么
  • 定义一个指针实际上是定义了一个指针变量
  • 因为指针不能单独定义,必须依附于某个变量
(5)为什么要有指针
  • 为了提升CPU的寻址效率

2. 指针内存布局和解引用

(1)指针内存布局
#include<stdio.h>

int main() {
    int a = 10;
    int *p = &a;
    return 0;
}

问题解析:

  1. 这里定义了几个变量?在哪里定义的?
    • 共定义了两个变量:整型变量和指针变量
    • 由于在函数内定义,是局部变量,因此定义在栈区
  2. 一个整型有4个字节,那么应该有4个地址!那么&a取了哪一个地址?如何全部访问这4个字节?
    • &a取的是四个连续字节中的最低地址
    • 然后根据类型连续访问四个字节
(2)指针地址指向图

image-20250809200317948

(3)解引用
#include<stdio.h>

int main()
{
    int a = 10;
    int *p = &a;
    
    int b = *p;//(1)
    *p = 20;   //(2)
        
    return 0;
}
  • (1)是将p后的值赋给了b 将p当作右值使用

  • (2)是访问的p所指向的目标所对应的空间 把p当作左值使用

*p的完整理解是: 取出p中存储的地址 访问该地址所指向的内存空间(通过指针变量访问 间接寻址)

二. 数组

1. 数组的基本概念和内存布局

(1) 数组概念
  • 数组是具有相同数据类型的集合(注: 数组的元素个数 也是数组类型的一部分)
#include<stdio.h>

#define N 10

int main()
{
    int a[N] = { 0 };//定义并初始化数组

    return 0;
}
(2) 数组内存布局
  • 在理解数组的内存布局前 先来看一下这段代码
#include<stdio.h>

#define N 10

int main()
{
    int a = 10;
    int b = 20;
    int c = 30;

    printf("a addr: %p\n", &a);
    printf("b addr: %p\n", &b);
    printf("c addr: %p\n", &c);

    return 0;
}

//代码运行结果
a addr: 0096FB58
b addr: 0096FB4C
c addr: 0096FB40
  • 可以发现 先定义的变量 地址是较大的 后续依次减小
  • 这是因为三个变量都在main函数内定义 都是开辟在栈区的临时变量 a先定义 意味着a先开辟空间 所以a先入栈 a的地址最高

image-20250810110715218


数组内存布局

#include<stdio.h>

#define N 10

int main()
{
    int a[N] = { 0 };

    for (int i = 0; i < N; i++)
    {
        printf("a[%d] addr: %p\n", i, &a[i]);
    }

    return 0;
}

//代码运行结果
a[0] addr: 00D5FE60
a[1] addr: 00D5FE64
a[2] addr: 00D5FE68
a[3] addr: 00D5FE6C
a[4] addr: 00D5FE70
a[5] addr: 00D5FE74
a[6] addr: 00D5FE78
a[7] addr: 00D5FE7C
a[8] addr: 00D5FE80
a[9] addr: 00D5FE84
  • 可以发现 数组内存排布: &a[0] < &a[1] < … < &a[8] < &a[9]

  • 与单个变量开辟空间不同 数组开辟空间规则是: 现在栈区整体开辟一块空间 然后将最低地址的空间 作为a[0]元素 依此类推

image-20250810164011925


理解指针+1

#include<stdio.h>

#define N 10

int main()
{
    char* c = NULL;
    short* s = NULL;
    int* i = NULL;
    double* d = NULL;

    printf("%d\n", c);		//0
    printf("%d\n\n", c + 1);//1

    printf("%d\n", s);		//0
    printf("%d\n\n", s + 1);//2

    printf("%d\n", i);		//0
    printf("%d\n\n", i + 1);//4

    printf("%d\n", d);		//0
    printf("%d\n\n", d + 1);//8


    return 0;
}
  • 根据结果来看 可知: 对指针+1 本质加上其所指向类型的大小

理解&a[0] &a的区别

#include<stdio.h>

#define N 10

int main()
{
    int arr[N] = { 0 };

    printf("%p\n", &arr[0]);    //首元素的地址

    printf("%p\n", &arr[0] + 1);//第二个元素的地址

    printf("%p\n", &arr);       //数组的地址

    printf("%p\n", &arr + 1);   //下一个数组的地址

    return 0;
}
  • &a[0]表示数组首元素的地址 &a表示数组的地址

报错!!!

#include<stdio.h>

#define N 5

int main()
{
    int arr[N] = { 0 };

    arr = {1,2,3,4,5}

    return 0;
}

  • 数组名可以做右值 表示数组首元素的地址 数组名不可做左值 做左值必须有空间可以修改 arr不可以整体使用 只能按照元素单位用

数组名只有在 &数组名和sizeof中单独使用才表示数组的地址 其余大部分时候均表示为数组首元素的地址

2. 指针和数组的关系

(1) 以指针形式访问和以数组形式访问
#include<stdio.h>
#include<string.h>

int main()
{
    char* str = "abcdef"; //str指针变量在栈上保存 "abcdef"在字符常量区保存 不可被修改
    char arr[] = "abcdef";//整个数组都在栈上保存 可以被修改

    //1.以指针的方式访问指针 和 以下标的方式访问指针
    printf("以指针的方式访问指针 和 以下标的方式访问指针\n");
    int len = strlen(str);
    for (int i = 0; i < len; i++)
    {
        printf("%c", *(str + i)); //指针+1 加上其指向类型的大小
        printf("%c", str[i]);
    }
    printf("\n");

    printf("以指针的方式访问数组 和 以下标的方式访问数组\n");
    for (int i = 0; i < (int)strlen(arr); i++)
    {
        printf("%c", *(arr + i));//数组名单独使用表示数组首元素的地址
        printf("%c", arr[i]);
    }

    return 0;
}
  • 结论: 指针和数组 在访问多个连续元素时 可以指针解引用方案 也可以[]方案
(2) 为什么要这样设计? (将指针和数组的元素访问设计成通用的方式)
//首先来看这样一段代码
#include<stdio.h>

//void ShowArr(int arr[], int n)
//如果没有打通访问方式 那么这里就需要写成int *arr 在遍历内容时 只能使用*方式
void ShowArr(int *arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", *(arr + i));
	}
	printf("\n");
}

int main()
{
	int arr[] = { 1, 2, 3, 4, 5 };
	int num = sizeof(arr) / sizeof(arr[0]);

	ShowArr(arr, num);

    //在数组的定义处想要访问 只能通过[]方式访问
    for(int i = 0; i < num; i++)
    {
        printf("arr[%d]: %d ", i, arr[i]);
    }
    
	return 0;
}

//通过上述描述得知 如果不通用  程序员需不断地在不同代码块中 进行习惯切换 会增加代码错误的概率

3 指针和数组传参

(1) 一维数组传参降维问题
  • 问题: 在ShowArr函数中的arr还是一个数组类型吗? 并不是

image-20250810220710840

  • 由图可以看出 在ShowArr函数内的arr是一个指针类型指针类型 这是为什么呢?
  1. 数组传参 会发生降维 降维成指针
  2. 为什么要降维? 如果不降维 就要发生数组拷贝 函数调用效率降低
  3. 降维成什么? 降维成为指向其内部元素类型的指针
  • 为什么在函数调用时要发生降维?
  1. C语言作为面向过程的语言 函数调用时必然发生形参实例化

  2. 实例化过程需要对实参进行临时拷贝 (即值传递)

  3. 若直接拷贝整个数组 如果降维成指针 只需要拷贝4个字节(32位环境)

    void process(int arr[1000]) { 
        // 调用时需拷贝1000*sizeof(int)字节
    }
    
(2) 一级指针传参
  • 问题: 指针作为参数 要不要发生拷贝
#include<stdio.h>

void test(char* p)
{
	printf("test: &p = %p\n", &p);//test: &p = 00EFF904
}

int main()
{
	char* p = "hello world";
	printf("main: &p = %p\n", &p);//main: &p = 00EFF9D8
	test(p);

	return 0;
}
  1. 需要发生拷贝 指针变量也是变量 也要符合变量要求 进行临时拷贝
  2. 上述这段代码 相当于将p保存的字符串"hello world"的地址传入test函数 用来初始化test函数中的指针变量p

三 多维数组和多级指针

1. 二维数组

(1)基本理解
  • 大部分的书中所画的二维数组 都是矩阵的样子
  • 在这里要澄清 书中的图 只能算示意图 并非真正的内存布局图
  • 可以想象一下 如果按照书中的理解 那么三维数组 四维数组又该怎么画呢?
(2)基本内存布局
#include<stdio.h>

int main()
{

	char a[3][4] = { 0 };

	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++) {
			printf("a[%d][%d]: %p\n", i, j, &a[i][j]);
		}
	}

	return 0;
}

//代码运行结果
a[0][0]: 00AFFBA0
a[0][1]: 00AFFBA1
a[0][2]: 00AFFBA2
a[0][3]: 00AFFBA3
a[1][0]: 00AFFBA4
a[1][1]: 00AFFBA5
a[1][2]: 00AFFBA6
a[1][3]: 00AFFBA7
a[2][0]: 00AFFBA8
a[2][1]: 00AFFBA9
a[2][2]: 00AFFBAA
a[2][3]: 00AFFBAB
  • 根据运行结果可以看出 二维数组的内存空间排布上 也是线性连续且递增
(3) 二维数组内存布局图
//以它为例: char[3][4] = { 0 };

image-20250811172007638

  • 由上图可知 二维数组 a[3] [4] 是由四个一维数组a[4]组成的 a[4]又是由四个char组成

为什么要这样画图?

  1. 数组的定义是: 具有相同类型元素的集合 特征是: 数组中可以保存任意类型(说明数组内也可以保存数组)
  2. 在理解上 也可以把所有数组都理解为一维数组 (二维数组可以看成一维数组 内部元素也是数组)
(4) 二维数组认知练习
#include<stdio.h>

//32位环境下

int main() 
{
    int a[3][4] = { 0 };
    printf("%zd\n", sizeof(a));           //数组名在sizeof中单独出现 表示整个数组 48 
    printf("%zd\n", sizeof(a[0][0]));     //第一个元素(一维数组)内的第一个整形 4
    printf("%zd\n", sizeof(a[0]));        //a[0]表示为第一个元素的数组名 单独使用 表示整个数组 16
    printf("%zd\n", sizeof(a[0] + 1));    //a[0]表示为第一个元素的数组名 未单独使用 表示首元素的地址(指针) 4
    printf("%zd\n", sizeof(*(a[0] + 1))); //二维数组内部的一维数组的第一个元素 4
    printf("%zd\n", sizeof(a + 1));       //第二个元素(数组)的地址 数组指针类型 4
    printf("%zd\n", sizeof(*(a + 1)));    //第二个元素 16
    printf("%zd\n", sizeof(&a[0] + 1));   //第二个元素(数组)的地址 4
    printf("%zd\n", sizeof(*(&a[0] + 1)));//第二个元素 16
    printf("%zd\n", sizeof(*a));          //*是操作符 数组名在表达式内出现 首元素地址 * 第一个元素 16
    printf("%zd\n", sizeof(a[3]));        //如果打印实际值 是无效的 因为越界了 但sizeof只关心类型 第四个元素 16
    
    return 0;
}

#include<stdio.h>

int main()
{
	int a[5][5];
	int (*p)[4];

	p = a;

	printf("a_ptr=%p,p_ptr=%p\n", &a[4][2], &p[4][2]);
	printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);//FFFFFFFC -4

	return 0;
}

image-20250811215000170

2. 二级指针

(1) 初步理解
  • 指针变量也是变量 是变量就有地址
  • 二级指针保存的就是某个“一级指针变量”的地址 也就是指针的指针
(2) 二级指针代码理解
#include<stdio.h>

int main()
{
	int a = 10;
	int* p = &a;
	int** pp = &p;
	p = 100;    //将p保存的地址改为100

	*p = 100;   //将p保存的a的地址所指向的内容更改为100

	pp = 100;   //将pp保存的地址更改为100 

	*pp = 100;  //将pp保存的地址所指向的内容改为100

	**pp = 100; //将pp保存的地址所指向的地址所指向的内容改为100

	return 0;
}

3. 二维数组参数和二级指针参数

(1) 二维数组传参
#include <stdio.h>

void show(char a[][4])
{
	printf("show: %d\n", sizeof(a));
}

int main()
{
	char a[3][4] = { 0 };
	printf("main: %d\n", sizeof(a));
	show(a);

	return 0;
}
  • 二维数组可以理解为一维数组 一维数组内部元素也是数组

  • 所以二维数组传参也会发生降维 降维成数组指针

  • 并且在接受参数时 第一维度可以省略 第二维度不可省略 因为char a[3] [4]这个数组类型时char [*] [4] 数组元素个数也属于数组类型的一部分

#include <stdio.h>

void show(char a[][4])
{
	printf("show: %d\n", sizeof(a));
}

int main()
{
	char a[3][4] = { 0 };
	printf("main: %d\n", sizeof(a));
	show(a);

	return 0;
}
  • 二维数组可以理解为一维数组 一维数组内部元素也是数组

  • 所以二维数组传参也会发生降维 降维成数组指针

  • 并且在接受参数时 第一维度可以省略 第二维度不可省略 因为char a[3] [4]这个数组类型时char [*] [4] 数组元素个数也属于数组类型的一部分

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值