C++笔记(初阶)

 一、C++入门基础

1.第一个C++程序

#include<iostream>
using namespace std;
int main()
{
cout << "hello world\n" << endl;
return 0;
}

C++中标准输入输出的头文件是<iostream>不需要加.h

2.命名空间

1.namespace的价值

变量、函数和后⾯要学到的类都是⼤量存在的,这些变量、函数和类的名称将都存在于全局作⽤域中可能会导致很多冲突。使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

比如有定义一个变量rand,但是头文件中同时已经存在了rand函数,这时就会报错

2.namespace的定义

定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。

namespace hiro//这个是空间名,可以自定义

{

        

int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};

}//这里不需要加 ;

namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下⾯的rand不在冲突了

C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的⽣命周期命名空间域和类域不影响变量⽣命周期。

namespace只能定义在全局,当然他还可以嵌套定义

namespace hiro//这个是空间名,可以自定义

{

        namespace hiro_a

        {

        }

}

项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突
C++标准库都放在⼀个叫std(standard)的命名空间中

3.命名空间使⽤

编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以下⾯程序会编译报错。所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
指定命名空间访问,项⽬中推荐这种⽅式。
using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
// 指定命名空间访问
int main()
{
printf("%d\n", N::a);
return 0;
}
// using将命名空间中某个成员展开
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
// 展开命名空间中全部成员
using namespce N;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}

用 "::"来进行引用

3.C++输入和输出

<iostream> 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象。
std::cin 是 istream 类的对象,它主要⾯向窄字符(narrow characters (of type char))的标准输⼊流
std::cout 是 ostream 类的对象,它主要⾯向窄字符的标准输出流
std::endl 是⼀个函数,流插⼊输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区
<<是流插⼊运算符,>>是流提取运算符。(C语⾔还⽤这两个运算符做位运算左移/右移)
使⽤C++输⼊输出更⽅便,不需要像printf/scanf输⼊输出时那样,需要⼿动指定格式,C++的输⼊输出可以⾃动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是C++的流能更好的⽀持⾃定义类型对象的输⼊输出。
cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使⽤⽅式去⽤他们。
⼀般⽇常练习中我们可以using namespace std,实际项⽬开发中不建议using namespace std
这⾥我们没有包含<stdio.h>,也可以使⽤printf和scanf,在包含<iostream>间接包含了。vs系列编译器是这样的,其他编译器可能会报错。
// io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码
// 可以提⾼C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);

4.缺省参数

缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数。(有些地⽅把缺省参数也叫默认参数
全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值
带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
函数声明和定义分离时缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值
//全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
	cout << "a= " << a << endl;
	cout << "b= " << b << endl;
	cout << "c= " << c << endl<<endl;
}
//半缺省
void Func2(int a, int b = 20, int c = 30)
{
	cout << "a= " << a << endl;
	cout << "b= " << b << endl;
	cout << "c= " << c << endl<<endl;
}
//void Func2(int a=10, int b = , int c = 30)不能这么给形参
int main()
{
    Func1(1, 2, 3);
    Func1();
    Func2(1,2,3);
    Func2(1, 2);
    Func2(1);

    //Func1(1,3)不能这样跳着给实参
  
}

5.函数重载

C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。
//函数重载
//1.参数类型不同
int ADD(int left, int right)
{
	cout << "int ADD(int left, int right)" << endl;
	return left + right;
}
double ADD(double left, double right)
{
	cout << "double ADD(double left, double right)" << endl;
	return left + right;
}//函数类型不同不可以

//是重载但是调用有问题
void f1()
{
	return;
}
void f1(int a = 10)
{
	return;
}

//可以这样
namespace hiro
{
	void f2()
	{
		return;
	}
}
void f2(int a = 10);
//3.参数顺序不同
void f4(int a, int b)
{
	cout << "f(int a,char b)" << endl;
}
void f4(int b, int a)
{
	cout << "f(int b,char a)" << endl;
}

6.引用

1.引⽤的概念和定义

引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间
类型& 引⽤别名 = 引⽤对象;
C++中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的<< 和 >>,这⾥引⽤也和取地址使⽤了同⼀个符号&
//引用

int main()
{
int m = 0;
int& n = m;
int& p = m;
p++;
cout << p << endl;
n++;
cout << m << endl<<endl;
}

2.引用的特性

引⽤在定义时必须初始化
⼀个变量可以有多个引⽤
引⽤⼀旦引⽤⼀个实体,不能再引⽤其他实体
int main()
{
int a = 10;
// 编译报错:“ra”: 必须初始化引⽤
//int& ra;
int& b = a;
int c = 20;
// 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向,
// 这⾥是⼀个赋值
b = c;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}

3.引⽤的使⽤

引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率改变引⽤对象时同时改变被引⽤对象
引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些
引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向,Java的引⽤可以改变指向
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 0, y = 1;
cout << x <<" " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}

比如写链表时就可以利用到引用:

typedef struct ListNode
{
	int val;
	struct ListNode* next;
}LTNode, * PNode;

// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名
// 这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序
//void ListPushBack(LTNode** phead, int x)
 //void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{
	PNode newnode = (PNode)malloc(sizeof(LTNode));
	newnode->val = x;
	newnode->next = NULL;
	if (phead == NULL)
	{
		phead = newnode;

		{
			//...
		}
	}

栈中

int& STTop(ST& rs)
 {
 assert(rs.top > 0);

return rs.a[rs.top];//返回一个引用,引用的名字不重要
}

4.const引用

可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤
不需要注意的是类似 int& rb = a*3; d  ouble d = 12.34; int& rd = d; 这样⼀些场
景下a*3的结果保存在⼀个临时对象中 int& rd = d 也是类似,在类型转换会产⽣临时对象存储中间值,也就是时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以
所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象。

产生临时对象的情况:

1.函数参数或返回值是值类型(而非引用 / 指针)时,编译器会创建临时对象来拷贝数据 

2. 隐式类型转换

当表达式中需要某种类型,但提供的是另一种可转换的类型时,编译器会创建临时对象完成转换

3. 表达式运算中的临时对象

5.指针和引⽤的关系

C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间
引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些

6.inline

⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就需要建⽴栈帧了,就可以提⾼效率
inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略
C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调试,C++设计了inline⽬的就是替代C的宏函数
vs编译器 debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下以下两个地⽅。
inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{
int ret = x + y;
ret += 1;
ret += 1;
ret += 1;
return ret;
}
int main()
{
// 可以通过汇编观察程序是否展开
// 有call Add语句就是没有展开,没有就是展开了
int ret = Add(1, 2);
cout << Add(1, 2) * 5 << endl;
return 0;
}

