C语言的罗盘指针——深入理解C语言指针及其应用

举报
未见花闻 发表于 2022/04/29 23:06:26 2022/04/29
【摘要】 C语言的罗盘指针——深入理解C语言指针及其应用

⭐️前面的话⭐️

前面已经简要介绍了数组指针,字符串库函数和指针等内容,在这篇文章我们将继续深入了解有关字符串库函数和指针的探索。

📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创!
📆华为云首发时间:🌴2022年4月29日🌴
✉️坚持和努力一定能换来诗与远方!
💭参考书籍:📚《C语言程序设计》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


1.字符指针与字符串

字符指针即指向字符类型的指针。

char* pch = NULLchar ch = 's';
pch = &ch;//字符指针

1.1字符指针与字符串的关系

在C语言中,并没有字符串这种数据类型,但是在编程中又常常遇到字符串,那在C语言中然后去表达字符串呢?
有两种方式,一是使用字符数组表示字符串,本质上就是定义一个char类型的数组变量去储存字符串,使用字符数组表示字符串得到的是一个字符串变量,它是能够被修改的;二是使用字符指针的方式去表达字符串,本质上就是使用一个char指针指向字符串的第一个字符,因为在输出字符串的时候计算机是以\0为标志来打印字符串的。但是要注意的是,它和使用字符数组不同,字符数组定义的是一个字符串变量,而字符指针定义的是一个字符串常量,是不能进行修改的。

#include <stdio.h>

int main()
{
	char str1[40] = "Come on, China Olympic Games!";//使用字符数组定义字符串变量

	char* str2 = "Come on, China Olympic Games!";//使用字符指针定义字符串常量

	printf("str1:%s\nstr2:%s\n", str1, str2);
	return 0;
}

:bulb:运行结果:

str1:Come on, China Olympic Games!
str2:Come on, China Olympic Games!

D:\gtee\C-learning-code-and-project\test_813\Debug\test_813.exe (进程 6960)已退出,代码为 0。
按任意键关闭此窗口. . .

如果不小心对字符串常量修改,会引发读写访问冲突的异常,对于VS这个异常只有在调试的时候才能看得见而在编译的时候是不会报警告或者报错的,万一是在一个很大的工程中出现了这种问题,是很难察觉的,所以一般定义一个不能被修改的变量或常量的时候,一般在定义的数据类型前面加上一个const,这样如果不小心修改了这个const修饰的变量或常量,编译器在编译的时候就会报错,这样就能更容易地发现读写冲突异常的情况。

:bulb:面试题:

#include <stdio.h>
int main()
{
    char str1[] = "hello str.";
    char str2[] = "hello str.";
    char* str3 = "hello str.";
    char* str4 = "hello str.";
    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;
}

使用字符数组表达的是字符串变量,本质就是数组,每个数组是独立储存的,所以就算两个数组储存相同的内容,但是两者地址一定是不相同的。
而使用字符指针表达的是字符串常量,所以内容相同,字符指针指向的就是同一个地址。
知道了这个区别,那这道面试题也能迎刃而解了。
:bulb:运行结果:

str1 and str2 are not same
str3 and str4 are same

D:\gtee\C-learning-code-and-project\test_813\Debug\test_813.exe (进程 16488)已退出,代码为 0。
按任意键关闭此窗口. . .

1.2常用的几个字符串库函数

在前面介绍字符数组的时候简要提到了这几个与字符串相关的库函数,现在我将详细介绍这些常用的库函数。

1.2.1输出输入字符串的函数

puts(字符数组);//输出
int puts(const char* string);
gets(字符数组);//输入
char* gets(char* buffer);

在这里插入图片描述
:mag_right:puts:函数所在库为stdio.h;函数参数为const修饰的char*类型(使用const修饰是为了防止字符串被修改);函数返回值为int,如果函数执行失败则会返回EOF
在这里插入图片描述

