C++ 复习

发布于:2025-05-15 ⋅ 阅读:(19) ⋅ 点赞:(0)

VS 修改 C++ 语言标准

在这里插入图片描述

右键项目-属性

输入输出

//引用头文件,用<>包裹起来的一般是系统提供的写好的代码 编译器会在专门的系统路径中去进行查找
#include <iostream>
//自己写的代码文件一般都用""包裹起来 编译器会在当前文件所在的目录中査找,找不到才会去系统路径重查找
//#include "xxx"

int main()
{
    // std:: 命名空间
    // cout  该命名空间下的一个对象
    // <<    是插入运算符,将字符串插入到 std::cout 对象中,以便输出到控制台
    std::cout << "Hello World!\n";

    //换行
    std::cout << "等待输入" << std::endl;

    //声明一装载输入内容的容器
    std::string input;
    //等待玩家输入,必须输入一些内容后回车才认为结束
    std::cin >> input;

    //主函数可以省略
    //return 0;
}

命名空间

#include <iostream>
//引用命名空间
using namespace std;

int main()
{
    cout << "Hello World" << endl;
}

变量

#include <iostream>
using namespace std;

//外部变量,允许不同文件访问的全局变量,生命周期是整个程序运行期
extern int GlobeInt = 30;
//外部函数
extern void Test();

int main()
{
#pragma region MyRegion
    //折叠代码
#pragma endregion

    //long 类型范围 -21亿 ~ 21亿 或 -9223亿亿 ~ 9223亿亿
    //不同操作系统(windows,UNIX,Linux)上所占的空间不同
    //windows 上 long 和 int 的存储空间一样
    long l = 10L;
    cout << "长整型: " << l << endl;
    cout << sizeof(int) << " " << sizeof(long) << endl;

    //无符号
    unsigned long ul = 20UL;
    cout << ul << endl;

    //范围 -9223亿亿 ~ 9223亿亿
    long long ll = 200000000000LL;
    cout << ll << endl;

    //6~7位有效数字
    float f = 1.5f;
    cout << f << endl;

    //15~16位有效数字
    double d = 4.6;
    cout << d << endl;

    //有效数字 18~21,或 33 ~ 36,不同操作系统不同
    long double ld = 1.8L;
    cout << ld << endl;

    //R"(内容)",包裹不想被转义的字符串
    string str = R"(""\""\n)";
    cout << str << endl;

    char c = 'A';
    //两个 char 相加,会被转为 int 进行加法运算,要避免这种写法
    str = c + "Q";
    cout << str << endl;

	//自动变量,让编辑器自动推导变量类型,生命周期和普通变量一样
	auto i = 10;

	//静态变量,程序整个生命周期内一直存在,局部静态变量也会一直存在
	static int j = 20;
}

字符串操作

#include <iostream>
#include <string>

using namespace std;

int main()
{
    //其他类型转字符串
    string str = to_string(123);
    str.append("456");
    cout << str << endl;

    //字符串转其他类型
    string str2 = "234";
    int i = stoi(str2);
    cout << i << endl;

    long l = stol(str2);
    cout << l << endl;

    unsigned long ul = stoul(str2);
    cout << ul << endl;
}

异常捕获

#include <iostream>
#include <string>

using namespace std;

int main()
{
    try 
    {
        int i2 = stoi("测试");
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;
    }
}

数组

一维数组

#include <iostream>
using namespace std;

int main()
{
    //3种数组定义方式
    int arr[5];
    arr[0] = 1;
    //赋值之前可能是任意数
    cout << arr[0] << endl;

    int arr2[5] = { 0, 1, 2, 3, 4 };
    cout << arr2[2] << endl;

    int arr3[] = { 0, 1, 2, 3, 4 };
    cout << arr3[3] << endl;

    //获取数组元素个数
    int count = sizeof(arr) / sizeof(arr[0]);
    cout << count << endl;

    //数组首地址,16进制
    cout << arr << endl;
    //转为10进制
    cout << (int)arr << endl;
    //数组第一个元素地址
    cout << &arr[0] << endl;

    //数组名是常量,不可以进行赋值操作
    //arr = 100;
}

二维数组

#include <iostream>
using namespace std;

int main()
{
    //4种定义二位数组的方式
    //数组名[行数][列数]
    int arr[2][3];
    arr[0][0] = 1;
    cout << arr[0][0] << endl;

    int arr2[2][3] = { {1,2,3}, {4,5,6} };
    cout << arr2[0][1] << endl;

    int arr3[2][3] = { 1,2,3,4,5,6 };
    cout << arr2[0][2] << endl;

    int arr4[][3] = { 1,2,3,4,5,6 };
    cout << arr4[1][0] << endl;
}

函数

函数声明

#include <iostream>
using namespace std;

//函数声明,告诉编译器有这么个函数,写在 main 函数之后的函数需要声明
int Max(int a, int b);

int main()
{
	int c = Max(1, 2);
	cout << c << endl;
}

int Max(int a, int b) 
{
	return a > b ? a : b;
}

函数分文件编写
创建后缀名为 .h 的头文件,在头文件中写函数的声明
创建后缀名为 .cpp 的源文件,在源文件中写函数的定义

创建 Max.h 头文件,头文件里尽量少 include 其他头文件,不要使用 using namespace,防止代代相传

int Max(int a, int b);

