C++(中级+高级部分)

文章目录

第十二章 C++对c的扩展

面向对象编程概述

面向对象的三大特点

  • 封装 :把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。 类将成员变量和成员函数封装在类的内部,根据需要设置访问权限,通过成员函数管理内部状态。
  • 继承 :继承所表达的是类之间相关的关系,这种关系使得对象可以继承另外一类对象的特征和能力。 继承的作用:避免公用代码的重复开发,减少代码和数据冗余。
  • 多态 :多态性可以简单地概括为“一个接口,多种方法”,字面意思为多种形态。程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念

::作用域运算符

::解决归属问题(局部变量与全局变量重名时,函数内部使用全局变量,可以使用::作用域运算符)

在这里插入图片描述

命名空间 namepace

创建名字是程序设计过程中一项最基本的活动,当一个项目很大时,它会不可避免地包含大量名字。
c++允许我们对名字的产生和名字的可见性进行控制。 我们之前在学习c语言可以通过static关键字来使
得名字只得在本编译单元内可见,在c++中我们将通过一种通过命名空间来控制对名字的访问。

数据类型的别名typedef

  • 语法:typedef 原数据类型名 别名;

C++11还可以用using关键字创建数据类型的别名。

  • 语法:using 别名=原数据类型名;

C++命名空间(namespace)

在c++中,名称(name)可以是符号常量、变量、函数、结构、枚举、类和对象等等。工程越大,名称
互相冲突性的可能性越大。另外使用多个厂商的类库时,也可能导致名称冲突。为了避免,在大规模程
序的设计中,以及在程序员使用各种各样的C++库时,这些标识符的命名发生冲突,标准C++引入关键字namespace(命名空间/名字空间/名称空间),可以更好地控制标识符的作用域。

命名空间的本质就是封装

命名空间使用语法

1、创建一个命名空间:

namespace A{
    
    
	int a = 10;
}
namespace B{
    
    
	int a = 20;
}
void test(){
    
    
cout << "A::a : " << A::a << endl;//10
cout << "B::a : " << B::a << endl;//20
}

2、命名空间只能全局范围内定义(以下错误写法)

void test(){
    
    
	namespace A{
    
    
		int a = 10;
	}
	namespace B{
    
    
		int a = 20;
	}
	cout << "A::a : " << A::a << endl;
	cout << "B::a : " << B::a << endl;
}

3、命名空间可嵌套命名空间

namespace A{
    
    
	int a = 10;
	namespace B{
    
    
		int a = 20;
	}
}
void test(){
    
    
	cout << "A::a : " << A::a << endl;
	cout << "A::B::a : " << A::B::a << endl;
}

4、命名空间是开放的,即可以随时把新的成员加入已有的命名空间中

namespace A{
    
    
	int a = 10;
}
//命名空间重名会优化成一个空间
namespace A{
    
    
	void func(){
    
    
		cout << "hello namespace!" << endl;
	}
}
void test(){
    
    
	cout << "A::a : " << A::a << endl;
	A::func();
}

5、声明和实现可分离

函数前面可以填作用域也可以填函数返回值

namespace MySpace{
    
    
	//声明
	void func1();
	void func2(int param);
}
void MySpace::func1(){
    
    
	cout << "MySpace::func1" << endl;
}
void MySpace::func2(int param){
    
    
	cout << "MySpace::func2 : " << param << endl;
}

6、无名命名空间
意味着命名空间中的标识符只能在本文件内访问,相当于给这个标识符加上了static,使得其可以作为内部连接

namespace{
    
    
	int a = 10;
	void func(){
    
     cout << "hello namespace" << endl; }
}
void test(){
    
    
	cout << "a : " << a << endl;
	func();
}

7、命名空间别名

namespace veryLongName{
    
    
	int a = 10;
	void func(){
    
     cout << "hello namespace" << endl; }
}
void test(){
    
    
	namespace shortName = veryLongName;
	cout << "veryLongName::a : " << shortName::a << endl;
	veryLongName::func();
	shortName::func();
}

using声明 命名空间中的成员 可用

namespace A{
    
    
	int paramA = 20;
	int paramB = 30;
	void funcA(){
    
     cout << "hello funcA" << endl; }
	void funcB(){
    
     cout << "hello funcA" << endl; }
}
void test(){
    
    
	//1. 通过命名空间域运算符
	cout << A::paramA << endl;
	A::funcA();
	
	//2. using声明成员可用
	using A::paramA;
	using A::funcA;
	cout << paramA << endl;
	//cout << paramB << endl; //不可直接访问
	funcA();
	
	//3. 同名冲突
	int paramA = 20; //相同作用域注意同名冲突
}

using声明成员碰到函数重载

namespace A{
    
    
	void func(){
    
    }
	void func(int x){
    
    }
	int func(int x,int y){
    
    }
}
void test(){
    
    
	using A::func;
	func();
	func(10);
	func(10, 20);
}

如果命名空间包含一组用相同名字重载的函数,using声明就声明了这个重载函数的所有集合

using 声明整个命名空间可用

namespace A{
    
    
	int paramA = 20;
	int paramB = 30;
	void funcA(){
    
     cout << "hello funcA" << endl; }
	void funcB(){
    
     cout << "hello funcB" << endl; }
}
void test01(){
    
    
	using namespace A;
	cout << paramA << endl;
	cout << paramB << endl;
	funcA();
	funcB();
	
	//不会产生二义性,若当前局部变量有定义则使用,若无则到命名空间里面寻找
	int paramA = 30;
	cout << paramA << endl;
}
namespace B{
    
    
	int paramA = 20;
	int paramB = 30;
	void funcA(){
    
     cout << "hello funcA" << endl; }
	void funcB(){
    
     cout << "hello funcB" << endl; }
}
void test02(){
    
    
	using namespace A;
	using namespace B;
	//二义性产生,不知道调用A还是B的paramA
	cout << paramA << endl;
}

注意:使用using声明或using编译指令会增加命名冲突的可能性。也就是说,如果有名称空间,并在代码中使用作用域解析运算符,则不会出现二义性。

struct类型增强

c中定义结构体变量需要加上struct关键字,c++不需要。 c中的结构体只能定义成员变量,不能定义成员
函数。c++即可以定义成员变量,也可以定义成员函数。

结构体中即可以定义成员变量,也可以定义成员函数

struct Student{
    
    
	string mName;
	int mAge;
	void setName(string name){
    
     mName = name; }
	void setAge(int age){
    
     mAge = age; }
	void showStudent(){
    
    
		cout << "Name:" << mName << " Age:" << mAge << endl;
	}
};

c++中定义结构体变量不需要加struct关键字

void test01(){
    
    
	Student student;//c++中定义结构体变量不需要加struct关键字
	student.setName("John");
	student.setAge(20);
	student.showStudent();
}

bool类型关键字

标准c++的bool类型有两种内建的常量true(转换为整数1)和false(转换为整数0)表示状态。
这三个名字都是关键字。 bool类型只有两个值,true(1值),false(0值) ,bool类型本质上是1字节的整数(unsigned char),取值只有1和0。 给bool类型赋值时,非0值会自动转换为true(1),0值会自动转换false(0)

void test()
{
    
    
	cout << sizeof(false) << endl; //为1,//bool类型占一个字节大小
	bool flag = true;
	flag = 10; //给bool类型赋值时,非0值会自动转换为true(1),0值会自动转换false(0)
}

引用(reference)

  • 在c/c++中指针的作用基本都是一样的,但是c++增加了另外一种给函数传递地址的途径,这就是按引用 传递(pass-by-reference)。
  • 变量名实质上是一段连续内存空间的别名,是一个标号(门牌号) 程序中通过变量来申请并命名内存空间,通过变量的名字可以使用存储空间,对一段连续的内存空间只能取一个别名吗? c++中新增了引用的概念,引用可以作为一个已定义变量的别名。
  • 引用的主要用途是用作函数的形参和返回值

引用的定义

  • 引用的本质:就是给变量名取个别名。
  • 引用定义的步骤:

1、&别名
2、给哪个变量取别名 就定义该变量
3、从上往下整体替换!!【替换方式理解较为直观】

  • 声明/创建引用的语法:数据类型 &引用名=原变量名;
  • C和C++用&符号来指示/取变量的地址,C++给&符号赋予了另一种含义。
  • 必须在声明引用的时候初始化,初始化后不可改变。

引用的本质

  • 引用是指针常量的伪装。
  • 引用的底层机制实际上是和指针一 样的。不要相信有别名,不要认为引用可以节省一个指针的空间,因为这一切不会发生,编译器还是会把引用解释为指针。所以引用和指针本质上没有区别。
  • 引用是编译器提供的一个有用且安全的工具,去除了指针的一些缺点,禁止了部分不安全的操作。
  • 程序员拥有引用,但编译器仅拥有指针(地址)

普通变量的引用

1、系统不会为引用 开辟空间
2、引用必须初始化

int a = 10;

//需求:给变量a 取个别名叫b
//定义的时候 &修饰变量为引用 b就是a的别名(引用)
//系统不会为引用 开辟空间
int &b = a;//引用必须初始化

//a和b代表同一空间内容
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
//地址相同
cout<<"&a = "<<&a<<endl;
cout<<"&b = "<<&b<<endl;

//操作b等价操作a
b = 100;
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;

数组的引用

int arr[5]={
    
    10,20,30,40,50};
int n = sizeof(arr)/sizeof(arr[0]);

int (&myArr)[5] = arr;
int i=0;
for(i=0;i<n;i++)
{
    
    
	cout<<myArr[i]<<" ";//10 20 30 40 50
}
cout<<endl;

指针变量的引用

int num = 10;
int *p = &num;
int* &myP = p;

cout<<"*p = "<<*p<<endl;//10
cout<<"*myP = "<<*myP<<endl;//10

函数的引用

void fun01(void)
{
    
    
	cout<<"fun01"<,endl;
}
void (&myFun)(void) = fun01;

myFun();//fun01

引用作为函数的参数

  • 函数内部可以 通过 引用 操作外部变量

  • 这种方法也叫按引用传递或传引用。(传值、传地址、传引用只是说法不同,其实都是传值。)

  • 引用的本质是指针,传递的是变量的地址,在函数中,修改形参会影响实参。

void swap01(int *p1, int *p2)
{
    
    
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
	
}
void swap02(int &x, int &y)
{
    
    
	//形参赋值其实有如下赋值语句:int &x=a, int &y=b
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
    
    
	int a = 10;
	int b = 20;
	cout<<"a = "<<a<<", b = "<<b<<endl;//a = 10, b = 20
	//swap01(&a, &b);
	swap02(a, b);
 
	cout<<"a = "<<a<<", b = "<<b<<endl;//a = 20, b = 10
}

引用的语法更清楚简单:

  1. 函数调用时传递的实参不必加“&”符
  2. 在被调函数中不必在参数前加“*”符引用作为其它变量的别名而存在,因此在一些场合可以代替指针。
  3. C++主张用引用传递取代地址传递的方式,因为引用语法容易且不易出错。

4) 传引用不必使用二级指针。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

void func1(int** p)      // 传地址,实参是指针的地址,形参是二级指针。
{
    
    
	*p = new int(3);       // p是二级指针,存放指针的地址。
	cout << "func1内存的地址是:" << *p << ",内存中的值是:" << **p << endl;
}

void func2(int*& p)     // 传引用,实参是指针,形参是指针的别名。
{
    
    
	p = new int(3);         // p是指针的别名。
	cout << "func2内存的地址是:" << p << ",内存中的值是:" << *p << endl;
}

int main()
{
    
    
	int* p = nullptr;    // 存放在子函数中动态分配内存的地址。

	func1(&p);      // 传地址,实参填指针p的地址。
	func2(p);      // 传引用,实参填指针p。

	cout << "main 内存的地址是:" << p << ",内存中的值是:" << *p << endl;

	delete p;
}

引用作为函数的返回值类型

函数的返回值被拷贝到一个临时位置(小就放在寄存器中或大就放在栈),然后调用者程序再使用这个值。

cout << sqrt(25); 
//sqrt(25)的返回值5被拷贝到临时的位置,然后传递给cout。

如果返回的是一个结构体,同样是将把整个结构体拷贝到临时的位置。【解决方法是返回地址或引用,不会导致内存拷贝】

语法:

返回值的数据类型& 函数名(形参列表);

注意:

  • 如果返回局部变量的引用,其本质是野指针,后果不可预知。
  • 可以返回函数的引用形参、类的成员、全局变量、静态变量static。
  • 返回引用的函数是被引用的变量的别名,将const用于引用的返回类型,这样变量就无法修改。

返回函数的引用形参【cout实现的原理】

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

const int &func2(int &ra)    // 返回的是引用。将const用于引用的返回类型,这样变量就无法修改
{
    
    
	ra++;
	cout << "ra的地址是:" << &ra << ",ra=" << ra << endl;
	return ra;
}

int main()
{
    
    
	int a = 3;
	const int& b = func2(a);      // 返回的是引用。

	cout << " a的地址是:" << &a << ", a=" << a << endl;
	cout << " b的地址是:" << &b << ", b=" << b << endl;

	// func2(a) = 10;             // 返回引有的函数是被引用的变量的别名。由于返回值是const,不能被修改

	// cout << " a的地址是:" << &a << ", a=" << a << endl;
	// cout << " b的地址是:" << &b << ", b=" << b << endl;
}

1、不要返回普通局部变量的引用

int& getData(void)
{
    
    
	int num = 10;
	//不要返回局部变量的引用,其本质是野指针,后果不可预知。
	return num;//返回num 函数调用的结果 就是num的别名
}
int main()
{
    
    
	//b就是num的别名
	int &b = getData();//b是getData()的别名,而getData()是num别名,所以间接可得b是num别名
}

2、返回值类型为引用 可以完成链式操作(调用)

struct Stu
{
    
    
	//结构体中可以定义成员函数
	//函数的成员是引用
	//函数的返回值是引用
	Stu& printStu(Stu &ob, int value)
	{
    
    
		cout<<value<<" ";
		return ob;
	}
};
int main()
{
    
    
	Stu ob1;
	ob1.printStu(ob1, 100).printStu(ob1, 200).printStu(ob1, 300);//100 200 300
}

引用的形参和const

如果引用的数据对象类型不匹配,当引用为const时,C++将创建临时变量,让引用指向临时变量。
什么时候将创建临时变量呢?

  • 引用是const。
  • 数据对象的类型是正确的,但不是左值。
  • 数据对象的类型不正确,但可以转换为正确的类型。

结论:如果函数的实参不是左值或与const引用形参的类型不匹配,那么C++将创建正确类型的匿名变量,将实参的值传递给匿名变量,并让形参来引用该变量。

将引用形参声明为const的理由有三个:

  • 使用const可以避免无意中修改数据的编程错误。【最主要用途】
  • 使用const使函数能够处理const和非const实参,否则将只能接受非const实参。
  • 使用const,函数能正确生成并使用临时变量。

左值与非左值

  • 左值是可以被引用的数据对象,可以通过地址访问它们,例如:变量、数组元素、结构体成员、引用和解引用的指针。
  • 非左值包括字面常量(用双引号包含的字符串除外)和包含多项的表达式。【字符串常量有地址】
#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

void func1(int no, string str)    // 传值。
{
    
    
	cout << "亲爱的" << no << "号:" << str << endl;
}

void func2(const int* no,const string* str)    // 传地址。
{
    
    
	cout << "亲爱的" << *no << "号:" << *str << endl;
}

void func3(const int& no, const string& str)    // 传引用。
{
    
    
	cout << "亲爱的" << no << "号:" << str << endl;
}

int main()
{
    
    
	

	func1(8, "我是一只小小鸟。");
	func2(8, "我是一只小小鸟。");//传递地址即便加了const也不能转换
	func3('X', "我是一只小小鸟。");//有const,c风格的字符串会转化为形参string,字符常量会转化为形参int类型
	 
}

补充:

1、给常量取别名,不能通过常引用 修改 内容。

//int &a = 10;//err
const int &a = 10;//a就是10的别名
//a=100;//err,用const修饰后不能修改
cout<<a<<endl;//10

2、常引用 作为函数的参数:防止函数内部修改外部的值。【主要】

//形参不加const则会导致函数内部可以修改函数外部的值
void printInt(const int &a)
{
    
    
	//a = 200;//err
	cout<<"a = "<<a<<endl;
}
int main()
{
    
    
	int num = 100;
	printInt(num);//a = 100
}

各种形参的使用场景

传值、传地址和传引用的指导原则《C++ Primer Plus》

1)如果不需要在函数中修改实参

  • 如果实参很小,如C++内置的数据类型或小型结构体,则按值传递。
  • 如果实参是数组,则使用const指针,因为这是唯一的选择(没有为数组建立引用的说法)
  • 如果实参是较大的结构,则使用const指针或const引用。
  • 如果实参是类,则使用const引用,传递类的标准方式是按引用传递(类设计的语义经常要求使用引用)。

2)如果需要在函数中修改实参

  • 如果实参是内置数据类型,则使用指针。只要看到func(&x)的调用,表示函数将修改x。
  • 如果实参是数组,则只能使用指针。
  • 如果实参是结构体,则使用指针或引用。
  • 如果实参是类,则使用引用。

例如:对于基本类型,cin使用引用,因此可以使用cin>>a,而不是cin>>&a。

内联函数

声明内联函数

内联函数 必须在定义的时候 使用关键字inline修饰, 不能在声明的时候使用inline

//函數声明的时候 不要使用inline
int myAdd(int x, int y);
int main()
{
    
    
	cout<<myAdd(100,200)<<endl;
}
//内联函数 在定义的时候使用inline
inline int myAdd(int x, int y)
{
    
    
	return x+y;
}

内联函数:在编译阶段 将内联函数中的函数体 替换函数调用处。避免函数调用时的开销。

宏函数和内联函数的区别

宏函数和内联函数 都会在适当的位置 进行展开 避免函数调用开销。

宏函数在预处理阶段展开
内联函数在编译阶段展开

宏函数的参数没有类型,不能保证参数的完整性。
内联函数的参数有类型 能保证参数的完整性。

宏函数没有作用域的限制,不能作为命名空间、结构体、类的成员
内联函数有作用域的限制,能作为命名空间、结构体、类的成员

内联函数的注意事项

在内联函数定义的时候加inline修饰

类中的成员函数 默认都是内联函数(不加inline 也是内联函数)

有时候 就算加上inline也不一定是内联函数(内联函数条件)

  • 不能存在任何形式的循环语句
  • 不能存在过多的条件判断语句
  • 函数体不能过于庞大
  • 不能对函数取地址

有时候不加inline修饰 也有可能是内联函数。
内不内联 由编译器决定。

函数重载

函数重载的概述

能使名字方便使用,是任何程序设计语言的一个重要特征!

同样一个字在不同的场景下具有不同的含义。那么在c++中也有一种类似的现象出现,同一个函数名在不同场景下可以具有不同的含义。

函数重载 是c++的多态的特性(静态多态)。

函数重载:用同一个函数名 代表不同的函数功能。

注意:

  • 在实际开发中,视需求重载各种数据类型,不要重载功能不同的函数。
  • 使用重载函数时,如果数据类型不匹配,C++尝试使用类型转换与形参进行匹配,如果转换后有多个函数能匹配上,编译将报错。
  • 引用可以作为函数重载的条件,但是,调用重载函数的时候,如果实参是变量,编译器将形参类型的本身和类型引用视为同一特征。
  • 如果重载函数有默认参数,调用函数时,可能导致匹配失败。
  • const不能作为函数重载的特征。

函数重载的条件

同一作用域,函数的参数类型不同、个数不同、顺序不同 都可以重载。(返回值类型不能作为重载的条件)

void printFun(int a)
{
    
    
	cout<<"int"<<endl;
}
void printFun(int a, char b)
{
    
    
	cout<<"int char"<<endl;
}
void printFun(char a, int b)
{
    
    
	cout<<"char int"<<endl;
}
void printFun(char a)
{
    
    
	cout<<"char"<<endl;
}
int main()
{
    
    
	printFun(10);//int
	printFun(10, 'a');//int char
	printFun('a', 10);//char int
	printFun('a');//char
}

思考:为什么函数返回值不作为重载条件呢?

  • 当编译器能从上下文中确定唯一的函数的时,如int ret =func(),这个当然是没有问题的。然而,我们在编写程序过程中可以忽略他的返回值。那么这个时候,一个函数为 void func(int x);另一个为int func(int x);当我们直接调用func(10),这个时候编译器就不确定调用那个数。所以在c++中禁止使用返回值作为重载的条件

函数重载的底层实现原理

void func(){
    
    }
void func(int x){
    
    }
void func(int x,char y){
    
    }

以上三个函数在linux下生成的编译之后的函数名为

_Z4funcv //v 代表void,无参数
_Z4funci //i 代表参数为int类型
_Z4funcic //i 代表第一个参数为int类型,第二个参数为char类型

不同的编译器可能会产生不同的内部名,只是举例说明

函数的默认参数

c++在声明函数原型的时可为一个或者多个参数指定默认(缺省)的参数值,当函数调用的时候如果没有指定这个值,编译器会自动用默认值代替。

语法:返回值 函数名(数据类型 参数=值, 数据类型 参数=值,……);

  • 如果函数的声明和定义是分开书写的,在函数声明中书写默认参数,函数的定义中不能书写默认参数。
  • 函数必须从右到左设置默认参数。也就是说,如果要为某个参数设置默认值,则必须为它后面所有的参数设置默认值。
  • 调用函数的时候,如果指定了某个参数的值,那么该参数前面所有的参数都必须指定。
#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

void func(int bh,const string &name="西施", const string& message="我喜欢你。")    // 向超女表白的函数。
{
    
    
	cout << "亲爱的"<<name<<"("<<bh<<"):" << message << endl;
}

int main()
{
    
    
	func(3,"冰冰","我是一只傻傻鸟。"); //非左值转换,形参需要加const
	func(5);
}
void TestFunc01(int a = 10, int b = 20){
    
    
	cout << "a + b = " << a + b << endl;
}

//注意点:
//1. 形参b设置默认参数值,那么后面位置的形参c也需要设置默认参数
void TestFunc02(int a,int b = 10,int c = 10){
    
    }

//2. 如果函数声明和函数定义分开,函数声明设置了默认参数,函数定义不能再设置默认参数
void TestFunc03(int a = 0,int b = 0);
void TestFunc03(int a, int b){
    
    }

int main(){
    
    

	//1.如果没有传参数,那么使用默认参数
	TestFunc01();
	//2. 如果传一个参数,那么第二个参数使用默认参数
	TestFunc01(100);
	//3. 如果传入两个参数,那么两个参数都使用我们传入的参数	
	TestFunc01(100, 200);
	return 0;
}

默认参数和函数重载同时出现 一定要注意二义性

void func(int x)
{
    
    
	cout<<"A:int x="<<x<<endl;
}
void func(int x,int y=10)
{
    
    
	cout<<"B:int x="<<x<<" y="<<y<<endl;
}
int main()
{
    
    
	func(10,20);//ok
	func(10);//err 产生二义性
}

占位参数

c++在声明函数时,可以设置占位参数。占位参数只有参数类型,而没有参数名。一般情况下,在函数体内部无法使用占位参数。

void TestFunc01(int a,int b,int){
    
    
	//函数内部无法使用占位参数
	cout << "a + b = " << a + b << endl;
}
//占位参数也可以设置默认值,但是无法使用
void TestFunc02(int a, int b, int = 20){
    
    
	//函数内部依旧无法使用占位参数
	cout << "a + b = " << a + b << endl;

}
int main(){
    
    

	//错误调用,占位参数也是参数,必须传参数
	//TestFunc01(10,20);
	
	//正确调用
	TestFunc01(10,20,30);
	
	//正确调用
	TestFunc02(10,20);
	
	//正确调用
	TestFunc02(10, 20, 30);
	return 0;
}

什么时候用,在后面我们要讲的操作符重载的后置++要用到这个.

extern “C” 浅析

