c++ 类与默认函数、包括构造函数和析构函数的特点

构造函数用来初始化类的对象,与父类的其它成员不同,它不能被子类继承(子类可以继承父类所有的成员变量和成员方法,但不继承父类的构造方法)。因此,在创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用其父类的构造方法。

构造函数

有自定义构造函数,则无默认构造函数;无自定义复制构造函数,则隐含生成默认复制构造函数。

如果没有显式的构造函数,编译器会给一个默认的构造函数,并且该默认的构造函数仅仅在没有显式地声明构造函数情况下创建。

一旦声明自定义版本的构造函数,则有可能导致定义的类型不再是POD类型。

如想是恢复pod特性 ,在默认函数定义或声明时加上“= default”

= default: 缺省函数,显示指示编译器生成该函数的默认版本。

构造函数可以被重载,因为构造函数可以有多个且可以带参数。

class MyClass
{
public:
    MyClass() = default;
    MyClass(int a) :m_i(a) {
    };
    ~MyClass();

private:
    int m_i;
};

MyClass::MyClass()
{
}

MyClass::~MyClass()
{
}

在以前,程序员若希望限制一些默认函数的生成,例如,单件类的实现需要阻止其生成拷贝构造函数和拷贝赋值函数,可以将拷贝构造函数和拷贝赋值函数声明为private成员,并且不提供函数实现。 见effective c++ 条款5。另外 C++11 标准给出了非常简洁的方法,即在函数的定义或者声明加上”= delete“。

= delete:删除函数,

class NoCopyConstructor2
{
    public:
        NoCopyConstructor2() = default;

        NoCopyConstructor2(int i)
            :data(i)
        {
        }
        NoCopyConstructor2(const NoCopyConstructor2&) = delete;

    private:
        int data;
};

构造函数和析构函数不能被继承,但子类可以调用基类的构造函数和析构函数.

 

     std::vector<string> vecs;
    vecs.push_back("1");
    vecs.push_back("2");
    std::vector<string> vecm = std::move(vecs); // 免去很多拷贝
 
    结果是 vecs 的大小为0

  /分析: vecs 是左值,通过调用std::move(vecs)函数后,变成右值(具体是将亡值)。
         vecm是左值,所以该赋值操作具体就是移动构造函数。

 

其实上述2条结论,都还需进一步判断。 需要知道是默认的还是自定义的移动构造函数、拷贝构造函数。见下面的描述分析。

拷贝构造函数

拷贝构造函数是使用类对象的引用作为参数的构造函数,它能够将参数的属性值拷贝给新的对象,完成新对象的初始化。

在什么情况下系统会调用拷贝构造函数:

(1)用类的一个对象去初始化另一个对象时

(2)当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用

(3)当函数的返回值是类的对象或引用时

什么时候必须重写拷贝构造函数

当类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。

无定义默认为浅拷贝。应定义,且为深拷贝。

C++中拷贝赋值函数的形参能否进行值传递?

不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数,如此循环,无法完成拷贝,栈也会满。

class NoCopyConstructor2
{
    public:
        NoCopyConstructor2() {};

        NoCopyConstructor2(int i)
            :data(i)
        {
        }
        NoCopyConstructor2(const NoCopyConstructor2& c) {
        this->data = c.data;
};

    private:
        int data;
};

 移动构造函数

如果都没自定义拷贝构造函数、移动构造函数。不太清楚返回值到底使用哪个构造函数。其实这种情况也没实际意义。

  编译器会隐式生成一个移动构造函数。如果声明了自定义的拷贝构造函数、拷贝赋值函数、析构函数中的一个或多个,编译器就不会生成默认版本。

所以该类含有成员指针,如果函数返回该类对象时,要特别关注成员指针,到底想要深拷贝还是移动的。解决办法:

1.如果已经自定义了拷贝构造函数、或赋值操作符,但没有自定义移动构造函数。函数返回对象时,执行拷贝构造函数或赋值操作函数。

2.如果自定义了移动构造函数和拷贝构造函数,会优先执行移动构造函数。

//代码1
class cbase 
{
public:
	int m_aaa;
	int m_b ;
	int *m_ptr;
	cbase();
	cbase(const cbase &base);
	cbase & operator = (const cbase &base);
	cbase(cbase &&base);
	virtual ~cbase();
};
cbase::cbase()
	:m_ptr(new int(1))
{
	m_aaa = 1;
}


