之前我们给对象赋初值的方法是:在创建对象时,编译器调用构造函数、拷贝构造函数和赋值运算符重载来给对象赋初值。但这不能叫做初始化,只能叫做赋值(函数体内赋值),因为初始化只能初始化一次,而构造函数体内可以多次赋值。
你或许会疑问,赋值和初始化有什么不同?
我们举个简单的例子,const常量:我们知道,const常量是不可以被随意修改的,只有在初始化时得到一个值,此后无法通过赋值等方式改变其值。
好,接下来我们看这段代码:class Date { public: Date(int year = 1999, int month = 1, int day = 1, const int a = 1) { _year = year; _month = month; _day = day; _a = a; } private: int _year; int _month; int _day; const int _a; };
VS2022编译器告诉我们,const成员变量
_a
还没有初始化呢,但是我明明在构造函数里写了_a = a
啊!为什么?很简单,构造函数里的一系列操作根本不是初始化!也就是说仅仅通过构造函数来赋初始值根本满足不了所有的成员变量。那该怎么初始化呢,初始化列表就是用来初始化的,也只有它才是真正的初始化(这里是指在类中)。
概念
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{ }
private:
int _year;
int _month;
int _day;
};
使用场景
你要是说你就喜欢在构造函数体内给变量赋初值,那自然也是可以的,但是在下面的情境中必须要使用初始化列表:
class A
{
public:
A(int a)
: _a(a)
{}
private:
int _a;
};
class B
{
public:
B(A a, int ref)
: _aobj(a)
, _ref(ref)
, _n(10)
{}
private:
A _aobj; // 自定义类型,无默认构造函数
int& _ref; // 引用
const int _n; // 常量const
};
初始化列表只能写在构造函数中(包括拷贝构造)。
成员变量只能初始化一次,不要在初始化列表中多次初始化。
成员变量在初始化列表中的初始化顺序(谁先声明先在列表处初始化谁)与其在初始化列表中的先后次序无关,与在类中声明次序有关。
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化,编译器会自动生成默认的。
class A
{
public:
A(int a)
: _a(a)
{}
private:
int _a;
};
class Date
{
public:
Date()
{}
private:
int _year = 1;
int _month = 2;
int _day = 3;
};
// 编译器会自动生成初始化列表,并且把1、2、3初始化给这几个成员变量。这叫成员变量缺省值。
当成员变量缺省值、参数缺省值、初始化列表同时存在,先后顺序是什么呢?
我们可以看到,
_year
使用初始化列表,_month
使用参数(缺省参数),_day
使用成员变量缺省值。由此可以得出:
- 初始化列表的括号里是啥,初始化结果就是啥。比如_year就是2, _month就是用的month(如果不给这个3,那么 _month就是缺省参数1)
- 如果初始化列表中没有,那么自动生成初始化列表,成员变量的缺省值就是最终结果,比如_day就是0
构造函数的功能不仅是构造与初始化对象,对于只有单个参数或者除第一个参数无默认值,其余均有默认值的构造函数,还具有类型转换的作用。
你或许会疑问类型转换在哪里,接下来我们看一段代码:
class A { public: A(int a) : _a(a) { cout << "A(int a)" << endl; } A(const A& a) { cout << "A(const A& a)" << endl; } A& operator=(const A& a) { cout << "A& operator=(const A& a)" << endl; return *this; } private: int _a; }; int main() { A a1(1); // 构造函数 A a2 = 2; // 原本应该是构造一个A类型的临时变量,再把临时变量拷贝构造给a2;现在变成了直接用2构造a2,这是编译器的优化,两步优化成一步。 a2 = a1; // 赋值运算符重载 A a3 = a1; // 拷贝构造 return 0; }
输出结果如下:
此时的代码
A a2 = 2;
通过编译器的优化,就相当于隐式类型转换,而如果我们在构造函数前面加上关键字explicit
,那么编译就无法通过了:
这就是explicit
的作用:禁止构造函数进行隐式类型转换。
在智能指针中会用到这个功能。
static并不陌生,它可以将临时变量等转变成静态变量。在类中,用static
修饰的成员变量,称为静态成员变量,用static
修饰的成员函数,称为静态成员函数。
静态成员变量必须在类外定义、初始化。定义时不必加static关键字,在类中只是声明。
class A
{
public:
// ...
private:
static int _a;
};
// 全局位置
int A::_a = 0;
静态成员(静态成员变量+静态成员函数)为所有类对象所共享,不属于某个具体的对象,存放在静态区。
静态成员可以在类外用类名::静态成员
或者对象.静态成员
来访问。但是前提是它是public的,因为静态成员也是类的成员,受public、protected、private 访问限定符的限制
静态成员函数没有隐藏的this指针,不能访问任何非静态成员。也就是说静态成员函数和静态成员变量是一起存在的,可以通过静态成员函数来访问静态成员变量。
静态成员函数不能访问非静态成员变量,凡是非静态成员函数可以访问静态成员变量。
对于类中的非共有成员,我们无法在类外访问,而友元可以突破这个限制。
友元可以突破封装,有时虽然提供便利,但会增加耦合度,所以不宜多用。
概念:友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend
关键字。
在前面写日期类的时候(参考我的上一篇博客),我们遇到一个问题:重载operator<<
的时候,无法将其重载为成员函数,原因是:cout的输出流对象和隐藏的this指针在抢占第一个参数的位置。this指针默认是第一个参数(左操作数),而我们使用cout时,cout一般是左操作数(第一个参数),此时发生冲突。于是我们不能使用隐藏的this指针了,也就是operator<<
必须重载成全局函数。但是此时又有一个问题:类外无法访问类内私有成员,此时就到友元发挥作用了。(operator>>同理)
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 2000, 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 << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d1;
cout << d1;
cin >> d1;
cout << d1;
return 0;
}
特性:
友元类,和友元函数的声明方式相似,在另一个类中加上friend class 类名
。
友元类中的所有成员函数都是另一个类的友元函数。
class Time
{
// 声明日期类是时间类的友元类,在Date类中就可以直接访问任何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, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTime(int hour, int minute, int second)
{
// 因为Date是Time的友元类,Date的成员函数都是Time的友元函数
this->_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
特性:
概念:一个类定义在另一个类的内部,这个类就叫内部类。
内部类是一个独立的类,它不属于外部的类,不能通过外部类的对象去访问内部类的成员。
特性:
内部类是外部类的友元类。
内部类可以定义在外部类的任何地方。但是要在类外直接创建一个内部类的话,内部类要放在外部类的public下。
class A
{
public:
A(int a)
: _a(a)
{}
private:
int _a;
static int _s;
public:
// 内部类B
class B
{
public:
B(int b)
: _b(b)
{}
void Func()
{
cout << _s << endl;
A a1(1);
cout << a1._a << endl;
}
private:
int _b;
};
};
// 全局域
int A::_s = 1;
int main()
{
A::B b1(10); // 内部类B要写在外部类A的public下,才可以通过这种方式创建B类对象。
b1.Func();
cout << sizeof(A) << endl; // 结果是4,说明静态成员变量、内部类都不算作外部类的大小。
return 0;
}
内部类可以直接访问外部类中的static
成员,不需要外部类的类名或对象名。(不需要类名::静态成员
或者对象.静态成员
的形式,请参考上面的代码)
外部类的大小与内部类无关。
endl;
A a1(1);
cout << a1._a << endl;
}
private:
int _b;
};
};
// 全局域
int A::_s = 1;
int main()
{
A::B b1(10); // 内部类B要写在外部类A的public下,才可以通过这种方式创建B类对象。
b1.Func();
cout << sizeof(A) << endl; // 结果是4,说明静态成员变量、内部类都不算作外部类的大小。
return 0;
}
```
内部类可以直接访问外部类中的static
成员,不需要外部类的类名或对象名。(不需要类名::静态成员
或者对象.静态成员
的形式,请参考上面的代码)
外部类的大小与内部类无关。