内存管理:
内存管理是指对程序运行过程中内存的分配、使用和释放进行有效的控制和管理,
以确保程序能够正确、高效地运行。
自动分配: 在函数内部定义的局部变量,其内存是自动分配和释放的。
当函数被调用时,为这些变量分配内存空间,函数执行结束后,自动释放这些内存。
静态分配:使用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 = #
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 = #
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 系统中)。