相关文章链接 :
1.【嵌入式开发】C语言 指针数组 多维数组
2.【嵌入式开发】C语言 命令行参数 函数指针 gdb调试
3.【嵌入式开发】C语言 结构体相关 的 函数 指针 数组
4.【嵌入式开发】gcc 学习笔记(一) - 编译C程序 及 编译过程
5.【C语言】 C 语言 关键字分析 ( 属性关键字 | 常量关键字 | 结构体关键字 | 联合体关键字 | 枚举关键字 | 命名关键字 | 杂项关键字)
6.【C 语言】编译过程 分析 ( 预处理 | 编译 | 汇编 | 链接 | 宏定义 | 条件编译 | 编译器指示字 )
7.【C 语言】指针 与 数组 ( 指针 | 数组 | 指针运算 | 数组访问方式 | 字符串 | 指针数组 | 数组指针 | 多维数组 | 多维指针 | 数组参数 | 函数指针 | 复杂指针解读)

文章目录

  • 一. 指针
    • 1. 指针 简介
      • ( 1 ) 指针 概念 ( 本质 | 占用内存 ① 32位 4字节 ② 64 位 8 字节 | * ① 声明指针 ② 获取指向的值 )
      • ( 2 ) 指针 简单示例 ( * 的读写内存作用 | 指针相关类型大小)
    • 2. 传值 和 传址 调用
      • ( 1 ) 相关概念 ( 传值调用 复制实际值 | 传址调用 复制地址值 )
      • ( 2 ) 传址调用 ( 改变外部变量值 )
    • 3. 常量 和 指针
      • ( 1 ) 相关概念 ( 核心原则 左数右指 | 左数 ① const int* p ② int const* p 数据时常量 | 右指 int* const 指针是常量 )
      • ( 2 ) 验证 常量 指针 相关概念 ( 左数右指 )
  • 二. 数组
    • 1. 数组 简介
      • ( 1 ) 数组 概念 ( 数组地址 | 数组大小 显示 隐式 声明 | 数组初始化 [ 效率比后期赋值高 ] )
      • ( 2 ) 数组 示例 ( 定义 | 大小 | 初始化 )
    • 2. 数组地址与名称 概念
      • ( 1 ) 数组 概地址 ( 数组名 [ 数组首元素地址 ] 和 &数组名 [ 数组地址 ] | 数组名 类似于 常量指针 | 数组拷贝 )
      • ( 2 ) 数组 示例 ( 数组名 | 地址 | 数组拷贝禁止情况 )
    • 3. 数组 与 指针 区别
      • ( 1 ) 概念简介 ( ① 数组名就是首元素地址 不需要寻址 | ② 指针 中保存一个地址 指向首元素地址 需要寻址 | printf 打印 数组 或 指针 : 根据占位符自动判断打印地址还是打印内存中的具体内容 )
      • ( 2 ) 代码示例 ( 数组 | 指针 编译器处理上的区别 )
  • 三. 数组 指针 分析
    • 1. 指针 加减 运算方式
      • ( 1 ) 指针 加减法 运算 ( 指针指向的位置在同一个数组中改变才有意义 )
      • (2) 数组大小计算示例
      • ( 3 ) 指针 加法运算示例 ( 指针地址 + 4/8 * 被加数 )
      • ( 4 ) 指针 减法 运算示例
    • 2. 指针 比较 运算方式
      • ( 1 ) 指针 比较 运算 ( 大于 小于 大于等于 小于等于 运算的前提是 必须指向同一数组 中的元素 | 任意两指针只能进行 等于 不等于 的比较 )
      • ( 2 ) 指针 比较 运算代码示例 ( 用 指针 遍历数组 )
    • 3. 数组访问方式
      • ( 1 ) 下标 指针 访问 ( 推荐使用下标访问 )
      • ( 2 ) 下标 指针 访问 数组 性能 代码示例
    • 3. int array[]; array 和 &array 区别
      • ( 1 ) int array[] 中 array 和 &array 意义 ( ① array 数组首元素地址 | ② &array 数组地址 )
      • ( 2 ) array 和 &array 计算 代码示例
    • 4. 数组参数
      • ( 1 ) 数组参数 概念 ( 退化成指针 | 需要带上数组长度作为 附属参数 )
      • ( 2 ) 数组参数 代码示例 ( 数组大小 | 数组参数大小 )
    • 5. 数组 指针 对比 ( 内存分配 : ① 指针 分配 4 / 8 字节 ② 数组分配所有元素地址 | 作为参数 | 常量[ 数组 ] 变量[ 指针 ] 区别 )
  • 四. 字符串
    • 1. 字符串概念
      • ( 1 ) 概念 ( 本质 是 char[] 数组 | '\0' 结尾 | 存储位置 栈 堆 常量区 )
      • ( 2 ) 示例代码 ( 字符串概念 | 字符串 )
    • 2. 字符串 长度
      • ( 1 ) 字符串长度计算 ( 不包括 '\0' | 标准库中有该函数)
      • ( 2 ) 代码示例 ( 字符串长度计算示例 )
      • ( 3 ) 代码示例 ( 自己实现 strlen 方法 )
    • 3. 字符串函数 长度不受限制 情况
      • ( 1 ) 不受限制的字符串函数 ( 函数自动寻找 '\0' 确定字符串大小 | stpcpy | strcat | strcmp )
      • ( 2 ) 代码示例 ( 自己实现字符串拷贝函数 )
    • 4. 字符串函数 长度受限制 情况
      • ( 1 ) 受限制的字符串函数 ( 推荐使用 降低错误率 )
  • 五. 指针数组 与 数组指针
    • 1. 数组指针
      • ( 1 ) 数组类型介绍 ( 数组元素类型 | 数组大小 | 举例 int[8] )
      • (2) 数组指针简介 ( 指向数组的 一个 指针 | 数组指针类型定义方式 : 数组元素类型 ( * 指针名称 ) [数组大小] )
      • ( 3 ) 代码示例 ( 定义数组类型 | 数组指针用法 )
    • 2. 指针数组
      • ( 1 ) 指针数组简介 ( 数组中存储的元素是指针 | 数组指针 int (*array)[5] 本质是指针 | 指针数组 int* array[5] 本质是数组 )
      • ( 2 ) 代码示例 ( 指针数组使用案例 )
    • 3. main 函数参数 分析
      • ( 1 ) main 函数简介
      • (2) main 函数 代码示例
  • 六. 多维数组 和 多维指针
    • 1. 二维指针 ( 指向指针的指针 )
      • ( 1 ) 二维指针简介 ( 指向指针的指针 )
      • ( 2 ) 代码示例 ( 指针的传址调用 | 指向指针的指针 | 重置指针指向的空间 )
    • 2. 二维数组
      • ( 1 ) 二维数组 ( 存放方式 | 数组名 | 首元素类型 | 数组名 类似 常量指针 | )
      • (2) 代码示例 ( 以一维数组方式遍历二维数组 | 体现二维数组的数据排列 )
    • 3. 数组名
      • ( 1 ) 数组名 简介 ( 数组首元素地址 | &数组名 是 数组地址 )
      • ( 2 ) 代码示例 ( 数组名指针指向的内容 | 二维指针数组名对应的指针运算 )
      • ( 3 ) 代码示例 ( 一维数组遍历 | 二维数组遍历 )
      • ( 4 ) 代码示例 ( 为二维数组申请内存空间 )
  • 五. 数组参数 与 指针参数
    • 1. 数组参数退化为指针参数的意义
      • ( 1 ) 数组参数退化的相关概念 ( 指针退化成数组 )
      • ( 2 ) 代码示例 ( 二维数组参数 的指针退化 | 外层指针退化 | 内层数组指针没有退化 )
  • 六. 函数指针
    • 1. 函数类型 和 函数指针
      • (1) 相关概念 ( 函数类型要素 ① 返回值, ② 参数类型, ③ 参数个数, ④ 隐含要素 : 参数顺序 | 函数指针类型 返回值类型 (*变量名) (参数列表) )
      • ( 2 ) 代码示例 ( 定义函数指针 : ①typedef int(FUN)(int); FUN* p; 或者 ② void(*p1)(); | 给 函数指针 赋值 , 右值 可以直接使用 ① 函数名 或 ② &函数名 | 调用函数指针方法 : ① 函数指针变量名(参数) ② (*函数指针变量名)(参数) | 函数名 和 &函数名 是等价的 | 函数指针变量名(参数) 和 (*函数指针变量名)(参数) 也是等价的 )
    • 2. 回调函数
      • ( 1 ) 回调函数相关概念
      • ( 2 ) 代码示例 ( 回调函数示例 )
    • 3. 解读 复杂的 指针声明 ( 难点 重点 | ①找出中心标识符 ②先右 后左 看 确定类型 提取 ③ 继续分析 左右看 ... )

注意 : 博客中出现的关于指针的计算方式, 如果在 32 位电脑中, 指针的地址类型是 unsigned int 类型 , 占 4 字节 , 在 64 位电脑中 指针地址的类型是 unsigned long int , 占 8 个字节 ;

一. 指针

1. 指针 简介

( 1 ) 指针 概念 ( 本质 | 占用内存 ① 32位 4字节 ② 64 位 8 字节 | * ① 声明指针 ② 获取指向的值 )

指针简介 :

  • 1.指针本质 : 指针本质也是一个变量 ;
  • 2.占用内存 : 指针变量也要在内存中占用一定大小的空间, 不同 类型的指针占用的内存大小都是 相同的 ;

32位系统 指针 占用内存大小 4 字节, 64位系统 指针 占用内存大小 8 字节;

  • 3.指针变量保存的值 : 指针变量中保存的是内存地址的值 ;

符号简介 :

  • 1.声明指针 : 在 声明指针变量时, * 表示声明一个指定类型变量的指针 ;
  • 2.使用指针 : 使用指针的时候, * 表示指针变量地址指向的内存中的值, 可以读取该地址的实际数据值 或者 向地址中写入实际数据值 ;

( 2 ) 指针 简单示例 ( * 的读写内存作用 | 指针相关类型大小)

指针简单示例 :

  • 1.代码示例 :
#include <stdio.h>int main()
{//1. 指针简单使用, * 符号作用int i = 666;//声明 int 类型 指针, 使用 * 符号声明指针int *p = &i;//这里验证下 i 即存放在 p 地址的内容, * 用于读取 指针 p 地址中的数据printf("%d, %x, %d\n", i, p, *p);//等价于 i = 888, *p 代表指针指向的内容, p 是指针的地址, 之类 * 用于向 p 地址指向的内存中写入数据*p = 888;//改变一个变量的大小可以使用其地址来改变, 不一定必须使用变量名称printf("%d, %x, %d\n", i, p, *p);//2. 指针大小示例//32位系统 指针 占用内存大小 4 字节, 64位系统 指针 占用内存大小 8 字节int* p_int;char* p_char;//a. 打印 int* 类型指针 和 char* 类型指针的 指针变量本身大小//b. 打印 指针指向的内容大小, int 指针指向 int 类型, 因此 sizeof(*p_int) 结果是 4, sizeof(*p_char) 结果是 1printf("%ld, %ld, %ld, %ld\n", sizeof(p_int), sizeof(p_char), sizeof(*p_int), sizeof(*p_char));//打印 int* 和 char* 类型大小, 打印 int 和 char 类型大小 printf("%ld, %ld, %ld, %ld\n", sizeof(int*), sizeof(char*), sizeof(int), sizeof(char));return 0;
}
  • 2.运行结果 :

2. 传值 和 传址 调用

( 1 ) 相关概念 ( 传值调用 复制实际值 | 传址调用 复制地址值 )

传值调用 :

  • 1.产生复制情况 : 传值调用时 会发生 实参数据值 复制到 形参中 ;

传址调用 :

  • 1.实现方式 : 将指针当做函数的参数, 因为指针也是变量, 可以当做参数使用 ;
  • 2.适用场景 : 如果需要在函数中修改实参的值, 并且执行函数完毕后保留下来, 这里就用到传址调用, 使用指针作为函数参数 ;
  • 3.适用场景2 : 参数数据类型较复杂, 如果参数很大, 传值调用需要实参到形参的复制, 会浪费性能 ;

( 2 ) 传址调用 ( 改变外部变量值 )

代码示例1 :

  • 1.代码 :
#include <stdio.h>//传值调用案例, 任意改变参数的值, 不影响传入的变量值
int fun_1(int a, int b)
{a = 444;b = 444;
}//传址调用案例, 如果在函数中修改了地址指向的内存的值, 那么最终的值改变了
int fun_2(int* a, int* b)
{*a = 444;*b = 444;
}int main()
{int x = 666, y = 888;//传值调用fun_1(x, y);printf("x = %d, y = %d\n", x, y);//传址调用fun_2(&x, &y);printf("x = %d, y = %d\n", x, y);return 0;
}
  • 2.执行结果 :

