C++Primer 第五版 3.类设计者的工具

当一个构造函数的第一个参数是自身类型的引用,且任何额外的参数都有默认值,则此构造函数是拷贝构造函数(也叫复制构造函数)。

格式:

class Foo
{
public:
	Foo(); //默认构造函数
	Foo(const Foo&); //拷贝构造函数
};

注意:1.拷贝构造函数第一个参数必须是一个引用类型。 2.虽然可以定义非const引用的拷贝构造函数,但是此参数几乎总是一个const的引用。3.拷贝构造函数不应该是explicit的。


赋值运算符的格式为:

class Foo
{
public:
	Foo & operator = (const Foo &) //赋值运算符
};

注意:1.赋值运算符通常返回指向其左侧运算对象的引用(return *this)。


析构函数释放对象的使用资源,销毁对象的非static数据成员。析构函数是类的成员函数,名字由波浪号接类名构成,没有返回值,也不接受参数。

格式为:

class Foo
{
public:
	~Foo(); //析构函数
};

注意:1.在析构函数中,不存在类似构造函数中初始化列表的东西来控制如何控制成员如何销毁。析构部分是隐式的。

            2.隐式销毁一个内置指针类型的成员不会delete它所指向的对象。

            3.智能指针是类类型,所以具有析构函数,智能指针指向的对象在析构阶段会被自动销毁。


我们可以通过将拷贝构造成员定义为=default来显式地要求编译器生成默认的版本。如果在类内使用=default修饰成员函数的声明,则默认为内联函数。

class Sales_data
{
public:
	Sales_data() = default;
	Sales_data(const Sales_data &) = default;
	Sales_data & operator = (const Sales_data &);
	~Sales_data() = default;
};

Sales_data & Sales_data::operator=(const Sales_data &) = default;


有时候需要阻止拷贝。如iostream类阻止了拷贝,以免多个对象写入或读取相同的IO缓冲。阻止的方法有以下几种。

1.定义删除的函数。在函数的参数列表后面加入= delete

class Sales_data
{
public:
	Sales_data() = default;
	Sales_data(const Sales_data &) = delete;
	Sales_data & operator = (const Sales_data &) = delete;
	~Sales_data() = default;
};

 =delete通知编译器,不希望定义这些成员。

与=default不同,=delete必须出现在函数第一次声明的时候。

特别注意:析构函数不能是删除的成员。


2.private拷贝控制。在C++11新标准发布之前,类是通过将其拷贝构造函数和拷贝运算符声明为private来阻止拷贝。

class Sales_data
{
private:
	Sales_data(const Sales_data &) ;
	Sales_data & operator = (const Sales_data &) ;
public:
	Sales_data() = default;
	~Sales_data();
};
由于默认构造函数和析构函数是pubilc的,用户可以定义对象,但是不能拷贝这个类型的对象。

如果类成员有指针,则设计类可以这样:

class HasPtr
{
private:
	string *ps;
	int i;
public:
	HasPtr(const string &s = string()) :ps(new string(s)), i(0){}
	HasPtr(const HasPtr &p) :ps(new string(*p.ps)), i(p.i){}
	HasPtr &operator = (const HasPtr &);
	~HasPtr(){ delete ps; }
};

//HasPtr & HasPtr::operator = (const HasPtr &rhs)
//{
//	auto newp = new string(*rhs.ps);
//	delete ps;
//	ps = newp;
//	i = rhs.i;
//	return *this;
//}

HasPtr & HasPtr::operator = (const HasPtr &rhs)
{
	if (this != &rhs)
	{
		delete ps;
		ps = new string(*rhs.ps);
	}
	i = rhs.i;
	return *this;
}

除了定义拷贝控制成员,管理资源类通常还定义一个swap的函数。如果按照标准库的swap我们需要一次拷贝和两次赋值。 而我们希望的是swap交换指针。即

string *tmp = v1.ps;

v1.ps = v2.ps;

v2.ps = tmp;

类的设计如下:

class HasPtr
{
	friend void swap(HasPtr &, HasPtr &); //这里设计的是全局的swap函数。
private:
	string *ps;
	int i;
};

inline void swap(HasPtr &lhs, HasPtr &rhs)
{
	using std::swap;
	swap(lhs.ps, rhs.ps); //交换指针,不是交换string数据
	swap(lhs.i, rhs.i); //使用默认的swap
}

