1. c++简介

C++ 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。

C++ 被认为是一种中级语言,它综合了高级语言和低级语言的特点。

注意:使用静态类型的编程语言是在编译时执行类型检查,而不是在运行时执行类型检查。

C++ 完全支持面向对象的程序设计,包括面向对象开发的四大特性:

  • 封装:封装是将数据和方法组合在一起,对外部隐藏实现细节,只公开对外提供的接口。这样可以提高安全性、可靠性和灵活性。
  • 继承:继承是从已有类中派生出新类,新类具有已有类的属性和方法,并且可以扩展或修改这些属性和方法。这样可以提高代码的复用性和可扩展性。
  • 多态:多态是指同一种操作作用于不同的对象,可以有不同的解释和实现。它可以通过接口或继承实现,可以提高代码的灵活性和可读性。
  • 抽象:抽象是从具体的实例中提取共同的特征,形成抽象类或接口,以便于代码的复用和扩展。抽象类和接口可以让程序员专注于高层次的设计和业务逻辑,而不必关注底层的实现细节。

2. 基础知识

1. 变量和数据类型

使用编程语言进行编程时,需要用到各种变量来存储各种信息。变量保留的是它所存储的值的内存位置。这意味着,当您创建一个变量时,就会在内存中保留一些空间。

c++中七种基本的 C++ 数据类型及其扩展:

类型 范围
char 1 -128到127 或者 0到255
unsigned char 1 0到255
signed char 1 -128到127
int 4 -2147483648 到 2147483647
unsigned int 4 0 到 4294967295
signed int 4 -2147483648 到 2147483647
short int 2 -32768 到 32767
unsigned short int 2 0 到 65,535
signed short int 2 -32768 到 32767
long int 8 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
signed long int 8 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
unsigned long 8 0 到 18,446,744,073,709,551,615
float 4 单精度型浮点型
double 8 双精度型浮点型
long long 8
long double 16
wchar_t 2或4 1个宽字符

类型转换

