C++ 面向对象编程(下)兼谈对象模型

C++ 面向对象编程(下)兼谈对象模型

勿在浮沙筑高台

一 导读

  • 泛型编程(Generic Programming)和面向对象编程(Object-Oriented Programming)
  • 探索Inheritance形成的对象模型(Object Model):
    • this指针
    • 虚指针
    • 虚表
    • 虚机制。
  • 书籍:
    • 《c++ primer》
    • 《Effective C++》
    • 《STL源码剖析》泛型编程
  • 更多细节需要深入的:
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二 转换函数Conversion function

自定义一个类,表示分数,但是希望它做加减乘除的时候,自动转为double。

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

//使用
Fraction f(3,5);
double d = 4 + f;//调用operator double()将 f 转化为 0.6

三 非explicit单实参构造函数 non-explicit-one-argument ctor

我们的使用场景仍然和上面一样,希望Fraction d = 4 + my_frac; ,只需要把4隐式构造成Fraction,然后用操作符+相加起来。

设计模式里面的【代理】,就用到了转换函数。

class Fraction{
public:
    Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den){ }
    //
    Fraction operator+(const Fraction& f){
        return Fraction(...)
    }
}

//使用
Fraction f(3,5);
Feaction d2 = f + 4;//调用non-explicit-one-argument ctor将 4 转化为 Fraction(4,1);
//然后调用operator 

并存问题

  • 二义性:ambiguous。只要多余一条路可行,编译器就会报错
    • f会被转化为double,double+4 似乎可以转换为Fraction
    • 要把4转换为fraction么?fraction+fraction似乎行得通
class Fraction{
public:
    Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den){ }
    //
    Fraction operator+(const Fraction& f){
        return Fraction(...)
    }
    operator double() const{ 
        return (double)(m_numerator/m_denominator);
    }
}

//使用
Fraction f(3,5);
Fraction d2 = f + 4;//[Error]:ambiguous

explicit单实参构造函数 explicit-one-argument ctor

  • explicit:只用在构造函数前面。要求不能自动调用(隐式转换),必须单独调用构造方法。比如3变成3/1的隐式转换可以被explicit取消掉
    • 此时只能用转化成double的方法,而不能把double转化成Fraction。
class Fraction{
public:
    Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den){ }
    //
    Fraction operator+(const Fraction& f){
        return Fraction(...)
    }
    operator double() const{ 
        return (double)(m_numerator/m_denominator);
    }
}

//使用
Fraction f(3,5);
Fraction d2 = f + 4;//[Error]:conversion from 'double' to 'Fraction' requested

标准库Vector举例

  • 我们可以看到标准库中传回的值本来是reference?但我们要求返回的是一个bool值,如何处理?
    写一个转换函数即可

四 pointer-like classes

智能指针–伪指针

伪指针(pointer-like classes)是指作用类似于指针的对象,实现方式是重载*->运算符.

标准库中的shared_ptr类是一个典型的伪指针类,代码如下:

template<class T>
class shared_ptr {
public:
    T& operator*() const {		// 重载 * 运算符
        return *px;
    }

    T* operator->() const {		// 重载 -> 运算符
        return px;
    }
    //...
    
private:
    T *px;
    // ...
};

使用:

  • 指针指向对象,利用指针调用对象的函数
    • 箭头符号有个特殊的机制:作用下去的结构会立刻再作用下去
struct Foo
{
	...
    void method(void){...}
};


shared_ptr<Foo> sp(new Foo);

Foo f(*sp); 		// 语句1: 被解释为 f(*px)
sp -> method();		// 语句2: 被解释为 px -> method()

智能指针–迭代器

标准库中的迭代器_List_iterator(是一个双向链表)也是一个伪指针类,需要写出几乎所有的计算方式,代码如下:

template<class _Tp, class Ref, class Ptr>
struct _List_iterator {
    _List_iterator& operator++() { ...	}
    _List_iterator operator++(int) { ...	}
    _List_iterator& operator--(){ ...	}
    _List_iterator operator--(int)  { ...	}
    bool operator==(const _Self &__x) { ... }
    bool operator!=(const _Self &__x) { ... }
    Ref operator*() { ...	}
    Ptr operator->() { ...	}
};

作为迭代器的双向链表的代码:

  • 外层的人看不懂,所以*运算符重载的目的就是为了拿出数据data。
  • 而->就是返回某个元素的地址
T& reference operator*()const{
    return (*node).data; //用*调用的时候,返回的时候node的data部分,即某个列表元素
}

T* pointer operator->() const{
    return &(operator*()); //这里返回某个列表元素的地址
}

