C语言从入门到精通 第九章(指针与动态内存分配)【上】

写在前面:

  1. 本系列专栏主要介绍C语言的相关知识,思路以下面的参考链接教程为主,大部分笔记也出自该教程。
  2. 除了参考下面的链接教程以外,笔者还参考了其它的一些C语言教材,笔者认为重要的部分大多都会用粗体标注(未被标注出的部分可能全是重点,可根据相关部分的示例代码量和注释量判断,或者根据实际经验判断)。
  3. 如有错漏欢迎指出。

参考教程:C语言程序设计从入门到进阶【比特鹏哥c语言2024完整版视频教程】(c语言基础入门c语言软件安装C语言指针c语言考研C语言专升本C语言期末计算机二级C语言c语言_哔哩哔哩_bilibili

一、指针(基础版)

1、指针概述

(1)内存编号是从0开始记录的,一般用十六进制数字表示,可以通过指针间接访问内存 ,也 可以利用指针变量保存地址。(下图中a是变量名,p是指针变量名)

(2)指针变量声明的一般形式:

<数据类型> * <变量名>;

①数据类型是指针所指对象的类型,在C++中指针可以指向任何C++类型。

②变量名即指针变量名。

(3)普通变量存放的是数据,指针变量存放的是地址

(4)指针类型与所指对象之间的关系:

|-------------------------|------------------------------|
| int *px; | 指向整型变量的指针 |
| char *pc; | 指向字符型变量的指针 |
| char *apc[10]; | 由指向字符的指针构成的数组,即指针数组 |
| char( *pac)[10]; | 指向字符数组的指针,即数组指针 |
| int *fpi( ); | 返回值为指向整型量的指针的函数,即指针函数 |
| int( *pfi)( ); | 指向返回值为整型量的函数的指针,即函数指针 |
| int( *p[4][3])( ); | 指针数组,数组中每个元素为指向返回值为整型量的函数的指针 |
| int *( *pfpi)( ); | 指向函数的指针,该函数的返回值为指向整型量的指针 |

2、指针和地址

(1)在使用任何指针变量之前必须先给它赋一个指向合法具体对象的地址值,否则该指针是野指针,程序运行时会出现问题。使一个指针指向一个具体对象的方法有:

①使用new运算符(或malloc和alloc等函数)给指针分配一个具体空间。

②将另一个同类型的指针赋给它以获得值。

③通过&运算符指向某个对象。

(2)指针使用两种特殊的运算符------"*"和"&"。

①一元(单目)运算符&用于返回其操作对象的内存地址,其操作对象通常为一个变量名。

②一元(单目)运算符*用于返回其操作数所指对象的值 (或者说访问所指对象),其操作对象必须是一个指针,这个操作称为解引用。

(3)举例:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int num = 10;
	int *p = &num;   //指针变量专门用来存放地址
	*p = 20;         //通过解引用可以对num进行操作
	printf("%d\n", *p);

	char ch = 'w';
	char* pc = &ch;   //指针变量专门用来存放地址
	*pc = 'q';        //通过解引用可以对ch进行操作
	printf("%c\n", ch);
	
	return 0;
}

3、指针所占内存空间

所有指针类型在32位操作系统下是4个字节 ,在64位 操作系统下是 8个字节

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
//指针变量的大小取决于地址的大小
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
	printf("%d\n", sizeof(char *));
	printf("%d\n", sizeof(short *));
	printf("%d\n", sizeof(int *));
	printf("%d\n", sizeof(double *));
	return 0;
}

4、指针和指针类型

(1)每种类型的变量都有其对应类型的指针变量,甚至指针变量也有其对应类型的指针变量(也就是"指针的指针")。

①char* 类型的指针存放char类型变量的地址。

②short* 类型的指针存放short类型变量的地址。

③int* 类型的指针存放int类型变量的地址。

④char** 类型的指针存放char*类型指针变量的地址。

⑤short** 类型的指针存放short*类型指针变量的地址。

⑥int** 类型的指针存放int*类型指针变量的地址。

