【C语言进阶】第二节:指针

1、字符指针

int main()
{
	const char* pstr = "hello world";//这里是把一个字符串放到pstr指针变量里了吗?
	printf("%s\n", pstr);
	return 0;
}

上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。

 2、指针数组

在初阶我们学习过,指针数组是一个存放指针的数组。

3、数组指针

3.1 数组指针的定义

数组指针是指针,是能够指向数组的指针

下面代码哪个是数组指针?

int *p1[10];
int (*p2)[10];

[]的优先级比*高,所以p1先和[]结合组成一个数组,所以p1是一个指针数组。

p2是数组指针,因为p先和*结合,说明p是一个指针变量,然后指向的是一个大小为10的整型数组。

3.2 数组指针的使用

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

int main()
{
	int arr[10] = { 0 };
	int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
	//但是我们一般很少这样写代码
	return 0;
}

看下面一个例子,程序输出的结果是什么?

int main()
{
	int aa[2][5] = { 10,9,8,7,6,5,4,3,2,1 };
	int* ptr1 = (int*)(&aa + 1);
	int* ptr2 = (int*)(*(aa + 1));
	printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
	return 0;
}

&aa取出来的是整个数组的地址,+1跳过整个数组,所以ptr1指向数组最后一个元素的下一个位置。

aa的类型是int (*)[5],表示指向5个整型元素的数组指针。
aa+1将指针移动到aa[1],即第二行的起始地址。此时它指向的是一个完整的数组(第二行)的起始位置。
使用*解引用这个指针,将它转换为int*,指向第二行的第一个元素。

输出结果:1,6

使用:

void print_arr1(int arr[3][5], int row, int col)
{
	int i = 0;
	for (i = 0; i < row; i++)
	{
        int j = 0;
		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;
	for (i = 0; i < row; i++)
	{
        int j = 0;
		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,0 };
	print_arr1(arr, 3, 5);
	//数组名arr,表示首元素的地址
	//但是二维数组的首元素是二维数组的第一行
	//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
	//可以数组指针来接收
	print_arr2(arr, 3, 5);
	return 0;
}

4、数组参数、指针参数

4.1 一维数组的传参

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 };
	test(arr);
	test2(arr2);
}

4.2 二维数组的传参

void test(int arr[3][5])//与void test(int arr[][5])等效
{}
void test(int arr[][])//错误,编译器无法确定每一行的元素个数,导致无法正确计算元素的内存地址
{}
void test(int arr[][5])
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。

void test(int* arr)//错误,int*与int (*)[5]是不同的类型,无法进行隐式转换
{}
void test(int* arr[5])//错误,这是一个指针数组,实际上是int**,与int (*)[5]不同,无法进行隐式转换,导致编译错误
{}
void test(int(*arr)[5])//数组指针正确
{}
void test(int** arr)//错误,同上
{}

int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

4.3 一级指针传参

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

//比如
void test(char* p)
{}

char ch = '2';
char* ptr = &ch;
char arr[] = "abcdef";

test(&ch);
test(ptr);
test(arr);

4.4 二级指针传参

当函数的参数为二级指针的时候,可以接收什么参数?

void test(char **p)
{}

char c = 'b';
char* pc = &c;
char** ppc = &pc;
char* arr[10];

test(&pc);
test(ppc);
test(arr);

5、函数指针

首先看一段代码:

void test()
{
	printf("haha\n");
}

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

输出的是两个地址,这两个地址使test函数的地址。 那函数的地址要想保存起来,该怎么保存呢? 下面我们看代码:

void test()
{
    printf("haha\n");
}

//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void* pfun2();

首先能存储地址,就要求pfun1或pfun2是指针。

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。

6、函数指针数组

//把函数的地址存到一个数组中,这个数组就叫函数指针数组,那函数指针数组如何定义呢?
int (*parr1[10])();
int* parr2[10]();
int (*)() parr3[10];

parr1:是函数指针数组
parr1先和[]结合,表示是一个数组,包含10个元素。
*parr1[10]表示数组中的每个元素都是一个指针。
(*parr1[10])()表示这些指针指向的函数没有参数。
int表示这些函数返回一个int类型的值。

parr2:不合法

parr2被声明为一个数组,包含10个元素。
每个元素都是一个函数,这些函数返回int*,并且不接受任何参数。
但是不合法。在C语言中,数组不能包含函数。只能有指向函数的指针数组。

parr3:是函数指针数组

这是一种简化的声明方式,实际上与第一个声明int (*parr1[10])();等效。

函数指针数组的用途:

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("选择+-*/操作: ");
		scanf("%d", &input);
		if (input <= 4 && input >= 1)
		{
			printf("请输入操作数: ");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
			//或者ret = p[input(x, y);
			printf("结果是:%d\n", ret);
		}
		else
		{
			printf("输入错误\n");
		}
	}
	return 0;
}

