字符指针
在指针的类型中我们知道有一种char*的字符指针的指针类型,一般使用如下:
int main()
{
char ch = 'a';
char* pc = &ch; // 取出ch的地址存放到pc中
*pc = 'q'; // 通过pc找到ch,并将其更改为字符q
printf("%c\n", *pc);
return 0;
}
还有一种使用的方式如下:
int main()
{
char* str = "hello world"; // 这种方式是把字符串hello world的首字符的地址放到了指针变量str中
printf("%c\n", *str); // h
printf("%s\n", str); // hello world
return 0;
}
上面代码的意思是把一个常量字符串的首字符的地址存放在指针变量str中
观察如下代码输出结果
int main()
{
char str1[] = "hello world";
char str2[] = "hello world";
char* str3 = "hello world";
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
原因:
用相同的常量字符串去初始化不同的数组时候会开辟出不同的内存块,所以str1和str2不同;
当几个指针指向相同的字符串的时候,只会在内存空间开辟一块空间来存储这个字符串,不同的指针同时指向这块空间字符串的首字符地址,所以str3和str4相同。
数组指针
之前我们学习了指针数组,先来复习下指针数组是什么意思?
指针数组,本质上是一个数组,数组中存放的元素是指针(地址)。
int* arr1[10]; // 一级整型指针的数组
char* arr2[10]; // 一级字符指针的数组
char** arr3[10]; // 二级字符指针的数组
指针数组的使用
示例一
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* arr[] = { &a, &b, &c };
for (int i = 0; i < 3; i++)
{
// 这里arr表示数组首元素的地址,也就是地址的地址,所以需要两次解引用操作
printf("%d ", *(*(arr+i))); // 等价于 printf("%d ", *arr[i]);
}
return 0;
}
示例二
int main()
{
int a[] = { 1,2,3,4,5 };
int b[] = { 2,3,4,5,6 };
int c[] = { 3,4,5,6,7 };
int* arr[3] = { a, b, c };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", *(*(arr+i)+j));
// 等价于 printf("%d ", *(arr[i]+j));
// 也等价于 printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
回顾完指针数组后,我们来看下数组指针
数组指针是一种指针–指向数组的指针
下面代码哪个是数组指针?
int * p1[10];
int (*p2)[10];
解释:
p1首先和[]结合,说明p1是一个数组,数组中有10个元素,每个元素的类型是int;
p2首先和*号结合,说明p2是一个指针变量,然后指向一个大小为10的整型数组,所以p2是一个指针,一个指向数组的数组指针。
这里需要注意:[]的优先级要高于*,所以要加上()来保证p2先与*号结合。
代码示例:
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int (*p)[10] = &arr; // &arr取出的是整个数组的地址,p就是一个数组指针,存放的是一个数组的地址
return 0;
}
&数组名和数组名
对于以下数组:
int arr[10];
arr和&arr分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr表示的是什么?
看如下代码:
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
我们发现输出结果一样,但它们所表示的含义也是一样的吗?
再看以下代码:
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
printf("%p\n", arr+1);
printf("%p\n", &arr+1);
return 0;
}
根据以上代码,我们可以发现,&arr和arr虽然值一样,但是意义是不一样的。
从以上代码我们可以看出arr+1增加了4,&arr+1增加了40;从中我们可以推断出&arr表示的是数组的地址。
总结:
数组名是数组首元素的地址;
但是有2个例外:1、sizeof(数组名),sizeof中单独放入数组名,数组名表示整个数组,计算的是整个数组的大小,单位是字节;2、&数组名,取出的是整个数组的地址。
数组指针的使用
数组指针在一维数组中的使用:
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int (*parr)[10] = &arr;
for (int i = 0; i < 10; i++)
{
printf("%d ", *(*parr + i));
}
printf("\n");
return 0;
}
数组指针在一维数组上使用十分的不便.
数组指针在二维数组中的使用:
// 二维数组的数组名表示首元素的地址,二维数组的首元素是第一行
// *parr找到的就是数组的第一行,相当于arr[0],又相当于一维数组的数组名
void print(int (*parr)[5], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", *((*parr + 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);
return 0;
}
数组参数 指针参数
一维数组传参
#include <stdio.h>
void test(int arr[])//ok? yes
{}
void test(int arr[10])//ok? yes
{}
void test(int *arr)//ok? yes 数组名也就是数组首元素地址
{}
void test2(int *arr[20])//ok? yes 指针数组传参指针数组接收
{}
void test2(int **arr)//ok? yes 数组名是数组首元素地址,数组的首元素是int*类型,所以使用int**接收
{}
int main()
{
int arr[10] = { 0 };
int *arr2[20] = { 0 };
test(arr);
test2(arr2);
}
二位数组参数
void test(int arr[3][5])//ok? yes
{}
void test(int arr[][])//ok? no
{}
void test(int arr[][5])//ok? yes
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。//这样才方便运算。
void test(int*arr)//ok? no 数组名是数组首元素地址,二维数组的首元素是第一行即arr[0],第一行的地址,所以要用数组指针来接收
{}
void test(int*arr[5])//ok? no
{}
void test(int(*arr)[5]) //ok? yes
{}
void test(int**arr)//ok? no
{}
intmain()
{
int arr[3][5] = { 0 };
test(arr);
}
一级指针传参
void print(int* p, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
}
int main()
{
int arr[10] = {0};
int *p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
// 一级指针p传参
print(p, sz);
return 0;
}
思考:
当一个函数的参数部分为一级指针的时候,函数能传入什么参数?
例如:
void test1(int*p)
{}
//test1函数能接收什么参数?
// 可以接收整型一维数组名;整型一级指针变量;int a = 10,可以接收 &a
void test2(char*p)
{}
//test2函数能接收什么参数?
// 字符一维数组名;字符一级指针变量;char ch = 'a', 可以接收 &ch
二级指针传参
#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;
}
思考:
当函数的参数为二级指针的时候,函数能传入什么参数?
// 能传入二级指针变量
// 能传入一级指针的地址
// 能传入存放一级指针的数组
void test(int** p2)
{
**p2 = 20;
}
int main()
{
int a = 10;
int *pa = &a;
int **ppa = &pa;
test(ppa);
test(&pa);
int *arr[10] = {0};
test(arr);
printf("%d\n", a);
return 0;
}
函数指针
函数指针指的是指向函数的指针,指针变量中存放的时函数的地址
先看一段代码:
void test()
{
printf("hello world\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出结果:
我们知道&test取出的是函数的地址,通过以上代码,我们发现函数名test等价于&test,他们含义也是相同的,这里不同于数组名和&数组名。
我们如何把函数的地址保存起来或者说存到什么变量里呢?
看如下代码:
void test()
{
printf("hello world\n");
}
// 下面pfun1和pfun2哪个有能力存放test函数地址呢?
void (*pfun1)();
void *pfun2();
首先要向存放test函数的地址,就要求是个指针,那pfun1和pfun2哪个是指针呢?
答案是pfun1;
pfun1首先与*结合,说明pfun1是个指针,指向一个函数,这个函数无参,返回值类型为void;而pfun2首先与()结合,说明pfun2是个函数,函数返回值类型为void*
代码:
int add(int x, int y)
{
return x + y;
}
int main()
{
// pfun就是一个函数指针变量
int (*pfun)(int, int) = &add; // 等价于 int (*pfun)(int, int) = add ; // 因为 add 等价于 &add
//int ret = (*pfun)(3, 5);
int ret = pfun(3, 5); // 因为pfun相当于&add,add 等价于 &add,所以pfun相当于add
printf("ret = %d\n", ret);
return 0;
}
阅读以下两段有趣的代码
(*(void (*)())0)();
void (*signal(int, void (*)(int)))(int);
解析:
第一段代码作用是调用0地址处的函数,该函数无参,返回值类型void
void (*)() 函数指针类型
void (*)()0 对0进行强制类型转化,被解释为一个函数地址
*(void (*)())0 对0地址进行解引用操作,找到0地址处的函数
(*(void (*)())0)() 调用0地址处的函数
第二段代码
signal首先和()结合,说明signal是一个函数名
signal函数的第一个参数是int,第二个参数是函数指针类型
该函数指针指向一个参数为int,返回值为void的函数
signal函数的返回值类型也是一个函数指针类型
该函数指针也指向一个参数为int,返回值为void的函数
综上signal是一个函数的声明
上面那个signal的函数声明,如上所写,我们不是很好去理解,我们通常比较好理解形式是如下:
void (*)(int) signal ( int, void (*)(int));
返回值类型 函数名 参数1 参数2
这样就比较好理解了,但是C语言语法不支持如此书写!
我们如果想简化可以使用typedef将void (*)(int)进行类型重定义。
typedef void(*)(int) pfun_t; // 这样也是不支持的
// 我们需要写成如下形式
typedef void(*pfun_t)(int);
然后,我们就可以简化成这样:
pfun_t signal(int, pfun_t);
函数指针数组
函数指针数组是指存放函数指针的数组
把函数的地址存放到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];
以上只有parr1是函数指针数组的定义,parr1首先与[]结合,说明parr1是一个数组,数组中有10个元素,每个元素的类型是 int (*) () 的函数指针。
函数指针数组的用途:转移表
我们先用如下代码,实现一个简单的整数加减乘除的计算器:
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 menu()
{
printf("*********************************\n");
printf("******** 1.add 2.sub ********\n");
printf("******** 3.mul 4.div ********\n");
printf("******** 0.exit ********\n");
printf("*********************************\n");
}
int main()
{
int input = 0;
int ret = 0;
int x = 0;
int y = 0;
do
{
menu();
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;
}
以上代码我们发现十分的冗余!
使用函数指针数组改进如下:
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 menu()
{
printf("****************************\n");
printf("***** 1.add 2.sub *****\n");
printf("***** 3.mul 4.div *****\n");
printf("***** 0.exit *****\n");
printf("****************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
int(*arr[5])(int, int) = {NULL, add, sub, mul, div};
do
{
menu();
printf("请输入>:");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数>:");
scanf("%d %d", &x, &y);
ret = arr[input](x, y);
printf("ret = %d\n", ret);
}
else if(input == 0)
{
printf("退出程序!\n");
}
else
{
printf("输入有误,请重新输入!\n");
}
} while (input);
return 0;
}
指向函数指针数组的指针
指向函数指针数组的指针是一个指针,指向一个数组,数组中的元素都是函数指针
如何定义:
int (*p)(int, int) // 函数指针
int (*arr[10])(int, int) // 函数指针数组
int (* (*parr)[10] )(int, int) = &arr // 取出函数指针数组的地址,parr就是一个指向函数指针数组的指针
回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被从来调用其所指向的函数时,我们就说这是回调函数。
我们利用回调函数将上面使用switch语句实现的简单整数计算器改进下:
void menu()
{
printf("******************************\n");
printf("******* 1.add 2.sub ******\n");
printf("******* 3.mul 4.div ******\n");
printf("******* 0.exit ******\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 calc(int(*pfun)(int, int))
{
int x = 0;
int y = 0;
printf("请输入两个操作数>:");
scanf("%d %d", &x, &y);
return pfun(x, y);
}
int main()
{
int input = 0;
do
{
menu();
int x = 0;
int y = 0;
int ret = 0;
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
ret = calc(add);
printf("ret = %d\n", ret);
break;
case 2:
ret = calc(sub);
printf("ret = %d\n", ret);
break;
case 3:
ret = calc(mul);
printf("ret = %d\n", ret);
break;
case 4:
ret = calc(div);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序!\n");
break;
default:
printf("输入有误!请重新输入!\n");
break;
}
} while (input);
return 0;
}
我们再来看一下qsort函数的使用:
qsort是一个通用的排序算法,它不光能排序整数,字符,结构体等数据也能够进行排序。
先来看一下它的各个参数的含义:
void qsort(void* base, // base中存放的是待排序数据中第一个对象的地址
size_t num, // 排序数据元素的个数
size_t size, // 排序数据中一个元素的大小,单位是字节
int(*compar)(const void*, const void*) // 是用来比较待排序数据中的2个元素的函数
);
我们先利用qsort函数来排序一个整型数组:
#include <stdlib.h>
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void print(int* arr, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", *(arr+i));
}
printf("\n");
}
int main()
{
int arr[10] = { 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);
print(arr, sz);
return 0;
}
再利用qsort函数来排序一个结构体数据:
根据年龄进行排序:
#include <stdlib.h>
struct Stu
{
char name[20];
int age;
};
// 通过年龄进行排序
int cmp_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
void print(struct Stu* p, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("姓名:%s, 年龄:%d\n", (p+i)->name, (p+i)->age);
}
}
int main()
{
struct Stu s[3] = { { "zhangsan", 30 }, { "lisi", 38 }, {"wangwu", 20} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_by_age);
print(s, sz);
return 0;
}
根据姓名进行排序:
#include <stdlib.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
};
// 通过姓名进行排序
int cmp_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void print(struct Stu* p, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("姓名:%s, 年龄:%d\n", (p + i)->name, (p + i)->age);
}
}
int main()
{
struct Stu s[3] = { { "zhangsan", 30 }, { "lisi", 38 }, { "wangwu", 20 } };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_by_name);
print(s, sz);
return 0;
}
我们之前写过冒泡排序,可以对一个整型数据进行升序或者降序,接下来,我们就仿照sqsort函数,来写一个通用的冒泡排序算法:
// 交换两个元素
void swap(char* buf1, char* buf2, int width)
{
for (int 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))
{
for (int i = 0; i < sz - 1; i++)
{
for (int j = 0; j < sz - 1 - i; j++)
{
// 比较两个元素
if (cmp((char*)base+width*j, (char*)base+width*(j+1)) > 0)
{
// 交换两个元素
swap((char*)base + width*j, (char*)base + width*(j + 1), width);
}
}
}
}
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void print(int* arr, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", *(arr+i));
}
printf("\n");
}
int main()
{
int arr[10] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
print(arr, sz);
return 0;
}
指针和数组的一些题目
// 一维数组
int main()
{
int a[] = {1,2,3,4};
printf("%d\n", sizeof(a)); //16 数组名单独放入sizeof中,计算的是整个数组的大小
printf("%d\n", sizeof(a+0));//4/8 这里数组名表示首元素地址
printf("%d\n", sizeof(*a));// 4 *a就是首元素1
printf("%d\n", sizeof(a+1));//4/8 这里数组名表示首元素地址
printf("%d\n", sizeof(a[1]));//4 a[1]就是数组中元素2,等价于*(a + 1)
printf("%d\n", sizeof(&a));//4/8 &a数组名取出的时整个数组的地址,类型为 int(*)[4]
printf("%d\n", sizeof(*&a));//16 &a取出整个数组的地址,又解引用,找到整个数组
printf("%d\n", sizeof(&a+1));//4/8
printf("%d\n", sizeof(&a[0]));//4/8 a[0]表示数组的首元素,&a[0]表示数组首元素的地址
printf("%d\n", sizeof(&a[0]+1));// 4/8 数组第二个元素的地址
return 0;
}
// 字符数组
#include <string.h>
int main()
{
char arr[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
printf("%d\n", sizeof(arr)); //6 数组名单独放入sizeof中,计算的是整个数组的大小
printf("%d\n", sizeof(arr+0));// 4/8 这里数组名表示首元素地址
printf("%d\n", sizeof(*arr));//1 这里数组名表示首元素地址
printf("%d\n", sizeof(arr[1]));//1 等价于*(arr+1)
printf("%d\n", sizeof(&arr));//4/8 取出的时整个数组的地址
printf("%d\n", sizeof(&arr+1));//4/8
printf("%d\n", sizeof(&arr[0]+1));//4/8 第二个元素的地址
printf("--------------------------------");
printf("%d\n", strlen(arr));//随机值
printf("%d\n", strlen(arr+0));//随机值
printf("%d\n", strlen(*arr)); // error
printf("%d\n", strlen(arr[1]));// error
printf("%d\n", strlen(&arr));// 随机值
printf("%d\n", strlen(&arr+1));// 随机值-6
printf("%d\n", strlen(&arr[0]+1));// 随机值-1
return 0;
}
// 字符数组
int main()
{
char arr[] = "abcdef";
printf("%d\n", sizeof(arr)); // 7
printf("%d\n", sizeof(arr+0));// 4/8
printf("%d\n", sizeof(*arr));// 1 字符a
printf("%d\n", sizeof(arr[1]));// 1 字符b
printf("%d\n", sizeof(&arr));// 4/8
printf("%d\n", sizeof(&arr+1));// 4/8
printf("%d\n", sizeof(&arr[0]+1));// 4/8
printf("---------------------------");
printf("%d\n", strlen(arr));// 6
printf("%d\n", strlen(arr+0));// 6
printf("%d\n", strlen(*arr));// error
printf("%d\n", strlen(arr[1]));// error
printf("%d\n", strlen(&arr));// 6
printf("%d\n", strlen(&arr+1));// 随机值
printf("%d\n", strlen(&arr[0]+1));// 5
printf("---------------------------");
char* p = "abcdef";
printf("%d\n", sizeof(p));// 4/8
printf("%d\n", sizeof(p+1));// 4/8
printf("%d\n", sizeof(*p));// 1
printf("%d\n", sizeof(p[0]));// 1
printf("%d\n", sizeof(&p));// 4/8
printf("%d\n", sizeof(&p+1));// 4/8
printf("%d\n", sizeof(&p[0]+1));// 4/8
printf("---------------------------");
printf("%d\n", strlen(p)); // 6
printf("%d\n", strlen(p+1));// 5
printf("%d\n", strlen(*p));// error
printf("%d\n", strlen(p[0]));// error
printf("%d\n", strlen(&p));// 随机值
printf("%d\n", strlen(&p+1));// 随机值
printf("%d\n", strlen(&p[0]+1)); // 5
return 0;
}
// 二维数组
int main()
{
int a[3][4] = { 0 };
printf("%d\n", sizeof(a)); // 48 这里数组名表示整个数组,因为单独放在sizeof中
printf("%d\n", sizeof(a[0][0]));// 4 第一行的第一个元素
printf("%d\n", sizeof(a[0]));// 16 a[0] 可以理解为第一行的数组名
printf("%d\n", sizeof(a[0]+1));// 4/8 a[0]理解为第一行的数组名,因为不是单独放在sizeof中,所以表示首元素的地址
printf("%d\n", sizeof(*(a[0] + 1)));// 4 第一行第二个元素
printf("%d\n", sizeof(a + 1));//4/8 二维数组第二行的地址
printf("%d\n", sizeof(*(a + 1)));// 16 二位数组的第二行
printf("%d\n", sizeof(&a[0]+1));// 4/8 a[0]是第一行的数组名,&a[0]取出的时第一行的地址,+1就是第二行的地址
printf("%d\n", sizeof(*(&a[0] + 1)));// 16 二位数组的第二行
printf("%d\n", sizeof(*a)); // 16 a表示二维数组的首元素地址,这里即第一行的地址,所以*a就是第一行
printf("%d\n", sizeof(a[3])); // 16 a[3]相当于二维数组的第四行的数组名,虽然不存在,但是也能通过类型计算大小
return 0;
}