首页 > 其他分享 >深入理解指针(C语言)

深入理解指针(C语言)

时间:2024-11-25 23:32:47浏览次数:9  
标签:arr 指向 int C语言 深入 数组 printf 指针

本文目录

引言

指针作为C语言语法学习中的一块既极重要又极难懂的知识点,让初学C语言的我们常常苦不堪言。而本文就是为了让像曾经的作者一样的宝子们深刻理解指针这一章节的内容而作,那接下来就跟随作者的视角,一起把指针理解透彻!
温馨提醒:本章内容并不涉及动态内存管理,链表,树等数据结构的实现的相关内容!

在这里插入图片描述

概要

由于之前我们已经了解了指针的内涵及产生,所以本文的讲解内容有:指针的类型,指针的步长,指针的解引用,指针的运算以及指针的应用(数组处理,函数传参,字符串处理等)

在这里插入图片描述

正文

一 指针的类型

指针的类型繁复,为了避免出现知识点的遗漏,这里作者采用一种自认为不错的分类方法——根据指向的内容进行分类,把指针分为了以下几种:

(1)内置数据类型指针

顾名思义,这些指针指向内置类型的变量,如整型指针,字符指针等。

类型内涵及应用
整型指针int*指向整数的指针
浮点型指针float*double*指向浮点数的指针
字符(型)指针char*指向字符的指针,常用于字符串处理
布尔型指针bool*指向布尔值的指针

拓展:bool型出现在C99及以后的C++标准中,用于存储真(true)或假(false)的数据类型。

(2) 数组指针与指向数组的指针

  • 数组指针:一个指向整个数组的指针。例如, int ( * arrPtr)[10] 是一个指向包含10个整数的数组的指针。
  • 指向数组元素的指针:这种指针通常用于遍历数组。例如,cint * ptr = arr; 其中 arr 是一个整数数组, ptr 可以用来遍历这个数组。

(3)函数指针

函数指针是指向函数的指针,允许通过指针调用函数。

例如:

cint (*funcPtr)(int, int); // 一个指向返回整型并接受两个整型参数的函数的指针

(4) 结构体指针与联合体指针

  • 结构体指针(struct) * :指向结构体的指针。结构体是一种用户自定义的数据类型,可以包含多个不同类型的成员。

例:

#include<stdio.h>
struct book {
	char name[20];
	char author[20];
	int prince;
};
void print(struct book* p) {
	printf("%s %s %d\n", (*p).name, (*p).author, (*p).prince);
	printf("%s %s %d\n", p->name, p->author, p->prince);    //“->”操作符可用在:结构体指针指向我们想要访问的结构体中的元素;
}

int main() {
	struct book b1 = {"C语言", "张三", 10};
	printf("%s %s %d\n", b1.name, b1.author, b1.prince);     //“.”操作符可用在:找到我们想要访问的结构体的元素。
	print(&b1);
	return 0;
}

struct book* p就是一个结构体变量

  • 联合体指针(union)*:指向联合体的指针。联合体也是一种用户定义的类型,但其所有成员共享同一块内存空间

(5)空指针(void*)

空指针是一种特殊的指针类型,不指定具体的指向类型。它可以指向任何类型的数据,但使用前通常需要转换为适当的类型。
例如:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//integer_qsort函数与一次排序的if判断语句共同用来控制整型数组排序的方向
int integer_sort(void* e1, void* e2) {
	int* a1 = (int*)e1;
	int* a2 = (int*)e2;
	return *a1 - *a2;
}
//Invert函数用来交换两个元素(用循环和char*类型的指针一个一个字节地对c1和c2变量的值进行交换)
void Invert(char* c1, char* c2, int width) {
	int i = 0;
	for (; i < width; i++) {
		char p = *c1;
		*c1 = *c2;
		*c2 = p;
		c1++;
		c2++;
	}
}
//冒泡排序的qsort函数实现各种数组的排序
void Bubble_qsort(void* base, int sz, int width, int(*p)(void*, void*)) {
	int i = 0;
	char* b1 = (char*)base;
	for (; i < sz; i++) {
		//控制排序的次数
		int j = 0;
		int ischange = 0;
		for (; j < sz - i - 1; j++) {
			//一次的排序
			if (p(b1 + j * width, b1 + (j + 1) * width) > 0) {
				Invert(b1 + j * width, b1 + (j + 1) * width, width);
				ischange = 1;
				//说明数组还是无序
			}
		}
		// 如果本次冒泡中,元素没有交换,则本次开始冒泡时,数据已经有序了,后面的冒泡可以不用进行了
		if (!ischange){
			break;
	}
	}
}
//test1函数用来展示我们自己设计的qsort函数对一个整型数组的排序
test1() {
	int arr[10] = { 2, 1, 3, 4, 5, 6, 7, 8, 9, 10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int width = sizeof(arr[0]);
	Bubble_qsort(arr, sz, width, integer_sort);
	int i = 0;
	for (; i < sz; i++) {
		printf("%d ", arr[i]);
	}
}
int main() {
	test1();
	return 0;
}

因为qsort函数是要实现对各种数组的排序,所以不知道要传给函数Bubble_qsort和函数integer_sort的是什么类型的数据的指针,函数Bubble_qsort和函数integer_sort的形参就用了void *,使得不管什么主函数传的是什么类型的指针函数都能接收;但是在进行解引用时,要把void *转换为已知的一般数据类型指针进行使用。例如,int* a1 = (int*)e1; int* a2 = (int*)e2;

(6)指针的指针

这是一个指向指针的指针,即双重间接引用
例如:
int ** pptr;pptr 是一个指向 int * 类型指针的指针。
int(**p)]10];p是一个指向数组指针的指针,但其实没什么意义。

