C语言基础13——动态内存管理。动态开辟内存函数、常见动态内存错误、C程序内存开辟讲解、柔性数组、atoi函数讲解

目录

为什么存在动态内存分配?

动态内存函数的介绍

malloc()和free()函数

calloc()函数

realloc()函数

常见的动态内存错误

经典笔试题

C/C++程序的内存开辟

柔性数组

练习

atoi()函数


为什么存在动态内存分配?

我们使用过的开辟空间的方法:

int val = 20; //在栈空间上开辟了4个字节
char arr[10] = {0}; //在栈空间上开辟10个字节的连续空间

/*
 * 上述开辟空间的方式有两个特点:
 * - 开辟出的空间大小是固定的
 * - 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
 *
 * 但是对于空间的需求,不仅仅是上述的情况。有时候需要空间的大小,要在程序运行的时候才能知道。
 * 如果数组需要的空间大小不能确定,编译时为数组开辟固定空间大小的方式就不满足使用了。
 * 此时,就需要动态开辟内存。
 */

动态内存函数的介绍

内存中数据的存储

/*
 * 栈区:存储局部变量、函数形参
 * 堆区:动态内存开辟,就在堆区中开辟。(malloc、calloc、free、realloc)
 * 静态区(也叫数据段):存储全局变量、静态变量。
 */

malloc()和free()函数

/*
 * malloc()函数
 * 函数原型:void* malloc(size_t size);
 * 作用:分配一块size字节的空间,返回一个指向这块空间开头的指针。新分配的内存块的内容未初始化,剩余的值不确定。
 * 参数:size,内存块的大小,以字节为单位。size_t是无符号整型。
 * 返回值:
 * - 内存开辟成功,则返回指向开辟空间的指针。
 * - 返回的指针类型始终为void*,可以将其转换为所需要的数据指针类型,以便使用。
 * - 内存开辟失败,则返回空指针。因此一定要对该函数的返回值做检查。
 * - 如果size为0,C语言标准没有定义,返回值取决于特定的库实现(取决于编译器)
 *   但是一般不要这样做,因为这样做的话,可能会返回一块空间的起始地址,但是大小是0。此时如果使用返回的这个地址,就是个错误的做法。
 */
/*
 * free()函数
 * 函数原型:void free(void* ptr);
 * 作用:先前通过调用calloc、malloc或realloc分配的内存块被解除分配(被释放),使其可再次被分配。
 * 参数:ptr,指向一个要释放内存的内存块。该内存块应该是之前通过调用malloc、calloc或realloc进行内存分配的。
 * 注意:
 * - 如果传递的参数是一个空指针,则不会执行任何动作。
 * - 如果ptr指向的空间不是动态开辟的,free函数的行为是未定义的。也就是说,free()只能释放动态开辟的空间
 * - free()只是释放了ptr指针指向的动态开辟的那块空间。
 *   释放完动态开辟的空间,ptr指针中存储的地址还是那块空间的,即使那块空间已经被释放掉了。
 *   所以在该空间释放后,我们要在主函数中,手动的将这个指针=NULL。避免出现使用野指针的情况。
 */

#include <stdlib.h>

int main()
{
    int arr[10]; //在栈区中分配空间

    //动态内存开辟
    int* p = (int*)malloc(10*sizeof(int)); //malloc返回void*类型指针,所以要强制转换为int*类型指针,然后用int*类型指针变量接收
    //使用开辟的空间前,看其是否开辟成功
    if(p == NULL)
    {
        perror("main"); //如果p为空指针,则说明malloc()动态开辟内存失败,输出错误信息。
        return 0;//结束方法
    }
    //使用
    int i;
    for(i=0 ; i<10 ; i++)
    {
        *(p+i) = i;
    }
    //也可以这样使用
    for(i=0 ; i<10 ; i++)
    {
        printf("%d ",p[i]); //p[i] 会被解析为*(p+i)
    }

    //使用之后回收
    free(p);
    //free()只是将我们动态开辟的空间还回去,但是p还是指向刚刚开辟的那个空间,所以我们要手动将p修改为空指针。
    p = NULL;
    return 0;
}

calloc()函数

