目录
类
默认成员函数
构造函数
析构函数
拷贝构造函数
赋值运算符重载
运算符重载
赋值运算符重载
前置++和后置++重载
const成员
取地址及const取地址操作符重载
再谈构造函数
构造函数体赋值
初始化列表
explicit关键字
static成员
友元
友元函数
友元类
内部类
匿名对象
C 语言结构体中只能定义变量,在 C++ 中,结构体内不仅可以定义变量,也可以定义函数。C++兼容C语言,struct以前的用法C++依然可用,同时struct升级成了类,在 C++ 中更喜欢用 class 来代替 。
以栈为例:
typedef int DataType; struct Stack { void Init(size_t capacity) { _array = (DataType*)malloc(sizeof(DataType) * capacity); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _capacity = capacity; _size = 0; } void Push(const DataType& data) { // 扩容 _array[_size] = data; ++_size; } DataType Top() { return _array[_size - 1]; } void Destroy() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } DataType* _array; size_t _capacity; size_t _size; }; int main() { Stack s; s.Init(10); s.Push(1); s.Push(2); s.Push(3); cout << s.Top() << endl; s.Destroy(); return 0; }
这里注意我们如果将这里的struct变成class就会报错:
原因就是类的访问限定
类的访问限定符有三种:public(公有)、protected(保护)、private(私有)
说明:
1. public 修饰的成员在类外可以直接被访问2. protected 和 private 修饰的成员在类外不能直接被访问3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止4. 如果后面没有访问限定符,作用域就到 } 即类结束。5. class的默认访问权限为private,struct为public(因为struct要兼容C)
类的访问限定符为C++的封装奠定基础,C++的封装就是通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
类的定义:
1.声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
2.类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
(也就是说类外定义函数需要指明函数的类域)
类的实例化:用类类型创建对象的过程,称为类的实例化
1. 类是对对象进行描述的 ,是一个 模型 一样的东西,限定了类有哪些成员,定义出一个类 并没 有分配实际的内存空间 来存储它;2. 一个类可以实例化出多个对象, 实例化出的对象 占用实际的物理空间,存储类成员变量类对象的大小计算:类对象的大小只计算成员变量的大小,不算成员函数,成员函数被放在公共代码区,需要调用某个函数时不在每个实例化对象中找而是直接在公共代码区找。注意空类或仅有成员函数的类的大小,编译器会给一个字节占位来标识这个类的对象。
既然这里类里面的函数是放在公共区域的,那么不同的实例化对象调用同一个函数是如何产生不同的结果的呢?比如下面的Data类是如何让d1和d2得到不同的Print结果的?
这里实际上有一个隐藏的this指针
C++ 编译器给每个 “ 非静态的成员函数 “ 增加了一个隐藏 的指针参数,让该指针指向当前对象 ( 函数运行时调用该函数的对象 ) ,在函数体中所有 “ 成员变量 ” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成 。放在这里也就是说编译器将上面的代码悄悄转换成了如下图所示的样子:this指针的特性:1. this 指针的类型:类类型 * const ,即成员函数中,不能给 this 指针赋值。2. 只能在 “ 成员函数 ” 的内部使用,不能在形参和实参显示传递。3. this 指针本质上是 “ 成员函数 ” 的形参(因此存在栈中) ,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以 对象中不存储 this 指针 。4. this 指针是 “ 成员函数 ” 第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。
对比这两道题深入理解this指针
左边选C,右边选B
首先分析,这里的p->Print()不会发生解引用,因为Print函数不在p对象中,但是p会作为实参传递给this指针,虽然p是一个空指针,但第一个没有对this指针进行解引用,因此不会报错,但第二个需要访问_a,本质是this->_a,因此会运行崩溃。
构造函数 是一个 特殊的成员函数,名字与类名相同 , 创建类类型对象时由编译器自动调用 ,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次 。需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象 。其特征如下:1. 函数名与类名相同。2. 无返回值。(不需要手动补充void)3. 对象实例化时编译器 自动调用 对应的构造函数。这里构造函数的作用相当于取代了Init函数4. 构造函数可以重载(可能有多种初始化方式)。5. 如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。6. C++ 把类型分成内置类型 ( 基本类型 ) 和自定义类型。内置类型就是语言提供的数据类型,如:int/char... ,自定义类型就是我们使用 class/struct/union等自己定义的类型,编译器生成默认的构造函数会对自定义类型成员 调用它的默认成员函数,对内置类型不做初始化处理。(所以一般情况下,有内置类型成员,就需要自己写构造函数,不能用编译器自己生成的;全部都是自定义类型成员,可以考虑让编译器自己生成)注意: C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即: 内置类型成员变量在类中声明时可以给默认缺省值 。7. 无参 的构造函数和 全缺省 的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作 。析构函数 是特殊的成员函数,其 特征 如下:1. 析构函数名是在类名前加上字符 ~ 。2. 无参数无返回值类型。3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。4. 对象生命周期结束时, C++ 编译系统系统自动调用析构函数。这里析构函数的作用相当于取代了Destroy函数5. 编译器生成的默认析构函数,对自定类型成员调用它自己的析构函数。内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可。6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数;有资源申请时,一定要写,否则会造成资源泄漏。
拷贝构造函数 : 只有单个形参 ,该形参是对本 类类型对象的引用 ( 一般常用 const 修饰 ) ,在用 已存 在的类类型对象创建新对象时由编译器自动调用 。拷贝构造函数也是特殊的成员函数,其 特征 如下:1. 拷贝构造函数 是构造函数的一个重载形式 。2. 拷贝构造函数的 参数只有一个 且 必须是类类型对象的引用 ,使用 传值方式 编译器直接报错 ,因为会引发 无穷递归 调用。这里引发无穷递归的原因是C++规定自定义类型传参时,不同于 内置类型的直接拷贝,它 必须调用拷贝构造去完成,因此这里传参时陷入了循环。这里传参时加const可以避免函数内部修改出错导致原数据被修改。3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。注意:在编译器生成的默认拷贝构造函数中, 内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的 。4. 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。我们看看如果涉及到资源申请不重写拷贝构造函数会产生什么结果,我们依然拿栈举例:得到的运行结果如下:程序崩溃了,我们调试分析一下原因仔细观察这里的监视窗口不难发现这里拷贝成功了,但是,_array的地址是一样的,也就是说st1和st2中的_array指向的是同一块空间,这就导致程序结束时析构函数会调用两次,同一块空间不可以析构两次,因此会崩溃。所以这里需要进行深拷贝。
运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字 operator 后面接需要重载的运算符符号 。函数原型: 返回值类型 operator 操作符 ( 参数列表 )
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载
- 是否重载运算符才需要看这个运算符对这个类是否有意义
为了引出我们的运算符重载,我们依然以Date类举例,对于简单的内置类型比如int,double要想比较大小,可以直接使用> < ==这些运算符直接进行比较,那么如果我们对Date这样的自定义类型进行比较,应该怎么做呢,首先想到的是单独写一个比较函数
这样当然可以解决问题,但是不同的人命名风格不同,函数名未必清晰,因此存在不便,如果可以让自定义类型可以像内置类型那样d1
这里依然有个问题,这里的<重载在了类外面,如果成员变量私有,那么这个运算符重载是错误的,因为它访问不到类里面的成员变量,虽然可以使用后续的友元进行访问,但是友元会破坏封装因此一般情况不使用友元。当然这里可能会想到直接将运算符重载作为成员函数,但是依然会报错:
这里的报错原因给的是此运算符函数参数太多,原因就是隐含的this指针,也就是说这里实际有三个参数,但是运算符重载必须是几个操作数对应几个参数,因此这里应该只有两个操作数,两个参数,因此需要做如下修改就通过了:
赋值运算符什么场景可能涉及呢,比如这里的日期类,如果我需要将d2的值给d1,那么就需要d1=d2,相当于实现一次拷贝。
注意这里区别于拷贝构造,拷贝构造是用一个已经存在的对象初始化另一个对象,本质是构造函数;而这里是已经存在的两个对象之间复制拷贝,本质是运算符重载函数。
这是最简单的赋值运算符重载,它存在一些问题,首先这当然只是值拷贝(浅拷贝),当然这里是日期类,值拷贝就可以了;可能存在一种自己给自己赋值的场景:d1=d1;还有一个点是这里没有返回值,也就是说,如果我要连续赋值,这样的函数实现不了:d3=d1=d2。
因此这里做出如下改动。
1.赋值运算符只能重载成类的成员函数不能重载成全局函数。原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。2.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝 。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
前置++:返回+1之后的结果
注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率。后置++:前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。
注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1,而temp是临时对象,因此只能以值的方式返回,不能返回引用。
如果我们在d2前加const修饰,那么d2调用Print函数时会出现错误,这里的报错原因实质是权限的放大,这里明确写可以写成:d1.Print(&d1); d2.Print(&d2); 这里对于d1,取地址传的是Date*,而形参隐含的this也是Date* ,所以属于权限的平移,而d2取地址传的是const Date*,属于权限的放大。(权限只能缩小不能放大)
要想满足要求,只需要将这里的this转换为const Date*即可,修改方式如下:(这样修改对于d1属于权限的缩小,d2属于权限的平移)
将 const 修饰的 “ 成员函数 ” 称之为 const 成员函数 , const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中 不能对类的任何成员进行修改。所以如果某个函数不需要对成员变量进行修改,最好加上const(普通对象可以传,const对象也可以传),但是如果需要对成员变量进行修改则不能加const。
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。一般使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值 ,而不能称作初始化。因为 初始化只能初始 化一次,而构造函数体内可以多次赋值 。
初始化列表(对象的成员定义的位置):以一个 冒号开始 ,接着是一个以 逗号分隔的数据成员列表 ,每个 " 成员变量 " 后面跟一个放在括号中的初始值或表达式。【注意】1. 每个成员变量在初始化列表中最多 只能出现一次 ( 初始化只能初始化一次 )。2. 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量(必须在定义时初始化)const成员变量(必须在定义时初始化)自定义类型成员( 且该类没有默认构造函数时 )类里面的成员变量只是进行了声明,并不叫定义,真正定义发生在实例化对象时整体定义,要想单独定义只能发生在初始化列表中。内置类型不在初始化列表显示给定初始值是不做处理的,除非在声明时给定缺省值(C++11打的补丁)。如果显示初始化即使给定缺省值也不会用缺省值。
虽然初始化列表大部分初始化工作都可以完成,但是并不代表函数体赋值没有用,比如:
我们可能遇到这样的情况:
aa1是构造,aa2是隐式类型转换(整型转换成自定义类型),这里隐式类型转换会产生一个临时对象,这个临时对象是利用2调用构造函数产生的,然后这个临时对象再用拷贝构造得到aa2,也就是说这里会涉及一次构造函数,一次拷贝构造,编译器会优化这里的过程(同一个表达式中连续的构造编译器一般都会优化),直接用2构造得到aa2,可能你会怀疑这里不存在这个所谓的临时变量,而是直接构造的,ok用下面的例子就可以证明是否存在这个临时变量:
对比这两个图可以发现,第一个报错原因是无法进行类型转换,解决方法是在aa3类型前加const,运行结果中发现调用了一次构造函数,这说明aa3=2这个表达式中构造过对象,aa2可以优化是因为存在连续的构造,但这里没有连续构造,因为aa3是引用,因此不存在优化,所以说明这里确实存在临时对象,我们知道临时对象具有常性,因此加const就可以转换过去。
这里提到这个隐式类型转换的目的就是为了引出我们的explicit关键字,如果我们不希望这里的隐式类型转换的发生,那么我们就可以用explicit修饰构造函数:
声明为 static 的类成员 称为 类的静态成员 ,用 static 修饰的 成员变量 ,称之为 静态成员变量; 用 static 修饰 的 成员函数 ,称之为 静态成员函数 。静态成员变量一定要在类外进行初始化
特性1. 静态成员 为 所有类对象所共享 ,不属于某个具体的对象,存放在静态区2. 静态成员变量 必须在 类外定义 ,定义时不添加 static 关键字,类中只是声明3. 类静态成员即可用 类名 :: 静态成员 或者 对象 . 静态成员 来访问4. 静态成员函数 没有 隐藏的 this 指针 ,不能访问任何非静态成员5. 静态成员也是类的成员,受 public 、 protected 、 private 访问限定符的限制6. 静态成员函数不可以调用非静态成员函数,但是非静态成员函数可以调用静态成员函数
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。友元分为: 友元函数 和 友元类
友元函数 可以 直接访问 类的 私有 成员,它是 定义在类外部 的 普通函数 ,不属于任何类,但需要在类的内部声明,声明时需要加friend 关键字。说明:友元函数 可访问类的私有和保护成员,但 不是类的成员函数友元函数 不能用 const 修饰友元函数 可以在类定义的任何地方声明, 不受类访问限定符限制一个函数可以是多个类的友元函数友元函数的调用与普通函数的调用原理相同
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。友元关系是单向的,不具有交换性。比如 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time 类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。友元关系不能传递。如果 C 是 B 的友元, B 是 A 的友元,则不能说明 C 时 A 的友元。友元关系不能继承。
概念: 如果一个类定义在另一个类的内部,这个内部的类就叫做内部类 。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。注意:内部类就是外部类的友元类 ,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。特性:1. 内部类可以定义在外部类的 public 、 protected 、 private 都是可以的。2. 注意内部类可以直接访问外部类中的 static 成员,不需要外部类的对象 / 类名。3. sizeof( 外部类 )= 外部类,和内部类没有任何关系。
匿名对象特点是不用取名字,但 生命周期只有当前行 ,下一行他就会自动调用析构函数,但是c onst可以延长它的生命周期 ,引用的生命周期在哪它就在哪。匿名对象具有常性不能像A aa1(); 这样定义对象,因为编译器无法识别这是一个函数声明,还是对象定义。