类型转换是将一个数据类型的值转换为另一种数据类型的值。C++ 中有四种类型转换:静态转换、动态转换、常量转换和重新解释转换。

  • 静态转换

    • 静态转换是将一种数据类型的值强制转换为另一种数据类型的值。

    • 静态转换通常用于比较类型相似的对象之间的转换,例如将 int 类型转换为 float 类型。

    • 静态转换不进行任何运行时类型检查,因此可能会导致运行时错误。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      void func(void* ptr) {                    //其他类型指针->void*指针->其他类型指针
      double* pp = static_cast<double*>(ptr); //将指针类型转换的函数
      }

      int main() {
      int a = 3;
      long b = a; //安全,可以隐式转换,不会出现警告
      double c = 1.23;
      long d = (long)c; //C风格:显式转换
      long d1 = static_cast<long>(c); //c++风格:显式转换

      //double* pd1 = &a; //错误,不能隐式转换
      double* pd2 = (double*)&a; //c风格,强制类型转换
      //double* pd3 = static_cast<double*>(&a); //错误,static_cast不支持不同类型指针的转换,但可以通过void*指针中转
      void* pv = &a; //任何类型的指针都可以隐式的转换为void*(无符号型指针)
      double* pd3 = static_cast<double*>(pv); //然后通过static_cast将void*转换为其他类型的指针
      cout << pd3 << endl;

      }
  • 动态转换

    • 动态转换通常用于将一个基类指针或引用转换为派生类指针或引用。动态转换在运行时进行类型检查,如果不能进行转换则返回空指针或引发异常。

      1
      2
      3
      4
      class Base {};
      class Derived : public Base {};
      Base* ptr_base = new Derived;
      Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base); // 将基类指针转换为派生类指针
  • 常量转换

    • 常量转换用于将 const 类型的对象转换为非 const 类型的对象。

    • 常量转换只能用于转换掉 const 属性,不能改变对象的类型。

      1
      2
      const int i = 10;
      int& r = const_cast<int&>(i); // 常量转换,将const int转换为int
  • 重新解释转换

    • 重新解释转换将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换。

    • 重新解释转换不进行任何类型检查,因此可能会导致未定义的行为。

    • int i = 10;
      float f = reinterpret_cast<float&>(i); // 重新解释将int类型转换为float类型
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18

      ```c++
      #include<iostream>
      using namespace std; //使用std这个命名空间
      int number; //全局变量可以不用赋值,默认为0

      int main() {
      int number = 1; //局部变量
      cout << "number=" << number << endl;
      cout << "::number=" << ::number << endl; //访问全局变量前要加::

      //定义常量
      const float Pi = 3.14;
      cout << "Pi=" << Pi << endl;

      //特殊字符
      cout << "hello world\t\"hello world\"\n \?" << endl;
      }

c++中有两种类型的表达式:

  • 左值:指向内存位置的表达式被称为左值表达式。左值可以出现在赋值号的左边或右边。
  • 右值:术语右值指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。

c++中的变量作用域:

  • 局部作用域:在函数内部声明的变量具有局部作用域,它们只能在函数内部访问。局部变量在函数每次被调用时被创建,在函数执行完后被销毁。
  • 全局作用域:在所有函数和代码块之外声明的变量具有全局作用域,它们可以被程序中的任何函数访问。全局变量在程序开始时被创建,在程序结束时被销毁。
  • 块作用域:在代码块内部声明的变量具有块作用域,它们只能在代码块内部访问。块作用域变量在代码块每次被执行时被创建,在代码块执行完后被销毁。
  • 类作用域:在类内部声明的变量具有类作用域,它们可以被类的所有成员函数访问。类作用域变量的生命周期与类的生命周期相同。

2. 修饰符类型

类型限定符提供了变量的额外信息,用于在定义变量或函数时改变它们的默认行为的关键字。

限定符 含义
const const定义常量,表示该变量的值不能被修改
volatile 修饰符volatile告诉该变量的值可能会被程序以外的因素改变,如硬件或其他线程
restrict 由restrict修饰的指针是唯一一种访问它所指向的对象的方式。只有 C99 增加了新的类型限定符 restrict
mutable 表示类中的成员变量可以在 const 成员函数中被修改
static 用于定义静态变量,表示该变量的作用域仅限于当前文件或当前函数内,不会被其他文件或函数访问
register 用于定义寄存器变量,表示该变量被频繁使用,可以存储在CPU的寄存器中,以提高程序的运行效率
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const int NUM = 10; // 定义常量 NUM,其值不可修改
volatile int num = 20; // 定义变量 num,其值可能会在未知的时间被改变
class Example {
public:
int get_value() const {
return value_; // const 关键字表示该成员函数不会修改对象中的数据成员
}
void set_value(int value) const {
value_ = value; // mutable 关键字允许在 const 成员函数中修改成员变量
}
private:
mutable int value_;
};
void example_function(register int num) {
// register 关键字建议编译器将变量 num 存储在寄存器中
// 以提高程序执行速度
// 但是实际上是否会存储在寄存器中由编译器决定
}

3. 运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int main() {
//短路求值
int i = 0;
1 < 2 || ++i;
cout << "i=" << i << endl;

//位运算符
unsigned char bits = 0xb5; // 1011 0101
cout << hex; //十六进制表示(设置cout的输出格式为十六进制表示)
//算术操作符会导致 整型提升(bits原来是8位,经过移动操作后,c++会将其提升为int类型32位)
cout << "bits左移2位:" << (bits << 2) << endl; // 0000 0000 0000 0000 0000 0010 1101 0100
cout << "bits右移2位:" << (bits >> 2) << endl; // 0000 0000 0000 0000 0000 0000 0010 1101

cout << dec; //十进制表示
cout << (200 << 3) << endl; //扩大8倍

//位逻辑运算符
unsigned char uc1 = 5; // 0000 0101
unsigned char uc2 = 12; // 0000 1100

cout << (~uc1) << endl; //按位取反 1111 1010 最高位是1,是负数(负数是以补码形式表示的),原码:0000 0110
cout << (uc1 & uc2) << endl; //按位取与 0000 0100
cout << (uc1 | uc2) << endl; //按位取或 0000 1101
cout << (uc1 ^ uc2) << endl; //按位异或 0000 1001

//案例:从一组数里找出只出现一次的那个数
int i1 = 5, i2 = 12, i3 = 12, i4 = 9, i5 = 5;
cout << "只出现一次的那个数:" << (i1 ^ i2 ^ i3 ^ i4 ^ i5) << endl;

//强制类型转换
int total = 20, num = 6;
double avg = total / num;
cout << "avg=" << avg << endl;
cout << "avg=" << (double)total / num << endl; //c语言风格
cout << "avg=" << double(total) / num << endl; //c++函数调用风格
cout << "avg=" << static_cast<double>(total) / num << endl; //c++强制类型转换运算符
}

输出的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
i=0
bits左移2位:2d4
bits右移2位:2d
1600
-6
4
13
9
只出现一次的那个数:9
avg=3
avg=3.33333
avg=3.33333
avg=3.33333

4.流程控制

1.范围for循环

1
2
3
4
5
int main() {
for (int num : {1, 2, 3, 4, 5, 6, 7}) {
cout << "现在的数字是:" << num << endl;
}
}

2.99乘法表

1
2
3
4
5
6
7
8
int main(){
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
cout << j << "X" << i << "=" << i * j << '\t';
}
cout << endl;
}
}

3.绘制爱心曲线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main() {
//爱心曲线方程:(x^2+y^2-a)^3-x^2 * y^3 = 0
double a = 0.8;
double bound = 1.3 * sqrt(a);
//x,y坐标变换步长
double step = 0.1;
//二维码扫描所有点
for (double y = bound; y >= -bound; y -= step) {
for (double x = -bound; x <= bound; x += 0.5*step) {
//代入曲线方程,计算每个点是否在曲线内
double result = pow((pow(x,2) + pow(y, 2) - a), 3) - pow(x, 2) * pow(y, 3);
if (result <= 0) {
cout << '*';
}
else {
cout << " ";
}
}
cout << endl;
}
}

5. 指针与常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int main() {
//定义指针
int* p1;
long* p2;
long long* p3;

cout << "p1在内存中的长度为: " << sizeof(p1) << endl; //8
cout << "p2在内存中的长度为: " << sizeof(p2) << endl; //8
cout << "p3在内存中的长度为: " << sizeof(p3) << endl; //8

//指针的使用
int a = 3;
int b = 9;
long c = 93;
p1 = &a;
p2 = &c;
cout << "a的地址为:" << &a << endl; //0000007A6770FB94
cout << "b的地址为:" << &b << endl; //0000007A6770FBB4
cout << "c的地址为:" << &c << endl; //0000007A6770FBD4
cout << "p1=" << p1 << endl; //0000007A6770FB94
cout << "p2=" << p2 << endl; //0000007A6770FBD4

*p1 = 12; //修改p1指针指向的变量值
cout << "a=" << a << endl; //12
p1 = &b; //将p1指向的变量改为b的地址
*p1 = 25;
cout << "a=" << a << endl; //12
cout << "b=" << b << endl; //25

cout << "------------------" << endl;
//指向常量的指针
const int c1 = 10, c2 = 25;
const int* pc = &c1; //pc指向int类型常量的指针, int* pc = &c1;会报错
cout << "*pc=" << *pc << endl; //10
pc = &c2; //指向的常量不能修改,但可以修改指针的指向
cout << "*pc=" << *pc << endl; //25

cout << "------------------" << endl;
//指针常量(const指针):指针的指向不能改
int* const cp = &a; //只能指向int型变量,不能是常量
*cp = 93; //指向的地址不能变,但里面的值可以变
cout << "a=" << a << endl; //93

cout << "------------------" << endl;
//指针数组和数组指针
int* pa[5]; //指针数组:数组里面存的是指针
int(*ap)[5]; //数组指针:指向数组的指针
cout << "pa在内存中的长度为: " << sizeof(pa) << endl; //5*8 = 40
cout << "ap在内存中的长度为: " << sizeof(ap) << endl; //是一个指针,所以长度是8
int arr[] = { 1,2,3,4,5, };
ap = &arr; //ap指向的是整个arr数组,不能是ap=arr;
}

6. 引用与常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main() {
//引用:地址是同一个地址,而且引用一个变量过后,不能再绑定其他变量,地址就定了,但里面的值可以改
int a = 10, b = 25;
int& ref = a; // int& ref =90; 将会报错
cout << "ref=" << ref << endl; //10
cout << "a的地址为:" << &a << endl; //0000001CE5EFF964
cout << "ref的地址为:" << &ref << endl; //0000001CE5EFF964

//对常量的引用
const int zero = 0;
const int& cref = zero;
//cref = 10; cref是zero的一个常量引用了,因此不能修改所引用的值
int i = 3;
const int& cref2 = i; //可以用一个变量做初始值
const int& cref3 = 10; //可以用字面值常量做初始化值

//引用和指针常量
int t = 10;
int& ref6 = t;
int* const p = &t;
ref6 = 20;
cout << "t=" << t << endl; //20
*p = 15;
cout << "t=" << t << endl; //15

//绑定指针的引用
int* ptr = &a;
int*& prep = ptr;
//没有指向引用的指针,因为引用不是一种数据类型,它也是绑定其他数据类型的
}

常量引用:通过使用 const int&,你可以创建一个引用,该引用可以引用常量(const int)、变量(int)或字面值(10),但你不能通过这个引用修改其引用的值。

引用与指针常量的区别

引用:

  • 绑定后不可变,始终指向同一个变量
  • 可以通过引用来修改其所绑定变量的值

指针常量:

  • 绑定后不可变,指向的对象不能改变
  • 可以通过指针来修改其所指向变量的值

结论:引用和指针常量在绑定后都是不能再绑定到其他变量,但它们的用法和语义上有明显的区别。引用更像是一个别名,而指针常量则是一个变量,其值是一个内存地址。

7. 对象特性

1.构造函数和析构函数

构造函数和析构函数都是必须有的实现,如果我们自己不提供,编译器会提供一个空实现的构造和析构

  • 构造函数

    • 没有返回值,不用写void
    • 函数名与类名相同
    • 构造函数可以有参数,可以发生重载
    • 创建对象的时候,构造函数会自动调用,而且只调用一次
  • 析构函数(进行清理的操作)

    • 没有返回值,不写void
    • 函数名和类名相同,在名称前加~
    • 析构函数不可以有参数的,不可以发生重载
    • 对象在销毁前,会自动调用析构函数,而且只调用一次

2.成员变量和成员函数分开存储

空对象占用内存空间为1,c++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置。

下面这个程序中,当Person类中有int num的时候,程序输出为4,当没有int num的时候,输出的结果为1。

1
2
3
4
5
6
7
8
9
10
11
class Person {
int num; //非静态成员变量,属于类的对象上
static int sum; //静态成员变量,不属于类对象上
void fun() {}; //非静态成员函数,不属于类对象上
static void fun1() {}; //静态成员函数,不属于类对象上
};

int main() {
Person p;
cout << "类的大小:" << sizeof(p) << endl;
}

3.this指针和指针常量

this指针的本质是指针常量,指针的指向是不可以修改的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
public:
//在成员函数后面加const,修饰的是this指向,让指针指向的值也不能修改,相当于const Person * const this
void showPerson() const { //常函数
//this->a = 100;
this->b = 100; //不会报错
}
void func() {};
int a;
mutable int b; //特殊变量,即使在常函数中,也可以修改这个值
};

int main() {
Person p;
p.showPerson();
const Person p1; //在对象前面加const,变为常对象
}

8. 运算符重载

1.赋值运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//在普通赋值运算符情况下,当有些属性创建在堆区,此时就会出现浅拷贝的问题
class Person {
public:
Person(int age) {
m_Age = new int(age);
}
~Person() {
if (m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
}
//重载 赋值运算符
Person& operator=(Person& p) {
//编译器提供的是浅拷贝
if (m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
m_Age = new int(*p.m_Age);
return *this;
}
int* m_Age;
};
int main() {
Person p1(18);
Person p2(24);
Person p3(30);
p3 = p2 = p1;
cout << *p3.m_Age << endl;
}

2.关系运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Person {
public:
Person(string name, int age) {
m_name = name;
m_age = age;
}
//重载 == 号
bool operator==(Person& p) {
if (this->m_name == p.m_name && this->m_age == p.m_age) {
return true;
}
return false;
}
//重载 != 号
bool operator!=(Person& p) {
if (this->m_name == p.m_name && this->m_age == p.m_age) {
return false;
}
return true;
}
string m_name;
int m_age;
};
int main() {
Person p1("lxx", 24);
Person p2("lxx", 24);
if (p1 == p2) {
cout << "相同" << endl;
}
else {
cout << "不相同" << endl;
}
if (p1 != p2) {
cout << "不相同" << endl;
}
else {
cout << "相同" << endl;
}
}

3.加号运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//运算符重载:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
class Person {
public:
//1、成员函数来重载+运算符
//下面调用的本质:Person p3 =p1.operator+(p2);
/*Person operator+(Person& p) {
Person temp;
temp.ma = this->ma + p.ma;
temp.mb = this->mb + p.mb;
return temp;
}*/
int ma;
int mb;
};

//2、全局函数重载
//下面调用的本质:Person p3 = operator+(p1, p2);
Person operator+(Person& p1, Person& p2) {
Person temp;
temp.ma = p1.ma + p2.ma;
temp.mb = p1.mb + p2.mb;
return temp;
}
//3、运算符重载也可以发生函数重载
Person operator+(Person& p1, int num) {
Person temp;
temp.ma = p1.ma + num;
temp.mb = p1.mb + num;
return temp;
}

int main() {
Person p1;
p1.ma = 10;
p1.mb = 10;
Person p2;
p2.ma = 10;
p2.mb = 10;
Person p3 = p1 + p2;
// Person p3 = p1.operator+(p2); //第一种重载也可以使用这种方式来使用
Person p4 = p1 + 12;
cout << p3.ma << "\t" << p3.mb << endl;
cout << p4.ma << "\t" << p4.mb << endl;
}

4.左移运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {

public:
//利用成员函数重载 左移运算符 p.operator<<(cout) 简化版本是p<<cout
//不能利用成员函数进行重载<<运算符,因为无法实现 cout在左侧
int ma;
int mb;
};
ostream& operator<<(ostream &cout, Person &p) { //operator<<(cout,p)的本质是cout<<p
cout << "ma=" << p.ma << " mb=" << p.mb;
return cout;
}
int main() {
int a = 10;
Person p;
p.ma = 10; p.mb = 10;
cout << p << endl;
}

9. 继承

1.以public方式继承父类

  • 父类中的公共权限成员 到 子类中依然是公共权限
  • 父类中的保护权限成员 到 子类中依然是保护权限
  • 父类中的私有权限成员 在 子类中访问不到

2.以protected方式继承父类

  • 父类中的公共权限成员 到 子类中是保护权限
  • 父类中的保护权限成员 到 子类中是保护权限
  • 父类中的私有权限成员 在 子类访问不到

3.以private方式继承父类

  • 父类中的公共权限成员 到 子类中是私有权限
  • 父类中的保护权限成员 到 子类中是私有权限
  • 父类中的私有权限成员 在 子类访问不到

注意:继承中的构造函数和析构函数的顺序:先构造父类,再构造子类,析构函数顺序与构造函数顺序相反

如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数。如果想访问到父类中被隐藏的同名成员函数,需要加作用域

菱形继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal {
public:
int ma;
};
//利用虚继承,解决菱形继承的问题
//继承之前,加上关键字virtual变为虚继承
//Animal类称为虚基类
class sheep:virtual public Animal {
};

class Tuo :virtual public Animal {
};

class yyy :public sheep, public Tuo {
};
int main() {
yyy s;
s.sheep::ma = 18;
s.Tuo::ma = 24;
//加上虚继承后,访问的就是同一个地址下的值了,解决了资源浪费的问题
cout << s.sheep::ma << endl;
cout << s.Tuo::ma << endl;
cout << s.ma << endl;
}

10. 多态

1.多态满足的条件

  • 有继承关系
  • 子类重写父类的虚函数(重写:函数返回值类型、函数名、参数列表 完成相同)

多态使用:父类的指针或者引用 指向子类对象

2.纯虚函数:只要有一个纯虚函数,这个类就是抽象类
抽象类的特定:

  • 无法实例化
  • 抽象类的子类,必须要重写父类中的纯虚函数,否则也属于抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
public:
//纯虚函数
virtual void func() = 0;
};

class son : public Base {
virtual void func() {
cout << "子类中func的调用" << endl;
}
};

int main() {
//Base b; //无法实例化抽象类
Base* p = new son;
p->func();
}

3.虚析构函数和纯虚析构函数共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

区别:如果是纯虚析构函数,该类是抽象类,无法实例化对象。

虚析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
virtual ~Base() { // 虚析构函数
std::cout << "Base destructor called" << std::endl;
}
};

class Derived : public Base {
public:
~Derived() { // 派生类的析构函数
std::cout << "Derived destructor called" << std::endl;
}
};

int main() {
Base* basePtr = new Derived(); // 基类指针指向派生类对象
delete basePtr; // 删除时调用派生类和基类的析构函数
return 0;
}

纯虚析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
public:
virtual ~Base() = 0; // 纯虚析构函数声明
};

Base::~Base() { // 纯虚析构函数的定义
std::cout << "Base destructor called" << std::endl;
}

class Derived : public Base {
public:
~Derived() { // 派生类的析构函数
std::cout << "Derived destructor called" << std::endl;
}
};

int main() {
Base* basePtr = new Derived(); // 基类指针指向派生类对象
delete basePtr; // 删除时调用派生类和基类的析构函数
return 0;
}

11. 模板

1.函数模板的应用:建立一个通用函数,其函数反回值类型和形参可以不具体制定,用一个虚拟的类型来表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template<typename T>  //声明一个模板,告诉编译器后面代码种紧跟这的T不要报错,T是一个通用数据类型
void mySwap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}

template<class T>
void func() {
cout << "func调用" << endl;
}

int main() {
int a = 10;
int b = 20;
char c = 't';
//1、自动类型推到
mySwap(a, b);
//mySwap(a, c); //错误,T推到不出一致的类型,一个是int,一个是char
//2、显示指定类型
//mySwap<int>(a, b);
cout << "a=" << a << endl;
cout << "b=" << b << endl;
//func(); //报错,模板必须要确定出T的数据类型,才可以使用
func<int>();
}

2.调用规则

  • 如果函数模板和普通函数都可以调用,优先调用普通函数
  • 可以通过空模板参数列表 强制调用 函数模板
  • 函数模板可以发生函数重载
  • 如果函数模板可以产生更好的匹配,优先调用函数模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void myprint(int a, int b) {
cout << "普通函数调用" << endl;
}

template<typename T>
void myprint(T a, T b) {
cout << "模板函数调用" << endl;
}
template<typename T>
void myprint(T a, T b, T c) {
cout << "模板函数重载调用" << endl;
}

int main() {
int a = 10;
int b = 20;
myprint(a,b); //调用的是普通函数模板

//通过空模板参数列表,强制调用函数模板
myprint<>(a, b);

//函数模板也可以重载
myprint(a, b, 100);

//如果函数模板可以产生更好的匹配,优先调用函数模板
char c1 = 'a';
char c2 = 'b';
myprint(c1, c2); //虽然普通函数可以隐式的转换类型,但调用的是模板函数调用
}

3.函数模板的全特化

模板的调用不是万能的,当对自定义类型数据等进行比较时,会有问题。利用全特化的模板,可以解决自定义类型的通用性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
template<typename T>
bool mycompare(T &a, T &b) { //函数模板
if (a == b) {
return true;
}
return false;
}

class person { //自定义类型
public:
int ma;
int mb;
person(int a,int b) {
this->ma = a;
this->mb = b;
}
};
//利用具体化person的版本实现代码,具体化优先调用
template<>bool mycompare(person& p1, person& p2) { //template<>是模板全特化
if (p1.ma == p2.ma && p1.mb == p2.mb) {
return true;
}
return false;
}

int main() {
int a = 20;
int b = 20;
bool ref = mycompare(a, b); //是int类型,调用原始模板即可
if (ref) {
cout << "相等" << endl;
}
else {
cout << "不相等" << endl;
}
person p1(10, 20);
person p2(10, 20);
bool ref1 = mycompare(p1, p2); //是person类型,调用自己写的全特化的模板
if (ref1) {
cout << "相等" << endl;
}
else {
cout << "不相等" << endl;
}
}

模板全特化 :

  • template<> 表示这是一个模板的完全特化版本。也就是说,这个 mycompare 函数专门用于 person 类型的对象比较。
  • 全特化意味着 mycompare 函数模板的原型存在,但这里的版本仅适用于 person 类型。

4.类模板

类模板与函数模板的区别

  • 类模板没有自动类型推导的使用方法
  • 类模板在模板参数列表中可以有默认参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<class NameType,class AgeType>
class Person {
public:
Person(NameType name, AgeType age) {
this->mname = name;
this->mage = age;
}
void showperson() {
cout << mname << "\t" << mage << endl;
}
NameType mname;
AgeType mage;
};

int main(){
Person<string, int> p1("孙悟空", 39);
p1.showperson();
}

类模板中的成员函数并不是一开始就创建的,在调用时才去创建的。

2. STL教程

C++ 标准模板库是一套功能强大的 C++ 模板类和函数的集合,它提供了一系列通用的、可复用的算法和数据结构。

STL 的设计基于泛型编程,这意味着使用模板可以编写出独立于任何特定数据类型的代码。

STL 分为多个组件,包括容器、迭代器、算法、函数对象和适配器等。

1. array容器

std::array 是 C++ 标准库中的一个模板类,它定义在 <array> 头文件中。std::array 模板类提供了一个固定大小的数组,其大小在编译时确定,并且不允许动态改变。与 C 语言中的数组相比,具有更好的类型安全和内存管理特性。

1.std::array的基本语法:

std::array<T, N> array_name;

  • T 是数组中元素的类型。
  • N 是数组的大小,必须是一个非负整数。

声明与初始化

1
2
3
int main() {
std::array<int, 5> arr = {1, 2, 3, 4, 5}; // 声明一个定长为5的int数组
}

2.特点

  • 类型安全std::array 强制类型检查,避免了 C 语言数组的类型不安全问题。
  • 固定大小:数组的大小在编译时确定,不能在运行时改变。
  • 内存连续std::array 的元素在内存中是连续存储的,这使得它可以高效地访问元素。
  • 标准容器std::array 提供了与 std::vector 类似的接口,如 size(), at(), front(), back() 等。

3.常用的一些函数

函数 说明
at(size_t pos) 返回指定位置的元素,带边界检查
operator[] 返回指定位置的元素,不带边界检查
front() 返回数组的第一个元素
back() 返回数组的最后一个元素
data() 返回指向数组数据的指针
size() 返回数组大小(固定不变)
fill(const T& value) 将数组所有元素设置为指定值
swap(array& other) 交换两个数组的内容
begin() / end() 返回数组的起始/结束迭代器

4.特性

特性 std::array
大小 编译时固定
边界检查 at() 提供边界检查
内存管理 栈上分配
性能 高效
接口 支持 STL 标准接口

2. vector容器

vector是 STL 中的一个容器类,用于存储动态大小的数组。vector是一个序列容器,它允许用户在容器的末尾快速地添加或删除元素。与数组相比,<vector> 提供了更多的功能,如自动调整大小、随机访问等。

1.声明与初始化

1
2
3
4
5
6
int main() {
std::vector<int> vec1; // 空的vector
std::vector<int> vec2(5); // 长度为5的vector,元素默认初始化
std::vector<int> vec3(5, 10); // 长度为5的vector,元素值为10
std::vector<int> vec4 = {1, 2, 3, 4}; // 使用初始化列表初始化
}

2.常用的一些函数

函数 说明
push_back(const T& val) 在末尾添加元素
pop_back() 删除末尾元素
at(size_t pos) 返回指定位置的元素,带边界检查
operator[] 返回指定位置的元素,不带边界检查
front() 返回第一个元素
back() 返回最后一个元素
data() 返回指向底层数组的指针
size() 返回当前元素数量
capacity() 返回当前分配的容量
reserve(size_t n) 预留至少 n 个元素的存储空间
resize(size_t n) 将元素数量调整为 n
clear() 清空所有元素
insert(iterator pos, val) 在指定位置插入元素
erase(iterator pos) 删除指定位置的元素
begin() / end() 返回起始/结束迭代器

3.特性

特性 std::vector
大小 动态可变
存储位置 连续内存
访问性能 随机访问快速
插入和删除性能 末尾操作性能高,其他位置较慢
内存增长方式 容量不足时成倍增长

3. list容器

<list> 是 C++ 标准模板库中的一个序列容器,它允许在容器的任意位置快速插入和删除元素。与数组或向量<vector>不同,list 不需要在创建时指定大小,并且可以在任何位置添加或删除元素,而不需要重新分配内存。

1.声明和初始化

1
2
3
4
5
6
int main() {
std::list<int> lst1; // 空的list
std::list<int> lst2(5); // 包含5个默认初始化元素的list
std::list<int> lst3(5, 10); // 包含5个元素,每个元素为10
std::list<int> lst4 = {1, 2, 3, 4}; // 使用初始化列表
}

2.特点

  • 双向迭代<list> 提供了双向迭代器,可以向前和向后遍历元素。
  • 动态大小:与数组不同,<list> 的大小可以动态变化,不需要预先分配固定大小的内存。
  • 快速插入和删除:可以在列表的任何位置快速插入或删除元素,而不需要像向量那样移动大量元素。

3.常用的一些函数

函数 说明
push_back(const T& val) 在链表末尾添加元素
push_front(const T& val) 在链表头部添加元素
pop_back() 删除链表末尾的元素
pop_front() 删除链表头部的元素
insert(iterator pos, val) 在指定位置插入元素
erase(iterator pos) 删除指定位置的元素
clear() 清空所有元素
size() 返回链表中的元素数量
empty() 检查链表是否为空
front() 返回链表第一个元素
back() 返回链表最后一个元素
remove(const T& val) 删除所有等于指定值的元素
sort() 对链表中的元素进行排序
merge(list& other) 合并另一个已排序的链表
reverse() 反转链表
begin() / end() 返回链表的起始/结束迭代器

4.特性

特性 std::list
内存结构 非连续内存,双向链表
访问性能 顺序访问较快,随机访问慢
插入/删除性能 任意位置插入、删除快
适用场景 频繁在中间插入/删除
迭代器稳定性 稳定,元素插入或删除不会失效

5.注意事项

  • <list> 的元素是按插入顺序存储的,而不是按元素值排序。
  • 由于 <list> 的元素存储在不同的内存位置,所以它不适合需要随机访问的场景。
  • 与向量相比,<list> 的内存使用效率较低,因为每个元素都需要额外的空间来存储指向前后元素的指针。

4. deque容器

<deque> 提供了双端队列的实现,它在C++中以模板类的形式存在,允许存储任意类型的数据。

<deque> 是一个动态数组,它提供了快速的随机访问能力,同时允许在两端进行高效的插入和删除操作。这使得 <deque> 成为处理需要频繁插入和删除元素的场景的理想选择。

1.声明和初始化

1
2
3
4
5
6
int main() {
std::deque<int> d; // 空的deque
std::deque<int> d(5); // 包含5个默认初始化元素的d
std::deque<int> d(5, 10); // 包含5个元素,每个元素为10
std::deque<int> d = {1, 2, 3, 4}; // 使用初始化列表
}

2.常用的一些函数

函数名称 功能描述
operator= 赋值操作符,赋值给 deque 容器。
assign() 用新值替换 deque 容器中的所有元素。
at(size_type pos) 返回 pos 位置的元素,并进行范围检查。
operator[](size_type pos) 返回 pos 位置的元素,不进行范围检查。
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
begin() 返回指向第一个元素的迭代器。
end() 返回指向末尾元素后一位置的迭代器。
rbegin() 返回指向最后一个元素的逆向迭代器。
rend() 返回指向第一个元素之前位置的逆向迭代器。
empty() 检查容器是否为空。
size() 返回容器中的元素个数。
max_size() 返回容器可容纳的最大元素个数。
clear() 清除容器中的所有元素。
insert(iterator pos, const T& value) pos 位置插入 value 元素。
erase(iterator pos) 移除 pos 位置的元素。
push_back(const T& value) 在容器末尾添加 value 元素。
pop_back() 移除容器末尾的元素。
push_front(const T& value) 在容器前端添加 value 元素。
pop_front() 移除容器前端的元素。
resize(size_type count) 调整容器大小为 count,多出部分用默认值填充。
swap(deque& other) 交换两个 deque 容器的内容。
get_allocator() 返回一个用于构造双端队列的分配器对象的副本。

注意:在使用 front() 或 back() 之前,确保双端队列不为空,否则会引发未定义的行为。如果需要检查双端队列是否为空,可以使用 empty() 成员函数。

5. stack容器

<stack> 是 C++ 标准模板库的一部分,它实现了一个后进先出的数据结构。这种数据结构非常适合于需要”最后添加的元素最先被移除”的场景。

<stack> 容器适配器提供了一个栈的接口,它基于其他容器(如 dequevector)来实现。栈的元素是线性排列的,但只允许在一端(栈顶)进行添加和移除操作。

1.常用的操作

  • push(): 在栈顶添加一个元素。
  • pop(): 移除栈顶元素。
  • top(): 返回栈顶元素的引用,但不移除它。
  • empty(): 检查栈是否为空。
  • size(): 返回栈中元素的数量。

2.注意事项

  • <stack> 不提供直接访问栈中元素的方法,只能通过 top() 访问栈顶元素。
  • 尝试在空栈上调用 top()pop() 将导致未定义行为。
  • <stack> 的底层容器可以是任何支持随机访问迭代器的序列容器,如 vectordeque

6. queue容器

C++ 标准库中的 <queue> 头文件提供了队列数据结构的实现。队列是一种先进先出的数据结构,它允许在一端添加元素(称为队尾),并在另一端移除元素(称为队首)。

1.常用的操作

  • empty(): 检查队列是否为空。
  • size(): 返回队列中的元素数量。
  • front(): 返回队首元素的引用。
  • back(): 返回队尾元素的引用。
  • push(): 在队尾添加一个元素。
  • pop(): 移除队首元素。

2.注意事项

  • 队列不允许随机访问元素,即不能直接通过索引访问队列中的元素。
  • 队列的实现通常使用链表或动态数组,这取决于具体的实现。

7. priority_queue容器

在 C++ 中,<priority_queue> 是标准模板库的一部分,用于实现优先队列。优先队列是一种特殊的队列,它允许我们快速访问队列中具有最高(或最低)优先级的元素。

在 C++ 中,priority_queue 默认是一个大顶堆,这意味着队列的顶部元素总是具有最大的值。

priority_queue 是一个容器适配器,它提供了对底层容器的堆操作。它不提供迭代器,也不支持随机访问。

1.常用的操作

  • empty(): 检查队列是否为空。
  • size(): 返回队列中的元素数量。
  • top(): 返回队列顶部的元素(不删除它)。
  • push(): 向队列添加一个元素。
  • pop(): 移除队列顶部的元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//自定义类型,使用优先队列
class person1 {
public:
person1(int a, int b) {
this->a = a;
this->b = b;
}
//方法1
//bool operator<(const person1& p) const {
// return this->a > p.a; //小于符号是大顶堆,大于符号是小顶堆
//}
int a;
int b;
};
//方法2
struct cmp {
bool operator()(const person1& p1, const person1& p2) const {
return p1.b < p2.b;
}
};
int main(){
//设置que为小顶堆
priority_queue<int, vector<int>, greater<int>> que; //参3为less<int>,则是大顶堆

//自定义类型的优先队列
//priority_queue<person1>q; //方法1的调用
priority_queue<person1, vector<person1>, cmp>q; //方法2的调用
}

8. set容器

C++ 标准库中的 <set> 是一个关联容器,它存储了一组唯一的元素,并按照一定的顺序进行排序。

<set> 提供了高效的元素查找、插入和删除操作。它是基于红黑树实现的,因此具有对数时间复杂度的查找、插入和删除性能。

<set> 容器中存储的元素类型必须满足以下条件:

  • 元素类型必须可以比较大小。
  • 元素类型必须可以被复制和赋值。

1.常用的操作

  • insert(元素): 插入一个元素。
  • erase(元素): 删除一个元素。
  • find(元素): 查找一个元素。
  • size(): 返回容器中元素的数量。
  • empty(): 检查容器是否为空。

2.set容器的特点:

  • 所有元素插入时会自动排序
  • set容器不存在重复的值

3.unordered_set容器

提供了一种基于哈希表的容器,用于存储唯一的元素集合。与 set 不同,unordered_set 不保证元素的自动排序,但通常提供更快的查找、插入和删除操作。常用的操作和set容器大致一样。

9.map容器

<map> 是标准模板库的一部分,它提供了一种关联容器,用于存储键值对。

map 容器中的元素是按照键的顺序自动排序的,这使得它非常适合需要快速查找和有序数据的场景。

1.定义和特性

  • 键值对map 存储的是键值对,其中每个键都是唯一的。
  • 排序map 中的元素按照键的顺序自动排序,通常是升序。
  • 唯一性:每个键在 map 中只能出现一次。
  • 双向迭代器map 提供了双向迭代器,可以向前和向后遍历元素。

2.常用的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main() {
//插入
map<int, int>m;
//第一种
m.insert(pair<int, int>(1, 10));
//第二种
m.insert(make_pair(2, 20));
//第三种
m.insert(map<int, int>::value_type(3, 30));
//第四种(不建议,但可以用来访问)
m[4] = 40;

//删除
m.erase(m.begin()); //删除第一个元素
m.erase(2); //按照key删除一个元素

//清空
m.erase(m.begin(), m.end());
}

通过key设置排序方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class mycompare {
public:
bool operator()(int v1, int v2) const {
return v1 > v2; //小于是升序,大于的降序
}
};
int main() {
//排序
map<int, int, mycompare>m;
m.insert(make_pair(2, 20));
m.insert(make_pair(5, 50));
m.insert(make_pair(1, 10));
m.insert(make_pair(4, 40));
m.insert(make_pair(3, 30));
for (map<int, int, mycompare>::iterator it = m.begin(); it != m.end(); it++) {
cout << it->first << "\t" << it->second << endl;
}
}

3. algorithm算法库

C++ 标准库中的 <algorithm> 头文件提供了一组用于操作容器(如数组、向量、列表等)的算法。这些算法包括排序、搜索、复制、比较等,它们是编写高效、可重用代码的重要工具。

<algorithm> 头文件定义了一组模板函数,这些函数可以应用于任何类型的容器,只要容器支持迭代器。这些算法通常接受两个或更多的迭代器作为参数,表示操作的起始和结束位置。

1. 排序算法sort

定义:对容器中的元素进行排序。

语法:sort(container.begin(), container.end(), compare_function);

其中 compare_function 是一个可选的比较函数,用于自定义排序方式。

其它算法:

  • std::partial_sort: 对部分区间排序
  • std::stable_sort: 稳定排序,保留相等元素的相对顺序。
1
2
3
4
5
6
int main() {
std::vector<int> numbers = { 7, 2, 9, 1, 5, 6 };
std::sort(numbers.begin(), numbers.end()); //普通排序(默认升序)
std::partial_sort(numbers.begin(), numbers.begin() + 3, numbers.end()); //前3个是有序,后续的无序(获取最小的3个数)
std::stable_sort(numbers.begin(), numbers.end()); //普通排序(相等元素的相对位置不变)
}

2. 搜索算法find

定义:在容器中查找与给定值匹配的第一个元素。

语法:auto it = find(container.begin(), container.end(), value);

如果找到,it 将指向匹配的元素;如果没有找到,it 将等于 container.end()。

其它算法:

  • std::binary_search: 对有序区间进行二分查找。
  • std::find_if: 查找第一个满足特定条件的元素。
1
2
3
4
5
6
7
8
9
10
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
auto it = std::find(numbers.begin(), numbers.end(), 3);

std::sort(vec.begin(), vec.end()); // 先排序
bool found = std::binary_search(vec.begin(), vec.end(), 4); //找4

auto it = std::find_if(vec.begin(), vec.end(), [](int x) { return x > 3; });

}

3. 复制算法copy

定义:将一个范围内的元素复制到另一个容器或数组。

语法:copy(source_begin, source_end, destination_begin);

1
2
3
4
5
int main() {
std::vector<int> source = {1, 2, 3, 4, 5};
int destination[5];
std::copy(source.begin(), source.end(), destination); //将source中的元素复制到数组destination
}

4. 比较算法equal

定义:比较两个容器或两个范围内的元素是否相等。

语法:bool result = equal(first1, last1, first2, compare_function);

1
2
3
4
5
int main() {
std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = {1, 2, 3, 4, 5};
bool are_equal = std::equal(v1.begin(), v1.end(), v2.begin());
}

5. 修改算法

常用的语法:

  • std::reverse: 反转区间内的元素顺序。
    • std::reverse(vec.begin(), vec.end());
  • std::fill: 将指定区间内的所有元素赋值为某个值。
    • std::fill(vec.begin(), vec.end(), 0); // 所有元素设为 0
  • std::replace: 将区间内的某个值替换为另一个值。
    • std::replace(vec.begin(), vec.end(), 1, 99); // 将所有 1 替换为 99
  • std::copy: 将区间内的元素复制到另一个区间。
    • std::copy(vec.begin(), vec.end(), vec2.begin());

6. 归并算法merge

定义:将两个有序区间合并到一个有序区间。

语法:std::merge(vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), result.begin());

其它算法:

  • std::inplace_merge: 在单个区间中合并两个有序子区间。
    • std::inplace_merge(vec.begin(), middle, vec.end());

7. 集合算法

常用的语法:

  • std::set_union: 计算两个有序集合的并集。

    • auto it = std::set_union(vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), result.begin());
    • 其中返回的it是result中得到的并集元素中最后一个的下一个位置
  • std::set_intersection: 计算两个有序集合的交集。

    • auto it = std::set_intersection(vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), result.begin());
  • std::set_difference: 计算集合的差集。

    • auto it = std::set_difference(vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), result.begin());

8. 其它有用算法

常用的语法:

  • std::accumulate(需要 库):计算范围内元素的累计和。

    • int sum = std::accumulate(vec.begin(), vec.end(), 0);
  • std::for_each: 对区间内的每个元素执行相应操作。

    • std::for_each(vec.begin(), vec.end(), [](int& x) { x += 1; });
  • std::min_elementstd::max_element: 查找区间内的最小值和最大值。

    • auto min_it = std::min_element(vec.begin(), vec.end());
    • auto max_it = std::max_element(vec.begin(), vec.end());