后台开发工程师技术能力体系之编程语言2——类与对象

类与对象

  结构化设计是以功能为目标来构造应用系统,这种设计思路使得程序员不得不将由客体构成的现实世界映射到由功能模块组成的解空间中,背离了人们观察和解决问题的基本思路。面向对象的设计思想,让计算机直接模拟现实的环境,用人类解决问题的思路、习惯和步骤来设计应用程序,更加符合人类理解现实世界的思维。

1.类的基本概念

// 头文件Sales_data.h
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <iostream>
struct Sales_data {
	// 构造函数
	Sales_data() = default; //默认构造函数
	Sales_data(const std::string &s):bookNo(s) {}
	Sales_data(const std::string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n) {}
	Sales_data(std::istream &);
	
	// 成员函数
	std::string isbn() const { return bookNo; }
	Sales_data& combine(const Sales_data&);
	double avg_price() const;
	// 数据成员
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
// Slaes_data的非成员接口函数
Sales_data add(const Sales_data&,const Sales_data&);
std::ostream& print(std::ostream&,const Sales_data&);
std::istream& read(std::istream&,Sales_data&);
#endif

// 源文件Sales_data.cpp
#include "Sales_data.h"
#include <iostream>
using namespace std;
Sales_data& Sales_data::combine(const Slaes_data &rhs){
	units_sold += rhs.units_sold;
	revenue += rhs.revenue;
	return *this;
}
double Sales_data::avg_prive() const{
	if(units_sold)
		return revenue/units_sold;
	else
		return 0;
}

istream& read(istream& is,Sales_data &item)
{
	doule price = 0;
	is >> item.bookNo >> item.units_sold >> prince;
	item.revenue = price + item.unies_sold;
	return is;
}
ostream& print(ostream& os,const Sales_data& item)
{
	os << item.isbn() << " " << item.units_sold << " "
	<< item.revenue << " " << item.avg_price();
	return os;
}
Sales_data add(const Sales_data& lhs,const Sales_data& rhs)
{
	Sales_data sum = lhs;
	sum.combine(rhs);
	return sum;
}

基本概念
  一个类定义了一个类型,以及与其相关联的一组操作。类的基本思想是数据抽象封装数据抽象是一种依赖于接口实现分离的编程技术,类的接口包括用户所能执行的操作类的实现包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。
成员函数
  定义和声明成员函数的方式与普通函数差不多,成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部,定义在类内部的函数是隐式的inline函数。作为接口组成部分的非成员函数,它的定义和声明都在类的外部
  成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this,编译器负责把该对象的地址传递给成员函数的隐式形参this。在成员函数内部,我们可以直接使用其他的类成员,实际上是因为任何对类成员的直接访问都被看作this的隐式引用。this是一个常量指针,也就是说一旦初始化(隐式)完毕,我们就不能改变this中保存的地址。对于const成员函数而言,其中const的作用就是修改隐式this指针的类型,默认情况下,this的类型是指向非常量类类型的常量指针,因此我们不能把一个常量对象地址传递给this,也就是说我们不能在一个常量对象上调用普通的非const成员函数。对于const成员函数而言,其隐式的this指针是一个指向常量的指针,因此在常量对象上可以调用const成员函数。
  类本身就是一个作用域,类的成员函数的定义嵌套在类的作用域之内。在成员函数体内可以随意使用类中的其他成员而无须在意这些成员出现的次序,因为编译器在处理类时,首先编译成员的声明,然后才轮到成员函数体。当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配,也就是说,返回类型、参数列表和函数名都得与类内部的声明保持一致。如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定const属性。同时,类外部定义的成员的名字必须包含它所属的类名,并使用作用域运算符进行说明,编译器一旦遇到这样的函数名,就能理解剩余的代码是位于该类的作用域内的
非成员函数
  类的作者常常需要定义一些辅助函数,尽管这些辅助函数定义的操作从概念上来说属于类的接口的组成部分,但它们实际上并不属于类本身。我们定义非成员函数的方式与定义其他普通函数一样,通常把函数的声明和定义分离开来。如果函数在概念上属于类但是不定义在类中,则它一般应与类声明(而非定义)在同一个头文件中,在这种方式下,用户使用接口的任何部分都只需要引入一个头文件即可。
构造函数
  每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时,只要类的对象被创建,就会执行构造函数。不同于其他成员函数,构造函数不能被声明成const,因为当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性,所以,构造函数在const对象的构造过程中可以向其写值。
  如果我们没有为类显式定义构造函数,那么编译器会为我们隐式定义一个默认构造函数。对于大多数类来说,这个合成的默认构造函数将按照如下规则初始化类的数据成员:如果存在类内的初始值,则用它来初始化成员;否则,默认初始化该成员(对于内置类型或复合类型,比如数组和指针,它们的值将是未定义的)
  编译器合成的默认构造函数只适合非常简单的类,对于绝大多数普通的类来说,必须定义它自己的默认构造函数。原因有三:1、编译器只有在发现类不包括任何构造函数的情况下才会替我们合成一个默认的构造函数。一旦我们定义了一些其他的构造函数,那么除非我们再定义一个默认的构造函数,否则类将没有默认构造函数2、对于某些类来说,合成的默认构造函数可能执行错误的操作,例如对于含有内置类型或复合类型成员的类,如果没有类内初始值,那么合成的默认构造函数将会进行默认初始化,这些数据成员的值将是未定义的3、有的时候编译器不能为某些类合成默认的构造函数,例如,如果类中包含了一个其他类类型的成员并且这个成员没有默认构造函数,那么编译器将无法初始化该成员,对于这样的类来说,我们必须自定义默认构造函数,否则该类将没有可用的默认构造函数
  如果一个构造函数不接受任何实参,如Sales_data(),那么它就是一个默认构造函数。我们定义这个构造函数的目的仅仅是因为我们既需要其他形式的构造函数,也需要默认的构造函数。我们希望这个函数的作用完全等同于之前使用的合成默认构造函数,在C++11新标准中,如果我们需要默认的行为,可以通过在参数列表后面写上=default来要求编译器生成构造函数,如Sales_data() = default;=default可以和声明一起出现在类的内部(内联),也可以作为定义出现在类的外部(不是内联)。
  在构造函数的参数列表(冒号)和函数体(花括号)之间的代码称为构造函数初始值列表,如bookNo(s),units_sold(n),revenue(p*n),它负责为新创建的对象的一个或几个数据成员赋初值,当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。
拷贝、赋值和析构
  除了定义类的对象如何初始化之外。类还需要控制拷贝、赋值和销毁对象时发生的行为。如果我们不主动定义这些操作,则编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个数据成员执行拷贝、赋值和销毁操作。但对于某些类来说,合成的版本无法正常工作,例如当类需要分配类对象之外的资源时,合成的版本常常会失效(如果类包含vector或string成员,其合成版本能够正常工作)。

2.访问控制与封装

