指针

内存相关概念

外存:又称拓展存储器,长期存放数据,是可掉电的设备,常见的外存设备有:硬盘、flash、ROM、U 盘、光盘、磁带。

内存:暂时存放数据的设备,掉电丢失数据,厂家的内存有:RAM,DDR.

内存分为物理内存和虚拟内存。

  • 物理内存:实实在在的存储设备;
  • 虚拟内存:操作系统虚拟出来的内存。

操作系统会将虚拟内存和物理内存进行映射。

在 32 位操作系统下,每个进程的寻址范围位 0x00000000~0xffffffff,即 4G,我们在编程时看到的内存地址都是虚拟地址。

在程序运行时,操作系统会将虚拟内存进行分区:

  • 堆:在动态申请内存时,会在堆区开辟空间;
  • 栈:主要存放局部变量;
  • 静态全局区:
    • 位初始化的静态全局区:没有初始化的静态变量(static 修饰的变量),或全局变量存放在此区;
    • 初始化了的静态全局区:初始化过的全局变量、静态变量存在此区。
  • 代码区:存放代码的区;
  • 文字常量区:存放常量的区;

指针的相关概念

本文均在 32 为平台上进行讨论。

系统给每个存储单元分配了一个编号,从 0x00000000~0xffffffff,这个编号就是地址。

指针就是地址。

image

指针变量:一个存放地址编号的变量。

在 32 位平台下,地址总线是 32 位的,所以地址是 32 位编号,占用 4 个字节,所以在 32 位平台下指针变量占用 4 个字节。

鱼代表鱼,虾代表虾,乌龟代表是王八。对应指针变量只能存放对应类型变量的地址,例如 int 类型的指针只能存放 int 类型变量的地址。

char 占 1 个字节,它有一个地址编号,这个地址编号就是其地址;int 占 4 个字节,它占有 4 个字节的存储单元,有 4 个地址编号。

image

指针的定义方法

定义指针的语法:

1
2
3
数据类型 *指针变量名;
int *p;
int *p1, p2;

与指针相关的运算符:&*.

& 是取地址运算符,* 是取值运算符。

image
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(int argc, char *argv[]) {

int a = 100;
int *p;
p = &a;

printf("a = %d %d\n", a, *p);
printf("&a = %p %p\n", &a, p);

return 0;
}

输出结果为:

image

指针大小:在 32 位系统下,所有类型的指针都是 4 个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main(int argc, char *argv[]) {
char *a;
short *b;
int *c;
long *d;
float *e;
double *f;

printf("sizeof(a) = %lld\n", sizeof(a));
printf("sizeof(b) = %lld\n", sizeof(b));
printf("sizeof(c) = %lld\n", sizeof(c));
printf("sizeof(d) = %lld\n", sizeof(d));
printf("sizeof(e) = %lld\n", sizeof(e));
printf("sizeof(f) = %lld\n", sizeof(f));

return 0;
}

输出结果为:

image

指针的分类

  1. char 型指针;
  2. short int 型指针;
  3. int 指针;
  4. long 指针;
  5. float 型指针;
  6. double 型指针;
  7. 函数指针;
  8. 结构体指针;
  9. 指针的指针;
  10. 数组指针。

不管是声明指针,在 32 位操作系统上,均占有 4 个字节。

指针和变量的关系

指针可以存放变量的地址。

在程序中,访问变量的值可以字节使用变量名,例如:

1
2
int a;
a = 100;

也可以通过指针来访问变量的值:

1
2
3
int *p;
p = &a;
*p = 100;

注意:

  1. 指针在试用期一定要初始化。
  2. 指针只能指向开辟好空间的地址,不能随意保存地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(int argc, char *argv[]) {

int *p1, *p2, temp, a, b;
p1 = &a;
p2 = &b;
printf("请输入:a b的值:\n");
scanf("%d %d", p1, p2); // 给p1和p2指向的变量赋值
temp = *p1; // 用p1指向的变量(a)给temp赋值
*p1 = *p2; // 用p2指向的变量(b)给p1指向的变量(a)赋值
*p2 = temp; // temp给p2指向的变量(b)赋值
printf("a=%d b=%d\n", a, b);
printf("*p1=%d *p2=%d\n", *p1, *p2);

return 0;
}

