第一章 基础知识
1. 命名空间
出现命令空间的原因是为了解决,变量、函数和类的名称出现命名冲突的问题,
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int rand = 0;
int main()
{
printf("%d\n", rand);
return 0;
}
- 这里的rand和C标准库的stdlib.h中的rand函数会发生命名冲突,rand重定义
解决方案:将变量添加到自定义的命令空间中即可
#include <iostream>
using namespace std;
namespace lyc {
int rand = 10;
}
int main()
{
printf("%d\n", lyc::rand);
return 0;
}
说明一下:
- :: 叫做域作用限定符
- 编译器一般先在局部域中找,再从全局域找,再从自定义域中找
命令空间嵌套问题
//命名空间的嵌套定义
namespace N1 //定义一个名为N1的命名空间
{
int a;
int b;
namespace N2 //嵌套定义另一个名为N2的命名空间
{
int c;
int d;
}
}
说明一下:
- 同一个工程中允许存在多个相同名称的命名空间,编译器最后会将其成员合成在同一个命名空间中
- 但是我们不能在相同名称的命名空间中定义两个相同名称的成员
2. 缺省参数
它的定义是这样说的:指在声明或定义函数时,为函数的参数指定一个默认值,在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参
全缺省参数
#include <iostream>
using namespace std;
void Print(int a = 10, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
int a = 0, b = 0, c = 0;
Print();
return 0;
}
半缺省参数
#include <iostream>
using namespace std;
void Print(int a, int b, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
int a = 0, b = 0, c = 0;
Print(a,b);
return 0;
}
说明一下:
- 半缺省参数必须从右往左依次给出,不能间隔着给
3. 函数重载
C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表必须不同
#include<iostream>
using namespace std;
double Add(double left, double right)
{
return left + right;
}
double Add(double right, double left)
{
return left + right;
}
int main()
{
cout << Add(1.1, 2.2) << endl;
return 0;
}
说明一下:
-
因为是同类型的,所以顺序改变是没有用的,编译器是不能区分的
再比如:
#include<iostream>
using namespace std;
short Add(short left, short right)
{
return left + right;
}
int Add(short left, short right)
{
return left + right;
}
int main() {
cout << Add(1, 2) << endl;
cout << Add(1, 2) << endl;
return 0;
}
说明一下:
-
返回值类型不同是不够成重载的,编译器是不能区分的
函数重载的优势
#include<iostream>
using namespace std;
//两个函数类型不同
//void Swapi(int* p1, int* p2)
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//void Swapd(double* p1, double* p2)
void Swap(double* p1, double* p2)
{
double tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int main()
{
int a = 1, b = 2;
double c = 1.1, d = 2.2;
Swap(&a, &b);
Swap(&c, &d);
cout << a;
cout << c;
return 0;
}
说明一下:
- 在C++中出现了函数重载使代码更加简洁,看上就像是在调用同一函数一样
- 而C语言是没有函数重载的概念,写起来就会麻烦点,代码也不简洁
3.1 理解C语言不支持函数重载
这一堆报错就是gcc编译器中的函数重定义
所以我们采用g++编译器,就发现没有问题
3.2 C++的函数名修饰规则
首先我们需要知道程序翻译的最后一个过程是链接
说明一下:
- 汇编阶段: 把每个源文件汇总出来的符号分配一个地址(若符号只是一个声明,则给其分配一个无意义的地址),然后分别生成一个符号表
- 链接期间: 会将每个源文件的符号表进行合并,若不同源文件的符号表中出现了相同的符号,则取合法的地址为合并后的地址
- 而上面的代码gcc编译器想要编译通过,只能改代码,改函数名,
- C语言没有函数名修饰规则,不支持函数重载
总结(简单来说)
C语言不能支持重载,是因为同名函数就没办法区分,而C++是通过函数修饰规则来区分的,只要函数的形参列表不同,修饰出来的名字就不一样,也就支持了重载
另外我们也理解了,为什么函数重载要求参数不同,与返回值没关系,因为函数名修饰规则修饰出来的函数名字与返回值无关
4. 引用
给变量取了一个别名,不开辟空间
#include<iostream>
using namespace std;
int main()
{
int a = 0;
int& b = a;
cout << &b << endl; // 取地址
cout << &a << endl; // 取地址
a++;
b++;
cout << b << endl;//2
cout << a << endl;//2
return 0;
}
- 这里的int & b = a;&不是取地址,而是C++的引用,注意和C语言区分
4.1 引用的三个特性
#include<iostream>
using namespace std;
int main()
{
int a = 1;
// int& b; // 1、引用在定义时必须初始化
int& c = a;
int& d = c;
int& b = a; // 2、一个变量可以有多个引用
++a;
cout << b << endl;
cout << a << endl;
int x = 10;
// 3、引用一旦引用一个实体,再不能引用其他实体
b = x; // 这里b不是x的别名,是把x赋值给a的别名b
cout << b << endl;
++x;
cout << x << endl;
return 0;
}
- 引用在定义时必须初始化,一个变量可以有多个引用,引用一旦引用实体就不能引用其他实体
4.2 使用场景
#include<iostream>
using namespace std;
int& Count1()
{
int n = 0;
n++;
return n;
}
int& Count2()
{
static int n = 0;
n++;
return n;
}
int main() {
int& ret1 = Count1();
printf("ret1第一次:%d\n", ret1);
printf("ret1第二次:%d\n", ret1);
int& ret2 = Count2();
printf("ret2第一次:%d\n", ret2);
printf("ret2第二次:%d\n", ret2);
return 0;
}
- Count1函数中的n是在栈区开辟空间的,而这里是传引用返回,返回的是n这个变量,而Count1函数栈帧被销毁之后,自然变量n就不在了,自然就是随机值
- 注意:这里ret1第一次打印是1,实属巧合,再打印第二次,结果是随机值(如果栈帧结束,系统会清理栈帧并置成随机值),因为printf函数也是会形成栈帧,就可能会把Count1函数之前形成栈帧的那块空间覆盖掉
- 被static修饰的变量是放在静态区的,Count2函数返回对象是出了作用域还存在,自然两次打印结果都是1
综上所述:出了作用域还在的——>传引用返回,出了作用域被销毁——>传值返回
4.3 常引用
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
cout << typeid(a).name() <<endl;//int
cout << typeid(b).name() << endl;//int
// 权限不能放大
const int c = 20;
int& d = c;//error
//const int& d = c;
// 权限可以缩小
int e = 30;
const int& f = e;
int ii = 1;
double dd = ii;
double& rdd = ii;//error
const double& rdd = ii;//正确
const int& x = 10;
return 0;
}
说明一下:
- 对于同类型的变量,被const修饰之后,由之前的可读可写变成了可读不可写
- 对于不同类型的变量,通常会发生隐式类型转换,并不会改变原变量类型,中间都会产生一个临时变量
- 临时变量具有常性,所以临时变量可读不可写,这也是变量ii不能赋值给dd,但却可以赋值给rdd的原因,也是出现了const int& x = 10;这样的代码的原因
- 不能将一个安全的类型交给一个不安全的类型,这是error
4.4 引用和指针的关系
从使用场景来说
- 引用和指针使用场景基本一样,但是链表的链式结构是引用无法代替的,只能使用指针
从语法特性来说
- 引用在定义时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,
- 而指针可以在任何时候指向任何一个同类型实体。
- 没有NULL引用,但有NULL指针。
- 在sizeof中的含义不同:引用的结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
- 引用进行自增操作就相当于实体增加1,
- 而指针进行自增操作是指针向后偏移一个类型的大小。
- 有多级指针,但是没有多级引用。
- 访问实体的方式不同,指针需要显示解引用,而引用是编译器自己处理。
- 引用比指针使用起来相对更安全
从底层来说
#include<iostream>
using namespace std;
int main()
{
// 语法的角度,ra没有开空间
int a = 10;
int& ra = a;
ra = 20;
// 语法的角度,pa开辟了4或8个字节的空间
int* pa = &a;
*pa = 20;
return 0;
}
- lea是一条取地址的汇编命令,在语法的角度,引用是没有开辟空间,但通过反汇编可以发现:引用就是指针,连汇编也一样
5. 内联函数
C++中引入内联函数主要是为了解决宏的问题或者说,优化C语言中的宏
5.1 宏的优缺点
优点: 代码维护性强,宏函数效率高(减少栈帧的建立)
缺点: 可读性差,没有类型安全检查,不方便调试(预处理就会被替换掉)
5.2 在反汇编中查看inline的作用
- release版本的优化太强,可能在反汇编中什么都看不见
- 如果要在Debug版本下,通过反汇编来查看inline的作用,需要进行上面的配置
#include<iostream>
using namespace std;
inline void Add(int a, int b)
{
int c = a + b;
printf("Add(%d, %d)->%d\n", a, b, c);
}
void Sub(int a, int b)
{
int c = a - b;
printf("Sub(%d,%d)->%d\n", a, b, c);
}
int main()
{
Add(1, 2);
Sub(1, 2);
return 0;
}
说明一下:
- 以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开
- 没有函数调用建立栈帧 的开销,内联函数提升程序运行的效率。
5.3 inline函数三大特性
inline是一种以空间换时间的做法,省去调用函数开销,所以代码很长,或者有递归的函数不适宜作为内联函数
inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内部实现代码指令长度比较长(10行左右,不同编译器不同)/递归等等,编译器优化时会忽略掉内联,这时inline将不起作用
inline不建议声明和定义分离,如果分离就会导致链接错误,因为inline被展开,就没有函数地址,链接就会找不到
6. auto关键字(C++11)
C++11 规定:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
6.1 auto正确推导场景
auto与指针和引用结合起来使用
#include<iostream>
using namespace std;
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
说明一下:
- 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
在同一行定义多个变量
#include<iostream>
using namespace std;
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
int main()
{
TestAuto();
return 0;
}
说明一下:
-
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对 第一个类型进行推导,然后用推导出来的类型定义其他变量
6.2 auto不能推导的场景
auto不能作为函数的参数
#include<iostream>
using namespace std;
// 此处代码编译失败,auto不能作为形参类型,
//因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{
//...
}
int main()
{
TestAuto(1);
return 0;
}
auto不能直接用来声明数组
#include<iostream>
using namespace std;
// 此处代码编译失败,auto不能作为形参类型,
//因为编译器无法对a的实际类型进行推导
void TestAuto()
{
int a[] = { 1,2,3 };
auto b[] = { 4,5,6 };
}
int main()
{
TestAuto();
return 0;
}
6.3 基于范围的for循环(C++11)
#include<iostream>
using namespace std;
void TestFor_CPP()
{
int array[] = { 1, 2, 3, 4, 5 };
for (auto& e : array)//这里只能用引用
e *= 2;
for (auto e : array)
cout << e << " ";
}
void TestFor_C()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
printf("%d ", array[i]);
}
int main()
{
TestFor_C();
cout << endl;
TestFor_CPP();
return 0;
}
说明一下:
- 迭代器的循环与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环
- for循环迭代也是有条件的:a. 范围必须确定,b. 迭代对象也必须实现++和--
7. 指针空值nullptr(C++11)
C语言中的NULL其实是一个关键字,在stddef.h中
- 可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量
- 其实在使用空值的指针时,都不可避免的会遇到一些麻烦
C++98中的指针空值 vs C++11中的指针空值
#include<iostream>
using namespace std;
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
int* p = NULL;
f(0);
f(NULL);
f(p);
// C++11 nullptr 关键字 替代NULL
f(nullptr);
int* ptr = nullptr;
return 0;
}
说明一下:
- 在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下,将其看成是一个整形常量
- 如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
- 而C++11中指针控制nullptr就是一个无类型的指针,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr
第二章 类和对象
1. 类的实例化
说明一下:
- 类不占用空间,在使用的时候才发生类的实例化,才开辟了空间
- 开辟了空间叫定义,没有开辟空间的叫声明
- static 修饰的声明,只在当前文件可见,链接时没有放进符号表,所以两次打印的地址不同
- extern 修饰的声明,在链接时会放进符号表,所以两次打印的地址相同2类对象模型
2. 类对象的存储方式
说明一下:
- C++类对象的存储方式是:只保存成员变量,成员函数存放在公共的代码段
- 编译链接时就根据函数名去公共代码区找到函数地址
3. 类的大小
#include<iostream>
using namespace std;
class A1
{
public:
void PrintA()
{
cout << _a << endl;
}
void func()
{
cout << "void A::func()" << endl;
}
private:
char _a;
int _i;
};
class A2 {
public:
void f2() {}
};
class A3{
};
int main()
{
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(A3) << endl;
return 0;
}
说明一下:
- 一个类的大小,实际就是该类中”成员变量”之和,这里和C语言一样也会发生内存对齐
- 空类的大小:编译器给了空类一个字节来唯一标识这个类的对象
4. 成员变量命名规则的建议
class Date
{
public:
void Init(int year)
{
_year = year;
}
private:
int _year;
};
驼峰法:
- 函数名、类名等所有单词首字母大写 DateMgr
- 变量首字母小写,后面单词首字母大写 dateMgr
- 成员变量,首单词前面加_ _dateMgr
5. this指针
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year; // 年 -> 声明
int _month; // 月
int _day; // 日
};
int main()
{
Date d1;
d1.Init(2022, 7, 17);
Date d2;
d2.Init(2022, 7, 18);
d1.Print();
d2.Print();
return 0;
}
Date类中有 Init 与 Print 两个成员函数,他们都是放在公共代码区中的,而函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,lnit这个函数如何知道应该对d1进行初始化,而不是给d2进行初始化?
C++中通过引入this指针解决该问题,即:
-
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,
-
让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问
-
只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成
5.1 显示this指针
这里我将编译器隐藏起来的this指针显示出来了,但这样编译器是会报错的
#include<iostream>
using namespace std;
class Date
{
public:
void Init(Date* const this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print(Date* const this)
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year; // 年 -> 声明
int _month; // 月
int _day; // 日
};
int main()
{
Date d1;
d1.Init(&d1,2022, 7, 17);
Date d2;
d2.Init(&d2,2022, 7, 18);
d1.Print();
d2.Print();
return 0;
}
-
注意:实参和形参位置不能显示传递和接收this指针,但是可以在成员函数内部使用this指针
5.2 this指针的特性
- this指针的类型:类类型* const,即成员函数中,不能给this指针赋值
- 只能在 “成员函数” 的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
- this指针是“成员函数”第一个隐含的指针形参,vs下面传递是通过ecx寄存器传递的,这样this访问变量的时候可以提高效率,不过这些优化取决于编译器
5.3 关于this指针的存储位置
栈区,因为this指针本质上是“成员函数”的形参
5.4 用this指针证明类对象的存储方式
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "Print()" << endl;
//cout << this << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print(); //正常运行
return 0;
}
说明一下:
- 这段代码不会发生崩溃,编译报错的情况,它是会正常运行的
- p->Print如果对p进行了解引用,就会报错,但这里是正常运行的,可见类对象的存储方式是:只保存成员变量,成员函数存放在公共的代码段
6. 类的6个默认成员函数
#include<iostream>
using namespace std;
class A
{
};
int main()
{
cout << sizeof(A);
return 0;
}
- 如果一个类中什么成员都没有,简称为空类
- 空类并不是真的什么都没有,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数
默认成员函数:用户没有显式实现,编译器自动生成的成员函数(默认成员函数)
6.0 编译器自动生成的默认成员函数
#include<iostream>
using namespace std;
typedef int DataType;
class Time
{
public:
/*Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}*/
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
说明一下:
- 在C++中规定默认生成构造函数
- a:内置类型成员不做处理
- b:自定义类型成员会去调用他的默认构造函数
- C++中的这个设计就会导致一个问题
- 比如说在这段代码中,生成d1的时候就会去调用它的默认构造函数,而_year和_month和_day都是内置类型,只有_t是自定义类型
- 对于_t这个对象又会去调用它的默认构造函数,_hour和 _minute和 _second都是内置类型,但是又会因为内置类型成员不做处理。导致最后调用了默认构造函数之后还是随机值
- 当然这条规则不仅适用在构造函数中,析构函数也同样适合,
- 1. 但如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数就可以,比如Date类
- 2. 但如果类中有资源申请时,一定要写,否则会造成资源泄漏,如 Stack 类。 malloc 开辟的空间
但之后C++对于上面的那个问题,打了一个补丁,如下图
- 但是给的默认值不是初始化,是给的缺省值
6.1 构造函数
构造函数的特点
特点:函数名与类名相同,无返回值的概念,对象实例化时编译器自动调用对应的构造函数,构造函数可以重载,不传参也可以调用
无参构造和带参构造
#include<iostream>
using namespace std;
typedef int DataType;
class Date
{
public:
Date()
{
_year = 2022;
_month = 7;
_day = 27;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date A;
A.Print();
return 0;
}
构造函数的冲突
如果我们这里把全缺省的构造,改成半缺省的构造,就会发生一个构造函数冲突的问题
#include<iostream>
using namespace std;
typedef int DataType;
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
说明一下:
- 这里编译不会通过的,无参的构造函数和半缺省/全缺省的构造函数,默认构造函数都叫构造函数,它们中只能出现其中的一个
关于无参/半缺省/全缺省的构造函数,使用时应该合理使用,否则编译器无法正确匹配,不要出现下面这个案例:没有合适的默认构造函数可用
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
理解构造函数内语句
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_year = year*2;
_month = month;
_month = month*2;
_day = day;
_day = day*2;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date A;
return 0;
}
说明一下:
- 构造函数体中的语句准确的来说应该叫:赋初值,而不能称作初始化
- 因为初始化只能初始化一次,而构造函数体内可以多次赋值
构造函数体内调用成员函数的构造函数
如果我们在一个类中的构造函数里面调用其他类中的构造函数时,就会发生一个新的问题
#include <iostream>
using namespace std;
class Time
{
public:
//Time(int hour = 0)
Time(int hour)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
// 要初始化_t 对象,必须通过初始化列表
Date(int year, int hour)
{
// 函数体内初始化
_year = year;
Time t(hour);
_t = t;//拷贝构造
}
private:
int _year;
Time _t;
};
int main()
{
Date d(2022, 1);
return 0;
}
说明一下:
- 这里想初始化Date里面的_t成员,直接用Time里的构造函数是有问题的
- 正确的做法是将Date的构造函数改造一下,使其通过构造函数初始化列表进行初始化
初始化列表
#include <iostream>
using namespace std;
class Time
{
public:
Time(int hour)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year, int hour, int& x)
:_year(year)
,_t(hour)
, _N(10)
, _ref(x)
{
// 函数体内初始化
_year = year;
_ref++;
}
private:
int _year;
Time _t;
const int _N;
int& _ref;
};
int main()
{
int y = 0;
Date d(2022, 1, y);
return 0;
}
类中包含以下成员,必须放在初始化列表位置进行初始化
比如说:a. 引用成员变量,b. const成员变量(它只能在定义的地方初始化,有且只有一次机会)
所以我推荐在写构造函数的时候,能用初始化列表就用初始化列表
初始化列表的初始顺序
类中构造函数初始化的顺序是:只与声明成员变量的顺序有关,与初始化列表的顺序无关
#include <iostream>
using namespace std;
class A
{
public:
// 初始化应该按声明顺序初始化
A(int a)
// 成员变量定义
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
// 成员变量声明
int _a2;
int _a1;
};
int main() {
A aa1(1);
aa1.Print();
return 0;
}
6.2 析构函数
析构函数特点
特点:a.析构函数名是在类名前加上字符 ~ ,b. 无参数无返回值类型,c. 一个类只能有一个析构函数且不能重载若未显式定义,系统会自动生成默认的析构函数,f. 对象生命周期结束时,C++编译系统系统自动调用析构函数
析构顺序
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
{
_a = a;
cout << "A(int a = 0)->" << _a << endl;
}
~A()
{
cout << "~A()->" << _a << endl;
}
private:
int _a;
};
A aa3(3);
void f()
{
static A aa0(0);
A aa1(1);
A aa2(2);
static A aa4(4);
}
// 构造顺序:3 0 1 2 4
// 析构顺序:~2 ~1 ~4 ~0 ~3
int main()
{
f();
//f();
return 0;
}
说明一下:
- 构造函数: 全局优先,依次构造
- 析构函数: 栈区优先,先进后出
如果对f()函数调用两次呢?
- 变量aa1和aa2是局部变量(存放在栈区中),出了函数的作用域就不存在了,生命周期也就没了
- 变量aa3和aa4被static修饰(存放在静态区),出了函数的的作用域依旧存在,
- 全局变量aa3就不用多说了
6.3 拷贝构造函数
拷贝构造函数的特点
特点:a. 拷贝构造函数是构造函数的一个重载形式,b. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,c. 使用传值方式编译器直接报错,因为会引发无穷递归调用
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
说明一下:
- 这种情况就叫做无穷拷贝,传值传参会发生无穷递归,形参是实参的临时拷贝
深浅拷贝问题
由于在C++中编译器自动生成的默认拷贝构造函数是浅拷贝,如果是程序员动态内存开辟的话,是会出现深浅拷贝的问题,需要我们手动实现拷贝构造函数
#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
//构造函数
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
//析构函数
~Stack()
{
if (_array) {
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
- 代码会直接崩溃,因为这段代码会对同一段空间直接析构2次
6.4 赋值重载函数
运算符重载到类外面
#include<iostream>
using namespace std;
// 全局的operator==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2) {
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
//cout << operator==(d1, d2) << endl;
cout << (d1 == d2) << endl;
}
int main()
{
Test();
return 0;
}
运算符重载到类里面
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return this->_year == d2._year
&& this->_month == d2._month
&& this->_day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
//cout << d1.operator==(d2) << endl;
cout << (d1 == d2) << endl;
}
int main()
{
Test();
return 0;
}
说明一下:
- C++默认的拷贝构造函数和赋值运算符重载(=)确实都是浅拷贝
- 将operator定义在类里面,就不用把类中的成员变量公开了,
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ? : . 这5个运算符不能重载。这个经常在笔试选择题中出现
赋值运算符重载格式
#include<iostream>
using namespace std;
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
cout << "Time& operator=(const Time& t)" << endl;
//防止自己跟自己赋值
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time a1;
Time a2 = a1;
Time a3 = a2 = a1;
return 0;
}
说明一下:
- 赋值重载函数应该返回一个类的对象(*this),由于这个对象出了作用域还存在,所以可以用传引用返回你
- 赋值运算符重载是需要支持连续赋值
- 当然这个成员函数也会出现,深浅拷贝的问题
6.5 取地址及const取地址操作符重载
#include<iostream>
using namespace std;
class Date
{
public:
// 全缺省的构造函数
Date(int year = 1,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date A;
const Date B;
cout << &A << endl;
cout << &B << endl;
return 0;
}
说明一下:
- 这两个默认成员函数一般不用重新定义 ,编译器默认会生成
-
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如 想让别人获取到指定的内容
7. 操作符前置++和后置++的重载
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator++()// 前置加加
{
_day += 1;
return *this;
}
Date operator++(int)// 后置加加
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date d1(2022, 1, 13);
d = d1++;
d = ++d1;
return 0;
}
- C++中为了解决前置++和后置++中函数名是相同的问题,对后置++默认多传一个参数(编译器做的),用int接收
- 有了上面的解决方法,前置++和后置++就构造了函数重载
8. const成员函数
#include<iostream>
using namespace std;
class A
{
public:
//void Print(const A* const this)
void Print() const
{
//_year = 1;
cout << _year << "/" << _month << "/" << _day << endl;
}
//void Print(A* const this)
void Print()
{
_year = 1024;
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 1; // 年
int _month = 1; // 月
int _day = 1; // 日
};
int main()
{
A d1;
const A d2;
d1.Print();
d2.Print();
return 0;
}
说明一下:
-
被const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针 ,表明在该成员函数中 不能对类的任何成员进行修改
-
被const修饰会改变权限,权限的访问是不能放大的
9. explict关键字:禁掉隐式类型转换
#include <iostream>
using namespace std;
class Date
{
public:
explicit Date(int year)
//Date(int year)
:_year(year)
{
cout << "Date(int year)" << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
};
int main()
{
Date d1(2022); // 直接调用构造
Date d2 = 2022; // 隐式类型转换:构造 + 拷贝构造 + 编译器优化 ->直接调用构造
const Date& d3 = 2022;
return 0;
}
说明一下:
- explicit可以禁掉构造函数的类型转换
10. 匿名对象
#include <iostream>
using namespace std;
//explicit关键字 + 匿名对象
class Date
{
public:
explicit Date(int year)
//Date(int year)
:_year(year)
{
cout << "Date(int year)" << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
};
class Solution {
public:
int Sum_Solution(int n) {
// ...
return 0;
}
};
int main()
{
// 匿名对象 -- 声明周期只有这一行
Date(2000);
// 匿名对象 一些使用场景
Solution slt;
slt.Sum_Solution(10);
Solution().Sum_Solution(10);//匿名对象的应用
return 0;
}
说明一下:
- explicit对匿名对象没用,匿名对象 -- 生命周期只有这一行
11. static成员
特性一
#include <iostream>
using namespace std;
class Test
{
private:
static int _n;
};
int main()
{
cout << sizeof(Test) << endl;
return 0;
}
说明一下:
- 静态成员为所有类对象共享,不属于某个具体的对象
- 静态成员的大小并不计入其总大小之和
特性二
class Test
{
private:
static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;
说明一下:
- 静态成员变量必须在类外定义,定义时不添加static关键字
- 这里静态成员变量_n虽然是私有,但是我们在类外突破类域直接对其进行了访问。这是一个特例,不受访问限定符的限制,否则就没办法对静态成员变量进行定义和初始化了
特性三
静态成员函数没有隐藏的this指针,不能访问任何非静态成员,含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量
class Test
{
public:
static void Fun()
{
cout << _a << endl; //error不能访问非静态成员
cout << _n << endl; //correct
}
private:
int _a; //非静态成员
static int _n; //静态成员
};
11.1 访问静态成员变量的方法
当静态成员变量为公有时,有以下几种访问方式
#include <iostream>
using namespace std;
class Test
{
public:
static int _n; //公有
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
Test test;
cout << test._n << endl; //1.通过类对象突破类域进行访问
cout << Test()._n << endl; //3.通过匿名对象突破类域进行访问
cout << Test::_n << endl; //2.通过类名突破类域进行访问
return 0;
}
说明一下:
- test._n // 通过类对象突破类域进行访问
- Test()._n // 通过匿名对象突破类域进行访问
- Test::_n // 通过类名突破类域进行访问
当静态成员变量为私有时,有以下几种访问方式
#include <iostream>
using namespace std;
class Test
{
public:
static int GetN()
{
return _n;
}
private:
static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
Test test;
cout << test.GetN() << endl; //1.通过对象调用成员函数进行访问
cout << Test().GetN() << endl; //2.通过匿名对象调用成员函数进行访问
cout << Test::GetN() << endl; //3.通过类名调用静态成员函数进行访问
return 0;
}
- 总之:都是调用静态成员函数访问静态成员变量
11.2 静态成员函数可以调用非静态成员函数吗
不可以,因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数
11.3 非静态成员函数可以调用静态成员函数吗
可以,因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制
11.4 在类中创建一个函数实现计算类实例化创建出了多少个对象
解决思路:在类中创建一个私有的静态成员变量,每次实例化的时候都让这个静态成员变量++,
再对外提供一个能得到这个静态成员的方法,
class A
{
public:
// 构造函数
A()
{
++_scount;
}
// 拷贝构造函数
A(const A& t) { ++_scount; }
// 静态成员函数 -- 没有this指针
static int GetCount()
{
//_a = 1;
return _scount;
}
private:
int _a;
// 静态成员变量,属于整个类,生命周期整个程序运行期间,存在静态区
static int _scount; // 声明
};
// 类外面定义初始化
int A::_scount = 0;
11.5 求1+2+3+…+n
要求: 求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)
#include <iostream>
#include <string>
using namespace std;
class Add {
public:
Add() {
_num++;
_ret += _num;
}
static int _num;
static int _ret;
};
int Add::_num = 0;
int Add::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
// 解决多个案例
Add::_num = 0;
Add::_ret = 0;
Add* p = new Add[n];
return Add::_ret;
}
};
int main()
{
cout << Solution().Sum_Solution(10) << endl;
return 0;
}
说明一下:
- 一个静态成员变量记录数字的变化,另一个静态成员变量累积数的和
- 类的对象实例化的时候(new的时候),是会调用构造函数的,则我们就可以在构造函数中实现这个算法
- 由于存在多个测试案列,所以静态成员变量是需要清零的
12. 友元函数
特性
- 友元函数可以访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同,但友元关系是单向的,不具有交换,不能传递
#include <iostream>
using namespace std;
class Date
{
// 友元: 使私有在类外面能够访问
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month; int _day;
};
ostream& operator<<(ostream& _cout, const Date& d) {
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d) {
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
说明一下:
- 我们要使用cin >> d时,就需要重载一个operator>>函数,但如果我们写在类里,那第一个参数本来应该是this指针,但却传了个ostream& cin,这就很尴尬了
- 但如果我们在类外面重载operator>>函数时,由于成员变量是私有时,类外不能访问,
但这个时候就可以用友元函数突破类域的限制
友元的缺陷
友元提供了一种突破封装的方式,虽然提供了便利,但是友元会增加耦合度,破坏了封装,所以友元不宜多用
13. 内部类
#include <iostream>
using namespace std;
class A
{
public:
// B定义在A的里面
// 1、受A的类域限制,访问限定符
// 2、B天生是A的友元,即B可以访问A中的成员变量
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a._h << endl;//OK -- 友元
}
private:
int _b;
};
private:
int _h;
static int k;
};
int A::k = 1;
int main()
{
cout << sizeof(A) << endl;
A a;
A::B b;
return 0;
}
说明一下:
- 内部类中的类天生是外面类的友元,可以访问外部类的成员变量
14. 编译器的优化
14.1 对连续表达式的优化
#include <iostream>
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
void f1(W w)
{
}
void f2(const W& w)
{
}
int main()
{
W w1;
f1(w1);
f2(w1);
cout << endl << endl;
f1(W()); // 构造+拷贝构造--编译器的优化--直接构造
return 0;
}
说明一下:
- 连续一个表达式步骤中,连续构造一般都会优化
- 构造+拷贝构造--编译器的优化--直接构造
14.2 对返回值的优化
#include <iostream>
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
W f(W u)
{
W v(u);
W w = v;
return w;
}
int main()
{
W x;
W y = f(x); // 1次构造 4次拷贝
return 0;
}
说明一下:
- 函数传值返回是要借助寄存器的,换句话说也是需要调用拷贝构造的
- 但由于编译器存在优化,它不借助中间寄存器,构造函数就会少一次,
- 而linux中的g++优化更大,比vs2019都少一次
第三章 内存管理 && 泛型编程
1. C++内存管理方式
1.1 管理内置类型
#include <iostream>
using namespace std;
void Test1()
{
int* p1 = (int*)malloc(sizeof(int));
free(p1);
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 10);
// 这里不需要free(p2),因为p3是在p2的空间上reallocd的
free(p3);
}
void Test2()
{
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 申请5个int的数组
int* p3 = new int[5];
// 申请1个int对象,初始化为5
int* p4 = new int(5);
// 申请5个int的数组,初始化为1,2,3,4,5
int* p5 = new int[5] {1, 2, 3, 4, 5};
free(p1);
delete p2;
delete[] p3;
delete p4;
delete[] p5;
}
int main()
{
Test1();
Test2();
return 0;
}
说明一下:
- C++11才支持new[] 用{}初始化 ,C++98不支持
- 使用new申请的对象,自然也该用delete进行释放,一段对象,使用delete []释放
- 针对内置类型,new/delete跟malloc/free没有本质的区别,只有用法的区别,
- 从写法上来看,new/delete用法简化的代码的书写
1.2 管理自定义类型
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
// malloc和free动态开辟内置类型的空间
A* p1 = (A*)malloc(sizeof(A));
if (p1 == NULL)
{
perror("malloc fail");
return 0;
}
free(p1);
cout << "-------------" << endl;
// new和delete动态开辟类置类型的空间
//A* p2 = new A;
A* p2 = new A(10);
delete p2;
cout << endl << endl;
A* p3 = new A[2];
delete[] p3;
return 0;
}
说明一下:
- new和delete对自定义类型,是会调用构造和析构进行初始化和清理的
- new申请空间,是不需要检查返回值的,申请失败会直接抛出异常
2. operator new与operator delete函数
首先说明这两个不是运算符重载,它们是两个全局函数,一个申请空间,一个释放空间
说明一下:
- 从底层上看,一个new是通过malloc申请的,另一个delete是通过free释放的,但是做了封装
3. malloc/free和new/delete的区别
相同点:都是从堆上申请空间,并且需要用户手动释放
不同点:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new申请的空间会初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
- malloc的返回值是void*,在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请失败时,返回的是NULL,因此使用时必须判空,new不需要判空,但是new需要捕获异常
4. 定位new和表达式
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象,简单来说就是:手动调用构造函数
使用场景
定位new表达式在实际中一般是配合内存池使用,因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,就需要使用定位new表达式进行显示调用构造函数进行初始化
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0) //构造函数
:_a(a)
{}
~A() //析构函数
{}
private:
int _a;
};
int main()
{
//new(place_address)type 形式
A* p1 = (A*)malloc(sizeof(A));
new(p1)A;
//new(place_address)type(initializer-list) 形式
A* p2 = (A*)malloc(sizeof(A));
new(p2)A(2021);
//析构函数也可以显示调用
p1->~A();
p2->~A();
return 0;
}
说明一下:
- 定位new的格式为:new(对象)类型
- 在未使用定位new表达式进行显示调用构造函数进行初始化之前,malloc申请的空间还不能算是一个对象,它只不过是与A对象大小相同的一块空间,因为构造函数还没有执行
5. 内存泄漏问题
内存泄漏是因为程序员疏忽或错误造成程序未能释放已经不再使用的内存的情况
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记住匹配的去释放
- 采用RAII思想或者智能指针来管理资源
- 有些公司内部规范使用内部实现的私有内存管理库,该库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测
解决分类:a.事前预防型 : 智能指针,b.事后查错型。如泄漏检测工具
6. 泛型编程-函数/类模板
说明一下:
- template<typename 名字>,typename后面类型名字是随便取的,Ty、K、V,一般是大写字母或者单词首字母大写,这里的T 代表是一个模板类型(虚拟类型)
- 这里的typename也可以改成class,在这里是没有区别的,但是不能使用struct代替class
6.1 隐式实例化和显式实例化
#include <iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
//Add(1.1, 2); // 推演实例化矛盾,报错
// 编译器自动推演,隐式实例化
cout << Add(1, 2) << endl;
cout << Add((int)1.1, 2) << endl;
cout << Add(1.1, (double)2) << endl;
// 显示实例化
cout << Add<int>(1.1, 2) << endl;
cout << Add<double>(1.1, 2) << endl;
return 0;
}
说明一下:
- 类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错
- 隐式实例化:让编译器根据实参推演模板参数的实际类型
-
显式实例化:在函数名后的<>中指定模板参数的实际类型
必须显示实例化的情况
当这个函数里面需要通过类型,进行某种运算,某种解析时,就必须显示调用函数模板
比如下面这种情况:
#include <iostream>
using namespace std;
template<class T>
T* Func(int n)
{
T* a = new T[n];
return a;
}
int main()
{
// 必须显示实例化才能调用
Func<int>(10);
return 0;
}
说明一下:
- 这里的Func模板,必须用显示实例化才能调用
6.1 模板参数的匹配原则
#include <iostream>
using namespace std;
// 专门处理int的加法函数
int Add(int left, int right) {
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right) {
return left + right;
}
int main()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
return 0;
}
说明一下:
-
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
-
对于非模板函数和同名函数模板,如果其他条件都相同,会优先调用非模板函数
-
模板函数不允许自动类型转换auto,但普通函数可以进行自动类型转换
6.3 非类型模板参数
#include <iostream>
using namespace std;
//非类型模板参数 -- 常量
namespace lyc {
template<class T, size_t N = 10>
class array
{
private:
T _a[N];
};
}
int main()
{
lyc::array<int> a0;
lyc::array<int, 100> a1; // 100
lyc::array<double, 1000> a2; // 1000
return 0;
}
说明一下:
- 上面代码中的N就是一个非类型模板参数
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的,非类型的模板参数必须在编译期就能确认结果
6.4 函数模板的特化
函数模板的特化 就是对在函数定义的函数名后面指定类型,指定模板参数的类型
#include <iostream>
using namespace std;
struct Date
{
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
bool operator>(const Date& d) const
{
if ((_year > d._year)
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day > d._day))
{
return true;
}
else
{
return false;
}
}
int _year;
int _month;
int _day;
};
// 函数模板 -- 参数匹配
template<class T>
bool Greater(T left, T right)
{
return left > right;
}
// 特化--针对某些类型进行特殊化处理
template<>
bool Greater<Date*>(Date* left, Date* right)
{
return *left > *right;
}
int main()
{
cout << Greater(1, 2) << endl; // 可以比较,结果正确
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Greater(d1, d2) << endl; // 可以比较,结果正确
Date* p1 = &d1;
Date* p2 = &d2;
cout << Greater(p1, p2) << endl; // 可以比较,结果错误
return 0;
}
说明一下:
- 如果没有函数模板的特化,第三个Greater(p1,p2)比较的就是日期的地址,而不是日期
6.5 类模板的特化
类模板的特化 就是对在类定义的类名后面指定类型,指定类模板参数的类型
#include <iostream>
#include <queue>
using namespace std;
struct Date
{
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d) const
{
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day))
{
return true;
}
else
{
return false;
}
}
int _year;
int _month;
int _day;
};
// 类模板
namespace bit
{
template<class T>
struct less
{
bool operator()(const T& x1, const T& x2) const
{
return x1 < x2;
}
};
//特化
template<>
struct less<Date*>
{
bool operator()(Date* x1, Date* x2) const
{
return *x1 < *x2;
}
};
}
int main()
{
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
bit::less<Date> lessFunc1;
cout << lessFunc1(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
bit::less<Date*> lessFunc2;
cout << lessFunc2(p1, p2) << endl;
return 0;
}
6.6 全特化
全特化即是将模板参数列表中所有的参数都确定特化
6.7 偏特化
偏特化即是将模板参数列表中一部分参数确定特化
6.8 模板分离编译问题
模板分离编译报错的原因:
- 在类里面定义的函数,编译器默认会把它当作内联函数,在vector<int>实例化的时候, 这些成员函数也会实例化,直接就有定义,编译阶段就确定地址了,
- push_back和insert这两个函数在.h中只有声明,没有定义,那么地址就只能在链接阶段中去找了
但此时在链接阶段是找不到的,会报错,因为
- 在模板定义的.cpp文件中,包含了模板的声明.h,在.cpp中就会展开.h
- 但是.cpp(实现)中的T无法确定,就不能实例化,
- 那么push_back,insert就没有进入符号表,所以在链接阶段就会找不到,然后报错
第四章 简单介绍STL容器
1. string接口
1.1 reserve开闭空间
#include <iostream>
#include <string>
using namespace std;
void test_string7()
{
string s;
// reverse 逆置
s.reserve(100); //保留 开空间
//s.resize(1000, 'x'); // 开空间 + 初始化
size_t sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
cout << "making s grow:\n";
for (int i = 0; i < 1000; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
int main()
{
test_string7();
return 0;
}
说明一下:
- reserve(100)开闭100个字节的空间,编译器会比我们想要开闭的空间更多一些
- 不同的平台,扩容机制也不同,有的是扩2倍,有的是扩1.5倍,
1.2 c_str转换为C风格字符串
#include <iostream>
#include <assert.h>
using namespace std;
void test_string10()
{
string filename("test.cpp");
cout << filename << endl;
cout << filename.c_str() << endl;
filename += '\0';
filename += "string.cpp";
cout << filename << endl; // string 对象size为准
cout << filename.c_str() << endl; // 常量字符串对象\0
cout << filename.size() << endl;// 如果有\0,会算\0
cout << filename.length() << endl;// 不算f\0
string copy = filename;
cout << copy << endl << endl;
}
int main()
{
test_string10();
return 0;
}
说明一下:
- string中的c_str会把string变成以‘\0’结尾的字符串
- cout << filename << endl; // string 对象size为准
- cout << filename.c_str() << endl; // 常量字符串对象\0
1.3 find查找 && substr分割
#include <iostream>
#include <assert.h>
using namespace std;
void test_string10()
{
string filename("test.cpp");
cout << filename << endl;
cout << filename.c_str() << endl;
filename += '\0';
filename += "string.cpp";
cout << filename << endl; // string 对象size为准
cout << filename.c_str() << endl; // 常量字符串对象\0
cout << filename.size() << endl;// 如果有\0,会算\0
cout << filename.length() << endl;// 不算f\0
string copy = filename;
cout << copy << endl << endl;
}
int main()
{
test_string10();
return 0;
}
说明一下:
- find 和 substr都是左闭右开的
- find 和 rfind都是从左向右看下标
1.4 append尾插
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("I");
string s2(" like");
//append(string)完成两个string对象的拼接
s1.append(s2); //I like
//append(str)完成string对象和字符串str的拼接
s1.append(" C++"); //I like C++
//append(n, char)将n个字符char拼接到string对象后面
s1.append(3, '!'); //I like C++!!!
cout << s1 << endl; //I like C++!!!
return 0;
}
说明一下:
- append函数相当于尾插
1.5 to_string转为字符串 && stoi && stod
#include <iostream>
#include <string>
using namespace std;
void test_string12()
{
int ival;
double dval;
cin >> ival >> dval;
string istr = to_string(ival);
string dstr = to_string(dval);
cout << istr << endl;
cout << dstr << endl;
istr = "9999";
dstr = "9999.99";
ival = stoi(istr);
dval = stod(dstr);
cout << typeid(ival).name() << endl;
cout << typeid(dval).name() << endl;
}
int main()
{
test_string12();
return 0;
}
说明一下:
- to_string:其他类型转换成字符
- stoi:字符转化成整型
- stod:字符转化成浮点型
1.6 clear清空字符串
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("CSDN");
//clear()删除对象的内容,该对象将变为空字符串
s.clear();
cout << s << endl; //空字符串
return 0;
}
说明一下:
- 使string变成空字符串
2. vector接口
2.1 reserve开辟空间 && resize调整大小
#include <iostream>
#include <vector>
namespace std
{
void TestVectorExpand()
{
size_t sz;
vector<int> v;
//v.resize(100);
v.reserve(100);
sz = v.capacity();
cout << "making v grow:\n";
for (int i = 0; i < 1000; ++i)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
void test_vector3()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
cout << v1.max_size() << endl;
TestVectorExpand();
}
}
int main()
{
std::test_vector3();
return 0;
}
说明一下:
- 空间只能变大或者不变
- reserve开空间,它是期待预留空间,
- 如果原来空间大,期待预留空间小,则空间不变
- 如果原来空间小,期待预留空间大,则空间变大
- resize开空间和初始化,它是调整元素个数,
- 如果调整之后元素个数比空间小,则空间不变,size会变小
- 如果调整之后元素个数比空间大,则空间变大,size会变大
- max_size返回的是容纳最大元素的数
2.2 insert插入 && erase删除
插入,删除,扩容都有可能会触发,迭代器失效问题
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
// 在所有的偶数前面插入这个偶数的2倍
auto it = v1.begin();
while (it != v1.end())
{
if (*it % 2 == 0)
{
it = v1.insert(it, *it * 2);
}
it++;
}
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
return 0;
}
在实际过程中产生迭代器失效的原因:
-
insert插入返回的迭代器是新的插入的迭代器
-
pos指向位置已经不再是原来的值了(因为数据挪动)
- 扩容导致野指针问题
解决方案也很简单:只要让它插入值之后,多走一步就行了
总之: insert/erase pos位置,不要再次直接访问pos,可能出现各种出乎意料的结果,这就是所谓的迭代器失效
3. list接口
3.1 sort排序
std::list
是双向链表,不能随机访问,所以不能用 std::sort()
。
std::sort()
要求随机访问迭代器(如 vector 提供的)。
std::list
提供自己的成员函数 sort()
,性能等效于归并排序(merge sort),非常适合链表
#include <iostream>
#include <list>
#include <vector>
#include <algorithm>
#include <time.h>
using namespace std;
void test_op()
{
srand(time(0));
const int N = 10000000;
vector<int> v;
v.reserve(N);
list<int> lt1;
list<int> lt2;
for (int i = 0; i < N; ++i)
{
auto e = rand();
lt1.push_back(e);
lt2.push_back(e);
}
// 拷贝到vector排序,排完以后再拷贝回来
int begin1 = clock();
for (auto e : lt1)
{
v.push_back(e);
}
sort(v.begin(), v.end());// 调用库里面的sort
size_t i = 0;
for (auto& e : lt1)
{
e = v[i++];// 拷贝回去
}
int end1 = clock();
int begin2 = clock();
// sort(lt.begin(), lt.end()); error
lt2.sort();// 调用list自己的sort
int end2 = clock();
printf("copy vector sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);
}
int main()
{
test_op();
return 0;
}
- 其实list中的sort排序是有点慢的
4. 理解 stack && queue
说明一下:
- 栈和队列的模拟实现都是通过复用来实现的
- 栈的适配器Container可以是vector或者是list,队列的适配器Container可以是list
- 库里面提供了适配器,使得栈和队列的底层有了更大的发挥空间,更像泛型编程
5. 关于适配器Container的缺省参数queue
说明一下:
- deque就像是list和vector的合体,不仅支持头删头插,尾删尾删,还支持随机访问
- 在deque的底层中,是先开一段空间,空间不够再开一段空间(这里没有扩容)
优点:
- a.头尾插入删除, b.随机访问
缺点:
- operator[]计算稍显复杂,大量使用,将会导致性能下降(与vector的[]相比)
- 中间插入删除效率不高,迭代器会很复杂
头尾的插入和删除都非常适合,相比vector和list而言,所以很适合去做stack和queue的默认适配器
6. priority_queue队列
priority_queue<int, vector<int>, less<int>> q1;
- priority_queue的底层是用数据结构中的堆实现,
- 不传第三个模板参数(仿函数)默认是大堆,less<int>
6.1 堆的向上调整算法
#include <iostream>
#include <functional>
#include <queue>
using namespace std;
int main()
{
priority_queue<int> q;
q.push(3);
q.push(6);
q.push(0);
q.push(2);
q.push(9);
q.push(8);
q.push(1);
while (!q.empty())
{
cout << q.top() << " ";
q.pop();
}
cout << endl; //9 8 6 3 2 1 0
return 0;
}
6.2 堆的向下调整算法
以大堆为例,使用堆的向下调整算法有一个前提,就是待向下调整的结点的左子树和右子树必须都为大堆
//堆的向下调整(大堆)
void AdjustDown(vector<int>& v, int n, int parent)
{
//child记录左右孩子中值较大的孩子的下标
int child = 2 * parent + 1;//先默认其左孩子的值较大
while (child < n)
{
if (child + 1 < n&&v[child] < v[child + 1])//右孩子存在并且右孩子比左孩子还大
{
child++;//较大的孩子改为右孩子
}
if (v[parent] < v[child])//左右孩子中较大孩子的值比父结点还大
{
//将父结点与较小的子结点交换
swap(v[child], v[parent]);
//继续向下进行调整
parent = child;
child = 2 * parent + 1;
}
else//已成堆
{
break;
}
}
}
7. 仿函数-不是函数
简单来说就是一个重载了()的类
#include <iostream>
#include <algorithm>
#include <queue>
#include <functional>
using namespace std;
/*仿函数/函数对象 -- 类,重载operator()
类对象可以像函数一样去使用*/
namespace bit
{
template<class T>
class less
{
public:
bool operator()(const T& l, const T& r) const
{
return l < r;
}
};
template<class T>
class greater
{
public:
bool operator()(const T& l, const T& r) const
{
return l > r;
}
};
}
int main()
{
bit::less<int> lsFunc;
cout << lsFunc(1, 2) << endl;
// 等价于下面
//cout << lsFunc.operator()(1, 2) << endl;
bit::greater<int> gtFunc;
cout << gtFunc(1, 2) << endl;
// 等价于下面
//cout << lsFunc.operator()(1, 2) << endl;
return 0;
}
8. 比较sort的仿函数和priority_queue的仿函数
一句话:sort传的是对象(匿名对象),prioriyt_queue传的是类型
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
int main()
{
int arr[] = { 0,1,2,3,4,5,6 };
vector<int> v(arr,arr+sizeof(arr)/sizeof(arr[0]));
sort(v.begin(), v.end(), greater<int>());// 不传第三个参数默认排升序
// sort(v.begin(), v.end());// 不传第三个参数默认排升序
for (int i = 0; i < v.size(); i++)
{
cout << v[i] << " ";
}
cout << endl;
priority_queue<int,vector<int>,greater<int>> pq;// 不传第三个参数模板参数默认排大堆
pq.push(4);
pq.push(3);
pq.push(2);
pq.push(1);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
return 0;
}
9. set && map
9.1 键值对->pair类型
键值对是用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息
template <class T1,class T2>
struct pair
{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair()
:first(T1())
,second(T2())
{}
pair(const T1& a, const T2& b) :first(a), second(b)
{}
};
说明一下:
- pair用了两个模板参数,set和map中某些函数的返回值是pair
9.2 lower_bound && upper_bound
#include <iostream>
//#include <functional>
#include <map>
#include <set>
#include <string>
using namespace std;
void test_set2()
{
std::set<int> myset;
std::set<int>::iterator itlow, itup;
for (int i = 1; i < 10; i++)
myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90
itlow = myset.lower_bound(25); // >= val
itup = myset.upper_bound(60); // > val //
cout << "[" << *itlow << "," << *itup << ")" << endl;
// 删掉一段迭代器区间(左闭右开)
myset.erase(itlow, itup); // 10 20 70 80 90
std::cout << "myset contains:";
for (std::set<int>::iterator it = myset.begin(); it != myset.end(); ++it)
std::cout << ' ' << *it;
std::cout << '\n';
}
int main() {
test_set2();
return 0;
}
说明一下:
- 一个迭代器的区间通常是左闭右开的
- lower_bound(val)返回这个set中第一个>=val值的跌迭代器
- upper_bound(val)返回这个set中第一个>val值的跌迭代器
9.3 equal_range返回范围边界
#include <iostream>
//#include <functional>
#include <map>
#include <set>
#include <string>
using namespace std;
void test_set2()
{
std::set<int> myset;
for (int i = 1; i <= 5; i++) {
myset.insert(i * 10); // myset: 10 20 30 40 50
}
//std::pair<std::set<int>::const_iterator, std::set<int>::const_iterator> ret;
pair<set<int>::const_iterator, set<int>::const_iterator> ret;
ret = myset.equal_range(40); // xx <= val < yy
std::cout << "the lower bound points to: " << *ret.first << '\n';
std::cout << "the upper bound points to: " << *ret.second << '\n';
}
int main() {
test_set2();
return 0;
}
说明一下:
- equal函数返回的一个pair类型,其中pair中存储的都是迭代器
10. multiset->允许出现重复节点
#include <iostream>
//#include <functional>
#include <map>
#include <set>
#include <string>
using namespace std;
void test_set3()
{
int a[] = { 3,1, 2, 1, 6, 3, 8,3, 5,3 };
multiset<int> s(a, a + sizeof(a) / sizeof(int));
// 排序
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
cout << s.count(3) << endl;
// find时,如果有多个值,返回中序的第一个
auto pos = s.find(3);
while (pos != s.end())
{
cout << *pos << " ";
++pos;
}
cout << endl;
// 删除所有的3
s.erase(3);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
pos = s.find(1);
if (pos != s.end())
{
s.erase(pos);
}
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
int main() {
test_set3();
return 0;
}
说明一下:
- multiset和set类似,本质也是一颗二插搜索树,
- 不同点在:这棵树允许出现重复节点
- multiset的find函数,如果有多个值,返回中序的第一个
- multiset的erase函数,如果删除的是val,那么删除这棵二叉树中所有的val
11. multimap
- multimap除了允许出现重复键值对之外,其他的和map相同
- 由于multimap容器允许键值冗余,调用[ ]运算符重载函数时,应该返回键值为key的哪一个元素的value的引用存在歧义,因此在multimap容器当中没有实现[ ]运算符重载函数
第五章 继承
1. 继承基类成员访问方式的变化
说明一下:
-
基类 private 成员在派生类中无论以什么方式继承都是不可见的, 只有基类自己能够访问
-
基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。 取两者权限小的那个
-
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。 -
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承
2. 基类和派生类对象赋值转换
#include <iostream>
#include <string>
using namespace std;
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student sobj;
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
//sobj = (Student)pobj; // error
return 0;
}
说明一下:
- 子类对象可以赋值给父类对象/指针/引用
- 这里虽然是不同类型,但是它们之间也不是隐式类型转换
- 这里算是一个特殊支持,语法天然支持的
- 这种操作也被称之为切片操作
3. 重定义/隐藏
#include <iostream>
using namespace std;
class Person
{
public:
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 学号:" << _num << endl; // 999
cout << " 身份证号:" << Person::_num << endl; // 111
}
//protected:
int _num = 999; // 学号
};
int main()
{
Student s;
cout << s._num << endl;
cout << s.Person::_num << endl;
return 0;
}
说明一下:
-
在继承体系中基类和派生类都有独立的作用域。
-
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫 重定义 (在子类成员函数中,可以使用 基类::基类成员 显示访问) -
在实际中在继承体系里面最好不要定义同名的成员
4. 继承与友元
#include <iostream>
#include <string>
using namespace std;
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
//friend void Display(const Person& p, const Student& s);
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
-
这个函数虽然是父类的友元,但是友元关系不能继承,无法访问子类的私有和保护成员
5. 继承与静态成员
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person() { ++_count; }
//protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;// 全局变量要在类外定义
class Student : public Person
{
protected:
int _stuNum; // 学号
};
int main()
{
Person p;
Student s;
p._name = "张三";
cout << s._name << endl;
cout << Student::_count << endl;
++Person::_count;
cout << Student::_count << endl;
cout << &Person::_count << endl;
cout << &Student::_count << endl;
return 0;
}
说明一下:
- 父类的static静态成员和子类继承父类的static静态成员,是一样的,无论派生出多少个子类,都只有一个static成员实例
6. 菱形继承
- 菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份
- 补充除了上面的那种继承关系,这种继承也叫做菱形继承
6.1 案例
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
int _a;
};
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
//d._a = 105;//_a不明确,可能是B的,也可能是C的
d._b = 3;
d._c = 4;
d._d = 5;
B b = d;
B* pb = &d;
C c = d;
C* pc = &d;
/*cout << "pb->" << pb << endl;
cout << "pc->" << pc << endl;*/
cout << &d.B::_a << endl;
cout << &d.C::_a << endl;
return 0;
}
- B类中的_a和C类中的_a是不一样,都开辟了不同空间来存储
6.2 菱形虚拟继承
为了解决菱形继承的二义性和数据冗余问题,出现了虚拟继承
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
int _a;
};
//class B : public A
class B : virtual public A
{
public:
int _b;
};
//class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._a = 105;//这就是A中的_a
d._b = 3;
d._c = 4;
d._d = 5;
B b = d;
B* pb = &d;
C c = d;
C* pc = &d;
/*cout << "pb->" << pb << endl;
cout << "pc->" << pc << endl;*/
cout << &d.B::_a << endl;
cout << &d.C::_a << endl;
return 0;
}
说明一下:
- virtual使一个继承变成虚拟继承
- B类中的_a和C类中的_a是一样,_a只开辟了一段公共空间来存储
- sizeof(B)大小是12,它里面存了一个_a,_b,还有一个指针
6.3 菱形虚拟继承原理
说明一下:
- 其中D类对象当中的_a成员被放到了最后,而在原来存放两个_a成员的位置变成了两个指针,这两个指针叫虚基表指针,它们分别指向一个虚基表。
- 虚基表中包含两个数据,
第一个数据是为多态的虚表预留的存偏移量的位置(这里我们不必关心),
第二个数据就是当前类对象位置距离公共虚基类的偏移量。也就是说,这两个指针经过一系列的计算,最终都可以找到成员_a
7. 继承的总结与反思
继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象
- 人和学生,植物和玫瑰花,就是is-a的关系
组合是一种 has-a 的关系。假设B组合了A,每个B对象中都有一个A对象
- 轮胎和车,脑袋和眼睛,就是has-a的关系
即能用 public继承 也能用 组合 的的关系
- 锅和铁的关系,vector/list/deque和stack的关系,属于两种都能用,但是 优先使用对象组合,而不是类继承
第六章 多态
多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果,
比如买车票这件事:普通人买票->正常买票,学生买票->半价买票,军人买票->优先买票
补充一下:发生多态,调用函数时,与类型无关,指向谁调用谁
1. 虚函数的理解
class Person
{
public:
//被virtual修饰的类成员函数
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
说明一下:
-
只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual。
- 虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。虚函数这里的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性
2. 虚函数重写的两个条件
条件一:这个函数是虚函数
条件二:三同:函数名,参数,返回值都要相同
//父类
class Person
{
public:
//父类的虚函数
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
//子类
class Student : public Person
{
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
//子类
class Soldier : public Person
{
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
{
cout << "优先-买票" << endl;
}
};
满足虚函数重写的两大特例
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
// 虚函数重写/覆盖条件 : 虚函数 + 三同(函数名、参数、返回值)
// 特例1:子类虚函数不加virtual,依旧构成重写 (实际最好加上)
//void BuyTicket() { cout << "买票-半价" << endl; }
virtual void BuyTicket() { cout << "买票-半价" << endl; }
//重写的协变:返回值可以不同, 但是要求返回值必须是父子关系的指针或者引用
//.....
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "优先买票" << endl; }
};
// 多态两个条件:
// 1、虚函数重写
// 2、父类指针或者引用去调用虚函数
// void Func(Person* p) // 父类指针或者引用去调用虚函数
void Func(Person& p)// 父类指针或者引用去调用虚函数
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
情况一:子类虚函数不加virtual,依旧构成重写(实际中最好加上)
情况二:重写的协变:返回值可以不同,但是要求返回值必须是父子关系的指针或者引用
3. 多态的原理
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void Func() { cout << "Func" << endl; }
int _a = 0;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
int _b = 0;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
说明一下:
- vfptr是一张虚函数的表,本质就是一个函数指针数组
- 多态调用:运行时是去指向对象的虚表中找到函数地址,并进行调用(在符合多态的两个条件时,)
- 普通调用:编译链接时就确认了函数地址,运行时直接调用
4. 在继承中把析构函数定义成虚函数
#include <iostream>
using namespace std;
//建议在继承中析构函数定义成虚函数
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
//int* _ptr;
};
class Student : public Person {
public:
// 析构函数名会被处理成destructor,所以这里析构函数完成虚函数重写
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* ptr1 = new Person;
delete ptr1;
Person* ptr2 = new Student;
delete ptr2;
return 0;
}
说明一下:
- 这段代码在不同的编译器中,结果可能不一样,
- 析构函数名会被处理成destructor,所以这里析构函数完成虚函数重写
- 发生了多态,与指针类型无关
5. 抽象类 && 纯虚函数
说明一下:
- 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类 不能实例化出对象
抽象类中纯虚函数的应用
#include <iostream>
using namespace std;
class Car
{
public:
virtual void Drive() = 0;
};
class BMW : public Car
{
public:
virtual void Drive()
{
cout << "操控-好开" << endl;
}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-豪华舒适" << endl;
}
};
int main()
{
//Car c;
//BMW b;
Car* ptr = new BMW;
ptr->Drive();
ptr = new Benz;
ptr->Drive();
return 0;
}
说明一下:
- 其中如果不需要用到父类的虚函数,仅仅是为了实现多态,
- 将父类的虚函数写成纯虚函数,将父类定义成抽象类
6. 理解虚表
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person::买票-全价" << endl;
}
virtual void Func1()
{
cout << "Person::Func1()" << endl;
}
};
class Student : public Person {
public:
// 这里只用BuyTicket()完成了虚函数的重写
virtual void BuyTicket()
{
cout << "Student::买票-半价" << endl;
}
virtual void Func2()
{
cout << "Student::Func2()" << endl;
}
};
int main()
{
// 同一个类型的对象共用一个虚表
Person p1;
Person p2;
// vs下 不管是否完成重写,子类虚表跟父类虚表都不是同一个
Student s1;
Student s2;
return 0;
}
说明一下:
- 同一个类型的对象共用一个虚表
- vs下 不管是否完成重写,子类虚表跟父类虚表都不是同一个
第七章 C++11
1. 统一的列表初始化
#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <set>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
int x1 = 1;
// 能看懂,但不建议使用
int x2 = { 2 };
int x3 { 2 };
Date d1(2022, 11, 22);// ->调用构造函数
// 能看懂,但不建议使用
Date d2 = {2022, 11, 11}; // ->调用构造函数
Date d3{ 2022, 11, 11 };// ->调用构造函数
vector<int> v1 = { 1, 2, 3, 4, 5, 6 };
vector<int> v2 { 1, 2, 3, 4, 5, 6 };
list<int> lt1 = { 1, 2, 3, 4, 5, 6 };
list<int> lt2{ 1, 2, 3, 4, 5, 6 };
auto x = { 1, 2, 3, 4, 5, 6 };
cout << typeid(x).name() << endl;
return 0;
}
- C++11中增大了{ }的使用范围,要求能看懂,但是不建议使用
1.1 {}在自定义类型中的应用
#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <set>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 11, 22);
Date d2 = {2022, 11, 11};
Date d3{ 2022, 11, 11 };
vector<Date> v3 = {d1, d2, d3};
// C++11中{}的真正用途
vector<Date> v4 = { { 2022, 1, 1 }, {2022, 11, 11} };
map<string, string> dict = { { "sort", "排序" }, { "insert", "插入" } };
// 赋值重载
// 这里的auto无法自动推导,所以自己显示类型
initializer_list<pair<const string, string>> kvil = { { "left", "左边" }, { "left", "左边" } };
dict = kvil;
return 0;
}
1.2 initializer_list模板引入
说明一下:
- C++11增加了 initializer_list,以及对vector和list等容器的更新,所以才支持上面的{}用法
2. 新增decltype关键字
#include <iostream>
using namespace std;
int main()
{
int x = 10;
// typeid拿到只是类型的字符串,不能用这个再去定义对象什么的
//typeid(x).name() y = 20;
decltype(x) y1 = 20.22;
auto y2 = 20.22;
cout << y1 << endl;
cout << y2 << endl;
return 0;
}
说明一下:
- typeid拿到只是类型的字符串,不能用这个再去定义对象什么的
- decltype不仅仅可以拿到变量的类型,还可以去定义对象
3. C++11新增容器
C++11新增了array,forward_list,以及unordered_map,unordered_set,其实也就后面两个有用
简单介绍array容器
#include <iostream>
#include <array>
using namespace std;
int main()
{
const size_t N = 100;
int a1[N];
// C语言数组越界检查,越界读基本检查不出来,越界写是抽查
a1[N];
//a1[N] = 1;
//a1[N + 5] = 1;
// 越界读写都可以被检查出来
array<int, 1> a2;
a2[N];
a2[N] = 1;
a2[N + 5] = 1;
return 0;
}
说明一下:
- array容器只要是越界就一定能被检查出来
- C语言数组越界检查,越界读基本检查不出来,越界写是抽查
4. 左值引用& 和 右值引用&&
#include <iostream>
#include <utility>
using namespace std;
int main()
{
// 左值: 能取地址
int* p = new int(0);
int b = 1;
const int c = 2;
// 对左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
// 右值:不能取地址
10;
1.1 + 2.2;
// 对右值的右值引用
int&& rr1 = 10;
double&& rr2 = 1.1 + 2.2;
// 对右值的左值引用
// double& r1 = 1.1 + 2.2;// error
const double& r1 = 1.1 + 2.2;
// 对左值的右值引用
//int&& rr5 = b;// error
int&& rr5 = move(b);
return 0;
}
说明一下:
- 左值: 能取地址
- 右值: 不能取地址,如:临时对象,const的不能被修改的对象
- 如果对右值使用左值引用,需要加上const
- 如果对左值使用右值引用,需要是使用move函数
4.1 左值引用可以解决的问题
- 做参数,a. 减少拷贝 b.做输出型参数
- 做返回值,a.减少拷贝, 提高效率 b.引用返回,可以修改返回对象(比如: operator[])
4.2 左值引用无法解决的问题
- 引用返回的前提是返回值出了作用域之后还在,
- 但是无法解决string中的to_string的返回值,以及有些函数返回值是二维数组的问题
4.3 右值引用的实际用途: 移动构造 + 移动赋值
就是为了解决原来拷贝构造和赋值重载,会拷贝一个临时变量,借助临时变量进行传值的问题,而有了这个移动构造/移动赋值之后,就可以减少一次拷贝,具体还是跟编译器优化有关系
没有移动构造
说明一下:
- 这里的string需要自己写,才能看到结果
- 这里的g++编译器优化的更厉害,这里虽然定义了ret
但是没有使用,所以g++一次都没有拷贝构造 - -fno-elide-constructors可以取消编译器的优化
加上移动构造
说明一下:
- 函数参数的匹配原则是会优先匹配最合适自己的参数,
- to__string(-3456)中的-3456是一个右值,会优先匹配到移动构造,会发生右值引用返回
- 右值:1.内置类型右值-纯右值 2. 自定义类型右值 - 将亡值
- 将亡值就是快要亡了的值,所以它的值就可以直接交换(swap)
- 将拷贝构造变成移动构造,会极大的提高效率
没加移动赋值
说明一下:
- g++编译器优化的很厉害
加上移动赋值
说明一下:
- 移动赋值和移动构造一样,减少了拷贝,提高了效率
4.4 容器插入接口->右值版本
说明一下:
- C++11以后,几乎所有的容器插入接口都提供了右值版本
- 插入过程中,如果传递对象是右值对象,那么就会进行资源转移减少拷贝
5. 右值引用问题:引用折叠
当函数模板参数为右值引用时,这是可以把传递过来的参数看成左值或者右值,但是编译器只会把它看成左值引用,无法解析为我们原来的意思,这个问题就叫做引用折叠
#include <iostream>
#include <utility>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用:t既能引用左值,也能引用右值
// 引用折叠
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
说明一下:
- 模板参数的右值引用叫万能引用,T&& t中的t即能引用左值,也能引用右值
- 这里会触发引用折叠,编译器会把它识别成左值
6. 完美转发解决引用折叠问题
说明一下:
- 完美转发:确保函数模板参数为右值引用时,使传递过来的参数能被编译器解析为左值或者右值
- 它通过std::forward<模板>(参数)实现
7. 新的类功能
7.1 C++中默认成员函数
- 构造 析构 拷贝构造
- 赋值重载 取地址重载 const取地址重载
- 和C++11新增的移动构造,移动赋值
7.2 移动构造 && 移动赋值的注意事项
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。
- 那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,
- 对于内置类 型成员会执行逐成员按字节拷贝,
- 自定义类型成员,则需要看这个成员是否实现移动构造,
- 如果实现了就调用移动构造,没有实现就调用拷贝构造
- 移动赋值运算符重载同上
7.3 强制生成默认函数的关键字default
7.4 禁止生成默认函数的关键字delete
8. 可变参数模板
#include <iostream>
using namespace std;
// 可变参数的函数模板
template <class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
string str("hello");
ShowList();
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', str);
return 0;
}
说明一下:
- sizeof...(args)// 求模板参数的个数
使用方法
// 解决只有一个参数的情况
void ShowList()
{
cout << endl;
}
// Args... args代表N个参数包(N >= 0)
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{
cout << "ShowList("<<val<<", " << sizeof...(args) << "参数包)" << endl;
ShowList(args...);
}
int main()
{
string str("hello");
ShowList(1, 'A', str);
return 0;
}
说明一下:
- 通过递归调用去使用模板参数
9. emplace_back接口的引入
#include <list>
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
Date& operator=(const Date& d)
{
cout << "Date& operator=(const Date& d))" << endl;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
list<Date> lt1;
cout << "---------------------------------" << endl;
lt1.push_back(Date(2022, 11, 16));
cout << "---------------------------------" << endl;
lt1.emplace_back(2022, 11, 16);
cout << "---------------------------------" << endl;
return 0;
}
说明一下:
- 内置类型:emplace_back和push_back对内置类型的处理是一样的
- 自定义类型: 因为emplace_back可以直接传参数 就只会发生构造 比push_back少一次的拷贝
10. lambda表达式语法
[捕捉列表](参数列表)mutable->返回值类型{函数体实现}
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时, 参数列表不可省略 (即使参数为空)
注意:
- 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空
- 因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
- 定义了lambda,还需要调用
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
// 两个数相加的lambda
auto add1 = [](int a, int b)->int{return a + b; };
cout << add1(1, 2) << endl;
// 省略返回值
auto add2 = [](int a, int b){return a + b; };
cout << add2(1, 2) << endl;
// 交换变量的lambda
int x = 0, y = 1;
auto swap1 = [](int& x1, int& x2)->void{int tmp = x1; x1 = x2; x2 = tmp; };
swap1(x, y);
cout << x << ":" << y << endl;
auto swap2 = [](int& x1, int& x2)
{
int tmp = x1;
x1 = x2;
x2 = tmp;
};
swap2(x, y);
cout << x << ":" << y << endl;
return 0;
}
10.1 捕捉列表说明
不传参数,默认捕捉的对象不能修改
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
#include <iostream>
using namespace std;
static int f = 1;// 全局
int ff = 2;// 全局
int func()
{
int a, b, c, d, e;
a = b = c = d = e = 1;
// [=] 全部传值捕捉
auto f1 = [=]() {
cout << a << b << c << d << e << endl;
};
f1();// 调用
//auto f2 = [=, a]() {};// error
// 混合捕捉
auto f2 = [=, &a]() {
a++;
cout << a << b << c << d << e << endl;
};
f2();// 调用
static int x = 0;
if (a)
{
// []也能捕捉全局变量
auto f3 = [&, a]() {
//a++;
b++, c++, d++, e++, x++;
cout << a << b << c << d << e << endl;
f++, ff++;
cout << f << " " << ff << endl;
};
f3();// 调用
}
return 0;
}
int main()
{
func();
不同的栈帧,不可捕捉
//auto f4 = [&, a]() {
// //a++;
// b++, c++, d++, e++, x++;
// cout << a << b << c << d << e << endl;
// f++, ff++;
// cout << f << " " << ff << endl;
//};
return 0;
}
说明一下:
-
[&, a]表示除了a是值传递,其他的都是引用传递
-
auto f2 = [=, a]() {};捕捉列表不允许变量重复传递 ,否则就会导致编译错误
-
在块作用域以外的lambda函数捕捉列表必须为空。
-
不同的栈帧,不可捕捉,比如f4要去捕捉其他栈帧的变量
#include <iostream>
using namespace std;
void (*PF)();
int main()
{
auto f1 = [] {cout << "hello world" << endl; };
auto f2 = [] {cout << "hello world" << endl; };
//f1 = f2// error
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值个相同类型的函数指针
PF = f2;
PF();
return 0;
}
说明一下:
- lambda表达式之间不能相互赋值
- 但是允许使用一个lambda表达式拷贝构造一个新的副本
- 但是可以将lambda表达式赋值给相同类型的函数指针
10.2 函数对象与lambda表达式
#include <iostream>
using namespace std;
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
// lambda_uuid
class lambda_xxxx
{
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// 仿函数lambda_uuid
// lambda -> lambda_uuid
auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
r2(10000, 2);
auto r3 = [=](double monty, int year)->double{return monty*rate*year; };
r3(10000, 2);
return 0;
}
说明一下:
- 实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()
11. function包装器 - lambda表达式的引用
一般用来包装lambda表达式
说明一下:
-
Ret: 被调用函数的 返回类型
-
Args…:被调用 函数的形参
#include <iostream>
#include <functional>
using namespace std;
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 使用包装器
// 函数指针
function<double(double)> f1 = f;
cout << useF(f1, 11.11) << endl;
// 函数对象
function<double(double)> f2 = Functor();
cout << useF(f2, 11.11) << endl;
// lamber表达式对象
function<double(double)> f3 = [](double d)->double{ return d / 4; };
cout << useF(f3, 11.11) << endl;
// 不使用包装器
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
11.1 经典案例-逆波兰表达式求值
说明一下:
- 包装器将多个相似的小函数包装在一起
12. bind 绑定 - 配合function包装器
#include <iostream>
#include <functional>
#include <map>
using namespace std;
int Div(int a, int b)
{
return a / b;
}
int Plus(int a, int b)
{
return a + b;
}
int Mul(int a, int b, double rate)
{
return a * b * rate;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
using namespace placeholders;
int main()
{
// 调整个数, 绑定死固定参数
function<int(int, int)> funcPlus = Plus;
//function<int(Sub, int, int)> funcSub = &Sub::sub;
function<int(int, int)> funcSub = bind(&Sub::sub, Sub(), _1, _2);
function<int(int, int)> funcMul = bind(Mul, _1, _2, 1.5);
map<string, function<int(int, int)>> opFuncMap =
{
{ "+", Plus},
{ "-", bind(&Sub::sub, Sub(), _1, _2)}
};
cout << funcPlus(1, 2) << endl;
cout << funcSub(1, 2) << endl;
cout << funcMul(2, 2) << endl;
cout << opFuncMap["+"](1, 2) << endl;
cout << opFuncMap["-"](1, 2) << endl;
cout << "------------------------" << endl;
int x = 2, y = 10;
cout << Div(x, y) << endl;
// 调整顺序, _1表示第一个参数,_2表示第二个参数
function<int(int, int)> bindFunc2 = bind(Div, _2, _1);
cout << bindFunc2(x, y) << endl;
return 0;
}
可以将bind函数看作是一个通用的 函数适配器 ,它接受一个可调用对象,生成一个新的可调用对
象来“适应”原对象的参数列表。
调用bind的一般形式:auto newCallable = bind(callable,arg_list);
- 其中,newCallable本身是一个可调用对象,
- arg_list是一个逗号分隔的参数列表,
- 对应给定的 callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
- arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示 newCallable的参数,它们占据了传递给newCallable的参数的“位置”。
- 数值n表示生成的可调用对象中参数的位置:
- _1为newCallable的第一个参数,_2为第二个参数,以此类推
bind包装器的意义
- 将一个函数的某些参数绑定为固定的值,让我们在调用时可以不用传递某些参数。
- 可以对函数参数的顺序进行灵活调整
第八章 异常&&智能指针
1. C++异常概念
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。
- try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
2. 异常的抛出和捕获
2.1 异常的抛出和匹配原则
#include <iostream>
#include <functional>
#include <map>
using namespace std;
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw "Division by zero condition!";
else
return ((double)a / (double)b);
}
void Func()
{
try
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
/*catch (int errid)
{
cout << errid << endl;
}*/
catch (const char* errmsg)
{
cout << errmsg << endl;
}
cout << "Func() end" << endl;
}
int main()
{
while (1)
{
try
{
Func();
}
catch (int errid)
{
cout << errid << endl;
}
catch (char errmsg)
{
cout << errmsg << endl;
}
/*catch (const char* errmsg)
{
cout << errmsg << endl;
}*/
catch (...) // 捕获任意类型的异常 -- 防止出现未捕获异常时,程序终止
{
cout << "未知异常" << endl;
}
}
return 0;
}
说明一下:
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
- 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象, 所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似 于函数的传值返回)
- catch(...)可以捕获任意类型的异常,当问题是不知道异常错误是什么。
2.2 将异常信息和id封装成一个类
#include <iostream>
#include <string>
using namespace std;
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _errid(id)
{}
virtual string what() const
{
return _errmsg;
}
int GetErrid()
{
return _errid;
}
protected:
string _errmsg;
int _errid;
};
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
Exception e("除0错误", 1);
throw e;
}
else
{
return ((double)a / (double)b);
}
}
void Func1()
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
void Func2()
{
}
// 1、抛异常可以抛任意类型对象
// 2、捕获时,要求类型匹配
int main()
{
while (1)
{
try
{
Func1();
}
catch (const Exception& e)
{
cout << e.what() << endl;
}
catch (...) // 捕获任意类型的异常 -- 防止出现未捕获异常时,程序终止
{
cout << "未知异常" << endl;
}
}
return 0;
}
说明一下:
- 抛异常可以抛任意类型对象
- 捕获时,要求类型匹配
2.3 在函数调用链中异常栈展开匹配原则
栈展开过程如下:
说明一下:
- 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句,如果有匹配的,则处理
- 没有则退出当前函数栈,继续在调用函数的栈中进行查找 不断重复上述过程,若到达main函数的栈,依旧没有匹配的,则终止程序
2.4 服务器开发中通常使用的异常继承体系
- 在实际中抛出和捕获的匹配原则中,并不都是类型完全匹配,可以抛出派生类的对象,再使用基类捕获,
- 因为继承关系,只需要捕捉基类的异常信息
2.5 C++ 11异常处理的优点
- 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包 含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
- 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们 也需要使用异常
- 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如 T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回 值表示错误
2.6 C++11异常处理的缺点
- 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会 导致我们跟踪调试时以及分析程序时,比较困难。
- 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
- C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常 安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
- C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
- 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常 规范有两点:
- 抛出异常类型都继承自一个基类。
- 函数是否抛异常、抛什么异常,都 使用 func() throw();的方式规范化。
3. 智能指针
3.1 案例
内存泄露:是指因为疏忽或错误,造成程序未能释放已经不再使用的内存的情况。比如
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* ptr = new int;
//...
cout << div() << endl;
//...
delete ptr;
}
int main()
{
try
{
func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
说明一下:
- 执行上述代码时,如果用户输入的除数为0,那么div函数中就会抛出异常,这时程序的执行流会直接跳转到主函数中的catch块中执行(此时第十五行就没有被执行)
- 最终导致func函数中申请的内存资源没有得到释放
解决方案:利用异常的重新捕获解决,具体方案如下:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* ptr = new int;
try
{
cout << div() << endl;
}
catch (...)
{
delete ptr;
throw;
}
delete ptr;
}
int main()
{
try
{
func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
说明一下:
- 对于这种情况,我们可以在func函数中先对div函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出
3.2 SmartPtr
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
SmartPtr<int> sp(new int);
//...
cout << div() << endl;
//...
}
int main()
{
try
{
func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
- 无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,
- 只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放
3.3 SmartPtr问题
对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃。比如:
int main()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1); //拷贝构造
SmartPtr<int> sp3(new int);
SmartPtr<int> sp4(new int);
sp3 = sp4; //拷贝赋值
return 0;
}
- 编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此用sp1拷贝构造sp2后,相当于这sp1和sp2管理了同一块内存空间,当sp1和sp2析构时就会导致这块空间被释放两次
4. RAII思想
是一种利用对象生命周期来控制程序资源的简单技术->资源管理权的转移
#include <iostream>
#include <string>
#include <windows.h>
#include <time.h>
#include <memory>
using namespace std;
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
//private:
int _a1 = 0;
int _a2 = 0;
};
int main()
{
auto_ptr<A> ap1(new A);
ap1->_a1++;
ap1->_a2++;
auto_ptr<A> ap2(ap1);
//ap1->_a1++;// 空指针的使用
//ap1->_a2++;
SmartPtr<A> sp1(new A);
sp1->_a1++;
sp1->_a2++;
//SmartPtr<A> sp2(sp1);
return 0;
}
说明一下:
- sp1和sp2都是指向的同一资源
- RAll的核心就是管理权的转移所以需要浅拷贝
- 在C++98的时候 ,auto_ptr 资源管理权转移,不负责任的拷贝,会导致被拷贝对象悬空
- 大多数时候的auto_ptr都是禁止使用的
4.1 模拟实现auto_ptr
在拷贝构造 && 赋值重载的情况下会出现那个不负责任的拷贝(被拷贝对象悬空)
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
//private:
int _a1 = 0;
int _a2 = 0;
};
namespace bit {
// C++98 auto_ptr 管理权转移,被拷贝对象的出现悬空问题
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
// ap1 = ap2;
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 特殊情况: 自己跟自己赋值
if (this != &ap)
{
// 如果_ptr之前有管理的资源,将它释放掉,再进行赋值
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
int main()
{
bit::auto_ptr<A> ap1(new A);
ap1->_a1++;
ap1->_a2++;
cout << ap1->_a1 << " : " << ap1->_a2 << endl;
bit::auto_ptr<A> ap2(ap1);
//ap1->_a1++;// 空指针的使用
//ap1->_a2++;
//cout << ap1->_a1 << " : " << ap1->_a2 << endl;
cout << ap2->_a1 << " : " << ap2->_a2 << endl;
return 0;
}
说明一下:
- C89的auto_ptr 是资源管理权转移,是不负责任的拷贝,会导致被拷贝对象悬空
4.2 模拟实现unique_ptr
禁掉拷贝构造 && 赋值重载,就不会出现那个不负责任的拷贝了(被拷贝对象悬空 )
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
//private:
int _a1 = 0;
int _a2 = 0;
};
namespace bit {
template<class T>
class unique_ptr
{
private:
// 防拷贝 C++98 ,只声明不实现
//unique_ptr(unique_ptr<T>& ap);
//unique_ptr<T>& operator=(unique_ptr<T>& ap);
public:
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
// 防拷贝 C++11
unique_ptr(unique_ptr<T>& ap) = delete;// 禁掉默认拷贝构造
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete; // 禁掉默认赋值重载
~unique_ptr()
{
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
int main()
{
bit::unique_ptr<A> ap1(new A);
ap1->_a1++;
ap1->_a2++;
cout << ap1->_a1 << " : " << ap1->_a2 << endl;
bit::unique_ptr<A> ap2(ap1);// error
//ap1->_a1++;// 空指针的使用
//ap1->_a2++;
//cout << ap1->_a1 << " : " << ap1->_a2 << endl;
//cout << ap2->_a1 << " : " << ap2->_a2 << endl;
return 0;
}
说明一下:
- C++11新增的 unique_ptr是禁止智能指针的拷贝,但是没有解决根本问题
4.3 模拟实现shared_ptr
共同管理一段空间,引用计数 记录管理者 并 解决拷贝问题
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
//private:
int _a1 = 0;
int _a2 = 0;
};
namespace bit {
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(new int(1))
{}
void Release()
{
// 减减被赋值对象的计数,如果是最后一个对象,要释放资源
if (--(*_pCount) == 0)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
delete _pCount;
}
}
~shared_ptr()
{
Release();
}
// sp1(sp2)
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount)
{
(*_pCount)++;// 资源管理者加1个
}
// sp1 = sp5
// sp1 = sp1
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 处理自己 = 自己的情况
if (_ptr == sp._ptr)
{
return *this;
}
// 赋值,首先需要将自己管理的资源释放
Release();
// 共管新资源,++计数
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
// 引用计数解决拷贝问题
int* _pCount;
};
}
int main()
{
bit::shared_ptr<A> ap1(new A);
ap1->_a1++;
ap1->_a2++;
cout << ap1->_a1 << " : " << ap1->_a2 << endl;
bit::shared_ptr<A> ap2(ap1);
ap1->_a1++;// 正常使用
ap1->_a2++;
cout << ap1->_a1 << " : " << ap1->_a2 << endl;
cout << ap2->_a1 << " : " << ap2->_a2 << endl;
return 0;
}
说明一下:
- C++11新增的 shared_ptr解决了99%的智能指针的拷贝问题
- 核心思路: 使用引用计数标记这个资源有多少个管理者(浅拷贝)
- 这里不能使用静态计数对象,因为静态成员属于整个类,存放在静态区,只有一份
4.4 shared_ptr导致的循环引用问题
#include <iostream>
#include <string>
using namespace std;
struct Node
{
int _val;
std::shared_ptr<Node> _next;
std::shared_ptr<Node> _prev;
~Node()
{
cout << "~Node" << endl;
}
};
int main()
{
std::shared_ptr<Node> n1(new Node);
std::shared_ptr<Node> n2(new Node);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
- 这个循环引用就是shared_ptr不能解决的1%的情况
4.5 模拟实现weak_ptr
weak_ptr封装了shared_ptr ,它的next和prev不增加计数
#include <iostream>
using namespace std;
namespace bit {
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(new int(1))
{}
void Release()
{
// 减减被赋值对象的计数,如果是最后一个对象,要释放资源
if (--(*_pCount) == 0)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
delete _pCount;
}
}
~shared_ptr()
{
Release();
}
// sp1(sp2)
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount)
{
(*_pCount)++;// 资源管理者加1个
}
// sp1 = sp5
// sp1 = sp1
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 处理自己 = 自己的情况
if (_ptr == sp._ptr)
{
return *this;
}
// 赋值,首先需要将自己管理的资源释放
Release();
// 共管新资源,++计数
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_pCount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
// 引用计数解决拷贝问题
int* _pCount;
};
// 辅助型智能指针,使命配合解决shared_ptr循环引用问题
template<class T>
class weak_ptr
{
public:
// ----- 构造函数 ------
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr(const weak_ptr<T>& wp)
:_ptr(wp._ptr)
{}
// ----- 构造函数 ------
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
public:
T* _ptr;
};
}
struct Node
{
int _val;
/*std::shared_ptr<Node> _next;
std::shared_ptr<Node> _prev;*/
bit::weak_ptr<Node> _next;
bit::weak_ptr<Node> _prev;
~Node()
{
cout << "~Node" << endl;
}
};
int main()
{
bit::shared_ptr<Node> n1(new Node);
bit::shared_ptr<Node> n2(new Node);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
说明一下:
- 循环引用 -- weak_ptr不是常规智能指针,没有RAII,不支持直接管理资源
- _next 和_prev是weak_ ptr时,它不参与资源释放管理,可以访问和修改到资源,
- weak_ptr封装了shared_ptr
- 但是不增加次数,不存在循环引用的问题,
- weak_ptr主要用shared_ptr构造,用来解决shared_ptr循环引用问题
- weak_ptr就是为了解决shared_ptr循环引用问题产生的,它就是shared_ptr的一个小根班
5. 定制删除器
#include <iostream>
#include <stdlib.h>
using namespace std;
// 仿函数: 重载()
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
template<class T>
struct Free
{
void operator()(T* ptr)
{
cout << "free" << ptr << endl;
free(ptr);
}
};
struct Node
{
int _val;
std::shared_ptr<Node> _next;
std::shared_ptr<Node> _prev;
~Node()
{
cout << "~Node" << endl;
}
};
// 定制删除器
void test_shared_ptr3()
{
// shared_ptr传对象
// 仿函数对象
std::shared_ptr<Node> n1(new Node[5], DeleteArray<Node>());
std::shared_ptr<Node> n2(new Node);
std::shared_ptr<int> n3(new int[5], DeleteArray<int>());
std::shared_ptr<int> n4((int*)malloc(sizeof(12)), Free<int>());
// lambda
std::shared_ptr<Node> n5(new Node[5], [](Node* ptr) {delete[] ptr; });
std::shared_ptr<Node> n6(new Node);
std::shared_ptr<int> n7(new int[5], [](int* ptr) {delete[] ptr; });
std::shared_ptr<int> n8((int*)malloc(sizeof(12)), [](int* ptr) {free(ptr); });
// unique_ptr传类型
std::unique_ptr<Node, DeleteArray<Node>> up(new Node[5]);
}
int main()
{
//std::shared_ptr<Node> n1(new Node[5]);// error
test_shared_ptr3();
return 0;
}
- 定制删除器解决类型不匹配的的问题,delete ptr和delete ptr[]