C++类和对象进阶:初始化列表和static成员深度详解

1. 前言

在C++面向对象编程中,构造函数初始化列表和静态成员是提升代码质量与安全性的重要特性。本文深度详解关于C++构造函数的初始化列表C++类中的static成员,其中static成员包含static成员变量static成员函数,以及介绍static的相关实践场景

2. 构造函数初始化成员变量的方式

2.1 构造函数体内赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

class Date{
    
    
public:
	//赋值方式初始化
	Date(int year, int month, int day){
    
    
		_year = year;
		_month = month;
		_day = day;
 	}
private:
	int _year;
	int _month;
	int _day;
};

我们可以利用以上构造函数,在创建一个对象的时候对该对象进行初始化。

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化构造函数体中的语句只能将其称为赋初值,,而不能称作初始化。
因为初始化只能初始化一次,而构造函数体内可以多次赋值。

2.2 初始化列表

初始化列表:以一个冒号开始,接着是一个**以逗号分隔的数据成员列表**, 每个"成员变量"后面跟一个放在括号中的初始值或表达式

例如:

class Date{
    
    
public:
	//初始化列表初始化
	Date(int year, int month, int day)
 		: _year(year)
 		, _month(month)
 		, _day(day)
 	{
    
    }
private:
	int _year;
	int _month;
	int _day;
};
int main(){
    
    
	Date d1(2025, 2, 18);
	//传入的参数会通过初始化列表的方式进行初始化。
	return 0;
}

2.2.1 初始化列表的注意事项

首先回顾一下往期文章提出的对默认构造函数的理解:
C++类和对象进阶:构造函数和析构函数详解

默认构造函数,以下三种函数都可以被称作是默认构造函数。

  1. 无参构造函数。
  2. 全缺省构造函数。
  3. 我们没写编译器默认生成的构造函数
  • 总结来说就是,不需要传参的构造函数,都属于是默认构造函数
  1. 无参构造函数。没有参数,因此无需传参。
  2. 全缺省构造函数。参数全缺省,不需要传参。
  3. 我们没写编译器默认生成的构造函数。编译器生成的,我们无法显示调用。自动调用时无需传参。

注意

  1. 要把初始化列表理解成非静态成员变量创建(占内存)的地方。类中只是成员变量的声明
  2. 无论是否显式指定初始化列表,都会走一遍初始化列表来对成员变量进行初始化。
    (静态成员变量在类外进行初始化)
  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
    • 引用成员变量
    • const成员变量
    • 自定义类型成员(且该类没有默认构造函数时)

分析这三类变量的特征:

  • 引用成员变量:引用成员变量的初始化是“绑定”过程,而非赋值操作。构造函数体内无法完成初始化(此时成员已定义,只能赋值),因此必须在初始化列表(引用变量创建时)初始化
  • const成员变量const成员变量的初始化是“定义时赋值”,构造函数体内无法修改其值。const变量在定义后其值不可修改,因此必须在创建时初始化,也就是必须在初始化列表初始化
  • 自定义类型成员(该类没有默认构造函数时):编译器只会调用默认构造函数,若类没有默认构造函数(即无参构造函数或所有参数均有默认值的构造函数或编译器自己生成的构造函数),也就说程序员自己写了构造函数且调用时需要显式传参,则必须显式调用其某个构造函数,该工作在初始化列表中进行。

对比以上三类特殊情形:

变量类型 核心特征 初始化方式 未正确初始化的后果
引用成员变量 必须绑定对象,不可重新绑定 初始化列表中绑定 编译错误(未初始化引用)
const成员变量 不可修改,需定义时赋值 初始化列表中赋值 编译错误(未初始化常量)
自定义类型成员(无默认构造函数) 必须显式调用构造函数 初始化列表中调用带参构造函数 编译错误(找不到默认构造函数)

2.3 初始化列表的初始化顺序

成员变量在类中声明次序就是其在初始化列表中的初始化顺序与其在初始化列表中的先后次序无关

我们来看如下代码:

class MyClass {
    
    
public:
	MyClass(int init_value)
		: _value1(init_value)   // 类中的声明顺序决定初始化顺序
		, _value2(_value1)  // _value2先声明,会先用 _value1 初始化 _value2
							// 此时 _value1 还未完成初始化,是随机值
	{
    
    }
	void Print() const {
    
    
		cout << "_value1: " << _value1
			<< "  _value2: " << _value2 << endl;
	}
private:
	int _value2;     
	int _value1;
};
int main() {
    
    
	MyClass obj(5);
	obj.Print();  // 输出:_value1: 5  _value2: 随机值
	return 0;
}

在这里插入图片描述

结论:

  • 初始化列表中,变量初始化的顺序应该和变量在类中声明的次序保持一致
  • 尽量使用初始化列表对成员变量进行初始化,因为不管程序员是否使用初始化列表,对于自定义类型成员变量,一定会先试用初始化列表初始化。
  • 不能在初始化列表中完成的,在函数体内用语句来完成(如开辟空间后对指针的检查等)

初始化列表总结:

  • ⽆论是否显式写初始化列表,每个构造函数都有初始化列表
  • ⽆论是否在初始化列表显式初始化成员变量,每个成员变量都要⾛初始化列表初始化
    在这里插入图片描述

3. 类的静态成员

3.1 引入

设计一个类,计算程序中创建了多少个该类的类对象

#include <iostream>
//设计一个程序,统计当前正在使用的某个对象有多少个
int _scount = 0;	//我们可以利用全局变量