7、回调函数

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

qsort函数的使用: 

int int_cmp(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}

int main()
{
	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}
struct Stu
{
	char name[20];
	int age;
};

struct Stu arr[] = { 
	{"zhangsan", 20},
	{"lisi", 50},
	{"wangwu", 15} };

int cmp_stu_by_age(const void* p1, const void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
void test1()
{
	qsort(arr, 3, sizeof(arr[0]), cmp_stu_by_age);
	for (int i = 0; i < 3; i++)
	{
		printf("%s: %d\n", arr[i].name, arr[i].age);
	}
}

int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
void test2()
{
	qsort(arr, 3, sizeof(arr[0]), cmp_stu_by_name);

	for (int i = 0; i < 3; i++)
	{
		printf("%s: %d\n", arr[i].name, arr[i].age);
	}
}
  • void*的指针 - 无具体类型的指针,void*类型的指针可以接收任意类型的地址。
  • 这种类型的指针是不能直接解引用操作,也不能直接进行指针运算。

qsort函数的模拟实现:

void swap(char* p1, char* p2, int size)
{
	for (int i = 0; i < size; i++)
	{
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		p1++;
		p2++;
	}
}

void my_qsort(void* arr, int num, int size, int(*cmp)(const void*, const void*))
{
	for (int i = 0; i < num - 1; i++)
	{
		for (int j = 0; j < num - 1 - i; j++)
		{
			if ((cmp((char*)arr + (j * size), (char*)arr + ((j + 1) * size)) > 0))
			{
				swap((char*)arr + (j * size), (char*)arr + (j + 1) * size, size);
			}
		}
	}
}

int cmp(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}

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

8、指针和数组笔试题

int main()
{
	//一维数组
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a + 0));
	printf("%d\n", sizeof(*a));
	printf("%d\n", sizeof(a + 1));
	printf("%d\n", sizeof(&a));
	printf("%d\n", sizeof(*&a));

	//字符串数组
	char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));
	//printf("%d\n", strlen(*arr));
	printf("%d\n", strlen(&arr));
	printf("%d\n", strlen(&arr + 1));

	//字符指针
	char* p = "abcdef";
	printf("%d\n", strlen(&p));

	//二维数组
	int aa[3][4] = { 0 };
	printf("%d\n", sizeof(aa));
	printf("%d\n", sizeof(aa[0] + 1));
	printf("%d\n", sizeof(*(aa[0] + 1)));
	printf("%d\n", sizeof(aa + 1));
	return 0;
}

答案及解析: 

int main()
{
	//一维数组
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a + 0));//4/8,a+0指向数组的首元素
	printf("%d\n", sizeof(*a));//4,*a等同于a[0]
	printf("%d\n", sizeof(a + 1));//4/8,a+1指向数组的第二个元素
	printf("%d\n", sizeof(&a));//4/8,&a是指向整个数组的指针,类型为int (*)[4]
	printf("%d\n", sizeof(*&a));//16,*&a等价于a,即整个数组

	//字符串数组
	char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));//7,最后要加上终止符'\0'
	//printf("%d\n", strlen(*arr));//编译错误,*arr等价于arr[0],类型为char
	printf("%d\n", strlen(&arr));//6,&arr的类型是char (*)[7],可隐式转换为char*
	printf("%d\n", strlen(&arr + 1));//随机值,&arr+1指向数组末尾下一个位置

	//字符指针
	char* p = "abcdef";
	printf("%d\n", strlen(&p));//随机值,&p的类型是char**,可隐式转换为char*
	//但&p指向的是指针本身的内存地址,而不是一个有效的字符串

	//二维数组
	int aa[3][4] = { 0 };
	printf("%d\n", sizeof(aa));//48
	printf("%d\n", sizeof(aa[0] + 1));//4/8
	//aa[0]表示数组首元素的地址,也就是aa[0][0]的地址
	//aa[0]+1,是aa[0][1]的地址
	printf("%d\n", sizeof(*(aa[0] + 1)));//4,*(aa[0] + 1)等价于aa[0][1]
	printf("%d\n", sizeof(aa + 1));//4/8,是指向aa[1]的指针,类型为int (*)[4]
	return 0;
}

总结:数组名的意义

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。

  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。

  3. 除此之外所有的数组名都表示首元素的地址。

8.1 练习1

int main()
{
	int a[5] = { 1, 2, 3, 4, 5 };
	int* ptr = (int*)(&a + 1);
	printf("%d,%d", *(a + 1), *(ptr - 1));
	return 0;
}

&a是指向整个数组的指针,类型为int (*)[5],但是我们强转成了int*

