C++中存在的头文件类/函数完整实现

在Tecent/Mars开源工程中我们用到它Comm模块部分的代码,其中这部分代码都是一些C++头文件实现代码。同时它的类命名也未尝有命名空间,这里对此产生了两个疑惑:
1.在头文件中实现一个无命名空间的类会有命名冲突吗?
2.这种写法的原理是什么?

Tecent/Mars工程

在查阅了一些资料后认为应该是跟C++中头文件进行类/函数定义有关,接下来就学习一下相关的概念。

内联函数的基本含义

在计算机科学中,内联函数(有时称作在线函数或编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展)

也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。

但在选择使用内联函数时,必须在程序占用空间和程序执行效率之间进行权衡,因为过多的比较复杂的函数进行内联扩展将带来很大的存储资源开支。另外还需要特别注意的是对递归函数的内联扩展可能引起部分编译器的无穷编译。

显然内联函数是一种针对编译器的优化,它将函数体在编译阶段展开到栈中从而提高了调用效率。


内联函数与宏定义


内联函数的缺点

C++中内联函数声明方式

可能很多开发者不知道,inline只是对编译器的一个请求而非命令。该请求可以隐式地进行也可以显式地声明。

当你的函数较复杂(比如有循环、递归),或者是虚函数时,编译器很可能会拒绝把它inline。因为虚函数调用只有运行时才能决定调用哪个,而inline是在编译器便要嵌入函数体。 有些编译器在dianotics级别编译时,会对拒绝inline给出warning。

隐式的办法便是把函数定义放在类的定义中:

1
2
3
4
class Person{
...
int age() const{ return _age;} // 这会生成一个inline函数!
};

例子中是成员函数,如果是友元函数也是一样的。除非友元函数定义在类的外面。

显式的声明则是使用inline限定符:

1
2
template<typename T>
inline const T& max(const T& a, const T& b){ return a<b ? b: a;}

Effective C++(内联函数)

static限定符

C语言中的static

static修饰的变量作用是修改它的生命期/作用域。
1.对于变量来说,它们的生命期都提升到程序执行前申请到程序结束才释放。
2.而全局变量的作用域默认为文件内只有用external才能让外部可见,局部变量的作用域依然是代码块内。

static修饰的函数,它起到修改限定这个函数的作用域表示这个函数只能在本文件中使用。

C++中的static

对比C语言中的特性它略有不同。添加了static的变量称作这个类的静态数据成员。
1.它不能再类中定义和初始化,只能在类中声明,在类外进行定义和初始化,默认初始化为0。
2.生命期都提升到程序执行前申请到程序结束才释放,所有类的实例都共享这个静态变量内存区。

static修饰的函数称作这个类的静态成员函数。
1.在类外定义静态成员函数时,不用再加static关键字,只要在类中声明时加上即可。
2.静态成员函数只能访问静态数据成员和静态成员函数,普通成员函数可以访问静态成员函数和静态数据成员。
3.静态成员函数属于类,不属于任意一个类对象。
4.静态成员函数没有this指针。

在C++头文件中的直接进行类/函数定义

这里我们来学习一下具体的示例

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
#ifdef __APPLE__
#include <libkern/OSAtomic.h>

#define splock OSSpinLock
#define splockinit(lock) \
{ *lock = OS_SPINLOCK_INIT; }
#define splocklock OSSpinLockLock
#define splockunlock OSSpinLockUnlock
#define splocktrylock OSSpinLockTry

class SpinLock
{
public:
typedef splock handle_type;

public:
SpinLock() { splockinit(&lock_); }

bool lock() {
splocklock(&lock_);
return true;
}

bool unlock() {
splockunlock(&lock_);
return true;
}

bool trylock() {
return splocktrylock(&lock_);
}

splock *internal() { return &lock_; }

private:
SpinLock(const SpinLock &);
SpinLock &operator=(const SpinLock &);

private:
splock lock_;
};

