首页 > 编程语言 >C++对象模型:constructor

C++对象模型:constructor

时间:2024-11-01 20:23:53浏览次数:1  
标签:初始化 int 模型 C++ 编译器 constructor 拷贝 public 构造函数

构造函数constructor

explicit的引入,是为了能够制止“单一参数的constructor”被当作一个conversion运算符

带有默认构造函数的对象成员

若一个类中包含对象成员,且该对象有默认构造函数,此时:

  • 若该类没有构造函数
    则编译器会合成一个默认构造函数,且发生在真正调用时
  • 若该类有构造函数,但没有初始化对象成员
    则在已有构造函数中按声明顺序生成构造对象成员的代码,且发生在构造最开始的时候

被合成的默认构造函数,只满足编译器的需要,而不是程序的需要
如初始化类对象成员是编译器的需要,初始化其他类成员变量则是程序的需要

编译器是如何避免合成多个默认构造函数的呢?如在不同文件中构造同一个类
编译器会将合成的默认构造函数,拷贝构造函数,析构函数,赋值运算符重载以inline的方式完成,inline函数有静态链接static linkage,不会被文件以外者看到,若函数太复杂,则会合成出一个explicit non-inline static实例

带有默认构造函数的父类

  • 若子类没有构造函数,则会合成默认构造函数调用父类构造函数
  • 若子类已有构造函数,则会生成调用父类构造函数的代码,且发生在初始化对象成员之前

带有虚函数的类

还有两种情况也需要合成默认构造函数

  • 类中声明了虚函数
  • 类的继承链中,包含虚继承关系
class Widget{
public:
  virtual void flip()=0;
};

void flip(const Widget& w){ w.flip(); }

会生成虚函数表vtbl和其指针vptr

w.flip()的虚调用操作会被改写,以满足多态

(*w.vptr[1])(&w); // &w 为当前对象的this指针

在初始化时,编译器会给每个对象的vptr设定初值,放置适当的vtbl地址
此时会合成,或在已有构造函数中生成相关初始化代码

带有虚基类的类

class X{ public: int i; };
class A:public virtual X{ public: int j; };
class B:public virtual X{ public: int n; };
class C:public A,public B{ public: int m; };

void foo(const A* pa){ pa->i=1024; }

编译器无法知道X::i的实际偏移量(经由pa存取),因为pa的真正类型是可改变的

编译器必须改变“执行存取操作”的代码,使X::i可以延迟至执行期决定
cfront的做法是在子类对象中安插虚基类指针
pa->_vbcX->i=1024;,其中_vbcX是编译器产生的指针,指向虚基类X

此时会合成,或在已有构造函数中生成相关代码,来初始化该指针

为什么虚函数和虚基类只有在运行时才能确定
因为编译器不解析赋值操作

拷贝构造函数copy constructor

如下场景,会调用类的拷贝构造函数

对象的显式初始化操作

X xx=x;

对象作为参数传递给函数
extern void foo(X x);
foo(xx);

对象作为函数返回值
X foo_bar(){
X xx;
return xx;
}

default memberwise initialization

若一个类没有提供显式的拷贝构造函数,则在拷贝构造时,内部以default memberwise initialization方式完成
即拷贝原生类型的成员变量,但对于成员对象,会采用递归的方式施行memberwise initialization
(在同类对象间拷贝构造时)

决定一个类是否生成默认的拷贝构造函数,在于该类是否展现出bitwise copy semantics
(不展现才会生成)

位逐次拷贝bitwise copy semantics

以下声明展现了bitwise copy semantics

class Word{
public:
  Word(const char*);
  ~Word(){ delete[] str; }
private:
  int cnt;
  char* str;
};

这种情况下,不需要合成一个default copy constructor,因为上述声明展现了default copy semantics
而对象的拷贝初始化操作也就不需要以一个函数调用收场

什么时候一个类不展现bitwise copy semantics呢?
1.成员变量中包含成员对象,且对象的类型有拷贝构造函数(包括编译器生成)
2.继承自父类,且父类有拷贝构造函数(包括编译器生成)
3.类中声明了虚函数
4.类的继承链中,包含虚继承关系

