C语言指针从入门到进阶

C语言指针从入门到进阶

指针定义

在计算机科学中,指针是编程语言中的一个对象,利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”(指针是一个变量,用来存放地址)。意思是通过它能找到以它为地址的内存单元(内存空间编址是以一个字节为一个地址)。

int a = 10;  // 变量a占用了4个地址空间
int *p = &a; //取a的地址,是拿到4个地址空间的起始位置地址
// 将此地址存放到 p 中,此时p就是指针变量, int * 是它的类型
// * 说明该变量是一个指针变量,存放的是地址,int 说明该地址指向的是一个整型变量
*p = 20; // 指针的使用

在32位机器上,地址是32个0或者1组成的二进制序列,那地址就得用4个字节的空间去存储,指针变量的大小就应该是4个字节。如果在64位机器上,有64根地址线,那一个指针变量的大小是8个字节。

指针和指针类型

	printf("%d\n", sizeof(char *));
	printf("%d\n", sizeof(short *));
	printf("%d\n", sizeof(int *));
	printf("%d\n", sizeof(long *));
	printf("%d\n", sizeof(float *));
	printf("%d\n", sizeof(double *));
	// 32 位机上输出全都是4,64位机上输出为8

为什么所有类型的指针大小输出相同,还要将指针分为这么多类型呢?指针类型的存在还是自有意义的:

int a = 10;  // 将10存放到a在的地址空间
int *p = &a;
*p = 0;  // 将p所指的地址空间内容修改为0

// 若创建的指针类型不是 int
int a = 0x11223344;
char *p = &a;  // 警告,类型不匹配,但是 p 有能力存放 a 的地址
*p = 0;  // 原先的地址空间中存放的是 11223344,修改后现在存放的内容是 11223300

1、说明,int *char *每次在解引用时决定访问的空间大小。

int a = 10;
int *p1 = &a;
char *p2 = &a;
printf("%p\n", p1);
printf("%p\n", p1+1);
printf("%p\n", p2);
printf("%p\n", p2+1);

2、指针类型的不同,还决定了每次指针前进一步跨越的字节大小。

字符指针

字符指针即类型为char*的指针。

// 使用字符指针存放一个字符的地址
char ch = 'a';
char* pc = &ch;
*pc = 'b';

// 使用字符指针存放一个字符串的首字符地址
char* pstr = "hello";
printf("%s\n", pstr);
*pstr = 'w'; //报错,pstr指向的是一个常量字符串,该常量字符串不能被修改。
// 常量字符串“hello”是不能被修改的,将字符串的首字母地址交给 char*类型的指针变量,而该指针变量是不受保护的
// 所以上述写法是不够严谨的
const char* pstr = "hello";
// 此时加上const修饰*p,表示p指向的内容是不能被改变的
	char arr1[] = "abcdef";
	char arr2[] = "abcdef";
	
	char* p1 = "abcdef";
	char* p2 = "abcdef";
	// 常量字符串不会被修改,内存优化后只会保存一份
	
	if(arr1 == arr2)
	{
		printf("arr1 == arr2");
	}
	if(p1 == p2)
	{
		printf("p1 == p2");
	}
// 输出结果为 p1 == p2
// arr1 == arr2 判断的是两个数组的首元素地址是否相等
// 如果想比较两个字符串的内容是否相等,要用 strcmp

指针与数组名

int arr[10] = {0};
printf("%p\n", arr);  // 数组名是数组首元素地址
printf("%p\n", &arr[0]);  // 首元素的地址
printf("%p\n", &arr);  // 数组的地址
// 打印结果:
// 000000000065FDF0
// 000000000065FDF0
// 000000000065FDF0

虽然上述结果一样,但是一定要坚信三者是不同的以及数组名是数组首元素地址,值虽然一样,但是意义却不一样。

int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", arr+1);
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0]+1);
printf("%p\n", &arr);
printf("%p\n", &arr+1);
// 打印结果:
// 000000000065FDF0
// 000000000065FDF4

// 000000000065FDF0
// 000000000065FDF4

// 000000000065FDF0
// 000000000065FE18

通过上述结果可以看出,数组名的确是数组首元素地址,而非数组的地址。数组首元素的地址+1跳过的是一个元素,而数组的地址+1跳过的是整个数组。所以牢记arr是数组首元素地址,而&arr是数组的地址。

再看一段代码

printf("%d\n", sizeof(arr));  // 40
// arr 不是表示的是数组的首元素地址么,为什么此处打印结果为40?
// sizeof(arr) 和 &arr 两种情况下比较特殊,表示整个数组
// 除以上两种情况外,都表示数组首元素地址
int arr[10];
*arr; // 4字节,因为arr的类型为int*
*&arr; // 40字节,因为&arr的类型为int (*)[10];

指针运算

1、指针±整数及指针的关系运算