list<Foo>::iterator ite;
*ite; //获得一个Foo object
ite->method(); //调用Foo::method;
//相当于(*ide).method();
//相当于(&(*ite))->method();

五 仿函数 Function-like classes

**伪函数(function-like classes)**是指作用类似于函数的对象,实现方式是重载()运算符,标准库中的几个伪函数如下:

template<class T>
struct identity {
    const T &
    operator()(const T &x) const { return x; }
};

//暗示你要使用一个pair,一对取出第一个
template<class Pair>
struct select1st {
    const typename Pair::first_type &
    operator()(const Pair &x) const { return x.first; }
};

//暗示你要使用一个pair,一对取出第二个
template<class Pair>
struct select2nd {
    const typename Pair::second_type &
    operator()(const Pair &x) const { return x.second; }
};

这些仿函数都继承了一些奇怪的父类

  • 方法没有

  • 只有一些typedef

六 namespace 经验谈

  • 可以把要测试的全局类、方法等单独放一个namespace中

七 class template模板类

类模板实例化时需要指定具体类型:

template<typename T>
class complex {
public:
    complex(T r = 0, T i = 0)
    	: re(r), im(i) 
    {}

    complex &operator+=(const complex &);

    T real() const { return re; }
    T imag() const { return im; }

private:
    T re, im;
}

// 类模板实例化时需要指定具体类型
complex<double> c1(2.5, 1.5);
complex<int> c2(2, 6);

八 function template 函数模板

常见的min、max、memset()之类的都是函数模板

  • 函数模板在使用时,甚至不必指明type,编译器会自己进行实参推导(argument deduction)。
template<class T>
inline const T &min(const T &a, const T &b) {
    return b < a ? b : a;
}

// 函数模板实例化时不需要指定具体类型
min(3, 2);
min(complex(2, 3), complex(1, 5));

九 成员模板

成员模板用于指定成员函数的参数类型:

  • 不少构造参数就有这些运用

  • 这种结构常用于子类到父类的转换,使构造函数更有弹性

template<class T1, class T2>
struct pair {
    typedef T1 first_type;
    typedef T1 second_type;

    T1 first;
    T2 second;

    pair() : first(T1()), second(T2()) {}
    pair(const 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) {}
}
pair<Derived1, Derived2> p1;	// 使用子类构建对象
pair<Base1, Base2> p2(p1);		// 将子类对象应用到需要父类的参数上
//-->pair<Base1, Base2> p2(pair<Derived1, Derived2>);	
  • shared_ptr也是用成员模板进行构造函数的

    template<typename _Tp>
    class shared_ptr:public __shared_ptr<_Tp>
    {
        ...
        template<typename _Tp1>
        explicit shared_ptr(_Tp1* _p) : __shared_ptr<_Tp>(__p){}
        ...
    }
    

十 模板特化specialization

模板特化用来部分针对某些特定参数类型执行操作:

//泛化
template<class Key>
struct hash {
    // ...
};


//下面都是特化
template<>
struct hash<char> {
    size_t operator()(char x) const { return x; }
};
int
template<>
struct hash<int> {
    size_t operator()(int x) const { return x; }
};

template<>
struct hash<long> {
    size_t operator()(long x) const { return x; }
};

十一 模板偏特化 partial-specialization

模板偏特化有两种形式:

  1. 个数的偏: 指定部份参数类型
  2. 范围的偏: 缩小参数类型的范围

示例如下:

  1. 个数的偏:

    template<typename T, typename Alloc>
    class vector{
    // ...  
    };
    
    template<typename Alloc>
    class vector<bool, Alloc>{	// 指定了第一个参数类型
    // ...  
    };
    
  2. 范围的偏

    注意:T*使用的T和直接使用T并不相同

    template<typename T>
    class C{
    // 声明1...  
    };
    
    template<typename T>
    class C<T*>{	// 指定了参数类型为指针类型
    // 声明2...  
    };	
    
    C<string> obj1;		// 执行声明1
    C<string*> obj2;	// 执行声明2
    

十二 模板模板参数 template template parameter

有点没听懂。。。

模板模板参数是指模板的参数还是模板的情况

template<typename T, template<typename T> class Container>
class XCls {
private:
    Container<T> c;
public:
    // ...
};

在上面例子里,XCls的第二个模板参数template<typename T> class Container仍然是个模板,因此可以在类声明内使用Container<T> c语句对模板Container进行特化,使用方式如下:

但是下面的例子list必须是List<String>,因为用的是同一个T