/*
 * calloc()函数
 * 函数原型:void* calloc(size_t num,size_t size);
 * 作用:为num个元素的数组分配一块内存,每个元素的大小为字节长,并将其所有位初始化为0。
 * malloc与calloc的区别:malloc不会初始化,而calloc会将其空间内所有字节初始化为0。
 * 参数: size_t是无符号整型
 * - num:要分配的元素数
 * - size:每个元素的大小
 *
 * 返回值:
 * - 内存开辟成功,则返回指向函数分配的内存块的指针。
 * - 返回的指针类型始终为void*,可以将其转换为所需要的数据指针类型,以便使用。
 * - 内存开辟失败,则返回空指针。因此一定要对该函数的返回值做检查。
 *
 */

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

int main()
{
    int* p = (int*)calloc(10,sizeof(int));
    if(p == NULL)
    {
        return 0;
    }
    int i ;
    for(i=0 ; i<10 ; i++)
    {
        printf("%d ",*(p+i));
    }
    free(p);
    p=NULL;
    return 0;
}

realloc()函数

/*
 * 有时候我们发现申请的空间太小了/太大了,为了合理的使用内存,就需要对内存的大小做灵活的调整,使用realloc函数就可以对动态开辟的内存进行调整
 * realloc()函数 —— 使动态内存管理更加灵活。
 * 函数原型:void* realloc(void* ptr,size_t size);
 * 作用:
 * - 重新分配之前调用mallochu或calloc所分配的内存块的大小,也就是更改ptr指向的内存块的大小。
 * -
 *
 * 参数:
 * - ptr,指针指向一个要重新分配内存的内存块,该内存块之前是通过调用malloc、calloc或realloc进行分配内存的。
 *   如果为空指针,其功能类似于malloc。会分配一个新的内存块,并且函数返回一个指向其开头的指针。
 * - size:内存块的新的大小,以字节为单位。
 *   如果size为0,则返回值取决于特定的库实现:它可能是空指针或不应取消引用的其他位置。
 *
 * 返回值: 指向重新分配后的内存块的指针。
 * realloc调整内存会出现两种情况:
 * - 1、原空间之后有足够大的空间,直接在原内存空间之后直接追加空间,原来空间的数据不发生变化。
 *   此时空间块的起始地址并没有改变,返回的还是原来指向这块空间的指针,只是这块空间变大了。
 * - 2、原来空间之后没有足够大的空间了。就需要在堆空间中重新寻找一个合适大小的连续空间来使用
 *   会将原来内存块中的数据拷贝到这个空间中,然后释放掉原来的内存块。并且返回指向这块新的内存块的指针。
 * 注意:realloc()如果没有找到合适的空间来调整大小,则会返回空指针。
 *
 */

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

int main()
{
    int* p = (int*)calloc(10,sizeof(int));
    if(p == NULL)
    {
        perror("main");
        return 0;
    }
    //使用
    int i ;
    for(i=0 ; i<10 ; i++)
    {
        *(p+i) = 5;
    }
    //这里需要p指向的空间更大,需要20个int空间
    //realloc()调整空间
    int* ptr = (int*)realloc(p,20* sizeof(int));

    //int* ptr = (int*)realloc(p,2000* sizeof(int));
    //可以将空间扩容放大一点,然后加断点,调试查看p指针中存储的地址的变化。

    //这里为什么不直接使用p来接收其返回值呢?
    //因为当需要的空间过大时,realloc()函数可能找不到合适的空间来调整大小,此时就返回NULL。
    //所以用另一个指针ptr来接收,如果返回的不是空指针,则说明内存大小调整成功,将p=ptr即可。
    //如果没有找到合适的空间,扩容失败,则p中存储原来指向的空间。并不会丢失。如果时直接用p接收,当扩容失败时,就会出现p=NULL这种情况
    if(ptr != NULL)
    {
        p = ptr;
    }
    //释放
    free(p);
    p=NULL;
    return 0;
}

当realloc()的第一个参数为空指针时,其功能类似于malloc

#include <stdlib.h>
int main()
{
    //类似于malloc,直接在堆区开辟40个字节空间,并返回指向这块空间的指针。
    int* p = (int*)realloc(NULL,40);
}

