C语言进阶

2019-07-27  本文已影响0人  五分钟诗人

指针

指针也是变量,在C语言中扮演者重要的角色。有许多使用指针的理由,比如:

什么是指针?

指针可以说是一种简单的整型变量,存储着某个值的内存地址,而不是存储值本身。

计算机的内存是一系列序列化的数据,一个指针指向内存特定的部分。我们的程序可以使用指针来指向一个很大的内存,这取决于我们怎么看待要存的数据。

字符串指针

我们已经讨论了string,但现在我们可以更加深入一些,了解C中的string实际上是什么。

下面这行代码:

char * name = "John";

做了两件事情:

如果我们尝试将name作为数组来访问,这也是可以的,name是字符J,因为它存放着字符串John的首个字符的地址。

既然我们已经知道内存单元是有顺序的,这意味着我们可以像数组一样来访问string的每一个字符,直到到达最后一个终止字符\0

解引用

解引用是指我们可以获取一个指针变量的实际值,而不只是其内存地址。其实,之前我们已经在数组中使用了解引用,只是我们不知道而已。比如,中括号[0]操作符,访问数组中的第一个元素。并且,数组实际上是指针,访问第一个元素相当于解引用。相应地,数组的名字存放的是数组的第一个元素的内存地址。我们可以使用星号(*)来解引用。

如果我们想创建一个数组指向存放在栈中的不同元素,我们可以这样写:

/* define a local variable a */
int a = 1;

/* define a pointer variable, and point it to a using the & operator */
int * pointer_to_a = &a;

printf("The value a is %d\n", a);
printf("The value of a is also %d\n", *pointer_to_a);

注意:我们使用&操作符来指向变量a

然后我们使用解引用操作符来获取指针变量的内容;我们也可以改变所引用的内容。

int a = 1;
int * pointer_to_a = &a;

/* let's change the variable a */
a += 1;

/* we just changed the variable again! */
*pointer_to_a += 1;

/* will print out 3 */
printf("The value of a is now %d\n", a);

结构体

使用

C中的结构体是一种特殊的、大型的变量,可以存储不同类型的各种变量。结构体是C中类和对象的基础,结构体用来:

结构体中最简单的例子就是point了,point包含了两个变量xy,分别是点的横纵坐标。让我们定义point

struct point {
    int x;
    int y;
};

现在,我们来定义每一个点并使用它。假设函数draw接收一个点的xy坐标,并将其显示在屏幕上。如果没有struct,我们就必须指定两个参数,每个坐标需要一个。

/* draws a point at 10, 5 */
int x = 10;
int y = 5;
draw(x, y);

使用struct,我们可以使用指针来传递参数:

/* draws a point at 10, 5 */
struct point p;
p.x = 10;
p.y = 5;
draw(p);

为了访问point的内部变量,我们使用.操作符。

typedef

typedef允许我们将类型重命名,这样,我们就可以摆脱很长的struct类型的定义。typedef定义结构体示例如下:

typedef struct {
    int x;
    int y;
} point;

这允许我们像这样定义一个point

point p;

结构体内部也可以包含指针,也就能够包含string,或者包含其他结构体,这就是struct的魅力所在。比如,我们如下定义vehicle结构体:

typedef struct {
    char * brand;
    int model;
} vehicle;

因为brand是一个字符指针,vehicle类型可以包含string

vehicle mycar;
mycar.brand = "Ford";
mycar.model = 2007;

函数按照引用传递

含义

如果函数按值传递,那么实际参数只是原来的值的一份拷贝。但如果我们拷贝引用而不是值呢?这就能够让我们控制传来的参数,相应地,对原值的修改也会生效。

让我们实现一个函数addone,将一个值加一,按照以下方式写则不会生效。

void addone(int n) {
    n++;
}

int n;
printf("Before: %d\n", n);
addone(n);
printf("After: %d\n", n);

然而,这样却是可行的。

void addone(int * n) {
    (*n)++;
}

int n;
printf("Before: %d\n", n);
addone(&n);
printf("After: %d\n", n);

第二个版本的addone不同之处在于,它接收了一个指针变量n作为参数,然后直接操纵它,因为它知道n在内存中的地址。

