C++篇:第八章_类_知识点大全
C++篇为本人学C++时所做笔记(特别是疑难杂点),全是硬货,虽然看着枯燥但会让你收益颇丰,可用作学习C++的一大利器
八、类
(一)类的概念与规则
- “子类”和“子类型”的区别:
① 替换原则只适合于”子类型”关系,而一般编程语言只是考虑了”子类”关系,
② 子类 : 说明了新类是继承自父类,故不能说继承实现了子类型化
③ 子类型 : 强调的是新类具有父类一样的行为(未必是继承),故只有在公有继承下,派生类是基类的子类型
④ 子类型关系是不可逆且不对称的;子类型关系是可传递的
⑤ 子类型化与类型适应性是一致的
-
不可变类:说的是一个类一旦被实例化,就不可改变自身的状态。常见的比如String和基本数据类型的包装类,对于这种不可变类,一旦在进行引用传递的时候,发现当形参改变的时候二者地址不一样;但当形参不做改变,只是单纯把实参赋给形参的话二者地址是一样的,所以在方法中对形参的改变,并不会影响实际参数。
-
类成员函数的定义不必须放在类定义体内部,但所有成员必须在类内部声明
-
可以把子类的对象赋给父类的引用
-
向上转换一定成功,向下转换不一定成功。向下转换必须存在虚函数,不然编译错误
-
通用多态又分为参数多态和包含多态;特定多态分为过载多态和强制多态
-
以下三种情况下需要使用初始化成员列表:
① 情况一、需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化)
② 情况二、需要初始化const修饰的类成员
③ 情况三、需要初始化引用成员数据
-
在C++中数据封装是通过各种类型来实现的 // 错误;C++通过类来实现封装性 ,把数据和与这些数据有关的操作封装在一个类中,或者说,类的作用是把数据和算法封装在用户声明的抽象数据类型中
-
所谓的继承使子类拥有父类所有的属性和方法其实可以这样理解,子类对象确实拥有父类对象中所有的属性和方法,但是父类对象中的私有属性和方法,子类是无法访问到的,只是拥有,但不能使用。就像有些东西你可能拥有,但是你并不能使用。所以子类对象是绝对大于父类对象的,所谓的子类对象只能继承父类非私有的属性及方法的说法是错误的。可以继承,只是无法访问到而已
-
可不可以继承不是由静态还是实例决定,它是由修饰符来决定的
-
派生类与基类之间的特殊关系之一:基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示类型转换的情况下引用派生类对象
-
如果函数内部有同名的类定义,则全局声明在该函数内部是无效的,有效的是局部定义的(变量等均遵循这一规则)
-
在类中定义或声明的数据类型的作用域是类内部,因此它们不能在类外使用
-
若::前没有类名则该运算符不是类作用域限定符的含有,而是命名空间域限定符含义
-
根据重载或默认参数函数的要求,必须在第1次出现函数声明或定义时就明确函数是否重载或有默认参数
-
对象指针访问对象中的成员要用指针成员引用运算符“->”
-
基类是对派生类的抽象,派生类是对基类的具体化
-
在C++中多态性通过重载多态(函数和运算符重载)、强制多态(类型强制转换),类型参数化多态(模板),包含多态(继承及虚函数)四种形式实现
-
类中数据成员的生存期由对象的生存期决定 ,因为类中的成员变量是在类的对象声明时创建,在对象生存期结束后截止
-
关于类和对象的描述不能说 是类就是 C 语言中的结构体类型,对象就是 C 语言中的结构体变量,因为c语言的结构只是一个简单的构造数据类型,只能简单的封装数据;
c++的结构是支持面向对象程序设计的关键概念,是一种抽象数据类型,不仅如此还具有封装特性,可以把数据和函数封装在一起,并且可以限制成员访问权限,同时还具有继承和多态等特性等,故能说C++语言中结构是一种类 -
类是一种数据结构;类是具有共同行为的若干对象的统一描述体
-
基类与派生类之间的关系可以有如下几种描述:
① 派生类是基类的具体化:基类抽取了它的派生类的公共牲,而派生类通过增加行为将抽象类变为某种有用的类型
② 派生类是基类定义的延续
③ 派生类是基类的组合:在多继承时,一个派生类有多于一个的基类,这时派生类将是所有基类行为的组合
- 在继承图中,有向线是从派生类指向基类
(二)类的使用
- 创建对象的示例:
① CSomething a();//只是定义一个方法,方法返回一个CSomething对象
② CSomething b(2);//增加1个对象
③ CSomething c[3];//对象数组,增加3个对象
④ CSomething &ra=b;//引用不增加对象
⑤ CSomething *pA=c;//地址赋值,不增加对象
⑥ CSomething *p=new CSomething;//在堆上构造一个对象,增加1个对象
-
面向对象程序设计方法的优点包含可重用性、可扩展性、易于管理和维护,但不简单易懂
-
对象之间通信实际上就是通过函数传递信息,封装是把数据和操作结合在一起,继承是对于类的方法的改变和补充,重载是多态性之一
-
自动变量(auto)和寄存器变量(register)属于动态存储,调用时临时分配单元
-
class 类名; // 类声明,非类定义因为没有类体;此时在声明之后,定义之前,类是一个不完全类型,因此不能定义该类型的对象,只能用于定义指向该类型的指针及引用,或者用于声明(不能是定义)使用该类型作为形参类型或返回类型的函数
-
类中不能具有自身类型的数据成员
-
如果对象的数据成员中包括动态分配资源的指针,则当对象赋值给同类对象时,只复制了指针值而没有复制指针所指向的内容
-
对象的赋值只对其中的非静态数据成员赋值,而不对成员函数赋值
-
普通函数与类的应用:
① 当函数形参是对象指针时,C++不能对对象指针进行任何隐式类型转换
② 函数返回对象时,将其内存单元的所有内容复制到一个临时对象中
③ 函数返回对象指针或引用,本质上返回的是对象的地址而不是它的存储内容,因此不要返回局部对象的指针或引用,因为它在函数返回后是无效的
- 赋值兼容规则:指在需要基类对象的任何地方,都可以使用公有派生类的对象替代
① 派生类的对象可以赋值给基类对象
② 派生类对象可以初始化基类的引用
③ 派生类对象的地址可以赋给指向基类的指针
- 在公有继承方式下,间接派生类对象可以直接调用基类中的公有成员函数,去访问基类的私有数据成员
(三)联编(绑定):
-
定义:将模块或函数合并在一起生成可执行代码的处理过程,同时对每个模块或函数分配内存空间,并对外部访问也分配正确的内存地址
-
静态联编与动态联编的区别:
① 静态联编 :指在编译阶段就将函数实现和函数调用关联起来;它对函数的选择是基于指向对象的指针或引用类型,通过对象名调用虚函数,在编译阶段就能确定调用的是哪一个类的虚函数
② 动态联编:在程序执行的时候才将函数实现和函数调用关联;一般情况下都是静态联编,涉及到多态和虚拟函数就必须使用动态联编了;通过基类指针调用,在编译阶段无法通过语句本身来确定调用哪一个类的虚函数,只有在运行时指向一个对象后,才能确定调用时哪个类的虚函数
③ 动态联编在编译时只根据兼容性规则检查它的合理性,即检查它是否符合派生类对象的地址可以赋给基类的指针的条件
④ C++默认静态绑定,当需要解释为动态绑定时的办法就是将基类中的函数声明成虚函数。然而即使是调用虚函数,也只有在使用基类指针或引用的时候才动态绑定,其它还是静态绑定的,并且基类的构造函数中对虚函数的调用不进行动态绑定
(四)内存
- C/C++内存分配函数:
① malloc 函数: void *malloc(unsigned int size)在内存的动态分配区域中分配一个长度为size的连续空间,如果分配成功,则返回所分配内存空间的首地址,否则返回NULL,申请的内存不会进行初始化。
② calloc 函数: void *calloc(unsigned int num, unsigned int size)按照所给的数据个数和数据类型所占字节数,分配一个 num * size 连续的空间。calloc申请内存空间后,会自动初始化内存空间为 0,但是malloc不会进行初始化,其内存空间存储的是一些随机数据。
③ realloc 函数: void *realloc(void *ptr, unsigned int size)动态分配一个长度为size的内存空间,并把内存空间的首地址赋值给ptr,把ptr内存空间调整为size。申请的内存空间不会进行初始化。
④ new是动态分配内存的运算符,自动计算需要分配的空间,在分配类类型的内存空间时,同时调用类的构造函数(malloc不会调用构造函数,free也不会调用析构函数),对内存空间进行初始化,即完成类的初始化工作。动态分配内置类型是否自动初始化取决于变量定义的位置,在函数体外定义的变量都初始化为0,在函数体内定义的内置类型变量都不进行初始化。
⑤ 不能说new 是分配内存空间的函数,该对于new的描述是错误的
-
C和C++语言是手动内存管理的,申请的内存需要手动释放
-
使用New()和Delete()来创建和删除对象,且该对象始终保持到delete运算时,即使程序运行结束它也不会自动释放
-
基本类型的指针释放由内存直接执行,所以delete和delete[]都能表达回收内存的意思。但自定义的对象由析构函数发起回收,所以每一个对象都要调用一次delete,所以才有有new[]对应delete[],但new[]用delete也行,只是为了规范最好不要
-
C++中只有构造函数和析构函数或其成员函数时所占内存为1(不含虚函数),带有虚函数的时候内存为4,普通继承都是公用一张虚函数表,指针大小不增加。(当然有成员变量要加上成员变量的内存)
-
构造函数可以私有,但是此时直接定义类对象和new来定义对象都不再允许,因为new只管分配,不管构造,编译器不会允许未构造的动态内存分配
-
对象大小 = 虚函数指针 + 所有非静态数据成员大小 + 因对齐而多占的字节(所以成员函数(包括静态和非静态)和静态数据成员都是不占存储空间的),因为是占用全局的内存(因为是共享的),和全局变量分配的内存在同一个区域里面,且类一般是栈区,而静态变量是全局区域;成员函数(包括静态和非静态)和静态数据成员都是不占存储空间的
-
C++对空类或者空结构体 ,对其sizeof操作时候,默认都是 1个字节;多重继承的空类的大小也是1
-
一个空类默认会生成构造函数,拷贝构造函数,赋值操作符(赋值函数),析构函数
-
在C中使用malloc(C中的动态分配内存函数)时不需要强制类型转换,因为在C中从void*到其他类型的指针是自动隐式转换的;
在C++中使用malloc时必须要强制类型转换,否则会报错
① malloc和free申请和释放的虚拟内存,不是物理内存
② malloc的返回值是一个指针
③ malloc需要头文件stdlib.h
- New:
① new创建对象不一定需要定义初始值,例如用new分配数组空间时不能指定初值,但一般都会赋初值
② new会调用构造函数
③ 对于一个new运算符声明的指针,只能匹配一个delete使用,因此对一个指针可以使用多次delete是错误的
④ 用new运算动态分配得到的对象是无名的,它返回一个指向新对象的指针的值,故显然new建立的动态对象是通过指针来引用的
⑤ 若内存不足会返回0值指针
-
free用来释放内存空间,若要释放一个对象还需调用其析构函数
-
new和delete运算符可以重载
-
内存知识点:
① 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果;也是内存中的数据出现丢失
(五)访问权限
-
类成员的访问权限中,Private只能被本类的成员函数和其友元函数访问,不能被子类和其他函数访问
-
关于不同类型继承遵循访问属性只能缩小或等于的原则,即保护类型的成员函数就算是公有继承后还是保护类型的成员函数
-
公有继承的保护成员可以被类的方法访问,不能被对象访问,即可以被派生类成员访问,但不能被派生类用户访问
-
派生类用户(派生类对象)只有在public继承下访问基类的public成员
-
在派生类的函数中 能够直接访问基类的公有成员和保护成员 // 错,因为派生类的静态函数也不能访问直接基类的成员变量且如果是间接继承的基类的话要分情况讨论
-
在C++中,来自class的继承默认按照private继承处理,来自struct的继承默认按照public继承处理
-
赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代
(六)构造函数
- 构造函数:
① 若类A将其它类对象作为成员,且有父类,在定义类A的对象时,先调用父类的构造函数,在调用成员的构造函数,最后调用派生类的构造函数。可理解为“先父母,再客人,最后自己”(一定要注意构造函数和析构函数的调用顺序,尽管基类的构造函数是虚函数但定义派生类对象都一定会调用它(总是写错这种题))
② 构造函数可以定义在类的内部或外部,但构造函数初始化列表只在构造函数的定义中而不是函数原型的声明中指定
③ 在子类构造方法中调用父类的构造方法,super()必须写在子类构造方法的第一行,否则编译不通过(可以不写,但是系统会自动添加上,此处强调的是必须在第一行)
④ 默认构造函数分为三类,默认普通构造函数(无形参);默认拷贝构造函数,形参为左值引用;默认移动构造函数(当类没有自定义任何的拷贝控制成员,且每个非静态变量都可移动时,会默认生成移动构造函数),形参为右值引用
⑤ 缺省构造函数,拷贝构造函数,拷贝赋值函数,以及析构函数这四种成员函数被称作特殊的成员函数。这4种成员函数不能被继承(但不能被继承的基类成员在派生类中都存在)
⑥ 一般没有默认构造函数的成员,以及非静态的const或引用类型的成员都必须在构造函数初始化列表中进行初始化
⑦ 数据成员被初始化的次序就是数据成员的声明次序
⑧ 初始化式(定义构造函数时一个以冒号开始,接着是一个以逗号分隔的数据成员列表)可以是任意表达式
⑨ 必须在类的内部指定构造函数的默认参数
⑩ 默认构造函数由不带参数的构造函数或所有形参均是默认参数的构造函数定义
⑪ 在一个类中定义了全部都是默认参数的构造函数后,不能再定义重载构造函数
⑫ 构造函数不需用户调用,也不能被用户调用
⑬ 一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数
⑭ 派生类构造函数:
(1) 可以在一个类的构造函数中使用构造函数初始化列表显式的初始化其子对象
(2) 构造函数调用顺序:调用基类构造函数(按基类定义次序先后调用)->调用子对象构造函数(按声明次序先后调用)->执行派生类初始化列表->执行派生类初始化函数体
(3)如果基类和子对象所属类的定义中都没有定义带参数的构造函数,也不需要对派生类自己的数据成员初始化,则可以不必显式的定义派生类构造函数,反之必须显式定义派生类构造函数
(4) 如果在基类中没有定义构造函数,或定义了没有参数的构造函数,则定义派生类构造函数时可以不显式的调用基类构造函数
(5) 如果基类中既定义了无参构造函数,又定义了有参构造函数,则在定义构造函数时既可显式或不显式调用基类构造函数
(6) 在调用派生类构造函数时,系统会自动先调用基类的无参数构造函数或默认构造函数
(7) 派生类的构造函数的成员初始化列表中,不能包含基类的子对象初始化(由基类的构造函数完成),可包含基类构造函数、派生类中子对象的初始化、派生类中一般数据成员的初始化
- 复制构造函数(拷贝初始化构造函数):
① 拷贝函数和构造函数没有返回值
② 拷贝构造函数的参数可以使一个或多个,但左起第一个必须是自身类型的引用对象
③ 即使定义了其他除复制构造函数外的构造函数,编译器也会自动生成一个缺省的拷贝构造函数,但是不会是该类的保护成员
④ 拷贝初始化构造函数的作用是将一个已知对象的数据成员值拷贝给正在创建的另一个同类的对象
⑤ 拷贝构造函数的形参不限制为const,但是必须是一个引用,以传地址方式传递参数,否则导致拷贝构造函数无穷的递归下去,指针也不行,本质还是传值
⑥ 异常对象的初始化是通过拷贝初始化进行的
⑦ 一般形式为:类名(const 类名& obj)
- 转换构造函数:
① 实现从其他类型到类类型的隐式转换,只有一个形参,形参类型是需要转换的数据类型
② 使用explicit关键字可禁止由构造函数定义的隐式转换(指明该构造函数是显式的),该关键字只能用于类内部的构造函数声明上
③ 一般形式为:类名(const 指定数据类型& obj)
④ 该函数必须为成员函数,不能是友元类型
- 在什么情况下系统会调用复制构造函数
① 用类的一个对象显式或隐式去初始化另一个对象时
类名 对象1=对象2 // 复制初始化,调用复制构造函数(对象2已存在)
类名 对象1(对象2) // 直接初始化,调用与实参匹配的构造函数(对象2已存在)
② 函数实参按值传递对象时或函数返回对象时,函数形参是指针或引用类型时不调用
③ 根据元素初始化列表初始化数组元素时
- 构造函数中必须通过初始化列表来进行初始化情况
① 类成员为const类型
② 类成员为引用类型
③ 类成员为没有默认构造函数的类类型
④ 如果类存在继承关系,派生类必须在其初始化列表中调用基类的构造函数
-
编译器生成的默认构造函数只负责初始化有默认构造函数的成员对象,其他的一律不负责(int等内置类型数据成员的初始化,这个该由程序员去做)
-
如果一个类中所有数据成员是公有的,则可以在定义对象时对数据成员进行初始化
(七)析构函数
-
一个类只能有1个析构函数
-
编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。 因此, 将析构函数设为私有,类对象就无法建立在栈(静态)上了,只能在堆上(动态new)分配类对象
-
析构函数没有返回值,也没有参数,没有函数类型,因此不能被重载,但可以声明成虚函数,因为析构函数一般不需要重载,把它设为虚函数就可以了,系统自动帮你析构的,且作用是为了多态发生时能够完全析构
-
如果基类中的析构函数不是虚析构(没有被virtual修饰),即使基类指针指向子类对象,析构的时候也不先调用子类的析构函数,而只调用基类的析构函数
-
析构函数可以在类内声明,类外定义,但一个类中只能定义一个析构函数
-
定义析构函数时其名与类名完全相同-错
-
如果基类的析构函数是虚函数,delete基类的指针时,不仅会调用基类的析构函数,还会调用派生类的析构函数,而调用的顺序是先调用派生类的析构函数、然后调用基类的析构函数
如果基类的析构函数不是虚函数,那么,像这种定义一个基类的指针,指向一个派生类的对象,当你delete这个基类的指针时,它仅调用基类的析构函数,并不调用派生类的析构函数
-
编译器生成的析构函数称为合成析构函数;它是按成员在类中的声明次序的逆序撤销成员的,且合成析构函数并不删除指针成员所指向的对象(需要程序员显式编写析构函数去处理)
-
析构函数三法则:如果你自己写了析构函数,就必须同时写复制构造函数和赋值运算符重载,你不能只写一个
-
先构造的后析构,后构造的先析构
-
在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,对基类和子对象进行清理
-
析构函数可以重写(基类的析构函数要加virtual)
-
c++中析构函数是可以为纯虚函数的,但是前提是:必须为其实现析构函数,否则派生类无法继承,也无法编译通过
-
析构函数没有参数
(八)引用
- 引用的特点:
① 一定要初始化
② 引用对象要可以取地址 int &a=10//错误,int a=10;int &b=a;//正确
③ 引用不能改变
④ 引用变量使用过程中只能使用引用变量所引用的数值
⑤ 不能有空引用,引用必须与有效对象的内存单元关联
⑥ 指定类型的引用不能初始化到其他类型的对象上
⑦ C++中不能建立引用数组和指向引用的指针,也不能建立引用的引用
⑧ 引用像指针,但这不是c++定义的,更多是编译器的实现,所以不能说引用是指向变量的内存地址
- 使用”常引用”的原因:
① 保护传递给函数的数据不在函数中被改变
② 节省存储空间
③ 提高程序的效率
- 指针和引用之间的联系:
① 引用可以表示指针
② 引用和指针都是实现多态效果的手段
③ 引用本身是目标变量的别名,对引用的操作就是对目标变量的操作
④ 但不能说引用和指针都是指向变量的内存地址;因为引用是一个指针常量,c++做了隐式的转换,当你定义引用时,是定义了一个指针常量,使用引用时,c++会用*转为原值,只是这是隐式的;至于都指向地址,是c++编译器的定义
⑤ 引用声明后,引用的对象不可改变,对象的值可以改变,指针可以随时改变指向的对象以及对象的值
const int &q=x; //&q是对x变量的引用,但被定义为了常量,故q不再是变量,不能自增
(九)友元
-
友元机制-允许一个类将其非公有的成员的访问权授予指定的函数或类
-
友元的正确使用能提高程序的运行效率,但破坏了类的封装性和数据的隐蔽性,导致程序可维护性变差,因此一定要谨慎使用
-
友元包括友元函数和友元类
-
类A的友元类B和友元函数都可以访问类A的私有成员
-
因为友元函数没有当前对象,因此要定义单目运算符,就需要单参函数,要定义双目运算符,就需要双参函数
-
友元函数可以是另一个类的成员函数,称为友元成员函数
-
定义后置“十+”或后置“–“运算是特例,它们是单目运算符,但需要两个形参,头一个形参是作用对象,后一个是int形参
-
用友元函数可以定义成员函数不能实现的运算,例如一些双目运算符,右操作数是本类对象,而左操作数不是本类对象
-
一个类说明的友元函数不可以被派生类继承
-
友元函数可以像普通函数一样直接调用,不需要通过对象或指针
-
友元函数,不是类的成员函数,不能用类作用域符来标识该函数属于哪个类
-
在C++中友元函数是独立于当前类的外部函数,一个友元函数可以同时定义为两个类的友元函数,友元函数即可以在类的内部,也可以在类的外部定义;在类的外面定义友元函数时不必加关键字friend
-
友元关系是单向的,不能传递或继承
-
派生类的friend函数可以访问派生类本身的一切变量,包括从父类继承下来的protected域中的变量。但是对父类来说,他并不是friend的
-
因为友元函数不是类的成员,所以它不能直接访问对象的数据成员(若选项中有其他明显错误的选项则选其他选项),也不能通过this指针访问对象的数据成员,它必须通过作为入口参数传递进来的对象名(或对象指针,对象引用)来访问该对象的数据成员
-
由于一个友元函数可能属于多个不同的类,所以在访问时,必选加上对象名
-
友元函数不受访问控制符的限制
(十)静态
- 静态数据成员:
① 静态成员可以作为默认实参,非静态成员则不能,因为它的值不能独立于所属的对象而使用
② static类变量是所有对象共有,其中一个对象将它值改变,其他对象得到的就是改变后的结果;是final修饰的变量不能修改
③ 因为静态成员不能通过类构造函数进行初始化,因此静态数据成员必须在类外初始化,静态成员常量在类中初始化
④ 静态变量可以用来计算类的实例变量(构造函数中对进行静态变量+1操作,析构函数进行-1操作,便可计算现有对象数量)
⑤ static 修饰的变量只初始化一次, 当下一次执行到这一条语句的时候,直接跳过
⑥ 在外部变量的定义前面加上关键字static,就表示定义了一个外部静态变量。外部静态变量具有全局的作用域和全局的生存期,定义成static类型的外部变量将无法再使用extern将其作用范围扩展到其他文件中,而是被限制在了本身所在的文件内,为程序的模块化、通用性提供方便
⑦ 不能通过类名调用类的非静态成员函数
- 静态成员函数:
① 静态成员函数是类的成员函数,该函数不属于该类申请的任何一个对象,而是所有该类成员共同共有的一个函数
② 静态成员函数不能访问非静态数据成员,只能访问静态数据成员,故推出类方法中可以直接调用对象变量:错误
③ 静态成员函数不能被声明为const
④ 类的对象可以调用【非】静态成员函数
⑤ 类的非静态成员函数可以调用静态成员函数,但反之不能
⑥ 没有this指针
-
静态成员不属于对象,是类的共享成员,故在为对象分配的空间中不包括静态数据成员所占的空间
-
父类的static变量和函数在派生类中依然可用,但是受访问性控制(比如,父类的private域中的就不可访问),而且对static变量来说,派生类和父类中的static变量是共用空间的,这点在利用static变量进行引用计数的时候要特别注意。
static函数没有“虚函数”一说。因为static函数实际上是“加上了访问控制的全局函数”,全局函数哪来的什么虚函数 -
static变量:
① 局部
② 全局
③ static函数(也叫内部函数)只能被本文件中的函数调用,而不能被同一程序其它文件中的函数调用
-
成员指针只应用于类的非静态成员,由于静态类成员不是任何对象的组成部分,所以静态成员指针可用普通指针
-
可以使用作用域运算符“::”也可以使用对象成员引用运算符“.”或指针成员引用运算符“->”访问静态成员
-
类的静态成员是所有类的实例共有的,存储在全局(静态)区,只此一份,不管继承、实例化还是拷贝都是一份
-
初始化:
① 静态常量数据成员可以在类内初始化(即类内声明的同时初始化),也可以在类外,即类的实现文件中初始化,不能在构造函数中初始化,也不能在构造函数的初始化列表中初始化;
② 静态非常量数据成员只能在类外,即类的实现文件中初始化,也不能在构造函数中初始化,不能在构造函数的初始化列表中初始化;
③ 非静态的常量数据成员不能在类内初始化,也不能在构造函数中初始化,而只能且必须在构造函数的初始化列表中初始化;
④ 非静态的非常量数据成员不能在类内初始化,可以在构造函数中初始化,也可以在构造函数的初始化列表中初始化;
⑤ 即:
(十一)重载
① 函数名相同
② 参数必须不同(个数或类型或顺序)
③ 返回值类型可以相同也可以不同;因为函数并不会总是有返回值,函数的返回类型不能作为函数重载的判断依据
-
函数重载是指在同一作用域内(在同一个类或名字空间中) ,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数
-
只有当修饰的const为底层const而非顶层const时才可以区分,也就是说const必须修饰指针指向的对象而非指针本身(即const写在后面)
-
子类重新定义父类虚函数的方法叫做覆写不是重载
-
C++语言规定,运算符“=”、“[]”、“()”、“->”以及所有的类型转换运算符只能作为成员函数重载
-
友元函数重载时,参数列表为1,说明是1元,为2说明是2元
成员函数重载时,参数列表为空,是一元,参数列表是1,为2元 -
声明成员函数的多个重载版本或指定成员函数的默认参数只能在类内部进行
(十二)重写
-
重写就叫覆盖。如果没有virtual就是隐藏
-
被重写的函数不能是static的。必须是virtual的
-
重写函数必须有相同的类型,名称和参数列表
-
重写函数的访问修饰符可以不同。尽管父类的virtual方法是private的,派生类中重写改写为public,protected也是可以的
-
重写要求基类函数为虚函数,且基类函数和派生类函数函数名,参数等都相同
-
方法的覆盖对返回值的要求是:小于等于父类的返回值
-
方法的覆盖对访问要求是:大于等于父类的访问权限
-
重定义(隐藏)是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
① 如果子类的函数与父类的名称相同,但是参数不同,父类函数被隐藏(重定义)
② 如果子类函数与父类函数的名称相同&&参数也相同&&但是父类函数没有virtual,父类函数被隐藏
③ 如果子类函数与父类函数的名称相同&&参数也相同&&但是父类函数有virtual,父类函数被覆盖(重写)
-
fun(int)是类Test的公有成员函数,p是指向成员函数fun()的指针,则调用fun函数正确写法应为p=&Test::fun
-
如果在派生类中声明了与基类成员函数同名的新函数,即使函数的参数不同,从基类继承的同名函数的所有重载形式也都会被覆盖
(十三)虚函数、虚基类和虚继承
-
virtual只在类体中使用
-
虚函数
① 虚函数必须是类的一个成员函数,不能使友元函数,也不能是静态的成员函数
② C++规定构造函数不能是虚函数,而析构函数可以是虚函数。 这样死记硬背也是一种办法,但是不推荐。可以这么理解:假设构造函数为虚函数,而虚函数的调用需要虚表,虚表又由构造函数建立。这样就矛盾了。 就像儿子生了父亲一样,矛盾。所以,构造函数不能是虚函数
③ 纯虚函数是可以有函数体的,当我们希望基类不能产生对象,然而又希望将一些公用代码放在基类时,可以使用纯虚函数,并为纯虚函数定义函数体,只是纯虚函数函数体必须定义在类的外部
④ 虚函数能够被派生类继承
⑤ 只要父类成员函数定义成虚函数,无论子类是否覆盖了父类的虚函数,调用父类虚函数时都会找到子类并子类相应的函数
⑥ 虚函数不可以内联,因为虚函数是在运行期的时候确定具体调用的函数,内联是在编译期的时候进行代码展开,两者冲突
⑦ 当编译器编译含有虚函数的类时,为该类增加一个指向虚函数表(相当于一个指针数组)的指针
⑧ 派生类能继承基类的虚函数表,而且只要是和基类同名(参数也相同)的成员函数,无论是否使用virtual声明,它们都自动成为虚函数
⑨ 当用基类指针或引用对虚函数进行访问时,系统将根据运行时指针或引用所指向的或引用的实际对象来确定调用对象所在类的虚函数版本
⑩ 虚函数不可以重载
⑪ 在父类的构造函数和析构函数中都不能调用纯虚函数(不能以任何方式调用)
⑫ 在构造函数执行完以后虚函数表才能正确初始化,同理在析构函数中也不能调用虚函数,因为此时虚函数表已经被销毁
⑬ 下面情况调用不会出现多态性:
(1) 通过对象调用虚函数不会出现多态(通过指针或者引用才会有多态性,因为动态绑定(多态)只有在指针和引用时才有效,其他情况下无效)
(2) 在构造函数里面调用虚函数不会出现多态
(3) 指定命名域调用不会出现多态
⑭ 设置虚函数需要注意以下几个方面:
(1) 只有类的成员函数才能说明为虚函数。虚函数的目的是为了实现多态,多态和集成有关,所以声明一个非成员函数为虚函数没有任何意义。
(2) 静态成员函数不能是虚函数。静态成员函数对于每一个类只有一份代码,所有的对象共享这份代码,它不归某个对象所有,所以没有动态绑定的必要性。不能被继承,只属于该类。
(3) 内联函数不能为虚函数。内联函数在程序编译的时候展开,在函数调用处进行替换。虚函数在运行时进行动态绑定的。
(4) 构造函数不能为虚函数。虚函数表在构造函数调用后才建立,因而构造函数不可能成为虚函数。虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用函数。
(5) 析构函数可以是虚函数,而且通常声明为虚函数。
(6) 友元函数:友元函数不能被继承,所以不存在虚函数
⑮ 基类的成员不能直接访问派生类的成员,但可以通过虚函数间接访问派生类的成员
⑯ virtual 函数是动态绑定,而缺省参数值却是静态绑定。 意思是你可能会在“调用一个定义于派生类内的virtual函数”的同时,却使用基类为它所指定的缺省参数值。
结论:绝不重新定义继承而来的缺省参数值!
⑰ 虚析构函数:
(1) 若基类的析构函数声明为虚函数,则由该基类所派生的所有派生类的析构函数也都自动成为虚函数
- 虚基类:
① 虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的
② 设置虚基类的目的:消除二义性
③ 为保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承,C++编译系统只执行最后的派生类对虚基类构造函数的调用,而忽略基类的其他派生类,这样就保证了虚基类的数据成员不会被多次初始化
④ 如果虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有直接派生类和间接派生类中都要通过构造函数的初始化表对虚基类进行初始化
⑤ 虚基类的构造函数先于非虚基类的构造函数执行
-
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念(并不是单纯将数据代码连接起来,是数据和操作数据的函数.),这样能避免受到外界的干扰和误用,从而确保了安全
-
在派生列表中,同一基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过两个直接基类分别继承自同一间接基类,也可以直接继承某个基类,再通过另一个基类再次继承该类。但是,如果某个类派生过程中出现多次,则派生类中将包含该类的多个子对象,这种默认情况在很多有特殊要求的类中是行不通的。虚继承就是为了应对这一情况而产生,虚继承的目的是令某个类做出声明,承诺愿意共享其基类。这样不论虚基类在继承体系中出现多少次,派生类中都只包含唯一一个共享的虚基类子对象
(十四)抽象类
- 抽象类有一下几个特点:
① 抽象类只能用作其他类的基类,不能建立抽象类对象。
② 抽象类不能用作参数类型、函数返回类型或显式转换的类型。
③ 可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。
- 抽象:
① 抽象类指针可以指向不同的派生类
② 抽象类只能用作其他类的基类,不能定义抽象类的对象。
③ 抽象类不能用于参数类型、函数返回值或显示转换的类型
④ 抽象类可以定义抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。
⑤ 抽象类不可以为final的
⑥ 如果一个非抽象类从抽象类中派生,一定要通过覆盖来实现继承的抽象成员,因为如果一个非抽象类从抽象类中派生,不通过覆盖来实现继承的抽象成员,此时,派生类也会是抽象类
⑦ 抽象类就是必须要被覆盖的 所以才不能和final一起用
(十五)final
- final的作用:
① 修饰变量,使用final修饰基本类型的变量,一旦对该变量赋值之后,就不能重新赋值了。但是对于引用类型变量,他保存的只是引用,final只能保证引用类型变量所引用的地址不改变,但不保证这个对象不改变,这个对象完全可以发生改变
② 修饰方法,方法不可被重写,但是还是可以重载
③ 修饰类,类不可继承
(十六)类模板
1.类模板的使用实际上是将类模板实例化成一个具体的类(不能写模板类),而模板类是类模板实例化后的一个产物
- C++中为什么用模板类的原因:
① 可用来创建动态增长和减小的数据结构
② 它是类型无关的,因此具有很高的可复用性。
③ 它在编译时而不是运行时检查数据类型,保证了类型安全
④ 它是平台无关的,可移植性
⑤ 可用于基本数据类型
- 函数模板与类模板的区别:
① 函数模板的实例化是由编译程序处理函数调用时自动完成的;
② 类模板的实例化是由程序员在程序中显式的指定;
③ 函数模板针对参数类型不同和返回值类型不同的函数;
④ 类模板针对数据成员、成员函数和继承的基类类型不同的类
⑤ 一般函数模板习惯用typename作为类型形参之前的关键字,类模板习惯用class
- 在用类模板定义对象时,必须为模板形参显式指定类型实参
- 类模板形参还可以是非类型(普通类型)形参,在定义对象时,必须为每个非类型形参提供常量表达式以供使用
- 类模板形参也可设置默认值
- 类模板的成员函数都是模板函数
- 关于模板和继承的叙述:
① 模板和继承都可以派生出一个类系
② 从类系的成员看,继承类系的成员比模板类系的成员(类模板可作为基类)较为稳定
③ 从动态性能看,模板类系比继承类系(虚函数实现动态多态性)具有更多的动态特性
(十七)成员指针
-
数据成员指针一般形式:数据成员类型 类名::*指针变量名 = 成员地址初值
-
成员函数指针:
① 必须保证三个方面与它所指函数的类型相匹配
(1) 函数形参的类型和数目,包括成员是否为const
(2) 返回类型
(3) 所属类的类型
② 一般形式:返回类型(类名::*指针变量名)(形式参数列表)【const】=成员地址初值
-
若指向成员函数的指针不是类成员,则正确指向的格式为:指针=类名::成员函数名
-
使用类成员指针:
① 通过对象成员指针引用”.“可以从类对象或引用及成员指针间接访问类成员,或者通过指针成员指针引用”->“可以从指向类对象的指针及成员指针访问类成员
② 对象成员指针引用运算符左边的运算对象必须是类类型的对象,指针成员指针引用运算符左边的运算对象必须是类类型的指针,两个运算符的右边运算对象必须是成员指针
- 赋值指向成员函数的指针:
① 如果是类的静态成员函数,那么使用函数指针和普通函数指针没区别,使用方法一样
② 如果是类的非静态成员函数,那么使用函数指针需要加一个类限制一下
③ 成员指针只应用于类的非静态成员,由于静态类成员不是任何对象的组成部分,所以静态成员指针可用普通指针
(十八)常对象、常数据成员、常成员函数、常指针、常引用
- 常成员函数:
① 常函数定义:数据类型 类名::函数名()const; // 在声明和定义函数时都要有const关键字
② const成员函数表示该成员函数只能读类数据成员,而不能修改类成员数据。定义const成员函数时,把const关键字放在函数的参数表和函数体之间。
为什么不将const放在函数声明前呢?因为这样做意味着函数的返回值是常量(只读),意义完全不同。无返回值就返回值类型为void
③ 常成员函数可以访问const数据成员,也可以访问非const数据成员
④ 常成员函数不能调用另一个非常成员函数
⑤ const放在函数前分为两种情况:
(1) 返回指针,此时该对象只能立即复制给新建的const char,而不能是char,意在强调值被保存在常指针中
(2) 返回一个值,此时const无意义,应当避免(即本文档函数使用的第六条)
- 常对象:
① 常对象定义时const写在类名的前或后都行
② 常对象中的数据成员都是const的,因此必须要有初值,且无论什么情况其数据成员都不能修改
③ 除合成默认构造函数或默认析构函数外,常对象也不能调用非const型的成员函数
④ 可以修改常对象中由mutable声明的数据成员
⑤ 常对象数据成员只能被常成员函数访问,不能被非常成员函数访问
⑥ 只能用指向常对象的指针变量指向
- 常数据成员:
① const写在数据成员类型前
② 非静态的常数据成员只能通过构造函数初始化列表初始化
③ 可以被【非】const成员函数访问
④ 函数形参前加const关键字是为了提高函数的可维护性
- 指向对象的常指针:
① const在指针变量名前*后
② 对象的常指针必须在定义时初始化,因为指针的指向不能改变
- 指向常对象的指针:
① const写在类名前
② 即使指向一个非const的对象,其指向的对象依旧不能通过指针来改变
- 对象的常引用:
① const写在类名前
(十九) 组合
- 区分组合和继承:
① 继承:若在逻辑上B 是一种A (is a kind of),则允许B 继承A 的功能,它们之间就是Is-A 关系。如男人(Man)是人(Human)的一种,女人(Woman)是人的一种。那么类Man 可以从类Human 派生,类Woman也可以从类Human 派生(男人是人,女人是人);在继承体系中,派生类对象是可以取代基类对象的
② 组合:若在逻辑上A 是B 的“一部分”(a part of),则不允许B 继承A 的功能,而是要用A和其它东西组合出B,它们之间就是“Has-A(有)关系”。例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分,所以类Head 应该由类Eye、Nose、Mouth、Ear 组合而成,不是派生而成
③ 继承是纵向的,组合是横向的
(二十)嵌套类
- 嵌套类:
① 嵌套类是独立的类,基本上与它们的外围类不相关
② 嵌套类(内嵌类)的名字只在其外围类(包容类)的作用域中可见
③ 外围类对嵌套类的成员没有特殊访问权,并且嵌套类对其外围类也没有特殊访问权
④ 嵌套类就像外围类的成员一样,具有与其他成员一样的访问限制属性等
⑤ 嵌套类可以直接引用外围类的静态成员、类型名和枚举成员,当然,引用外围类作用域外的类型名或静态成员需要作用域运算符”::“
⑥ 在外围类域外定义的嵌套类对象其作用域不属于外围类
⑦ 外围类与嵌套类之间是一个主从关系
⑧ 使用嵌套类的目的是隐藏类名,减少全局标识符
⑨ 嵌套类中可以说明静态成员
⑩ 嵌套类也是局部类,必须遵循局部类的规定,嵌套类的成员也必须定义在嵌套类内部
(二十一)局部类
- 局部类:
① 局部类所有成员(包括函数)必须完全定义在类体内
② 局部类只能访问外围作用域中定义的类型名(即局部类的作用域局限于定义它的成员函数内部)、静态变量和枚举成员,不能使用定义该类的函数中的变量
③ 外围函数对局部类的私有成员没有特殊访问权,当然 局部类可以将外围函数设为友元
④ 在局部类中不能声明静态数据成员
⑤ 局部类的成员函数只能使用隐式内联方式实现
⑥ 局部类也是可以嵌套的,嵌套类的定义可以在局部类的之外,但是其定义要和局部类在一个作用域内
(二十二)this指针
- this指针:
① this指针存在的目的是保证每个对象拥有自己的数据成员,但共享处理这些数据成员的代码
② this指针并不是对象的一部分,this指针所占的内存大小是不会反应在sizeof操作符上的
③ this指针的类型取决于使用this指针的成员函数类型以及对象类型
(1) 假如this指针所在类的类型是Stu_Info_Mange类型,并且如果成员函数是非常量的,则this的类型是:Stu_Info_Mange * const 类型,即一个指向非const Stu_Info_Mange对象的常量(const)指针
(2) 假如成员函数是常量类型,则this指针的类型是一个指向constStu_Info_Mange对象的常量(const)指针
(3) this指针是const限定的,故既不允许改变this指针的指向,也不允许改变this指向的内容
④ this只能在成员函数中使用。全局函数,静态函数都不能使用this(静态与非静态成员函数之间有一个主要的区别就是静态成员函数没有this指针)。实际上,成员函数默认第一个参数为T* const register this
⑤ this在成员函数的开始执行前构造的,在成员的执行结束后清除
⑥ this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们也无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以&this获得),也可以直接使用的
⑦ 在C++中,根据this指针类型识别类层次中不同类定义的虚函数版本,因为this是表示当前对象函数的指针
⑧ 什么时候会用到this指针:
(1) 在类的非静态成员函数中返回类对象本身时,直接使用return *this;
(2) 当参数与数据成员名相同时,例this->n=n // 不能写成n=n