该文章Github地址:https://github.com/AntonyCheng/c-notes
在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.csdn.net/AntonyCheng/article/details/136555245),该模板集成了最常见的开发组件,同时基于修改配置文件实现组件的装载,除了这些,模板中还有非常丰富的整合示例,同时单体架构也非常适合SpringBoot框架入门,如果觉得有意义或者有帮助,欢迎Star & Issues & PR!
上一章:由浅到深认识C语言(7)
8.指针的概念与应用
8.1.关于内存
存储器:计算机的组成中,用来存储程序和数据,辅助CPU进行运算处理的重要部分;
外存:又叫外部存储器,长期存放数据,掉电不丢失数据,常用设备:硬盘,flash,rom,u盘,光盘,磁带;
内存:又叫内部存储器,暂时存放数据,掉电数据丢失,常见设备:ram,DDR;
内存作用:
- 内存是沟通CPU与硬盘的桥梁;
- 暂存放CPU中的运算数据;
- 暂存与硬盘等外部存储器交换的数据;
内存的分类:
- 物理内存:内存物理硬件;
- 虚拟内存:操作系统虚拟处理的内存;
注意:
- 操作系统会在物理内存和虚拟内存之间做映射;
- 在32位系统下,每个进程的寻址地址是4G:0x00 00 00 00 ~ 0xff ff ff ff;
- 写程序时,我i们操作的是虚拟空间;
- 在运行程序时,操作系统会将虚拟内存进行分区:
- 堆:在动态申请内存的时候,在堆里开辟内存;
- 栈:主要存放局部变量;
- 静态全局区:
- 未初始化的静态全局区;
- 初始化的静态全局区;
- 代码区:存放代码程序;
- 文字常量区:存放常量;
- 内存是以字节为单位来存储数据,可以将程序中的虚拟寻址空间看成一个很大的一维字符数据(因为字符是一字节的);
8.2.指针的概念
操作系统给内存的每个存储单元分配了一个编号,32位系统从 0x00 00 00 00 ~ 0xff ff ff ff ,64位系统从 0x00 00 00 00 00 00 00 00 ~ 0xff ff ff ff ff ff ff ff ,这个编号称之为地址,而指针就是地址;指针很重要,甚至可以通过相邻指针来推算数据类型,需要结合 C 语言中的数据类型所占内存来进行操作;
以64位为例, 2 64 2^{64} 264如下:
注意:我们认为逻辑地址是从 1 开始的,而物理地址是从 0 开始的,当物理地址转换成虚拟地址之后,也是从 0 开始的,也就是说,我们能打印出来的指针范围介于 0 ~ 18446744073709551615之间:
#include<stdio.h>
static void test() {
int* a = 0;
int* b = 18446744073709551615;
printf("%p\n", a);
printf("%p\n", b);
}
int main(int argc, char* argv[]) {
test();
return 0;
}
打印效果如下:
然而,指针也可以为负数,但是不要让它为负数,因为负地址中存放着系统数据,例如 bios 系统等;
8.3.指针变量
概念
本质是一个变量,但是并不存放普通的数据类型,而是存放一个地址编号,即存放指针;
所占内存大小:
16位系统指针变量所占大小为:2B 示例:0x00 00 ==> 二字节;
32位系统指针变量所占大小为:4B 示例:0x00 00 00 00 ==> 四字节;
64位系统指针变量所占大小为:8B 示例:0x00 00 00 00 00 00 00 00 ==> 八字节;
以64位系统为例:
#include<stdio.h>
static void test() {
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(float*));
printf("%d\n", sizeof(double*));
printf("%d\n", sizeof(char***));
return;
}
int main(int argc, char* argv[]) {
test();
return 0;
}
打印效果如下:
注意:地址编号不能决定指针变量所占字节,始终是 8B;
变量的定义
定义指针变量的步骤:
*
修饰指针变量名;- 明确所保存变量的数据类型(前提),去定义一个普通变量;
- 从上往下整体替换,
*
挨着数据类型;
学习必知:取地址操作是 &变量名;
演示如下:
//stepOne
*p;
//stepTwo
int num;
//stepThree
int* p;
实例初识:
#include<stdio.h>
static void test() {
//由系统自动分配一个合法的空间
int num = 10;
//按需求创建指针
int* p;
printf("%p\n", &num);
//&num代表的是num的首地址
p= #
printf("%p\n", p);
}
int main(int argc, char* argv[]) {
test();
return 0;
}
以上程序会在内存里发生如下过程:
打印效果如下:
注意:由上得知一个字节的内存都会有一个地址,而一个数据类型,例如 int 有 4 个字节,所以在 int 里会有 4 个地址,而 & 该数据变量时,接收到的只是首地址;
变量的使用
对保存的地址空间进行读写操作;
在使用中,*p 表示取 p 所保存的地址编号对应的空间内容; ==> 指针变量 p 的解引用;
知识点梳理:
#include<stdio.h>
static void test() {
int num = 10;
int* p = #
//以下是各个变量使用时所代表的内容
//num代表10这个整数
//&num代表num整型变量的首地址值
//p代表num整形变量的首地址值
//&p代表p指针变量的首地址值
//*p代表10这个整数
}
int main(int argc, char* argv[]) {
test();
return 0;
}
输出示例如下:
#include<stdio.h>
static void test() {
int num = 10;
int* p = #
printf("&num = %p\n", &num);
printf("p = %p\n", p);
printf("*p = %p\n", *p); //对于*p的使用
printf("*p int = %d\n", *p); //对于*p的使用
}
int main(int argc, char* argv[]) {
test();
return 0;
}
打印效果如下:
输入示例如下:
#include<stdio.h>
static void test() {
int num = 10;
int* p = #
printf("请输入要改变的值:");
scanf_s("%d", p);
printf("*p = %p\n", *p);
printf("*p int = %d\n", *p);
printf("num = %d\n",num);
}
int main(int argc, char* argv[]) {
test();
return 0;
}
打印效果如下:
变量的类型
这里区分两种类型 { 指针变量自身的类型 指针变量所指向的类型 \bf{这里区分两种类型} \begin{cases} \bf{指针变量自身的类型} \\ \bf{指针变量所指向的类型} \end{cases} 这里区分两种类型{指针变量自身的类型指针变量所指向的类型
区分方法:
- 指针变量自身的类型就是把变量名给删去,即
int* p
的自身类型是int*
; - 指针变量所指向的类型就是把变量名和离此变量名最近的第一个
*
给删去,即int* p
所指向的变量是int
;
排序顺序:
0x04 | 0x03 | 0x02 | 0x01 |
---|
示例一: 指针变量取值宽度;
宽度指的是从目标部分首地址开始往后取地址的固定宽度(地址是按照1B内存排列的,固定宽度指的是所指向类型的宽度,即由所指向类型长度决定);
#include<stdio.h>
static void test() {
int num = 0x01020304; //这里有4B内存
int* p1 = # //int往后取的是4B宽度
short* p2 = # //short往后取的是2B宽度
printf("*p1 = %#x\n", *p1);
printf("p2 = %#x\n", *p2); //证明指针排序和取值顺序问题
}
int main(int argc, char* argv[]) {
test();
return 0;
}
打印效果如下:
示例二: 指针变量取值跨度;
跨度指的是从变量首地址开始往后取到目标部分前一个地址的单位长度(单位长度指把所指向类型的长度看成一个单位,即int的单位长度就是4B,int的两个单位长度就是8B,即由指针所指向类型长度决定);
#include<stdio.h>
static void test() {
int num = 0x01020304;
short* p1 = #
int* p2 = #
printf("pOfNum = %d\n", p1);
printf("pOfNum'sShort = %d\n", p1 + 1);
printf("pOfNum'sInt = %d\n", p2 + 1);
printf("pOfNum'sInts(2) = %d\n", p2 + 2);
}
int main(int argc, char* argv[]) {
test();
return 0;
}
打印效果如下:
关于宽度和跨度图解:
0x04 | 0x03 | 0x02 | 0x01 |
---|
若要取 0x0203 ,则 0x04 这1B长度就是跨度,而 0x02 和 0x03 这2B长度就是宽度;
综合案例:
现有 int num = 0x01020304;
,要求自定义变量取出 0x0102
;
#include<stdio.h>
static void test() {
int num = 0x01020304; //内存中存的顺序是 04 03 02 01
//第一步:这里将指针从04首移到了03尾,然后按照short长度升序排成 03 04
short* p = #
//第二步:这里又将指针从03尾(02首)移到了01尾,然后按照short长度升序排成 01 02
short* p1 = p + 1;
//这里的第一步和第二步可以调换;
printf("%#x", *p1);
}
int main(int argc, char* argv[]) {
test();
return 0;
}
打印效果如下:
此时,跨度和宽度相等,如果跨度和宽度不相等,就需要用到类型转换,且选择宽度和跨度中最小的那个值作为新指针变量的长度(单位长度的倍数);
类型转换:
示例如下:
我们再去取 0x0203
;
#include<stdio.h>
void test() {
int a = 0x01020304;
char* p = &a;
short* p1 = (short*)(p + 1);
printf("%#x", *p1);
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
变量的初始化
如果局部指针变量不初始化,保存的是随机的地址编号,记住千万不要操作,因为会导致“断错误”,用打印来演示:
#include<stdio.h>
void test() {
int* p;
printf("001\n");
printf("%d\n", p); //vs中会直接报错,在vc++中可以运行,但是一旦运行到这里,程序会自动结束;
printf("002\n");
}
int main(int argc, char* argv[]) {
test();
return;
}
所以我们要进行指针变量的初始化,我们不想让指针变量指向任何地方,可以初始化为 NULL,而 NULL 指针就是 0 地址,从 0 地址到某一地址中包含着很多系统底层的数据,所以记住这里也不能操作,若操作,也会导致“断错误”;
int* p = NULL;
//注意这里是 NULL(大写),因为这里运用到了宏,宏规则上是大写;
示例如下:
#include<stdio.h>
void test() {
int* p = NULL;
printf("001\n");
printf("%p\n",p);
printf("002\n");
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
正常的初始化:
为了将指针变量初始化为合法的地址,则应该让指针变量存入合法的地址;
int num = 10;
int* p = #
使用变量须知
取地址符&
和指针解引用符*
区别:
对一个变量取地址 &+变量名
,整个表达式的类型就是 **变量的类型 + *
**;
对一个指针变量取星花 *+变量名
,整个表达式的类型就是 指针变量的类型 - *
;
如果 * 和 & 同时存在,可以互相抵消,规则如下:
==当 ***** 和 & 交错出现时,按顺序抵消即可,如例 *&*&p ==> p ,*&*&*p ==> *p ,&*&p > &p;
当 ***** 和 & 非交错出现时,不能出现 && 这种情况;
除有一种排列 外, ***** 和 & 的数量必须相同,即化简结果始终为 p ,这一种排列 就是 *&**&p 这种排列,整个符号项内只能出现这一次 &**& 这种排列,即化简结果始终为 *p 或者 p;
论证:*p == num;
#include<stdio.h>
void test() {
int num = 10; //num = 10;
int* p = # //p = &num
//*p == *# ?
if (*p == *&num) {
printf("*p = %d\n", *p);
printf("*&num = %d\n", *&num);
printf("num = %d\n", num);
printf("*p == *&num == num\n");
}
else {
printf("no_equal");
}
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
指针注意事项
-
void 不能定义变量,但 void* 可以定义指针变量;
因为普通变量用void定义,系统不知道其大小;但是指针的大小在系统里是固定的,即系统知道其大小;
该指针叫万能指针,它可以保存任意一级指针:
#include<stdio.h> void test() { void* p; char ch; p = &ch; int num; p = # float f; p = &f; } int main(int argc, char* argv[]) { test(); return; }
但是万能指针不能直接引用其 *p ,因为 void 所指向的是没有长度的数据,即不知道它的宽度;
#include<stdio.h> void test() { int num = 10; void* p = # printf("%d", *p); } int main(int argc, char* argv[]) { test(); return; }
报错如下:
所以我们应该实现对万能指针进行强制类型转换;
#include<stdio.h> void test() { int num = 10; void* p = # printf("%d", *((int*)p)); //p 临时的指向类型为 int,系统能确定宽度为 4B } int main(int argc, char* argv[]) { test(); return; }
打印效果如下:
-
*不要对没有初始化的指针变量取 ;
在 vc++ 中不会出现语法错误,但是会出现“断错误”,在 vs 中会直接报错;
因为 p 没有初始化,内容随机,也就是 p 指向了一个位置空间,系统不允许用户取值 *p 操作;
-
*不要对初始化为 NULL 的指针变量取 ;
在 vc++ 中不会出现语法错误,但是会出现“断错误”,在 vs 中会直接报错;
因为 NULL 处的地址是(void*)0 地址,也就是内存的起始地址,这是受系统保护的;
-
不要给指针变量赋普通的数值;
例如
int* p = 1000;
在 vc++ 和 vs 中不会出现语法错误,但是这一个地址我们 99.99% 的概率没有声明,也就是该位置是非法的,所以会出现“段错误”,所以不能使用 *p; -
指针变量不要操作越界的空间;
示例如下:
#include<stdio.h> void test() { char ch = 10; int* p = &ch; printf("p = %d", *p); } int main(int argc, char* argv[]) { test(); return; }
打印效果如下:
因为 char 所占空间是 1B ,而 int 所占空间是 4B,这样一取值就超过了 char 的地址,这样就造成了越界,越界了 3B;
8.4.数组元素的指针
图解如下:
引例一:
定义一个指针变量,保存 arr 数组首元素的地址;
#include<stdio.h>
void test() {
int arr[] = { 10,20,30,40,50 };
int* p = &arr[0]; //这里是目标代码
printf("arr[%d] = %d", 0, *p);
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
引例二:
通过数组元素的指针变量遍历数组的元素;
#include<stdio.h>
void test() {
int arr[5] = { 10,20,30,40,50 };
int n = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0]; //整体循环的前提就是p的初始化为首地址
for (int i = 0; i < n; i++) {
printf("%d ",*(p++));
/*
printf("%d ",*(p + i));
*/
/*
printf("%d ",*p);
p++
*/
}
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
引例三:
通过数组元素的指针变量给数组的元素获取键盘输入;
#include<stdio.h>
void test() {
int arr[5]={0};
int n = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
printf("请输入%d个整数,存入数组:\n",n);
for (int i = 0; i < n; i++) {
scanf_s("%d", p++);
/*
scanf_s("%d", p + i);
*/
}
p = &arr[0];
printf("你的数组如下\n");
for (int i = 0; i < n; i++) {
printf("%d ", *p++);
}
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
数组的 [] 和 *() 关系
引入如下代码:
int arr[5]={10,20,30,40,50};
int n = sizeof(arr) / sizeof(arr[0]);
//数组名 arr 作为地址代表的是首元素地址
//数组名 arr 作为类型代表的是数组总大小
在早期的 C 语言中是没有 []
这个标识的,大都用 *()
,即 arr[1]
等价于 *(arr+1)
,然而 *(arr+1)
也可以写作 *(1+arr)
,即可推出 *(1+arr)
等价于 1[arr]
;
所以我们可以凭借着上述表达来解释一个很重要的问题:为什么 arr
数组名代表首元素地址?
首元素地址为 &arr[0]
==> &*(arr+0)
==> arr+0
==> arr
;
所以当我们对一个数组元素进行初始化时,我们可以直接如下操作:
int arr[5] = {0};
int* p = arr;
示例如下:
#include<stdio.h>
void test() {
int arr[5] = { 0 };
int n = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
printf("请输入%d个整数,存入数组:\n", n);
for (int i = 0; i < n; i++) {
scanf_s("%d", p++);
}
p = arr;
printf("你的数组如下\n");
for (int i = 0; i < n; i++) {
printf("%d ", *p++);
}
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
arr 和 &arr 的区别
区别如下:
-
arr 是数组的首元素地址;
arr
是以指针变量所指向的数据类型长度为单位,即arr+1
所跳过的是第一个数组元素;数组名 arr 也是一个符号常量(类似于宏),它不能被赋值,自加和自减等;
-
&arr 是数组的首地址;
&arr
是以整个数组的数组长度为单位,即&arr+1
所跳过的是整个数组;
相同点如下
arr
和 &arr
所代表的地址值一样;
图解如下:
示例如下:
#include<stdio.h>
void test() {
int arr[5] = { 10,20 };
printf("%d 和 %d\n", arr, arr + 1);
printf("%d 和 %d\n", &arr, &arr + 1);
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
指向同一数组的两指针关系
指向同一数组的两个指针变量相减,返回的是相差元素的个数;
#include<stdio.h>
void test() {
int arr[5] = { 10,20,30,40,50 };
int* p1 = arr;
int* p2 = arr + 3;
int* p3 = p2 - p1;
printf("%d", p3);
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
指向同一数组的两个指针变量不能相加,原因是指针变量很大(电话号码),相加的话直接就越界了;
#include<stdio.h>
void test() {
int arr[5] = { 10,20,30,40,50 };
int* p1 = arr;
int* p2 = arr + 3;
int* p3 = p1 + p2;
printf("%d",p3);
}
int main(int argc, char* argv[]) {
test();
return;
}
报错效果如下:
指向同一数组的两个指针变量可以比较大小;
#include<stdio.h>
void test() {
int arr[5] = { 10,20,30,40,50 };
int* p1 = arr;
int* p2 = arr + 3;
if (p1 > p2) {
printf("P1>P2");
}
else {
printf("P2>=P1");
}
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
指向同一数组的两个指针变量可以互相赋值;
#include<stdio.h>
void test() {
int arr[5] = { 10,20,30,40,50 };
int* p1 = arr;
int* p2 = arr + 3;
p1 = p2;
printf("p1 = %u\np2 = %u", p1, p2);
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
在不越界的情况下 []
中的内容可以为负数;
引用以上所学 *() == []
;
#include<stdio.h>
void test() {
int arr[5] = { 10,20,30,40,50 };
int* p1 = arr;
int* p2 = arr + 3;
printf("%d", p2[-2]); //p2[-2] == *(p2 - 2)
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
题目案例
请回答以下打印结(*
和 ++
同优先级,运算时自右向左):
#include<stdio.h>
void test() {
int arr[5] = { 10,20,30,40,50 };
int* p = arr;
printf("%d\n",*p++);
// ++ 在右边时先使用,后加减,所以这里会先使用去取 * 然后赋值给 %d ,即 10 ,注意在同一个程序里面,p 已经移动了一次指针,即 *p = 20;
printf("%d\n",(*p)++);
// 小括号优先级别最高,++ 在右边时先使用,后加减,所以这里是先计算 %d == *p == 20 ,又注意在同一程序里,*p 已经 ++ 了一次,即此时 *p = 21;
printf("%d\n",*(p++));
// 小括号优先级别最高,++ 在右边时先使用,后加减,所以这里是先计算 %d == *p == 21 ,又注意在同一程序里,p 已经移动了一次指针,即 *p = 30;
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
8.5.指针数组
指针数组:本质是数组,只是每个元素是指针;
图解如下:
示例如下:
#include<stdio.h>
void test() {
int arr1[5] = {10,20,30,40,50};
int* arr2[5];
int i = 0;
for (i = 0; i < 5; i++) {
arr2[i] = &arr1[i];
}
for (i = 0; i < 5; i++) {
printf("%d ", *arr2[i]);
}
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
字符指针数组
**注意:**以下情况在不同的编译器中可能有不同,这是以 VS 为例,其他编译器需要视情况更改代码;
字符数组中是不可以包含多个字符串的:
但是在字符指针数组中,可以包含多个字符串;
此时指针存放的是每一个字符串的首元素地址,打印效果如下:
即:*ch[i]
就会遍历数组元素中每个数组的首元素;
注意,这里的首元素是 char 类型;
通过结合对数组单元名和字符串名的理解可知:字符串名(这里要将一个字符数组看成一个整体字符串)代表的是整个字符串的首元素地址,而这里的数组单元名也代表的整个字符串的首元素地址,所以我们可以通过遍历数组单元名来遍历一个字符串数组,诚然,二维字符数组和这种方式大同小异,但是这种方式能够更形象地解决 C 语言中没有 String数组 概念的问题;
示例如下:
#include<stdio.h>
void test() {
char* ch[3] = { "abc","def","gih" };
for (int i = 0; i < 3; i++) {
printf("%s ",ch[i]);
}
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
就如上例,如果我们只想输出 abc 中的 b ,二维数组可以通过更换深层遍历数值实现,而这里需要知道 b 的地址,由指针所学可知,通过在指针上做加运算就能实现;
#include<stdio.h>
void test() {
char* ch[3] = { "abc","def","gih" };
printf("%c ", *ch[0] + 1);
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
8.6.数组指针
数组指针:
本质就是指针变量,只是该变量保存的是数组的首地址,即指向数组的指针,因为它是指针,所以它的大小是 8B(64位系统);
基本格式举例:
int arr[5] = {10,20,30,40,50};
int (*p)[5] = arr;
必须加上小括号,因为 []
的优先级大于 *
;
变量数据类型: int* []
虽然指针变量 p 的大小是 8B (64位系统),但是此时的 p+1 所跳过的大小是整个数组(条件:数组指针变量和需要操作的数组变量数值个数相同,即中括号中的数值大小相同),p 的跳转大小和需要操作的数组整体大小没关系,仅和自己的数组变量个数和数据类型有关;
图解:
示例如下:
#include<stdio.h>
void test() {
int arr[5] = { 10,20,30,40,50 };
int(*p)[5] = NULL; //这里就证明跳转大小和需要操作的数组整体大小没关系
printf("%p\n", p);
printf("%p\n", p + 1);
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
若我们想用数组指针去遍历一个数组中的数,我们需要理解一下 int(*p)[5] 的含义,当我们已经用这个指针指向了一个长度为 5 的数组时,即 int(*p)[5] = arr; ,此时数组指针 (*p) 已经指向了数组的首元素地址,且指针变量宽度为 5 个 int 长度,沿着这种思路,我们将 int(*p)[5] = arr; 改成 int(*p)[1] = arr; 此时就表示指针 (*p) 已经指向了数组的首元素,且指针变量宽度为 1 个 int 长度,我们就可以移动该指针 (*p)+1 以便于达到单个遍历的效果,如果我们想要改变它的遍历起始位置,则需要改变被操作数组的起始值,即赋予我们指针一个非首元素的地址,但此时要注意 (*p)+1 依然是一个指针,所以要去取它的值的话,需要整体取 * ;
使用数组指针单个遍历示例如下:
#include<stdio.h>
void test() {
int arr[5] = { 10,20,30,40,50 };
int length = sizeof(arr) / sizeof(arr[0]);
int(*p)[1] = arr;
for (int i = 0; i < length; i++) {
printf("%d ", *(*p + i));
}
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
这样就可以把二维数组看成只有一行的一维数组:
细细转换一下可以发现:*(*p+1)
==> *(*p(0)+1)
==> *(p[0]+1)
==> p[0][1]
♥♥♥
数组指针应用
数组指针的应用多在于二维数组,接下来就将二维数组的性质和数组指针的性质相结合;
图解如下:
一定要注意,对行地址取完 * 之后依然是地址,只是变成了列地址;
案例:
对于一个数组,用普通变量和指针变量去查找;
#include<stdio.h>
void test() {
int arr[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
//查询 6 的位置;
printf("普通变量查找到:%d\n", arr[1][1]);
printf("指针变量查找到:%d\n", *(* (arr + 1) + 1));
//遍历第二行地址
int length = sizeof(arr[1]) / sizeof(arr[1][0]);
int i;
printf("普通变量遍历这个数组第二列是:");
for (i = 0; i < length; i++) {
printf("%d ", arr[1][i]);
}
printf("\n");
printf("指针变量遍历这个数组第二列是:");
for (i = 0; i < length; i++) {
printf("%d ", *(*(arr + 1) + i));
}
printf("\n");
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
常用的一些遍历变形如下:
*arr + 2
==> 第二列的列地址;
arr[1]
==> *(arr + 1) 第一行第零列的列地址;
&arr[0] + 2
==> &*(arr + 0) + 2 == arr + 2 第二行的行地址;
**arr
==> *(*(arr+0)+0) == arr[0][0] 第一行第一列;
我们再用指针完完整整写一个二维数组的遍历:
#include<stdio.h>
void test() {
int arr[3][4];
int num = sizeof(arr) / sizeof(arr[0]);
int length = sizeof(arr[1]) / sizeof(arr[1][0]);
int i,j;
for (i = 0; i < num; i++) {
for (j = 0; j < length; j++) {
scanf_s("%d", &*(* (arr + i)+j));
}
}
//查询 6 的位置;
printf("普通变量查找到:%d\n", arr[1][1]);
printf("指针变量查找到:%d\n", *(* (arr + 1) + 1));
//遍历第二行地址
printf("普通变量遍历这个数组第二列是:");
for (i = 0; i < length; i++) {
printf("%d ", arr[1][i]);
}
printf("\n");
printf("指针变量遍历这个数组第二列是:");
for (i = 0; i < length; i++) {
printf("%d ", *(*(arr + 1) + i));
}
printf("\n");
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
任何维度的数组在物理存储上都是一维的,但是为了便于人们分析,将一维数组逻辑化为了多维,而指针就还原了物理存储上的一维,即用 arr+数字 跨越每一行的长度并指向终点行地址,用 *(arr+数字)+数字 跨越每一行中每一列的长度并指向具体列地址;
所以我们可以把二维数组用一维循环去遍历:
#include<stdio.h>
void test() {
int arr[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
int length1 = sizeof(arr) / sizeof(arr[0]);
int length2 = sizeof(arr[0]) / sizeof(arr[0][0]);
int i;
for (i = 0; i < length1 * length2; i++) {
printf("%d ", *((*arr )+i));
}
printf("\n");
for (i = 0; i < length1 * length2; i++) {
printf("%d ", *(*arr + i));
}
printf("\n");
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
根据上例思考一下,为什么这两种写法顺序不同,但是能达到一样的效果?此时我们应该想一想 i 的宽度,因为此时遍历的是一组一维的 int 类型数据,这是底层原有的模样,所以 i 的宽度应该是 4B,所以只要出现 +1 ,就代表指针往后跳跃了 4 个单位,所以就会忽略二维结构规则,进而原始输出;
8.7.多级指针
图解如下:
1 级指针变量保存 0 级指针变量(普通变量)的地址;
……
1+n 级指针变量保存 n 级指针变量(普通变量)的地址;
反取数值时,几级指针变量上取几个 * ;
示例如下:
#include<stdio.h>
void test() {
int n = 10;
int* p1 = &n;
printf("%d\n", *p1);
int** p2 = &p1;
printf("%d\n", **p2);
int*** p3 = &p2;
printf("%d\n", ***p3);
}
int main(int argc, char* argv[]) {
test();
return;
}
打印效果如下:
8.8.指针作为函数的参数
如果需要在函数内部修改外部变量的值,就需要将外部变量的地址传递给函数(以指针作为函数参数);
先举一个例子:
我们用空值函数对调两个 int 型整数;
错误示例:
#include<stdio.h>
void method(int a,int b) {
int temp = a;
a = b;
b = temp;
}
int main(int argc, char* argv[]) {
int a = 1;
int b = 2;
method(a,b);
printf("a = %d;b = %d\n", a, b);
return;
}
打印效果如下:
由于实参传送数值,形参会在拿到数值后在内存空间中创建属于自己的地址,所以在函数中所做出的所有改变都不是对实参做出的操作,而是对形参,所以我们不应该传送数值,而应该传输地址;
正确示例:
#include<stdio.h>
void method(int *a,int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main(int argc, char* argv[]) {
int a = 1;
int b = 2;
method(&a,&b);
printf("a = %d;b = %d\n", a, b);
return;
}
打印效果如下:
再举一个例子:
在函数内部给空指针 p 赋值;
#include<stdio.h>
//因为在main中传入的*p本就是一个一级*,所以要用二级*去接收它
void test(int**p) {
static int num = 10;
*p = #
}
int main(int argc, char* argv[]) {
int* p = NULL;
test(&p);
printf("*p = %d\n", *p);
return;
}
打印效果如下:
再次总结
如果想在函数内部修改外部变量的值,就需要将外部变量的地址传递给函数,以指针变量作为形参,而且如果想要传入地址,那么必须让形参指针级别比实参指针级别高一级别,比如要传入 int num = 10; 那么实参传入的就是 int 型,所以要用 int* 型去接收,再比如要传入 int* num = NULL ,那么实参传入的就是 int* 型,所以要用 int** 型去接收,依次类推;
但在结构体中,该方法会失效;
所以我们应该理清楚传参的特点,我们应当注意,基本类型变量名不能代表地址,即使该变量名前面有 * 修饰也不行,所以当我们传入基本类型变量名时,都应当加上 & 取地址符号,所以在函数内要用更高一级 * 作为函数的形参;但是对于引用类型变量名来说,它们的就可以代表地址,所以传参时不用加上 * 取地址符号,所以在函数内不需要用更高一级 * 作为函数的形参;
8.9.一维数组名作为函数参数
如果函数内部想操作(读和写)外部数组的元素,请将外部数组的数组名传入函数;
一维数组,以 int arr[5] 为例,传入函数后会被自动优化成 int arr* ,所以以后要将一维数组传入函数时,务必使用指针形式传入;
示例如下:
#include<stdio.h>
void test(int*arr,int n) {
for (int i = 0; i < n; i++) {
scanf_s("%d", &*(arr + i));
}
}
int main(int argc, char* argv[]) {
int arr[5];
int n = sizeof(arr) / sizeof(arr[0]);
test(arr,n);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
}
打印效果如下:
8.10.二维数组名作为函数参数
二维数组名作为函数参数和一维数组大同小异;
无论是几维数组,数组名作为函数的形参时,都会被优化成数组指针,且只有话离数组名最近的那个遍历位;
int arr[3] ==> int* p;
int arr[3][4] ==> int* p[4];
int arr[3][4][5] ==> int* p[4][5];
int arr[3][4][5][6] ==> int* p[4][5][6];
二维数组名最为函数参数示例如下:
#include<stdio.h>
void test(int*arr,int hang_length,int lie_length) {
printf("请输入你的数组:");
for (int i = 0; i < hang_length * lie_length; i++) {
scanf_s("%d", &*(arr + i));
}
}
int main(int argc, char* argv[]) {
int arr[3][4];
int hang_length = sizeof(arr) / sizeof(arr[0]);
int lie_length = sizeof(arr[0]) / sizeof(arr[0][0]);
test(arr,hang_length,lie_length);
for (int i = 0; i < hang_length; i++) {
printf("第%d列", i + 1);
for (int j = 0; j < lie_length; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
return;
}
打印效果如下:
8.11.指针作为函数的返回值
函数不要返回普通局部变量的地址,因为普通局部变量的生命周期是整个函数,函数一结束,该变量就会被回收,届时返回的那个地址再次使用时就会出现很严重的错误,解决办法就是将该变量定义成静态变量或者是全局变量;
所以我们可以用函数的返回值来更改函数外部的变量值:
#include<stdio.h>
int* test() {
static int num = 10;
return #
}
int main(int argc, char* argv[]) {
int* p = NULL;
p = test();
printf("%d\n", *p);
return;
}
打印效果如下:
相比于将指针作为参数去修改的方式,返回值的方式更值得推荐;
8.12.函数指针
函数名代表的是函数的入口地址;
**定义:**函数指针本质是指针,保存的是函数的入口地址;
所以函数名和函数指针就是大名和小名的关系;
基本格式:
为了防止指针变量和小括号结合,所以得用小括号将 * 和指针变量括起来;
指针变量所指向的函数是什么样的参数数据类型,小括号里就应该是什么样的参数数据类型;
指针变量所指向的函数必须有返回值,而且该返回值和指针变量所指向的数据类型一样;
普通函数变量调用和指针函数变量调用的方式一模一样;
int test(int a,int b){}
int(*p)(int, int) = NULL;
示例如下:
#include<stdio.h>
int test(int a,int b) {
return a + b;
}
int main(int argc, char* argv[]) {
int(*p)(int, int) = NULL;
p = test;
printf("&test = %u\n", test);
printf("&p = %u\n", p);
printf("test() = %d\n", test(10, 20));
printf("p() = %d\n", p(10, 20));
return;
}
打印效果如下:
函数指针参数
函数指针作为函数参数示例如下:
用函数指针去创建一个能够加减乘的整数计算器程序;
#include<stdio.h>
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int my_calc(int a, int b, int (*p)(int, int)) {
return p(a, b);
}
int main(int argc, char* argv[]) {
int a, b;
printf("请输入两个要操作地数:\n");
scanf_s("%d %d", &a, &b);
printf("加法计算结果:%d\n", my_calc(a, b, add));
printf("减法计算结果:%d\n", my_calc(a, b, sub));
printf("乘法计算结果:%d\n", my_calc(a, b, mul));
return;
}
打印效果如下: