
🫧个人主页:小年糕是糕手
💫个人专栏:《C++》《C++同步练习》《数据结构》《C语言》
🎨你不能左右天气,但你可以改变心情;你不能改变过去,但你可以决定未来!

目录
[1.3、explicit 禁用隐式转换](#1.3、explicit 禁用隐式转换)
一、类型转换
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;(假设A有A(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;
}
关键结论
- 类间转换的核心:目标类提供 "源类对象为参数的构造函数" (如 B 类的
B(const A& a));B b1 = a1;是隐式转换,但编译器优化后直接构造,无临时对象和拷贝;const B& ref1 = a1;是隐式转换生成临时 B 对象,再绑定引用(临时对象必须存在);- 若想禁用 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析构函数的调用顺序为()
cppC 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