可以看到这里定义了一个SpinLock类并依赖于系统的splock具体内部实现,这种写法可以看做是对splock的代码扩展。

这种写法有副作用吗?

这里我们使用一个简单的示例来看有没有问题。
Header_Imp类:

1
2
3
4
5
6
7
8
#ifndef Header_Imp_h
#define Header_Imp_h
#include <stdio.h>
class HeaderImpTest {
public:
void func() { printf("我是头文件中_______HeaderImpTest类的func实现\n"); };
};
#endif /* Header_Imp_h */

Implementation类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//.hpp
#ifndef Implementation_hpp
#define Implementation_hpp
#include <stdio.h>
class Test {
public:
void func();
};
#endif /* Implementation_hpp */

// .cpp
#include "Implementation.hpp"
class HeaderImpTest {
public:
void func() { printf("我是实现文件___0___中HeaderImpTest类的func实现\n"); };
};

void Test::func() {
HeaderImpTest *test = new HeaderImpTest();
test->func();
};

Implementation1类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef Implementation1_hpp
#define Implementation1_hpp
#include <stdio.h>
class Test1 {
public:
void func();
};
#endif /* Implementation1_hpp */

#include "Implementation1.hpp"
class HeaderImpTest {
public:
void func() { printf("我是实现文件___1___中HeaderImpTest类的func实现\n"); };
};

void Test1::func() {
HeaderImpTest *test = new HeaderImpTest();
test->func();
};

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "Header_Imp.h"
#include "Implementation.hpp"
#include "Implementation1.hpp"

int main () {
HeaderImpTest *test = new HeaderImpTest();
test->func();

Test *test1 = new Test();
test1->func();

Test1 *test2 = new Test1();
test2->func();
return 0;
}

这里我们在三个文件中分别定义了HeaderImpTest这个类,然后在main.cpp中实例化三个对象

最终会发现在编译阶段并不会提示错误,但是在运行时期会使用先编入的那个实现。这就造成了副作用,即这种没有添加命名空间的类在运行时才能发现问题。

当我们给HeaderImpTest加上namespace之后就正常了。

之后又做了一个C语言中inline函数无命名前缀然后再另一个文件定义同样符号的实验

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
inline void testFunc() {
printf("我是头文件中_______testFunc\n");
}

class HeaderImpTest {
public:
void func() {
printf("我是头文件中_______HeaderImpTest类的func实现\n");
testFunc();
};
};

namespace Implementation {
class HeaderImpTest;
}
using namespace Implementation;

void testFunc() {
printf("我是实现文件___0___中testFunc\n");
}

class Implementation::HeaderImpTest {
public:
void func() { printf("我是实现文件___0___中HeaderImpTest类的func实现\n"); };
};

void Test::func() {
HeaderImpTest *test = new HeaderImpTest();
test->func();
testFunc();
};

发现结论是一致的,在编译时并不会报错,但是头文件的testFunc()函数的实现变成了实现文件中testFunc()函数的结果,而同时加入在另一个实现文件中定义testFunc()函数则会编译失败。

所以直接在头文件中定义类/函数实现假如未处理它们的命名则有可能产生严重的副作用,且只能等到运行时才会发现问题。

C语言中命名冲突解决
C++中命名冲突
另一个相似的示例
内联函数细节

总结

对于一开始提出的两个疑问,这里可以进行比较明确的回答。

第一,在头文件中实现一个类/函数且不加命名空间,这种写法确实不会导致命名冲突因为在编译阶段编译器只会拿多个同名类中最先编入的那个类的实现,而在编写阶段假设我们同时将两个类暴露则会检测出类名冲突。同时这种写法是不安全的,因为在运行的时候假设有多个同名类且无命名空间则会导致真正实现是不正确的。

第二,直接在头文件中定义类/函数应该是一种编码风格,我们可以为每个类加上前置命名空间就不会出现第一个问题了。