再谈构造函数
构造函数体赋值
在创建对象的时候,编译器通过调用构造函数,给对象中的各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
我们需要注意的是:虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象的初始化,**构造函数体中的语句只能将其称作为赋初值,**而不能称作初始化。因为初始化只能初始一次,而构造函数体内可以多次赋值。
初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分割的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
注意:
1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(该类没有默认构造函数)
下面我们通过代码来看一下初始化列表需要注意的地方
class A
{
public:
A(int x)
{
cout << "A(int x)" << endl;
_x = x;
}
private:
int _x;
};
class Date
{
friend class Time;
public:
//可以理解成,一个对象的单个成员变量在初始化列表时
//这个其实算是初始化列表初始化和函数体内初始化不同的地方,也是它的价值体现
//尽量使用初始化列表初始化
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
,_n(10)
,_ret(year)
, _a(1)//显示去调用_a的构造函数
{
_day = day;
//_n = 10;//不能在函数体内初始化,必须使用初始化列表初始化
//_ret = year;
}
private:
//成员变量声明
int _year;
int _month;
int _day;
//他们必须在定义的时候初始化
const int _n;
int& _ret;
A _a;
};
大家可能有些不理解为什么const成员变量和引用成员变量必须在初始化列表里面初始化,也就是定义的时候初始化。我们通过一段代码来类比一下,我想你就明白了,接下来我们再来看一段代码.
#include<iostream>
using namespace std;
int main()
{
const int n;
n = 10;
int& ret;
}
int main()
{
int a = 10;
int& b = a;//定义时初始化
const int c = 5;//定义时初始化
const int d;//定义时未初始化
}
前面学习引用的时候,我们知道引用必须在定义的时候初始化,同样的const类型的变量也必须在定义的时候初始化。
因此在类里面也一样,我们的const成员变量与引用成员变量必须在定义的时候初始化,也就是在初始化列表位置初始化。
第三个自定义类型成员(该类没有默认构造函数)也必须在初始化列表里面初始化,这个我们得再说一次,帮大家再复习一下前面的知识点
默认构造函数——不用传参就可以调用的构造函数
- 全缺省的构造函数
- 无参的构造函数
- 我们不写编译器默认生成的构造函数
尽量使用初始化列表初始化
因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化,因为初始化列表实际上就是当你实例化一个对象时,该对象成员变量定义的地方。这个其实就是初始化列表初始化和函数体内初始化不同的地方,也是它的价值体现。
严格来说
1**.对于内置类型,其实使用初始化列表和函数体内初始化时没有差别的**
Date(int year = 0, int month = 1, int day = 1)
//使用初始化列表初始化
:_year(year)
,_month(month)
,_day(day)
//在构造函数体内初始化
{
_year = year;
_month = month;
_day = day;
}
2.但是对于自定义类型来说,最好使用初始化列表初始化,因为效率更高。
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
//自定义类型,使用初始化列表 ->构造函数
Date(int day, int hour)
:_t(hour)
{}
//自定义类型,不使用初始化列表-> 构造函数 + operator=
Date(int day, int hour)
{
//函数体内初始化
Time t(hour);
_t = t;
}
private:
int _day;
Time _t;
};
可以看到对于自定义类型,在函数体内初始化比使用初始化列表初始化多调了一次赋值运算符重载,这样效率就会低一些。
在讲下一个知识点之前,我们先来看一段代码
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print()
{
cout << _a1 << "" << _a2;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
你认为这段代码的输出结果是什么呢?
答案是1和随机值
为什么会这样呢?
这也就是我们接下来要说的一个知识点:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表的先后次序无关
因此按照在类中的声明顺序,初始化的时候会先初始化a2,但是此时由于a1还未被初始化,因此就会把a1的随机值给到a2。然后轮到a1初始化,将1传给a,再把a给到a1。因此最后a1是1,而a2是随机值。所以在我们写代码的时候最好是声明次序与初始化列表的先后次序一致。
expllicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还支持隐式类型转换。
class A
{
public:
//单个参数的构造函数
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A&)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void f()
{
//...
cout << "f()" << endl;
}
private:
int _a;
};
int main()
{
//单参数的构造函数,支持隐式类型转换
A aa2 = 2;
return 0;
}
在语法上,代码中A aa2 = 2等价于下面的两句代码
A tmp(2);//先调用构造
A aa2(tmp)//再调用拷贝构造
所以在早期的编译器中,当编译器遇到A aa2 = 2这句代码时,会先构造一个临时对象,再用临时对象拷贝构造给aa2。但是现在的编译器已经做了优化,当看到A aa2 = 2这句代码时,直接调用构造函数A aa2(2),这就叫做隐式类型转换。
实际上,在我们学习C语言的时候就已经接触过隐式类型转换了,只是我们当时没有太注意罢了,下面这段代码也是隐式类型转换。
int i = 0;
double d = i;//隐式类型转换
这段代码中并不是直接把i的值给d,而是编译器会先创建一个double类型的临时变量接收a的值,再将临时变量赋值给d。
上面A aa2 = 2这段代码的可读性不是很好,如果我们想禁止单参数构造函数的隐式类型转换的话,可以用关键字explicit来修饰构造函数。
static成员
概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
特性
- 静态成员为所有类对象共享,不属于某个具体的对象,它是放在静态区的
- 静态成员变量必须在类外定义,定义时不添加static关键字
- 类静态成员即可用类名::静态成员或者对象.静态成员来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员和类的普通成员一样,也有public、protected、private这3种访问级别,也可以具有返回值
下面我们通过代码来看一下它的这些特性吧
class A
{
public:
A()
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
void f()
{
GetN();
}
static int GetN()
{
//没有this指针,所以不能访问_a
//_a = 1;//this->_a
//f(); this->f()
return _n;
}
public:
//这里只是声明,不在构造函数初始化,静态的成员变量一定要在类外全局进行初始化
//公有
static int _n;//n是存在静态区,属于整个类,也属于类中的所有对象
int _a;
};
//静态成员变量必须在类外定义,定义时不添加static关键字
//静态成员变量初始化,特例,不受访问限定符限制,否则就没办法定义初始化了
int A::_n = 0;
//类似于这里,const不能修改,但是定义的时候可以,否则没办法初始化
const int n = 10;
//n = 20;
int main()
{
A a1;
A a2;
A();
//静态成员变量不属于某个具体的对象,它是为所有类对象共享的。
//突破类域就能访问,在类外面访问的时候还要看访问限定符
cout << A::_n << endl;//public
cout << a1._n << endl;//public
cout << a2._n << endl;//public
cout << A()._n << endl;//public
return 0;
}
当上面的public变成private之后,我们就不能像上面那样突破类域直接去访问静态成员变量n了,而是得通过静态成员函数间接访问静态成员变量n.
class A
{
public:
A()
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
void f()
{
GetN();
}
static int GetN()
{
//没有this指针,所以不能访问_a
//_a = 1;//this->_a
//f(); this->f()
return _n;
}
private:
//这里只是声明,不在构造函数初始化,静态的成员变量一定要在类外全局进行初始化
static int _n;//n是存在静态区,属于整个类,也属于类中的所有对象
int _a;
};
//静态成员变量必须在类外定义,定义时不添加static关键字
//静态成员变量初始化,特例,不受访问限定符限制,否则就没办法定义初始化了
int A::_n = 0;
//类似于这里,const不能修改,但是定义的时候可以,否则没办法初始化
const int n = 10;
//n = 20;
int main()
{
A a1;
A a2;
A();
cout << a1.GetN() << endl;
cout << a2.GetN()<< endl;
cout << A().GetN() << endl;
return 0;
}
静态成员函数跟普通成员函数的区别:静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
那么问题来了:
1.静态成员函数可以调用非静态成员函数嘛?
不可以
2.非静态成员函数可以调用静态成员函数嘛?
可以的
学了上面的知识点之后我们来做一道面试题吧!!!
面试题:实现一个类,计算程序中定义了多少个类对象?
首先我们得清楚,类对象只会通过两个东西去定义,一个是构造函数,一个就是拷贝构造。因此我们只需要计算该程序中调用的构造函数与拷贝构造次数之和便能知道我们定义了多少个类对象。
//计算一个程序A中定义多少个对象出来
class A
{
public:
A()
{
cout << "A()" << endl;
++_n;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
++_n;
}
void f()
{
GetN();
}
static int GetN()
{
//没有this指针
//_a = 1;//this->_a
//f(); this->f()
return _n;
}
private:
//这里只是声明,不在构造函数初始化,静态的成员变量一定要在类外全局进行初始化
static int _n;//n是存在静态区,属于整个类,也属于类中的所有对象
int _a;
};
//静态成员变量必须在类外定义,定义时不添加static关键字
//静态成员变量初始化,特例,不受访问限定符限制,否则就没办法定义初始化了
int A::_n = 0;
void f(A a)
{
}
int main()
{
A a1;
A a2;
A();
f(a1);
cout << a1.GetN() << endl;
cout << a2.GetN() << endl;
cout << A().GetN() << endl;
return 0;
}
C++11的成员初始化新玩法
C++11支持非静态成员变量在声明时进行初始化赋值,但是需要注意的是这里不是初始化,这里是给声明的成员变量缺省值。
class B
{
public:
B(int b = 0)
:_b(b)
{}
int _b;
};
class A
{
public:
void Print()
{
cout << a << endl;
cout << b._b << endl;
cout << q << endl;
}
private:
//注意这里是声明不是定义,这里只是在非静态成员变量声明时给一个缺省值
int a = 10;
B b = 20;
int * q = (int*)malloc(4);
//静态成员变量不能给缺省值
static int n;
};
int main()
{
A a;
a.Print();
return 0;
}
这里需要再强调一下:初始化列表才是成员变量定义初始化的地方。对于成员变量来说如果你给定了值,那就用你给定的值初始化,如果未给定值就用缺省值初始化,如果也没有给缺省值,那么对于内置类型来说它就是随机值。静态成员变量是不能给缺省值的,并且静态成员变量需要在类外面初始化。
友元
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元函数
对于我们之前实现的日期类,现在我们想尝试去重载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);
ostream& operator<<(ostream& cout)
{
cout << _year << "-" << _month << "-" << _day << endl;
return cout;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 5, 31);
Date d2(2021, 4, 20);
d1<<cout;
d2<<cout;
return 0;
}
我们都知道C++的<<和>>非常的神奇,因为它们能够自动识别输入和输出的类型,我们使用的时候不必像C语言一样增加数据格式的控制。实际上,这一点也不神奇,内置类型的对象能直接使用cout和cin的输入输出,这是因为库里面已经将它们的<<和>>重载好了,<<和>>能够自动识别类型,是因为它们之间构成了函数重载,然后编译器会去调用匹配的那一个去使用。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。接下来我们就借助友元函数来重载operator<<与operator>>全局函数吧
class Date
{
public:
//友元函数
friend ostream& operator<<(ostream& out, const Date&d);
friend istream& operator>>(istream& in, Date& d);
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
d1<<cout; ->d1.operator<<(&d1,cout);
//ostream& operator<<(ostream& cout)
//{
// cout << _year << "-" << _month << "-" << _day << endl;
// return cout;
//}
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(2021, 5, 31);
Date d2(2021, 4, 20);
cout << d1 << d2;
cin >> d1 >> d2;
cout << d1 << d2;
return 0;
}
如此一来我们达到我们的目标了。但是这里需要注意的是:<<和>>运算符重载函数具有返回值是为了实现连续的输入和输出操作。
友元函数说明:
- 友元函数可以访问类的私有和保护成员,但它不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用和普通函数的调用和原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类的非公有成员。
比如说:有A和B两个类,在B类中声明了A类为其友元类,那么在A类中可以直接访问B类的私有成员变量,但是在B类中访问A类的私有成员变量则不行。
比如说:如果B是A的友元,C是B的友元,则不能说明C是A的友元。
class 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 = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
_t._hour = 1;
}
void SetTimeOfDate(int hour, int minute, int second)
{
//直接访问时间类的私有成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
内部类
概念及特性
概念:如果一个类定义在另一个类的内部,这个类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,即内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的
- 注意内部类可以直接访问外部类中的static、枚举成员、不需要外部类的对象/类名
- size(外部类)=外部类,和内部类没有关系
class A//外部类
{
public:
class B//内部类
{
public:
void f(const A& a)
{
cout << k << endl;
cout << a._a << endl;
cout << a.k << endl;
}
private:
int _b;
};
private:
static int k;
int _a;
};
int A::k = 1;
int main()
{
cout << sizeof(A) << endl;
A::B b;
b.f(A());
return 0;
}
可以看到这里外部类A的大小为4,与内部类的大小是无关的。
再次理解封装
C++是基于面向对象的程序,面向对象的三大特性即:封装、继承、多态。
C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起;通过访问限定符选择性的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。
下面通过一个例子来让大家更好的理解封装性带来的好处,比如:乘火车出行
我们来看下火车站:
售票系统:负责售票—用户凭票进入,对号入座。
工作人员:售票、咨询、安检、保全、卫生等。
火车:带用户到目的地。
火车站中所有工作人员配合起来,才能让大家坐车有条不紊的进行,不需要知道火车的构造,票务系统是如何操作的,只要能正常方便的应用即可。
但是大家想象一下,如果是没有任何管理的开放性站台呢? 火车站没有围墙,站内火车管理调度也是随意,乘车也没有规矩,比 如:
再次理解面向对象
可以看出,面向对象其实是在模拟抽象映射现实世界:
|