int arr[5];
int *p;
for(p=&arr[0]; p<&arr[5];)
{
	*p++ = 1;
}
// 进行数组的初始化

这里有一个细节,可以将上述代码改写为以下形式

	int arr[5];
	int *p;
	for(p=&arr[4]; p>=&arr[0]; )
	{
		*p-- = 1;
	}

实际上,上述代码在绝大部分的编译器上是可以顺利执行的,但是应该避免这样写,因为标准并不保证它可行。
规定,允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
2、指针-指针

	int arr[] = {1,2,3,4,5,6,7,8,9,0};
	printf("%d\n", &arr[9]-&arr[0]);
	// 结果为 9
	// 指针减去指针得到的是指针和指针之间元素的个数
	// 相减的两个指针必须来自同一块存储空间
// 指针-指针的应用,实现 strlen
// 库函数实现
char *p = "abcdef";
int len = strlen(p);
// 循环模拟实现
int my_strlen(char *str)
{
	int count = 0;
	while(*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}
// 递归模拟实现
int my_strlen(char *str)
{
	if(*str == 0)
		return 0;
	else
		return 1 + my_strlen(str+1);
}
// 指针模拟实现
int my_strlen(char *str)
{
	char *start = str;
	while(*str != '\0')
	{
		str++;
	}
	return str - start;
}

二级指针

int a = 10;
int *p = &a;  // p是一个指针变量,在内存中也有地址空间
int **pp = &p;  
// *说明p是一个指针变量,int说明指针指向的是一个int数据
// *说明pp也是一个指针变量,int *说明指针pp指向的是一个int *的数据

指针数组

指针数组是一个数组,顾名思义,是用来存放指针的数组。例如int *arr[5];指针数组中的每一个元素类型都是一个int *,代表一个指针类型。

// 用 char** 接收 char* 地址 
void print(char** arr, int sz) 
{
	int i = 0;
	for(i=0; i<sz; i++)
	{
		printf("%s\n", *(arr+i));
		//也也可以使用 printf("%s\n", arr[i]);
	}
}

int main()
{
	char* arr[3] = {"zhangsan", "lisi", "wangwu"};  // 数组中存放每个字符串的首字母地址
	int sz = sizeof(arr)/sizeof(arr[0]);
	print(arr, sz);  //将数组首元素地址传给形参,该地址是 char* 类型 
	
	return 0;
} 
	int arr1[] = {1,2,3,4,5};
	int arr2[] = {2,3,4,5,6};
	int arr3[] = {3,4,5,6,7};
	int arr4[] = {4,5,6,7,8};
	int i = 0;
	int j = 0;
	int* arr[4] = {arr1, arr2, arr3, arr4};
	for(i=0; i<4; i++)
	{
		for(j=0; j<5 ;j++)
		{
			printf("%d ", arr[i][j]);
			// 通过下标i拿到arr[i]对应的数组的首元素地址,再通过下标j找到该整型数组中的第j个元素
			// 看似是二维数组,其实不是
			// 实际上 arr[i][j] 是等于 *(*(arr+i)+j)
		}
		printf("\n");
	}

数组指针

int (*p)[10];
// p先和*结合,说明p是一个指针,然后指针指向的是一个大小为10个整型的数组
int arr[10];
int (*p)[10] = &arr;  // 数组的地址
// int (*p)[10] 中 int (*)[10]为数组指针类型
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int* p = arr;  //使用指针访问数组时,直接将数组首元素地址赋值给指针
int (*p)[10] = &arr;  //但是当整个数组的地址取出交给数组指针时,再用数组指针操作一维数组,就变得很别扭,如下
for(int i=0; i<10; i++)
{
	printf("%d ", (*p)[i]);
	// 或者 printf("%d ", *((*p)+i));
	// [i] 即 *(+i)
	// 可以看到在一维数组中使用数组指针会把问题变复杂,所以二维数组以上再使用数组指针会更好
}
// 二维数组使用数组指针
void print(int (*arr)[5], int x, int y)
{
	int i = 0;
	int j = 0;
	for(i=0; i<x; i++)
	{
		for(j=0; j<y; j++)
		{
			printf("%d ", *(*(arr+i)+j));
		}
		printf("\n");
	}
}


int main()
{
	int arr[3][5] = {1,2,3,4,5,2,3,4,5,6,3,4,5,6,7};
	print(arr, 3, 5);	// 二维数组的数组名在函数传参时,数组名表示的是第一行的地址,而第一行是大小为5的int数组
	return 0;
}
int arr[5];  // 整型数组,存放5个int
int *arr[10];  // 指针数组,存放10个int*
int (*arr)[10];  // 数组指针,指向大小为10,类型为int的数组
int (*arr[10])[5];  // arr与[10]先结合,是一个10个元素的数组,每个元素的类型为int (*)[5],即数组指针类型
// 即这是一个存放了10个指向大小为10,类型为int的数组指针的数组

数组参数与指针参数

将数组或指针传给函数。

// 一维数组传参
void test1(int arr[]){}

void test1(int arr[3]){}

void test1(int *arr){}

void test2(int *arr[3]){}

void test2(int **arr){}


int main()
{
	int arr1[3] = {1,2,3};
	int *arr2[3] = {0};
	test1(arr1);
	test2(arr2);
}
// 二维数组传参
void test(int *arr){}
// 错误

void test(int *arr[5]){}
// 错误

void test(int (*arr)[5]){}
// 可以,数组指针

void test(int **arr){}
// 错误

int main()
{
	int arr[3][5] = {0};
	test(arr);  // arr表示第一行地址,是一个数组的地址,数组的地址存放到数组指针中
}

函数指针

void test()
{
	printf("hello");
}

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

这里函数名取地址和单独使用函数名,都是取函数的地址的意思。打印结果相同。关于地址,我们需要对其进行解引用操作才能找到其对应的内容,但是在函数调用时,可以不对其地址解引用,直接使用函数名就能起到调用函数的作用。

	test();
	(*test)();
	// 两种方式都可以成功的调用test函数

函数的地址也需要指针来进行存储,下面以一函数为例,说明函数指针的声明方式:

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

int main()
{
	int (*p)(int, int) = &add;
	// 函数指针的声明,需要说明参数类型及返回值,p和*先结合,说明是一个指针,后面跟()说明是一个函数指针
	printf("%d\n", (*p)(1, 2));  // 函数指针的使用
	printf("%d\n", p(1, 2));  // 函数指针不需要解引用,*相当于摆设,可以省略
}

在使用函数指针时,要注意区分函数指针和函数的声明

int (*p)(int, int);  // 函数指针的声明
int *p(int, int);  // 函数的声明

两行有趣的代码,加深函数指针的理解

(*(void (*)())0)();  
// 调用0地址处,无参返回值类型为 void 的这样的一个函数
// (void (*)())为强制类型转换
void (*signal(int, void(*)(int)))(int);
// signal(int, void(*)(int))为函数名和参数,除此剩余部分为返回值类型
// 即,声明了一个signal函数,该函数的参数的类型为int和void(*)(int),函数的返回值类型为void(*)(int)
// 可以对以上写法进行化简
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

函数指针数组

parr 先和[]结合,说明parr是一个数组,该数组里面存放10个元素,元素的类型为int (*) ()

int (*parr[10])();  // 声明函数指针数组
int (*pfun[4])(int, int) = {add, div, sub, mul};  // 函数指针数组的应用:转移表,通过下标找到要跳转的函数的位置

指向函数指针数组的指针

int (*pfun[4])(int, int) = {add, div, sub, mul};  // 函数指针数组
int (* (*p)[4])(int, int) = &pfun;  // 指向函数指针数组的指针

举一个例子:

char* test1(int x, int* y){}

char* test2(int x, int* y){}

int main()
{
	char* (*p)(int, int*) = test1;  // 函数指针
	char* (*parr[2])(int, int*) = {test1, test2};  // 函数指针数组
	char* (*(*pparr)[2])(int, int*) = &parr;  //指向函数指针数组的指针
}

回调函数

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

在C语言库函数中,有一个用于各类型数据进行排序的快速排序函数qsort,其函数原型为

void qsort(void* base, size_t num, size_t width, int (*cmp)(const void* e1, const void* e2));

注意这里的 void* 类型,用于接收任意类型的指针

int a = 10;
void* p = &a;  // void*类型可以接收任何指针类型
*p;  // 非法的间接寻址,p是一个void*类型的指针时,解引用时无法确定访问多少字节
p+1;  // void*类型是未知大小的指针,也无法进行+1操作

举例如何使用qsort函数进行排序

struct Stu
{
	char name[20];
	int age;
};

int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

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()
{
	int arr[] = {9,8,7,6,5,4,3,2,1,0};
	int sz = sizeof(arr)/sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
}

void test2()
{
	struct Stu s[3] = {{"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15}};
	int sz = sizeof(s)/sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}

下面根据qsort函数调用的思想,模拟实现冒泡排序通用算法

void Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for(i=0;  i<width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

void bubble_sort(void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2))
{
	int i = 0;
	int j = 0;
	for(i=0; i<sz-1; i++)
	{
		for(j=0; j<sz-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);
			}
		}
	}
}

void test2()
{
	struct Stu s[3] = {{"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15}};
	int sz = sizeof(s)/sizeof(s[0]);
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);  
	// 这里的cmp_stu_by_age是函数指针,将该指针传递到bubble_sort函数中,再由bubble_sort函数调用cmp_stu_by_age,即回调函数
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页