C 语言指针

指针基础

  1. 打印变量的地址,以 16 进制显示
    int a;
    scanf("%d", &a); // 20
    
    int *p = &a;
    printf("%#x",p); // 0x1bdffc04
    
  2. 打印出指针类型的空间大小
    printf("%d", sizeof(int *));  // MSVC 64 位编译器是 8 个字节大小,32 位编译器是 4 个字节大小
    
  3. 多层次的指针,通常称为“指针套娃”,需要仔细管理。

    例如,当你有一个整型变量 a 和一个指向它的指针 p,随后创建一个指向 p 的指针 pp,这就形成了一个双层指针结构。在这个结构中,pp 存储的是 p 的地址。

    int a = 1;
    int *p = &a;
    int **pp = &p;    // 指针变量的地址存入到另一个指针变量中
    

    逻辑流程如下:

    1. 声明并初始化一个整型变量 a,然后获取它的地址:int a = 1; int *p = &a;

    2. 声明一个二级指针 pp,并将其初始化为 p 的地址: int **pp = &p;

    3. 通过解引用 pp,我们可以访问 p,然后再通过 p 访问 a 的值。这个过程可以表示为: int value = *((*pp));int value = *((*pp));

      这里 *(*pp) 首先解引用 pp 得到 p 的值,即 a 的地址,然后再次解引用得到 a 的值。

    4. &pp 表示 pp 本身的地址,这与通过 pp 访问的 p 的地址是不同的。在实际编程中,通常不需要使用 &pp,除非在某些特定的上下文中需要获取一个指针变量本身的地址。

    QQ20240710-183822.png

只读指针变量和(指向)只读变量(的)指针

const 用于指针时,它的位置决定了它修饰的数据类型。const 位于星号 * 的左侧时,修饰的是指针指向的数据;位于星号右侧时,修饰的是指针本身。

理解 const 修饰指针的规则,可以采用 “从后向前看” 的方法。即从变量名开始,向前找到最近的 const 关键字,它就修饰紧挨着它的数据类型。

  • int *constconst 修饰 int *,即指针本身是常量。
  • const int *const 修饰 int,即指针指向的数据是常量。
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

特殊指针

指针指向自己

在 C 语言中,一个指针可以被赋值为自己的地址,这种指针被称为自引用指针。这种情况下,指针的值就是它自己的内存地址。例如,如果我们有一个指针 p,并且执行 p = &p,那么 p 就指向了自己的内存地址。

逻辑如下:

  1. 首先获取指针 p 的地址,使用 &p

  2. 然后将这个地址赋值给 p 本身,即 p = &p

在这种情况下,p 的值是它自己的地址,所以 p&p 实际上是相等的。但是,*p 应该是 p 所指向的地址的值,而不是 p 本身的地址。如果我们声明了一个指针 p 并让它指向自己,那么 *p 的值应该是一个未定义的或者是一个随机的值,因为 p 的地址可能没有被初始化为一个有效的内存地址。

因此,表达式 p == &p 是正确的,因为 p 被赋值为它自己的地址。但是,*p == p 通常是不正确的,因为 *p 应该是 p 所指向地址的值,而不是地址本身。

另外,如果 p 指向一个变量 a,即 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

野指针

在 C 语言中,一个 “野指针” 是指一个指针变量,它可能没有被正确初始化,或者它指向的内存地址已经不再有效。这种情况可能发生在多种场景下,比如一个全局指针变量被赋值为一个局部变量的地址。当局部变量的生命周期结束,它所占用的内存可能被系统回收或重新分配给其他变量,此时全局指针变量就变成了 “野指针”,因为它指向的内存内容是不确定的。

int *dangerous_pointer;

void DangeousPointer() {
  int *a;
  dangerous_pointer = &a;
}

空指针

在 C 语言中,NULL 是一个特殊的宏定义,它被用来表示一个空指针。空指针不指向任何有效的内存地址,常用于初始化指针变量,表示它们尚未指向一个具体的数据对象。

NULL 通常被定义为 (void *)0,这意味着它是一个指向 void 类型的指针,并且其值为 0。它可以用来初始化任何类型的指针变量,因为 C 语言允许指针类型之间的隐式转换。

当一个指针变量被赋值为 NULL,它的值就是 0。在内存中,这表示它没有指向任何有效的内存地址。

由于 NULL 指针没有指向任何内存地址,所以它指向的值是未定义的,尝试解引用 NULL 指针(即访问它所指向的内存位置)将导致未定义行为,通常是程序崩溃。

