C语言——指针

1. 指针

首先看一个指针的基本示例

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 main() {
char a = 'F';
int f = 123;

char *pa = &a;
int *pb = &f;

printf("a = %c\n", *pa);
printf("f = %d\n", *pb);

// 间接访问变量并修改值
*pa = 'C';
*pb += 2;

printf("a = %c\n", *pa);
printf("f = %d\n", *pb);

// 查看地址长度
printf("size of pa is %d\n", sizeof(pa));
printf("size of pb is %d\n", sizeof(pb));

// 打印地址
printf("address of a is %p\n", pa);
printf("address of f is %p\n", pb);

return 0;
}

运行结果如下:可以看到我的电脑是64位的,地址长度为8个字节

注意避免使用野指针

1
2
int *a;
*a = 123;

上面这种写法,首先定义了一个指针变量,但是没有赋初值,就是我们常说的野指针。a 指针随机指向了一个地址,这个地址可能是系统重要文件的地址,通过赋值语句,若强行修改了系统重要文件,将带来很多的麻烦。

虽然现在的系统都有保护,但是尽量避免出现野指针。

2. 指针和数组

数组名就是数组第一个元素的地址

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

int main() {
char str[128];

printf("输入字符串:");
scanf("%s", str); // 使用数组名则不需要取址符

printf("str = %s\n", str);

printf("str 的地址是:%p\n", str);
printf("str[0] 的地址是:%p\n", str);

return 0;
}

运行结果如下:

使用指针访问数组

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

int main() {
char str[] = {'a', 'w', 'e', 'l', 'l', 'f', 'r', 'o', 'g', };
char *p = str; // 指针指向数组

printf("str[1] = %c, str[2] = %c, str[3] = %c, str[4] = %c\n", *(p + 1), *(p + 2), *(p + 3), *(p+4));

return 0;
}

运行结果如下:

指针 + 1 不是地址加1,聪明的编译器将代码翻译为地址偏移当前数据类型对应的空间大小

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

int main() {
char str[] = {'a', 'w', 'e', 'l', 'l', 'f', 'r', 'o', 'g', };
int nums[] = {0, 1, 2, 3, 4, 5, 6, };
char *p = str; // 指针指向数组
int *p1 = nums;

printf("str[1] = %c, str[2] = %c, str[3] = %c, str[4] = %c\n", *(p + 1), *(p + 2), *(p + 3), *(p+4));
printf("nums[1] = %d, nums[2] = %d, nums[3] = %d, nums[4] = %d\n", *(p1 + 1), *(p1 + 2), *(p1 + 3), *(p1+4));

return 0;
}

运行结果如下:

利用数组遍历指针指向的内容

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

int main() {
char *str = "I'am a wellfrog, but never give up climbing up!";
int i, length = strlen(str);

for (i = 0; i < length; i++) {
printf("%c", str[i]);
}

return 0;
}

运行结果如下:

3. 指针与二维数组

4. NULL 与 void

void

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

int main() {
int num = 1024;
int *pi = &num;
char *ps = "Awellfrog";
void *pv;

pv = pi;
printf("pi:%p, pv:%p\n", pi, pv);
printf("pv:%d\n", *pv); // 没有声明指针类型,编译器懵了,不知道分配多大空间

ps = pv;
printf("ps:%p, pv:%p\n", ps, pv);
printf("pv:%s\n", pv);

return 0;
}
1
2
3
4
5
6
7
8
9
'''
pv = pi;
printf("pi:%p, pv:%p\n", pi, pv);
printf("pv:%d\n", *(int *)pv); // 声明指针类型强转为 int 型,并解引用

ps = pv;
printf("ps:%p, pv:%p\n", ps, pv);
printf("pv:%s\n", pv); // 因为字符串打印只需要首地址,所以指针类型可以强转也可以不强转
'''

NULL

NULL 的定义是指向位置 0 的 void 类型指针,计算机中 0 位置一般会被置为空,即 NULL 是指向空,或没有意义的意思。

1
#define NULL ((void *)0)
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
int *p1 = NULL;
int *p2; // 没有初始化,指向随机,称为野指针

printf("p1 = %p, p2 = %p\n", p1, p2); //打印地址
printf("%d\n", *p1); // 打印内容

return 0;
}

可以看到这里的 gcc 编译器精明的将野指针初始化为地址 0。而打印该无效内容地址下的内容,计算机则会出现问题,停止工作。

NULL 不是 NUL('\0')。

  • NULL 用于指针和对象,表示控制,指向一个不被使用的地址。
  • '\0' 表示字符串的结尾。

5. 指向指针的指针

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