与拷贝控制成员不同,swap并不是必要的。但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段。


定义swap的类通常用swap来定义它们的赋值运算符。这些运算符使用了一种名为拷贝并交换的技术。这种技术将左侧运算对象与右侧运算对象的一个副本进行交换:

//注意rhs是按值传递的,意味着HasPtr的拷贝构造函数
//将右侧运算对象中的string拷贝到rhs
HasPtr & HasPtr::operator = (HasPtr rhs)
{
	//交换左侧运算对象和局部变量rhs的内容
	swap(*this, rhs); //rhs现在指向本对象曾经使用的内存
	return *this; //rhs被销毁,从而delete了rhs中的指针
}

注意:1.参数rhs传递进来的时候已经是一个副本了。

            2.当赋值结束之后,局部变量rhs被销毁,此析构函数delete rhs现在指向的内存,也就是释放掉左侧运算对象中原来的内存。

            3.使用拷贝和交换的赋值运算符是异常安全的,且能够正确处理自动赋值。


当一个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的(显示)参数数量比运算对象的数量少一个。


通常情况下,我们将运算符作用于类型正确的实参,从而以这种间接方式“调用”重载的运算符函数。然而我们也可以向调用普通函数一样直接调用运算符函数,先指定函数名字,然后传入数量正确、类型适当的实参。

一个非成员运算符函数的等价调用:

data1 + data2;
operator + (data1, data2);
一个成员运算符函数的等价调用:

data1 += data2;
data1.operator += (data2);

1.=,[ ],( ),->这四个操作符重载必须定义为类成员函数。

2.复合赋值操作符重载通常应定义为类成员函数。复合赋值操作符如*=,+=等。但是不一定得这么做。

3.改变对象状态或者与给定对象联系紧密的操作符,如++,---,*等,通常定义为类成员函数。

4.对称操作符,如算术,相等,关系,位操作符,最好定义成普通非成员函数。


值得注意的是,如果我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。


输入输出运算符重载必须是非成员函数。(不然会出现data << cout这样的结果),返回的类型为相对应的形参。


一般的,输出运算符重载第一个形参是非常量的ostream对象的引用。第二个形参一般来说是一个常量的引用。

通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符。

ostream & operator << (ostream &os, const Sales_data &item)
{
        //os << XXX  
        return os;
}

输入运算符重在第一个形参是非常量对象的引用, 第二个形参也是一个非常量的对象。因为输入运算符的目的就是将数据读入到这个对象中。

输入运算符必须处理输入可能失败的情况!!!而输出运算符则不需要。

istream & operator >> (istream &is, Sales_data &item)
{
	//is >> XXX
	if (is)
	{

	}
	else
		item = Sales_data();
	return is;
}


通常情况下,我们把算术和关系运算符(==,!=,>,<)定义成非成员函数以允许左侧或者右侧的运算对象进行转换,因为这些运算符一般不会改变运算对象的状态,所以形参都是常量的引用。(上面第4条)

如果定义了算术运算符(+=*/ %),一般也会定义一个对应的复合赋值运算符(+=,-=,*=,/=,%=)。此时,最有效的方法就是使用复合运算符来定义算术运算符。返回局部变量的副本作为其结果。

Sales_data operator + (const Sales_data &lhs, const Sales_data &rhs)
{
	Sales_data sum = lhs;
	sum += rhs;
	return sum;
}
相等运算符重载分别比较对象的每一个数据成员即可。


赋值运算符必须定义为类的成员,复合赋值运算符通常情况下也应该这么做。这两类运算符都应该返回左侧运算符对象的引用。(return *this)


如果一个类包含下标运算符,则它通常会定义两个版本,一个返回普通引用,一个返回常量引用。

class StrVec
{
private:
	string *elements;
public:
	string &operator [](size_t n)
	{
		return elements[n];
	}
	const string & operator[](size_t n) const
	{
		return elements[n];
	}
};


++和--既可写在 变量 之前,称为 前置运算 ,如:++a;--a;++和--也可以写在变量之后,称为 后置运算 ,如:a++;a--。

class strBlobPtr
{
public:
	strBlobPtr &operator ++(); //前置运算符重载
	strBlobPtr &operator --();

