面向对象编程是一个巨大的编程范式。C++中的类class就是基于对象的程序设计。
我们可以用类来定义一个新的类型,这些新类型就可以像内置类型一样使用。
内置类型颗粒度太太小,现实需求又非常复杂,这就需要我们把内置类型适度的进行拼搭,拼搭成一个能描述现实问题的大粒度颗粒,来解决现实问题。
C++的数据类型有:常量、变量、布尔类型、指针类型、字符串类型、引用类型、枚举类型、数组类型、vector容器类型、复数类型、pair类型、类类型。所以类也是一种数据类型。
你可以把类看成一个新的数据类型,或者说是应用程序中的一种设施,这种设施是
把数据和函数封装在一起的设施
。
类的重点内容有:
a、怎样定义一个类?通过
共有类接口
和
私有类接口
实现。也就是
信息隐藏
(information hiding)的概念。
b、怎样定义和操纵类的对象实例?
类域、嵌套类、做为名字空间成员的类、局部类
...
c、类对象的初始化、析构、赋值如何实现?
特殊成员函数
:构造函数constructor、析构函数destructor、拷贝赋值操作符copy assignment operator...
这里重点强调一组特殊成员函数:
转换函数conversion functions
,就是将class类型定义一组标准转换。当类对象被用作函数实参、或作为内置或重载操作符的操作数时,这些转换函数就由编译器隐式的调用。
e、
操作符重载
的概念和设计,使我们能够使用内置操作符来操作class类型的操作数,使class类型对象的用法像内置类型对象的用法一样直观。 所以,我们把像赋值、下标、调用、new和delete等重载操作符声明为一个类的
友元friend
,使其拥有特殊的访问权限。
1、类和结构体的联系和区别
关于结构体的内容可以参考我的另外一篇博文:
【C语言学习笔记】八、结构体-CSDN博客
C++之所以兼容结构体,是因为希望和C保持兼容性。结构体是C的语法,但C中没有类。
类和结构体可以说没什么区别。唯一的区别就是结构体中的变量默认都是共有的,类中的变量没有public声明就都默认是私有的。
public表示可以在类以外的任何地方访问这些变量
2、什么时候用类什么时候用结构体?
虽然类和结构体没有太大的区别,但是它们还是各自有各自的应用场景,不是随便通用的。比如当我们想管理很多变量时,那你用结构体,你的代码就非常清晰。如果你还想写一些函数,甚至是使用继承,建议还是写成类的形式,因为这会涉及到更多的内容。比如当一个结构体继承一个类的时候,编译器就会警告,虽然程序还是可以运行,但还是有一些语义上的区别的。
总之:结构体的定位是数据的结构,就是只用结构体表示一些数据;如果你还实现更复杂的功能,那就用类。
3、类定义、类声明
4、类的 数据成员、成员函数
可见数据成员就是我们经常说的属性;成员函数就是我们经常说的方法。以后我们尽量用属性和方法来表述。
5、可见性:public、private、protect
小结:public、private、protest是从类自身、类实例、类继承方面规定了类属性和类方法的作用域。
后面我们还要学一个关键字static。在类中的static则是从类内、类外维度(命名空间维度)规定了类属性和类方法。详细内容参考下面的第7点。
6、写一个最简单的类
(1)梳理需求
我打算写一个类,类的功能是实现日志信息管理。
由于日志系统可大可小,可简可繁,不仅是打印信息到控制台,还可以打印不同的颜色、或者通过网络输出日志信息到一个文件。所以一个log系统可十行代码也可上万行。
现在我就写一个最最简单的Log系统,实现向控制台写入文本的能力,并且区分日志级别(错误、警告、信息或跟踪)的功能即可
。
(2)分解需求
把日志信息分3个级别:错误、警告、信息或跟踪
当我把级别设置为"信息",就打印信息、警告、错误3种日志;
当我把级别设置为"警告",就打印警告、错误2种日志;
当我把级别设置为"错误",就打印错误1种日志;
(3)代码写作过程:
上面的Log类中的公共变量我用了两次public是因为:我喜欢把类中不同的部分分开来写,比如,public方法写在一部分,public变量又放另一部分,public静态变量又会放其他一部分。这只是每个人的编程风格而已,只是为了更清晰一点而已。
说明:上面的步骤只是展示了如何逻辑清晰的写一个类,但事实上上面的代码是非常糟糕的,后面我们将使用更多的概念来改进这个类,使其达到专业生产级水平的代码。
7、静态static
C++中的静态static关键字有3个意思:
一是,当你在类或者结构体外部使用static关键字时,表示你static的符号,其链接只能在内部,也就是你static定义的符号只能在翻译单元可见。
二是,当你在类或结构体内部使用static关键字时,表示该符号将与类的所有实例共享内存。也就是说该静态变量是该类类型的所有对象共享访问的。同样的效果也适用于静态方法。
三是,函数内部使用static关键字时,就类似python中的闭包效果。
(1)static关键字在类或结构体外部时:
上图s_Variable是定义一个静态变量,s表示这个变量是静态的,意思是这个变量只会在这个翻译单元内部链接。
静态变量或静态函数意味着,当需要将这些变量或函数与实际定义的符号链接时,链接器不会在这个翻译单元的作用域之外,寻找那个符号定义。
(2)static关键字在类或结构体内部时:
如果我创建一个名字叫Entity的类,我不断创建Entity的实例,但是static变量或函数永远只有一个,
所有实例共享这一个static变量或函数
。
所以,如果某个实例更改了static变量或函数,那所有实例的这个变量和函数都会跟着改变。所以,我们一般都不会通过实例更改static变量或函数,没意义嘛。
所以设置static变量的目的只有一个:就是所有实例可以共享这个变量。而不是通过实例去改变这个变量,这样做毫无意义。
比如银行账户,每个账户的姓名和余额都不一样,但每个账户的利率是一样的。如果我们给每个账户都单独设置一个利率变量,是不是就非常浪费。所以使用static变量是有意义的。
如果我们代码开头用了using namespace std;语句,那比如cout,我们就用std::cout,但是这里的x,y被static限制后,就是在Entity域中的名字,所以我们要用Entity::x。上图中间图的红框,就是我们声明xy的命名空间,所以后面我们可以使用e.x,e.y来直接引用。
意思就是x,y是Entity空间的公共变量,所以Entity空间产生的实例都是可以调用和修改的。从这个意义上说,x,y就既可以是pubulic也可以是private.
在类中的每个非静态方法都是要获取当前类的一个实例作为其参数的,这可以类比python中的self参数。
但是类中的静态方法是不需要实例为其参数的。类中的静态方法可以被任何实例调用、也可以通过命名空间直接调用,因为在类里是公共的嘛。
而且类中的静态方法只能访问和修改类中的非静态变量。类中的静态方法是不能访问类中的非静态变量的。
总之,关键字static就是一个作用域的功能。static变量或函数是类外的变量和函数,虽然它也写在类内部;非static变量或函数是类内的变量和函数。类外的函数访问类外的变量,肯定访问不了类内的变量了。
(3)static在函数内部时:
static在函数内部时,也叫局部静态 local static
局部静态变量允许我们声明一个变量,这个变量的生存期相当于整个程序的生存期,但是作用域确实这个函数内部的。这一下就让我想起python中的闭包!是不是,你有没有同感!
小结:函数可以访问外部变量,也可以在函数体内更改外部变量。但是函数体内的变量只能在函数体内改变。但是如果函数体内的变量是static的,那这个变量就相当于是全局变量,但是这个全局变量不能在函数体外更改,只能被该函数更改。
8、枚举类
枚举ENUM,enumeration的缩写。枚举就是一个数值集合。就是给一个名字赋值,但不是平时我们说的就赋一个值的那种,是另外一种赋值方法。
9、构造函数
类在每次实例化对象时,都会运行一个特殊的方法就是构造函数。
构造函数的名称必须与类的名称相同、必须没有返回值、可以有参数也可以没有
!
所以构造函数就是类中的一种特殊方法,就是每次实例化一个实例的时候,就自动调用的一个方法。上图是我们手动写了一个构造函数,也就是我们自己指定了一个构造函数。如果我们在类中不写构造函数,那就会有一个已经写好的构造函数默认让你用了,就是默认就已经执行了一个构造函数。
如果不实例化对象,那构造函数就不会被运行。
总之,
构造函数就是创建类实例时运行
。有人知道是为什么吗?
有心的小伙伴估计都已经猜出来了:比如上图你现在写的类Entity,即使你不写Entity这个特殊成员函数————构造函数,其实你还是要执行一个初始化的构造函数的!因为即使你的Entity类不是继承的子类,但只要你写类,肯定就默认有一个父类,就是默认你已经继承了一个父类,这个父类就是所有类的父亲,不管啥类都是这个父类的子类,所以当我们把Entity类写完后,初始化Entity时,其实首先运行的代码就是父类的代码(
在上面小标题5-可见性的最后一个小例子就有演示
!),而父类的代码中就肯定有一个父类的构造函数,所以父类的这个构造函数首先被执行,所以你实例化Entity时,其实构造函数就已经被在你看不见的地方被执行了。
但是这里还有一个道道就是,如果我在Entity中也写了我自己的构造函数,那此时如果你的构造函数和父类的构造函数是同名同参,那就不会执行父类的构造函数了,只执行Entity的构造函数,这叫
函数重载
。而且这也是允许的,所以不仅仅是构造函数,任何函数都适用。如果你决得父类中哪个方法不顺眼,那你可以在子类中再写一个同名的方法,那子类中的方法就自动替换了父类中方法。当然对于构造函数,它比较特殊,因为它要和类名同名,所以一般情况是,父类的构造函数一般和子类的构造函数不同名(因为我们写类不可能写一个和父类一样名字的类),那此时子类实例化对象时,其实是执行了2次初始化,一次是父类的初始化代码,一次是子类的初始化代码。
或者这么说吧,我们显式的看到你写的类Entity只有类体中的那些代码,其实当你实例化这个类时,系统给这个类分配的内存是大于这个类本身拥有的变量的长度的,就是编译器自动给你写的类拷贝了父类中的代码,所以你现在写的Entity类编译完毕,其实前半部分是其父类的代码,后半部分才是Entity的代码。而前半部分父类的代码中又有父类的同名构造函数,所以是不是就先执行了父类的构造函数,这是第一次初始化。然后运行到后半部分代码,也就是Entity代码部分,Entity中又有和Entity同名的函数,也就是Entity自己的构造函数,此时就要执行Entity函数了,也就是第二次初始化了。
说明:此处如果你使用的是类的静态方法那就没法实现了,因为类的静态方法无法访问类中的非静态变量。
以后还会讨论堆内存的分配问题。当我们使用new关键字创建一个对象实例时,也会调用构造函数。
也就是因为这种特殊函数,或者说因为这种重载特性,除了用于初始化的构造函数外,我们还可以写一些,比如,删除构造函数、复制构造函数、移动构造函数等等。。。
10、重载、多态
本来讲完构造函数就应该开始讲析构函数了,因为它们是一对儿的。但是前面频频提到重载和多态,所以这里把重载和多态先讲了。
重载也叫函数重载,意思就是你写类的时候,即使你没有继承任何父类,其实底层也是默认你继承了一个元类,就是类的祖宗。所以你写的类编译后的代码其实是把祖宗的代码也复制了一份后的代码,也就是加入了祖宗的代码指令。当然如果你写类的时候有继承,那你的代码在编译时,编译器就拷贝了祖宗的代码+你继承的类的代码。这才是你的类的全部指令。
所以,这里就出现一个问题,当你看祖宗或者你父类中哪个方法不顺眼时,你可以写一个
同名同参
的方法,此时你自己的方法就替换了祖宗或者父类中的同名方法,这就叫做
函数重载
。C++的类是支持函数重载的。
如果你不写类,你直接写两个同名同参的函数,那编译器会毫不客气地给你报错,不给你编译。但是你在类中写两个同名同参的方法,ok,没问题,后面的方法直接覆盖前面的方法。一切都顺利。
那么
多态
是什么呢?就是
有相同的函数(方法)名,但有不同的参数的不同函数(方法)版本
。前面我们说如果你写两个同名同参函数,编译器会报错,但是如果你写的是两个同名不同参的函数,那是没关系的,不会报错。当函数调用时,编译器会根据参数判断调用哪个函数。这就是多态。同理,如果同名不同参的函数写在了类里面,那就是两个同名不同参的方法,编译器也是不会报错的,更不会像同名同参那样覆盖的!也是根据参数判断调用哪个方法。
下面一个小例子演示一下什么是重载和多态:
可能大家有些晕了,这里再来一波小结:
同名同参函数只能写到类里面,否则编译器就报错。写到类里面的同名同参函数(方法)肯定是一个在父类里面,一个在子类里面,因为子类看不惯父类的这个函数的功能,所以自己再写了一个同名同参的,替换了父类中的。这就叫
重载
。就是自己在子类中写的同名同参函数把父类中的函数替换了,或者说截胡了,所以叫重载。
同名不同参的函数,不管是写在类里面还是写在类外面,编译器都不会报错!!编译器是接受同名不同参的。这叫
多态现象
。编译器会根据参数来决定调用哪个函数。
构造函数
是在类里面写的一个
和类名一样
的函数,但是这个函数没有返回值,就是连void也不能写!而且这个函数可带参数也可不带参数。当编译器看到这样的函数时,类实例化时,这个函数就直接一起执行了。所以实例化后的实例对象都是已经初始化过的了。
说明:后面还有虚函数,也是和这些概念搅合在一起的,建议本小部分和后面的11虚函数部分一起看,后面的虚函数也用的是这里的案例。
11、析构函数
构造函数是你创建一个新的实例对象时运行的。就是是在创建新的实例对象时,自动被调用的,通常用于设置变量或者一些初始化功能。
而析构函数则是在销毁对象时运行的。一个对象要被销毁时,析构函数就自动被调用,通常是卸载变量、清理你使用过的内存等功能。
析构函数同样适用于栈和堆分配的对象。如果你用new分配一个对象,当你调用delete时,析构函数就会被调用。如果只是一个栈对象,当作用域结束时,栈对象将被删除,此时析构函数也会被调用。
你在构造函数中初始化了一些变量,你就得在析构函数中卸载或销毁这些变量,否则就容易内存泄漏。
比如如果你在堆上手动分配了任何类型的内存,那么你得手动清理。
12、继承
重写、多态、继承是类的几个重要特点。重写和多态前面反复演示过了,所以这里讲继承也都简单多了。
类之间的继承,就是相互关联的类的层次结构,这些继承关系是一个虚表(V-table)来维护的。
继承最主要的好处就是避免代码重复编写。包含公共功能的基类--从基类中分离,从最初的父类中创建子类
我们可以把类之间的公共功能放到一个父类(基类)中,然后从父类中创建的子类就免去了相同的代码一遍遍的复制。也就是好像不用一遍遍写模板了。
13、关键字virtual、override:通过虚函数让你完全无视被重载的父类方法
虚函数是面向对象编程中非常重要的概念。
虚函数和前面的重载、多态都是有联系和区别的。把前面的重载和多态都彻底弄明白了,这里也就很容易明白。
下面这个例子是对指针和类的详细拆解:
明白了指针后,我们再接上面的8(重载和多态),继续看:重载并不是覆盖,也不是删除,只是替换执行而已。所以我们用指针还是可以找到被重载的函数的:
从上例也可以看到,子类中同名同参的方法虽然可以通过重载,替换父类中的相应方法,但并不意味着父类中的那个同名同参的方法不存在了,其实还是存在的。如果想让它等同不存在,就要用虚函数,也就是使用关键字virtual和override:
说明: 虚函数是引入了一种叫动态联编(Dynamic Dispatch)的概念,就是通过维护一个V表(虚函数表)来实现的,基类中有一个成员指针,指向V表,所以生成V表是需要额外的内存来存储V表的,是要增加一点开销的。
V表是一个表,它包含基类中所有虚函数的映射,这样在virtual方法运行时,就将它们映射到正确的覆写(voerride)的函数。也就是如果你覆写了一个基类中的函数,那就将基类中的基函数标记为虚函数virtue,然后把你写的那个函数标记为覆写override,就表示是重写了。
小结:当类中出现同名同参的方法时,就会自动启动重载机制,就是自动执行子类中的同名同参方法。但是这并不表示父类中的同名同参方法被删除了、不存在了,通过指针还是可以调用的。所以如果你永远不想用父类中的方法,那你就用虚函数,即使指针调用了父类中的同名同参方法,也会自动跳转到被覆写的子类方法上。
14、C++接口(纯虚函数)
纯虚函数是一种特殊类型的虚函数。其本质上与其他语句(如Java或C)中的抽象方法或接口相同。
纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数。
其实这种做法在面向对象编程中是非常常见的,这通常被称为
接口
interface,其他语言有interface关键字,而C++中的接口是一个类,这个类中只包含一个未实现的方法:
说明:
一是,纯需函数必须被实现,才能创建这个类的实例。
上例中的Entity必须得重写GetClassName,因为Entity继承了Printable,而Printable中有纯需函数,所以类Entity是必须得重写得,否则无法实例化。
但是Player类是继承的Entity类,而Entity类中没有纯虚函数,所以类Player是没有像Entity类中GetClassName那样的必重写函数。但是Entity中有虚函数GetName,意思就是函数GetName可以被重写,所以Player中重写了两个GetName,这里是展示多态这个知识点。
二是,接口只是C++的一个类而已。有了这个类,我们就可以将这个类(抽象基类)作为参数(类型)放入一个通用的函数中。
上例中,如果类Player也重写了类Printable中的纯需函数GetClassName后,是不是就和Entity类一样,当然其他更多的类都同理,都可以作为Print函数的实参了。所以纯虚函数所在的类就是一个接口,它让所有重写它的类都可以变成一个统一的实参。这样类就可以当实参传递了,就可以调用这个方法或做其他的事情了。反观函数PrintName,其参数只能限制在Entity的长度,而利用Printable类中的纯虚函数GetClassName,就可以实现任意长度的实参传递了。因为通过virtual和override,也就是V表进行映射了,让代码执行跳转了。
静态
枚举
C ++
什么是
静态
枚举
?
静态
枚举
是一个单头C ++ 17库,它提供编译时
枚举
信息,而无需使用任何宏,也不必使用一些宏魔术来定义
枚举
。 据我所知,这是第一个实现get_enumerator实现的库。
static_enum::get_enumerators创建具有所有
枚举
值(按值排序)的std::array<Enum>
static_enum::enum_cast可以像static_cast一样用于将
枚举
转换为字符串或从
枚举
创建字符串
static_enum::to_string从
枚举
变量获取名称,返回constexpr std::optional<std>
static_enum::from_string从字符串中获取
枚举
变量,返回一个constexpr std::optional<Enum>
缺点在哪里?
静态
枚举
使用编译器内部函数-即
文章目录
C++
类
的常量数据成员,
静态
数据成员,常量
静态
数据成员const成员static成员
静态
整型常量数据成员
C++
类
的常量数据成员,
静态
数据成员,常量
静态
数据成员
const成员
const成员可以提供
类
内初始值,也可以在构造函数初始化列表中进行初始化。
#include <iostream>
using namespace std;
class A {
public:
A(int temp) :j(temp){}
A(int a, int b) : i(a), j(b)
看开源代码看到
类
的
静态
成员函数可以使用的
类
内定义的
枚举
值,似乎与之前
学
习的时候看到
静态
成员函数只能调用
类
内的
静态
成员变量。于是做了一个小实验,发现真的可以,不知如何解释。做一个小的记录,有时间调查一下,如有大神指教。 #include<iostream>
using namespace std;class testClass{
enum{
MODIFY = 1,
实例成员:非
静态
成员,需要实例化才能访问。
静态
类
:一个
类
的所有成员都是
静态
的。
枚举
:
静态
成员最常用的功能就是
枚举
,
枚举
是一组有意义的常量。常量值一般是相关的,为方便起见而将其作为一个
类
的属性组合在一起。
枚举
就是
静态
常量。
function Day(){
throw new Error("这是
静态
类
,不...
哎,零散的几个名词,无数次的让我感到茫然。似乎知道,却又说不出其中的区别。真是“无可奈何花落去,似曾相识燕归来”。四个字“模糊记忆”。
今天实在忍受不了这种对一个程序员智商的屈辱了,决定弄个清楚:
多态
是一种机制,这种机制可以有多种实现形式:比如覆盖、
重载
、
重写
、
模板
都是
多态
的实现形式。简而言之,在一个
继承
关系中(一个
类
或多个
类
),调用相同的函数名却执行不同操作的一种机制。
实现
多态
的函数返
C++
多态
有两种形式,动态
多态
和
静态
多态
;动态
多态
是指一般的
多态
,是通过
类
继承
和
虚函数
机制实现的
多态
;
静态
多态
是通过
模板
来实现,因为这种
多态
实在编译时而非运行时,所以称为
静态
多态
。
动态
多态
例子:
#include
#include
*Shape
class CShape
public:
CShape(){}
virtual ~CShape(){}