构造函数新理解
构造函数函数体赋值
根据前面讲的构造函数,在创建对象的时候,编译器可以通过调用构造函数,通过函数内的语句给对象中的各个成员变量一个合适的初始值。
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、如果一个类中包含以下的变量,必须在初始化列表的位置进行初始化:
(1) 引用成员变量。这里的引用变量是声明,而不是定义,所以不会报错(引用必须引用已经定义的变量,这里当作声明编译器不会报错,在用类定义对象之后直接引用)。
(2) const成员变量。const变量只能初始化一次,所以必须在初始化列表中初始化。
(3) 自定义类型成员,这里的自定义类型成员必须是没有默认的构造函数,如果有默认构造函数,编译器会先去调用它的默认构造函数,不会调用自定义列表。
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
: _obj(a)
, _ref(ref)
, _n(10)
{}
private:
A _obj;//没有默认构造函数
int &_ref;//引用变量
const int _n;//const变量
};
3、建议尽量使用初始化列表初始化,对于内置类型,使用函数体赋值和初始化列表赋值没有差别;但是对于自定义类型,建议使用初始化列表。
4、成员变量在类中声明次序就是其在初始化列表中的初始化顺序,和它在初始化列表中的先后顺序无关。
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();
}
来看上面这段代码会输出什么?
可以看出_a1 = 1,_a2 = 随机值
这是因为在声明的时候_a2在前面,_a1在后面,因此在初始化列表中,先把_a2初始化为_a1,此时_a1为随机值赋值给_a2;然后把_a1赋值为1。
explicit关键字
构造函数对于单个参数的构造函数,具有隐式类型转换的作用。
class Date
{
public:
Date(int year)
:_year(year)
{}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1(2018);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2019;
}
从代码中的 d1 = 2019,对于在main函数中的" = "应该属于默认的赋值运算符重载,此时赋值的类型应当也是日期类,但是此时却赋值了一个2019,并且能够成功运行。
这是因为对于 d1 = 2019在语法意义上,是先构造,在拷贝构造。
在老的编译器上,这个表达式会先构造出一个临时对象A tmp(2019),然后再对tmp进行一个拷贝构造A d1(tmp),这样子这个表达式才能成功运行。
在新的编译器上,是直接调用构造函数,A d1(2019)。
但是这样子代码的可读性不够好,可以用explicit修饰构造函数,禁止对单参数的构造函数产生隐式类型转换。
explicit Date(int year)
:_year(year)
{}
static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量称为静态成员变量;用static修饰的成员函数,称为静态成员函数。
注意:静态的成员变量一定要在类外进行初始化。
特性
(1) 静态成员为所有类对象所共享,不属于某个具体的实例。
class A
{
public:
A()
{
++_count;
}
private:
static int _count;
int a;
int b;
};
int A::_count = 0;//静态变量初始化
int main()
{
A a1;
cout << sizeof(a1) << endl;
return 0;
}
在A类中,我们定义了三个变量,一个是静态变量_count,另外还有两个int变量,那么通过计算A类的大小发现,A类的大小为8,也就是两个int的大小,并没有把_count计算在内。因此可以证明静态成员变量并不属于某一个具体的对象。
(2) 静态成员变量必须在类外初始化,定义时不添加static关键字。
如果初始化的时候不加变量的类型则会报错,因此在对static成员变量初始化的时候,必须加上成员类型,不需要加static,因为在声明的时候已经告诉编译器这是一个静态变量。
在类中的是static变量的声明,而初始化则是在类外。如果static变量是公有的,那么在任何时候都可以访问;如果static是私有的,那么只会在初始化的时候访问一次,其他时候不能访问。
(3) 访问静态成员变量有两种方式:
类名::静态成员
对象.静态成员
(4) 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。因为静态成员函数是存在静态区的,不属于任何对象,自然也不会有this指针。
class A
{
public:
A()
{
++_count;
}
static int GetCount()
{
return _count;
}
private:
static int _count;
int a;
int b;
};
_count静态成员变量是私有的,那么想要访问就必须用一个公有的函数来进行访问。
如果在静态函数中想要访问非静态成员,编译器就会报错,静态函数中没有对象,而非静态成员的访问必须是有特定对象的。
static成员有两个重要的点:
(1) 静态成员不可以调用非静态成员函数。因为非静态成员函数是与特定对象相对的,静态成员函数没有this指针,是无法调用非静态成员函数,这属于权限的放大。
(2) 非静态成员可以调用静态成员函数。因为静态成员函数是属于所有对象的,那么任何一个对象都可以调用。这属于权限的缩小。
C++11的成员初始化
在C++11中,支持给非静态成员变量在声明时进行初始化赋值,但是要注意,这里并不是初始化,依旧是声明,相当于是在声明的时候给成员变量一个缺省值。
class A
{
public:
A()
{
++_count;
}
int sum()
{
GetCount();
return 0;
}
static int GetCount()
{
return _count;
}
private:
int a = 10;
int b = 20;
static int _count;
};
也就是说,如果成员变量在初始化列表中,就用初始化列表的值初始化,如果不在初始化列表中,就用这个缺省值来进行初始化。
注意:静态成员变量不支持初始化赋值
友元
友元分为友元函数和友元类。
友元函数
尝试在日期类中重载“<<”运算符
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
然后在主函数中尝试正常使用“<<”运算符
发现编译器会报错,因为在重载的成员函数里面,this指针在第一个参数的位置,也就是左操作数,而cout是右操作数,只有将上面的式子中左右操作数颠倒一下才可以正常使用。
但是这就不符合cout的使用习惯,也与C++的语法不符合,要想解决这个问题,就必须把这个成员函数写在全局,也就是类的外面。
写在外面是可以解决上述问题,但是又带来了新的问题,类的成员变量是私有的,在类外是不能访问的。那么这里就需要友元来解决了。
友元函数是定义在类外的全局函数,可以直接访问类的私有成员。友元函数需要在类的内部声明,声明时需要加friend关键字。
class Date
{
//友元函数声明
friend ostream& operator<<(Date d, ostream& _cout);
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(Date d, ostream& _cout)
{
_cout << d._year << "-" << d._month << "-" << d._day << endl;
return _cout;//有返回值是为了能够连续输出
}
注意:
(1) 友元函数能够访问类的私有成员和保护成员,但是它本身不属于类的成员函数。
(2) 友元函数不能用const修饰。
(3) 一个函数可以是多个类的友元函数。
友元类
友元类本质上和友元函数的思想是一样的,就是在另一个类中可以访问声明友元类的成员变量或函数。
在一个类中声明友元类和声明友元函数是一样的,在声明前面加上friend(如果声明的友元类定义在该类的下方,要加上前置声明)。
class Date;//前置声明
class 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;
};
如上代码,就可以在日期类中直接访问时间类的私有变量。
对于友元类,有这样几个特性:
1、友元类的所有成员函数可以是另一个类的友元函数,都可以访问另一个类的非公有成员。
2、友元关系是单向的,不具有交换性。就是说日期类是时间类的友元,日期类可以访问时间类的非公有成员;但是在时间类中并不能访问日期类的非公有成员。
3、友元关系不具有传递性。比如:B是A的友元,C是B的友元,并不能认为C是A的友元。
内部类
概念:如果一个类定义在另一个类的内部,定义在一个类的内部的类称为内部类,内部类外部的类称为外部类。
class A
{
public:
class B
{
public:
void func(const A& a)
{
cout << _k << endl;//内部类访问外部类的静态成员是可以的
couot << a.b << endl;//访问A的非公有成员也是可以的
}
};
private:
static int _k;
int b;
};
内部类是外部类的友元类,可以通过外部类的对象参数访问外部类中的所有成员;但是要注意的是外部类并不是内部类的友元,内部类是一个独立的类,不属于外部类。也就是说,外部类不能访问内部类中的成员,外部类没有任何访问内部类的权限。
内部类特性:
1、内部类可以是public、private、protect成员
2、内部类可以直接访问外部类中的static成员、枚举成员,不需要加外部类的对象名。
3、sizeof(外部类) = 外部类,与内部类没有关系,上面讲了,内部类是独立的,不属于外部类。所以计算外部类的大小的时候与内部类无关。