XCls<string, list> mylst1;	// mylst1的成员变量c是一个list<string>

使用该语法可以解决适配问题:


template<typename T>
using Lst = list<T, allocator<T>>;

XCls<string, Lst> mylst1;	// mylst2的成员变量c是一个list<string>

这不是模板模板参数

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; // 底层容器
	// ...
};
123456789

上面例子中stack类的第二模板参数class Sequence=deque<T>不再是一个模板,而是一个已经特化的类,在实现特化stack的时候需要同时特化class Sequence=deque<T>的模板参数.

stack<int> s1;				
stack<int, list<int>> s2;	// 特化第二模板参数时应传入特化的类而非模板

在上面的例子中s2在特化时第二模板参数被设为list<int>,是一个特化了的类,而非模板参数,实际上如果愿意的话,甚至可以将第二模板参数设为list<double>,与第一模板参数T不同,也能编译通过;而模板模板参数就不能这样了,模板模板参数的特化是在类声明体中进行的,类声明体里制定了使用第一模板参数来特化第二模板参数.

十三 STL介绍和c++ 11特性使用

  • 如何确认当前cpp版本

    cout<<__cpluscplus; 如果输出201103,则表示是C++11。

十四 三个主题 可变参数和语法糖

可变模板 variadic templates (since C++ 11)

…就是一个所谓的pack(包)

使用方法就是递归调用,大包可以分为一个参数和小包,打印参数后,递归传入小包,直到1+0的情况产生,打印后再次递归调用的就是print().

//0个的版本,必须有
void print(){
    
}


template <typename T,typename... Types>
void print(const T& firstArg,const Types&... args)
{
    cout << firstArg << endl;
    cout<<sizeof...(args)<<endl; //这里就知道参数包的size大小
    print(args...); //递归调用
}


//使用
print(7.5,"hello",bitset<16>(377),42);

auto

让编译器自动根据右式推导type。

list<string> c;

//手动写,写这个必须是程序员的基本素养
list<string>::iterator ite;
ite = find(c.begin(),c.end(),target);

//auto写
auto ite = find(c.begin(),c.enmd(),target);

错误写法:

auto ite;
ite = find(c.begin(),c.end(),target);

因此,auto的变量必须先赋值才能用。不需要先赋值的变量是无法用auto的

极端想法:所有的都用auto来写。

初学者不建议如此使用,除非你明确了解所有的变量。

ranged-base for(since C++ 11)

新的循环方式,像Python一样。

  • pass-by-value: copy操作,会把右边的值传到左边的元素elem,因此可以正常打印,但想要修改elem却不可以。elem只是一个新的变量。
  • pass-by-reference:如果想要影响原来的东西,则必须要传引用
for(decl : coll){
    statement
}


//临时遍历
for (int i:{2,3,4,7}){
    cout<<i<<endl;
}

vector<double> vec;
for(auto elem:vec){ 	//pass-by-value,一个copy动作
    cout<<elem<<endl;
}


for(const auto& elem:vec){ //pass-by-reference
    cout<<elem<<endl;
}

十五 reference

引用和指针

  • pointer:地址就是指针的一种形式,p is a pointer to x。指针可以更换指向
  • reference:&在后面就不是取地址了,r is a reference to x,我们要把r看做一个整数。且从此之后r再也不能更改指向,只能指向x
    • 可以进行多重引用。
    • sizeof(r) = sizeof(x)&x = &r即引用大小与原来的元素大小一样,值也一样,地址也一样。(这是编译器做的封装假象,底层肯定是不一样的)。
    • 引用比指针要优雅,而且可以使得接口调用的时候,保持一致(无论是否引用,都一致)。
    • (Java里面所有变量都是reference)
int x = 0;
int* p = &x; 	//指针就是地址的一种形式
int& r = x;		//r 代表 x。现在r,x都是0
int x2=5;

r = x2;			//[Error]!r不能重新代表其他物体。现在r、x都是5。
int& r2 = r;	//现在r2是5(r2 代表 r;亦相当于代表x)

引用的用途:引用被用作美化的指针

在编写程序时,很少将变量类型声明为引用,引用一般用于声明参数类型(parameter type)和返回值类型(return type).

// 参数类型声明为引用,不影响函数体内使用变量的方式
void func1(Cls obj) { opj.xxx(); }          // 值传递参数
void func2(Cls *Pobj) { pobj->XXX(); }      // 指针传递参数,函数体内使用变量的方式需要修改
void func3(Cls &obj) { obj.xxx(); }         // 引用传递参数,函数体内使用变量的方式与值传递相同

