-
auto : 自动推导类型 auto a = 10 (c++11版本)auto也是定义局部类型变量
-
: 常量: 在程序运行过程中, 值不可以修改, 2: 变量
-
全局变量提前声明, 后期必须要赋值: extern int data; cout << data <
-
cin >> num >> num1, 输入的时候以空格或者回车隔开 如果输入的数据类型错误, 会跳过并且后面一个变量的值为0
作用: 用于跳出选择结构或者循环结构
-
break使用时机:
-
出现在switch, 终止case并跳出switch. 如果在每一个调节执行后不加break, 会一次执行case下的语句
-
出现在循环循环语句中, 作用是跳出当前的循环语句
-
出现在嵌套循环中, 跳出最近的内存循环语句
-
-
continue语句: 在循环语句中, 跳出本次循环中余下尚未执行的语句, 继续执行下一次循环
-
goto语句: 可以无条件的跳转语句 goto 标记 标记一般用大写
特点:
- 数组中的每个数据元素多是相同的数据类型**
- 放在一个连续的内存空间里
- 定义数组的时候必须有初始长度
1.声明数组的几种方式:长度必须设置
int arr[5] ;生成一个长度为5的数组
int arr[5] = {1,2,3,4,5}.如果长度没填充物,会自动补充为0
int arr[] = {1,2,3,4}
2.数组的在内存中的大小
sizeof(arr)获取内存大小
数组个数:sizeof(arr)/ sizeof(arr[0])
查看数组首地址:cout<< arr<< endl//16进制的 cout<< arr<< endl:10进制的
数组的第一元素地址等于数组地址 cout<< &arr[0] << endl
数组是一个常量不能进行赋值操作
int arr[] = { 0,1,2,3,4,5};
int length = sizeof(arr) / sizeof(arr[0]);
int start = 0; int end = length - 1; int temp = 0;
for (int i = 0; i < length; i++)
{
cout << "初始数据: " << arr[i] << "t";
}
cout << endl;
while (start < end)
{
temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
start++; end--;
}
for (int i = 0; i < length; i++)
{
cout << "逆序数据: " << arr[i] << "t";
}
3.2 二位数组
二位数组的声明:
1.数据类型 数组名[行数] [列数]
2.数据类型 数组名[行数] [列数] = {{数据1}{数据2},{数据3}{数据4}}
3.数据类型 数组名[2] [2] = {{数据1},{数据2},{数据3},{数据4}}
int arr[2][2] ={1,2,3,4} ,程序自动划分
4.数据类型 数组名[ ] [2] = {{数据1},{数据2},{数据3},{数据4}}
int arr[ ][2] ={1,2,3,4} ,程序自动划分 能省略行数不能显示列数
二维数组变量名称用途
1.可以查看内存空间大小
2.可以查看二维数组的首地址
查看地址
arr:查看数组地址/首地址
arr[0]:查看第一行地址/首地址
&arr[0][0]:查看第一个元素地址
int arr[][3] =
{
{1,2,3},
{1,2,3}
};
int row = sizeof(arr) / sizeof(arr[0]);
int col = sizeof(arr[0]) / sizeof(arr[0][0]);
for (size_t i = 0; i < row; i++)
{
for (int t = 0; t < col; t++)
{
cout << arr[i][t] << "t";
}
cout << endl;
}
4. 函数
函数的实参如果是一个值: 只能改变形参的值, 不能改变实参的值
函数的实参如果是一个地址: 利用指针作, 可以改变实参的
当要使用数组作为函数形参时,因为一个数组不能使用另外一个数组初始化,也不能将一个数组赋值给另外一个数组
4.0 函数传数组实参: 数组首地址(arr), 数组长度(sizeof(arr) / sizeof(arr[0])).
形参: 指针, 数组长度(int * p,length)
语法:
返回值类型 函数名 (参数列表)
{
函数体语句
return表达式
}
int fn(int num1, int num2){
int result = num1 + num2;
return rusult;
}
fn(2,4)
如果函数不需要返回值,声明的时候可以写void
void fn(){
}
函数调用的形参会在内存中开辟一个新的空间.所以形参是修饰不了实参的
4.1函数有哪几种样式1.无参无反
2.有参无反
3.有参无反
4.有参有反
4.2函数的声明int fn(int a,int b) 函数的声明. 函数的声明可以有多次,函数的定义只有一个.函数的声明可以让函数的定义写在任意位置4.3函数的分文件编写
1.创建.h后缀名的头文件
2.创建.cpp后缀名的源文件
3.在头文件中写函数的声明
4.在源文件中写函数的定义
5. 指针指针的作用:可以通过指针间接访问内存
指针也是种数据类型.
指针大小:在32位操作系统中 (4个字节), 在64位下是(8个字节)
地址是一个16进制的数
5.1 怎么访问数据找到地址编号-->地址标号里面存了数据 指针:可以通过指针来保存地址,然后通过这个地址来寻找数据(指针就是一个地址)5.2 定义指针
定义指针的语法: 数据类型 * 指针变量§. 指针英文:point
int a = 10; int * p; //创建指针 p = &a; //指针跟变量名有联系 指针就是地址5.3 使用指针
可以通过解引用的方式来找到指针指向的内存 :*p
*p: 找到指针指向的内存中的数据.可以来修改/操作指向地址里面的数据
5.4 空指针和野指针空指针: 指针变量指向内存中编号为0的空间 内存条中编号为0的地址
用途: 初始化指针变量
注意: 空指针指向的内存是不可以访问的, 0 ~ 255之间的内存编号是系统占用的,不能访问
野指针: 指针变量指向是非法的内存空间
列子: int * p = (int *)0x1100 这个16进制的数没有被声明
NULL指针是不指向任何一个地址的指针。这样的指针一般是允许的。当一个指针为NULL的时候,不要对它进行存取。
5.4 const修饰指针const修饰指针有三种情况
-
const修饰指针 --常量指针 const int * p = &a
指针的指向可以修改, 但是指针指向的值不可以修改
*p = &a//正确 p = 20;//错误
-
const修饰常量 --指针常量 int * const p = &a
指针的指向不可以改, 指针指向的值可以改
-
const既修饰指针, 又修饰常量 const int * const p = &a
指针的指向和指针指向的值多不可被修改
作用: 利用指针访问数组的元素
5.7 指针和函数作用: 修改函数实参的值
5.8 指针/数组/函数返回函数的二维数组, 用指针接收, 不能直接用新数组
int *sort(int *arr, int length)
{
int temp = 0;
for (int i = 0; i < length -1; i++)
{
for (int t = 0; t < length - i - 1; t++)
{
if (arr[t+1] > arr[t] )
{
temp = arr[t];
arr[t] = arr[t + 1];
arr[t + 1] = temp;
}
}
}
return arr;
}
int main()
{
int arr[] = { 1,4,7,2,3,1,10,9,8,11 };
int* p = arr;
int length = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < length; i++)
{
cout << arr[i] << " ";
}
cout << endl;
int* newArr;
newArr = sort(p,length);
int newLen = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < length; i++)
{
cout << newArr[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
6. 结构体
6.1语法:
结构体创建 struct 结构体名{结构体成员列表} 结构体访问: 创建结构体对象, 通过对象.属性来访问
结构体指针: 利用->可以通过结构体指针访问结构体属性
struct Person
{
string name;
int age;
};
int main()
{
Person s1[] =
{
{ "张三",2 },
{"李四",3}
};
Person* p = s1;
int length = sizeof(s1) / sizeof(s1[0]);
for (int i = 0; i < length; i++)
{
cout << p->name << "t" << p->age << endl;
p++;
}
system("pause");
return 0;
}
值传递, 返回会创建一个新的地址 把实参的值赋值给这个新的地址, 如果是指针, 则是创建一个指针(4字节)直接访问实参的地址
为什么要用const, 为了提高效率, 而且不能让用户更改
6.2随机数: int a = rand() % (max - min + 1) + min;rand () % 3 随机到0 - 2;
7. 内存4区
-
代码区: 存放函数体的二进制代码,由操作系统进行管理(所有的中文,英文)
程序编译后, 程序运行前(代码区, 全局区).
代码区是共享(对于频繁执行的程序, 只需要在内存中有一份代码就行)
代码区是只读的(防止程序意外地修改了它的指令)
-
全局区: 存放全局变量全局变量定义在main前面), 全局常量(const)和静态变量(static)
代码区和全局区多是代码运行前
const修饰的局部变量在栈区
常量: 字符串常量, const修饰的变量(const修饰的局部变量, const修饰的全局变量)
有编译器释放
-
栈区: 由编译器自动分配释放, 存放函数的参数值, 局部变量等
注意事项: 不要返回函数局部变量的地址, 栈区开辟的数据有编译器自动释放, 函数执行完,自动销毁地址的值,不会保留内存地址的值, 形参的数据也会放在栈区, 第一次能读取(编译器自动保留一次)
-
堆区: 有程序员分配和释放, 若程序员不释放, 程序结束时有操作系统回收
利用new关键字可以在堆区开辟 new int(xx)
new返回的是该数据类型的指针 int * p = new int(10) int * p = new arr[10]
利用new在堆区开辟一个数组, 返回的是地址的首地址
手动释放堆区数据:
- 利用delete关键字delete xx
- 释放数组, 利用delete关键字delete [] xx
指针本质也是局部变量, 放在栈上, 指针保存的数据是在堆区(解引用)
new 返回的是该数据类型的指针
手动释放堆区的数据: 利用关键字delete i
手动释放堆区的数组: delete[]arr
8 引用-
引用的基本使用
作用: 给变量起别名
语法: 数据类型 &别名 = 原名
int a = 10; int & b = a; b = 20; cout << a <
int &a = b == int* const a = b
-
引用注意事项
- 引用必须要初始化, 创建的时候就要初始化, 不能只创建不初始化
- 引用在初始化后, 不可以更改. 一旦成为某一个的别名就不能在更改为其他变量的别名了
-
引用做函数参数
作用: 函数传参时, 可以利用引用的技术让形参修饰实参
优点: 可以简化指针修改实参
-
引用做函数的返回值
注意: 1. 避免返回局部变量的引用, 2. 函数的调用可以左值
如果函数的返回值是全局变量/静态 引用, 这个函数调用可以作为左值
-
引用的本质
本质: 引用的本质是一个c++指针常量 int &a = b == int* const a = b, 所以说引用不能更改(一旦初始化就不可以发生改变)
-
常量的引用
使用场景: 用来修饰形参, 防止误操作
注意点: 引用的是一个合法的内存空间, 不能直接赋值为一个自变量, 只能赋值是个变量或者常量,如果加上const就可以,因为编辑器自动帮我们开辟一个变量空间地址,相当于 const int & a = 10 编译器帮我们转换为int temp=10;
const int & a = temp,加入const只会不能修改值,只能读
-
函数的默认参数
在c++中, 函数的形参列表中的形参是可以有默认值的
如果实参有值, 则形参默认值替换为实参的值
实参传变量明而不是地址, 函数形参用引用接收
注意事项:
- 如果在形参某个位置, 那个从这个位置往右,从左到右多必须有默认值
- 如果函数声明有默认参数, 函数实现就不能有默认参数(声明和实现只能有一个默认参数)
-
函数占位参数
-
语法: 返回值类型 函数名 (数据类型){}
c++中函数的新仓列表里可以有占位参数, 用来占位, 调用函数时必须填补该位置
注意点:
-
-
函数重载
-
作用: 函数名可以相同, 提高复用性
-
函数重载满足条件
- 同一个作用域
- 函数名称相同
- 函数参数类型不同或者个数不同或者顺序不同
- 函数的返回值不可以作为函数重载的条件
-
函数重载注意事项
- 引用作为重载条件
- const既修饰指针又修饰值和 指针常量可以作为条件
- 函数重载碰到函数有默认参数, 不能作为条件
其他: 函数的形参最好使用引用, 这样它就不会再额外拷贝一份数据了
- 引用作为重载条件
-
-
函数体默认参数
在c++中, 函数的形参列表中的形参是可以有默认值得
语法: 返回值类型 函数名(参数 = 默认值){}
fn(int a, int b = 20, int c = 30){ a+b+c } int a = 10 int b = 30 fn(a, b,)//70注意事项:
1.如果某个位置已经有了默认参数, 那么从这个位置往后, 从左到右多必须有默认参数
void fn(int a, int b =20; int c) 会报错
2.如果函数声明有默认参数, 函数实现就不能有默认参数(二意化), 二者只能其中一个有默认参数
-
函数占位参数, 占位参数可以有默认值
语法:返回值类型 函数名 (数据类型){}
void fn(int , double = 10) ;
fn(10);
-
函数重载
作用: 函数名可以形同, 提高复用性
函数重载满足条件
- 同一个作用域下
- 函数名称相同
- 函数参数类型不同或者个数不同或者顺序不同
注意: 函数的返回值不可以作为函数重载的条件
函数重载注意事项
- 引用作为重载条件
- 函数重载碰到函数默认参数
void fn() { xxx; } void fn(int a) { } fn(10) -
函数重载注意事项(二意性)
-
引用作为重载条件
如果同时存在
- 常量引用的形参函数只有函数实参为常量是调用
- 普通引用的形参函数只有函数实参为变量是调用
-
函数重载碰到函数默认参数
9 类和对象
-
通过类生成对象,
-
有public(公共属性), protect(保护权限)子级可以访问, private(私有属性)
-
struct(默认权限是公共属性), class(默认权限是私有)
-
私有属性优点:
可以自己控制读写
对于写权限, 我们可以检测数据的有效性类
-
构造函数和析构函数
构造函数语法: 类名()
-
函数名与类名相同, 构造函数对自动调用, 而且只有一个
如果程序没写构造函数和析构, 编译器会自动帮我们提供 不过是个空的实现
有参数, 和重载
-
作用: 用于创建对象时, 为对象的成员属性赋值,构造函数由编译器自动调用, 无须手动调用
析构函数:~类名()
- 析造函数不能发生重载, 在对象销毁前会自动调用析构函数, 并且只有一次
- 如果程序没写构造函数, 编译器会自动帮我们提供 不过是个空的函数
- 作用:执行清理工作
-
-
构造函数的分类及调用
-
分类方式
按参数分类: 有参构造和无参构造
按类型分类: 普通构造和拷贝构造
-
三种调用方式
括号法:
注意:person p1(): 不是个构造函数, 编译器会认为它是个函数的声明
显示法:
无参构造: person p1 不能加(), 编译器会以为是函数的声明,
有参构造: person p1(10)
拷贝构造:person(const 类名 &p1)
注意点: person(xx) 是一个匿名对象, 匿名对象:当前行执行完, 系统会立即回收匿
不要利用拷贝构造函数初始化匿名对象 编译器会认为创建一个新的对象
person (p3)=== person p3`, 编译器保存重定义p3
隐式转换法
person p3 = 10 相当于: person p3 = person (10)
-
-
拷贝构造函数调用时机(只要是不直接用地址就能拷贝)
类名(const 类名&p)
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部变量对象
-
构造函数调用规则
-
默认构造函数(无参, 函数体为空)
-
默认析构函数(无参, 函数体为空)
自己为什么要写析构: 释放堆区的数据
-
默认拷贝构造函数, 对属性进行值拷贝
规则: 创建一个类, c++编译器会给每个类至少添加3个函数
- 如果用户定义有参构造函数, c++不在提供默认无参构造, 但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数, c++不会在提供其他构造函数
-
-
深浅拷贝
浅拷贝: 带来的问题是堆区的内存重复释放(对象是先进后出, 会导致先进去的释放一个空的指针)
深拷贝; 在堆区重新申请空间, 进行拷贝操作,(销毁堆区的数据, 如果浅拷贝会导致重复销毁, 只能用深拷贝再次在堆区创建一个地址)
-
类成员
先构造成员对象在构造本身, 析构相反
-
静态成员函数
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明, 内外初始化
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
- 静态函数声明:static void fn(){}
- 静态函数调用: 通过对象来访问: xxx p; p.fn(), 通过类名来访问xxx::fn()
- 注意: 静态成员函数也是有访问权限的.
- 静态变量要类内声明, 类外初始化
- 静态成员变量
-
初始化列表: 类名(int number, int number1):属性1(number), 属性2(number1);
对象成员: 这个对象是另一个对象的属性
-
静态成员
类内声明, 内外初始化
这个类静态成员属性, 所有这个类的对象共享同一个类静态成员函数(改变后 所有这个类对象的这个静态成员属性多会改变),
静态成员属性可以通过对象来访访问, 也可以通过类名访问Person::静态成员属性, 静态成员属性和函数也是有访问权限的
class Person { public: static int a; } person::a = xx;静态成员函数: 静态成员函数只能访问静态成员变量
-
对象模型和this指针
成员变量和成员函数时分开存储的, 只有非静态成员变量才属于类的对象上
在c++中, 类内的成员变量和成员函数分开存储.
只有非静态成员变量才属于类的对象上
空对象占用的内存空间为1.
c++编译器会给每个空对象也分配一个字节空间, 是为了区分空对象占内存的位置,每一个空对应多有一个独一无二的内存地址, 如果这个对象上有属性 则大小为属性的大小, 成员函数不在这个类的对象上
-
this指针
this指向被调用的成员函数所属的对象
this指针的用途:
- 当形参和成员变量同名时, 可以用this指针来区分
- 在类的非静态成员函数中返回对象对象本身, 可以使用return* this(链式思想)
创建一个类的空指针, 如果用这个指针来访问类的成员函数会报错,原因: 空指针本来就没有内容
-
提高代码健壮性
-
如果指针为空就给它返回
-
if(this == null){ return; }
-
-
const修饰成员函数
常函数
-
常函数内不可以修改成员属性
-
成员函数加const后我们称为这个函数为常函数
-
成员属性声明时加关键字mutable后, 在常函数中依然可以修改
-
语法:void fn()const{}指针常量
class Person { public: void fn() const { a = 20 // 报错,不能修改指针常量 b = 20 // 可以修改 } private: int a = 10 mutable int b = 10 }
常对象:
this指针的本质是一个指针常量
- 常对象只能调用常函数
- 声明对象前加const称该对象为常对象
- 常对象也不可以修改这个类里面的属性(除了mutable)以外
const Person p;
-
-
关键字: friend
全局函数做友元
可以访问私有属性
friend void fn1(const building& s1);
类做友元
friend class goodgay成员函数做友元:
成员函数做友元
friend void godgay::fn();
概率: 对已有的运算符重新进行定义, 赋予另一种功能 .
运算符重载也可以发生函数重载
运算符重载对于内置的数据类型的表达式的运算符是不可能改变的 (int a + )
-
重载加号: 可以利用成员函数或者全局函数重载
类名 operator+ (传入的类) == person operator+(person &p)
person operator+(person &p1, person &p2){return `person}
-
不会利用成员函数来重载左运算符(无法保证cout在左侧), 只能用全局函数来重载左运算符. cout :本质是ostream输出流
ostream& operator<<(ostream &cou, person &p) {...;return cou}
-
递增运算符
前置递增放回引用,后置递增返回值
-
赋值运算符重载
当属性有在堆区开辟的时候, 带来的问题重复删除内存空间, 得要手动实现等号赋值重载重载在堆区开辟内存
-
函数调用运算符重载
仿函数 void operator()(int num) {cout << num << endl}; person p1; p1(2); int operator()(int a, int b) {return a + b;} int a = person()(2, 3);匿名函数调用完就会释放- 函数调用运算符()也可以重载
- 由于重载后使用的方式非常像函数的调用, 因此称为仿函数(在一个类中重载了小括号)
- 仿函数没有固定写法, 非常灵活
9.3 继承
继承是面向对象三大特征之一: 可以减少重复的代码
语法: class 子类 : 继承方式 父类
子类(派生类) 父类(基类)
9.3.1 继承方式继承方式一共有三种:
父类的私有权限多不能被访问
-
公共继承
继承过来的属性权限不发生变化
-
保护继承
继承过来的父类元素在子类成员了保护权限里面的属性
-
私有继承
继承过来的父类元素在子类成员了私有权限里面的属性
- 父类中所有非静态成员属性多会被子类继承下去
- 父类中的私有成员被编译器影藏了
- cd命令:报告单个布局 cl /d1 reportSingleClassLayout类名 文件名
子类继承父类后, 当创建子类对象, 也会调用父类的构造函数
顺序: 父类构造 子类构造 子类析构 父类析构
9.3.4. 继承同名成员处理方式- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要作用域Son.Base::m_A
- 如果子类中出现和父类同名的成员函数,子类的同名成员会应影藏掉父类中所有同名成员函数, 父类的重载也一样会被影藏掉了
静态成员属性特点
- 类内声明, 类外初始化
- 编译阶段分配内存
- 所有对象共使数据
访问方式
- 通过对象访问 class p1; p1.m_A
- 通过类名来访访问class p1; p1::m_A
子类元素继承多个爹
语法: class 子类: 继承方式 父类1, 继承方式 父类2..
多继承可能会引用父类有同名成员出现, 需要加作用域区分
在实际开发中不建议用多继承
9.3.7 菱形继承(钻石继承)概念:
- 两个派生类继承同一个基类
- 又有某个类同时继承者两个派生类
当菱形继承, 需要加作用域进行区分.但是菱形继承导致数据有两份, 资源浪费
可以利用虚继承 解决菱形继承的问题: 在继承之前加上关键字virtual:
vbptr: 虚基类指针, 指向虚基类表的偏移量(继承了父类的指针, 通过这个指针通过偏移量找到唯一的数据)
菱形继承带来的问题 :
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
- 利用虚继承可以解决菱形继承问题
多态分为两大类:
- 静态多态: 函数重载和运算符重载属于静态多态, 复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数胡地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
- 利用virtual可以实现地址晚绑定
动态多态满足条件:
函数重写和函数重载的区别: 函数重写必须返回值类型也要相同
语法: virtual void fn()
-
有继承关系 2. 子类重写父类的虚函数
-
动态多态的使用
父类的指针或者引用 指向子类对象:parent *p = new son1 指针需要手动释放
空类的大小是1: 静态成员函数
动态的成员函数大小4: 指针为4个字节
重载和重写:
- 重载: 函数名,返回值类型相同.参数不同
- 重写: 函数返回值类型, 函数名, 参数列表要完全相同
vfptr: 虚函数指针/虚函数表指针 :作用是指向虚函数表(VFtable)
VFtable: 虚函数表: 作用: 表内记录虚函数的地址& parent :: fn
v - virtual 虚
f - function 函数
ptr - pointer 指针
父类有虚函数, 子类继承父类的虚函数, 当子类重写父类的虚函数, 子类中的虚函数表会替换成子类的虚函数地址, 父类的虚函数表不发生变化. 当父类的指针或者引用指向子类对象时候, 发生多态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oXSghXBX-1656318798101)(E:yunPanOneDrive桌面c++图片资源ddfc969df6953dc0e9f8e270d0c464c.png)]
由于写了个虚函数, 类的内部发生了改变,多了个指针(虚函数指针) 指向虚函数表,虚函数内部有虚函数地址,当子类重写了虚函数, 子类会把自身的虚函数表中的虚函数地址替换掉,替换成子类的虚函数地址
3. 多态的有点在真是的开发中提倡 开闭原则**: 对扩展进行开发, 对修改进行关闭**
- 代码组织结构清晰
- 可读性强
- 利益前期和后期的扩展以及维护
在多态中, 通常父类中虚函数的实现是毫无意义的, 主要多事调用子类重写的内容
因此可以将虚函数作为纯虚函数
纯虚函数语法: virtual 返回值类型 函数名(参数列表) = 0;
当类中有了纯虚函数, 这个类也称为抽象类
抽函数特点:
- 无法实例化对象(栈堆多不可以)
- 子类必须重写抽象类中的纯虚函数, 否则这个子类也属于抽象类
父类指针在析构的时候 不会调用子类中析构函数, 导致子类如果有堆区的属性会造成内存泄漏
解决方式: 将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 多需要有具体得函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构, 该类属于抽象类, 无法实例化对象
- 纯虚析构 既要声明也要实现: 原因,父类的属性也需要析构
虚析构语法: virtual ~类名(){}
纯虚析构语法: virtual ~类名() = 0, 类外还需要实现类名::~类名(){}
总结:
-
虚析构活纯虚析构就是用来解决通过父类指针释放子类对象
-
如果子类中没有堆区数据, 可以不写为虚析构活纯虚析构
-
拥有纯虚析构函数的类也属于抽象类
程序运行时产生的数据多属于临时数据, 程序一旦运行结束会被释放
通过文件可以将数据持久化
c++中对文件操作需要包含头文件
文件类型分为两种:
- 文本文件 - 文件以文本的ASCLL码形式存储在计算机中
- 二进制文件 - 文件以文本的二进制形式存储在计算机中, 用户一般不能直接读懂它们
操作文件(文件流)的三大类
- ofstream : 写操作
- ifstream : 读操作
- fstream: 读写操作
写文件步骤:
-
包含头文件
#include
-
创建流对象
ofstream ofs
-
打开文件
ofs.open(“文件路径”, 打开方式)
-
写数据
ofs <<"写入的数据";
-
关闭文件
ofs.close();
文件打开方式:
| 打开方式 | 解释 |
|---|---|
| ios:in | 为读文件而打开文件 |
| ios::out | 为写文件而打开文件 |
| ios::ate | 初始位置: 文件尾 |
| ios::app | 追加方式写文件 |
| ios::trunc | 如果文件存在先删除, 再创建 |
| ios::binary | 二进制方式 |
注意: 文件打开方式可以配合使用,利用|操作符;
例如: 用二进制方式写文件ios::binary | ios::out;
2.1 读文件读文件与写文件步骤相似, 但是读取方式相对于比较多
读文件步骤:
-
包含头文件
#include
-
创建流对象
ifstream ifs
-
打开文件并判断文件是否打开成功
ifs.open(“文件路径”, 打开方式)
-
读数据
四种方式读取
第一种
char buf[1023] = {0}; while(ifs >> buf){ cout << buf << endl; }第二种
char buf[1024] = {0}; while(ifs.getline(buf,sizeof(buf))){ cout << buf << endl; }第三种
string buf; while(getline(ifs,buf)){ cout << buf << endl; }第四种不推荐, 效率低
char c while((c = ifs.get()!= EOF)){ //EOF: end of file(文件尾部) cout << c; } -
关闭文件
ifs.closs();
总结:
- 读文件可以利用ifstream, 或者fstream类
- 利用is_open函数可以判断文件是否打开
- close 关闭文件
最好使用c语言字符串不然容易出现错误
以二进制的方式对文件进行读写操作
打开方式要指定为ios::binary
2.2.1写文件二进制方式写文件只要利用流对象调用成员函数write
函数原型: ostream & write(const char * buffer, int len);
参数解释: 字符指针buffer:指向内存中一段存储空间, len是读写的字节数
11 模板泛型编程:
- 泛型模板的主要技术是模板
- c++两种模板机制: 函数模板和类模板
特点:
- 模板不可以直接使用, 它只是个框架
- 模板的通用不是万能的
-
创建语法:
template
函数声明或定义 解释:
template 声明创建模板
typename 表面其后面的符号是一种数据类型, 可以用class代替
T 通用的数据类型, 名称可以替换, 通常为大写字母 -
使用语法
- 自动类型推导(编译器自动识别):fn(参数a,参数b)
- 显示指定类型: fn<数据类型>(参数a, 参数b)
-
作用:
模板的目的是为了提高复用性, 将类型参数化
-
函数模板注意事项:
- 自动类型推导, 必须推导出一致的数据类型T, 才可以使用
- 模板必须要确定出T的数据类型, 才可以使用
-
选择排序逻辑
选择一个临时下标,每次赋值为外层便利的数组下标, 判断这个下标的元素与数组剩下个数的元素值, 比如顺序排序,if(arr[temp] > arr[t]){交换下标},每次内层循环一次. 判断临时变量是否等于外层循环. 如果不等于. 交换值 -
普通函数与函数模板的区别
- 普通函数调用时可以发生自动类型转换(隐式转换)
- 函数模板调用时, 如果利用自动类型推导, 不会发生隐式转换
- 如果利用显示指定类型方式, 可以发生隐式转换
-
普通函数与函数模板的调用规则
- 如果函数模板和普通函数多可以实现, 优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板 fn<>();
- 函数模板也可以发生重载
- 如果函数模板可以更好的匹配, 优先调用函数模板(如果函数中存在隐式转换会调用模板函数)
-
模板的局限性
对特定的类型提供具体化操作
- 利用具体化的模板, 可以解决自定义类型的通用化 优先调用实列化的模板函数template<>bool fn(Person& s1, Person& s2)
一: 类模板和函数模板区别:
- 类模板不能用自动类型推导, 只能用显示指定类型.
- 类模板有默认参数.
- 类模板中的成员函数并不是一开始就创建的, 在调用时才创建
二: 类模板对象做函数参数
看变量的类型:typeid(变量).name()
传入方式:
最常用的是第一种
- 指定传入的类型: 直接显示对象的数据类型
- 参数模板化: 将对象中的参数变为模板进行传递
- 整个类模板化: 将这个对象类型模板化进行传递
三:类模板与继承
注意点:
- 子类继承父类模板, 必须要确定父类的数据类型
- 如果想要灵活指定父类中T的类型, 子类也需要变类模板
四: 类模板成员函数类外实现
五: 类模板分文件编写
问题: 类模板创建时机是在调用后才会创建
解决:
- 方法1: 直接包含源文件(不常用)
- 方法2: 将.h和.cpp中的内容写到一起, 将后缀名改为.hpp(类模板)文件
六: 类模板与友元
为大写字母
-
使用语法
- 自动类型推导(编译器自动识别):fn(参数a,参数b)
- 显示指定类型: fn<数据类型>(参数a, 参数b)
-
作用:
模板的目的是为了提高复用性, 将类型参数化
-
函数模板注意事项:
- 自动类型推导, 必须推导出一致的数据类型T, 才可以使用
- 模板必须要确定出T的数据类型, 才可以使用
-
选择排序逻辑
选择一个临时下标,每次赋值为外层便利的数组下标, 判断这个下标的元素与数组剩下个数的元素值, 比如顺序排序,if(arr[temp] > arr[t]){交换下标},每次内层循环一次. 判断临时变量是否等于外层循环. 如果不等于. 交换值 -
普通函数与函数模板的区别
- 普通函数调用时可以发生自动类型转换(隐式转换)
- 函数模板调用时, 如果利用自动类型推导, 不会发生隐式转换
- 如果利用显示指定类型方式, 可以发生隐式转换
-
普通函数与函数模板的调用规则
- 如果函数模板和普通函数多可以实现, 优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板 fn<>();
- 函数模板也可以发生重载
- 如果函数模板可以更好的匹配, 优先调用函数模板(如果函数中存在隐式转换会调用模板函数)
-
模板的局限性
对特定的类型提供具体化操作
- 利用具体化的模板, 可以解决自定义类型的通用化 优先调用实列化的模板函数template<>bool fn(Person& s1, Person& s2)
一: 类模板和函数模板区别:
- 类模板不能用自动类型推导, 只能用显示指定类型.
- 类模板有默认参数.
- 类模板中的成员函数并不是一开始就创建的, 在调用时才创建
二: 类模板对象做函数参数
看变量的类型:typeid(变量).name()
传入方式:
最常用的是第一种
- 指定传入的类型: 直接显示对象的数据类型
- 参数模板化: 将对象中的参数变为模板进行传递
- 整个类模板化: 将这个对象类型模板化进行传递
三:类模板与继承
注意点:
- 子类继承父类模板, 必须要确定父类的数据类型
- 如果想要灵活指定父类中T的类型, 子类也需要变类模板
四: 类模板成员函数类外实现
五: 类模板分文件编写
问题: 类模板创建时机是在调用后才会创建
解决:
- 方法1: 直接包含源文件(不常用)
- 方法2: 将.h和.cpp中的内容写到一起, 将后缀名改为.hpp(类模板)文件
六: 类模板与友元



