类与命名空间
<<C++primer>>12.1.4中讲到:
在一个给定的源文件中,一个类只能被定义一次。如果在多个文件中定义一个类,那么每个文件中的定义必须是完全相同的。
解析:定义一个类,指的是定义同一个类。如果名字相同,实现不同,那属于不同的类。
由此我们容易想到以下问题:
(1) 此处类的定义指什么?若是普通变量的定义,那是不可以在多个文件中同时定义的,只能在一处定义,多处声明,否则会发生重定义。但为何类的定义却可以,值得斟酌。
(2) 如果实现不同,名字相同会出现什么情况呢?
(3) 用include和不用include又有何不同?
(4) 头文件为.h和不为.h又如何呢?
1 实例一
//car1.h
#include<iostream>
class car{
public:
car(){std::cout<<"car1:"<<std::endl;}
car(int v,intc):value(v),capacity(c)
{std::cout<<"car1:"<<std::endl;}
void show()
{
std::cout<<"show car1:"<<std::endl;
std::cout<<"value:"<<value<<"
capacity:"<<capacity<<std::endl;
}
int GetV(){return value;}
private:
int value;
int capacity;
};
|
//car2.h
#include<iostream>
class car{
public:
car(){std::cout<<"car1:"<<std::endl;}
car(int v,intc):value(v),capacity(c)
{std::cout<<"car1:"<<std::endl;}
void show()
{
std::cout<<"show car1:"<<std::endl;
std::cout<<"value:"<<value<<"
capacity:"<<capacity<<std::endl;
}
int GetV(){return value;}
private:
int value;
int capacity;
};
|
//namespace.cpp
#include"car1.h"
#include"car2.h"
//很奇怪,没有出现重定义,我们可以猜测其实这里说的类定义其实就是类似于结构体声明。多次声明不会出现任何问题。只是c才是变量的实例化。声明类并不分配实实在在的空间。
int main()
{
car c(100,10);
c.show();
std::cout<<c.GetV()<<std::endl;
return 0;
}
|
由以上的小测试程序我们基本可以得出:类的定义实际上是一种声明活动,并没有空间的分配。
上面头文件中只是类里有内联函数,我们知道内联函数定义在头文件不会引起重定义,那么我们试着实现一个非内联函数:
int GetC();//在类内部的声明
int car::GetC()
{
return capacity;
}
|
结果我们惊奇的发现,结果仍然正常运行。这是否说明类的成员函数也必须在实例化对象后才会实例化呢,这样每一个对象会维护一个成员函数的副本?
为了验证这个问题,我们用一个静态函数来测试:
static void test();
void car::test()
{
std::cout<<“for test!“<<std::endl;
}
|
结果仍然好使,这说明类的整个定义过程都视为声明过程,不会出现重定义错误。
保险起见,我们在.h中再尝试一个全局函数:
void test2()
{
std::cout<<"outer Func!"<<std::endl;
}
|
结果仍然编译运行正常,无语真把我逼急了,声明一个全局变量int a=10;结果还是正常,但是如果两次包含carx.h(x=1 or 2)就会出现重定义错误。后来尝试把car2.h中的输出部分car1字符串改为car2,发现car2.h覆盖了car1.h。这是什么原因呢,是C++对C头文件兼容的一些额外处理吗?不懂
我们再来测试一下C++ 标准头文件格式(直接把后缀名.h去掉即可):结果惊人的发现:
包括类名在内的所有命名都发生了重定义行为。
这是否说明C++的类定义实际不是一种类声明行为呢,不尽然。我们可以在namespace.cpp文件中尝试写两条extern int a =10一样的语句,结果同却不会有重定义错误。这样我们可以得出一个初步的猜测:
C++中,类的定义 并不是一种声明行为,而是一种定义行为,所以在同一作用域内是不可以定义多次的。
然后我们只让namespace.cpp包含一个头文件,然后执行下面命令:g++ namespace.cpp car1 car2
报错:car1:file not recognized: File format not recognized
collect2: ld returned 1 exitstatus
说明该命令中的car1 car2无法识别,那么改为.cpp,同时在两个文件的类中都加入一个静态内联函数:
static void view()
{
std::cout<<"inline static!"<<std::endl;
}
|
然后执行:g++namespace.cpp car1.cpp car2.cpp
此时出现重定义的函数只有:
GetC(),test()----都在类外部实现
test2()-----全局函数
由此可得出本文最开始列出的那句书上的话具有一定的局限性,你可以在不同的文件中定义同一个类,但该类决不可以有类外实现的函数,否则会出现重定义。
推荐:C++中类的定义和实现须放在不同的文件中,类体应该放在头文件中。但外部实现的成员函数应该放在对应的.cpp中,这是最佳实践,避免不必要的错误。
这里似乎有一个困惑的地方,那就是既然类的定义并非声明动作,那么为何在不同文件中定义链接时不会出现重定义呢,而它在外部实现的成员函数却出现重定义呢?
解释:
(1) C++中类的代码区属于文件作用域,类似于全局static数据。
(2) C++中类外实现的函数属于全局作用域,类似全局的非static成员,具有external属性。
(3) 再次强调,类的定义是一种定义行为。
在不同源文件中定义同一个类,但实质上这两个类没有内在的联系,改变其中一个不会影响另外一个,两个类只不过是恰好特征完全一样罢了,就好比牛顿和莱布尼茨尽管都发明了微积分,但它们谁了没抄谁的,谁也不受水的约束。所以这样的设计类的方法是糟糕的,我们并不提倡。
其实上述重定义问题,在C++中是完全有方法避免的,那就是命名空间,如下代码所示:
//car1
#include<iostream>
namespace nsp{
class car{
public:
car(){std::cout<<"car1:"<<std::endl;}
car(int v,int c):value(v),capacity(c){std::cout<<"car1:"<<std::endl;}
void show()
{
std::cout<<"show car1:"<<std::endl;
std::cout<<"value:"<<value<<" capacity:"<<capacity<<std::endl;
}
int GetV(){return value;}
int GetC();
static void test();
static void view()
{
std::cout<<"inline static!"<<std::endl;
}
friend void brother(car &c)
{
std::cout<<"inner brother!"<<std::endl;
}
friend void sister();
private:
int value;
int capacity;
};
void sister()
{
std::cout<<"outer sister!"<<std::endl;
}
int car::GetC()
{
return capacity;
}
void car::test()
{
std::cout<<"for test!"<<std::endl;
}
void test2()
{
std::cout<<"outer Func!"<<std::endl;
}
}
|
//car2
#include<iostream>
namespace liao{
class car{
public:
car(){std::cout<<"car2:"<<std::endl;}
car(int v,int c):value(v),capacity(c){std::cout<<"car2:"<<std::endl;}
void show()
{
std::cout<<"show car2:"<<std::endl;
std::cout<<"value:"<<value<<" capacity:"<<capacity<<std::endl;
}
int GetV(){return value;}
int GetC();
static void test();
static void view()
{
std::cout<<"inline static!"<<std::endl;
}
friend void brother(car& c)
{
std::cout<<"inner brother!"<<std::endl;
}
friend void sister();
private:
int value;
int capacity;
};
void sister()
{
std::cout<<"outer sister!"<<std::endl;
}
int car::GetC()
{
return capacity;
}
void car::test()
{
std::cout<<"for test!"<<std::endl;
}
void test2()
{
std::cout<<"outer Func!"<<std::endl;
}
}
|
//namespace.cpp
#include"car1"
#include"car2"
using namespace std;
class A{
public:
A(int i):a(i){}
friend void f(){} //can’t call(无法调用)
friend void f(A& obj)
{
//cout<<obj.a;
}
friend ostream& operator<<(ostream& os,A& obj)
{
os<<obj.a;
}
private:
int a;
};
int main()
{
cout<<"***********car1*************"<<endl;
nsp::car c(100,10);
c.show();
std::cout<<c.GetV()<<std::endl;
std::cout<<c.GetC()<<std::endl;
nsp::car::test();
nsp::car::view();
brother(c);
//nsp::brother(c);
nsp::sister();
nsp::test2();
cout<<"***********car2*************"<<endl;
liao::car b(100,10);
b.show();
std::cout<<b.GetV()<<std::endl;
std::cout<<b.GetC()<<std::endl;
liao::car::test();
liao::car::view();
brother(b);
//liao::brother(b);
liao::sister();
liao::test2();
cout<<"***********test for friend Func*************"<<endl;
A a(3);
cout<<a<<endl;
//f();
f(a);
return 0;
}
|
输出:
***********car1*************
car1:
show car1:
value:100 capacity:10
100
10
for test!
inline static!
inner brother!
outer sister!
outer Func!
***********car2*************
car2:
show car2:
value:100 capacity:10
100
10
for test!
inline static!
inner brother!
outer sister!
outer Func!
***********test for friend Func*************
3
|
通过两个命名空间我们就可以区分两个同名的类,把它们的作用域局限在某个空间中,这样就可以防止重定义的行为。道理非常简单不肖多说。
有一个值得说明的发现,这个也与上面得出的结论有关。那就包括友元函数在内的成员函数如果在类内部定义则只有文件作用域,在外部定义的具有全局作用域。此处的全局作用域是指链接属性,访问时还必须加上空间名及作用域操作符。
友元函数在类内部实现有点特殊,如本例中,brother()不直接属于空间nsp和liao而且属于类域构成的空间中,可是友元函数却不是类的一个成员,这就不可以直接用对象调用对象.函数名(),也不能使用类名::函数名()那么怎么调用呢,只能传递一个该类类型的实参,用这个实参来定位函数所在的类域。
但是如果f的形参不是A类型的行不行呢?当然行,但必须具备有效的从A到f形参类型的转换,例如:
#include <iostream>
class A
{
public:
friend void f( int a ){ } operator int( ){ return a; }
private:
int a;
};
int main( void )
{
A a;
f( a );
return 0;
}
|
小结:友元函数的调用(包括显示声明为inline的函数)
(1) 若在外部实现,内部声明,链接属性为external。调用形式为空间名::函数名();
(2) 若在内部声明,内部实现,链接属性为internal。
1) 若参数无本类类型,也不能通过隐式类型转换接受本类类型的参数。那么无法调用
2) 反之,则直接用函数名调用(传递正确参数即可)。
关于类与命名空间的问题就到这了,遇到问题再说!
命名空间:命名空间祥解