【c++笔记】类和对象流食般投喂(下)

声明:以下知识相关资料来自比特官网和小编手搓~
类和对象(下)

++1、再探构造函数++
++2、类型转换++
++3、static成员++
++4、友元++
++5、内部类++
++6、匿名对象++
++7、对象拷贝时的编译器优化++

1、再探构造函数

在之前 "类和对象流食般投喂(中)" 这篇文章里面,我们已经介绍了关于构造函数的80%的知识点了,在此篇文章里面,我们投喂一下剩下的20%流食 --- 初始化列表。

完成对类实例化出对象时的初始化,这个活初始化列表也能干,而且他更加的全面,所以在 "用户不显示实现构造函数且编译器自动生成的默认构造函数不能满足对象的初始化" 这个场景时,我们就会用到初始化列表,不过还是建议能写初始化列表就写上,能兜底。

初始化列表用法&特点:

1、初始化列表的写法是以冒号起手,然后跟着一个以逗号间隔的列表,插在函数名与函数体之间的位置,列表中每个"成员变量"后面跟着一个括号,括号里面可以是一个初始值或者一个表达式

2、初始化列表本质是类实例化出的对象被初始化定义的地方,所以只能初始化一次,成员变量只能在列表出现一次

3、C++规定,引用成员变量、const成员变量、没有默认构造的类类型变量,都必须在初始化列表初始化,否则会编译报错

4、C++11规定,成员变量声明处可以使用缺省值,这个设定是给未显示在初始化列表的成员变量使用的,他会调用缺省值进行对象的初始化

5、初始化列表的初始化顺序,是按照成员变量的声明顺序进行初始化的,跟初始化列表里面的排序无关,但建议列表的成员变量排序和成员变量的声明顺序保持一致

初始化列表小总结:

1、用户是否显示实现初始化列表,每个构造函数都有初始化列表(毕竟一个对象的创建,肯定会有一个定义的地方,之前只知道声明在哪,现在知道掌管定义的地方就是初始化列表)

2、成员变量是否显示实现在初始化列表中,成员变量的初始化都会调用初始化列表
成员变量走初始化列表的逻辑:

1、显示实现初始化列表

2、未显示实现初始化列表

2.1、成员变量声明处有缺省值

2.2、成员变量声明处无缺省值

2.2.1、内置类型成员变量无明确规定

2.2.2、自定义类型成员变量调用他自己的默认构造函数

2.2.3、引用成员变量/const成员变量/没有默认构造的成员变量,编译报错

cpp 复制代码
//引用成员变量/const成员变量/没有默认构造的成员变量,必须写在初始化列表中/不写但有缺省值
//不满足:会报错

class Date
{
public:
    Date(int& x, int year = 1, int month = 1, int day = 1)
        :_year(year)
        ,_month(month)
        ,_day(day)
        ,_t(12)
        ,_ref(x)
        ,_n(1)
    {
        // error C2512: "Time": 没有合适的默认构造函数可⽤
        // error C2530 : "Date::_ref" : 必须初始化引⽤
        // error C2789 : "Date::_n" : 必须初始化常量限定类型的对象
    }

    void Print() const
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;

    Time _t;         // 没有默认构造
    int& _ref;       // 引⽤
    const int _n;    // const
};
cpp 复制代码
class Date
{
public:

private:
    // 注意这⾥不是初始化,这⾥给的是缺省值,这个缺省值是给初始化列表的
    // 如果初始化列表没有显⽰初始化,默认就会⽤这个缺省值初始化
    int _year = 1;
    int _month = 1;
    int _day;

    Time _t = 1;
    const int _n = 1;
    int* _ptr = (int*)malloc(12);
};

来个小题目:

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
    A(int a)       // a = 1
        :_a1(a)    // 再用a初始化_a1,_a1 = 1
        , _a2(_a1) // 先用_a1初始化_a2,但是此时_a1还没有初始化,是个随机值
    {}

void Print() 
{
    cout << _a1 << " " << _a2 << endl;
}

private:
    int _a2 = 2; //先初始化_a2
    int _a1 = 2; //再初始化_a1
};

int main()
{
    A aa(1);
    aa.Print();
}

2、类型转换