cbase::cbase(const cbase &base)
{
	this->m_aaa = base.m_aaa;
	this->m_b = base.m_b;
	this->m_ptr = new int(*(base.m_ptr));
}

cbase & cbase::operator=(const cbase &base)
{
    if(this == &base)
        return *this;
	this->m_aaa = base.m_aaa;
	this->m_b = base.m_b;
	*(this->m_ptr) =* (base.m_ptr);
	return *this;
}

cbase::cbase(cbase &&base)
{
	this->m_aaa = base.m_aaa;
	this->m_b = base.m_b;
	this->m_ptr = base.m_ptr;
	base.m_ptr = nullptr;
}

cbase::~cbase()
{
	delete m_ptr;
	m_ptr = nullptr;
	
}
cbase getbase()
{
	cbase bsel;
	return bsel;
}

调用  cbase  ba = getbase();

同时自定义了移动构造函数和拷贝构造函数,会优先执行移动构造函数。
所以调用的是1次构造函数,1次移动构造函数,2次析构函数

调用std::move()函数,优先调用自定义移动构造函数。然后是自定义的移动构造函数,最后是默认的移动构造函数。

cbase base1;
cbase base2 = std::move(base1);

这是也是调用1次构造函数,1次移动构造函数,2次析构函数。
如果把自定义移动构造函数屏蔽掉,std::move()函数会调用自定义的拷贝构造函数。
如果是
cbase base1;
std::move(base1);
则只调用一次析构函数,不会调用移动构造函数,所以std::move 函数必须结合“=”一起使用。
cbase base1 = std::move(getbase());


这样调用1次构造函数,2次移动构造函数(1次是getbase函数返回值,一次是std::move函数)

另外:拷贝构造/赋值 和移动构造/赋值函数必须同时提供,或者同时不提供,才能保证类同时具备拷贝和移动语义。只声明其中一种的话,类仅能实现一种语义。 

移动构造函数底层原来是使用右值引用的方法

 表现出来的函数用:std::move();

具体见:

template<class _Ty>
	_NODISCARD constexpr remove_reference_t<_Ty>&&
		move(_Ty&& _Arg) noexcept
	{	// forward _Arg as movable
	return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
	}

具体见:c++ 11 新特性之 左值右值_baidu_16370559的博客-CSDN博客

析构函数:

如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

如果析构函数中异常非抛不可,那就用try catch来将异常吞下,必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外。

析构函数不可以被重载。而析构函数只能有一个,且不能带参数。

延伸:

继承构造函数

构造函数和析构函数是用来处理对象的创建和析构的,它们只知道对在它们的特殊层次的对象做什么。所以,在整个层次中的所有的构造函数和析构函数都必须被调用,也就是说,构造函数和析构函数不能被继承。

子类的构造函数会显示的调用父类的构造函数或隐式的调用父类的默认的构造函数进行父类部分的初始化。析构函数也一样。它们都是每个类都有的东西,如果能被继承,那就没有办法初始化了。

不是所有的函数都能自动地从基类继承到派生类中的。

创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用其父类的构造方法

如果没有显式的构造函数,编译器会给一个默认的构造函数,并且该默认的构造函数仅仅在没有显式地声明构造函数情况下创建。

构造原则如下:

    1. 如果子类没有定义构造方法,则调用父类的无参数的构造方法。

    2. 如果子类定义了构造方法,不论是无参数还是带参数,在创建子类的对象的时候,首先执行父类无参数的构造方法(可能是默认无参构造函数,可能是父类自定义的无参构造函数),然后执行自己的构造方法。

    3. 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数,则会调用父类的默认无参构造函数。

    4. 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数且父类自己提供了无参构造函数,则会调用父类自己的无参构造函数。

    5. 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数且父类只定义了自己的有参构造函数,则会出错(如果父类只有有参数的构造方法,则子类必须显示调用此带参构造方法)。

    6. 如果子类调用父类带参数的构造方法,需要用初始化父类成员对象的方式

关键在于:基类如果写了有参构造函数,需要初始化基类的成员;在子类中这些基类的成员一样要初始化,是通过显式调用基类构造函数的方式实现的,如果你不做显式调用,则不会自动进行基类成员的初始化

