基础知识
- 基于对象:Object Based 面对的是单一class的设计。
- 面向对象:Object Oriented 面对的是多重classes的设计,涉及到类和类之间的关系。
- 课程中设计到两种不同类设计:没有指针(成员变量)的类和带指针(成员变量)的类设计。
- 头文件一般采用h结尾,源文件一般采用cpp,但是也不一定!(如stl很多没有后缀名)
- 头文件采用防御式声明,采用
#ifndef *** #define *** #endif
,避免多次引用。 - 注意声明文件的内容顺序,一般是前置声明、类声明、类定义。(疑问,采用源文件进行类定义,与采用头文件进行类定义有哪些区别)
- 有的函数在类声明时在类的内部直接定义(直接内联),内联只是一种编译提示,是否真的内联取决于函数复杂程度和编译器实现。
- 构造函数的默认参数和初始化列表的使用,初始化列表很重要,和复制不同!能提高程序的初始化性能。
- 构造函数可以有多个重载。
- 如果将构造函数放在private区域,则该类不能在外部构造对象,一般配合设计模式使用,采用工厂模式来构造类,禁止直接构造类的时候使用。例如:定义一个类的静态函数getInstance,该函数返回一个静态的对象。
- 常量成员函数的意义很重要,一般不改变成员变量的函数都声明为常量函数,在函数声明后面添加 const。方便常量对象直接调用。
- 明白参数传递中传值和传引用的意义,传引用与传地址效率一样。在类对象的参数传递中尽可能采用传引用的方式,对于不修改类对象的参数传递尽可能采用常量引用。
- 返回值同样重视传值和传引用,此时注意局部变量考虑到其生命周期,在传引用时要尤其注意,不然会出现野指针。
- 对于友元函数,可以直接访问友元的私有成员变量。相同class的各个对象之间互为友元。
- 关于操作符重载,用于实现带有符号语义的函数,注意其语法要求。
- 对于返回引用的情况,参考对序列化输出和连加连减等操作。
- 明白操作符重载什么时候需要成员函数,什么时候需要非成员函数。
带指针(变量)的类设计
拷贝构造函数、拷贝函数和析构函数
- 三大函数。带指针的类设计一定要重视这三大函数,主要是涉及到危险的指针赋值操作。
堆和栈
- 栈对象(变量)在离开作用域时销毁,调用对象的析构函数。
- 静态栈对象,离开作用于还存在,在整个程序结束的时候析构。
- 全局对象的生命周期,比main函数早存在,在整个生命周期结束之后才结束。
- 堆对象(变量)控制权交给程序员,自己创建(new),自己负责销毁(delete),所以一定要注意指针的赋值(拷贝)操作,容易产生问题,在学习c++11的智能指针后尽可能多使用智能指针。
- 堆对象的生成,使用new,先分配堆空间,再调用构造函数。
new的动作分解:首先分配足够的内存空间,然后将内存进行转型操作,然后调用对象的构造函数void* mem = operator new(sizeof(class));
pc = static_cast<class*>(mem);
pc->class::class(*);
- 堆对象的释放,使用delete,先调用析构函数,然后再释放堆内存。
class::~class(pointer); operatr delete(pointer)
- 在String的设计中,在析构函数中调用delete释放字符串。
- 候老师的重点内容,在new复数类(上一课例子),在调试模式下会多得到32个字节,中间是类的大小(两个double),后面还有四个字节,加上一头一尾的小cookie中,一共是8+(32+4)+(4+4)=52个,在vc下分配内存是16的倍数,为64个。为什么?在回收的时候顺利的回收。在relese模式下没有头尾的添加,但是有cookie,8+(4+4)=16。注意小cookie中的地址记录大小和最后一个位比特来表示是借出还是回收。字符串类(String,成员变量只有一个char指针,4比特),在调试模式下4+(32+4)+(4+4)=48,在relese模式下位4+(4+4)=12,变成16的倍数是16。
- 如果分配的是数组:array new对应array delete。
class *p = new class[3];
(8X3)+(32+4)+(4X2)+4,最后的加4是VC的做法,用一个整数来记录数组的长度,结果是72,调整到16的倍数为80,其余的模式以此类推。正确的搭配模式下调用array delete时,看到cookie,知道要删除的空间的大小,不会引起内存泄漏,但是会根据记录数组的区域,3次调用析构函数。如果array new不用array delete的话,只会调用一次析构函数,这样剩下的指针所指对象不会调用析构函数。感悟:透彻明了!
字符串String类的实现细节。
- 头文件中添加防卫式定义。
- 字符串里面的类属性,放数组不好因为事先不知道大小,一般放一个指针,大小根据放的内容动态分配;32位平台上一个指针4个byte。
- big three 函数:拷贝构造函数(优先考虑传入引用,不修改变量的值,所以添加const),拷贝复制函数作为成员函数,在返回时传出引用。析构函数,释放开辟的堆内存。
- 注意String的获取成员变量m_data指针,返回const修辞的char指针。
- 拷贝赋值函数注意首先判断是否是自我赋值。
if(this==&str)return *this;
类模板函数模板
静态static
- 类里面的可以包含static函数和static成员变量
- 每个类对象包含各自的成员变量,一个成员函数要被多个对象调用,需要this指针。
- 对象中静态static成员不变量属于类,只存在一份。(多个对象共用)
- 类的静态函数没有this指针,所以不能访问对象里面的类成员,只能是处理静态数据。
- 静态类成员数据,一定要在类的外面设初值或者叫定义。 注意定义方法
type class::member = ...
- 调用static函数的方式有两种,一是通过对象调用,第二种是通过类来调用。
模板技术
template<typename T> class name{};
- 编译器会根据不同的参数,生成不同的代码,所以使用类模板可能造成代码的膨胀。
- 了解模板函数的意义和作用处理。
- c++中的算法大量使用模板。
命名空间
- 为避免命名冲突,使用命名空间。(比较简单)
- 命名空间可以分多段定义。
- using namespace *;全开
- using ::;指定打开
组合与继承探讨类与类之间的关系
复合 Composition (has-a)
- (自己的理解)一个类包含(有)另一个类的对象。注意UML类图,采用实心菱形,箭头指向包含的对象,菱形指向包含别人对象的类。
- 适配器模式,一个类调用另外类已有的函数(接口),用来满足新类对接口和名称的要求。
- 从内存的角度来解释复合,层层包含。
- 构造函数之间的关系,container拥有component,外部的构造函数先调用内部的默认构造函数,即构造由内而外。
container::container(...):compoent(){……}
- 外部的析构函数先执行自己,再调用内部的析构函数,析构由外而内。
container::~container(...):{……^compoent();}
委托关系(Delegation)按引用的复合
- 一个类包含一个类的指针,UML类图使用空心的菱形代替复合中的实心菱形。Pimpl
- 用指针相连,生命周期就不一致。
- 例子中采用委托实现字符串的引用计数。copy on write
继承 (is-a)
- 一个类从另一个类继承部分属性和方法。
- uml类图,空心三角形指向父类。
- 使用继承,传达一种信息,子类是一种(父类)
- 继承跟虚函数搭配最有价值–重载。
- 从内存的角度来看,子类的对象中有父类的成分。
- 构造由内而外,derived的构造函数首先调用base的默认构造函数。
Derived::Derived(...):Base(){....};
- 析构由外而内,derived的析构函数先执行自己,然后才调用base的析构函数。
Derived::~Derived(...){...~Base();};
- 父类的析构函数必须是虚函数,否则会出现不可预期的情况。
- 非虚函数,你不希望继承的类重新定义(覆盖override)它。
- 虚函数,你希望继承类重新定义(覆盖)它,而且你对它已经有默认定义。
- 纯虚函数,你希望继承类一定要重新定义它。virtual …… = 0;
- 子类对象调用父类的函数,父类的函数中采用虚函数,再调用子类重载的函数。父类中将关键动作延缓到子类中来实现,这种函数的做法叫做Template method,在框架中大量使用。
继承加复合关系下的构造和析构
- 子类从父类继承,子类还包含一个类的对象,构造函数先调用父类构造还是复合的对象?
- 父类包含一个复合对象,子类继承。应该先调用复合构造,父类构造和子类构造。
- 学习文件资源管理类中使用的 Composite 委托加继承的设计方法。
- portotype 设计模式。现在创建未来的类。一个类包含一个静态对象,自身的对象,自己创造了自己。
- 静态成员变量一定记住在类的外面进行定义。
转换函数 conversion function
- 从一种类型转换成另外一种类型,相互转换。
- 定义转换函数:函数不可以有参数,没有返回参数。
operator 转换类型() const {return 类型}
转换函数注意合理性。
non-explicit-one-argument actor
- (一个实参就够了)非explicit的带一个实参的构造函数。从一个实参构建一个对象。可以把别的东西转换成对象。
- 转换函数和non-explicit-one-argument actor在一起的时候,会造成二义性,编译器会报错。
- explicit-one-argument ctor 明确的一参数构造函数,不要不同类型的转换。explicit大部分用在构造函数的前面。
pointer-like classes 关于智能指针
- 像指针的类,比指针再多一些东西。
- 智能指针shared_ptr
- 封装了一个真正的指针,指针所允许的动作该类都支持。
*
和->
的操作。T& operator*()const { return *px; } T* operator->()const { return px; }
- 一个符号作用在对象上就消耗掉了,
->
符号除外,得到的指针对象继续用箭头符号。 - 关于标准库STL的迭代器。另外一种类似于指针的类。
reference operator*()const { return (*node).data; }//reference 相当于T&
pointer operator->() const { return &(operator*());} //pointer 相当于T*
function-like classes 仿函数
- 函数的特点,函数名称,小括号()-函数调用操作符,可以接受一个小括号作为操作符,那么就可以成为function-like。
const T& operator()(const T& x) const {return x;}
- 一定会重载 ()操作。
- 标准库中,仿函数都会去继承奇特的base classes.
namespace 经验谈
- 尽可能使用命名空间,防止变量名和函数名的冲突。
class模板
-
template<typename T> ……
T抽象变量类型。
function模板
template<typename T>函数定义
成员模板 member template
- 在模板类中存在一个新的模板,外面的模板是一个允许变化的东西,如果外部变化项确定,里面的变量又可以变化。
- 把两个继承类构成的pair放进一个有两个基类的pair中是可行的。反之不可以。
- 父类的指针可以指向子类的对象。up-cast。
- 智能指针模板为了实现up-cast,必须使用成员模板。
模板特化 specialization
- 泛化,在用的时候进行类型化。
- 设计模板之后,想绑定某种类型,就叫做特例化。指定了特定类型后编译器会根据参数选择相关代码。
partial specialization 偏特化
- 个数上的偏。模板有多个模板参数,对部分参数进行特例化
- 范围上的偏。从任意类型,特例化到指针这一种类型。
template temeplate parameter 模板模板参数
template<typename T, template<typename T> class Container> class XCls{private: Container<T> c;……}
XCls<string, list> mylst1; 错误
template<typename T> using Lst=list<T, allocator<T>>>; XCls<string, Lst> mylst2; 正确
关于c++标准库
- 数据结构容器和算法。
- 多使用标准库,写小例子测试标准库。
- 测试是否支持c++11,
cout<<__cplausplus <<endl;
三个主题(标准库中的新语法)
- 数量不定的模板参数
- auto关键字。auto自动确定变量类型。
- ranged-base for for(decl: coll){statement},注意传值和传引用。
for(auto& elem: vec){elem*=3;}
对象模型 关于vptr和vtbl
- 虚指针和虚表,一个类的对象内存占用什么样的内存?当一个类有虚函数的时候,对象里面就会多一个指针。一个虚函数和一万个虚函数是一样的。
- 继承会把成员变量继承也会把函数继承下来。
- 一般的函数和虚函数区别。
- 虚拟表中存放的都是指针,虚函数指针。
- 编译器看见调用虚函数时,采用动态绑定。通过虚指针,查看虚表,再看调用的是哪一个函数。(普通函数调用采用动态绑定)
(* p->vptr[n])(p);
编译器会找到n的位置编号。 - 静态绑定 call cll……
- 动态绑定,条件1、通过指针调用;2、指针是向上转型 up case;3、调用的是虚拟函数。(多态)
关于this
- 模板方法,this指针的使用场景。会把当前对象当做this指针传到方法里面。一个父类的方法A里面调用了一个虚函数,这个虚函数在子类中重载,这样当子类调用父类的方法A时,会通过父类的A函数,调用子类重载过的虚函数。
- 所有的成员函数都隐藏了一个this参数。
关于 Dynamic Binding
- 非指针调用不会产生动态绑定。
- 通过指针找到虚指针,找到虚表,找到相应的函数地址。
关于const
- 当成员函数的const和non-const版本同事存在是,const object只会(只能)调用const版本,non-const object只会(只能)调用non-const版本。
- const object 调用const 成员函数可行,但是non-const成员函数不可行。
- non-const object 可以调用 const 成员函数,non-const 成员函数。
- non-const 成员函数可以调用const 成员函数,反之则不行。
关于New 和 Delete
- new 先分配 memory,再调用ctor。
- delete 先调用dtor,再释放memory。
- array new,一定要搭配 array delete。
重载 new 和 delete 全局函数(编译器调用)
重载全局 ::new ::new[] ::delete ::delete[]
- inline void* operator new(size_t_size){……分配内存}
- inline void operatpr delete(void* ptr){……释放内存}
- 上面的重载函数不能放在namespace中,是全局的函数,影响是非常大的。
重载成员函数 new 和 delete
- 成员函数 void* operator new(size_t);
- 成员函数 void operator delete(void*, size_t);
- 成员函数 void* operator new[](size_t);
- 成员函数 void operator delete[](void*, size_t);
- 调用函数时添加了::,调用全局的函数,绕过类所定义的new和 delete版本。
- 关于new 和new[]参数的大小。有虚函数的对象对多一个指针的大小4。
- 对象数组[],对多一个4字节的区域,记录数据的大小是多少。
- 我们可以重载类成员的operator new(),写出多个版本,前提是每个版本的声明都必须有独特的参数列,其中第一个参数必须是size_t,其余参数以new 所指定的placement arguments为初值。出现于new(……)小括号内的便是所谓placement arguments。
- 我们也可以重载类成员operator delete(),写出多个版本,但是绝对不会被delete调用。只有当new所调用的ctor抛出异常,才会调用这些重载的函数operator delete()。它们只能这样被调用,主要用来归还还未完全创造成功的object所占用的memory。即使operatordelete(……)未能一一对应operator new(……)。也不会出现任何报错。
标准库中String使用 new(extra)扩展
- string采用new(extra)进行自己的内存分配,用于实现特定内存结构中引用计数的处理。
引用 reference
int x = 0; int& r = x; sizeof(r) == sizeof(x); &x = &r;
- object和其引用的大小相同,地址也相同(全都是假象),java里面的变量都是引用。
- 编译器实现都是使用指针来实现引用,但是在使用时可以从逻辑上把引用当做原值来使用。
- 声明引用的时候一定要有初值。设置完以后不能再变化。指针可以变化。
- 引用的地址和原始类型的地址相同
- 引用通常不用再变量的声明,引用主要用于参数类型(传参数)和返回类型(返回参数)的描述。
- const是不是函数签名的一部分?是
析构和构造函数
继承关系下的构造和析构
- 继承类的构造函数首先调用父类的默认构造函数再执行自己。
- 继承类的析构函数先执行自己再执行父类的析构函数。
复合关系下的构造和析构
- 拥有者的构造函数先调用组件的默认构造函数,然后再执行自己。
- 拥有者析构函数先执行自己,然后才地哦啊用组件的析构函数。
继承加复合关系下的构造和析构
- 继承类的构造函数先执行基类的默认构造函数,然后调用组件的默认构造函数,然后再执行自己。
Derived::Derived(……):Base(), Component(){};
- 继承类的析构函数首先执行自己,然后调用组件的析构函数,然后调用基类的析构函数。
Derived::~Derived(……){……~Component(), ~BAse()}