【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解1

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解

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 初始化列表总结

  1. 每个构造函数都有初始化列表,即使你没有显式地写出它。
  2. 每个成员变量都必须被初始化,即使它没有在初始化列表中显式地被初始化。
  3. 对于引用类型常量没有默认构造函数的类类型成员,必须在初始化列表中进行初始化。
  4. C++11 允许在成员变量声明时提供默认值,这些默认值会在初始化列表中未显式初始化时使用。
  5. 初始化顺序取决于成员变量在类中的声明顺序,而不是它们在初始化列表中的顺序。

2. 类型转换详解

在C++中,类型转换(Type Conversion)是指将一种数据类型转换为另一种数据类型的过程。对于类而言,C++允许将内置类型类类型转换为其他类类型,这一功能在面向对象编程中非常有用。类型转换可以是显式的(explicit)或隐式的(implicit),并且它们涉及构造函数、转换运算符和explicit关键字。

2.1 内置类型转换为类类型

C++支持将内置类型(如intdouble等)隐式地转换为自定义的类类型。这是通过定义带有内置类型参数的构造函数来实现的。

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

目录
相关文章
|
20天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
21 4
|
20天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
19 4
|
20天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
17 1
|
26天前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
60 0
|
26天前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
49 0
|
26天前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
57 0
|
26天前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
72 0
|
6天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
24 3
|
23天前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
48 5
|
25天前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)

推荐镜像

更多