	strBlobPtr operator++(int); //后置运算符重载
	strBlobPtr operator--(int);
};

定义递增和递减运算符的类应该同时定义前置版本和后置版本,这些运算符通常被定义成类的成员。(第3条)

(前置运算符应该返回递增或者递减之后的引用)(返回*this)

strBlobPtr& strBlobPtr::operator++()
{
	++curr;
	return *this;
}

(后置运算符返回的是一个值而非引用)

strBlobPtr strBlobPtr::operator++(int)
{
	strBlobPtr ret = *this;
	++ *this;
	return ret;
}


如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类也能存储状态,所以与普通函数想必它们更加灵活。

调用运算符也必须是成员函数。(上面第1条)


如果类定义了调用运算符,则该类的对象称作函数对象。


而lanbda也是函数对象。但是lanbda表达式产生的类不含默认构造函数,赋值运算符及默认析构函数。


前面介绍过C++语言中有几种可调用的对象:函数,函数指针,lambda表达式,bind创建的对象以及重载了函数调用运算符的类。

然而,不同类型的可调用对象可以共享同一种调用形式,调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型。

如:

int(int,int)
就是一个函数类型,它接受两个int,返回一个int。


而不同的调用对象共享同一种调用形式的情况,有时我们希望把它们看成具有相同的类型。如:

int add(int i, int j)
{
	return i + j;
}

auto mad = [](int i, int j){return i % j; };

struct divide
{
	int operator()(int denominator, int divisor)
	{
		return denominator / divisor;
	}
};
上面三种可调用对象,共享的是用一种调用形式:
int(int,int)

我们可以使用function的新标准库来把上述可调用对象看作同一种类型。(头文件:functional)。使用方法:

function<int(int,int)> 
而我们可以把这个function类型重新定义一个map。

#include <iostream>
#include <functional>
#include <map>
using namespace std;

int add(int i, int j)
{
	return i + j;
}

auto mod = [](int i, int j){return i % j; };

struct divide
{
	int operator()(int denominator, int divisor)
	{
		return denominator / divisor;
	}
};

int main()
{
	map<string, function<int(int, int)>> binops;
	binops.insert(make_pair("+", add));
	binops.insert(make_pair("-", minus<int>()));
	binops.insert(make_pair("*", [](int i, int j){return i * j; }));
	binops.insert(make_pair("/", divide()));
	binops.insert(make_pair("%", mod));

	function<int(int, int)> f1 = add;
	function<int(int, int)> f2 = divide();
	function<int(int, int)> f3 = [](int i, int j){return i * j; };

	//输出: 6 2 8
	cout << f1(4, 2) << endl;
	cout << f2(4, 2) << endl;
	cout << f3(4, 2) << endl;
	

	//输出:15 5 50 2 0
	cout << binops["+"](10, 5) << endl;
	cout << binops["-"](10, 5) << endl;
	cout << binops["*"](10, 5) << endl;
	cout << binops["/"](10, 5) << endl;
	cout << binops["%"](10, 5) << endl;

	return 0;
}

不能直接将重载函数的名字放入function类型中,可以存函数指针,或者使用lambda。


类型转换运算符是类的一种特殊成员函数。它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下所示:

operator type() const;

其中type表示某种类型。

注意:类型转换函数必须是类的成员函数,不能声明返回类型,形参列表也必须为空。

举例:

class SmallInt
{
private:
	size_t val;
public:
	SmallInt(int i = 0) :val(i){}
	operator int() const
	{
		return val;
	}
};


使用:

SmallInt si;
si = 4;  //首先将4隐式地转换成SmallInt,然后调用SmallInt::operator=


#include <iostream>

using namespace std;

class SmallInt
{
private:
	size_t val;
public:
	SmallInt(int i = 0) :val(i){}
	operator int() const
	{
		return val;
	}
};

int main()
{
	int c = SmallInt(4);
	cout << c << endl;
}


小提示:避免过度使用类型转换函数。在实践中,很少提供类型转换运算符。


在C++语言中,对于某些函数,基类希望它的派生类定义适合自身的版本,此时基类就将这些函数声明成虚函数(在其函数声明语句之前加上关键字viarual)。派生类必须在其内部对所有重新定义的虚函数进行声明(如果没有声明派生类会直接继承其在基类的版本)。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式的也是虚函数(派生类可以在这样的函数之前加上virtual,但是并不是非的这么做。此外基类中的形参和派生类中的形参必须严格匹配)


