1.一维数组
1.1概念:
- 元素类型角度:数组是相同类型的变量的有序集合
- 内存角度:连续的一大片内存空间
在讨论多维数组之前,我们还需要学习很多关于一维数组的知识。首先让我们学习一个概念。
初始化方式:略
1.2 一维数组数组名:
考虑下面这些声明:
int a; int b[10]; |
我们把a称作标量,因为它是个单一的值,这个变量是的类型是一个整数。我们把b称作数组,因为它是一些值的集合。下标和数名一起使用,用于标识该集合中某个特定的值。例如,b[0]表示数组b的第1个值,b[4]表示第5个值。每个值都是一个特定的标量。
那么问题是b的类型是什么?它所表示的又是什么?一个合乎逻辑的答案是它表示整个数组,但事实并非如此。在C中,在几乎所有数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型:如果他们是int类型,那么数组名的类型就是“指向int的常量指针”;如果它们是其他类型,那么数组名的类型也就是“指向其他类型的常量指针”。
请问:数组首地址指针和数组名是等价的吗? |
答案是否定的。数组名在表达式中使用的时候,编译器才会产生一个指针常量。那么数组在什么情况下不能作为指针常量呢?在以下两种场景下:除了这俩种,其余都等价
|
int arr[10]; //arr = NULL; //arr作为指针常量,不可修改 int *p = arr; //此时arr作为指针常量来使用 printf("sizeof(arr):%d\n", sizeof(arr)); //此时sizeof结果为整个数组的长度 printf("&arr type is %s\n", typeid(&arr).name()); //int(*)[10]而不是int* |
1.3 下标引用
int arr[] = { 1, 2, 3, 4, 5, 6 }; |
*(arr + 3) ,这个表达式是什么意思呢?
首先,我们说数组在表达式中是一个指向整型的指针,所以此表达式表示arr指针向后移动了3个元素的长度。然后通过间接访问操作符从这个新地址开始获取这个位置的值。这个和下标的引用的执行过程完全相同。所以如下表达式是等同的,且下标可以为负数。
*(arr + 3) arr[3] |
问题1:数组下标可否为负值?
可以的,相当于解引用
问题2:请阅读如下代码,说出结果:
int arr[] = { 5, 3, 6, 8, 2, 9 }; int *p = arr + 2; printf("*p = %d\n", *p);给机器看 printf("*p = %d\n", p[-1]);给人看 |
那么是用下标还是指针来操作数组呢?对于大部分人而言,下标的可读性会强一些。
1.4数组和指针应用
指针和数组并不是相等的。为了说明这个概念,请考虑下面两个声明:
int a[10]; int *b; |
声明一个数组时,编译器根据声明所指定的元素数量为数组分配内存空间,然后再创建数组名,指向这段空间的起始位置。声明一个指针变量的时候,编译器只为指针本身分配内存空间,并不为任何整型值分配内存空间,指针并未初始化指向任何现有的内存空间。
因此,表达式*a是完全合法的,但是表达式*b却是非法的。*b将访问内存中一个不确定的位置,将会导致程序终止。另一方面b++可以通过编译,a++却不行,因为a是一个常量值。
1.5 数组作为函数参数的数组名(失去俩个特殊形式)
当一个数组名作为一个参数传递给一个函数的时候发生什么情况呢?我们现在知道数组名其实就是一个指向数组第1个元素的指针,所以很明白此时传递给函数的是一份指针的拷贝。所以函数的形参实际上是一个指针。但是为了使程序员新手容易上手一些,编译器也接受数组形式的函数形参。因此下面两种函数原型是相等的:
int print_array(int *arr); int print_array(int arr[]); |
我们可以使用任何一种声明,但哪一个更准确一些呢?答案是指针。因为实参实际上是个指针,而不是数组。同样sizeof arr值是指针的长度,而不是数组的长度。
现在我们清楚了,为什么一维数组中无须写明它的元素数目了,因为形参只是一个指针,并不需要为数组参数分配内存。另一方面,这种方式使得函数无法知道数组的长度。如果函数需要知道数组的长度,它必须显式传递一个长度参数给函数。
2.多维数组
2.1概念:
如果某个数组的维数不止1个,它就被称为多维数组。接下来的案例讲解以二维数组举例。
三种初始化方式:(线性存储方式)
void test01(){ //二维数组初始化 int arr1[3][3] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; int arr2[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int arr3[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; //打印二维数组 for (int i = 0; i < 3; i++){ for (int j = 0; j < 3; j ++){ printf("%d ",arr1[i][j]); } printf("\n"); } } |
2.2 二维数组的数组名
一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第1个元素。
多维数组也是同理,多维数组的数组名也是指向第一个元素,只不过第一个元素是一个数组。例如:
int arr[3][10] |
可以理解为这是一个一维数组,包含了3个元素,只是每个元素恰好是包含了10个元素的数组。
arr就表示指向它的第1个元素的指针,所以arr是一个指向了包含了10个整型元素的数组的指针。
二维数组数组名指向第一行的数组指针
俩种特殊情况:
- 当数组名作为sizeof操作符的操作数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。
- 当数组名作为&操作符的操作数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。&arr+1类型是int(*)[5][3]; arr的类型是int(*)【3】类型
可以报错一下会有显示出数据类型来
2.3下标和几种引用
数组名取地址就是首元素地址
2.4数组名做函数参数(失去俩个特殊形式)
数组名传进来退化成一维数组指针,或者直接写成二维数组这种形式表示一维数组指针也是可以的
2.5总结
3. 指向数组的指针(数组指针)
3.1概念:
(&arr不是第一个元素地址,代表整个数组,是一个数组指针)
数组指针,它是指针,指向数组的指针。数组指针,指的是数组名的指针,即数组首元素地址的指针。即是指向数组的指针。例:int (*p)[10]; p即为指向数组的指针,又称数组指针。
数组的类型由元素类型和数组大小共同决定:int array[5] 的类型为 int[5];C语言可通过typedef定义一个数组类型:
3.2定义数组指针有一下三种方式:
//方式一 void test01(){ //先定义数组类型,再用数组类型定义数组指针 int arr[10] = {1,2,3,4,5,6,7,8,9,10}; //有typedef是定义类型,没有则是定义变量,下面代码定义了一个数组类型ArrayType typedef int(ArrayType)[10]; //int ArrayType[10]; //定义一个数组,数组名为ArrayType ArrayType myarr; //等价于 int myarr[10]; ArrayType* pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr for (int i = 0; i < 10;i++){ printf("%d ",(*pArr)[i]); } printf("\n"); } //方式二 void test02(){ int arr[10]; //定义数组指针类型 typedef int(*ArrayType)[10]; ArrayType pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr for (int i = 0; i < 10; i++){ (*pArr)[i] = i + 1; } for (int i = 0; i < 10; i++){ printf("%d ", (*pArr)[i]); } printf("\n"); } //方式三(直接用数组指针变量)(常用) void test03(){ int arr[10]; int(*pArr)[10] = &arr; for (int i = 0; i < 10; i++){ (*pArr)[i] = i + 1; } for (int i = 0; i < 10; i++){ printf("%d ", (*pArr)[i]); } printf("\n"); } |
3.3应用场景:
稍稍有些开发经验的C语言程序员应该都定义过下面这样的函数:
void fun(char *p, int len);
参数 len 用于描述指针 p 索引的内存长度,这样的定义方式显然无法保证传递给 fun() 函数的内存长度等于期望值。假如 fun() 函数的开发者期望传递给 fun() 函数的指针 p 索引的内存长度为 10,那么只能寄希望于他的同事严格遵守开发者的意图(这并不容易),毕竟
fun(p, 9);
fun(p, 110);
都是合法的,这样的函数调用编译器不会产生任何错误或者警告信息。如果 fun() 函数采用数组指针的方式定义,情况就不同了,相关C语言代码如下:
void fun(char (*p)[10]);
这样的定义方式强制 fun() 函数的调用者传递索引指定长度内存的指针,否则编译器将发出警告或者错误信息。
4.指针数组(元素为指针)
4.1概念
每个元素都是指针的数组
4.2类型
1. 栈区指针数组 --引入选择排序法
思路:先当第一个元素是最小的,拿第一个元素和其他元素比较,如果后面元素更小,交换位置
然后到第二个元素当做最小的,同理。(多写一步更新操作,大同小异)
数字的:
字符串的:char* p[] = { "bbb", "aaa", "ccc", "eee", "ddd"};
//数组做函数函数,退化为指针 void array_sort(char** arr,int len){ for (int i = 0; i < len; i++){ for (int j = len - 1; j > i; j --){ //比较两个字符串 if (strcmp(arr[j-1],arr[j]) > 0){//比较字符串 char* temp = arr[j - 1]; arr[j - 1] = arr[j]; arr[j] = temp; } } } } //打印数组 void array_print(char** arr,int len){ for (int i = 0; i < len;i++){ printf("%s\n",arr[i]); } printf("----------------------\n"); } void test(){ //主调函数分配内存 //指针数组 char* p[] = { "bbb", "aaa", "ccc", "eee", "ddd"}; //char** p = { "aaa", "bbb", "ccc", "ddd", "eee" }; //错误 int len = sizeof(p) / sizeof(char*); //打印数组 array_print(p, len); //对字符串进行排序 array_sort(p, len); //打印数组 array_print(p, len); } |
2.堆区指针数组
//分配内存 char** allocate_memory(int n){ if (n < 0 ){ return NULL; } char** temp = (char**)malloc(sizeof(char*) * n); if (temp == NULL){ return NULL; } //分别给每一个指针malloc分配内存 for (int i = 0; i < n; i ++){ temp[i] = malloc(sizeof(char)* 30); sprintf(temp[i], "%2d_hello world!", i + 1); } return temp; } //打印数组 void array_print(char** arr,int len){ for (int i = 0; i < len;i++){ printf("%s\n",arr[i]); } printf("----------------------\n"); } //释放内存 void free_memory(char** buf,int len){ if (buf == NULL){ return; } for (int i = 0; i < len; i ++){ free(buf[i]); buf[i] = NULL; } free(buf); } void test(){ int n = 10; char** p = allocate_memory(n); //打印数组 array_print(p, n); //释放内存 free_memory(p, n); } |
5.总结
5.1 编程提示
- 源代码的可读性几乎总是比程序的运行时效率更为重要
- 只要有可能,函数的指针形参都应该声明为const
- 在多维数组的初始值列表中使用完整的多层花括号提高可读性
5.2 内容总结
在绝大多数表达式中,数组名的值是指向数组第1个元素的指针。这个规则只有两个例外,sizeof和对数组名&。
指针和数组并不相等。当我们声明一个数组的时候,同时也分配了内存。但是声明指针的时候,只分配容纳指针本身的空间。
当数组名作为函数参数时,实际传递给函数的是一个指向数组第1个元素的指针。
我们不单可以创建指向普通变量的指针,也可创建指向数组的指针。