.h文件书写(侯捷第二讲)

文件引入

include< .h>包含的是库文件。
include “ .h”包含的是自己写的文件

关于全局变量

  头文件可以放什么?

  1. struct/union 的声明,C++可以在头文件的 struct/class 内部随即给出方法的实现
  2. typedef 声明
  3. 函数,类声明
  4. extern 变量声明
  5. 一些const常量
      绝对不能放
  6. 全局定义变量(声明都不要有,全局变量声明就是定义)
  7. 给出函数的定义(可以有声明,若非要在.h文件给出定义则需要结合extern预防重定义)
a.h
1
2
3
4
5
6
7
8
9
10

#pragma once
#include <iostream>
using namespace std;

void fun()
{
cout << "5";
}

b.cpp
1
2
3
4
5
6
7
8

#include "a.h"
#include <iostream>

int main() {
fun();
}

a.cpp
1
2
3
4

#include "a.h"
#include <iostream>

以上代码会有函数重定义报错,把a.h的函数声明留下,把定义搬到a.cpp就好了。

  否则重定义教你做人。

  C语言在头文件中包含全局变量有三种形式:

  1. 在头文件中声明(extern)一个全局变量,在一个C文件中定义全局变量,然后在所有的引用了这个头文件的C文件里都可以使用该全局变量值。
    如在main.c定义 int a = 3;
    然后定义一个main.h,那么所有引用main.h的c文件中,均可以访问a并更改其值。
  2. 在头文件中定义一个静态全局变量。那么所有引用该头文件的C文件,均拥有一个作用域在本文件范围内的同名静态全局变量。不同文件中的该变量虽然同名,但却是不同的变量。可以这样防止重命名冲突
      比如在a.h中定义:
    static int a = 3;
    那么在a.c中引用a.h,并将a值修改为4。
    在b.c中也引用a.h,不对a值做修改,打印a值,仍为3, 不会因为a.c中的修改而改变值,这是因为有static生成内部链接,让全局变量a/函数的作用域变小了,即使加extern也无法被其他文件调用。
  3. 在头文件中定义全局变量。该头文件仅可以被一个C文件引用一次。
    比如a.h中定义
    int a = 3;
    在a.c中可以引用a.h,并对a进行访问和修改。
    如果在同项目下有b.c引用了a.h,编译器在链接时会报同名全局变量的错误,导致编译失败。

头文件中的防卫式声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
    #ifndef __COMPLEX__  
#define __COMPLEX__
。。。
#endif
```
&emsp;&emsp;一般来说,在写c++代码的时候,每个cpp都会对应一个头文件(.h文件),当然也可能由于文件内容过于简单,没有cpp文件,只有头文件。我们的头文件可能是专门为某个cpp文件写的,也可能是公共的,为多个cpp文件服务。

&emsp;&emsp;防卫三段式只能保证同一个头文件不会在同一个编译单元中被重复展开,比如你在一个.cpp中#include "a.h"后又#include "b.h",而你在b.h中已经#include "a.h"了,这样三段式可以保证a.h只被编译一次而不是两次。但方位头文件可保护不了你在多个编译单元中重复定义相同的符号(重定义)。
&emsp;&emsp;只要头文件有了上面的三行代码,你就可以在任意用到该头文件的地方include该头文件。如果是第一次include,由于没有定义__HEAD__,因此就会首次定义,当第二次include该头文件时,由于已经定义了__HEAD__,所以就直接跳过了,不会造成重复声明。

&emsp;&emsp;建议:为了不必要的麻烦,最好在每个头文件上加上这样的防卫式声明。即使现在你只有一个cpp使用该头文件,万一之后再有别的cpp使用该头文件嘞。
PS:防卫式声明一般这样写:__(2个下划线,不是1个)+头文件名的大写(不包括.h)+__(2个下划线,不是1个),例如头文件为head.h时,就使用__HEAD__,这是为了防止重复。
&emsp;&emsp;补充:如果要在多个源文件之间共享全局变量(不要在头文件中定义全局变量,比如int a=5;但可以const int a=5,定义struct 等),你需要在每个源文件中extern声明该全局变量,然后在一个源文件中进行定义即可,其他使用外部链接声明即可,不需要#include定义头文件了。


# 类的写法
## 构造函数标准写法

```c++
class complex
{
public:
complex(double r = 0, double i = 0)
: re(r), im(i)
{ }

complex& operator += (const complex&);//有分号,是声明;这里传的是引用,const是说不能改引用的内容。
double real () const(return re;)//没分号,是定义。class里的函数分为会改变数据的,和不会改变数据的,不会改变数据内容,加const。
double imag () const(return im;)//没分号,是定义
private:
double re, im;

friend complex& __doapl (complex*, const complex&)
}
```
其中的构造函数等同于
```c++
complex(double r = 0, double i = 0)
{re = r; im = i}
```
构造函数的使用:
```c
complex c1(2,1);
complex c2;
complex* p = new complex(4);
```
注意类里面没有指针,所以类里面不需要析构函数。
## 解析complex& operator += (const complex&);
&emsp;&emsp;+=是二元操作符,有二元操作数,左或右,编译器看到这一行会把二元操作符作用在左边的操作数上,如果左边的操作数对此二元操作数有定义,编译器就会去找重载函数。
```c++
complex& complex::operator += (this,const complex& r)
{
return __doapl (this, r);
}