输出结果:

image

注意:对应类型的指针,只能保持对应类型数据的地址,如果想让不同类型的指针互相赋值,需要强制类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main(int argc, char *argv[]) {

int a = 0x1234, b = 0x5678;
char *p1, *p2;
printf("%#x %#x\n", a, b);
p1 = (char *)&a;
p2 = (char *)&b;
printf("%#x %#x\n", *p1, *p2);
p1++;
p2++;
printf("%#x %#x\n", *p1, *p2);

return 0;
}

输出结果:

image

注意:

  1. 使用 * 对指针取值时,取几个字节,由指针类型决定,int 类型的指针取 4 个字节,double 类型的指针取 8 个字节。
  2. 对指针进行 +1 操作时,指针会跳过若干字节,跳过的字节的大小有指针类型决定,例如 int 类型的指针会跳过 4 个字节。

指针和数组元素之间的关系

数组元素与指针的基本关系

变量存放在内存中,有自己的地址编号。数组时多个相同类型变量的集合,每个变量都占用内存空间,都有自己的内存编号,数组在内存中是连续存放的。

指针变量可以指向存放数组元素的地址。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, char *argv[]) {

int a[10];
int *p;
p = &a[0]; // 指针变量p保存了数组a中第0个元素的地址,即a[0]的地址

return 0;
}

访问数组数组元素的方法

方式 1:数组名[索引]

1
2
int a[10];
a[2] = 100;

方式 2:指针名 + 下标

1
2
3
4
int a[10];
int *p;
p = a;
p[2] = 100;

在 C 语言中,数组名就是数组的首地址,即第 0 个元素的地址,是个常量。

注意:p 和 a 不同,p 是指针变量,而 a 是个常量。所以可以使用等号给 p 赋值,但不能给 a 赋值。例如:int a[10]; a++; 这种用法是错误的,因为 a 是数组名,是一种地址常量。

方式 3:通过指针运算加取值的方法来引用数组的元素

1
2
3
4
int a[10];
int *p;
p = a;
*(p + 2) = 100;

p 是第 个元素的地址,p + 2a[2] 这个元素的地址,对第二个元素的地址取值,即 a[2]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main(int argc, char *argv[]) {

int a[5] = {0, 1, 2, 3, 4};
int *p = a;

// 只要将数组名赋值给同类型的指针变量,则此时的指针变量与数组名可
// 以用相同的方法操作数组
printf("a[2]=%d\n", a[2]);
printf("p[2]=%d\n", p[2]);

//*(a + n) <==> *(p + n) <==> a[n] <==> p[n]
printf("*(p+2) = %d\n", *(p + 2));
printf("*(a+2) = %d\n", *(a + 2));

printf("p=%p\n", p);
printf("p+2=%p\n", p + 2);
printf("&a[0] = %p\n", &a[0]);
printf("&a[2] = %p\n", &a[2]);

return 0;
}

输出结果:

image

指针的运算

指针可以加一个整数

往后指几个它指向的变量,结果还是个地址。

注意:一般来说,指针指向数组是加一个整数才有意义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main() {

int a[10];
int *p, *q;
// p和q间隔8个字节,意味着加一个整数最终移动的字节数与指针变量的类型也有关系
p = a;
q = p + 2;
printf("p = %p\n", p);
printf("q = %p\n", q);

return 0;
}
image

两个相同类型指针可以比较大小

注意:只有相同类型的指针指向同一个数组里的元素,比较大小才有意义。

指向前面元素的指针小于指向后面元素的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main() {
int a[10];
int *p, *q;
p = &a[1];
q = &a[6];
if (p < q) {
printf("p < q\n");
} else if (p > q) {
printf("p > q\n");
} else {
printf("p = q\n");
}
return 0;
}

输出结果:

image

两个相同类型的指针可以做减法

注意:只有相同类型的指针指向同一个数组里的元素,做减法才有意义。

做减法的结果是两个指针之间有多少个元素。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {

int a[10];
int *p, *q;
p = &a[0];
q = &a[3];
printf("%lld\n", q - p);

return 0;
}

