C++ 面向对象程序设计 14万字总结笔记

在这里插入图片描述
文章的索引都在目录处可以找到
在这里插入图片描述

学好C++可以采取以下几个步骤:

  1. 掌握基本语法:C++的语法对于初学者来说可能是一件比较难的事情,所以需要花时间掌握C++的语言基础和语法规则,例如数据类型、流程控制、函数等。

  2. 学会面向对象编程(OOP):C++是一种面向对象的编程语言,因此理解OOP是很重要的。需要掌握OOP的概念、继承、多态、抽象类等,以便更好地利用C++的优势。

  3. 编写代码并调试:C++是一种强类型语言,它需要程序员深入了解数据类型的底层实现,能够更细致地参阅自己的代码,并排除错误。需要学习调试技巧和如何使用调试工具,以确保代码的正确性。

  4. 使用标准库:C++ Standard Library中包含了许多已经定义好的数据结构和函数,使用这些工具可以极大地提高开发效率。因此,了解C++ STL是非常有意义的。

  5. 成为C++社区的一员:参加一些在线的编程社区,不断地新开发C++项目,与其他代码师沟通交流,就可以更好地发广义娱乐热情和技能。

  6. 学习实战知识:将所学知识运用到实际项目中,可以巩固所学的语法和概念,并提高自己对C++应用的熟练程度。

总之,想要学好C++编程,需要投入一定的时间和精力,从基础获取学习,积累实践经验,多策略学习,以此才能更快进步。

程序设计与算法(一)

C语言程序设计

郭 炜

信息科学技术学院

郭炜(Wei Guo),现为北京大学信息科学技术学院教授,博士生导师。他于1996年在清华大学计算机科学与技术系获得学士学位,2001年在美国弗吉尼亚理工大学计算机科学系获得博士学位。

他的研究方向主要包括自然语言处理、机器学习、社交网络分析等领域。他曾主持和参与了多项国家级科研项目,并发表了大量高水平学术论文,其学术成果受到广泛关注和高度评价。他曾担任ACL/IJCNLP 2015大会联合主席,是ACM SIGIR、AAAI和ACL等国际学术组织的活跃会员。

此外,他也是一位优秀的教师和指导者,在长期从事科研工作的同时,他始终注重对学生的培养和指导,并为许多学生提供了良好的实习和研究机会。他曾多次获得北京大学教学优秀奖和教学贡献奖,并连续3年被评为“全国知名数据科学家”。

总之, 郭炜教授是我国自然语言处理和机器学习领域的杰出代表,他的研究成果和教学贡献都得到了国内外的高度认可。

微博:http://weibo.com/guoweiofpku
在这里插入图片描述

学会程序和算法,走遍天下都不怕!

讲义照片均为郭炜拍摄

微信公众号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iVIT9vco-1688033782511)(2023-04-17-16-51-00.png)]

来自中国大学MOOC

https://www.icourse163.org/learn/PKU-1001553023?tid=1466879449#/learn/announce

郭炜老师还在中国大学MOOC开设另外三门好评如潮的4.9分高分课程,特别适合后续学习,请不要错过:

  1. 程序设计与算法(二)算法基础(国家精品)

http://www.icourse163.org/course/PKU-1001894005

  1. 程序设计与算法(三)C++面向对象程序设计(国家精品)

http://www.icourse163.org/course/PKU-1002029030

  1. 实用Python程序设计 (强烈推荐,Python的百科书式大全课程,入门、提高均非常适合!)

http://www.icourse163.org/course/PKU-1460924165

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tNImXfxQ-1688033782513)(2023-04-17-16-52-12.png)]

面向对象的程序设计概论

学好C++的建议

如果您想学好C++,这里有一些建议:

学好C++需要先掌握C语言的基本知识。因为C++是在C语言的基础上扩展而来的,所以熟悉C语言的基本语法和数据结构等知识是非常重要的。

学习编程最好从入门级别做起。建议您可以看一些新手教程,比如《C++ Primer》、《C++ Primer Plus》等书籍,慢慢熟悉语言的基本语法和核心概念。

多练习编程实践。只有不断尝试,不断打码,才能快速理解语言的用法和特点,提高自己的编程技术。

参与开源社区的项目。加入一些相关的开源社区,阅读、参考一些优秀的代码,借鉴他人的经验和思路,提高自己的编程实力。

反省自己的错误。无论是初学者还是资深程序员,都必须时刻审视自己的编程风格和代码质量,及时发现和纠正问题,不断提高自己的编程水平。

总之,想学好C++,需要坚持不懈地学习和实践,不断锤炼自己的编程技能。

初步理解

相比较于面向过程的程序设计来说有更多的封装的函数可以使用,相比较来说会比较方便。但是如何去设计整个程序的思路也是需要一定的训练的。

C++是C语⾔的继承,它既可以进⾏C语⾔的过程化程序设计,⼜可以进⾏以抽象数据类型为特点的
基于对象的程序设计,还可以进⾏以继承和多态为特点的⾯向对象的程序设计。C++擅⻓⾯向对象程序设计的同时,还可以进⾏基于过程的程序设计,因⽽C++就适应的问题规模⽽论,⼤⼩由之。

C++不仅拥有计算机⾼效运⾏的实⽤性特征,同时还致⼒于提⾼⼤规模程序的编程质量与程序设计
语⾔的问题描述能⼒。

C++刷算法的认识

所以说, C++ 是⼀⻔可以兼容C语⾔语法的⾯向对象语⾔。其实对于刷算法来说,它是否是⾯向对象

语⾔并不重要,它关于⾯向对象的部分(继承封装多态之类)我们也可以完全不学习,⽽且对于已经懂C语⾔的⼈来说,想要⽤ C++ 刷算法,⼏乎没有多少学习成本。也就是说,你完全可以在 C++ 的⽂件⾥⾯使⽤C语⾔的语法,这不会产⽣报错。⽽那些好⽤的 C++ 特性⼜可以随意的使⽤,像是增强版功能的C语⾔。对于刷算法来说, C++ 最⼤的好处是拥有 STL (标准模版库),就像 Java 的集合框架⼀样, C++ ⾥⾯封装了很多常⽤的数据结构,⽽只需掌握STL和C++的⼀些和C语⾔稍有区别的基本语法,就完全可以使⽤ C++ 来刷PAT、LeetCode和蓝桥杯,这对算法之路是⼤有裨益的~下⾯是⼀段关于 STL 的简介:

这些“容器”有list,vector,set,map等,STL也是算法和其他⼀些组件的集合。STL的⽬的是标准化
组件,这样就不⽤重新开发,可以使⽤现成的组件。STL现在是C++的⼀部分,因此不⽤安装额外的
库⽂件。

什么意思呢,⽐如说刷算法过程中需要⽤到集合 set ,我们知道 set 的特点是集合内的元素不重复
(特别地,在 C++ ⾥⾯, set 是会⾃动排序的集合, unordered_set 是不会⾃动排序的集合),⽐如
在set⾥分别放进 4、1、1、2、3 这⼏个元素,set⾥会⾃动变成 1、2、3、4 ,如果我们只会C语⾔,可
能需要⼀个个把数据放到数组⾥⾯,然后⼿动编写⼀些代码,检查进来的每⼀个元素在数组中是否已
经存在,如果存在就不要再放进数组,最后对整个数组进⾏排序,才能达到set的效果。但是如果直接
使⽤ C++ ,我们就可以在头⽂件⾥⾯加个 #include <set> ,然后直接把 set 当作⼀个类似于数组的容
器,把所有元素直接丢到⾃⼰定义的这个set类型的容器⾥,就会⾃动实现去重(去除重复元素)和排
序的步骤~⽐如说题⽬要求将最后所有的答案去重后按从⼩到⼤的顺序输出,就可以直接将所有的答
案元素放到set⾥⾯输出即可~这样我们在刷题过程中就能更好地集中精⼒解决代码思路、算法⽅⾯的
问题,⽽不是⼀个简单的答案输出或语法⽅⾯的问题,在考试过程中也能⼤⼤地节省时间,降低代码
的错误率~

二进制和十六进制

用0和1表示各种信息

计算机的电路由逻辑门电路组成。一个逻辑门电路可以看成一个开关,每个开关的状态是“开"(高电位)或“关”(低电位),即对应于1或0

课程推荐 【【计算机科学速成课】[40集全/精校] - Crash Course Computer Science】 https://www.bilibili.com/video/BV1EW411u7th/?share_source=copy_web&vd_source=3b2cc08efb537592debc1e358b5d787f

计算机的电路由逻辑门电路组成。一个逻辑门电路可以看成一个开关,每个开关的状态是“开"(高电位)或“关”(低电位),即对应于1或0

二进制数的一位,取值只能是0或1,称为一个“比特”(bit),简写:b

计算机的电路由逻辑门电路组成。一个逻辑门电路可以看成一个开关,每个开关的状态是“开"(高电位)或“关”(低电位),即对应于1或0

二进制数的一位,取值只能是0或1,称为一个“比特”(bit),简写:b

八个二进制位称为一个“字节”(byte),简写: B

计算机的电路由逻辑门电路组成。一个逻辑门电路可以看成一个开关,每个开关的状态是“开"(高电位)或“关”(低电位),即对应于1或0

二进制数的一位,取值只能是0或1,称为一个“比特”(bit),简写:b

八个二进制位称为一个“字节”(byte),简写: B

1024(210)字节称为1KB ,1024KB称作1MB(1兆),

1024MB称作1GB,

1024GB称作1TB。

用0和1表示各种信息

0和1足以表示和传播各种信息。

比如, 用8个连续的0或1(即1个字节)来表示一个字母、数字或标点符号

,比如用“00100000”表示空格,用“01100001”表示字母“a”,用

“01100010”表示字母“b”,用“01100011”表示字母“c”……。由8个

0或者1的组成的串,一共有28即256种不同的组合,这就足以表示10个阿拉伯数字以及英语中用到的所有字母和标点符号了。此即为ASCII编码方案。

图片、视频和可执行程序,也可以用0和1表示

给定一个K进制数

给定一个K进制数,求它是多大

假设有一个n+1位的K进制数,它的形式如下:

AnAn-1An-2。。。。。。A2A1A0 (比如 八进制数 1723)
则其大小为:

A0×K0 + A1×K1 + ……+ An-1×Kn-1+ An×Kn

数就是数,没有进制之分,只有数的表示形式,才有进制之分。
所谓“十进制数”,是“数的十进制表示形式" 的简称。 


给定一个数,求其K进制表示形式
10
 求数的K进制表示形式 -- 短除法
给定一个整数N和进制K,那么N可表示成以下形式:
N = A0×K0+A1×K1+A2×K2+……+An-1×Kn-1+An×Kn
= A0 +K (A1 +A2×K1+……+An-1×Kn-2+An×Kn-1
)
N除以K所得到的余数是A0,商是A1+A2×K1+……+An-1×Kn-2+An×Kn-1。将这个商再除
以K,就得到余数A1,新的商是
A2 + A3×K1+……+An-1×Kn-3+An×Kn-2
不停地将新得到的商除以K,直到商变成0,就能依次求得A0 、A1、 A2 …… An-1 、An
。显然,Ai <K ( i = 0…n),且最终得到的K进制数就是:
AnAn-1An-2。。。。。。A2A1A0


K进制小数
11
K进制小数 0.A0A1……An的值是:
A0×K-1+A1×K-2+……+An×K-(n+1) 
(0.12)10 = 1 ×10-1 + 2 ×10-2
(0.1)3 = 1 ×3
-1 即1/3, 表示成10进制就是无限循环小数
可见,n进制下的有限位小数,在m进制下可能就无法精确表示,因为会无限
循环
十进制有限位小数,在二进制的情况未必能用有限位数表示出来。计算机内
存有限,不可能存放无限位,因此计算机的小数运算会有误差。比如,计算
机其实无法精确表示 4.9,只能精确表示4.899999999...之类一个很接近的数 

十六进制数

十六进制数应该有16个数字,除0到9外:

A 10

B 11

C 12

D 13

E 14

F 15

小写也可以

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jGH2Rnxh-1688033782514)(2023-04-17-17-02-49.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4n6Knc53-1688033782514)(2023-04-17-17-03-18.png)]

为什么是C++而不是C语言

为什么是C++而不是C语言

C语言是好东西,但是有点弱

C++ 更是好东西,但是有点烦

我们要学的,是C++的一部分,基本上就是:

C语言+ STL (STL是C++中能让你节省大量编程时间的神兵!)

因为暂时不写大程序,因此不用关心“面向对象”的事情!

C++的基本数据类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UrT4bE1l-1688033782514)(2023-04-17-17-08-03.png)]

用sizeof运算符求变量占用字节数

sizeof(变量名)
sizeof(类型名)
能够得到某个变量或某一类型变量占用的字节数
int n1 = 10;
double f;
char c;
printf("%d,%d,%d,%d",sizeof(n1),sizeof(short),
sizeof(double),sizeof(c));
输出: 4,2,8,1

在这里插入图片描述
在这里插入图片描述

变量和数据类型进阶

有符号整数和无符号整数

short、int、long、long long 类型的变量,可以表示正数,也可以表示负数,称为有符号的整数类型。

unsigned short, unsigned int, unsigned long,unsigned long long类型的变量,只会被看作非负数,称为无符号的整数类型

有符号整数的表示方式

将最左边的位(最高位)看作“符号位”。符号位为0,则表示是非负数,其绝对值就是除符号位以外的部分;符号位为1,则表示是负数,其绝对值是所有位取反(0变1,1变0)后再加1。

将一个负整数表示为二进制的方法:

(1) 设置符号位为1

(2) 其余位等于绝对值取反再加1

有符号整数的表示方式

给定一个负整数的二进制表示形式,求该负整数:
该负整数的绝对值是其二进制表示形式取反再加1(取反加1后的结果要看作是正数)。

将一个负整数表示为二进制的方法:

1) 设置符号位为1

2) 其余位等于该负数的绝对值的二进制表示形式取反再加1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QLt3VYyq-1688033782515)(2023-04-17-17-14-27.png)]

字符类型到整型的互相转换

字符型数据可以转换成整型数据
int k = 'a' ; //k内容变为'a'的ASCII码,即97
printf("%d",k) ; //输出:97
 整型数据也可以转换为字符型数据,但只会留下最右边的一个字节(第0位
到第7位),其他字节丢弃
int n = 98;
char k = n ; //k内容变98,98是字符'b'的ASCII码
printf("%c",k) ; //输出:b

类型自动转换示例

#include <cstdio>
#include <iostream>
using namespace std;
int main() {
    
    
int n1 = 1378; //1378的十六进制形式是 0x562
short n2;
char c = 'a';
double d1 = 7.809;
double d2;
n2 = c+1; //n2变为98 , 97是'a'的ASCII码
printf("c=%c,n2=%d\n",c,n2); //输出 c=a,n2=98
c = n1; // n1是0x562, 0x62被当做ASCII码赋值给c,c变为 'b'
printf("c=%c,n1=%d\n",c,n1); //输出 c=b,n1=1378
n1 = d1; // d1=7.809, 去掉小数部分后赋值给n1,n1变为7
printf("n1=%d\n", n1); //输出 n1=7
d2 = n1; //d2变为7
printf("d2=%f\n",d2); //输出 d2=7.000000
return 0;

cin,cout和scanf,printf比较

cin,cout 速度比scanf,printf慢,输入输出数据量大时用后者

一个程序里面不要同时用cin和scanf,不要同时用cout和printf

部分运算符的优先级

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Yebu3Gg-1688033782515)(2023-04-17-17-20-40.png)]

结构的概念

现实需求

在现实问题中,常常需要用一组不同类型的数据来描述一个事物。比如一个学生的学号、姓名和绩点。一个工人的姓名、性别、年龄、工资、电话…

如果编程时要用多个不同类型的变量来描述一个事物,就很麻烦。当然希望只用一个变量就能代表一个“学生”这样的事物。

C++允许程序员自己定义新的数据类型。因此针对“学生”这种事物,可以定义一种新名为Student的数据类型,一个Student类型的变量就能描述一个学生的全部信息。同理,还可以定义数据类型 Worker以表示工人。

结构(struct)

用“struct”关键字来定义一个“结构”,也就定义了一个新的数据类型:
struct 结构名
{
    
    
类型名 成员变量名;
类型名 成员变量名;
类型名 成员变量名;
……
}

结构(struct)

例:

struct Student {
    
    
unsigned ID;
char szName[20];
float fGPA;
};
Student 即成为自定义类型的名字,可以用来定义变量
Stuent s1,s2;

访问结构变量的成员变量

一个结构变量的成员变量,可以完全和一个普通变量
一样来使用,也可以取得其地址。使用形式:

结构变量名.成员变量名

StudentEx stu;
cin >> stu.fGPA;
stu.ID = 12345;
strcpy(stu.szName, "Tom");
cout << stu.fGPA;
stu.birthday.year = 1984;
unsigned int * p = & stu.ID; //p指向stu中的ID成员变量
11
struct Date {
    
    
int year;
int month;
int day;
};
struct StudentEx {
    
    
unsigned ID;
char szName[20];
float fGPA;
Date birthday;
};

变量的生存期

所谓变量的“生存期”,指的是在此期间,变量占有内存空间,其占有的内存空间只能归它使用,不会被用来存放别的东西。

而变量的生存期终止,就意味着该变量不再占有内存空间,它原来占有的内存空间,随时可能被派做他用。

全局变量的生存期,从程序被装入内存开始,到整个程序结束。

静态局部变量的生存期,从定义它语句第一次被执行开始,到整个程序结束为止。

函数形参的生存期从函数执行开始,到函数返回时结束。非静态局部变量的生存期,从执行到定义它的语句开始,一旦程序执行到了它的作用域之外,其生存期即告终止。

标识符的作用域
void Func(int m)
{
    
    
for( int i = 0; i < 4;++i ) {
    
    
if( m <= 0 ) {
    
    
int k = 3;
m = m *( k ++ );
}
else {
    
    
k = 0; //编译出错,k无定义
int m = 4;
cout << m;
}
}
i= 2; //编译出错,i无定义
}
void Func(int m)
{
    
    
for( int i = 0; i < 4;++i ) {
    
    
if( m <= 0 ) {
    
    
int k = 3;
m = m *( k ++ );
}
else {
    
    
k = 0; //编译出错,k无定义
int m = 4;
cout << m;
}
}
i= 2; //编译出错,i无定义
}
32

C++中的STL(标准模板库)

STL概述

STL: (Standard Template Library) 标准模板库

包含一些常用的算法如排序查找,还有常用的数据结构如可变长数组、链表、字典等。

使用方便,效率较高

要使用其中的算法,需要#include

C++中的STL(标准模板库)是一个非常强大的工具,为程序员提供了许多高效的数据结构和算法。在本文中,我们将探讨STL的基本概念、使用方法和一些常用的数据结构和算法。

STL的基本概念

STL是由一组C++模板类和函数组成的库,包含了许多不同的容器、算法和迭代器。容器是一种存储和管理数据的对象,算法是对数据进行操作的函数,迭代器则是容器和算法之间的桥梁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QmryqkEg-1688033782515)(2023-04-18-20-30-09.png)]

用sort进行排序

对基本类型的数组从小到大排序: 
sort(数组名+n1,数组名+n2);
n1和n2都是int类型的表达式,可以包含变量
如果n1=0,+ n1可以不写
将数组中下标范围为[n1,n2)的元素从小到大排序。下标为n2的元素不在排序
区间内
用sort进行排序(用法一)
int a[] = {
    
    15,4,3,9,7,2,6};
sort(a,a+7); //对整个数组从小到大排序
int a[] = {
    
    15,4,3,9,7,2,6};
sort(a,a+3); // 结果:{3,4,15,9,7,2,6}
int a[] = {
    
    15,4,3,9,7,2,6};
sort(a+2,a+5); //结果:{15,4,3,7,9,2,6}
7
用sort进行排序(用法二)
对元素类型为T的基本类型数组从大到小排序:
sort(数组名+n1,数组名+n2,greater<T>();
int a[] = {
    
    15,4,3,9,7,2,6};
sort(a+1,a+4,greater<int>()); // 结果:{15,9,4,3,7,2,6}
8
用sort进行排序(用法三)
用自定义的排序规则,对任何类型T的数组排序
sort(数组名+n1,数组名+n2,排序规则结构名());
排序规则结构的定义方式:
struct 结构名
{
    
    
bool operator()( const T & a1,const T & a2) const {
    
    
//若a1应该在a2前面,则返回true。
//否则返回false。
}
}; 9
sort排序规则注意事项
struct 结构名
{
    
    
bool operator()( const T & a1,const T & a2) const {
    
    
//若a1应该在a2前面,则返回true。
//否则返回false。
}
};
排序规则返回 true,意味着 a1 必须在 a2 前面
返回 false,意味着 a1 并非必须在 a2 前面
排序规则的写法,不能造成比较 a1,a2 返回 true 比较 a2,a1 也返回 true 
否则sort会 runtime error
比较 a1,a2 返回 false 比较 a2,a1 也返回 false,则没有问题
10
用sort进行排序(用法三)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
struct Rule1 //按从大到小排序
{
    
    
bool operator()( const int & a1,const int & a2) const {
    
    
return a1 > a2;
}
};
struct Rule2 //按个位数从小到大排序
{
    
    
bool operator()( const int & a1,const int & a2) const {
    
    
return a1%10 < a2%10;
}
};
11
用sort进行排序(用法三)
void Print(int a[],int size) {
    
    
for(int i = 0;i < size;++i) 
cout << a[i] << "," ;
cout << endl;
}
int main()
{
    
    
int a[] = {
    
     12,45,3,98,21,7};
sort(a,a+sizeof(a)/sizeof(int)); //从小到大
cout << "1) "; Print(a,sizeof(a)/sizeof(int));
sort(a,a+sizeof(a)/sizeof(int),Rule1()); //从大到小
cout << "2) "; Print(a,sizeof(a)/sizeof(int));
sort(a,a+sizeof(a)/sizeof(int),Rule2()); //按个位数从小到大
cout << "3) "; Print(a,sizeof(a)/sizeof(int));
return 0;
}
1) 3,7,12,21,45,98,
2) 98,45,21,12,7,3,
3) 21,12,3,45,7,98,
用sort对结构数组进行排序(用法三)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
struct Student {
    
    
char name[20];
int id;
double gpa;
};
Student students [] = {
    
     
{
    
    "Jack",112,3.4},{
    
    "Mary",102,3.8},{
    
    "Mary",117,3.9},
{
    
    "Ala",333,3.5},{
    
    "Zero",101,4.0}};
13
用sort对结构数组进行排序(用法三)
struct StudentRule1 {
    
     //按姓名从小到大排
bool operator() (const Student & s1,const Student & s2) const {
    
    
if( stricmp(s1.name,s2.name) < 0)
return true;
return false;
}
};
struct StudentRule2 {
    
     //按id从小到大排
bool operator() (const Student & s1,const Student & s2) const {
    
    
return s1.id < s2.id;
}
};
struct StudentRule3 {
    
    //按gpa从高到低排
bool operator() (const Student & s1,const Student & s2) const {
    
    
return s1.gpa > s2.gpa;
}
};
14
用sort对结构数组进行排序(用法三)
void PrintStudents(Student s[],int size){
    
    
for(int i = 0;i < size;++i)
cout << "(" << s[i].name << "," 
<< s[i].id <<"," << s[i].gpa << ") " ;
cout << endl;
}
15
用sort对结构数组进行排序(用法三)
int main()
{
    
    
int n = sizeof(students) / sizeof(Student);
sort(students,students+n,StudentRule1()); //按姓名从小到大排
PrintStudents(students,n);
sort(students,students+n,StudentRule2()); //按id从小到大排
PrintStudents(students,n);
sort(students,students+n,StudentRule3()); //按gpa从高到低排
PrintStudents(students,n);
return 0;
}
(Ala,333,3.5) (Jack,112,3.4) (Mary,102,3.8) (Mary,117,3.9) (Zero,101,4)
(Zero,101,4) (Mary,102,3.8) (Jack,112,3.4) (Mary,117,3.9) (Ala,333,3.5)
(Zero,101,4) (Mary,117,3.9) (Mary,102,3.8) (Ala,333,3.5) (Jack,112,3.4)

二分查找算法

STL中的二分查找算法

 STL提供在排好序的数组上进行二分查找的算法
binary_search
lower_bound
upper_bound
18
用binary_search进行二分查找(用法一)
在从小到大排好序的基本类型数组上进行二分查找
binary_search(数组名+n1,数组名+n2,); 
n1和n2都是int类型的表达式,可以包含变量
如果n1=0,+ n1可以不写
查找区间为下标范围为[n1,n2)的元素,下标为n2的元素不在查找区间内
在该区间内查找"等于"值”的元素,返回值为true(找到)或false(没找到)
"等于"的含义: a 等于 B <=> a < b和b < a都不成立
19
用binary_search进行二分查找(用法二)
在用自定义排序规则排好序的、元素为任意的T类型的数组中进行二分查找
binary_search(数组名+n1,数组名+n2,值,排序规则结构名()); 
n1和n2都是int类型的表达式,可以包含变量
如果n1=0,+ n1可以不写
查找区间为下标范围为[n1,n2)的元素,下标为n2的元素不在查找区间内
在该区间内查找"等于"值的元素,返回值为true(找到)或false(没找到)
查找时的排序规则,必须和排序时的规则一致!
"等于"的含义: a 等于 b <=> "a必须在b前面""b必须在a前面"都不成立20
用binary_search进行二分查找(用法二)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
struct Rule //按个位数从小到大排
{
    
    
bool operator()( const int & a1,const int & a2) const {
    
    
return a1%10 < a2%10;
}
};
void Print(int a[],int size) {
    
    
for(int i = 0;i < size;++i) {
    
    
cout << a[i] << "," ;
}
cout << endl;
}
用binary_search进行二分查找(用法二)
int main() {
    
    
int a[] = {
    
     12,45,3,98,21,7};
sort(a,a+6);
Print(a,6);
cout <<"result:"<< binary_search(a,a+6,12) << endl;
cout <<"result:"<< binary_search(a,a+6,77) << endl;
sort(a,a+6,Rule()); //按个位数从小到大排
Print(a,6);
cout <<"result:"<< binary_search(a,a+6,7) << endl;
cout <<"result:"<< binary_search(a,a+6,8,Rule()) << endl;
return 0;
}
22
3,7,12,21,45,98,
result:1
result:0
21,12,3,45,7,98,
result:0
result:1
"等于"的含义: a 等于 b <=> "a必须在b前面"和"b必须在
a前面"都不成立
用binary_search进行二分查找(用法二)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
struct Student {
    
    
char name[20];
int id;
double gpa;
};
Student students [] = {
    
     
{
    
    "Jack",112,3.4},{
    
    "Mary",102,3.8},{
    
    "Mary",117,3.9},
{
    
    "Ala",333,3.5},{
    
    "Zero",101,4.0}};
23
用binary_search进行二分查找(用法二)
struct StudentRule1 {
    
     //按姓名从小到大排
bool operator() (const Student & s1,const Student & s2) const {
    
    
if( stricmp(s1.name,s2.name) < 0)
return true;
return false;
}
};
struct StudentRule2 {
    
     //按id从小到大排
bool operator() (const Student & s1,const Student & s2) const {
    
    
return s1.id < s2.id;
}
};
struct StudentRule3 {
    
    //按gpa从高到低排
bool operator() (const Student & s1,const Student & s2) const {
    
    
return s1.gpa > s2.gpa;
}
};
24
用binary_search进行二分查找(用法二)
int main(){
    
    
Student s;
strcpy(s.name,"Mary");
s.id= 117;
s.gpa = 0;
int n = sizeof(students) / sizeof(Student);
sort(students,students+n,StudentRule1()); //按姓名从小到大排
cout << binary_search( students , students+n,s,
StudentRule1()) << endl;
strcpy(s.name,"Bob");
cout << binary_search( students , students+n,s,
StudentRule1()) << endl;
sort(students,students+n,StudentRule2()); //按id从小到大排
cout << binary_search( students , students+n,s,
StudentRule2()) << endl;
return 0;
}
用lower_bound二分查找下界(用法一)
在对元素类型为T的从小到大排好序的基本类型的数组中进行查找
T * lower_bound(数组名+n1,数组名+n2,);
返回一个指针 T * p;
*p 是查找区间里下标最小的,大于等于"值" 的元素。如果找不到,p指向下标为n2的
元素
26
用lower_bound二分查找下界(用法二)
在元素为任意的T类型、按照自定义排序规则排好序的数组中进行查找
T * lower_bound(数组名+n1,数组名+n2,,排序规则结构名());
返回一个指针 T * p;
*p 是查找区间里下标最小的,按自定义排序规则,可以排在"值"后面的元素。如果找
不到,p指向下标为n2的元素
27
用upper_bound二分查找上界(用法一)
在元素类型为T的从小到大排好序的基本类型的数组中进行查找
T * upper_bound(数组名+n1,数组名+n2,);
返回一个指针 T * p;
*p 是查找区间里下标最小的,大于"值"的元素。如果找不到,p指向下标为n2的元素
28
用upper_bound二分查找上界(用法二)
在元素为任意的T类型、按照自定义排序规则排好序的数组中进行查找
T * upper_bound(数组名+n1,数组名+n2,,排序规则结构名());
返回一个指针 T * p;
*p 是查找区间里下标最小的,按自定义排序规则,必须排在"值"后面的元素。如果找
不到,p指向下标为n2的元素
29
lower_bound,upper_bound用法示例
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
struct Rule 
{
    
    
bool operator()( const int & a1,const int & a2) const {
    
    
return a1%10 < a2%10;
}
};
void Print(int a[],int size) {
    
    
for(int i = 0;i < size;++i) {
    
    
cout << a[i] << "," ;
}
cout << endl;
} 30
lower_bound,upper_bound用法示例
#define NUM 7 
int main()
{
    
    
int a[NUM] = {
    
     12,5,3,5,98,21,7};
sort(a,a+NUM);
Print(a,NUM); // => 3,5,5,7,12,21,98,
int * p = lower_bound(a,a+NUM,5);
cout << *p << "," << p-a << endl; //=> 5,1
p = upper_bound(a,a+NUM,5);
cout << *p << endl; //=>7
cout << * upper_bound(a,a+NUM,13) << endl; //=>21
31
lower_bound,upper_bound用法示例
sort(a,a+NUM,Rule());
Print(a,NUM); //=>21,12,3,5,5,7,98,
cout << * lower_bound(a,a+NUM,16,Rule()) << endl; // => 7
cout << lower_bound(a,a+NUM,25,Rule()) - a<< endl; // => 3
cout << upper_bound(a,a+NUM,18,Rule()) - a << endl; // => 7
if( upper_bound(a,a+NUM,18,Rule()) == a+NUM)
cout << "not found" << endl; //=> not found
cout << * upper_bound(a,a+NUM,5,Rule()) << endl; // =>7
cout << * upper_bound(a,a+NUM,4,Rule()) << endl; // =>5
return 0;
}

multiset

multiset用法

multiset<T> st; 
定义了一个multiset变量st,st里面可以存放T类型的数据,并且能自
动排序。开始st为空
排序规则:表达式 “a < b” 为true,则 a 排在 b 前面
可用 st.insert添加元素,st.find查找元素,st.erase删除元素,复杂度
都是 log(n)
multiset 用法
#include <iostream>
#include <cstring>
#include <set> //使用multiset和set需要此头文件
using namespace std;
int main()
{
    
    
multiset<int> st; 
int a[10]={
    
    1,14,12,13,7,13,21,19,8,8 };
for(int i = 0;i < 10; ++i)
st.insert(a[i]); //插入的是a [i]的复制品
multiset<int>::iterator i; //迭代器,近似于指针
for(i = st.begin(); i != st.end(); ++i) 
cout << * i << ","; 
cout << endl;
输出:1,7,8,8,12,13,13,14,19,21,
40
multiset 用法
i = st.find(22); //查找22,返回值是迭代器
if( i == st.end()) //找不到则返回值为 end()
cout << "not found" << endl;
st.insert(22); //插入 22
i = st.find(22);
if( i == st.end())
cout << "not found" << endl;
else
cout << "found:" << *i <<endl; 
//找到则返回指向找到的元素的迭代器
输出:
not found
found:22
41
i = st.lower_bound(13);
//返回最靠后的迭代器 it,使得[begin(),it)中的元素
//都在 13 前面 ,复杂度 log(n)
cout << * i << endl;
i = st.upper_bound(8);
//返回最靠前的迭代器 it,使得[it,end())中的元素
//都在 8 后面,复杂度 log(n)
cout << * i << endl;
st.erase(i); //删除迭代器 i 指向的元素,即12
for(i = st.begin(); i != st.end(); ++i) 
cout << * i << ",";
return 0;
}
输出:
13
12
1,7,8,8,13,13,14,19,21,22,
42
1,7,8,8,12,13,13,14,19,21,
multiset 上的迭代器
multiset<T>::iterator p; 
p是迭代器,相当于指针,可用于指向multiset中的元素。访问multiset中的元素要通
过迭代器。
与指针的不同:
multiset上的迭代器可 ++--, 用 !=== 比较,不可比大小,不可加减整数,不
可相减
multiset 上的迭代器
multiset<T> st;
st.begin() 返回值类型为 multiset<T>::iterator, 
是指向st中的头一个元素的迭代器
st.end() 返回值类型为 multiset<T>::iterator, 
是指向st中的最后一个元素后面的迭代器
对迭代器 ++ ,其就指向容器中下一个元素,-- 则令其指向上一个元素
44
自定义排序规则的multiset 用法
#include <iostream>
#include <cstring>
#include <set>
using namespace std;
struct Rule1 {
    
    
bool operator()( const int & a,const int & b) const {
    
     
return (a%10) < (b%10); 
}//返回值为true则说明a必须排在b前面
};
int main() {
    
    
multiset<int,greater<int> > st; //排序规则为从大到小
int a[10]={
    
    1,14,12,13,7,13,21,19,8,8 };
for(int i = 0;i < 10; ++i)
st.insert(a[i]);
multiset<int,greater<int> >::iterator i; 
for(i = st.begin(); i != st.end(); ++i) 
cout << * i << ",";
cout << endl;
45 输出:21,19,14,13,13,12,8,8,7,1,
自定义排序规则的multiset 用法
multiset<int,Rule1 > st2;
//st2的元素排序规则为:个位数小的排前面
for(int i = 0;i < 10; ++i)
st2.insert(a[i]);
multiset<int,Rule1>::iterator p; 
for(p = st2.begin(); p != st2.end(); ++p) 
cout << * p << ",";
cout << endl;
p = st2.find(133);
cout << * p << endl;
return 0;
}
输出:
1,21,12,13,13,14,7,8,8,19,
13 46
自定义排序规则的multiset 用法
multiset<int,Rule1 > st2;
//st2的元素排序规则为:个位数小的排前面
for(int i = 0;i < 10; ++i)
st2.insert(a[i]);
multiset<int,Rule1>::iterator p; 
for(p = st2.begin(); p != st2.end(); ++p) 
cout << * p << ",";
cout << endl;
p = st2.find(133);
cout << * p << endl;
return 0;
}
输出:
1,21,12,13,13,14,7,8,8,19,
13 47
find(x): 在排序容器中找一个元素y,使得
“x必须排在y前面”和 “y必须排在x前面”
都不成立
自定义排序规则的multiset 用法
#include <iostream>
#include <cstring>
#include <algorithm>
#include <set>
using namespace std;
struct Student {
    
    
char name[20];
int id;
int score;
};
Student students [] = {
    
     {
    
    "Jack",112,78},{
    
    "Mary",102,85},
{
    
    "Ala",333,92},{
    
    "Zero",101,70},{
    
    "Cindy",102,78}};
