一、一级指针
指针也是一个变量,指针存放的内容是一个内存地址,该地址指向一块内存空间。
1.1 指针变量的定义
一级指针变量的数据类型会在基本数据了类型之后多了一个*
号,指针变量只能存放内存地址(一个16进制的数),不能将一个基本数据类型直接赋值给一个指针变量。
如果要取出一级指针变量指向的内存地址所对应的值的话,可以通过在指针变量前加一个*
号来获取
int *p;//表示定义一个指针变量p, 类型是int *;
*p;//代表获取该指针指变量指向的内存块对应的实际数据
int *p = 100; //错误, 只能指向内存地址, 是一个16进制的数
*p =100 ; //正确, 因为*p操作的是变量的值.
1.2 &取地址运算符
通过&
符号可以取得一个变量在内存当中的地址,然后就可以赋值给指针变量了。
#include<stdio.h>
int main()
{
int a = 100;
int *p;// 定义了一个int *类型的指针变量,名字叫做p
p = &a;// 给指针变量赋值,把a的内存地址赋值给指针变量p
*p = 200;//修改a的值,效果等于直接给a赋值, *p代表指针指向变量的值,而p代表指向变量的内存地址
printf("p=%p,*p=%d,a=%d\n",p,*p,a);
// 通常为了书写方便,可以直接这样给指针变量赋值
// int *p1 = &a;
// 直接修改a变量的值,内存地址并不会修改
a = 50;
// 查看*p的值
printf("p=%p,*p=%d,a=%d\n",p,*p,a);
return 0;
}
两次输出结果如下:
可以看到*p和a变量的值是一样的,并且修改值并不会影响p指针变量指向的内存地址。
1.3 无类型指针
定义一个指针变量,但不指定它指向具体哪种数据类型。可以通过强制转化将void *转化为其他类型指针,也可以用(void *)将其他类型指针强制转化为void类型指针。
void *p 他可以指向任意类型的内存地址
1.4 空指针与野指针
NULL在C语言中的定义为(void *)0
,它是一个宏常量, #define NULL 0 .如果一个指针变量没有明确指向一块内存地址, 那么就把这个变量指向NULL,这个指针就是空指针,空指针是合法的,例如:
int *p = NULL;
// 或者
int *p;
p = NULL;
指向NULL的指针叫空指针,没有具体指向任何变量地址的指针(也就是没有初始化值的指针)叫野指针。
#include<stdio.h>
int main()
{
// 定义了一个int *类型的指针变量p
int *p;
// 如果指针变量没有初始化就使用,那就是野指针,
// 由于它不知道指向的内存地址是啥,所以无法修改这块内存地址所对应的数值, 程序执行的时候会报错
*p = 100;
return 0 ;
}
1.5 指针的兼容性
指针之间赋值比普通数据类型赋值检查更为严格,例如:不可以把一个double *
赋值给int *
,它是不会自动类型转换的.
原则上一定是相同类型的指针指向相同类型的变量地址,不能用一种类型的指针指向另一种类型的变量地址。
1.6 常量指针与指针常量
1、指向常量的指针(常量指针)
常量指针的const书写在指针类型之前,例如:
const char *p;
定义一个指向常量的指针, 但是这个指针不能通过*p
的方式取修改变量的值了。但是可以通过*p的方式读取变量的值。
#include<stdio.h>
int main()
{
int a = 10; //定义一个变量
// 定义一个指向int类型的地址的指针变量,指向a变量的内存地址
const int *p = &a; // 指针类型是 const int *
*p = 100; // 编译会通过不了,常量指针的常量不能改变, 但是直接给a变量赋值还是可以的.只是不允许通过*p的方式
printf("a=%d\n",*p);
return 0;
}
2、指针常量
指针常量的const书写在指针类型之后,例如:
char * const p;
定义一个指针常量,一旦初始化之后其内容不可改变,也就是说不能再指向其他变量的地址了。但是可以通过*p的方式改变变量的值。
#include<stdio.h>
int main()
{
int a = 10; //定义一个变量
//定义一个指针常量,指向a变量的内存地址
int *const p = &a; //指针类型是 int *const
int b = 20; // 允许修改变量
p = (int *)&b;//编译通不过,因为p是一个常量,不能被修改,指针常量的指针不能修改
printf("%d\n",*p);
return 0;
}
原则上在C语言中常量指针的常量不能修改、指针常量的指针不能修改;但是C语言中通过const定义的常量是有问题的,因为可以通过指针变量来间接的修改常量的值,所以在C语言中用#define常量比较多,而C++的const是没办法改的。
1.7 指针与数组的关系
一个变量有地址,一个数组包含若干个元素,每个元素在内存中都有地址,而且是连续的内存地址.也就是说如果数组是char类型的,那么每个元素的内存地址就是相差1, 如果是int类型的数组, 那么元素间的内存地址是相差4,例如:
int a[10];
int *p = a;
p和&a[0]的地址,以及a数组名的地址都是一样的。 也就是说数组名的地址等于数组首元素的内存地址。
#include<stdio.h>
int main()
{
char a[10];
printf("a=%p\na[0]=%p\na[1]=%p\na[2]=%p\n",a,&a[0],&a[1],&a[2]);
printf("========================\n");
int b[10];
printf("b=%p\nb[0]=%p\nb[1]=%p\nb[2]=%p\n",b,&b[0],&b[1],&b[2]);
return 0;
}
输出结果如下:
a=0x16efa760e
a[0]=0x16efa760e
a[1]=0x16efa760f
a[2]=0x16efa7610
========================
b=0x16efa75e4
b[0]=0x16efa75e4
b[1]=0x16efa75e8
b[2]=0x16efa75ec
从结果也可以看出char数组的每个元素内存地址相差1, int数组的元素内存地址相差4.
注意:当指针变量指向一个数组名的时候,C语言语法规定指针变量名可以当做数组名使用.但是指针变量的sizeof不是数组的sizeof, 指针变量的sizeof在64位系统是8个字节, 32位系统是4个字节.
#include<stdio.h>
int main() {
int a[] = {1, 2, 3, 4, 5};
int *p; // 指针变量
// 数组可以直接赋值给指针变量,不用&取地址,因为变量名a的地址就是数组首元素的内存地址
p = a;
// 如果指针指向的是数组,那么变量名可以当做数组用
p[3] = 100;
// 数组的长度和指针变量的长度是不一样的
printf("数组长度=%lu字节,指针变量长度=%lu字节\n", sizeof(a), sizeof(p));
// 操作指针变量来操作数组元素
int i;
for (i = 0; i < 5; i++) {
printf("a[%d]=%d\n", i, p[i]);// 和操作数组一样操作元素
}
return 0;
}
输出结果如下:
数组长度=20字节,指针变量长度=8字节
a[0]=1
a[1]=2
a[2]=3
a[3]=100
a[4]=5
注意:指针变量如果指向的不是数组名或者数组的首元素的内存地址,那么用指针去取数组的元素的时候是有区别的,假设指针指向的是数组下标为2的元素的内存地址, 那么p[0] 就不再是等于a[0]了, 而是等于a[2]了, 同理p[1]就应该等于a[3],其他依次类推。
1.8 指针位移运算
指针运算不是简单的整数加减法,而是指针指向的数据类型在内存中占用字节数做为倍数的运算。
例如:char *p;
当执行p++;
后,移动的字节数是sizeof(char)
这么多的字节数;如果是int *p1;
那么p1++
后移动的字节数就是sizeof(int)
。
#include<stdio.h>
int main()
{
int a[5] = {0};
int *p = a;//指向数组名的指针,就是数组首元素的地址
*p = 100;// 给指针变量对应地址的变量赋值100,相当于p[0] = 100;
printf("移动前指针位置:%p\n",p);
//移动指针
//移动2,表示移动了sizeof(int) *2 个字节,对应就是数组下标=2的元素内存地址
p += 2;
*p = 20;//修改a[2]的值为20
printf("移动后指针位置:%p\n",p);
int i;
for(i = 0 ;i < 5; i++)
{
printf("a[%d]=%d\n",i,a[i]);
}
return 0;
}
结果如下:
移动前指针位置:0x16efbb600
移动后指针位置:0x16efbb608
a[0]=100
a[1]=0
a[2]=20
a[3]=0
a[4]=0
可以看到指针+2后,移动前后的内存地址刚好相差8个Byte,也就是2个int元素的占用的内存大小.
练习-通过指针位移实现strstr截取子串的功能
思路就是先从源字符串中找到一个开始截取的位置,这个位置通过对比子串的首字符来判断,找到开始位置后就可以逐个字符和子串进行对比, 如果都一样那么就说明找到了,否则会进行下一次循环重新找截取的开始位置.
#include <stdio.h>
char *my_strstr(const char *str, const char *substr)
{
const char *my_str = str;
const char *my_substr = substr;
char *res = NULL; // 最终返回的结果
while (*my_str)
{
if (*my_str != *my_substr)
{
// 逐个取my_str中的字符,直到找到和my_substr的首字符相同时为止
++my_str;
continue;
}
// 记录匹配开始的首地址,也就是最终返回的结果
res = (char *)my_str;
// 每次都需要参与比较的子串
char *temp_substr = (char *)my_substr;
while (*temp_substr)
{
if (*my_str != *temp_substr) //后续比较只要不同就跳出
{
break;
}
// 同时对my_str和temp_substr指针++,实现逐个字符对比
++my_str;
++temp_substr;
}
if (*temp_substr == '\0')
{
break; //子串都匹配到末尾了,所以找到了
}
}
return res;
}
int main()
{
char *str = "hello world";
printf("res=%s\n", my_strstr(str, "or"));// res=orld
printf("res=%s\n", my_strstr(str, "ll"));// res=llo world
printf("res=%s\n", my_strstr(str, "ld"));// res=ld
return 0;
}
1.9 指针与char数组
在C语言中所有的数据类型都可以当做是一个char的数组.
#include<stdio.h>
int main()
{
int a = 0x12345678; //定义一个16进制数,int占4个字节,相当于char数组长度是4个BYTE
char *p = (char *)&a;//定义char * 的指针,指向a的地址
int i = 0 ;
for(i =0;i < sizeof(a); i++)
{
printf("a[%d]=%x\n",i,p[i]);//由于是小端对齐,所以输出结果是78,56,34,12倒着输出的.内存地址从左->右表示从高->底
}
return 0;
}
练习-将一个int数转成ip地址
我们知道一个int占4个字节,刚好对应4个char, 对于unsigned char的取值范围就是0~255, 所以刚好和ip地址每段的取值符合.所以可以将int数转成ip地址。
#include<stdio.h>
int main()
{
int a = 987654321;
unsigned char *p = (unsigned char *)&a; //定义unsigned char *类型的指针变量p 对应a变量的地址,这里其实是指针强转
//在C语言中基本数据类型可以当做char数组对待,所以可以这样操作
printf("%u.%u.%u.%u\n",p[3],p[2],p[1],p[0]); // 结果为: 58.222.104.177
return 0;
}
直接ping 987654321 对应的ip地址就是上面用程序求出的ip: 58.222.104.177
练习-将一个字符串IP转成unsigned int类型.
这个案例就是上面案例的反过程, 先将字符串中每一段的数据取出来,然后操作指针来赋值.
#include<stdio.h>
int main()
{
char a[] = "192.168.1.1";
unsigned int ip = 0;
unsigned char * p = (unsigned char *) &ip;
//先从字符串中取值
int a1,a2,a3,a4;
sscanf(a,"%d.%d.%d.%d",&a1,&a2,&a3,&a4);
//小端对齐赋值
p[0] = a4;
p[1] = a3;
p[2] = a2;
p[3] = a1;
printf("ip=%u\n",ip); // ip=3232235777
return 0;
}
如下所示,对数字3232235777进行ping时,查看的ip刚好就是192.168.1.1
练习-使用指针对二维数组进行排序
#include<stdio.h>
int main()
{
int a[2][3] = {{10,21,13},{42,5,9}}; //二维数组
int *p = (int *)a; // 定义一个指针, 指向数组a
int i,j;
//下面通过操作指针的方式来排序, 由于内存是连续的,所以可以当做一维数组看待
int size = sizeof(a) / sizeof(a[0][0]); // 6
for(i = 0 ; i < size; i++)
{
for(j=0 ; j< size - i -1 ; j++)
{
if(p[j] > p[j+1])
{
// 直接通过指针操作内存地址对应的值
int temp = p[j];
p[j] = p[j+1];
p[j+1] = temp;
}
}
}
//输出排序后的结果
for( i =0 ; i <sizeof(a) /sizeof(a[0]) ; i++)
{
for( j =0; j<sizeof(a[0]) / sizeof(a[0][0]); j++)
{
printf("%d,",a[i][j]); // 5,9,10,13,21,42,
}
}
return 0;
}
练习-使用指针对字符串取反操作
int main() {
char str[20] = "hello world";
char *start = str;//&(str[0]); //创建首指针
char *end = start + strlen(str) - 1; //创建尾指针, 通过指针偏移到尾部
while (start < end) {
// 通过指针操作值
char tmp = *start;
*start = *end;
*end = tmp;
// 偏移首位指针
start++;
end--;
}
printf("%s\n", str); // dlrow olleh
return 0;
}
1.10 指针数组
由于指针是一个变量,所以也可以以数组的形式出现。 指针数组的字节长度不受数据类型的影响,而是受到操作系统的影响, 在32位系统中,一个指针的长度是4个BYTE, 64位系统是8个BYTE, 所以32位系统中指针数组的长度等于 元素个数*4
; 64位系统中指针数组的长度等于元素个数*8
.
#include<stdio.h>
int main()
{
//定义一个int *类型的10个元素的指针数组
int *a[10] = {NULL};
//定义一个char *类型的10个元素的指针数组
char *b[10] = {NULL};
printf("%lu,%lu\n",sizeof(a),sizeof(b)); // 80,80
return 0;
}
指针数组元素的赋值
#include<stdio.h>
int main()
{
int *a[10] = {NULL};
int i = 5;
a[0] = &i; //取i的地址给a[0]指针
*a[0] = 100; //修改i的值
printf("i=%d\n",i); // i=100
return 0;
}
1.11 指针的间接赋值和取值
任何一块连续的内存空间,都可以操作指针来间接赋值和取值,有2种方式
- 通过指针偏移,然后取*操作后当做左值使用就可以被赋值了,当做右值就可以取值了.
- 通过指针变量名[index]的方式,当做左值使用就可以被赋值了,当做右值就可以取值了.
#include <stdio.h>
int main()
{
int a[] = {1, 2, 3, 4};
int *p = a; //指针默认指向首元素的地址
// 两种取值方式
int i = *(p + 1);
int j = p[2];
printf("取值:a[1]=%d,a[2]=%d\n", i, j); // 取值:a[1]=2,a[2]=3
// 两种赋值方式
*(p + 2) = 100;
p[3] = 200;
printf("赋值:a[2]=%d,a[3]=%d\n", a[2], a[3]); // 赋值:a[2]=100,a[3]=200
return 0;
}
1.12 一维数组指针
当指针指向数组的地址时,此时指针的步长就是sizeof(arr)了,也就是数组所有元素的大小之和.
下面介绍3种方式来定义数组的指针:
- 先定义数组类型,然后再定义数组指针类型
#include <stdio.h>
// 1.自定义一个数组类型,注意圆括号内的是类型的名称,这个和非数组类型的定义方式不一样
// 非数组类型的定义方式形如: typedef unsigned int u32; 表示定义了一个无符号的int类型
typedef int(ARRY5)[5];
int main()
{
// 2.接着就可以定义它的指针类型了
ARRY5 arr = {1, 2, 3, 4, 5}; //等效于int arr[5] = {1,2,3,4,5};
ARRY5 *p = &arr;
printf("%lu\n", sizeof(arr)); // 20
printf("%d\n", p); // 1082189744
printf("%d\n", (p + 1)); // 1082189764,和上面的数相差20,也就是刚好是sizeof(arr)
for (int i = 0; i < 5; ++i)
{
// printf("arr[%d]=%d\n",i,p[0][i]);
// printf("arr[%d]=%d\n",i, (*p)[i]);
printf("arr[%d]=%d\n", i, *(*p + i));
//上面*p和p[0]效果一样都是为了取出指针的值也就是arr数组,数组名也是指针,所以*(*p)=数组第一个元素
}
return 0;
}
- 先定义数组指针的类型然后指向数组的地址
#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
typedef int(*ARRAY5)[5]; // 直接定义数组指针类型
ARRAY5 p = &arr; // 然后使用指针指向数组地址
for (int i = 0; i < 5; i++)
{
printf("arr[%d]=%d\n", i, *(*p + i)); //*p得到数组名,数组名也是指针类型,*p+i移动指针,然后*(..)是取值
}
return 0;
}
- 直接定义数组指针变量并指向数组地址
注意是变量,而不是类型哦
#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int(*pArray)[5] = &arr; // 这里pArray是一个指针变量名,而不是类型哦
for (int i = 0; i < 5; i++)
{
printf("arr[%d]=%d\n", i, *(*pArray + i));
}
return 0;
}
1.13 二维数组指针
二维数组和一维数组一样,除了使用sizeof和使用&取数组地址的这2种情况外都可以把数组名当做指针使用,是指向首元素地址的指针,只不过二维数组的首元素是一维数组, 所以该指针是指向一维数组的指针,指针的步长是一个一维数组的大小.
#include <stdio.h>
int main()
{
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
printf("%d\n",sizeof arr[0]); //12,每个一维数组的大小都是12,因为一维数组有3个int元素
printf("%d\n",arr); //1710757312
printf("%d\n",arr+1);//1710757324,和上面相差12, 也就是一个一维数组的大小
}
既然二维数组的数组名是一个指向一维数组的指针变量,那么这个二维数组名其实就可以看成int(*pArray)[3]
的指针变量了.
#include <stdio.h>
int main()
{
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int(*pArray)[3] = arr; //由此可见arr的类型是一个指向一维数组的指针类型.
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 3; ++j)
{
// printf("%d\n",pArray[i][j]);//pArray[i]得到每一个一维数组
printf("%d\n", (*(pArray + i))[j]); //*(pArray+i)也可以得到每一个一维数组,等效于pArray[i]
}
}
}
当然我们可以不用这么麻烦,直接把数组名arr当做指针变量来使用,例如通过指针方式获取arr[1][2]的值还可以这么操作
//通过指针方式获取arr[1][2]的值
//1.arr是一维数组指针类型,arr+1表示指针偏移一个步长,也就是指向arr[1]的地址
//2.*(arr+1)得到arr[1]的一维数组,一维数组名也是指项它首元素的指针,类型是int*
//3.*(arr+1)+2表示int*类型的指针偏移2个步长,此时指向的就是arr[1][2]元素的地址
//4.*(*(arr+1)+2)表示取arr[1][2]地址的值
int num = *(*(arr+1)+2);
printf("%d\n",num); //6
因此,当二维数组传递给函数参数的时候可以这样写
#include <stdio.h>
void printArray(int (*arr)[3], int len1, int len2) //二维数组当做函数参数传递时会退化为指向一维数组地址的指针
{
for (int i = 0; i < len1; ++i)
{
for (int j = 0; j < len2; ++j)
{
printf("arr[%d][%d]=%d\n", i, j, *(*(arr + i) + j)); // 等效于arr[i][j];
}
}
}
int main()
{
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
printArray(arr, 3, 3);
return 0;
}
二维数组的指针取
*
操作后得到的是一维数组,而一维数组取*
后得到的是数组首元素。
二、二级和多级指针
指针就是一个变量,既然是变量就也就存在内存地址,所以可以定义一个指向指针的指针变量。二级指针的定义会比一级指针多一个*
;
二级指针指向的是一级指针的地址, 一级指针指向的是变量的地址, 如下图所示:
换成代码就是:
int a = 10;
int *p = &a; //一级指针指向变量a
int **pp = &p; //二级指针指向一级指针变量p
//反过来就是修改变量的值
*pp = NULL; // 表示将1级指针变量的值赋值为NULL;
**pp = 100; //表示把变量a的值赋值为100;
2.1 二级指针与一级指针数组的关系
一级指针数组里面存放的元素都是一级指针变量, 而指针数组的名字就是数组首元素的内存地址, 也就是一级指针的内存地址, 所以可以用二级指针的来指向.当指针指向数组的时候就可以当做数组本身来使用了.
int main()
{
int *a[3] = {NULL};//一级指针数组
int x,y,z;
a[0] = &x; //给数组元素赋值
a[1] = &y;
a[2] = &z;
int **p = a;//二级指针指向数组名,数组名等效于指向&a[0],而a[0]是个一级指针.所以指向一级指针地址的指针那就是二级指针了.
//指针指向数组就可以当做数组用
int i;
for(i=0;i<3;i++)
{
printf("p[%d]=%p\n",i,p[i]); //打印数组元素,数组元素是一级指针
}
*p[0] = 100;//等效于 *a[0] =100; 就是将0下标的指针指向的变量的值修改为100,因为p[0]=a[0]=一级指针,然后加*就是修改值
*p[1] = 200;
*a[2] = 300;
printf("x=%d,y=%d,z=%d\n",x,y,z);
return 0;
}
结果如下:
p[0]=0x16b1735f8
p[1]=0x16b1735f4
p[2]=0x16b1735f0
x=100,y=200,z=300
2.2 多级指针
每多加一级指针, 指针类型的*
号就会多一个.
#include<stdio.h>
int main()
{
int a = 10;
int *p = &a; // 一级指针
int **pp = &p; //二级指针
int ***ppp = &pp; //三级指针
int ****pppp = &ppp; //四级指针
int *****ppppp = &pppp; //五级指针
*****ppppp = 100;//修改a的值
printf("a=%d\n",a);
return 0;
}
三、指针在函数中的应用
3.1 在函数参数中使用指针
在C语言中规定,如果想通过函数内部修改外部实参的值,那么就需要给函数的参数传递实参的地址,否则是无法修改的。因为函数内对形参的改变不会影响到外部实参的值。
#include<stdio.h>
//错误的交换姿势
void swap1(int a, int b )
{
int temp = a;
a = b;
b = temp;
}
//正确的交换姿势,形参定义为指针, 可以接收变量的地址
void swap2(int *a,int *b)
{
//下面的交换就是交换地址对应的变量的值,地址是不会改变的,这样可以保证唯一性
int temp = *a; //取a指针变量对应的地址的变量的值
*a = *b; //取b指针变量对应的地址的变量的值,赋值给a变量
*b = temp;
}
int main()
{
int a = 1;
int b = 2;
swap1(a,b);
printf("swap1->a=%d,b=%d\n",a,b);
swap2(&a,&b); //传递变量的地址
printf("swap2->a=%d,b=%d\n",a,b);
return 0;
}
结果如下:
swap1->a=1,b=2
swap2->a=2,b=1
可以看到swap1并没有交换成功, 而swap2成功了.
3.2 数组名作为函数的参数
C语言规定当一个数组名作为函数的形参的时候,它就变成了指针变量了,而不再是数组名了。
指针是什么类型就看数组首元素是什么类型,例如数组首元素是int类型,那么指针类型就是int*, 如果数组是double类型就是double*,如果是int*类型,那么指针就是int**类型.
所以下面3种参数的写法其实是一样的
void test1(int a[10]);
void test2(int a[]);
void test3(int *a); //第三种写法常用
//第三种写法如果不想被函数的内部修改元素的值, 那就可以这样定义
void test3(const int *a); //相当于指向常量的指针 ,当然别人想改还是可以强转来修改的
注意:如果把一个数组名作为函数的参数,那么在函数内部就不知道这个数组的元素个数了,因为sizeof算出来的是指针的大小, 32位系统是4个BYTE, 64位是8个BYTE, 所以如果想知道数组的长度,就需要增加一个参数来标明这个数组的大小. 除非数组是一个字符串, 那么可以不用传递长度.因为字符串可以有strlen方法来获取长度
#include<stdio.h>
//void test(int a[10])
//void test(int a[]);
void test(int *a) //如果传入的是数组名,就变成了指针变量,所以可以这样写
{
printf("test->sizeof=%lu\n",sizeof(a)); // test->sizeof=8
//操作指针就可以操作数组
a[2] = 100;
}
int main()
{
int a[10] = {0,1,2,3,4,5,6,7,8,9};
test(a); //将数组名传入
printf("main->sizeof=%lu\n",sizeof(a)); // main->sizeof=40
int i;
for(i=0;i<sizeof(a)/sizeof(a[0]);i++)
{
printf("a[%d]=%d,",i,a[i]);//a[0]=0,a[1]=1,a[2]=100,a[3]=3,a[4]=4,a[5]=5,a[6]=6,a[7]=7,a[8]=8,a[9]=9,
}
return 0;
}
当数组传递给函数参数后,由于会变成了指针变量,因此无法在函数内部获取数组的长度,所以需要额外传递数组的长度作为参数给函数内部使用
#include <stdio.h>
void printIntArray(int *arr, int len) // 如果传递的是数组名就会退化为指针
{
for (int i = 0; i < len; i++)
{
// printf("arr[%d]=%d\n",i,arr[i]);
printf("arr[%d]=%d\n", i, *(arr + i));
}
}
void printStringArray(char **arr, int len) // 如果传递的是数组名就会退化为指针
{
for (int i = 0; i < len; i++)
{
//printf("arr[%d]=%s\n", i, arr[i]);
printf("arr[%d]=%s\n", i, *(arr + i));
}
}
int main()
{
int a[] = {1, 2, 3, 4, 5};
printIntArray(a, 4);
char *str[] = {"aaaa", "bbbb", "cccc", "dddd"};
printStringArray(str, 4);
return 0;
}
3.3 函数指针
所谓函数指针就是指向函数内存地址的指针。一个函数在编译的时候会分配一个入口地址,这个入口地址就是函数的指针,函数名就是函数的入口地址。
函数指针的定义方式:
函数的返回值类型(*指针变量名称)(参数列表)
例如:
int(*p)(int)
定义一个指向int func(int n)
类型的函数地址的指针.
函数可以通过函数指针来调用.
int(*p)()代表一个指向函数的指针,但是没有固定指向哪一个函数, 这个可以动态指向某一个函数名.
void man() {
printf("抽烟\n");
printf("喝酒\n");
printf("打牌\n");
}
void woman() {
printf("化妆\n");
printf("逛街\n");
printf("网购\n");
}
int main() {
void (*p)(); //定义一个无返回值,无参数的函数指针,指针变量名是p
int i = 0;
scanf("%d", &i); // 读取用户输入
if (i == 0)
p = man; //指向man函数
else
p = woman; //指向woman函数
p(); //通过指针调用函数
return 0;
}
函数指针可以直接调用函数,编译器会自动取
*
,当然也可以手动取*
, 例如(*p)();
3.4 回调函数
将函数的指针作为另一个函数的参数时,这个参数就被称为回调函数。
#include <stdio.h>
int max(int a, int b) {
if (a > b)
return a;
else
return b;
}
int add(int a, int b) {
return a + b;
}
//参数1是一个函数指针(回调函数)
void func(int(*p)(int, int), int a, int b) {
int res = p(a, b);//调用p指针对应的函数执行
printf("%d\n", res);
}
int main() {
int i = 0;
scanf("%d", &i); // 接收控制台输入的值
if (i == 0)
func(max, 10, 20); //传参的时候,直接传入函数的名字,因为函数名就是地址,可以用函数指针接收
else
func(add, 10, 20);
return 0;
}
练习-定义打印任意类型的数组
#include <stdio.h>
//定义一个打印任意类型数组的函数
//参数1:数组名
//参数2:元素的长度
//参数3:数组的长度
//参数4:回调函数
void print_array(void *arr, int element_size, int arr_size, void (*print_fun)(void *))
{
//先修改指针的步长为1个字节
char *my_arr = (char *)arr;
//根据element_size,偏移指针到数组的指定元素的首地址
for (int i = 0; i < arr_size; ++i)
{
//每次指针偏移的长度是 element_size * i
char *element = my_arr + element_size * i;
//通过回调函数实现具体的数据打印,因为只有调用方知道元素的具体类型
print_fun(element);
}
}
//打印int类型的数组的回调函数
void print_int_array(void *data)
{
//修改指针步长为int类型
int *element = (int *)data;
printf("%d\n", *element);
}
struct Person
{
char name[30];
int age;
};
//打印结构体的回调函数
void print_person_array(void *data)
{
//修改步长为struct Person类型
struct Person *p = (struct Person *)data;
//打印内容
printf("name=%s,age=%d\n", p->name, p->age);
}
int main()
{
//定义int数组
int arr[] = {1, 2, 3, 4, 5};
print_array(arr, sizeof(int), 5, print_int_array);
//定义结构体数组
struct Person person_array[3] = {{"tom", 20}, {"jack", 30}, {"petter", 40}};
print_array(person_array, sizeof(struct Person), 3, print_person_array);
return 0;
}
3.5 函数指针数组
既然函数指针也是变量,那么就可以存在数组的形式,定义的格式是:
返回值(*函数指针变量名[num])(参数列表)
#include <stdio.h>
void fun1()
{
printf("func1 run...\n");
}
void fun2()
{
printf("func2 run...\n");
}
void fun3()
{
printf("func3 run...\n");
}
int main()
{
void (*func_array[3])(); // 定义函数指针数组
// 存入3个函数
func_array[0] = fun1;
func_array[1] = fun2;
func_array[2] = fun3;
for (int i = 0; i < 3; ++i)
{
func_array[i](); // 从数组取出每一个函数指针,然后执行
//或者对函数指针取*执行
//(* func_array[i])();
}
return 0;
}
3.6 指针数组作为mian函数的形参使用
int main(int argc, char ** argv) //参数2是二级指针,可以接收一级指针数组, 写法等效于char *argv[]
main函数是操作系统调用的,所以main函数的参数也是操作系统在调用的时候自动填写的,其中argc打包命令行参数的数量, argv代表命令行的具体参数.
下面写一个计算 2个数的和的程序,通过main函数的参数来完成.
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char **args)
{
if(argc <=2)
{
printf("参数不足,使用方式:%s 数字1 数字2\n",args[0]);
return 0;
}
int a = atoi(args[1]); //char *类型可以直接访问字符串
int b = atoi(args[2]);
printf("sum=%d\n",a+b);
return 0;
}
下面是一个四则运行实例代码
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char **args)
{
if(argc <=3)
{
printf("参数不足,使用方式:%s 数字1 运算符 数字2\n",args[0]);
return 0;
}
int a = atoi(args[1]); //char *类型可以直接访问字符串
char *c = args[2]; //接收运算符, 注意char*指向的是字符串而不是char , 是双引号的字符串内容
int b = atoi(args[3]);
switch(c[0]) //取字符串首字符
{
case '+':
printf("%d%s%d=%d\n",a,c,b,a+b);
break;
case '-':
printf("%d%s%d=%d\n",a,c,b,a-b);
break;
case '*': //注意乘法在控制台输入的时候需要转义 \* ,否则在Linux上代表是所有的意思
printf("%d%s%d=%d\n",a,c,b,a*b);
break;
case '/':
printf("%d%s%d=%d\n",a,c,b,a/b);
break;
}
return 0;
}
3.7 指针作为函数参数的输入和输出特性
3.7.1 输入特性
主调函数分配内存(栈/堆),被调函数使用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void printIntArray(int *arr, int len) //如果传递的是数组名就会退化为指针
{
for (int i = 0; i < len; i++)
{
// printf("arr[%d]=%d\n",i,arr[i]);
printf("arr[%d]=%d\n", i, *(arr + i));
}
}
void printStringArray(char **arr, int len) //如果传递的是数组名就会退化为指针
{
for (int i = 0; i < len; i++)
{
//printf("arr[%d]=%s\n", i, arr[i]);
printf("arr[%d]=%s\n", i, *(arr + i));
}
}
void printString(char *s)
{
printf("%s", s);
}
int main()
{
int a[] = {1, 2, 3, 4, 5}; //主调函数分配内存(栈)
printIntArray(a, 4); //被调函数使用
char *str[] = {"aaaa", "bbbb", "cccc", "dddd"}; //主调函数分配内存(栈)
printStringArray(str, 4); //被调函数使用
char *s = malloc(sizeof(char) * 100); //主调函数分配内存(堆)
memset(s, 0, 100);
strcpy(s, "helloworld");
printString(s); //被调函数使用
free(s);
s = NULL;
return 0;
}
输出结果:
arr[0]=1
arr[1]=2
arr[2]=3
arr[3]=4
arr[0]=aaaa
arr[1]=bbbb
arr[2]=cccc
arr[3]=dddd
helloworld
3.7.2 输出特性
被调函数分配内存(栈/堆),主调函数使用.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void init(char **str)
{
// 被调函数分配内存后才能使用
char *temp = malloc(sizeof(char) * 100);
memset(temp, 0, 100);
strcpy(temp, "helloworld");
*str = temp;
}
int main()
{
char *p = NULL;
init(&p); // 主调函数使用,需要取地址
if (p)
{
printf("p=%s\n", p); // p=helloworld
free(p);
p = NULL;
}
return 0;
}
总结:
1.输入特性中不需要传递指针变量的地址,因为不涉及更改指针的值,而输出特性中需要传递指针变量的地址,需要更改指针变量的值.
2.具体是输入还是输出特性是相对于被调函数的, 被调函数能够直接使用,那就是输入,不能直接使用就是输出.
四、内存操作与指针
memset、memcpy、memmove这3个函数分别是内存的设置、内存的拷贝和内存的移动。使用时需要引入string.h头文件。
4.1 memset
表示设置内存,接收3个参数
void *memset(void *s, int c, size_t n);
参数1:是一个无类型的指针, 用于接收一个内存首地址,如果要操作数组,那就传数组名,因为数组名对应的是首元素的内存地址. 可以被指针变量接收.
参数2:要设置的值,如果填0就是0.
参数3:表示这块内存的大小,单位是字节
例如:
#include<stdio.h>
#include<string.h>
int main()
{
int a[5] = {1,2,3,4,5};
//清空a数组的值
memset(a,0,sizeof(a));
int i;
for(i=0;i<5;i++)
{
printf("a[%d]=%d,",i,a[i]); // a[0]=0,a[1]=0,a[2]=0,a[3]=0,a[4]=0,
}
return 0;
}
4.2 memcpy
用于在2块内存之间拷贝数据,接收3个参数.
void *memcpy(void *dest, const void *src, size_t n);
参数1:目标地址的指针变量
参数2:源地址的指针变量, 且不能修改源地址变量的值,他是const修饰的.
参数3:拷贝多少内容,单位字节
#include<stdio.h>
#include<string.h>
int main()
{
int a[5] = {1,2,3,4,5};
//拷贝到b数组
int b[10] = {0};
memcpy(b,a,sizeof(a)); //参数1和2都是无类型的,所以其他数据类型,包括字符串都可以使用.
int i;
for(i=0;i<10;i++)
{
printf("b[%d]=%d,",i,b[i]);//b[0]=1,b[1]=2,b[2]=3,b[3]=4,b[4]=5,b[5]=0,b[6]=0,b[7]=0,b[8]=0,b[9]=0,
}
return 0;
}
再来看看字符串的拷贝
#include<stdio.h>
#include<string.h>
int main()
{
char a[] = "hello"; //定义一个字符串
char b[10] = {0}; //定义一个char数组,char数组元素为0时,用%c输出的时候是看到不到的.
//拷贝
memcpy(b,a,sizeof(a));
int i;
for(i=0;i<10;i++)
{
//输出b的每个元素的值
printf("b[%d]=%c\n",i,b[i]);
}
//拷贝完后, b就变成字符串了
printf("b=(%s)\n",b);
return 0;
}
使用memcpy的时候,一定要确保内存没有重叠区域.
例如 :
int a[10] ={0};
memcpy(&a[3], &a[0], 20); //拷贝20个字节也就是5个int数据
将a[0]之后的5个int数据拷贝到a[3]之后的5个int数据,他们中间下标3和4之间存在内存重叠区域.
4.3 memmove
表示内存移动, 参数也memcpy一样,用法也一样.
#include<stdio.h>
#include<string.h>
int main()
{
int a[10] = {0,1,2,3,4,5,6,7,8,9};
int b[10] = {0};
//移动a的内存到b,并不会改变a的内存
memmove(b,a,sizeof(a));
int i;
for(i=0;i<10;i++)
{
printf("a[%d]=%d,b[%d]=%d\n",i,a[i],i,b[i]);
}
return 0;
}
结果如下:
a[0]=0,b[0]=0
a[1]=1,b[1]=1
a[2]=2,b[2]=2
a[3]=3,b[3]=3
a[4]=4,b[4]=4
a[5]=5,b[5]=5
a[6]=6,b[6]=6
a[7]=7,b[7]=7
a[8]=8,b[8]=8
a[9]=9,b[9]=9
五、指针和字符串
在C语言中,大多数的字符串操作其实就是指针的操作.
5.1 通过指针修改字符串中的字符
#include<stdio.h>
int main()
{
char s[] = "hello world";
char *p = s; //数组名就是首元素的地址, 也就是'h'字符的地址, 所以*p取的值是'h'
printf("*p=%c\n",*p); //取首元素的值,*p=h
p[0] = 'a'; //修改首元素的值,相当于s[0]='a'
printf("*p=%c\n",*p);// *p=a
return 0;
}
5.2 通过指针偏来修改字符串
#include<stdio.h>
int main()
{
char buf[100] = "hello world";
char *p = buf; // 指向字符串首地址
printf("*p=%c\n",*p); // *p=h
*(p+3) = 'A';//修改当前指针后3个地址对应的值,注意此时指针没有移动, 指向的还是首地址,它的值还是'h'
printf("*p=%c,buf=%s\n",*p,buf); // *p=h,buf=helAo world
//移动指针
p+=6; //+=赋值,会真正移动指针
printf("*p=%c\n",*p); // *p=w
*p = 'B';//修改此时指针指向地址的值
printf("buf=%s\n",buf); // buf=helAo Borld
return 0;
}
5.3 char *和字符串的关系
由于char * 是一级指针可以接收一个字符串变量(char 数组名) 或者字符串常量,所以按照%s输出指针变量其实是和输出数组名的效果一样的, 都是字符串内容. 如下所示:
char a[] = "hello"; //定义一个字符串
char *c = a; //一级指针指向数组名,可以当做数组使用
printf("c=%s,a=%s\n",c,a); //结果都是hello字符串
字符串常量和字符串变量定义和区别
1、字符串常量
定义:在一个双引号""内的字符序列或者转义字符序列称为字符串常量
例如:“HA HA!” “abc” “\n\t”
这些字符串常量是不能改变的,如果试图改变指针所指向的内容是错误的,因为字符串常量是存在静态内存区的,不可以改变的。
如定义字符串常量:
char* a="i love you.";
*a='h'; //试图改变它
这是错误的。系统显示:
windows上报错如下:
string.exe 中的 0x00d71398 处未处理的异常: 0xC0000005: 写入位置 0x00d7573c 时发生访问冲突或者报该内存不能为written。
在gcc环境下报错如下:
Process finished with exit code 138 (interrupted by signal 10: SIGBUS)
2、字符串变量
在C语言中没有纯粹的c语言字符串变量,可以通过一个字符数组来体现,这样就可以对字符数组中的内容进行改变!
如定义字符串变量:
char a[]="i love you.";
*a='h'; //将数组首地址对应的元素的值修该为h
5.4 char ** 和char*[]以及字符串的关系
char ** 是二级指针,用于接收一级指针的地址, 而char *[]是一级指针数组, 数组名就是首元素(一级指针)的地址,所以可以被char ** 接收, 那如果char ** 指向数组名,那么就可以当做数组使用. 所以就有下面的关系:
char a[] ="hello";//定义一个字符串变量,字符串是特殊的字符数组
char *s[2]; //定义一个一级指针数组,元素都是一级指针
s[0] = a;//一级指针指向字符串数组名,数组名就是地址,可以当做a来使用, 也就是char *可以当做a使用
char **p = s; //二级指针指向一级指针数组名, 可以当做s来使用
printf("p[0]=%p,s[0]=%p,a=%p,a[0]=%p\n",p[0],s[0],a,&a[0]); //结果都是同一个地址, p[0]的类型是char *,所以*p取出的也是char *类型
printf("p[0]=%s,s[0]=%s,a=%s,a[0]=%c\n",p[0],s[0],a,a[0]);//结果都是字符串内容hello, 除了a[0] = 'h'
printf("*p=%p,**p=%c\n",*p, **p);//*p的结果类型是char * ,而**p的结果类型是char
结果如下:
p[0]=0x16b4bb5fc,s[0]=0x16b4bb5fc,a=0x16b4bb5fc,a[0]=0x16b4bb5fc
p[0]=hello,s[0]=hello,a=hello,a[0]=h
*p=0x16b4bb5fc,**p=h
总结:
char**
指向char*
的数组, 所以char**
的指针变量可以当做char*
数组用, 而char*
指向char数组,所以char *
可以当做char数组用, 因此char **
的指针变量可以直接操作char数组的元素.