第七课 动态内存
常量指针和指针常量
内存:
- 堆
- 栈
动态对象
- 在堆中创建
new
delete
既是操作符(可以修改默认语义),也是关键字new``delete
可以重载:只能重载开辟内存 – 不一定从编译器管理的内存找,可以自定义在堆上、栈上管理
为什么需要 new 和 free
- 用
malloc
创建对象:不能 对象的创建必须调用构造函数,构造函数不是显式调用的。 - 需要一种新的机制:除了能分配内存,还能调用构造函数;除了能收回内存,还能调用析构函数
new
返回的是A*
,而malloc
返回的是void*
创建对象
int *intPtr = new int
:在堆上创建了基本类型,是一种兼容- 栈上的对象都有名称,堆上的对象都是无名对象,只能通过指针访问。
- 指针本身也是一种数据类型,和字长一样大
删除对象
- 创建对象时,实际上创建了两个内存块:
delete ptr
删除的是Object
,如果生命周期没有结束,仍然可以访问ptr
。如果再次调用,会使用已经被删除的内存,出现了段错误- 所以需要
ptr = null
- 同时,可以避免
double free
。如果ptr
置为Null
了,delete
是没有用的
在使用void*
时,使用delete
,会根据指针类型,调用构造函数。但是,如果是**void***
,直接**delete**
,只会释放内存,不会调用构造函数,所以需要类型转换。类型决定了调用哪些函数!
以编译为主的语言,类型十分重要
动态对象数组
- 使用初始化列表,可以显式初始化,所以不一定需要默认构造函数了
1
2
3
4
5A *p;
new A
new A[100] //返回的都是A*
数组的首地址和0号位置的地址一致
delete[]p
[]
不能省略:从声明类型上看,不知道**p**
指向的是存储**A**
的数组还是**A**
一个对象delete
调用析构函数,归还内存。- 如何知道数组要调用多少次析构函数?
new A[100]
会多分配4个字节,返回的地址之前有四个字节用来存储元素个数
- 如果没有
[]
,- 则不会找4个字节,只会调用一次析构函数。会导致内存泄漏
- 起始地址是地址减去4个字节,会直接从中间释放,导致段错误
1 | int *p |
该写法是正确的。对于内置数据类型,不需要调用析构函数,所以不会添加4个字节,所以直接删去整块内存
动态2D数组
创建
删除
缺点:红色的部分是额外的内存开销
所以:要用一维数组模拟二维数组a[i][j] = a[i*4+j]
进行操作符重载即可
Const成员
1 | class A{ |
1 | class A{ |
static
:所有的对象共享一份数据static const
:所有的对象共享一份常量。必须在定义的时候初始化,不能用初始化列表初始化 在列表中初始化,则说明不同对象可以修改了。作为const
,必须在声明时初始化,作为static
,不能在列表中初始化,所以只能定义时初始化了const 对象
:对象的成员变量不应该被改变
编译器不知道哪些操作会改变A
的值,哪些不会改变
所以需要使用:
const成员函数
- 非const对象可以访问所有的成员函数、变量;const对象除了不能访问非const成员函数外,其它都可以访问。
- const对象中的成员变量也是可以修改的。通过引用修改即可。
**indirect_int++**
- 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改,例如const char * getname()。
- 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如char * getname() const。
f()
不是const函数,则不能调用show
可以调用。
对于B.cpp
,只需要看a
是否调用了非const
成员函数
如果在f
后面也加入const
,可能会出错,则需要进行复杂的检查,因为有可能会不断嵌套其他函数。
如果A a2(0,0)
,所以需要对象中的变量和对象同步
每一个函数,都自带一个指针 void f(A* const this)
该const
表明指针中的内容不可变void show(const A* const this);
const A和A不是同一种类型,涉及到const类型转换
const靠近谁,就是谁不可变,所以第一个const
A*不可变,即A中的内容不可变
调用常量对象,就只能调用该对象中this指向的const的成员函数f
中a
不可以++,但是引用indirect_int
可以加加,因为语法上,**indirect_int**
是引用,引用是不变量 ,后面对引用的操作和引用本身无关
- 对象外的内存和对象本身无关,不受到
**const**
的限制 - 如果
**indirect_int**
指向**a**
,因为编译器无法区分该变量的内存是在类内还是在类外,所以编译器可以通过。 - 所以即使声明常量对象,也无法保证类内的变量不可改变(至少编译器无能为力)
- 所以,退一步:如果在变量前加入
**mutable**
,则该变量就是可以在**const**
成员函数中被修改:通过**const cast <A*> this **
强制类型转换,来实现**mutable**
静态成员
静态成员变量
- 一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量,都可以引用它。
- static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
- 静态成员变量必须初始化,而且只能在类体外进行。例如:
int Student::m_total = 10;
初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。全局数据区的变量都有默认的初始值 0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值。
- 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。
- 在 C++ 中,static 静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化
什么时候定义:、
1 | class A |
static const
什么时候定义:
const
要在成员初始化表中初始化,是在对象创建的时候调用的- 共享+不可变:在类内声明的时候定义
const static int x = 0
静态成员函数
- 兼容了对象和名空间的语法
- 既是对象的函数也可解释为类的函数
- 区分静态、动态:
- 控制对象的创建
- 实现共享
单例模式
控制对象的创建
特殊:构造函数和拷贝构造函数声明为private
– 类外不能new
一个singleton
,因为是在new
中调用构造函数,但是是私有的,所以禁止在类外new
一个对象,所以对象的创建是可控的
懒初始化:用到了再去创建,规定了创建过程是动态的,由new
来创建,而不是由static
控制。
但类外不能创建,所以只能在类内创建 – 如何调用里面的方法?
所以:需要一个静态的入口,
- 静态区:专门负责动态区的对象的创建和消亡:
_static singleton * instance() _``_static void destroy() _
- 只能通过静态成员方法来控制
友元
C++友元函数和友元类(C++ friend关键字)
借助友元(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的 private 成员。
- 不是类的成员
① 程序第 4 行对 Address 类进行了提前声明,是因为在 Address 类定义之前、在 Student 类中使用到了它,如果不提前声明,编译器会报错,提示’Address’ has not been declared。类的提前声明和函数的提前声明是一个道理。
② 程序将 Student 类的声明和实现分开了,而将 Address 类的声明放在了中间,这是因为编译器从上到下编译代码,show() 函数体中用到了 Address 的成员 province、city、district,如果提前不知道 Address 的具体声明内容,就不能确定 Address 是否拥有该成员(类的声明中指明了类有哪些成员)**。
这里简单介绍一下类的提前声明。一般情况下,类必须在正式声明之后才能使用;但是某些情况下(如上例所示),只要做好提前声明,也可以先使用。
但是应当注意,类的提前声明的使用范围是有限的,只有在正式声明一个类以后才能用它去创建对象。如果在上面程序的第4行之后增加如下所示的一条语句,编译器就会报错:Address addr;
//企图使用不完整的类来创建对象
因为创建对象时要为对象分配内存,在正式声明类之前,编译器无法确定应该为对象分配多大的内存。编译器只有在“见到”类的正式声明后(其实是见到成员变量),才能确定应该为对象预留多大的内存。在对一个类作了提前声明后,可以用该类的名字去定义指向该类型对象的指针变量(本例就定义了 Address 类的指针变量)或引用变量(后续会介绍引用),因为指针变量和引用变量本身的大小是固定的,与它所指向的数据的大小无关。
③ 一个函数可以被多个类声明为友元函数,这样就可以访问多个类中的 private 成员。
声明
- 两次声明:可以把friend看做继承
- 遵循先声明后使用的原则,没有声明则不能确定类型的内存大小
- 可以没有
class B
,但不可以没有class C
。因为没有像C使用C::f
。没有class B
,可以当做一种前向声明,但是作为一个友元,需要写成**friend B**
,因为肯定是引用已有的的
不完整声明
- 因为vector &v 是一个引用,内存大小是确认的,所以可以进行不完整的声明。
如果A B类互相引用对方,则头文件不能互相引用,所以要在A的头文件中进行前向声明。但是因为没有完整声明,所以A的show和B的show都要写在B的cpp文件中 =>设计有点问题 =>引入都要同时引用两个类的头文件。
- 友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。
- 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。
友元和继承
1 | class Base { |
封装原则
迪米特法则:信息流在模块之间流动应该是最小的,对象之间的依赖是最小的。
static和const辨析
static
变量在类内声明,且只能在类外初始化 在类外初始化是保证static成员变量只被定义一次的好方法。const
变量只能类内初始化,不能类外初始化static const
既可以在类内初始化,也可以做类外初始化,但是不能重复初始化static
定义了类内变量后,不可以static int A::num2 = 50;``static
关键字如果用了类的名空间,就只能在类内使用- 非静态变量只能在类内初始化,不能在类外初始化