在C++中,当我们使用基类的引用(或指针)调用一个虚函数时会发生动态绑定


基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。(!!只有被继承的类才应该定义虚析构函数,Effiective C++第七条:如果类带有virtual函数,它就应该定义一个虚析构函数)


任何构造函数之外的非静态函数都可以是虚函数。


保护(protected)类型的成员是希望派生类有权访问,同时禁止其他用户访问。


C++11新标准规定,允许派生类显式地注明类中的虚函数。在最后加上一个关键字override。(因为如果是想继承虚函数,名字虽然相同但是形参列表不同,这仍然是合法的行为,编译器默认为一个新的函数。)


派生类初始化的时候首先是初始化基类部分,然后按照声明的顺序依次初始化派生类的成员。


派生类可以访问基类的公有成员和受保护的成员。


如果基类定义了一个静态成员,那么在整个静态体系中只存在该成员的唯一定义。不论从基类派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。


如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。


有时候我们会定义这样一种类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类,为了防止继承的发生,C++11新标准规定,可以在类名后面跟一个关键字final。


我们还能把某个函数指定为final,则派生类覆盖该函数的操作都将引发错误。


我们也可以为纯虚函数提供定义,不过函数体定义必须在类的外部。也就是说,我们不能再类的内部为一个=0的函数提供函数体。


含有纯虚函数的类是抽象类。我们不能创建抽象类的对象。


派生类的成员和友元只能访问派生类对象的基类部分的受保护成员,对于普通基类对象中的成员不具有特殊的访问权限。


某个类对继承而来的成员的访问权限受到两个因素的影响:一是在基类中该成员的访问说明符,而是在派生列表中的访问说明符。


对基类成员的访问权限只与基类中的访问说明符有关。而派生访问说明符目的是控制派生类对象对于基类成员的访问权限(后面那个是对象的调用)。


友元关系不能继承。派生类的友元也不能随意访问基类的成员。但是基类的友元可以访问派生类的基类部分。


默认情况下,使用class关键字定义的派生类是私有继承的,而使用struct关键字定义的派生类是公有继承的。


如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类的成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏。也就是说该派生类的对象只能调用这个派生类的成员而不能调用基类的成员。


名字查找与继承的方法:

假定我们调用p->mem或者(obj.mem()),则依次执行以下4个步骤:

1.首先确定p(或者obj)的静态类型。

2.在p(或者obj)的静态类型对应的类中查找mem。如果找不到,则依次在直接基类中不断查找直到继承链的顶端。如果找遍了该类以及基类仍找不到,则编译器将报错。

3.假如找到了mem,则进行类型检查,以便确认对于当前找到的mem,本次调用是否合法。

4.如果合法,则根据调用的是否是虚函数而产生不同代码。

   4.1如果mem是虚函数而且我们是通过引用或者指针进行调用,则编译器产生的代码将在运行时确定到底运行虚函数的哪个版本。依据是对象的动态类型。

   4.2反之,如果mem不是虚函数或者我们是通过对象(非引用或者指针)进行调用的,则编译器产生一个常规函数的调用。


在C++11新标准中,派生类能够重用其直接基类定义的构造函数。此时如果派生类有自己的数据成员,则这些成员将被默认初始化。格式:

using 直接基类:直接基类

#include <iostream>

using namespace std;

class A
{
public:
	int m_a;
	int m_b;
	int m_c;
public:
	A(int a = 1,int b = 2, int c = 3) :m_a(a), m_b(b), m_c(c){}
};

class B :public A
{
public:
	using A::A;
public:
	int m_d = 0;
};

int main()
{
	B b;
	//输出:1 2 3 0
	cout << b.m_a << " " << b.m_b << " " << b.m_c << " " << b.m_d << endl;
	return 0;
}

当我们使用容器存放继承体系的对象时,通常采取间接存储的方式。(存放指针(更好的是智能指针))

#include <iostream>
#include <vector>
#include <memory>
#include <string>

using namespace std;

class Quote
{
private:
	string bookNo;
protected:
	double price = 0.0;
public:
	Quote() = default;
	Quote(const string &book, double sales_price) :bookNo(book), price(sales_price){}
	virtual double net_price(size_t n) const
	{
		return n * price;
	}
	virtual ~Quote() = default;
};

