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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 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

目录
相关文章
|
1月前
|
自然语言处理 编译器 Linux
|
26天前
|
设计模式 安全 数据库连接
【C++11】包装器:深入解析与实现技巧
本文深入探讨了C++中包装器的定义、实现方式及其应用。包装器通过封装底层细节,提供更简洁、易用的接口,常用于资源管理、接口封装和类型安全。文章详细介绍了使用RAII、智能指针、模板等技术实现包装器的方法,并通过多个案例分析展示了其在实际开发中的应用。最后,讨论了性能优化策略,帮助开发者编写高效、可靠的C++代码。
35 2
|
4天前
|
安全 编译器 C++
C++ `noexcept` 关键字的深入解析
`noexcept` 关键字在 C++ 中用于指示函数不会抛出异常,有助于编译器优化和提高程序的可靠性。它可以减少代码大小、提高执行效率,并增强程序的稳定性和可预测性。`noexcept` 还可以影响函数重载和模板特化的决策。使用时需谨慎,确保函数确实不会抛出异常,否则可能导致程序崩溃。通过合理使用 `noexcept`,开发者可以编写出更高效、更可靠的 C++ 代码。
12 0
|
4天前
|
存储 程序员 C++
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
22 0
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
2月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
29 1
|
2月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
73 2
|
2月前
|
C++
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
29 0
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
71 2

推荐镜像

更多
下一篇
DataWorks