《C++ Primer》学习笔记 — 面向对象与泛型

一、重载操作符和类型转换

1、操作符与多态

运算符不支持多态相关的操作:

#include <iostream>
using namespace std;

class CLS_Base
{
public:
	CLS_Base operator = (const CLS_Base& other)
	{
		cout << "CLS_Base operator = (const CLS_Base& other)" << endl;
		return *this;
	}
};

class CLS_Derived : public CLS_Base
{
public:
	CLS_Derived operator = (const CLS_Derived& other)
	{
		cout << "CLS_Derived operator = (const CLS_Derived& other)" << endl;
		return *this;
	}
};

int main()
{
	CLS_Base* pBase = new CLS_Derived;
	pBase->operator=(CLS_Derived());
}

在这里插入图片描述
即使我们在操作符前面加上virtual关键字也是相同的结果。

2、成员函数 or 非成员函数

以下几条准则可以帮助我们判断应该将操作符重载函数设置为成员函数还是非成员函数:
(1)赋值(=)、下标([ ])、调用(( ))、成员访问剪头(->)运算符必须是成员函数;
(2)复合赋值运算符一般来说应该是成员函数;
(3)改变对象状态的运算符或者与给定类型密切相关的运算符,如递增(++)、递减(–)和解引用(*)运算符,通常应该是成员函数;
(4)具有对称性的运算符可能转换任意一段的运算对象,例如算数、相等性、关系和位运算符等,因此它们通常应该是非成员函数。

3、类型转换二义性

如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型间只存在唯一一种转换方式。否则的话,我们编写的代码将很有可能会具有二义性。

(1)转换构造函数和转换函数

class CLS_Test2;
class CLS_Test1
{
public:
	CLS_Test1() {};
	CLS_Test1(CLS_Test2&) {};
};

class CLS_Test2
{
public:
	CLS_Test2() {};
	operator CLS_Test1() {};
};

void test(const CLS_Test1&){}

int main()
{
	CLS_Test2 obj2;
	test(obj2); // multiple definition
}

(2)多个内置类型的转换函数

class CLS_Test
{
public:
	operator short() {};

	operator double() {};
};

void testLong(long){}

int main()
{
	CLS_Test obj;
	testLong(obj); // multiple definition
}

(3)函数重载与类型转换

当我们使用函数重载时,如果多个自定义的类型转换都提供了可行参数匹配,我们认为这些转换一样好。我们并不会考虑可能出现的标准类型转换。只有当重载函数能通过同一个类型转换函数得到匹配时,我们才会考虑其中出现的标准类型转换:

class CLS_Test2;
class CLS_Test1
{
public:
	CLS_Test1(int) {};
};

class CLS_Test2
{
public:
	CLS_Test2(double) {};
};

void test(const CLS_Test1&) {}
void test(const CLS_Test2&) {}

int main()
{
	test(10); // multiple definition
}

从标准类型的角度看,从10到int的转换应该优先级更高。然而,正如我们前面所说,在进行重载函数匹配时,我们并不会在标准类型的维度下进行参数比较。因此这里会产生二义性问题。

二、面向对象程序设计

1、访问控制

类的成员访问权限分为publicprotectedprivate三种。这三种权限直接影响了类的用户以及派生类能够使用和访问哪些成员方法和变量。其中,对于受保护的成员变量,其仅能被当前类的友元或成员函数通过当前类对象访问:

class CLS_Base
{
	friend void test(CLS_Base& obj)
	{
		obj.m_iMem = 1;
	}

protected:
	int m_iMem;
};

class CLS_Derived : public CLS_Base
{
	friend void testBase(CLS_Base& obj)
	{
		obj.m_iMem = 1;	// invalid 
	}

	friend void testDerived(CLS_Derived& obj)
	{
		obj.m_iMem = 1;	// valid
	}
};

2、继承方式