int main() {
int num = 520;
int *p = &num;
int **pp = &p; // 二级指针,指向指针 p 的地址

printf("num:%d\n", num); // num 的值
printf("*p:%d\n", *p); // p 指向 num 的地址,解引用为 num 的值
printf("**p:%d\n", **pp); // pp 指向 p 的地址,解引用为 p 的值(即 &num),再解引用为 num 的值
printf("&p:%p, pp:%p\n", &p, pp);
printf("&num:%p,p:%p,*pp:%p\n", &num, p, *pp);

return 0;
}
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
#include <stdio.h>

int main() {
char *cBooks[] = {
"《C语言程序设计》",
"《C primer plus》",
"《C 与指针》",
"《带你学 C 带你飞》",
};
char **byFishC;
char **classicBooks[3]; // 二维指针数组,每个数组的值是一个二维地址
int i;

byFishC = &cBooks[3]; // 指针的值是 cBooks[3] 的首地址
classicBooks[0] = &cBooks[0];
classicBooks[1] = &cBooks[1];
classicBooks[2] = &cBooks[2];

printf("FishC出版的:%s\n", *byFishC); // 解引用是 cBooks[3] 的内容
printf("经典书籍:\n");

for (i = 0; i < 3; i++) {
printf("%s\n", *classicBooks[i]);
}

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

int main() {
int array[][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10,11},};
int **p = array;
int i, j;

printf("p: %p, array: %p\n", p, array);
printf("p+1: %p, array + 1: %p\n", p + 1, array + 1);

return 0;
}

二维指针不知道每行有几列,所以不能直接使用列指针访问。

1
2
3
'''
int (*p)[4] = array; // 告诉二维指针每行列是多少,此时可以使用列指针
'''

所以可以使用列指针搭配行指针遍历数组

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

int main() {
int array[][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10,11},};
int (*p)[4] = array; //
int i = 0, j = 0;

for (i = 0; i < 3; i++) {
for (j = 0; j < 3; j++) {
printf("%2d ", *(*(p + i) + j));
}
}

return 0;
}

还可以骚一点,这样就离被开除不远啦!

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

int main() {
int array[][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10,11},};
int (*p)[3][4] = &array; // 取数组的地址(因为此时 p 是一个指向二维数组的地址的指针,p 比 array 高一个维度)
int i = 0, j = 0;

for (i = 0; i < 3; i++) {
for (j = 0; j < 3; j++) {
printf("%2d ", *(*(*p + i) + j)); // 多一层解引用
}
}

return 0;
}

6. 常量和指针

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

int main() {
const float pi = 3.14; // 定义一个由 const 修饰的只读变量(不可修改,即视为常量)

printf("pi = %f\n", pi); // 可正常打印

return 0;
}

但是如果对 const 修饰的变量进行修改,则编译器报错。

1
2
3
4
'''
const float pi = 3.14;
pi = 5;
'''
  • 指向常量的指针: const int *p = NULL;
  • 常量指针: int * const p = NULL;
  • 指向常量的常量指针不能修改常量的值
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {
int num = 1;
const int cnum = 2;
const int *pc = &cnum;

printf("cnum:%d, &ccum:%p\n", cnum, &cnum);
printf("*pc:%d, pc:%p\n", *pc, pc);

return 0;
}

若修改指针指向的值,则错误:

1
2
3
4
5
6
7
'''
printf("*pc:%d, pc:%p\n", *pc, pc);

*pc = 10;

return 0;
'''
  • 指向常量的指针指向变量,不能通过常量指针修改变量的值,但是可以通过直接修改变量修改变量的值。

  • 常量指针指向变量,常量指针本身不能被修改,但是可以通过指针修改变量。

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

    int main() {
    int num = 1;
    int num2 = 2;
    const int cnum = 2;
    int * const pc = &num;

    *pc = 10;
    printf("*pc=%d, num=%d\n", *pc, num);

    // pc = &num2;

    return 0;
    }

    如果解注释修改常量指针,则报错。

    常量指针本身还是一个 int 型指针,所以不能指向 const int 型变量。

    1
    2
    3
    int num = 1;
    const int cnum = 2;
    int * const pc = &cnum;
  • 指向常量的常量指针:指针自身不能被修改,指针指向的值也不能被修改。

    1
    2
    const int cnum = 1;
    const int * const p = &cnum;
  • 指向“指向常量的常量指针”的指针。

    1
    2
    3
    const int cnum = 1;
    const int * const p = &cnum;
    const int * const *pp = &p;

    或者按照如下书写方式,但是不好理解:

    1
    2
    3
    const int cnum = 1;
    const int const *p = &cnum;
    const int const **pp = &p;

