我正在开发一个对象和函数库,并且有一个头文件,这里名为super.hpp
,它包含一些初始化任务。
super.hpp
#ifndef H_INIT
#define H_INIT
#include <iostream>
#include <string>
static bool isInit = false;
struct settings_struct{
std::string path = "foo";
void load(){ path = "bar"; }
};
struct initializer_struct{
settings_struct settings;
initializer_struct(){
if(!isInit){
std::cout << "Doing initialization\n";
settings.load();
isInit = true;
}
// settings.load();
}//====================
~initializer_struct(){
if(isInit){
std::cout << "Doing closing ops\n";
isInit = false;
}
}
};
static initializer_struct init; // static declaration: only create one!
#endif
我对此标题的意图是创建initializer_struct
对象一次;构造时,此结构执行一些操作,为整个库设置标志和设置。其中一个操作是创建从XML文件加载设置的设置结构;构造init结构时,此操作也应仅发生一次,因此变量(此处为path
)将从设置文件中保存。 super.hpp
标头包含在库中的所有对象中,因为不同的对象以不同的容量使用,即,无法预测应用程序中将使用哪些对象,因此我包含{super.hpp
标头所有这些中都有1}}标题,以保证无论使用哪个对象都会调用它。
我的问题是这样的:当我在主应用程序加载的多个类/对象中包含super.hpp
时,结构init
似乎被重新初始化并且变量在{构造的{1}}将被默认值覆盖。要查看此操作,请考虑以下附加文件:
TEST.CPP
settings_struct
classA.hpp
#include "classA.hpp"
#include "classB.hpp"
#include <iostream>
int main(int argc, char *argv[]){
(void) argc;
(void) argv;
classA a;
classB b;
std::cout << "Settings path = " << init.settings.path << std::endl;
std::cout << "Class A Number = " << a.getNumber() << std::endl;
std::cout << "Class B Number = " << b.getInteger() << std::endl;
}
classA.cpp
#ifndef H_CLASSA
#define H_CLASSA
class classA{
private:
double number;
public:
classA() : number(7) {}
double getNumber();
};
#endif
classB.hpp
#include "super.hpp"
#include "classA.hpp"
double classA::getNumber(){ return number; }
classB.cpp
#ifndef H_CLASSB
#define H_CLASSB
class classB{
private:
int number;
public:
classB() : number(3) {}
int getInteger();
};
#endif
编译并运行示例
#include "super.hpp"
#include "classB.hpp"
int classB::getInteger(){ return number; }
我希望test.out的输出如下:
g++ -std=c++11 -W -Wall -Wextra -Weffc++ -pedantic -c classA.cpp -o classA.o
g++ -std=c++11 -W -Wall -Wextra -Weffc++ -pedantic -c classB.cpp -o classB.o
g++ -std=c++11 -W -Wall -Wextra -Weffc++ -pedantic classA.o classB.o test.cpp -o test.out
./test.out
然而,当我运行它时,我得到&#34;设置路径= foo&#34;。因此,我的结论是Doing initialization
Settings path = bar
Number = 7
Doing closing ops
,initializer_struct
不止一次被构建。第一次,布尔值init
为false,设置结构isInit
函数将load
设置为&#34; bar。&#34;对于所有后续初始化,path
为真,因此不会再次调用isInit
函数,并且似乎来自未初始化load
(即settings
)的变量值会覆盖先前加载的值,因此path = "foo"
中init.settings.path
的输出。
这是为什么?为什么每次包含标题时都会构造test.cpp
对象?我原以为包含警卫会使标题代码多次被调用。如果我在init
中将init
变量设为非静态变量,则会创建多个副本,并且输出会打印多次#34;进行初始化&#34;和&#34;做结束操作。&#34;另外,如果我在test.hpp
构造函数中的条件语句之外取消注释settings.load()
函数调用,那么输出将提供&#34; bar&#34;的设置路径。最后,从initializer_struct()
中删除包含super.hpp
会导致路径值为&#34; bar&#34;,进一步支持我的假设,即classA.cpp
的多个包含会导致多个构造函数调用
我想避免使用test.hpp
super.hpp` - 这就是我将命令放在条件语句中的原因。有什么想法吗?如何确保设置文件只读一次并且加载的值不会被覆盖?这是一个完全钝的方法来设置我的库使用的一些标志和设置吗?如果是这样,您是否有任何建议可以使流程更简单和/或更优雅?
谢谢!
编辑:更新为包含两个对象类,以更接近地表示我更复杂的设置
答案 0 :(得分:2)
在头文件中,您可以定义这些static
个全局对象:
static bool isInit = false;
static initializer_struct init;
这些static
个全局对象在包含此头文件的每个翻译单元中实例化。您将在每个翻译单元中获得这两个对象的副本。
initializer_struct(){
但是,这个构造函数只会在您的应用程序中定义一次。编译器实际上将在包含这些头文件的每个翻译单元中编译构造函数,并且在每个翻译单元中,构造函数将使用其翻译单元中的static
全局对象。
但是,当您链接应用程序时,将删除所有翻译单元中的重复构造函数,并且只有一个构造函数实例将成为最终可执行文件的一部分。未指定哪些重复实例将被删除。链接器将选择其中一个,这是幸运的赢家。无论构造函数的实例是什么,它都只使用来自其自己的翻译单元的static
个全局对象。
有一种方法可以正确执行此操作:将static
全局对象声明为static
类成员,然后在其中一个转换单元中实例化那些static
个全局对象。使用main()
的翻译单元是一个很好的选择。然后,将会有一个副本。
答案 1 :(得分:0)
根据Varshavchik的建议,我做了一些改动。首先,我已将super.hpp
标头替换为一个非常基本的类,我的库中的所有对象都可以扩展:
<强> base.hpp 强>
#ifndef H_BASE
#define H_BASE
#include <iostream>
#include <string>
struct settings_struct{
settings_struct(){
std::cout << "Constructing settings_struct\n";
}
std::string path = "foo";
void load(){ path = "bar"; }
};
struct initializer_struct{
settings_struct settings;
initializer_struct(){
std::cout << "Constructing initializer_struct\n";
}
~initializer_struct(){
std::cout << "Doing closing ops\n";
}
void initialize(){
std::cout << "Doing initialization\n";
settings.load();
}
};
class base{
public:
static initializer_struct init;
static bool isInit;
base();
};
#endif
<强> base.cpp 强>
#include "base.hpp"
initializer_struct base::init;
bool base::isInit = false;
base::base(){
if(!isInit){
init.initialize();
isInit = true;
}
}
其他文件或多或少保持不变,只有一些变化。首先,classA
和classB
都扩展了base
类:
class classA : public base {...}
class classB : public base {...}
现在,当构造任何对象时,将调用基类构造函数,该构造函数初始化设置和其他变量一次。 isInit
和init
都是base
类的静态成员,因此包含base
标头或扩展base
对象的所有对象都可以访问其值,这符合我的需要。可以通过
base::init.settings.path
输出现在是我所期望的,并希望它是Settings path = bar
而不是&#34; foo&#34;
答案 2 :(得分:0)
您几乎拥有它,只需将静态isInit移动到您的类的静态成员,并将init的实例化移动到翻译单元。这将是生成的文件:
<强> super.hpp 强>
#ifndef H_INIT
#define H_INIT
#include <iostream>
#include <string>
struct initializer_struct{
static bool isInit;
struct settings_struct{
std::string path = "foo";
void load(){ path = "bar"; }
} settings;
initializer_struct(){
if(!isInit){
std::cout << "Doing initialization\n";
settings.load();
isInit = true;
}
// settings.load();
}//====================
~initializer_struct(){
if(isInit){
std::cout << "Doing closing ops\n";
isInit = false;
}
}
};
extern initializer_struct init; // extern declaration, instantiate it in super.cpp
#endif
<强> super.cpp 强>
#include "super.hpp"
bool initializer_struct::isInit = false;
initializer_struct init;
但是你最好使用单例模式。使用单例模式,您可以确保只实例化一个类的实例。您可以在此处获取一些信息:C++ Singleton design pattern
看起来像这样:
<强> singleton.hpp 强>
#pragma once
class initializer_struct{
public:
struct settings_struct{
std::string path = "foo";
void load(){ path = "bar"; }
} settings;
static initializer_struct *GetInstance() {
if (_instance == NULL) {
_instance = new initializer_struct();
}
return _instance;
}
~initializer_struct(){
}
private:
initializer_struct(){
if(!isInit){
std::cout << "Doing initialization\n";
settings.load();
isInit = true;
}
}
static initializer_struct *_instance;
}
<强> singleton.cpp 强>
#include "singleton.hpp"
initializer_struct *initializer_struct::_instance = NULL;
您甚至可以通过将_instance从指针更改为只是一个对象来进行加载初始化,将其声明为singleton.cpp中的对象并将GetInstance()原型更改为:
initializer_struct &GetInstance() { return _instance; }
然而,后者要注意静态初始化顺序fiasco(http://yosefk.com/c++fqa/ctors.html#fqa-10.12)。简而言之,如果您的类初始化不依赖于另一个类初始化,则可以执行后一种方法,因为您不知道将首先初始化哪一个。