在之前的投喂中,我们可以察觉出,编译器总是将数据分成内置类型和自定义类型两类,来对成员变量进行分析,所以两个大类其实是有一定割裂的,但是C++为了连贯便利,支持了内置类型隐式转换成类类型对象,不过需要有相关内置类型为参数的构造函数。在构造函数之前加上explicit就不再支持隐式类型转换了。类类型对象之间也支持隐式类型转换,同样需要相应的构造函数支持。

cpp 复制代码
class A
{
public:
    // 构造函数explicit就不再⽀持隐式类型转换
    // explicit A(int a1)
    A(int a1)
        :_a1(a1)
    {}
    
    //explicit A(int a1, int a2)
    A(int a1, int a2)
        :_a1(a1)
        , _a2(a2)
    {}

int Get() const
{
    return _a1 + _a2;
}

private:
    int _a1 = 1;
    int _a2 = 2;
};
cpp 复制代码
class B
{
public:
    B(const A& a)
        :_b(a.Get())
    {}

private:
    int _b = 0;
};
cpp 复制代码
int main()
{
    // 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3
    // 编译器遇到连续构造+拷⻉构造->优化为直接构造
    A aa1 = 1;
    aa1.Print();
    
    const A& aa2 = 1;
    
    // C++11之后才⽀持多参数转化
    A aa3 = { 2,2 };

    // aa3隐式类型转换为b对象
    // 原理跟上⾯类似
    B b = aa3;
    const B& rb = aa3;  // aa3是A类,转换成B类会创建一个临时变量,
                        // 编译器自动帮你做的:
                        //B temp(aa3); 
                        // 调用B的转换构造函数,生成临时对象temp
                        //把 const 引用绑定到这个临时对象,const B& rb 就绑定到了temp上
                        //const 引用绑定临时对象时,C++ 标准有一个特殊规则:临时对象的生命周期会被延长到和引用的生命周期一致。
//原本A类的对象转成B类类型,存在于一个临时变量里面,生命周期只在当前行,const引用将其生命延长
        
    return 0;
}

3、static成员

用static修饰的成员变量,被称为静态成员变量,他的生命周期和全局变量一样,同是放在静态区,在域的定位中是放在全局域的,由此看出,静态成员变量是被所有类对象所共享的,不属于某一个对象,不存在对象中,因而静态成员变量的初始化是不走类的构造函数初始化列表的那一套的,他的初始化必须是在类之外进行,所以声明处就给不了缺省值了。

用static修饰的成员函数,被称为静态成员函数,C++规定静态成员函数是没有this指针的,因而他只能访问同为静态的静态成员,而不能访问非静态的成员,因为没有this指针,但是非静态的成员函数没有限制,静态/非静态都可以访问。

全局变量像是正常成年人,静态成员变量像是背负了债务的正常成年人,受债务限制,你想用他,你得帮他还债,使他摆脱枷锁;这里的静态成员也是类的成员,所以是受public/protected/private访问限定符限制的,用户想使用,就要突破类域,也就是解除成员的枷锁,帮他还债,此时,还债突破枷锁的方式有:类名::静态成员/对象.静态成员。

cpp 复制代码
class A
{
public:

private:
    // 类⾥⾯声明
    static int _scount; //不可以给缺省值,因为不走类构造函数初始化列表的那一套
};

// 类外⾯初始化
int A::_scount = 0;
cpp 复制代码
int main()
{
    // 编译报错:error C2248: "A::_scount": ⽆法访问 private 成员(在"A"类中声明)
    //cout << A::_scount << endl;

    return 0;
}

4、友元

友元是一种突破类访问限定符封装的方式,细分为友元函数和友元类,使用方式是在函数/类的声明前加上一个friend,然后放在一个类中的任意位置,不受访问限定符约束,比较自由,这样使用后,这个类传递了这个外部函数是我这个类的朋友,我邀请他到我内部玩,他什么都可以把玩,不限制(外部友元函数可以访问类中私有和保护的成员)。

友元函数仅仅是声明,写在类里面并不是类的成员函数。

一个函数可以是多个类的友元函数(友元函数是:类在内部喊话:"这个外部函数是我的朋友" 的行为的代名词),也就是说,现有三个人,我可以是每个人的朋友。

友元类:类在内部喊话:"这个外部类是我的朋友";友元类里面的函数,也可以看成是友元函数,(可以把友元类当成所有友元函数喊话的集合)。

