隐式类型转换
基础知识
隐式类型转换是指两个不同类型的变量在进行运算时(包括赋值),编译器会自动将其中一个变量的类型转换成另外一个变量的类型。比如:
int main() { int a = 0; double b = a; const double& rb = a; const int& c = 1; }
如上,为什么int类型可以赋值给double呢?就是因为存在隐式的类型转换。这个赋值并不是将a直接赋值给b的,而是根据b的类型产生了一个临时变量,将a的值赋给临时变量,再由临时变量赋值给b。
对于rb来说也是一样的,只不过对于引用和指针来说要考虑权限的缩小和放大的问题,而产生的临时变量具有常性,所以对于rb变量要加上const修饰。
最后一个也是大同小异,对于整形数据1来说要先产生一个临时变量将1赋值给临时变量,最后由临时变量赋值给c,又由于临时变量具有常性,所以要加const修饰。
构造函数的隐式类型转换
在C++98中,单参数的构造函数也支持隐式函数重载,这里说的单参数是指只需要传一个参数的函数,包括单参数,全缺省和半缺省。
class Date { public: Date(int year=2022, int month = 2, int day = 10) :_year(year) ,_month(month) ,_day(day) { cout << "构造函数" << endl; } Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; cout << "拷贝构造" << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1); Date d3 = 2022; return 0; }
可以看到对于日期类我们不但可以使用日期类来拷贝构造和构造来初始化,同样可以运用赋值一个整形来初始化。日期类和整形两种不同的类型直接可以赋值,正是隐式类型转换的原因。
这里还要讲一下d3,从上图可以发现将一个整形类型的数赋值给一个日期类似乎只是调用了一个构造函数,但真是这样吗?
其实不是这样的,前面有提到对于赋值操作的时候其实并不是直接赋值而是要通过一个临时变量做中转的。也就是说要先产生一个日期类临时变量将这个整形赋值给这个日期类的临时变量,产生日期类临时变量的时候需要调用一次拷贝构造吧。将临时变量赋值给d3的时候又要调用一次d3的构造函数,所以这个过程其实是拷贝构造+构造得到的,不过编译器做了优化跳过了拷贝构造的过程。但是如果你使用的是一些较老的编译器就可能没有优化。
**C++11又对这个语法进行了拓展,支持多参数的构造函数,**只是在传递多参数时需要使用一个花括号
explicit 关键字
这个关键字用于修饰构造函数,可以禁止隐式类型转换的行为:
class Date { public: explicit Date(int year = 2022, int month = 10, int day = 10) :_year(year) , _month(month) , _day(day) { cout << " 构造" << endl; }; Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; cout << " 拷贝构造" << endl; } private: int _year; int _month; int _day; };
类型转换的意义
隐式类型转换其实是有风险的,一个double赋值给int就会直接丢失小数部位的数据。因此也就有了explicit关键字的存在,那么既然有风险我们不直接禁止呢?因为隐式类型转换在很多时候可以方便我们:
int main() { string s1("hello"); push_back(s1); push_back("hello"); }
在上述代码的情况下,如果有隐式类型的转换,我们在插入s1时就不必要先构造一个string,而是可以直接使用hello做参数,其实类似这样的情况还非常多,以后你就会发现了
static成员
基础知识
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化 ,下面看一个面试题:
面试题:实现一个类,计算程序中创建出了多少个类对象。
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; void TestA() { cout << A::GetACount() << endl; A a1, a2; A a3(a1); cout << A::GetACount() << endl; }
我们知道要创建一个类对象就一定会调用构造函数,所以构造函数被调用的次数就是创建类对象的个数。虽然使用全局变量也可以达到这个目的,但是并不建议使用全局变量,因为全局变量随处都可以修改,而使用static作为类的成员变量的话会受到类域的限制所以相对会更安全。
static成员变量
静态成员变量指的是用static修饰的成员变量,具有以下特性:
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
- 静态成员变量必须在类外定义,定义时不添加 static 关键字,类中只是声明;
- 静态成员变量的访问受类域与访问限定符的约束;
因为静态成员变量不是属于任一类对象的而是存在于静态区由所有对象共享的所以不能将静态成员变量写入初始化列表:
写道初始化列表中的成员变量在每个成员被实例化时就会定义并且初始化,不能将静态成员写入初始化列表就从侧面表明静态成员并不是属于某一个对象的。
此外类中只是声明,又不能在初始化列表中定义静态成员,那么静态成员应该在哪定义呢?静态成员需要在全局定义并且要加上类访问限定符,此时不用再加static关键字:
一般来说在类外是无法访问到类中的成员变量的,不只是因为类域的原因再一个就是因为访问限定符的阻碍,但静态变量在定义的时候只需要标明域就可以打破访问限定符的限制,这是一个特例需要我们记住。
其实静态成员变量除了在定义的时候可以无视访问限定符以外,其他时候和普通成员变量没什么区别:
在有了静态成员变量后,统计对象创建的个数时就可以使用静态成员变量了,但是此时又面临类访问限定符限制的问题,为了解决这个问题,C++给出了静态成员函数来解决
static成员函数
静态成员函数是指用static关键字来修饰的函数,有如下特性:
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
- 静态成员也是类的成员,同样受类域和访问限定符的约束;
静态成员函数最特别的地方在于它没有那个隐藏的this指针,所以我们在调用的时候不用传对象的地址,因此可以直接使用域限定符直接调用,而不需要创建对象,就能直接访问到类里面的静态成员变量。但是相应的没了this指针就不能访问到非静态成员变量和成员函数。因为非静态成员变量需要实例化以后才有实体
class A { public: A(int i = 0) :_i(i) { _n++; } A(const A& a) { _i = a._i; _n++; } static int GetN() { return _n; } private: int _i; static int _n; }; int A::_n = 0;
可以看到这里有红色波浪线,但是程序却正确运行了,其实编译器给的这个红色波浪线的警告就像某些人很准的第六感一样,并不是每次都能带你走向正确的,要以输出列表为准,因为输出列表给的报错是最正确的,同时要从第一个错误开始解决。
虽然静态成员函数无法调用非静态成员变量,但是非静态成员函数可以调用静态成员变量
最后来做一道题来巩固一下静态成员的知识:求1+2+3+…+n
class Sum { public: Sum() { //每构造一次_i++一次,再用ret累加i _i++; _ret+=_i; } static int GetRet() { return _ret; } private: static int _i; static int _ret; }; int Sum::_i=0; int Sum::_ret=0; class Solution { public: int Sum_Solution(int n) { //在这里定义n个对象即可,每定义一个对象就要调用一次构造函数 Sum arr[n]; return Sum::GetRet(); } };
友元
引入
在C++中我们经常使用cout和cin配合流提取<<及流插入>>来使用,因为它们可以自动识别内置类型。其实它们之所以可以做到自动识别内置类型是因为函数重载和运算符重载
可以看到,cin 和 cout 分别是 istream 和 ostream 类的两个全局对象,而 istream 类中对流提取运算符 >> 进行了运算符重载,osteam 中对流插入运算符 << 进行了运算符重载,所以 cin 和 cout 对象能够完成数据的输入输出;同时,istream 和 ostream 在进行运算符重载时还进行了函数重载,所以其能够自动识别数据类型;
为了方便自定义类型的输入输出,我们也可以重载一下<<和>>
class Date { public: Date() :_year(2023) ,_month(2) ,_day(17) {} //因为类对象是隐藏的this指针,所以不用再显示的传对象 istream& operator>>(istream& in) { in >> _year; in >>_month; in >> _day; return in; } ostream& operator<<(ostream& out)const { out << _year << " " << _month << " " << _day << endl; return out; } private: int _year; int _month; int _day; };
我们重载了以后编译器还是说没有域这些操作数匹配的运算符,为什么?因为我们在类中定义的这个函数,所以第一个参数默认为隐藏的this指针,也就是左参数必须是类对象,因此这个运算符应该要这样使用:
可以看到这样使用就没有任何问题了,但是这样的可读性不高,重载这个运算符又显得没有意义了(运算符重载就是为了提高可读性)。或许你会说将这个运算符重载写成全局函数,但是其实这样也是不行的,因为要受到类访问限定符的限制。为了解决这个问题呢C++就提出了一个叫友元的东西,友元又分为友元函数和友元类;
友元函数
**友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。 **
class Date { //友元声明,可以放在类的任意位置 friend istream& operator>>(istream& in, Date& d); friend ostream& operator<<(ostream& out,const Date& d); public: Date() :_year(2023) ,_month(2) ,_day(17) {} private: int _year; int _month; int _day; }; inline istream& operator>>(istream& in,Date&d) { in >> d._year; in >> d._month; in >> d._day; return in; } inline ostream& operator<<(ostream& out,const Date&d) { out <<d._year << " " <<d._month << " " << d._day << endl; return out; }
因为这两函数使用频率相当的高,且函数体内容较短,所以将其设置为内联函数,可以提高效率。同时为了可以连续这两个运算符我们需要返回值,又因为cin和cout是全局变量,所以可以使用传引用返回。
总结
- 友元函数可访问类的私有和保护成员,但不是类的成员函数;
- 友元函数不能用 const 修饰;
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
- 一个函数可以是多个类的友元函数;
- 友元函数的调用与普通函数的调用原理相同;
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员 (不受访问限定符的限制)。
class Time { friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类 中的私有成员变量 public: Time(int hour = 0, int minute = 0, int second = 0) : _hour(hour) , _minute(minute) , _second(second) {} private: int _hour; int _minute; int _second; }; class Date { public: Date(int year = 1900, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} void SetTimeOfDate(int hour, int minute, int second) { // 直接访问时间类私有的成员变量 _t._hour = hour; _t._minute = minute; _t._second = second; } private: int _year; int _month; int _day; Time _t; };
这就好像你把别人带到你的家里,他可以看到你家里的任何东西,但是他没把你带到他家去所以你不知道他家里有什么。
友元类有如下特点:
1.友元关系是单向的,不具有交换性;比如上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time 类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行;
2.友元关系不能传递;如果C是B的友元, B是A的友元,则不能说明C是A的友元;
3.友元关系不能继承,继承的相关知识我们到C++进阶再详细学习
内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
class A { private: static int k; int h; public: class B // B天生就是A的友元 { public: void foo(const A& a) { cout << k << endl;//OK cout << a.h << endl;//OK } }; }; int A::k = 1; int main() { A::B b; b.foo(A()); return 0; }
内部类有如下特性:
1.内部类天生就是外部类的友元,所以内部类可以通过外部类的对象参数来访问外部类中的所有成员;但外部类不是内部类的友元
2.内部类定义在外部类的 public、protected、private 处都是可以的,但是内部类实例化对象时要受到外部类的类域和访问限定符的限制;
3.内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名;
4.内部类是一个独立的类,它不属于外部类,所以 sizeof (外部类) == 外部类;
5.内部类在C++中很少被使用,在Java中使用频繁,所以大家只需要了解有这个东西即可
匿名对象
所谓匿名对象就是在定义时不给它取名字,这样的对象生命周期只有定义那一行,因为没有名字所以无法被别人使用,一旦出了那一行就没有人能记得它了。除了生命周期和普通类对象不同以外,其他都是一样的。
class A { public: A() :_a(10) { cout << "构造函数" << endl; } ~A() { cout << "析构函数" << endl; } private: int _a; }; int main() { A(); return 0; }
有时候我们需要调用一个类的成员函数,但是调用一个类的成员函数就必须要有一个类的对象,如果只是为了调用这个函数,那么就可以使用匿名对象,不但减少了代码量,也减少了内存占用。
拷贝对象时编译器的一些优化
在传参和传返回值的过程中,编译器会做一些优化减少拷贝的次数,在如下场景中特别有用:
class A { public: A(int a = 0) :_a(a) { cout << "A(int a)" << endl; } A(const A& aa) :_a(aa._a) { cout << "A(const A& aa)" << endl; } A& operator=(const A& aa) { cout << "A& operator=(const A& aa)" << endl; if (this != &aa) { _a = aa._a; } return *this; } ~A() { cout << "~A()" << endl; } private: int _a; }; void f1(A aa) {} A f2() { A aa; return aa; }
**优化场景一:传参隐式类型转换,连续构造+拷贝构造->优化为直接构造 **
可以看到这里只调用了一次构造函数,但是根据前面说的隐式类型转换我们可以知道中间有个临时变量的产生,需要先构造这个临时变量,再将这个临时变量拷贝构造aa,但编译器经过优化以后直接成了将1去构造aa;
优化场景二:匿名对象 – 构造+拷贝构造 --> 直接构造
这个与场景一类似,本来是先用2来构造一个匿名对象,然后使用这个匿名对象来拷贝构造aa,经过编译器优化后变为直接使用2去构造aa;
优化场景三:传值返回 – 构造+拷贝构造+拷贝构造 --> 直接构造
f2 函数返回的是局部的匿名对象,所以编译器会先用匿名对象去拷贝构造一个临时对象,然后再用临时对象来拷贝构造aa2,而编译器优化后变为直接使用无参来构造aa2;即构造+拷贝构造+拷贝构造优化为直接构造。
再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
- 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
- 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
- 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能知道洗衣机是什么东西。
- 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了
在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象
如果你能看到这里,那我必须要给你一个大大的赞👍👍