文章目录
指针进阶
我们在初阶时就已经接触过指针,了解了指针的相关内容,有:
- 指针定义:指针变量,用于存放地址。地址唯一对应一块内存空间。
- 指针大小:固定32位平台下占4个字节,64位8个字节。
- 指针类型:类型决定指针±整数的步长及指针解引用时访问的大小。
- 指针运算:指针解引用,指针±整数,指针-指针,指针关系运算。
本章节在此基础上,对C语言阶段指针进行更深层次的研究。
字符指针
字符指针,存入字符的地址,类型为char*
字符指针的作用
- 指向单个字符
char ch = 'w';
char* pc = &ch;
*pc = 'a';
这里指针pc就是ch的地址,指向字符ch。
字符地址的类型是字符指针类型(char*)。
- 指向字符串首字符
char* pstr = "hello world";
printf("%s\n", pstr);//hello world
printf("%s\n", pstr+1);// ello world
printf("%c\n", *(pstr+1));//e
pstr就是字符串首字符的地址,指向字符’h’。
pstr+1指向第二个字符’e’,以此类推。
打印字符串,遇到’\0’停止打印。
"hello world"是常量字符串,无法修改,其实应该加const,但是舍弃const这个字符串还是不可以修改。
有如下例题
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
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与str2多是字符串首字符地址,str3与str4也是字符串首字符的地址。
str1 == str2实际上是地址的比较
str1和str2是普通的数组,是在内存上开辟了两块空间不过存放了一样的数据。
str3和str4指向常量字符串,存放在内存的常量区,是不可被修改且具有唯一性即常量区只存放一个。所以str3和str4指向的都是同一个字符串。
所以str1 != str2,str3 == str4。
总结
- 常量字符串不可被修改,存放在内存的常量区。
- 具有唯一性即常量区只存放一个。
指针数组
指针数组的定义
指针数组是一个存放指针的数组
int* arr1[10];整型指针数组
char* arr2[4];字符指针数组
char** arr3[5];二级字符指针数组
指针数组的使用
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int c =30;
int d = 40;
int* arr[4] = {&a, &b, &c, &d};
//arr是数组,有四个元素,每个元素多是int*
for (int i=0; i<4; i++)
{
printf("%d ", *arr[i]);
}
return 0;
}
int main()
{
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {2, 3, 4, 5, 6};
int arr3[] = {3, 4, 5, 6, 7};
int* p[] = {arr1, arr2, arr3};
int i = 0;
for (i=0; i<3; i++)
{
int j = 0;
for (j=0; j<5; j++)
{
//printf("%d ", *(*(p+i)+j));
printf("%d ", p[i][j]);
}
printf("\n");
}
return 0;
}
int main()
{
const char* p[] = {"abcd", "bcde", "cdef", "defg"};
int i = 0;
for (i=0; i<4; i++)
{
//printf("%s\n", p[i]);
//printf("%s\n", *(p+i));
int j = 0;
for (j=0; j<4; j++)
{
//printf("%c ", p[i][j]);
printf("%c", *(*(p+i)+j));
}
printf("\n");
}
return 0;
}
总结 - 很重要
p[i] == *(p+i)
p[i][j] == *(*(p+i)+j) == *(p+i)[j] == *(p[i]+j)
数组指针
数组指针就是数组的地址,指向数组的指针。
数组指针的定义
int arr[10] = {0};
//1.
int* pa = arr;
//2.
int* par = &arr;
//3.
int* parr[10] = {arr, arr+1};
//4.
int (*parrr)[10] = &arr;
1.pa是整型指针,指向数组首元素。
2.&arr是数组指针类型, 而par是整型指针类型,报错。
3.parr是指针数组,每个元素多是int*,[]比*优先级高。
4.parrr是数组指针,指向的数组有10个元素,每个元素为int。
总结
- 去掉名字就是类型
- 字符地址就是字符指针类型,整型地址就是整型指针类型etc.
- int[10] - 整型数组类型
- int* - 整型指针类型
- int*[10] - 指针数组类型
- int(*)[10] - 指针数组类型
数组名和&数组名
&数组名是整个数组的地址
sizeof(数组名)是整个数组的大小,单位是byte
除了以上两种情况,所有其他数组名多是首元素地址
int arr[10] = {0};
//arr是数组首元素的地址
//&arr[0]:首元素的地址
//&arr:整个数组的地址
//有两种情况是整个数组
//1.&arr
//2.sizeof(arr),单位是字节
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", &arr);
printf("%p\n", arr+1);
printf("%p\n", &arr[0]+1);
printf("%p\n", &arr+1);
return 0;
数组指针的使用
遍历数组,使用数组或是指针作形参接收就行了。且所谓的用数组接收仅是理解层面,本质上都是指针。
void print1(int arr[], int sz)
{
int i = 0;
for (i=0; i<sz; i++)
{
printf("%d ", arr[i]);
}
}
void print2(int* p, int sz)
{
int i = 0;
for (i=0; i<sz; i++)
{
printf("%d ", *(p+i));
}
}
void print3(int (*p)[10], int sz)
{
int i = 0;
for (i=0; i<sz; i++)
{
//printf("%d ", (*p)[i]);
printf("%d ", *(*p+i));
}
}
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz = sizeof(arr) / sizeof(arr[0]);
//print1(arr, sz);
//print2(arr, sz);
print3(&arr, sz);
return 0;
}
下面是二维数组传参
#define ROW 3
#define COL 5
void print(int (*p)[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 ", p[i][j]);
printf("%d ", *(*(p+i)+j));
}
printf("\n");
}
}
void print2(int arr[][COL], int row, int col)
{
int i = 0;
for (i=0; i<row, i++)
{
for j = 0;
for (j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[ROW][COL] = {1, 2, 3, 4, 5, 2, 3, 4, 5, 6, 3, 4, 5, 6, 7};
print1(arr, ROW, COL);
print2(arr, ROW, COL);
return 0;
}
例题
//1.
int arr[5];
//2.
int *pa1[5];
//3.
int (*pa2)[10];
//4.
int (*pa3[10])[5];
1.整型数组,int[5] - 整型数组类型
2.指向数组,int*[5] - 指向数组类型
3.数组指针, int()[10] - 数组指针类型
4.pa3是一个数组,数组里有10个元素,每个元素类型为int()[5] - 数组指针类型
数组传参 指针传参
一维数组传参
void test1(int arr1[])
{}
void test1(int arr1[10])
{}
void test1(int* arr1)
{}
void test2(int* arr2[20])
{}
void test2(int* arr2[])
{}
void test2(int** arr2)
{}
int main()
{
int arr1[10] = {0};
int* arr2[20] = {0};
test1(arr1);
test2(arr2);
return 0;
}
以上一维数组传参均可以
二维数组传参
void test(int arr[3][5])
{}
void test(int arr[][5])
{}
void test(int (*p)[5])
{}
int main()
{
int arr[3][5] = {0};
test(arr);
return 0;
}
一级指针传参
void print(int* p, int sz)
{
int i = 0;
for (i=0; i<sz; i++)
{
printf("%d ", *(p+i));
}
}
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
print(p, sz);
return 0;
}
思考:
当一个函数的参数部分为一级指针的时候,函数可以接收哪些参数?
int a = 10;
test(&a);
int arr[10] = {0};
test(arr);
int* p = &a;
test(p);
二级指针传参
void test(int** p)
{}
int main()
{
int a = 10;
int* pa = &a;
int** paa = &pa;
int* arr[10];
test(&pa);
test(arr);
test(paa);
return 0;
}
函数指针
函数指针的定义
函数指针:存放函数地址的指针
函数名 == &函数名
int Add(int x, int y)
{
return x+y;
}
int main()
{
//int (*p)(int, int) = Add;
int (*p)(int, int) = &Add;
//int ret = (*p)(3, 5);
int ret = p(3, 5);
printf("%d\n", ret);
return 0;
}
例题
//1.
(*(void(*)())0)();
//2.
void (*signal(int, void(*)(int)))(int);
void()()是函数指针类型,把0强制类型转换为函数指针类型,也就是把0强制转换为指针,指针中存放着指向的函数的地址,((void()())0)就是指针所指向的函数,就相当于p,也就是p(),这里无参数。
首先这是个函数,函数的两个参数类型为int,void()(int),函数的返回类型为void (*)(int)。
typedef void (*ptr)(int);
ptr signal(int, ptr);
函数指针数组
函数指针数组是一个数组,数组每一个元素多是函数指针类型。
函数指针数组的定义
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 main()
{
int (*p[4])(int, int) = {Add, Sub, Mul, Div};
return 0;
}
函数指针数组的使用
利用函数指针数组实现计算器,以简化调用过程。
转移表
//计算器实现1.0
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 main()
{
int x = 0;
int y = 0;
int input = 0;
do{
menu();
printf("请选择:>");
scanf("%d", &input);
switch(input)
{
case 0:
printf("退出成功\n");
break;
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
int ret = add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
int ret = sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
int ret = mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("输入操作数:");
scanf("%d %d", &x, &y);
int ret = div(x, y);
printf("%d\n", ret);
break;
default:
printf("请重新选择\n");
break;
}
} while(input);
return 0;
}
我们看到上面代码特别繁琐,我们使用函数指针数组实现计算器。
//计算器实现2.0
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 main()
{
int x = 0;
int y = 0;
int input = 0;
int (*parr[5])(int, int) = {0, add, sub, mul, div};
do{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input >= 0 && input <= 4)
{
if (input == 0)
{
printf("退出成功\n");
}
else
{
printf("请输入操作数:");
scanf("%d %d", &x, &y);
int ret = parr[input](x, y);
printf("%d\n", ret);
}
}
else
{
printf("请重新选择\n");
}
}while (input);
return 0;
}
函数指针数组实现不同选择情况下,通过函数地址“跳转”到不同的函数的功能。
这样的函数指针数组成为转移表。(跳转功能)
回调函数
switch语句的性能要优于if语句,所以我们这里不舍弃switch语句。
//计算器实现3.0
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;
}
void Calc(int (*p)(int, int), int x, int y)
{
printf("请输入操作数:");
scanf("%d %d", &x, &y);
int ret = p(x, y);
printf("%d\n", ret);
}
int main()
{
int x = 0;
int y = 0;
int input = 0;
do{
menu();
printf("请选择:>");
scanf("%d", &input);
switch(input)
{
case 0:
printf("退出成功\n");
break;
case 1:
Calc(add, x, y);
break;
case 2:
Calc(sub, x, y);
break;
case 3:
Calc(mul, x, y);
break;
case 4:
Calc(div, x, y);
break;
default:
printf("请重新选择\n");
break;
}
}while (input);
return 0;
}
通过函数指针调用的函数叫做回调函数,回调函数即使第三方调用函数的参数也在其中被调用。
若想在调用函数中随条件变化而调用不同的函数,就必须使用回调函数的方法:调用函数中使用函数指针,指向不同函数。回调函数在大型工程中显得非常方便。
指向函数指针数组的指针
顾名思义,指向函数指针数组的指针存放函数指针数组的地址。
void test1(char* p)
{}
void test2(char* p)
{}
int main()
{
void (*par[10])(char*) = {test1, test2};
void (*(*parr)[10])(char*) = ∥
return 0;
}
回调函数的使用
qsort 快速排序
//冒泡排序
void Bubble_sort(int arr[], int sz)
{
int i = 0;
for (i=0; i<sz-1; i++)
{
int j = 0;
for (j=sz-1; j>i; j--)
{
if (arr[j] < arr[j-1])
{
int tmp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = tmp;
}
}
}
}
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz = sizeof(arr) / sizeof(arr[0]);
Bubble_sort(arr, sz);
for (int i=0; i<10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
与冒泡排序作对比发现,冒泡排序仅需起始地址和元素个数即可,暗含了其他信息。由于过度具体化,冒泡排序只能排序整型数组,且比较函数过于简单无需单独列出。
因为qsort
排序可适用于多种类型如浮点型,字符型,自定义类型的数据,故无法规定具体类型,所以需要多个参数去描述元素的基本信息。
qsort
之所以能够适应多种数据,是因为参数void* base再搭配上num和size就描述出任意一种类型。
为什么将参数base的类型定义为void*呢?如下述代码所示。
int a = 10;
char* p1 = &a;//类型不兼容
float f = 1.2f;
char* p2 = &f;//类型不兼容
void* p1 = &a;
void* p2 = &f;
确定类型的地址之间直接赋值会提示类型不兼容,强制转化也可能会导致精度丢失。
故使用无(具体)类型void*
,又称通用类型,即可以接收任意类型的指针,但是无法进行指针运算(解引用,±整数等)。
void* p1;
void* p2;
p++;//error
*p;//error
p1-p2;//error
p1 > p2;//error
1.base
:用于存入数据的起始地址。类型定义为void*,可接受任意类型的指针。
2.num
:待排序的元素个数。
3.size
:元素大小,单位是byte。
明确了排序的起始位置,元素个数和元素大小,貌似已经够了。但是并无法排序所有类型,因此必须自定义一个抽象的比较函数指定元素的比较方式。
4.cmp
:比较函数,用于指定元素的比较方式。
*p1
小于*p2
,返回值小于0。*p1
等于*p2
,返回值等于0。*p1
大于*p2
,返回值大于0。
5.p1
p2
:进行比较的两个元素的地址。
qsort可以说是一个半库函数半自定义函数。自定义在于其函数最后一个参数为比较函数,该函数内部实现自由,但返回值必须按照规定返回相应的数值。
小结
需要qsort
函数排序各种类型的数据。
- 故base起始地址不可为固定的指针类型,只能用void*。
- 既然是通用类型还要明确比较元素的个数和大小。
- 最后,排序最核心的比较大小,为适应不同的类型元素必须自定义专门的比较函数。
qsort
实现冒泡排序
//比较函数:整型
#include <stdlib.h>
int int_cmp(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2;
}
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), int_cmp);
}
比较函数int_cmp
不需要传参,作为回调函数由qsort
直接调用。比较函数的传参过程由qsort
内部实现。
qsort
实现结构体排序
#include <stdlib.h>
struct Stu
{
char* name;
int age;
float score;
};
void score_cmp(const void* p1, const void* p2)
{
//升序
return ((struct Stu*)p1) -> score - ((struct Stu*)p2) -> score;
//降序
return ((struct Stu*)p2) -> score - ((struct Stu*)p1) -> score;
}
void name_cmp(const void* p1, const void* p2)
{
return strcmp(((struct Stu*)p1) -> name, ((struct Stu*)p2) -> name);
}
int main()
{
struct Stu s[3] = {{"zhangsan", 22, 66.2f}, {"lisi", 23, 99.9f}, {"wangwu", 24, 86.5f}};
int sz = sizeof(s) / sizeof(s[0]);
//按成绩排序
qsort(s, sz, sizeof(s[0]), score_cmp);
//按名字排序
qsort(s, sz, size(s[0]), name_cmp);
}
由此可得,提取出一个比较函数,具体交换的方式由
qsort
内部实现。
模拟实现qsort
用qsort的函数逻辑,实现冒泡排序。
int cmp(const void* p1, const void* p2)
{
return *((int*)p1) - *((int*)p2);
}
int name_cmp(const void* p1, const void* p2)
{
return strcmp((struct Stu*)p1 -> name, (struct Stu*)p2 -> name);
}
int score_cmp(const void* p1, const void* p2)
{
return (struct Stu*)p1 -> score - (struct Stu*)p2 -> score;
}
void Swap(const char* p1, const char* p2, int size)
{
int i = 0;
for (i=0; i<size, i++)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
void my_bubble_sort(void* base, size_t num, size_t size, int (*p)(const void*, const void*))
{
int i = 0;
for (i=0; i<num-1; i++)
{
int j = 0;
for (j=num-1; j>0; j--)
{
if (p((char*)base+size*(j-1), (char*)base+size*j) > 0)
{
Swap((char*)base+size*(j-1), (char*)base+size*j, size);
}
}
}
}
void print1(int arr[], int sz1)
{
int i = 0;
for (i=0; i<sz1; i++)
{
printf("%d ", arr[i]);
}
}
void print2(struct Stu* p, int sz2)
{
int i = 0;
for (i=0; i<sz2; i++)
{
printf("%s ", (p+i) -> name);
}
}
void print3(struct Stu* p, int sz2)
{
int i =0;
for (i=0; i<sz2; i++)
{
printf("%f ", (p+i) -> score);
}
}
struct Stu
{
char* name;
int age;
float score;
}
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz1 = sizeof(arr) / sizeof(arr[0]);
my_bubble_sort(arr, sz1, size(arr[0]), cmp);
struct Stu s[3] = {{"wangwu", 22, 45.6}, {"lisi", 23, 98.7}, {"zhaosan", 25, 65.3}};
int sz2 = sizeof(s) / sizeof(s[0]);
my_bubble_sort(s, sz2, sizeof(s[0]), name_cmp);
my_bubble_sort(s, sz2, sizeof(s[0]), score_cmp);
print1(arr, sz1);
print2(s, sz2);
print3(s, sz2);
return 0;
}
地址统一强转为char*,以最小字节单位一个字节进行比较和交换,使代码更具有普适性。