complex& __doapl(complex* ths, const complex& r)
{
ths->re += r.real();
ths->im +=r.imag();
return *ths;
}

  函数名是operator +=,函数调用后返回一个别名,这里定义一个complex c1(2,1);complex c2(3,4);c1 += c2;最后返回给c1。任何成员函数都有一个this point,谁调用成员函数这个this就指向谁。注意this是用complex&接收的,而与 this相同类型的临时变量value不能用complex&接收,因为value会在函数执行完毕后被释放掉。

注意

  需要注意的是上面返回了一个别名。但如果最终的重载运算结果不存储在二元操作数之中,需要临时存储,比如7+c1,那么重载运算就要放到class body之外,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
complex operator + (const complex& c1, const complex& c2)
{
return complex(c1.real()+c2.real(), c1.imag+c2.imag())
}
```
&emsp;&emsp;这里用到了临时对象typename(),省掉了命名(这也不是构造函数)。其中typename也可以是int,double等。这行代码执行完毕后空间被释放掉。
### cout<< ;原理
```c++
#include <iostream.h>

complex conj (const complex& x)
{
return complex (x.real(), x,imag());
}

ostream& operator <<(ostream& os,const complex& x)
{
return os<<'('<< x.real()<<','<<x.imag()<<')';
}
```

&emsp;&emsp;返回ostream&是为了应对cout<< c1 << conj(c2);的情况。
&emsp;&emsp;为什么conj()返回的临时对象可以被operator <<的别名接收?conj()运行完不是清理内存了吗?这是因为c++的内存清理机制是惰性的,它并不会在此函数调用完立即清除其占用的内存,return的临时对象编译器会保留一下,此时这个临时对象是有实际的地址,因此可以传给operator <<函数中的别名类型,执行到下一行,也就是cout结束后才把conj()之中临时对象的内存释放掉。
# 类中的this
上个大类中
```c++
complex c1,c2,c3;
cout<<c1.real();
cout<<c2.real();

用c语言这么写:
1
2
3
complex c1,c2,c3;
cout<< complex::real(&c1);
cout<< complex::real(&c2);

这个real函数:
1
2
3
4
5
6
7
8
class complex
{
public:
double real() const
{ return this->re; }//this->可写可不写,编译器会帮忙补齐的。
provate:
double re, im;
}

  因此在调用类中的函数时,编译器会把this指针指向调用函数的对象。

类中的静态变量

  创建一个银行账户的大类,创建的每个对象都是存款人,这些存款人在银行中有相同的利率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Account
{
public:
static double m_rate;//声明
static void set_rate(const double& x) {m_rate = x}
}

double Account::m_rate = 8.0;//定义

int main()
{
Account::set_rate(5.0);
Account a;
a.set_rate(7.0);
}

类中的静态成员变量

  类中的静态成员变量与类中的成员变量不同,无论创建多少个类的对象,静态成员都只有一份

类中的静态函数

  静态函数和一般的成员函数一样也只有一份。成员函数都是默认有this指针的,而静态成员函数没有this指针,因此它只能操作静态成员变量。

  也就是说你没有创建任何对象时,你也可以使用静态成员变量和静态函数,比如银行现在没有人来存钱,就不用创建对象,可以通过设置静态变量来先设定利率。这样以后即使10000个人创建对象,你也可以通过调整静态变量实时更改利率。如果把利率放在成员变量中,挨个更改每个已创建对象的利率是一个极其麻烦的过程!