《C语言进阶剖析》6.数组指针与字符串(二)

一.《第33课 - main函数与命令行参数》


二.《第34课 - 多维数组和多维指针》    


三.《第35课 - 数组参数和指针参数分析》


四.《第36课 - 函数与指针分析》

1. 函数类型

(1)C语言中的函数有自己特定的类型,函数的类型由返回值参数类型参数个数共同决定

        比如,int add(int i, int j) 的类型为 int(int, int)

(3)C语言中通过typedef为函数类型重命名,typedef return_type name(parameter list)

        比如,typedef int f(int, int);       typedef void p(int);

2. 函数指针

(1)函数指针用于指向一个函数

(2)函数名是执行函数体的入口地址

(3)有三种定义函数指针的方法

        ① 首先使用typedef重命名函数类型,然后使用它定义函数指针变量,typedef  return_type FuncType (parameter list);    FuncType* pfunc;

              

        ② 直接定义,return_type (*pointer) (parameter list);  

      • pointer 是函数指针变量名       // pointer是一个指针变量       
      • return_type 是所指函数的返回值类型
      • parameter list 是所指函数的参数类型列表

             

        ③  使用typedef直接定义函数指针类型,然使用此类型定义函数指针变量,typedef  return_type (* PFuncType)(parameter list);   pFuncType pfunc; 

             

【函数指针的使用】

#include <stdio.h>

typedef int (FUNC)(int);    // 使用typedef重命名函数类型
int test(int i)
{
    return i * i;
}

void f()
{
    printf("Call f()...\n");
}

int main()
{
    FUNC* pt = test;    // 函数名就是函数体的入口地址

    // 前面学习数组时强调过,对于int a[10];这个数组而言,a和&a的值是相同的,但它们表示的意义不同
    // 与数组不同,函数名与对函数名取址表示的意义是相同的,即下面的f和&f含义相同
    void(*pf)() = &f;   // 老式编译器可能有这种写法;现代编译器直接使用函数名即可

    // 三者的意义相同
    printf("pf = %p\n",pf);   // pf = 0x40058d
    printf("f = %p\n",f);     // f = 0x40058d
    printf("&f = %p\n",&f);   // &f = 0x40058d


    pf();   // 使用函数指针调用函数

    (*pf)();  // 老式写法

    printf("Function pointer call: %d\n", pt(2));   // 使用函数指针调用函数

    return 0;
}

3. 回调函数

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

(2)回调机制原理

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

(3)回调机制中的调用者和被调函数互不依赖

1 #include <stdio.h>
 2 
 3 typedef int (*Weapon)(int);
 4 
 5 // 使用回调函数动态切换装备
 6 void fight(Weapon wp,int arg)
 7 {
 8     int result = 0;
 9     
10     printf("Fight boss!\n");
11     
12     result = wp(arg);
13 
14     printf("Boss loss:%d\n",result);
15 }
16 
17 // 使用刀作为武器
18 int knife(int n)
19 {
20     int ret = 0;
21     int i = 0;
22 
23     for (i=0; i< n; i++)
24     {
25         printf("Knife attack:%d\n",1);
26         ret++;
27     }
28 
29     printf("\n");
30 
31     return ret;   
32 }
33 
34 // 使用剑作为武器
35 int sword(int n)
36 {
37     int ret = 0;
38     int i = 0;
39 
40     for (i=0; i< n; i++)
41     {
42         printf("Sword attack:%d\n",5);
43         ret++;
44     }
45 
46     printf("\n");
47 
48     return ret;   
49 }
50 
51 // 使用枪作为武器
52 int gun(int n)
53 {
54     int ret = 0;
55     int i = 0;
56 
57     for (i=0; i< n; i++)
58     {
59         printf("Gun attack:%d\n",10);
60         ret++;
61     }
62 
63     printf("\n");
64 
65     return ret;   
66 }
67 
68 int main()
69 {
70     fight(knife, 3);    // 用刀砍3次
71     fight(sword, 4);    // 用剑刺4次
72     fight(gun, 5);      // 开枪5次
73 
74     return 0;
75 }
76 
77 /*  程序运行结果
78     Fight boss!
79     Knife attack:1
80     Knife attack:1
81     Knife attack:1
82 
83     Boss loss:3
84     Fight boss!
85     Sword attack:5
86     Sword attack:5
87     Sword attack:5
88     Sword attack:5
89 
90     Boss loss:4
91     Fight boss!
92     Gun attack:10
93     Gun attack:10
94     Gun attack:10
95     Gun attack:10
96     Gun attack:10
97 
98     Boss loss:5
99 */

