⭐在对C++ 中类的6个默认成员函数有了初步了解之后,现在我们进行对类相关特性的深入探讨!
???【C++】类的默认成员函数:深入剖析与应用(上)
【C++】类的默认成员函数:深入剖析与应用(下)
目录
?前言
?再谈构造函数
(一)构造函数体赋值
(二)初始化列表
(三)explicit关键字
?static成员
(一)静态成员变量
(二)静态成员函数
?友元
(一)友元函数
(二)友元类
?内部类
(一)定义与访问
(二)内部类的用途
?总结
?前言
在 C++ 编程中,类是构建复杂程序的基石。?它提供了一种将数据和操作数据的方法进行封装的机制,使得程序更加模块化、可维护和可扩展。在之前对类的学习中,我们已经了解了一些基本概念,如构造函数、拷贝构造函数和析构函数等。然而,类还有许多其他重要的特性,这些特性对于深入理解和掌握 C++ 编程至关重要。?本文将进一步探讨构造函数的更多细节,以及 Static 成员、友元、内部类等特性,并再次深入理解封装的概念。
?再谈构造函数
构造函数在 C++ 类中扮演着至关重要的角色。它主要用于对象的初始化操作。当创建一个类的对象时,构造函数会被自动调用。
(一)构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date {public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; }private: int _year; int _month; int _day;};
❓问题如下:
虽然在构造函数中通过赋值操作给对象的成员变量赋予了初始值,但严格意义上来说这只是在构造函数体中进行的赋初值操作,而不是真正的初始化。
真正的初始化是在对象创建时就确定下来,并且只能进行一次。而在构造函数体内部,可以进行多次赋值操作,这就与初始化的概念有所不同。
?现在我们引出一个概念,来解决这一问题——初始化列表
(二)初始化列表
为了实现真正的初始化,可以使用初始化列表。
初始化列表是在构造函数的参数列表之后,函数体之前,以冒号开头,后面跟着一系列成员变量的初始化表达式。
?代码如下 :
class Date {public: Date(int year, int month, int day) : _year(year), _month(month), _day(day) { // 构造函数体,可以进行其他操作,但这里不能再对成员变量进行初始化 }private: int _year; int _month; int _day;};
❗注意:
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)类中包含以下成员,必须放在初始化列表位置进行初始化:?引用成员变量
?const成员变量
?自定义类型成员(且该类没有默认构造函数时)
#include <iostream>// 自定义类 CustomClass 的定义class CustomClass {public: // 构造函数,接收一个整数值进行初始化 CustomClass(int val) : value(val) { std::cout << "CustomClass constructor called with value: " << value << std::endl; }private: int value;};// 主类 MyClass 的定义class MyClass {public: int& ref; // 引用成员变量 const int constVal; // const 成员变量 CustomClass custom; // 自定义类型成员变量 // 构造函数,接收一个引用、一个整数和一个整数作为参数 MyClass(int& r, int v, int customVal) : ref(r), constVal(v), custom(customVal) { std::cout << "MyClass constructor called." << std::endl; }};int main() { int num = 10; // 创建 MyClass 对象,传入相应参数 MyClass obj(num, 20, 30); return 0;}
☝MyClass
包含了引用成员变量、const
成员变量和一个自定义类型的成员变量。在构造函数中,必须使用初始化列表来正确初始化这些特殊类型的成员变量。
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,✅对于自定义类型成员变量,一定会先使用初始化列表初始化。
#include <iostream>class CustomType {public: CustomType(int val) : data(val) { std::cout << "CustomType constructor called with value: " << data << std::endl; }private: int data;};class MyClass {public: MyClass(int customVal) : customMember(customVal) { std::cout << "MyClass constructor called." << std::endl; }private: CustomType customMember;};int main() { MyClass obj(42); return 0;}
在这个例子中,MyClass
有一个自定义类型CustomType
的成员变量customMember
。当创建MyClass
的对象时,即使在MyClass
的构造函数中没有显式地写出初始化列表,但实际上编译器会先尝试使用初始化列表来初始化customMember
,这就调用了CustomType
的构造函数并输出相应信息。如果CustomType
没有默认构造函数,那么就必须在MyClass
的构造函数中显式地使用初始化列表来正确初始化customMember
。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
#include <iostream>class MyClass {public: MyClass(int a, int b, int c) : c_member(c), b_member(b), a_member(a) { std::cout << "Constructor called." << std::endl; } void printMembers() { std::cout << "a_member: " << a_member << std::endl; std::cout << "b_member: " << b_member << std::endl; std::cout << "c_member: " << c_member << std::endl; }private: int a_member; int b_member; int c_member;};int main() { MyClass obj(1, 2, 3); obj.printMembers(); return 0;}
在这个例子中,构造函数的初始化列表中成员变量的初始化顺序看起来是 c_member
、b_member
、a_member
,?但实际上成员变量的初始化顺序是由它们在类中的声明顺序决定的,即先初始化 a_member
,再初始化 b_member
,最后初始化 c_member
。如果在初始化列表中打乱顺序,初始化的结果仍然是按照声明顺序进行的。
(三)explicit关键字
构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。
接收单个参数的构造函数具体表现?:
构造函数只有一个参数构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值全缺省构造函数#include <iostream>class Date{public: // 1. 单参构造函数,没有使用 explicit 修饰,具有类型转换作用 // explicit 修饰构造函数,禁止类型转换---explicit 去掉之后,代码可以通过编译 explicit Date(int year) : _year(year) { } /* // 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用 explicit 修饰,具有类型转换作用 // explicit 修饰构造函数,禁止类型转换 explicit Date(int year, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) { } */ Date& operator=(const Date& d) { if (this!= &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; }private: int _year; int _month; int _day;};void Test(){ Date d1(2022); // 用一个整形变量给日期类型对象赋值 // 实际编译器背后会用 2023 构造一个无名对象,最后用无名对象给 d1 对象进行赋值 // 将 1 屏蔽掉,2 放开时则编译失败,因为 explicit 修饰构造函数,禁止了单参构造函数类型转换的作用}
上述代码可读性不是很好,用explicit修饰构造函数,将会禁止构造函数的隐式转换。
有一个接受单个整数参数的构造函数Date(int year)
,用于初始化日期对象的年份部分。这个构造函数使用了explicit
关键字修饰,这意味着它不能进行隐式类型转换。如果没有这个关键字,编译器可能会在某些情况下自动使用这个构造函数进行隐式的类型转换。
?static成员
(一)静态成员变量
?静态成员变量是属于类的变量,而不是属于某个具体的对象。它在整个类的所有对象之间共享。
1.定义与初始化
静态成员变量需要在类内声明,但不能在类内初始化(除了一些特殊的静态常量整数类型可以在类内初始化)。例如:class MyClass {public: static int staticVar;};int MyClass::staticVar = 0; // 在类外初始化
2.访问方式
可以通过类名直接访问静态成员变量,也可以通过对象访问,但通常建议通过类名访问,以体现其类属性。例如:MyClass obj;MyClass::staticVar = 10; // 通过类名访问obj.staticVar = 20; // 通过对象访问也是允许的,但不规范
(二)静态成员函数
?静态成员函数也是属于类的函数,它不依赖于具体的对象实例。
1.特点
它只能访问静态成员变量和其他静态成员函数,因为它没有this
指针指向具体的对象。例如: class MyClass {public: static int staticVar; static void staticFunction() { staticVar++; // 可以访问静态成员变量 // 不能访问非静态成员变量,因为没有this指针 }};
2.调用方式
与静态成员变量类似,可以通过类名直接调用静态成员函数。例如:MyClass::staticFunction();
?友元
⭐友元机制允许一个类或函数访问另一个类的私有成员。它打破了类的封装性,但在某些特定情况下是非常有用的。
(一)友元函数
1.定义
友元函数是在一个类中声明为友元的普通函数。它不是类的成员函数,但可以访问该类的私有成员。例如:class MyClass {private: int privateVar;public: friend void friendFunction(MyClass obj);};void friendFunction(MyClass obj) { // 可以访问obj的privateVar std::cout << obj.privateVar << std::endl;}
❗说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数友元函数不能用const修饰友元函数可以在类定义的任何地方声明,不受类访问限定符限制一个函数可以是多个类的友元函数友元函数的调用与普通函数的调用原理相同(二)友元类
1.定义
友元类是在一个类中声明为友元的另一个类。友元类的所有成员函数都可以访问声明它为友元的类的私有成员。例如:class MyClass {private: int privateVar;public: friend class FriendClass;};class FriendClass {public: void accessPrivate(MyClass obj) { // 可以访问obj的privateVar std::cout << obj.privateVar << std::endl; }};
2.使用场景
当两个类之间存在紧密的合作关系,需要相互访问对方的私有成员时,可以使用友元类。例如,一个图形绘制类和一个图形变换类可能需要相互访问对方的私有数据来实现复杂的图形操作。?内部类
?内部类是定义在另一个类内部的类。它具有一些特殊的性质和用途。
(一)定义与访问
1.定义
内部类可以在一个外部类的任何部分定义,包括私有部分、保护部分和公共部分。例如:class OuterClass {private: int outerVar; class InnerClass { public: void innerFunction() { // 可以访问OuterClass的成员吗?这取决于具体情况 } };};
2.访问
外部类可以通过创建内部类的对象来访问内部类的成员。内部类也可以访问外部类的成员,但需要注意访问权限。如果内部类定义在外部类的公共部分,那么它可以像普通类一样被外部访问和使用。如果定义在私有部分,只有外部类的成员函数可以创建内部类的对象并访问其的成员。例如:OuterClass outerObj;OuterClass::InnerClass innerObj; // 创建内部类对象(如果InnerClass是公共的)outerObj.innerObj.innerFunction(); // 通过外部区outerObj.innerObj.innerFunction(); // 通过外部类对象访问内部类对象的成员(如果InnerClass是公共的且有合适的访问路径)
(二)内部类的用途
1.隐藏实现细节
内部类可以将一些与外部类相关但又不想暴露给外部的实现细节封装起来。例如,一个复杂的容器类可能使用内部类来实现其内部的数据结构,如链表节点类可以作为容器类的内部类。2.实现辅助功能
可以利用内部类来实现一些辅助功能,这些功能与外部类紧密相关但又不适合作为外部类的直接成员函数。例如,一个文件读取类可能有一个内部类用于处理文件的缓冲和读取位置等细节。?总结
C++ 中类的这些特性为我们提供了强大的编程工具,让我们能够更好地组织和管理代码。希望本文能够帮助你更深入地理解 C++ 类的相关特性,提升你的编程能力。???
以后我将深入研究继承、多态、模板等特性,并将默认成员函数与这些特性结合,以解决更复杂编程问题!欢迎关注我?【A Charmer】