面向对象和面向过程的区别
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
类大小的计算
结构体内存对齐规则
第一个成员在与结构体偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数=编译器默认的一个对齐数与该成员大小的较小值。
VS中默认对齐数为8
结构体总体大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
参考之前写的文章:C进阶_结构体内存对齐-CSDN博客
除此之外,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
class和struct的区别
C++需要兼容C语言,所以C++中struct可以当成结构体来使用。另外,C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class类定义的类默认访问权限是private。
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显式地写出继承方式。
模板参数列表中,只有class和typename关键字,不存在struct。
this指针
C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问的。只不过所有操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针的特性
this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
只能在“成员函数”的内部使用。
this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针。
this指针是“成员函数”的第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
this指针可以为空,当我们调用函数时,如果函数内部不需要使用到this,也就是不需要通过this指向当前对象并对其进行操作时才可以为空(当我们在其中什么都不放或者在里面随便打印一个字符串),如果调用的函数需要指向当前对象,并进行操作,则会发生错误(空指针引用),就跟C语言中不能进行空指针的解引用。
8个默认成员函数
构造函数
概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
特性
构造函数是一个特殊的成员函数,需要注意的是构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
函数名与类名相同。
无返回值。
对象实例化时,编译器自动调用对应的构造函数。
构造函数可以重载。
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
C++把类型分为内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如int/char...自定义类型就是我们使用class/struct/union等自己定义的类型。编译器生成默认的构造函数会对自定义类型成员调用它的默认成员函数。C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都可以认为是默认构造函数。
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
注意:构造函数体内的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
析构函数
概念
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源清理的工作。
特性
析构函数是特殊的成员函数,其特征如下:
析构函数名是在类名前面加上字符~。
无参数无返回值类型。
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意!析构函数不能重载!
对象生命周期结束时,C++编译系统自动调用析构函数。
编译器默认生成的析构函数,会对自定义类型成员调用它的析构函数。
如果类中没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Data类;有资源申请时,一定要写,否则会造成资源泄露,比如Stack类。
拷贝构造函数
概念
拷贝构造函数即只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特性
拷贝构造函数也是特殊的成员函数,其特征如下:
拷贝构造函数是构造函数的一个重载形式。
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译直接报错,因为会引发无穷递归调用。
若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没有必要的。类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数一定要写,否则就是浅拷贝。
拷贝构造函数典型调用场景:
使用已存在的对象创建新对象;
函数参数类型为类类型对象;
函数返回值类型为类类型对象。
为了提高程序效率,一般对象传参时,尽量使用引用类型。返回时根据实际场景,能用引用尽量使用引用。
初始化列表
初始化列表就是一个以冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。
注意:
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数时)
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
赋值运算符重载函数
运算符重载
为了增强代码的可读性,C++引入了运算符重载。
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型operator操作符(参数列表)。
注意:
不能通过连接其它符号来创建新的操作符,如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的类型+,不可以改变含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针
应注意这些运算符不可以重载:
(1).
(2).*
(3)::
(4)sizeof
(5)?:
如果运算符重载成全局的,就需要成员变量是公有的。要保证封装性,可以使用友元函数,或者放弃使用全局重载,重载成成员函数。
赋值运算符重载
赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否给自己赋值
返回*this:要符合连续赋值的含义
赋值运算符只能重载成类的成员函数不能重载成全局函数,因为:如果赋值运算符不显示实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。也就是说,内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
注意如果类中未涉及到资源管理,赋值运算符是否实现都可以。但是一旦涉及到资源管理,就必须要用深拷贝去实现了。
前置++和后置++重载
前置++:返回+1之后的结果。因为this指向的对象函数结束后不会销毁,所以以引用方式返回提高效率。
Data& operator++(){
_day+=1;
return *this;
}
后置++:和前置++一样,都是一元运算符。C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。后置++是先使用后+1,因此需要返回+1之前的旧值,所以需要在实现时将this保存一份,然后给this+1
Data& operator++(int){
Data temp(*this);
_day+=1;
return temp;
}
const成员
将const修饰的“成员函数”称为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
const函数与非const函数的调用规则
在相同的函数参数及相同的名字的情况下,const函数与非const函数可以构成重载函数,但是const成员函数不能改变任何的非静态变量;
const对象默认调用const成员函数,非const对象默认调用非const成员函数;
在同时存在const函数和非const重载函数的前提下,若非const对象想调用const成员函数,则需要显示的转化,例如(const Student&)obj.getAge();
若const对象想调用非const成员函数,同理进行强制类型转换const_cast<Student&>(constObj).getAge();(注意constObj一定要加括号)
当类中只有一种函数存在的情况
非const对象可以调用const成员函数或者非const成员函数;
const对象只能调用const成员函数,若直接调用非const成员函数编译器会报错。
取地址及const取地址操作符重载
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况才需要重载,如:获取到指定内容。
友元
友元提供了一种突破封装的方式,可以提高便利。但是友元会增加耦合度,破坏封装,所以友元不宜多用。
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
注意:
友元函数可以访问类的私有和保护成员,但不是类的成员函数;
友元函数不能用const修饰;
友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
一个函数可以是多个类的友元函数;
友元函数与普通函数的调用原理相同。
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性
友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C是A的友元。
友元关系不能继承
static成员
概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量。用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
特性
静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明;
类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问;
静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
静态成员也是类的成员,受public、protected、private访问限定符的限制;
静态成员函数不能直接调用非静态成员函数,非静态成员函数可以调用静态成员函数。