代码示例2 :

  • 1.代码 :
#include <stdio.h>//传址调用, 替换传入的变量值
int swap(int *a, int *b)
{int tmp = *a;*a = *b;*b = tmp;
}int main()
{int x = 666, y = 888;printf ("x = %d, y = %d\n", x , y);swap(&x, &y);printf ("x = %d, y = %d\n", x , y);return 0;
}
  • 2.执行结果 :

3. 常量 和 指针

( 1 ) 相关概念 ( 核心原则 左数右指 | 左数 ① const int* p ② int const* p 数据时常量 | 右指 int* const 指针是常量 )

参考 : const 关键字 ;

const 修饰指针 : 需要符合下面的规则 :

声明 特征
const int* p p指针地址可变 p指针指向的内容不可变 (const 在 * 左边, 数据不可变)
int const* p p指针地址可变 p指针指向的内容不可变 (const 在 * 左边, 数据不可变)
int* const p p指针地址不可变 p指针指向的内容不可变 (const 在 * 右边, 地址不可变)
const int* const p p指针地址不可变 p指针指向的内容不可变 (const 在 * 左边 和 右边, 数据和地址都不可变)

const 修饰指针规则 : ***左数 右指 (左边数据是常量, 右边指针是常量)***;
左数 : const 出现在 * 左边时, 指针指向的数据为常量, 指向的数据不可改变;
右指 : const 出现在 * 右边时, 指针地址本身是常量, 指针地址不可改变;


( 2 ) 验证 常量 指针 相关概念 ( 左数右指 )

参考 : const 关键字 ;

const 修饰指针规则 : 左数右指;
左数 : const 出现在 * 左边时, 指针指向的数据为常量, 指向的数据不可改变;
右指 : const 出现在 * 右边时, 指针地址本身是常量, 指针地址不可改变;

const 关键字 代码示例 : 修饰指针

  • 1.代码示例1 : const 出现在 * 左边, const int* p = &i;
#include <stdio.h>int main()
{//定义普通的变量, 用于取地址用int i = 666;//定义一个 const 在 * 左边的例子, 意义是 指针指向的内容是常量//按照规则, 指针地址可改变, 指针指向的数据不可变const int* p = &i; //指针指向的数据不可改变, 这里会报错*p = 444;return 0;
}

  • 2.代码示例2 : const 出现在 * 左边, int const* p = &i;
#include <stdio.h>int main()
{//定义普通的变量, 用于取地址用int i = 666;//定义一个 const 在 * 左边的例子, 意义是 指针指向的内容是常量//按照规则, 指针地址可改变, 指针指向的数据不可变int const* p = &i;//指针指向的数据不可改变, 这里会报错*p = 444;return 0;
}

  • 3.代码示例3 : const 出现在 * 右边, int* const p = &i;
#include <stdio.h>int main()
{//定义普通的变量, 用于取地址用int i = 666;//定义一个 const 在 * 右边的例子, 意思是 地址是常量//按照规则, 指针地址不可改变, 指针指向的内容可变int* const p = &i;//指针指向的数据不可改变, 这里会报错p = NULL;return 0;
}

  • 4.代码示例4 : const 同时出现在 * 左边 和 右边, const int* const p = &i;
#include <stdio.h>int main()
{//定义普通的变量, 用于取地址用int i = 666;//定义 const 同时出现在 * 左边 和 右边, 则指针的地址 和 指向的数据都不可改变const int* const p = &i;//下面的两个操作, 一个是想修改指针地址, 一个是想修改指针值, 这两个都报错.p = NULL;*p = 444;return 0;
}


二. 数组

1. 数组 简介

( 1 ) 数组 概念 ( 数组地址 | 数组大小 显示 隐式 声明 | 数组初始化 [ 效率比后期赋值高 ] )

数组 简介 :

  • 1.概念 : 数组 是 相同类型 的 变量 的 有序集合 ;
  • 2.数组示例 :
 int array[6];

定义数组 int array[6];
意义 : 数组中包含 6 个 int 类型的数据 , 数组中每个元素都是 int 类型的 ;
第一个元素地址 : array 是数组中第一个元素的起始地址;
下标 : 可以通过下标来获取数组中指定位置的元素, array[0] 是第一个元素的位置, array[5] 是第六个元素的位置 ;

数组大小 :

  • 1.数组定义时必须声明大小 : 数组在定义时, 必须显示 或 隐式 的声明数组的大小 ;
  • 2.显示声明数组大小 : 定义数组时, 在数组名称后的中括号中声明数组大小 ;
int array[5];
int array[5] = {1, 2, 3} ; //这个也是显示声明, 数组大小为 5, 但是只指定了 前三个元素的大小 ;
  • 3.隐式声明数组大小 : 声明数组时, 不在中括号中声明数组大小, 只在初始化中初始化指定个数的元素, 那么元素的个数就是数组的大小 ;
//隐式初始化, 该数组个数为 4
int array[] = {0, 1, 2, 3};

数组初始化 :

  • 1.完全初始化 : 数组大小为5, 将 5 个元素都在定义时指定位置 ;
  • 2.部分初始化 : 数组大小为5, 如果初始化前 1 ~ 4 个元素, 剩余的元素默认初始化为 0 ;
  • 3.初始化效率 : 初始化效率很高, 远远比依次赋值要高, 因此建议定义数组时最好初始化 ;
  • 4.最佳实践 :
//这里只对数组的第一个元素进行初始化为0, 那么其余的元素默认也初始化为0, 初始化效率要远远高于依次赋值的效率
int array[5] = {0}

( 2 ) 数组 示例 ( 定义 | 大小 | 初始化 )

数组 大小 初始化 示例 :

  • 1.代码 :
#include <stdio.h>//数组大小 和 初始化 示例
//数组大小 :
//初始化 : 如果不初始化, 那么数组中就是随机值; 全部初始化, 部分初始化 : 其余默认为 0int main()
{//1. 显示声明数组大小, 其实际大小以中括号为准, 大小为 5, 5个元素只有 前3个初始化为 0, 1, 2//初始化说明 : int array_1[5] = {0, 1, 2};int array_3[5];int array_4[5] = {0};//2. 隐式声明数组大小, 其实际大小为 3, 三个元素全部初始化int array_2[] = {0, 1, 2};printf("array_1 大小 : %ld, array_1 数组个数 : %ld\n", sizeof(array_1), sizeof(array_1)/sizeof(*array_1));printf("array_2 大小 : %ld, array_2 数组个数 : %ld\n", sizeof(array_2), sizeof(array_2)/sizeof(*array_2));//打印 array_2 数组结果, 其中数组元素内容是 初始化值printf("打印 int array_1[5] = {0, 1, 2}; 数组结果 : \n");int i = 0;for(i = 0; i < sizeof(array_1)/sizeof(*array_1); i ++){printf("array_1[%d] = %d\n", i, array_1[i]);}//打印 array_3 数组结果, 其中数组元素内容是随机值printf("打印 int array_3[5]; 数组结果 : \n");for(i = 0; i < sizeof(array_3)/sizeof(*array_3); i ++){printf("array_3[%d] = %d\n", i, array_3[i]);}//打印 array_4 数组结果, 其中数组元素内容是随机值printf("打印 int array_4[5] = {0}; 数组结果 : \n");for(i = 0; i < sizeof(array_4)/sizeof(*array_4); i ++){printf("array_4[%d] = %d\n", i, array_4[i]);}return 0;
}
  • 2.执行结果 :

2. 数组地址与名称 概念

( 1 ) 数组 概地址 ( 数组名 [ 数组首元素地址 ] 和 &数组名 [ 数组地址 ] | 数组名 类似于 常量指针 | 数组拷贝 )

数组地址名称 简介 :

  • 1.数组名称 : 数组名称 等价于 数组 首元素 地址 ;

    • 注意 : 数组名 不是 数组的首地址 , &数组名 才是数组的首地址 , 但是这两个的值是相同的 ;
  • 2.数组地址 : 使用 & 取数组的地址, 才能获取数组的地址 ;
  • 3.值相同 : 数组的 首元素地址 与 数组地址是相同的 ;
  • 4.数组地址 与 数组首元素地址 : 这两个地址不是等价的, 其意义完全不同 ;

数组名称 :

  • 1.数组名称的本质 : 数组名 类似于 常量指针, 数组名称 不能作为左值, 不能被赋值 ; 数组名 只能作为右值, 被赋值给别的指针 , 数组名在***大多数情况下可以当做常量指针理解***, 但是 数组名绝对不是真正的常量指针 ;
  • 2.数组名代表的地址 : 数组名称 指向 数组首元素的地址, 其绝对值 与 数组地址 相同;

数组名称不作为常量指针的场合 : 数组名类似于常量, 但不是常量, 下面两种场合数组名与常量指针不同 ;

  • 1.sizeof 取大小时 : 使用 sizeof 操作符获取 array 数组大小时, sizeof 作用域常量指针获取的是指针的大小, sizeof 作用于数组名, 获取的是数组的大小 , 不是单个指针的大小;
  • 2.作为 & 参数时 : & 只能用于变量, 不能用于常量, 因此 &数组名 是取数组的地址, 这种用法 不符合常量指针的特点 ;

数组拷贝禁用数组名直接赋值 :

  • 1.禁止使用的方式 : 数组拷贝不能 直接使用 数组名1 = 数组名2 的方式进行拷贝 或者 赋值 ;
  • 2.常量指针 : 数组名 类似于 常量指针, 其***不能作为赋值的左值, 只能做右值使用*** ;
  • 3.数组大小 : 数组还有一个隐含的大小属性, 如 sizeof(数组名) 就可以获取整个数组的大小, 单纯的数组名称只是一个地址, 如果使用地址进行互相赋值, 数组的大小属性无法体现, 因此 C 语言规范, 禁用数组名 作为左值 ;

( 2 ) 数组 示例 ( 数组名 | 地址 | 数组拷贝禁止情况 )

数组代码示例 :

  • 1.代码示例 :
#include <stdio.h>int main()
{int array_1[8] = {0};int array_2[] = {0, 1, 2, 3};//array_1 的类型是 int *//&array_1 的类型是 int*[8], 根据编译时的  warning 警告可以看到这两个类型printf("array_1 : %x, &array_1 : %x \n", array_1, &array_1 );//这种用法是错误的, array_1 类似于一个常量指针, 其不能当做左值//数组除了地址信息之外, 还附带大小信息, 如果只是地址赋值, 大小信息无法带过去, 因此数组不能这样拷贝赋值//C语言 不支持 这样的赋值//array_1 = array_2; return 0;
}
  • 2.执行结果 :

3. 数组 与 指针 区别

( 1 ) 概念简介 ( ① 数组名就是首元素地址 不需要寻址 | ② 指针 中保存一个地址 指向首元素地址 需要寻址 | printf 打印 数组 或 指针 : 根据占位符自动判断打印地址还是打印内存中的具体内容 )

printf 打印 数组 与 指针 变量 :

  • 1.处理数组 : 编译器不会寻址 , 直接将 数组名代表的内存空间地址对应的数据打印出来 , 因为数组名就代表了数组的首地址, 不需要再次寻址 ; 数组名代表的地址 就是 内容的首地址 , 不用去寻址查找内容 ;
  • 2.处理指针 : 编译器会寻址 , 查找 指针变量的四个字节的内容, 指针变量的四个字节的地址指向的内容 , 然后将指针指向的内容打印出来 , 指针的地址 与 实际内容的地址 不连续, 是断开的 ;

下面这张图形象的说明了 指针 与 数组的 区别 :

指针的起始地址 和 数组的起始地址 :

  • 1.指针起始地址 : 这里要区分 指针保存的地址 和 指针起始地址,
    ( 1 )指针保存的地址 : 是指 指针变量 4 字节 (32位系统的, 64位 8 个字节) , 这四个或 8个字节中保存了一个地址 , 这个地址指向另外一段内存空间, 这个地址是指针保存的地址, 又叫指针指向的地址, 在下图中标注的 指针变量中保存(指向)的地址① , 这个地址还是 实际内容的起始地址① ;
    ( 2 )指针起始地址 : 是指指针变量所在的地址, 是 ***指针变量的四个字节的第一个字节所在内存的首地址 ***, 在下图中标注的 指针起始地址② ;
  • 2.数组起始地址 : 数组名就是数组的起始地址, 又是数组首元素地址 , int array[10], array 是一个地址, 在下图中标注的 数组首地址③, 这个地址还是数组 数组实际内容的首地址③ ;
  • 3.图示 :

