指针的进阶
已知的指针的概念:
-
指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
-
指针的大小是固定的4/8个字节(32位平台/64位平台)。
-
指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
-
指针的运算。
1. 字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char* ;
1.1 一般使用:
1.2 更合适的写法
常量字符串不可以修改
int main()
{
char ch = 'w';
char* pc = &ch;
const char* ps = "abcdef";
printf("%s\n", ps);
//*ps='w'; //常量字符串不可以修改
//printf("%c\n", *ps);
return 0;
}
代码 const char* ps = “abcdef” ; 特别容易让人以为是把字符串 abcdef放到字符指针 ps 里了;
但是,本质是把字符串 abcdef 首字符的地址放到了ps 中。
1.2.1 改为可修改的数组
char arr[] = "abcdef";
char* p = arr;
1.3 经典的面试题:
注:比较的是地址而非内容(strcmp)
int main()
{
char str1[] = "hello LJ.";
char str2[] = "hello LJ.";
const char* str3 = "hello LJ.";
const char* str4 = "hello LJ.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");//打印
if (str3 == str4)
//这里str3和str4指向的是一个同一个常量字符串
printf("str3 and str4 are same\n");//打印
//指向同一个字符串的时候,他们实际会指向同一块内存
else
printf("str3 and str4 are not same\n");
return 0;
}
C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
2. 指针数组
指针数组是一个存放指针的数组。
int* arr1[10]; //整形指针的数组
char* arr2[4]; //一级字符指针的数组
char** arr3[5];//二级字符指针的数组
2.1 应用举例
char* arr[] = { "Li Jun","puppy","Mr.beam","naughty dog" };
int i = 0;
for (i=0;i<5;i++)
{
printf("%s\n", arr[i]);
}
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int* arr[3] = { arr1,arr2,arr3 };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d",arr[i][j]);
}
printf("\n");
}
3. 数组指针
3.1 数组指针的定义
整型指针: int * pint; 能够指向整型数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
int main()
{
int a = 10;
int* p1 = &a;
char ch = 'w';
char* p2 = &ch;
int arr[10] = { 1,2,3,4,5 };
int(*pa)[10]=&arr;//取出的是数组的地址存放到pa中,pa是数组指针变量
//int(*)[10] -> 数组指针 类型,去掉名字pa
return 0;
}
下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];//数组指针
//p1, p2分别是什么?
解释:
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
3.2 &数组名VS数组名
对于下面的数组:
int arr[10];
arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那**&arr数组名**到底是啥?
int main()
{
//不论数组首元素地址还是数组,从值的角度来说,三组的第一行打印的值结果一样
int arr[10] = { 0 };
printf("%p\n", arr);//int*
printf("%p\n", arr+1);//差4
printf("%p\n", &arr[0]);//int*
printf("%p\n", &arr[0]+1);//差4
printf("%p\n", &arr);//int(*pa)[10]=&arr; - 数组指针类型
//&arr 表示的是数组的地址,而不是数组首元素的地址
printf("%p\n", &arr+1);//数组的地址+1,跳过整个数组的大小 - 差40
return 0;
}
3.3 数组指针的使用
那数组指针是怎么使用的呢?
int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
//但是我们一般很少这样写代码
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
3.3.1 在一维数组中的使用
void print1(int* arr, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(arr + i));
}
printf("\n");
}
void print2(int(*p)[10], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ",(*p)[i] );//arr
}
printf("\n");
}
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);
return 0;
}
3.3.2 在二维数组中的使用(多用)
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
int i = 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 (*p)[5]
{
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
//*(p+i) -> p[i]
//*(*(arr+i)+j)相当于第i行的地址
//*(*(p+i)+j)
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {{1,2,3,4,5},{6,7,8,9,10},{1,3,5,7,9}};
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
3.4 回顾
int arr[5];//整型数组
int *parr1[10];//指针数组
int (*parr2)[10];//数组指针
int (*parr3[10])[5];//数组
//int(*)【5】
//parr【3】是存放指针的数组
4. 数组参数、指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
4.1 一维数组传参
//数组传参,形参可以是数组,也可以是指针
void test(int arr[])//可行,数组传参本质是传递地址
{}
void test(int arr[10])//形参是数组
{}
void test(int* arr) //形参是指针
{}
void test2(int* arr[20])//形参是数组
//也可以写成int* arr[]
{}
void test2(int** arr)//形参是指针的形式
{}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
4.2 二维数组传参
void test(int arr[3][5])//ok
{}
void test(int arr[][])//error二维数组不可行列都省略
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int* arr)//一行的地址,一维数组的地址,与二维数组不匹配
{}
void test(int* arr[5])//error要指针,int* arr[5]为指针数组,是数组
{}
void test(int(*arr)[5])//ok,一维数组指针
{}
void test(int** arr)//传递行的地址,一维数组的地址,不可用二级指针接收
//二级指针用来接收一级指针变量的地址
{}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
4.3 一级指针传参
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;
}
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
比如:
void test1(int *p)
{}
int a=0;
test(&a);
int* ptr=&a;
test(ptr);
int arr[10];
test(arr);
4.4 二级指针传参
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
test(&p);
int **pp = &p;
test(pp);
int* arr[10];
test(arr);
return 0;
}
当函数的参数为二级指针的时候,可以接收什么参数?
void test(char **p)
{}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);
return 0;
}
5. 函数指针
//函数指针-也是指针-指向函数的指针
int Add(int x, int y)
{
return x + y;
}
int main()
{
int arr[10] = { 0 };
//两者打印值相同
printf("%p\n", &arr);//数组首元素地址
printf("%p\n", arr);//数组地址
//Add和&Add拿到的都是函数的地址,没有区别
printf("%p\n", Add);
printf("%p\n", &Add);
//pf就是函数指针变量
int(*pf)(int, int) = Add;//&Add
int sum=(*)pf(3,5);
//也可写成:
int sum=pf(3,5);
//类比:int sum = Add(3, 5);
return 0;
}
5.1 照猫画虎
int test(const char* str, double d)
{}
int main()
{
int (*pt)(const char*,double) = &test;
int (*pt)(const char* str, double d) = &test;
return 0;
}
5.2 保存函数的地址
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();//存放
//pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void
void *pfun2();
//能给存储地址,就要求pfun1或者pfun2是指针
5.3 阅读两段有趣的代码:
5.3.1 代码1
(*(void (*)())0)();
//void (*)()函数指针类型
//(void (*)())0 把0当作一个函数的地址
// * - 解引用 ()- 函数调用
// sum:把0直接转换成一个void(*)()的函数指针,然后去调用0地址处的函数
// 也可写成:
((void (*)())0)();
5.3.2 代码2
void (*signal(int, void(*)(int)))(int);
//上述代码是一次函数声明
//声明的函数叫:signal
//函数的第一个参数是int类型
//函数的第二个参数是一个函数指针类型
//该函数指针指向的函数,参数是int,返回类型是void
//signal函数的返回类型也是一个函数指针类型void(*)(int)
注 :推荐《C陷阱和缺陷》
这本书中提及这两个代码。
5.3.3 简化代码2
void (*signal(int, void(*)(int)))(int);
//采用:
typedef unsigned int unit;
两种写法:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
6. 函数指针数组
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,
比如:
int *arr[10];
//数组的每个元素是int*
6.1 定义函数指针的数组
int (*parr1[10])();//函数指针的数组
int *parr2[10]();
int (*)() parr3[10];
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?是 int (*)() 类型的函数指针。
6.2 函数指针数组的使用
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x + y;
}
int main()
{
//可以存放多个【参数相同、返回类型相同】的函数的地址
//函数指针数组:
int (* pfArr[2])(int, int) = { Add,Sub };
//指针int (*pf)(int, int)
int ret=pfArr[0](2, 3);
printf("%d\n", ret);
ret = pfArr[1](2, 3);
printf("%d\n", ret);
return 0;
}
6.3 计算器
写一个整数的加、减、乘、除
6.3.1 switch…case语句写法
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:sum ****\n");
printf("**** 3:mul 4:div ****\n");
printf("**** 0:exit ****\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;
}
6.3.2 使用函数指针数组的实现
函数指针数组的用途:转移表
//加减乘除的函数实现部分同上
void menu()
{
printf("*************************\n");
printf("**** 1:add 2:sum ****\n");
printf("**** 3:mul 4:div ****\n");
printf("**** 0:exit ****\n");
printf("*************************\n");
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
while (input)
{
menu();
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;
}
6.3.3 do…while 语句
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
//函数指针数组--转移表
int (*pfArr[5])(int, int) = { 0,add,sub,mul,div };
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算机\n");
break;
}
if (input >= 1 && input <= 4)
{
printf("输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("%d\n", ret);
}
else
printf("选择错误\n");
} while (input)
return 0;
}
6.3.4 回调函数实现
void calc(int(*p)(int, int))
{
int x, y;
int input = 1;
int ret = 0;
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret =p(x, y);
printf("ret = %d\n", ret);
}
//switch语句中写:
case 1:
calc(add);
7. 指向函数指针数组的指针
指向函数指针数组的指针是一个 指针
指针指向一个 数组 ,数组的元素都是 函数指针 ;
//函数指针:
int (*pf) (int,int);
//函数指针数组:
int (*pfArr[4]) (int,int);
//指向函数指针数组的指针
int (*(*ptr)[4]) (int,int)=&pfArr;
7.1 定义
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;
}
8. 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
8.1 qsort函数的使用:
C语言标准库提供的的排序函数(快排)
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{
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), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
8.2 模拟实现qsort
使用回调函数,模拟实现qsort(采用冒泡的方式)。
#include<stdio.h>
#include<stdlib.h>
int cmp_int(const void* e1, const void* e2)
{
if (*(int*)e1 > *(int*)e2)
return 1;
else if (*(int*)e1 < *(int*)e2)
return -1;
else
return 0;
}
void print(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
printf("%d ", arr[i]);
printf("\n");
}
int main()
{
int arr[] = { 2,1,3,7,5,9,6,8,0,4 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
print(arr, sz);
return 0;
}
8.2.1 void*
void*的指针是非常宽容的,可以接收任意类型的地址
char ch='w';
int i=20;
void* p=&ch;
void* p=&i;
*p=200;//写法错误:无具体类型的指针,解引用后值类型未知
*(int*)p=200;//正确写法,先用指定类型解引用,再进行修改
p++;//写法错误