常量指针和指针常量
内存:

image.png

动态对象

  • 在堆中创建
  • new delete既是操作符(可以修改默认语义),也是关键字
  • new``delete可以重载:只能重载开辟内存 – 不一定从编译器管理的内存找,可以自定义在堆上、栈上管理

为什么需要 new 和 free

  • malloc 创建对象:不能 对象的创建必须调用构造函数,构造函数不是显式调用的。
  • 需要一种新的机制:除了能分配内存,还能调用构造函数;除了能收回内存,还能调用析构函数
  • new返回的是A*,而malloc返回的是void*

image.png

创建对象

image.png

  • int *intPtr = new int:在堆上创建了基本类型,是一种兼容
  • 栈上的对象都有名称,堆上的对象都是无名对象,只能通过指针访问。
  • 指针本身也是一种数据类型,和字长一样大

image.png

删除对象

  1. 创建对象时,实际上创建了两个内存块:
  2. delete ptr 删除的是Object,如果生命周期没有结束,仍然可以访问ptr。如果再次调用,会使用已经被删除的内存,出现了段错误
  3. 所以需要 ptr = null
  4. 同时,可以避免double free。如果ptr置为Null了, delete是没有用的

在使用void*时,使用delete,会根据指针类型,调用构造函数。但是,如果是**void***,直接**delete**,只会释放内存,不会调用构造函数,所以需要类型转换。类型决定了调用哪些函数!

以编译为主的语言,类型十分重要

动态对象数组

image.png

  • 使用初始化列表,可以显式初始化,所以不一定需要默认构造函数了
    1
    2
    3
    4
    5
    A *p;
    new A
    new A[100] //返回的都是A*
    数组的首地址和0号位置的地址一致
    delete[]p
  1. []不能省略:从声明类型上看,不知道**p**指向的是存储**A**的数组还是**A**一个对象
  2. delete调用析构函数,归还内存。
  3. 如何知道数组要调用多少次析构函数?

new A[100]会多分配4个字节,返回的地址之前有四个字节用来存储元素个数

  1. 如果没有[]
    1. 则不会找4个字节,只会调用一次析构函数。会导致内存泄漏
    2. 起始地址是地址减去4个字节,会直接从中间释放,导致段错误

1
2
3
int *p 
p = new int[100]
delete p

该写法是正确的。对于内置数据类型,不需要调用析构函数,所以不会添加4个字节,所以直接删去整块内存

动态2D数组

创建

image.png

删除

image.png
缺点:红色的部分是额外的内存开销
所以:要用一维数组模拟二维数组a[i][j] = a[i*4+j] 进行操作符重载即可

Const成员

image.png

1
2
3
4
5
6
7
8
9
class A{
public:
A(int num){
x = num;
// 会报错
// Cannot assign to non-static data member 'x' with const-qualified type 'const int'
}
const int x;
};
1
2
3
4
5
6
7
8
class A{
public:
A(int num):x(num){
// x = num;
}
const int x;
};

static:所有的对象共享一份数据
static const:所有的对象共享一份常量。必须在定义的时候初始化,不能用初始化列表初始化 在列表中初始化,则说明不同对象可以修改了。作为const,必须在声明时初始化,作为static,不能在列表中初始化,所以只能定义时初始化了
const 对象:对象的成员变量不应该被改变
image.png
编译器不知道哪些操作会改变A的值,哪些不会改变
所以需要使用:

const成员函数

  1. 非const对象可以访问所有的成员函数、变量;const对象除了不能访问非const成员函数外,其它都可以访问。
  2. const对象中的成员变量也是可以修改的。通过引用修改即可。**indirect_int++**
  • 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改,例如const char * getname()。
  • 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如char * getname() const。

image.png

  1. f()不是const函数,则不能调用
  2. show可以调用。

对于B.cpp,只需要看a是否调用了非const成员函数
如果在f后面也加入const,可能会出错,则需要进行复杂的检查,因为有可能会不断嵌套其他函数。
如果A a2(0,0),所以需要对象中的变量和对象同步
image.png
每一个函数,都自带一个指针
void f(A* const this)const表明指针中的内容不可变
void show(const A* const this);
const A和A不是同一种类型,涉及到const类型转换
const靠近谁,就是谁不可变,所以第一个constA*不可变,即A中的内容不可变
调用常量对象,就只能调用该对象中this指向的const的成员函数
image.png
fa不可以++,但是引用indirect_int可以加加,因为语法上,**indirect_int**是引用,引用是不变量 ,后面对引用的操作和引用本身无关

  • 对象外的内存和对象本身无关,不受到**const**的限制
  • 如果**indirect_int**指向**a**,因为编译器无法区分该变量的内存是在类内还是在类外,所以编译器可以通过。
  • 所以即使声明常量对象,也无法保证类内的变量不可改变(至少编译器无能为力)
  • 所以,退一步:如果在变量前加入**mutable**,则该变量就是可以在**const**成员函数中被修改:通过**const cast <A*> this **强制类型转换,来实现**mutable**

静态成员

image.png

