一个类定义了一个类型,以及与其关联的一组操作。所谓类,是用户自定义的数据类型。
类机制是C++最重要的特性之一。实际上,C++最初的一个设计焦点就是能定义使用上像内置类型一样自然的类类型(class type)。
类的定义一般分为两部分,其中之一是“头文件”,用来声明类所提供的各种操作行为,另一文件是“程序源代码文件”,包含这些操作行为的实现内容。
//头文件Box.h
#include <iostream>
using namespace std;
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
// 成员函数声明
double get(void);
void set( double len, double bre, double hei );
};
为了使用标准库设施,我们必须包含相关的头文件。类似的,我们也需要使用头文件来访问为自己的应用程序所定义的类。习惯上,头文件根据其中定义的类的名字来命名。
// 程序源代码文件Box.cpp
// 成员函数定义
double Box::get(void)
{
return length * breadth * height;
}
void Box::set( double len, double bre, double hei)
{
length = len;
breadth = bre;
height = hei;
}
//主入口文件main.cpp
#include <iostream>
#include "Box.h"
int main( )
{
Box Box1; // 声明 Box1,类型为 Box
double volume = 0.0; // 用于存储体积
// box 1 详述
Box1.height = 5.0;
Box1.length = 6.0;
Box1.breadth = 7.0;
// box 1 的体积
Box1.set(16.0, 8.0, 12.0);
volume = Box1.get();
return 0;
}
1.认识头文件
此程序以两个#include指令开始,其中一个使用了新的形式。
包含来自标准库的头文件时,也应该用尖括号(<>)包围头文件名。
对于不属于标准库的头文件,则用双引号(" ")包围。
1.1 C++为什么会有头文件?
为什么不扫描所有.c文件,然后把其中的函数定义都找出来,生成统一的一个声明文件,以后编译每个.c文件的时候都依赖这个文件就可以了。类似于java那样。
原因如下:
(1)如果编译器想成功编译并链接某个源码文件,需要知道两点信息:数据类型和重定位信息。以Java为例,Java二进制包内存放有数据类型信息和重定位信息;而C++二进制库符号表内只存放了重定位信息,因此,若想成功编译链接C++源码,还需要头文件告知编译器数据类型信息。而早期的硬件存储空间还是运算能力都还不太够,通过.h/.c手动的把声明和实现分开来解决问题是个非常简单有效的办法。
(2)C语言设计之初是没有头文件的,数据都是定长的。但是后来出现了 short int 、long int ;char等类型,函数的调用者不知道压栈的长度。例如函数add(x,y)调用:应该是先压2个字节、再压4个字节喃,还是先压4个字节,再压2个字节;还是连续压2个4字节?在这种情况下,函数调用需要提前声明,以便让调用者得知函数的参数与返回值尺寸。
(3)C++最初是作为C语言的扩展而开发的,因此它继承了C语言的一些特性,例如单独编译和模块化设计。C++有头文件是要兼容C,而 C++ 的Module包含元数据信息还在委员会讨论之中。
(4)使用头文件的好处是可以将代码分为接口和实现两部分,接口定义在头文件中,实现定义在源文件中。这样可以避免每次都重新编译整个程序,提高编译速度。还可以有效地隐藏实现细节,提高代码的可读性和可维护性。
(5)此外,使用头文件还可以方便地共享代码,因为多个源文件可以包含同一头文件,从而共享其中的代码。
头文件(header)使类或其他名字的定义可被多个程序使用的一种机制。程序通过#include指令使用头文件。
1.2 如何编写头文件?
头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量等。头文件也经常用到其他头文件的功能。
编写头文件头文件常用的技术是预处理器。
在C++中,预处理器是一种在编译之前对源代码进行预处理的程序。它主要用于实现一些在编译时无法完成的任务,例如包含头文件、宏定义、条件编译等。
(1)包含头文件:使用#include指令可以包含头文件,以便访问其中定义的函数、变量、类等内容。
#include <iostream>
#include "myheader.h"
(2)宏定义:使用#define指令可以定义一个宏,以便在源代码中使用。宏可以定义一个常量、一个函数、一个代码块等。
#define PI 3.1415926
#define MAX(a, b) ((a) > (b) ? (a) : (b))
(3)条件编译:使用#if、#ifdef和#ifndef等指令可以进行条件编译,以便根据条件选择编译不同的代码。
例如,我们使用#ifdef和#endif指令进行条件编译,当定义了DEBUG宏时,输出Debug mode;否则,输出Release mode。
#ifdef DEBUG
cout << "Debug mode" << endl;
#else
cout << "Release mode" << endl;
#endif
(4)文件包含:使用#include指令可以包含其他源代码文件,以便将多个源代码文件组合成一个程序。例如,我们使用#include指令包含了myheader.h和mysource.cpp两个源代码文件,以便将多个源代码文件组合成一个程序。
#include "myheader.h"
#include "mysource.cpp"
预处理器的主要作用是在编译之前对源代码进行处理,以便生成一个新的源代码文件,这个文件包含了预处理器指令所表示的内容。预处理器可以使代码更加模块化、可读、易于维护,从而提高编程效率和代码质量。
编写头文件时应该遵循以下规范:
(1)头文件应该包含防止重复包含的预处理器指令。例如,使用#ifndef、#define和#endif指令可以防止头文件被多次包含。
正确用法:
#ifndef MYCLASS_H
#define MYCLASS_H
#include <iostream>
// MyClass类的声明
class MyClass {
public:
MyClass(int count);
void print();
private:
int m_count;
};
// MyClass类的实现
MyClass::MyClass(int count) : m_count(count) {}
void MyClass::print() {
std::cout << "count = " << m_count << std::endl;
}
#endif
错误示例:
//包含了同一个头文件myheader.h两次
//并没有使用#ifndef、#define和#endif等预处理器指令来防止重复包含,从而导致链接错误。
#include "myheader.h"
#include "myheader.h"
(2)头文件应该包含必要的声明和定义,但不应该包含不必要的内容。例如,不应该在头文件中包含函数的实现代码,因为这会导致多个源代码文件包含同一个实现代码,从而导致链接错误。
错误示例1:
//多次包含同一个头文件
#include <iostream>
#include <iostream>
错误示例2:
//包含了string头文件,但是并没有在程序中使用string类型或相关函数。
//这样会增加编译时间和可执行文件大小,降低程序的性能。
#include <iostream>
#include <string>
(3)头文件应该使用全局唯一的命名规范,避免与其他头文件或变量名称冲突。一般建议使用项目名或类名作为头文件的前缀,例如MyClass.h。
(4)头文件应该避免包含其他头文件,除非这些头文件是必需的。如果必须包含其他头文件,应该在头文件中使用前向声明来避免循环包含。
错误示例:
//包含了myheader.h头文件,但在函数中使用了MyClass类型,却没有使用前向声明来告知编译器这个类型的存在。
//这会导致编译器无法在编译函数前找到MyClass类型的定义,从而导致编译错误。
#include "myheader.h"
// 使用MyClass类型
void func(MyClass obj) {
// ...
}
正确用法:
// 前向声明MyClass类
class MyClass;
// 使用MyClass类型
void func(MyClass obj) {
// ...
}
// 包含MyClass类的头文件
#include "myclass.h"
//在上面的代码中,我们使用class MyClass;语句进行了MyClass类的前向声明
//然后在函数中使用了MyClass类型。
//最后,我们包含了MyClass类的头文件myclass.h,以便访问MyClass类的实现。
关于前向声明的解释:
// Forward declaration of class B
class B;
// Class A uses class B
class A {
public:
void doSomething(B& b);
};
// Class C uses class A and class B
class C {
public:
void doSomethingElse(A& a, B& b);
};
// Class B definition
class B {
public:
void doSomething();
};
// Implementation of A::doSomething()
void A::doSomething(B& b) {
b.doSomething();
}
// Implementation of C::doSomethingElse()
void C::doSomethingElse(A& a, B& b) {
a.doSomething(b);
b.doSomething();
}
在这个例子中,类C同时使用了类A和类B,如果直接在类C的头文件中包含类A和类B的头文件,很容易造成循环包含。使用前向声明可以避免这个问题。同时,由于类A和类B的定义分别在其他头文件中,所以在类A和类B的头文件中也必须使用前向声明来避免循环包含。