======C++面向对象高级编程(下)第一周======
本页内容是 //Boolan// 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
\\
接下来,我们开始实现指针操作:
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 的过程中,编译器也支持对自定义类型的对象的推测。模板本身可以编译,但使用的时候会根据具体的内容再编译一次,而这个过程中需要增加额外的验证(比如是否对自定义类型进行了相应的运算重载);而这个过程很可能导致编译失败。
===成员模板===
我们来考虑一下这样的情况:如果我们有四个类,有如下的关系:
\\
\\
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 {....};
需要注意的几点:
* 引用必须初始化
* 引用通常主要用于参数类型和返回类型的描述,而不用于声明变量。
==引用绑定后不可更改==
指针指向对象以后还能修改,引用一旦绑定对象,就不能再次更改了,为什么呢?
\\
从编译的角度来说,程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。