4. 对象的管理

本文最后更新于 2024年1月27日 下午

对象的管理

这是浙江大学翁恺老师的公开课,《面向对象设计C++》
视频地址:
https://www.bilibili.com/video/BV1yQ4y1A7ts/?spm_id_from=333.337.search-card.all.click&vd_source=3074f6f6ab43a114c5af8727fa4f7255

本节对应视频11-13部分。

动态制造对象

new

之前提到的所有的对象都是本地变量,除了这种方式之外,C++中可以动态的进行内存分配,为此C++中有两个关键字:newdelete,这两个关键字都是运算符。new用于制造一个对象,并为其动态的分配内存:

1
2
3
new int; //new后面如果是类型,那么将会分配一块该类型的内存
new Stash;//new后面如果是对象,那么将会分配一块该对象的内存,然后调用这个对象的构造函数
new int[10];//new后面如果是类型,那么将会分配10个该类型的内存
new返回的结果是分配好的内存,比如:

1
int * psome = new int [10]; //申请10个int的内存空间交给指针psome

如此new返回的是这10个内存空间的地址。
内存的申请不需要操作系统介入,因为在程序开始运行前操作系统会自动分配一定大小的内存空间,因此内存的申请是申请占用操作系统已经为编译器分配好的空间。
在申请的过程当中,首先会在堆里面寻找一块合适的空间,足够放下目标,然后指针会指向这个地址。与此同时会有一张表会记录下申请下这个内存,表包括了内存的大小和内存的地址。

1
2
3
4
int *p=new int;
int *a=new int[10];
Student *q=new Student();
Student *q=new Student[10];

如果申请一个类,编译器也会分配这个类大小的空间,申请完后会调用这个类的构造函数。
此时表中是记录的这个类占用的内存大小和内存的首地址。

delete

delete用于删除一个对象并回收这个对象的内存。删除过程会先调用这个对象的析构函数,然后删除这个对象占用的空间。

1
2
delete p; //
delete [] psome; //删除为psome分配的10个int的空间,这10个对象的析构函数都会被调用
如果申请的是包含多个对象的一块空间,那么应该使用delete [],如此这个空间中所有的析构函数都会被调用。而没有[]delete只会调用第一个析构函数。
1
delete p;
回收的过程会查找记录的表,并解除对应内存的大小。
1
delete q;
在解除类的占用空间时,由于q的类型对编译器已知,因此编译器会首先调用q的析构函数。

1
2
delete r;
delete[] r;

但是如果r是一个对象数组,delete r虽然也会解除全部占用的空间,但是只会调用一个析构函数。

delete函数的几个使用原则:
- 不要delete未被new申请占用的空间。
- 不要对同一个内存区块delete两次。
- 如果new []申请的是一个内存块,那么相应地解除占用时也应该使用delete []. - 如果new申请的一个单个实体,那么相应的解除占用也应该使用delete.
- 删除空指针(null point)是安全的。

内存泄露

虽然多线程操作系统可以在一个程序运行结束之后自动回收这个程序的内存。如果只进行new,并不使用delete,在程序结束之后new占用的内存也会被操作系统自动回收。但是如果该程序需要一直运行,那么最终就会导致内存泄漏。

访问限制

访问属性

接下来来说访问属性的问题,之前的例程中一直都在使用privatepublic。这涉及到了这个类里面的哪些成员只能从内部访问,哪些成员可以从外部访问。
C++中类的成员的访问属性有三个:pubilicprivateprotected
public是公开的,外部可以访问这些成员;private是私有的,只有类里面的成员函数可以访问这些成员(函数或者变量);protected只有类自己以及子类可以访问这些成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using namespace std;
class A {
private:
int i;
int *p;
public:
A() {p=0; cout<< "A::A()" << endl;} //需要初始化p
~A() { if (p) delete p; cout<< "A::~A()" << endl;}
void set(int ii) {i = ii;}
void g(A *q) { cout << q ->i;} // 此处在成员函数中,一个private的指针访问了private的i
};

int main () {
A *p = new A[10];
A b;
b.set(100);
p[0].g(&b) //此处调用了g()
return 0;
};

上面的程序可以通过编译并且正确运行,说明属于类A的指针*p通过g()访问到了同属于Ab的地址。从上面可以看出,私有的概念仅限于不同类之间,而不是不同对象之间。
同一个类的不同对象之间是可以相互访问对方的私有变量
事实上,由于C++的面向对象的特性仅限于源代码级别,private的限制仅仅在编译阶段,在运行阶段(已经降低为二进制机器码)并不会进行限制。

访问授权

C++中还有一个特性是friend,如果声明某个全局函数/其他类/其他类中的某个函数是这个对象的friend,那么全局函数/其他类/其他类可以访问这个对象的私有成员。
friend的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct X; // 前向声明,为了让之后Y中的X通过编译
struct Y {
void f(X*);
};

struct X {
private:
int i;
public:
void initialize();
friend void g(X*, int); // global friend
friend void Y::f(X*); // struct member friend
friend struct Z; // entire struct is a friend
friend void h();
};


void X::initialize() {
i = 0;
}

void g(X *x, int i) {
x -> i = i;
}

void Y::f(X *x) {
x->i = 47;
}
friend的授权也是在编译阶段检查的。

缺省权限

不去限制访问属性的时候, class的缺省权限为private,结构体struct的缺省权限为public
一般首选class,只有在类非常简单的时候可以选择使用struct

列表初始化

在之前的例程中,指针int *p初始化的方法为A() {p=0; cout<< "A::A()" << endl;}。 C++中还有另一种方法进行初始化:A():p(0) {cout<< "A::A()" << endl;}这种方法称为初始化列表。
这种方法是在构造函数以后用:写上成员变量的名字以及用0表示这个变量的初始值。

1
2
3
4
5
class Point {
private:
const float x,y;
Point(float xa = 0.0, float ya = 0.0):y(ya),x(xa) {}
};

列表初始化和在构造函数中进行赋值的初始化方法是不同的。

1
Student::Student(string s):name(s) {} // initialization
初始化列表中初始化变量的操作早于调用构造函数, 并且可以初始化任何类型的数据,这样避免了构造函数中进行重复操作。

1
Student::Student(string s): {name=s;} //assignment

赋值初始化是在调用构造函数的过程中进行的,初始化必须要有一个默认构造函数才能进行。
为了避免编译过程出现错误,C++中对于类中变量的初始化建议使用列表初始化方法而不是赋值初始化。


4. 对象的管理
https://l61012345.top/2024/01/03/学习笔记/C++面向对象设计/4. 对象管理/
作者
Oreki Kigiha
发布于
2024年1月3日
更新于
2024年1月27日
许可协议