(2)指针的类型决定了对指针解引用的时候有多大的权限(能操作几个字节),拿一个int*型的指针和一个char*型的指针举例,如果两个指针都指向同一个地址,对char*型指针解引用只能操作1个字节的内存,但是对int*型指针解引用就能操作连续4个字节的内存(这里认为int型变量占用4个字节)。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int n = 0x11223344;    //0x开头表示这是一个十六进制数
	char *pc = (char *)&n;  //将&n(地址常量)强制转换为字符指针类型,赋给字符指针变量
	int *pi = &n;
	*pc = 0;
	*pi = 0;
	return 0;
}

(3)如果使用不匹配一个变量类型的指针访问该变量,很可能会引发不必要的错误。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int n = 9;
	float *pFloat = (float *)&n;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);

	*pFloat = 9;   //整型和浮点型的存储方式不一样,指针类型不对,访问时就会有问题
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	return 0;
}

5、指针运算

(1)指针和整型量可以进行加减 ,若p为指针,n为整型量,则p+n和p-n是合法的,同样p++也是合法的,它们的结果同指针所指对象类型的占用字节数相关

①如若p为指向字符数组第一个元素的指针,则p+1为指向字符数组第二个元素的指针,实际增加1(1是char型数据所占的字节数)。

②如若p为指向整型数组第一个元素的指针,则p+1为指向整型数组第二个元素的指针,实际增加1个整型单位长(假如int类型长度为32位,则实际增加4个字节,可借助sizeof运算符计算某个类型的长度)。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int n = 10;
	char *pc = (char*)&n;
	int *pi = &n;

	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc + 1);
	printf("%p\n", pi);
	printf("%p\n", pi + 1);
	//指针的类型决定了指针向前或者向后走一步有多大(距离)
	return 0;
}

(2)若p1、p2为指针,当二者指向同一类型时,可以进行赋值,如:

p2 = p1;

注:该语句使得两指针指向同一空间,若其中一个指针所指空间被删除(释放),则另一个指针所指空间亦会被删除,两个指针均变成野指针。

(3)两个指向同一类型的指针,可进行==、>、<等关系运算,其实就是地址的比较 ,比如++++数组中第一个元素的地址会小于第二个元素的地址,以此类推++++(不同数组的元素地址相互比较虽然也被允许,但是基本上没有任何意义)。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>

int main() {

	//字符串逆序输出
	char *p, *q, temp;
	char s[210] = "zifuchuangnixushuchu";
	p = s;  //s可代表字符数组s的首地址,也就是第一个元素的地址
	q = s + strlen(s) - 1;  //q指向字符串s的最后一个字符
	while (p < q)
	{
		temp = *p;
		*p = *q;
		*q = temp;
		p++;
		q--;
	}
	printf("%s\n", s);

	return 0;
}

(4)两个 指向同一数组成员 的指针可进行相减,结果为两个指针之间相差的元素个数,假如p指向数组头,q指向数组尾,则q-p+1表示数组长度。(指针之间不能相加)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int my_strlen(char *s)
{
	char *p = s;
	while (*p != '\0')
		p++;
	return p - s;  //指针-指针的绝对值是指针和指针之间元素的个数(不是所有指针都能相减,要指向同一片空间的两个指针才能相减)
}
int main()
{
	char arr[] = "abcdefefsdf";
	int len = my_strlen(arr);

	return 0;
}

6、指针和数组的联系

(1)在C语言中,数组的名字就是指向该数组第一个元素(下标为0)的指针,即该数组第一个元素的地址,也即该数组的首地址。(特殊情况在介绍数组的一章有列出,这里不再赘述)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
	int *p = arr; //指针存放数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		printf("&arr[%d] = %p   <====> p+%d = %p\n", i, &arr[i], i, p + i);
	}
	return 0;
}

(2)一般情况下,一个数组元素的下标访问 a[i]等价于相应的指针访问 *(a+i)。需要注意的是,数组名和指针(变量)是有区别的,前者是常量,即数组名是一个常量指针,而后者是指针变量。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	int *p = arr; //指针存放数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

