【C2】指针和数组



2021年11月21日    Author:Guofei

文章归类: C    文章编号: 10002

版权声明:本文作者是郭飞。转载随意,但需要标明原文链接,并通知本人
原文链接:https://www.guofei.site/2021/11/21/c2.html


指针

  • 指针是一种数据类型,它指向内存
  • 地址编号本身是 8 个字节(64位系统)or 4个字节(32位系统)的 无符号整数
int *p;
// 1. 定义一个指针变量 p,规定它指向 int
// 2. 如果用 void *p 可以指向任何类型

int a;
p = &a; // 把 a 的内存赋值给 p
// 或者 int *p = &a;
a = 1;
printf("%p = %p \n",p,&a); // 打印内存编号

*p = 10; // 给指针指向的位置赋值,*p 就代表a
// *p 指的是,取出 p 对应那块内存的内容,
// &a 指的是,取出 a 分配的内存编号
  1. register int a; 定义的变量是无法用 & 取地址的,因为它一直在寄存器中。
  2. 一定要保证类型一致,不然会发生混乱

空指针和野指针

// 野指针,会产生错误
int *p ;
*p = 100;
// 只是声明了一个指针,却未指向内存

// 空指针
int *p = NULL;
// 空指针是允许的
// NULL 其实是0

指向常量的指针 与 指针常量

// 指向常量的指针:你可以修改指针,但不能修改它指向的那个值。
int a = 0;
const int *p = &a; // p 指向常量的指针
// 可以通过 *p 读取,但不能通过 *p 写。可以通过 a 写。
// *p = 1; // 这个是不允许的
// a = 1; // 这个是允许的
// 可以这么理解,获取了 a 的只读权限


// 常量指针
int *const p = &a;
// 这个 p 是常量指针,不能再指向其它变量,例如 p = &a2 是错的


// 错误用法
const int a = 100;
int *p = &a; // 必需 const int *p = &a;
*p = 1;
printf("%d != %d\n", a, *p); // 不相同,好像和编译器有关,不知道为啥
printf("%p == %p", p, &a); // 相同
// 上面这个用法是不符合规范的。

指针运算

p + 1; // 指向下一个 “数据单元”,而不是16进制地址加1
p - 1;
// 实际移动多少 16 进制大小呢?取决于定义指针时的指定类型。例如 int 是 4,double 是 8
// 如果指针类型和变量类型不一致(虽然不符合规范),以指针类型为准


// 同样,也可以有这个:
p += 5;
p -= 3;
p++;
p--;

*(p + 3) = 100;
p[3] = 100; // 等价写法,即使不是数组,也能这么写,位移3个单位

有趣的例子1

int a = 0x12345678;
char *p = &a; // 也可以 char *p = (char *) &a;

printf("%x\n", *p);
printf("%x\n", *(p + 1));
printf("%x\n", *(p + 2));
printf("%x\n", p[3]);
// 返回:
// 78
// 56
// 34
// 12

// 1. 颠倒过来是因为整数是“正向对其”的
// 2. 也可以写入

有趣的例子2:ip 其实对应一个 int

unsigned int a = 987654321;
unsigned char *p = (unsigned char *)&a;
for(int i=3;i>=0;i--){
    printf("%u.",p[i]);
}

思考: ip 如何转回为 int

unsigned char a[4] = {58, 222, 104, 177};
unsigned char tmp[4];
for (int i = 0; i < 4; i++) {
    tmp[i] = a[3 - i];
}
unsigned int *p = &tmp;
printf("%d",*p);

指针与数组

指向数组元素的指针 int *p

int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p1 = arr; // 写法1
int *p3 = &arr[1]; // 写法2
int *p2 = (int *) &arr; // 写法3,&arr 是指向整个数组的指针,转为 int * 类型


p[3] = 100; // 可以直接当数组名用
*(p + 3); // 等价用法
printf("%ld != %ld ", sizeof(arr), sizeof(p)); //但又不完全一样
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = &arr[1];

p[3] = 100; // 其实是 a[4]

指向整个数组的指针 int (*p)[10]