NULL 本身不是一个变量,它没有地址。NULL 是一个常量,它代表了一个指针的值,即 0。当你尝试获取 NULL 的地址,即 &NULL,这是不合法的,因为 NULL 不是一个对象,没有存储在内存中的地址。

在 3.2 的例子中,我们可以在函数 DangeousPointer() 中加一句 dangerous_pointer = NULL; 避免产生野指针。

指针的运算

指针的加法

在 C 语言中,指针加法是一种基本操作,它允许我们将指针向前或向后移动特定的字节数。这种移动的步长取决于指针指向的数据类型的大小。例如,如果我们有一个指向 int 的指针并对其执行加法操作(如 int *ptr; ptr++;),指针 ptr 将移动到下一个 int 变量的地址,这通常意味着向前移动了四个字节(在 32 位系统中)。同样,如果指针是指向 double 的(如 double *dp; dp++;),执行加法操作将使指针向前移动八个字节,因为 double 类型通常占用八个字节。

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

数组和指针的联系

在 C 语言中,数组名 array 在大多数情况下都被用作指向数组首元素的指针。实际上,数组名在很多语境下会被编译器视为一个指向数组第一个元素的常量指针,这意味着一旦定义,它就不能再被指向其他地址。然而,这并不影响我们通过数组名访问数组元素或进行遍历。

值得注意的是,尽管语法 array[3]3[array] 在 C 语言中都是合法的,它们都访问数组的第四个元素(假设数组索引从 0 开始),但后者的可读性较差,因为它违反了常规的数组访问习惯。通常,我们使用方括号 [] 来包围索引,以明确表示我们正在访问数组中的特定位置。

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
printf("%d, ", array[3]);        // 3,推荐写法

指针变量比大小

在 C 语言中,指针变量之间的比较操作是基于它们所指向的内存地址进行的。当我们比较两个指针变量时,实际上是在比较它们各自存储的内存地址值。这意味着,如果指针 p1p2 分别存储了不同的地址,p1 > p2 的比较结果将取决于 p1 的地址值是否大于 p2 的地址值。

只有当指针指向相同的数组或内存区域时,它们的比较才有意义。比较指向不同内存区域的指针(如不同的数组或动态分配的内存块)可能会导致未定义的结果或错误。

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

左值和右值

在 C 语言的赋值语句中,等号左边通常是代表一个内存位置的变量,而等号右边则是一个表达式,其计算结果将被赋值给左侧的变量。这意味着左侧必须是一个可以存储值的有效内存地址,如一个变量或一个数组元素。右侧可以是任何能够产生值的表达式,包括常量、变量、函数调用或其他算术或逻辑操作的结果。

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;
}

指针参数作为返回值

使用函数返回值的流程

在 C 语言中,调用函数并处理其返回值的过程遵循特定的调用约定。当一个函数被调用时,其参数通常首先被推入调用栈中,这是通过将参数复制到函数栈帧的特定位置来实现的。然后,函数开始执行。

对于返回值,大多数 C 编译器使用寄存器来存储小的数据类型的返回值,如整数或指针。当函数执行完毕并准备返回时,它的返回值会被放置在特定的寄存器中。如果返回值是一个复合数据类型或者大小超过了寄存器能够存储的范围,它可能会被存储在堆栈上,或者通过引用参数来传递。

当控制权返回到调用者时,调用者可以通过读取寄存器中的值或从堆栈中获取返回值来使用函数的返回结果。对于小数据类型,通常只需要从寄存器中读取返回值,而对于大数据类型,则可能需要进行一次内存拷贝操作。

并非所有情况下都会发生两次拷贝。实际上,返回值的传递机制取决于编译器的优化和函数返回类型的具体情况。

#include <stdio.h>

int SumIntArray(int array[], int length) {
    int sum = 0;
    for (int i = 0; i < length; ++i) {
        sum += array[i];
    }
    // 第一次拷贝,函数栈中的函数返回值拷贝到寄存器中
    // mov  eax, DWORD PTR [rbp-4]    # _10, sum
    return sum; 
}

int main() {
    int array[5] = {0, 1, 2, 3, 4};
    // 第二次拷贝,函数返回值从寄存器拷贝至函数栈中
    //  mov DWORD PTR [rbp-4], eax    # b, tmp85
    int b = SumIntArray(array, 5); 
    printf("%d", b);
    return 0;
}

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

MinGW 编译器下,通过 GDB 可以查看寄存器的值:

# 第一次拷贝
(gdb) info registers rbp
rbp            0x5ffe10            0x5ffe10
# 第二次拷贝(gdb) info registers rbp
rbp            0x5ffe60            0x5ffe60

大数返回值的处理