(7)常量指针与指向常量的指针

  • 常量指针(const pointer):指针本身是常量,不能改变其指向的地址,但可以修改指向的内容(如果内容不是常量)。例如, int * const ptr = &var;
  • 指向常量的指针(pointer to const):指针指向的内容是常量,不能通过该指针修改其内容,但可以改变指针指向的地址。例如, const int *ptr = &var;
  • 指向常量的常量指针:既不能改变指针指向的地址,也不能通过该指针修改指向的内容。例如,const int * const ptr = &var;

趣味解释:

//const的用处及用法(趣味讲解):
#include<stdio.h>
int main() {
	int n = 1000;
	int num = 100; //男孩num有100元
	//int* p = &num;//女孩p看上了男孩num,所以他们就确定了男女朋友关系,并且男孩num把他的100元钱给了女孩保管
	//*p -= 10;   //有一天女孩想要要吃一份100元的套餐,但是女孩并没有经过和男孩的讨论就买了这份套餐
	//男孩发现了之后刚想生气,但是压下怒气思考之后对女孩提了一个要求,就是女孩不准再和其他男的当男女朋友
	//int* const p = &num;
	// p = & n;                 //语法错误  //如果同意了这个要求,女生就没有办法和别的男的(例如男生n)建立男女关系
	//* p -= 任意数字;          //ok        //这个条件下女孩p就可以支配男孩的钱财
	//但是女孩并没有同意这个要求,她只是拿男孩当提款机
	const int* p = &num;
	p = &n;                   //ok          //所以女孩可以和别的男的建立关系
	// * p -=任意数字;       //语法错误     //但是女孩不能再支配男孩的钱,她只能用到男孩的交给她保管的100元,以后就用不了了
	printf("%p\n", p);
	printf("%p\n", &n);
	printf("%p\n", &num);
	return 0;
}

二 指针的步长

  • 1.指针步长的定义
    基本概念: 指针的步长是指针变量在进行算术运算(如+1)时,地址值变化的量。这个变化量取决于指针所指向的数据类型的宽度。
    与数据类型的关系:指针步长取决于指针指向的数据类型的大小(char * 的指针指向的数据类型为char,char类型的数据大小为1字节,所以char * 指针的步长位1字节)。

  • 2.指针步长的计算
    基本规则:由上可知,只要知道指向的数据类型的大小就可以得到指针的步长,所以——指针步长=sizeof(指针所指向的类型)。

示例:

#include<stdio.h>
int main(){
	int a = 0;
	char b = 0;
	double c = 0;
	int* pa = &a;
	char* pb = &b;
	double* pc = &c;
	printf("pa指针的步长为%zd\n", sizeof(a));
	printf("pb指针的步长为%zd\n", sizeof(b));
	printf("pc指针的步长为%zd\n", sizeof(c));
	return 0;
}

运行结果为:

在这里插入图片描述

  • 3.指针步长的应用
    数组遍历等,后面会涉及

三 指针的解引用

指针的解引用是指通过指针访问或修改指针指向的内存内容的操作。以下是对指针解引用的详细解释:

1.定义与概念

  • 定义解引用是指使用一个指针来访问它所指向的内存位置中存储的值
  • 概念在编程中,指针是一个变量,它存储的是另一个变量的内存地址。而解引用则是通过这个指针来获取或修改它所指向的内存位置中的值。

