c++--类型行为控制

1.c++的类

1.1.c++的类关键点
c++类型的关键点在于类存在继承。在此基础上,类存在构造,赋值,析构三类通用的关键行为。
类型提供了构造函数,赋值运算符,析构函数来让我们控制三类通用行为的具体表现。
为了清楚的说明类的构造,析构,赋值行为。我们采用一个具体的实例来验证这种行为。

class Norm1
{
public:
    Norm1() {
        printf("Norm1()\n");
    }
    Norm1(char i):m_i(i) {
        printf("Norm1(int)_%c\n", m_i);
    }
    Norm1(const Norm1 &a):m_i(a.m_i){
        printf("Norm1(const Norm1&)_%c\n", m_i);
    }
    Norm1(Norm1&& a):m_i(a.m_i) {
        printf("Norm1(Norm1&&)_%c\n", m_i);
    }
    Norm1& operator=(const Norm1& a)
    {
        m_i = a.m_i;
        printf("Norm1=(const Norm1&)_%c\n", m_i);
        return *this;
    }
    Norm1& operator=(Norm1&& a)
    {
        m_i = a.m_i;
        printf("Norm1=(Norm1&&)_%c\n", m_i);
        return *this;
    }
    ~Norm1()
    {
        printf("~Norm1()_%c\n", m_i);
    }
private:
    char m_i='d';
};

class Norm2
{
public:
    Norm2() {
        printf("Norm2()\n");
    }
    Norm2(char i):m_i(i) {
        printf("Norm2(int)_%c\n", m_i);
    }
    Norm2(const Norm2 &a):m_i(a.m_i){
        printf("Norm2(const Norm2&)_%c\n", m_i);
    }
    Norm2(Norm2&& a):m_i(a.m_i) {
        printf("Norm2(Norm2&&)_%c\n", m_i);
    }
    Norm2& operator=(const Norm2& a)
    {
        m_i = a.m_i;
        printf("Norm2=(const Norm2&)_%c\n", m_i);
        return *this;
    }
    Norm2& operator=(Norm2&& a)
    {
        m_i = a.m_i;
        printf("Norm2=(Norm2&&)_%c\n", m_i);
        return *this;
    }
    ~Norm2()
    {
        printf("~Norm2()_%c\n", m_i);
    }
private:
    char m_i='d';
};

class Base
{
public:
    Base():m_n2('b'),m_n1('b') {
        printf("Base()\n");
    }
    Base(char i):m_n2(i),m_n1(i) {
        printf("Base(int)\n");
    }
    Base(const Base &a):m_n2(a.m_n2), m_n1(a.m_n1){
        printf("Base(const Base&)\n");
    }
    Base(Base&& a):m_n2(std::move(a.m_n2)),m_n1(std::move(a.m_n1)) {
        printf("Base(Base&&)\n");
    }

    Base& operator=(const Base& a)
    {
        m_n1 = a.m_n1;
        m_n2 = a.m_n2;
        printf("Base=(const Base&)\n");
        return *this;
    }
    Base& operator=(Base&& a)
    {
        m_n1 = std::move(a.m_n1);
        m_n2 = std::move(a.m_n2);
        printf("Base=(Base&&)\n");
        return *this;
    }
    ~Base()
    {
        printf("~Base()\n");
    }
private:
    Norm1 m_n1;
    Norm2 m_n2;
};

class A : public Base
{
public:
    A():Base('b'),m_n1('a'),m_n2('a') {
        printf("A()\n");
    }
    A(char i): Base(i),m_n1(i),m_n2(i) {
        printf("A(int)\n");
    }
    A(const A &a):Base(a),m_n1(a.m_n1),m_n2(a.m_n2){
        printf("A(const A&)\n");
    }
    A(A&& a):Base(a),m_n1(std::move(a.m_n1)),m_n2(std::move(a.m_n2)) {
        printf("A(A&&)\n");
    }

    A& operator=(const A& a)
    {
        Base::operator=(a);
        m_n1 = a.m_n1;
        m_n2 = a.m_n2;
        printf("A=(const A&)\n");
        return *this;
    }
    A& operator=(A&& a)
    {
        Base::operator=(std::move(a));
        m_n1 = std::move(a.m_n1);
        m_n2 = std::move(a.m_n2);
        printf("A=(A&&)\n");
        return *this;
    }

    ~A()
    {
        printf("~A()\n");
    }
private:
    Norm1 m_n1;
    Norm2 m_n2;
};

2.关于类的构造

2.1.构造函数执行顺序

#include "test.h"
int main()
{
    A a1;
    return 0;
}