以下在Linux下测试:

  • c函数: void MyFunc(){} ,被编译成函数: MyFunc

  • c++函数: void MyFunc(){},被编译成函数: _Z6Myfuncv

通过这个测试,由于c++中需要支持函数重载,所以c和c++中对同一个函数经过编译后生成的函数名是
不相同的,这就导致了一个问题,如果在c++中调用一个使用c语言编写模块中的某个函数,那么c++是
根据c++的名称修饰方式来查找并链接这个函数,那么就会发生链接错误,以上例,c++中调用MyFunc
函数,在链接阶段会去找Z6Myfuncv,结果是没有找到的,因为这个MyFunc函数是c语言编写的,生成
的符号是MyFunc。 那么如果我想在c++调用c的函数怎么办? extern "C"的主要作用就是为了实现c++代
码能够调用其他c语言代码。加上extern "C"后,这部分代码编译器按c语言的方式进行编译和链接,而 不是按c++的方式。

fun.h


#ifndef MYMODULE_H
#define MYMODULE_H
#include<stdio.h>
#if __cplusplus
extern "C"{
    
    
#endif
	extern void func1();
	extern int func2(int a,int b);
#if __cplusplus

}


#endif
#endif


fun.c

#include<stdio.h>

#include"fun.h"
void func1(){
    
    
	printf("hello world!");
}
int func2(int a, int b){
    
    
	return a + b;
}

main.cpp

#include<iostream>
#include "fun.h"

using namespace std;
int main(){
    
    
	func1();
	cout << func2(10, 20) << endl;
	return 0;
}

第十三章 类和对象

类和对象的基本概念

类的封装性

类将具有共性的数据和方法封装在一起,加以权限区分用户只能通过公共方法 访问私有数据

类的权限分为:private(私有)、protected(保护)、public(公有)3种权限。

在类的外部,只有public修饰的成员才能被访问,在没有涉及继承与派生时, private和protected是同
等级的,外部不允许访问。用户在类的外部可以通过public的方法间接访问private和protected数据

在这里插入图片描述
在一个类体的定义中,private 和 public 可以出现多次。

结构体的成员缺省为public,类的成员缺省为private。

private的意义在于隐藏类的数据和实现,把需要向外暴露的成员声明为public。

定义一个类

类的关键字:class

#include <iostream>

using namespace std;

//类Data 是一个类型
class Data
{
    
    
	//类中 默认为私有
private:
	int a;//不要给类中成员 初始化
protected://保护
	int b;
public://公共
	int c;
	//在类的内部 不存在 权限之分
	void showData(void)
	{
    
    
		cout<<a<<" "<<b<<" "<<c<<endl;
	}
};
int main()
{
    
    
	//类实例化一个对象
	Data ob;
	//类外不能直接访问 类的私有和保护数据
	//cout<<ob.a <<endl;//err
	//cout<<ob.b <<endl;//err
	cout<<ob.c <<endl;
	//类中的成员函数 需要对象调用
	ob.showData();
}

设计一个类步骤:思考该类有哪些数据成员 操作这些数据的成员函数 数据位私有 成员函数为公有

成员函数在类外实现

成员函数 :在类中声明, 类外实现

class Data
{
    
    
private:
	int mA;
public:
	//类中声明
	void setA(int a);
	int getA(void);
};
//类外实现,也属于类的内部,可以使用私有或保护的变量
//在qt中通过alt+enter可以自动写好类的外实现
void Data::setA(int a)
{
    
    
	mA = a;
}
int Data::getA()
{
    
    
	return mA;
}

类在其他源文件中实现

通过Qt Creator为c++工程添加一个类(步骤如下)

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

注意:类定义在同文件data.h中,而data.cpp是用来实现类的成员函数
data.h

#ifndef DATA_H
#define DATA_H
class Data
{
    
    
private:
	int mA;
public:
	void setA(int a);在qt中通过alt+enter可以自动写好类的外实现
	int getA(void);
};
#endif // DATA_H

data.cpp

#include "data.h"
void Data::setA(int a)
{
    
    
	mA=a;
}
int Data::getA()
{
    
    
	return mA;
}

main.cpp

#include <iostream>
#include "data.h"
using namespace std;

int main(int argc, char *argv[])
{
    
    
	Data ob;
	ob.setA(100);
	cout<<"mA = "<<ob.getA()<<endl;//mA = 100
	return 0;
}

简单使用类

  • 1)类的成员函数可以直接访问该类其它的成员函数(可以递归)。【类的成员函数可以借助成员变量保存数据的状态】
  • 2)类的成员函数可以重载,可以使用默认参数。
  • 3)类指针的用法与结构体指针用法相同。
  • 4)类的成员可以是任意数据类型(类中使用枚举,可读性更好,作用域是类,不是整个程序)。
  • 5)可以为类的成员指定缺省值(C++11标准)。
  • 6)类可以创建对象数组,就像结构体数组一样。
  • 7)对象可以作为实参传递给函数,一般传引用。
  • 8)可以用new动态创建对象,用delete释放对象。
  • 9)在类的外部,一般不直接访问(读和写)对象的成员,而是用成员函数。数据隐藏是面向对象编程的思想之一。
  • 10)对象一般不用memset()清空成员变量,可以写一个专用于清空成员变量的成员函数。
  • 11)对类和对象用sizeof运算意义不大,一般不用。
  • 12)用结构体描述纯粹的数据,用类描述对象。【纯粹的数据类型是指结构体中只有C++内置的数据类型和C风格的字符串】【且用于操作系统提供的库函数和网络通信的函数只有结构体,没有类这个定义】
  • 13)在类的声明中定义的函数都将自动成为内联函数;在类的声明之外定义的函数如果使用了inline限定符,也是内联函数。
  • 14)为了区分类的成员变量和成员函数的形参,把成员变量名加m_前缀或_后缀,如m_name或name_。

构造函数

  • 所谓的构造函数就是为了初始化数据成员,例如:基类构造函数负责初始化被继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。

初始化和清理

当我们创建对象的时候,这个对象应该有一个初始状态,当对象销毁之前应该销毁自己创建的一些数据。对象的初始化和清理也是两个非常重要的安全问题,一个对象或者变量没有初始时,对其使用后果是未知,同样的使用完一个变量,没有及时清理,也会造成一定的安全问题。c++为了给我们提供这种问题的解决方案,构造函数和析构函数,这两个函数将会被编译器自动调用,完成对象初始化和对象清理工
作。 无论你是否喜欢,对象的初始化和清理工作是编译器强制我们要做的事情,即使你不提供初始化操
作和清理操作,编译器也会给你增加默认的操作,只是这个默认初始化操作不会做任何事,所以编写类 就应该顺便提供初始化函数

构造函数的概述

类实例化对象的时候, 系统自动调用构造函数 ,完成对象的初始化。
如果用户不提供构造函数 编译器 会自动添加一个默认的构造函数(空函数)

构造函数的定义方式

构造函数名 和 类名相同,没有返回值类型(连void都不可以),可以有参数(可以重载),权限为 public

先给对象开辟空间(实例化) 然后调用构造函数(初始化)。

class Data
{
    
    
public:
	int mA;
public:
	//无参构造函数
	Data()
	{
    
    
		mA=0;
		cout<<"无参构造函数"<<endl;
	}
	//有参构造函数
	Data(int a)
	{
    
    
		mA=a;
		cout<<"有参构造函数 mA="<<mA<<endl;
	}
};
int main()
	{
    
    
	//隐式调用无参构造函数(推荐)
	Data ob1;
	//显示调用无参构造函数
	Data ob2 = Data();
	//隐式调用有参构造函数(推荐)
	Data ob3(10);
	//显示调用有参构造函数
	Data ob4 = Data(10);
	//匿名对象(无参) 当前语句技术 立即释放
	Data();
	Data(20);
	//构造函数隐式转换(类中只有一个数据成员)
	Data ob5 = 100;
}

在这里插入图片描述

提供构造函数的影响

如果用户不提供任何构造函数 编译器默认提供一个空的无参构造。
如果用户定义了构造函数(不管是有参、无参),编译器不再提供默认构造函数。
例如:自己写了有参构造后系统不会提供无参构造,如果再使用无参构造则会报错

#include <iostream>

using namespace std;
class Data
{
    
    
public:
    int mA;
public:
#if 0
    //无参构造函数
    Data()
    {
    
    
        mA=0;
        cout<<"无参构造函数"<<endl;
    }
#endif
#if 1
    //有参构造函数
    Data(int a)
    {
    
    
        mA=a;
        cout<<"有参构造函数 mA="<<mA<<endl;
    }
#endif
};
#include<string.h>

int main(int argc, char *argv[])
{
    
    
    Data ob;//报错
    cout<<ob.mA<<endl;
    return 0;
}

析构函数

析构函数的定义方式

函数名和类名称相同,在函数名前加~,没有返回值类型,没有函数形参。(不能被重载)

当对象生命周期结束的时候 系统自动调用析构函数。

先调用析构函数 再释放对象的空间。

class Data1
{
    
    
public:
	int mA;
public:
	//无参构造函数
	Data1()
	{
    
    
		mA=0;
		cout<<"无参构造函数"<<endl;
	}
	//有参构造函数
	Data1(int a)
	{
    
    
		mA=a;
		cout<<"有参构造函数 mA="<<mA<<endl;
	}
	//析构函数
	
	
	~Data1()	
	{
    
    
		cout<<"析构函数 mA="<<mA<<endl;
	}
};

ob2和ob4是栈的结构,所以析构时ob4先出栈

在这里插入图片描述

一般情况下,系统默认的析构函数就足够。但是如果一个类有指针成员,这个类必须 ,写析构函数,释放 ,指针成员所指向空间。

系统默认回收对象本身的空间,不会回收对象中的指针指向的空间

#include<string.h>
class Data2
{
    
    
public:
	char *name;
public:
	Data2(){
    
    
		name=NULL;
	}
	Data2(char *str)
	{
    
    
		//指针成员指向堆空间,需要写析构函数清理堆空间
		name = new char[strlen(str)+1];
		strcpy(name, str);
		cout<<"有参构造"<<endl;
	}
	
	
	~Data2()
	{
    
    
		if(name != NULL)
		delete [] name;
		cout<<"析构函数"<<endl;
	}
};
int main(int argc, char *argv[])
{
    
    
	Data2 ob("hello world");
	cout<<ob.name<<endl;
	return 0;
}

注意事项

  • 创建对象的时候,如果重载了构造函数,编译器根据实参匹配相应的构造函数。没有参数的构造函数也叫默认构造函数。

  • 创建对象的时候(如果没有构造函数、构造函数没有参数、构造函数的参数都有默认参数)不要在对象名后面加空的圆括号,编译器误认为是声明函数,在使用对象里面的变量时会报错

  • 在构造函数名后面加括号和参数不是调用构造函数,是创建匿名对象

  • 以下两行代码有本质的区别:

//只构造和析构一次
CGirl girl = CGirl("西施"20);  // 显式创建对象。
//构造和析构两次
CGirl girl;                   // 创建对象。
girl = CGirl("西施"20);        // 创建匿名对象,然后给现有的对象赋值。
  • 用new/delete创建/销毁对象时,也会调用构造/析构函数。
// CGirl *girl=new CGirl;                   // 创建超女对象,不设置任何初始值。
// CGirl *girl=new CGirl("西施");        // 创建超女对象,为成员姓名设置初始值。
CGirl *girl=new CGirl("西施",8);     // 创建超女对象,为成员姓名和年龄设置初始值。

girl->show();    // 显示超女的自我介绍。
delete girl;
  • 不建议在构造/析构函数中写太多的代码,可以调用成员函数。
  • 除了初始化,不建议让构造函数做太多工作(只能成功不会失败)。
  • C++11支持使用统一初始化列表。
CGirl girl = {
    
    "西施"20};
CGirl girl  {
    
    "西施"20};
CGirl* girl = new CGirl{
    
     "西施"20 };

拷贝构造函数

拷贝构造函数的定义

拷贝构造:本质是构造函数

拷贝构造的调用时机:旧对象 给新对象 初始化 才会调用拷贝构造。

它的功能是把已存在对象的成员变量赋值给新对象的成员变量。

用一个已存在的对象创建新的对象语法:

类名 新对象名(已存在的对象名);
类名 新对象名=已存在的对象名;

拷贝构造函数的语法:

类名(const 类名& 对象名){
    
    ......}
#include <iostream>
using namespace std;
class Data
{
    
    
public:
	int mA;
public:
	Data()
	{
    
    
		cout<<"无参构造"<<endl;
	}
	Data(int a)
	{
    
    
		mA = a;
		cout<<"有参构造 mA="<<mA<<endl;
	}
#if 1

	//拷贝构造的定义形式:ob就是旧对象的引用
	//参数:当前类的常引用
	Data(const Data &ob)
	{
    
    
		//一旦实现了 拷贝构造函数 必须完成赋值操作
		mA = ob.mA;
		cout<<"拷贝构造函数"<<endl;
	}
#endif
	~Data()
	{
    
    
		cout<<"析构函数mA="<<mA<<endl;
	}
	};
int main(int argc, char *argv[])
{
    
    
	Data ob1(10);
	//旧对象给新对象初始化 就会调用拷贝构造函数
	Data ob2 = ob1;
	cout<<"ob2.mA ="<<ob2.mA<<endl;
	return 0;
}
  • 如果用户不提供拷贝构造 编译器会自动提供一个默认的拷贝构造(完成赋值动作–浅拷贝)

  • 人为写拷贝构造函数是为了完成深拷贝

  • 拷贝构造函数可以重载,可以有默认参数。

类名(......,const 类名& 对象名,......){
    
    ......}
  • 如果类中重载了拷贝构造函数却没有定义默认的拷贝构造函数,编译器也会提供默认的拷贝构造函数。

拷贝构造 和 无参构造 有参构造的关系

如果用户定义了 拷贝构造或者有参构造 都会屏蔽无参构造。
如果用户定义了 无参构造或者有参构造 不会屏蔽拷贝构造。

拷贝构造是特殊的有参构造,只是参数是类的常引用

拷贝构造几种调用形式

1、旧对象给新对象初始化 调用拷贝构造

Data ob1(10);
Data ob2 = ob1;//调用拷贝构造

2、给对象取别名 不会调用拷贝构造

Data ob1(10);
Data &ob2 = ob1;//不会调用拷贝构造

3、普通对象作为函数参数 调用函数时 会发生拷贝构造

void func(Data ob)//Data ob=ob1
{
    
    
}

int main()
{
    
    
	Data ob1(100);//有参构造
	func(ob1);//拷贝构造
}

4、函数返回值普通对象
Visual Studio会发生拷贝构造

在这里插入图片描述
在这里插入图片描述
函数返回值特点:

  • 返回到临时区(栈区)【当没有变量去接收时会自动清除】
  • 返回到寄存器

Qtcreater,linux不会发生
在这里插入图片描述
优化后该空间直接交给ob2析构

拷贝构造的浅拷贝和深拷贝

默认的拷贝构造 都是浅拷贝。
如果类中没有指针成员, 不用实现拷贝构造和析构函数。
如果类中有指针成员,且指向堆区空间, 必须实现析构函数释放指针成员指向的堆区空间,必须实现拷贝构造完成深拷贝动作。

#include<iostream>
#include<string.h>

using namespace std;
class Data5
{
    
    
public:
	char* name;
public:
	Data5()
	{
    
    
		name = NULL;
	}
	Data5(char* str)
	{
    
    
		name = new char[strlen(str) + 1];
		strcpy(name, str);
		cout << "有参构造 name=" << name << endl;
	}
	Data5(const Data5& ob)//深拷贝
	{
    
    
		//为对象的指针成员申请独立的空间
		name = new char[strlen(ob.name) + 1];
		strcpy(name, ob.name);
		cout << "拷贝构造函数" << endl;
	}
	
	
	~Data5()
	{
    
    
		cout << "析构函数name = " << name << endl;
		if (name != NULL)
		{
    
    
			delete [] name;
			name = NULL;
		}
	}
};
void test05()
{
    
    
	Data5 ob1((char *)"hello world\n");
	Data5 ob2 = ob1;
}

Data5 ob2 = ob1;实例化默认会发送浅拷贝,会导致在析构的时候会报错
在这里插入图片描述

初始化列表

构造函数的执行可以分成两个阶段:初始化阶段和计算阶段(初始化阶段先于计算阶段)。

  • 初始化阶段:全部的成员都会在初始化阶段初始化。
  • 计算阶段:一般是指用于执行构造函数体内的赋值操作。

构造函数除了参数列表和函数体之外,还可以有初始化列表。

初始化列表的语法:

类名(形参列表):成员一(值一), 成员二(值二),..., 成员n(值n)
{
    
    ......}

注意:

  • 1)如果成员已经在初始化列表中,则不应该在构造函数中再次赋值。如果在构造函数中赋值会覆盖掉
    在这里插入图片描述

  • 2)初始化列表的括号中可以是具体的值,也可以是构造函数的形参名,还可以是表达式。【构造函数形参可以取代掉】
    可以是构造函数的形参名
    在这里插入图片描述
    可以是表达式
    在这里插入图片描述

  • 3)初始化列表与赋值有本质的区别,如果成员是类,使用初始化列表调用的是成员类的拷贝构造函数,而赋值则是先创建成员类的对象(将调用成员类的普通构造函数),然后再赋值。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

class CBoy                // 男朋友类。
{
    
    
public:
    string m_xm;                      // 男朋友的姓名。
    CBoy()                                 // 没有参数的普通构造函数,默认构造函数。  
    {
    
     m_xm.clear();  cout << "调用了CBoy()构造函数。\n"; }
    CBoy(string xm)                 // 有一个参数的普通构造函数。  
    {
    
     m_xm = xm;  cout << "调用了CBoy(string xm)构造函数。\n"; }
    CBoy(const CBoy& bb)     // 默认拷贝构造函数。  
    {
    
     m_xm = bb.m_xm;  cout << "调用了CBoy(const CBoy &bb)拷贝构造函数。\n"; }
};

class CGirl                 // 超女类CGirl。
{
    
    
public:
    string    m_name;                     // 姓名属性。
    const int         m_age;               // 年龄属性。
    CBoy    m_boy;                         // 男朋友的信息。

    //采用赋值的方法调用的是成员对象的普通构造函数
    CGirl(string name, int age, CBoy boy) :m_name(name), m_age(age),      // 三个参数的普通构造函数。
    {
    
    
    	m_boy.m_xm = boy.m_xm;
        cout << "调用了CGirl(name,age,boy)构造函数。\n";
    }
    //形参为引用则不会调用成员对象的拷贝构造
    CGirl(string name, int age, CBoy& boy) :m_name(name), m_age(age),      // 三个参数的普通构造函数。
    {
    
    
    	m_boy.m_xm = boy.m_xm;
        cout << "调用了CGirl(name,age,boy)构造函数。\n";
    }
    //使用初始化列表会调用成员对象的拷贝构造
    CGirl(string name, int age, CBoy& boy) :m_name(name), m_age(age),m_boy(boy)      // 三个参数的普通构造函数。
    {
    
    
        cout << "调用了CGirl(name,age,boy)构造函数。\n";
    }
    // 超女自我介绍的方法,显示姓名、年龄、男朋友。
    void show() {
    
     cout << "姓名:" << m_name << ",年龄:" << m_age << ",男朋友:" << m_boy.m_xm << endl; }
};

int main()
{
    
    
    CBoy boy("哈哈");
    CGirl g1("冰冰",18,boy);
    g1.show();
}
  • 4)如果成员是类,初始化列表对性能略有提升。【因为有初始化列表,对象的初始化和赋初始值是一步操作,否则是2步操作】
  • 5)如果成员是常量和引用,必须使用初始列表,因为常量和引用只能在定义的时候初始化。
#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

class CBoy                // 男朋友类。
{
    
    
public:
    string m_xm;                      // 男朋友的姓名。
    CBoy()                                 // 没有参数的普通构造函数,默认构造函数。  
    {
    
     m_xm.clear();  cout << "调用了CBoy()构造函数。\n"; }
    CBoy(string xm)                 // 有一个参数的普通构造函数。  
    {
    
     m_xm = xm;  cout << "调用了CBoy(string xm)构造函数。\n"; }
    CBoy(const CBoy& bb)     // 默认拷贝构造函数。  
    {
    
     m_xm = bb.m_xm;  cout << "调用了CBoy(const CBoy &bb)拷贝构造函数。\n"; }
};

class CGirl                 // 超女类CGirl。
{
    
    
public:
    string    m_name;                     // 姓名属性。
    const int         m_age;                // 年龄属性为常量,则需要用初始化列表初始化
    CBoy&     m_boy;                         // 男朋友的信息。对象是引用则需要用初始化列表初始化
    
    CGirl(string name, int age, CBoy& boy) :m_name(name), m_age(age),m_boy(boy)      // 三个参数的普通构造函数。
    {
    
    
        cout << "调用了CGirl(name,age,boy)构造函数。\n";
    }
    // 超女自我介绍的方法,显示姓名、年龄、男朋友。
    void show() {
    
     cout << "姓名:" << m_name << ",年龄:" << m_age << ",男朋友:" << m_boy.m_xm << endl; }
};

int main()
{
    
    
    CBoy boy("子都");

    CGirl g1("冰冰",18,boy);
    
    g1.show();
}
  • 6)如果成员是没有默认构造函数的类,则必须使用初始化列表。
  • 7)拷贝构造函数也可以有初始化列表。
  • 8)类的成员变量可以不出现在初始化列表中。
  • 9)构造函数的形参先于成员变量初始化。

对象成员

在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象,叫做成员对象。

先调用对象成员的构造函数,再调用本身的构造函数。 析构函数和构造函数调用顺序相反,先自身,后成员
在这里插入图片描述

上述中,类会自动调用对象成员的无参构造。【默认】

初始化列表

类想调用对象成员 有参构造 必须使用初始化列表。

class A
{
    
    
public:
	int mA;
public:
	A()
	{
    
    
		mA = 0;
		cout<<"A的无参构造"<<endl;
	}
	A(int a)
	{
    
    
		mA = a;
		cout<<"A的有参构造"<<endl;
	}
	~A()
	{
    
    
		cout<<"A的析构函数"<<endl;
	}
};
class B
{
    
    
public:
	int mB;
	A ob;//成员对象
public:
	B()
	{
    
    
		cout<<"B类的无参构造"<<endl;
	}
	//初始化列表 成员对象 必须使用对象名+()
	//先执行ob(a),有参构造的隐式调用,此时a=10
	//然后执行b的有参构造
	B(int a, int b):ob(a)
	{
    
    
		mB = b;
		cout<<"B类的有参构造"<<endl;
	}
	~B()
	{
    
    
		cout<<"B的析构函数"<<endl;
	}
};
int main(int argc, char *argv[])
{
    
    
	B ob1(10,20);
	cout<<"mA ="<<ob1.ob.mA<<", mB ="<<ob1.mB<<endl;
	return 0;
}

explicit关键字

c++提供了关键字explicit,禁止通过构造函数进行的隐式转换。声明为explicit的构造函数不能在隐式转换中使用。

注意explicit用于修饰构造函数,防止隐式转化。 是针对单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造)而言。

class MyString{
    
    
public:
	explicit MyString(int n){
    
    
		cout << "MyString(int n)!" << endl;
	}
	MyString(const char* str){
    
    
		cout << "MyString(const char* str)" << endl;
	}
};
int main(){
    
    

	//给字符串赋值?还是初始化?
	//MyString str1 = 1;会发送隐式转换
	
	MyString str2(10);
	//寓意非常明确,给字符串赋值
	//MyString str3 = "abcd";会发送隐式转换
	MyString str4("abcd");
	return 0;
}

类的对象数组

对象数组:本质是数组 数组的每个元素是对象。

class A

{
    
    
public:
	int mA;
public:
	A()
	{
    
    
		mA = 0;
		cout<<"A的无参构造 mA="<<mA<<endl;
	}
	A(int a)
	{
    
    
		mA = a;
		cout<<"A的有参构造mA="<<mA<<endl;
	}
	

