第一节 导读
知识清单:
首先来列一张清单,清点一下后面课程将会深入的细节:
- operator type()const;
- explicit complex(...):initialization list{}
- pointer-like object
- function-like object
- Namespace
- template specialization
- Standard Library
- variadic template(since C++11)
- move ctor(since C++11)
- Rvalue reference(since C++11)
- auto(since C++11)
- lambda(since C++11)
- range-base for loop(since C++11)
- unordered containers(cince C++11)
目标:
- 在先前的基础课程所培养的正规、大气的编程素养上,继续探讨更多技术。
- 泛型编程(Generic Programming)和面向对象编程(Object-Oriented Programming)虽然分属不同思维,但它们正是C++的技术主线,所以本课程也讨论template(模板)。
- 深入探索面向对象之继承关系(inheritance)所形成的对象模型(Object Model),包括隐藏于底层的this指针,vptr(虚指针),vtbl(虚表),virtual mechanism(虚机制),以及虚函数(virtual functions)造成的polymorphism(多态)效果。
推荐书目
- 《C++ Primer》
- 《The C++ Programming Language》
- 《Effective Modern C++》
- 《Effective C++》
- 《The C++ Standard Library》
- 《STL源码剖析》
第二、三节 non-explicit one argument constructor & Conversion Function(转换构造与类型转换函数)
为了方便对照学习,记忆,决定把二三节内容放在一起讲解。
转换构造函数
定义
在CPP中,类的构造函数可以省略不写,这时CPP会为它自动创建一个隐式默认构造函数(implicit default constructor);也可以由用户定义带参数的构造函数,构造函数也是一个成员函数,他可以被重载;当一个构造函数只有一个参数,而且该参数又不是本类的const引用时,这种构造函数称为转换构造函数(non-explicit ont argument constructor)。(该段引自百度百科)
class Complex
{
private:
double real,imag; //复数的实部和虚部
public:
Complex(double x)
{
real=x;
imag=0;
}
//与下方等价
/*
Complex(double x,double y=0):real(x),imag(y)
{
}
*/
};
这个构造函数即 转换构造函数。
如上文。构造函数只有一个参数 double x,它也不是本类的const引用。
应用
通过转换构造函数可以将一个指定类型的数据转换为类的对象。
1.用于定义
转换构造函数一般由系统自动调用(当然代码里自己调用完全没问题),这点很利于编程。
例如:
- Complex t=5.0;
- Complex t(5.0);
- Complex t=Complex(5.0);
- Complex t=(Complex)5.0;
这时系统就自动调用了 Complex(double x)将 5.0转换成Complex类,再赋值给t。
2.用于计算
通常来讲,转换构造函数更多搭配运算符重载用来计算。
class Complex
{
public:
Complex(double x,double y=0)//转换构造
:real(x),imag(y){}
Complex operator+(const Complex& f)//操作符重载
{
return Complex(......);
}
private:
double real;
double imag;
};
Complex t=5.0;
Complex b=t + 4.8;
编译器会隐式调用转换构造函数将5.0转换为Complex成员并赋值给t。第二步同理,将4.8转换成Complex成员后调用'+'的重载函数完成计算。
类型转换函数
通过转换构造函数可以将一个指定类型的数据转换为类的对象。但是不能反过来将一个类的对象转换为一个其他类型的数据(例如将一个Complex类对象转换成double类型数据)。
C++提供类型转换函数(type conversion function)来解决这个问题。类型转换函数的作用是将一个类的对象转换成另一类型的数据。如果已声明了一个Complex类,可以在Complex类中这样定义类型转换函数:
operator double() const//类型转换函数
{
return real;
}
从函数结构来看,与重载函数类似,都需要关键字operator,只不过这里的转换的是类型而已,double在Complex类中经过重载后,Complex就被赋予了一种新的含义,既可以当做Complex类型本身使用,也可以当做double类型来使用。
我们来举一个简单的例子:
class Complex
{
public:
Complex():real(0),imag(0)
{}
Complex(double x,double y):real(x),imag(y)
{}
operator double() const
{
return real;
}
private:
double real;
double imag;
};
Complex t(5,0);
Complex b=t + 4.8;
此时我们的b=t+4.8运算有了另一种解法,即将Complex对象t通过隐式调用类型转换函数转换为double对象完成计算。
小结:
- 转换构造函数可以将一个指定类型的数据转换为类的对象。
- 类型转换函数可以将一个类的对象转换为一个其他类型的数据。
我们了解了转换构造函数与类型转换函数可以为Complex b=t+4.8这样的运算提供两个不同角度的解法,那么如果Complex类同时拥有了两种函数,又会怎样呢?
class Complex
{
public:
Complex():real(0),imag(0)
{}
Complex(double x,double y=0)//转换构造
:real(x),imag(y){}
Complex(double x,double y):real(x),imag(y)
{}
Complex operator+(const Complex& f)//操作符重载
{
return Complex(......);
}
operator double() const//
{
return real;
}
private:
double real;
double imag;
};
Complex t(5,0);
Complex b=t + 4.8;
编译器会提示ambiguous(歧义),即有多重解。当编译器可选择的方案不止一种,会出现这种提示。在案例中,编译器既可以通过转换构造函数将4.8转换为Complex对象,与可以通过类型转换函数将t转换为double对象完成计算。
但是在实际使用的过程中,难免会遇到这种情况,好在CPP为我们提供解决的办法:explicit。
explicit关键字多用在转换构造函数之前,其作用是指定该构造函数只能被显示调用(即创建实例时的调用,如Complex a(5,0)),而不可以再被隐式调用,这样就解决了程序的ambiguous问题。
第四节 pointer-like classes(关于智能指针)
我们知道智能指针能够比原生指针做更多事情,例如处理线程安全,提供写时复制,确保协议,并且提供远程交互服务等等等等许许多多强大的功能。但其实不论是多牛的智能指针,在它的内部一定至少有一个原生指针在工作。现在就我们从语法的角度来初窥智能指针。
以shared_ptr为例:
template<class T>
class shared_ptr
{
public:
T& operator*() const//重载*
{return *px;}
T* operator->() const//重载->
{return px;}
shared_ptr(T* p):px(p){}
private:
T* px;
long* pn;
}
struct Foo
{
void method(){}
};
shared_ptr<Foo> sp(new Foo);
Foo f(*sp);
sp->method();
//px->method();
解析:
智能指针的本质,其实就是把一个原生指针包装在类中,再向类中写进各种对指针操作符的重载,使用户在使用类时可以完全按照指针的语法去使用。这样做的优势很明显,我们可以根据自己的需求向类中写入各种功能,相当于“组装一个无所不能的指针”。
语法其实不难理解,只是重载一下指针的操作符"*"与"->",但是其中有一个小细节很容易被忽略,及时是有多年经验的工程师也未必能解释清楚,在这里再次感谢侯老师。我举个例子:
我们已经对操作符“*”和“->”进行了重载,当编译器在执行*sp时,返回值是*px,这很好理解,可是在执行sp->时,返回值是sp,为什么能起到和sp->一样的效果呢?
原来,CPP为了支持这种做法,在这里进行了特殊的处理,使得->可以无限次的使用,即在sp之后自动补齐->。(注:只有在这种情况下)
看完了shared_ptr,我们再来看看迭代器。
迭代器(iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址。迭代器修改了常规指针的接口,所谓迭代器是一种概念上的抽象:那些行为上像迭代器的东西都可以叫做迭代器。然而迭代器有很多不同的能力,它可以把抽象容器和通用算法有机的统一起来。
迭代器提供一些基本操作符:*、++、==、!=、=。这些操作和C/C++“操作array元素”时的指针接口一致。不同之处在于,迭代器是个所谓的复杂的指针,具有遍历复杂数据结构的能力。其下层运行机制取决于其所遍历的数据结构。因此,每一种容器型都必须提供自己的迭代器。事实上每一种容器都将其迭代器以嵌套的方式定义于内部。因此各种迭代器的接口相同,型号却不同。这直接导出了泛型程序设计的概念:所有操作行为都使用相同接口,虽然它们的型别不同。(以上定义摘自百度百科)
下面我们来看一下迭代器的实现:
template<class T>
struct __list_node
{
void* prev;
void* next;
T data;
}
template<class T,class Ref,class Ptr>
struct __list_iterator
{
typedef __list_iterator<T,Ref,Ptr> self;
typedef Ptr pointer;
typedef Ref reference;
typeder __list_node<T>* link_type;
link_type node;
bool operator==(const self& x)const{return node==x.node;}
bool operator!=(const self& x)const{return node!=x.node;}
reference operator*()const{return (*node).data;}
pointer operator->()const{return &(operator*());}
self& operator++(){node=(link_type)((*node).next);return *this;}
self operator++(int){self tmp=*this;++*this;return tmp;}
self& operator--(){node=(link_type)((*node).prev);return *this;}
self& operator--(int){self tmp=*this;--*this;return tmp;}
};
我们把其中隔开的两个函数抽出,简单的分析一下。
从使用者的角度来讲,只会通过右上角的方式来调用,然而实际的处理过程如左侧所示:
- 当执行*ite时,会获得(*node).data;其中*node为一个object,data为其中的成员。
- 当执行ite->method()时,会调用上方的operator*()获得(*node).data,返回其地址。
这样一来就完美的将原生指针node包裹在了迭代器__list_iterator中。
第五节 function-like classes (仿函数)
仿函数在标准库中有着广泛的应用,这节课我们将从标准库中抽取一个案例来探讨仿函数的用法,对于为什么要让一个类模仿函数行为,这节我们不做讨论。
通常来讲,如果一个东西可以接收小括号这种操作符我们就叫它函数,或者像函数的东西。
上面是标准库中的一段代码(有省略)。
select1st与select2st分别通过对()的重载提取pair对象的第一个元素和第二个元素。
图片中灰色处省略了部分代码,展开如下:
再来看看标准库中其它的仿函数:
我们发现标准库中的仿函数通常要继承一些古怪的base,下面是base的原型:
在这里我们不对base做任何讨论,在后面有专门讲解STL的课程会深入讲解。
第六节 namespace 经验谈
#include<iostream>
namespace lalala
{
int a=5;
}
namespace lalala1
{
int a=10;
}
int main()
{
std::cout<<lalala::a<<std::endl;
std::cout<<lalala1::a<<std::endl;
return 0;
}
很小的话题,给出一段示例代码,相信有一定C++基础的人都可以理解。
第七节 class template
template<typename T>
class complex
{
public:
complex(T r=0,T i=0)
:re(r),im(i)
{}
complex& operator +=(const complex&);
T real () const { return re; }
T imag () const { return im; }
private:
T re,im;
friend complex& _doapl(complex*,const complex&);
};
complex<double> c1(2.5,1.5);
complex<int> c2(2,6);
解析:
template的基本使用方法,不做赘述。
在定义object时指明模板类型,传入后与符号T绑定。
PS:在template<...>的尖括号中,class与typename是等价的。
第八节 Function Template(函数模板)
class stone
{
public:
stone(int w,int h,int we)
:_w(w),_h(h),_weight(we)
{}
bool operator< (const stone& rhs) const
{ return _weight < rhs._weight; }
private:
int _w,_h,_weight;
}
template<class T>
inline
const T& min(const T& a,const T& b)
{
return b<a?b:a;
}
stone r1(2,3),r2(3,3),r3;
r3=min(r1,r2);
解析:
stone两个object r1,r2,传入min函数。min函数在接收参数后将参数类型与模板类型T绑定(实参引导),确认类型后a,b进行'<'操作,编译器会进入T类(stone)类内寻找对应的重载函数来执行。
PS:该用法在“(GeekBand)C++面向对象高级编程(上)第二周笔记(1)”中有过介绍。