回调函数使用示例

五.《第38课 - 动态内存分配》

1. 动态内存分配的意义

(1)C语言中的一切操作都是基于内存的

(2)变量和数组都是内存的别名

内存分配由编译器在编译期间决定

定似数组的时候必须指定数组长度

数组长度是在编译器就必须确定的

需求 : 程序运行的过程中,可能需要使用一些额外的内存空间

2. malloc 和 free

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

(2)malloc所分配的是一块连续的内存

(3)malloc以字节为单位并且不带

3. malloc/calloc/realloc三兄弟

前面已经介绍了malloc函数,这一节介绍一下calloc函数和realloc函数。

(1)calloc函数    ==>  在堆上分配一段内存空间,与malloc不同的是,calloc会将分配的内存空间初始化为0

函数原型: void *calloc(size_t  nmemb,  size_t  size);

函数参数:在程序的堆空间上分配nmemb个长度为size的连续空间,即分配的内存大小为 nmemb * size。与malloc函数不同的是,calloc会将分配的内存空间初始化为0。

函数返回值:

  • 成功返回分配的动态内存的起始地址
  • 失败返回NULL
  • 如果 nmemb 或 size 为0,返回NULL或一个指针,只不过这个指针对应的内存长度为0

malloc的m我知道是memory的意思,calloc的c不知道是什么意思。。。Google搜索了一下 What does the first “c” stand for in “calloc”?

(2)realloc函数    ==>  重新在堆上分配一块size大小的内存空间,并将ptr指向的内存空间内容拷贝到这块新的内存空间,并释放掉ptr指向的内存空间(在realloc之后就不能再使用ptr指向的内存空间了)

函数原型:void *realloc(void *ptr,  size_t  size);

函数参数:ptr为需要重新分配内存空间的指针,它是之前malloc、calloc、realloc函数的返回值;size为新的内存空间的大小

函数返回值:

  • 成功返回新分配的动态内存空间的起始地址
  • 失败返回 NULL
  • 如果传入的 ptr 为NULL,等价于 malloc(size)
  • 如果传入的ptr不等于NULL且size为0,等价于 free(ptr)

【calloc和realloc的使用】

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

#define SIZE 5

int main()
{
    int *pI = malloc(SIZE * sizeof(int));
    short *pS = calloc(SIZE, sizeof(short));

    int i;
    for (i=0; i<SIZE; i++) {
        /*
            在Linux上,malloc的打印也为0,这个只是编译器的特性,不写写依赖编译器的代码,不具有可移植性
            在Windows上,malloc的打印就是随机值
            calloc的打印在Linux和Windows上都为0
        */
        printf("pI[%d] = %d, pS[%d] = %hd\n", i, pI[i], i, pS[i]);
    }

    printf("Before: pI = %p\n", pI);  // Before: pI = 0x23b7010

    pI = realloc(pI, 2 * SIZE * sizeof(int));

    printf("After: pI = %p\n", pI);   // After: pI = 0x23b7050
                                      // 可以看到使用realloc重新申请内存后,pI的值变了

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

    return 0;
}

六.《第39课 - 程序中的三国天下》

1. 程序中的栈

1

2. 函数调用过程

1

2. 程序中的堆

1

3. 程序中的静态存储区

七.《第40课 - 程序的内存布局》

八.《第41课 - 内存操作经典问题分析一》

1. 野指针

(1)野指针指的是 指针变量的值是非法的内存地址,操作野指针会导致程序运行出错。

       野指针不是NULL指针而是指向了非法地址;NULL指针并没有危害而且很好判断和调试。

(2)C语言中无法判断一个指针所保存的地址是否合法

(3)野指针产生的原因        ※ 写程序时一定要考虑下面这些注意点

   ● 局部指针变量没有被初始化。局部指针变量存储在栈上,如果没有初始化,指针变量的值是"垃圾值"。

   ● 指针所指向的变量在指针使用之前已经被销毁了,典型的例子就是返回局部变量或者局部数组,具体原因可以参考 第39课 - 程序中的三国天下

   ● 使用 malloc() 申请的动态内存在被 free() 掉之后,仍然使用已经释放过的指针

   ● 进行了错误的指针运算,比如越界访问内存

   ● 进行了错误的强制类型转换,比如 int *p = (int *)123456;