7.nullptr

NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void*)的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0调⽤了f(int x),因此与程序的初衷相悖。f((void*)NULL);调⽤会报错。
C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型
#include<iostream>
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
// 本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int
x),因此与程序的初衷相悖。
f(NULL);
f((int*)NULL);

// 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型
 // f((void*)NULL);

 f(nullptr);

 }

二、类和对象

1.类的定义

1.类定义格式

class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后⾯分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的⽅法或者成员函数
为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前⾯或者后⾯加_ 或者 m开头,注意C++中这个并不是强制的,只是⼀些惯例,具体看公司的要求。
C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类。
定义在类⾯的成员函数默认为inline

2.访问限定符

C++⼀种实现封装的⽅式⽤类将对象的属性与⽅法结合在⼀块,让对象更加完善,通过访问权限选择性的将其接⼝提供给外部的⽤⼾使⽤
public修饰的成员在类外可以直接被访问protected和private修饰的成员在类外不能直接被访问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。
访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有访问限定符,作⽤域就到 }即类结束。
class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public。

3.类域

类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作⽤域操作符指明成员属于哪个类域
类域影响的是编译的查找规则,Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪⾥,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。
声明和定义分离,需要指定类域
#include<iostream>
using namespace std;
class Stack
{
public:
// 成员函数
void Init(int n = 4);
private:
// 成员变量
int* array;
size_t capacity;
size_t top;
};
// 声明和定义分离,需要指定类域
void Stack::Init(int n)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}

int main()
{
Stack st;
st.Init();
return 0;
}

2.实例化

1.实例化概念

⽤类类型在物理内存中创建对象的过程,称为类实例化出对象
类是对象进⾏⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,⽤类实例化出对象时,才会分配空间。
⼀个类可以实例化出多个对象,实例化出的对象 占⽤实际的物理空间,存储类成员变量。打个⽐⽅:类实例化出对象就像现实中使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房⼦才能住⼈。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。

class Date
{
public:
	Date(int year = 2026, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	int _year;
	int _month;
	int _day;//这里只是声明,并没有开辟空间
};
int main()
{
// Date类实例化出对象d1和d2
Date d1(2026,2,1);
Date d2(2026,4,3);
}

2.对象⼤⼩

函数不占对象的大小,函数存放在代码段

函数指针是⼀个地址,调⽤函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找,只有动态多态是在 运⾏时找,就需要存储函数地址

内存对⻬规则
第⼀个成员在与结构体偏移量为0的地址处
其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处
注意:对⻬数 = 编译器默认的⼀个对⻬数 与 该成员⼤⼩的较⼩值
VS中默认的对⻬数为8
结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍
如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍。

// 内部嵌套结构体
struct Inner {
    char a;   // 大小1,对齐数min(1,8)=1
    int b;    // 大小4,对齐数min(4,8)=4
    double c; // 大小8,对齐数min(8,8)=8
};

// 外部结构体
struct Outer {
    char x;        // 大小1,对齐数1
    Inner inner;   // 嵌套结构体,先算Inner的属性
    short y;       // 大小2,对齐数min(2,8)=2
};

Inner最大对齐数:8 | Inner总大小:16

Outer最大对齐数:8 | Outer总大小:32

没有任何成员的类的大小是1,纯粹是为了占位标识对象存在

3.this指针

Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调⽤Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这⾥就要看到C++给了⼀个隐含的this指针解决这⾥的问题
编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this指针。⽐如Date类的Init的真实原型为, void Init(Date* const this, int year,int month, int day)
类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this->_year = year;
C++规定不能在实参和形参的位置显⽰的写this指针(编译时编译器会处理),但是可以在函数体内显⽰使⽤this指针
class Date
{
public:
// void Init(Date* const this, int year, int month, int day)
void Init(int year, int month, int day)
{
// 编译报错:error C2106: “=”: 左操作数必须为左值
// this = nullptr;
// this->_year = year;
_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;//这里可以加上this->,但是没必要 
}
private:
// 这⾥只是声明,没有开空间
int _year;
int _month;
int _day;
};
int main()
{
// Date类实例化出对象d1和d2
Date d1;
Date d2;
// d1.Init(&d1, 2024, 3, 31);
d1.Init(2024, 3, 31);
d1.Print();//这里不能写d1.Print(&d1);
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
//this指针理论上会放到栈里,vs放到寄存器

4.C++和C语⾔实现Stack对⽐

C++中数据和函数都放到了类⾥⾯,通过访问限定符进⾏了限制不能再随意通过对象直接修改数据,这是C++封装的⼀种体现,这个是最重要的变化。这⾥的封装的本质是⼀种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后⾯还需要不断的去学习。
C++中有⼀些相对⽅便的语法,⽐如Init给的缺省参数会⽅便很多,成员函数每次不需要传对象地址,因为this指针隐含的传递了,⽅便了很多,使⽤类型不再需要typedef⽤类名就很⽅便

C++实现栈的代码

#include<iostream>
#include<cassert>
using namespace std;

typedef int STDataType;
class Stack
{
public:
	// 成员函数
	//构造函数
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}

		_capacity = n;
		_top = 0;
	}
	//析构函数
	//后生成的先析构
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}
	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}

			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_top++] = x;
	}

	void Pop()
	{
		assert(_top > 0);
		--_top;
	}

	bool Empty()
	{
		return _top == 0;
	}

	int Top()
	{
		assert(_top > 0);

		return _a[_top - 1];
	}

	void Destroy()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	// 成员变量
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

3. 类的默认成员函数

默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后⾯再讲解。默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯去学习:
第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?

5.构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),⽽是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数⾃动调⽤的特点就完美的替代的了Init。
构造函数的特点:
1. 函数名与类名相同
2. ⽆返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
3. 对象实例化时系统会⾃动调⽤对应的构造函数。
4. 构造函数可以重载
5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显式定义编译器将不再⽣成。
6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调⽤的构造就叫默认构造
7. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我们下个章节再细细讲解。

说明:C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型,如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。
class Date
{
public:
// 1.⽆参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
 }

// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
= day;
 }

// 3.全缺省构造函数
 /*Date(int year = 1, int month = 1, int day = 1)
 {
_year = year;
 _month = month;
_day = day;
 }*/
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};