	~A()
	{
    
    
		cout<<"A的析构函数 mA = "<<mA<<endl;
	}
};
int main()
{
    
    

	//对象数组 每个元素都会自动调用构造和析构函数
	//该调用方式中 ,对象数组不初始化 每个元素 调用无参构造	
	A arr1[5];
	
	//该调用方式中 ,对象数组的初始化 必须显示使用有参构造 逐个元素初始化
	A arr2[5]={
    
    A(10),A(20),A(30),A(40),A(50) };
	int n =sizeof(arr2)/sizeof(arr2[0]);
	int i=0;
	for(i=0;i<n;i++)
	{
    
    
		cout<<arr2[i].mA<<" ";
	}
	cout<<endl;
}

析构采用栈的思想
在这里插入图片描述

动态对象创建

动态创建的概述

当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。 所以动态的意思意味着不确定性。 为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然c早就提供了动态内存分配(dynamicmemory allocation),函数malloc和free可以在运行时从堆中分配存储单元。 然而这些函数在c++中不能很好的运行,因为它不能帮我们完成对象的初始化工作。

c的方式创建动态对象

当创建一个c++对象时会发生两件事:

  1. 为对象分配内存
  2. 调用构造函数来初始化那块内存 第一步我们能保证实现,需要我们确保第二步一定能发生。c++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因。 C动态分配内存方法为了在运行时动态分配内存,c在他的标准库中提供了一些函数,malloc以及它的变种calloc和realloc,释放内存的free,这些函数是有效的、但是原始的,需要程序员理解和小心使用。为了使用c的动态内存分配函数在堆上创建一个类的实例,我们必须这样做:
class Person{
    
    
public:
	Person(){
    
    
		mAge = 20;
		pName = (char*)malloc(strlen("john")+1);
		strcpy(pName, "john");
	}
	void Init(){
    
    
		mAge = 20;
		pName = (char*)malloc(strlen("john")+1);
		strcpy(pName, "john");
	}
	void Clean(){
    
    
		if (pName != NULL){
    
    
			free(pName);
		}
	}
public:
	int mAge;
	char* pName;
};
int main(){
    
    
	//分配内存
	
	Person* person = (Person*)malloc(sizeof(Person));
	if(person == NULL){
    
    
		return 0;
	}
	
	//调用初始化函数
	person->Init();
	//清理对象
	person->Clean();
	//释放person对象

	free(person);
	return 0;
}

问题:

  1. 程序员必须确定对象的长度。
  2. malloc返回一个void指针,c++不允许将void赋值给其他任何指针,必须强转。
  3. malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。
  4. 用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是由编译器调用),用户有可能忘记调用初始化函数。

c的动态内存分配函数太复杂,容易令人混淆,是不可接受的,c++中我们推荐使用运算符new 和delete。

new创建动态对象

C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。

Person* person = new Person;

New操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单

delete释放动态对象

new表达式的反面是delete表达式。delete表达式先调用析构函数,然后释放内存。

class Person{
    
    
public:
	Person(){
    
    
		cout << "无参构造函数!" << endl;
		pName = new char[strlen("undefined") + 1];
		strcpy(pName, "undefined");
		mAge = 0;
	}
	Person(char* name, int age){
    
    
		cout << "有参构造函数!" << endl;
		pName = new char[strlen(name) + 1];
		strcpy(pName, name);
		mAge = age;
	}
	void ShowPerson(){
    
    
		cout << "Name:" << pName << " Age:" << mAge << endl;
	}
	~Person(){
    
    
		cout << "析构函数!" << endl;
		if (pName != NULL){
    
    
			delete [] pName;
			pName = NULL;
		}
	}
public:
	char* pName;
	int mAge;
};
int main(){
    
    
	Person* person1 = new Person;
	Person* person2 = new Person("John",33);
	person1->ShowPerson();
	person2->ShowPerson();
	delete person1;
	delete person2;
}

动态对象数组

当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数

class Person{
    
    
public:
	Person(){
    
    
		pName = NULL;
		mAge = 0;
	}
	Person(char* name, int age){
    
    
		pName = new char[strlen(name)+1];
		strcpy(pName, name);
		mAge = age;
	}
	~Person(){
    
    
		if (pName != NULL){
    
    
			delete [] pName;
		}
	}
public:
	char* pName;
	int mAge;
};
void test(){
    
    
	//栈聚合初始化局部数组
	Person person[] = {
    
     Person("john", 20), Person("Smith", 22) };
	cout << person[1].pName << endl;
	//创建堆上对象数组必须提供构造函数
	Person* workers = new Person[20];
	delete [] workers;//申请的时候有中括号,取消也需要中括号
}

静态成员

  • 在类定义中,它的成员(包括成员变量和成员函数),这些成员可以用关键字static声明为静态的,称为静态成员。 不管这个类创建了多少个对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象
    共享。
  • 用静态成员可以变量实现多个对象之间的数据共享,比全局变量更安全性。

静态成员变量

static修饰的静态成员 属于类而不是对象。(所有对象 共享 一份 静态成员数据)。

在这里插入图片描述

static修饰的成员 定义类的时候 必须分配空间。
static修饰的静态成员数据 必须类中定义 类外初始化。【因为静态成员数据是先于对象存在的, 静态成员变量不会在创建对象的时候初始化】

class Data
{
    
    
public:
	int a;//普通成员数据
	//类中定义
	static int b;//静态成员数据
};
//类外初始化
int Data::b=100;//不用加static
void test01()
{
    
    
	//静态成员数据 通过类名称直接访问(属于类),在还没定义对象之前
	cout<<Data::b<<endl;
	//静态成员数据 通过对象访问(共享)
	Data ob1;
	cout<<ob1.b<<endl;//100
	ob1.b = 200;
	Data ob2;
	ob2.b = 300;
	cout<<Data::b<<endl;//300
}

静态成员函数

静态成员函数 是属于类 而不是对象(所有对象 共享)

class Data
{
    
    
	static void func()//静态成员函数
	{
    
    
	}
}

当静态数据为私有的时候,需要通过静态函数获取私有数据

静态成员函数 可以直接通过类名称访问

在这里插入图片描述

静态成员函数内 只能操作静态成员数据。因为其他成员数据属于对象,而此时对象还没有创建,只存在类这个概念。
在非静态成员函数中,可以访问静态成员。
在这里插入图片描述
静态成员函数中没有this指针。因为没有对象

单例模式设计

单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案

只能实例化一个对象:通过构造函数私有化,外界无法调用构造函数去创建对象,只能在类中实例化

#include <iostream>
using namespace std;
class SingleTon//单例模式
{
    
    
//构造私有化 防止实例化其他对象
private:
	SingleTon(){
    
    
		count=0;
		cout<<"构造"<<endl;
	}
 
	SingleTon(const SingleTon &ob){
    
    
		count=0;
	}
	~SingleTon()
	{
    
    
		cout<<"析够"<<endl;
	}
private:
	//const防止p 在类内部 被修改指向
	static SingleTon * const p;//保存唯一的实例地址
	int count;//统计任务执行次数
public:
	static SingleTon * getSingleTon(void)//获取唯一的实例地址
	{
    
    
		return p;
	}
	//用户自定义 任务函数
	void printString(char *str)
	{
    
    
		count++;
		cout<<"当前第"<<count<<"次任务打印:"<<str<<endl;
	}
};
//类中声明,类外初始化,要加作用域才能访问到私有数据
SingleTon *const SingleTon::p = new SingleTon;//创建唯一的实例


int main(int argc, char *argv[])
{
    
    
	//获取单例的地址
	SingleTon *p1 =SingleTon::getSingleTon();
	p1->printString("离职证明1");
	p1->printString("学历证明1");
	p1->printString("学位证明1");
	p1->printString("身份证明1");
	SingleTon *p2 =SingleTon::getSingleTon();
	p2->printString("离职证明2");
	p2->printString("学历证明2");
	p2->printString("学位证明2");
	p2->printString("身份证明2");
	return 0;
}

在这里插入图片描述

c++面向对象模型

成员变量和成员函数的存储

c++实现了“封装”,“数据”和“处理数据的操作(函数)”是分开存储的。 c++中的非静态数据成员直接内含在 类对象中,成员函数虽然内含在class声明之内,却不出现在对象中。 每一个非内联成员函数只会诞生一份函数实例。
在这里插入图片描述

sizeof(Data1)的大小只是 a 和 b所占空间大小(类的对象所占空间大小)。

class MyClass01{
    
    
public:
	int mA;
};
class MyClass02{
    
    
public:
	int mA;
	static int mB;
};
class MyClass03{
    
    
public: 
	void printMyClass(){
    
    
		cout << "hello world!" << endl;
	}
public:
	int mA;
	static int mB;
};
class MyClass04{
    
    
public:
	void printMyClass(){
    
    
		cout << "hello world!" << endl;
}
	static void ShowMyClass(){
    
    
		cout << "hello world!" << endl;
	}
public:
	int mA;
	static int mB;
};
int main(){
    
    
	MyClass01 mclass01;
	MyClass02 mclass02;
	MyClass03 mclass03;
	MyClass04 mclass04;
	cout << "MyClass01:" << sizeof(mclass01) << endl; //4
	//静态数据成员并不保存在类对象中
	cout << "MyClass02:" << sizeof(mclass02) << endl; //4
	//非静态成员函数不保存在类对象中
	cout << "MyClass03:" << sizeof(mclass03) << endl; //4
	//静态成员函数也不保存在类对象中
	cout << "MyClass04:" << sizeof(mclass04) << endl; //4
	return 0;
}

通过上面的案例,我们可以的得出:C++类对象中的变量和函数是分开存储。

C++类中有两种数据成员:nonstatic、static,三种函数成员:nonstatic、static、virtual。

对象指针表负责管理地址【类对象中变量和函数的地址是连续的,但是静态成员变量和成员函数在静态存储区中,不是在对象中】
在这里插入图片描述

  • 对象内存的大小包括:1)所有非静态数据成员的大小;2)由内存对齐而填补的内存大小;3)为了支持virtual成员而产生的额外负担。
  • 对象的地址是第一个非静态成员变量的地址,如果类中没有非静态成员变量,编译器会隐含的增加一个1字节的占位成员。

this指针

  • 如果类的成员函数中涉及多个对象,在这种情况下需要使用this指针。
  • this指针存放了对象的地址,它被作为隐藏参数传递给了成员函数,指向调用成员函数的对象(调用者对象)。
  • 每个成员函数(包括构造函数和析构函数)都有一个this指针,可以用它访问调用者对象的成员。(可以解决成员变量名与函数形参名相同的问题)
  • *this可以表示对象。
  • 如果在成员函数的括号后面使用const,那么将不能通过this指针修改成员变量。

1、this指针工作原理

通过上例我们知道,c++的数据和操作也是分开存储,并且每一个非内联成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码 。那么问题是:这一块代码是如何区分那个对象调用自己的呢?

在这里插入图片描述

c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象

成员函数通过this指针即可知道操作的是那个对象的数据。·This指针是一种隐含指针·,它隐含于每个类的非静态成员函数中。This指针无需定义,直接使用即可。

注意:静态成员函数内部没有this指针,静态成员函数不能操作非静态成员变量【因为和对象有关,而静态成员变量和静态成员函数都与对象无关】

2、函数形参和成员同名可以使用this指针解决。

构造函数也属于成员函数(非静态),此时this指向调用的对象,this保存的是调用对象的地址
在这里插入图片描述

3、this来完成链式操作

构造函数和析构函数也有this

cout方法原理就是这个
在这里插入图片描述

const修饰成员函数

用const修饰的成员函数时,const修饰this指针指向的内存区域,成员函数体内不可以修改本类中的任何普通成员变量, 当成员变量类型符前用mutable修饰时例外

class Data
{
    
    
public:
	int a;
	int b;
	mutable int c;
public:
	Data(int a, int b,int c)
	{
    
    
		this->a = a;
		this->b = b;
		this->c = c;
	}
	//const 修饰成员函数为只读(该成员函数不允许对 成员数据 赋值) mutable修饰的成员除外
	//该函数不可以修改普通成员数据
	void showData(void) const
	{
    
    
		//a = 100;//err
		c=100;
		cout<<a<<" "<<b<<" "<<c<<endl;
	}
};
int main()
{
    
    
	Data ob1(10,20,30);
	ob1.showData();
}

在类的成员函数后面加const关键字,表示在成员函数中保证不会修改调用对象的成员变量。
注意:

  • 1)mutable可以突破const的限制,被mutable修饰的成员变量,将永远处于可变的状态,在const修饰的函数中,mutable成员也可以被修改。
  • 2)非const成员函数可以调用const成员函数和非const成员函数。
  • 3)const成员函数不能调用非const成员函数。
  • 4)非const对象可以调用const修饰的成员函数和非const修饰的成员函数。
  • 5)const对象只能调用const修饰的成员函数,不能调用非cosnt修饰的成员函数。【常对象只能访问加了const的成员函数】
  • 6)构造函数和析构函数不允许使用const

这里出现了令人纠结的三个问题:

  • 1、为什么要保护类的成员变量不被修改?
  • 2、为什么用const保护了成员变量,还要再定义一个mutable关键字来突破const的封锁线?
  • 3、到底有没有必要使用const和mutable这两个关键字?
    保护类的成员变量不在成员函数中被修改,是为了保证模型的逻辑正确,通过用const关键字来避免在函数中错误的修改了类对象的状态。并且在所有使用该成员函数的地方都可以更准确的预测到使用该成员函数的带来的影响。而mutable则是为了能突破const的封锁线,让类的一些次要的或者是辅助性的成员变量随时可以被更改。没有使用const和mutable关键字当然没有错,const和mutable 关键字只是给了建模工具更多的设计约束和设计灵活性,而且程序员也可以把更多的逻辑检查问题交给编译器和建模工具去做,从而减轻程序员的负担。
#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

class CGirl                 // 超女类CGirl。
{
    
    
public:
    mutable string    m_name;                     // 姓名属性。
    int         m_age;                         // 年龄属性。

    // 两个参数的普通构造函数。
    CGirl(const string &name, int age) 
    {
    
     m_name = name; m_age = age;  cout << "调用了CGirl(name,age)构造函数。\n"; }
    
    // 超女自我介绍的方法,显示姓名、年龄。
    void show1() const
    {
    
     
        m_name="西施show1";
        cout << "姓名:" << m_name << ",年龄:" << m_age << endl; 
        show2();//const成员函数不能调用非const成员函数
    }
    void show2() const
    {
    
    
        m_name = "西施show2";
        cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
    }
    void show3() 
    {
    
    
        m_name = "西施show3";
        cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
    }
    void show4() 
    {
    
    
        m_name = "西施show4";
        cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
    }
};

int main()
{
    
    
    const CGirl g1("冰冰",18);
    
    g1.show1();//const对象只能调用const修饰的成员函数
}

友元

类的主要特点之一是数据隐藏,即类的私有成员无法在类的外部(作用域之外)访问。但是,有时候需要在类的外部访问类的私有成员,怎么办?

解决方法是使用友元函数,友元函数是一种特权函数,c++允许这个特权函数访问私有成员。

这一点从现实生活中也可以很好的理解: 比如你的家,有客厅,有你的卧室,那么你的客厅是Public的,所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去,但是呢,你也可以允许你的闺蜜好基友进去。 程序员可以把一个全局函数、某个类中的成员函数、甚至整个类声明为友元。

友元的语法

使用friend关键字声明友元。

friend关键字只出现在声明处一个函数或者类 作为了另一个类的友元 那么这个函数或类 就可以直接访问 另一个类的私有数据。

友元 重要用在运算符重载上。

普通全局函数作为类的友元

#include <string>
using namespace std;
class Room
{
    
    
	friend void visiting01(Room &room);
private:
	string bedRoom;//卧室
public:
	string setingRoom;//客厅
public:
	Room(string bedRoom, string setingRoom)
	{
    
    
		this->bedRoom = bedRoom;
		this->setingRoom = setingRoom;
	}
};
//普通全局函数
void visiting01(Room &room)
{
    
    
	cout<<"访问了"<<room.setingRoom<<endl;
	cout<<"访问了"<<room.bedRoom<<endl;
}
int main(int argc, char *argv[])
{
    
    
	Room room("卧室","客厅");
	visiting01(room);
	return 0;
}

类的某个成员函数 作为另一个类的友元

注意顺序:类的友元写在前面 ,同时他的成员函数要在所有类的最下方外部实现

class Room;//向前声明 只能说明类名称
class goodGay
{
    
    
public:
	void visiting01(Room &room);
	void visiting02(Room &room);
};


 
class Room
{
    
    
	friend void goodGay::visiting02(Room &room);
private:
	string bedRoom;//卧室
public:
	string setingRoom;//客厅
public:
	Room(string bedRoom, string setingRoom)
	{
    
    
		this->bedRoom = bedRoom;
		this->setingRoom = setingRoom;
	}
};
void goodGay::visiting01(Room &room)
{
    
    
	cout<<"访问了"<<room.setingRoom<<endl;
	//cout<<"访问了"<<room.bedRoom<<endl;
}
void goodGay::visiting02(Room &room)
{
    
    
	cout<<"好基友张三访问了"<<room.setingRoom<<endl;
	cout<<"好基友张三访问了"<<room.bedRoom<<endl;
}
int main(int argc, char *argv[])
{
    
    
	Room room("卧室","客厅");
	goodGay ob;
	ob.visiting01(room);
	ob.visiting02(room);
	return 0;
}

整个类作为 另一个类的友元

这个类的所有成员函数 都可以访问另一个类的私有数据.

class Room;//向前声明 只能说明类名称
class goodGay
{
    
    
public:
	void visiting01(Room &room);
	void visiting02(Room &room);
};
class Room
{
    
    
	friend class goodGay;
private:
	string bedRoom;//卧室
public:
	string setingRoom;//客厅
public:
	Room(string bedRoom, string setingRoom) 
{
    
    

	this->bedRoom = bedRoom;
	this->setingRoom = setingRoom;
}
};
void goodGay::visiting01(Room &room)
{
    
    
	cout<<"访问了"<<room.setingRoom<<endl;
	cout<<"访问了"<<room.bedRoom<<endl;
}
void goodGay::visiting02(Room &room)
{
    
    
	cout<<"好基友访问了"<<room.setingRoom<<endl;
	cout<<"好基友访问了"<<room.bedRoom<<endl;
}
int main(int argc, char *argv[])
{
    
    
	Room room("卧室","客厅");
	goodGay ob;
	ob.visiting01(room);
	ob.visiting02(room);
	return 0;
}

友元的注意事项

1.友元关系不能被继承。
2.友元关系是单向的,类A是类B的朋友,但类B不一定是类A的朋友。
3.友元关系不具有传递性。类B是类A的朋友,类C是类B的朋友,但类C不一定是类A的朋友

友元案例(遥控器的类)

请编写电视机类,电视机有开机和关机状态,有音量,有频道,提供音量操作的方法,频道操作的方
法。由于电视机只能逐一调整频道,不能指定频道,增加遥控类,遥控类除了拥有电视机已有的功能,
再增加根据输入调台功能。

提示:遥控器可作为电视机类的友元类

#include <iostream>
using namespace std;
class TV;
//遥控器的类作为TV的友元
class Remote
{
    
    
private:
	TV *p;
public:
	Remote(TV *p);
	void offOrOn(void);
	void upVolume(void);
	void downVolume(void);
	void upChannel(void);
	void downChannel(void);
	void showTv(void);
	void setChannel(int channel);
};
class TV
{
    
    
	friend class Remote;
	enum{
    
    OFF,ON};
	enum{
    
    minVol, maxVol=10};
	enum{
    
    minChan, maxChan=25};
private:
	int state;
	int volume;
	int channel;
public:
	TV()
	{
    
    
		state = OFF;
		volume = minVol;
		channel = minChan;
	}
	void offOrOn(void);
	void upVolume(void);
	void downVolume(void);
	void upChannel(void);
	void downChannel(void);
	void showTv(void);
};
int main(int argc, char *argv[])
{
    
    
	TV tv;
	Remote re(&tv);
	re.offOrOn();
	re.upVolume();
	re.upVolume();
	re.setChannel(10);
	re.showTv();
	return 0;
}
void TV::offOrOn()
{
    
    
	state = (state==OFF?ON:OFF);
}
void TV::upVolume()
{
    
    
	if(volume == maxVol)
	{
    
    
		cout<<"音量已经最大"<<endl;
		return;
	}
	volume++;
}
void TV::downVolume()
{
    
    
	if(volume ==minVol)
	{
    
    
		cout<<"音量已经最小"<<endl;
		return;
	}
	volume--;
}
void TV::upChannel()
{
    
    
	if(channel == maxChan)
	{
    
    
		cout<<"频道已经最大"<<endl;
		return;
	}
	channel++;
}
void TV::downChannel()
{
    
    
	if(channel == minChan)
	{
    
    
		cout<<"频道已经最小"<<endl;
		return;
	}
	channel--;
}
void TV::showTv()
{
    
    
	cout<<"当前电视机的状态:"<<(state==OFF?"关":"开")<<endl;
	cout<<"当前电视机的音量:"<<volume<<endl;
	cout<<"当前电视机的频道:"<<channel<<endl;
}
Remote::Remote(TV *p)
{
    
    
	this->p = p;
}
void Remote::offOrOn()
{
    
    
	p->offOrOn();
}
void Remote::upVolume()
{
    
    
	p->upVolume();
}
void Remote::downVolume()
{
    
    
	p->downVolume();

}
void Remote::upChannel()
{
    
    
	p->upChannel();
}
void Remote::downChannel()
{
    
    
	p->downChannel();
}
void Remote::showTv()
{
    
    
	p->showTv();
}
void Remote::setChannel(int channel)
{
    
    
	if(channel>=TV::minChan && channel<=TV::maxChan)
	{
    
    
		p->channel = channel;
	}
	else
	{
    
    
		cout<<"频道"<<channel<<"不在有效范围内"<<endl;
	}
}

设计动态数组类案例

在这里插入图片描述
视频网址

运算符重载

运算符重载基本概念

运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

语法: 定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里的@代表了被重载的运算符。
思路:

  • 1、弄懂运算符的运算对象的个数。(个数决定了 重载函数的参数个数)
  • 2、识别运算符左边的运算对象 是类的对象 还是其他.
    • 类的对象:全局函数实现(不推荐) 其他:只能是全局函数实现
    • 成员函数实现(推荐,少一个参数,因为成员函数被对象调用,有this指针,是隐式传递)
      • 例如:ob1+ob2 等于 ob1.operator+(ob2),此时operator是成员函数,默认传递参数this,所以可以少传递

重载<<运算符(全局函数实现)

#include <string>
using namespace std;
class Person
{
    
    
	friend ostream& operator<<(ostream &out, Person &ob);
private:
	int num;
	string name;
	float score;
public:
	Person(){
    
    }
	//初始化列表
	Person(int num,string name,float score):num(num),name(name),score(score){
    
    }
};
//全局函数重载operator<<
//完成链式操作
ostream& operator<<(ostream &out, Person &ob)
{
    
    
	out<<ob.num<<" "<<ob.name<<" "<<ob.score<<endl;
	return out;
}
int main(int argc, char *argv[])
{
    
    
	Person lucy(100,"lucy", 99.8f);
	Person bob(101,"bob", 99.8f);
	cout<<lucy<<bob<<endl;//operator<<(cout, lucy);
	return 0;
}

如果使用全局函数 重载运算符 必须将全局函数设置成友元。

重载>>运算符(全局函数实现)

#include <iostream>
#include <string>

using namespace std;
class Person
{
    
    
	friend ostream& operator<<(ostream &out, Person &ob);
	friend istream& operator>>(istream &in, Person &ob);


 
private:
	int num;
	string name;
	float score;
public:
	Person(){
    
    }
	Person(int num,string name,float score):num(num),name(name),score(score){
    
    }
};
//全局函数重载operator<<
ostream& operator<<(ostream &out, Person &ob)
{
    
    
	out<<ob.num<<" "<<ob.name<<" "<<ob.score<<endl;
	return out;
}
//全局函数重载operator>>
istream& operator>>(istream &in, Person &ob)
{
    
    
	in>>ob.num>>ob.name>>ob.score;
	return in;
}
int main(int argc, char *argv[])
{
    
    
	Person lucy;
	Person bob;
	cin>>lucy>>bob;
	cout<<lucy<<bob<<endl;
	return 0;
}

