C/C++语言特性(内存管理、指针操作、编译链接机制)

内存管理:

内存管理是指对程序运行过程中内存的分配、使用和释放进行有效的控制和管理

以确保程序能够正确、高效地运行。 

自动分配: 在函数内部定义的局部变量,其内存是自动分配和释放的。

当函数被调用时,为这些变量分配内存空间,函数执行结束后,自动释放这些内存

静态分配:使用static关键字修饰的变量,在程序编译时就分配好内存,并且在程序的整个生命周期内都存在。

它的作用域取决于其定义的位置,如果在函数内部定义,其作用域仍局限于该函数内,但内存不会随着函数调用结束而释放

动态分配:通过malloc、calloc、realloc等函数在堆上动态分配内存,根据程序的运行时需求灵活地申请内存空间

在 C++ 中,还可以使用new和delete运算符进行动态内存分配和释放

malloc:仅负责分配指定大小的内存块,不会对分配的内存进行初始化

calloc:会分配指定数量和大小的内存块,并且会将分配的内存初始化为零

realloc:用于调整已经分配的内存块的大小。如果新的大小比原来大,新增的部分不会被初始化;如果新的大小比原来小,原内存块会被截断。

new:对于内置类型,如 int、char 等,new 不会进行初始化;但对于自定义类型(类、结构体),会调用其默认构造函数进行初始化。

malloc、calloc、realloc:如果内存分配失败,会返回 NULL 指针,因此在使用这些函数时,需要手动检查返回值是否为 NULL。

new:new 在分配内存失败时不会抛出异常,而是返回 nullptr(在 C++ 中,NULL 通常用 nullptr 表示)。

可以通过检查返回的指针是否为 nullptr 来判断内存分配是否成功。malloc、calloc、realloc:使用 free 函数释放分配的内存

new:使用 delete 运算符释放分配的内存;如果是使用 new[] 分配的数组,需要使用 delete[] 释放。

指针操作

指针可以直接操作内存地址

1.指针是一个变量,其值为另一个变量的内存地址。定义指针时,需要指定指针所指向的数据类型。

#include <stdio.h>

int main() {
    int num = 10;
    // 定义一个指向 int 类型的指针,并初始化为 num 的地址
    int *ptr = &num; 

    printf("num 的值: %d\n", num);
    printf("ptr 指向的值: %d\n", *ptr);
    printf("num 的地址: %p\n", &num);
    printf("ptr 存储的地址: %p\n", ptr);

    return 0;
}

在上述代码中,* 用于声明指针变量,& 是取地址运算符。ptr 指向 num 的内存地址,通过 *ptr 可以访问 num 的值。

2.指针也可以进行算术运算,包括加、减等操作。指针的算术运算与指针所指向的数据类型的大小有关。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr; // 指针指向数组首元素

    printf("第一个元素: %d\n", *ptr);
    ptr++; // 指针向后移动一个 int 类型的位置
    printf("第二个元素: %d\n", *ptr);

    return 0;
}

这里 ptr++ 使指针向后移动了一个 int 类型的大小,即 4 个字节(在常见的 32 位系统中)。

3.在 C/C++ 中,数组名在很多情况下会隐式转换为指向数组首元素的指针。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr; // 数组名转换为指向首元素的指针

    for (int i = 0; i < 5; i++) {
        printf("第 %d 个元素: %d\n", i + 1, *(ptr + i));
    }

    return 0;
}

arr 作为数组名,在赋值给 ptr 时,自动转换为指向数组首元素的指针。*(ptr + i) 等价于 arr[i]

4.指针可以作为函数的参数,允许函数直接修改调用者提供的变量的值;也可以作为函数的返回值。

#include <stdio.h>

// 指针作为函数参数
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 指针作为函数返回值
int *getArray() {
    static int arr[5] = {1, 2, 3, 4, 5};
    return arr;
}

int main() {
    int x = 10, y = 20;
    swap(&x, &y);
    printf("交换后 x: %d, y: %d\n", x, y);

    int *result = getArray();
    for (int i = 0; i < 5; i++) {
        printf("数组元素 %d: %d\n", i + 1, *(result + i));
    }

    return 0;
}

在 swap 函数中,通过指针参数直接修改了 x 和 y 的值。getArray 函数返回一个指向静态数组的指针。

5.指针可以指向结构体变量,通过指针访问结构体成员时,使用 -> 运算符。

#include <stdio.h>

typedef struct {
    int age;
    char name[20];
} Person;

int main() {
    Person p = {25, "John"};
    Person *ptr = &p;

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

    return 0;
}

ptr->name 和 ptr->age 分别访问结构体 p 的 name 和 age 成员。

6.指针可以指向另一个指针,形成多级指针。常见的是二级指针。

#include <stdio.h>

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

    printf("num 的值: %d\n", **pptr);

    return 0;
}

pptr 是一个二级指针,指向 ptr,通过 **pptr 可以访问 num 的值