struct Rule {
    
    
bool operator() (const Student & s1,const Student & s2) const {
    
    
if( s1.score != s2.score) return s1.score > s2.score;
else return (strcmp(s1.name,s2.name) < 0); 
}
}; 48
自定义排序规则的multiset 用法
int main()
{
    
    
multiset<Student,Rule> st;
for(int i = 0;i < 5;++i)
st.insert(students[i]); //插入的是students[i]的复制品
multiset<Student,Rule>::iterator p;
for(p = st.begin(); p != st.end(); ++p)
cout << p->score <<" "<<p->name<<" "
<< p->id <<endl;
Student s = {
    
     "Mary",1000,85};
p = st.find(s);
if( p!= st.end())
cout << p->score <<" "<< p->name<<" "
<< p->id <<endl;
return 0;
} 49
92 Ala 333
85 Mary 102
78 Cindy 102
78 Jack 112
70 Zero 101
85 Mary 102

set

set的用法

set和multiset的区别在于容器里不能有重复元素
a和b重复  “a必须排在b前面” 和“b必须排在a前面”都不成立
set插入元素可能不成功
51
set的用法
#include <iostream>
#include <cstring>
#include <set>
using namespace std;
int main()
{
    
    
set<int> st;
int a[10] ={
    
     1,2,3,8,7,7,5,6,8,12 };
for(int i = 0;i < 10; ++i)
st.insert(a[i]);
cout << st.size() << endl; //输出:8
set<int>::iterator i;
for(i = st.begin(); i != st.end(); ++i)
cout << * i << ","; //输出:1,2,3,5,6,7,8,12,
cout << endl; 
52
set的用法
pair<set<int>::iterator, bool> result = st.insert(2);
if( ! result.second ) //条件成立说明插入不成功
cout << * result.first <<" already exists." 
<< endl;
else
cout << * result.first << " inserted." << endl;
return 0;
} 
输出:
2 already exists.
53
pair<set<int>::iterator, bool>struct {
    
    
set<int>::iterator first;
bool second;
};
pair模板的用法
pair<T1,T2>类型等价于:
struct {
    
    
T1 first;
T2 second;
};
例如:pair<int, double> a; 
等价于:
struct {
    
    
int first;
double second;
} a;
a.first = 1;
a.second = 93.93;

STL容器

STL中的容器分为序列容器和关联容器两种类型。序列容器包括vector、deque和list,它们的主要区别在于它们的存储方式和访问元素的效率。关联容器包括set、map和multiset/multimap,它们使用的是二叉树结构来存储元素,因此能够快速地查找和插入元素。

STL算法

STL中的算法包括排序、查找、替换、合并、拷贝等,这些算法都是以泛型的方式实现的,即它们可以用于任何类型的数据,而不需要重复地编写代码。使用STL算法可以大大提高代码的可读性和可维护性。

STL迭代器

迭代器是STL容器和算法之间的桥梁,它们提供了一种统一的方式来访问容器中的元素。STL迭代器分为输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器五种类型,每种类型的迭代器都有其特定的功能和限制。

STL的使用方法

使用STL非常简单,只需要包含相应的头文件即可。例如,要使用vector容器,只需要包含头文件,就可以创建一个vector对象并进行各种操作。使用STL算法也非常简单,只需要调用相应的算法函数,并传递容器或迭代器作为参数即可。

常用的STL操作

下面列出了一些常用的STL操作:

1.创建容器对象:
vector<int> v;
set<string> s;
map<int, string> m;

2.往容器中添加元素:
v.push_back(10);
s.insert("hello");
m[1] = "world";

3.遍历容器中的元素:
for(auto it = v.begin(); it != v.end(); it++) {
    
    
    cout << *it << endl;
}

4.使用STL算法:
sort(v.begin(), v.end());
auto it = find(s.begin(), s.end(), "hello");
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));

multimap

multimap的用法

multimap容器里的元素,都是pair形式的
multimap<T1,T2> mp;
则mp里的元素都是如下类型:
struct {
    
    
T1 first; //关键字
T2 second; //值
};
multimap中的元素按照first排序,并可以按first进行查找
缺省的排序规则是 "a.first < b.first" 为true,则a排在b前面
5
multimap的应用
一个学生成绩录入和查询系统,接受以下两种输入:
Add name id score
Query score
name是个不超过16字符的字符串,中间没有空格,代表学生姓名。id
是个整数,代表学号。score是个整数,表示分数。学号不会重复,分数
和姓名都可能重复。
两种输入交替出现。第一种输入表示要添加一个学生的信息,碰到这
种输入,就记下学生的姓名、id和分数。第二种输入表示要查询,碰到这
种输入,就输出已有记录中分数比score低的最高分获得者的姓名、学号
和分数。如果有多个学生都满足条件,就输出学号最大的那个学生的信
息。如果找不到满足条件的学生,则输出“Nobody”
输入样例:
Add Jack 12 78
Query 78
Query 81
Add Percy 9 81
Add Marry 8 81
Query 82
Add Tom 11 79
Query 80
Query 81
输出样例:
Nobody
Jack 12 78
Percy 9 81
Tom 11 79
Tom 11 79
#include <iostream>
#include <map> //使用multimap和map需要包含此头文件
#include <cstring>
using namespace std;
struct StudentInfo {
    
    
int id;
char name[20];
};
struct Student {
    
    
int score;
StudentInfo info; 
};
typedef multimap<int,StudentInfo> MAP_STD; 
// 此后 MAP_STD 等价于 multimap<int,StudentInfo>
// typedef int * PINT;
// 则此后 PINT 等价于 int *。 即 PINT p; 等价于 int * p;
int main() {
    
    
MAP_STD mp;
Student st;
char cmd[20];
while( cin >> cmd ) {
    
    
if( cmd[0] == 'A') {
    
    
cin >> st.info.name >> st.info.id >> st.score ;
mp.insert(make_pair(st.score,st.info ));
} //make_pair生成一个 pair<int,StudentInfo>变量
//其first 等于 st.score, second 等于 st.info
else if( cmd[0] == 'Q' ){
    
    
int score;
cin >> score;
MAP_STD::iterator p = mp.lower_bound (score);
if( p!= mp.begin()) {
    
     
--p;
score = p->first; //比要查询分数低的最高分
MAP_STD::iterator maxp = p; 
int maxId = p->second.id; 
for(; p != mp.begin() && 
p->first == score; --p) {
    
    
//遍历所有成绩和score相等的学生
if( p->second.id > maxId ) {
    
    
maxp = p;
maxId = p->second.id ;
}
}
if( p->first == score) {
    
     
//如果上面循环是因为 p == mp.begin() 而终止,则p指向的元素还要处理
if( p->second.id > maxId ) {
    
    
maxp = p;
maxId = p->second.id ;
}
}
cout << maxp->second.name << " " 
<< maxp->second.id << " " 
<< maxp->first << endl;
}
//lower_bound的结果就是 begin,说明没人分数比查询分数低
else cout << "Nobody" << endl;
}
}
return 0;
}

map

map的用法

和multimap区别在于:
不能有关键字重复的元素
可以使用 [] ,下标为关键字,返回值为first和关键字相同的元
素的second
插入元素可能失败

#include <iostream>
#include <map> 
#include <string>
using namespace std;
struct Student {
    
    
string name;
int score;
};
Student students[5] = {
    
     
{
    
    "Jack",89},{
    
    "Tom",74},{
    
    "Cindy",87},{
    
    "Alysa",87},{
    
    "Micheal",98}}; 
typedef map<string,int> MP;
int main()
{
    
    
MP mp; 
for(int i = 0;i < 5; ++i)
mp.insert(make_pair(students[i].name,students[i].score));
cout << mp["Jack"] << endl; // 输出 89
mp["Jack"] = 60; //修改名为"Jack"的元素的second 
for(MP::iterator i = mp.begin(); i != mp.end(); ++i)
cout << "(" << i->first << "," << i->second << ") ";
//输出:(Alysa,87) (Cindy,87) (Jack,60) (Micheal,98) (Tom,74)
cout << endl;
Student st;
st.name = "Jack";
st.score = 99;
pair<MP::iterator, bool> p =
mp.insert(make_pair(st.name,st.score));
if( p.second ) 
cout << "(" << p.first->first << "," 
<< p.first->second << ") inserted" <<endl;
else
cout << "insertion failed" << endl; //输出此信息
mp["Harry"] = 78; //插入一元素,其first为"Harry",然后将其second改为78 
MP::iterator q = mp.find("Harry");
cout << "(" << q->first << "," << q->second <<")" <<endl;
//输出 (Harry,78)
return 0;
}
map例题:单词词频统计程序
输入大量单词,每个单词,一行,不超过20字符,没有
空格。 按出现次数从多到少输出这些单词及其出现次数
。出现次数相同的,字典序靠前的在前面
输入样例:
this
is
ok
this
plus
that
is
plus
plus
输出样例:
plus 3
is 2
this 2
ok 1
that 1
#include <iostream>
#include <set>
#include <map>
#include <string>
using namespace std;
struct Word {
    
    
int times;
string wd;
};
struct Rule {
    
    
bool operator () ( const Word & w1,const Word & w2) const {
    
    
if( w1.times != w2.times)
return w1.times > w2.times;
else
return w1.wd < w2.wd;
}
};
int main()
{
    
    
string s;
set<Word,Rule> st;
map<string,int> mp;
while( cin >> s ) 
++ mp[s] ;
for( map<string,int>::iterator i = mp.begin(); 
i != mp.end(); ++i) {
    
    
Word tmp;
tmp.wd = i->first;
tmp.times = i->second;
st.insert(tmp);
}
for(set<Word,Rule>::iterator i = st.begin(); 
i != st.end(); ++i) 
cout << i->wd << " " << i->times << endl;
}

结论
STL是C++中非常强大的一个工具,它提供了一种统一的方式来处理数据结构和算法的实现。使用STL可以大大提高代码的效率和可读性,同时也能够减少错误和bug的出现。如果你还没有使用STL,那么现在是时候学习一下了!

(三)面向对象程序设计

引用的概念

 下面的写法定义了一个引用,并将其初始化为引用某个变量。

类型名 & 引用名 = 某变量名; 

int n = 4; 
int & r = n; // r引用了 n, r的类型是(教材第62页)

 下面的写法定义了一个引用,并将其初始化为引用某个变量。

类型名 & 引用名 = 某变量名; 

int n = 4; 
int & r = n; // r引用了 n, r的类型是 int &(教材第62页)

 下面的写法定义了一个引用,并将其初始化为引用某个变量。

类型名 & 引用名 = 某变量名; 

int n = 4; 
int & r = n; // r引用了 n, r的类型是 int &

 某个变量的引用,等价于这个变量,相当于该变量的一个别名。

int n = 4;
int & r = n;
r = 4; 
cout << r; //输出 4
cout << n; 
n = 5;
cout << r; 



int n = 4;
int & r = n;
r = 4; 
cout << r; //输出 4
cout << n; //输出 4
n = 5;
cout << r; //输出5 

 定义引用时一定要将其初始化成引用某个变量。



 **引用只能引用变量,不能引用常量和表达式。**

double a = 4, b = 5;
double & r1 = a;
double & r2 = r1; // r2也引用 a
r2 = 10;
cout << a << endl; 
r1 = b; 
cout << a << endl; 

double a = 4, b = 5;
double & r1 = a;
double & r2 = r1; // r2也引用 a
r2 = 10;
cout << a << endl; // 输出 10
r1 = b; 
cout << a << endl; 

double a = 4, b = 5;
double & r1 = a;
double & r2 = r1; // r2也引用 a
r2 = 10;
cout << a << endl; // 输出 10
r1 = b; // r1并没有引用b 
cout << a << endl; 

double a = 4, b = 5;
double & r1 = a;
double & r2 = r1; // r2也引用 a
r2 = 10;
cout << a << endl; // 输出 10
r1 = b; // r1并没有引用b 
cout << a << endl; //输出 5

C语言中,如何编写交换两个整型变量值的函数?(与C++进行对比)

void swap( int * a, int * b)
{
    
    
int tmp;
tmp = * a; * a = * b; * b = tmp;//利用指针进行交换
}
int n1, n2;
swap(& n1,& n2) ; // n1,n2的值被交换


有了C++的引用:

void swap( int & a, int & b)
{
    
    
int tmp;
tmp = a; a = b; b = tmp;
}
int n1, n2;
swap(n1,n2) ; // n1,n2的值被交换

int n = 4;
int & SetValue() {
    
     return n; }
int main() 
{
    
    
SetValue() = 40;
cout << n;
return 0;
}

int n = 4;
int & SetValue() {
    
     return n; }
int main() 
{
    
    
SetValue() = 40;
cout << n;
return 0;
} //输出: 40

常引用

定义引用时,前面加const关键字,即为“常引用”

int n;
const int & r = n; 
r 的类型是 const int & 


不能通过常引用去修改其引用的内容:

int n = 100;
const int & r = n;
r = 200; //编译错
n = 300; // 没问题

常引用和非常引用的转换

const T & 和T & 是不同的类型!!!

T & 类型的引用或T类型的变量可以用来初始化const T & 类型的引用。

const T 类型的常变量和const T & 类型的引用则不能用来初始化T &类型的引用,除非进行强制类型
转换。

“const”关键字的用法

1) 定义常量
const int MAX_VAL = 23const string SCHOOL_NAME = "Peking University"1) 定义常量指针

int n,m;
const int * p = & n;
* p = 5; 
n = 4; 
p = &m; 

1) 定义常量指针

不可通过常量指针修改其指向的内容
int n,m;
const int * p = & n;
* p = 5; //编译出错
n = 4; 
p = &m; 



int n,m;
const int * p = & n;
* p = 5; //编译出错
n = 4; //ok
p = &m; 
35

int n,m;
const int * p = & n;
* p = 5; //编译出错
n = 4; //ok
p = &m; //ok, 常量指针的指向可以变化



不能把常量指针赋值给非常量指针,反过来可以

const int * p1; int * p2;
p1 = p2; //ok
p2 = p1; //error
p2 = (int * ) p1; //ok,强制类型转换




函数参数为常量指针时,可避免函数内部不小心改变
参数指针所指地方的内容

void MyPrintf( const char * p )
{
    
    
strcpy( p,"this"); //编译出错
printf("%s",p); //ok
}

2) 定义常引用

不能通过常引用修改其引用的变量

int n;
const int & r = n;
r = 5; //error
n = 4; //ok

动态内存分配

用new 运算符实现动态内存分配(教材P109)

第一种用法,分配一个变量:

P = new T; 

T是任意类型名,P是类型为T * 的指针。

动态分配出一片大小为 sizeof(T)字节的内存空间,并且将该
内存空间的起始地址赋值给P。比如:

int * pn;
pn = new int; 
* pn = 5;

用new 运算符实现动态内存分配(教材P109)

第二种用法,分配一个数组:

P = new T[N]; 

T :任意类型名
P :类型为T * 的指针
N :要分配的数组元素的个数,可以是整型表达式
动态分配出一片大小为 sizeof(T)*N字节的内存空间,并
且将该内存空间的起始地址赋值给P。



动态分配数组示例:

int * pn;
int i = 5;
pn = new int[i * 20];
pn[0] = 20;
pn[100] = 30; //编译没问题。运行时导致数组越界

用delete运算符释放动态分配的内存

用“new”动态分配的内存空间,一定要用“delete”运算符进行释放
delete 指针;//该指针必须指向new出来的空间

int * p = new int;
* p = 5;
delete p;
delete p; //导致异常,一片空间不能被delete多次

用delete运算符释放动态分配的数组

用“delete”释放动态分配的数组,要加“[]”

delete [ ] 指针;//该指针必须指向new出来的数组

int * p = new int[20];
p[0] = 1;
delete [ ] p;

内联函数

函数调用是有时间开销的。如果函数本身只有几条语句,执行非常快,而且函数被反复执行很多次,相比之下调用函数所产生的这个开销就会显得比较大。

为了减少函数调用的开销,引入了内联函数机制。编译器处理对内联函数的调用语句时,是将整个函数的代码插入到调用语句处,而不会产生调用函数的语句。

1.inline int Max(int a,int b)
{
    
    
if( a > b) return a;
return b;
}

2.下面是一个简单的示例,展示如何使用内联函数来计算两个整数之和:

