类和对象
面向过程和面向对象
面向过程
- 函数堆积(在乎的是过程)
面向对象
- 对象之间的交互(在乎的是模块)
面向过程和面向对象的关系()
- 例如:蛋炒饭于盖浇饭的区别
类的定义
Class 类名
{
函数;
变量
};
- 类里面的函数可以在类里面进行定义
- 缺点:多次引用的时候会出现函数重复定义
- 有点:但是编译器很有可能会将它定义为内联函数
- 类的声明(函数的声明)在
.h
头文件里,类的定义(函数的定义)在.c
文件里
类的封装定义
类+访问限定符 就叫封装
- 访问限定符:
- public
- private 在类外不能被一个对象直接访问
- proteced 在类外不能被一个对象直接访问
不加访问限定符的class类成员默认都是private的。
不加访问限定符的struct成员默认都是public的,为了和c语言的struct的性质兼容。
通过指针的方式可以修改任何限定符的限定的成员。
注意:访问限定符只在编译的时候有用,但是在运行时时不起作用的(最常用的体现方式就是我们通过public的函数来修改private的变量成员)
编译器如何编译一个类
- 识别类名
- 识别类中成员变量
- 识别类中的成员函数并对成员函数进行修改
- 每个的函数加上一个this指针形参
- 在调用的时候编译器会将此形参自动传参,但大部分的this指针式通过寄存器传参的,而可变参数函数是通过压栈的方式传参的。
this指针
- 指向当前对象,在当前类函数中使用
- 当前对象中并不包含this指针
- this类型:类类型* const,所以不能人为的置空
- this是成员函数第一个默认参数,由编译器通过ecx(thiscall)寄存器来自动传递
- 不定参数函数的this参数是通过压栈传递的(_cdecl)
有如下坑人的代码:
class Test
{
public:
void TestF()
{
this->a = 0;
}
private:
int a;
};
int main()
{
Test *t = NULL;
t->TestF();
}
//问:此代码为什么会崩溃?
你可能认为是因为
t
指针是空的,所以访问其成员函数会出错。
这样想的话,你就大错特错了,首先在调用TestF函数的时候是通过这样的方式调用的:
Test::TestF(t);
而根本就不会去解引用t指针,所以在调用的时候是不会出错的,出错的地方在调用函数之后,对形参this进行解引用,此时的this就是实参t,所以在函数内部解引用一个空指针会出错。
类的大小的计算
类成员变量相加(注意内存对齐)
类成员在内存中的存储:
- 成员1
- 成员2
- …
可见类中成员在内存中的存储并没有存储类的成员函数。
一种初始化方式
int main()
{
int a=0;
int b(0);//与int b = 0是一样的效果
}
这种初始化的方式也可以对对象初始化,调用的是拷贝构造函数。
默认的六个成员函数
构造函数
- 创建一个对象的时候编译器会自动执行的一个函数(此函数执行完成之后,此对象才算创建完毕)(thiscall调用约定)
- 构造函数的函数名和类名相同,并且不能设置返回值
- 构造函数在整个对象的生命周期只能被调用一次
- 构造函数可以在类中定义,也可以在类外定义
- 构造函数可以重载
- 定义类的时候,编译器会给它合成一个缺省的构造函数(说明每个类都有构造函数),并且此构造函数没有参数,也什么都不干。如果用户定义了构造函数,编译器则不会生成缺省的构造函数
- 全缺省参数的构造函数和无参的构造函数都叫做缺省构造函数,并且这种构造函数只能存在一个
- 构造函数的函数体里面是赋值,不是初始化。赋值可以是多次,但初始化只能是一次
构造函数的初始化列表(完成各个变量的初始化,如果不加,编译器会加上默认的参数初始化列表):
class Test { public: Test(int arg1, int arg2) : _arg2(arg2)//注意是冒号 , _arg1(arg1) private: int _arg1; int _arg2; }
- 注意:初始化列表中各个成员变量的出现按先后次序无关,按照成员在类中的声明次序进行初始化
- 建议:初始化列表中的而成员次序与成员在类中的声明 次序保持一致
用以下方式进行初始化:
Test t1(1,2); //会将1先赋给arg1,将赋给arg2,然后在类中成员变量中找出声明的顺序, //在通过初始化列表找到要初始化的值,也就是先将arg1赋给成员变量_arg1, //然后将arg2赋给成员变量_arg2
在初始化列表中使用this指针会失败的原因:因为还没有初始化完成,所以这个对象还不完整,所以不能使用this指针
那些成员必须在初始化列表中进行初始化?
- 如果类的成员中有引用,那么此类在创建对象的时候,只能用构造函数的初始化列表进行初始化。
- 如果类中有被const修饰的成员变量
- 如果类的成员变量中有另一个类的对象(该类中的构造函数含有非缺省的参数)。
析构函数
- 销毁一个对象(即对象的生命周期到了)的时候编译器会自动执行一个函数(thiscall调用约定)
- 析构函数的函数名是在类名的前面加上一个
~
符号,并且既不能设置参数也不能设置返回值 - 析构函数在整个对象的生命周期只能被调用一次
- 析构函数可以在类中定义,也可以在类外定义
- 析构函数不可以重载,因为析构函数没有参数
- 定义类的时候,编译器会给它合成一个缺省的析构函数(说明每个类都有析构函数),并且此析构函数没有参数,也什么都不干。如果用户定义了析构函数,编译器则不会生成缺省的析构函数
- 析构函数的调用顺序和对象创建的顺序相反(因为是对象是在栈里面,所以需要符合栈的特性)
拷贝构造函数
- 单参数,并且是类类型的引用(原因如下:)
- 如果不传引用可以通过编译,那么传的引用为传值
- 传值的时候因为实参是一个对象类型,所以,先得生成实参对象,而生成实参对象的方式就是通过拷贝构造函数。
- 而拷贝构造函数是通过传值的,所以还需要创建形参对象。。。。形成无休止的递归。
- 因此拷贝构造函数只能传类类型的引用。
- 创建对象时,使用同类对象来进行初始化
- 拷贝构造函数是拷贝类中的所有成员变量,包括栈中数组的每个元素,原封不动的拷贝
- 如果类中有指向堆里面的指针,在拷贝之后两个对象将会指向同一块堆空间,那么在销毁的时候就可能会出现两次free同一块空间。
运算符重载
将一个对象之家二赋给相同类的另一个对象是可以的(直接用等号相连),其默认的作用和默认的拷贝构造函数一样,所以也有可能造成多次释放堆空间,并且赋值不会释放原有的空间,造成内存泄露。
- 运算符重载时操作数的个数必须和此运算符的操作数的数量相同(注意重载时候编译器加入的this指针,即this为双操作树运算符的左操作数)
- 运算符也可以重载在普通函数的位置,但是不会有this指针,所以应注意操作数的个数,并且操作数至少有一个类类型的对象。
- 重载运算符不会改变运算符的优先级,和运算方向。
- 运算符重载时,运算前后值不需要变化的操作数最好加上const,并且为了更小的开销,传参的时候最好传引用,返回类类型变量的时候最好也是返回引用。
注意两种++运算符的重载。
//前置++ Date& operator++() { this->_day += 1; return *this; } //后置++ Date operator++(int) { Date tmp(*this); this->_day += 1; return tmp; }
输出运算符
<<
的重载在类中建立友元关系:
friend ostream& operator<<(ostream& _cout, const Date& d);
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout<< d._year << “-” << d.mouth << “-” << d.day;
return _cout;
}
普通类型的取地址符的重载
const operator&()
{
return this;
}
const修饰的取地址符的重载
const operator&()const
{
return this;
}
友元函数
在类中声明 friend [函数声明];
在友元函数中可以访问此类中的私有成员。
一个函数可以式多个类的友元函数
友元类
在类中声明 friend [class 类名]
在友元类中可以访问声明所在类中的私有成员
友元关系不可传递
友元关系是单向的
友元的优点:
- 减少了开销
友元的缺点:
- 破坏了类的封装特性
const关键字
const修饰普通类型的变量使变量具有常量属性,并且是在编译的时候进行替换
const修饰类的成员函数,在此函数中不可以修改类的成员变量
成员函数+const = 成员函数的this指针前面加上const
class Test { public: void TestConst()const { ... } private: ... }
以上的成员函数在编译器编译后加上this指针后会变成如下
void TestConst(Test *cosnt this)const
因为成员函数加上const修饰代表其函数内不能修改类成员变量的值,所以就可以用一下方式替代
void TestConst(const Test *const this);
cosnt修饰的对象只能调用const修饰的类的成员函数。
- 因为const修饰的对象意思就是此对象的成员变量不可修改,但是如果调用非const修饰的成员函数,而此成员函数就有可能修改对象的成员变量,所以为了安全,const修饰的对象不能调用const修饰的类成员函数。
- cosnt修饰的成员函数本来就是不让改变成员变量,所以const修饰的对象可以调用const修饰的成员函数。
非const修饰的对象可以调用cosnt修饰的成员函数
如果想要指定的const成员函数修改某个成员变量,可以跟此成语变量前面加上mutable
关键字(这样使const修饰的对象变得又不安全了)
函数返回指针的时候,如果不希望此指针指向的内容被修改,那么就需要返回的指针类型为const 指针。
返回被const修饰的形参的时候,返回值的类型必须被const修饰
构造函数不可以使用const修饰
友元函数不能用const修饰
explicit修饰构造函数会抑制构造函数的类型隐式转换。
编译器感觉自己需要的时候才会合成默认的构造函数(和前面讲的有点矛盾)
static修饰类成员
static修饰的成员变量叫做静态成员,静态成员为此类的所有对象共享。
静态成员需要在声明的时候加上static
,定义的时候不需要加,如果要成员函数的定义在类里面的时候直接讲static加在函数之前即可。
静态成员变量在类外可以通过类名::成员变量
来访问
static修饰类成员变量(静态成员变量):
- 在类中使用
static 类型 变量名
来声明静态成员变量,可以在三个访问限定符的任何一个之中,也可以使用const
来修饰。 - 静态成员必须在类外定义并初始化,并且不需要加
static
用类型 类名 变量名=初始化值
来进行初始化。 - 类的大小不包含静态成员变量。
- 静态成员变量不能房子初始化列表总进程初始化,因为初始化列表是初始化类的成员变量。
static修饰成员函数(静态成员函数):
- 在类中直接在函数前面加上static即代表此函数是静态成员函数。
- 静态的成员函数不能访问非静态的成员变量。
- 静态的成员函数只能调用静态的成员变量和静态的成员函数。
- 非静态的成员函数可以调用静态的成员函数和静态成员变量。
- 静态成员函数不能访问非静态的成员函数和变量,也不能用const修饰,因为静态的成员函数没有编译器自动传递的 this 指针。
类的设计
- 根据问题的解决方法设计数据模型。
- 根据对数据模型的抽象(包含什么样的数据,以及方法)
- 对类进行实例化(创建对象)。
- 实现封装:
- 对类的细节进行封装(打包成整体)
- 对象与对象之间进行交互(将相应成员进行设置)