一.类与对象
1.访问限定符只在编译时有用,目的是通过语法限定访问。(关联多态虚函数基类和派生类访问限定符不同,能否使用的问题)
2.this指针每个对象使用同一函数结果不同原因是每个对象传了自己的地址,给函数形参暗含的this指针。
每个成员函数参数其实都默认有个不能改指向的this指针,但在参数部分不能写明,因为这是编译器处理的事,内部明着写或不写,都可以用,因为编译器处理好了。
问:1.this指针在哪里:在栈上或者寄存器上
其实是通过p指针调用了成员函数,成员函数接受了此类指针后确定是此类,能访问,再执行打印,由于打印没用到对象内部,不需解引用,也就不会出错。
(过程:把A类型变量*p地址传给能调用的print,this=p=nullptr,使用print成员函数)
下面的访问了对象内部,解引用了空指针,就会运行出错(语法出错是没有相应对象,c++代码书写有问题,不是标准定义的;(比如 int写成了in,漏写个分号,漏写个括号,把int型参数赋值给字符串,等等),一般语法错误在编译时都是可以被编译器发现,发出警示),或类型错误,运行出错指出现未定义行为如解引用空指
3.
内存对齐-->类的成员变量存在内存对齐。
对齐数=编译器默认对齐数与该类型大小的较小值。
总大小:最大对齐数的整数倍
开头变量默认对齐到0。
让其不对齐:更改编译器默认对齐数:#pragma pack(n)
为什么要内存对齐:CPU是按其地址总线大小的整数倍读取的,如32位,4字节,如果一次读取就是一个变量数据那效率高,但可能显示char1字节和int的前3个字节,在一次读Int的最后字节,再拼接起来。
4.当sizeof()测只有成员函数或没有成员的类(对象)时,每个编译器会返回不同的值,表明已有此类或此对象已经初始化。
5.类的使用:
一般类直接在头文件里定义,类型定义不会导致重复定义错误。
二.类的6个默认成员函数
----初始化和清理{ 1.构造 2.析构 }
-----拷贝复制{ 1.拷贝构造 2.赋值重载 }
------取地址重载{ 1.普通对象 2.const对象取地址 }
1.构造函数
1.构造函数在对象生命周期内只能使用一次。无返回值。
类实例化(对象初始化)时自动调用此函数,
构造函数无参数时,对象初始化时不能加空括号,不然认为是函数声明。
1.自动化:
编译器在你没写构造函数时,会为你加一个,功能为:
1.可能处理你的内置类型变量(内置类型为语言自带的类型,如:Int,double...)
有的编译器会给你的内置类型变量赋0,有的不会动,所以里面还是随机值。(太假了,这个功能竟然还看编译器)。
2.调用自定义类型变量的构造函数(层层调用)。(如果你的成员自定义类型变量中有自己写的构造函数,这至少可以让这个变量内部成员初始化)。
应用:两个栈实现一个队列,类中放两个栈,类实例化时,栈内部都初始化了。
2.使用
在全是自定义类型成员时不用自己写,或者给缺省值给成员变量
无需传参的构造函数:1.无参构造。2.全缺省参数构造。3.编译器生成的构造。
(一般三者取其一)
3.初始化列表按照声明顺序初始化,在这之前类没有任何成员变量空间开辟,之后都开辟了
2.析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。(销毁含非栈内资源的成员变量)
1.规则
1. 无参数无返回值类型。
2. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载(全自动,没地方给你加括号传参调用)
3. 对象生命周期结束时,C++编译系统系统自动调用析构函数
4.需要public修饰,结束时才能在外界自动调用。
2.什么时候自己写:默认的析构函数不会销毁动态申请的空间(有malloc,fopen,new,链接等操作的),需要你自己写。
3.调用顺序:如果对象内部有自定义类型变量,编译器会先调用自定义类型变量的析构,再调用当前类的析构(层层销毁返回)
3.拷贝构造
1.使用要求
参数只能是一个接受实参的同类引用
Qt:1.为什么不能是对象接受而是引用接受。
Ans:对象接受会造成无限递归调用,使用拷贝构造要传参,将实参对象给形参要调用拷贝构造拷贝,使用拷贝构造要传参......
Qt:2.为什么不能用指针接收?
Ans:指针是会达到与引用同样效果的,但是由于拷贝构造在1.对象间相互赋值 2.传参 时会自动调用拷贝构造,所以需要一定的格式,引用更为方便,所以编译器认为:指针做参数的构造不是拷贝构造。不会调用指针实现的拷贝效果。
2.分类:
默认为浅拷贝:根据地址一字节一字节拷贝。深拷贝:如果被拷贝对象里有一块空间,深拷贝后,新对象会自己另外申请一块空间。(必须手动完成)
3.使用时机:
注:一般不写析构函数就不写拷贝构造,写了拷贝构造就要写(满足条件同,有指针或一些值指向资源,如:malloc,流,new)
4.赋值运算符重载
1.使用要求:
1.运算符重载的参数必须含有至少一个对象(类型转换的运算符 bool operator bool()除外 )
2.根据原有运算符为二目/单目运算符决定重载参数只能是2/1个
3.作为类的成员函数重载时,参数默认会添一个this指针,注意参数数量
4.运算符必须是语言已有的,不能自己创造
5.有的运算符不能重载,如:(.*),(.),(::),(?:),(sizeof).
(sizeof也是运算符,或者说关键字)
6.必须有个自定义类型对象,其他可为任意类型,内置类型,自定义类型都可。
2.使用本质:运算符操作对象当作实参按序传给函数
3.重载位置:
1.在全局。
缺陷:
不能直接访问对象私有成员,只能通过getter或友元。
2.在类中。(建议)
缺陷:
参数会多个this指针。注意写的参数个数应-1。
优点:
能直接访问同类所有对象的成员。
(类的访问权限是类的所有对象->类的成员函数可以访问同类其他对象的私有成员)
4.区分赋值与拷贝构造:
1.使用场景:
拷贝构造是用已经初始化的对象对未初始化的对象进行拷贝初始化,赋值的两个对象都已经初始化了。
2.易混点:
拷贝构造有两种写法:
1.带括号。data d2(d1);
2.赋值符号。data d2=d1;由于未初始化,编译器会调拷贝构造
赋值:
data d2;
d2=d1;对象都已经初始化了,编译器调拷贝构造。
3.格式
对象类型& operator=(另一个对象别名){代码块 return *this;}
三.内联函数
1.使用:不能声明和定义分离多个文件,因为其不进函数表,就地找,一个文件找不到就出错
使用位置:多个文件使用可以直接定义写入头文件,不进函数表,不会冲突
2.使用要求:最好不含for,while,switch等语句,编译器会看作正常函数,不会展开
3.优点:节省栈空间,比宏方便。缺点:多次直接就地展开指令空间会很大
四.引用
1.C++不能引用数组
2.引用类型引用不同类型:
1.基类引用 引用 派生类,不需类型转换,直接赋值兼容转换,基类部分给基类引用(与自身类型虚函数表指针)
2.const类型转换引用,引用只能允许相同类型,而=允许类型转换,但是类型转换生成临时变量具有常性,需要用const引用接收
补充:临时变量产生场景:
1.类型转换
2.表达式运算
3.函数传值返回:将d1拷贝构造给编译器的临时变量,再返回临时变量。(对传值返回的引用接收要用const引用接收)
3.引用与指针比较
1.不同:1.引用在语法上不开辟空间,指针开辟指针变量
(底层两者都开辟空间,引用也是用指针实现的)
2.引用定义时要初始化,且不能初始化为常数和空,且定义后不能改变引用对象,指针相反。
3.sizeof引用为其引用的类型的大小
4.引用不能替代指针,引用不能改,指针在链表中为必要
5.引用的变量必须已经初始化
6.权限可缩小不能放大
2.相同:效率相同(底层相同)
4.使用
1.函数传参涉及修改值
2.需要返回静态可修改值(函数返回值)
五.函数重载
1.使用要求:两个函数形参必须不同(类型,类型顺序,类型个数满足其一)。与返回值类型无关(编译器是根据你传入的参数类型为你匹配相应函数)
2.调用歧义:1.缺省参数导致传入参数相同,不同的为缺省的,结果没传,
2.类型转换。1.自动类型提升。浮点float转double,整形比int低的转为int,指针转 不了就歧义
2.指针间或其他指针与void*
3.函数重载的类型识别操作顺序 1.精确匹配 2.const转换(大多数情况下,数组名,函数名可以看作指针(sizeof,&名除外)3.自动类型提升 4.类型指针到void*类型(其他指针类型间不能转换,除非在环境中这两个指向空间大小相同,(指针变量大小一定相同由系统位数决定),否则发生值变化)。
4.本质实现:
问题:为什么C语言不允许函数重载,C++允许呢?函数重载怎么实现?
回答:程序从源码到可执行文件有几步骤:预编译,编译,汇编,链接。
在C语言中, 对于函数来说,其在变化中经过以下几个过程:
符号汇总 1.在编译阶段,编译器对每个文件会进行符号汇总,包括函数名(符号的一种)。
形成符号表 2.在汇编阶段,编译器对每个文件的函数找地址形成符号表。某个文件中已声明未找到定义(即没有效地址),其地址为无效地址。
符号决议和重定位 3.在链接阶段,链接器根据符号表,将函数名对应唯一一个地址,若一个文件中的函数只有无效地址,链接器会从其他文件的符号表里寻找地址,以保证链接时程序每个函数都能找到地址并调用。
-->>总之C语言是根据函数名汇总并寻找地址的,所以一个函数名对应一个地址,如果多个相同函数名的函数,在符号决议和重定位时,链接就会出错。(C语言编译器在编译的语法检查阶段就会报错)
在C++中
在编译器执行形成符号表阶段,会根据函数名和形参为每个不同函数生成唯一标识符(返回值不在函数区分标准中),后根据标识符执行符号决议和重定位,链接时程序中的每个函数都能找到相应地址并调用。
-->>总结:C语言根据函数名进行函数寻址,C++根据函数的唯一标识符进行寻址调用。
六.初始化列表
1.必须在初始化列表里初始化的成员:
1.const修饰的成员 (给缺省值同样在初始化列表中)
2.引用
同const,引用必须在定义时初始化,且后续不能改,所以如果要让其引用外部变量,必须在初始化列表给值,给缺省值必须能拿到一些对象初始化就有的变量。(如全局,在其之前初始化的成员变量)
3.自定义类型含参构造的对象
若要调用只能在初始化列表中加括号传参构造,不然初始化列表默认调用默认构造。
不能通过给缺省值来调他的普通构造(给缺省值必须要有赋值符号),拷贝构造可以,前者必须在初始化列表中写。
2.使用要求:
初始化列表中括号内可以写表达式语句如:malloc...
3.初始化顺序
按照成员变量声明的顺序初始化。(所以初始化列表中顺序最好与声明顺序相同)
原因:成员对象是按声明顺序在内存中存的
七.隐式转换构造
1.使用:
在外部直接给对象初始化传值。
1.单传参
2.多传参
编译器将值隐式类型转化了,期间产生了一个常量性临时变量(编译器自己生成的),用值初始化这个临时变量,再将临时变量通过拷贝构造初始化要初始化的对象(date)。
2.隐式类型转化的值前面可以不写赋值符号。(同加了赋值符号的)(少用)
八.静态成员变量
1.使用要求:
1.外部定义时不能加static--->规定(类域限定符包含static就会让其一般处理,即定义一个外部的新的变量)
2.受访问限制符限制
2.调用:
(属于整个类和对象)
1.通过类指定调用
2.通过特定对象调用
实际使用
如果类中有私有静态成员变量,可以只使用静态成员函数,让其只能访问静态成员,可以保证访问安全性,通过类直接调用,更快捷。
3.注意:
类大小不包含静态成员变量(在静态区)
九.友元:
1.使用:
1.运算符全局重载(较麻烦)
2.外部类为内部类的友元
3.函数为类的友元
4.类为类的友元
2.使用要求:
1.友元声明可以写在类中的任意位置,不被类中访问限定符限制
2.友元可以访问类保护的私有成员但不是类的成员函数->没有this指针的概念
3.不能被const修饰->没this指针概念
4.不能多用,破坏了封装
5.成员函数声明为另一个类的友元
(1)成员函数必须写在调用类的定义的后面、(2)包含成员函数的类的定义要写在调用类之前
十.嵌套类
使用:
-
表示归属关系:嵌套类可以表示一种“属于”关系,即嵌套类是外部类的一部分,这在逻辑上和代码组织上都是有意义的。
- 作为成员的私有实现:例如,一个类可能需要一个特殊的迭代器或智能指针,而这些类型不需要对外公开,就可以将其定义为嵌套类。
- 实现工厂模式:可以在一个类内部定义多个嵌套类,每个嵌套类负责创建不同类型的对象。
class OuterClass { public:class NestedClass {private:int nestedValue;public:NestedClass(int value) : nestedValue(value) {}void display() const {std::cout << "Nested Value: " << nestedValue << std::endl;}};NestedClass nestedObject;OuterClass(int value) : nestedObject(value) {}void useNested() {nestedObject.display();} };int main() {OuterClass oc(10);oc.useNested();// 直接访问嵌套类OuterClass::NestedClass nc(20);nc.display();return 0; }
注意:
1.外部类不能直接访问内部类的私有成员
2.内部类可以访问外部类的私有成员
十一.匿名对象
没有名称的临时创建的对象,通常用于临时操作或作为函数的实参或返回值。
生命周期:
int(3)
和int(4)
是所谓的临时对象(或临时变量),它们是右值表达式,它们在表达式结束时就会被销毁,不允许被取地址。
常引用(const &
)可以绑定到右值,包括匿名对象(即临时对象)。这允许你延长匿名对象的生命周期,使其至少与常引用的生命周期一样长。
使用场景:
临时调用成员函数
十二.new与delete
1.本质
new的操作是operator new函数与对象构造函数构成的,operator new 可以理解是malloc加上失败抛异常
2.语法
1.new开单空间
类型指针=new 类型(初始化值);//自定义类型会让初始化值隐式类型转化再拷贝构造
2.new开多空间
类型指针=new 类型[n个]{初始化值};
1.delete释放单空间
delete 指针;
2.delete释放多空间
delete []指针;
注意:如果传参的个数少于开辟的类型个数,对内置类型,其他变量赋值0,自定义类型调用它的默认构造。
3.初始化参数与某成员构造参数个数相同情况
要调用多参构造,要在花括号内部加{参数},外部花括号是指多个对象初始化表,内部花括号指单个对象多参数表。
十三.定位new与new[],delete[]
new (pointer) Type(arguments);
其中,pointer
是一个指向预分配的内存区域的指针,Type
是要构造的对象的类型,arguments
是传递给对象构造函数的参数。
new[]的底层是operator new[]与所有对象的构造,operator new[] 的底层是operator new,和数组个数记录空间创建操作
delete[]的底层是所有对象的析构与operator delete[],operator delete[]底层是operator delete和指针记录空间的跳转和后移操作
十四.模板
1.函数模板实例化
1.隐式实例化
编译器根据传入实参类型自动推导
2.显示实例化
直接指定类型
用法:在函数名后的<>中指定模板参数的实际类型
实质:指定模板参数类型,让编译器帮你转换
优点:
重复实例化的问题
编译时间:每次模板被实例化时,编译器都需要生成代码。如果同一个模板在多个文件中被实例化,编译器会在每个文件中都生成相同的代码,这会增加编译时间。显式实例化可以确保模板只被编译一次,减少编译时间。
模板类的实例化:
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>,中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。->类不能重载,模板名不能为类名。
十五.switch语句
一。功能
1.选择,由case N:完成
2.switch语句本身没有分支功能,分支功能由break完成
二。注意
1.switch语句如果不加break,在一次判断成功后会执行下面全部语句并跳过判断
2.switch的参数必须是整形或者是计算结果为整形的表达式,在C和C++中,switch
语句的case
标签不允许直接使用浮点数,因此不会发生隐式类型转换。
3.default无论写在switch语句的哪个地方,都用来处理剩下的情况
4.多种情况,一种输出,可以将部分条件不写break,写一块
5.switch语句默认直接进case,最后default,在switch与case之间语句无效
6.case内不被认为是代码块,不能定义变量,要定义变量,可以在case中加花括号
7.case值必须是真常量,不能是const修饰的变量
建议:频率高的判断case写最前,default最好用来处理真默认其他情况
声明与定义分离
要求:1.模板函数 必须在前面加上模板参数列表,告诉编译器是哪个模板的东西。
2.模板类成员函数 在模板参数列表基础上 必须前面加模板名<所有模板参数>::
2.不能声明与定义分离在两个文件。->编译器找这麻烦
十六.模板进阶
1.非类型模板参数
template <typename T, T* Ptr>
class PointerHolder {
public:T& get() {return *Ptr;}// 其他成员函数...
};int globalVar = 42;
PointerHolder<int, &globalVar> holder;
定义:
指不是类型的模板参数,C++20前只支持整形,之后支持内置类型
注意:
1.这参数被用时被看作常量,不可修改,但能定义静态数组
2.不同参数模板生成的类不是同一类型(模板本质是根据不同参数生成不同类)
3.可以给缺省值(类的,非类型参数的都行)
(补充:用时实例化:类中的成员函数只有在被使用时才会实例化它(按需实例化),才会语法检查)
(模板尖括号位置不变,都是在名字后)
用法:
静态栈指定大小初始化
实例:array
封装了静态数组(栈上的),优点:越界检查严格(自定义类型的operator[]检查),支持.size(),但不初始化
----vector直接平替
补充:模板使用,模板参数为模板
#include<iostream>
#include<vector>
using namespace std;
template<class T>
void PrintVector(const vector<T>& v)
{for (auto e : v){cout << e<<" ";}cout << endl;
}
int main()
{vector<int> v1{ 1,2,3,4 };vector<double> v2{ 1.1,2.2,3.3,4.4 };PrintVector(v1);PrintVector(v2);return 0;
}
补充:必须使用typename的
#include<iostream>
#include<vector>
using namespace std;
template<class T>
void PrintVector(const vector<T>& v)
{//vector<T>::const_iterator it = v.begin();\错误:编译器语法检查从上往下检查,但只检查实例化了的模板类,不然只检查模板的‘壳’,\比如模板定义语法,分号尖括号等\这里没有实例化,但又用了模板里面的内容,\编译器不会再去找(成本高,数量多),\但不知道这指定的是类里的类型还是静态成员,\所以要程序猿加个typename告知这是类型//总结:要调 未实例化的模板类 里的类型要加typenametypename vector<T>::const_iterator it = v.begin();
//也可以直接用auto做类型避开while (it != v.end()){cout << *it++ << " ";}cout << endl;
}
int main()
{vector<int> v1{ 1,2,3,4 };vector<double> v2{ 1.1,2.2,3.3,4.4 };PrintVector(v1);PrintVector(v2);return 0;
}
2.模板特化
首先不建议全特化,有const修饰的坑而且可以有直接写指定函数替代,编译器会优先匹配已有函数。
1。全特化
#include<iostream>
#include<vector>
using namespace std;
template<class T>
bool LESS(const T& t1, const T& t2)
{return t1 > t2;
}
template<>
bool LESS(int* const& t1, int* const& t2)
{return *t1 > *t2;
}
int main()
{int a = 1;int b = 2;cout << LESS(&a, &b) << endl;return 0;
}
2.偏特化
#include<typeinfo>
using namespace std;
template<class T,class B>
bool LESS(const T& t1, const B& t2)
{return t1 > t2;
}
template<class T>
bool LESS( T& t1, int* const& t2)
{return *t1 > *t2;
}
int main()
{int a = 1;int b = 2;//cout << typeid(&b).name() << endl;//&b类型为int*因为临时变量常性只对引用和运算起效int* const p = &b;cout << LESS(&a, p) << endl;return 0;
}
3.指定大类
#include<iostream>
#include<vector>
#include<typeinfo>
using namespace std;
template<class T,class B>
bool LESS(const T& t1, const B & t2)
{return t1 > t2;
}
template<class T,class B>
bool LESS( T* const & t1, B* const & t2)
{return *t1 > *t2;
}
int main()
{int a = 1;int b = 2;cout << LESS(&a, &b) << endl;return 0;
}
3。模板分离多文件问题
多个cpp文件包定义会冲突->要.h
模板项目可以是.h或.hpp既是.h也是.cpp
为什么包.h就能用函数->调用函数就是去找这个函数第一句指令的地址,链接时去找定义,
不能分离多个文件原因:
模板定义声明分离多个文件找不到->但是函数模板没有地址,不会被编译生成指令,没有对应模板生成的函数进符号表。
->调用的地方知道实例化成什么,但只有函数/类声明没有模板定义,模板定义的地方不知道实例化成什么,编译器也不会一个文件一个文件找
解决:显式实例化,在对应模板定义文件内实例化一个对应的。
template
int Add(const int& left,const int& right)
编译器在链接前多文件不会交互,为了编译速度,不然去单独找模板,速度极慢。
真正解决使用模板:将模板定义写入一个文件(调用的地方就有定义)
模板类成员函数短的放里面(内联),长的放外面,但都在一个文件
模板的优点:
复用代码(编译器帮助实例化),增强灵活性(迭代器通过模板就能实现)
缺点:
代码膨胀(一个实例化一个类),错误不好检查
十七.继承
1.权限:
子类的访问权限=min(父类访问权限,继承方式)
2.赋值兼容转换
1.使用要求:
子类的对象能赋值给父类对象,指针,引用。
2.特点:
没有类型转换--->没有产生临时变量,将子类满足的一部分给父类对象,指针,引用。
3.不同于:
截断(高到低),提升(低到高)---->都是类型转换---->产生临时变量--->不能直接给引用指针(有引用和指针的常性)
注意:只限于公有继承
(公有继承保证了派生类至少拥有基类的公有接口,因此赋值兼容转换不会导致类型安全的问题)
3.隐藏
1.使用要求:
父类和子类有同名成员函数构成隐藏而不是重载
2.原理
子类和父类属于不同域(全局域,局部域,类域,命名空间域),编译器在编译时会先去子类查找,编译时就确定使用哪个函数定义,而不是像函数重载,在链接时根据函数标识符确定。
例:
下面哪个正确()
A.fun构成重载
B.fun构成隐藏
C.编译报错
D.运行报错
答案:B,C
继承后两个fun函数位于两个域,不构成重载,同名隐藏,编译时根据函数名找域内是否有这个函数名,以及参数检查,找到fun(int i),且这个域内没重载函数,(fun()被隐藏了),发现参数不对,编译时语法检查就报错了,而不是等到链接时根据被修饰的函数名找函数。(函数名修饰是符号表的概念)
3.总结:
--->继承子类父类最好不用同名函数,同名如果要调父类的要指定作用域为父类
4.继承的默认成员函数的顺序
对于子类:编译器自动生成的构造:调用父类的及自定义类型的默认构造
析构,拷贝构造同上。
构造,析构顺序:
构造:
强制保证先父类再子类
在C++中,当你定义一个类的自定义构造函数时,如果没有显式调用父类的构造函数(无论是默认构造函数还是其他构造函数),那么默认情况下,编译器会自动调用父类的默认构造函数(无参构造函数)。
注意:
初始化列表用于初始化派生类自己的成员以及调用基类的构造函数
强制要求使用父类的构造,而不是在初始化列表对父类成员一个一个初始化
实际初始化顺序不是初始化列表顺序,而是成员的声明顺序(先父类再子类声明顺序)
--->最好初始化列表的顺序和声明顺序一致--->不要在初始化列表中初始化父类的某个成员
目的:让子类再初始化列表调用父类的成员的值不会出错
析构:
强制保证先子类再父类
1.在子类析构函数最后自动强制再调用父类析构,即如果在子类析构里调父类析构,最后还会再调。
2.直接调调不动父类析构:由于多态,析构函数名字编译器统一处理成destructor,构成隐藏。要指定作用域。
拷贝构造:
一般不需自己写,若涉及深拷贝,自己写(同)
深拷贝:将父类拷贝构造显式调用
自己写会自动调父类的拷贝构造/赋值运算符重载吗?不会,要自己显示调用。
十八.多态
1.使用要求:
1.基类的指针或引用调用虚函数
2.子类对此虚函数进行了重写
(
1.为什么不能通过基类对象实现多态
因为基类接受子类对象,编译器只会处理成员变量,一个类对象里面的vptr永远不会变,永远都会指向所属类型的虚函数表。
)
2.虚函数:
(虚函数解决了菱形继承中名称冲突和数据内容不一致的问题)
在成员函数前加virtual变成虚函数
1.虚函数的重写;
派生类中重写和基类虚函数函数名,返回值,参数列表一样,完成重写
(可以不加virtual,虚函数被继承下来保持了虚函数的属性)
重写的例外:
1.如果在基类的声明中带有默认实参值,则通过基类指针调用该函数时,就总是从函数的基类版本中接受默认实参值。
2.协变(基类与派生类虚函数返回值类型不同) 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
3.析构函数的重写。无论基类析构函数是否加virtual,派生类只要定义都会重写,因为编译器把他们名字统一改成了destructor方便重写
2.override
override明确地表示一个函数是对基类中一个虚函数的重载。更重要的是,它会检查基类虚函数和派生类中重载函数的签名不匹配问题。如果签名不匹配,编译器会发出错误信息
3.final
final:修饰虚函数,表示该虚函数不能再被重写
3.抽象类;
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象.
使用抽象类:如果不实现多态,不要把函数定义成虚函数。
( 注意:抽象类对象不能用,即使将派生类对象拷贝给它(虚表指针还是一样),其就不该有实例,抽象类指针和引用都可以用)
多态原理:
动态绑定,即运行时才绑定函数地址,有虚函数的类内存中先是一个指向虚表的虚表指针__vfptr,虚表是一个指针数组,一般情况这个数组最后面放了一个nullptr,存放虚函数函数地址(函数地址实际上是jump到函数第一条指令地址的地址),派生类会重新开辟空间新立虚表,将重写的虚函数地址覆盖原虚函数地址,在运行时使用虚函数实际上是通过虚函数指针去虚表找某个函数地址,取出来再call执行函数指令。
补充:
虚函数表C++没规定存在哪,VS编译器中实际上存在常量区,使其不易修改(可以比较其地址与常量,静态变量地址的远近得出),且基类虚表和子类虚表不同,且一个类只有一份,与对象个数无关。
总结一下派生类的虚表生成:a.拷贝 b。重写就替换 c.新的放最后
静态绑定:编译时确定已经声明函数,一个文件内定义的直接确定地址,其他链接再找地址
动态绑定:运行时确定地址(虚函数)
打印虚表:取前4字节为虚表指针转为虚表地址传给PrintVTable
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[]) { cout " ", i, vTable[i]); VFPTR f = vTable[i]; f(); } cout << endl; }
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
拓展:
多继承的虚表:
按照派生类声明的继承顺序分配内存,有多个虚表指针指向多个虚表位置(继承了几个就有几个虚表),新增的虚函数放到继承的第一个基类的虚表里。
指向的虚表是否是“新的”取决于派生类是否重写了基类的虚函数。如果重写了,通常会有新的虚表;如果没有重写,那么虚表指针可能指向与基类相同的虚表。
这个类再被继承又是一样的流程:
-
直接基类的虚表:新派生类首先继承其直接基类的虚表。如果直接基类是多继承的,那么新派生类会继承所有这些基类的虚表指针。
-
虚函数的重写:如果新派生类重写了任何虚函数,那么在它的虚表中,这些函数的条目会被更新为指向新派生类中重写函数的地址。
-
新增虚函数:如果新派生类添加了新的虚函数,这些函数的地址会被添加到虚表中。
杂项
1.在C++中NULL与0同,都是int类型的数据,使用时赋值给空指针由于类型转换没问题,但在函数重载传参时类型显得尤为重要,两者都是Int,于是有了补丁nullptr,为void*类型指针。
2.(注意:常量型对象与常量引用对象都不能调用其成员函数,只能通过一模一样类型的对象调用),临时对象的常性不影响调用。
3.关键字explicit
在构造函数函数头前加,不允许隐式转化初始化
C++内存管理
4.模板函数返回自动类型(建议,方便隐式类型转换)
注意:auto不能做函数参数,无法判断函数标识符
5.iostream的std命名空间中有swap()函数模板,直接用就行
6.(补充:数组越界检查
C++数组越界检查编译器一般只检查邻近位置,如int a[10],使用只检查a[10]和a[11]的越界使用,而且只检查越界写,其他检查不出来。
为什么?数组越界检查每次使用检查成本高,所以设置标志位置 检查越界写,越界读不会报错,检查标志位置值有没有被修改。)
7.在C++中,inlin
e
关键字必须放在函数的返回类型之前或者紧接在返回类型之后,而不是放在参数列表之后
inline int add(int a, int b) {return a + b;
}// 或者int inline add(int a, int b) {return a + b;
}
在C++中,inline
关键字通常应该放在函数的定义处,而不是声明处。这是因为inline
建议编译器在调用点展开函数体以减少函数调用的开销,而这一建议是在函数定义时才真正有用的。
8.string等stl里的模板容器属于自定义类型,指针属于内置类型
9.按需实例化:一个类被实例化了,不是所有成员函数都被实例化了,只有用的函数才会被实例化-->不调用不报错的原因
10.const修饰
1.普通类型(除了指针),const int& a,const修饰的是a本身,常引用
2.指针 const int*&a,const修饰的是指向的int,修饰本身要在指针后面int* const & a
关联特化失败:
template<class T>
bool compare(const T& a, const T& b)
{return a > b;
}
//特化,特殊处理某类型
template<>
bool compare(const int*& a, const int*& b)
{return *a > *b;
}
const修饰特化指针修饰的是指向类型,而不是模板中的参数本身
改为:
#include<iostream>
#include<vector>
using namespace std;
template<class T>
bool compare(const T& a, const T& b)
{return a > b;
}
//特化,特殊处理某类型
template<>
bool compare(int* const & a, int* const & b)
{return *a > *b;
}
int main()
{cout << compare(1, 2) << endl;int a1 = 0;int b1 = 2;int* a = &a1;int* b = &b1;cout << compare(a,b) << endl;return 0;
}
11.成员函数地址不允许转化
12.IO对象不允许拷贝
13.接收单词按单词出现次数,次数同再字典序排序:
#include<iostream>
#include<vector>
#include<algorithm>
#include<map>
using namespace std;
struct Compare
{bool operator()(const pair<string, int>& p1, const pair<string, int>& p2) //&f{return p1.second > p2.second ? 1 : p1.second == p2.second ? p1.first < p2.first:false;}
};
int main()
{map<string, int> mp;string midstring;while (cin >> midstring){mp[midstring]++;}vector<pair<string, int>> vc(mp.begin(), mp.end());sort(vc.begin(), vc.end(),Compare());for (auto& e : vc){cout << e.first << " " << e.second << endl;}return 0;
}