输出结果:

image

两个相同类型的指针可以相互赋值

注意:只有相同类型的指针才可以互相赋值(void * 类型的除外)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main() {

int a = 100;
int *p, *q;
p = &a;
printf("a = %d %d\n", a, *p);
q = p;
printf("*q = %d\n", *q);
*q = 999;
printf("a = %d\n", a);

return 0;
}

输出结果:

image

指针数组

指针可以保存数组元素的地址,也可以定义一个数组,数组中的元素是若干个相同类型的指针变量,这个数组就是指针数组

定义指针数组的语法:

1
类型说明符 *数组名[元素个数];

例如:

1
2
3
4
5
6
int *p[10];
int a;
p[1] = &a;
int b[10];
p[2] = &b[3];
// p[2] 和 *(p + 2) 是等价的,都是指针数组中的第 2 个元素。

指针数组按照其元素进行分类。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {

char *name[5] = {"Follw me", "BASIC", "Greatwall", "FORTRAN", "Computer"};
int i;
for (i = 0; i < 5; i++) {
printf("%s\n", name[i]);
}
return 0;
}

输出结果:

image

指针的指针

指针的指针又称为二级指针。

指针本身也是一个变量,也有地址,可以用二级指针保存其地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(int argc, char *argv[]) {

int a = 100;
int *p = &a;
int **q = &p;

printf("a = %d %d %d\n", a, *p, **q);
printf("&a = %p %p %p\n", &a, p, *q);
printf("&p = %p %p\n", &p, q);
printf("&q = %p\n", &q);

return 0;
}

输出结果:

image

字符串和指针

在 C 语言中,字符串是以 '\0' 结尾的若干字符的集合。

字符串的存储形式:数组、字符串指针、堆。

  1. char string[100] = "I love C!";, 定义了一个字符数组 string,用来存放多个字符,并且使用 I love C!\0string 数组初始化。
  2. char *str = "I love C!",定义了一个指针变量 str,这个字符串中的字符不能存放在 str 变量中,str 只存放了字符 I 的地址,"I love C!" 存放在文字常量区。
  3. char *str = (char*)malloc(10 * sizeof(char));,动态申请了 10 个字节的存储空间,使用首地址个给 str 赋值,可以使用 strcpy(str, "I love C") 的方式将字符串 "I love C!" 拷贝到 str 指向的内存中。

可修改性:

  1. 栈区和全局区内存中的内容是可修改的。
1
2
char str[100] = "I love C!";
str[0] = 'y'; // 这是 ok 的
  1. 文字常量区的内容是不可修改的。
1
2
char *str = "I love C!";
*str = 'y'; // 这是不行的
  1. 堆区的内容是可以修改的。
1
char *str = (char*)malloc(10 * sizeof(char));

初始化:

字符数组、指向字符串的指针可以在定义时初始化:

1
2
char buf_aver[] = "hello world";
char *buf_point = "hello world";

堆中存放的字符串不能初始化,只能使用 strcpyscanf 进行赋值:

1
2
3
4
char *buf_heap;
buf_heap = (char*)malloc(15);
strcpy(buf_heap, "hello world");
scanf("%s", buf_heap);

使用时赋值:

字符数组使用 scanf 或者 strcpy 进行赋值:

1
2
3
4
char buf_aver[128];
buf_aver = "hello kitty"; //这样是错误的,数组名是个常量
strcpy(buf_aver, "hello kitty"); // 正确
scanf("%s", buf_aver); // 正确

指向字符串的指针:

1
2
3
char *buf_point;
buf_point = "hello kitty"; // 正确,buf_point 指向另外一个字符串
strcpy(buf_point, "hello kitty"); // 错误

数组指针

二维数组

二维数组,有行,有列。二维数组可以看成有多个一维数组构成的,是多个一维数组的集合。

例如:

1
int a[3][5];

定义了一个 3 行 5 列的二维数组。

可以认为二维数组 a 由 3 个一维数组构成,每个元素是一个一维数组。