a+1是一个指针,指向数组的第二个元素,类型为int*

所以*(a+1)为2,*(ptr-1)为5。

8.2 练习2

struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p;
//假设p的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	return 0;
}

1. 是一个常规指针加法操作,p是指向struct Test的指针,sizeof(struct Test)的大小是20字节。当进行指针加法时,增加的量是按照指针所指向类型的大小来计算的。
p + 0x1 ⇒ 0x100000 + 0x1 × sizeof(struct Test) = 0x100014

2. 在这个表达式中,指针p转换为unsigned long类型,然后与1相加。这里的加法直接在指针的数值上进行,而没有考虑指针的数据类型的大小。
(unsignedlong)p + 0x1 ⇒ 0x100000 + 0x1 = 0x100001

8.3 练习3

int main()
{
	int a[4] = { 1, 2, 3, 4 };
	int* ptr1 = (int*)(&a + 1);
	int* ptr2 = (int*)((int)a + 1);
	int* ptr3 = a + 1;
	printf("%x,%x,%x", ptr1[-1], *ptr2, *ptr3);
	return 0;
}

%x是打印一个无符号整数的十六进制表示形式

1.定义数组和指针
数组a:
假设数组 a 的起始地址为 0x100000,则各元素的地址如下:
a[0] : 0x100000 → 值为 1 (0x00000001)
a[1] : 0x100004 → 值为 2 (0x00000002)
a[2] : 0x100008 → 值为 3 (0x00000003)
a[3] : 0x10000C → 值为 4 (0x00000004)

指针ptr1:
&a的类型是int (*)[4],即指向整个数组的指针。
因此,&a + 1的地址为0x100000 + 16 = 0x100010。
将其转换为int*类型,ptr1指向0x100010。

指针ptr2:
(int)a将数组a的地址转换为整数类型,即0x100000。
ptr2指向0x100001。

指针ptr3:
a + 1指向数组的第二个元素a[1],即0x100000 + 4 = 0x100004。

2.打印输出
ptr1[-1]等价于 *(ptr1 - 1),即 *(0x100010 - 4) = *(0x10000C)。
0x10000C是a[3]的地址,值为4。

ptr2指0x100001。在x86的小端存储中:
a[0] = 1,在内存中表示为0x01 0x00 0x00 0x00,位于 0x100000 到 0x100003。
ptr2指向0x100001,即第二个字节0x00。
读取int类型的数据意味着读取从0x100001开始的4个字节:0x00 0x00 0x00 0x02。
由于是小端模式,0x00 0x00 0x00 0x02表示0x02000000。

ptr3指向0x100004,即a[1]的地址。
a[1] = 2,在内存中表示为0x02 0x00 0x00 0x00。
因此*ptr3的值为2,以十六进制表示为 2。

输出结果:4,2000000,2

8.4 练习4

int main()
{
	int a[3][2] = { (0, 1), (2, 3), (4, 5) };
	int* p;
	p = a[0];
	printf("%d", p[0]);
	return 0;
}

初始化时的{ (0, 1), (2, 3), (4, 5) }是逗号表达式!实际初始化为1,3,5,0,0,0

所以a[0][0]的值为1

8.5 练习5

int main()
{
	int a[5][5];
	int(*p)[4];
	p = a;
	printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
	return 0;
}

输出结果:FFFFFFFC,-4

8.6 练习6

int main()
{
	char* c[] = { "ENTER","NEW","POINT","FIRST" };
	char** cp[] = { c + 3,c + 2,c + 1,c };
	char*** cpp = cp;
	printf("%s\n", **++cpp);
	printf("%s\n", *-- * ++cpp + 3);
	printf("%s\n", *cpp[-2] + 3);
	printf("%s\n", cpp[-1][-1] + 1);
	return 0;
}

 **++cpp:
++cpp:将cpp从cp[0]移动到cp[1]

*--*++cpp+3:
++cpp:将cpp从cp[1]移动到cp[2]
(*的优先级比加号高)*cpp:即c+1
--*cpp:将c+1减1,变为c+0
*--*cpp:得到"ENTER"
"ENTER"+3:指针偏移3个字符,指向字符串"ER"

*cpp[-2]+3:
cpp[-2]:等价于*(cpp-2),cpp-2指向cp[0]
*cpp[-1]:即c+3,指向"FIRST"
"FIRST"+3:指针偏移3个字符,指向字符串"ST"

cpp[-1][-1]+1:
cpp[-1]:等价于*(cpp-1),cpp-1指向cp[1]
cpp[-1][-1]:等价于*(*(cpp-1)-1),c+2-1等于c+1,指向"NEW"
"NEW"+1:指针偏移1个字符,指向字符串"EW"

所以输出结果:POINT ER ST EW

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值