可以重载的运算符

在这里插入图片描述

重载+运算符(全局函数实现)

#include <iostream>
#include <string>

using namespace std;
class Person
{
    
    
	friend ostream& operator<<(ostream &out, Person ob);
	friend istream& operator>>(istream &in, Person &ob);
	friend Person operator+(Person &ob1, Person &ob2);
private:
	int num;
	string name;
	float score;
public:
	Person(){
    
    }
	Person(int num,string name,float score):num(num),name(name),score(score){
    
    }
};
//全局函数重载operator<<,参数二不采用引用
ostream& operator<<(ostream &out, Person ob)
{
    
    
	out<<ob.num<<" "<<ob.name<<" "<<ob.score<<endl;
	return out;
}
//全局函数重载operator>>
istream& operator>>(istream &in, Person &ob)
{
    
    
	in>>ob.num>>ob.name>>ob.score;
	return in;
}
//全局函数重载+运算符
Person operator+(Person &ob1, Person &ob2)
{
    
    
	Person tmp;
	tmp.num = ob1.num+ob2.num;
	tmp.name = ob1.name+ob2.name;//string类型可以直接拼接
	tmp.score = ob1.score+ob2.score;
	return tmp;
}
int main(int argc, char *argv[])
{
    
    
	Person lucy(100,"lucy", 88.8);
	Person bob(101,"bob",99.9);
	cout<<lucy+bob<<endl;
	return 0;
}

重载+运算符(成员函数实现 推荐)

#include <iostream>
#include <string>

using namespace std;
class Person
{
    
    
	friend ostream& operator<<(ostream &out, Person ob);
	friend istream& operator>>(istream &in, Person &ob);
private:
	int num;
	string name;
	float score;
public:
	Person(){
    
    }
	Person(int num,string name,float score):num(num),name(name),score(score){
    
    }
	//成员函数重载+运算符,不用设置右元
	Person operator+(Person &ob)
	{
    
    
	//默认传递参数this
		Person tmp;
		tmp.num = num+ob.num;
		tmp.name = name+ob.name;
		tmp.score = score+ob.score;
		return tmp;
	}
};
//全局函数重载operator<<
ostream& operator<<(ostream &out, Person ob)
{
    
    
	out<<ob.num<<" "<<ob.name<<" "<<ob.score<<endl;
	return out;
}
//全局函数重载operator>>
istream& operator>>(istream &in, Person &ob)
{
    
    
	in>>ob.num>>ob.name>>ob.score;
	return in;
}
int main(int argc, char *argv[])
{
    
    
	Person lucy(100,"lucy", 88.8);
	Person bob(101,"bob",99.9);
	
	cout<<lucy+bob<<endl;
	//lucy+bob ===> lucy.operator+(bob)
	//cout<<lucy.operator +(bob)<<endl;
	
	return 0;
}

重载==运算符符(成员函数实现 推荐)

#include <iostream>
#include <string>

using namespace std;
class Person
{
    
    
	friend ostream& operator<<(ostream &out, Person ob);
	friend istream& operator>>(istream &in, Person &ob);
private:
	int num;
	string name;
	float score;
public:
	Person(){
    
    }
	Person(int num,string name,float score):num(num),name(name),score(score){
    
    }
	//成员函数重载+运算符
	Person operator+(Person &ob)
	{
    
    
		Person tmp;
		tmp.num = num+ob.num;
		tmp.name = name+ob.name;
		tmp.score = score+ob.score;
		return tmp;
	}
	//成员函数重载==运算符
	bool operator==(Person &ob)
	{
    
    
		if(num==ob.num && name==ob.name && score==ob.score)return true;

		return false;
	}
};
//全局函数重载operator<<
ostream& operator<<(ostream &out, Person ob)
{
    
    
	out<<ob.num<<" "<<ob.name<<" "<<ob.score<<endl;
	return out;
}
//全局函数重载operator>>
istream& operator>>(istream &in, Person &ob)
{
    
    
	in>>ob.num>>ob.name>>ob.score;
	return in;
}
int main(int argc, char *argv[])
{
    
    
	Person lucy(100,"lucy", 88.8);
	Person bob(100,"lucy",88.8);
	if(lucy == bob)
	{
    
    
		cout<<"相等"<<endl; 
	}
	else
	{
    
    
		cout<<"不相等"<<endl;
	}
	return 0;
}

重载++/–运算符

重载的++和–运算符有点让人不知所措,因为我们总是希望能根据它们出现在所作用对象的前面还是后面来调用不同的函数。解决办法很简单,例如当编译器看到++a(前置++),它就调用operator++(a),当编译器看到a++(后置++),它就会去调用operator++(a,int).//利用缺省参数

++a(前置++),它就调用operator++(a),
a++(后置++),它就会去调用operator++(a,int)

–a(前置–),它就调用operator–(a),
a–(后置–),它就会去调用operator–(a,int)

案例1:重载后置++

类名称 operator++(int)
{
    
    
	//先保存 旧的值old
	//自增++
	
	return old;//返回旧值
}
#include <iostream>
include <string>
#using namespace std;
class Person
{
    
    
	friend ostream& operator<<(ostream &out, Person ob);
	friend istream& operator>>(istream &in, Person &ob);
private:
	int num;
	string name;
	float score;
public:
	Person(){
    
    }
	Person(int num,string name,float score):num(num),name(name),score(score){
    
    }
	//成员函数重载+运算符
	Person operator+(Person &ob)
	{
    
    
		Person tmp;
		tmp.num = num+ob.num;
		tmp.name = name+ob.name;
		tmp.score = score+ob.score;
		return tmp;
	}
	//成员函数重载==运算符
	
	
	 
	bool operator==(Person &ob)
	{
    
    
		if(num==ob.num && name==ob.name && score==ob.score)
		return true;
		return false;
	}
	//重载后置++,默认传递参数this,谁调用this指向谁,存储的是地址值,要解引用得到对象
	Person operator++(int)
	{
    
    
		
		//保存旧值
		Person old = *this;
		//++
		
		this->num++;
		this->name= this->name+this->name;
		this->score++;
		//返回值旧值,由于是局部对象,不能返回引用
		return old;
	}
};
//全局函数重载operator<<
ostream& operator<<(ostream &out, Person ob)
{
    
    
	out<<ob.num<<" "<<ob.name<<" "<<ob.score<<endl;
	return out;
}
//全局函数重载operator>>
istream& operator>>(istream &in, Person &ob)
{
    
    
	in>>ob.num>>ob.name>>ob.score;
	return in;
}
int main(int argc, char *argv[])
{
    
    
	Person lucy(100,"lucy", 88.8);
	Person bob;
	bob = lucy++;
	cout<<bob<<endl;
	cout<<lucy<<endl;
	return 0;
}

案例2:重载前置++

类名称 operator++()
{
    
    
	//自增++
	return *this;
}
#include <iostream>
#include <string>

using namespace std;
class Person
{
    
    
	friend ostream& operator<<(ostream &out, Person ob);
	friend istream& operator>>(istream &in, Person &ob);
private:
	int num;
	string name;
	float score;
public:
	Person(){
    
    }
	Person(int num,string name,float score):num(num),name(name),score(score){
    
    }
	//成员函数重载+运算符
	Person operator+(Person &ob)
	{
    
    
		Person tmp;
		tmp.num = num+ob.num;
		tmp.name = name+ob.name;
		tmp.score = score+ob.score;
		return tmp;
	}
	//成员函数重载==运算符
	bool operator==(Person &ob)
	{
    
    
		if(num==ob.num && name==ob.name && score==ob.score)
		return true;
		return false;
	}
	//重载后置++
	Person operator++(int)
	{
    
    
		//保存旧值
		Person old = *this;
		//++
		
		this->num++;
		this->name= this->name+this->name;
		this->score++;
		//返回值旧值
		return old;
	}
	//重载前置++
	Person operator++()
	{
    
    
		//++
		this->num++;
		this->name= this->name+this->name;
		this->score++;
		return *this;
	}
};


 
//全局函数重载operator<<
ostream& operator<<(ostream &out, Person ob)
{
    
    
	out<<ob.num<<" "<<ob.name<<" "<<ob.score<<endl;
	return out;
}
//全局函数重载operator>>
istream& operator>>(istream &in, Person &ob)
{
    
    
	in>>ob.num>>ob.name>>ob.score;
	return in;
}
int main(int argc, char *argv[])
{
    
    
	Person lucy(100,"lucy", 88.8);
	Person bob;
	bob = ++lucy;
	cout<<bob<<endl;
	cout<<lucy<<endl;
	return 0;
}

设计MyString案例

#include <iostream>
#include <string.h>
#include <string>
using namespace std;
class MyString
{
    
    
	friend ostream& operator<<(ostream &out, MyString ob);
	friend istream& operator>>(istream &in, MyString &ob);
private:
	char *str;
	int size;
public:
MyString();
MyString(char *str);
MyString(const MyString &ob);
~MyString();
int getSize();
//成员函数重载[]运算符
char& operator[](int pos);
//重载+运算符
MyString operator+(MyString &ob);
MyString operator+(char *str);
//重载赋值运算符(只有指针成员operator=必须深拷贝)


 
MyString& operator=(MyString ob);
MyString& operator=(char *str);
//重载关系运算符>
bool operator>(MyString ob);
bool operator>(char *str);
};
//全局函数重载<<运算符
ostream& operator<<(ostream &out, MyString ob)
{
    
    
out<<ob.str;
return out;
}
istream& operator>>(istream &in, MyString &ob)
{
    
    
char buf[128]="";
cin>>buf;
//判断ob.str是否为NULL
if(ob.str != NULL)
{
    
    
delete [] ob.str;
ob.str=NULL;
}
ob.size = strlen(buf);
ob.str = new char[ob.size+1];
memset(ob.str, 0, ob.size+1);
strcpy(ob.str, buf);
return in;
}
int main(int argc, char *argv[])
{
    
    
MyString str1="hello";
str1[1]='H';
cout<<str1[1]<<endl;
cout<<str1<<endl;
MyString str2;
cin>>str2;
cout<<str2<<endl;
MyString str3 = "hello";
MyString str4 = "world";
cout<<str3+str4<<endl;
cout<<str3+"world"<<endl;
MyString str5="hello world";
MyString str6="world";
str6 = str5;
cout<<str6<<endl;
str6="hello str6";
cout<<str6<<endl;


 
if(str5 > str6)
{
    
    
cout<<"大于"<<endl;
}
else
{
    
    
cout<<"不大于"<<endl;
}
return 0;
}
MyString::MyString()
{
    
    
str=NULL;
size=0;
}
MyString::MyString(char *str)
{
    
    
this->str = new char[strlen(str)+1];
memset(this->str,0,strlen(str)+1);
size = strlen(str);
strcpy(this->str,str);
}
MyString::MyString(const MyString &ob)
{
    
    
size = ob.size;
str = new char[size+1];
memset(str,0,size+1);
strcpy(str,ob.str);
}
MyString::~MyString()
{
    
    
if(str != NULL)
{
    
    
delete [] str;
str=NULL;
}
}
int MyString::getSize()
{
    
    
return size;
}
char& MyString::operator[](int pos)
{
    
    
if(pos<0 || pos>=size)
{
    
    
cout<<"下标越界"<<endl;
exit(-1);
}
return str[pos];


 
}
MyString MyString::operator+(MyString &ob)
{
    
    
MyString tmp;
tmp.size = this->size+ob.size;
tmp.str = new char[tmp.size +1];
memset(tmp.str, 0, tmp.size+1);
strcpy(tmp.str, this->str);
strcat(tmp.str, ob.str);
return tmp;
}
MyString MyString::operator+(char *str)
{
    
    
MyString tmp;
tmp.size = this->size+strlen(str);
tmp.str = new char[tmp.size +1];
memset(tmp.str, 0, tmp.size+1);
strcpy(tmp.str, this->str);
strcat(tmp.str, str);
return tmp;
}
MyString &MyString::operator=(MyString ob)
{
    
    
//str5=str4
if(str != NULL)
{
    
    
delete [] str;
str=NULL;
}
size = ob.size;
str = new char[size+1];
memset(str, 0,size+1);
strcpy(str, ob.str);
return *this;
}
MyString &MyString::operator=(char *str)
{
    
    
//str5=str4
if(this->str != NULL)
{
    
    
delete [] this->str;
this->str=NULL;
}
size = strlen(str);
this->str = new char[size+1];
memset(this->str, 0,size+1);
strcpy(this->str, str);


 
return *this;
}
bool MyString::operator>(MyString ob)
{
    
    
if(str==NULL || ob.str == NULL)
{
    
    
exit(-1);
}
if(strcmp(str, ob.str) > 0)
return true;
return false;
}
bool MyString::operator>(char *str)
{
    
    
if(this->str==NULL || str == NULL)
{
    
    
exit(-1);
}
if(strcmp(this->str, str) > 0)
return true;
return false;
}

重载函数调用运算符

重载()运算符 一般用于 为算法 提供策略。
当对象和()结合 会触发 重载函数调用运算符

#include <iostream>
using namespace std;
//仿函数

class Print
{
    
    
public:
	//重载函数调用运算符
	void operator()(char *str)
	{
    
    
		cout<<str<<endl;
	}
};
int main(int argc, char *argv[])
{
    
    
	Print ob;
	//对象和()结合触发operator()调用
	ob("hello Print");
	//Print()为匿名对象,类名称是匿名对象也可以与小括号结合
	Print()("hello print");
	return 0;
}

智能指针(指针运算符(*、->)重载)

智能指针:解决 堆区空间的对象 释放问题忘记析构

创建智能指针对类进行包裹

#include <iostream>
using namespace std;
class Data
{
    
    
public:
	Data()
	{
    
    
		cout<<"Data的无参构造"<<endl;
	}
	~Data()
	{
    
    
		cout<<"Data的析够"<<endl;
	}
	void func()
	{
    
    
		cout<<"Data的func函数"<<endl;
	}
};
//智能指针
class SmartPointer
{
    
    
private:
	Data *p;
public:
	SmartPointer(){
    
    }
	SmartPointer(Data *p)
	{
    
    
		this->p = p;
	}
	//
	~SmartPointer()
	{
    
    
		delete p;
	}
	// 重载->运算符
	Data* operator->()
	{
    
    
		return p;
	}
	Data& operator*()
	{
    
    
		return *p;
	}
};
int main(int argc, char *argv[])
{
    
    
//通过智能指针创建对象
	SmartPointer sp(new Data);
	//sp.operator ->()->func();//局部对象sp调用结束后会析构
	sp->func();
	
	//Date *p = new Date;
	//p->func();
	//delete p;

	 
	(*sp).func();//不取*号就是地址,取信号就是对象,用不同的运算符重载
	return 0;
}

不要重载 && ||

不能重载operator&& 和 operator|| 的原因是,无法在这两种情况下实现内置操作符的完整语义。说得
更具体一些,内置版本版本特殊之处在于:内置版本的&&和||首先计算左边的表达式,如果这完全能够
决定结果,就无需计算右边的表达式了–而且能够保证不需要。我们都已经习惯这种方便的特性了。 我
们说操作符重载其实是另一种形式的函数调用而已,对于函数调用总是在函数执行之前对所有参
数进行求值。

class Complex{
    
    
public:
	Complex(int flag){
    
    
		this->flag = flag;
	}
	Complex& operator+=(Complex& complex){
    
    
		this->flag = this->flag + complex.flag;
		return *this;
	}
	bool operator&&(Complex& complex){
    
    
		return this->flag && complex.flag;
	}
public:
	int flag;
};
int main(){
    
    
	Complex complex1(0); //flag 0
	Complex complex2(1); //flag 1
	//原来情况,应该从左往右运算,左边为假,则退出运算,结果为假
	//这边却是,先运算(complex1+complex2),导致,complex1的flag变为complex1+complex2
	的值, complex1.a = 1
	// 1 && 1
	//complex1.operator&&(complex1.operator+=(complex2))
	if (complex1 && (complex1 += complex2)){
    
    
		//complex1.operator+=(complex2)
		cout << "真!" << endl;
		
	}
	else{
    
    
		cout << "假!" << endl;
		return 0;
}

第十四章 继承

继承和派生

为什么需要继承

提高代码重用,提高开发效率。

继承的基本概念

继承可以理解为一个类从另一个类获取成员变量和成员函数的过程。

c++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不
仅拥有旧类的成员,还拥有新定义的成员。 一个B类继承于A类,或称从类A派生类B。这样的话,类A成
为基类(父类), 类B成为派生类(子类)。 派生类中的成员,包含两大部分: 一类是从基类继承过来
的,一类是自己增加的成员。 从基类继承过过来的表现其共性,而新增的成员体现了其个性。
在这里插入图片描述

派生类的定义

class 父类{
    
    };
class 子类:继承方式 父类名
{
    
    
//新增子类数据
};

继承方式:private protected public(推荐)

在这里插入图片描述

所有父类私有在子类中不可访问,公共继承 保持不变,保护继承变保护,私有继承变私有。

class Base
{
    
    
private:
	int a;
protected:
	int b;
public:
	int c;
};
class Son:public Base
{
    
    
	//子类中 能访问 protected b public c
	public:
	void func(void)
	{
    
    
	
		cout<<b<<c<<endl;
		//cout<<a<<endl;//不可访问
	}
};
int main()
{
    
    
	Son ob;
	//cout<<ob.b<<endl;//类外无法访问保护的数据b
	cout<<ob.c<<endl;
}
  • 如果不考虑继承关系,protected成员和private成员一样,类外不能访问。但是,当存在继承关系时,protected和private就不一样了。基类中的protected成员可以在派生类中访问,而基类中的 private成员不能在派生类中访问。

  • 不管继承方式如何,基类中的private成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)

  • 由于private和protected继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以,在实际开发中,一般使用public。

使用 using 关键字可以改变基类成员在派生类中的访问权限。

注意:using只能改变基类中public和protected成员的访问权限,不能改变private成员的访问权限,因为基类中的private成员在派生类中是不可见的,根本不能使用。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

class A  {
    
            // 基类
public:
    int m_a=10;
protected:
    int m_b=20;
private:
    int m_c = 30;
};

class B :public A        // 派生类
{
    
    
public:
    using A::m_b;         // 把m_b的权限修改为公有的。
private:
    using A::m_a;         // 把m_a的权限修改为私有的。
};

int main()
{
    
    
    B b;   
    // b.m_a = 11;
    b.m_b = 21;
    //b.m_c = 21;
}

继承中的构造和析构

子类的构造析构顺序

父成子,子成父
在这里插入图片描述

class Base
{
    
    
public:
	Base()
	{
    
    
		cout<<"父类构造"<<endl;
	}
	~Base()
	{
    
    
		cout<<"父类析够"<<endl;
	}
};
class Other
{
    
    
public:
	Other()
	{
    
    
		cout<<"Other构造"<<endl;
	}
	
	
	~Other()
	{
    
    
		cout<<"Other析够"<<endl;
	}
};
class Son:public Base
{
    
    
public:
	Other ob;
public:
public:
	Son()
	{
    
    
	cout<<"Son构造"<<endl;
	}


	~Son()
	{
    
    
	cout<<"Son析够"<<endl;
	}
};
int main()
{
    
    
	Son ob;
	return 0;
}

在这里插入图片描述
注意:派生类的构造函数总是调用一个基类构造函数,包括拷贝构造函数。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。
               
class A {
    
            // 基类
public:
    int m_a;
private:
    int m_b;
public:
    A() : m_a(0) , m_b(0)                     // 基类的默认构造函数。
    {
    
     
        cout << "调用了基类的默认构造函数A()。\n";  
    }
    A(int a,int b) : m_a(a) , m_b(b)     // 基类有两个参数的构造函数。
    {
    
     
        cout << "调用了基类的构造函数A(int a,int b)。\n";  
    }
    A(const A &a) : m_a(a.m_a+1) , m_b(a.m_b+1)   // 基类的拷贝构造函数。
    {
    
    
        cout << "调用了基类的拷贝构造函数A(const A &a)。\n";
    }
               
    // 显示基类A全部的成员。
    void showA() {
    
     cout << "m_a=" << m_a << ",m_b=" << m_b << endl; }
};
               
class B :public A        // 派生类
{
    
            
public:
    int m_c;
    B() : m_c(0) , A()             // 派生类的默认构造函数,指明用基类的默认构造函数(不指明也无所谓)。
    {
    
    
        cout << "调用了派生类的默认构造函数B()。\n";
    }
    B(int a, int b, int c) : A(a, b), m_c(c)           // 指明用基类的有两个参数的构造函数。
    {
    
    
        cout << "调用了派生类的构造函数B(int a,int b,int c)。\n";
    }
    B(const A& a, int c) :A(a), m_c(c)              // 指明用基类的拷贝构造函数。
    {
    
    
        cout << "调用了派生类的构造函数B(const A &a,int c) 。\n";
    }
           
    // 显示派生类B全部的成员。
    void showB() {
    
     cout << "m_c=" << m_c << endl << endl; }
};          
             
int main()
{
    
    
    B b1;                 // 将调用基类默认的构造函数。
    b1.showA();     b1.showB();
       
    B b2(1, 2, 3);      // 将调用基类有两个参数的构造函数。
    b2.showA();     b2.showB();
            
    A a(10, 20);      // 创建基类对象。
    B b3(a, 30);      // 将调用基类的拷贝造函数。
    b3.showA();     b3.showB();
}     

子类调用成员对象、父类的有参构造

子类实例化对象时 会自动调用 成员对象、父类的默认构造

子类实例对象时 必须使用初始化列表 调用成员对象、父类的有参构造。

初始化列表时:父类写类名称 成员对象用对象名

#include <iostream>
using namespace std;
class Base
{
    
    
public:
	int a;
public:
	Base()
	{
    
    
		cout<<"父类默认构造"<<endl;
	}
	Base(int a)
	{
    
    
		this->a = a;
		cout<<"父类有参构造"<<endl;
	}
	
	~Base()
	{
    
    
		cout<<"父类析够"<<endl;
	}


 
};
class Other
{
    
    
public:
	int b;
public:
	Other()
	{
    
    
		cout<<"Other默认构造"<<endl;
	}
	Other(int b)
	{
    
    
		this->b = b;
		cout<<"Other有参构造"<<endl;
	}
	
	{
    
    
	~Other()
		cout<<"Other析够"<<endl;
	}
};
class Son:public Base
{
    
    
public:
	Other ob;
	int c;
public:
	Son()
	{
    
    
		cout<<"Son构造"<<endl;
	}
	//父类写类名称 成员对象用对象名
	Son(int a, int b, int c):Base(a), ob(b)
	{
    
    
		this->c = c;
		cout<<"Son有参构造"<<endl;
	}
	~Son()
	{
    
    
		cout<<"Son析够"<<endl;
	}
};
int main(int argc, char *argv[])
{
    
    
	Son ob(10,20,30);
	return 0;
}

在这里插入图片描述

子类和父类的同名处理

同名成员 最简单 最安全的处理方式:加作用域

类是一种作用域,每个类都有它自己的作用域,在这个作用域之内定义成员。

子类和父类 同名 成员数据(变量)

子类默认优先访问 子类的同名成员
必须加父类作用域 访问父类的同名成员

class Base
{
    
    
public:
	int a;
public:
	Base(int a)
	{
    
    
		this->a = a;
	}
};
class Son:public Base
{
    
    
public:
	int a;
	Son(int x, int y):Base(x)
	{
    
    
		a = y;
	}
};
int main()
{
    
    
	Son ob(10,20);
	//子类默认优先访问 子类的同名成员
	cout<<ob.a<<endl;//20
	
	//必须加父类作用域 访问父类的同名成员
	cout<<ob.Base::a<<endl;//10
	return 0;
}

