C++ 类和对象4---(初始化列表,类型转化,static成员)

目录

1.再探构造函数---初始化列表

[1.1 定义](#1.1 定义)

[1.2 语法形式](#1.2 语法形式)

[1.3 核心特性](#1.3 核心特性)

[1.4 初始化逻辑](#1.4 初始化逻辑)

[1.5 示例](#1.5 示例)

[2. 类型转换](#2. 类型转换)

[2.1 内置类型->类类型隐式转换](#2.1 内置类型->类类型隐式转换)

[1. 定义](#1. 定义)

[2. 语法形式](#2. 语法形式)

[3. 核心特性](#3. 核心特性)

[4. 示例](#4. 示例)

[2.2 类类型->类类型隐式转换](#2.2 类类型->类类型隐式转换)

[1. 定义](#1. 定义)

[2. 核心特性](#2. 核心特性)

[3. 示例](#3. 示例)

[3. static成员](#3. static成员)

[3.1 静态成员变量](#3.1 静态成员变量)

[1. 定义与特点](#1. 定义与特点)

[2. 示例](#2. 示例)

[3.2 静态成员函数](#3.2 静态成员函数)

[1. 定义与特点](#1. 定义与特点)

[2. 示例](#2. 示例)

[3.3 核心总结](#3.3 核心总结)

[4. 总结:](#4. 总结:)


1.再探构造函数---初始化列表

1.1 定义

  • 什么是初始化列表呢?
  • 初始化成员变量主要有两种方式:
  1. **构造函数体内赋值 :**比如 name = n; age = a; , 但这种方式对于 const 成员 , 引用成员 , 没有默认构造函数的类类型成员等场景不适用 , 而且对于一些类类型成员 , 会先默认初始化再赋值 , 有额外开销。
  2. 初始化列表 : 这是更优的方式 , 能在成员变量创建时就直接初始化 , 避免额外操作 , 还能处理必须初始化的场景(如 const 成员 , 引用成员等)。

在C++中 , 构造函数的初始化列表是一种特殊的语法 , 用于在构造函数体执行之前初始化类的成员变量。它比在构造函数体内赋值更高效 , 且某些场景下必须使用。

1.2 语法形式

初始化列表的基本形式为:

cpp 复制代码
类名(参数列表) : 成员变量1(初始化值1), 成员变量2(初始化值2), ... 
{
    // 构造函数体(可选)
}

其中 , 类名是要定义构造函数的类的名称;参数列表是构造函数接收的参数;成员变量1 , 成员变量2等是类的成员变量;初始化值1 , 初始化值2等是用于初始化对应成员变量的值或表达式;冒号: 是初始化列表的起始标记 , 逗号用于分隔不同的成员初始化项 , 构造函数体可根据需要编写相关逻辑。

1.3 核心特性

初始化列表具有以下核心特性:

1. 初始化时机更早

  • 每个成员变量在初始化列表中只能出现一次 , 语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
  • 成员变量的初始化在初始化列表中完成 , 这一过程发生在构造函数体执行之前。而如果在构造函数体内对成员变量进行赋值 , 成员变量会先被默认初始化一次 , 之后再进行赋值操作 , 这会造成额外的性能开销 , 尤其是对于复杂的类类型对象 , 初始化列表的这种提前初始化的特性能显著提高效率。

2. 必须使用的场景

  • 常量成员( const 修饰的成员变量) : const 变量一旦初始化就不能再修改 , 所以必须在初始化列表中完成初始化 , 无法在构造函数体内通过赋值的方式设置其值。
  • 引用成员( & 修饰的成员变量): 引用必须在定义时就绑定到一个对象 , 不能先定义再赋值 , 因此只能在初始化列表中进行初始化。
  • 没有默认构造函数的类类型成员 : 如果类的成员是另一个类的对象 , 且该类没有默认构造函数(即没有无参数的构造函数) , 那么必须在初始化列表中调用该类的带参数构造函数来初始化这个成员对象 , 否则编译器无法自动生成默认的初始化代码。

3. 初始化顺序与声明顺序一致

  • 成员变量的初始化顺序是由它们在类中声明的顺序决定的 , 和在初始化列表中的顺序没有关系。例如 , 在类中先声明成员变量 a , 再声明成员变量 b , 那么无论在初始化列表中是 a 在前还是 b 在前 , a 都会先被初始化 , 然后才是 b 。如果不注意这一点 , 可能会导致一些逻辑错误 , 所以建议成员变量在类中的声明顺序和在初始化列表中的顺序保持一致。

4. 隐性存在性

  • 无论是否显式地写出初始化列表 , 每个构造函数都有初始化列表。而且不管成员变量有没有在初始化列表中显式地进行初始化 , 每个成员变量都会通过初始化列表来初始化(要么使用显式指定的值 , 要么使用缺省值 , 要么按照编译器或类的默认规则进行初始化)。
  • 尽量使用初始化列表初始化 , 因为那些你不在初始化列表初始化的成员也会走初始化列表 , **如果这个成员在声明位置给了缺省值 , 初始化列表会用这个缺省值初始化。**如果你没有给缺省值 , 对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器 , C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数 , 如果没有默认构造会编译错误。
  • C++11支持在成员变量声明的位置给缺省值 , 这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。

1.4 初始化逻辑

成员变量走初始化列表的逻辑可总结为:

  • 显式出现在初始化列表的成员变量 : 按照初始化列表指定的值完成初始化。
  • 未显式出现在初始化列表的成员变量:
    • 若在类声明中已设置缺省值 , 则按缺省值初始化
    • 若未设置缺省值:
      • 对于内置类型成员 : 初始化结果不确定(可能是随机值或默认值 , 如0) , 具体取决于编译器实现
      • 对于自定义类型成员:调用该类型的默认构造函数完成初始化
  • 特殊成员变量(引用 , const成员 , 无默认构造函数的成员):
    • 必须在初始化列表中显式初始化
    • 或在声明时提供缺省值
    • 否则将导致编译错误

1.5 示例

cpp 复制代码
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int& x, int year = 1, int month = 1, int day = 1)
		:_day(day)
		,_month(month)
		,_ref(x)
		,_i(10)
		,_t(1)
	{
		//_year = year;
		//...
	}
private:
	// 声明
	int _year = 2000;  // 缺省值
	int _month;
	int _day = 1;

	int& _ref;
	const int _i = 100;
	Time _t;
};

int main() {
    int num = 100;
    Date d(num, 2025, 8, 13);
    return 0;
}

我们结合 Date 类来详细讲讲初始化列表的使用。

1. 代码整体结构与类关系 : 代码中有两个类:

  1. Time 类 : 表示"时间" , 有一个int类型成员 _hour , 构造函数用初始化列表初始化 _hour。
  2. Date 类 : 表示"日期" , 包含多个成员(_year 、_month 、_day 、_ref 、_i 、_t) , 构造函数用初始化列表初始化这些成员。其中 _t 是Time类型的成员 , 因此 Date 的构造会涉及 Time 的构造。

2. 各成员初始化逻辑(结合初始化列表核心特性)

(1). Time 类的初始化(自身构造)

cpp 复制代码
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
  • 初始化列表作用 : Time 的构造函数通过 :_hour(our) 直接初始化成员 _hour , 避免了"先默认初始化 _hour , 再赋值"的额外开销 , 效率更高。

(2). Date 类的初始化包含多类型成员)

cpp 复制代码
class Date 
{
public:
    Date(int& x, int year = 1, int month = 1, int day = 1)
        : _day(day)       // 普通int成员,用参数day初始化
        , _month(month)   // 普通int成员,用参数month初始化
        , _ref(x)         // 引用成员,必须在初始化列表绑定引用
        , _i(10)          // const成员,必须在初始化列表初始化
        , _t(1)           // Time类型成员,调用Time(1)构造
    {
        // 构造函数体(此处可省略,或添加额外逻辑)
        cout << "Date 构造完成" << endl;
    }
private:
    int _year = 2000;  // 缺省值:若初始化列表不显示初始化,用2000
    int _month;
    int _day = 1;      // 缺省值:若初始化列表不显示初始化,用1
    int& _ref;         // 引用成员
    const int _i = 100;// 缺省值:若初始化列表不显示初始化,用100
    Time _t;           // Time类类型成员
};

对 Date 各成员的初始化逻辑分析:

  • _day(day) 、_month(month) : 普通int成员 , 用构造函数参数直接初始化 , 效率高于"先默认初始化再赋值"。
  • _ref(x): 引用成员必须在初始化列表绑定引用(因为引用不能"先定义 , 后赋值") , 这里绑定到参数 x (外部传入的 int 引用)。
  • _i(10): const 成员必须在初始化列表初始化(const变量初始化后不可修改) , 此处用10覆盖了声明时的缺省值100。
  • _t(1): Time 类类型成员 , 调用Time的构造函数Time(1) 初始化(因为Time没有默认构造 , 必须显式指定构造参数)。
  • _year : 声明时有缺省值2000 , 且初始化列表未显式初始化它 , 因此会用缺省值2000初始化。

(3). 主函数测试(验证初始化列表的执行逻辑)

cpp 复制代码
int main() {
    int num = 100;
    // 实例化Date对象,传入num的引用、year=2025、month=8、day=13
    Date d(num, 2025, 8, 13);
    return 0;
}

运行结果与分析 : 程序运行后 , 输出如下(顺序体现了"成员初始化顺序由声明顺序决定"):

cpp 复制代码
Time 构造:hour = 1
Date 构造完成
  • 首先 , Date 的成员 _t(Time类型)被初始化 , 调用 Time(1)) , 输出Time构造 : hour = 1 。
    • 然后 , Date自身的构造函数体执行 , 输出Date构造完成 。

(4). 初始化列表的核心特性总结

    1. 初始化时机更早 : _t 的初始化(调用Time构造)发生在Date构造函数体之前 , 避免了Time先默认构造再赋值的开销。
    1. 必须使用的场景全覆盖
  • 引用成员 _ref : 通过 _ref(x) 绑定引用。
  • const 成员 _i : 通过 _i(10) 初始化。
  • 无默认构造的类成员 _t : 通过 _t(1) 调用Time的带参构造。
    1. 初始化顺序由声明顺序决定: Date中成员声明顺序是_year→_month→_day→_ref→_i→_t ,因此初始化顺序也是如此(_t 虽然在初始化列表中写在较前位置 , 但实际在声明中最后 , 所以最后初始化 _t ?不 , 这里 _t 的声明在 Date 的私有成员中是最后一个 , 所以初始化顺序是:_year(缺省值)→ _month(参数)→ _day(参数)→ _ref(绑定)→ _i(10)→ _t(调用 Time(1))。运行结果中先输出 Time 构造 , 也验证了 _t 是最后初始化的成员。
    1. 隐性存在性 : 即使 Date 的构造函数体为空 , 所有成员(包括_year用缺省值, _t 用 Time(1) )都通过初始化列表完成了初始化。

通过这个例子 , 能清晰看到初始化列表在"高效初始化""处理特殊成员(引用 , const , 自定义类型)""遵循声明顺序"等方面的作用

2. 类型转换

在C语言中 , 我们学习过的类型转化主要是4个方面的:

    1. 整形之间
    1. 整形和浮点数之间
    1. 整形和指针之间
    1. 指针和指针之间

但是在C++中 , 我们学习的类型转化主要是围绕着内置类型和类型进行转化的 , 因此就衍生了下面两种类型的转化:

    1. 内置类型->类类型
    1. 类类型->类类型

2.1 内置类型->类类型隐式转换

1. 定义

内置类型到类类型的隐式转换 , 是指在C++中 , 当一个类存在以某个内置类型(int , double等)为参数的构造函数时 , 编译器可以自动将该内置类型的值转换为该类的对象 , 无需显式调用构造函数。

2. 语法形式

假设有类 MyClass , 其构造函数为 MyClass(内置类型参数) , 那么可以直接用内置类型的值来创建类对象 , 形式如下:

cpp 复制代码
MyClass obj = 内置类型值; 

或者将内置类型值绑定到类的引用(需是 const 引用 , 因为临时对象具有常性):

cpp 复制代码
const MyClass& ref = 内置类型值;

3. 核心特性

  • 依赖构造函数必须存在以目标内置类型为参数的构造函数 , 这是隐式转换的基础。编译器通过该构造函数来完成内置类型值到类对象的转换。
  • 自动进行:转换过程由编译器自动触发 , 不需要开发者手动调用构造函数来创建对象 , 简化了代码编写。
  • 可被 explicit 禁止:如果在构造函数前加上 explicit 关键字 , 那么这种隐式转换就会被禁止。此时 , 必须显式地调用构造函数(如 MyClass obj(内置类型值); )才能创建类对象 , 防止一些意外的 , 不符合预期的隐式转换发生。
  • 临时对象参与:当进行隐式转换时 , 会先创建一个类的临时对象 , 然后再根据具体场景(如对象赋值、引用绑定等)使用该临时对象。

4. 示例

假设有一个表示"分数"的类 Score , 其构造函数接收一个int类型的参数(表示分数值):

cpp 复制代码
class Score 
{
private:
    int value;
public:
    // 以int为参数的构造函数(支持隐式转换的关键)
    Score(int v)
    :value(v) 
    {} 
    
    void print() 
    {
        cout << "分数:" << value << endl;
    }
};

隐式转换的场景:

1. 直接赋值创建对象

cpp 复制代码
Score s = 90;  // 隐式转换:int类型 90 → Score对象
s.print();  // 输出:分数:90

这里编译器自动调用 Score(90) 构造函数 , 将 int 类型的 90 转换为 Score 对象。

2. 绑定到const引用

cpp 复制代码
const Score& rs = 85;  // 隐式转换:int类型 85 → 临时Score对象,rs绑定到该临时对象
rs.print();  // 输出:分数:85

3. 作为函数参数传递

cpp 复制代码
void showScore(Score s) 
{
    s.print();
}

showScore(95);  // 隐式转换:int类型 95 → Score对象,作为参数传入
// 输出:分数:95

用 explicit 禁止隐式转换:

如果构造函数被 explicit 修饰:

cpp 复制代码
class Score 
{
public:
    explicit Score(int v)
    : value(v) 
    {}  // 禁止隐式转换
    // ...(其他成员同上)
};

此时 , 上述隐式转换的写法会报错 , 必须显式调用构造函数:

cpp 复制代码
Score s(90);  // 正确:显式构造
const Score& rs = Score(85);  // 正确:显式创建临时对象
showScore(Score(95));  // 正确:显式转换后传参

2.2 类类型->类类型隐式转换

1. 定义

当类 B 中存在一个以类 A 的对象(或引用)为参数的构造函数时 , 编译器可以自动将 A 类型的对象转换为 B 类型的对象 , 无需显式调用构造函数 , 这就是类类型之间的隐式转换。

2. 核心特性

  • 依赖目标类的构造函数:转换的关键是目标类(如 B )有一个参数为源类(如 A )对象(或引用)的构造函数。
  • 自动触发:当需要 B 类型对象的场景中出现 A 类型对象时 , 编译器会自动调用 B 的对应构造函数完成转换。
  • 可被 explicit 禁止:若 B 的构造函数被 explicit 修饰 , 则隐式转换失效 , 必须显式调用构造函数(如 B b(a);)。

3. 示例

假设我们有 Student 类(源类型)和 Person 类(目标类型):

cpp 复制代码
// 源类:Student
class Student 
{
private:
    string name;
public:
    Student(string n) 
    : name(n)
    {}
    string getName() const 
    {
       return name;
    }
};

// 目标类:Person(有一个接收Student的构造函数)
class Person 
{
private:
    string personName;
public:
    // 以Student为参数的构造函数(支持隐式转换)
    Person(const Student& s)
    : personName(s.getName()) 
    {}
    
    void show() const 
    {
        cout << "Person: " << personName << endl;
    }
};

隐式转换场景:

1. 直接用 Student 对象创建 Person 对象

cpp 复制代码
Student s("张三");
Person p = s;  // 隐式转换:Student对象s → Person对象p
p.show();  // 输出:Person: 张三

2. 绑定到 const 引用

cpp 复制代码
const Person& rp = s;  // 隐式转换:s → 临时Person对象,rp绑定到该临时对象
rp.show();  // 输出:Person: 张三

3. 作为函数参数传递

cpp 复制代码
void printPerson(Person p) 
{
    p.show();
}
printPerson(s);  // 隐式转换:s → Person对象,作为参数传入
// 输出:Person: 张三

如果 Person 的构造函数被 explicit 修饰:

cpp 复制代码
class Person 
{
public:
    explicit Person(const Student& s) 
    : personName(s.getName()) 
    {}  // 禁止隐式转换
    // ...
};

则上述隐式转换写法会报错 , 必须显式转换

cpp 复制代码
Person p(s);  // 正确:显式转换
printPerson(Person(s));  // 正确:显式转换后传参

3. static成员

在 C++ 中 , static 成员(包括静态成员变量和静态成员函数)是属于类本身的成员 , 而非类的某个具体对象 , 其核心特性是共享性和独立性(不依赖于对象存在)。

3.1 静态成员变量

1. 定义与特点

  • 属于类 , 而非对象:所有对象共享同一份静态成员变量 , 内存中只存在一份拷贝。
  • 必须类外初始化:在类内声明 , 类外通过 类名::变量名 形式初始化(需指定类型) , 初始化时不加 static。
  • 访问方式:可通过 类名::变量名 直接访问 , 也可通过对象(对象.变量名 或 对象指针->变量名)访问。
  • 生命周期:从程序启动到结束 , 与全局变量类似 , 但作用域受类限制。

2. 示例

cpp 复制代码
class MyClass 
{
public:
    static int count;  // 类内声明静态成员变量
};

// 类外初始化(必须执行,否则链接错误)
int MyClass::count = 0;  

int main() 
{
    MyClass obj1, obj2;
    
    // 访问方式1:类名直接访问
    MyClass::count = 1;
    cout << MyClass::count << endl;  // 输出:1
    
    // 访问方式2:通过对象访问(共享同一份数据)
    obj1.count++; 
    cout << obj2.count << endl;  // 输出:2(obj1和obj2共享count)
    
    return 0;
}

3.2 静态成员函数

1. 定义与特点

  • 属于类 , 无 this 指针 : 静态成员函数不依赖于对象 , 因此无法访问类的非静态成员(非静态成员依赖 this 指针指向的对象) , 但可以访问静态成员变量。
  • 访问方式 : 同静态成员变量 , 可通过 类名::函数名 或对象调用。
  • 不能被 virtual 修饰 : 因为静态成员函数不与对象绑定 , 而虚函数依赖于对象的动态类型。

2. 示例

cpp 复制代码
class MyClass 
{
private:
    static int count;  // 静态成员变量
    int num;  // 非静态成员变量
public:
    // 静态成员函数:只能访问静态成员
    static void increment() 
    {
        count++;  // 正确:访问静态成员
        // num++;  // 错误:无法访问非静态成员
    }
    
    static int getCount() 
    {
        return count;
    }
};

int MyClass::count = 0;  // 初始化静态成员变量

int main()   
{
    MyClass obj;
    
    // 调用方式1:类名直接调用
    MyClass::increment();
    cout << MyClass::getCount() << endl;  // 输出:1
    
    // 调用方式2:通过对象调用
    obj.increment();
    cout << obj.getCount() << endl;  // 输出:2
    
    return 0;
}

3.3 核心总结

  • 静态成员变量:实现类级别的数据共享 , 需类外初始化 , 生命周期伴随程序。
  • 静态成员函数 : 用于操作静态成员 , 无this指针 , 不能访问非静态成员 , 可直接通过类名调用。
  • 两者均属于类 , 是实现"类级功能"(如计数器 , 工具方法)的重要机制。

4. 总结:

本文深入讲解了C++中的三个重要特性:构造函数初始化列表 , 类型转换和static成员。初始化列表部分详细介绍了其语法 , 核心特性(如初始化时机 , 必须使用场景)和初始化逻辑 , 通过Date类示例展示了实际应用。类型转换部分重点分析了内置类型到类类型和类类型之间的隐式转换机制 , 包括语法形式 , 核心特性和explicit关键字的作用。static成员部分则阐述了静态成员变量(类外初始化、共享性)和静态成员函数(无this指针、类级访问)的特点和使用方法。全文通过丰富示例帮助读者理解这些核心概念的实际应用场景和注意事项。以上就是关于本篇文章的所有内容 , 感谢大家的观看!