printf 打印 数组 或 指针 的 内容 或 地址 : 针对 字符数组 和 字符指针, 根据占位符自动判断打印地址还是打印内存中的具体内容 ;

  • 1.打印字符串 : 如果想要打印出 数组或指针的 字符串, 那么使用 %s 作为占位符 ;
  • 2.打印地址 : 如果想要打印出 数组或指针的地址 , 那么使用 %x 作为占位符 ;
  • 3.智能判断 : printf 时, 方法中会自动判断 占位符 的类型, 来判断是否要寻址, 如果 %x 则只打印地址, 如果使用 %s, 则会自动根据对应的地址打印出其内容 ;
  • 4.代码示例 :
#include <stdio.h>int main()
{char array[10] = {'H', 'e', 'l', 'l', 'o'};char *str = "Hello";//1. 针对数组打印 //   ( 1 ) 如果检测到 占位符 为 %s, 则会将组名首地址内存中的数据, 并一直到 \0 都打印出来(注意 不寻址)//   ( 2 ) 如果检测到 占位符 为 %x, 则会自动将数组首地址打印出来printf("array : %s\n", array);printf("array : %x\n", array);//2. 针对指针打印 //   ( 1 ) 如果检测到 占位符 为 %s, 则会寻址查找指针地址指向的内存, 将该内存中的字符串打印出来//   ( 2 ) 如果检测到 占位符 为 %x, 则会将指针地址打印出来printf("str : %s\n", str);printf("str : %x\n", str);return 0;
}
  • 5.执行结果 :

( 2 ) 代码示例 ( 数组 | 指针 编译器处理上的区别 )

代码示例 :

  • 1.代码1 : 文件 test_1.c 内容 ;
#include <stdio.h>//编译器如何处理 数组 和 指针
//1. 外部文件定义 : 在另外一个文件定义 char 指针 : char *p = "Hello";
//      ( 1 ) 编译器操作 : 在符号表中放置符号 p, 然后为符号 p 分配空间,
//                         查询到 p 是指针, 给 p 符号分配 4 字节, 4 字节存放地址, 指向 "Hello" 字符串 地址;
//                         当打印 p 指针时, 编译器会按照指针地址寻址, 将指针指向的内存中取值并打印出来 ;
//2. 本文件声明 : 在本文件声明 : extern char p[] ;
//      ( 2 ) 编译器操作 : 声明引用外部文件变量 p, 到符号表中查找 p, 但是编译器认为 p 是数组,
//                         p 是数组名, 代表数组地址;
//                         当打印 p 数组时, 编译器会直接将 p 当做数组地址 打印出来;//printf 打印变量规则 :
//( 1 ) 打印指针 : 编译器会寻址, 查找指针指向的内容, 然后将指针指向的内容打印出来 ;
//( 2 ) 打印数组 : 编译器不会寻址, 直接将数组名代表的内存空间地址打印出来 ;//代码遵循原则 : 声明指针 数组, 在外部声明时类型要一致 ; extern char p[];int main()
{//1. 此时 p 是数组, 直接打印 p, 会将数组地址打印出来, 以 %s 打印一个数组地址 会出乱码 printf("*p = %s\n", p);//2. 正确使用数组 p 打印字符串的方法(模仿编译器行为手工寻址) : p 是指针, 指向 "Hello", 但是本文件中声明为类数组, 数组与指针打印时编译器会做不同处理;// ( 1 ) 首先 p 是地址 ( 数组首地址 ), //         ① 将 p 转为 unsigned int* 类型的指针 : (unsigned int*)p ;//         ② 说明 : 此处是将一个变量强制转为 指向 unsigned int 类型的 指针, 这个是一个二维指针, 是指向地址的指针//                    为了获取 p 的地址(其地址是 unsigned int 类型的), 使用 * 即可提取 p 地址 ; // ( 2 ) 获取字符串地址 : 获取 (unsigned int*)p 指向的地址, 即 字符串的地址, 使用 *((unsigned int*)p) 获取字符串地址;// ( 3 ) 将字符串地址强转为char*指针 : (char*) (*((unsigned int*)p)) 即指向字符串的指针, 打印这个指针会将字符串打印出来printf("*p = %s\n", (char*) (*((unsigned int*)p)));return 0;
}
  • 2.代码2 : 文件 test_2.c 中的内容 ;
char *p = "Hello";
  • 3.执行结果 : 执行 gcc test_1.c test_2.c 命令进行编译 , 执行 编译后的可执行文件 ./a.out ;

三. 数组 指针 分析

1. 指针 加减 运算方式

( 1 ) 指针 加减法 运算 ( 指针指向的位置在同一个数组中改变才有意义 )

指针运算规则 :

  • 1.指针是变量 : 只要是变量就可以进行运算, 可以加减运算, 指针 + 1 运算如下 ;
  • 2.指针 + num 运算规则 : p + num(整数) , 反应到地址运算上 即 等价于 *(unsigned int)p + num * sizeof(p) , 其地址不是加1个字节, p 的地址的增量 是 所指向的数据类型的大小 乘以 被加数 的地址;

指针指向数组元素规则 :
前提 : 指针指向一个数组元素时有以下规则 :
: 指针 + 1 指向数组的下一个元素 ;
: 指针 - 1 指向数组的上一个元素 ;

数组运算规则 :

  • 1.数组本质 : 数组的***元素存储空间是连续的***, 从数组首地址(数组元素首地址 | 数组名)开始 ;
  • 2.数组空间大小 : 数组的空间通过 sizeof(数组元素类型) * 数组大小 计算的, 这个数组元素类型是数组声明的时候指定的, 数组大小是数组声明或者初始化时指定的 ;
  • 3.数组名 : 数组名 是 数组首元素的地址, 又是***数组地址***, 即***数组所在内存空间的首地址*** ;
  • 4.数组名 看做 常量指针 : 数组名可以看做指向数组首元素的常量指针, 当数组名 + 1 时, 可以看做指针 进行了 加 1 运算, 其地址按照指针运算规则, 增加了 数组元素大小 * 1 ;

指针减法运算 :

  • 1.指针之间的运算 : 两个指针之间 只能进行 减法运算, 加法乘法除法不行, 并且 进行减法运算的两个指针的类型必须相同 ;
  • 2.指针减法运算的前提 : 进行减法运算的***两个指针类型必须是同一个类型*** ;
  • 3.指针减法运算的意义 : 指针减法运算时 两个指针指向同一个数组才有实际的意义, 计算结果是 同一个数组 两个指针指向位置的下标差 ;
  • 4.同类型无意义减法 : 如果两个指针指向相同类型的不同数组, 即使减法有结果, 这个结果也是没有任何意义的;

指针减法的过程 : 指针1 - 指针2 = ( 指针1指向的地址 - 指针2指向的地址 ) / sizeof (指针1和指针2的相同类型)


(2) 数组大小计算示例

数组大小计算代码示例 :

  • 1.代码示例 :
#include <stdio.h>int main()
{int array[10] = {0};//打印出数组整体占用的内存数, 以及数组元素个数 printf("sizeof(array) = %ld, size = %ld \n", sizeof(array), sizeof(array)/sizeof(*array));return 0;
}
  • 2.编译执行结果 :

( 3 ) 指针 加法运算示例 ( 指针地址 + 4/8 * 被加数 )

数组名 指针 加法示例 :

  • 1.代码示例 :
#include <stdio.h>int main()
{int array[10] = {0};//打印出数组首元素地址, 打印出数组名 + 1 的值printf("array 地址 : %x,  array + 1 地址 : %x\n", array, array + 1);return 0;
}
  • 2.编译运行结果 : 示例中的 array + 1 比 array 的地址大 4 个字节 ;

指针运算 : int * p, p + 1 代表的地址是 p 的地址 加上 4, 即加上了 一个 int 类型大小的地址;


( 4 ) 指针 减法 运算示例

指针减法代码示例 :

  • 1.代码示例 :
#include <stdio.h>int main()
{int array_1[] = {0, 1, 2, 3, 4, 5};int array_2[] = {6, 7, 8, 9};//1. 定义指针 p1_0 指向 array_1 数组中的第 0 个元素int* p1_0 = array_1;//2. 定义指针 p1_5 指向 array_1 数组中的第 5 个元素int* p1_5 = &array_1[5];//3. 定义指针 p2_0 指向 array_2 数组中的第 0 个元素int* p2_0 = array_2;char c = 'c';//4. 定义了一个 char 类型指针char *p_c = &c;//1. 计算 p1_5 指针指向的元素 与 p1_0 指向的元素, 两个元素在数组中的下标差printf("%d\n", p1_5 - p1_0);//2. ( p1_5 - p1_0 ) 与 ( ( unsigned int ) p1_5 - ( unsigned int ) p1_0 ) / sizeof ( int ) ) 是等价的 ;printf("%d\n", ( ( unsigned int ) p1_5 - ( unsigned int ) p1_0 ) / sizeof ( int ) );//3. 指针之间不支持加法, 这个操作在编译时就会报错, 这里注释掉 ; //printf("%d\n", p1_5 + p1_0);//4. 两个指针间的计算倒是没毛病, 但是两个指针分别指向两个数组, 这个计算的结果没有实际意义 ; printf("%d\n", p1_5 - p2_0);//5. 指针间进行运算的前提是 : 两个指针的类型必须相同, 这两个指针类型不同, 一个 int* 一个 char* , 编译时报错 ; //printf("%d\n", p1_5 - p_c);//6. 指针之间 不能进行乘法 和 除法, 编译时会报错//printf("%d\n", p1_5 * p1_0);//7. 指针之间 不能进行乘法 和 除法, 编译时会报错//printf("%d\n", p1_5 / p1_0);return 0;
}
  • 2.编译运行结果 :

2. 指针 比较 运算方式

( 1 ) 指针 比较 运算 ( 大于 小于 大于等于 小于等于 运算的前提是 必须指向同一数组 中的元素 | 任意两指针只能进行 等于 不等于 的比较 )

指针的比较运算 :

  • 1.同一数组的比较运算 : 对于 大于 ( > ) , 小于 ( < ) , 大于等于 ( >= ) , 小于等于 ( <= ) 四种类型运算, 指针之间进行这四种运算的前提示 两个指针 必须都指向同一个数组的元素 ;
  • 2.任意指针的比较运算 : 对于 等于 ( == ) , 不等于 ( != ) 两种比较运算, 指针之间进行这两种比较运算, 可以是任意指针, 指针指向不同数组也可进行这两种运算 ;

( 2 ) 指针 比较 运算代码示例 ( 用 指针 遍历数组 )

使用指针遍历数组代码示例 :

  • 1.代码示例 :
#include <stdio.h>int main()
{char array_str[] = {'H', 'e', 'l', 'l', 'o'};//1. 定义数组第一个元素的起始指针char* p_start = array_str;//2. 定义数组最后一个元素 之后的指针, 这个指针只是做比较用的, 不会真正的寻址char* p_end = array_str + (sizeof(array_str) / sizeof(*array_str));//3. 定义循环控制变量char* p = NULL;//4. 遍历数组for(p = p_start; p < p_end; p ++){printf("%c", *p);}//5.此处换行printf("\n");return 0;
}
  • 2.编译执行结果 :

3. 数组访问方式

( 1 ) 下标 指针 访问 ( 推荐使用下标访问 )

下标访问数组 和 指针访问数组 的示例 : 这两种访问数组的方式是等价的 ;

  • 1.下标访问数组 :
int array[5] = {0};
array[1] = 1;
array[2] = 2;
  • 2.指针访问数组 :
int array[5] = {0};
*(array + 1) = 1;
*(array + 2) = 2;

下标访问 和 指针访问 对比 :

  • 1.可读性 : 使用下标访问数组, 数组的可读性会大大的提高, 指针访问数组不易理解 , 下标访问在可读性上优于指针访问数组 ;
  • 2.性能 : 当使用一个固定的增量访问数组时, 指针访问 的性能 优于 下标访问;

推荐使用方式 : 现在的编译器编译出来的代码, 性能上 指针访问 与 下标访问基本相同, 出于代码可读性考虑, 推荐使用下标访问数组的方式 ;

下标 指针访问数组性能分析 : 以 数组 中的元素互相赋值为例 ;

  • 1.下标访问 : 如访问 array[3] ( 数组第 4 个元素 ) , 其首地址地址是 array 首地址 加上 3 个元素地址 ( 第三个元素的尾地址就是第四个元素的首地址 ) , 其预算方式是这样的 : ( unsigned int ) array + sizeof(int) * 3 ;
  • 2.指针访问 : 如访问 array[3] , 以指针的形式, 如果每次递增 1 个下标, 那么运算方式是 ( unsigned int ) array + 4 即可, 这里每次只做加法, 下标访问每次都要用乘法, 乘法运算要比加法运算费时 ;

