C++程序设计II & 对象模型
一、导读
二、conversion function 转换函数
1.实例
//定义
class Fraction{
public:
Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den){}
//here!here!
operator double() const {
return (double)(m_numerator / m_denominator);
}
private:
int m_numerator;
int m_denominator;
}
//使用
Fraction f(3,5);
double d2 = 4 + f;//转换函数将f转换成double类型,然后4隐式转换成double,从而相加
2.语法
- 关键字operator
- 无返回值类型
- 将要转换的类型作为函数名
- 无参数
- 一般情况下转换函数不会改变原对象的类型,所以需要加上const关键字
3.用途
- 只要类的设计者认为可以该类可以转换成其他类的能力,并且写出合理的转换逻辑,就可以给类设计转换函数
三、non-explicit-one-argument ctor
1.实例
//定义
class Fraction{
public:
//here!here!
Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den){}
Fraction operator+(const Fraction& F) {
return Fraction(……);
}
private:
int m_numerator;
int m_denominator;
}
//使用
Fraction f(3,5);
double d2 = f + 4;//调用non-explict ctor 将4转换成Fraction类,从而可以调用operator+
2.语法
- one-argument的意思是给一个实参即可,多于一个也行
- 构造函数前不加任何关键字即是non- explicit
3.使用
- 可以把构造函数所需实参对应类型的变量转换成当前类,对比转换函数,其实是实现了两种对象间的互相转换。
4.conversion function vs. non-explict-one-argument ctor
//cf和neoac均存在的定义
class Fraction{
public:
//here!here!
Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den){}
//here!here!
operator double() const {
return (double)(m_numerator / m_denominator);
}
Fraction operator+(const Fraction& F) {
return Fraction(……);
}
private:
int m_numerator;
int m_denominator;
}
//使用
Fraction f(3,5);
Fraction d2 = f + 4;
//此时会报错:ambiguous
//存在两条可行的路线:f->double->double+double->Fraction或者4->Fraction->Fraction+Fraction->Fraction
- 从以上可以看出,设计者做的任何动作都会影响:设计变量的类型、加法逻辑的顺序等等
四、explict-one-argument ctor
1.实例
//cf和neoac均存在的定义
class Fraction{
public:
//here!here!
explict Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den){}
operator double() const {
return (double)(m_numerator / m_denominator);
}
Fraction operator+(const Fraction& F) {
return Fraction(……);
}
private:
int m_numerator;
int m_denominator;
}
//使用
Fraction f(3,5);
Fraction d2 = f + 4;
//该例是上一个例子仅在ctor前加上explict关键字
//此时仍然报错,是因为没有一条可行的路线,因为不能将4转换成Fraction类型,所以无法走operator+以及double+完后转成Fraction的这两条路线
2.语法
- 加上explict即可,使用范围较小,一般在构造函数前,很少情况用在模版中
3.用途
- 显示说明不运行使用构造函数对其他类型进行转换到当前类型的做法
五、pointer-like classes 智能指针
1.实例
//智能指针定义
template<class T>
class shared_ptr{
public:
//here!here!
T& operator*() const{
return *px;
}
//here!here!
T* operator->() const{
return px;
}
shared_ptr(T* p) : px(p) {}
private:
T* px;
long* pn;
};
//使用
struct Foo{
void method(void);
};
shared_ptr<Foo> sp(new Foo);
Foo f(*sp);
sp->method();//调用了智能指针重载的->,返回了其中存放的指针px,但是->的特性是持续作用于对象上,所以对px继续做->操作
2.语法
- c++2.0后有多种智能指针类
- 智能指针中都需要包含一个指针成员,都需要定义操作符*和->,同时操作符中定义内容参考上面的实例👆
六、pointer-like classes 迭代器
1.实例
template<class T, class Ref, class Ptr>
struct _list_iterator {
……
typedef _list_node<T>* link_type;
link_type node;
reference operator*() const {return (*node).data;}
pointer operator->() const {return &(operator*());}
……
}
template<class T>
struct _list_node {
void* prev;
void* next;
T data;
}
2.语法
- 迭代器和其他智能指针相似的是都会拥有一个指针成员并定义操作符*和->,不同的是迭代器还会定义其他操作
3.用途
- 对迭代器取*号,就是取出迭代器指向元素的data
list<Foo>::iterator ite;
……
*ite;//意思是从Foo对象的链表里获得一个Foo object
- 对迭代器取->,就是对迭代器所指向元素进行->操作
ite->method();
//意思是调用Foo::method()
//相当于 (*ite).method() 此时是调用Foo对象的method方法
//相当于 (&(*ite))->method() 此时是调用Foo对象指针的method方法
七、function-like classes 仿函数
1.实例
template<class T>
struct identity {
const T& operator()(const T& x) const {return x;}
};
template<class Pair>
struct select1st {
const typename Pair::first_type&
operator() (const Pair& x) const
{return x.first;}
};
template<class Pair>
struct select2nd {
const typename Pair::second_type&
operator ()(const Pair& x) const
{return x.second;}
};
//std定义一个pair类,可以放入两种类型
template<class T1, class T2>
struct pair {
T1 first;
T2 second;
……
}
1.1 实例延伸
实则仿函数的完整写法如下,会继承一个类,下图继承unary_function
下图继承binary_function
下图是unary_funtion类和binary_function类,前者用于单操作数的仿函数继承,后者用于双操作数的仿函数继承
2.语法
- 为了让一个类看起来像是一个函数,需要重载 ()这个操作符。
- 仿函数的实现需要继承一个类,👆提到,至于为什么是在std讨论的问题。
八、namespace
1.实例
九、class template 类模板 & function template 函数模板
1.实例
(I)课程中的复数类
函数模板无需指定类型名称,编译器会针对function template做实参推导,注意通用的处理逻辑可能需要自定义类型自行进行操作符重载一类的定义
十、member template 成员模板
1.实例
//实例一
//定义
template<class T1, class T2>
struct pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair():first(T1()),second(T2()){}
pair(const T1& a, const T2& b) : first(a),second(b){}
template <class U1, class U2>
pair(const pair<U1, U2>& p) : first(p.first), second(p.second){}
};
//使用
class 鱼类 {};
class 鲫鱼 : public 鱼类 {};
class 鸟类 {};
class 麻雀 : public 鸟类 {};
pair<鱼类,鸟类> p;
pair<鲫鱼,麻雀> p2(p);
//等同于下面
pair<鱼类,鸟类> p2(pair<鲫鱼,麻雀>());
//实例二
//定义
template<typename _TP>
class shared_ptr : public _shared_ptr<_TP>{
template<typename _Tp1>
explicit shared_ptr(_Tp1* _p):_shared_ptr<_Tp>(_p){}
};
//使用
Base* ptr = new Derived1;
//shared_ptr想实现 派生类和基类 间的向上转型,如下
shared_ptr<Base1> sptr(new Derived1);
2.语法
- 在成员函数上套一个template的声明即可
3.用途
- 显而易见,在给出的两种实例中就是给天然不具备向上转型的复杂类型,提供向上转型的能力。
- 多见于标准库中的类的构造函数,注意必须满足类构造函数对变量的赋值规则,所以基本是就是用在派生类和基类间。
十一、specialization 模板特化
1.实例
template<class key>
struct hash {};
template<>
struct hash<char>{
size_t operator()(char x) const {return x;}
};
template<>
struct hash<int>{
size_t operator()(int x) const {return x;}
};
template<>
struct hash<long>{
size_t operator()(long x) const {return x;}
};
//使用
cout << hash<long>()(1000);//hash<long>() 用于寻找特化的模板
//(1000)的小括号用来调用操作符
十二、partial specialization 偏特化
1.实例
//实例一,个数上的“偏”
template<typename T, typename Alloc=……>
class vector{
};
template<typename Alloc=……>
class vector<bool, Alloc>{
};
//实例二,范围上的“偏”
template<typename T>
class C{
};
template<typename T>
class C<T*>{
};
//使用
C<string> obj1; //使用实例二中的第一套泛化模板
C<string*> obj2; //使用实例二中的第二套偏特化模板
2.语法
见实例,语法较简单
3.用途
- 偏特化是客户个性化需求的产物。对于实例一来说,若创建bool类型的容器,势必会造成空间的浪费(vector最小的元素类型是char 8bit,但是bool值只有1bit)。所以针对多个模板参数的模板类,可以对部分参数进行特化,但类似构造函数缺省参数,需从前往后进行。
- 实例二,范围从模板的所有类型缩小到模板的所有类型指针。
十三、template template parameter 模板模板参数
1.实例
//实例一
//定义
template<typename T,
template<typename T>
class Container
class XCls{
private:
Container<T> c;
public:
};
//使用
XCls<string, list> mylst1; //报错,因为list除了元素类型这一参数外,还有分配器的参数,所以需要提前指定后才能使用。
XCls<string, Lst> mylst2; //正确
template<typename T>
using Lst = list<T, allocator<T>>;
//实例二--------------------------------------
//定义
template<typename T,
template<typename T>
class SmartPtr
class XCls{
private:
SmartPtr<T> sp;
public:
XCls() : sp(new T){}
};
//使用
XCls<string, shared_ptr> p1; //正确,大部分智能指针只有一个模板参数,所以此处的报错与此无关,与各自指针的特性有关。
XCls<string, auto_ptr> p2; //正确
XCls<string, weak_ptr> p3; //错误
XCls<string, unique_ptr> p4; //错误
//实例三------------------------------------
//定义
template<class T, class Sequence = deque<T>>
class stack{
protected:
Sequence c; //底层对象
};
//使用
stack<int> s1;
stack<int, list<int>> s2;
2.语法
- 使用模板类“template< T > class X ” 作为模板类的模板参数
- 注意实例三中的模板参数,由于使用上必须制定模板所需要的类型,所以不属于模板模板参数
3.用途
十四、C++标准库
十五、C++11的三个重要特性
15.1 variadic template
1.实例
//print的空参数重载定义
void print(){
}
//print的模板函数定义
template<typename T, typename… Types>
void print(const T& firstArg, const Type&… args){
cout<<firstArg<<endl;
print(arg…);
}
2.语法
- 在对应的地方加上…即可,表示一个包(pack)
- 用于template paraneters,就是template paraneters pack。
- 用于function parameters types,就是function parameters types pack。
- 用于function parameters,就是function parameters pack。
- pack的作用就是把多个参数分为一个参数和一包参数。程序员自行去定义对一个参数和一包参数的操作,不一定需要像实例一样对一包参数进行递归调用。
- 使用不定模板时,可以使用 “sizeof…()” 的函数显示一包参数的数量。
3.用途
- 用于参数数量不定的函数。如实例中的print函数,可以接受数量不定的参数并换行输出。
- pack将print接收的多个参数划分为单个参数的firstArg和一包参数args。
- print中的处理逻辑:每次都标准输出firstArg,然后将args传入print继续递归调用。
- 在最后一次递归调用print时,此时print接收到0个参数,无法满足print的模板函数定义所需的第一个参数。所以需要定义print的无参重载定义,以供最后一次0参数的print调用。
15.2 auto 一种语法糖
1.实例
//情况一
list<string> c;
list<string>::iterator ite;
ite = find(c.begin(), c.end(), target);
//情况二,等价情况一
auto ite = find(c.begin(), c.end(), target);
//情况三,错误,未赋值,编译器无法自动推导变量类型
auto ite;
ite = find(c.begin(), c.end(), target);
2.语法
- auto
3.用途
- 在自我清楚的情况下,简写一些文本较长的变量。
- 只适用于定义变量就赋值的情况。
15.3 ranged- base for
1.实例
//decl 是一个自定义的变量名 coll必须是一个容器
for (decl : coll) {
statement
}
vector<double> vec;
for (auto elem : vec) {
cout << elem << endl; //因为是pass by value 修改elem不会影响vec中的元素
}
for (auto& elem : vec) {
elem *= 3; //pass by reference
}
十六、reference
1.实例
1.1 实例一
int x = 0; //x是一个变量,int类型。
int* p = &x; //p是一个变量,pointer to integer,p指向x。
int& r = x; //r是一个变量,reference to integer,r代表x。
int x2 = 5;
r = x2; //并不代表r代表x2了,只是r的值改成了x2的值,此时r和x的值都是5
int& r2 = r; //r2代表r,r代表x,即r2代表x
1.2 实例二
1.3 实例三
1.4 实例四
double imag(const double& im){}
double imag(const double im){} //Ambiguity
2.语法
- 三种类型(值/指针/引用)占用内存情况:举例int占用4字节;指针在32位的pc中,占用4个字节;引用占用的字节与被引用的变量类型所占字节相等。
- 引用在实现上,都是用指针去实现;在逻辑上要把引用看成被代表的变量。
- 引用必须赋初值,且后续不会改变去代表其他变量,但是可以被改变值。
- 引用与被引用的变量大小相等,引用和被引用变量的地址也相等(是一种假象)。
- 引用很少用于声明变量,而是用在参数类型和返回值上。因为用于参数类型时,在函数体内引用的参数与值参数所调用的操作一样,均是使用 . 操作符,且在调用函数,传入的参数写法也相同。并且传引用还比传值快(为什么?不是大小相等吗,难道就因为大小相等是假象且引用有一根指针)。
- 函数签名是指函数名和参数,不包括返回值类型,注意修饰函数签名的const关键字也是函数签名。函数签名相同的两个函数不能同时存在。
- 像在实例四中会被认为是函数签名相同,因为在调用函数时都是 imag(obj),编译器无法区分。
3.用途
十七、复合&继承关系下的构造与析构
- 继承 :构造base->self 析构self->base
- 复合: 构造component->self 析构self->component
- 继承&复合(与编译器相关,不一定都是这个规则):构造base->component->self 析构self->compoent->base
十八、对象模型:关于vptr和vtbl
1.实例
分析:
- A有两个虚函数A::vfun1和A::vfun2;B全部继承A的虚函数,同时推翻了A的vfun1,定义了自己的vfun1,B有两个虚函数B::vfun1和A::vfun2;C全部继承B的虚函数,同时推翻了B的vfun1,定义了自己的vfun1,C有两个虚函数C::vfun1和B::vfun2(A::vfun2),此时系统共有4个虚函数,见上图的第3列的virtual functions。
- 在每个对象的虚指针所指向的虚表中都有相应的指针指向各自包含的虚函数。每个表都有A::vfun1,同时也有各自定义的vfun2。
- 图中最下方的两行代码,是描述了一个过程:p是指向c的一根指针,此时用p调用C类定义的一个虚函数,此时编译器走的不是call XXX的静态绑定路线,而是从虚指针到虚表的过程。n指的是该虚函数在虚表中的位置,在类中第一个声明的虚函数,n=0。
分析:
- 上图描述了设计虚表和虚函数的使用场景。可以用只能装相同类型的容器装载不同类型的对象。将容器中的类型定义为父类指针即可,然后将子类指针赋给父类指针(向上转型)。
2.语法
-
只要有虚函数,不管有多少个,只会在对象中除虚函数以外所有的变量和函数之和的基础上增加4个字节(一根指针)
-
只要父类中定义了虚函数(对象内存有一根指针),子类中也会有虚函数(对象内存也有一根指针)。函数的继承指的是继承函数的调用权。
-
静态绑定:call XXX。动态绑定:三大条件——通过指针,该指针向上转型,指针调用的是虚函数,此时编译器走的路线就是虚函数到虚表,如下:
(*(p->vptr)[n])(p); (* p->vptr[n])(p);
-
动态绑定-虚机制-多态。虽然都是用的父类指针,但是却指向的不同的类型。
3.用途
十九、对象模型:关于Dynamic Binding
1.实例
B b;
A a = (A)b;
a.vfunc1(); //此时a是对象,就算是用b转型后赋值,也是不满足动态绑定的条件的,此处依然是静态绑定
A* pa = new B;
pa->vfunc1(); //dynamic binding
pa =&b;
pa->vfunc1(); //dunamic binding
1.1 静态绑定的汇编
- 在静态绑定的汇编中其实就是在call后给出函数确切的地址。
1.2 动态绑定的汇编
- 此处的call后的dword ptr [edx] 完成的就是最右边的c代码:按照指针-虚指针-虚表找到第n个函数指针,传入p进去。
二十 又谈const
在此处特谈const用在类成员函数的用法
const可用在成员函数中,且一般不用于全局函数。
const放在函数参数的小括号后,函数体的大括号前
1.实例
//实例一
const String str("hello world");
str.print();
//实例二
charT
operator[](size_type pos) const {/*不必考虑COW*/};
reference
operator[](size_type pos) {/*必须考虑COW*/}
//COW:Copy On Write
2.语法
- const对象调用非const方法这条路线是会报错。对于不会改变member data的函数一定要记得加上const修饰符。对象与函数的const关系参照上图表⬆️。
- 同一个函数的const和non-const版本可以同时存在(验证了前面const是函数签名的说法)。此时,有一条新规则:const object只会只能调用const版本;non-const object只会只能调用non-const版本
3.用途
- 在此着重讲一下同一函数的有无const版本。在实例二中,是string类型的两个成员函数:操作符[]的有无const的重载。string的设计涉及到共享的设计模式,可以对非常量对象赋值,此时是多个字符串共享一个空间。当如果修改某一个对象时,单独拷贝一份给它,剩下的对象依旧共享,即COW。常量对象无这种需求。
- 所以对是否是常量对象的字符串做[]操作时,需要考虑到的处理逻辑是不同的。此时利用语法2可以满足这种设计需求。设计[]的有无const版本,常量对象只调用const版本,非常量对象只调用non-const版本。
二十一、对象模型:关于this
1.实例
2.语法
- 类的成员函数都有一个隐藏的参数——this指针。此机制可以用来实现动态绑定。
- 在上例中,main中创建子类对象,调用子类对象的父类函数。此过程如代码:
CDocument::OnFileOpen(&myDoc);
此时有指针条件,也满足向上转型。同时在执行到serialize函数时,补充为:
this->serialize();
满足调用虚函数的条件,此时实现动态绑定机制。
3.用途
- 如图中的template method就是借由对象模型中的动态绑定机制实现的。在父类的函数中除通用逻辑以外,存在需要对应子类才能进行的特殊操作,那么就可以将这部分代码做成虚函数。
二十二、关于new,delete,operator new,operator delete
1.实例
new 和 delete是不能被重载的。
但是在Part I中知道,new和delete的实现基于operator new 和 operator delete。
这两个函数是可以被我们重载的。
1.1 实例一
重载的接口写法示例:(实例一)
1.2 实例二
类中的operatpr new 和 delete 重载:(实例二)
1.3 实例三
类中的operatpr new[] 和 delete[] 重载:(实例三)
1.4 实例四
一个完整的重载示例:(实例四)
1.5 实例五
重载后窥见new和delete实际做的动作:(实例五)
2.语法
- 如上图实例一二三展示。
- 实例四中完整展示重载模板,右边的重载函数定义中的cout部分内容可以由程序员自行定义。
- 同时编译器提出特定语法,在new和delete前加操作符::,可以强制执行全局的new和delete,而绕过用户的重载版本。
- 在四中定义重载特定内容后,调用new和delete可以显示编译器在创造和回收对象时所作的特殊动作:在构建一个容量大小为n,对象大小为x字节的对象数组时,分配的内存,不仅仅是n * 字节x,而是n * x + 4字节。额外分配的4字节是计数器,用来表示数组大小,用在声明创建对象数组和回收时调用构造函数和析构函数的次数。
3.用途
- 重载operator new、delete、new[]、delete[]可以让程序员接管内存操作,用于自定义内存操作,创建内存池。
二十三、placement opertor new,delete
placement实质上还是对operator new 和 delete 的重载。
上面重载类型属于对global operator new 和 delete的定义重写,参数类型和数量都与global一致。
placement在保持与global的参数相同的基础上,可额外加上其他的参数类型。
1.实例
1.1 实例一
//placement重载后创建对象的例子
FOO* pf = new(300,'c')Foo;
1.2 实例二
给出在类中写出的placemnet 重载的实际例子
2个构造函数(无参和有参抛异常) 4个operator重载new(1个一般重载、1个标准库placement重载、2个自定义placement重载)
1.3 实例三
根据实例二的operator new重载写出对应的operator delete
同时使用多个重载new和两个构造函数创建对象
1.4 实例四
string中的reference counting(当前字符串有多少对象共享的计数器)的设计
string类中的rep类的create函数——使用placement operator new创建rep对象
Rep* p = new(extra) Rep;
2.语法
- 保证placement重载的函数具有与一般重载的一样的函数类型和个数的基础上,可以加入其他的参数。
- placement重载的operator new函数可以通过在创建对象时显性调用,但是在回收对象的时候默认调用global的operator delete或者重载的一般operator delete(优先),而不是placement。
- **只有当new的过程中抛出异常了,才会去调用对应的placement operator delete。**实例三的例子,就是在有参构造函数中抛异常用于测试:new对象的过程先调用operator new分配内存再调用构造函数,构造函数抛出异常会导致new的过程失败。在第五个创建对象的过程中,虽然没有如期打印相应的placement operator delete 的信息,但其实是是跟编译器有关,正常是会打印的。
- 不需要对每一个placement operator new都写相应的placement operator delete。
3.用途
- 像实例四中,如果需要在new对象时悄无声息的多分配一块内存区域,就可以使用placement operator new来完成。