【C++】类和对象(五) -- 类型转换、static成员

🫧个人主页:小年糕是糕手

💫个人专栏:《C++》《C++同步练习》《数据结构》《C语言》

🎨你不能左右天气,但你可以改变心情;你不能改变过去,但你可以决定未来!



目录

一、类型转换

1.1、隐式类型转换

1.2、编译器优化

[1.3、explicit 禁用隐式转换](#1.3、explicit 禁用隐式转换)

1.4、多参数下的隐式类型转换

1.5、类类型之间的隐式类型转换

1.6、总结:

二、static成员

2.1、静态成员变量

2.2、静态成员函数

2.3、总结

2.4、练习


一、类型转换

1.1、隐式类型转换

我们之前就学过隐式类型转换,他会创建一个临时变量用来储存,这个临时对象具有常属性,下面我们首先来看一个我们之前学过的例子:

cpp 复制代码
#include<iostream>
using namespace std;

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

	//这里不可以写成
	double& ref = i;
	//因为隐式类型转换会创建临时变量,临时变量具有常属性
	//这样会造成权限放大,正确写法如下:
	const double& ref = i;
	return 0;
}

之前我们都是内置类型转换成内置类型,下面我们来了解一下内置类型转换成类类型对象:

C++ 支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。

下面我们来看代码解释(注意看注释):

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	A(int a1)
		:_a1(a1)
	{}
private:
	int _a1 = 1;
	int _a2 = 2;
};

//C++在用类类型做形参的时候不建议用传值传参
//传值传参会调用拷贝构造是一种浪费
void func(const A& aa = 1)
//1为什么可以作为这里的缺省值?
//函数形参是const A& 类型,而1是int类型,C++ 会通过A类中以int为参数的构造函数
//将1隐式转换为临时的A对象,这个临时对象可以绑定到const引用上,因此1能作为缺省值。
{
	//..
	//这里我们传普通对象可以传过去但是传const对象就不可以
	//所以我们尽量加上const
}

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


int main()
{
	//这是我们常说的用构造来初始化,直接构造
	A a1(1);

	//用整型去做参数可以构造一个A对象
	//隐式类型转换:用int构造临时A对象,再拷贝构造给a2
	//1会先去构造一个临时对象,临时对象再去拷贝构造给a2
	A a2 = 1;

	const A& ref1 = a1;
	//引用的是中间产生的临时变量
	const A& ref2 = 1;

	func(a1);
	func(1);

	//这是我们之前学的往栈里面插入一个数据
	//首先我们需要定义一个栈类型的变量st1
	//接下来我们要去定义一个A类型的a3,并且构造为3
	//最后我们便可以调用st1中栈里的函数Push,并且将a3传过去
	Stack st1;
	A a3(3);
	st1.Push(a3);
	//当我们学完隐式类型转换可以直接写成
	//用int类型的3构造一个临时的A对象,再将这个临时对象传递给Push函数的形参
	st1.Push(3);

当然了编译器也会进行一定的优化:

1.2、编译器优化
cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a1)
		:_a1(a1)
	{ }

	A(const A& aa)
	{
		cout << "A(const A & aa)" << endl;
	}

private:
	int _a1 = 1;
};

int main()
{
	//构造
	A a1(1);

	//优化
	//隐式类型转换
	//2为参数构造临时对象,临时对象拷贝构造a2 -> 优化为直接构造
	A a2 = 2;

	//不会优化
	const A& ref1 = 3;
	return 0;
}

这里const A& ref1 = 3;不会被优化的原因是:

C++ 的 "拷贝省略" 优化(如A a2 = 2;直接构造而非先临时对象再拷贝),针对的是对象初始化场景 (用临时对象拷贝构造新对象);而const A& ref1 = 3;引用绑定场景 ------ 这里的临时对象是为了绑定到const引用而创建的,它本身是一个 "右值临时对象",C++ 标准要求这个临时对象必须存在(因为引用需要绑定到实际对象上),所以不会对 "临时对象的创建" 做优化。