2.语法与用法

  • 语法在C语言等编程语言中,解引用通常使用星号(*)作为操作符。例如, * p 表示解引用指针 p,即获取 p 所指向的内存位置中的值。

  • 用法

    1. 获取值通过解引用可以获取指针所指向的变量的值。例如,如果有一个整型变量 a,并且有一个指向 a 的指针 p,那么可以通过 * p 来获取 a 的值。
    2. 修改值同样地,通过解引用也可以修改指针所指向的变量的值。例如,可以使用 * p = 新值; 来将 a 的值修改为“新值”。

示例说明:

假设有以下代码段:

int a = 10; // 定义一个整型变量 a 并初始化为 10
int *p = &a; // 定义一个指向整型的指针 p,并将 a 的地址赋给 p

此时,指针 p 存储了变量 a 的内存地址。接下来,我们可以通过解引用来获取或修改 a 的值:

int value = *p; // 解引用 p,获取 a 的值,并将其赋给变量 value
*p = 20; // 解引用 p,并将 a 的值修改为 20

在上述代码中,第一行通过解引用 p 获取了 a 的值,并将其赋给了变量 value。第二行则通过解引用 p 修改了 a 的值为 20。

3.解引用权限

对指针变量的解引用权限大小主要取决于指针的类型。在C语言中,不同类型的指针具有不同的解引用权限,这决定了它们能够访问或修改多少字节的内存数据。

具体来说:

  • 指针类型定义了指针解引用时所能操作的数据类型和大小。例如,char*类型的指针解引用时只能访问一个字节的内存(因为char类型通常占用一个字节),而int*类型的指针解引用时能访问四个字节的内存(假设int类型占用四个字节)。
  • 指针的解引用权限还与其所指向的内存区域有关。如果指针指向了一个合法的、已分配的内存地址,并且该地址上的数据类型与指针类型相匹配,那么指针就可以正常地解引用并访问或修改该内存区域的数据。
  • 需要注意的是,如果指针指向了非法的内存地址(如未初始化的指针、已被释放的内存等),或者指针类型与被指向的内存区域的数据类型不匹配,那么进行解引用操作可能会导致程序崩溃或产生不可预料的结果。

因此,在使用指针时,我们需要确保指针已经正确初始化,并且其类型与被指向的内存区域的数据类型相匹配,以避免潜在的错误和安全问题。

4.注意事项

  • 解引用操作只能用于指针类型的变量,不能指向其他类型的变量
  • 在使用解引用操作符之前,应确保指针指向有效的对象或内存位置,以避免未定义行为或程序崩溃等问题。

四 指针运算

指针运算是在信息工程中,对用来指示一个内存地址的计算机语言的变量或中央处理器(CPU)中寄存器进行的操作。以下是对指针运算的详细解析:

(1)常见的指针运算

  1. 取址运算使用运算符“&”获取指定变量在内存中的地址。例如,int num = 5; int *ptr; ptr = &num;,这里ptr就是一个存储了num变量地址的指针变量。

  2. 取值运算使用运算符“*”根据一个给定的内存地址取出该地址中存储的变量的值。例如,int num = 20; int *ptr = &num; printf("%d", *ptr);,输出结果为20,即ptr所指向的地址中存储的值。

  3. 加减运算指针与整数可以进行加减运算,其作用是将指针向后或向前移动整数个步长(步长为指针类型所占用的字节数),指向新的地方。例如,int arr[5] = {1, 2, 3, 4, 5}; int *ptr = arr; ptr = ptr + 2;,此时ptr指向数组的第三个元素。需要注意的是,只有当两个指针指向同一个数组中的元素时,相减才有意义,结果表示它们之间的元素个数;而指针之间不能进行相加运算,因为两个地址相加没有实际意义

  4. 自增自减运算指针的自增自减运算相当于与整数1相加减,即指针向后或向前移动一个步长。只是自增和自减运算符在指针前与指针后运算先后顺序不同。如果自增或自减运算符在指针前,则先进行指针自增再让指针参与其他运算;如果自增或自减运算符在指针后面,则指针先参与其他运算再进行自增或自减。

  5. 比较运算可以对指向同一数组的两个指针进行比较,以确定它们在数组中的相对位置。指向前面元素的指针变量小于指向后面元素的指针变量(数组的内容存放习惯是从低地址到高地址)。

(2)指针运算的注意事项

  1. 越界访问:进行指针算术运算时要确保不会超出所指向的内存区域,否则会导致未定义的行为,甚至引发程序崩溃。
  2. 空指针:在使用指针之前,要确保它不是一个空指针(NULL),否则可能导致程序崩溃。可以通过函数assert()判断指针是否为NULL来避免这种情况的发生。

例如:

