【极客班】《c++面向对象高级编程下第一周》学习笔记

这门课主要偏重于泛型编程(generic programming)以及底层对象模型(this,vptr,vtbl,多态(polymorphism)等)。

首先提到的类成员函数是转换函数(conversion function)和隐式单参数构造函数(non-explicit one argument constructor).如讲义中提到的下面例子:

#include <iostream>

using namespace std;

class Fraction

{

public:

Fraction(int num, int den = 1)

: m_numerator(num), m_denominator(den) {

if(m_denominator != 0)

{

int gcd_val = gcd(m_numerator, m_denominator);

if(gcd_val != 1)

{

m_numerator = m_numerator / gcd_val;

m_denominator = m_denominator / gcd_val;

}

}

}

#if 1

operator double() const {

return (double)(m_numerator/m_denominator);

}

#endif

static int gcd(int a, int b)

{

if(b == 0)

return a;

else

return gcd(b, a%b);

}

Fraction operator+(const Fraction& f) {

return Fraction(m_numerator * f.m_denominator + m_denominator * f.m_numerator, m_denominator * f.m_denominator);

}

friend ostream& operator<<(ostream& os, const Fraction& f);

private

int m_numerator; //分子

int m_denominator; //分母

};

ostream& operator<<(ostream& os, const Fraction& f)

{

os << f.m_numerator << '/' << f.m_denominator;

return os;

}

int main(void)

{

Fraction f1(3,5);

Fraction f2 = f1 + 4;

cout << f1 << " " << f2 << endl;

return 0;

}

这个类中double()是conversion function,此处构造函数是隐式单参数构造函数。

这种情况下,有可能将将f转换成double,然后将得到double与4相加,得到结果转换成Fraction.也可能对4使用构造函数转换成Fraction,然后将f和构造生成的Fraction对象相加,将最终结果赋给f2.这时候会产生二义性。编译时有下面的错误信息:

error: use of overloaded operator '+' is ambiguous (with operand types 'Fraction' and 'int')

如果将double()函数的定义注释掉就可以正常编译执行,并能得到下面输出信息:

3/5 23/5

另一个有趣的类是智能指针(shared_ptr、unique_ptr等),智能指针是像指针的类,其定义如下:

template <class T>

{

public:

    T& operator*() const { return *px; }

    T* operator->() const { return px; }

    shared_ptr(T* p) : px(p) {}

private:

    T *px;

....

}

struct Foo

{

...

    void method(void) {...}

};

shared_ptr<Foo> sp(new Foo);

Foo f(*sp);

sp->method();

对于这里的sp对象调用*或者->,实际作用的是该对象内的指针成员变量px.

另外sp->method()中sp->对应于px,那么貌似px后直接跟随method(),感觉会有奇怪。而事实上sp后会一直带有隐式的->,所以仍可以调用px->method函数。

还有一个比较有趣的是迭代器对象,例如__list_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;

  reference operator*() const { return (*node).data; }

  pointer operator->() const { return &(operator*()); }

  self & operator++ () { node = (link_type)((*node).next); return *this; } //后置++操作符

  self operator++(int) { self tmp = *this; ++*this; return tmp; } //前置++操作符

};

template<class T>

struct __list__node{

  void *prev;

  void *next;

  T data;

};

list<Foo>::iterator ite;

*ite;

ite->method();

//相当于调用(&(*node).data)->method();

//相当于调用(*node).data.method()

另外有意思的一种类是类似于函数的类(function-like class),一个简单的小例子,代码如下:

#includeusing namespace std;

class A

{

public:

A(int m=0,int n=0):x(m), y(n) {}

int operator()()

{

return x + y;

}

int operator()(int z)

{

return x + y + z;

}

int operator() (int z, int r)

{

return x + y + z + r;

}

private:

int x;

int y;

};

int main(void)

{

A a(3,4);

cout << a() << endl;

cout << a(2) << endl;

cout << a(2,3) << endl;

return 0;

}

在使用时可以创建对象,然后像使用函数一样给这个对象传递参数,就会调用重载()的函数。

这节课中提到模板类重载()操作符的情况,课堂中提到下面例子:

template <class T>

struct identity {

const T& operator()(const T& x) const { return x; }

};

template <class T>

{

struct plus T operator() (const T&x, const T& y) const { return x + y; }

};

这两个模板类在重载()操作符时会隐含的用到下面这两个特殊的基类:

template <class Arg, class Result> struct unary_function{

  typedef Arg_argument_type;

  typedef Result result_type;

};

template <class Arg1, class Arg2, class Result> struct binary_function {

  typedef Arg1 first_argument_type;

  typedef Arg2 second_argument_type;

  typedef Result result_type;

};

这两个基类参数分别为1个或者2个。

我查看了STL网页上关于Functors 的介绍,这里面提到,所有的函数、函数指针以及重载()操作符的类都可以被称为functor(或者function object)。而在STL算法中functor只需要零个、一个或者两个参数,分别用generator、unary_function、binary_function,这三个functor对应于函数f(), f(x)和f(x,y).