7、空指针、野指针和无类型指针

(1)不指向任何数据的指针称为空指针,其地址值为0,地址0处不能用于存储数据。可以用指针常量NULL(其值为0)来初始化一个指针变量,使之成为空指针。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main() 
{

	//空指针
	//指针变量p指向内存地址编号为0的空间
	int *p = NULL;    //内存编号0 ~255为系统占用内存,不允许用户访问

	return 0;
}

(2)指向非法的内存空间(不能访问的空间)的指针称为野指针。定义指针却不初始化,可能就会出现野指针。

①指针未初始化,就会出现野指针。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int *p;//局部变量指针未初始化,默认为随机值
	*p = 20;
	return 0;
}

②指针越界访问,此时指针也是野指针。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	int *p = arr;
	int i = 0;
	for (i = 0; i <= 11; i++)
	{
		//当指针指向的范围超出数组arr的范围时,p就是野指针
		*(p++) = i;
	}
	return 0;
}

③指针指向的空间被释放,这也会导致指针变成野指针。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int* text(void)
{
	int a;
	int* b = &a;
	return b;  //返回指针变量b,指针变量b指向的变量a在函数结束后就会被销毁
}

int main()
{
	int* c = text();
	printf("%d", *c);

	return 0;
}

④为了避免野指针的出现,可以有以下措施:

[1]建议每次在定义指针时都对指针进行初始化(不确定需要初始化为哪个变量的地址时就初始化为空指针,这条需要与第四条措施配合使用)。

[2]指针指向空间一旦释放就立即使之置NULL(这条需要与第四条措施配合使用)。

[3]编写函数时避免返回局部变量的地址。

[4]在使用指针之前检查其有效性(这条主要是为了防止对空指针解引用,其实也是避免对野指针解引用)。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int *p = NULL;
	//....(若干行代码)
	int a = 10;
	p = &a;
	if (p != NULL)
	{
		*p = 20;
	}
	return 0;
}

(3)可以用void来定义一个指针变量,称为无类型指针或void指针,例如:

void *pt = NULL;

①无类型指针不与任何特定的数据类型相关联,但却可用来指向任何类型的数据

任何类型的指针可以赋值给无类型指针,但反过来却不行

③无类型指针在一些特殊场合会派上用场。

8、const指针

(1)const修饰指针(const放在*之前)------常量指针:

①常量指针所指向的数据不可改动。

②常量指针本身可以改为指向其它数据。

③定义常量指针时可以不用初始化。

(2)const修饰常量(const放在*之后)------指针常量:

①指针常量所指向的数据可以改动。

②指针常量本身不可以改为指向其它数据。

③定义指针常量时必须初始化。

(3)const既修饰指针,又修饰常量------常量指针常量(前两种指针的综合):

①常量指针常量所指向的数据不可以改动。

②常量指针常量本身不可以改为指向其它数据。

③定义常量指针常量时必须初始化。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main() {

	//1、const修饰指针  常量指针-指针指向的值不可以改
	int a = 10;
	int b = 10;
	const int*p1 = &a;
	//*p1 = 20; 这句是错的 
	p1 = &b;

	//2、const修饰常量  指针常量-指针的指向不可以改
	int * const p2 = &a;
	//p2 = &b; 这句是错的
	*p2 = 20;

	//3、const修饰指针和常量
	const int * const p3 = &a;
	//*p3 = 100;  错的
	//p3 = &b;    错的

	return 0;
}

9、二级指针和指针数组

(1)指针变量也是变量,既然是变量就要有地址,那么指针变量的地址就可以存放在二级指针(指针的指针)中。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int a = 10;
	Int *pa = &a;    //a的地址存放在pa中,pa为一级指针
	int **ppa = &pa; //pa的地址存放在ppa中,ppa为二级指针

	int b = 20;
	*ppa = &b;    //pa = &b;
	**ppa = 30;   //*pa = 30;  //a = 30;

	return 0;
}

(2)指针具有自己的数据类型,那么就有该数据类型对应的数组------指针数组,其定义方式如下:

