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;
}
总结:数组名的意义
sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
除此之外所有的数组名都表示首元素的地址。
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) = 0x1000142. 在这个表达式中,指针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