Loading

候捷-C++程序设计(Ⅱ)兼谈对象模型

笔记参考

本文参考的一些学习笔记:

学习目标

学习目标如下:

  • 在培养正规、大气的编程素养上继续探讨更多的技巧
  • 泛型编程也是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 templatefunction templatemember 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

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

}
posted @ 2022-09-25 18:04  二次元攻城狮  阅读(58)  评论(0编辑  收藏  举报