创建 Max.cpp 源文件

#include "Max.h"

int Max(int a, int b)
{
	return a > b ? a : b;
}

其他文件中调用

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

using namespace std;

int main()
{
	int c = Max(1, 2);
	cout << c << endl;
}

inline 内联函数

调用内联函数时,直接将代码插入到调用处,而不是执行常规的函数调用,目的是减少函数调用开销,内联函数调用会导致代码膨胀,所以适合一些小的频繁调用的函数

inline int Add(int a, int b)
{
	return a + b;
}

指针

#include <iostream>
using namespace std;

int main()
{
	int a = 10;
	//取地址
	int * p = &a;
	cout << p << endl;

	//解引用,找到指针指向内存中的数据
	cout << *p << endl;
	*p = 20;
	cout << a << " " << *p << endl;

	//32位下4字节,64位下8字节,不管什么类型的指针
	cout << sizeof(p) << endl;
}

空指针

#include <iostream>
using namespace std;

void Fun(int * p) 
{
	cout << "指针" << endl;
}

void Fun(int p)
{
	cout << "整数" << endl;
}

int main()
{
	//空指针用于给变量初始化
	int * p = NULL;
	//空指针不能访问
	//*p = 10;

	//输出为整数,违反直觉
	Fun(NULL);
}

在这里插入图片描述
查看 NULL 的定义,void * 是无类型指针或通用指针。它可以指向任何类型的数据,但在解引用之前需要将其转换为具体的类型。
当 NULL 被用作指针时,它可能会被隐式转换为整数类型,这可能导致难以发现的错误。

C++11 引入了 nullptr 关键字,专门用来区分空指针和 0,替代 NULL

int * p = nullptr;

野指针

指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针

出现野指针的常见操作:

  • 指针声明之后没有初始化
  • 变量内存释放后,指针变量还保存着该变量的内存地址

常量指针,指针常量

常量指针:const 在 * 之前,指向的值不能修改(不能通过解引用修改值),但指向可以改变。可以理解为常量的指针,指向常量的指针,const 限定了 * ,那么 * 操作不能用

int a = 1;
int b = 2;
const int * p = &a;
p = &b;		//指向可以修改
*p = 3;		//错误,指向值不能修改
b = 4;		//可以修改原始变量值

指针常量:const 在 * 之后,指向的值可以修改,但指向不能改变。可以理解为指针类型的常量,const 限定了指针,那么指针不能操作

int a = 1;
int b = 2;
int * const p = &a;
p = &b;		//错误,指向不能修改
*p = 3;		//指向的值可以修改

const 修饰指针和常量,指向和值都不能修改

int a = 1;
const int* const p = &a;

指针访问数组

int arr[5] = { 1,2,3,4,5 };
//数组名是首地址
int * p = arr;
cout << "第一个元素:" << *p << endl;

for (int i = 0; i < 5; i++)
{
	cout << *p << endl;
	p++;	//向后偏移一个类型长度
}

结构体

定义

struct Student
{
	string name;
	int age;
}s3;	//在定义结构体的时候创建变量 s3

//函数参数改为指针,实现引用传递,避免值传递复制新的副本,减少内存空间
//常量指针,防止修改值
void PrintStu(const Student* s) 
{
	s->age = 20; //错误,不能修改
	cout << s->name << endl;
}

int main()
{
	Student s1;
	s1.name = "张三";
	s1.age = 10;

	Student s2 = { "李四", 20 };

	s3.name = "王五";

	Student s4[2] =
	{
		{"张三", 10},
		{"李四", 20},
	};

	//指针操作
	Student * p = &s2;
	cout << p->name << p->age << endl;

	PrintStu(&s3);
}

内存四区

代码区 存放函数体的二进制代码,特点是共享和只读
全局区 存放全局变量,静态变量以及常量(字符串常量和 const 修饰的全局常量),程序结束后由操作系统释放
栈区 存放局部变量,函数参数,返回地址,由编译器自动分配和释放
堆区 由程序员分配和释放,如不释放,程序结束后由操作系统回收

四区的意义:不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程

new 运算符

//堆上创建整型数据,new 返回该数据类型的指针
int * p = new int(10);
//释放堆中的数据
delete p;

int * p2 = new int[10];
//释放数组要加[]
delete[] p2;

引用

作用: 给变量起别名

引用和指针的区别:

  • 引用必须初始化且不能修改指向,指针可以初始化为空并可以修改指向
  • 引用没有专门的内存地址,指针有专门的内存地址
  • 指针可以进行加减运算移动指向,引用不行
int a = 10;
//语法: 数据类型 &别名 = 原名
//引用必须初始化,且初始化后不能更改
int &b = a;

cout << a << " " << b << endl;	//10 10
b = 100;
cout << a << " " << b << endl; //100 100

int c = 20;
//&b = c; //错误,不能更改

int* p = new int(66);
int& refP = *p;
delete p;
//指针悬空后,引用也就成为悬空引用
p = nullptr;

引用做函数参数,和地址传递效果一样,语法更简单

//值传递,形参不会修饰实参
void Swap01(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;
}

