一、字符指针变量
我们知道有种指针类型为字符指针(char*)。
#include <stdio.h>
int main()
{char ch = 'w';char* pch = &ch;printf("%c\n", *pch);return 0;
}
其实它还有一种使用方式。
#include <stdio.h>
int main()
{char* pstr = "hello world";printf("%s\n", pstr);return 0;
}
在代码 char* pstr = "hello world";
中,指针 pstr
存放的正是字符串 "hello world"
首字符 'h'
的地址。在 C 语言里,字符串是以字符数组的形式存储在内存中的,并且以 '\0'
(空字符)作为字符串结束的标志。当定义一个指针指向一个字符串常量时,该指针的值就是这个字符串存储在内存中起始位置(也就是首字符所在位置)的地址。当在printf
函数中使用%s
格式化符并且参数是一个字符指针(如char *pstr
)时,printf
函数会把这个指针当作字符串的起始地址。它会从这个地址开始读取字符,一直读取到遇到'\0'
(字符串结束标志)为止。例如,在代码char* pstr="hello world"; printf("%s\n", pstr);
中,pstr
指向字符串"hello world"
的首字符'h'
。printf
会从这个地址开始,逐个字符地输出,直到遇到'\0'才停止。
现在我们来看一看《剑指offer》中的一道题。
#include <stdio.h>
int main()
{char str1[] = "hello";char str2[] = "hello";const char* str3 = "hello";const char* str4 = "hello";if (str1 == str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n");if (str3 == str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0;
}
我们来分析一下结果为什么是这样的。当我们使用char数组存放字符串时,就会为数组分配独立的内存空间来存放字符串,所以str1和str2都指的是这两个数组的首字符的地址,即使字符串相同,所对应的地址也不相同。在 C 语言中,像 "hello"
这样的常量字符串在程序中通常只存储一份在只读的内存区域。当定义 str3
和 str4
这两个指针并让它们指向同一个常量字符串"hello"
时,它们实际上都指向了这个字符串的起始地址。所以当进行 str3 == str4
比较时,比较的是两个指针所存储的地址值,因为它们都指向同一个 "hello"
字符串所在的内存位置,所以地址是相同的。其实造成差异的本质原因是前两个都是在定义字符串变量,也就是字符数组,它本身是没有地址的,而后两个常量字符串本身就是有地址的,这里只是将地址取出来存放在指针中。
二、数组指针变量
int(*p)[10];
这就是数组指针的形式。p是这个变量的名字,*说明这个变量是指针,还剩下int [10]就说明它指向的对象是大小为10的整型数组类型的。
注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。否则这个变量就不是数组指针了,而是指针数组。
那么数组指针变量怎么初始化呢?其实和我们之前初始化指针变量一样。
#include<stdio.h>
void test(int arr[2][3],int a,int b)
{for (int i = 0; i < a; i++){for (int j = 0; j < b; j++){printf("%d ", arr[i][j]);}printf("\n");}
}
int main()
{int arr[2][3] = { {1,2,3},{2,3,4} };test(arr,2,3);return 0;
}
之前我们使用二维数组传参都是如上图的代码这样去进行操作的。其实二维数组就是一维数组的数组,也就是二维数组的每个元素是一个一维数组。所以二维数组的数组名表示的就是第一行的地址,是一个一维数组的地址。根据上面的例子,第一行的一维数组的类型就是 int [3] ,所以第一行的地址的类型就是数组指针类型 int(*)[3] 。那就意味着二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址,那么形参也是可以写成指针形式的。如下:
#include<stdio.h>
void test(int(*arr)[3], int a, int b)
{for (int i = 0; i < a; i++){for (int j = 0; j < b; j++){printf("%d ", arr[i][j]);}printf("\n");}
}
int main()
{int arr[2][3] = { {1,2,3},{2,3,4} };test(arr,2,3);return 0;
}
四、函数指针变量
#include<stdio.h>
void test(int(*arr)[3], int a, int b)
{for (int i = 0; i < a; i++){for (int j = 0; j < b; j++){printf("%d ", arr[i][j]);}printf("\n");}
}
int main()
{int arr[2][3] = { {1,2,3},{2,3,4} };test(arr,2,3);void (*p)(int(*arr)[3], int a, int b) = test;return 0;
}
p是变量名,*表示这个变量是指针,前面的void是函数的返回值,后面加的是函数的参数。而且在参数中可以省略具体的变量名,只留下参数的数据类型。在这里就是将a,b去掉,只留下(int,int)。那我们怎么去使用这个函数指针变量呢?
#include<stdio.h>
int add(int a, int b)
{return a + b;
}
int main()
{int (*p)(int, int) = add;printf("%d", p(1, 2));return 0;
}
使用函数指针名加参数就能实现,当然将函数指针名解引用后加参数也能实现,但是直接使用函数指针名编译器就可以理解这是对函数指针所指向的函数的调用,使用起来会更加方便,一般不推荐显式解引用来使用函数。
接下来,我们来看两段c陷阱与缺陷中的代码。当然,这些代码只是为了让我们更加理解指针等知识,其实某些写法并不推荐。
1.(*(void (*)())0)();
来分析一下代码,我们能看到中间的void (*)(),知道这是一个函数指针,参数为空,返回值为void,后面还跟着一个0,那我们就可以知道,这里是将0强制类型转换了,将它变成了函数指针类型,然后将整体解引用,后面再加上空参,就是在调用函数。
2.void (*signal(int , void(*)(int)))(int);
看到signal(int , void(*)(int))应该就能知道这是一个函数名加上参数,一个是整型类型的,一个是函数指针类型的。其实,剩下的void(*)(int)是这个函数的返回值,这种写法确实比较奇怪,我们在这里也不需要多纠结,这个代码整体就是一个函数的声明。
五、typedef 关键字
typedef unsigned int uint;
//将unsigned int重命名为uint
一般类型都可以这样进行操作,但是对于数组指针和函数指针稍微有点区别,新的类型名必须在*的右边:
#include <stdio.h>
int add(int x, int y)
{return x + y;
}
int main()
{int arr[5] = { 0 };typedef int(*parr_t)[5];//int(*)[5]->parr_ttypedef void(*pf_t)(int);//void(*)(int)->pf_tparr_t p1 = arr;pf_t p2 = add;return 0;
}
六、函数指针数组
int (*parr1[3])();
那么函数指针数组有什么用呢,接下来我们来看转移表。举例:计算器的⼀般实现:
#include<stdio.h>
void menu()
{printf("***0.exit***\n");printf("***1.add****\n");printf("***2.sub****\n");printf("***3.mul****\n");printf("***4.div****\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 n;int(*p[5])(int, int) = { 0,add,sub,mul,div };while (1){menu();scanf("%d", &n);if (n > 0 && n < 5){int a, b;printf("请输入两个操作数...\n");scanf("%d%d", &a, &b);printf("结果是:%d\n", p[n](a, b));}else if (n == 0){printf("退出计算器\n");break;}elseprintf("输入有误\n");}return 0;
}
如上图的代码,我们就能实现简易的加减乘除计算器,这就使用了函数指针数组。那么转移表又是什么呢?其实它指的就是运用函数指针数组以数组方式去调用里面的函数,使代码变得更加简洁。
七、回调函数
#include<stdio.h>
void menu()
{printf("***0.exit***\n");printf("***1.add****\n");printf("***2.sub****\n");printf("***3.mul****\n");printf("***4.div****\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 is(int(*p)(int, int))
{int a, b;printf("请输入两个操作数...\n");scanf("%d%d", &a, &b);printf("结果是:%d\n", p(a, b));
}
int main()
{int n;int(*p[5])(int, int) = { 0,add,sub,mul,div };while (1){menu();scanf("%d", &n);if (n > 0 && n < 5){switch (n){case 1:{is(add);break;}case 2:{is(sub);break;}case 3:{is(mul);break;}case 4:{is(div);break;}}}else if (n == 0){printf("退出计算器\n");break;}elseprintf("输入有误\n");}return 0;
}
八、qsort 使用举例
qsort的作用是对数组的元素进行排序,使用 compar 函数确定顺序,将base 指向的数组的 num 个元素进行排序,每个元素大小为size。该函数不返回任何值,但通过对 compar 定义的元素进行基数重新排序来修改指向的数组的内容。我们能发现参数中的指针都是void*类型的,这是因为我们进行操作的数据类型不是固定的。
我们先来看一下compar 函数,它可以比较元素对,并将指向它们的指针作为参数。其返回值如下图,一般可以简化成小于返回-1,等于返回0,大于返回1。
1.使用qsort函数排序整型数据
#include<stdio.h>
#include<stdlib.h>
int int_compar(const void* p1, const void* p2)
{return *((int*)p1) - *((int*)p2);
}
int main()
{int arr[] = { 4,3,6,12,8,2,9,7,1,5 };qsort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(int), int_compar);for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){printf("%d ", arr[i]);}return 0;
}
2.使用qsort排序结构数据
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{char name[20];int age;
};
int cmp_stu_by_age(const void* e1, const void* e2)
{return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_stu_by_name(const void* e1, const void* e2)
{return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test1()
{struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
void test2()
{struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{test1();test2();return 0;
}
通过调试我们可以看出结构体中的数据确实被排序了。
3.qsort函数的模拟实现
#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size)
{int i = 0;for (i = 0; i < size; i++){char tmp = *((char*)p1 + i);*((char*)p1 + i) = *((char*)p2 + i);*((char*)p2 + i) = tmp;}
}
void bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{int i = 0;int j = 0;for (i = 0; i < count - 1; i++){for (j = 0; j < count - i - 1; j++){if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0){_swap((char*)base + j * size, (char*)base + (j + 1) * size,size);}}}
}
int main()
{int arr[5] = { 2,3,4,1,5 };bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), int_cmp);for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){printf("%d ", arr[i]);}return 0;
}
九、sizeof和strlen的对比
1.sizeof
#include <stdio.h>
int main()
{int a = 10;printf("%d\n", sizeof(a));printf("%d\n", sizeof a );printf("%d\n", sizeof(int));return 0;
}
2.strlen
size_t strlen(const char* str);