关于类和对象依旧有许多难点,这篇博客将会讲解关于类的构造函数的初始化列表,静态成员,友元,内部类,以及匿名对象等一些比较复杂的东西。
初始化列表
我们之前就已经学过类和对象的构造函数,但是实际上那并不算是对象的初始化,其实算是给成员变量赋值。
而我们用c语言和c++时都知道,变量初始化只能初始化一次,而赋值可以在函数内赋值无数次。
既然说构造函数只能算是给成员变量赋值,那么怎样才能证明?
根据之前学的知识,我们都知道用const修饰的对象都只能在初始化的时候确定值;
那么我们用默认构造函数初始化const成员变量不就可以证明了吗?
我们直接看看:
发现用默认构造函数实际上不能初始化用const修饰的变量。
这就变相的证明了默认构造函数内部实际上不是初始化,而是赋值。
那么类的对象在哪里才算是初始化呢?
这就轮到初始化列表出场了。
不过在了解初始化列表之前需要先了解下初始化列表的使用场景。
初始化列表使用场景:
1.用来初始化const成员变量
2.用来初始化无默认构造函数的自定义类型成员变量
3.用来初始化引用成员变量
所谓初始化列表,实际上就是在构造函数下,以一个冒号为开始,成员之间以逗号隔开的列表,每一个成员变量后面用括号跟上赋的值。
初始化列表的使用方式:
我们可以看到,确实是成功的初始化了成员变量。
而上面也说了,初始化列表不仅能够初始化const成员变量;
也可以修饰其它两种变量,其中,自定义类型的变量需要好好了解一下。
class B {
public :
B(int _b)
{
b = _b;
}
private:
int b;
};
class A {
public:
A()
:a(10)
,_b(5)
{
}
private:
const int a;
B _b;
};
int main()
{
A _a;
return 0;
}
通过这里我们可以发现,实际上初始化列表在初始化自定义类型的变量的时候;
会调用对应的构造函数,并且将括号里的数据用来初始化成员变量,而若是初始化列表没有显示初始化自定义类型的成员变量时,就会调用默认构造函数,无则报错;
此外,还有引用的成员变量需要使用初始化列表才能用。
这里我们可以看到,成功的初始化了rc这个引用类型的成员变量。
初始化列表的规则
对象的所有成员都会走一套初始化列表,面对自定义类型的变量,若是初始化列表没有显示初始化就会调用对应的默认构造函数,无则报错,而面对内置类型,有显示初始化就用显示的值,无则用随机值或者构造函数内部的数值。
此外,还有一个规则
初始化列表的初始化顺序是根据成员变量的声明顺序决定的。
这样我们发现,成员变量的声明顺序是 先a2后a1,而初始化列表则是先a1后a2,这就说明,初始化列表的初始化顺序由变量的声明顺序决定。
explicit 关键字
之前我们写过Date类,而Date类中的构造函数其实还有其他用处。
比如有一个构造函数只有一个参数,或者说只有第一个参数是没有缺省值的时候,会出现隐式类型转换。
我们先来看看代码。
我们发现,Date类型的对象d1居然能够直接用int类型的常量来初始化。
实际上这里涉及到类的隐式类型转换。
首先编译器会将用构造函数创建一个Date类型的中间变量,再用拷贝构造将中间变量的值给d1。
当然,实际上这只是便于理解的说法,现在的编译器都对这个过程进行了优化。
过程变成了直接用构造函数来将2022作为参数来构造d1。
下图可证:
这样实际上用处并不大,因为大部分的类的成员变量都不只有一个,比如Date类就有三个成员变量。
当然,我们也可以这样初始化一个变量,不过格式需要注意,如下:
就好像是初始化数组一样,这样也是可以的,但是这样会影响可读性,因此c++针对这个出现了一个关键字——explicit。
将explicit关键字放于构造函数之前,就可以禁止这样的隐式类型转换。
我们可以看到,对象实例化的地方出现了报错。
因此这样就可以避免这种错误出现。
静态成员变量以及函数(static)
在类中,有一种特殊的成员变量——static成员变量,这种被称为静态成员变量,那么这种变量有何妙用呢?接下来我们就来深入了解一下吧。
在之前我们学习过,static修饰的变量和普通变量不一样,普通变量都是在栈区,除非你是动态开辟的那么就在堆区,而static修饰的则在静态区。
而类中的成员若是用static修饰会怎样呢?
static修饰的成员特性
1.静态成员为所有对象共享,存放在静态区。
2.静态成员变量必须在类外定义,定义时不用加static关键字。
3.类的静态成员可以直接用类名::静态成员或者对象.静态成员来访问。
4.静态成员函数没有隐藏的this指针。
5.静态成员也受限定符限制。
了解了静态成员的特性后,我先来直接看看实现。
#include<iostream>
using namespace std;
class Date {
private:
int _year;
int _month;
int _day;
static int time;
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
static void Print1()
{
cout << "静态成员函数" << endl;
// Print2();静态成员函数不能调用非静态成员函数,因为非静态成员函数必须在对象初始化后才能使用
}
void Print2()
{
cout << "非静态成员函数" << endl;
Print1();//而非静态成员函数可以调用静态成员函数
}
};
int Date::time = 1;
int main()
{
Date::Print1();
Date d1;
cout << endl << endl;
d1.Print2();
return 0;
}
我们发现,静态成员函数不能调用非静态成员函数,而非静态成员函数则 能调用静态成员函数。
此外,定义的time的static类型的变量只能在外面才能初始化,并且构造成员函数无法初始化静态成员变量,但是限定符依旧能够限制外部直接访问time这个静态成员变量。
友元
友元函数
之前我们实现了Date类,但是我们还有几个方法没有实现。
比如用istream直接输入Date变量,而不是通过构造函数来创建。
当然,我们可以在类里面实现,但是这样我们就会有隐藏的this指针,我们的输入就会变成这样:
Date d1;
d1>>cin;
这样就和cin不同了,因此 为了可读性,我们只能在类外面实现这种函数。
但是我们Date类的成员变量又有访问限定符private来防止外部直接访问,那么我们该怎么办呢?
这里就轮到友元出场了。
class Date {
friend istream& operator>>(istream& in, Date& d);
friend ostream& operator<<(ostream& out, const Date& d);
private:
int _year;
int _month;
int _day;
public:
static int time;
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
static void Print1()
{
cout << "静态成员函数" << endl;
// Print2();静态成员函数不能调用非静态成员函数,因为非静态成员函数必须在对象初始化后才能使用
}
void Print2()
{
cout << "非静态成员函数" << endl;
Print1();//而非静态成员函数可以调用静态成员函数
}
};
int Date::time = 1;
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << '/' << d._month << '/' << d._day << endl;
return out;
}
int main()
{
Date d;
cin >> d;
cout << d;
return 0;
}
看过实现后,再来看看友元函数的特性:
友元类的特性
1.友元函数可以直接访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能用const修饰
3.友元函数可以在类的任意位置定义,不受类的访问限定符限制
4.一个函数可以是多个类的友元
5.友元函数的调用和普通函数的调用原理相同
友元类
友元可不止只有友元函数可以使用,实际上,也有友元类存在。
而友元类实际上差不多。
class Time {
friend class Date;
private:
int _hour;
int _minte;
public:
Time()
{
}
void Print()
{
cout << _hour << _minte << endl;
}
};
class Date {
private:
int _year;
int _month;
int _day;
Time t;
public:
Date()
{
}
void Print()
{
cout << _year << _month << _day << t._hour << t._minte << endl;
}
};
我们在Date类创建了一个Time类型的成员变量t,我们就能够直接访问 t 的成员变量。
友元类的特性
1.友元关系是单向的,不具有交换性。2.友元关系不能传递3.友元类不能继承
内部类
当我们在一个类的内部定义了另一个类,那么这个类就是内部类。
而这个内部类实际上就是相当于外部类的友元函数,因此内部类可以直接使用外部类的变量。
但是这个友元只是单向的,外部类并不能用内部类的成员。
class A {
private:
int a;
static int k;
public:
class B {
private:
public :
void Print(const A& d)
{
cout << d._a << endl;
cout << k << endl;
}
};
};
此外,内部类受外部类的访问限定符和类域限制。
内部类的特性:
1. 内部类可以定义在外部类的 public、protected、private 都是可以的。2. 注意内部类可以直接访问外部类中的 static成员,不需要外部类的对象/ 类名。3. sizeof( 外部类)= 外部类,和内部类没有任何关系。
匿名对象
在c++中有这样一种奇怪的对象,它没有名字,被称为匿名对象,我们直接看看如何实现。
class A {
private:
int _a;
public:
A(int a)
:_a(a)
{
}
~A()
{
cout << "这是一个析构函数" << endl;
}
};
int main()
{
A a1(1);
A(1);
return 0;
}
这个匿名对象十分神奇,它的生命周期只有这一行。
那么匿名对象有什么用呢?
比如,我们需要用类的方法返回一个值。
比如这样的类:
class Solution {
private:
int n;
public:
Solution(int _n)
:n(_n)
{
}
int addFromTo(int from,int to)
{
int ret = 0;
for (int i = from; i <= to; i++)
{
ret += i;
}
return ret;
}
};
int main()
{
Solution d(5);
cout << d.addFromTo(0, 100) << endl;
return 0;
}
我们为了计算从0到100的和而创造了一个对象。
但是这个对象可能之后都用不着了;
这时候就可以用匿名对象了。
编译器的优化
在一些新一点的编译器中,对象的创建会被编译器优化。
我们先创建一个这样的类。
class A {
private:
int _a;
public:
A(int a = 0)
:_a(a)
{
cout << "这是一个构造函数" << endl;
}
~A()
{
cout << "这是一个析构函数" << endl;
}
A(const A& a)
{
cout << "这是拷贝构造" << endl;
}
};
1.优化场景1
int main()
{
A a = 1;
return 0;
}
在以前的编译器中,这里的对象应该是先构造一个中间变量再用中间变量拷贝构造出a。
顺序本来是这样的:
但是现在是这样了:
我们可以看到这里只有一次构造函数。
2.优化场景2
int f(A a)
{
}
int main()
{
f(A());
return 0;
}
像这里,如果是用匿名对象来传参,就会直接优化,
变成一次构造函数:
3.优化场景3
A f2()
{
A a;
return a;
}
int main()
{
A ret = f2();
return 0;
}
像这样的场景,本来应该是构造加拷贝构造再拷贝构造的。
而这里编译器会做一个优化,免去中间的拷贝构造,化为一个拷贝构造。
这就是编译器的优化。
以上就是类的剩余知识了,谢谢大家。