C语言深度剖析笔记2

接上半部分 C语言深度剖析笔记1 https://blog.csdn.net/halazi100/article/details/125844487
C语言深度剖析笔记1

指针

变量回顾
既然程序中的变量只是一段存储空间的别名,那么是不是必须通过这个别名才能使用这段存储空间呢?

答案是否定的,还可以通过其他的方式间接的访问;比如指针;

思考

int main() {
    int i = 5;
    int *p = &i; //p保存了变量i的地址;
    printf("%d, %08x\n", i, p);
    *p = 10; //通过p可以间接的访问变量i的存储空间;
    pritnf("%d, %08x\n", i, p);
    return 0;
}

指针的本质

  • 指针本质上也是一个变量;
  • 指针需要占用一定的内存空间;
  • 指针用于保存内存空间的地址;

问:不同类型的指针占用的内存空间大小相同吗?

答:都是相同的,都保存变量的地址,所用指针变量的空间大小是一样的;
而指针变量的大小又跟系统(寻址能力)有关,32位系统指针的大小是4个字节,64位系统的指针所占存储空间为8字节;

*号的意义

  • 在指针声明时,*号表示所声明的变量为指针;
  • 在指针使用时,*号表示取指针所指向的内存空间中的值;

但是取多长的空间要根据指针变量的类型;

如果是整型指针就会取4字节,如果是字符类型的指针就会取一个字节;

// 指针声明
int i = 0;
int j = 0;
int *p = &i; //声明p为指向整型变量的指针类型,并定义为变量i的地址;
// 取值
j = *p; //取出指针p所指的存储空间(int大小)的值赋值给j变量;

*号相当于一把钥匙,通过这把钥匙可以打开内存,读取内存中的值;

/*
 * 指针变量所占存储空间大小演示
 */
#include <stdio.h>
int main() {
    int i = 5;
    int *pI = &i;
    char *pC;
    float *pF;
    printf("%0X, %0X, %d, %d\n", pI, &i, *pI, i); //变量pI的值就是i的地址;
    // 所有类型的指针变量所占的存储空间都相同;
    // 32位系统下占4字节,64位系统下占8字节;
    printf("%d, %d, %0X\n", sizeof(int*), sizeof(pI), &pI);
    printf("%d, %d, %0X\n", sizeof(char*), sizeof(pC), &pC);
    printf("%d, %d, %0X\n", sizeof(float*), sizeof(pF), &pF);
    // 指针变量也是一个变量,也有自己的存储空间和地址;
    return 0;
}

函数的传值调用与传址调用

  • 指针也是变量,因此可以声明指针参数;
  • 当需要在一个函数体内改变实参的值,则需要使用指针参数;
  • 函数调用时实参的值将复制到形参;
  • 指针适用于复杂数据类型作为参数的函数中;
