这篇有关类和对象的知识是对之前一些不完整知识的补充以及新的知识的介绍。
目录
3.11流插入和流提取在类istream和ostream中的重载。
1.再谈构造函数
1.1构造函数的对类对象的赋值
我们都知道构造函数是用于对类对象的初始化,为类对象的赋予一个合适的初始化值。但实际上,我们对于初始化这个词有不太正确的认识,对类对象成员的初始化的操作本是仅仅能进行一次的操作,但作为构造函数,我们却可以在其内进行多次赋值操作,所以准确来说,构造函数的真正作用是对类对象成员进行赋值,而并非是进行初始化。
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
};
1.2初始化列表
实际上,类成员的初始化另有其法,就是初始化列表。初始化列表是真正意义上对类对象进行初始化,构造函数体和初始化列表两者可以配合一同使用,此外我们之前对给定成员变量的缺省值实际上也是被使用在初始化列表中的。
1.21初始化列表的使用
初始化列表实际上是在构造函数之前对类成员进行初始化的操作,其用法是:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
};
需要注意的是,类中的各个成员变量只能在初始化列表中进行一次初始化操作,但可在后续的{}中为成员多次赋值或进行其他操作。
1.22 初识化列表的作用
有同学可能会疑惑,初始化列表的作用不是与构造函数的作用没有太大区别吗,两者都可以对类成员变量进行初始化,但实际上,仅靠构造函数是无法对所有的成员变量进行初始化操作的。对于以下成员变量,是必须在初始化列表中进行初始化的。
- 引用成员变量
- const修饰的成员变量
- 没有默认构造的自定义成员变量
初始化列表实际上是为了处理以上三种成员变量的初始化而出现的,看下面代码,包含了三个成员,这三个成员无法放在构造函数体内进行初始化,而都应当放在初始化列表进行初始化。
class A//无默认构造函数
{
private:
int _a;
public:
A(int a)
:_a(a)
{}
};
class B
{
private:
int& _m;//引用
const int _n;//const修饰
A _a;//无默认构造的自定义类
public:
//正确的初始化
B(int m, int n)
:_m(m)
,_n(n)
,_a(0)
{}
//错误的初始化
/*B(int m, int n)
{
_m = m;
_n = n;
_a = 0;
}*/
};
如果我们仍然使用函数体内进行初始化的方法,就会出现以下错误:
1.23初始化列表的使用规定
1.我们在初始化类成员时,尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于构造函数的使用,实际上是一定会先使用初始化列表初始化成员变量。
2.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
让我们看下面的代码:
class A
{
private:
int _a2;
int _a1;
public:
A(int a)
:_a1(a)//再对a1初始化 -> 1
, _a2(_a1)//先初始化a2 -> 随机值
{}
void Print()
{
cout << _a1 << endl << _a2 << endl;
}
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
由于成员变量的初始化顺序只和成员在声明时的顺序一致的,所以这段代码在将1作为参数去初始化类A对象时,先初始化的是先声明的变量a2,而a2初始化的值是a1的值,也就是随机值,然后再对a1进行初始化,使用传递的参数1对a1初始化得到值为1。
所以,对于我们在使用初始化列表初始化成员的时候,尽量保持与声明成员时相同的顺序。以免可以出现一些难以意料的错误。
1.3构造函数的隐式类型转换
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 1, int month = 1, int day = 1)//多参数带缺省值构造函数
:_year(year)
,_month(month)
,_day(day)
{}
/*Date(int year)//单个参数构造函数
:_year(year)
,_month(month)
,_day(day)
{}*/
void Print()const//const Date *this
{
cout << _year << "." << _month << "." << _day << endl;
}
};
执行上述代码,我们以数字2和3.14用于初始化类对象,此过程就发生了隐式类型转换,对于使用相同和相近类型的成员变量int型和浮点型去初始化类对象,此过程会先生成中间变量,先由数字构造生成一个与类该类相同的类型,也就是生成了Date类型的中间变量,然后再通过该中间变量调用拷贝构造去初始化类对象。
1.31多参数发生的隐式转换
除了单个参数对类初始化会发生转换,含有多个参数的去初始化类对象,同样可能发生隐式类型转换。多参数的参数使用 { } 和逗号将各个参数分隔开,不要写成()否则就是变成使用逗号表达式了,还需要注意的是,含多个参数所发生的隐式类型转换中各个参数,同样是需要与类各个成员类型相同或相近,否则会报错。
我们同样使用上数的Date类,用多个参数同样可以达到初始化的效果,由多个参数发生的转换其原理还是一样的,先通过与类成员相同或相似的变量构造生成一个该Date类的中间变量,再由该中间变量调用拷贝构造去初始化类对象。
1.32关键字explicit
关键字explicit是用于修饰构造函数,在构造函数前面加上该关键字,就可以防止对调用构造函数而引发隐式类型转换,可以避免隐式类型转换的发生。
1.4拓展:有关构造、拷贝构造 、赋值重载之间的区分
对于类中的这几个构造函数,很多人十分容易混淆,下面让我们好好来缕清楚这几个之间的区别, 通过以下类A来充分理解这几个之间的区别。
class A
{
private:
int _a;
public:
A(int a = 0)//构造重载,默认构造
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)//拷贝构造
{
_a = aa._a;
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)//赋值重载
{
_a = aa._a;
cout << "A& operator==(const A& aa)" << endl;
return *this;
}
};
构造:通过传递参数使用构造重载初始化对象,或者不传参数使用默认构造来初始化对象。
解释:a1是通过传递参数,调用构造重载而初始化对象,a2则不传递参数,使用默认构造初始化对象,两者都是使用构造。
拷贝构造:使用已经存在的类对象去初始化另一个类对象,或者通过值传递已经存在的类对象参数。
解释:对象a1的初始化调用了第一次的构造函数,a2和a3都是通过使用已经存在的对象a1来进行初始化的,所以两者调用的均为拷贝构造,而对于func1()函数的调用,其参数是值传递,使用在传递参数a2时需要通过调用拷贝构造来形成函数的参数,而func2()函数的调用,其参数为引用传递,直接将a2作为别名传递,不需要调用成员函数。
赋值重载:使用两个已经存在的类对象进行赋值操作。
2.static成员
2.1静态成员变量
- 被全体对象共享,共同使用,不属于某一个具体的对象。
- 需要在类中声明,在类之外定义,声明时加static修饰,定义时不加static修饰。
- 生命周期是直到程序结束,本质上是受类域和类访问限定符private等限制的全局变量,但只能在类中使用。
- 类静态成员可用类名::静态成员或者对象.静态成员来访问。
看以下代码来理解一下,我们使用静态成员来记录类对象的创建个数。 每次发生构造或者拷贝构造时都会使该静态成员变量值自增1。
class A
{
private:
static int count;//静态成员类中声明
int _a;
public:
A(int a = 0)
:_a(a)
{
++count;
cout << "A(int A = 0)" << endl;
}
A(const A& aa)
{
_a = aa._a;
++count;
cout << "A(const A& aa)" << endl;
}
int Getcount()
{
return count;
}
};
int A::count = 0;//类外定义
void func(A aa)
{
A a = aa;
cout << a.Getcount() << endl;
}
我们首先创建了一个类对象a1,调用构造函数初始化,count自增1,通过成员函数的获取到静态成员count值1,调用函数func()通过传值传参将a1的值拷贝构造初始化func()函数中的形参,count再自增1,在func内部又由已经存在的对象形参去初始化另一个对象,此过程也发生了拷贝构造,count再次自增1,count最后的到的值为3。
2.2静态成员函数
静态成员函数同样是被static所修饰的成员函数。
特性:
- 没有隐藏的this指针,不能访问任何非静态成员。
- 本质上是受类域和类访问限定符的private等限定的全局函数,但只能在类中使用。
- 类静态函数可用类名::静态成员函数或者对象.静态成员函数来访问。
看以下代码,我们对于之前的代码仅将成员函数Getcount()用static修饰使其变为静态成员函数,其余均不改变。
class A
{
private:
static int count;//静态成员声明
int _a;
public:
A(int a = 0)
:_a(a)
{
++count;
cout << "A(int A = 0)" << endl;
}
A(const A& aa)
{
_a = aa._a;
++count;
cout << "A(const A& aa)" << endl;
}
static int Getcount()
{
return count;
}
};
int A::count = 0;//定义
由下面代码的执行结果,我们可以发现,静态成员函数的调用,无论是否创建对象,都可以进行直接调用,这是因为静态成员函数已经没有隐藏的this指针,不要创建对象传递隐藏的this指针进行调用函数,
差异:与非静态成员函数相比,静态成员函数没有隐藏的this指针,其调用既可以通过类名配合域作用限定符调用直接调用,也可以使用类对象进行调用访问,不需要像普通成员函数必须要创建类对象才可以调用,但是由于没有this指针,静态成员函数也不能访问使用非静态的成员变量。
3.流插入(<<)和流提取(>>)运算符的重载
之前我们介绍的运算符的重载中,有两个比较特殊的运算符的重载我们还没有介绍,在这里补充。
介绍:流插入<< 和流提取>>本是C++中使用输入和输出的手段,不同与C语言中的输入输出函数,流运算符的使用输入输出配合 cout 和 cin 可以完成自动识别类型,那这是为什么呢?我们首先要了解以下。
3.1cout和cin
我们首先要了解cout和cin两个是什么,实际上
cout和cin是C++中iostream中所定义的两个类的两个对象。cout对应的是类ostream的对象,而cin对应的是类istream的对象。
3.11流插入和流提取在类istream和ostream中的重载。
流插入<<在类ostream中进行了重载,流提取>>在类istream中进行了重载。 这两个运算符的重载分为了多种不同参数类型的重载,使得面对不同的参数类型,cout和cin对象能够调用不同的重载函数完成输入和输出。
则就相当于我们平时使用的输入和输出是调用了类中不同对应的输入输出重载函数,例如
流插入的使用:
流提取的使用:
通过以上我们也能够发现,cout和cin实际上作为两个类的对象,在面对不同参数会调用不同的io类(istream类和ostream类的简称)中的重载函数。
3.2 << 和 >> 在自定义类型中的重载
我们都知道,流插入 << 和流提取 >> 能够识别的类型只有C++中本身就有的内置类型,对于自定义类型的变量,io类对象cout和cin中没有对应的重载函数,所以本身是无法识别自定义类型的变量的。但我们可以借用已经重载过输入输出的类对象cout,cin来完成识别各个不同参数,使得可以cout和cin能够完成对自定义类型的输入和输出,因为自定义类型的本质也是各个内置类型所组成的。
重载时需要注意的点:
- ostream、istream对象参数一定要位于重载函数的第一个参数,否则可能会出现歧义,此外使用的类参数一定是引用类型的参数。
- 为了支持连续输入输出,重载函数的返回值需要是对应类型对象的引用。
下面我们给一个例子:
class A
{
private:
int _a;
char _ch;
public:
A(int a, char ch)
:_a(a)
, _ch(ch)
{}
int Get_a()const//const修饰的成员变量不能被改变
{
return _a;
}
char Get_ch()const
{
return _ch;
}
};
ostream& operator<<(ostream& out, const A& aa)//const修饰aa防止其被修改
{
out << aa.Get_a() << aa.Get_ch();
return out;
}
我们创建一个类A,包含了一个int型和char型的成员变量,我们使用对流插入运算符的重载完成对cout配合流插入可以完成对自定义类型的输出。由于对于对象成员变量一般都设置为私有,不能直接访问,我们设置了专门用于获取成员变量值的对应函数。
4.友元
4.1友元函数
前面有关与流插入和流提取的重载中,我们有遇到过对于私有成员变量不能够直接访问的问题,需要写专门的成员函数来获取对应对,这里我们要介绍的友元则可以突破封装,直接能够访问对象的私有,不受类访问限定符限制
友元函数的使用只需要在函数名前面加上关键字friend修饰即可。
4.11特性
- 友元函数不能使用const修饰。
- 可以访问类的私有和受保护的成员变量
- 友元函数使用声明时,可在类中的任何地方,定义时在类中或类之外均可。
- 友元函数不是类的成员函数,而是属于全局函数,与定义的位置无关,并不是定义在类中就是类的成员函数,其并没有类成员函数所具备的隐藏this指针参数。
- 如果定声明和定义分开写,只有声明时需要friend修饰。
下面使用经典日期类Date相关代码来理解一下。
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
friend ostream & operator<< (ostream& out,const Date & d)//声明+定义
{
out << d._year << "." <<d._month << "." <<d._day;
return out;
}
friend istream& operator>>(istream& in, Date& d);//声明
};
istream& operator>>(istream& in, Date& d)//定义
{
in >> d._year >> d._month >> d._day;
return in;
}
对于自定义类Date中重载流插入流提取运算符的重载,其使用与普通函数是一致的,我们可以直接调用函数名加传递参数就能完成调用,或者也可以使用类istream和ostream的对象cin、cout来使用更简单的输入输出。
4.2友元类
友元类相当于友元函数的封装,与友元函数一样,友元类是声明在类中任意处,友元类中所有的成员函数都是所声明类的友元函数,也就是说,友元类中的任意成员函数都可以访问使用另一个类中的私有成员变量。
友元类的使用同样是使用关键字friend来修饰。
4.21特性
- 友元类中任意成员函数均为声明所在类的友元函数,可访问使用类的私有成员变量。
- 友元的关系是单向的,友元类所声明所在的类,只能说明该类为另一个类的友元,而另一个类却并不为该类的友元。
- 友元关系不能传递,也不能继承。好比A为B的友元类,B为C的友元类,但是并不能说明A为C的友元,继承的话,目前还没有学,以后学过了再做补充。
以下给出一个简单的例子:
class A
{
friend class B;
private:
int _a = 0;
};
class B
{
private:
int _b;
A a;
public:
B(int b = 0)
:_b(b)
{
a._a = 0; //直接访问A中的私有成员
}
};
友元类的声明是声明在类之中的,其定义最好定义在类之外,否则就边为了内部类,内部类的相关知识我们接下来也是要介绍的。
5.内部类
概念:内部类是一个定义在类中的一个类。
内部类也是一个独立的类,它不属于它所声明定义所在外部类,不占用外部类的存储空间,更不能通过外部类的对象去用,我们可以理解内部类是一个只存在与外部内中的一个单独的类,其使用受到了两层保护,一个是外部类的域作用限定符,另一个是内部类本身具有的限制,所以内部类是一个只能为外部类所使用的一个特殊存在。
5.1特性
- 内部类是一个先天的友元类,可以访问外部类的私有成员变量。
- 内部类不占有外部类的存储空间,在计算外部类大小的时候,内部类的存在与否不会影响外部类大小的计算。
- 内部类可定义在类的各个位置,无论是private或者public、protected都可。
class A
{
private:
static int count;
int _a = 0;
public:
class B//先天为A的友元类
{
private:
int _b = 0;
public:
void func()
{
A a;
a._a = 10;//可通过对象直接访问对应私有成员
count++;//静态成员变量不需要对象即可直接访问
}
};
};
int A::count = 0;
内部类可通过对应的外部类对象去访问使用其私有成员变量,对于外部类的静态成员变量,内部类不需要外部类的对象就可以直接访问。但不能通过外部类的对象去访问使用内部类。
6.匿名对象
顾名思义,匿名对象是一个不带有具体名称的类对象,不用取名字就能使用,其使用的生命周期只有当前使用代码的一行,属于是一个临时的对象,用完就被销毁,使用比较有限,但在某些场景下很好用。
使用:类名 ()
下面给一个简单的例子:
class A
{
private:
int _a;
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)"<< endl;
}
~A()
{
cout << "~A()" << endl;
}
};
执行下面代码,先后创建了两个匿名对象,我们会发现,匿名对象的创建使用构造函数后紧接着就掉用了析构函数,这是因为匿名对象的生命周期就只有当前创建使用时的一行代码,紧接着生命周期就结束,自动调用析构函数。
对于匿名对象的使用,我们可以选择性对其是否初始化。
7.编译器对构造函数和拷贝函数的优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,针对于一行代码中出现多次调用构造函数或者拷贝构造的情况,目前大部分的编译器都会做出相关优化。
7.1优化规律
1.连续的构造+拷贝构造:被优化为调用一个构造函数
2.连续的拷贝构造+拷贝构造:被优化为一个拷贝构造
以下面代码为例:
class A
{
private:
int _a;
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
A(const A& aa)
{
_a = aa._a;
cout << "A(const A& aa" << endl;
}
};
A func()
{
A a(1);
return a ;
}
对与一行中代码中出现连续使用构造函数和拷贝构造函数的情况,编译器会将其优化为单个的构造函数,此过程a的初始化发生了隐式类型转换,1首先会调用构造函数转换为类对象,然后再通过调用拷贝构造去初始化a。
需要注意的是,并不是所以的编译器都会做优化的处理,此外不同的编译器的优化程度可能有所不同,但对于上面提到的两种优化情况,目前比较新的编译器都会做此项处理。
结语
这节对于C++中最后一篇类和对象的知识介绍到这里就结束了,感谢各位的观看,下期将继续带来其他干货知识,如果有错误或者需要改进的地方还请指出,如果有帮助的话,还请点个赞呀!