目录
1.再谈构造函数
(1)引入初始化
在C++类与对象(二)中谈到了构造函数的初级应用,里面介绍了怎样利用构造函数给成员变量赋值。这里会讲解如何初始化成员变量。(赋值和初始化不能混为一谈)
接下来验证一下,我们之前的构造函数是赋值,而不是初始化。
代码一:我们都知道,const类型的变量需要初始化,如果在以前的构造函数中对它不能成功进行初始化,说明之前的构造函数是赋值,而不是初始化。
此代码编译时直接报错,看来这并不是赋值,那么如何对成员变量进行初始化?(本文代码均在win10系统下的vs2019验证)
//代码一
#include "iostream"
using namespace std;
class Test {
public:
int _a;
const int _b;
public:
Test(int a,int b) {
_a = a;
_b = 10;
}
};
int main() {}
(2)初始化列表
C++中为了初始化成员变量,引入初始化列表。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个”成员变量“后面跟一个放在括号中的初值表达式。
代码二:用法举例。使用初始化列表后,const成员初始化成功。
//代码二
#include "iostream"
using namespace std;
class Test {
public:
int _a;
const int _b;
public:
Test(int a)
:_a(a)
,_b(10)
{}
};
int main() {
Test t1(5);
cout << t1._b << endl;//输出 10
}
(3)初始化列表性质
<1>每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。一旦出现多次编译器就会报错。
<2>初始化列表可以初始化引用类型的变量。
代码三:
//代码三
#include "iostream"
using namespace std;
class Test {
public:
int _a;
int& _b;
public:
Test(int a)
:_a(a)
,_b(_a)
{}
};
int main() {
Test t1(5);
cout << t1._b << endl;//输出 10
}
<3>初始化列表可以初始化自定义类型成员。(该类没有默认构造函数)
当类类型的成员变量没有默认构造函数时,必须在初始化列表进行初始化。因为当一个类显式实现了构造有参的构造函数(不包括无参构造函数和全缺省构造函数,因为他们也是默认的构造函数),编译器将不会生成默认的构造函数,也就无法调用了。
如果类类型成员变量有默认的构造函数,那么就算不初始化,编译器也会自动调用它的默认的构造函数。
代码四:该类类型变量有全缺省构造函数,可以不在初始化列表初始化。
//代码四
#include "iostream"
using namespace std;
class Time {
private:
int _hour;
int _minute;
int _second;
public:
Time(int a = 10) {
cout << "Time构造函数被调用" << endl;
}
};
class Date {
private:
int _year;
int _month;
int _day;
Time t1;
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
};
int main() {
Date d1(1,2,3);
}
代码五:Time类中有非缺省的带参构造函数。所以Date的构造函数需要对t1对象进行初始化。
//代码五
#include "iostream"
using namespace std;
class Time {
private:
int _hour;
int _minute;
int _second;
public:
Time(int hour,int minute,int second)
:_hour(hour)
,_minute(minute)
,_second(second)
{}
};
class Date {
private:
int _year;
int _month;
int _day;
Time t1;
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
,t1(14,59,50)
{}
};
int main() {
Date d1(1,2,3);
}
<4>成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
代码六:验证特性<4>。按照初始化列表,使用year初始化_year,day初始化_day,_day初始化_month。那么最终打印结果应该是:_year: 2021 _month: 10_day: 10。
但实际上_month打印后却是随机数,因为成员变量的初始化顺序是其在类中声明的顺序,与初始化列表的排列顺序无关。_day的初始化应该是在_month的后面,但_month在_day初始化之前就访问_day,这时候_day肯定没有初始化,所以是这样的结果。
//代码六
#include "iostream"
using namespace std;
class Date {
public:
int _year;
int _month;
int _day;
public:
Date(int year,int day,int month)
:_year(year)
, _day(day)
,_month(_day)
{}
};
int main() {
Date d1(2021,10,5);
cout << "_year: " << d1._year << " _month: " << d1._month << " _day: " << d1._day;
//输出 _year: 2021 _month: -858993460 _day: 10
}
(4)explicit关键字
构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
先观察代码七,里面有一个很有意思的现象,整形的2022竟然可以为对象赋值。
代码七:以前只知道内置类型存在隐式类型转换,难道对象也有吗?通过打印地址来进行分析。
//代码七
#include "iostream"
using namespace std;
class Date {
public:
int _year;
int _month;
int _day;
public:
Date(int year)
:_year(year)
{
cout << "构造函数调用:" << this << endl;
}
Date& operator=(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
cout << "赋值重载调用:" << this << endl;
cout << "赋值重载调用:" << &d << endl;
return *this;
}
~Date() {
cout << "析构函数调用:" << this << endl;
}
};
int main() {
Date d1(1999);
cout << "从此处开始观察" << endl;
cout << "======================" << endl;
d1 = 2022;
}
下图是代码七的打印结果,对其进行分析:
首先打印出来的地址一定是d1对象的,那么另一个自然是赋值重载中的参数对象的地址。并且这个地址也调用过构造函数,所以其实系统是先为2022创建了一个对象,然后调用赋值运算符重载来完成复制,而不是看到的直接赋值。
那么如何阻止这种情况发生呢?只需要在单参构造函数名前加上explicit关键字,就不会出现这样的情况了。如:
代码八:
//代码八
explicit Date(int year)
:_year(year)
{
cout << "构造函数调用:" << this << endl;
}
(5)总结初始化
<1>每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
<2>初始化列表可以初始化引用类型的变量。
<3>初始化列表可以初始化自定义类型成员。(该类没有默认构造函数)
<4>成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
2.static成员
(1)引入
来想一个问题,如何统计类创建了多少个对象?第一反应是,使用全局变量。
代码九:通过全局变量来记数。根据打印结果看出,全局变量可行,首先调用两次构造函数,count从1到2,生命周期结束,调用析构函数,cont从2到0。
//代码九
#include "iostream"
using namespace std;
int count = 0;
class Date {
public:
int _year;
public:
Date(int year)
:_year(year)
{
::count++;
cout << ::count << " ";
}
~Date() {
::count--;
cout << ::count << " ";
}
};
void Test() {
Date d1(1999);
Date d2(2000);
}
int main() {
Test();//打印 1 2 1 0
}
采用全局变量的弊端:全局变量也可以被其他函数使用或修改,不安全。但它给了我们提示:我们需要在类中创建一个可以被该类所有对象所共享的变量来计数,并且这个变量要安全。
这就需要static关键字了。
在类中被static修饰的成员变量被称为静态成员变量。
(2)特性与验证
<1>静态成员变量必须在类外定义,因为在类中只是声明,定义时不添加static关键字。
代码十:定义与声明方式如下。注意:定义时需要加上 类名::,但不需要加static。
//代码十
#include "iostream"
using namespace std;
int count = 0;
class Date {
public:
int _year;
static int count;
public:
Date(int year)
:_year(year)
{}
};
int Date::count = 0;
int main() {}
<2>静态成员为所有类对象所共享,是类的属性,不属于某个具体的实例,没有存储在对象中。
代码十一:通过计算代码十中对象大小验证。d对象大小是4,说明对象中只有_year。
//代码十一
#include "iostream"
using namespace std;
class Date {
public:
int _year;
static int count;
public:
Date(int year)
:_year(year)
{}
};
int Date::count = 0;
int main() {
Date d(1999);
cout<<sizeof(d);//输出 4
}
<3>类静态成员可使用类名::静态成员或者对象.静态成员来访问。
静态成员函数:用static修饰的类的成员函数。
为什么要有静态成员函数?原因:为安全起见,我们需要将计数变量 count 设置为private权限,这就导致无法在类外直接访问 count ,所以我们就需要在类中定义 public 方法获取 count 的大小。
问题来了,既然可以用公有方法获取 count 的值,为什么还需要静态成员函数?原因:普通函数需要用 对象.成员函数 的方式调用。如果没有创建对象,就无法调用方法。那如果既不想创建对象,还想获取 count 怎么办?将获取 count 的函数修饰为静态成员函数,原因就是因为特性<3>,如上文。静态成员可使用类名::静态成员 这样的方式调用而不需要创建对象。
代码十二:
//代码十二
#include "iostream"
using namespace std;
class Date {
public:
int _year;
static int count;
public:
Date(int year)
:_year(year)
{}
static void GetPrin() {
cout << "对象个数:" << count << endl;
}
};
int Date::count = 0;
int main() {
Date::GetPrin();//输出 对象个数:0
}
<4>静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
代码十三:在静态成员中试图打印 this 指针时报错。这意味着,静态成员函数中没有隐藏的this指针,在静态成员函数中将无法访问任何非静态的成员。
//代码十三
#include "iostream"
using namespace std;
class Date {
public:
int _year;
static int count;
public:
Date(int year)
:_year(year)
{}
static void GetPrin() {
cout << this << endl;
cout << "对象个数:" << count << endl;
}
};
int Date::count = 0;
int main() {
Date::GetPrin();
}
<5>静态成员函数不能设置为 const 成员函数,因为const成员函数本质就是修饰this指针。
<6>静态成员和类的普通成员一样,也有public、protected、private这三种访问权限,也可以有返回值。
<7>静态成员变量不能在构造函数的初始化列表中初始化。因为初始化列表只初始化对象中存储的变量,经过上面的验证,静态成员变量并没有在对象中存储,所以不可以。
<8>静态成员函数不能作为虚函数。(以后会写关于它的)
(3)总结
<1>静态成员为所有类对象所共享,是类的属性,不属于某个具体的实例。没有存储在对象中。
<2>静态成员变量必须在类外定义,定义时不添加static关键字。
<3>类静态成员即可使用类名::静态成员或者对象.静态成员来访问。
<4>静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
<5>静态成员函数不能设置为 const 成员函数,因为const成员函数本质就是修饰this指针。
<6>静态成员和类的普通成员一样,也有public、protected、private这三种访问权限,也可以有返回值。
<7>静态成员变量不能在构造函数的初始化列表中初始化。
<8>静态成员函数不能作为虚函数。