//地址传递,形参会修饰实参
void Swap02(int * a, int * b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

//引用传递,形参会修饰实参,这里的参数是原变量的别名
void Swap03(int &a, int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

引用作为函数返回值

//不要返回局部变量的引用,栈上会自动释放
int& Test1()
{
	int a = 10;
	return a;
}

int& Test2()
{
	static int a = 10;	//静态变量,在全局区
	return a;
}

int main()
{
	int& ref = Test2();
	cout << ref << endl; //10
	//函数调用可以作为左值
	Test2() = 20;
	cout << ref << endl; //20
}

引用的本质是 C++ 内部实现的一个指针常量

int a = 10;
//自动转换为 int * const ref = 10
int & ref = a;
//自动转换为 *ref = 20
ref = 20;

常量引用

void Test(const int & val)
{
	val = 20; //错误,防止修改
}

int main()
{
	//编译器转换为 int temp = 10;  const int & ref = 10;
	const int & ref = 10;
	ref = 20; //错误,加 const 之后变成只读
}

左值:表达式结束后仍然存在的对象,通常是可以被赋值的,可以取到地址的
右值:临时对象,表达式结束后释放,没有明确的存储位置,不能获取地址,比如字面量

//x左值,右侧结果3就是一个右值
int x = 1 + 2;

左值引用,右值引用

int a = 10;
//左值引用
int& refA = a;

//右值引用,延长右值的生命周期
int&& refB = 3;

类和对象

C++ 面向对象三大特征:封装,继承,多态

封装:把属性和行为作为一个整体,通过 public,protect,private 对属性和行为加以权限控制。

struct 和 class 的区别

唯一区别就是默认的访问权限不一样,struct 默认权限为 public,class 默认权限为 private

构造函数,拷贝构造,析构函数

默认情况下,编译器至少给一个类添加4个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符operator=,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,编译器不在提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,编译器不会再提供其他构造函数
class Person
{
public:
	//无参构造
	Person() 
	{
		cout << "默认构造" << endl;
	}
	
	//有参构造
	Person(int a) 
	{
		age = a;
		cout << "有参构造" << endl;
	}

	//拷贝构造函数,用于复制对象,const阻止修改原对象
	Person(const Person& p)
	{
		//拷贝所有属性
		age = p.age;
		cout << "拷贝构造" << endl;
	}

	//析构函数,不能有参数,因此不能重载
	~Person()
	{
		cout << "析构" << endl;
	}

	int age;
};

//值传递,会调用拷贝构造,实参传递给形参
void Test1(Person p) {}

//​RVO(Return Value Optimization)返回值优化,是编译器对函数返回对象时的拷贝操作的优化
//值方式返回局部对象,RVO关闭会拷贝一个新的对象返回,调用拷贝构造,RVO开启则不会
Person Test2() 
{
	Person p1;
	return p1;
}

int main()
{
	//调用方式
	//1.括号法
	Person p1;		//调用默认无参构造
	Person p2(10); //调用有参构造
	Person p3(p2); //调用拷贝构造,使用已经创建的对象来初始化新对象

	//2.显示法
	Person p4 = Person(10); //有参构造
	Person p5 = Person(p4); //拷贝构造

	//3.隐式转换法
	Person p6 = 10; //相当于 Person p6 = Person(10);
	Person p7 = p6; //拷贝构造

	Test1(p1); 	//拷贝构造
	Person p8 = Test2(); //可能触发拷贝构造,看编辑器是否优化
}

深拷贝,浅拷贝

浅拷贝:简单的赋值拷贝操作。问题:可能导致堆区内存重复释放
深拷贝:在堆区重新申请空间,进行拷贝操作,如果有属性在堆区开辟内存,一定要自己提供拷贝构造函数

class Person
{
public:
	Person(int age)
	{
		//在堆上创建内存
		pAge = new int(age);
	}

	~Person()
	{
		if (pAge != nullptr) 
		{
			delete pAge;
			pAge = nullptr;
		}
		cout << "析构" << endl;
	}

	int * pAge;
};

int main()
{
	Person p1(10);
	Person p2(p1); //拷贝构造
	//p1 p2的 pAge 指向同一个内存地址,p2析构释放内存,p1析构再释放就会报错
}

初始化列表

class Person
{
public:
	//传统初始化操作
	//Person(int a, int b, int c)
	//{
	//	m_a = a;
	//	m_b = b;
	//	m_c = c;
	//}

	//初始化列表,初始化属性
	Person(int a, int b, int c) : m_a(a), m_b(b), m_c(c)
	{
		//和上面构造函数效果一样
	}

	int m_a;
	int m_b;
	int m_c;
};

类对象成员构造,析构顺序

class A{};

class B 
{
	A a;
};

创建 B 对象时,构造顺序:先 A 后 B,销毁 B 对象时,析构顺序:先 B 后 A

静态成员变量,静态成员函数

class Person
{
public:
	//静态函数只能访问静态变量,
	static void func()
	{
		m_a = 30;
		//m_b = 10;	//错误,不能确定m_b属于哪个对象,因此不能访问
		cout << "静态函数" << endl;
	}

	//静态成员变量,所有对象共享一份数据
	//编译阶段分配内存,类内声明
	static int m_a;
	int m_b;
};

//类外初始化
int Person::m_a = 10;

int main()
{
	Person p1;
	p1.m_a = 20;
	cout << "通过对象访问" << p1.m_a << endl;
	cout << "通过类名访问" << Person::m_a << endl;

	//通过对象调用
	p1.func();
	//通过类名调用
	Person::func();
}

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

class Person{};

class Animal
{
public:
	//非静态成员变量占用对象内存空间,其他的都不占用
	int age;
};

int main()
{
	Person p;
	Animal a;
	//编辑器会给空对象分配一个字节空间,是为了区分空对象占内存的位置
	cout << sizeof(p) << endl; //1
	cout << sizeof(a) << endl; //4
}

this 指针

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

class Person
{
public:
	Person(int age)
	{
		//this指针是隐含在每一个非静态成员函数内的指针,指向调用这个函数所属的对象
		this->age = age;
	}

	Person& AddAge(Person & p)
	{
		this->age += p.age;
		//this是指向调用对象的指针,*this就是对象本体
		return *this;
	}

	int age;
};

int main()
{
	Person p1(10);
	Person p2(10);
	//链式编程思想
	p2.AddAge(p1).AddAge(p1).AddAge(p1);
	cout << p2.age << endl; //40
}

常函数,常对象

class Person
{
public:
	//常函数,const 修饰的是 this 指向,让 this 指向的值也不能修改
	void Test() const
	{
		//m_a = 10; //错误,常函数中不能修改普通成员变量
		m_b = 20;
	}

	void Test2(){}

	int m_a;
	mutable int m_b; //mutable 是可变的,常函数中也能修改
};

int main()
{
	//常对象,不能修改成员变量
	const Person p;
	//p.m_a = 10; //错误,不能修改
	p.Test();       //常对象只能调用常函数
	//p.Test2();  //错误,不能调用普通成员函数,因为普通成员函数可以修改属性
}

友元

友元的目的是让一个函数或类访问另一个类中的私有成员
1.全局函数做友元

class Person
{
	//申明这个全局函数可以访问私有成员
	friend void GlobeFun(Person& p);
private:
	int m_a;
};

//全局函数
void GlobeFun(Person& p)
{
	p.m_a = 10;
}

2.类做友元

class Person
{
	//申明这个类可以方法私有成员
	friend class FriendClass;
private:
	int m_a;
};

class FriendClass
{
public:
	void Visit()
	{
		p = new Person();
		p->m_a = 10;
	}

	Person* p;
};

3.成员函数做友元
注意声明类和方法的顺序,否则访问不到私有成员

//前向声明依赖的类
class Person;

class FriendClass
{
public:
	//定义方法但不实现
	void Visit(Person& p);

	Person* p;
};

class Person
{
	//申明这个类的成员函数可以方法私有成员
	friend void FriendClass::Visit(Person& p);

private:
	int m_a;
};

// 实现友元方法
void FriendClass::Visit(Person& p) 
{
	p.m_a = 10;
}

运算符重载

加法重载

class Person
{
public:
	int m_a;

	//1.成员函数重载
	//Person operator+(Person& p)
	//{
	//	Person temp;
	//	temp.m_a = this->m_a + p.m_a;
	//	return temp;
	//}
};

//2.全局函数重载
Person operator+(Person& p1, Person& p2)
{
	Person temp;
	temp.m_a = p1.m_a + p2.m_a;
	return temp;
}

int main()
{
	Person p1;
	Person p2;
	Person P3 = p1 + p2;
}

左移运算符重载,可以配合友元使用

class Person
{
	friend ostream& operator<<(ostream& cout, Person& p);

public:
	Person(int a)
	{
		m_a = a;
	}

private:
	int m_a;
};

//只能利用全局函数重载左移运算符
ostream& operator<<(ostream& cout, Person& p)
{
	cout << "m_a: " << p.m_a << endl;
	return cout;
}

int main()
{
	Person p(10);
	cout << p << endl;
}

递增运算符重载

class MyInteger
{
	//前置++,返回引用,为了一直对一个数据操作
	MyInteger& operator++()
	{
		m_Num++;
		return *this;
	}

	//后置++,返回临时值,int 是个占位参数,用于区分前置和后置++
	MyInteger operator++(int)
	{
		//先记录当前值返回,然后加1
		MyInteger temp = *this;
		m_Num++;
		return temp;
	}

private:
	int m_Num;
};

赋值运算符重载

class Person
{
public:
	Person(int a)
	{
		m_a = new int(a);
	}

	//重载赋值运算符
	Person& operator=(Person& p)
	{
		//有属性在堆区,先释放,再深拷贝
		if (m_a != nullptr)
		{
			delete m_a;
			m_a = nullptr;
		}

		m_a = new int(*p.m_a);
		return *this;
	}

	int* m_a;
};

int main()
{
	Person p1(10);
	Person p2(20);
	Person p3(30);
	p3 = p2 = p1;
	cout << *p1.m_a << endl; //10
	cout << *p2.m_a << endl; //10
	cout << *p3.m_a << endl; //10
}

关系运算符重载(>, <, >=. <=, ==)

class Person
{
public:
	Person(string name, int age)
	{
		m_Name = name;
		m_Age = age;
	}

	//重载关系运算符
	bool operator==(Person& p)
	{
		return this->m_Name == p.m_Name && this->m_Age == p.m_Age;
	}

	string m_Name;
	int m_Age;
};

int main()
{
	Person p1("Tom", 10);
	Person p2("Tom", 10);
	if (p1 == p2) 
	{
		cout << "相等" << endl;
	}
	else
	{
		cout << "不相等" << endl;
	}
}

函数调用运算符()重载

由于重载后的使用方式非常像函数调用,因此也成为仿函数

class MyPrint
{
public:
	//重载函数调用运算符
	void operator()(string test)
	{
		cout << test << endl;
	}
};

int main()
{
	MyPrint m;
	m("hello");
}

继承

减少重复代码
在这里插入图片描述

class Base
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C; //私有成员也会继承下去
};

class Son : public Base
{
public:
	int m_D;
};

int main()
{
	//父类所有的非静态成员属性都会被子类继承,private 成员访问不到也会继承
	cout << sizeof(Son) << endl; //16
}

继承中的构造和析构顺序

构造顺序:先父类后子类
析构顺序:先子类后父类

多继承

实际开发中不建议使用

class Base1
{
public:
	Base1()
	{
		m_A = 20;
	}
	int m_A;
};

class Base2
{
public:
	Base2()
	{
		m_A = 30;
	}
	int m_A;
};

class Son : public Base1, public Base2
{
public:
	Son()
	{
		m_B = 10;
	}
	int m_B;
};

int main()
{
	Son s;
	cout << sizeof(Son) << endl; //12
	//父类中出现同名成员,加作用域区分
	cout << s.Base1::m_A << endl; //20
	cout << s.Base2::m_A << endl; //30
}

菱形继承

class Animal
{
public:
	int m_A;
};

//加 virtual 变成虚继承,公共父类为虚基类
//解决菱形继承导致的数据重复问题
class Sheep : virtual public Animal{};

class Tuo : virtual public Animal{};

//类内有个虚基类指针 vbptr, 指向虚基类表 vbtable,通过偏移值找到对应属性
class SheepTuo : public Sheep, public Tuo {};


int main()
{
	SheepTuo st;
	st.m_A = 10;
	cout << st.m_A << endl;		   //10
	cout << st.Sheep::m_A << endl; //10
	cout << st.Tuo::m_A << endl;   //10
}

多态

静态多态:函数重载,运算符重载。编译阶段确定函数地址
动态多态:子类覆写虚函数。运行时确定函数地址

class Animal
{
public:
	//虚函数
	virtual void Speak()
	{
		cout << "动物说话" << endl;
	}
};

class Cat : public Animal
{
public:
	//重写虚函数
	void Speak()
	{
		cout << "猫说话" << endl;
	}
};

void DoSpeak(Animal& animal)
{
	animal.Speak();
}

int main()
{
	Cat c;
	DoSpeak(c);
	cout << sizeof(Cat) << endl; //8,64位机器指针占8字节
}

原理:类内有虚函数指针 vfptr,指向虚函数表 vftable,表内记录虚函数地址。
虚函数表是编译时生成的,虚函数表指针是对象构造时生成。

在这里插入图片描述

纯虚函数

//有纯虚函数就是抽象类,无法实例化对象
class Animal
{
public:
	//纯虚函数,子类必须重写纯虚函数,否则也属于抽象类
	virtual void Speak() = 0;
};

虚析构,纯虚析构

class Animal
{
public:
	Animal()
	{
		cout << "Animal 构造" << endl;
	}

	//虚析构,父类指针释放子类对象时,调用子类的析构函数
	//virtual ~Animal()
	//{
	//	cout << "Animal 析构" << endl;
	//}

	//纯虚析构,有纯虚函数的类就是抽象类
	virtual ~Animal() = 0;
};

//纯虚析构的实现
Animal::~Animal()
{
	cout << "Animal 纯虚析构" << endl;
}

class Cat : public Animal
{
public:
	Cat(string name)
	{
		m_Name = new string(name);
		cout << *m_Name << "Cat 构造" << endl;
	}

	~Cat()
	{
		cout << "Cat 析构" << endl;
		if (m_Name != nullptr)
		{
			delete m_Name;
			m_Name = nullptr;
		}
	}

	string * m_Name;
};

int main()
{
	Animal* animal = new Cat("Tom");
	//Animal 不是虚析构的话,不会调用子类的析构函数
	delete animal;
}

文件操作

文件操作三大类:

  • ofstream 写操作
  • ifstream 读操作
  • fstream 读写操作
打开方式 解释
ios::in 为读文件而打开文件
ios::out 为写文件而打开文件
ios::ate 初始位置:文件尾
ios:app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方式

写文本文件

#include <iostream>
#include<fstream> //头文件
using namespace std;

int main()
{
	//创建流对象
	ofstream ofs;

	//指定打开方式
	ofs.open("test.txt", ios::out);

	//写内容
	ofs << "姓名: 张三" << endl;
	ofs << "年龄: 10" << endl;

	//关闭
	ofs.close();
}

读文本文件

#include <string>
#include <iostream>
#include<fstream>
using namespace std;

int main()
{
	//创建流对象
	ifstream ifs;

	//指定打开方式
	ifs.open("test.txt", ios::in);

	if (!ifs.is_open())
	{
		cout << "文件打开失败" << endl;
		ifs.close();
		return 0;
	}

	//读内容
	//第1种方式
	//char buf[1024] = { 0 };
	//while (ifs >> buf)
	//{
	//	cout << buf << endl;
	//}

	//第2种方式
	//char buf[1024] = { 0 };
	//while (ifs.getline(buf, sizeof(buf)))
	//{
	//	cout << buf << endl;
	//}

	//第3种方式,getline 全局函数
	string buf;
	while (getline(ifs, buf))
	{
		cout << buf << endl;
	}

	//第4种方式,不推荐
	//char c;
	//while ((c = ifs.get()) != EOF)
	//{
	//	cout << c;
	//}

	//关闭
	ifs.close();
}

写二进制文件

#include <string>
#include <iostream>
#include<fstream> //头文件
using namespace std;

class Person
{
public:
	char m_Name[64];
	int m_Age;
};

int main()
{
	//创建流对象
	fstream fs;

	//指定打开方式
	fs.open("person.txt", ios::out | ios::binary);

	//写内容
	Person p = { "张三", 20 };
	fs.write((const char*)&p, sizeof(p));

	//关闭
	fs.close();
}

读二进制文件

#include <string>
#include <iostream>
#include<fstream> //头文件
using namespace std;

class Person
{
public:
	char m_Name[64];
	int m_Age;
};

int main()
{
	//创建流对象
	ifstream ifs;

	//指定打开方式
	ifs.open("person.txt", ios::in | ios::binary);

	if (!ifs.is_open())
	{
		cout << "文件打开失败" << endl;
		ifs.close();
		return 0;
	}

	//写内容
	Person p;
	ifs.read((char*)&p, sizeof(p));
	cout << p.m_Name << " " << p.m_Age << endl;

	//关闭
	ifs.close();
}

模板

函数模板

template<typename T> //typename 可以替换成 class
void MySwap(T& a, T& b)
{
	T temp = a;
	a = b;
	b = temp;
}

int main()
{
	int a = 10;
	int b = 20;
	//自动类型推导
	MySwap(a, b);

	//显示指定类型
	MySwap<int>(a, b);
}

普通函数与函数模板区别:

  • 普通函数调用时可以发生自动类型转换(隐式类型转换,比如 char 转 int)
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换,如果利用显示指定类型的方式,可以发生隐式类型转换

模板局限性:自定义数据类型不一定能跑通

class Person
{
public:
	Person(string name, int age)
	{
		this->m_Name = name;
		this->m_Age = age;
	}

	string m_Name;
	int m_Age;
};

template<typename T>
bool MyCompare(T& a, T& b)
{
	return a == b;
}

//具体化模板,解决自定义类型的通用化
template<> bool MyCompare(Person & p1, Person& p2)
{
	return p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age;;
}

int main()
{
	Person p1("Tom", 10);
	Person p2("Tom", 10);
	if (MyCompare(p1, p2))
	{
		cout << "相等" << endl;
	}
	else 
	{
		cout << "不相等" << endl;
	}
}

类模板

类模板与函数模板区别主要有两点:

  1. 类模板必须显示指定类型
  2. 类模板在模板参数列表中可以有默认参数
template<typename NameType, typename AgeType = int>
class Person
{
public:
	Person(NameType name, AgeType age)
	{
		this->m_Name = name;
		this->m_Age = age;
	}

	NameType m_Name;
	AgeType m_Age;
};

int main()
{
	Person<string, int> p1("张三", 10);
	Person<string> p2("李四", 10);  //int可以省略
}

类模板中成员函数和通类中成员函数创建时机是有区别的:

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建
template<typename T>
class MyClass
{
public:
	//类模板中的成员函数,不是一开始就创建,而是调用时生成的
	void Show()
	{
		obj.Test();
	}

	T obj
};

class Person
{
public:
	void Test()
	{
		cout << "test" << endl;
	}
};

int main()
{
	MyClass<Person> m;
	m.Show();
}

类模板与继承

template<typename T>
class Base
{
public:
	T m;
};

//子类继承需要指定类型,知道类型才能给子类分配内存
class Son1 : public Base<int>{};

//如果想要灵活指定父类中的T类型,子类也需要变成类模板
template<typename T1, typename T2>
class Son2 : public Base<T1> 
{
public:
	Son2()
	{
		cout << typeid(T1).name() << endl;
	}
	T2 obj;
};

类模板分文件编写

如果类模板写在其他文件中,引用其头文件 .h 文件,链接不到模板函数的具体实现

解决办法:

  1. 直接引用源文件 .cpp,如 #include “person.cpp”,一般不这样做
  2. 将 .h 和 .cpp 文件写到一起,文件后缀改成 .hpp

创建 person.hpp 文件

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

template<typename T1, typename T2>
class Person
{
public:
	Person(T1 name, T2 age);

	T1 m_Name;
	T2 m_Age;
};

template<typename T1, typename T2>
Person<T1, T2>::Person(T1 name, T2 age)
{
	this->m_Name = name;
	this->m_Age = age;
}

主文件中引用

#include "person.hpp"

int main()
{
	Person<string, int> p("Jerry", 10);
}

类型转换

dynamic_cast 动态转换

只能用于对象的指针或引用的转换,完成父类向子类的转换,父类中需要有虚函数,子类转父类是一定可以转换成功的,用普通的类型转换即可,不需要使用 dynamic_cast。

class Base
{
public:
    virtual ~Base(){}
};

class Son : public Base{};

int main()
{
    Base b1;
    Son* s1 = dynamic_cast<Son*>(&b1); //类型不兼容
    cout << (s1 == nullptr) << endl;

    Base* b2 = new Base();
    try
    {
        Son& s2 = dynamic_cast<Son&>(*b2); //类型不兼容
    }
    catch (const std::bad_cast& e)
    {
        cerr << "转换失败" << e.what() << endl;
    }

    Base* b3 = new Son();
    Son* s3 = dynamic_cast<Son*>(b3); //成功转换
    cout << s3<< endl;
    
    return 0;
}

编译时检查:如果父类没有虚函数就会报错
运行时检查:转换时会检查 RTTI(Run Time Type Information)判断是否可以转换,b1 是 Base 类型,与目标类型不兼容,返回 nullptr,b2 同理,但引用转换失败会抛异常,b3 实际指向子类对象,类型匹配,可以转换成功。

static_cast

只要两个类型是兼容的就可以转换,所有的隐式转换都可以使用 static_cast 替代,只做编译时检查

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

int main()
{
    Base* b1 = new Base();
    Son* s1 = static_cast<Son*>(b1);
    cout << s1 << endl;

    Son* s2 = new Son();
    Base* b2= static_cast<Base*>(s2);
    cout << b2 << endl;

    Other* o1 = new Other();
    Son* s3 = static_cast<Son*>(o1); //编译错误

    return 0;
}

子类转父类,或者父类转子类都行,不需要父类是多态类

reinterpret_cast 重解释转换

允许将一个类型的指针转换成另一个类型指针,而不会检查类型是否有效

int* i = new int(2);
float* f = reinterpret_cast<float*>(i);

const_cast 常量转换

主要用于常量转非常量,转换的类型必须是指针或者引用

const char* str = "hello";
char* str2 = const_cast<char*>(str);
str2[0] = 'w'; //内容不可修改,运行时报错

STL

六大组件:容器,算法,迭代器,仿函数,适配器,空间适配器

  1. 容器:各种数据结构,存储数据
    序列容器​​:元素顺序排列,如 vector, list, deque
    ​​关联容器​​:通过键(key)快速查找元素
    有序关联容器:基于红黑树实现,如 set, map
    ​​无序关联容器​​:基于哈希表实现,如 unordered_set, unordered_map。
  2. 算法:各种常用算法
  3. 迭代器:提供一种统一的方式遍历容器中的元素,容器和算法之间的胶合剂
  4. 仿函数:一个类将()重载为成员函数,行为类似函数
  5. 适配器:修改现有组件接口,使其适用于不同场景,如 stack(基于 deque 或 list 实现)
  6. 内存分配器:管理容器的内存分配和释放

vector 可变长度的数组

在这里插入图片描述

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void MyPrint(int val)
{
	cout << val << " ";
}

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	
	//通过迭代器遍历元素
	for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
	{
		cout << *it << endl;
	}

	//使用 STL 提供的遍历算法
	for_each(v.begin(), v.end(), MyPrint);

	cout << v1.size() << " " << v1.capacity() << endl; //10 13
	//改变数组大小为n,使得 size = n,不影响 capacity
	v1.resize(5);
	cout << v1.size() << " " << v1.capacity() << endl; //5 13

	//数据量较大时,预分配内存,防止多次扩容
	v.reserve(10000);
}