#include <stdio.h>
void swap(int a, int b) {
    int tmp = a;
    a = b;
    b = tmp;
}
void swap_p(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
int main() {
    int a = 3, b = 5;
    int *pa = &a, *pb = &b;
    swap(a, b);     // 不能改变实参a和b的值;
    printf("a = %d, b = %d\n", a, b);
    swap_p(pa, pb); // 不能改变实参pa和pb的值,但是能改变所指普通变量a和b的值;
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

如果希望在函数中改变某个变量的值,就需要传递这个变量的地址(高一级指针);

  • 如果需要改变一个普通变量,则需要这个普通变量的地址做形参(一级指针);
  • 如果需要改变一个指针变量,则需要这个指针变量的地址做形参(二级指针);
  • 如果需要改变一个二级指针变量,则需要这个二级指针变量的地址做形参(三级指针);

const与指针

  • const实际上是修饰它左边的东西;
  • const的位置可以移动,但不能越过*号;
int const *p; //指针p可变,p指向的int为只读;
const int *p; //同上;
int * const p; //p指针只读,p指向的int可变;
const int * const p; //p指针和p指向的int都不可变;

指针是C语言中的一种特别的变量,这种变量的值是另一个存储空间的地址;
可以通过指针修改内存中的指定地址处的内容;
通过指针可以获取或改变硬件寄存器的值,因此可以用来编写与硬件打交道的程序,如驱动;

数组

数组的概念

数组是相同类型的变量的有序集合;

int a[5]; 数组包含5个int类型的数据空间;

  • a代表数据的第一个元素的起始地址,也代表这20个字节的存储空间的名字;
  • a[0],a[1]等都是a中的元素,并非元素的名字,数组中的元素没有名字;

数组的大小

数组在一片连续的内存空间中存储元素;

数组元素的个数可以显示或隐式指定;

  • int a[5] = {1, 2};, a[2], a[3], a[4]的值都是0;
  • int b[] = {1, 2};, b中包含2个元素;

数组地址与数组名

  • 数组名代表数组第一个元素的地址;
  • 数组的地址需要用取地址符&才能得到;
  • 数组第一个元素的地址与数组的地址相同,意义不同;
  • 数组第一个元素的地址与数组的地址是两个不同的概念;

数组名的盲点

  • 数组名可以看做一个常指针;int a[5];,a的类型是int *const;
  • 数组名"指向"的是内存中数组首元素的起始地址;
  • 在表达式中数组名只能做为右值使用;
  • 只有在下列场合中数组名不被看作常指针;
    • 数组名作为sizeof关键字的参数;
    • 数组名作为&运算符的参数;
int a[5], b[5];
a = b; //错误,数组名是常指针,不能做左值;
#include <stdio.h>
char *p = "Hello World!"; //定义p为指针
// extern char p[]; //声明p为数组; //报错,说明指针和数组并不相同;
int main() {
    printf("%s\n", p);
    return 0;
}
  • 数组是一片连续的内存空间,是相同类型的变量的有序集合;
  • 数组的地址和数组首元素的地址大小相同,意义不同;
  • 数组名在大多数情况下被当成常指针处理;
  • 数组名其实并不是指针,在外部声明时不能混淆;
  • 数组和指针在某些情况下可以混用;
#include <stdio.h>
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printf("%ld\n", (long int)sizeof(arr)); //20
    printf("%p, %p\n", arr, &arr[0]); //等价
    printf("%p, %p\n", arr + 1, &arr[1]); //等价
    int arr2[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    printf("%ld\n", (long int)sizeof(arr2));
    printf("%p, %p\n", arr2, &arr2[0][0]); //等价
    printf("%p, %p\n", arr2 + 1, &arr2[1][0]); //等价
    return 0;
}

数组的本质

  • 数组是一段连续的内存空间
  • 数组的空间大小为sizeof(元素type) * array_size;
  • 数组名可看作指向数组的第一个元素的常指针;

数组a+1的意义是什么?结果是什么?

  • a表示数组的第一个元素的地址,
  • a+1表示数组第二个元素的地址;

指针运算的意义是什么?结果又是什么?

  • 指针是一种特殊的变量,与整数的运算规则为
    p+n <=> (unsigned int)p + n*sizeof(*p);

当指针p指向一个同类型的数组元素时,p+1将指向当前元素的下一个元素,p-1将指向当前元素的上一个元素;

  • 指针之间只支持减法运算,且参与指针运算的指针类型必须相同;
    p1-p2 <=> ((unsigned int)p1 - (unsigned int)p2) / sizeof(type);

只有当两个指针指向同一个数组中的元素时,指针相减才有意义,其意义为指针所指元素的下标差;
当两个指针指向的元素不在同一个数组中时,结果未定义;

  • 指针也可以进行关系运算<, <=, >, >=,进行这些运算的前提是同时指向同一个数组中的元素;
  • 任意两个指针之间的比较运算(==, !=)无限制;

以下标的形式访问数组中的元素

int a[5];
a[1] = 3;
a[3] = 5;

以指针的形式访问数组中的元素

int a[5];
*(a + 1) = 3;
*(a + 3) = 5;

从理论上讲,当指针以固定增量在数组中移动时,其效率高于下标产生的代码;
当指针增量为1且硬件具有硬件增量模型时,表现更佳;

注意:
现代编译器的生成代码优化率大大提到,在固定增量时,下标形式的效率已经和指针形式相当,但从可读性和代码维护的角度而言,下标形式更优;

数组中a&a的区别

  • a为数组,是数组首元素的地址;
  • &a为整个数组的地址;
  • a&a的意义不同,其区别在于指针运算;

a+1 <=> (undigned int)a + sizeof(*a);
&a+1 <=> (unsigned int)(&a) + sizeof(*&a);

#include <stdio.h>
int main() {
    char arr1[10];
    printf("char arr1:%p, arr1+1:%p\n", arr1, arr1+1); //offset 1
    // char arr1:0x7ffe6dfefc3e, arr1+1:0x7ffe6dfefc3f
    printf("char &arr1:%p, &arr1+1:%p\n", &arr1, &arr1+1); //offset 10
    // char &arr1:0x7ffe6dfefc3e, &arr1+1:0x7ffe6dfefc48

    int arr4[10];
    printf("int arr4:%p, arr4+1:%p\n", arr4, arr4+1); //offset 4
    // int arr4:0x7ffe6dfefc10, arr4+1:0x7ffe6dfefc14
    printf("int &arr4:%p, &arr4+1:%p\n", &arr4, &arr4+1);//offset 40
    // int &arr4:0x7ffe6dfefc10, &arr4+1:0x7ffe6dfefc38
    return 0;
}
#include <stdio.h>
void main()
{
    int TestArray[5][5] = {
        {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}
    };
    int* p1 = (int*)(&TestArray + 1);//array butt, *(p1-1)==35
    printf("*(p1-1):%d\n", *(p1-1));
    int* p2 = (int*)(*(TestArray + 1) + 6);//&(16)+6
    printf("*p2:%d\n", *p2);

    printf("Result: %d; %d; %d; %d; %d\n",
            *(*TestArray), *(*(TestArray + 1)),
            *(*(TestArray + 3) + 3),
            p1[-8], p2[4]);
    //Result: 11; 16; 29; 28; 26
}

数组参数

在C语言中,数组作为函数参数时,编译器将其编译为对应的指针;
void f(int a[]); <=> void f(int *a);
void f(int a[5]); <=> void f(int *a);
一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来表示数组的大小;

指针和数组的对比

  • 数组声明时编译器自动分配一片连续的内存空间;
  • 指针声明时只分配了用于容纳指针的4字节空间;
  • 在作为函数参数时,数组参数和指针参数等价;
  • 数组名在多数情况下可以看做常指针,其值(指向)不能变;
  • 指针的本质是变量,保存的值被看作内存中的地址;

C语言中的字符串

  • 从概念上讲,C语言中没有字符串数据类型;
  • 在C语言中使用字符数组来模拟字符串;
  • C语言中的字符串是以'\0'结束的字符数组;
  • C语言中的字符串可以分配于栈空间,堆空间或者只读存储区;
#include <stdio.h>
#include <malloc.h>
int main() {
    char s1[] = {'H', 'e', 'l', 'l', 'o'}; //s1在栈区
    char s2[] = {'H', 'e', 'l', 'l', 'o', '\0'};
    char* s3 = "Hello"; //只读区
    char* s4 = (char*)malloc(6*sizeof(char));//s4在栈区,指向堆区内存
    s4[0] = 'H';
    s4[1] = 'e';
    s4[2] = 'l';
    s4[3] = 'l';
    s4[4] = 'o';
    s4[5] = '\0';
    free(s4);
    s4 = NULL;
    return 0;
}

字符串长度

  • 字符串长度就是字符串所包含的字符个数;
  • C语言中的字符串长度值的是第一个'\0'字符前出现的字符个数;
  • C语言中通过'\0'结束符来确定字符串长度;
#include <stdio.h>
#include <assert.h>
size_t strlen(const char* s) {
    size_t length = 0;
    assert(s);
    while (*s++) {
        length++;
    }
    return length;
}
int main() {
    printf("%d\n", strlen("123456789"));
    return 0;
}

strlen()的返回值是用无符号数定义的,因此相减不可能产生负数,以下的语句不等价;

char *a = "123";
char *b = "1234";
if (strlen(a) >= strlen(b)) {
    //...
}
if (strlen(a) - strlen(b) >= 0) {
    //...
}

数组的类型

如下声明合法吗

int array[5];
int matrix[3][3];
int *pa = array;
int *pm = matrix;

问题
array代表数组首元素的地址;
array和&array的地址值相同,但是意义不同。那么指向他们的指针类型相同吗?

#include <stdio.h>
void main()
{
    int TestArray[5][5] = {
        {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}
    };

    int* p1 = (int*)(&TestArray + 1);//array butt, *(p1-1)==35
    printf("*(p1-1):%d\n", *(p1-1));
    int* p2 = (int*)(*(TestArray + 1) + 6);//&(16)+6
    printf("*p2:%d\n", *p2);

    printf("Result: %d; %d; %d; %d; %d\n",
            *(*TestArray), *(*(TestArray + 1)),
            *(*(TestArray + 3) + 3),
            p1[-8], p2[4]);
    //Result: 11; 16; 29; 28; 26
}

数组类型

  • C语言中的数组有自己特定的类型;
  • 数组的类型由元素类型和数组大小共同决定

int array[5]的类型为int [5];
但不能直接通过int [5] arr;来定义数组变量,不合法;

  • C语言中通过typedef为数组类型重命名
    typedef type(ARR)[n];

ARR即是数组类型的别名,可以直接用来定义数组变量;

typedef int(AI_5)[5];
typedef float(AF_10)[10];
AI_5 iarray;
AF_10 fArray;

数组指针

用于指向一个数组;

  • 数组名就是数据首元素起始地址,但并不是数组的起始地址;
  • 通过取地址符&作用于数组名可以得到数组的起始地址;

数组指针及类型

  • 可通过数组类型定义数组指针ArrayType* pointer;
typedef type (Array)[n];
Array *pointer;
  • 可以通过typedef定义数组指针类型,然后再定义数组指针变量;
typedef type (*ArrayPointer)[n];
ArrayPointer pointer;
  • 也可直接定义数组指针类型变量
type (*pointer)[n];
- `pointer`为数组指针变量名
- `type`为指向的数组的类型
- `n`为指向的数组的大小

typedef是用来定义(重命名)类型的;没有typedef会直接定义一个变量;

数组指针的使用

#include <stdio.h>
typedef int (Ints_5)[5];
typedef float (Floats_10)[10];
typedef char (Chars_9)[9];
int main() {
    Ints_5 a1;
    float fs[10];
    Chars_9 cs;

    Floats_10 *pf = &fs;
    // typedef char (*Chars9Pointer)[9];
    // Chars9Pointer pc = &cs;
    char (*pc)[9] = &cs;
    char (*pcw)[4] = (char (*)[4])cs;
    
    printf("%ld, %ld\n", sizeof(Ints_5), sizeof(a1)); //20,20
    int i = 0;
    for(i=0; i<10; i++) {
        (*pf)[i] = i;
    }
    for(i=0; i<10; i++) {
        printf("%.3f\n", fs[i]);
    }
    printf("%p, %p, %p, %p, %p\n", &cs, pc, pc+1, pcw, pcw+1);
    //0x7ffda6ead92f, 0x7ffda6ead92f, 0x7ffda6ead938, 0x7ffda6ead92f, 0x7ffda6ead933
    return 0;
}

指针数组

  • 指针数组是一个普通的数组
  • 指针数组中每个元素为一个指针
  • 指针数组的定义type * array[n];
    • type * 为数组中每个元素的类型
    • array 为数组名
    • n为数组大小

指针数组的使用

#include <stdio.h>
#include <string.h>
#define DIM(a) (sizeof(a)/sizeof(*a))
int lookup_keyword(const char* key, const char* table[], const int size) {
    int ret = -1;
    int i = 0;
    for (i=0; i<size; i++) {
        if (strcmp(key, table[i]) == 0) {
            ret = i;
            break;
        }
    }
    return ret;
}

int main() {
    const char* keyword[] = {
            "do",
            "for",
            "if",
            "register",
            "return",
            "switch",
            "while",
            "case",
            "static"
    };
    printf("%d\n", lookup_keyword("return", keyword, DIM(keyword))); //4
    printf("%d\n", lookup_keyword("main", keyword, DIM(keyword))); //-1
}

小结

  • 数组指针本质上是一个指针;
  • 数组指针指向的值是数组的地址;
  • 指针数组本质上是一个数组;
  • 指针数组中每个元素的类型是指针;

二级指针

  • 指向指针的指针
  • 指针变量会在内存中占用一定的空间
  • 可以定义指针来保存指针变量的地址值
#include <stdio.h>
int main()
{
    int a = 0;
    int *p = NULL;
    int **pp = NULL;
    pp = &p;
    p = &a;
    return 0;
}

为什么需要指向指针的指针

  • 指针本质上也是变量
  • 对于指针也同样存在传值调用与传址调用

多级指针的分析与使用

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

int reset(char **pp, int size, int new_size)
{
    if ((pp == NULL) || (new_size <= 0)) {
        return 0;
    }
    char *p = *pp;
    char *pt = (char*)malloc(new_size);
    int len = (size < new_size) ? size : new_size;
    int i = 0;
    for (i = 0; i < len; i++) {
        // *(pt + i) = *(p + i);
        pt[i] = p[i];
    }
    pt[new_size-1] = '\0';
    free(*pp);
    *pp = pt;
    return 1;
}

int main() {
    char *p = (char*)malloc(15);
    strncpy(p, "hello world", 15);
    printf("%p, %s\n", p, p);
    if (reset(&p, 15, 10)) {
        printf("%p, %s\n", p, p);
    }
    return 0;
}

二维数组与二级指针

  • 二维数组在内存中以一维的方式排布
  • 二维数组中的第一维是一维数组
  • 二维数组的数组名可看做常量指针

以一维方式访问二维数组

#include <stdio.h>
#include <stdlib.h>
void printArray(int *const a, int size)
{
    printf("printArray: %d\n", size);
    int i = 0;
    for (i=0; i<size; i++) {
        printf("%d\n", a[i]);
    }
}

int main()
{
    int a[4][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, {9, 10, 11}};
    int *p = &a[0][0];
    printArray(p, 12);
    return 0;
}

思考
int matrix[2][4];,matrix到底是2行4列,还是4行2列

数组名

  • 一维数组名代表数组首元素的地址,int a[5], a的类型为int *;
  • 二维数组名同样代表数组首元素的地址,int m[2][5],m的类型为int (*)[5];

结论

  • 二维数组名可以看做是指向数组的常量指针
  • 二维数组可以看做是一维数组
  • 二维数组中的每个元素都是同类型的一维数组
#include <stdio.h>
int main()
{
    int a[5][5] = {
        {11,12,13,14,15},
        {21,22,23,24,25},
        {31,32,33,34,35},
        {41,42,43,44,45},
        {51,52,53,54,55}
    };
    int (*p)[4];
    //11,12,13,14;
    //15,21,22,23;
    //24,25,31,32;
    //33,34,35,41;
    //42,43,44,45;
    //51,52,53,54;
    //55
    p = (int (*)[4])a;
    printf("%d, %d\n", p[4][2], a[4][2]);//44,53
    printf("%ld\n", &p[4][2] - &a[4][2]);//-4
    return 0;
}

以指针的形式遍历二维数组

#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
    int a[2][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}};
    int row = 0;
    int col = 0;
    
    for (row=0; row<2; row++) {
        for (col=0; col<4; col++) {
            printf("%d ", *(*(a+row) + col));
            // printf("%d ", a[row][col]);
        }
        printf("\n");
    }
}

