C语言指针进阶(进阶)
目录
1、字符指针
2、指针数组
3、数组指针
3.1数组指针的定义
3.2 &数组名VS数组名
3.3 数组指针的使用
4、数组传参、指针传参
4.1 一维数组传参
4.2 二维数组传参
4.3 一级指针传参
4.4 二级指针传参
5、函数指针
5.1 阅读分析代码
5、2函数指针的用途
6、函数指针数组(转移表)
7、指向函数指针数组的指针
8、回调函数
8.1、void *介绍
8.2类比qsort写自己的冒泡排序
9、指针和数组笔试题解析
10、指针笔试题
学习之前要对指针有所了解,零基础的要先看一下C语言指针(初级)
本文章重点
1.字符指针
2.数组指针
3.指针数组
4.数组传参和指针传参
5.函数指针
6.函数指针数组
7.指向函数指针数组的指针
8.回调函数
9.指针和数组面试题的解析
指针的主题,我们在初级阶段的《指针》中已经接触过指针,我们知道指针的概念:
1.指针就是变量,用来存放地址,地址唯一标识一块内存空间。
2.指针的大小是固定的4/8个字节(32位平台/64位平台)。
3.指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限(不同类型的指针解引用的字节个数不同)。
4.指针的运算
1、字符指针
在指针的类型中我们知道有一种指针类型为字符指针char*;
一般使用:
#include<stdio.h>
int main(){char x = 'a';char* px = &x;printf("%c\n", *px);*px = 'b';printf("%c", *px);
}
还有一种使用方式:
#include<stdio.h>
int main(){const char* p = "abcdef"; //把字符串首字符a的地址赋值给了p。//这里的abcef是常量字符串是不可以修改的。printf("%s", p);
}
上面代码的意思是把一个常量字符串的首字符a的地址存放到指针变量p中。
那就有这样的面试题:
#include<stdio.h>
int main(){const char* p1 = "abc";const char* p2 = "abc";char arr1[] = "abc";char arr2[] = "abc";if (p1 == p2)printf("p1 == p2\n");elseprintf("p1 != p2\n");if (arr1 == arr2)printf("arr1 == arr2\n");elseprintf("arr1 != arr2\n");
}
输出结果:
这里的p1和p2指向的是同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。
但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以arr1和arr2不同,p1和p2相同。
2、指针数组
在初阶的时候我们已经提到了指针数组,指针数组是一个存放指针的数组。
int* arr1[10]; //整形指针的数组char* arr2[4]; //以及字符指针的数组char** arr3[5]; //二级字符指针的数组
使用指针数组模拟二维数组:
#include<stdio.h>
int main()
{int arr1[] = {1, 2, 3, 4};int arr2[] = {5, 6, 7, 8};int arr3[] = {9, 10, 11, 12};int* parr[3] = {arr1, arr2, arr3};int i = 0;for(i = 0; i < 3; i++){int j = 0;for(j = 0; j < 4; j++){//方法一
// printf("%d ", *(parr[i] + j)); //这里的[]就相当于解地址。//方法二 printf("%d ", parr[i][j]);}printf("\n");}return 0;
}
3、数组指针
3.1数组指针的定义
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉了:整形指针:int *p;能够指向整型数据的指针。浮点数指针:float *p能够指向浮点类型的指针。
那么数组指针就是:能够指向数组的指针。
int* p1[10];
//解释:p1:是一个指针数组。
int (*p2)[10];
//解释:p2先和*结合,说明p2是一个指针变量,
// 然后指着的是一个大小为10个整数的数组。
// 所以p2是一个指针,指向一个数组,叫做数组指针//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p2先和*结合。
3.2 &数组名VS数组名
对于下面的数组:
int arr[10];
arr和&arr分别是啥?
我们之前说过数组名表示数组首元素的地址。
那&arr数组名到底是啥?
//数组名确实能表示首元素的地址
//但是有两个例外:
//1、sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
//2、&数组名,这里的数组名表示整个数组,取出的是整个数组的地址
#include<stdio.h>
int main(){int arr[] = {2, 3, 6, 5, 4, 1, 9, 8, 7};printf("%p\n",arr);printf("%p\n",&arr[0]);printf("%p\n",&arr); //里的数组名表示整个数组,取出的是整个数组的地址 return 0;
}
经过运行:发现上面三个地址是一样的那么他们到底什么区别呢?这里我们用指针 +- 整数再来看他们的不同 。
//数组名确实能表示首元素的地址
//但是有两个例外:
//1、sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
//2、&数组名,这里的数组名表示整个数组,取出的是整个数组的地址
#include<stdio.h>
int main(){int arr[] = {2, 3};printf("%p\n",arr);printf("%p\n",arr+1);printf("------------------\n");printf("%p\n",&arr[0]);printf("%p\n",&arr[0] + 1);printf("------------------\n");printf("%p\n",&arr); //里的数组名表示整个数组,取出的是整个数组的地址 printf("%p\n",&arr+1);return 0;
}
这里我们发现arr和&arr[0]的两组地址完全相同。但是这里的&arr + 1和arr + 1就不同了,经过计算发现arr指针 +1跳过了4个地址。&arr指针 +1 直接跳过了8个地址。这里就可以得出&arr表示取出了整个数组的地址。
数组名确实能表示首元素的地址
但是有两个例外:
1、sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
2、&数组名,这里的数组名表示整个数组,取出的是整个数组的地址
上面的明白了,我们再看数组的地址应该怎么存储呢?
整型指针是用来存放整型的地址。
字符指针是用来存放字符的地址。
数组指针是用来存放数组的地址。
int arr[10] = {0};int (*p)[10] = &arr; //p的类型去掉p就是p的类型 int (*)[10]
char* arr[5] = { 0 }; //指针数组char* (*pc)[5] = &arr;
注意:数组指针 int (*p)[10] = &arr 中括号[ ]中的10必须要给。定义的数组和数组指针[ ]中必须相同。比如下面代码1就是错误的,改正为代码2才可以。
//代码1
int arr[] = {0, 1, 2, 3}; //指针数组
int (*pc)[] = &arr;//代码2
int arr[] = {0, 1, 2, 3}; //指针数组
int (*pc)[4] = &arr;
3.3 数组指针的使用
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
看代码:
#include<stdio.h>
int main()
{int arr[9] = { 1,2,3,4,5,6,7,8,9 };int (*p)[9] = &arr; //把数组arr的地址赋值给数组指针变量p。int sz = sizeof(arr) / sizeof(arr[0]);int i = 0;for (i = 0; i < sz; i++) { //数组指针在一维数组的应用; 但是这种使用方式很少使用printf("%d ", (*p)[i]);//printf("%d ", *((*p) + i));}return 0;
}
一个数组指针的使用:二维数组传参
#include<stdio.h>
//不使用指针传参。
void print(int list[3][3]) {int i = 0;int j = 0;for (i = 0; i < 3; i++) {for (j = 0; j < 3; j++) {printf("%d ", list[i][j]);}printf("\n");}
}
void print1(int (*p)[3]) { //print1这里形参值的是二维数组第一行元素的地址。//传入的第一行一维元素的地址,// 用指针数组来接收一行元素的地址。int i = 0;int j = 0;for (i = 0; i < 3; i++) {for (j = 0; j < 3; j++) {printf("%d ", *(*(p + i) + j));//printf("%d ", p[i][j]);}printf("\n");}}
int main()
{int arr[3][3] = {1,2,3,4,5,6,7,8,9};print(arr); //这里二维数组传参时传入的是二维地址的第一行的地址。printf("使用指针传参\n");print1(arr);return 0;
}
学了指针数组和数组指针我们来看一下下面的代码分析一下代码的意思。
int arr[5]; //arr是整型数组int* parr1[10]; //parr1是整型指针数组int (*parr2)[10]; //parr2是数组指针int (*parr3[10])[5];//parr3是存放数组指针的数组
int (*parr3[10])[5]图解:
4、数组传参、指针传参
在写代码的时候难免把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
4.1 一维数组传参
#include<stdio.h>
void test(int arr[]) { //ok
}void test(int arr[10]) {//ok
}void test(int* arr) { //ok
}void test2(int* arr[20]) { //ok
}
void test2(int** arr) { //ok
}int main()
{int arr[10] = { 0 };int* arr2[20] = { 0 };test(arr);test2(arr2);return 0;
}
4.2 二维数组传参
#include<stdio.h>
void test(int arr[3][5]) { //ok
}void test(int arr[][]) {//NO //形参的二维数组行可以省略,列不能省略。
}
void test(int arr[][5]) {//ok
}
//总结:二维数组传参,函数形参的设计只能省略第一维
//因为对一个二维数组,可以不知道有多少行,但是必须知道有多少列
//这样才方便运算。
void test(int* arr) { //no
}void test(int* arr[5]) {//no
}void test(int (*arr)[5]) {//ok
}void test(int** arr) {//no
}int main()
{int arr[3][5] = {0};test(arr);return 0;
}
4.3 一级指针传参
#include<stdio.h>void print(int* p, int n) {int i = 0;for (i = 0; i < n; i++) {printf("%d ", *(p + i));}
}int main() {int arr[10] = { 1, 2,3,4,5,6,7,8,9,10 };int* p = arr; //这里p接收的是arr数组首元素的地址。int sz = sizeof(arr) / sizeof(arr[0]);//一级指针p传给函数print(p, sz);return 0;
}
4.4 二级指针传参
#include<stdio.h>
void test(int **ptr) {printf("%d\n", **ptr);
}
int main() {int n = 10;int* p = &n;int** pp = &p;test(&p);test(pp);
}
总结:传入的实参的类型要和形参类型相匹配。
5、函数指针
我们前面提到了数组指针,是指向函数的指针。那么函数指针则就是指向函数的指针。
#include<stdio.h>
int add(int a,int b){return a + b;
}
int main() {int arr[5] = { 0 };//&数组名 -> 取出的数组的地址int (*p)[5] = &arr; //数组指针//&函数名-> 取出的就是函数的地址printf("%p\n", &add);printf("%p\n", add);//对于函数来书,&函数名和函数名都是函数的地址。//那么我们考虑怎么把函数的地址存起来呢?int (*pa)(int, int) = &add; //函数地址存储方法。printf("%p\n", pa);//函数返回类型 (*指针变量)(函数参数1类型,函数参数2类型...) = %函数名
}
函数指针调用函数:
这里使用函数指针调用函数,会让人感觉莫名其妙,感觉多此一举。后面会说到函数指针的其他用法。在这里只是更加深刻的了解一下函数指针。
#include<stdio.h>
int add(int a,int b){return a + b;
}
int main() {int arr[5] = { 0 };int (*p)(int, int) = &add;int ant = (*p)(3, 5); //用函数指针调用函数int ant1 = p(3, 5); printf("%d\n", ant);printf("%d\n", ant1);
}
5.1 阅读分析代码
我们学习了上面的知识这里来分析两个代码。
下面两段代码:出自《C陷阱和缺陷》
//代码1:
( *( void(*)())0 )()
上面的代码是一次函数调用。调用的是0作为地址处的函数。
1.( void(*)())0 把0强制类型转换为:无参,返回类型是空的函数的地址。
2.调用0地址的这个函数。
//代码2:
void (* signal( int, void(*)(int) ) ) (int)
1、signal是函数名。
2、传入的两个参数为int、 void(*)(int)【函数指针类型】。
3、函数声明但是没有返回类型,signal( int, void(*)(int))。总结:signal是一个函数名,以上代码是一次函数声明。声明的signal函数的第一个参数的类型是int,第二个参数的类型是函数指针,该函数指针指向的函数参数是int,返回类型是void,signal函数的返回类型也是一个函数指针类型。
上面的代码难以理解这里可以简化代码。
typedef void(*pf_t)(int);
#include<stdio.h>
int main() {void (*signal(int, void(*)(int)))(int);pf_t signal(int, pf_t);
}
5、2函数指针的用途
编写一个程序实现加减乘除。
#include<stdio.h>
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 input = 0;int x = 0;int y = 0;int ant = 0;do {menu();printf("请选择 >");scanf("%d", &input);switch (input) {case 0:printf("退出程序");break;case 1:printf("请输入两个整数:");scanf("%d %d", &x, &y);ant = Add(x, y);printf("%d\n", ant);break;case 2:printf("请输入两个整数:");scanf("%d %d", &x, &y);ant = Add(x, y);printf("%d\n", ant);break;case 3:printf("请输入两个整数:");scanf("%d %d", &x, &y);ant = Add(x, y);printf("%d\n", ant);break;case 4:printf("请输入两个整数:");scanf("%d %d", &x, &y);ant = Add(x, y);printf("%d\n", ant);break;default:printf("输入错误");break;}} while (input); //选择0则直接退出return 0;
}
上面的代码case1 - 4中大量代码出现了冗余,只有第三条语句不同,我们应该怎么进行改进呢?
这里我们通过上面学的函数指针进行改进代码。写一个calc函数来计算。
#include<stdio.h>
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 casc(int (*p)(int, int)) {int x = 0;int y = 0;int ant = 0;printf("请输入两个整数:\n");scanf("%d %d", &x, &y);ant = p(x, y);printf("%d\n", ant);}
int main() {int input = 0;do {menu();printf("请选择 >\n");scanf("%d", &input);switch (input) {case 0:printf("退出程序\n");break;case 1:casc(Add);break;case 2:casc(Sub);break;case 3:casc(Mul);break;case 4:casc(Div);break;default:printf("输入错误\n");break;}} while (input);return 0;
}
6、函数指针数组(转移表)
函数指针数组的定义:
#include<stdio.h>
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 input = 0;int x = 0;int y = 0;int ant = 0;int (*p[5])(int, int) = { 0, Add, Sub, Mul, Div }; //函数指针数组。do {menu();printf("请选择 >\n");scanf("%d", &input);if (input == 0) {printf("退出程序");break;}else if (input >= 1 && input <= 4) {printf("请输入两个整数:\n");scanf("%d %d", &x, &y);ant = p[input](x, y);printf("%d\n", ant);}else {printf("输入错误\n");}} while (input);return 0;
}
7、指向函数指针数组的指针
在这里只是简单提一下,后面的不在深挖啦。
指向函数数组的指针是一个指针
指针指向一个数组,数组的元素都是函数指针;
如何定义?
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h>
int add(int a, int b) {return a + b;
}
int main() {int (*pa[5])(int, int) = { add }; //函数地址存储方法。int (*(*pp)[5])(int, int) = &pa; //指向函数指针的指针printf("%p", pp);
}
8、回调函数
回调函数就是一个通过函数指针调用的函数,如果把函数的指针(地址)作为函数传递给另一个函数,当这个指针被用来调用其指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件时由另外的一方调用,用于对该事件或条件进行响应。
前面的计算机已经使用了回调函数。这里我们再介绍一下qsort函数。
#include <cstdlib> //头文件。
qsort(, , ,); //四个参数。
#include<stdio.h>
#include <cstdlib>
int cmp_int(const void* e1, const void* e2) {return *((int*)e1) - *((int*)e2); //e1 > e2返回大于0,e1 = e2等于0;e1 < e2小于0
}
int main() {int arr[10] = { 9,8,7,5,6,3,2,1,0,10 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_int);int i = 0;for (i = 0; i < sz; i++) {printf("%d ", arr[i]);}return 0;
}
qsort实现结构体排序:
#include<stdio.h>
#include <cstdlib>
#include<string.h>
struct Sut {char name[10];int age;
};
int cmp_struct(const void* e1, const void* e2) {return strcmp(((struct Sut*)e1)->name, ((struct Sut*)e2)->name);
}
int main() {struct Sut s[10] = { {"张三",15}, {"李四",45},{"王五",18} };int i = 0;int sz = sizeof(s) / sizeof(s[0]);//排序前printf("排序前\n");for (i = 0; i < sz; i++) {printf("%s ", s[i].name);}printf("\n");//排序qsort(s, sz, sizeof(s[0]), cmp_struct);//排序后printf("排序后\n");for (i = 0; i < sz; i++) {printf("%s ", s[i].name);}return 0;
}
8.1、void *介绍
void* 是无具体类型的指针,可以接收任意类型的地址
void* 是无具体类型的指针,所以不能解引用操作,也不能+-整数。
8.2类比qsort写自己的冒泡排序
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h>
#include <cstdlib>
#include<string.h>
int my_cmp(const void* e1, const void* e2);
void Swap(char* x, char* y, int width) {int i = 0;for (i = 0; i < width; i++) {char tmp = *x;*x = *y;*y = tmp;x++;y++;}
}void my_bubble_sort(void* arr, int sz, int width, int(*cmp)(const void* e1, const void* e2)) {int i = 0;for (i = 0; i < sz - 1; i++) {int flag = 0; //假设一开始是有序的int j = 0;for (j = 0; j < sz - 1 - i; j++) {//前面的大于后面的交换if (cmp((char*)arr + j * width, (char*)arr + (j + 1) * width) > 0) {//交换函数Swap(((char*)arr + j * width), ((char*)arr + (j + 1) * width), width);flag = 1; //证明交换则是无序}}if (flag == 0) {break;}}
}
int my_cmp(const void* e1, const void* e2) {return *((int*)e1) - *((int*)e2);}
int main() {int arr[10] = { 1,3,5,6,2,4,9,8,7,10 };int sz = sizeof(arr) / sizeof(arr[0]);my_bubble_sort(arr, sz, sizeof(arr[0]), my_cmp);int i = 0;for (i = 0; i < sz; i++) {printf("%d ", arr[i]);}return 0;
}
9、指针和数组笔试题解析
☆☆☆下面几组题考察的是数组名的理解,指针的运算和意义。
在看题之前先了解一下strlen和sizeof:
strlen
strlen是求字符串长度的,关注的是字符中的\0,计算的是\0之前出现的字符的个数
strlen是库函数,只针对字符串
sizeof
sizeof只关注占用内存空间的大小,不在乎内存中放的是什么。
sizeof是一个操作符
整型数组
字符数组
下一组:
strlen函数头文件为#include<string.h>传入参数是一个地址。
#include<stdio.h>
#include<string.h>int main() {char arr[] = { 'a', 'b','c','d','e' ,'f' };printf("%d\n", strlen(arr)); //随机值printf("%d\n", strlen(arr + 0));//随机值//printf("%d\n", strlen(*arr)); // -->strlen('a') -->strlen(97);//野指针//printf("%d\n", strlen(arr[1])); // -->strlen('b') -->strlen(98);//野指针printf("%d\n", strlen( & arr));//随机值printf("%d\n", strlen(&arr + 1));//随机值 - 6printf("%d\n", strlen( & arr[0] + 1));//随机值 - 1return 0;
}
#include<stdio.h>
#include<string.h>int main() {char arr[] = "abcdef";printf("%d\n", strlen(arr)); //6printf("%d\n", strlen(arr + 0));//6//printf("%d\n", strlen(*arr)); // -->strlen('a') -->strlen(97);//野指针//printf("%d\n", strlen(arr[1])); // -->strlen('b') -->strlen(98);//野指针printf("%d\n", strlen( & arr));//6printf("%d\n", strlen(&arr + 1));//随机值printf("%d\n", strlen( & arr[0] + 1));//5return 0;
}
#include<stdio.h>
#include<string.h>
int main() {char arr[] = "abcdef";printf("%d\n", sizeof(arr)); //7 包括/0,7个字节printf("%d\n", sizeof(arr + 0));//4/8printf("%d\n", sizeof(*arr)); // 1printf("%d\n", sizeof(arr[1])); //1 printf("%d\n", sizeof(&arr));//4/8printf("%d\n", sizeof(&arr + 1));//4/8printf("%d\n", sizeof(&arr[0] + 1));//4/8return 0;
}
下一组:
#include<stdio.h>
#include<string.h>
int main() {char* p = "abcdef"; //字符指针 p中放的是a的地址printf("%d\n", sizeof(p)); //4/8printf("%d\n", sizeof(p + 1));//4/8printf("%d\n", sizeof(*p)); //1 这里解引用访问的是aprintf("%d\n", sizeof(p[0])); //1printf("%d\n", sizeof(&p)); //4/8printf("%d\n", sizeof(&p + 1));//4/8printf("%d\n", sizeof(&p[0] + 1));//4/8return 0;
}
#include<stdio.h>
#include<string.h>
int main() {char* p = "abcdef"; //字符指针 p中放的是a的地址printf("%d\n", strlen(p)); //6 printf("%d\n", strlen(p + 1));//5//printf("%d\n", strlen(*p)); //err//printf("%d\n", strlen(p[0])); //errprintf("%d\n", strlen(&p)); //随机值printf("%d\n", strlen(&p + 1));//随机值printf("%d\n", strlen(&p[0] + 1));//5return 0;
}
二维数组
#include<stdio.h>
#include<string.h>
int main() {int a[3][4] = { 0 };printf("%d\n", sizeof(a)); //48printf("%d\n", sizeof(a[0][0])); //4printf("%d\n", sizeof(a[0])); //16printf("%d\n", sizeof(a[0] + 1)); //4/8printf("%d\n", sizeof(a + 1)); //4/8printf("%d\n", sizeof(*(a + 1))); //16printf("%d\n", sizeof(&a[0] + 1));//4/8printf("%d\n", sizeof(*(&a[0] + 1)));//16printf("%d\n", sizeof(*a));//16printf("%d\n", sizeof(a[3]));//16return 0;
}
下面是对面代码结果的解释:
总结:
1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3.除此之外所以的数组名都表示首元素的地址。
10、指针笔试题
笔试题1:
笔试题2:
假设下面代码p的值为0x100000.如下表表表达式的值分别为多少?
已知,结构体Test类型的变量大小20个字节(在x86环境下)。
x86环境下。
笔试题3:
笔试题4:
笔试题5:
%p打印地址,地址是没有原反的概念的,下面代码%p直接打印的是-4的补码。
笔试题6:
#include<stdio.h>
#include<string.h>
int main() {int aa[2][5] = { 1,2,3,4,5,6,7,8,9,10 };int* ptr1 = (int*)(&aa + 1);//&aa是表示整个数组的地址 + 1。跳过整个数组。int* ptr2 = (int*)(*(aa + 1));printf("%d %d", *(ptr1 - 1), *(ptr2 - 1));return 0;
}
笔试7:
笔试8:
这种题要画图分析;要学会画图。要考虑内存。