  我们为类定义了接口,但是没有任何机制强制用户使用这些接口。我们使用访问说明符来加强类的封装性:1、定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口;2、定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了(即隐藏了)类的实现细节

class Sales_data {  //将struct改为了class
public: //添加了访问说明符
	Sales_data() = default; 
	Sales_data(const std::string &s):bookNo(s) {}
	Sales_data(const std::string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n) {}
	Sales_data(std::istream &);

	std::string isbn() const { return bookNo; }
	Sales_data& combine(const Sales_data&);
private: //添加了访问说明符
	double avg_price() const;
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

  使用struct和class定义类唯一的区别就是默认的访问权限,struct的默认访问权限是public,而class的默认访问权限是private。
友元
  如果Sales_data的数据成员是private的,那么read、print和add函数就无法正常编译了,因为这些函数不是类的成员,无法访问Slaes_data的非公有成员。类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元

class Sales_data {  
// Slaes_data的非成员接口函数的友元声明
friend Sales_data add(const Sales_data&,const Sales_data&);
friend std::ostream& print(std::ostream&,const Sales_data&);
friend std::istream& read(std::istream&,Sales_data&);
public: 
	Sales_data() = default;
	Sales_data(const std::string &s):bookNo(s) {}
	Sales_data(const std::string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n) {}
	Sales_data(std::istream &);
	std::string isbn() const { return bookNo; }
	Sales_data& combine(const Sales_data&);
private: 
	double avg_price() const;
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
// Slaes_data的非成员接口函数声明
Sales_data add(const Sales_data&,const Sales_data&);
std::ostream& print(std::ostream&,const Sales_data&);
std::istream& read(std::istream&,Sales_data&);