常见的动态内存错误

  • 对NULL指针的解引用操作

    int main()
    {
        int* p = (int*)malloc(10000000000000000);
        int i = 0;
        //当开辟的空间过大,而没有做空指针检查时,就对其进行引用
        //内存中的容量不够,malloc就会返回空指针。对空指针进行解引用是错误的。
        for(i=0 ; i<10 ; i++)
        {
            *(p+i) = i;
        }
        return 0;
    }
    
  • 对动态开辟空间的越界访问

    #include <stdlib.h>
    
    int main()
    {
        int* p = (int*)malloc(10*sizeof(int));
        if(p == NULL)
        {
            return 1;
        }
        int i;
        //我们开辟的是10个整形空间,是40个字节,但是p是int*类型指针,每次向后走4个字节。
        //这里的i应该是<10,超过之后就会越界访问。
        for(i=0 ; i<40 ; i++)
        {
            *(p+i) = i;
        }
        free(p);
        p=NULL;
        return 0;
    }
    
  • 对非动态开辟的内存使用free释放

    #include <stdlib.h>
    
    int main()
    {
        int arr[10] = {0};  //栈区
        int* p = arr;
        //使用
        free(p);//使用free释放非动态开辟的空间。报错
        p = NULL;
        return 0;
    }
    
  • 使用free释放一块动态开辟内存的一部分

    #include <stdlib.h>
    
    int main()
    {
        int* p = (int*)malloc(10*sizeof(int));
        if(p == NULL)
        {
            return 1;
        }
        int i = 0;
        for(i=0 ; i<5 ; i++)
        {
            //p向后走了5个位置,指向第六个元素。
            *p++ = i;
        }
        //释放时只释放了一半,不能从中间开始释放,必须是从开头释放。
        //并且指向一个内存块的指针向后走之后,除非再走回开头,否则找不到这个内存块的起始位置。
        //当没人记得这块内存空间的起始位置时,这块空间可能就永远找不到了。这里就存在内存泄漏的问题
        free(p);
        p=NULL;
        return 0;
    }
    
  • 对同一块动态内存多次释放

    #include <stdlib.h>
    
    int main()
    {
        int* p =(int*)malloc(100);
        //使用
        //释放
        free(p);
    
        //避免出现多次释放同一块内存空间的方法:在释放之后将这个指针指为NULL
        //p=NULL;
        //再次释放时,free(p);也就是free(NULL);传了一个空指针,什么都不会发生
    
        //对同一块动态开辟的空间,再次释放。
        free(p);
        return 0;
    }
    

