目录 Table of Contents
10 扩展补充 类模板 函数模板 以及其它
到目前为止我们已经讲过了基于对象的程序设计
也就是写单一的 class, 对于这种单一的 class, 它的一个经典分类就是一种是带指针的, 一种是不带指针的
现在我们需要补充一些细节
static
所谓静态的意思, 你在一个 class 里面, class 里面有数据的部分, 有函数的部分, 你只要在数据或者函数前面加上 static
, 那么它就成为静态的数据或者静态的函数
在以前我们没有谈到这个 static 的时候, 使用者如果这样创建复数的话
创建出来的三个复数内存里面, 这里面的数据就是在这个类里面, 非静态的所有的那一些数据都创建了一份
complex c1, c2, c3;
cout << c1.real();
cout << c2.real();
从 C 的角度来讲, 我们是在调用相同的函数, 但是传给它的是不同的参数, 传的是不同的地址, 这样它才能够处理不同地方的数据, 这个函数只有这么一份
complex c1, c2, c3;
cout << copmlex::real(&c1);
cout << complex::real(&c2);
在我们之前没有 static 这个概念的时候, 函数只有这么一份
现在扩展开来, 现在在内存中的样子是图中的右上角
成员函数只有一份, 但是它要来处理很多对象, 一份函数要处理很多的对象, 一定要由某个东西告诉它你要处理谁, 它靠的就是 this pointer
所以这个图的意思是有一个 this pointer 会传进来, 然后函数才能根据 this pointer 来找到它要处理的数据在哪里
先前讲复数的时候说过, 成员函数有一个隐藏的参数 this pointer, 但是我们不能把它给写在代码里面, 这个是编译器自动会帮我们写的
因为是编译器帮我们写的, 所以你在函数体代码里面可以用它, 图中黄色的部分你可以不写, 你不写的话编译器就帮你加这个黄色的部分
现在我们加上静态 static, 加了这个之后的这一种数据, 它就跟对象脱离了, 它不属于对象了, 它单独的某一份在内存中某一个区域
那静态的函数, 它的身份和一般的成员函数, 在内存里面一样也是只有一份
那既然这个静态的数据只有一份, 那我们什么时候使用这么只有一份的数据. 例如你现在设计银行的这个体系, 户头的体系, 有一百万个人来开户, 既然有一百万个人, 那么你这个程序肯定要创建出一百万个人出来, 一百万个账户出来; 但是其中有一样东西不应该和账户有关, 那就是利率, 这个利率应该是一百万个人看到的同样的东西, 这个时候我们应该把利率设计为静态数据
那什么时候使用静态函数呢 ? 静态函数的特征和一般函数的差别在于, 静态函数没有 this pointer. 既然没有 this pointer, 可见它不能像一般成员函数那样, 去取, 去访问, 去处理这些对象里面的东西. 那显然它只能去存取处理静态的数据了
现在我们看一个例子
class Account
{
public:
static double m_rate; // m_rate 就是利率, 声明
static void set_rate(const double& x) { m_rate = x; } // 静态函数处理静态数据
};
double Account::m_rate = 8.0; // 从语法角度来讲, 既然是静态数据, 那么你一定要在 class 的外面写上这么一行; 这一行动作有人叫作设初值, 有人说叫声明, 严格属于来讲这个叫作定义
// 语法写出类型, 写出全名, 至于要不要给初值, 都可以
一般我们说写一行下来, 造成这里面的变量获得内存, 这一行代码叫作 定义, 所以在这个类里面说这一个变量是静态的, 这个只是 声明 而已
然后我们看看静态函数, 静态函数没有 this pointer, 所以它只能处理静态数据
我们再来看看如何调用这个静态函数
int main()
{
Account::set_rate(5.0); // 通过 class name 调用
// 就是我这个银行还没开张, 还没有人来开户, 但是我先要把这个利率设置好
Account a;
a.set_rate(7.0); // 通过 object 调用
}
调用 static 函数的方法有两种 :
-
通过 object 调用
-
通过 class name 调用
但是要注意, 以前通过对象调用函数的时候, 对象的地址会被传进去变成 this pointer, 由于这里是静态函数, 所以编译器不会帮我们把 this pointer 写进去
把 ctor 放在 privat 区
Singleton
class A
{
public:
static A& getInstance(return a;);
setup(){/*...*/}
private:
A();
A(const A& rhs);
static A a;
//...
};
先前讲复数的时候, 我们提到过一点
这是一个很有名的设计模式, 需求是我们写的 class 只需要产生一个对象
我现在写了一个 class A, 我在私有区域里面放了一个自己 A 这种东西, 并且是静态的, 所以当没有任何人创建 A 的对象的时候, 这个 a 本身已经存在了
然后我不想外界来创建对象, 我就把它的构造函数, 图里面有两个构造函数, 都放在 private 里面, 所以没有任何人可以来创建对象了
因此可以感觉出来这个 A 就只有一份 a 自己这个东西
既然外界不能创建, 那么外界怎么取得这唯一的这一个呢 ? 我们再设计一个静态函数, 叫作 getInstance()
, 这个函数就把刚刚那个唯一的自己 return 回去, 所以这个函数就等于是外界唯一的窗口, 唯一的接口, 外界只能通过这个函数来得到唯一的这个 a
外界只能这么写 A::getInstance().setup();
来调用 setup()
函数
这个设计模式叫作 singleton, 单子模式, 单例模式, 很容易就实现了出来
但是我们这个写法还不是最完美的, 我们来检讨一下, 你虽然写好了这个 a, 但是如果外界都没有用到的话, 这个 a 仍然存在, 好像有点浪费
所以更好的写法是这样的
class A
{
public:
static A& getInstance();
setup(){/*...*/}
private:
A();
A(const A& rhs);
//...
};
A& A::getInstance()
{
static A a; // 静态的自己放在这个函数里面
return a;
}
刚刚我们的例子里面, class private 区里面有一个静态的自己, 现在我们把它放到唯一的对外的窗口这里面来
好处就是, 一个函数里面静态的意思是什么, 只要有人调用到这个函数, 这个静态的数据才会出现 (函数调用开辟空间), 离开这个函数之后这个静态数据还在
这个意思就是, 如果没人用的话, 这个单例就不存在; 一旦有人用了一次, 这个单例才出现, 并且也永远只有这一份
cout
在前面的例子里面, 我们把东西打印出来, 都是用的 cout, 也许你会疑惑, 为什么 cout 可以接收各种类型的数据, 是不是它对这个 <<
运算符做了很多的重载, 是不是这样呢 ?
我们把标准库里面的源代码挖出来, extern _IO_ostream_withassign cout;
这个就是 cout
它这个数据类型, 我们往上看, 这个继承自 ostream, 所以我们可以简单地说 cout 就是一种 ostream, 然后我们去看 ostream 的定义
的确, 如我们想象的, 它做了很多的运算符重载, 正是因为这样 cout 才能接收各种类型的数据并且把它们打印出来
class template 类模板
template<typename T>
class complex
{
public:
complex(T r = 0, T i = 0) : re(r), im(i) {}
complex& operator += (const complex&);
T real() const { return re; }
T imag() const { return im; }
private:
T re, im;
friend complex& __doapl(complex*, const complex&);
};
模板是很大一部分的东西, 我们这里讲的是它的精髓部分
前面我们讲复数的时候, 我们实部虚部是设计为 double 的, 但是这样太死了, 如果它将来是整数是浮点数怎么办
所以我现在把实部虚部到底是什么类型, 我不把它给写死, 我就用一个符号来表示, 我全部用 T 来表示
然后我在第一行告诉编译器, 这个 T 是一个符号而已
这样本来它是一个 class, 现在它就变成了一个 class template 类模板
用的时候, 这么用
complex<double> c1(2.5, 1.5);
complex<int> c2(2, 6);
//...
这样子编译器看到它, 第一行就是把 T 全部替换为 double, 这样得到一份代码
编译器看到第二行, 又把上面的 T 全部替换为 int, 又得到一份代码
所以使用者这么用, 会得到两份这么几乎完全一样的代码, 只有这个 T 不一样
所以有的人会说, 模板会造成代码的膨胀, 但是这个膨胀是必要的, 因为你这里一个是实部虚部放的 double, 一个是放的 int, 这是两种情况, 一定需要两份代码, 这并不是缺点
function template 函数模板
我这里设计了一个 class 叫作 stone, 给了一些初值, 然后我要取出这 2 个 stone 里面最小的那一个
这个比大小的函数, 最简单的就是设计成右边这样了
那么本来这个比大小函数都是为 stone 服务的话, 那么这里面 a 和 b 传进来都是 stone 类型
但是我现在想, 好像传任何东西的比大小都是这样写
那么我是不是可以把 a 和 b, 不要它们是 stone, 而是把它们变成 T; 比大小的结果不是 a 就是 b, 所以这里的返回类型也是 T
我们希望有语法支持我们这样的想法, C++ 的确有这种语法, 但是我们必须要在上面告诉编译器这个 T 未定, 所以要写出黄色的这一行
先前那个是 template(typename T)
, 这里的是 template class T
, 其实两个关键字都是相通的
现在我们比大小, 编译器就会对 function template 进行实参推导, 这个引数也是实参的意思
这个就和类模板有很大区别了, 类模板使用的时候必须明确说明这个 typename 绑定的是什么数据类型
但是现在是 funtion template, 不必明确指出来, 因为编译器会做实参推导. 编译器会说 OK, 既然这个 r1 r2 都是 stone,, 所以它就跑到右边来, 把所有的 T 都替换为 stone
继续编译下去, 既然这个 a 和 b 要比大小, 这两个都是 stone, 那么 stone 要怎么比大小, 这个编译器就不知道了
那怎么办呢, 操作符重载. 编译器看这个小于号 <
是作用在 b 身上, 而 b 是一种 T, T 现在是 stone, 于是编译器就去找这个 stone 看里面有没有定义 <
这个函数
namespace
我们先前写的例子都没有用到 namespace, 其实有用到但是我们自己没有写
语法是这样的
namespace std //名称自定义
{
//...
}
所有里面的东西都被包装在这个命名空间里面
你到一个地方去, 你怕你发展的一些东西和别人同名, 同名是不可以的, 你自己写一个 namespace, 别人也写一个 namespace, 这样两个就不会打架同名了
那么既然被包装在一个单元里面, 我们怎么去用呢 ?
这里有几种方法
现在这里特别提出有个东西叫做 std, 这就是标准库所有的东西都被包在 std 里面
- using directive
等同于你把这个整个包装打开, 从此你不用再写 std 里面的全名了
#include <iostream.h>
using namespace std;
int main()
{
//cin << ...;
//cout << ...;
return 0;
}
这是全开
- using declaration
或者是我要一行一行地打开, 这种写法叫 using 声明
#include <iostream.h>
using std::cout; // 我写了这一行, 接下来我写 cout 就不用写全名了
// 所谓全名就是加上 namespace 的名称
int main()
{
//std::cin << ...; // 全名
//cout << ...;
return 0;
}
- 写出每一个的全名
你也可以这样把每一个的全名写出来
#include <iostream.h>
int main()
{
//std::cin << ...;
//std::cout << ...;
return 0;
}
当然最开始的那一种 using namespace std;
是最方便的