小结

  • C语言中只有一维数组,而且数组大小必须在编译期就作为常数确定
  • C语言中的数组元素可是任何类型的数据,即数组的元素可以是另一个数组
  • C语言中只有数组的大小和数组首元素的地址是编译期直接确定的

数组参数和指针做参数

为什么C语言中的数组参数会退化为指针

  • C语言中只会以值拷贝的方式传递参数
  • 当向函数传递数组时,不是将整个数组拷贝一份传入函数,而是将数组名看做常量指针传递数组首元素地址

C语言以高效为最初设计目标,在函数传递时如果拷贝整个数组会大大降低执行效率

二维数组参数

  • 二维数组参数同样存在退化问题
    • 二维数组可以看做是一维数组
    • 二维数组中的每个元素是一维数组
  • 二维数组参数中第一维的参数可以省略

以下几种形式等价

  • void f(int a[5]);
  • void f(int a[]);
  • void f(int *a);

以下几种形式等价

  • void g(int a[3][3]);
  • void g(int a[][3]);
  • void g(int (*a)[3]);

函数参数等价关系

  • 一维数组 float a[5]; 等价于 指针 float *a;
  • 指针数组 int *a[5]; 等价于 指针的指针 int **a;
  • 二维数组 char a[3][4]; 等价于 数组的指针 char (*a)[4];

