C++面向对象补充(三)

一、auto

注意:使用auto时,一定要让编译器帮你进行推导,即声明定义跟赋值需要同时进行,不能只声明定义而不进行=赋值操作。

list<string> c;
...
list<string>::iterator ite; // ite是一个迭代器类型
ite = find(c.begin(), c.end(),target);
// 或者可以写成:
// auto ite = find(c.begin(), c.end(), target);

// 但不可以:
// auto ite;
// ite = find(c.begin(), c.end(), target);

注意:不可以一直用auto,因为有些场景下,变量只需要进行声明(不可能总是一声明变量遍马上进行赋值操作),但却不急着马上赋值,而是选择在后面某个过程中再进行赋值操作!!

二、ranged-base for

/*
decl表示变量,coll表示为容器类型

for( decl : coll){
	statement;
}
*/

for(int i : {2, 3, 5, 7, 9, 13, 17, 19}){ // {}表示是一个容器,编译器从容器中一个一个抓取元素出来赋值给i
	cout << i << endl;					// 相比之前版本,遍历一个容器的方法简单很多,以前是需要拿容器的迭代器 iterator 去取的;或者用标准库的foreach去操作,
}										// 现在只需要用新语法for( decl : coll)即可

vector<double> vec;
...
for(auto elem : vec){
	elem *= 3;
}
for(auto& elem : vec){
	elem *= 3;
}

注意:
1.第一个for(auto elem : vec)是pass by value,这时内部操作elem *= 3并不会改变原来的值;
2.而第二个for(auto& elem : vec)是pass by reference,这时内部操作elem *= 3会改变原来的值。
在这里插入图片描述

三、reference

在这里插入图片描述

int x = 0;
int* p = &x;	// p是一个指针类型,打印出来实际上是一串地址,而&x处于赋值号‘=’右边实际上是解引用的意思,即把x的地址取出来。
int& r = x;		// 而&r处于赋值号‘=’左边,表示的是将变量r与变量x绑定在一起,一经绑定便不能再与其他变量绑定。
				// 实际上编译器在进行引用操作时,底层实际上也是将引用看成一个指针,由上图可知,int& r和int* p一样在底层同样是由指针指向int x所在的内存位置的。
int x2 = 5;		
r = x2;		// 这里并不是说r又与x2进行了绑定,这里表达的是将x2的值赋给了r而已,即现在r,x都是5。

注意:
1.声明一个reference时,一定要同时进行赋初值,即告诉reference它要代表谁!
2.对于r和x,即object x其reference r来说,
虽然大小相同即sizeof( r ) == sizeof( x ),地址也相同即&x == &r,
但实际上在底层编译器对于r的操作是拿指针去实现的(在底层操作时相当于指针类型的变量),只不过从逻辑的角度我们可以将r看成与x一样是个int型变量。

trpedef struct Stag{
	int a, b, c, d;
}s;

int main(){
	double x = 0;
	double* p = &x; // p指向x,p的值是x的地址
	double& r = x;  // r代表x,现在r,x都是0
	
	cout << sizeof(x) << endl;  // 8
	cout << sizeof(p) << endl;  // 4
	cout << sizeof(r) << endl;  // 8 这是一个假象,因为在底层上其实r是指针大小的即4(32位系统下),只不过逻辑上其打印显示出来的大小是double类型的大小8.
	cout << p << endl;  // 0065FDFC
	cout << *p << endl;  // 0
	cout << x << endl;  // 0
	cout << r << endl;  // 0
	cout << &x << endl;  // 0065FDFC
	cout << &r << endl;  // 0065FDFC

	S s;
	S& rs = s;
	cout << sizeof(s) << endl;  // 16
	cout << sizeof(rs) << endl;  // 16
	cout << &s << endl;  // 0065FDE8
	cout << &rs << endl;  // 0065FDE8
}

reference可以说是一种漂亮的指针,其通常不用于声明变量,而用于参数类型(parameters type)和返回类型(return type)的描述。

void func1(Cls* pobj){
	pobj->xxx();
}
void func2(Cls obj){
	obj.xxx();
}
void func3(Cls& obj){
	obj->xxx();
}
...
Cls obj;
// 注意:接口不同,传入参数的形式不同!!
func1(&obj);
func2(obj);
func3(obj);

在这里插入图片描述

若函数签名相同则不能同时存在,如上图中,double imag(const double& im){…}与double imag(const double im){…},
此时若传入参数为double imag(a),则编译器不知是调用何者,则报错。
而const也是函数签名的一部分,所以double imag(const double& im) const{…}与double imag(const double im){…}可以同时存在。

四、Object Model(对象模型)

- 关于vptr和vtbl