7. 函数参数与指针

  • 利用指针实现交换函数,通过传入实参的地址,直接修改实参。(如果只是简单的传入实参,利用形参交换,形参会在函数结束时被释放,而 main 中的实参未被修改。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void swap(int *x, int *y); // 由于编译器顺序执行,这里要提前声明,后面补全函数。

void swap(int *x, int *y) {
int temp;

temp = *x;
*x = *y;
*y = temp;
}

int main() {
int a = 1, b = 2;

swap(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
  • 参数为数组时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

void get_array(int b[10]);

void get_array(int b[10]) {
b[1] = 520;

printf("sizeof(b) = %d\n", sizeof(b));
printf("b[1] = %d\n", b[1]);
}

int main() {
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7,8,9};

get_array(a);
printf("sizeof(a) = %d\n", sizeof(a));
printf("a[1] = %d\n", a[1]);

return 0;
}

可以看到,传入的并不是整个数组,而是 8 位的数组首地址(64 位系统)。而在 main 中是整个数组 a[10] 十个整数的大小。

通过传入地址,可以修改原来数组中的实参值。

  • 可变参数(variable-argument)
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>
#include <stdarg.h>

int my_sum(int n, ...);

int my_sum(int n, ...) { // n 代表输入的参数个数
int i, sum = 0;
va_list vap; // 声明一个字符串指针列表

va_start(vap, n); // 将输入的 n 个数加载到列表 vap 中
for (i = 0; i < n; i++) {
sum += va_arg(vap, int); // 将列表 vap 中的数按 int 类型取出
}
va_end(vap); // 结束该获取参数的过程

return sum;
}

int main() {
int result;

result = my_sum(3, 1, 2, 3);
printf("result1 = %d\n", result);

result = my_sum(5, 1, 2, 3, 4, 5);
printf("result = %d\n", result);

return 0;
}

8. 指针函数与函数指针

指针函数: 返回值为指针的函数。

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

char *getWord(char);

char *getWord(char c) {
switch(c) {
case 'A' : return "Apple";
case 'B' : return "Banana";
case 'C' : return "Cat";
case 'D' : return "Dog";
default : return "None";
}
}

int main() {
char input;

printf("请输入一个字母: ");
scanf("%c", &input);

printf("%s\n", getWord(input));

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char *getWord(char c) {

char str1[] = "Apple";
char str2[] = "Banana";
char str3[] = "Cat";
char str4[] = "Dog";
char str5[] = "None";

switch(c) {
case 'A' : return str1;
case 'B' : return str2;
case 'C' : return str3;
case 'D' : return str4;
default : return str5;
}
}

上面的程序返回一个独立的字符串可以正常执行,但是如果返回局部变量的字符数组则报错。此处显示不能返回函数中局部变量的地址,因为该局部地址将会在函数结束之后随函数一同被释放。

你肯定想问:为什么直接返回一个字符串就行呢?

那是因为字符串是一类特殊的存在,它会自己存储在一个特殊的区域,不会和局部变量一样立即消失。

函数指针: 指向函数的指针。

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

int square(int);

int square(int num) {
return num * num;
}

int main() {
int num;
int (*fp)(int); // 定义一个函数指针,它将指向一个输入为 1 个 int ,同时返回值为 1 个 int 的函数。

printf("请输入一个参数: ");
scanf("%d", &num);

fp = square;

printf("%d * %d = %d\n", num, num, (*fp)(num));

return 0;
}

上面的语句还可以修改为以下几种写法都可以正常运行,但是不推荐,因为不容易理解。

1
fp = &square; // 函数指针指向时,直接指向函数的地址。(因为函数名即代表其首地址,这与数组类似)
1
printf("%d * %d = %d\n", num, num, fp(num)); // 直接使用 fp(num) ,因为 fp 存的就是 square ,但是这样容易把 fp 当作一个函数

传入的参数是指针

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
#include <stdio.h>

int add(int, int);
int sub(int, int);
int calc(int (*)(int, int), int, int);

int add(int a, int b) {
return a + b;
}

int sub(int a, int b) {
return a - b;
}

int calc(int (*fp)(int, int), int a, int b) {
return (*fp)(a, b);
}


int main() {
printf("3 + 5 = %d\n", calc(add,3,5));
printf("3 - 5 = %d\n", calc(sub, 3, 5));

return 0;
}

更变态一点,再加一个 select ,即返回值为函数的函数。

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
#include <stdio.h>

int add(int a, int b);
int sub(int a, int b);
int calc(int (*)(int, int), int, int);
int (*select(char))(int, int); // select 是一个指向输入为两个int,返回值为 1 个 int 的函数指针

int add(int a, int b) {
return a + b;
}

int sub(int a, int b) {
return a - b;
}

int calc(int (*fp)(int, int), int a, int b) {
return (*fp)(a, b);
}

int (*select(char op))(int, int) {
switch(op) {
case '+' : return add;
case '-' : return sub;
}
}

int main() {
int a, b;
char op;
int (*fp)(int, int); // 定义一个函数指针

printf("请输入一个式子(例如1+3):");
scanf("%d%c%d", &a, &op, &b);

fp = select(op); // 函数指针指向 select 返回的函数
printf("%d %c %d = %d\n", a, op, b, calc(fp, a, b));
return 0;
}