int main()
{
// 如果留下三个构造中的第⼆个带参构造,第⼀个和第三个注释掉
// 编译报错:error C2512: “Date”: 没有合适的默认构造函数可⽤
Date d1; // 调⽤默认构造函数
Date d2(2025, 1, 1); // 调⽤带参的构造函数

// 注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,否则编译器⽆法
// 区分这⾥是函数声明还是实例化对象
// warning C4930: “Date d3(void)”: 未调⽤原型函数(是否是有意⽤变量定义的?)
Date d3();

d1.Print();
d2.Print();

return 0;
}

6.析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。析构函数的功能类⽐我们之前Stack实现的Destroy功能,⽽像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
析构函数的特点:
1. 析构函数名是在类名前加上字符 ~
2. ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)
3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
4. 对象⽣命周期结束时系统会⾃动调⽤析构函数
5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数
也就是就算自己写的析构函数中没有初始化自定义成员,编译器也会进行初始化工作
7. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack。
8. ⼀个局部域的多个对象,C++规定后定义的先析构

7.拷⻉构造函数

如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数
拷⻉构造的特点:
1. 拷⻉构造函数是构造函数的⼀个重载
2. 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。 拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。
3. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成
4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
这时候如果拷贝的是指针那么就会出问题,拷贝出的对象中的指针会指向源对象中的指针
5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造也不需要我们显⽰实现MyQueue的拷⻉构造。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写拷⻉构造,否则就不需要
6. 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤)没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样传引⽤返回可以减少拷⻉但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回

传引用的时候没有进行传值,只有拷贝,所以不会无穷递归;传值传参的时候需要进行拷贝构造,而这里的拷贝构造需要传值传参,所以造成了无穷递归

void Func1(Date d)
 {
cout << &d << endl;
d.Print();
}

Date& Func2()
{
Date tmp(2024, 7, 5);
tmp.Print();

return tmp;
}

int main()
{
Date d1(2024, 7, 5);
// C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥传值传参要调⽤拷⻉
构造
// 所以这⾥的d1传值传参给d要调⽤拷⻉构造完成拷⻉,传引⽤传参可以较少这⾥的拷⻉
Func1(d1);

cout << &d1 << endl;
// 这⾥可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造
Date d2(&d1);
d1.Print();
d2.Print();

 //这样写才是拷⻉构造,通过同类型的对象初始化构造,⽽不是指针
Date d3(d1);
d2.Print();

// 也可以这样写,这⾥也是拷⻉构造
Date d4 = d1;
 d2.Print();

// Func2返回了⼀个局部对象tmp的引⽤作为返回值
 // Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤
Date ret = Func2();
ret.Print();

return 0;
}
// 两个Stack实现队列
class MyQueue
{
public:
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
// Stack不显⽰实现拷⻉构造,⽤⾃动⽣成的拷⻉构造完成浅拷⻉
// 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃
Stack st2 = st1;
MyQueue mq1;
// MyQueue⾃动⽣成的拷⻉构造,会⾃动调⽤Stack拷⻉构造完成pushst/popst
// 的拷⻉,只要Stack拷⻉构造⾃⼰实现了深拷⻉,他就没问题
MyQueue mq2 = mq1;
return 0;
}

8.赋值运算符重载

1.运算符重载

当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错
运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体
重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数
如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致
不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@。
.*   ::  sizeof   ?:   .   这5个运算符不能重载。
操作符⾄少有⼀个类类型参数不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)不能通过运算符重载去修改内置类型的原有行为
⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意义,但是重载operator+就没有意义。
重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。
 

    Date operator++(int)
    {
        Date tmp(*this);
        *this += 1;

        return tmp;
    }

重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性
重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	//cout从左往右调用,所以这里要返回out来继续插入下一个值
	return out;
}
istream& operator>>(istream& in, Date& d)
{
	while (1)
	{
		cout << "请输出年月日:";
		in >> d._year >> d._month >> d._day;
		if (!d.CheckDate())
		{
			cout << "输入非法日期,请重新输入" << endl;
		}
		else
		{
			break;
		}
	}
	return in;
}

2.赋值运算符重载

赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象

1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引⽤,否则会传值传参会有拷⻉
2. 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景
3. 没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认拷⻉构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的赋值重载函数。
4. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的赋值运算符重载会调⽤Stack的赋值运算符重载,也不需要我们显⽰实现MyQueue的赋值运算符重载。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要

9.取地址运算符重载

1.const成员函数

const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后⾯
const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。
const 修饰Date类的Print成员函数,Print隐含的this指针Date* const this 变为 constDate* const this
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}

2.取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。

10.再探构造函数(初始化列表)

之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅式,就是初始化列表,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式
每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地⽅
引⽤成员变量,const成员变量,没有默认构造的类类型变量必须放在初始化列表位置进⾏初始化,否则会编译报错。
C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的
尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误
初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)//初始化列表
		,_month(month)//括号里可以放初始值,也可以放表达式
		,_day(day)
		//,x(1)//const类型的初始化要用初始化列表
		//,y(day)//引用也要
		//,_time(1)//没有默认构造的自定义类型必须用
		//,_ptr((int*)malloc(sizeof(int)*5))//可以和函数混着用,比如这样
		////,_year(year)每个成员变量只能出现一次,像这样在写一次就不行
	{
		//if (_ptr == NULL)
		//{
		//	perror("malloc fail");
		//	return;
		//}
		//else
		//{

		//}
	}
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	//声明
	int _year=1;
	int _month=1;
	int _day=1;
	//int* _ptr=(int*)malloc(sizeof(int)*5);//这种也可以给缺省值
	//Time _time=1;//这里也是
	////必须在初始化列表初始化
	//const int x;
	//int& y;
};

11. 类型转换

C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数
构造函数前⾯explicit就不再⽀持隐式类型转换
类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持
#include<iostream>
using namespace std;
class A
{
public:
// 构造函数explicit就不再⽀持隐式类型转换
// explicit A(int a1)
A(int a1)
:_a1(a1)
{}
//explicit A(int a1, int a2)
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
B(const A& a)
:_b(a.Get())
{}
private:
int _b = 0;
};
int main()
{
// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3
// 编译器遇到连续构造+拷⻉构造->优化为直接构造
A aa1 = 1;
aa1.Print();
const A& aa2 = 1;
// C++11之后才⽀持多参数转化
A aa3 = { 2,2 };
// aa3隐式类型转换为b对象
// 原理跟上⾯类似
B b = aa3;
const B& rb = aa3;
return 0;
}

12. static成员