注意事项

  • C语言中无法向一个函数传递任意的多维数组
  • 为了提供正确的指针运算,必须提供除第一维之外的所有维长度

限制

  • 一维数组参数,必须提供一个标示数组结束位置的长度信息
  • 二维数组参数,不能直接传递给函数
  • 三维或更高维数组参数,无法使用

传递与访问二维数组的方式

#include <stdio.h>
void access(int a[][3], int row)
{
    // typeof(a): int (*)[3];
    int col = sizeof(*a) / sizeof(int);
    int i = 0;
    int j = 0;
    // printf("sizeof(a)=%ld, sizeof(*a)=%ld\n", sizeof((int (*)[3])a), sizeof(*a));//8,12
    // 'sizeof' on array function parameter 'a' will return size of 'int (*)[3]'
    printf("sizeof(a)=%ld, sizeof(*a)=%ld\n", sizeof(a), sizeof(*a));//8,12
    for (i=0; i<row; i++){
        for (j=0; j<col; j++) {
            printf("%d ", a[i][j]);
        }
        printf("\n");
    }
}

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

函数类型

  • C语言中的函数有自己特定的类型
  • 函数的类型由返回值,参数类型和参数个数共同决定,int add(int i, int j)的类型为int(int, int)
  • C语言中通过typedef为函数类型重命名,typedef type name(parameter list)

如下ft1和ft2分别表示一种函数类型
typedef int ft1(int, int);
typedef void ft2(int);

函数指针

  • 函数指针用于指向一个函数;
  • 函数名是执行函数体的入口地址;

如何定义函数指针类型

  • 函数类型加*即可
//typedef type (functype)(paramerer list);
typedef type functype(paramerer list);
typedef functype* funcpointertype;
  • 直接声明函数指针类型
typedef type (*funcpointtype)(paramerer list);

如何定义函数指针(变量)

  • 可通过函数类型定义函数指针 functype *pointer;
typedef type functype(paramerer list);
functype *pointer;
  • 先定义函数指针类型,在定义函数指针变量
typedef type (*funcpointtype)(paramerer list);
funcpointtype pointer;
  • 也可以直接定义函数指针
type (*pointer)(parameter list);
- `pointer` 为函数指针变量名
- `type` 为指向函数的返回值类型
- `parameter list`为指向函数的参数类型列表

typedef是用来定义(重命名)类型的;没有typedef会直接定义一个变量;

函数指针的本质与使用

#include <stdio.h>
int test(int i)
{
    return i * i;
}

int main()
{
    //define funciton type then define function pointer var;
    // typedef int(Function)(int); //is also ok
    typedef int Function(int); //Function两边可以加上小括号
    Function *pf1 = test; //define a function pointer var pt by "functype *pointer"
    printf("Function pointer call: %d\n", pf1(2));

    //define function pointer type
    typedef Function * FunctionPointer;
    FunctionPointer pf2 = test;
    printf("Function pointer call: %d\n", pf2(2));

    //define function pointer type
    typedef int (*FunctionPointer3)(int);
    FunctionPointer3 pf3 = test;
    printf("Function pointer call: %d\n", pf3(2));

    //define function pointer var
    int (*pf4)(int);
    pf4 = test;
    printf("Function pointer call: %d\n", pf4(2));
    return 0;
}

回调函数

回调函数是利用函数指针实现的一种调用机制

回调机制原理

  • 调用者不知道具体事件发生时需要调用的具体函数
  • 被调函数不知道何时被调用,只知道被调用后需要完成的任务
  • 当具体事件发生时,调用者通过函数指针调用具体函数

回调机制将调用者和被调函数分开,两者互不依赖,实现解耦

回调函数使用示例

#include <stdio.h>
typedef int(*FunctionPointer)(int);//define function pointer type
int handler(int n, FunctionPointer f)
{
    return n*f(n);
}

int f1(int x)
{
    printf("%s: x + 1\n", __func__);
    return x + 1;
}

int f2(int x)
{
    printf("%s: 2x + 1\n", __func__);
    return 2*x - 1;
}

int f3(int x)
{
    printf("%s: -x\n", __func__);
    return -x;
}

int main()
{
    printf("x * f1(x): %d\n", handler(3, f1));
    printf("x * f2(x): %d\n", handler(3, f2));
    printf("x * f3(x): %d\n", handler(3, f3));
    return 0;
}

指针阅读技巧解析-右左法则

  1. 从最里层的圆括号中未定义的标识符看起
  2. 首先往右看,再往左看
  3. 当遇到圆括号或者方括号时可以确定部分类型,并调转方向
  4. 重复2,3步骤,只到阅读结束

复杂指针的阅读

#include <stdio.h>
int main()
{
    //p1为指针,指向函数,指向的函数有(int*,f)两个参数,返回值int;
    //f也是一个函数指针,指向的函数参数是int*,返回值为int;
    int (*p1)(int*, int (*f)(int*));

    //p2为数组,有5个元素,这5个元素为指针类型,指针指向函数,函数类型为int(int*)
    int (*p2[5])(int*);

    //p3为指针,数组指针,指向数组有5个元素,每个元素为指针,指针是函数指针,指向的函数类型为int(int*)
    int (*(*p3)[5])(int*);

    //p4为指针,函数指针,参数为int*,返回值为指针,是函数指针,指向的函数类型为int*(int*)
    int *(*(*p4)(int*))(int*);

    //p5为指针,函数指针,参数为int*,返回值为指针,指向数组,指向的数组类型为int[5]
    int (*(*p5)(int*))[5];
    //实际不会如此复杂
    // typedef int(Array)[5];
    // typedef Array* (Function)(int *);
    // Function *p5;
    return 0;
}