// 方法1:
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int (*arr1_p)[10] = &arr;
// int (*arr1_p)[] = &arr; // 省略长度,但不能用 arr1_p + 1 了


// 方法2:
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
typedef int (MY_ARRAY)[10];
MY_ARRAY *arr1 = &arr;

// 方法3:
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
typedef int (*MY_ARRAY_p)[10];
MY_ARRAY_p arr1_p = &arr;

解释:

  • 其“基本单元”是数组,p+1 指向下一个数组
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int (*arr1_p)[10] = &arr;

printf("%p = %p\n", arr1_p, *arr1_p);
// 普通的数组也是这样,不知道如何深入理解

arr1_p + 1; // 是把这个数组当做一个“整体”,然后移动到下一个位置:5*int 个 Byte
*arr1_p; // 是个指针,指向第一个元素,指针类型是 int *
*arr1_p + 1; // 一个指针,指向第二个元素
*(*arr1_p + 1) ; // 第1个元素
*(arr1_p + 1) - 1; // 一个指针,指向倒数第1个元素
arr1_p[1] - 1; // 等价于上面的

指针组成的数组 int *p[5]

指针数组:一个数组,数组的元素是指针

int *p[5]; // 一个数组,它的元素是5个指针(叫做指针数组)
p[2]; // 这是一个指针
*p[2]; // 这是那个指针指向的值

// 你需要先把指针的指向赋值好,才能用这个指针
int a = 2;
p[3] = &a;
*p[3] = 5;

二级指针

指向指针的指针

int a = 0;
int *p = &a;
int **pp;//二级指针,指向指针的指针
pp = &p;
// 或者 int **p = &p;


**pp = 10; // 通过二级指针访问 a

如果想用一个指针指向指针数组,必须用一个二级指针指向它

int value = 10;
int *arr[10];//一个指针数组
arr[2] = &value;//指针数组的第2个,指向一个 int

int **p = arr; //指向指针数组的指针,必须用二级指针

p[2]; // 其实是 arr[2] ,存放的是一个指针
*p[2] = 5; // 那么,这个指针其实指向 value

类似的,还有多级指针

二维数组与指针

int mat[4][3] = {
        {1,  2,  3},
        {4,  5,  6},
        {7,  8,  9},
        {10, 11, 12}
};


printf("%p\n", mat); // 指向第0行第0个元素: &arr[0][0]
printf("%p\n", mat + 1); // 指向第1行: &arr[1][0]
printf("%p\n", &mat + 1); //指向下一块: &arr[3][2] + 1

// 用一个指针,指向二维数组(元素是一行)
int(*p)[3] = mat;
//    p[i][j] 等价于 *(*(p+i)+j)
printf("%d", *(*(p + 1) + 2));  // 指向第1行第2个元素

// 用一个指针,指向整个二维数组(基本单元是整个 mat)
int (*p_mat)[3][3] = &mat


// 一维 arr 行为类似 int *,其基本元素是 int,arr + 1 指向下一个 int
// 二维 mat 行为类似 int (*p)[10],其基本元素是一行,mat + 1 指向下一行

指针作为函数的入参

// 1. 一维数组作为入参
int my_func1(int *arr, int len_arr);

// 2. 二维数组作为入参
// c99可以,c90不行:
int my_func1(int height, int width, int p[height][width]) {
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            printf("%d\t", p[i][j]);
        }
        printf("\n");
    }
    return 0;
}

int mat[4][3] = {
        {1,  2,  3},
        {4,  5,  6},
        {7,  8,  9},
        {10, 11, 12}
};
my_func1(4, 3, mat);


// 2.2 c90之前的版本只能这样(以 int * 的形式传入指针):
int my_func1(int height, int width, const int *p) {
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            printf("%d\t", *((int *) p + width * i + j));
        }
        printf("\n");
    }
    return 0;
}

// 2.3 不推荐的做法,因为它们必须事先确定列数
//my_fun(int len1, int len2, int mat[3][3])
//my_fun(int len1, int len2, int mat[][3])
//my_fun(int len1, int len2, int (*p)[3])

