目录
一.面向过程和面向对象
1.两者的定义
2.两者的特点
3.两者的实现方式
二.类的基本知识
1.类的定义
2.成员权限
3.类大小计算 —— *内存对齐规则
4.this指针
三.类的默认成员函数
*1.默认构造函数
*2.析构函数
*3.拷贝构造函数
a.拷贝构造的特征
b.拷贝构造函数的作用时机
c.深拷贝与浅拷贝
*4.赋值运算符重载
四.初始化列表
1.什么是初始化列表?
2.初始化列表存在的意义?
3.初始化列表对不同类型数据的初始化规则
a.内置类型的初始化
b.类类型成员的初始化
c.初始化列表与构造函数体的关系
4.隐式类型转换
五.类的static静态成员
1.静态成员变量
a.声明与定义
b.使用场景与特性
2.静态成员函数
a.声明与定义
b.静态成员函数的特性
一.面向过程和面向对象
1.两者的定义
什么是面向过程?
面向过程是C语言的编程范式,以过程为核心,强调事件的流程性和顺序性。它直接将解决的问题分析出来,然后用函数将步骤一步一步实现,再依次调用这些函数来完成整个任务。关注的是问题的解决步骤,而不是问题的本质。
什么是面向对象?
面向对象是C++的编程范式,以对象为核心,强调事件的角色和主体。它通过分析问题,将构成问题的事物分解成若干个对象,这些对象通过相互调用和协作来解决问题。关注问题的本质,而不是解决问题的具体步骤。
2.两者的特点
面向对象(C++):①适合解决复杂的问题,需要多方的协作和抽象;②代码复用性高,扩展性好,易于维护;③提供了封装、继承、多态三大特性,这些特性提供了高度的灵活性、可维护性和扩展性,适合处理复杂的程序设计和大型项目。
面向过程(C语言):①适合解决简单的问题,不需要过多的协作和抽象;②关注问题的解决步骤,使得编程任务明确,具体步骤清晰,便于节点分析,效率高。③但代码复用性低,扩展性差,不易维护。同时,面向过程编程中只有封装,没有继承和多态。
我们知道,C++是C语言的 Plus 版,那么,为什么C++的编程范式不继承C语言的面向过程,反而要改为面向对象呢?
原因:C++选择面向对象编程范式是为了更好地满足人类思维习惯、提高代码复用性和扩展性、易于开发大型软件产品、提升代码稳定性和可维护性以及满足复杂系统设计的需要。
3.两者的实现方式
面向过程的编程主要关注在程序中以函数的形式组织代码,程序被分解成一系列可重用的函数,每个函数执行特定的任务,整个程序的控制流程通过函数之间的调用来实现。
面向对象的编程主要关注在程序中以类的形式组织代码,程序被分解成一系列具有关联性的类对象,不同类对象的含义不同、方法集也不同,整个程序的控制流程是通过不同类对象间的协作和调用来实现。
函数,我们在C语言中对其已经是非常熟悉了,那么,类是什么,又该如何理解??
我们知道,面对对象(类)的三大特性:封装、继承、多态;
封装 —— 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现的细节,仅对外公开接口来和对象进行交互。
继承 —— 在某些特定的场景下,所有对象既要有一些共性属性,但每个对象又要有一些个性化的属性。这时,我们可以创建一个描述公共属性数据的类(基类),然后用个性化的类(派生类)去继承它。
多态 —— 具有父子关系的两个类对象去完成一个相同的行为(调用同一个函数),而表现出不同的结果(执行方法不同),即传入不同的对象,调“看似相同的函数”,拥有不同的实现方法。
本文,咱们重点聊封装,也就是类的实现~~
二.类的基本知识
1.类的定义
类是一个用户定义的类型,它封装了数据(成员变量)和操作这些数据的方法(成员函数)。类定义了对象的结构,包括它可以存储什么类型的数据以及可以对这些数据执行哪些操作。
2.成员权限
public(公有)、protected(保护)、private(私有)
a. public 修饰的类成员可以在类外面被直接访问;
b. protected 和 private修饰的成员不可以在类外面被直接访问;
c. 访问权限的作用域从该访问限定符出现的位置到下一个访问限定符出现的位置为止;
d. *class的默认访问权限是private, struct由于要兼容C语言,故其默认访问权限是public.
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
类的实例化
简单来说,类的实例化,就是定义一个类对象。如上图中的 Date d("2024.10.14"); 这就是类的实例化~~
类对象的大小该如何计算??--- 对象的大小只计算成员变量(遵循内存对齐规则),不计算成员函数!!
3.类大小计算 —— *内存对齐规则
a.第一个成员在结构体偏移量为0的地址处;
b.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认对齐数(VS默认对齐数是8) 与 该成员变量大小 中较小的那个值;
c.结构体总大小为:最大对齐数(所有变量类型最大者 与 默认对齐数 中较小的那个)的整数倍;
d.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
举个例子,计算类的大小:
注意:没有成员变量的类对象,需要1byte,是为了占位,表示对象存在,而不存储数据!
如:class A1{public : void f2()} 和 class A2{} 的类对象大小都是1个字节。
4.this指针
类实例化出来的对象,调类里面的成员函数时,表面上:d1.Print( ); 实际上: d1.Print( &d1 );
void Print(Data* const this){ }
this是成员函数的隐藏参数,它指向调用成员函数的类对象地址。注意形参的类型,其指针是被const修饰的,this无法改变,但*this(其指向的内容)可以改变!!
this不能在形参和实参显示传递,但是可以在函数内部显示使用,由于this是形参,所以this指针是跟普通参数一样存在函数的栈帧里面。
A* p =nullptr; p->Print();
p调用Print(),*Print的地址在公共代码段,而不在对象中,所以*不会发生解引用!!
三.类的默认成员函数
在C++中,当一个类被定义时,编译器会自动为其生成一些默认的成员函数,这些函数被称为“默认成员函数”(即,如果我们不写,编译器会自动生成)。
*1.默认构造函数
想要理解默认构造函数,前提是先了解什么是构造函数?
构造函数是一种特殊的成员函数,它的主要作用是初始化类的对象。当你创建一个类对象时,构造函数会被自动调用,来给类对象的成员变量赋初始值。
构造函数的特征:
①函数名与类名相同;
②函数无返回类型,也没有返回值;
③对象实例化时编译器自动调用对应的构造函数;
④构造函数可以重载;
⑤如果类中没有显示定义构造函数,那么C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义,编译器将不再自动生成。
⑥无参的构造函数和全缺省的构造函数只能存在一个。
所以,什么是默认构造函数?--- 不传参数就能调用的构造函数,称为默认构造函数(只能同时存在一个)。
编译器默认生成的构造函数,对内置类型的成员变量不做处理,对自定义类型的成员变量,会去调用它的构造函数!!
所以,当类中有内置类型成员时,就需要自己写构造函数,不能用编译器自己自动生成的;只有当类中全是自定义类型成员时,可以考虑让编译器自动生成!!
在C++11中,可以给在声明时赋给成员变量缺省值,去弥补编译器自动生成的默认构造函数的缺陷。
构造函数的调用,举例:Date d; √ Date d1(2024,3,3); √ d1.Date(2024,3,3); √ Date d2(); ×
第一种:调用默认构造函数;第二种:调用构造函数;第三种:匿名对象,调用构造函数;第四种对象实例化错误原因:与函数的声明有冲突,编译器难以识别。
*2.析构函数
当一个对象不再被需要,即将被销毁时,析构函数会被自动调用。析构函数的主要任务包括释放对象在生命周期内动态分配的资源,如内存、文件句柄、网络连接等,以防止资源泄露。
析构函数的特征:
①析构函数名是在类名前加上字符"~" ;
②析构函数没有参数和返回值类型 ;
③一个类只能有一个析构函数(即析构函数不能重载),若未定义,系统会自动生成默认的析构函数。
④对象的生命周期结束时,C++编译系统会自动调用该对象的析构函数;
系统默认生成的析构函数,对内置类型不做处理,对自定义类型会去调用它的析构函数!!
在函数内实例化出来的对象,其开辟出来的空间在函数的栈帧上,根据栈的后进先出原则,后被定义的对象会被先释放(析构)。
*3.拷贝构造函数
拷贝构造函数是由编译器调用来完成基于同一类的其他对象的构建及初始化的函数。其形参必须是引用,但并不限制为const,一般普遍的会加上const限制以避免修改实参。拷贝构造函数名的一般形式为:ClassName(const ClassName& obj)。
a.拷贝构造的特征
① 拷贝构造函数是构造函数的一种重载形式;
② 拷贝构造函数的参数只有一个且*必须是类对象的引用,用传值的方式编译器会报错,因为会引发无穷递归调用。
---原因:当函数的参数是内置类型时,实参会直接拷贝给形参;而当其参数是自定义类型时,实参会以拷贝构造的形式传给形参。
③ *若未显示定义,编译器会生成默认的拷贝构造函数,默认的拷贝构造函数对内置类型对象的内存存储按字节完成拷贝,这种拷贝叫做浅拷贝,或值拷贝;对自定义类型对象会调用它的拷贝构造函数。
b.拷贝构造函数的作用时机
① 用一个对象去初始化同类的另一个对象时。例如:Person p2(p1);
② 做函数参数,且为传值传参;或做函数返回值,传值返回时。
c.深拷贝与浅拷贝
浅拷贝:只复制对象中的成员变量的值,而不复制指向动态分配内存的指针。这意味着原始对象和拷贝对象将共享相同的内存资源。浅拷贝可能导致多个对象同时释放同一块内存,从而引发内存错误或程序崩溃等问题。
深拷贝:不仅复制对象的成员变量的值,还会为新对象分配独立的内存空间并复制原始对象中的实际数据。这样两个对象就拥有各自独立的内存资源,不会相互影响。
*4.赋值运算符重载
默认情况下,C++编译器会为类提供一个默认的赋值运算符(即operator=),但这个默认实现通常只进行浅拷贝。如果类中包含动态分配的内存、指向其他资源的指针或需要特殊处理的成员,那么默认的赋值运算符可能无法满足需求,此时就需要重载赋值运算符。
赋值运算符重载的格式:
参数类型:const T&,传递引用可以提高传参效率;
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值;
在进行复制操作前,我们要检测是否出现“自己给自己赋值”的情况;
返回*this:要符合连续复制的含义;
用户没有显示实现时,编译器会生成一个默认赋值运算符重载,对内置类型浅拷贝,对自定义类型会去调用它的赋值重载!!
注意:
①不能通过连接其它符号来创建新的操作符,比如:operator@(@不是操作符);
②重载操作符必须有一个类 类型的参数;
③用于内置类型的运算符,其含义不能改变;
④作为类成员函数重载时,其形参看起来比实参数目少一个,是因为成员函数的第一个参数是隐藏的this指针;
⑤赋值运算符重载只能定义为成员函数,不能定义成全局函数,除非在类里面实现函数的声明;
注意:.* : :(域作用限定符) sizeof ? :(三目) .(成员访问) 这五个运算符不能重载!!
非成员函数运算符重载时,d1<d2; 会被编译器转化成 operator<(d1,d2);
成员函数运算符重载时,d1<d2; 会被编译器转化成 d1.operator<(d2);
移动拷贝函数和移动赋值函数也是类的默认成员函数,但这两个函数设计C++移动语义方面的知识,不方便在入门时对其做过多讲解,博主会在后续博客中再对其详述~~
四.初始化列表
1.什么是初始化列表?
初始化列表紧跟在构造函数的参数列表之后,使用冒号(:)开始,后面跟着一个以逗号分隔的成员变量列表,每个成员变量后面跟着一个放在括号中的初始值或表达式。
由上图我们知道,所谓初始化列表,其实就是类实例化时,成员变量定义的地方(给成员变量赋予初始值)。
但是,类的成员变量不是可以在构造函数中进行初始化吗?这个初始化列表是不是有点多余?--- 不多余,因为一些特殊类型的成员变量只能在初始化列表中定义!!
2.初始化列表存在的意义?
类中包含以下类型成员时,必须放在初始化列表的位置上才能初始化。
a.引用成员变量;
b.const成员变量;
c.自定义类型成员(该类没有默认构造函数时);
为什么上述类型的成员只有放在初始化列表中才能进行初始化,而不能在构造函数中??
我们知道,const成员变量和引用成员变量都有一个特征:必须在定义的时候初始化,而类的初始化列表,就是类成员变量定义的地方!!
我们在 private: string _name; int _age; 这里并不是对_name和_age进行定义,而只是对其声明,它们真正定义的地方是在初始化列表。
当自定义类型只有需要传参的构造函数时,自定义类型的成员变量就无法自动初始化,所以需要再其定义的地方传参,就必须在初始化列表。
当自定义类型的成员有默认构造函数时,就算没有在初始化列表显示定义该成员变量,系统也会自动调用该变量的默认构造函数!!
注意:成员变量在初始化列表中,是按它们声明的顺序去定义的,与其在初始化列表中的顺序无关!!
3.初始化列表对不同类型数据的初始化规则
a.内置类型的初始化
对于内置类型(如int、float、char等),可以在初始化列表中直接提供初始值,也可以使用花括号进行列表初始化(C++11及以后版本)。但是,不能使用空括号()来初始化内置类型的成员变量,因为内置类型没有默认构造函数。
b.类类型成员的初始化
对于类类型的成员变量,可以使用该类型的构造函数进行初始化。如果类类型成员变量有默认构造函数,也可以在初始化列表中使用空括号()来调用默认构造函数。
c.初始化列表与构造函数体的关系
初始化列表是在构造函数体之前执行的。因此,在构造函数体中,只能对已经初始化的成员变量进行进一步的赋值操作,而不能再次进行初始化。
4.隐式类型转换
A aa1(1); 正常构造。 A aa2=2; 隐式类型转换,整形转——>自定义类型
隐式类型转换的过程:以 “A aa2=2;” 为例,先调用A的构造函数,所传参数是2,生成一个A类型的临时对象,再将这个临时对象拷贝构造给aa2 !!
但是,在同一个表达式内,连续的构造和拷贝构造,编译器又会优化这个过程,将其优化为:用2直接构造aa2。
如何防止在类对象在定义的时候发生隐式类型转换?--- 用 explicit 修饰构造函数即可,如:
五.类的static静态成员
静态成员变量是类级别的变量,而不是实例级别的变量。这意味着静态成员变量属于类本身,而不是类的任何特定对象。无论创建了多少个类的实例,静态成员变量只会有一个副本,并由所有实例共享。
1.静态成员变量
a.声明与定义
声明:在类内部使用 static 关键字声明静态成员变量。这告诉编译器该成员变量是静态的,并且它将在类的所有对象之间共享。
定义:静态成员变量必须在类外部进行定义(除非它是const
并且已经初始化了)。这是因为静态成员变量不属于任何对象,因此不能在类内部(即构造函数或成员函数中)进行初始化。
我们可以通过 cout << Person : : _aa << endl; 直接访问类的静态成员,而不需要通过类对象,前提是_aa是public公有的。
注意:由于静态成员变量不独属于某个类对象,所以静态成员变量不能在初始化列表内定义,只能在类外面定义!!静态成员变量不可以给缺省值(const修饰的整型变量除外)!!
b.使用场景与特性
用途:静态成员变量常用于需要跨多个对象共享数据的场景,如计数器、缓存、配置参数等。
生命周期:静态成员变量的生命周期与程序相同(生命周期是全局的)。静态成员变量存储在静态区上,而不是堆或栈上,它们在程序开始执行时被创建,在程序结束时被销毁。
初始化顺序:静态成员变量的初始化顺序是按照它们在类中声明的顺序进行的,而不是按照它们在定义时的先后顺序。
2.静态成员函数
a.声明与定义
静态成员函数是类的一种特殊成员函数,它与静态成员变量类似,也是属于类本身而不是类的任何特定对象。静态成员函数可以访问类的静态成员变量和其他静态成员函数,但不能直接访问类的非静态成员变量或非静态成员函数(除非通过类的对象或指针)。
声明与定义:在类内部使用 static 关键字声明静态成员函数。这告诉编译器该成员函数是静态的,并且它可以在没有对象的情况下被调用。
静态成员函数没有this指针,指定类域+域作用限定符 就可以直接访问。
b.静态成员函数的特性
访问权限:静态成员函数可以具有public、protected 或 private 访问权限,就像非静态成员函数一样。
没有this指针:静态成员函数没有this
指针,这意味着它们不能访问或修改调用它们的对象的任何非静态成员变量和非静态成员函数。但是,如果提供了对象的指针或引用作为参数,静态成员函数可以间接地访问非静态成员。
用途:静态成员函数通常用于实现与类本身相关但不依赖于特定对象状态的功能,如工厂方法、实用程序函数等。