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
模板偏特化有两种形式:
- 个数的偏: 指定部份参数类型
- 范围的偏: 缩小参数类型的范围
示例如下:
-
个数的偏:
template<typename T, typename Alloc> class vector{ // ... }; template<typename Alloc> class vector<bool, Alloc>{ // 指定了第一个参数类型 // ... };
-
范围的偏
注意: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特性使用
十四 三个主题 可变参数和语法糖
可变模板 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
也是函数签名的一部分,因此const
和non-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个条件:
- 必须是通过指针this来调用函数.(实测,通过
.
运算符调用不会触发动态绑定) - 必须是up-cast(子类向父类转型:向上转型)
- 调用的是虚函数.
- 必须是通过指针this来调用函数.(实测,通过
- 这种虚函数的绑定方法就称为多态
- 虚函数触发动态绑定的条件是同时满足以下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
被视为函数签名的一部分,也就是说const
和non-const
版本的同名成员函数可以同时存在. -
两个版本的同名函数同时存在时,常量对象只能调用
const
版本的成员函数,非常量对象只能调用non-const
版本的成员函数.
在STL的string
类重载[]
运算符时,就同时写了const
和non-const
版本的实现函数:
class template std::basic_string<...> {
// ...
charT operator[] (size_type pos) const { // 常量成员函数,只有常量对象才能调用该函数
// 不用考虑copy on write
// ...
}
reference operator[] (size_type pos) { // 非常量成员函数,只有非常量对象才能调用该函数
// 需要考虑copy on write
// ...
}
}
当重载[]操作符的时候,设计两个函数,
- const member function不必考虑COW(访问同一个内存空间的object)
- 但是non-const member function是必须考虑COW(申请一个新的内存空间,copy过去)。
- 当成员函数的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
默认的new
和delete
运算符是通过malloc
和free
函数实现的,重载这四个函数会产生很大影响,因此一般不应重载这4个函数.影响无边无际
重载new
和delete
重载class自己的成员操作符,ok。
-
可以在类定义中重载
new
、delete
、new[]
和delete[]
运算符,重载之后new
语句创建该类别实例时会调用重载的new
运算符. -
若重载之后却仍要使用默认的
new
运算符,可以使用::new
和::delete
语句. -
重载delete(),不会被delete调用,只当new调用的构造函数抛出异常的时候,才调用来归还占用的内存。
重载new 、delete 运算符 | 重载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字符串内容,所以需要悄悄摸摸地申请额外的内存。