#include<stdio.h>
#include<assert.h>
size_t my_strlen(const char* p) {
	char* p1 = p;
	assert(p != NULL);
	while (*p++ != '\0');
	return (p - p1 - 1);
}
int main() {
	char arr[] = "abcdef";
	printf("%zd\n", my_strlen(arr));
	return 0;
}

这里我们用函数assert()对字符数组的首元素地址 arr进行了断言判断,可以防止字符数组中全为’\0’导致程序崩溃的情况。

  1. 类型匹配指针的类型必须与所指向的数据类型匹配,否则可能导致数据解释错误或程序崩溃。在进行指针运算时,要注意保持指针类型的正确性。

指针的应用

(1)数组处理

(2)字符串处理

(3)函数传参

指针与数组有着非常密切的关系,由此产生了非常多的笔试题题目,但讨论数组处理也会涉及函数传参字符串处理。所以我们没有必要生生分开他们之间的联系而去单独讨论,以十个笔试题和一个回调函数的实现来见识指针在C语言诸多运用。

题1:
判断代码运行结果

#include<stdio.h>
#include<string.h>
int main() {
	char  arr[] = "abcd";
	//char arr[] = {'a', 'b', 'c', 'd', '\0'}
	printf("%d\n", sizeof(arr)); 
	printf("%d\n", sizeof(arr + 0 ));  
	printf("%d\n", sizeof (*arr));   
	printf("%d\n", sizeof(arr[1]));   
	printf("%d\n", sizeof(&arr));    
	printf("%d\n", sizeof(&arr + 1));   
	printf("%d\n", sizeof(&arr[0] + 1));   

	printf("%d\n", strlen(arr));   
	printf("%d\n", strlen(arr + 0));    
	printf("%d", strlen (*arr));     
    printf("%d", strlen(arr[1]));     
	printf("%d\n", strlen (&arr));     
	printf("%d\n", strlen (&arr + 1));    
	printf("%d\n", strlen (&arr[0] + 1));    
	
	return 0;
}

建议思考写下自己的答案再看后文进行核对与理解

答案解析
5sizeof()内仅有arr(数组名), arr代表整个数组,所以sizeof计算的是整个数组的大小——5(单位:字节)
4/8除了仅有arr&arrarr代表的是整个数组,其他arr代表的都是数组首元素的地址,地址的大小就是4/8(至于4/8取决于32位机器或64位机器)
1*arr就是数组首元素'a'(int),大小为1
1arr[1] == *(arr + 1),就是数组的第二个元素,大小为1
4/8&arr中的arr代表的是整个数组,所以&arr代表的是整个数组的地址,大小就是4/8
4/8&arr + 1中的arr代表的是整个数组,所以&arr + 1代表的是整个数组后的和数组一样大小的连续元素的地址,大小就是4/8
4/8第二个元素的地址
4strlen(arr)arr代表的是数组首元素的地址,所以strlen()从数组首元素开始数到至’\0’,结果就为:4
4strlen(arr+0)arr代表的是数组首元素的地址,所以strlen()从数组首元素开始数至’\0’,结果就为:4
非法访问*arr表示数组首元素,而strlen()要的是地址,所以非法访问
非法访问同上
4&arr取出了整个数组的地址,就数整个数组,结果就为:4
随机&arr + 1 就跳过了整个数组,不知道什么时候出现’\0’,也不知道任何元素的信息,所以打印的是个随机数
3&arr[0] + 1 就跳过了首元素,结果就为:3

考察:一维数组的数组名——特殊与一般
涉及:一维数组,strlen(),sizeof()

题2:
判断代码运行结果

#include<stdio.h>
#include<string.h>
int main() {
	int arr[2][3] = { 0 };
	printf("%d\n", sizeof(arr)); //sizeof()内仅有arr(数组名),arr代表整个数组,所以sizeof计算的是整个数组的大小——6 * 4 = 24(单位:字节)
	printf("%d\n", sizeof(arr + 0 ));   //除了仅有arr或&arr中arr代表的是整个数组,其他arr代表的都是数组首元素的地址(对于二维数组来说,数组首元素就是数组第一行的地址),地址的大小就是4/8
	printf("%d\n", sizeof (*arr));   //*arr中的arr代表的是第一行元素的地址,所以*arr就是数组第一行元素{0, 0, 0},大小为3 * 4 = 12
	printf("%d\n", sizeof(arr[1]));   //arr[1] == *(arr + 1),就是数组的第二行元素,大小为12
	printf("%d\n", sizeof(&arr));    //&arr中的arr代表的是整个数组,所以&arr代表的是整个数组的地址,大小就是4/8
	printf("%d\n", sizeof(&arr + 1));   //&arr + 1中的arr代表的是整个数组,所以&arr + 1代表的是整个数组后的和数组一样大小的连续元素的地址,大小就是4/8
	printf("%d\n", sizeof(&arr[0] + 1));   //4/8
	printf("%d\n", sizeof(arr[3]));     //注意:这里并不会造成非法访问,因为早在编译的阶段,sizeof就已将确定了arr[3]的类型是一个地址大小就是4/8

	return 0;
}

