Vec类——实现Vec类——复制控制——动态的Vec类型对象——灵活的内存管理
实现Vec类
为了实现通用性,需要写一个模板类。如下所示:
template <class T> class Vec{
public:
//接口
private:
};
当使用 Vec<int> V;时,编译器会将所有T的地方都替换成为int。
内存分配
需要注意的是,当使用new T[n]为Vec分配空间时,会运行T的构造函数为元素进行默认初始化。也就是说,只有在T具有默认构造函数的时候才能创建一个Vec<T>。
构造函数
全部构造函数都有一个目标,就是确保对象被正确的初始化。
explicit Vec(std::size_t n, const T& val=T()){create (n,val);} //使用默认的变量作为第二个参数
explicit这个关键字只在定义带一个参数的构造函数时才有意义。如果声明一个构造函数是explicit的,那么编译器只有在用户显示的使用构造函数时才会调用它,否则就不调用。在某些环境中,构造函数可能会被隐式的调用,这时,explicit的使用显得至关重要。
Vec <int> vi(100);//正确,显示的调用Vec的构造函数,以一个int类型数据作参数
Vec <int> vi=100;//错误:隐式的调用Vec的构造函数,并将他复制到vi
类型定义
迭代器类型:const / nonconst
类型定义:typedef
容器中存储的每个对象的类型: value_type
reference和const_reference 是value_type&和const value_type&的同义词
迭代器相减1后得到的结果类型:difference_type
索引与大小
重载运算符函数的定义与任何一个其他是基本相同的:必须有一个函数名,带几个参数,并指定其返回类型。重载运算符时,要将运算符刚在关键字operator后面。一定程度上来说,运算符的种类(为一元运算符还是二元运算符)决定该函数要带多少个参数。如果运算符是一个函数而不是一个成员,那么函数的参数个属于运算符的操作数一样多。第一个参数一定是左操作数,第二个参数一定是右操作数。如果运算符被定义成一个成员函数,那么他的左操作数必须为 调用该运算符的对象。可见,成员运算符函数比简单的运算符函数要少带一个参数。通常,运算符函数既可以是成员函数,也可以是非成员函数。但是索引运算符则必须为成员函数。
复制控制
复制构造函数
无论对于显式还是隐式的复制,都有一个名为copy constructor(复制构造函数)的特殊的构造函数进行。与其它的构造函数一样,赋值构造函数也是与类名同名的一个成员函数。由于它是用于复制一个已存在的同类型对象,一次初始化一个新的对象,因此复制构造函数只带一个参数,该参数与类本身具有相同的类型。
赋值运算符
一个类中可以定几种不同的赋值运算符(习惯上会通过不同的参数进行重载),其中以一个指向类自身的常量引用作为参数的版本比较特殊:它定义了再讲一个自定义类型值(对象)赋给另一个自定义类型(对象)时的操作。尽管类定义中可以定义几个不同版本的“operator=”函数,但是我们一般都足以这个特殊的版本作为代表,将他们叫做“赋值运算符”。
赋值时总是将一个已经存在的值(运算符左侧的对象)擦去,然后代之以一个新的值(运算符右侧的对象)。而进行复制时,我们先创建一个新的对象,因此不需要对一个已经存在的对象进行删除操作。
自我赋值:我们通过显示的检查左操作数与右操作数是否同一个对象来处理自我赋值的情况,正确处理这种情况对于赋值操作非常重要。使用关键词——this,只在成员函数内部才有效,代表指向函数操作的对象的指针。对于二元操作来说,例如赋值操作等,this总是指向左操作数。通常情况下,我们在需要只想对象本身的时候用this关键字。
在头文件里,由于模板参数是隐式的,因此不需要重复写<T>。而在头文件外面,必须声明返回类型,因此要在必要的地方显示的写出模板参数。
赋值不是初始化
在使用“=”为一个变量赋一个初始值时,程序自动调用复制构造函数。但是在赋值表达式中,程序调用operator=赋值操作函数。
赋值与初始化存在两个主要的区别:即赋值(operator=函数)总是删除一个旧的值;而初始化则没有这步操作。确切的说,初始化包括一个新对象并同时给它一个初始的值。
以下会发生初始化:声明一个变量;在函数的入口处用到函数参数时;函数返回中使用函数返回只时;构造初始化。
赋值只在表达式中使用使用=运算符的时候会被调用。例如:
string url_ch=”jaiojfawefoijo”;//初始化
string spaces(url_ch.size(),’ ’);//初始化
string y; //初始化
y = url_ch;//赋值
析构函数
析构函数(destructor),由他来定义如何删除该类的一个对象实例。析构函数的函数名是在类的名称前加一个波浪线前缀(~)。析构函数不带有任何参数,而且没有返回值。
默认操作
如果没有显示的定义复制构造函数和赋值运算符函数或者析构函数,那么编译器会自动为类生成相应的默认版本的的函数,进行一些默认的操作。
如果成员变量是类的对象实例,那么在对他们进行赋值、复制和删除时会调用相应类的各种对应构造函数或函数;如果成员变量为C++自带的变量类型,那么在进行前述那些操作时,不会额外做任何工作。即如果通过默认构造函数删除一个指针变量时,不会释放该指针指向的对象占用的内存空间。
默认构造函数有一个默认的操作。如果类中不存在定义任何构造函数,那么编译器将自动生成一个默认的不带任何参数的构造函数。这个构造函数可以通过对象自身在初始化时采取的方式,对其成员数据进行递归初始化(默认初始化、值初始化)。
注意:如果在类中显示的定义了任何构造函数,编译器将不会自动生成那个默认的构造函数。而在有些情况下,默认构造函数是必须的。:其中一种情况就是生成默认构造函数本身。在为一个雷生成默认构造函数时,为了对每一个数据成员进行默认初始化,要求成员数据的类型都具有相应的默认构造函数。所以,记住要为每一个类定义一个默认构造函数,既可以显示的定义,也可以隐式的定义。
三位一体原则
复制构造函数、析构函数以及赋值运算符函数相互之间关系非常密切,他们仨构成了“三位一体规则”:如果类需要一个析构函数,则他同时可能也需要一个复制构造函数与一个赋值运算符函数。
灵活的内存管理
使用new()运算符还可能为程序带来过多的资源开销。
在<memory>头文件中提供了一个名为allocator<T>的类,他可以分配一块预备用于存储T类型对象但是尚未被初始化的内存块,并返回一个指向这块内存块的头元素的指针。提供了一些功能的成员函数:allocate分配一块被指定了类型但却未被初始化的内存块;deallocate释放未被初始化的内存;construct在尚未被初始化的内存区域中构造单个对象;destroy函数删除它的参数所指的类型T的对象。