- 数组
- 1. 一维数组的创建和初始化
- 1.1 数组的创建
- 1.2 数组的初始化
- 1.3 一维数组的使用
- 1.4 一维数组在内存中的存储
- 2. 数组名的含义
- 2.1 a 和 &a的区别
- 2.2 理解 &a[0] 和 &a 的区别
- 数组名使用的时候,只有两种情况代表整个数组:
- 2.3 数组名作为函数参数
- 2.3.1 冒泡排序函数的错误设计
- 2.3.2 冒泡排序函数的正确设计
- 2.4 数组名a做为左值和右值的区别
- 3. 二维数组的创建和初始化
- 3.1 二维数组的创建
- 3.2 二维数组的初始化
- 3.3 二维数组的使用
- 3.4 二维数组在内存中的存储
- 3.5 数组越界
- 指针
- 1. 指针是什么?
- 1.1 指针的含义
- 1.2 指针的内存布局
- 1.3 指针的解引用
- 1.4 如何将数值存储到指定的内存地址
- 1.5 究竟该如何理解编址(地址是如何产生的)
- 2. 指针和指针类型
- 2.1 指针 ± 整数
- 2.2 指针的解引用
- 3. 野指针
- 3.1 野指针成因
- 3.1.1 指针未初始化
- 3.1.2 指针越界访问
- 3.1.3 指针指向的空间释放
- 3.2 如何规避野指针
- 4. 指针运算
- 4.1 指针 ± 整数
- 4.2 指针-指针
- 4.3 指针的关系运算
- 指针和数组的关系
- 1. 以指针的形式访问和以数组的形式访问
- C为何要把数组和指针的访问方式设计成通用的?
数组
概念:数组是具有相同数据类型的集合。
#include1. 一维数组的创建和初始化 1.1 数组的创建#define N 10 int main() { int a[N] = {0};//定义并初始化数组 return 0; }
数组是一组相同类型元素的集合。 数组的创建方式:
type_t arr_name [const_n];
type_t 是指数组的元素类型
arr_name 是数组的名字
const_n 是一个常量表达式,用来指定数组的大小
数组创建的实例:
//代码1 int arr1[10]; //代码2 int count = 10; int arr2[count];//数组时候可以正常创建? //代码3 char arr3[10]; float arr4[1]; double arr5[20];
//该代码只能在C99标准下能使用
int n = 10;
scanf("%d ",&n);
int arr[n];//这种数组是不能初始化的
注:数组创建,在C99标准之前, [] 中要给一个常量才可以,不能使用变量。在C99标准之后,数组的大小可以是变量,支持了变长数组的概念。gcc 编译器支持变长数组。
数组的类型
int arr[10] = {0};
类型为 int [10] ,去掉数组名就是他的类型
1.2 数组的初始化数组的初始化是指:在创建数组的同时给数组的内容一些合理的初始值(初始化)。
看代码:
//不完全初始化,剩余的元素默认初始化为0
int arr1[10] = {1,2,3};
int arr2[5] = {1,2,3,4,5};
int arr3[] = {1,2,3,4};
char ch1[10] = {'a','b', 'c'};
//'a' 'b' 'c' ' ' ' ' ' ' ' ' ' ' ' ' ' '
char ch2[10] = "abc";
//'a' 'b' 'c' ' ' ' ' ' ' ' ' ' ' ' ' ' '
char ch3[] = {'a','b', 'c'};
//'a' 'b' 'c'
char ch4[] = "abc";
//'a' 'b' 'c' ' '
数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。
但是对于下面的代码要区分,内存中如何分配。
char arr1[] = "abc";
//'a' 'b' 'c' ' '
char arr2[3] = {'a','b','c'};
//'a' 'b' 'c'
1.3 一维数组的使用
对于数组的使用我们之前介绍了一个操作符:[] ,下标引用操作符。它其实就是数组访问的操作符。 我们来看代码:
#includeint main() { int arr[10] = {0};//数组的不完全初始化。在一个连续空间上存储 //计算数组的元素个数的方法 int sz = sizeof(arr)/sizeof(arr[0]); //对数组内容赋值,数组是使用下标来访问的,下标从0开始。所以: int i = 0; //做下标 for(i=0; i arr[i] = i; } //输出数组的内容 for(i=0; i printf("%d ", arr[i]); } return 0; }
总结:
-
数组是使用下标来访问的,下标是从0开始。
-
数组的大小可以通过计算得到。
int arr[10]; int sz = sizeof(arr)/sizeof(arr[0]);1.4 一维数组在内存中的存储
#include#define N 10 int main() { int a = 10; int b = 20; int c = 30; printf("%pn", &a); printf("%pn", &b); printf("%pn", &c); return 0; }
运行结果:
我们发现,先定义的变量,地址是比较大的,后续依次减小
这是为什么呢?
a,b,c都在main函数中定义,也就是在栈上开辟的临时变量。而 a 先定义意味着,a 先开辟空间,那么 a 就先入栈,所以 a 的地址最高,其他类似。
接下来我们探讨数组在内存中的存储。 看代码:
//打印数组的每个元素的地址 #includeint main() { int arr[10] = {0}; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); for(i=0; i printf("&arr[%d] = %pn", i, &arr[i]); } return 0; }
输出的结果如下:
仔细观察输出的结果,我们知道,随着数组下标增长,元素的地址,也在有规律的递增。 由此可以得出结论:数组在内存中是连续存放的。
但是,我们发现,数组的地址排布是:&arr[0] < &arr[1] < &arr[2] < ... < &arr[9] 。该数组在 main 函数中定义,那么也同样在栈上开辟空间。
数组有多个元素,那么肯定是 arr[0] 先被开辟空间啊,那么肯定 &arr[0] 地址最大啊,可是事实上并非如此!
实际上,数组是整体申请空间的,然后将地址最低的空间,作为 arr[0] 元素,依次类推!
在开辟空间的角度,不应该把数组认为成为一个个独立的元素,应该整体开辟空间,整体释放。
2. 数组名的含义 2.1 a 和 &a的区别#include#include int main() { int a[5] = { 1, 2, 3, 4, 5 }; int *ptr = (int*)(&a + 1); //非法空间,不能访问,但可以指向 printf("%d %dn", *(a + 1), *(ptr - 1));//ptr return 0; }
| arr | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 下标 | [0] | [1] | [2] | [3] | [4] |
结论:&a 叫做数组的地址,a 作右值叫做数组首元素的地址,本质是类型不同,进而进行+ -计算步长不同。
2.2 理解 &a[0] 和 &a 的区别在C语言中我们要区分一个概念:首元素的地址和数组的地址。
#includeint main() { char* c = NULL; short* s = NULL; int* i = NULL; double *d = NULL; printf("%dn", c); //0 printf("%dnn", c + 1); //1 printf("%dn", s); //0 printf("%dnn", s + 1); //2 printf("%dn", i); //0 printf("%dnn", i + 1); //4 printf("%dn", d); //0 printf("%dnn", d + 1); //8 return 0; }
口诀:对指针+1,本质加上其所指向类型的大小。
如果发生类型转化呢?
#includeint main() { int a = 10; double *d = (double *)&a; printf("%pn", d); printf("%pnn", d + 1); printf("%pnn", (char *)d + 1); return 0; }
强制类型转换后看强转的类型。
二级指针及其往上所有的指针+1,都是 +sizeof(指针)个字节(32位下是4个字节,64位下是8个字节)
#include#define N 10 int main() { int arr[N] = { 0 }; printf("%pn", &arr[0]); //首元素的地址 printf("%pnn", &arr[0]+1); //第二个元素的地址 printf("%pn", &arr); //数组的地址 printf("%pn", &arr+1); //跳过整个arr数组,访问下一个数组的地址 return 0; }
&arr[0] 和 &arr 虽然地址数字一样大,但是类型意义完全不同。
因为首元素的地址和数组的地址,在地址对应得字节是重叠的!所以,地址数据值相等。
数组名使用的时候,只有两种情况代表整个数组:-
sizeof(数组名),计算整个数组的大小,sizeof 内部单独放一个数组名,数组名表示整个数组。
-
&数组名,取出的是数组的地址(值和首元素的地址一样,但是意义不一样)。&数组名,数组名表示整个数组。
-
除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。
往往我们在写代码的时候,会将数组作为参数传个函数,比如:我要实现一个冒泡排序函数将一个整形数组排序。
冒泡排序基本思想(升序):对所有相邻记录的值进行比较,如果 a[j]>a[j+1] ,则将其交换,最终达到有序。原始的冒泡排序算法,对由n个值组成的序列,最多经过(n-1)次冒泡排序就可以使其成为有序序列,算法框架为:for(int i = 0; i 数组传参的时候,形参有两种写法: 那我们将会这样使用该函数: 方法1运行后出现问题,调试之后可以看到 bubble_sort 函数内部的 sz ,是1。 难道数组作为函数参数的时候,不是把整个数组传递过去? &数组名,取出的是数组的地址(值和首元素的地址一样,但是意义不一样)。传参时把数组的首元素的地址传递过去了,函数中的形参arr是一个指针变量,arr 保存传过去的地址的大小,又因为地址的大小在x86下是4个字节,所以sizeof(arr)的大小为4。 当数组传参的时候,实际上只是把数组的首元素的地址传递过去了。 所以即使在函数参数部分写成数组的形式: int arr[] 表示的依然是一个指针: int *arr 。所以,函数内部的 sizeof(arr) 结果是4。 如果方法1 错了,该怎么设计? 数组名可以做右值,代表数组首元素的地址 数组名做右值,本质等价于 &arr[0] 该代码可以运行… 数组名不可以做左值! 能够充当左值的,必须是有空间且可被修改的,arr 不可以整体使用,只能按照元素为单位进行使用 二维数组如果有初始化,行可以省略,列不能省略 二维数组的使用也是通过下标的方式。 看代码: arr[3][4] 的逻辑结构 可以把二维数组理解为:一维数组的数组 内存中一个字节给一个地址,int 类型占四个字节 像一维数组一样,这里我们尝试打印二维数组的每个元素。 输出的结果是这样的: 通过结果我们可以分析到,其实二维数组在内存中也是连续存储的(物理结构) 数组的下标是有范围限制的。 数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。 所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。 C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就 是正确的, 所以我们在写代码时,最好自己做越界的检查。 二维数组的行和列也可能存在越界。 指针理解的2个要点: 指针是内存中一个最小单元的编号,也就是地址 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量 总结:指针就是地址,口语中说的指针通常指的是指针变量。 在开始讲述前,我们先引入一个概念,对于我们定义的一个变量,当变量在等号的左边时,意味着我们要使用这个变量定义时开辟的空间,当变量在等号的右边时,意味着我们要使用这个变量所开辟的空间里的内容。 我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量 指针变量就是一个变量,只不过里面保存的是一个地址(指针)数据。指针变量:空间(左值)+ 内容(右值:指针(地址)) 结论:指针就是地址,指针变量是一个变量,变量内部保存指针(地址)数据,通过访问这个地址就能找到对印的内存单元。(存放在指针中的值都被当成地址处理)。 一个整型四个字节,每个字节都有地址,取地址的时候取得是第一个字节的地址(最低的地址)。 指针指向最低的那个地址,C语言中任何变量取地址,永远取的是他所开辟的众多字节当中地址数字最小的那一个字节作为该变量的地址。 *p 完整理解是,取出 p 中的地址,访问该地址指向的内存单元(空间或者内容)(其实通过指针变量访问,本质是一种间接寻址的方式) 口诀:在同类型情况下,对指针进行解引用,代表指针所指向的目标!所以*p,就是a 知道了指针的本质就是地址,地址就是数据,那么我们可以直接通过地址数据对变量进行访问吗? 所以通过地址直接访问内存是不可能的! p是一个 int* 类型的变量 具有指向性的数字(数据),我们称之为指针! 这里问个问题:指针变量是不是数据?当然是数据,那既然是数据,是不是就需要一个地址保存,所以指针变量也有自己的地址且能被保存!这就引出了二级指针变量的概念:二级指针变量保存指针变量的地址。 首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协同,至少相互之间要能够进行数据传递。 但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。 而CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。不过,我们今天关心一组线,叫做地址总线。 CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址(就如同宿舍很多,需要给宿舍编号一样) 计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。 钢琴 吉他 上面没有写上“都瑞咪发嗦啦”这样的信息,但演奏者照样能够准确找到每一个琴弦的每一个位置,这是为何?因为制造商已经在乐器硬件层面上设计好了,并且所有的演奏者都知道。本质是一种约定出来的共识! 硬件编址也是如此,我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2中含义,2根线就能表示4中含义,依次类推。32根地址线,就能表示2^32中含义,每一种含义都代表一个地址。 地址信息被下达给内存,在内存内部,就可以找到改地址对应的数据,将数据在通过数据总线传入CPU内寄存器 计算机只认识2进制。32位机器有32根地址线。 一根地址线用有无电信号来表示0和1。32根地址线同时可以传输32个比特位,这里就有2的32次方个地址。 所谓的编址(地址)体现在地址总线的排列组合上! 每个地址标识一个字节,那我们就可以给 2^32个地址*1字节 = 2^10*2^10*2^10*2^2 = 4GB 4G的空间进行编址。在地址总线的约束下,寻址范围只有4GB内存的物理范围。 同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,大家可以自己尝试计算。16GB 这里我们就明白: 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以 一个指针变量的大小就应该是4个字节。 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地 址。 总结: 指针变量是用来存放地址的,地址是唯一标示一块地址空间的。 指针的大小在32位平台是4个字节,在64位平台是8个字节。 要将 &num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢? 我们给指针变量相应的类型。 这里可以看到,指针的定义方式是:type + *。 其实: char* 类型的指针是为了存放 char 类型变量的地址。 short* 类型的指针是为了存放 short 类型变量的地址。 int* 类型的指针是为了存放 int 类型变量的地址。 那指针类型的意义是什么? **总结:**指针的类型决定了指针向前或者向后走一步有多大(距离)。 总结: 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。 在调用free或delete释放空间后,指针指向的内容被销毁,空间被释放,但是指针的值并未改变,仍然指向这块内存,这就使得该指针成为野指针。因此在调用free或 delete之后,应将该指针置为NULL。 1.指针初始化 实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。 标准规定: 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。 我们看一个例子: 运行结果: 可见数组名和数组首元素的地址是一样的。 结论:数组名表示的是数组首元素的地址。(2种情况除外) 那么这样写代码是可行的: 既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。 例如: 运行结果: 所以 p+i 其实计算的是数组 arr 下标为 i 的地址。 那我们就可以直接通过指针来访问数组。 如下: 为何要存在指针?提高查找定位的效率! 我们再看一组代码: 指针和数组本质上寻址方案不同,数组在栈区寻找,字符指针在字符常量区寻找。 结论: 我们假设:数组只能 [] 访问,指针只能 * 解引用访问 为什么要降维?如果不降维,就要发生数组拷贝,函数调用效率降低,所以要降维成指针。 降维成什么?所有的数组,传参都会降维成指针,降维成为指向其内部元素类型的指针! 在C语言中,任何函数调用,只要有形参实例化,必定形成临时拷贝,函数调用时的实参传给形参,初始化形参的内容! 假设指针和数组访问元素的方式不通用,程序员需要不断在不同的代码片段处,进行习惯的切换,就会增加代码出错的可能!为了让程序员统一使用数组,减少出错的概率,数组和指针的访问方式设计成通用的! 上面代码其实没有什么问题,不过,如果没有将指针和数组元素访问打通,那么在C中(面向过程) 如果有大量的函数调用且有大量数组传参,会要求程序员进行各种访问习惯的变化,就有提升代码出错的概率和调试的难度。 所以干脆,C将指针和数组的访问方式打通,让程序员在函数内,也好像使用数组那样进行元素访问,本质值减少了编程难度! 很高兴能够为大家带来帮助,码字不易,如果大家觉得本篇文章有帮助的话,可以支持。//形参是数组的形式
void bubble_sort(int arr[]);
//形参是指针的形式
void bubble_sort(int* arr);
//方法1:
#include
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
int i = 0;
for(i=0; i
2.4 数组名a做为左值和右值的区别
#include
#include
3. 二维数组的创建和初始化
3.1 二维数组的创建
//数组创建
int arr[3][4]; //3行4列
char arr[3][5]; //3行5列
double arr[2][4]; //2行4列
3.2 二维数组的初始化
//数组初始化,不够的补0。
int arr[3][4] = {1,2,3,4};
//1 2 3 4
//0 0 0 0
//0 0 0 0
int arr[3][4] = { {1,2}, {4,5} };
//1 2 0 0
//4 5 0 0
//0 0 0 0
int arr[][4] = { {2,3}, {4,5} };
//2 3 0 0
//4 5 0 0
#include
该行右边是列的下标 0 1 2 3 第一行(下标从0开始) 1 2 3 4 下标 arr[0][0] arr[0][1] arr[0][2] arr[0][3] 第二行(下标为1) 5 6 7 8 下标 arr[1][0] arr[1][1] arr[1][2] arr[1][3] 第三行(下标为2) 9 10 11 12 下标 arr[2][0] arr[2][1] arr[2][2] arr[2][3] #include
#include
//同样的一个a,在不同的表达式中,名称是一样的,但是含义是完全不同的
int a = 10;
a = 20;//使用的是a的空间:左值
int b = a;//使用的是a的内容:右值
int a = 10; //在内存中开辟一块空间
int* p = &a; //这里我们对变量a,取出它的地址,可以使用&操作符
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个指针变量
p = (int *)0x1234;//p变量的空间:左值
int* q = p; //p变量的内容:右值
#include
#include
#include
2. 指针和指针类型
int num = 10;
p = #
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
//大小都是相同的(x86下是4字节,x64下是8字节)
#include
#include
#include
3.1.2 指针越界访问
#include
3.1.3 指针指向的空间释放
2.小心指针越界
3.指针指向空间释放即使置NULL
4.避免返回局部变量的地址
5.指针使用之前检查有效性#include
4. 指针运算
4.1 指针 ± 整数
#define N_VALUES 5
float values[N_VALUES];
float *vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}
4.2 指针-指针
int my_strlen(char *s)
{
char *p = s;
while(*p != ' ' )
p++;
return p-s;
}
4.3 指针的关系运算
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
*vp = 0;
}
#include
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址
#include
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i
#include
C为何要把数组和指针的访问方式设计成通用的?
数组传参,发生降维,降维成指针。换句话说,arr在main里面是数组,传入InitArr函数之后,就成了数组首元素的指针变量。#include



