C语言指针系列 - 一级指针.一维数组,二级指针,二维数组,指针数组,数组指针,函数指针,指针函数
1. 数组名
C语言中的数组名是一个特殊的存在, 从本质上来讲, 数组名是一个地址, 我们可以打印一个指针的值,和打印一个数组的值来观察出这个本质:
int nArray[10] ={ 0 };
int *p = nArray;
printf("nArray:%p , p = %p\n", nArray,p);
打印出来的将会是两个相同的值.但是数组名并不意味着和指针完全相同, 数组名还有另一个本质,数组名是一个常量,不允许被赋值. 但指针允许被赋值,例如:
p = nArray;
nArray = p; // 语句将会被报错:表达式必须可修改的左值
因此,我们可以得到数组名的两个性质:
1. 是一个地址.
2. 是一个常量地址.
只要不对数组名进行赋值, 对指针进行的任何操作都可以应用在数组名上, 例如对数组名取值, 对数组名解引用,进行算术运算:
1 printf("%d",nArray ); //取数组名的值,并将这个值打印.
2 int n = *nArray; //对数组名解引用,取得一个地址上的值.
3 nArray + 1; //对数组名进行算术运算,返回一个被增加一个单位的地址
2. 二级指针和多级指针
2.1 一级指针的使用
当我们对一个指针解引用时, C语言会为我们从指针变量中取出一个地址,随后再从这个地址中取出一个值.
例如:
1 int a = 10; /*a的地址等于0x1000*/
2 int *p = &a;/*此时p保存的值是0x1000*/
3 printf("%d",*p);
在第三条语句中, *p的作用就是对指针解引用, 在解引用时,发生了两个动作:
1. 对指针变量p取值, 取出0x1000
2. 将上一步得到的0x1000上进行*运算符的操作,该操作得到0x1000上的整型值
因此,这个表达式的结果就是10;
2.2 二级指针的本质
再说到主题二级指针, 二级指针的本质也和一级指针一样,这里的一样指的是这两种属性:
1. 占用4字节的空间
2. 保存的值是一个地址
但定义二级指针的语法是不一样的,定义二级指针也比较简单: int **p;
在定义时使用两个*运算符.
对于一级指针能够进行的操作,在二级指针上面同样是可行的,而且得到的是一致的效果.
如:
1 int **p = (int**)0x1000; // 允许
2 int **p2 = p + 1 ; // 允许,p2的值是p被增加一个单位的值.即0x1004
3 int n = *p; // 错误
我们可以看到, 第三条语句将会被报以"int*的值不能赋值到int型的实体"的编译错误.
这是因为, *p表达式的结果是一个指针.如果你想要从一个二级指针中得到一个值.那么你需要再加上一个*运算符:
int n = **p;
这条语句可以分解成一下步骤:
1. 取变量p的值 => 0x1000
2. 对0x1000解引用,得到0x1000保存的值
3. 把第二步得到的值当成是一个地址,再对这个地址解引用, 得到这个地址的值
因此,你可以看到, 在对一级指针解引用时, 分解的步骤到第2步就结束了. 但对二级指针,还需要进行第3步.
2.3 二级指针引用的规则和规律
2.3.1 规则:
对一级指针进行解引用时, 得到的结果将是一个变量的值.
对二级指针进行解引用时, 得到的结果是一个一级指针.
2.3.2 规律
对三级指针进行解引用时, 得到的结果是一个二级指针.
对n级指针进行解引用时,得到的结果是一个(n-1)级指针.
3. 二维数组的解引用
3.1 二维数组名
一维数组的本质就是一个常量一级指针, 而二维数组的本质就是一个常量二级指针.因此, 只要不对这个常量进行赋值, 任何施加在二级指针上的可允许操作都能够施加在二维数组名上.
3.2 将二维数组名赋值给一个指针.
一维数组名可以直接赋值给一个一级指针. 但是二维数组却不能直接赋值给一个二级指针.
在将二维数组赋值给一个二级指针时,我们需要考虑两件事情:
int nArray[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
int** pp = (int**)nArray; // 通过强制类型转换可以将二维数组名赋值给二级指针
int n1 = nArray[1][2] ; // n1的值将等于6
int n2 = pp[1][2]; // 执行到此处时将会被报错.
我们可以通过对int n2 = pp[1][2];这条语句进行分解, 来揪出错误之源:
1. 对变量pp取值 => 得到数组nArray的首地址
2. 地址[1] ==转换为=> *(地址+1)
3. 将上一步的结果当成一个地址:
地址[2] ==转换为=> *(地址+2)
在这三步分解中, 第二步时, 取到的结果是2(数组首地址的第二个元素的值). 但在进行第3步时, 2被当成一个地址,进行了解引用, 错误的根源就在这里了.因此, 我们也可以看出, 将一个二级指针当成数组来使用时, 二级指针的内存模型将如下图所示:
二级指针将是以下内存格子的首地址,每个格子将占用4个字节. 当我们对一个二级指针解引用时, 得到的是一个地址,这个地址就是一个一级指针. 当我们将一个二级指针增加一个单位时, 得到的结果就是第二个格子的内容,也是一个内存地址.
+--------+ [0x1000]
| 地址1 |
+--------+ [0x1004]
| 地址2 |
+--------+ [0x1008]
| 地址3 |
+--------+ [0x100C]
| 地址4 |
+--------+ [0x1010]
但一个二维数组中的在内存中的模型如下图所示:
+--------+
| 元素1 |
+--------+
| 元素2 |
+--------+
| 元素3 |
+--------+
| 元素4 |
+--------+
而nArray[1][2] 这样的语法将会被转换成: *(数组首地址 + (1 * 低维数组的最大下标 + 2)),而对二级指针进行p[1][2]时, 这样的语法将转换成: *( *( p + 1 ) + 2).
我们可以看到, 在对数组nArray解引用时, 编译器会将1解释一个单位, 这个单位就是: 低维度数组的最大下标.
而一个二级指针中, 并没有保存低维度数组最大下标这样的信息.
因此, 一个二维数组赋值给一个二级指针时, 即使二级指针可以使用[]运算符,但得到的结果将完全不一样. 因为二级指针在对一个地址解引用时,没有把这个地址当成是一个数组名.不是数组名, 就没有想要保存一个二维数组的地址, 只能使用数组指针.
4. 数组指针
草莓, 是一种水果, 蛋糕, 是一种糕点. 草莓蛋糕是一种蛋糕.
数组, 是相同类型的变量的集合. 指针, 是一个保存变量地址的变量. 数组指针是一种指针.
因此, 数组指针从本质上来说, 它是一个占用4个字节的指针.
数组有不同维度的数组, 如一维数组, 二维数组.
在保存一维数组时, 我们可以直接使用一个一级指针来保存.
保存二维数组时, 如果使用二级指针保存二维数组的地址,会导致二维数组的低维度最大下标的信息丢失. 而数组指针这样的类型将能够保存低维度最大下标信息.
4.1 定义数组指针:
int nArray[3][3];
int (*p)[3] = nArray;
语句2就是在定义一个数组指针p , 在语句2中, *p必须被圆括号括起来, 这样一来,就能使*运算符先和变量p结合, 让p变成一个一级指针变量, 而[3]让指针p变成一个指向最低维度为3的数组指针.
这个数组指针就只能保存最低维度为3的二维数组,而二维数组的高维度的最大小标数将被会限制.如:
int nArray1[10][3] , nArray2[3][10];
int (*p)[3];
p = nArray1; // 语法通过
p = nArray2; // 语法不通过,因为nArray2的低维度最大下标是10,而p只能指向低维度最大小标为3的的数组
4.2 对数组指针进行解引用
int nArray[3][3];
int (*p)[3] = nArray;
nArray[2][1]; 和 p[2][1] 将会得到一个一致的结果.
4.3 保存多维数组的数组指针:
int nArray[4][3][3];
保存这样的指针, 指针必须能够描述低维度的最大下标.
int (*p)[3][3];
4.4 传递一个多维数组到函数.
在传递时, 只需要根据要传递的数组的低维度最大下标来定义形参即可.
即将形参定义成一个数组指针.
5. 指针数组.
指针数组, 从本质上来说是一个数组,数组中保存的每一个元素都是一个指针.指针数组也有多种形式:
1. 保存一级指针的数组
int *p[10];
这里的标识符先和[]结合,说明p是一个具有10元素的数组. int* 说明这个数组中的每一个元素是都是int类型的一级指针.
2. 保存多级指针的数组
int** p[10];
3. 保存数组指针的数组
3.1 保存二维数组的数组指针的数组
int (*p[5])[10]; // 标识符p先和[]结合,说明p是一个具有5个元素的数组, 在和int(*)[10]结合, 说明数组中的每一个元素都是指向数组的指针.
3.2 保存多维数组的数组指针的数组
int (*p[5])[3][3];
6. 函数指针
现在, 我们学会了如何定义一个指针来保存数组的地址.
在c语言当中, 函数是有地址的, 这个地址中,保存着这个函数的代码(机器码).函数名就是这些机器码的首地址. 当我们使用一个函数时, 使用到的就是函数的地址.但不仅仅是函数的地址.
一个函数名包含着以下信息:
1. 函数地址
2. 参数列表,即函数的参数个数,每个参数的数据类型
3. 函数的返回值类型
因此, 如果要保存一个函数的地址, 也需要额外保存另外两个信息. c语言提供了类似的语法让我们能够保存函数地址和额外信息的指针, 就是函数指针.
int (*pfn)(int arg1, int arg2);
在这一条语句中, 定义了一个标识符pfn, 这个标识符pfn被()括起来, 所以,它先和*运算符结合, 说明pfn是一个一级指针.
这个指针指向的类型是 : int (*)(int arg1, int arg2); 而这这种类型就是一个函数的类型, ()左边是函数的返回值, ()右边是函数的参数列表.
6.1 函数指针的赋值
数类型也必须匹配才能够赋值.
6.2 函数指针的使用.
正如一个数组指针可以被当成真正的数组名来说用一样. 函数指针也能够被当成真正的函数名来使用. 如:
int n = pfn(1,2);
这条语句会先对变量pfn取值,将取出的值当成是一个函数地址去调用一个函数,并将实参1和2传入到函数中, 最后将函数表达式的结果,即函数的返回值赋值给变量n.
7. 指针函数
7.1 返回普通指针的函数
当一个函数的返回值是一个函数指针时, 这个函数就被称为指针函数.
例如:
char* strstr( const char* str,const char* sub );
fun是一个函数, 它的返回值类型是char*
这种指针函数相对来说还是比较简单, 我们想要保存这样的函数的地址时, 我们需要定义这种类型的函数的指针:
char* (*pfn)( const char* str,const char* sub );
7.2 返回函数指针的函数
一个函数可以返回另一个函数的地址.这个时候,它的返回值就是一个函数指针了.
如:
char* (*)( const char* str,const char* sub ) fun(int nIndex);
标识符fun是一个函数, 标识符fun的左边是它的函数返回值,标识符右边是它函数参数列表.但实际上C语言不支持这个语法, 需要将fun(int nIndex)部分写在(*)中:
char* (*fun(int nIndex))( const char* str,const char* sub );
现在, 我们可以重新来解释这一条语句.
1. fun先和参数列表结合,说明fun是一个函数.
2. 这个函数的参数列表就是int nIndex.
3. 这个函数的返回值类型就是去除函数名,和参数列表的部分:
char* (*)( const char* str,const char* sub );
4. 返回值是一个指针. 这个指针是一个函数指针. 这种函数指针拥有char*类型的返回值,参数列表中有2个都是const char*类型的形参.