c++详解之explicit,static成员,友元,内部类,匿名对象,拷贝对象的编译器优化
关于对象的隐性类型转换
类型转换
我们知道当我们的将一个内置类型的变量强制赋值个另一个内置类型就会发生类型转换,可以显性的也可以隐形的
int main()
{
int a = 1;
double b = 1.1111;
a = b;
return 0;
}
这样样子就发生了一次隐形的类型转换,每次发生类型转换都会产生一个具有常性的临时变量!
那么对象能不呢发生强制类型转换呢?答案是可以的!
关键字 explicit
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
用explicit修饰构造函数,将会禁止构造函数的隐式转换。
对于单个参数——c++ 98支持
class Date { public: // 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用 //explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译 explicit Date(int year) :_year(year) {} //虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用 // explicit修饰构造函数,禁止类型转换 /*explicit Date(int year, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {}*/ //是半缺省的,只穿一个参数就可以!转换 //要传两个参数的就不可以! //全缺省的也可以支持!全缺省可以当做无参,一个参数都是可以的! Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year; int _month; int _day; }; int main() { Date d1(2022); Date d2 = 2022;//这个会发生强制类型转换 //这两个都是拷贝构造! Date d3(d1); Date d4 = d3;//不会发生强制类型转换! Date& D1 = D1; Date& D2 = 2022;//编译不通过 //发生了强制类型转换!产生了临时变量,临时变量具有常性所以使用Date& 接收发生了权限的放大! const Date& D3 = 2022;//编译通过! //和下面的这种情况是一样的! int i = 0; double d = i; const double& rd = i; return 0; }
对于多个参数的构造——c++11开始支持
老版的编译器可能不支持这个!例如dev6.0
class Date { public: //如果加了explicit就不支持类型转换! Date(int year, int month, int day) : _year(year) , _month(month) , _day(day) {} Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year; int _month; int _day; }; int main() { Date d1 = { 2022,1,1 }; //从结果上等价于 Date d2(2022,1,1); //但是过程上是不一样的! Date& D1 = { 2022,1,1 };//编译不通过 const Date& D1 = { 2022,1,1 };//编译通过 //原理和上面的单参数是一样的! }
这个隐式类型转换的作用是什么呢?
#include <iostream> #include<string> using namespace std; void Push_Back(const string& s); int main() { string s1("hello"); Push_Back(s1); string s2 = "hello"; Push_Back(s2); Push_Back("hello"); //等价于构造+调用函数! }
s1就是直接构造一个对象出来,s2会发生强制类型转换,经过编译器优化后也可以直接进行构造出一个对象s2
我们发现当我们想要调用这个Push_Back函数的时候如果使用前两种,我们要都要先构造一个对象出来,然后才使用
但是我们可以直接使用第三种方式,将两行代码二合一
静态成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用 static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
实现一个类,计算程序中创建出了多少个类对象。
我们可以使用有两种方式来实现这个问题
int N = 0;//使用全局变量!
class A
{
public:
A(int a = 0)
{
_a = a;
N++;
}
A(A& a)
{
N++;
}
private:
int _a;
};
int main()
{
A a1;
cout << N << endl;
A a3(a1);
cout << N << endl;
A a4 = 5;
cout << N << endl;
A a5 = a1;
cout << N << endl;
return 0;
}
但是在c++中我们很不推荐使用全局变量,因为这不安全!不具有封装!很容易被人直接访问进行修改!
静态成员变量
我们引入了静态成员变量来解决这个问题!
//}
class A
{
public:
A(int a = 0)
{
_a = a;
N++;
}
A(A& a)
{
N++;
}
int GetN()//使用共有函数获得私有的成员变量!
{
return N;
}
private:
int _a;
static int N;
//不写成共有就是为了封装
};
int A::N = 0;//类外初始化
void F1(A a)
{
}//传值传参 会调用一次构拷贝构造
A F2(A a)
{
A aa;
return aa;
}// 传值传参 调用一次拷贝构造
// 实例化一个对象 调用一个构造函数
//传值返回 创建一个临时变量 调用一次拷贝构造!
A& F3()
{
A aa;
return aa;
}
// 实例化一个对象代用一次构造!
//引用返回 不用创建临时变量!
A& F4(A& a)
{
return a;
}
// 引用传参和引用返回 不调用构造!
int main()
{
A a1;
cout << a1.GetN() << endl;//1
A a2(a1);
cout << a2.GetN() << endl;//2
A a3 = 5;
cout << a3.GetN() << endl;//3
A a4 = a1;
cout << a4.GetN() << endl;//4
F1(a1);
cout << a4.GetN() << endl;//5
F2(a1);
cout << a4.GetN() << endl;//8
F3();
cout << a4.GetN() << endl;//9
F4(a1);
cout << a4.GetN() << endl;//9
return 0;
}
为什么静态成员变量一定要在类外进行初始化?
从上面的我们可以看出,a1,a2,a3,a4都是公用同一个的N,==这说明了静态成员变量绝不是存在类里面的!==
class A { public: A(int a = 0) : N(0)//报错 { _a = a; N++; } A(A& a) : N(0)//报错! { N++; } int GetN() { return N; } private: int _a; static int N = 0;//也不可以加缺省值!因为这个地方的缺省值是在初始化列表发挥作用的! }; //生命周期变成了全局,但是收到内内域的限制! int A::N = 0;//加A:: 是为了区分全局变量!
因为N是所有成员共享的!一旦在类里面的初始化,没调用一次构造函数都要初始化一次!这样是不好的!所以只能在类外进行初始化!
static 全局变量!
改变全局变量的作用域,让全局变量只能改源文件使用!
static 局部变量!
改变局部变量的生命周期,但是作用域不变!仅仅只能在该函数域内使用!
生命周期变成全局
stack 类变量!
改变类变量的生命周期,但是作用域不变!仅仅只能在类域中使用! 生命周期变成全局!
静态成员函数的访问方式
对于共有的
class A
{
public:
A(int a = 0)
{
_a = a;
}
int GetN()
{
return N;
}
static int N;
private:
int _a;
//static int N;
};
int A::N = 0;
//是共有的时候!
int main()
{
A aa;
A aa2;
cout << A::N << endl;
cout << aa.N << endl;
cout << aa2.N << endl;
A* ptr = nullptr;
cout << ptr->N << endl;
return 0;
}
因为静态成员变量N是属于整个类的所以任意一个对象都可以调用N,也可以直接使用类调用!
==为什么空指针可以解引用?==因为静态成员函数不在类里面所以本质上根本没有发生解引用!
==不过我们一般很少将成员函数放在共有!也不推荐放在共有!==
对于私有
我们一般采用共有函数来获得私有的成员的数据!
class A
{
public:
A(int a = 0)
{
_a = a;
++N;
}
A(A& a)
{
N++;
}
static int GetN()
{
return N;
}
private:
int _a;
static int N;
};
A F5()
{
A aa;
return aa;
}
int main()
{
A aa;
F5();
cout << aa.GetN() -1 << endl;
}
但是有没有觉得这个很麻烦?如果有时候我们仅仅只是想要获得一下N的数据,但是我们又得必须去创建一个对象来使用GetN函数这样不是显得很累赘?
所以c++使用==静态成员函数==来解决这个问题!
静态成员函数
class A
{
public:
A(int a = 0)
{
_a = a;
N++;
}
A(A& a)
{
N++;
}
static int GetN()
{
return N;
}
private:
int _a;
static int N;
};
int A::N = 0;
A F5()
{
A aa;
return aa;
}
int main()
{
F5();
cout << A::GetN() << endl;
}
使用了静态成员函数我们发现我们可以不创建类的对象就可以访问类里面的静态成员函数!
静态成员函数的特点——不存在this指针!
静态成员函数不存在this指针!也就是意味着静态成员函数不能访问非静态成员变量!
class A
{
public:
A(int a = 0)
{
_a = a;
N++;
}
A(A& a)
{
N++;
}
static int GetN()
{
return N;
cout << _a << endl;
}
private:
int _a;
static int N;
};
int A::N = 0;
int main()
{
cout << A::GetN() << endl;
}
为什么呢?因为访问非静态成员变量都必须使用到this指针!
class A
{
public:
void test()
{
cout << _a << endl;
}
//函数在编译器编译后悔变成
void test(A* const this)
{
cout << this->_a <<endl;
}
private:
int _a;
static int N;
};
所以静态成员函数是无法访问非静态的成员变量!
但是非静态的成员函数是可以访问静态成员变量的!
静态成员总结
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
静态成员函数的运用
默认构造函数在私有的情况下如何在栈上创建一个类?
class A
{
public:
static A CreatObj(int a = 0)
{
A aa(a);
return aa;
}
private:
int _a;
A(int a = 0)
{
_a = a;
}
};
int main()
{
A aa = A::CreatObj();//使用拷贝构造来创建类!
return 0;
}
友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,**破坏了封装,所以 友元不宜多用。 **
友元分为:友元函数和友元类
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year>> d._month >> d._day;
return in;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
是指不能像类函数在后面加const去修饰this指针!
因为友元函数就是一个普通的函数,没有this指针!
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
当我像在一个类里面去直接访问另一个类的私有成员变量!(无论是静态还是非静态!)
那么这时候我们就要用到友元类!
class Time
{
friend class Date;//声明日期类是时间类的朋友,那么日期类就可以直接访问时间类的私有成员!
//但是时间类并不可以访问日期类!
public:
Time(const int hour = 0, const int minute = 0, const int second = 0)
{
_hour = hour;
_minute = minute;
_second = second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(const int year = 1, const int month = 1, const int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void SetTime(const int hour, const int minute, const int second)
{
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
总结
- 友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接 访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
可以认为将Date伪装成Date的朋友,但是其实两个人并不是真正的朋友!
- 友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承
内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类, 它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
class A
{
public:
A(int a = 0)
{
_aa = a;
}
private:
int _aa;
class B
{
public:
B(int b = 0)
{
_bb = b;
}
private:
int _bb;
};
};
//A和B就是两个独立的类
//B定义在类外还是类内其实几乎都是一样的
//唯一的区别就是B类的访问受到A的类域和访问限制符的限制!
int main()
{
A aa;
B bb;//会直接报错!
//因为我们用一个类型,一个函数,一个对象编译器首先会在局部搜所
//局部找不到就去全局,不会去类域,命名空间域里面搜所!
A::B bb;
//我们要通过A去访问B!
//通俗的来讲不是直营,而是加盟!B仅仅是给A一个牌子,让A去使用!但是B不属于A!
cout << sizeof(A) << endl;
cout << sizeof(A::B) << endl;
return 0;
}
内部类是外部类的天生的友元
内部类可以随意访问外部类的私有成员!但是外部类不能!
class A
{
public:
A(int a = 0)
{
_aa = a;
}
void test2()
{
B bb;
_aa = bb._bb;
_aa = bb._N2;
}//报错!
class B
{
public:
B(int b = 0)
{
_bb = b;
}
void test()
{
A aa;
_bb = aa._aa;
_bb = _N;//可以直接访问不用任何的标识符
}//可以通过!
private:
int _bb;
static int _N2;
};
private:
int _aa;
static int _N;
};
内部类总结
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
这个和一般的友元是不一样的!一般友元是是不可以直接访问static的成员
- sizeof(外部类)=外部类,和内部类没有任何关系。
匿名对象
我们已知构造对象的方式有
class A
{
public:
A(int a = 0)
{
_aa = a;
}
private:
int _aa;
};
int main()
{
//有名对象
A aa1(2);//构造
A aa2 = 2;//强制类型转换!经编译器优化后直接使用2进行构造!
A aa3(aa1);//拷贝构造
A aa = aa3;//拷贝构造
//A aa(); 这种可能是声明也可能是无参,有二义性
//匿名对象!
A();
A(1);
//这方式也是在定义对象!
//所以也会调用构造函数!
return 0;
}
//
return 0;
}
匿名对象的特点
- 普通对象的生命周期是当前函数域,但是匿名对象的生命周期是当前行!下一行就销毁!
匿名对象的价值
class Solution
{
public:
int Sum_solution(int n)
{
//.....
return n;
}
};
//我想调用一次该函数!
A F()
{
A ret(10);
return ret;
}//像是这种函数我们可以直接简化为
A F()
{
return A(10);
}
int main()
{
//我们一般的做法就是
Solution s;
int x = 0;
s.Sum_solution(x);
//但是这样子有点麻烦,而且创建一个对象只为了使用一个函数,其他地方也不使用
//但是有了匿名对象后我们可以直接
Solution().Sum_solution(x);
//省略了创建对象的过程,而且还节省了空间,因为匿名对象的生命周期只有该行!
return 0;
}
编译器的优化(重点!)
现代编译器相比早期编译都会在特定的情况下会对代码的流程进行优化!
现在我们介绍一下在什么情况下现代编译器会对代码进行优化
早期编译器是例如devc++这种古早编译器
class A
{
public:
A(int a = 0)
{
_aa = a;
cout << "构造函数" << endl;
}
A(const A& a)//最好加上const 可以更具泛用性!
{
cout << "拷贝构造函数" << endl;
}
private:
int _aa;
};
class B
{
public:
B(int x = 0, int y = 0)
{
_x = x;
_y = y;
}
private:
int _x;
int _y;
};
情况1 但单参数和多参数的构造函数
int main() { //情况1 但单参数和多参数的构造函数 A aa = 1; B bb = { 1,2 }; //优化前的流程是 A tmp(1) -> A aa(tmp) 先构造在拷贝构造 //优化后变成了 A aa(1) 直接进行构造! return 0; }
情况2传参进行优化
void f1(A aa) { } void f2(A& aa) { } void f3(const A& aa) { } int main() { //情况2传参进行优化 A aa1(1);//构造 f1(aa1);//拷贝构造传参 //正常情况下就是一个构造+拷贝构造到!这个情况下不能优化! //因为不清楚aa1究竟会不会被下面的其他函数调用所以不能优化! //但是如果像下面一样写的话就会触发优化! f1(A(1)); //因为构造+拷贝构造都发生在同一行里面,所以编译器会触发优化! //优化后 直接构造! //这个可以看出匿名对象具有常性!拷贝构造不加const编译过不去,但是其实不会触发拷贝构造! f1(1);//这个是发生了强制类型转换 //会先用构造一个临时变量,然后在使用拷贝构造! //但是经过编译器优化后 就变成了直接构造 //加了引用都不会发生优化,因为引用之后都是直接进行构造然后直接使用! f2(A(1)); //因为是匿名对象,具有常性,所以是无法通过的! f3(A(1));//加了const之后就可以使用 f3(1);//强制类型转换 变成了具有常性的临时对象 return 0; }
情况3 传值返回
A f4() { A aa(10); return aa; } //如果在这个函数里面不需要用对象做什么事情可以使用下面的代码更加简洁! A f5() { return A(10); } int main() { f4();//这个情况下调用了一次构造,一次拷贝构造!无法进行优化! //构造和拷贝构造是两个步骤 A ret = f4(); //这个应该是先构造 ,然后拷贝构造进行传值返回,然后再使用这个临时对象进行一次拷贝构造! //但是其实中间这个临时对象有点多余所以编译器会进行优化直接拷贝构造到ret上去! A ret; ret = f4(); //这这样就不存在优化了! //所以上面的写法更好! f5(); //因为构造和拷贝构造都是在同一个步骤里面触发编译器优化变成了直接构造! A ret = f5(); //本来应该 构造 + 拷贝构造 +拷贝构造 因为这三个过程都在同一个步骤里面! //所以触发编译器优化! 变成了直接一个构造! return 0; }