// 参数类型声明为引用,不影响参数传递的方式
Cls obj;
func1(obj);     // 值传递参数
func2(&obj);    // 指针传递参数,传递参数时需要对参数作出修改
func3(obj);     // 引用传递参数,传递参数时不需对参数做出修改

值得注意的是,因为引用传递参数和值传递参数的用法相同,所以两个函数的函数签名(signature)相同,不能同时存在.

double imag(const double& im){...}
double imag(const double  im){...}
//Ambiguity 签名相同

有意思的是,指示常量成员函数const也是函数签名的一部分,因此constnon-const的同名成员函数可以在同一类内共存.

十六 复合&继承关系下的构造和析构

Inheritance

Derived继承自Based

  • 构造由内而外

    Derived::Derived(...): Base(){...}
    
  • 析构由外而内

    Derived::~Derived(...){... ~Base()};
    

Composition

  • 构造由内而外

    Container::Container(...): Component(){...}
    
  • 析构由外而内

    Container::~Container(...){... ~Component()};
    

Inheritance+Composition

之前好像探究过

十七 对象模型

理解对象模型,才能真正理解多态动态绑定.

成员函数和成员变量在内存中的分布

动态绑定:

下面程序在内存中的布局如下所示:

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1;
    int m_data2;
};

class B : public A {
public:
    virtual void vfunc1();
    void vfunc2();
private:
    int m_data3;
};

class C : public B {
public:
    virtual void vfunc1();
    void vfunc2();
private:
    int m_data1;
	int m_data4;
};

其在内存中的布局如下图所示

  • 先看成员变量部分: 对于成员变量来说,每个子类对象重都包含父类的成分,值得注意的是,C类的m_data1字段和父类A类的字段m_data1相同,这两个字段共存于C类的对象中。变量的内存会被继承,指针会被修改
  • 再看函数的部分,每个含有虚函数的对象都包含一个特殊的虚指针vptr,指向存储函数指针的虚表vtbl.编译器根据vtbl表中存储的函数指针找到虚函数的具体实现.这种编译函数的方式被称为动态绑定
  • 共有八个函数。
    • 非虚函数4个,(同名设为新的函数,但不是原来的函数,最好不要命名为同名)
    • 虚函数4个:其中vfunc2一直被继承,占用虚表的一个位置
      • A函数有两个虚函数,分别对应虚表的两个指针
      • B函数集继承了A的一个虚函数,但是推翻了它自己有一个虚函数。同时,它集成了A的v2,所以分别对应虚表的两个指针
      • C和B同理,对应虚表的两个指针

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

静态绑定和动态绑定

  • 对于一般的非虚成员函数来说,其在内存中的地址是固定的,编译时只需将函数调用编译成call(汇编的一种)命令即可,这被称为静态绑定.

  • 对于虚成员函数,调用时根据虚表vtbl判断具体调用的实现函数,相当于先把函数调用翻译成(*(p->vptr)[n])(p)或者(*p->vptr[n])(p),这被称为动态绑定.p

    • 虚函数触发动态绑定的条件是同时满足以下3个条件:
      1. 必须是通过指针this来调用函数.(实测,通过.运算符调用不会触发动态绑定)
      2. 必须是up-cast(子类向父类转型:向上转型
      3. 调用的是虚函数.
    • 这种虚函数的绑定方法就称为多态

十八 关于this

this是个关键字,是个指针,也是种观念(this Object)

  • 在C++里,所有的成员函数一定有一个隐藏的this pointer

  • 规则:谁调用我,谁的地址就是这个this

    • 这里OnFileOpen的this就是myDoc的地址。调用函数是会经历一次向上转型,再调用到子类myDoc的虚函数

十九 关于Dynamic Binding 动态绑定

//静态绑定
B b;
A a= (A)b; //向上转型
a.vfunc1(); //这是对象的调用,调用父类的虚函数,这是静态绑定,汇编形式:call xxx

//动态绑定
A* pa = new B; //向上转型
pa->vfunc1();  //调用父类的虚函数,动态绑定,call后面接的就不是一个固定的地址

pa = &b;
pa->vfunc1();

谈谈Const

这里Cpp primer这本书说得贼全,但是很复杂。侯老师说得比较精炼。

  • 成员函数含有const,意味着该函数不打算改变class的data。(可以读,但不可以看改)
  • 不含有const,意味着可以改变class的data
  • 当成员函数的const和non-const版本同时存在,
    • const object 智能调用const版本。
    • non-const object只能调用non-const版本。
const object (data member不得改动)non-const object (data members可改动)
const member functions (保证不改变data members)允许允许
non-const member functions (不保证 dat members不变)不允许允许

常量对象无法调用非常量成员函数。

//如果print设计时没有使用const,那么就会报错
const String str("hello world");
str.print();
COW: Copy and Write
  • 指示常量成员函数的const被视为函数签名的一部分,也就是说constnon-const版本的同名成员函数可以同时存在.

  • 两个版本的同名函数同时存在时,常量对象只能调用const版本的成员函数,非常量对象只能调用non-const版本的成员函数.

在STL的string类重载[]运算符时,就同时写了constnon-const版本的实现函数:

class template std::basic_string<...> {
	// ...
    
    charT operator[] (size_type pos) const {	// 常量成员函数,只有常量对象才能调用该函数
        // 不用考虑copy on write
        // ...
    }

	reference operator[] (size_type pos) {		// 非常量成员函数,只有非常量对象才能调用该函数
        // 需要考虑copy on write
        // ...
    }
}

当重载[]操作符的时候,设计两个函数,

  1. const member function不必考虑COW(访问同一个内存空间的object)
  2. 但是non-const member function是必须考虑COW申请一个新的内存空间,copy过去)。
  3. 当成员函数的const和non-const同时存在的时候,C++特性:const object智能调用const版本,non-const object只能调用non-const版本。

