C++类模板的对象模型

x33g5p2x  于2021-11-09 转载在 C/C++  
字(3.2k)|赞(0)|评价(0)|浏览(339)

1. Template的“实例化”行为:(Template Instantiation)

1.1 Template类模板中成员变量的实例化:

考虑下面的 template Point class:

template <class Type>
class Point {
public:
	enum Status { unallocated, normalized };

	Point(Type x = 0.0, Type y = 0.0, Type z = 0.0);
	~Point();

	void* operator new(size_t);				//重载 new运算符
	void operator delete(void*, size_t);	//重载 delete运算符

private:
	static Point<Type> *free_list;
	static int chunkSize;
	Type _x, _y, _z;
};

首先,当编译器看到 template class 类模板的声明时,它会做出什么反应?
在实际程序中,什么反应也没有。即使template模板类中的static静态数据成员和enum数据也都不可用,它们中的每一个必须通过template模板类的某个实体来存取或操作,例如:

Point<float>::Status s;		//ok

Point::Status s;			//error

也就是说,Point类模板中的enum和static数据会在每一个实例中都产生出来,Point 和 Point 会分别产生单独的数据。

通过对模板类型对象求sizeof大小也可以看出模板实例化的差别:

template <typename _Tp>
class Demo {
public:
	_Tp  a;
};

int main() {
	Demo<int> 		_m1;
	Demo<double> 	_m2;

	cout << sizeof(Demo) << endl;	//编译报错
	cout << sizeof(_m1) << endl;	//4
	cout << sizeof(_m2) << endl;	//8

	return 0;
}

对template类模板类型直接求sizeof将会编译报错,对实例化后的模板对象求sizeof则与其实例化的类型相关:

error: use of class template 'Demo' requires template arguments
        cout << sizeof(Demo) << endl;

1.2 Template类模板中成员函数的实例化:

member functions 成员函数(至少对于那些未被使用过的)不应该被“初始化”。 只有在member functions被使用的时候,C++ Standard 才要求它们被“实例化”。

目前的编译器并不精确遵循这项要求。

之所以由类模板的使用者来主导“实例化”规则,有两个主要原因:

  1. 空间和时间效率的考虑:
    如果class中有100个member functions,但你的程序只针对某个类型使用其中两个,针对另一个类型使用其中五个,那么将其他193个函数都“实例化”将会花费大量的时间和空间;
  2. 尚未实现的机能:
    并不是一个template实例化的所有类型就一定能够完整支持一组member functions所需要的所有运算符。如果只“实例化”那些真正用到的member functions,template就能够支持哪些原本可能会造成编译使其错误的类型。

这些函数在什么时候“实例化”,目前流行两种策略:

  1. 在编译的时候;
  2. 在链接的时候。

对于不同类型的实例化,类模板中的成员函数也会产生多个实例。

2. Static Data Member:(类的静态数据成员)

静态数据成员独立于class之外,可以将其视为一个global全局变量。

注意:静态数据成员变量在类内只是声明,要放到类外定义和初始化,且必须要在类外初始化!

如果未对类内声明过的static成员初始化(在类外),或在类内对其初始化,则编译时将会报错。
static关键字只需要放在变量的声明处(类内),定义和初始化时不必写出,否则编译时将会报错。

实际上static静态成员变量并不存储在对象的内存空间中,所以存取static静态成员变量也不需要类对象,直接使用类名即可访问(与static静态成员函数方式相同)。

编译器会对每个类的static静态成员做“name-manling”处理,以避免不同的类中声明同名static静态成员时造成同名冲突。

举例:

class A {
public:
	static void func() {        //静态函数可以通过类名直接访问,只能访问类的static静态成员,不能访问非静态普通成员
        cout << _a << endl;
    }
private:
	static int _a;				//static静态成员变量在类内只能算是声明,并未定义和初始化,此时使用_a 将会报错
};

------
编译报错:

/tmp/ccOZViis.o:在函数‘A::func()’中:
test.cpp:(.text._ZN1A4funcEv[_ZN1A4funcEv]+0x6):对‘A::_a’未定义的引用
collect2: 错误:ld 返回 1

在上面的例子中,编译是将会报错,理由是类A中的静态成员变量 static int _a 在类内只能算作声明,实际上并未定义及初始化。

正确的写法是:

class A {
public:
	static void func() {        
        cout << _a << endl;
    }
private:
	static int _a;				
};

int A::_a = 0;	//static静态成员变量必须要在类外定义,此时不用带static关键字

另外,由于static静态成员变量不属于类,所以对类求sizeof大小时,也不会包含static静态成员变量。
例如:

class A {
    static int _a; 		//类中将不会包含 _a 的大小
    int        _b;
};

int main() {
    cout << sizeof(int) << '\n';
    cout << sizeof(A) << '\n';
}

------
4
4

3. NonStatic & Static Member Function:(类的非静态&静态成员函数)

类的静态成员函数与非静态成员函数都是存储在代码段中,
C++的设计准则之一是 非静态成员函数 至少必须与一般的非成员函数具有相同的效率。
所以成员函数虽然是写在类的内部,而实际上在编译阶段,编译器会将其转换为非成员函数,二者在底层上并无什么区别。

将一个非静态成员函数改写为一般的非成员函数,大致经过三个步骤:

  1. 改写成员函数的原型,增加一个形参,接收一个类类型的this指针;
  2. 改写成员函数的内部对类的非静态成员变量的操作方式,改为经由this指针操作;
  3. 改写成员函数为一个外部函数,将函数名经“name-manling”处理。
class Point3d {
	float magnitude3d() {}
};

---> 1. 改写成员函数增加*this形参:

float magnitude3d(Point3d const *this) {}	//this指针是指针常量,指向固定

---> 2. 改写成员函数内部访问成员变量的方式为通过this指针操作:

float magnitude3d(Point3d const *this) { return this->x; }

---> 3. 改写成员函数名为“name-manling”格式:

float Point3d_magnitude3d(Point3d const *this) { return this->x; }

类的静态成员函数的特点是:

  1. 没有this指针,因此不能操作类的非静态成员变量;
  2. 但可以不必非要经过类对象调用,可由类名直接调用。

相关文章