在这里插入图片描述
当一个class类里头有一个(或者一万个)虚函数时,则生成的类对象里面便会有一个虚指针(无论多少个虚函数都只有一个虚指针!!),其指向一个虚表,虚表里面记载的是对应的虚函数地址信息。
在测对象a的大小时,其测出的大小比其包含的数据相加起来还要多4(32位系统下),即是虚指针的大小。

各成员函数在内存的情况如上图,
对于对象a,其非虚函数有A::func1(),A::func2(),虚函数有A::vfunc1(),A::vfunc2();
对于对象b,由于其继承了父类A,则其非虚函数有A::func1(),A::func2(),B::func2(),虚函数有A::vfunc2(),B::vfunc2()【注意此时对象b已经重写了父类A中的虚函数A::vfunc1(),所以在上图中b的虚指针指向的B的虚表中,其中一个地址指向的不再是原来的A::vfunc1()的地址。】
对于对象c,由于其继承了父类B和A,则其非虚函数有A::func1(),A::func2(),B::func2(),C::func2(),虚函数有A::vfunc2(),C::vfunc1()【注意此时对象c已经重写了父类A中的虚函数A::vfunc1()和父类B中的虚函数B::vfunc1(),所以在上图中c的虚指针指向的C的虚表中,其中一个地址指向的既不是原来的A::vfunc1()的地址也不是原来的B::vfunc1()的地址。】

底层原理:
在C中,当创建出一个指针P指向一个类对象时,若想要调用对象的成员函数,则编译器看到调用的动作,则编译成特定的语法call XXXX,这便是一种静态绑定。但若是通过指针去调用虚函数时,则不能进行静态绑定,而进行动态绑定。而 动态绑定的实现原理便是上图的,通过指针P找到对应对象c的虚指针vptr,然后再找到对应的虚表vtble,然后再在虚表里面找到看起对应的是第n个虚函数,从而进行对应虚函数的调用。
对于动态绑定的实现原理解释成C的形式,则是

(*(p->vptr)[n])(p);
// 或
(*p->vptr[n])(p);

具体例子解释:
在这里插入图片描述

这里,A::draw()相当于A::vfunc1(),而B::draw(),C::draw()则是对A::draw()进行了虚函数重写。

list<A*>myLst 创建出来的myLst容器里放置的是同一大小的指针元素A*(因为容器要求其中的元素大小一致),而且A*是向上转型的父类指针(up-cast),即通过子类指针赋值的父类指针(如A* = new B),因为这样创建出来的A*指针才能动态指向各个子类,从而调用子类重写的虚函数如B::draw(),C::draw()等,对应于这个例子中则是调用画出不同形状的draw()函数。

在C中对函数的调用只是进行静态绑定,这样运用在这个例子中,则是需要对子类成员函数做出一个个具体的 if 条件判断才能对应调用其构画不同形状的子类成员函数draw(),这样是相当麻烦的。

而C++中,编译器看到一个函数的调用,它是有两方面的考量即进行静态绑定还是动态绑定。若为静态绑定,则是编译成call XXXX的形式,可看成函数调用地址是固定的;
但如果符合某些条件则发生函数调用时则可进行动态绑定:
1.函数调用必须是通过指针来调用;
2.该调用函数的指针是up-cast型的;
3.调用的函数是虚函数。
只要满足以上条件,编译器就会把调用动作编译成(*(p->vptr)[n])( p )的形式,即动态绑定的形式,或称此为虚机制。而动态绑定意味着,调用的是哪个子类重写的虚函数,是不一定的,是需要看指针p指向哪个子类,就调用对应子类的虚函数,是动态的。而父类指针A*由于其可以由多种子类指针赋值表示,所以也称为多态,即A*的指针类型是多种多样的,可指向不同的子类从而调用其对应的不同的虚函数。

总而言之,虚函数、动态绑定、虚机制、虚指针+虚表,这些指的都是上面的同一个过程,同一件事情。

- 关于this

虚函数在使用时,一般有两种方式,多态Template Method(设计模式的一种)
在这里插入图片描述

由于CMyDoc是子类且继承了父类CDocument,则创建出来的对象myDoc,当需要用到其指针时则其类型是up-cast型的,即向上转型的指针。而虽然myDoc.OnFileOpen()是对象调用函数(可能会说函数OnFileOpen()没有传入参数,但实际上所有的成员函数都一定会有个隐藏的thispoint作为隐藏的参数),但因为是子类对象调用通过父类继承的成员函数(而不是自身子类的函数),所以实际上是指针调用函数,且调用的函数里面有虚函数,则满足三大条件为动态绑定。
另外传进去的是子类对象的指针,所以当在父类成员函数中,走到this->Serialize()这一步时,由于动态绑定,则指针指向了子类对象myDoc内存中的虚指针vptr,且通过虚指针指向对应vtbl虚表中的第n个对应的虚函数,即转成C的形式为(*(this->vptr)[n])(this)的过程。