友元关系都是单向的,可以类比单相思,11喜欢22,22喜不喜欢11,得听22承认;友元关系也不具有传递性,11喜欢22,22喜欢33,但是11喜不喜欢33是未知的。

友元可以提供便利,但增加耦合度(两个模块的绑定深度),破坏封装,不宜多用。
访问私有成员变量的问题:

cpp 复制代码
有⼏种⽅法可以解决:
1、成员放公有
2、Date提供getxxx函数
3、友元函数
4、重载为成员函数
cpp 复制代码
// 前置声明,都则A的友元函数声明编译器不认识B
class B;

class A
{
    // 友元声明
    friend void func(const A& aa, const B& bb);
private:
    int _a1 = 1;
    int _a2 = 2;
};

class B
{
    // 友元声明
    friend void func(const A& aa, const B& bb);
private:
    int _b1 = 3;
    int _b2 = 4;
};
cpp 复制代码
class A
{
    // 友元声明
    friend class B;
private:
    int _a1 = 1;
    int _a2 = 2;
};

5、内部类

内部类:天生自带双向访问权限 ,不用写任何 friend,房子管家人,家人随便用房子,双向互通无限制 内部类和外部类是互相全开权限。

内部类是把一个独立的类从原本在全局写,移到了在一个类的类域里面写,只是多了空间壁垒和访问限制,所以内部类是不包含在外部类实例化出的对象中的。

内部类默认是外部类的友元类。

内部类本质也是一种封装,当A类和B类的耦合度很高时,A类实现出来就是给B类用的,可以把A类设计成内部类,此时已经有了空间壁垒了,还想再突出专属特性,可以把内部类写在private/protected的位置。

cpp 复制代码
class A
{
private:
    static int _k;
    int _h = 1;
public:
    class B // B默认就是A的友元
    {
    public:
        void foo(const A& a)
        {
            cout << _k << endl; //OK
            cout << a._h << endl; //OK
        }
        int _b1;
    };
};

int A::_k = 1;

6、匿名对象

匿名对象的使用方法:类型(实参)

之前有名对象的使用方法:类型 对象(实参)

匿名对象和拷贝时创建的临时对象的定位是未命名对象,他们的生命周期都只是在当前一行。

适用场景:一般临时定义一个对象在当前一行使用,就可以用匿名对象。

cpp 复制代码
class Solution 
{
public:
    int Sum_Solution(int n) 
    {
        //...
        return n;
    }
};

int main()
{
    // 不能这么定义对象,因为编译器⽆法识别下⾯是⼀个函数声明,还是对象定义
    //A aa1();

    // 但是我们可以这么定义匿名对象,匿名对象的特点不⽤取名字,
    // 但是他的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数
    A();
    A(1);

    A aa2(2);

    // 匿名对象在这样场景下就很好⽤,当然还有⼀些其他使⽤场景,这个我们以后遇到了再说
    Solution().Sum_Solution(10);

    return 0;
}

7、对象拷贝时的编译器优化

现代编译器为了追求效率,会在确保正确性的前提下,对于传值/返回值的拷贝会进行省略。

编译器的不同,版本的不同,优化的程度也是不同的,VS2019的debug相对温和,VS2019的realse有点激进,而VS2022的debug直接用的就是VS2019realse那版了。

主流编译器对于连续的拷贝会进行合并优化,更"激进"的编译器还支持跨行跨表达式的合并优化

相关推荐
熬夜敲代码的猫2 小时前
教你如何使用set和map
c++·算法
凉、介9 小时前
Armv8-A virtualization 笔记 (二)
笔记·学习·嵌入式·arm·gic
智者知已应修善业10 小时前
【ICL8038芯片正弦波三角波方波发生器电路】2024-1-5
驱动开发·经验分享·笔记·硬件架构·硬件工程
踩着两条虫10 小时前
「AI + 低代码」的可视化设计器
开发语言·前端·低代码·设计模式·架构
JoneBB10 小时前
ABAP Webservice连接
运维·开发语言·数据库·学习
探序基因11 小时前
身高与基因的关系
笔记
即使再小的船也能远航11 小时前
【Python】安装
开发语言·python
Irissgwe11 小时前
类与对象(三)
开发语言·c++·类和对象·友元
️是7811 小时前
信息奥赛一本通—编程启蒙(3395:练68.3 车牌问题)
数据结构·c++·算法