静态成员变量

  1. 一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量,都可以引用它。
  2. static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
  3. 静态成员变量必须初始化,而且只能在类体外进行。例如:

int Student::m_total = 10;
初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。全局数据区的变量都有默认的初始值 0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值。

  1. 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。
  2. 在 C++ 中,static 静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化

image.png
什么时候定义:、

1
2
3
4
5
6
7
class A
{ int x,y;
static int shared;
.....
};
int A::shared=0;
// 放在类外的.cpp文件中

static const什么时候定义:

  • const要在成员初始化表中初始化,是在对象创建的时候调用的
  • 共享+不可变:在类内声明的时候定义 const static int x = 0

静态成员函数

image.png
image.png

  • 兼容了对象和名空间的语法
  • 既是对象的函数也可解释为类的函数
  • 区分静态、动态:
    • 控制对象的创建
    • 实现共享

单例模式

控制对象的创建
特殊:构造函数和拷贝构造函数声明为private – 类外不能new一个singleton,因为是在new中调用构造函数,但是是私有的,所以禁止在类外new一个对象,所以对象的创建是可控的
懒初始化:用到了再去创建,规定了创建过程是动态的,由new来创建,而不是由static控制。
但类外不能创建,所以只能在类内创建 – 如何调用里面的方法?
所以:需要一个静态的入口

  • 静态区:专门负责动态区的对象的创建和消亡:
  • _static singleton * instance() _``_static void destroy() _
  • 只能通过静态成员方法来控制

image.png

友元

C++友元函数和友元类(C++ friend关键字)
借助友元(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的 private 成员。

  • 不是类的成员

image.png
① 程序第 4 行对 Address 类进行了提前声明,是因为在 Address 类定义之前、在 Student 类中使用到了它,如果不提前声明,编译器会报错,提示’Address’ has not been declared。类的提前声明和函数的提前声明是一个道理。
② 程序将 Student 类的声明和实现分开了,而将 Address 类的声明放在了中间,这是因为编译器从上到下编译代码,show() 函数体中用到了 Address 的成员 province、city、district,
如果提前不知道 Address 的具体声明内容,就不能确定 Address 是否拥有该成员(类的声明中指明了类有哪些成员)**。

这里简单介绍一下类的提前声明。一般情况下,类必须在正式声明之后才能使用;但是某些情况下(如上例所示),只要做好提前声明,也可以先使用。
但是应当注意,
类的提前声明的使用范围是有限的只有在正式声明一个类以后才能用它去创建对象。如果在上面程序的第4行之后增加如下所示的一条语句,编译器就会报错:
Address addr;//企图使用不完整的类来创建对象
因为创建对象时要为对象分配内存,在正式声明类之前,编译器无法确定应该为对象分配多大的内存。编译器只有在“见到”类的正式声明后(其实是见到成员变量),才能确定应该为对象预留多大的内存。
在对一个类作了提前声明后,可以用该类的名字去定义指向该类型对象的指针变量(本例就定义了 Address 类的指针变量)或引用变量(后续会介绍引用),因为指针变量和引用变量本身的大小是固定的,与它所指向的数据的大小无关。
一个函数可以被多个类声明为友元函数,这样就可以访问多个类中的 private 成员。

image.png

声明

  • 两次声明:可以把friend看做继承
  • 遵循先声明后使用的原则,没有声明则不能确定类型的内存大小
  • 可以没有class B,但不可以没有class C。因为没有像C使用C::f。没有class B,可以当做一种前向声明,但是作为一个友元,需要写成**friend B**,因为肯定是引用已有的的

image.png

不完整声明

image.png

  • 因为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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
protected :
int prot_mem;
// 该基类中没用友元函数
};

class Sneaky : public Base {
friend void clobber(Sneaky &); // clobber 是Sneaky的友元函数,可以访问Sneaky中的private和protected变量
friend void clobber(Base &); //clobber 并不是Base的友元函数
int j;
};

void clobber(Sneaky &s) {
// 通过继承,相当于派生类本身有prom_mem变量,所以可以当作普通的私有、保护变量处理
s.j = s.prot_mem = 0; // 因此可以被友元函数访问
}
void clobberUnfriend(Sneaky &s) { // 普通函数,没有声明为友元函数时,只能访问对象的公有变量
// s.j = 0; // error 需要public
// s.prot_mem = 0;
}
void clobber(Base &b) { b.prot_mem = 0; } // 该函数需要在Base中定义为友元函数才可以使用

封装原则

image.png
迪米特法则:信息流在模块之间流动应该是最小的,对象之间的依赖是最小的。

static和const辨析

  1. static变量在类内声明,且只能在类外初始化 在类外初始化是保证static成员变量只被定义一次的好方法。
  2. const变量只能类内初始化,不能类外初始化
  3. static const既可以在类内初始化,也可以做类外初始化,但是不能重复初始化
  4. static定义了类内变量后,不可以static int A::num2 = 50;``static关键字如果用了类的名空间,就只能在类内使用
  5. 非静态变量只能在类内初始化,不能在类外初始化