互换容器

void MyPrint(vector<int>& v)
{
	for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
	{
		cout << *it << " ";
	}
	cout << endl;
}

int main()
{
	vector<int> v1;
	for (int i = 0; i < 10; i++)
	{
		v1.push_back(i);
	}

	vector<int> v2;
	for (int i = 10; i < 20; i++)
	{
		v2.push_back(i);
	}

	//交互两个数组内容
	v1.swap(v2);

	MyPrint(v1);
	MyPrint(v2);

	cout << v1.size() << " " << v1.capacity() << endl; //10 13
	//收缩内存,vector<int>(v1) 是一个匿名对象,使用 v1 的元素初始化,
	// 再和 v1 交换,匿名对象自动回收
	vector<int>(v1).swap(v1);
	cout << v1.size() << " " << v1.capacity() << endl; //10 10
}

deque 双端队列

在这里插入图片描述

内部原理:

在这里插入图片描述

deque 采用分段连续的内存空间来存储元素,内部有个中控器记录每个缓冲区的地址,缓冲区中存放真实数据

void MyPrint(const deque<int>& d)
{
	//只读迭代器
	for (deque<int>::const_iterator it = d.begin(); it != d.end(); it++)
	{
		cout << *it << " ";
	}
	cout << endl;
}

int main()
{
	deque<int> d;
	for (int i = 0; i < 10; i++)
	{
		d.push_back(i);
	}
	MyPrint(d);
}

