首页 > 代码库 > C++ 文件include规则 常量定义
C++ 文件include规则 常量定义
总结一句话就是: C++的函数声明,变量声明,类定义写在头文件里,而函数实现,变量定义,类方法实现写在.cpp文件中;但是对于内联函数和模版类,函数的实现也要写在头文件里!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1. 将类的成员变量、类方法的定义写在.h中,将类方法的实现写在.cpp中,不要include .cpp文件,不要在.h文件中只写class MyClass; ,一定要写类成员变量和方法的全部定义!!!类方法的实现写在.cpp文件中。
2. 类模版或者模版的定义一定要写在同一个.h中,不要写在.cpp中,不能分开写!!!可以参考 http://blog.csdn.net/ixsea/article/details/6695496 中的解释:对于模板函数来说,只有被调用的模板函数才被实例化,这里的被调用并不要求它必须被main函数调用。某个普通函数调用了模板函数,该模板函数就将对应产生一个实例,而调用它的普通函数可能并不被main调用,也即有可能并不被执行。而普通函数.h 和 .cpp可以分开的原因是已经实例化好的,因此可以根据.h中的定义找到函数实现的位置!!!全文为:
----------------------------------------------------------------------------------------------------------------------------------------
观点
包含模型是C++模板源代码的一种组织方式,它鼓励将模板代码全部放在一个.h头文件中,这样可以避免莫名其妙的链接错误。
莫名其妙的链接错误
一般而言,程序员习惯将函数和类的声明放在.h文件、把它们的实现放在.cpp文件,这种多文件组织方式一直被倡导。一方面,这种分离使得代码逻辑清晰,想要了解程序用到哪些全局函数和类,只要查看.h文件就可以。如果把声明和实现都揉在一起,带来的麻烦可想而知,要在一堆乱糟糟的代码中寻找函数名、类名、成员名是一种折磨。另一方面,在构建动态链接库时,这种组织方式是必需的。因为动态链接库是二进制级别上的代码复用,它的一大优点就是具体的实现过程被隐藏起来,全部揉在一个.h文件中显然不符合要求。
然而不幸的是,当程序员仍然按照这种好的习惯编写模板代码时,却出现了问题。比如下面这个简单的例子:
- // Bigger.h
- template<typename T>
- T Bigger(T,T);
- //Bigger.cpp
- #include"Bigger.h"
- template<typename T>
- T Bigger(T a,T b)
- {
- return a>b?a:b;
- }
- //main.cpp
- #include"Bigger.h"
- #include<iostream>
- using namespace std;
- int main()
- {
- cout<<Bigger(10,20)<<endl;
- system("pause");
- return 0;
- }
这几行代码很简单,分成了三个文件Bigger.h、Bigger.cpp以及main.cpp,分别对应模板函数Bigger的声明、定义和使用。看起来结构清晰,符合好的编码习惯,编译链接却得到这样的错误提示:
Error 1 error LNK2019: unresolved external symbol "int __cdecl Bigger<int>(int,int)" (??$Bigger@H@@YAHHH@Z) referenced in function _main E:\Codes\Chapter3Lab\includeModel\main.obj
意思是链接器找不到main.obj里Bigge<int>函数的实现。这种看起来毫无道理的链接错误,也很好的体现了模板的实例化规则。
模板的实例化规则
对于模板函数来说,只有被调用的模板函数才被实例化,这里的被调用并不要求它必须被main函数调用。某个普通函数调用了模板函数,该模板函数就将对应产生一个实例,而调用它的普通函数可能并不被main调用,也即有可能并不被执行。
模板类也有类型的实例化规则,特别的是即使显式实例化了类模板,类模板的成员函数也未必被实例化,这是模板类的“不完全”实例化规则,读者可以点击这里了解更多。
链接错误的解释
了解了模板的实例化规则,就可以对上面的链接错误做出解释了。main.cpp中调用了Bigger(10,20),按理说这将引起模板函数Bigger(T,T)被实例化为普通函数,然而在main.cpp所属的翻译单元里并没有Bigger(T,T)的实现,对main.cpp所属的翻译单元来说,Bigger(T,T)的实现是不可见的。因此,由main.cpp所属翻译单元编译得到main.obj时,编译器假设Bigger<int>(int,int)在其它翻译单元中。
Bigger.cpp虽然有Bigger(T,T)的实现,但是由于在Bigger.cpp所属翻译单元中Bigger并没有被调用,因此Bigger.cpp就没有义务对模板函数Bigger(T,T)进行实例化,于是由它产生的Bigger.obj中也找不到的Bigger<int>(int,int)。
本文前述例子中的链接错误信息正是表达的这个意思。
链接错误的进一步探讨
既然是因为Bigger.cpp没有义务对Bigger(T,T)进行实例化,那么在Bigger.cpp中增加一个调用Bigger<int>(int,int)函数的普通函数是否就可以了呢?在Bigger.cpp文件中添几行代码,如下所示:
- //Bigger.cpp
- #include"Bigger.h"
- template<typename T>
- T Bigger(T a,T b)
- {
- return a>b?a:b;
- }
- void g() //增加一个调用Bigger<int>(int,int)的普通函数g()
- {
- Bigger(1,2);
- }
编译、链接成功,允许结果正确,进一步验证了上述观点。
解决方法 - 包含模型
本文列出的例子很简单,规模小,所以按照模板的实例化规则,“人为”地介入到模板函数的实例化过程中并让程序成功运行。但是,在规模较大的程序里,想要人为介入加以控制几乎是不可能的,应该使用C++推荐的包含模型。
具体做法并不复杂:把模板的声明和定义放在一个.h文件中,凡是用到该模板的.cpp文件包含它所在的.h文件就可以了。上面的例子使用包含模型改写,最终是代码是这样的:
- // Bigger.h
- template<typename T>
- T Bigger(T a,T b)
- {
- return a>b?a:b;
- }
- //main.cpp
- #include"Bigger.h"
- #include<iostream>
- using namespace std;
- int main()
- {
- cout<<Bigger(10,20)<<endl;
- system("pause");
- return 0;
- }
不过仍然有一个问题值得思考:当多个.cpp文件同时包含Bigger.h时,就有可能产生多份相同类型的实例化,这样是否会造成最终生成的.exe文件变得庞大?这个问题理论上是存在的,不过现在大多数编译器都对此作了一定的优化,一个模板的相同类型有多份实例化体时,编译器最终只保留一个,这样就避免了“代码膨胀”的问题。
下面给一个例子:
//main.cpp
#include "person.h" #include "SmartPointer.h" using namespace std; int test() { //auto_ptr<person> p(new person("Cici")); //SmartPointer<person> p(new person("Cici")); //p -> tell(); SmartPointer<person> r(new person("taoqi")); SmartPointer<person> p(new person("Cici")); p -> tell(); { SmartPointer<person> q = p; q -> tell(); r = q; SmartPointer<person> s(r); s -> tell(); } r -> tell(); return 0; } int main(){ test(); return 0; }
//SmartPointer.h
#ifndef SMARTPOINTER_H #define SMARTPOINTER_H template<typename T> class SmartPointer { public: SmartPointer(T* ptr); ~SmartPointer(); SmartPointer(SmartPointer<T>& sptr); T* operator->(); T& operator*(); SmartPointer<T>& operator=(SmartPointer<T>& sptr); T getValue(); protected: T* ref; unsigned* ref_count; }; template<typename T> SmartPointer<T>::SmartPointer(T* ptr){ ref = ptr; ref_count = (unsigned*)malloc(sizeof(unsigned)); *ref_count = 1; } template<typename T> SmartPointer<T>::~SmartPointer(){ --*ref_count; if (*ref_count == 0) { delete ref; free(ref_count); ref = NULL; ref_count = NULL; } } template<typename T> SmartPointer<T>::SmartPointer(SmartPointer<T>& sptr) { ref = sptr.ref; ref_count = sptr.ref_count; ++(*ref_count); } template<typename T> T* SmartPointer<T>::operator->() { return ref; } template<typename T> T& SmartPointer<T>::operator*() { return *ref; } template<typename T> SmartPointer<T>& SmartPointer<T>::operator=(SmartPointer<T>& sptr){ if (this != &sptr) { ref = sptr.ref; ref_count = sptr.ref_count; ++(*ref_count); } return *this; } template<typename T> T getValue() { return *ref; } #endif//person.h
#ifndef PERSON_H #define PERSON_H #include <string> #include <iostream> using namespace std; class person { public: person(string name); ~person(void); void tell(); private: string name; }; #endif
//person.cpp
#include "person.h" person::person(string name):name(name){ } void person::tell(){ cout << "Hi! I am " << name << endl; } person::~person(){ cout << "Bye!" << endl; }
另外一篇文章关于内部链接和外部连接的解释:
http://www.cnblogs.com/magicsoar/p/3840682.html
内部链接与外部链接
那么什么内部链接和外部链接又是什么呢?
我们知道C++中声明和定义是可以分开的
例如在vs中,我们可以一个函数声明定义放在b.cpp中,在a.cpp只需再声明一下这个函数,就可以在a.cpp中使用这个函数了
a.cpp
void show(); int main() { show(); return 0; }
b.cpp
#include <iostream> void show() { std::cout << "Hello" << std::endl; }
而通过之前的了解,我们知道每个编译单元间是相互独立不知道彼此的存在的
那么a.cpp又是如何知道show函数的定义的呢
其实在编译一个编译单元(.cpp)生成相应的obj文件过程中
编译器会将分析这个编译单元(.cpp)
将其所能提供给其他编译单元(.cpp)使用的函数,变量定义记录下来。
而将自己缺少的函数,变量的定义也记录下来。
所以可以认为a.obj和b.obj记录了以下的信息
然后在链接器连接的时候就会知道a.obj需要show函数定义,而b.obj中恰好提供了show函数的定义,通过链接,在最终的可执行文件中我们能看到show函数的运行
哪这些又和内部链接,外部链接有什么关系呢?
那些编译单元(.cpp)中能向其他编译单元(.cpp)展示,提供其定义,让其他编译单元(.cpp)使用的的函数,变量就是外部链接,例如全局变量
而那些编译单元(.cpp)中不能向其他编译单元(.cpp)展示,提供其定义的函数,变量就是内部链接,例如static函数,inline函数等
好了让我们看下编译单元,内部链接和外部链接比较正式的定义吧
编译单元:当一个c或cpp文件在编译时,预处理器首先递归包含头文件,形成一个含有所有 必要信息的单个源文件,这个源文件就是一个编译单元。
内部连接:如果一个名称对编译单元(.cpp)来说是局部的,在链接的时候其他的编译单元无法链接到它且不会与其它编译单元(.cpp)中的同样的名称相冲突。
外部连接:如果一个名称对编译单元(.cpp)来说不是局部的,而在链接的时候其他的编译单元可以访问它,也就是说它可以和别的编译单元交互。
----------------------------------------------------------------------------------------------------------------------------------------
3. 使用下面两种方式防止重复include:
#ifndef PERSON_H
#define PERSON_H
#endif
或者
#pragma once
4. 给出在定义类内部可用常量,文件作用域常量,全局常量的写法:
a. 类成员变量是无法在成员变量定义的时候初始化的(除非是const static),因此在这个时候,成员变量初始化列表是const变量初始化的唯一机会了。。。写成
class MyClass {
private:
const int a1 = 3;
const char* s1 = "abc";
}
是大错而且特错的!!!!同时注意只能在构造函数的初始化列表里初始化
b. 定义全局变量时,extern int a; 只是声明,并没有定义,但是extern int a = 3却是在定义;当然可以在a.cpp中 extern int a = 3; 在b.cpp 中extern int a;来声明。但是比较规范的做法是可以把extern int a; 扔到b.cpp 的头文件b.h中,在b.cpp中只是定义int a = 3; 其他文件用的时候只是#include "b.h"即可。
c. static const int a = 3; 要写在.cpp中,因为只在这个文件中使用,.h文件是供其他人include用的:
因此正确的代码是:
//main.cpp
//main.cpp #include"MyClass.h" #include<iostream> using namespace std; int main() { MyClass myClass(30,"abc"); cout << "a2 = " << a2 << endl; cout << "s2 = " << s2 << endl; return 0; }
//MyClass.cpp
//MyClass.cpp #include "MyClass.h" #include <iostream> using namespace std; const int a2 = 2; const char* const s2 = "s2"; static const int a3 = 3; static const char* const s3 = "s3"; MyClass::MyClass(const int a = 30, const char* const s = "abc"):a1(a),s1(s){ cout << "a3 = " << a3 << endl; cout << "s3 = " << s3 << endl; }
//MyClass.h
//MyClass.h #ifndef MYCLASS_H #define MYCLASS_H extern const int a2; extern const char* const s2; class MyClass { private: const int a1; const char* const s1; public: MyClass(const int a, const char* const s); }; #endif
5. 内联函数一定要写在头文件里:
inline函数的特征是在调用的地方插入相应函数的代码,所以编译之后的目标文件里是没有inline函数体的,因为在要调用的地方它都已经用相应的语句替换掉了(当然这只限于内联成功的情况)。
如果我们将inline函数写在cpp文件里,但是绝大多数情况下,在我们用第三方类库的时候,我们只有头文件和目标文件(没有cpp文件),当你调用那个内联函数时,编译器没办法找到它。所以说将inline函数写在cpp文件中是没什么用的
6. 最后附上一篇const, static等不同变量初始化的日志,引以为戒:
http://blog.csdn.net/gljseu/article/details/9750877
1、普通的变量:一般不考虑啥效率的情况下 可以在构造函数中进行赋值。考虑一下效率的可以再构造函数的初始化列表中进行。
class CA
{
public:
int data;
……
public:
CA();
……
};
CA::CA():data(0)//……#1……初始化列表方式
{
//data = http://www.mamicode.com/0;//……#1……赋值方式
};
2、static 静态变量:
static变量属于类所有,而不属于类的对象,因此不管类被实例化了多少个对象,该变量都只有一个。在这种性质上理解,有点类似于全局变量的唯一性。
class CA
{
public:
static int sum;
……
public:
CA();
……
};
int CA::sum=0;//……#2……类外进行初始化
3、const 常量变量:
const常量需要在声明的时候即初始化。因此需要在变量创建的时候进行初始化。一般采用在构造函数的初始化列表中进行。
class CA
{
public:
const int max;
……
public:
CA();
……
};
CA::CA():max(100)
{
……
}
4、Reference 引用型变量:
引用型变量和const变量类似。需要在创建的时候即进行初始化。也是在初始化列表中进行。但需要注意用Reference类型。
class CA
{
public:
int init;
int& counter;
……
public:
CA();
……
};
CA::CA():counter(&init)
{
……
}
5、const static integral 变量:
对于既是const又是static 而且还是整形变量,C++是给予特权的(但是不同的编译器可能有会有不同的支持,VC 6好像就不支持)。可以直接在类的定义中初始化。short可以,但float的不可以哦。
// 例float类型只能在类外进行初始化
// const float CA::fmin = 3.14;
class CA
{
public:
//static const float fmin = 0.0;// only static const integral data members can be initialized within a class
const static int nmin = 0;
……
public:
……
};
总结起来,可以初始化的情况有如下四个地方:
1、在类的定义中进行的,只有const 且 static 且 integral 的变量。
2、在类的构造函数初始化列表中, 包括const对象和Reference对象。
3、在类的定义之外初始化的,包括static变量。因为它是属于类的唯一变量。
4、普通的变量可以在构造函数的内部,通过赋值方式进行。当然这样效率不高。
类的定义体中只能初始化const integral data型的量。对于static型的量,那就放在.cpp文件中吧!当然了,还不能放在成员函数中(非静态成员函数可以使用静态数据成员的吧! 静态成员函数只能调用静态数据成员。),因为static量是类的,不是某个对象的。那样的话每个对象都来操作属于所有对象(类)的东西,岂不是会乱套,所以不能允许这种行为。
但是,static量可以在类的构造函数中赋值,当然是不可以放在初始化成员列表中的,可是在构造函数中赋值时不可以使用copy construction,提示这样的错误:
term does not evaluate to a function taking 1 arguments
那么,对于类里面的static函数的声明和定义是这样的:
static函数的声明可以像普通成员函数一样声明,只是在前面加上一个static关键字。
如:
private:
static int GetXYZ();
而在,定义时,像static变量那样,也是不可以加上static关键字,若写成:
static int A::GetXYZ()
{
…………
}
就会提示:
‘static‘ should not be used on member functions defined at file scope
所以应该写成是这样:
int A::GetXYZ()
{//他是static型函数的性质,就用其他方法来辨别吧,比如在这儿写上:this is a static function
…………
}
至于static函数的使用,可以再你所编写的代码段中这样插入:
………………
A::GetXYZ(); //可以看出他是类的东东,不是对象的
………………
当然,对于public型的static量(假设叫CString S_str),可以这样使用:
A::S_str = "Hello !";
CString str = A::S_str;
c++成员变量初始化问题 分类: c/c 小结 2009-11-03 17:19
C++为类中提供类成员的初始化列表
类对象的构造顺序是这样的:
1.分配内存,调用构造函数时,隐式/显示的初始化各数据成员
2.进入构造函数后在构造函数中执行一般计算
1.类里面的任何成员变量在定义时是不能初始化的。
2.一般的数据成员可以在构造函数中初始化。
3.const数据成员必须在构造函数的初始化列表中初始化。
4.static要在类的定义外面初始化。
5.数组成员是不能在初始化列表里初始化的。
6.不能给数组指定明显的初始化。
这6条一起,说明了一个问题:C++里面是不能定义常量数组的!因为3和5的矛盾。这个事情似乎说不过去啊?没有办法,我只好转而求助于静态数据成员。
到此,我的问题解决。但是我还想趁机复习一下C++类的初始化:
1.初始化列表:CSomeClass::CSomeClass() : x(0), y(1){}
2.类外初始化:int CSomeClass::myVar=3;
3.const常量定义必须初始化,C++类里面使用初始化列表;
4.C++类不能定义常量数组。