不同的继承方式影响了基类中的各种权限的成员方法和变量在基类中的权限。这里的权限是针对使用者和非直接派生类而言,对于直接派生类自身并不受影响:

#include <iostream>
using namespace std;

class CLS_Base
{
public:
	void testPub() {};
protected:
	void testPro() {};
private:
	void testPri() {};
};

class CLS_Derived : private CLS_Base
{
public:
	void testBase()
	{
		testPub(); // valid
		testPro(); // valid
		testPri(); // invalid 
	}
};

class CLS_Derived2nd : public CLS_Derived
{
public:
	void testBase()
	{
		testPub(); // invalid
		testPro(); // invalid
		testPri(); // invalid 
	}
};

int main()
{
	CLS_Derived obj;
	obj.testPub(); // invalid
	obj.testPro(); // invalid
	obj.testPri(); // invalid 
}

私有继承会将基类的公有和受保护成员变为派生类的私有成员。因此当该派生类再被继承时,这些已经成为私有成员的变量和方法将无法再被访问。

3、派生类和基类的转换

派生类和基类之间的转换取决于继承方式。总的来说,一个函数是否能使用到这种转换与该函数对类内成员的访问权限是一致的:
(1)只有D公有地继承B,用户才能使用该转换;
(2)无论D使用何种方式继承B,D的成员函数和友元函数都可以使用该转换;
(3)如果D公有或保护继承B,则D的派生类的成员和友元可以使用该转换。

4、构造函数与拷贝控制

基类默认构造函数的二义性将导致派生类的构造函数被删除:

#include <iostream>
using namespace std;

class CLS_Base
{
public:
	CLS_Base()
	{
		cout << "CLS_Base" << endl;
	};

	CLS_Base(int i = 10)
	{
		cout << "CLS_Base i = 10" << endl;
	};
};

class CLS_Derived : public CLS_Base{};

int main()
{
	CLS_Derived obj; // invalid the default constructor of CLS_Derived has been deleted
}

5、右值引用变量 和 std::move

我们前面提到过,可以将一个右值赋给一个右值引用类型的变量,而该变量本身是一个左值。C++参考手册中也提到,当右值引用变量类型的变量用于表达式中时,它其实是一个左值:

#include <iostream>
using namespace std;

class CLS_Test
{
public:
	void test() &
	{
		cout << "void test() &" <<endl;
	}

	void test() &&
	{
		cout << "void test() &&" << endl;
	}
};

CLS_Test test()
{
	return CLS_Test();
}

int main()
{
	test().test();
	CLS_Test&& obj = test();
	obj.test();
	std::move(obj).test();

	cout << boolalpha << is_same_v<decltype(obj), decltype(std::move(obj))> << endl;
	cout << boolalpha << is_same_v<decltype(obj), CLS_Test&&> << endl;
}

在这里插入图片描述
我们直接通过obj调用test方法,obj被认为是一个左值。因此调用的是左值引用的版本。我们使用std::moveobj转化为右值后,调用的才是右值引用版本。因此当我们想调用右值引用的函数时,需要调用std::move将变量(参数或this)转化为右值引用。

三、模板与泛型编程

1、类模板

默认情况下,类模板函数的实例化发生在调用时:

class CLS_Test
{
public:
	bool operator>(const CLS_Test& other)
	{
		return true;
	}
};

template <typename T>
class CLS_Template
{
	T data;
public:
	bool greater(const CLS_Template& other)
	{
		return data > other.data;
	}

	bool less(const CLS_Template& other)
	{
		return data < other.data;
	}
};

int main()
{
	CLS_Template<CLS_Test> test1;
	CLS_Template<CLS_Test> test2;
	test1.greater(test2);
	//test1.less(test2);
}

然而,如果我们对类模板进行了显式实例化,则类的所有函数都会被实例化。

template class CLS_Template<CLS_Test>; // invalid

2、智能指针删除器的绑定