stack 栈

在这里插入图片描述

栈中只有顶端的元素才可以被外界使用,因此栈不允许有遍历行为

queue 队列

在这里插入图片描述

list 双向链表

在这里插入图片描述

pair 队组

只含有两个元素,可以看作是只有两个元素的结构体

pair<string, int> p1("zhangsan", 12);
pair<string, int> p2 = make_pair("lisi", 18);

set,multiset

插入元素后自动排序
set 不允许有重复元素,multiset 允许有重复元素

set<int> s;
s.insert(10);
s.insert(20);
s.insert(30);
set<int>::iterator pos = s.find(20);
if (pos != s.end())
{
	cout << "找到元素 " << *pos << endl;
}

//返回队组,迭代器,bool 是否插入成功
pair<set<int>::iterator, bool> res = s.insert(40);
if (res.second) 
{
	cout << "插入成功 " << *res.first << endl;
}

set 默认从小到大排序,使用仿函数自定义排序规则,自定义的数据类型必须定义排序规则

class MyCompare
{
public:
	bool operator()(int a, int b) const
	{
		return a > b;
	}
};

int main()
{
	set<int, MyCompare> s;
	s.insert(10);
	s.insert(20);
	s.insert(30);

	for (set<int, MyCompare>::iterator it = s.begin(); it != s.end(); it++)
	{
		cout << *it << " ";
	}
	cout << endl;
}

