【C++初阶】六、类和对象(初始化列表、static成员、友元、内部类)
=========================================================================
相关代码gitee自取:
C语言学习日记: 加油努力 (gitee.com)
=========================================================================
接上期:
【C++初阶】五、类和对象
(日期类的完善、流运算符重载函数、const成员、“&”取地址运算符重载)-CSDN博客
=========================================================================
目录
一 . 初始化列表
构造函数体内赋值
初始化列表的作用和使用
初始化列表的作用:
初始化列表的使用:
补充:explicit关键字
二 . static成员
static成员概念:
static成员特性:
三 . 友元
友元函数
友元类
四 . 内部类
内部类的概念:
内部类的特性:
补充:拷贝对象时的一些编译器优化
补充:调用拷贝构造函数还是“=”赋值运算符重载函数
优化一:“未初始化对象 = 内置类型”
优化二:“通过匿名对象调用函数”
优化三:“通过内置类型调用函数”
优化四:“未初始化对象接收函数传值返回的临时对象”
本篇博客相关代码:
Test.cpp文件 -- C++文件:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
一 . 初始化列表
构造函数体内赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值,
在调用完构造函数,执行完构造函数体内的赋值后,虽然对象中已经有了一个初始值,
但是还不能将其称为对对象中成员变量的初始化,
构造函数体中的语句只能将其称为赋初值,而不能称为初始化。
因为初始化只能初始化一次,而构造函数体内是可以多次赋值的
初始化列表的作用和使用
初始化列表的作用:
- 初始化列表的作用可以简单理解成:在成员变量定义时先对其进行初始化
(先执行初始化列表,再执行构造函数体中的内容)
- 当一个类的成员变量中有:
引用变量类型、const成员变量、自定义类型成员(且该类中没有默认构造函数时)
当有这三类成员变量时,就必须要使用到初始化列表了
- 对于成员变量中的: 引用变量类型 和 const成员变量,
这两种成员变量都要求在定义时就得被初始化,构造函数体内赋值无法满足这个条件;
- 而对于成员变量中的:自定义类型成员(且该类中没有默认构造函数时),
这种成员变量初始化时编译器通常会调用其默认构造函数,
但因为没有默认构造函数,只有有参构造函数,
所以需要在编译器调用默认构造函数前,
就先通过初始化列表调用其有参构造函数进行初始化,
这也是构造函数体内赋值无法实现的
---------------------------------------------------------------------------------------------
初始化列表的使用:
- 使用格式:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,
每个“成员变量”后面跟一个放在括号中的初识值或表达式
注意事项:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置上进行初始化:
引用成员变量、const成员变量、自定义类型成员(且该类没有默认构造函数时)图示:
- 尽量使用初始化列表进行初始化,因为不管你是否使用初始化列表,
对于自定义类型成员变量,一定会先使用初始化列表初始化
- 成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,
与其在初始化列表中的先后次序无关,
所以最好将成员变量的声明次序和其在初始化列表中的先后次序保持一致图示:
补充:explicit关键字
- 构造函数不仅可以构造和初始化对象,
对于单个参数或者除第一个参数无缺省值其余均有缺省值的构造函数,
还具有隐式类型转换的作用
- 使用这种隐式类型转换时,代码可读性可能不是很好,
有些地方可能不允许出现这种隐式类型转化的情况发生,
这时就可以使用 explicit关键字 来修饰该构造函数,这样就会禁止构造函数的隐式转换图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
二 . static成员
static成员概念:
声明为static的类成员称为类的静态成员,
用static修饰的成员变量称为静态成员变量;用static修饰的成员函数称为静态成员函数。
其中静态成员变量是在类中声明,在类外初始化的(实现 / 定义)
---------------------------------------------------------------------------------------------
static成员特性:
- 静态成员是所有类对象共享的,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外初始化(实现 / 定义),
定义时不用添加static关键字,类中只是声明
- 类静态成员可以使用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,所以不能访问任何非静态成员,
但非静态成员是可以访问类的静态成员函数的,
因为非静态成员可以找到其对应的类,通过类就能找到类中的静态成员函数
(只要有办法找到静态成员函数的类域,就能访问其中的静态成员函数)
- 静态成员也是类的成员,
所以也会受到 public 、protected 、private 访问限定符的限制图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
三 . 友元
友元提供了一种突破封装的方式,有时能够提供便利。
但是友元会增加代码的耦合度,一定程度上破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
友元函数
上期博客中我们对 “<<” 和 “>>” 两个流运算符进行了重载,
实现了 “<<”流运算符重载函数 和 “>>”流运算符重载函数 ,实现过程中,
我们发现无法将两者实现在类中,重载定义为成员变量,
因为这两个流运算符对左操作数有一定的要求,
其左操作数一般为 cout输出流对象 或 cin输入流对象,
如果将其重载为成员函数的话,
成员函数隐藏的this指针就会抢占其左操作数的位置(第一个参数的位置)
- 所以要将这两个流运算符重载为全局函数,
但这时又会导致类外的全局函数无法访问类中的私有成员变量,
此时就需要通过友元来解决该问题
- 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,
不属于任何类,但是需要在类的内部进行声明,声明时需要加 friend关键字
- 友元函数可以访问类中的私有(private)和保护(protected)成员,
但其不是类的成员函数
- 友元函数不能用const进行修饰
- 友元函数可以在类定义的任何地方进行声明,且其不受类访问限定符的限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用和普通函数的调用原理相同
图示:
友元类
友元类的所有成员函数都可以是另一个类的友元函数,
都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性
如:假设有A类和B类,在A类中声明B类为其友元类,
那么就可以在B类中直接访问A类的私有成员变量,
但想在A类中访问B类中的私有成员变量则不行
- 友元关系不能传递
如:C是B的友元,B是A的友元,则不代表C就是A的友元
- 友元关系不能继承(继承会在之后了解到)
图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
四 . 内部类
内部类的概念:
如果将一个类定义在另一个类的内部,那么这个类就叫做内部类。
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。
外部类对内部类没有任何”优越“的访问权限
注意:
内部类就是外部类的友元类,根据友元类的定义,
内部类可以通过外部类的对象参数来访问外部类中的所有成员。
但是外部类不是内部类的友元
---------------------------------------------------------------------------------------------
内部类的特性:
- 将内部类定义在外部类的 public、protected、private 中都是可以的,
内部类也会受到相应的访问权限限制
- 内部类中可以直接访问外部类中的static成员,不需要通过外部类的 对象 / 类名
- 计算外部类大小:sizeof(外部类)=外部类大小,和内部类没有任何关系
图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
补充:拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,
这个在一些场景下还是非常有用的,
注:以下的优化都是在建立在同一个表达式的基础上的
补充:
调用拷贝构造函数还是“=”赋值运算符重载函数
- 调用拷贝构造函数:
当使用 “=” ,左操作数是没进行初始化的对象,且右操作数是已经存在的对象时,
这种情况就会调用拷贝构造函数,通过右操作数的对象拷贝初始化左操作数的对象
- 调用“=”赋值运算符重载函数:
当使用“=”,且左右操作数都是已经存在的对象时,
这种情况就会调用“=”赋值运算符重载函数,将右操作数对象赋值给左操作数对象图示:
---------------------------------------------------------------------------------------------
优化一:“未初始化对象 = 内置类型”
- 在同一个表达式中,在这种情况下,
第一步:先通过内置类型构造出一个临时对象
(调用:构造函数)
- 第二步:再通过临时对象拷贝构造初始化左操作数的未初始化对象
(调用:拷贝构造函数)
- 优化:构造函数 + 拷贝构造函数 => 构造函数
编译器中这两次调用实际只会调用一次构造函数
(不同编译器优化不同,这里以VS2020为例)图示:
---------------------------------------------------------------------------------------------
优化二:“通过匿名对象调用函数”
- 在同一个表达式中,在这种情况下,
第一步:先初始化匿名对象
(调用:构造函数)
- 第二步:再传值传参匿名对象调用函数
(调用:拷贝构造函数)
- 优化:构造函数 + 拷贝构造函数 => 构造函数
编译器中这两次调用实际只会调用一次构造函数
(不同编译器优化不同,这里以VS2020为例)图示:
---------------------------------------------------------------------------------------------
优化三:“通过内置类型调用函数”
- 在同一个表达式中,在这种情况下,
第一步:先通过内置类型构造出一个临时对象
(调用:构造函数)
- 第二步:再传值传参临时对象调用函数
(调用:拷贝构造函数)
- 优化:构造函数 + 拷贝构造函数 => 构造函数
编译器中这两次调用实际只会调用一次构造函数
(不同编译器优化不同,这里以VS2020为例)图示:
---------------------------------------------------------------------------------------------
优化四:“未初始化对象接收函数传值返回的临时对象”
- 在同一个表达式中,在这种情况下,
第一步:函数传值返回时拷贝临时对象进行返回
(调用:拷贝构造函数)
- 第二步:再通过返回的临时对象进行拷贝初始化对象
(调用:拷贝构造函数)
- 优化:拷贝构造函数 + 拷贝构造函数 => 拷贝构造函数
编译器中这两次调用实际只会调用一次拷贝构造函数
(不同编译器优化不同,这里以VS2020为例)图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
本篇博客相关代码:
Test.cpp文件 -- C++文件:
//#define _CRT_SECURE_NO_WARNINGS 1 //包含IO流头文件: #include <iostream> //展开std命名空间: using namespace std; //class A //{ //public: // //全缺省构造函数(默认构造函数): // A(int a = 0) // //初始化列表: // :_a(a) // { // /* // * Date类中成员变量 A _aa 初始化时, // * 会调用这个默认构造函数, // * 这个默认构造函数再通过初始化列表 // * 对 _aa 进行初始化 // */ // } //private: // //成员变量: // int _a; //}; // // 日期类: //class Date //{ //public: // // //Date(int year, int month, int day) // //{ // // //构造函数体内初始化: // // _year = year; // // _month = month; // // _day = day; // // // _ref = year; //引用变量 // // _n = 1; //const变量 // //} // // //Date(int year, int month, int day) // // //初始化列表:冒号开始,逗号分割 // // :_year(year) //初始化:年 // // ,_month(month) //初始化:月 // // ,_day(day) //初始化:日 // // ,_ref(year) //初始化:引用变量 // // ,_n(1) //初始化:const变量 // //{ // // /* // // * 引用变量 和 const变量, // // * 都必须在定义时就进行初始化, // // * 初始化列表就可以解决这个问题 // // */ // //} // // Date(int year, int month, int day) // //初始化列表:冒号开始,逗号分割 // :_ref(year) //初始化:引用变量 // ,_n(1) //初始化:const变量 // ,_aa(10) //初始化:自定义对象 // /* // * 引用变量 和 const变量, // * 都必须在定义时就进行初始化, // * 初始化列表就可以解决这个问题 // * // * 执行到这里, // * 剩下的3个成员变量没有在初始化列表中初始化, // * 但它们也已经被定义了,只是因为是内置类型, // * 编译器会默认给它们一个随机值, // * 如果是自定义类型成员变量的话则会去调用 // * 其默认构造函数 // * // * 如果该自定义类型没有合适的默认构造函数, // *(全缺省构造函数、显式定义无参构造函数、 // * 编译器默认生成的构造函数) // * 只有显式定义的有参构造函数,那么该对象 // * 的初始化也可以放在初始化列表中。 // * 就像这里的_aa一样,在初始化列表中, // * 直接调用其有参构造函数进行初始化。 // * 就是在编译器调用其默认构造函数前, // * 先在初始化列表中调用其有参构造函数进行初始化 // * // * 初始化列表中的初始化顺序和 // * 成员变量声明的顺序是一样的, // * 所以建议初始化列表顺序和声明顺序保持一致 // */ // { // //构造函数体内初始化: // _year = year; //初始化:年 // _month = month; //初始化:月 // _day = day; //初始化:日 // } // //private: // //声明成员变量,未开空间: // // int _year = 1; // int _month = 1; // int _day = 1; // /* // * 这里给的1是缺省值, // * 如果 初始化列表 中没有 // * 对应成员变量的初始化, // * 那么该成员变量的值就会是这里设置的缺省值 // *(这里缺省值的功能就类似初始化列表) // */ // // //引用变量:必须在定义时就初始化 // int& _ref; // //const变量:必须在定义时就初始化 // const int _n; // //自定义类型对象: // A _aa; //}; // // //class Stack //{ //public: // //栈类构造函数: // Stack(int n = 2) // :_a((int*)malloc(sizeof(int)*n)) // ,_top(0) // ,_capacity(n) // /* // * 虽然说尽量使用初始化列表进行初始化, // * 但也不是说就完全不在构造函数体中写代码了, // * // * 初始化列表中也可以进行动态内存开辟, // * 但有些初始化或检查的工作,初始化列表也不能全部搞定, // * 想这里就没有办法对开辟的动态空间进行检查 // */ // { // //…… // //构造函数体内: // // //动态空间检查工作: // if (_a == nullptr) // { // perror("malloc fail"); // exit(-1); // } // // //数据拷贝工作: // memset(_a, 0, sizeof(int) * n); // // //想这里的两种工作初始化列表就完成不了 // } // // //…… //private: // int* _a; // int _top; // int _capacity; //}; // //class MyQueue //{ //public: // MyQueue(int n1 = 10, int n2 = 20) // :_s1(n1) // ,_s2(n2) // //通过初始化列表自己控制自定义的初始化值 // //不受制于自定义类型中构造函数的缺省值 // {} // //private: // Stack _s1; // Stack _s2; //}; // 主函数: //int main() //{ // //实例化对象--定义成员变量:对象整体定义 // //每个成员变量在 初始化列表 中进行定义 // Date d1(2023, 10, 31); // /* // * 对象中的 引用变量(_ref) 和 const变量(_n), // * 应该在示例化对象时就已经被定义好了, // * 所以我们实例化时不需要传这两个变量的参数, // * 要完成这个步骤就需要依靠 初始化列表了 // */ // // MyQueue(); // MyQueue(100, 1000); // // /* // * 总结--初始化列表的作用: // * // * 1、解决必须在定义时就要求初始化的类型变量问题 // * (如:引用类型成员变量、const成员变量、 // * 自定义类型中只有有参构造函数的初始化) // * // * 2、让一些自定义类型的成员变量自己显式控制初始化值 // * // * 3、尽量使用初始化列表进行初始化, // * 初始化列表就是成员变量定义的地方, // * 在定义的地方就进行初始化会更好一点, // *(80%-100%的工作初始化列表能完成, // * 还有一些工作只能在函数体中完成, // * 所以要将初始化列表和函数体结合起来使用) // */ // // return 0; //} //namespace ggdpz //{ // //定义一个全局变量: // int count = 0; //统计一共创建了多少个A对象 // // /* // * 因为我们完全展开了stdC++标准库, // * 且库中有count同名变量, // * 所以为了避免命名冲突, // * 定义一个命名空间,在命名空间中定义自己的count // */ //} A类: //class A //{ //public: // //构造函数: // A() { ++ggdpz::count; } // // //拷贝构造函数: // A(const A& t) { ++ggdpz::count; } // // /* // * 当调用了一次构造函数或拷贝构造函数时, // * 就说明创建了一个对象, // * ++count即创建了一个对象 // */ // // //析构函数: // ~A() { } // //private: // //}; // 创建一个函数: //A func() //{ // //创建一个A对象: // A aa; // // //返回该对象: // return aa; //} // 主函数: //int main() //{ // //创建一个A对象: // A aa; // //调用func()函数: // func(); // // ggdpz::count++; // /* // * 但如果使用全局变量, // * 我这里也可以直接调用让其+1, // * 但我们实际并没有创建对象, // * 这时候统计的创建对象个数就是错的了 // * // * 但如果把count统计变量设置为成员变量的话, // * 就可以解决该问题了, // */ // // //打印创建了多少个对象: // cout << ggdpz::count << endl; // // return 0; //} A类: //class A //{ //public: // //构造函数: // A() { ++count; } // // //拷贝构造函数: // A(const A& t) { ++count; } // // /* // * 当调用了一次构造函数或拷贝构造函数时, // * 就说明创建了一个对象, // * ++count即创建了一个对象 // */ // // //析构函数: // ~A() { } // // //Get()方法: // static int GetCount() //只读不写 // { // /* // * 静态成员函数,没有隐藏的this指针, // * 所以没法在函数体中使用非静态成员变量了, // */ // // //返回想要获得的count: // return count; // } // //private: // //私有成员变量:: // // //统计创建对象个数 // static int count; //只是声明 // /* // * 如果是:int count = 0; // * 这里虽然是用于统计创建对象个数, // * 但是每个对象中都有一个count成员变量, // * 而且都是独立的,统计计数时不是加到同一个count上, // * 所以起不到统计的作用 // * // * 所以这里应该使用static进行修饰, // * 就可以让被修饰的成员变量能够被共享, // * 让该类的所有对象共用该成员变量, // * 这样就可以在成员变量上统计创建对象个数了 // * // * 成员变量使用static进行修饰后就不支持给缺省值了, // * 即不能写成:static int count = 0; // * 因为这里给的缺省值实际上是给初始化列表的, // * 而初始化列表中是初始化某个对象, // * static不是对象,不会在初始化列表中执行, // * 所以不能这样给缺省值 // */ //}; // ///* //* static成员变量声明是在类中声明, //* 但实现(定义)是在类外面实现(定义)的, //* 只能初始化一次,而且其初始化不是在实例化对象时, //* 而是在执行主函数之前就已经初始化了, //* 这也是其不能在类中初始化列表中进行初始化的原因 //* //* 其实本质还是一个全局变量,只不过被放在类域中了, //* 是这个类专属的“全局变量”(私有成员变量) //*/ //int A::count = 0; // 创建一个函数: //A func() //{ // //创建一个A对象: // A aa; // // //返回该对象: // return aa; //} // 主函数: //int main() //{ // //创建一个A对象: // A aa; // //调用func()函数: // func(); // // /* // * 此时就不能直接对其进行++了, // * 因为是该类专属的“全局变量”, // * 属于整个类,属于这个类的所有对象, // * 受到了访问限定符的影响(private), // * 所以不能直接对static成员变量进行调用 // */ // A::count++; // // /* // * 如果static成员变量是共有的(public): // * 则可以通过类域直接访问, // * 也可以通过对象进行调用, // * 但通过对象进行调用实际是找到该对象的类, // * 然后再进行访问的 // */ // cout << A::count << endl; //通过类域直接访问 // cout << aa.count << endl; //通过对象进行调用 // //aa.count -> aa是A类的对象,count是A类中的 -> A::count // // /* // * 如果static成员变量是私有的(private): // * 那要怎么获得它呢?这时我们可以定义对应的Get()方法, // * 再通过对象调用对应的Get()方法来获取static成员变量, // * Get()方法只读不写,只能读不能写 // */ // //通过对应Get()方法获得count的值: // cout << aa.GetCount() << endl; // // /* // * 但如果static成员变量是私有的,同时又没有对象, // * 要怎么获得static成员变量呢? // * // * 方式一:为了调用static成员变量而创建一个对象, // * 因为是专门为了调用而创建的对象,所以调用时要-1, // * 减去创建这个对象的次数(不是很好的方式) // * // * 方式二:如果 类型+() 匿名对象调用对应的Get()方法, // * 匿名对象的生命周期只有写它的那一行,过了这一行后, // * 就会调用析构函数“销毁”匿名对象,匿名对象也会调用构造函数, // * 所以如果使用匿名函数查看这里的count的话,同样需要-1 // * (也不是很好的方式) // * // * 方式三:将对应的Get()方法设置为静态成员函数, // * 静态成员函数的特点:没有this指针,方式一和方式二中, // * 因为Get()方法中有this指针,所以必须通过对象调用this指针 // * 来使用Get()方法获取count的值。 // * 而将其设置为静态成员函数没有this指针的话(在类域中), // * 调用时就不用依靠对象了,通过类域进行调用即可 // */ // //方式一:通过有名对象 // A aa; //有名对象 // cout << aa.GetCount() - 1 << endl; //非静态成员函数 // // //方式二:通过匿名对象 // //这里的 A() 就是匿名对象 // cout << A().GetCount() - 1 << endl; //非静态成员函数 // // //方式三:通过静态成员函数 // cout << A::GetCount() << endl; //静态成员函数 // //直接通过A类域调用到对应的Get( )方法 // cout << A().GetCount() << endl; // /* // * 设置成静态成员函数后,依旧可以通过对象来调用, // * A()匿名对象不是通过this指针调用Get方法的, // * 而是通过A()找到对应类域,再在类域中调用到Get方法的 // *(静态成员函数只要能找到其对应的类域即可进行调用) // * // * 总结: // * 一、 // * 可以将静态成员函数和静态成员变量看成是 // * “类域中受到了限制的全局函数和全局变量”, // * 两者本质没太大的区别,使用sizeof计算有 // * 静态成员函数或静态成员变量的类时, // * 也不会计算类中静态成员函数或变量的大小。 // *(类的大小不包括静态成员函数或变量) // * 使用静态(static)修饰本质是为了和非静态进行区别 // * // * 二、 // * 静态成员函数和静态成员变量属于这个类的所有对象, // * 而且它们受到类域和访问限定符的限制 // * // */ // // return 0; //} A类(单参数构造函数): //class A //{ //public: //共有成员函数: // // //(有参)构造函数: // explicit A(int a) // //初始化列表: // :_a(a) // {} // //private: //私有成员变量: // int _a = 0; //}; // 日期类(多参数构造函数): //class Date //{ //public: //共有成员函数: // // //多参数构造函数: // explicit Date(int year, int month = 1, int day = 1) // : _year(year) // , _month(month) // , _day(day) // {} // //private: //私有成员变量: // int _year; // int _month; // int _day; //}; // 主函数: //int main() //{ // //正常调用构造函数进行初始化: // A aa1(1); // A aa2(2); // // /* // * 之前了解过的赋值运算符重载, // * 其两个操作数都是类类型, // * 但如果左操作数是类类型, // * 右操作数是内置类型呢: // */ // A aa3 = 3; //正常赋值:类类型 = 内置类型 // //左操作数为类类型,右操作数为内置类型 // /* // * 对应构造函数为:A(int i) // * 此时就是将内置类型隐式转换成了自定义类型对象, // * 涉及到类型转换,那就会产生一个临时变量(具有常属性), // * 这里会产生一个 A(3) 的临时对象(通过构造函数产生), // * 再对 A(3) 进行拷贝构造到 aa3 (通过拷贝构造函数), // * 所以会先调用构造函数,再调用拷贝构造函数, // * 之所以支持这个转换, // * 是因为A类中有 int类型的单参数构造函数 // *(单参数构造函数的类型不同支持的隐式转换类型也不同) // */ // // const A& ra = 3; //引用:类类型 = 内置类型 // /* // * 这里对象ra引用的是 类型转换时产生的临时对象(变量), // * 临时对象(变量)具有常属性,其类型是 const A , // * 所以引用时的类型应该是:const A& // */ // // //那么如果不想让类类型接收隐式转换的内置类型该怎么办呢? // /* // * 在构造函数前加上关键字explicit, // * 即在构造函数 A(int a) 前加上变成: // * explicit A(int a) ,此时该构造函数就不支持 // * 类类型接收隐式转换的内置类型了 // *(加上explicit后的构造函数就不支持隐式类型转换了) // *(但显式类型转换还是可以的) // */ // // //“特殊”多参数构造函数正常调用初始化: // Date d1(2023, 11, 2); // // //类类型 = “(多个内置类型)” // Date d2 = (2023, 11, 3); // //等价于:Date d2 = 3 // /* // * 对于“特殊”多参数构造函数: // * Date(int year, int month = 1, int day = 1) // * // * 这里是可以编译过的,但d2的日期是 3/1/1 , // * 因为这里的 (2023,11,3) 其实是逗号表达式, // * 只以最后的值为准,即3,3被放在“年”的位置, // * 而后面的月和天其实是缺省值,所以结果是 3/1/1 // * // * 这里的多参数构造函数是半缺省构造函数, // * 只有一个“年”必须给初始化值,所以该构造函数 // * 是支持用一个值进行初始化的。 // * // * 所以只要一个对象的构造函数能够支持一个值完成初始化, // * 就可以通过 类类型 = 内置类型 进行初始化 // * (构造函数通过缺省值进行调整) // */ // // //多参数构造函数: // Date d4 = { 2023,11,2 }; // /* // * C++11中是支持类对象多参数初始化的, // * 不过使用的是大括号{},列表初始化, // * 初始化过程中也有产生临时对象, // * 实际执行还是通过构造函数进行初始化的 // */ // // const Date& d5 = { 2023,11,2 }; // //证明列表初始化也有产生临时对象,其类型也有常属性 // // return 0; //} 时间类: //class Time //{ // /* // * 日期类想访问时间类私有成员变量, // * 应在时间类中将日期类设置为友元类 // * // * 声明日期类为时间类的友元类, // * 则在日期类中就可以直接访问Time类中, // * 的私有成员变量: // */ // friend class Date; //友元类 // /* // * 友元关系是单向的, // * 此时Date日期类能访问Time时间类的私有成员变量, // * 但Time时间类不能访问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 //{ // //友元函数声明: // //“<<”流运算符重载函数(类中友元声明): // friend ostream& operator<<(ostream& _cout, const Date& d); // //“>>”流运算符重载函数(类中友元声明): // friend istream& operator>>(istream& _cin, Date& d); // // /* // * 友元函数:让函数能够访问类中的私有成员 // * // * 友元声明虽然写在类中,但它并不是成员函数, // * 没有隐藏的this指针,友元函数类中声明类外定义, // * 没有指定类域。 // * 就是一个全局函数,并声明“我是你的友元(“朋友”)” // */ // // /* // * 友元会让耦合度变高,某种程度上破坏了封装 // */ // //public: //共有成员函数: // //全缺省构造函数: // Date(int year = 1900, int month = 1, int day = 1) // : _year(year) // , _month(month) // , _day(day) // {} // // //日期类设置当天日期时间函数: // void SetTimeOfDate(int hour, int minute, int second) // { // //直接访问时间类中的私有成员变量: // _t._hour = hour; // _t._minute = minute; // _t._second = second; // // /* // * 将日期类设置为时间类的友元类后, // * 日期类中可以直接访问时间类中的私有成员函数 // */ // } // //private: //私有成员变量: // int _year; //年 // int _month; //月 // int _day; //日 // Time _t; //时间类对象 //}; // “<<”流运算符重载函数(全局中实现): //ostream& operator<<(ostream& _cout, const Date& d) //{ // _cout << d._year << "-" << d._month << "-" << d._day; // // return _cout; //} // “>>”流运算符重载函数(全局中实现): //istream& operator>>(istream& _cin, Date& d) //{ // _cin >> d._year >> d._month >> d._day; // // return _cin; //} A类: //class A //{ //private: //私有成员变量(A类): // int h; //4个字节 // //public: //公有成员函数: // // //在A类内部再定义一个B类: // class B //内部类B // { // public: //公有(B类): // int _b; // // void func() // { // A aa; // aa.h++; //直接访问外部类的私有成员 // /* // * 因为内部类天生就是外部类的友元, // * 所以内部类中可以直接访问外部类的私有成员 // */ // } // }; // // /* // * A类和内部类B的关系: // * 内部类B可以认为就是一个普通的类, // * 只是其会受到 A的类域 和 访问限定符 的限制, // * 而且内部类天生就会是外部类的友元(重点) // * // * 跟静态成员变量和函数类似,性质跟在全局时一样, // * 只是受到了 类域 和 访问限定符 的限制 // * // * C++中内部类比较少用 // */ //}; // 主函数: //int main() //{ // //计算拥有内部类的A类的大小: // cout << sizeof(A) << endl; // /* // * 这里A类的大小计算后是4个字节, // * 也就是说计算A类时是不会计算内部类B的 // */ // // //可以直接定义A对象: // A aa; // // //但无法直接定义内部类B对象: // B bb; //爆红 // // //指定其类域后就可以定义其对象了: // A::B bb; //内部类B的权限为公有 // // A::B bb; // /* // * 指明类域后,但如果内部类B的权限为私有, // * 那单单指明类域也无法创建内部类B对象, // * 而是只能在类内部进行使用 // */ // // return 0; //} //扩展内容:一些构造时的优化(不同编译器优化不同) //A类: class A { public: //公有成员函数: //构造函数(有参): A(int a = 0) :_a(a) { cout << "A(int a)" << endl; } //拷贝构造函数: A(const A& aa) :_a(aa._a) { cout << "A(const A& aa)" << endl; } //“=”赋值运算符重载函数: A& operator=(const A& aa) { cout << "A& operator=(const A& aa)" << endl; if (this != &aa) { _a = aa._a; } return *this; } //析构函数: ~A() { cout << "~A()" << endl; } private: //私有成员变量: int _a; }; //主函数: int main() { A aa1(1); //调用(有参)构造函数 A aa2(aa1); //调用拷贝构造函数 //如果有一个已经存在的对象(aa1): A aa3 = aa1; //调用拷贝构造函数 /* * 一个已经存在的对象拷贝初始化 * 另一个要创建的对象 -- 赋值拷贝 * * 其实就相当于上面的:A aa2(aa1) */ //如果两个都是已经存在的对象(aa2 和 aa3): aa2 = aa3; //调用"="赋值运算符重载函数 return 0; } //优化一 -- 主函数: int main() { //优化一: A aa1 = 1; //这里就是以下第三步中的b类型 /* * 第一步:先用1构造一个临时对象(构造函数) * 第二步:再通过临时对象拷贝构造出aa1(拷贝构造函数) * * 第三步(优化): * 同一个表达式中(注意), * 连续构造+构造 / 构造+拷贝构造 / 拷贝构造+拷贝构造 * 这些操作可能会被合二为一 * a、 构造+构造 -> 构造 * b、 构造+拷贝构造 -> 构造 * c、 拷贝构造+拷贝构造 -> 拷贝构造 * * 这里就是b类型,先调用 构造+拷贝构造 ,实际只调用了一次构造函数 */ return 0; } //func函数: void func(A aa) {} //主函数: int main() { A aa1(1); //初始化:调用构造函数 func(aa1); //传值传参:调用拷贝构造函数 /* * 这里虽然也是先调用构造函数, * 再调用拷贝构造函数,但这里不是再同一个表达式中 */ //优化二: //通过匿名对象调用函数: func(A(2)); /* * 这里需要先初始化匿名对象, * 调用构造函数, * 再传值传参匿名对象调用func函数, * 调用拷贝构造函数 * * 这里跟上面不同,是在同一个表达式中, * 所以 构造 + 拷贝构造 -> 构造函数 * 合二为一只会调用一次构造函数 */ //优化三(和优化一类似): func(3); /* * 先用3构造一个临时对象, * 调用构造函数, * 再用临时对象进行传值传参, * 调用拷贝构造函数 * * 这里也是在同一个表达式中, * 所以所以 构造 + 拷贝构造 -> 构造函数 * 合二为一只会调用一次构造函数 */ return 0; } A func() { A aa; /* * 函数中初始化一个A类型对象, * 调用构造函数 */ return aa; /* * 因为是传值返回,所以拷贝出aa的临时对象, * (调用拷贝构造函数) */ } //主函数: int main() { //优化四: A aa1 = func(); /* * 传值返回时调用了一次拷贝构造函数, * 再通过返回的临时变量拷贝初始化aa1, * (调用拷贝构造函数) * * 即传值返回时调用了一次拷贝构造函数, * 然后又继续调用一次拷贝构造函数进行 * 拷贝初始化对象aa1, * 所以是连续调用了两次拷贝构造函数 * (同一个表达式中) * * 所以 拷贝构造 + 拷贝构造 -> 拷贝构造 * 合二为一只会调用一次拷贝构造函数 */ A aa2; aa2 = func(); /* * 这里则不会进行优化, * 因为这时的“=”两边都是已经初始化了的对象, * 所以这里会调用“=”赋值运算符重载函数进行赋值, * 所以不会像上面一样被优化 */ return 0; } /* * 总结: * 再同一个表达式中: * 构造 + 构造 -> 构造 * 构造 + 拷贝构造 -> 构造 * 拷贝构造 + 拷贝构造 -> 拷贝构造 * 现在的编译器基本都会做到 * * 但一些新的编译器可能还会跨表达式优化, * 把中间一些不必要对象也优化掉 -- 跨表达式优化 * (不同编译器可能不同) * 是一种激进优化,会使编译器的维护更加复杂, * 可能会使之前的代码在优化后有bug */