考察:二维数组数组名一般情况下意义上等于数组第一行的地址,数值上等于第一个元素的地址
涉及:sizeof(),二维数组

题3:
判断代码运行结果

#include<stdio.h>
#include<string.h>
int main() {
	char* p = "abcd";
	//这个代码的意思:把首元素的地址(a的地址)放到指针变量p中
	//p就相当于一般的arr(除了两种特殊情况除外)(首元素的地址)
	printf("%d\n", sizeof(p)); //地址的大小就是4/8(字节)(至于4/8取决于32位机器或64位机器)
	printf("%d\n", sizeof(p + 0 ));   //同上
	printf("%d\n", sizeof (*p));   //*p就是数组首元素'a'(char),大小为1(字节)
	printf("%d\n", sizeof(p[1]));   //p[1] == *(p + 1),就是数组的第二个元素,大小为1(字节)
	printf("%d\n", sizeof(&p));    //&p中的p代表的是数组首元素的地址,所以&p代表的是存储指针变量p的地址,大小就是4/8
	printf("%d\n", sizeof(&p + 1));   //&p + 1中的p代表的是数组首元素,所以&p + 1代表的是存储指针变量p的地址处后一位的地址,大小就是4/8
	printf("%d\n", sizeof(&p[0] + 1));   //b的地址,大小就是4/8

	printf("%d\n", strlen(p));   //strlen(p)中p代表的是数组首元素的地址,所以strlen函数从数组首元素开始数至'\0',结果就为:4
	printf("%d\n", strlen(p + 0));    //strlen(p + 0)中p代表的是数组首元素的地址,所以strlen函数从数组首元素开始数至'\0',结果就为:4
	//printf("%d", strlen (*p));     //*p表示数组首元素,而strlen函数要的是地址,所以非法访问
	//printf("%d", strlen(p[1]));     //同上
	printf("%d\n", strlen (&p));     //&p取出了数组首元素的地址的地址,不知道什么时候出现'\0',也不知道任何元素的信息,所以打印的是个随机数
	printf("%d\n", strlen (&p + 1));    //&p + 1 就跳过了整个数组,不知道什么时候出现'\0',也不知道任何元素的信息,所以打印的是个随机数
	printf("%d\n", strlen (&p[0] + 1));    //&p[0] + 1 就跳过了首元素,结果就为:3

	return 0;
}

考察:字符指针——把首元素的地址(a的地址)放到指针变量p中
涉及:字符指针,strlen()

题4:
判断代码运行结果


#include<stdio.h>
struct test {
	int Num;
	char* pcname;
	short sDate;
	char cha[2];
	short sBa[4];
}* p;
//假设 * p = 0x00000000;
//已知结构体变量test的大小为20字节;
int main() {
	printf("%p\n", p + 1);
	//指针加1跳过整个指针权限的内容(步长)(解引用权限),所以这里跳过了整个结构体变量,也就是20个字节,结果用十六进制表示就是00000014
	printf("%u\n", (unsigned long)p + 1);
	//先把结构体指针变量p强转成无符号长整型变量,再加1就是让一个整型变量加1,结果就是加1,用十进制表示就是1
	printf("%p\n", (unsigned int*)p + 1);
	//先把结构体指针变量p强转成整型指针变量,再加1就是让一个整型指针变量加1,指针加1跳过整个权限的内容,所以这里跳过了整个整型变量,也就是4个字节,结果用十六进制表示就是00000004
}

考察:指针加1的意义,是跳过一个步长的地址
涉及:结构体指针,基本指针

题5:
判断代码运行结果

