温馨提示:这篇文章已超过469天没有更新,请注意相关的内容是否还可用!
摘要:本篇文章介绍了C++中的类和对象,包括初始化列表、static成员、友元以及内部类的概念和使用。文章详细解释了如何通过初始化列表来初始化对象的成员变量,static成员的特点和用法,以及如何通过友元函数和内部类来扩展类和对象的功能。这篇文章适合C++初学者,能够帮助他们更好地理解和掌握类和对象的相关知识。
=========================================================================
相关代码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 //展开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
- 在同一个表达式中,在这种情况下,
- 在同一个表达式中,在这种情况下,
- 在同一个表达式中,在这种情况下,
- 在同一个表达式中,在这种情况下,
- 调用拷贝构造函数:
- 将内部类定义在外部类的 public、protected、private 中都是可以的,
- 友元关系是单向的,不具有交换性
- 所以要将这两个流运算符重载为全局函数,
- 静态成员是所有类对象共享的,不属于某个具体的对象,存放在静态区
- 构造函数不仅可以构造和初始化对象,
- 尽量使用初始化列表进行初始化,因为不管你是否使用初始化列表,
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 使用格式:
还没有评论,来说两句吧...