C语言指针

发布于 2021-08-11  0 次阅读


指针基础

  1. 打印变量的地址,以 16 进制显示
    int a;
    scanf("%d", &a);
    
    int *p = &a;
    printf("%#x",p);    
    
  2. 打印出指针类型的空间大小
    printf("%d", sizeof(int *));  // msvc 64位编译器是8个字节大小,gcc 编译器是4个字节大小
    
  3. 注意指针套娃,最不注意的是 &pp 的值,这个用不到,但不能忽略
    int *p = &a;
    int **pp = &p;    // 指针变量的地址存入到另一个指针变量中
    

    此处逻辑:首先系统通过 &pp 的地址 0xad28bffd60 找到变量 pp = &p,获取到其中的值 0xad28bffd58,这个值就是指针变量 p 的地址,然后通过这个地址去找到指针变量 p,发现 p 里面的值是 0xad28bffd64(&a),然后去找该地址 &a 对应的值,发现里面的值是 100,正好是变量 a 的值

    clion64_cdINTaHAzc.png

C语言只读指针变量和(指向)只读变量(的)指针

const 需要从后向前看,修饰的是 const 左边的数据类型。注意,* 可以看做是指针数据类型,就如同 int 是整数数据类型一样。

int b = 0;

int *const p = &a;  // 此时 const 修饰的是指针,所以指针变量不可更改,但指针指向的变量值可以修改
// *p = 3;  OK
// p = &b;  ERROR

int const *p = &a;    // 此时 const 修饰的是指针变量指向的值,所以指针变量可以更改,但指针指向的变量值不可以修改
// *p = 3;  ERROR
// p = &b;  OK

int const *const pp = &p;    // 这里第一个 const 修饰的是指针,所以指针变量的值不可改变;第二个 const 修饰的是指针变量指向的值,也不可改变
// *pp = 3;  ERROR
// pp = &b;  ERROR

特殊的指针

指针指向自己,指针指向的变量值是该指针变量所在的地址。首先找到指针变量 p 的地址 &p,其值为 0x98f2fc58,然后根据该值找到指针变量 p,其值为 0x98f2fc58,然后根据该地址,找到该指针变量指向的值,也就是 *p 的值 0x98f2fc58。这边最有意思的是,p == &p*p == p,所以 p == &p==*p (和 p == &a,那么 *p == a 其实一样的)

// int *p = (int *)0x98f2fc28;  直接硬编码指针变量的值  

int *p; // 定义一个指针,指针的值
p = &p;

printf("%#x", &p);    // 0x98f2fc28
printf("\n%#x",p);    // 0x98f2fc28
printf("\n%#x",*p);   // 0x98f2fc28

野指针(一个指针变量,不知道它指哪去了),例如全局的指针变量指向了一个函数调用完毕后就会销毁的变量地址,之后这个全局的指针变量指向哪是完全未知的

int *dangerous_pointer;

void DangeousPointer()
{  
  int *a;

  dangerous_pointer = &a;
}

上述代码在函数 DangeousPointer() 中加一句 dangerous_pointer = NULL; 即可,NULL 为空指针

总结:

  1. 不要将硬编码赋值给指针
  2. 空指针NULL的用法
  3. 避免产生野指针

clion64_M6Fu44x2y1.png

指针的运算

指针的加法,看一个 demo,指针 +1 是每次移动一个指针数据类型的大小,比如 int * 移动4个字节,double * 移动8个字节

int a = 2;
int *p = &a;

printf("%d\n",p);    // 250607300
printf("%d\n",p+1);  // 250607304
printf("%d\n", sizeof(int));  // 4

printf("\n");

double b = 3.0;
double *po = &b;
double **poi = &po;
printf("%d\n",poi);    // 250607368
printf("%d\n",poi+1);  // 250607376
printf("%d\n", sizeof(double *));  // 8

讲解数组 array 和指针的联系,array 指针就像是不可更改的指针变量。注意 3[array] 也可以使用,只不过可读性太差

{
    int array[] = {0,1,2,3,4};
    int *p = array;
    printf("%d, ",*(p+3));      // 3
    printf("%d, ",*(3+p));      // 3
    printf("%d, ",p[3]);        // 3
    printf("%d, ",3[p]);        // 3
    printf("%d, ",*(array+3));  // 3
    printf("%d, ",3[array]);    // 3
}

