构造函数体赋值
创建一个类对象时,编译器通过调用构造函数,给类对象中各个成员变量赋初值:
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
{
_year = year;
_month = month;
_day = day;
_year = 2023;//构造函数体内允许对成员变量进行多次赋值
}
private:
int _year;
int _month;
int _day;
};
用初始化列表初始化对象
1.初始化列表用法
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括 号中的初始值或表达式。
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
:_year(year) //初始化列表初始化
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
2.初始化列表特性
(1)初始化列表能只能初始化一次,多次初始化会报错:
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
:_year(year)
,_month(month)
,_day(day)
,_month(month) //err: 初始化列表多次初始化
{}
private:
int _year;
int _month;
int _day;
};
编译器也允许构造函数赋初值和初始化列表初始化混用:
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
:_year(year) //两者混用
,_month(month)
{
_day = day;
}
private:
int _year;
int _month;
int _day;
};
混用时初始化列表初始化和构造函数赋初值不冲突:
但是是初始化优先,因为赋初值相当于是给的缺省值,别人初始化了就初始化,没有就缺省值。
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
: _year(year) //两者不冲突
, _month(month)
{
_day = day;
_year = 2023;
}
private:
int _year;
int _month;
int _day;
};
但混用时初始化列表初始化还是要遵循只能初始化一次成员变量的原则:
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
: _year(year) //初始化列表初始化
, _month(month)
, _year(2023) //_year在初始化列表里被初始化了两次,不允许
{
_day = day;
}
private:
int _year;
int _month;
int _day;
};
(2)const成员变量、引用成员变量、没有默认构造函数的自定义类型成员只能在初始化列表初始化。
①const成员变量必须在定义的时候初始化
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
: _year(year)
, _month(month)
, _n(2) //const成员变量必须使用初始化列表进行初始化
{
_day = day;
//_n = 2; //const成员变量不能在函数体内初始化
}
private:
int _year;
int _month;
int _day;
const int _n = 1;
};
②引用成员变量必须在定义的时候初始化
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
: _year(year)
, _month(month)
,_ref(year)//引用成员变量要在初始化列表初始化
{
_day = day;
//_ref = year; //引用成员变量不能在函数体内初始化
}
private:
int _year;
int _month;
int _day;
int& _ref;
};
③没有默认构造函数的自定义类型成员变量
#include <iostream>
using namespace std;
class A
{
public:
//默认构造函数是不用传参就可以调用的构造函数,有3种:
//1.无参默认构造函数
//2.带参全缺省的默认构造函数
//3.我们不写,编译器自动生成的默认构造函数
A(int x)//不属于以上任何一种,所以A类的对象没有默认构造函数
{
cout << "A(int x)" << endl;
_x = x;
}
private:
int _x;
};
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
: _year(year)
, _month(month)
, _a(20)//没有默认构造函数的自定义类型成员变量必须在初始化列表进行初始化
{
_day = day;
}
private:
int _year;
int _month;
int _day;
A _a;
};
const成员变量、引用成员变量、没有默认构造函数的自定义类型成员变量必须在初始化列表内初始化的原因:
①初始化列表是对象的成员变量定义的地方。
②对象的内置类型成员变量在初始化列表定义时没有要求必须初始化,因此既可以在初始化列表进行初始化,也可以在构造函数体内初始化。
③而const成员变量、引用成员变量、没有默认构造函数的自定义类型成员变量不能先定义再初始化,它们在初始化列表内定义,并且必须在定义时就初始化,因此必须在初始化列表内初始化。
(3) 尽量使用初始化列表初始化,因为不管是否使用初始化列表,虽然对于内置类型没有差别,但是对于自定义类型成员变量,一定会先使用初始化列表初始化。
为什么会先使用初始化列表初始化?
如下,Date类没有默认构造函数,因为22行的构造函数不属于默认构造函数中的任意一种,在对Date类的对象d进行初始化时,会调用Date类的默认构造函数,所以对象d的day实参12和hour实参12都没有被传进去,_t作为Date类的自定义类型成员变量会调用Time类的默认构造函数,_hour默认传参为0,因此打印_hour的值也为0,d的参数没有传成功:
#include<iostream>
using namespace std;
class Date; // 前置声明
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0)
: _hour(hour)
{
cout << _hour << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int day, int hour)
{}
private:
int _day;
Time _t;
};
int main()
{
Date d(12, 12);
return 0;
}
假如Date类的构造函数不使用初始化列表进行初始化,使用函数体内初始化时,要把Date类的构造函数的形参hour的值给d,那么就必须构造一个Time类对象t,对该对象传参传hour,再使用赋值运算符重载函数将对象t拷贝给_t:
#include<iostream>
using namespace std;
class Date; // 前置声明
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0)
: _hour(hour)
{
cout << _hour << endl;
}
private:
int _hour;
};
class Date
{
public:
//自定义类型,不使用初始化列表,就需要使用构造函数 + operator=
Date(int day, int hour)
{
//函数体内初始化
Time t(hour);//调用Time类的构造函数
_t = t;
_day = day;
}
private:
int _day;
Time _t;
};
int main()
{
Date d(12, 12);
cout << 4 << endl;
return 0;
}
这还不如直接使用使用初始化列表初始化呢,还不需要赋值运算符重载函数:
class Date
{
public:
//自定义类型,使用初始化列表,只需要构造函数
Date(int day, int hour)
:_t(hour)
{
_day = day;
}
private:
int _day;
Time _t;
};
因此,建议尽量直接使用初始化列表进行初始化。
(4)成员变量初始化的顺序就是成员变量在类中的声明次序,与初始化列表中的先后次序无关。
如下代码,类成员变量中先声明了_a2,再声明了_a1,因此初始化的顺序是先初始化_a2,再初始化_a1:
#include <iostream>
using namespace std;
class A
{
public:
A(int a)
: _a1(a)
, _a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;//先声明_a2
int _a1;//后声明_a1
};
int main() {
A aa(1);
aa.Print();
}
先声明_a2就会先初始化_a2,用_a1初始化_a2,由于此时_a1还是随机值,因此_a2的值也是随机值,_a1使用a的值1进行初始化,因此,_a1的值为1。
所以,建议类中的成员变量声明的顺序和初始化列表中初始化的顺序一致。
二、explicit关键字
1.内置类型的隐式转换
int i = 0;
double d = i;//隐式类型转换
double d = i;并不是将i直接赋值给d,而是用i创建一个临时变量,再把临时变量的值给d,那么d改变的是临时变量的值,而不是i的值,因为程序执行完毕后,i的值并未发生改变。
int i = 0;
const double& d = i;//d引用了临时变量,临时变量具有常性,所以d也必须具有常性
如果d作为引用,那么必须加上const关键字进行修饰,因为d不是i的引用,是临时变量的引用,而临时变量具有常性,不允许引用权限放大。
2.如何避免单参构造函数初始化发生隐式类型转换
正常的类对象初始化如下面的aa1,也可以使用拷贝构造初始化,如aa2。由于c++支持隐式类型转换,因此也支持单参数构造函数初始化,如aa3:
#include<iostream>
using namespace std;
class A
{
public :
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
A aa1(1);//构造aa1对象
A aa2(aa1);//拷贝构造,程序没写拷贝构造,编译器会自动生成拷贝构造函数,对内置类型完成浅拷贝
A aa3 = 3;//单参数的构造函数,会发生隐式类型转换
return 0;
}
那么是如何转换的捏?
A aa3 = 3;
对于自定义类型A,aa3是A类型,3是整形。编译器会先拿A构造一个临时对象temp,3作为参数传给这个临时对象temp,再拿aa3(temp)去拷贝构造,发生隐式类型转换,即先构造,再拷贝构造:
//A aa3 = 3;
A temp(3); //先构造
A aa3(temp); //再拷贝构造
不过现在的编译器已经优化过了,会直接调用构造函数A aa(3)。
如果不想让单参数的构造函数发生隐式类型转换,可以使用explicit关键字修饰构造函数,表明该构造函数是显式的,而不是隐式的,就会避免发生不期望的类型转换,使用场景如下:
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
A aa1(1);//构造aa1对象
A aa2(aa1);//拷贝构造,程序没写拷贝构造,编译器会自动生成拷贝构造函数,对内置类型完成浅拷贝
A aa3 = 'x';//先拿A构造一个临时对象temp,字符x作为参数传给这个临时对象temp,会发生隐式类型转换,再拿aa3(temp)去拷贝构造
return 0;
}
aa3作为A类的对象,构造时传参应该传int型,但却传了char型,由于发生隐式类型转换,因此编译也没毛病,但是它传参就是不伦不类。这时候可以给A的构造函数加上explicit声明不让该单参构造函数发生隐式类型转换,编译就会报错:
class A
{
public:
explicit A(int a)
:_a(a)
{}
private:
int _a;
};
这时候唯一解决办法就是乖乖给aa3传int型参数了。
三、匿名对象
1.匿名对象定义
没有名字的对象叫做匿名对象,A(3)跟aa1和aa2相比少了个对象名,没有名字,aa1和aa2的生命周期在main函数内,A(3)的生命周期只在当前行:
#include<iostream>
using namespace std;
class A
{
public:
explicit A(int a)
:_a(a)
{
cout << "A(int a):"<< a << endl;
}
A(const A& aa)
{
cout << "A(const A&)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);//生命周期在main函数内
A aa2(aa1);//生命周期在main函数内
A(3);//构造匿名对象,生命周期只在这一行
return 0;
}
F10调试:当执行完A(3)还没执行return 0时,aa1和aa2的生命周期还没有结束,不会调用析构函数,此时打印的析构函数只能是匿名对象A(3)的析构函数:
所以A(3)这一行执行完就调析构函数了。
2.匿名对象应用场景
假设有一个函数f,且A类的构造函数全缺省:
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)//构造函数全缺省
:_a(a)
{
cout << "A(int a):"<< a << endl;
}
A(const A& aa)
{
cout << "A(const A&)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void f()//f函数
{
cout << "f()" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);//生命周期在main函数内
A aa2(aa1);//生命周期在main函数内
A(3);//构造匿名对象,生命周期只在这一行
return 0;
}
调用f()函数时,需要定义一个A类对象,才能调用A类函数f,这就需要写两行:
int main()
{
A aa1(1);//生命周期在main函数内
A aa2(aa1);//生命周期在main函数内
A(3);//构造匿名对象,生命周期只在这一行
A aa4;//需要定义一个A类对象,才能调用f
aa4.f();
return 0;
}
对象aa4 在main函数结束后才会销毁。如果定义对象只是为了调用函数,那么可以考虑直接定义一个匿名对象:
int main()
{
A aa1(1);//生命周期在main函数内
A aa2(aa1);//生命周期在main函数内
A(3);//构造匿名对象,生命周期只在这一行
A aa4;//需要定义一个A类对象,才能调用f
aa4.f();
A().f();//定义匿名对象来调用函数f()
return 0;
}
这个匿名对象就是为了调用函数f,这个匿名对象后边也没人用它,在当前行调用完f()函数就销毁了。
Static成员
概念:
声明为static的类成员叫做类的静态成员,分为两种:
(1)静态成员变量:用static修饰的成员变量
(2)静态成员函数:用static修饰的成员函数
一、静态成员变量
如何计算A定义了多少个对象?
#include<iostream>
using namespace std;
class A
{
private:
static int _n;//仅仅只是声明,_n存在于静态区,属于整个类,也属于类的所有对象
};
int main()
{
A a1;
A a2;
A();
return 0;
}
所有对象要么是构造出来的,要么是拷贝构造出来的,因此要声明一个全局的成员变量,生命周期也必须是全局的,那么生命周期被改变了,这个成员变量就需要用static进行修饰,即静态成员变量,static int _n;
1.静态成员变量必须在类外定义和初始化的原因
那么如何对_n进行定义初始化呢?如果在类内初始化,会导致每个对象都包含该静态成员。静态成员变量必须在类外定义和初始化的原因:
(1)声明不分配内存,定义会分配内存,如果在类内定义静态成员变量,那么每个对象初始化时都要为静态成员变量分配一块空间,会造成重复定义。
(2)静态成员和类处于同一级别,普通成员和对象处于同一级别。 类级别的成员,应先于类对象的存在而存在,且静态成员变量应被所有类对象共享,所以静态成员变量不能放在类内当对象初始化时才初始化。
如何在类外定义和初始化?虽然是私有静态成员变量,但是不受访问限定符限制:
#include<iostream>
using namespace std;
class A
{
public:
A()//构造函数
{
++_n;
}
A(const A& a)//拷贝构造函数
{
++_n;
}
private:
static int _n;//n存在于静态区,属于整个类,不属于某个对象
};
//静态成员变量不受访问限定符限制,否则就没办法初始化了;声明时已经加了static,这里不需要再加static
int A::_n = 0;//在全局指定类域为A,并把值初始化为1
int main()
{
A a1;
A a2;
A();
return 0;
}
现在只需要获取_n的值就可以了,它是私有的,我们获取不了。
2.如何在类外面访问静态成员变量
有两种方式可以在类外面访问静态成员变量:
(1)修改静态成员变量的访问限定符,注掉private,就可以在类外获取_n的值:
①类名::静态成员变量
②对象.静态成员变量
#include<iostream>
using namespace std;
class A
{
public:
A()//构造函数
{
++_n;
}
A(const A& a)//拷贝构造函数
{
++_n;
}
//private:
static int _n;//n存在于静态区,属于整个类,不属于某个对象
};
int A::_n = 0;
int main()
{
A a1;
A a2;
A();
//这三行访问的都是全局的_n,因此打印结果都一样,都是3
cout << A::_n << endl;//类名::静态成员变量
cout << a1._n << endl;//对象.静态成员变量
cout << a2._n << endl;//对象.静态成员变量
}
(2)在类外面访问要看访问限定符:如果这个静态成员变量_n就是私有的,在类外面无法访问,需要定义一个公有的成员函数访问静态成员变量,通过对象调用成员函数获取_n的值:
#include<iostream>
using namespace std;
class A
{
public:
A()//构造函数
{
++_n;
}
A(const A& a)//拷贝构造函数
{
++_n;
}
//获取静态成员变量的值
int getN()
{
return _n;
}
private:
static int _n;//n存在于静态区,属于整个类,不属于某个对象
};
int A::_n = 0;
int main()
{
A a1;
A a2;
//这两行访问的都是全局的_n,因此打印结果都一样,都是3
cout << A().getN() << endl;
cout << a1.getN() << endl;
cout << a2.getN() << endl;
}
二、静态成员函数
静态成员函数跟普通函数区别:没有this指针,不能访问非静态成员。能访问静态成员是由于静态成员属于整个类,只要突破类域就能访问,突破类域有两种方式:
(1)对象可以帮助静态成员突破类域
(2)指定静态成员的类
因此一般定义一个静态成员函数来访问静态成员变量。
#include<iostream>
using namespace std;
class A
{
public:
A()//构造函数
{
++_n;
}
A(const A& a)//拷贝构造函数
{
++_n;
}
//静态成员函数,没有this指针,不能访问非静态成员
static int GetN()
{
//_a = 1;//不能访问非静态成员
return _n;
}
private:
static int _n;//n存在于静态区,属于整个类,不属于某个对象
int _a;
};
int A::_n = 0;
int main()
{
A a1;
A a2;
//这三行访问的都是全局的_n,因此打印结果都一样
cout << A().GetN() << endl;//匿名对象可以帮助静态成员突破类域
cout << a1.GetN() << endl;//对象可以帮助静态成员突破类域
cout << A::GetN() << endl;//指定静态成员的类,让编译器到类里面去找
}
三、static成员特性
1. 静态成员为所有类对象所共享,不属于某个具体的对象实例
2. 静态成员变量必须在类外定义,定义时不添加static关键字
3. 类静态成员可用类名::静态成员来访问,也可以用对象.静态成员来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以有返回值6. 普通成员只能通过对象访问,不能通过类名访问。
7. 静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)