二维数组 a 中,a + 1 指向下一个元素,即下一个一维数组,即下一行。

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char *argv[]) {
int a[3][5];
printf("a=%p\n", a);
printf("a+1=%p\n", a + 1);
return 0;
}

输出结果:

image

数组指针的概念

数组本身是一个指针,指向一个数组,对数组指针加 1,指向下一个数组。

数组指针可以用来保存二维数组的首地址。

数组指针的定义方法

数组指针的定义语法:

1
指向的数组的类型 (*指针名)[指向的数组元素的个数]

例如:

1
int (*p)[5]

定义了一个指向有 5 个元素的 int 类型的指针 p,p + 1 会跳过一个有 5 个数组元素的数组。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(int argc, char *argv[]) {
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);
return 0;
}

输出结果:

image

数组指针的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

void fun(int (*p)[5], int x, int y) { p[0][1] = 101; }

int main() {
int a[3][5] = {0};
fun(a, 3, 5);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 5; j++) {
printf("%d ", a[i][j]);
}
printf("\n");
}
return 0;
}

输出结果:

image

各种数组指针的定义

一维数组指针

一维数组指针,加 1 后指向下一个一维数组。

1
int (*p)[5];

配合每行有 5 个 int 类型的二维数组来用:

1
2
3
4
5
6
7
8
9
int a[3][5];
int b[4][5];
int c[5][5];
int d[6][5];

p = a;
p = b;
p = c;
p = d;

上述操作都是可以的。

二维数组指针

二维数组指针,加 1 后指向下一个二维数组。

1
int (*p)[4][5];

配合三维数组来用,三维数组由若干 4 行 5 列的二维数组组成。

1
2
3
4
5
6
7
8
9
int a[3][4][5];
int b[4][4][5];
int c[5][4][5];
int d[6][4][5];

p = a;
p = b;
p = c;
p = d;

上述操作都是可以的。

三维数组指针

三维数组指针,加 1 后指向下一个三维数组

1
int (*p)[4][5][6];

p + 1 跳一个三维数组,配合 int a[7][4][5][6]; 使用。

容易混淆的内容

指针数组:是个数组,数组元素是指针。

数组指针:是个指针,指向一个数组。

指针的指针:是指向指针的指针。

数组名字取地址

对一维数组名取地址,变成一位数组指针,即加 1 跳一个一维数组。

1
int a[10];

a + 1 跳一个 int 元素,即 a[1] 的地址,aa + 1 相差一个元素,4 个字节。

&a 就变成了一个一维数组指针,是 int(*p)[10] 类型的,(&a) + 1&a 相差一个数组,10 个元素,40 个字节。

数组名字和指针变量的区别

1
2
3
int a[10];
int *p;
p = a;

相同点:a 是数组的名字,是 a[0] 的地址,p 也保存了 a[0] 的地址,即 ap 都指向 a[0],所以在访问数组元素时,ap 是等价的。

不同点:

  1. a 是常量,p 是变量,可以使用 = 运算符为 p 赋值,但是不能为 a 赋值;
  2. a 取地址和对 p 取地址得到的结果不同。对 a 取地址得到的是指针数组,对 p 取地址得到的是指针的指针。

多维数组中指针的转换

在二维数组中,行地址取 * 会将指针降级,由行地址变成指向这一行第 0 个元素的指针,取 * 之后还是会指向同一个地方,但指针类型不一样了。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
int a[3][5];
printf("a=%p\n", a);
printf("a +1=%p\n", a + 1);
printf("*a =%p\n", *a);
printf("(*a)+1 =%p\n", (*a) + 1);

return 0;
}

输出结果:

image

指针与函数的关系

指针作为函数的参数

指针可以作为函数的参数。

C 语言中的参数传递方式:复制传参、地址传参。

复制传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

void fun(int a, int 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);
}

int main(int argc, char *argv[]) {
int a = 100, b = 20;
printf("before fun: a = %d, b = %d\n", a, b);
printf("&a = %p, &b = %p\n", &a, &b);
fun(a, b);

printf("after fun: a = %d, b = %d\n", a, b);

return 0;
}

输出结果:

image

地址传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

void fun(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);
}