static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化
静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。
⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数
突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
静态成员也是类的成员,受public、protected、private 访问限定符的限制
静态成员变量不能在声明位置给缺省值初始化,因为缺省值是构造函数初始化列表的,静态成员变量不属于某个对象,不⾛构造函数初始化列表
#include<iostream>
using namespace std;
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
static int GetACount()
{
return _scount;
}
private:
// 类⾥⾯声明
static int _scount;
};
// 类外⾯初始化
int A::_scount = 0;

13. 友元

友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前⾯加friend并且把友元声明放到⼀个类的⾥⾯
外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数
友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制
⼀个函数可以是多个类的友元函数。
友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员
友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。
友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。
有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤
#include<iostream>
using namespace std;
// 前置声明,否则A的友元函数声明编译器不认识B
class B;
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);

return 0;
}



class A
{
// 友元声明
friend class B;
 private:
int _a1 = 1;
int _a2 = 2;
};

14.内部类

如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类
内部类默认是外部类的友元类
内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地⽅都⽤不了

15. 匿名对象

类型(实参) 定义出来的对象叫做匿名对象,相⽐之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象
匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象
// 匿名对象在这样场景下就很好⽤,当然还有⼀些其他使⽤场景,这个我们以后遇到了再说
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
Solution().Sum_Solution(10);

16.对象拷⻉时的编译器优化

现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷⻉
如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进⾏跨⾏跨表达式的合并优化

三、内存管理

1. C/C++内存分布

【说明】
1. 又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
3. 用于程序运行时动态内存分配,堆是可以上增长的。
4. 数据段(静态区)--存储全局数据和静态数据
5. 代码段--可执行的代码/只读常量

2.C++内存管理方式

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理

1. new/delete操作内置类型

void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[3];
delete ptr4;
delete ptr5;
delete[] ptr6;
}

申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]

2.newdelete操作自定义类型

class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
// new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间
还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(1);
free(p1);
delete p2;
// 内置类型是几乎是一样的
int* p3 = (int*)malloc(sizeof(int)); // C
int* p4 = new int;
free(p3);
delete p4;
A* p5 = (A*)malloc(sizeof(A)*10);
A* p6 = new A[10];
free(p5);
delete[] p6;
return 0;
}

在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。

3.operator newoperator delete函数

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间

new是先调用malloc再调用构造函数,delete先调用析构再调用free

operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的

4.newdelete的实现原理

1.内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL

2.自定义类型

delete的原理
1. 在空间上执行析构函数,完成对象中资源的清理工作
2. 调用operator delete函数释放对象的空间
new T[N]的原理
1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
2. 在申请的空间上执行N次构造函
delete[]的原理
1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

5.定位new表达式(placement-new)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
使用格式:
new (place_address) type
//或者
new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
// 定位new/replacement new
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没
有执行
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}

6.malloc/freenew/delete的区别

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
1. malloc和free是函数,new和delete是操作符
2. malloc申请的空间不会初始化,new可以初始化
3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
5. malloc申请空间失败时,返回的是NULL因此使用时必须判空,new不需要,但是new需要捕获异常
6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化delete在释放空间前会调用析构函数完成空间中资源的清理释放

补充:new[]申请数组时,时如果对象中有非平凡析构的成员,那么会在头部多开4/8字节用来存放数组的长度

四、模板

1. 泛型编程

在想要实现一个通用的函数时,可以通过函数重载,但是这样太麻烦,要写很多个,代码复用率很低,这时就可以使用函数模板来解决

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础

2. 函数模板

1.函数模板概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化根据实参类型产生函数的特定类型版本

2.函数模板格式

template<typename T1, typename T2,......,typename Tn>
返回值类型函数名(参数列表){}
template<typename T>    //这里的typename也可以用class,效果都一样
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}

3.函数模板的原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此

4.函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化

1. 隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
/*
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要
背黑锅
Add(a1, d1);
*/
// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
Add(a, (int)d);
return 0;
}
2. 显式实例化:在函数名后的<>中指定模板参数的实际类型
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错

5.模板参数的匹配原则

 1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数 
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化,这里调用的是非模板函数
Add<int>(1, 2); // 调用编译器特化的Add版本  ,这里<>强制编译器使用函数模板
}
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的
Add函数
}
3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

3. 类模板

1 .类模板的定义格式

template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
#include<iostream>
using namespace std;
// 类模版
template<typename T>
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
void Push(const T& data);
private:
T* _array;
size_t _capacity;
size_t _size;
};
// 模版不建议声明和定义分离到两个文件.h 和.cpp会出现链接错误
template<class T>
void Stack<T>::Push(const T& data)
{
// 扩容
_array[_size] = data;
++_size;
}
int main()
{
Stack<int> st1; // int
Stack<double> st2; // double
return 0;
}

2.类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类
// Stack是类名,Stack<int>才是类型
Stack<int> st1; // int
Stack<double> st2; // double

4.非类型模板参数

模板参数分类类型形参与非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
template<class T, size_t N = 10>

5.模板的特化

1.概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; // 可以比较,结果正确
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 可以比较,结果错误
return 0;
}
Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误

此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化

2.函数模板特化

函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出

3. 类模板特化

1 .全特化
全特化即是将模板参数列表中所有的参数都确定化
template<class T1, class T2>
class Data
{
public:
Data() {cout<<"Data<T1, T2>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
template<>
class Data<int, char>
{
public:
Data() {cout<<"Data<int, char>" <<endl;}
private:
int _d1;
char _d2;
};
2.偏特化
任何针对模版参数进一步进行条件限制设计的特化版本
1.部分特化
将模板参数类表中的一部分参数特化
template <class T1>
class Data<T1, int>
{
public:
Data() {cout<<"Data<T1, int>" <<endl;}
private:
T1 _d1;
int _d2;
};
2.参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>    //这里有<>所以是特化
{
public:
Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout<<"Data<T1&, T2&>" <<endl;
}
private:
const T1 & _d1;
const T2 & _d2;
};
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout<<"Data<T1&, T2&>" <<endl;
}
private:
const T1 & _d1;
const T2 & _d2;
};

3.类模板特化应用示例

有如下专门用来按照小于比较的类模板Less:
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
int main()
{
Date d1(2022, 7, 7);
Date d2(2022, 7, 6);
Date d3(2022, 7, 8);
vector<Date> v1;
v1.push_back(d1);
v1.push_back(d2);
v1.push_back(d3);
// 可以直接排序,结果是日期升序
sort(v1.begin(), v1.end(), Less<Date>());
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序
// 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象
// 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期
sort(v2.begin(), v2.end(), Less<Date*>());
return 0;
}
通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指针,结果就不一定正确。因为:sort最终按照Less模板中方式比较,所以只会比较指针,而不是比较指针指向空间中内容,此时可以使用类版本特化来处理上述问题:
// Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return *x < *y;
}
};

