1.类的定义在c语言中我们使用struct定义结构体以封装多个类型的变量来表达一个复杂的结构,在c中除了struct可以继承c语言的语法,还可以使用class来定义一个类.在类中可以定义成员变量和成员函数.1.1类定义格式class和struct都可以作为类的关键字.格式为:classname(){};(注意不要省略定义结束后面的分号)类中的内容叫做类的成员,类中的变量叫做类的属性或者成员变量;类中的函数称为类的方法或者成员函数.通常情况下为了区分类的成员变量所以在前面加上_.和c语言不同的是有时候我们不希望别人访问类中的某些成员,因此引入了访问限定符.定义在类里面的成员函数默认为inline1.1.1inline-内联函数与编译链接1.预处理:预处理是所有环节中最粗暴简单的,它只负责扫描代码中的#include,#define并把它们粘贴到当前文件中.2.编译:编译器拿到预处理后的c代码,进行词法 语法 语义分析然后生成一个地址尚未确定的(.o/.obj)文件,随后对内联函数展开进行优化.程序在内存中就是一个地址列表,写出来的每一行程序都有对应的存储地址.而每一行指令在内存中的存储不是顺序的,但cpu的执行顺序是顺序的.因此在编译阶段,编译器对每个函数都生成一份机器码存放在.o文件中然后生成一个call地址命令,在执行时根据call到的地址进行跳转等执行完毕通过RET返回指令继续往下执行.由于此时并不知道其具体地址所以call的地址那一栏会先用0占位,等到链接阶段再补上具体的地址,这叫做重定位/地址绑定除此之外,编译器还会根据是否为内联函数给每一段机器码添加强/弱符号:普通函数添加强符号,内联函数添加弱符号.3.链接:链接器发挥作用,把先前编译阶段生成的.o文件,#include包含的库文件等所有文件目标缝合在一起,随后处理它们之间的链接关系,在call指令中填充相应地址,根据强弱符号来去除重定义或者干脆报错.这里就不得不先简单解释一下c的单一定义原则(ODR)单一定义原则明确规定:在整个程序中非内联(普通)函数,全局变量,只能有且仅有一处定义.之前在编译阶段添加的强弱符号就是用来解决这种问题的.强弱符号是与.o中的机器码(函数定义)绑定的,所以程序中出现两个强符号时就意味着出现了两份定义,此时编译器会直接报错multiple definition of ‘xxx’(多重定义).当出现两个弱符号时却会丢掉其中一份,不报错,这也是inline内联函数的重要作用.那么inline在这些过程中又干了什么呢?inline是在编译阶段开始发挥作用的:编译器会先判断这个内联函数是否可以被展开,如果代码量少且简单,那么编译器会直接把这段代码粘贴到调用的地方,不会生成call指令.如果代码很长,复杂或者使用了递归就不展开.此时按照正常函数来走,call它的地址.第二步,在.o文件中对每一段机器码添加强弱符号,如果写了inline就添加弱符号.在链接阶段,如果出现两个弱符号的情况就删掉一份.(这里删掉一份定义不影响调用,因为删掉重复的弱符号定义不影响调用因为链接器留一份就够了用于给未展开的 CALL 填地址。如果代码已经展开连这一份都不需要)inline的重要作用是防止重定义,但是为什么会有重定义情况发生呢?虽然在同一个.cpp中头文件被限制只能包含一次,但是对不同文件却没有这个限制.那么我在main1.cpp中包含一次在main2.cpp中又包含一次,这样一个程序中就有了两份内联函数体,恰好都有弱符号标志才能够只保留一份避免重定义冲突.由此可见,inline的意义在于使得把函数定义写在头文件中的行为变得合法,如果没有inline,一个普通函数的定义写在头文件中必然会发生冲突.了解了以上这些知识,就能够理解inline的正确用法:inline关键字必须要和函数定义绑定在一起,并且通常写在头文件中如果声明与定义分离并且inline与声明放在一起:(代码简单这里直接用截图了)1.预处理—main.cpp 里通过 #include把 inline void func(); 这一行文本复制了进来。utils.cpp 里也一样复制了这行声明紧接着是自己定义的 void func() { … }。此时没有任何异常两个 .cpp 文件都拿到了这个声明。2.编译—utils.cpp这里的函数定义前面没有 inline 关键字.编译器判定规则极其死板只要定义处没有 inline这就是一个“普通函数”。既然是普通函数编译器必须为它生成一份完整的机器码并且打上的标签是强符号.编译 main.cpp:编译器看见了 inline 关键字但它扭头去找 func 的大括号 {} 在哪里。虽然写了 inline但你没把函数体给我看我根本不知道里面有几行代码我无法展开。所以我只能无视这个 inline把它当成一句普通的‘函数声明’,随后编译器只能生成标准的 CALL 调用指令.3.链接—直接当普通函数处理找地址.如果声明与定义分离并且都写了inline:1.预处理—复制粘贴2.编译—编译 utils.cpp,inline函数生成一份弱符号,但是由于编译器优化发现在这个文件内没有调用于是干脆丢掉了机器码.编译 main.cpp:发现调用,结果没找见定义只能生成call地址3.链接—根据call地址去utils.cpp中找定义,但是由于编译器优化把机器码直接丢掉了,找不到定义,直接报错.1.2类域使用类有什么好处呢?其一是在不同的类中可以有相同名称的成员函数成员变量,访问一个重名变量/函数时用::指定其类域即可.此前我们接触过全局变量和局部变量的概念,准确的来说是全局域和局部域,这两者间的规则是:局部域可以访问全局,但全局无法访问局部域中的局部变量,如果局部和全局重名,在局部域内会优先使用局部变量.类定义了一个新的作用域,类所有的成员都在类的作用域中.在类体外访问类中的成员时需要使用::作用域操作符指明成员属于哪个类域.当使用::时,编译器会忽略任何查找规则直接跳转到指定类域查找.当声明与定义分离时需要在定义前也指定类域否则编译器会把这个函数当作全局函数,那么编译时一旦在全局找不到函数中使用的成员就会报错,而指定类域后就会直接跳转到类域寻找.1.3访问限定符为了防止c语言中只要突破作用于限制就能随意访问修改成员的情况,c在类域中还添加了访问限定符.访问限定符是c实现封装的方式,通过访问限定符选择性地将接口提供给外部的用户使用.访问限定符分为三种:public,private,protected. public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接访问(可以通过写一个get函数或者在类中添加友元间接访问)访问权限作用域从该访问限定符出现到遇到下一个访问限定符为止,如果后面没有访问限定符就直到类结尾结束.class中没有被访问限定符修饰时默认private,struct则默认为public通常情况下我们不希望类外随便访问类中的成员变量,所以将成员函数设置为public,成员变量设置为private2.实例化2.1实例化概念在类中的成员函数可以是声明或者定义,那么类中的成员变量是声明还是定义呢?其实类中的成员变量是没有开辟空间的,只是声明这个类中有哪些成员,就像房子的图纸一样,图纸不能住人尽管上面标注很详细,但只要盖好了房子就能住人了.classDate{...private:int_year;int_month;int_day;};Date d1;我们把创建这样一个Date类类型的d1对象叫做类的实例化.此时d1就按照Date中的变量开辟了空间,成员变量有了定义.2.2对象大小那么类对象是怎么计算大小的呢?首先,成员变量肯定要计算大小并且要符合对齐规则:第一个成员在与结构体偏移量为零的地址处其他成员变量要对齐到对齐数的整数倍地址处对齐数编译器默认对齐数和该成员大小的较小值(vs中默认对齐数为8)结构体总大小为最大对齐数的整数倍(如果有嵌套情况就先算嵌套部分的大小然后按整体代入大的部分)类中的成员函数算不算大小呢?其实是不算的.因为成员函数本质上是一串指令,地址是固定不变的,而且这个地址也不需要存储,编译时的call指令能找到他们的地址classA{public:voidfunc(inta){...}};classB{};但是像上面这里两个类,成员函数不计算大小,那他们的大小都是0?实际上,为了证明这两个类存在过所以给它们分配了1字节空间用于占位标识.3.this指针在成员函数中难免会使用到类中的成员变量,那么成员函数是怎么访问的成员变量?成员函数是怎么知道自己的操作对象是哪个呢?c使用一个隐含的this指针解决问题编译器编译后类的成员函数默认都会在第一个形参位置生成一个该类类型的this指针用于传递调用此成员函数的类对象,例如一个类对象的init函数voidInit(Date*constthis,intyear,intmonth,intday){this-_yearyear;this-_monthmonth;this-_dayday;}函数中访问该对象的成员变量实际上是通过this指针访问的,后置const用于修饰this保证指向的对象不会被改变.c规定在形参中this指针的传递由编译器完成,不允许手动显式添加,不过在函数体内部可以使用(为了方便,通过this调用成员变量的过程可以省略,直接使用即可)下面程序运行结果是:A.编译报错B.运行崩溃C.正常运行#includeiostreamusingnamespacestd;classA{public:voidPrint(){coutA::Print()endl;}private:int_a;};intmain(){A*pnullptr;p-Print();return0;}首先,空指针解引用并非语法问题,不会编译报错.这里其实并不是对p指针解引用,因为p指针是个类类型指针,而print函数存在代码段里,类中没有print函数所以不是解引用,本质上还是把p指针传递给this,然后在函数中使用p指针.又因为函数中仅仅是输出语句不涉及this指针§的解引用所以正常运行,选C#includeiostreamusingnamespacestd;classA{public:voidPrint(){coutA::Print()endl;cout_aendl;}private:int_a;};intmain(){A*pnullptr;p-Print();return0;}这里和上一题类似,只不过在函数中访问了成员变量_a,需要通过this指针解引用,而this指针是传递的空指针p空指针解引用会导致运行崩溃,选B4.类的默认成员函数默认成员函数就是用户没有显式实现,编译器自动生成的成员函数称为默认成员函数.一个类在我们不写的情况下会生成6个默认成员函数.c11以后还会增加两个默认成员函数,移动构造和移动赋值学习默认成员函数需要分成两个方面:我们不写时编译器默认生成的函数行为是什么?如果编译器默认生成的函数不满足我们的需求,该怎么实现?1.构造函数构造函数的作用相当于Init初始化函数,在对象实例化时自动调用来初始化对象.构造函数函数名和类名相同,无返回值(连void也不需要写),实例化时自动调用构造函数可以根据需求重载如果没有显式写构造函数,编译器默认生成的构造函数对内置类型不做处理,对自定义类型的变量会调用它的默认构造,如果没有或者不符合条件就会报错无参构造函数,全缺省构造函数,编译器自动生成的构造函数都叫做默认构造函数(不传实参就能调用的函数就叫构造函数)// 1.⽆参构造函数Date(){_year1;_month1;_day1;}// 2.带参构造函数Date(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}// 3.全缺省构造函数Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}构造函数分为两个部分,初始化列表和定义部分.在定义部分可以看成是对成员变量的赋值,但是有一些特殊的变量,比如引用变量,const修饰的变量,它们在初始化的时候就需要绑定一个对象,不能在定义部分赋值修改.所以c干脆搞了个东西叫做初始化列表,统一让所有变量都在初始化列表进行定义初始化,这样就可以定义那些特殊变量了.初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式Date::Date(intyear,intmonth,intday)//当函数声明和定义分离时,默认参数只能出现一次.//通常情况下只在函数声明中指定默认参数,而在函数定义中不指定.:_year(year),_month(month),_day(day){}每个成员变量在初始化列表中只能出现一次引用成员变量,const成员变量,没有默认构造的类类型变量必须放在初始化列表c11支持在成员声明的位置给缺省值,主要是给没有显示写在初始化列表的成员使用的.尽量使用初始化列表初始化,因为所有变量都会走初始化列表初始化顺序按照类中成员变量的声明顺序进行下面程序的运行结果是什么()A.1 1 B.2 2 C.编译报错 D.1 随机值 E.1 2 F.2 1#includeiostreamusingnamespacestd;classA{public:A(inta):_a1(a),_a2(_a1){}voidPrint(){cout_a1 _a2endl;}private:int_a22;int_a12;};intmain(){Aaa(1);aa.Print();}解析:根据初始化列表的规则可知成员变量走初始化列表的顺序是根据声明顺序进行,也就是说针对这个类,走初始化列表的顺序是先进行a2然后进行a1.当我们使用a1初始化a2的时候,此时a1还没有进行初始化,也就是说里面是随机值.所以a2也是随机值.然后再初始化a1.a1使用a来初始化,主程序中给a传递的参数是1所以a1的值是1答案应该选D2.析构函数析构函数相当于destory函数用于释放前面调用的资源,因此通常情况下不需要写,只有当函数动态开辟资源后才需要写析构函数析构函数在对象生命周期结束后自动调用析构函数名是在类名前面加上字符~,没有参数没有返回值(同样也不需要加void)一个类只能有一个析构函数,如果不写编译器自动生成,自动生成的析构函数对内置类型不做处理,对自定义类型会调用它的析构函数一个局部域的多个对象,后定义的先析构~Stack(){cout~Stack()endl;free(_a);_anullptr;_top_capacity0;}3.拷贝构造函数当我们初始化一个对象时所用的参数为一个该类的对象时,就需要使用拷贝构造函数.拷贝构造函数是构造函数的一个重载,所以特性与构造函数相同拷贝构造的第一个参数必须是类类型对象的引用,如果不使用引用在语法逻辑上会引起无限递归直接报错,后面的参数必须有缺省值c规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造完成(本质都是拷贝一份对象返回然后销毁原对象,这个临时拷贝的对象具有常性所以如果要对传值返回的临时对象进行引用需要const修饰)DateDate::func1(){return*this;}如果不写拷贝构造编译器会自动生成,自动生成的拷贝构造对内置类型成员变量会完成浅拷贝,对自定义类型的成员变量会调用它的拷贝构造.也就是说,如果在类中没有开辟动态空间就没必要写拷贝构造,浅拷贝满足我们的需求,但是如果类中有变量指向了一块空间就必须手写进行深拷贝,否则浅拷贝会使两个变量共用一块空间.小技巧:如果一个类显式写了析构函数释放资源那么就需要写拷贝构造4.赋值运算符重载4.1运算符重载为什么我们可以随意使用,-,*,/这些运算符号呢?因为对于内置类型来说,基本运算是有规律可循的,编译器已经通过穷举把一系列内置类型的基本运算都写了重载函数,但是自定义类型如何运算编译器并不清楚,这就需要引入运算符重载让我们自己来写.c规定类类型对象使用运算符时必须转换成调用对应运算符重载,如果没有对应的运算符重载就会编译报错.运算符重载是具有特殊名字的函数,是由operator和后面要定义的运算符共同构成.和正常函数一样有返回类型参数列表以及函数体重载运算符函数的参数个数和该运算符作用的运算对象数量一样多.一元运算符有一个参数,二元运算符有两个参数.二元运算符的左侧运算对象传给第一个参数右侧运算对象传给第二个参数如果一个重载运算符函数是成员函数则它的第一个对象默认传给隐式的this指针。因此运算符重载作为成员函数时参数比运算对象少一个。而在重载流插入和流提取运算符时,由于左侧对象是istream和ostream,而右侧对象才是类类型的对象.如果重载为类的成员函数,那么istream和ostream将会传递给this指针,而右侧的类类型对象会传递给istream和ostream的引用.这样就本末倒置了,因此流插入和流提取运算符需要重载为全局函数才能让两个参数的类型对应.运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致不能通过连接语法中没有的符号来创建新的操作符比如operator,此外C还规定.* :: sizeof ?: .以上五个运算符不能重载重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义一个类需要重载哪些运算符是看哪些运算符重载后有意义重载运算符时有前置和后置之分,为了方便区分,后置重载时增加一个int形参跟前置构成函数重载.但是这里的int形参也只是为了方便区分在实际使用时不需要传递任何参数4.2赋值运算符重载赋值运算符重载是一个运算符重载规定必须重载为成员函数赋值运算符重载的参数建议写成const当前类类型的引用否则会传真传参调用拷贝构造其实在通常情况下一般建议都写成const当前类类型的引用不仅可以减少拷贝构造而且不用担心权限的放大问题返回值建议也写成当前类类型的引用以提高效率.返回值的目的是为了支持连续赋值场景如果不写编译器会自动生成赋值运算符重载.和默认拷贝构造函数类似对内置类型成员会完成浅拷贝,对自定义类型成员变量会调用它的赋值重载没有就会报错.如果一个类类型的成员变量中有指向资源编译器自动生成的赋值运算符重载的浅拷贝不符合我们的需求因此需要手写赋值运算符重载,析构函数,拷贝构造需要手写的情况相同–都是因为类类型实例化对象时开辟了动态空间因此这三个往往都需要一起写5.取地址运算符重载我们都知道this指针中有一个隐含的const目的是修饰this使其指向的对象不被随意修改那如果我们想修饰*this使该对象不被随意修改该怎么做呢?我们将const修饰的成员函数称为const成员函数const修饰成员函数放到成员函数参数列表的后面const实际修饰该成员函数隐含的this指针表示在该成员函数中不能对类的任何成员进行修改取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载一般这两个函数编译器自动生成的足够我们的需求不需要显示写5.类型转换在C语言中我们有时候会用到不同类型之间的转换比如int类型转换成float类型,float类型转换成int类型.这些都属于内置类型的转换,C也支持这一点.那内置类型可不可以和类类型之间进行转换?C支持内置类型隐式类型转换为类类型对象,但这需要有相关内置类型为参数的构造函数.类类型的对象之间也可以隐式转换,需要相应的构造函数支持.简而言之就是要想在两个类型之间进行转换他们必须要通过函数建立联系这样才能在隐式转换的时候自动调用#includeiostreamusingnamespacestd;classA{public:// 构造函数explicit就不再⽀持隐式类型转换// explicit A(int a1)A(inta1):_a1(a1){}//explicit A(int a1, int a2)A(inta1,inta2):_a1(a1),_a2(a2){}voidPrint(){cout_a1 _a2endl;}intGet()const{return_a1_a2;}private:int_a11;int_a22;};classB{public:B(constAa):_b(a.Get()){}private:int_b0;};intmain(){// 1构造⼀个A的临时对象再⽤这个临时对象拷⻉构造aa3// 编译器遇到连续构造拷⻉构造-优化为直接构造A aa11;aa1.Print();constAaa21;// C11之后才⽀持多参数转化A aa3{2,2};// aa3隐式类型转换为b对象// 原理跟上⾯类似B baa3;constBrbaa3;return0;}这里的两个隐式类型转换的本质都是利用两个类型之间的关系的函数先传参构造一个临时对象,然后再用临时对象进行拷贝构造.当然编译器在这里也是存在优化的在遇到连续构造和拷贝构造的时候会优化为直接构造6.static成员成员变量和成员函数都可以用static进行修饰.用static修饰的成员变量称为静态成员变量,被修饰后静态成员变量不再属于某个对象而属于整个类,不论有多少个对象实例化都共用同一个静态成员变量(静态成员变量存放在静态区)因此访问静态成员只需要突破类域就可以,可以通过类名静态成员或者对象.静态成员来访问。静态成员变量不属于任何对象所以不会走初始化列表因此需要在类外进行初始化用static修饰的成员函数叫做静态成员函数静态成员函数没有this指针。静态成员函数中可以访问其他的静态成员但是不能访问非静态成员因为没有this指针。非静态的成员函数可以访问任意的静态成员变量和静态成员函数但是静态成员也是类的成员受到访问限定符的限制。classA{public:A(){_scount;}A(constAt){_scount;}~A(){--_scount;}staticintGetACount(){return_scount;}private:// 类⾥⾯声明staticint_scount;};// 类外⾯初始化intA::_scount0;intmain(){coutA::GetACount()endl;A a1,a2;Aa3(a1);coutA::GetACount()endl;couta1.GetACount()endl;// 编译报错error C2248: “A::_scount”: ⽆法访问private 成员(在“A”类中声明)//cout A::_scount endl;return0;}7.友元之前我们提到过流插入流提取操作符需要重载为全局函数但是这样就会受到访问限定符的限制那我们该怎么才能获取类中的成员呢?有缘提供了一种突破类访问限定符封装的方式有缘分为友元函数和友元类在函数声明或者类声明的前面加friend并且把友元声明放到一个类的里面。通过友元就可以访问类的私有和保护成员但是有缘会增加耦合度破坏了封装所以有缘不宜多用。友元函数仅仅是一种声明而且是一种单向的宣称关系它不具有交换性也不能传递也不是类的成员函数。8.内部类如果一个类定义在另一个类的内部这个内部类就叫做内部类。内部类是一个独立的类跟定义在全局相比他只是受外部类类域限制和访问限定符限制所以外部类定义的对象中不包含内部类。内部类默认是外部类的友元类它本身也是一种封装如果把它放到private或者protected位置那么它就是专属内部类其他地方都用不了。内部类和外部类之间可以互相访问他们的所有成员只不过当访问的是非静态成员变量时,必须先有一个此类的对象实例.classA{private:staticint_k;int_h1;public:classB// B默认就是A的友元{public:voidfoo(constAa){cout_kendl;//OKcouta._hendl;//OK}int_b1;};};intA::_k1;intmain(){coutsizeof(A)endl;A::B b;A aa;b.foo(aa);return0;}9.匿名对象用类型(实参)定义出来的对象叫做匿名对象相比我们之前定义的有名对象来说匿名对象生命周期只在当前一行一般临时定义一个对象当前用一下即可就可以定义匿名对象。