======C++面向对象高级编程(下)第一周====== 本页内容是 //Boolan// C++ 开发工程师培训系列的笔记。\\ 因个人水平有限,我撰写的笔记不免出现纰漏。如果您发现错误,请留言提出,谢谢! ---- ====转换函数==== C++ 中可以对 build-in 类型的数据进行自动转换。但当我们希望对自定义的类型进行自动转化的时候,我们就需要自定义一个规则来匹配类型。我们称这个匹配类型的方法为**转换函数**(//Conversion Function//)。 \\ \\ 转换函数可以通过两种不同的路径来实现: - 改变对象的类型,使类对象的类型匹配表达式,也就是**向外转化**。 - 改变表达式中其他 operand 的类型,使其匹配对象的类型,也就是**向内转化**。 第一种方式是通过**类型转化函数**中改变对象的类型来实现的;而第二种方式则是通过**构造函数**来实现。 ===类型转化函数=== C++ 中类型转化函数的使用方法和操作符重载类似。我们可以通过如下方式来申明一个转换函数: operator double() const { return (double) (m_numerator / m_denominator); } 如果我们进行如下的调用: Fraction f(3,5); Fraction d2 = f + 4; 编译器会首先检查第二行的表达式中 ''f'' 的类型。当发现 ''f'' 的类型和 ''4'' 不匹配时,编译器检查是否有符合 ''f'' 的类型转换规则。因此,这里 ''f'' 通过定义被转化成了 ''0.6'',从而可以和 ''4'' 进行相加。 \\ \\ 几点需要注意的是: - 在函数名前面不能指定函数类型,函数没有参数。 - 该函数返回值的类型是函数声明中指定的类型。 - 类型转换函数只能作为**成员函数**,不能作为友元函数或普通函数。因为因为被转换是本类对象。 ===转换构造函数=== 转换函数在实质上是使得对象能转化成其他类型的数据。我们可以从另外一个方面来考虑一下:如果我们转换表达式里其他的元素来适配类对象的类型,也可以达到同样的效果。C++ 中的,我们可以通过转换构造函数(//Converting Constructor//)来实现这一点。\\ \\ 转换构造函数 的作用是**将一个其他类型的数据转换成一个类的对象**。当一个构造函数**只有一个参数**,而且该参数又不是本类的 const 引用时,这种构造函数称为转换构造函数。 \\ Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) { } 如果我们进行同样的调用: Fraction d2 = f + 4.0; 此时,编译器发现了 ''+'' 号的存在,于是去查看我们是如何重载 ''+'' 的,然后根据 ''+'' 需要的算子类型,尝试用我们先前定义的构造函数去转换这个算子。我们定义的构造函数是将 ''n'' 转化为 ''(n, 1)'',因此对 ''4'' 也能进行同样的操作。 Fraction d2 = f + Fraction(4, 1); 当然,为了完成这个加法,我们需要重载类对象之间的 ''+'' 运算: Fraction operator + (const Fraction& f) { return .....; } 需要注意的是,这里的构造函数有两个 parameter,但提供一个 argument 就足够了;因此这样的构造函数也被称为 //Non-explicit-on-argument Constructor//。 ===Explicit 关键字=== ''explicit'' 指定符**指定构造函数或转换函数不允许隐式转换**,也就是告诉编译器:本函数拒绝将其他类型的数据转化为类对象。 \\ \\ 先前给出了转换类型的两种方法。假设我们将这两种方法放置在了一个类里,那么编译器也分不出来优先使用谁,也就是所谓的二义性(//Ambiguous//)。 \\ \\ 但如果我们加上 ''explicit'',则会出现 "//Conversion from 'double' to 'Fraction' requested//” 的错误。原因就是构造函数拒绝将 ''4'' 转化为 ''(4,1)''。 \\ \\ //注:代码会不会产生二义性是取决于我们如何设计这些方法。// ====功能类的类别与实现==== 有时候 C++ 提供的 build-in 类型和功能并不能完全的满足我们的要求。我们通常通过设计一些功能型类来加强 build-in 类型和功能。显然的是,不管添加什么新功能,我们设计的这些功能类都必须包含以前 build-in 类型所具有的基本功能。 \\ \\ C++ 功能型类的类型往往分成两种: * //Pointer like class//,即类对象功能类似于指针的类 *// Function like class//,即类对象功能类似于函数的类 ===Pointer-like class=== 这种类型的类包含了指针的基本功能。C++ 中有两个非常著名的例子:**智能指针**(Smart Pointer)、**迭代器**(Iterator)。 \\ \\ 为了实现这些类,我们需要在这些类中实现指针的基本操作 :''*'' (取值),''->''(调用)。 ==智能指针== 要在智能指针中实现指针的基本功能,我们可以来这么做: \\ \\ 首先,通过一个构造函数来创造一个指针,指针的默认值就是一个基础的指针: shared_ptr(T* p): px(p) {} //px is a space for storing pointer 而这个基础指针是通过类对象的创建得到的: shard_ptr sp(new Type); \\ 接下来,我们开始实现指针操作: T& operator * () const { return *px; } //dereference T* operator -> () const { return px; } // return px 调用的方法如下: Type f(*sp); sp->func(); // using pointer up to call function func() 需要注意的是:''->'' 操作符会一直对得到的结果作用下去,直到得到最终的结果。对于 ''->'' 在智能指针的实现,''sp->'' 转换后得到的是 ''px''。由于 ''->'' 的特性,''sp->func()'' 实际上可以写成 ''px(->)func()'',也就是 ''px->func()''。 ==迭代器== 迭代器的功能是指向容器中的元素,因此迭代器实际上的功能也包含指针的功能。带迭代器除了实现基本的功能之外,还需要做一些移位、比较的功能,因此迭代器比起基本指针还需要设计**自增/减**等功能。 \\ \\ 而迭代器也需要对对象进行操作,因此和智能指针的设计有一些区别。以链表作为例子,因为链表的指针信息是存储在节点中的,所以对指针基本操作的实现如下: T& operator * () const { return (*node).data; } // returned a reference T* operator -> () const { return &( operator * ()); } // returned an address 这里重载 ''->'' 的写法实际上满足了使用端的意图: Foo::function(); (*iter).function(); (&(*iter)) -> function(); //pointer needed here 而 ''->'' 的实现通过嵌套 ''*'' 的实现完成了功能。 ===Function-like class=== 类的功能也可以被设计为像一个函数一样工作。我们都知道函数的结构是 ''name + function call operator "()"'',因此这种类实际上是通过重载 ''()'' 来实现类似函数的功能。比如我需要实现一个 ''pair'' 类的操作功能(''pair'' 包含两个不同类型数据): /* read the first element*/ struct select1st ... { const typename Pair::first_type& operator () (const Pair& x) const { return x.first); }; 上面的对 ''()'' 进行了重载:接收一个 ''pair'' 对象,然后返回该对象的第一个元素。这个类的对象,实际上具有了函数的功能。我们可以把这样的类称为**仿函数**。 \\ \\ 上面的代码中中间有一段 ''...'',这实际上是这些函数需要继承的父类。而这些父类一边可以表示为: unary_function; binary_function; 这些父类实际上表示了类的参数个数。这些奇特的父类看上去大小是 ''0'',但实际测试上很可能为 ''1''。 ====用Namespace测试函数==== ''namespace'' 的主要作用就是防止函数或者类的名称冲突。利用这个特性,我们可以用 ''namespace'' 来做测试。比如我们要测试 20 个函数,那么我们就可以把每一个函数放到单独的一个 ''namespace'' 里: namespace test01 { void test_function_01() { .... } } namespace test02 { void test_function_02() { .... } } 而在测试中我们就可以通过命名空间单独测试每一个函数了: test01::test_function_01(); test02::test_function_02(); 这样做的好处在于,每一个测试函数都是完全独立的,包括全局变量,我们也可以用 ''namespace'' 来模拟。通过这种方法,我们可以最大限度的避免函数之间共享变量带来的测试上的误差。 ====模板==== 函数模板和类模板的定义和使用请参考:[[cs:programming:cpp:boolan_cpp:oop_a_week2#类模板和函数模板|面向对象(上)第二周笔记3.3]]。\\ \\ 有一点需要注意的是,在函数的 argument deduction 的过程中,编译器也支持对自定义类型的对象的推测。模板本身可以编译,但使用的时候会根据具体的内容再编译一次,而这个过程中需要增加额外的验证(比如是否对自定义类型进行了相应的运算重载);而这个过程很可能导致编译失败。 ===成员模板=== 我们来考虑一下这样的情况:如果我们有四个类,有如下的关系: \\ \\
\\ \\ 用先前的 ''pair'' 类做为范例,我们可能会有这样的组合: * ''pair'' * ''pair'' ''catfish'' 类和 ''craw'' 类 分别属于 ''fish'' 类 和 ''bird'' 类的子类,将他们的类对象作为相应父类的初始化值肯定是合情合理的;但语法上的实现有一点问题:''pair'' 类类型接收 ''fish'' & ''bird'' 类类型以后,该类的构造函数只能接受 ''fish'' 类 和 ''bird'' 类作为初始化参数。我们要怎么让 ''pair'' 接受 ''catfish'' 类和 ''craw'' 类作为它构造函数的初始化类型? \\ \\ 为了解决这个问题,我们需要用到**成员模板**。成员模板允许我们在类模板中使用新的模板类型,从而使得类中被模板化的类型可以改变为新类型。 \\ \\ 就上述的问题来说,通过父类的构造函数,接收相应的子类作为初始值的方法,可以表现为如下代码: temeplate pair(const pair& p) :first(p.first), second(p.second) {} 成员模板在类模板中会表现为函数模板的形式,因此这里的 ''U1''、''U2'' 则是两个对应的子类的类型。当我们调用该构造函数的时候,参数是子类,但我们的 ''pair'' 对象仍然可以建立。 \\ \\ 除此之外,类模板也可以应用到指针类上。对于子类,我们一般可以通过 ''upcast'' ,即父类的对象可以是子类的方法来创建一个新类对象: Animal ptr = new pig; // a pointer to a animal. this animal could be a pig 智能指针作为一种类,因此也需要通过成员模板来实现接收参数的变更: template explicit shared_ptr(Tp1* pointer): base_shared_ptr(pointer) {} 在这里,''base_shared_ptr'' 作为父类的指针,接收的是 ''Tp'' 类型的参数,而我们通过成员模板定义的类型,成功的将子类交给了 ''base_shared_ptr'' 的构造函数进行初始化。如果要实现与上述普通指针一样的功能,具体的调用可以写成这样: shard_ptr spter(new pig); ===模板的特化=== **特化**(//Specialization//)相对于**泛化**(比如模板)而言。它表达了一种概念:对于某一些指定的类型,我们可以采用相应的处理方式来对其进行操作。这样的处理往往很高效,但我们需要制定规则来让编译器知道应该怎么做。而 C++ 的特化则很好的解决了这个问题。 \\ \\ 全特化(//Full Specialization//)的模板参数列表应当是空的,并且应当给出模板的 "实参" 列表(函数模板不需要,因为也可以通过 argument deduction 推导出类型: template<> struct hash {...}; struct hash {...}; ..... 应用则可以写成如下代码;对应不同的类型,编译器会指定不同的类: cout << hash () (1000); cout << hash () ('a'); ===模板的偏特化=== 模板的**偏特化**(Partial Specialization)是相对于全特化来说的,指只对模板参数中的一部分内容进行特化。偏特化分为两种: * 对模版参数列表中的某一个参数进行特化(个数上的偏特化) * 对模板参数类型选择范围的缩小(范围上的偏特化) \\ 第一种偏特化的例程如下: template class A { ... }; 类 ''A'' 的模板接受两个参数,一个指定为 ''int'' 类型的参数,另外一个参数则可以为任意类型。 \\ \\ 第二种片特化的例程如下: template class B { .... }; //partial specialization from T to T* template class B { .... }; 上面模板的参数从接收任意的类型,通过**偏特化**变成了接受指向任意参数类型的指针。 ===模板作为模板的参数==== 模板的参数自己本身也可以是模板。一个比较常见的例子就是我们的模板参数需要用到标准库的容器(标准库中的很多容器都是模板类)。模板做参数的写法如下: temeplate class Container > Class A { private: Container c; ...... }; 如果作为参数的模板只需要一个参数,那么我们可以写成如下格式: /*using smart pointer as a template parameter*/ A p1; 要注意的是:如果作为参数的模板需要的**参数不止一个**,且**没有提供默认值**,我们就不能直接像下面这么写: A myList1; // error, becuase list need a allocator parameter。 我们我们可以用 type alias 来写: template using Lst = list>; A myLIst2; // ok 还必须提到的是,如果在使用的过程中,作为参数的模板已经被指定了类型,那么该参数不能被称为模板模板参数: stack> s2; //list is not a template template parameter ====标准库简介==== 标准库是 C++ 的另一个重要的部分。标准库包含了**容器**(Container),**算法**(Algorithms),**迭代器**(Iterators),**仿函数**(Functors)等内容: * 容器代表了数据结构 * 算法代表了我们学习的算法 * 迭代器是对容器操作的指针 * 仿函数是实现了函数功能的类 \\ 关于学习方法:学习库这样的内容,最好的办法是把里面的方法都自己用一遍。 ====C++11的额外内容==== C++ 11(2.0) 提供了很多新内容。这些新内容需要新版本的编译器支持: * [[http://cpprocks.com/c11-compiler-support-shootout-visual-studio-gcc-clang-intel/|CppRocks]] 可以查看你的编译器是否支持 C++11。 * 如果 IDE 是 Dev C++, 可以通过 ''Project Options'' -> ''Complier'' -> ''Code Generation'' -> ''Language standard'' 设置编译版本为 ''ISO C++11''。 * 可以通过输出 ''__cplusplus'' 宏来确定当前 C++ 版本;该宏按 C++ 标准被要求必须集成到编译器里面。 ===参数可变的模板=== 传统C++中的模板必须指定参数个数。C++11 给出了模板的一个新内容:我们可以给模板指定动态数量的参数;我们称这样的模板为**参数可变的模板** (//Variadic Template//)。 \\ \\ //Variadic Template// 声明的形式为 //First + Rest(Pack)// 的形式,也就是第一个元素为一部分,剩余的其他元素作为一整体为另一部分: template void print(const &T, const Types&...args) { cout << firstArg << endl; print(args...); } void print() {} //base case 我们用 ''...'' 在这里表示剩余的元素是一个整体。而 ''print()'' 函数则用于递归打印每一个参数的值。 \\ \\ 如果我们这么调用: print(7.5, "hello", bitset<16>(377), 42); 打印结果为: 7.5 hello 00000000101111001 42 ''sizeof...'' 函数用于求 //Rest Pack// 中的元素。 ===Auto=== ''auto'' 指定符是 C++11中的一个语法糖。该指定符可以让编译器自动推断变量类型。关于 ''auto'' 的详细内容请查看:[[cs:programming:cpp:cpp_primer:2_var_n_types#autotypespecifier|Auto的基础信息]]。 \\ \\ 需要注意的是,使用 ''auto'' 并不代表依赖 ''auto''。使用 ''auto'' 的前提是你必须清楚的知道自己需要定义什么类型的变量。''auto'' 只能帮你简化输入,而并不能帮你做其他事情。 ===Rang Base For=== C++11 中提出了 for 循环的的一种新的写法://Range for//。这种循环的方式依赖于**迭代器**,主要使用对象是标准库里的容器。这种循环的写法如下: for (declaration : expression) statments; ''expression'' 部分必须是一个序列:比如:列表初始化的序列、数组、string、vector 等等。 \\ \\ range for 通过迭代器操作元素。如果想修改元素,我们需要用引用; for (auto &ele : container) ele *= 3 ; 引用虽然在调用函数的时候也开辟了栈空间存储自身,但存储的是 **argument 的地址**;所有对这个变量的使用都是通过指针在操作 argument。因此,我们在函数内修改了引用的时候,也就修改了 arugment 的值。 ===Reference=== 从内存的角度上来看,变量的种类有三种:值,指针,引用。指针是一种地址的形式,表达**指向**该地址的值;引用可以看作是一种和值绑定的别名,换句话说,引用**代表**了值。因此,指针的大小是一个字长,而引用的大小则是它对应的值是大小。 \\ \\ 引用从实现手段上是通过指针实现的,但从逻辑上,设计者希望将引用表达成一种代表的概念。因此,**引用的大小和被其代表的对象大小相同**,而两者的**地址也是相同的**。因此,当描述函数参数的时候,对于值传递和引用传递,他们具有 //Same Signature//;也就是说,如果有如下声明: double imag(const double& im); double imag(const double im); 当我们调用 ''imag'' 函数的时候,会引起二义性。 imag(4.0); // Ambiguity 不过,''const'' 也算是 signature 的一部分。因此如下定义不会被判定为二义性: double imag(const double& im) {....}; double imag(const double im) const {....}; 需要注意的几点: * 引用必须初始化 * 引用通常主要用于参数类型和返回类型的描述,而不用于声明变量。 ==引用绑定后不可更改== 指针指向对象以后还能修改,引用一旦绑定对象,就不能再次更改了,为什么呢? \\ 从编译的角度来说,程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。