子类和父类 同名 成员函数

class Base
{
    
    
public:
	void fun01()
	{
    
    
		cout<<"Base 无参的fun01"<<endl;
	}
};
class Son:public Base
{
    
    
public:
	void fun01()
	{
    
    
		cout<<"Son 无参的fun01"<<endl;
	}
};
int main()
{
    
    
	Son ob;
	//子类默认优先访问 子类的同名成员
	ob.fun01();
	//必须加父类作用域 访问父类的同名成员
	ob.Base::fun01();
	return 0;
}

在这里插入图片描述

子类 重定义 父类的 同名函数

重载:无继承,同一作用域,参数的个数不同、顺序不同、类型不同 都可重载

重定义:有继承, 子类 重定义 父类的同名函数(参数可以不同)(非虚函数)子类一旦 重定义了父类 的同名函数(不管参数是否一致),子类中都将屏蔽父类所有的同名函数。

class Base
{
    
    
public:
	void fun01()
	{
    
    
		cout<<"Base 无参的fun01"<<endl;
	}
	void fun01(int a)
	{
    
    
		cout<<"Base 的fun01 int"<<endl;
	}
	void fun01(int a, int b)
	{
    
    
		cout<<"Base 的fun01 int int"<<endl;
	}
};
class Son:public Base
{
    
    
public:
	void fun01(string a)
	{
    
    
		cout<<"Son 的fun01 char"<<endl;
	}
};
int main()
{
    
    
	Son ob;
	ob.fun01("hello");
	ob.fun01();
	ob.fun01(10);
	ob.fun01(10,20);
	return 0;
}

在这里插入图片描述

需要加父类的作用域 才能识别 屏蔽的函数

在这里插入图片描述

子类不能继承父类的成员

不是所有的函数都能自动从基类继承到派生类中。
构造函数和析构函数用来处理对象的创建和析构操
作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继
承,必须为每一个特定的派生类分别创建。
另外operator=也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。 在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。

多继承

不提倡使用多继承,只有在比较简单和不出现二义性的情况时才使用多继承,能用单一继承解决的问题就不要使用多继承。

多继承的概念

我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争
议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。

多继承的格式

class 父类1{
    
    };
class 父类2{
    
    };
class 子类:继承方式1 父类1, 继承方式2 父类2
{
    
    
//新增子类数据
};

子类就拥有了父类1,父类2的大部分数据.

class Base1
{
    
    
public:
	int a;
};
class Base2
{
    
    
public:
	int b;
};
class Son:public Base1, public Base2
{
    
    
public:

};
int main()
{
    
    
	Son ob;//局部对象没有初始化,参数是随机的
	cout<<ob.a<<" "<<ob.b<<endl;
	return 0;
}

在这里插入图片描述

多继承中的同名成员处理

如果多继承中 遇到同名成员 需要加父类作用域解决。

class Base1
{
    
    
public:
	int a;
	Base1(int a):a(a){
    
    }
};
class Base2
{
    
    
public:
	int a;
	Base2(int a):a(a){
    
    }
};
class Son:public Base1, public Base2
{
    
    
public:
	int a;
	Son(int a, int b,int c):Base1(a),Base2(b),a(c){
    
    }
};
int main()
{
    
    
	Son ob(10,20,30);
	cout<<ob.a<<endl;//子类a
	cout<<ob.Base1::a<<endl;//Base1 a
	cout<<ob.Base2::a<<endl;//Base2 a
	return 0;
}

菱形继承

有了多继承,就存在菱形继承

菱形继承:有公共祖先的继承 叫菱形继承。
最底层的子类 数据 会包含多份(公共祖先的数据)
在这里插入图片描述

class Animal
{
    
    
public:
	int data;
};
class Sheep :public Animal{
    
    };
class Tuo :public Animal {
    
    };
class SheepTuo:public Sheep,public Tuo{
    
    };
int main()
{
    
    
	SheepTuo ob;
	memset(&ob, 0, sizeof(SheepTuo));
	//cout << ob.data << endl;//二义性
	cout << ob.Sheep::data << endl;
	cout << ob.Tuo::data << endl;
}

怎么才能只要公共祖先的一份数据呢?

虚继承

虚继承 解决 菱形继承中 多份公共祖先数据的问题。

虚继承可以解决菱形继承的二义性和数据冗余的问题。

虚继承的方式

在继承方式 前加virtual修饰,子类虚继承父类 子类只会保存一份公共数据。

#include<iostream>
#include<string.h>

using namespace std;
class Animal
{
    
    
public:
	int data;
};
class Sheep :virtual public Animal{
    
    
};
class Tuo :virtual public Animal {
    
    
};
class SheepTuo:public Sheep,public Tuo{
    
    };
int main()
{
    
    
	SheepTuo ob;
	cout << ob.data << endl;
	return 0;
}

在这里插入图片描述

虚继承的实现原理(VS有效)

1、打开命令行开发模式
在这里插入图片描述

2、找到类所在的源文件路径
在这里插入图片描述

3、在命令行中 导出类的布局

cl /d1 reportSingleClassLayoutAnimal main.cpp

Animal布局:
在这里插入图片描述

Sheep布局:
在这里插入图片描述

  • 虚基类指针指向一个虚基类表,表里记录了访问data的偏移量

Tuo布局:

在这里插入图片描述

SheepTuo布局:
在这里插入图片描述

在这里插入图片描述

虚继承 会在子类中产生 虚基类指针(vbptr) 指向 虚基类表(vbtable), 虚基类表纪录的是通过该指针访
问公共祖先的数据的偏移量。
注意:

  • 虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的.工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替。

继承的特殊关系

派生类和基类之间有一些特殊关系。

2)可以把派生类对象赋值给基类对象(包括私有成员),但是,会舍弃非基类的成员。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

class A {
    
            // 基类
public:
    int m_a=0;
private:
    int m_b=0;
public:
    // 显示基类A全部的成员。
    void show() {
    
     cout << "A::show() m_a=" << m_a << ",m_b=" << m_b << endl; }
    // 设置成员m_b的值。
    void setb(int b) {
    
     m_b = b;  }
};

class B :public A        // 派生类
{
    
    
public:
    int m_c=0;
    // 显示派生类B全部的成员。
    void show() {
    
     cout << "B::show() m_a=" << m_a << "m_c=" << m_c << endl; }
};

int main()
{
    
    
    B b;
    A a;

    b.m_a = 10; 
    b.setb(20);          // 设置成员m_b的值。
    b.m_c = 30;
   
    a->show();         // 调用的是A类的show()函数。
    a = b;			   //派生类对象赋值给基类对象
	a->show();         // 调用的是A类的show()函数。
	//结果:m_a = 0,m_b = 0
	//m_a = 10,m_b = 20
}

3)基类指针可以在不进行显式转换的情况下指向派生类对象。
4)基类引用可以在不进行显式转换的情况下引用派生类对象。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

class A {
    
            // 基类
public:
    int m_a=0;
private:
    int m_b=0;
public:
    // 显示基类A全部的成员。
    void show() {
    
     cout << "A::show() m_a=" << m_a << ",m_b=" << m_b << endl; }
    // 设置成员m_b的值。
    void setb(int b) {
    
     m_b = b;  }
};

class B :public A        // 派生类
{
    
    
public:
    int m_c=0;
    // 显示派生类B全部的成员。
    void show() {
    
     cout << "B::show() m_a=" << m_a << "m_c=" << m_c << endl; }
};

int main()
{
    
    
    B b;
    A* a = &b;//基类指针指向派生类对象

    b.m_a = 10; 
    b.setb(20);          // 设置成员m_b的值。
    b.m_c = 30;
    b.show();            // 调用的是B类的show()函数。

    a->m_a = 11;
    a->setb(22);          // 设置成员m_b的值。基类指针或引用只能调用基类的方法,不能调用派生类的方法。
    // a->m_c = 30;
    
    a->show();         // 调用的是A类的show()函数。
}

注意:
1)基类指针或引用只能调用基类的方法,不能调用派生类的方法。
2)可以用派生类构造基类。
3)如果函数的形参是基类,实参可以用派生类。
4)C++要求指针和引用类型与赋给的类型匹配,这一规则对继承来说是例外。但是,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针(没有价值,没有讨论的必要)。

第十五章 多态

  • 多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。 多态性(polymorphism)提供
    接口与具体实现之间的另一层隔离,从而将”what”和”how”分离开来。多态性改善了代码的可读性和组
    织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新
    的功能时也能扩展。

  • 静态多态(编译时多态,早绑定):函数重载、运算符重载、重定义

  • 动态多态(运行时多态,晚绑定):虚函数

  • 基类指针只能调用基类的成员函数,不能调用派生类的成员函数。如果在基类的成员函数前加virtual 关键字,把它声明为虚函数,基类指针就可以调用派生类中同名的成员函数,通过派生类中同名的成员函数,就可以访问派生对象的成员变量。

  • 有了虚函数,基类指针指向基类对象时就使用基类的成员函数和数据,指向派生类对象时就使用派生类的成员函数和数据,基类指针表现出了多种形式,这种现象称为多态。

  • 基类引用也可以使用多态。

注意:

  • 1)只需要在基类的函数声明中加上virtual关键字,函数定义时不能加。
  • 2)在派生类中重定义虚函数时,函数特征要相同。
  • 3)当在基类中定义了虚函数时,如果派生类没有重定义该函数,那么将使用基类的虚函数。
  • 4)在派生类中重定义了虚函数的情况下,如果想使用基类的虚函数,可以加类名和域解析符。
  • 5)如果要在派生类中重新定义基类的函数,则将它设置为虚函数;否则,不要设置为虚函数,有两方面的好处:首先效率更高;其次,指出不要重新定义该函数。
#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。
            
class CAllComers {
    
            // 报名者类
public:
    int  m_bh = 0;             // 编号。
    virtual void show() {
    
     cout << "CAllComers::show():我是" << m_bh << "号。 " << endl; }
    virtual void show(int a) {
    
     cout << "CAllComers::show(int a):我是" << m_bh << "号。 " << endl; }
};
             
class CGirl :public CAllComers {
    
            // 超女类
public:
    int m_age = 0;           // 年龄。
    void show() {
    
     cout << "CGirl::show():我是" << m_bh << "号, " << m_age << "岁。" << endl; }
    void show(int a) {
    
     cout << "CGirl::show(int a):我是" << m_bh << "号, " << m_age << "岁。" << endl; }//函数重载
};
        
int main()
{
    
            
    CAllComers a;  a.m_bh = 3;                             // 创建基类对象并对成员赋值。
    CGirl g;             g.m_bh = 8; g.m_age = 23;    // 创建派生类对象并对成员赋值。
            
    CAllComers* p;            // 声明基类指针。
    //p = &a;   p->show();   // 让基类指针指向基类对象,并调用虚函数。
    p = &g;   p->show();   // 让基类指针指向派生类对象,并调用虚函数。
    p->show(5);     p->CAllComers::show(5);

CAllComers &ra = a;ra.show();            // 声明基类引用。
CAllComers &rg = g;rg.show();            // 声明基类引用。

}

虚函数

知识点引入

在这里插入图片描述

父类指针(引用)保存 子类空间地址的目的 就是让算法通用。

父类指针 保存 子类空间地址(带来的问题)

class Animal
{
    
    
public:
	void speak(void)
	{
    
    
	cout<<"动物在说话"<<endl;
	}
};
class Dog:public Animal
{
    
    
public:
//函数功能重写
	void speak(void)
	{
    
    
	cout<<"狗在汪汪"<<endl;
	}
};
int main()
{
    
    
	Animal *p = new Dog;
	p->speak();//动物在说话
}

其实用户的需求:p->speak 希望等到的是“狗在汪汪” 而不是“动物在说话”。原因在此:
在这里插入图片描述
指针变量能操作的空间大小由指针变量指向的类型决定,所以p只能操作父类的这一部分数据

虚函数的定义

成员函数前加virtual修饰

class Animal
{
    
    
public:
	//虚函数
	virtual void speak(void)
	{
    
    
		cout<<"动物在说话"<<endl;
	}
};
class Dog:public Animal
{
    
    
public:
	//子类重写 父类的虚函数:函数名、返回值类型、参数类型个数顺序 必须完全一致
	//虚函数可以写virtual也可以不写
	virtual void speak(void)
	{
    
    
		cout<<"狗在汪汪"<<endl;
	}
};
class Cat:public Animal
{
    
    
public:
	//子类重写 父类的虚函数
	void speak(void)
	{
    
    
		cout<<"猫在喵喵"<<endl;
	}
};
int main()
{
    
    
	Animal *p1 = new Dog;
	p1->speak();//"狗在汪汪"
	Animal *p2 = new Cat;
	p2->speak();//"猫在喵喵"
}

多态条件:有继承、子类重写父类的虚函数,父类指针 指向子类空间。

虚函数的动态绑定机制

Animal的类的结构:

在这里插入图片描述

如果一个类中的成员函数 被virtual修饰,那么这个函数就是虚函数。类就会产生一个虚函数指针(vfptr)指向了一张虚函数表(vftable)。如果这个(父)类 没有涉及到继承, 这时虚函数表中 纪录就是当前虚函数入口地址。

Dog的类存结构:

涉及到继承时,子类会把父类中的虚函数指针和虚函数表继承,并修改虚函数表中重写的虚函数入口地址
在这里插入图片描述
在这里插入图片描述
本质上还是调用父类的虚函数,只是该虚函数指针指向的地址修改了

#include <iostream>
using namespace std;
class Animal
{
    
    
public:
	//虚函数
	virtual void speak(void)
	{
    
    
	cout<<"动物在说话"<<endl;
	}
};
class Dog:public Animal
{
    
    
public:
#if 1
	//子类重写 父类的虚函数
	void speak(void)
	{
    
    
	cout<<"狗在汪汪"<<endl;
	}
#endif


};
class Cat:public Animal
{
    
    
public:
	//子类重写 父类的虚函数
	void speak(void)
	{
    
    
		cout<<"猫在喵喵"<<endl;
	}
};
void AnimalSpeak(Animal *p)
{
    
    
	p->speak();
	return;
}
int main(int argc, char *argv[])
{
    
    
	AnimalSpeak(new Dog);//狗在汪汪
	AnimalSpeak(new Cat);//猫在喵喵
	return 0;
}

重载、重定义、重写的区别

重载:同一作用域,同名函数,参数的顺序、个数、类型不同 都可以重载。函数的返回值类型不能作为重载条件(函数重载、运算符重载)
重定义:有继承,子类 重定义 父类的同名函数(非虚函数)参数顺序、个数、类型可以不同。子类的同名函数会屏蔽父类的所有同名函数(可以通过作用域解决)
重写(覆盖):有继承,子类 重写 父类的虚函数返回值类型、函数名、参数顺序、个数、类型都必须一致

多态的对象模型

  • 类的普通成员函数的地址是静态的,在编译阶段已指定。
  • 如果基类中有虚函数,对象的内存模型中有一个虚函数表,表中存放了基类的函数名和地址。
  • 如果派生类中重定义了基类的虚函数,创建派生类对象时,将用派生类的函数取代虚函数表中基类的函数。

C++中的多态分为两种:静态多态与动态多态。

  • 静态多态:也成为编译时的多态;在编译时期就已经确定要执行了的函数地址了;主要有函数重载和函数模板。
  • 动态多态:即动态绑定,在运行时才去确定对象类型和正确选择需要调用的函数,一般用于解决基类指针或引用派生类对象调用类中重写的方法(函数)时出现的问题。

纯虚函数

通过上面的讲解发现,如果基类一定派生出子类,而子类一定会重写父类的虚函数,那么父类的虚函数
中的函数体感觉是无意义,可不可以不写父类虚函数的函数体呢?可以的,那就必须了解纯虚函数。

目的:提供接口

纯虚函数的定义方式

class Animal
{
    
    
public:
	//纯虚函数,不写函数体但又要和函数声明区别开来
	virtual void speak(void)=0;
};

一旦类中有纯虚函数,那么这个类 就是抽象类。
抽象类 不能实例化 对象。(Animal ob;错误)
抽象类 必须被继承 同时 子类 必须重写 父类的所有纯虚函数,否则 子类也是抽象类。

#include <iostream>
using namespace std;
//有 纯虚函数的类 为抽象类

class Animal
{
    
    
public:
	//纯虚函数
	virtual void speak(void)=0;
};
class Dog:public Animal
{
    
    
public:
	//子类一定要重写 父类的所有纯虚函数
	void speak(void)
	{
    
    
		cout<<"狗在汪汪"<<endl;
	}
};
int main(int argc, char *argv[])
{
    
    
	//Animal ob;//err 抽象类不能实例化对象
	Animal *p = new Dog;
	p->speak();//"狗在汪汪"
	return 0;
}

抽象类主要的目的 是设计 类的接口:

虚函数案例-饮品制作

在这里插入图片描述

#include <iostream>
using namespace std;
//抽象制作饮品

class AbstractDrinking{
    
    
public:
	//烧水
	virtual void Boil() = 0;
	//冲泡
	virtual void Brew() = 0;
	//倒入杯中
	virtual void PourInCup() = 0;
	//加入辅料
	virtual void PutSomething() = 0;
	//规定流程


	void MakeDrink(){
    
    
		this->Boil();
		Brew();
		PourInCup();
		PutSomething();
	}
};
//制作咖啡
class Coffee : public AbstractDrinking{
    
    
public:
	//烧水
	virtual void Boil(){
    
    
		cout << "煮农夫山泉!" << endl;
	}
	//冲泡
	virtual void Brew(){
    
    
		cout << "冲泡咖啡!" << endl;
	}
	//倒入杯中
	virtual void PourInCup(){
    
    
		cout << "将咖啡倒入杯中!" << endl;
	}
	//加入辅料
	virtual void PutSomething(){
    
    
		cout << "加入牛奶!" << endl;
	}
};
//制作茶水
class Tea : public AbstractDrinking{
    
    
public:
	//烧水
	virtual void Boil(){
    
    
		cout << "煮自来水!" << endl;
	}
	//冲泡
	virtual void Brew(){
    
    
		cout << "冲泡茶叶!" << endl;
	}
	//倒入杯中
	virtual void PourInCup(){
    
    
		cout << "将茶水倒入杯中!" << endl;
	}
	//加入辅料
	virtual void PutSomething(){
    
    
		cout << "加入食盐!" << endl;
	}
};
//业务函数
void DoBussiness(AbstractDrinking* drink){
    
    
	drink->MakeDrink();
	delete drink;//释放空间
}
int main(int argc, char *argv[])
{
    
    
	DoBussiness(new Coffee);
	cout << "--------------" << endl;
	DoBussiness(new Tea);
	return 0;
}

在这里插入图片描述

虚函数和纯虚函数的区别

虚函数:virtual修饰 有函数体 不会导致父类为抽象类。
纯虚函数:virtual修饰,=0,没有函数体 导致父类为抽象类。子类必须重写父类的所有纯虚函数。

虚析构函数

虚析构函数的定义

virtual修饰析构函数
虚析构:通过父类指针 释放整个子类空间。

class Animal
{
    
    
public:
	//虚函数
	virtual void speak(void)
	{
    
    
		cout << "动物在说话" << endl;
	}
	//虚析构
	virtual ~Animal()
	{
    
    
		cout<<"Animal的析构函数"<<endl;
	}
};
class Dog :public Animal
{
    
    
public:
	//子类重写 父类的虚函数
	void speak(void)
	{
    
     
	cout << "狗在汪汪" << endl;
	}
	
	{
    
    
	~Dog()
	cout<<"Dog的析构函数"<<endl;
	}
};
int main()
{
    
    
	Animal* p1 = new Dog;
	p1->speak();
	//p指向的空间是父类,没有虚析构只能释放父类的空间
	//虚析构,通过父类指针释放子类所有空间
	delete p1;
}

在这里插入图片描述

构造的顺序:父类—>成员---->子类
析构的顺序:子类—>成员---->父类
在这里插入图片描述
不需要重写,虽然析构函数也属于成员函数

纯虚析构函数

纯虚析构函数的定义

纯虚析构的本质:是析构函数,各个类的回收工作。而且析构函数不能被继承。
必须为纯虚析构函数提供一个函数体(因为虚构函数的存在意义是为了完成清理类的工作)。
纯虚析构函数 必须在类外实现.

#include <iostream>
using namespace st  ;
class Animal
{
    
    
public:
	//纯虚函数
	virtual void speak(void)=0;
	//纯虚析构函数 必须在类外实现
	virtual ~Animal()=0;
};
class Dog :public Animal
{
    
    
public:
	//子类重写 父类的虚函数
	void speak(void)
	{
    
    
		cout << "狗在汪汪" << endl;
	}
	
	
	~Dog()
	{
    
    
		cout<<"Dog的析构函数"<<endl;
	}
};
Animal::~Animal()
{
    
    
	cout<<"Animal的析构函数"<<endl;
}
int main(int argc, char* argv[])
{
    
    
	Animal* p1 = new Dog;
	p1->speak();
	delete p1;
	return 0;
}

在这里插入图片描述

虚析构和纯虚析构的区别?

虚析构:virtual修饰,有函数体,不会导致父类为抽象类。
纯虚析构:virtual修饰,=0,函数体必须类外实现,导致父类为抽象类。

第十六章 模板

模板的概述

c++提供了函数模板(function template.)所谓函数模板,实际上是建立一个通用函数,其函数类型和形 参类型不具体制定,用一个虚拟的类型来代表。这个通用函数就成为函数模板。凡是函数体相同的函数都可以用这个模板代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能。 c++提供两种模板机制:函数模板和类模板 类属 - 类型参数化,又称参数模板

总结:

  • c++面向对象编程思想:封装、继承、多态
  • c++泛型编程思想:模板
  • 模板的分类:函数模板、类模板
  • 将功能相同,类型不同的函数(类)的类型抽象成虚拟的类型。当调用函数(类实例化对象)的时
    候,编译器自动将虚拟的类型 具体化。这个就是函数模板(类模板)。

其实就是一个接口可以操作多种数据类型【即不关心数据类型】

函数模板

函数模板的定义方式

模板关键字template

//T只能对当前函数有效,可以分行也可以不分行
template<typename T>
void swapAll(T &a, T &b)
{
    
    
	T tmp = a;
	a = b;
	b=tmp;
	return;
}
int main()
{
    
    
	int a = 10, b = 20;
	//函数调用时 根据实参的类型 会自动推导T的类型
	swapAll(a, b);
	cout<<a<<" "<<b<<endl;
	char a1 = 'a', b1 = 'b';
	swapAll(a1, b1);
	cout<<a1<<" "<<b1<<endl;
}

函数模板 会编译两次:

  • 第一次:是对函数模板 本身编译
  • 第二次:函数调用处 将T的类型具体化

函数模板目标:模板是为了实现泛型,可以减轻编程的工作量,增强函数的重用性。

函数模板的注意点

1、函数模板 和 普通函数 都识别。(优先选择 普通函数)

//T只能对当前函数有效 typename可以换成class
template<typename T>
void swapAll(T &a, T &b)
{
    
    
	T tmp = a;
	a = b;
	b=tmp;
	cout<<"函数模板"<<endl;
	return;
}
void swapAll(int &a, int &b)
{
    
    
	int tmp = a;
	a = b;
	b=tmp;
	cout<<"普通函数"<<endl;
	return;
}
int main()
{
    
    
	int a = 10, b = 20;
	swapAll(a, b);//调用普通函数
	cout<<a<<" "<<b<<endl;
}

2、函数模板 和 普通函数 都识别。强制使用函数模板

int main()
{
    
    
	int a = 10, b = 20;
	//强制使用函数模板
	swapAll<>(a, b);//调用函数模板,系统推倒
	swapAll<int>(a, b);//人为推导
	cout<<a<<" "<<b<<endl;
}

3 、函数模板 自动类型推导时 不能对函数的参数 进行 自动类型转换。