:mag_right:gets:函数所在库为stdio.h;函数参数为char*类型(从缓冲区读取一行字符串,buffer是缓冲区的意思);函数返回值为char*(字符串的第一个元素地址),函数不能传入空指针,如果函数执行失败会返回EOF

用puts和gets函数只能输出或输入一个字符串。

puts作用:将一个字符串(以′\0′结束的字符序列)输出到终端。
用puts函数输出的字符串中可以包含转义字符。
在用puts输出时将字符串结束标志′\0′转换成′\n′,即输出完字符串后换行。
gets作用:从终端输入一个字符串到字符数组,并且得到一个函数值。该函数值是字符数组的起始地址。

1.2.2字符串连接函数

strcat(字符串1, 字符串2);
char *strcat( char *strDestination, const char *strSource );

在这里插入图片描述
:mag_right:strcat:函数所在库为string.h;函数参数有两个,第一个为char*类型(目的字符串地址)第二个参数为const char*类型(被连接字符串的地址);函数返回值为char*(返回strDestination的元素地址)。
strcat函数将strSource追加到strDestination后面。

strcat作用:把两个字符数组中的字符串连接起来,把字符串2接到字符串1的后面,结果放在字符数组1中,函数调用后得到一个函数值——字符数组1的地址。
字符数组1必须足够大,以便容纳连接后的新字符串。
连接前两个字符串的后面都有′\0′,连接时将字符串1后面的′\0′取消,只在新串最后保留′\0′。

1.2.3字符串复制函数

strcpy(字符串1, 字符串2);
char *strcpy( char *strDestination, const char *strSource );

在这里插入图片描述

:mag_right:strcpy:函数所在库为string.h;函数参数有两个,第一个为char*类型(目的字符串地址)第二个参数为const char*类型(被复制字符串的地址);函数返回值为char*(返回strDestination的元素地址)。
strcpy函数将strSource复制给strDestination

strcpy作用:将字符串2复制到字符数组1中去。
字符数组1必须定义得足够大,以便容纳被复制的字符串2。字符数组1的长度不应小于字符串2的长度。
“字符数组1”必须写成数组名形式,“字符串2”可以是字符数组名,也可以是一个字符串常量。
若在复制前未对字符数组1初始化或赋值,则其各字节中的内容无法预知,复制时将字符串2和其后的′\0′一起复制到字符数组1中,取代字符数组1中前面的字符,未被取代的字符保持原有内容。
不能用赋值语句将一个字符串常量或字符数组直接给一个字符数组。字符数组名是一个地址常量,它不能改变值,正如数值型数组名不能被赋值一样。
可以用strncpy函数将字符串2中前面n个字符复制到字符数组1中去。
将str2中最前面2个字符复制到str1中,取代str1中原有的最前面2个字符。但复制的字符个数n不应多于str1中原有的字符(不包括′\0′)。

1.2.4字符串比较函数

strcmp(字符串1, 字符串2);
int strcmp( const char *string1, const char *string2 );

在这里插入图片描述
:mag_right:strcmp:函数所在库为string.h;函数参数有两个,第一个为constchar*类型(第一个字符串地址)第二个参数为const char*类型(第二个字符串的地址);函数返回值为int(相同则返回0,字符串1>字符串2则返回一个正整数,字符串1<字符串2则返回一个负整数)。

:bulb:对两个字符串比较不能直接用str1>str2进行比较,因为str1和str2代表地址而不代表数组中全部元素,而只能用 (strcmp(str1,str2)>0)实现,系统分别找到两个字符数组的第一个元素,然后顺序比较数组中各个元素的值。

strcmp作用:比较字符串1和字符串2。
字符串比较的规则是: 将两个字符串自左至右逐个字符相比(按ASCII码值大小比较),直到出现不同的字符或遇到′\0′为止。
(1) 如全部字符相同,则认为两个字符串相等;
(2) 若出现不相同的字符,则以第1对不相同的字符的比较结果为准。
比较的结果由函数值带回。
(1) 如果字符串1与字符串2相同,则函数值为0。
(2) 如果字符串1>字符串2,则函数值为一个正整数。
(3) 如果字符串1<字符串2,则函数值为一个负整数。

