C语言:指针详解续

一、字符指针变量

我们知道有种指针类型为字符指针(char*)。

#include <stdio.h>
int main()
{
	char ch = 'w';
	char* pch = &ch;
	printf("%c\n", *pch);
	return 0;
}

其实它还有一种使用方式。

#include <stdio.h>
int main()
{
	char* pstr = "hello world";
	printf("%s\n", pstr);
	return 0;
}

在代码 char* pstr = "hello world"; 中,指针 pstr 存放的正是字符串 "hello world" 首字符 'h' 的地址。在 C 语言里,字符串是以字符数组的形式存储在内存中的,并且以 '\0'(空字符)作为字符串结束的标志。当定义一个指针指向一个字符串常量时,该指针的值就是这个字符串存储在内存中起始位置(也就是首字符所在位置)的地址。当在printf函数中使用%s格式化符并且参数是一个字符指针(如char *pstr)时,printf函数会把这个指针当作字符串的起始地址。它会从这个地址开始读取字符,一直读取到遇到'\0'(字符串结束标志)为止。例如,在代码char* pstr="hello world"; printf("%s\n", pstr);中,pstr指向字符串"hello world"的首字符'h'printf会从这个地址开始,逐个字符地输出,直到遇到'\0'才停止。

现在我们来看一看《剑指offer》中的一道题。

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

我们来分析一下结果为什么是这样的。当我们使用char数组存放字符串时,就会为数组分配独立的内存空间来存放字符串,所以str1和str2都指的是这两个数组的首字符的地址,即使字符串相同,所对应的地址也不相同。在 C 语言中,像 "hello" 这样的常量字符串在程序中通常只存储一份在只读的内存区域。当定义 str3 和 str4 这两个指针并让它们指向同一个常量字符串"hello" 时,它们实际上都指向了这个字符串的起始地址。所以当进行 str3 == str4 比较时,比较的是两个指针所存储的地址值,因为它们都指向同一个 "hello" 字符串所在的内存位置,所以地址是相同的。其实造成差异的本质原因是前两个都是在定义字符串变量,也就是字符数组,它本身是没有地址的,而后两个常量字符串本身就是有地址的,这里只是将地址取出来存放在指针中。

二、数组指针变量

数组指针变量是一种指针变量,存放的应该是数组的地址,能够指向数组的指针变量。
int(*p)[10];

这就是数组指针的形式。p是这个变量的名字,*说明这个变量是指针,还剩下int [10]就说明它指向的对象是大小为10的整型数组类型的。

注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。否则这个变量就不是数组指针了,而是指针数组。

那么数组指针变量怎么初始化呢?其实和我们之前初始化指针变量一样。