//x86,小端
#include<stdio.h>
   int main() {
	int a[4] = { 1, 2, 3, 4 };
	int* p = (int*)(&a + 1);
	int* p1 = (int*)((int)(a + 1) + 1);   //如果我们定义一个:int* pa = a;   则a + 1就等价于*(pa + 1)
	printf("%x\n", p[-1]);  
	//1.p[-1] == *(p - 1),因为p的类型为int*,所以p-1就是向前挪动一个整型(4个字节)的长度,指向了第4个元素的第一个字节的最左端
	//2.再进行解引用,根据p的访问权限可知从当前位置向后访问一个整型(4个字节),就得到了数组的第4个元素——4
	//3.又因为%x是用来打印十六位进制数且会去掉前面的0,所以打印就是4
	printf("%x\n", * p1); 
	//1.a + 1: 
	             //低地址                                       高地址//低地址                                       高地址                                                         
	             //01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00    //01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00
	   //地址设为:00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f    //00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f    
	   //指针位置:pa                                              -->              pa    
	//2.(int)(a + 1): 把 地址04 强转成一个大小等于 04 的整型数据
	//3.(int)(a + 1) + 1:一个整型数据加1,就是数值加1,此时((int)(a + 1) + 1)就得到了一个数值大小为 05 的整型数据
	//4.((int*)((int)(a + 1) + 1)):把数值大小为 05 的整型数据强转为 地址05
	//5.int* p1 = (int*)((int)(a + 1) + 1):把上面得到的 05的地址 赋给 类型为int*的 整形指针变量p1
	//6.printf("%x\n", * p1):再进行解引用,根据p的访问权限可知从当前位置向后访问一个整型(4个字节),就得到了00 00 00 03(小端存储),转换为03 00 00  00(原值)(小端存储是以字节为单位的)
	//7.又因为%x是用来打印十六位进制数且会去掉前面的0,所以打印就是3000000
	return 0;
}

考察并涉及:小端存储,%x的作用,指针运算

题6:
判断代码运行结果

#include<stdio.h>
int main() {
	int arr[] = { 1, 2, 3, 4 };
	int* p = (int*)(&arr + 1);
	printf("%d\n", *(p - 1));
	//没什么好讲的
	printf("%d\n", *(arr + 1));
	//同上
	return 0;
}

考察并涉及:一维数组数组名

题7:
判断代码运行结果

#include<stdio.h>
int main() {
	int arr[2][3] = { (1, 2), (3, 4), (5, 6)};
	//逗号表达式的值就是','右边的表达式的结果(但要注意:','左边的表达式也会执行,且是先执行的)
	//所以该二维数组的元素为://2 4
	                          //6 0
	                          //0 0
	int* p;
	p = arr[0];
	printf("%d\n", p[1]);
	//两种理解p[1]的方法
	//1.用数组的格式理解:p = arr[0],则p[1] = arr[0][1] = 4;
	//2.用数组的本质理解:arr[0]作为二维数组的第一行的数组元素的数组名,代表的是数组第一行的首元素的地址,即——&arr[0][0],
	                    //p[1] == *(p + 1) == *(&arr[0][0] + 1) == 4
	return 0;
}

考查并涉及:指针与二维数组的关系和逗号表达式的作用

题8:
判断代码运行结果

#include<stdio.h>
int main() {
	int arr[5][5];
	//数组元素:|0 0 0 0 0 | 0 0 0 0 0 | 0 0 0 0 0 | 0 0 0 0 0 | 0 0 0 0 0 | 
	//             arr[0]      arr[1]     arr[2]      arr[3]      arr[4]
	//地址设为:|0 1 2 3 4 | 5 6 7 8 9 | a b c d e | f 0 1 2 3 | 4 5 6 7 8 |
	int(*p)[4] = (int(*)[4])arr;
	//arr作为二维数组的数组名,类型为:int(*)[5],与数组指针变量p(类型为:int(*)[4])类型基本一致,所以强转之后可以和数组名一样理解
	printf("%p,%d\n", &p[4][2] - &arr[4][2], &p[4][2] - &arr[4][2]);
	//以p为二维数组的数组名的二维数组:
	//    元素:| 0 0 0 0 | 0 0 0 0 | 0 0 0 0 | 0 0 0 0 | 0 0 0 0 | 0 0 0 0 | 0 
	//             p[0]      p[1]      p[2]      p[3]       p[4]     p[5]     p[6]
	//地址设为:| 0 1 2 3 | 4 5 6 7 | 8 9 a b | c d e f | 0 1 2 3 | 4 5 6 7 | 8
	//所以 &p[4][2] - &arr[4][2] == 12 - 16 == -4,再根据占位符的功能打印的结果为:FFFFFFFC(十六进制),-4(十进制)
	return 0;
}

考察:%p打印的是无符号十六进制的地址值
涉及:二维数组,占位符%p,数组指针。

题9:
判断代码运行结果

#include<stdio.h>
int main() {
	char* arr[3] = { "hello", "world", "bite" };
	//arr[0] == &'h' , arr[1] == &'w', arr[2] == &'b'
	//即数组元素:&'h'| &'w' | &'b'
	//元素名:  arr[0]|arr[1]|arr[2]

	char** pa = arr;
	pa++;
	printf("%s\n", *pa);
	return 0;
}

