C语言(深度解剖版)--开发人员使用

 内存分配

内存池的概念:

操作系统把一些内存放入内存池供动态分配使用,没有了系统再放进去。malloc的空间都在内存池里,释放了也会返回到内存池供后续使用,减少了系统调用的次数,直到程序结束才回收。所以free后内存占用没有明显减少是因为这个,作用:提高性能、效率

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

int main() {
    // 从内存池中分配100字节
    char *ptr = (char*)malloc(100);
    if(ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    
    // 使用内存
    strcpy(ptr, "示例字符串");
    printf("内容: %s\n", ptr);
    
    // 释放内存,返回内存池
    free(ptr);
    ptr = NULL; // 防止悬挂指针
    
    return 0;
}
注意事项
  • 分配的成功或失败要判断==NULL​​​​​​​
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr = (int*)malloc(10 * sizeof(int));
    if(arr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    // 使用内存
    free(arr);
    return 0;
}
  • 保证malloc和free成对使用
  • 申请和释放次数匹配,将释放后赋为NULL, 避免悬挂指针
  • 内存分配的常见错误
  • 3个错误(同一个动态空间多次free。泄露。只释放一部分。返回栈地址。) free释放的是指针指向的空间,不是释放指针变量的地址,所以最后赋空指针
//只释放一部分错误实例
#include <stdio.h>
#include <stdlib.h>

int main() {
    struct s {
        char *name;
        int age;
    } *p;

    p = (struct s*)malloc(sizeof(struct s));
    if(p == NULL) return 1;
    p->name = (char*)malloc(50);
    if(p->name == NULL) return 1;

    strcpy(p->name, "Alice");
    p->age = 30;

    // 只释放结构体本身,未释放name成员
    free(p);
    p = NULL;
    return 0;
}

//返回栈地址错误示例
#include <stdio.h>
#include <stdlib.h>

int* invalid_return() {
    int a = 10;
    return &a; // 错误: 返回栈地址
}

int main() {
    int *ptr = invalid_return();
    printf("值: %d\n", *ptr); // 未定义行为
    return 0;
}
  • 分配0字节空间会怎么样:可以分配但不能用,尺子上的刻度,2个点才能连成线

4.动态分配结构体时指针成员要单独开辟和释放空间(必须先释放成员空间再释放结构体空间

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

struct Person {
    char *name;
    int age;
};

int main() {
    struct Person *p = (struct Person*)malloc(sizeof(struct Person));
    if(p == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 单独为指针成员分配内存
    p->name = (char*)malloc(50);
    if(p->name == NULL) {
        free(p);
        return 1;
    }

    strcpy(p->name, "张三");
    p->age = 25;

    printf("姓名: %s, 年龄: %d\n", p->name, p->age);

    // 释放指针成员空间
    free(p->name);
    p->name = NULL;
    // 释放结构体空间
    free(p);
    p = NULL;

    return 0;
}

总结:动态开辟内存时结构体要为指针成员单独分配空间

结构体本身的成员是有被编译器分配内存的,给这个结构体开辟动态内存只是换了一个空间而已,但成员是指针,要明白给指针开辟内存和给指针的指向分配空间是两回事。

错误例子

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

struct s {
    char *name; // 没有初始化
} p;

int main(){
    p = (struct s*)malloc(40);
    strcpy(p->name, "lc"); // 未分配name的指向空间,name仍然是野指针
    return 0;
}

纠正:

struct s {
    char *name;
} p;

int main(){
    p = (struct s*)malloc(sizeof(struct s));
    if(p == NULL) return 1;
    p->name = (char*)malloc(50);
    if(p->name == NULL) {
        free(p);
        return 1;
    }
    strcpy(p->name, "lc");
    printf("姓名: %s\n", p->name);

    // 释放内存
    free(p->name);
    p->name = NULL;
    free(p);
    p = NULL;
    return 0;
}

内存其他和扩展:

资源泄露:

  • 包括内存泄露和句柄泄露。句柄泄露就是关于文件之类的泄露的(还有操作系统、数据库连接、进程、线程等)。文件打开了没有关闭,创建使用完后没有销毁。
  • 防止内存耗尽
  • 其实内存不可能耗尽。栈虽然效率高但容量有限。少用静态区的变量


 指针:

1.高级指针的应用场景

指针数组

一般不用于存放任意类型的数据的地址,一般用于结构体
 #include <stdio.h>

    struct Student {
        char name[50];
        int age;
    };

    int main() {
        struct Student s1 = {"张三", 20};
        struct Student s2 = {"李四", 22};
        struct Student s3 = {"王五", 21};

        struct Student *ptrArray[3] = {&s1, &s2, &s3};

        for(int i = 0; i < 3; i++) {
            printf("姓名: %s, 年龄: %d\n", ptrArray[i]->name, ptrArray[i]->age);
        }

        return 0;
    }

数组指针

 #include <stdio.h>

    int main() {
        int arr[3][4] = {0};
        int (*p)[4] = arr; // p指向一个包含4个int的数组

        for(int i = 0; i < 3; i++) {
            for(int j = 0; j < 4; j++) {
                p[i][j] = i * 4 + j;
                printf("%d ", p[i][j]);
            }
            printf("\n");
        }

        return 0;
    }

函数指针

 #include <stdio.h>

    int add(int a, int b) {
        return a + b;
    }

    int main() {
        int (*func_ptr)(int, int) = add;
        int result = func_ptr(5, 3);
        printf("结果: %d\n", result);
        return 0;
    }

函数指针数组

 #include <stdio.h>

    int add(int a, int b) {
        return a + b;
    }

    int subtract(int a, int b) {
        return a - b;
    }

    int main() {
        int (*func_arr[2])(int, int) = {add, subtract};
        printf("加法: %d\n", func_arr[0](10, 5));
        printf("减法: %d\n", func_arr[1](10, 5));
        return 0;
    }

二级指针

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

    int main() {
        int **dp = (int**)malloc(2 * sizeof(int*));
        if(dp == NULL) return 1;

        for(int i = 0; i < 2; i++) {
            dp[i] = (int*)malloc(3 * sizeof(int));
            if(dp[i] == NULL) return 1;
            for(int j = 0; j < 3; j++) {
                dp[i][j] = i * 3 + j;
            }
        }

        for(int i = 0; i < 2; i++) {
            for(int j = 0; j < 3; j++) {
                printf("%d ", dp[i][j]);
            }
            printf("\n");
        }

        // 释放内存
        for(int i = 0; i < 2; i++) {
            free(dp[i]);
        }
        free(dp);

        return 0;
    }
2.指针指向字符串的含义

是字符串字面量

 #include <stdio.h>

    int main() {
        const char *str = "Hello, World!"; // 字符串字面量
        printf("字符串: %s\n", str);
        return 0;
    }

字符数组是有内存空间分配的连续的,所以是一个个字符组成的数组自然是变量

而字符指针只是指向字符串,没有分配空间,所以这个字符串是已经在编译器分配好的常量

重点:3.指针步长及指针数组的移动

指针移动的步长取决于类型的大小。 指针类型的大小决定指针步长和指针解引用访问空间的大小

#include <stdio.h>

int main() {
    int a[3][4] = {
  {0,1,2,3}, {4,5,6,7}, {8,9,10,11}};
    int (*p)[4] = a; // 指向二维数组中的一维数组

    p = p + 1; // 移动到第二行
    printf("第二行第一个元素: %d\n", (*p)[0]); // 输出4

    struct MyStruct {
        int x;
        double y;
    } s[2] = {
  {1, 2.0}, {3, 4.0}};
    struct MyStruct *sp = s;

    sp = sp + 1; // 移动到第二个结构体
    printf("结构体y: %.1f\n", sp->y); // 输出4.0

    return 0;
}

指针数组移动:因为数组也是类型,所以按类型来走步长

int a[3][4不可省略] = {
  {0}};
int (*p)[4不可省略] = &a;
p+1等价于a[0]+1*[4]等于a[1]跳了4个元素,即到了第二行

结构体指针+1则是跳过整个结构体

4.解引用含义

解引用后控制权就不再是指针,而是指向的变量

-> 等价于 (*).

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr = &num;

    printf("指针指向的值: %d\n", *ptr); // 解引用访问值

    *ptr = 20; // 通过解引用修改值
    printf("修改后的值: %d\n", num);

    // 等价于 (*ptr).Var
    typedef struct {
        int Var;
    } MyStruct;

    MyStruct s;
    MyStruct *sp = &s;
    sp->Var = 30; // 等价于 (*sp).Var = 30;
    printf("结构体Var: %d\n", s.Var);

    return 0;
}
5.传值和传址的区别

传值:临时拷贝

传址:间接达到本体效果,引用才是本体

#include <stdio.h>

// 值传递
void increment(int a) {
    a++;
}

// 地址传递
void increment_address(int *a) {
    (*a)++;
}

int main() {
    int x = 5;
    increment(x);
    printf("值传递后x: %d\n", x); // 输出5

    increment_address(&x);
    printf("地址传递后x: %d\n", x); // 输出6

    return 0;
}
6.void指针的含义和规则
  • void能接受任何,但别人接受不了void
  • void*不是通用指针类型,是保存其类型的指针
  • void指针不能++——、解引用,除非强转
  • void无类型,不知道变量的内存放在哪里,所以不能创建变量实例,void代表一种抽象的类型,理解了面向对象的抽象基类的概念就容易理解void了
#include <stdio.h>

int main() {
    int a = 10;
    double b = 3.14;
    void *ptr;

    ptr = &a; // 可以指向任何类型
    printf("整数: %d\n", *(int*)ptr);

    ptr = &b;
    printf("浮点数: %.2f\n", *(double*)ptr);

    // void指针不能直接解引用或进行算术运算
    // printf("%d\n", *ptr); // 错误
    // ptr++; // 错误

    void a;//也是是错的

    return 0;
}
 
7. 0指针的作用和规则
  • NULL只能用于指针,相当于 (void*)0,有时候也是宏定义的0,所以严格意义上可能会混淆整型0和指针类型。
  • 尺子上刻度为0的地址,因此不能被使用
  • 0地址是空指针。

作用:初始化为0,不指向任何,可防止悬挂或野指针

#include <stdio.h>

int main() {
    int *ptr = NULL; // 初始化为空指针

    if(ptr == NULL) {
        printf("指针未指向任何地址\n");
    }

    // 避免悬挂指针
    // ptr = (int*)0x11; // 需要确保地址合法
    return 0;
}

8.野指针和悬挂指针

野指针:指向和使用未知的未定义的。发生在(未初始化,指针越界访问)

悬挂指针:指向和使用已经销毁的。相当于宾馆房卡过期了还赖在宾馆住,发生在(指针被free后没赋予空指针,函数返回栈地址

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

// 野指针
void wild_pointer() {
    int *ptr; // 未初始化
    *ptr = 10; // 未定义行为
}

// 悬挂指针
void suspended_pointer() {
    int *ptr = (int*)malloc(sizeof(int));
    if(ptr == NULL) return;
    *ptr = 20;
    free(ptr);
    // 悬挂指针,没有赋NULL
    // printf("%d\n", *ptr); // 未定义行为
}

int main() {
    // wild_pointer(); // 会导致程序崩溃
    suspended_pointer();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值