( 2 ) 下标 指针 访问 数组 性能 代码示例


3. int array[]; array 和 &array 区别

( 1 ) int array[] 中 array 和 &array 意义 ( ① array 数组首元素地址 | ② &array 数组地址 )

数组 int array[] 中 array 和 &array 意义 :

  • 1.数组元素首地址 : array 是数组首元素地址, sizeof ( *array ) 计算的是数组中单个元素的大小 ;
  • 2.数组地址 : & 是数组的首地址, 代表的是整个数组的地址 ;

两种指针的运算 :

  • 1.array + 1 运算 : array + 1 的运算过程是 ( unsigned int ) array + sizeof (array) , 该运算相当于计算***数组中第二个元素的首地址** , 等价于 array[1] ;
  • 2.&array + 1 运算 : &array + 1 的运算过程是 ( unsigned int ) ( &array ) + sizeof(&array)***, 其中 &array 结果是 array, sizeof ( &array) 的结果 等价于 sizeof ( array ), 这是整个数组的大小, 因此 &array + 1 的*结果是数组的尾地址* ;

( 2 ) array 和 &array 计算 代码示例

代码示例 :

  • 1.代码 :
#include <stdio.h>//注意 : 在 64 位电脑上, 计算指针时需要将指针地址墙砖为 unsigned long int 类型int main()
{int array[5] = {0, 1, 2, 3, 4};//1. 计算 p1 指针指向 : //       ( 1 ) &array 相当于 数组的地址, & array + 1 等价于 (unsigned long int) &array + sizeof ( *&array )//             等价于 (unsigned long int) &array + sizeof ( array ) , sizeof ( array ) 计算的是整个数组的长度//         ( 2 ) 经过上述计算, 该指针指向 数组的尾地址, 即最后一个元素的结尾处int *p1 = ( int* )( &array + 1 );//2. 计算 p2 指针指向 : //      ( 1 ) 单纯地址 : (unsigned long int)array 这个操作将一个有类型的指针 直接强转为单纯的地址;//       ( 2 ) 单纯地址增加 : (unsigned long int)array + 1 就是之前的地址单纯的加 1, 不再含有指针运算中 增加 数组元素 字节大小倍数 的意义;//     ( 3 ) 拆分int类型字节 : 这样如果计算 p2 指针指向的数据, 会将 4 个字节拆散计算, 可能从 元素1 中1取 3 个字节, 元素2 中取 1个字节//         ( 4 ) 大小端方式选择 : 计算方式注意大端模式 和 小端模式, 一版的电脑都是小端模式, //      ( 5 ) 小端模式计算方式 : 这里我们按照小端模式计算, 小端模式即 高地址存放高字节, 低地址存放低字节int *p2 = ( int* )( (unsigned long int)array + 1 );//3. 计算 p3 指针指向//       ( 1 ) array + 1 等价于 ( unsigned long int ) array + sizeof ( *array ), 该地址等价于 array[1] 地址//     ( 2 ) 经过上述计算, p3 指针指向了 第二个元素的首地址int *p3 = ( int* )( array + 1 );//1. p1[-1] : p1 指向数组的尾地址, 其 -1 下标 即指向了数组最后一个元素, 等价于 array[4];//2. p2[0] : p2 指向了数组的第一个元素的 第二个字节地址, //        那么 p2[0] 的值是 数组第一个元素的 2 , 3, 4 三个字节, 再加上 第二个元素的 第一个字节;//        小端地址策略 : 高位地址存高位, 低位地址存低位, 那么 第二元素第一字节是 高位, 其次是第一数组元素的 4, 3, 2 字节//3. p3[1] : p3 指向了数组的第二个元素首地址, p3[1] 等价于 array[3]printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);return 0;
}
  • 2.编译运算结果 :
  • 3.图示 :
  • 4.p2 指针计算过程 : 由上图可以看到 指针指向的位置开始取一个 int 类型, 地址由低到高 四个字节存储的数据为 0 0 0 1, 由于是小端模式, 高位地址存放在高位 其大小为 0x01 00 00 00 , 转为十进制是 16777216 ;

4. 数组参数

( 1 ) 数组参数 概念 ( 退化成指针 | 需要带上数组长度作为 附属参数 )

数组参数相关概念 :

  • 1.数组作为参数时编译器行为 : 数组作为参数时, 编译器会将数组 退化成 指针, 此时这个指针就没有数组的长度信息了 ;

    示例 1 : ***void method(int array[]) 等价于 method(int p)**, 此时 *p 中是不包含数组的长度信息的 ;
    示例 2 : *void method(int array[100]) 等价于 method(int p), 此时 *p 中是不包含数组的长度信息的 ;

  • 2.数组作为参数时的最佳用法 : 数组作为参数时, 应该定义另一个 int 类型的参数, 作为数组的长度信息 ;


( 2 ) 数组参数 代码示例 ( 数组大小 | 数组参数大小 )

代码示例 :

  • 1.代码 :
#include <stdio.h>/*编译器在编译时, 就将参数改为了 int* array 了C 语言中不会有数组参数的, 即使有, 也在编译时被替换成指针了
*/
void function(int array[100])
{printf("方法中数组参数 array 大小 : %ld\n", sizeof(array));
}int main()
{int array[100] = {0};printf("main 方法中 array 数组大小 : array : %ld \n", sizeof(array));function(array);return 0;
}
  • 2.编译执行结果 :

5. 数组 指针 对比 ( 内存分配 : ① 指针 分配 4 / 8 字节 ② 数组分配所有元素地址 | 作为参数 | 常量[ 数组 ] 变量[ 指针 ] 区别 )

内存空间分配区别 :

  • 1.指针 ( 分配 4 或 8 字节 ) : 声明指针的时候 只分配了 容纳指针的 4字节 (32位系统) 或 8 字节 (64 位系统) ;
  • 2.数组 ( 分配连续内存 ) : 声明数组时 分配了一篇容纳数组所有元素的一片连续内存空间 ;

参数上的区别 ( 等价 ) : 作为参数时, 数组 和 指针 参数时等价的, 数组会退化为指针, 丢失长度信息 ;

指针 数组 的 性质 :

  • 1.数组 ( 常量 ) : 数组大部分情况下可以当做常量指针, 不能作为左值使用, 不能被赋值 ; (sizeof 和 & 作用域数组名时除外) ;
  • 2.指针 ( 变量 ) : 指针是变量, 变量中保存的值 是 内存中的一个地址 ;

四. 字符串

1. 字符串概念

( 1 ) 概念 ( 本质 是 char[] 数组 | ‘\0’ 结尾 | 存储位置 栈 堆 常量区 )

字符串相关概念 :

  • 1.字符串本质 : C 语言中没有字符串这个数据类型, 使用 char[] 字符数组来模拟字符串 ;
  • 2.字符串要求 : 不是所有的字符数组都是字符串, 只有***以 ‘\0’ 结尾的字符数组***才是字符串 ;
  • 3.字符串存储位置 : 栈空间, 堆空间, 只读存储区 (常量区) ;

( 2 ) 示例代码 ( 字符串概念 | 字符串 )

代码示例 :

  • 1.代码 (正确的版本) :
#include <stdio.h>
#include <malloc.h>int main()
{//1. s1 字符数组不是以 '\0' 结尾, 不是字符串char s1[] = {'H', 'e', 'l', 'l', 'o'};//2. s2 是字符串, 其在 栈内存 中分配内存控件char s2[] = {'H', 'e', 'l', 'l', 'o', '\0'};//3. s3 定义的是字符串, 在 只读存储区 分配内存空间 //    s3 指向的内容无法修改, 如果想要修改其中的数据, 会在执行时报段错误char* s3 = "Hello";//这个操作在执行时会报段错误, 因为 s3 指针指向只读存储区//s3[0] = 'h';//4. s4 是以 '\0' 结尾, 是字符串, 在 堆空间 中分配内存char* s4 = (char*)malloc(2*sizeof(char));s4[0] = 'H';s4[1] = '\0';return 0;
}
  • 2.编译运行结果 ( 错误版本 报错提示 ) : 取消 s3[0] = ‘h’; 注释, 尝试修改 只读存储区的数据 , 运行时会报段错误 ;

2. 字符串 长度

( 1 ) 字符串长度计算 ( 不包括 ‘\0’ | 标准库中有该函数)

字符串长度 :

  • 1.概念 : 字符串包含的字符个数, 不包含 ‘\0’ , 只包括有效字符 ;
  • 2.计算字符串长度 : 根据从字符串开始到 ‘\0’ 结束, 计算不包括 ‘\0’ 的字符个数 ;
  • 3.数组不完全使用 : 如果数组长度100, 在第50个元素位置出现了 ‘\0’, 那么这个字符串长度是 49, 数组长度是 100 ;

针对 C 标准库已有的函数 :

  • 1.不要自己实现 C 标准库功能 : C 标准库是优化到极致, 个人修改的效果比库函数效果要差 ;
  • 2.复用库函数效率高 : 不要重复制造轮子 ;

( 2 ) 代码示例 ( 字符串长度计算示例 )

代码示例 :

  • 1.代码 :
#include <stdio.h>
#include <string.h>int main()
{//1. 字符串长度 : 以 '\0' 之前的个数为准, 不包括 '\0' , 字符串长度 5//   即使后面有有效的字符, 那么也不属于字符串 c //2. 数组长度 : 数组长度 666char c[666] = {'H', 'e', 'l', 'l', 'o', '\0', 'w', 'o', 'r', 'l', 'd'};printf("字符串长度 : %ld, 字符数组长度 : %ld\n", strlen(c), sizeof(c));return 0;
}
  • 2.编译运行结果 :

( 3 ) 代码示例 ( 自己实现 strlen 方法 )

实现 strlen 方法代码示例 ( 普通版本 ) :

  • 1.代码 :
#include <stdio.h>
#include <assert.h>size_t strlen(const char* s)
{size_t len = 0;//1. 如果 s 为 NULL, 直接中断程序assert(s);//2. 指针先自增, 在取指针内指向的数据值, 看看是否为'\0'//       如果指向的数据为 '\0', 那么循环中断执行下面的内容while(* s++){len ++;}return len;
}int main()
{char * s1 = "0123";char * s2 = NULL;//1. 测试 s1 的实际长度, 返回 4printf("s1 长度 : %u\n", strlen(s1));//2. 测试空字符串长度, 运行时会中断printf("s2 长度 : %u\n", strlen(s2));return 0;
}
  • 2.编译运行结果 :

实现 strlen 方法代码示例 ( 递归版本 ) :

  • 1.代码 :
#include <stdio.h>
#include <assert.h>size_t strlen(const char* s)
{//1. assert(s) 先验证是否为 NULL , 如果为 NULL 中断程序//2. 递归退出条件 : *s 为 '\0' 时, 递归退出//3. s + 1 即指向 字符串的 下一个 char 元素, 计算 下一个 char 到结尾的个数, //        当指向 '\0' 时, 之后的字符串个数为0, 然后依次退出递归return ( assert(s), ( *s ? (strlen(s + 1) + 1) : 0 ) );
}int main()
{char * s1 = "0123";char * s2 = NULL;//1. 测试 s1 的实际长度, 返回 4printf("s1 长度 : %u\n", strlen(s1));//2. 测试空字符串长度, 运行时会中断printf("s2 长度 : %u\n", strlen(s2));return 0;
}
  • 2.编译执行结果 :

3. 字符串函数 长度不受限制 情况

( 1 ) 不受限制的字符串函数 ( 函数自动寻找 ‘\0’ 确定字符串大小 | stpcpy | strcat | strcmp )

不受限制的字符串函数相关概念 :

  • 1.字符串常用方式 : 一般在函数中使用字符串时, 需要指明字符串的大小, 因为字符串数组 一旦当做函数参数时, 就退化成指针, 失去了大小信息 ;
  • 2.字符串相关的函数不需要大小信息 : 在 string.h 中的方法, 不需要传入大小信息, 函数中会自动寻找 ‘\0’ 来计算字符串的长度 ;
  • 3.参数不是字符串则出错 : 不受限制字符串函数如果传入的字符串没有 ‘\0’ , 则会出错 ;

不受限制的字符串函数示例 :

       char *stpcpy(char *dest, const char *src);//字符串拷贝char *strcat(char *dest, const char *src);//字符串拼接int strcmp(const char *s1, const char *s2);//字符串比较

