C语言指针系列 - 一级指针.一维数组,二级指针,二维数组,指针数组,数组指针,函数指针,指针函数
1. 数组名
C语言中的数组名是一个特殊的存在, 从本质上来讲, 数组名是一个地址, 我们可以打印一个指针的值,和打印一个数组的值来观察出这个本质:
int nArray[10] ={ 0 };
int *p = nArray;
printf("nArray:%p , p = %p\n", nArray,p);
打印出来的将会是两个相同的值.但是数组名并不意味着和指针完全相同, 数组名还有另一个本质,数组名是一个常量,不允许被赋值. 但指针允许被赋值,例如:
p = nArray;
nArray = p; // 语句将会被报错:表达式必须可修改的左值
因此,我们可以得到数组名的两个性质:
1. 是一个地址.
2. 是一个常量地址.
只要不对数组名进行赋值, 对指针进行的任何操作都可以应用在数组名上, 例如对数组名取值, 对数组名解引用,进行算术运算:
printf("%d",nArray ); //取数组名的值,并将这个值打印.
int n = *nArray; //对数组名解引用,取得一个地址上的值.
nArray + ; //对数组名进行算术运算,返回一个被增加一个单位的地址
2. 二级指针和多级指针
2.1 一级指针的使用
当我们对一个指针解引用时, C语言会为我们从指针变量中取出一个地址,随后再从这个地址中取出一个值.
例如:
int a = ; /*a的地址等于0x1000*/
int *p = &a;/*此时p保存的值是0x1000*/
printf("%d",*p);
在第三条语句中, *p的作用就是对指针解引用, 在解引用时,发生了两个动作:
1. 对指针变量p取值, 取出0x1000
2. 将上一步得到的0x1000上进行*运算符的操作,该操作得到0x1000上的整型值
因此,这个表达式的结果就是10;
2.2 二级指针的本质
再说到主题二级指针, 二级指针的本质也和一级指针一样,这里的一样指的是这两种属性:
1. 占用4字节的空间
2. 保存的值是一个地址
但定义二级指针的语法是不一样的,定义二级指针也比较简单: int **p;
在定义时使用两个*运算符.
对于一级指针能够进行的操作,在二级指针上面同样是可行的,而且得到的是一致的效果.
如:
int **p = (int**)0x1000; // 允许
int **p2 = p + ; // 允许,p2的值是p被增加一个单位的值.即0x1004
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*类型的形参.
C语言指针系列 - 一级指针.一维数组,二级指针,二维数组,指针数组,数组指针,函数指针,指针函数的更多相关文章
- C语言数组:C语言数组定义、二维数组、动态数组、字符串数组
1.C语言数组的概念 在<更加优美的C语言输出>一节中我们举了一个例子,是输出一个 4×4 的整数矩阵,代码如下: #include <stdio.h> #include &l ...
- PHP如何判断一个数组是一维数组或者是二维数组?用什么函数?
如题:如何判断一个数组是一维数组或者是二维数组?用什么函数? 判断数量即可 <?php if (count($array) == count($array, 1)) { echo '是一维数组' ...
- poj3067 二维偏序树状数组
题解是直接对一维升序排列,然后计算有树状数组中比二维小的点即可 但是对二维降序排列为什么不信呢?? /* */ #include<iostream> #include<cstring ...
- 06-01 Java 二维数组格式、二维数组内存图解、二维数组操作
二维数组格式1 /* 二维数组:就是元素为一维数组的一个数组. 格式1: 数据类型[][] 数组名 = new 数据类型[m][n]; m:表示这个二维数组有多少个一维数组. n:表示每一个一维数组的 ...
- 洛谷 P1972 [SDOI2009]HH的项链-二维偏序+树状数组+读入挂(离线处理,思维,直接1~n一边插入一边查询),hahahahahahaha~
P1972 [SDOI2009]HH的项链 题目背景 无 题目描述 HH 有一串由各种漂亮的贝壳组成的项链.HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含 ...
- C++ 指针二维数组, C++二维指针数组笔记
C++ 二维动态数组 一. 已知第一维 #include <iostream> using namespace std; int main(int argc, char const *ar ...
- C语言 一维数组叠加为二维数组样例
这里参看memcpy的用法,将一个一维整型数组不停的叠加为二维数组 使用宏定义来控制二维数组的行列 代码如下: #include <stdio.h> #include <stdlib ...
- 计算机二级-C语言-对标志位的巧妙使用。对二维数组数据进行处理。对文件进行数据输入。
//函数fun的功能是:计算形参x所指数组中平均值(规定所有数均为正数),将所指数组中大于平均值的数据移至数组的前部,小于等于的移至后部,平均值作为返回值,在主函数中输出平均值和后移的数据. //重难 ...
- PHP多个一维数组合并成二维数组的简易方法
当我们需要进行数组遍历数据的时候,需要将多个一维数组进行二维的转换,方法很简单.如下: <?php $a= array('张三','李四','王五'); $b= array ('23','24' ...
随机推荐
- mosquitto.conf之log配置
# ================================================================= # Logging # 日志信息 # ============= ...
- 用Go语言异常机制模拟TryCatch异常捕捉
有的同学看到Go和TryCatch一起出现,心里可能会说,难道Go语言升级了,加入了try...catch语句.哈哈,其实Go语言从创建之初就没打算加入try...catch语句,因为创建Go的那帮大 ...
- 洛谷 - P2278 - 操作系统 - 模拟
https://www.luogu.org/problemnew/show/P2278 题目没有说同时到达的优先级最大的应该处理谁. 讲道理就是处理优先级最大的. #include<bits/s ...
- 201621123016 《Java程序设计》第6周学习总结
1. 本周学习总结 1.1 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图或相关笔记,对面向对象思想进行一个总结. 注1:关键词与内容不求多,但概念之间的联系要清晰 ...
- ZOJ3359【阅读理解】
前言: 和队友一发入魂,很强势. 比赛中题目长的,就和队友一起读,这样比较快,然后还不会梦游,把点一句一句地搞出来. 思路: 在头5次,每次有人踢球就可能会输. 后面谁没进,对方进了救输. 代码: / ...
- mongodb c# 序列化时 , Id引起的问题
1. c# 序列化时,如果没有指名_id , 如果class,struct有MemberName为 Id ,_id , 则自动识别为Id . 如果此时,这个"Id"是只读属性,就 ...
- NSString 是否存在空格
NSString *_string = [NSString stringWithFormat:@"123 456"]; NSRange _range = [_string rang ...
- nil 与 release
nil就是把一个对象的指针置为空,只是切断了指针与内存中对象的联系:而release才是真正通知内存释放这个对象. 如果没有release就直接nil,那么虽然不会出错,却等于自己制造内存泄漏了,因为 ...
- Trie树(小)总结 By cellur925
关于\(Trie\)树的详细介绍,还请移步这篇深度好文 基本操作 插入 void insert() { int p=0; int len=strlen(tmp+1); for(int i=1;i< ...
- PHP不重新编译,单独添加模块扩展的方法
php自身提供了很多扩展,比如curl,gmp, mbstring等.我们在编译安装php时未必安装了所有扩展.那么在安装完php后,如果想单独安装某个php自身的扩展怎么办呢? 我们以curl扩展模 ...