简单总结:

  • A a2 = 2;:属于 "用临时对象拷贝构造新对象",符合拷贝省略的优化条件;
  • const A& ref1 = 3;:属于 "引用绑定临时对象",临时对象必须存在,因此无优化。
1.3、explicit 禁用隐式转换

构造函数前面加 explicit 就不再支持隐式类型转换。

  • 核心作用 :在类的构造函数前添加explicit关键字后,该构造函数将无法触发 "隐式类型转换"(即不能通过内置类型 / 其他类对象自动转换为当前类的对象)。
  • 典型场景 :比如原本A a = 1;(假设AA(int)构造函数)可以隐式转换,加explicit后必须显式写A a(1);A a = A(1);才合法。
cpp 复制代码
#include <iostream>
using namespace std;

// 无 explicit 修饰的类
class A {
public:
    int _a;
    // 普通构造函数(支持隐式转换)
    A(int a) : _a(a) {
        cout << "A(int a) 构造函数调用" << endl;
    }
};

// 有 explicit 修饰的类
class B {
public:
    int _b;
    // explicit 修饰的构造函数(禁用隐式转换)
    explicit B(int b) : _b(b) {
        cout << "explicit B(int b) 构造函数调用" << endl;
    }
};

int main() {
    // 1. 无 explicit 的情况:支持隐式转换
    A a1 = 10;  // 合法:int 10 隐式转换为 A 类对象
    A a2(20);   // 显式构造(始终合法)
    const A& refA = 30;  // 合法:临时 A 对象绑定到 const 引用
    cout << "a1._a: " << a1._a << ", a2._a: " << a2._a << ", refA._a: " << refA._a << endl;

    // 2. 有 explicit 的情况:禁用隐式转换
    // B b1 = 10;  // 编译报错:无法从 int 隐式转换为 B
    B b2(20);   // 显式构造(仍合法)
    // const B& refB = 30;  // 编译报错:无法隐式转换生成临时 B 对象
    const B& refB = B(30);  // 合法:显式构造临时对象再绑定引用
    cout << "b2._b: " << b2._b << ", refB._b: " << refB._b << endl;

    return 0;
}
1.4、多参数下的隐式类型转换
cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:

	//单参数
	A(int a1)
		:_a1(a1)
	{
		cout << "A(int a1)" << endl;
	}

	A(const A& aa)
	{
		cout << "A(const A& aa)" << endl;
	}

	//多参数
	A(int a1, int a2)
		:_a1(a1)
		 ,_a2(a2)
	{ }

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

//C++在用类类型做形参的时候不建议用传值传参
//传值传参会调用拷贝构造是一种浪费
void func(const A& aa = 1)
//1为什么可以作为这里的缺省值?
//函数形参是const A& 类型,而1是int类型,C++ 会通过A类中以int为参数的构造函数
//将1隐式转换为临时的A对象,这个临时对象可以绑定到const引用上,因此1能作为缺省值。
{
	//..
	//这里我们传普通对象可以传过去但是传const对象就不可以
	//所以我们尽量加上const
}

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


int main()
{
	//对于多参数
	A a3(1, 1);
	A a4 = { 1,1 };
	//ref2引用的是中间的临时对象
	const A& ref1 = { 1,1 };

}

这段代码是多参数构造函数下的隐式类型转换场景,核心含义可以拆解为:

1. 多参数构造的两种写法

  • A a3(1, 1);:显式调用多参数构造函数创建对象;
  • A a4 = {1,1};:C++11 及以后支持的列表初始化 ,本质是通过多参数构造函数进行隐式转换 (用{1,1}构造临时A对象,再初始化a4)。

2. 引用绑定临时对象

const A& ref1 = {1,1}; 的逻辑是:

  • 先用{1,1}隐式构造一个临时的A对象
  • 再将这个临时对象绑定到const A&类型的引用ref1上;
  • 这里的 "中间的临时对象",就是指这个由{1,1}构造出的临时A对象(因为引用必须绑定到实际对象,所以临时对象会被创建出来)。

补充说明