  友元声明只能出现类定义的内部,但是类内出现的具体位置不限友元不是类的成员,因此不受它所在区域访问控制级别的约束,但一般来说,最好在类定义开始或结束前的位置集中声明友元。友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明,如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明

3.类的其他特性

类的成员
  我们可以在类中定义一个类型成员,类型成员同样需要遵守访问控制。类型成员可以使用using或typedef进行声明,此外,与普通成员有所不同,用来定义类型的成员必须先定义后使用。类型成员通常出现在类开始的地方。

class Screen{
public:
	typedef std::string::size_type pos; //等价于 using pos = std::string::size_type;
private:
	pos cursor = 0;
	pos height = 0,weight = 0;
	std::string constenes;
}

  定义在类内部的成员函数是自动inline的;如果在类内声明,在类外定义,我们无需在声明和定义的地方同时说明inline(虽然这是合法的),只需要其中一个地方说明inline即可,通常只在类外定义的地方说明inline即可,因为这样使类更容易理解。inline成员函数应该与相应的类定义在同一个头文件中

class Screen{
public:
	char get() const {return contents[cursor];} //隐式内联
	inline char get(pos ht,pos wd) const; // 显示内联
	Screen &move(pos r,pos c); //能在类外定义时被设为内联
};
char Screen::get(pos r,pos c) const
{
	pos row = r*width;
	return contents[row+c];
}

inline Screen &Screen::move(pos r,pos c) //在定义处指定为inline
{
	pos row = r*width;
	cursor = row + c;
	return *this;
}

  有时我们希望能修改类的某个数据成员,即使是在一个const成员函数内。要到达这个目的,可以通过在该数据成员的声明中加入mutable关键字,这种数据成员称为可变数据成员。一个可变数据成员永远不会是const,即使它是const对象的成员。因此,在一个const成员函数中可以改变一个可变成员的值

class Screen{
public:
	void some_member() const;
private:
	mutable size_t access_ctr;
};

void Screen::some_member() const
{
	++access_ctr;
}

友元再探
  我们除了可以把普通的非成员函数定义成友元,还可以把其他类定义成友元,也可以把其他类(之前已定义过的)的成员函数定义成友元。

class Screen{
	friend class Window_mgr; //Window_mgr是Screen的友元类,它的成员可以访问Screen类的私有部分
};

  如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。

class Window_mgr{
public:
	using ScreenIndex = std::vector<Screen>::size_type;
	void clear(ScreenIndex);
private:
	std::vector<Screen> screens{Screen(24,80,' ')};
};
void Window_mgr::clear(ScreenIndex i)
{
	Screen &s = screens[i];
	s.contents = string(s.height*s.width,' '); //可以直接访问Screen的私有成员contents
}

  必须注意,友元关系不存在传递性,也就是说,如果Windows_mgr有它自己的友元,则这些友元并不具有访问Screen的特权。每个类负责控制自己的友元类或友元函数。
  除了令这个Window_mgr作为友元之外,Screen还可以只为clear提供访问权限。当把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类,并按照一定的组织结构来满足声明和定义的依赖关系:1、首先定义Window_mgr类,其中声明clear函数,但是不能定义它;2、接下来定义Scree,包括对clear的友元声明;3、最后定义clear,此时才可以使用Screen的成员。

// 1、首先定义Window_mgr类,其中声明clear函数
class Window_mgr{
public:
	void clear(ScreenIndex);
};

// 2、接下来定义Scree,包括对clear的友元声明
class Screen{
	// Window_mgr::clear必须在Screen类之前被声明
	friend void Window_mgr::clear(ScreenIndex);
}

// 3、最后定义clear
void Windows_mgr::clear(ScreenIndex)
{
/*......*/
}

  注意,友元声明的作用只是影响访问权限,它本身并不具有普通意义上的声明作用,换句话说,如果要调用该友元函数,必须单独进行声明。友元函数可以在类内部定义,此时它的作用域已经扩展到类外的作用域,该函数应该在类外单独进行声明。

struct X{
	friend void f() { /* 友元函数可以定义在类的内部*/} // 即使将友元函数定义在类内
	X() {f();} //错误,f还没有被声明
	void g();
	void h();
};
void X::g() {return f(); } //错误,fh还没有被声明
void f(); //声明那个定义在X中的函数
void X::h() {return f(); } //正确,现在f的声明在作用域中了

4.类的作用域