<数组的元素类型>*<指针名>[<数组元素个数>]

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int* arr1[10]; //整型指针的数组,每个元素是一个整型指针
	char *arr2[4]; //一级字符指针的数组,每个元素是一个字符型指针
	char **arr3[5];//二级字符指针的数组,每个元素是一个字符型指针的指针
}

二、指针(进阶版)

1、字符指针与字符串

(1)C/C++会把常量字符串存储到单独的一个内存区域,在代码中使用常量字符串时,实际上使用的是该常量字符串在内存中的首元素地址

(2)常量字符串实际上也是一个字符数组,其每个元素是字符常量,它们存储在内存的一片连续空间中,那么就可以使用字符指针指向其中的一个元素,不过内存中的常量字符串不可修改,所以指向其元素的字符指针类型为常变量指针。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	char ch = 'w';
	char *pc = &ch; //char*是字符指针
	*pc = 'w';

	const char* pstr = "hello world.";  //把字符串的首字符地址放到pstr指针变量中
	printf("%s\n", pstr);
	printf("%c\n", &pstr);

	return 0;
}

(3)当几个指针指向同一个字符串的时候,它们实际会指向同一块内存,但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	const char *str3 = "hello bit.";
	const char *str4 = "hello bit.";
	if (str1 == str2)
		printf("str1 and str2 are same\n");
	else
		printf("str1 and str2 are not same\n");

	if (str3 == str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");

	return 0;
	//这里str3和str4指向的是一个同一个常量字符串,C/C++会把常量字符串存储到单独的一个内存区域
	//当几个指针指向同一个字符串的时候,他们实际会指向同一块内存
	//但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块,所以str1和str2不同,str3和str4不同
}

2、数组指针

(1)前面有介绍过,数组是有自己的类型的,比如定义一个含10个整型元素的数组,那么这个数组的类型就是"int [10]",既然数组也有自己的类型,那么也应该有相应的指针,这个指针就是数组指针,需要说明的是,++++元素个数不同的数组,即使元素类型相同,它们也不是同类型的数组++++。数组指针的定义格式如下:

<数组的元素类型>(*<指针名>)[<数组元素个数>]

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int(*p)[10];  //数组指针的定义
	//p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组,所以p是一个指针,指向一个数组,叫数组指针
	//[]的优先级要高于*号的,所以必须加上()来保证p先和*结合
	int arr[5];           //整型数组,该数组有5个元素
	int *parr1[10];       //指针数组,该数组有10个元素
	int(*parr2)[10];      //数组指针,指向的数组有10个元素
	int(*parr3[10])[5];   //数组指针数组(该数组有10个元素,数组指针指向的数组各有5个元素)

	return 0;
}

(2)前面还介绍过,数组名除了两种特殊情况外都表示其首元素的地址,而其中一种特殊情况就是"&数组名",这种情况下获得的是数组的地址,虽然它和数组首元素的地址值相同,可是意义却不一样,主要体现在与整型数据之间的运算。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	printf("arr = %p\n", arr);
	printf("&arr= %p\n", &arr[0]);
	printf("&arr= %p\n", &arr);
	printf("arr+1 = %p\n", arr + 1);
	printf("&arr= %p\n", &arr[0] + 1);
	printf("&arr+1= %p\n", &arr + 1);
	return 0;
	//根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的
	//实际上,&arr 表示的是数组的地址,而不是数组首元素的地址
	//本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型
	//数组的地址 + 1,意义是跳过整个数组的大小,所以 &arr + 1 相对于 &arr 的差值是40
}

(3)数组指针必须匹配数组类型的地址,而不是数组元素类型的地址(即使地址值相同也不行)。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p,但是一般很少这样写代码
	return 0;
}

(4)对数组指针进行一次解引用操作,得到的是指向数组首元素的指针,可以对这个指针再进行一次解引用,从而访问到数组中的单个元素。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p,但是一般很少这样写代码
	printf("%p\n", *p);         //数组首元素的地址
	printf("%d\n", **p);        //1(数组的第1个元素)
	printf("%d\n", *(*p + 1));  //2(数组的第2个元素)
	return 0;
}