经典笔试题

  • 程序执行会有什么结果?

    # include <stdlib.h>
    
    void GetMemory(char* p)
    {
        //接收到str后,将其拷贝一份给了局部变量p
        //将动态开辟的空间的位置返回给p
        p = (char*)malloc(100);
        //使用
        //动态开辟的空间没有释放,会发生内存泄漏。
        //函数调用结束:局部变量p被释放掉,并且其指向的动态内存空间如果没有释放,则只能等待程序结束才会释放。
    }
    
    void Test(void)
    {
        //定义char类型指针变量
        char* str = NULL;
        //传值调用,这里是把指针变量str穿了过去,并不是str的地址。
        GetMemory(str);
        //在该函数调用结束之后,str还是空指针。并且函数中动态开辟的空间没有释放,并且找不到这块空间。发生内存泄漏
        //将一个字符串拷贝到空指针中去,拷贝失败
        strcpy(str,"hello world");
        printf(str);
    }
    int main()
    {
        Test();
        return 0;
    }
    

    改正,方法1:

    # include <stdlib.h>
    
    char* GetMemory(char* p)
    {
        p = (char*)malloc(100);
        return p;
    }
    
    void Test(void)
    {
        char* str = NULL;
        //接收返回的指针,也就是说,此时的str指向GetMemory中动态开辟的100个字节空间
        str = GetMemory(str);
    
        //使用
        //拷贝
        strcpy(str,"hello world");
        //输出
        //这种打印方式怎么跟之前不一样呢?之前的是:printf("%s",str);
        //char* p = "hello world";  是把h的地址保存到了char*类型指针变量p中
        //再回想我们打印一个字符串的时候:printf("hello world"),实际上是把指向h的指针传递了进去。
        //而这里str也是一个字符指针,所以我们将其传递进去,是可以直接打印的。遇到字符串结束符\0停止打印。
        printf(str);
    
        //释放
        free(str);
        str=NULL;
    }
    int main()
    {
        Test();
        return 0;
    }
    

    方法2:

    # include <stdlib.h>
    
    void GetMemory(char** p)
    {
        //*p解引用找到str,将动态开辟的空间的地址保存到str中
        *p = (char*)malloc(100);
    }
    
    void Test(void)
    {
        char* str = NULL;
        //传址调用,str是char*类型指针变量,所以&str需要用char**二级指针来接收
        //因为是传址,所以str现在指向动态开辟的100个字节空间
        GetMemory(&str);
    
        //使用
        strcpy(str,"hello world");
        printf(str);
    
        //释放
        free(str);
        str=NULL;
    }
    int main()
    {
        Test();
        return 0;
    }
    
  • 返回栈空间地址问题

    #include <stdio.h>
    
    char* GetMemory(void)
    {
        //char类型数组p
        char p[] = "hello world";
        //将数组名p返回。
        return p;
    
        //这是返回栈空间地址的问题,其局部变量出了所在范围,就会自动销毁。
        //堆中的空间就不一样,比如我们动态开辟的空间,就是在堆中开辟。
        //堆中存储的空间除非我们手动释放,否则只能等待main函数结束,也就是退出程序后才会释放。
    }
    void Test(void)
    {
        char* str = NULL;
        //GetMemory()函数调用结束后,str现在存储的是指向GetMemory()中char类型数组p的指针
        //但是p数组是局部变量,GetMemory()函数调用结束后,局部变量释放掉了。
        //也就是说str指向的是一个已经释放掉的空间
        str = GetMemory();
        printf(str);
    }
    
    int main()
    {
        Test();
        return 0;
    }
    

    以下程序有什么问题?

    //同样还是返回局部变量地址问题:返回了int类型变量x的地址,但是x在f1()函数调用结束之后就销毁掉了,此时如果在主函数中,用一个指针来接收这个返回值,再去访问,就是:访问已经释的空间,这种内存访问是非法的。
    int* f1(void)
    {
        int x =10;
        return &x;
    }
    

    以下程序问题

    int* f2(void)
    {
        //定义一个int*类型指针变量ptr,并没有初始化。ptr没有指向的地址。
        int* ptr;
        //解引用ptr,为其赋值10。注意:ptr并没有初始化,解引用未初始化的指针,是野指针问题。
        *ptr =10;
        return ptr;
    }
    
  • 动态开辟的空间,使用之后一定要释放:free();

    #include <stdio.h>
    
    void GetMemory(char** p,int num)
    {
        *p =(char*)malloc(num);
    }
    void Test()
    {
        char *str = NULL;
        GetMemory(&str,100);
        strcpy(str,"hello");
        printf(str);
    
        //使用之后要释放
        //free(str);
        //str = NULL;
        
    }
    int main()
    {
        Test();
        return 0;
    }
    
  • 释放掉动态开辟的空间后,指向这块动态空间的指针最好也置为空

    #include <stdio.h>
    
    void Test(void)
    {
        //str指向动态开辟的100个字节空间
        char* str = (char*)malloc(100);
        //将"hello"拷贝到str指向的中间中
        strcpy(str,"hello");
        //释放str指向的动态开辟的空间
        free(str);
        //*改正:str = NULL; 手动将str置为空。
        //此时虽然动态开辟的空间释放掉了,但是str还指向这块空间,所以str肯定是不为空的。
        //然后进行操作,就是访问已经释放掉的空间,是非法访问。
        //所以我们再free(str)释放掉动态开辟的空间后,一定要手动将str置为空:str = NULL;
        if(str != NULL)
        {
            strcpy(str,"world");
            printf(str);
        }
    }
    int main()
    {
        Test();
        return 0;
    }
    

C/C++程序的内存开辟

C/C++程序内存分配的几个区域

/*
 * 栈区(stack)
 * - 在调用函数时,函数内部局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。
 * - 栈内存分配运算内置于处理器的指令集上,效率很高,但是分配的内存容量有限。
 *   栈区主要存放:运行函数而分配的局部变量、函数形参、返回的数据、返回的地址等。
 * - 局部变量是栈区分配空间的。栈区的特点:栈上面创建的变量,出了其作用域就会销毁。
 *   被static修饰的变量存放在数据段/静态区,在数据段上创建的变量,直到程序结束才销毁。也就是说其生命周期变长。
 *
 * 堆区(heap)
 * - 一般由程序员分配释放;若程序员不释放,程序结束时可能由操作系统回收。分配方式类似于链表。
 *
 * 数据段/静态区(static)
 * - 存放全局变量、静态数据。程序结束后由系统释放。
 *
 * 代码段
 * - 存放函数体(类成员函数和全局函数)的二进制代码。
 */