// TestBigValue:
// push    rbp
// mov     rbp, rsp
__int128 TestBigValue() {
    // mov      eax, 0
    // mov      edx, 0
    return 0;
// pop     rbp
// ret
}

// main:
// push    rbp
// mov     rbp, rsp
// sub     rsp, 16
int main() {
    //  mov     eax, 0
    //  call    TestBigValue
    //  mov     QWORD PTR [rbp-16], rax
    //  mov     QWORD PTR [rbp-8], rdx
    __int128 big_int = TestBigValue();
    //  mov     eax, 0
    return 0;
    //  leave
    //  ret
}

这里使用了两个寄存器进行存储。

#include <stdio.h>

typedef struct {
    char *name_;
    int gender_;
    int age_;
    char *school_name_;
} Student;

// TestStruct:
// push    rbp
// mov     rbp, rsp
// mov     QWORD PTR [rbp-40], rdi
Student TestStruct() {
    //  mov     QWORD PTR [rbp-32], OFFSET FLAT:.LC0    # student.names
    //  mov     DWORD PTR [rbp-24], 1                   # student.gender
    //  mov     DWORD PTR [rbp-20], 17                  # student.age
    //  mov     QWORD PTR [rbp-16], OFFSET FLAT:.LC1    # student.school_name
    Student student = {"Sakurakouji Runa", 1, 18, "Firia joshi daigaku"};
    //  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
    return student;
// mov     rax, QWORD PTR [rbp-40]
// pop     rbp
// ret
}

// main:
// push    rbp
// mov     rbp, rsp
// sub     rsp, 32
int main() {
    // lea     rax, [rbp-32]
    // mov     rdi, rax
    // mov     eax, 0
    // call    TestStruct
    Student student = TestStruct();
    // mov     eax, 0
    return 0;
// leave
// ret
}

这里使用了多个寄存器进行存储。

为了减少在函数调用过程中的数据拷贝,特别是在返回大型数据结构时,可以采用一种优化技术:将用于接收返回值的指针作为参数传递给函数。这样,函数可以直接在调用者提供的内存地址上构建或修改数据,从而避免了数据的两次拷贝。

具体来说,第一次拷贝发生在将参数传递到函数的栈帧时。然后,函数执行过程中,计算结果直接存储到由指针参数指定的内存位置,通常是调用者栈帧中的一个变量。这种方法只涉及到一次数据拷贝 —— 从函数的参数栈到寄存器,然后直接写入目标内存地址,省略了将返回值从寄存器拷贝回调用者栈帧的步骤。

// SumIntArray:
// push    rbp
// mov     rbp, rsp
// mov     QWORD PTR [rbp-24], rdi
// mov     DWORD PTR [rbp-28], esi
int SumIntArray(int array[], int length) {
  //  mov     DWORD PTR [rbp-4], 0
  int sum = 0;
  //  mov     DWORD PTR [rbp-8], 0
  for (int i = 0; i < length; ++i) {
    //  jmp     .L2
    // .L3
    //  mov     eax, DWORD PTR [rbp-8]
    //  cdqe
    //  lea     rdx, [0+rax*4]
    //  mov     rax, QWORD PTR [rbp-24]
    //  add     rax, rdx
    //  mov     eax, DWORD PTR [rax]
    //  add     DWORD PTR [rbp-4], eax
    sum += array[i];
    //  add     DWORD PTR [rbp-8], 1
    //  .L2:
    //  mov     eax, DWORD PTR [rbp-8]
    //  cmp     eax, DWORD PTR [rbp-28]
    //  jl      .L3
  }
  // mov     eax, DWORD PTR [rbp-4]
  return sum;
}
// pop     rbp
// ret
// SumIntArray:
// push    rbp
// mov     rbp, rsp
// mov     QWORD PTR [rbp-24], rdi
// mov     DWORD PTR [rbp-28], esi
// mov     QWORD PTR [rbp-40], rdx
void SumIntArray2(int array[], int length, int *sum) {
    //  mov     rax, QWORD PTR [rbp-40]
    //  mov     DWORD PTR [rax], 0
    *sum = 0;
    //  mov     DWORD PTR [rbp-4], 0
    //  jmp     .L2
    for (int i = 0; i < length; ++i) {
        //  .L3:
        //  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
        *sum += array[i];
        // add     DWORD PTR [rbp-4], 1
        //  .L2:
        //  mov     eax, DWORD PTR [rbp-4]
        //  cmp     eax, DWORD PTR [rbp-28]
        //  jl      .L3
    }
    //  nop
    //  nop
    //  pop     rbp
    //  ret
}