map,multimap

map 中所有元素都是 pair,第一个是 key,第二个是 value,根据 key 排序

map<int, int> m;
m.insert(pair<int, int>(1, 10));
m.insert(pair<int, int>(3, 30));
m[2] = 20;
m[4] = 40;

for (map<int, int>::iterator it = m.begin(); it != m.end(); it++)
{
	cout << it->first << " " << it->second << endl;
}

string

string 和 char* 区别:
char* 是一个指针。
string 是一个类,类内部封装了char*,管理这个字符串,内部分装了操作字符串的方法。

内建函数对象

#include <functional>
using namespace std;

int main()
{
	vector<int> v;
	v.push_back(50);
	v.push_back(60);
	v.push_back(30);

	//使用内置函数对象
	sort(v.begin(), v.end(), greater<int>());

	for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
	{
		cout << *it << " ";
	}
	cout << endl;
}

智能指针

智能指针是一个类对象,内部封装了指针,离开作用域后调用析构函数,释放指向的内存

unique_ptr

对管理的资源具有独占性,两个 unique_ptr 不能指向同一个对象

#include <iostream>
#include <memory>
using namespace std;
class A
{
public :
	A(int n)
	{
		this->num = new int(n);
	}

	int GetNum()
	{
		return *num;
	}

	int* num;