shared_ptrunique_ptr删除器绑定的方式不一样。unqiue_ptr较为简单,直接将删除器设置为模板参数。而shared_ptr就较为复杂了,它支持在构造时设置删除器,也可以在reset时,重新设置新的删除器(在reset时,旧的删除器负责删除旧的对象,新的删除器用于以后新对象的删除)。那么,它是怎么实现的呢?源代码,继承和包含层次较为复杂,我们用简单的结构来模拟下:

#include <iostream>
using namespace std;

class CLS_Base
{
public:
	virtual void destroy()
	{
	}
};

template<typename T, typename Deleter>
class CLS_SharedBase : public CLS_Base
{
	Deleter del;
	T* ptr;
public:
	CLS_SharedBase(T* _ptr, Deleter _del) :
		del(_del),
		ptr(_ptr)
	{}

	virtual void destroy() override
	{
		del(ptr);
	}
};

template<typename T>
class CLS_SharedPtr
{
	CLS_Base* pBase;
public:
	template<typename Deleter>
	CLS_SharedPtr(T* _ptr, Deleter deleter) :
		pBase(new CLS_SharedBase<T, Deleter>(_ptr, deleter))
	{
		cout << "construct this = " << this << endl;
	}

	template<typename T, typename DeleterNew>
	void reset(T* other, DeleterNew deleter)
	{
		auto temp = CLS_SharedPtr(other, deleter);
		swap(temp.pBase, pBase);
	}

	~CLS_SharedPtr()
	{
		pBase->destroy();
		cout << "destruct this = " << this << endl;
	}
};

int main()
{
	CLS_SharedPtr<int> s(new int(1), [](void* p) {cout << "deleter1 p = " << p << endl; });
	s.reset(new int(2), [](void* p) {cout << "deleter2 p = " << p << endl; });
}

在这里插入图片描述
我们可以看出,delete的可重设依赖于多态性。虽然智能指针本身对于同样的类型只实例化一次,其中真正保存引用数据的模板类(在shared_ptr中为 _Ref_count_resource)却被实例化多次。

3、函数模板实参顺序

能够应用于使用模板参数限制的函数形参自动转换只有const转换以及数组和指针的转换。对于普通函数实参,其转换规则与普通函数一致。有时,用户希望指定实参类型,我们需要注意,指定的类型必须是模板参数中最左侧的类型。例如,对于下面的代码:

template<typename T1, typename T2, typename T3>
T3 test(T2, T1){}

如果我们想指定返回值类型,必须将三个模板参数类型都指定,或者将模板参数顺序颠倒。

4、引用折叠与右值引用

对比普通函数和模板函数在参数为右值引用时的参数传递:

template<typename T>
void foo(T&&){}

void test(int&&) {}

int main()`在这里插入代码片`
{
	int i = 42;
	foo(i);
	test(i); // invalid
}

我们可以将一个变量绑定到模板参数为右值引用的模板函数上。正常情况下,我们不能将一个右值引用绑定到一个左值上,但是C++规定了两个例外规则,允许这种绑定。这两个例外正是move这种标准库函数正确工作的基础(move也是模板库)。