另外,有趣的是,c++11中认为unary_function和binary_function已经过时了,不建议使用,貌似c++标准委员会建议c++17将它们删除,stackoverflow上面有讨论的页面 ,不过我没看懂。

课堂中还讲到namespace,写个测试小函数:

#includeusing namespace std;

namespace yy1

{

int sum(const int& x,const int& y)

{

return x + y;

}

}

int main(void)

{

cout << yy1::sum(4,5) << endl;

return 0;

}

还讲到了类模板和函数模板。

类模板定义如下:

template <typename T>

class Point

{

  public:

    Point(T m = 0, T n = 0):x(m), y(n) {}

    T DistanceFromOrigin() {return sqrt(x * x + y * y); }

  private:

    int x;

    int y;

};

使用方式如下:

Point<double> p(3,4);

而函数模板定义如下:

template <class T> inline const T& min(const T& a, const T&b)

{

  return b < a ? b : a;

}

在使用min函数时,会对T的类型进行推导。

另外还有类成员函数模板,其用法跟函数模板类似。

对于模板,还可以用template specialization,讲义中提到下面的例子:

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; }

};

还有部分特例化(partial specialization),讲义中例子如下;

template <typename T, typename Alloc=...> class vector

{

  ...

};

template <typename Alloc = ...>  class vector<bool, Alloc>

{ ... }

还有比较有趣的模板模板参数(template template parameter), 看下面例子:

template<typename T, template<typename T> class SmartPtr>

class XCls

{

  private:

    SmartPtr<T> sp;

  public:

    XCls:sp(new T) {}

};

使用上面定义的例子如下:

XCls<string, shared_ptr> p1;


可以编写在程序中输出__cplusplus的值来判断当前使用的c++标准。

我当前使用的是clang 3.4,如果使用默认编译选项,那么输出结果是199711 对应c++0x

如果添加-std=c++11,那么输出结果是201103  对应c++11

如果添加-std=c++1y,那么输出结果是201305.

c++11中支持auto,可以自动推断变量类型,并且c++11支持范围操作,看下面例子:

vector v = {1,2,3,4,5};

for(auto item: v)

cout << item << endl;

另外,c++中,如果输出对象和reference的大小和地址,其值是相等的。

但实际上reference应该是用指针来实现的。reference通常不直接定义,主要用于参数和返回类型。

类之间的关系有继承(inheritance)和复合(composition)以及二者的结合。

而其构造函数是从内到外执行,析构函数是从外到内执行。


本节有一个有趣的习题,就是对象内存布局,看下面例子:

#include <iostream>

using namespace std;

class Base

{

int x;

char y;

public:

void f1() const{

cout << "f1" << endl;

}

virtual void g1() const {}

};

class Derived:public Base

{

char z;

public:

void s1() const{

cout << "s1" << endl;

}

virtual void g1() const{}

};

int main(void)

{

Base b1,b2;

b1.f1();

Derived d1,d2;

d1.s1();

return 0;

}

对上面程序(名称为object_model.cpp)进行编译,:

clang++ object_model.cpp  -g -o object_model

nm 命令查看object_model,然后能找到其中几个函数地址:

08048880 W _ZNK4Base2f1Ev

08048950 W _ZNK4Base2g1Ev

08048940 W _ZNK7Derived2g1Ev

08048900 W _ZNK7Derived2s1Ev

使用c++filt 对demange这几个函数名称,得到下面信息:

mangled name       demangled name

_ZNK4Base2f1Ev  Base::f1() const

_ZNK4Base2g1Ev Base::g1() const

_ZNK7Derived2g1Ev Derived::g1() const

_ZNK7Derived2s1Ev Derived::s1() const

使用gdb调试object_model,然后在main函数末尾处打断点:

(gdb) p &b1

$1 = (Base *) 0xffffced8

(gdb) p &(b1.x)

$2 = (int *) 0xffffcedc

(gdb) p &(b1.y)

$3 = 0xffffcee0 "p\211\004\b"

(gdb) p b1.f1

$4 = {void (const Base * const)} 0x8048880

(gdb) p b1.g1

$5 = {void (const Base * const)} 0x8048950

(gdb) info vtbl b1

vtable for 'Base' @ 0x8048a64 (subobject @ 0xffffced8):

[0]: 0x8048950

类似地,对b2也执行类似的操作,结果如下:

(gdb) p &b2

$6 = (Base *) 0xffffcec8

(gdb) p &(b2.x)

$7 = (int *) 0xffffcecc

(gdb) p &(b2.y)$8 = 0xffffced0 "p\211\004\b\200\206\004\bd\212\004\b\364\277\200Ap\211\004\b"

(gdb) p b2.f1

$9 = {void (const Base * const)} 0x8048880