	~A()
	{
		if (num != nullptr)
		{
			cout << "析构" << *num << endl;
			delete num;
			num = nullptr;
		}
	}
};

int main()
{
	{
		unique_ptr<A> ptr1(new A(1));
		cout << *ptr1->num << endl;
		cout << ptr1->GetNum() << endl;
	}
	//离开作用域调用析构

	unique_ptr<A> ptr2 = make_unique<A>(2);
	//赋值空指针,调用析构
	ptr2 = nullptr;

	unique_ptr<A> ptr3 = make_unique<A>(3);
	//ptr3调用析构
	ptr3.reset(new A(4));

	unique_ptr<A> ptr4 = make_unique<A>(5);
	unique_ptr<A> ptr5 = make_unique<A>(6);
	//ptr5调用析构
	ptr5 = move(ptr4);

	cout << "-------" << endl;
}

输出

1
1
析构1
析构2
析构3
析构6
-------
析构5
析构4

shared_ptr,weak_ptr

多个 shared_ptr 指向同一个对象时,有共同的引用计数记录指针数量,当引用计数为 0 时,释放指向的内存

//创建对象,调用构造函数,引用+1
shared_ptr<A> ptr1 = make_shared<A>(1);
//赋值对象,引用+1
shared_ptr<A> ptr2 = ptr1;
//拷贝构造,引用+1
shared_ptr<A> ptr3(ptr2);
cout << ptr1.use_count() << endl; //3