```c++
#include <iostream>
inline int sum(int a, int b) // 定义内联函数
{
    
    
    return a + b; // 直接返回两数之和
}
int main()
{
    
    
    int x = 5;
    int y = 10;
    std::cout << "sum of " << x << " and " << y << " is " << sum(x, y) << '\n';

    return 0;
}
在编译期间,编译器通过将`sum()`函数的代码插入到主程序中,来避免函数调用时发生额外的开销。此外,由于此函数的代码块非常简短,因此提高效率的作用更加明显。

函数重载

一个或多个函数,名字相同,然而参数个数或参数类型不相同,这叫做函数的重载。

以下三个函数是重载关系:
int Max(double f1,double f2) {
    
     }
int Max(int n1,int n2) {
    
     }
int Max(int n1,int n2,int n3) {
    
     }

函数重载使得函数命名变得简单。

编译器根据调用语句的中的实参的个数和类型判断应该调用哪个函数。
下面是一个简单的示例,展示如何使用函数重载来实现不同数据类型的加法运算:

#include <iostream>

// 加法运算:两个整数相加
int sum(int a, int b)
{
    return a + b;
}

// 加法运算:两个浮点数相加
double sum(double a, double b)
{
    return a + b;
}

int main()
{
    // 整数相加
    int x = 5;
    int y = 10;
    std::cout << "sum of " << x << " and " << y << " is " << sum(x, y) << '\n';

    // 浮点数相加
    double d1 = 2.5;
    double d2 = 3.7;
    std::cout << "sum of " << d1 << " and " << d2 << " is " << sum(d1, d2) << '\n';

    return 0;
}

在此示例中,我们定义了两个具有相同名称sum的函数,但是输入参数的类型不同。分别为整型和浮点型,这样就可以使用函数重载来实现对不同类型数据的处理和计算。

函数缺省参数

C++中,定义函数的时候可以让最右边的连续若干个参数有缺省值,那么调用函数的时候,若相应位置不写参数,参数就是缺省值

void func( int x1, int x2 = 2, int x3 = 3) 
{
    
     }
func(10 ) ; //等效于 func(10,2,3)
func(10,8) ; //等效于 func(10,8,3)
func(10, , 8) ; //不行,只能最右边的连续若干个参数缺省

函数参数可缺省的目的在于提高程序的可扩充性。

即如果某个写好的函数要添加新的参数,而原先那些调用该函数的语句,未必需要使用新增的参数,那么为了避免对原先那些函数调用语句的修改,就可以使用缺省参数。

类和对象

结构化程序设计
C语言使用结构化程序设计:

程序 = 数据结构 + 算法

程序由全局变量以及众多相互调用的函数组成。

算法以函数的形式实现,用于对数据结构进行操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QI5c2NTp-1688033782516)(2023-06-05-19-28-48.png)]

结构化程序设计的不足:

结构化程序设计中,函数和其所操作的数据结构,没有直观的联系。

随着程序规模的增加,程序逐渐难以理解,很难一下子看出来:

某个数据结构到底有哪些函数可以对它进行操作?

某个函数到底是用来操作哪些数据结构的?

任何两个函数之间存在怎样的调用关系?

结构化程序设计没有“封装”和“隐藏”的概念。要访问某个数据结构中的某个变量,就可以直接访问,那么当该变量的定义有改动的时候,就要把所有访问该变量的语句找出来修改,十分不利于程序的维护、扩充。

难以查错,当某个数据结构的值不正确时,难以找
出到底是那个函数导致的。

重用:在编写某个程序时,发现其需要的某项功能,在现有的某个程序里已经有了相同或类似的实现,那么自然希望能够将那部分代码抽取出来,在新程序中使用。

在结构化程序设计中,随着程序规模的增大,由于程序大量函数、变量之间的关系错综复杂,要抽取这部分代码,会变得十分困难。

总之,结构化的程序,在规模庞大时,会变得难以理解,难以扩充(增加新功能),难以查错,难以重用。

软件业的目标是更快、更正确、更经济地建立软件。

• 如何更高效地实现函数的复用?

• 如何更清晰的实现变量和函数的关系?使得程序更清晰更易于修改和维护。

面向对象程序设计和面向过程程序设计的对比

下面分别给出一个简单示例展示面向对象程序设计和面向过程程序设计之间的区别:

面向对象程序设计示例(C++)

#include <iostream>
using namespace std;

class Rectangle // 定义矩形类
{
    
    
public:
    double width; // 矩形宽度
    double height; // 矩形高度

    // 计算矩形面积
    double area() {
    
     return width * height; }

    // 输出矩形属性信息
    void printInfo()
    {
    
    
        cout << "Width: " << width << endl;
        cout << "Height: " << height << endl;
        cout << "Area: " << area() << endl;
    }
};

int main()
{
    
    
    Rectangle r1; // 创建一个矩形对象
    r1.width = 2.5; // 设置矩形宽度
    r1.height = 3.7; // 设置矩形高度
    r1.printInfo(); // 输出矩形属性信息

    return 0;
}

在上述代码中,我们定义了一个Rectangle类,包括矩形的属性(宽、高)以及行为(计算面积、输出信息)。然后在主函数中,创建了一个矩形对象,并通过其成员函数实现对矩形的操作。

面向过程程序设计示例(C语言)

#include <stdio.h>

// 计算矩形面积
double area(double width, double height)
{
    
    
    return width * height;
}

// 输出矩形信息
void printInfo(double width, double height)
{
    
    
    printf("Width: %.2f\n", width);
    printf("Height: %.2f\n", height);
    printf("Area: %.2f\n", area(width, height));
}

int main()
{
    
    
    double w = 2.5; // 矩形宽度
    double h = 3.7; // 矩形高度

    printInfo(w, h); // 输出矩形属性信息

    return 0;
}

在上述代码中,我们定义了area函数和printInfo函数来计算矩形的面积和输出矩形的属性信息。然后在主函数中,通过调用这些函数来实现对矩形的操作。

从上述两个示例可以看出,面向对象程序设计注重对象的封装、抽象和继承等特性,代码清晰、易读、易于修改,对于大型程序的开发而言具有较强的优势。面向过程程序设计则更加强调算法的设计和流程控制,并希望通过简单、清晰的代码来完成某些重复性工作。

面向对象的程序设计

面向对象的程序设计方法,能够较好解决上述问题。

面向对象的程序 = 类 + 类 + …+ 类

设计程序的过程,就是设计类的过程。

面向对象的程序设计方法:

将某类客观事物共同特点(属性)归纳出来,形成一个数据
结构(可以用多个变量描述事物的属性);将这类事物所能进行的行为也归纳出来,形成一个个函数,这些函数可以用来操作数据结构(这一步叫“抽象”)。然后,通过某种语法形式,将数据结构和操作该数据结构的函数“捆绑”在一起,形成一个“类”,从而使得数据结构和操作该数据结构的算法呈现出显而易见的紧密关系,这就是“封装”。

面向对象的程序设计具有“抽象”,“封装”“继承”“多态”四个基本特点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZUsnCQ39-1688033782516)(2023-06-05-19-35-56.png)]

类和对象

在C++中,类(Class)是一种面向对象的概念,它描述了一个包含数据和方法(函数)的抽象实体,用来定义某个对象的属性和行为。类只是模板或蓝图,在创建对象时依据其定义,用于声明一个具有特定属性和功能的新数据类型。

而对象(Object)则是通过实例化一个类(可以理解为从类中生成一个具体的实例)而得到的一个真实存在的事物,拥有类所描述的属性和行为。通过操作对象的属性和行为,我们可以完成各种任务和操作。

例如,我们可以定义一个名为Rectangle的类来描述矩形,如下所示:

class Rectangle 
{
    
    
public:
    double width;
    double height;

    double area()
    {
    
    
        return width * height;
    }
};

该类中包含属性widthheight,表示矩形的长和宽,并定义了一个成员函数area()用于计算矩形的面积。现在我们可以通过实例化这个类来创建一个真正的矩形对象,如下所示:

Rectangle r; // 创建一个矩形对象
r.width = 2.5; // 设置矩形的宽度
r.height = 3.7; // 设置矩形的高度
double a = r.area(); // 调用矩形的成员函数计算矩形的面积

这里的r就是一个矩形对象,它包含了类Rectangle中定义的属性和行为(成员函数),我们可以通过直接操作这些属性来进行计算,而不必关心具体的实现方式。

C++中的类和对象提供了一种抽象和封装的机制,帮助程序员更好地管理和组织代码,并以更高效、更安全的方式进行编程和设计。

对象的内存分配

和结构变量一样,对象所占用的内存空间的大小,等于所有成员变量的大小之和。

对于上面的CRectangle类,sizeof(CRectangle) = 8

每个对象各有自己的存储空间。一个对象的某个成员变量被改变了,不会影响到另一个对象。

对象间的运算
和结构变量一样,对象之间可以用 “=”进行赋值,但是不能用 “==”,“!=”,“>”,“<”“>=”“<=”进行比较,除非这些运算符经过了“重载”。

用法1:对象名.成员名

CRectangle r1,r2;
r1.w = 5;
r2.Init(5,4); //Init函数作用在 r2 上,即Init函数执行期间访问的
//w 和 h是属于 r2 这个对象的, 执行r2.Init 不会影响到 r1。

用法2. 指针->成员名

CRectangle r1,r2;
CRectangle * p1 = & r1;
CRectangle * p2 = & r2;
p1->w = 5;
p2->Init(5,4); //Init作用在p2指向的对象上

用法3:引用名.成员名

CRectangle r2;
CRectangle & rr = r2;
rr.w = 5;
rr.Init(5,4); //rr的值变了,r2的值也变
void PrintRectangle(CRectangle & r)
{
    
     cout << r.Area() << ","<< r.Perimeter(); }
CRectangle r3;
r3.Init(5,4); 
PrintRectangle(r3);

类成员的可访问范围

在类的定义中,用下列访问范围关键字来说明类成员可被访问的范围:

private: 私有成员,只能在成员函数内访问
– public : 公有成员,可以在任何地方访问
– protected: 保护成员,以后再说

以上三种关键字出现的次数和先后次序都没有限制。

定义一个类
class className {
    
    
private:
私有属性和函数//说明类成员的可访问范围
public:
公有属性和函数//说明类成员的可访问范围
protected:
保护属性和函数//说明类成员的可访问范围
};

如果某个成员前面没有上述关键字,则缺省地被认为
是私有成员。


class Man {
    
    
int nAge; //私有成员
char szName[20]; // 私有成员
public:
void SetName(char * szName){
    
    
strcpy( Man::szName,szName);
} 
};

在类的成员函数内部,能够访问:当前对象的全部属性、函数;同类其它对象的全部属性、函数。

在类的成员函数以外的地方,只能够访问该类对象的公有成员

class CEmployee {
    
     
private:
char szName[30]; //名字
public :
int salary; //工资
void setName(char * name); 
void getName(char * name);
void averageSalary(CEmployee e1,CEmployee e2);
};
void CEmployee::setName( char * name) {
    
    
strcpy( szName, name); //ok
}
void CEmployee::getName( char * name) {
    
    
strcpy( name,szName); //ok
}
void CEmployee::averageSalary(CEmployee e1,
CEmployee e2){
    
    
cout << e1.szName; //ok,访问同类其他对象私有成员
salary = (e1.salary + e2.salary )/2;
}
int main()
{
    
    
CEmployee e;
strcpy(e.szName,"Tom1234567889"); //编译错,不能访
问私有成员
e.setName( "Tom"); // ok
e.salary = 5000; //ok
return 0;
}
int main()
{
    
    
CEmployee e;
strcpy(e.szName,"Tom1234567889"); //编译错,不能访
问私有成员
e.setName( "Tom"); // ok
e.salary = 5000; //ok
return 0;

设置私有成员的机制,叫“隐藏

“隐藏”的目的是强制对成员变量的访问一定要通过成员函数进行,那么以后成员变量的类型等属性修改后,只需要更改成员函数即可。否则,所有直接访问成员变量的语句都需要修改。

如果将上面的程序移植到内存空间紧张的手持设备上,希望szName 改为 char szName[5],若szName不是私有,那么就要找出所有类似

strcpy(e.szName,“Tom1234567889”);

这样的语句进行修改,以防止数组越界。这样做很麻烦。

如果将szName变为私有,那么程序中就不可能出现(除非在类的
内部)strcpy(e.szName,“Tom1234567889”);这样的语句,所有对 szName的访问都是通过成员函数来进行,比如:

e.setName( “Tom12345678909887”);

那么,就算szName改短了,上面的语句也不需要找出来修改,只要改 setName成员函数,在里面确保不越界就可以了。

用struct定义类

struct CEmployee {
    
     
char szName[30]; //公有!!
public :
int salary; //工资
void setName(char * name); 
void getName(char * name);
void averageSalary(CEmployee 
e1,CEmployee e2);
};
和用"class"的唯一区别,就是未说明是公有还是私有的成员,就是公有

成员函数的重载及参数缺省

成员函数的重载(Overloading)指的是在同一个类中定义多个名称相同但参数个数或参数类型不同的成员函数,以实现类似的功能但具有不同的行为。重载可以极大提高代码的复用性和可读性,在需要使用同一函数名但行为却略有不同的情况下,使用重载能够让代码更为简洁。

重载的方式具体有两种:

  1. 同名不同参:函数名称相同,但是参数个数或类型不同,如下所示:
class Rectangle
{
    
    
public:
    double width;
    double height;

    double area()
    {
    
    
        return width * height;
    }

    int area(int times)
    {
    
    
        return width * height * times;
    }
};

上述例子定义了两个area方法,其功能都是计算矩形面积。在第一个函数中,该方法不接受任何参数,返回浮点数类型的计算结果;而在第二个函数中,该方法接受一个整型参数,并将浮点数类型的面积值乘以这个参数,最后返回整型类型的计算结果。这样在调用时,可以根据不同的需求选择不同的方法来处理数据:

Rectangle r;
r.width = 2.5;
r.height = 3.7;

double a = r.area(); // 调用第一个area方法
int b = r.area(2);   // 调用第二个area方法
  1. 同名同参但类型不同:函数名称和参数完全相同,但是返回值类型不同。例如,可以有一个成员函数和一个友元函数都名为operator+(), 其形参和行为相同,只是前者的调用方式限定在该类的对象上。

成员函数也支持参数缺省(Default Arguments)的语法,允许在定义成员函数时声明某个或某些参数的默认值,而在函数调用时如果没有传递对应的参数,则使用默认值,如下所示:

class Rectangle
{
    
    
public:
    double width;
    double height;

    double area(double rate = 1.0)
    {
    
    
        return width * height * rate;
    }
};

上述代码中, double rate = 1.0 声明了一个默认参数,当调用该函数时如果没有指定rate值,则默认为1.0。这个特性广泛用于提高重载函数的可读性,增加使用方便性。

总之,通过使用重载和参数缺省,我们可以面向对象设计中实现更丰富、灵活和易用的编程风格来应对不同的运算需求。

使用缺省参数要注意避免有函数重载时的二义性

class Location {
    
    
private :
int x, y;
public:
void init( int x =0, int y = 0 );
void valueX( int val = 0) {
    
     x = val; }
int valueX() {
    
     return x; }
};
Location A;
A.valueX(); //错误,编译器无法判断调用哪个valueX

构造函数(constructor)

基本概念

成员函数的一种

名字与类名相同,可以有参数,不能有返回值(void也不行)

作用是对对象进行初始化,如给成员变量赋初值

如果定义类时没写构造函数,则编译器生成一个默认的无参数的构造函数

•默认构造函数无参数,不做任何操作

如果定义了构造函数,则编译器不生成默认的无参数的构造函数对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数一个类可以有多个构造函数

为什么需要构造函数:

  1. 构造函数执行必要的初始化工作,有了构造函数,就不必专门再写初始化函数,也不用担心忘记调用初始化函数。
  2. 有时对象没被初始化就使用,会导致程序出错。
class Complex {
    
    
private:
double real, imag;
public:
void Set( double r, double i);
}; //编译器自动生成默认构造函数
Complex c1; //默认构造函数被调用
Complex * pc = new Complex; //默认构造函数被调用

class Complex {
    
    
private :
double real, imag;
public:
Complex( double r, double i = 0);
}; 
Complex::Complex( double r, double i) {
    
    
real = r; imag = i;
}
Complex c1; // error, 缺少构造函数的参数
Complex * pc = new Complex; // error, 没有参数
Complex c1(2); // OK
Complex c1(2,4), c2(3,5);
Complex * pc = new Complex(3,4);

可以有多个构造函数,参数个数或类型不同

class Complex {
    
    
private :
double real, imag;
public:
void Set( double r, double i );
Complex(double r, double i );
Complex (double r );
Complex (Complex c1, Complex c2); 
}; 
Complex::Complex(double r, double i)
{
    
    
real = r; imag = i;
}

Complex::Complex(double r)
{
    
    
real = r; imag = 0;
}
Complex::Complex (Complex c1, Complex c2); 
{
    
    
real = c1.real+c2.real;
imag = c1.imag+c2.imag;
}
Complex c1(3) , c2 (1,0), c3(c1,c2);
// c1 = {3, 0}, c2 = {1, 0}, c3 = {4, 0};

构造函数最好是public的,private构造函数

不能直接用来初始化对象

class CSample{
    
    
private:
CSample() {
    
    
}
};
int main(){
    
    
CSample Obj; //err. 唯一构造函数是private
return 0;
}

在C++中,构造函数(Constructor)是一种特殊的成员函数,它用于在对象创建时初始化类的成员变量。通常,每个类都至少有一个构造函数。

构造函数的名称必须与类的名称相同,没有返回类型(void、int等),并且不能手动调用。构造函数的作用是在创建对象时为成员变量赋初值,如下所示:

class Rectangle 
{
    
    
public:
    double width;
    double height;

    // 构造函数
    Rectangle() 
    {
    
    
        width = 0;
        height = 0;
    }
};

上述代码定义了一个Rectangle类,并在其中定义了一个构造函数,该构造函数将宽度和高度初始化为空值。

构造函数在对象被创建时自动执行,可以根据不同需求重载多个不同的构造函数。比如,在构造函数中使用参数列表为成员变量赋值:

class Rectangle 
{
    
    
public:
    double width;
    double height;

    // 构造函数 #1
    Rectangle(double w, double h) 
    {
    
    
        width = w;
        height = h;
    }

    // 构造函数 #2
    Rectangle() 
    {
    
    
        width = 0;
        height = 0;
    }
};

上述代码中定义了两个构造函数,第一个构造函数用来传入实际的宽度和高度进行初始化,而第二个构造函数不需要任何参数,则按照预设值将其初始化为0。

通过构造函数,在创建对象时就可以有效地进行成员变量的初始化,提高了代码的效率和可读性。需要注意的是,在一个类中只能有一个析构函数(Destructor),它与构造函数类似但在对象被销毁时启动执行。

另外需要注意的是,C++中的构造函数也可以通过初始化列表(Initialization List)的方式来对成员变量进行初始化,这种方式比在构造函数体中赋值效率更高,并且允许直接初始化常量和引用类型的成员变量。如下所示:

class Rectangle 
{
    
    
public:
    double width;
    double height;

    // 构造函数 #1
    Rectangle(double w, double h): width(w), height(h)
    {
    
    
        // 无需将参数分别复制到成员变量中
    }

    // 构造函数 #2
    Rectangle() : width(0), height(0)
    {
    
    
        // 直接将成员变量初始化为0
    }
};

最后还需要提出的是,如果一个类没有定义构造函数,则编译器将自动提供一个默认的构造函数,其中的成员变量都将按照其数据类型的默认规则进行初始化,如int为0、string为空等。如果需要完全掌握类的初始化过程,建议手动实现构造函数以确保代码的可靠性和安全性。

//构造函数在数组中的使用

在C++中,我们可以通过定义构造函数来自定义对象的创建和初始化过程。而当需要创建一个对象数组时,同样也可以使用构造函数来对每个对象进行初始化。

在创建对象数组时,编译器会首先调用默认的构造函数来初始化其中的所有对象。如果类定义了自己的构造函数,则编译器会使用这个构造函数来进行初始化。例如,下面的代码定义了一个Rectangle类,并在该类中添加了一个构造函数:

class Rectangle 
{
    
    
public:
    double width;
    double height;

    // 构造函数
    Rectangle(double w, double h) 
    {
    
    
        width = w;
        height = h;
    }
};

可以按常规方式声明和实例化一个对象:

Rectangle r(3.5, 4.5);

但是如果要创建多个矩形怎么办呢?我们可以使用类似于下面示例的数组语法来进行初始化:

Rectangle rects[] = {
    
    Rectangle(1, 2), Rectangle(2, 3), Rectangle(3, 4)};

上述代码中,我们利用花括号{}列表形式来对数组元素进行初始化,其中每个元素都使用了 Rectangle 的构造函数来创建并初始化它们自己的成员变量widthheight

需要注意,当使用构造函数创建对象数组时,数组中的每个元素都是独立的实例,它们之间没有任何关联。因此,在不同位置修改其中任意的元素都不会影响其他元素,也就是说它们是彼此独立的。

总之,通过构造函数我们可以在创建对象时对其成员变量进行初始化,并利用数组语法来创建多个并存的对象。这种方法非常灵活,方便快捷,在实际开发中具有很高的应用价值。

除了以上提到的花括号{}列表初始化方法,还可以使用循环来初始化对象数组中的元素。例如:

Rectangle rects[3]; // 创建一个长度为3的矩形数组

for (int i = 0; i < 3; ++i) 
{
    
    
    rects[i] = Rectangle(i+1, i+2); // 使用构造函数创建并赋值给每个数组元素
}

上述代码中,我们首先创建了一个长度为3的Rectangle类型数组,并且没有指定初始值,然后使用循环遍历数组的每个元素,使用构造函数进行初始化。需要注意的是,这种方法只有在数组已经被定义时才能使用,因为在C++中数组一旦被定义就不能改变其大小。

总之,使用构造函数初始化对象数组的方法非常灵活多样,开发者可以自由选择合适的方法来实现自己的需求。同时,也需要注意在对象数组的使用中要保证构造函数的正确性和安全性,以避免潜在的问题。
除了在对象数组中使用构造函数进行初始化外,有时候我们还需要手动调用析构函数来释放对象占用的资源。在C++中,通过使用delete操作符可以显式地调用对象的析构函数,并将其从内存中销毁。

例如,在下面这个例子中,我们定义了一个类MyClass,并在其中添加了构造函数和析构函数:

class MyClass 
{
    
    
public:
    MyClass() {
    
     /* 构造函数代码 */ }
    ~MyClass() {
    
     /* 析构函数代码 */ }
};

此时,如果我们想要手动销毁某些对象实例,可以使用delete操作符来调用它们的析构函数,如下所示:

MyClass* obj = new MyClass(); // 创建一个MyClass类型对象

// 在不需要obj对象时,手动释放该对象
delete obj; // 调用析构函数并将对象从内存中销毁

上述代码中,我们利用关键字new来创建了一个对象实例,并将其保存到指向该对象的指针obj中。然后,在不再需要该对象时,我们可以使用delete操作符手动释放并销毁该对象,从而避免内存泄露和资源浪费问题。

需要注意的是,在调用delete操作符时,如果删除的对象是通过数组元素创建的,则必须使用delete[]操作符来销毁该数组,如下所示:

MyClass* objs = new MyClass[10]; // 创建一个包含10个对象的数组

// 在不需要objs数组时,手动释放该数组
delete[] objs; // 调用析构函数并将数组从内存中销毁

总之,在C++中使用构造函数和析构函数,可以在对象创建和销毁时对其进行一系列操作,例如对成员变量进行初始化和资源释放等,具有极高的可定制性和实用价值。但同时也要注意对构造函数和析构函数的正确使用和合理管理,以确保代码效率和正确性。

//复制构造函数 copy constructor

基本概念

只有一个参数,即对同类对象的引用。

形如 X::X( X& )或X::X(const X &), 二者选一后者能以常量对象作为参数

如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能。

复制构造函数是一个特殊的构造函数,用于创建一个新对象,该对象与另一个已经存在的同类对象具有相同的值。通常在需要创建一个与已有对象值相同的新对象时使用。下面是一个C++语言的示例代码,其中定义了一个名为Person的类,包含成员变量name和age,并实现了一个复制构造函数。

#include <iostream>
#include <string>

using namespace std;

class Person {
    
    
public:
    string name;
    int age;
    Person(string n, int a) : name(n), age(a) {
    
    } // 构造函数
    Person(const Person &p) {
    
                      // 复制构造函数
        name = p.name;
        age = p.age;
    }
};

int main() {
    
    
    Person p1("Alice", 20);
    Person p2 = p1;     // 使用复制构造函数创建p2对象,其值与p1相等
    cout << p1.name << " " << p1.age << endl;
    cout << p2.name << " " << p2.age << endl;
    return 0;
}

在上述示例代码中,我们定义了一个Person类,它包含两个成员变量:name和age。在构造函数中初始化这两个成员变量。另外,还实现了一个复制构造函数,在创建新的Person类对象时,将原对象的属性进行拷贝并赋给新对象,从而使新对象与原对象的值相等。

在主函数中,首先创建了一个Person类型的对象“p1”,然后通过使用复制构造函数创建了一个新对象“p2”。最后,分别输出两个对象的姓名和年龄。可以看到,p1和p2的值是相等的。

总之,复制构造函数在C++语言中是一个很常用的函数,通过它我们可以将已有对象的值复制并赋给一个新的对象。

复制构造函数有以下几个特点:

  1. 它是一个特殊的构造函数,带有唯一的参数,即对同类的引用。
  2. 当使用同类的对象来初始化一个新的对象时,复制构造函数会被自动调用。例如,如上述示例中的语句“Person p2 = p1;”。
  3. 复制构造函数的作用是实现对象的浅拷贝或深拷贝。在进行复制操作时,可以根据需要选择对成员变量进行赋值,从而使得新对象与原对象具有相同的值。
  4. 如果没有显式地定义复制构造函数,编译器也会自动生成一个默认的复制构造函数,但该函数只是简单地将原始对象的所有成员变量复制到新对象中,不能满足所有情况的需求,因此通常我们需要根据特定场景自己定义复制构造函数。

需要注意的是,由于复制构造函数经常用到,所以如果程序中存在指向动态内存的指针或者引用,那么在定义复制构造函数时要非常小心。复制构造函数中的逻辑运算应该保证复制后的对象和原来的对象彼此独立,而且同样避免出现野指针等错误。

通常情况下,当类中包含了动态分配的内存时,需要重载复制构造函数以实现正确的深度复制。下面给出一个示例代码:

class Person {
    
    
public:
    string *name;          // 定义指向string类型的指针
    int age;
    Person(string n, int a) : age(a) {
    
           // 构造函数
        name = new string(n);                // 动态分配内存并初始化
    }
    Person(const Person &p) : age(p.age) {
    
      // 复制构造函数
        name = new string(*(p.name));        // 深拷贝指向string的指针
    }
    ~Person() {
    
                                 // 析构函数
        delete name;                        // 释放动态分配的内存
    }
};

在上述示例中,我们更改了Person类的实现方式,将其中的name变量从string类型变为了指向string类型的指针,并重载了复制构造函数与析构函数。

在构造函数中,我们使用了动态内存分配语句new string(n),这意味着我们为name变量分配了一块新的内存,并将它的指针赋值给了name。同时,在析构函数中通过调用delete name来释放动态分配的内存。

在复制构造函数中,我们为新对象分配了一块新的内存,并将原对象指针所指向的内容复制到新的内存中。这样,我们实现了对原对象和新对象内存空间的分离管理。

总之,正确地重载复制构造函数能够避免由于复制操作引起的一些问题,如野指针、内存泄漏等等。因此,在定义类的时候,如果有必要使用动态内存,就必须同时重新制定析构函数与复制构造函数来进行动态内存管理。

复制构造函数一般用于以下场景:

  1. 对象的初始化:在创建对象时,可以通过拷贝已有对象来初始化新的对象。此时,编译器会自动调用复制构造函数。

  2. 对象的赋值:当把一个对象赋值给另一个对象时,也会调用复制构造函数。例如,Person p1; Person p2 = p1;。这样就会使用p1对象的值进行初始化p2对象。

  3. 以值传递方式传递对象:当函数参数是一个对象时,如果该参数被以值传递的方式传递,那么将会调用其复制构造函数。例如,在定义函数void printPerson(Person p);时,如果调用printPerson(p1),那么它就会调用复制构造函数进行参数传递。

需要注意的是,在上述三种情况下,会自动调用复制构造函数,从而创建一个与原始对象相同的新对象。但在实际业务中,由于各种原因,程序员有时也要手动调用复制构造函数,来产生一个新对象并初始化它的内容。

//常量引用参数的使用

void fun(CMyclass obj_ ) {
    
    
cout << "fun" << endl;
}
这样的函数,调用时生成形参会引发复制构造函数调用,开销比较大。
所以可以考虑使用 CMyclass & 引用类型作为参数。
如果希望确保实参的值在函数中不应被改变,那么可以加上const 关键字:
void fun(const CMyclass & obj) {
    
    
//函数中任何试图改变 obj值的语句都将是变成非法
}

类型转换构造函数

定义转换构造函数的目的是实现类型的自动转换。

只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数。

当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)。

类型转换构造函数是一种将一个对象从一种类型转换为另一种类型的方式。

在C++中,我们可以在类中定义类型转换构造函数,来实现将类的对象隐式地转换成其他类型。其中,类型转换构造函数也是一种特殊的构造函数,它允许直接将一种类型的对象转换为另一种类型的对象。

下面给出一个简单的示例:

#include<iostream>
using namespace std;
class Complex{
    
    
    private:
        double real,imag;
    public:
        Complex(int a):real(a), imag(0){
    
        // 定义类型转换构造函数
            cout << "Converted!" << endl;
        }
};
int main(){
    
    
    int num = 3;
    Complex c1 = num;   // 将整型数num转换为Complex类型,此处会自动调用类型转换构造函数
    return 0;
}

在上述代码中,我们定义了一个名为Complex的类,包含两个成员变量realimag。在这个类的定义中,我们定义了一个类型转换构造函数,该构造函数只有一个参数,即一个整型数a,它用于把整数转换成复数。

在主函数中,我们定义了一个整数类型的变量num,并使用Complex c1 = num;语句将它隐式转换为了Complex类型的对象。运行程序后,可以看到输出信息“Converted!”,表示成功调用了类型转化构造函数。

总之,类型转换构造函数是一种可用于将一种类的对象隐式转化为另一种类型的构造函数。虽然可以很方便地将它们用作数据类型转换,但需要注意的是,它可能会导致程序出现潜在的问题(如精度误差等)。因此,在使用时必须小心谨慎。

//析构函数destructors

名字与类名相同,在前面加‘~’, 没有参数和返回值,一个类最多只能有一个析构函数。

析构函数对象消亡时即自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放分配的空间等。

如果定义类时没写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做。

如果定义了析构函数,则编译器不生成缺省析构函数。

析构函数是与构造函数相对应的一种特殊函数,主要用于在对象生命周期结束时清理对象占用的内存资源。

在C++中,每个类都可以定义自己的析构函数,它由一个波浪号~和类名组成,并且不需要参数或返回值。通常情况下,需要清理动态分配的内存、关闭打开的文件、释放网络连接等等操作都可以在析构函数中实现。

在下面的示例代码中,我们定义了一个名为Person的类,其中包含有两个成员变量nameage,我们在这个类中定义了一个简单的析构函数,用于在对象被销毁时输出一条消息:

#include<iostream>
using namespace std;

class Person {
    
    
public:
    string name;
    int age;
    Person(string n, int a): name(n), age(a) {
    
     // 构造函数
        cout << "Object created." << endl;
    }
    ~Person(){
    
                                     // 析构函数
        cout << "Object destroyed." << endl;
    }
};

int main() {
    
    
    Person p1("张三", 20);
    return 0;
}

在上述代码中,我们定义了一个Person对象p1,并初始化了它的属性(姓名和年龄)。在main()函数结束后,程序运行完毕,也就是Person对象p1的生命周期结束时,会自动调用析构函数~Person()来释放占用的内存资源。在这个析构函数中,我们简单地输出了一条消息“Object destroyed.”。

总之,析构函数是类的一个特殊函数,在对象生命周期结束时自动调用(或者手动调用),它的主要作用就是清理对象所占用的资源,例如动态分配的内存、打开的文件等。借助析构函数,我们能够更好地控制程序使用内存资源的方式,并避免出现内存泄漏等问题。

为什么需要析构函数

在C++中,我们经常要使用动态内存分配来创建对象,例如使用new关键字来为一个对象分配内存空间。由于程序运行完毕后需要释放这部分动态分配的内存空间,因此我们需要使用delete关键字来释放这些内存空间。

如果我们没有合理地释放这些内存,在程序执行一段时间之后,就会耗尽机器物理内存,导致系统崩溃或程序运行效率下降。因此,为了避免出现内存泄漏等问题,我们可以使用析构函数来释放所占用的内存资源。

此外,析构函数的另一个重要作用是帮助我们管理类中的各种数据结构和状态。通常我们可以使用析构函数来释放资源但需要注意的是,在大多数情况下,析构函数在对象被销毁时自动调用,程序员无需手动调用。

综上所述,析构函数是为了解决程序中动态内存申请和管理的问题而存在的,它是一种特殊的函数,当对象生命周期结束时自动调用,主要用于清理对象所占用的资源、还原对象的状态等。使用析构函数能够有效地管理内存资源,防止内存泄漏以及其它相关问题的发生。

析构函数和数组

在C++中,数组的生命周期也和其他对象一样;当程序执行到数组的作用域结束时,或者使用delete运算符来释放动态分配的数组空间时,数组也要被销毁。因此,如果一个类定义了析构函数,则它可以管理动态数组,并在销毁对象时自动调用。

下面是一个简单的示例代码,展示了如何在类中使用析构函数来管理动态数组:

#include<iostream>
using namespace std;

class Array{
    
    
private:
    int *arr;
    int size;
public:
    Array(int s):size(s) {
    
              // 构造函数,创建一个大小为s的数组
        arr = new int[s];
    }
    ~Array() {
    
                          // 析构函数,释放数组所占用的内存
        delete[] arr;
    }
};

int main(){
    
    
    Array a1(10);                  // 创建大小为10的数组对象
    return 0;
}

在上述代码中,我们定义了一个名为Array的类,其中包含一个指向int类型的指针arr,用于动态分配内存,并且定义了一个整型变量用于记录数组的大小。在类的构造函数Array(int s)中,我们使用new运算符为这个数组分配了s个元素的内存,并保存指向其地址的指针。然后,在析构函数~Array()中,我们使用delete[]运算符来释放数组的内存空间。

main()函数中,我们创建一个大小为10的数组对象a1,用于展示如何在类中使用析构函数来管理动态数组。当程序执行结束时,自动调用Array类的析构函数,进而自动释放动态分配的内存空间。

值得注意的是,在使用析构函数管理动态数组时需要小心一些细节问题,下面列出几点需要特别注意的事项:

  1. 析构函数应该使用delete[]运算符释放数组所占用的内存空间,否则可能会导致内存泄漏或程序崩溃。

  2. 如果在类中定义了其他指针类型的成员变量,例如char*型指针,则需要编写相应的析构函数来释放这些指针所指向的内存空间。

  3. 在使用动态数组时,应该进行异常处理,防止因为内存分配失败导致内存泄漏或程序崩溃。

  4. 除非有必要,否则不应该将动态数组作为类成员来使用,否则可能会破坏类的封装性和可移植性。如果必须使用动态数组作为类成员变量,则应该使用复制构造函数和赋值运算符来处理对象之间的拷贝和赋值操作。

总之,使用析构函数可以有效地管理动态数组并防止内存泄漏等问题,但需要程序员对其进行正确的使用与实现。

析构函数和运算符 delete

delete 运算导致析构函数调用。

Ctest * pTest;
pTest = new Ctest; //构造函数调用
、
pTest = new Ctest[3]; //构造函数调用3次
delete [] pTest; //析构函数调用3次

若new一个对象数组,那么用delete释放时应该写 []。否则只delete一个对象(调用一次析构函数)

析构函数在对象作为函数返回值返回后被调用

class CMyclass {
    
    
public:
~CMyclass() {
    
     cout << "destructor" << endl; }
};
CMyclass obj;
CMyclass fun(CMyclass sobj ) {
    
     //参数对象消亡也会导致析
//构函数被调用
return sobj; //函数调用返回时生成临时对象返回
}
int main(){
    
    
obj = fun(obj); //函数调用的返回值(临时对象)被
return 0; //用过后,该临时对象析构函数被调用
}

输出:
destructor
destructor
destructor

构造函数和析构函数什么时候被调用

构造函数和析构函数的调用时机如下:

构造函数:

  • 在创建对象时自动被调用,即当我们定义一个类的对象,使用它的构造函数来初始化该对象时就会被自动调用。
  • 构造函数还可以在派生类中显示调用基类的构造函数。这通常是通过在子类的成员初始化列表中调用它来实现的。

析构函数:

  • 当一个对象生命到达其范围的尽头或者当对动态分配内存空间的对象应用delete运算符时,析构函数将被自动调用。
  • 在一些情况下,析构函数也可以显式地调用来释放对象的资源。

例如,考虑下面这个小例子:

#include<iostream>
using namespace std;

class Person {
    
    
public:
    string name;
    int age;
    Person() {
    
    
        cout << "Constructor called!" << endl;
    }
    ~Person() {
    
    
        cout << "Destructor called!" << endl;
    }
};
int main() {
    
    
    Person p1;               // 创建Person类型的对象p1,此时构造函数被调用
    return 0;
}   

在上述代码中,我们定义了一个名为Person的类,并在其中定义了构造函数和析构函数。在main()函数中,我们创建一个Person类型的对象p1,它的定义触发了构造函数的调用;在程序结束之前,Person对象p1的范围已经结束,同时达到了它的生命周期结束这一条件,触发了析构函数的调用。

总之,构造函数和析构函数都将在对象创建和销毁时分别被自动调用。对于不同类型的类,当创建或销毁实例时,C++编译器会自动调用相应的构造函数和析构函数来处理相关的操作。

需要注意的是,如果一个类包含静态成员变量,则必须显式地定义该静态成员变量的构造函数和析构函数。因为静态对象必须在程序开始执行之前被初始化,在程序结束时被销毁。例如:

#include<iostream>
using namespace std;

class MyClass {
    
    
public:
    MyClass() {
    
    
        cout << "Constructor called." << endl;
    }
    ~MyClass() {
    
    
        cout << "Destructor called." << endl;
    }
};

class WithStaticMember{
    
    
public:
    static MyClass sm;    // 静态成员变量,必须显式定义其构造函数和析构函数
    WithStaticMember() {
    
    
        cout << "WithStaticMember constructor called." << endl;
    }
    ~WithStaticMember() {
    
    
        cout << "WithStaticMember destructor called." << endl;
    }
};

MyClass WithStaticMember::sm;  // 显式定义静态成员变量的构造函数和析构函数

int main() {
    
    
    WithStaticMember wsm;      // 创建对象wsm,此时调用相关构造函数
    return 0;
}

在上述代码中,我们定义了一个名为WithStaticMember的类,并在其中定义了包含一个静态成员变量sm的类成员。由于静态成员变量必须在类外部进行初始化,所以我们需要在程序文件级别上定义其构造函数和析构函数。在main()函数中,我们创建WithStaticMember类的实例wsm,此时相关构造函数自动调用;当程序结束时,对应的析构函数被自动调用。

总之,在包含静态成员变量的类中,必须显式定义其构造函数和析构函数以正确地初始化和销毁这些变量,否则会导致程序无法编译或运行异常。

//C++程序到C程序的翻译

class CCar {
    
    
public:
int price;
void SetPrice(int p);
};
void CCar::SetPrice(int p) 
{
    
     price = p; }
int main()
{
    
    
CCar car;
car.SetPrice(20000);
return 0;
}
struct CCar {
    
     
int price; 
};
void SetPrice(struct CCar * this,
int p)
{
    
     this->price = p; }
int main() {
    
    
struct CCar car;
SetPrice( & car, 
20000);
return 0;
}

this指针作用

其作用就是指向成员函数所作用的对象

非静态成员函数中可以直接使用this来代表指向该函数作用的对象的指针。

class Complex {
    
    
public:
double real, imag;
void Print() {
    
     cout << real << "," << imag ; }
Complex(double r,double i):real(r),imag(i) 
{
    
     }
Complex AddOne() {
    
    
this->real ++; //等价于 real ++;
this->Print(); //等价于 Print
return * this;
} 
}; 
int main() {
    
    
Complex c1(1,1),c2(0,0);
c2 = c1.AddOne();
return 0;
} //输出 2,1
class A
{
    
     
int i;
public: 
void Hello() {
    
     cout << i << "hello" << endl; } 
};void Hello(A * this ) {
    
     cout << this->i << "hello" 
<< endl; } 
//this若为NULL,则出错!!
int main() 
{
    
     
A * p = NULL;
p->Hello();Hello(p);
} // 输出:hello

在C++中,this指针是一个特殊的指针,它指向当前对象的地址。它可以在类的成员函数中使用,用于访问调用该函数的对象的成员变量和成员函数。
当类的成员函数被调用时,编译器会隐式地传递一个this指针作为参数给该函数,以便让函数知道它是哪个对象的成员函数被调用了。通过this指针,我们可以在成员函数中访问对象的成员变量和其他成员函数。
举个例子,假设有一个名为"Person"的类,其中有一个成员变量name和一个成员函数printName(),那么在printName()函数中,可以使用this指针来访问name成员变量,如下所示:

class Person {
    
    
private:
    std::string name;
public:
    void setName(std::string n) {
    
    
        this->name = n;
    }
    void printName() {
    
    
        std::cout << "My name is: " << this->name << std::endl;
    }
};

在上面的例子中,this->name表示当前对象的name成员变量,this->name = n表示将传入的n赋值给当前对象的name成员变量。
需要注意的是,this指针只能在非静态成员函数中使用。静态成员函数没有this指针,因为它们不属于任何具体的对象。
总结一下,this指针在C++中用于访问当前对象的成员变量和成员函数,它是一个隐含传递给成员函数的指针。通过this指针,可以方便地操作和使用对象的成员。

在C++中,this指针是一个特殊的指针,它指向当前对象的地址。它可以在类的成员函数中使用,用于访问调用该函数的对象的成员变量和成员函数。
当类的成员函数被调用时,编译器会隐式地传递一个this指针作为参数给该函数,以便让函数知道它是哪个对象的成员函数被调用了。通过this指针,我们可以在成员函数中访问对象的成员变量和其他成员函数。
举个例子,假设有一个名为"Person"的类,其中有一个成员变量name和一个成员函数printName(),那么在printName()函数中,可以使用this指针来访问name成员变量,如下所示:

class Person {
    
    
private:
    std::string name;
public:
    void setName(std::string n) {
    
    
        this->name = n;
    }
    void printName() {
    
    
        std::cout << "My name is: " << this->name << std::endl;
    }
};

在上面的例子中,this->name表示当前对象的name成员变量,this->name = n表示将传入的n赋值给当前对象的name成员变量。
需要注意的是,this指针只能在非静态成员函数中使用。静态成员函数没有this指针,因为它们不属于任何具体的对象。

当涉及到多个对象之间的交互时,this指针也非常有用。通过使用this指针,我们可以将一个对象作为参数传递给另一个对象的成员函数,从而实现对象之间的通信。
举个例子,假设我们有两个类:Student和Teacher。在Teacher类中,我们定义了一个成员函数addStudent(),用于向Teacher对象中添加一个Student对象。代码如下所示:

class Student {
    
    
private:
    std::string name;
public:
    Student(std::string n) : name(n) {
    
    }
    std::string getName() {
    
    
        return name;
    }
};
class Teacher {
    
    
private:
    std::vector<Student> students;
public:
    void addStudent(Student& student) {
    
    
        students.push_back(student);
        std::cout << "Added student: " << student.getName() << std::endl;
    }
};

在上面的例子中,addStudent()函数接受一个Student对象的引用作为参数。通过使用this指针,我们可以在Teacher对象中调用该函数,并将当前Teacher对象作为参数传递给addStudent()函数,从而将自己添加到Teacher对象的students容器中。
使用示例如下:

int main() {
    
    
    Teacher teacher;
    Student student1("Alice");
    teacher.addStudent(student1);
    Student student2("Bob");
    teacher.addStudent(student2);
    return 0;
}

在上述示例中,我们创建了一个Teacher对象teacher,并创建了两个Student对象student1和student2。然后,我们通过调用teacher的addStudent()函数,将student1和student2添加到teacher对象的students容器中。

这个例子展示了如何使用this指针在对象之间传递信息,实现对象之间的交互。通过this指针,我们可以方便地操作其他对象的成员函数和成员变量。

this指针和静态成员函数

静态成员函数中不能使用 this 指针!

因为静态成员函数并不具体作用与某个对象!

因此,静态成员函数的真实的参数的个数,就是程序中写出的参数个数!

在C++中,this指针和静态成员函数之间有一些区别。

  1. 静态成员函数没有this指针:由于静态成员函数不属于任何特定的对象,它们没有this指针。因此,在静态成员函数中不能使用this指针来访问对象的成员变量或其他非静态成员函数。
  2. 静态成员函数可以直接访问静态成员:静态成员函数只能访问静态成员变量和其他静态成员函数。静态成员是与类关联而不是与对象关联的,所以无需实例化对象即可访问。这是因为静态成员在编译时已经被分配了内存空间。
  3. 非静态成员函数可以访问this指针:非静态成员函数可以使用this指针来访问调用该函数的对象的成员变量和其他非静态成员函数。this指针是一个隐式传递给非静态成员函数的指针,它指向当前对象的地址。

需要注意的是,静态成员函数可以通过类名直接调用,而非静态成员函数必须通过对象来调用。另外,静态成员函数在全局作用域中也是可见的,可以通过类名来访问,而非静态成员函数则需要通过对象或对象指针来访问。

举个例子,假设有一个名为"Math"的类,其中包含一个静态成员变量PI和一个静态成员函数add(),以及一个非静态成员变量num和一个非静态成员函数multiply()。代码如下所示:

class Math {
    
    
public:
    static const double PI;
    static int add(int a, int b) {
    
    
        return a + b;
    }
    int num;
    int multiply(int a, int b) {
    
    
        return a * b;
    }
};
const double Math::PI = 3.14159;

在上述示例中,静态成员变量PI可以直接通过类名访问,如Math::PI。静态成员函数add()也可以通过类名直接调用,如int sum = Math::add(2, 3)
而非静态成员变量num和非静态成员函数multiply()需要通过对象来访问,如:

Math math;
math.num = 10;
int product = math.multiply(4, 5);

总结一下,this指针只能在非静态成员函数中使用,用于访问当前对象的成员变量和其他非静态成员函数。静态成员函数没有this指针,只能访问静态成员变量和其他静态成员函数。静态成员通过类名直接访问,而非静态成员需要通过对象来访问。

在C++中,静态成员是属于类本身而不是类的实例的成员。静态成员在所有类的对象之间是共享的,它们被存储在静态数据区中,并且在程序运行期间只有一份副本。

静态成员可以是静态成员变量或静态成员函数。

  1. 静态成员变量:静态成员变量是与类关联而不是与类的对象关联的。它们由类的所有对象共享,无论创建多少个对象,静态成员变量都只有一个副本。静态成员变量通常用于表示与类相关的全局属性或计数器等。定义静态成员变量时,需要在声明前加上static关键字,并在类外部初始化。
class MyClass {
    
    
public:
    static int count;
};
int MyClass::count = 0; // 静态成员变量的初始化
int main() {
    
    
    MyClass::count++; // 访问静态成员变量使用类名和作用域解析运算符
    return 0;
}
  1. 静态成员函数:静态成员函数是与类关联而不是与类的对象关联的函数。它们不操作特定对象的成员变量,也没有this指针。静态成员函数可以直接通过类名调用,无需实例化对象。静态成员函数通常用于执行与类相关的一般操作,而不依赖于特定对象。
class MyClass {
    
    
public:
    static void printMessage() {
    
    
        std::cout << "This is a static member function." << std::endl;
    }
};
int main() {
    
    
    MyClass::printMessage(); // 调用静态成员函数使用类名和作用域解析运算符
    return 0;
}

需要注意的是,静态成员变量和静态成员函数都属于类本身,而不是类的对象。因此,在静态成员函数中不能直接访问非静态成员变量或非静态成员函数,只能访问其他静态成员。

总结一下,静态成员是属于类本身的成员,与类的对象无关。静态成员变量由类的所有对象共享,并且在程序运行期间只有一个副本。静态成员函数不依赖于特定对象,可以通过类名直接调用。静态成员通常用于表示与类相关的全局属性或执行与类相关的一般操作。

静态成员

普通成员变量每个对象有各自的一份,而静态成员变量一共就一份,为所有对象共享。

普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象。

因此静态成员不需要通过对象就能访问。

在C++中,静态成员是属于类本身而不是类的实例的成员。静态成员在所有类的对象之间是共享的,它们被存储在静态数据区中,并且在程序运行期间只有一份副本。

静态成员可以是静态成员变量或静态成员函数。

  1. 静态成员变量:静态成员变量是与类关联而不是与类的对象关联的。它们由类的所有对象共享,无论创建多少个对象,静态成员变量都只有一个副本。静态成员变量通常用于表示与类相关的全局属性或计数器等。定义静态成员变量时,需要在声明前加上static关键字,并在类外部初始化。
class MyClass {
    
    
public:
    static int count;
};
int MyClass::count = 0; // 静态成员变量的初始化
int main() {
    
    
    MyClass::count++; // 访问静态成员变量使用类名和作用域解析运算符
    return 0;
}
  1. 静态成员函数:静态成员函数是与类关联而不是与类的对象关联的函数。它们不操作特定对象的成员变量,也没有this指针。静态成员函数可以直接通过类名调用,无需实例化对象。静态成员函数通常用于执行与类相关的一般操作,而不依赖于特定对象。
class MyClass {
    
    
public:
    static void printMessage() {
    
    
        std::cout << "This is a static member function." << std::endl;
    }
};
int main() {
    
    
    MyClass::printMessage(); // 调用静态成员函数使用类名和作用域解析运算符
    return 0;
}

需要注意的是,静态成员变量和静态成员函数都属于类本身,而不是类的对象。因此,在静态成员函数中不能直接访问非静态成员变量或非静态成员函数,只能访问其他静态成员。

总结一下,静态成员是属于类本身的成员,与类的对象无关。静态成员变量由类的所有对象共享,并且在程序运行期间只有一个副本。静态成员函数不依赖于特定对象,可以通过类名直接调用。静态成员通常用于表示与类相关的全局属性或执行与类相关的一般操作。

当使用静态成员时,有一些特性和注意事项需要了解:

  1. 静态成员变量的初始化:静态成员变量在类外部进行初始化,通常在源文件中进行。可以通过在类定义之外使用类名和作用域解析运算符来访问和初始化静态成员变量。
  2. 静态成员函数的调用:静态成员函数可以直接通过类名和作用域解析运算符来调用,无需实例化对象。它们不操作特定对象的成员变量,也没有this指针。因此,在静态成员函数内部不能直接访问非静态成员变量或非静态成员函数。
  3. 访问权限:静态成员可以具有公共、私有或受保护的访问权限修饰符,就像其他类成员一样。可以使用public、private或protected关键字来控制对静态成员的访问权限。
  4. 静态成员的作用域和生存周期:静态成员在类的整个生命周期都存在,并且在所有类的对象之间是共享的。静态成员的作用域限于其定义所在的类,可以通过类名和作用域解析运算符来访问。
  5. 静态成员的存储位置:静态成员变量在程序运行期间只有一个副本,它们被存储在静态数据区中。因此,无论创建多少个类的对象,静态成员变量始终保持相同的值。
  6. 静态成员和非静态成员之间的区别:静态成员属于类本身,而非静态成员属于类的对象。静态成员在所有对象之间共享,而非静态成员每个对象都有自己的副本。此外,静态成员函数没有this指针,无法访问非静态成员。

需要注意的是,静态成员的设计应该遵循一定的规则和原则。静态成员通常用于表示与类相关的全局属性或执行与类相关的一般操作,而不依赖于特定对象的状态。在使用静态成员时,应该明确其适用范围和目的,以保证代码的可读性和可维护性。

总结一下,静态成员是属于类本身的成员,与类的对象无关。静态成员变量在程序运行期间只有一个副本,静态成员函数可以直接通过类名调用。静态成员通常用于表示与类相关的全局属性或执行与类相关的一般操作。在使用静态成员时,应注意初始化、访问权限、作用域、生存周期等特性。

如何访问静态成员

如何访问静态成员

1) 类名::成员名
CRectangle::PrintTotal();
1) 对象名.成员名
CRectangle r; r.PrintTotal();
1) 指针->成员名
CRectangle * p = &r; p->PrintTotal();
1) 引用.成员名
CRectangle & ref = r; int n = ref.nTotalNumber; 

要访问静态成员变量和静态成员函数,可以使用类名和作用域解析运算符来进行访问。

  1. 访问静态成员变量:使用类名和作用域解析运算符::来访问静态成员变量。例如,如果有一个名为MyClass的类,并且该类包含一个静态成员变量staticVar,可以通过MyClass::staticVar来访问它。
class MyClass {
    
    
public:
    static int staticVar;
};
int MyClass::staticVar = 10; // 初始化静态成员变量
int main() {
    
    
    int value = MyClass::staticVar; // 访问静态成员变量
    return 0;
}
  1. 调用静态成员函数:同样地,使用类名和作用域解析运算符::来调用静态成员函数。例如,如果有一个名为MyClass的类,并且该类包含一个静态成员函数staticFunc,可以通过MyClass::staticFunc()来调用它。
class MyClass {
    
    
public:
    static void staticFunc() {
    
    
        // 静态成员函数的实现
    }
};
int main() {
    
    
    MyClass::staticFunc(); // 调用静态成员函数
    return 0;
}

需要注意的是,静态成员变量和静态成员函数在编译期间就已经存在,无需实例化对象即可访问和调用。它们不依赖于特定对象的状态,因此可以直接通过类名进行访问。

总结一下,要访问静态成员变量和调用静态成员函数,使用类名和作用域解析运算符::来进行访问和调用。静态成员变量和静态成员函数在编译期间就已经存在,并且无需实例化对象即可访问和调用。

除了使用类名和作用域解析运算符来访问静态成员,还有其他几种方式可以进行访问。

  1. 通过对象访问静态成员:虽然静态成员是与类关联而不是与对象关联的,但实际上也可以通过对象来访问静态成员。这是因为在编译器中,对象会自动转换为对应的类类型指针,从而可以使用指针来访问静态成员。
class MyClass {
    
    
public:
    static int staticVar;
};
int MyClass::staticVar = 10; // 初始化静态成员变量
int main() {
    
    
    MyClass obj;
    int value = obj.staticVar; // 通过对象访问静态成员变量
    return 0;
}

尽管可以通过对象访问静态成员,但这样做并不推荐,因为它可能会造成混淆,并且容易让人误以为静态成员是与对象相关联的。
2. 通过指针访问静态成员:与使用对象类似,可以使用指向类类型的指针来访问静态成员。这里需要注意的是,由于静态成员不依赖于特定对象的状态,可以直接通过类名来访问,无需通过指针间接访问。

class MyClass {
    
    
public:
    static int staticVar;
};
int MyClass::staticVar = 10; // 初始化静态成员变量
int main() {
    
    
    MyClass* ptr = nullptr;
    int value = ptr->staticVar; // 通过指针访问静态成员变量
    return 0;
}

需要注意的是,在使用对象或指针访问静态成员时,静态成员的访问权限仍然适用。如果静态成员是私有的,则无法通过对象或指针进行访问。

除了使用类名和作用域解析运算符来访问静态成员外,还可以通过对象或指针来访问。但这些方式并不推荐,因为静态成员是与类关联而不是与对象关联的。最佳实践是直接使用类名来访问静态成员,以确保清晰明确。

静态成员变量本质上是全局变量,哪怕一个对象都不存在,类的静态成员变量也存在。

静态成员函数本质上是全局函数。

设置静态成员这种机制的目的是将和某些类紧密相关的全局变量和函数写到类里面,看上去像一个整体,易于维护和理解。

注意事项

在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数。

void CRectangle::PrintTotal()
{
    
    
cout << w << "," << nTotalNumber << "," << 
nTotalArea << endl; //wrong
}

示例:

确实,你可以使用静态成员变量和静态成员函数来封装总数和总面积,并确保在整个程序中只有一个副本。
下面是一个示例代码:

class Rectangle {
    
    
private:
    int width;
    int height;
    static int totalCount;
    static int totalArea;
public:
    Rectangle(int w, int h) : width(w), height(h) {
    
    
        totalCount++;
        totalArea += width * height;
    }
    static int getTotalCount() {
    
    
        return totalCount;
    }
    static int getTotalArea() {
    
    
        return totalArea;
    }
};
int Rectangle::totalCount = 0;
int Rectangle::totalArea = 0;
int main() {
    
    
    Rectangle rect1(10, 20);
    Rectangle rect2(5, 15);
    int count = Rectangle::getTotalCount();
    int area = Rectangle::getTotalArea();
    return 0;
}

在上述示例中,我们定义了一个Rectangle类,其中包含私有的width和height成员变量,以及静态的totalCount和totalArea成员变量。构造函数会在创建每个矩形对象时自动增加totalCount,并将面积累加到totalArea中。通过静态成员函数getTotalCount()和getTotalArea(),我们可以获取整个程序中所有矩形的总数和总面积。

使用静态成员封装这些变量的好处是,它们与类相关联,并且在整个程序中只有一个副本。这样可以更容易地理解和维护代码,确保数据的一致性。

需要注意的是,静态成员变量和静态成员函数可以在类的定义中声明,在类外部初始化。并且它们不依赖于特定对象的状态,因此可以直接通过类名来访问。

总结一下,使用静态成员变量和静态成员函数可以封装总数和总面积,并确保在整个程序中只有一个副本。这样可以更容易地理解和维护代码,同时保持数据的一致性。

成员对象和封闭类

在C++中,成员对象和封闭类是一种关系,其中封闭类包含一个成员对象作为其成员之一。这种关系可以通过将另一个类的对象声明为封闭类的成员变量来实现。

通过使用成员对象,封闭类可以利用其他类提供的功能,并且可以访问成员对象的成员变量和成员函数。这样,封闭类可以将其他类的功能组合在一起,以实现更复杂的行为。

上例中,如果 CCar类不定义构造函数, 则下面的语句会编译出错:

CCar car;

因为编译器不明白 car.tyre该如何初始化。car.engine 的初始化没问题,用默认构造函数即可。

任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的。

具体的做法就是:通过封闭类的构造函数的初始化列表。

成员对象初始化列表中的参数可以是任意复杂的表达式,可以包括函数,变量,只要表达式中的函数或变量有定义就行。

下面是一个示例代码:

class Engine {
    
    
public:
    void start() {
    
    
        // 引擎启动逻辑
    }
};
class Car {
    
    
private:
    Engine engine;
public:
    void startCar() {
    
    
        engine.start();
        // 其他汽车启动逻辑
    }
};

在上述示例中,Car类包含一个Engine对象作为其成员变量。通过将Engine对象声明为Car类的成员变量,Car类可以使用Engine类提供的start()函数来启动引擎。在Car类的startCar()函数中,我们可以调用engine对象的start()函数来启动引擎,并执行其他与汽车启动相关的逻辑。

使用成员对象和封闭类的好处是可以实现代码的模块化和可重用性。封闭类可以通过成员对象来获取其他类的功能,并将其组合在一起,从而实现更高级的行为。

需要注意的是,在封闭类的构造函数中,成员对象的构造函数也会被调用。类似地,在封闭类的析构函数中,成员对象的析构函数也会被调用。这确保了成员对象在封闭类的生命周期内正确地进行构造和销毁。

总结一下,成员对象和封闭类是一种关系,其中封闭类包含其他类的对象作为其成员之一。这种关系允许封闭类利用其他类提供的功能,并通过组合来实现更复杂的行为。使用成员对象和封闭类可以实现代码的模块化和可重用性。

在C++中,成员对象和封闭类是一种关系,其中封闭类包含一个成员对象作为其成员之一。这种关系可以通过将另一个类的对象声明为封闭类的成员变量来实现。
通过使用成员对象,封闭类可以利用其他类提供的功能,并且可以访问成员对象的成员变量和成员函数。这样,封闭类可以将其他类的功能组合在一起,以实现更复杂的行为。
下面是一个示例代码:

class Engine {
    
    
public:
    void start() {
    
    
        // 引擎启动逻辑
    }
};
class Car {
    
    
private:
    Engine engine;
public:
    void startCar() {
    
    
        engine.start();
        // 其他汽车启动逻辑
    }
};

在上述示例中,Car类包含一个Engine对象作为其成员变量。通过将Engine对象声明为Car类的成员变量,Car类可以使用Engine类提供的start()函数来启动引擎。在Car类的startCar()函数中,我们可以调用engine对象的start()函数来启动引擎,并执行其他与汽车启动相关的逻辑。

使用成员对象和封闭类的好处是可以实现代码的模块化和可重用性。封闭类可以通过成员对象来获取其他类的功能,并将其组合在一起,从而实现更高级的行为。

在封闭类的构造函数中,成员对象的构造函数也会被调用。类似地,在封闭类的析构函数中,成员对象的析构函数也会被调用。这确保了成员对象在封闭类的生命周期内正确地进行构造和销毁。

成员对象和封闭类是一种关系,其中封闭类包含其他类的对象作为其成员之一。这种关系允许封闭类利用其他类提供的功能,并通过组合来实现更复杂的行为。使用成员对象和封闭类可以实现代码的模块化和可重用性。

封闭类构造函数和析构函数的执行顺序

封闭类对象生成时,先执行所有对象成员的构造函数,然后才执行封闭类的构造函数。

对象成员的构造函数调用次序和对象成员在类中的说明次序一致,与它们在成员初始化列表中出现的次序无关。

当封闭类的对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数。次序和构造函数的调用次序相反。

封闭类的复制构造函数用于创建一个新的对象,该对象是从现有的同类型对象进行复制而来。在复制构造函数中,通常需要对成员对象进行深拷贝,以确保新对象和原对象拥有独立的资源。

下面是一个示例代码:

class MyClass {
    
    
private:
    int* data;
public:
    MyClass(const MyClass& other) {
    
    
        // 执行深拷贝操作
        data = new int(*other.data);
    }
    // 其他成员函数和构造函数的实现
    ~MyClass() {
    
    
        delete data;
    }
};

在上述示例中,我们定义了一个名为MyClass的封闭类,并在其中包含一个动态分配的int类型数据成员data。在复制构造函数中,我们使用new关键字创建了一个新的int对象,并将其初始化为原对象中data指针指向的值的副本。这样可以确保新对象和原对象具有独立的资源。
需要注意的是,在复制构造函数中,还需要处理其他成员变量的复制,以确保新对象拥有与原对象相同的状态。

此外,当定义自定义的复制构造函数时,还应该考虑以下几点:

  1. 深拷贝 vs 浅拷贝:如果成员对象本身包含指针或动态分配的内存,复制构造函数应该执行深拷贝,即创建新的资源副本。如果成员对象是只读或不可变的,可以使用浅拷贝。
  2. 异常安全性:在执行深拷贝操作时,应保证异常安全性。即使在复制过程中抛出了异常,也要正确处理资源的释放,以防止内存泄漏或资源泄漏。
  3. 赋值运算符重载:除了复制构造函数外,还应该重载赋值运算符(=)以支持对象的赋值操作。赋值运算符重载函数的实现与复制构造函数类似。

总结一下,封闭类的复制构造函数用于创建一个新的对象,并以深拷贝的方式复制成员对象和资源。需要对每个成员变量进行适当的复制操作,以确保新对象和原对象具有独立的状态。同时,还应考虑异常安全性和赋值运算符重载的实现。

友元(friends)

友元分为友元函数和友元类两种

友元(friends)是C++中一种特殊的访问权限,它允许某个类或函数访问另一个类的私有成员。通过将一个类或函数声明为另一个类的友元,可以在友元类或函数中直接访问该类的私有成员。
下面是一个示例代码:

class MyClass {
    
    
private:
    int privateData;
public:
    MyClass() : privateData(0) {
    
    }
    friend class FriendClass;  // 声明FriendClass为MyClass的友元
    void printPrivateData() {
    
    
        std::cout << privateData << std::endl;
    }
};
class FriendClass {
    
    
public:
    void modifyPrivateData(MyClass& obj, int newData) {
    
    
        obj.privateData = newData; // 在FriendClass中直接访问MyClass的私有成员
    }
};
int main() {
    
    
    MyClass obj;
    FriendClass friendObj;
    
    friendObj.modifyPrivateData(obj, 42);
    obj.printPrivateData(); // 输出: 42
    
    return 0;
}

在上述示例中,我们声明了一个名为MyClass的类,并声明了一个名为FriendClass的类为其友元。在FriendClass中,我们可以直接访问MyClass的私有成员privateData,并进行修改。这得益于FriendClass被声明为MyClass的友元。

需要注意的是,友元关系是单向的,即如果类A声明类B为友元,那么类A的成员函数可以访问类B的私有成员,但类B的成员函数不能自动访问类A的私有成员。如果需要相互访问,需要进行相应的声明。

此外,友元关系是一种破坏封装性的机制,因此应该谨慎使用。合理使用友元可以提供对类的特定部分的访问权限,使得某些操作更方便,但也可能导致代码的可维护性和安全性降低。

总结一下,C++中的友元机制允许一个类或函数访问另一个类的私有成员。通过将一个类或函数声明为另一个类的友元,在友元类或函数中可以直接访问被授权类的私有成员。友元关系是单向的,并且应该谨慎使用,以避免破坏封装性和引入安全问题。

**友元关系可以分为两种类型:友元函数(friend function)和友元类(friend class)。

  1. 友元函数:友元函数是在一个类中声明的非成员函数,并且被声明为该类的友元。这意味着友元函数可以直接访问该类的私有成员。通过友元函数,可以将某个外部函数与类建立关联,以便在实现特殊操作或提供其他功能时访问类的私有成员。
class MyClass {
    
    
private:
    int privateData;
public:
    friend void friendFunction(MyClass& obj);  // 声明friendFunction为MyClass的友元
    // 其他成员函数和构造函数的实现
};
void friendFunction(MyClass& obj) {
    
    
    obj.privateData = 42;  // 在友元函数中直接访问MyClass的私有成员
}
  1. 友元类:友元类是在一个类中声明的另一个类,并且被声明为该类的友元。这意味着友元类的成员函数可以直接访问该类的私有成员。通过友元类,可以使得一个类能够访问另一个类的私有成员,从而实现更灵活的设计和组合。
class FriendClass {
    
    
public:
    void modifyPrivateData(MyClass& obj) {
    
    
        obj.privateData = 42;  // 在FriendClass中直接访问MyClass的私有成员
    }
};
class MyClass {
    
    
private:
    int privateData;
public:
    friend class FriendClass;  // 声明FriendClass为MyClass的友元
    // 其他成员函数和构造函数的实现
};

需要注意的是,友元关系应该被谨慎使用,以确保封装性和安全性。友元关系的目的是为了提供灵活性和特殊情况下的访问权限,但过度使用可能会导致代码不易理解和维护。在设计中,应仔细考虑是否真正需要友元关系,并根据具体需求和设计原则进行选择。

友元类之间的关系不能传递,不能继承。

当使用友元关系时,还需要注意以下几点:

  1. 友元的范围:友元关系是通过类而不是对象来建立的。一个类的所有对象都可以访问另一个类的私有成员,只要这个类在其声明中将另一个类声明为友元。
  2. 友元的传递性:如果类A声明类B为友元,同时类B声明类C为友元,那么类A的成员函数也可以访问类C的私有成员。这种传递性的友元关系使得在复杂的代码结构中,可以通过一系列的友元声明来实现灵活的访问权限。
  3. 函数作为友元:除了类之间可以建立友元关系外,函数也可以被声明为类的友元。这样,该函数就可以直接访问该类的私有成员。函数作为友元可以用于提供特定操作或算法所需的访问权限。
  4. 友元关系不具备继承性:友元关系不会被继承。即使派生类继承了基类的友元关系,它自己并不具备对基类私有成员的访问权限。
  5. 封装和信息隐藏:友元关系破坏了封装性,因此应谨慎使用。友元关系通常用于某些特殊的情况下,例如需要访问私有成员进行优化或实现特殊的操作。

需要明智地使用友元关系,以平衡代码的封装性和灵活性。友元机制可以提供对类的私有成员的访问权限,但也可能导致代码的可维护性和安全性降低。因此,应该仔细考虑并评估在特定情况下使用友元关系的利弊,并确保其使用符合设计原则和需求。

常量成员函数

如果不希望某个对象的值被改变,则定义该对象的时候可以在前面加 const关键字。

常量成员函数(const member function)是指在类中声明的成员函数,在其声明末尾添加const关键字。常量成员函数承诺不会修改对象的状态,因此它们不能修改类的非静态成员变量,也不能调用非常量成员函数(除非这些成员函数也被声明为常量成员函数)。
常量成员函数对于处理只读操作或者保护数据的完整性很有用,因为它们可以确保在使用常量对象或通过常量引用/指针访问对象时,不会意外地修改对象的状态。
下面是一个示例代码:

class MyClass {
    
    
private:
    int data;
public:
    int getValue() const {
    
    
        // 这是一个常量成员函数,不能修改成员变量
        return data;
    }
    void setValue(int value) {
    
    
        // 非常量成员函数可以修改成员变量
        data = value;
    }
};

在上述示例中,getValue()被声明为常量成员函数,因此它不能修改data成员变量的值。而setValue()是非常量成员函数,可以修改data成员变量。
当你有一个常量对象时,只能调用常量成员函数来访问其成员变量和执行操作。例如:

int main() {
    
    
    const MyClass obj;
    int value = obj.getValue();  // 可以调用常量成员函数
    // obj.setValue(10);  // 错误,常量对象无法调用非常量成员函数
    return 0;
}

需要注意的是,在常量成员函数中不能修改成员变量,也不能调用非常量成员函数,除非这些非常量成员函数也被声明为常量成员函数。

常量成员函数在类中声明的成员函数末尾添加const关键字。它们承诺不会修改对象的状态,因此对于只读操作或保护数据完整性很有用。常量成员函数可以在常量对象或通过常量引用/指针访问对象时调用,并且不能修改成员变量或调用非常量成员函数(除非这些成员函数也被声明为常量成员函数)。

当使用常量成员函数时,还需要注意以下几点:

  1. 常量对象调用:常量对象只能调用常量成员函数。这是因为常量对象被视为不可修改的,所以只能使用常量成员函数来访问对象的状态。
  2. 重载:常量成员函数和非常量成员函数可以进行重载。如果有两个成员函数,一个是常量成员函数,另一个是同名的非常量成员函数,它们可以根据调用对象的常量性来区分。
class MyClass {
    
    
public:
    int getValue() const {
    
    
        // 常量成员函数
        return 10;
    }
    int getValue() {
    
    
        // 非常量成员函数
        return 20;
    }
};
int main() {
    
    
    const MyClass obj1;
    MyClass obj2;
    int value1 = obj1.getValue();  // 调用常量成员函数
    int value2 = obj2.getValue();  // 调用非常量成员函数
    return 0;
}
  1. 返回类型:常量成员函数可以返回实际值,也可以返回常量引用或指针。如果返回一个非常量类型的实际值,它会被复制到调用者的副本中;如果返回常量引用或指针,则避免了无谓的复制。

  2. mutable关键字:在常量成员函数中,如果希望修改某些成员变量,可以使用mutable关键字来修饰这些成员变量。被mutable修饰的成员变量可以在常量成员函数中被修改。

class MyClass {
    
    
private:
    mutable int cacheValue;
public:
    int getValue() const {
    
    
        if (cacheValue == 0) {
    
    
            // 计算并缓存值
            cacheValue = calculateValue();
        }
        return cacheValue;
    }
    int calculateValue() const {
    
    
        // 计算值的逻辑
        return 42;
    }
};

在上述示例中,cacheValue成员变量被声明为mutable,因此可以在常量成员函数中更新它的值。
常量成员函数在保护对象状态的同时,也提供了对只读操作的方便访问。通过合理使用常量成员函数,可以增强代码的安全性和可靠性,并遵循面向对象设计的原则。

常量成员函数是指在类中声明的成员函数末尾添加const关键字。它们只能用于常量对象或通过常量引用/指针访问对象,并且不能修改成员变量或调用非常量成员函数(除非这些成员函数也被声明为常量成员函数)。常量成员函数可以进行重载,返回实际值、常量引用或指针,并且可以使用mutable关键字修饰某些成员变量以在常量成员函数中进行修改。

如果一个成员函数中没有调用非常量成员函数,也没有修改成员变量的值,那么,最好将其写成常量成员函数。

对C++中const的说明

在C++中,const是一个关键字,用于指定对象或变量是只读的,即不可修改。它可以应用于不同的上下文中,包括:

  1. 对象和变量声明:通过在变量或对象的声明前加上const关键字,可以将其标记为只读。这意味着一旦被初始化,就不能再修改该对象或变量的值。
const int x = 10; // 声明一个只读的整数常量x
const MyClass obj; // 声明一个只读的MyClass对象
  1. 函数参数:使用const关键字修饰函数参数,表示该参数在函数内部是只读的,在函数执行过程中不能被修改。
void print(const std::string& str) {
    
    
    // 该函数不能修改str的内容
    std::cout << str << std::endl;
}
  1. 成员函数:在成员函数后面添加const关键字,表示该成员函数是一个常量成员函数。常量成员函数承诺不会修改对象的状态,并且只能调用其他常量成员函数或访问类的只读成员变量。
class MyClass {
    
    
public:
    void foo() const {
    
    
        // 这是一个常量成员函数
        // 不能修改成员变量或调用非常量成员函数
    }
};
  1. 返回类型:const关键字也可以用于指定函数或操作符的返回类型是只读的。
const int calculateValue() {
    
    
    // 返回一个只读的整数值
    return 42;
}
const MyClass operator+(const MyClass& obj) const {
    
    
    // 返回一个只读的MyClass对象
    // 不能修改当前对象或调用非常量成员函数
}

const关键字对于增强代码的可读性、安全性和可维护性非常有帮助。它可以避免意外的修改,保护数据的完整性,并提供了更好的接口设计和封装性。

需要注意的是,使用const关键字并不意味着该对象或变量在内存中是只读的,而仅仅表示在代码中对其进行修改是不被允许的。

当使用const关键字时,还有一些细节和注意事项需要考虑:

  1. 可以重载非constconst成员函数:在同一个类中,可以同时定义一个非const版本和一个const版本的成员函数。这样,在调用对象为常量或非常量时,编译器会根据调用对象的常量性选择相应的成员函数。
class MyClass {
    
    
public:
    void foo() {
    
    
        // 非const 版本的成员函数
    }
    void foo() const {
    
    
        // const 版本的成员函数
    }
};
  1. 常量对象只能调用常量成员函数:常量对象只能调用常量成员函数,因为常量对象被视为只读对象,不允许修改其状态。但非常量对象可以调用常量成员函数和非常量成员函数。
void someFunction(const MyClass& obj) {
    
    
    obj.foo();  // 可以调用常量成员函数
    MyClass nonConstObj;
    nonConstObj.foo();  // 也可以调用非常量成员函数
}
  1. 返回类型是const的影响:如果函数返回类型是const,则返回的值通常不能被修改。
const int getValue() {
    
    
    return 42;  // 返回的值是只读的
}
int main() {
    
    
    const int value = getValue();
    // value = 10;  // 错误,value是只读的
    return 0;
}
  1. 指针和引用的const:当使用指针或引用时,const关键字可以应用于指针本身或指向的对象。这样可以限制对指针或引用的修改,或者限制被指向的对象的修改。
int x = 10;
const int* ptr = &x;  // 指向常量的指针,不能通过ptr修改x的值
int y = 20;
int* const ref = &y;  // 指向整数的常量指针,不能通过ref修改指针的指向
  1. mutable成员变量:mutable关键字可以用于修饰类的成员变量,它表示该成员变量可以在常量成员函数中被修改。
class MyClass {
    
    
private:
    mutable int count;
public:
    void increment() const {
    
    
        ++count;  // 在常量成员函数中可以修改mutable成员变量
    }
};

需要注意的是,const关键字应根据需要和语义正确地应用。它可以提高代码的可读性、安全性和可维护性,但也需要谨慎使用以避免过度使用。正确使用const关键字可以帮助捕捉编程错误、保护数据完整性,并提供更好的接口设计和封装性。

当使用const关键字时,还有一些概念和技巧需要了解:

  1. 保证线程安全性:在多线程环境中,常量对象的成员函数是线程安全的。由于常量对象的状态不会被修改,多个线程可以同时访问常量对象的成员函数而无需额外的同步机制。
  2. 常量性转换:常量性可以通过类型转换来进行转换。即可以将非常量对象转换为常量对象进行只读操作。这通过将对象引用或指针的类型从非常量改变为常量来实现。
void func(const MyClass& obj) {
    
    
    // 可以接受常量对象作为参数并进行只读操作
}
int main() {
    
    
    MyClass obj;
    const MyClass& constRef = obj;  // 将非常量对象转换为常量引用
    const MyClass* constPtr = &obj;  // 将非常量对象的地址转换为常量指针
    return 0;
}
  1. const和函数重载:常量性可以用作函数重载的条件之一。如果一个函数的参数是常量对象或常量引用,那么可以重载该函数以提供对常量对象的特殊处理。
class MyClass {
    
    
public:
    void process() {
    
    
        // 非const 版本的成员函数
    }
    void process() const {
    
    
        // const 版本的成员函数
    }
};
int main() {
    
    
    MyClass obj;
    const MyClass constObj;
    obj.process();      // 调用非const版本的process函数
    constObj.process(); // 调用const版本的process函数
    return 0;
}
  1. const修饰符位置:在函数声明中,const关键字可以放在成员函数的后面,也可以放在参数列表的后面。这两种形式的意义是相同的,但通常将const关键字放在函数后面更为常见。
class MyClass {
    
    
public:
    void process() const;   // const放在函数后面
    void update() const;    // const放在参数列表后面
};
void MyClass::process() const {
    
    
    // const成员函数的实现
}
void MyClass::update() const {
    
    
    // const成员函数的实现
}

需要根据具体情况正确使用const关键字。合理使用const可以增强代码的安全性、可读性和可维护性,并帮助捕捉编程错误。它提供了一种约束机制,用于指定只读操作和不会修改对象状态的函数,从而增加了代码的健壮性和可靠性。

常量成员函数的重载

在C++中,常量成员函数可以根据被调用对象的常量性进行重载。这意味着可以定义一个非常量版本和一个常量版本的成员函数,分别用于处理常量对象和非常量对象。
下面是一个示例代码:

class MyClass {
    
    
public:
    void foo() {
    
    
        // 处理非常量对象的逻辑
        std::cout << "Non-const version" << std::endl;
    }
    void foo() const {
    
    
        // 处理常量对象的逻辑
        std::cout << "Const version" << std::endl;
    }
};
int main() {
    
    
    MyClass obj1;
    const MyClass obj2;
    obj1.foo();  // 调用非常量版本的foo函数
    obj2.foo();  // 调用常量版本的foo函数
    return 0;
}

在上述示例中,MyClass类定义了两个名为foo()的成员函数,一个是非常量版本,另一个是常量版本。当调用非常量对象obj1foo()函数时,会调用非常量版本;而当调用常量对象obj2foo()函数时,会调用常量版本。

通过使用常量成员函数的重载,可以根据对象的常量性来选择合适的操作方式。这样可以保证对常量对象的只读操作以及非常量对象的修改操作都能得到正确的处理。

需要注意的是,常量成员函数的重载不仅与常量性有关,还与函数的参数类型和数量相关。因此,在进行函数重载时,需要确保函数的签名(包括参数类型、常量性等)是不同的。

总结一下,常量成员函数可以根据对象的常量性进行重载,以提供对常量对象和非常量对象的不同处理。通过合理使用常量成员函数的重载,可以保证对象在不同常量性下得到适当的操作,并增加代码的灵活性和可读性。

mutable成员变量(可以在const成员函数中修改的成员变量)

可以在const成员函数中修改的成员变量

在C++中,mutable关键字用于修饰类的成员变量,它表示该成员变量可以在常量成员函数中被修改,即使这些函数通常是不允许修改对象状态的。
下面是一个示例代码:

class MyClass {
    
    
private:
    mutable int count;
public:
    void increment() const {
    
    
        ++count;  // 在常量成员函数中可以修改mutable成员变量
    }
};

在上述示例中,count成员变量被声明为mutable,这意味着即使在常量成员函数(如increment())中,也可以对其进行修改。默认情况下,常量成员函数是不允许修改对象的状态的,但使用mutable关键字可以打破这个限制。
mutable关键字适用于那些在实现细节中需要跟踪或缓存信息的成员变量。例如,在某个类中计算并缓存某个值,而不希望每次调用时都重新计算,可以使用mutable来标记该成员变量。
需要注意以下几点:

  • mutable只能应用于非静态成员变量,因为静态成员变量是与类而不是对象相关联的。
  • mutable成员变量的修改仅限于同一对象内部,并不会影响其他对象的状态。
  • 尽管mutable允许在常量成员函数中修改变量,但仍应该谨慎使用。这是因为常量成员函数通常被认为是不会引起对象状态变化的,因此对于需要修改的情况,最好考虑其他可行的设计方案。

总结一下,mutable关键字用于修饰类的成员变量,表示该成员变量可以在常量成员函数中被修改。它在某些情况下提供了更灵活的设计选择,但也应该谨慎使用,以避免滥用导致代码逻辑混乱或违背设计原则。

运算符重载

在数学上,两个复数可以直接进行+、-等运算。但在C++中,直接将+或-用于复数对象是不允许的。
• 有时会希望,让对象也能通过运算符进行运算。这样代码更简洁,容易理解。
• 例如:complex_a和complex_b是两个复数对象;求两个复数的和, 希望能直接写:

complex_a + complex_b

在数学上,两个复数可以直接进行+、-等运算。但在C++中,直接将+或-用于复数对象是不允许的。
• 有时会希望,让对象也能通过运算符进行运算。这样代码更简洁,容易理解。
• 例如:complex_a和complex_b是两个复数对象;

求两个复数的和, 希望能直接写:

complex_a + complex_b

运算符重载(Operator Overloading)是一种特性,允许我们重新定义已有的运算符的行为,以适应自定义类型的操作。在编程语言中,运算符通常只能用于内置类型或标准库提供的类型,但通过运算符重载,我们可以为自定义类型赋予相应的运算符行为。
在大多数面向对象编程语言中,如C++、Python和Java,都支持运算符重载。不同的语言可能对运算符重载的实现方式有所区别。

以C++为例,通过在类中定义特殊的成员函数,可以实现对运算符的重载。例如,当我们创建一个名为"Vector"的自定义类时,可以重载"+"运算符来执行向量的加法操作:

class Vector {
    
    
public:
    int x, y;
    Vector(int x, int y) : x(x), y(y) {
    
    }
    Vector operator+(const Vector& v) {
    
    
        return Vector(x + v.x, y + v.y);
    }
};
int main() {
    
    
    Vector v1(1, 2);
    Vector v2(3, 4);
    Vector result = v1 + v2;
    // result.x = 1+3 = 4, result.y = 2+4 = 6
    return 0;
}

在上述示例中,通过重载"+“运算符,我们可以直接使用”+"操作符来对两个Vector对象进行加法运算。
需要注意的是,在进行运算符重载时,要遵循一些约定和规则,以确保正确的行为。这包括运算符的参数类型、返回类型、操作数数量等。

当进行运算符重载时,我们可以重载多个不同的运算符,并给它们赋予适合自定义类型的行为。以下是一些常见的运算符,可以在许多编程语言中进行重载:

  1. 算术运算符:例如 +、-、*、/ 等。通过重载这些运算符,我们可以定义自定义类型之间的加法、减法、乘法和除法操作。
  2. 关系运算符:例如 ==、!=、<、>、<=、>= 等。通过重载这些运算符,我们可以定义自定义类型之间的相等性、大小比较等关系操作。
  3. 赋值运算符:通常为 =。重载赋值运算符可以使自定义类型支持对象之间的赋值操作。
  4. 下标运算符:通常为 []。通过重载下标运算符,我们可以使自定义类型像数组一样使用下标来访问元素。
  5. 函数调用运算符:通常为 ()。重载函数调用运算符使得对象可以像函数一样被调用。
  6. 类型转换运算符:通过重载类型转换运算符,我们可以使对象能够隐式或显式地转换为其他类型。

要注意的是,在进行运算符重载时,应遵循一些约定和规则,以确保代码的清晰性和可读性。一些常见的指导原则包括:

  • 不要改变运算符的原始含义,以免引起混淆。
  • 遵循运算符的语义约定,例如加法运算符应进行加法操作。
  • 保持重载的运算符的行为与内置类型的一致性,以避免意外的行为。

此外,不同编程语言可能对运算符重载有自己的规则和限制。在使用时,建议查阅相关文档或参考示例代码,以确保正确地进行运算符重载。

总之,运算符重载使得我们可以为自定义类型赋予特定的运算符行为,提供更灵活、直观的代码编写方式,并增强代码的可读性和可维护性。

运算符重载的实质是函数重载

可以重载为普通函数,也可以重载为成员函数

把含运算符的表达式转换成对运算符函数的调用。

把运算符的操作数转换成运算符函数的参数。

运算符被多次重载时,根据实参的类型决定调用哪个运算符函数

运算符重载示例

下面是几个在C++中进行运算符重载的示例:

  1. 加法运算符重载:
#include <iostream>
class Vector {
    
    
public:
    int x, y;
    Vector(int x, int y) : x(x), y(y) {
    
    }
    Vector operator+(const Vector& v) {
    
    
        return Vector(x + v.x, y + v.y);
    }
};
int main() {
    
    
    Vector v1(1, 2);
    Vector v2(3, 4);
    Vector result = v1 + v2;
    std::cout << result.x << " " << result.y << std::endl;
    return 0;
}

输出结果:4 6
2. 关系运算符重载:

#include <iostream>
class Date {
    
    
public:
    int year, month, day;
    Date(int year, int month, int day) : year(year), month(month), day(day) {
    
    }
    bool operator==(const Date& other) {
    
    
        return (year == other.year && month == other.month && day == other.day);
    }
    bool operator!=(const Date& other) {
    
    
        return !(*this == other);
    }
};
int main() {
    
    
    Date d1(2022, 10, 1);
    Date d2(2022, 10, 1);
    
    if (d1 == d2) {
    
    
        std::cout << "Dates are equal" << std::endl;
    } else {
    
    
        std::cout << "Dates are not equal" << std::endl;
    }
    
    return 0;
}

输出结果:Dates are equal
3. 赋值运算符重载:

#include <iostream>
class Point {
    
    
public:
    int x, y;
    Point(int x, int y) : x(x), y(y) {
    
    }
    Point& operator=(const Point& other) {
    
    
        x = other.x;
        y = other.y;
        return *this;
    }
};
int main() {
    
    
    Point p1(1, 2);
    Point p2(3, 4);
    
    p1 = p2;
    
    std::cout << p1.x << " " << p1.y << std::endl;
    
    return 0;
}

输出结果:3 4

这些示例演示了如何重载不同类型的运算符,使自定义类型具有相应的行为。在实际使用中,可以根据需要进行更多类型的运算符重载,并根据具体情况来定义运算符的行为。请注意,上述代码只是示例,实际应用中可能需要添加错误处理、边界检查等额外的逻辑。

赋值运算符的重载

有时候希望赋值运算符两边的类型可以不匹配,比如,把一个int类型变量赋值给一个Complex对象,
或把一个 char * 类型的字符串赋值给一个字符串对象,此时就需要重载赋值运算符“=”。赋值运算符“=”只能重载为成员函数。

赋值运算符"="只能作为类的成员函数进行重载。在C++中,赋值运算符的重载函数必须是一个成员函数,并且没有返回类型。

如果想要实现不同类型之间的赋值操作,可以使用类型转换构造函数(或转换运算符)来实现类型的隐式转换。然后再通过重载赋值运算符来执行相应的赋值操作。

以下是一个示例,展示了将int类型的变量赋值给Complex对象的情况:

#include <iostream>
class Complex {
    
    
public:
    double real, imag;
    Complex(double real = 0.0, double imag = 0.0) : real(real), imag(imag) {
    
    }
    // 转换构造函数
    Complex(int value) : real(value), imag(0.0) {
    
    }
    // 赋值运算符重载
    Complex& operator=(const Complex& other) {
    
    
        if (this == &other) {
    
    
            return *this;
        }
        real = other.real;
        imag = other.imag;
        return *this;
    }
};
int main() {
    
    
    int intValue = 5;
    Complex complexObj;
    complexObj = intValue;  // 将int类型的变量赋值给Complex对象
    std::cout << complexObj.real << " + " << complexObj.imag << "i" << std::endl;
    return 0;
}

在上述示例中,通过定义Complex类的转换构造函数,我们可以将int类型的值隐式转换为Complex对象。然后,在赋值运算符重载函数中,我们可以将右侧的Complex对象的成员变量值赋给当前对象。

需要注意的是,在进行类型转换时,应该确保转换是合理和安全的,并且不会导致数据丢失或意外行为。同时,赋值运算符的行为应符合预期,确保正确处理各种边界情况。

总结起来,通过利用类型转换构造函数和赋值运算符重载,我们可以实现不同类型之间的赋值操作,提供更灵活的代码编写方式,以适应特定需求。

浅拷贝和深拷贝

浅拷贝和深拷贝是在对象复制过程中涉及的两个概念,用于描述如何复制对象及其数据。

  1. 浅拷贝(Shallow Copy):
    浅拷贝是指将一个对象的值复制到另一个对象,但仅复制对象本身的成员变量,而不复制动态分配的资源。这意味着,对于指向内存资源(如堆内存)的指针成员变量,仅复制指针的值,而不创建新的资源副本。因此,原始对象和副本对象将共享同一块内存。
    示例:
#include <iostream>
class MyString {
    
    
public:
    char* data;
    MyString(const char* str = nullptr) {
    
    
        if (str != nullptr) {
    
    
            int length = std::strlen(str);
            data = new char[length + 1];
            std::strcpy(data, str);
        } else {
    
    
            data = nullptr;
        }
    }
    // 拷贝构造函数(浅拷贝)
    MyString(const MyString& other) {
    
    
        data = other.data;  // 仅复制指针的值
    }
};
int main() {
    
    
    MyString original("Hello");
    MyString copy(original);  // 调用拷贝构造函数(浅拷贝)
    original.data[0] = 'X';  // 修改原始对象的数据
    std::cout << "Original: " << original.data << std::endl;  // 输出:Xello
    std::cout << "Copy: " << copy.data << std::endl;  // 输出:Xello(共享同一块内存)
    return 0;
}

在上述示例中,浅拷贝的拷贝构造函数仅复制指针的值,这意味着原始对象和副本对象将共享相同的data指针,即它们指向同一块内存。如果修改了其中一个对象的data数据,会影响另一个对象。
2. 深拷贝(Deep Copy):
深拷贝是指将一个对象及其相关资源复制到另一个对象,包括动态分配的内存资源。深拷贝创建了一个新的独立对象,其中包含与原始对象完全相同的数据,但是它们使用不同的内存空间。因此,对其中一个对象进行修改不会影响另一个对象。
示例:

#include <iostream>
class MyString {
    
    
public:
    char* data;
    MyString(const char* str = nullptr) {
    
    
        if (str != nullptr) {
    
    
            int length = std::strlen(str);
            data = new char[length + 1];
            std::strcpy(data, str);
        } else {
    
    
            data = nullptr;
        }
    }
    // 深拷贝的拷贝构造函数
    MyString(const MyString& other) {
    
    
        if (other.data != nullptr) {
    
    
            int length = std::strlen(other.data);
            data = new char[length + 1];
            std::strcpy(data, other.data);
        } else {
    
    
            data = nullptr;
        }
    }
    // 深拷贝的赋值运算符重载
    MyString& operator=(const MyString& other) {
    
    
        if (this == &other) {
    
      // 检查自我赋值
            return *this;
        }
        delete[] data;  // 释放原有资源
        if (other.data != nullptr) {
    
    
            int length = std::strlen(other.data);
            data = new char[length + 1];
            std::strcpy(data, other.data);
        } else {
    
    
            data = nullptr;
        }
        return *this;
    }
};
int main() {
    
    
    MyString original("Hello");
    MyString copy(original);  // 调用深拷贝的拷贝构造函数
    original.data[0] = 'X';  // 修改原始对象的数据
    std::cout << "Original: " << original.data << std::endl;  // 输出:Xello
    std::cout << "Copy: " << copy.data << std::endl;  // 输出:Hello(独立的内存空间)
    return 0;
}

在上述示例中,深拷贝的拷贝构造函数和赋值运算符重载会创建一个新的data数组,并将原始对象的数据复制到其中。这样,原始对象和副本对象将使用不同的内存空间,修改其中一个对象的data数据不会影响另一个对象。

总结:

浅拷贝只是简单地复制成员变量的值,包括指针的值,而不复制相关资源。深拷贝则复制了对象及其相关资源,创建了一个独立的新对象,避免了对象之间共享资源的问题。在设计类时,需要根据实际需求决定是使用浅拷贝还是深拷贝,并在拷贝构造函数和赋值运算符重载中进行相应的处理。

运算符重载为友元函数

一般情况下,将运算符重载为类的成员函数,是较好的选择。

但有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元。

在C++中,运算符重载既可以作为成员函数进行重载,也可以作为友元函数进行重载。通过将运算符重载声明为友元函数,我们可以访问类的私有成员变量,并且不需要通过类的对象来调用运算符。
下面是一个示例,展示了如何将加法运算符"+"重载为友元函数:

#include <iostream>
class Complex {
    
    
public:
    double real, imag;
    Complex(double real = 0.0, double imag = 0.0) : real(real), imag(imag) {
    
    }
    // 声明友元函数
    friend Complex operator+(const Complex& c1, const Complex& c2);
};
// 定义友元函数
Complex operator+(const Complex& c1, const Complex& c2) {
    
    
    return Complex(c1.real + c2.real, c1.imag + c2.imag);
}
int main() {
    
    
    Complex c1(1.0, 2.0);
    Complex c2(3.0, 4.0);
    
    Complex result = c1 + c2;  // 调用友元函数
    std::cout << "Real: " << result.real << ", Imaginary: " << result.imag << std::endl;
    
    return 0;
}

在上述示例中,我们将加法运算符"+"声明为Complex类的友元函数。这样,我们可以在友元函数中直接访问Complex类的私有成员变量real和imag,并执行相应的加法操作。在主函数中,我们通过调用友元函数来执行两个Complex对象的加法运算,并将结果存储在result对象中。

需要注意的是,友元函数声明应放在类的定义中,并且在类的外部定义实际的友元函数。这样可以确保友元函数能够访问类的私有成员变量。

总结起来,通过将运算符重载声明为友元函数,我们可以直接访问类的私有成员变量,并实现对自定义类型的运算符重载。友元函数提供了一种更灵活的方式来定义运算符重载,尤其在需要访问类的私有成员时非常有用。

运算符重载实例:可变长整型数组

下面是一个示例,展示了如何通过运算符重载创建一个可变长整型数组类,并实现对数组进行加法运算的功能:

#include <iostream>
#include <vector>
class VarIntArray {
    
    
private:
    std::vector<int> data;
public:
    VarIntArray() {
    
    }
    VarIntArray(std::initializer_list<int> list) : data(list) {
    
    }
    // 运算符重载:加法运算符
    VarIntArray operator+(const VarIntArray& other) const {
    
    
        VarIntArray result;
        size_t maxSize = std::max(data.size(), other.data.size());
        for (size_t i = 0; i < maxSize; i++) {
    
    
            int value1 = (i < data.size()) ? data[i] : 0;
            int value2 = (i < other.data.size()) ? other.data[i] : 0;
            result.data.push_back(value1 + value2);
        }
        return result;
    }
    void print() const {
    
    
        for (int num : data) {
    
    
            std::cout << num << " ";
        }
        std::cout << std::endl;
    }
};
int main() {
    
    
    VarIntArray arr1 = {
    
    1, 2, 3};
    VarIntArray arr2 = {
    
    4, 5, 6, 7};
    VarIntArray result = arr1 + arr2;
    arr1.print();       // 输出:1 2 3
    arr2.print();       // 输出:4 5 6 7
    result.print();     // 输出:5 7 9 7
    return 0;
}

在上述示例中,我们定义了一个VarIntArray类来表示可变长整型数组。通过使用std::vector来存储实际的数组数据。在构造函数中,我们使用了std::initializer_list来接受初始化列表,方便创建对象时传递初始值。

然后,我们重载了加法运算符"+",使得两个VarIntArray对象可以通过加法运算进行相加。在重载函数中,我们根据两个数组长度的较大值,遍历对应位置上的元素,将其相加并添加到结果数组中。

最后,在主函数中,我们创建了两个VarIntArray对象arr1和arr2,并使用加法运算符将它们相加,将结果存储在result对象中,然后分别输出三个对象的内容。

通过运算符重载,我们可以使自定义类型具有与内置类型类似的行为,提供更直观和灵活的操作方式。对于可变长数组这样的类,通过重载加法运算符,可以很方便地实现数组的合并操作。

流插入运算符流提取运算符的重载

流插入运算符和流提取运算符是C++中常用的运算符重载之一,它们分别用于将自定义类型的对象插入到输出流中和从输入流中提取对象。

  1. 流插入运算符"<<"
    流插入运算符重载函数通常返回一个std::ostream&类型,并接受两个参数:一个是要输出的流对象(如std::ostream),另一个是要插入到流中的自定义类型对象。在重载函数中,我们可以根据需要将自定义类型的成员变量以特定的格式插入到输出流中,并返回流对象本身。
    以下是一个示例,展示了如何重载流插入运算符:
#include <iostream>
class Point {
    
    
public:
    int x, y;
    Point(int x = 0, int y = 0) : x(x), y(y) {
    
    }
    // 流插入运算符重载
    friend std::ostream& operator<<(std::ostream& out, const Point& p);
};
// 定义流插入运算符重载函数
std::ostream& operator<<(std::ostream& out, const Point& p) {
    
    
    out << "(" << p.x << ", " << p.y << ")";
    return out;
}
int main() {
    
    
    Point p(3, 4);
    std::cout << "Point: " << p << std::endl;  // 使用流插入运算符输出自定义类型对象
    return 0;
}

在上述示例中,我们定义了一个Point类来表示二维坐标点。通过将流插入运算符重载声明为友元函数,我们可以在重载函数中直接访问Point类的私有成员变量,并将其以特定的格式插入到输出流中。
在主函数中,我们创建了一个Point对象p,并使用流插入运算符将其插入到std::cout输出流中,将结果打印到控制台上。
2. 流提取运算符">>"
流提取运算符重载函数通常返回一个std::istream&类型,并接受两个参数:一个是要输入的流对象(如std::istream),另一个是要从流中提取的自定义类型对象的引用。在重载函数中,我们可以根据需要从输入流中读取数据,并将其赋值给自定义类型对象的成员变量。
以下是一个示例,展示了如何重载流提取运算符:

#include <iostream>
class Point {
    
    
public:
    int x, y;
    Point(int x = 0, int y = 0) : x(x), y(y) {
    
    }
    // 流提取运算符重载
    friend std::istream& operator>>(std::istream& in, Point& p);
};
// 定义流提取运算符重载函数
std::istream& operator>>(std::istream& in, Point& p) {
    
    
    in >> p.x >> p.y;
    return in;
}
int main() {
    
    
    Point p;
    std::cout << "Enter coordinates (x y): ";
    std::cin >> p;  // 使用流提取运算符从输入流中提取自定义类型对象
    std::cout << "Point: (" << p.x << ", " << p.y << ")" << std::endl;
    return 0;
}

在上述示例中,我们同样定义了一个Point类来表示二维坐标点。通过将流提取运算符重载声明为友元函数,我们可以在重载函数中直接访问Point类的私有成员变量,并从输入流中读取数据并赋值给它们。
在主函数中,我们创建了一个Point对象p,并使用流提取运算符从std::cin输入流中提取数据,并将其赋值给p的成员变量。然后,我们将p的坐标打印到控制台上。

通过重载流插入运算符和流提取运算符,我们可以方便地将自定义类型对象插入到输出流中,并从输入流中提取数据赋值给对象的成员变量。这样,我们可以使用标准的输入输出方式与自定义类型对象进行交互。

类型转换运算符和自增、自减运算符的重载

类型转换运算符和自增、自减运算符是C++中常用的运算符重载之一,它们可以让我们更方便地进行类型转换和实现对象的自增、自减操作。

  1. 类型转换运算符
    类型转换运算符用于将一个类的对象转换为另一个类型。在C++中,类型转换运算符可以被重载为成员函数,并且没有返回类型。
    以下是一个示例,展示了如何重载类型转换运算符:
#include <iostream>
class MyString {
    
    
private:
    std::string data;
public:
    MyString(const std::string& str = "") : data(str) {
    
    }
    // 类型转换运算符重载:将MyString转换为std::string
    operator std::string() const {
    
    
        return data;
    }
};
int main() {
    
    
    MyString myStr("Hello");
    std::string str = static_cast<std::string>(myStr);  // 使用类型转换运算符将MyString转换为std::string
    std::cout << str << std::endl;  // 输出:Hello
    return 0;
}

在上述示例中,我们定义了一个MyString类来表示字符串对象。通过重载类型转换运算符,我们可以将MyString对象转换为std::string类型。在重载函数中,我们只需返回MyString对象的data成员变量即可。
在主函数中,我们创建了一个MyString对象myStr,并使用类型转换运算符将其转换为std::string类型,将结果存储在str对象中。然后,我们将str对象输出到控制台上。
2. 自增、自减运算符
自增和自减运算符用于对对象进行递增或递减操作。在C++中,自增和自减运算符可以被重载为成员函数,分别对应前缀形式和后缀形式。
以下是一个示例,展示了如何重载自增、自减运算符:

#include <iostream>
class Counter {
    
    
private:
    int count;
public:
    Counter(int value = 0) : count(value) {
    
    }
    // 前缀形式自增运算符重载
    Counter& operator++() {
    
    
        ++count;
        return *this;
    }
    // 后缀形式自增运算符重载
    Counter operator++(int) {
    
    
        Counter temp(*this);
        ++count;
        return temp;
    }
};
int main() {
    
    
    Counter counter(5);
    ++counter;  // 使用前缀形式自增运算符递增对象
    std::cout << "Count: " << counter.count << std::endl;  // 输出:Count: 6
    counter++;  // 使用后缀形式自增运算符递增对象
    std::cout << "Count: " << counter.count << std::endl;  // 输出:Count: 7
    return 0;
}

在上述示例中,我们定义了一个Counter类来表示计数器对象。通过重载自增运算符,我们可以实现对象的自增操作。在重载函数中,前缀形式自增运算符返回递增后的对象本身,而后缀形式自增运算符则返回递增前的对象的副本。

在主函数中,我们创建了一个Counter对象counter,并连续使用前缀和后缀形式的自增运算符对其进行递增操作。然后,我们将计数器的值输出到控制台上。

通过重载自增、自减运算符,我们可以方便地实现自定义类型对象的自增、自减操作,提供更直观的操作方式。需要注意的是,在重载自增、自减运算符时,应根据语义约定和常规用法来定义运算符的行为。

运算符重载的注意事项

  1. C++不允许定义新的运算符 ;

  2. 重载后运算符的含义应该符合日常习惯;

complex_a + complex_b
word_a > word_b
date_b = date_a + n
  1. 运算符重载不改变运算符的优先级;

  2. 以下运算符不能被重载:“.”、“.*”、“::”、“?:”、sizeof;

  3. 重载运算符()、[]、->或者赋值运算符=时,运算符重载函数必须声明为类的成员函数。

在进行运算符重载时,有一些重要的注意事项需要考虑:

  1. 符合语义:运算符重载应与其原始含义相符,遵循常规用法和语义约定。这样可以提高代码的可读性,并减少使用者的困惑。

  2. 类型匹配:运算符重载应适用于自定义类型,并与内置类型的行为保持一致。确保操作数的类型与预期一致,并考虑各种可能的组合和转换情况。

  3. 返回类型:运算符重载函数应返回适当的类型,以确保正确的结果。通常将返回值作为引用或常量引用,以避免不必要的副本构造。

  4. 顺序和优先级:运算符重载的行为和优先级应与其原始含义一致。如果需要改变优先级,请使用括号来明确表达意图。

  5. 自我赋值检查:对于赋值运算符等涉及到自我赋值的情况,应首先检查对象是否与自身相同。如果是,则直接返回当前对象的引用,以避免意外行为。

  6. 异常安全性:保证在运算符重载中处理异常,以确保程序的安全性和稳定性。在分配内存、访问资源等可能引发异常的操作中,应采取适当的异常处理措施。

  7. 常量成员函数:在某些情况下,运算符重载函数可能需要声明为常量成员函数。这样可以确保运算符的重载不会修改对象的状态。

  8. 一致性和预期行为:运算符重载的行为应与用户的预期一致,并遵循编程语言的规范和约定。提供适当的文档、注释和示例代码,以帮助其他开发者正确使用运算符重载。

在进行运算符重载时,建议查阅相关的文档和规范,并参考标准库中对于运算符的重载实现。此外,进行单元测试和边界条件的测试也是验证运算符重载行为正确性的重要手段。

总结起来,运算符重载是一项强大的特性,可以提高代码的可读性和灵活性。但要注意遵循语义约定、类型匹配、返回类型、自我赋值检查等原则,以确保正确且一致的行为。

继承和派生

在C++中,继承和派生是面向对象编程的两个重要概念,用于实现类与类之间的关系。
继承是指一个类可以从另一个类中继承属性和方法,并且可以在此基础上扩展出自己的属性和方法。被继承的类称为基类(父类),继承的类称为派生类(子类)。在C++中,可以通过以下方式定义一个派生类:

class DerivedClass : public BaseClass {
    // 派生类的成员变量和成员函数
};

在上面的示例中,DerivedClass是派生类,BaseClass是基类。关键字public表示使用公有继承,表示DerivedClass继承了BaseClass的所有public和protected成员,但不继承BaseClass的private成员。
派生类可以访问基类的public和protected成员,但不能访问基类的private成员。当派生类的成员变量或成员函数与基类的成员变量或成员函数同名时,可以使用作用域解析运算符::来指定调用哪个类的成员。
在派生类中,可以通过以下方式调用基类的构造函数:

class DerivedClass : public BaseClass {
public:
    DerivedClass(int x, int y, int z) : BaseClass(x, y), m_z(z) {}
private:
    int m_z;
};

在上面的示例中,调用了BaseClass的构造函数,并将参数x和y传递给它。

派生类中还可以重写(override)基类的成员函数,即在派生类中重新定义一个和基类相同名称、参数列表和返回类型的成员函数。在调用派生类的成员函数时,会优先调用派生类中的函数,如果派生类中没有定义相应的函数,则会调用基类的函数。

继承和派生是面向对象编程的重要概念,可以实现代码的复用和扩展。在使用继承和派生时,需要注意类之间的关系,避免出现循环继承等问题。同时,需要注意访问权限的控制,避免对private成员的直接访问。

需要继承机制的例子

继承机制可以实现代码的复用和扩展,下面是一些需要继承机制的例子:

  1. 图形类的继承
    在图形类中,可以定义一个基类Shape,包括图形的公共属性和方法,如颜色、位置、面积、周长等。然后可以通过继承机制定义各种具体的图形类,如矩形类、圆形类、三角形类等。这样,可以避免在每个具体的图形类中都定义公共的属性和方法,提高了代码的复用性。
  2. 汽车类的继承
    在汽车类中,可以定义一个基类Vehicle,包括汽车的公共属性和方法,如品牌、型号、颜色、速度、加速度等。然后可以通过继承机制定义各种具体的汽车类,如轿车类、越野车类、卡车类等。这样,可以避免在每个具体的汽车类中都定义公共的属性和方法,提高了代码的复用性。
  3. 员工类的继承
    在员工类中,可以定义一个基类Employee,包括员工的公共属性和方法,如姓名、性别、年龄、职位、工资等。然后可以通过继承机制定义各种具体的员工类,如经理类、销售员类、工人类等。这样,可以避免在每个具体的员工类中都定义公共的属性和方法,提高了代码的复用性。

继承机制可以极大地提高代码的复用性和可维护性,同时也可以实现代码的扩展。在使用继承机制时,需要注意类之间的关系,选择适当的继承方式,避免出现循环继承等问题。同时,需要注意访问权限的控制,避免对private成员的直接访问。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AO70jKCl-1688033782517)(2023-06-19-20-36-13.png)]

派生类的写法

在C++中,可以通过继承来创建派生类。派生类继承了基类的所有成员函数和成员变量,并且可以在此基础上扩展出自己的成员函数和成员变量。
派生类的定义方式如下:

// 基类
class BaseClass {
public:
    int m_varBase;
    void funcBase();
};
// 派生类
class DerivedClass : public BaseClass {
public:
    int m_varDerived;
    void funcDerived();
};

在上面的代码中,DerivedClass是派生类,BaseClass是基类。关键字public表示使用公有继承,表示DerivedClass继承了BaseClass的所有public和protected成员,但不继承BaseClass的private成员。
当派生类的成员变量或成员函数与基类的成员变量或成员函数同名时,可以使用作用域解析运算符::来指定调用哪个类的成员。例如,可以通过DerivedClass::m_varBase来访问基类中的成员变量m_varBase。
派生类中可以重写(override)基类的成员函数,即在派生类中重新定义一个和基类相同名称、参数列表和返回类型的成员函数。在调用派生类的成员函数时,会优先调用派生类中的函数,如果派生类中没有定义相应的函数,则会调用基类的函数。
在派生类中,可以通过以下方式调用基类的构造函数:

class DerivedClass : public BaseClass {
public:
    DerivedClass(int x, int y, int z) : BaseClass(x, y), m_varDerived(z) {}
private:
    int m_varDerived;
};

在上面的代码中,调用了BaseClass的构造函数,并将参数x和y传递给它。

继承和派生是面向对象编程的重要概念,可以实现代码的复用和扩展。在使用继承和派生时,需要注意类之间的关系,避免出现循环继承等问题。同时,需要注意访问权限的控制,避免对private成员的直接访问。

派生类对象的内存空间

派生类对象在内存中的空间由两部分组成:基类部分和派生类部分。
基类部分是从基类中继承而来的,派生类对象中包含了基类对象的完整副本。在内存中,基类对象的成员变量和成员函数的布局与基类定义的布局相同。
派生类部分是派生类自己定义的成员变量和成员函数,它们被添加到了基类对象的末尾。派生类的成员变量和成员函数的布局与派生类定义的布局相同。
由于派生类对象包含了基类对象的完整副本,因此可以通过派生类对象访问基类对象中定义的成员变量和成员函数。同时,由于基类部分和派生类部分在内存中是连续的,因此派生类对象可以强制转换为基类对象的指针或引用,并且可以在基类对象的范围内使用。
例如,假设有如下的基类和派生类:

class Base {
public:
    int m_varBase;
    void funcBase() {}
};
class Derived : public Base {
public:
    int m_varDerived;
    void funcDerived() {}
};

那么,派生类对象在内存中的布局如下图所示:

|<-- 基类部分 -->|<-- 派生类部分 -->|
| m_varBase       | m_varDerived      |
| funcBase()      | funcDerived()     |

可以看到,派生类对象中包含了基类对象的完整副本,基类部分和派生类部分在内存中是连续的。

需要注意的是,在派生类中访问基类成员时,需要使用作用域解析运算符::来指定基类成员的名称和访问权限,否则可能会产生二义性。例如,可以使用Base::m_varBase来访问基类中的成员变m_varBase。

继承和派生是面向对象编程的重要概念,可以实现代码的复用和扩展。在使用继承和派生时,需要注意类之间的关系,避免出现循环继承等问题。同时,需要注意访问权限的控制,避免对private成员的直接访问。

示例程序:学籍管理
下面是一个简单的学籍管理的实例程序,展示了如何使用继承来实现不同类型的学生对象。

#include <iostream>
#include <string>
using namespace std;
// 基类:学生
class Student {
public:
    Student(string name, int age, string gender)
        : m_name(name), m_age(age), m_gender(gender) {}
    void display() {
        cout << "姓名:" << m_name << endl;
        cout << "年龄:" << m_age << endl;
        cout << "性别:" << m_gender << endl;
    }
private:
    string m_name;
    int m_age;
    string m_gender;
};
// 派生类1:本科生
class Undergraduate : public Student {
public:
    Undergraduate(string name, int age, string gender, string major)
        : Student(name, age, gender), m_major(major) {}
    void display() {
        Student::display();
        cout << "专业:" << m_major << endl;
    }
private:
    string m_major;
};
// 派生类2:研究生
class Postgraduate : public Student {
public:
    Postgraduate(string name, int age, string gender, string research)
        : Student(name, age, gender), m_research(research) {}
    void display() {
        Student::display();
        cout << "研究方向:" << m_research << endl;
    }
private:
    string m_research;
};
int main() {
    Student s1("张三", 20, "男");
    s1.display();
    Undergraduate s2("李四", 22, "女", "计算机科学与技术");
    s2.display();
    Postgraduate s3("王五", 25, "男", "计算机视觉");
    s3.display();
    return 0;
}

在上面的代码中,Student是基类,包含了学生的姓名、年龄和性别信息。Undergraduate是派生类1,继承了Student的属性,并添加了专业信息。Postgraduate是派生类2,继承了Student的属性,并添加了研究方向信息。在每个派生类中,都重写了基类的display()函数,以便输出自己的属性。
在主函数中,创建了一个基类对象s1,一个Undergraduate对象s2和一个Postgraduate对象s3,分别输出了它们的属性。
继承可以实现代码的复用和扩展,使得程序更加灵活和可维护。在使用继承时,需要注意类之间的关系,选择适当的继承方式,避免出现循环继承等问题。同时,需要注意访问权限的控制,避免对private成员的直接访问。

继承关系&复合关系

继承关系和复合关系是面向对象编程中两种不同的关系。它们分别用于描述不同的对象之间的关系和组合方式。

继承关系是一种"is-a"的关系,用于描述一个类是另一个类的一种特殊形式。在继承关系中,子类继承了父类的属性和方法,并且可以在此基础上添加自己的属性和方法,从而实现代码的复用和扩展。例如,可以定义一个Animal类作为基类,然后定义Dog类和Cat类作为Animal类的子类,从而实现复用和扩展。

复合关系是一种"has-a"的关系,用于描述一个类包含另一个类的对象。在复合关系中,一个类实例化了另一个类的对象,并将其作为自己的成员变量使用。例如,可以定义一个Car类,包含了多个Wheel类对象,从而实现复杂的组合关系。

虽然继承关系和复合关系都可以用于实现代码的复用和扩展,但它们的应用场景不同。继承关系适用于描述"is-a"的关系,即一个类是另一个类的一种特殊形式;而复合关系适用于描述"has-a"的关系,即一个类包含另一个类的对象。

需要注意的是,在使用继承和复合时,需要考虑类之间的耦合性问题。继承关系会使得子类与父类之间产生紧密的耦合关系,一旦父类发生改变,子类也需要相应地进行修改。而复合关系则可以降低类之间的耦合度,使得类之间更加独立和灵活。

类之间的两种关系是继承关系和组合关系。
下面分别给出继承关系和组合关系的代码实例。
继承关系:

#include <iostream>
#include <string>
using namespace std;
// 基类:人
class Person {
public:
    Person(string name, int age) : m_name(name), m_age(age) {}
    void display() {
        cout << "姓名:" << m_name << endl;
        cout << "年龄:" << m_age << endl;
    }
private:
    string m_name;
    int m_age;
};
// 派生类:学生
class Student : public Person {
public:
    Student(string name, int age, string school) : Person(name, age), m_school(school) {}
    void display() {
        Person::display();
        cout << "学校:" << m_school << endl;
    }
private:
    string m_school;
};
int main() {
    Person p1("李四", 20);
    p1.display();
    Student s1("张三", 18, "清华大学");
    s1.display();
    return 0;
}

在上面的代码中,Person是基类,Student是派生类。Student继承了Person的属性和方法,并添加了自己的属性m_school。在Student中,重写了基类的display()函数,以便输出自己的属性。
组合关系:

#include <iostream>
#include <string>
using namespace std;
// 基类:轮胎
class Tyre {
public:
    Tyre(int size) : m_size(size) {}
    void display() {
        cout << "轮胎尺寸:" << m_size << endl;
    }
private:
    int m_size;
};
// 派生类:汽车
class Car {
public:
    Car(string brand, int size) : m_brand(brand), m_tyre(size) {}
    void display() {
        cout << "品牌:" << m_brand << endl;
        m_tyre.display();
    }
private:
    string m_brand;
    Tyre m_tyre;
};
int main() {
    Car c1("宝马", 18);
    c1.display();
    return 0;
}

在上面的代码中,Tyre是基类,Car是派生类。Car包含了一个Tyre对象m_tyre,从而实现了复杂的组合关系。在Car中,重写了自己的display()函数,以便输出自己的属性和包含的Tyre对象的属性。

需要注意的是,在使用继承和组合时,需要考虑类之间的耦合性问题。继承关系会使得子类与父类之间产生紧密的耦合关系,一旦父类发生改变,子类也需要相应地进行修改。而组合关系则可以降低类之间的耦合度,使得类之间更加独立和灵活。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jjdAoarO-1688033782518)(2023-06-19-20-43-47.png)]

复合关系的使用

复合关系是面向对象编程中的一种关系,指一个类对象包含了另一个类对象,用于描述对象之间的组合关系。

下面以一个简单的图形类作为例子,说明复合关系的使用。

#include <iostream>
#include <string>
using namespace std;
// 点类
class Point {
public:
    Point(int x, int y) : m_x(x), m_y(y) {}
private:
    int m_x;
    int m_y;
};
// 图形类
class Shape {
public:
    Shape(string type, int width, int height, int x, int y) 
        : m_type(type), m_width(width), m_height(height), m_point(x, y) {}
    void display() {
        cout << "图形类型:" << m_type << endl;
        cout << "宽度:" << m_width << endl;
        cout << "高度:" << m_height << endl;
        cout << "位置:" << m_point.m_x << ", " << m_point.m_y << endl;
    }
private:
    string m_type;
    int m_width;
    int m_height;
    Point m_point; // 复合关系
};
int main() {
    Shape s("矩形", 100, 50, 10, 20);
    s.display();
    return 0;
}

在上述代码中,Point类表示坐标点,Shape类表示图形类,包含了图形类型、宽度、高度和位置信息,其中位置信息通过复合关系包含了一个Point对象。在Shape类中,定义了display()函数以便输出各个属性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D0b6a6RI-1688033782518)(2023-06-19-20-46-31.png)]

复合关系的使用

 正确的写法:
为“狗”类设一个“业主”类的对象指针;
为“业主”类设一个“狗”类的对象指针数组。
class CMaster; //CMaster必须提前声明,不能先
//写CMaster类后写Cdog类
class CDog {
    
    
CMaster * pm;
};
class CMaster {
    
    
CDog * dogs[10];
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fIDja8ER-1688033782518)(2023-06-19-20-47-14.png)]

派生类覆盖基类成员

派生类可以定义一个和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员。要在派生类中访问由基类定义的同名成员时,要使用作用域符号::

在C++中,派生类可以覆盖(重写)基类的成员函数或成员变量,从而实现自己的功能。这种覆盖的方式也称为重载。
下面以一个简单的例子说明派生类如何覆盖基类的成员。

#include <iostream>
#include <string>
using namespace std;
// 基类:人
class Person {
public:
    Person(string name, int age) : m_name(name), m_age(age) {}
    virtual void display() { // 基类的display()函数可以被派生类覆盖,使用virtual关键字进行声明
        cout << "姓名:" << m_name << endl;
        cout << "年龄:" << m_age << endl;
    }
private:
    string m_name;
    int m_age;
};
// 派生类:学生
class Student : public Person {
public:
    Student(string name, int age, string school) : Person(name, age), m_school(school) {}
    void display() { // 派生类覆盖了基类的display()函数
        cout << "姓名:" << Person::m_name << endl;
        cout << "年龄:" << Person::m_age << endl;
        cout << "学校:" << m_school << endl;
    }
private:
    string m_school;
};
int main() {
    Person* p1 = new Person("李四", 20); // 基类指针指向基类对象
    p1->display();
    Person* p2 = new Student("张三", 18, "清华大学"); // 基类指针指向派生类对象
    p2->display();
    return 0;
}

在上述代码中,Person是基类,Student是派生类。Student覆盖了Person的display()函数,输出了自己的属性和基类的属性。在main()函数中,指针p1指向基类对象,指针p2指向派生类对象,分别调用了它们的display()函数。
需要注意的是,在使用派生类覆盖基类成员时,需要满足以下条件:

  1. 派生类中的函数名和基类中的函数名相同;
  2. 派生类和基类的函数参数列表相同;
  3. 派生类和基类的函数返回类型相同或者是类型兼容的(如派生类可以返回基类的指针);
  4. 派生类的访问级别不能低于基类的访问级别;
  5. 基类的成员函数必须使用virtual关键字进行声明,以便派生类可以覆盖它。

通过派生类覆盖基类成员,可以实现多态性,使得程序更加灵活和可扩展。在实际编程中,也经常会用到这种技术。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c58319Gu-1688033782519)(2023-06-19-20-48-26.png)]

类的保护成员

另一种存取权限说明符:protected
• 基类的private成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数
• 基类的public成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数
– 派生类的成员函数
– 派生类的友元函数
– 其他的函数
• 基类的protected成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数
– 派生类的成员函数可以访问当前对象和其它对象的基类的保护成员

在C++中,类的保护成员是指只能在类的成员函数内部和派生类的成员函数中访问,而不能在类外访问的成员。保护成员的访问级别介于公有成员和私有成员之间。
下面以一个简单的例子说明类的保护成员的使用。

#include <iostream>
#include <string>
using namespace std;
// 基类:动物
class Animal {
public:
    Animal(string name, string color) : m_name(name), m_color(color) {}
    void display() {
        cout << "名称:" << m_name << endl;
        cout << "颜色:" << m_color << endl;
        cout << "年龄:" << m_age << endl; // 可以在基类成员函数中访问保护成员
    }
protected:
    int m_age; // 保护成员
private:
    string m_name;
    string m_color;
};
// 派生类:狗
class Dog : public Animal {
public:
    Dog(string name, string color, int age) : Animal(name, color) {
        m_age = age; // 可以在派生类的成员函数中访问保护成员
    }
    void bark() {
        cout << "汪汪叫!" << endl;
    }
};
int main() {
    Dog d("旺财", "棕色", 3);
    d.display();
    d.bark();
    // cout << d.m_age << endl; // 保护成员不能在类外访问
    return 0;
}

在上述代码中,Animal是基类,Dog是派生类。基类Animal中定义了一个保护成员m_age,派生类Dog中通过继承可以访问到这个保护成员,因此可以在派生类的成员函数中对m_age进行赋值。在main()函数中,通过Dog的对象d调用了它们的成员函数。

需要注意的是,保护成员可以被派生类访问,但是不能被类外访问。如果在类外访问保护成员,编译器会报错。保护成员的作用在于,它可以在保证封装性的同时,让派生类能够访问基类的成员,从而实现更加灵活和可扩展的类设计。

派生类的构造函数

在C++中,派生类的构造函数可以调用基类的构造函数,以初始化基类的成员变量。派生类的构造函数可以有自己的参数列表,但是必须调用基类的构造函数来初始化基类的成员变量。如果派生类没有显式地调用基类的构造函数,则编译器会默认调用基类的默认构造函数。
下面以一个简单的例子说明派生类的构造函数的使用。

#include <iostream>
#include <string>
using namespace std;
// 基类:人
class Person {
public:
    Person(string name, int age) : m_name(name), m_age(age) {
        cout << "调用了Person的构造函数" << endl;
    }
    void display() {
        cout << "姓名:" << m_name << endl;
        cout << "年龄:" << m_age << endl;
    }
private:
    string m_name;
    int m_age;
};
// 派生类:学生
class Student : public Person {
public:
    Student(string name, int age, string school) : Person(name, age), m_school(school) {
        cout << "调用了Student的构造函数" << endl;
    }
    void display() {
        cout << "姓名:" << Person::m_name << endl;
        cout << "年龄:" << Person::m_age << endl;
        cout << "学校:" << m_school << endl;
    }
private:
    string m_school;
};
int main() {
    Student s("张三", 18, "清华大学");
    s.display();
    return 0;
}

在上述代码中,Person是基类,Student是派生类。Student的构造函数中调用了Person的构造函数,以初始化Person的成员变量。在main()函数中,创建了Student的对象s,并调用了它的成员函数。

需要注意的是,派生类的构造函数必须调用基类的构造函数,以初始化基类的成员变量。在派生类的构造函数内部,可以使用初始化列表来调用基类的构造函数,并对派生类的成员变量进行初始化。如果没有使用初始化列表,则编译器会默认调用基类的默认构造函数和派生类的默认构造函数(如果有的话)。在使用派生类的构造函数时,也需要考虑构造函数的重载、默认参数等问题。

在创建派生类的对象时,需要调用基类的构造函数:初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数之前,总是先执行基类的构造函数。

• 调用基类构造函数的两种方式

– 显式方式:在派生类的构造函数中,为基类的构造函数提供参数.
derived::derived(arg_derived-list):base(arg_base-list)
– 隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数则自动调用基类的默认构造函数.
• 派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数。

包含成员对象的派生类的构造函数写法

class Skill {
    
    
public:
Skill(int n) {
    
     }
};
class FlyBug: public Bug {
    
    
int nWings;
Skill sk1, sk2;
public:
FlyBug( int legs, int color, int wings);
};
FlyBug::FlyBug( int legs, int color, int wings):
Bug(legs,color),sk1(5),sk2(color) ,nWings(wings) {
    
    
}

当一个类包含成员对象时,派生类必须在其构造函数中显式地调用成员对象的构造函数来初始化它们。派生类的构造函数必须使用初始化列表来调用基类的构造函数和成员对象的构造函数。
下面以一个简单的例子说明包含成员对象的派生类的构造函数的写法。

#include <iostream>
#include <string>
using namespace std;
// 基类:人
class Person {
public:
    Person(string name, int age) : m_name(name), m_age(age) {
        cout << "调用了Person的构造函数" << endl;
    }
    void display() {
        cout << "姓名:" << m_name << endl;
        cout << "年龄:" << m_age << endl;
    }
private:
    string m_name;
    int m_age;
};
// 成员对象:地址
class Address {
public:
    Address(string province, string city) : m_province(province), m_city(city) {
        cout << "调用了Address的构造函数" << endl;
    }
    void display() {
        cout << "地址:" << m_province << "省" << m_city << "市" << endl;
    }
private:
    string m_province;
    string m_city;
};
// 派生类:学生
class Student : public Person {
public:
    Student(string name, int age, string school, string province, string city) : Person(name, age), m_school(school), m_address(province, city) {
        cout << "调用了Student的构造函数" << endl;
    }
    void display() {
        cout << "姓名:" << Person::m_name << endl;
        cout << "年龄:" << Person::m_age << endl;
        cout << "学校:" << m_school << endl;
        m_address.display();
    }
private:
    string m_school;
    Address m_address; // 包含成员对象
};
int main() {
    Student s("张三", 18, "清华大学", "北京", "海淀");
    s.display();
    return 0;
}

在上述代码中,Person是基类,Address是成员对象,Student是派生类。在Student的构造函数中,需要使用初始化列表来调用Person的构造函数和Address的构造函数,以初始化它们的成员变量。在main()函数中,创建了Student的对象s,并调用了它的成员函数。

需要注意的是,在派生类的构造函数中调用成员对象的构造函数,需要在初始化列表中使用成员对象的名称来进行调用,而不是使用构造函数的名称来进行调用。此外,也需要考虑构造函数的重载、默认参数等问题。

封闭派生类对象的构造函数的执行顺序

在创建派生类的对象时:

  1. 先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员;

  2. 再执行成员对象类的构造函数,用以初始化派生类对象中成员对象。

  3. 最后执行派生类自己的构造函数

在派生类对象消亡时:

  1. 先执行派生类自己的析构函数
  2. 再依次执行各成员对象类的析构函数
  3. 最后执行基类的析构函数
    析构函数的调用顺序与构造函数的调用顺序相反。

public继承的赋值兼容规则

class base {
    
     };
class derived : public base {
    
     };
base b;
derived d;

• 如果派生方式是 private或protected,则上述三条不可行。
在C++中,公有继承(public inheritance)的赋值兼容规则遵循“is-a”关系,即派生类对象可以赋值给基类对象,但是反过来基类对象不能赋值给派生类对象。
具体来说,如果一个基类指针或引用指向一个派生类对象,那么该指针或引用可以调用基类对象中的成员函数,但是不能调用派生类对象中新增的成员函数。如果需要调用派生类对象中新增的成员函数,可以使用动态类型识别(dynamic_cast)来实现。
下面以一个简单的例子说明公有继承的赋值兼容规则。

#include <iostream>
#include <string>
using namespace std;
// 基类:人
class Person {
public:
    Person(string name, int age) : m_name(name), m_age(age) {}
    void display() {
        cout << "姓名:" << m_name << endl;
        cout << "年龄:" << m_age << endl;
    }
private:
    string m_name;
    int m_age;
};
// 派生类:学生
class Student : public Person {
public:
    Student(string name, int age, string school) : Person(name, age), m_school(school) {}
    void display() {
        cout << "姓名:" << Person::m_name << endl;
        cout << "年龄:" << Person::m_age << endl;
        cout << "学校:" << m_school << endl;
    }
    void study() {
        cout << "学生正在学习" << endl;
    }
private:
    string m_school;
};
int main() {
    Person* p1 = new Student("张三", 18, "清华大学");
    p1->display(); // 调用基类成员函数
    // p1->study(); // 错误,无法调用派生类新增的成员函数
    Person& p2 = *(new Student("李四", 19, "北京大学"));
    p2.display(); // 调用基类成员函数
    // p2.study(); // 错误,无法调用派生类新增的成员函数
    return 0;
}

在上述代码中,Person是基类,Student是派生类。在main()函数中,使用基类指针p1和基类引用p2分别指向了一个派生类对象。通过p1和p2可以调用基类对象中的成员函数,但是不能调用派生类对象中新增的成员函数。

需要注意的是,公有继承的赋值兼容规则只适用于指针和引用,而不适用于对象。如果需要使用基类对象来初始化派生类对象,需要使用转换操作符(static_cast、dynamic_cast等)来进行类型转换。

直接基类和间接基类

在C++中,一个派生类可以从一个或多个基类中继承成员变量和成员函数。基类可以分为直接基类和间接基类两种类型。
直接基类是指在派生类的声明中明确指定的基类。例如:

class B { ... };
class D : public B { ... };

在上述代码中,B是D的直接基类。
间接基类是指在派生类的基类中递归地包含其他基类。例如:

class B { ... };
class C : public B { ... };
class D : public C { ... };

在上述代码中,C是D的直接基类,B是D的间接基类。因为D继承了C中的成员变量和成员函数,而C又继承了B中的成员变量和成员函数。
需要注意的是,派生类在继承基类时,只能通过直接基类来访问其成员函数和成员变量,不能通过间接基类来访问。如果需要访问间接基类中的成员函数和成员变量,可以通过直接基类的成员函数来调用。
下面以一个简单的例子说明直接基类和间接基类的概念。

#include <iostream>
using namespace std;
class A {
public:
    void f() {
        cout << "调用了A的f函数" << endl;
    }
};
class B : public A {
public:
    void g() {
        cout << "调用了B的g函数" << endl;
    }
};
class C : public B {
public:
    void h() {
        cout << "调用了C的h函数" << endl;
    }
};
int main() {
    C c;
    c.f(); // 调用直接基类A的成员函数
    c.g(); // 调用直接基类B的成员函数
    c.h(); // 调用自身的成员函数
    return 0;
}

在上述代码中,A是C的间接基类,B是C的直接基类。在main()函数中,创建了C的对象c,并调用了它的成员函数。可以看到,只有直接基类的成员函数和自身的成员函数可以被调用,间接基类的成员函数无法被直接调用。需要通过直接基类的成员函数来间接调用。

虚函数和多态

虚函数

在 C++ 中,虚函数(Virtual Function)是一种在基类中使用的特殊函数,它在基类中被声明为虚函数后,在派生类中也可以被重新定义。虚函数实现了多态特性,可以通过基类指针或引用以及动态绑定的方式,来访问派生类中的同名函数。
虚函数的定义格式如下:

class Base{
public:
    virtual void func() {
        // function body
    }
};

在上述代码中,func() 函数被声明为虚函数。在派生类中,可以重新定义该函数,实现多态。
当以基类的指针或引用调用虚函数时,程序会在运行时判断当前指针或引用所指向的对象的类型,然后动态地绑定该函数的调用地址。因此,当指针或引用指向派生类对象时,调用的就是派生类中的函数。
下面是一个简单的例子,演示了虚函数的用法:

#include <iostream>
using namespace std;
class Shape {
public:
    virtual float area() {
        cout << "Parent class area :" << endl;
        return 0;
    }
};
class Rectangle : public Shape {
public:
    float area() {
        cout << "Rectangle class area :" << endl;
        return (width * height);
    }
private:
    int width;
    int height;
public:
    Rectangle(int w, int h) {
        width = w;
        height = h;
    }
};
class Triangle : public Shape {
public:
    float area() {
        cout << "Triangle class area :" << endl;
        return (0.5 * base * height);
    }
private:
    int base;
    int height;
public:
    Triangle(int b, int h) {
        base = b;
        height = h;
    }
};
int main() {
    Shape* shape;
    Rectangle rec(10, 7);
    Triangle tri(10, 5);
    shape = &rec;
    shape->area();
    shape = &tri;
    shape->area();
}

在上述代码中,Shape 是基类,RectangleTriangle 是派生类。Shape 中的 area() 函数被声明为虚函数,因此可以在派生类中进行重载。在 main() 函数中,声明了一个 Shape 类型的指针变量 shape,通过它来访问派生类中的同名函数。当 shape 指向 Rectangle 对象时,调用的是 Rectangle 中的 area() 函数;当 shape 指向 Triangle 对象时,调用的是 Triangle 中的 area() 函数。

在类的定义中,前面有 virtual 关键字的成员函数就是虚函数。

class base {
    
    
virtual int get() ;
};
int base::get() 
{
    
     }virtual 关键字只用在类定义里的函数声明中,

写函数体时不用

多态的表现形式

在面向对象编程中,多态(Polymorphism)是指同一个函数或方法能够接受不同类型的参数或者返回不同类型的结果。多态是面向对象编程的三大特征之一(封装、继承、多态)。
在 C++ 中,多态的表现形式主要有以下两种:

  1. 重载函数
    在 C++ 中,函数重载(Function Overloading)也是一种多态的表现形式。同一个函数名可以被用于多个参数类型或者参数个数不同的函数,编译器会根据函数的参数类型和个数来决定调用哪个函数。例如:
void add(int a, int b) {
    cout << "调用的是int类型加法函数:" << a + b << endl;
}
void add(double a, double b) {
    cout << "调用的是double类型加法函数:" << a + b << endl;
}
int main() {
    add(1, 2); // 调用的是int类型加法函数:3
    add(1.5, 2.6); // 调用的是double类型加法函数:4.1
    return 0;
}

在上述代码中,add() 函数被重载了两次,分别针对 intdouble 类型的参数。在 main() 函数中,根据传入的参数类型,编译器会自动选择调用对应的函数。
2. 虚函数
前面已经介绍了虚函数的概念和用法,虚函数实现了运行时多态性。通过基类指针或引用以及动态绑定的方式,来访问派生类中的同名函数。
例如:

#include <iostream>
using namespace std;
class Shape {
public:
    virtual float area() {
        cout << "Parent class area :" << endl;
        return 0;
    }
};
class Rectangle : public Shape {
public:
    float area() {
        cout << "Rectangle class area :" << endl;
        return (width * height);
    }
private:
    int width;
    int height;
public:
    Rectangle(int w, int h) {
        width = w;
        height = h;
    }
};
class Triangle : public Shape {
public:
    float area() {
        cout << "Triangle class area :" << endl;
        return (0.5 * base * height);
    }
private:
    int base;
    int height;
public:
    Triangle(int b, int h) {
        base = b;
        height = h;
    }
};
int main() {
    Shape* shape;
    Rectangle rec(10, 7);
    Triangle tri(10, 5);
    shape = &rec;
    shape->area();
    shape = &tri;
    shape->area();
}

在上述代码中,Shape 是基类,RectangleTriangle 是派生类。Shape 中的 area() 函数被声明为虚函数,因此可以在派生类中进行重载。在 main() 函数中,声明了一个 Shape 类型的指针变量 shape,通过它来访问派生类中的同名函数。当 shape 指向 Rectangle 对象时,调用的是 Rectangle 中的 area() 函数;当 shape 指向 Triangle 对象时,调用的是 Triangle 中的 area() 函数。这就是运行时多态性的表现。

派生类的对象可以赋给基类引用

通过基类引用调用基类和派生类中的同名虚函数时:

(1)若该引用引用的是一个基类的对象,那么被调用是基类的虚函数;

(2)若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。这种机制也叫做“多态”。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x3SvH3WZ-1688033782520)(2023-06-20-20-11-19.png)]

多态的作用

在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。

使用多态的游戏程序实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QfNyOCXd-1688033782520)(2023-06-20-20-13-01.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pRRIqO9H-1688033782521)(2023-06-20-20-13-30.png)]

基本思路:

为每个怪物类编写 Attack、FightBack和 Hurted成员函数。

Attact函数表现攻击动作,攻击某个怪物,并调用被攻击怪物的

Hurted函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的 FightBack成员函数,遭受被攻击怪物反击。

Hurted函数减少自身生命值,并表现受伤动作。

FightBack成员函数表现反击动作,并调用被反击对象的Hurted成
员函数,使被反击对象受伤。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4vHr7mnE-1688033782521)(2023-06-20-20-14-16.png)]

非多态的实现方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zSV6IHlV-1688033782521)(2023-06-20-20-14-39.png)]

代码示例

下面是一个使用多态的游戏程序实例,该程序模拟了一个简单的 RPG 游戏,其中包含了不同类型的角色,每种角色都有自己的攻击力和防御力。具体实现如下:

#include <iostream>
#include <string>
using namespace std;
// 角色基类
class Character {
protected:
    string name;
    int attack;
    int defense;
public:
    Character(string name, int attack, int defense) {
        this->name = name;
        this->attack = attack;
        this->defense = defense;
    }
    virtual int getAttack() { return attack; } // 获取攻击力
    virtual int getDefense() { return defense; } // 获取防御力
    virtual void attackTarget(Character* target) {} // 攻击目标
};
// 具体角色类:战士
class Warrior : public Character {
public:
    Warrior(string name, int attack, int defense) : Character(name, attack, defense) {}
    int getAttack() { return attack * 2; } // 攻击力加倍
    void attackTarget(Character* target) {
        int damage = getAttack() - target->getDefense();
        damage = max(damage, 0);
        cout << name << " 对 " << target->name << " 造成了 " << damage << " 点伤害!" << endl;
    }
};
// 具体角色类:法师
class Mage : public Character {
public:
    Mage(string name, int attack, int defense) : Character(name, attack, defense) {}
    int getDefense() { return defense / 2; } // 防御力减半
    void attackTarget(Character* target) {
        int damage = getAttack() - target->getDefense();
        damage = max(damage, 0);
        cout << name << " 对 " << target->name << " 造成了 " << damage << " 点伤害!" << endl;
    }
};
// 游戏主程序
int main() {
    Warrior warrior("战士", 50, 30);
    Mage mage("法师", 40, 40);
    Character* player = &warrior;
    Character* enemy = &mage;
    cout << player->name << " 攻击 " << enemy->name << ":" << endl;
    player->attackTarget(enemy);
    cout << enemy->name << " 攻击 " << player->name << ":" << endl;
    enemy->attackTarget(player);
    return 0;
}

在上述代码中,角色基类 Character 中定义了角色的基本属性和行为,其中 getAttack()getDefense() 函数是虚函数,分别获取角色的攻击力和防御力。具体的角色类 WarriorMage 继承自角色基类 Character,并分别重载了虚函数 getAttack()getDefense(),以及定义了攻击目标的具体行为。

在游戏主程序中,先创建了一个战士和一个法师角色,然后通过基类指针来访问角色的属性和行为。当战士攻击法师时,调用的是战士类中的 attackTarget() 函数,而当法师攻击战士时,调用的是法师类中的 attackTarget() 函数。由于这两个函数都是虚函数,并且被重新定义了,所以实际上调用的是派生类中的函数,实现了运行时多态性。

!!!更多多态程序实例!!!

几何形体处理程序

几何形体处理程序: 输入若干个几何形体的参数,

要求按面积排序输出。输出时要指明形状。

Input:

第一行是几何形体数目n(不超过100).下面有n行,每行以一个字母c开头.

若 c 是 ‘R’,则代表一个矩形,本行后面跟着两个整数,分别是矩形的宽和高;

若 c 是 ‘C’,则代表一个圆,本行后面跟着一个整数代表其半径

若 c 是 ‘T’,则代表一个三角形,本行后面跟着三个整数,代表三条边的长度

Output:

按面积从小到大依次输出每个几何形体的种类及面积。每行一个几何形体,输出格式为:

形体名称:面积

课本代码示例

#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class CShape
{
    
    
public:
virtual double Area() = 0; //纯虚函数
virtual void PrintInfo() = 0;
}; 
class CRectangle:public CShape
{
    
    
public:
int w,h; 
virtual double Area();
virtual void PrintInfo();
};
class CCircle:public CShape {
    
    
public:
int r; 
virtual double Area();
virtual void PrintInfo();
};
class CTriangle:public CShape {
    
    
public:
int a,b,c; 
virtual double Area();
virtual void PrintInfo();
}; 
double CRectangle::Area() {
    
     
return w * h; 
}
void CRectangle::PrintInfo() {
    
    
cout << "Rectangle:" << Area() << endl;
}
double CCircle::Area() {
    
    
return 3.14 * r * r ;
}
void CCircle::PrintInfo() {
    
    
cout << "Circle:" << Area() << endl;
}
double CTriangle::Area() {
    
    
double p = ( a + b + c) / 2.0;
return sqrt(p * ( p - a)*(p- b)*(p - c));
}
void CTriangle::PrintInfo() {
    
    
cout << "Triangle:" << Area() << endl; 
}
CShape * pShapes[100];
int MyCompare(const void * s1, const void * s2);
int main()
{
    
     
int i; int n;
CRectangle * pr; CCircle * pc; CTriangle * pt;
cin >> n;
for( i = 0;i < n;i ++ ) {
    
    
char c;
cin >> c;
switch(c) {
    
    
case 'R':
pr = new CRectangle();
cin >> pr->w >> pr->h;
pShapes[i] = pr; 
break; 
case 'C':
pc = new CCircle();
cin >> pc->r;
pShapes[i] = pc;
break;
case 'T':
pt = new CTriangle();
cin >> pt->a >> pt->b >> pt->c;
pShapes[i] = pt; 
break;
} 
}
qsort(pShapes,n,sizeof( CShape*),MyCompare);
for( i = 0;i <n;i ++)
pShapes[i]->PrintInfo(); 
return 0;
}
int MyCompare(const void * s1, const void * s2)
{
    
    
double a1,a2;
CShape * * p1 ; // s1,s2 是 void * ,不可写 “* s1”来取得s1指向的内容
CShape * * p2;
p1 = ( CShape * * ) s1; //s1,s2指向pShapes数组中的元素,数组元素的类型是CShape *
p2 = ( CShape * * ) s2; // 故 p1,p2都是指向指针的指针,类型为 CShape ** 
a1 = (*p1)->Area(); // * p1 的类型是 Cshape * ,是基类指针,故此句为多态
a2 = (*p2)->Area();
if( a1 < a2 ) 
return -1;
else if ( a2 < a1 )
return 1;
else
return 0;
} 
case 'C':
pc = new CCircle();
cin >> pc->r;
pShapes[i] = pc;
break;
case 'T':
pt = new CTriangle();
cin >> pt->a >> pt->b >> pt->c;
pShapes[i] = pt; 
break;
} 
}
qsort(pShapes,n,sizeof( CShape*),MyCompare);
for( i = 0;i <n;i ++)
pShapes[i]->PrintInfo(); 
return 0;
}

如果添加新的几何形体,比如五边形,则只需要从CShape派生出CPentagon,以及在main中switch语句中增加一个case,其余部分不变有木有!

用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组,就能对各个派生类对象做各种操作,是很常用的做法

下面是一个利用 C++ 实现的几何形体处理程序,可以输入若干个几何形体的参数,并按照面积排序输出各个几何形体的种类及面积:
```c++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const double pi = 3.14159265358979323846;
// 几何形体基类
class Shape {
    
    
public:
    virtual double getArea() = 0; // 获取面积
    virtual string getName() = 0; // 获取名称
};
// 具体几何形体类:矩形
class Rectangle : public Shape {
    
    
private:
    double width;
    double height;
public:
    Rectangle(double w, double h) {
    
    
        width = w;
        height = h;
    }
    double getArea() {
    
     return width * height; }
    string getName() {
    
     return "矩形"; }
};
// 具体几何形体类:圆形
class Circle : public Shape {
    
    
private:
    double radius;
public:
    Circle(double r) {
    
     radius = r; }
    double getArea() {
    
     return pi * radius * radius; }
    string getName() {
    
     return "圆形"; }
};
// 具体几何形体类:三角形
class Triangle : public Shape {
    
    
private:
    double a, b, c;
public:
    Triangle(double aa, double bb, double cc) {
    
    
        a = aa;
        b = bb;
        c = cc;
    }
    double getArea() {
    
    
        double p = (a + b + c) / 2;
        return sqrt(p * (p - a) * (p - b) * (p - c));
    }
    string getName() {
    
     return "三角形"; }
};
// 比较函数,用于排序
bool cmp(Shape* s1, Shape* s2) {
    
     return s1->getArea() < s2->getArea(); }
