这回来一些烧脑(ku zao)的
在一个阳光明媚的下午,我带着愉快的心情,打开笔记本,但我完全没有想到,我接下来看到的是一些类的比较烧脑(ku zao)的特性。这回我们将会看到:
类的声明、其他的友元方式、友元声明与作用域、类的作用域。
今天的代码不多,这回不先贴代码了(因为没有完整的类),来,开始吧!
1.类类型
每一个类定义了唯一的类型。对于两个类来说,即使他们的成员完全一样,这两个类也是不同的类型。代码很简单,我就不贴了。贴张测试图片吧。
2.类的声明
跟函数和变量一样。类的声明和定义也是可以分离开的。
补充:函数的声明和定义分离开不用多说。变量怎么实现声明和定义分离呢?
extern int i //声明i而非定义i
int j //声明并定义j
好,接着回来说类。类的只声明不定义的方式就是:
class Screen
这种方式是告诉程序有一个名字是Screen,并且它是一个类类型。这种声明有一个专门的名词:前向声明。在这个类被声明之后定义之前,它是不完全类型。使用场景非常有限。
- 可以定义指向这种类型的指针或引用。
- 可以声明以不完全类型作为参数或者返回类型的函数
当我们在其他程序定义Screen对象时,这个类必须是被定义过的。因为编译器要知道给这个对象分配多少存储空间啊!
还有就是,类的数据成员不能是该类自己。但可以是指向该类的指针和引用。因为要想把类作为数据,这样这个类必须是定义完成的。
class Link_Screen
{
Screen windows;
Link_Screen* next;
Link_Screen* prev;
//这样定义是允许的。因为可以定义指向不完全类型的指针或引用。
};
到这里,不知道你有没有一些疑问?
先拿函数来说:既然我们把函数的声明和定义分离开了,那我们在其他文件使用该函数时,我们是怎么找到这个函数的定义的呢?
首先来说,一个文件如果想要使用别处定义的名字的话,必须在自己文件中包括对这个名字的声明。也就是使得这个名字被编译器所知道。
来看一个简单的例子。这个例子是在linux上做的。因为linux可以一步一步操作编译连接过程。
我们在add.h中做出对add函数的声明,在add.cpp中做出对其的定义。然后在main.cpp中使用它。
add.h
#include<iostream>
using namespace std;
int add(int x,int y);
add.cpp
#include<iostream>
#include"add.h"
using namespace std;
int add(int x,int y)
{
return x+y;
}
main.cpp
#include"add.h"
#include<iostream>
using namespace std;
int main()
{
cout<<add(1,2)<<endl;
}
首先是编译。编译完会生成两个目标文件。
然后是连接。将add.o和main.o连接起来。生成可执行文件。
这就不难解释为什么main.cpp能使用add函数了。原来是把它们链接起来了呀。
main.cpp中#include"add.h" 的原因是:要使用别处定义的名字,必须在本文件中包含其声明。
add.cpp中#include"add.h" 的原因是:要编译器检查声明和定义是否一致。
好,这就是函数,那类呢?
类也是一样的,我们把类分为.h和.cpp文件。h文件中包含的是对类的成员函数的声明已经对数据成员的定义。在cpp文件中包含对类成员函数的定义。 我们在使用这个类的时候,把这个h文件include进来,就可以定义这个类对象和使用它的成员函数了。原理跟上面的函数原理差不多。
3.其他的友元方式
前面我们提到了把普通函数定义为友元,我们还可以把类定义为友元类,以及只把类中的某些成员函数定义为友元。
友元类:
Window_mgr.h
#pragma once
#include<vector>
#include<string>
#include"Screen.h"
using namespace std;
class Window_mgr
{
public:
using ScreenIndex = vector<Screen>::size_type;
void clear(ScreenIndex i)
{
Screen& s = screens[i];
s.contents = string(s.hight * s.width, ' ');
}
private:
vector<Screen> screens{ Screen(40,40,' ') };//类内初始值
};
在这个类中,之所以能使用Screen类的私有成员,就是因为这里:
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非共有成员在内的所有成员。
成员函数作为友元
把单个成员函数提出来作为友元。但我还没弄出来…
重载函数与友元
就一句话,如果我们想把重载函数都变为友元,那需要挨个声明。
4.类的作用域(最后一个)
每个类都会定义自己的作用域,一个类就是一个作用域,这解释了为什么在类的外部定义成员函数时,必须提供类名和函数名。
一旦遇到了类名,编译器就将剩余部分认为成是在类的作用域里了。可以直接使用类的其他成员而无需再次授权。
但返回值不行!
void Window_mgr::clear(ScreenIndex i)
{
Screen& s = screens[i];
s.contents = string(s.hight * s.width, ' ');
}
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen& s)
{
screens.push_back(s);
return screens.size() - 1;
}
5.名字查找与类的作用域(这真的是最后了)
对于普通函数来说,名字查找比较简单粗暴。但对于类来说就不是那么回事了。类的定义分两步。
- 首先编译成员的声明
- 直到类全部可见后,才编译函数体。
这样就使得成员函数体直到整个类可见后才会被处理,因此它能使用类中定义的任何名字。
但请注意,这种两阶段过程只适合于成员函数体。对与普通的成员函数声明就不是那么回事了,看下面的例子。
typedef double Money;
string bal;
class Account
{
public:
Money balance() {return bal;}
private:
Money bal;
}
在这个例子中,当编译器看到balance()函数的声明时,会在类中搜索Money的声明,这里只考虑balance()前面的部分,当在类中没有找到时,会到类外进行查找,然后找到了typedef中的Money。
再看那个成员函数体中的bal变量,它适合于“两阶段法”,因此bal表示的是类中数据成员的bal。
来总结一下,类成员函数体中变量的名字查找过程:
- 首先,在成员函数内查找该名字的声明。这时要求前向。
- 若未找到,则在整个类中查找。
- 若再未找到,在类外成员函数定义之前的地方继续查找。
还需要注意最后一点!
当成员定义在类的外部时,名字查找不仅要考虑类定义之前的全局作用域中的声明,而且还要考虑成员函数定义之前的全局作用域的声明。详见P257。