考查:字符指针在接收字符串时,接受的是字符串的首元素的地址
涉及:指针数组,占位符%s。

题10:
判断代码运行结果

#include<stdio.h>
int main(){
	char* c[4] = { "hello", "world", "bite", "pengge" };
	//一级字符指针数组c:
	//       数组元素值:| &'h'| &'w'| &'b'| &'p'|
	//         元素类型:|char*|char*|char*|char*|
	//         元素意义:|指向"hello"的指针(一开始指向'h')|指向"world"的指针(一开始指向'w')|指向"bite"的指针(一开始指向'b')|指向"pengge"的指针(一开始指向'p')
	//           元素名:| c[0]| c[1]| c[2]| c[3]|
	//         地址设为:   0      1    2     3
	char** cp[4] = {c + 3, c + 2, c + 1, c};
	//二级字符指针数组cp:
	//         数组元素:|  3  |  2  |  1  |  0  |
	//           元素名:|cp[0]|cp[1]|cp[2]|cp[3]|
	//         地址设为:   a     b    c     d
	char*** cpp = cp;
	//    *cpp == &cp[0] == a
	printf("%s\n", **++cpp);
	//都只能先从cpp开始分析:
	//1.++cpp:cpp先自增1——*cpp = a + 1 == b;
	//2.**(++cpp):两次解引用——(1)*(++cpp) == b --> (2)**(++cpp) == 2;
	//3.%s的作用:从所给地址开始,一直打印字符至'\0'处,所以打印的结果为:bite
	printf("%s\n", *-- *++cpp + 3);
	//1.++cpp:cpp再自增1——*cpp = b + 1 == c;
	//2.*++cpp:解引用——*cpp == c;
	//3.-- *++cpp:c自减1——*c = 1 - 1 ==0;
	//4.*-- *++cpp:解引用——*c == 0;
	//5.*-- *++cpp + 3:一级字符指针 + 3——*0 = 'h' + 3 == 'l'(临时,不是真的加);
	//6.打印结果为:lo
	printf("%s\n", *cpp[-2] + 3);
	//1.cpp[-2]:cpp指向的值先减2再解引用——(1)cpp - 2——*cpp = c - 2 == a(临时,不是真的加);(2)*(cpp - 2)——*(cpp - 2) = a;
	//2.*cpp[-2]:再解引用——**(cpp - 2) == *a == 3;
	//3.*cpp[-2] + 3:一级字符指针 + 3——*3 = 'p' + 3 =='g'(临时,不是真的加);
	//4.打印结果为:gge
	printf("%s\n", cpp[-1][-1] + 1);
	//1.cpp[-1]:cpp指向的值先减1再解引用——(1)cpp - 1——*cpp = c - 1 == b(临时,不是真的加);(2)*(cpp - 2)——*(cpp - 2) = b;
	//2.cpp[-1][-1]:b指向的值先减1再解引用——(1)b - 1——*b = 2 - 1 == 1(临时,不是真的加);(2)*1——*1 = 'w';
	//3.cpp[-1][-1] + 1:一级字符指针 + 1——*1 = 'w' + 1 == 'o'(临时,不是真的加);
	//4.打印为:orld
	return 0;
}

考察:操作符的优先级只在操作数的相邻位置才考虑;++ 和-- 会真实改变变量存的数据
涉及:二级指针,操作符++和–,解引用。

回调函数

回调函数(Callback Function)是一种编程模式,在这种模式中,一个函数作为参数传递给另一个函数,并在某个时刻被调用。这种模式允许程序在特定事件或条件发生时执行特定的代码块,而无需阻塞程序的其余部分继续运行。

例如:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void menu() {
	printf("*******************\n");
	printf("****1.Add 2.Sub****\n");
	printf("****3.Mul 4.Div****\n");
	printf("*******0.exit******\n");
	printf("*******************\n");
}
int Add(int x, int y) {
	return x + y;
}

int Sub(int x, int y) {
	return x - y;
}

int Mul(int x, int y) {
	return x * y;
}

int Div(int x, int y) {
	return x / y;
}
int(*p[5])(int, int) = {NULL, Add, Sub, Mul, Div};
int main() {
	int input = 0;
	int n = 0;
	int m = 0;
	do {
		menu();
		printf("请选择您要的的计算器模式:>");
		scanf("%d", &input);
		if (input == 0) {
			printf("退出\n");
			break;
		}else if (input <= 4 && input >= 1) {
			printf("请输入两个操作数:>");
			scanf("%d%d", &n, &m);
			int ret = p[input](n, m);
			printf("%d\n", ret);
		}
		else {
			printf("输入错误\n");
		}

	} while (input);
	return 0;
}