  每个类都定义了它自己的作用域,在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问,对于类型成员则使用作用域运算符访问,例如Screen::pos ht = 24,wd = 25;
作用域和定义在类外部的成员
  一个类就是一个作用域的事实能够很好解释为什么当我们在类的外部定义成员函数时,必须同时提供类名和函数名。因为在类的外部,成员的名字被隐藏起来了。一旦遇到了类名,定义的剩余部分就在类的作用域之内了(剩余部分包括参数列表和函数体,但不包括返回类型),结果就是我们可以直接使用类的其他成员而无须再次授权了。

class Window_mgr{
public:
	ScreenIndex addScreen(const Screen&);
}

Window_mgr::ScreenIndex //返回类型在类名之前,位于作用域外,必须重新使用类名指定作用域
Window_mgr::addScreen(const Screen&s)
{
	screens.push_back(s);
	return screens.size()-1;
}

名字查找与类的作用域
  对于顺序编写的程序,名字查找的过程比较直截了当:1、首先在其名字所在的块中寻找其声明语句,只考虑名字使用之前出现的声明;2、如果没有找到,继续查找外层作用域;3、如果最终没有找到匹配的声明,报错
  对于类内部的成员函数来说(不包括返回类型和参数列表),名字查找的规则有所不同:1、首先,编译所有成员的声明;2、直到类全部可见后才编译函数体,编译器实际上是在处理完类中的全部声明后才会处理成员函数的定义。
  对于类成员的名字查找(包括成员函数的返回类型和参数列表):1、首先在类的作用域范围内查找,并且只会考虑使用名字之前的声明;2、如果没有找到就会在类外层作用域中查找(成员函数定义之前的范围);3、如果最终没有找到,则报错。

using Money = bool;
string bal;
class X
{
public:
    // using Money = int; // 如果这条声明语句没有注释,则Money是int的别名
    Money f() { // 这个的Money是外层作用域的Money,是bool的别名
    return bal; // bal是在成员函数体内出现,是在整个类可见后才被处理,因此bal是Money类型,而不是外层作用域的string类型
    } 
    // using Money = double; // 这条声明语句在Money出现之后,并不起作用
private:
	Money bal;
};

  一般来说,内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过。然而在类中,如果成员已经使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字;如果还没有成员使用外层作用域中的某个名字,则在类中可以重新定义

using Mondey = double;
class Account1 {
public:
	Money balance() {return bal;} //使用外层作用域的Money
private:
	using Mondey = double; //错误:不能重新定义Money
	Money bal;
};

class Account2 {
public:
	using Mondey = double; //正确
	Money balance() {return bal;} //使用内层作用域的Money
private:
	Money bal;
};

  在成员函数的函数体中的名字查找:1、首先在函数范围内查找,包括参数;2、在类内查找,类的所有成员都可见;3、在成员函数定义之前的外层作用域内查找。由于这种机制会产生一些名字隐藏的现象:1、函数参数名会隐藏成员名;2、类内的名字会隐藏类外的名字。对于被隐藏的类内成员名,可以通过类名或者this指针来访问;对于被隐藏的类外对象名,可以通过作用域运算符访问。为了避免这些名字被隐藏,一般来说,不建议使用其他成员的名字作为某个成员函数的参数,此外也不建议在类内作用域使用外层作用域中的名字。

class Screen {
public:
	void setCursor (int width,int height){
		cursor = width*height; //这里的height是参数height而不是数据成员height
	}
privateint width= 0;
	int height = 0;
}
int height;
class Screen {
public:
	void setCursor (int ht){
		height= ht; //这里的height是数据成员height而不是全局height
	}
privateint height = 0;
}

5.构造函数再探

构造函数初始值列表
  构造函数的初始值列表的作用就是显式初始化成员,在进入构造函数体之前执行。如果没有在初始值列表中显示初始化的成员,将在进入构造函数体之前执行默认初始化。有时可以忽略数据成员初始化和赋值之间的差异,但如果成员是const、引用或者属于某种未提供默认构造函数的类类型,那么就必须通过构造函数初始值列表来为这些成员提供初值。

class ConstRef{
public:
	ConstRef(int ii):i(ii),ci(ii),ri(i) { } 
private:
	int i;
	const int ci; //const成员必须通过初始值列表显示初始化
	int &ri; //引用成员必须通过初始值列表显示初始化
}