前两种情况,会在编译器默认生成的拷贝构造函数中,调用相关成员对象或父类的拷贝构造函数
第三种情况,以子类对象初始化父类对象时,需要保证vptr的操作安全,此时生成的父类的拷贝构造函数会设定vptr的值,而不是直接从子类中拷贝,这也解释了上一章代码

ZooAnimal za= b;
za.rotate();

调用的是ZooAnimal::rotate()

第四种情况

class Raccoon:public virtual ZooAnimal{
public:
  Raccoon(){}
  Raccoon(int val){}
private:
};

class RedPanda:public Raccoon{
public:
  RedPanda(){}
  RedPanda(int val){}
private:
};

若以一个Raccoon object作为另一个Raccoon object的初值,则bitwise copy绰绰有余
若以RedPanda object作为Raccoon object的初值,编译器则需要生成拷贝构造函数,并初始化virtual base class pointer/offset

下面这种情况,编译器无法知道bitwise copy semantics是否还保持,因为无法知道Raccoon指针是指向Raccoon object还是derived class object

Raccoon *ptr;
Raccoon little_critter= *ptr;

程序转化program transformation

显式初始化

T t1(t0);
T t2= t0;
T t1= T(t0);

会转化成:先声明,再拷贝构造

T t1;
T t2;
T t3;

t1.T::T(t0);
t2.T::T(t0);
t3.T::T(t0);

参数初始化
将一个class object当作函数实参,或函数返回值
参数传递时,会以memberwise方式进行
在编译器实现计算上,有以下两种转化策略
策略一
引入临时性对象,并用拷贝构造函数初始化,再以bitwise方式传递给形参
函数形参也必须被转化,需要以引用方式声明
策略二
将实参对象实际拷贝构造在函数堆栈中

返回值初始化
cfront中采用双阶段转化

  1. 声明一个class object的引用__result
  2. 在return前,使用返回值来拷贝构造传入的引用

对于函数指针

X (*pf)();
pf= bar;

转化为

void (*pf)(X&);
pf= bar;

在使用者层面优化
定义一个构造函数constructor,可以直接计算返回值,而不是调用拷贝构造函数

在编译器层面优化
将返回值直接使用__result代替,称为Named Return Value(NRV)优化
NRV优化现在被认为是C++编译器义不容辞的优化操作
如下代码

class Test{
  friend Test foo(double);
public:
  Test(){
    memset(arr, 0, 100*sizeof(double));
  }
private:
  double arr[100];
};

此时,编译器不会做NRV优化,因为没有拷贝构造函数,如下加上inline copy constructor

inline Test(const Test& t){
  memcpy(this, &t, sizeof(test));
}

是否需要拷贝构造函数

没有任何理由要提供一个拷贝构造函数,因为编译器自动实施了最好的行为

若一个class要大量memberwise初始化操作,则提供一个copy constructor的explicit inline函数实例是合理的(在编译器提供NRV的前提下)

若使用更有效率的memset()memcpy()作为拷贝构造函数的实现,则需要在class中不含任何由编译器产生的内部成员

成员的初始化

必须使用成员初始化列表的情况
1.初始化引用reference成员
2.初始化const成员
3.调用基类构造函数,且其有一组参数
4.调用成员的构造函数,且其有一组参数

class Word{
  String name_;
  int cnt_;
public:
  Word(){
    name_= 0;
    cnt_= 0;
  }
};

编译器会生成一个临时对象,可能的转化如下

public:
  Word(){
    // 默认构造
    name_.String::String();

    // 临时对象
    String temp= String(0);
    // memberwise拷贝
    name_.String::operator=(temp);
    temp.String::~String();

    cnt_= 0;
  }

若使用列表初始化

Word::Word():name_(0){
  cnt_= 0;
}

编译器可能的转化如下

Word::Word() {
  // 直接调用构造函数
  name_.String::String(0);
  cnt_= 0;
}

成员初始化列表到底做了什么,是不是简单的函数调用
可以回答,当然不是简单的函数调用

编译器会在构造函数中生成代码,将初始化列表中的变量按在类中的声明顺序初始化
如下代码

class X{
  int i;
  int j;
  X(int val):j(val),i(j){}
};

由于声明顺序的缘故,i会比j先执行,会导致问题,这种情况,GNU C++编译器g++会做出告警
建议做出如下调整

class X{
  int i;
  int j;
  X(int val):j(val){
    i= j;
  }
};

