一、问题
如以下代码,当class A中有数据成员是class B时,我们在A中定义成员变量的时候,是应该使用B的指针类型、引用类型还是直接使用类型B呢?
class B{
public:
int _int_elem;;
double _double_elem;
};
// 方式一
// 利用指针或者智能指针定义class A中数据成员B
class A
{
public:
B* _b;
};
// 方式二
// 利用引用定义class A中数据成员B
class A
{
public:
// 引用成员变量必须使用构造函数初始化列表进行初始化
A(B& b):_b(b){}
B& _b;
};
// 方式三
// class A中数据成员B使用数据实体定义
class A
{
public:
B b;
};
二、结论
成员变量使用引用、指针还是直接使用对象是视情况而定。
- 因为引用类型必须在初始化列表初始化,并且不能被重新赋值,使用较少。
- 指针类型适合在以下几种情况下使用:
- 当成员变量要使用多态时,将成员变量定义成指针。
- 当成员变量是可选的,在有些情况下不会使用,可以将成员变量定义成指针,节省空间。
- 当成员变量是一个很大的对象时,将成员定义成指针,避免栈溢出,linux默认栈大小可以通过ulimit -s查看,默认为8Mb。
- 当成员变量对应的资源不是对象独有,而是多个对象共同所有。
- 其他情况直接定义指定类型对象较好,无需进行内存管理,无需担心空指针。
三、各自优缺点
3.1 使用指针类型数据成员
3.1.1 优点
- 类对象在内存中占用的会更小。需要注意,只是类对象本身占用的空间变小了,不等同于可以节省内存空间,要是指针指向的空间是每个类对象独有的,是不会节省内存空间的,要是指针指向的空间是多个类对象共同所有的,则可以起到节省空间的作用。
- 只需要指针指向类的前置声明,不需要引入对应类的头文件,减少依赖,加快编译速度。
- 对应成员可以使用多态。
- 懒初始化,当成员变量是可选的,在有些情况下不会使用时,可以避免分配空间。
3.1.2 缺点
- 需要进行内存管理。通常需要自己实现析构函数,拷贝构造函数和赋值运算符,保证对象被正确销毁,拷贝,赋值,例如如下示例。
#include <iostream>
#include <memory>
class Wheel{
public:
Wheel(double radius):_radius(radius){}
double _radius;
};
class Car{
public:
Car(double wheel_radius){
_wheel_ptr = std::make_shared<Wheel>(wheel_radius);
}
std::shared_ptr<Wheel> _wheel_ptr;
};
int main(){
Car car1(10);
Car car2 = car1;
std::cout << "car1._wheel_ptr.get(): " << car1._wheel_ptr.get() << std::endl;
std::cout << "car2._wheel_ptr.get(): " << car2._wheel_ptr.get() << std::endl;
return 0;
}
// 输出:
// car1._wheel_ptr.get(): 0x605020
// car2._wheel_ptr.get(): 0x605020
// Car car2 = car1; 这一行调用默认拷贝构造函数,导致car1和car2的_wheel_ptr成员变量指向同一个Wheel对象,这个实际想要的不符,应该每个Car都有属于自己的车轮,所以不能使用默认生成的拷贝构造函数,需要重新实现拷贝构造函数。
#include <iostream>
#include <memory>
class Wheel{
public:
Wheel(double radius):_radius(radius){}
double _radius;
};
class Car{
public:
Car(double wheel_radius){
_wheel_ptr = std::make_shared<Wheel>(wheel_radius);
}
// 需要自己实现拷贝构造函数
Car(const Car& car){
_wheel_ptr = std::make_shared<Wheel>(car._wheel_ptr->_radius);
}
std::shared_ptr<Wheel> _wheel_ptr;
};
int main(){
Car car1(10);
Car car2 = car1;
std::cout << "car1._wheel_ptr.get(): " << car1._wheel_ptr.get() << std::endl;
std::cout << "car2._wheel_ptr.get(): " << car2._wheel_ptr.get() << std::endl;
return 0;
}
// 输出:
// car1._wheel_ptr.get(): 0x605020
// car2._wheel_ptr.get(): 0x605040
// car1 和 car2分别拥有属于自己的Wheel对象。
// 同理 通常情况下,析构函数,拷贝构造函数和赋值运算符都不能使用C++默认生成的,需要自己实现。但是要是数据成员不使用指针,则可以避免这个问题。
3.2 定义对应类型的对象为数据成员
3.2.1 优点
- 不需要通过代码进行内存管理
- 直接定义成员变量各个成员变量内存连续,有更快的访问速度。但当指针类型成员变量,内存分布不一定连续,更有可能造成cache miss.
3.2.2 缺点
- 对应成员不能使用多态。
- 需要引入对应类的头文件,降低编译速度。
- 每个类实例占用的内存空间更大。
3.3 使用引用类型数据成员
必须在构造函数的初始化列表中初始化引用类型数据成员,不能使用默认构造函数,不能使用默认赋值操作符,引用类型不能被重新赋值。因为这些限制,引用类型很少被用作成员变量。
参考链接
https://stackoverflow.com/questions/8589739/pointer-vs-non-pointer-members-of-a-class
https://stackoverflow.com/questions/38706329/smart-pointers-vs-value-as-member-variables/38706627
https://stackoverflow.com/questions/22146094/why-should-i-use-a-pointer-rather-than-the-object-itself
https://stackoverflow.com/questions/1175646/c-when-should-i-use-a-pointer-member-in-a-class