如果A的多参数构造函数被explicit修饰,那么A a4 = {1,1};const A& ref1 = {1,1};都会编译失败(禁用了隐式转换),必须显式写A a4({1,1});const A& ref1 = A{1,1};

1.5、类类型之间的隐式类型转换

我们之前学的都是内置类型和内置类型之间的隐式类型转换;内置类型与类类型之间的隐式类型转换,下面我们来看第三种类类型之间的隐式类型转换:

类类型的对象之间也可以隐式转换,需要相应的构造函数支持。

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:

	//单参数
	A(int a1)
		:_a1(a1)
	{
		cout << "A(int a1)" << endl;
	}

	A(const A& aa)
	{
		cout << "A(const A& aa)" << endl;
	}

	//多参数
	A(int a1, int a2)
		:_a1(a1)
		 ,_a2(a2)
	{ }

	//不去修改的我们尽量带上const
	int Get() const
	{ 
		return _a1 + _a2;
	}

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

class B
{
public:
	B(const A& a)
		:_b(a.Get())
	{
	}
private:
	int _b = 0;
};


int main()
{
	A a1(1, 1);
	//A和B是不能转换的,就算是强制类型转换也不行
	//那怎么样才能转换呢?
	//有对应的构造即可
	B b1 = a1;//这里是直接构造(编译器优化的结果)
	const B& ref1 = a1;
}

关键结论

  1. 类间转换的核心:目标类提供 "源类对象为参数的构造函数" (如 B 类的B(const A& a));
  2. B b1 = a1; 是隐式转换,但编译器优化后直接构造,无临时对象和拷贝;
  3. const B& ref1 = a1; 是隐式转换生成临时 B 对象,再绑定引用(临时对象必须存在);
  4. 若想禁用 A→B 的隐式转换,给 B 的构造函数加explicit即可
1.6、总结:
  • C++ 支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
  • 构造函数前面加 explicit 就不再支持隐式类型转换。
  • 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。

注:有一定关联才能转换,例如内置类型与内置类型间的转换,整型家族之间,整型和浮点数都表示数据大小;整型和指针之间,指针是地址的编号,但是浮点数和指针之间便不可以转换(没有关联关系)

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	// 构造函数explicit就不再⽀持隐式类型转换 
	// explicit A(int a1)
	A(int a1)
		:_a1(a1)
	{
	}
	//explicit A(int a1, int a2)
	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{
	}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
	int Get() const
	{
		return _a1 + _a2;
	}
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
public:
	B(const A& a)
		:_b(a.Get())
	{
	}
private:
	int _b = 0;
};
int main()
{
	// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3 
	// 编译器遇到连续构造+拷⻉构造->优化为直接构造 
	A aa1 = 1;
	aa1.Print();
	const A& aa2 = 1;
	// C++11之后才⽀持多参数转化 
	A aa3 = { 2,2 };
	// aa3隐式类型转换为b对象 
	// 原理跟上⾯类似 
	B b = aa3;
	const B& rb = aa3;
	return 0;
}

二、static成员

我们在C语言中就学过static,他的作用是让局部变量存静态区,还能限制全局变量 / 函数的作用域为当前文件;在C++中 static 可修饰变量 / 函数,核心是使其属于类而非单个对象,支持类域直接访问,且静态函数无 this 指针、仅能访问静态成员。

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

  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。

下面我们来看一段代码来初步了解一下static(注意看注释):

cpp 复制代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
using namespace std;

class A
{
public:
private:
	int _a1 = 1;
	int _a2 = 1;
	//不存在对象当中,存在静态区的
	//受到类域和访问限定符限制的专属的全局变量
	//他的生命周期是全局的,不能给缺省值
	static int _count;
};

class B
{
public:
private:
	//声明
	int _b1 = 1;
	int _b2 = 1;
	
public:
	//声明
	static int _count;
};

//定义 -- 这时候可以初始化
int B::_count = 0;

