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

C 专栏收录该内容
18 篇文章 2 订阅

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


文章目录



注意 : 博客中出现的关于指针的计算方式, 如果在 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) 结果是 1
	printf("%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* pp指针地址可变 p指针指向的内容不可变 (const 在 * 左边, 数据不可变)
int const* pp指针地址可变 p指针指向的内容不可变 (const 在 * 左边, 数据不可变)
int* const pp指针地址不可变 p指针指向的内容不可变 (const 在 * 右边, 地址不可变)
const int* const pp指针地址不可变 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>

//数组大小 和 初始化 示例
//数组大小 : 
//初始化 : 如果不初始化, 那么数组中就是随机值; 全部初始化, 部分初始化 : 其余默认为 0

int 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. 数组长度 : 数组长度 666
	char 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 的实际长度, 返回 4
	printf("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 的实际长度, 返回 4
	printf("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. 安全编程, 传入的两个值不能为 NULL
	assert(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
	//	 数组指针 大小 为 20
	printf("%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* p
	int *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];

这里写图片描述


  • 5
    点赞
  • 1
    评论
  • 26
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值