3、数组和指针作为函数参数

(1)指针可以作为函数参数进行传递,函数将指针从实参拷贝到形参中,对形参解引用可以访问这个指针指向的变量(无论这个变量在哪定义,只要没被销毁即可被访问),待函数返回时,销毁的只是形参的指针,实参的指针指向的变量不会受函数结束而被影响。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

void print(int *p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d\n", *(p + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9 };
	int *p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//一级指针p,传给函数
	print(p, sz);   //print(&arr[0], sz);
	return 0;
}

(2)既然一级指针可以作为函数参数进行传递,那么二级指针自然也是可以的,只是使用时需要二次解引用才能访问到其指向的变量。

①例1:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

void test(int** ptr)
{
	printf("num = %d\n", **ptr);
}
int main()
{
	int n = 10;
	int*p = &n;
	int **pp = &p;
	test(pp);
	test(&p);
	return 0;
}

②例2:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

void test(char **p)
{

}
int main()
{
	char c = 'b';
	char*pc = &c;
	char**ppc = &pc;
	char* arr[10];
	test(&pc);
	test(ppc);
	test(arr);   //arr是指针数组的首元素地址,也就是说它是一级指针的地址
	return 0;
}

(3)当函数的形参列表中有一维数组时,实际上这个形参接收的是数组的首元素地址,在形参列表中可以不写明数组的元素个数。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//C语言不允许函数重载(也就是函数同名),下面这样写只是为了方便展示和测试
void test(int arr[])
{}
void test(int arr[10])
{}
void test(int *arr)
{}
void test2(int *arr[20])
{}
void test2(int **arr)
{}
int main()
{
	int arr[10] = { 0 };
	int *arr2[20] = { 0 };  //含20个整型指针的指针数组
	test(arr);   //数组arr的首元素地址(此时arr代表整型指针)
	test2(arr2); //数组arr2的首元素地址(此时arr代表整型指针的指针)
}

