侯捷 c++面向对象程序设计

时间:2022-06-03 19:49:30

基础知识

  • 基于对象: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()}