6.模板分离编译

1.什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

2.模板的分离编译

假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}

3.解决方法

 1. 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种。
2. 模板定义的位置显式实例化。这种方法不实用,不推荐使用。 

五、STL

1.STL介绍

STL(standard template libaray-标准模板库)C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架

STL的六大组件:

2.string类

string提供了对字符串的各种操作函数

1.auto和范围for

1.auto
在早期C/C++auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
用auto声明指针类型时,用autoauto*没有任何区别,但用auto声明引用类型时则必须加&
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
auto不能直接用来声明数组
2.范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围自动迭代,自动取数据,自动判断结束
范围for可以作用到数组和容器对象上进行遍历
范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到
for (auto& e : array)
e *= 2;
for (auto e : array)
cout << e << " " << endl;

2.vs和g++下string结构的说明

1.vs
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
vs下string的结构
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义
string中字符串的存储空间:
当字符串长度小于16时,使用内部固定的字符数组来存放
当字符串长度大于等于16时,从堆上开辟空间

大多数情况下字符串的长度都小于16,那string对象创建
好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高

2.g++下string的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个
指针,该指针将来指向一块堆空间,内部包含了如下字段
空间总大小
字符串有效长度
引用计数
指向堆空间的指针,用来存储字符串

3.string各种接口的使用

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;
//class string   //operator[]大概就是这样
//{
//public:
//	char& operator[](size_t i)
//	{
//		assert(i<_size)
//		return _str[i];   //这里返回第i个字符的引用,就可以用类似 s5[3] = 'x'; 来改变第i个位置的字符
//	}
//private:
//	char* _str;
//	size_t _size;
//	size_t _capacity;
//};
int main()
{
	string s1;
	string s2("hello world");
	string s3(s2);
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	string s4(s2, 6, 5);//从第n个位置拷贝m个字符
	cout << s4 << endl;
	string s5(s2, 6, 15);//这样会直接拷贝到最后,不会报错
	string s6(s2, 6 /*,nops*/ );//这里会直接相当于传npos,npos=-1 但是size_t类型是无符号的
	                            //所以这里会直接给最大值42亿多,也就相当于直接拷贝到最后
	string s7("hello world", 5); //拷贝前n个字符
	string s8(10, 'X');//拷贝n个字符到字符串中,这里必须是  ''
	cout << s7 << endl;
	cout << s8 << endl;
	s5[3] = 'x';   //和数组一样改变字符串第i个位置的字符, 越界了还会直接报错
	cout << s5 << endl;
	for (int i = 0; i < s2.size(); i++)   //string中提供了一个size接口来获得字符串的从长度
	{
		cout << s2[i] << " ";        //这种方法只有底层是数组的时候可以,其他类型的时候就要用迭代器了
	}
	cout << endl;
	
	//必须指明类域
	string::iterator it = s2.begin();   //这里用了迭代器,it可以看作是一个指针,  
										//如果it不是指针,这里的运算符也可以用运算符重载来进行操作
	while (it != s2.end())//这里的end()返回的是 past the end ,就是end的下一个位置
	{
		*it += 2;//迭代器这里可以修改s2里的字符
		cout << *it << " ";
		it++;
	}
	cout << endl;
	
	//遍历的另一种方法
	//范围for   这个适用于数组和容器
	for (auto ch : s2)  //这里的auto是自动推导,就是自动推导这里需要的类型
						//这里可以写成 char chr:s2
						//底层就是迭代器,这里编译的时候会变成上面迭代器的样子
						//但是上面的迭代器可以修改,这里的ch只是拷贝
	{
		ch-=2;				//这里只能修改ch的值,s2不会变
		cout << ch << " ";
	}
	cout << endl;

	cout << s2 << endl;
	
	for (auto& ch : s2)  //想要修改的话需要把这里改成引用,这里ch就变成了s2里每个字符的引用 

	{
		ch-=2;				
		cout << ch << " ";
	}
	cout << endl;

	int a = 0;
	cout << typeid(s2).name() << endl;  //typeid可以用来看类型
	cout << typeid(a).name() << endl; 


	//auto e;   //必须有初值,auto是通过右边的初值来推导类型的
	//auto a = 1, b = 1.1;            //这样会报错,必须是同一种类型
	//auto a[]={4,5,6}          //不能用来定义数组
	//int func1(auto a)            //不能做参数
	//auto func2(int a)         // 但是可以做返回值


	//反向迭代器   就是把迭代器全都倒过来了
	string::reverse_iterator rit = s2.rbegin();
	while (rit != s2.rend())
	{
		cout << *rit << " ";
		rit++;  //++也会倒着走   
	}
	cout << endl;



	const string s9("hello world");  //这里是const  auto会调用最匹配的  也就是const迭代器
	
	//const迭代器      //只能读不能写    cit指向的内容不能被修改    
	//string::const_iterator cit = s9.begin();
	auto cit = s9.begin();

	while (cit != s9.end())
	{
		cout << *cit << " ";
		cit++;  
	}
	cout << endl;

	//const反向迭代器
	//string::const_reverse_iterator crit = s9.rbegin();
	auto crit = s9.rbegin();

	while (crit != s9.rend())
	{
		cout << *crit << " ";
		crit++;
	}
	cout << endl;



	//string的长度/数据个数
	cout << s2.size() << endl;
	cout << s2.length() << endl;

	cout << s2.max_size() << endl;//string最大能开多长
	cout << s2.capacity() << endl;//有效空间

	s1.reserve(100); //预留n个字节的空间    大小不包含'\0'

	s1.reserve(10);     //这里会不会再缩小空间不一定,但是对内容肯定不会有影响   
						//也就是不会缩的比size还小
						//vs这里不会缩小    linux里的g++会缩小
	cout << s1.empty() << endl;
	s1.shrink_to_fit();   //会把多余的内存还给系统



	return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;
class Solution
{
public:
    bool isLetter(char ch)
    {
        if (ch >= 'a' && ch <= 'z')
            return true;
        if (ch >= 'A' && ch <= 'Z')
            return true;
        return false;
    }
    string reverseOnlyLetters(string s)
    {
        int left = 0, right = s.size() - 1;
        while (left < right)
        {
            while (left < right && !isLetter(s[left]))
            {
                ++left;
            }
            while (left < right && !isLetter(s[right]))
            {
                --right;
            }
            swap(s[left++], s[right--]);
        }
        return s;
    }
    int firstUniqChar(string s) {
        int count[26] = { 0 };
        for (auto ch : s)
        {
            count[ch - 'a']++;
        }
        for (int i = 0; i < s.size(); ++i)
        {
            if (count[s[i] - 'a'] == 1)
                return i;
        }
        return -1;
    }
    string addStrings(string num1, string num2) {
        int end1 = num1.size() - 1, end2 = num2.size() - 1;
        int next = 0;
        string tmp;
        while (end1 >= 0 || end2 >= 0)
        {
            int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
            int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
            int ret = val1 + val2 + next;
            next = ret / 10;
            ret = ret % 10;
            tmp.insert(tmp.begin(), ret + '0');
        }
        if (next)
        {
            tmp.insert(tmp.begin(), '1');
        }
        return tmp;
    }
};
void test1()
{
    string s1 = "hello world";
    s1.push_back(' ');
    s1.push_back('x');//尾插字符

    s1.append("yyyy");//尾插字符串

    s1 += "bbb";//这样的尾插更好用
    cout << s1 << endl;

    s1.insert(0,"aa");//头插//在第n个位置插入

    s1.insert(0, 1, 'a');//插入一个需要这么写
    //s1.insert(0,'a');          //这样写不行,没有提供这种写法
    cout << s1 << endl;

    s1.erase(5, 1);//在第a个位置开始删除b个字符
    cout << s1 << endl;

    s1.erase(s1.begin());//头删
    cout << s1 << endl;

    s1.erase(s1.end()--);//尾删
    cout << s1 << endl;

    s1.erase(6);  //第n个后面全删
    cout << s1 << endl;

    s1.replace(4, 1, "&");  
    cout << s1 << endl;

    //把s2里的所有空格都换成%
    string s2 = "hello world 11 222 333 ";
    cout << s2 << endl;
    size_t pos = s2.find(' ');//在s2中找到空格的位置并返回其索引
    while (pos != string::npos)
    {
        s2.replace(pos, 1, "%%");
        pos = s2.find(' ',pos + 2);//后面可以加上寻找开始的位置
    }
    cout << s2 << endl;

    auto str1 = s2.c_str(); //输出s2字符串的指针,这是用来兼容c的
    //比如
    //string file;
    //cin >> file;
    //FILE* fout = fopen(file.c_str(), "r");
    //char ch=fgetc(fout);
    //while (ch != EOF)
    //{
    //    cout << ch;
    //    ch = fgetc(fout);
    //}
    //fclose(fout);

    string str3 = "hello world abc";
    size_t pos2 = str3.find(" ");   
    //size_t pos2 = str3.rfind(" ");  如果是找abc就可以这样

    string suffix ="1111111111111111";
    suffix = str3.substr(pos2);  //用s3从pos位置往后的内容覆盖suffix
    cout << suffix << endl;
}
int main()
{
	string s1 = "hello world";
	s1.resize(20,'x');  //会扩容,也会缩小  
	s1.resize(20);  //没有指定的时候会把开的空间设置成  '\0'


 /*   Solution sol;
    string test1 = "a-bC-dEf-ghIj";
    string res1 = sol.reverseOnlyLetters(test1);
    cout << "测试用例1:" << test1 << endl;
    cout << "预期结果:j-Ih-gfE-dCba-" << endl;
    cout << "实际结果:" << res1 << endl << endl;*/

    test1();


	return 0;
}

4.一些string类成员函数的实现:

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace hiro
{
	class string
	{
	public:
		string()
			:_str(new char[1]{'\0'})
			,_size(0)
			,_capacity(0)
		{ }
		string(const char* str)
		{
			_size = strlen(str);
			//capacity不包含 \0
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		size_t size()
		{
			return _size;
		}
		char* begin()
		{
			return _str;
		}
		char* end()
		{
			return _str + _size;
		}
		size_t size()const
		{
			return _size;
		}
		const char* begin()const
		{
			return _str;
		}
		const char* end()const
		{
			return _str + _size;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		const char& operator[](size_t pos)const
		{
			assert(pos < _size);
			return _str[pos];
		}

		~string()
		{
			free(_str);
			_size = _capacity = 0;
		}
		
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
	void test_string1();
}

3.vector

就是数组

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
#include<vector>
using namespace std;
vector<vector<int>> generate(int numRows) {   //创建一个杨辉三角
	vector<vector<int>> vv(numRows);
	for (int i = 0; i < vv.size(); ++i)
	{
		vv[i].resize(i + 1, 1);
	}
	for (int i = 2; i < vv.size(); ++i)
	{
		for (int j = 1; j < vv[i].size() - 1; ++j)
		{
			vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
		}
	}
	return vv;
}
void TestVectorExpand()
{
	size_t sz;
	vector<int> v;
	sz = v.capacity();
	cout << "making v grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		v.push_back(i);
		if (sz != v.capacity())
		{
			sz = v.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}
void vector_test1()
{
	vector<int> v1;  //顺序表的初始化
	vector<int> v2(10,1);      //给10个1
	vector<int> v3(++v2.begin(),--v2.end());   //用v2的数据来初始化
	for (size_t i = 0; i < v3.size(); i++)
	{
		cout << v3[i] << " ";
	}
	cout << endl;
	vector<int>::iterator it = v3.begin();
	while(it != v3.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	for (auto a : v3)
	{
		cout << a<<" ";
	}
	cout << endl;
}
void vector_test2()
{
	vector<int> v1(10, 1);
	v1.reserve(20);
	cout << v1.capacity() << endl;    
	cout << v1.size() << endl;
	v1.reserve(15);
	cout << v1.capacity() << endl;
	cout << v1.size() << endl;
	v1.reserve(5);
	cout << v1.capacity() << endl;    //不会缩容
	cout << v1.size() << endl;
	cout << endl;

	v1.resize(20);
	cout << v1.capacity() << endl;
	cout << v1.size() << endl;
	v1.resize(15);
	cout << v1.capacity() << endl;
	cout << v1.size() << endl;
	v1.resize(5);
	cout << v1.capacity() << endl;    //不会缩容但是会改变size 
	cout << v1.size() << endl;		//数据多的话会删除数据
}
void vector_test3()
{
	vector<int> v(10, 1);
	//v.push_back(5);
	//v.insert(v.begin(), 2);  //必须用迭代器的形式
	//for (auto e : v)
	//{
	//	cout << e << " ";
	//}
	//cout << endl;
	//v.insert(v.begin() + 3, 10);   //可以直接这样来在某个位置插入
	//for (auto e : v)
	//{
	//	cout << e << " ";
	//}
	//cout << endl;
	//vector<string> v2;       //还可以用vector存储其他的自定义类型
	//string s1("xxxx");
	//v2.push_back(s1);
	//v2.push_back("yyyy");
	//for (const auto& e : v)  //这里不加引用的话要多次拷贝构造
	//{
	//	cout << e << " ";
	//}
	//cout << endl;
	//cout << endl;
	vector<vector<int>> vv(5, v);  //相当于二维数组   一个10行5列的vector
	vv[1][2] = 3;
	vv.operator[](1).operator[](2) = 3;  //上面的实际上是这样的代码   
										 //operator传的是引用,这样才能对内容进行访问
	for (auto ee : vv)
	{
		for (auto e : ee)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	cout << endl;
	cout << endl;
}
int main()
{
	//TestVectorExpand();  //这里可以看出来vs下的vector是1.5倍扩容,和string一样
	//vector_test1();
	vector_test3();
	return 0;
}
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace hiro
{
	template <class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		const_iterator begin() const
		{
			return _start;
		}
		const_iterator end() const
		{
			return _finish;
		}
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t old_size = size();
				T* tmp = new T[n];
				memcpy(tmp, _start, size() * sizeof(T));
				delete[] _start;

				_start = tmp;
				_finish = tmp + old_size;
				_end_of_storage = tmp + n;
			}
		}
		size_t capacity()
		{
			return _end_of_storage - _start;
		}
		size_t size()
		{
			return _finish - _start;
		}
		size_t capacity()const
		{
			return _end_of_storage - _start;
		}
		size_t size()const
		{
			return _finish - _start;
		}
		void push_back(const T& x)
		{
			//扩容
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}
			*_finish = x;
			++_finish;
		}
		T& operator[](size_t i)
		{
			assert(i < size());
			return _start[i];
		}
		const T& operator[](size_t i) const
		{
			assert(i < size());
			return _start[i];
		}
		bool empty()
		{
			return _start == _finish;
		}
		void pop_back()
		{
			assert(!empty());
			--_finish;
		}
		iterator insert(iterator pos, const T& x)  //这里pos前面不能加&  传过来的是临时变量的时候,具有常性,就不能用了
		{
			//扩容
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				pos = _start + len;
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				end--;
			}
			*pos = x;
			++_finish;
			return pos;   
		}
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;

	};
	/*void Print_Vector(const vector<int>& v)
	{
		for (size_t i = 0; i < v.size(); i++)
		{
			cout << v[i] << " ";
		}
		cout << endl;
		vector<int>::const_iterator it = v.begin();
		while (it != v.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}*/
	template<class T>
	void Print_Vector(const vector<T>& v)
	{
		for (size_t i = 0; i < v.size(); i++)
		{
			cout << v[i] << " ";
		}
		cout << endl;

		//在没有实例化的类模板里取东西的时候
		//编译器不知道这里的const_iterator是类型还是静态成员变量
		//这里加上typename是在告诉编译器这里是一个类型
		//typename vector<T>::const_iterator it = v.begin();
		auto it = v.begin();
		while (it != v.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test1()
	{
		vector<int> v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);
		Print_Vector(v1);
		vector<double> v2;
		v2.push_back(1.1);
		v2.push_back(2.2);
		v2.push_back(3.3);
	
		Print_Vector(v2);
		v2.insert(v2.begin(), 10.2);
		Print_Vector(v2);
		int x;
		cin >> x;
		auto pos = find(v1.begin(), v1.end(), x);
		if (pos != v1.end())
		{
			//insert之后pos就失效了,不能使用
			pos=v1.insert(pos, 30);
			//*pos *= 10; 
			pos = v1.insert(pos, 100);	//前面的insert加上返回值之后就可以先更新以下pos再访问
			*(pos + 2) *= 10;
		}
		Print_Vector(v1);

	}
}

4.list

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<list>
#include<algorithm>
using namespace std;
void test1()
{
	list<int>lt;
	lt.push_back(1);
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(4);
	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(1);
	lt.push_back(3);
	list<int>::iterator it = lt.begin();
	while (it!=lt.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	for (auto i : lt)
	{
		cout << i << " ";
	}
	cout << endl;
	
	lt.sort();
	lt.unique();//去重  但是要求链表有序
	for (auto i : lt)
	{
		cout << i << " ";
	}
	cout << endl;
	lt.remove(1);  //删除某个值
	for (auto i : lt)
	{
		cout << i << " ";
	}
	cout << endl;
}
struct A
{
public:
	A(int a1=1,int a2=2)
		:_a1(a1)
		,_a2(a2)
	{ }
private:
	int _a1;
	int _a2;
};
void test2()
{
	list<A> lt;
	A aa1;
	lt.push_back(aa1);
	lt.push_back(A(3,3));
	//支持直接传构造A的参数
	lt.emplace_back(3, 3);  //emplace_back还可以这样赋值
}
int main()
{
	test1();
	return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
namespace hiro
{
	template <class T>
	struct list_node
	{
		list_node(const T& data=T())
			:_data(data)
			,_next(nullptr)
			,_prev(nullptr)
		{}
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;
	};
	template<class T>
	struct list_iterator
	{
		typedef list_node<T> Node;
		typedef list_iterator<T> Self;
		Node* _node;
		list_iterator(Node * node)
			:_node(node)
		{}
		T& operator*()
		{
			return _node->_data;
		}
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		bool operator!=(const Self& s)
		{
			return _node != s._node;
		}
	};
	template <class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		typedef list_iterator<T> iterator;
		list()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}
		iterator begin()
		{
			/*iterator it(_head->next);
			return it;*/
			return _head->_next;
		}
		iterator end()
		{
			/*iterator it(_head);
			return it;*/
			return _head;
		}
		/*void push_back(const T& x)
		{
			Node* newnode = new Node(x);
			Node* tail = _head->_prev;
			tail->_next = newnode;
			newnode->_next = _head;
			_head->_prev = newnode;
			newnode->_prev = tail;
			++_size;
		}*/
		void push_back(const T& x)
		{
			insert(end(), x);
		}
		void push_front(const T& x)
		{
			insert(begin(), x);
		}
		void insert(iterator pos,const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;

			Node* newnode = new Node(x);
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++size;
		}
		void erase(iterator pos)
		{
			assert(pos!=end())
			Node* next = pos._node->_next;
			Node* prev = pos._node->_prev;
			
			prev->_next = next;
			next->_prev = prev;
			delete pos._node;
			--size;
		}
		void pop_back()
		{
			erase(--end());
		}
		void pop_front()
		{
			erase(begin());
		}
		size_t size()
		{
			return _size;
		}
		bool empty()
		{
			return _size == 0;
		}
	private:
		Node* _head;
		size_t _size;
	}; 
	void test1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
		for (auto e : lt)
		{
			cout << e << " ";
		}
		cout << endl;
	}
}
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cassert>
using namespace std;
namespace hiro
{
	template <class T>
	struct list_node
	{
		list_node(const T& data = T())
			:_data(data)
			, _next(nullptr)
			, _prev(nullptr)
		{
		}
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;
	};
	template<class T,class REF,class PTR>
	struct list_iterator
	{
		typedef list_node<T> Node;
		typedef list_iterator<T,REF,PTR> Self;
		Node* _node;
		list_iterator(Node* node)
			:_node(node)
		{
		}
		REF operator*()
		{
			return _node->_data;
		}
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		Self& operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		Self& operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}
		bool operator!=(const Self& s)
		{
			return _node != s._node;
		}
		PTR operator->()
		{
			return &_node->_data;
		}
	};
	/*template<class T>
	struct const_list_iterator
	{
		typedef list_node<T> Node;
		typedef const_list_iterator<T> Self;
		Node* _node;
		const_list_iterator(Node* node)
			:_node(node)
		{}
		const T& operator*()const
		{
			return _node->_data;
		}
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		Self& operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		Self& operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}
		bool operator!=(const Self& s)const
		{
			return _node != s._node;
		}
		const T* operator->()const
		{
			return &_node->_data;
		}
	};*/
	template <class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		typedef list_iterator<T,T&,T*> iterator;
		typedef list_iterator<T,const T&,const T*> const_iterator;
		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}
		list()
		{
			empty_init();
		}
		list(const list<T>& lt)
		{
			empty_init();
			for (auto e : lt)
			{
				push_back(e);
			}
		}
		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}
		list<T>&operator=(list<T>& lt)
		{
			swap(lt);
			return *this;
		}
		iterator begin()
		{
			/*iterator it(_head->next);
			return it;*/
			return _head->_next;
		}
		const_iterator end()const
		{
			/*iterator it(_head);
			return it;*/
			return _head;
		}
		const_iterator begin()const
		{
			/*iterator it(_head->next);
			return it;*/
			return _head->_next;
		}
		iterator end()
		{
			/*iterator it(_head);
			return it;*/
			return _head;
		}
		/*void push_back(const T& x)
		{
			Node* newnode = new Node(x);
			Node* tail = _head->_prev;
			tail->_next = newnode;
			newnode->_next = _head;
			_head->_prev = newnode;
			newnode->_prev = tail;
			++_size;
		}*/
		void push_back(const T& x)
		{
			insert(end(), x);
		}
		void push_front(const T& x)
		{
			insert(begin(), x);
		}
		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;

			Node* newnode = new Node(x);
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
			return newnode;  //为了防止迭代器失效
		}
		iterator erase(iterator pos)
		{
			assert(pos != end());
			Node* next = pos._node->_next;
			Node* prev = pos._node->_prev;

			prev->_next = next;
			next->_prev = prev;
			delete pos._node;
			--_size;
			return next;//为了防止迭代器失效
		}
		void clear()
		{
			auto it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		void pop_back()
		{
			erase(--end());
		}
		void pop_front()
		{
			erase(begin());
		}
		size_t size()const
		{
			return _size;
		}
		bool empty()const
		{
			return _size == 0;
		}
		
	private:
		Node* _head;
		size_t _size;
	};
	struct AA
	{
		int _a1 = 1;
		int _a2 = 2;
	};
	ostream& operator<<(ostream& out, const AA& aa)
	{
		out << aa._a1 << " " << aa._a2;
		return out;
	}
	template<class Container>
	void Print_Container(const Container& v)
	{
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
		for (auto e : lt)
		{
			cout << e << " ";
		}
		cout << endl;
		list<AA> lta;
		lta.push_back(AA());
		lta.push_back(AA());
		lta.push_back(AA());
		lta.push_back(AA());
		list<AA>::iterator ita = lta.begin();
		while (ita != lta.end())
		{
			cout << ita->_a1/*这里其实是应该是两个->  省略了一个*/ <<" " << ita->_a2 << endl;
			//cout << ita.operator->()->_a1 << ita->_a2 << endl;
			ita++;
		}
		cout << endl;
		Print_Container(lta);
	}
}