不受限制字符串函数 的 相关注意事项 :

  • 1.字符串必须以 ‘\0’ 结尾 : 此类函数相关的字符串必须以 ‘\0’ 结尾, 因为字符串长度是根据找到的 ‘\0’ 来计算的, 如果没有 ‘\0’ 会报错 ;

  • 2.字符串长度改变相关 : strcpy ( 字符串拷贝 ) 和 strcat ( 字符串拼接 ) 必须保证 拷贝 或 拼接的 目标数组 有足够的空间来保存结果字符串 ;

  • 3.字符串比较函数 : strcmp 两个字符串比较, 如果返回 0 , 表示两个字符串相等 ;

    • 函数 : int strcmp(const char *s1, const char *s2);
    • ( 1 ) 返回值 等于 0 : 两个字符串相等 ;
    • ( 2 ) 返回值 大于 0 : 第一个字符串 大于 第二个字符串 ;
    • ( 3 ) 返回值 小于 0 : 第一个字符串 小于 第二个字符串 ;

    注意字符串要求 : strcmp 函数不会修改 s1 和 s2 字符串的值, 但是两个字符串必须符合要求 以 ‘\0’ 结尾 ;


( 2 ) 代码示例 ( 自己实现字符串拷贝函数 )

实现拷贝字符串函数 :

  • 1.代码 :
#include <stdio.h>
#include <assert.h>//函数作用, 将 src 字符串 拷贝到 dst 指针指向的内存中, 同时将拷贝完的结果 dst 返回
char* strcmp ( char* dst, const char* src )
{//1. 安全编程, 传入的两个值不能为 NULLassert(dst && src);//2. 将 src 指针指向的 字符 赋值给 dst 指针指向的值, 然后两个指针自增 1//       如果赋值的指针不等于 '\0' , 那么继续赋值, 如果赋值的值为 '\0' 就退出循环while( (* dst++ = * src++) != '\0' );//3. 其返回值也是 dst 参数, 参数也可以当作返回值使用return dst;
}int main()
{char dst[20];printf("%s\n", strcpy(dst, "字符串拷贝"));return 0;
}
  • 2.编译运行结果 :

4. 字符串函数 长度受限制 情况

( 1 ) 受限制的字符串函数 ( 推荐使用 降低错误率 )

长度受限制的字符串函数 :

  • 1.概念 : 长度受限制的字符串函数, 其 字符串参数 会 跟随一个字符串先关的长度测参数, 一般为 size_t 类型, 用于限定字符串的字符数 ;
  • 2.推荐使用 : 在函数调用的时候, 优先使用长度受限制的字符串函数, 这样会减少出现错误的几率 ;

长度受限字符串函数 举例说明 :

  • 1.字符串拷贝 : char *strncpy(char *dest, const char *src, size_t n) ;

    • ( 1 ) 作用 : 拷贝 src 中 n 个字符 到 dest 目标字符串中 ;
    • ( 2 ) src 长度 小于 n : 使用 ‘\0’ 填充剩余空间 ;
    • ( 3 ) src 长度 大于 n : 只赋值 n 个字符, 并且不会使用 ‘\0’ 结束 , 因为已经复制了 n 个字符了 ;
  • 2.字符串拼接 : char *strncat(char *dest, const char *src, size_t n) ;
    • ( 1 ) 作用 : 从 src 字符串中赋值 n 个字符 到 dest 字符串中 ;
    • ( 2 ) 始终 ‘\0’ 结尾 : 函数始终在 dest 字符串之后添加 ‘\0’;
    • ( 3 ) 不填充剩余空间 : 对于拼接后剩余的数组空间, 不使用 ‘\0’ 填充 ;
  • 3.字符串比较 : int strncmp(const char *s1, const char *s2, size_t n) ;
    • ( 1 ) 作用 : 比较 src 和 dest 中前 n 个字符 是否相等 ;

五. 指针数组 与 数组指针

1. 数组指针

( 1 ) 数组类型介绍 ( 数组元素类型 | 数组大小 | 举例 int[8] )

数组类型 :

  • 1.数组类型要求 : 数组的类型有两个决定要素, 分别是 ① 数组元素类型 和 ② 数组大小 ;
  • 2.数组类型示例 : int array[8] 的类型是 int[8] ;

数组类型定义 :

  • 1.数组类型重命名 : 使用 typedef 实现 , typedef type(数组名)[数组大小] ;
  • 2.数组类型重命名示例 :
    • ( 1 ) 自定义一个 int[5] 类型的数组 : typedef int(ARRAY_INT_5)[5] ;
    • ( 2 ) 自定义一个 float[5] 类型的数组 : typedef float(ARRAY_FLOAT_5)[5] ;
  • 3.根据自定义的数组类型声明变量 :
    • ( 1 ) 使用自定义的 ARRAY_INT_5 声明变量 : ARRAY_INT_5 变量名 ;
    • ( 2 ) 使用自定义的 ARRAY_FLOAT_5 声明变量 : ARRAY_FLOAT_5 变量名 ;

(2) 数组指针简介 ( 指向数组的 一个 指针 | 数组指针类型定义方式 : 数组元素类型 ( * 指针名称 ) [数组大小] )

数组指针 : 本质是一个指针 ;

  • 1.数组指针作用 : 数组指针 用于 指向一个数组 ;
  • 2.数组名意义 : 数组名是数组首元素地址, 不是数组的首地址 , &数组名 是数组的首地址 ;
  • 3.数组首地址 : & 数组名 是数组首地址 , 数组首地址 不是 数组名( 数组首元素地址 ) ;
  • 4.数组指针定义 : 数组指针是通过 定义 数组类型 的指针;
    • ( 1 ) 数组类型 : 是之前说过的 包含 ① 数组元素类型 , ② 数组大小 两个要素, 定义数组类型 : typedef int(ARRAY_INT_5)[5] 定义一个 int[5] 类型的数组类型 ;
    • ( 2 ) 定义数组指针 : ARRAY_INT_5* 指针名称 ;
  • 4.数组指针定义的另外方式 : 类型 ( * 指针名称 ) [数组大小] ;
    • ( 1 ) 示例 : *int (p)[5] , 定义一个指针 p, 指向一个 int[5] 类型的指针 ;
    • ( 2 ) 不推荐此种写法 : 可读性很差 ;

数组指针 和 数组首元素指针 大小打印 :

  • 1.代码示例 :
#include <stdio.h>int main()
{//定义数组int array[5] = {0};//1. 普通指针, 普通指针类型是 int, 指向数组首元素首地址, 其指向内容大小为数组的首元素int *p = array;//2. 数组指针, 数组指针类型是 int[5], 指向数组首地址, 其指向的内容大小是整个数组 typedef int(ARRAY_INT_5)[5];ARRAY_INT_5 *p1 = &array;//3. 打印数字首元素指针 和 数组指针 指向的内存大小//     数组首元素指针 大小 为 4//    数组指针 大小 为 20printf("%ld, %ld\n", sizeof(*p), sizeof(*p1));return 0;
}
  • 2.编译运行结果 :

( 3 ) 代码示例 ( 定义数组类型 | 数组指针用法 )

代码示例 :

  • 1.代码 :
#include <stdio.h>//1. 自定义数组类型 int[5] 类型为 ARRAY_INT_5, 包含信息 ①int 类型数组 ②数组包含5个元素
typedef int(ARRAY_INT_5)[5];//2. 自定义数组类型 float[5] 类型为 ARRAY_FLOAT_5, 包含信息 ①float 类型数组 ②数组包含5个元素
typedef float(ARRAY_FLOAT_5)[5];//3. 自定义数组类型 char[5] 类型为 ARRAY_CHAR_5, 包含信息 ①char 类型数组 ②数组包含5个元素
typedef char(ARRAY_CHAR_5)[5];int main()
{//1. 使用自定义数组类型定义数组 : 定义一个 int 类型数组, 个数为 5个, 等价于 int array_1[5];ARRAY_INT_5 array_1;//( 1 ) sizeof(ARRAY_INT_5) 打印 ARRAY_INT_5 类型大小, //     该类型包含信息 ① int 类型数组 大小 5 个 ② 大小为 20 字节//( 2 ) sizeof(array_1) 打印 array_1 数组大小printf("%ld, %ld\n", sizeof(ARRAY_INT_5), sizeof(array_1));//2. 常规方法定义数组float array_2[5];//3. 数组指针 : 定义一个数组指针, 指向数组地址, 使用 &数组名 来获取数组地址, 其指向的内存内容大小为 20 字节//    注意区分数组首元素地址, array_2 是数组首元素地址, 指向内容大小为 4 字节//   注意区分 float* p = array_2, 这个指针是指向数组首元素地址的指针ARRAY_FLOAT_5* p = &array_2;//( 1 ) (*p)[i] 解析 : p 是数组指针, 其地址是数组地址 &array_2, //        *p 就是数组地址中存放的数组内容 *(&array_2), 即 (*p)[i] 等价于 array_2[i]//       (*p)[i] = i 语句 等价于 array_2[i] = i ;int i = 0;for( i = 0; i < sizeof(array_2)/sizeof(*array_2); i ++){(*p)[i] = i;}//( 2 ) 打印数组中每个值for(i = 0; i < sizeof(array_2)/sizeof(*array_2); i ++){printf("array_2[%d] = %f\n", i, array_2[i]);}//4. 使用自定义数组类型定义数组ARRAY_CHAR_5 array_3;//5. 常规方法定义数组指针 : char(*p1)[5] 等价于 ARRAY_CHAR_5* p1; char(*p1)[5] = &array_3;//6. 定义数组指针, 但是这个赋值过程中 左右两边类型不一致, //   array_3 会被强转为 char[6] 类型的数组指针char(*p2)[6] = array_3;//( 1 ) &array_3 : 是 array_3 数组的 数组地址, 绝对值 等于 其数组首元素地址//( 2 ) p1 + 1 : p1 指针 是 array_3 的数组指针, 该指针指向一个数组, p1 + 1 增加的是一个数组的地址//( 3 ) p2 + 1 : p2 指针 也是一个数组指针, 这个数组比 array_3 数组大一个, 因此 p2 + 1 地址可能比 p1 + 1 大1字节printf("&array_3 地址值 : %x, p1 + 1 地址值 : %x, p2 + 1 地址值 : %x\n", &array_3, p1 + 1, p2 + 1);return 0;
}
  • 2.编译运行结果 :

2. 指针数组

( 1 ) 指针数组简介 ( 数组中存储的元素是指针 | 数组指针 int (array)[5] 本质是指针 | 指针数组 int array[5] 本质是数组 )

指针数组 相关概念 :

  • 1.指针数组概念 : 指针数组是一个普通的数组, 其元素类型是 指针类型 ;
  • 2.指针数组定义 : 类型* 数组名称[数组大小] ;
    • ( 1 ) 指针数组 : int* array[5] ;
    • ( 2 ) 数组指针 : int (*array)[5] ;

( 2 ) 代码示例 ( 指针数组使用案例 )

指针数组代码示例 :

  • 1.代码 :
#include <stdio.h>/*1. 函数作用 : 传入一个字符串, 和 一个字符串数组, 找出字符串在字符串数组中的索引位置, 从 0 开始计数2. const char* key 参数分析 : ( 1 ) 常量分析 : 左数右指(const 在 * 左边 数据是常量, const 在 * 右边 指针是常量), 这里数据是常量, 不可修改( 2 ) 参数内容 : 字符串类型, 并且这个字符串内容不能修改3. const char* month[] 参数分析 : 指针数组( 1 ) 常量分析 : 左数右指, 指针指向的数组内容不能修改( 2 ) 参数内容 : 指针数组, 每个指针指向一个字符数组, 这些字符数组都是字符串, 这些指针不可改变
*/
int find_month_index(const char* key, const char* month[], const int month_size)
{int index = -1;//4. 遍历指针数组中指向的每个字符串, 与传入的 key 进行对比, 如果相等, 那么返回字符串在指针数组的索引//       ( 1 ) 对比函数 : 注意 strcmp 函数, 对比两个字符串, 如果相等 则 返回 0 ;int i = 0;for(i = 0; i < month_size; i ++){if(strcmp(key, month[i]) == 0){index = i;}}return index;
}int main()
{//1. 定义 指针数组, month 数组中, 每个元素都是一个指针, 每个指针指向字符串 const char* month[] = {  "January", "Febrary", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"  }; printf("查询 July 字符串索引 : %d\n", find_month_index("July", month, sizeof(month)/sizeof(*month)));printf("查询 HanShuliang 字符串索引 : %d\n", find_month_index("HanShuliang", month, sizeof(month)/sizeof(*month)));return 0;
}
  • 2.编译执行结果 :

