初始化列表
给类的对象初始化(赋初值)有两种手段:
- 在构造函数的函数体内赋值;
- 初始化列表
第一个方式在讨论构造函数时已经进行过说明,在创建对象时,编译器通过调用构造函数给对象中的各个成员变量赋一个合适的初始值。虽然通过调用构造函数,对象的各成员变量有了一个初始值,但严格来说并不能将其称为对象成员的初始化,因为初始化只进行一次,而在构造函数体内可以进行多次赋值。事实上,初始化列表是成员变量被定义和初始化的位置。
初始化列表是构造函数的一部分,在进入构造函数体之前执行,完成对成员的定义和初始化。初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式,编译器将这些初始值或表达式的返回值赋给成员变量。
class Date
{
private:
int _year; //声明位置
int _month;
int _day;
public:
Date(int year = 1912, int month = 10, int day = 10)
:_year(year), //初始化列表:定义和初始化位置
_month(month),
_day(day)
{ }
/*…………*/
};
使用初始化列表需要注意的是:
- 每个成员变量在初始化列表中最多只能出现一次,因为初始化只能进行一次。
- 类中的以下成员必须在初始化列表进行初始化:
- 引用成员变量
- const成员变量
- 没有默认构造函数的自定义成员变量。
- 尽量使用初始化列表初始化,无论是否写了初始化列表,自定义成员一定会先使用初始化列表。
- 初始化列表可以完成对大部分成员变量的初始化,但不能解决所有问题,有些行为需要在构造函数体内进行。
- 初始化列表的初始化顺序与类中的声明顺序一致,与初始化列表的顺序无关。建议按照类中成员的声明顺序写初始化列表。
class A
{
public:
A(int a)
:_a(a), //再用初始化_a,为a
_b(_a) //先用_a初始化_b,为随机值
{ }
void Print()
{
cout << _a << " " << _b << endl;
}
private:
int _b;
int _a;
};
- C++11中,在类中声明成员变量时可以给其一缺省值,该缺省值供初始化列表使用。
静态对象和静态成员
静态对象
被static
修饰的对象为静态对象。静态对象只创建一次,即只调用一次构造函数。静态对象存储在静态区。
静态成员变量
有时需要一些具有全局性质的变量,在对某个类操作的整个过程中都起作用,此时若使用全局变量,则在程序的任何地方都可以对其进行访问和操作,缺乏安全性。此时静态成员变量可以在满足需求的同时保证封装,以提高使用时的安全性。
被static
修饰的成员变量为静态成员变量。普通的成员变量属于每一个实例化的类对象,存储在对象中,处于对象的层面;而静态成员变量属于类,为类的每个对象共享,存储在静态区,处于类的层面。
规定静态成员变量在类里面声明,在类外面(全局)定义。静态成员变量不走初始化列表,在类中声明静态变量时不能给缺省值。静态成员变量受类的访问限定符的限制,静态成员变量的访问需要突破类域和类的访问限定符。
//设计一个类,可以计算出程序中有多少个该类的对象
class B
{
private:
static int _count; //静态成员变量的声明
public:
B() {
++_count;
}
B(const B& b) {
++_count;
}
~B() {
--_count;
}
static int GetCount() {
return _count;
}
};
int B::_count = 0; //静态成员变量的定义和初始化
静态成员函数
用static
修饰的成员函数为静态成员函数。静态成员函数不含隐藏的 this 指针参数,指定类域并突破访问限定符即可访问,不需要指定实例化的对象,一般与静态成员变量配套出现。同时,由于静态成员函数不含this指针参数,所以其不能访问类的非静态成员。静态成员函数受类的访问限定符的限制,静态成员函数的调用需要突破类域和类的访问限定符。
关于静态成员的访问关系总结如下:
- 静态成员函数可以访问静态成员;
- 静态成员函数不可以访问非静态成员;
- 非静态成员函数可以访问静态成员;
- 静态成员函数可以调用构造函数,因为调用构造函数不需要 this 指针。
//设计一个类,使该类只能在栈区或者堆区实例化对象
class C
{
public:
static C GetStackObj()
{
C newObj; //静态成员函数可以调用构造函数
return newObj;
}
static C* GetHeapObj()
{
C* pNewObj = new C;
return pNewObj;
}
private:
C() //将构造函数设为私有以实现封装
{ }
};
int main()
{
//指定类域并突破访问限定符即可访问静态成员函数
C cStatck = C::GetStackObj();
C* cHeap = C::GetHeapObj();
return 0;
}
友元和内部类
友元函数
用friend
关键字在类中声明友元函数,友元函数可以访问类的所有成员。友元函数是定义在类外面的普通函数,不含this指针,不属于任何类,但需要在类内部声明。
友元函数不能被 const 修饰;友元函数可以在类的任何地方进行声明;一个函数可以是多个类的友元,友元函数的调用方式与普通函数相同。
class Date
{
//声明友元函数
friend ostream& operator<<(ostream& cout, const Date& d);
private:
int _year;
int _month;
int _day;
public:
Date(int year = 1840, int month = 6, int day = 1)
:_year(year),
_month(month),
_day(day)
{ }
};
//定义友元函数
ostream& operator<<(ostream& cout, const Date& d)
{
//直接访问类的私有成员
cout << d._year << ' ' << d._month << ' ' << d._day;
return cout;
}
友元类
用friend
关键字在类中声明类的友元类,一个类的友元类的所有成员函数为被友元类的友元函数,可以访问被友元类的所有成员。
友元关系是单向的,不具有交换性;友元关系不能传递;友元关系不能继承。
友元提供了一种突破封装的方式,一方面提供了便利,另一方面破坏了封装,增加了耦合,所以友元不宜多用。
内部类
如果一个类 A 在另一个类 B 中声明,那么类 A 即为类 B 的内部类。内部类是一个独立的类,不属于外部类,外部类对内部类的成员没有任何优越的访问权限。
内部类是外部类的友元类,内部类可以通过外部类的对象访问外部类的所有成员。内部类的访问受外部类的访问限定符的限制。内部类可以直接访问外部类的静态成员,不需要类名或对象。内部类只是一个声明,计算外部类的大小时,不将内部类计入在内。
/*求1+2+3+...+n,要求不能使用乘除法、循环、递归*/
class Solution
{
private:
class Tmp //Solution类的内部类
{
public:
Tmp()
{
_ret += _i;
++_i;
}
};
private:
static int _i;
static int _ret;
public:
int Sum_Solution(int n)
{
Solution::Tmp a[n];
return _ret;
}
};
int Solution::_i = 1;
int Solution::_ret = 0;
匿名对象
匿名对象的特点是没有对象名,匿名对象的生命周期只在本行。匿名对象适用于一些对象即用即销毁的场景。匿名对象具有常属性,引用时只能进行const引用,且const引用会延长匿名对象的生命周期,使匿名对象的生命周期为当前函数的局部域。
class D
{
private:
int _n;
public:
D(int n = 0)
:_n(n) {
cout << "D(int n)" << endl;
}
int GetN() {
return _n;
}
~D() {
cout << "~D()" << endl;
}
};
int main()
{
D d; //普通对象,生命周期在当前函数的局部域
D(1); //匿名对象,生命周期在当前行
cout << "-------" << endl;
cout << D(2).GetN() << endl;
cout << "-------" << endl;
//D& rD = D(20); //错误
const D& rD = D(20);
D(3);
cout << "-------" << endl;
return 0;
}
类对象的隐式类型转换和构造时编译器的优化
class E
{
private:
int _i;
public:
E(int i)
:_i(i)
{
cout << "E(int i)" << endl;
}
E(const E& e)
:_i(e._i)
{
cout << "E(const E& e)" << endl;
}
E& operator=(const E& e)
{
cout << "E& operator=(const E& e)" << endl;
_i = e._i;
return *this;
}
~E()
{
cout << "~E()" << endl;
}
};
针对像上面这样只有一个成员变量的类,如果写出E e1 = 41;
这样的代码,编译器并不会将其视为错误语法,而会先以变量 41 调用构造函数,产生一个具有常属性的临时对象,并将e1
以该临时对象调用拷贝构造。这种行为实质上是一种隐式类型转换,类似将一个浮点型变量赋值给一个整型变量,会产生一个中间变量进行中转。
事实上,针对这种在同一行的连续构造和拷贝构造,现行的编译器往往会进行优化,直接调用一步构造函数。
针对以下情况,现行编译器往往会进行优化:
- 同一行连续构造 + 拷贝构造 -> 构造;
- 同一行连续拷贝构造 + 拷贝构造 -> 拷贝构造
需要注意的是,同一行的连续拷贝构造和赋值重载编译器往往不会进行优化。
E func1()
{
E e;
return e;
}
void func2(E e)
{
}
int main()
{
E e1 = 41; //同一行连续构造 + 拷贝构造 -> 构造
//E& e2 = 44; // wrong
const E& e2 = 44;
E e3 = E(48); //同一行连续构造 + 拷贝构造 -> 构造
func2(E(52));//同一行连续构造 + 拷贝构造 -> 构造
E e4 = func1(); //同一行连续拷贝构造 + 拷贝构造 -> 拷贝构造
e1 = func1(); //同一行连续拷贝构造 + 赋值重载 -> 不优化
e1 = E(62); //同一行连续拷贝构造 + 赋值重载 -> 不优化
return 0;
}