目录
- 1. 再谈构造函数
- 1.1 构造函数体赋值
- 1.2 初始化列表
- 1.3 explicit关键字
- 2. static成员
- 2.1 概念
- 2.2 特性
- 3. 友元
- 3.1 友元函数
- 3.2 友元类
- 4. 内部类
- 5. 匿名对象
- 6. 拷贝对象时的一些编译器优化
1. 再谈构造函数
1.1 构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
那么对象中的成员是在哪里完成初始化的呢?
1.2 初始化列表
实例化一个对象时,是给这个对象整体开辟一块空间,而其中的每个成员变量则是在初始化列表中进行初始化的,而非在构造函数体内。
初始化列表的语法:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式:
class Date
{
public:
Date(int year, int month, int day)
:_year(year), _month(month), _day(day)
{ }
private:
int _year;
int _month;
int _day;
};
初始化列表所在的位置在于函数头和函数体(大括号)之间的部分,所有成员变量的初始化都是在这里进行的,然后才会进入函数体。
有些情况只能使用初始化列表来初始化
注意:
-
每个成员变量在初始化列表中最多只能出现一次(最多只能初始化一次)。
-
类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量、
const
成员变量和自定义类型成员(且该类没有默认构造时)。 -
对于自定义类型成员变量,一定会先使用初始化列表初始化。
对于const
和引用变量来说,const
变量只有一次初始化的机会,就是在定义的时候,而引用变量也是要在定义的时候就要引用一个实体,因为函数体中进行的是赋值,并不是初始化,因此要在初始化列表中进行定义和初始化。
对于自定义类型,若没有能直接使用的默认构造,则也是必须通过初始化列表传递参数,这样编译器才能调用它的构造函数;若有默认构造,则不需要在初始化列表中显式地写。
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int& ref)
:_aobj(a) //调用它的构造
,_ref(ref)
,_n(10)
{}
A _aobj; //A类中没有默认构造
int& _ref; //引用
const int _n; //const成员
};
对于这种类型,也需要使用初始化列表来初始化:
class C {
public:
C(int a = 0)
:_a(a)
{ }
int _a;
};
class D {
public:
C _c;
};
int main() {
D d0;
return 0;
}
D类型中的成员变量是自定义类型C的对象,并且C类中已经实现了默认全缺省构造函数,因此D类中是可以不写默认构造的,不写编译器也会自动生成然后调用C的构造,但是每次构造后_c中的成员变量_a的值始终是缺省值0,那如果想通过给D类型对象传参来控制_c对象中_a变量的值时该怎么做?
同样只能通过初始化列表的方式,显式地传参调用C类中的默认缺省构造,因此D类中的构造函数要自己实现,并且要满足有一个默认构造:
class D {
public:
//最好写成全缺省的构造
//即默认构造
D(int x = 0)
:_c(x)
{ }
C _c;
};
这样就可以在调用的时候给D类对象传递参数来控制_c对象中_a的值:
int main() {
D d0;
//不传,_a为缺省值0
D d1(1);
//传了,_a就是你传递的值
return 0;
}
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,编译器都会先走一遍初始化列表。
初始化列表能完成绝大多数的初始化工作,但始终有些还是无法完成,只能在函数体内部解决,所以若初始化列表不方便做的,就放在函数体中做
有了对初始化列表的理解,对于给成员变量声明缺省参数的做法,本质也是调用构造函数时把缺省值传递给初始化列表来对其进行初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
这里的输出结果是1和随机值,因为_a2先声明_a1后声明,因此先用_a1初始化_a2,但是_a1此时是随机值,然后在用传入的参数1来初始化_a1。
1.3 explicit关键字
class AA {
public:
AA(int a) :_a(a) {
cout << "AA(int a)" << endl;
}
AA(const AA& a) :_a(a._a) {
cout << "AA(const AA& a)" << endl;
}
private:
int _a;
};
int main() {
AA a1(1);
AA a2 = 2;
return 0;
}
对于以上两种实例化对象的方式,第一种很明显是构造。
第二种,因为2是整形,与AA类型不符合,因此会发生隐式类型转换,转换过程为:先用2去构造一个AA类型的临时对象,再用这个临时对象去拷贝构造a2,但是编译器会对这种在一个表达式中连续构造的情况进行优化,直接优化为用2来构造a2对象,以此提高效率。
如何证明编译器创建了一个临时对象?
可以发现如果引用这个临时对象,编译器会报错,因为临时对象具有常量属性,必须要用const
修饰。
对于内置类型也是同样的情况,这里也发生了隐式类型转换:
d并不是引用i,而是编译器用i初始化了一个double
类型的临时对象,d是引用的这个临时对象,同样需要加上const
来修饰。
如果不想让隐式类型转换的行为发生,就需要用到explicit关键字,用它来修饰构造函数,即禁止了构造发生隐式类型转换的情况。
2. static成员
若想统计一个类实例化了多少对象(程序中现存的对象),该怎么做呢?
其中一种做法是使用全局变量,构造或拷贝构造就++,析构–:
int _scount = 0;
class AA {
public:
AA() { ++_scount; }
AA(const A & t) { ++_scount; }
~AA() { --_scount; }
private:
int _a;
};
这种方法可以解决问题,但因为它是全局变量,任何地方都可以修改它,所以若其它地方不小心修改了这个全局变量,那么结果就会出错。
2.1 概念
介于这种情况,C++设计出了静态成员变量,把该变量封装在了类中:
class AA {
public:
AA() { ++_scount; }
AA(const A & t) { ++_scount; }
~AA() { --_scount; }
private:
int _a;
static int _scount;
};
什么是静态成员?
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
与普通成员变量不同的是:
普通成员变量是属于不同的对象,即不同的对象存储着不同的成员变量;而静态成员变量是属于整个类的,单独存储在静态区,因此不属于任何一个对象,但是每个对象都可以共享使用它;初始化的方法也不同,静态成员变量一定要在类外进行初始化,不属于任何对象就不能在构造函数中初始化,只能在全局。
同样也无法声明缺省值,除非加上
const
,但是给了const
就无法修改了
//类外
int AA::_scount = 0;
是类中的成员,需要在变量名前指定类域,同样只能初始化一次。
若要访问它,可以通过下面这种方式:
//指定访问类中的静态成员变量
int cnt = AA::_scount;
但如果_scount
被private
修饰,外部也是没有办法访问到的,因此也需要使用公有的成员函数来访问:
class AA {
public:
AA() { ++_scount; }
AA(const A & t) { ++_scount; }
~AA() { --_scount; }
int GetAACount() {
return _scount;
}
private:
int _a;
static int _scount;
};
如果上面这种成员函数,则必须通过创建一个对象来调用,因为要传递this
指针,若不想创建对象就调用这个成员函数,则要把其定义为静态成员函数:
static int GetACount() {
return _scount;
}
与普通成员函数的区别在于参数里没有this
指针,又因为没有this
指针,所以静态成员函数无法访问非静态成员变量。
下面这段代码会报错:
static int GetACount() {
//无法访问非静态成员变量*_a
_a = 0;
return _scount;
}
调用这个静态成员函数的方式除了通过对象来调用的方式之外还多了一种:类名::函数名()
。
//直接指定类域调用
int cnt = AA::GetACount();
//创建对象调用也是可以的
AA a;
int cnt = a.GetACount();
因此当类中有静态成员变量时大多数情况下都要为其配备一个静态成员函数。
静态成员变量具有全局性,只不过受访问限定符的约束
【问题】
- 非静态成员函数可以调用类的静态成员函数吗?
- 静态成员函数可以调用非静态成员函数吗?
有了上面的理解就很好回答了,非静态成员函数中访问对象中的成员变量或者调用其它成员函数都是通过this指针来操作的,即使是静态成员也可以通过this指针访问,所以非静态成员函数可以调用类的静态成员函数。
而静态成员函数并没有传递this指针,因此是没有办法访问非静态成员的。
如何设计一个类,使得在类外只能在栈上创建对象?
class AA {
public:
static AA getStackObj() {
AA aa;
return aa;
}
private:
AA() {}
int _a = 0;
};
int main() {
AA a = AA::getStackObj();
return 0;
}
将构造函数私有化,外面无法直接调用,但类中不受限制,然后在类中定义一个公有的成员函数,创建一个栈上的对象将其返回,但是该函数必须要是静态成员函数,这样不需要对象即可调用,否则得先创建对象,但是构造函数被私有化了,创建不了,那这个函数就也无法调用了,所以要定义为静态成员函数。
只能在堆上创建对象也是同理
2.2 特性
简单总结下静态成员的特性:
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问。
- 静态成员函数没有隐藏的
this
指针,不能访问任何非静态成员。 - 静态成员也是类的成员,受
public、protected、private
访问限定符的限制。
3. 友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
耦合度是指两者之间的关联程度
友元分为:友元函数和友元类。
3.1 友元函数
上篇文章简单介绍过重载operator<<
,使其能输出自定义类型,然后发现没办法将operator<<
重载成成员函数。
因为cout
的输出流对象和隐含的this
指针在抢占第一个参数的位置。可是this
指针默认是第一个参数也就是左操作数了。
但实际使用中cout
需要是第一个形参对象,才能正常使用。所以要将operator<<
重载成全局函数。
但又会导致类外没办法访问成员,此时就需要友元来解决,operator>>
同理。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, 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& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year >> d._month >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
关于友元函数:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数。
- 友元函数不能用const修饰。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元函数的调用与普通函数的调用原理相同。
3.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有成员。
class Time
{
// 声明日期类为时间类的友元类
//则在日期类中就直接访问Time类中的私有成员变量
friend class Date;
//Data类中的所有成员函数都是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;
};
关于友元类:
- 友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承,后续会介绍。
4. 内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
class A {
private:
static int k;
int h;
void print() const {
cout << h;
}
public:
class B {
public:
void foo(const A& a) {
cout << k << endl;//OK?
cout << a.h << endl;//OK?
a.print();//OK?
}
};
};
int A::k = 1;
A类的大小是4字节,k是静态成员变量,不存储在对象中,B类只是在A类中声明,并没有占空间,只有用B创建对象才会占空间,所以实际A类对象中只有变量h占了4个字节。
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
实例化一个A类对象,并不会实例化B类的对象,除非A类中有B类的对象
若想创建B类对象,必须要先指定它在哪个类域中:
int main() {
//指定类域
A::B b;
return 0;
}
内部类受访问限定符的限制,若是私有,则外部无法直接找到B类
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
由于内部类天生就是外部类的友元,因此B类中的foo函数想输出A类中的私有成员变量以及调用A类中的成员函数是OK的
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
5. 匿名对象
class A
{
public:
A(int a = 0) :_a(a) {
cout << "A(int a)" << endl;
}
~A() {
cout << "~A()" << endl;
}
private:
int _a;
};
定义匿名对象:
int main()
{
//有名对象
A aa1(1);
//匿名对象
A(2);
return 0;
}
有名对象和匿名对象的区别:
有名对象的生命周期在它当前被定义的局部域中,而匿名对象的是生命周期只有被定义的那一行。
匿名对象的用途之一在于,只想单纯地调用该类中的成员函数,比如以下类:
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
则可以使用匿名对象来调用:
int main()
{
int ret = Solution().Sum_Solution(10);
return 0;
}
这种情况下用匿名对象比较方便。
能否引用一个匿名对象?
int main()
{
const A& a = A();
return 0;
}
可以,但必须用const
修饰,匿名对象与临时对象相似,都具有常量属性,因此要加上const
,除此之外,被引用的匿名对象的生命周期被延长了,延长到它当前被定义的局部域中。
6. 拷贝对象时的一些编译器优化
对于以下代码:
A func() {
A aa;
return aa;
}
int main()
{
A ret = func();
return 0;
}
一般而言,调用这个函数返回时会发生两次拷贝构造,传值返回要拷贝构造一个临时对象,返回的是这个临时对象,然后再用返回的这个临时对象拷贝构造ret。
比较老的编译器是这种情况,而最新的编译器都会对其进行优化,对于同一行的表达式如果有构造+拷贝构造或者连续拷贝构造会直接优化为拷贝构造。
只有同一个表达式中这种类似于连续构造或者拷贝构造的情况才会触发编译器的优化,不同的表达式中不会发生
因此能写在一行就尽量写在一行,不仅方便,编译器还会对其进行优化。
可以发现二者的区别,第一种写法比第二种写法直接优化了一个构造和一个赋值重载,优化是比较大的。