在这里插入图片描述

柔性数组

C99中,引入了柔性数组的概念。什么是柔性数组呢?结构体中的最后一个元素允许是未知大小的数组,这个数组就被叫做“柔性数组”成员

  • 柔性数组的定义

    struct S
    {
        int n;
        //柔性数组成员
        int arr[]; //柔性数组的大小是未知的。
    
        //有些编译器是这样编写。
        //int arr[0];
    };
    
  • 柔性数组的特点

    /*
     * 柔性数组的特点:
     * - 结构体中的柔性数组前面必须至少包含一个其他成员。
     * - sizeof返回的包含柔性数组成员的结构体的大小,没有计算柔性数组所占的大小。
     * - 包含柔性数组成员的结构,要用malloc()函数进行动态内存分配,分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
     */
    
    struct S
    {
        int n;
        int arr[]; //柔性数组成员
    };
    
    int main()
    {
        printf("%d",sizeof(struct S));//4
        //动态分配内存
        //如果期望柔性数组大小是10个整型,则分配空间的大小:结构体的大小+10个整形大小
        struct S* ps = (struct S*)malloc(sizeof(struct S)+10*sizeof(int));
        return 0;
    }
    
  • 柔性数组的使用

    #include <stdio.h>
    
    struct S
    {
        int n;
        int arr[]; //柔性数组成员
    };
    
    int main()
    {
        printf("%d",sizeof(struct S));//4
        //如果期望柔性数组大小是10个整型,则分配空间的大小:结构体的大小+10个整形大小
        struct S* ps = (struct S*)malloc(sizeof(struct S)+10*sizeof(int));
    
        //使用
        ps->n = 10;
        int i;
        for(i=0 ; i<10 ; i++)
        {
            ps->arr[i] = i;
        }
    
        //扩容:数组大小从10变成20个整型。
        struct S* ptr = (struct S*)realloc(ps,sizeof(struct S)+20* sizeof(int));
        if(ptr != NULL)
        {
            ps = ptr;
        }
        //使用
    
        //释放
        free(ps);
        ps = NULL;
        return 0;
    }
    
  • 我们使用一个int*类型指针arr来代替数组,看是否能够代替柔性数组

    #include <stdlib.h>
    
    struct S
    {
        int n;
        int* arr; //定义一个int*类型指针
    };
    
    int main()
    {
        printf("%d",sizeof(struct S)); //16
        //为结构体动态开辟空间。这个结构体大小是可变的。
        struct S* ps = (struct S*)malloc(sizeof(struct S));
        if(ps == NULL)
        {
            return 1;
        }
        //但是指针并没有初始化,也就是说我们要为int*开辟一个动态内存空间,才能使得arr长度可变。
        ps->arr = (int*)malloc(10*sizeof(int));
        if(ps->arr == NULL)
        {
            return 1;
        }
        //使用
        int i;
        for(i=0 ; i<10 ; i++)
        {
            ps->arr[i] = i;
        }
        //扩容
        int * ptr = (int*)realloc(ps->arr,20*sizeof(int));
        if(ptr != NULL)
        {
            ps->arr = ptr;
        }
        //使用
        
        //释放
        //先释放为arr指针开辟的空间。不能显示放ps指向的空间,ps的空间释放了,就不能使用ps去找arr了。所以先释放ps->arr
        free(ps->arr);
        ps->arr = NULL;
        free(ps);
        ps = NULL;
        return 0;
    }
    

    因为结构是固定大小,所以也可以将结构改为静态内存开辟,指针再用动态开辟的方式

    /*
     * - 用静态开辟的方式也可以实现相同的效果。以上使用动态开辟的原因是因为柔性数组的所有数据都是动态开辟的。
     * - 思考:如果是一块空间,静态开辟和动态开辟都可以实现相同的效果,使用哪种方式更好?
     *   需要根据需求来看,栈空间是有限的,如果是大量的数据肯定是要去堆上开辟的。
     */
    #include <stdlib.h>
    
    struct S
    {
        int n;
        int* arr; //定义一个int*类型指针
    };
    
    int main()
    {
        printf("%d",sizeof(struct S)); //16
        //因为结构体其中存储的是两个固定大小的类型,所以可以创建一个结构体变量,然后取其地址赋给结构体指针ps。
        struct S s1 = {0};
        struct S* ps = &s1;
        //结构体中的int*类型变量arr要达到数组的效果,也就是说我们要为int*开辟一个动态内存空间.
        ps->arr = (int*)malloc(10*sizeof(int));
        if(ps->arr == NULL)
        {
            return 1;
        }
        //使用
        ps->n = 10;
        int i;
        for(i=0 ; i<10 ; i++)
        {
            ps->arr[i] = i;
        }
        //扩容
        int * ptr = (int*)realloc(ps->arr,20*sizeof(int));
        if(ptr != NULL)
        {
            ps->arr = ptr;
        }
        //使用
    
        //释放
        free(ps->arr);
        ps->arr = NULL;
        return 0;
    }
    
  • 相对于以上那种方式,柔性数组的优势在哪里

    /*
     * 在堆区中申请空间后,堆区中可能有很多已经申请了的内存块。
     * - 内存块与内存块之间留下的没有被使用的空间,被叫做:内存碎片。这些内存碎片不算大又不算小,被再次利用的可能性是很低的。
     *   如果malloc()申请空间的次数越多,则堆区中的内存碎片越多,内存的利用率就会降低。
     *   所以malloc()动态开辟空间的次数不能太多,多了之后会增加内存虽贫,也会降低效率。
     *
     * - 对内存的使用:如果大量频繁的使用内存块,一会开辟一会释放、一会开辟完又调整大小重新分配内存空间。第一会带来内存碎片,第二会降低效率
     * - 所以在软件设计中,有个概念叫内存池。就是我们在内存中,为我们运行的程序直接申请出一大块内存,这一大块空间就叫内存池。
     *   当程序中有需要使用内存空间的时候,就不需要申请了,直接在这个内存池中拿就可以了,用完之后再还给这块内存池。
     *   这样内存统一进行管理:统一申请,同一归还。这样内存维护效率会更高。
     *
     * 局部性原理:一个是空间局部性,一个是时间局部性。
     * - 空间局部性:当我们使用一块内存时,接下来如果又有要使用内存的地方,则80%的可能性是使用这块内存周边的内存。这样内存大概是连续的,效率比较高。
     *   不可能说是,这里使用一下,然后跳到一个很远的地方去使用,这样效率会很低。
     * 回到刚刚的例子:
     * - 如果是柔性数组,其所在结构体如果开辟空间,int类型变量n的空间之后,就是柔性数组的空间。
     * - 而如果是int*,则开辟的空间:先是int类型变量n与int*类型指针变量arr的空间;然后是动态为arr开辟的空间。
     *   变量arr所在空间与arr指向的空间不连续,之间的距离相对就较长。
     *   而柔性数组的空间,就在其所在结构体的空间内,是连续的。
     * - 所以相对来说,柔性数组的效率更高。
     */
    
  • 柔性数组拓展阅读

    https://coolshell.cn/articles/11377.html
    

