Bootstrap

C语言,对于数组、数组名、数组元素、字符串、指针的一些总结

目录

一维数组

一维数组的传参与调用

二维数组以及多维数组

二维数组的传参与调用

计算二维数组的行数、列数、元素个数

二维数组的初始化

二维数组在内存中的布局体现

字符串

指针


一维数组

        数组名不是指针,但是除了单独作为sizeof()和&的操作数,编译器会将数组名隐式转换为一个指向数组首元素的指针来处理(数组名被隐式转换成数组首元素地址)。

        作为sizeof()和&的操作数时,数组名表示整个数组

        具体的解释说明请看下面的程序以及注释

int main()
{
	int a[] = { 1, 2, 3, 4 };
	
	printf("数组a[0]占用内存的字节数:%d\n", sizeof(a[0]));
	//注意sizeof的参数只需要填a,不可以填a[]。计算整个数组占用内存的大小,单位为Byte
	printf("数组a[]占用内存的字节数:%d\n", sizeof(a));			
	printf("数组a[]的元素个数:%d\n", sizeof(a)/sizeof(a[0]));

	printf("\n");
	//a 和 &a[0] 的数值和意义都是一样的,都是数组首元素的地址,是一个int类型的指针
	// &a 和上面二者的数值上虽然也是一样的,但是意义不一样,&a 表示的是整个数组的地址,是一个数组a[]类型的指针
	printf("a     = %p\n", a);			//数组首元素的地址,是一个int类型的指针
	printf("&a[0] = %p\n", &a[0]);		//数组首元素的地址,是一个int类型的指针
	printf("&a    = %p\n", &a);			//整个数组的地址,数值上等于数组首元素的地址,是一个数组a[]类型的指针

	printf("\n");
	printf("a+1     = %p\n", a+1);		//数组首元素的地址 + 一个数组元素的大小,跳过了第一个元素
	printf("&a[0]+1 = %p\n", &a[0]+1);	//数组首元素的地址 + 一个数组元素的大小,跳过了第一个元素
	printf("&a+1    = %p\n", &a+1);		//整个数组的地址 + 整个数组的大小,跳过了整个数组
	return 0;
}

运行结果如下:

同时,注意区分【数组名】【数组元素】

        对于数组元素中的方括号,[ ],其实是一个操作符,下标引用操作符,专门用来访问数组元素的操作符。它有两个操作数,一个是前面的数组名,一个是方括号里面的下标

(两个操作数的位置可以交换,不影响结果,即a[3]与3[a]是一样的,但不建议这样写)。

它的功能是这样的:

数组元素,a[i]转换成*(a+i)
例如:a[3]转换成*(a+3)

//*(a+3)与*(3+a)是一样的,这就解释了,为什么两个操作数的位置可以交换,不影响结果

还有一些容易搞混淆的概念:

&arr[2] == arr + 2;		//数组arr的第三个元素的地址
arr[2] == *(arr + 2);	//数组arr的第三个元素的值
*arr + 2				//数组arr第一个元素的值加2

一维数组的传参与调用

将数组作为参数传递给函数,有两种方法(两种方法的本质是一样的,只是书写形式不一样)

void myFunction(int* array, int size);     //指针表示法
void myFunction(int array[], int size);    //数组表示法
//上面两种方法都表示array是一个指向int类型的指针变量,是等价的
//但是只有在函数原型或者函数定义头中,才可以用int array[]代替int* array
//指针表示法,可以使函数名隐式转换成指针变量这个属性表示得更加明显,也更加接近机器语言
//数组表示法,可以让函数是处理数组的这一意图更加明显,比较直观,更容易理解

       

调用方式是一样的,都是

myFunction(myArray, size);

//不需要在myArray后面加上中括号[ ],如果加了,就会报错

        在函数内部操作数组元素的方法,和在数组定义处操作数组元素的方法是一样的:

array[i] = array[i] * 2;