(4)当函数的形参列表中有二维数组时,实际上这个形参接收的是数组的首元素地址,而二维数组可看做元素类型为一维数组的一维数组,所以形参接收的是一个一维数组的地址 。(需要说明的是,二维数组传参,函数形参的设计只能省略二维数组的行数

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

void print_arr1(int arr[3][5], int row, int col)
{
	int i = 0, j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
void print_arr2(int(*arr)[5], int row, int col)
{
	int i = 0, j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
	print_arr1(arr, 3, 5);
	//数组名arr,表示首元素的地址,但是二维数组的首元素是二维数组的第一行
	//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址,可以用一维数组指针来接收(元素个数需匹配)
	print_arr2(arr, 3, 5);
	return 0;
}

4、函数指针

(1)除了变量具有地址外,函数也具有地址(因为函数的代码也是需要存储空间的),换言之,函数也应该具有指针,这个指针就是函数指针,函数指针的类型与其返回值、参数个数和参数类型有关

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

void test()
{
	printf("hehe\n");
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

(2)函数指针的定义和使用:

①函数指针的定义格式如下:

<返回值类型>(*<指针名>)(形参列表(仅给出类型))

②使用函数指针调用函数的方式如下:

(*<指针名>)(实参列表) 或 <指针名>(实参列表)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int(*pf)(int, int) = &Add;   //定义时顺便初始化,使函数指针指向Add函数,等效于int(*pf)(int, int) = Add;
	int a = (*pf)(2, 3);   //通过函数指针调用Add函数,等效于int a = pf(2, 3);
	printf("%d\n", a);

	return 0;
}

(3)函数指针的类型名较为复杂,为了方便管理和使用,可使用typedef关键字对其进行重命名,格式如下:

typedef <返回值类型>(*<新的类型名>)(<形参列表(仅给出类型)>)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

typedef void(*pfun_t)(int);   //将指向void(int)类型的函数指针的类型名命名为"pfun_t"

pfun_t signal(int, pfun_t)
{
	return (pfun_t)0;
}

int main()
{
	//代码1
	(*(void(*)())0)();
	//0被强制类型转换为void(*)()类型(函数指针类型),接着被*解引用,调用在地址为0处的无参函数

	//代码2
	void(*signal(int, void(*)(int)))(int);
	//这是个函数声明,signal函数的返回值类型是void(*)(int)
	//可以将其简化为4-9行的代码

	return 0;
}

(4)函数指针和普通指针一样,也应该具有数组。

①函数指针数组的定义方式如下:

<返回值类型>(*<指针名>[<元素个数>])(形参列表(仅给出类型))

②通过函数指针数组中的元素调用对应函数分方式如下:

(*<指针名>[<元素下标>])(实参列表) 或 <指针名>[<元素下标>](实参列表)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#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 div(int a, int b)
{
	return a / b;
}
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
	while (input)
	{
		printf("*************************\n");
		printf(" 1:add           2:sub \n");
		printf(" 3:mul           4:div \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		if ((input <= 4 && input >= 1))
		{
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
		}
		else
			printf("输入有误\n");
		printf("ret = %d\n", ret);
	}
	return 0;
}

(5)既然普通数组有指向它的指针,那么函数指针数组自然也有指向它的指针,不过这种操作已经过于复杂了,在实际开发中可能并不会用到。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

void test(const char* str)
{
	printf("%s\n", str);
}
int main()
{
	//函数指针pfun
	void(*pfun)(const char*) = test;
	//函数指针的数组pfunArr
	void(*pfunArr[5])(const char* str);
	pfunArr[0] = test;
	//指向函数指针数组pfunArr的指针ppfunArr
	void(*(*ppfunArr)[5])(const char*) = &pfunArr;
	*ppfunArr[0] = test;
	return 0;
}

5、回调函数

(1)回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

(2)举例:

①例1:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <search.h>

//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{
	return (*(int *)p1 - *(int *)p2);    //升序
	//return (*(int *)p2 - *(int *)p1);  //降序
}
int main()
{
	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	int i = 0;

	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

②例2:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <search.h>
#include <string.h>

struct Stu
{
	char name[20];
	int age;
};
int Stu_cmp(const void * p1, const void * p2)   //按名字排
{
	return strcmp(((struct Stu*)p1)->name,((struct Stu*)p2)->name);
}

void test()
{
	struct Stu s[] = { {"zhangsan",15},{"lisi",30},{"wangwu",25} };
	qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), Stu_cmp);
	for (int i = 0; i < sizeof(s) / sizeof(s[0]); i++)
	{
		printf("姓名:%s  年龄:%d\n", s[i].name, s[i].age);
	}
}

int main()
{
	test();
	
	return 0;
}

③例3:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int int_cmp(const void *p1, const void *p2)  //整型数据的比较函数(升序)
{
	return(*(int*)p1 - *(int*)p2);  //返回比较结果
}

void _swap(void *p1, void *p2, int width)  //width是需要交换的元素的数据类型的长度
{
	int i = 0;
	for (; i < width; i++)  //逐个字节进行交换(这是为了兼容不同类型的数据)
	{
		char temp = *((char*)p1 + i);
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = temp;
	}
}

void bubble(void *base, int num, int width, int(*cmp)(const void *, const void *))
{
	for (int i = 0; i < num - 1; i++)
	{
		int flag = 1;
		for (int j = 0; j < num - 1 - i; j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				_swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
				flag = 0;  //如果该轮冒泡产生数据交换,说明排序可能还没完成
			}
		}
		if (flag == 1)  //如果该轮冒泡没有产生数据交换,说明已经排序完成,直接结束排序
		{
			break;
		}
	}
}

int main()
{
	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), int_cmp);
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d  ", arr[i]);
	}

	return 0;
}

6、关于指针和数组的一些程序

(1)例1:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int a[5] = { 1, 2, 3, 4, 5 };
	int *ptr = (int *)(&a + 1);  //&a表示的是数组的地址,对其+1则是跳过了一整个数组,得到的结果再强制转换成数组元素的地址
	printf("%d,%d", *(a + 1), *(ptr - 1));  //这里的a表示数组首元素地址
	return 0;
}
//程序的结果是2,5