练习

  • 关于动态函数malloc

    malloc函数向内存中申请一块连续的空间,并返回起始地址

    malloc申请空间失败,返回NULL指针。

    malloc不可以向内存申请0字节的空间。如果size为0,C语言标准没有定义,返回值取决于特定的库实现(取决于编译器)。但是一般不要这样做,因为这样做的话,可能会返回一块空间的起始地址,但是大小是0。此时如果使用返回的这个地址,就是个错误的做法。

    malloc申请的空间,使用后需要进行释放。

    动态申请的空间在内存中的堆区中。

  • malloc与calloc的区别

    malloc函数与calloc函数的功能类似,都是申请一块连续的空间。

    malloc函数申请的空间不初始化,而calloc申请的空间会被初始化为0

    realloc函数可以动态调整申请内存的大小,可大可小。

    动态开辟的空间,使用后,都必须调用free函数进行释放。

  • 一个数组中只有两个数字是出现一次,剩下的数字都是出现两次。编写函数,找出这连个只出现一次的数字

    //按位异或 ^  。 如果对应位相同(如都为1,或都为0),则为0;如果对应位数不相同(一个是1一个是0 或 1个是0一个是1),则为1。
    //一个数组中只有两个数字是出现一次,其他数字都出现了两次
    #include <stdio.h>
    void Find(int arr[],int sz)
    {
        //1、将这一组的所有数字都进行异或
        int i;
        int ret = 0; //初始值为0,0与哪个数字异或,就输出哪个数字。
        for(i=0 ; i<sz ; i++)
        {
            ret ^= arr[i];
        }
    
    
        //2、计算ret的哪一位为1。  ret = 3  二进制:011
        int pos = 0;
        for(i=0 ; i<32 ; i++)
        {
            //按位与  & 。都为1,才为1
            if(((ret >> i) & 1) == 1)
            {
                pos = i;
                break;
            }
        }
    
        //把从低位到高位的第pos位相同的放到一组。也就是说:第pos位,为1的放一组,为0的放一组。
        int num1 = 0;
        int num2 = 0;
        for(i=0 ; i<sz ; i++)
        {
            //将该元素直接右移pos位,然后与1进行按位与运算。
            if( ( ( arr[i]>>pos) & 1 ) == 1)
            {   //如果该数的pos位是1,则与num1取余
                num1 ^= arr[i];
            }
            else
            {   //如果该数的pos位是0,则与num2取余
                num2 ^= arr[i];
            }
        }
    
        //执行结束后,num1与num2就是两个只出现过一次的数
        printf("%d %d\n",num1,num2);
    
    }
    
    int main()
    {
        int arr[] = {1,2,3,4,5,6,1,2,3,4};
        //进行分组:
        //1 3 1 3 5
        //2 4 2 4 6
        //5和6要在不同的组。
    
        //进异或:1^2^3^4^5^6^1^2^3^4 = 5^6 != 0 ,因为有两个1、2、3、4,相同但数进行异或就为0。
        //101   5的二进制
        //110   6的二进制
        //011   5^6的结果
    
        /*
         * - 我们将这一组数组进行分组:所有的数字的二进制,最后一位为1的放在一组,最后一位为0的放在一组。
         *   这样5和6肯定不在一组,因为这两个数字的最后一位不同
         * - 就成为:
         *   最后一位为1的:1 3 1 3 5
         *   最后一位为0的:2 4 2 4 6
         * - 因为5和6的倒数第二位也不同,所以我们也可以按照倒数第二位进行分组
         *   倒数第二位为1的:2 3 2 3 6
         *   倒数第二位为0的:1 4 1 4 5
         *
         * - 思路:将这一组数字进行异或,看异或的结果哪一列为1,为1就说明这两个不同的数在这个位置上的位不同。
         *   就比如这组数字,有两列都为1,那我们选择其中任何一列作为区分将这一组数字分为两组即可。
         */
    
        //找出这两个只出现一次的数字
        int sz = sizeof(arr)/sizeof(arr[0]);
        Find(arr,sz);
    
        /*
         * 如果需要将两个不同的数字带回main函数。
         * - 则需要定义两个变量 int x=0; int y=0; 然后为函数添加两个int*类型参数:find(arr,sz,&x,&y);
         *   在函数中的最后,将int指针解引用,*指针=数字即可。
         *
         * - 也可以定义一个数组int num[2],函数:find(arr,sz,num);
         *   在函数的最后将计算好的数字放入num[0]和num[1]即可。
         *
         * - 注意这里传入的是x、y、num数组首元素的地址。这样的参数叫做返回型参数。
         *   将我们需要的数据,通过定义的参数返回回来。
         *   注意其参数类型肯定是指针类型,因为只有传地址,才能把数据带回来。
         */
        return 0;
    }
    