1.2.5测字符串长度的函数

strlen(字符串);
size_t strlen( const char *string );//size_t本质上是unsigned int 类型

在这里插入图片描述
:mag_right:strlen:函数所在库为string.h;函数参数为const char*类型(需要被计算字符串长度的字符串地址);函数返回值为unsigned int(size_t)(返回传入字符串长度大小)。

strlen作用:测试字符串长度的函数。函数的值为字符串中的实际长度(不包括′\0′在内)。

1.2.6转换为大小写的函数

strlwr(字符串);//大写字母转小写字母
char *_strlwr( char *string );
strupr(字符串)//小写字母转大写字母

在这里插入图片描述
:mag_right:strlwr:函数所在库为string.h;函数参数为char*类型(需要被转换成小写的字符串地址);函数返回值为char*(返回传入字符串地址)。
在这里插入图片描述

:mag_right:strupr:函数所在库为string.h;函数参数为char*类型(需要被转换成大写的字符串地址);函数返回值为char*(返回传入字符串地址)。

作用:
srtlwr将字符串中大写字母换成小写字母。
strupr将字符串中小写字母换成大写字母。

2.指针数组与数组指针

2.1指针数组

指针数组就是数组元素类型为指针的数组。

int* arr1[10]; //整形指针的数组
char* arr2[40]; //一级字符指针的数组
char** arr3[50];//二级字符指针的数组

2.2数组指针

数组指针就是指向一个数组的指针。要弄清楚数组指针是什么,首先必须知道数组指针是指针还是数组。我们从概念名字上进行分析,就能知道它是指针而不是数组。
我们以类比的方式来搞清楚数组指针究竟是什么?
我们已经学习过整型指针int*,浮点数指针float* double*,我们以类型名+变量名的方式定义一个相应类型的指针。

a = 8;
b = 2.2;
int* pa = &a;//整型指针
double* pb = &b;//双精度浮点型指针

同理数组指针也是如此,不过需要与指针数组区分开来

int* arr[10];//整型指针数组,是一个数组,存放了10个整型指针类型元素
int (*parr)[10];//这就是一个数组指针,是不是和指针数组有点像呢?
//因为[]优先级高于*,如果不加括号,变量名首先会与[]结合表示成一个数组
//如果(*变量名),那变量名首先会与*结合就表示一个指针

int (*parr)[10]表示一个指针,指向一个数据类型为int元素个数为10的一个数组,所以parr是一个数组指针变量。

2.3数组名与&数组名

数组名一般情况下指的是数组首元素地址,与数组名[0]等价。但是有二般情况,使用sizeof(数组名),这里的数组名指的是全数组整体;&arr并不是指数组名存放的地址的地址,而是指一个数组整体的首地址。数组名+1加的是该数组存放元素的数据类型的大小,如果为int类型的数组,那就是加4,而&arr+1加的是数组整体的大小,如果一个int类型的数组有10个元素,那就是加40。

3.数组传参与指针传参

3.1数组传参

3.1.1一维数组传参

对于test1 test2函数形参可以这么写呢?

