C++类与对象超详细入门指南
前言
💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力!
👍 点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力!
🚀 分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对C++感兴趣的朋友,让我们一起进步!
1. 初始化列表——再谈构造函数
1.1 初始化成员变量的方式
初始化列表 是构造函数中用于初始化类成员变量的一种特殊机制。与在构造函数体中直接赋值不同,初始化列表可以提高效率,尤其是对于某些特定类型的成员变量,它是唯一可行的初始化方式。
1.1.1 构造函数内部赋值 vs 初始化列表
在C++中,我们有两种主要方式来初始化类的成员变量:
- 构造函数内部赋值:在构造函数体内给成员变量赋值。
例如:
class A { public: A(int x) { this->_x = x; // 在构造函数体内赋值 } private: int _x; };
初始化列表赋值:在构造函数的初始化列表中直接对成员变量进行初始化。
例如:
class A { public: A(int x) : _x(x) { // 使用初始化列表赋值 } private: int _x; };
1.1.2 两者的区别
- 内置类型(如
int
):对于内置类型,使用初始化列表和在构造函数体内赋值在效率上几乎没有差别。内置类型没有构造函数,也不会进行隐式初始化(即它们可能持有垃圾值)。构造函数体内赋值或初始化列表赋值都只进行一次操作。因此,选择哪种方式主要是基于代码的清晰性和一致性。 - 类类型:对于类类型的成员变量,如果没有使用初始化列表,成员变量会先调用默认构造函数进行初始化,然后在构造函数体内再赋值。这样就相当于进行了两次操作::一次初始化,一次赋值。而使用初始化列表时,成员变量只会被初始化一次,效率更高。
- 例如,考虑如下代码:
class Member { public: Member(int value = 5) : _value(value) {} private: int _value; }; class A { public: A(int x) { _member = Member(x); // 先默认构造后再赋值 } private: Member _member; };
上面代码中,_member
会首先调用 Member
类的默认构造函数,然后再在构造函数体内通过赋值进行重新初始化。
而如果使用初始化列表:
class A { public: A(int x) : _member(x) { // 直接通过初始化列表初始化 } private: Member _member; };
_member
只会被初始化一次,避免了不必要的性能开销。
- 特殊情况:某些成员变量,例如常量 (
const
)、引用类型 (reference
) 或没有默认构造函数的对象,必须通过初始化列表进行初始化,否则编译器会报错。
1.1.3 为什么要使用初始化列表
- 效率:如前所述,初始化列表避免了成员变量的二次初始化,特别是在类类型成员中,性能优势更为明显。
- 必要性:某些类型的成员变量,如
const
、引用类型,或没有默认构造函数的类成员,必须通过初始化列表进行初始化,否则编译器无法自动处理这些成员的初始化。
1.1.4 示例
class Time { public: Time(int hour) : _hour(hour) { cout << "Time() called" << endl; } private: int _hour; };
在这个例子中,Time
类的构造函数使用了初始化列表,将传入的参数 hour
直接赋值给成员变量 _hour
。这样,_hour
在对象构造时就被初始化,而不需要在构造函数体内赋值。
1.2 初始化列表的语法
语法结构:初始化列表的使用方式是在构造函数名后跟一个冒号,接着是一个以逗号分隔的成员变量列表,每个成员变量后面紧跟括号中的初始值或表达式。
基本语法格式:
ClassName(参数列表) : 成员变量1(初始值), 成员变量2(初始值), ... { // 构造函数体 }
1.2.1 示例:
class MyClass { public: MyClass(int a, int b) : _a(a), _b(b) { // 构造函数体 } private: int _a; int _b; };
在这里,_a
被初始化为 a
,_b
被初始化为 b
。
1.3 引用成员变量、const成员变量的初始化
有些成员变量,比如引用类型和常量类型,只能通过初始化列表进行初始化。
1.3.1 引用类型成员的初始化
引用类型成员变量在 C++ 中必须在声明时被初始化,不能在构造函数体内赋值,必须使用初始化列表。
class MyClass { public: MyClass(int& ref) : _ref(ref) { // _ref 是引用类型,必须在初始化列表中初始化 } private: int& _ref; };
1.3.2 const成员变量的初始化
常量成员变量 (const
) 也必须在对象创建时初始化,之后不能修改。因此也必须在初始化列表中进行初始化。
class MyClass { public: MyClass(int n) : _n(n) { // _n 是 const 类型,必须在初始化列表中初始化 } private: const int _n; };
1.4 没有默认构造函数的类类型变量
如果一个类的成员变量是另一个没有默认构造函数的类类型变量,它也必须在初始化列表中进行初始化。
1.4.1 示例
class Time { public: Time(int hour) : _hour(hour) {} private: int _hour; }; class Date { public: Date(int year, int month, int day) : _year(year), _month(month), _day(day), _t(12) { // _t 是 Time 类型,必须在初始化列表中调用 Time 的构造函数 } private: int _year; int _month; int _day; Time _t; // Time 没有默认构造函数 };
1.5 成员变量默认值的使用 (C++11)
C++11 引入了成员变量默认值的概念。可以在类的声明中为成员变量提供默认值,这些默认值将在没有通过初始化列表显式初始化时使用。
class MyClass { public: MyClass() : _b(2) { // _a 使用默认值1 // 构造函数体 } private: int _a = 1; // 默认值 int _b; };
1.6 初始化顺序
尽管初始化列表中的成员可以按任何顺序出现,但成员变量的初始化顺序是按照它们在类中声明的顺序进行的,而不是它们在初始化列表中的顺序。
1.6.1 示例
class MyClass { public: MyClass(int a, int b) : _b(b), _a(a) { // 尽管 _b 在初始化列表中先出现,但 _a 会首先被初始化 } private: int _a; int _b; };
为了保持代码的一致性和可读性,建议初始化列表的顺序和成员变量声明的顺序一致。
1.7 初始化列表总结
- 每个构造函数都有初始化列表,即使你没有显式地写出它。
- 每个成员变量都必须被初始化,即使它没有在初始化列表中显式地被初始化。
- 对于引用类型、常量和没有默认构造函数的类类型成员,必须在初始化列表中进行初始化。
- C++11 允许在成员变量声明时提供默认值,这些默认值会在初始化列表中未显式初始化时使用。
- 初始化顺序取决于成员变量在类中的声明顺序,而不是它们在初始化列表中的顺序。
2. 类型转换详解
在C++中,类型转换(Type Conversion)是指将一种数据类型转换为另一种数据类型的过程。对于类而言,C++允许将内置类型或类类型转换为其他类类型,这一功能在面向对象编程中非常有用。类型转换可以是显式的(explicit)或隐式的(implicit),并且它们涉及构造函数、转换运算符和explicit
关键字。
2.1 内置类型转换为类类型
C++支持将内置类型(如
int
、double
等)隐式地转换为自定义的类类型。这是通过定义带有内置类型参数的构造函数来实现的。
2.1.1 隐式类型转换
在没有explicit
关键字修饰构造函数的情况下,编译器会自动将符合构造函数参数类型的内置类型值隐式转换为类对象。
示例:
class A { public: A(int a1) : _a1(a1) {} void Print() { cout << _a1 << endl; } private: int _a1; }; int main() { A obj = 10; // 隐式将 int 10 转换为 A 类型对象 obj.Print(); // 输出: 10 }
在上面的代码中,整数 10
被隐式地转换为类 A
的对象,编译器自动调用了A
的构造函数。可以直接通过A obj = 10;
来创建对象,这是隐式类型转换的常见形式。
2.1.2 explicit
防止隐式转换
有时候,隐式类型转换会引发意想不到的错误或逻辑问题。为了防止这些错误,C++允许我们使用explicit
关键字修饰构造函数,这样可以禁止该构造函数参与隐式转换。
示例:
class A { public: explicit A(int a1) : _a1(a1) {} void Print() { cout << _a1 << endl; } private: int _a1; }; int main() { // A obj = 10; // 错误:explicit 阻止了隐式转换 A obj(10); // 正确:必须显式调用构造函数 obj.Print(); // 输出: 10 }
在这个例子中,explicit
关键字阻止了A obj = 10;
的隐式类型转换,必须使用A obj(10);
进行显式调用构造函数来创建对象。这种方式避免了潜在的类型转换混淆问题。
2.2 类类型之间的转换
C++也允许将一个类类型的对象隐式转换为另一个类类型。这通常通过类的构造函数来实现。例如,一个类可以通过接受另一个类类型对象的构造函数进行隐式转换。
2.2.1 类类型之间的隐式转换
在下面的例子中,B
类通过构造函数接受一个A
类对象,这样当我们将A
类对象赋值给B
类时,C++会自动进行隐式转换。
示例:
class A { public: A(int a1) : _a1(a1) {} int Get() const { return _a1; } private: int _a1; }; class B { public: B(const A& a) : _b(a.Get()) {} void Print() { cout << _b << endl; } private: int _b; }; int main() { A objA(10); B objB = objA; // A 类型对象隐式转换为 B 类型对象 objB.Print(); // 输出: 10 }
在这里,B
类的构造函数接受一个A
类对象,因此当我们将objA
赋值给objB
时,C++会隐式调用B
的构造函数将A
对象转换为B
对象。
2.2.2 阻止类类型的隐式转换
与内置类型的隐式转换类似,我们也可以使用explicit
关键字来防止类类型之间的隐式转换。如下所示:
class B { public: explicit B(const A& a) : _b(a.Get()) {} void Print() { cout << _b << endl; } private: int _b; }; int main() { A objA(10); // B objB = objA; // 错误:explicit 阻止了隐式转换 B objB(objA); // 正确:显式调用构造函数 objB.Print(); // 输出: 10 }
在这个例子中,explicit
关键字阻止了A
对象隐式转换为B
对象,必须显式调用B
的构造函数。
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解2:https://developer.aliyun.com/article/1617497