atoi()函数

推荐阅读:《🗡剑指offer》

  • 函数使用

    /*
     * atoi()函数
     * 函数原型:int atoi(const char* str);
     * 作用:把str指向的字符串,将其内容解释为整数,该整数作为int类型返回。
     * - 该函数首先要丢弃尽可能多的空白字符,根据需要,直到找到第一个非空白字符。
     *   然后从这个字符开始,采用可选的初始加号或减号,后跟尽可能多的十进制数字,并将它们解释为一个数值。
     * - 字符串可以在构成整数的字符之后包含其他字符,这些字符将被忽略,并且对该函数的行为没有影响。
     * - 如果str中第一个非空白字符不是有效的整数,或者由于str为空或仅包含空白租房而不存在此类序列,则不执转换并返回0。
     *
     * 参数:str,要转换为整数的字符串。
     * 返回值:
     * - 成功时,函数将转换后的整数作为int值返回。
     * - 如果转换后的值超出int的可表示范围,则会导致未定义的行为。
     */
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        //因为常量字符串不可以修改,所以最好使用const修饰*p
        const char* p ="-1223  ";
        int ret = atoi(p);
        printf("%d\n",ret);//-1234
        return 0;
    }
    
    
  • 模拟实现

    #include <stdio.h>
    #include <ctype.h>
    #include <limits.h>
    
    //因为出现异常的情况很多,而正清情况就一种:读取到字符串结束符\0就结束。
    //所以我们定义一个枚举,其值有两种:一个非法,一个合法
    enum State
    {
        INVALID,  //非法:0
        VALID     //合法:1
    };
    //定义为全局变量。因为合法的情况就一种,所以将默认值定义为非法。当合法时,将这个值置为合法即可
    enum State state = INVALID;
    int my_atoi(const char* s)
    {
        //flag用来标记数字是正数还负数。默认设置为正数
        int flag = 1;
        //如果s是空指针/空字符串,则返回0
        if(s == NULL || *s == '\0')
        {
            return 0;
        }
    
        //检测字符串中是否有空字符,如果有就跳过
        //isspace用来检查,一个字符是否为空字符,如果是则返回1,如果不是则返回0。
        while(isspace(*s))
        {
            s++;
        }
    
        //检查第一个非空字符是否为正负数。
        if(*s == '+')
        {
            s++;
        }
        else if(*s == '-')
        {
            flag = -1;
            s++;
        }
    
        //处理数字字符的转换
        //n用来存储转换的数字。因为不能超过int类型,所以我们将其定义为long long
        long long n = 0;
        //如果当前是数字字符,则计算值。
        while(isdigit(*s))
        {
            //*s是数字字符,-'0' 得到的就是*s对应的数字。如:'2'-'0'就得到数字2。
            n = n*10 + (*s - '0');
            s++;
        }
    
        //在这里计算判断,看n是否超出了int的长度。如果超出了,则减去一个int范围
        // int范围也就是0~2147483647,共2147483648个数,-1~-2147483648,也是2147483648个数
        //这里使用INT_MAX以及INT_MIN会溢出,所以我们直接算出这个值:2147483648*2=4294967296
        while( n > 4294967296)
        {
            n = n - 4294967296;
        }
    
        //此时的n
        n = flag*n;
    
        //此时,n肯定是在int范围内了。判断a是否超出了正数/负数范围,如果超出了,则对其进行处理
        //我们不支持其超出范围,但还是因该返回值,但不是正常情况。所以不设置state,返回后,其还是非法情况。
        //如果n是正数,则看其是否超出正数范围。如果超出,则进行转换。
        if( n > INT_MAX)
        {
            n = -1*n+2*(n-INT_MAX-1);
            return (int)n;
        }
        //如果n是负数,则看其是否超出负数范围。如果超出,则进行转换。
        else if (n < INT_MIN)
        {
            n = -1*n+2*(n-INT_MIN);
            return (int)n;
        }
    
        //进行到这里,说明碰见了非数字字符。可能是\0正常结束,也可能是碰见字母字符。
        if(*s == '\0')
        {
            //如果是读取到字符串结尾,则说明是正常的。
            state = VALID;
        }
        return (int)n;
    }
    
    int main()
    {
        //因为常量字符串不可以修改,所以最好使用const修饰*p
        const char* p ="+988294967296";
    
        //传入函数的可能是
        //1.一个空指针
        //2.空字符串  ""
        //3.遇到了非数字字符
        //4.超出int范围
    
        int ret = my_atoi(p);
        if(state)
        {
            printf("程序遇到文件结束符正常执行:%d\n",ret);
        }
        else
        {
            printf("程序遇到异常,结果仅供参考:%d\n",ret);
        }
        //对比
        ret = atoi(p);
        printf("库函数 atoi()计算结果对比:%d\n",ret);
        return 0;
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值