template<typename T>
void myPrintAll(T a, T b)
{
    
    
	cout<<a<<" "<<b<<endl;
	cout<<"函数模板"<<endl;
}


 
void myPrintAll(int a, int b)
{
    
    
	cout<<a<<" "<<b<<endl;
	cout<<"普通函数"<<endl;
}
int main()
{
    
    
	myPrintAll(10, 20);//普通函数
	myPrintAll('a','b');//函数模板
	myPrintAll(10,'b');//普通函数,调用函数模板会失败因为推导失败
	//强制说明T为int类型 就支持自动类型转换
	myPrintAll<int>(10,'b');//函数模板
}

函数模板的重载

template<typename T>
void myPrintAll(T a)
{
    
    
	cout<<a<<endl;
	cout<<"函数模板"<<endl;
}
template<typename T>
void myPrintAll(T a, T b)
{
    
    
	cout<<a<<" "<<b<<endl;
	cout<<"函数模板"<<endl;
}

函数模板的局限性

当函数模板 推导出 T为数组或其他自定义类型数据 可能导致运算符 不识别。
解决办法一:运算符重载(推荐)

#include <iostream>
using namespace std;
class Data
{
    
    
	friend ostream& operator<<(ostream& out, Data ob);
private:
	int data;
public:
	Data(){
    
    }
	Data(int data)
	{
    
    
	this->data = data;
	}
};
ostream& operator<<(ostream& out, Data ob)
{
    
    
	out<<ob.data;
	return out;
}


 
template<typename T>
void myPrintAll(T a)
{
    
    
	cout<<a<<endl;
	cout<<"函数模板"<<endl;
}
int main(int argc, char *argv[])
{
    
    
	myPrintAll(10);//10
	myPrintAll('a');//'a'
	Data ob1(100);
	myPrintAll(ob1);//100
	return 0;
}

解决办法二:具体化函数模板

template<typename T>
void myPrintAll(T a)
{
    
    
	cout<<a<<endl;
	cout<<"函数模板"<<endl;
}
class Data
{
    
    
	friend void myPrintAll<Data>(Data a);
private:
	int data;
public:
	Data(){
    
    }
	Data(int data)
{
    
    
	this->data = data;
}
};
//函数模板具体化,默认先调用上面的函数模板,当识别到数据类型是Data时
template<> void myPrintAll<Data>(Data a)
{
    
    
	cout<<a.data<<endl;
	cout<<"函数模板"<<endl;
}
int main(int argc, char *argv[])
{
    
    
	myPrintAll(10);
	myPrintAll('a');
	Data ob1(100);
	myPrintAll(ob1);
	return 0;
}

类模板

类模板基本概念

类模板和函数模板的定义和使用类似,我们已经进行了介绍。有时,有两个或多个类,其功能是相同
的,仅仅是数据类型不同。 类模板用于实现类所需数据的类型参数化。

类模板的定义方式

//类模板
template<class T1, class T2>
class Data
{
    
    
private:
	T1 a;
	T2 b;
public:
	Data(){
    
    }
	Data(T1 a, T2 b)
	{
    
    
		this->a = a;
		this->b = b;
	}
	void showData()
	{
    
    
		cout<<a<<" "<<b<<endl;
	}
};

类模板 实例化对象 不能自动类型推导(重要),要指定模板的数据类型。
在这里插入图片描述

类模板的成员函数在类外实现

类模板的成员函数在类外实现时,不管成员函数是有参还是无参,都需要加类的作用域修饰,有固定格式。需要重新写上template<class T1, class T2>,否则系统无法识别

//类模板
template<class T1, class T2>
class Data
{
    
    
private:
	T1 a;
	T2 b;
public:
	Data(){
    
    }
	Data(T1 a, T2 b);
	void showData();
};
template<class T1, class T2>
Data<T1,T2>::Data(T1 a, T2 b)
{
    
    
	this->a = a;
	this->b = b;
}
template<class T1, class T2>
void Data<T1,T2>::showData()
{
    
    
	cout<<a<<" "<<b<<endl;
}

函数模板作为类模板的友元

//类模板
template<class T1, class T2>
class Data
{
    
    
	template<typename T3, typename T4>
	friend void myPrintData(Data<T3, T4> &ob);
private:
	T1 a;
	T2 b;
public:
	Data(){
    
    }
	Data(T1 a, T2 b);
	void showData();
};
//函数模板
template<typename T3, typename T4> void myPrintData(Data<T3, T4> &ob)
{
    
    
	cout<<ob.a<<" "<<ob.b<<endl;
}
int main()
{
    
    
	//类模板实例化对象调用有参构造
	Data<int, char> ob1(100, 'A');
	myPrintData(ob1);
}

普通函数作为类模板的友元

//类模板
template<class T1, class T2>
class Data
{
    
    
	friend void myPrintData(Data<int, char> &ob);
private:
	T1 a;
	T2 b;
public
	Data(){
    
    }
	Data(T1 a, T2 b);
	void showData();
};
//普通函数
void myPrintData(Data<int, char> &ob)
{
    
    
	cout<<ob.a<<" "<<ob.b<<endl;
}
int main()
{
    
    
	Data<int, char> ob1(100, 'A');
	myPrintData(ob1);
}

模板头文件 和源文件分离问题

原因是经过2次编译,所以最好不要将模板头文件与源文件分离
类模板的头文件一般是类名称和函数的结合体,所以文件命名为.hpp
data.hpp


#ifndef DATA_H
#define DATA_H
#include<iostream>
using namespace std;
template<class T1, class T2>
class Data
{
    
    
private:
	T1 a;
	T2 b;
public:
	Data();//在data.cpp文件中生成时要加上类的参数类型,例如Data<T1, T2>
	Data(T1 a, T2 b);
	void showData(void);
};
template<class T1, class T2>
Data<T1, T2>::Data()
{
    
    
	cout<<"无参构造"<<endl;
}
template<class T1, class T2>
Data<T1, T2>::Data(T1 a, T2 b)
{
    
    
	this->a = a;
	this->b = b;
}
template<class T1, class T2>
void Data<T1, T2>::showData(void)
{
    
    
	cout<<a<<" "<<b<<endl;
}
#endif // DATA_H

main.cpp

#include <iostream>
#include"data.hpp"

using namespace std;
int main(int argc, char *argv[])
{
    
    
	Data<int,char> ob1(100,'A');
	ob1.showData();
	return 0;
}

案例:设计数组类模板

数组类模板:可以存放任意数据类型
myarray.hpp

#ifndef MYARRAY_HPP
#define MYARRAY_HPP
#include<iostream>
#include<string.h>
#include<string>
using namespace std;
template<class T>
class MyArray
{
    
    
	template<typename T1>
	friend ostream& operator<<(ostream& out, MyArray<T1> ob);
private:
	T *arr;//保存数组首元素地址
	int size;//大小
	int capacity;//容量
public:
	MyArray();
	MyArray(int capacity);
	MyArray(const MyArray &ob);
	~MyArray();
	MyArray& operator=(MyArray &ob);
	void pushBack(T elem);
	void sortArray();
};
#endif // MYARRAY_HPP
template<class T>
MyArray<T>::MyArray()
{
    
    
	capacity = 0;
	size = 0;
	arr = NULL;
}
template<class T>
MyArray<T>::MyArray(int capacity)
{
    
    
	this->capacity = capacity;
	size = 0;
	arr = new T[this->capacity];
	memset(arr, 0,sizeof(T)*capacity);
}
template<class T>
MyArray<T>::MyArray(const MyArray &ob)
{
    
    
	if(ob.arr == NULL)
	{
    
    
		arr = NULL;
		size = 0;
		capacity=0;
	}
	else
	{
    
    
		capacity = ob.capacity;
		size = ob.size;
		arr = new T[capacity];
		memcpy(arr, ob.arr, sizeof(T)*capacity);
	}
}
template<class T>
MyArray<T>::~MyArray()
{
    
    
	if(arr != NULL)
	{
    
    
		delete [] arr;
	}
}
template<class T>
MyArray<T> &MyArray<T>::operator=(MyArray<T> &ob)
{
    
    
	//判断this->arr是否存在空间
	if(arr != NULL)
	{
    
    
		delete [] arr;
		arr=NULL;
	}
	size = ob.size;
	capacity = ob.capacity;
	arr = new T[capacity];
	memset(arr,0,sizeof(T)*capacity);
	memcpy(arr, ob.arr, sizeof(T)*capacity);
	return *this;
}
template<class T>
void MyArray<T>::pushBack(T elem)
{
    
    
	if(size==capacity)//满
	{
    
    
	//扩展容量
	capacity = (capacity==0?1:2*capacity) ;
	
	
	 
	//申请空间
	T *tmp = new T[capacity];
	if(arr != NULL)
	{
    
    
		//将旧空间的内容拷贝到新空间
		memcpy(tmp,arr,sizeof(T)*size);
		//释放旧空间
		delete [] arr;
		
	}
		arr = tmp;
	}
	arr[size] = elem;
	size++;
	return;
}
template<class T>
void MyArray<T>::sortArray()
{
    
    
	if(arr == NULL)
	{
    
    
		cout<<"容器为空间"<<endl;
	}
	else
	{
    
    
		int i=0,j=0;
		for(i=0;i<size-1;i++)
		{
    
    
			for(j=0;j<size-i-1;j++)
			{
    
    
				if(arr[j] > arr[j+1])
				{
    
    
					T tmp = arr[j];
					arr[j] = arr[j+1];
					arr[j+1] = tmp;
				}
			}
		}
	}
	return;
}
template<typename T1>
ostream& operator<<(ostream& out, MyArray<T1> ob)
{
    
    
	int i=0;
	for(i=0;i<ob.size;i++)
	{
    
    
		out<<ob.arr[i]<<" ";
	}
	return out;
}

main.cpp

#include <iostream>
#include "myarray.hpp"

using namespace std;
class Person
{
    
    
	friend ostream& operator<<(ostream& out, Person ob);
private:
	int num;
	string name;
	float score;
public:
	Person(){
    
    }
	Person(int num,string name, float score)
	{
    
    
		this->num = num;
		this->name = name;
		this->score = score;
	}
	bool operator>(const Person &ob)
	{
    
    
		return num>ob.num;
	}
};
ostream& operator<<(ostream& out, Person ob)
{
    
    
	int i=0;
	out<<ob.num<<" "<<ob.name<<" "<<ob.score<<endl;
	return out;
}
int main(int argc, char *argv[])
{
    
    
	MyArray<int> ob1(5);
	ob1.pushBack(10);
	ob1.pushBack(30);
	ob1.pushBack(20);
	ob1.pushBack(50);
	ob1.pushBack(40);
	cout<<ob1<<endl;
	ob1.sortArray();
	cout<<ob1<<endl;
	MyArray<char> ob2(5);
	ob2.pushBack('A');
	ob2.pushBack('C');
	ob2.pushBack('D');
	ob2.pushBack('B');
	ob2.pushBack('F');
	cout<<ob2<<endl;
	ob2.sortArray();
	cout<<ob2<<endl;


 
	MyArray<Person> ob3;
	ob3.pushBack(Person(100,"lucy", 88.8f));
	ob3.pushBack(Person(103,"bob", 89.9f));
	ob3.pushBack(Person(105,"tom", 98.8f));
	ob3.pushBack(Person(102,"德玛", 99.8f));
	cout<<ob3<<endl;
	ob3.sortArray();
	cout<<ob3<<endl;
	return 0;
}

类模板的派生

类模板 派生出 普通类

#include <iostream>
using namespace std;
//类模板

template<class T1, class T2>
class Base
{
    
    
private:
	T1 a;
	T2 b;
public:
	Base(){
    
    }
	Base(T1 a, T2 b);
	void showData();
};
template<class T1, class T2>
Base<T1,T2>::Base(T1 a, T2 b)
{
    
    
	this->a = a;
	this->b = b;
}
template<class T1, class T2>
void Base<T1,T2>::showData()
{
    
    
	cout<<a<<" "<<b<<endl;
}
//类模板派生处普通类,基类必须写普通类,所以参数要具体化
class Son1:public Base<int,char>
{
    
    
public:
	int c;
public:
//初始化列表
	Son1(int a, char b, int c):Base<int,char>(a, b){
    
    
		this->c = c;
	}
};
int main(int argc, char *argv[])
{
    
    
	Son1 ob1(100, 'A', 200);
	ob1.showData();
	cout<<ob1.c<<endl;
	return 0;
}

类模板 派生处 类模板

#include <iostream>
using namespace std;
//类模板

template<class T1, class T2>
class Base
{
    
    
private:
	T1 a;
	T2 b;
public:
	Base(){
    
    }
	Base(T1 a, T2 b);
	void showData();
};
template<class T1, class T2>
Base<T1,T2>::Base(T1 a, T2 b)
{
    
    
	this->a = a;
	this->b = b;
}
template<class T1, class T2>
void Base<T1,T2>::showData()
{
    
    
	cout<<a<<" "<<b<<endl;
}

//类模板派生处类模板,参数任然是抽象化
template<class T1, class T2, class T3>
class Son1:public Base<T1,T2>
{
    
    
public:
	T3 c;
public:
	Son1(T1 a, T2 b, T3 c):Base<T1,T2>(a, b){
    
    
		this->c = c;
	}
};
int main(int argc, char *argv[])
{
    
    
	Son1<int,char, int> ob1(100, 'A', 200);
	ob1.showData();
	cout<<ob1.c<<endl;
	return 0;
}

第十七章 类型转换

自动类型转换与强制类型转换

整型从低到高:

har -> short -> int -> long -> long long

浮点型从低到高:

loat -> double -> long double

自动类型转换的规则如下:

  • 如果一个表达式中出现了不同类型操作数的混合运算,较低类型将自动向较高类型转换。
  • 当表达式中含有浮点型操作数时,所有操作数都将转换为浮点型。
  • 赋值运算的右值类型与左值类型不一致时,将右值类型提升/降低为左值类型。
  • 赋值运算右值超出了左值类型的表示范围,把该右值截断后赋给左值,所得结果可能毫无意义。

强制类型转换的语法:(目标类型)表达式或目标类型(表达式)

标准c++提供了一个显示的转换的语法,来替代旧的C风格的类型转换。 使用C风格的强制转换可以把想
要的任何东西转换成我们需要的类型。那为什么还需要一个新的C++类型的强制转换呢? 新类型的强制 转换可以提供更好的控制强制转换过程,允许控制各种不同种类的强制转换。C++风格的强制转换其他 的好处是,它们能更清晰的表明它们要干什么。程序员只要扫一眼这样的代码,就能立即知道一个强制 转换的目的

上行、下行转换的概述

在这里插入图片描述

static_cast静态类型转换

class Base{
    
    };
class Son:public Base{
    
    };
class Other{
    
    };

用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
基本类型:支持

int num = static_cast<int>(3.14);//ok

上行转换:支持 安全

Base *p = static_cast<Base *>(new Son);

下行转换:支持 (不安全)

Son *p2 = static_cast<Son *>(new Base);

不相关类型转换:不支持

Base *p3 = static_cast<Base *>(new Other);//err

dynamic_cast静态类型转换

dynamiccast主要用于类层次间的上行转换和下行转换
基本类型:不支持

int num = dynamic_cast<int>(3.14);//err

上行转换:支持

Base *p1 = dynamic_cast<Base *>(new Son);//ok

下行转换:不支持(不安全)

Son *p2 = dynamic_cast<Son *>(new Base);//err

不相关类型转换:不支持

Base *p3 = dynamic_cast<Base *>(new Other);//err

const_cast常量转换

1、将const修饰的指针或引用 转换成 非const (支持)

const int *p1;
int *p2 = const_cast<int *>(p1);
const int &ob = 10;
int &ob1 = const_cast<int &>(ob);

2、将非const修饰的指针或引用 转换成 const (支持)

int *p3;
const int *p4 = const_cast<const int *>(p3);
int data = 10;
const int &ob2 = const_cast<const int &>(data);

重新解释转换(reinterpret_cast) (最不安全)

在这里插入图片描述

第十八章 异常

异常的基本概念

遇到错误 抛出异常 捕获异常

异常:是指在程序运行的过程中发生的一些异常事件(如:除0溢出,数组下标越界,所要读取的文件不
存在,空指针,内存不足,访问非法内存等等)。(异常是一个类)

c++异常机制相比C语言异常处理的优势?

  • 函数的返回值可以忽略,但异常不可忽略。(忽略异常 程序结束)
  • 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。

异常的抛出和捕获

异常的抛出和捕获

try
{
    
    
	throw 异常值;
}
catch(异常类型1 异常值1)
{
    
    
	处理异常的代码1;
}
catch(异常类型2 异常值2)
{
    
    
	处理异常的代码2;
}
catch(...)//任何异常都捕获
{
    
    
	处理异常的代码3;
}

异常抛出和捕获案例

int ret = 0;
try
{
    
    
	//throw 1;
	//throw 'A';
	throw 2.14f;
}
catch(int e)//捕获
{
    
    
	cout<<"int异常值为:"<<e<<endl;
}
catch(char e)//捕获
{
    
    
	cout<<"char异常值为:"<<e<<endl;
}
catch(...)//捕获所有异常
{
    
    
	cout<<"其他异常值为:"<<endl;
}

栈解旋

异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构【局部对象会析构】。析构的顺序与构造的顺序相反,这一过程称为栈的解旋.

try
{
    
    
	Data ob1;
	Data ob2;
	Data ob3;
	throw 1;//抛出异常后 ob3 ob2 ob1依次自动释放(栈解旋)
}

栈解旋案例

#include <iostream>
using namespace std;
class Data
{
    
    
public:
	int a;
public:
	Data(){
    
    }
	Data(int a)
	{
    
    
		this->a = a;
		cout<<"构造函数"<<a<<endl;
	}
	~Data()
	{
    
    
		cout<<"析构函数"<<a<<endl;
	}
};
int main()
{
    
    
	int ret = 0;
	try
	{
    
    
		Data ob1(10);
		Data ob2(20);
		Data ob3(30);
		throw 1;
	}
	catch(int)//捕获
	{
    
    
		cout<<"int异常值为:"<<endl;
	}
	catch(char)//捕获
	{
    
    
		cout<<"char异常值为:"<<endl;
	}
	catch(...)//捕获
	{
    
    
		cout<<"其他异常值为:"<<endl;
	}
	
}

在这里插入图片描述

异常的接口声明

异常的接口声明:描述的是 可以抛出哪些类型的异常

函数默认 可以抛出任何类型的异常(推荐)

void fun01()
{
    
    
	//throw 1;
	//throw '1';
	throw "hello";
}

只能抛出特定类型异常

void fun02() throw(int,char)
{
    
    
	//throw 1;
	//throw '1';
	throw "hello";//抛出 不能捕获
}

不能抛出任何异常

void fun03() throw()
{
    
    
	throw 1;
	//throw '1';
	//throw "hello";//抛出 不能捕获
}

异常变量的生命周期

class MyException
{
    
    
public:
	MyException(){
    
    
		cout << "异常变量构造" << endl;
	};
	MyException(const MyException & e)
	{
    
    
		cout << "拷贝构造" << endl;
	}
	~MyException()
	{
    
    
		cout << "异常变量析构" << endl;
	}
};

以普通对象 接异常值

在这里插入图片描述

以对象指针 接异常值

在这里插入图片描述

对象引用 接异常值(推荐)

抛出匿名对象,用引用去接。1、引用不占用内存空间,2、不会发生拷贝构造,3、栈空间会自动释放
在这里插入图片描述

异常的多态

//异常基类
class BaseException{
    
    
public:
	virtual void printError(){
    
    };
};
//空指针异常
class NullPointerException : public BaseException{
    
    
public:
	virtual void printError(){
    
    
		cout << "空指针异常!" << endl;
	}
};
//越界异常
class OutOfRangeException : public BaseException{
    
    
public:
	virtual void printError(){
    
    
		cout << "越界异常!" << endl;
	}
};
void doWork(){
    
    
	//throw NullPointerException();
	throw OutOfRangeException();
}
int main()
{
    
    
	try{
    
    
		doWork();
	}
	catch (BaseException& ex)//父类引用去接 可以捕获搭配该父类派生出的所有子类的子类
	{
    
    
		ex.printError();
	}
}

C++标准异常

在这里插入图片描述

异常名称 描述
exception 所有标准异常类的父类
bad_alloc 当operator new and operator new[],请求分配内存失败时
bad_exception 这是个特殊的异常,如果函数的异常抛出列表里声明了badexception异常,当函数内部抛出了异常抛出列表中没有的异常,这是调用的unexpected函数 中若抛出异常,不论什么类型,都会被替换为badexception类型
bad_typeid 使用typeid操作符,操作一个NULL指针,而该指针是带有虚函数的类,这时抛出bad_typeid异常
bad_cast 使用dynamic_cast转换引用失败的时候
ios_base::failure io操作过程出现错误
logic_error 逻辑错误,可以在运行前检测的错误
runtime_error 运行时错误,仅在运行时才可以检测的错误

logic_error的子类

异常名称 描述
length_error 试图生成一个超出该类型最大长度的对象时,例如vector的resize操作
domain_error 参数的值域错误,主要用在数学函数中。例如使用一个负值调用只能操作非负数的函数
outofrange 超出有效范围
invalid_argument 参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是’0’或’1’的时候,抛出该异常

runtime_error的子类

异常名称 描述
range_error 计算结果超出了有意义的值域范围
overflow_error 算术计算上溢
underflow_error 算术计算下溢
invalid_argument 参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是’0’或’1’的时候,抛出该异常

在这里插入图片描述

编写自己的异常

基于标准异常基类 编写自己的异常类。
重写父类的what要加const throw否则会抛出系统的标准异常

class NewException:public exception
{
    
    
private:
	string msg;
public:
	NewException(){
    
    }
	NewException(string msg)
	{
    
    
		this->msg = msg;
	}
	//重写父类的what
	virtual const char* what()const throw()//防止父类在子类前抛出标准异常
	{
    
    
		//将string类转换成char *
		//c_str()是string类自带的成员函数
		return this->msg.c_str();
	}
	~NewException(){
    
    }
};
int main()
{
    
    
	try
	{
    
    
		throw NewException("哈哈,自己的异常");
	}
	catch(exception &e)
	{
    
    
		cout<<e.what()<<endl;
	}
}

第十九章 STL之容器

STL 概述

长久以来,软件界一直希望建立一种可重复利用的东西,以及一种得以制造出”可重复运用的东西”的方
法,让程序员的心血不止于随时间的迁移,人事异动而烟消云散,从函数(functions),类别(classes),函数
库(function libraries),类别库(classlibraries)、各种组件,从模块化设计,到面向对象(object oriented),为的就是复用性的提升。

复用性必须建立在某种标准之上。但是在许多环境下,就连软件开发最基本的数据结构(datastructures) 和算法(algorithm)都未能有一套标准。大量程序员被迫从事大量重复的工作,竟然是为了完成前人已经完成而自己手上并未拥有的程序代码,这不仅是人力资源的浪费,也是挫折与痛苦的来源。

为了建立数据结构【即容器,对数据的存取】和算法【对数据的操作】的一套标准,并且降低他们之间的耦合关系,以提升各自的独立性、弹性、交互操作性(相互合作性,interoperability),诞生了 STL

STL的基本概念

STL(Standard Template Library,标准模板库),是惠普实验室开发的一系列软件的统 称。现在主要出现
在 c++中,但是在引入 c++之前该技术已经存在很长时间了。 STL 从广义上分为: 容器(container) 算法 (algorithm) 迭代器(iterator),容器和算法之间通过迭代器进行无缝连接STL 几乎所有的代码都采用了模 板类或者模板函数,这相比传统的由函数和类组成的库来说提供了更好的代码重用机会。STL(Standard
Template Library)标准模板库,在我们 c++标准程序库中隶属于 STL的占到了 80%以上。

STL的六大组件

这六大组件分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。

1、容器:存放数据
2、算法:操作数据
3、迭代器:算法 通过迭代器 操作容器数据
4、仿函数:为算法提供更多的策略
5、适配器:为算法提供更多参数的接口
6、空间配置器:为算法和容器 动态分配、管理空间

STL 的一个重要特性是将数据和操作分离。数据由容器类别加以管理,操作则由 特定的算法完成。