小结

  • 右左法则总结于编译器对指针变量的解析过程
  • 指针阅读练习的意义在于理解指针的组合定义
  • 可通过typedef简化复杂指针的定义

动态内存分配

为什么使用动态内存分配

  • C语言中的一切操作都是基于内存
  • 变量和数组都是内存的别名,如何分配这些内训由编译器在编译器决定
    • 定义数组的时候必须指定数组长度
    • 数组长度是在编译期就必须决定的

在程序运行的过程中,可能需要一些额外的内存空间。

malloc和free用于执行动态内存分配和释放

  • malloc所分配的是一块连续的内存,以字节为单位,且不带任何的类型信息
  • free用于将动态内存归还系统

函数原型如下

  • void *malloc(size_t size);
  • void free(void *pointer);

注意
malloc实际分配的内存可能会比请求的稍微多一点,但是不能依赖于编译器的这个行为;

  • 当请求的动态内存无法满足时,malloc返回NULL;
  • 当free的参数为NULL时,函数直接返回;

malloc(0);会返回什么

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int *p = (int*)malloc(0);
    printf("%p\n", p);
    free(p);
    p = NULL;
    return 0;
}

不停的malloc(0);不释放,也会导致内存泄露;

因为malloc申请的内存空间比实际申请的稍大,现代操作系统一般都是4字节对齐(malloc(1)得到的也许是4);

你认识malloc的兄弟吗

其他动态内存分配函数

  • calloc()可以指定分配内存的类型信息并初始化为0

    • void *calloc(size_t num, size_t size);
    • calloc的参数代表所返回内存的类型信息
    • calloc会将返回的内存初始化为0
  • realloc()函数可以调整动态分配内存的大小;

  • void *realloc(void *pointer, size_t new_size);
  • 在使用realloc之后应该使用其返回值
  • 当pointer的第一个参数为NULL时,等价于malloc

calloc和realloc的使用

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int i = 0;
    int *pI = (int*)malloc(5 * sizeof(int));
    short *pS = (short*)calloc(5, sizeof(short));

    for (i=0; i<5; i++) {
        printf("pI[%d] = %d, pS[%d] = %d\n", i, pI[i], i, pS[i]);
    }

    printf("before realloc, pI:%p\n", pI); // before realloc, pI:0x558fc4c9b260
    pI = (int*)realloc(pI, 10 * sizeof(int));
    printf("after realloc, pI:%p\n", pI);  //  after realloc, pI:0x558fc4c9b6b0
    for (i=0; i<10; i++) {
        printf("pI[%d] = %d\n", i, pI[i]);
    }

    free(pI);
    free(pS);
    return 0;
}

动态内存与指针使用原则

  • 绝不返回局部变量和局部数组的地址,局部变量在定义后立即初始化;
  • 字符数组必须确认0结束符后才能成为字符串;
  • 任何使用与内存操作相关的函数必须指定长度信息;
  • 通常在哪个函数申请的空间,应在那个函数释放;
  • 动态内存申请之后,应该立即检查指针值是否为NULL, 防止使用NULL指针;
  • 指针变量申请后立即赋值为NULL,使用指针变量时首先判NULL;
  • free指针之后立即赋值为NULL,防止指针悬空;
  • malloc操作和free操作必须匹配,防止内存泄露和多次释放;

小结

  • 动态内存分配是C语言中的强大功能;
  • 程序能够在需要的时候有机会使用更多的内存;
  • malloc单纯的从系统中申请固定字节大小的内存;
  • calloc能以类型大小为单位申请内存并初始化为0;
  • realloc能用于重置内存大小;

堆栈

堆栈是程序运行过程中才会存在的区域;
编译时可执行文件中并没有保存堆栈;

进程中的栈

  • 栈是现代计算机程序里最为重要的概念之一;
  • 栈在进程中用于维护函数调用上下文,没有栈就没有函数,没有局部变量;
  • 栈保存了一个函数调用所需的维护信息;
    • 函数参数,函数返回地址;
    • 局部变量;
    • 函数调用上下文;

进程中的堆

为什么有了栈还需要堆?

栈上的数据在函数返回后就被自动释放掉,无法传递到函数外部,如局部数组;

  • 堆是进程中一块巨大的内存空间,可由程序自由使用;
  • 堆中被程序申请使用的内存在程序主动释放前将一直有效;

系统对堆空间的管理方式有空闲链表法,位图法,对象池法等;

程序中的静态存储区

  • 程序静态存储区随着程序的运行而分配空间,直到进程结束;

在程序的编译期静态存储区的大小就已经确定;

  • 程序的静态存储区主要用于保存程序中的全局变量和静态变量;
  • 与栈和堆不同,静态存储区的信息最终会保存到可执行程序中;

小结

  • 栈,堆和静态存储区是C语言程序常涉及的三个基本存储区;
  • 栈区主要用于函数调用使用;
  • 堆区主要用于内存的动态申请和释放;
  • 静态区用于保存全局变量和静态变量;

程序的存储

进程和程序的概念

  • 程序是保存在硬盘上的"可执行文件";
  • 进程是运行在内存中的程序实例;

也就是说,程序是静态的文件,进程是内存中真正运行的部分;

  • size filename命令可以查看二进制文件的静态存储信息;
  • readelf -hSW filename命令也可以查看二进制文件的信息;
    linux可执行文件是按ELF格式存储的,通过size命令可以查看到.text,.data,.bss段的大小,
    程序运行时系统调用exec将文件装入内存,将.bsss段的数据设置为0,并从代码区开始执行程序;

代码在可执行文件中的存储

  • .text 代码区,存放可执行代码;
  • .rodata 只读常量区,在某些资料中把该区并入代码区;
  • .data (初值不为0)的(全局和静态)变量;
  • .bss (无初值或初值为0)的(全局和静态)变量;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jBFcpvAC-1658113869111)(./image/31.1.object-memory-store-location.jpg)]

代码在可执行程序中的对应关系

#include <stdio.h>
int global_init_var = 84; //.DATA
int global_uint_var;      //.BSS

//.text
void func1(int i)
{
    printf("%d\n", i);
}

int main()
{
    static int static_var = 85; //.DATA
    static int static_var2;     //.BSS

    int a = 1; //不会存储到目标文件,而是在函数执行时分配到栈区
    int b;

    char *p = "hello world";//"hello world" @ .rodata
    func1(static_var + static_var2 + a + b);//.text
    return 0;
}

同是全部变量和静态变量,为什么初始化的和未初始化的保存在不同段中?

