//
// Created by zhangy233 on 2021/1/13.
//
#include <iostream>
#include <string>
#include <swap.h>
#include <vector>
#include <list>
#include <map>
#include <algorithm>
#include <numeric>
#include <functional>
#include <sstream>
#include <fstream>
#include <Students.h> // 封装文件中的类
using i32 = int;
using u32 = unsigned int;
using i64 = long int;
using u64 = unsigned long int;
using f32 = float;
// 函数的定义(函数在调用之前,需要提前定义)
void say_hello(string name = "佚名")
{
cout << "我叫 '" << name << "'" << endl;
}
int sum(int num1,int num2){
return num1 > num2 ? num1:num2;
}
// 函数的声明(声明一个函数,声明函数之后即可先调用,再定义)
int sub(int a, int b);
void swap2(int *p1, int *p2){
// 通过指针的地址,交换两个变量的值
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void sorted(int l, int *p){
cout << *p << endl; // 打印的是指针 p 中保存的 首元素的地址中的内容
for (int i = 0; i < l - 1; ++i) {
for (int j = 0; j < l - i - 1; ++j) {
if (p[j] > p[j +1]){
int temp = p[j];
p[j] = p[j + 1];
p[j + 1] = temp;
}
}
}
}
void stu_1() {
printf("stu_1: 变量与运算符\n");
// 查看变量的内存地址(使用 & 来查看变量地址)
int a, b;
cout << "变量 a 的内存地址为: " << &a << endl;
// 定义变量起别名(在 引用 部分会进行详细整理) 使用 type &a=b; name1是name2的引用, 如果是int &a;这样就是错的,没有指定a代表哪一个变量,一个别名只能对应一个原始变量,但是一个原始变量可以有多个别名,而且别名也可以有自己的别名
int c = 10;
int d = c;
int& e = c;
cout << "&a: " << &a <<endl;
cout << "&b: " << &b <<endl;
cout << "&c: " << &c <<endl;
int age = 18;
float height = 175.4;
char char_a = 'a'; // 字符型只占用1个字节,用来存储对应的ASCII码
std::cout << char_a << std::endl;
std::cout << sizeof(char_a) << std::endl;
std::cout << (int)char_a << std::endl; // 查看字符型对应的ASCII码值
std::cout << int(char_a) << std::endl;
std::cout << sizeof(123) << std::endl;
std::cout << "Hello\tWorld!" << std::endl; // \t 是制表符
std::cout << "Hel\tWorld!" << std::endl;
std::cout << "Helllllo\tWorld!" << std::endl;
std::cout << "Hello, \n \\ World!" << std::endl; // \n 换行, \\转义字符
cout << 2233 <<endl;
cout << "true" << true << endl; // 布尔值只占用1个字节, true 为 非0的值都为真
cout << "false" << false << endl; // 布尔值只占用1个字节 false 为 0
cout << "sizeof(true)" << sizeof(true) << endl; // 布尔值只占用1个字节
// 字符串型(注意,需要使用双引号)
char str1[] = "你好啊 \n"; // c语言风格字符串数组
char *str2 = "hello"; // c语言风格字符串
string str3 = "我很好"; // c++创建字符串
cout << str1 << str2 << endl;
// // 数据的输入
// //1. 整型
// int i1;
// cout << "请给变量i1赋值数字: " << endl;
// cin >> i1;
// cout << "您输入的为: " << i1 << endl;
//
// //2. 浮点型
// float f;
// cout << "请给变量b赋值小数" << endl;
// cin >> f;
// cout << "您输入的为: " << f << endl;
//
// //3. 字符型
// char ch;
// cout << "请给变量ch赋值字符" << endl;
// cin >> ch;
// cout << "您输入的为: " << ch << endl;
//
// //4. 字符串型
// string str;
// cout << "请给变量str赋值字符串" << endl;
// cin >> str;
// cout << "您输入的为: " << str << endl;
//
// //5. bool类型(非0的值都为true)
// bool bo = false;
// cout << "请给变量bo赋值 bool 类型" << endl;
// cin >> bo;
// cout << "您输入的为: " << bo << endl;
// 算术运算符
int g = 10;
int h = 3;
cout << "加号 " << 10 + 3 <<endl;
cout << "减号 " << 10 - 3 << endl;
cout << "乘号 " << 10 * 3 << endl;
cout << "除号 " << 10 / 3 << endl; // 两个int 类型相除 只能得到int类型的结果!!
cout << "除号 " << 10 / 3.0 << endl; // 如果存在浮点数相除,则就能得到浮点数的结果
cout << "除号 " << 0.5 / 0.25 << endl; // 如果浮点数相除,能整除的话可能得到浮点数或者整数
cout << "取模 " << 18 % 4 << endl;
int num;
// 前置递增表达式(先自增+1 然后返回, 得到11, 再进行后续表达式的计算)
int y = 10;
num = ++y *10; // 11 * 10
cout << num << " " << y << endl;
// 后置递增表达式(优先进行表达式的计算得到100,再自增+1)
int u = 10;
num = u++ *10;
cout << num << " " << u << endl;
// // 赋值运算符(+= -= *= /= %=)
// int a = 0;
// a += 1;
// cout << "+= " << a << endl;
// 比较运算符(== != <= >=)
// int a = 10;
// int b = 20;
// cout << (a==b) << endl; // 由于优先级运算符,所以先括起来,再换行
// cout << (a!=b) << endl;
// cout << (a>=b) << endl;
// cout << (a<=b) << endl;
// // 逻辑运算符(&&与 ||或 !非)
// bool bo = false;
// bo = !bo;
// cout << bo << endl;
// bool flag = 1>2 && 2>1;
// cout << flag << endl;
// int n = 0 || 3;
// cout << n << endl;
}
void stu_2(){
printf("stu_2: 分支判断与循环\n");
// 分支判断
// int ret;
// cout << "请输入分数" << endl;
// cin >> ret;
// cout << "您输入的是: " << ret << "分" << endl;
// if(ret >= 600){
// cout << "您合格了" << endl;
// } else if(ret < 600 && ret > 500){
// cout << "加把劲就合格了" << endl;
// } else{
// cout << "您未及格" << endl;
// };
// switch 判断需要使用return 或者 break 进行终止
// char a;
// cout << "请输入字母" << endl;
// cin >> a;
// switch (a) {
// case 'a':{
// cout << "变量中存储的是" << a << endl;
// return 0;
// }
// case 'b':{
// cout << "变量中存储的是" << a << endl;
// break;
// }
// case 'c':{
// cout << "变量中存储的是" << a << endl;
// break;
// }
// case 'd':{
// cout << "变量中存储的是" << a << endl;
// break;
// }
// };
// // 循环
// int sum = 0;
// while (sum <= 10000000){
// sum ++;
// if(sum % 2 == 0){
// continue;
// }
// cout << sum << endl;
// if(sum >= 1000){
// break;
// }
// };
// // do while循环是至少执行一遍之后开始循环
// int a = 0;
// cout << "开始循环: " << a << endl; //
// do {
// a ++;
// cout << "执行完毕: " << a << endl; //
// }while (a == 0);
// for循环
// for (int i = 0; i < 100; ++i) {
// cout << i << endl;
// if(i == 50){
// break;
// }
// }
// // goto语句
// for (int i = 0; i < 10; ++i) {
// for (int j = 0; j < 10; ++j) {
// if(i > 5){
// goto flag;
// }
// cout << (i *j) << endl;
// }
// }
// cout << "循环结束了" << endl; // 这一句话没有打印,因为goto直接跳转到下面了
// flag: // 创建 goto 的 flag
// cout << "运行结束" << endl;
}
void stu_3() {
printf("stu_3: 数组\n");
// 数组(放在一块连续的内存空间中)
// 第一种,只指定数组长度, 数组的默认值都为0, 对于越界的读取出来会出现错乱
cout << "第一种" << endl;
int arr1[5];
arr1[1] = 1;
cout << arr1[0] << endl;
cout << arr1[1] << endl;
cout << arr1[2] << endl;
cout << arr1[3] << endl;
cout << "arr1[100], 超过了5 之后: " << arr1[100] << endl; // 越界了
// 第二种
cout << "第二种" << endl;
int arr2[5] = {0, 1, 2}; // 如果没有指定数组长度,但是没有填充完,默认用 0 补充
cout << arr2[2] << endl;
cout << arr2[4] << endl;
// 第三种不指定数组长度,自动判断长度
cout << "第三种" << endl;
int arr3[] = {1, 2, 5, 7};
//int arr3[]; // 报错,这种方式需要设置一个初始的值,
cout << arr3[2] << endl;
cout << arr3[4] << endl;
arr3[4] = 9; // 手动根据下标设置元素
cout << arr3[4] << endl;
arr3[10] = 10; // 手动设置一个下标
cout << arr3[8] << endl; // 下标中间如果没有数据, 则用0补齐
// 关于数组的数组名('数组名'是一个指向 &'数组名'[0] 的 指针常量 无法被修改, 即数组第一个元素地址)
int arr[5] = {10,20,30,40,50};
int* p = arr;
cout << *arr << endl; // 默认是数组的元素首地址
cout << "(&(arr[0]) == arr): "<< (&(arr[0]) == arr) << endl; // 为 true
cout << (arr + 0 == arr) << endl; // 数组偏移 0 个位置, 和数组相同
cout << (p == arr) << endl; // 为 true
cout << *(arr + 0) << endl;
cout << *(arr + 1) << endl;
////// arr = {}; // 无法修改因为数组名是一个指针常量 * const arr, 无法修改
*arr = 99; // 可以修改指针保存内容所指向的地址的内容
cout << arr[0] << "," << arr[1] << "," << arr[2] << "," << arr[3] << "," << arr[4] << "," << endl;
// 查看数组中每个元素的内存地址
cout << &arr[0] << "," << &arr[1] << "," << &arr[2] << "," << &arr[3] << "," << &arr[4] << "," << endl;
// // 一维数组数组名
int arr_1[] = {1, 2, 3, 4, 5};
cout << sizeof(arr) << endl; // 查看数组的总大小, 因为int占用4个字节, 数组中目前有5个元素,所以占用了20个字节
cout << "数组元素个数: " << sizeof(arr_1) / sizeof(arr_1[0]) << endl;
cout << "数组在内存中的首地址: " << arr_1 << endl;
cout << "数组0号下标的元素地址: " << &arr_1[0] << endl; // 0号下标的元素和arr的内存地址一样
cout << "数组1号下标的元素地址: " << &arr_1[1] << endl;
// 数组案例1 (找最大数)
int num[] = {1, 2, 1, 9, 6};
int flag = 0;
for (int i = 0; i < sizeof(num) / sizeof(num[0]); ++i) {
if(num[i] > flag){
flag = num[i];
}
}
cout << "最大的数是" << flag << endl;
// 数组案例2 (数组逆序)
int start = 0;
int end = sizeof(num) / sizeof(num[0]);
while (start <= end){
int flag = num[start];
num[start] = num[end];
num[end] = flag;
start ++,
end --;
}
cout << num << endl;
// 数组案例3 (冒泡排序)
cout << "冒泡排序: " << endl;
int arrPao[] = {3, 1, 2, 1, 4, 3, 7};
int len = sizeof(arrPao) / sizeof(arrPao[0]);
for (int i = 0; i < len; i++) {
for (int j = 0; j < len - i - 1; j++) {
int now = arrPao[j];
int next = arrPao[j + 1];
if (now > next) {
arrPao[j + 1] = now;
arrPao[j] = next;
}
}
}
for (int i = 0; i < len; ++i) {
cout << arrPao[i] << endl;
}
// 二维数组
// 1. 指定行列, 创建一个2行3列的数组(如果不指定元素,则默认补0)
int arr4[2][3];
cout << arr4[0][1] << endl;
cout << arr4[1][2] << endl;
cout << arr4[2][3] << endl;
// 2. 指定行列以及对应的值, 创建一个3行4列的数组
int arr5[3][4] = {{1, 2, 3, 4}, {2, 3, 4, 5}, {4, 5, 6, 7,}};
// 3. 指定行列自动切分对应的值
int arr6[2][3] = {0, 1, 2, 3, 4, 5}; // 自动把内部元素切分成两行三列 {{1, 2, 3}, {4, 5, 6}}
cout << arr6[0][0] << endl;
cout << arr6[1][0] << endl;
// 3. 指定列,但是不指定行,会自动切分元素自动切分对应的值
int arr7[][3] = {0, 1, 2, 3, 4, 5, 6, }; // 自动把内部元素切分成两行三列 {{1, 2, 3}, {4, 5, 6}}
cout << arr7[0][0] << endl;
cout << arr7[1][0] << endl;
cout << arr7[2][0] << endl;
cout << arr7[2][1] << endl; // 访问不存在的元素则补0
// 二维数组名
double arr8[3][4] = {{1, 2, 3, 4}, {2, 3, 4, 5}, {4, 5, 6, 7,}};
cout << sizeof(arr8) << endl; // 查看数组的总大小
cout << sizeof(arr8[0]) << endl; // 查看数组中的元素的大小
}
void stu_4(){
printf("stu_4: 函数\n");
// 函数的调用
say_hello(); // 如果不传姓名,则使用函数参数的默认值
cout << sum(1, 2) << endl;
cout << sub(1, 2) << endl; // 由于事先声明了函数所以可以直接调用
// 引用外部文件内的函数
// 1. 创建.h后缀名的头文件
// 2. 创建.cpp后缀名的源文件
// 3. 在头文件中写函数的声明
// 4. 在源文件中写函数的定义
// 调用时,在需要调用的文件中 使用 #include "头文件名" 即可引用
swap(1, 2);
}
// 函数在调用之后才定义, 不报错是因为在上方已经事先声明了会有此函数
int sub(int a, int b){
return a + b;
};
void stu_5(){
printf("stu_5: 指针\n");
// 指针(指针在 32 位操作系统中,占 4 个字节, 64位操作系统中占 8 个字节, 指针保存的是变量的地址)
int a = 10;
int *p;
p = &a; // 创建一个指针 p, 让其保存变量 a 的内存地址, 注意 int* p 意味着这个 p 指针保存的内容指向的地址的内容也需要为int
// p = &'1'; // 出错, 验证上面的注释
cout << "&a: " << &a << endl; // 打印 a 的内存地址(和 p 相同)
cout << "p: " << p << endl; // 打印指针 p 保存的内容
cout << "*p: " << *p << endl; // 打印指针 p 内容所指的地址的内容
// 空指针(指针变量指向内存中编号为0的空间, 是无法被访问的, 一般用来初始化指针变量, 注意 0~255之间的内存是无法访问的, 是由系统占用的)
int *p_1 = NULL;
int *p_3 = nullptr; // 目前推荐使用 nullptr, 这是一个c++ 真正意义上的空指针类型
// cout << *p_1 << endl; // 无法访问
// 野指针(指针变量指向了一块非法的内存空间,无法被访问)
int *p_2 = (int *)0x0100;
// cout << *p_2 << endl; // 报错了
// 修改指针(修改指针所保存内容指向地址的内容,不会改变原变量的地址)
// int *o; // 创建一个指针o
// int b = 20; // 创建一个变量
// o = &b; // 让指针 o 保存变量 b 的内存地址
// cout << "修改前 b 内容: " << b << ", 修改前 b 地址: " << &b << endl;
// *o = 28; // 修改指针 o 保存的内容所指地址的内容为 28
// cout << "修改后 b 内容: " << b << ", 修改后 b 地址: " << &b << endl; // b的地址没有发生改变
// 指针常量(指针保存的内容无法修改,但指针保存的内容所指向的地址的内容可以修改)
// 方便记忆1.0
// 其实可以从右往左读遇到p就替换成“p is a ”遇到*就替换成“point to”, const int*p就可以读作p是一个指向整型常量的指针, int *const p读作p是一个常量指针,指向整型
// 方便记忆2.0
// const int p; // p 为常量,初始化后不可更改
// const int *p; // *p 为常量,不能通过*p改变它指向的内容
// int const *p; // *p 为常量,同上
// int*const p; // p 为常量,初始化后不能再指向其它内容
// int a = 10;
// int b = 20;
// int * const p = &a; // 创建一个指针常量p, p保存的内容无法改变
//// p = &b; // 无法再次修改,编译不通过
// cout << "修改前 a 内容: " << a << ", 修改前 a 地址: " << &a << endl;
// *p = b; // 修改指针保存的内容所指的地址的内容为变量 b 的内容
// cout << "修改后 a 内容: " << a << ", 修改后 a 地址: " << &a << endl;
// 常量指针1 (指针保存的内容可以修改, 但是指针保存内容所指向的地址的内容无法修改)
// int a = 10;
// int b = 20;
// int const * p = &a; // 创建常量指针
//// *p = b; // 无法修改
// cout << "修改前 p 内容: " << p << ", 修改前 p 内容指向地址的内容: " << *p << endl;
// p = &b;
// cout << "修改后 p 内容: " << p << ", 修改后 p 内容指向地址的内容: " << *p << endl;
// TODO 下面 两种常量指针 都是一样的意思 累心声明放在那里都无所谓
// int d = 123;
// int const *b = &d;
// const int *c = &d;
// 二级指针(指针的指针)
// int a = 10;
// cout << "&a: " << &a << endl;
// cout << "a: " << a << endl;
// int* p1 = &a;
// cout << "*p1: " << *p1 << endl;
// cout << "p1: " << p1 << endl;
// int **p2 = &p1;
// cout << "**p2: " << **p2 << endl;
// cout << "*p2: " << *p2 << endl;
// cout << "p2: " << p2 << endl;
// // 数组指针(数组的指针)
// // 通过数组名访问元素
int arr[5] = {11,22,33,44,55};
cout << "*arr " << *arr << endl; // 默认是数组的元素首地址
cout << arr[0] << endl;
cout << (&arr[0] == arr) << endl; // 默认是数组的元素首地址
cout << *(arr + 0) << endl;
// // 通过指针访问数组元素
// int *p = arr; // 使用数组名将数组元素的首地址赋值给指针 p, 注意不需要使用 & 因为本身数组名是指针常量保存的是数组的元素首地址
// cout << p[0] << endl;
// cout << p[1] << endl;
// cout << *(p + 1) << endl;
// 数组名与指针变量的区别
// arr++; // 报错 数组名是一个指针常量, 保存的是第一个元素的(内容的指向地址),
// 数组指针的自增操作 (数组指针,指向了数组的第一个地址, 可以通过解引用得到其第一个值, 当指针进行自增时, 指针会偏移 4 个字节)
// int arr[] = {2,3};
// int * p_arr = arr;
// cout << "指针自增前: " << *p_arr << endl;
// p_arr++; // 指针是指针变量 所以允许改变自增
// cout << "指针自增后: " << *p_arr << endl;
// p_arr++; // 越界了
// cout << "指针自增后: " << *p_arr << endl; // 越界了
// // 指针数组访问元素
// int arr[] = {1, 2, 3, 4};
// int *arr_ptr = arr;
// cout << arr[1] << endl;
// cout << arr_ptr[1] << endl;
// 指针与函数 (通过指针直接交换两个变量的地址)
// int a = 10;
// int b = 20;
// cout << "a: " << a << ", b: " << b <<endl;
// swap2(&a, &b); // 直接传递地址,可以交换变量
// cout << "a: " << a << ", b: " << b <<endl;
// 利用指针和函数实现冒泡排序
int arr1[] = {2,3,4,1,2,5,4,9,1,2};
int *d = arr1;
cout << (d == arr1) << endl; // true
cout << *d << endl;
cout << *arr1 << endl;
int len = sizeof(arr1) / sizeof(arr1[0]);
sorted(len, arr1);
for (int i = 0; i < len; ++i) {
cout << arr1[i] << ", ";
}
}
void stu_6(){
printf("stu_6: 结构体\n");;
// 结构体基本概念(解构体和类相同, 只有细微差别)
// 1. 自定义数据类型(一些类型集合组成的一个类型)
struct Student{
string name;
int age;
Student(){ // 无参构造函数
cout << "Student 被创建了" << endl;
}
Student(string name, int age):name(name),age(age) // 有参构造函数, 并使用初始化列表
{
cout << "Student 被创建了" << endl;
}
} s3; // 创建结构体的时候可以顺便创建一个变量为 s3(很少这样使用)
s3.name = "张三"; // 给上面顺便创建的变量设置值, 如果没有值, 则会使用默认值
cout << "s3.age: " << s3.age << endl; // int类型的默认值为随机数
// 2. 通过学生类型创建具体学生(创建变量的时候 struct 可以省略)
// 2.1 struct Student s1
Student s1;
s1.name = "小明"; // 给属性赋值
s1.age = 18; // 给属性赋值
cout << s1.name << " " << s1.age << endl;
// 2.2 struct Student s2 = {...} 在创建结构体的时候传入参数
struct Student s2{"张三", 19};
cout << s2.name << " " << s2.age << endl;
// 2.3 在创建结构体的时候顺便创建一个变量
s3.name = "小芳";
s3.age = 17;
cout << s3.name << " " << s3.age << endl;
// 结构体数组(创建一个存放结构体的数组)
Student My_Students[3] {{"张三", 20},
{"王二", 23},
{"李四", 18}};
cout << My_Students[0].name << endl;
// 3. 结构体指针
// 结构体指针无法通过 "." 访问到指针类型内的属性, 需要通过箭头 "->" 来访问
Student s4 = {"张三", 27};
Student *stu_ptr = &s4;
cout << "结构体指针类型通过箭头 -> 访问指针内的属性: " << stu_ptr -> name << " " << stu_ptr -> age << endl;
// 4. 复合结构体(一个结构体中的字段可以使用另一个结构体类型)
struct Teacher{
string name; // 老师的名字
Student sArray[3]; // 老师的学生
};
Teacher t1 = {
"王老师",{
{"小明", 12},
{"小亮", 14},
}
};
// 5. 结构体作为地址传递
// 形参传递 一般函数内修改, 不会影响到外部的变量, COPY 了一份新的变量到函数内部(和py js 都不同, 与rust 类同)
// 指针传递, 会同步影响到外部, 性能更快因为没有发生 COPY, 传递的仅仅是个指针
Student s5 = {
"小张",
13
};
}
struct Student{
string name;
int age;
};
void print_stu1(Student s1){
s1.age = 100;
cout << s1.name << " " << s1.age << endl;
};
void print_stu2(Student * s1){
s1 -> age = 100;
cout << s1->name << " " << s1->age << endl;
};
// 全局变量
int a_g = 10;
// 全局静态变量
static int s_a_g = 10;
// 全局常量
const int c_a_g = 10;
int * func_1(){
int a = 123;
return &a;
}
int* func_2(){
int* a = new int(123);
return a;
}
void stu_7(){
printf("stu_5: 内存4大区\n");
// 代码区, 数据区, 栈区, 堆区,
// 栈区: 由编译器在需要的时候分配, 在不需要的时候自动清除的变量的存储区
// 堆区: 由程序员分配释放, 若程序员不释放, 程序结束时可能由OS回收
// 数据区: 包括静态全局区和静态区
// 代码区: 包括只读存储区和文本区, 其中只读存储区存储字符串常量, 就是常量区, 文本区存储程序的机器代码
// 局部常量和局部变量是分配在一起的
// 全局变量, 全局常量,字符串常量, 静态变量是分配在一起的
// 局部变量
int a_l = 10;
// 局部静态变量
static int s_a_l = 10;
// 局部常量
const int c_a_l = 10;
cout << "局部变量: " << &a_l << endl;
cout << "局部静态变量: " << &s_a_l << endl;
cout << "局部常量: " << &c_a_l << endl;
cout << "字符串常量: " << &"hello" << endl; // 字符串常量
cout << "全局变量: " << &a_g << endl;
cout << "全局静态变量: " << &s_a_g << endl;
cout << "全局常量: " << &c_a_g << endl;
// 2. 栈区 (是无法返回栈中的地址的, 函数运行结束会回收栈中的数据此时, 会造成垂悬指针, TODO 但在c++中这里不报错??)
int* f_1 = func_1();
cout << "栈区内返回的地址解引用之后的值为: " << *f_1 << endl;
// 3. 堆区 基本使用 new关键字 在堆开辟一块新的内存, 将其地址返回到外部(地址本身是保存在栈中的)
// new关键字返回的堆区内存的指针
int* f_2 = func_2();
cout << "堆区内返回的地址解引用之后的值为: " << *f_2 << endl;
// 堆区手动释放地址, 使用 delete 关键字 TODO 这里没报错竟然还可以访问
delete f_2;
cout << "堆区数据手动释放之后的值为: " << *f_2 << endl;
// 4. 堆区申请一个数组
int *arr = new int[3]; // 申请一个长度为10 的数组
for (int i = 0; i < 3; i++) {
arr[i] = i + 100;
}
for (int i = 0; i < 3; ++i) {
cout << "手动在堆区存放了一个数组, 并赋值: " << arr[i] << endl;
}
// 手动释放申请的堆区数组 (注意需要增加一个中括号表示释放的是一个数组, 且数组内的元素也需要逐个析构)
delete[] arr;
}
int& func_ref_1(){
static int a = 123;
return a;
}
void stu_8(){
printf("stu_5: 引用\n");;
// 引用意义在, 给一个变量起别名 (两个变量使用同一个内存地址), 引用必须引用一块合法的内存空间, 禁止使用临时变量(右值)
// 引用必须初始化 不允许先声明再使用: int &b; 禁止使用这种语法
// 引用一旦初始化, 则不允许再次初始化
// 引用的本质在c++中实现是一个 指针常量: int * const XXX = & BBB
// 引用可以当值传递到函数内, 这里不做demo 类似指针了
// 引用同时存在垂悬引用的问题! 函数内不要返回 局部变量的引用 到外部, 如果需要返回, 请延长函数内变量引用的生命周期为 'static
// 函数也可以作为表达式的左值 如下例子
// 1. 引用的基本使用
int a = 10;
int &c = a; // 初始化引用
// int &c = b; // 禁止再次初始化
cout<< "修改引用前 a: " << a << ", b: " << c << endl;
// 修改 引用 的值
c = 20;
// 打印两个值
cout<< "修改引用后 a: " << a << ", b: " << c << endl;
// 2. 延长引用的声明周期 为 'static
int &d = func_ref_1(); // 由于内部变量为 static静态变量, 伴随着整个程序的运行, 所以引用的生命周期 也被延长到了 'static
cout << "函数内返回的 static变量 的引用的值为: " << d << endl;
// 3. 函数也可以作为表达式的左值
func_ref_1() = 2233; // 这里可以看成 &static int a = 2233, 因为static伴随着整个程序的运行周期
cout << "函数作为表达式的左值之后, 打印d: " << d << endl;
// 4. 常量引用 (主要用来修饰形参, 防止误操作)
// 常量引用在创建时, 编译器会为我们隐性绑定一个临时变量
// int &ref_1 = 10; // 引用必须引用一块合法的内存
const int &ref_2 = 10; // 加上const之后, 编译器将代码修改为 int temp = 10; int &ref_2 = temp;
// void func_ref_2(const int &a){
// a = 20; // 出错, 禁止这么用, 因为函数参数签名使用了 const 了
// }
}
void func_1(int a, int b = 10){
cout << "函数可以有默认参数, a 的值为: " << a << ", b 使用默认的值为: " << b << endl;
}
void func_2(int a, int){
cout << "函数可以有占位参数, a 的值为: " << a << endl;
}
void func_3(int a, int = 20){
cout << "函数可以有占位参数, 且占位参数也可以有默认参数, a 的值为: " << a << endl;
}
void stu_9(){
printf("stu_5: 函数的参数\n");;
// 函数可以有默认参数
func_1(1);
// 函数可以有占位参数, 就是没有默认值, 只有一个位置在哪里(后面会讲到具体用途), 且占位参数也可以有默认参数
func_2(20, 30);
func_3(22);
}
// 函数重载
void func_1(int a, string b) {
cout << "int a, string b, " << "a: " << a << ", b: " << b << endl;
}
void func_1(string a, int b) {
cout << "string a, int b, " << "a: " << a << ", b: " << b << endl;
}
void func_2(int & a){
cout << "int a, " << "a: " << a << endl;
}
void func_2(const int & a){
cout << "const int a, " << "a: " << a << endl;
}
void stu_10(){
printf("stu_5: 函数的重载 overloading\n");
// 提高复用性, 规则有3点
// 1. 重载的函数必须要同一个作用域下
// 2. 函数名称相同
// 3. 函数的参数: 类型不同 或者 个数不同 或者顺序不同
// 注意: 函数的返回值不可以作为函数重载的条件
func_1(10, "小明");
func_1("小明", 20);
// 引用参数的函数重载
func_2(10); // 临时变量会进入到常量引用当中
int c = 123;
func_2(c);
}
class P {
public:
P(){
cout << "P 默认构造函数" << endl;
}
P(const P & p){
cout << "P 拷贝构造函数" << endl;
}
};
void P_test(const P p){
}
P create_P(){
P p;
return p; // 创建了一个临时变量
} // 运行到这里 函数内中的p已经被析构了, 返回外部的是一个临时变量
void stu_11(){
printf("stu_5: 类和对象\n");
// public: 公有属性, 类本身,类实例和子类都可以访问其成员
// protected: 受保护的属性, 只有类本身可以访问, 实例不可以访问, 子类可以访问
// private: 私有属性, 只有类本身可以访问, 实例不可以访问, 子类不可以访问
// struct 和 class 惟一的区别就在于 默认的成员访问的权限不同, struct默认权限为共有, class默认权限为私有
// 1. 类的基本使用
class Student {
public:
string name;// 公开的属性
private:
int age = 20; // 私有属性
public:
int get_age(){ // 公开的方法
return age;
}
};
Student s1;
s1.name = "张三";
cout << s1.name << ": " << s1.get_age() << endl;
// 2. struct 和class 区别: class 如果不写属性的权限, 则默认为私有权限
class Stu1{
string name;
};
struct Stu2{
string name;
};
Stu1 stu1;
// stu1.name = "张三"; // 禁止访问 class 如果不写属性的权限, 则默认为私有权限
Stu2 stu2;
stu2.name = "张三";
// 3. 类的文件封装, 头文件中声明属性以及方法, 源文件中实现方法
Students students = {"张三"}; // 创建一个外部文件中的实例
cout << "调用外部文件中实现的实例的方法, 获取姓名为: " << students.get_name() << endl;
// 4. 对象的初始化和清理(如果我们不提供构造和析构函数, 编译器会默认提供空实现), 构造函数可以发生重载
// 4.1 构造函数用法:
// 构造函数没有返回值, 也不写void
// 函数名称与类名相同,
// 构造函数可以有参数, 因此可以发生重载
// 程序在调用对象时候会自动调用构造函数, 无需手动调用, 且只会调用一次
// 4.2 析构函数用法
// 析构函数没有返回值, 也不写void
// 函数名称与类名相同, 在名称前增加 ~ 符号
// 构造函数不可以有参数, 因此不可以发生重载
// 程序在对象销毁之前会自动调用析构, 无需手动调用, 而且只会调用一次
class Person{
public:
Person() {
cout << "Person 构造函数运行了" << endl;
}
~Person(){
cout << "Person 析构函数运行了" << endl;
}
};
Person p1;
cout << "对象创建完毕" << endl;
// 5. 构造函数的分类以及调用
// 按照参数分类: 无参构造(默认构造) 和 有参构造
// 按照类型分类: 普通构造, 拷贝构造
class People{
public:
string name;
People(){
cout << "无参构造函数" << endl;
}
People(string n){
name = n;
cout << "有参构造函数" << endl;
}
People(const People & p){
name = p.name;
cout << "拷贝构造函数" << endl;
}
~People(){
cout << "People 析构函数执行了" << endl;
}
};
// 括号法
People p_1; // 这个会调用无参构造函数, 注意: 调用无参构造函数 不要加括号
People p__1(); // 加了括号之后, 编译器会认为这是一个函数的声明
People p_2("李四"); // 调用有参构造函数
People p_3(p_2); // 调用拷贝构造函数
cout << "p_2 的 name 为: " << p_2.name << endl;
cout << "p_3 的 name 为: " << p_3.name << endl;
// 显式法 等号右边为一个 匿名对象;
People pp_1 = People();
People pp_2 = People("李四");
People pp_3 = People(pp_2);
// 匿名构造函数注意事项1: 如果没有左值,当前行结束后, 系统会立即释放
cout << 1 << endl;
People(); // 本行结束, 立即被析构了
cout << 2 << endl;
// 注意事项2: 不要利用拷贝构造函数, 初始化匿名对象, 编译器会认为 People(pp_2) === People pp_2 你再创建一个新的对象导致名称重复了
// People(pp_2);
// 隐式转换(调用一次拷贝构造函数)
People p4 = pp_3;
cout << "隐式转换来创建一个 People: " << p4.name << endl;
// 6. 拷贝构造函数调用时机
// 6.1 使用一个已经创建完毕的对象来初始化一个新对象
// 6.2 值传递的方式给函数的参数传值
// 6.3 以值的方式返回局部变量
// P p6;
// P_test(p6); // 在值传递的时候会调用拷贝构造函数隐式转换, 因为编译器展开这里 会创建一个临时变量 P temp = p6
// P p7 = create_P(); // 调用无参构造函数创建一个 匿名对象 后, 再赋予一个变量
// 7. 构造函数的调用规则(默认情况下c++会为 类默认添加3个函数, 默认无参构造函数, 默认析构函数, 默认拷贝构造函数(对属性一一拷贝), 还有一个在运算符重载哪里会说明)
// 7.1 如果用户自己定义有参构造函数, c++不再提供无参构造函数, 但是会提供默认拷贝构造函数
// 7.2 如果用户自己定义拷贝构造函数, 那么c++不再提供其他构造函数
class Teacher{
public:
string name;
Teacher(string n){
name = n;
cout << "Teacher 默认无参构造函数" << endl;
}
};
Teacher t1("张老师");
Teacher t2(t1); // c++ 默认为Teacher 实现了拷贝构造函数, 这里会拷贝 t1属性 来创建 t2实例
cout << "t2的名字为: " << t2.name << endl;
}
void stu_12(){
printf("stu_12: 对象的深浅拷贝\n");
// 如果利用编译器提供的 默认拷贝构造函数, 则只是浅拷贝操作
// 1. 这个例子, 在析构p2之前, 肯定是p1先被析构了, 导致p2中的 age 指向了一块不存在的内存, 就无法析构
// class People{
// public:
// string name;
// int *age;
// People(){
// cout << "默认构造参数调用" << endl;
// }
// People(string n, int a){
// name = n;
// age = new int(a); // 年龄保存在了堆区
// cout << "默认有参参数调用" << endl;
// }
//
// ~People(){
//
// // 堆区内存需要手动清理
// delete age; // 这里被在被第一个实例析构了, 之后拷贝出来的实例中的 age属性 都指向了空指针
// age = nullptr;
//
// cout << "析构函数调用" << endl;
// }
// };
//
// People p1 = People("小张", 20);
// cout << "p1 的姓名为: " << p1.name << endl;
//
// People p2 = People(p1); // 直接传进去一个实例, 使用编译器的默认实现, 拷贝构造函数
// cout << "p2 的姓名为: " << p2.name << endl;
// 2. 对上面的进行改造, 重写拷贝构造函数, 使用自己的逻辑, 进行 "深拷贝"
class People{
public:
string name;
int *age;
People(){
cout << "默认构造参数调用" << endl;
}
People(string n, int a){
name = n;
age = new int(a); // 年龄保存在了堆区
cout << "默认有参参数调用" << endl;
}
People(const People &p){
name = p.name;
age = new int(*p.age); // 手动new 一个新的堆区内存
}
~People(){
// 堆区内存需要手动清理
delete age;
age = NULL;
cout << "析构函数调用" << endl;
}
};
People p1 = People("小张", 20);
cout << "p1 的姓名为: " << p1.name << endl;
People p2 = People(p1); // 直接传进去一个实例, 使用编译器的默认实现, 拷贝构造函数
cout << "p2 的姓名为: " << p2.name << endl;
}
void stu_13(){
printf("stu_5: 初始化列表\n");;
// 主要给类进行初始化操作, 只有构造函数才可以这样写, 这样性能更高, 没有进入函数体内进行参数的内存拷贝, 和二次赋值操作, 而是直接给与实例初始值
// 内部实现貌似是直接 move参数, 没有发生内存拷贝
// 1. 手动实现初始化操作
class People1{
public:
string name;
int age;
People1(string n, int a)
{
name = n; // 这里手动又赋值, 没有直接初始化速度快
age = a;
}
};
People1 p1 = People1("张三", 18);
cout << "手动实现初始化操作的实例: " << p1.name << ", " << p1.age << endl;
// 2. 使用初始化列表
class People2{
public:
string name;
int age;
People2(string n, int a): name(n), age(a)
{
// 留空即可, 上面使用了初始化列表可以自动为我们实现 赋值操作
}
};
People1 p2 = People1("李四", 17);
cout << "初始化列表的实例: " << p2.name << ", " << p2.age << endl;
}
void stu_14(){
printf("stu_5: 类实例的drop顺序\n");;
// 构造时, 需要先创建 子对象, 然后再创建父对象,
// drop时, 需要先drop掉父对象, 再drop子对象,
// 很明显! 父对象的生命周期不能超过子对象, 不能指向一片空内存, 为了内存安全
class Student {
public:
string name;
Student(string n): name(n)
{
cout << "Student 构造函数执行" << endl;
}
~Student(){
cout << "Student 析构函数执行" << endl;
}
};
class Teacher{
public:
string name;
Student student;
Teacher(string n, Student s): name(n), student(s)
{
cout << "Teacher 构造函数执行" << endl;
}
~Teacher(){
cout << "Teacher 析构函数执行" << endl;
}
};
cout << "------" << endl;
Teacher t = Teacher("张老师", Student("小明"));
cout << "------" << endl;
}
class stu_People{
public:
static string name; // 一个公开静态属性
int gender; // 一个普通的成员属性
// 普通成员方法
void get_info_1(){
cout << "一个普通的成员方法, 访问成员变量: " << name << " " << gender << endl;
}
// 静态成员函数
static void get_info_2(){
cout << "一个静态成员函数, 只能访问静态方法: " << name << endl;
}
private:
static int age; // 一个私有静态属性
};
string stu_People::name = "小明";
int stu_People::age = 18;
void stu_15(){
printf("stu_5: 静态成员变量/函数\n");;
// 静态成员变量: 所有对象(实例)共享同一份数据, 在编译阶段已经分配了内存, 需要在类内声明类外 进行初始化
// 静态成员变量不属于某个对象上, 所有对象都共用一个数据(一块内存)
// 静态成员变量可以通过对象(实例)来访问静态变量, 也可以使用 类本身(class::attr) 访问静态成员变量
// 静态成员变量也可以被 访问权限(共有, 私有, 受保护的) 限制
// 1. 基本使用
stu_People s1 = stu_People();
stu_People s2 = stu_People();
cout << "s1 的姓名为: " << s1.name << endl;
cout << "s2 的姓名为: " << s2.name << endl;
s1.name = "小亮"; // 修改s1静态成员变量
cout << "s1 的姓名为: " << s1.name << endl;
cout << "s2 的姓名为: " << s2.name << endl; // s2 因为和 s1 的静态成员变量在一个内存地址上, 所以这里随着s1的改变而改变
// 2. 访问权限限制 (这里访问私有的静态成员属性)
// cout << s2.age << endl; // 'age' is a private member of 'stu_People'
// 3. 静态成员函数: 所有一个对象共享同一个函数, 静态成员函数只能访问静态成员变量! (因为函数共享一块内存, 不同的类调用时必须保证内存地址也是固定的, 如果不是静态成员变量而是一个普通变量那么函数不知道访问具体是哪个实例身上的变量)
s1.get_info_1();
s1.get_info_2();
}
class People_3{
public:
int age; // 4 个字节
int gender; // 4 个字节
static string name;
};
string People_3::name = "小亮";
void stu_16(){
printf("stu_16: 成员变量与成员函数 的存储\n");;
// 只有 非静态成员变量(属性), 才属于对象身上, 其他的都在公共区
// 1. 空对象占用的内存空间为1, 编译器为每个空对象分配 1字节 空间, 用来表示占用的内存位置
class People_1{};
People_1 p1 = People_1();
cout << "sizeof(p1): " << sizeof(p1) << endl;
// 2. 非静态成员属性, 属于类对象上, 对象大小按照内成员的大小分配
class People_2{
int age; // 4 个字节
int gender; // 4 个字节
};
People_2 p2 = People_2();
cout << "sizeof(p2): " << sizeof(p2) << endl; // 8个字节
// 3. 静态成员属性, 不属于类对象上, p3 虽然多了一个静态成员属性, 但是整个对象的大小依然是 8字节
People_3 p3 = People_3();
cout << "sizeof(p3): " << sizeof(p3) << endl; // 8个字节
// 4. 成员函数 成员属性, 是分开存储的, 下面对象的大小是4字节, 显然是其内属性 age的大小 仅仅, 函数并没有存储在对象上
class People_4{
int age;
void get_age(){}
};
People_4 p4 = People_4();
cout << "sizeof(p4): " << sizeof(p4) << endl; // 4 个字节
}
void stu_17(){
printf("stu_16: this 指针\n");;
// this 指向了调用者本身, 注意 它是一个指针, 类似 self, this指针实质上是一个 指针常量, 本身是不可以修改的, 但是this指针 本身的值是可以修改的: 类型 * const this
// 一般解决名称冲突(形参的变量名和类本身的属性冲突了)
class Student{
int age1;
void set_age1(int age1){
age1 = age1; // 很明显这里使用的是形参的 age
}
int age2;
void set_age2(int age2){
this->age2 = age2; // this 是一个指针, 指向了类实例
}
};
}
void stu_18(){
printf("stu_16: 空指针访问成员属性\n");
// 在类中 访问成员属性的时候默认增加了 this->attr 来访问属性, 所以在空指针可以访问方法, 但是方法中如果访问了属性那么是禁止这样做的
class P{
public:
int flag;
void call_func(){
cout << "只要不调用属性, 访问时没问题的" << endl;
}
void get_flag(){
cout << "肯定出错" << flag << endl;
}
};
P * p = nullptr;
p->call_func(); // 调用的方法中如果没有访问属性, 那么是可以调用的
// p->get_flag(); 出错了
}
void stu_19(){
printf("stu_16: 常函数/常对象, const 修饰成员以及函数\n");
// 常函数, 成员函数后添加const 后这个函数成为 常函数
// 常函数内 不可以 修改成员属性, 如果需要修改需要给成员属性 添加 mutable 关键字
// 常函数加入const 之后,本质上修饰的是内部的this指针, 函数体内部的 this 指向 从指针常量变为 常量指针常量: const 类型 * const this, 内部的值不允许修改
// 类中的函数 在访问属性的时候 本质上使用的 是 self->attr , 隐性的增加了 this指针
class Student{
public:
int age1;
mutable int age2;
void set_age(int a) const {
// age1 = a; // 在常函数中 修改变量会报错: cannot assign to non-static data member within const member function 'set_age'
this->age2 = a; // 修饰的是添加 mutable 的关键字的属性, 是可以修改的
}
};
// 常对象, 声明对象前增加const关键字, 不允许修改普通的成员, 但是对于添加 mutable关键字 之后的属性, 依然可以修改
// 常对象, 只能调用常函数, 无法调用普通函数
Student const s = Student();
// s.age1 = 123; // 报错, 常对象 是无法修改成员属性的: cannot assign to variable 's' with const-qualified type 'const Student'
s.age2 = 2233; // 没问题
}
// 20.1 全局函数做友元
class s_20_Class {
friend void get_age_2(s_20_Class c); // 注册友元函数
public:
s_20_Class(){
age = 10;
}
private:
int age; // 私有属性
};
void get_age_1(s_20_Class c) {
// cout << c.age << endl; // 私有成员在外部无法访问滴: 'age' is a private member of 's_20_Class'
}
void get_age_2(s_20_Class c) {
cout << "全局函数做友元: "<< c.age << endl; // 木有问题, 这个函数的 已经 注册为了 s_20_Class的友元函数
}
// 20.2 类做友元
class Son_01{ // 定义一个 子
friend class Father_01; // 注册友元类
private: // 私有属性
string name;
public:
Son_01(){
name = "小明";
}
};
class Father_01{ // 定义一个 父
public:
Son_01 s;
void get_son_name(){
cout << "类做友元: 从父 中 访问 子 的私有元素 的名字为: " << s.name << endl;
}
};
// 20.3 成员函数做友元
class Son_02; // 提前声明
class Father_02{ // 提前声明好 类
public:
Son_02 *s;
void get_son_name_01();
void get_son_name_02();
Father_02();
};
class Son_02{ // 给提前声明的类 做出 相关实现
friend void Father_02::get_son_name_02(); // 只让 02 指定成员函数做友元
private:
string name;
public:
Son_02(){ // 构造函数 设置 name 的默认值
name = "亮亮";
}
};
Father_02::Father_02(){
s = new Son_02;
}
void Father_02::get_son_name_01() {
// cout << s->name << endl; // 这个函数没有注册呀: 'name' is a private member of 'Son_02'
}
void Father_02::get_son_name_02() {
cout << "只让某些成员函数作为友元: " << s->name << endl;
}
void stu_20(){
printf("stu_16: 友元\n");;
// 让私有成员 在外部 对象/函数 可以访问, 在类的内部 通过关键字 friend 配合签名 来注册友元
// 全局函数做友元
s_20_Class c = s_20_Class();
get_age_2(c);
// 类做友元 (在一个类中 可以任意地方访问 友元类中的私有属性)
Father_01().get_son_name();
// 成员函数做友元 (一个类中, 只有某些成员函数才能访问友元类中的私有属性)
Father_02().get_son_name_02();
}
// 左移运算符
class My_left{
public:
int age;
// void operator<<(My_left &ml){ // 这样无法实现, 需要在外部实现
// cout << ml.age << endl;
// }
My_left(int a){
this->age = a;
}
};
ostream& operator<< (ostream &out, My_left &ml){
out << ml.age;
return out;
}
void stu_21(){
printf("stu_16: 运算符重载\n");;
// 可以分为 成员函数重载 和 全局函数重载, 全局函数重载本质就是函数重载
// 加法运算符 + (返回值)
class MyAdd{
public:
int age;
MyAdd operator+ (MyAdd &ma) { // 为了可以连续的相加, 这里需要返回一个新的 实例对象到外部
return MyAdd(this->age + ma.age);
}
MyAdd(int a){
age = a;
}
};
MyAdd add_01 = MyAdd(10);
MyAdd add_02 = MyAdd(20);
MyAdd add_03 = MyAdd(30);
MyAdd add_count = add_01 + add_02 + add_03;
cout << "add_01 和 add_02 的加起来为: " << add_count.age << endl;
// 左移运算符 << (可以输出自定义数据类型, 不能利用成员函数重载 << 运算符, 因为无法实现 cout 在左侧)
My_left m(10);
cout << "自定义输出类型打印类实例 m: " << m << endl;
// 前置递增运算符 ++i (前置递增返回引用)
// 后置递增运算符 i++ (后置递增返回值, int 是一个占位参数, 可以用于区分前置和后置)
// 赋值运算符 = (c++ 除了提供默认内的3个构造函数, 还会提供一个 赋值运算符 =, 用来实现 作为右值的操作, 注意这里需要手动实现深拷贝, )
// 关系运算符 == !=
// 函数调用运算符 (仿函数(就类似python中的 call ))
class My_call{
public:
void operator() (int a){
cout << "类当做函数调用: " << a << endl;
}
};
My_call()(20);
}
void stu_22(){
printf("stu_16: 继承\n");;
// 继承的语法:(class 子类: 继承方式 父类)
// 继承方式一共有3种继承 公共 私有 保护
// 父类私有属性, 无论哪种继承方式 都无法继承过来
// 公共继承 public: 按照父类中所有属性定义的权限原封不动进行继承, (父类私有属性无法继承)
// 私有继承 private: 父类中的所有属性, 继承过来在子类中表现为私有属性, (父类私有属性无法继承)
// 保护继承 protected: 父类中的所有属性, 继承过来在子类中的表现为保护属性, (父类私有属性无法继承)
// 基础父类
class base_father{
public: // 公开
int p_1;
private: // 私有
int p_2;
protected: // 受保护的
int p_3;
};
// 1. 公共继承
class Son_01: public base_father {
void test(){
p_1; // 访问继承来的公开属性
// p_2; // 私有属性无法被继承
p_3; // 访问继承来的受保护的属性
}
};
Son_01 s1;
s1.p_1; // 访问公开属性
// s1.p_2; // 私有属性没有被继承, 就无从访问
// s1.p_3; // 受保护的属性类实例无法访问
// 2. 保护继承
class Son_02: protected base_father {
void test(){
p_1; // 访问继承来的受保护的属性
// p_2; // 私有属性无法被继承
p_3; // 访问继承来的受保护的属性
}
};
Son_02 s2;
// s2.p_1; // 保护属性无法通过类实例来进行访问
// s2.p_2;
// s2.p_3;
// 私有继承
class Son_03: private base_father { // 私有继承过来的, 在子类中都为私有属性
};
class Son_04: public Son_03 {
void test(){
// p_1; // 父类中的属性都是私有属性, 当然无法访问了
// p_3; // 父类中的属性都是私有属性, 当然无法访问了
}
};
}
void stu_23(){
printf("stu_16: 继承中的对象内存模型\n");
// 父类中所有非静态成员属性都会被子类继承下去
// 父类中私有成员属性, 是被编译器隐藏了, 因此访问不到, 但是确实会继承下去
// 基础父类
class base_father{
public: // 公开
int p_1;
private: // 私有
int p_2;
protected: // 受保护的
int p_3;
};
// 子类
class Son_01: public base_father{
public:
int p_4;
};
Son_01 s1;
cout << "继承过来的 子类 的大小: " << sizeof(s1) << endl;
}
void stu_24(){
printf("stu_16: 继承中的析构顺序\n");;
// 一切为了内存安全, 子类的生命周期, 一定要比父类的短(子类不允许出现继承空指针, 所以子类一定是先销毁)
// 创建子类的时候会先创建父类: 创建父类 -> 创建子类 -> 销毁子类 -> 销毁父类 TODO 由内到外创建, 由外到内销毁
class Father_01 {
public:
Father_01(){
cout << "Father_01 构造函数" << endl;
}
~Father_01(){
cout << "Father_01 析构函数" << endl;
}
};
class Son_01: public Father_01 {
public:
Son_01(){
cout << "Son_01 构造函数" << endl;
}
~Son_01(){
cout << "Son_01 析构函数" << endl;
}
};
Son_01 s = Son_01();
}
void stu_25(){
printf("stu_16: 继承中同名属性和函数的处理\n");;
// 如果子类中存在和父类中相同的属性, 如果需要使用父类中的属性,则需要增加父类作用域: 子类实例.父类::属性
// 如果存在同名的方法, 编译器会隐藏掉所有的父类同名成员函数, 如要访问同样增加作用域即可, 子类实例.父类::方法
// 静态成员 和静态方法 有两种访问方法(通过类实例 or 通过类本身), 但是处理方法和 非静态的是一致的添加父类作用域即可, 注意通过对象访问: (子类实例::父类::静态成员), 通过 类本身 访问: (子类::父类::静态成员)
class Father_01{
public:
int age;
string test() {
return "父类中的方法";
}
Father_01(){
age = 20;
}
};
class Son_01: public Father_01{
public:
int age;
string test() {
return "子类中的方法";
}
Son_01(){
age = 10;
}
};
Son_01 s = Son_01();
cout << "子类调用本身的属性: " << s.age << endl;
cout << "子类调用本身的方法: " << s.test() << endl;
cout << "子类调用父类中重名的的属性: " << s.Father_01::age << endl;
cout << "子类调用父类中重名的的方法: " << s.Father_01::test() << endl;
}
void stu_26(){
printf("stu_16: 多继承/菱形继承\n");;
// 多个继承使用 逗号 "," 隔开即可
// 多个父类中如果出现同名的方法或属性, 则加作用域即可: 子类.父类::attr
class Father_01{
public:
void test(){
cout << "Father_01 的方法" << endl;
}
};
class Father_02: public Father_01{
public:
void test(){
cout << "Father_02 的方法" << endl;
}
};
class Father_03: public Father_01{
public:
void test(){
cout << "Father_03 的方法" << endl;
}
};
class Father_04: public Father_02, public Father_03{
public:
void test(){
cout << "Father_04 的方法" << endl;
}
};
Father_04 f4 = Father_04();
f4.Father_02::test(); // 调用其中一个父类的方法
f4.Father_03::test(); // 调用其中一个父类的方法
}
void stu_27(){
printf("stu_16: 虚继承\n");;
// 继承时 使用 virtual 关键字表示这是一个虚继承
// vbptr 为了解决 菱形继承的二义性, 导致 子类继承多个父类, 父类中出现相同属性名时, 出现不确定性, 也出现了浪费, 编译器不知道应该使用哪个父类的属性, 我们需要手动增加父类的作用域来指定使用的属性,
// 有了虚继承, 之后, 子类继承只有一份, vbptr就是 vbtable一个虚表, 只是一个地址, 记录的是一个表
class Father_01{
public:
int age;
};
//继承前加 virtual 关键字后,变为虚继承
//此时公共的父类 Father_01 称为虚基类
class Father_02: virtual public Father_01{
};
class Father_03: virtual public Father_01{
};
class Father_04: public Father_02, public Father_03{
};
Father_04 f4 = Father_04();
f4.Father_02::age = 20;
f4.Father_03::age = 30;
cout << "访问虚基类: " << f4.age << endl;
}
// 不使用虚函数
class F1 {
public:
void show(){
cout << "这是父类, 没有使用虚函数" << endl;
}
};
class S1: public F1 {
public:
void show(){
cout << "这是子类, 没有使用虚函数" << endl;
}
};
void show(F1 *f) {
f->show();
}
// 使用虚函数 定义计算器抽象类
class Computer{
public:
int a;
int b;
virtual int get_ret() = 0; // 纯虚函数声明
};
// 使用虚函数 计算结果的函数, 接收一个胖指针, 用来调用虚函数
int get_ret(Computer *c){
return c->get_ret();
}
void stu_28(){
printf("stu_16: 子类型/多态/虚函数\n");
// 侯捷老师的 精品视频 https://www.bilibili.com/video/BV1K4411974P?p=30 很多抽象的概念, 没有办法写笔记, 只能靠自己悟(ps: 我曾经有点深入的学习过rust, 这里理解是非常轻松的 和 trait object 几乎没差)
// 父类的指针指向子类对象
// 使用多态时在函数签名时, 需要接受父类型的指针 (子类型是父类型的子集, 所以子类型传入参数时也会符合签名)
// 注意在不使用虚函数的时候, 有些和预想的不一样, 见下面的 例子1
// 在写方法时, 在返回值类型前, 添加使用 virtual 关键字表示这是一个虚函数
// c++多态这里和rust的模式差不多, 一个胖指针, 虚函数 vfptr: virtual function pointer, 类似 trait object
// 1. 不使用虚函数
F1 f1 = F1();
S1 s1 = S1();
show(&f1);
show(&s1); // 这里我们想使用 s1 的方法, 但是由于函数签名时 使用了父类型的签名, 虽然也能传入子类型, 但是调用时却调用的父类型中的方法
// 2. 使用虚函数: 计算器 Demo
cout << "下面是使用了虚函数的计算器------" << endl;
// 加法
class Add: public Computer {
public:
int get_ret() { // 重写纯虚函数
return a + b;
}
};
// 减法
class Sub: public Computer {
public:
int get_ret() { // 重写纯虚函数
return a - b;
}
};
// 这里 Add 是 Computer 的子集, 所以 Add可以看做是 Computer类型, 可以 匹配下面函数的签名
Computer *computer;
// 加法
computer = new Add;
computer->a = 10;
computer->b = 20;
cout << "这里传入一个 Computer胖指针, 通过继承 Add 的结果为: " << get_ret(computer) << endl;
delete computer; // 手动释放
Add a = Add();
a.a = 20;
a.b = 30;
Add *a_ptr = &a;
cout << "这里传入 Add的指针 通过继承 Add 的结果为: " << get_ret(a_ptr) << endl;
// 减法
computer = new Sub;
computer->a = 20;
computer->b = 15;
cout << "通过继承 Sub 的结果为: " << get_ret(computer) << endl;
delete computer; // 手动释放
}
void stu_29(){
printf("stu_16: 纯虚函数/抽象类\n");;
// 当类中有了纯虚函数, 这个类也成为抽象类, 声明虚函数的后面添加 = 0, 语法: virtual 返回值类型 函数名(参数列表) = 0;
// 抽象类无法实例化为对象, 且子类必须重写抽象类中的纯虚函数, 如果子类无法实现, 则子类也是抽象类,无法实例化
// 定义一个抽象类
class Base {
public:
virtual void say_hello() = 0; // 定义一个纯虚函数
};
// 尝试实例化抽象类
// Base b = Base(); // 抽象类无法实例化, 错误: allocating an object of abstract class type 'Base'
// 尝试 实例化 没有实现纯虚函数的子类
class B1: public Base {};
// B1 b1 = B1(); // 子类没有实现纯虚函数, 则也无法被实例化
// 实现纯虚函数的子类可以实例化
class B2: public Base{
void say_hello(){
};
};
B2 b2 = B2();
}
void stu_30(){
printf("stu_16: 虚析构/纯虚析构\n");
// 为了解决父类指针指向子类对象时, 子类对象无法释放的问题(如果子类中没有堆区数据, 可以省略)
// 父类的指针指向了子类对象, 所以当 delete 父类指针的时候, 不会执行 子类的析构代码, 如果子类中有堆区的数据, 就会导致子类中堆区的数据不会释放, 造成内存泄漏
// 给父类的析构函数, 增加 virtual 关键字, 使其变为虚函数, 可以解决父类指针释放子类对象时, 不执行子类析构函数的问题
// 纯虚析构, 需要有声明, 也还需要有外部实现, 有了纯虚析构之后, 这个类也属于抽象类, 则子类也必须要实现其自己的析构函数
class Father_01{
public:
Father_01(){
cout << "Father_01 的构造函数" << endl;
}
~Father_01(){
cout << "Father_01 的析构函数" << endl;
}
virtual void say() = 0;
};
class Son_01: public Father_01{
public:
int *age; // 子类有一个属性是在堆区
Son_01(int a){
this->age = new int(a);
cout << "Son_01 的构造函数" << endl;
}
~Son_01(){
if (this->age != NULL) {
cout << "Son_01 的析构函数" << endl;
delete this->age;
this->age = NULL;
}
}
void say() {
cout << *this->age << ", hello" << endl;
}
};
// 创建一个胖指针(父类指针 指向子类对象)
Father_01 *s1 = new Son_01(20);
s1->say();
// 后续不再使用, 需要手动管理刚才 new 出来的 子类对象
delete s1;
// TODO 上方的代码在clion中运行结果并不如意, 正确的运行结果如下, 可以发现, 父类对象的胖指针在析构的时候, 并没有执行 子类实例的析构函数, 就导致内存泄漏, 可以把析构函数转为一个 虚析构函数即可解决问题
// Father_01 的构造函数
// Son_01 的构造函数
// 20, hello
// Father_01 的析构函数
cout << "----------" << endl;
class Father_02{
public:
Father_02(){
cout << "Father_02 的构造函数" << endl;
}
virtual ~Father_02(){
cout << "Father_02 的析构函数" << endl;
}
virtual void say() = 0;
};
class Son_02: public Father_02{
public:
int *age; // 子类有一个属性是在堆区
Son_02(int a){
this->age = new int(a);
cout << "Son_02 的构造函数" << endl;
}
~Son_02(){
if (this->age != NULL) {
cout << "Son_02 的析构函数" << endl;
delete this->age;
this->age = NULL;
}
}
void say() {
cout << *this->age << ", hello" << endl;
}
};
// 创建一个胖指针(父类指针 指向子类对象)
Father_02 *s2 = new Son_02(20);
s2->say();
// 后续不再使用, 需要手动管理刚才 new 出来的 子类对象
delete s2;
// TODO 上方的代码 在delete 父类型胖指针的时候 会对子类实例 进行正常析构了, 当然也可以给父类析构函数 定义成 纯虚析构函数, 纯虚析构函数需要在内部定义, 外部实现
// Father_02 的构造函数
// Son_01 的构造函数
// 20, hello
// Son_01 的析构函数
// Father_02 的析构函数
// 父类纯虚析构函数: 如下, 纯虚析构函数需要在内部定义, 外部实现
}
// 父类纯虚析构函数的实现
class Father_03{
public:
Father_03(){
cout << "Father_03 的构造函数" << endl;
}
virtual ~Father_03() = 0; // 内部定义析构函数
virtual void say() = 0;
};
Father_03::~Father_03(){
cout << "外部实现析构函数" << endl;
}
void stu_31(){
printf("stu_16: 文件操作\n");
// 操作文件的3大类(ofstream:写操作, ifstream: 读操作, fstream : 读写操作), 需要先引入进来
// 文件打开方式: ios::in(只读), ios::out(只写), ios::ate(初始位置在文件尾), ios::app(追加方式写文件), ios::trunc(如果文件存在,先删除再创建), ios::binary(以二进制方式)
// 文件打开方式可以配合使用, 利用管道 | 操作符, 例如用二进制方式写文件: ios::binary | ios:: out
// TODO 写文本数据 到文件步骤
// 1. 创建流对象
fstream fs_1 = fstream();
// 2. 打开文件
fs_1.open("/Users/zhangy233/Library/Mobile Documents/com~apple~CloudDocs/c++/stu_31.txt", ios::out);
// 3. 写数据
fs_1 << "hello_1" << endl;
fs_1 << "hello_2" << endl;
// 4. 关闭文件
fs_1.close();
cout << "写入完毕---" <<endl;
// TODO 读文件中 文本数据步骤
// 1. 创建流对象, 构造时直接传入参数, 下面就不需要用 open了
fstream f_2 = fstream("/Users/zhangy233/Library/Mobile Documents/com~apple~CloudDocs/c++/stu_31.txt", ios::in);
// 2. 判断是否打开成功
if (!f_2.is_open()) {
cout << "文件打开失败" << endl;
}
// 3. 多种方式可以读取文件
// 方式 1 (创建一个 buffer, 使用 右移运算符 读取文件)
// string buf_1; // c++的方式读取文件(使用 string 当做 buffer)
// char buf_1[1024] = {0}; // 推荐使用数组当做buffer
// while (f_2 >> buf_1) { // 每次循环都会读取一行, 直到文件尾
// cout << "使用 string 当做buffer 读文件: " << buf_1 << endl;
// }
// 方式 2 使用文件对象提供的方法
// char buf_1[1024] = {0};
// f_2.getline(buf_1, sizeof(buf_1)); // 读取一行
// cout << buf_1 << endl;
// while (f_2.getline(buf_1, sizeof(buf_1))) { // 循环读取所有行, 直到最后
// cout << buf_1 << endl;
// }
// 方式 3 使用文件对象提供的方法, 使用 string 当做buffer
// string buf = string();
// while (getline(f_2, buf)) {
// cout << buf << endl;
// }
// 方式4 (不推荐使用了, 一个字一个字的读取)
// char c;
// while ((c = f_2.get()) != EOF) {
// cout << c;
// }
// 4. 关闭文件
f_2.close();
// TODO 写 二进制数据 文件输出流对象 可以通过write函数,以二进制方式写数据
// ostream& write(const char * buffer,int len)
// 字符指针buffer指向内存中一段存储空间, len是读写的字节数
class P {
public:
char name[64];
int age;
};
// 创建一个对象
P p = {
"张三",
20
};
// 打开一个文件
fstream f_3 = fstream("/Users/zhangy233/Library/Mobile Documents/com~apple~CloudDocs/c++/stu_31_bytes.txt",
ios::in | ios::out | ios::binary);
// 强制转换 对象类型指针 -> 字符型指针
cout << "(char *) &p: " << (char *) &p << "-----" << sizeof((char *) &p) << endl;
// 写入
f_3.write((char *) &p, sizeof((char *) &p));
f_3.close();
// TODO 读 二进制数据 二进制方式读文件主要利用流对象调用成员函数read
// istream& read(char *buffer,int len)
// 字符指针buffer指向内存中一段存储空间, len是读写的字节数
fstream f_4 = fstream("/Users/zhangy233/Library/Mobile Documents/com~apple~CloudDocs/c++/stu_31_bytes.txt",
ios::in | ios::out | ios::binary);
P p_r = P();
f_4.read((char *) &p_r, sizeof(p_r));
cout << "读取完毕: " << p_r.name << "--" << p_r.age << endl;
f_4.close();
}
// 一个交换两个变量数据的函数
template<class T, typename Q> void swap_32(T& t, Q& q){
T swap = t;
t = q;
q = swap;
}
template<class T> void func_32(){
cout << "func_32 调用" << endl;
}
template<class T>
T create_ojb(){
cout << typeid(T).name();
return T();
}
int s = create_ojb<int>();
void stu_32() {
printf("stu_16: 函数模板\n");
// 函数模板利用关键字 template<class 泛型, typename 泛型> 在声明时可以使用 class 或者 typename
// TODO 模板必须要确定出T的数据类型, 才可以使用
// 自动类型推导
int a = 1;
int b = 3;
swap_32(a, b);
cout << "经过函数模板 进行数值交换之后的值: a " << a << " b " << b << endl;
// 显式指定类型
int c = 3;
int d = 5;
swap_32<int, int>(c, d);
cout << "经过函数模板 进行数值交换之后的值: c " << c << " d " << d << endl;
// 模板必须要确定出 T 的数据类型, 才可以使用, 并且推导的数据类型必须要一直
// func_32(); // 错误, 这里无法推导出 泛型 的数据类型
func_32<int>(); // 利用显示指定类型的方式, 给 T 一个类型, 才可以使用该模板
}
void func_33_1(int a, int b) {
cout << "func_33 普通函数, 被调用了 打印 a + b: " << a + b << endl;
}
template <class T>
void func_33_2(T a, T b) {
cout << "func_33 函数模板, 被调用了 打印 a + b: " << a + b << endl;
}
void stu_33(){
printf("stu_16: 普通函数与函数模板的区别\n");;
// 普通函数可以发生隐式类型转换
// 函数模板 如果使用自动类型推导, 不可以发生隐式类型转换, 但是 如果手动显式指定类型使用,则可以发生隐式类型转换
int a = 10;
char b = 'a';
// 普通函数: 在接收 int 类型的函数参数, 传入 char 类型, 会发生隐式类型转换
func_33_1(a, b); // 正常输出了, char 类型的 a 被转为了 97, 97 + 10 = 107
// 函数模板 不会发生隐式类型转换
// func_33_2(a, b);
// 函数模板 手动指明类型, 可以发生隐式类型转换
func_33_2<int>(a, b);
}
void func_34_1(int a, int b) {
cout << "普通函数 (int a, int b) func_34_1 被调用了, a: " << a << ", b" << b << endl;
}
template <class T> void func_34_1(T a, T b) {
cout << "函数模板函数 (T a, T b) func_34_1 被调用了, a: " << a << ", b" << b << endl;
}
template <class T> void func_34_1(T a, T b, T c) {
cout << "函数模板函数 (T a, T b, T c) func_34_1 被调用了, a: " << a << ", b" << b << ", c" << c <<endl;
}
void stu_34(){
printf("stu_16: 普通函数与函数模板的调用规则\n");
// ps: 既然提供了函数模板, 最好就不要提供普通函数, 否则容易出现二义性
int a = 10;
int b = 20;
int c = 30;
// 1. 如果函数模板和普通函数都可以实现,优先调用普通函数
func_34_1(a, b);
// 2. 可以通过空模板参数列表来强制调用函数模板
func_34_1<>(a, b);
// 3. 函数模板也可以发生重载
func_34_1<>(a, b, c);
// 4. 如果函数模板可以产生更好的匹配,优先调用函数模板
func_34_1(a, b, c); // 这里 不用空模板参数列表强制调用函数模板, 也可以调用, 是因为函数模板这里本身接收 3个 参数, 而普通函数只接收两个
}
void stu_35(){
printf("stu_16: 模板的局限性\n");
// 有点类似 函数模板的重载 就不做练习了
}
// 创建一个类模板, 且指定默认类型
template <class NameType, class AgeType = int>
class Student_36{
public:
NameType name;
AgeType age;
Student_36(NameType n, AgeType a){
this->name = n;
this->age = a;
}
};
void stu_36(){
printf("stu_16: 类模板\n");
// 类模板和函数模板语法相似, 在声明模板template后面加类, 此类称为类模板
// 类模板使用只能用显示指定类型方式: 类模板使用时候, 不可以用自动类型推导, 必须使用显示指定类型的方式, 使用类模板
// 类模板中的模板参数列表可以有默认参数: 可以在声明模板类型的时候, 指定默认的类型, 在没有显示指明类型的时候, 使用默认类型
Student_36<string, int> s1 = Student_36<string, int>("张三", 12); // 匿名类的方式
Student_36<string, int> s2("李四", 17); // 创建类型的方式
Student_36<string> s3("王五", 22); // 使用 模板中 指定的默认类型
// Student_36 s = Student_36("张三", 12); // 禁止使用, 必须显示的指明类型
// 类模板中的成员函数并不是一开始就创建的, 在调用时才去创建, 才能引发一些错误之类
}
template <class NameType, class AgeType = int>
class Student_37{
public:
NameType name;
AgeType age;
Student_37(NameType name, AgeType age) {
this->name = name;
this->age = age;
}
void show(){
cout << "name: " << name << ", age: " << age << endl;
}
};
// 指定传入类型
void show_1(Student_37<string, int> &s) {
cout << "show_1: " << "指定了传入类型: " << typeid(s).name() << endl;
s.show();
}
// 参数 泛型化
template <class N, class A>
void show_2(Student_37<N, A> &s) {
cout << "show_2: " << "参数泛型化, 查看类型: " << typeid(N).name() << "---" << typeid(A).name() << endl;
s.show();
}
// 将整个对象进行作为泛型
template <class S>
void show_3(S &s) {
cout << "show_2: " << "将整个对象进行作为泛型, 查看类型: " << typeid(S).name() << endl;
s.show();
}
void stu_37(){
printf("stu_16: 类模板对象作为 函数模板的参数\n");
// 指定传入的类型: 直接显式对象的数据类型 ps: 这种方法使用最大
// 参数泛型化: 将对象中的参数也变为变为模板(泛型)进行传递
// 整个类泛型化: 将这个对象类型 泛型化进行传递
// TODO 技巧: 使用 typeid(泛型).name() 可以查看泛型的具体类型
Student_37<string, int> s = Student_37<string, int>("小明", 14);
show_1(s);
show_2(s);
show_3(s);
}
template <class T>
class Father_38{
public:
T name;
};
// 继承时指明父类中 T 的类型
// class Son_38_01: public Father_38{ // 错误, 这里必须指明 父类中 泛型的类型来分配内存
class Son_38_01: public Father_38<int>{
};
// 继承时, 把子类也写为一个类模板, 这样会更灵活
template <class T, class A>
class Son_38_02: public Father_38<T>{
public:
A age;
};
void stu_38(){
printf("stu_16: 类模板与继承\n");;
// 当子类继承的父类是一个类模板时, 子类在声明的时候, 需要指定父类中 T 的类型, 如果不指定, 编译器无法给子类分配内存
// 如果想灵活的指定出父类中 T 的类型, 子类也需变为类模板
Son_38_02<string, int> s = Son_38_02<string, int>();
s.name = "李四";
s.age = 14;
}
template<class NameType, class AgeType>
class Student_38{
public:
NameType name;
AgeType age;
// // 普通成员函数实现
// Student_38(NameType name, AgeType age) {
// this->name = name;
// this->age = age;
// }
//
// // 普通成员函数实现
// void show_info(){
// cout << "姓名: " << this->name << ", age: " << this->age << endl;
// }
// 类外实现成员函数, 需要提前声明
Student_38(NameType,AgeType);
void show_info();
};
// 构造函数, 类外实现
template <class NameType, class AgeType>
Student_38<NameType, AgeType>::Student_38(NameType name, AgeType age){
this->name = name;
this->age = age;
}
// 普通成员函数, 类外实现
template <class A, class B>
void Student_38<A, B>::show_info() {
cout << "普通成员函数, 如果类外实现, 也需要加上完成的成员列表 :" << endl;
cout << "姓名: " << this->name << ", age: " << this->age << endl;
}
void stu_39(){
printf("stu_16: 类模板成员函数类外实现\n");
// 类模板中成员函数类外实现时, 需要加上完成的泛型成员列表
Student_38<string, int> s = Student_38<string, int>("小亮", 14);
s.show_info();
}
// #include "Student_40.h" // 运行时如果只包含头文件, 会错误: 包含头文件, 编译器编译阶段也不会报错, 只会在调用时发生报错, 需要下面包含 cpp文件
// #include "Student_40.cpp" // 包含源文件的方式导入, 但是不常用
#include "Student_40.hpp" // 约定俗成,使用 .hpp 文件, 头文件和源文件写到一起, 比较常用
void stu_40(){
printf("stu_16: 类模板分文件编写 \n");;
// 成员函数只会在调用阶段才会创建, 编译器在编译阶段不会检查 类模板中的代码, 只会在调用的时候检查
Student_40<string, int> s = Student_40<string, int>("张三", 15); // 如果只包含头文件, 编译器编译不会报错, 但是只要运行就要报错: linker command failed with exit code 1
s.show();
// 第一种解决方式: 直接包含 .cpp 源文件, 而由于 .cpp 中又 include .h头文件, 所以编译器又去 .h头文件 文件中查看, 这样编译器看完了全部的文件
// Student_40<string, int> s = Student_40<string, int>("小明", 12);
// s.show();
// 第二种解决方式: 声明和实现 写在一个文件中, 不要再分开切了, 并且写在一起的文件名约定俗成为 .hpp
}
template<class NameType, class AgeType>
class Student_41 {
friend void show_41(Student_41<NameType, AgeType> &s) {
cout << "类模板友元函数, 类内实现: {}" << s.name << " " << s.age << endl;
}
private:
NameType name;
AgeType age;
public:
Student_41(NameType name, AgeType age){
this->name = name;
this->age = age;
}
};
void stu_41(){
printf("stu_16: 类模板与友元\n");
// 全局函数类内实现: 直接在类内声明友元即可 (TODO 在类模板中添加 friend 关键字之后, 该方法就变成了一个全局函数, 可以再全局调用, 脱离了类本身)
Student_41<string, int> s = Student_41<string, int>("小明", 14);
// show_41(); // TODO 变为全局函数调用, 这种写法只能在 vs 编译器上才能编译成功
// 全局函数类外实现: 需要提前让编译器知道全局函数的存在 (太复杂了, 这么复杂有个球用 不搞了)
}
template <class T>
void func_vector(T a){
cout << "第三种方法遍历 回调函数: " << a << endl;
}
// 一个自定义的类
class Student_42{
public:
string name;
Student_42(string name){
this->name = name;
}
};
void stu_42(){
printf("stu_16: STL 初识\n");
// 每一个容器都有自己的迭代器, 迭代器是用来遍历容器中的元素
// begin 方法返回迭代器的指针, 这个迭代器指针指向容器中的第一个数据
// end 方法返回容器 最后一个元素 的 下一个位置 的指针
// 1.1 创建vector容器对象, 并且通过模板参数指定容器中存放的数据的类型
vector<int> vi = vector<int>();
// 1.2 添加元素到容器内
vi.push_back(1);
vi.push_back(2);
// 1.3.1 第一种遍历方式 使用 while 循环
vector<int>::iterator vt_begin = vi.begin();
vector<int>::iterator vt_end = vi.end();
while (vt_begin != vt_end) {
cout << "第一种方式遍历 while循环: " << *vt_begin << endl;
vt_begin++;
}
// 1.3.2 第二种遍历方式 使用 for 循环
for (vector<int>::iterator v_begin = vi.begin(); v_begin != vi.end(); v_begin++) {
cout << "第二种方式遍历 for循环: " << *v_begin << endl;
}
// 1.3.3 第三种遍历方式 使用STL 提供的标准遍历算法(使用前需要引入 头文件 algorithm), 检查源码,其实也和第二种方法相同, 只不过这里把每次遍历的结果放入到回调函数中
for_each(vi.begin(), vi.end(), func_vector<int>);
// 1.4 存放自定义数据类型
vector<Student_42> vs = vector<Student_42>();
vs.push_back(Student_42("张三"));
vs.push_back(Student_42("李四"));
for (vector<Student_42>::iterator vs_begin = vs.begin(); vs_begin < vs.end(); vs_begin++) {
cout << "迭代器本身是一个指针, 可以直接 * 解引用使用: " << (*vs_begin).name << endl;
}
for (vector<Student_42>::iterator vs_begin = vs.begin(); vs_begin < vs.end(); vs_begin++) {
cout << "迭代器本身是一个指针, 也可以使用 -> 箭头来使用: " << vs_begin->name << endl;
}
// 存放对象指针(容器内也可以存放对象的指针, 使用时 需要解引用, 再解引用得到本身的值)
vector<Student_42 *> vs_ptr = vector<Student_42 *>();
Student_42 _s1 = Student_42("王五");
Student_42 _s2 = Student_42("赵六");
vs_ptr.push_back(& _s1);
vs_ptr.push_back(& _s2);
for (vector<Student_42 *>::iterator vs_ptr_iter = vs_ptr.begin(); vs_ptr_iter < vs_ptr.end(); vs_ptr_iter++) {
cout << "vector 也可以存放对象的指针,在使用时先解引用 即可得到对象的指针: " << (*vs_ptr_iter)->name << endl;
}
// vector容器中 存放 vector容器
vector<vector<int>> v_lis = vector<vector<int>>();
vector<int> v1 = vector<int>();
vector<int> v2 = vector<int>();
vector<int> v3 = vector<int>();
for (int i = 0; i < 4; i++) {
v1.push_back(i + 10);
v2.push_back(i + 20);
v3.push_back(i + 30);
}
v_lis.push_back(v1);
v_lis.push_back(v2);
v_lis.push_back(v3);
// 遍历嵌套容器
for (vector<vector<int>>::iterator v_1_begin = v_lis.begin(); v_1_begin < v_lis.end(); v_1_begin++) {
for (vector<int>::iterator v_2_begin = (*v_1_begin).begin(); v_2_begin < (*v_1_begin).end(); v_2_begin++) {
cout << *v_2_begin << " ";
}
cout << endl;
}
}
void stu_43(){
printf("stu_16: string 容器\n");
// 1. string 是 c++ 风格的字符串 (c风格 的为 char * ), 本质上是一个类, 是一个维护 char * 的容器
// 方法太多, 用的时候再查
// 2. 创建字符串的几种方式
// string(): 创建一个空的字符串 例如: string str;
// string(const char* s): 使用字符串s初始化
// string(const string& str): 使用一个string对象初始化另一个string对象
// string(int n, char c): 使用n个字符c初始化
// 1. 创建字符串 构造函数原型
string str_1 = string(); // string() 创建一个空的字符串
string str_2 = string("hello"); // string(const char * xxx)
string str_3 = string(str_2); // string(const string & xxx) 拷贝构造函数
string str_4 = string(10, 'a'); // string(int n, char c) 使用 n个 字符c 初始化字符串
// 2. 查看字符串大小
cout << "str_4 的字符串大小为: " << str_4.size() << endl;
// 3. 字符串赋值操作
string str_5 = string(" hi");
string str_6 = string("world");
str_6 += str_5;
char *_char_6 = " say";
str_6 += _char_6;
cout << "str_6 += 后 str_6 等于: " << str_6 << endl;
str_6.append(" hello");
cout << "str_6使用append 方法后 等于: " << str_6 << endl;
// 字符串 插入和删除
string str_7 = "hello";
str_7.insert(0, "hello "); // 插入
cout << "str_7 在0号位置 插入 char * 之后" << str_7 << endl;
str_7.erase(1, 2); // 从 1号 下标开始, 删除 2个 字符
cout << "从 1号 位置删除两个字符后, str_7为: " << str_7 << endl;
// 字符串 查找替换
string str_8 = string("abcdefg");
cout << "在字符串中 查找字符 \"f\" 的下标为: " << str_8.find("f") << endl;
str_8.replace(1, 2, "vvvv"); // 从下标1 的位置, 替换6个字符为 vvvv
cout << "字符串替换之后的新字符串为: " << str_8 << endl;
}
string print_v(vector<int> &v) {
string s = string();
for (int i = 0; i < v.size(); i++) {
char buf[512];
sprintf(buf, "%d ", v[i]);
s += buf;
}
return s;
}
string print_l(const list<int> &lis) {
string s = string();
for (list<int>::const_iterator li = lis.begin(); li != lis.end(); li++) {
char buf[512];
sprintf(buf, "%d ", li);
s += buf;
}
return s;
}
void stu_44(){
printf("stu_16: vector 容器\n");
// 和数组非常相似, 类似单端数组, 但是 vector 可以进行动态扩展
// vector 在扩展空间时, 会寻找更大的内存空间, 然后将原数据拷贝到新空间, 释放源空间, 造成了一次内存拷贝,所以我们最好提前指定 预留空间大小, ps: 已经扩大的容器无法通过clear resize pop erase 来缩小容量
// vector 构造函数
vector<int> v0 = {1, 2, 3};
vector<int> v1 = vector<int>(); // 无参构造
v1.push_back(1);
v1.push_back(2);
vector<int> v2 = vector<int>(v1); // 拷贝构造
vector<int> v3 = vector<int>(v2.begin() + 2, v2.end()); // 从v2 第 2 下标开始到最后的区间 创建容器 v3
vector<int> v4 = vector<int>(10, 999); // 创建一个长度大小为 10, 默认内容为 999 的 vector
// 判断是否为空 empty
if (!v4.empty()) {
cout << "v4 不为空" << endl;
}
// 提前预留空间 reserve
vector<int> v5 = vector<int>();
v5.reserve(10000);
// 容器的容量和大小 capacity size
v5.push_back(1);
v5.push_back(2);
v5.push_back(3);
v5.push_back(4);
cout << "v5容器的容量为: " << v5.capacity() << endl;
cout << "v5容器目前大小: " << v5.size() << endl;
// vector 的重置与清除 resize clear
v5.resize(3); // 重新指定容器的长度为num, 若容器变长, 则以默认值填充新位置, 如果变短, 则超长的元素被丢弃
cout << "v5 resize(3) 容量: " << v5.capacity() << endl;
cout << "v5 resize(3) 大小: " << v5.size() << endl;
v5.clear(); // 清除所有元素( 但是容器本身的容量不会发生改变)
cout << "v5 clear() 容量: " << v5.capacity() << endl;
cout << "v5 clear() 大小: " << v5.size() << endl;
// 数据的插入/删除/赋值 insert erase assign
vector<int> v6 = vector<int>();
v6.push_back(1); // 末尾插入
v6.push_back(2);
v6.push_back(3);
v6.push_back(4);
v6.insert(v6.begin() + 3, 100); // 在下标为3的位置插入 一个元素 100
v6.insert(v6.begin() + 1, 2, 999); // 在下标为3的位置插入 两个为 999 的元素
cout << "经过一系列的插入 v6为: " << print_v(v6) << endl;
v6.erase(v6.begin() + 1); // 删除指定位置 下表为1 的元素
cout << "删除下标为1 的元素后, v6 为: " << print_v(v6) << endl;
v6.erase(v6.begin() + 2, v6.end()); // 删除一个区间的元素
cout << "删除一个区间的元素后, v6 为: " << print_v(v6) << endl;
cout << "直接使用 中括号传入下标 读取, 因为 重载了 operator[]: " << v6[0] << endl;
cout << "使用at 方法传入下标读取也可以: " << v6.at(0) << endl;
vector<int> v7 = vector<int>();
v7.assign(v6.begin(), v6.end()); // 使用一个迭代器区间赋值
v7.assign(10, 999); // 容器中有 10 个 999
v7 = v6; // operator= 拷贝构造 赋值
cout << "经过了一系列赋值 v7: " << print_v(v7) << endl; // 1 999 和v6 一样
// 数据的交换: TODO 由于vector在删除之后, 容量并 不会随之缩小, 我们需要 使用 swap 方法, 创建一个新的容器, 并相互交换两个容器的内容, 因为创建的新的容器属于临时变量, 所以在所在行运行之后, 就被销毁, 不会造成内存浪费
vector<int> v8 = vector<int>();
for (int i = 0; i < 10000; ++i) {
v8.push_back(i);
}
cout << "经过插入数据 v8的 容量大小: " << v8.capacity() << ", v8的元素个数: " << v8.size() << endl;
v8.erase(v8.begin() + 3, v8.end());
cout << "clear 之后数据 v8的 容量大小: " << v8.capacity() << ", v8的元素个数: " << v8.size() << endl; // 虽然erase 了但是 容器的容量并没有缩小, 导致一直占用内存空间
vector<int>(v8).swap(v8); // vector<int>(v8) 这里会根据v8创建一个内容和v8一样的一个临时变量(语句结束被销毁), .swap方法会交换两个容器达到缩小容器的目的
cout << "经过一个临时变量交换之后 v8的 容量大小: " << v8.capacity() << ", v8的元素个数: " << v8.size() << endl;
}
void stu_45(){
printf("stu_16: list 容器\n");
// TODO 不支持随机访问的迭代器, 的容器, 不可以使用标准算法, 比如 全局sort函数, 但是内部会提供一些方法,这里需要使用list自己的 sort 方法
// 双端链表, 可以两端插入和删除, 插入和删除比 vector 快, 但是查找和遍历比 vector 慢, 不允许随机访问 front back 或者 迭代器 ++ -- 不允许 +n
// list 的迭代器只能前移和后移, 不支持跳跃访问, 且无法使用下标 或者 at 方法进行随机访问元素
// 反转和排序 需要使用本身提供的方法,
}
void stu_46(){
printf("stu_66: set 容器/multiset 容器\n");
// set容器不允许有重复的值, 且会自动排序
// set 插入insert 方法会返回一个 pair对象, 包含一个这个元素插入在set中的 下标的迭代器 和 插入结果
// set 没有resize方法, 因为resize会使用默认的值填充, 而set容器不允许有重复的值
// set 默认从小到大排序在插入之时就已经无法改变顺序了, 如果需要指定排序规则, 则需要在创建 set 时就要指定 仿函数
// set容器存放自定义数据类型, 都需要手动指定排序规则, 因为我们自定义数据类型有可能没有实现 operator< 或 operator>
}
void stu_47(){
printf("stu_66: pair 对组\n");
// 对组的创建方式: 直接对象创建
pair<int, string> p1 = pair<int, string>(1, "one");
// 对组的创建方式: 全局函数创建
pair<int, vector<char>> p2 = make_pair(3, vector<char>('3'));
// 对组的获取
cout << "对组使用 first 属性获取第一个值: " << p1.first << ", 对组使用 second 属性获取第二个值: " << p1.second << endl;
}
void stu_48(){
printf("stu_66: map 容器\n");
// map 的插入操作 需要插入使用 pair对组 对象, map不允许插入重复的key, 后续插入的相同的key, 则插入不成功 (multimap 允许有重复的key)
// 所有元素都会根据元素的键值 从小到大 自动排序, 如果需要指定排序规则, 则需要在创建map之时 指定排序 仿函数
// map在指定排序规则时, 和set 稍微不同, map需要的仿函数需要加const 变为一个 仿常函数
map<int, char> mp1 = map<int, char>();
// 插入和删除
mp1.insert({1, '1'});
mp1.insert(make_pair(2, '2')); // 使用全局函数, 创建插入一个pair 对象
mp1.insert(pair<int, char>(3, '3')); // 插入一个pair 对象
for (map<int, char>::const_iterator it = mp1.begin(); it != mp1.end(); it++) {
cout << "插入之后 遍历map first: " << it->first << ", second: " << it->second << endl;
}
mp1.erase(2); // 删除指定key
auto ret = mp1.erase(mp1.begin()); // 从 begin++ 的地方开始往后删, 删除成功后返回下一个元素的迭代器
for (map<int, char>::const_iterator it = mp1.begin(); it != mp1.end(); it++) {
cout << "删除之后 遍历map first: " << it->first << ", second: " << it->second << endl;
}
// 查找和统计
// find(key): 查找 key 是否存在, 存在则返回该 key的元素的迭代器, 如果不存在, 则返回 end
// count: 统计 key 出现的个数, 如果是map 那肯定要不是0要不是1, multimap允许有重复的key 会有多个
// 创建 map 时指定排序规则 TODO 注意这里 map需要使用的仿函数, 需要是一个常函数, 这里和 set 不同
class MyFunction{
public:
bool operator()(int a, int b) const { // 这里需要加const 变为一个常函数
return a > b;
}
};
// 在创建 map 的时候指定排序规则, 接收一个仿函数
map<int, char, MyFunction> m1;
m1.insert(pair<int, char>(1, '1'));
m1.insert(make_pair(2, '2'));
for (map<int, char, MyFunction>::iterator it = m1.begin(); it != m1.end(); it++) {
cout << "指定排序规则, 创建 map 时指定仿函数, 遍历map first: " << it->first << ", second: " << it->second << endl;
}
}
void stu_49(){
printf("stu_66: 函数对象(仿函数)/谓词\n");
// 谓词: operator() 返回 bool值 的叫做谓词, 仿函数如果需要 1个参数就叫一元谓词, 2个参数叫做两元谓词
class MyFunction{
public:
bool operator()(int a, int b) const { // 两元谓词
return a > b;
}
};
// 在创建 map 的时候指定排序规则, 接收一个仿函数
map<int, char, MyFunction> m1;
m1.insert(pair<int, char>(1, '1'));
m1.insert(make_pair(2, '2'));
for (map<int, char>::iterator it = m1.begin(); it != m1.end(); it++) {
cout << "指定排序规则, 创建 map 时指定仿函数, 遍历map first: " << it->first << ", second: " << it->second << endl;
}
}
void stu_50(){
printf("stu_66: 常用仿函数\n");
// 仿函数在使用时间, 需要包含他 functional 头文件
// 1. 关系运算仿函数
// template<class T> bool equal_to<T> //等于
// template<class T> bool not_equal_to<T> //不等于
// template<class T> bool greater<T> //大于
// template<class T> bool greater_equal<T> //大于等于
// template<class T> bool less<T> //小于
// template<class T> bool less_equal<T> //小于等于
// 2. 逻辑仿函数
// template<class T> bool logical_and<T> //逻辑与
// template<class T> bool logical_or<T> //逻辑或
// template<class T> bool logical_not<T> //逻辑非
// 使用内置的大于仿函数
map<int, int, greater<int>> m1 = map<int, int, greater<int>>();
m1.insert(make_pair(1, 20));
m1.insert(make_pair(2, 20));
for (map<int, int>::iterator it = m1.begin(); it != m1.end(); it++) {
cout << "指定排序规则, 创建 map 时指定 greater 大于仿函数, 遍历map first: " << it->first << ", second: " << it->second << endl;
}
}
void stu_51(){
printf("stu_66: 常用算法 \n");
// 1. 常用遍历算法
// 1.1 for_each(iterator begin, iterator end, _func) 根据_func 依次遍历元素 begin:起始位置迭代器, end: 结束位置
vector<int> v1 = {1, 2, 3, 4};
for_each(v1.begin(), v1.end(), [&](int a)->void { cout << "for_each 遍历的结果: " << a << endl; });
// 1.2 transform(iterator beg1, iterator end1, iterator beg2, _func); 搬运容器到另一个容器中
vector<int> v2 = {1, 2, 3, 4, 5};
vector<int> v3 = {};
v3.resize(v2.size()); // 需要开辟好空间
transform(v2.begin(), v2.end(), v3.begin(), [&](int a)->bool{return a * 10;});
cout << "transform 把v2 的值 *10 后存入 v3, v3为: " << print_v(v3) << endl;
// 2. 常用查找算法
// 2.1 find(iterator beg, iterator end, value) 按值查找元素,找到返回指定位置迭代器,找不到返回结束迭代器位置
vector<int> v4 = {1, 2, 3, 4};
vector<int>::iterator ret_v4 = find(v4.begin(), v4.end(), 5);
cout << "find 返回目标元素的迭代器, 如果没找到返回 end位置: " << (v4.end() == ret_v4) << endl;
// 2.2 find_if(iterator beg, iterator end, _Pred) 根据谓词 返回满足条件的元素位置的迭代器, 找不到返回结束迭代器的位置
// 2.2 adjacent_find(iterator beg, iterator end) 查找重复的相邻元素, 返回相邻元素的第一个位置的迭代器
vector<int> v5 = {2, 3, 4, 5, 56, 56};
vector<int>::iterator ret_v5 = adjacent_find(v5.begin(), v5.end());
cout << "adjacent_find 寻找两个相邻的元素为: " << *ret_v5 << " " << *(ret_v5++) << endl;
// 2.3 binary_search(iterator beg, iterator end, value) 二分查找效率很高, 注意在无序序列中不可用 比如: vector<int> v = {1, 2, 1, 3, 2} 没有顺序是不可以使用的
// 2.3 count(iterator beg, iterator end, value); 统计元素个数
// 2.4 count_if(iterator beg, iterator end, _Pred) 按照条件统计元素个数
// 3. 常用排序算法
// 3.1 sort(iterator beg, iterator end, _Pred) sort默认从小到大 使用小于关系仿函数 排序
vector<int> v6 = {1, 2, 3, 4, 5};
sort(v6.begin(), v6.end(), greater<int>()); // 默认使用 less, 这里使用 greater 从大到小排序
cout << "sort 指定谓词, 从大到小排序后, v6为: " << print_v(v6) << endl;
// 3.2 random_shuffle(iterator beg, iterator end) 对序列随机调整顺序
random_shuffle(v6.begin(), v6.end());
cout << "random_shuffle 对v6打乱了顺序后, v6为: " << print_v(v6) << endl;
// 3.3 merge(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest) 两个容器合并到第三个容器中
// 注意两个容器必须是有序的
// 目标容器需要提前开辟内存空间
vector<int> v7 = {1, 2, 3};
vector<int> v8 = {5, 6, 7, 8};
vector<int> v9 = {};
v9.resize(v7.size() + v8.size()); // 提前预留空间
merge(v7.begin(), v7.end(), v8.begin(), v8.end(), v9.begin());
cout << "merge 合并两个容器元素到一个新的容器 v9: " << print_v(v9) << endl;
// 3.4 reverse(iterator beg, iterator end) 反转指定范围的元素
// 4. 常用拷贝和替换算法
// copy // 容器内指定范围的元素拷贝到另一容器中
// replace // 将容器内指定范围的旧元素修改为新元素
// replace_if // 容器内指定范围满足条件的元素替换为新元素
// swap // 互换两个容器的元素
// 常用算术生成算法, 使用时包含的头文件为 #include <numeric>
// accumulate // 计算容器元素累计总和
// fill // 向容器中填充指定的值
vector<int> v10 = {1, 2, 3};
int sum_v10 = accumulate(v10.begin(), v10.end(), 0);
cout << "accumulate 计算 vector: " << print_v(v10) << ", 的值为: " << sum_v10 << endl;
// 5. 常用集合算法
// set_intersection // 求两个容器的交集
// set_union // 求两个容器的并集
// set_difference // 求两个容器的差集
}
void stu_52(){
printf("stu_66: lambda 表达式\n");
// lambda 加了糖的仿函数/函数对象(lambda没有默认构造函数)
// lambda 默认是 inline function
// [...](...) mutable/throwSpce -> retType {...}
// 如果没有 mutable/throwSpce -> retType 这些选项, 那么lambda可以简写为 [...]{...}
// lambda 的类型可以通过 auto 自动推导 或者 decltype函数得到
// [传值方式](参数列表)->返回值类型 {函数体}(立即执行)
// 关于传值方式, 概念参考RUST, 这里只介绍几种传值方式的使用方法:
// 空: 没有任何函数对象参数。
// =: 函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
// &: 函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是引用传递方式(相当于是编译器自动为我们按引用传递了所有局部变量)。
// this: 函数体内可以使用 Lambda 所在类中的成员变量。
// a: 将 a 按值进行传递。按值进行传递时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下函数是 const 的,要修改传递进来的拷贝,可以添加 mutable 修饰符。
// &a: 将 a 按引用进行传递。
// a,&b: 将 a 按值传递,b 按引用进行传递。
// =,&a,&b: 除 a 和 b 按引用进行传递外,其他参数都按值进行传递。
// &,a,b: 除 a 和 b 按值进行传递外,其他参数都按引用进行传递。
int a = 10;
[&a]()mutable ->void{ a = 20; }(); // 传入一个引用的a, 并且立即执行
cout << "经过 lambda 表达式修改后 a为: " << a << endl;
}
class Stu_53{
public:
void show_53();
void say() const { cout << "hello!" << endl; } // 写在类内, 编译器会尽量为我们便以为内联函数, 具体我们无法控制
};
inline void Stu_53::show_53() {
cout << "在外部声明函数的时候添加 inline 关键字, 编译器会尽量为我们的函数编译成内联函数" << endl;
}
void stu_53(){
printf("stu_66: 类的内联函数\n");
// 内联函数 性能会较外部 稍快, 写在类内的函数, 编译器在编译时,会尽量为我们编译为内联函数, 如果函数太复杂编译器可能无法编译成内联函数
// 在外部声明函数的时候添加 inline 关键字, 编译器会尽量为我们的函数编译成内联函数, TODO 当然也只是 "尽量", 具体我们无法控制
// 其他补充, 类方法中如果没有修改成员变量, 那么应该尽量增加 const 关键字
Stu_53 s_53 = Stu_53();
s_53.say();
}
void stu_54(){
printf("stu_66: 常对象/常函数\n");
// 常对象中, 只能调用常函数
class Stu{
public:
Stu(int age) {
this->age = age;
}
int show_1(){
return age;
}
int show_2() const {
return age;
}
private:
int age;
};
const Stu s1 = Stu(12);
// 尝试 从 类方法中获取 s1 的age
// s1.show_1();
// 调用 常函数
s1.show_2();
}
class Stu_55{
private:
int age;
public:
Stu_55(int age){
this->age = age;
}
// 不同的实例互为友元
void show(const Stu_55 &s) {
cout << this->age << "访问到了 : " << s.age << endl;
}
};
// 普通的函数, 禁止访问 私有属性
void show(const Stu_55 &s) {
// cout << "今年: " << s.age << endl; // 访问报错了
}
void stu_55(){
printf("stu_66: 友元(相同的各个 class 实例互为 friends 友元)\n");
Stu_55 s12 = Stu_55(12);
Stu_55 s13 = Stu_55(13);
s12.show(s13);
}
void stu_56(){
printf("stu_66: 关于拷贝构造函数 和 拷贝赋值 调用的备注\n");
string s1 = "hello";
// string s2(s1);
string s2 = s1; // TODO 注意这里是调用了 拷贝构造函数, 而不是 拷贝赋值, 因为这里 是新创建了一个 s2
string s3;
s3 = s2; // 这里才是调用了 拷贝赋值 的函数
}
void stu_57(){
printf("stu_66: new 底层实现\n");
// 在new 时, 会 先分配内存, 再调用构造函数
int *i = new int(123);
// 上面的代码编译器会转化为如下
int *_i; // 声明变量
try {
void *mem = operator new(sizeof(int)); // 1. 分配内存
_i = static_cast<int *>(mem); // 2. 转型, 无类型指针, 转为指定类型指针
// _i-> 类名::构造函数(123) // 3. 调用构造函数(这种动作, 只有编译器才可以做, 我们是不可以调用的)
} catch (std::bad_alloc) {
}
}
void stu_58(){
printf("stu_66: new 动态分配 所得到的内存块剖析\n");
// 在 使用new 在堆区开辟一个内存时, 会在头尾部 隐性 各增加一个cookie, 用来记录整块内存的长度大小, cookie包裹内 就是内存的大小, cookie 头的内存地址的最后一位用来区分内存当前状态 0未分配, 1已经分配
// 在 new 一个数组的时候, 会增加 1 个字节 用来记录数组的长度
// array new 一定要 搭配 array delete 使用, 不然 drop只会调用一次 array 中的元素只会销毁一个, 剩下的没有销毁, 会造成内存泄漏
class Double{
int a, b;
public:
Double(int a, int b){
this->a = a;
this->b = b;
}
};
// 创建一个普通的对象
Double d1 = Double(1, 2);
cout << "创建一个普通对象的大小为: " << sizeof(d1) << endl;
// 使用 new 关键字创建一个 动态对象
Double *d2 = new Double(2, 3);
cout << "创建一个动态对象大小为: " << sizeof(d2) << endl;
delete d2;
// class V{
// public:
// ~V(){
// cout << "V 析构函数调用了 " << endl;
// }
// };
//
// V v4 = V();
// V v5 = V();
//
// V *p = new V[2];
//
// cout << "----" << endl;
// p[0] = v4;
// p[1] = v5;
// // delete p; // 已经出现警告了: 'delete' applied to a pointer that was allocated with 'new[]'; did you mean 'delete[]'?
// delete[]p;
// cout << "----" << endl;
// TODO 下面这个例子 我没有做成功, QAQ
class P{
public:
~P(){
cout << "P 被析构了" << endl;
}
};
class V{
public:
P *p;
V(){
this->p = new P;
}
~V(){
delete p;
cout << "V 被析构了" << endl;
}
};
V * v = new V[3];
for (int i = 0; i < 3; ++i) {
v[i] = V();
}
cout << "-----" << endl;
delete[] v; // array new 一定要 搭配 array delete 使用
}
//class Once_class{
//private:
// Once_class();
// Once_class(const Once_class &rhs);
//public:
// static Once_class& get_instance();
// void show(){
// cout << "show" << endl;
// }
//};
//Once_class & Once_class::get_instance() {
// static Once_class o;
// return o;
//}
void stu_59(){
printf("stu_66: 单例模式\n");
// 单例模式的核心就是 把构造函数写入到私有属性当中, 这样就无法实例化对象, 并 使用 static 静态成员变量
// 下面我并不能运行 ,很奇怪, c++报错信息也太少了
// Once_class::get_instance().show();
}
void stu_60(){
printf("stu_66: 转换函数/自动类型转换\n");
// 注: 转换函数不用写返回值类型(程序员写, 写错了怎么办, 这里编译器已经做好了
// operator 想要转的类型(){ return xxx; }
// 如果在一些 表达式中, 比如 +-*/ 如果我们没有实现对应的运算符重载, 那么编译器会 尝试转换 我们自定义类型为目标类型
// 构造函数 发生隐性类型转换时, 构造函数,需要只有 一个 实参 才可以发生并调用
// 如果需要禁用 构造函数 某些转换, 可以添加 explicit 关键字, 明确某些 方法/函数 的功能, 禁止隐性类型转换调用, 如下例子:
class My_int{
private:
int i;
public:
operator int() const {
return i * 10;
}
operator string() const {
return to_string(this->i);
}
My_int(const int i) {
cout << "创建了: " << i << endl;
this->i = i;
}
};
My_int i1 = My_int(123);
cout << "我们使用: operator 类型(){} 可以指定转换函数, 转为 string: " << string(i1) << " "<< typeid(string(i1)).name() << endl;
cout << "我们使用: operator 类型(){} 可以指定转换函数 转为 int " << int(i1) << " " << typeid(int(i1)).name() << endl;
// 虽然没有实现运算符重载, 但是编译器 会尝试为 表达式转换为目标类型 int
int i2 = i1 + 10;
cout << "i1 + 2 = " << i1 << " + " << 10 << " = " << i2 << endl;
// 虽然不是目标类型, 但是这里并不影响 传入数组中, 因为编译器在发现类型不满足的时候, 会尝试为我们转为 目标类型
int v[3] = {};
v[0] = My_int(0); // 这里会发生类型转换
v[1] = My_int(1);
v[2] = My_int(2);
for (int i = 0; i < sizeof(v) / sizeof(v[0]); ++i) {
cout << "循环打印v, v中的值: " << v[i] << endl;
}
// 我们期望的是 自定义类型的数组, 但是给的确实 int类型的数组, 这时编译器会尝试为我们 转换创建 为指定类型
My_int m_li[3] = {4, 5, 6};
// 编译器为我们创建 目标类型
My_int m1 = 123;
// TODO 添加 explicit 关键字, 明确 函数/方法 的功能, 禁止隐性类型转换调用
class My_double_1{ // 一个普通的类
private:
double d;
public:
My_double_1(double d){
cout << "My_double_1 被调用了 " << endl;
this->d = d;
}
};
My_double_1 d1 = 1; // 这里 构造函数 会把int 隐性转为 double
class My_double_2{ // 尝试给这个类型的 构造函数 添加 explicit关键字
private:
double d;
public:
explicit My_double_2(double d){
cout << "My_double_2 被调用了 " << endl;
this->d = d;
}
};
// My_double_2 d2 = 1; // 显然 这里禁止隐性类型转换 Error: no viable conversion from 'double' to 'My_double_2'
My_double_2 d2 = My_double_2(1.2);
}
class Foo;
template<class T>
class My_ptr { // 模仿智能指针
public:
T& operator * () const { // 解引用 需要返回 px指针指向的值
return *px;
}
T* operator -> () const { // 这里需要返回一个指针, 因为 自定义只能指针通过 -> 调用之后, 这里继续返回指针 我们的保存的数据
cout << "My_ptr -> 调用了" << endl;
return px;
}
My_ptr(Foo *p):px(p){
}
private:
T *px;
long *pn;
};
class Foo{
public:
void show(void ){
cout << "Foo show!" << endl;
}
};
void stu_61(){
printf("stu_66: 箭头符号/智能指针初探\n");
// 箭头符号 作用下去得到的结果, 会继续箭头符号下去(只有这个符号,
My_ptr<Foo> m = My_ptr<Foo>(new Foo()); // 模仿智能指针
m->show(); // 这里 m-> 会消耗掉 operator ->, 并返回 T*, 但是由于 -> 符号的特殊性, 返回值会继续 -> 下去
(*m).show(); // 解引用调用 和上面一样都能调用
}
template<class T>
class M_c{ // 任意类型都可以满足这个要求
public:
M_c(){
}
};
template<>
class M_c<int> { // 指定类型, 对 int 特殊处理
public:
M_c() {
cout << "int 泛型特化 的特殊处理" << endl;
}
};
template<>
class M_c<long> { // 指定类型, 对 long 特殊处理
public:
M_c() {
cout << "long 泛型特化 的特殊处理" << endl;
}
};
void stu_62() {
printf("stu_66: 泛化特化\n");
// 泛化: 任意类型都可以满足要求
// 特化: 但是某些具体的实现, 需要特殊的处理
// 特化的语法: 在声明类的后面 使用 <类型> 来指定类型
// 泛化
M_c<char> mc = M_c<char>();
M_c<float> mf = M_c<float>();
// 特化(对某些类型进行特殊处理)
M_c<long> ml = M_c<long>();
M_c<int> mi = M_c<int>();
}
void stu_63(){
printf("stu_66: x\n");
// 标准库中的 vector 对 bool 值进行了 局部特化
}
void stu_64(){
printf("stu_66: 模板的模板参数\n");
// 太复杂了, 有用到的时候再查吧
// T<U<int>>
}
void print() {} // 重载没有传入参数的 函数的调用, 防止无限递归
// 声明一个泛型T 和一个收集参数类型(使用typename... 来声明这是一个收集参数类型)
template<typename T, typename... Types>
void print(const T & t, const Types&... args){ // 语法很奇怪
cout << "The pack number is " << sizeof...(args) << " " << t << endl;
print(args...); // 展开 收集参数
}
void stu_65(){
printf("stu_65: 数量不定模板参数\n");
// ... 用来组包, 以及解包, 我们可以很方便的 实现递归
// ... 可以是任意类型
// Variadic Templates 和 initializer list 是有区别的, initializer list 内只能接受统一类型
print(1, 'a', "hello", 1.234);
}
void stu_66(){
printf("stu_66: auto 关键字\n");
// 在类型名很长的情况下, 我们可以使用 auto 关键字,让编译器自动推断返回值类型, 减少代码量
// 注意使用 auto 的变量, 必须为其赋值, 有初值才能自动推断
vector<int> v1 = {1, 2, 3};
// 手动写完整类型
vector<int>::iterator ite = v1.begin();
// 使用 auto 关键字, 让编译器自动推断
auto ite_a = v1.begin();
// 必须为其赋值, 有初值才能自动推断, 下面这一句是错误的
// auto ite_;
}
void stu_67(){
printf("stu_66: ranged-base for: 新的 for 循环\n");
// 更方便的 for 循环: for(类型 变量: 容器){}
// 编译器会把 容器内的元素, 一个一个拿出来赋值到变量
// 在使用的时候, 赋值到变量时是 pass by value, 我们也可以 pass by reference, 传引用性能更快, 如果不加 const 关键字, 可以直接修改引用指向的容器中的源数据
// for 实际上是迭代器 加了糖 我们可以直接使用引用, 脱糖后代码类似下面
// for (vector<int>::iterator it = v.begin(); it < v.end(); it++) {
// int ret = *it; // 正常的 for
// int& ret = *it; // 如果进行了 引用操作 这里会使用引用的值
// }
vector<int> v = {2, 2, 3};
for (int i = 0; i < v.size(); ++i) {
cout << "传统的遍历一个容器的方法: " << v[i] << endl;
}
vector<int> v1 = {1, 2, 3, 5};
for (int i:v1){
cout << "使用语法糖 for 循环容器中 int 类型的元素: " << i << endl;
}
vector<char> v2 = {'a', 'b', 'c'};
for (auto i:v2){
cout << "使用语法糖 for 循环容器中 auto 类型的元素: " << i << endl;
}
// pass by reference, 性能更快
vector<int> v3 = {7, 8, 9};
for (int &i:v3){
// for (int const &i:v3){ // 添加const 关键字, 也可以禁止引用修改
i += 100; // 因为传进来的是引用, 我们这里修改, 可以直接影响到源容器中的数据
}
for(int i:v3){
cout << "pass by reference 修改之后再次循环, 查看容器中的值: " << i << endl;
}
}
void stu_68(){
printf("stu_66: reference 再次探究\n");
// 指针更像是指向一块内存, 而 reference 更像是代理/代表(作为函数的参数传递, 引用表现得更好, 参数的使用更接近 pass by value)
// reference 一旦声明初值, 则不能够重新代表其他 "物体"
// 对象 和 reference 的大小相同, 以及内存地址也相同(全都是假象, 引用本质上是一个指针常量)
// reference 作为函数的签名, 不能够作为重载的条件, 会产生歧义
string s1 = "hello";
string *s1_p = &s1; // 创建一个指针
string &s1_r = s1; // 创建一个引用
cout << "对象 string s1 的大小为: " << sizeof(s1) << endl;
cout << "指针 string *s1_p 的大小为: " << sizeof(s1_p) << endl;
cout << "引用 string &s1_r 的大小为: " << sizeof(s1_r) << endl; // 大小和 s1 相同, 由此可见, s1_r 就是 s1, s1 就是 s1_r;
string s2 = " world";
s1_r = s2; // 引用不能重新代表其他 "物体", 这里 s1 和 s1_r 都是 " world"了
}
void stu_69(){
printf("stu_66: const 再次探究\n");
// 当常量函数 和普通函数同时存在, 常量对象只能调用常量函数, 非常量对象只能调用非常量函数
class Stu{
public:
void say(){
cout << "普通函数调用了" << endl;
}
void say() const {
cout << "常量函数调用了" << endl;
}
};
Stu s1 = Stu();
s1.say();
Stu const s2 = Stu();
s2.say();
}
void func_null_test(int i) {
cout << "调用了 int 参数的函数" << endl;
}
void stu_70(){
printf("stu_66: nullptr 替换 NULL/0\n");
// 这样做更加语义化, 因为设置一个指针为空, v = NULL; v = 0(歧义太明显了), 语义化不如 nullptr
// 为了修复下面的调用 下面这个 明明传入的是 NULL 空指针, 但是却进入了 int 类型的调用, NULL 是 0
func_null_test(NULL);
// 新的
while (true) {
int *i = new int(1);
delete i; // 释放堆区内存
i = nullptr; // 设置指针指向为空
}
}
class Stu_71{
public:
int age;
string name;
Stu_71(int a, string n) : age(a), name(n) {}
};
void stu_71(){
printf("stu_66: initializer list 一致性初始化\n");
// 任何初始化变量都可以使用 大括号
// 编译器遇到 {} 便生成 initializer list <T>, 他关联到一个 c++2.0 的新容器 array<T, n>, 该容器内的元素可以被编译器分解逐一传给类型的 构造函数
// 一些情况除外, 比如一个专门接收 initializer的签名, 则不会进行分解, 会优先匹配
// 禁止 缩小/窄化 转化, 比如double 转为 声明的int 类型
int i_li[] = {1, 2, 3};
vector<int> v1 = {1, 2, 3, 4};
Stu_71 s1 = {13, "小张"};
int i1; // 声明一个变量
int i2 = {}; // 声明一个变量, 并使用大括号设置一个初值 为 0
// int i3 = {1.23}; // 禁止 double 转为声明的 int 转换类型
double d1 = {1};
}
void print_i(std::initializer_list<int> i) {
for(auto &c:i){
cout << "initializer_list 接收的参数: " << c << endl;
}
}
void stu_72(){
printf("stu_66: initializer list class\n");
// initializer list 容器可以接受任意类型,(和...的区别是, ... 容器中可以是任意类型, initializer list容器中必须是相同类型)
// initializer list 这 "一包东西" 本质是一个 array(2.0的新容器), 编译器在准备好array容器之后, 会调用initializer list私有的构造函数, 把 头部和长度 传进去
// 接收不定长的参数
print_i({1, 2, 3, 4, 5});
max(1, 2);
// max(1, 2, 3); // 出错, 如果传入三个参数比大小则没有对应的处理函数
max({1, 2, 3, 4, 5}); // 没问题, max min 等算法 对 initializer list 有对应的重载
max({'a', 'b', 'c'});
}
class F_73{
public:
void show_1(){
cout << "show_1" << endl;
}
void show_2(){
cout << "show_2" << endl;
}
};
class S_73:public F_73{
public:
void show_2() = delete;
};
void stu_73(){
printf("stu_66: =default / =delete\n");
// =delete 可以用来删除 类中的一些方法 比如继承的方法, 或者构造方法 析构方法等
// =default 用来处理 构造函数的默认方法, 如果我们写了拷贝构造函数之后, 还需要默认构造函数(或者如果用户自己定义有参构造函数,那么编译器就不提供无参构造函数), 则可以再默认构造函数后 添加 =default
S_73().show_1(); // 可以调用
// S_73().show_2(); // 无法调用这个方法被删除了 error: attempt to use a deleted function
}
template <class T>
using Vec = std::vector<T>;
void stu_74(){
printf("stu_66: Alias Template\n");
// 通过 "别名" 来创建一个 vector
Vec<int> v = {1, 2, 3, 4};
for (auto i:v) {
cout << i << endl;
}
}
// 声明一个函数指针
using my_func_class = void (*)(int, int);
void f1(int, int) {
}
void stu_75(){
printf("stu_66: Alias Type\n");
// 在c++ 函数的名称就是函数的指针
my_func_class fn = f1;
}
void stu_76(){
printf("stu_66: noexcept\n");
// 下面就表示 只要x.swap(y)的方法 noexcept为true, 那么 swap也不会异常
// void swap(Type x, Type y) noexcept(noexcept(x.swap(y)))
// {
//
// }
}
template<class T1, class T2>
auto add(T1 x, T2 y) -> decltype(x+y) // 编译器推导的类型就是 x+y 运算符重载的返回类型
{
return x+y;
}
void stu_77() {
printf("stu_66: decltype\n");
// 在我们不知道返回值类型的时候, 可以让编译器进行推导
auto a = add(1, 2);
cout << "a 的类型为: " << typeid(a).name() << " " << typeid(decltype(a)).name();
}
void stu_78(){
printf("stu_66: c++ 的三元运算\n");
int a = 1 < 2 ? 100 : 200;
cout << a << endl;
}
// 例子1
void say_str(string &&s) {
cout << "move 的函数 " << s << endl;
}
void say_str(string &s) {
cout << "copy 的函数 " << s << endl;
}
void test_str(string &s) {
cout << "test_str 的函数 " << s << endl;
}
void stu_79(){
printf("stu_66: x\n");
// 临时变量和右值我们, 无法使用 void func(T &x) 的签名 来接收临时变量比如 func(123), 对右值的引用需要特殊处理 void func(T &&x)
// move解决一些不必要的copy, 比如容器在扩容的时候, 旧容器的内容需要转移到新容器, 如果一个一个copy 会制造一些临时对象后再内存拷贝会导致很慢
// c++ 中 临时变量编译器认为是右值; func(x): x就是右值 这里会发生一次copy 到函数栈内, 我们使用 move语义 可以使我们拿到右值的引用, 并让数据源的引用"断开"
// 对于左值也想让其转移所有权 可以使用move 关键字, (move 其实是 static_cast<T&&>() 的语法糖)
// move 对于vector容器扩容时性能提升巨大
// swap 也是操作的容器的首尾指针来进行交换容器
string s = "hello";
say_str(s); // 左值, 会调用 copy 的版本
say_str("hi"); // 临时变量编译器会认为是右值, 会调用 move 版本
say_str(move(s)); // 使用 move 关键字, 强制转为 move 语义
// test_str("hehe"); // test_str接收一个引用, 右值我们是拿不到引用的
}
// 例子2
void F2(string &s){
cout << "F2 copy " << s << endl;
}
void F2(string &&s){
cout << "F2 move " << s << endl;
}
void F1(string &&s){
F2(s);
}
void F3(string &&s){
F2(forward<string>(s));
}
void stu_80(){
printf("stu_66: move 语义的传递\n");
// 函数的参数 临时变量右值 传入函数内因为函数接受用到了形参变量接收, 就变得有名字了 就变为了左值, 但其实我们是想, 依然按照 move 语义 继续向下传递
// 如果我们想让其继续向下传递 可以使用 forward 函数进行转发传递
F1("你好"); // 这里我们想让其内部依然是 move语义, 但是运行结果却是copy的(因为参数传入F1 就为左值了)
F3("世界");
}
//#include "source_1/sou1.h"
//#include <sou1.h> // 因为 cmakelists 设置的头文件搜索路径所以可以直接使用
//
//#include <fmt/core.h>
//#include <fmt/chrono.h> // 时间需要的头文件
//#include <fmt/ranges.h> // 打印容器需要的头文件
//void stu_81(){
// printf("stu_66: x\n");
// show_sou1();
//
// // 打印标准输出
// fmt::print("hello, world\n");
//
// // 格式化字符串(可以使用下标来指定填充位置)
// string s1 = fmt::format("我叫: {0}, 今年 {2}岁, 上 {1}年级\n", "小明", "2", "7");
// cout << s1 << endl;
// // std::string s = fmt::format("I'd rather be {1} than {0}.", "right", "happy");
//
// // 打印时间
// using namespace std::literals::chrono_literals;
// fmt::print("Default format: {} {}\n", 42s, 100ms);
// fmt::print("strftime-like format: {:%H:%M:%S}\n", 3h + 15min + 30s);
//
//
// // 打印一个容器
// vector<int> v1 = {1, 2, 3, 4};
// string sv = fmt::format("打印一个vector: {0}", v1);
// cout << sv << endl;
//
//
// // 打印指定小数位
// fmt::print("小数指定4位 四舍五入 打印: {:.4f} \n", 3.1415926);
//
// // 字符宽度设置这里设置宽度为10, 字符摆放位置这里向右摆放, 字符串填充 使用*填充(默认是空格填充)
// fmt::print("10个字符宽度填充: {:*>10} \n", "小明今年8岁了");
//}
//void stu_82(){
// printf("stu_66: 异常处理\n");
// //throw: 当问题出现时, 程序会抛出一个异常, 这是通过使用 throw 关键字来完成的
// //catch: 在您想要处理问题的地方, 通过异常处理程序捕获异常, catch 关键字用于捕获异常
// //try: try 块中的代码标识将被激活的特定异常, 它后面通常跟着一个或多个 catch 块
//
// // ... 可以捕获任何异常
// // exception 所有标准 C++ 异常的父类
// // what() 是异常类提供的一个公共方法, 它已被所有子异常类重载, 这将返回异常产生的原因
//
// try {
// fmt::print("准备抛出异常\n");
// throw "致命错误";
// } catch (const char* msg) {
// fmt::print("捕获到了异常: {} \n", msg);
// }
//
//
// try {
// int a = 1 / 0;
// } catch (exception e) {
// fmt::print("捕获到了异常: {}", e.what());
// }
//}
void stu_83() {
printf("stu_83: 函数指针\n");
// 函数指针指向的是函数而非对象, 和其他指针一样, 函数指针指向某种特定类型, 函数的类型由它的返回类型和形参类型共同决定, 与函数名无关
// my_F前面有个 *, 因此 my_F是个指针, 右侧是形参列表, 表示 my_F 指向的是函数, 左侧发现函数的返回值类型是bool值, 因此 my_F就是一个指向函数的指针
bool (*my_F)(const string s1, const i32 i1);
// 注意 my_F两段的括号不可少, 如果不写这对阔考, 则 my_F 是一个返回值为 bool指针 的函数
bool *my_F_1(const string s1, const i32 i1);
}
int main(){
// 变量与运算符
// stu_1();
// 分支判断与循环
// stu_2();
// 数组
// stu_3();
// 函数
// stu_4();
// 指针
// stu_5();
// 结构体
// stu_6();
// 内存4大区
// stu_7();
// 引用
// stu_8();
// 函数的参数
// stu_9();
// 函数重载 overloading
// stu_10();
// 类和对象以及构造函数
// stu_11();
// 对象的深浅拷贝
// stu_12();
// 初始化列表
// stu_13();
// 类 的drop顺序 (基本和 Rust 这里概念差不多)
// stu_14();
// 静态成员变量/函数
// stu_15();
// 成员变量与成员函数 的存储
// stu_16();
// this 指针
// stu_17();
// 空指针访问成员属性
// stu_18();
// 常函数/常对象, const 修饰成员以及函数
// stu_19();
// 友元
// stu_20();
// 运算符重载
// stu_21();
// 继承
// stu_22();
// 继承中的对象内存模型
// stu_23();
// 继承中的 构造/析构 顺序
// stu_24();
// 继承中同名属性的处理
// stu_25();
// 多继承/菱形继承
// stu_26();
// 虚继承
// stu_27();
// 子类型/多态/虚函数
// stu_28();
// 纯虚函数/抽象类
// stu_29();
// 虚析构/纯虚析构
// stu_30();
// 文件操作
// stu_31();
// 函数模板(类似泛型)
// stu_32();
// 普通函数与函数模板的区别
// stu_33();
// 普通函数与函数模板的调用规则
// stu_34();
// 函数模板的局限性
// stu_35();
// 类模板
// stu_36();
// 类模板对象做函数模板
// stu_37();
// 类模板与继承
// stu_38();
// 类模板方法的类外实现
// stu_39();
// 类模板分文件编写
// stu_40();
// 类模板与友元
// stu_41();
// STL 初识
// stu_42();
// string 容器
// stu_43();
// vector 容器
// stu_44();
// list 容器
// stu_45();
// set 容器/multiset 容器
// stu_46();
// pair 对组
// stu_47();
// map 容器
// stu_48();
// 函数对象(仿函数)/谓词
// stu_49();
// 常用仿函数
// stu_50();
// 常用算法
// stu_51();
// lambda 表达式
// stu_52();
/* 知识拾遗系列-----下面是观看侯捷老师的视频 对 c++ 知识体系的补充 */
// 1. 数据尽量放在 private中, 方法尽量放在public中
// 2. 函数或方法的 参数以及返回值 首先考虑 reference 进行传递
// 3. TODO 如果方法不修改 变量, 那么 尽量 尽量 加上 const 关键字 (因为方法内使用属性, 编译器会为方法添加一个隐性的this 参数, 内部访问属性会隐性的增加 this->attr 指针
// 成员函数会隐性的增加一个 this 参数在函数签名中
// 4. 在构造函数创建时, 应该尽量使用初始化列表, 减少一个内存拷贝赋值操作, 提高性能
// 5. +=*/运算符重载, 尽量设置为 全局函数(非成员函数), 而不是内联或者外联函数, 这是为了左加右, 或者右加左, 不同类型的考量(: 侯捷)
// 6. 防御性编程 在进行相同比较的时候 应该尽量写为 0 == x; 而不是 x == 0, 防止写成 x = 0; 这样编译阶段即可发现错误
// 7. 如果一个类的成员 有指针, 那么我们需要手动实现深拷贝, 重写拷贝构造, 和拷贝赋值(先delete自己的数据, 再存放外部的数据)
// 8. 在声明变量时, 尽量使用 const 关键字修饰
// 9. 新增的 noexcept 关键字, 用来声明 码农保证 这个函数或者方法不会发生异常, 默认参数为 true, 也可以进行套娃 noexcept(noexcept) 表示在符合xx条件的时候这个函数不会异常 (对于vector 内的元素 的移动语义赋值运算符, 需要我们声明noexcept)
// 10. override 如果我们写子类的方法, 和父类中重名, 编译器不知道我们是进行重写 还是重载, 加上override 明确我们要重写帮助编译器帮我们纠错(重写方法的签名要和父类一样, 如果不一样那么就是重载了)
// 11. final 对类使用表示该类已经是最后的子类了 请不要再继承它了 对方法使用表示之类在继承时不要对添加了final的方法复写重写
// 12. decltype 我们也不知道返回值是什么类型时可以通过decltype编译时得到类型, 编译时展开代码时就可以知道了, 这个关键字其实就类似 typeof
// 类的内联函数
// stu_53();
// 类的可变与不可变 常对象/常函数 1
// stu_54();
// 实例对象互为友元 (相同的各个 class 实例互为 friends 友元)
// stu_55();
// 关于拷贝构造函数 和 拷贝赋值 调用的备注
// stu_56();
// new 底层实现, 在new 时, 会 先分配内存, 再调用构造函数
// 在delete 时, 先调用析构函数, 再释放内存
// stu_57();
// new 动态分配 所得到的内存块剖析
// stu_58();
// 单例模式
// stu_59();
// 转换函数/自动类型转换
// stu_60();
// 箭头符号/星号/智能指针初探(让一个类像指针)
// stu_61();
// 泛化特化
// stu_62();
// 偏特化/局部特化
// stu_63();
// 模板的模板参数
// stu_64();
// Variadic Templates 收集参数/数量不定模板参数 ...
// stu_65();
// auto 关键字
// stu_66();
// range-based for statement: 新的 for 循环
// stu_67();
// reference 再次探究
// stu_68();
// const 再次探究 常对象/常函数 2
// stu_69();
// nullptr 替换 NULL/0
// stu_70();
// 一致性初始化 initializer list初探
// stu_71();
// initializer list class 设置初值
// stu_72();
// =default / =delete
// stu_73();
// using Alias Template 模板的 "别名"
// stu_74();
// using Alias Type 类型的 "别名"
// stu_75();
// 9. noexcept
// stu_76();
// decltype
// stu_77();
// c++ 的三元运算
// stu_78();
// c++ 右值引用, move/"所有权"初探
// stu_79();
// move 语义的传递
// stu_80();
// 下面是一些知识汇总
// 使用自己的库 和第三方库fmt(笔记都在cmakelists文件里)
// stu_81();
// 异常处理
// stu_82();
// 函数指针
stu_83();
return 0;
}