在这里插入图片描述
容器与迭代器一一对应

算法分为:质变算法和非质变算法。

质变算法:是指运算过程中会更改区间内的元素的内容。例如拷贝,替换,删除等 等
非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍 历、寻找极值等

string类

string 容器基本概念

C 风格字符串(以空字符结尾的字符数组)太过复杂难于掌握,不适合大程序的开发,所以 C++标准库定义 了一种 string 类,定义在头件。
String 和 c 风格字符串对比:

  • Char * 是一个指针,String 是一个类,包含数据和方法
  • string 封装了 char * ,管理这个字符串,是一个 char 型的容器。
  • String 封装了很多实用的成员方法
  • 查找 find,拷贝 copy,删除 delete 替换 replace,插入 insert
  • 不用考虑内存释放和越界
  • string 管理 char*所分配的内存。每一次 string 的复制,取值都由 string 类负责维护,不用担心复制
    越界和取值越界等。

string 容器常用操作

内部已经重写了cout<<对象

1、string 构造函数

string();//创建一个空的字符串 例如: string str;
string(const string& str);//使用一个 string 对象初始化另一个 string 对象
string(const char* s);//使用字符串 s 初始化
string(int n, char c);//使用 n 个字符 c 初始化 v

2、string 基本赋值操作

//重写赋值操作符
string& operator=(const char* s);//char*类型字符串 赋值给当前的字符串
string& operator=(const string &s);//把字符串 s 赋给当前的字符串
string& operator=(char c);//字符赋值给当前的字符串
//成员函数赋值
string& assign(const char *s);//把字符串 s 赋给当前的字符串
string& assign(const char *s, int n);//把字符串 s 的前 n 个字符赋给当前的字符串
string& assign(const string &s);//把字符串 s 赋给当前字符串
string& assign(int n, char c);//用 n 个字符 c 赋给当前字符串
string& assign(const string &s, int start, int n);//将 s 从 start 开始 n 个字符赋值给字符串

返回值是引用可以完成链式操作
举例:
在这里插入图片描述

3、string 存取字符操作

char& operator[](int n);//通过[]方式取字符
char& at(int n);//通过 at 方法获取字符

返回值是引用(对象的别名)

在这里插入图片描述
[] 越界不会抛出异常 ,at方法 越界会抛出异常

4、string 拼接操作

string& operator+=(const string& str);//重载+=操作符
string& operator+=(const char* str);//重载+=操作符
string& operator+=(const char c);//重载+=操作符
string& append(const char *s);//把字符串 s 连接到当前字符串结尾
string& append(const char *s, int n);//把字符串 s 的前 n 个字符连接到当前字符串结尾
string& append(const string &s);//同 operator+=()
string& append(const string &s, int pos, int n);//把字符串 s 中从 pos 开始的 n 个字符连接到当前字符串结尾
string& append(int n, char c);//在当前字符串结尾添加 n 个字符 c

5、string 查找和替换

int find(const string& str, int pos = 0) const; //查找 str 第一次出现位置, 从 pos 开始查找
int find(const char* s, int pos = 0) const; //查找 s 第一次出现位置,从 pos开始查找
int find(const char* s, int pos, int n) const; //从 pos 位置查找 s 的前 n 个字符第一次位置
int find(const char c, int pos = 0) const; //查找字符 c 第一次出现位置
int rfind(const string& str, int pos = npos) const;//查找 str 最后一次位置, 从 pos开始查找
int rfind(const char* s, int pos = npos) const;//查找 s 最后一次出现位置,从pos 开始查找
int rfind(const char* s, int pos, int n) const;//从 pos 查找 s 的前 n 个字符最后一次位置
int rfind(const char c, int pos = 0) const; //查找字符 c 最后一次出现位置
string& replace(int pos, int n, const string& str); //替换从 pos 开始 n 个字符为字符串 str
string& replace(int pos, int n, const char* s); //替换从 pos 开始的 n 个字符为字符串s

失败返回-1

6、string 比较操作

/*
compare 函数在>时返回 1,<时返回 -1,==时返回 0。
比较区分大小写,比较时参考字典顺序,排越前面的越小。
大写的 A 比小写的 a 小。
*/
int compare(const string &s) const;//与字符串 s 比较
int compare(const char *s) const;//与字符串 s 比较

重载了> < ==等关运算符 ,可以直接调用运算符进行比较

7、string 子串

string substr(int pos = 0, int n = npos) const;//返回由 pos 开始的 n 个字符组成的字符串

8、string 插入和删除操作

string& insert(int pos, const char* s); //插入字符串
string& insert(int pos, const string& str); //插入字符串
string& insert(int pos, int n, char c);//在指定位置插入 n 个字符 c
string& erase(int pos, int n = npos);//删除从 Pos 开始的 n 个字符

9、string 和 c-style 字符串转换

//string 转 char*
string str = "itcast";
const char* cstr = str.c_str();

//char* 转 string
char* s = "itcast";
string str(s);

在这里插入图片描述

在 c++中存在一个从 const char 到 string 的隐式类型转换,却不存在从一个 string对象到 Cstring 的自
动类型转换。对于 string 类型的字符串,可以通过 c_str()函数返回 string 对象对应的 C_string. 通常,程
序员在整个程序中应坚持使用 string 类对象,直到必须将内容转化为 char 时才将其转换为 C_string. 提
示: 为了修改 string 字符串的内容,下标操作符[]和 at 都会返回字符的引用。但当字符串的内存被重新
分配之后,可能发生错误.

string s = "abcdefg";
char& a = s[2];
char& b = s[3];
a = '1';
b = '2';
cout << s << endl;
cout << (int*)s.c_str() << endl;
s = "----------------------------";
//a = '1';
//b = '2';

cout << s << endl;
cout << (int*)s.c_str() << endl;

string str = “hehehehe”;
str.size()获取字符串长度
str.capacity()获取容量大小

vector容器

vector容器的概述

vector 的数据安排以及操作方式,与 array 非常相似,两者的唯一差别在于空间的运用的灵活性。Array 是静态空间,一旦配置了就不能改变,要换大一点或者小一点的空间,可以,一切琐碎得由自己来,首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间。Vector 是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素。因此 vector 的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必害怕空间不足而一开始就要求一个大块头的 array了。 Vector 的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率,一旦 vector 旧空间满了,如果客户每新增一个元素,vector内部只是扩充一个元素的空间,实为不智,因为所谓的扩充空间(不论多大),一如刚所说,是”配置新空间-数据移动-释放旧空间”的大工程,时间成本很高,应该加入某种未雨绸缪的考虑,稍后我们便可以看到 vector 的空间配置策略。
在这里插入图片描述

v.begin():获取容器的起始迭代器(指向第0个元素)
v.end():获取容器的结束迭代器(指向最后一个元素的下一个位置)

vector 的数据结构

Vector 所采用的数据结构非常简单,线性连续空间,它以两个迭代器 Myfirst 和Mylast 分别指向配置得
来的连续空间中目前已被使用的范围,并以迭代器_Myend指向整块连续内存空间的尾端。 为了降低空
间配置时的速度成本,vector 实际配置的大小可能比客户端需求大一些,以备将来可能的扩充,这边是
容量的概念。换句话说,一个 vector 的容量永远大于或等于其大小,一旦容量等于大小,便是满载,下
次再有新增元素,整个 vector 容器就得另觅居所。

注意: 所谓动态增加大小,并不是在原空间之后续接新空间(因为无法保证原空间之后尚有可配置的空 间),而是一块更大的内存空间,然后将原数据拷贝新空间,并释放原空间。因此,对 vector 的任何操作,一旦引起空间的重新配置,指向原 vector 的所有迭代器就都失效了。这是程序员容易犯的一个错误,务必小心

vector 常用 API 操作

1、vector 构造函数

vector<T> v; //采用模板实现类实现,默认构造函数
//容器 的类型需要指定参数类型
vector(v.begin(), v.end());//将 v[begin(), end())区间中的元素拷贝给本身。
vector(n, elem);//构造函数将 n 个 elem 拷贝给本身。
vector(const vector &vec);//拷贝构造函数。

定义迭代器
在这里插入图片描述
未雨绸缪机制

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    
    
    vector<int> v1;
    cout<<"容量:"<<v1.capacity()<<" 大小:"<<v1.size()<<endl;

    vector<int>::iterator it;

    int i=0;
    int count =0;

    for(i=0;i<1000;i++)
    {
    
    
        v1.push_back(i);
        if(it != v1.begin())
        {
    
    
            count++;
            cout<<"第"<<count<<"次开辟空间容量:"<<v1.capacity()<<endl;
            it=v1.begin();
        }
    }
	return 0;
}

在这里插入图片描述

2、vector 常用赋值操作

assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem);//将 n 个 elem 拷贝赋值给本身。
vector& operator=(const vector &vec);//重载等号操作符
swap(vec);// 将 vec 与本身的元素互换。

3、vector 大小操作

size();//返回容器中元素的个数
empty();//判断容器是否为空
resize(int num);//重新指定容器的长度为 num,若容器变长,则以默认值0填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
resize(int num, elem);//重新指定容器的长度为 num,若容器变长,则以 elem 值填充新位置。如果容器变短,则末尾超出容器长>度的元素被删除。
capacity();//容器的容量
reserve(int len);//容器预留 len 个元素长度,预留位置不初始化,元素不可访问。

4、vector 数据存取操作

at(int idx); //返回索引 idx 所指的数据,如果 idx 越界,抛出 out_of_range 异常。
operator[];//返回索引 idx 所指的数据,越界时,运行直接报错

front();//返回容器中第一个数据元素
back();//返回容器中最后一个数据元素

5、vector 插入和删除操作

insert(const_iterator pos, int count,ele);//迭代器指向位置 pos 插入 count个元素 ele.
push_back(ele); //尾部插入元素 ele
pop_back();//删除最后一个元素
erase(const_iterator start, const_iterator end);//删除迭代器从 start 到 end 之间的元erase(const_iterator pos);//删除迭代器指向的元素
clear();//删除容器中所有元素,容量不会变

6、巧用 swap 收缩内存空间

原理是调用拷贝构造

vector<int> v1;
v1.reserve(1000);
v1.push_back(10);
v1.push_back(20);
v1.push_back(30);
v1.push_back(40);
cout<<"容量:"<<v1.capacity()<<", 大小:"<<v1.size()<<endl;//1000 4

//resize只能修改大小 不能修改容量
//v1.resize(4);

//vector<int>(v1)匿名对象传递参数v1,把一个对象赋值给匿名对象,会发生调用拷贝构造
vector<int>(v1).swap(v1);
cout<<"容量:"<<v1.capacity()<<", 大小:"<<v1.size()<<endl;//4 4

vector 提升

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    
    
    vector<int> v1(5,10);
    vector<int> v2(5,100);
    vector<int> v3(5,1000);

    //需求:定义一个vector容器存放v1 v2 v3
    vector< vector<int> > v;
    v.push_back(v1);
    v.push_back(v2);
    v.push_back(v3);

    //遍历
    vector< vector<int> >::iterator it=v.begin();
    for(;it!=v.end();it++)
    {
    
    
        //*it ==vector<int>
        vector<int>::iterator mit=(*it).begin();
        for(;mit!=(*it).end();mit++)
        {
    
    
            cout<<*mit<<" ";
        }
        cout<<endl;
    }
	return 0;
}
#include <iostream>
#include <vector>
using namespace std;

//使用STL算法对vector容器排序
#include<algorithm>
void printVectorInt(vector<int> &v)
{
    
    
    vector<int>::iterator it=v.begin();
    for(;it!=v.end();it++)
    {
    
    
        cout<<*it<<" ";
    }
    cout<<endl;
}
int main()
{
    
    
    vector<int> v1;
    v1.push_back(20);
    v1.push_back(60);
    v1.push_back(50);
    v1.push_back(30);
    v1.push_back(40);
    v1.push_back(10);
    printVectorInt(v1);

    //排序算法
    sort(v1.begin(), v1.end());
    printVectorInt(v1);
	return 0;
}
#include <iostream>
#include <vector>
using namespace std;
#include<string>
class Person
{
    
    
    friend bool comparePerson(Person &ob1, Person &ob2);
    friend void printVectorPerson(vector<Person> &v);
private:
    int num;
    string name;
    float score;
public:
    Person(){
    
    }
    Person(int num,string name, float score)
    {
    
    
        this->num = num;
        this->name = name;
        this->score = score;
    }
};
void printVectorPerson(vector<Person> &v)
{
    
    
    vector<Person>::iterator it=v.begin();
    for(;it!=v.end();it++)
    {
    
    
        //*it==Person
        cout<<(*it).num<<" "<<(*it).name<<" "<<(*it).score <<endl;
    }
}

bool comparePerson(Person &ob1, Person &ob2)
{
    
    
    return ob1.num < ob2.num;
}

int main()
{
    
    
    vector<Person> v1;
    v1.push_back(Person(100,"lucy", 77.7f));
    v1.push_back(Person(103,"bob", 77.7f));
    v1.push_back(Person(101,"tom", 77.7f));
    v1.push_back(Person(104,"德玛", 77.7f));
    v1.push_back(Person(105,"小法", 77.7f));

    printVectorPerson(v1);

    //对自定义类型的vector排序 需要指定排序规则
    sort(v1.begin(), v1.end(), comparePerson);

    printVectorPerson(v1);

	return 0;
}

deque 容器

deque 容器基本概念

Vector 容器是单向开口的连续内存空间,deque 则是一种双向开口的连续线性空间。所谓的双向开口,
意思是可以在头尾两端分别做元素的插入和删除操作,当然,vector 容器也可以在头尾两端插入元素,
但是在其头部操作效率奇差,无法被接受
在这里插入图片描述

Deque 容器和 vector 容器最大的差异,一在于 deque 允许使用常数项时间对头端进行元素的插入和删
除操作。二在于 deque 没有容量的概念,因为它是动态的以分段连续空间组合而成,随时可以增加一段
新的空间并链接起来,换句话说,像vector 那样,”旧空间不足而重新配置一块更大空间,然后复制元
素,再释放旧空间”这样的事情在 deque 身上是不会发生的。也因此,deque 没有必须要提供所谓的空
间保留(reserve)功能. 虽然 deque 容器也提供了 Random Access Iterator,但是它的迭代器并不是普通
的指针,其复杂度和 vector 不是一个量级,这当然影响各个运算的层面。因此,除非有必要,我们应该
尽可能的使用 vector,而不是deque。对 deque 进行的排序操作,为了最高效率,可将 deque 先完整
的复制到一个 vector 中,对 vector 容器进行排序,再复制回 deque

deque 容器实现原理

Deque 容器是连续的空间,至少逻辑上看来如此(物理上不连续,逻辑上连续),连续现行空间总是令我们联想到array 和vector,array 无法成长,vector 虽可成长却只能向尾端成长,而且其成长其实是一个假象,事实上(1) 申请更大空间 (2)原数据复制新空间 (3)释放原空间三步骤,如果不是 vector 每次配置新的空间时都留有余裕,其成长假象所带来的代价是非常昂贵的。 Deque 是由一段一段的定量的连续空间构成。一旦有必要在deque 前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在deque 的头端或者尾端。

Deque 最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了
重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。 既然 deque 是分段连续内存空间,
那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。
Deque 代码的实现远比 vector 或 list 都多得多。 Deque 采取一块所谓的map(注意,不是 STL 的 map
容器)作为主控,这里所谓的 map 是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一
个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是deque 的存储空间的主体。
在这里插入图片描述

deque 常用 API

1、deque 构造函数

deque<T> deqT;//默认构造形式
deque(beg, end);//构造函数将[beg, end)区间中的元素拷贝给本身。
deque(n, elem);//构造函数将 n 个 elem 拷贝给本身。
deque(const deque &deq);//拷贝构造函数。

2、deque 赋值操作

assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem);//将 n 个 elem 拷贝赋值给本身。
deque& operator=(const deque &deq); //重载等号操作符
swap(deq);// 将 deq 与本身的元素互换

3、deque 大小操作

deque.size();//返回容器中元素的个数
deque.empty();//判断容器是否为空
deque.resize(num);//重新指定容器的长度为 num,若容器变长,则以默认值填充新位置。如果容器变
短,则末尾超出容器长度的元素被删除。
deque.resize(num, elem); //重新指定容器的长度为 num,若容器变长,则以 elem 值填充新位置,如
果容器变短,则末尾超出容器长度的元素被删除。

4、deque 双端插入和删除操作

push_back(elem);//在容器尾部添加一个数据
push_front(elem);//在容器头部插入一个数据
pop_back();//删除容器最后一个数据
pop_front();//删除容器第一个数据

5、deque 数据存取

at(idx);//返回索引 idx 所指的数据,如果 idx 越界,抛出 out_of_range。
operator[];//返回索引 idx 所指的数据,如果 idx 越界,不抛出异常,直接出错。
front();//返回第一个数据。
back();//返回最后一个数据

6、deque 插入操作

insert(pos,elem);//在 pos 位置插入一个 elem 元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在 pos 位置插入 n 个 elem 数据,无返回值。
insert(pos,beg,end);//在 pos 位置插入[beg,end)区间的数据,无返回值。

7、deque 删除操作

clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除 pos 位置的数据,返回下一个数据的位置。

stack容器

stack 容器基本概念

stack 是一种先进后出(First In Last Out,FILO)的数据结构,它只有一个出口,形式如图所示。stack 容器
允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取 stack 的其 他元素。换言之,stack 不允许有遍历行为。 有元素入栈的操作称为:push,将元素出 stack 的操作称为pop.

在这里插入图片描述

Stack 所有元素的进出都必须符合”先进后出”的条件,只有 stack 顶端的元素,才有机会被外界取用。
Stack 不提供遍历功能,也不提供迭代器。

stack常用API

1、stack 构造函数

stack<T> stkT;//stack 采用模板类实现, stack 对象的默认构造形式:
stack(const stack &stk);//拷贝构造函数

2、stack 赋值操作

stack& operator=(const stack &stk);//重载等号操作符

3、stack 数据存取操作

push(elem);//向栈顶添加元素
pop();//从栈顶移除第一个元素
top();//返回栈顶元素

4、stack 大小操作

empty();//判断堆栈是否为空
size();//返回堆栈的大小

queue 容器

queue 容器基本概念

Queue 是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新
增元素,从另一端移除元素。
在这里插入图片描述

Queue 所有元素的进出都必须符合”先进先出”的条件,只有 queue 的顶端元素,才有机会被外界取用。
Queue 不提供遍历功能,也不提供迭代器。

queue常用API

queue<T> queT;//queue 采用模板类实现,queue 对象的默认构造形式:
queue(const queue &que);//拷贝构造函数
push(elem);//往队尾添加元素
pop();//从队头移除第一个元素
back();//返回最后一个元素
front();//返回第一个元素
queue& operator=(const queue &que);//重载等号操作符
empty();//判断队列是否为空
size();//返回队列的大小

list容器

list 容器基本概念

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接
次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每
个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相较于
vector 的连续线性空间,list 就显得负责许多,它的好处是每次插入或者删除一个元素,就是配置或者
释放一个元素的空间。因此,list 对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的
元素插入或元素的移除,list 永远是常数时间。 List 和vector 是两个最常被使用的容器。 List 容器是一
个双向链表。

在这里插入图片描述

采用动态存储分配,不会造成内存浪费和溢出 链表执行插入和删除操作十分方便,修改指针即可,不需
要移动大量元素 链表灵活,但是空间和时间额外耗费较大。

list常用API

1、list 构造函数

list<T> lstT;//list 采用采用模板类实现,对象的默认构造形式:
list(beg,end);//构造函数将[beg, end)区间中的元素拷贝给本身。
list(n,elem);//构造函数将 n 个 elem 拷贝给本身。
list(const list &lst);//拷贝构造函数。

2、 list 数据元素插入和删除操作

push_back(elem);//在容器尾部加入一个元素
pop_back();//删除容器中最后一个元素
push_front(elem);//在容器开头插入一个元素
pop_front();//从容器开头移除第一个元素
insert(pos,elem);//在 pos 位置插 elem 元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在 pos 位置插入 n 个 elem 数据,无返回值。
insert(pos,beg,end);//在 pos 位置插入[beg,end)区间的数据,无返回值。
clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除 pos 位置的数据,返回下一个数据的位置。
remove(elem);//删除容器中所有与 elem 值匹配的元素。

3、list 大小操作

size();//返回容器中元素的个数
empty();//判断容器是否为空
resize(num);//重新指定容器的长度为 num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
resize(num, elem);//重新指定容器的长度为 num,若容器变长,则以 elem 值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。

4、list 赋值操作

assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem);//将 n 个 elem 拷贝赋值给本身。
list& operator=(const list &lst);//重载等号操作符
swap(lst);//将 lst 与本身的元素互换。

5、list 数据的存取

front();//返回第一个元素。
back();//返回最后一个元素。

6、list 反转排序

reverse();//反转链表,比如 lst 包含 1,3,5 元素,运行此方法后,lst 就包含 5,3,1元素。
sort(); //list 排序

举例

#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
void printListInt(list<int> &l)
{
    
    
    list<int>::iterator it=l.begin();
    for(;it!=l.end();it++)
    {
    
    
        cout<<*it<<" ";
    }
    cout<<endl;
}
void test01()
{
    
    
    list<int> l1;
    l1.push_back(10);
    l1.push_back(20);
    l1.push_back(30);
    l1.push_front(40);
    l1.push_front(50);
    l1.push_front(60);

    printListInt(l1);//60 50 40 10 20 30

    //list容器的迭代器是双向迭代器  不支持+2(随机访问迭代器支持)  支持++
    list<int>::iterator it=l1.begin();
    //it+2;
    it++;
    it++;
    l1.insert(it, 3, 100);
    printListInt(l1);//60 50 100 100 100 40 10 20 30

    //STL提供的算法 只支持随机访问迭代器 而list是双向迭代器 所以sort不支持list
    //sort(l1.begin(), l1.end());
    l1.sort();
    printListInt(l1);//10 20 30 40 50 60 100 100 100
    l1.reverse();
    printListInt(l1);
}



int main(int argc, char *argv[])
{
    
    
    test01();
    return 0;
}

set/multiset容器

set/multiset 容器基本概念

set 的特性是。所有元素都会根据元素的键值自动被排序,set 的元素即是键值又是实值。
在这里插入图片描述

set 不允许两个元素有相同的键值。
set容器的迭代器是只读迭代器,不允许修改键值,会破坏set的内存布局。
multiset 特性及用法和 set 完全相同,唯一的差别在于它允许键值重复。

set常用API

1、set 构造函数

set<T> st;//set 默认构造函数:
mulitset<T> mst; //multiset 默认构造函数:
set(const set &st);//拷贝构造函数

2、set 赋值操作

set& operator=(const set &st);//重载等号操作符
swap(st);//交换两个集合容器

3、set 大小操作

size();//返回容器中元素的数目
empty();//判断容器是否为空

4、set 插入和删除操作

insert(elem);//在容器中插入元素。
clear();//清除所有元素
erase(pos);//删除 pos 迭代器所指的元素,返回下一个元素的迭代器。
erase(beg, end);//删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
erase(elem);//删除容器中值为 elem 的元素。

5、set 查找操作

find(key);//查找键 key 是否存在,若存在,返回该键的元素的迭代器;若不存在,返回 set.end();
count(key);//查找键 key 的元素个数
lower_bound(keyElem);//返回第一个 key>=keyElem 元素的迭代器。
upper_bound(keyElem);//返回第一个 key>keyElem 元素的迭代器。
equal_range(keyElem);//返回容器中 key 与 keyElem 相等的上下限的两个迭代器

举例迭代器是只读迭代器,原因是采用平衡二叉树的方法管理数据

#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
#include<set>
void printSetInt(set<int>  &s)
{
    
    
    set<int>::const_iterator it=s.begin();
    for(;it!=s.end();it++)
    {
    
    
        cout<<*it<<" ";
    }
    cout<<endl;
}