- 关于Dynamic Binding

在这里插入图片描述因为a是父类A的对象,虽然初值是b经过转型赋值而得,但a.vfunc1()实际上是对象调用函数,而不是指针调用函数,所以不是动态绑定,且调用的是A的虚函数即A::vfun1()。同时由汇编码可看出,其是call xxx后面是个固定的地址,所以为静态绑定。

在这里插入图片描述
指针pa是由子类B new出来的父类A型指针,所以为up-cast指针,且通过指针调用虚函数,所以为动态绑定。而且汇编码call 后面跟的是个动态的地址。

五、关于const

const若放在函数的后面,则只能放在成员函数后面修饰,不能用于修饰全局函数的。

在这里插入图片描述常量对象不能调用非常量成员函数!!


const String str("hello world");
str.print();

// 若当初设计 string::print()时未指明const,
// 那么上面的代码str.print()便是由const object调用non-const member function,出错!!

当成员函数的const和non-const版本均存在时,怎么判断对象调用的是何版本(如non-const对象既可调用const版本,也可调用non-const版本):

常量对象const object只能且只会调用const版本,
非常量对象non-const object只能且只会调用non-const版本。

// COW:Copy on Write
// class template std::basic_string<...>有如下两个member functions:
charT operator[](size_type pos) const{
	....
	/*不必考虑COW*/
}
reference operator[](size_type pos){
	...
	/*必须考虑COW*/
}

另外,non-const member functions可调用const member functions,反之不行。
(可理解为限定范围大的non-const member functions可包含限定范围小的const member functions

六、关于new和delete

一般我们所使用的new和delete,如String* ps = new String(“hello”)和delete ps 称为表达式,表达式是不能改变的不能重写的(即我们使用表达式new或delete时其会分解成的几个执行步骤是不会变的),而后续其相应的分解下去的所调用的operator new()或operator delete()函数则是可以重载的。

在这里插入图片描述
在这里插入图片描述

- 重载 ::operator new, ::operator delete, ::operator new[], ::operator delete[] (全局的)

void* my Alloc(size_t size){
	return malloc(size);
}
void myFree(void* ptr){
	return free(ptr);
}

// 它们不可以被声明于一个namespace内
inline void* operator new(size_t size){
	cout << "global new() \n";
	return myAlloc(size);
}
inline void* operator new[](size_t size){
	cout << "global new[]() \n";
	return myAlloc(size);
}
inline void operator delete(void* ptr){
	cout << "global delete() \n";
	return myFree(ptr);
}
inline void operator delete[](void* ptr){
	cout << "global delete[]() \n";
	return myFree(ptr);
}

- 重载 member operator new/delete or member operator new[]/delete[]

class Foo{
public:
	void* operator new(size_t); 			\\ size_t为new出的对象的大小
	void operator delete(void*, size_t);	\\ 第二参数size_t可省略

	void* operator new[](size_t);
	void operator delete[](void*, size_t);
}

int main(){
	Foo* p = new Foo;
	...
	delete p;

	Foo* p = new Foo[N];
	...
	delete[] p;
}

实际上这里的 operator new(),operator delete(),operator new[ ](),operator delete[ ]() 都应 是静态的,因为本身这是个创建自身类对象的过程,而非static函数只能通过对象来进行调用但此时对象没创建好又谈什么通过对象调用函数。
所以operator new(),operator delete(),operator new[ ](),operator delete[ ]() 只能是static函数,则不需通过对象调用而可以直接通过类名调用!【这里之所以没加static关键字是因为编译器自己就默认operator new()等这些都是static函数了,就算不加系统编译器也会默认把它们当成static的

表达式new Foo会分解成三个步骤:
1.void* mem = operator new(sizeof(Foo)); // 调用Foo类重载的new函数,创建出存放一个Foo类所需的内存空间并返回指向该内存空间的地址;
2.p = static_cast<Foo*>(mem); // 将指向新建内存地址的void型指针转型为Foo型指针并赋值给p;
3.p->Foo::Foo(); // 指针p调用Foo类的构造函数。
表达式delete p会分解成两个步骤:
1.p -> ~Foo(); // 由指针p调用Foo类的析构函数;
2.operator delete( p ); // 调用Foo类重载的delete函数,且传入参数为指针p。

表达式new Foo[N]会分解成三个步骤:
1.void* mem = operator new(sizeof(Foo) * N + 4)); // 调用Foo类重载的new函数,创建出Foo类* N + 4(存放统计数组元素个数的数据信息) 大小的内存空间;
2.p = static_cast<Foo*>(mem); // 创建出存放Foo类元素的数组所需的内存空间并返回指向该内存空间的地址;
3.p->Foo::Foo(); // 指针p调用Foo类的构造函数,调用N次
表达式delete[] p会分解成两个步骤:
1.p -> ~Foo(); // 由指针p调用Foo类的析构函数,调用N次
2.operator delete( p ); // 调用Foo类重载的delete[]函数,且传入参数为指针p。

