侯捷面向对象 & C++ Primer
构造函数
-
尽量用初始化列表(*)
-
构造函数一般放在public区,但存在特殊情况——不想要外界访问创建对象(比如单例设计模式),此时构造函数放在private区
参数传递与返回值
-
设计类成员函数时,如在构思接口阶段即判断该函数不会修改数据成员,须使用const关键字描述函数。因为存在使用者创建常量对象,此时需要对象中的成员函数是const描述(*)
-
参数传递和返回值都尽量使用引用(*),为了提升效率,但存在不使用的情况
-
之所以可以提升效率是因为,一般的类对象占用内存较大,而引用只有4字节所以效率会快很多。当传入基本类型时一般不传引用,因为占用空间差不多
-
如果返回值是返回函数内部创造的局部变量,不可以使用返回引用
-
除了提升效率以外,使用引用进行传递的好处:
由上图可见 _doapl函数中返回*ths,表示返回一个object,但是函数返回值类型是引用形式来接收这个返回值,直接用value也能由引用接收
-
-
指针和引用:指针对象使用 -> 调用成员;引用使用 . 调用成员
-
输出输入操作符的重载语法:
-
友元函数
-
显式定义:正常使用 friend 修饰成员函数
-
隐式定义:特殊情况如下
-
操作符重载和临时对象
-
设计函数是否为成员或者非成员取决于左边的操作数是否一直是类对象
-
成员函数类型的操作符中都有一个隐藏的参数——this指针,表示调用者
-
操作符连续使用,需要实际考虑使用情形,不可直接设置返回值类型为void
-
非成员函数类型的操作符,没有this指针
-
临时对象语法 typename(),不能用引用传递(前面已提到)
三大函数:拷贝构造、拷贝赋值、析构
-
类有两大类:带指针和不带指针的。不带指针的类可以使用编译器自带的拷贝函数,带指针必须自定义bigthere函数:拷贝构造、拷贝赋值和析构
- 使用编译器自带的拷贝是浅拷贝,只拷贝指针,会导致原来b指针所指的内存变成“孤儿”,即没人知道它在哪,这就是内存泄漏问题;同时会导致a和b同时看见一块内存,此时b是a的别名——alias,也很危险。
- 深拷贝重点是拷贝内容,构造函数就新开合适的内存拷贝内容;赋值函数就杀掉原有数据然后新开合适的容量后拷贝内容
-
属于构造函数,但是接收的参数是相同的类,这是拷贝构造函数;属于赋值的重载函数,但是接收的参数是相同的类,这是拷贝复制函数
-
拷贝赋值函数没做检测自我赋值的话:除了效率低,还会有问题:
如果没有检测,但是还是写出来 “a = a;” 这样的代码的话,走拷贝赋值的逻辑就会错误:首先两个指向同一内存,清空内容后没有内容供拷贝
堆、栈与内存管理
-
栈Stack是存在于某作用域Scope的一块内存空间。调用函数时,函数本身会形成一个stack用于存放参数以及返回地址,函数本体内声明的任何变量,所使用的内存块均来自于函数的stack
-
堆Heap,是由操作系统提供的一块全局性的内存空间,程序可以动态分配获取若干内存块
-
基于上述内存分类,程序中声明的对象可由以下划分:
- stack objects
stack object的生命在作用域结束之际自动执行析构函数,回收内存。
- static local objects
static local objects生命持续到整个程序结束
- global objects
global objects同样持续到整个程序结束
- heap objects
用new操作符得到的对象是动态获得堆内存的对象,其生命在被delete的时候结束。如果不及时delete,同样会有内存泄漏的风险
-
解析new和delete操作符:是用于带指针类的创建与回收,那不带指针的类可以用吗?
- new的实现:包装c的molloc函数
三步:分配内存返回void *指针;将void *指针转型为对象指针;调用构造函数赋初值。
- delete的实现:包装c的free函数
两步:第一步调用析构函数,注意析构函数是对对象中的指针成员使用delete[],释放对象创建时所动态分配的内存以及对象指针成员;第二步释放对象指针本身所占内存
-
动态分配的内存块
编译器实际分配给新创建的对象的内存远大于对象本身大小,一般由以下部分构成:对象本身大小 + 32字节的上置debug所需内存块 + 4字节的下置debug所需内存块 + 上下共计8字节的cookie边界内存块
注意:若不在调试模式下,不会分配debug内存块;同时最终所计算内存块的大小必须是16的倍数,若不是,编译器会增加pad内存块至最小16倍数。
- array对象的内存分配情况
- array new 搭配array delete的原因
单个的delete p 的确可以释放左边的内存,但编译器只会调用一次析构函数,释放一块动态分配的内存,对象数组中的其他对象中指针所指向的内存不予处理,造成内存泄漏。
补充
- 实例化出的对象,都会有一份非静态数据成员,静态的数据成员只有一份,多个对象共享;非静态的成员函数与静态的成员函数只有一份,差别在于静态函数没有this指针,只能处理静态的数据。
观察黄色部分,不管是否给初值,都必须对静态数据做定义操作;注意通过对象调用静态方法时,是不会传this指针进去。
第二种方法更优,只有有人需要A的对象时,在第一次调用取实例的方法时,才会创建对象。
- 类模板class template
- 函数模板 function template
参数推导很好,不用明确写尖括号给定T的类型。
- 命名空间 namespace
组合与继承
- 复合 composition HAS A
把一个很强的东西改装成一个更细化的东西——适配器adapter 设计模式
- 复合关系的构造与析构
编译器只能隐形地帮你调用默认构造函数,特殊需求需要自己显式地指出
- 委托 delegation 引用复合
我有一根指针指向为我实现的东西——handle / body(PIMPL ponit to implemention)很典型
这个例子可以实现多个对象共享一个内容,若其中一个对象需要改内容,那么单独copy一份给他改(copy on write)
-
继承 inheritance
public 继承就是 IS A 的关系
-
继承关系的构造与析构
原理同复合的构造与析构,注意父类的析构函数必须是虚函数
虚函数与多态
-
数据的继承体现在内存,而函数的继承体现在调用权
-
继承中的虚函数
- 非虚函数:你不希望派生类derived class覆写override
- 虚函数:你希望派生类覆写,且该函数已有默认定义
- 纯虚函数:你希望派生类一定要覆写,因为基类不知道怎么定义,只能交给派生类,你对他没有默认定义(其实纯虚函数可以有定义)
-
虚函数的用法演示
值得一提的是这种设计模式——Template Method,把固定的逻辑写在一个框架内,把不能明确的操作放在虚函数中。
调用方会被当作this point隐藏参数传入方法中,调用派生类覆写过的方法时,this指针起到作用。
- 继承和组合的构造和析构
上面那种结构不明确顺序;下面的结构是明确的
d,把固定的逻辑写在一个框架内,把不能明确的操作放在虚函数中。
调用方会被当作this point隐藏参数传入方法中,调用派生类覆写过的方法时,this指针起到作用。
- 继承和组合的构造和析构
[外链图片转存中…(img-NzAYBUDM-1669890213504)]
上面那种结构不明确顺序;下面的结构是明确的