  构造函数初始值列表只是说明了用于初始化成员的值,而不限定初始化的具体执行顺序。实际上,成员的初始化顺序是由它们在类定义中的出现顺序决定的,与该顺序一致。一般来说,初始化的顺序没什么特别要求,但如果一个成员是用另一个成员来初始化的,那么这两个成员的初始化顺序就很关键了。

class X{
	int i;
	int j;
public:
	X(int val):j(val),i(j) {} //未定义,实际的初始化顺序是先i后j
}

  从构造函数初始值的形式上来看仿佛是先用val初始化了j,然后再用j初始化i。实际上,i先被初始化,但此时j的值是未定义的。为了避免误解,最好令构造函数初始值的顺序与成员声明的顺序保持一致,而且如果可能的话,尽量避免使用某些成员初始化其成员。
  如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。

6.类的静态成员

  有时我们希望一些成员与类本身直接相关,而不是与类的各个对象保持关联,我们通过在成员的声明之前加上关键字static使其与类关联在一起。和其他成员一样,静态成员可以是public或private的,还可以是常量、引用、指针、类类型等。类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。类似的,静态成员函数也不与任何对象绑定在一起,它们不包含this指针,因此静态成员函数不能声明成const的,我们也不能在staitc函数体内使用this指针。这一限制即适用于this的显式使用,也对调用非静态成员的隐式调用有效。
  和其他成员函数一样,我们既可以在类的内部也可以在外部定义静态成员函数当在类的外部定义静态成员时,不能重复static关键字,该关键字只能出现在类内部的声明语句。因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的,也就是说它们不是由类的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态数据成员。相反,我们必须在类的外部定义和初始化每个静态成员。类似于全局变量,静态数据成员定义在任何函数之外,因此一旦它被定义,就将一直存在于程序的整个生命周期。为了确保对象只被定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中

// Account.h
class Account{
public:
	void calculate() { amount += amount * interestRate; } //内联成员函数
	static double rate() { return interestRate; } // 内联静态成员函数
	static void rate(double);
private:
	std::string owner;
	double amount;
	static double interestRate;
	static double initRate();
};
// Account.cpp
#include "Account.h"
void Account::rate(double newRate){ // 静态成员函数在类外定义时不需要加static关键字
	interestRate = newRate;
}

double Account::interestRate = initRate(); // 类外定义静态数据成员

double Account::initRate(){ // 静态成员函数在类外定义时不需要加static关键字
	/*......*/
}

  通常情况下,类的静态数据成员不应该在类的内部初始化。然而,我们可以为静态数据成员提供const整数类型的类内初始值,不过要求静态数据成员必须是字面值常量类型的constexpr,初始值必须是常量表达式即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员,只是在类外部定义时就不能再指定一个初始值了

class Account{
	static constexpr int period = 30;
};

  静态成员能用于某些场景,而普通成员不能。例如,静态数据成员可以是不完全类型,特别的,静态数据成员的类型可以就是它所属的类类型,而非静态数据成员则受到限制,只能声明成它所属类的指针或引用

class Bar{
	static Bar meml; //正确,静态成员可以是不完全类型
	Bar* mem2; //正确,指针成员可以是不完全类型
	Bar mem3; //错误,数据成员必须是完全类型
}

  静态成员和普通成员的另一个区别是我们可以使用静态成员作为默认实参,非静态数据成员不能作为默认实参因为默认实参是在编译时确定的,非静态数据成员的值本身就属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终将引发错误。

class Screen{
public:
	Screen& clear(char = bkground);
private:
	static const char bkground;
}

7.类模板