指针变量比大小(比较的是指针变量的值,其实就是地址高低了),小 demo,注意只限于连续的内存之中

int result = (p+3)>(p+1)?1:0;
int result2 = (p+3)==(p+3)?1:0;
printf("\n%d", result);    // 1
printf("\n%d", result2);   // 1

左值和右值

int main(){

  int a;
  a = 2; // a是左值,2是右值

  int *p = &a; // p是左值,&a是右值

  int b = *p; // b是左值,*p是右值

  // 左值是内存空间,右值是内存空间里的值
  int array[] = {0};
  int *pp = array;

  *pp++ = 5; // ctrl + W 的妙用,IDE 自动展开寻找优先级,此处光标展开先是 pp++,然后是 *pp++

  return 0;
}

指针参数作为返回值

调用函数,使用函数返回值的流程:函数的返回值是一开始是存储在函数栈中的,然后复制到寄存器;在其他地方调用函数,使用函数的返回值时,会把寄存器中的函数返回值复制到到函数栈中进行使用,共完成两次拷贝​。

int SumIntArray(int array[], int length){
  int sum = 0;
  for (int i = 0; i < length; ++i) {
    sum += array[i];
  }
  return sum;
}

int main(){
  int array[5]={0,1,2,3,4};
  int b = SumIntArray(array, 5);
  printf("%d", b);
  return 0;
}

这个 demo 中,SumIntArray(array, 5) 的结果 sum 的值,就是先从函数栈拷贝到 eax 寄存器中,然后再从 eax 寄存器拷贝到变量 b 所在的函数栈地址中。

第一次拷贝:函数栈中的函数返回值拷贝到寄存器中。

return sum;
mov     eax, DWORD PTR [rbp-4]

第二次拷贝:函数返回值从寄存器拷贝至函数栈中。

int b = SumIntArray(array, 5);
lea     rax, [rbp-32]
mov     esi, 5
mov     rdi, rax
call    SumIntArray
mov     DWORD PTR [rbp-4], eax

之所以会发生两次拷贝,是因为指向函数栈的指针的值是变化的( [rbp-4] 的值是变化的 ),所以要将函数返回值暂存到寄存器中,等到要使用的时候再调用寄存器使用。

在 mingw 编译器下,通过 GDB 可以查看寄存器的值,这里查看 rbp 寄存器值的变化。

clion64_UX9ReljrFu.png

clion64_LreSqwDGRa.png

所以要用 eax 寄存器暂存一下函数的返回值,因为 ebp 值会变。

寄存器 EDX 和 RDX 的关系,RDX 的低32位就是 EDX,同样 EAX 是 RAX 的低32位,低32位清0的情况下,高32位也会清0,即 EDX 初始化等同 RDX 初始化。

结构体返回值:当函数返回值过大,一个寄存器存不下该怎么办?使用多个寄存器进行存储。

第一次拷贝

typedef struct{
    char *name;
    int gender;
    int age;
    char *school_name;
}Student;

Student TestStruct(){
    Student student = {"like", 1, 23,"SZ"};
    return student;
}
//return student;
mov     rcx, QWORD PTR [rbp-40]
mov     rax, QWORD PTR [rbp-32]
mov     rdx, QWORD PTR [rbp-24]
mov     QWORD PTR [rcx], rax
mov     QWORD PTR [rcx+8], rdx
mov     rax, QWORD PTR [rbp-16]
mov     QWORD PTR [rcx+16], rax

第二次拷贝

Student student = TestStruct();
lea     rax, [rbp-32]
mov     rdi, rax
mov     eax, 0
call    TestStruct
mov     eax, 0

将想返回的值,它的指针变量作为参数传入到函数中,避免发生两次拷贝,现在只发生了一次拷贝,从函数栈拷贝到寄存器中,没有后面那次寄存器拷贝至函数栈了。

//*sum += array[i];
mov     rax, QWORD PTR [rbp-40]
mov     edx, DWORD PTR [rax]
mov     eax, DWORD PTR [rbp-4]
cdqe
lea     rcx, [0+rax*4]
mov     rax, QWORD PTR [rbp-24]
add     rax, rcx
mov     eax, DWORD PTR [rax]
add     edx, eax
mov     rax, QWORD PTR [rbp-40]
mov     DWORD PTR [rax], edx

