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语言指针系列 - 一级指针.一维数组,二级指针,二维数组,指针数组,数组指针,函数指针,指针函数的更多相关文章

  1. C语言数组:C语言数组定义、二维数组、动态数组、字符串数组

    1.C语言数组的概念 在<更加优美的C语言输出>一节中我们举了一个例子,是输出一个 4×4 的整数矩阵,代码如下: #include <stdio.h> #include &l ...

  2. PHP如何判断一个数组是一维数组或者是二维数组?用什么函数?

    如题:如何判断一个数组是一维数组或者是二维数组?用什么函数? 判断数量即可 <?php if (count($array) == count($array, 1)) { echo '是一维数组' ...

  3. poj3067 二维偏序树状数组

    题解是直接对一维升序排列,然后计算有树状数组中比二维小的点即可 但是对二维降序排列为什么不信呢?? /* */ #include<iostream> #include<cstring ...

  4. 06-01 Java 二维数组格式、二维数组内存图解、二维数组操作

    二维数组格式1 /* 二维数组:就是元素为一维数组的一个数组. 格式1: 数据类型[][] 数组名 = new 数据类型[m][n]; m:表示这个二维数组有多少个一维数组. n:表示每一个一维数组的 ...

  5. 洛谷 P1972 [SDOI2009]HH的项链-二维偏序+树状数组+读入挂(离线处理,思维,直接1~n一边插入一边查询),hahahahahahaha~

    P1972 [SDOI2009]HH的项链 题目背景 无 题目描述 HH 有一串由各种漂亮的贝壳组成的项链.HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含 ...

  6. C++ 指针二维数组, C++二维指针数组笔记

    C++ 二维动态数组 一. 已知第一维 #include <iostream> using namespace std; int main(int argc, char const *ar ...

  7. C语言 一维数组叠加为二维数组样例

    这里参看memcpy的用法,将一个一维整型数组不停的叠加为二维数组 使用宏定义来控制二维数组的行列 代码如下: #include <stdio.h> #include <stdlib ...

  8. 计算机二级-C语言-对标志位的巧妙使用。对二维数组数据进行处理。对文件进行数据输入。

    //函数fun的功能是:计算形参x所指数组中平均值(规定所有数均为正数),将所指数组中大于平均值的数据移至数组的前部,小于等于的移至后部,平均值作为返回值,在主函数中输出平均值和后移的数据. //重难 ...

  9. PHP多个一维数组合并成二维数组的简易方法

    当我们需要进行数组遍历数据的时候,需要将多个一维数组进行二维的转换,方法很简单.如下: <?php $a= array('张三','李四','王五'); $b= array ('23','24' ...

随机推荐

  1. mosquitto.conf之log配置

    # ================================================================= # Logging # 日志信息 # ============= ...

  2. 用Go语言异常机制模拟TryCatch异常捕捉

    有的同学看到Go和TryCatch一起出现,心里可能会说,难道Go语言升级了,加入了try...catch语句.哈哈,其实Go语言从创建之初就没打算加入try...catch语句,因为创建Go的那帮大 ...

  3. 洛谷 - P2278 - 操作系统 - 模拟

    https://www.luogu.org/problemnew/show/P2278 题目没有说同时到达的优先级最大的应该处理谁. 讲道理就是处理优先级最大的. #include<bits/s ...

  4. 201621123016 《Java程序设计》第6周学习总结

    1. 本周学习总结 1.1 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图或相关笔记,对面向对象思想进行一个总结. 注1:关键词与内容不求多,但概念之间的联系要清晰 ...

  5. ZOJ3359【阅读理解】

    前言: 和队友一发入魂,很强势. 比赛中题目长的,就和队友一起读,这样比较快,然后还不会梦游,把点一句一句地搞出来. 思路: 在头5次,每次有人踢球就可能会输. 后面谁没进,对方进了救输. 代码: / ...

  6. mongodb c# 序列化时 , Id引起的问题

    1.  c# 序列化时,如果没有指名_id , 如果class,struct有MemberName为 Id ,_id , 则自动识别为Id . 如果此时,这个"Id"是只读属性,就 ...

  7. NSString 是否存在空格

    NSString *_string = [NSString stringWithFormat:@"123 456"]; NSRange _range = [_string rang ...

  8. nil 与 release

    nil就是把一个对象的指针置为空,只是切断了指针与内存中对象的联系:而release才是真正通知内存释放这个对象. 如果没有release就直接nil,那么虽然不会出错,却等于自己制造内存泄漏了,因为 ...

  9. Trie树(小)总结 By cellur925

    关于\(Trie\)树的详细介绍,还请移步这篇深度好文 基本操作 插入 void insert() { int p=0; int len=strlen(tmp+1); for(int i=1;i< ...

  10. PHP不重新编译,单独添加模块扩展的方法

    php自身提供了很多扩展,比如curl,gmp, mbstring等.我们在编译安装php时未必安装了所有扩展.那么在安装完php后,如果想单独安装某个php自身的扩展怎么办呢? 我们以curl扩展模 ...