这是一个似乎没有一个真正答案的广泛问题。
我很长时间以来对组合对象的初始化感到困惑。我已经被正式教导为所有成员数据提供getter和setter,并且支持对象的原始指针而不是自动对象 - 这似乎与Stack Overflow上的许多人(例如this热门帖子)建议形成对比。
那么,我应该如何初始化对象组合对象?
这是我尝试使用我在学校学到的东西进行初始化的方法:
class SmallObject1 {
public:
SmallObject1() {};
};
class SmallObject2 {
public:
SmallObject2() {};
};
class BigObject {
private:
SmallObject1 *obj1;
SmallObject2 *obj2;
int field1;
int field2;
public:
BigObject() {}
BigObject(SmallObject1* obj1, SmallObject2* obj2, int field1, int field2) {
// Assign values as you would expect
}
~BigObject() {
delete obj1;
delete obj2;
}
// Apply getters and setters for ALL members here
};
int main() {
// Create data for BigObject object
SmallObject1 *obj1 = new SmallObject1();
SmallObject2 *obj2 = new SmallObject2();
int field1 = 1;
int field2 = 2;
// Using setters
BigObject *bobj1 = new BigObject();
// Set obj1, obj2, field1, field2 using setters
// Using overloaded contructor
BigObject *bobj2 = new BigObject(obj1, obj2, field1, field2);
return 0;
}
这个设计很有吸引力,因为它对我来说是可读的。 BigObject
指向其成员对象的事实使得初始化后初始化obj1
和obj2
成为可能。然而,动态内存可能使程序更加复杂并且在路上混乱,因此内存泄漏成熟。此外,使用getter和setter会使类混乱,也可能使成员数据太容易访问和变异。
这实际上是不好的做法吗?我经常发现需要将成员对象与其所有者分开初始化的时间,这使得自动对象没有吸引力。另外,我考虑过让更大的对象构造自己的成员对象。从安全的角度来看,这似乎更有意义,但从客观责任的角度来看则更少。
答案 0 :(得分:0)
我已经被正式教导为所有成员数据提供getter和setter,并支持原始指针而不是自动对象
不幸的是,你被教导错了。
绝对没有理由赞成原始指针而不是@Bean
@JobScope
public FlatFileItemReader<?> yourReaderBean(
@Value("#{jobParameters[filename]}") String filename){
FlatFileItemReader<?> itemReader = new FlatFileItemReader<?>();
itemReader.setLineMapper(lineMapper());
itemReader.setResource(new ClassPathResource(filename));
return itemReader;
}
,std::vector<>
等标准库构造,或者如果你需要std::array<>
,{{1 }}
有缺陷的软件中最常见的罪魁祸首是(推出自己的)内存管理暴露了漏洞,更糟糕的是这些通常难以调试。
答案 1 :(得分:0)
我已经被正式教导为所有成员数据提供getter和setter,并支持原始指针而不是自动对象
就个人而言,我对所有数据成员都有setter和getter没有任何问题。拥有并且可以节省很多悲伤是一种很好的做法,特别是如果你冒险进入线程。事实上,许多UML工具会为您自动生成它们。你只需要知道要返回什么。在此特定示例中,不要将原始指针返回到SmallObject1 *
。请改为SmallObject1 * const
。
关于
的第二部分原始指针
用于教育目的。
对于您的主要问题:构建对象存储的方式取决于更大的设计。 BigObject
是唯一可以使用SmallObject
的类吗?然后我将它们作为私有成员完全放在BigObject
内部并在那里进行所有内存管理。如果SmallObject
在不同的对象之间共享,而不一定是BigObject
类,那么我会做你做的。但是,我会将const的引用或指针存储到它们中,而不是在BigObject
类的析构函数中删除它们 - BigObject
没有分配它们,因此不应该删除它。
答案 2 :(得分:0)
请考虑以下代码:
class SmallObj {
public:
int i_;
double j_;
SmallObj(int i, double j) : i_(i), j_(j) {}
};
class A {
SmallObj so_;
int x_;
public:
A(SmallObj so, int x) : so_(so), x_(x) {}
int something();
int sox() const { return so_.i_; }
};
class B {
SmallObj* so_;
int x_;
public:
B(SmallObj* so, int x) : so_(so), x_(x) {}
~B() { delete so_; }
int something();
int sox() const { return so_->i_; }
};
int a1() {
A mya(SmallObj(1, 42.), -1.);
mya.something();
return mya.sox();
}
int a2() {
SmallObj so(1, 42.);
A mya(so, -1.);
mya.something();
return mya.sox();
}
int b() {
SmallObj* so = new SmallObj(1, 42.);
B myb(so, -1.);
myb.something();
return myb.sox();
}
方法'A'的缺点:
SmallObject
的具体用法使我们依赖于它的定义:我们不能只是向前宣布它,SmallObject
实例对我们的实例(不共享)是唯一的,接近'B'的缺点有几个:
B
之前执行动态内存分配反对使用自动对象的一个论点是按值传递它们的 cost 。
这是可疑的:在很多普通的自动对象中,编译器可以针对这种情况进行优化并在线初始化子对象。如果构造函数是微不足道的,它甚至可以在一个堆栈初始化中完成所有操作。
这是GCC的-O3实现a1()
_Z2a1v:
.LFB11:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
subq $40, %rsp ; <<
.cfi_def_cfa_offset 48
movabsq $4631107791820423168, %rsi ; <<
movq %rsp, %rdi ; <<
movq %rsi, 8(%rsp) ; <<
movl $1, (%rsp) ; <<
movl $-1, 16(%rsp) ; <<
call _ZN1A9somethingEv
movl (%rsp), %eax
addq $40, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
突出显示的(; <<
)行是编译器一次性就地构建A和它的SmallObj子对象。
a2()的优化非常相似:
_Z2a2v:
.LFB12:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
subq $40, %rsp
.cfi_def_cfa_offset 48
movabsq $4631107791820423168, %rcx
movq %rsp, %rdi
movq %rcx, 8(%rsp)
movl $1, (%rsp)
movl $-1, 16(%rsp)
call _ZN1A9somethingEv
movl (%rsp), %eax
addq $40, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
那里有b():
_Z1bv:
.LFB16:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA16
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
movl $16, %edi
subq $16, %rsp
.cfi_def_cfa_offset 32
.LEHB0:
call _Znwm
.LEHE0:
movabsq $4631107791820423168, %rdx
movl $1, (%rax)
movq %rsp, %rdi
movq %rdx, 8(%rax)
movq %rax, (%rsp)
movl $-1, 8(%rsp)
.LEHB1:
call _ZN1B9somethingEv
.LEHE1:
movq (%rsp), %rdi
movl (%rdi), %ebx
call _ZdlPv
addq $16, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 16
movl %ebx, %eax
popq %rbx
.cfi_def_cfa_offset 8
ret
.L6:
.cfi_restore_state
.L3:
movq (%rsp), %rdi
movq %rax, %rbx
call _ZdlPv
movq %rbx, %rdi
.LEHB2:
call _Unwind_Resume
.LEHE2:
.cfi_endproc
显然,在这种情况下,我们付出沉重的代价来通过指针而非价值。
现在让我们考虑以下代码:
class A {
SmallObj* so_;
public:
A(SmallObj* so);
~A();
};
class B {
Database* db_;
public:
B(Database* db);
~B();
};
从上面的代码中,您对A的构造函数中“SmallObj”的所有权的期望是什么?您对B中“数据库”所有权的期望是什么?您是否打算为您创建的每个B构建一个唯一的数据库连接?
为了进一步回答您关于支持原始指针的问题,我们需要看一下2011 C ++标准,该标准引入了std::unique_ptr
和std::shared_ptr
的概念,以帮助解决自Cs以来存在的所有权歧义{ {1}}(返回指向字符串副本的指针,记得自由)。
标准委员会之前有一个建议在C ++ 17中引入observer_ptr
,这是一个围绕原始指针的非拥有包装。
使用这些方法引入了大量的锅炉板:
strdup()
我们知道auto so = std::make_unique<SmallObject>(1, 42.);
A a(std::move(so), -1);
拥有我们分配的a
实例的所有权,因为我们通过so
明确授予其所有权。但所有明确的成本都会造成影响。对比:
std::move
或
A a(SmallObject(1, 42.), -1);
所以我认为整体上很少有人喜欢用于组合的小对象的原始指针。您应该查看您的材料,从而得出结论,因为您似乎可能忽略或误解了何时使用原始指针的因素。
答案 3 :(得分:0)
其他人已经描述了优化原因,我现在从类型/功能角度来看它。根据Stroustrup的说法,“建立类不变量是每个构造函数的作用”。你的班级在这里是什么?知道(并定义!)非常重要,否则你会用if
污染你的成员函数来检查操作是否有效 - 这并不比没有类型更好。在90年代,我们有类似的类,但是现在我们确实坚持不变的定义,并希望对象始终处于有效状态。 (函数式编程更进一步,尝试从对象中提取变量状态,因此对象可以是const。)
std::optional<SmallObject>
个成员。可选通常在本地分配(与堆相比),因此您可以从缓存局部性中受益。请注意,我们中许多喜欢功能样式的人认为构建器是反模式的,并且仅用于反序列化(如果有的话)。背后的原因,很难推断出一个builer(什么出来,它会成功,哪个构造函数得到calles)。如果你有两个整数,那就是:两个整数。您最好的选择通常只是将它们保存在单独的变量中,然后由编译器进行各种优化。我不会感到惊讶的是,如果这些碎片奇迹般地落入碎片并且你的整体将被“构建”,那么以后就不需要复制了。
OTOH,如果你发现其他人在很多地方都有相同的参数'得到限制'(得到他们的价值),那么你可能会为他们引入一种类型。在这种情况下,您的两个整数将是一个类型(最好是结构)。您可能决定是否要将其作为BigObject
的基类,成员,或者只是一个单独的类(如果您有多个绑定顺序,则必须选择第三个) - 在任何一种情况下,您的构造函数现在将采用新的类而不是两个整数。您甚至可以考虑弃用其他构造函数(采用两个整数的构造函数)作为1.新对象可以轻松构造,2。它可以共享(例如,在循环中创建项目时)。如果你想保留旧的构造函数,请将其中一个作为另一个的委托。