3. main 函数参数 分析

( 1 ) main 函数简介

main 函数分析 :

  • 1.main 函数 : main 函数是 ① 程序的 入口函数 , ② 操作系统调用的函数 ;
  • 2.main 函数示例 :
int main()
int main(int argc)
int main(int argc, char* argv[])
int main(int argc, char* argv[], char* env[])
  • main 函数参数说明 :

    • ( 1 ) int argc 参数 : 程序命令行参数个数 ;
    • ( 2 ) char argv[] 参数* : 程序命令行字符串参数数组, 这是一个数组指针, 数组中每个元素都是指向一个字符串的指针 ;
    • ( 3 ) char env[] 参数* : 环境变量数组, 这是一个数组指针, 数组中每个元素都是指向一个字符串的指针 ; 这个环境变量 在 Windows 中是配置的 环境变量, 在 Linux 中是配置在 /etc/profile ( 一种设置方式, 还有很多设置方式 ) 中定义的环境变量 ;

(2) main 函数 代码示例

main 函数代码示例 :

  • 1.代码示例 :
#include <stdio.h>int main(int argc, char* argv[], char* env[])
{//1. 循环控制变量int i = 0;//2. 打印 main 函数参数printf("########参数个数%d 开始打印参数\n", argc);for(i = 0; i < argc; i ++){printf("%s\n", argv[i]);}printf("########参数打印完毕\n");printf("\n");printf("\n");//3. 打印环境变量printf("########开始打印环境变量\n");for(i = 0; env[i] != NULL; i ++){printf("%s\n", env[i]);}printf("########环境变量打印完毕\n");return 0;
}
  • 2.编译运行结果 : 环境变量打印出来的东西太多了, 就不一一截图查看 ;


六. 多维数组 和 多维指针

1. 二维指针 ( 指向指针的指针 )

( 1 ) 二维指针简介 ( 指向指针的指针 )

指向 指针 的 指针 ( 二维指针 ) :

  • 1.指针变量 : 指针变量会占用 内存空间 , 很明显可以使用 & 获取指针变量的地址 ;

    • ( 1 ) 32 位系统 : 指针占 4 字节空间 ;
    • ( 2 ) 64 位系统 : 指针占 8 字节空间 ;
  • 2.指向 指针变量 的指针 : 定义一个指针, 这个指针 保存一个 指针变量 的地址 ( 不是 指针变量指向的地址, 是指针变量所在的本身的地址 ) ;

指针变量 的 传值 和 传址 调用 :

  • 1.指针变量传值调用 ( 一维指针 ) : 直接将指针值传入, 修改的是 指针 指向的内存空间内容 ;

如 : void fun ( char *p ) , 这是相对于指针的传值调用, 相对于 char 类型数据的传址调用, 用于修改 p 指针指向的内存中的值 ;

  • 2.指针变量传址调用 ( 二维指针 ) : 在函数内部 修改 函数外部的变量, 需要传入一个地址值, 如果要修改的是一个指针, 那么需要传入指针的地址, 即参数是一个指向指针的指针 ; 指针变量传址调用, 修改的是 指针 指向的 指针变量 ;

如 : void fun(char ** pp) 该传址调用 即 传入的是 char* 指针的地址, 修改的是 pp 二维指针 指向的 char* 类型指针 ;

  • 3.函数中修改函数外部变量 : 只能使用指针 指向这个外部变量, 才可以修改这个外部变量 , 如果这个外部变量本身就是一个指针 , 那么就必须传入这个指针的地址, 那么传入的参数的内容就是一个二维指针 ;

( 2 ) 代码示例 ( 指针的传址调用 | 指向指针的指针 | 重置指针指向的空间 )

代码示例 :

  • 1.代码 :
#include <stdio.h>
#include <malloc.h>/*1. 方法作用 : 为参数 char **p 指向的 指针 重新分配内存空间2. char **p 参数 : 需要在函数中修改函数外部的变量, 就需要传入一个 指向要修改的目标 的指针变量需要修改的内容 是一个指针, 那么需要传入的参数就是 指向 指针变量 的指针这样才能完成 传址调用, 用来修改函数外部的变量 3. */
int reset_memory(char **p, int size, int new_size)
{//1. 定义函数中使用的变量//( 1 ) 定义返回值int ret = 0;//( 2 ) 循环控制变量int i = 0;//( 3 ) 新空间的大小int len = 0;//( 4 ) 申请的新空间char* p_new = NULL;//( 5 ) 用于计算用的指向新空间的指针char* p_new_tmp = NULL;//( 6 ) 用于指向老空间指针, 使用 * 与 传入的二维指针 计算 得来//     char** p 是指向 char* 指针 的 指针, 使用 *p 即可获得 指向 char* 的指针char* p_old = *p;//2. 前置条件安全判定, 避免无意义崩溃if(p == NULL || new_size <= 0){return ret;}//3. 重新分配空间, 并拷贝内存中的内容//( 1 ) 重新分配内存空间p_new = (char*)malloc(new_size);//( 2 ) 为计算使用的指针赋值, 之后赋值是需要使用指针的自增, 为了不改变指针内容, 这里我们设置一个临时指针p_new_tmp = p_new;//( 3 ) 获取 新空间 和 老空间 内存大小的最小值, 将老空间的内容拷贝到新空间中//        1> 新空间大于老空间 : 只拷贝所有老空间中的内容到新空间中//        2> 新空间小于老空间 : 只拷贝能将新空间填满的内容, 字符串可能丢失 '\0'len = (size < new_size) ? size : new_size;//( 4 ) 将老空间中的内容拷贝到新空间中for(i = 0; i < len; i ++){*p_new_tmp++ = *p_old++ ;}//4.释放原空间, 并修改传址调用的参数内容free(*p);*p = p_new;ret = 1;return ret;
}int main(int argc, char* argv[], char* env[])
{//1. 第一次为 char 类型指针分配 10 个字节空间char* p = (char*)malloc(10);//      打印指针 p 指向的内存地址, 即分配的内存空间地址printf("p 第一次分配空间后指向的地址 : %x\n", p);//2. 重置内存空间, 原来分配 10字节, 现在改为分配 8 字节//       注意 : 在 reset_memory 函数中改变函数外部变量的值, 需要传址调用, 即将变量的地址传到函数中reset_memory(&p, 10, 8);//       打印重置空间后的指针指向的地址printf("p 重置空间后指向的地址 : %x\n", p);return 0;
}
  • 2.编译运行结果 :

2. 二维数组

( 1 ) 二维数组 ( 存放方式 | 数组名 | 首元素类型 | 数组名 类似 常量指针 | )

二维数组 相关概念 : 二维数组 int array[5][5] ;

  • 1.二维数组存放方式 : 二维数组在内存中以 一维数组 方式排布 ;
  • 2.二维数组数组名 : 代表 二维 数组 首元素 地址, 其 首元素 是一个一维数组 , 即 array[0] ;
  • 3.二维数组首元素类型 : 数组名 array 指向二维数组首元素, 那么其类型是 数组指针, 数组类型 为 int[5] ( ① int 类型数组, ② 含有 5 个元素 ) ;
  • 4.数组名类似常量指针 : 二维数组的数组名可以看做常量指针, 除了两种情况 sizeof 计算大小 和 & 获取地址时 ;
  • 5.具体的数据值存放 : 二维数组第一维是 数组指针, 第二围才是具体的数据值 ;
  • 6.二维数组图示 :

一些注意点 :
1.编译器没有二维数组概念 : C语言中没有二维数组改变, 编译器 都按照一维数组来处理, 数组的大小在编译时就确定了 ;
2.二维数组由来 : C 语言中的数组元素可以是任何类型, 即可以是一维数组, 这样就产生了二维数组 ;
3.首元素地址确定时间 : 在编译阶段确定的 除了 数组大小外, 数组的首元素也是在编译阶段确定的, 在程序运行阶段首元素地址不能被修改 (看做常量) ;


(2) 代码示例 ( 以一维数组方式遍历二维数组 | 体现二维数组的数据排列 )

代码示例 :

  • 1.代码 :
#include <stdio.h>
#include <malloc.h>/*遍历一维数组1. int *array 参数解析 : 传入的一维数组的首地址 2. int size 参数解析 : 用于限制数组大小, 数组传入后也会退化为指针, 数组是带有元素个数属性的, 因为数组类型是 int[9], 但是指针不包含元素个数 指针类型是 int*
*/
void array_traverse (int *array, int size)
{int i = 0;for(i = 0; i < size; i ++){//使用数组递增的方式打印数组元素printf("array[%d] = %d\n", i, *array++);}
}int main()
{//1. 定义二维数组int array[2][2] = {{0, 1}, {2, 3}};//2. 获取二维数组首地址, 将其赋值给 一维数组 int* pint *p = &array[0][0];//3. 打印一维数组, 可以看到, 二维数组的数据排列//      先将 {0, 1} 放入的内存, 然后紧接着存放 {2, 3} 数据array_traverse(p, 4);return 0;
}
  • 2.编译执行结果 :

代码分析 :
将二维数组的首地址赋值给 类型相同 的一维数组, 遍历该一维数组, 并且该数组的大小为 二维数组所有值得大小 , 由此可以看出, 二维数组的数据排布是按照索引, 先放入二维数组的第一个数组元素, 在按照索引依次将数组放入内存中 ;


3. 数组名

( 1 ) 数组名 简介 ( 数组首元素地址 | &数组名 是 数组地址 )

数组名 相关概念 :

  • 1.数组名 : 数组名代表了 数组首元素地址 ;
  • 2.一维数组 数组名 : int array[2], array 指向数组首地址, 其指向了一个 int 类型首元素, array 类型为 int * ;
  • 3.二维数组 数组名 : int array[2][3] , array 指向数组首地址, 其指向了 类型 int[3] 数组的首元素, array 的类型是 int(*)[5] ;
    • ( 1 ) 类似常量指针 : 二维数组的数组名 可以看做为 常量指针 ;
    • ( 2 ) 看做一维数组 : 二维数组可以看做一维数组, 只是这个一维数组内的元素 是 一维数组 ;
    • ( 3 ) 二维数组元素 : 二维数组中每个元素都是 基础类型同类型的 一维数组,

( 2 ) 代码示例 ( 数组名指针指向的内容 | 二维指针数组名对应的指针运算 )

代码示例 :

  • 1.代码 :
#include <stdio.h>int main()
{//1. 定义二维数组, array 代表了 数组首地址//     array 的本质是 指向 一个 int(*)[5] 类型的一维数组 的指针int array[5][5];//2. 定义数组指针, 指针 p 指向一个 int(*)[4] 一维数组 int(*p)[4];//3. 注意了, 两个指针类型不同//  ( 1 ) array 指针 : 指向 int(*)[5] 类型的一维数组, array + 1 即内存中移动了 5 个 int 大小的内存//          ① 下标运算 : array[1] 即 array 指针 + 1 ;//    ( 2 ) p 指针 :   指向 int(*)[4] 类型的一维数组, p + 1 即内存中移动了 4 个 int 大小的内存p = array;/*4. 指针计算过程 : ( 1 ) &array[4][2] 计算 : array 指针指向 int(*)[5] 一维数组, array[4] 计算过程是 要加上 4 个 int(*)[5] 一维数组 , array[4] 指向 内存中(二维数组起始为0) 第 20 个元素, array[4][2] 指向第 22 个元素, &array[4][2] 是一个指向 array[4][2] 的int 类型指针( 2 ) &p[4][2] 计算 : p 指针指向 int(*)[4] 一维数组, p[4] 计算过程是 要加上 4 个 int(*)[4] 一维数组 , p[4] 指向 内存中(二维数组起始为0) 第 16 个元素, p[4][2] 指向第 18 个元素, &p[4][2] 是一个指向 p[4][2] 的int 类型指针( 3 ) 一个指向第 22 个元素, 一个指向第 18 个元素, 其起始地址是一样的, 因此结果是 -4*/printf("%ld\n", &p[4][2] - &array[4][2]);return 0;
}
  • 2.编译运行结果 :
  • 3.图示 :

( 3 ) 代码示例 ( 一维数组遍历 | 二维数组遍历 )

代码示例 :

  • 1.代码 :
#include <stdio.h>//1. 宏定义, 使用该宏 计算数组大小
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(*a))int main()
{//1. 遍历一维数组int array_1d[5] = {0, 1, 2, 3, 4};int i = 0;for(i = 0; i < ARRAY_SIZE(array_1d); i++){/**(array_1d + i) 执行过程 : ( 1 ) array_1d + i : 首元素指针 + i, 即获取指向第 i 个元素的指针( 2 ) *(array_1d + i) : 获取第 i 个元素指向的数据, 该表达式等价于 array_1d[i]*/printf("array_1d[%d] = %d\n", i, *(array_1d + i));}//2. 遍历二维数组int array_2d[3][3] = { {0, 1, 2}, {3, 4, 5}, {6, 7, 8} };int j = 0;for(i = 0; i < ARRAY_SIZE(array_2d); i ++){for(j = 0; j < ARRAY_SIZE(array_2d[0]); j ++){/**(*(array_2d + i) + j) 计算过程 : ( 1 ) array_2d : 是二维数组数组首元素, 本质是数组指针, 类型是 int(*)[3], 指向一个 int[3] 数组 ;( 2 ) array_2d + i : 是指向数组 第 i 个元素, 其地址本质能上移动了 i 个 int[3] 数组所占的空间 ;( 3 ) *(array_2d + i) : array_2d + i 是一个数组指针, 使用 * 即获得其指向的内容 一个 int[3] 数组;( 4 ) *(array_2d + i) + j : 获取其指向的 int[3] 类型数组的 第 j 个指针 ;( 5 ) *(*(array_2d + i) + j) : 即获取 int[3] 类型数组中 第 j 个指针指向的实际的 int 数据 ;*/printf("array_2d[%d][%d] = %d\n", i, j, *(*(array_2d + i) + j));}}return 0;
}
  • 2.编译运行结果 :