#include <stdio.h>
int main()
{
	int arr1[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int* arr2[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr2[i] = &arr1[i];
	}
	test1(arr1);
	test2(arr2);
	return 0;
}
//test1
 void test1(int arr[]);//方式1
 void test1(int arr[10]);//方式2
 void test1(int* arr);//方式3
//test2
void test2(int* arr[]);//方式1
void test2(int* arr[10]);//方式2
void test2(int** arr);//方式3

3.1.2二维数组传参

对于test函数形参可以这么写呢?

#include <stdio.h>
int main()
{
	int arr[4][3] = { 0 };
	test(arr);
	return 0;
}
void test(int arr[4][3]);//方式1
void test(int arr[][3]);//方式2
void test(int arr[][]);//不能省略列数,错误
//对于二维数组来说,每个元素指的是每一行,首元素指的是第一行数组,所以可以使用数组指针传参
void test(int (*arr)[3]);//方式3

3.2指针传参

3.2.1一级指针传参

一个函数test(int* p)能接受什么参数?

int arr[10] = {0};
test(arr);//1.数组名
int a = 10;
int* pa = &a;
test(pa);//或
test(&a);//2.一级指针

3.2.2二级指针传参

一个函数test(int** p)能接受什么参数?

int n = 10;
int* p = &n;
int** pp = &p;
int* arr[10] = {0};
test(arr);//1.整型指针数组
test(pp);
test(&p);//2.二级指针

4.函数指针与函数指针数组

4.1函数指针

如果在程序中定义了一个函数,在编译时会把函数的源代码转换为可执行代码并分配一段存储空间。这段内存空间有一个起始地址,也称为函数的入口地址。每次调用函数时都从该地址入口开始执行此段函数代码。
函数名就是函数的指针,它代表函数的起始地址。
函数名与数组名有点相似,对函数名取地址得到的还是函数的地址。

#include <stdio.h>
void test()
{
	printf("hehe\n");
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

:bulb:运行结果:

004713B1
004713B1

D:\gtee\C-learning-code-and-project\test_813\Debug\test_813.exe (进程 20032)已退出,代码为 0。
按任意键关闭此窗口. . .

可以定义一个指向函数的指针变量,用来存放某一函数的起始地址,这就意味着此指针变量指向该函数。例如:

int (*p)(int,int);

定义p是一个指向函数的指针变量,它可以指向函数类型为整型且有两个整型参数的函数。此时,指针变量p的类型用int (*)(int,int)表示。

:star:函数指针定义通式:类型名 (*指针变量名)(函数参数表列)
:star:对于函数指针你要注意以下几点:

:lemon::定义指向函数的指针变量,并不意味着这个指针变量可以指向任何函数,它只能指向在定义时指定的类型的函数。
:lemon::如果要用指针调用函数,必须先使指针变量指向该函数。
:lemon::在给函数指针变量赋值时,只须给出函数名而不必给出参数。
:lemon::用函数指针变量调用函数时,只须将(*p)代替函数名即可(p为指针变量名),在(*p)之后的括号中根据需要写上实参。
:lemon::对指向函数的指针变量不能进行算术运算,如p+n,p++,p–等运算是无意义的。
:lemon::用函数名调用函数,只能调用所指定的一个函数,而通过指针变量调用函数比较灵活,可以根据不同情况先后调用不同的函数。
小栗子

#include <stdio.h>

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int a = 2;
	int b = 6;
	int (*p)(int, int) = Add;
	int c = Add(a, b);
	int d = (*p)(a, b);
	printf("%d\n%d", c, d);
	return 0;
}

:bulb:运行结果:

8
8
D:\gtee\C-learning-code-and-project\test_813\Debug\test_813.exe (进程 21292)已退出,代码为 0。
按任意键关闭此窗口. . .

:bulb:分析两个有趣的代码

#include <stdio.h>
int main()
{
	//代码1 
	(*(void (*)())0)();
	//代码2
	void (*signal(int, void(*)(int)))(int);
	return 0;
}

:key:代码1:
void (*)()是一个参数为空返回值为空的函数指针类型,(void (*)())0意思是将0强制转换成这种指针类型,(* (void (*)())0)()解引用被转换过的0执行在该地址的函数。
:key:代码2:
定义了一个名为signal的函数,其中参数为整型int和参数为int 返回值为空的函数指针void(*)(int),返回值为void (*)(int)类型的函数指针。
可以使用typedef将这个代码简化:

typedef void (*pf)(int)
void (*signal(int, void(*)(int)))(int);
//相当于
pf signal(int, pf);

4.2函数指针数组及其在计算器的应用

//函数指针
int (*func)(int, int);
//指针数组
int* arr[10];
//函数指针数组
int (*funcarr[10])(int, int);
//函数指针数组的指针
int (*(*pfuncarr)[10])(int, int);

函数指针数组用途:转移表

:corn:栗子:整型加减乘除计算器

菜单及其加减乘除函数

#include <stdio.h>
void menu()
{
	printf("**********************************\n");
	printf("**********************************\n");
	printf("*******        1.add       *******\n");
	printf("*******        2.sub       *******\n");
	printf("*******        3.mul       *******\n");
	printf("*******        4.div       *******\n");
	printf("*******        0.exit      *******\n");
	printf("**********************************\n");
	printf("**********************************\n");

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

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

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

int Div(int x, int y)//int (*)(int, int)
{
	return x / y;
}

void Calc(int(*pf)(int, int))
{
	int x = 0;
	int y = 0;
	int ret = 0;

	printf("请输入2个操作数:>");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("ret = %d\n", ret);
}

1.使用函数指针传参实现

int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			Calc(Add);
			break;
		case 2:
			Calc(Sub);
			break;
		case 3:
			Calc(Mul);
			break;
		case 4:
			Calc(Div);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

:bulb:运行结果:

**********************************
**********************************
*******        1.add       *******
*******        2.sub       *******
*******        3.mul       *******
*******        4.div       *******
*******        0.exit      *******
**********************************
**********************************
请选择:>1
请输入2个操作数:>2 3
ret = 5
**********************************
**********************************
*******        1.add       *******
*******        2.sub       *******
*******        3.mul       *******
*******        4.div       *******
*******        0.exit      *******
**********************************
**********************************
请选择:>3
请输入2个操作数:>2 3
ret = 6
**********************************
**********************************
*******        1.add       *******
*******        2.sub       *******
*******        3.mul       *******
*******        4.div       *******
*******        0.exit      *******
**********************************
**********************************
请选择:>0
退出计算器

D:\gtee\C-learning-code-and-project\test_813\Debug\test_813.exe (进程 7012)已退出,代码为 0。
按任意键关闭此窗口. . .

2.使用函数指针数组实现

int main()
{
	int input = 0;
	do
	{
		int x = 0;
		int y = 0;
		int ret = 0;
		menu();
		printf("请选择:>");//1
		scanf("%d", &input);

		int (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div};
		                            //0   1   2    3    4
		if (input == 0)
		{
			printf("退出计算器\n");
		}
		else if(input>=1 && input<=4)
		{
			printf("请输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = pfArr[input](x, y);
			printf("%d\n", ret);
		}
		else
		{
			printf("选择错误\n");
		}
	} while (input);
	return 0;
}

:bulb:运行结果:

**********************************
**********************************
*******        1.add       *******
*******        2.sub       *******
*******        3.mul       *******
*******        4.div       *******
*******        0.exit      *******
**********************************
**********************************
请选择:>1
请输入2个操作数:>2 3
5
**********************************
**********************************
*******        1.add       *******
*******        2.sub       *******
*******        3.mul       *******
*******        4.div       *******
*******        0.exit      *******
**********************************
**********************************
请选择:>3
请输入2个操作数:>2 3
6
**********************************
**********************************
*******        1.add       *******
*******        2.sub       *******
*******        3.mul       *******
*******        4.div       *******
*******        0.exit      *******
**********************************
**********************************
请选择:>0
退出计算器

D:\gtee\C-learning-code-and-project\test_813\Debug\test_813.exe (进程 16872)已退出,代码为 0。
按任意键关闭此窗口. . .

5.回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
比如上述计算器中的Calc函数,这个函数通过传入的函数指针,去找到Add Sub Mul Div中的一个函数并调用它。

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

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

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

int Div(int x, int y)//int (*)(int, int)
{
	return x / y;
}

void Calc(int(*pf)(int, int))
{
	int x = 0;
	int y = 0;
	int ret = 0;

	printf("请输入2个操作数:>");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);//通过函数指针返回调用调用加减乘除函数
	printf("ret = %d\n", ret);
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。