  类模板是用来生成类的蓝图的,与函数模板不同,编译器不能为类模板推断出模板参数类型。为了使用类模板,我们必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表,它们被绑定到模板参数,编译器用这些模板实参来实例化出特定的类。

template<typename T>
class Blob{
public:
    using value_type = T;
    using size_type = typename std::vector<T>::size_type;
    // 构造函数
    Blob();
    Blob(std::initialize_list<T> il);
    // Blob中的元素数目
    size_type size() const { return data->size(); }
    bool empty() const {return data->empty(); }
    // 添加和删除元素
    void push_back(const T &t) {data->push_back(t); }
    // 移动版本
    void push_back(T &&t) { data->push_back(std::move(t)); }
    void pop_back();
    // 元素访问
    T& back();
    T& operator[](size_type i);
private:
    std::shared_ptr<std::vector<T>> data;
    void check(size_type i,const std::string &msg) const;
};

  与其他类相同,我们既可以在类模板内部,也可以类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。类模板的成员函数本身是一个普通函数,但是,类模板的每个实例都有其自己版本的成员函数。因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。默认情况下,一个类模板的成员函数只有当程序用到它时,才进行实例化。如果一个成员函数没有被使用,则它不会被实例化。

template <typename T>
void Blob<T>::check(size_type i,const std::string &msg) const
{
    if (i >= data->size())
        throw std::out_of_range(msg);
}

template <typename T>
T& Blob<T>::back()
{
    check(0,"back on empty Blob");
    return data->back();
}

  当我们使用一个类模板类型时必须提供模板实参,但这一规定有一个例外,在类模板自己的作用域中,我们可以直接使用模板名而不提供参数

template <typename T>
class BlobPtr{
public:
	BlobPtr& operator++(); //直接使用模板名而不提供参数
	BlobPtr operator--(int);//直接使用模板名而不提供参数
}

  当我们在类模板外定义其成员时,必须记住,只有当我们遇到类名后才表示进入类的作用域。

template <typename T>
BlobPtr<T> BlobPtr<T>::operator--(int) //函数返回类型并不在类的作用域,因此需要提供参数
{
	BlobPtr ret = *this; //函数体位于类的作用域,因此不用提供参数
	++*this;
	return ret;
}

8.析构函数

  析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,还可能做一些其他工作析构函数释放对象使用的资源,并销毁对象的非static数据成员。析构函数不接受参数,因此不能被重载,也就是说,对于一个给定类,只会有唯一一个析构函数。
  如同构造函数有一个初始化部分(初始值列表)和一个函数体析构函数也有一个函数体和一个析构部分在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化在一个析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁。在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何收尾工作,通常,析构函数释放对象生存期分配的所有资源。在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数,内置类型没有析构函数,因此销毁内置类型成员什么也不需要做,特别的,隐式销毁一个内置指针类型的成员不会delete它所指向的对象,但智能指针是类类型,具有析构函数,智能指针成员在析构阶段会被自动销毁。
  无论何时一个对象被销毁时,就会自动调用其析构函数:1、变量在离开其作用域时被销毁;2、当一个对象被销毁时,其成员被销毁;3、容器(无论是标准容器还是数组)被销毁时,其元素被销毁;4、对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁;5、对于临时对象,当创建它的完整表达式结束时被销毁。由于析构函数自动运行,我们的程序可以按需分配资源,而(通常)无须担心何时释放这些资源。

{ //新作用域
    // p和p2指向动态分配的对象
    Slaes_data *p = new Slaes_data; //p是一个内置指针
    auto p2 = make_shared<Slaes_data>(); //p2是一个shared_ptr
    Slaes_data item(*p); //拷贝构造函数将*p拷贝到item中
    vector<Slaes_data> vec; //局部对象
    vec.push_back(*p2); //拷贝p2指向的对象
    delete p; //对p指向的对象执行析构函数,当指向一个对象的引用或指针离开作用域时,析构函数不会执行
} //退出局部作用域:对item、p2和vec调用析构函数
  //销毁p2会递减其引用计数;如果引用计数变为0,对象被释放,在本例中,引用计数会变为0
  //销毁vec会销毁它的元素

  当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数,这个合成的析构函数的函数体为空,例如~Sales_data() { }。在(空)析构函数体执行完毕后,成员会被自动销毁。认识到析构函数体自身并不直接销毁成员是非常重要的,成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一个部分而进行的。

发布了19 篇原创文章 · 获赞 6 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/hjc132/article/details/105649953