• 【C++】类和对象 (下篇)


    一、初始化列表

    1、概念

    类和对象中篇 中我们学习了C++的六个默认成员函数,其中构造函数用于对对象进行初始化,即在创建对象时,编译器会自动调用构造函数,给对象中各个成员变量一个合适的初始值;

    class Date
    {
    public:
    	Date(int year = 1970, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    虽然上述构造函数调用之后,对象个成员变量中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数函数体中的语句只能将其称为赋初值,而不能称作初始化;因为初始化只能初始化一次,而构造函数体内可以进行多次赋值;那么成员变量在哪里初始化呢?

    另外,我们知道类里面只是成员变量的声明,并不是成员变量的定义,因为类并不会在内存中占用空间;而只有当我们用类实例化出具体的对象时才会对成员变量进行定义;而对象是整体定义的,那么对象中具体的每一个成员变量又在哪里定义呢?

    C++类对象中的成员变量在初始化列表处进行定义与初始化初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式

    class Date
    {
    public:
    	Date(int year = 1970, int month = 1, int day = 1)
    		: _year(year)  //初始化列表
    		, _month(month)
    		, _day(day)
    	{}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    image-20221014145221368

    2、特性

    初始化列表有如下几个特性特性:

    1、初始化列表是每个成员变量定义和初始化的地方,所以每个成员变量 (内置类型和自定义类型) 都一定会走初始化列表,无论我们是否在初始化列表处写;并且初始化操作只能进行一次;

    2、如果我们在初始化列表写了编译器就会用显式写的来初始化;如果我们没有在初始化列表写,那么对于内置类型,编译器会使用随机值来初始化,对于自定义类型,编译器会调用自定义类型的默认构造函数来初始化,如果没有默认构造编译器就会报错;

    下面我们来验证上面的两点特性:

    image-20221014150604398

    image-20221014150834573

    对于内置类型 _day,如果没有显式在初始化列表初始化,编译器会使用随机值来初始化;而对于自定义类型 aa,如果没有显式定义编译器会调用自定义类型的默认构造函数来初始化;

    image-20221014151307360

    如果自定义类型既没有在初始化列表显式定义,也没有默认构造函数,编译器就会报错;

    3、如果类中包含以下成员,则必须放在初始化列表初始化:

    • 引用成员变量;
    • const 成员变量;
    • 没有默认构造的自定义类型;

    在前面的学习中我们知道,引用是一个变量的别名,它必须在定义的时候初始化,并且一旦引用了一个变量就不能再去引用另一个变量;同样,const 作为只读常量,也必须在定义的时候初始化,且初始化之后不能在其他地方修改

    而通过上面的学习,构造函数函数体内执行的是赋值语句,成员变量只能在初始化列表进行定义与初始化:image-20221014152642334

    所以对于使用 const 修饰的以及引用类型的成员变量,我们必须在初始化列表处对其进行初始化:image-20221014152923817

    同样,对于没有默认构造函数的自定义类型来说,我们也必须在初始化列表处对其进行初始化,否则编译器就会报错,我们以MyQueue为例:

    class Stack
    {
    public:
    	Stack(int capacity)
    		:_top(0)
    		, _capacity(capacity)
    	{
    		_a = (int*)malloc(sizeof(int) * capacity);
    		if (_a == nullptr)
    		{
    			perror("malloc fail\n");
    			exit(-1);
    		}
    	}
    
    	~Stack()
    	{
    		free(_a);
    		_a = NULL;
    		_top = _capacity = 0;
    	}
    
    	void Push(int x)
    	{
    		_a[_top++] = x;
    	}
    
    private:
    	int* _a;
    	int _top;
    	int _capacity;
    };
    
    class MyQueue
    {
    public:
    	MyQueue()
    	{};
    
    	void Push(int x)
    	{
    		_pushST.Push(x);
    	}
    
    	Stack _pushST;
    	Stack _popST;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    image-20221014153522469

    可以看到,我们这里的 Stack 类提供的是带参构造,并没有给缺省值,所以如果我们不在 MyQueue 构造函数的初始化列表中对 _pushST 与 _popST 进行初始化,编译器会直接报错;image-20221014154004817

    另外,从 Stack 的构造函数中可以看到,构造函数的初始化列表与函数体是可以配合使用的,即可以让始化列表和函数体分别完成一部分工作;

    4、尽量使用初始化列表初始化,因为无论我们否使用初始化列表,类的成员变量都会先使用初始化列表进行初始化;

    例如 MyQueue 类 (此处的 Stack 具有默认构造函数):image-20221014154718428

    我们可以看到,即使我们显式定义的构造函数什么也没有写,_pushST 和 _popST 也完成了初始化工作,因为无论我们是否在初始化类比处显示写,类的成员变量都会走初始化列表,其中类的自定义类型会调用它的默认构造来完成初始化工作;

    5、C++11中对于内置类型打的补丁 – 内置类型成员变量可以在声明的时候给定一个缺省值,其在初始化列表处起作用;

    我们之前在学习构造函数时,因为不知道初始化列表的存在,所以认为默认生成的构造函数对内置类型不处理,而C++11为了弥补这个缺陷打了一个补丁,即可以在声明的时候给一个缺省值;但现在我们知道了,内置类型也会在初始化列表进行初始化,只是因为初始化的是一个随机值,所以好像没有初始化一样;而C++11这个补丁就是在初始化列表处生效的;image-20221014160050177

    6、成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关;

    如下:

    class A
    {
    public:
    	A(int a)
    		:_a1(a)
    		, _a2(_a1)
    	{}
    
    	void Print() {
    		cout << _a1 << " " << _a2 << endl;
    	}
    
    private:
    	int _a2;
    	int _a1;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    image-20221014160308039

    由于在类中 _a2 的声明在 _a1 之前,所以在初始化列表处 _a2(_a1) 语句被先被执行,而此时 _a1 还是一个随机值,所以最终 _a2 输出随机值;


    二、隐式类型转换

    1、概念

    隐式类型转换是指当两个不同类型的变量之间进行运算时,编译器会自动将其中一个变量的类型转换为另一个变量的类型;在之前 C++引用 的常引用中我们也接触过,比如:

    int main()
    {
    	int a = 0;
    	double b = a;
    	const double& rb = a;
        const int& c = 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如上,如果我们将 int 的 a 赋值给 double 的 b,则 a 不会被直接赋值给 b,编译器会先根据 a 创建一个 double 类型的临时变量 tmp,然后将 tmp 赋值给 b;

    对于 rb 来说也是一样的,只不过 rb 是引用类型,而引用和指针需要考虑权限的问题,所以用引用类型的变量 rb 去引用 a 生成的临时变量 tmp 需要使用 const 修饰 (临时变量具有常性);

    对于最后一条语句来说也大同小异 – 由于数字 1 只存在于指令中,在内存中并不占用空间,所以当我们对其进行引用时,10会先赋给一个临时变量,然后我们再对这个临时变量进行引用;同时由于临时变量具有常性,所以我们需要使用 const 修饰;

    上面这些知识在我前面 C++引用 的常引用部分有详细的介绍,如果有遗忘的小伙伴可以去复习一下;

    2、构造函数的类型转换

    在C++98中,单参数的构造函数也支持隐式类型转换,即对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,其具有类型转换的作用;比如下面这样:

    class Date
    {
    public:
    	Date(int year)
    		:_year(year)
    	{
    		cout << "Date 构造" << endl;
    	};
    
    	Date(const Date& d)
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    		cout << "Date 拷贝构造" << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    image-20221014211934254

    image-20221014211126263

    如上,对于具有单参构造函数的Date类,我们不仅可以使用构造和拷贝构造的方式来实例化对象,还可以通过直接赋值一个整数来实例化对象;

    而这其实是隐式类型转换的结果:

    对于d3来说,由于2022和d3的类型不同,所以编译器会进行类型转换,即先使用2022来构造一个临时的Date对象,然后用这个临时对象来对d3进行拷贝构造,所以d3是构造+拷贝构造的结果;

    不过现在比较新版的编译器都对这种情况进行了优化,不再创建临时的Date对象,而是直接使用2022来构造d3,所以我们看到的现象是创建d3没有调用拷贝构造函数;而在老版的编译器下是会调用拷贝构造函数的,比如VC 6.0、VS2003等编译器;

    对于d4来说,d4是Date对象的引用,所以编译器会先用2022来构造一个Date类型的临时对象,然后d4再对这个临时对象进行引用,所以只会调用一次构造函数;同时由于临时对象具有常性,所以需要使用const修饰;

    注意:单参构造函数并不是指只能有一个参数,而是指在调用时只传递一个参数,所以半缺省和全缺省的构造函数也是可以的;image-20221014212542928

    C++11对上述语法进行了拓展,支持多参数的构造函数,只是传递参数时多个参数之间需要使用花括号,如下:image-20221014212940713

    image-20221014213037044

    3、explicit 关键字

    explicit 关键字用于修饰构造函数,其作用是禁止构造函数的隐式类型转换,如下:

    class Date
    {
    public:
    	explicit Date(int year = 1970, int month = 1, int day = 1)
    		:_year(year)
    		, _month(month)
    		, _day(day)
    	{
    		cout << "Date 构造" << endl;
    	};
    
    	Date(const Date& d)
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    		cout << "Date 拷贝构造" << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    image-20221014213245875

    4、类型转换的意义

    构造函数的类型转换在一些特定的地方有着很大的意义,比如要在一个顺序表尾插一个元素,而这个元素是 string 类型的对象,如下:

    int main()
    {
    	string s1("hello");
    	push_back(s1);
    
    	push_back("hello");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如上,有了隐式类型转换我们在插入一个 string 对象时就不必先去构造 s1,然后再传递 s1,而是直接使用 “hello” 做参数即可,其会自动转化为 string 类型;等等类似的情景还有非常多,具体的我们在后面遇到再说;


    三、static 成员

    1、概念

    声明为 static 的类成员称为类的静态成员,其中用 static 修饰的成员变量,称之为静态成员变量,用 static 修饰的成员函数,称之为静态成员函数;下面我们以一个面试题来引出类的静态成员的相关知识点;

    面试题:实现一个类,计算程序中创建出了多少个类对象;

    我们知道,类创建对象一定会调用构造函数或者拷贝构造函数,所以我们只需要定义一个全局变量,然后在构造函数和拷贝构造函数中让其自增即可,如下:

    int N = 0;
    class A
    {
    public:
    	A(int i = 0)
    		:_i(i)
    	{
    		N++;
    	}
    
    	A(const A& a)
    	{
    		_i = a._i;
    		N++;
    	}
    
    private:
    	int _i;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    image-20221014223844925

    虽然使用全局变量的方法可以十分简便的达到我们的目的,但是我们不建议使用全局变量,因为全局变量可以被任何人修改,十分不安全;所以我们需要使用另外一种比较安全的方法 – 静态成员变量;

    2、static 成员变量

    静态成员变量是指用 static 关键字修饰的成员变量,其特性如下:

    • 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
    • 静态成员变量必须在类外定义,定义时不添加 static 关键字,类中只是声明;
    • 静态成员变量的访问受类域与访问限定符的约束;

    接下来我们围绕这三点特性来展开说明:

    1、由于静态成员变量在静态区 (数据段) 开辟空间,并不在对象里面,所以它不属于某单个对象,而是所有对象共享;

    class A
    {
    public:
    	A(int m = 0)
    		:_m(m)
    	{}
    
    public:
    	int _m;
    	static int _n;
    };
    
    int A::_n = 0;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    image-20221015143647401

    可以看到,当我们把类的静态成员变量设置为 public 并在类外进行定义初始化后,我们可以直接通过类名+域作用限定符或者通过一个空指针对象来访问,这说明_n并不存在于对象里面;

    2、类的静态成员变量在类中只是声明,必须在类外进行定义且定义时需要指定类域,其不在初始化列表处进行定义初始化,因为新建对象并不会改变它的值;

    image-20221015144614899

    image-20221015144745372

    tips:当我们的程序出现错误时,输出列表提供的错误信息是最准确的,且我们应该从第一个错误开始解决;

    3、静态成员变量的访问受类域与访问限定符的约束;

    image-20221015145015427

    image-20221015145108004

    如上,静态成员变量在访问时和普通的成员变量区别不大,同样受类域和访问限定符的约束,只是因为其不存在于对象中,所以我们可以通过 A:: 来直接访问;

    注:可以看到,静态成员变量在定义声明的时候只受类域的限制,而没有受到访问限定符的限制,这是一个特例,大家记住即可;

    学习了静态成员变量的相关知识以后,我们就可以换一种方式来统计对象的创建个数了:

    class A
    {
    public:
    	A(int i = 0)
    		:_i(i)
    	{
    		_n++;
    	}
    
    	A(const A& a)
    	{
    		_i = a._i;
    		_n++;
    	}
    
    private:
    	int _i;
    	static int _n;
    };
    
    int A::_n = 0;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    image-20221015150059968

    但是这里出现了一个新的问题:为了保证类的封装性我们需要将成员变量设置为private,但是这样又使得我们无法在类外获取到它们,那该怎么办呢?针对这个问题,C++设计出了静态成员函数;

    3、static 成员函数

    静态成员函数是指用 static 关键字修饰的成员函数,其特性如下:

    • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
    • 静态成员也是类的成员,同样受类域和访问限定符的约束;

    由于静态成员函数没有隐藏的 this 指针,所以我们在调用的时候自然也就不需要传递对象的地址,即我们可以通过类名+域作用限定符直接调用,而不需要创建对象;但是相应的,没有了 this 指针我们也无法去调用非静态的成员变量与成员函数,因为非静态成员变量需要实例化对象来开辟空间,非静态成员函数的调用则需要传递对象的地址;

    class A
    {
    public:
    	A(int i = 0)
    		:_i(i)
    	{
    		_n++;
    	}
    
    	A(const A& a)
    	{
    		_i = a._i;
    		_n++;
    	}
    
    	static int GetN()
    	{
    		return _n;
    	}
    
    private:
    	int _i;
    	static int _n;
    };
    
    int A::_n = 0;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    image-20221015151637942

    注意:虽然静态成员函数函数不可以调用非静态成员,但是非静态成员函数是可以调用静态成员的 (调用静态成员时编译器不传递对象地址即可);

    最后,让我们来做一道与静态成员相关的练习题:求1+2+3+…+n

    求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

    class Sum
    {
    public:
        Sum()
        {
            ++_i;
            _ret += _i;
        }
    
        static int GetRet()
        {
            return _ret;
        }
    
    private:
        static int _i;
        static int _ret;
    };
    
    int Sum::_i = 0;
    int Sum::_ret = 0;
    
    class Solution {
    public:
        int Sum_Solution(int n) {
            Sum arr[n];
            return Sum::GetRet();
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    四、友元

    1、输入输出的重载

    在C++中,我们使用 cin 和 cout 配合流插入 >> 与流提取 << 符号来完成数据的输入输出,并且它们能自动识别内置类型;image-20221015153800138

    那么它们是如何做到输入与输出数据以及自动识别内置类型的呢?答案是运算符重载与函数重载;

    image-20221015155008573

    image-20221015154944901

    可以看到,cin 和 cout 分别是 istream 和 ostream 类的两个全局对象,而 istream 类中对流提取运算符 >> 进行了运算符重载,osteam 中对流插入运算符 << 进行了运算符重载,所以 cin 和 cout 对象能够完成数据的输入输出;同时,istream 和 ostream 在进行运算符重载时还进行了函数重载,所以其能够自动识别数据类型;

    那么,对于我们自己定义的自定义类型,我们也可以对 << 和 >> 进行运算符重载,使其支持输入与输出数据;我们以Date为例:

    class Date
    {
    public:
    	Date(int year = 1970, int month = 1, int day = 1)
    		:_year(year)
    		, _month(month)
    		, _day(day)
    	{}
    
    	//流插入
    	ostream& operator<<(ostream& out) const
    	{
    		cout << _year << "/" << _month << "/" << _day;
    		return out;
    	}
    
    	//流提取
    	istream& operator>>(istream& in)
    	{
    		in >> _year;
    		in >> _month;
    		in >> _day;
    		return in;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    image-20221015160045738

    但是这里有一个问题:如果运算符重载为类的成员函数,那么运算符的左操作数必须是本类的对象,因为 this 指针的类型是本类的类型,也就是说,如果我们要把 << >> 重载为类的成员函数,那么本类的对象 d 就必须是做操作数,即我们必须像下面这样调用:image-20221015160546002

    但是这样显然违背了我们的初衷 – 我们进行运算符重载的目的是提高程序的可读性,而上面这样很可能会给函数的使用带来很大的困扰;所以对于 << >> 我们只能重载为全局函数;

    但是重载为全局函数又会出现一个新的问题 – 在类外部无法访问类的私有数据;但是我们又不可能将类的私有数据改为共有,这样代价太大了,那么有没有一种办法可以在类外直接访问类的私有成员呢?有的,它就是友元;

    2、友元函数

    友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要 friend 关键字;如下:

    class Date
    {
    	//友元声明 -- 可以放置在类的任意位置
    	friend istream& operator>>(istream& in, Date& d);
    	friend ostream& operator<<(ostream& out, const Date& d);
    
    public:
    	Date(int year = 1970, int month = 1, int day = 1)
    		:_year(year)
    		, _month(month)
    		, _day(day)
    	{}
    
    	//流插入
    	ostream& operator<<(ostream& out) const
    	{
    		cout << _year << "/" << _month << "/" << _day;
    		return out;
    	}
    
    	//流提取
    	istream& operator>>(istream& in)
    	{
    		in >> _year;
    		in >> _month;
    		in >> _day;
    		return in;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    //流提取
    inline istream& operator>>(istream& in, Date& d)
    {
    	in >> d._year;
    	in >> d._month;
    	in >> d._day;
    	return in;
    }
    
    //流插入
    inline ostream& operator<<(ostream& out, const Date& d)
    {
    	cout << d._year << "/" << d._month << "/" << d._day;
    	return out;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    image-20221015162126008

    注:1、由于流插入和流提取的重载内容较少,且调用频率很高,所以我们可以把其定义为内联函数;

    2、为了支持连续输入以及连续输出,我们需要将函数的返回值设置为 istream 和 ostream 对象的引用;

    友元函数总结:

    • 友元函数可访问类的私有和保护成员,但不是类的成员函数;
    • 友元函数不能用 const 修饰;
    • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
    • 一个函数可以是多个类的友元函数;
    • 友元函数的调用与普通函数的调用原理相同;

    3、友元类

    C++中除了有友元函数,还有友元类 – 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的私有成员;如下:

    class Time
    {
    	//友元类
    	friend class Date; 
    
    public:
    	Time(int hour = 0, int minute = 0, int second = 0)
    		: _hour(hour)
    		, _minute(minute)
    		, _second(second)
    	{}
    
    private:
    	int _hour;
    	int _minute;
    	int _second;
    };
    
    class Date
    {
    public:
    	Date(int year = 1970, int month = 1, int day = 1)
    		: _year(year)
    		, _month(month)
    		, _day(day)
    	{}
    
    	void SetTimeOfDate(int hour, int minute, int second)
    	{
    		// 可以直接访问Time类私有的成员变量
    		_t._hour = hour;
    		_t._minute = minute;
    		_t._second = second;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    	Time _t;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    image-20221015164103762

    友元类有如下特点:

    • 友元关系是单向的,不具有交换性;比如上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time 类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行;
    • 友元关系不能传递;如果C是B的友元, B是A的友元,则不能说明C是A的友元;
    • 友元关系不能继承,继承的相关知识我们到C++进阶再详细学习;

    五、内部类

    概念:如果一个类定义在另一个类的内部,这个类就叫做内部类;内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员;外部类对内部类没有任何优越的访问权限

    class A
    {
    public:
    	A(int a = 0)
    		:_a(a)
    	{}
    
    	//内部类
    	class B
    	{
    	public:
    		B(int b = 0)
    			:_b(b)
    		{}
    
    	private:
    		int _b;
    	};
    
    private:
    	int _a;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    内部类有如下特性:

    • 内部类天生就是外部类的友元,所以内部类可以通过外部类的对象参数来访问外部类中的所有成员;但外部类不是内部类的友元;
    • 内部类定义在外部类的 public、protected、private 处都是可以的,但是内部类实例化对象时要受到外部类的类域和访问限定符的限制;
    • 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名;
    • 内部类是一个独立的类,它不属于外部类,所以 sizeof (外部类) == 外部类;
    • 内部类在C++中很少被使用,在Java中使用频繁,所以大家只需要了解有这个东西即可;
    class A
    {
    public:
    	A(int a = 0)
    		:_a(a)
    	{}
    
    	//内部类
    	class B
    	{
    	public:
    		B(int b = 0)
    			:_b(b)
    		{}
    
    		void SetA(A& a, int n)
    		{
    			a._a = n;  
    			_i = 1;
    		}
    
    	private:
    		int _b;
    	};
    
    private:
    	int _a;
    	static int _i;
    };
    
    int A::_i = 0;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    image-20221015171440647


    六、匿名对象

    在C++中,除了用类名+对象名创建对象外,我们还可以直接使用类名来创建匿名对象,匿名对象和正常对象一样,在创建时自动调用构造函数,在销毁时自动调用析构函数;但是匿名对象的生命周期只有它定义的那一行,下一行就会立马销毁;

    class A
    {
    public:
    	A(int a = 0)
    		:_a(a)
    	{
    		cout << "A 构造" << endl;
    	}
    
    	~A()
    	{
    		cout << "A 析构" << endl;
    	}
    
    private:
    	int _a;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image-20221015173905804

    匿名对象如下场景下很好用,当然还有一些其他使用场景,这个我们以后遇到了再说:

    image-20221015174318664


    七、编译器的一些优化

    在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的;如下:

    class A
    {
    public:
    	A(int a = 0)
    		:_a(a)
    	{
    		cout << "A 构造" << endl;
    	}
    
    	A(const A& aa)
    		:_a(aa._a)
    	{
    		cout << "A 拷贝构造" << endl;
    	}
    
    	A& operator=(const A& aa)
    	{
    		cout << "A 赋值重载" << endl;
    		if (this != &aa)
    		{
    			_a = aa._a;
    		}
    		return *this;
    	}
    
    	~A()
    	{
    		cout << "A 析构" << endl;
    	}
    private:
    	int _a;
    };
    
    //传值传参
    void f1(A aa) {}
    
    //传值返回
    A f2()
    {
    	A aa;
    	return aa;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    优化场景1:传参隐式类型转换 – 构造+拷贝构造 --> 直接构造

    image-20221015175540042

    我们调用 f1 函数,并使用1作为参数,由于1和f1的形参不同,所以会发生隐式类型转换,即编译器会先用1去构造一个A类型的临时变量,然后用这个临时变量去拷贝构造aa,所以这里本来应该是构造+拷贝构造,但是编译器将其优化为了直接使用1去构造aa;

    优化场景2:匿名对象 – 构造+拷贝构造 --> 直接构造

    image-20221015180446029

    和场景1类似,本来是先用2来构造一个匿名对象,然后使用这个匿名对象来拷贝构造aa,经过编译器优化后变为直接使用2去构造aa;

    优化场景3:传值返回 – 构造+拷贝构造+拷贝构造 --> 直接构造

    image-20221015181429100

    f2 函数返回的是局部的匿名对象,所以编译器会先用匿名对象去拷贝构造一个临时对象,然后再用临时对象来拷贝构造aa2,而编译器优化后变为直接使用无参来构造aa2;即构造+拷贝构造+拷贝构造优化为直接构造;

    上面就是编译器一些优化的场景,作为一个程序员来说,我们应该主动去触发编译器的这些优化场景,从而提高我们的程序效率;

    注:编译器只能对一句表达式中的某些操作进行优化,而不能将两句表达式优化成一句,因为这样可能会使程序发生错误;


    八、再次理解类和对象

    现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:

    1. 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程;
    2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中;
    3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西;
    4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了;

    在类和对象阶段,大家一定要体会到:类是对某一类实体 (对象) 来进行描述的,描述该对象具有哪些属性,哪些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象image-20221015182704132


  • 相关阅读:
    传统瀑布模型和实际瀑布模型
    2.7 PE结构:重定位表详细解析
    如何才能避免辛苦开发出来的产品惨遭市场冷遇?
    C++位图—布隆过滤器
    Pytorch框架学习记录8——最大池化的使用
    大神方案|如何重写一个万行代码的类文件
    聚鑫数藏平台——引领数字资产管理新风向
    OLED显示模块的工作原理、结构特点、应用领域和发展趋势
    【运维项目经历|026】Redis智能集群构建与性能优化工程
    C++ Reference: Standard C++ Library reference: Containers: deque: deque: rbegin
  • 原文地址:https://blog.csdn.net/m0_62391199/article/details/127340956