7.野指针与空指针

  • 野指针:指向未分配或已释放内存的指针,使用野指针会导致未定义行为。要避免野指针,在释放指针指向的内存后,将指针置为 NULL
  • 空指针:不指向任何有效内存地址的指针,通常用 NULL 或 nullptr(C++)表示。可以在定义指针时初始化为空指针,避免成为野指针。
  • #include <stdio.h>
    
    int main() {
        int *ptr = NULL; // 初始化为空指针
    
        if (ptr == NULL) {
            printf("指针为空\n");
        }
    
        return 0;
    }

    指针操作在 C/C++ 中非常灵活,但也容易出错。使用指针时要特别注意内存管理,避免内存泄漏和野指针等问题。

编译链接机制

C/C++ 语言的编译链接机制是将源代码转换为可执行程序的重要过程

编译阶段

编译阶段是把源代码文件(.c 或 .cpp 文件)转换为目标文件(.o 或 .obj 文件)的过程,通常可细分为预处理、编译和汇编三个子阶段。

预处理
  • 主要任务:处理源代码中的预处理指令,像 #include#define#ifdef 等。
  • 具体操作
    • 展开 #include 指令包含的头文件内容,把相应头文件的代码插入到 #include 指令所在位置。
    • 进行宏替换,将 #define 定义的宏替换为其对应的值。
    • 处理条件编译指令,依据条件决定是否包含某段代码。
#include <stdio.h>
#define PI 3.14

int main() {
    double radius = 5.0;
    double area = PI * radius * radius;
    printf("圆的面积是: %f\n", area);
    return 0;
}

在预处理阶段,#include <stdio.h> 会被替换为 stdio.h 文件的实际内容,PI 会被替换为 3.14

编译
  • 主要任务:将预处理后的代码转换为汇编代码。编译器会对代码进行词法分析、语法分析、语义分析等操作,检查代码的语法和语义错误,生成中间代码,最后将中间代码转换为汇编代码。
  • 示例:经过编译,上述示例代码会被转换为类似下面的汇编代码(简化示例):
    .section .data
radius:
    .double 5.0
    .section .text
    .globl main
main:
    pushq %rbp
    movq %rsp, %rbp
    movsd radius(%rip), %xmm0
    movsd .LC0(%rip), %xmm1
    mulsd %xmm1, %xmm0
    mulsd %xmm0, %xmm1
    movq %rax, -8(%rbp)
    movsd -8(%rbp), %xmm0
    leaq .LC1(%rip), %rdi
    movl $1, %eax
    call printf
    movl $0, %eax
    popq %rbp
    ret
汇编
  • 主要任务:将汇编代码转换为机器码,生成目标文件。汇编器会把汇编指令翻译成对应的机器指令,每个汇编语句对应一条或多条机器指令。
  • 目标文件格式:目标文件通常包含代码段、数据段、符号表等信息。代码段存储程序的机器指令,数据段存储全局变量和静态变量等数据,符号表记录了程序中定义和引用的符号(如函数名、变量名等)及其地址。

链接阶段

链接阶段是将多个目标文件和库文件链接成一个可执行文件的过程,主要解决符号引用问题。

符号解析
  • 符号的定义与引用:在编译过程中,每个目标文件都会生成自己的符号表,记录该文件中定义和引用的符号。当一个目标文件引用了另一个目标文件中定义的符号时,就需要在链接阶段进行符号解析。
  • 示例:假设有两个源文件 main.c 和 add.c
// main.c
#include <stdio.h>

extern int add(int a, int b);

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

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

在 main.c 中,add 函数是一个外部引用的符号。在链接阶段,链接器会在 add.c 生成的目标文件中找到 add 函数的定义,并将 main.c 中对 add 的引用与 add.c 中 add 函数的实际地址进行关联。

重定位
  • 主要任务:在编译过程中,目标文件中的代码和数据的地址是相对于目标文件本身的相对地址。链接阶段,链接器会将各个目标文件的代码段和数据段合并,并为每个符号分配最终的内存地址,这个过程称为重定位。
  • 示例:假设 main.c 生成的目标文件中 add 函数的调用地址是一个相对地址,链接器会根据 add.c 生成的目标文件中 add 函数的实际地址,将 main.c 中 add 函数的调用地址修改为最终的内存地址。
库文件的链接
  • 静态库:静态库是一组目标文件的集合,链接时会将静态库中的目标文件直接复制到可执行文件中。静态库的优点是可执行文件独立,不依赖于外部库文件;缺点是可执行文件体积较大。静态库文件的扩展名通常为 .a(在 Linux 系统中)或 .lib(在 Windows 系统中)。
  • 动态库:动态库在链接时不会将库文件的代码复制到可执行文件中,而是在程序运行时动态加载。动态库的优点是可执行文件体积小,多个程序可以共享同一个动态库;缺点是程序运行时依赖于动态库文件。动态库文件的扩展名通常为 .so(在 Linux 系统中)或 .dll(在 Windows 系统中)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值