C++学习笔记(1)

0

本文涉及:

  • conversion function

  • non-explicit one argument constructor

  • explicit one argument constructor

  • 智能指针

  • 迭代器

  • 仿函数

转换相关知识

conversion function(转换函数)

转换函数涉及某一种类型的转换,对于某一个类A,存在A转为另一种类型B(A–>B),或B转为A(A<–B)的情况,当然这里谈的是类的实例

1
2
3
4
5
6
7
8
9
10
11
class Fraction	//分数
{
public:
Fraction(int num,int den = 1):m_numerator(num),m_denominator(den){}
operator double() const{
return (double)(m_numerator / m_denominator);
}
private:
int m_numerator; //分子
int m_denominator; //分母
};

分数可以视为一个有理数,分子除以分母即为double,所以设计一个分数,一种很合理的解释是它可以被当成double。

设计一个转换函数,格式为operator type() const{...},不可以传入参数,也没有返回类型,返回类型由type给出,转换函数也不应该改变内部的data,所以通常用const去限定。

operator double() const{ return (double)(m_numerator / m_denominator);

当然分数转为有理数就是分子除以分母。

1
2
Fraction f(3,5);
double d=4 + f; //调用operator double()将f转换为0.6

编译器在执行4 + f时,可能会去找一个全局的函数,operator+ (int,Fraction),如果能找到,这一行就可以通过,但是没有;编译器也可能试图将f转为double,double + double,于是去找转换函数,将3/5转为0.6。

以上为一个转出(A–>B)的例子,在认为合理的情况,可以设计多个转换函数,当然也不限制转出为基本类型,可以为任何已经出现的type。

non-explicit one argument constructor(隐式的单实参构造函数)

1
2
3
4
5
6
7
8
9
10
11
12
class Fraction	//分数
{
public:
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(int num,int den = 1) : m_numerator(num) , m_denominator(den) {}对于这个构造函数,包含两个参数,但后面一个有默认值,若创建分数只给出第一个参数,默认分母为1。其实这样设计是合理的,数学中3即为3/1。

这样设计之后,这个构造函数就是one argument(单实参),理解为只要一个参数就足以,但也不妨接受两个参数。

1
2
3
Fraction f(3,5);
Fraction d1=f + 4; //调用 non-explicit ctor 将f转换为Fraction(4,1)
//然后调用operatoe+

编译器在执行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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Fraction	//分数
{
public:
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; //分母
};

若转换函数和非显式的单实参构造函数并存,编译将会出现错误。

1
2
Fraction f(3,5);
Fraction d2=f + 4; //[error] ambiguous

执行f+4时,编译器可以将4转换为Fraction吗?可以,调用构造函数即可,Fraction和Fraction相加,可以吗?也可以,调用operator+即可;但是,将Fraction转为double也可以行得通。

对于编译器而言,有两条路线可行,没有好换之分,产生二义性问题!ambiguous

所以对于类体的设计,设计者要充分考虑,既要实现必要的功能,也必须避免二义性问题发生。

explicit one argument ctor(显示的单实参构造函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Fraction	//分数
{
public:
explicit Fraction(int num,int den = 1):m_numerator(num),m_denominator(den){}
//explicit 明白的,明确的
operator double() const{
return (double)(m_numerator / m_denominator);

Fraction operator+ (const Fraction& f){
return Fraction(...);
}
private:
int m_numerator; //分子
int m_denominator; //分母
};

构造函数加入explicit关键词,告诉编译器,不要自动做操作,我的设计是明确的,即设计为构造函数就在需要构造函数时才调用。这样的话,阻止上述(3 –> 3/1)的发生。

1
2
Fraction f(3,5);
Fraction d2=f + 4; //[error] conversion from 'double' to 'Fraction' requested 请求将'double'转换为'Fraction'

所以执行f+4时,4 无法变为 4/1,尝试将分数转为有理数时,所以发生了错误。

explicit关键字的使用场景不多见,使用在构造函数之前时一种较为常见的情况。


标准库中有使用到转换函数例子

1

特殊的类

一个C++的class设计出来,可能会像两种东西,一种是所产生的对象行为像一个指针,另一种所产生的对象行为像一个函数。

pointer-like classes(关于智能指针)

为什么设计的类要像一个指针,因为我们期望它可以实现比指针更多的功能,通常把这样的类称为智能指针。

C++2.0之前就包含智能指针(现在已弃用),之后有好几种智能指针。每一种智能指针中,一定包含一个真正的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
......
};

上面展示的代码模拟了shared_ptr的写法,是早期的标准库写法,新版本做了很大的改动。对于指针T* px,是类中真正的指针,指向一个T类型的对象。

2

对于裸指针(普通的指针)所能允许的操作,设计的对象也必须能够允许。可以作用在shared_ptr上的操作符例如*、->操作符,要做操作符重载,因为这个智能指针要代表裸指针,所以要支持指针操作。当然,指针会包含其他的一些操作,但*、->是很好的例子。

1
2
3
4
5
6
7
struct Foo
{
......
void method(void) {......}
}

shared_ptr<Foo> sp(new Foo);

将裸指针包装到“聪明”的指针中,shared_ptr就要实现构造函数接受裸指针,shared_ptr(T* p) : px(p) {}

1
2
Foo f(*sp);		//对应解引用
sp->method(); //px->method();

使用者对于sp,当成一般的指针使用。*sp相当于解引用操作,设计者要实现对应的呼应,return *px返回真正的对象;sp->通过智能指针箭头符号,调用指向对象的成员,设计者实现return px传出真正的指针。

3

对于->符号的理解,智能指针已经调用了operator-> ()函数,那真正的指针是如何访问对象成员的呢?因为->对于作用的结果,具有向下一级传递的性质,为什么会出现这种情况?因为这是C++语法的实现。

对于作用的结果,等同于真正的指针调用对象,正确性是毋庸置疑的。

pointer-like classes(关于迭代器)

C++标准库中包含很多重要的容器,容器本身带着迭代器。迭代器的重要功能是代表容器中的元素, 因此它也像一个指针,也可以描述为一种智能指针。

4

对比shared_ptr,迭代器不仅要处理*和->,还要重载++、--、==、!=。其他类型的指针,可能不必处理++、--,当然也就可能不需要移动指针。但迭代器需要遍历整个容器,所以可以移动是不可或缺的功能。

5

标准库提供的链表是双向链表,内部维护两个指针,与智能指针同理,迭代器也必然维护一个真正的指针——nodenode指向链表节点。

6

链表的使用者自然不用理会内部的细节,但设计者要考虑对迭代器做解引用,目的是拿到节点的data对象,所以要做出的呼应是,return (*node).data。至于->符号,思路与shared_ptr一致,返回所指向的对象的地址。

function-like classes(所谓仿函数)

上面提到过一个C++的class设计出来,可能会像两种东西,一种是所产生的对象行为像一个指针,另一种所产生的对象行为像一个函数。

对于像一个函数的理解,就要理解对于C++中的函数,是一种什么形式呢,粗略地理解返回类型 函数名(形参表){函数体},但为什么是这样呢?有一种理解是,函数是函数名调用()操作符作用的结果,而对于可以接受()操作符的,当然也可以视为函数。当然这里存在是一个函数或是像一个函数的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class T>
struct identity /*继承*/{
const T& operator() (const T& x) const
{ return x; }
};

template<class Pair>
struct select1st /*继承*/{
const typename Pair::frist_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; }
};

上述的例子在标准库中没有说明,但有些编译器有这种例子,这三个类都对()操作符做了重载,可以接受这个操作符,这样设计的类,行为就像函数。

对于select1st来说,它可以返回Pair类型一对中的第一个成员。

select1st<pair> ()()

对于第一个括号,表明创建了一个临时的无名对象且并没有传入任何参数,而第二个括号是调用operator()函数重载的操作符。

1
2
3
4
5
6
7
8
9
template<class T1,class T2>
struct pair {
T1 first;
T2 second;
pair() : first(T1()),second(T2()) {}
pair(const T& a,const T& b)
: first(a),second(b) {}
......
};

对于标准库中pair早期的实现,可以将两种类型的对象包装成一对使用。

类中对括号实现重载,设计者的用意就是,是它像一个函数一样使用。这种类型的实例称为函数对象,或者仿函数。

仿函数实现中注释的继承,其实是unary_function<typename,typename>,而标准库中其他的仿函数也是类似的实现。对于这些仿函数,都有一些奇怪的base classes。

7


总结本文提到的内容,无一不是对操作符重载的使用,引用侯捷老师的话说,操作符重载是标准库中很重要的组成。

-------------本文结束感谢您的阅读-------------