//array[i] 会自动转换成 *(array + i)

        在开头就说了, 数组名不是指针,但是除了作为sizeof()和&的操作数,编译器会将数组名隐式转换为一个指向数组首元素的指针来处理(数组名被隐式转换成数组首元素地址)。

        所以将数组作为参数传递给函数时,实际上传递的是地址。仅仅有地址,我们通常是无法计算数组的长度的,所以在将数组作为参数传递给函数时,通常还会吧数组的大小给传进去。


二维数组以及多维数组

        弄明白二维数组对于我们弄明白多维数组的帮助是非常大的。

        理解二维数组的一个比较好的方法是:将二维数组看做是数组的数组

    //我们创建一个二维数组(不初始化它)
    int dimension_2[3][2];

    //主数组(master array)有3个元素(3行)
    //这3个元素的元素名分别为dimension_2[0]、dimension_2[1]、dimension_2[2]

    //每个元素都是一个内含两个int值的数组
    //比如元素dimension_2[0]就包含了int类型的dimension_2[0][0]和dimension_2[0][1]

我们上面说到

        数组名不是指针,但是除了单独作为sizeof()和&的操作数,编译器会将数组名隐式转换为一个指向数组首元素的指针来处理(数组名被隐式转换成数组首元素地址)。

        对于一维数组

    //对于一维数组
    int array1[] = {1, 2, 3, 4, 5, 6};
    array1 == &array1[0]; 

        那么对于二维数组呢?

    //对于二维数组
    int array[3][4] = { { 1,  2,  3,  4},
                        { 5,  6,  7,  8},
                        { 9, 10, 11, 12} };

    printf("array        = %p\n", array);
    printf("array[0]     = %p\n", array[0]);    
    printf("array[0][0]  = %p\n", array[0][0]);
    printf("\n");

    printf("&array       = %p\n", &array);
    printf("&array[0]    = %p\n", &array[0]);
    printf("&array[0][0] = %p\n", &array[0][0]);
    printf("\n");

    printf("array + 1        = %p\n", array + 1);
    printf("array[0] + 1     = %p\n", array[0] + 1);
    printf("array[0][0] + 1  = %p\n", array[0][0] + 1);
    printf("\n");

    printf("&array + 1       = %p\n", &array + 1);
    printf("&array[0] + 1    = %p\n", &array[0] + 1);
    printf("&array[0][0] + 1 = %p\n", &array[0][0] + 1);
    printf("\n");

        从运行结果可以看出来,在数值上,

        array == array[0] == &array == &array[0] == &array[0][0]

        为什么会这样呢?看明白下面这张图就可以理解了。

记住一个概念,数组名被隐式转换成数组首元素地址

        

    //相信现在,你对数组指针有了更深的理解了
    // 1.
    array == &array[0]

    // 2.
    array[0] == &array[0][0]

    //那么我们现在对 1 进行解引用
    // 3.
    *array == *&array[0]
    *array == array[0]

    //我们把 2 和 3 结合一下
    // 4.
    *array == array[0] == &array[0][0]
    *array == &array[0][0]

    //对④进行解引用
    // 5.
    **array == *&array[0][0]
    **array == array[0][0]
    //我们对array进行了两次解引用,得到了一个int类型的数据,这说明,array是地址的地址
    //或者说array是指针的指针,它指向的指针指向int类型的数据array[0][0]
    //地址的地址或者指针的指针就是【双重间接】(double indirection)的例子

二级指针(变量)中存放的是一级指针(变量)的地址

对二维数组名解引用两次,就可以得到数组中的值。        

  • 使用两个解引用操作符(*)
  • 或者使用两个方括号([ ])
  • 或者使用一个解引用操作符(*)和一个方括号([ ])

都可以达到这个目标。

下面的内容参考了《C Primer Plus》的P303