(2)例2:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

struct Test
{
	int Num;
	char *pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p = (struct Test*)0x100000;
//假设p 的值为0x100000
//已知,结构体Test类型的变量大小是20个字节
int main()
{
	printf("%p\n", p + 0x1);  //0x100014
	printf("%p\n", (unsigned long)p + 0x1);  //0x100001(强制转换为无符号长整型,不是地址)
	printf("%p\n", (unsigned int*)p + 0x1);  //0x100004
	return 0;
}

(3)例3:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int a[4] = { 1, 2, 3, 4 };
	int *ptr1 = (int *)(&a + 1);
	int *ptr2 = (int *)((int)a + 1);  //数组a首元素地址强制转换为整型,其值+1,然后再转换回整型指针,但是整型占4个字节
	printf("%x,%x", ptr1[-1], *ptr2);  //4,2000000   ptr1[-1]等价于*(ptr1 - 1)
	return 0;
}

(4)例4:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int a[3][2] = { (0, 1), (2, 3), (4, 5) };  //大括号内的是逗号表达式,值为最后一个表达式的值,也就是说,这是不完全初始化
	int *p;
	p = a[0];  //a[0]为第一行数组的数组名,这样p就是第一行数组的首元素地址
	printf("%d", p[0]);  //1  p[0]为第一行数组的首元素,即a[0][0]
	return 0;
}

(5)例5:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

typedef int(*i_4)[4];

int main()
{
	int a[5][5];//五五二十五个元素
	int(*p)[4];//指向含有四个整型元素数组的指针
	p = (i_4)a;
	printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);  //FFFFFFFC,-4(指针相减,得到两个指针间的元素个数;以地址形式输出,那么负数就要求补码)
	//p[4][2]等价于*(*(p + 4) + 2),注意p是指向含有四个整型元素数组的指针,而a数组中每个一维数组都有五个元素
	return 0;
}

(6)例6:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int *ptr1 = (int *)(&aa + 1);    //&aa为数组aa的地址
	int *ptr2 = (int *)(*(aa + 1));  //aa为数组aa[2]的首元素地址,即aa[0]
	printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));  //10,5
	return 0;
}

(7)例7:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	const char *a[] = { "work","at","alibaba" };
	const char**pa = a;  //目前pa指向指向work首字母地址的指针
	pa++;                //pa指向指向at首字母地址的指针
	printf("%s\n", *pa); //at
	return 0;
}

(8)例8:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	const char *c[] = { "ENTER","NEW","POINT","FIRST" };
	const char**cp[] = { c + 3,c + 2,c + 1,c };
	const char***cpp = cp;
	printf("%s\n", **++cpp);        //POINT
	printf("%s\n", *--*++cpp + 3);  //ER
	printf("%s\n", *cpp[-2] + 3);   //ST
	printf("%s\n", cpp[-1][-1] + 1); //EW
	return 0;
}
相关推荐
万物得其道者成12 分钟前
React Zustand状态管理库的使用
开发语言·javascript·ecmascript
学步_技术18 分钟前
Python编码系列—Python抽象工厂模式:构建复杂对象家族的蓝图
开发语言·python·抽象工厂模式
wn53141 分钟前
【Go - 类型断言】
服务器·开发语言·后端·golang
Hello-Mr.Wang1 小时前
vue3中开发引导页的方法
开发语言·前端·javascript
救救孩子把1 小时前
Java基础之IO流
java·开发语言
WG_171 小时前
C++多态
开发语言·c++·面试
宇卿.1 小时前
Java键盘输入语句
java·开发语言
Amo Xiang1 小时前
2024 Python3.10 系统入门+进阶(十五):文件及目录操作
开发语言·python
friklogff1 小时前
【C#生态园】提升C#开发效率:深入了解自然语言处理库与工具
开发语言·c#·区块链
重生之我在20年代敲代码3 小时前
strncpy函数的使用和模拟实现
c语言·开发语言·c++·经验分享·笔记