注意:当调用addone函数时,我们我们必须传递一个引用的值,而不是变量本身。当函数知道了变量的地址,它就不会拷贝变量本身。

指向结构体的指针

让我们创建一个函数叫call,它使xy坐标都向正方向移动。注意,我们不是传递两个参数,而是只传递一个结构体的指针。

void move(point * p) {
    (*p).x++;
    (*p).y++;
}

然而,如果我们希望解引用结构体并访问其内部的成员时,我们有一种更加简明的语法,这种方法在结构体中很常用。我们可以像下面那样重写函数:

void move(point * p) {
    p->x++;
    p->y++;
}

动态内存分配

动态内存分配是C的一个重要主题。它允许创建诸如链表之类的复杂数据结构。动态分配内存允许我们无需在程序中指定变量的初始存储空间。

为了动态地分配一块内存,我们需要指定一个指针来存放新分配的内存的首地址。这样,我们就可以通过这个指针来访问这块内存,并且一旦我们不再需要这块内存,我们将使用同样的指针来释放它。

让我们假设我们想要分配一段person结构体的内存。person定义如下:

typedef struct {
    char * name;
    int age;
} person;

使用下面的语法,来分配一个新的person实例,并将地址传递给myperson变量。

person * myperson = malloc(sizeof(person));

这就告诉编译器我们想要动态地分配一段内寻,只是用来存放person这个结构体,然会返回一个指向该结构体的指针。

我们可以使用->表示法来访问person的成员。

myperson->name = "John";
myperson->age = 27;

在我们使用完所分配的struct变量之后,我们可以使用free来释放那段内存。

free(myperson);

注意:free并不删除myperson本身,它删除的是myperson指向的变量所在的内存占用。myperson变量仍然指向那段内存,我们不再使用那个内存区域了,并且我们应该确保我们不会再使用那段内存区域,不然可能会有问题,因为那段区域可能已经是其他数据了。

数组和指针

在先前指针的章节中,你已经知道了指针可以用来存放变量的地址,比如,在下面的代码中,pc存放字符变量c的地址。

char c = 'A';
char *pc = &c;

这里,c是一个标量,可以存放单一的值。然而,你已经熟悉了数组可以存放多个相同类型的值,并且在连续分配的内存空间中。所以,你可能会考虑,我们能不能使用指针?实际上,我们可以。

让我们从简单的代码开始看它输出什么。我们稍后讨论其每一个行为。

char vowels[] = {'A', 'E', 'I', 'O', 'U'};
char *pvowels = &vowels;
int i;

// Print the addresses
for (i = 0; i < 5; i++) {
    printf("&vowels[%d]: %u, pvowels + %d: %u, vowels + %d: %u\n", i, &vowels[i], i, pvowels + i, i, vowels + i);
}

// Print the values
for (i = 0; i < 5; i++) {
    printf("vowels[%d]: %c, *(pvowels + %d): %c, *(vowels + %d): %c\n", i, vowels[i], i, *(pvowels + i), i, *(vowels + i));
}

上面代码的典型输出如下:(不同机器会存在差别)

&vowels[0]: 4287605531, pvowels + 0: 4287605531, vowels + 0: 4287605531

&vowels[1]: 4287605532, pvowels + 1: 4287605532, vowels + 1: 4287605532

&vowels[2]: 4287605533, pvowels + 2: 4287605533, vowels + 2: 4287605533

&vowels[3]: 4287605534, pvowels + 3: 4287605534, vowels + 3: 4287605534

&vowels[4]: 4287605535, pvowels + 4: 4287605535, vowels + 4: 4287605535

vowels[0]: A, *(pvowels + 0): A, *(vowels + 0): A

vowels[1]: E, *(pvowels + 1): E, *(vowels + 1): E

vowels[2]: I, *(pvowels + 2): I, *(vowels + 2): I

vowels[3]: O, *(pvowels + 3): O, *(vowels + 3): O

vowels[4]: U, *(pvowels + 4): U, *(vowels + 4): U

你可能已经猜到了,&vowels[i]给出了vowels数组中的第i+1个元素,并且,因为是一个字符数组,每一个元素占一个字节,所以其内存区域就简单地以字节分隔。我们创建一个指针pvowels然后将数组的地址赋给它。pvowels+i是一个合法的操作,即使在正常的情况下这看起来没什么意义。特别地,在上面的输出中,&vowels[i]pvowels+i是相同的。所以在使用这种方法输出上不要有疑虑。

