前言
本章内容总结了数组、指针的内容。

C语言 | 快速了解C的发展史🧡💛💚💙 C语言 | 【耗费一夜总结三本C语言系列】之 指针、数组 一文透彻~~~🧡💛💚💙 C语言 | 【耗费一夜总结三本C语言系列】之 结构体、联合、枚举🧡💛💚💙 C语言 | 【耗费一夜总结三本C语言系列】之 声明🧡💛💚💙 C语言 | 【耗费一夜总结三本C语言系列】之 作用域 在也不用担心分不清变量的作用域拉!!!🧡💛💚💙 C语言 | 【耗费一夜总结三本C语言系列】之 编译步骤 会用C还不知道C如何编译???🧡💛💚💙 C语言 | 【耗费一夜总结三本C语言系列】之 数据类型总结🧡💛💚💙 C语言 | 【耗费一夜总结三本C语言系列】之 位及进制的用法🧡💛💚💙
一、初识数组💤
数组与结构是C语言中的聚合数据类型。
int arr[10];
以上是声明一个数据类型为int的长度为10的数组;
- 虽然数组的长度为10,但是使用时,数组下标编号是由0开始的。即
arr[0]是数组的第一个元素,arr[9]为数组的最后一个元素;
小拓展:为何数组的下标是以0开始的呢???:
这都归咎于编译器设计者,由于该设计者使用底层语言开发。而计算机的偏移量以0开始计数,已在设计者心中根深蒂固。
1.1 数组名 —— 🔔一个数组的名称具有哪些含义
数组名是一个指针常量,是第一个元素的地址(内存中数组的起始位置);
- 是一个指向(
数据类型)的常量指针; - 且只有
数组名在表达式中使用时,编译器才会为它产生一个指针常量; - 指针常量则
内存是固定的,不能修改。即程序运行后不能修改;
由于数组名为指针常量,不能接收一个数组,但可用相应类型的指针来接收
int arr[2] = {1, 2};
int arr1[2];
int *arr2;
arr1 = arr;
arr2 = arr;
arr2 = &arr[0];
在俩种情况下数组名不为指针常量
- 作为
sizeof操作数时,返回整个数组的大小;【故在将其值除以数据类型的大小,即可获取数组长度(长度包含结束符)】 - 为
&操作数时一个指向数组的指针;
1.2 了解了数组,该如何初始化?💿
声明数组时,将根据声明所指定的元素数量为数组保留内存空间,再创建数组名,其值是一个常量,指向这个段空间的起始位置;
int arr[3] = {4, 1, 9};
数组初始化按照以上格式,内容在{}内,内部使用,分隔开。
int arr[3] = {0};
即可将数组初始化为0;
int arr[] = {1, 2, 3};
若省略[]内的数字,则编译器会根据初始化列表的项来确定大小;
1.2.1 何为只读数组???
声明时,加上const关键字即可将数组初始化为只读数组,不能修改内部元素;
const int arr[3] = {1, 2, 3};
int i=0;
for(; i<3; ++i)
printf("%5d", arr[i]);
1.2.2 C99新特性 —— 指定初始化器
C99新特定,可指定数组内部元素的初始化;
int arr[10] = { [4] = 10 };
以上数组初始化第5个元素为10,其余全为0;
int arr[] = { [4] = 10, 11, 23 };
编译器会自动向后扩大数组,该数组的长度为7。而arr[5] = 11,arr[6] = 23
1.2.3 C99新特性 —— 变长数组
int n = 10;
int arr[n];
C99新增新型数组,为变长数组(VLA)。
1.3 编译器为何不对数组边界做处理????
使用数组时,需要注意数组下标的边界。在使用过程中,数组越界编译器可能不会检查该错误,而异常终止(根据编译器而异);
1.3.1 为什么编译器不检查数组越界的问题呢?
由于编译器为了提高程序的运行速度。若需要检查下标,C涉及的开销比我们想象的要多,则编译器需要在运行时,添加额外的指令来对数组的每个下标检查表达式所指向的元素是否属于同一个数组,这样会减低程序的运行效率。而该错误只要在开发过程中稍加注意即可避免;
1.4 多维数组💨
int arr[2][10];
该数组有2个元素,每个元素内有10个元素;
1.4.1 多维数组初始化
初始化时,可省略第一个[]内的数值,外部的数值需要保留;
int arr[][3] = {{2, 3, 1}, {4, 5, 6}};
1.4.2 多维数组与函数传参
与初始化的含义类似,[]外部的数组需要保留,内部可省略;
二、会了数组,少不了指针💞
2.1 指针简介
C语言中的一个重要概念及其特点。由于计算机的硬件指令非常依赖地址,而指针在某个程度上把程序员的指令更加接近机器的方式表达,提高程序效率。 指针是一个值为内存地址的变量。在同一CPU架构下,不同数据类型的指针变量其占用存储单元的长度是相同的,但其占用的存储空间不同;
- 指针不仅能对
数据本身,也可以对该变量的地址进行操作;
2.2 与指针操作相关的运算符
*运算符
该运算符也被称为解引用运算符;
&运算符
&加上变量名即可取出变量名的地址;
指针求差
两个指向统一个数组的不同元素可以通过求差来获取元素之间的距离;
2.2.1 指针操作中运算符的优先级
自增运算符与*
两者优先级相同,遵循从左向右;
[]与*
[]优先级高于*;
2.3 声明指针【需注意】💥
int *arr = NULL;
以上语句仅仅声明了指针,系统只给指针本身分配内存(arr),并未分配存储数据的内存(&arr);
三、指针、数组看似相似,实际不是💦
3.1 指针与下标💫
int arr[10] = {0};
arr+2 = &arr[2];
*(arr+2) = arr[2];
- 地址按
字节编址; - 指针加1也就是增加一个
存储单元(所指向的类型大小),即下一个元素的地址;
3.1.1 有时指针比数组下标更高效
为啥是有时呢?
- 只有指针在被
正确的使用才能达到效率高的效果,否则将会本末倒置,使程序的效率更低;
使用下标
int i, arr[3];
for(i=0, i<3; ++i)
arr[i] = i+1;
编译器需要在程序中插入相应的指令,获得i的值,在把它与相应的数据类型的长度相乘,需要花费一定的时间和空间。
使用指针
int *p, i=0;
int arr[3];
for(p = arr; p < arr + 3; ++p, ++i)
*p = i;
每次循环需要换到下一个地址时,指向要加上1x数据类型的长度。与下标用法的区别在于,它只需要在编译时执行一次即可。
归纳
- 当声明为
寄存器变量的指针一般会比其他的指针效率高;
3.2 const修饰指针
3.2.1 const对指针的几种限定方式
const限定一个变量不允许被改变,可用于修饰指针;
指向const的指针
不允许p修改指向的数据的值,即不能修改其值;
const int *p;
const指针
不允许指针指向别处,但可以修改它的值;
int const *p;
const修饰指及其指向
即不能修改值,也不能修改指向;
const int const *p;
3.2.2 const涉及指针的安全性
不安全的情况
int x = 3;
const int *y = &x;
int *p;
p = y;
不安全,把const赋值到非const上;
安全的情况
int x = 3;
int *y = &x;
const int *p;
p =y;
把非const赋值给const,前提是一级解引用;
3.3 指针或数组做函数参数💢
3.3.1 函数形参
只有在函数定义头或者函数声明中才能使用int arr[] 代替int *arr;
3.3.1 数组在函数中传值
若在函数传参中,数组使用传值的方式,则效率会比较低。
- 当按值传递数组时,编译器必须分配
足够的空间来存储原数组的副本,在把原数组的内容拷贝到新的数组;造成空间上的浪费以及时间上的损耗; - 故使用
传递地址的方法,效率大大提升; - 若想要保护数据不被修改,则加上const关键词;
3.4 指针与多维数组💭
3.4.1 多维数组与取地址、取值间的转换
int p[3][2];
- p为(p[0])数组首元素的地址;
p == &p[0],p[0]是第二维数组的首元素(p[0][0]);p[0] == &p[0][0] p:是占一个int大小的地址;p[0]:占用两个int大小的地址;- 不论是占用一个int还是两个int起始的地址都是相同的;
p = p[0]
- 故当给
p+1时相当于增加一个int长度,而给p[0]+1则增加两个int长度;
若搞不懂*,&,[]
一般情况下,就记作*需抵消一个[],&需增加一个[],*可抵消[];
**p = *&p[0][0] = p[0][0]
3.4.2 指向多维数组的指针
[]优先级高于*;
int (*p)[2];
此时,p相当于一个二维数组
int *p[2];
此时,内部都是int指针的数组;

|