1.导读
勿在浮沙筑高台
本课程既有面向对象,也有泛型编程。是上门课程的续集,主要讲上门课程没有提到的东西。
在正规、大气的素养上,继续探讨更多的技术。
- 泛型编程(Generic Programming)和面向对象编程(Object-Oriented Programming)。两大C++技术主线。
- 深入理解对象之继承(Inheritance)所形成的对象模型(Object Model),this指针,vptr(虚指针),vtbl(虚表),virtual mechanism(虚机制)以及虚函数(virtual functions)造成了polymorphism(多态)效果。
2.conversion funciton,转换函数
Example:
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; //分母
};
转换函数,以operator开头,函数名称为要转换的目标类型,没有参数。通常情况下,转换函数只涉及到类型的转换,而不会改变类的变量,因此要加上const。
{
Fraction f(3,5);
double d = 4 + f; //调用operator double()将f转换为0.6
}
当编译器看到上面第二行代码时,首先它找的是一个operator+的函数,第一个参数是int、float或double等,第二个参数是Fraction,如果有这个函数,那么这一步则没有问题。如果找不到,则编译器再找是否有转换函数将Fraction转换为double,找到后,这一步同样没有问题。
对于一个Class,只要合理,那么就可以写N个转换函数。对于转换的类型,不一定要是基本类型,任何一个type,在之前出现过,编译器编译到这行代码时可以认出,那么就可以。
3.non-explicit one argument constructor
Example:
class Fraction{
public:
Fraction(int num, int den = 1)
: m_numerator(num), m_denominator(den) { }
Fraction operator+ (cosnt Fraction& f){
return Fraction(...);
}
private:
int m_numerator; //分子
int m_denominator; //分母
};
Fraction的构造函数,虽然有两个parameter,但是有一个已经有默认值,则只有一个argument。另外,explicit也是一个关键字,这个构造函数没有加,因此这个构造函数就称为non-explicit one argument constructor。
{
Fraction f(3,5);
Fraction d2 = f + 4; //调用non-explicit ctor将4转为Fraction(4,1),然后调用operator+
}
当编译器看到上面第二行代码时,会找operator+的函数,但是operator+函数的parameter需要的类型为Fraction,所以编译就想办法看看是否可以将4转换为Fraction来完成这条语句。所以4被转换为Fraction(4,1),所以这条语句也可以编译通过。这种转换,是将别的类型转换为Fraction,同样是转换,但是这种情况不能称为转换函数。
conversion function vs. non-explicit one argument ctor
non-explicit:
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+ (cosnt Fraction& f){
return Fraction(...);
}
private:
int m_numerator; //分子
int m_denominator; //分母
};
此时转换函数和non-explicit one argument ctor共存。
{
Fraction f(3,5);
Fraction d2 = f + 4; //[Error] ambiguous
}
编译器在看到上面第二行代码时,共有两条路来通过这条语句的编译:
- 第一条路,可以将4转换为Fraction(4,1);
- 第二条路,可以将f装换为double,再将结果转换为Fraction。
因此在这种情况下,编译器就会报错,因为它不知道该走哪条路。
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+ (cosnt Fraction& f){
return Fraction(...);
}
private:
int m_numerator; //分子
int m_denominator; //分母
};
explicit的意思是明确的,意思是告诉编译器,不要再自动的将4变为Fraction(4,1)。
{
Fraction f(3,5);
Fraction d2 = f + 4; //[Error] conversion from 'double' to 'Fraction' requested
}
当编译器看到第二行语句时,会报错不能将double类型转换为Fraction。会报错的原因,则是因为explicit关键字。
conversion function(转换函数)在标准库的应用
template<class Alloc>
class vector<bool, Alloc>{
public:
typedef __bit_reference reference;
protected:
reference operator[](size_type n){
return *(begin() + difference_type(n));
}
...
};
在上面的vector,operator[]应该返回一个bool,但是它返回的type是__bit_reference,因此需要一个转化函数将它转换为bool。
struct __bit_reference{
unsigned int* p;
unsigend int mask;
...
public:
operetor bool() const { retuen !(!(*p & mask)); }
...
};
因此在struct __bit_reference中就有一个bool的转换函数。
4.pointer-like classes,智能指针
Example1: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;
...
};
类中一定有一个真正的C++指针,它的行为类似于真正的指针,所以能用在指针上的操作,也要能用在这个类中,所以要重载*和->。
struct Foo{
...
void method(void) { ... }
};
shared_ptr<Foo> sp(new Foo);
Foo f(*sp);//实际调用了operator*()
sp->method(); //实际调用了operator->() ==> px.method();
};
在operator->中,作用在sp上,返回了px,此时已经消耗掉了,但是实际还有一个->操作符,在C++中,->操作符对得到的数据还会继续->下去。而对于智能指针,不会用到'.'操作符,所以无需考虑。
Example2:iterator
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;
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); }
self operator++(int) { self tmp = *this; ++*this; return tmp; }
self& operator--() { node = (link_type)((*node).prev); }
self operator--(int) { self tmp = *this; --*this; return tmp; }
};
template<class T>
struct __list_node{
void* prev;
void* next;
T data;
};
迭代器需要考虑++和--操作符的重载。
上图中node是一个迭代器。有个真正的指针指向链表的元素。当对迭代器进行*操作时,就是要取得链表的元素。即为return (*node).data;当对迭代器进行->操作时,实际想要获得元素指针再对指针进行->操作,即为return &(operator*());如下图所示。
我们重点对比了operator*()和operator->()。
5.Function-like classes,仿函数
Example:
template<class T>
struct identity{
const T&
operator()(cosnt T& x) const { return x; }
}:
template<class Pair>
struct select1st{
const typename Pair::first_type&
operator()(cosnt Pair& x) const { return x.first; }
};
template<class Pair>
struct select2nd{
const typename Pair::second_type&
operator()(cosnt Pair& x) const { return x.second; }
};
template<class T1, class T2>
struct pair{
T1 first;
T2 second;
pair() : first(T1()), second(T2()) {}
pair(cosnt T1& a, const T2& b)
:first(a), second(b) { }
...
};
pair<int,float> p;
int n = select1st<pair>()(p); //第一个()创建临时对象,第二个()调用operator()
重载了operator(),使类看起来像一个函数。
6.namespace
namespace最主要的作用,使团队开发时,避免类名称和变量名称的冲突。
7.class template
Example:
template<typename T>
class complex{
public:
complex(T r = 0, T i = 0) : re(r), im(i) { }
...
T real() const { return re; }
T imag() const { return im; }
private:
T re, im;
...
};
{
complex<double> c1(3.5, 1.5);
complex<int> c2(5, 6);
...
}
在使用时,指定模板类的具体数据类型。
8.Function Template
Example:
template<class T>
inline
const T& min(const T&a, const T& b){
return b < a ? b : a;
}
不同于类魔板,在使用时不需指明模板类型,编译器会进行实参推到(argument deduction)。
stone r1(2, 3), r2(3, 3), r3;
r3 = min(r1, r2);
实参推到的结果,T为stone,编译器再去找ostone::perator<。如果找不到,则会出错。
9.Member Template
Example1:pair
template<class T1, class T2>
struct pair{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair() : first(T1()), second(T2()) {}
pair(cosnt 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) {}
};
模板类的某个函数是模板函数,则这个函数是Member Template。一半将模板类的构造函数设置为Member Template。
class Base1{};
class Derived1:public Base1{};
class Base2{};
class Derived2:public Base2{};
pair<Derived1, Derived2> p;
pair<Base1, Base2> p2(p);
===>
pair<Derived1, Derived2> p;
pair<Base1, Base2> p2(pair<Derived1, Derived2>());
这样的构造,用子类构造父类,可以,但是用父类构造子类,不行。因为必须满足构造函数中初始化列表的first(p.first), second(p.second)
。
Example2:shared_ptr
template<typename _Tp>
class shared_ptr: public __shared_ptr<_Tp>{
...
template<typename _Tp1>
explicit shared_ptr(_Tp1* __p)
: __shared_ptr<_Tp>(__p){}
...
};
Base1* ptr1 = new Derived1; //up-cast
shared_ptr<Base1> sptr(new Derived1); //类似up-cast
10.specialization,模板特化
Example:
template<class Key>
struct hash {};
template<>
struct hash<char>{
size_t operator()(char x) const { return x; }
};
template<>
struct hash<int>{
size_t operator()(int x) const { return x; }
};
template<>
struct hash<long>{
size_t operator()(long x) const { return x; }
};
模板特化,就是针对模板,针对某些指定类型,需要某种特殊的优化和处理,那么就可以对模板进行特化处理,从而实现该特定类型下的优化代码。
cout << hash<long>()(1000);
11.partial specialization,模板偏特化(局部特化)
Example1:个数的偏
template<typename T, typename Alloc=...>
class vector{
...
};
template<typename Alloc=...>
class vector<bool, Alloc>{
...
};
上例中有两个模板参数,指定一个,另一个留着,则是个数上的偏。注意,模板参数不能跳过,比如原本有5个,偏特化的时候,不能指定1、3、5,只能按顺序123...去特化。
Example2:范围的偏
template<typename T>
class C{
...
};
template<typename T>
class C<T*>{
...
};
上例中,第一个泛化范围最广,第二个属于偏特化,但是指明是指针,这种是范围上的偏特化。
C<string> obj1; //调用第一个泛化
C<string*> obj2; //调用第而个偏特化
12.template template parameter,模板模板参数
Example1:
template<typename T,
template<typename T> class Container
>
class XCls{
private:
Container<T> c;
public:
...
}:
在模板参数列表上,class和typename是互通的,是历史遗留问题,在别的地方,不能互换使用。
template<typename T>
using Lst = list<T, allocator<T>>;
XCls<string, list> mylst1; //此种用法错误,lsit不止需要一个模板参数
XCls<string, Lst> mylst2;//此种写法可以,指定了其他模板参数的类型
Example2:
template<typename T,
template<typename T> class SmartPtr
>
class XCls{
private:
SmartPtr<T> sp;
public:
XCls() : sp(new T){ }
}:
XCls<string, shared_ptr> p1;
XCls<string, unique_ptr> p2;//不可以
XCls<int, weak_ptr> p1;//不可以
XCls<long, auto_ptr> p2;
Example3:
template<class T, class Sequence = duque<T>>
class stack{
friend operator== <> (cosnt stack&, const stack&);
friend operator< <> (cosnt stack&, const stack&);
protected:
Sequence c;
...
};
stack<int> s1;
stack<int, list<int>> s2;
以上写法已经不属于模板,它的参数也不是模板模板参数,所有的参数都已经指明类型,在类中使用Sequence声明c的时候,也不用指明类型,因为在参数列表已经指明了,用的就是T。而模板模板参数,在声明变量时,还需指明模板类型。在使用的时候,模板参数列表的第二个参数,也已经指明了具体类型,而模板模板参数,不需要指明第二个模板的具体类型。
13.关于C++标准库
C++标准库,非常的重要。除了容器,还有更重要的一部分的内容,算法。
Algorithms + Data Structures = Programs
学习C时,C的语法虽然很简单,但是也有其标准库,最好每一个函数都使用一遍,做到融会贯通,不要去写标准库已经有的函数,做到物尽其用。
14.三个主题
1.variadic template(since C++11)
数量不定模板参数
void print(){
}
template<typename T, typename...Types>
void print(const T& firstArg, const Types&... args){
cout << fitsyArg << endl;
print(args...);
}
Inside variadic templates,sizefo...(args)
yields the number of arguments.
在函数中递归调用,但是当最后一个参数print之后,剩余的args...为0个,所以需要另一个print函数,参数为空,也没有任何动作。
...就是一个所谓的pack(包)
- 用于 template parameters,就是 template parameters pack(模板参数包)
- 用于 function parameter types,就是function parameter types pack(函数参数类型包)
- 用于 function parameters,就是function parameters pack(函数参数包)
我们要注意每一个...
出现的位置,各不相同。
2.auto(since C++11)
list<string> c;
...
list<string>::iterator ite;
ite = find(c.begin, c.end(), target);
===>
list<string> c;
...
auto ite = find(c.begin, c.end(), target);
list<string> c;
auto ite; //在使用auto时,一定可以让编译器帮你推算出类型,所以这里是错误的。
ite = find(c.begin, c.end(), target);
3.ranged-base for(since C++11)
语法:
for(decl : coll){
statement
}
Example:
for(int i : {2, 3, 5, 7, 9, 13, 17, 19}){
cout << i << endl;
}
vector<double> vec;
...
for(auto elem : vec){ //pass by value
cout << elem << endl;
}
for(auto& elem : vec){ //pass by reference
elem *= 3;
}
15.Reference
在C++中,共有三种变量,一种是值本身,一种是指针,再就是reference。
int x = 0; //variable本身
int* p = &x; //P是一个变量,它的类型是point to integer
int& r = x; //r是一个变量,它的类型是reference to integer
//r代表x,现在r,x都是0。必须要有一个初值。
int x2 = 5;
r = x2; //r不能重新代表其他变量,现在r,x都是5。
int& r2 = r; //现在r2 是 5(r2代表r,亦相当于代表x)
虽然reference在底层也是一个指针,但是reference是一种代表,编译器制造了一种假象,它的大小,就是它所代表变量的大小,它的地址,也是它所代表变量的地址。
object和其reference的大小相同,地址也相同(但都是假象)。
- 1)
sizeof(r) == sizeof(x);
- 2)
&x == &r;
void func1(Cls* pobj) { pobj->xxx(); }
void func2(Cls obj) { pobj.xxx(); }
void func3(Cls& obj) { pobj.xxx(); }
Cls obj;
func1(&obj); //接口不通,困扰
func2(obj); //调用接口相同,很好
func2(obj); //调用接口相同,很好
reference通常不用于声明变量,而是用于参数类型(parameters type)和返回类型(return type)的描述。
double imag(cosnt double& im) { ... }
double imag(cosnt double im) { ... }
以上两个函数被视为"same signature"(所以二者不能同时存在)。当调用imag(d)时,编译器无法知道该调用哪一个。
Q:const是不是函数签名的一部分?
A:是。
class Test
{
public:
double GetDouble() const{
cout << "GetDouble() const" << endl;
return GetDouble(3.14);
}
double GetDouble() {
cout << "GetDouble()" << endl;
return GetDouble(3.14);
}
double GetDouble(const double& d) const {
cout << "GetDouble(const double& d) const" << endl;
return d;
}
double GetDouble(const double d) {
cout << "GetDouble(const double d)" << endl;
return d;
}
};
{
Test t;
cout << t.GetDouble() << endl;
const Test ct;
cout << t.GetDouble() << endl;
}
结果:
GetDouble()
GetDouble(const double d)
3.14
GetDouble() const
GetDouble(const double& d) const
3.14
在具体调用时,调用const版本还是非const版本,取决于类的对象是否是const。