// 主程序
int main() {
    
    
    int n;
    cin >> n;
    vector<Shape*> shapes;
    for (int i = 0; i < n; i++) {
    
    
        char c;
        cin >> c;
        if (c == 'R') {
    
    
            double w, h;
            cin >> w >> h;
            shapes.push_back(new Rectangle(w, h));
        }
        else if (c == 'C') {
    
    
            double r;
            cin >> r;
            shapes.push_back(new Circle(r));
        }
        else if (c == 'T') {
    
    
            double a, b, c;
            cin >> a >> b >> c;
            shapes.push_back(new Triangle(a, b, c));
        }
    }
    sort(shapes.begin(), shapes.end(), cmp);
    for (int i = 0; i < n; i++) {
    
    
        cout << shapes[i]->getName() << ":" << shapes[i]->getArea() << endl;
        delete shapes[i];
    }
    return 0;
}
```
在上述代码中,几何形体基类 `Shape` 定义了接口函数 `getArea()` 和 `getName()`,分别用于获取几何形体的面积和名称。具体的几何形体类 `Rectangle`、`Circle` 和 `Triangle` 继承自 `Shape`,并实现了这两个接口函数。

在主程序中,首先输入几何形体的数目 `n`,然后根据每个几何形体的名称和参数创建相应的对象,并添加到 `shapes` 向量中。最后利用 `sort()` 函数和比较函数 `cmp()` 对 `shapes` 向量进行排序,按照面积从小到大排序。最后遍历 `shapes` 向量,输出每个几何形体的名称和面积,并释放相应的内存。

构造函数和析构函数中调用虚函数

在构造函数和析构函数中调用虚函数,不是多态。编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数

在构造函数和析构函数中调用虚函数是一种危险的做法,因为虚函数的调用是运行时决定的,而在构造函数和析构函数中,对象的状态可能还没有完全初始化或已经被销毁,此时调用虚函数可能会产生未定义的行为。

具体来说,在构造函数中调用虚函数可能会导致以下问题:

  1. 对象可能还没有完全初始化,此时调用虚函数可能会访问到未初始化的成员变量或无效的指针,导致程序崩溃或产生未定义的行为。
  2. 对象的动态类型可能还没有被确定,此时调用虚函数会默认使用基类的实现,而不是派生类的实现,导致程序产生错误的行为。
    在析构函数中调用虚函数可能会导致以下问题:
  3. 对象的动态类型可能已经被销毁,此时调用虚函数会导致程序崩溃或产生未定义的行为。
  4. 调用虚函数可能会触发虚函数表的查找和调用,而在对象被销毁的过程中,虚函数表和虚函数指针可能已经被销毁,导致程序出现未定义的行为。

因此,在构造函数和析构函数中应该尽量避免调用虚函数,或者采用其他解决方案,如在构造函数中使用初始化列表初始化成员变量,或者在析构函数中只进行简单的资源释放操作。如果确实需要在构造函数或析构函数中调用虚函数,应该尽可能保证对象的状态已经完全初始化或还没有被销毁,并且应该注意虚函数的实现是否能够正确地处理这种情况。

多态实现的关键 — 虚函数表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-24r4yzri-1688033782521)(2023-06-20-20-22-42.png)]
多态是面向对象编程的重要特性之一,其中的关键就是通过运行时绑定来实现函数调用的动态分派。在 C++ 中,实现多态的一种常见方式是通过虚函数来实现,其中的关键就是需要使用虚函数表(vtable)来存储类的虚函数指针,从而实现动态绑定。

虚函数表是一个由编译器自动生成的静态变量,用于存储类的虚函数指针。对于每个含有虚函数的类,编译器都会在编译时为其生成一个虚函数表,并将其作为类的元数据存储在程序的数据段中。虚函数表中的每个条目都是一个指向虚函数的指针,其中的顺序和声明顺序相同。因此,当类的对象被创建时,虚函数表的地址会被存储在对象的头部,成为对象的虚函数指针(vptr)。

当使用基类类型的指针或引用调用虚函数时,会根据对象的实际类型来查找虚函数表,并通过虚函数指针调用相应的虚函数。具体来说,编译器会在对象的虚函数指针(vptr)所指向的虚函数表中查找相应的虚函数,然后通过函数指针调用相应的函数。因此,由于虚函数的调用是在运行时动态决定的,因此可以实现多态的目的。

需要注意的是,在使用虚函数时,需要满足以下条件:

  1. 虚函数必须是类的成员函数。
  2. 虚函数必须在基类中被声明为虚函数,并在派生类中被覆盖(override)或实现(implement)。
  3. 虚函数的参数和返回类型必须与基类中的虚函数完全相同。
  4. 虚函数表的大小和顺序必须与类的继承关系相对应。
  5. 虚函数的调用必须通过基类类型的指针或引用进行,才能触发动态绑定。

通过虚函数表的实现,C++ 实现了多态的功能,使得程序具有更加灵活和可扩展的特性。但需要注意的是,虚函数表的实现也增加了程序的开销和复杂性,因此在设计和实现程序时需要谨慎考虑。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jivpN9CP-1688033782522)(2023-06-20-20-24-16.png)]

虚析构函数

通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数

但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。

解决办法:把基类的析构函数声明为virtual 派生类的析构函数可以virtual不进行声明

通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数

一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义成虚函数。

注意:不允许以虚函数作为构造函数

虚析构函数是指在析构函数前加上关键字 “virtual”,用于实现多态的析构函数。
当使用基类的指针或引用来删除一个派生类的对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。这样就会导致派生类中分配的内存资源没有被正确释放,从而导致内存泄漏的问题。
而通过将基类的析构函数声明为虚函数,可以实现在析构对象时动态绑定,调用派生类的析构函数。这样就能够正确地释放派生类中分配的内存资源,避免内存泄漏的问题。
例如:

class Base {
public:
    virtual ~Base() { // 声明为虚函数
        std::cout << "Base::~Base()" << std::endl;
    }
};
class Derived : public Base {
public:
    ~Derived() override { // 实现派生类的析构函数
        std::cout << "Derived::~Derived()" << std::endl;
    }
};
int main() {
    Base *p = new Derived();
    delete p; // 动态绑定,调用派生类的析构函数
    return 0;
}

在上述代码中,基类的析构函数被声明为虚函数,派生类的析构函数被实现并重写了基类的虚析构函数。在删除派生类对象时,通过基类的指针进行删除,并利用动态绑定机制实现了对派生类析构函数的调用。

虚析构函数是为了解决基类指针删除派生类对象时可能出现的内存泄漏问题,是实现多态的关键。在设计和实现对象继承体系时,应该尽可能地将析构函数声明为虚函数。

需要注意的是,如果派生类没有显式地提供析构函数,那么编译器会自动生成一个默认的析构函数。这个默认的析构函数也会被声明为虚函数,因为它继承了基类的虚析构函数。
另外,需要注意的是,虚析构函数只适用于基类指针或引用删除派生类对象时的情况。如果使用对象的直接名字进行删除,那么只会调用对象自身的析构函数,而不会触发动态绑定,也不会调用派生类的析构函数。
例如:

class Base {
public:
    virtual ~Base() { // 声明为虚函数
        std::cout << "Base::~Base()" << std::endl;
    }
};
class Derived : public Base {
public:
    ~Derived() override { // 实现派生类的析构函数
        std::cout << "Derived::~Derived()" << std::endl;
    }
};
int main() {
    Derived d;
    Base *p = &d;
    p->~Base(); // 不会触发动态绑定,只会调用 Base 的析构函数
    return 0;
}

在上述代码中,对象 d 的析构函数是派生类 Derived 的析构函数,在删除对象 d 时会被自动调用。但是,如果使用基类指针直接调用对象的析构函数,不会触发动态绑定,只会调用基类的析构函数,从而导致派生类的析构函数没有被正确调用。

因此,需要在使用虚析构函数时,注意使用基类指针或引用来删除派生类对象,以确保正确调用派生类的析构函数,避免内存泄漏的问题。

此外,需要注意的是,如果派生类的析构函数需要进行特殊的资源释放操作,例如释放动态分配的内存或关闭文件等,那么必须在派生类的析构函数中显式地调用基类的虚析构函数。这样才能保证在析构派生类对象时,先调用派生类的析构函数,再调用基类的析构函数,从而避免资源泄漏的问题。
例如:

class Base {
public:
    virtual ~Base() {
        std::cout << "Base::~Base()" << std::endl;
    }
};
class Derived : public Base {
public:
    Derived() {
        data = new int[10];
    }
    ~Derived() override {
        delete[] data; // 释放动态分配的内存
        std::cout << "Derived::~Derived()" << std::endl;
        // 显式调用基类的虚析构函数
        // ensure the base class's destructor is executed
        Base::~Base();
    }
private:
    int *data;
};
int main() {
    Base *p = new Derived();
    delete p;
    return 0;
}

在上述代码中,派生类 Derived 在构造函数中分配了一个 int 数组的内存空间,然后在析构函数中释放了这个内存空间。同时,派生类的析构函数中显式地调用了基类的虚析构函数,以确保在析构对象时,先调用派生类的析构函数,再调用基类的析构函数,从而避免资源泄漏的问题。

总之,虚析构函数是实现多态和避免内存泄漏的关键,需要注意使用基类指针或引用来删除派生类对象,以及在派生类的析构函数中显式地调用基类的虚析构函数,以确保正确地释放资源。

纯虚函数和抽象类

包含纯虚函数的类叫抽象类

 抽象类只能作为基类来派生新类使用,不能创建独立的抽象类的对象

 抽象类的指针和引用可以指向由抽象类派生出来的类的对象

A a ; // 错,A 是抽象类,不能创建对象
A * pa ; // ok,可以定义抽象类的指针和引用
pa = new A ; //错误, A 是抽象类,不能创建对象

 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数。

 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。

纯虚函数和抽象类是 C++ 中用于实现接口和多态的重要特性。
纯虚函数是指在函数声明的结尾处使用 “= 0” 指明的函数,它是一种没有实现的虚函数。纯虚函数的作用是为了实现接口,即定义一个接口,而不需要提供具体的实现。由于纯虚函数没有具体实现,因此不能直接创建该类的对象,必须在派生类中实现该函数才能使用。例如:

class Shape {
public:
    virtual double area() = 0; // 纯虚函数
};
class Circle : public Shape {
public:
    double area() override { // 实现纯虚函数
        return 3.14 * radius * radius;
    }
private:
    double radius;
};

抽象类是指含有纯虚函数的类,它不能被直接实例化,只能被用作派生其他类的基类。抽象类的作用在于,定义了一组接口,而不需要提供具体的实现。由于抽象类含有纯虚函数,因此必须在派生类中实现所有的纯虚函数才能使用。例如:

class Shape {
public:
    virtual double area() = 0; // 纯虚函数,使得 Shape 变成了抽象类
    virtual void draw() = 0;   // 纯虚函数
};
class Circle : public Shape {
public:
    double area() override {
        return 3.14 * radius * radius;
    }
    void draw() override {
        // 绘制圆形
    }
private:
    double radius;
};

需要注意的是,抽象类可以包含非纯虚函数,但是含有纯虚函数的类一定是抽象类。因为含有纯虚函数的类不能被直接实例化,必须被用作派生其他类的基类,提供接口的定义。

纯虚函数和抽象类的作用是为了实现接口和多态,使得程序具有更加灵活和可扩展的特性。在实际的程序设计中,可以使用抽象类或纯虚函数来定义接口,从而使得程序的设计更加模块化和可维护。

输入和输出

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vG4xqoVi-1688033782522)(2023-06-20-20-53-48.png)]
与输入输出流操作相关的类有很多,以下是一些常见的:

  1. std::ifstream / std::ofstream:用于读取和写入文件。
  2. std::stringstream / std::ostringstream:用于将字符串作为流来处理。
  3. std::cin / std::cout:用于输入和输出标准输入输出流。
  4. std::wifstream / std::wofstream:用于读取和写入宽字符文件。
  5. std::istringstream / std::ostringstream:用于将字符串流化为输入输出流。
  6. std::wstringstream / std::wostringstream:用于将 wchar_t 类型字符串转化为输入输出流。

以上这些类都是基于标准输入输出流 std::stream 的派生类,使用这些类的时候需要包含 头文件。在使用这些类时,通常需要注意打开和关闭文件、读写字符或字符串的方式等操作。同时,需要注意数据的编码方式,例如 ASCII 码和 Unicode 码等,以确保数据的正确读写和传输。

标准流对象

标准流对象是指在 C++ 标准库中预定义好的输入输出流对象,包括三个标准流对象:cin、cout、cerr 和 clog。

  1. cin:用于标准输入(通常是终端或命令行窗口),可以使用流提取运算符(>>)读取数据。
  2. cout:用于标准输出(通常是终端或命令行窗口),可以使用流插入运算符(<<)输出数据。
  3. cerr 和 clog:用于输出错误信息和日志信息,与 cout 不同的是,cerr 和 clog 通常不会被重定向。cerr 是标准错误流,通常用于输出错误信息;clog 是标准日志流,通常用于输出日志信息。
    这些标准流对象都是全局对象,可以在任何地方访问它们。在使用标准流对象时,需要包含 头文件。
    例如,可以使用 cin 和 cout 来实现从标准输入读取数据并输出到标准输出:
#include <iostream>
int main() {
    int num;
    std::cout << "Please enter a number: ";
    std::cin >> num;
    std::cout << "The number you entered is: " << num << std::endl;
    return 0;
}

在上述代码中,使用 std::cout 来输出提示信息,然后使用 std::cin 来读取一个整数,最后再使用 std::cout 输出读取到的整数。

需要注意的是,标准流对象是缓冲流,缓冲区的大小可以通过调用 std::ios_base::sync_with_stdio 函数来设置。同时,可以通过调用 std::cin.clear() 和 std::cin.ignore() 函数来清空 cin 输入缓冲区和忽略输入缓冲区中的指定字符。

cin对应于标准输入流,用于从键盘读取数据,也可以被重定向
为从文件中读取数据。
 cout对应于标准输出流,用于向屏幕输出数据,也可以被重定
向为向文件写入数据。
 cerr对应于标准错误输出流,用于向屏幕输出出错信息,
 clog对应于标准错误输出流,用于向屏幕输出出错信息,
 cerr和clog的区别在于cerr不使用缓冲区,直接向显示器输出信息;而输出到clog中的信息先会被存放在
缓冲区,缓冲区满或者刷新时才输出到屏幕。

判断输入流结束

可以用如下方法判输入流结束:

int x;
while(cin>>x){
    
    ..
}
return 0;
 如果是从文件输入,比如前面有
freopen(“some.txt”,”r”,stdin);

那么,读到文件尾部,输入流就算结束

 如果从键盘输入,则在单独一行输入Ctrl+Z代表输入流结束

istream 是 C++ 中用于输入流的基类,它提供了一些用于输入流操作的成员函数,以下是一些常用的成员函数:

  1. getline():从输入流中读取一行数据。可以指定分隔符,例如:
    std::string line;
    std::getline(std::cin, line); // 从标准输入中读取一行数据
    
  2. ignore():忽略输入流中的指定字符或一定数量的字符。可以指定忽略的字符数或忽略到某个分隔符为止,例如:
    std::cin.ignore(100, '\n'); // 忽略输入流中的 100 个字符或遇到换行符为止
    
  3. peek():查看输入流中下一个字符但不移动输入流的指针位置,例如:
    char ch = std::cin.peek(); // 查看输入流中下一个字符
    
  4. putback():将一个字符放回输入流中,例如:
    std::cin.putback('x'); // 将字符 'x' 放回输入流中
    
  5. read():从输入流中读取指定数量的字节到指定的内存位置,例如:
    char buf[100];
    std::cin.read(buf, 100); // 从输入流中读取 100 个字节到 buf 中
    
  6. seekg() / tellg():用于移动输入流的指针位置和获取当前指针位置的函数。seekg() 可以将指针移动到指定位置,tellg() 可以获取当前指针的位置,例如:
    std::cin.seekg(10); // 将输入流的指针位置移动到第 10 个字符处
    std::streampos pos = std::cin.tellg(); // 获取当前输入流的指针位置
    

需要注意的是,以上成员函数是从 istream 类继承来的,也可以在派生类中使用。同时,需要注意输入流的状态,例如输入流是否已经到达文件结尾或输入流是否存在错误等。可以使用 eof()、fail()、bad() 和 good() 成员函数来获取输入流的状态。

istream类的成员函数

istream 是 C++ 中用于输入流的基类,它提供了一些用于输入流操作的成员函数,以下是一些常用的成员函数:

  1. getline():从输入流中读取一行数据。可以指定分隔符,例如:
    std::string line;
    std::getline(std::cin, line); // 从标准输入中读取一行数据
    
  2. ignore():忽略输入流中的指定字符或一定数量的字符。可以指定忽略的字符数或忽略到某个分隔符为止,例如:
    std::cin.ignore(100, '\n'); // 忽略输入流中的 100 个字符或遇到换行符为止
    
  3. peek():查看输入流中下一个字符但不移动输入流的指针位置,例如:
    char ch = std::cin.peek(); // 查看输入流中下一个字符
    
  4. putback():将一个字符放回输入流中,例如:
    std::cin.putback('x'); // 将字符 'x' 放回输入流中
    
  5. read():从输入流中读取指定数量的字节到指定的内存位置,例如:
    char buf[100];
    std::cin.read(buf, 100); // 从输入流中读取 100 个字节到 buf 中
    
  6. seekg() / tellg():用于移动输入流的指针位置和获取当前指针位置的函数。seekg() 可以将指针移动到指定位置,tellg() 可以获取当前指针的位置,例如:
    std::cin.seekg(10); // 将输入流的指针位置移动到第 10 个字符处
    std::streampos pos = std::cin.tellg(); // 获取当前输入流的指针位置
    

需要注意的是,以上成员函数是从 istream 类继承来的,也可以在派生类中使用。同时,需要注意输入流的状态,例如输入流是否已经到达文件结尾或输入流是否存在错误等。可以使用 eof()、fail()、bad() 和 good() 成员函数来获取输入流的状态。

C++文件读写

C++ 文件读写通常使用标准库中的 ifstream 和 ofstream 类来实现,这两个类都继承自 istream 和 ostream 类,提供了丰富的文件读写方法。
文件读取可以使用 ifstream 类,例如:

#include <iostream>
#include <fstream>
#include <string>
int main() {
    std::ifstream infile("filename.txt");
    std::string line;
    while (std::getline(infile, line)) {
        std::cout << line << std::endl;
    }
    infile.close();
    return 0;
}

在上述代码中,使用 ifstream 类打开名为 filename.txt 的文件,然后使用 getline() 函数读取文件中的每一行数据,并输出到标准输出流中。最后调用 close() 函数关闭文件。
文件写入可以使用 ofstream 类,例如:

#include <fstream>
#include <string>
int main() {
    std::ofstream outfile("filename.txt");
    outfile << "Hello, World!" << std::endl;
    outfile.close();
    return 0;
}

在上述代码中,使用 ofstream 类打开名为 filename.txt 的文件,然后使用流插入运算符(<<)将字符串 “Hello, World!” 写入文件中。最后调用 close() 函数关闭文件。
需要注意的是,文件读写操作可能会抛出异常,因此需要使用 try-catch 块来捕获异常并进行处理。同时,需要确保使用文件流之前先打开文件,使用之后再关闭文件,以保证操作的正确性和文件C++ 文件读写通常使用流操作,主要包括以下几个步骤:

  1. 打开文件:使用 std::ofstream 或 std::ifstream 类型的对象打开文件,可以指定打开方式和文件名等信息。
    std::ofstream outfile("example.txt"); // 以写入方式打开文件
    std::ifstream infile("example.txt"); // 以读取方式打开文件
    
  2. 写文件:通过流插入运算符(<<)写入数据到文件中。
    outfile << "This is a line of text" << std::endl;
    
  3. 读文件:通过流提取运算符(>>)读取文件中的数据。
    std::string line;
    while (std::getline(infile, line)) {
        std::cout << line << std::endl;
    }
    
  4. 关闭文件:使用 std::ofstream 或 std::ifstream 类型的对象关闭文件。
    outfile.close();
    infile.close();
    

需要注意的是,文件读写操作可能会出现错误,例如文件不存在、文件无法打开、文件权限不足等。可以使用流状态函数(eof()、fail()、bad() 和 good())或异常机制来处理这些错误。
完整的文件读写示例代码如下:

#include <iostream>
#include <fstream>
#include <string>
int main() {
    std::ofstream outfile("example.txt"); // 以写入方式打开文件
    if (!outfile.is_open()) {
        std::cerr << "Failed to open file" << std::endl;
        return -1;
    }
    outfile << "This is a line of text" << std::endl;
    outfile.close(); // 关闭文件
    std::ifstream infile("example.txt"); // 以读取方式打开文件
    if (!infile.is_open()) {
        std::cerr << "Failed to open file" << std::endl;
        return -1;
    }
    std::string line;
    while (std::getline(infile, line)) { // 逐行读取文件中的内容
        std::cout << line << std::endl;
    }
    infile.close(); // 关闭文件
    return 0;
}

以上代码先使用 std::ofstream 打开文件并写入一行文本,然后关闭文件,接着使用 std::ifstream 打开同一个文件并逐行读取其内容,并在标准输出中输出每行文本。需要注意的是,可以通过检查文件是否成功打开来判断文件是否存在或是否有读写权限。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V2csTOYp-1688033782522)(2023-06-20-21-02-28.png)]

除了使用 ofstream 和 ifstream 类,还可以使用 fstream 类来实现文件读写,它同时继承了 istream 和 ostream 类,可以用于读写文件。以下是一个使用 fstream 类读写文件的示例:

#include <iostream>
#include <fstream>
#include <string>
int main() {
    std::fstream file("example.txt", std::ios::in | std::ios::out | std::ios::trunc);
    if (!file.is_open()) {
        std::cerr << "Failed to open file" << std::endl;
        return -1;
    }
    file << "This is a line of text" << std::endl;
    std::string line;
    while (std::getline(file, line)) {
        std::cout << line << std::endl;
    }
    file.close();
    return 0;
}

在上述代码中,使用 fstream 类打开名为 example.txt 的文件,同时指定了打开方式为输入输出和截断文件。然后将一行文本写入文件中,接着使用 getline() 函数读取文件中的每一行数据,并输出到标准输出流中。最后调用 close() 函数关闭文件。
需要注意的是,在使用 fstream 类进行文件读写时,需要指定文件的打开方式。常用的文件打开方式包括:

  1. ios::in:以读取方式打开文件。
  2. ios::out:以写入方式打开文件。
  3. ios::app:在文件末尾追加内容。
  4. ios::trunc:打开文件并截断文件,即删除文件中的所有内容。
    同时,还可以使用按位或操作符将多个打开方式组合起来。例如,可以使用以下方式打开文件:
std::fstream file("example.txt", std::ios::in | std::ios::out | std::ios::app);

在上述代码中,打开文件的方式包括输入输出和在文件末尾追加内容。

需要注意的是,当打开文件时指定了 ios::trunc 标志时,文件的所有内容将被删除。因此,在使用此方式打开文件时,需要谨慎操作。

文件的读写指针

 对于输入文件,有一个读指针;

 对于输出文件,有一个写指针;

 对于输入输出文件,有一个读写指针;

 标识文件操作的当前位置, 该指针在哪里,读写操作就在哪里进行。

=文件的读写指针是非常重要的概念,它标识了文件操作的当前位置。

对于输入文件,读指针指示了当前读取位置在文件中的位置。当我们打开一个文件进行读取操作时,文件读指针默认位于文件的开头,每次读取操作后,文件读指针会自动向后移动,指向下一个要读取的位置。

对于输出文件,写指针指示了当前写入位置在文件中的位置。当我们打开一个文件进行写入操作时,文件写指针默认位于文件的开头,每次写入操作后,文件写指针会自动向后移动,指向下一个要写入的位置。

对于输入输出文件,读写指针可以同时进行读取和写入操作。在打开文件时,我们可以指定文件的读写指针初始位置,也可以使用 fseek() 函数移动文件的读写指针。

在进行文件读写操作时,我们需要非常注意文件读写指针的位置,以保证文件读写操作的正确性和安全性。如果读写指针的位置不正确,可能会导致数据的丢失或者文件内容的不一致。因此,在进行文件读写操作时,需要严格遵守文件读写指针的规则和约定。

二进制文件读写

二进制文件是一种以二进制编码形式存储数据的文件,与文本文件不同,二进制文件不以字符为单位进行存储,而是以二进制数据块为单位进行存储。在 C++ 中,可以使用二进制方式进行文件读写,实现对二进制文件的读写操作。
二进制文件的读写与文本文件的读写不同,主要体现在以下两个方面:

  1. 打开文件时需要使用二进制方式进行打开,即指定文件打开方式为 std::ios::binary。
  2. 读写操作时需要以二进制数据块为单位进行读写,而不是以字符为单位进行读写。
    以下是一个使用二进制方式读写二进制文件的示例:
#include <iostream>
#include <fstream>
struct Person {
    char name[20];
    int age;
    double height;
};
int main() {
    std::ofstream outfile("example.bin", std::ios::binary);
    if (!outfile) {
        std::cerr << "Failed to open file for writing" << std::endl;
        return -1;
    }
    Person person = {"Alice", 28, 1.65};
    outfile.write((const char*)&person, sizeof(person));
    outfile.close();
    std::ifstream infile("example.bin", std::ios::binary);
    if (!infile) {
        std::cerr << "Failed to open file for reading" << std::endl;
        return -1;
    }
    Person read_person;
    infile.read((char*)&read_person, sizeof(read_person));
    std::cout << "Name: " << read_person.name << std::endl;
    std::cout << "Age: " << read_person.age << std::endl;
    std::cout << "Height: " << read_person.height << std::endl;
    infile.close();
    return 0;
}

在上述代码中,首先使用 ofstream 类以二进制方式打开文件,并将一个 Person 结构体对象写入文件中。然后使用 ifstream 类以二进制方式打开文件,并读取文件中的二进制数据块,将其解析为一个 Person 结构体对象,并输出其成员变量的值。

需要注意的是,在进行二进制文件读写操作时,需要注意文件读写指针的位置,以保证二进制数据块的正确读写。另外,在进行文件读写操作时,应当确保写入和读取的二进制数据块的大小相同,否则可能会导致数据的丢失或者文件内容的不一致。

文件拷贝程序mycopy 示例

/*用法示例:
mycopy src.dat dest.dat 
即将 src.dat 拷贝到 dest.dat 如果 dest.dat 原来就有,则原来的文件会被覆
盖 */
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char * argv[])
{
    
    
if( argc != 3 ) {
    
    
cout << "File name missing!" << endl;
return 0;
}
ifstream inFile(argv[1],ios::binary|ios::in); //打开文件用
于读
if( ! inFile ) {
    
    
cout << "Source file open error." << endl;
return 0;
}
ofstream outFile(argv[2],ios::binary|ios::out); //打开文
件用于写
if( !outFile) {
    
    
cout << "New file open error." << endl;
inFile.close(); //打开的文件一定要关闭
return 0;
}
char c;
while( inFile.get(c)) //每次读取一个字符
outFile.put(c); //每次写入一个字符
outFile.close(); 
inFile.close();
return 0;
}

二进制文件和文本文件的区别

Linux,Unix下的换行符号:‘\n’ (ASCII码: 0x0a)

Windows 下的换行符号:‘\r\n’ (ASCII码: 0x0d0a) endl 就是 ‘\n’

Mac OS下的换行符号: ‘\r’ (ASCII码:0x0d)

导致 Linux, Mac OS 文本文件在Windows 记事本中打开时不换行二进制文件和文本文件的区别

 Unix/Linux下打开文件,用不用 ios::binary 没区别

 Windows下打开文件,如果不用 ios::binary,

则:读取文件时,所有的 ‘\r\n’会被当做一个字符’\n’处理,即少读了一个字
符’\r’。

写入文件时,写入单独的’\n’时,系统自动在前面加一个’\r’,即多写了一
个’\r

函数模板

交换两个整型变量的值的Swap函数:
void Swap(int & x,int & y) 
{
    
    
int tmp = x;
x = y;
y = tmp;
}
交换两个double型变量的值的Swap函数:
void Swap(double & x,double & y) 
{
    
    
double tmp = x;
x = y;
y = tmp;
}

用函数模板解决:

用函数模板解决:
template <class 类型参数1class 类型参数2,……>
返回值类型 模板名 (形参表)
{
    
    
函数体
};
template <class T>
void Swap(T & x,T & y) 
{
    
    
T tmp = x;
x = y;
y = tmp;
}
函数模板
int main()
{
    
    
int n = 1,m = 2;
Swap(n,m); //编译器自动生成 void Swap(int & ,int & )函数
double f = 1.2,g = 2.3;
Swap(f,g); //编译器自动生成 void Swap(double & ,double & )函数
return 0;
}
void Swap(double & x,double & y) 
{
    
    
double tmp = x;
x = y;
y = tmp

在C++中,函数模板是一种通用的函数定义,可以应用于不同的数据类型。它允许编写一次代码以适应多种不同的数据类型,实现代码的复用和泛化。

函数模板使用关键字 “template” 开始,并且后面跟着模板参数列表。模板参数列表可以包含一个或多个类型参数(如T、U等)或非类型参数(如整数常量)。例如:

template <typename T>
T max(T a, T b) {
    
    
    return (a > b) ? a : b;
}

上述代码中的函数模板 max 接受两个相同类型的参数,并返回较大的值。类型参数 T 可以是任何数据类型,比如整数、浮点数、字符等。

在实际调用函数模板时,编译器根据参数的类型将模板进行实例化,生成对应类型的函数。例如:

int result1 = max<int>(3, 5);      // 实例化为 max<int>, 返回 5
double result2 = max<double>(2.7, 1.5);  // 实例化为 max<double>, 返回 2.7
char result3 = max<char>('a', 'b');     // 实例化为 max<char>, 返回 'b'

在上面的例子中,通过 <类型> 的形式来指定实例化的具体类型,这样编译器就能够根据传入的类型生成对应的函数。如果没有显式指定类型,编译器会根据参数的类型自动推导出实例化的类型。

函数模板还可以有多个类型参数,并且可以有默认参数值。此外,你还可以在函数模板外定义非模板函数,它们可以与函数模板进行重载。

函数模板是C++中一种强大的工具,利用它可以编写通用且具有复用性的代码,可以处理不同类型的数据。

当需要在函数模板中处理多个不同类型的参数时,可以使用多个类型参数。

例如,下面是一个函数模板 swap,用于交换两个值:

template <typename T>
void swap(T& a, T& b) {
    
    
    T temp = a;
    a = b;
    b = temp;
}

上述代码中的 swap 函数模板接受两个相同类型的引用参数,并交换它们的值。使用该函数模板时,编译器将根据实参的类型实例化对应的函数。

int x = 5, y = 10;
swap(x, y);  // 实例化为 swap<int>(x, y),交换 x 和 y 的值

double a = 2.5, b = 3.7;
swap(a, b);  // 实例化为 swap<double>(a, b),交换 a 和 b 的值

除了类型参数,函数模板还可以包含非类型参数。非类型参数可以是整数、枚举、指针或引用类型,但不能是浮点数、类类型或 void 类型。

下面是一个示例,演示如何在函数模板中使用非类型参数来指定数组的大小:

template <typename T, int size>
void printArray(const T (&arr)[size]) {
    
    
    for (int i = 0; i < size; ++i) {
    
    
        cout << arr[i] << " ";
    }
    cout << endl;
}

上述代码中的 printArray 函数模板接受一个固定大小的数组,并打印每个元素。通过将数组大小作为非类型参数传递给函数模板,可以在编译时知道数组的大小。

int intArray[] = {
    
    1, 2, 3, 4, 5};
printArray(intArray);  // 实例化为 printArray<int, 5>(intArray)

double doubleArray[] = {
    
    1.5, 2.7, 3.9};
printArray(doubleArray);  // 实例化为 printArray<double, 3>(doubleArray)

这样,函数模板就可以根据不同的数组大小生成对应的函数。

需要注意的是,在函数模板的定义和声明中,通常将模板参数放在尖括号 < > 中,并使用关键字 typenameclass 来声明类型参数。然而,你也可以使用非类型参数来调整模板的行为。

同时,函数模板还可以具有默认模板参数,以便更灵活地使用。默认模板参数允许指定某个或某些参数的默认值,使得在函数调用时可以省略掉这些参数。

C++中的函数模板是一种强大的工具,可以处理多个不同类型的参数,其中可以包含类型参数和非类型参数。通过使用函数模板,可以实现通用、可复用的代码,并根据实参的类型和值来自动生成对应的函数。

函数模板和函数的次序

在有多个函数和函数模板名字相同的情况下,编译器如下处理一条函数调用语句

  1. 先找参数完全匹配的普通函数(非由模板实例化而得的函数)。
  2. 再找参数完全匹配的模板函数。
  3. 再找实参数经过自动类型转换后能够匹配的普通函数。
  4. 上面的都找不到,则报错。

类模板

类模板 – 问题的提出
• 为了多快好省地定义出一批相似的类,可以定义类模板,然后由类模
板生成不同的类
• 数组是一种常见的数据类型,元素可以是:
– 整数
– 学生
– 字符串
– ……
• 考虑一个可变长数组类,需要提供的基本操作
– len():查看数组的长度
– getElement(int index):获取其中的一个元素
– setElement(int index):对其中的一个元素进行赋值
– ……

### 类模板的定义

template <typename 类型参数1typename 类型参数2,……>
//类型参数表
class 类模板名
{
    
    
成员函数和成员变量
};

类模板里成员函数的写法:
template <class 类型参数1class 类型参数2,……> //类型参数表
返回值类型 类模板名<类型参数名列表>::成员函数名(参数表)
{
    
     
……
}


用类模板定义对象的写法:
类模板名 <真实类型参数表> 对象名(构造函数实参表);
类模板示例: Pair类模板
template <class T1,class T2>
class Pair
{
    
    
public:
T1 key; //关键字
T2 value; //值
Pair(T1 k,T2 v):key(k),value(v) {
    
     };
bool operator < ( const Pair<T1,T2> & p) const; 
};
template<class T1,class T2>
bool Pair<T1,T2>::operator < ( const Pair<T1,T2> & p) const 
//Pair的成员函数 operator <
{
    
     
return key < p.key; 
}

类模板示例:Pair类模板

int main()
{
    
    
Pair<string,int> student("Tom",19); 
//实例化出一个类 Pair<string,int>
cout << student.key << " " << student.value; 
return 0;
}
输出:
Tom 19

类模板(Class Template)是C++中另一种通用编程的工具,它允许定义一种通用的类,可以用于不同的数据类型。

类模板使用关键字 template 开始,并在尖括号 < > 中包含一个或多个类型参数。类型参数可以在类定义的内部作为类型的占位符使用。例如:

template <typename T>
class MyStack {
    
    
private:
    T* elements;
    int top;
    int capacity;

public:
    MyStack(int size) {
    
    
        elements = new T[size];
        capacity = size;
        top = -1;
    }

    // 其他成员函数的实现省略...
};

上述代码中的 MyStack 类模板定义了一个栈的数据结构,其中的元素类型 T 是一个占位符,在实际使用时会根据传入的类型进行实例化。

在实际使用类模板时,需要根据实际需求显式实例化特定类型的类。例如:

MyStack<int> intStack(10);       // 实例化为 MyStack<int>
MyStack<double> doubleStack(5);  // 实例化为 MyStack<double>

在上述代码中,分别创建了一个 int 类型和一个 double 类型的 MyStack 对象。每个对象都是根据相应的类型实例化的类。

类模板还可以有多个类型参数,并且可以包含非类型参数,与函数模板类似。可以通过提供特定类型的实参来显式指定实例化的类型,也可以让编译器自动推导类型。

类模板还可以有成员函数、成员变量、构造函数和析构函数等,可以根据具体需求进行实现。在定义类模板的成员函数时,可以像普通类一样使用 T 来引用模板参数所表示的类型。

template <typename T>
class MyStack {
    
    
    // ...

    void push(T element) {
    
    
        if (top + 1 >= capacity) {
    
    
            // 扩展容量代码...
        }
        elements[++top] = element;
    }

    T pop() {
    
    
        if (top < 0) {
    
    
            // 异常处理代码...
        }
        return elements[top--];
    }

    // ...
};

总结一下,类模板是C++中用于定义通用类的工具,允许创建可以适应不同数据类型的类。类模板使用关键字 template 和类型参数来定义,并通过显式实例化或类型推导来生成特定类型的类。类模板还可以包含成员函数、成员变量等,用于处理特定类型的数据。

用类模板定义对象

编译器由类模板生成类的过程叫类模板的实例化。由类模板实例化得到的类,叫模板类。

同一个类模板的两个模板类是不兼容的

同一个类模板的两个模板类是不兼容的

是的,对于同一个类模板,不同的模板实例(即不同的模板参数)会生成不同的类型,这些类型在编译时是完全独立和不兼容的。

例如,考虑下面的类模板 MyStack

template <typename T>
class MyStack {
    
    
    // ...
};

使用 MyStack<int> 实例化的对象和使用 MyStack<double> 实例化的对象是完全不同的类型,它们之间是不兼容的。

MyStack<int> intStack;
MyStack<double> doubleStack;

intStack.push(5);
doubleStack.push(3.14);

int x = intStack.pop();         // 类型为 int
double y = doubleStack.pop();   // 类型为 double

在上述代码中,intStackdoubleStack 是两个完全不同的对象,它们的行为和类型都是根据实例化时的模板参数来确定的。

由于不同的模板实例生成的类型是不兼容的,因此不能将 MyStack<int> 的对象赋值给 MyStack<double> 的对象,也不能将它们混合使用。

MyStack<int> intStack;
MyStack<double> doubleStack;

// 以下代码是不允许的,会导致类型错误:
doubleStack = intStack;          // 错误:不兼容的类型
double x = intStack.pop();       // 错误:类型不匹配
intStack.push(3.14);             // 错误:类型不匹配

因此,对于同一个类模板生成的不同模板实例,它们是不兼容的,并且在使用时需要注意保持类型一致。

函数模版作为类模板成员

函数模板可以作为类模板的成员函数。类模板中的成员函数也可以是函数模板,允许在不同的实例化类型上进行通用操作。

下面是一个示例,演示了如何在类模板中定义函数模板作为成员函数:

template <typename T>
class MyVector {
    
    
private:
    T* elements;
    int size;

public:
    MyVector(int s) : size(s) {
    
    
        elements = new T[size];
    }

    template <typename U>
    void setValue(int index, U value) {
    
    
        if (index >= 0 && index < size) {
    
    
            elements[index] = static_cast<T>(value);
        }
    }

    // 其他成员函数的实现...
};

在上述代码中,MyVector 是一个类模板,其中定义了一个名为 setValue 的成员函数模板。此函数模板接受两个参数,一个是 index 表示要设置值的索引,另一个是 value 表示要设置的值。该函数模板可以适用于不同的数据类型 TU

使用示例:

MyVector<int> myIntVector(5);
myIntVector.setValue(0, 10);          // 设置索引0处的值为10

MyVector<double> myDoubleVector(3);
myDoubleVector.setValue(1, 3.14);     // 设置索引1处的值为3.14

在上述示例中,我们分别创建了一个 MyVector<int> 和一个 MyVector<double> 对象,并使用 setValue 函数模板设置了不同类型的值。

通过在类模板中定义函数模板,可以实现对不同类型的数据进行通用操作,增加了代码的灵活性和复用性。

类模板与派生

• 类模板从类模板派生
• 类模板从模板类派生
• 类模板从普通类派生
• 普通类从模板类派生

类模板可以作为基类用于派生其他类。通过派生,可以在派生类中使用基类的模板参数,并添加额外的成员变量和成员函数。

下面是一个示例,演示了如何使用类模板作为基类进行派生:

template <typename T>
class MyBaseTemplate {
    
    
protected:
    T data;

public:
    MyBaseTemplate(const T& value) : data(value) {
    
    }

    void printData() const {
    
    
        std::cout << "Data: " << data << std::endl;
    }
};

template <typename T>
class MyDerivedTemplate : public MyBaseTemplate<T> {
    
    
private:
    int additionalData;

public:
    MyDerivedTemplate(const T& value, int additional) : MyBaseTemplate<T>(value), additionalData(additional) {
    
    }

    void printAllData() const {
    
    
        MyBaseTemplate<T>::printData();
        std::cout << "Additional Data: " << additionalData << std::endl;
    }
};

在上述代码中,MyBaseTemplate 是一个类模板,它有一个模板参数 T 和一个成员变量 data。派生类 MyDerivedTemplate 继承自 MyBaseTemplate<T>,并添加了一个额外的成员变量 additionalData

派生类中的构造函数使用基类的构造函数进行初始化,并将额外的参数传递给派生类的成员变量。

派生类还可以调用基类的成员函数,如示例中的 printData() 函数。使用作用域解析运算符 :: 可以访问基类的成员函数。

使用示例:

MyDerivedTemplate<int> myDerived(10, 20);
myDerived.printAllData();

在上述示例中,我们创建了一个 MyDerivedTemplate<int> 对象,并将值 1020 分别传递给基类和派生类的构造函数。然后,调用派生类的 printAllData() 函数,它会分别打印基类的数据和派生类的额外数据。

通过派生,我们可以在派生类中扩展和特化基类模板的功能,实现更灵活和具体化的代码。

类模板从类模板派生

类模板可以从另一个类模板派生,这样可以在派生类中使用基类的模板参数,并添加额外的模板参数和成员函数。

下面是一个示例,演示了如何从类模板派生另一个类模板:

template <typename T>
class MyBaseTemplate {
    
    
protected:
    T data;

public:
    MyBaseTemplate(const T& value) : data(value) {
    
    }

    void printData() const {
    
    
        std::cout << "Data: " << data << std::endl;
    }
};

template <typename T, typename U>
class MyDerivedTemplate : public MyBaseTemplate<T> {
    
    
private:
    U additionalData;

public:
    MyDerivedTemplate(const T& value, const U& additional) : MyBaseTemplate<T>(value), additionalData(additional) {
    
    }

    void printAllData() const {
    
    
        MyBaseTemplate<T>::printData();
        std::cout << "Additional Data: " << additionalData << std::endl;
    }
};

在上述代码中,MyBaseTemplate 是一个类模板,它有一个模板参数 T 和一个成员变量 data。派生类 MyDerivedTemplate 是一个带有两个模板参数 TU 的类模板,它从 MyBaseTemplate<T> 派生而来,并添加了一个额外的模板参数 U 和成员变量 additionalData

派生类中的构造函数使用基类的构造函数进行初始化,并将额外的参数传递给派生类的成员变量。

派生类还可以调用基类的成员函数,使用作用域解析运算符 :: 可以访问基类的成员函数。

使用示例:

MyDerivedTemplate<int, double> myDerived(10, 3.14);
myDerived.printAllData();

在上述示例中,我们创建了一个 MyDerivedTemplate<int, double> 对象,并将值 103.14 分别传递给基类和派生类的构造函数。然后,调用派生类的 printAllData() 函数,它会分别打印基类的数据和派生类的额外数据。

通过从类模板派生另一个类模板,可以实现更加灵活和通用的代码结构,同时具备模板参数的扩展能力。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

普通类从模板类派生

普通类也可以从模板类派生,这样可以在派生类中使用模板类的具体化版本。派生类不需要显式地指定模板参数,因为已经在模板类中进行了定义。

下面是一个示例,演示了如何从模板类派生普通类:

template <typename T>
class MyTemplateClass {
    
    
protected:
    T data;

public:
    MyTemplateClass(const T& value) : data(value) {
    
    }

    void printData() const {
    
    
        std::cout << "Data: " << data << std::endl;
    }
};

class MyDerivedClass : public MyTemplateClass<int> {
    
    
private:
    int additionalData;

public:
    MyDerivedClass(const int& value, int additional) : MyTemplateClass<int>(value), additionalData(additional) {
    
    }

    void printAllData() const {
    
    
        MyTemplateClass<int>::printData();
        std::cout << "Additional Data: " << additionalData << std::endl;
    }
};

在上述代码中,MyTemplateClass 是一个模板类,它有一个模板参数 T 和一个成员变量 data。派生类 MyDerivedClassMyTemplateClass<int> 派生而来,并添加了一个额外的成员变量 additionalData

派生类的构造函数使用基类的具体化版本 MyTemplateClass<int> 进行初始化,并将额外的参数传递给派生类的成员变量。

派生类可以调用基类的成员函数,使用作用域解析运算符 :: 可以访问基类的成员函数。

使用示例:

MyDerivedClass myDerived(10, 20);
myDerived.printAllData();

在上述示例中,我们创建了一个 MyDerivedClass 对象,并将值 1020 分别传递给基类和派生类的构造函数。然后,调用派生类的 printAllData() 函数,它会分别打印基类的数据和派生类的额外数据。

通过从模板类派生普通类,可以使用特定的模板参数类型,而不必在派生类中指定额外的模板参数。这样可以更方便地使用模板类的功能,并提供更具体化的代码实现。

类模板与友员函数

• 函数、类、类的成员函数作为类模板的友元
• 函数模板作为类模板的友元
• 函数模板作为类的友元
• 类模板作为类模板的友元

类模板可以定义友元函数,这样友元函数可以访问类模板的私有成员和保护成员。友元函数可以在类定义内或外定义。

以下是一个示例,演示了如何在类模板中定义和使用友元函数:

template <typename T>
class MyTemplateClass {
    
    
private:
    T data;

public:
    MyTemplateClass(const T& value) : data(value) {
    
    }

    template <typename U>
    friend void printData(const MyTemplateClass<U>& obj);
};

template <typename U>
void printData(const MyTemplateClass<U>& obj) {
    
    
    std::cout << "Data: " << obj.data << std::endl;
}

在上述代码中,MyTemplateClass 是一个类模板,它有一个模板参数 T 和一个私有成员变量 data。类模板中定义了一个友元函数 printData,该函数可以访问 MyTemplateClass 的私有成员变量 data

用户可以在类模板定义内部或外部定义友元函数。在上述示例中,友元函数 printData 的定义位于类模板定义外部,但在定义友元函数时需要使用类模板的具体化版本 MyTemplateClass<U>

使用示例:

MyTemplateClass<int> obj(10);
printData(obj);

在上述示例中,我们创建了一个 MyTemplateClass<int> 对象,并将值 10 传递给构造函数。然后,我们调用友元函数 printData,它会打印类模板对象的私有成员变量 data

通过定义友元函数,类模板可以在需要访问私有或保护成员时提供额外的灵活性和扩展性。这使得友元函数可以直接操作类模板对象的内部数据,而无需通过公有接口。

在C++中,函数、类、类的成员函数和函数模板都可以作为类模板的友元。下面分别介绍这些情况:

  1. 函数作为类模板的友元:
template <typename T>
class MyTemplateClass {
    
    
    // 声明函数为友元
    friend void myFriendFunction<T>(const MyTemplateClass<T>& obj);
};

template <typename T>
void myFriendFunction(const MyTemplateClass<T>& obj) {
    
    
    // 可以访问MyTemplateClass的私有成员和保护成员
}
  1. 类作为类模板的友元:
template <typename T>
class MyFriendClass {
    
    
   // ...
};

template <typename T>
class MyTemplateClass {
    
    
    // 声明类为友元
    friend class MyFriendClass<T>;
};
  1. 类的成员函数作为类模板的友元:
template <typename T>
class MyTemplateClass {
    
    
private:
    T data;

    // 声明类的成员函数为友元
    friend void MyTemplateClass<T>::myFriendMemberFunction();
    
    void myFriendMemberFunction() {
    
    
        // 可以访问MyTemplateClass的私有成员和保护成员
    }
};
  1. 函数模板作为类模板的友元:
template <typename T>
class MyTemplateClass {
    
    
    // 声明函数模板为友元
    template <typename U>
    friend void myFriendFunctionTemplate(const MyTemplateClass<U>& obj);
};

template <typename U>
void myFriendFunctionTemplate(const MyTemplateClass<U>& obj) {
    
    
    // 可以访问MyTemplateClass的私有成员和保护成员
}
  1. 类模板作为类模板的友元:
template <typename T>
class MyFriendClass {
    
    
   // ...
};

template <typename T>
class MyTemplateClass {
    
    
    // 声明类模板为友元
    template <typename U>
    friend class MyFriendClass<U>;
};

以上示例为各种不同情况下如何声明和使用类模板的友元。友元关系允许其他函数、类或成员函数访问类模板中的私有成员和保护成员,从而提供更大的灵活性和扩展性。请根据实际需求选择适合的友元类型。

类模板与静态成员变量

类模板与static成员

• 类模板中可以定义静态成员,那么从该类模板实例化得到的所有类,
都包含同样的静态成员。
#include <iostream>
using namespace std;
template <class T>
class A
{
    
    
private:
static int count;
public:
A() {
    
     count ++; }
~A() {
    
     count -- ; };
A( A & ) {
    
     count ++ ; }
static void PrintCount() {
    
     cout << count << endl; }
};
类模板与static成员
template<> int A<int>::count = 0;
template<> int A<double>::count = 0;
int main()
{
    
    
A<int> ia;
A<double> da;
ia.PrintCount();
da.PrintCount();
return 0;
}

输出:
1
1

类模板与静态成员变量

类模板和静态成员变量可以结合使用。可以在类模板中声明和定义静态成员变量,并且所有实例化的类都共享同一个静态成员变量。以下是示例代码:

template <typename T>
class MyTemplateClass {
    
    
public:
    static int count; // 声明静态成员变量

    MyTemplateClass() {
    
    
        count++; // 在构造函数中对静态成员变量进行操作
    }
};

template <typename T>
int MyTemplateClass<T>::count = 0; // 静态成员变量的定义和初始化

int main() {
    
    
    MyTemplateClass<int> obj1;
    MyTemplateClass<int> obj2;
    MyTemplateClass<double> obj3;

    std::cout << "Count for int: " << MyTemplateClass<int>::count << std::endl; // 输出2
    std::cout << "Count for double: " << MyTemplateClass<double>::count << std::endl; // 输出1

    return 0;
}

在上述示例中,MyTemplateClass 是一个类模板,其中声明了一个静态成员变量 count。在类模板外部,我们需要对静态成员变量进行定义和初始化,使用类似于普通类的静态成员变量的语法。

main() 函数中,我们创建了几个类模板的实例。每当创建一个实例时,构造函数会自动递增静态成员变量 count。因为静态成员变量是被所有实例共享的,所以每个实例的构造都会影响到所有实例。

最后,我们通过类名加作用域解析运算符 :: 来访问不同类型的静态成员变量,并将其输出到控制台。

总结来说,类模板可以具有静态成员变量,并且所有实例化的类都共享同一个静态成员变量。这在跟踪和计数类模板对象的数量时非常有用。

标准模版库

https://blog.csdn.net/shaozheng0503/article/details/129101932?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168802585416800211563089%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=168802585416800211563089&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-2-129101932-null-null.268v1koosearch&utm_term=stl&spm=1018.2226.3001.4450

标准模板库(Standard Template Library,STL)是C++的一个重要组成部分,提供了一套丰富的数据结构和算法容器。STL中的容器、算法和迭代器是三个主要的组件。

以下是STL中常见的几个组件:

  1. 容器(Containers):STL提供了各种容器,包括向量(vector)、链表(list)、双向链表(deque)、集合(set)、映射(map)等。这些容器类似于数据结构,用于存储和管理数据。

  2. 算法(Algorithms):STL提供了大量的算法,包括排序、搜索、合并、查找等操作。这些算法可以应用于各种不同的容器,使得对容器进行操作变得非常简单。

  3. 迭代器(Iterators):迭代器是用于遍历容器中元素的一种通用方式。STL中的迭代器提供了一种统一的接口,使得可以以相同的方式访问不同类型的容器。

  4. 函数对象(Function Objects):函数对象是可调用对象(如函数指针或重载了函数调用运算符的类对象),用于在算法中定义自定义的操作。

  5. 分配器(Allocators):分配器用于管理内存的分配和释放,可以在STL的容器和算法中使用不同的分配器。

STL的设计目标是提供高效、灵活、通用的数据结构和算法,使得C++开发者可以更加便捷地进行编程。使用STL可以大大减少开发时间和代码复杂性,并且提供了可移植性和重用性。在C++开发中,使用STL可以极大地提高代码质量和开发效率。

泛型程序设计

C++ 语言的核心优势之一就是便于软件的重用

C++中有两个方面体现重用:

1.面向对象的思想:继承和多态,标准类库
2.泛型程序设计(generic programming) 的思想: 模板机制,以及标准模板库 STL

简单地说就是使用模板的程序设计法。

将一些常用的数据结构(比如链表,数组,二叉树)和算法(比如排序,查找)写成模板,以后则不论数据结构里放的是什么对象,算法针对什么样的对象,则都不必重新实现数据结构,重新编写算法。

标准模板库 (Standard Template Library) 就是一些常用数据结构和算法的模板的集合。有了STL,不必再写大多的标准数据结构和算法,并且可获得非常高的性能。

STL中的基本的概念

容器:可容纳各种数据类型的通用数据结构,是类模板

迭代器:可用于依次存取容器中元素,类似于指针

算法:用来操作容器中的元素的函数模板

 sort()来对一个vector中的数据进行排序

 find()来搜索一个list中的对象

算法本身与他们操作的数据的类型无关,因此他们可以在从简单数组到高度复杂容器的任何数据结构上使用。

int array[100];
该数组就是容器,而 int * 类型的指针变量就可以作为迭代器,sort算
法可以作用于该容器上,对其进行排序:
sort(array,array+70); //将前70个元素排序
迭代器

容器概述

没错,您提到的是STL中的一些常见容器和容器适配器。让我详细介绍一下它们:

  1. 顺序容器:
  • 向量(vector):向量是一个动态数组,能够在其末尾高效地添加和删除元素。它还支持随机访问,即可以通过索引直接访问元素。
  • 双端队列(deque):双端队列与向量类似,但也允许在头部进行高效插入和删除操作。
  • 链表(list):链表是一种双向链表,可以在任意位置进行高效的插入和删除操作,但无法通过索引直接访问元素。
  1. 关联容器:
  • 集合(set):集合是一个有序且不含重复元素的容器。它支持高效的搜索、插入和删除操作。
  • 多重集合(multiset):多重集合与集合类似,但允许容器中存在重复的元素。
  • 映射(map):映射是一种键-值对的容器,每个元素都有一个唯一的键。它支持按照键进行高效的搜索、插入和删除操作。
  • 多重映射(multimap):多重映射与映射类似,但允许容器中存在重复的键。
  1. 容器适配器:
  • 栈(stack):栈是一种后进先出(LIFO)的容器适配器。它基于双端队列或列表实现,只允许在顶部进行插入和删除操作。
  • 队列(queue):队列是一种先进先出(FIFO)的容器适配器。它基于双端队列或列表实现,允许在一端进行插入,在另一端进行删除。
  • 优先队列(priority_queue):优先队列是一种基于堆的容器适配器,它保证队列中具有最高优先级的元素始终位于队列前端。

这些容器和容器适配器提供了不同的数据组织方式和操作特性,可以根据需要选择最合适的容器来存储和管理数据。每个容器都有其独特的优势和适用场景,了解它们的特点和使用方法将对编程非常有帮助。

对象被插入容器中时,被插入的是对象的一个复制品。许多算法,比如排序,查找,要求对容器中的元素进行比较,有的容器本身就是排序的,所以,放入容器的对象所属的类,往往还应该重载 == 和 < 运算符。

迭代器

 用于指向顺序容器和关联容器中的元素
 迭代器用法和指针类似
 有const 和非 const两种
 通过迭代器可以读取它指向的元素
 通过非const迭代器还能修改其指向的元素

迭代器是STL中的一个重要概念,它用于遍历容器中的元素并访问它们。通过迭代器,我们可以以一种统一的方式对不同类型的容器进行操作,而不需要关心具体容器的实现细节。

STL中定义了多种类型的迭代器,每种迭代器具有不同的特性和功能。以下是STL中的常见迭代器分类:

  1. 输入迭代器(Input Iterator):输入迭代器用于在容器中从前向后遍历元素,并支持读取元素的值。输入迭代器只能单向移动,不支持修改容器中的元素。

  2. 输出迭代器(Output Iterator):输出迭代器用于在容器中从前向后遍历元素,并支持修改元素的值。输出迭代器只能单向移动,不支持读取元素的值。

  3. 正向迭代器(Forward Iterator):正向迭代器可以像输入迭代器和输出迭代器一样进行单向遍历和修改。与输入迭代器和输出迭代器不同的是,正向迭代器支持多次遍历,即可以对同一个容器进行多次迭代。

  4. 双向迭代器(Bidirectional Iterator):双向迭代器比正向迭代器更强大,它支持从前向后和从后向前遍历容器中的元素。双向迭代器可以进行递增和递减操作。

  5. 随机访问迭代器(Random Access Iterator):随机访问迭代器是最强大的迭代器类型,它具有所有其他迭代器的功能,并且支持随机访问容器中的元素。随机访问迭代器可以像指针一样进行算术运算,使得我们可以在常数时间内访问容器中的任意元素。

使用迭代器,我们可以通过以下方式遍历容器中的元素:

for (auto it = container.begin(); it != container.end(); ++it) {
    
    
    // 访问迭代器指向的元素
    // *it 可以获取当前迭代器指向的元素
}

需要注意的是,不同类型的容器可能提供不同类型的迭代器。例如,向量和双端队列提供随机访问迭代器,而链表只提供双向迭代器。在编写代码时,我们需要根据具体容器的特性选择合适的迭代器类型来进行操作。

迭代器是STL的核心概念之一,掌握了迭代器的使用方法,可以更加灵活和高效地操作容器中的元素。
当然,继续回答您关于迭代器的问题。

在STL中,除了常见的迭代器类型外,还有一些其他类型的迭代器和相关概念:

  1. 反向迭代器(Reverse Iterator):反向迭代器是一种特殊的迭代器,它可以逆序遍历容器中的元素。通过rbegin()rend()成员函数可以获取反向迭代器的起始和结束位置。
for (auto rit = container.rbegin(); rit != container.rend(); ++rit) {
    
    
    // 访问反向迭代器指向的元素
}
  1. 常量迭代器(Const Iterator):常量迭代器用于对容器中的元素进行只读访问,不允许修改元素的值。通过cbegin()cend()成员函数可以获取常量迭代器的起始和结束位置。
for (auto cit = container.cbegin(); cit != container.cend(); ++cit) {
    
    
    // 访问常量迭代器指向的元素,只允许进行读取操作
}
  1. 插入迭代器(Insert Iterator):插入迭代器是一种特殊的迭代器,它可以在容器中插入新的元素。插入迭代器是通过std::inserter()函数创建的。
std::vector<int> vec;
std::fill_n(std::inserter(vec, vec.begin()), 5, 0);
// 在vec的开始位置插入5个0
  1. 流迭代器(Stream Iterator):流迭代器用于将容器中的元素通过输入输出流进行读写。通过std::istream_iteratorstd::ostream_iterator可以创建输入和输出流迭代器。
// 从标准输入流中读取整数,并存储到容器中
std::vector<int> vec(std::istream_iterator<int>(std::cin), std::istream_iterator<int>());

// 将容器中的元素通过空格分隔输出到标准输出流
std::copy(vec.begin(), vec.end(), std::ostream_iterator<int>(std::cout, " "));

这些特殊类型的迭代器扩展了STL对容器的操作能力,使得我们可以更灵活地使用迭代器进行元素的遍历、修改和插入等操作。根据实际需求,选择合适的迭代器类型能够提高代码的可读性和效率。

迭代器是用于指向顺序容器(如向量、列表等)和关联容器(如映射、集合等)中的元素的对象。它的用法与指针非常相似,可以通过迭代器访问和操作容器中的元素。

迭代器分为const迭代器和非const迭代器两种类型。const迭代器只能读取其指向的元素,而非const迭代器除了读取,还可以修改其指向的元素。

使用迭代器可以通过以下方式读取和修改元素:

  • 读取元素:通过解引用操作符*来获取迭代器指向的元素的值。例如*it获取迭代器it指向的元素。

  • 修改元素:通过解引用操作符*获取元素的引用,然后对引用进行修改。例如*it = value将迭代器it指向的元素设置为value

下面是一个使用迭代器进行读取和修改的示例代码:

#include <iostream>
#include <vector>

int main() {
    
    
    std::vector<int> vec = {
    
    1, 2, 3, 4, 5};

    // 使用非const迭代器读取和修改元素
    for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
    
    
        // 读取元素的值
        int value = *it;
        std::cout << value << " ";

        // 修改元素的值
        *it = value * 2;
    }
    std::cout << std::endl;

    // 输出修改后的元素
    for (const auto& num : vec) {
    
    
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

输出:

1 2 3 4 5
2 4 6 8 10

通过迭代器,我们可以灵活地对容器中的元素进行读取和修改,使得我们能够更方便地操作容器中的数据。

算法简介

您对STL算法的描述非常准确!

算法是函数模板,大多数在头文件中定义。STL提供了可以在各种容器中通用的算法,如查找、排序等。

这些算法通过迭代器来操作容器中的元素。许多算法可以对容器中的一个局部区间进行操作,因此需要两个参数:起始元素的迭代器和终止元素的后一个元素的迭代器。比如,排序算法和查找算法就是如此。

有些算法会返回一个迭代器作为结果。例如,find()算法用于在容器中查找一个元素,并返回指向该元素的迭代器。

不仅可以处理容器,STL算法也可以处理普通数组。由于数组也可以使用迭代器进行访问,因此可以将数组的指针视为其首元素的迭代器,并将指向(末尾+1)的指针视为终止元素的迭代器。

下面是一个示例代码,演示了如何使用STL算法在容器和数组中查找元素:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    
    
    std::vector<int> vec = {
    
    1, 2, 3, 4, 5};

    // 在容器中使用find算法查找元素
    auto it = std::find(vec.begin(), vec.end(), 3);
    if (it != vec.end()) {
    
    
        std::cout << "找到了元素3" << std::endl;
    } else {
    
    
        std::cout << "未找到元素3" << std::endl;
    }

    int arr[] = {
    
    1, 2, 3, 4, 5};

    // 在数组中使用find算法查找元素
    auto ptr = std::find(arr, arr + 5, 3);
    if (ptr != arr + 5) {
    
    
        std::cout << "找到了元素3" << std::endl;
    } else {
    
    
        std::cout << "未找到元素3" << std::endl;
    }

    return 0;
}

输出:

找到了元素3
找到了元素3

通过STL算法,我们可以高效地在容器和数组中执行各种操作,大大提高了代码的复用性和可读性。

vector和deque

vectordeque都是C++标准库中的容器,用于存储和管理元素。它们有一些共同点,也有一些不同之处。

相同点:

  1. 动态大小:vectordeque都是动态数组,可以在运行时根据需要动态调整容器的大小。
  2. 随机访问:两者都支持随机访问,可以通过索引直接访问容器中的元素。
  3. 迭代器支持:两者都提供迭代器来遍历容器中的元素。
  4. 支持尾部插入和删除:vectordeque都能够在尾部进行元素的插入和删除操作,并且具有接近常数时间的复杂度。

不同点:

  1. 内存分配方式:vector使用连续的内存块来存储元素,因此在内存中是紧密排列的。而deque使用多个小块的连续内存空间分别存储元素,这些小块之间通过指针链接起来。这使得deque能够在两端进行高效的插入和删除操作。
  2. 空间占用:由于vector使用连续的内存块,因此在同样数量的元素情况下,vector所占用的空间较小,而deque则相对较大,因为它需要维护额外的指针来连接不同的块。
  3. 中间插入和删除:vector对于中间位置的插入和删除操作比较低效,因为需要移动后续的元素;而deque在任何位置都能够高效地进行插入和删除操作。
  4. 迭代器失效:由于vector使用连续的内存块,因此添加或删除元素可能会导致已存在的迭代器失效。而deque由于使用了指针链接的方式,所以在除插入和删除操作发生在容器两端时,其他位置的迭代器仍然有效。

选择使用vector还是deque取决于具体的需求。如果需要频繁在容器的前部或中间位置进行插入和删除操作,并且不关心迭代器的失效问题,可以选择deque。如果对空间占用和迭代器的稳定性有更高的要求,或者只在尾部进行插入和删除操作,可以选择vector

无论选择哪个容器,它们都提供了丰富的成员函数和算法支持,可以方便地进行元素的访问、插入、删除、排序等操作。

双向链表list

双向链表(list)是C++标准库中的一种容器,与vectordeque相比,它有一些独特的特点和用途。

以下是关于双向链表list的一些特点:

  1. 结构:list是由一系列节点构成的,每个节点都包含一个值和指向前一个节点和后一个节点的指针。这种结构使得插入和删除操作在任意位置上都具有常数时间复杂度。

  2. 插入和删除:由于双向链表的节点指针,list在任意位置进行插入和删除操作都非常高效,不会涉及元素的移动和内存重新分配。

  3. 访问:与vectordeque不同,list不支持通过随机索引进行快速访问,因为它没有连续的内存块。要访问list中的元素,需要遍历链表。

  4. 内存占用:由于每个节点都需要额外的指针来连接前后节点,所以相对于vectordequelist在空间上的开销会更大。

  5. 迭代器稳定性:与vector不同,list的插入和删除操作不会使迭代器失效,因为节点的指针不会改变。但是,当对于正在被删除的节点进行解引用时,迭代器会失效。

由于上述特点,list在某些场景下具有一定的优势:

  1. 频繁的插入和删除操作:如果需要频繁地在容器的任意位置进行插入和删除操作,并且对于随机访问性能要求不高,那么list是一个很好的选择。

  2. 迭代器稳定性要求高:当需要在插入和删除操作后仍然保持对元素的有效引用时,list的迭代器稳定性是一个重要的考虑因素。

需要注意的是,由于list在访问元素时需要遍历链表,所以如果需要频繁地进行随机访问操作,或者对存储空间占用有较高要求,可能会选择使用vectordeque

当然,下面是一个使用list的简单代码示例,演示了如何插入、删除和遍历链表中的元素:

#include <iostream>
#include <list>

int main() {
    
    
    std::list<int> myList;  // 创建一个int类型的双向链表

    // 在链表尾部插入元素
    myList.push_back(10);
    myList.push_back(20);
    myList.push_back(30);

    // 在链表头部插入元素
    myList.push_front(5);

    // 遍历输出链表中的元素
    std::cout << "Elements in the list: ";
    for (const auto& element : myList) {
    
    
        std::cout << element << " ";
    }
    std::cout << std::endl;

    // 删除链表中的特定元素
    myList.remove(20);

    // 遍历输出删除后的链表元素
    std::cout << "Elements after removing 20: ";
    for (const auto& element : myList) {
    
    
        std::cout << element << " ";
    }
    std::cout << std::endl;

    return 0;
}

在上面的示例中,首先我们包含了<iostream><list>头文件来使用list容器。然后,我们创建了一个名为myListlist对象,并使用push_back()push_front()函数在链表中插入一些元素。

接着,我们使用一个范围-based for 循环来遍历并输出链表中的元素。注意,list是一个双向链表,所以我们可以使用前向迭代器(const auto& element)来访问元素。

然后,我们使用remove()函数删除了链表中的特定元素(此处为20)。

最后,我们再次遍历并输出删除后的链表元素。

好的,我将为您介绍一些list的具体操作。

  1. 插入元素:

    • push_back(value):在链表尾部插入一个元素。
    • push_front(value):在链表头部插入一个元素。
    • insert(position, value):在指定位置插入一个元素。
  2. 删除元素:

    • pop_back():删除链表尾部的元素。
    • pop_front():删除链表头部的元素。
    • erase(position):删除指定位置的元素。
    • remove(value):删除链表中所有与给定值相等的元素。
  3. 访问元素:

    • front():返回链表头部的元素的引用。
    • back():返回链表尾部的元素的引用。
  4. 遍历元素:

    • 使用循环或范围-based for 循环来遍历链表中的元素,使用迭代器访问每个元素。
  5. 清空容器:

    • clear():清空整个链表,删除所有元素。
  6. 获取大小:

    • size():返回链表中元素的个数。
    • empty():检查链表是否为空。

需要注意的是,由于list是一个双向链表,对于插入和删除操作,它们在任意位置上都具有常数时间复杂度。而对于查找元素,由于访问是基于遍历的,所以时间复杂度为线性。

函数对象

在C++中,函数对象(Function Object),也称为仿函数(Functor),是一种重载了函数调用运算符 operator() 的对象。函数对象可以像普通函数一样被调用,并且可以具有自己的状态和成员变量。

函数对象的主要优势是它们可以像普通函数一样使用,并且可以通过在类中定义函数调用运算符 operator() 来自定义函数的行为。这使得函数对象非常灵活,可以根据需要进行定制。

下面是一个简单的示例,展示了如何创建和使用函数对象:

#include <iostream>

// 定义一个函数对象类
class MultiplyBy {
    
    
public:
    MultiplyBy(int factor) : factor_(factor) {
    
    
    }

    int operator()(int value) {
    
    
        return value * factor_;
    }

private:
    int factor_;
};

int main() {
    
    
    MultiplyBy multiplyByTwo(2);

    // 使用函数对象进行计算
    int result = multiplyByTwo(5);  // 等同于 multiplyByTwo.operator()(5)

    std::cout << "Result: " << result << std::endl;  // 输出结果: 10

    return 0;
}

在上面的示例中,我们定义了一个名为 MultiplyBy 的函数对象类。它接受一个整数作为构造函数的参数,并将其存储为类的成员变量 factor_。然后,我们重载了函数调用运算符 operator(),将输入的值乘以 factor_ 并返回结果。

main 函数中,我们创建了一个名为 multiplyByTwo 的函数对象,并将构造函数参数设置为 2。然后,我们使用函数对象进行计算,将 5 作为参数传递给它,得到结果 10

函数对象非常有用,在许多情况下可以代替普通函数或函数指针,例如在标准库中的算法中使用自定义的操作。

当然,这里是一个更完整的代码示例,演示了使用函数对象来自定义排序规则:

#include <iostream>
#include <algorithm>
#include <vector>

// 定义一个函数对象类
class CompareLength {
    
    
public:
    bool operator()(const std::string& str1, const std::string& str2) {
    
    
        return str1.length() < str2.length();
    }
};

int main() {
    
    
    std::vector<std::string> words = {
    
    "apple", "banana", "cherry", "date"};

    // 使用函数对象进行排序
    CompareLength compare;
    std::sort(words.begin(), words.end(), compare);

    // 输出排序结果
    for (const auto& word : words) {
    
    
        std::cout << word << " ";
    }
    std::cout << std::endl;

    return 0;
}

在上面的示例中,我们定义了一个名为 CompareLength 的函数对象类。它重载了函数调用运算符 operator(),并接受两个字符串作为参数。在函数对象内部,我们比较两个字符串的长度,并返回比较结果。

main 函数中,我们创建了一个 std::vector<std::string> 类型的 words 容器,其中包含一些单词。然后,我们创建了一个名为 compare 的函数对象实例。

使用 std::sort 算法和 compare 函数对象对 words 进行排序。该算法会根据我们定义的排序规则,即字符串长度进行排序。

最后,我们使用循环输出排序后的结果。

执行上述代码,输出将是按字符串长度排序的结果:

date apple banana cherry

这个示例展示了如何使用函数对象来自定义排序规则。这种灵活性使得函数对象在许多场景下非常有用,例如 STL 的算法和容器的自定义操作。

std::setstd::multiset

std::setstd::multiset 都是 C++ 标准库中的关联容器,用于存储元素,并根据元素的值进行自动排序。它们之间的主要区别在于元素的唯一性。

  • std::set:是一个集合容器,其中每个元素的值都是唯一的。即使插入重复的元素,也只会保留一个。它使用红黑树数据结构实现,具有对数时间复杂度的插入、查找和删除操作。

  • std::multiset:是一个多重集合容器,可以存储多个相同值的元素。它允许插入重复的元素,并根据值的顺序进行排序。同样,它使用红黑树数据结构实现,具有对数时间复杂度的插入、查找和删除操作。

以下是一个示例代码,展示了如何使用 std::setstd::multiset

#include <iostream>
#include <set>

int main() {
    
    
    std::set<int> setOfNumbers;
    setOfNumbers.insert(10);
    setOfNumbers.insert(30);
    setOfNumbers.insert(20);
    setOfNumbers.insert(40);

    std::cout << "std::set:" << std::endl;
    for (const auto& num : setOfNumbers) {
    
    
        std::cout << num << " ";
    }
    std::cout << std::endl;

    std::multiset<int> multisetOfNumbers;
    multisetOfNumbers.insert(10);
    multisetOfNumbers.insert(30);
    multisetOfNumbers.insert(20);
    multisetOfNumbers.insert(30);

    std::cout << "std::multiset:" << std::endl;
    for (const auto& num : multisetOfNumbers) {
    
    
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

在上面的示例中,我们首先创建了一个 std::set<int> 类型的容器 setOfNumbers,并插入了一些整数。由于 std::set 中的元素是唯一的,重复插入的元素只会保留一个。然后,我们使用循环打印出集合中的元素。

接下来,我们创建了一个 std::multiset<int> 类型的容器 multisetOfNumbers,并插入了一些整数。由于 std::multiset 允许存储重复元素,我们插入了两个值为 30 的元素。然后,同样使用循环打印出多重集合中的元素。

执行上述代码,输出将是:

std::set:
10 20 30 40
std::multiset:
10 20 30 30

这个示例展示了 std::setstd::multiset 的基本用法和区别。根据您的需求,可以选择适合的容器类型。

map和multimap

std::mapstd::multimap 是 C++ 标准库中的关联容器,用于存储键值对,并根据键的值进行自动排序。它们之间的主要区别在于键的唯一性。

  • std::map:是一个映射容器,其中每个键的值都是唯一的。即使插入具有相同键的多个元素,也只会保留一个,并且后续的插入将覆盖先前的值。它使用红黑树数据结构实现,具有对数时间复杂度的插入、查找和删除操作。

  • std::multimap:是一个多重映射容器,可以存储具有相同键的多个元素。它允许插入具有相同键的多个元素,并根据键的顺序进行排序。同样,它使用红黑树数据结构实现,具有对数时间复杂度的插入、查找和删除操作。

以下是一个示例代码,展示了如何使用 std::mapstd::multimap

#include <iostream>
#include <map>

int main() {
    
    
    std::map<int, std::string> mapOfNumbers;
    mapOfNumbers.insert(std::make_pair(3, "Three"));
    mapOfNumbers.insert(std::make_pair(1, "One"));
    mapOfNumbers.insert(std::make_pair(2, "Two"));
    mapOfNumbers.insert(std::make_pair(4, "Four"));

    std::cout << "std::map:" << std::endl;
    for (const auto& pair : mapOfNumbers) {
    
    
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    std::multimap<int, std::string> multimapOfNumbers;
    multimapOfNumbers.insert(std::make_pair(3, "Three"));
    multimapOfNumbers.insert(std::make_pair(1, "One"));
    multimapOfNumbers.insert(std::make_pair(2, "Two"));
    multimapOfNumbers.insert(std::make_pair(3, "Another Three"));

    std::cout << "std::multimap:" << std::endl;
    for (const auto& pair : multimapOfNumbers) {
    
    
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    return 0;
}

在上面的示例中,我们首先创建了一个 std::map<int, std::string> 类型的容器 mapOfNumbers,并插入了一些键值对。由于 std::map 中的键是唯一的,重复插入具有相同键的元素将覆盖先前的值。然后,我们使用循环打印出映射中的键值对。

接下来,我们创建了一个 std::multimap<int, std::string> 类型的容器 multimapOfNumbers,并插入了一些键值对。由于 std::multimap 允许存储具有相同键的多个元素,我们插入了两个值为 3 的键值对。然后,同样使用循环打印出多重映射中的键值对。

执行上述代码,输出将是:

std::map:
1: One
2: Two
3: Three
4: Four
std::multimap:
1: One
2: Two
3: Three
3: Another Three

这个示例展示了 std::mapstd::multimap 的基本用法和区别。根据您的需求,可以选择适合的容器类型。

容器适配器

容器适配器(Container Adapters)是 C++ 标准库中提供的特殊容器,它们基于现有的容器类型,提供了不同的接口和功能。常见的容器适配器包括栈(stack)、队列(queue)和优先队列(priority_queue)。

这些容器适配器的底层实现可以使用各种不同类型的容器,例如 std::dequestd::liststd::vector。下面我们来详细介绍每个容器适配器的特点和用法:

  1. 栈(stack):

    • 使用 LIFO(后进先出)的方式操作元素。
    • 可以使用 std::dequestd::liststd::vector 作为底层容器。
    • 提供 push()pop()top() 等操作函数。
    • 示例代码:
      #include <iostream>
      #include <stack>
      
      int main() {
              
              
          std::stack<int> stackOfNumbers;
      
          stackOfNumbers.push(1);
          stackOfNumbers.push(2);
          stackOfNumbers.push(3);
      
          while (!stackOfNumbers.empty()) {
              
              
              std::cout << stackOfNumbers.top() << " ";
              stackOfNumbers.pop();
          }
      
          return 0;
      }
      
  2. 队列(queue):

    • 使用 FIFO(先进先出)的方式操作元素。
    • 可以使用 std::dequestd::list 作为底层容器。
    • 提供 push()pop()front()back() 等操作函数。
    • 示例代码:
      #include <iostream>
      #include <queue>
      
      int main() {
              
              
          std::queue<int> queueOfNumbers;
      
          queueOfNumbers.push(1);
          queueOfNumbers.push(2);
          queueOfNumbers.push(3);
      
          while (!queueOfNumbers.empty()) {
              
              
              std::cout << queueOfNumbers.front() << " ";
              queueOfNumbers.pop();
          }
      
          return 0;
      }
      
  3. 优先队列(priority_queue):

    • 类似于队列,但元素按照一定的优先级进行排序。
    • 默认情况下,使用 < 运算符进行比较,也可以自定义比较函数。
    • 可以使用 std::vectorstd::deque 作为底层容器。
    • 提供 push()pop()top() 等操作函数。
    • 示例代码:
      #include <iostream>
      #include <queue>
      
      int main() {
              
              
          std::priority_queue<int> pqOfNumbers;
      
          pqOfNumbers.push(3);
          pqOfNumbers.push(1);
          pqOfNumbers.push(2);
      
          while (!pqOfNumbers.empty()) {
              
              
              std::cout << pqOfNumbers.top() << " ";
              pqOfNumbers.pop();
          }
      
          return 0;
      }
      

这些容器适配器提供了特定的接口和行为,方便我们在特定场景下使用。您可以根据需要选择适合的容器适配器来实现相应的功能。

STL中的算法

STL中的算法大致可以分为以下七类:
1)不变序列算法
2)变值算法
3)删除算法
4)变序算法
5)排序算法
6)有序区间算法
7)数值算法

当然,我可以为您提供一些代码示例来演示不同类别的 STL 算法。下面是一些简单的示例代码:

  1. 不变序列算法(Non-modifying Sequence Algorithms):

    #include <iostream>
    #include <vector>
    #include <algorithm>
    
    int main() {
          
          
        std::vector<int> numbers = {
          
          1, 2, 3, 4, 5};
    
        // 使用 std::find 查找元素
        auto it = std::find(numbers.begin(), numbers.end(), 3);
    
        if (it != numbers.end()) {
          
          
            std::cout << "Element found at index: " << std::distance(numbers.begin(), it) << std::endl;
        } else {
          
          
            std::cout << "Element not found" << std::endl;
        }
    
        return 0;
    }
    
  2. 变值算法(Mutating Algorithms):

    #include <iostream>
    #include <vector>
    #include <algorithm>
    
    int main() {
          
          
        std::vector<int> numbers = {
          
          1, 2, 3, 4, 5};
    
        // 使用 std::fill 将容器中的元素填充为指定值
        std::fill(numbers.begin(), numbers.end(), 0);
    
        // 输出变化后的容器
        for (const auto& num : numbers) {
          
          
            std::cout << num << " ";
        }
        std::cout << std::endl;
    
        return 0;
    }
    
  3. 删除算法(Removing Algorithms):

    #include <iostream>
    #include <vector>
    #include <algorithm>
    
    int main() {
          
          
        std::vector<int> numbers = {
          
          1, 2, 3, 4, 5};
    
        // 使用 std::remove_if 删除满足条件的元素
        numbers.erase(std::remove_if(numbers.begin(), numbers.end(), [](int num) {
          
          
            return num % 2 == 0; // 删除偶数
        }), numbers.end());
    
        // 输出变化后的容器
        for (const auto& num : numbers) {
          
          
            std::cout << num << " ";
        }
        std::cout << std::endl;
    
        return 0;
    }
    
  4. 变序算法(Partitioning Algorithms):

    #include <iostream>
    #include <vector>
    #include <algorithm>
    
    int main() {
          
          
        std::vector<int> numbers = {
          
          1, 2, 3, 4, 5};
    
        // 使用 std::partition 将容器中的元素根据奇偶分为两部分
        auto partitionIt = std::partition(numbers.begin(), numbers.end(), [](int num) {
          
          
            return num % 2 != 0; // 奇数在前,偶数在后
        });
    
        // 输出变化后的容器
        for (const auto& num : numbers) {
          
          
            std::cout << num << " ";
        }
        std::cout << std::endl;
    
        return 0;
    }
    
  5. 排序算法(Sorting Algorithms):

    #include <iostream>
    #include <vector>
    #include <algorithm>
    
    int main() {
          
          
        std::vector<int> numbers = {
          
          5, 2, 4, 1, 3};
    
        // 使用 std::sort 对容器进行排序
        std::sort(numbers.begin(), numbers.end());
    
        // 输出变化后的容器
        for (const auto& num : numbers) {
          
          
            std::cout << num << " ";
        }
        std::cout << std::endl;
    
        return 0;
    }
    

这只是一小部分示例代码,您可以根据需要选择合适的算法并结合具体的容器类型来使用。
当然,我可以为您进一步讲解每个示例代码的作用和使用方法。

  1. 不变序列算法(Non-modifying Sequence Algorithms):
    这个示例使用了 std::find 算法来在容器中查找指定元素。它接受三个参数:待查找的范围的起始迭代器、结束迭代器和要查找的值。如果找到了该值,则返回指向该元素的迭代器;如果没有找到,则返回结束迭代器。

  2. 变值算法(Mutating Algorithms):
    这个示例使用了 std::fill 算法来将整个容器中的元素都填充为指定的值。它接受三个参数:待填充的范围的起始迭代器、结束迭代器和要填充的值。

  3. 删除算法(Removing Algorithms):
    这个示例使用了 std::remove_if 算法来删除容器中满足特定条件的元素。它接受三个参数:待删除元素的范围的起始迭代器、结束迭代器和一个可调用对象(函数或函数对象),用于判断哪些元素应该被删除。这个算法首先会将满足条件的元素移动到容器末尾,然后返回一个指向新的逻辑末尾的迭代器。最后通过使用容器的 erase 成员函数将这些元素从容器中移除。

  4. 变序算法(Partitioning Algorithms):
    这个示例使用了 std::partition 算法来根据指定的条件将容器中的元素进行变序。它接受三个参数:待变序元素的范围的起始迭代器、结束迭代器和一个可调用对象,用于判断哪些元素应该在前面,哪些应该在后面。这个算法会重新排列容器中的元素,使得满足条件的元素在前面,不满足条件的元素在后面。返回值是一个迭代器,指向第一个不满足条件的元素。

  5. 排序算法(Sorting Algorithms):
    这个示例使用了 std::sort 算法对容器中的元素进行排序。它接受两个参数:待排序元素的范围的起始迭代器和结束迭代器。它会按照默认的升序排序规则对元素进行排序。排序后,容器中的元素将按照从小到大的顺序排列。

猜你喜欢

转载自blog.csdn.net/shaozheng0503/article/details/131461883