目录
1. 再识构造函数
1.1 构造函数体赋值
在C++入门3——类与对象(2)中,我们已经知道了构造函数的基本功能是给对象中的各个成员变量赋一个合适的初始值,这个初始化的过程是在构造函数体内部进行的:
class Date
{
public:
Date(int year, int month, int day)
{
//构造函数体内部初始化
_year = year;
_month = month;
_day = day;
}
private:
//成员变量的声明
int _year;
int _month;
int _day;
};
int main()
{
//调用Date构造函数
Date d1(2002, 7, 7);
return 0;
}
需要再度明确的是:
private下的int _year;int _month;int _day;只是对成员变量的声明,并没有开创空间,所以这些成员变量并不是在此刻定义的,既然这样,那么他们又是在何处定义的呢?
事实上,在上面代码中,成员变量在构造函数体内部的初始化其实就是定义成员变量的一种方法。
之前的成员变量都是普通类型,C++已经学到现在了,我们总归是要尝试新类型,活出不一样的人生。
现在,我要用到引用&类型和const类型,还用之前的方法定义Date构造函数:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
_ret = year;
_x = 1;
}
private:
int _year;
int _month;
int _day;
int& _ret;
const int _x;
};
我们会发现报错了:
这是为什么呢?
我们需要有清晰认知:
在调用Date构造函数之后,对象中虽然已经有了一个初始值,但是这并不是对 对象中成员变量的初始化,这只能称为赋初值,不能称为初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
在C++入门1——从C语言到C++的过渡中,我们知道,引用&和const在定义时就要进行初始化,可是这样来定义构造函数编译器又会报错,如何来解决呢?这时就要用初始化列表来解决了。
1.2 初始化列表
初始化列表:以冒号开始,以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。这便是定义成员变量的另一种方法。
class Date
{
public:
Date(int year, int month, int day)
//初始化列表初始化
:_year(year)
,_month(month)
,_day(day)
,_ret(year)
,_x(1)
{}
private:
int _year;
int _month;
int _day;
int& _ret; // 引用 : 必须在定义的时候初始化
const int _x; // const : 必须在定义的时候初始化
};
也可以混着用:
class Date
{
public:
Date(int year, int month, int day)
//初始化列表
: _ret(year)
, _x(1)
{
// 剩下3个成员没有在初始化列表显示写出来定义
// 但是他也会定义,只是内置类型默认给的随机值
// 如果是自定义类型成员会去调用它的默认构造函数
// 函数体内部初始化
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
int& _ret;
const int _x;
};
注意:
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次);
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量;
const成员变量;
自定义类型成员(且该类没有默认构造函数时)
&引用成员变量和const成员变量已经说过,接下来着重探讨一下自定义类型成员变量:
我们知道,类都会默认生成6个默认成员函数(请看类的6个默认成员函数 ) ,构造函数就是其中一个,在我们不显示定义构造函数时,编译器会生成一个默认的构造函数供我们使用,并且是自动定义自动调用,这为我们提供了许多方便。
可是一些类,如顺序表、链表、栈和队列等数据结构类,它们的默认构造函数往往并不符合我们的要求,我们当然需要自己定义构造函数,如下:
#include <iostream>
using namespace std;
typedef int DataType;
class SeqList//SeqList类
{
public:
SeqList()
{
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (_a == nullptr)
{
perror("malloc failed");//如果扩容失败,说明原因
exit(-1);//直接退出
}
_size = 0;//当size≥capacity时就动态开辟空间
_capacity = 4;//初始化数组容量为4
}
//打印
void Print()
{
for (int i = 0; i < _size; i++)
{
cout << _a[i] << endl;
}
}
private:
int* _a;
int _size;
int _capacity;
};
class MySL
{
public:
MySL()
:_sl()
{}
private:
SeqList _sl;//自定义类型成员,且该类没有默认构造函数
};
所以,自定义类型成员(且该类没有默认构造函数)在初始化时也需要用初始化列表初始化。
思考如下代码的运行结果:
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();
}
运行结果:
首先把结论抛出来:
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关!
成员变量声明时,先声明的是a2,其次是a1,所以在初始化时,先初始化a2,再初始化a1
在调用函数时把a传递给了a1, 而在传值时,先把a1传递给a2,此时a1是随机值,故a2也是随机值,然后再把a传递给a1,a1就是1。
总结:
初始化列表解决的问题:
1. 必须在定义的地方显示初始化 ①引用 ②const ③没有默认构造自定义成员;
2. 有些自定义成员想要显示初始化,自己控制;
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化;
4. 刚说完尽量使用初始化列表初始化,那么以后我们都用初始化列表,摒弃函数体内部初始化可以吗?
答案是不能,因为有些初始化或者检查的工作,初始化列表也不能全部搞定,就像上面的SeqList类的检查扩容,初始化列表就不能完成工作。
因此我们对初始化列表应该抱有能用尽用的原则,对于实在不能用初始化列表的,应该使用函数体内部初识化。
5. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
1.3 构造函数的类型转换与explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用。
class Date
{
public:
Date(int year)
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
};
函数调用时:
int main()
{
Date d1(2002);
Date d2 = 2003;//发生了隐式类型转换,内置类型int转换为自定义类型
d1.Print();
d2.Print();
return 0;
}
代码正常编译通过,那么Date d2=2003;为什么能够编译通过呢?
因为这里发生了隐式类型转换,能支持这里的类型转换是因为Date构造函数的单参数类型为int(如果是多参数构造函数,第一个参数未缺省的半缺省函数或全缺省函数也支持隐式类型转换)。
也就是说,这里先产生了一个临时变量 tmp,tmp调用构造函数将其初始化为2003,再调用拷贝构造将tmp拷贝给d2。
为了更加了解此过程的临时变量,我们先拿已经很熟悉的int类型转double类型举例:
int a = 3;
double b = a;
我们知道,&引用是对相同类型变量起别名,现在,既然int与double能够相互转换,我就用double类型对int类型的a取别名:
int a = 3;
double b = a;
double& b1 = a;
这里为什么又不行了呢?
其实这里报错并不是因为类型不同,是因为b1并不是对a取别名, 而是对产生的临时变量取别名,临时变量具有常属性,所以需要在前面加一个const:
int a = 3;
double b = a;
const double& b1 = a;
既然存在隐式类型转换,那么我们有没有方法阻止隐式类型转换呢?
这时explicit关键字的作用就来了:如果我们不想发生隐式类型转换,就在构造函数的前面加explicit:
class Date
{
public:
explicit Date(int year)
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
};
思考如下代码的运行结果:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year,int month = 2,int day = 2)
:_year(year)
,_month(month)
,_day(day)
{}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2002, 7, 7);
Date d2 = (2003, 7, 7);
d1.Print();
d2.Print();
return 0;
}
答案是否出乎你的预料呢?
这是因为这里的Date d2 = (2003, 7, 7);括号里其实是逗号表达式(如果忘记了逗号表达式,可以看初始C语言5——操作符详解),等价于Date d2=7;
2. static成员
2.1 static成员的引入
了解static成员之前,先做一道题:
实现一个类,计算程序中创建出了多少个类对象。
解析:首先应该明确都在什么情况下会创建类对象:调用构造函数时会创建类对象,如果程序中遇到传值,会调用拷贝构造函数,这时也会创建类对象,所以如果设计数器,在构造函数和拷贝构造函数里面都需要计数。
解法1:使用全局的count计数器
#include <iostream>
using namespace std;
namespace xxk
{
int count = 0;//count与C++库函数重名,避免重名,放到命名空间内
}
class A
{
public:
A() { ++xxk::count; }
A(const A& t) { ++xxk::count; }
~A() {}
private:
};
A func()
{
A a;
return a;
}
int main()
{
A a1;
func();
cout << xxk::count << endl;
return 0;
}
解法1可行,可是要知道,我现在计算的是A对象,如果程序里还有一个B对象,那么最后得到的count值就是A和B两个对象的类对象数量了呀!所以把count设为全局变量并不是很靠谱。
解法2:使用成员变量的count计数器
#include <iostream>
using namespace std;
class A
{
public:
A() { ++count; }
A(const A& t) { ++count; }
~A() {}
private:
int count=0;
};
A func()
{
A a;
return a;
}
int main()
{
A a1;
func();
cout << count << endl;
return 0;
}
结果显示这样的程序根本跑不动:成员变量count只在本作用域起作用,也就是说,A类的每个对象都会有一个count,a1对象的count只属于a1,a2对象的count只属于a2......
解法1存在弊端,解法2又是bug,那现在问题的最优解是什么呢?
2.2 static成员的用法及特性
这时就要用到static成员了:用static修饰的成员变量属于这个类的全部对象
概念:声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用 static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。(类内声明,类外定义)
最优解:
#include <iostream>
using namespace std;
class A
{
public:
A() { ++count; }
A(const A& t) { ++count; }
~A() {}
//private:
static int count ;
};
int A::count = 0;
A func()
{
A a;
return a;
}
int main()
{
A a1;
func();
cout << A::count << endl;
return 0;
}
上面的成员变量count设为公有,因为类外无法访问类的私有成员变量。
如若想将其设为私有又想正常访问,可以借鉴日期类获取大小月,润平月的接口函数一样,定义一个静态成员函数:
故真正的最优解为:
#include <iostream>
using namespace std;
class A
{
public:
A() { ++count; }
A(const A& t) { ++count; }
~A() {}
//设置为静态成员函数,没有了this指针
//只能在类外调用,不会传参,更不会修改count
static int Acount()
{
return count;
}
private:
static int count;
};
int A::count = 0;
A func()
{
A a;
return a;
}
int main()
{
A a1;
func();
cout << a1.Acount()<< endl;
//如果未定义a1,为了调用Acount函数,不得不定义一个A变量
A a;
cout << a.Acount()-1 << endl;//为调用而定义,所以需要-1
return 0;
}
总结:
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明;
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员来访问;
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制 。
3. 友元
上面讲static的用法时,我们发现如果count被private访问限定符修饰,类外就无法访问count,为了能够正常访问count,我们提供了两种方法,一种时将其用public访问限定符修饰,一种是在类内定义一个接口函数,通过调用函数来实现类内成员的访问。
那么有没有方法,能使成员变量既可以不用public访问限定符修饰,也可以不用定义接口函数呢?
为了解决这类问题,友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类。
3.1 友元函数
在C++入门之 ostream和istream详解中,我们已经尝试了对operator<<和operator>>的重载
因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
代码解释如下:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
因此应该将其重载为全局函数:
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。
class Date
{
//友元函数声明
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public :
Date(int year = 2, int month = 2 , int day = 2)
:_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;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
注意:
友元函数可访问类的私有和保护成员,但不是类的成员函数;
友元函数不能用const修饰;
友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
一个函数可以是多个类的友元函数;
友元函数的调用与普通函数的调用原理相同。
3.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
1.友元关系是单向的,不具有交换性:
比如下面的Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接;
访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2.友元关系不能传递:
如果C是B的友元, B是A的友元,则不能说明C时A的友元;
3.友元关系不能继承。
友元类与友元函数用法相似:
class Time
{
// 声明Date类为Time类的友元类,
// 则在Date类中就直接访问Time类中的私有成员变量
friend class Date;
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)
{
// 直接访问Time类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
4. 内部类
概念:如果一个类定义在另一个类的内部,这个内部的类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访 问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的;
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名;
3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
static int k;
int h = 0;
public:
class B // B天生就是A的友元
{
public:
void func(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;//访问内部类需要用作用域限定符::
A a1;
b.func(a1);
return 0;
}
5. 匿名对象
所谓匿名对象,顾名思义,就是无名的对象,也可以理解为存在却销声匿迹的对象;与之对应的是有名对象,本篇博客到此之上定义的类对象都是有名对象。
那么匿名对象要怎么定义,有哪些用途以及特性呢?
匿名对象的特点是不用取名字,生命周期只在当前行。
我们在讲解static成员时,计算一个类存在多少类对象的题中,最优解的代码中有这样一行:
在这里,我们为了调用Acount函数,不得不再定义一个a对象然后再调用Acount函数,可因为这里新定义了一个对象,所以又需要令结果-1;那么有没有方法可以不用让结果-1呢?
解决这个问题,我们就可以创建一个匿名对象:
#include <iostream>
using namespace std;
class A
{
public:
A()
{++count;}
A(const A& t) { ++count; }
~A()
{}
static int Acount()
{
return count;
}
private:
static int count;
};
int A::count = 0;
A func()
{
A a;
return a;
}
int main()
{
A a1;
func();
cout << a1.Acount()<< endl;
//如果未定义a1,为了调用Acount函数,定义一个匿名变量
//A aa1();
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
//A();
// 但是我们可以这样定义匿名对象,匿名对象的特点不用取名字
cout << A().Acount() - 1<< endl;//为调用而定义,所以需要-1
return 0;
}
(本篇完)