【野指针初探】

#include <stdio.h>
#include <malloc.h>


int main()
{
    int* p1 = (int*)malloc(40);  // 分配40字节
    int* p2 = (int*)1234567;     // 错误的强制类型转换, 123456是一个非法地址
    int i = 0;

    for(i = 0;i < 40; i++)
    {
        *(p1 + i) = 40 - i;  // p1只分配了40字节,这里越界访问(40 * 4)字节
    }

    free(p1);

    for(i = 0; i < 40; i++)
    {
       p1[i] = p2[i];   // p1越界访问 + p1已经释放,p2非法内存
    }

    return 0;
}

2. 基本原则

(1)绝不返回局部变量和局部数组的地址

(2)任何变量在定以后必须初始化为 0

(3)字符数组必须以 '\0' 结束符后才能成为字符串

(4)任何使用与内存操作相关的函数必须指定长度信息

【无处不在的野指针】

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

struct Student
{
    char* name;
    int number;
};

char* func()
{
    char p[] = "Hello World!";
    return p;  // 返回局部数组
}

void del(char* p)
{
    printf("%s\n", p);
    free(p);
}

int main()
{
    struct Student s;  // 结构体没有初始化
    char* p = func();  // 返回局部的数组地址

    // strcpy(s.name, p);  // s结构体中的name是野指针

    s.number = 99; // 合法

    p = (char*)malloc(5);

    strcpy(p, "Hello World!");  // 内存越界访问

    del(p);

    return 0;
}

内存错误是实际产品开发中最常见的问题,然而绝大多数的bug都可以通过遵循基本的编程原则和规范来避免。因此,在学习的时候要牢记和理解内存操作的基本原则、目的和意义!!!


九.《第42课 - 内存操作经典问题分析二》

1. 常见内存错误

(1)结构体成员指针未初始化

(2)结构体成员指针未分配足够的内存

(3)内存分配成功但并未初始化

(4)内存操作越界

【常见内存错误1】

#include <stdio.h>
#include <malloc.h>

void test(int* p, int size) // 对内存操作时,有带长度信息
{
    int i = 0;
    for(i = 0; i<size; i++)
    {
        printf("%d\n", p[i]);
    }

    free(p);  // 不合符谁申请谁释放原则
}

void func(unsigned int size)
{
    int* p = (int*)malloc(size * sizeof(int));
    int i = 0;

    if(size % 2 != 0)  // 当size为奇数时会产生内存泄漏
    {
         return;
    }

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

    test(p, 5);  // test内部释放了p所指内存,不符合谁申请谁释放原则

    free(p);     // 重复释放p  ==>  Error in `./a.out': double free or corruption (fasttop): 0x0000000001c29010
                 // 在main函数动态申请的内存应该在main函数中释放,谁申请谁释放

    func(9);     // 参数为奇数 ==> 产生内存泄漏
    func(10);    // 正常

    return 0;
}

【常见内存错误2】

#include <stdio.h>
#include <malloc.h>

struct Demo
{
    char* p;
};

int main()
{
    struct Demo d1;  // 结构体未被初始化,d1.a是野指针
    struct Demo d2;

    char i = 0;

    for(i='a'; i< 'z'; i++)
    {
        d1.p[i] = i;  // d1.p是野指针,这里将产生段错误
    }

    d2.p = (char*)calloc(5, sizeof(char));

    printf("%s\n", d2.p);

    for(i='a'; i< 'z'; i++)
    {
        d2.p[i] = i;  // 前面使用calloc只分配了5个字节,但是 'a'~'z'是26个字节,越界访问
    }

    free(d2.p);

    return 0;
}

2. 内存操作的交通规则

(1)动态内存申请之后,应该立即检查指针值是否为NULL,防止使用NULL指针

         

(2)free指针之后必须立即赋值为NULL

         

(3)任何与内存操作相关的函数都必须带长度信息  

         

(4)malloc操作和free操作必须匹配,遵循谁申请谁释放的原则,不要跨函数释放,防止内存泄漏和多次释放

  ● 当 malloc 的次数多于 free 时,会产生内存泄漏

  ● 当 malloc 的次数少于 free 时,程序可能崩溃

   

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值