// 3. 字符串组成的数组作为入参
void my_func1(int argc, char **args) {
    for (int i = 0; i < argc; i++) {
        printf("%s, ", args[i]);
    }
}

char *str_lst[] = {"hello", "world"};
my_func1(2, str_lst);


// 4. 以指向 xx 的指针作为入参,功能是可以在函数中改它的值
// 展示入参是:指向 int 的指针 + 指向一维数组的指针
// 它们的形式是 int *(形式上和一维数组一样), int **(形式上和二维数组一样)
void my_fun(int *size, int **array) {
    *size = 5;
//    在函数中让它真正的指向一个一维数组,因此必需配合上面的 *size,否则外面不可见它的大小
    *array = malloc(sizeof(int) * 5);
    for (int i = 0; i < 5; i++) {
        (*array)[i] = i + 10;
    }
}

// 如何调用:
int *row = malloc(sizeof(int));
int **cols = malloc(sizeof(int *));
my_fun(row, cols);

知识点:当一个数组名作为入参时,自动作为指针变量。

// 以下三个是等价的:
int tst(int *a); // 规范写法
int tst(int a[10]);
int tst(int a[]);
// 编译的时候,3个都会编译成第1个

如果函数不修改数组,可以约定一个 const

int tst(const int *a); // 这样,函数内部就无法修改数组 a 了
// 其实做个强转后,还是能改的 int *p = (int *)a; p[5] = 999;
// C++ 没这漏洞了
// 约定:函数只要不改数组,都加 const

如果数组作为入参,数组的大小在函数中是不可见的

int tst(const int *a) {
    printf("%lu\n", sizeof(a));
    // 是指针本身的大小,必然是8
    // ???好像并不是,用“指向数组的指针”,与数组有关系
    return 0;
}


// 因此,往往需要额外传入一个数组长度。不过字符串不需要,因为字符串是 0 结尾的
int tst(int len_arr,int *arr) {
    return 0;
}

// 指针作为入参,分不清是指向数组还是数字
int tst(int *arr, int *size);
// arr是数组,size 是指向数字的指针,但看起来形式一样

// 额外:这些值不一样:
int *a1;
printf("打印指针本身的大小:8 %lu\n", sizeof(a));
// 8

int a2[10];
printf("打印整个数组的大小:4x10=40 %lu\n", sizeof(a));
// 40,如果是
char a3[10] // sizeof 返回 1x10 = 10

指针作为函数的返回值

一个函数也可以返回一个指针

int *tst(); // 函数返回指针

main函数的参数

// 适配使用命令行启动
int main(int argc, char **args) {
    printf("argc = %d\n",argc);
    for (int i = 0; i < argc; i++) {
        printf("%s, ", args[i]);
    }
    printf("\n");
    return 0;
}

// 如何使用?
// 编译后:./main -l -s 0
// 打印:
// argc = 4
// ./main, -l, -s, 0,

// !注意,在linux下,* 指的是通配符,被目录下的全部文件替换,要想传入星号,要传入 \*

// 实际上 args[argc] 也是有值的,是 NULL

指向函数的指针

//    f 是一个指针,指向函数
int (*f)();

// f 是一个指针,指向函数,这个函数的返回值是整型指针
int *(*f2)();

例子

int res1 = f(5);
int res2 = (*pf)(5);
int res3 = pf(5);
printf("%d = %d =%d\n", res1, res2, res3);

用途:提前不知道调用哪个函数


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

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


int main() {

// 声明一个函数指针
    int (*f)();

    enum type {
        ADD, SUB
    };
    enum type t = SUB;

    // 根据情况,给函数指针赋值
    if (t == ADD) {
        f = &my_add;
    } else {
        f = &my_sub;
    }

    printf("%d\n", f(1, 4));
}

使用 函数指针数组,上面的代码可以进一步简化

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

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

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

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

int main() {
//    一个指针,指向一个数组,数组的内容是函数,这个函数返回 int 类型
    int (*f[])(int, int) ={my_add, my_sub, my_mul, my_div};
    printf("%d\n", f[1](1, 4));
}

