C++面向对象补充(二)
一、conversion function
- 开头的情况
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;
};
int main(){
Fraction f(3, 5);
double d = 4 + f; // 调用operator double() 将f转为0.6
}
转化函数一般不会改变data,所以需要加const限定才规范。
- non-explicit-one-argument ctor
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;
};
int main(){
Fraction f(3, 5);
Fraction d2 = f + 4; // 调用 non-explicit ctor将4转为Fraction(4, 1)
// 然后再调用operator+
}
这里提到one-argument ctor的概念,即只要传入一个实参,如4,编译器就会默认创建Fraction对象(4,1),但需注意,只有前面没有加explicit时才可以,如果加了关键字explicit限定即explicit Fraction(int num, int den = 1),则编译器不会再自动将4转化为(4,1),因为explicit是具体的意思,此时需要你给出明确的指令才可以将4->(4,1)。
如果编译器在编译时出现有两种或以上情况选择的时候,编译器会认为出现歧义,无法做出判断选择,则会报错
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;
};
int main(){
Fraction f(3, 5);
Fraction d2 = f + 4; // 此时编译器无法判断,是使用方法一将f转化为double值与4相加,
// 还是使用方法二将4转化Fraction类型,再调用operator+。
}
在上面这种情况下,编译器会报错 “[Error] ambiguous“ (即出现歧义)。
注意:在开头的情况下,double d = 4 + f,也有两种转化方式即4->(4,1)或 f -> double,但因为开头的情况下没有重载+号,所以编译器不会存在歧义,只能选择f -> double来进行编译运行。
加上explicit关键字时
class Fraction{
public:
explicit 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;
};
int main(){
Fraction f(3, 5);
Fraction d2 = f + 4; // 因为关键字explicit的存在,所以4不会再转化为(4,1)
// 此时编译器进行的是将f转化为double类型
}
但此时仍报错,只不过错误信息为 "[Error] conversion from ‘double’ to ‘Fraction’ requested’’ ,即double类型的结果无法转化为Fraction类型,因为在关键字explicit的限定下,需要有明确的指令使double转化为Fraction。
explicit关键字主要用于构造函数前
二、pointer-like classes
- 智能指针
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(void){...}
};
int main(){
shared_ptr<Foo> sp(new Foo);
Foo f(*sp);
sp->method(); // px->method();
}
这里小圆圈表示shared_ptr类型的指针sp,里面包含着Foo类型的指针px。
shared_ptr sp(new Foo);
new Foo创建一个Foo类型的指针,调用shared_ptr的构造函数shared_ptr(T* p)【其传入参数为Foo类型的指针】,创建shared_ptr型指针对象sp(且Foo型指针被包含在内)。
Foo f(*sp);
* 为解引用,拿出指针所指向的具体内容。而在shared_ptr类定义里面,已经定义了对 * 的重载,所以 * 作用在sp上,然后就消耗掉了,即 *sp 表示返回Foo类型的指针 *px 所指向的内容。
sp->method();
这里用到了->的重载,首先->作用在sp上表示返回px,但继续保留->下来继续作用在px上(不像上面 * 那样作用完就消耗掉了),由此便可解释sp->method() 实际相当于px->method() 。
- 迭代器
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;
typedef _list_node<T>* link_type;
link_type node; // 是一个指针类型
reference operator*() const
return (*node).data;
pointer operator->() const
return &(operator*());
}
下图右图中,生成一个list迭代器ite(即node,是个指针类型),而 *ite 则为取出对应地址存放的data,即获得一个Foo object。
对于 ite->method(),因为->的重载实际相当于调用了一次 * 的重载,然后再用取址符号&作用于其上,则ite->method()为先取到 ite 指针指向的那块内存地址的data内容,然后再取址符号&取到对应内容的地址,实则是&(Foo object),所以相当于Foo::method(),或者(*ite).method() 或者 (&(*ite))->method() 。
三、member template
设置了4个class,其关系如图
class Base1{};
class Derived1: public Base1{};
class Base2{};
class Derived2: public Base2{};
template<class T1, class T2>
struct pair{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair():first(T1()), second(T2()){} // 类型+()就是调用这个类的默认构造函数,
// 又clss T1为函数模板,则T1()表示调用T1函数类的默认构造函数
// 则T1 first表示创建了个类型为T1的类对象first
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){}
};
int main(){
pari<Derived1, Derived2> p;
pari<Base1, Base2> p2(p);
// 以上两步相当于
// pair<Base1, Base2> p2(pair<Derived1, Derived2>());
}
对于代码pair<Base1, Base2> p2(pair<Derived1, Derived2>())
首先pair<Derived1, Derived2>(),直接调用pair的默认构造函数pair()创建了个对象,然后再将创建的对象作为参数传入,所以调用的是pair(const pair<U1, U2>& p)的构造函数。
pair(const pair<U1, U2>& p): first(p.first), second(p.second){}
另外在调用这个构造函数时需注意,在这个例子中,p.first表示鲫鱼(Derived1),而鲫鱼只能放在鱼类(Base1)中,所以first(p.first),而不是second(p.first);同理,p.second表示麻雀(Derived2),只能放在鸟类(Base2)中,即second(p.second)。
template<typename _Tp>
class shared_ptr: public _shared_ptr<_Tp>{
...
template<typename _Tp1>
explicit shared_ptr(_Tp1* _p): _shared_ptr<_Tp>(_p){}
...
};
Base1* ptr = new Derived1; // up-cast 指针上移即子类指针上移成父类指针
shared_ptr<Base1>sptr(new Derived1); // 模拟 up-cast
explicit shared_ptr(_Tp1* _p): _shared_ptr<_Tp>(_p){}
因为shared_ptr的构造函数加了关键字explicit限定,所以只有在明确传入指针参数的情况下才会调用explicit shared_ptr(_Tp1* _p)这个构造函数。
所以shared_ptrsptr(new Derived1)这里,先new出Derived1子类对象指针,然后作为传入参数,调用explicit shared_ptr(_Tp1* _p)这个构造函数,则_p代表传入的new Derived,然后赋给_shared_ptr<_Tp>即_shared_ptr,即相当于Base1* sptr = new Derived1。
四、template specialization(模板特化)
template特化,即绑定了特定的参数,则template<>里面为空,而一般类定义时设置了对应的参数,如下:
// 模板泛化,Key是任意的,可随意指定
template <class Key>
struct hash{};
// 模板特化
template<> // 下面的struct设置了特定的参数char
struct hash<char>{
size_t operator()(char x) const{
return x;
}
};
template<> // 下面的struct设置了特定的参数int
struct hash<int>{
size_t operator()(int x) const{
return x;
}
};
template<> // 下面的struct设置了特定的参数long
struct hash<long>{
size_t operator()(long x) const{
return x;
}
};
int main(){
cout << hash<long>()(1000) << endl;
return 0;
}
hash<long>()(1000)
第一个空的小括号表示设置的是临时对象
而第二个小括号表示调用了struct hash的成员函数size_t operator()(long x),因为为带参传递调用,则会调用对应的成员函数。
另外hash<long>()(1000)调用的是struct hash<long>{}而不是其他的特化版本,也不调用泛化的struct hash{},因为在指定特化的情况下会选择调用对应的特化版本。
partial specialization
- 个数的偏
// 模板泛化的情况
template <typename T, typename Alloc=...>
class vector{
...
};
// 模板偏特化的情况,相对于上面,其绑定了第一个参数typename T为 bool,
// 所以此时template<typename Alloc=...>,只需传入剩下的参数即可
template<typename Alloc=...>
class vector<bool, Alloc>{
...
};
- 范围的偏
// 模板泛化的情况
template < typename T >
class C{
...
};
// 模板偏特化的情况,相对于上面,其限定模板类型必须为指针类型,即在指针类型的范围内去任意指定模板参数
template< typename T >
class C < T* >{
...
};
int main(){
C<string> obj1; // 调用的是第一种情况
C<string*> obj2; // 调用的是第二种情况
return 0;
}
五、template template parameter(模板模板参数)
template <typename T, template<typename T> class Container >
class XCls{
private:
Container<T> c;
public:
...
};
int main(){
XCls<string, list> mylst1; // 此是错误的!!
}
此时,模板里面有模板即第二个模板参数为模板template<typename T> class Container ,Container只是一个符号名称,如也可设置为T1,U…之类的,表示可指定任意的类型。
类XCls的定义是希望可任意指定一个容器如list,然后任意指定容器list里面的元素是string类型,即设计者希望设计出来的XCls类是具有一定的弹性的,所以第二个模板参数便设置为模板模板参数类型。
比如,XCls类也可也指定为vector,然后容器vector里面的元素类型是int型。
XCls<string, list> mylst1 从模板模板参数的写法上来说是无错误的,其表示指定的Container是list容器,然后指定list里面的元素是string类型,即生成的类对象mylst1里面生成了个容器 list<string> c。
但语法上会出现错误,因为容器如list是有第二模板参数的,有的容器还有第三模板参数(平时我们用容器list的时候虽然可以list<int>,省略了第二模板参数,但那是因为有默认值定义了),而在此处用于模板模板参数时,容器有多少个模板参数就都需要定义出来,而不能省略。
所以应设置为以下形式:
template <typename T, template<typename T> class Container >
class XCls{
private:
Container<T> c;
public:
...
};
template <typename T>
using Lst = list<T, allocator<T>>;
int main(){
XCls<string, Lst> mylst1; // 此是正确的,因为其将容器list的所有模板参数都进行了设置而无省略。
// 注:此时第二个参数Lst是个未定的模板参数,
// 虽然在设计上实际是由string指定的了,但从第一层面来说其仍只是个未定的模板参数。
}
再比如,模板模板参数为智能指针的情况:
template <typename T, template<typename T> class SmartPtr >
class XCls{
private:
SmartPtr<T> sp;
public:
XCls(): sp(new T){}
};
int main(){
XCls<long, unique_ptr> p1; // 此是错误的!!
XCls<long, weak_ptr> p2; // 此是错误的!!
XCls<long, shared_ptr> p3; // 此是正确的。
XCls<long, auto_ptr> p4; // 此是正确的。
}
因为智能指针上面列举的都只指定一个模板参数即可,所以这里后面两个shred_ptr和auto_ptr可以使用正确,而前面两个使用错误是因为weak_ptr和unique_ptr的一些独特特性不可以这样使用而出错,并不是上面设计出错的问题。
注意:下面这种情况不是template template parameter !!
template <class T, class Sequence = deque<T>>
class stack{
friend bool operator== <> (const stack&, const stack&);
friend bool operator< <> (const stack&, const stack&);
protected:
Sequence c;
...
};
int main(){
stack<int> s1;
stack<int, list<int>> s2;
}
这是标准库里对stack容器类的标准定义的一部分,由stack<int> s1 可以看出,平时我们使用时只需指定第一个模板参数即可,第二个模板参数若不指定时则是默认的。
辨析:
有人认为Sequence是个模板类型名称符号,而deque<T>又是个模板参数,则class Sequence = deque<T>作用在template<>的第二个参数里面不就相当于一个模板模板参数吗?
其实对于第二个参数其已不是模板参数,因为stack<int, list<int>> s2中的第二个参数list<int>实际上已经是确定下来的了,很明确的一个参数了,而不是像XCls<string, Lst> mylst1中的第二个模板参数Lst = list<T, allocator<T>>表示的是未确定的模板参数。
六、variadic templates(数量不定的模板)
void print(){} // 此为当传入参数为空时,调用的情况。
template<typename T, typename... T1> // typename... T1表示后面的指定模板参数有数量不定个
void print(const T& firstArg, const T1&... args){
cout << firstArg << endl;
cout << sizeof...(args) << endl; // sizeof...()统计有多少个参数
print(args...); // 不断递归传递数量不定的模板参数进去
}
int main(){
print(7.5, "hello", bitset<16>(377), 42);
// 最终打印出来的结果:
// 7.5
// hello
// 00000001011111001
// 42
return 0;
}
这里,
… 此时是表示一个语法符号了,表示一个pack(包)
用于template parameters,就是template parameters pack(模板参数包)
用于function parameters,就是function parameters pack(函数参数包)
用于function parameter types,就是function parameter types pack(函数参数类型包)
注意 … 的不同位置放置使用!!
当参数递归到最后的42时,此时传入参数为空,则调用的不再是函数void print(const T& firstArg, const T1&… args),而是函数void print()。
另外,由bitset<16>(377)可以打印出来00000001011111001,则表明bitset类里面肯定重载了<<,否则无法打印出来对应的值。