如果你仔细看一下之前的代码,你就会发现,我们还使用了另外一种更加奇怪的表示方法:vowels+i,这也是一样的。另一方面,pvowels+ivowels+i同样是返回数组vowels的第i+1个元素。为什么会这样?

这是因为数组本身也是一个常指针,指向数组的第一个元素。换句话说,vowels&vowels[0]、和vowels+0都指向同样的位置。

数组的动态内存分配

到现在为止,我们已经知道了可以使用指针来访问数组。除此之外,我们还可以使用块指针动态分配内存。这两个方面可以结合在一起,正如下面的代码所阐述的。

// Allocate memory to store five characters
int n = 5;
char *pvowels = (char *) malloc(n * sizeof(char));
int i;

pvowels[0] = 'A';
pvowels[1] = 'E';
*(pvowels + 2) = 'I';
pvowels[3] = 'O';
*(pvowels + 4) = 'U';

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

printf("\n");

free(pvowels);

在上面的代码中,我们分配了连续的5个字节去存储字符数据。我们使用数组下标遍历数组仿佛pvowels是一个数组。然后,注意pvowels实际上是一个指针。就像指针和数组一节所说的,数组和指针实际上都是一样的事情。

所以这有什么用?记住当定义数组的时候,数组的大小是必须知道的。但是,在一些场景下,我们并不知道数组的所需要的实际长度,我们可能就尽可能大地定义数组,这样会造成内存浪费。然而,使用动态分配内存的时候,我们可以将程序的内存按需分配。并且,不再使用的内存也可以使用free函数来释放。在不好的方面,如果使用了动态内存分配,我们就必须显式地调用free函数来释放内存,否则将有可能发生内存泄漏。

举一反三,我们可以按照同样的方式为二维数组动态分配内存,这也可以拓展到多维数组。不像一维数组,我们在这里需要使用指针的指针,正如下面的代码所示。

int nrows = 2;
int ncols = 5;
int i, j;

// Allocate memory for nrows pointers
char **pvowels = (char **) malloc(nrows * sizeof(char *));

// For each row, allocate memory for ncols elements
pvowels[0] = (char *) malloc(ncols * sizeof(char));
pvowels[1] = (char *) malloc(ncols * sizeof(char));

pvowels[0][0] = 'A';
pvowels[0][1] = 'E';
pvowels[0][2] = 'I';
pvowels[0][3] = 'O';
pvowels[0][4] = 'U';

pvowels[1][0] = 'a';
pvowels[1][1] = 'e';
pvowels[1][2] = 'i';
pvowels[1][3] = 'o';
pvowels[1][4] = 'u';

for (i = 0; i < nrows; i++) {
    for(j = 0; j < ncols; j++) {
        printf("%c ", pvowels[i][j]);
    }

    printf("\n");
}

// Free individual rows
free(pvowels[0]);
free(pvowels[1]);

// Free the top-level pointer
free(pvowels);

递归

函数调用自身时将发生递归现象。递归可以使代码更加整洁、优雅。但当递归树较深时,内存占用将很严重,时间复杂度也不佳。

使用递归的情况:

递归总是可以分为两个部分的。终止部分指示了递归到什么程度为止,而递归部分则解决子问题。

比如,下面的例子将使用递归加法来代替乘法:

#include <stdio.h>

unsigned int multiply(unsigned int x, unsigned int y)
{
    if (x == 1)
    {
        /* Terminating case */
        return y;
    }
    else if (x > 1)
    {
        /* Recursive step */
        return y + multiply(x-1, y);
    }

    /* Catch scenario when x is zero */
    return 0;
}

int main() {
    printf("3 times 5 is %d", multiply(3, 5));
    return 0;
}

链表

简介

链表是最好最简单的动态数据结构的例子,使用指针来实现。然而,理解指针对于理解链表如何工作是很关键的。

一般地,链表作为一种线性表,可以在链表的任何地方,按需增长和收缩。

然而,链表也有一些缺点:

什么是链表?