class A {
public:
    A() {

    };
    A(int a,int b):m_a(a),m_b(b) {        
        std::cout << "A" << a << std::endl;
    };
    A(char* a)
    {
        std::cout << "A" << &a << std::endl;
    };
    void f(float ff) {
        std::cout << "f" << ff << std::endl;
    }
private:
    int m_a;
    int m_b;
};

class B :public  A
{
public:
  
 B(int a,int b):A(a,b) {    //子类构造方法带参数,显式调用父类的构造方法,这个就是初始化列表

    };
    using A::f;
    void f(std::string str) {
        std::cout << "fF" << str << std::endl;
    }

};

int main()
{

    A AAA(1,2);
    B BBB(10,11);

}

一旦使用了继承构造函数,编译器就不会为派生类生成默认构造函数了。

为了构建子类的构造函数而调用父类的构造函数时,给父类里那些子类不用的参数赋个值就OK了。所以哪怕父类的构造函数有100000个参数,子类的构造函数其实依然可以是无参数的。

使用using来继承基类构造函数  

将基类T中的系列构造函数传递到派生类U中。前提是:基类的构造函数是public.注意:继承构造函数只能初始化基类中的数据成员,对于派生类中的数据成员,仍然需要自行处理。

 背景:派生类无法继承基类构造函数,当类B继承于类A的时候,它会继承类A中的数据成员与普通成员函数。但是某些成员函数是无法被继承下来的,比如类A(基类)中的合成构造函数(包括构造、析构、拷贝等等)。因此,类B在初始化类A的成员时候,需要显示调用类A的构造函数以达到初始化的目的。

class T
{
public:
   T() = default;
   T(const int &a){}
   T(const int &a, float &b){}
};

class U : public T{
public:
    using T::T;  //using声明基类T的构造函数

    int m_b{1};  //C++11新特性-快速初始化成员变量。
};
继承构造函数可能遇到的问题

1.如果基类的构造函数是private属性,那么派生类无法声明基类的继承构造函数。

2.如果派生类是是以虚继承的方式从基类进行派生,在派生类中也无法声明基类的继承构造函数。

3. 当派生类是多继承方式时候,可能会出现继承类冲突隐患。可以通过显式定义继承类的冲突的析构函数。

如:把上面的class B改成

class B :public  A
{
public:
    using A::A;
    using A::f;
    void f(std::string str) {
        std::cout << "fF" << str << std::endl;
    }

};

派生类中的数据成员初始化的解决的办法主要有两个:

一是使用C++11特性就地初始化成员变量,可以通过=、{}对非静态成员快速地就地初始化,以减少多个构造函数重复初始化变量的工作,注意初始化列表会覆盖就地初始化操作。

class Base
{
public:
	Base(int va) :m_value(va), m_c('0') {}
	Base(char c) :m_c(c), m_value(0) {}
private:
	int m_value;
	char m_c;
};
 
class Derived :public Base
{
public:
	//使用继承构造函数
	using Base::Base;
 
	//假设派生类只是添加了一个普通的函数
	void display()
	{
//dosomething		
	}
private:
//派生类新增数据成员
double m_double{0.0};
};

二是新增派生类构造函数,使用构造函数初始化列表初始化。

class Base
{
public:
	Base(int va) :m_value(va), m_c('0') {}
	Base(char c) :m_c(c), m_value(0) {}
private:
	int m_value;
	char m_c;
};
 
class Derived :public Base
{
public:
	//使用继承构造函数
	using Base::Base;
 
	//新增派生类构造函数
	Derived(int a,double b):Base(a),m_double(b){}
 
	//假设派生类只是添加了一个普通的函数
	void display()
	{
		//dosomething		
	}
private:
	//派生类新增数据成员
	double m_double;
};

委派构造函数

1.委托构造函数:在初始化列表中调用“基准版本”的构造函数。

2.目标构造函数:被调用的基准版本。

委托构造函数不能有初始化列表。

1.在c++,构造函数不能同时委派和使用初始化列表。如果委托构造函数要给变量赋值,初始化代码必须放在函数体中。

2.c++11,目标构造函数执行总是先于委派构造函数。

3.在委托构造的链状关系,不能形成委托环。

其他:

explicit关键字修饰的类构造函数,不能进行自动地隐式类型转换(即不能直接使用赋值操作符号“=”),只能显式地进行类型转换(如(类型)变量)。

C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).

注意:只有一个参数的构造函数,或者构造函数有n个参数,但有n-1个参数提供了默认值,这样的情况才能进行类型转换。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值