二十 New&Delete

区分**new表达式new运算符**.我们一般程序中写的是**new表达式**,在中可以看到,new表达式和delete表达式会被翻译成多条语句,其中用到了new运算符和delete运算符.

new表达式的解析delete表达式的解析
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二十一 Operator New&Operatoe Delete

默认的newdelete运算符是通过mallocfree函数实现的,重载这四个函数会产生很大影响,因此一般不应重载这4个函数.影响无边无际

重载newdelete

重载class自己的成员操作符,ok。

  • 可以在类定义中重载newdeletenew[]delete[]运算符,重载之后new语句创建该类别实例时会调用重载的new运算符.

  • 若重载之后却仍要使用默认的new运算符,可以使用::new::delete语句.

  • 重载delete(),不会被delete调用,只当new调用的构造函数抛出异常的时候,才调用来归还占用的内存。

重载newdelete运算符重载new[]delete[]运算符

二十二 array new重载示例

  • 这里面Foo的数据大小为12字节(int4字节,long4字节,string大小为指针大小,4字节)
  • 如果加上虚函数之后,会产生vptr,因此有虚函数的大小为16字节。
  • 主要看第三步和第四步,new一个Foo[5],发现大小是64,不是60(12 * 5)。**多出来的一个4字节,记录了这个数组的长度。**从上到下进行构造,从下到上进行析构。
  • 同理,下面的大小是84,也不是80。

二十三 new(),delete() placement重载

  • 可以重载类的成员操作符函数,new()、delete()。
  • 第一个参数必须是size_t。(new(300,‘c’))
  • 参数是用来分配空间的,实际上是malloc(100);
  • 重载delete(),不会被delete调用,只当new调用的构造函数抛出异常的时候,才调用来归还占用的内存。
  • 故意写错第一个参数,编译器会报错。

二十四 basic_string使用new(extra)扩充申请量

  • 无声无息,多分配一些东西
    |
    | ------------------------------------------------------------ | ------------------------------------------------------------ |
    | | |

二十二 array new重载示例

[外链图片转存中…(img-13Bt6bnP-1707740958509)]

  • 这里面Foo的数据大小为12字节(int4字节,long4字节,string大小为指针大小,4字节)
  • 如果加上虚函数之后,会产生vptr,因此有虚函数的大小为16字节。
  • 主要看第三步和第四步,new一个Foo[5],发现大小是64,不是60(12 * 5)。**多出来的一个4字节,记录了这个数组的长度。**从上到下进行构造,从下到上进行析构。
  • 同理,下面的大小是84,也不是80。

二十三 new(),delete() placement重载

  • 可以重载类的成员操作符函数,new()、delete()。
  • 第一个参数必须是size_t。(new(300,‘c’))
  • 参数是用来分配空间的,实际上是malloc(100);
  • 重载delete(),不会被delete调用,只当new调用的构造函数抛出异常的时候,才调用来归还占用的内存。
  • 故意写错第一个参数,编译器会报错。

二十四 basic_string使用new(extra)扩充申请量

  • 无声无息,多分配一些东西
  • basic_string多分配了一些东西 extra,basic_string需要做引用计数的一个头部,再加实际的string字符串内容,所以需要悄悄摸摸地申请额外的内存。
  • 19
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值