class Bulk_quote:public Quote
{
private:
	size_t min_qty = 0;
	double discount = 0.0;
public:
	Bulk_quote() = default;
	Bulk_quote(const string&book, double p, size_t qty, double disc) :
		Quote(book, p), min_qty(qty), discount(disc){}
	double net_price(size_t cnt) const override
	{
		if (cnt >= min_qty)
			return cnt * (1 - discount) * price;
		else
			return cnt * price;
	}
};

int main()
{
	vector<shared_ptr<Quote>> basket;
	basket.push_back(make_shared<Quote>("0-201-2323", 50));
	basket.push_back(make_shared<Bulk_quote>("0-201-3113", 50, 10, 0.25));
	//输出:562.5   15 * 50 * 0.75
	cout << basket.back()->net_price(15) << endl;
	return 0;
}

当我们调用一个函数模板时,编译器用函数实参来为我们推断模板实参。除了定义类型参数,还可以在模板中定义非类型参数。当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断的值所代替。 这些值必须是常量表达式。使用的方式是不通过class或者typename而采用一个特定的类型名。

#include <iostream>

using namespace std;

template<typename T, unsigned N>
void print(T(&arr)[N])
{
	for (const auto &c : arr)
		cout << c << " ";
	cout << endl;
}

int main()
{
	double d[4] = { 1.0, 2.1, 3.2, 4.2 };
	print(d);
	int c[] = { 4, 3, 2, 1 };
	print(c);
	return 0;
}

此外函数模板可以声明为inline或者constexpr的, 放在模板参数列表之后,返回类型之前。


一般我们将类的定义和函数声明放头文件中,而普通函数和类的成员函数的定义放在源文件中。但是模板则不同,模板的头文件通常即包括声明也包括定义。


类模板与函数模板的不同是,编译器不能为类模板推断参数类型。使用类模板必须在模板名后的尖括号中提供额外信息。但是这也有一个例外,就是在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。


C++11新标准允许为类模板定义一个类型别名:

template<typename T>
using twin = pair<T, T>;

int main()
{
	twin<int> number;
}


 此外也可以使用模板默认实参。
template<typename T = int>
class Numbers
{
private:
	T val;
public:
	Numbers(T v = 0) :val(v){}
};

int main()
{
	Numbers<long long> lots_of_precision;
	Numbers<> average_percision; //空的<>表示我们希望采用默认类型
}


#include <iostream>
#include <memory>

using namespace std;

const int SPACE_CAPACITY = 8;

template<typename T>
class Vector
{
private:
	shared_ptr<T> data;
	int theSize;
	int theCapatity;
public:
	explicit Vector(int initSize = 0) : theSize(initSize), theCapatity(theSize + SPACE_CAPACITY),
		data(new T[initSize], [](T *p){delete [] p}){}

	Vector(const Vector &rhs) :data(nullptr)
	{
		operator = (rhs);
	}

	Vector & operator = (const Vector & rhs)
	{
		if (this != &rhs)
		{
			delete[] data;

			theSize = rhs.theSize;
			theCapatity = rhs.theCapatity;

			data = new T[theCapatity];
			for (int i = 0; i != theSize; ++i)
				data[i] = rhs.data[i];
		}
		return *this;
	}

	~Vector()
	{
		delete[] data;
	}

	void resize(int newSize)
	{
		if (newSize > theCapatity)
			reverse(newSize);
		theSize = newSize;
	}

	void reverse(int newCapacity)
	{
		if (newCapacity < theCapatity)
			return;

		T *oldArray = data;
		data = new T[newCapacity];
		for (int i = 0; i != theSize; ++i)
			data[i] = oldArray[i];

		delete[] oldArray;
	}

	T &operator[](int index)
	{
		return data[index];
	}

	const T &operator[](int index) const
	{
		return data[index];
	}

	bool empty() const
	{
		return theSize == 0;
	}

	void push_back(const T & val)
	{
		if (theSize == theCapatity)
			reverse(2 * theCapatity + 1);
		data[theSize++] = val;
	}

	void pop_back()
	{
		--theSize;
	}

	const T &back() const
	{
		return data[theSize - 1];
	}

	const T &front() const
	{
		return data[0];
	}
};


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值