使用指针参数作为返回值的好处:

  1. 避免函数返回值带来的开销
  2. 实现函数多个返回值的目的

动态内存分配

malloc

在 C 语言编程中,使用 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;
}

函数参数应为二维指针

在 C 语言中,若需在函数间传递二维数组,应使用二维指针作为参数。例如,声明一个函数参数为 int **array,允许函数访问和修改多维数组的内容。此外,当使用 free () 函数释放内存后,内存块中原有的数据并不会被清除,它们依然保留在内存中。这意味着,如果再次使用 malloc 为相同大小的数组分配内存,新内存块中将包含之前释放的内存块的数据。因此,在使用 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

calloc 函数是 C 语言中用于动态内存分配的另一种工具,它在功能上与 malloc 相似,但有一个关键的区别:calloc 在分配内存后会自动将所有字节初始化为零。这意味着当你使用 calloc 来分配内存时,得到的内存块中的每个字节都被预设为 0 值,省去了手动初始化的步骤。

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

realloc

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);

分配内存之后

在 C 语言编程中,动态内存分配是一个常见的操作,但这个过程并不总是成功的。当调用 malloccallocrealloc 函数时,可能会由于多种原因(如内存不足)导致分配失败。因此,在这些函数调用之后,应该立即检查返回的指针是否为 NULL,这是一个良好的编程实践。

如果指针是 NULL,这意味着内存分配未能成功,程序应该采取适当的错误处理措施,例如释放已分配的内存(如果调用的是 realloc),并可能需要记录错误或终止程序。

if (players) {
    ...  // 指针指向的地方有内存才有意义
} else {
    ...
}

注意事项

常见指针使用错误:

  1. 忘记释放内存:使用 malloccallocrealloc 分配的内存,如果不再需要,应该使用 free() 函数及时释放,以避免内存泄漏。
  2. 使用已释放的内存:释放内存后,如果没有将指针设置为 NULL,再次使用该指针可能导致野指针错误。
  3. 内存越界:访问数组或内存块的边界之外,可能导致未定义行为或程序崩溃。
  4. 指针丢失:改变指针的值,使其指向其他内存地址,可能导致原始内存无法释放,因为没有任何指针指向它。

改进建议:

  1. 保持指针稳定:一旦分配了内存,避免修改指针的值,直到内存被释放。
  2. 释放后归零:释放内存后,应将指针显式设置为 NULL,以避免悬垂指针问题。
  3. 避免多指针管理:尽量不要让多个指针指向同一块动态分配的内存,这可能导致释放内存时的混乱。
  4. 遵守释放原则:坚持“谁分配,谁释放”的原则,确保内存管理的清晰和一致性。

函数指针与 typedef

打印现有的函数地址

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

函数地址存入指针变量

在 C 语言中,函数名本身就表示了函数的入口地址,这意味着我们可以将函数的地址赋给一个指针变量,这种指针称为函数指针。为了正确地声明一个函数指针,我们需要确保指针的类型与它所指向的函数的签名(返回类型和参数列表)一致。

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

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

// 对比上面进行改写,数据类型 void + * + 指针变量名 + 函数的参数

使用函数指针

函数指针在 C 语言中允许我们将函数的执行能力 “存储” 在一个变量中。使用函数指针就像使用普通函数一样,但首先需要声明一个正确类型的指针变量,使其指向特定的函数。

一旦函数指针被正确初始化,就可以通过以下几种方式调用它指向的函数:

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()/ &pointer: 0x1ffa118b
  1. 直接使用函数指针调用:func (&p, 5, 1); 这里 func 是指向函数的指针。

  2. 使用函数名调用:InitPointer (&p, 5, 1); 这里 InitPointer 是函数名。

  3. 通过解引用函数指针调用:(*func)(&p, 5, 1); 这里通过解引用 func 来调用函数。

  4. 通过解引用函数名调用:(*InitPointer)(&p, 5, 1); 这种方式较少使用,但语法上是正确的。这种用法与数组名类似,数组名是指向数组第一个元素的指针常量。相应地,函数名在大多数表达式中表现得就像指向函数的指针常量。当我们对函数名进行解引用时,我们实际上是在获取函数的入口地址,并直接调用该地址处的代码。

在上述示例中,p 是动态分配的内存,因此在不再需要时,应使用 free (p); 来释放内存。

函数指针与函数名的关系在于,函数名在大多数情况下被编译器视为指向该函数的指针常量。因此,当我们将函数名用于调用时,编译器实际上在背后使用了一个隐式的函数指针。函数指针提供了这种隐式行为的显式形式,允许程序员在运行时动态地改变函数的行为。

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};
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