int main()
{
	A aa1;
	cout << sizeof(aa1) << endl;
	//我们看到sizeof结果是8,所以计算大小时,并没有加上count
	//私有的不可以访问
	cout << aa1._count << endl;
	//公有的情况下指定类域就可以访问
	B bb1;
	cout << bb1._count << endl;
	cout << B::_count << endl;
	return 0;
}
2.2、静态成员函数
  • 用 static 修饰的成员函数,称之为静态成员函数,静态成员函数没有 this 指针。

  • 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有 this 指针。

  • 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	A(int a = 0)
		:_a1(a)
		,_a2(a)
	{
		++_count;
	}

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

	//没有this指针了,就不可以改变量了
	static int GetCount()
	{
		//_a1++; 不能访问非静态成员,没有this指针
		return _count;
	}

private:
	int _a1 = 1;
	int _a2 = 1;
//public:
	//声明
	static int _count;
};

int A::_count = 0;

int main()
{
	A aa1;
	cout << aa1.GetCount() << endl;
	cout << A::GetCount() << endl;

	return 0;
}
2.3、总结
  • 用 static 修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。

  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。

  • 用 static 修饰的成员函数,称之为静态成员函数,静态成员函数没有 this 指针。

  • 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有 this 指针。

  • 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

  • 突破类域和访问限定就可以访问静态成员,可以通过类名::静态成员 或者 对象。静态成员 来访问静态成员变量和静态成员函数。

  • 静态成员也是类的成员,受 public、protected、private 访问限定符的限制。

  • 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。

cpp 复制代码
// 实现⼀个类,计算程序中创建出了多少个类对象? 
#include<iostream>
using namespace std;
class A
{
public:
	A()
	{
		++_scount;
	}
	A(const A& t)
	{
		++_scount;
	}
	~A()
	{
		--_scount;
	}
	static int GetACount()
	{
		return _scount;
	}
private:
	// 类⾥⾯声明 
	static int _scount;
};
// 类外⾯初始化 
int A::_scount = 0;
int main()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetACount() << endl;
	cout << a1.GetACount() << endl;
	// 编译报错:error C2248: "A::_scount": ⽆法访问 private 成员(在"A"类中声明) 
	//cout << A::_scount << endl;
	return 0;
}
2.4、练习

设已经有A,B,C,D4个类的定义,程序中A,B,C,D构造函数的调用顺序为()

设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数的调用顺序为()

cpp 复制代码
C c;
int main()
{
   A a;
   B b;
   static D d;
   return 0;

}

答案:

构造顺序:CABD

析构顺序:BADC

解析:

全局变量在main之前就要初始化,这里C的初始化就要调用构造,所以C一定先构造,接下来进入main函数,我们要知道局部static对象,都是在第一次运行到定义位置时,才初始化,所以构造顺序就是CABD;析构顺序肯定B在A之前,因为析构顺序与构造顺序相反(同时定义在栈里的变量后定义的先析构),C是在main函数之前就初始化,所以C的析构应该是在main函数之后,D在静态区,而main函数结束之后局部的、静态的会先析构,故D在C之前(static D d;是局部静态变量,其生命周期是程序结束前,但会在main函数执行完毕后、全局变量析构之前析构 )故析构顺序为BADC


相关推荐
噜啦噜啦嘞好37 分钟前
生产者消费者模型
linux·开发语言
CoovallyAIHub40 分钟前
2025年值得关注的5款数据标注工具
深度学习·算法·计算机视觉
十五年专注C++开发41 分钟前
嵌入式软件架构设计浅谈
c语言·c++·单片机·嵌入式
Blossom.11841 分钟前
基于Qwen2-VL+LayoutLMv3的智能文档理解系统:从OCR到结构化知识图谱的落地实践
开发语言·人工智能·python·深度学习·机器学习·ocr·知识图谱
烧冻鸡翅QAQ41 分钟前
考研数学:多元函数微分学
考研
FuckPatience41 分钟前
C# 补码
开发语言·算法·c#
稚辉君.MCA_P8_Java42 分钟前
Gemini永久会员 VB返回最长有效子串长度
数据结构·后端·算法
ULTRA??43 分钟前
C++20模块( import 核心用法)
c++·c++20
星释43 分钟前
Rust 练习册 106:太空年龄计算器与宏的魔法
开发语言·后端·rust