预备知识
内存地址
- 字节:字节是内存的容量单位,英文名Byte,一个字节有8位,即1Byte = 8bits
- 地址:系统为了便于区分每一个字节而对它们逐一进行编号,称为内存地址,简称地址。int a = 5;
基地址
- 单字节数据:对于单字节数据而言,其地址就是其字节编号。
- 多字节数据:对于多字节数据而言,其地址是其所有字节中编号最小的那个,称为基地址(首地址)。
取址符
-
每个变量都是一块内存,都可以通过取址符
&
获取其地址。 -
例如:
int a =100; printf("整型变量a的地址是:%p\n",&a); int c = 'x'; printf("字符变量c的地址是:%p\n",&c); // 地址采用12位16进制数表示
-
注意:
- 虽然不同的变量的尺寸是不同的,但是它们的地址的尺寸是一致的。
- 不同的地址虽然形式上看起来是一样的,但由于它们代表的内存尺寸类型都不同,因此它们在逻辑上是严格区分的。
为什么要引入指针
- 为函数修改实参提供支持。
- 为动态内存管理提供支持
- 为动态数据结构(链表,队列等)提供支持。
- 为内存访问提供另一只 途径。
变量指与指针变量
指针概述
-
内存地址:系统为了内存管理的方便,将内存划分为一个个的内存单元的(通常是1个字节),并为每一个内存单元进行编号,内存单元的编号称之为该内存单元的地址。一般情况下,我们每一个变量都是由多个内存单元构成的,多以每个变量的内存地址,其实就是这个变量对应的第一个内存单元的地址,也叫基地址/首地址。
-
变量指针:变量指针称之为该变量的指针(本质是地址)。变量地址往往是指在 内存中第一个内存单元的编号(首地址)
int a; &a --- 变量a的地址,也称为变量a的指针 int arr[2]; &arr --- 数组arr的地址,也称为数组的指针
指针就是地址,地址就是指针
-
指针变量:专门存放指针的变量(本质是变量),简单来说,用来存放地址的变量就是指针变量。
-
指向:指针变量中存放谁的地址,就说明该指针变量指向了谁
-
指针的尺寸
- 指针的尺寸,指的是指针所占内存的字节数
- 指针所占内存,取决于地址的长度,而地址的长度则取决于系统的寻址范围,即字长
- 结论:指针尺寸只跟系统的字长有关系,跟具体的指针的类型无关。
-
在C语言中对内存数据(如变量、数组元素等)的存取有两种方式:
-
直接存取:
-
通过基本类型(整型、浮点型、字符型)的变量,访问这个变量代表的内存空间的数据
-
通过数组元素的引用,访问这个引用代表的内存空间的数据。
int a = 10;// 存 printf("%d",a); // 取 int arr[] = {11,22,33}; // 存 arr[0] = 66; // 存 printf("%d",arr[0]); // 取
-
-
间接存取:
-
通过指针变量,间接访问内容中的数据。
-
*
:读作解引用int main() { // 定义一个普通变量 int i = 3; // 定义一个指针变量,并赋值 int *i_point = &i; // 指针变量的数据类型要和存储的地址变量类型一致 // 访问普通变量(直接访问) printf("直接访问:%d\n", i); // 访问指针(地址访问)%p访问地址 printf("地址访问:%p,%p\n", i_point, &i); // 访问指针变量(指针访问)解引用:通过指针变量存储的地址,访问这个地址对应空间的数据 printf("间接访问:%d", *i_point); // 3 // i_point(地址) 和 *i_point(数据) return 0; }
-
指针变量的定义
语法:
数据类型 *变量列表;
举例:
int a; // 普通变量,拥有真实的数据存储空间 int *a_, *b_; // 指针变量无法存储数据,只能存储其他变量的地址
注意:
-
虽然定义指针变量
*a
,是在变量名前加上*
,但是实际变量名依然为a
,而不是*a
; -
使用指针变量间接访问内存数据时,指针变量必须要有明确的指向;
-
如果想要借助指针变量间接访问指针变量保存的内存地址上的数据,可以使用指针变量前加
*
来间接访问。指针变量前加*
,我们称之为对指针变量的解引用
。int i = 5, *p; p = &i; // 将i的地址赋给指针变量p printf("%x,%p\n",p ,p); // 两个都是打印地址,%x打印的地址不带0x,%p打印的地址带0x printf("%d\n", *p); // 间接访问i的值,也被称为解引用p对应地址空间的值。 *p = 10; // p访问地址 ,*p访问地址空间对应的数据
-
指针变量只能指向同类型的变量,借助指针变量访问内存,一次访问的内存大小是取决于指针变量的类型。
-
指针变量在定义时可以初始化:这一点和普通变量是一样的。
int i = 5; int *p = &i; // 将i的地址赋值给指针变量p printf("%d\n", *p); // 解用于p,通过地址间接访问i
-
指针变量的使用
使用
-
指针变量的赋值
// 方式1 int a, *p; p = &a; // 指针变量的值是其他变量的地址 // 方式2 int a, *p, *q = &a; p = q;
-
操作指针变量的值
int a, *p, *q = &a; p = q; printf("%p", p); // 此时返回的是变量a的地址空间
-
操作指针变量指向的值
int a = 6, *q = &a, b = 25; *q = 10; printf("%d,%d", *q, a); // 10,10 q = &b; printf("%d,%d", *q, a); // 25, 10
两个有关运算符的使用
&
取值运算符。&a是变量a的地址。*
指针运算符(或称之为"间接访问"运算符,解引用符),*p是指针变量p指向的对象的值。
案例1
需求:通过指针变量访问整型的变量。
代码:
#include <stdio.h>
void main()
{
int a = 3, b = 4, *pointer_1 = &a, *pointer_2 = &b;
printf("a=%d,b=%d\n", *pointer_1, *pointer_2);
}
案例 2:
需求:声明a,b两个普通变量,使用简介存取的 方式实现数据的交换。
代码:
#include <stdio.h>
void main()
{
int a = 3, b = 4, *p_a = &a, *p_b = &b;
}
指针变量做函数参数
指针变量做函数参数往往传递的是变量的地址(首地址),借助于指针变量间接访问是可以修改实参变量数据的。
案例1:
需求:要求函数处理,用指针变量做函数的参数
-
方式1:交换指向(指向的普通变量的值不变)
#include <stdio.h> // 自定义一个函数,实现两个数的比较 void swap(int *p_a, int *p_b) { int *p_t; // 这种写法,只会改变指向,不会改变地址对应空间的数据 p_t = p_a; p_a = p_b; p_b = p_t; printf("%d > %d", *p_a, *p_b); } void main() { int a = 3, b = 5; // int *p_a = &a, *p_b = &b; // if(a < b) swap(p_a,p_b); if(a < b) swap(&a,&b); else printf("%d > %d\n",a ,b); }
-
方式2:交换数据(指向的普通变量的值改变)
#include <stdio.h> // 自定义一个函数,实现两个数的比较 void swap(int *p_a, int *p_b) { int *t; // 这种写法,改变的是指针指向地址空间的数据 t = *p_a; *p_a = *p_b; *p_b = t; printf("%d > %d", *p_a, *p_b); } void main() { int a = 3, b = 5; // int *p_a = &a, *p_b = &b; // if(a < b) swap(p_a,p_b); if(a < b) swap(&a,&b); else printf("%d > %d\n",a ,b); }
数组指针与指针数组
指针变量指向数组
数组元素的指针
-
数组的指针就是数组中第一个元素的地址,也就是数组的首地址。
-
数组元素的指针是数组元素的首地址。因此,同样可以用指针变量来指向数组或数组元素。
-
在C语言中,由于数组名代表数组的首地址,因此,数组名实际上 也是指针。
// 定义了一个普通数组 int a[] = {11,22,33}; // 使用指针变量存储数组的第一个元素的首地址,也就是数组的首地址 int *p = &a[0]; // 数组的首地址 // 在C语言中,由于数组名代表数组的首地址,因此,数组名实际上也就是指针 int *p = a;
注意:虽然我们定义了一个指针变量接收数组的地址,但不能理解为指针变量指向了数组,而应该理解为指向了数组的元素。
指针的运算
指针运算:指针变量必须要 指向数组的 某个元素。
序号 | 指针运算 | 说明 |
---|---|---|
1 | 自增:p++、++p、p += 1 | 让指针变量指向下一个元素 |
2 | 自减:p–、–p、p -= 1 | 让指针变量指向上一个元素 |
3 | 加一个数:p+1 | 下一个元素的(首)地址 |
4 | 减一个数:p-1 | 上一个元素的(首)地址 |
5 | 指针相减:p1-p2 | p1,p2之间相差几个元素 |
6 | 指针比较:p1 < p2 | 前面的指针小于后面的指针 |
小贴士:
① 如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的 下一个元素,p-1指向同一数组中的上一个元素。即p+1或p-1也表示地址。但要注意的是,虽然指针变量p中存放的是地址,但p+1并不表示该地址加1,而表示在原地址的基础上加了该数据类型所占的字节数d。
② 如果p原来指向a[0],执行++p后p的值改变了,在p的原值基础上加d,这样p就指向数组的下一个元素a[1]。d是数组元素占的字节数。
③ 如果p的初值为&a[0]则p+i 和a+i 就是数组元素a[i]的地址,或者说,它们指向a数组的第 i个元素 。
④ (p+i) 或(a+i)是p+i或a+i所指向的数组元素,即a[i]。
⑤ 如果指针变量p1和p2都指向同一数组,如执行p2-p1,结果是两个地址之差除以数组元素的长度d。
#include <stdio.h>
int main(int argc,char *argv[])
{
int arr[] = {11,22,33,44,55};
int *p1 = arr + 4;
int *p2 = arr + 1;
printf("%ld\n",p1 - p2);// 3
return 0;
}
案例1:
// 定义一个普通数组
int arr[] = {11,22,33,44,55};
// 计算数组的大小 = 数组总字节数/一个元素的字节
int len = sizeof(arr)/sizeof(arr[0]);
// 创建指针变量
int *p = arr;
// 创建循环变量
register int i = 0; // 将变量存放在寄存器中,提高执行效率
// 遍历
for(; i < len; i++)
{
printf("[1] %d ",arr[i]); // 下标法,这种写法,可读可写
printf("[2] %d ", *(arr + i)); // 指针法,这里的只读,指的是arr不能被重新赋值
printf("[3] %d ", *(p + i)); // 指针法,这种更为灵活,可读可写,指的是p可以
printf("\n");
}
通过指针引用数组元素
引用一个数组元素,可以用:
① 下标法:如a[i]形式。
② 指针法:如*(a+i)
或者*(p+i)
,其中a是数组名,p是指向数组元素的指针变量。其初值:p = a;
案例
需求:有一整型数组a,有10个元素。输出数组中的全部元素。
分析:要输出各元素的值,有三种方法
-
下标法:通过改变下标输出所有元素
#include <stdio.h> void main() { int arr[10]; int i; // 给数组元素赋值 for(i = 0; i < 10; i++) scanf("%d", &arr[i]); // 遍历数组 for(i = 0; i < 10; i++) prontf("%-4d", arr[i]); }
-
指针法:通过数组名计算出数组元素的地址,找出数组元素值
#include <stdio.h> void main() { int arr[10]; int i; // 给数组元素赋值 for(i = 0; i < 10; i++) scanf("%d", arr+i); // 遍历数组 for(i = 0; i < 10; i++) prontf("%-4d", *(arr+i)); }
-
指针法:(用指针变量指向数组元素)
#include <stdio.h> void main() { int arr[10]; int *p; int i; p = arr; // 给数组元素赋值 for(i = 0; i < 10; i++) scanf("%d", p+i; // 遍历数组 for(p = arr; p < (arr + 10); p++) prontf("%-4d", *p); }
以上3种写法比较:
- 第①种写法和第②种写法执行效率相同。系统是将arr[i]转换为*(arr+i)处理的,即先计算出地址,因此比较费时。
- 第③种方法比第①②种方法快。用指针变量直接指向数组元素,不必每次都重新计算地址。(p++)能大大提高执行效率。
- 用下标法比较直观,而用地址法或者指针变量的方法难以很快判断出当前处理的元素。
使用指针变量指向数组元素时(上面第③种写法),注意以下三点:
①
*(p--)相当于arr[i--],先*p,再p--
②
*(++p)相当于arr[++i],先++p,再*p
③
*(--p)相当于arr[--i],先--p,再*p
数组名作函数参数
表现形式:
-
形参和实参都是数组 名
void fun(int arr[], int len ){} { int arr[] = {11,22,33}; fun(arr,sizeof(arr)/sizeof(arr[0])) }
-
实参用数组名,形参用指针变量
void fun(int *p, int len ){} { int arr[] = {11,22,33}; fun(arr,sizeof(arr)/sizeof(arr[0])) }
-
实参形参都用指针变量
void fun(int *p, int len ){} { int arr[] = {11,22,33}; int *p = arr; fun(p,sizeof(arr)/sizeof(arr[0])) }
-
实参为指针变量,形参为数组名
void fun(int arr[], int len ){} { int arr[] = {11,22,33}; int *p = arr; fun(p,sizeof(arr)/sizeof(arr[0])) }
案例:
需求:将数组a中n个整数按相反顺序存放
代码:
#include <stdio.h>
/**
* 数组的反转:数组实现
* */
void inv(int arr[], int len)
{
// 反转思路:将第0个和n-1个进行 对调,将第1个和n-2个对调
// 定义循环变量
int i = 0, temp;
// 遍历数组
for( ; i < len / 2; i++ )
{
// 交换
temp = arr[i];
arr[i] = arr[len-1-i];
arr[len-1-i] = temp;
}
}
/**
* 遍历数组
* */
void get(const int arr[], int len)
{
for(int i = 0; i < len; i++)
{
printf("%-4d", arr[i]);
}
printf("\n");
}
/**
* 数组反转:指针实现
* const 给变量的数据类型前面添加const,代表这个变量是只读变量,无法对此做出修改
* */
void inv2(int *p,const int len)
{
// 定义循环变量
int *i = p, *j = &p[len - 1],temp;
// 遍历数组
for(; i < j; i++,j--)
{
temp = *i;
*i = *j;
*j = temp;
}
}
int main()
{
int arr[] = {11,22,33,44,55,66};
get(arr,sizeof(arr)/sizeof(arr[0]));
inv2(arr,sizeof(arr)/sizeof(arr[0]));
get(arr,sizeof(arr)/sizeof(arr[0]));
inv2(arr,sizeof(arr)/sizeof(arr[0]));
get(arr,sizeof(arr)/sizeof(arr[0]));
return 0;
}
数组指针与指针数组
数组指针
概念:数组指针是指向数组的指针,本质上还是指针
特点:
- 现有数组,后有指针
- 它指向的是一个完整的数组
一维数组指针:
-
语法:
数据类型 (*指针变量名)[容量];
-
案例:
#include <stdio.h> int main() { // 一维数组指针 // 现有数组再有指针 int arr[] = {100,200,300}; // 获取数组元素的个数 int len = sizeof(arr) / sizeof(arr[0]); // 定义一个数组指针,指向arr这个数组 int (*p)[3] = &arr; // 此时p不是指向arr数组的第一个元素,而是指向arr这个数组本身 printf("%x,%x,%x,%x\n", p, arr, &arr[0], &arr); printf("%x\n", p); // p++; //此时p++会跳出整个数组,访问到一块未知的内存,程序中避免这种写法 // printf("%p\n", p); // 如何访问数组指针 printf("%d\n", (*p)[2]); // 300 // 遍历 for( int i = 0; i < len; i++ ) { printf("%d ", (*p)[i]); } printf("\n"); return 0; }
我们之前所学的是指向数元素的指针,本质上还是指针变量
二维数组指针
-
语法
数据类型 (*指针变量名)[行容量][列容量];
-
案例
#include <stdio.h> int main(int argc,char *argv[]) { // 创建一个普通的二维数组 int arr[][3] = {10,20,30,100,200,300,1000,2000,3000}; // 创建一个二维数组指针 // 一个二维数组本质上还是一个一维数组,只不过它的元素也是数组 int (*p)[3][3] = &arr; printf("%d\n",(*p)[1][0]); // 遍历 for(int i = 0; i < sizeof(arr)/sizeof(arr[0]);i++) { int len = sizeof(arr[i])/sizeof(int); for(int j = 0; j < len; j++) { printf("%-4d",(*p)[i][j]); } printf("\n"); } printf("\n"); return 0; }
方法2:
#include <stdio.h> int main(int argc,char *argv[]) { // 创建一个普通的二维数组 int arr[][3] = {10,20,30,100,200,300,1000,2000,3000}; // 创建一个二维数组指针 // 一个二维数组本质上还是一个一维数组,只不过它的元素也是数组 int (*p)[3] = arr; // 取二维数组的第一个元素 {10,20,30} printf("%d\n",(*p)[0]);// 10 // 获取元素2000 printf("2000-%d,%d,%d",*(*(p+2)+1),*(p[2]+1),p[2][1]);// *(*(p+1)+2) 300 return 0; }
-
指针和数组中符号优先级:
() > [] > *
-
通过指针引用多维数组
案例:
需求:用指向元素的指针变量指向二维数组的元素的值
#include <stdio.h>
int main()
{
/**
* 用指向元素的指针变量指向二维数组的元素的值
* */
// 定义一个普通的二维数组
int a[3][4] = {01,02,03,04,
11,12,13,14,
21,22,23,24};
// 定义一个指针变量 ,用来接收二维数组的元素值
int *p = a; // &a[0][0]
while( p <= &a[2][3] )
{
if( (p - a[0])%4 == 0) printf("\n");
printf("%4d", *p);
p++;
}
return 0;
}
案例2:
需求:用指向
#include <stdio.h>
int main()
{
/**
* 用指向元素的指针变量指向二维数组的元素的值
* */
// 定义一个普通的二维数组
int a[3][4] = {10,20,30,40,
11,12,13,14,
21,22,23,24};
// 创建一个一维的数组指针指向一个二维数组
int (*p)[4] = a; // 等价于 &arr[0],p代表我们这个二维数组
// 创建两个变量,代表我们对应数据的行和列
int row,col;
// 通过控制台来输入
printf("请输入行号和列号:");
scanf("%d,%d", &row, &col);
printf("arr[%d][%d] = %d", row, col, *(*(p + row)+col)); // *(p[row]+col) | p[row][col]
return 0;
}
指针数组
概念:指针数组是一个数组,数组中的每一个元素都是一个指针
特点:
- 先有指针,后有数组
- 指针数组的本质是一个数组,只是数组中的元素类型为指针
语法:
数据类型 *数组名[容量];
案例:
#include <stdio.h>
int main()
{
/**
* 指针数组
* */
// 定义三个变量
int a = 10, b = 20, c = 30;
// 定义指针数组,指针数组用来存放指针的
int *arr[3] = {&a,&b,&c};
// 获取数组大小
int len = sizeof arr/sizeof arr[0];
// 遍历数组
for( int i = 0; i < len; i++ )
{
printf("%-4d ",*arr[i]); // 等价于**(arr + i)
}
printf("\n");
return 0;
}
建议:我们一般使用指针数组处理字符串。
字符数组和字符指针
字符串实现
在C语言中,表示一个字符串有以下两种形式:
- 用字符数组存放一个字符串。
- 用字符指针指向一个字符串。
案例
#include <stdio.h>
int main()
{
/**
* 方式1:使用字符数组实现字符串
* */
char str[] = "I Love You"; // 字符串常量存放在常量池,常量池在代码区(方法区)
printf("%s", &str[0]);
/**
* 使用字符指针实现字符串
* */
char *str1 = "I Love You";
printf("%s",str1);
return 0;
}
注意:字符数组和字符指针变量都能实现字符串的储存与运算。(字符指针–>字符类型的指针变量)
字符 数组和字符指针的联系
-
字符数组由元素组成,每个元素中存放 一个字符,而字符指针变量中存放的是地址,也能作为函数参数。
-
只能对字符数组中的各个元素赋值,而不能用赋值语句对整个字符数组赋值。
char arr[3]; arr[2] = 'A'; // 正确,对 字符数组中的元素赋值 arr = {'A','b','C'}; // 错误,(可以理解 为数组名就是一个常量,也就是一旦创建,就不能改变)
-
字符数组名虽然代表地址,但数组名的值不能变,因为数组名是常量
-
对于字符串中字符的存取,可以用下标法,也可以用指针。
案例:
#include <stdio.h>
int main(int argc,char *argv[])
{
// 使用两种方式定义字符串
char str1[] = "你好,张欣!";
char *str2 = "你好,张欣!";// 我们将数据类型为char的指针变量称之为字符指针
// 测试赋值
// str1 = "你好,张鹏!"; // 不能对字符数组整体赋值,如果要赋值,请使用string库下strcpy();
str2 = "你好,张鹏!";
// 打印输出
printf("%s,%s\n",str1,str2);
char a[] = "I Love You!";
char *b = "I Love You!";
// 使用下标法和指针法来访问字符串
printf("%c,%c,%c,%c\n",a[2],*(a+2),b[2],*(b+2));
return 0;
}
字符串作为形式参数
-
实参与形参都可以是字符数组
void fun(char str[], int len){..} void main() { char str[] = "hello"; fun(str,sizeof(str)/sizeof(str[0])); }
-
实参用字符数组,形参用字符指针
void fun(char *str,int len) { str[2] = 'A'; } void main() { char str[] = "hello"; //常量池,此时的赋值,将常量池中的数据读取出来,存入栈中数组对应的位置 fun(str,sizeof(str)/sizeof(str[0])); }
-
实参和形参都是指针变量(在函数内部不能对字符串中的字符做修改)
void fun(char *str,int len) { str[2] = 'A'; // 错误,常量不能被改变 } void main() { char *str = "hello"; // 此时,str指向常量池中的地址 fun(str,sizeof(str)/sizeof(str[0])); }
-
实参是指针类型,形参是字符数组(在函数内部不能对字符串中的字符做修改)
void fun(char str[],int len){ str[2] = 'A'; // 错误,字符串常量一旦创建,就不能被改变 } void main() { char *str = "hello"; fun(str,sizeof(str) / sizeof(str[0]); }
注意:
- 字符数组在创建的时候,会在内存中开辟内存空间,内存空间可以存放字符数据;字符指针在创建的时候,需要依赖于字符数组,字符指针在内存开辟的内存空间中,存放的是数组 元素的地址。字符指针的创建依赖于字符数组,字符数组可以独立存在,而字符指针不能独立存在。
- 字符数组可以初始化,但不能赋值:字符指针可以初始化,也可以赋值。
案例:
#include <stdio.h>
/**
* 定义一个函数,实现字符串的拷贝,返回字符串长度
* @param source 拷贝的源字符串
* @param target 需要保存拷贝数据的目标字符串
* @return 字符串的大小
*/
int str_copy(char *source,char *target)
{
// 定义一个循环变量
int i = 0;
while(source[i]!='\0')
{
// 实现拷贝
*(target+i) = *(source+i);// 指针法
// target[i] = source[i];// 下标法
i++;
}
// 拷贝结束后,一定要给target末尾加上\0
target[i] = '\0';
return i;
}
int main(int argc,char *argv[])
{
// 定义两个数组,从键盘录入字符串
char source[20],target[20];
printf("请输入一个字符串:\n");
scanf("%s",source);
int len = str_copy(source,target);
printf("%s,%s,%d\n",source,target,len);
return 0;
}
案例:
#include <stdio.h>
/**
* 定义一个函数,实现字符串的截取
* @param source 源字符串
* @param start 开始截取的位置
* @param end 截取结束的位置
* @param target 截取后的字符串
* @return 新字符串长度
*/
int str_split(char *source,int start,int end,char *target)
{
// 定义循环变量
int i = 0, k = 0;
// 遍历源字符串(数组)
while(source[i] != '\0')
{
// 根据位置截取
if(i >= start && i < end)
{
// 将截取的字符串存入target "helloworld"
target[k] = source[i];
k++;
}
i++;
}
// 新字符串需要末尾添加\0
target[k] = '\0';
return k;
}
int main(int argc,char *argv[])
{
char *str = "abcdefg";
char target[100];
int len = str_split(str,2,5,target);
printf("%s,%s,%d\n",str,target,len);
return 0;
}
函数指针与指针函数
函数指针
定义:函数指针本质上是指针,它是函数的指针(定义了一个指针变量,变量中存储了函数的地址)。函数都有一个入口地址,所谓指向函数的指针,就是指向函数的入口地址。这里函数名就代表入口地址。
函数指针存在的意义:
- 让函数多了一种调用方式
- 函数指针作为形参,可以形式调用(回调函数)
定义格式:
返回值类型 (*变量名) (形式参数列表);
举例:
int (*p)(int a,int b);
函数指针的初始化:
-
定义的同时赋值
// 函数指针需要依赖于函数,先有函数,再有指针 //定义一个函数 int add(int a,int b){return a+b;} // 定义一个函数指针,并给它赋值 // 通过以下代码我们发现:函数指针的返回类型和依赖函数的返回类型一致,函数指针的参数个数和依赖函数一致 int (*p)(int a,int b) = add; // 赋值一定要注意:函数不能带有()
-
先定义后赋值
//定义一个函数 int add(int a,int b){return a+b;} // 定义一个函数指针 //int (*p)(int a,int b); int (*p)(int)(int); // 一般写作这种 // 给函数指针赋值 p = add;
注意:
- 函数指针指向的函数要和函数指针定义的返回值类型,形参列表对应,否则编译报错
- 函数指针是指针,但不能指针运算,如p++等,没有实际意义
- 函数指针作为形参,可以形成回调
- 函数指针作为形参,函数调用时的实参只能是与之对应的函数名,不能带小括号()
- 函数指针的形参列表中的变量名可以省略
案例
#include <stdio.h>
/**
* 定义一个函数指针
*/
int max(int a,int b)
{
if(a > b)
return a;
return b;
}
int main(int argc,char *argv[])
{
// 定义测试数据
int a = 3,b = 2,c;
// 直接函数调用
c = max(a,b);
printf("%d,%d两个数中的最大值是%d\n",a,b,c);
// 定义一个函数指针
int (*p)(int,int) = max;
// 间接函数调用
c = p(a,b);
printf("%d,%d两个数中的最大值是%d\n",a,b,c);
c = (*p)(a,b);
printf("%d,%d两个数中的最大值是%d\n",a,b,c);
return 0;
}
回调函数
概念
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个
函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数
的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进
行响应。
为什么要用回调函数
因为可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特
定原型和限制条件的被调用函数。
简而言之,回调函数就是允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数
在处理相似事件的时候可以灵活的使用不同的方法。
实现
/**
* 回调函数1
*/
int callback_1(int a)
{
printf("hello,this is callback_1:a=%d\n",a);
return 0;
}
/**
* 回调函数2
*/
int callback_2(int b)
{
printf("hello,this is callback_2:b=%d\n",b);
}
/**
* 实现回调函数
*/
int handle(int x,int (*callback)(int))
{
printf("开始执行!\n");
callback(x);
printf("执行结束!\n");
}
int main(int argc,char *argv[])
{
handle(100,callback_1);
handle(200,callback_2);
return 0;
}
指针函数
定义:本质上是函数,这个函数的返回值类型是指针,这个函数称为指针函数。
语法:
指针类型 函数名(形参列表)
{
函数体;
return 指针变量;
}
举例:
int *get(int a) // 与下面的等价
int* get(int a)
{
int *b = &a;
return b;
}
int main()
{
int *a = get(5);
printf("%d\n", *a);
}
注意:
在函数中不要返回一个局部变量的地址,因为函数调用完毕后,局部变量会被回收 ,使得返回的地址就不明确,此时返回的指针就是野指针。
解决方案:
如果非要访问,可以给这个局部变量添加static
,可以延长它的生命周期,从而避免野指针(尽量少用,因为存在内存泄漏)
案例:
#include <stdio.h>
/**
* 定义一个函数,传入学生的序号,返回这个学生的所有课程成绩
* @param p 二维数组
* @param n 学生索引(二维数组中行号)
* @return 学生成绩(行号对应的列数组)
*/
float* search(float (*p)[4],int n)
{
// 定义一个指针,用来接收查询到的某个学生的所有课程
float *pt;
pt = *(p+n);// *(p+n),*p[n],p[n]
return pt;
}
int main(int argc,char *argv[])
{
// 准备一个二维数组
float score[][4] = {{60,70,80,89},{55,66,77,88},{90,89,90,91}};
int m;
float *p;
printf("请输入学生序号(0~2):\n");
scanf("%d",&m);
printf("第%d个学生的成绩:\n",m);
// 用来接收某个学生的所有成绩
p = search(score,m);
// 遍历成绩
for(int i = 0;i < 4;i++)
printf("%5.2f\t",*(p+i));
printf("\n");
return 0;
}
二级指针
二级指针
说明:指针除了一级指针,还有多级指针,但是我们一般开发中最多用到二级指针。三级指针本质上和二级指针差不多。
定义:二级指针,又被称之为多重指针,引用一级指针的地址,此时这个指针变量就得定义成二级指针。
int a = 10; // 普通变量
int *p = &a; // 一级指针
int **w = &p; // 二级指针
int ***x = &w; // 三级指针
定义格式:
数据类型 **变量名 = 指针数组的数组名或者一级指针的地址
举例:
// 指针数组
int arr = {11,22,33};
int *arr_ = {&arr[0],&arr[1],&arr[2]};
// 一级指针
int a = 10;
int *p = &a;
// 二级指针
// 字符型指针数组,本质上是一个二维的char数组
char* str[3] = {"abc","aaa034","12a12"};
// 如果要用一个指针变量来接收,就需要一个二级指针
char* *p_ = str; // str指向首元素的地址
int arr[2][3]= {1,2,3,
11,22,33};
int **k = arr; // 编译报错,数据类型不相等(二维数组不等于二级指针)
结论:
- 二级指针和指针数组等效,和二维数组不等效。
- 二维数组和数组指针是等效,和二级指针等效
二级指针的用法:
- 如果是字符的二级指针,可以像遍历字符串一样遍历它
- 如果是其他二级指针。就需要解引用两次访问
main函数原型
定义:main函数有多种定义格式,main函数也是函数,函数相关的结论对main函数也有效(也可以定义main函数的函数指针)。
main函数的完整写法:
int main(int argc, char *argv[]){}
int main(int argc, char **argv){} // 等价上面的
扩展写法
int main(){} // 等价 main(){}
int main(void){}
void main(){}
说明:
1.argc,argv是形参,它们两可以修改
2.main函数的扩展写法有些编译器不支持,编译报警告
3.argc和argv的常规写法:
4.main函数是系统函数通过函数指针的回调形式调用的
指针重要案例
如果一个函数需要返回数组,建议将这个函数定义成指针函数(返回值为指针的函数)
如果一个被调函数需要接收主调函数传递一个非char类型的数组,建议被调函数的参数用数组指针
// 需求:定义一个函数求一个学生4门成绩的总和,数组原型{{67,68,69},{77,78,79},{97,98,99}}行:学生,列:学生对应的成绩 // 分析:根据需求,要求传递一个数组(数组指针),返回一个数组(指针函数) // 定义一个函数,从总成绩中,求某一个学生的成绩 // @param n 索引,表示某个学生 float* sum(float (*arr)[],int n) { return *(arr+n); } int main() { // 创建一个二维数组 float scores[3][3] = {{67,68,69},{77,78,79},{97,98,99}}; float * }
如果一个被调函数的参数是一个字符数组
{"qqq","aaa"...}
,建议将参数类型定义为字符指针数组char *arr[]
或者字符二级指针char **arr
// 需求:用一个指针数组,存储一组字符串,要求写一个函数取出数组中的字符串 int get_str(char **p,int n) { return *(p+n); } int main() { char *arr[3] = {"hello","wangwu","zhangsan"}; char *str = get_str(arr, a1); printf("%s\n", str); }
如果需要将一个函数作为另一个函数的形参,建议将该函数的形参用函数指针表示
int add(int a, int b){return a+b;} int jisuan(int a,int b,int (*ADD)(int,int)) { printf("开始计算:\n"); // 指行函数 ADD(a,b); } int main() { int a = 5,b = 3; jisuan(a,b,add); }
常量指针与指针常量
常量:分为字面量和只读常量,字面量就是我们平时直接操作的量:printf(“%d\n”,12);printf(“%s\n”,“hello”);只读常量使用关键字const
修饰,凡是被这个关键字修饰的变量,一旦赋值,值就不能被改变。
语法:
// 字面量
printf("%d\n",12);
// 只读常量
const int a = 10;
a = 21; // 编译错误。因为此时这个变量是只读常量,所以不可改变
常量指针:
定义格式:
const 数据类型 *变量名;
举例:
const int *p; // p就是常量指针
结论
- 常量指针指向的数据不能被改边(不能解引用间接修改数据)
- 常量指针的地址可以改变(指向是可以改变)。
应用场景:作为形式参数,实际参数需要给一个常量。
void foreach(const int *array,const int len){···}
案例:
#include <stdio.h>
/**
* 常量指针
* */
int main(int argc,char *argv[])
{
// 定义变量
int a = 10;
//定义常量指针
const int *p = &a;
//*p = 100; 编译报错
printf("%d\n",*p);
// 定义变量
int b = 20;
p = &b;
printf("%d\n",*p);
return 0;
}
指针常量
定义:指针的常量,指针的指向不能改变
定义格式:
数据类型* const 变量名;
举例:
int* const p; // 指针常量
案例:
#include <stdio.h>
/**
* 指针常量
* */
int main(int argc,char *argv[])
{
// 定义变量
int a = 10;
//定义常量指针
int* const p = &a;
*p = 100;
printf("%d\n",*p);
// 定义变量
int b = 20;
//p = &b; 编译报错,常量的地址不可被改变
printf("%d\n",*p);
return 0;
}
常量指针常量
定义语法:
const 数据类型* const 变量名;
举例:
const int* const p;
注意:p的指向不能被改变(地址不可更改),p指向的数据不能改变(地址对应的数据不可更改)
野指针、空悬指针、空指针
野指针
定义:指向一块未知区域(已经销毁或者访问受限的内存区域外的已存在或不存在的内存区域)的指针,被称作野指针。野指针是危险的。
危害:
- 引用野指针,相当于访问了非法的内存,常常会导致段错误(segmentation fault),也有可能编译运行不报错。
- 引用 野指针,肯能会破坏系统的关键数据,导致系统崩溃等严重后果
野指针产生的场景:
-
变量未初始化,通过指针访问该变量
int a; int* p = &a; // p是野指针 printf("%d\n", *p); // 访问野指针,数据不安全
-
指针变量未初始化
int* p; // p是野指针 printf("%d\n", *p);
-
指针指向的内存空间被(free)回收了
int *p = malloc(4); *p =12; // 此时不是野指针 free(p); printf("%d\n", *p);// 此时的p就是野指针
-
指针函数中直接返回了局部变量的地址
int* get_num() { int a = 15; int* p = &a; // 此时p对应的数据是一个局部作用域的数据 return p; } main() { int* p = get_num();// 此时p是野指针 }
-
如何避免野指针?
-
指针变量要及时初始化,如果暂时没有对应的值,建议赋初值NULL。
-
数组操作(遍历和指针运算)时,注意数组的长度,避免越界
-
指针指向的内存空间被回收,建议给这个指针变量赋值为NULL
int* p = (int*)malloc(10); free(p); p = NULL;
-
指针变量使用之前要检查它的有效性(非空检验)
int* p = NULL; if(!p) { return -1; }
-
空指针
很多情况下,我们不可避免的会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针指向的内存已经被释放了等等。一般的做法是将这些危险的野指针指向一块确定的内存,比如零地址内存(NULL)。
定义:空指针即保存了零地址的指针(赋值为NULL的指针),也就是指向零地址的指针。(NULL是空常量 ,它的值是0,这个NULL一般存放在0x00000000的位置,这个地址只能存放NULL,不能被其他程序修改)
示例:
// 1.刚定义的指针,让其指向零地址以确保安全
char* p1 = NULL;
int* p2 = NULL;
// 2.被释放了内存的指针,让其指向 零地址以确保安全
char *p3 = malloc(100);
free(p3);
p3 = NULL;
空悬指针
在C语言中,空悬指针指的是指向已删除(或释放)的内存位置的指针。如果一个指针指向的内存已经被释放,但指针本身并未重新指向其他有效的内存地址,那么这个指针就变成了空悬指针。空悬指针会引发不可预知的错误,并且如果一旦发生,就很难定位,因此在编程中尽量避免使用空悬指针。
void与void*的区别
定义:
- void:是空类型,是数据类型的一种
- void*:是指针类型,是指针类型的一种,可以匹配任意类型的指针,类似于通配符,又被叫做万能指针。
void:
-
说明:void作为返回值类型使用,表示没有返回值;作为形参,表示形参列表为空,在调用的时候不能给实参
-
举例:
// 函数定义 void fun(void){..} // 等效于void fun(){..} // 函数调用 fun(();)
void*:
-
说明;
-
void*是一个指针类型,但该指针的数据类型不明确,无法通过解引用获取内存中的数据,因为
void*
不知道访问几个内存单元。 -
void*是一种数据类型,可以作为函数
返回值类型
,也可以作为形参类型
-
void*类型的变量在使用之前必须强制类型转换,明确它能够访问几个字节的内存空间
int* p = (int*)malloc(4);
-
-
举例:
#include <stdio.h> #include <stdlib.h> // 函数定义 void* fun(void* p) // 指针函数(返回值类型是指针的函数,此时返回的是不明确类型,需要外部强转) { int *p; // double *p; // long *p; // char *p; return p; } // 函数调用 void main() { int m = 10; int *p = &m; void* a = fun(p);// 这种接收方式,实际上没有意义,推荐:int *a = (int*)fun(p); printf("%p\n",a);// 可以正常打印,打印出一个地址 *a = 10;// 编译报错,void*变量不能解引用访问数据 int *w = (int*)a; *w = 10;// 编译和运行正常,void*变量a在使用前已经强制类型转换了,数据类型明确了,访问的内存单元明确了。 }
-
说明:
- void*作为返回值类型,这个函数可以返回任意类型的指针
- void*作为形参类型,这个函数在调用时,可以给任意类型的指针
-
总结:
- void*类似于通配符,不能对
void*
类型的变量解引用(因为不明确数据类型,所以无法 确定内存单元的大小) - void*在间接访问(解引用)前要强制转换,但不能太随意,否则存和取的数据类型不一致
- void*类似于通配符,不能对