链表是一系列动态分配的节点的集合,每个节点都包含一个值与一个指针。指针总是指向链表的下一个成员,做后一个成员指向为Null

通常使用一个局部的指针变量来指向链表的第一个元素,如果指针为NULL,那就认为链表为空。

linked_list.jpeg

让我们来定义链表的节点:

typedef struct node {
    int val;
    struct node * next;
} node_t;

注意:我们使用struct来实现数据上的递归,即node内部的next依然是node类型。我们将节点命名为node_t

现在我们可以使用节点了。让我么创建一个局部变量指向链表的第一个节点。(叫head

node_t * head = NULL;
head = malloc(sizeof(node_t));
if (head == NULL) {
    return 1;
}

head->val = 1;
head->next = NULL;

我们刚才只是创建了链表的第一个变量,我们还必须设置它的值,并且其下一个节点应该为空,如果我们的链表只有这个元素的话。注意:我们总是检查malloc分配内存是否成功。

在链表的尾部增加一个节点,我们只需要更改next域指向这个新的节点。

node_t * head = NULL;
head = malloc(sizeof(node_t));
head->val = 1;
head->next = malloc(sizeof(node_t));
head->next->val = 2;
head->next->next = NULL;

我们可以按照这样的方式不断地增加元素,直到最后一个元素的next域为NULL

遍历链表

让我们创建一个打印链表所有节点的函数。使用current指针来追踪当前要打印的节点。在打印玩一个节点之后,将current的指针域指向下一个节点,然后再次打印,直到到达链表的结尾。(next域为空)

void print_list(node_t * head) {
    node_t * current = head;

    while (current != NULL) {
        printf("%d\n", current->val);
        current = current->next;
    }
}

在表尾增加元素

为了迭代整个链表,我们使用一个叫做current的指针。我们在每一次循环中都把它推向下一个节点,直到我们到达最后。然后我们就可以在current插入一个节点。

void push(node_t * head, int val) {
    node_t * current = head;
    while (current->next != NULL) {
        current = current->next;
    }

    /* now we can add a new variable */
    current->next = malloc(sizeof(node_t));
    current->next->val = val;
    current->next->next = NULL;
}

在表头增加元素

要在表头添加元素,我们只需要按照以下的步骤来做:

我们将使用函数来完成这个操作,为了修改表头的指针域,我们还需要传递一个指针的指针,这样才能修改next域本身。

void push(node_t ** head, int val) {
    node_t * new_node;
    new_node = malloc(sizeof(node_t));

    new_node->val = val;
    new_node->next = *head;
    *head = new_node;
}

移除表头元素

要移除变量,我们只需要进行反操作:

以下是代码:

int pop(node_t ** head) {
    int retval = -1;
    node_t * next_node = NULL;

    if (*head == NULL) {
        return -1;
    }

    next_node = (*head)->next;
    retval = (*head)->val;
    free(*head);
    *head = next_node;

    return retval;
}

移除表尾元素

移除链表的最后一个元素需要遍历整个链表,找到最后一个元素的地址,然后直接释放其内存空间,将上一个节点的next域修改为NULL。注意:要找到最后一个元素,我们还必须额外设置一个变量来检查是否到达倒数第二个,这是关键。

int remove_last(node_t * head) {
    int retval = 0;
    /* if there is only one item in the list, remove it */
    if (head->next == NULL) {
        retval = head->val;
        free(head);
        return retval;
    }

    /* get to the second to last node in the list */
    node_t * current = head;
    while (current->next->next != NULL) {
        current = current->next;
    }

    /* now current points to the second to last item of the list, so let's remove current->next */
    retval = current->next->val;
    free(current->next);
    current->next = NULL;
    return retval;

}

移除特定的元素

无论是通过索引还是节点值的方式给出要删除的节点,我们都必须遍历整个链表,找到我们想要删除的元素,然后删除它。移除的过程只需要修改指针域。

这是删除的算法:

以下是一些边界案例,在删除的时候需要注意。

int remove_by_index(node_t ** head, int n) {
    int i = 0;
    int retval = -1;
    node_t * current = *head;
    node_t * temp_node = NULL;

    if (n == 0) {
        return pop(head);
    }

    for (i = 0; i < n-1; i++) {
        if (current->next == NULL) {
            return -1;
        }
        current = current->next;
    }

    temp_node = current->next;
    retval = temp_node->val;
    current->next = temp_node->next;
    free(temp_node);

    return retval;

}

联合体

C中的联合体和结构体比较像,结构体中的每个变量都有自己的存储空间,但联合体是多个变量使用同一个内存空间,因此,联合体的大小取决与最大的变量的占用空间大小。

所以,如果你想以不同的方式读取一个变量,比如,读取一个整数的每一个字节,你可以这样做:

union intParts {
  int theInt;
  char bytes[sizeof(int)];
};

这样,你就可以看到一个int类型的每个字节的内容。

union intParts parts;
parts.theInt = 5968145; // arbitrary number > 255 (1 byte)

printf("The int is %i\nThe bytes are [%i, %i, %i, %i]\n",
parts.theInt, parts.bytes[0], parts.bytes[1], parts.bytes[2], parts.bytes[3]);

// vs

int theInt = parts.theInt;
printf("The int is %i\nThe bytes are [%i, %i, %i, %i]\n",
theInt, *((char*)&theInt+0), *((char*)&theInt+1), *((char*)&theInt+2), *((char*)&theInt+3));

// or with array syntax which can be a tiny bit nicer sometimes

printf("The int is %i\nThe bytes are [%i, %i, %i, %i]\n",
    theInt, ((char*)&theInt)[0], ((char*)&theInt)[1], ((char*)&theInt)[2], ((char*)&theInt)[3]);

比如,你可能有一个struct类型的数字,但你不想像下面那样使用:

struct operator {
    int intNum;
    float floatNum;
    int type;
    double doubleNum;
};

因为你的程序可能还有很多这样的变量,这种存储方式显然会浪费内存空间。所以,你可以这样做:

struct operator {
    int type;
    union {
      int intNum;
      float floatNum;
      double doubleNum;
    } types;
};

operator这个结构体现在的大小是type的大小再加上types联合体的最大的变量的大小。

用法:

operator op;
op.type = 0; // int, probably better as an enum or macro constant
op.types.intNum = 352;

而且,如果你不指定联合体的名字,你甚至可以直接通过结构体访问!

struct operator {
    int type;
    union {
        int intNum;
        float floatNum;
        double doubleNum;
    }; // no name!
};

operator op;
op.type = 0; // int
// intNum is part of the union, but since it's not named you access it directly off the struct itself
op.intNum = 352;

另外,一种可能更加有用的特征是,当你有很多和相同类型的变量的时候,你想同时使用名称(可读性)和索引(迭代需要),在这种情况下,你可以这样做:

union Coins {
    struct {
        int quarter;
        int dime;
        int nickel;
        int penny;
    }; // anonymous struct acts the same way as an anonymous union, members are on the outer container
    int coins[4];
};

联合体变量是共享内存的!

union Coins change;
for(int i = 0; i < sizeof(change) / sizeof(int); ++i)
{
    scanf("%i", change.coins + i); // BAD code! input is always suspect!
}
printf("There are %i quarters, %i dimes, %i nickels, and %i pennies\n",
    change.quarter, change.dime, change.nickel, change.penny);

指针运算

如前所述,指针其实是一个整型的变量,因此,可以对其进行基本的运算。

指针自增

就像任何变量一样,++操作符对那个变量执行增加1的操作。在这里指针变量的自增将会使指针指向下一个内存地址。结合数组的操作,我们来看一下是如何实现的。

#include <stdio.h>

int main()
{
    int intarray[5] = {10,20,30,40,50};

    int i;
    for(i = 0; i < 5; i++)
        printf("intarray[%d] has value %d - and address @ %x\n", i, intarray[i], &intarray[i]);

    int *intpointer = &intarray[3]; //point to the 4th element in the array
    printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 4th element

    intpointer++; //now increase the pointer's address so it points to the 5th elemnt in the array
    printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 5th element

    return 0;
}

指针自减

--操作实现指针指向上一个内存地址。

#include <stdio.h>

int main()
{
    int intarray[5] = {10,20,30,40,50};

    int i;
    for(i = 0; i < 5; i++)
        printf("intarray[%d] has value %d - and address @ %x\n", i, intarray[i], &intarray[i]);

    int *intpointer = &intarray[4]; //point to the 5th element in the array
    printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 5th element

    intpointer--; //now decrease the point's address so it points to the 4th element in the array
    printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 4th element

    return 0;
}

指针加法

我们可以将一个整数值加到一个指针变量上,就像下面这样。

#include <stdio.h>

int main()
{
    int intarray[5] = {10,20,30,40,50};

    int i;
    for(i = 0; i < 5; i++)
        printf("intarray[%d] has value: %d - and address @ %x\n", i, intarray[i], &intarray[i]);

    int *intpointer = &intarray[1]; //point to the 2nd element in the array
    printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 2nd element

    intpointer += 2; //now shift by two the point's address so it points to the 4th element in the array
    printf("address: %x - has value %d\n", intpointer, *intpointer); //print the addres of the 4th element

    return 0;
}

注意:这里的内存地址一下子就改变了8。你可能会感到奇怪?答案很简单,因为我们的指针是一个int类型的指针,而int类型的大小是4个字节,所以指针每增加1,内存地址就增加4。

指针减法

同样地,我们可以对指针进行减法操作。

#include <stdio.h>

int main()
{
    int intarray[5] = {10,20,30,40,50};

    int i;
    for(i = 0; i < 5; i++)
        printf("intarray[%d] has value: %d - and address @ %x\n", i, intarray[i], &intarray[i]);

    int *intpointer = &intarray[4]; //point to the 5th element in the array
    printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 5th element

    intpointer -= 2; //now shift by two the point's address so it points to the 3rd element in the array
    printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 3rd element

    return 0;
}

其他操作

还有更多的操作,比如比较><==。这个和常规变量的比较基本相同,只不过在这里比较的是内存地址而已。

函数指针

还记得指针吗?我们用它来指向一个字符数组和表示string。当我们能灵活地操纵是真的时候,事情就变得有趣。我们现在就做一些有趣的事情,尝试将指针指向一个函数。

为什么指向一个函数?

可能这时你的脑海里第一个想法是,我可以简单地通过函数名来调用函数,为什么要使用一个指针来指向一个函数?!好问题。现在想一下你使用一个sort函数来对数组进行排序,有时你想对其升序有时降序,你该如何选择?函数指针!

函数指针语法

void (*pf)(int);

我开始同意你开始的想法了。这样定义实在太复杂了!或者你可能会重新读代码,并且尝试去理解指针到底指向哪里。从外面开始读,*pf是一个指向了一个函数。void是函数的返回值。最后,int是函数的参数。明白了?很好!

让我们向函数里面插入指针,然后再次读代码。

char* (*pf)(int*)

再次,*pf是函数指针,char*是函数的返回值,我们甚至猜到这是stringint*是参数的类型。

好了,理论就到此为止。让我们去看看实际环境的一些恶心代码吧!以下是一个例子。

#include <stdio.h>
void someFunction(int arg)
{
    printf("This is someFunction being called and arg is: %d\n", arg);
    printf("Whoops leaving the function now!\n");
}

int main()
{
    void (*pf)(int);
    pf = &someFunction;
    printf("We're about to call someFunction() using a pointer!\n");
    (pf)(5);
    printf("Wow that was cool. Back to main now!\n\n");
}

记得我们之前所说的sort函数吗?我们做一些相同的事情。这次我们不是对集合进行升序排序,我们来定义自己的排序规则。

#include <stdio.h>
#include <stdlib.h> //for qsort()

int compare(const void* left, const void* right)
{
    return (*(int*)right - *(int*)left);
//go back to ref if this seems complicated:                http://www.cplusplus.com/reference/cstdlib/qsort/
}
main()
{
    int (*cmp) (const void* , const void*);
    cmp = &compare;

    int iarray[] = {1,2,3,4,5,6,7,8,9};
    qsort(iarray, sizeof(iarray)/sizeof(*iarray), sizeof(*iarray), cmp);

    int c = 0;
    while (c < sizeof(iarray)/sizeof(*iarray))
    {
        printf("%d \t", iarray[c]);
        c++;
    }
}

现在,我们反思一下,为什么我们使用函数指针?因为灵活。

上一篇下一篇

猜你喜欢

热点阅读