5.stack 和queue

1.queue.h

#pragma once
#include<iostream>

namespace hiro
{
	template <class T, class Container = list<T>>
	class queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_front();
		}
		const T& front()const
		{
			return _con.front();
		}
		const T& back()const
		{
			return _con.back();
		}
		size_t size()const
		{
			return _con.size();
		}
		bool empty()const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

2.stack.h

#pragma once
#include<iostream>

namespace hiro
{
	template <class T,class Container = vector<T>>
	class stack
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_back();
		}
		const T& top()const
		{
			return _con.back();
		}
		size_t size()const
		{
			return _con.size();
		}
		bool empty()const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

3.priority_queue

#pragma once
#include <algorithm>
#include<vector>
namespace hiro
{
	template<class T,class Container=vector<T>>
	class priority_queue
	{
	public:
		void Swap(T& a, T& b)
		{
			T tmp = a;
			a = b;
			b = tmp;
		}
		void AdjustUp(int child)
		{
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				//默认大堆
				if (_con[parent] < _con[child])
				{
					Swap(_con[parent], _con[child]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
		void AdjustDown(int parent)
		{
			int child = parent * 2 + 1;
			while (child < _con.size())
			{
				if (child + 1 < _con.size() && _con[child] < _con[child + 1])
				{
					child++;
				}
				if (_con[parent] < _con[child])
				{
					Swap(_con[parent], _con[child]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}
		void push(const T& x )
		{
			_con.push_back(x);
			AdjustUp(_con.size()-1);
		}
		void pop()
		{
			std::swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			AdjustDown(0);
		}
		const T& top()const
		{
			return _con[0];
		}
		// 判空
		bool empty() const
		{
			return _con.empty();
		}

		// 元素个数
		size_t size() const
		{
			return _con.size();
		}
	private:
		Container _con;
	};
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值