一、数组指针
1. 二维数组
二维数组像一个表格,元素按行和列排列。
例:
int a[3][5];
定义了一个 3 行 5 列的一个二维数组。
可以认为二维数组 a 由 3 个元素组成,其中每个元素都是一个包含 5 个整数的一维数组。
**回顾:**数组的名字是数组的首地址,是第 0 个元素的地址,是个常量,数组名字加 1 指向下个元素。
二维数组 a 中 ,a+1 指向下个元素,即下一个一维数组,即下一行。
2. 数组指针
数组指针是个指针,指向一个数组,+1 跳一个数组,即指向下个数组。
数组指针的作用就是可以保存二维数组的首地址。
3. 数组指针的定义方法
指向的数组的类型(*指针变量名)[指向的数组的元素个数]
int (*p)[5];//定义了一个数组指针变量 p,p 指向的是整型的有 5 个元素的数组
p+1 往下指 5 个整型,跳过一个有 5 个整型元素的数组。
例子:
#include <stdio.h>
// 定义数组指针
void test1()
{
int a[3][5];
int (*p)[5];
printf("a = %p\n", a);
printf("a + 1 = %p\n", a+1);
p = a;
printf("p = %p\n", p);
printf("p + 1 = %p\n", p + 1);
}
int main()
{
test1();
return 0;
}
执行结果:
a = 00000033553ff900
a + 1 = 00000033553ff914
p = 00000033553ff900
p + 1 = 00000033553ff914
例子:数组指针的用法 1
// 数组指针的用法
// 可以将二维数组的首地址传递到另一个函数里面,
// 此时函数的形参就需要定义为数组指针
void fun(int (*p)[5], int x, int y)
{
p[0][1] = 666;
}
void test2()
{
int i, j;
int a[3][5] = {0};
fun(a, 3, 5);
for(i=0;i<3;i++)
{
for(j=0;j<5;j++)
printf("%d ", a[i][j]);
printf("\n");
}
}
int main()
{
test2();
return 0;
}
执行结果:
0 666 0 0 0
0 0 0 0 0
0 0 0 0 0
4. 各种数组指针的定义
一维数组指针,加 1 后指向下个一维数组
int(*p)[5];
配合每行有 5 个 int 型元素的二维数组来用
int a[3][5];
int b[4][5];
int c[5][5];
int d[6][5];
p=a;p=b;p=c;p=d;
都是可以的
5. 注意
指针数组:是个数组,有若干个相同类型的指针构成的集合
int *p[10];
数组 p 有 10 个 int *类型的指针变量构成,分别是 p[0] ~p[9]
数组指针:是个指针,指向一个数组,加 1 跳一个数组
int (*p)[10];
P 是个指针,p 是个数组指针,p 加 1 指向下个数组,跳 10 个整型。
指针的指针:\
int **p;//p 是指针的指针
int *q;
p=&q;
6. 数组名字取地址:变成数组指针
a+1 跳一个整型元素,是 a[1]的地址
a 和 a+1 相差一个元素,4 个字节
&a 就变成了一个一维数组指针,是 int(*p)[10]类型的。
(&a) +1 和&a 相差一个数组即 10 个元素即 40 个字节。
例子:
void test3()
{
int a[10];
printf("a = %p\n", a);
printf("a + 1 = %p\n", a+1);
printf("&a = %p\n", &a);
printf("&a + 1 = %p\n", &a+1);
}
int main()
{
test3();
return 0;
}
执行结果
a = 000000688f7ff5e0
a + 1 = 000000688f7ff5e4
&a = 000000688f7ff5e0
&a + 1 = 000000688f7ff608
7. 数组名字和指针变量的区别
int a[10];
int *p;
p=a;
相同点:
a 是数组的名字,是 a[0]的地址,p=a 即 p 也保存了 a[0] 的地址,即 a 和 p 都指向 a[0],所以在引用数组元素的时候,a 和 p 等价
引用数组元素回顾:
a[2]、*(a+2)、p[2]、*(p+2)
都是对数组 a 中 a[2] 元素的引用。
不同点:
(1) a 是常量、p 是变量
可以用等号 = 给 p 赋值,但是不能用等号给 a 赋值
(2) 对 a 取地址,和对 p 取地址结果不同
因为 a 是数组的名字,所以对 a 取地址结果为数组指针。
p 是个指针变量,所以对 p 取地址(&p)结果为指针的指针。
void test4()
{
int a[10];
int *p;
p = a;
printf("a = %p\n", a); // a 是一维数组
printf("&a = %p\n", &a); // &a 是一维数组指针
printf("&a + 1 = %p\n", &a+1); // &a+1 跳 40 个字节
printf("p = %p\n", p); // p 是一个数组指针
printf("&p = %p\n", &p); // &p 是指针的指针
printf("&p + 1 = %p\n", &p+1); // &p+1 跳一个int*类型的指针,8个字节
}
int main()
{
test4();
return 0;
}
8. 多维数组中指针的转换
在二维数组中,行地址取 * 不是取值的意思,而是指针降级的意思,由行地址(数组指针)变成这一行第 0 个元素的地址。取 * 前后还是指向同一个地方,但是指针的类型不一样了。
//二维数组的数组名降级问题
void test5()
{
int a[3][5];
printf("a = %p\n", a);// a 表示第0行第0个元素的地址
printf("a + 1 = %p\n", a+1);// a+1 指向下一行第0个元素,跳过 5×4 个字节
printf("*a = %p\n", *a);// *a 表示第0行第0个元素的地址
printf("(*a) + 1 = %p\n", (*a)+1);// (*a)+1 跳过 4 个字节,指向第0行第1个元素
}
int main()
{
test5();
return 0;
}
执行结果:
a = 000000b5587ffc50
a + 1 = 000000b5587ffc64
*a = 000000b5587ffc50
(*a) + 1 = 000000b5587ffc54
二维数组的数组名默认是一个行指针,加 1 指向下一行的首地址。
二维数组名取 * ,表示地址的降级,意味着行指针降级为列指针,加 1 保存下一个元素的地址。
一维数组的数组名默认是一个列指针,加 1 保存下一个元素的地址。
一维数组的数组名取 & ,则是地址的升级,将列指针升级为行指针,加 1 保存下一行元素的首地址。
二、指针和函数的关系
可以给函数传一个整型、字符型、浮点型的数据,也可以给函数传一个地址。
函数的传参方式:复制传参、地址传参、全局传参(几乎用不到)
1. 复制传参
将实参的值传给形参,不管形参怎么变化,跟实参都没有关系
#include <stdio.h>
// 函数的传参方式之复制传参
void myfun1(int a, int b)// 这里的 a 和 b 是形参
{
int temp;
temp = a;
a = b;
b = temp;
printf("in fun: a = %d, b = %d\n", a, b);
printf("&a = %p, &b = %p\n", &a, &b);
}
void test1()
{
int a = 100;
int b = 20;
printf("before fun: a = %d, b = %d\n", a, b);
printf("&a = %p, &b = %p\n", &a, &b);
myfun1(a, b);// 这里 a 和 b 是实参
printf("after fun: a = %d, b = %d\n", a, b);// 无论形参怎么变化,实参都不变
printf("&a = %p, &b = %p\n", &a, &b);
}
int main()
{
// printf("Hello World!\n");
test1();
return 0;
}
执行结果:
before fun: a = 100, b = 20
&a = 0000001c90fffd4c, &b = 0000001c90fffd48
in fun: a = 20, b = 100
&a = 0000001c90fffd20, &b = 0000001c90fffd28
after fun: a = 100, b = 20
&a = 0000001c90fffd4c, &b = 0000001c90fffd48
2. 地址传参
将实参的地址传递给形参,函数体对形参进行变换,实参的值会随之变换。
例:
// 函数的传参方式之地址传参
void myfun2(int *p, int *q)
{
int temp;
temp = *p;
*p = *q;
*q = temp;
printf("in fun: *p = %d, *q = %d\n", *p, *q);
printf("p = %p, q = %p\n", p, q);
}
void test1()
{
int a = 100;
int b = 20;
printf("before fun: a = %d, b = %d\n", a, b);
printf("&a = %p, &b = %p\n", &a, &b);
myfun2(&a, &b);// 把 a 和 b 的地址传给形参
printf("after fun: a = %d, b = %d\n", a, b);// 无论形参怎么变化,实参都不变
printf("&a = %p, &b = %p\n", &a, &b);
}
int main()
{
// printf("Hello World!\n");
test1();
return 0;
}
执行结果:
before fun: a = 100, b = 20
&a = 0000006a51dff72c, &b = 0000006a51dff728
in fun: *p = 20, *q = 100
p = 0000006a51dff72c, q = 0000006a51dff728
after fun: a = 20, b = 100
&a = 0000006a51dff72c, &b = 0000006a51dff728
例:看似是地址传参,实则复制传参
void test2(char *p)// 形参是一级指针
{
p = "hello kitty";
printf("in fun: %s\n", p);
}
int main()
{
char *p = "hello world";// 实参是一级指针
test2(p);// 传入的是一级指针,本质上是复制传参,而不是地址传参
printf("after fun: %s\n", p);
return 0;
}
执行结果:
in fun: hello kitty
after fun: hello world
把上例改为地址传参,要传给形参一个一级指针的地址,形参应该是一个二级指针
void test2(char **p)// 形参是二级指针
{
*p = "hello kitty";
// printf("in fun: %s\n", p);
}
int main()
{
char *p = "hello world";// 实参是一级指针
test2(&p);// 传入的是一级指针的地址,是地址传参
printf("after fun: %s\n", p);
return 0;
}
总结:要想改变主调函数中变量的值,必须传变量的地址,而且还得通过 *地址
去赋值,无论这个变量是什么类型的。
在地址传参中,如果实参为普通变量,那么形参得是一级指针;如果实参为一级指针,那么形参得是二级指针。以此类推……
3. 传递数组
传递数组理论上都是地址传参。
例:传递一维数组
void fun1(int *p)
{
printf("%d\n", p[2]);
printf("%d\n", *(p+3));
}
void test3()
{
int a[10]={1, 2, 3, 4, 5, 6, 7, 8};
fun1(a);
}
int main()
{
test3();
return 0;
}
执行结果:
3
4
例:传递二维数组
// void fun2(p[][4]) // 形式1
void fun2(int (*p)[4]) // 形式2:通过数组指针
{
printf("%d\n", p[0][2]);
printf("%d\n", *(*(p + 1) + 2));
}
void test4()
{
int a[2][4] = {1, 2, 3, 4,
5, 6, 7, 8};
fun2(a);
}
int main()
{
test4();
return 0;
}
执行结果:
3
7
为什么 *(*(p + 1) + 2)
会打印出 7 呢?
(1) (*p)[4]
:指针 p 指向的是一个一维数组,这个数组有 4 个整数。
(2) p + 1
:指针算数。将指针向前移动足够的字节,以指向下一个包含4个整数的数组,即第二行。
(3) *(p + 1)
:对 p + 1 的解引用操作。它告诉编译器,“给我这个地址所指向的内容”,在这个上下文中,它就是第二行的数组。所以,*(p + 1) 实际上就是 a[1],即第二行的数组。
(4) *(p + 1) + 2
:这是在第二行数组上加上 2 ,指向第二行的第三个元素。
(5) *(*(p + 1) + 2)
:解引用,得到第二行第三列的元素值,即 7 。
4. 指针函数:返回值为指针的函数
一个函数可以返回整型数据、字符数据、浮点型的数据,也可以返回一个指针。
例:
// 指针函数
char *fun3()
{
char str[100]="hello world"; // 函数内部定义,存储在栈区,随着当前代码段结束而释放
return str; // 数组名是一个地址常量
}
void test5()
{
char *p; // 定义一个字符指针
p = fun3();
printf("p = %s\n", p);
}
int main()
{
test5();
return 0;
}
执行结果:
p = (null)
为什么会出现null
呢?
因为 str
是在 *fun3
函数内部定义的,保存在内存的栈区,它会随着当前代码段结束而释放。
那怎么办呢?
把 char str[100]="hello world";
改成 static char str[100]="hello world";
静态区的空间不会随着当前代码段的结束而释放。
// 指针函数
char *fun3()
{
static char str[100]="hello world";
return str; // 数组名是一个地址常量
}
void test5()
{
char *p; // 定义一个字符指针
p = fun3();
printf("p = %s\n", p);
}
int main()
{
test5();
return 0;
}
执行结果:
p = hello world
5. 函数指针:指向函数的指针
在运行程序的时候,会将函数的指令加载到内存的代码段。所以函数也有起始地址。
C 语言规定:函数的名字就是函数的首地址,即函数的入口地址。
定义一个指针变量,去存储函数的地址,这个指针变量就是函数指针。\
(1) 函数指针针变量的定义方法
返回值类型 (*函数指针变量名)(形参列表);
int (*p)(int,int); //定义了一个函数指针变量 p,p 指向的函数必须有一个整型的返回值,且有两个整型参数。
int max(int x,int y)
{
// 函数体
}
int min(int x,int y)
{
// 函数体
}
可以用这个 p 存放这类函数的地址。
p=max;
p=min;
(2) 调用函数的方法
a. 通过函数的名字去调函数(最常用的)
int max(int x,int y)
{
int temp;
if(x<y)
{
temp = y;
}
else if(x>y)
{
temp = x;
}
else
{
temp = x;
}
printf("max = %d\n", temp);
}
void test6()
{
max(3,5);
}
int main()
{
test6();
return 0;
}
执行结果:
max = 5
b. 通过函数指针变量去调用
int max(int x,int y)
{
int temp;
if(x<y)
{
temp = y;
}
else if(x>y)
{
temp = x;
}
else
{
temp = x;
}
printf("max = %d\n", temp);
}
void test6()
{
int (*p)(int, int);
p = max;
p(3, 5);
}
int main()
{
test6();
return 0;
}
执行结果:
max = 5
(3) 函数指针数组:每个元素都是函数指针的数组
返回值类型 (*函数指针变量名[函数指针个数])(形参列表)
int(*p[10])(int,int);
定义了一个函数指针数组,有 10 个元素 p[0] ~ p[9],每个元素都是函数指针,指向的函数,必须有整型的返回值,两个整型参数。
(4) 函数指针最常用的地方
回调函数:通过函数指针调用的函数
#include <stdio.h>
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int multi(int x, int y)
{
return x * y;
}
int div(int x, int y)
{
return x / y;
}
int operate(int (*p)(int, int), int a, int b)
{
return p(a, b);
}
int main()
{
int a, b, c, d;
a = operate(add, 3, 4);
b = operate(sub, 8, 12);
c = operate(multi, 2, 9);
d = operate(div, 9, 3);
printf("a, b, c, d = %d, %d, %d, %d\n", a, b, c, d);
return 0;
}
执行结果:
a, b, c, d = 7, -4, 18, 3
三、经常容易混淆的指针
- 指针数组
int *a[10];
数组 a 中有 10 个整型的指针变量 a[0] ~ a[9]
。
- 数组指针
int (*a)[10];
指向一个数组,加 1 指向下一个数组。
- 指针的指针
int **p;
用来保存指针的地址。
-
指针函数:返回值为指针的函数。
-
函数指针:指向函数的指针。
四、特殊指针
- 空类型的指针(void *)
char *
类型的指针指向 char 型的数据。
int *
类型的指针指向 int 型的数据。
float *
类型的指针指向 float 型的数据。
void *
通用指针,任何类型的指针都可以给 void * 类型的指针变量赋值。
举例子:
有个函数叫 memset
void * memset(void *s,int c,size_t n);
这个函数的功能是将 s 指向的内存前 n 个字节,全部赋值为 c。
Memset 可以设置字符数组、整型数组、浮点型数组的内容,所以第一个参数,就必须是个通用指针。
它的返回值是 s 指向的内存的首地址,可能是不同类型的地址,所以返回值也得是通用指针。
注意:void* 类型的指针变量,也是个指针变量,在 32 位系统下,占 4 个字节。
- 空指针 NULL
char *p=NULL;
可以认为 p 哪里都不指向,也可以认为 p 指向内存编号为 0 的存储单位。
在 p 的四个字节中,存放的是 0x00 00 00 00 一般 NULL 用在给指针初始化。
五、main 函数传参
int main(int argc, char *argv[])
argc:是一个 int 类型的变量,表示命令终端传入的参数的个数
argv:是一个指针数组,用于保存每一个命令终端传入的参数。
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
printf("argc=%d\n",argc);
for(i=0;i<argc;i++)
{
printf("argv[%d]=%s\n",i,argv[i]);
}
return 0;
}
此例暂且不表,后续学习 Linux 时再说。