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; };
构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值
1.2 初始化列表
初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
举个栗子:
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 = 2023, int month = 4, int day = 12) { //变量在使用前已经被初始化了,此时是在给变量赋值 _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
通过调试可以看到在被赋值之前变量已经被初始化为了随机值
如果此时在成员变量中新增一个const
修饰的成员变量会怎么样呢?
这里发现直接报错了,这是为什么呢?
这是因为赋值的前提是已经被初始化,成员变量在被赋值前已经被编译器初始化成随机值了,而const
修饰的变量是具有常性的,只能初始化一次,所以这里_num
已经被初始化为随机值了,且因被const
修饰,具有常性无法被赋值
这就体现了原构造函数初始化方式的缺陷:
无法给const修饰
的成员初始化,无法给引用
的成员初始化,无法给自定义成员
初始化(且该类没有默认构造函数时)
于是就提出了**初始化列表
**来完成初始化的任务
** 对象调用构造函数,初始化列表就是该对象定义(开空间)所有成员变量的位置**
class Date { public: Date(int year = 2023, int month = 4, int day = 12, const int num = 24) :_year(year) //初始化列表 ,_month(month) ,_day(day) ,_num(num) {} private: int _year; int _month; int _day; const int _num; };
通过调试会发现,进入构造函数体之前会先走一遍初始化列表,进入函数体后成员变量已经被初始化了,可见初始化列表完美的解决了上面的缺陷
注意:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用
成员变量const
成员变量自定义类型
成员(且该类没有默认构造函数时)
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
1.3 explicit关键字
类型转换:
在赋值双方类型不匹配时,编译器会创建一个同类型的临时变量(具有常性),将左值拷贝给临时变量再拷贝给右值
double d = 6.66; int a = d; //隐式类型转换,截取d的整数部分拷贝给临时变量再拷贝给a
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用
//单参数 class A { public: A(int a) :_a(a) {} private: int _a; }; //多参数 class B { public: B(int x = 12, int y = 24, int z = 36) :_a(x) ,_b(y) ,_c(z) {} private: int _a; int _b; int _c; }; int main() { A a(12); //构造函数 A b = 24; //编译器优化为A b(100) int类型赋值给自定义类型(中间发生类型转换, 产生临时变量) B c = { 1,2,3 }; //编译器优化为A c(1, 2, 3) return 0; }
用explicit修饰构造函数,将会禁止构造函数的隐式转换
class A { public: explicit A(int a) :_a(a) {} private: int _a; }; int main() { A a(12); //构造函数 A b = 24; //err 报错:不存在从int转换到A的适当构造函数 return 0; }
2. static成员
2.1 概念
- 声明为
static
的类成员称为类的静态成员
- 用
static
修饰的成员变量,称之为静态成员变量
,静态成员变量位于静态区,一定要在类外进行初始化 - 用
static
修饰的成员函数,称之为静态成员函数
下面我们实现一个类,计算程序中创建出了多少个类对象:
class A { public: A() { ++_count; } A(const A& a) { ++_count; } ~A() { --_count; } static int getCount() //静态成员函数,没有this指针 { return _count; } private: static int _count; //存放在静态区 }; int A::_count = 0; //类外初始化 int main() { //静态成员函数在静态区,不需要实例化对象再调用函数 cout << A::getCount() << endl; //0 A a1; A a2(a1); cout << A::getCount() << endl; //2 return 0; }
2.2 特性
- 静态成员是存放在静态区的,为所有类对象所共享,不属于某个具体的对象
- 静态成员变量必须在类外定义,定义时不添加
static
关键字,类中只是声明 - 类静态成员即可用 类名
::
静态成员 或者 对象.静态成员 来访问 - 静态成员函数是存放在静态区的,没有隐藏的
this指针
,不能访问任何非静态成员 - 静态成员也是类的成员,受
public
、protected
、private
访问限定符的限制 - 这里有两个问题:
- 静态成员函数可以调用非静态成员函数吗?
- 答案是不行的,静态成员函数是存放在静态区的,没有隐藏的this指针,不能访问任何非静态成员
- 非静态成员函数可以调用类的静态成员函数吗?
答案是可以的,静态成员函数是存放在静态区的,没有隐藏的this指针,不能访问任何非静态成员
3. 友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用
3.1 友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend
关键字
class A { //声明外部函数 Add_A 为A类的友元函数 friend int Add_A(); public: A(int x = 12, int y = 24) :_a(x) ,_b(y) {} private: int _a; int _b; }; int Add_A() { return A()._a + A()._b; } int main() { cout << Add_A() << endl; return 0; }
注意:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用
const
修饰 - 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
3.2 友元类
使用friend
修饰的类,称为友元类
class Time { // 声明日期类为Time类的友元类,则在日期类中可以直接访问Time类中的私有成员变量,但是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; }; class Date { public: Date(int year = 1900, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} 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; };
注意:
- 友元关系是单向的,不具有交换性
- 友元关系不能传递:如果
C
是B
的友元,B
是A
的友元,则不能说明C
是A
的友元 - 友元关系不能继承
4. 内部类
4.1 概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限
注意:
内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元
class A { public: //B是A的内部类,天生就是A的友元 class B { private: int _c; int _d; }; private: int _a; int _b; };
4.2 特性
- 内部类可以定义在外部类的
public
、protected
、private
都是可以的 ,受类域限定符的限制 - 注意内部类可以直接访问外部类中的
static
成员,因为是静态区的,不需要外部类的对象/类名 sizeof(外部类+内部类)=sizeof(外部类)
,和内部类没有任何关系
5. 匿名对象
我们知道C语言
可以创建匿名结构体
,C++
则支持创建匿名对象
5.1 语法
- 语法:类名()
- 声明周期就是调用完成后,,只有短短一行
class A { public: A(int a = 12) :_a(a) { cout << "A(int a)" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { A(); //匿名对象,生命周期就是本行结束 return 0; }
5.2 使用场景
匿名对象适用于一次性场景,也适用于优化性能
举个栗子:
class A { public: int Getnum(int n) { return n; } }; int main() { A().Getnum(12); //不创建对象直接调用成员函数,提高效率 //等价于 A aa; aa.Getnum(12); return 0; }
6. 拷贝对象时的编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝
下面来定义一个类进行探究:
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& aa)" << endl; } //赋值重载函数 A& operator=(const A& aa) { cout << "A& operator=(const A& aa)" << endl; if (this != &aa) { _a = aa._a; } return *this; } //默认析构函数 ~A() { cout << "~A()" << endl; } private: int _a; };
6.1 参数优化
类外定义函数:
void func1(A aa) {} int main() { //传值传参 A a1; func1(a1); return 0; }
我们换一种写法
int main() { func1(12); return 0; }
这里本来的流程应该是:构造(隐式转换) + 拷贝构造(传参) + 构造(创建a1)
,这里编译器对其进行了优化:连续构造 + 拷贝构造 -> 优化为直接构造
再来看一种情况:
int main() { //连续构造 + 拷贝构造->优化为一个构造 func1(A(12)); }
连续构造 + 拷贝构造->优化为一个构造
6.2 返回优化
类外定义函数:
A func2() { return A(100); } int main() { //一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 A a2 = func2(); return 0; }
这里本来的流程是:构造(创建匿名对象) + 构造(临时变量) + 拷贝构造(匿名拷贝给临时) + 拷贝构造(临时拷贝给a2)
,这里编译器对其进行优化:连续拷贝构造+拷贝构造->优化为一个拷贝构造
再来看一种情况:
int main() { //连续拷贝构造+拷贝构造->优化为一个拷贝构造 A a2 = func2(); //连续拷贝构造+赋值重载->无法优化 a2 = func2(); return 0; }
连续拷贝构造+赋值重载->无法优化
总结:
- 对象返回时,接收返回值对象,尽量拷贝构造方式接收,不要赋值接收
- 对象返回时,函数中返回对象时,尽量返回匿名对象
- 函数传参时,尽量使用
const&
传参
7. 再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
- 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
- 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对 洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
- 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象
C++类和对象(下)到这里就介绍结束了,类和对象的章节也已经完结,本篇文章对你由帮助的话,期待大佬们的三连,你们的支持是我最大的动力!
文章有写的不足或是错误的地方,欢迎评论或私信指出,我会在第一时间改正