1.再谈构造函数
首先我们先来回忆一下构造函数:
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。即构造函数其实就是帮我们对类的成员变量赋一个初值。
1.1构造函数赋值
下面我们来看个例子:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为 初始化只能初始化一次,而构造函数体内可以多次赋值。
下面我们再来看一个例子:
class T
{
private:
int _T1;
int _T2;
};
这里的
int _T1; int _T2;
是对成员变量_T1、 _T2
的声明,在这里只是声明类里这样两个成员变量。
那定义又是在哪里呢?
这里是对对象整体的定义。
那么对象的每个成员变量又是什么时候定义的呢?
我想这时老铁心里肯定这样想:变量整体定义了,它的成员不都也定义了吗?成员不都是属于这个对象的吗?
下面我们再看这个例子:
在这里我们发现程序无法正常运行,大家来想一下,const修饰的变量有什么特点?
是不是const修饰的变量必须在定义的时候初始化。
我想这个时候大家一定想到了:
之前我们在讲解构造函数的时候说,C++11允许内置类型成员变量在类中声明的时候可以给缺省值。
这里程序能够运行了
但是这是C++11才提出来的,那C++11之前呢?如何解决这样的问题呢?
所以我们必须要给成员变量也找一个定义的位置,不然像const这样的成员变量不好处理。
那么成员变量定义到底在哪里呢?
1.2初始化列表
面对上述问题,我们的祖师爷还是把目标锁定在了构造函数。
在构造函数里面呢又搞了一个东西叫做——初始化列表。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
举个例子:
对于上面类中const int _a的初始化我们就可以放在初始化列表进行处理:
class T
{
public:
T(int t1, int t2, int a)
: _T1(t1)
, _T2(t2)
, _a(a)
{
}
private:
int _T1;
int _T2;
const int _a = 1;
};
int main()
{
T t(1,2,3);
return 0;
}
注意:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 以下三种类成员变量,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
没有默认构造函数的自定义类型成员
这里不难理解,因为引用成员变量和const成员变量都必须在定义的时候初始化。
对于没有默认构造函数的自定义类型成员:
因为默认生成的构造函数对内置类型不做处理,对自定义类型会去调用它对应的默认构造函数(不需要传参的构造函数都是默认构造函数),所以如果自定义类型成员没有默认构造函数我们就需要自己去初始化它。
-
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,成员变量都会在初始化列表定义。
-
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
1.3explicit关键字
我们先举个例子:
class T
{
public:
T(int t1)
: _T1(t1)
{
}
private:
int _T1;
int _T2;
};
我们可以用这种方式去创建对象:
int main()
{
T t(1);
return 0;
}
除此之外还可以这样:
int main()
{
T t2 = 1;
return 0;
}
这个地方
T t2 = 1;
,1是一个整型,怎么可以直接去初始化一个类对象呢?
其实这里是一个隐式类型转换。和内置类型之间的隐式类型转换转化是一样的,会产生一个临时变量。
那这里T t2 = 1;
是如何转换的呢?
这里也会产生一个临时变量,这个临时变量就是用1去构造出来的一个T类型的对象,然后再用这个临时对象去拷贝构造我们的t2。
下面我们用一个小例子证明一下:
class T
{
public:
T(int t1)
: _T1(t1)
{
cout << "T(int t)" << endl;
}
T(const T& t)
: _T1(t._T1)
{
cout << "T(const T& t)" << endl;
}
private:
int _T1;
};
int main()
{
T t2 = 1;
return 0;
}
注意:拷贝构造函数也是有初始化列表的,因为拷贝构造函数是构造函数的一个重载形式。
那我们现在运行程序,看T t2 = 1;
是不是先用1调构造函数创建一个临时变量,然后再调拷贝构造构造t2。
这里确实调用了构造函数,但是并没有调用拷贝构造函数。
那问题到底出在哪里了?
其实,C++编译器针对自定义类型产生临时变量的情况,会进行优化。编译器用1构造一个对象,然后再去调拷贝构造,效率受到影响,所以优化成一步,直接拿1去构造我们要创建的对象、
。
当然,不一定所有的编译器都会优化,但是一般比较新一点的编译器在这里都会优化。
在这里想告诉大家的是:
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
那如果我们这里不想让它支持类型转换了,有没有什么办法呢?
这就要用到一个关键字——
explicit
我们只需在对应得构造函数前面加上explicit
关键字:
对于单参数的构造函数是支持这种类型转换的,那多参数的构造函数呢?
这里C++98是不支持多参数的构造函数进行隐式类型转换的。但是C++11对这块进行了扩展,使得多参数的构造函数也可以进行隐式类型转换
2.static成员
2.1概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用 static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
那么static成员有什么用呢? 我们先来看一个小题目:
实现一个类,计算程序中创建出了多少个类对象。
老铁们思考一下,可以怎么做?
首先要创建一个类对象,一定是通过构造函数或者拷贝构造创建的。那我们可以定义一个全局变量n,初值为0。然后每次调用构造函数或者拷贝构造创建对象时就让n++。
但是这种方法真的好吗?
其实是不太好的,首先这里我们定义一个全局变量,首先它可能会发生命名冲突;其次,全局变量在哪都能访问(C++讲究封装)。
我想这个时候老铁可能又想到一种方法:
我们把统计个数的这个n变量放到类里面,这样它就属于这个类域了,然后如果不想让它在类外面被访问到,我们可以把它修饰成私有的。
但是这种方法真的可行吗?
如果直接放到类里面,作为类的一个成员变量,那它就属于对象,但我们要统计程序中创建对象的个数,这样我们每次创建一个对象n就会定义一次,是不是不行啊,不能让它属于每个对象,应该让它属于整个类。
2.1静态成员变量
对于上述问题我们可以这样解决:
在它前面加一个static修饰,让它成为静态成员变量。那这样它就不再属于某个具体对象了,而是存储在静态区,为所有类对象所共享。规定静态成员变量的初始化(定义的时候赋初值)一定要在类外,定义时不添加static关键字,类中只是声明。
2.2静态成员函数
静态成员函数有一个特性:静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
因为非静态成员是属于对象的,都是通过this指针去访问的,而静态成员函数是没有this指针的。
2.3特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明,静态成员变量一定要在类外进行初始化
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
3.匿名对象
现在有这样一个类:
class T
{
public:
T(int t1 = 0)
: _T1(t1)
{
cout << "T(int t)" << endl;
}
~T()
{
cout << "~T()" << endl;
private:
int _T1;
};
我们现在想要用这个类去创建对象,除了我们之前学的方法之外,其实我们还可以这样创建对象:
这里我们用T这个类创建了一个匿名对象。
匿名对象的特点就是没有名字,但是它的生命周期只在创建它的这一行。
但是要注意,和临时变量一样,如果我们用匿名对象去初始化一个引用的话,它的生命周期就会被延长至该引用被销毁。并且这里肯定都要加const的,因为临时变量和匿名对象都具有常性。
那匿名对象有什么用呢?
现在有一个类
Solution
,里面有一个非静态成员函数Sum_Solution
,我们知道想要调用类里面的非静态成员函数,是需要通过对象去调用的:
那现在有了匿名对象,我们就可以这样调用了:
4.友元函数
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
4.1友元函数
在之前的类和对象的学习中我们讲过运算符重载:
现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类里面的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
4.2友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
- 友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承,在继承位置再给大家详细介绍。
5.内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
比如这样:
class A
{
private:
int a;
public:
class B
{
private:
int b;
};
};
内部类并不属于外部类,它和对应的外部类是相互独立的,只是受外部类类域的限制。
对于上面那个类来说,我们想拿A中的内部类B去创建对象,这样是不行的:
这样才行:
内部类天生就是其对应的外部类的友元类。参见友元类的定义,内部类可以通过外部类的对象参数来访 问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
6.再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:文章来源:https://www.toymoban.com/news/detail-675531.html
- 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
- 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
- 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣 机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
- 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有哪些属性,哪些方法,描述完成后就形成了一种新的自定义类型,用然后用该自定义类型就可以实例化具体的对象。
文章来源地址https://www.toymoban.com/news/detail-675531.html
到了这里,关于0基础入门C++之类和对象下篇的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!