一、再谈构造函数
1.1初始化列表
在上一章节中,对于类我们可以形象的比喻为房子的图纸,而真正对于类的初始化可以比喻为建造了一个实体房子,即创建对象,对于房子中的各个房间都有特定的位置构造,那么对于类中的成员变量他们又是如何被初始化呢?如何去理解成员变量初始化?
在构造函数中,我们可以对成员变量赋初始值,那么这叫成员变量的初始化吗?其实并不是,这种情况称为函数体内的初始化,而且这种初始化可以多次赋值,在函数体内可以多次使用给不同值,而真正的成员变量初始化是在初始化列表中初始化的,且在初始化列表中只能被初始化一次。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。
例如:
#include <iostream>
using namespace std;
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(2023, 12, 4);
return 0;
}
1.2初始化列表的注意事项
1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
例如:
#include <iostream>
using namespace std;
class A
{
public:
A(int a)//有参构造函数
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int b)
:_claim(a)//这三个都必须用初始化列表初始化
, _copy(b)
, _n(10)
{}
private:
A _claim;//自定义类型成员
int& _copy;//引用成员变量
const int _n;//const成员变量
};
int main()
{
B b1(3,4);
return 0;
}
对于引用成员变量和const成员变量在定义时都必须初始化,这是前面学过的内容,那么在类中,他们的初始化走的就是初始化列表。
自定义类型_claim成员中有一个有参构造函数 ,前面章节就学过在一个类中,会去调用该类中自定义类型成员的默认构造函数,而现在B类中因为有一个自定义有参构造函数,而A类中没有默认构造函数,只有一个有参构造函数,那么编译就会报错,说找不到默认构造函数,因此在这里需要使用初始化列表初始化自定义类型成员,B类的自定义构造函数才会去调用A类中的有参构造函数。
3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
#include <iostream>
using namespace std;
class Time
{
public:
Time(int hour =0)
:_hour(hour)
{
cout << "Time(int hour = 0)" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int day)
{}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d1(1);
return 0;
}
运行结果:
上前面章节中,我们提到,对于内置类型成员和自定义类型成员都存在时,编译器会对他们都处理,而对于内置类型成员就会给初始值,只不过是随机值,在这里给的随机值其实走的就是初始化列表,由编译器自己实现。而对于自定义类型成员上面也已经说了必须走初始化列表。总结就是不管是内置还是自定义成员最好都使用初始化列表。
4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,意思就是成员变量的声明次序是什么样的,那么对应的在初始化列表中先走初始化的就是声明的顺序
#include <iostream>
using namespace std;
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)//先走这个,此时_a1是随机值,那么_a2就是随机值
{}
void print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.print();
return 0;
}
运行结果:
_a2先声明,那么先走_a2的初始化,而此时_a1并没有被初始化,是随机值,那么_a2就是随机值,之后再走_a1的初始化,值为1
注意:有些初始化列表或检查的工作,初始化列表也不能全部搞定,还要用函数体,例如:malloc了一个空间,那么空间检查工作就能用初始化列表搞定,而要用函数体。
1.3explict关键字
对于只有一个参数和多个参数的构造函数,在创建对象并进行初始化时,可以通过隐式类型转换的方式来进行初始化,单参和多参的初始化是有区别的。而在构造函数前引入explicit关键字就可以禁止这种隐式类型转化。
先来看单参隐式类型转换:
class Date
{
public:
Date(int year)//单参构造函数
:_year(year)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022);
d1 = 2023;
return 0;
}
上述代码中,除了定义对象时将值传过去初始化对像,还可以通过直接赋值初始化对象(d1 = 2023),他们之间类型不同,为什么可以直接赋值?
其实是发生了隐式类型转换,用2023构造了一个临时对象,再将该临时对象给了d1对象。
那么在构造函数前加上explicit就会禁止隐式类型转换。
class Date
{
public:
explicit Date(int year)//单参构造函数
:_year(year)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023);
return 0;
}
此时不能直接赋值,否则就会报错。
多参隐式类型转换:
class Date
{
public:
Date(int year, int month=11, int day=8)//多参构造函数
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023);
d1 = { 2023,12,8 };
return 0;
}
与单参不一样的是多参构造进行初始化对象时,可以通过花括号来进行初始化,注意不要写成圆括号,这样就成了一个逗号表达式了。同理在构造函数前加上explicit就可以禁止该多参隐式类型转换。
注意:explicit修饰构造函数时,禁止的是传参时隐式类型的转换,但是有一种情况,在只有一个参数的情况下,如果进行强转的话,explicit就会失去效果。
class Date
{
public:
explicit Date(int year)//单参构造函数
:_year(year)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023);
d1 = (Date)2023;
return 0;
}
二、Static成员
2.1概念+特性
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
特性:
1.静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2.静态成员变量必须在类外定义,定义时不添加static关键字,而声明则是在类里面
3.对于类中的静态成员,可以用 A::静态成员或者对象.静态成员来访问
4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5.静态成员也是类的成员,受public、protected、private访问限定的限制
来看一个例子:实现一个类,计算程序中创建了多少个类对象。
#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;//静态成员变量,在类中是声明,受private保护
};
int A::_scount = 0;//静态成员变量在外面定义
int main()
{
A a1, a2;
A a3(a2);
cout << A::GetACount() << endl;
cout << a1.GetACount() << endl;
cout << A().GetACount() << endl;//A()为匿名对象
return 0;
}
运行结果:
其中用static定义的类成员存放在静态区,他就不属于具体的某个对象, 而是属于所有对象公有(a1,a2,a3),而平常定义的不是static的成员变量就是属于具体的某个对象,就不是公有的,所以对于_scount成员变量它是属于所有对象,_scount改变,那么每个对象中的_scount也会改变。通过类A创建了三个对象,那么_scount就加了三次,对于static定义的静态成员,不仅可以用A::静态成员形式来调用成员变量,还可以通过对象来调用,所以相比一些情况,当没有创建对象时,可以定义静态成员,并通过类直接调用静态成员。
还有一种情况就是创建匿名对象A(),那么又会调用一次构造函数,_scount再加1为4,匿名对象有一个特点就是他的生命周期只在他所在的一行。
想必有疑问的是静态成员不是共有的吗,为什么前面输出3而后面输出4,因为A()这个匿名对象是在后面定义的,前面的早已输出完了。还有就是析构函数不是会调用吗,那么_scount不是会自减吗,析构函数是在对象销毁前调用的,在输出完结果之后调用的。
【问题】
1.静态成员函数可以调用非静态成员函数吗?
当然不可以,因为静态成员函数是没有this指针的,那么对象的地址就传不了给this指针。
2.非静态成员函数可以调用类的静态成员函数吗?
当然可以,他有this指针,对象的地址可以传给this指针。
三、友元
友元提供了一种突破封装的方式,有时提供了便利。正因如此,破坏了封装,增加了耦合度(彼此之间的联系程度),比如类与类之间通过友元就会增加联系,但可能会导致一个类被另一个类错用,所以友元不宜多用。
友元分为:友元函数和友元类
3.1友元函数
先举个例子来引出友元函数:
尝试重载operator<<:
版本1(×)
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
ostream& operator << (ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return cout;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 1, 6);
cout << d1;
return 0;
}
版本2(√)
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
ostream& operator << (ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return cout;
}
//private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 1, 6);
d1 << cout;
return 0;
}
运行结果:
版本3(√)
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
//private:
int _year;
int _month;
int _day;
};
ostream& operator << (ostream& _cout,const Date& d1)
{
_cout << d1._year << "-" << d1._month << "-" << d1._day << endl;
return cout;
}
int main()
{
Date d1(2024, 1, 6);
cout << d1;
return 0;
}
运行结果:
对于双操作数<<,第一个操作数为左操作数,第二个操作数为右操作数,版本1中cout为控制台对象作为第一个操作数,它的类型是ostream,与隐含的this指针抢占了第一个参数位置,this指针是指向当前对象,访问当前对象的成员函数和成员变量,所以this指针的类型是类的类型,与ostream类型不一致,所以cout对象不能传给this指针,且d1对象传给_cout也是类型不一致,若cout传给了_cout,那么d1对象传给谁呢?传给this指针吗?并不是,d1对象是第二个操作数,而this指针是接收第一个操作数,所以d1对象没有谁接收,虽然cout对象不会传给_cout对象。
版本2中,d1对象作为第一个操作数,传给隐含的this指针,cout对象传给_cout对象,这种方式在语法上是正确的,但是缺点就是可读性不好,正常认为是d1对象流入到cout控制台对象,而现在变成了cout控制台对象流入到d1对象。
版本3就解决了版本1,版本2的问题,将运算符的重载实现成了全局函数,而不是成员函数,全局函数中就不存在this指针了,就可以正常的指定参数。
在上面版本中,是将成员变量改成了公有的,才能对其访问,但正常来说成员变量是私有的,那么如何实现?这就引入了友元函数 :友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。look
#include <iostream>
using namespace std;
class Date
{
friend ostream& operator << (ostream& _cout, const Date& d1);
friend istream& operator >> (istream& _cin, Date& d1);
public:
Date()
{}
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator << (ostream& _cout, const Date& d1)
{
_cout << d1._year << "-" << d1._month << "-" << d1._day << endl;
return cout;
}
istream& operator >> (istream& _cin, Date& d1)
{
_cin >> d1._year >> d1._month >> d1._day;
return _cin;
}
int main()
{
Date d1(2024, 1, 6);
Date d2;
cout << d1;
cin >> d2;
cout << d2;
return 0;
}
已有两个全局的重载函数,这两个重载函数要想使用类中的私有成员,那么在类中,需声明该重载函数 并在前面加上friend,表明该重载函数是该类的朋友,有权限使用访问该类的私有成员。
注意:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类的访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
输出结果:
3.2友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。也就是我这个类是另一个类的朋友,那么我的类中的成员函数就可以访问另一个类中的私有成员。
注意:
- 友元关系是单向的,不具有交换性。也就是我是他的朋友,我可以访问他的私有成员,但他不能访问我的成员。
- 友元关系不能传递:如果c是b的友元,b是a的友元,则不能不能说明c是a的友元。
- 友元关系不能继承,之后继承章节会讲。
例子:
#include <iostream>
using namespace std;
class Time
{
friend class Date;//Date类是Time类的友元,那么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 = 2024, int month =1, int day = 17)
:_year(year)
, _month(month)
, _day(day)
{}
void friendsetTime(int hour = 20, int minute = 30, int second = 50)
{
//Date类中直接访问Time类中的私有成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d1;
d1.friendsetTime();
return 0;
}
友元不能传递的例子也是类比一下,小伙伴应该都没问题。
四、内部类
4.1概念+特性
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,他不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,因为内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元
特性:
1.内部类可以定义在外部类的public、protected、private都是可以的。
2.注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3.sizeof(外部类)的大小不包括内部类的大小,与内部类没有任何关系
#include <iostream>
using namespace std;
class A
{
public:
class B//B类为内部类,A类为外部类,B类天生就是A类的友元
{
public:
void f(const A& a)
{
cout << k << endl;//内部类可以直接访问外部类的成员
cout << a.k << endl;//通过外部类对象访问自己的成员变量
cout << a.h << endl;
cout << sizeof(A) << endl;//只计算了h的大小,内部类和静态成员变量都不算。
}
};
private:
static int k;//静态成员内部声明
int h;//这里没赋值,那么编译器会给个默认值
};
int A::k = 1;//静态成员外部定义
int main()
{
A::B b;
b.f(A());//A()为匿名对象
return 0;
}
运行结果:
五、匿名对象
在静态变量那提过了匿名对象,那么现在进一步进行描述以及注意的点。look
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << a << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class solution
{
public:
int sum_solution(int n)
{
n += 10;
return n;
}
};
int main()
{
A aa1=1;//隐式类型转换
//A aa1();//不能这么定义对象,因为编译器无法识别这是对象定义,还是函数声明
A();//这是匿名对象,匿名对象的特点是可以不用取名字,但是他的生命周期只有这一行
A(2);//创建匿名对象并进行传参
cout << solution().sum_solution(10) << endl;
return 0;
}
运行结果:
匿名对象的生命周期只在定义的这一行,由代码也可以反映,匿名对象销毁了就调用了析构函数 ,匿名对象在很多地方下都挺方便的,减少了代码的冗余量。
六、拷贝对象时的一些编译器优化
在新的编译器下,对于自定义类型的对象进行拷贝时,进行以下组合,编译器会相应的优化:
- 构造+拷贝构造->优化为构造
- 拷贝构造+拷贝构造->拷贝构造
- 拷贝构造+赋值重载->无法优化
接下来用一段代码进行解析 :
#include <iostream>
using namespace std;
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;
}
int main()
{
f1(1);//隐式类型转换,1发生隐式类型转换,构造一个临时对象,再拷贝构造给aa对象。
f1(A(2));//A(2)创建一个匿名对象,再拷贝构造给aa对象
cout << endl;
A aa2 = f2();//f2()函数中,return aa时创建了临时对象,aa由临时对象接收,临时对象再拷贝构造给f2(),f2()再拷贝给aa2对象
cout << endl;
A aa1;//创建一个对象
aa1 = f2();//f2()中发生了拷贝构造,再将f2()赋值给aa1,注意此时进行的是赋值拷贝,而不是拷贝构造
cout << endl;
return 0;
}
运行结果加分析:
由上面结果可以分析,在这些构造组合中,代码需在同一行才会发生优化。像aa1对象,是先创建了aa1对象,再进行赋值拷贝,这样就不会进行优化了 ,而是每一步都执行。还有一点,越新的编译器他的优化效果更厉害,上面结果及结论是在vs2019下适合,在vs2022并不一定。
创作不易,点个小赞吧~