//仿函数:重载函数调用运算符()的类
class MyGreater
{
    
    
public:
    bool operator()(int v1, int v2)
    {
    
    
        return v1>v2;
    }
};

void printSetInt(set<int,MyGreater>  &s)
{
    
    
	//修改排序规则,仿函数既是函数又是类型,MyGreater类会自动生成对象,对象与小括号结合会触发重载函数
    set<int,MyGreater>::const_iterator it=s.begin();
    for(;it!=s.end();it++)
    {
    
    
        cout<<*it<<" ";
    }
    cout<<endl;
}
void test02()
{
    
    
    //set<int,排序规则 > s1;
    set<int,MyGreater> s1;
    s1.insert(30);
    s1.insert(10);
    s1.insert(20);
    s1.insert(50);
    s1.insert(40);
    printSetInt(s1);

}

int main(int argc, char *argv[])
{
    
    
    test01();
    return 0;
}

set存放自定义数据必须修改排序规则

#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
#include<set>
class MyGreaterPerson;
class Person
{
    
    
    friend class MyGreaterPerson;
    friend void printSetPerson(set<Person, MyGreaterPerson> &s);
private:
    int num;
    string name;
    float score;
public:
    Person(){
    
    }
    Person(int num, string name, float score)
    {
    
    
        this->num = num;
        this->name = name;
        this->score = score;
    }

};
class MyGreaterPerson
{
    
    
public:
    bool operator()(Person ob1, Person ob2)
    {
    
    
        return ob1.num < ob2.num;
    }
};
void printSetPerson(set<Person, MyGreaterPerson> &s)
{
    
    
    set<Person, MyGreaterPerson>::const_iterator it=s.begin();
    for(;it!=s.end();it++)
    {
    
    
        //*it==Person
        cout<<(*it).num<<" "<<(*it).name<<" "<<(*it).score<<endl;
    }
}
void test03()
{
    
    
    //set存放自定义数据必须修改排序
    set<Person, MyGreaterPerson> s;
    s.insert(Person(100,"lucy", 88.8f));
    s.insert(Person(103,"tom", 88.8f));
    s.insert(Person(105,"bob", 88.8f));
    s.insert(Person(104,"德玛", 88.8f));
    s.insert(Person(102,"小法", 88.8f));
    printSetPerson(s);

}
void test04()
{
    
    
    set<int> s1;
    s1.insert(10);
    s1.insert(30);
    s1.insert(50);
    s1.insert(70);
    s1.insert(90);
    printSetInt(s1);

    set<int>::const_iterator ret;
    ret = s1.find(50);
    if(ret != s1.end())
    {
    
    
        cout<<"找到的结果为:"<<*ret<<endl;
    }

    cout<<s1.count(50)<<endl;
}
int main()
{
    
    
	test02();
	return 0;
}

equal_range(keyElem);//返回容器中 key 与 keyElem 相等的上下限的两个迭代器
举例

#include <iostream>
#include <list>
#include <algorithm>
#include<set>
using namespace std;
void test05()
{
    
    
    set<int> s1;
    s1.insert(10);
    s1.insert(30);
    s1.insert(50);
    s1.insert(70);
    s1.insert(90);
    set<int>::const_iterator ret;
    ret = s1.lower_bound(50);
    if(ret!=s1.end())
    {
    
    
        cout<<"下限为:"<<*ret<<endl;
    }
    ret = s1.upper_bound(50);
    if(ret!=s1.end())
    {
    
    
        cout<<"上限为:"<<*ret<<endl;
    }

    //以对组的方式 存储上下限 pair
    pair< set<int>::const_iterator , set<int>::const_iterator> pa;
    pa = s1.equal_range(50);
    if(pa.first != s1.end())
    {
    
    
        cout<<"下限为:"<<*(pa.first)<<endl;
    }
    if(pa.second != s1.end())
    {
    
    
        cout<<"上限为:"<<*(pa.second)<<endl;
    }
}

int main(int argc, char *argv[])
{
    
    
    test07();
    return 0;
}

对组(pair)

对组(pair)将一对值组合成一个值,这一对值可以具有不同的数据类型,两个值可以分别用 pair 的两个
公有属性 first 和 second 访问。 类模板:template <class T1, class T2> struct pair. 如何创建对组?

//第一种方法创建一个对组
pair<string, int> pair1(string("name"), 20);
cout << pair1.first << endl; //访问 pair 第一个值
cout << pair1.second << endl;//访问 pair 第二个值
//第二种
pair<string, int> pair2 = make_pair("name", 30);
cout << pair2.first << endl;
cout << pair2.second << endl;
//pair=赋值
pair<string, int> pair3 = pair2;
cout << pair3.first << endl;
cout << pair3.second << endl;

map/multimap 容器

map/multimap 基本概念

Map 的特性是,所有元素都会根据元素的键值自动排序。Map 所有的元素都是pair,同时拥有实值和键
值,pair 的第一元素被视为键值,第二元素被视为实值,map 不允许两个元素有相同的键值。 我们可
以通过 map 的迭代器改变 map 的键值吗?答案是不行,因为 map 的键值关系到 map 元素的排列规
则,任意改变 map键值将会严重破坏 map 组织。如果想要修改元素的实值,那么是可以的。 Map 和
list 拥有相同的某些性质,当对它的容器元素进行新增操作或者删除操作时,操作之前的所有迭代器,在
操作完成之后依然有效,当然被删除的那个元素的迭代器必然是个例外。 Multimap 和 map 的操作类
似,唯一区别 multimap 键值可重复。Map 和 multimap 都是以红黑树为底层实现机制。

在这里插入图片描述

map容器:每个元素都是 键值-实值 成对存储,自动根据键值排序, 键值不能重复,不能修改。

map/multimap 常用 API

1、map 构造函数

map<T1, T2> mapTT;//map 默认构造函数:
map(const map &mp);//拷贝构造函数

2、map 赋值操作

map& operator=(const map &mp);//重载等号操作符
swap(mp);//交换两个集合容器

3、 map 大小操作

size();//返回容器中元素的数目
empty();//判断容器是否为空

4、 map 插入数据元素操作

map.insert(...); //往容器插入元素,返回 pair<iterator,bool>
map<int, string> mapStu;
// 第一种 通过 pair 的方式插入对象
mapStu.insert(pair<int, string>(3, "小张"));
// 第二种 通过 pair 的方式插入对象
mapStu.inset(make_pair(-1, "校长"));
// 第三种 通过 value_type 的方式插入对象
mapStu.insert(map<int, string>::value_type(1, "小李"));
// 第四种 通过数组的方式插入值

mapStu[3] = "小刘";
mapStu[5] = "小王";

5、 map 删除操作

clear();//删除所有元素
erase(pos);//删除 pos 迭代器所指的元素,返回下一个元素的迭代器。
erase(beg,end);//删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
erase(keyElem);//删除容器中 key 为 keyElem 的对组。

6、map 查找操作

find(key);//查找键 key 是否存在,若存在,返回该键的元素的迭代器;/若不存在,返回 map.end();
count(keyElem);//返回容器中 key 为 keyElem 的对组个数。对 map 来说,要么是 0,要么是 1。对 multimap 来说,值可能大于 1。
lower_bound(keyElem);//返回第一个 key>=keyElem 元素的迭代器。
upper_bound(keyElem);//返回第一个 key>keyElem 元素的迭代器。
equal_range(keyElem);//返回容器中 key 与 keyElem 相等的上下限的两个迭代器

举例:

#include <iostream>
#include <map>
#include <algorithm>
using namespace std;
class Person
{
    
    
    friend void test01();
    friend void printMapAll(map<int,Person> &m);
private:
    int num;
    string name;
    float score;
public:
    Person() {
    
    }
    Person(int num, string name, float score)
    {
    
    
        this->num = num;
        this->name = name;
        this->score = score;
    }
};

void printMapAll(map<int,Person> &m)
{
    
    
    map<int,Person>::const_iterator it=m.begin();
    for(;it!=m.end();it++)
    {
    
    
        //*it==pair<int,Person>
        cout<<(*it).first<<" "<<(*it).second.name<<" "<<(*it).second.score<<endl;
    }
}
int main()
{
    
    
    map<int,Person> m;
    //方式1
    m.insert(pair<int,Person>(103,Person(103,"lucy", 88.8f)));
    //方式2(推荐)
    m.insert( make_pair(101,Person(101,"bob", 88.8f)));
    //方式3:
    m.insert(  map<int,Person>::value_type(102,Person(102,"tom",66.6f)) );
    //方式4:(危险)
    m[104]=Person(104,"德玛",99.9f);

    printMapAll(m);

    //cout<<m[101].num<<" "<<m[101].name<<" "<<m[101].score<<endl;
    cout<<m[107].num<<" "<<m[107].name<<" "<<m[107].score<<endl;
    cout<<"-----------"<<endl;
    printMapAll(m);
	return 0;
}

STL 容器使用时机

在这里插入图片描述

  1. vector 的使用场景:比如软件历史操作记录的存储,我们经常要查看历史记录,比如上一次的记录,上上次的记录,但却不会去删除记录,因为记录是事实的描述。

  2. deque 的使用场景:比如排队购票系统,对排队者的存储可以采用 deque,支持头端的快速移除,尾端的快速添加。如果采用 vector,则头端移除时,会移动大量的数据,速度慢。 vector 与 deque 的比较: 一:vector.at()比 deque.at()效率高,比如 vector.at(0)是固定的,deque 的开始位置 却是不固定的。 二:如果有大量释放操作的话,vector 花的时间更少,这跟二者的内部实现有关。
    deque 支持头部的快速插入与快速移除,这是 deque 的优点。

  3. list 的使用场景:比如公交车乘客的存储,随时可能有乘客下车,支持频繁的不确实位置元素的移除插入。

  4. set 的使用场景:比如对手机游戏的个人得分记录的存储,存储要求从高分到低分的顺序排列。

  5. map 的使用场景:比如按 ID 号存储十万个用户,想要快速要通过 ID 查找对应的用户。二叉树的查找效率,这时就体现出来了。如果是vector 容器,最坏的情况下可能要遍历完整个容器才能找到该用户。

第二十章 STL之算法

函数对象

重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对
象,也叫仿函数(functor),其实就是重载“()”操作符,使得类对象可以像函数那样调用。
注意:

  1. 函数对象(仿函数)是一个类,不是一个函数。
  2. 函数对象(仿函数)重载了”() ”操作符使得它可以像函数一样调用。

分类:

  • 如果函数对象 有一个参数 叫:一元函数对象
  • 如果函数对象 有二个参数 叫:二元函数对象
  • 如果函数对象 有三个参数 叫:多元函数对象
//函数对象也称为(仿函数)
class Print
{
    
    
public:
	void operator()(char *str)
	{
    
    
		cout<<str<<endl;
	}
		
};
int main()
{
    
    
	Print ob;
	ob("hello world");
	Print()("hello world");//匿名对象
}

总结:

  • 1、函数对象通常不定义构造函数和析构函数,所以在构造和析构时不会发生任何问题,避免了函数调
    用的运行时问题。
  • 2、函数对象超出普通函数的概念,函数对象可以有自己的状态
  • 3、函数对象可内联编译,性能好。用函数指针几乎不可能
  • 4、模版函数对象使函数对象具有通用性,这也是它的优势之一

谓词

返回值为bool类型的普通函数或仿函数 都叫谓词。

  • 如果谓词 有一个参数 叫:一元谓词
  • 如果谓词 有二个参数 叫:二元谓词

一元谓词

#include<vector>
#include<algorithm>
//普通函数
bool greaterThan30(int value)
{
    
    
	return value>30;
}
class GreaterThan30
{
    
    
public:
	//仿函数
	bool operator()(int value)
	{
    
    
		return value>30;
	}
};
int main()
{
    
    
	vector<int> v1;
	v1.push_back(10);
	v1.push_back(30);
	v1.push_back(50);
	v1.push_back(70);
	v1.push_back(90);
	//find_if条件查找
	vector<int>::iterator ret;
	
	//普通函数提供策略 函数名
	//ret = find_if(v1.begin(), v1.end(), greaterThan30);
	//仿函数提供策略 类名称+()
	ret = find_if(v1.begin(), v1.end(), GreaterThan30());
	if(ret != v1.end())
	{
    
    
		cout<<"寻找的结果:"<<*ret<<endl;
	}
}

底层会遍历两个数据
在这里插入图片描述

二元谓词

bool myGreaterInt(int v1, int v2)
{
    
    
	return v1>v2;
}
class MyGreaterInt
{
    
    
public:
	bool operator()(int v1, int v2)
	{
    
    
		return v1>v2;
	}
};
int main()
{
    
    
	vector<int> v1;
	v1.push_back(10);
	v1.push_back(50);
	v1.push_back(30);
	v1.push_back(90);
	v1.push_back(70);
	printVectorAll(v1);
	//sort(v1.begin(), v1.end(), myGreaterInt);
	sort(v1.begin(), v1.end(), MyGreaterInt());
	printVectorAll(v1);
}

在这里插入图片描述

内建函数对象

STL 内建了一些函数对象。分为:算数类函数对象,关系运算类函数对象,逻辑运算类仿函数。这些仿函数
所产生的对象,用法和一般函数完全相同,当然我们还可以产生无名的临时对象来履行函数功能。

6个算数类函数对象,除了 negate 是一元运算,其他都是二元运算。

template<class T> T plus<T>//加法仿函数
template<class T> T minus<T>//减法仿函数
template<class T> T multiplies<T>//乘法仿函数
template<class T> T divides<T>//除法仿函数
template<class T> T modulus<T>//取模仿函数
template<class T> T negate<T>//取反仿函数

6个关系运算类函数对象,每一种都是二元运算。

template<class T> bool equal_to<T>//等于
template<class T> bool not_equal_to<T>//不等于
template<class T> bool greater<T>//大于
template<class T> bool greater_equal<T>//大于等于
template<class T> bool less<T>//小于
template<class T> bool less_equal<T>//小于等于

逻辑运算类运算函数,not 为一元运算,其余为二元运算。

template<class T> bool logical_and<T>//逻辑与
template<class T> bool logical_or<T>//逻辑或
template<class T> bool logical_not<T>//逻辑非

举例:

#include <iostream>
#include <vector>
#include <algorithm>//引入
using namespace std;
void printVectorInt(vector<int> &v)
{
    
    
    vector<int>::iterator it=v.begin();
    for(;it!=v.end();it++)
    {
    
    
        cout<<*it<<" ";
    }
    cout<<endl;
}
void test02()
{
    
    
    vector<int> v1;
    v1.push_back(10);
    v1.push_back(50);
    v1.push_back(30);
    v1.push_back(90);
    v1.push_back(70);
    printVectorInt(v1);

    sort(v1.begin(), v1.end(), greater<int>());//类模板要指定参数类型,此时是创建匿名对象
    printVectorInt(v1);
}
void test03()
{
    
    
    vector<int> v1;
    v1.push_back(10);
    v1.push_back(50);
    v1.push_back(30);
    v1.push_back(90);
    v1.push_back(70);

    vector<int>::iterator  ret;
    //使用适配器绑定参数
    ret = find_if(v1.begin(), v1.end(), bind2nd(greater<int>(),30));
    if(ret != v1.end())
    {
    
    
        cout<<*ret<<endl;
    }
}
int main()
{
    
    
	test02();
	return 0;
}

适配器

适配器 为算法 提供接口。

函数对象适配器

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//第二步:公共继承binary_function   参数萃取
class printInt:public binary_function<int,int,void>
{
    
    
public:
	//第三步:整个函数加const修饰
	void operator()(int value, int tmp) const
	{
    
    
		cout<<"value="<<value<<" tmp="<< tmp<<endl;
	}
};
int main(int argc, char *argv[])
{
    
    
	vector<int> v1;
	v1.push_back(10);
	v1.push_back(30);
	v1.push_back(50);
	v1.push_back(70);
	v1.push_back(90);
	
	//for_each 提取容器的每个元素
	//第一步bind2nd 或 bind1st
	//bind2nd将100绑定到第二个参数tmp行 容器的元素在value上
	for_each(v1.begin(), v1.end(), bind2nd(printInt(), 100) );
	cout<<endl;
	return 0;
}

上图是bind2nd,下图是bind1st
在这里插入图片描述

函数指针适配器 ptr_fun

普通函数名 作为适配器
在这里插入图片描述
函数名在c++中不能代表函数入口地址, 利用函数指针得到函数入口地址

成员函数 作为适配器 mem_fun_ref

class Data
{
    
    
public:
	int data;
public:
	Data(){
    
    }
	Data(int d){
    
    
		data = d;
	}
	void printInt(int tmp)
	{
    
    
		cout<<"value="<<data+tmp<<endl;
	}
};


 
int main()
{
    
    
	vector<Data> v1;
	v1.push_back(Data(10));
	v1.push_back(Data(30));
	v1.push_back(Data(50));
	v1.push_back(Data(70));
	v1.push_back(Data(90));
	//for_each 提取容器的每个元素
	for_each(v1.begin(), v1.end(), bind2nd(mem_fun_ref(&Data::printInt),100) );
	cout<<endl;
}

在这里插入图片描述

取反适配器

not1 一元取反
在这里插入图片描述

not2 二元取反

int main()
{
    
    
	vector<int> v1;
	v1.push_back(10);
	v1.push_back(40);
	v1.push_back(50);
	v1.push_back(20);
	v1.push_back(30);
	
	
	 
	
	//lambda 表达式 c++11才支持
	//[]里面啥都不写 lambda不能识别 外部数据
	//[=] lambda能对 外部数据 读操作
	//[&] lambda能对 外部数据 读写操作
	for_each(v1.begin(), v1.end(), [&](int val){
    
    
		cout<<val<<" ";
	});
	cout<<endl;
	sort(v1.begin(), v1.end(),not2(greater<int>()));
	for_each(v1.begin(), v1.end(), [&](int val){
    
    
		cout<<val<<" ";
	});
	cout<<endl;
}

在这里插入图片描述

算法概述

算法的头文件#include 是所有 STL 头文件中最大的一个,其中常用的功能涉及到比较,交换,查找,遍历,复制,修改,反转,排序,合并等。

常用遍历算法

for_each 遍历算法

/*
遍历算法 遍历容器元素
@param beg 开始迭代器
@param end 结束迭代器
@param _callback 函数回调或者函数对象
@return 函数对象
*/

for_each(iterator beg, iterator end, _callback);

transform 算法

/*
transform 算法 将指定容器区间元素搬运到另一容器中
注意 : transform 不会给目标容器分配内存,所以需要我们提前分配好内存
@param beg1 源容器开始迭代器
@param end1 源容器结束迭代器
@param beg2 目标容器开始迭代器
@param _cakkback 回调函数或者函数对象
@return 返回目标容器迭代器
*/
transform(iterator beg1, iterator end1, iterator beg2, _callback);

在这里插入图片描述
回调函数会将形参返回到参数三指定的位置

常用查找算法

find 算法

/*
find 算法 查找元素
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param value 查找的元素
@return 返回查找元素的位置
*/
find(iterator beg, iterator end, value);
//用迭代器会接返回值

find_if 算法

/*
find_if 算法 条件查找
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param callback 回调函数或者谓词(返回 bool 类型的函数对象)
@return bool 查找返回 true 否则 false
*/

find_if(iterator beg, iterator end, _callback);

adjacent_find 算法

/*
adjacent_find 算法 查找相邻重复元素
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param _callback 回调函数或者谓词(返回 bool 类型的函数对象)
@return 返回相邻元素的第一个位置的迭代器
*/
adjacent_find(iterator beg, iterator end, _callback);
//返回值用迭代器去接

binary_search 算法

/*
binary_search 算法 二分查找法
注意: 在无序序列中不可用
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param value 查找的元素
@return bool 查找返回 true 否则 false
*/
bool binary_search(iterator beg, iterator end, value);
//返回值是布尔值

count 算法

/*
count 算法 统计元素出现次数
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param value 回调函数或者谓词(返回 bool 类型的函数对象)
@return int 返回元素个数

*/
count(iterator beg, iterator end, value);

count_if 算法

/*
count_if 算法 统计元素出现次数
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param callback 回调函数或者谓词(返回 bool 类型的函数对象)
return int 返回元素个数
*/
count_if(iterator beg, iterator end, _callback);

在这里插入图片描述

常用排序算法

merge 算法

/*
merge 算法 容器元素合并,并存储到另一容器中,要先开辟第三个容器的空间
注意:两个容器必须是有序的,谁小谁先移动
@param beg1 容器 1 开始迭代器
@param end1 容器 1 结束迭代器
@param beg2 容器 2 开始迭代器
@param end2 容器 2 结束迭代器
@param dest 目标容器开始迭代器
*/
merge(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);

在这里插入图片描述

sort 算法

/*
sort 算法 容器元素排序
@param beg 容器 1 开始迭代器
@param end 容器 1 结束迭代器
@param _callback 回调函数或者谓词(返回 bool 类型的函数对象)
*/
sort(iterator beg, iterator end, _callback)

random_shuffle 算法

/*
random_shuffle 算法 对指定范围内的元素随机调整次序
@param beg 容器开始迭代器
@param end 容器结束迭代器
*/
random_shuffle(iterator beg, iterator end);

可以hi设置随机数种子
在这里插入图片描述

reverse 算法

/*
reverse 算法 反转指定范围的元素
@param beg 容器开始迭代器
@param end 容器结束迭代器
*/
reverse(iterator beg, iterator end)

常用拷贝替换算法

copy 算法

/*
copy 算法 将容器内指定范围的元素拷贝到另一容器中
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param dest 目标起始迭代器
*/
copy(iterator beg, iterator end, iterator dest)

往终端上输出
在这里插入图片描述

replace 算法

/*
replace 算法 将容器内指定范围的旧元素修改为新元素
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param oldvalue 旧元素
@param oldvalue 新元素
*/
replace(iterator beg, iterator end, oldvalue, newvale)

replace_if 算法

/*
replace_if 算法 将容器内指定范围满足条件的元素替换为新元素
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param callback 函数回调或者谓词(返回 Bool 类型的函数对象)
@param oldvalue 新元素
*/
replace_if(iterator beg, iterator end, _callback, newvalue)

在这里插入图片描述

swap 算法

/*
swap 算法 互换两个容器的元素
@param c1 容器 1
@param c2 容器 2
*/
swap(container c1, container c2)

常用算术生成算法

accumulate 算法

/*
accumulate 算法 计算容器元素累计总和
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param value 累加值
*/
accumulate(iterator beg, iterator end, value)

fill 算法

/*
fill 算法 向容器中添加元素
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param value t 填充元素
*/
fill(iterator beg, iterator end, value)

常用集合算法

在这里插入图片描述

set_intersection 算法

/*
set_intersection 算法 求两个 set 集合的交集
注意:两个集合必须是有序序列
@param beg1 容器 1 开始迭代器
@param end1 容器 1 结束迭代器
@param beg2 容器 2 开始迭代器
@param end2 容器 2 结束迭代器
@param dest 目标容器开始迭代器
@return 目标容器的最后一个元素的迭代器地址
*/
set_intersection(iterator beg1, iterator end1, iterator beg2, iterator end2,iterator dest)

set_union 算法

/*
set_union 算法 求两个 set 集合的并集
注意:两个集合必须是有序序列
@param beg1 容器 1 开始迭代器
@param end1 容器 1 结束迭代器
@param beg2 容器 2 开始迭代器
@param end2 容器 2 结束迭代器
@param dest 目标容器开始迭代器
@return 目标容器的最后一个元素的迭代器地址
*/
set_union(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest)

set_difference 算法

/*
set_difference 算法 求两个 set 集合的差集
注意:两个集合必须是有序序列
@param beg1 容器 1 开始迭代器
@param end1 容器 1 结束迭代器
@param beg2 容器 2 开始迭代器
@param end2 容器 2 结束迭代器
@param dest 目标容器开始迭代器
@return 目标容器的最后一个元素的迭代器地址
*/
set_difference(iterator beg1, iterator end1, iterator beg2, iterator end2,iterator dest)

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/NRWHF/article/details/129542989