我们调试也能看到 &arr p 的类型是完全一致的。
三、二 维数组传参的本质
#include<stdio.h>
void test(int arr[2][3],int a,int b)
{
	for (int i = 0; i < a; i++)
	{
		for (int j = 0; j < b; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[2][3] = { {1,2,3},{2,3,4} };
	test(arr,2,3);
	return 0;
}

之前我们使用二维数组传参都是如上图的代码这样去进行操作的。其实二维数组就是一维数组的数组,也就是二维数组的每个元素是一个一维数组。所以二维数组的数组名表示的就是第一行的地址,是一个一维数组的地址。根据上面的例子,第一行的一维数组的类型就是 int [3] ,所以第一行的地址的类型就是数组指针类型 int(*)[3] 。那就意味着二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址,那么形参也是可以写成指针形式的。如下:

#include<stdio.h>
void test(int(*arr)[3], int a, int b)
{
	for (int i = 0; i < a; i++)
	{
		for (int j = 0; j < b; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[2][3] = { {1,2,3},{2,3,4} };
	test(arr,2,3);
	return 0;
}

四、函数指针变量

函数指针变量就是存放函数地址的指针变量。我们将刚才的代码稍微改动一下,我们就会发现函数其实有自己的地址。而且函数名就是函数的地址,当然使用&函数名的方式也可以。
那么函数指针变量是什么样的呢?我们再将代码改动一下。
#include<stdio.h>
void test(int(*arr)[3], int a, int b)
{
	for (int i = 0; i < a; i++)
	{
		for (int j = 0; j < b; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[2][3] = { {1,2,3},{2,3,4} };
	test(arr,2,3);
	void (*p)(int(*arr)[3], int a, int b) = test;
	return 0;
}

p是变量名,*表示这个变量是指针,前面的void是函数的返回值,后面加的是函数的参数。而且在参数中可以省略具体的变量名,只留下参数的数据类型。在这里就是将a,b去掉,只留下(int,int)。那我们怎么去使用这个函数指针变量呢?

#include<stdio.h>
int add(int a, int b)
{
	return a + b;
}
int main()
{
	int (*p)(int, int) = add;
	printf("%d", p(1, 2));
	return 0;
}

使用函数指针名加参数就能实现,当然将函数指针名解引用后加参数也能实现,但是直接使用函数指针名编译器就可以理解这是对函数指针所指向的函数的调用,使用起来会更加方便,一般不推荐显式解引用来使用函数。

接下来,我们来看两段c陷阱与缺陷中的代码。当然,这些代码只是为了让我们更加理解指针等知识,其实某些写法并不推荐。

1.(*(void (*)())0)();

来分析一下代码,我们能看到中间的void (*)(),知道这是一个函数指针,参数为空,返回值为void,后面还跟着一个0,那我们就可以知道,这里是将0强制类型转换了,将它变成了函数指针类型,然后将整体解引用,后面再加上空参,就是在调用函数。

2.void (*signal(int , void(*)(int)))(int);

看到signal(int , void(*)(int))应该就能知道这是一个函数名加上参数,一个是整型类型的,一个是函数指针类型的。其实,剩下的void(*)(int)是这个函数的返回值,这种写法确实比较奇怪,我们在这里也不需要多纠结,这个代码整体就是一个函数的声明。

五、typedef 关键字

typedef 是用来类型重命名的,可以将复杂的类型,简单化。比如,你觉得 unsigned int 写起来不方便,如果能写成 uint 就方便多了,那么我们可以使用:
typedef unsigned int uint;
//将unsigned int重命名为uint

一般类型都可以这样进行操作,但是对于数组指针和函数指针稍微有点区别,新的类型名必须在*的右边

#include <stdio.h>
int add(int x, int y)
{
	return x + y;
}
int main()
{
	int arr[5] = { 0 };
	typedef int(*parr_t)[5];//int(*)[5]->parr_t
	typedef void(*pf_t)(int);//void(*)(int)->pf_t
	parr_t p1 = arr;
	pf_t p2 = add;
	return 0;
}

六、函数指针数组

要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?是 int (*)() 类型的函数指针。
int (*parr1[3])();

那么函数指针数组有什么用呢,接下来我们来看转移表。举例:计算器的⼀般实现:

#include<stdio.h>
void menu()
{
	printf("***0.exit***\n");
	printf("***1.add****\n");
	printf("***2.sub****\n");
	printf("***3.mul****\n");
	printf("***4.div****\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 main()
{
	int n;
	int(*p[5])(int, int) = { 0,add,sub,mul,div };
	while (1)
	{
		menu();
		scanf("%d", &n);
		if (n > 0 && n < 5)
		{
			int a, b;
			printf("请输入两个操作数...\n");
			scanf("%d%d", &a, &b);
			printf("结果是:%d\n", p[n](a, b));
		}
		else if (n == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else
			printf("输入有误\n");
	}
	return 0;
}

如上图的代码,我们就能实现简易的加减乘除计算器,这就使用了函数指针数组。那么转移表又是什么呢?其实它指的就是运用函数指针数组以数组方式去调用里面的函数,使代码变得更加简洁。

七、回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。 我们可以把调用的函数的地址以参数的形式传递过去,使用函数指针接收,函数指针指向什么函数就调用什么函数。下面我们就使用回调函数来实现上面说的简易计算器。
#include<stdio.h>
void menu()
{
	printf("***0.exit***\n");
	printf("***1.add****\n");
	printf("***2.sub****\n");
	printf("***3.mul****\n");
	printf("***4.div****\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;
}
void is(int(*p)(int, int))
{
	int a, b;
	printf("请输入两个操作数...\n");
	scanf("%d%d", &a, &b);
	printf("结果是:%d\n", p(a, b));
}
int main()
{
	int n;
	int(*p[5])(int, int) = { 0,add,sub,mul,div };
	while (1)
	{
		menu();
		scanf("%d", &n);
		if (n > 0 && n < 5)
		{
			switch (n)
			{
			case 1:
			{
				is(add);
				break;
			}
			case 2:
			{
				is(sub);
				break;
			}
			case 3:
			{
				is(mul);
				break;
			}
			case 4:
			{
				is(div);
				break;
			}
			}
		}
		else if (n == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else
			printf("输入有误\n");
	}
	return 0;
}

八、qsort 使用举例

qsort的作用是对数组的元素进行排序,使用 compar 函数确定顺序,将base 指向的数组的 num 个元素进行排序,每个元素大小为size。该函数不返回任何值,但通过对 compar 定义的元素进行基数重新排序来修改指向的数组的内容。我们能发现参数中的指针都是void*类型的,这是因为我们进行操作的数据类型不是固定的。

我们先来看一下compar 函数,它可以比较元素对,并将指向它们的指针作为参数。其返回值如下图,一般可以简化成小于返回-1,等于返回0,大于返回1。

1.使用qsort函数排序整型数据

#include<stdio.h>
#include<stdlib.h>
int int_compar(const void* p1, const void* p2)
{
	return *((int*)p1) - *((int*)p2);
}
int main()
{
	int arr[] = { 4,3,6,12,8,2,9,7,1,5 };
	qsort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(int), int_compar);
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

2.使用qsort排序结构数据

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{
	char name[20];
	int age;
};
int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_stu_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test1()
{
	struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
void test2()
{
	struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{
	test1();
	test2();
	return 0;
}

通过调试我们可以看出结构体中的数据确实被排序了。

3.qsort函数的模拟实现

使用回调函数,模拟实现qsort(采用冒泡的方式)。
#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size)
{
	int i = 0;
	for (i = 0; i < size; i++)
	{
		char tmp = *((char*)p1 + i);
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = tmp;
	}
}
void bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < count - 1; i++)
	{
		for (j = 0; j < count - i - 1; j++)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
			{
				_swap((char*)base + j * size, (char*)base + (j + 1) * size,size);
			}
		}
	}
}
int main()
{
	int arr[5] = { 2,3,4,1,5 };
	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;
}

九、sizeof和strlen的对比

1.sizeof

sizeof 计算变量所占内存内存空间大小的,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。sizeof 只关注占用内存空间的大小,不在乎内存中存放什么数据。操作数是变量可以不加括号。
#include <stdio.h>
int main()
{
	int a = 10;
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof a );
	printf("%d\n", sizeof(int));
	return 0;
}

2.strlen

strlen 是C语言库函数,功能是求字符串长度。函数原型如下:
size_t strlen(const char* str);
统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。
strlen 函数会⼀直向后找 \0 字符,直到找到为止,所以可能存在越界查找。
3. sizeof 和 strlen的对比
sizeof:
(1)  sizeof是操作符
(2) sizeof计算操作数所占内存的大小,单位是字节
(3) 不关注内存中存放什么数据
strlen:
(1) strlen是库函数,使用需要包含头文件 string.h
(2) srtlen是求字符串长度的,统计的是 \0 之前字符的个数
(3) 关注内存中是否有 \0 ,如果没有 \0 ,就会持续往后找,可能
会越界
下面还有一些题目,可以使我们理解得更加深刻。
#include <stdio.h>
#include <string.h>
int main()
{
	//在x64环境下运行
	int a[] = { 1,2,3,4 };
	printf("%zd\n", sizeof(a));//16
	printf("%zd\n", sizeof(a + 0));//8
	printf("%zd\n", sizeof(*a));//4
	printf("%zd\n", sizeof(a + 1));//8
	printf("%zd\n", sizeof(a[1]));//4
	printf("%zd\n", sizeof(&a));//8
	printf("%zd\n", sizeof(&a + 1));//8
	printf("%zd\n", sizeof(&a[0]));//8
	printf("%zd\n", sizeof(&a[0] + 1));//8
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%zd\n", sizeof(arr));//6
	printf("%zd\n", sizeof(arr + 0));//8
	printf("%zd\n", sizeof(*arr));//1
	printf("%zd\n", sizeof(arr[1]));//1
	printf("%zd\n", sizeof(&arr));//8
	printf("%zd\n", sizeof(&arr + 1));//8
	printf("%zd\n", sizeof(&arr[0] + 1));//8
	char arr2[] = { 'a','b','c','d','e','f' };
	printf("%zd\n", strlen(arr2));//随机
	printf("%zd\n", strlen(arr2 + 0));//随机
	//printf("%zd\n", strlen(*arr2));//err
	//printf("%zd\n", strlen(arr2[1]));//err
	printf("%zd\n", strlen(&arr2));//随机
	printf("%zd\n", strlen(&arr2 + 1));//随机
	printf("%zd\n", strlen(&arr2[0] + 1));//随机
	char arr3[] = "abcdef";
	printf("%zd\n", sizeof(arr3));//7
	printf("%zd\n", sizeof(arr3 + 0));//8
	printf("%zd\n", sizeof(*arr3));//1
	printf("%zd\n", sizeof(arr3[1]));//1
	printf("%zd\n", sizeof(&arr3));//8
	printf("%zd\n", sizeof(&arr3 + 1));//8
	printf("%zd\n", sizeof(&arr3[0] + 1));//8
	char arr4[] = "abcdef";
	printf("%zd\n", strlen(arr4));//6
	printf("%zd\n", strlen(arr4 + 0));//6
	//printf("%zd\n", strlen(*arr4));//err
	//printf("%zd\n", strlen(arr4[1]));//err
	printf("%zd\n", strlen(&arr4));//6
	printf("%zd\n", strlen(&arr4 + 1));//随机
	printf("%zd\n", strlen(&arr4[0] + 1));//5
	char* p = "abcdef";
	printf("%zd\n", sizeof(p));//8
	printf("%zd\n", sizeof(p + 1));//8
	printf("%zd\n", sizeof(*p));//1
	printf("%zd\n", sizeof(p[0]));//1
	printf("%zd\n", sizeof(&p));//8
	printf("%zd\n", sizeof(&p + 1));//8
	printf("%zd\n", sizeof(&p[0] + 1));//8
	char* p2 = "abcdef";
	printf("%zd\n", strlen(p2));//6
	printf("%zd\n", strlen(p2 + 1));//5
	//printf("%zd\n", strlen(*p2));//err
	//printf("%zd\n", strlen(p2[0]));//err
	printf("%zd\n", strlen(&p2));//随机
	printf("%zd\n", strlen(&p2 + 1));//随机
	printf("%zd\n", strlen(&p2[0] + 1));//5
	int a2[3][4] = { 0 };
	printf("%zd\n", sizeof(a2));//48
	printf("%zd\n", sizeof(a2[0][0]));//4
	printf("%zd\n", sizeof(a2[0]));//16
	printf("%zd\n", sizeof(a2[0] + 1));//8
	printf("%zd\n", sizeof(*(a2[0] + 1)));//4
	printf("%zd\n", sizeof(a2 + 1));//8
	printf("%zd\n", sizeof(*(a2 + 1)));//16
	printf("%zd\n", sizeof(&a2[0] + 1));//8
	printf("%zd\n", sizeof(*(&a2[0] + 1)));//16
	printf("%zd\n", sizeof(*a2));//16
	printf("%zd\n", sizeof(a2[3]));//16
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值