循环引用问题:对象 A 的 shared_ptr 指向 B,B 的 shared_ptr 指向 A,形成循环引用,谁也释放不了。

weak_ptr 需要结合 shared_ptr 使用,解决循环引用问题。 weak_ptr 指向对象,只观察对象,但不增加引用计数,和 shared_ptr 之间可以相互转化,shared_ptr 可以直接赋值给它,它可以通过调用 lock 函数来获得 shared_ptr。

在这里插入图片描述

weak_ptr<A> wPtr;
{
	shared_ptr<A> sPtr = make_shared<A>(1);
	wPtr = sPtr;
	cout << "作用域内:" << wPtr.use_count() << endl; //1
}
cout << "作用域外:" << wPtr.use_count() << endl; //0
cout << "作用域外:" << wPtr.expired() << endl; //1

Lambda 表达式

Lambda 表达式用于快速定义一个匿名对象,也被称为闭包。

[捕获外部变量](参数列表) 可选限定符->返回类型 
{
	//函数代码
}
int x = 3;
float y = 2;

//x按值捕获,函数内部不能修改,y按引用捕获,函数内部可以修改
auto p = [x, &y](int a, int b)->float
    {
        y++;
        return x * y + a * b;
    };

cout << p(1, 2) << endl;

//加 mutable 限定符,捕获的变量都可以修改
auto p2 = [x, y](int a, int b) mutable->float
    {
        return x * y + a * b;
    };

// [=] 表示默认按值捕获
auto p3 = [=](int a, int b)->float
    {
        return x * y + a * b;
    };

// [&] 表示默认按引用捕获
auto p4 = [&](int a, int b)->float
    {
        return x * y + a * b;
    };

// 添加特例
auto p5 = [=, &y](int a, int b)->float
    {
        return x * y + a * b;
    };

//创建一个简单函数,作为另一个函数的参数
vector<int> nums{11,5,7,6,1,9,14,68,13,32};
sort(nums.begin(), nums.end(), [](int a, int b){return a < b; });

//泛型lambda
auto f = [](auto x, auto y)(return x + y;);

网站公告

今日签到

点亮在社区的每一天
去签到