C++学习笔记(1)
本文涉及:
conversion function
non-explicit one argument constructor
explicit one argument constructor
智能指针
迭代器
仿函数
转换相关知识
conversion function(转换函数)
转换函数涉及某一种类型的转换,对于某一个类A,存在A转为另一种类型B(A–>B),或B转为A(A<–B)的情况,当然这里谈的是类的实例
1 | class Fraction //分数 |
分数可以视为一个有理数,分子除以分母即为double,所以设计一个分数,一种很合理的解释是它可以被当成double。
设计一个转换函数,格式为operator type() const{...}
,不可以传入参数,也没有返回类型,返回类型由type给出,转换函数也不应该改变内部的data,所以通常用const去限定。
operator double() const{ return (double)(m_numerator / m_denominator);
当然分数转为有理数就是分子除以分母。
1 | Fraction f(3,5); |
编译器在执行4 + f
时,可能会去找一个全局的函数,operator+ (int,Fraction)
,如果能找到,这一行就可以通过,但是没有;编译器也可能试图将f转为double,double + double
,于是去找转换函数,将3/5转为0.6。
以上为一个转出(A–>B)的例子,在认为合理的情况,可以设计多个转换函数,当然也不限制转出为基本类型,可以为任何已经出现的type。
non-explicit one argument constructor(隐式的单实参构造函数)
1 | class Fraction //分数 |
Fraction(int num,int den = 1) : m_numerator(num) , m_denominator(den) {}
对于这个构造函数,包含两个参数,但后面一个有默认值,若创建分数只给出第一个参数,默认分母为1。其实这样设计是合理的,数学中3即为3/1。
这样设计之后,这个构造函数就是one argument(单实参),理解为只要一个参数就足以,但也不妨接受两个参数。
1 | Fraction f(3,5); |
编译器在执行f+4
时,同样会去找operator+
,当然它找到了,Fraction operator+ (const Fraction& f){ return Fraction(...)
,但我们的设计为加法作用于分数,分数和分数相加Fraction+Fraction
,而调用它的为f+4
,与设计不同,于是编译器试图将整数4转为Fraction。由于设计了单实参构造函数农户,因此,调用构造函数,将4 –> 4/1,完成计算。
以上为一个转入(A<–B)的例子,与上例的将这种东西转为别的东西,本例将别的东西转为这种东西,方向刚好相反,都是转换,但转换函数特指conversion function。
conversion function vs. non-explicit one argument ctor
1 | class Fraction //分数 |
若转换函数和非显式的单实参构造函数并存,编译将会出现错误。
1 | Fraction f(3,5); |
执行f+4
时,编译器可以将4转换为Fraction吗?可以,调用构造函数即可,Fraction和Fraction相加,可以吗?也可以,调用operator+
即可;但是,将Fraction转为double也可以行得通。
对于编译器而言,有两条路线可行,没有好换之分,产生二义性问题!ambiguous
所以对于类体的设计,设计者要充分考虑,既要实现必要的功能,也必须避免二义性问题发生。
explicit one argument ctor(显示的单实参构造函数)
1 | class Fraction //分数 |
构造函数加入explicit关键词,告诉编译器,不要自动做操作,我的设计是明确的,即设计为构造函数就在需要构造函数时才调用。这样的话,阻止上述(3 –> 3/1)的发生。
1 | Fraction f(3,5); |
所以执行f+4
时,4 无法变为 4/1,尝试将分数转为有理数时,所以发生了错误。
explicit
关键字的使用场景不多见,使用在构造函数之前时一种较为常见的情况。
标准库中有使用到转换函数例子
特殊的类
一个C++的class设计出来,可能会像两种东西,一种是所产生的对象行为像一个指针,另一种所产生的对象行为像一个函数。
pointer-like classes(关于智能指针)
为什么设计的类要像一个指针,因为我们期望它可以实现比指针更多的功能,通常把这样的类称为智能指针。
C++2.0之前就包含智能指针(现在已弃用),之后有好几种智能指针。每一种智能指针中,一定包含一个真正的指针。
1 | template<class T> |
上面展示的代码模拟了shared_ptr
的写法,是早期的标准库写法,新版本做了很大的改动。对于指针T* px
,是类中真正的指针,指向一个T类型的对象。
对于裸指针(普通的指针)所能允许的操作,设计的对象也必须能够允许。可以作用在shared_ptr
上的操作符例如*、->
操作符,要做操作符重载,因为这个智能指针要代表裸指针,所以要支持指针操作。当然,指针会包含其他的一些操作,但*、->
是很好的例子。
1 | struct Foo |
将裸指针包装到“聪明”的指针中,shared_ptr
就要实现构造函数接受裸指针,shared_ptr(T* p) : px(p) {}
。
1 | Foo f(*sp); //对应解引用 |
使用者对于sp
,当成一般的指针使用。*sp
相当于解引用操作,设计者要实现对应的呼应,return *px
返回真正的对象;sp->
通过智能指针箭头符号,调用指向对象的成员,设计者实现return px
传出真正的指针。
对于->
符号的理解,智能指针已经调用了operator-> ()
函数,那真正的指针是如何访问对象成员的呢?因为->
对于作用的结果,具有向下一级传递的性质,为什么会出现这种情况?因为这是C++语法的实现。
对于作用的结果,等同于真正的指针调用对象,正确性是毋庸置疑的。
pointer-like classes(关于迭代器)
C++标准库中包含很多重要的容器,容器本身带着迭代器。迭代器的重要功能是代表容器中的元素, 因此它也像一个指针,也可以描述为一种智能指针。
对比shared_ptr
,迭代器不仅要处理*和->
,还要重载++、--、==、!=
。其他类型的指针,可能不必处理++、--
,当然也就可能不需要移动指针。但迭代器需要遍历整个容器,所以可以移动是不可或缺的功能。
标准库提供的链表是双向链表,内部维护两个指针,与智能指针同理,迭代器也必然维护一个真正的指针——node
。node
指向链表节点。
链表的使用者自然不用理会内部的细节,但设计者要考虑对迭代器做解引用,目的是拿到节点的data
对象,所以要做出的呼应是,return (*node).data
。至于->
符号,思路与shared_ptr
一致,返回所指向的对象的地址。
function-like classes(所谓仿函数)
上面提到过一个C++的class设计出来,可能会像两种东西,一种是所产生的对象行为像一个指针,另一种所产生的对象行为像一个函数。
对于像一个函数的理解,就要理解对于C++中的函数,是一种什么形式呢,粗略地理解返回类型 函数名(形参表){函数体}
,但为什么是这样呢?有一种理解是,函数是函数名调用()
操作符作用的结果,而对于可以接受()
操作符的,当然也可以视为函数。当然这里存在是一个函数或是像一个函数的问题。
1 | template<class T> |
上述的例子在标准库中没有说明,但有些编译器有这种例子,这三个类都对()
操作符做了重载,可以接受这个操作符,这样设计的类,行为就像函数。
对于select1st
来说,它可以返回Pair
类型一对中的第一个成员。
select1st<pair> ()()
对于第一个括号,表明创建了一个临时的无名对象且并没有传入任何参数,而第二个括号是调用operator()
函数重载的操作符。
1 | template<class T1,class T2> |
对于标准库中pair
早期的实现,可以将两种类型的对象包装成一对使用。
类中对括号实现重载,设计者的用意就是,是它像一个函数一样使用。这种类型的实例称为函数对象,或者仿函数。
仿函数实现中注释的继承,其实是unary_function<typename,typename>
,而标准库中其他的仿函数也是类似的实现。对于这些仿函数,都有一些奇怪的base classes。
总结本文提到的内容,无一不是对操作符重载的使用,引用侯捷老师的话说,操作符重载是标准库中很重要的组成。