欢迎光临
我们一直在努力

【C+++】C++类和对象5:C++类型转换、初始化列表与static成员

📌 相关专栏

  • 【Linux专栏】
  • 【C语言专栏】
  • 【测试专栏】
  • 【MySQL专栏】
  • 【C++ 专栏】

📌 相关文章推荐

  • 【C++】C++类与对象4: 一文吃透C++友元、内部类、匿名对象与编译优化要点
  • 【Linux】 一文彻底搞懂传输层协议UDP!从端口号到传输机制一次讲透
  • 【C++】C++类与对象2:C++构造函数、运算符重载与流输入输出全面解析
  • 【测试】测试用例设计攻略(6大设计方法)

举爪爪打招呼

很高兴你点开这篇文章✨

这里会持续更新我喜欢的内容,关注我,一起慢慢变好呀

👍 点赞 ⭐ 收藏 💬 评论


文章目录

  • 前言
  • 一、初始化列表
    • 1.1 什么是初始化列表
    • 1.2 初始化 vs 赋值的本质区别
    • 1.3 必须在初始化列表中初始化的成员
    • 1.4 初始化顺序由声明顺序决定
    • 1.5 C++11:声明位置给缺省值
  • 二、隐式类型转换与explicit
    • 2.1 内置类型到类类型的隐式转换
    • 2.2 引用绑定临时对象
    • 2.3 explicit关键字:禁止隐式转换
    • 2.4 隐式转换的实际应用场景
  • 三、static静态成员
    • 3.1 静态成员变量
    • 3.2 静态成员变量的特点
    • 3.3 静态成员函数
    • 3.4 静态成员变量不能在声明位置给缺省值
  • 四、知识点汇总
  • 五、常见面试题
  • 六、本文所有代码
    • 🐾 </h3>Practice.cpp
    • 🐾 </h3>Test.cpp

前言

在前几篇文章中,我们学习了类的基础和进阶特性。这一篇我们来聊三个"细节决定成败"的知识点:

  • 初始化列表: 真正初始化成员变量的地方
  • 类型转换: 内置类型如何变成类类型
  • static成员: 属于类本身而非对象的成员

理解这些,我们就能真正掌握C++对象构造的底层机制。

🐶 🐾 ✨ 🐾 🐶


一、初始化列表

1.1 什么是初始化列表

初始化列表是构造函数中,在函数体之前用冒号和逗号分隔的成员变量初始化区域。

  • 所有传参成员变量定义初始化的地方,当进入函数体内时就已经完成了初始化
  • 对于自定义类型成员,如果存在了对于的默认构造函数是可以不在初始化列表进行初始化
  • 但是如果没有对于的默认构造函数则一定要在初始化列表进行初始化

class Date
{
public:
// 初始化列表:以冒号开始,逗号分隔
Date(int year, int month, int day)
: _year(year) // 初始化 _year
, _month(month) // 初始化 _month
, _day(day) // 初始化 _day
{
// 函数体内是赋值,不是初始化
}
private:
int _year;
int _month;
int _day;
};


1.2 初始化 vs 赋值的本质区别

对比项初始化列表构造函数体内
本质 成员变量定义并初始化 成员变量定义后赋值
执行时机 对象创建时立即执行 初始化列表之后执行
const成员 可以初始化 不能赋值
引用成员 可以绑定 不能绑定
效率 高(直接构造) 低(先默认构造再赋值)

class A
{
public:
// 初始化列表:真正的初始化
A(int a) : _a(a)
{

}

// 函数体内:先默认初始化,再赋值
// A(int a) //初始化完成
//{
// _a = a; // 这是赋值,不是初始化
// }
private:
int _a;
};


1.3 必须在初始化列表中初始化的成员

C++规定以下三种成员必须在初始化列表中初始化:

成员类型原因
const成员变量 const变量定义时必须初始化
引用成员变量 引用定义时必须绑定对象
没有默认构造函数的类类型成员 编译器无法自动构造

class Time
{
public:
Time(int hour) : _hour(hour) // 有参构造,无默认构造
{

}
private:
int _hour;
};