能否调用成员函数,以初始化数据成员

X::X(int val):i(xfoo(val)),j(val){}

成员函数的使用是合法的,因为此时this指针已经被构造
编译器可能的生成代码如下

X::X(int val){
  i= this->xfoo(val);
  j= val;
}

能否使用子类成员函数返回值,作为父类构造函数的实参

class FooBar:public X{
  int fval_;
public:
  int fval(){ return fval_; }
  FooBar(int val):fval_(val),X(fval()){}
};

编译器可能的生成代码如下

FooBar::FooBar(int val){
  X::X(this, this->fval());
  fval_= val
}

可知,这不是一个好主意

标签:初始化,int,模型,C++,编译器,constructor,拷贝,public,构造函数
From: https://www.cnblogs.com/sgqmax/p/18521183

相关文章

  • 深入解析 Transformers 框架(三):Qwen2.5 大模型的 AutoTokenizer 技术细节
    前面2篇文章,我们通过查看Transformers包代码,学习了Transformer包模块API设计、模型初始化和加载流程:第1篇:transformers推理Qwen2.5等大模型技术细节详解(一)transformers包和对象加载第2篇:transformers推理Qwen2.5等大模型技术细节详解(二)AutoModel初始化......
  • 【C++】类与对象(中)
    1.类的默认成员函数  默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后......
  • C++多线程:thread
    进程与线程进程:系统资源分配的最小单元,通常被定义为一个正在运行的程序实例线程:系统任务调度的最小单元进程间通信:管道,信号量,信号,消息队列,共享内存,套接字线程间通信:锁机制,信号量机制,信号机制,屏障同步:保证任务片段的先后顺序互斥:为了保证资源在同一时刻只能被一个线程使用,即......
  • C++多线程:mutex
    互斥量C++11互斥锁定义在<mutex>头文件中,提供了独占资源的特性C++11头文件中定义的互斥量互斥量说明mutex基本互斥量recursive_mutex递归互斥量timed_mutex定时互斥量recursive_timed_mutex递归定时互斥量std::mutex最基本的互斥量,提供了独占所有权......
  • C++多线程:condition_variable
    条件变量类似于pthread库中的pthread_cond_*()提供的功能,C++11标准提供了两种表示条件变量的类,分别是condition_variable和condition_variable_any,定义在头文件<condition_variable>中std::condition_variable当std::condition_variable对象调用wait()时,会阻塞当前线程,直到该s......
  • C++多线程:promise
    头文件包含:Providers类std::promisestd::packaged_taskFutures类std::futurestd::shared_futureProviders函数std::async()其他类型std::future_errorstd::future_errcstd::future_statusstd::launchstd::promise用来保存某一类型T的值,该值可以被future对......
  • C++多线程:package_task
    std::packaged_taskstd::packaged_task包装一个可调用对象,并允许获取该可调用对象计算的结果,可调用对象内部包含两个基本元素:1.被包装的任务任务是一个可调用对象,如函数指针或函数对象,该对象的执行结果会传递给共享状态2.共享状态用于保存任务的返回结果,并可通过future对象异......
  • 模拟大模型训练时,单双精度输出不一致?从而加剧幻觉?或导致幻觉?
        下面是Python代码。就同样的随机数据,分别在单精度、双精度下做模拟训练与预测,最后比较它们预测的值,发现不一致。    大家看看,代码是否有问题?importnumpyasnpimporttensorflowastffromtensorflow.keras.layersimportDense,LSTMfromtensorfl......
  • yolov8旋转目标检测从原理到模型训练、部署、验证、推理(附代码)
    定向边界框目标检测在这里插入图片描述导言定向目标检测是在传统目标检测的基础上更进一步的技术,它引入了一个额外的角度参数,以更精确地定位图像中的物体。传统的目标检测算法通常使用轴对齐的矩形包围框来框定物体,而定向目标检测则使用旋转的边界框,这些边界框能够更好......
  • C++标准库:chrono
    ratio先看一下ratio的定义template<intmax_tN,intmax_tD=1>classratio;ratio是由非类型参数定义的模板,用来定义一个比率N/D,如ratio<1,-2>表示-0.5标准库中定义的常用ratio类型typedefinition说明ratio<1,1000>std::milli1/1000ratio<1,1000000>std::mic......