class A {
    
    
public:
	A() {
    
     ++_scount; }	//构造函数
	A(const A& t) {
    
     ++_scount; }	//拷贝构造函数
	~A() {
    
     --_scount; }		//析构函数
};
int main() {
    
    
	cout << __LINE__ << ": " << _scount << endl;	// 是 1 ,此处还没进入Func函数,static 对象还没创建
	A aa1;
	Func();	//3
	Func(); //3
	return 0;
}

在这里插入图片描述
以上程序确实可以实现统计,但全局变量有极大的缺陷:

void Func() {
    
    
	static A aa2;	//局部静态对象,只会创建一次,不在函数栈帧内,在静态区
	cout << __LINE__ << ": " << _scount << endl;	//3
	//全局变量的劣势:任何地方都可以随意改变,不安全
	//_scount++;
}

因此,我们想到了利用类来对计数器进行封装,并将计数器设置成静态成员变量。

什么是静态成员?

  • 声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量
  • static修饰的成员函数,称之为静态成员函数静态成员变量一定要在类外进行初始化。

利用静态成员实现的类:

class A {
    
    
public:
	A(){
    
     
		++_scount;}
	A(const A& t) {
    
     ++_scount; }
	~A() {
    
     --_scount; }
private:
	static int _scount;		//此处只是该成员变量的声明
	//类内的静态成员,相当于用类去封装全局变量
};
//全局位置,类外定义   类的 static 成员,类内声明,类外初始化  static 成员定义时不受访问限定符的限制
int A::_scount = 0;
  • 需要尤其注意,静态成员变量,在类内是声明,需要在类外进行初始化:int A::_scount = 0;这是规定的写法,通过类作用域限定符来访问

3.2 静态成员变量

private:
	// 非静态成员变量 ----- 属于每一个类对象, 存储在对象里面
	int _a1 = 1;	//成员变量给缺省值,会自动进入初始化列表
	int _a2 = 2;

	// 静态成员变量 ----- 属于类,类的每个对象共享,存储在静态区, 生命周期是全局的,不能用初始化列表初始化
	static int _scount;
};

需要注意:静态成员变量和非静态成员变量存储的位置不同。

  • 静态成员变量:属于类内,类的每个对象共享,存储在静态区, 生命周期是全局的,程序运行期间持续存在,不能用初始化列表初始化。
  • 非静态成员变量: 属于每一个类对象, 存储在对象里面

3.3 静态成员函数

静态成员函数一般是和静态成员变量成对出现的。
我们在类中添加以下函数方便我们获取_scount的值

public:
static int GetACount() {
    
    
	return _scount;
}

静态成员函数的特点:

  • 没有this指针
  • 指定类域和访问限定符就可以访问
  • 可以直接访问类内的静态成员变量

通过指定类域和访问限定符访问静态成员函数。

int main(){
    
    
	//由于静态成员变量是私有的
	//可以通过静态成员函数来访问静态成员变量
	cout << A::GetACount() << endl;
	return 0;
}

因此我们可以得出:

  1. 静态成员函数,不能访问类内的非静态成员变量,因为没有this指针(没有传入调用对象的地址)
  2. 静态成员函数不能调用非静态成员函数,非静态成员函数的调用需要传递this指针,但static成员函数没有this指针

3.4 静态成员的注意事项

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。类内声明,类外初始化
  3. 类静态成员(成员变量和成员函数)可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能用const修饰不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
  6. 静态成员变量:不能是auto推导类型。

在这里插入图片描述

  • 核心特点
    • 静态成员函数无this指针,无法访问非静态成员
    • 静态成员函数可直接调用无需实例化

3.5 静态成员的另一个实践场景

静态成员函数的妙用
设计一个类,在类外只能在 栈 上创建对象
设计一个类,在类外只能在 堆 上创建对象

我们可以这样做

class Obj {
    
    
public:
	//通过类作用域限定符来调用函数获取对象
	static Obj GetStackObj() {
    
    
		Obj obj;
		return obj;
	}
	static Obj* GetHeapObj() {
    
    
		Obj* obj = new Obj;
		return obj;
	}
	//构造函数私有化,防止直接调用构造函数在栈或堆上创建对象。
private:
	Obj(){
    
    }
private:
	int _a1 = 1;
	int _a2 = 2;
};
int main() {
    
    
	//这三种方式都会调用构造函数,我们将构造函数私有化后,就无法再类外创建 堆/栈 上的对象了
	/*static OBj o1;
	OBj o2;
	Obj* o3 = new Obj;*/

	//提供对外的接口
	//无需创建对象,通过类作用域限定符来调用静态成员函数。
	Obj obj_1 = Obj::GetStackObj();
	Obj* p_obj = Obj::GetHeapObj();
	return 0;
}
  • 实现原理
    1. 私有化构造函数
    2. 通过静态工厂方法控制对象创建

4. 常见问题解答

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

  • 静态成员不属于单个对象,类内声明仅表示存在性,需在程序全局空间进行内存分配

Q2:静态成员函数能否调用非静态成员函数?

  • 不能。非静态成员函数隐含this指针参数,而静态函数无this指针

Q3:如何选择初始化列表与构造函数体?

  • 优先使用初始化列表,特别是对于const/引用成员/无默认构造函数的自定义类型等必须初始化的场景

4. 总结对比

特性 初始化列表 静态成员
作用对象 对象成员初始化 类级别共享数据/操作
关键优势 处理特殊类型成员初始化 减少全局变量使用
典型应用场景 const/引用成员初始化 计数器、工具类函数
内存管理 对象内存空间 静态存储区

以上就是本文的所有内容了,码字整理不易,欢迎各位大佬在评论区留言交流

猜你喜欢

转载自blog.csdn.net/2301_80064645/article/details/145691280
今日推荐