示例:

class Foo{
public:
	int _id;
	long _data;	
	string _str; // 其里面相当于一个指针,所以大小为4(32位)

public:
	Foo(): _id(0){
		cout << "default ctor.this=" << this << "id=" << _id << endl;
	}
	Foo(int i): _id(i){
		cout << "ctor.this=" << this << "id=" << _id << endl;
	}
	// virtual
	~Foo(){
		cout << "dtor.this=" << this << "id=" << _id << endl;
	}
	static void* operator new(size_t size);  // 加了static更加规范官方!
	static void operator delete(void* pdead, size_t size);
	static void* operator new[](size_t size);
	static void operator delete[](void* pdead, size_t size);
};

// 重载 member operator new/delete or member operator new[]/delete[] 
void* Foo::operator new(size_t size){
	Foo* p = (Foo*)malloc(size);
	cout << ...
	return p;
} 
void Foo::operator delete(void* pdead, size_t size){
	cout << ...
	free(pdead);
} 
void* Foo::operator new[](size_t size){
	Foo* p = (Foo*)malloc(size);
	cout << ...
	return p;
} 
void Foo::operator delete[](void* pdead, size_t size){
	cout << ...
	free(pdead);
} 

int main(){
	Foo* pf = new Foo;    // 因为重载了member operator new,所以调用void* Foo::operator new(size_t size)
	delete pf;

	// 下面强制采用globals
	Foo* pf = ::new Foo;	// 调用的是全局的 void* ::operator new(size_t);
	::delete pf;			// 调用的是全局的 void ::operator delete(void* p);
	return 0;
}

对应内存大小:
在这里插入图片描述
对于单个的Foo类对象,其大小为4(int)+4(long)+4(string实际相当于指针)=12,若加上虚函数时,则还需加上个虚指针的大小(4)即为16。同理对于Foo类的数组[]对象来说也一样,只不过此时数组对象还多出一个统计元素个数的内存空间(4),即为16*5+4=84。

另外注意图上的构造和析构的箭头顺序过程!
在这里插入图片描述
若new或delete时使用全局操作符号::,则就算重写了new和delete的成员函数,但编译器只会强制调用global version(全局的版本)。

- 重载 new(),delete()

重载new()可以有多个版本,但每个版本第一参数必须是size_t,其余参数以new所指定的placement arguments为初值即(…)小括号内的参数。

Foo* pf = new(300, 'c')Foo; // 第一参数必须是size_t表示大小为300,第二参数可以任意指定

在这里插入图片描述

重载delete()也可以有多个版本,但一般情况下不会被表达式delete调用。只有当new所调用的ctor抛出exception(异常),才会调用这些重载版本的operator delete()。而且它只可能这样被调用,主要用来归还还未能完全创建功的object所占用的memory(如在调用对应类的ctor函数时出现exception)。

在这里插入图片描述
上图代码中的第五个语句Foo* p5 = new(100) Foo(1); 调用了对应重载版本 new(size_t, long extra) 分配内存后,调用构造函数Foo(int){…throw Bad();},所以出现exception(异常),在G2.9中调用了对应重载new(size_t, long extra)的重载函数delete(void*, long)【即在分配好内存进行构造函数的调用时,在构造的过程发生异常没有成功,则应该将刚刚分配的内存释放掉,否则会造成内存泄漏。所以对应new()的相应的delete()会被调用,从而有机会去释放掉由于构造异常失败后,回收刚刚对应的new所分配的内存,避免内存泄漏。】但在G4.9则没调用(虽然没调用,但也没问题,只是说明放弃处理ctor发出的异常)。

  • new(extra)扩充申请量
    在这里插入图片描述

  • 在创建basic_string类对象,是通过调用create进入其源码操作即进入到new(extra)Rep代码段,然后其被编译器编译为:
    1.void* men = operator new(sizeof(Rep), extra);
    而这里的operator new(sizeof(Rep), extra)即是调用 struct Rep内部重载inline static void* operator new(size_t, size_t),而operator new(size_t, size_t)又进入其源码【右边的代码段】,且传入第二参数为extra
    2.p = static_cast<Rep*>(men);
    3.p->Rep::Rep();

  • 最终new分配出来的空间:字符串放在扩充出来的extra那里,而Rep段则存放共享计数的即有多少个人使用这个字符串。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值