C语言规定,未初始化变量的初值为0,这个清0的操作是由启动代码完成的,已初始化变量的初值的设置也是由启动代码完成的。

为了启动代码的简单化,编译链接器会把已初始化的变量放在同一个段.data,这个段的映像(包含了各个变量的初值)保存在"只读数据段",这样启动代码就可以简单地复制这个映像到.data段,所有的已初始化变量就都初始化了;
而未初始化变量也放在同一个段.bss,启动代码简单地调用memset就可以把所有未初始化变量都清0;

查看可执行文件信息的命令

二进制文件信息查看

  • readelf -hSW filename
  • objdump -s filename

符号表查看

  • nm -nC filename

依赖动态库查看

  • readelf -d filename
  • ldd filename

通过file $(which ldd),可以看出/usr/bin/ldd: Bourne-Again shell script, ASCII text executable
ldd其实是一个脚本文件,ldd file命令最终其实是调用/lib64/ld-linux-x86-64.so.2 --list file,
脚本中实际是LD_TRACE_LOADED_OBJECTS=1 /lib64/ld-linux-x86-64.so.2 file

进程的内存空间分布

重点中的重点
a.out可执行程序经过系统装载(exec)并启动后产生进程,a.out与进程的地址空间有一部分映射关系;
堆栈段在程序运行后才正式存在,是程序运行的基础;
程序和进程不同,程序是静态概念,进程是动态概念;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1El7T0pj-1658113869113)(./image/31.2.process-memory-map.png)]

内核空间
  ┌─────────────────┬──────────────────────────────────┐(4GB)
  │3G-4G系统保留区域,所有进程共享,每个进程默认分配8k内核栈 │
  └─────────────────┴──────────────────────────────────┘(3GB)
用户空间
  ┌──────────────────┬──────────────────────────────────┐(3GB)
  │  6.栈区  .stack  │  用户栈靠近3G的位置开始,默认8M上限  │
  ├──────────────────┼──────────────────────────────────┤
  │  5.堆区  .heap   │                                  │
  ├──────────────────┼──────────────────────────────────┤
  │  4.BSS段  .BSS   │ (无初值或初值为0)的(全局和静态)变量 │
  ├──────────────────┼──────────────────────────────────┤
  │  3.数据段 .data   │ (初值不为0)的(全局和静态)变量      │
  ├──────────────────┼──────────────────────────────────┤
  │  2.只读数据.rodata│                                  │
  │  1.代码区 .text   │                                  │
  └──────────────────┴──────────────────────────────────┘(0GB)
  1. 内核栈
    第3G-4G为系统保留区域,所有进程共享,为内核空间,每个进程默认分配8k内核栈;作用:
  • 保存中断现场,对于嵌套中断,被中断程序的现场信息依次压入系统栈,中断返回时逆序弹出;
  • 保存操作系统子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量;

用户空间与内核空间通过系统调用进行切换;

  1. 栈区.stack
    此处指用户栈,内存的分配和回收都自动进行;
    每当一个函数调用出现时就从这个段落中分配一段空间,当函数调用结束时它所分配的空间被自动回收;
  • 内存的分配和回收都自动进行;
  • 用于保存局部变量(包括函数形参,块变量,函数返回值等);
  • 不同函数调用的空间遵循后进先出的原则;递归调用时如果递归层次太多可能会导致栈空间不足而导致栈溢出;
  • 栈区从靠近3G的位置开始使用,如果数组越界可能会破坏原先压栈的数据如函数返回地址等;
  • 用户栈空间上限默认为8M,可以通过ulimit -a命令查看;
  1. 堆区.heap
    动态分配内存的区域,也叫自由区,受系统保护;
  • new,delete,malloc(),free()都是针对堆区内存;
  • 堆区的内存完全由程序员管理,包括分配和回收;
  • 如果程序员没有回收将造成内存泄漏,直到进程结束才会被系统回收;
  1. BSS段.bss
  • 存储没有初值或初值为0的普通全局变量或初值不0的static变量;
  • BSS段会在main()执行之前自动清0;
  1. 全局区.data
  • 保存初值不为0的static变量和初值不为0的全局变量;
  • 主函数执行之前分配,进程结束时回收;
  • 这个段落的大小不会随着程序的运行而改变;
  1. 只读常量区.rodata
  • 存放字符串字面值(就是双引号""括起来的字符串)和const修饰的全局变量;
  • 在某些资料中把该区并入代码区;
  1. 代码区.text
  • 存放代码(包括函数)的区域,
  • 只读的区域;

a.out进程的地址空间在代码区之前还有一段未映射区域

程序术语对应关系

  • 静态存储区通常指程序中的.bss.data段;
  • 只读区通常指程序中的.rodata段;
  • 局部变量所占空间为栈.stack空间;
  • 动态空间为堆.heap空间;
  • 程序可执行代码存放于.text段;

野指针与内存操作

  • 野指针通常是因为指针变量中保存的值不是一个合法的内存地址而造成的;
  • 野指针不是NULL指针,是指向不可用的内存的指针;
  • NULL指针不容易用错,因为if语句很好判断一直指针是不是NULL;

使用野指针不一定会导致严重问题,但是却很可能会导致严重问题;
C语言中没有任何手段可以判断一个指针是否为野指针

野指针的由来

  1. 局部指针变量没有被初始化
#include <stdio.h>
#include <string.h>
struct Student {
    char* name;
    int number;
};
int main()
{
    struct Student s;
    // s.name = (char *)malloc(20);
    strcpy(s.name, "hello"); // OOPS!
    s.number = 99;
    // free(s.name);
    return 0;
}
  1. 使用已经释放过的指针
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void func(char* p)
{
    printf("%s\n", p);
    free(p);
}

int main()
{
    char* s = (char*)malloc(5);
    strcpy(s, "hello");
    func(s);
    printf("%s\n", s); // OOPS!
    return 0;
}
  1. 指针所指向的变量在指针使用前被销毁
#include <stdio.h>
char* func()
{
    char p[] = "hello";
    return p;
}
int main()
{
    char* s = func();
    printf("%s\n", s); // OOPS!
    return 0;
}

非法内存操作分析

  • 结构体声明指针未初始化
  • 没有为结构体指针分配足够的内存
#include <stdio.h>
#include <stdlib.h>
struct Demo {
    int* p;
};
int main()
{
    struct Demo d1;
    struct Demo d2;
    int i = 0;
    for (i=0; i<10; i++) {
        d1.p[i] = 0; // OOPS!
    }
    d2.p = (int*)calloc(5, sizeof(int));
    for (i=0; i<10; i++) {
        d2.p[i] = i; // OOPS!
    }
    free(d2.p);
    return 0;
}
  • 内存分配成功,但并未初始化