//与array[2][1]等价的指针表示法是*(*(array + 2) +1)。
//这看起来有点复杂,但是可以利用下面的思路来帮助理解
array                   //二维数组的首元素的地址(每个元素都是内含4个int值的一维数组)
array + 2               //二维数组的第3个元素(一维数组)的地址
* (array + 2)           //二维数组的第3个元素(一维数组)的首元素(1个int值)的地址
* (array + 2) + 1       //二维数组的第3个元素(一维数组)的第2个元素(也是1个int值)的地址
* (* (array + 2) + 1)   //二维数组的第3个元素(一维数组)的第2个元素(也是1个int值)的值,即二维数组的第3行第2列的值(array[2][1])

        以上的分析只是说明,指针表示法( *(*(array + 2) +1) )和数组表示法(array[2][1])之间是可以相互转换的,如果你在程序中恰好用到了指向二维数组的指针,而且要通过该指针获取值时,最好使用简单的数组表示法,而不是使用指针表示法。

        编译器会将数组表示法转换成指针表示法。

二维数组的传参与调用

在说二维数组前,先复习一下一维数组的

void myFunction(int* array, int size);     //指针表示法
void myFunction(int array[], int size);    //数组表示法

二维数组同样有好几种方法

  • 将二维数组看做一维数组时
  • 将二维数组看做一维数组的数组时

请参考下面的文章,如果有时间,我可能会单独写一篇笔记来写这个:

109. 【C语言进阶】指针进阶_数组指针的使用,数组参数和指针参数_哔哩哔哩_bilibili

二维数组作为形参--传值的3三种方式_二维数组做形参的表示方法-CSDN博客

二维数组做形参写法_二维数组作为形参的定义格式-CSDN博客

C语言如何传递动态二维数组做函数参数 - 知乎

我在这里只说一点:

        在声明一个指向N维数组的指针时,只能省略最左边方括号中的值(写上值也可以,但是值将被忽略)。

        第一对方括号只用于表明这是一个指针,而其他的方括号则用于描述指针所指向数据对象的类型。

        比如,下面的声明是等价的

int TestFunction(int arr[][12][20][30], int rows);
int TestFunction(int (*arr)[12][20][30], int rows);    //注意(*arr)的括号是不能省略的

//这里把四维四组看做是三维数组的数组
//arr是指向12*20*30的int数组
int array[10];

这整个数组的类型就是    int [10]
void myFunction(int* array[]);
void myFunction(int* (*array));
void myFunction(int** array);

//上面三种表示方法等效,都是将二维数组看做是一维数组
//但是前两种写法不建议,太不常规了,需要思考才可以理解意思

void myFunction(int array[][4]);
void myFunction(int (*array)[4]);

//上面两种表示方法等效,都是将二维数组看做“内含4个int值的一维数组”的数组

//无论是一维数组还是N维数组,在声明时,都需要告诉编译器,这个指针所指向数据对象的类型是什么

//一维数组很容易解决这个问题,因为一维数组的元素都是基础数据类型

//但是当我们把N维数组看做(N-1)维数组的数组时,就需要注意,这个(N-1)维数组是什么类型的?
//除第一个方括号外的其余方括号就起到告诉编译器这个(N-1)维数组的类型的作用。

//对于二维数组写成这样(int array[][4]),就是告诉编译器
//我的(N-1)维数组的类型是包含4个int类型的一维数组

        那么,将二维数组看做一维数组,和将二维数组看做一维数组的数组都可以正确声明,那么他们之间有什么区别吗?

        他们所包含的信息不一样

  • 将二维数组看做一维数组,第一维、第二维的大小信息都没有
  • 将二维数组看做一维数组的数组,没有第一维的大小信息,有第二维的大小信息

计算二维数组的行数、列数、元素个数

    //创建一个二维数组,并初始化为0
    int arr[5][12] = {0};

    //计算二维数组的行数
    int row = 0;
    row = sizeof(arr) / sizeof(arr[0]);
    printf("row    = %d\n", row);

    //计算二维数组的列数
    int column = 0;
    column = sizeof(arr[0]) / sizeof(arr[0][0]);
    printf("column = %d\n", column);
    
    //计算二维数组的元素个数
    int total = 0;
    total = sizeof(arr) / sizeof(arr[0][0]);
    printf("total  = %d\n", total);

运行结果为:

二维数组的初始化

        无论是多少维的数组,在初始化时,只有第一维的下标可以省略。

        对应到二维数组,就是可以不指定行数,但是必须指定列数。

        内部的花括号是可以省略的,但是最外面的花括号不可以省略。

