候捷-C++程序设计(Ⅱ)兼谈对象模型
笔记参考
本文参考的一些学习笔记:
- C++转换函数(conversion function)
- C++转换函数 (conversion function)与 C++中explicit关键字
- pointer-like classes, 关于智能指针
- C++之Function-Like Classes,仿函数
- C++之模板template
- Specialization模板特化
- C++ 对象的内存布局(上)
- object Model(对象模型):关于vptr和vtbl
- 详解C++重载new, delete
学习目标
学习目标如下:
- 在培养正规、大气的编程素养上继续探讨更多的技巧。
- 泛型编程也是C++的技术主线,探讨template(模板)。
- 深入探索面向对象之继承关系所形成的对象模型,包括this指针、虚指针、虚表以及虚机制造成的多态效果。
转换函数与explicit
设计一个类Fraction表示分数,包含分子和分母,能自动转换为double类型并参与运算,代码如下:
class Fraction
{
public:
Fraction(int numerator, int denominator = 1)
:m_numerator(numerator), m_denominator(denominator)
{
}
//转换函数
operator double() const
{
return (double)m_numerator / m_denominator;
}
//加操作符重载
Fraction oprateor + (const Fraction& f)
{
int nNumerator = m_numerator*f.m_denominator + this->m_denominator* f.m_numerator;
int nDenominator = m_denominator*m_denominator;
return Fraction(nNumerator, nDenominator);
}
private:
int m_numerator; //分子
int m_denominator; //分母
};
int main()
{
Fraction f(3,5);
double sum = 4 + f;
std::cout << "sum = " << sum << std::endl; //sum = 4.6
return 0;
}
考虑将Fraction转化为其string类型,则转换函数的代码如下:
//转换函数
operator string() const
{
return to_string(m_numerator) + "/" + to_string(m_denominator);
}
//调用转换函数
string str = f ;
std::cout << "Result is: " << str << std::endl; //Result is: 3/5
在c++中,explicit只能用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。
上面的类Fraction如果换一种方法使用就会报错:
int main()
{
Fraction f(3,5);
Fraction sum = f + 4; //Error ambiguous 编译器不知道调用哪个 f可以转为double 4也可以通过构造函数隐式转为Fraction对象
return 0;
}
如果为类Fraction的构造函数添加explicit关键字:
explicit Fraction(int numerator, int denominator = 1)
:m_numerator(numerator), m_denominator(denominator)
{
}
Fraction sum = f + 4; //Error ambiguous +操作符重载函数中4无法转为Fraction
pointer-like classes
pointer-like classes , 类的行为看起来像指针,有智能指针、迭代器两大类。
注:C++里操作指针的操作符有 -> 和 * ,实质上就是类重载了这两个操作符,使得类的行为看起来像指针。
智能指针的经典实现如下:
template<class T>
class share_ptr
{
public:
T& operator*() const
{return *px;}
T* operator->() const
{return px;}
shared_ptr(T* p): px(p){}
private:
T* px;
long* pn;
......
};
//使用share_ptr
struct Foo
{
......
void method(void) {......}
};
shared_ptr<Foo> sp(new Foo);
Foo f(*sp);
sp->method();
sp->method(); 智能指针调用method()方法。操作符 -> 属于调用操作符重载,这句就相当于px->method()。操作符->作用在指针对象sp上,得到指针对象px,此时符号->并没有消耗掉可以继续使用。
迭代器的经典实现如下,需要重载 ++、--符号:
template <class T, class Ref, class Ptr>
struct __list_iterator { //这是一个链表
typedef __list_iterator<T, Ref, Ptr> self;
typedef Ptr pointer;
typedef Ref pointer;
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); return *this }
self& operator++(int) { self tmp = *this; ++*this; return tmp; }
self& operator--() { node = (link_type)((*node).prep); return *this }
self& operator--(int) { self tmp = *this; --*this; return tmp; }
};
function-like classes
函数由返回类型、函数名称、参数(小括号,作用在函数名上)和函数主体组成,小括号内含参数这部分也称作函数调用操作符。
如果有个东西能接受小括号操作符,那它就是函数function,或者仿函数function-like。
先认识一下std::pair,它主要的作用是将两个数据组合成一个数据,两个数据可以是同一类型或者不同类型,代码如下:
template <class T1, class T2>
struct pair
{
T1 first;
T2 second; //first和second类型不需要一样
pair() : first(T1()), second(T2()) {}
pair(const T1& a, const T2& b)
: first(T1()), second(T2())
......
};
标准库中与pair有关的仿函数如下:
template<class Pair>
struct select1st : public unary_function<Pair, typename Pair::first_type>
{
const typename Pair::first_type&
operator() (const Pair& x) const { return x.first; }
};
template<class Pair>
struct select2nd : public unary_function<Pair, typename Pair::second_type>
{
const typename Pair::second_type&
operator() (const Pair& x) const { return x.second; }
};
上面的两个类中都包括操作符重载operator(),叫做function-like classes,那么由类创建的对象就叫做函数对象functor。
这两个类各自继承类unary_function,类似的基类还有binary_function,两者的详细内容参考 unary_function和binary_function详解。
总的来说,unary_function这些基类的作用是记录仿函数的参数类型,在例如bind2nd中这很有用处,参考 C++ bind2nd用法。
bind2nd代码如下:
template<typename _Operation, typename _Tp>
inline binder2nd<_Operation>
bind2nd(const _Operation& __fn, const _Tp& __x)
{
//如果不继承binary_function,就无法获取第二参数
typedef typename _Operation::second_argument_type _Arg2_type;
return binder2nd<_Operation>(__fn, _Arg2_type(__x));
}
模板template
C++中的模板可以分为三类:class template、function template、member template,示例代码如下:
//class template
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(const T1& a, const T2& b)
: first(a), second(b) {}
//member template
template<class U1, class U2>
pair(const pair<U1, U2>& p)
: first(p.first), second(p.second) {}
};
上面的示例可以这样使用:
模板特化与偏特化
C++中的模板特化不同于模板的实例化,模板参数在某种特定类型下的具体实现称为模板的特化。模板特化有时也称为模板的具体化或全特化,分别有函数模板特化和类模板特化,类模板特化示例如下:
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>() (100) << endl;
模板的偏特化包括两种:个数的偏、范围的偏。
个数的偏:如果模板参数有两个,你想绑定其中一个,示例代码如下:
template<typename T, typename Alloc=...>
class vector
{
...
};
template<typename Alloc=...>
class vector<bool, Alloc> //绑定了T
{
...
};
*注:要按顺序从左到右绑定模板参数,不能跳跃式绑定第一三五个模板参数。
范围的偏:如果接收任意类型的T,你想缩小接收范围为指向任意类型T的指针,示例代码如下:
template<typename T>
class C
{
...
};
//调用
C<string> obj1
template<typename T>
class C<T*>
{
...
};
//调用
C<string*> obj2
模板模板参数
关于模板模板参数,网上有很多文章但都存在一些错误。这里给出我自己的解释,模板模板参数是指模板的参数是一个模板(该模板的模板参数由之前的模板参数指定),后面会专门用一篇文章解释。
一个模板模板参数的示例:
template<typename T, tamplate<typename T> class SmartPtr> //第二个模板参数是一个智能指针
class XCls
{
private:
SmartPtr<T> sp;
public:
XCls()
: sp(new T) {}
};
XCls<string, shared_ptr> p1; //编译通过
XCls<double, unique_ptr> p2; //编译出错
XCls<int, weak_ptr> p3; //编译出错
XCls<long, auto_ptr> p4; //编译通过
一个非模板模板参数的示例:
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;
...
};
stack<int> s1;
stack<int, list<int>> s2;
引用(reference)
按我的理解,引用就是漂亮的指针,底层是用指针实现的,但它的实际大小与所引用的数据的大小相同(编译器制造的假象)。
关于虚指针(vptr)和虚表(vtbl)
设置了3个class,A,B,C之间是继承的关系,内存布局如下:
通过new c可以得到p,通过p要调用vfunc1(),因为vfunc1是虚函数,所以编译器不能通过老式的call一个地址(静态绑定),就只能通过动态绑定去寻找这个函数,实际调用过程等价于:
(* (p-vptr)[n])(p);
//或
(* p->vptr[n])(p);
符合下列3个条件则会通过动态绑定调用:
- 是指针
- 向上转型(子类对象用父类指针表示)
- 调用的是虚函数
在单一的继承中,对象的内存布局如下(参考 C++ 对象的内存布局(上)):
- 虚函数表在最前面的位置。
- 成员变量根据其继承和声明顺序依次放在后面。
- 在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。
在多重继承中,对象的内存布局如下(参考 C++ 对象的内存布局(上)):
- 每个父类都有自己的虚表。
- 子类的成员函数被放到了第一个父类的表中。
- 内存布局中,其父类布局依次按声明顺序排列。
- 每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
关于this
通过一个对象调用函数,这个对象的地址就是this。this指针是“成员函数”的第一个隐藏参数,由编译器自动给出。
友元函数没有 this 指针,因为友元不是类的成员,只有成员函数才有 this 指针。
动态绑定
在这里重复一下动态绑定调用的条件:
- 是指针
- 向上转型(子类对象用父类指针表示)
- 调用的是虚函数
需要注意,虚函数并不一定都是动态调用,必须符合上面这3个条件。
const成员函数
关于const成员函数记住以下规则即可:
const object (data members 不得变动) |
non-const object (data members可变动) |
|
---|---|---|
const member functions (保证不更改data members) |
√ | √ |
non-const member functions (不保证 data members 不变) |
× | √ |
常成员函数的const和non-const版本同时存在,const** object 只会(只能)调用const版本,non-const object 只会(只能)调用non-const版本。**
例如 class template std::basic_string<...> 有如下两个member functions:
charT
operator[] (size_type pos) const
{……/*不必考虑COW*/}
reference
operator[] (size_type pos)
{.../*必须考虑COW*/}
//COW:Copy On Write
上面这两个函数是重载关系,同时也验证了const是函数签名的一部分。
关于const的更多介绍可以参考以下文章:
重载new、delete
new、operator new、**placement new是三个不同概念,区别如下:
- new是一个关键字,主要做三件事:分配空间(调用operator new分配空间)、初始化对象、返回指针。
- operator new是一个操作符,和 + - 操作符一样,作用是分配空间。
- placement new是operator new的一种重载形式。
一个完整的重载new、delete示例,来自 详解C++重载new, delete:
class Foo {
public:
Foo() { std::cout << "Foo()" << std::endl; }
virtual ~Foo() { std::cout << "~Foo()" << std::endl; }
void* operator new(std::size_t size)
{
std::cout << "operator new" << std::endl;
return std::malloc(size);
}
void* operator new(std::size_t size, int num)
{
std::cout << "operator new" << std::endl;
std::cout << "num is " << num << std::endl;
return std::malloc(size);
}
void* operator new (std::size_t size, void* p)
{
std::cout << "placement new" << std::endl;
return p;
}
void operator delete(void* ptr)
{
std::cout << "operator delete" << std::endl;
std::free(ptr);
}
};
int main()
{
Foo* m = new(100) Foo;
Foo* m2 = new(m) Foo;
std::cout << sizeof(m) << std::endl;
//delete m2;
//::delete m;//强制调用全局的delete函数
delete m;
return 0;
}