(gdb) p b2.g1

$10 = {void (const Base * const)} 0x8048950

(gdb) info vtbl b2

vtable for 'Base' @ 0x8048a64 (subobject @ 0xffffcec8):

[0]: 0x8048950

再分别打印b1和b2这两个变量:

(gdb) p b1

$12 = {_vptr$Base = 0x8048a64, x = 1098956788, y = 112 'p'}

(gdb) p b2

$13 = {_vptr$Base = 0x8048a64, x = 1098956788, y = 112 'p'}

(gdb) p sizeof(b1)

$14 = 12

(gdb) p sizeof(b2)

$15 = 12

可以画出b1内存布局如下:          

_______________                    ---------------------------(vtbl)        

|  vptr(0x8048a64)          | --------> |        0x8048950   ()      |           

________________                  __________________      

|  x (4 byte)                                      |

__________________

| y (1 byte)                            |

___________________

|  padding(3 bytes)               |

___________________

b2的vptr和vtbl中保存的值跟b1完全相等。但是x和y的地址完全不同。

使用gdb打印d1和d2相关信息:

(gdb) p &d1

$16 = (Derived *) 0xffffceb8

(gdb) p &(d1.x)

$17 = (int *) 0xffffcebc

(gdb) p &(d1.y)

$18 = 0xffffcec0 "\001"

(gdb) p d1.f1

$19 = {void (const Base * const)} 0x8048880

(gdb) p d1.s1

$20 = {void (const Derived * const)} 0x8048900

(gdb) p d1.g1

$21 = {void (const Derived * const)} 0x8048940

(gdb) info vtbl d1

vtable for 'Derived' @ 0x8048a30 (subobject @ 0xffffceb8):

[0]: 0x8048940

(gdb) p &d2

$22 = (Derived *) 0xffffcea8

(gdb) p &(d2.x)

$23 = (int *) 0xffffceac

(gdb) p &(d2.y)

$24 = 0xffffceb0 "\b\206\377\367`\235\004\b0\212\004\b\273\211\004\b\001"
(gdb) p d2.f1

$25 = {void (const Base * const)} 0x8048880

(gdb) p d2.s1

$26 = {void (const Derived * const)} 0x8048900

(gdb) p d2.g1

$27 = {void (const Derived * const)} 0x8048940

(gdb) info vtbl d2

vtable for 'Derived' @ 0x8048a30 (subobject @ 0xffffcea8):

[0]: 0x8048940

d1内存布局如下(12 bytes):

_______________                    ---------------------------(vtbl)

|  vptr(0x8048a30)        | --------> |        0x8048940  (Derived::g1())      |

________________                  __________________

|  x    (4 bytes)                     |

__________________

| y         (1 bytes)                    |

___________________

|  z       (1 byte)                    |

____________________

|  padding(2 bytes)              |

___________________

d2中vptr和vtbl的值也是完全相等的,但x、y和z的地址不相等。

可以使用clang打印出对象的布局信息,使用下面命令(可能需要先将std以及cout注释掉):

clang -cc1  -fdump-record-layouts object_model.cpp

生成一个object_model.cpp.002t.class文件,里面有类似下面的信息(要先用c++filt对符号进行转换得到下面信息):

Vtable for Base

Base::vtable for Base: 3u entries

0    (int (*)(...))0

4    (int (*)(...))(& typeinfo for Base)

8    Base::g1


Class Base

size=12 align=4

base size=9 base align=4

Base (0x7f23ee353a80) 0

vptr=((& Base::vtable for Base) + 8u)


Vtable for Derived

Derived::vtable for Derived: 3u entries

0    (int (*)(...))0

4    (int (*)(...))(& typeinfo for Derived)

8    Derived::g1


Class Derived

size=12 align=4

base size=10 base align=4

Derived (0x7f23ee353e70) 0

vptr=((& Derived::vtable for Derived) + 8u)

Base (0x7f23ee353ee0) 0

primary-for Derived (0x7f23ee353e70)

按照这个生成信息可以知道,clang 3.4采用4字节对齐,并且虚函数表中有三项:

第一项是0

第二项是其type info

第三项才是其中的虚函数

g++中也有类似命令:

g++ -fdump-class-hierarchy object_model.cpp


在review其它同学的笔记时,有提到new/delete和malloc/free的区别。new/delete在自由存储区,而malloc/free是在堆上,new/delete是类型安全的,而malloc/free不是类型安全。在c++中只应当使用new/delete,尽量不要使用malloc/free.虽然二者有free storage和堆的差别,但实际上跟编译器实现有关,可能这两个区域位于相同的区域,也可能位于不同的区域。关于二者的比较可以参看下面的文章:

http://www.cnblogs.com/jiayouwyhit/archive/2013/08/06/3242124.html

http://stackoverflow.com/questions/240212/what-is-the-difference-between-new-delete-and-malloc-free



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

推荐阅读更多精彩内容