一.C++的历史与语言介绍
1.语言历史
创作者:丹麦科学家:bjarne strouusstrup(中文:本贾斯特劳斯特鲁普尼)
相关书籍:C++ primer...
大学内容&研究生阶段
C语言:面向过程的语言
simula语言:被誉为世界上第一门面向对象的语言(类体系或继承等)
alogl语言:是一门关于算法结构的语言
1973年:被美国的贝尔实验室看中(C语言,c++,晶体管,无线电等等),工作内容,改进C语言。
1983年:研发出c++语言----c with class -----c++实质是在C语言的基础上发展的(c++院子C语言的自增)
2.C++是C语言的增强
(1)C++是一门静态型语言,在编译时是需要先确定变量的类型,而C语言拥有更严格数据类型检查
例如:
C语言
const int a =10;
a =20; //直接报错
//如果间接更改
const int a =10;
int *p=&a;
*p=30; //只产生警告
C++
const int a =10;
int *p=&a;
*p=30;//直接报错
(2)新增变量引用
引用:是指给已经存在的变量,另取别名和已经存在的变量共用一块内存空间
符号:&(引用符)
格式:数据类型&标识符=初始值;
例如:int b = 10;
int & a = b;//给变量b取一个别名为a,a和b指向的是相同的内存空间
注意事项:a.引用型的变量一旦声明就必须要对其进行初始化工作 b.以及存在的变量(实体)可以用多个引用(别名)c.一个引用只能指向一个已经出栈的变量(实体)
例如:
#include "iostream"
#include "stdio.h"
using namespaec std;
int main()
{
int a=10;
int &b=a;
int &c=a;
b=20;
c=30;
cout<<"a"<<&a<<endl;
cout<<"b"<<&b<<endl;
cout<<"c"<<&c<<endl;
cout<<"a"<<a<<endl;
cout<<"b"<<b<<endl;
cout<<"c"<<c<<endl;
return 0;
}
练习:
使用函数实现两个数的交换(其中一种必须用引用)
#include <iostream>
// 通过值传递进行交换
void swapByValue(int a, int b) {
int temp = a;
a = b;
b = temp;
}
//通过指针传递
void swapByzhizhen(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// 通过引用进行交换
void swapByReference(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int num1 = 10;
int num2 = 20;
std::cout << "初始值:" << "num1 = " << num1 << ", num2 = " << num2 << std::endl;
// 通过值传递进行交换
swapByValue(num1, num2);
std::cout << "通过值传递交换后:" << "num1 = " << num1 << ", num2 = " << num2 << std::endl;
// 通过指针传递进行交换
swapByzhizhen(num1, num2);
std::cout << "通过值传递交换后:" << "num1 = " << num1 << ", num2 = " << num2 << std::endl;
// 通过引用进行交换
swapByReference(num1, num2);
std::cout << "通过引用交换后:" << "num1 = " << num1 << ", num2 = " << num2 << std::endl;
return 0;
}
(3)总结定义和初始化的区别
1)指针:使用*
来声明指针变量,并使用new
操作符在堆上分配内存。指针可以在声明时不初始化,也可以在后续赋值。
int* ptr; // 声明一个整型指针,未初始化
int* ptr = nullptr; // 同样是声明一个整型指针,但初始化为nullptr(空指针)
int* ptr = new int; // 动态分配一个整型变量的内存,并将其地址赋给指针
2)引用:使用&
来声明引用变量,并且在声明时必须进行初始化,初始化后不能再绑定到其他对象。
int num = 10;
int& ref = num; // 声明一个整型引用,并将其绑定到变量num
3)重新赋值:
a.指针:指针变量可以在运行时重新赋值,指向不同的内存地址。
int num1 = 10;
int num2 = 20;
int* ptr = &num1; // 将指针ptr指向变量num1的地址
ptr = &num2; // 可以将指针重新赋值为变量num2的地址
b.引用:一旦引用被初始化绑定到一个对象,就不能再绑定到其他对象,引用只能始终只能引用一个对象。
(4)空间占用
指针:指针本身占用一定的内存空间(通常是4或8字节)来存储地址值。
引用:引用在内存中没有自己的空间,它只是被认为是已经存在的变量的别名。
总结:指针和引用在C++中有着不同的特性和用途。指针可以为空、可以重新赋值、需要使用解引用操作符*
访问内存;而引用必须初始化、不能为null、无法重新绑定、直接通过名称访问所绑定对象。选择使用指针还是引用取决于具体的需求和场景。
常考面试题:指针和引用的区别?
答:a.指针声明时,可以不用初始化。但是引用一旦声明必须初始化
b.指针可以指向多个实体(只要不是:int *const p),但是引用只能指向一个实体
c.编译器会为指针分配内存空间,但不会为引用分配内存空间(引用的空间用的是实体的内存空间)
(5)新增函数带默认值
是指声明一个函数时,若该函数带参数,c++可以对参数先赋予一个初始值(默认值)
例如:c++写法:
void function (int a =50)
{
//函数体
}
//调用
function();
function(30);
注意事项:如果函数用多个参数,并且其中有带默认值的参数,那带默认值的参数放在不带默认值的参数后面
例如:
void function (int a , int b = 30)
................function(30);//报错
................function(30,40);//正确
(6)新增函数重载以及运算符重载
函数重载是指在同一个作用域内,可以定义多个同名函数,但它们的参数列表必须不同。通过函数重载,可以用相同的函数名实现不同参数类型或不同参数个数的函数功能。在调用函数时,编译器会根据参数的类型和数量来确定要调用的具体函数。函数重载可以提高代码的可读性和灵活性,使得程序设计更加方便。
函数重载:是指函数名相同,函数的参数(参数的类型,参数的个数,参数的顺序)不同,即可发生重载
另外介绍一个函数:
内联函数:如果函数是内联函数,那么用户在调用函数时,内联函数直接在调用处展开
内联函数的声明格式:
关键字:inline
格式:
inline 函数返回值类型 函数名 (参数列表)
{
//函数体
}
//例如:
inline void function()
{
//函数体
}
//调用
function()
为什么要用内联函数?相比于普通函数,耗费时间比较少,效率比较高.
1)声明内联函数的要求:内联函数一般只适用于短小,且重复率高的代码
inline int 素描(int a,int b)
{
return a + b;
}
2)内联函数一般不会放置循环体等
C语言不支持函数重载
c++支持函数重载:
//参数个数不同
void function();
void function(int);
//参数类型不同
void function (char);
void function(int);
//参数顺序不同
void function(int char);
void function)(char int );
1)新增智能指针:c++提供智能指针是为了方便用户管理堆内存空间,防止堆内泄漏
2)新增异常处理:c++可以主动产生(抛出)异常,还可以捕获并处理异常
3)新增面向对象:支出类对象,支持继承与多继承,支持泛型编程等等
4)支持命名空间或名字空间:用户可以对内存空间李的一个板块,自定义一个名字,c++会将该板块赋予一个作用域
作用:解决标识符重名的问题
3.环境搭建
主要采用Windows的visual studio 进行代码的编写,visual studio优点:具备了很多的套件功能,架构非常清晰
4.c++的文件名及基本结构
(1)C++的文件名
Windows下:以.cpp为主
Linux下:以.cpp为主,.cxx,.cc等等
默认C++的文件名后缀都是以.cpp为主
(2)C++的结构
1)主程序的文件__main文件:主程序main文件(main.cpp)里,包含了main函数,main函数是应用程序的入口
2)源程序文件__.cpp文件:该文件里主要放置变脸的初始化,函数的实现,类成员函数的实现等等
3)头文件__.h文件:该文件李主要放置变量的声明,函数的声明与定义,类的声明与定义,结构体的声明与定义等等
5.c++程序的编译流
注意的点是c++用的编译器是g++
面试题:C/C++的程序编译流程有哪些?具体做了什么样的工作?
(1)预处理
a.将除源文件以外的内容全部加入到原文件中
b.进行宏替换工作:#define num 10
c.删除掉注释:注释是不会被流贷编译阶段
d.进行条件编译:#idndrf #define #endif等等
(2)编译
a.将用户编写的代码翻译成汇编语言
b.检查用户编写的C/C++代码是否合语法规范
(3)汇编
a.将编译阶段生成汇编语言转换成机器语言(二进制)
(4)链接
a.生成可执行文件
6.了解C++的基本特征性或专业术语
(1)类:是用于描述一类事物(抽象的,不真实存在世界上的),例如,具体描述一类事物的特征和行为
(2)对象:是类的实例化或者具体化(真真实实存在于世界中)
(3)特征&属性:是指变量
(4)行为&方法:是指函数
(5)封装:将变量和变量的相关操作或动作封装在一起的过程,称为“封装”
(6)继承&多继承:一个新的类共享了其他已经存在的类的属性和方法,称之为“继承”
(7)多态:多种形态,C++是指一种接口多中实现方法
(8)静态联编(静态绑定):是指一个表达式或函数的地址在编译时就已经确定
(9)动态联编(动态绑定):是指一个表达式或函数的地址在运行时才确定,动态联编一般涉及到虚函数
7.C++的第一个程序
#include "iostream"//包含了iostream:是一个输入输出流类
using namespace std;
int main() {
cout << "hello world" <<endl; //this is first C++ code
return 0;
}
标注解释:
#include "iostream" //包含了iostream:是一个输入/输出流类
//#include <iostream>
//用户想要向控制台输入内容或输出内容,需要包含iostream类库
//iostream:istream(输入相关)和ostream(输出相关)类
// C++提供的输入/输出对象:cout、cerr、clog、cin等等
// iostream类里实例化了istream类(cin)和ostream类(cout,cerr,clog)
//注:如果用户使用C++标准库里的内容的话,#include包含头文件就不需要加后缀.h
//如果用户自己封装的库,那么#include包含头文件就需要加上.h
//#include包含头文件时使用符号“”和<>的区别
//符号:“”:编译器在执行应用程序时,首先会在当前项目里寻找库文件,然后去标准库里寻找库文件
//符号:<>:编译器在执行应用程序时,首先会在当前标准库里寻找库文件,然后去项目里寻找库文件
using namespace std;//使用命名空间(名字空间)std(std是C++给用户提供的一个名字空间)
//名字空间std里包含了相关的输入/输出流对象以及其他标准库生成对象等等
int main()//应用程序的入口
{
cout << "hello world!" << endl;//this is first C++ code
//cout:输出流对象:用户如果想要在控制台打印/输出消息,就可以用cout进行输出
//endl:结束流对象:如果用户消息输出完,就可以用endl结束
//符号:<<和>>:后面讲的运算符重载
//符号:<<:是指数据流动的方向(数据流出),数据从内存里取出来放入输出缓冲区里,再从输出缓冲区里取数据出来输出到控制台
//符号:>>:是指数据流动的方向(数据流入),数据从控制台输入到输入缓冲区里,再从输入缓冲区里输入到内存里
//字符串:"hello world":输出的数据,输入/输出流里数据的类型可以不用用户指定
return 0; //函数的返回值
}
二、内存模型与名字空间
1、名字空间
引入:
大家在上学时,班级里有没有同名同姓的人?张三和张三,如何区分同名同姓的人?胖瘦、男/女、学号、区域等等
程序:
例如:在程序里用户定义了两个一模一样的标识符,怎么解决两个标识符都存在于内存中?
利用名字空间进行解决:
将两个标识符放在不同的区域里即可解决标识符重名的问题
名字空间(命名空间):
是指从全局范围内取出一块区域并对其进行命名,C++会将该板块赋予一个作用域
2、名字空间的命名格式
关键字:namespace
(1)有名名字空间
命名格式:
namespace 标识符 //该标识符就是名字空间的名称
{
//变量
//函数
//结构体
//类
......
}
例如:
namespace zhang//zhang为命名空间的名称,标识符
{
int a=10;
void function()
{
cout << "hello world" << endl;
}
}
#第一种访问命名空间里的内容:
符号::: //域作用符号
格式:
命名空间名称 :: 访问的内容
例如:
zhang::a //访问名字空间zhang里的变量a
zhang::function()//访问名字空间zhang里的函数function
#第二种访问命名空间里的内容:
符号:::
关键字:using
格式:
using 命名空间名称::命名空间里的内容 //告诉编译器,如果后面出现指定的命名空间里的内容,则可以直接进行访问
例如:
using zhang :: a;
cout<<"a="<<a<<endl;
#第三种访问命名空间里的内容:
符号:::
关键字:using、namespace
格式:
using namespace 命名空间名称 //告诉编译器,如果后面出现命名空间里的内容,则可以直接进行访问
例如:
using namespace zhang;
cout<<"a="<<a<<endl;
function();
(2)名字空间的嵌套
命名空间也可以像C语言for循环等等发生嵌套
格式:
namespace 标识符1
{
//变量
//函数
//类
namepsace 标识符2
{
//变量
//函数
//类
.....
}
}
例如:
namespace zhang
{
int a=20;
void function()
{
cout << "hello world" << endl;
}
namespace zhang1
{
int a = 30;
void function()
{
cout << "你好,世界" << endl;
}
}
}
访问嵌套的名字空间里的内容:
格式:
命名空间1:: 命名空间2 :: ..... :: 命名空间里的内容
例如:
zhang::zhang1::a //访问命名空间zhang1里的变量a
zhang::zhang1::function() //访问命名空间zhang1里的函数function
(3)命名无名名字空间
格式:
namespace
{
//变量
//函数
//类
}
//例如:
namespace
{
int a = 40;
void function()
{
cout << "hello world" << endl;
}
}
注:
如果在全局的命名空间没有名称,那么其实质无名名字空间的作用域就是全局(等于全局)
访问格式:
::无名的名字空间里的内容
例如:
::a
::function();
3、内存模型
介绍:
动态内存分配
动态内存分配:
是指程序在运行时才分配内存空间,称之为“动态内存分配”,C语言提供了malloc/free函数用于动态内存空间的开辟和释放,C++提供了另外的操作符:new/delete用于动态内存空间的开辟和释放(new:用于空间的申请 delete:用于空间的释放)
面试题:malloc/free和new/delete的区别?
答:malloc和free是C语言中的内存分配和释放函数,而new和delete是C++中的操作符,用于完成相同的任务。以下是它们之间的区别:
类型信息:malloc和free是无类型的函数,只能分配和释放字节大小的内存块。而new和delete是类型相关的操作符,可以根据所需的类型来进行内存分配和释放,并会自动调用构造函数和析构函数。
内存大小:malloc需要手动指定要分配的内存块的大小(以字节为单位),而new会根据具体的类型自动计算所需的内存大小。
构造函数和析构函数的调用:使用new关键字分配内存时,会自动调用对象的构造函数来初始化对象。而使用malloc分配的内存块不会调用构造函数,需要手动调用构造函数进行初始化,否则对象的状态可能是未定义的。同样,delete操作符会自动调用对象的析构函数进行资源释放,而free函数不会调用析构函数,需要手动调用析构函数来释放资源。
异常处理:new操作符在分配内存失败时会抛出std::bad_alloc异常,以便程序可以捕获并处理异常情况。而malloc在分配失败时会返回空指针(NULL),需要进行手动的空指针检查。
数组分配:new操作符可以用于分配单个对象的内存,也可以用于分配对象数组的内存。而malloc只能分配字节大小的内存块,不会调用构造函数来初始化对象数组。
总的来说,new和delete提供了更方便、类型安全且面向对象的内存管理方式,适用于C++中的对象。而malloc和free是C语言的函数,可以用于C++中,但在C++中使用new和delete会更推荐,因为它们提供了更多的功能和类型安全性。
(1)使用new开辟变量地址空间
n操作符:new
格式:
数据类型名 * 标识符 =new 数据类型名
例如:
int * p = new int ; //向堆区开辟int大小的空间,并用p指向该空间
或
int *p ;
p=new int;
注:
1)使用new开辟变量地址空间时,同时进行初始化操作
格式:
数据类型名 * 标识符 = new 数据类型名(初始值);//开辟空间的同时,向指定的空间存放初始化值
例如:
int * p = new int (50); //向p指向的堆空间存放初始值50,初始值的类型与数据类型一致
2)数据类型名:可以是基本的数据类型,也可以是自定义的类型(例如:类类型)
释放new开辟的变量地址空间:
操作符:delete
格式:
delete 标识符 //释放标识符指向的堆空间
例如:
delete p ;
(2)使用new开辟数组地址空间
操作符:new
格式:
数据类型名 * 标识符 = new 数据类型名 [数组的大小] ;//从堆区开辟数组地址空间
例如:
int * p = new int[5] ; //向堆区开辟5*int大小的空间,并用p指向该空间
或
int *p;
p= new int[5] ;
注:
1)使用new开辟数组地址空间时,同时进行初始化操作
格式:
数据类型名 * 标识符 = new 数据类型名 [ 数组的大小 ]{初始值1,初始值2,初始值3,...........}
例如:
int * p = new int [5]{1,2,3,4,5};
或
int *p;
p= new int[5]{1,2,3,4,5} ;
释放new开辟的数组地址空间:
操作符:delete
格式:
delete [] 标识符 //释放标识符所指向的堆空间
例如:
delete [] p;
总结:malloc/free和new/delete的区别?(面试题)
(1)malloc,free是一个函数,而new,delete是一个操作符
(2)malloc申请空间是需要手动计算空间的大小,而new不需要手动计算
(3)malloc申请空间时需要强制转换,new不需要
(4)malloc申请空间时没有对空间初始化的工作,而new申请空间时可用对申请空间进行初始化工作
(5)malloc申请空间时适用于基本数据类型,而new申请空间时可用是自定义类型(也可以应用于基本数据类型)
(6)malloc只会申请空间,不会做其他工作,free只会释放申请的空间,也不会做其他工作,而new不仅申请空间还会执行类的构造函数工作,delete不仅释放空间还执行了类的析构函数的工作
4.C++里的作用域
(1)局部作用域:变量或表达式存在于函数体,循环图的内部,将函数体,循环体的内部称之为“局部作用域”,一旦变量或表达式超过了局部作用域,那么他的生命周期就结束了
例如:
void function ()
{
int a = 10;//a是属于局部作用域的变量
}
a = 20;//报错,a的生命周期结束了
(2)全局作用域:变量或表达式不存在于函数体,循环体的内部,将函数体,循环体等以外的区域称之为“全局作用域”,属于全局作用域里的变量会表达式对于这个文件都是有效(可见)
例如:
int a = 10;
void function ()
{
a = 20 ;
// 访问a
}
(3)命名空间作用域
命名空间作用域是属于全局作用域的也不大,如果变了或表达式属于命名空间作用域里的内容,那么超出了命名空间,表达式或变量的生命周期与会随之结束
例如:
namespace st
{
a = 20;
}
a = 30;//报错,变量a的生命周期即结束
(4)类作用域
类作用域也是属于全局作用域的一部分,如果变量或表达式存在于类中,改变了或表达式如果超过类作用域,那么生命周期也会结束
三.类和对象
面向对象编程:C++,C#,java,Python
面向对象:在面向对象的语言里,一切皆对象,是指将功能封装成对象,之后就利用对象去事项想要的功能。
面向过程编程:C语言等等
面向过程:在面向过程的语言里,将功能按步骤一步一步用代码来实现
1.类
是描述一类事物的特征和行为(如果描述事物,可用理解为该事物不存在(不是实体)),例如:用类描述人,人具备的特征,,年龄,性别,姓名,身高等等,具备的行为:吃饭,睡觉,走路,写字等等。
关键字:class
格式:
class 类名(标识符)
{
//描述事物的特征和行为
};
注:不能超过作用域,否则就失效
例如:描述一个人类
class people
{
// 描述人的特征
int age;
string sex;
char buf[20];
.........//根据实际需要添加属性
//描述人的行为
void sleep()
{
//用类来进行封装
}
void run()
{
}
viod play_game()
{
}
};
特征&属性:变量,如果把变量放到类里面,将该变量称之为“成员变量”
行为&方法:函数,如果吧函数放到类里面,将该函数称之为“成员函数”,更习惯于称为成员方法
(1)类成员函数的实现__.cpp里实现(注:直接在类内部实现成员函数也支持)
格式:
函数返回值类型 类名::函数名(参数列表)
{
}
例如:
void poeple::sleep()//在类的外部实现类的成员函数
{
}
练习:描述一个算法类,该算法类包含:计算加法,减法,除法,乘法功能。
class MyAlgorithm
{
public:
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
int multiply(int a, int b)
{
return a * b;
}
float divide(float a, float b)
{
if (b != 0)
{
return a / b;
}
else
{
// 处理除数为零的情况
qDebug() << "Error: Division by zero!";
return 0;
}
}
};
2.对象
是指将类进行实例化(该类是真真实实存在于内存中)的过程,称之为“对象”,只有将类进行实例化之后,才能使用类的成员(成员变量和成员方法)
然后对垒进行实例化:
(1)在栈区对类进行实例化
格式:类名 标识符;//这里的标识符,就称之为“对象”
例如:people zhangsan;//将people类实例化为张三对象,张三就具备people类的属性方法
C语言定义变量的格式:基本数据类型 标识符
例如:int a;
c++实例化类的格式:类类型(自定义的类型) 标识符
例如:people zhangsan
(2)在堆区进行实例化
格式:数据类型名 * 标识符 = new 数据类型名;
例如:
People * people = new people;// 在堆区进行实例化,对象名“people” ,注意:及时释放内存(释放people指向的空间delete 对象名)
3.类成员的访问
是指使用类的成员(类的成员变量和成员方法),是通过对象(实体)进行访问类成员的访问方式是根据类的实例化而定:
(1)对栈区的对象的成员进行访问
符号:.
格式:: 对象名 . 成员
例如:
People people ;
people . age ;//访问类的age属性
people .sleep ();//访问类的class方法
(2)对堆区的对象的成员的访问
符号:->
格式:对象名 -> 成员 ;
例如:
People * people = new People ;
people ->age ;//访问类的age属性
people->sleep(); //访问类的sleep方法
4.类的成员访问修饰符
C++提供了三种类的成员访问修饰符public(共有),protectd(受保护),private(私有的),C++引入类的成员访问修饰符是为了增加类的安全性
(1)public修饰符
如果类的成员放置在public的属性下,那么类的成员就具备公共属性(类的成员是公开的)
格式:在类中:
public:
成员1;//成员1具备公开属性
成员2;//成员2具备公开属性
..................
注:当最后一个成员是类后括号“}”或者是另外一个访问修饰符之间的成员都是属于public:下的成员
a.public下的成员对于类的外部都是可见的
b.public下的成员对于类的内部都是可见的
c.public下的成员对于类的友元也是可见的
d.public下的成员对于派生类(子类)是可见的
(2)protected修饰符
是指如果类的成员放置在protect属性下,那么类的成员就具备受保护的属性
格式:在类中:
private:
成员1;//成员1具备 private属性
成员2;//成员2具备 private属性
..................
注:当最后一个成员是类后括号“}”或者是另外一个访问修饰符之间的成员都是属于 protected:下的成员
a. protected下的成员对于类的外部都是不可见的
b. protected下的成员对于类的内部都是可见的
c. protected下的成员对于类的友元也是可见的
d. protected下的成员对于派生类(子类)是可见的
(3)private修饰符
是指如果类的成员放置在private属性下,那么类的成员就具备私有的属性
格式:在类中:
private:
成员1;//成员1具备 private属性
成员2;//成员2具备 private属性
..................
注:当最后一个成员是类后括号“}”或者是另外一个访问修饰符之间的成员都是属于private:下的成员
a. private下的成员对于类的外部都是不可见的
b.private下的成员对于类的内部都是可见的
c. private下的成员对于类的友元也是可见的
d. private下的成员对于派生类(子类)是不可见的
总结:
a.当类的内部没有任何的访问修饰符,那么类的成员默认是不可见的(放在private下)
b.实际开发中,一般将属性(变量),放置在private修饰符下
c.声明类的时候,默认类的名称首字母是大写的,变量和函数首字母是小写,其他单词的首字母是大写
5.构造函数2
是指C++类内体的一种特殊函数,即每一个类都会有一个构造函数,如果用户没有显示的将构造函数写出来,那么编译器就会生成一个隐试的改造函数
构造函数的格式:
类名(参数列表)
{
//构造函数体
}
例如:
class People
{
public:
People(参数列表)
{
//构造体函数
}
}
注:
(a)构造函数没有返回值,而普通成员函数有返回值
(b)构造函数名是类名,而不同函数的名称是任意的标识符
(c)构造函数创建类的对象时会被自动调用(类的对象参数一定要与构造函数的参数进行匹配),而普通成员函数需要用户手动调用
(d)构造函数的作用一般是初始化类的成员变量
(e)构造函数可以发生重载
(1)构造函数初始化类的成员变量的方式
a.像普通的函数之间传参即可
例如:
class People{
public:
People (int a,int b)
{
num 1=a;
num 2=b;
}
private:
int num 1;
int num 2;
}
b.使用构造函数的初始化列表形式
格式:
class 类名
{
public:
类名():成员变量1(初始值1),成员变量2(初始值2),......
{
}
}
例如:
class People
{
public:
People():num1(10),num2(20)
{
}
private:
int num1;
int num2;
}
练习:
下面是一个类的测试程序,请设计出能使用下面测试程序的类
int main()
{
test a , b;
a.Init(68,55)
b.Init(18,36)
a.Print();
b.Print();
return 0;
}
其输出结果:
68-55=13
18-36=-18
答:
#include "iostream"
using namespace std;
class lianxil {
private:
int num1;
int num2;
public:
void Init(int n1, int n2)
{
num1 = n1;
num2 = n2;
}
void Print()//函数为空
{
int diff = num1 - num2;
std::cout << num1 << "-" << num2 << "=" << diff << std::endl;
}
};
int main()
{
lianxil a, b;
a.Init(68, 55);
b.Init(18, 36);
a.Print();
b.Print();
return 0;
}
练习:用类模拟银行账户,用户可用向账户里存钱和取钱等功能
(1)新建银行账户
(2)用户可用向银行存钱
(3)用户可用向银行取钱
(4)用户可用通过账户等信息查询余额
#include <iostream>
class BankAccount
{
private:
std::string accountNumber;//银行账户号码
double balance;//余额
public:
// 构造函数
BankAccount(std::string accNumber, double initialBalance)
{
accountNumber = accNumber;
balance = initialBalance;
}
// 存款
void deposit(double amount)
{
balance += amount;
}
// 取款
void withdraw(double amount)
{
if (balance >= amount)
{
balance -= amount;
} else {
std::cout << "余额不足!" << std::endl;
}
}
// 查询余额
double getBalance()
{
return balance;
}
};
int main()
{
// 创建银行账户对象并初始化
BankAccount account("123456789", 1000.0);
// 存钱
account.deposit(500.0);
// 取钱
account.withdraw(200.0);
// 查询余额
double balance = account.getBalance();
std::cout << "当前余额:" << balance << std::endl;
return 0;
}
(2)调用类的构造函数的方式
a、实例化类的对象的时候,自动调用类的构造函数-----显示调用
b、直接使用类调用构造函数----隐式调用
例如:
People(参数);
面试题:
普通成员函数与构造函数的区别?
类与结构体的区别?
普通成员函数与构造函数的区别在于它们的作用和调用时机有所不同。
构造函数(Constructor): 构造函数是一种特殊的成员函数,用于创建对象时进行初始化操作。它具有以下特点:
构造函数的名称必须与类名相同。
构造函数没有返回类型,包括void,因为它的任务是初始化对象而不是返回值。
构造函数在创建对象时会自动被调用,无需显式地调用。
普通成员函数: 普通成员函数可以执行类中定义的操作或行为,它们并不用于对象的创建和初始化。普通成员函数具有以下特点:
普通成员函数的定义和调用与其他函数相似,需要通过对象来调用。
普通成员函数可以访问类中的成员变量和其他成员函数,具有更大的灵活性。
普通成员函数可以用于完成类的数据处理、状态修改或其他操作。
类与结构体的区别主要体现在默认访问权限和默认继承方式上,其它方面两者基本相同。
类(Class):
默认访问权限:私有(private)。
默认继承方式:私有继承(private inheritance)。
结构体(Struct):
默认访问权限:公有(public)。
默认继承方式:公有继承(public inheritance)。
除此之外,类和结构体的定义方式、成员函数和成员变量的使用等方面基本相同。在实际编程中,一般情况下可以根据需求选择使用类或结构体。
6、析构函数
是指C++里的一种特殊的函数,析构函数的作用是释放类的对象所占用的资源,如果用户没有显示的写出析构函数,那么编译器也会自动生成一个默认的隐式的析构函数,如果用户显示的写出析构函数,那么编译器不会自动生成默认的析构函数
格式:
~ 类名()
{
//析构函数体
}
例如:
~People()
{
//释放对象资源
}
析构函数的调用时机:
a、当类的对象使用完成时,会自动调用析构函数
b、当使用delete类的对象时,也会调用析构函数
注:
a、析构函数无返回值
b、析构函数会在类的对象使用完成时会调用析构函数
c、析构函数不能带有参数
d、析构函数不能发生重载
7、拷贝构造函数
拷贝:
copy,复制
一个对象的成员用另外一个对象来进行初始化(将已经存在对象重新复制一份)
如果用户没有显示的写出拷贝构造函数,那么编译器也会默认生成一个隐式的拷贝构造函数;如果用户显示的写出拷贝构造函数,那么编译器不会生成一个隐式的拷贝构造函数
拷贝构造函数的格式:
类名 (const 类名 & obj)
{
//拷贝构造函数体
}
例如:
People (const People & obj)
{
//拷贝构造函数体
}
拷贝:
将已经存在的对象的成员名和数据重新拷贝了一份
注:如果类里存在指针对象并且开辟空间,这时候拷贝会出现什么问题?堆空间被重复释放,造成程序崩溃的现象
(1)浅拷贝(浅拷贝一般使用的是隐式的拷贝构造函数)
浅拷贝,是指用已经存在对象初始化新的对象时,只拷贝变量名和值(注:没有指针向堆空间开辟空间的情形)
(2)深拷贝(深拷贝一般使用的是显示的拷贝构造函数)
深拷贝,是指类体里含有指针对象并且开辟了堆空间时,用已经存在对象初始化新的对象时,深拷贝不仅会拷贝指针对象名,还会重新复制一份指针对象指向地址
如何解决堆空间被重复释放,造成程序崩溃的现象?-----深拷贝(利用深拷贝重新申请堆空间)
当类里面如果没有指针对象开辟堆空间,那么浅拷贝和深拷贝没有区别
注意区分结构体和类的区别:结构体的成员没有限制,类对成员加了限制符。
笔试题:
编写一个Person类,包括:
1、普通数据成员:姓名,性别,年龄。
2、三个构造函数:无参数构造函数,有参数构造函数(参数:姓名的指针,年龄,性别),拷贝构造函数。
3、析构函数。
4、输出人员信息函数print()。
编写main()函数,分别调用三种构造函数,创建三个对象P1、P2、P3,其中P3的创建是用P2通过深拷贝复制得到的。
答:
#include <iostream>
#include <cstring> // 用于字符串操作
class Person {
private:
char* name;
char gender;
int age;
public:
// 构造函数:无参数构造函数
Person() {
name = nullptr;
gender = '\0';
age = 0;
}
// 构造函数:有参数构造函数
Person(const char* namePtr, int personAge, char personGender) {
// 使用动态内存分配来存储姓名
int nameLength = strlen(namePtr);
name = new char[nameLength + 1];
strcpy(name, namePtr);
gender = personGender;
age = personAge;
}
// 拷贝构造函数
Person(const Person& other) {
// 先将属性值复制
age = other.age;
gender = other.gender;
// 再进行深拷贝,分配新的内存并复制姓名
int nameLength = strlen(other.name);
name = new char[nameLength + 1];
strcpy(name, other.name);
}
// 析构函数
~Person() {
if (name != nullptr) {
delete[] name;
}
}
// 输出人员信息函数
void print() const {
std::cout << "姓名:" << name << std::endl;
std::cout << "性别:" << gender << std::endl;
std::cout << "年龄:" << age << std::endl;
}
};
int main() {
// 调用无参构造函数创建对象P1
Person P1;
P1.print();
// 调用有参构造函数创建对象P2
const char* namePtr = "张三";
int age = 20;
char gender = '男';
Person P2(namePtr, age, gender);
P2.print();
// 调用拷贝构造函数创建对象P3
Person P3(P2);
P3.print();
return 0;
}
在上面的示例代码中,首先定义了一个Person类,包含了所需的数据成员和成员函数。构造函数根据要求实现了无参构造函数、有参构造函数和拷贝构造函数。析构函数用于释放动态分配的内存。print()函数用于输出人员信息。
在main()函数中,通过调用不同的构造函数来创建对象P1、P2和P3,并分别进行输出。其中P3是通过P2进行深拷贝得到的。
8.this 指针
this指针是C++提供的一种特殊指针,this指针是普遍存在于普通函数里面(即类的成员函数的参数默认是带有this指针类型的参数),并且this指针是隐式的,该this指针是指向当前类的(this指针就是当前类)
如何使用:
class People
{
public:
void function()//function函数是类People的成员类,该函数带有this指针,并且this指针指向People
{
this ->a;//通过this指针访问类的成员a
}
}
9.友元
类似于现实世界中的朋友关系
C++提供友元概念是为了给类的外部提供一个访问类的成员接口
C++里的友元分为:友元类和友元函数
(1)友元函数
将一个普通函数声明为类的友元函数,称为“类的友元函数”,注,友元不是类的成员函数,友元函数不具备this的指针
声明友元函数的格式:
关键字:friend
格式:在类中声明友元函数:
class 类名
{
public:
friend 返回值类型 函数名 (参数列表);//将函数声明为类的友元
}
例如:
#include <iostream>
class MyClass
{
private:
int privateMember;
public:
MyClass() : privateMember(42) {}
friend void friendFunction(); // 声明友元函数
};
void friendFunction()
{
MyClass obj;
cout << "访问友元函数中的私有成员:" << obj.privateMember <<endl;
}
int main()
{
friendFunction(); // 直接调用友元函数
return 0;
}
练习:实现一个计算立方体的体积和表面积的类:class Cube
要求:a.立方体的边长必须要进行初始化的工作
b.立方体的边长要设置为类的私有属性
c.调用相关的函数能实现立方体的边长的设置,表面积(边长x边长x6)及体积的输出
d.不管在什么位置都能查询立方体的边长,表面积(SurfaceArea)及体积
(2)友元类
将一个类声明为另外一个类的友元,称之为“友元类”
友元类的声明格式:
关键字:friend
格式:
class A
{
public:
}
class B
{
public:
friend class A;//类A是类B的友元,可以通过类A访问类B的私有成员
}
练习:使用类和对象以及友元实现算术表达式的求值
例如:2+5+6-4+4-40的和
思路:(1)构建一个加法类和一个减法类
(2)加法类实现加法运算,减法类实现减法运算
(3)加法类和加法类中各自定义两个参与运算的私有变量
(4)声明友元,实现相关的表达式求值
class Cube
{
public:
Cube(double sideLength)
{
setSideLength(sideLength);
}
void setSideLength(double sideLength)
{
m_sideLength = sideLength;
}
double getSideLength() const
{
return m_sideLength;
}
double calculateSurfaceArea() const
{
return 6 * m_sideLength * m_sideLength;
}
double calculateVolume() const
{
return m_sideLength * m_sideLength * m_sideLength;
}
private:
double m_sideLength;
};
在这个例子中,我创建了一个名为Cube
的类,其中包含私有属性m_sideLength
表示立方体的边长。通过构造函数进行初始化,并提供了setSideLength
函数用于设置边长,getSideLength
函数用于查询边长,calculateSurfaceArea
函数用于计算表面积(边长x边长x6),以及calculateVolume
函数用于计算体积(边长x边长x边长)。
您可以使用这个类来创建立方体对象并进行相关计算操作。以下是一个示例代码:
int main()
{
Cube cube(5.0); // 创建一个边长为5的立方体对象
double sideLength = cube.getSideLength();
double surfaceArea = cube.calculateSurfaceArea();
double volume = cube.calculateVolume();
qDebug() << "边长:" << sideLength;
qDebug() << "表面积:" << surfaceArea;
qDebug() << "体积:" << volume;
return 0;
}
在这个示例中,我首先创建了一个边长为5的立方体对象cube
,然后使用它的成员函数获取边长、计算表面积和体积,并将结果打印出来。
通过调用setSideLength
函数,您可以在任何位置设置立方体的边长,然后调用相关函数获取边长、表面积和体积的值。
10.static关键字
C++类似于C语言也可以用static将类的成员声明为静态类型
如果将可以的成员声明为static类型,那么静态成员拥有者是整个类,不是一个对象特有的
如果在类的外面将变量或函数名声明为静态类型就与C语言类似
(1)将类的静态成员声明为static类型
关键字:static
声明格式:class类名
{
public :
static int a;//变量a是静态类型,对于整个类共享
}
注:如果将类的成员声明为静态类型,那么一般是选择在类外静态成员变量进行初始化工作
类外对静态成员变量初始化的格式:
变量的数据类型 类名 :: 变量名 = 初始值;
例如:
int People :: a = 10;//初始化
访问类的静态成员变量:
1)使用类的对象访问类的静态成员
2)直接使用类访问类的静态成员
格式:
类名::静态成员名
例如:
People ::a
(2)static声明类的静态成员函数
格式:
static 函数返回值 函数名 (参数列表)
{
}
例如:
static void function() //静态成员函数
{
cout << "hello world" << endl;
}
注:
1)静态成员函数中没有this指针
2)静态成员函数中不能调用非静态成员,但是可以调用静态成员
3)非静态成员函数既可以访问非静态成员,也可以访问静态成员
11、const关键字
const关键字也可以用于修饰类的成员,表示类的成员是只读的性质
(1)修饰类的成员变量
例如:
class People
{
const int a =10; //表示a是只读
}
(2)修饰类的成员函数
如果用const修饰类的成员函数,那么成员函数中不允许修改类的成员
例如:
class People
{
public:
void func()const
{
//表示func函数对于类是只读,即不可以在该函数修改类的成员
}
}
四、标准流(输入流和输出流、文件流等等)
1、流
通信的数据向水流一样从一个地方流向另外一个地方,称之为“流”
2、输入流
数据是从控制台或者显示器流向内存的过程,称之为“输入流”,为了防止数据在流动的过程中发生交叉,引起数据错误,一般编译器会分配一个缓冲区(缓冲区的大小为:512字节,可以使用宏:BUFSIZ),用于存放输入流中的数据
3、输出流
数据是从内存流向控制台或者显示器的过程,称之为“输出流”,为了防止数据在流动的过程中发生交叉,引起数据错误,一般编译器会分配一个缓冲区(缓冲区的大小为:512字节,可以使用宏:BUFSIZ),用于存放输出流中的数据
4、标准的输入流和标准的输出流
类库:
输入流:
istream: i:input stream:流
输出流:
ostream:o:output stream:流
输入输出流:
iostream:
iostrem类库里定义了一个命名空间:std
命名空间里实例化了istream类和ostream类
istream类实例化的对象:cin
ostream类实例化的对象:cout
5、标准的输出流
类:
ostream类
格式:
ostream类的对象<<数据类型;
或
ostream类的对象 << 数据类型1 <<数据类型2<<数据类型3<<..................
例如:
ostream &cout << 数据类型;
ostream &cout << 数据类型1 <<数据类型2<<数据类型3<<..................
ostream &cerr << 数据类型;
ostream &cerr << 数据类型1 <<数据类型2<<数据类型3<<..................
ostream &clog << 数据类型;
ostream &clog << 数据类型1 <<数据类型2<<数据类型3<<..................
符号:
“<<”,在C语言里是移位运算符,在C++里对该符号进行重载,重载之后该符号就演变为数据的流动方向(数据是从内存里流向控制台或显示器)
C语言:
20 << 2 ; //在运算符“<<”的两边的表达式要求是基本的数据类型
C++:
cout << 10 ;//C++里对“<<”进行重载之后,不仅适用于基本的数据类型,还适用于自定义的类型
符号“<<”重载的数据类型:
ostream &cout << (int);
ostream &cout << (char);
ostream &cout << (char *);
ostream &cout << (const char *);
ostream &cout << (long int );
ostream &cout << (doube long);
ostream &cout << (long long int);
...
ostream &cout<< 基本的数据类型;
或
ostream &cerr << (int);
ostream &cerr << (char);
ostream &cerr << (char *);
ostream &cerr << (const char *);
ostream &cerr << (long int );
ostream &cerr << (doube long);
ostream &cerr << (long long int);
...
ostream &cerr 基本的数据类型;
或
ostream &clog << (int);
ostream &clog << (char);
ostream &clog << (char *);
ostream &clog << (const char *);
ostream &clog << (long int );
ostream &clog << (doube long);
ostream &clog << (long long int);
...
ostream &clog 基本的数据类型;
适用输出流对象什么时候才会向控制台输出数据?
(1)当输出流对象遇到endl时,就会将输出缓冲区里的数据输出到显示器或控制台
(2)当输出缓冲区里的数据个数大于BUSIZ的时候,编译器也会将输出缓冲区里的数据输出到控制台或显示器
(3)当输出流对象结束时,也会将输出缓冲区里的数据输出到控制台或显示器
6、标准的输入流
类库:
istream类
格式:
输入流对象 >> 变量;
或
输入流对象>>变量1>>变量2>>变量3>>.........
或
输入流对象>>变量1
>>变量2
>>变量2
............
例如:
istream & cin >>变量1
或
istream & cin >>变量1>变量2>>变量3>>.....
注:
用户输入完数据之后,要告诉编译器输入完成------使用回车即可
7、输出流格式控制
(1) 用控制符控制输出格式
应当注意:这些控制符是在头文件iomanip中定义的,因而程序中应当包含头文件iomanip。通过下面的例子可以了解使用它们的方法,
例2 用控制符控制输出格式,
[cpp] view plain copy
#include <iostream>
#include<string>
#include <iomanip> //不要忘记包含此头文件
using namespace std;
int main()
{
int a;
cout<<"input a:";
cin>>a;
cout<<"dec:"<<dec<<a<<endl; //以上进制形式输出整数
cout<<"hex:"<<hex<<a<<endl; //以十六进制形式输出整数a
cout<<"oct:"<<setbase(8)<<a<<endl;//以八进制形式输出整数a
string pt= "China"; //pt指向字符串”China”
cout<<setw(10)<<pt<<endl; //指定域宽为10,输出字符串
cout<<setfill('*')<<setw(10)<<pt<<endl;//指定域宽10,输出字符串,空白处以“*”填充
double pi=22.0/7.0; //计算pi值
cout<<setiosflags(ios::scientific)<<setprecision(8);//按指数形式输出,8位小数
cout<<"pi="<<pi<<endl; //输出pi值
cout<<"pi="<<setprecision(4)<<pi<<endl;//改为4位小数
cout<<"pi="<<setiosflags(ios::fixed)<<pi<<endl;//改为小数形式输出,精度为4
cout<<"pi="<<fixed<<pi<<endl;//fixed确定小数点后精度为4
return 0;
}
运行结果如下:
inputa:34 (输入a的值)
dec:34 (十进制形式)
hex:22 (十六进制形)
oct:42 (八进制形式)
China (域宽为10)
***** China (域宽为10,空白处以'*'填充)
pi=3.14285714e+00 (指数形式输出,8位小数)
pi=3.1429e+00) (指数形式输小,4位小数)
pi=3.143 (小数形式输出,精度仍为4)
pi=3.1429(fixed确定小数点后精度为4 )
(2)用流对象的成员函数控制输出格式 除了可以用控制符来控制输出格式外,还可以通过调用流对象cout中用于控制输出格式的成员函数来控制输出格式。 流成员函数setf和控制符setiosflags括号中的参数表示格式状态,它是通过格式标志来指定的。格式标志在类ios中被定义为枚举值。因此在引用这些格式标志时要在前面加上类名ios和域运算符“::”。
例3 用流控制成员函数输出数据。
[cpp] view plain copy
#include <iostream>
using namespace std;
int main()
{
int a=21;
cout.setf(ios::showbase); //设置输出时的基数符号
cout<<"dec:"<<a<<endl; //默认以十进制形式输出a
cout.unsetf(ios::dec); //终止十进制的格式设置
cout.setf(ios::hex); //设置以十六进制输出的状态
cout<<"hex:"<<a<<endl; //以十六进制形式输出a
cout.unsetf(ios::hex); //终止十六进制的格式设置
cout.setf(ios::oct); //设置以八进制输出的状态
cout<<"oct:"<<a<<endl; //以八进制形式输出a
cout.unsetf(ios::oct); //终止以八进制的输出格式设置
char *pt="China"; //pt指向字符串”china”
cout.width(10); //指定域宽为10
cout<<pt<<endl; //输出字符串
cout.width(10); //指定域宽为10
cout.fill('*'); //指定空白处以'*'填充
cout<<pt<<endl; //输出字符串
double pi=22.0/7.0; //计算pi值
cout.setf(ios::scientific);//指定用科学记数法输出
cout<<"pi="; //输出"pi="
cout.width(14); //指定域宽为14
cout<<pi<<endl; //输出"pi值
cout.unsetf(ios::scientific); //终止科学记数法状态
cout.setf(ios::fixed); //指定用定点形式输出
cout.width(12); //指定域宽为12
cout.setf(ios::showpos); //在输出正数时显示“+”号
cout.setf(ios::internal); //数符出现在左侧
cout.precision(6); //保留6位小数
cout<<pi<<endl; //输出pi,注意数符“+”的位置
return 0;
}
运行情况如下:
dec:21 (十进制形式)
hex:Oxl5 (十六进制形式,以0x开头)
oct:025 (八进制形式,以O开头)
China (域宽为10)
*****china (域宽为10,空白处以'*'填充)
pi=**3.142857e+00 (指数形式输出,域宽14,默认6位小数)
****3.142857 (小数形式输㈩,精度为6,最左侧输出数符“+”)
五、运算符重载
面试题:
重写、覆盖/隐藏、重载的区别?
-
重写(Override):重写是指在子类中重新实现(覆盖)从父类继承而来的方法。通过重写,子类可以提供自己的实现逻辑,使得子类对象在调用该方法时执行子类的版本而不是父类的版本。重写通常用于多态性的实现,在运行时动态选择适当的方法。
-
覆盖/隐藏(Override/Hide):覆盖和隐藏是两个相关的概念,用于描述成员变量和静态方法的行为。
-
覆盖(Override):在继承关系中,子类可以重新定义从父类继承而来的成员变量,使得子类对象在访问该成员变量时使用子类的版本而不是父类的版本。覆盖只适用于非静态成员变量。
-
隐藏(Hide):在继承关系中,子类可以定义与父类相同名称的静态成员变量或静态方法,从而隐藏父类中的同名成员。隐藏只适用于静态成员变量和静态方法。
-
-
重载(Overload):重载是指在同一个类中定义多个方法,它们具有相同的名称但是参数列表不同(参数类型、参数个数或参数顺序)。通过重载,可以根据不同的参数来选择合适的方法进行调用。重载只与方法的签名有关,与返回值类型无关。
1.重写/覆盖/隐藏
(1)重写
重写是将原来的内容重新书写一遍(多态或抽象类里的概念)
例如:
void function()
{
}
重写:
void function()
{
}
(2)覆盖/隐藏
是将原来的内容进行覆盖或将原来的内容进行隐藏(继承或多继承)
例如:
父类:
void function()
{
}
void function2()
{
}
子类:
void function()
{
}
void function1()
{
}
子类继承父类,会将父类的成员继承到子类中,有条件:
当子类中与父类没有相同的成员,继承之后,子类会隐藏父类的成员
当子类中与父类有相同的成员,继承之后,子类会自动屏蔽或覆盖与父类相同的成员
(3)重载(函数)
前面讲的---翻一翻
1、运算符重载的概念
是指将基本的运算符赋于新的数据类型的运算,并且是将该运算符强制修饰为函数的形式
能实现重载的运算符有哪些?
能重载的运算符:
算术运算符:+、-、*、/、%、++、--
位操作运算符:&、|、~、^(位异或)、<(左移)、>(右移)
逻辑运算符:!、&&、||
比较运算符:<、>=、>等等
赋值运算符:=、+=、-=、*=、/=、%=、&=、|=、^=、>=
其他运算符:[]、()、->、,、new、delete、new[]、delete[]、*指针运算符
移位运算符:<<、>>
注意事项:
A、除成员访问运算符“.”、成员指针运算符“.*”和“->*”、作用域运算符“::”、sizeof运算符和三目运算符“?:”、预处理符号“#”以外,C++中的所有运算符都可 以重载(其中“=”和“&”不必用户重载)
B、重载运算符限制在C++语言中已有的运算符范围内的允许重载的运算符之中,不能创建新的运算符
C、运算符重载的实质是函数重载,遵循函数重载的选择原则
D、重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符操作数的个数及语法结构
E、运算符重载不能改变该运算符用于内部类型对象的含义
F、运算符重载是针对新类型数据的实际需要对原有运算符进行的适当的改造,重载的功能应当与原有功能相类似,避免没有目的地使用重载运算符
G、重载运算符的函数不能有默认的参数,否则就改变了运算符的参数个数
H、重载的运算符一般是用户自定义类型,否则就不是重载而是改变了现有的C++标准数据类型的运算符的规则
I、运算符重载可以通过成员函数的形式,也可是通过友元函数,还可以是非成员函数的形式。
2、成员函数运算符重载(使用运算符:-)
关键字:operator
格式:
返回值类型 operator 重载的运算符 (const 类名 &obj)
{
}
例如:
People operator - (const People &obj)
{
}
##########实现两个类类型的数据进行相减############
People1-People2
3、友元函数运算符重载
关键字:
类中:
friend 返回值类型 operator 重载的运算符 (const 类名 &obj1 ,const 类名 &obj2)
类外:
返回值类型 operator 重载的运算符 (const 类名 &obj ,const 类名 &obj2)
{
}
4、普通函数运算符重载
在某些编程语言中,可以对普通函数进行运算符重载,这样可以改变函数在特定运算符下的行为。运算符重载允许你使用自定义的逻辑来处理类似于加法、减法、乘法等运算符的操作。
例如,在C++中,你可以通过重载函数来改变类对象之间加法运算符的行为。下面是一个示例:
#include <iostream>
class Number {
private:
int value;
public:
Number(int v) : value(v) {}
int getValue() const { return value; }
// 运算符重载:+
Number operator+(const Number& other) {
return Number(value + other.getValue());
}
};
int main() {
Number num1(5);
Number num2(10);
Number sum = num1 + num2; // 使用重载的加法运算符
std::cout << "Sum: " << sum.getValue() << std::endl;
return 0;
}
上述代码中,Number
类定义了一个私有成员变量 value
和一个重载的加法运算符 operator+
。当两个 Number
对象相加时,重载的加法运算符将它们的值相加并返回一个新的 Number
对象。
练习1:使用类和对象以及友元实现算数表达式的求值
例如:2+5+6-4+4-40的和
#include "iostream"
using namespace std;
class jian
{
public:
jian(int a,int b);//构造函数
int a;
int b;
friend void jian(jian& jian1 ,int b);//减法功能
};
class jia
{
public:
jia(int a,int b);
int a;
int b;
friend void jia(jia &jia1 ,int b);//加法功能
};
void jian ::jian(jian& jian1 ,int b)
{
jian1.a = jian1.a-b;
}
void jia ::jia(jia &jia1 ,int b)
{
jia1.a = jia1.a+b;
}
jian ::jian(int a,int b)
{
this -> a=a;
this -> b=b;
}
jia ::jia(int a,int b)
{
this -> a=a;
this -> b=b;
}
//2+5+6-4+4-40
int main()
{
//1.使用普通成员函数实现表达式
//jian jian;
//jia jia;
//jia.jia(2,5);
//jia.jia(jia.a,6);
//jian.jian(jia.a,4);
//jia.jia(jian.a,4);
//jian.jian(jia.a,40);
//cout <<"2+5+6-4+4-40"<<jina.a<<endl;
//2.使用类的友元函数实现表达式求值
jia jia(2,0);
jian jian(0,0);
jia(jia2,5);
jia(jia2,6);
jian jian2(jia2.a,0);
jia(jian2,4);
jia jia3(jian2.a,0);
jia(jia3,4);
jian jian3(jia3.a,0);
jian(jian3.a,40)
cout <<"2+5+6-4+4-40"<<jina3.a<<endl;
return 0;
}
练习2:编写一个复合类Complex,使用运算重载的方式,对两个复数进行求和运算,并将求和运算的结果打印出来,需要实现复数的加减乘法
a+b*i;
a1+b1*i;
a*a1*-b+b1*
#include "iostream"
using namespace std;
class Complex
{
public:
Complex();
Complex(int a, int b);
int shi;//复数的实部
int xu; //复数的虚部
void print(Complex& obj);
};
Complex::Complex()
{
this->shi = 0;
this->xu = 0;
}
Complex::Complex(int a, int b)
{
this->shi = a;
this->xu = b;
}
void Complex::print(Complex& obj)
{
cout << obj.shi << "+" << obj.xu << "i" << endl;
}
Complex operator +(const Complex& obj1, const Complex& obj2)
{
Complex complex;
complex.shi = obj1.shi + obj2.shi;
complex.xu = obj1.xu + obj2.xu;
return complex;
}
Complex operator -(const Complex& obj1, const Complex& obj2)
{
Complex complex;
complex.shi = obj1.shi - obj2.shi;
complex.xu = obj1.xu - obj2.xu;
return complex;
}
Complex operator *(const Complex& obj1, const Complex& obj2)
{
Complex complex;
complex.shi = obj1.shi * obj2.shi - obj1.xu * obj2.xu;
complex.xu = obj1.shi * obj2.xu + obj1.xu * obj2.shi;
return complex;
}
int main()
{
Complex complex;
Complex complex1(10,20);//10+20i
Complex complex2(20,10);//20+10i
complex = complex1 + complex2;
complex.print(complex);
complex = complex1 - complex2;
complex.print(complex);
complex = complex1 * complex2;
complex.print(complex);
return 0;
}
六.类的继承与多态
1.继承
是指一个新的类共享一个或多个类的成员,类似于儿子继承父亲,儿子继承父亲和母亲,相对于新的类拥有了一个或多个已经存在的类的成员,继承又分为单继承和多继承;具有继承关系的类中,新的类,称之为“子类或者派生类”,一个或多个已经存在的类,称为“父类或者基类”
直接父类:类似于你的爸爸或者妈妈
间接父类:类似于你的爷爷或者奶奶,祖父或者1祖母,祖宗等等
如何声明一个具有继承关系的类?
格式:
单继承:
class 子类或派生类: 继承权限 父类名或者基类名 //继承权限是指。父类并不一定将所有成员继承到子类中
{
};
多继承:
class 子类或者派生类名 : 继承权限 1 父类或者基类1,继承权限2 父类或者基类2,..
{
};
例如:
class Father
{
int a;
int b;
void function();
void function1();
.........
}
继承:
class Son:继承权限 Father
{
int c;//Son类自己的特性
void function2();//Son类自己的特性
...
}
2、继承权限
C++提供了3种继承权限,public、protected、private继承权限
例如:
class Father
{
int a;
int b;
void function();
void function1();
..........
}
继承:
class Son:public/protected/private Father
{
int c; //Son类自己的特性
void function2(); //Son类自己的特性
....
}
(1)public继承
a、将父类的public下的成员,继承到子类的public访问修饰符下
b、将父类的protected下的成员,继承到子类的protected访问修饰符下
c、父类的private下的成员没有被继承到子类中
(2)protected继承
a、父类的public下的成员,继承到子类的protected访问修饰符下
b、父类的protected下的成员,继承到子类的protected访问修饰符下
c、父类的private下的成员没有被继承到子类中
(3)private继承
a、父类的public下的成员,继承到子类的private访问修饰符下
b、父类的protected下的成员,继承到子类的private访问修饰符下
c、父类的private下的成员没有被继承到子类中
C++里规定父类的构造函数和析构函数没有被继承到子类中
具有继承关系的类:
构造函数和析构函数的调用时机?
单继承:
构造函数的调用时机:
先执行父类的构造函数,再执行子类的构造函数
析构函数的调用时机:
先执行子类的析构函数,再执行父类的析构函数
多继承:
构造函数的调用时机:
先执行第一个继承的父类的构造函数,后执行其它继承的父类的构造函数
析构函数的调用时机:
先执行子类的析构函数,再执行后继承的父类的析构函数
注:
具有单继承关系的类中,当子类有与父类相同的成员,子类继承父类时,子类会自动屏蔽父类中与子类相同的成员
3、虚继承
C++提出虚继承的目的是为了解决菱形继承造成的二义性问题
虚继承是指:
继承时,在继承权限前加上一个关键字virtual,那就表示这种继承方式为虚继承例如:
#include "iostream"
using namespace std;
//虚继承
class A
{
public:
int a = 20;
};
class B1 :virtual public A
{
public:
//B1中继承了A的a
};
class B2 :virtual public A
{
public:
//B2中继承了A中的a
};
class C :public B1, public B2
{
public:
};
int main()
{
C c;
cout << "c.a=" << c.a << endl;
return 0;
}
4、多态
多态是指:
一种接口,多种实现方法(是指多种形态),通过父类接口,调用子类与父类同名的方法
一种接口,是指父接口
多种方法,是指子类实现父类同名的方法
(1)虚函数
在普通成员函数的返回之前加上一个关键字:virtual,那就表示该成员函数为虚函数
虚函数会在运行时,在内存里创建一张虚函数表(虚函数的指针,该指针指向虚函数表),虚函数表中存放的是类中的虚函数
多态是在运行时才知道函数或表达式的调用地址
(2)构成多态性的条件
1)具有继承关系的类中
2)类中具备虚函数
3)通过父类指针或引用指向子类
5、静态联编和动态联编
静态联编(静态绑定):
函数或表达式的地址在编译时就已经确定
例如:
函数的调用,函数重载时调用等等
动态联编(动态绑定):
函数或表达式的地址在运行时才确定
6、抽象类
举例:
现实世界中,有些事物是不能被实例化为具体的对象,例如:描述一个外星人类、描述一个鬼类,抽象的描述这一类型的事物(实际开发中,如果不想实例化该类,将该类设置为抽象类)
C++抽象类的表示:
是指含有纯虚函数的类,称之为“抽象类”,抽象类它只是用于描述一类事物具有的方法,但不具体实现(例如,动物类,具有“跑”方法,不同的动物“跑”的方式不一样),而是在子类中具体实现纯虚函数
纯虚函数:
令虚函数等于0,表示为“纯虚函数”
例如:
virtual void run()= 0 ;//表示run函数是一个纯虚函数
作用:
只是为了给子类提供一个基类,基类中描述子类具备的方法,基类中没有具体实现方法,而是在子类中具体实现
7、虚析构函数
虚析构函数是指在类的析构函数前加上关键字:virtual,那么就表示析构函数是虚析构函数
问题:当父类的指针指向子类,释放父类的指针时,子类的析构函数不会被调用(相当于是子类占用的资源不会被释放)
虚析构函数的作用:
当父类的指针指向子类,释放父类的指针时,子类的析构函数会被调用(有虚析构函数,子类的占用的资源会被释放)
要求:
(1)将父类的析构函数,声明为虚析构函数
(2)父类的指针指向子类,并释放父类指针时
七.异常
C++提供一种容错机制程序可以在程序里主动产生异常(错误)并且处理,还可以处理一些异常,例如:内存空间分配失败,数据溢出,函数传参错误等等
1.异常的梳理流程
第一产生异常(不仅仅是编译器产生用户代码产生异常,【编译器不会主动处理异常】还可以是用户自己想要产生异常)
第二是检测异常是否产生
第三是如果异常产生,那么就可以捕获异常,并且处理
异常处理流程,c++提供了三个关键字,用于协助用户处理
(1)throw:作用是抛出异常(错误)
格式: throw 数据类型 //数据类型可以是基本数据类型,还可以是自定义的类型(类类型)
(2)try : 是一个代码块,用于检查用户编写的代码是否产生异常
格式: try
{
//用户编写的代码
}
(3)catch :是一个代码块(函数),作用是捕获用户编写的代码产生异常并处理
格式: catch (数据类型 标识符)//标识符可用省略
{
//异常处理
}
注:
1)catch捕获异常时通过比较catch的参数类型与异常类型
2)一个try后面紧跟需要至少有一个catch快
3)try里如果确定有异常,但是try面膜没有写捕获该异常的catch快,那么该异常会被推送到上一级的异常处理,加入上一级也没有处理该异常的catch快,那么继续推送到上一级,如果最后 到main函数里面都没有处理异常的catch快,那么会调用Windows的termlate来终止程序
4)异常捕获的流程,首先是try后面的第一个catch块去匹配异常,如果没有匹配上,继续续往后的catch进行匹配
5)catch块里有三个英文字符串的小点“.”,表示捕获所有异常,一般将它放置到catch快的最后面
6)try...catch块可用发生嵌套,嵌套try...catch异常的捕获,遵循第3)步骤
2.标准的异常:C++给用户提供了异常类,异常类属于命名空间std里面的内容,std::exception(异常的父类),exception
3、自定义异常类
是指用户可以自己定义一个异常类继承自异常父类(exception),自定义的异常类需要重写what方法
#include "iostream"
using namespace std;
class MyException :public exception
{
public:
MyException(const char*str)
{
this->str1 = str;
//实现内容
}
const char* what()const
{
return str1;
}
private:
const char* str1;
};
int main()
{
try
{
throw MyException("bad_myexception");
}
catch (const MyException& e)
{
cout << "异常的类型:" << e.what() << endl;
}
return 0;
}
八、模板
模板,类似于画板(已经将图形给你画好,用户只需要根据图形填充相应的颜色即可),进入了泛型编程。
泛型编程:
是指一个函数或一个类的数据的类型是广泛的
1、函数模板
关键字:template、class/typename
格式:
template <class/typename 标识符> 函数返回值 函数名 (函数的参数列表) //将函数声明为一个模板
或
template<class/typename 标识符1 , class/typename 标识符2,....>函数返回值 函数名 (函数参数列表)//将函数声明为一个模板
例如:
template<class T>void function(T a ,T b) //function函数被声明为一个模板
{
}
或
template<class T1,class T2>void function(T1 a , T2 b) //function函数被声明为一个模板
{
}
解释:
(1)关键字template用于声明一个模板(模板函数和模板类)
(2)<class T> 中的T表示模板的类型形参,模板类型形参可以用class声明,也可以用typename声明,模板形参可根据实际需要进行增删
(3)函数的返回值,可以是基本的数据类型(void,int等等),也可以是类类型,也可以是模板形参类型
(4)函数的参数列表内的(T a ,T b),表示函数的形参,形参的类型可以实模板形参类型,也可以是基本的数据类型,还可以是类类型
(5)模板形参中的“T”就表示泛型,泛型是指可以是int\char\float等等
(6)函数模板可以发生重载
(7)模板形参要与函数形参一一对应(对于隐式调用而言),显式调用可以不用一一对应
2、函数模板的调用
(1)隐式调用
隐式调用类似于普通函数的调用(与普通函数调用一致)
函数名(实参1,实参2,....)
(2)显示调用
格式:
函数名<实参数据类型>(实参1,实参2,.......)
练习:
abs----->include"cmath"
设计一个直线类line(直线的方程为ax+by+c=0),其中直线包含了三个数据成员a,b,c
(1)包含一个显示数据成员的方法:display()
(2)包含一个求两直线交点的友元函数setpoint,该友元函数里要求当两直线平行或交点坐标(x和y)的绝对值大于等于10^8时,可抛出异常信息,给予用户提示
#include <iostream>
#include <cmath>
#include <exception>
class Line {
private:
double a, b, c; // 直线的方程参数
public:
Line(double _a, double _b, double _c) : a(_a), b(_b), c(_c) {}
// 显示数据成员的方法
void display() const {
std::cout << "Line equation: " << a << "x + " << b << "y + " << c << " = 0" << std::endl;
}
// 友元函数:求两直线交点
friend void setpoint(const Line& line1, const Line& line2);
};
// 函数签名:判断两个浮点数是否相等
bool isEqual(double x, double y) {
return std::abs(x - y) < 1e-8;
}
// 友元函数:求两直线交点
void setpoint(const Line& line1, const Line& line2) {
// 判断两直线是否平行
if (isEqual(line1.a * line2.b, line1.b * line2.a)) {
throw std::runtime_error("Parallel lines: no intersection point");
}
// 计算交点坐标
double x = (line2.b * line1.c - line1.b * line2.c) / (line1.a * line2.b - line2.a * line1.b);
double y = (line2.a * line1.c - line1.a * line2.c) / (line1.b * line2.a - line2.b * line1.a);
// 判断交点坐标是否合法
if (std::abs(x) >= 1e8 || std::abs(y) >= 1e8) {
throw std::runtime_error("Intersection point coordinates are out of range");
}
std::cout << "Intersection point: (" << x << ", " << y << ")" << std::endl;
}
int main() {
Line line1(2, 3, 4);
Line line2(1, -2, 5);
line1.display();
line2.display();
try {
setpoint(line1, line2);
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
练习:编写如下形式的模板: template <class Type>void sort(Type* A, int n, bool f),对一个具有n个Type类型的一维数组进行排序,f为1表示从小到大排序, f为0表示从大到小排序。
一下快速排序的一种方式:
//快速排序
int PartSort1(int* a, int left, int right)
{
int key = right;//选定基准值
while (left < right)
{
//选右边为基准值,左指针先走
while (left < right && a[left] <= a[key])
{
left++;
}
//右指针再走
while (left < right && a[right] >= a[key])
{
right--;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[key]);
return left;
}
void QuickSort(int* a, int left, int right)
{
assert(a);
if (left >= right)
{
return;
}
int keyi = PartSort1(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
#include <iostream>
// 模板函数:对一维数组进行排序
template <class Type>
void sort(Type* A, int n, bool f) {
for (int i = 0; i < n - 1; ++i) {
for (int j = 0; j < n - 1 - i; ++j) {
if (f) {
// 从小到大排序
if (A[j] > A[j + 1]) {
std::swap(A[j], A[j + 1]);
}
} else {
// 从大到小排序
if (A[j] < A[j + 1]) {
std::swap(A[j], A[j + 1]);
}
}
}
}
}
int main() {
int arr1[] = {5, 2, 8, 1, 9}; // 定义一个整型数组arr1,初始值为{5, 2, 8, 1, 9}
double arr2[] = {3.7, 2.2, 1.5, 4.8, 0.9}; // 定义一个双精度浮点型数组arr2,初始值为{3.7, 2.2, 1.5, 4.8, 0.9}
int size1 = sizeof(arr1) / sizeof(arr1[0]); // 计算arr1的大小
int size2 = sizeof(arr2) / sizeof(arr2[0]); // 计算arr2的大小
// 从小到大排序
sort(arr1, size1, 1); // 使用sort函数对arr1进行升序排序
std::cout << "Sorted arr1 in ascending order: ";
for (int i = 0; i < size1; ++i) {
std::cout << arr1[i] << " "; // 输出排序后的arr1
}
std::cout << std::endl;
// 从大到小排序
sort(arr2, size2, 0); // 使用sort函数对arr2进行降序排序
std::cout << "Sorted arr2 in descending order: ";
for (int i = 0; i < size2; ++i) {
std::cout << arr2[i] << " "; // 输出排序后的arr2
}
std::cout << std::endl;
return 0;
}
代码的解释如下:
1. #include <iostream>:包含输入输出流库,用于输入输出操作。
2. template <class Type>:定义了一个模板函数,用于对一维数组进行排序。Type 是一个占位.符,可以在调用函数时指定具体的数据类型。
3. void sort(Type* A, int n, bool f):模板函数 sort 接受三个参数,分别是需要排序的数组 A,数组大小 n,以及一个布尔值 f,用于指定排序顺序(true 表示升序,false 表示降序)。
4. for 循环嵌套用于实现冒泡排序算法,对给定的数组进行排序。
5. if-else 分支用于根据布尔值 f 来确定排序的顺序,如果为 true 则进行升序排序,如果为 false 则进行降序排序。
6. std::swap(A[j], A[j + 1]):使用 std::swap 函数交换数组中元素的位置。
7. int main():主函数。
8. int arr1[] = {5, 2, 8, 1, 9}; 和 double arr2[] = {3.7, 2.2, 1.5, 4.8, 0.9};:定义了一个整型数组 arr1 和一个双精度浮点型数组 arr2,并初始化它们的值。
9. int size1 = sizeof(arr1) / sizeof(arr1[0]); 和 int size2 = sizeof(arr2) / sizeof(arr2[0]);:计算数组 arr1 和 arr2 的大小。
10. sort(arr1, size1, 1);:调用 sort 函数对 arr1 进行升序排序。
11. std::cout << "Sorted arr1 in ascending order: ";:输出提示信息。
12. for 循环用于输出排序后的 arr1。
13. std::cout << std::endl;:输出换行符。
14. sort(arr2, size2, 0);:调用 sort 函数对 arr2 进行降序排序。
15. std::cout << "Sorted arr2 in descending order: ";:输出提示信息。
16. for 循环用于输出排序后的 arr2。
17. return 0;:程序正常结束。
3、类模板
是指将一个类声明为模板类,类似于函数模板;类模板的模板形参可以是整形(int、指针、引用),不可以是浮点型(float、double)和字符串类型(string、const char *等等)
关键字:template、class/typename
格式:
template<class 标识符> class 类名 //将一个类声明为模板类
{
}
或
template<class 标识符1 ,class 标识符2,.....>class 类名
{
}
例如:
template<class T>class A
{
}
(1)实例化模板类
格式:
类名 <指定数据类型>对象名(构造函数的参数);
例如:
Test <int> test(10,20);//实例化模板类Test的对象为test
九、智能指
C++11引入智能指针的目的是为了?解决以下这两种情况:
1)用户写代码时从堆空间申请了空间来使用,由于用户的疏忽,没有及时释放该堆空间,严重时会造成内存资源泄露
2)用户从堆空间申请空间来使用,使用了完及时的被释放,这时候释放的位置不对,例如:该空间不仅在一个地方使用,而且还在另外一个地方使用
C++提供了3中智能指针:
shared_ptr:共享指针
weak_ptr:弱指针
unique_ptr:唯一指针
//C++11之后舍弃了auto_ptr:自动指针(了解)
帮助用户管理申请的堆空间
1、智能指针
智能:
是指在用户使用完申请的空间时,帮助用户释放该空间(帮助用户管理堆空间)
指针:
是指重载了指针的一些运算,例如:*,->等等,即称为“指针”,注:实质上不是一个指针,而是一个模板类
2、具体介绍智能指针
(1)shared_ptr智能指针
shared:分享、共享
shared_ptr是指管理堆空间(用户用裸指针开辟的堆空间)时,不仅仅只让它自己管理该空间,其它的shared_ptr也可以管理该空间
如何使用:
用户用裸指针开辟了堆空间之后,将堆空间的管理权移交给shared_ptr智能指针,智能指针帮助用户去管理该堆空间
注:
智能指针shared_ptr里引入引用计数机制,引用计数机制的作用:
作用是可以查看有多少个shared_ptr同时管理同一个空间
即:当一个shared_ptr管理该空间时,引用计数+1操作
创建一个shared_ptr对象时,在内存里shared_ptr会指向两个空间
(1)shared_ptr指向所管理的空间
(2)shared_ptr指向引用计数的空间(注:多个shared_ptr用的是同一个引用计数空间)
什么时候智能指针释放所管理的堆空间?
(1)智能指针对象超出了所在作用域
(2)当智能指针里的引用计数减到0
3.shared_ptr相关的函数
(1)use_count()
查看引用计数的值
(2)swap(shared_ptr)
交换智能指针所管理的堆空间
(3)reset()
解绑所管理的堆空间
(4)unique()
判断shrared_ptr是否唯一管理堆空间,如果是返回true,否则返回false
(5)weak_ptr弱指针
该指针是用于协助shared_ptr管理堆内存空间,解决由于shared_ptr造成资源被循环利用得不到释放的问题
(6)unique_ptr唯一指针
是指unique_ptr管理空间时,只能由一个智能指针unique_ptr管理该空间,即一个unique_ptr对应于一个堆空间
十、标准STL
C++标准模板库是一种泛型编程,将算法抽象出来,独立于类型编程。
C++标准库提供等容器主要有
数组,链表,队列等等,
可以实现 增删改查、排序等操作
C++标准模板库的六大构成组件
泛型容器(containers)
特殊的数据结构,实现了数组、链表、队列、等等,实质是模板类
迭代器(iterators)
一种复杂的指针,可以通过其读写容器中的对象,实质是运算符重载
算法(algorithms)
读写容器对象的逻辑算法:排序、遍历、查找、等等,实质是模板函数
空间配置器(allocator)
容器的空间配置管理的模板类
配接器(adapters)
用来修饰容器、仿函数、迭代器接口
仿函数(functors)
类似函数,通过重载()运算符来模拟函数行为的类
组件间的关系
container(容器) 通过 allocator(配置器) 取得数据储存空间,
algorithm(算法)通过 iterator(迭代器)存取 container(容器) 内容,
functor(仿函数) 可以协助 algorithm(算法) 完成不同的策略变化,
adapter(配接器) 可以修饰或套接 functor(仿函数)。
这些组件最终实现一些常见的数据结构的快速使用。
1、vector
vector相当于一个动态数组
数组方式连续存储,可以使用[] 符号进行随机访问,
#include "vector"
(1)初始化vector对象的方式:
vector<T>v1 ; //默认的初始化方式,内容为空
vector <T>v2(v1) ;//v2是v1的一个副本
vector<T>v3(n ,i) ;//v3中包含了n个数值为i的元素
vector<T>v4(n) ;//v4包含了n个元素,每个元素的值为0
(2)vector常用函数
empty():判断向量是否为空,为空返回真,否则为假
begin():返回向量(数组)的首元素地址
end(): 返回向量(数组)的末元素的下一个元素的地址
clear():清空向量
front():返回得到向量的第一个元素的数据
back():返回得到向量的最后一个元素的数据
size():返回得到向量中元素的个数
push_back(数据):将数据插入到向量的尾部
pop_back():删除向量尾部的数据
注:
面试题:
静态数组与向量异同点?
相同点:
存储数据:静态数组和向量都可以存储一组有序的数据。
访问元素:静态数组和向量都可以通过索引访问元素,索引从0开始。
连续存储:静态数组和向量都使用连续的内存空间来存储元素,可以通过指针算术运算来快速访问元素。
不同点:
大小固定 vs. 动态调整:静态数组的大小是固定的,在创建时就需要指定大小,而且不能改变。向量在创建时也可以指定初始大小,但它会根据需要动态调整大小,可以自动扩展或缩小。
内存管理:静态数组在编译时分配内存,由于大小固定,内存是静态分配的。向量在运行时动态分配内存,可以根据需要进行扩展或释放内存。
添加和删除元素:静态数组的大小是固定的,无法动态地添加或删除元素。向量可以在任意位置插入或删除元素,而且支持在尾部高效地添加和删除元素。
效率:由于静态数组的大小固定,内存是静态分配的,因此访问元素的效率更高。向量需要动态分配内存,并且在容量不足时可能需要进行内存重新分配,因此在插入或删除元素时效率更低。
根据具体的需求,选择适合的数据结构可以提高程序的效率和灵活性。如果需要固定大小和快速访问元素,可以选择静态数组;如果需要动态调整大小、高效地插入和删除元素,可以选择向量。
2、list
链表相对于vector向量来说的优点在于:
(a)动态的分配内存,当需要添加数据的时候不会像vector那样,先将现有的内存空间释放,在次分配更大的空间,这样的话效率就比较低了。
(b)支持内部插入、头部插入和尾部插入
缺点:不能随机访问,不支持[]方式和vector.at()、占用的内存会多于vector(非有效数据占用的内存空间)
#include "list"
(1)初始化list对象的方式
list L0; //空链表
list L1(3); //建一个含三个默认值是0的元素的链表
list L2(5,2); //建一个含五个元素的链表,值都是2
list L3(L2); //L3是L2的副本
list L4(L1.begin(),L1.end()); //c5含c1一个区域的元素[begin, end]。
(2)list常用函数
begin():返回list容器的第一个元素的地址
end():返回list容器的最后一个元素之后的地址
rbegin():返回逆向链表的第一个元素的地址(也就是最后一个元素的地址)
rend():返回逆向链表的最后一个元素之后的地址(也就是第一个元素再往前的位置)
front():返回链表中第一个数据值
back():返回链表中最后一个数据值
empty():判断链表是否为空
size():返回链表容器的元素个数
clear():清除容器中所有元素
insert(pos,num):将数据num插入到pos位置处(pos是一个地址)
insert(pos,n,num):在pos位置处插入n个元素num
erase(pos):删除pos位置处的元素
push_back(num):在链表尾部插入数据num
pop_back():删除链表尾部的元素
push_front(num):在链表头部插入数据num
pop_front():删除链表头部的元素
sort():将链表排序,默认升序
.....
list<int> List
list<int>::iterator iter = List.begin();
for( ; iter!=List.end() ; iter++)
{
cout<<*iter<<endl ;
}
3、deque
合并了 vector 和 list的特点,
优点
随机访问方便,即支持[]操作符和vector.at(n)
在内部方便的进行插入和删除操作
可在两端进行push、pop
缺点占用内存多
使用区别
如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
如果你需要大量的插入和删除,而不关心随机存取,则应使用list
如果你需要随机存取,而且关心两端数据的插入和删除,则应使用deque
#include "deque"
deque<int> d1; //创建一个空的双端队列d
deque<int>d2(n) ;//创建一个元素个数为n的队列
deque<int>d3(n,num);//创建一个元素个数为n的队列,并且每个元素为num
成员函数:
push_back() //在队尾插入元素
push_front() //在队首插入元素
insert(d.begin()+1,9); //第一个元素之后插入9
size() //双端队列的大小
empty() //判断是否为空
begin() //队首的指针,指向队首元素
end() //队尾元素的下一位作为指针
rbegin() //以最后一个元素作为开始
rend() //以第一个元素的上一位作为指针
erase() //删除某一个元素
clear() //删除所有元素
pop_front() //删除队首元素
pop_back() //删除队尾元素
deque<int>::iterator it; //迭代器
练习:
本题目要求定义类模板实现2个数的最大值、最小值、加、减、乘、除等算术运算,在main () 函数中使用该类模板分别实例化为int型和double型的类,定义相关的对象,读入2个整数和2个浮点数,然后分别输出它们的最大值、最小值、加、减、乘、除的结果。
输入格式:
分别输入2组数字,第一行为2个整数, 以空格分隔;第二行为2个浮点数,以空格分隔。
输出格式:
分2行分别输出整数和浮点数的运算结果,每行依次输出2个数的最大值、最小值、加、减、乘、除等算术运算。输入样例:
30 2
2.1 3.2
输出样例:
30 2 32 28 60 15
3.2 2.1 5.3 -1.1 6.72 0.65625
#include <iostream>
using namespace std;
// 创建一个模板类 suanfa
template<class T> class suanfa
{
private:
T num1, num2;
public:
// 构造函数,接受两个参数 n1 和 n2,并将其赋值给私有成员变量 num1 和 num2
suanfa(T n1, T n2) : num1(n1), num2(n2) {}
// 返回 num1 和 num2 中较大的值
T getMax() {
return (num1 > num2) ? num1 : num2;
}
// 返回 num1 和 num2 中较小的值
T getMin() {
return (num1 < num2) ? num1 : num2;
}
// 返回 num1 和 num2 的和
T add() {
return num1 + num2;
}
// 返回 num1 和 num2 的差
T subtract() {
return num1 - num2;
}
// 返回 num1 和 num2 的乘积
T multiply() {
return num1 * num2;
}
// 返回 num1 和 num2 的商
T divide() {
return num1 / num2;
}
};
int main() {
int a, b;
double x, y;
// 读取两个整数和两个浮点数
std::cin >> a >> b;
std::cin >> x >> y;
// 实例化 suanfa 类的对象,使用模板参数分别为 int 和 double
suanfa<int> intCalc(a, b);
suanfa<double> doubleCalc(x, y);
// 按要求输出整数的运算结果
std::cout << intCalc.getMax() << " "
<< intCalc.getMin() << " "
<< intCalc.add() << " "
<< intCalc.subtract() << " "
<< intCalc.multiply() << " "
<< intCalc.divide() << std::endl;
// 按要求输出浮点数的运算结果
std::cout << doubleCalc.getMax() << " "
<< doubleCalc.getMin() << " "
<< doubleCalc.add() << " "
<< doubleCalc.subtract() << " "
<< doubleCalc.multiply() << " "
<< doubleCalc.divide() << std::endl;
return 0;
}
练习:
定义一个堆栈类模板,实现不同数据类型数据的入栈和出栈操作,堆栈的大小用非类型参数指定。
#include <iostream>
using namespace std;
template <typename T, int Size>
class Stack {
private:
T data[Size]; // 存储元素的数组
int top; // 栈顶指针
public:
Stack() : top(-1) {} // 构造函数,初始化栈顶指针为-1
bool isEmpty() const {
return (top == -1); // 栈为空的条件是栈顶指针为-1
}
bool isFull() const {
return (top == Size - 1); // 栈为满的条件是栈顶指针等于数组大小减一
}
void push(const T& element) {
if (isFull()) {
cerr << "Stack is full. Cannot push element." << endl;
return;
}
data[++top] = element; // 先将栈顶指针加一,再将元素入栈
}
T pop() {
if (isEmpty()) {
cerr << "Stack is empty. Cannot pop element." << endl;
return T();
}
return data[top--]; // 先返回栈顶元素,再将栈顶指针减一
}
};
int main() {
// 使用模板类实例化不同数据类型的堆栈对象
Stack<int, 5> intStack;
Stack<double, 3> doubleStack;
// 向 intStack 压入整数
intStack.push(10);
intStack.push(20);
intStack.push(30);
// 从 intStack 弹出并输出整数
cout << intStack.pop() << endl;
cout << intStack.pop() << endl;
cout << intStack.pop() << endl;
// 向 doubleStack 压入浮点数
doubleStack.push(1.23);
doubleStack.push(4.56);
// 从 doubleStack 弹出并输出浮点数
cout << doubleStack.pop() << endl;
cout << doubleStack.pop() << endl;
return 0;
}