#include <stdio.h>
#include <malloc.h>
int main()
{
    char* s = (char*)malloc(10);
    printf("%s\n", s); // OOPS!
    free(s);
    return 0;
}
  • 数组越界
#include <stdio.h>
void f(int a[10])
{
    int i = 0;
    for (i=0; i<10; i++) {
        a[i] = i; // OOPS!
        printf("%d\n", a[i]);
    }
}
int main()
{
    int a[5];
    f(a);
    return 0;
}
  • 内存泄露分析
#include <stdio.h>
#include <stdlib.h>
void f(unsigned int size)
{
    int* p = (int*)malloc(size*sizeof(int));
    int i = 0;
    if ((size % 2) != 0) {
        return; // OOPS! //memory leak
    }

    for (i=0; i<size; i++) {
        p[i] = i;
        printf("%d\n", p[i]);
    }
    free(p);
}

int main()
{
    f(9);
    f(10);
    return 0;
}
  • 多次指针释放
#include <stdio.h>
#include <stdlib.h>
void f(int* p, int size)
{
    int i = 0;
    for (i=0; i<size; i++) {
        p[i] = i;
        printf("%d\n", p[i]);
    }
    free(p);
}

int main()
{
    int* p = (int*)malloc(5 * sizeof(int));
    f(p, 5);
    free(p); // OOPS! //double free
    return 0;
}
  • 使用已释放的指针
#include <stdio.h>
#include <stdlib.h>
void f(int* p, int size)
{
    int i = 0;
    for (i=0; i<size; i++) {
        printf("%d\n", p[i]);
    }
    free(p);
}

int main()
{
    int* p = (int*)malloc(5 * sizeof(int));
    int i = 0;
    f(p, 5);
    for (i=0; i<5; i++) {
        p[i] = i; // OOPS! //invalid
    }
    return 0;
}

C语言中的"交通规则"

  • 用malloc申请了内存之后,应该立即检查指针值是否为NULL,防止使用NULL指针
  • 牢记数组的长度,防止数组越界,考虑使用柔性数组
  • 动态申请操作必须和释放操作匹配,防止内存泄露和多次释放
  • free指针之后必须立即赋值为NULL

认清函数真面目

函数由来

  • 程序=数据+算法
  • C程序=数据+函数

面向过程的程序设计

  • 面向过程是一种以过程为中心的编程思想
  • 首先将复杂的问题分解为一个个容易解决的问题
  • 分解过后的问题可以按照步骤一步步完成
  • 函数是面向过程在C语言中的体现
  • 解决问题的每个步骤可以用函数来实现

声明和定义

  • 程序中的声明可理解为预先告诉编译器实体的存在,如类型,变量,函数等等;
  • 程序中的定义明确指示编译器实体的意义;

声明和定义的不同

//global.c
int g_var = 0;
//test.c
#include <stdio.h>
extern int g_var;//声明
void f(int i, int j);//声明
int main()
{
    int g(int x);//声明
    g_var = 10;
    f(1, 2);
    printf("%d\n", g(3));
    return 0;
}
void f(int i, int j)
{
    printf("i + j = %d\n", i + j);
}
int g(int x)
{
    return 2 * x + g_var;
}

函数参数

  • 函数参数在本质上与局部变量相同,都是在栈上分配空间;
  • 函数参数的初始值是函数调用时的实参值;
#include <stdio.h>
int f(int i, int j)
{
    printf("%d, %d\n", i, j);
}
int main()
{
    int k = 1;
    f(k, k++);//2,1;实参求值完成是一个顺序点
    printf("%d\n", k);//2
    return 0;
}

函数参数的求值顺序依赖于编译器的实现,如f(a, a++)是先a++还是a不一定;
C语言大多数运算符对其操作数求值的顺序都是依赖于编译器的实现;

int i = f() * g();,不确定先计算f()还是g();不能写出要先计算f再计算g的函数;

程序中的顺序点

程序中存在一定的顺序点;
顺序点指的是执行过程中修改变量值的最晚时刻;
在程序达到顺序点的时候,之前所作的一切操作必须反应到后续的访问中;

顺序点的位置

  • 每个完整表达式结束时;
  • &&,||,?:,以及逗号表达式,的每个运算对象计算之后;
  • 函数调用中对所有实参的求值完成之后(进入函数体之前);
#include <stdio.h>
int main()
{
    int k = 2;
    int a = 1;
    k = k++ + k++;//顺序点在;之后,先k=2+2,再自增1/2次?
    printf("k = %d\n", k);//5
    if (a-- && a) { // 有&&顺序点,即if(0&&1)
        printf("a = %d\n", a);//不会执行
    }
    return 0;
}

C语言中没有类型的函数参数默认为int

f(i, j) {
    return i + j;
}

等价于

int f(int i, int j) {
    return i + j;
}

小结

  • C语言是一种面向过程的语言;
  • 函数可理解为解决问题的步骤;
  • 函数的实参并没有固定的计算次序;
  • 顺序点是C语言中变量改变的最晚时机;
  • 函数定义时参数和返回值的缺省类型是int;

可变参数

如何编写一个可以计算n个数平均值的函数

#include <stdio.h>
float func(int array[], int size)
{
    int i = 0;
    float avr = 0;
    for (i=0; i<size; i++) {
        avr += array[i];
    }
    return avr / size;
}

int main()
{
    int array[] = {1, 2, 3, 4, 5};
    printf("%f\n", func(array, 5));
    return 0;
}

函数参数个数固定;

可变参数的定义与使用

C语言中可以定义参数可变的函数

  • 参数可变函数的实现依赖于头文件stdarg.h;
  • va_list变量与va_start,va_endva_arg配合使用能够访问参数值;

示例

#include <stdio.h>
#include <stdarg.h>
float average(int n, ...)
{
    int i = 0;
    float sum = 0;
    
    va_list args;
    va_start(args, n);
    for (i=0; i<n; i++) {
        sum += va_arg(args, int);
    }
    va_end(args);
    return sum / n;
}

int main()
{
    printf("%f\n", average(5, 1, 2, 3, 4, 5));
    printf("%f\n", average(4, 1, 2, 3, 4));
    return 0;
}

可变参数的限制

  • 可变参数必须从头到尾按照顺序逐个访问;
  • 参数列表中至少要存在一个确定的命名参数;
  • 可变参数宏无法判断实际存在的参数的数量;
  • 可变参数宏无法判断参数的实际类型;

警告:va_arg中如果指定了错误的类型,那么结果是不可预测的;

小结

  • 可变参数是C语言提供的一种函数设计技巧;
  • 可变参数的函数提供了一种更方便的函数调用方式;
  • 可变参数必须顺序的访问;
  • 无法直接访问可变参数列表中间的参数值;