这是用回调函数模拟的计算器,也涉及指向函数指针数组的数组。

指向函数指针数组的指针可以访问通过访问函数指针调用函数

鉴于以上内容已经很多,就不往下介绍了,大家看完一定要好好消化!

在这里插入图片描述
加油!

标签:arr,指向,int,C语言,深入,数组,printf,指针
From: https://blog.csdn.net/z15879084549/article/details/144025535

相关文章

  • 深入理解与应用 multipart/form-data:文件上传与预览实现解析
    1.理解multipart/form-datamultipart/form-data是一种在HTTP请求中使用的MIME类型,主要用于在客户端和服务器之间传输包含文件或二进制数据的表单数据。它通过一个边界(boundary)来分隔不同的表单字段和文件数据。简单来说,multipart/form-data类型用于确保在表单中提......
  • Fail-Fast与Fail-Safe:深入理解Java中的这两种机制
    Fail-Fast与Fail-Safe:深入理解Java中的这两种机制在Java编程中,我们经常遇到“fail-fast”和“fail-safe”这两个术语,尤其是在处理多线程和集合框架时。但很多开发者可能并不完全清楚它们的具体含义和应用场景。本文将深入探讨这两种机制,帮助你更好地理解它们的工作原理以及......
  • 【C语言习题】(四)
    目录1.编写一个函数实现n的k次方,使用递归实现2.写一个递归函数DigitSum(n),输入一个非负整数,返回组成它的数字之和;例如,调用DigitSum(1729),则应该返回1+7+2+9,它的和是19;输入:1729,输出:193.递归方式实现打印一个整数的每一位4.非递归实现strlen5.递归实现strlen6.非......
  • 【C语言习题】(三)
    目录1.九九乘法表2.求10个整数中最大值(x)3.求10个整数中最大值(v)4.计算1/1-1/2+1/3-1/4+1/5……+1/99-1/100的值,打印出结果(三种解法如下)(1)(2)(3)5.编写程序数一下1到100的所有整数中出现多少个数字99192939495969798999(个位为9:i%10==9)90919293949......
  • C语言常用数据类型介绍(有图)
    数据类型其实是固定大小内存的别名,并且描述了一个变量存放什么类型的数据。简单来说,就是组织和操作数据。数据类型不仅帮助我们组织和操作数据,还决定了程序如何有效的利⽤内存。序号数据类型中文说明大小(字节)1short短整型22int基本整形43long(longint)长整型4(32位编译器......
  • 初入C语言
    在接触编程开始之前先让我们了解一下计算机的组成。计算机系统是由硬件系统和软件系统两大部分组成。而计算机硬件由五个基本部分组成:运算器、控制器、存储器、输入设备和输出设备。硬件:组成计算机的各种物理部件。(⿏标,键盘)软件:计算机中运⾏的程序和数据。计算机的工作原理......
  • 【Linux探索学习】第十六弹——进程地址空间:深入解析操作系统中的进程地址空间
    Linux学习笔记:https://blog.csdn.net/2301_80220607/category_12805278.html?spm=1001.2014.3001.5482前言:进程地址空间是操作系统进程管理的重要概念之一,它定义了进程在执行时所能访问的内存布局。理解进程地址空间不仅有助于掌握操作系统的运行原理,也为程序优化、内存管......
  • 灵茶山艾府-相向双指针
    两数之和暴力做法:双重循环嵌套o(n*n)双指针:由于数组有序,最小+最大,如果大于满足条件,右指针移动,如果小于左指针移动o(n)为什么快:优化获取信息,知道其中一个与其他所有数的和与目标的大小关系。三元数组和双指针:将数组排序,然后使用双指针进行寻找合适的,left从目前寻找的后面......
  • 研一小白零基础学习C语言(三)
    零基础学习C语言(三)研一人机与环境工程零基础接触学习C语言文章目录零基础学习C语言(三)前言一、算数运算符二、赋值操作符、单目操作符和强制类型转换三、printf详细介绍四、scanf详细介绍前言主要介绍了算数运算符、赋值操作符、单目操作符和强制类型转换、printf......
  • MySQL与Informix数据库中的同义表创建:深入解析与比较
    MySQL与Informix数据库中的同义表创建:深入解析与比较一、同义表的基本概念与用途1.定义与概念2.主要用途二、MySQL数据库中的同义表创建1.使用视图创建同义表2.使用别名创建同义表3.MySQL中的同义表限制与替代方案三、Informix数据库中的同义表创建......