上述代码编译运行后的输出:
在这里插入图片描述
这是因为对任意一个c++类型A,其构造顺序为:
(1). 基类构造.
(2). 成员变量按类内出现顺序构造.
(3). 类自身的构造函数体.
上述定义一个递归式的定义。

2.2.默认构造
(1). 定义
不需要传递任何参数即可执行的构造函数称为默认构造。

class A1
{
public:
	A1(){}
};

class A2
{
public:
	A2(int i = 0){}
};

上述A1()算默认构造,A2(int i = 0)也算默认构造。因为这些构造函数可以不用传递任何实参即可执行。如A1 a1;A2 a2;将分别执行上述构造函数。

(2).合成版本
a. 当一个c++类型定义中用户没有自定义任何构造函数时,编译器默认为其提供一个默认构造函数。

#include <iostream>
class A
{
public:
    A(const A&)
    {
        printf("A(const A&)\n");
    }
};

int main()
{
    A a;// err,因为已经存在自定义构造函数,无合成版本
    return 0;
}

b. 默认构造函数可行的前提是:
(1). 基类支持默认构造.
(2). 类的所有成员变量支持默认构造.

#include <iostream>
class T
{
private:
    T(){}
};

class A
{
public:
  
private:
    T t;
};

int main()
{
    A a;// err,因为类的成员t对A来说不支持默认构造
    return 0;
}

c. 合成的默认构造的行为
(1). 基类默认构造.
(2). 成员变量按类内出现顺序执行其默认构造.
(3). 类自身的构造函数体(合成下函数体为空).
上述定义是一个递归式的定义。

(3).显式请求合成版本
当用户自定义了构造函数,而我们依然希望编译器提供合成版本默认构造时可以用下述方式

#include <iostream>
class A
{
public:
    A(int i){
        printf("A(int)\n");
    }
    A()=default;
};

int main()
{
    A a;
    return 0;
}

3.拷贝构造

拷贝构造其实就是构造的一种类型。但是使用比较频繁,故单独拿出来再说一遍。
3.1. 拷贝构造执行顺序
拷贝构造依然属于构造,所以遵循构造执行顺序。

int main()
{
    A a1;
    A a2(a1);
    return 0;
}

上述代码编译后,输出为:
在这里插入图片描述
其中蓝色部分对应A a2(a1);这是因为对任意一个c++类型A,其拷贝构造顺序为:
(1). 基类构造.
(2). 成员变量按类内出现顺序构造.
(3). 类自身的构造函数体.
上述定义一个递归式的定义。

针对显式定义的拷贝构造,如何处理基类,成员的拷贝依赖构造函数初始值列表:
(1). 匹配到的A的构造函数中构造列表指定了如何构造下,以构造列表中指定的为准。
(2). 匹配到的A的构造函数中构造列表未指定时,会采用类型的默认构造。如果我们上述例子中将A的拷贝构造修改为:

A(const A &a){
    printf("A(const A&)\n");
}

再次执行

int main()
{
    A a1;
    A a2(a1);
    return 0;
}

输出如下:
在这里插入图片描述
3.2.拷贝构造
(1).定义
只需要传递类型自身一个实例即可执行的构造函数称为拷贝构造。

class A1
{
public:
	A1(const A1& a){}
	A1() = default;
};

class A2
{
public:
	A2(const A2& a, int i = 0){}
	A2() = default;
};

上述A1(const A1& a)算拷贝构造,A2(const A2& a, int i = 0)也算拷贝构造。因为这些构造函数只需要传递类型自身一个实例即可执行。如A1 a1;A2 a11(a1);A2 a2;A2 a22(a2);A2 a11(a1)A2 a22(a2)将分别执行上述拷贝构造。

因为拷贝构造也属于构造函数,所以定义了拷贝构造下,若希望提供合成版本默认构造函数,必须采用A1()=default这样的方式显式请求。

(2).合成版本
a. 对于拷贝构造,不同于默认构造。
默认构造是只要存在任何构造函数,就不会提供编译器合成的默认构造版本。
拷贝构造,允许存在其他构造函数,只要用户没有定义只接收自身实例即可执行的构造函数,编译器就会提供合成的拷贝构造。

b.合成版本行为表现

#include "test.h"
class B : public Base
{
public:
private:
    Norm1 m_n1;
    Norm2 m_n2;
};

int main()
{
    B b1;
    B b2(b1);
    return 0;
}

在这里插入图片描述
上述输出中红色对于合成版本默认构造。蓝色对应合成版本拷贝构造。
不过此时,对基类,执行的是基类的拷贝构造。对成员变量,执行的也是其拷贝构造。类自身的拷贝构造函数体是空的。需要注意合并拷贝构造和显式定义的拷贝构造的区别.显式定义下,基类,成员如何拷贝依赖构造函数初始值列表中的指定,未显式下将采用类型的默认构造,而非拷贝构造.

