目录
字符指针
int main()
{
char ch = 'a';
char* pc = &ch;
*pc = 'a';
return 0;
}
int main()
{
char* ps = "hello world";
printf("%s\n", ps);
return 0;
}
要注意,这其实是把这一串字符的首个字符的地址存到ps中去,而不是一整个字符串存进去。
#include<stdio.h>
int main()
{
char str1[] = "hello world";
char str2[] = "hello world";
const char* str3 = "hello world";
const char* str4 = "hello world";
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;
}
//结果
//str1 and str2 are not same
//str3 and str4 are same
我们可以看出虽然都是相同的字符,但是存入字符数组中时,每次都会开辟不同的空间。
但是字符串常量却是在相同的空间。原因就是二者分别是在内存的栈区和常量区。每次有新的变量是,栈区都会开辟新的空间,不管内容是否相同。但是字符串到常量区后,其内容不能被改变,所以相同内容的就不会多分配空间。
指针数组
数组指针
数组指针的定义
&数组名与数组名
我们已经知道数组名是首元素的地址,那么&数组名又和它有什么区别呢?
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
//结果
//0097F998
//0097F998
我们看到结果一样,但是再来看以下代码,结果便不一样了。
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr= %p\n", &arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr+1= %p\n", &arr+1);
return 0;
}
数组名加一,移动了四个字节的长度
&数组名加一,移动了四十个字节的长度
我们的数组是int类型,大小为10,可以得出结论:
数组指针的使用
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0, j = 0;
for (i = 0; i < row; i++)
{
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, j = 0;
for (i = 0; i < row; i++)
{
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,10 };
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
printf("\n");
print_arr2(arr, 3, 5);
return 0;
}
数组参数、指针参数
一维数组传参
#include <stdio.h>
//我们知道数组的传参有两种形式,一是数组的形式,二是指针的形式
//但其本质上都是以指针的形式传参的
void test(int arr[])
{}
void test(int arr[10])
{}
//上面两种是以数组的形式传参的,前者省略了数组大小,后者没有省略,二者都是可以的
void test(int* arr)
{}
void test2(int* arr[20])
{}
//上面两种是以指针的形式传参的,同上,都是可以的
void test2(int** arr)
{}
//arr2是指针数组,存放的类型是int*,所以传参时就要用二级指针来接收
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
二维数组传参
void test(int arr[3][5])//正确
{}
void test(int arr[][])//错误
{}
void test(int arr[][5])//正确
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int* arr)//错误
{}
void test(int* arr[5])//错误
{}
void test(int(*arr)[5])//正确
{}
void test(int** arr)//错误
{}
//二维数组的数组名,是首元素的地址,也就是一维数组的地址
//所以只有第三个是对的,arr是数组指针
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
以上是数组传参,接下来是指针传参。
一级指针传参
#include <stdio.h>
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d\n", *(p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
二级指针传参
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int* p = &n;
int** pp = &p;
test(pp);
test(&p);
return 0;
}
函数指针
#include <stdio.h>
void test()
{
printf("hello\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
通过这段代码及其结果显示,我们可以发现函数也是有地址的。既然有地址,那么我们就可以把地址保存起来。那么如何保存呢?
void test()
{
printf("hehe\n");
}
void (*pfun)();
//这里的pfun便把test函数的地址保存了
//因为有括号,我们可以知道pfun先与*结合,此时就是指着变量
//在看类型,去掉名字pfun,void(*)() 就是其类型——函数指针,这里的返回类型是void,参数也是void
接下来让我们阅读两个代码,加深一下理解
(*(void (*)())0)();
//看到括号这么多的代码,我们首先把括号分开,方便我们看
//( * ( void(*)() ) 0 ) ()
//分开后就很好看了
//从内向外看,void(*)()不正是我们刚才所讲的函数指针类型吗?
//继续向外看,函数指针类型在整形0前面,那不就是把0的类型强制转换了吗?
//强制转换后,0从整形就变成了函数指针类型
//0此时已经不再是整数0了,就变成了指向一个函数的指针,也就是一个地址
//继续向外看,前面有*,这就是把0这个地址解引用,就是找到了这个函数
//最后,函数名(),这不就是函数调用吗?
//总结,以上代码为函数调用,调用的是0作为地址处的函数
void (*signal(int , void(*)(int)))(int);
//分开括号 void( * signal ( int , void(*)(int) ) )(int);
//最内部int与void(*)(int)这两个类型在一个括号中,我们很容易就想到函数函数声明,signal便是函数名
//往外看,函数名与*没有括号结合,这就排除了函数指针
//于是我们将 signal ( int , void(*)(int) )这函数声明取出,便剩下了void (*)(int) 这部分
//函数的声明既要有参数类型,还要有返回类型,那么返回类型只能是void (*)(int)
//总结,以上代码为函数声明,函数返回类型是void (*)(int),参数类型和int和void (*)(int)
//但是这样写是很复杂的,类型可以用typedef重命名,我们就可以简化以上代码
//typedef int MyType;
//正常情况就是把 int 重新命名成MyType这样的形式
//但是函数指针类型冲命名时,不能简单类比,它这么高级,肯定有特殊之处
//typedef void (* MyType)(int);
//这就是把void (*)(int)重新命名成MyType
//简化以上代码为 MyType signal(int ,MyType);
函数指针数组
根据我们的经验,这一眼看过去就是数组,在里面放函数指针的数组,书写应该也大致是长这个样子 int (*parr1[10])(); 但是这有什么具体的用处呢?
函数指针数组的用途:转移表。这是个很重要的概念,我们通过代码介绍一下。
//该计算机就是为了举例子,不考虑浮点数的计算等
#include <stdio.h>
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;
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
我们可以看出,在case语句中有大量的代码冗余——代码基本一样,但多次重复出现。
通过使用函数指针数组优化代码:
#include <stdio.h>
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;
//数组中第一个0是为了占位,因为计算器功能从1开始的
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
while (input)
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
//先通过数组的下标找到了功能函数地址
//找到函数地址解引用就可以使用函数(不用解引用也可以使用)
ret = (*p[input])(x, y);
}
else
printf("输入有误\n");
printf("ret = %d\n", ret);
}
return 0;
}
指向函数指针数组的指针
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
回调函数

另外,需要补充的一点,qsort是可以对所有数据可以排序的,不管是整形数据,浮点型数据,字符串 还是结构体,只要比较函数可以写出来,那么qsort就都可以排序。
正是由于这个特性,我们可以观察到起始位置的指针和比较函数内元素的指针都是void*类型。这样的意义就是什么类型的数据都可以,可以理解为包容性。但是使用的时候不能直接使用,需要先强制类型转换成你使用的类型。
#include <stdio.h>
#include <stdlib.h>
//使用qosrt函数,得自己实现一个比较函数
int cmp(const void* p1, const void* p2)
{
//因为数组的类型是int型,所以先强制类型转换成int*
return (*(int*)p1 - *(int*)p2);
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}