class Date
{
public:
Date(int& x, int year=1,int month=1,int day=1)
: _year(year) // ✅ 普通成员
,_month(month)
,_day(day)
, _ref(x) // ✅ 引用成员(必须初始化)
, _n(1) // ✅ const成员(必须初始化)
, _t(1) // ✅ 无默认构造的成员(必须初始化)
{}

private:
int _year;
int _month;
int _day; // 普通成员

int& _ref; // 引用成员
const int _n; // const成员
Time _t; // 无默认构造的自定义类型
};

int main()
{
int i = 0;
Date d1(i, 2026);
return 0;
}


🐾 如果没有在初始化列表对 const 和引用成员进行初始化则失去了唯一一次初始化机会,当进入函数体内时相当于就已经变成了:const int _n; 和 int& _ref;

  • error C2530 : “Date::_ref” : 必须初始化引用
  • error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
  • error C2512: “Time”: 没有合适的默认构造函数可用

🐾 _n = 1

  • 在初始化列表中我们已经对 const 成员变量_n进行初始化
  • 此时在函数体内再次写导致报错,这就说明在函数体内并非我们认为的“初始化”而是进行赋值
  • 而const一旦初始化了就不能再进行赋值修改了

1.4 初始化顺序由声明顺序决定

重要规则 :初始化列表的执行顺序不是按列表中的顺序,而是按成员变量在类中声明的顺序。

class A
{
public:
A(int a)
: _a1(a) // 列表写的是 _a1 在前
, _a2(_a1) // _a2 在后
{
// 但实际执行顺序由声明顺序决定!
}
void Print() { cout << _a1 << " " << _a2 << endl; }

//声明
private:
int _a2 = 2; // 声明顺序:_a2 在前
int _a1 = 2; // 声明顺序:_a1 在后
};

int main()
{
A aa(1);
aa.Print(); // 输出:1 随机值
// 原因:先初始化 _a2(用未初始化的 _a1,得随机值)
// 再初始化 _a1(用参数1)
return 0;
}

我当时很懵,我以为应该是1 1,后来才知道问题出在哪。

  • 初始化列表的执行顺序,不是按我写的_a1(a)在前、_a2(_a1)在后这个顺序,而是按成员变量在类中声明的顺序。
  • 我的声明顺序是先_a2再_a1,所以实际执行顺序是:
    • 1. 先执行_a2(_a1),此时_a1还没初始化,是随机值,所以_a2 = 随机值
    • 2. 再执行_a1(a),把1赋给_a1

输出就是1和随机值。

建议:初始化列表的顺序和声明顺序保持一致,不然容易出这种坑。


1.5 C++11:声明位置给缺省值

C++11允许在成员变量声明的位置给缺省值,作为初始化列表的"后备方案"。

class Date
{
public:
Date(int year, int month)
: _year(year) // 使用传入的值
, _month(month) // 使用传入的值
{
// _day 没有在列表中,使用声明的缺省值 1
}

private:
int _year = 1; // 缺省值
int _month = 1; // 缺省值
int _day = 1; // 缺省值
};

// 如果既有缺省值又在初始化列表中,以初始化列表为准

🐶 🐾 ✨ 🐾 🐶


二、隐式类型转换与explicit

2.1 内置类型到类类型的隐式转换

当构造函数只有一个参数(或有除第一个参数外都有缺省值)时,编译器支持隐式类型转换。

class A
{
public:
// 单参数构造函数
A(int a=1)
: _a(a)
{

}

void Print()
{
cout << _a << endl;
}

private:
int _a;
};

int main()
{
A a1(1); // 直接构造
A a2 = 2; // 隐式转换:2 -> 临时A对象 -> 拷贝构造 -> 编译器优化为直接构造

// 理论流程:
// int 2
// ↓ 调用A(int)构造临时对象
// 临时A对象
// ↓ 拷贝构造a2
// a2

// 实际编译器优化后:直接构造a2,不产生临时对象
return 0;
}