c.合成的拷贝构造函数可行的前提是:
(1). 基类支持拷贝构造
(2). 类的所有成员变量支持拷贝构造

#include <iostream>
class T
{
public:
	T(){}
private:
    T(T& t){}
};

class A
{
public:
  
private:
    T t;
};

int main()
{
    A a;
    A a2(a);// err,因为A的成员t不支持拷贝构造。
    return 0;
}

(3).显式请求合成版本
拷贝构造严格来说没有显式请求的必要,因为只要用户没有通过只接收类型自身实例即可执行的构造函数,编译器总是会提供合成的拷贝构造。

4.关于类的赋值

4.1.赋值顺序

int main()
{
    A a1;
    A a2;
    a2 = a1;
    return 0;
}

在这里插入图片描述
上述紫红色的1,2,3对应a2=a1;
值得注意的是,赋值运算符不像构造函数,析构函数。
像构造函数无论如何总是会对基类执行构造,对类的成员执行构造。类的定义者只能通过构造初始化列表决定采用那个版本的基类构造,成员构造。但基类构造,成员构造作为两个执行阶段无论针对合成版本构造函数,还是用户自定义的构造函数总是存在的。
对赋值,如果用户自定义了赋值运算符,则,基类部分赋值,成员赋值全部依赖自定义函数体的实现。
上述紫红色包含1,2,3的原因是我们提供的自定义赋值运算符里面分别处理了基类赋值,成员赋值。
如果将A的赋值运算符修改为:

A& operator=(const A& a)
{
    printf("A=(const A&)\n");
    return *this;
}

再次执行

int main()
{
    A a1;
    A a2;
    a2 = a1;
    return 0;
}

输出将为:
在这里插入图片描述
此时不会对基类,成员执行赋值处理.

4.2.默认赋值运算符
(1).定义

class A1
{
public:
	A1& operator=(const A1& a)
	{
		...	
	}
};

作为赋值运算符:
a. 返回值必须是类型自身的引用类型。
b. 只能接收一个形参。
c. 传递类型自身实例时,可以匹配到形参。所以下述,也是合法的自定义赋值运算符。

class A1
{
public:
	A1& operator=(A1 a)
	{
		...	
	}
};

(2).合成版本
a.何时合成
a.1. 用户没有自定义赋值运算符时.
a.2. 类型没有定义移动拷贝构造函数.
a.3. 类的成员,基类的赋值运算符存在.

b.合成版本行为表现

#include "test.h"
class B : public A
{
private:
    Norm1 n1;
};

int main()
{
    B b1;
    B b2 = b1;
    return 0;
}

在这里插入图片描述
上述紫红色1,2对应B的合成版本赋值运算符。
合成的赋值运算符:
(1). 通过基类赋值运算符完成基类赋值。
(2). 对每个成员执行其赋值运算符完成成员赋值。

c.合成版本可行的前提是:
(1). 基类支持赋值运算符.
(2). 类的所有成员变量支持赋值运算符.
(3). 类型不含移动拷贝构造函数(实际验证,类型包含移动拷贝构造时,无法合成赋值运算符).

#include <iostream>
class T
{
public:
	T(){}
private:
    T& operator=(T&){}
};

class A
{
public:
  
private:
    T t;
};

int main()
{
    A a1;
    A a2;
    a2 = a1;// err,因为A的成员t不支持赋值运算符(t的赋值运算符对A不可见)
    return 0;
}

(3).显式请求合成版本
假设类型是A

class A{
public:
	A& operator=(const A&) = default;
	A& operator=(A&&) = default;
};

4.关于类的析构

4.1.析构顺序

#include "test.h"
int main()
{
    A a;
    return 0;
}

在这里插入图片描述
上述蓝色1,2,3对应A a;中实例a的析构过程。
析构函数执行顺序为:
(1).类自身的析构函数体
(2).成员变量按类内出现顺序反向析构
(3).基类析构
上述定义一个递归式的定义。

不像构造函数,析构函数没有初始值列表这样的东西。每个类型的析构函数也不存在多个版本。

4.2.合成版本
a.何时合成
只要用户没有自定义析构函数,编译器就会合成析构函数。

b.合成版本行为表现
(1). 类自身的析构函数体。合成版本函数体为空。
(2). 成员变量按类内出现顺序反向析构
(3). 基类析构
上述定义一个递归式的定义。

c.合成版本可行前提
(1). 类的基类支持析构
(2). 类的成员支持析构

4.3.显式请求合成版本
析构函数严格来说没有显式请求的必要,因为只要用户没有定义析构函数,编译器总是会提供合成的析构函数。

  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

raindayinrain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值