• 第四站:数组


    目录

    一、一维数组的创建和初始化

    1.数组的创建

    (1)基本定义,创建方式

    (2)经典的错误标准的零分

    2.数组的初始化

    3.一维数组的使用

    4.一维数组在内存中的存储

    二、二维数组的创建和初始化

    1.二维数组的创建

     2.二维数组的初始化

    3.二维数组的使用

    4.二维数组在内存中的存储

     三、数组越界

    四、数组作为函数参数

    1.关于冒泡排序的经典错误标准零分

    2.数组名到底是什么呢

    (1)一般的,数组名是首元素地址

    (2)数组不是首元素地址的两个特例

    第一个特例,sizeof(数组名)

    第二个特例,&数组名

    (3)总结

    (4)&数组名和直接使用数组名的区别

    3.再次研究冒泡排序

    总结


    一、一维数组的创建和初始化

    1.数组的创建

    (1)基本定义,创建方式

    定义:数组是一组相同类型元素的集合。

    数组的创建方式:type_t   arr_name   [const_n];

    //type_t 是指数组的元素类型
    //const_n 是一个常量表达式,用来指定数组的大小
    举例:
    int arr1[10];
    char arr2[5+6];
    double arr3[10];

    (2)经典的错误标准的零分

    如下代码所示,不满足常量表达式,因为n是一个变量。

    1. #include
    2. int main()
    3. {
    4. int n = 10;
    5. int arr[n];
    6. return 0;
    7. }
    注: 数组创建,在 C99 标准之前, [] 中要给一个 常量 才可以,不能使用变量。在 C99 标准支持了变长数组的概念,数组的大小可以使用变量指定,但是数组不能初始化。
    我们可以在linux 上的gcc编译器上使用变长数组,这个编译器是支持C99标准的

    2.数组的初始化

    下面是数组的初始化的各种方式以及区别

    这是整型数组

    1. #include<stdio.h>
    2. int main()
    3. {
    4. //创建的同时给数组一些值,这叫初始化
    5. int arr[10] = {1,2,3,4,5,6,7,8,9,10};//完全初始化
    6. int arr2[10] = { 1,2,3 };//不完全初始化,剩余的元素默认初始化为0
    7. int arr3[] = { 1,2,3,4,5,6,7,8,9,10 };//这里没有指定数组元素个数,编译会根据初始化的内容来确定数组的元素个数
    8. int arr4[] = { 1,2,3 };//3个元素
    9. int arr5[10] = { 1,2,3 };//10个元素
    10. }

     这是字符数组

    1. #include<stdio.h>
    2. int main()
    3. {
    4. //arr6与arr7一模一样都是有3个元素,并且为a,b,c
    5. char arr6[3] = { 'a', 'b', 'c' };
    6. char arr7[] = { 'a', 'b', 'c' };
    7. char arr8[10] = "abc";//十个元素,前三个为abc,后面都为\0
    8. char arr9[] = "abc";//四个元素,前三个为abc,最后一个为\0
    9. }

    还有一种就是全局数组不初始化默认为0,局部数组不初始化为随机值,这一点与全局变量不初始化默认为0,局部变量不初始化为随机值是类似的。

    下面是调用监视窗口查看全局数组a1和局部数组a的内容。

     还有这两个初始化一定要搞清楚他们的区别

    1. #include
    2. int main()
    3. {
    4. char arr8[] = "abc";
    5. char arr9[] = {'a', 'b', 'c'};
    6. printf("%s\n", arr8);
    7. printf("%s\n", arr9);
    8. }

    arr8中有\0,arr9没有\0

    运行结果为

    3.一维数组的使用

     在之前我们提到过一个下标引用操作符 [ ],他其实就是数组访问的操作符。

    1.数组都是由下标的,下标是从0开始的

    2.[]下标引用操作符

    下面是顺序打印数组的方法

    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    5. int i = 0;
    6. int sz = sizeof(arr) / sizeof(arr[0]);
    7. for (i =0 ; i < sz; i++)
    8. {
    9. printf("%d ", arr[i]);
    10. }
    11. return 0;
    12. }

    下面是倒序打印数组,跳着打印数组

    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    5. int i = 0;
    6. int sz = sizeof(arr) / sizeof(arr[0]);
    7. for (i =0 ; i < sz; i++)
    8. {
    9. printf("%d ", arr[i]);
    10. }
    11. printf("\n");
    12. for (i = sz-1; i >=0; i--)
    13. {
    14. printf("%d ", arr[i]);
    15. }
    16. printf("\n");
    17. for (i = 0; i < sz; i=i+2)
    18. {
    19. printf("%d ", arr[i]);
    20. }
    21. return 0;
    22. }

    4.一维数组在内存中的存储

    我们看这样一个代码
    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    5. int i = 0;
    6. int sz = sizeof(arr) / sizeof(arr[0]);
    7. for (i = 0 ; i < sz; i++)
    8. {
    9. printf("&arr[%d] = %p\n", i, &arr[i]);
    10. }
    11. return 0;
    12. }

    结果为

    我们发现每两个地址间差4,而我们每个int类型的元素是4个字节,所以我们得出以下结论
    1.一维数组在内存中是连续存放的
    2.随着数组下标的增长,地址是由低到高变化的
    我们画一个图理解一下

     

    因此我们只需要得到一个数组元素的地址,我们就可以顺藤摸瓜得到所有的数组元素以及他们的地址
    按照这个思路,我们可以这样打印出我们的数组元素
    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    5. int i = 0;
    6. int sz = sizeof(arr) / sizeof(arr[0]);
    7. int* p = &arr[0];
    8. for (i = 0 ; i < sz; i++)
    9. {
    10. printf("arr[%d]=%d\n", i, *(p+i));
    11. }
    12. return 0;
    13. }

    运行结果为

     我们也可以试一下这个代码,验证一下指针+i的地址与&arr[i]的地址

    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    5. int i = 0;
    6. int sz = sizeof(arr) / sizeof(arr[0]);
    7. int* p = &arr[0];
    8. for (i = 0 ; i < sz; i++)
    9. {
    10. printf("%p -----%p\n", p+i, &arr[i]);
    11. }
    12. return 0;
    13. }

    运行结果为

     可见两个地址是一样的

    二、二维数组的创建和初始化

    1.二维数组的创建

    int arr[3][4];
    char arr[3][5];
    double arr[2][4];
    我们这样理解二维数组,比如第一个就是由三行和四列的元素,共12个,也就是说第一个控制行,第二个控制列

     2.二维数组的初始化

    我们看这个代码
    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
    5. return 0;
    6. }

    我们调试进行监视, 可以得到他们的分布

    我们再试试没有放满的

    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr1[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
    5. int arr2[3][4] = { 1,2,3,4,5 };
    6. return 0;
    7. }
    监视内部

     我们在换一种初始化方式

    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr1[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
    5. int arr2[3][4] = { 1,2,3,4,5 };
    6. int arr3[3][4] = { {1,2} ,{3,4}, {5,6} };
    7. return 0;
    8. }
    内部为
     二维数组初始化时候的行是可以省略的,列不可以省略
    我们看下面的初始化例子

    或者我们改变列为2,则为

    3.二维数组的使用

    二维数组的使用也是通过下标的方式
    比如我想打印出二维数组arr4的每个元素,代码实现如下
    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr1[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
    5. int arr2[3][4] = { 1,2,3,4,5 };
    6. int arr3[3][4] = { {1,2} ,{3,4}, {5,6} };
    7. int arr4[][2] = { 1,2,3,4,5,6,7,8,9 };
    8. int i = 0;
    9. for (i = 0; i < 5; i++)
    10. {
    11. int j = 0;
    12. for (j = 0; j < 2; j++)
    13. {
    14. printf("%d ", arr4[i][j]);
    15. }
    16. printf("\n");
    17. }
    18. return 0;
    19. }

    4.二维数组在内存中的存储

    我们看这个代码
    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
    5. int i = 0;
    6. for (i = 0; i < 3; i++)
    7. {
    8. int j = 0;
    9. for (j = 0; j < 4; j++)
    10. {
    11. printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]);
    12. }
    13. }
    14. return 0;
    15. }

    结果为

     我们会发现,还是每个元素差四个字节

    也就是说二维数组也是和一维数组一样线性连续存储的!如下图所示,前四个为第一行,中间为第二行,后面为第三行,我们可以把一个二维数组当成一个一维数组来理解

     那我们二维数组也就可以使用一维数组那样采用指针的方式来打印

    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
    5. int i = 0;
    6. for (i = 0; i < 3; i++)
    7. {
    8. int j = 0;
    9. for (j = 0; j < 4; j++)
    10. {
    11. printf("%d ", arr[i][j]);
    12. }
    13. }
    14. printf("\n");
    15. int* p = &arr[0][0];
    16. for (i = 0; i < 12; i++)
    17. {
    18. printf("%d ", *(p + i));
    19. }
    20. return 0;
    21. }

     输出结果为

     而且我们二维数组每一行都可以理解为一个一维数组,这个一维数组的数组名为arr[0],arr[1]......如下图所示

     所以二维数组也是一维数组的数组

    所以我们的打印数组也可以这样进行

    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
    5. int i = 0;
    6. for (i = 0; i < sizeof(arr)/sizeof(arr[0]); i++)
    7. {
    8. int j = 0;
    9. for (j = 0; j < sizeof(arr[0])/sizeof(arr[0][0]); j++)
    10. {
    11. printf("%d ", arr[i][j]);
    12. }
    13. }
    14. }

     

     三、数组越界

    数组的下标是有范围限制的。
    数组的下规定是从 0 开始的,如果数组有 n 个元素,最后一个元素的下标就是 n-1
    所以数组的下标如果小于 0 ,或者大于 n-1 ,就是数组越界访问了,超出了数组合法空间的访问。
    C 语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就 是正确的, 所以程序员写代码时,最好自己做越界的检查
    比如下面的代码
    1. #include <stdio.h>
    2. int main()
    3. {
    4. int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    5. int i = 0;
    6. for (i = 0; i <= 10; i++)
    7. {
    8. printf("%d\n", arr[i]);//当i等于10的时候,越界访问了
    9. }
    10. return 0;
    11. }

    运行结果为,其中就出现了一个明显的错误。

     二维数组的行和列也可能存在越界。

    四、数组作为函数参数

    往往我们在写代码的时候,会将数组作为参数传个函数,比如说我们想写出一个冒泡排序。

    1.关于冒泡排序的经典错误标准零分

    那么什么是冒泡排序呢?

    比如说我们想要对下面一组数据进行升序的话

     我们是这样想的,我们让9和8进行比较,如果9大于8,那么9和8进行交换,然后交换以后变成以下

    此时我们让第二个和第三个位置数据继续进行比较,并进行交换。然后重复下去,直到最后会变成

    我们发现9已经来到了他应该来到的最终位置,那么我们前面八个数还需要进行排序。我们继续采用相同的方法。我们会发现,前八个数字最终也会

     此时我们发现8也来到了他应该来到的最终位置.

    不妨我们就将这称作一趟冒泡排序,每一趟冒泡排序都可以使得一个数放到他应该来到的最终位置

    那么问题来了,如果有10个数字,那么需要多少趟?

    显然为9趟,对于任意n个数字,只需n-1趟冒泡排序。

    于是我们就有冒泡排序的一个基本框架了

     这里的for循环可以确定需要多少趟冒泡排序,而里面每一层都是一趟冒泡排序

    那么一趟需要比较多少次呢?

    我们回顾前面的那个案例,不难发现,第一趟,需要9次比较,第二趟需要8次比较,我们总结规律,n个元素需要n-i-1次比较。

    由此得到代码实现如下所示

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include<stdio.h>
    3. void sort(int arr[])
    4. {
    5. int sz = sizeof(arr) / sizeof(arr[0]);
    6. int i = 0;
    7. for (i = 0; i < sz - 1; i++)
    8. {
    9. int j = 0;
    10. for (int j = 0; j < sz - i - 1; j++)
    11. {
    12. if (arr[j] > arr[j + 1])
    13. {
    14. int tmp = 0;
    15. tmp = arr[j];
    16. arr[j] = arr[j + 1];
    17. arr[j + 1] = tmp;
    18. }
    19. }
    20. }
    21. }
    22. int main()
    23. {
    24. int arr[] = { 4,5,1,3,7,10,100,45,323,852};
    25. sort(arr);
    26. for (i = 0; i < 10; i++)
    27. {
    28. printf("%d\n", arr[i]);
    29. }
    30. return 0;
    31. }

     然而当我们进行运行的时候,我们发现结果并非我们所期望的

     并没有进行交换,这是为什么呢?其实问题就出在了sizeof,我们数组的传参传的其实是一个指针,而非整个数组,所以sizeof(arr)计算出来的并不是一个数组的长度,而是arr这个指针的大小,所以我们就应该将sz给放到主函数中,然后在传参的时候传入sz

    代码实现如下

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include<stdio.h>
    3. void sort(int arr[],int sz)
    4. {
    5. int i = 0;
    6. for (i = 0; i < sz - 1; i++)
    7. {
    8. int j = 0;
    9. for (int j = 0; j < sz - i - 1; j++)
    10. {
    11. if (arr[j] > arr[j + 1])
    12. {
    13. int tmp = 0;
    14. tmp = arr[j];
    15. arr[j] = arr[j + 1];
    16. arr[j + 1] = tmp;
    17. }
    18. }
    19. }
    20. }
    21. int main()
    22. {
    23. int arr[] = { 4,5,1,3,7,10,100,45,323,852};
    24. int sz = sizeof(arr) / sizeof(arr[0]);
    25. sort(arr,sz);
    26. int i;
    27. for (i = 0; i < sz; i++)
    28. {
    29. printf("%d\n", arr[i]);
    30. }
    31. return 0;
    32. }

    运行后结果为

    可见符合我们的预期

    2.数组名到底是什么呢

    我们在冒泡排序中,数组名一会是指针,一会代表整个数组,一会代表首元素地址。相信到了这块很多人都晕了,数组名到底是什么呢?

    (1)一般的,数组名是首元素地址

    一般情况下数组名是首元素地址,那么很多人就更加困惑了,什么是非一般情况呢?先不要着急,我们先来看看数组名是首元素地址的案例

    1. int main()
    2. {
    3. int arr[10] = { 0 };
    4. printf("%p\n", &arr[0]);
    5. printf("%p\n", arr);
    6. return 0;
    7. }

    运行之后

    可见数组名就是首元素地址,而地址就是我们的指针。

    (2)数组不是首元素地址的两个特例

    第一个特例,sizeof(数组名)

    1. #include
    2. int main()
    3. {
    4. int arr[10] = { 0 };
    5. printf("%d", sizeof(arr));
    6. return 0;
    7. }

    运行结果为

    其实,sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节

    第二个特例,&数组名

    1. #include
    2. int main()
    3. {
    4. int arr[10] = { 0 };
    5. printf("%p\n", &arr[0]);
    6. printf("%p\n", arr);
    7. printf("%d\n", sizeof(arr));
    8. printf("%p\n", &arr);
    9. return 0;
    10. }

    运行结果为

     可见这里的数组名表示的是整个数组,&数组名取出的是数组的地址

    (3)总结

    一般情况下,数组名是首元素的地址,但有两个例外,一个是sizeof(数组名),一个是&数组名,这两种情况下,数组名代表的是整个数组。

    (4)&数组名和直接使用数组名的区别

    在这里,又有人有了一些困惑,arr直接打印出来的地址和&arr出来的地址有什么区别呢?看上去打印出来的值都一样啊?

    假设上面是我们的arr数组,我们运行如下代码
    1. #include<stdio.h>
    2. int main()
    3. {
    4. int arr[10] = { 0 };
    5. printf("%p\n", &arr[0]);
    6. printf("%p\n", &arr[0]+1);
    7. printf("%p\n", arr);
    8. printf("%p\n", arr+1);
    9. printf("%p\n", &arr);
    10. printf("%p\n", &arr+1);
    11. return 0;
    12. }

    运行后结果为

     可见,第一组+1后的地址是+4,第二组+1后的地址也是+4,而第三组+1后的地址是加了40

    我们发现,虽然结果一样,也就是他们的起点一样,但是他们的含义不同,&arr+1是直接跳过整个数组,而前两者仅仅跳过一个元素。

    3.再次研究冒泡排序

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include<stdio.h>
    3. void sort(int arr[],int sz)
    4. {
    5. int i = 0;
    6. for (i = 0; i < sz - 1; i++)
    7. {
    8. int j = 0;
    9. for (int j = 0; j < sz - i - 1; j++)
    10. {
    11. if (arr[j] > arr[j + 1])
    12. {
    13. int tmp = 0;
    14. tmp = arr[j];
    15. arr[j] = arr[j + 1];
    16. arr[j + 1] = tmp;
    17. }
    18. }
    19. }
    20. }
    21. int main()
    22. {
    23. int arr[] = { 4,5,1,3,7,10,100,45,323,852};
    24. int sz = sizeof(arr) / sizeof(arr[0]);
    25. sort(arr,sz);
    26. int i;
    27. for (i = 0; i < sz; i++)
    28. {
    29. printf("%d\n", arr[i]);
    30. }
    31. return 0;
    32. }

    我们现在应该能够理解了这段代码中,为什么sz要放在主函数内部了,虽然我传的是一个数组名,但是此时的数组名代表着首元素地址,而我们的形参虽然人模狗样的写着一个数组,但是其实他本质上是一个指针。也就说我们可以将他改为一个指针变量。

    我们可以将其改为指针试一试

    1. void sort(int* arr, int sz)
    2. {
    3. int i = 0;
    4. for (i = 0; i < sz - 1; i++)
    5. {
    6. int j = 0;
    7. for (int j = 0; j < sz - i - 1; j++)
    8. {
    9. if (arr[j] > arr[j + 1])
    10. {
    11. int tmp = 0;
    12. tmp = arr[j];
    13. arr[j] = arr[j + 1];
    14. arr[j + 1] = tmp;
    15. }
    16. }
    17. }
    18. }

    我们运行后,这个代码仍然是正确的,所以我们传数组可以使用一个指针来接受。当然我们也可以使用一个数组的形式来进行接受,这样其实更加便于初学者理解。

    而我们传入一个指针以后,我们就可以对这个指针进行+1,而整型指针+1,向后移动4个字节。刚好就是下一个数据。所以采用指针可以顺藤摸瓜,拿到整个数组的值。

    当然有人就有一些困惑了,为什么我们平时函数传参的时候是直接拷贝过去,而数组却是传地址呢?这一点大家可以思考以下,如果我有10000个元素的数组,那么我们还要传整个数组的话,那么对这对于计算机内存上,运行速度上都会产生极大的浪费。所以使用指针传参也是很合理的。


    总结

    本节主要讲解了数组的一些基本知识点,基本概念,以及数组传参时候的一些坑,如果对你有一些帮助的话,不要忘记点赞加收藏哦!!!

  • 相关阅读:
    客户案例 | 保险科技日新月异,打造“一站式”数字化转型解决方案
    Autoware中的点云3D聚类算法,保姆级算法阅读注释,一看就懂,非常详细!
    BurpSuit详细安装教程(含有免费安装包)
    安装配置Redis
    Linux云计算 |【第二阶段】NETWORK-DAY3
    希尔排序(缩小增量排序)
    cadence SPB17.4 - CIS DB - add MECHANICAL part
    error Missing “key“ prop for element in array react/jsx-key
    confluent-kafka-go依赖库编译体验优化
    苹果手机自身的ip地址怎么查
  • 原文地址:https://blog.csdn.net/jhdhdhehej/article/details/127714247