下面是正确的初始化例子:

    int a1[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
    int a2[3][4] = {{ 0, 1,  2,  3}, { 4, 5,  6,  7},{ 8, 9, 10, 11}};
    int a3[3][4] = {{ 0, 1,  2,  3},
                    { 4, 5,  6,  7},
                    { 8, 9, 10, 11}};

    //不完全初始化,编译器会认为这是一个2*4的数组,后面没有初始化的自动补0
    int a4[][4] = { 1 ,2, 3, 4, 5, 6};

    //不完全初始化,就初始化了三个元素,其余元素被初始化为0
    //分别是第一行的第一个元素、第二行的第一个元素、第三行的第一个元素
    //a[0][0] = 1 , a[1][0] = 2 , a[2][0] = 3
    int a5[3][4] = {{1}, {2}, {3}};

下面是错误的初始化例子:

//无法确定列数,无法确定数组的布局
int e1 = [3][] = {1, 2, 3, 4, 5, 6};

//创建了数组,但是没有初始化,无法确定行数
int e2 = [][4];

//创建了数组,但是没有初始化,无法确定行数、列数
int e3 = [][];

       

        在C99标准中,还可以使用指定初始化器来进行初始化,具体内容可以参考《C Primer Plus》的P281,这里不多介绍。

二维数组在内存中的布局体现

        无论是多少维的数组,在内存当中都是连续存放的,存放的顺序按照维度的顺序来。

        对应到二维数组,就是先行后列

    int arr1[12] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
    int arr2[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

    //arr2[3][4]也可以写成这样
    int arr3[3][4] = {{ 0, 1,  2,  3}, 
                      { 4, 5,  6,  7},
                      { 8, 9, 10, 11}};

        在内存的视角去看,一维数组和二维数组的布局是一样的。


字符串

创建字符串的两种方法

char arr[] = "hello";
//(hello)是存储在数组里面的,在栈区,可以修改它的内容

char* arr = "hello";
//指针指向字符串时,字符串是常亮,存储在常量区(常量区数据不可修改),而指针存储在栈区
//arr是一个指向常亮字符串的指针,可以改变arr的指向,但是不能改变指向的内容(hello)

指针

有一个指针变量p

*p++ , 即*(p++) , 先取内容,后指针+1

(*p)++ , 先取内容,后内容+1

*++p , 即*(++p) , 先指针+1,后取内容

在定义变量的语句中,会出现两个东西:变量名、类型。

把变量名去掉,剩下的就是类型。

//有这样一个一维数组
int arr1[10] = {0};
//变量名(数组名):arr1
//类型:int[10]

//如果我们想创建一个指向这一整个一维数组的指针,该如何操作?
int(*p1)[10] = &arr1;
//变量名(指针名):p1
//类型:int(*)[10],,,指向有十个元素的一维数组的指针,p1是指针,指向int[10]



//升级一下难度
//有这样一个一维指针数组
int* arr1[10] = {0};
//变量名(数组名):arr1
//类型:int*[10]

//如果我们想创建一个指向这一整个一个一维指针数组的指针,该如何操作?
int*(*p1)[10] = &arr1;
//变量名(指针名):p1
//类型:int*(*)[10],,,指向有十个元素的一维指针数组的指针,p1是指针,指向int*[10]

//同样,有一个这样的二维数组
int arr2[3][4] = {0};
//变量名(数组名):arr2
//类型:int[3][4]


//如果我们想创建一个指向这一整个二维数组的指针,该如何操作?
int(*p2)[3][4] = &arr2;
//变量名(指针名):p2
//类型:int(*)[3][4],,,指向三行四列大小的二维数组的指针,p2是指针,指向int[3][4]

typedef int arr9[9];
//这样的一句代码,是什么意思呢?是可以用arr9[9]来代替int的意思吗?

//不是
//是可以用arr9来代替int[9]这种类型

//arr9 test;
//这就相当于——int test[9]
//数组名(变量名):test
//类型:int [9]

写完之后,总感觉不够满意,后续应该会进行修改

;