使用指针参数作为返回值的好处:
1. 避免函数返回值带来的开销
2. 实现函数多个返回值的目的

动态内存分配

malloc

直接创建数组一般存储在函数调用栈上,而使用 malloc 分配内存从而进行存储的数据则是存放在堆区;堆区的数据与函数调用栈上的数据不同,堆区数据在函数释放后也不会释放内存,需要用 free() 主动释放

#include <stdio.h>
#include <stdlib.h>
#define PLAYER_COUNT 10

int main()
{
  int *players = malloc(PLAYER_COUNT * sizeof(int));
  for (int i = 0; i < PLAYER_COUNT; ++i)
  {
    players[i] = i;
  }
  for (int i = 0; i < PLAYER_COUNT; ++i)
  {
    printf("%d ", players[i]);
  }
  free(players);    // 释放内存
  return 0;
}

函数参数应为二维指针

free() 释放后的内存块对存在其中的值无影响,还会保留在内存块中,所以在调用 malloc 时,需要对新来的内存块上的值进行初始化操作(初始化操作中的二维指针,实质上就是在函数内部修改外部的指针变量)

#include <stdio.h>
#include <stdlib.h>
#define PLAYER_COUNT 10

void InitPointer(int **ptr, int length, int default_value)
{
  *ptr = malloc(length * sizeof(int));
  for (int i = 0; i < length; ++i) 
  {
    (*ptr)[i] = default_value;
  }
}
int main()
{
  // 和向函数传递想要修改的参数值的情况相同,必须将变量的地址传入函数才能进行修改,此处就是指针变量的地址传入InitPointer函数,进而修改指针变量的值
  int *players;
  InitPointer(&players, PLAYER_COUNT, 2);
  for (int i = 0; i < PLAYER_COUNT * 2; ++i)
  {
    printf("%d ", players[i]);
  }
  free(players);
  return 0;
}

calloc

函数 callocmalloc 不同,calloc 返回的内存会自动被清为 0,相当于做了一个预处理工作,不用自己做。

players = calloc(PLAYER_COUNT, sizeof(int));

realloc

realloc 函数,对原先的内存(注意,是在原有的内存基础上进行分配的)进行重新分配并增加或减少部分内存(以旧换新的感觉),但注意,realloc 新加的内存不会做清空操作,里面的值不能确定。

int *players;
InitPointer(&players, PLAYER_COUNT, 2);

// 函数 realloc 的用法
players = realloc(players, 4*sizeof(int));    // 重新分配四块内存
for (int i = 0; i < PLAYER_COUNT; ++i)
  printf("%d ", players[i]);    // 2 2 2 2 -842150451 ... ,只能确定前四个值,后面的值无法确定,因为是在原有空间的基础上重新分配了四块内存
printf("\n");
players = realloc(players, PLAYER_COUNT * 2 * sizeof(int));  // 重新分配20块内存
for (int i = 0; i < PLAYER_COUNT * 2; ++i) 
  printf("%d ", players[i]);  // 2 2 2 2 -842150451 -842150451 -842150451 ... 后面一直都是 -842150451,这个数字是什么含义?
free(players);

分配内存之后...

内存分配可能会失败,所以在使用 malloc calloc realloc 函数后,需要判断指针是否分配到了内存。

if(players)
{

  ...  // 指针指向的地方有内存才有意义

} else
{

  ...

}

注意事项

常见指针使用错误:
1. 忘记在使用指针完毕后释放内存
2. 使用了已经释放的内存(指针指向的内存被释放,但是指针变量并没有被销毁,还是可以继续使用的,如果在 free() 后再次使用该指针变量,就是这种情况,这种指针就是野指针)
3. 使用了超出边界的内存(类似数组越界)
4. 改变了内存的指针,导致无法正常释放(一个指针变量指向一块内存,如果这时将指针变量的值修改,使其指向其他内存,那原有的那块内存就会无法正常释放,因为没有指针变量指向它)