函数vs宏

  • 宏由预处理器直接替换展开的,编译器不知道宏的存在;
  • 函数是由编译器直接编译的实体,调用行为由编译器决定;
  • 多次使用宏会导致程序代码量增加;
  • 函数是跳转执行的,因此代码量不会增加;
  • 宏的效率比函数高,因为是直接展开,无调用开销;
  • 函数调用时会创建活动记录,效率不如宏;
#include <stdio.h>
#define RESET(p, len) while(len > 0) ((char*)p)[--len] = 0
void reset(void* p, int len)
{
    while (len > 0) {
        ((char*)p)[--len] = 0;
    }
}

int main()
{
    int array[] = {1, 2, 3, 4, 5};
    int len = sizeof(array);
    reset(array, len);
    RESET(array, len);
    return 0;
}

宏的优点和缺点

宏的效率比函数稍高,但是副作用巨大,容易出错;

#include <stdio.h>

#define ADD(a, b) a + b
#define MUL(a, b) a * b
#define _MIN_(a, b) ((a) < (b) ? (a) : (b))

int main()
{
    int i = 1;
    int j = 10;

    printf("%d\n", MUL(ADD(1, 2), ADD(3, 4)));//11 //1 + 2 * 3 + 4
    printf("%d\n", _MIN_(i++, j)); //2 //((i++) < (j) ? (i++) : (j)))
    return 0;
}

函数的优点和缺点

函数存在实参到形参的传递,因此无任何副作用,但是函数需要建立活动对象,效率受影响;

#include <stdio.h>
int add(int a, int b)
{
    return a + b;
}

int mul(int a, int b)
{
    return a * b;
}

int _min_(int a, int b)
{
    return a < b ? a : b;
}

int main()
{
    int i = 1;
    int j = 10;

    printf("%d\n", mul(add(1, 2), add(3, 4)));//21,1
    printf("%d\n", _min_(i++, j));//1

    return 0;
}

宏无可替代的优势

宏参数可以是任何C语言实体;

  • 宏参数类型不固定,因为会在预处理阶段进行替换,只要替换后的类型正确即可;
  • 宏的参数可以是类型名;
#define MALLOC(type, n) (type*)malloc(n*sizeof(type))
int *p = MALLOC(int, 5);

小结

  • 宏和函数并不是竞争对手;
  • 宏能接受任何类型的参数,效率高,易出错;
  • 函数的参数必须是固定类型,效率低,不易出错;
  • 宏可以实现函数不能实现的功能;

函数调用行为

活动记录

活动记录是函数调用时用于记录一系列相关信息的记录

  • 临时变量域: 用来存放临时变量的值,如k++的中间结果
  • 局部变量域: 用来存放函数本次执行中的局部变量
  • 机器状态域: 用来保存调用函数之前有关机器状态的信息,包括各种寄存器的当前值和返回地址等
  • 实参数域: 用于存放函数的实参信息
  • 返回值域: 为调用者函数存放返回值

参数入栈

函数参数的计算次序是依赖编译器实现的,函数参数的入栈次序是如何确定的呢?

  ┌──────────────────┬─────
  │  parameters      │     
  ├──────────────────┤   活
  │  return addr     │   
  ├──────────────────┤   动
  │  old EBP         │   
  ├──────────────────┤   记
  │  saved register  │   
  ├──────────────────┤   录
  │  local var       │     
  ├──────────────────┤     
  │  other data      │     
  └──────────────────┴─────

由调用约定来描述,我们自己来约定。

调用约定

当一个函数被调用时,参数会传递给被调用的函数,而返回值会被返回给调用函数。
函数的调用约定就是描述参数是怎么传递到栈空间的,以及栈空间由谁维护。

  • 参数传值顺序

    • 从右到左依次入栈: __stdcall, __cdecl, __thiscall;
    • 从左到右依次入栈: __pascal, __fastcall;
  • 调用栈堆栈清理

    • 调用者清除栈
    • 被调用函数返回后清除栈

注:在调用动态函数库的时候,我们就得规定调用约定,使双方一致,否则在调用的过程中会出错。
调用约定的用法,就是在指定的函数前面加上对应的符号就好,如:_pascal f();

小结

  • 函数调用是C语言的核心机制;
  • 活动纪律中保存了函数调用以及返回所需要的一切信息;
  • 调用约定是调用者和被调用者之间的调用协议,常用于不同开发者编写的库函数之间;

函数递归与函数设计技巧

递归概述

  • 递归是数学领域中的概念在程序设计中的应用;
  • 递归是一种强有力的程序设计方法;
  • 递归的本质为函数内部在适当的时候调用自身;

递归函数

C递归函数有两个主要的组成部分;

  • 递归点:以不同参数调用自身;
  • 递归出口:不再递归调用,又叫递归边界,必不可少;
fx = 1; (x=1)
fx = x * f(x - 1); (x>1)

实例分析

利用递归函数求n!

#include <stdio.h>
int func(int n)
{
    if (n <= 1) {
        return 1; //递归边界
    } else {
        return n * func(n - 1);
    }
}

int main()
{
    printf("n! = %d\n", func(5));
    return 0;
}

小结

  • C语言中的递归函数必然会使用判断语句,递归边界必不可少;
  • 递归函数在需要编写的时候定义函数的出口,否则栈会溢出;
  • 递归函数是一种分而治之的思想;

函数设计技巧

  • 不要在函数中使用全局变量,尽量让函数从意义上是一个独立的功能模块;

  • 参数名要能够体现参数的意义;

void str_copy(char *std_dest, char *str_src);

优于如下定义

void str_copy(char *str1, char *str2);
  • 如果参数是指针,且仅作输入参数用,则应在类型前加上const,以防止该指针在函数体内被意外修改
void str_copy(char *std_dest, const char *str_src);
  • 不要省略返回值的类型,如果函数没有返回值,那么应声明为void类型

  • 在函数体的入口处,对参数的有效性进行检查,对指针的检查尤为重要

  • 语句不可返回指向栈空间的指针,因为该内存在函数体结束时被自动销毁

  • 函数体的规模要小,尽量控制在80行代码之内,否则可能考虑是否违背职责单一,做了不该做的事

  • 相同输入应当产生相同的输出,尽量避免函数带有"记忆"功能

  • 避免函数有太多的参数,参数个数尽量控制在4个以内

  • 有时候函数不需要返回值,但为了增加灵活性,如支持链式表达式,可以附加返回值

char s[64];
int len = strlen(strcpy(s, "android"));
  • 函数名与返回值类型在语义上不可冲突
char c;
c = getchar();
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值