有什么补充或质疑欢迎在评论区指出来。
类 I
——若在类体内没有指明访问权限,默认的访问权限为private。
——类是一种数据类型,系统并不会为其分配内存空间。
——构造函数重载:同名不同参
——带有子对象的构造函数的执行顺序:https://blog.csdn.net/qq_15989473/article/details/103215333
拷贝构造函数
——拷贝构造函数的定义
1 | Sample(Sample &S) |
——拷贝构造函数的三种调用情况:
1 | // 1&2.同类对象的初始化 |
析构函数
——析构函数不能重载
1 | ~A(); //析构函数声明 |
——析构函数和构造函数的调用顺序相反
静态成员
——静态数据成员只能在类体内声明,类体外初始化(假如有一个类Sample)
1 | static int num; //类体内声明,需要static关键词 |
——静态成员的访问有两种方式:类名::函数名(); 或者 对象名.函数名();
1 | Sample::function(); |
推荐用第一种,因为指明了静态成员是属于整个类的。
——非静态成员函数可以任意访问静态和非静态成员 ,静态成员函数只能访问静态成员(数据成员或成员函数)
类 II
——系统给对象分配的内存只是用来存储数据成员的。成员函数的代码统一放在程序的代码区
this指针
——this指针是指向本类对象的指针。
——this指针是在用对象引用成员函数时系统自动创建的。
——this指针是被隐式定义在非静态成员函数的形参中。
——类的静态成员函数没有this指针。因为静态成员函数为类的所有对象所共有,不专属于某一个对象。所以在静态成员函数中不能直接访问非静态数据成员(因为没有this指针)
各种“常”
——常数据成员的初始化必须要用构造函数的初始化列表完成。
1 | Sample(int temp){const_num = temp;} // × |
常成员函数
——只能调用const成员函数。
——可以使用const与非const数据成员,但不能修改。
1 | int function() const; //声明 |
常对象
——常对象是指对象的数据成员的值在对象被调用时不能被改变。常对象必须进行初始化,且不能被更新。
1 | //定义格式(必须进行初始化) |
指向对象的常指针
1 | //指向对象的常指针 |
指向常对象的指针变量(指针值可以改变)
1 | //指向常对象的指针变量 |
注意与常指针相区分
总结
各种“常” | 含义 | 成员函数 | 数据成员 |
---|---|---|---|
void Sample::function() const | function为常成员函数 | 只能调用const成员函数 | 常变量都可以使用(包括private),但不能改变其值 |
const Sample& s=obj; | s是常引用,可以认为把对象obj的属性变成了const | 与常对象权限相同 | 与常对象权限相同 |
const Sample *pr | pr是指向常对象的指针 | 与常对象权限相同 | 与常对象权限相同 |
Sample const obj | obj为常对象 | 只能调用const成员函数 | 常&变量都可以使用(前提public),但不能改变其值 |
Sample *const pr=&obj; | pr为常指针(指针值不可以改变) | const & 非const 都可调用,形式:pr->function(); | const & 非const 都可调用,形式:pr->number; |
友元
友元函数
(此处源代码来自菜鸟教程 原链接)
——友元函数并不是成员函数,但有权访问私有、保护和公有成员(所有成员)。
——友元函数在类内声明,在类外定义
——友元函数不能直接访问类的成员,只能访问对象成员。
——调用友元函数时,在实际参数中需要指出要访问的对象。如下述printWidth函数中的形参Box box
——类与类之间的友元关系不能继承。(也就是说基类的友元函数继承不到派生类中)
1 | class Box |
——特别注意下面这种写法是错误的
1 | b.printWidth(b); //错误 |
因为友元函数不属于类对象b的成员函数。
友元类
——如果A 是 B的友元类 -> A的成员函数可以访问 B的私有成员
——友元类之间的关系不能传递,不能继承(此处来源)如:
B 是 A 的友元,C 是 B 的友元,C 不是 A 的友元。
A 是 B 的友元,不代表 B 是 A 的友元。
类模板
——类模板,可以定义相同的操作,拥有不同数据类型的成员属性。
——通常使用template来声明。告诉编译器,碰到T不要报错,表示一种泛型.
如下,一个普通的类模板:
1 | template <class T> |
——模板的实例化指函数模板(类模板)生成模板函数(模板类)的过程。对于函数模板而言,模板实例化之后,会生成一个真正的函数。而类模板经过实例化之后,只是完成了类的定义,模板类的成员函数需要到调用时才会被初始化。模板的实例化分为隐式实例化和显示实例化。(原文出处)
——类模板的参数也可以是非类型参数,普通值也可以作为模板参数
1 | template<class T,int NUM> |
继承与派生
三种继承方式的特点图解
公有继承
私有继承
保护继承
不同位置的访问权限
派生类成员 | 派生类中 | 派生类外部 | 下层派生类 |
---|---|---|---|
公用成员 | 可以 | 可以 | 可以 |
保护成员 | 可以 | 不可以 | 可以 |
私有成员 | 可以 | 不可以 | 不可以 |
不可访问成员 | 不可以 | 不可以 | 不可以 |
直接间接 基类或派生类
构造函数与析构函数的调用顺序
构造函数
析构函数
析构函数与构造函数的调用顺序刚好相反
多重继承
基本概念
声明方法
有关二义性
二义性的两种情况
消除二义性的两种方法
举个栗子
——我们先定义两个基类A与B,里面包含最简单的公有成员show:
1 | class A{ |
——然后再用一个类C继承A与B:
1 | class C:public A,public B{ |
——在主函数中调用公有成员show
1 | int main() |
——结果意料之中的报错了
1 | [Error] request for member 'show' is ambiguous |
——因为编译器不知道c.show()中的show到底是A的show还是B的show
解决方案1
√——用相应的类名来标识(消除二义性的方法一),在主函数中修改:
1 | int main() |
解决方案2
√——由派生类提供统一的接口
——那么我们重新定义一下C中的show成员函数,在派生类成员函数中用类名标识符调用基类同名成员函数:
1 | class C:public A,public B{ |
——这样的话就可以在主函数中直接用c.show()来实现用派生类的show函数间接调用A和B的show函数了:
1 | int main() |
解决方案3
√——利用同名隐藏
同名隐藏:当基类与派生类有同名成员时,派生类的成员会将基类成员屏蔽。
如果在定义派生类对象的模块中通过对象名访问同名的成员,则访问的是派生类的成员。
这里我们可以在派生类C中重载show成员函数,那么调用c.show()的时候就会优先用派生类的show函数,基类的show被屏蔽掉了。
比如给C添加一个同名成员show:
1 | class C:public A,public B{ |
这样的话就可以在主函数中直接调用,而不需要 类名::
1 | int main() |
虚基类
提出一个问题
——假设我们有这样组成结构的几个类:
——具体实现:
1 | class C{ |
——分析:这里C类被A类与B类继承,在A类与B类中分别有两个“不同”的数据成员num(来自C类)。也就是说,C类中的num变成了两份,一份在A中,一份在B中。
——验证一下分析:
1 | int main() |
——问题:但是num是C类里面的,他不应该有两份,这该怎么解决呢?
权宜之计
——使用虚继承的方式=>虚基类
——只需要让A和B虚继承C类,这样的话C就会只保留一份,比如我们改写一下A与B:
1 | //注意虚继承的格式:virtual 继承方式 基类名 |
——再在主函数中验证一下,A中的num与B中的num是否为同一份:
1 | int main() |
——可以看出,我们改变了A中的num,B中的num也改变了,说明C在继承过程中只保留了一份
——同时,因为A与B中的num是同一份,我们还可以用对象名加点的方式直接访问,而不会产生二义性。
1 | int main() |
虚继承的问题
——虚继承的构造函数是很麻烦的。
——清华大学郑莉老师的解释,传送门(自行跳转8:50):https://www.bilibili.com/video/av41347930?p=32
多态性
多态性概述
——多态是指操作系统接口具有表现多种形态的能力,即能根据操作环境的不同采取不同的处理方式。多态性是面向对象系统的主要特征之一,在这样的系统中,一组具有相同基本语义的方法能在同一接口下为不同的对象服务。
——多态的类型:重载多态、强制多态、包含多态、参数多态。
——多态的种类:C++语言支持的多态性可以按其实现时机分为编译时多态和运行时多态两类。
——绑定:是指把一个标识符名和一个存储地址联系在一起的过程。
——编译时的多态:绑定工作在编译连接阶段完成的情况称为静态绑定。
——运行时的多态:绑定工作在程序运行阶段完成的情况称为动态绑定。
运算符的重载
复数类为例
说明都在注释中给出
1 | class Complex{ |
最好重载为友元
比如:
1 | //+运算符重载的定义(重载为成员函数) |
这里的obj1+obj2相当于obj1.operator(obj2);
也就是说第一个操作数必须为这个类的对象
因此,如果我用10+obj,显然是不行的。
为了解决这个问题,我们要把运算符重载为友元函数。
1 | class Complex{ |
用cout的方式输出复数
普通的数甚至字符串都可以用cout直接输出,那么我们自己定义的类呢?当然可以!
先说明一下,通过查阅文档可知
1 | cout << a << b; |
相当于:
1 | operator<<(operator<<(cout,a),b); |
那么我们就可以自己重载一个了。
方法如下
在Complex类的public内声明一个友元函数
1 | //这里返回引用是为了下一次嵌套调用。 |
在类外定义:
1 | ostream& operator<<(ostream &out,Complex &obj){ |
这样就可以愉快的调用啦!
1 | int main() |