( 4 ) 代码示例 ( 为二维数组申请内存空间 )

算法思想 : 为 int[3][3] 申请内存空间 ;

  • 1.申请空间 : 分别申请 数组指针空间 和 数据空间 ;

    • ( 1 ) 申请数组指针空间 : 申请三个 数组指针 空间, 只是三个普通的指针, 但是该指针类型是 int(*)[3], 即指向 int[3] 数组的指针 ;
    • ( 2 ) 申请数据空间 : 申请 能存放 9个 int 值的数据空间 ;
  • 2.分配指针 : 将申请的三个 数组指针 , 分别指向对应的 9 个int 值空间的对应位置 ;

代码示例 :

  • 1.代码 :
#include <stdio.h>
#include <malloc.h>//1. 宏定义, 使用该宏 计算数组大小
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(*a))/*为二维数组分配内存空间1. 参数说明 : ( 1 ) int row : 二维数组的行数, 即 有多少 一维数组; ( 2 ) int column : 每个一维数组中的元素个数
*/
int** malloc_array_2d(int row, int column)
{//1. 分配 数组指针 空间, 分配数组指针空间, 共有 row 个指针int** ret = (int**)malloc(sizeof(int*) * row);//2. 分配数据存放空间, 即 row * column 个 int 类型数据存放的空间int * p = (int*)malloc(sizeof(int) * row * column);if(ret && p){//3_1. ret 和 p 内存分配成功, 那么开始 将 ret 数组指针 与 p 内存空间 连接起来int i = 0;for(i = 0; i < row; i ++){/*说明 : ( 1 ) ret 是分配的 数组指针 数组的 首元素, ret[i] 是第 i 个 数组指针 元素( 2 ) p + i*column 是 具体到 int 类型元素的首地址( 3 ) 这里将 int* 类型指针赋值给了 int(*)[column] 类型指针, 所幸是地址赋值, 将int* 指针存放的地址 赋值给了了 int(*)[column] 类型指针的地址*/ret[i] = (p + i * column);}}else{//3_2. 如果分配空间失败, 即 ret 或 p 有一个内存分配失败, 释放所有内存, 返回空free(ret);free(p);ret = NULL;}return ret;
}//释放申请的二维数组空间
void free_array_2d(int** array_2d)
{//array_2d[0] 代表了 第一个数组 的收个 int 类型元素地址, 该操作释放了int类型数据空间内存free(array_2d[0]);//array_2d 代表了 数组指针 数组的 首元素地址, 该操作释放了数组指针空间的内存free(array_2d);
}int main()
{//1. 申请二维数组空间int** array_2d = malloc_array_2d(3, 3);int i = 0, j = 0;//2. 为二维数组赋值for(i = 0; i < 3; i ++){for(j = 0; j < 3; j ++){*(*(array_2d + i) + j) = i * 3 + j;}}//3. 打印二维数组内容for(i = 0; i < 3; i ++){for(j = 0; j < 3; j ++){printf("array_2d[%d][%d] = %d\n", i, j, array_2d[i][j]);}}return 0;
}
  • 2.编译运行结果 :
  • 3.图示 :

五. 数组参数 与 指针参数

1. 数组参数退化为指针参数的意义

( 1 ) 数组参数退化的相关概念 ( 指针退化成数组 )

一维数组参数退化为指针 :

  • 1.C语言中的拷贝方式 : C 语言中只会以 传值拷贝 的方式来传递参数 ;

    • ( 1 ) 传递指针也是传值 ( 修改指针指向的地址的内容是用户行为 ) : 只是传的是指针变量的值, 但是这个变量中存放着地址, 函数中可以改变这个地址的值 ;
  • 2.数组传递的方式 :
    • ( 1 ) 传递整个数组 : 如果将整个数组传递过去, 如果数组中元素很多, 需要将数组所有元素都要拷贝一份 ;
    • ( 2 ) 传递数组首元素地址 : 将数组名 ( 数组首元素的地址 ) 看做常量指针传入函数 ;
    • ( 3 ) C 语言针对数组参数的效率考虑 : 假如数组有 10000 个元素, 传递数组效率就非常低了, 如果传递数组首元素指针, 只用拷贝指针变量的值, 只拷贝 4 ( 32位系统 ) 或 8 ( 64位系统 ) 个字节, 这样效率能大大提高 ;
  • 3.数组参数退化的意义 : 数组参数退化为指针, 程序的执行效率能大大的提高 ;

二维数组参数退化问题 :

  • 1.二维数组本质 : 二维数组也可以看做一维数组, 该一维数组中的每个数组元素都是一维数组 ;
  • 2.数组退化过程 :
    • ( 1 ) 一维数组参数退化过程 : void fun(int array[5]) <-> void fun(int array[]) <-> void fun(int* array) 以上的三种类型的参数都是等价的 ;

      • ① 第一次退化 : 数组的个数可以省略掉, 只需要表明数组元素类型即可, 数组元素类型 int[] 类型;
      • ② 第二次退化 : 只含有数组元素类型 不含数组个数的类型, 退化为 对应数组元素类型 的指针类型 ;
    • ( 2 ) 二维数组参数退化过程 : void fun(int array[3][3]) <-> void fun(int array[][3]) <-> void fun(int (*array)[3])
      • ① 第一次退化 : 数组的个数可以省略掉, 只需要表明数组元素类型即可, 数组元素类型 int[3] 类型;
      • ② 第二次退化 : 直接退化为指向 一维数组的 数组指针, 该数组指针类型为 int(*)[3] 类型;

下面列举数组参数与指针参数一些等价关系 : 去中括号 ( [] ), 变星号 ( * ) , 放左边;

数组参数 指针参数
一维数组 int array[5] 指针 *int array
一维指针数组 int array[5]* 指针 int* array*
二维数组 int array[3][3] 指针 *int (array)[3]

注意事项 :
1.多维数组参数要求 : 传递多维数组参数时, 需要将除第一维之外的其它所有维度的大小都带上 , 否则无法确定数组大小 和 类型, 编译时会报错 ;
2.数组参数限制 :
( 1 ) 一维数组 : 可以不带数组长度, 但是必须指定数组的大小 ;
( 2 ) 二维数组 : 数组 第一维 长度可以不带 ( 即 数组指针 元素个数可以省略 ) , 但是数组指针 指向的 数组类型大小必须指定 ( 第二维的大小必须指定 ) ;
( 3 ) 三维数组 : 数组 第一维 长度可不带, 但是第二维 和 第三维 长度 必须带上 ;


( 2 ) 代码示例 ( 二维数组参数 的指针退化 | 外层指针退化 | 内层数组指针没有退化 )

代码分析 : 如 int array[3][3] ;

  • 1.二维数组参数退化部分 : 二维数组本身 array 数组大小退化, 其退化为 int (*)[3] 类型, 指向一组数组指针的首地址 ;
  • 2.二维数组参数没有退化部分 : array 数组中, array 作为首元素, 其类型为 int[3] 类型, 该类型 包含 ① 其指向的一维数组 中的元素类型 int 和 ② 一维数组大小 3;

代码示例 :

  • 1.代码 :
#include <stdio.h>void traverse_array_2d(int array_2d[][2], int row)
{/*计算二维指针的列数( 1 ) array_2d 是二维指针中的 数组指针 数组中的首元素( 2 ) array_2d 是一个完整的数组指针, 该指针中包含着 其指向的数组的 类型 和 大小( 3 ) 数组指针退化时, 退化的只是 array_2d 的 数组指针 数组 (最外层的一维数组) 大小, 其每个元素都是一个 数组指针, 这个数组指针 包含 数组类型 和 大小, 没有退化*/int column = sizeof(*array_2d) / sizeof(*array_2d[0]);int i = 0, j = 0;for(i = 0; i < row; i ++){for(j = 0; j < column; j ++){printf("array_2d[%d][%d] = %d\n", i, j, array_2d[i][j]);}}
}int main()
{int array_2d[2][2] = {{0, 1}, {2, 3}};traverse_array_2d(array_2d, 2);return 0;
}
  • 2.编译执行结果 :

六. 函数指针

1. 函数类型 和 函数指针

(1) 相关概念 ( 函数类型要素 ① 返回值, ② 参数类型, ③ 参数个数, ④ 隐含要素 : 参数顺序 | 函数指针类型 返回值类型 (*变量名) (参数列表) )

函数类型 :

  • 1.函数类型引入 : 每个函数都有自己的类型 ;
  • 2.函数类型要素 : ① 返回值, ② 参数类型, ③ 参数个数, ④ 隐含要素 : 参数顺序 ;
    • 示例 : void fun(int a, float b) 函数的类型为 void( int, float ) ;
  • 3.函数重命名 : 使用 typedef 可以为函数重命名 , typedef 返回值类型 函数名称(参数列表) ;
    • 示例 : typedef void function(int, float), 就是将上面的 fun 函数重命名为 function ;

函数指针 :

  • 1.函数指针概念 : 函数指针 指向一个 函数类型 变量 ;
  • 2.函数名 : 函数名指向了 函数体 的 入口地址 ;
  • 3.函数指针定义( 宏定义类型 ) : 函数类型* 变量名 ;
  • 4.函数指针类型( 简单类型 ) : 返回值类型 (*变量名) (参数列表) ;

( 2 ) 代码示例 ( 定义函数指针 : ①typedef int(FUN)(int); FUN* p; 或者 ② void(*p1)(); | 给 函数指针 赋值 , 右值 可以直接使用 ① 函数名 或 ② &函数名 | 调用函数指针方法 : ① 函数指针变量名(参数) ② (*函数指针变量名)(参数) | 函数名 和 &函数名 是等价的 | 函数指针变量名(参数) 和 (*函数指针变量名)(参数) 也是等价的 )

代码示例 :

  • 1.代码 :
#include <stdio.h>//1. 定义函数类型 FUN, 其类型为 int(int)
typedef int(FUN)(int);//2. 定义一个 int(int) 类型的函数
int fun_1(int i)
{return i * i * i;
}//3. 定义一个 void() 类型的函数
void fun_2()
{printf("调用 fun_2 函数\n");
}int main()
{/*1. 将 fun_1 函数赋值给 FUN 类型指针( 1 ) FUN 是函数类型, 其类型是 int(int)( 2 ) fun_1 函数名是函数体的入口地址, 可以直接赋值给指针 ; */FUN* p = fun_1;//2. 通过指针调用函数, 指针变量名(参数) 可以调用指针指向的函数 ; printf("调用 p 指针结果 : %d\n", p(10));/*3. 定义 void() 类型的函数指针 p1( 1 ) 方法返回值(*函数指针变量名)(参数列表) 是定义一个函数指针( 2 ) &函数名 也可以获取函数的地址, 与 函数名 是等价的;注意 : 这里与数组不同, 数组名 和 &数组名 是两种不同的概念函数名 和 &函数名 是等价的*/void(*p1)() = &fun_2;/*4. 通过函数指针变量调用函数( 1 ) 通过 函数指针变量名(参数) 和 (*函数指针变量名)(参数) 两种方法都可以调用函数指针变量指向的函数( 2 ) 函数名 和 &函数名 是等价的, 函数指针变量名(参数) 和 (*函数指针变量名)(参数) 也是等价的*/p1();(*p1)();return 0;
}
  • 2.编译运行结果 :