动态内存使用建议:
1. 避免修改指向已分配内存的指针(对应 4)
2. 对于 free() 后的指针主动置为 NULL(对应 2)
3. 避免将过多的指针指向动态分配的内存(一旦其中一个指针使用 free() 操作,其4. 动态内存遵从谁分配谁释放的原则

函数指针与typedef

函数存在地址,能否打印现有的函数地址进行查看?

#include <stdio.h>

void InitPointer(int **ptr, int length, int default_value){
  *ptr = malloc(length * sizeof(int));
  for (int i = 0; i < length; ++i) {
    (*ptr)[i] = default_value;
  }
}

int main(){

  printf("main: %#x\n", main);    // main: 0x1ffa10e6
  printf("&main: %#x\n", &main);  // &main: 0x1ffa10e6

  printf("pointer: %#x\n", InitPointer);    // pointer: 0x1ffa118b
  printf("&pointer: %#x\n", &InitPointer);  // &pointer: 0x1ffa118b

  return 0;
}

函数有地址,那么能否将函数地址存入给指针变量呢?

答案是可以,那么这个存储函数地址的指针变量的类型应该取什么类型呢?(类比普通变量的地址存入到指针变量中的操作,思考普通变量与指针变量的关系)

int a;
int *p;    // 观察两者的区别,数据类型不变,(变量名)改换为(* + 变量名)

// void InitPointer(int **ptr, int length, int default_value)

// 对比上面进行改写,数据类型 void + * + 指针变量名 + 函数的参数
void (*func)(int **ptr, int length, int default_value) = &InitPointer;  // 注意加括号(*func)

如何使用函数指针?

就像使用函数一样去使用它,指针变量看做是函数名

func(&p, 5, 1);    // 等同于 InitPointer(&p, 5, 1);
InitPointer(&p, 5, 1);

(*func)(&p, 5, 1);
(*InitPointer)(&p, 5, 1);  // 注意,这样也能调用

for (int i = 0; i < 5; ++i) {
  printf("%d ", p[i]);    // 1 1 1 1 1
}

free(p); // p是动态分配的内存,别忘记要 free()

观察上面的代码,四种方式都可以正常调用函数,那么函数指针和函数名是什么关系呢?

(*InitPointer)(&p, 5, 1); 可以成功执行,说明函数名是一个指针变量,所以才可以解引用,而且函数名存储的是函数的地址。类比数组进行思考,数组的名称是数组的首地址,那么函数的名称是函数的首地址就非常自然了。

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

printf("a: %#x\n", a);    // a: 0x606ffc68
printf("&a: %#x\n", &a);  // &a: 0x606ffc68
printf("p: %#x\n", p);    // p: 0x606ffc68

分辨是函数还是函数类型的指针

// 1
int *f1(int, double); // == int *(f1(int, double)), f1 函数, 返回值是指针

// 2
int (*f2)(int, double); // 函数指针,返回值是 int

// 3
int *(*f3)(int, double); // 函数指针,返回值是 int*

// 4 这里实质上是一个函数指针,不过返回值是数组,但C语言不支持返回值是数组,所以会报错
// 写成刚才那样看不出返回值,如果写成这样呢:int[] (*f4)(int, double); 类似java的方式,是不是清晰多了
// 之所以把括号放在后面,是因为C定义数组的时候是 int a[] 而不是 int[] a,所以这里函数返回数组的时候,括号也要放在后面
// int (*f4)(int, double)[];  // 如果将 (*f4)(int, double) 看作一个整体,当成a,是不是 int a[];

// 5
// int (*f5)[](int, double);  // 把(*f5)看作f,这里就变换为 int f[](int, double),其实是个函数数组,但这在C语言中是不允许的

// 6 注意,[] 优先级比 * 优先级要高
int (*f6[])(int, double);    // == int (*(f6[]))(int, double); 注意结合顺序,f6先和[]结合
// 首先这是一个 f6 数组,然后是这是一个什么类型的数组呢?其实是一个指针类型的 f6 数组;然后这个指针是什么类型呢?其实是一个函数类型的指针;所以合起来就是函数指针的数组,每一个元素都是一个函数指针

typedef + 普通的变量,如何为其取别名并应用

typedef int *IntPtr;    // 将 int* 取别名为 IntPtr
int *p1; 
IntPtr p2;

typedef int IntArray[];     // typedef int[] IntArray; 这样写虽然不合法,但是不是清楚了许多,为创建数组取个别名,叫 IntArray
int array1[] = {1,2,3,4};
IntArray array2 = {1,2,3,4};

大变に气分がいい