指针的应用

如果想用函数改变某个变量的值,必须通过指针

int swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
    return 0;
}

结构体作为入参时,会发生大量复制,因此一般用一个指向结构体的指针作为入参。

内存布局与malloc

内存布局分为4部分

  1. 代码加载到 代码区
  2. 所有的static、external放到 静态区
  3. auto类型、函数的形参、函数的返回值,放到 栈区
    • 每个线程有自己的栈,
    • 栈的最大尺寸固定,超过会溢出
    • 变量离开作用范围后,栈上的数据自动释放。例如,函数调完后,函数内部声明的变量,其内存被清空.
  4. ,容量远远大于栈,堆内存的申请和释放必须通过代码完成
    • 栈大小是有限的,很大的数组适合用堆
    • 如果数组定义时大小不能确定,适合用堆

int func(int a, int b){ // 从右向左入栈。b先入栈

}

需要 #include <stdlib.h>

  • char *s = malloc(n) 分配 n 个字节
    • 只分配内存,不会帮你初始化
    • 推荐用 malloc(n * sizeof(int)),因为它可移植
  • calloc(cnt, size) 分配 cnt 个单位,每个单位 size 大小
    • 分配的内存自动初始化为 0
  • s1 = realloc(s1, size_new) 重新分配内存大小
    • 有可能重新分配内存地址
    • 原本的 s1 会自动释放
    • 新空间不会自动初始化为 0
    • realloc(NULL, size_new) 的效果等同于 malloc
  • free(p) 不要忘了了,否则容易内存溢出
char *s = malloc(10); // 在堆中分配10个字节的空间
printf("%p\n", s);
strcpy(s, "abcd");
printf("%p\n", s);

free(s); // 必须手动释放内存,不是释放变量s,而是释放s指向的内存空间
printf("%p\n", s);

s = malloc(20); //s是变量,所以可以再次使用
printf("%p\n", s);

堆可以和“返回指针的函数”连用

下面这段代码编译可以通过,但不符合规范

int *tst() {
    int a = 10;
    return &a;
}

int main() {
    int *p = tst();
    printf("%d", *p);// 函数结束后,a 对应的内存值已经没了,所以p指向一个无效的空间。这个结果是不可预知的
}

正确的做法:

#include<stdio.h>
#include <stdlib.h>
#include <string.h>

int *tst() {
    int *p = malloc(1 * sizeof(int));
    return p;
}

int main() {
    int *p = tst();
    printf("%d", *p);
    free(p); // 不要忘记释放内存
}

当然,字符数组作为指针,被返回时也会又相同的问题

// !!! 这是错的!
char *tst() {
    char a[100] = "hello";
    return a;
}

// 这样才是对的,(调用后别忘了释放内存)
char *tst() {
    char *a= malloc(100);
    strcpy(a,"hello");
    return a;
}


// 其实这样也是对的,因为静态变量,整个程序结束后才会释放内存
char *tst() {
    static char a[100] = "hello";
    return a;
}

// 这个也是对的,常量也是一直在内存中的
const char *tst() {
    const char *a = "hello";
    return a;
}
// 上面等价于,这也是可以的
const char *tst() {
    return "hello";
}

一些注意:

  • p = malloc(10) 分配内存后,不要用 p++,因为 free(p) 会释放之后的10个字节,导致整个程序崩溃
  • malloc 的入参可以是变量,意味着你可以不用魔法数字,而是动态指定大小。
  • 错误:忘记检查分配内存是否成功。if(p == NULL),不过有说法现代环境不太需要检查
  • 错误:使用超过了内存分配的边界
  • 错误:忘记 free
  • 错误:使用已经被 free 的内存

操作系统分配内存时,会一次给出4k(windows,实测MacBook是动态值),而不是每次 malloc 都分配一次。

  • 所以,如果你事先知道大概需要占用多少内存,你可以用 char *s = malloc(4*1024); free(*s) 先把内存空间申请下来

您的支持将鼓励我继续创作!