-
在先前基础课程所培养的正规、大器的编程素养上,继续探讨更多技巧。
-
泛型编程(Generic Programming)和面向对象编程(Object-Oriented Programming)虽然分属不同思维,但它们是c++的技术主线,所以本可能也讨论template(模板)。.
-
深入探索面向对象之继承关系(Inheritance)所形成的对象模型(Object Model),包括隐藏于底层的this指针,vptr(虚指针),vtbl(虚表),virtual mechanism(虚机制),以及virtual function(虚函数)造成的polymorphism(多态)效果。
把这种类对象转化为另一种类对象。
-
转换函数没有返回值(编译器给出,防止错误)
-
转换函数通常需要加const,因为不会改变类中数据
class Fraction
{
public:
Fraction(int num,int den = 1)
: m_numerator(num),m_denominator(den){}
//将该类与operator后所跟类型相互转化时,调用下方函数
operator double() const{
return (double)m_numerator/(double)m_denominator;
}
private:
int m_numerator; //分子
int m_denominator; //分母
}
//使用场景
{
Fration f(3,5);
double d = 4+f; //因为没有对int+Fration的重载,故调用operator double()将f转为0.6
}
3.non-explicit-one-argument ctor(没有explicit关键字的一实参构造函数)
把另一种类对象,转化为该种类对象。
class Fraction
{
public:
Fraction(int num,int den = 1) //non-explicit-one-argument ctor
: m_numerator(num),m_denominator(den){}
Fraction operator + (const Fraction& f){
return Fraction(...);
}
private:
int m_numerator;
int m_denominator;
}
//使用场景
{
Fration f(3,5);
Fraction d2 = f+4; //对Fraction+Fraction的情形进行过重载,又因为Fraction的构造函数只需要传入一个参数即可。故4转化为Fraction(4,1),再调用operator+
}
conversion function vs.non-explicit-one-argument ctor
class Fraction
{
public:
Fraction(int num,int den = 1) //non-explicit-one-argument ctor
: m_numerator(num),m_denominator(den){}
//conversion function
operator double() const{
return (double)m_numerator/(double)m_denominator;
}
//non-explicit-one-argument ctor 配套+号的重载
Fraction operator + (const Fraction& f){
return Fraction(...);
}
private:
int m_numerator;
int m_denominator;
}
//使用场景
{
Fration f(3,5);
Fraction d2 = f+4; //[Error] ambiguous 两种方式均可,意义不明
}
explicit-one-argument ctor
为了解决上文提到的错误形式,我们需要在构造函数前加上explicit关键字,明确构造函数定义,使得对象只能以复合构造函数定义的形式创建,不可从其他类对象转化而来。
class Fraction
{
public:
explicit Fraction(int num,int den = 1) //non-explicit-one-argument ctor
: m_numerator(num),m_denominator(den){}
//conversion function
operator double() const{
return (double)m_numerator/(double)m_denominator;
}
//non-explicit-one-argument ctor 配套+号的重载
Fraction operator + (const Fraction& f){
return Fraction(...);
}
private:
int m_numerator;
int m_denominator;
}
//使用场景
{
Fration f(3,5);
Fraction d2 = f+4; //[Error] conversion from 'double' to 'Fraction' requested
}
4.pointer-like classes,关于智能指针
pointer-like classes,关于智能指针
因为想要设计一个类似于指针的类,但比原来的指针可以做更多的事,因此,智能指针应运而生。
templatepointer-like classes,关于迭代器class shared_ptr { public: //智能指针类中必定包含如下两个运算符重载 T& operator*() const{ //对解引用符号的重载,即对传入的指针解引用,取得数据 return *px; } T* operator->() const{ //将自己定义的类型的指针转化为已有定义的类型指针,即shared_ptr转为T类型 return px; } shared_ptr(T* p):px(p){} //传入一个指针,并赋值给类中定义的指针px private: T* px; //智能指针中必定包含一个指针 long* pn; ...... } struct Foo { ...... void method(void) {...} } //运用场景 { share_ptr sp(new Foo);//new出一个Foo类型的指针,初始化赋值给share_ptr类型的对象sp Foo f(*sp); //对sp进行解引用操作,实际上对sp中所包含的指针进行解引用 sp->method(); //回看上文对->符号的重载,事实上sp+->符号的组合应被替换为返回值px,但对于->这种符号,特殊的地方在于(c++默许?特例?),返回的内容应为px+->,故本行代码等价于 px->method() }
在库中有一种常见的内容叫做容器,而迭代器的存在意义在于指向容器中的某个元素,类似于指针,也可以理解为是一种特殊的指针。
特别的,与其他pointer-like classes可能不同的是,迭代器除了需要重载必须的*与->运算符外,还需要对++与--运算符进行重载。
以下,为链表结构以及链表迭代器的代码定义
链表的结构如下图
特别的对链表迭代器中的重载有:
reference operator*() const //此处的返回值实际上即为T&
{ return (*node).data; }
point opertor->() const
{ return &(operator*()) } //此处的返回值实际上即为T*
//结合下部分进行此返回值的解释
//it->method()整体相当于(*node).data.method()
//而(*node).data可以替换为operator*()即,operator*().method()
//更换.为->,此处将前半部分使用&符号取址转化为指针,即&(operator())->method()
//故函数返回值应该为&(operator())
//进一步解释
list::itertor it;
...
*it ; //获得一个Foo object
it->method(); //意思是调用Foo::method();
//相当于(*it).method();
//相当于(&(*it))->method()
5.function like classes
function like classes,所谓仿函数
以下以 identity、select1st、select2nd三个function like classes(仿函数)为例,来了解仿函数的代码定义:
template标准库中,仿函数所使用的奇特的base classes 6.namespace 经验谈struct identity{ const T& //此处加上const的用意在于避免出现c = identity(a) = b 的情况出现,这种技巧常见于运算符重载中 operator()(const T&x)const //第一个()表示对()运算符的重载,第二个()才开始真正传入参数 { return x; } } template // 此处随定义模板类,但暗示数据类型应为pair型 struct select1st{ const typename Pair::first_type& operator()(const Pair& x) const { return x.first; } }; template struct select2nd{ const typename Pair::second_type& operator()(const Pair& x) const { return x.second; } }; //补充pair数据类型的代码定义 template struct pair{ T1 first; T2 second; pair() : first(T1()),second(T2()){} pair(const T1& a,const T2& b) :first(a),second(b){} ...... }
在大型的项目之中,可能会面临定义的函数、类、变量名称冲突的问题,为了避免这种情况,我们会用不同的namespace将不同成员编写的代码块相互区分开,以至于遇到相同的函数命名时也可以通过不同的namespace名称区分开来。
using namespace std; //--------------------------------- #include7.class template,类模板 8.function template,函数模版 9.member template,成员模版#include //share_ptr namespace jj01 { void test_member_template() {......} }//namespace //---------------------------------- #include #include //share_ptr namespace jj02 { template
using Lst = list >; void test_member_template_param() {......} }//namespace //---------------------------------- int main(int argc,char** argv) { jj01::test_member_template(); jj02::test_template_template_param(); }
成员模版常见于模版类的构造函数中,即对已经确定数据类型的类成员中的某些成员的数据类型再次做模版化的处理,使得构造函数更有弹性,这种代码编写技巧叫做成员模版。
成员模版的代码规则如下:
template//对类中的数据定义为模版类型T1、T2 struct pair{ ... T1 first; T2 second; pair():first(T1()),second(T2()){} pair(const T1& a,const T2& b): first(a),second(b){} template //在定义过数据类型后,可对其中某些可定义为模版的内容进行再次定义 pair(pair & p):first(p.first),second(p.second){}//此处能否传入的判断点在于,p.first的类型U1,能否传入first的类型T1之中,即U1是否为T1的子类?(不确定) }
具体的运用案例如下:
另一个智能指针的例子:
在泛化即模版化中,我们对某些数据类型进行了模版化,而特化即对模版化的数据类型可做一个预定义,在之后创建此类型的模版对象时,优先调用特化类中的内容。
template11.partial specialization,模版偏特化 个数的偏//此处将类hash中数据类型泛化为key struct hash{}; //模版特化 template<> //特化时,仍需调用此语句,不过不用制定模版类型名称 struct hash { //将模版类型key特化为char size_t operator()(long x) const {return x;} } //运动场景 { //hash ()创建末班类型为long的临时对象,后一个()中的内容为初始化,而上边已经对模版数据类型定义为long的情况进行特化,故优先调用特化内容 cout< ()(1000); }
对含有多个参数的模版类型,对其中的部分参数进行特化(非全部),即为参数个数上的偏特化。
下列案例的特化意义在于节省空间:
templateclass vector { ... }; template class vector { ... };
范围上的偏注意:个数上的偏特化情况不可发生跳跃,即对变量的特化要从左向右连续进行。
可以对模版类型的参数范围做一定的限定,但不限定于某一种(范围缩小),为范围上的偏特化。
例如如下案例将所有数据类型的参数特化为指针:
template12.template template parameter,模版模版参数class C { ... }; template //注意,特化处的T与泛化处的T意义不同,可以用其他类型名更换,例如typename U等 class c { ... }; //使用场景 { c obj1; // 调用泛化下代码 c obj2 // 调用偏特化下代码(范围上的偏) }
形如
templateclass Container >
一个模版参数本身是一个模版,即为模版模版参数。
//整个模板类构建意义在于,保证容器中存储数据的类型弹性的同时,保证存储容器的类型的弹性 template13.关于c++标准库 14.三个主题 variadic template,数量不定的模版参数(since C++11)//一个模版类 class Container //此处类的名称也是随意定义,因为此处的类为模版类 > class XCls { private: Container c; public: ...... }; //为了应对下列第一种不通过的情况,不通过的原因为容器的参数不止一个,之前不用给出类型值的原因为编译器会给出默认参数,但此处不可 template using Lst = list >; //运用场景 XCls mylst1; //错误 Xcls mylst2; //正确
在C++11之前,模版的定义中我们有多少个不同的类型就需要定义几个类型名称,但c++11中,我们可以将多个模版参数分为一个模版参数和一个模版参数包(template parameters pack),具体示例如下:
void print() //函数重载,无参数传入调用此函数
{
}
template //此处即为数目不定参数模版的语法
void print(const T& firstArg,const Types&... args)
{
cout<
print(7.5,"hello",bitset<16>(377),42);//此处模版参数包中的数据类型不需要相同
//屏幕打印的内容如下
7.5
hello
0000000101111001
42
}
...就是一个所谓的pack(包)
- 用于template parameters,就是template parameters pack(模版参数包)
- 用于 function parameters types,就是function parameters types pack(函数参数类型包)
- 用于 function parameters,就是function parameters pack(函数参数包)
auto (since c++11)另:如果想要知道“包”中到底装了多少个参数,可以由sizeof()指令来查询
在c++11之前,如果我们要定义一个迭代器,并从对应的容器中寻找内容,我们可能要书写如下代码:
listc; //首先我们需要定义一个确定类型的容器 .... //向容器中添加数据 list ::itertor ite; //创建一个指定数据类型的容器对应的迭代器对象 ite = find(c.begin(),c.end(),target); //去接受查找的结果
但c++之后,我们不需要去写一些复杂的变量类型定义,可以通过auto指令来交给编译器自己判断:
auto ite = find(c.begin(),c.end(),target); //注意,下面的代码段是错误的,原因是auto不知道自己要去识别为什么类型 auto ite; ite = find(c.begin(),c.end(),target);;ranged-base for(since c++11)
//另一种形式的for遍历,语法为
for(decl:coll){
statement
}
//运用场景 vector15.referencevec; ... for(auto elem:vec){ cout< elem*=3; //遍历元素,改变元素值,pass by reference }
变量、指针、引用的定义:
int x = 0; int* p = &x; int& r = x; //r代表x。现在r,x都是0,引用可以理解为起别名 int x2 = 5; r = x2; //r不能重新代表其他物体。现在r,x都是5,等同于赋值 int& r2 = r; //现在r2是5(r2代表r;亦相当于代表x)
三者在内存中的分布如下图:
reference的常见用途注意:如上图所示,实际上三个内容在内存中处于不同的位置,但关于引用需要特别强调的是:
1.sizeof® == sizeof(x);
2.&x = &r;
即object和其reference大小、地址均相同**(全都是假象)**
16.复合&继承关系下的构造和析构
17.对象模型(Object Model):关于vptr和vtbl --多态注意:当一个类即存在复合又存在继承关系,在执行构造函数时先执行Base类的构造(继承),再执行component类的构造(复合),最后执行自身类的构造,而析构执行的顺序与之相反。
在继承关系中,如果一个类中存在有虚函数,则在内存中会开辟一块空间存放vptr(虚拟指针),用于指向vtbl(虚拟表格),而vtbl则存放着指向虚函数的指针。
与此同时,不论该类的子类有没有对虚函数进行覆写,子类在内存中同样的存在一块空间来存放vptr以便使用。
如果子类对虚函数进行了覆写。那么vtbl中则会存放指向覆写的新的函数的指针,即覆写后的函数的地址。
vptr和vtbl出现的意义在于:改变c中使用call()通过地址调用对应函数的静态绑定模式,通过
- 指针调用
- 指针向上转型
- 调用虚函数
来动态绑定。
动态绑定不再局限于通过地址来找函数,而是通过指针p指向的对象的不同来动态寻找对应的函数。
另附:多态关系图,其中C(正方形)继承与B(矩形)继承与A(图形)
18.对象模型(Object Model):关于thisthis指针+动态绑定,可以在父类定义的函数中调用未来子类中覆写的函数,通过谁调用函数谁就是this的特性。
19.关于Dynamic Binding 静态绑定动态绑定
关于const
| ___ | const object(data members不得变动) | non-const object (data members)可变 |
|---|---|---|
| const member functions(保证不更改data members) | ✔ | ✔ |
| non-const member functions(不保证data members不变) | ✖ | ✔ |
注意:const算在签名中,可以被用于函数重载的区分。
当成员函数的const和non-const版本同时存在,
const object 只会(只能)调用const版本
non-const object 只会(只能)调用non-const版本。
以上的强调为了应付如下情况:
class template std::basic_string<...>
//该类有如下两个member functions
char T
operator[](size_type pos)const
{
......
}
reference
operator[](size_type pos)
{
......
}
// COW:Copy on Write
20.关于New和Delete
21.Operator new,operator delete
在全局对new、delete操作符进新重载
重载member operator new/delete
重载member operator new[ ]/delete[ ]
36.示例(重载new和delete) 37.重载new(),delete()示例 尾声
至此,这阶段c++部分的学习告一段落了,但c++知识的海洋永无边界,对c++的学习也永无止境。
STL标准库的宝库,还等着我们前去发掘,呈递我们到达新的高度!