A a2 = 2 这行代码发生了两步:

  • 2先去调用构造函数构造一个A的临时对象
  • 再用这个临时对象拷贝构造a2

  • 2.2 引用绑定临时对象

    临时对象具有常性,只能用const引用绑定。

    int main()
    {
    int i = 1;
    const double& ref1 = i; // int-> double生成临时对象,用const引用绑定

    A a1(1);
    const A& ref2 = a1; // 普通引用,绑定已有对象
    const A& ref3 = 1; // 1隐式转换生成临时A对象,const引用可延长生命周期

    return 0;
    }


    2.3 explicit关键字:禁止隐式转换

    在构造函数前加explicit,可以禁止编译器进行隐式类型转换。

    class A
    {
    public:
    explicit A(int a) : _a(a) {} // 加explicit,禁止隐式转换
    private:
    int _a;
    };

    void func(const A& aa) {}

    int main()
    {
    A a1(1); // ✅ 直接构造
    // A a2 = 2; // ❌ 错误!explicit禁止隐式转换
    // func(1); // ❌ 错误!不能将int隐式转换为A
    // const A& ref = 1; // ❌ 错误!不能隐式转换

    func(a1); // ✅ 正常传对象
    return 0;
    }


    2.4 隐式转换的实际应用场景

    class Stack
    {
    public:
    void Push(const A& a) {}
    };

    int main()
    {
    Stack s1;

    // 麻烦写法
    A a3(3);
    s1.Push(a3);

    // 简洁写法:利用隐式转换
    s1.Push(3); // 3 -> 临时A对象 -> 传给Push

    return 0;
    }

    🐶 🐾 ✨ 🐾 🐶


    三、static静态成员

    3.1 静态成员变量

    静态成员变量属于类本身,而不是属于某个对象。所有对象共享同一份静态成员变量。

    class A
    {
    public:
    A() { ++_scount; } // 构造时计数+1
    A(const A& t) { ++_scount; } // 拷贝构造时计数+1
    ~A() { _scount; } // 析构时计数-1

    static int GetACount() { return _scount; } // 静态成员函数

    private:
    static int _scount; // 静态成员变量(声明)
    int i = 0; // 普通成员变量
    };

    // 静态成员变量必须在类外定义并初始化
    int A::_scount = 0;

    int main()
    {
    cout << A::GetACount() << endl; // 0

    A a1, a2;
    {
    A a3(a1);
    cout << A::GetACount() << endl; // 3
    }

    cout << A::GetACount() << endl; // 2(a3已销毁)
    return 0;
    }


    3.2 静态成员变量的特点

    特点说明
    存储位置 静态区(不在对象中)
    对象大小 不包含静态成员变量
    访问方式 可通过类名或对象访问
    生命周期 程序运行时存在,程序结束才销毁
    初始化 必须在类外定义和初始化

    cout << sizeof(A) << endl; // 4(只包含普通成员i,不包含_scount)
    cout << A::_scount << endl; // 需要public才能访问,一般用静态成员函数
    cout << A::GetACount() << endl; // 推荐方式
    cout << a1.GetACount() << endl; // 也可以通过对象访问


    3.3 静态成员函数

    静态成员函数没有this指针,因此:

    • 只能访问静态成员变量
    • 不能访问非静态成员变量
    • 不能调用非静态成员函数

    class A
    {
    public:
    static int GetACount()
    {
    // i++; // ❌ 错误!不能访问非静态成员
    // Print(); // ❌ 错误!不能调用非静态成员函数
    return _scount; // ✅ 可以访问静态成员变量
    }

    void Print() { cout << i << endl; }

    private:
    static int _scount;
    int i = 0;
    };


    3.4 静态成员变量不能在声明位置给缺省值

    class A
    {
    private:
    static int _scount = 1; // ❌ 错误!静态成员变量不能在类内初始化
    int i = 0; // ✅ 普通成员可以给缺省值
    };
    // 原因:缺省值是给初始化列表用的,静态成员不走初始化列表

    🐶 🐾 ✨ 🐾 🐶


    四、知识点汇总

    知识点核心要点
    初始化列表 成员变量定义初始化的地方,在构造函数体之前执行
    必须用初始化列表 const成员、引用成员、无默认构造的类类型成员
    初始化顺序 按成员声明顺序,不是列表顺序
    声明给缺省值 C++11特性,作为初始化列表的后备
    隐式类型转换 单参数构造函数支持,用explicit禁止
    临时对象 具有常性,需const引用绑定
    static成员变量 属于类,所有对象共享,类外初始化
    static成员函数 无this指针,只能访问静态成员

    🐶 🐾 ✨ 🐾 🐶


    五、常见面试题

    🐾 Q1:初始化列表和构造函数体内赋值的区别?

    • 初始化列表是真正的初始化(定义时赋值),函数体内是赋值(先定义再赋值)。const成员和引用成员必须在初始化列表中完成。

    🐾 Q2:成员变量的初始化顺序由什么决定?

    • 由成员变量在类中的声明顺序决定,与初始化列表中的书写顺序无关。

    🐾 Q3:explicit关键字的作用?

    • 禁止单参数构造函数的隐式类型转换,防止不经意间的类型转换带来的问题。

    🐾 Q4:静态成员函数可以访问非静态成员吗?

    • 不可以。静态成员函数没有this指针,无法访问属于具体对象的非静态成员。

    🐾 Q5:静态成员变量为什么必须在类外初始化?

    • 静态成员变量属于类,不属于任何对象,不走初始化列表,需要在类外单独定义和分配存储空间。

    🐶 🐾 ✨ 🐾 🐶


    六、本文所有代码

    🐾 Practice.cpp

    #define _CRT_SECURE_NO_WARNINGS 1
    #include<iostream>
    using namespace std;

    //初始化列表练习题
    class A
    {
    public:
    A(int a)
    :_a1(a)
    ,_a2(_a1)//先初始化_a2再初始化_a1
    {

    }
    void Print()
    {
    cout << _a1 << " " << _a2 << endl;
    }
    private:
    int _a2 = 2;
    int _a1 = 2;
    //初始化列表中按照成员变量在类中声明顺序进行初始化,
    //跟成员在初始化列表出现的的先后顺序无建议声明顺序和初始化列表顺序保持一致。
    };

    int main()
    {
    A aa(1);
    aa.Print(); //1 随机值
    }


    🐾 Test.cpp

    #define _CRT_SECURE_NO_WARNINGS 1
    #include<iostream>
    using namespace std;

    //初始化列表
    class Time
    {
    public:
    Time(int hour) //有参构造函数,此时_t没有对应的默认构造函数,就必须在初始化列表进行初始化,否则报错
    :_hour(hour)//_hour=hour
    {
    cout << "Time()" << endl;
    }
    private:
    int _hour;
    };

    class Date
    {
    public:
    Date(int& x, int year = 1, int month = 1, int day = 1)
    :_year(year)
    , _month(month)
    , _day(day)
    , _ref(x)
    , _n(1)//初始化列表:所有 传参成员变量定义初始化的地方,当进入函数体内时就已经完成了初始化
    , _t(1) //对于自定义类型成员,如果存在了对于的默认构造函数是可以不在初始化列表进行初始化
    //但是如果没有对于的默认构造函数则一定要在初始化列表进行初始化
    //写了初始化相当于就变成了 Time _t(1); 此时有了实参也就能调用有参的构造函数了
    {
    //_n = 1; //在初始化列表中我们已经对 const 成员变量_n进行初始化
    // //此时在函数体内再次写导致报错,这就说明在函数体内并非我们认为的“初始化”而是进行赋值
    // //而const一旦初始化了就不能再进行赋值修改了

    //如果没有在初始化列表对 const 和引用成员进行初始化则失去了唯一一次初始化机会
    //当进入函数体内时相当于就已经变成了:const int _n; 和 int& _ref;
    // error C2530 : “Date::_ref” : 必须初始化引用
    // error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
    // error C2512: “Time”: 没有合适的默认构造函数可用
    }

    void Print() const
    {
    cout << _year << "-" << _month << "-" << _day << endl;
    }

    private:
    //声明
    int _year;
    int _month;
    int _day;

    int& _ref;
    const int _n; //这里并非是定义而只是声明,只是告诉你有这个东西
    //而我们讲过 const 和引用在定义时就必须立即绑定初始化,但这里只是声明就不会报错
    //真正定义初始化的地方是在构造函数的初始化列表中

    Time _t;
    };

    int main()
    {
    //int x; //内置类型定义时可以不初始化,编译器会进行“默认初始化”
    //const int a; //error C2734: “a”: 如果不是外部的,则必须初始化常量对象
    //Date& d1; //error C2530: “d1”: 必须初始化引用
    const int a = 0; //const变量在定义时就必须立即绑定初始化,否则报错

    int i = 0;
    Date d1(i);
    Date& d2 = d1; //引用和const同理,在定义时就必须立即绑定初始化,否则报错

    return 0;
    }

    class Time
    {
    public:
    Time(int hour)
    :_hour(hour)
    {
    cout << "Time()" << endl;
    }
    private:
    int _hour;
    };

    class Date
    {
    public:
    Date(int year = 1, int month = 1, int day = 1)
    :_year(year)
    , _month(month)
    , _day(day)
    //, _ref(x)
    //, _n(1)
    //, _t(1)
    {

    }

    //Date(int year = 1, int month = 1, int day = 1)
    ////:_year(year)
    ////, _month(month)
    ////, _day(day)
    ////, _ref(x)
    ////, _n(1)
    ////, _t(1)
    //{
    ////_day = day;
    ////这个构造函数代码就能完全体现出声明给缺省值和函数形参给缺省值两者的区别
    ////形参给不给缺省值主要决定两点(1)对象能否调用该函数(2)声明没有缺省值则由形参的缺省值来控制初始化
    //
    ////而一个对象能否成功初始化取决于的是是否有初始化列表和声明是否给缺省值
    ////如果有显示写初始化列表则有形参的值来决定初始化
    // //如果没有初始化列表则必须要有声明给缺省值,否则就不能完成真正的“初始化”
    //}

    void Print() const
    {
    cout << _year << "-" << _month << "-" << _day << endl;
    }

    private:
    int _year;
    int _month;
    int _day;

    ////C++11
    ////声明位置给缺省值 -> 初始化列表用的
    //int _year = 1111;
    //int _month = 1;
    //int _day = 11;

    int* _ptr = (int*)malloc(10); //这说明缺省值不一定就是值,也可以是表达式

    int i = 1;
    int& _ref = i;
    const int _n = 1;

    Time _t = 1;
    //如果给了缺省值则对于 const 成员、引用成员和没有默认构造函数的类成员都允许不显示在初始化列表中初始化
    };

    int main()
    {
    //对象定义
    //Date d1(2025, 1, 15);//对于 _day 既没有给缺省值也没有在初始化列表进行初始化的内置类型成员
    // //则就是生成一个随机值
    //d1.Print();

    Date d1(2026, 1, 15); //但是给了缺省值并且没有在初始化列表进行初始化,
    //则初始化列表会用这个缺省值进行初始化
    //但是如果既有缺省值也在初始化列表进行初始化则仍以初始化列表进行初始化
    d1.Print();
    return 0;
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    //类型转换
    class A
    {
    public:
    A(int a = 1)
    :_a(a)
    {

    }

    //多参数类型转换
    /*A(int a1 = 1, int a2 = 1)
    :_a1(a1)
    ,_a2(a2)
    {

    }*/

    A(const A& ra)//拷贝构造函数
    :_a(ra._a)
    {

    }

    void Print()
    {
    cout << _a << endl;
    }

    private:
    int _a = 1;
    int _a1 = 1;
    int _a2 = 1;
    };

    //类型转换实际应用
    class Stack
    {
    public:
    void Push(const A& ra)
    {
    //……
    }
    private:
    A _arr[10];
    int _top;
    };

    int main()
    {
    int i = 1;
    double d = i;

    //double& rd = i; //error C2440: “初始化”: 无法从“int”转换为“double &”
    const double& rd = i;

    A a1(1);
    a1.Print();

    //隐式类型转换
    //有两个过程:(1)2先去调用构造函数构造一个A的临时对象(临时对象的类型与a2一样)
    // (2)再通过临时对象调用拷贝构造函数拷贝构造a2
    //但如果编译器遇到连续构造+拷贝构造 -> 会优化为直接构造
    A a2 = 2;
    A a3 = 2.2;//C++支持double类型隐式转换成int类型,所以可以类型转换
    //A a4 = "xxx";//但是C++不允许const char[4]到int的隐式类型转换,没有直接的类型转换途径
    //error C2440: “初始化”: 无法从“const char [4]”转换为“A”

    A& a4 = a2;
    //A& a5 = 2;//error C2440: “初始化”: 无法从“int”转换为“A &”
    const A& a5 = 2;
    //和内置类型转换一样,由于类型转换生成的临时对象具有常性,必须加上 const 引用,否则会权限放大导致报错

    Stack st;
    //正常写法:先写一个A对象,再用A对象调用Push函数
    A aa1(1);
    st.Push(aa1);

    //类型转换的好处
    st.Push(3);
    return 0;

    //多参数的类型转换
    //A aa2 = { 1, 1 };
    //const A& aa3 = { 1, 1 };
    //st.Push(aa2);

    //st.Push({ 1, 1 }); //等价于 A aa2 = { 1, 1 }; + st.Push(aa2);
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    //static成员
    class A
    {
    public:
    A()
    {
    ++_scount; //非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
    //再次强调的是:因为静态成员变量不属于任何对象,这里并不是用隐含 this 指针进行访问,不要混淆
    }

    A(const A& t)
    {
    ++_scount;
    }

    ~A()
    {
    _scount;
    }

    static int GetACount() //静态成员函数,没有隐含的 this 指针形参
    {
    //i++; //error C2597: 对非静态成员“A::i”的非法引用
    //因为静态成员函数没有 this 指针,成员变量只能通过隐含的 this 指针来访问(this -> i)
    //这也说明了成员变量是独属于每个对象的(独一无二的)
    return _scount;
    }

    private:
    // 类里面声明
    static int _scount;
    //static int _scount = 1; //error
    //声明给缺省值是为初始化列表服务的,
    //当初始化列表没有显示写一个成员变量的初始化时,声明给缺省值就起到作用了
    //在初始化列表中初始化的成员变量是独属于每个对象的(独一无二的)
    //但是静态成员变量并不存在对象中,也就是说静态成员变量是不会走初始化列表的,也就不能给缺省值了
    int i = 0; //普通成员变量
    };

    // 类外面初始化
    int A::_scount = 0; //在类外就需要用 :: 指定类域才能进行初始化

    int main()
    {
    //cout << sizeof(A) << endl; //A的大小为1,也说明了A中并没有静态成员变量 _scount,其是存放在静态区

    //cout << A::_scount << endl;//静态成员变量突破类域就可以直接进行访问,但还是受访问限定符的限制
    cout << A::GetACount() << endl; //静态成员函数突破类域也能直接进行访问
    A a1, a2;
    {
    A a3(a1);
    cout << A::GetACount() << endl;
    cout << a1.GetACount() << endl;
    cout << a2.GetACount() << endl;
    cout << a3.GetACount() << endl;
    //创建三个对象进行初始化时,静态成员变量_scount进行了三次++,并且因为共享均为3
    }//代码块的局部域
    //当出了代码块时则会自动调用析构函数,此时则_scount–

    cout << A::GetACount() << endl;
    return 0;
    }


    下一篇我们来学习:

    • 内存管理
    • new
    • delete

    🐶 🐾 ✨ 🐾 🐶


    举爪爪求关注

    谢谢你看到这里呀

    如果喜欢这篇内容,点个关注,下次更新不迷路✨

    👍 点赞 ⭐ 收藏 💬 评论

    赞(0)
    未经允许不得转载:171主机测评 » 【C+++】C++类和对象5:C++类型转换、初始化列表与static成员
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址