全局变量 global variable

在函数之外定义,内存空间在执行程序之前就进行了分配,并且保存直到程序结束
保存全局变量的时间是 compile time.

本地变量 local variable

在函数中定义,本地变量的存储空间是在运行时期 run time 期间定义的

动态内存分配

new 关键字

我们通过 new 进行新的的空间申请

1
int * ip = new int;

如果我们想同时初始化内存值,则

1
int * ip = new int(5);

对于类的 初始化:我们会调用类的构造函数进行返回;
对于数组的初始化,我们使用语法

1
int *ip = new int[5]; // 这个 5 可以是一般变量 n

申请两次内存

![[Pasted image 20240328150603.png]]

delete 关键字

通过 deletej’jjj 释放内存空间,也即是说这会消除空间中的内容
注:

  • 不能同时 free 或者 delete 同一块内存, 也不能 delete 一般的非动态数据内容,但是不同指针可以delete不是其申请的空间头
  • 编译器对于没有清空申请内存空间的行为不会 warning!
  • 编译器对于内存删除的检查并不会进入完全的内存空间,真正的释放报错是因为运行时候的报错

删除数组

1
delete[] ip;

在申请数组空间的时候内存会自动记录数组的大小并且保存在前面,所以删除的时候不需要声明删除空间的大小

内存检查器 valgrind

1
valgrind --leak-check-full ./program (<args>)

这个可以检查内存是否存在泄露的行为
当然对于一般 debian 系的系统我们首先需要下载 valgrind:

1
2
sudo apt-get update;
sudo apt-get install valgrind -y

使用逻辑

在系统中,申请的空间是在 global 内存区的上面按照 heap 的数据结构进行排列的,与函数的stack区域在相反方向,二者中间存在极大的void 未分配空间,我们称呼stack 的生长为 grow down.

重载问题 functional overloading

在 c++ 中是可以有多个函数具有同一个名称的,区分点在于输入的变量类型 signature 不同,运行时候会自动匹配适合的数据类型的函数进入编译
对于输入不同变量的构造函数,我们可以定义不同的重载构造函数进行运算

一种解决办法:默认值

1
IntSet(int size = MAXELTS);

这样我们在构造的时候是否输入 size 都可以
但是这样就存在问题:编译器(使用者)会分不清少输入变量什么时候是哪个变量, 所以我们规定:写默认值必须是最后一个变量,否则就会分不清.

二维数组的内存申请和清除问题

对于一个二维数组,我们并不能直接申请 int ** mat = new int[r][c]
道理上只能申请一个连续内存, 示例如下:

1
2
3
4
int** mat = new int*[num_row];
for(int i= 0;i < num_row; i++){
(mat + i) = new int[num_col];
}

这样写有个显著的好处, 每一行单独申请空间,矩阵的存储空间不连续,这个对于高维矩阵的运算有好处,因为我们不需要保存连续的空间,离散保存有助于空间有效利用。
当然我们直接申请二维矩阵获得的是连续的内存空间.和我们直接申请一个 int* mat = new int[r*c]; 效果相同,需要很大一块连续的内存空间

清除数组内存空间

1
2
3
4
for(int i=  0;i < num_row; i++){
delete[] (mat+i);
}
delete[] mat;

传递值为类

一般写法传入为类的时候会对传入类的数据表进行拷贝,也就是local variable 会获得一个和原定义的类同样的指针地址,那么如果我对类的该指针进行操作,全局都会受到影响。当然最严重的是我们在该函数结束的时候会调用类的析构函数,按照常理我们会删除掉类的动态内存指针,那么local 的指针就会被释放,执行完之后该内存仍然失效,再次运行析构函数或者其他用到指针的函数就会出现 runtime-error.
这时候我们需要用到 软拷贝

深拷贝(Shallow Copy)

诞生思路

在复制赋值的时候,我们事实上只是需要一个数组,也就是说我们要拷贝一个的并不是一个地址

复制的构造器

我们最常见的赋值构造器是 = 符号,在函数调用的时候我们往往会隐式调用赋值这些ADT的数据内容。
于是我们定义在传入一个 ADT 的时候,必须使用 const MyClass & 关键字进行限制,称之为 浅拷贝

两种情况对比
1
2
void foo(const IntSet& is);
void foo(IntSet is);
  • 前者能通过赋值但是不会重新进入一个constructor
  • 后者可能会进入constructor的递归
  • 这里的 const 限定符是用来扩大传入范围的,也就是是否为 const 的变量都可以传进来
深拷贝的思路实现
  1. 动态指针申请同样大小的空间
    注:如果两者的指针尺寸不同,那么就必须先delete[] 本地指针然后重新 new
  2. 将输入动态数组的每一位分别赋值给本地数组的对应位置
  3. 复制尺寸等其他参数
    对于一般的c++程序运行的时候会默认执行浅拷贝,因此我们需要做的是手动编写硬拷贝脚本进行overload
赋值方法

c++ 存在一种称为链状的赋值方法如下:

1
x = y = z;

但是 c++ 的处理思路是从右到左

运算符重载思路 operator overload

重载运算符的写法如下:

1
IntSet &operator= (const IntSet &is);

使用 operator 关键字获得重载权限
注意整体可以看作是一个函数,需要有返回值为 IntSet& 或者说是 IntSet
调用的时候我们有两种方法使用:

1
2
IntSet a = b;  // 隐式调用函数
IntSet a.operator=(b); // 显式调用函数

从赋值语句我们可以看到,赋值是存在一个返回 值的,一般而言是最左边变量的内容,如果是已有的数据类型的赋值算法,一般返回的是 引用(由于引用是一个 lvalue ,鲁棒性更强)所以如果我们重载运算符为 一般的 IntSet 而不是 IntSet& 我们执行赋值之后只会有一个临时变量的返回值,所以结果并不好