2. 回调函数

( 1 ) 回调函数相关概念

回调函数简介 :

  • 1.回调函数实现 : 回调通过 函数指针 调用函数实现 ;
  • 2.回调函数特点 : 调用者 和 被调用的函数 互不知情, 互不依赖 ;
    • ( 1 ) 调用者 : 调用者不知道具体的函数内容, 只知道函数的类型 ;
    • ( 2 ) 被调函数 : 被调用的函数不知道 调用者 什么时候调用该函数, 只知道要执行哪些内容 ;
    • ( 3 ) 调用方式 : 调用者 通过 函数指针 调用具体的函数 ;

( 2 ) 代码示例 ( 回调函数示例 )

代码示例 :

  • 1.代码 :
#include <stdio.h>//1. 定义函数类型 FUN, 其类型为 int(int)
typedef int(FUN)(int);//2. 定义一个 int(int) 类型的函数
int fun_1(int i)
{return i * i * i;
}//3. 定义一个 int(int) 类型的函数
int fun_2(int i)
{return i + i + i;
}//4. 定义一个 int(int) 类型的函数
int fun_3(int i)
{return i + i * i;
}/*5. 此处参数 FUN function 是一个函数类型, 将 FUN 类型函数注册给 execute 函数
*/
int execute(int i, FUN function)
{return i + function(i);
}int main()
{//1. 给 execute 注册 fun_1 函数, 具体事件发生时调用 fun_1 函数printf("int i = 5; i * fun_1(i) = %d\n", execute(5, fun_1));//2. 给 execute 注册 fun_2 函数, 具体事件发生时调用 fun_2 函数printf("int i = 8; i * fun_2(i) = %d\n", execute(8, fun_2));//3. 给 execute 注册 fun_3 函数, 具体事件发生时调用 fun_3 函数printf("int i = 2; i * fun_3(i) = %d\n", execute(2, fun_3));return 0;
}
  • 2.编译运行结果 :

3. 解读 复杂的 指针声明 ( 难点 重点 | ①找出中心标识符 ②先右 后左 看 确定类型 提取 ③ 继续分析 左右看 … )

指针 定义 复杂性来源 :

  • 1.数组指针 : 数组指针类型为 int (*) [5] , 即 一个指向 int[5] 的指针, 其指针变量名称写在中间的括号中
  • 2.函数指针 : 函数指针类型为 int(*)(int, int), 即 一个指向 int(int, int) 类型函数的指针, 其指针变量名称写在中间的括号中 ;
  • 3.数组指针混合函数指针 : 如果出现了 数组指针 指向一个函数, 这个指针可读性很差, 理解需要一定的功力 ;

复杂指针阅读技巧 ( 主要是 区分 函数指针 和 数组指针 ) 右左法则 :

  • 1.最里层标示符 : 先找到最里层的圆括号中的标示符;

    数组指针和函数指针的标示符 ( 指针变量名 ) 都在中间的圆括号中, 因此该步骤先找到指针变量名

  • 2.右左看 : 先往右看, 再往左看 ;

  • 3.确定类型 : 遇到 圆括号 “()” 或者 方括号 “[]” 确定部分类型, 调转方向 ; 遇到 * 说明是指针 , 每次确定完一个类型 , 将该类型提取出来 , 分析剩下的 ;

    一种可能性 :
    int (*) [5] , 遇到中括号说明是数组指针类型,
    int(*)(int, int) , 遇到圆括号 说明是函数指针类型 ;

  • 4.重复 2 , 3 步骤 : 一直重复, 直到 指针 阅读结束 ;

指针阅读案例 :

  • 1.解读案例 1 :
 /*解读步骤 : 1. 研究第一个标示符 p  ( 1 ) 先找最里层的圆括号中的 标示符 p( 2 ) p 往右看, 是圆括号, 然后往左看, 是 * , 可以确定 p 是一个指针( 3 ) 将 (*p) 拿出来, 然后看剩下的部分, 右看是 圆括号 (, 明显是个函数类型, int (int*, int (*f)(int*)) 很明显是一个 函数类型2. 解读函数类型 int (int*, int (*f)(int*))( 1 ) 函数类型 int (int*, int (*f)(int*)) 的返回值类型是 int 类型( 2 ) 函数类型的第一个参数类型是 int* , 即 int 类型指针类型( 3 ) 函数类型的 第二个参数是 int (*f)(int*) 也是一个函数类型指针3. 解读 int (*f)(int*) 参数( 1 ) 标示符是 f, 由看 是 圆括号, 坐看是 * , 因此 f 是一个指针;( 2 ) 将(*f) 提取出来, int(int*) 是一个函数类型, 其返回值是 int 类型, 参数是 int* 指针类型总结 : 指针 p 是一个指向 int(int*, int (*f)(int*)) 类型函数的指针, 函数返回值是 int 类型, 参数是 int* 指针类型 和 int (*)(int*) 函数指针 类型指针 f 是一个指向 int(int*) 类型函数的指针, 其返回值是 int 类型, 参数是 int* 指针类型*/int (*p) (int*, int (*f)(int*));

  • 2.解读案例 2 :
 /*解读步骤 : 1. 确定 p1 的类型( 1 ) 找出最中心圆括号中的标示符, p1;( 2 ) 数组类型确定 : 右看发现中括号, 说明 p1 是一个数组, 数组中有 3 个元素, 数组的类型目前还不知道( 3 ) 数组内容确定 : 左看发现 *, 说明数组中存储的是 指针类型, 这里就知道了 ;目前知道了 数组 p1 的要素 : ① 数组中有 3 个元素, ② 数组元素类型是指针;2. 确定数组指针类型 : 上面确定了 p1 的数组个数 和 元素是指针, 但是指针指向什么不确定( 1 ) 将 (*p1[3]) 提取出来, int(int*) 明显是一个函数类型, 返回值是 int 类型, 参数是 int* 类型总结 : p1 是一个数组, 数组中含有 3 个元素, 数组元素类型为 int(*)(int*) 函数指针, 即 指向 int(int*) 类型函数的指针 */int (*p1[3])(int*);

  • 3.解读案例 3 :
 /*解读步骤 : 1. 确定 p2 类型 : ( 1 ) 找出最中心的圆括号中的标示符, p4, 右看是圆括号 ), 掉头左看是 * , 说明 p2 是一个指针;( 2 ) 将 (*p2) 提取出来, 分析int (*[5])(int*), ( 3 ) 右看是 [, 说明指针指向了一个数组, 该数组有 5 个元素( 4 ) 左看是 * , 说明数组中的元素是指针, 下面分析指针指向什么2. 确定指针数组中指针指向什么 : ( 1 ) 将 (*(*p2)[5]) 提取出来, 可以看到 指针指向 int(int*) 类型的函数总结 : p2 是一个数组指针, 指向一个数组, 该数组有 5 个元素, 每个元素都是一个指针, 数组中的指针元素指向 int(int*) 类型的函数*/int (*(*p2)[5])(int*);

  • 4.解读案例 4 :
 /*解读步骤 : 1. 确定 p3 基本类型 : ( 1 ) p3 右看是圆括号 ), 左看是 * , 说明p3 是指针( 2 ) 将 (*p3) 提取出来, int (*(int*))[5], 右看是 圆括号 (, 说明指针指向一个函数( 3 ) 函数的参数是 int*, 返回值是一个指针, 指向一个类型2. 确定返回值的类型( 1 ) 将 (*(*p3)(int*)) 提取出来, 右看是 [, 说明是数组类型, 剩下 int[5] 类型, 返回值指针指向一个 int[5] 类型的数组, 那么返回值类型是 int(*)[5] 数组指针总结 : p3 指向一个 函数, 函数的参数是 int* 指针, 返回值是 指向 int[5] 数组 的 数组指针*/int (*(*p3)(int*))[5];


【C 语言】指针 与 数组 ( 指针 | 数组 | 指针运算 | 数组访问方式 | 字符串 | 指针数组 | 数组指针 | 多维数组 | 多维指针 | 数组参数 | 函数指针 | 复杂指针解读)相关推荐

  1. 【C语言进阶深度学习记录】三十一 数组作为函数参数时退化为指针

    之前的学习数组的文章中,已经知道一维数组作为函数参数的时候,最终会被编译器编译为指针.今天来看看二维数组的情形 文章目录 1 为什么C语言中的数组作为函数参数会退化为指针? 2 二维数组作为函数参数如 ...

  2. C语言基础10——指针进阶。字符指针、指针数组、数组指针、函数指针、函数指针数组、回调函数、数组名详解、杨氏矩阵、字符串旋转

    目录 字符指针 指针数组 数组指针 数组传参.指针参数 函数指针 函数指针数组 指向函数指针数组的指针 回调函数 练习 数组名的意义 指针笔试题 字符指针 字符指针的另一种使用方式 #include ...

  3. C语言 —— 数组(数组的声明、初始化、访问)与字符串指针

    1.什么是数组 若将有限个类型相同的变量的集合命名,那么这个名称为数组名. 数组是在程序设计中,为了处理方便, 把具有相同类型的若干元素按无序的形式组织起来的一种形式. [1]  这些无序排列的同类数 ...

  4. 数组指针和指针数组,函数指针和指针函数,常量指针和指针常量,常量引用

    一.数组指针和指针数组 1.数组指针(行指针) 首先要知道数组指针是指向数组的指针.所以数组指针本质是个指针,只不过指向一个数组而已.格式为:T (*ptr)[]. 注意:"[]" ...

  5. 数组的下标访问和指针访问方式效率分析比较

    2019独角兽企业重金招聘Python工程师标准>>> 1. int array[10], a; for (a = 0; a < 10; a ++) {     array[a ...

  6. 【嵌入式开发】C语言 命令行参数 函数指针 gdb调试

    . 作者 : 万境绝尘 转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/21551397 | http://www.hanshul ...

  7. c语言函数中使用指针变量,C语言函数指针变量

    C语言函数指针变量 导语:在C语言中规定,一个函数总是占用一段连续的内存区, 而函数名就是该函数所占内存区的首地址. 这就是函数指针变量.下面是相关介绍,仅供参考! 在C语言中规定,一个函数总是占用一 ...

  8. C语言-函数原型,指针与函数

    C语言-函数原型,指针与函数 4.函数原型 4.1 函数原型是什么? 4.2 函数原型怎么用? 4.3 函数原型有什么用? 4.4 函数前置声明 5. 指针与函数 5.1 函数名 5.2 函数指针 5 ...

  9. c语言实现函数给主函数中的指针赋值的方法

    //利用二维指针.自从学了c之后,还没怎么用过二维指针,这么算是记住了 /* c语言实现函数给主函数中的指针赋值的方法*/#include<stdio.h>void f (int **p) ...

最新文章

  1. 机器视觉与计算机视觉的区别?
  2. R-Bioconductor安装
  3. matlab符号函数绘图法_转载:MATLAB 符号函数作图
  4. 一个拥有12年SAP CRM WebClient UI开发经验的开发人员的分享
  5. storm的并行度的解释--- ( 看完就能理解 )
  6. python封装c接口_用C为python3.1封装mysql接口(一)
  7. day26(模块 logging 高级用法、collection、random)
  8. CCF201509-3 模板生成系统(100分)
  9. 数据挖掘原理与实践学习(3)
  10. 【Android安全】fastboot相关
  11. NMOS PMOS Charge pump flying capacitor充电泵
  12. B站狂神说JavaWeb学习笔记
  13. 两栈共享空间的存储结构设计
  14. RGB和HSV相互转换
  15. python中的索引从几开始计数_列表的索引是从0开始的
  16. 3D MAX模型导入Revi
  17. 斯坦福四足机器人运动学逆解(笔记4/作业4)
  18. 能够找一个计算机好的人把六级成绩改一下吗,六级成绩查询
  19. Linux格式化sd卡博客,Linux下格式化U盘或者SD卡
  20. (赠书活动第3期)清华大学出版社618大促《IT系列丛书》

热门文章

  1. 2018总结及2019计划
  2. 35+非常棒的视差滚动(Parallax Scrolling)效果WordPress主题
  3. Linq to objects示例
  4. IE6Bug,外层container设置了overflow:auto,但是内层嵌套元素有position:relative的时候,显示错误。...
  5. Inserting/Removing shutters and filters
  6. 管理文库]我喜欢的10个经典管理学定律点评
  7. 算法学习:manacher
  8. redis入门(数据类型)
  9. adb 测试工作中的总结
  10. android studio gradle 配置