深入理解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;
}
问题解析:
- 这里定义了几个变量?在哪里定义的?
- 共定义了两个变量:整型变量和指针变量
- 由于在函数内定义,是局部变量,因此定义在栈区
- 一个整型有4个字节,那么应该有4个地址!那么&a取了哪一个地址?如何全部访问这4个字节?
- &a取的是四个连续字节中的最低地址
- 然后根据类型连续访问四个字节
(2)指针地址指向图

(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的地址最高

数组内存布局
#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]元素 依此类推

理解指针+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还是一个数组类型吗? 并不是

- 由图可以看出 在ShowArr函数内的arr是一个指针类型指针类型 这是为什么呢?
- 数组传参 会发生降维 降维成指针
- 为什么要降维? 如果不降维 就要发生数组拷贝 函数调用效率降低
- 降维成什么? 降维成为指向其内部元素类型的指针
- 为什么在函数调用时要发生降维?
-
C语言作为面向过程的语言 函数调用时必然发生形参实例化
-
实例化过程需要对实参进行临时拷贝 (即值传递)
-
若直接拷贝整个数组 如果降维成指针 只需要拷贝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;
}
- 需要发生拷贝 指针变量也是变量 也要符合变量要求 进行临时拷贝
- 上述这段代码 相当于将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 };

- 由上图可知 二维数组 a[3] [4] 是由四个一维数组a[4]组成的 a[4]又是由四个char组成
为什么要这样画图?
- 数组的定义是: 具有相同类型元素的集合 特征是: 数组中可以保存任意类型(说明数组内也可以保存数组)
- 在理解上 也可以把所有数组都理解为一维数组 (二维数组可以看成一维数组 内部元素也是数组)
(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;
}

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] 数组元素个数也属于数组类型的一部分
1144

被折叠的 条评论
为什么被折叠?



