GeekBand C++ 第四周

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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容