【C++】类与对象(3)
作者:爱写代码的刚子
时间:2023.5.9
本篇博客干货比较多,主要是对类和对象知识的进一步加深,可能有点晦涩。主要介绍的内容为:深入构造函数,初始化列表,友元,static成员,内部类,对封装的进一步理解。
目录
- 【C++】类与对象(3)
- 构造函数理解
- 初始化列表(是构造函数的一部分)
- explicit关键字
- static成员
- 成员变量和静态成员变量区别
- 成员函数和静态成员函数区别
- C++11 的成员初始化新玩法。
- 友元
- 友元函数
- 友元类
- 内部类(Java常用)
- 匿名对象
- const延长匿名对象的生命周期
- 理解封装
构造函数理解
构造函数不能称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
于是如何对类成员进行初始化?
于是引入初始化列表
初始化列表(是构造函数的一部分)
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
例:
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
- 问题1:既然有构造函数为什么要设计初始化列表?
有些成员变量必须在初始化列表初始化:
- A _aobj;(没有默认构造函数的类)
- int& _ref;(引用类型)
- const int _n;(const修饰的类型)
解释:
以上三种类型都必须在定义的时候初始化,而构造函数的本质并不是初始化而是赋值(函数体内赋值),而初始化列表的执行在构造函数之前,是真正意义上的初始化,所以初始化列表是对象成员定义的位置。
- 问题2:对成员变量给缺省值和初始化列表冲突吗?
例:
class Date
{
public:
Date(int year,int month,int day)
:x(2)
{}
private:
int x=1;
};
解释:
如果在初始化列表中显示地给了x的值,缺省值就失效了,用初始化列表来对x初始化,如果初始化列表中没有对x进行初始化,就会使用缺省值。
- 问题3如何理解没有默认构造函数的类只能用初始化列表初始化呢?
例:
class A
{
public:
A(int a)
{
_a = a;
}
private:
int _a;
};
class B
{
public:
B(int b)
:_b(b)
, a(2)
{}
private:
int _b;
A a;
};
解释:
这里说的没有默认构造函数(无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数)不是指编译器自动生成的默认构造函数,而是指无参构造函数、全缺省构造函数。这里A类写了构造函数所以编译器不会生成无参的构造函数,如果A没有初始化列表,初始化时没有参数传递,加上没有对应的构造函数(无参或全缺省),编译器会报错。
- 运用:用初始化列表初始化栈:
注:由于是举例说明,并没有具体实现Stack类。
class MyQueue
{
public:
MyQueue()
{}
MyQueue(int capacity)
:_pushst(capacity)
,_popst(capacity)
{}
private:
Stack _pushst;
Stack _popst;
};
- 注意1:注意引用的初始化!
举例:
class A
{
public:
A(int a,int& ref)//注意ref类型,不能为int类型
:_ref(ref)
,_n(1)
{}
private:
int& _ref;
int _n;
};
1. 初始化列表中ref必须是引用类型,如果是局部变量会导致ref销毁,_ref初始化后变为野引用
2. ref不能为常量(数字等const类型数据),常量作为参数会导致权限放大,编译器报错。
这说明引用并不是绝对安全的
-
注意2:注意初始化列表的初始化顺序!
初始化列表的初始化顺序是成员变量的定义顺序,与在初始化列表的先后顺序无关!
以下为错误用例:
由于A类中成员变量的定义顺序为_a2 _a1 ,所以先对_a2进行初始化,再对_a1进行初始化,而不是初始化列表中的顺序(先_a1再_a2)。
所以初始化列表中变量的顺序最好和成员变量的顺序保持一致,防止出现初始化错误。 -
注意3:初始化列表并不能代替函数体赋值
以下功能等初始化列表并不能很好实现:
- 用malloc开辟空间(虽然初始化列表括号里能使用malloc,但初始化列表并不能对空指针进行检查)
- 对数组进行初始化并检查空指针
- 动态开辟二维数组(循环+指针数组+动态开辟空间)
总有工作是初始化列表无法很好完成的,但建议优先考虑使用初始化列表
【总结】
- 每个成员变量在初始化列表中最多出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(该类没有默认构造函数)
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
引入:
- 在C++中,将整形赋值给对象是可以的。
如:A aa = 2;
这里的整形2发生了隐式类型转换(整形转换为自定义类型),既然发生了类型转换就一定会产生临时变量(这里产生了A类型的临时变量,再进行拷贝构造)。
过程:2构造一个A的临时对象,临时对象再拷贝构造aa。但是构造之后再进行拷贝构造过程较为繁琐,有些编译器会对这个过程进行优化,优化成直接进行构造。(编译器会优化连续的构造函数+拷贝构造)
以VS2019为例:
两者调用的都是构造函数,说明编译器进行了优化。
同一行一个表达式(分开写不会优化)中连续的构造+拷贝构造,优化为合二为一(也有合三为一)(《深度探索C++对象模型》)
【总结】
- 隐式类型,连续构造+拷贝构造->优化为直接构造
- 一个表达式中,连续构造+拷贝构造->优化为一个构造
- 一个表达式中,连续拷贝构造+拷贝构造->优化为一个拷贝构造
- 一个表达式中,连续拷贝构造+赋值重载->无法优化
- 问题1:既然直接进行构造函数那还会产生临时变量吗?
286行代码正确,而287行代码错误,说明仍然产生了临时变量。
所以我们可以理解为编译器的优化是直接构造函数,但语法上仍产生了临时变量。 - 问题2:为什么C++能实现对整形的隐式类型转换成自定义类型?
举例:在string类中可以将字符串转化为string类,如果要将字符串插入到链表中可以直接将字符串作为string类的参数(隐式类型转换)。 - 如果我们不想让隐式类型转换发生我们可以用explicit关键字
将explicit关键字加在只有一个参数的构造函数的开始,来防止发生隐式类型转换。
static成员
概念:声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
成员变量和静态成员变量区别
成员变量:属于每一个类对象,存储在对象里面。
静态成员变量:属于类,类的每一个对象共享。生命周期为全局,存储在静态区
如果对象里有静态成员变量那么它必须在类外面定义,因为静态成员变量是共享的,不是属于单独的具体的一个类,如果在类里面定义可能会发生多次初始化从而报错。
定义方式:
按理来说在类外面不能访问私有变量,这是特例。
如果在类外要访问私有的静态变量,可以用Get成员函数,或者用友元函数,或者改为public
成员函数和静态成员函数区别
成员函数:有this指针,
静态成员函数:无this指针,指定类域和访问限定符(. 和** :: ** 都可以)就可以访问(可以突破类域)
由于静态成员函数没有this指针,所以无法访问非静态的成员变量(无法指定某个类),但可以访问静态成员变量
【总结】
- 静态成员为所有类对象所共享,不属于某个具体的实例
- 静态成员变量必须在类外定义,定义时不添加static关键字
- 类静态成员即可用类名::静态成员或者对象.静态成员来访问
- 类里面的静态成员不能给缺省值
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
【问题】
- 静态成员函数可以调用非静态成员函数吗?(不能)
- 非静态成员函数可以调用类的静态成员函数吗?(可以)
C++11 的成员初始化新玩法。
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。
友元
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元函数
形式:friend + 函数声明;
友元函数并不是类成员,因此声明的位置不固定(一般在类开头声明)
友元函数在之前的博客【C++】类与对象(2补充运算符重载,const成员)(博客链接)中流插入和流提取中有提到:
我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理
友元函数:可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
说明:友元函数并不是特别好,因为友元函数一定程度上破坏了类的封装,(像Java这样的语言不太推荐用友元Java常用get成员函数和set成员函数)
【总结】
- 友元函数可访问类的私有和保护成员,但不是类的成员函数。
- 友元函数不能用const修饰。(因为友元函数不属于类,没有this指针)
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
4.== 一个函数可以是多个类的友元函数。==- 友元函数的调用与普通函数的调用和原理相同。
友元类
形式:friend class + 类名;
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
比如Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time
类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。 - 友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
内部类(Java常用)
概念及特性
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:==内部类就是外部类的友元类。==注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。(也就是内部类可以访问外部类成员,而外部类不能访问内部类成员)
内部类运用示例:
内部类用于
【总结】:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
匿名对象
如果只需要调用一次类里面的成员函数,可以使用匿名对象来访问成员函数,如果需要多次访问成员函数就不能使用匿名对象,匿名对象调用后就立即销毁(生命周期只在当前行,有名对象生命周期在局部域)。
注意:无论是有参调用构造函数还是无参调用构造函数都必须加括号。
A.print(10);是错误的写法,因为==类型不能调用函数。==匿名对象和普通对象传参一样,只是少了名字。
const延长匿名对象的生命周期
匿名对象是可以被引用的,但需要加const(匿名对象具有常性!!!),const延长了匿名对象的生命周期,使其不会变为野引用。
- 301行正确,302行错误(权限放大)。
- 之后顺序表和链表等会用到const延长匿名对象生命周期这一特性。(push一个匿名对象,作为push函数中的参数:const string& s)
理解封装
C++是基于面向对象的程序,面向对象有三大特性即:封装、继承、多态。
C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起;通过访问限定符选择性的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化
【C++】类与对象入门部分完结