第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板参数类型(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。因此当我们调用foo方法时,所推断出的模板参数为int&而非int

T被推断为int&看起来好像意味着foo的参数应该是一个**int&**的右值引用。通常我们不能直接定义一个引用的引用。但是通过类型别名或通过模板参数类型的间接定义是可以的。通过类型别名的定义如下:

typedef int& intRef;

int main()
{
	int a = 5;
	intRef&& b = a;
}

在这种情况下,我们可以使用第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用。只有在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即对于一个给定类型x
x& &x& &&x&& &都这定成类型x&
x&& &&折叠成x&&
我们使用代码测试下:

#include <iostream>
using namespace std;

template<typename T>
void foo(T&& other) 
{
	cout << boolalpha << "is_same_v<decltype(other), add_lvalue_reference<remove_reference<T>::type>::type> = " << is_same_v<decltype(other), add_lvalue_reference<remove_reference<T>::type>::type> << endl;
	cout << boolalpha << "is_same_v<decltype(other), add_rvalue_reference<remove_reference<T>::type>::type> = " << is_same_v<decltype(other), add_rvalue_reference<remove_reference<T>::type>::type> << endl;
}

int main()
{
	int i = 42;
	foo<int&>(i);
	foo<int&&>(move(i));
}

在这里插入图片描述
这两个规则带来的结果就是:如果一个模板函数的参数为T&&,则它既可以接收左值引用的实参,也可以接受右值引用的实参,且函数内部得到的形参类型同实参一致。同时,我们需要注意,如果参数类型形参类型和模板参数类型被推断为左值引用类型,那么该形参相当于输出参数。

5、右值与左值引用的模板函数重载

我们该如何编写模板函数以区分右值与左值引用参数的重载呢?

#include <iostream>
using namespace std;

template<typename T>
void foo(T&& other) 
{
	cout << "void foo(T&& other) " << endl;
}

template<typename T>
void foo(const T& other)
{
	cout << "void foo(const T& other)" << endl;
}

int main()
{
	int i = 42;
	const int && ri = 42;
	foo(i);
	foo(ri);
}

在这里插入图片描述
第一个模板函数可以绑定非const右值,第二个模板函数可以绑定左值和const右值。这里借助的是模板推导时的最少推导原则以及从右值到左值的默认转换。

6、转发

某些函数需要将一个或多个实参联同类型不变地转发给其它函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是左值还是右值。以下面的代码为例:

template<typename F, typename T>
void foo(F f, T t)
{
	f(t);
}

void rv(int&& other) 
{
	cout << "void rv(int&& other) " << endl;
}

void lv(int& other)
{
	other = 5;
}

(1)左值的转发

考虑以下测试代码:

int i = 42;
foo(lv, i);

在这种转发的情况下,虽然lv能够改变它的形参值,但是它改变的是foo函数调用它是传递给它的形参。而此形参实际上是i的一个拷贝。因此,这样的参数传递会导致lv函数修改实参的能力被抑制。那么我们该怎么修改foo函数呢?很简单,结合刚才学过的引用折叠,我们不难将之改为:

void foo(F f, T &&t)

这利用了我们前说的引用折叠时形参引用类型保持不变的特性。

(2)右值的转发

上述改变只解决了一半问题。因为此时我们还无法转发右值。考虑以下测试代码:

int i = 42;
foo(rv, move(i)); // invalid

虽然我们使用move(i)i转化为右值引用,但是在foo内部,形参t作为变量,是个左值,并不能传递给需要右值引用的rv函数。这时,我们就需要用到std::forward模板函数。它能保持原有实参的类型。该函数实际上返回的是转化为其模板参数所对应的右值引用类型的实参。如果模板参数为左值引用,则根据引用折叠,其返回的是左值引用;如果模板参数为右值引用,则返回右值引用类型。因此我们可以将foo函数改为:

template<typename F, typename T>
void foo(F f, T &&t)
{
	f(forward<T>(t));
}

7、可变参数模板的参数包转发

我们前面学习过参数包的展开,有时我们也需要将参数包转发给另一个函数处理:

template<T...Args>
void test(Args&&... args)
{
	f(std::forward<Args>args...);
}

这样每个参数都将被以其原始类型进行转发。

8、特例化类成员函数而非整个类

我们可以选择只特例化特定成员函数,而不是整个模板。如:

#include <iostream>
using namespace std;

template<typename T>
class CLS_Test
{
public:
	void testSpe()
	{
		cout << "testSpe" << endl;
	}
};

template<>
void CLS_Test<int>::testSpe()
{
	cout << "testSpe for int" << endl;
}

int main()
{
	CLS_Test<int> obj;
	obj.testSpe();
}

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值