int main(int argc, char *argv[]) {
int a = 100, b = 20;
printf("before fun: a = %d, b = %d\n", a, b);
printf("&a = %p, &b = %p\n", &a, &b);
fun(&a, &b);
printf("after fun: a = %d, b = %d\n", a, b);

return 0;
}

输出结果:

image

传数组

将数组为参数传递给函数,传递的是数组的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>

// 传一维数组
// void fun1(int p[])//形式1
// 形式2(常用)
void fun1(int *p) {
printf("%d\n", p[2]);
printf("%d\n", *(p + 3));
}

void test1() {
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8};
fun1(a);
}

// 传二维数组
// void fun2( int p[][4] )//形式1
// 形式2:通过数组指针
void fun2(int (*p)[4]) {

printf("%d\n", p[0][2]);
printf("%d\n", *(*(p + 1) + 2));
}

void test2() {
int a[2][4] = {1, 2, 3, 4, 5, 6, 7, 8};
fun2(a);
}

void fun3(char **q) {
int i;
for (i = 0; i < 3; i++) {
printf("%s\n", q[i]);
}
}

void test3() {
char *p[3] = {"hello", "world", "kitty"};
fun3(p);
}

int main() {
test1();
test2();
test3();
return 0;
}

输出结果:

image

指针函数

返回值为指针的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

// 指针函数:指针作为函数的返回值
char *fun() {
// 栈区开辟的空间会随着当前代码段的结束而释放空间
// char str[100] = "hello world";
// 静态区的空间不会随着当前代码段的结束而释放空间
static char str[100] = "hello world";
return str;
}

int main() {
char *p;
p = fun();
printf("p = %s\n", p);
return 0;
}

输出结果:

image

函数指针

在 C 语言中,函数名就是函数的首地址,可以定义一个函数指针变量指向这个函数。

函数指针的定义语法:

1
返回值类型 (*指针名)(形参列表);

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int max(int x, int y) {}
int min(int x, int y) {}

int main() {

int (*p)(int, int);

p = max;
p = min;

return 0;
}

调用函数的方法:

通过函数的名字调用:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int max(int x, int y) {}

int main() {
int num;
num = max(3, 5);
return 0;
}

通过函数指针调用:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int max(int x, int y) {}

int main() {
int num;
int (*p)(int, int);
p = max;
num = p(3, 5);
return 0;
}

函数指针数组:

是一个数组,每个元素都是一个函数指针。

定义方式:

1
返回值类型 (*指针名)[函数指针的个数](形参列表);

例如:

1
int (*p)[10](int, int);

定义了一个函数指针数组,有 10 个元素,每个元素指向一个返回值为 int,有两个 int 类型的参数的指针。

函数指针最常用的地方:将函数作为另外一个是函数的参数传递过去,即回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

int add(int x, int y) { return x + y; }

int sub(int x, int y) { return x - y; }

int mux(int x, int y) { return x * y; }

int dive(int x, int y) { return x / y; }

int process(int (*p)(int, int), int a, int b) {
int ret;
ret = (*p)(a, b);
return ret;
}

int main(int argc, char *argv[]) {
int num;
num = process(add, 2, 3);
printf("num = %d\n", num);
num = process(sub, 2, 3);
printf("num = %d\n", num);
num = process(mux, 2, 3);
printf("num = %d\n", num);
num = process(dive, 2, 3);
printf("num = %d\n", num);

return 0;
}

输出结果:

image

特殊指针

void 类型的指针:万能指针,可以将任意类型的指针赋值给他,但 void 类型的指针要转换成其他类型的指针必须要强转。有些函数的参数或者返回值就是 void * 类型。

NULL:空指针,编号为 0 的指针,地址为 0x00000000,一般用 NULL 给指针初始化。

main 函数传参

C 语言中主函数的头为:

1
int main(int argc, char *argv[]);

argv:记录输入的参数个数。

argv:记录输入的参数值。

1
2
3
4
5
6
7
8
9
10
11
#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;
}

使用如下命令运行:

1
main10.exe nihao hello world

输出结果为:

image

可以看到第一个参数是程序在计算机上的绝对路径,从第 2 个参数开始才是我们输入的值。